// 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.
*/
#define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kControl
#if defined(_WIN32)
#include "mongo/platform/basic.h"
#include
#include "mongo/util/ntservice.h"
#include "mongo/db/client.h"
#include "mongo/db/instance.h"
#include "mongo/stdx/chrono.h"
#include "mongo/stdx/future.h"
#include "mongo/stdx/thread.h"
#include "mongo/util/assert_util.h"
#include "mongo/util/exit.h"
#include "mongo/util/log.h"
#include "mongo/util/options_parser/environment.h"
#include "mongo/util/quick_exit.h"
#include "mongo/util/signal_handlers.h"
#include "mongo/util/text.h"
#include "mongo/util/winutil.h"
using std::string;
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,
const bool reinstall);
static void removeServiceOrDie(const wstring& serviceName);
bool shouldStartService() {
return _startService;
}
static DWORD WINAPI
serviceCtrl(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext);
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];
quickExit(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";
quickExit(EXIT_BADOPTIONS);
}
installService = true;
}
if (params.count("reinstall")) {
if (badOption != -1) {
log() << "--reinstall cannot be used with --" << disallowedOptions[badOption];
quickExit(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";
quickExit(EXIT_BADOPTIONS);
}
reinstallService = true;
}
if (params.count("remove")) {
if (badOption != -1) {
log() << "--remove cannot be used with --" << disallowedOptions[badOption];
quickExit(EXIT_BADOPTIONS);
}
removeService = true;
}
if (params.count("service")) {
if (badOption != -1) {
log() << "--service cannot be used with --" << disallowedOptions[badOption];
quickExit(EXIT_BADOPTIONS);
}
_startService = true;
}
if (params.count("processManagement.windowsService.serviceName")) {
if (badOption != -1) {
log() << "--serviceName cannot be used with --" << disallowedOptions[badOption];
quickExit(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];
quickExit(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];
quickExit(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];
quickExit(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];
quickExit(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,
reinstallService);
quickExit(EXIT_CLEAN);
} else if (removeService) {
removeServiceOrDie(_serviceName);
quickExit(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,
const bool reinstall) {
log() << "Trying to install Windows service '" << toUtf8String(serviceName) << "'";
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);
quickExit(EXIT_NTSERVICE_ERROR);
}
SC_HANDLE schService = NULL;
int retryCount = 10;
while (true) {
// 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.
schService = ::OpenService(schSCManager, serviceName.c_str(), SERVICE_ALL_ACCESS);
if (schService != NULL) {
log() << "There is already a service named '" << toUtf8String(serviceName)
<< (retryCount > 0 ? "', sleeping and retrying" : "', aborting");
::CloseServiceHandle(schService);
// If we are reinstalling the service, but SCM thinks it is installed, then wait
// and try again
if (--retryCount > 0 && reinstall) {
sleepmillis(500);
continue;
}
::CloseServiceHandle(schSCManager);
quickExit(EXIT_NTSERVICE_ERROR);
} else {
break;
}
}
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);
::CloseServiceHandle(schSCManager);
quickExit(EXIT_NTSERVICE_ERROR);
}
log() << "Service '" << toUtf8String(serviceName) << "' (" << toUtf8String(displayName)
<< ") installed with command line '" << commandLine << "'";
string typeableName((serviceName.find(L' ') != wstring::npos)
? "\"" + toUtf8String(serviceName) + "\""
: toUtf8String(serviceName));
log() << "Service can be started from the command line with 'net start " << typeableName << "'";
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);
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";
}
}
// 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.";
}
// Set the pre-shutdown notification with a timeout of 10 minutes.
// Windows will either wait for us to finish with SERVICE_STOPPED or it will timeout, whichever
// is first.
SERVICE_PRESHUTDOWN_INFO servicePreshutdownInfo;
servicePreshutdownInfo.dwPreshutdownTimeout = 10 * 60 * 1000; // 10 minutes
BOOL ret = ::ChangeServiceConfig2(
schService, SERVICE_CONFIG_PRESHUTDOWN_INFO, &servicePreshutdownInfo);
if (!ret) {
DWORD gle = ::GetLastError();
error() << "Failed to set timeout for pre-shutdown notification with error: "
<< errnoWithDescription(gle);
serviceInstalled = false;
}
::CloseServiceHandle(schService);
::CloseServiceHandle(schSCManager);
if (!serviceInstalled)
quickExit(EXIT_NTSERVICE_ERROR);
}
void removeServiceOrDie(const wstring& serviceName) {
log() << "Trying to remove Windows service '" << toUtf8String(serviceName) << "'";
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);
quickExit(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";
::CloseServiceHandle(schSCManager);
quickExit(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";
while (::QueryServiceStatus(schService, &serviceStatus)) {
if (serviceStatus.dwCurrentState == SERVICE_STOP_PENDING) {
Sleep(1000);
} else {
break;
}
}
log() << "Service '" << toUtf8String(serviceName) << "' stopped";
}
bool serviceRemoved = ::DeleteService(schService);
::CloseServiceHandle(schService);
::CloseServiceHandle(schSCManager);
if (serviceRemoved) {
log() << "Service '" << toUtf8String(serviceName) << "' removed";
} else {
log() << "Failed to remove service '" << toUtf8String(serviceName) << "'";
}
if (!serviceRemoved)
quickExit(EXIT_NTSERVICE_ERROR);
}
bool reportStatus(DWORD reportState, DWORD waitHint, DWORD exitCode) {
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_PRESHUTDOWN;
break;
}
ssStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
ssStatus.dwServiceSpecificExitCode = exitCode;
ssStatus.dwControlsAccepted = dwControlsAccepted;
ssStatus.dwCurrentState = reportState;
// Only report ERROR_SERVICE_SPECIFIC_ERROR when the exit is not clean
if (reportState == SERVICE_STOPPED && exitCode != EXIT_CLEAN)
ssStatus.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR;
else
ssStatus.dwWin32ExitCode = NO_ERROR;
ssStatus.dwWaitHint = waitHint;
ssStatus.dwCheckPoint =
(reportState == SERVICE_RUNNING || reportState == SERVICE_STOPPED) ? 0 : checkPoint++;
return SetServiceStatus(_statusHandle, &ssStatus);
}
// Minimum of time we tell Windows to wait before we are guilty of a hung shutdown
const int kStopWaitHintMillis = 30000;
// Run exitCleanly on a separate thread so we can report progress to Windows
// Note: Windows may still kill us for taking too long,
// On client OSes, SERVICE_CONTROL_SHUTDOWN has a 5 second timeout configured in
// HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control
static void serviceStop() {
// VS2013 Doesn't support future, so fake it with a bool.
stdx::packaged_task exitCleanlyTask([] {
Client::initThread("serviceStopWorker");
// Stop the process
// TODO: SERVER-5703, separate the "cleanup for shutdown" functionality from
// the "terminate process" functionality in exitCleanly.
exitCleanly(EXIT_WINDOWS_SERVICE_STOP);
return true;
});
stdx::future exitedCleanly = exitCleanlyTask.get_future();
// Launch the packaged task in a thread. We needn't ever join it,
// so it doesn't even need a name.
stdx::thread(std::move(exitCleanlyTask)).detach();
const auto timeout = Milliseconds(kStopWaitHintMillis / 2);
// We periodically check if we are done exiting by polling at half of each wait interval
while (exitedCleanly.wait_for(timeout) != stdx::future_status::ready) {
reportStatus(SERVICE_STOP_PENDING, kStopWaitHintMillis);
log() << "Service Stop is waiting for storage engine to finish shutdown";
}
}
static void WINAPI initService(DWORD argc, LPTSTR* argv) {
_statusHandle = RegisterServiceCtrlHandlerEx(_serviceName.c_str(), serviceCtrl, NULL);
if (!_statusHandle)
return;
reportStatus(SERVICE_START_PENDING, 1000);
ExitCode exitCode = _serviceCallback();
// During clean shutdown, ie NT SCM signals us, _serviceCallback returns here
// as part of the listener loop terminating.
// exitCleanly is supposed to return. If it blocks, some other thread must be exiting.
//
serviceStop();
reportStatus(SERVICE_STOPPED, 0, exitCode);
}
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");
reportStatus(SERVICE_STOP_PENDING, kStopWaitHintMillis);
// Note: This triggers _serviceCallback, ie ServiceMain,
// to stop by setting inShutdown() == true
shutdownNoTerminate();
// Note: we will report exit status in initService
}
static DWORD WINAPI
serviceCtrl(DWORD dwControl, DWORD dwEventType, LPVOID lpEventData, LPVOID lpContext) {
switch (dwControl) {
case SERVICE_CONTROL_INTERROGATE:
// Return NO_ERROR per MSDN even though we do nothing for this control code.
return NO_ERROR;
case SERVICE_CONTROL_STOP:
serviceShutdown("SERVICE_CONTROL_STOP");
// Return NO_ERROR since we handle the STOP
return NO_ERROR;
case SERVICE_CONTROL_PRESHUTDOWN:
serviceShutdown("SERVICE_CONTROL_PRESHUTDOWN");
// Return NO_ERROR since we handle the PRESHUTDOWN
return NO_ERROR;
}
// Return ERROR_CALL_NOT_IMPLEMENTED as the default
return ERROR_CALL_NOT_IMPLEMENTED;
}
void startService() {
fassert(16454, _startService);
// Remove the Control-C handler so that we properly process SERVICE_CONTROL_SHUTDOWN
// via the service handler instead of CTRL_SHUTDOWN_EVENT via the Control-C Handler
removeControlCHandler();
SERVICE_TABLE_ENTRYW dispTable[] = {
{const_cast(_serviceName.c_str()), (LPSERVICE_MAIN_FUNCTION)initService},
{NULL, NULL}};
log() << "Trying to start Windows service '" << toUtf8String(_serviceName) << "'";
if (StartServiceCtrlDispatcherW(dispTable)) {
quickExit(EXIT_CLEAN);
} else {
::exit(EXIT_NTSERVICE_ERROR);
}
}
} // namspace ntservice
} // namespace mongo
#endif