gecko-dev/toolkit/components/maintenanceservice/maintenanceservice.cpp

392 lines
12 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include <windows.h>
#include <shlwapi.h>
#include <stdio.h>
#include <wchar.h>
#include <shlobj.h>
#include "serviceinstall.h"
#include "maintenanceservice.h"
#include "servicebase.h"
#include "workmonitor.h"
#include "uachelper.h"
#include "updatehelper.h"
#include "registrycertificates.h"
// Link w/ subsystem window so we don't get a console when executing
// this binary through the installer.
#pragma comment(linker, "/SUBSYSTEM:windows")
SERVICE_STATUS gSvcStatus = { 0 };
SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr;
HANDLE gWorkDoneEvent = nullptr;
HANDLE gThread = nullptr;
bool gServiceControlStopping = false;
// logs are pretty small, about 20 lines, so 10 seems reasonable.
#define LOGS_TO_KEEP 10
BOOL GetLogDirectoryPath(WCHAR *path);
int
wmain(int argc, WCHAR **argv)
{
// If command-line parameter is "install", install the service
// or upgrade if already installed
// If command line parameter is "forceinstall", install the service
// even if it is older than what is already installed.
// If command-line parameter is "upgrade", upgrade the service
// but do not install it if it is not already installed.
// If command line parameter is "uninstall", uninstall the service.
// Otherwise, the service is probably being started by the SCM.
bool forceInstall = !lstrcmpi(argv[1], L"forceinstall");
if (!lstrcmpi(argv[1], L"install") || forceInstall) {
WCHAR updatePath[MAX_PATH + 1];
if (GetLogDirectoryPath(updatePath)) {
LogInit(updatePath, L"maintenanceservice-install.log");
}
SvcInstallAction action = InstallSvc;
if (forceInstall) {
action = ForceInstallSvc;
LOG(("Installing service with force specified..."));
} else {
LOG(("Installing service..."));
}
bool ret = SvcInstall(action);
if (!ret) {
LOG_WARN(("Could not install service. (%d)", GetLastError()));
LogFinish();
return 1;
}
LOG(("The service was installed successfully"));
LogFinish();
return 0;
}
if (!lstrcmpi(argv[1], L"upgrade")) {
WCHAR updatePath[MAX_PATH + 1];
if (GetLogDirectoryPath(updatePath)) {
LogInit(updatePath, L"maintenanceservice-install.log");
}
LOG(("Upgrading service if installed..."));
if (!SvcInstall(UpgradeSvc)) {
LOG_WARN(("Could not upgrade service. (%d)", GetLastError()));
LogFinish();
return 1;
}
LOG(("The service was upgraded successfully"));
LogFinish();
return 0;
}
if (!lstrcmpi(argv[1], L"uninstall")) {
WCHAR updatePath[MAX_PATH + 1];
if (GetLogDirectoryPath(updatePath)) {
LogInit(updatePath, L"maintenanceservice-uninstall.log");
}
LOG(("Uninstalling service..."));
if (!SvcUninstall()) {
LOG_WARN(("Could not uninstall service. (%d)", GetLastError()));
LogFinish();
return 1;
}
LOG(("The service was uninstalled successfully"));
LogFinish();
return 0;
}
if (!lstrcmpi(argv[1], L"check-cert") && argc > 2) {
return DoesBinaryMatchAllowedCertificates(argv[2], argv[3], FALSE) ? 0 : 1;
}
SERVICE_TABLE_ENTRYW DispatchTable[] = {
{ SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain },
{ nullptr, nullptr }
};
// This call returns when the service has stopped.
// The process should simply terminate when the call returns.
if (!StartServiceCtrlDispatcherW(DispatchTable)) {
LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError()));
}
return 0;
}
/**
* Obtains the base path where logs should be stored
*
* @param path The out buffer for the backup log path of size MAX_PATH + 1
* @return TRUE if successful.
*/
BOOL
GetLogDirectoryPath(WCHAR *path)
{
if (!GetModuleFileNameW(nullptr, path, MAX_PATH)) {
return FALSE;
}
if (!PathRemoveFileSpecW(path)) {
return FALSE;
}
if (!PathAppendSafe(path, L"logs")) {
return FALSE;
}
CreateDirectoryW(path, nullptr);
return TRUE;
}
/**
* Calculated a backup path based on the log number.
*
* @param path The out buffer to store the log path of size MAX_PATH + 1
* @param basePath The base directory where the calculated path should go
* @param logNumber The log number, 0 == updater.log
* @return TRUE if successful.
*/
BOOL
GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber)
{
WCHAR logName[64] = { L'\0' };
wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1);
if (logNumber <= 0) {
swprintf(logName, sizeof(logName) / sizeof(logName[0]),
L"maintenanceservice.log");
} else {
swprintf(logName, sizeof(logName) / sizeof(logName[0]),
L"maintenanceservice-%d.log", logNumber);
}
return PathAppendSafe(path, logName);
}
/**
* Moves the old log files out of the way before a new one is written.
* If you for example keep 3 logs, then this function will do:
* updater2.log -> updater3.log
* updater1.log -> updater2.log
* updater.log -> updater1.log
* Which clears room for a new updater.log in the basePath directory
*
* @param basePath The base directory path where log files are stored
* @param numLogsToKeep The number of logs to keep
*/
void
BackupOldLogs(LPCWSTR basePath, int numLogsToKeep)
{
WCHAR oldPath[MAX_PATH + 1];
WCHAR newPath[MAX_PATH + 1];
for (int i = numLogsToKeep; i >= 1; i--) {
if (!GetBackupLogPath(oldPath, basePath, i -1)) {
continue;
}
if (!GetBackupLogPath(newPath, basePath, i)) {
continue;
}
if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING)) {
continue;
}
}
}
/**
* Ensures the service is shutdown once all work is complete.
* There is an issue on XP SP2 and below where the service can hang
* in a stop pending state even though the SCM is notified of a stopped
* state. Control *should* be returned to StartServiceCtrlDispatcher from the
* call to SetServiceStatus on a stopped state in the wmain thread.
* Sometimes this is not the case though. This thread will terminate the process
* if it has been 5 seconds after all work is done and the process is still not
* terminated. This thread is only started once a stopped state was sent to the
* SCM. The stop pending hang can be reproduced intermittently even if you set
* a stopped state dirctly and never set a stop pending state. It is safe to
* forcefully terminate the process ourselves since all work is done once we
* start this thread.
*/
DWORD WINAPI
EnsureProcessTerminatedThread(LPVOID)
{
Sleep(5000);
exit(0);
return 0;
}
void
StartTerminationThread()
{
// If the process does not self terminate like it should, this thread
// will terminate the process after 5 seconds.
HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread,
nullptr, 0, nullptr);
if (thread) {
CloseHandle(thread);
}
}
/**
* Main entry point when running as a service.
*/
void WINAPI
SvcMain(DWORD argc, LPWSTR *argv)
{
// Setup logging, and backup the old logs
WCHAR updatePath[MAX_PATH + 1];
if (GetLogDirectoryPath(updatePath)) {
BackupOldLogs(updatePath, LOGS_TO_KEEP);
LogInit(updatePath, L"maintenanceservice.log");
}
// Disable every privilege we don't need. Processes started using
// CreateProcess will use the same token as this process.
UACHelper::DisablePrivileges(nullptr);
// Register the handler function for the service
gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler);
if (!gSvcStatusHandle) {
LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError()));
ExecuteServiceCommand(argc, argv);
LogFinish();
exit(1);
}
// These values will be re-used later in calls involving gSvcStatus
gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
gSvcStatus.dwServiceSpecificExitCode = 0;
// Report initial status to the SCM
ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
// This event will be used to tell the SvcCtrlHandler when the work is
// done for when a stop comamnd is manually issued.
gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr);
if (!gWorkDoneEvent) {
ReportSvcStatus(SERVICE_STOPPED, 1, 0);
StartTerminationThread();
return;
}
// Initialization complete and we're about to start working on
// the actual command. Report the service state as running to the SCM.
ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
// The service command was executed, stop logging and set an event
// to indicate the work is done in case someone is waiting on a
// service stop operation.
ExecuteServiceCommand(argc, argv);
LogFinish();
SetEvent(gWorkDoneEvent);
// If we aren't already in a stopping state then tell the SCM we're stopped
// now. If we are already in a stopping state then the SERVICE_STOPPED state
// will be set by the SvcCtrlHandler.
if (!gServiceControlStopping) {
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
StartTerminationThread();
}
}
/**
* Sets the current service status and reports it to the SCM.
*
* @param currentState The current state (see SERVICE_STATUS)
* @param exitCode The system error code
* @param waitHint Estimated time for pending operation in milliseconds
*/
void
ReportSvcStatus(DWORD currentState,
DWORD exitCode,
DWORD waitHint)
{
static DWORD dwCheckPoint = 1;
gSvcStatus.dwCurrentState = currentState;
gSvcStatus.dwWin32ExitCode = exitCode;
gSvcStatus.dwWaitHint = waitHint;
if (SERVICE_START_PENDING == currentState ||
SERVICE_STOP_PENDING == currentState) {
gSvcStatus.dwControlsAccepted = 0;
} else {
gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP |
SERVICE_ACCEPT_SHUTDOWN;
}
if ((SERVICE_RUNNING == currentState) ||
(SERVICE_STOPPED == currentState)) {
gSvcStatus.dwCheckPoint = 0;
} else {
gSvcStatus.dwCheckPoint = dwCheckPoint++;
}
// Report the status of the service to the SCM.
SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
}
/**
* Since the SvcCtrlHandler should only spend at most 30 seconds before
* returning, this function does the service stop work for the SvcCtrlHandler.
*/
DWORD WINAPI
StopServiceAndWaitForCommandThread(LPVOID)
{
do {
ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
} while(WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT);
CloseHandle(gWorkDoneEvent);
gWorkDoneEvent = nullptr;
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
StartTerminationThread();
return 0;
}
/**
* Called by SCM whenever a control code is sent to the service
* using the ControlService function.
*/
void WINAPI
SvcCtrlHandler(DWORD dwCtrl)
{
// After a SERVICE_CONTROL_STOP there should be no more commands sent to
// the SvcCtrlHandler.
if (gServiceControlStopping) {
return;
}
// Handle the requested control code.
switch(dwCtrl) {
case SERVICE_CONTROL_SHUTDOWN:
case SERVICE_CONTROL_STOP: {
gServiceControlStopping = true;
ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000);
// The SvcCtrlHandler thread should not spend more than 30 seconds in
// shutdown so we spawn a new thread for stopping the service
HANDLE thread = CreateThread(nullptr, 0,
StopServiceAndWaitForCommandThread,
nullptr, 0, nullptr);
if (thread) {
CloseHandle(thread);
} else {
// Couldn't start the thread so just call the stop ourselves.
// If it happens to take longer than 30 seconds the caller will
// get an error.
StopServiceAndWaitForCommandThread(nullptr);
}
}
break;
default:
break;
}
}