// ntservice.cpp
/* Copyright 2009 10gen Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*
* As a special exception, the copyright holders give permission to link the
* code of portions of this program with the OpenSSL library under certain
* conditions as described in each individual source file and distribute
* linked combinations including the program with the OpenSSL library. You
* must comply with the GNU Affero General Public License in all respects
* for all of the code used other than as permitted herein. If you modify
* file(s) with this exception, you may extend this exception to your
* version of the file(s), but you are not obligated to do so. If you do not
* wish to do so, delete this exception statement from your version. If you
* delete this exception statement from all source files in the program,
* then also delete it in the license file.
*/
#if defined(_WIN32)
#include "mongo/pch.h"
#include "mongo/util/ntservice.h"
#include "mongo/db/client.h"
#include "mongo/db/instance.h"
#include "mongo/util/assert_util.h"
#include "mongo/util/options_parser/environment.h"
#include "mongo/util/text.h"
#include "mongo/util/winutil.h"
using std::wstring;
namespace mongo {
namespace ntservice {
namespace {
bool _startService = false;
SERVICE_STATUS_HANDLE _statusHandle = NULL;
wstring _serviceName;
ServiceCallback _serviceCallback = NULL;
} // namespace
static void installServiceOrDie(
const wstring& serviceName,
const wstring& displayName,
const wstring& serviceDesc,
const wstring& serviceUser,
const wstring& servicePassword,
const std::vector& argv);
static void removeServiceOrDie(const wstring& serviceName);
bool shouldStartService() {
return _startService;
}
static void WINAPI serviceCtrl(DWORD ctrlCode);
void configureService(
ServiceCallback serviceCallback,
const moe::Environment& params,
const NtServiceDefaultStrings& defaultStrings,
const std::vector& disallowedOptions,
const std::vector& argv
) {
bool installService = false;
bool removeService = false;
bool reinstallService = false;
_serviceCallback = serviceCallback;
int badOption = -1;
for (size_t i = 0; i < disallowedOptions.size(); ++i) {
if (params.count(disallowedOptions[i])) {
badOption = i;
break;
}
}
_serviceName = defaultStrings.serviceName;
wstring windowsServiceDisplayName( defaultStrings.displayName );
wstring windowsServiceDescription( defaultStrings.serviceDescription );
wstring windowsServiceUser;
wstring windowsServicePassword;
if (params.count("install")) {
if ( badOption != -1 ) {
log() << "--install cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
if ( !params.count("systemLog.destination") ||
params["systemLog.destination"].as() != "file" ) {
log() << "--install has to be used with a log file for server output" << endl;
::_exit( EXIT_BADOPTIONS );
}
installService = true;
}
if (params.count("reinstall")) {
if ( badOption != -1 ) {
log() << "--reinstall cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
if ( !params.count("systemLog.destination") ||
params["systemLog.destination"].as() != "file" ) {
log() << "--reinstall has to be used with a log file for server output" << endl;
::_exit( EXIT_BADOPTIONS );
}
reinstallService = true;
}
if (params.count("remove")) {
if ( badOption != -1 ) {
log() << "--remove cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
removeService = true;
}
if (params.count("service")) {
if ( badOption != -1 ) {
log() << "--service cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
_startService = true;
}
if (params.count("processManagement.windowsService.serviceName")) {
if ( badOption != -1 ) {
log() << "--serviceName cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
_serviceName = toWideString(
params[ "processManagement.windowsService.serviceName" ].as().c_str() );
}
if (params.count("processManagement.windowsService.displayName")) {
if ( badOption != -1 ) {
log() << "--serviceDisplayName cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
windowsServiceDisplayName = toWideString(
params[ "processManagement.windowsService.displayName" ].as().c_str() );
}
if (params.count("processManagement.windowsService.description")) {
if ( badOption != -1 ) {
log() << "--serviceDescription cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
windowsServiceDescription = toWideString(
params[ "processManagement.windowsService.description" ].as().c_str() );
}
if (params.count("processManagement.windowsService.serviceUser")) {
if ( badOption != -1 ) {
log() << "--serviceUser cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
windowsServiceUser = toWideString(
params[ "processManagement.windowsService.serviceUser" ].as().c_str() );
}
if (params.count("processManagement.windowsService.servicePassword")) {
if ( badOption != -1 ) {
log() << "--servicePassword cannot be used with --" << disallowedOptions[badOption] << endl;
::_exit( EXIT_BADOPTIONS );
}
windowsServicePassword = toWideString(
params[ "processManagement.windowsService.servicePassword"
].as().c_str() );
}
if ( installService || reinstallService ) {
if ( reinstallService ) {
removeServiceOrDie(_serviceName);
}
installServiceOrDie(
_serviceName,
windowsServiceDisplayName,
windowsServiceDescription,
windowsServiceUser,
windowsServicePassword,
argv);
::_exit(EXIT_CLEAN);
}
else if ( removeService ) {
removeServiceOrDie(_serviceName);
::_exit( EXIT_CLEAN );
}
}
// This implementation assumes that inputArgv was a valid argv to mongod. That is, it assumes
// that options that take arguments received them, and options that do not take arguments did
// not.
std::vector constructServiceArgv(const std::vector& inputArgv) {
static const char*const optionsWithoutArgumentsToStrip[] = {
"-install", "--install",
"-reinstall", "--reinstall",
"-service", "--service"
};
// Pointer to just past the end of optionsWithoutArgumentsToStrip, for use as an "end"
// iterator.
static const char*const *const optionsWithoutArgumentsToStripEnd =
optionsWithoutArgumentsToStrip + boost::size(optionsWithoutArgumentsToStrip);
static const char*const optionsWithArgumentsToStrip[] = {
"-serviceName", "--serviceName",
"-serviceUser", "--serviceUser",
"-servicePassword", "--servicePassword",
"-serviceDescription", "--serviceDescription",
"-serviceDisplayName", "--serviceDisplayName"
};
// Pointer to just past the end of optionsWithArgumentsToStrip, for use as an "end"
// iterator.
static const char*const *const optionsWithArgumentsToStripEnd =
optionsWithArgumentsToStrip + boost::size(optionsWithArgumentsToStrip);
std::vector result;
for (std::vector::const_iterator iter = inputArgv.begin(),
end = inputArgv.end(); iter != end; ++iter) {
if (optionsWithoutArgumentsToStripEnd != std::find(optionsWithoutArgumentsToStrip,
optionsWithoutArgumentsToStripEnd,
*iter)) {
// The current element of inputArgv is an option that we wish to strip, that takes
// no arguments. Skip adding it to "result".
continue;
}
std::string name;
std::string value;
bool foundEqualSign = mongoutils::str::splitOn(*iter, '=', name, value);
if (!foundEqualSign)
name = *iter;
if (optionsWithArgumentsToStripEnd != std::find(optionsWithArgumentsToStrip,
optionsWithArgumentsToStripEnd,
name)) {
// The current element, and maybe the next one, form an option and its argument.
// Skip adding them to "result".
if (!foundEqualSign) {
// The next argv value must be the argument to the parameter, so strip it.
++iter;
}
continue;
}
result.push_back(*iter);
}
result.push_back("--service"); // Service command lines all contain "--service".
return result;
}
void installServiceOrDie(
const wstring& serviceName,
const wstring& displayName,
const wstring& serviceDesc,
const wstring& serviceUser,
const wstring& servicePassword,
const std::vector& argv
) {
log() << "Trying to install Windows service '" << toUtf8String(serviceName) << "'" << endl;
std::vector serviceArgv = constructServiceArgv(argv);
char exePath[1024];
GetModuleFileNameA( NULL, exePath, sizeof exePath );
serviceArgv.at(0) = exePath;
std::string commandLine = constructUtf8WindowsCommandLine(serviceArgv);
SC_HANDLE schSCManager = ::OpenSCManager( NULL, NULL, SC_MANAGER_ALL_ACCESS );
if ( schSCManager == NULL ) {
DWORD err = ::GetLastError();
log() << "Error connecting to the Service Control Manager: " << GetWinErrMsg(err) << endl;
::_exit(EXIT_NTSERVICE_ERROR);
}
// Make sure service doesn't already exist.
// TODO: Check to see if service is in "Deleting" status, suggest the user close down Services MMC snap-ins.
SC_HANDLE schService = ::OpenService( schSCManager, serviceName.c_str(), SERVICE_ALL_ACCESS );
if ( schService != NULL ) {
log() << "There is already a service named '" << toUtf8String(serviceName) << "', aborting" << endl;
::CloseServiceHandle( schService );
::CloseServiceHandle( schSCManager );
::_exit(EXIT_NTSERVICE_ERROR);
}
std::wstring commandLineWide = toWideString(commandLine.c_str());
// create new service
schService = ::CreateServiceW(
schSCManager, // Service Control Manager handle
serviceName.c_str(), // service name
displayName.c_str(), // service display name
SERVICE_ALL_ACCESS, // desired access
SERVICE_WIN32_OWN_PROCESS, // service type
SERVICE_AUTO_START, // start type
SERVICE_ERROR_NORMAL, // error control
commandLineWide.c_str(), // command line
NULL, // load order group
NULL, // tag id
L"\0\0", // dependencies
NULL, // user account
NULL ); // user account password
if ( schService == NULL ) {
DWORD err = ::GetLastError();
log() << "Error creating service: " << GetWinErrMsg(err) << endl;
::CloseServiceHandle( schSCManager );
::_exit( EXIT_NTSERVICE_ERROR );
}
log() << "Service '" << toUtf8String(serviceName) << "' (" << toUtf8String(displayName) <<
") installed with command line '" << commandLine << "'" << endl;
string typeableName( ( serviceName.find(L' ') != wstring::npos ) ?
"\"" + toUtf8String(serviceName) + "\"" :
toUtf8String(serviceName) );
log() << "Service can be started from the command line with 'net start " << typeableName << "'" << endl;
bool serviceInstalled;
// TODO: If necessary grant user "Login as a Service" permission.
if ( !serviceUser.empty() ) {
wstring actualServiceUser;
if ( serviceUser.find(L"\\") == string::npos ) {
actualServiceUser = L".\\" + serviceUser;
}
else {
actualServiceUser = serviceUser;
}
log() << "Setting service login credentials for user: " << toUtf8String(actualServiceUser) << endl;
serviceInstalled = ::ChangeServiceConfig(
schService, // service handle
SERVICE_NO_CHANGE, // service type
SERVICE_NO_CHANGE, // start type
SERVICE_NO_CHANGE, // error control
NULL, // path
NULL, // load order group
NULL, // tag id
NULL, // dependencies
actualServiceUser.c_str(), // user account
servicePassword.c_str(), // user account password
NULL ); // service display name
if ( !serviceInstalled ) {
log() << "Setting service login failed, service has 'LocalService' permissions" << endl;
}
}
// set the service description
SERVICE_DESCRIPTION serviceDescription;
serviceDescription.lpDescription = (LPTSTR)serviceDesc.c_str();
serviceInstalled = ::ChangeServiceConfig2( schService, SERVICE_CONFIG_DESCRIPTION, &serviceDescription );
#if 1
if ( ! serviceInstalled ) {
#else
// This code sets the mongod service to auto-restart, forever.
// This might be a fine thing to do except that when mongod or Windows has a crash, the mongo.lock
// file is still around, so any attempt at a restart will immediately fail. With auto-restart, we
// go into a loop, crashing and restarting, crashing and restarting, until someone comes in and
// disables the service or deletes the mongod.lock file.
//
// I'm leaving the old code here for now in case we solve this and are able to turn SC_ACTION_RESTART
// back on.
//
if ( serviceInstalled ) {
SC_ACTION aActions[ 3 ] = { { SC_ACTION_RESTART, 0 }, { SC_ACTION_RESTART, 0 }, { SC_ACTION_RESTART, 0 } };
SERVICE_FAILURE_ACTIONS serviceFailure;
ZeroMemory( &serviceFailure, sizeof( SERVICE_FAILURE_ACTIONS ) );
serviceFailure.cActions = 3;
serviceFailure.lpsaActions = aActions;
// set service recovery options
serviceInstalled = ::ChangeServiceConfig2( schService, SERVICE_CONFIG_FAILURE_ACTIONS, &serviceFailure );
}
else {
#endif
log() << "Could not set service description. Check the Windows Event Log for more details." << endl;
}
::CloseServiceHandle( schService );
::CloseServiceHandle( schSCManager );
if (!serviceInstalled)
::_exit( EXIT_NTSERVICE_ERROR );
}
void removeServiceOrDie(const wstring& serviceName) {
log() << "Trying to remove Windows service '" << toUtf8String(serviceName) << "'" << endl;
SC_HANDLE schSCManager = ::OpenSCManager( NULL, NULL, SC_MANAGER_ALL_ACCESS );
if ( schSCManager == NULL ) {
DWORD err = ::GetLastError();
log() << "Error connecting to the Service Control Manager: " << GetWinErrMsg(err) << endl;
::_exit(EXIT_NTSERVICE_ERROR);
}
SC_HANDLE schService = ::OpenService( schSCManager, serviceName.c_str(), SERVICE_ALL_ACCESS );
if ( schService == NULL ) {
log() << "Could not find a service named '" << toUtf8String(serviceName) << "' to remove" << endl;
::CloseServiceHandle( schSCManager );
::_exit(EXIT_NTSERVICE_ERROR);
}
SERVICE_STATUS serviceStatus;
// stop service if its running
if ( ::ControlService( schService, SERVICE_CONTROL_STOP, &serviceStatus ) ) {
log() << "Service " << toUtf8String(serviceName) << " is currently running, stopping service" << endl;
while ( ::QueryServiceStatus( schService, &serviceStatus ) ) {
if ( serviceStatus.dwCurrentState == SERVICE_STOP_PENDING ) {
Sleep( 1000 );
}
else { break; }
}
log() << "Service '" << toUtf8String(serviceName) << "' stopped" << endl;
}
bool serviceRemoved = ::DeleteService( schService );
::CloseServiceHandle( schService );
::CloseServiceHandle( schSCManager );
if (serviceRemoved) {
log() << "Service '" << toUtf8String(serviceName) << "' removed" << endl;
}
else {
log() << "Failed to remove service '" << toUtf8String(serviceName) << "'" << endl;
}
if (!serviceRemoved)
::_exit(EXIT_NTSERVICE_ERROR);
}
bool reportStatus(DWORD reportState, DWORD waitHint) {
if ( _statusHandle == NULL )
return false;
static DWORD checkPoint = 1;
SERVICE_STATUS ssStatus;
DWORD dwControlsAccepted;
switch ( reportState ) {
case SERVICE_START_PENDING:
case SERVICE_STOP_PENDING:
case SERVICE_STOPPED:
dwControlsAccepted = 0;
break;
default:
dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN;
break;
}
ssStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
ssStatus.dwServiceSpecificExitCode = 0;
ssStatus.dwControlsAccepted = dwControlsAccepted;
ssStatus.dwCurrentState = reportState;
ssStatus.dwWin32ExitCode = NO_ERROR;
ssStatus.dwWaitHint = waitHint;
ssStatus.dwCheckPoint = ( reportState == SERVICE_RUNNING || reportState == SERVICE_STOPPED ) ? 0 : checkPoint++;
return SetServiceStatus( _statusHandle, &ssStatus );
}
static void WINAPI initService( DWORD argc, LPTSTR *argv ) {
_statusHandle = RegisterServiceCtrlHandler( _serviceName.c_str(), serviceCtrl );
if ( !_statusHandle )
return;
reportStatus( SERVICE_START_PENDING, 1000 );
_serviceCallback();
reportStatus( SERVICE_STOPPED );
::_exit( EXIT_CLEAN );
}
static void serviceShutdown( const char* controlCodeName ) {
Client::initThread( "serviceShutdown" );
log() << "got " << controlCodeName << " request from Windows Service Control Manager, " <<
( inShutdown() ? "already in shutdown" : "will terminate after current cmd ends" ) << endl;
reportStatus( SERVICE_STOP_PENDING );
if ( ! inShutdown() ) {
// TODO: SERVER-5703, separate the "cleanup for shutdown" functionality from
// the "terminate process" functionality in exitCleanly.
exitCleanly( EXIT_WINDOWS_SERVICE_STOP );
reportStatus( SERVICE_STOPPED );
}
}
static void WINAPI serviceCtrl( DWORD ctrlCode ) {
switch ( ctrlCode ) {
case SERVICE_CONTROL_STOP:
serviceShutdown( "SERVICE_CONTROL_STOP" );
break;
case SERVICE_CONTROL_SHUTDOWN:
serviceShutdown( "SERVICE_CONTROL_SHUTDOWN" );
break;
}
}
void startService() {
fassert(16454, _startService);
SERVICE_TABLE_ENTRYW dispTable[] = {
{ const_cast(_serviceName.c_str()), (LPSERVICE_MAIN_FUNCTION)initService },
{ NULL, NULL }
};
log() << "Trying to start Windows service '" << toUtf8String(_serviceName) << "'" << endl;
if (StartServiceCtrlDispatcherW(dispTable)) {
::_exit(EXIT_CLEAN);
}
else {
::exit(EXIT_NTSERVICE_ERROR);
}
}
} // namspace ntservice
} // namespace mongo
#endif