From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- toolkit/components/maintenanceservice/Makefile.in | 13 + .../maintenanceservice_installer.nsi | 269 +++++++ .../maintenanceservice/maintenanceservice.cpp | 370 ++++++++++ .../maintenanceservice.exe.manifest | 25 + .../maintenanceservice/maintenanceservice.h | 16 + .../maintenanceservice/maintenanceservice.rc | 79 ++ toolkit/components/maintenanceservice/moz.build | 62 ++ toolkit/components/maintenanceservice/resource.h | 20 + .../components/maintenanceservice/servicebase.cpp | 84 +++ .../components/maintenanceservice/servicebase.h | 22 + .../maintenanceservice/serviceinstall.cpp | 760 +++++++++++++++++++ .../components/maintenanceservice/serviceinstall.h | 19 + .../tests/gtest/ServiceStartInteractiveOnly.cpp | 52 ++ .../maintenanceservice/tests/gtest/moz.build | 20 + .../components/maintenanceservice/workmonitor.cpp | 812 +++++++++++++++++++++ .../components/maintenanceservice/workmonitor.h | 5 + 16 files changed, 2628 insertions(+) create mode 100644 toolkit/components/maintenanceservice/Makefile.in create mode 100644 toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi create mode 100644 toolkit/components/maintenanceservice/maintenanceservice.cpp create mode 100644 toolkit/components/maintenanceservice/maintenanceservice.exe.manifest create mode 100644 toolkit/components/maintenanceservice/maintenanceservice.h create mode 100644 toolkit/components/maintenanceservice/maintenanceservice.rc create mode 100644 toolkit/components/maintenanceservice/moz.build create mode 100644 toolkit/components/maintenanceservice/resource.h create mode 100644 toolkit/components/maintenanceservice/servicebase.cpp create mode 100644 toolkit/components/maintenanceservice/servicebase.h create mode 100644 toolkit/components/maintenanceservice/serviceinstall.cpp create mode 100644 toolkit/components/maintenanceservice/serviceinstall.h create mode 100644 toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp create mode 100644 toolkit/components/maintenanceservice/tests/gtest/moz.build create mode 100644 toolkit/components/maintenanceservice/workmonitor.cpp create mode 100644 toolkit/components/maintenanceservice/workmonitor.h (limited to 'toolkit/components/maintenanceservice') diff --git a/toolkit/components/maintenanceservice/Makefile.in b/toolkit/components/maintenanceservice/Makefile.in new file mode 100644 index 0000000000..b07afbb0a9 --- /dev/null +++ b/toolkit/components/maintenanceservice/Makefile.in @@ -0,0 +1,13 @@ +# 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/. + +ifndef MOZ_WINCONSOLE +ifdef MOZ_DEBUG +MOZ_WINCONSOLE = 1 +else +MOZ_WINCONSOLE = 0 +endif +endif + +include $(topsrcdir)/config/rules.mk diff --git a/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi new file mode 100644 index 0000000000..685458db0e --- /dev/null +++ b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi @@ -0,0 +1,269 @@ +# 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/. + +; Set verbosity to 3 (e.g. no script) to lessen the noise in the build logs +!verbose 3 + +; 7-Zip provides better compression than the lzma from NSIS so we add the files +; uncompressed and use 7-Zip to create a SFX archive of it +SetDatablockOptimize on +SetCompress off +CRCCheck on + +RequestExecutionLevel admin + +Unicode true +ManifestSupportedOS all +ManifestDPIAware true + +!addplugindir ./ + +; Variables +Var TempMaintServiceName +Var BrandFullNameDA +Var BrandFullName + +; Other included files may depend upon these includes! +; The following includes are provided by NSIS. +!include FileFunc.nsh +!include LogicLib.nsh +!include MUI.nsh +!include WinMessages.nsh +!include WinVer.nsh +!include WordFunc.nsh + +!insertmacro GetOptions +!insertmacro GetParameters +!insertmacro GetSize + +; The test machines use this fallback key to run tests. +; And anyone that wants to run tests themselves should already have +; this installed. +!define FallbackKey \ + "SOFTWARE\Mozilla\MaintenanceService\3932ecacee736d366d6436db0f55bce4" + +!define CompanyName "Mozilla Corporation" +!define BrandFullNameInternal "" + +; The following includes are custom. +!include defines.nsi +; We keep defines.nsi defined so that we get other things like +; the version number, but we redefine BrandFullName +!define MaintFullName "Mozilla Maintenance Service" +!ifdef BrandFullName +!undef BrandFullName +!endif +!define BrandFullName "${MaintFullName}" + +!include common.nsh +!include locales.nsi + +VIAddVersionKey "FileDescription" "${MaintFullName} Installer" +VIAddVersionKey "OriginalFilename" "maintenanceservice_installer.exe" + +Name "${MaintFullName}" +OutFile "maintenanceservice_installer.exe" + +; Get installation folder from registry if available +InstallDirRegKey HKLM "Software\Mozilla\MaintenanceService" "" + +SetOverwrite on + +!define MaintUninstallKey \ + "Software\Microsoft\Windows\CurrentVersion\Uninstall\MozillaMaintenanceService" + +; Always install into the 32-bit location even if we have a 64-bit build. +; This is because we use only 1 service for all Firefox channels. +; Allow either x86 and x64 builds to exist at this location, depending on +; what is the latest build. +InstallDir "$PROGRAMFILES32\${MaintFullName}\" +ShowUnInstDetails nevershow + +################################################################################ +# Modern User Interface - MUI + +!define MUI_ICON setup.ico +!define MUI_UNICON setup.ico +!define MUI_WELCOMEPAGE_TITLE_3LINES +!define MUI_UNWELCOMEFINISHPAGE_BITMAP wizWatermark.bmp + +;Interface Settings +!define MUI_ABORTWARNING + +; Uninstaller Pages +!insertmacro MUI_UNPAGE_CONFIRM +!insertmacro MUI_UNPAGE_INSTFILES + +################################################################################ +# Language + +!insertmacro MOZ_MUI_LANGUAGE 'baseLocale' +!verbose push +!verbose 3 +!include "overrideLocale.nsh" +!include "customLocale.nsh" +!verbose pop + +; Set this after the locale files to override it if it is in the locale +; using " " for BrandingText will hide the "Nullsoft Install System..." branding +BrandingText " " + +Function .onInit + ; Remove the current exe directory from the search order. + ; This only effects LoadLibrary calls and not implicitly loaded DLLs. + System::Call 'kernel32::SetDllDirectoryW(w "")' + + SetSilent silent + ${Unless} ${AtLeastWin7} + Abort + ${EndUnless} +FunctionEnd + +Function un.onInit + ; Remove the current exe directory from the search order. + ; This only effects LoadLibrary calls and not implicitly loaded DLLs. + System::Call 'kernel32::SetDllDirectoryW(w "")' + + StrCpy $BrandFullNameDA "${MaintFullName}" + StrCpy $BrandFullName "${MaintFullName}" +FunctionEnd + +Section "MaintenanceService" + AllowSkipFiles off + + CreateDirectory $INSTDIR + SetOutPath $INSTDIR + + ; If the service already exists, then it will be stopped when upgrading it + ; via the maintenanceservice_tmp.exe command executed below. + ; The maintenanceservice_tmp.exe command will rename the file to + ; maintenanceservice.exe if maintenanceservice_tmp.exe is newer. + ; If the service does not exist yet, we install it and drop the file on + ; disk as maintenanceservice.exe directly. + StrCpy $TempMaintServiceName "maintenanceservice.exe" + IfFileExists "$INSTDIR\maintenanceservice.exe" 0 skipAlreadyExists + StrCpy $TempMaintServiceName "maintenanceservice_tmp.exe" + skipAlreadyExists: + + ; We always write out a copy and then decide whether to install it or + ; not via calling its 'install' cmdline which works by version comparison. + CopyFiles "$EXEDIR\maintenanceservice.exe" "$INSTDIR\$TempMaintServiceName" + + ; The updater.ini file is only used when performing an install or upgrade, + ; and only if that install or upgrade is successful. If an old updater.ini + ; happened to be copied into the maintenance service installation directory + ; but the service was not newer, the updater.ini file would be unused. + ; It is used to fill the description of the service on success. + CopyFiles "$EXEDIR\updater.ini" "$INSTDIR\updater.ini" + + ; Install the application maintenance service. + ; If a service already exists, the command line parameter will stop the + ; service and only install itself if it is newer than the already installed + ; service. If successful it will remove the old maintenanceservice.exe + ; and replace it with maintenanceservice_tmp.exe. + ClearErrors + ;${GetParameters} $0 + ;${GetOptions} "$0" "/Upgrade" $0 + ;${If} ${Errors} + ExecWait '"$INSTDIR\$TempMaintServiceName" forceinstall' + ;${Else} + ; The upgrade cmdline is the same as install except + ; It will fail if the service isn't already installed. + ; ExecWait '"$INSTDIR\$TempMaintServiceName" upgrade' + ;${EndIf} + + WriteUninstaller "$INSTDIR\Uninstall.exe" + WriteRegStr HKLM "${MaintUninstallKey}" "DisplayName" "${MaintFullName}" + WriteRegStr HKLM "${MaintUninstallKey}" "UninstallString" \ + '"$INSTDIR\uninstall.exe"' + WriteRegStr HKLM "${MaintUninstallKey}" "DisplayIcon" \ + "$INSTDIR\Uninstall.exe,0" + WriteRegStr HKLM "${MaintUninstallKey}" "DisplayVersion" "${AppVersion}" + WriteRegStr HKLM "${MaintUninstallKey}" "Publisher" "Mozilla" + WriteRegStr HKLM "${MaintUninstallKey}" "Comments" "${BrandFullName}" + WriteRegDWORD HKLM "${MaintUninstallKey}" "NoModify" 1 + ${GetSize} "$INSTDIR" "/S=0K" $R2 $R3 $R4 + WriteRegDWORD HKLM "${MaintUninstallKey}" "EstimatedSize" $R2 + + ; Write out that a maintenance service was attempted. + ; We do this because on upgrades we will check this value and we only + ; want to install once on the first upgrade to maintenance service. + ; Also write out that we are currently installed, preferences will check + ; this value to determine if we should show the service update pref. + ; Since the Maintenance service can be installed either x86 or x64, + ; always use the 64-bit registry for checking if an attempt was made. + ${If} ${RunningX64} + ${OrIf} ${IsNativeARM64} + SetRegView 64 + ${EndIf} + WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Attempted" 1 + WriteRegDWORD HKLM "Software\Mozilla\MaintenanceService" "Installed" 1 + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" + + ; Included here for debug purposes only. + ; These keys are used to bypass the installation dir is a valid installation + ; check from the service so that tests can be run. + WriteRegStr HKLM "${FallbackKey}\0" "name" "Mozilla Corporation" + WriteRegStr HKLM "${FallbackKey}\0" "issuer" "DigiCert SHA2 Assured ID Code Signing CA" + WriteRegStr HKLM "${FallbackKey}\1" "name" "Mozilla Fake SPC" + WriteRegStr HKLM "${FallbackKey}\1" "issuer" "Mozilla Fake CA" + ${If} ${RunningX64} + ${OrIf} ${IsNativeARM64} + SetRegView lastused + ${EndIf} +SectionEnd + +; By renaming before deleting we improve things slightly in case +; there is a file in use error. In this case a new install can happen. +Function un.RenameDelete + Pop $9 + ; If the .moz-delete file already exists previously, delete it + ; If it doesn't exist, the call is ignored. + ; We don't need to pass /REBOOTOK here since it was already marked that way + ; if it exists. + Delete "$9.moz-delete" + Rename "$9" "$9.moz-delete" + ${If} ${Errors} + Delete /REBOOTOK "$9" + ${Else} + Delete /REBOOTOK "$9.moz-delete" + ${EndIf} + ClearErrors +FunctionEnd + +Section "Uninstall" + ; Delete the service so that no updates will be attempted + ExecWait '"$INSTDIR\maintenanceservice.exe" uninstall' + + Push "$INSTDIR\updater.ini" + Call un.RenameDelete + Push "$INSTDIR\maintenanceservice.exe" + Call un.RenameDelete + Push "$INSTDIR\maintenanceservice_tmp.exe" + Call un.RenameDelete + Push "$INSTDIR\maintenanceservice.old" + Call un.RenameDelete + Push "$INSTDIR\Uninstall.exe" + Call un.RenameDelete + Push "$INSTDIR\update\updater.ini" + Call un.RenameDelete + Push "$INSTDIR\update\updater.exe" + Call un.RenameDelete + RMDir /REBOOTOK "$INSTDIR\update" + RMDir /REBOOTOK "$INSTDIR" + DeleteRegKey HKLM "${MaintUninstallKey}" + + ${If} ${RunningX64} + ${OrIf} ${IsNativeARM64} + SetRegView 64 + ${EndIf} + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "Installed" + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" + DeleteRegKey HKLM "${FallbackKey}\" + ${If} ${RunningX64} + ${OrIf} ${IsNativeARM64} + SetRegView lastused + ${EndIf} +SectionEnd + diff --git a/toolkit/components/maintenanceservice/maintenanceservice.cpp b/toolkit/components/maintenanceservice/maintenanceservice.cpp new file mode 100644 index 0000000000..d46feb1fc7 --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.cpp @@ -0,0 +1,370 @@ +/* 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 +#include +#include +#include +#include + +#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 logFilePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(logFilePath) && + PathAppendSafe(logFilePath, L"maintenanceservice-install.log")) { + LogInit(logFilePath); + } + + 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. (%lu)", GetLastError())); + LogFinish(); + return 1; + } + + LOG(("The service was installed successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"upgrade")) { + WCHAR logFilePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(logFilePath) && + PathAppendSafe(logFilePath, L"maintenanceservice-install.log")) { + LogInit(logFilePath); + } + + LOG(("Upgrading service if installed...")); + if (!SvcInstall(UpgradeSvc)) { + LOG_WARN(("Could not upgrade service. (%lu)", GetLastError())); + LogFinish(); + return 1; + } + + LOG(("The service was upgraded successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"uninstall")) { + WCHAR logFilePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(logFilePath) && + PathAppendSafe(logFilePath, L"maintenanceservice-uninstall.log")) { + LogInit(logFilePath); + } + LOG(("Uninstalling service...")); + if (!SvcUninstall()) { + LOG_WARN(("Could not uninstall service. (%lu)", 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[] = { + {const_cast(SVC_NAME), + (LPSERVICE_MAIN_FUNCTIONW)SvcMain}, // -Wwritable-strings + {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. (%lu)", 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); +} + +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 logFilePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(logFilePath)) { + BackupOldLogs(logFilePath, LOGS_TO_KEEP); + if (PathAppendSafe(logFilePath, L"maintenanceservice.log")) { + LogInit(logFilePath); + } + } + + // 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. (%lu)", 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. + BOOL success = 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, success ? NO_ERROR : 1, 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; + } +} diff --git a/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest new file mode 100644 index 0000000000..e6bfba8ca5 --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest @@ -0,0 +1,25 @@ + + + +MaintenanceService + + + + + + + + + + + + + + + + diff --git a/toolkit/components/maintenanceservice/maintenanceservice.h b/toolkit/components/maintenanceservice/maintenanceservice.h new file mode 100644 index 0000000000..596b2b44ee --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.h @@ -0,0 +1,16 @@ +/* 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/. */ + +#ifndef TOOLKIT_COMPONENTS_MAINTENANCESERVICE_MAINTENANCESERVICE_H_ +#define TOOLKIT_COMPONENTS_MAINTENANCESERVICE_MAINTENANCESERVICE_H_ + +#include + +void WINAPI SvcMain(DWORD dwArgc, LPWSTR* lpszArgv); +void SvcInit(DWORD dwArgc, LPWSTR* lpszArgv); +void WINAPI SvcCtrlHandler(DWORD dwCtrl); +void ReportSvcStatus(DWORD dwCurrentState, DWORD dwWin32ExitCode, + DWORD dwWaitHint); + +#endif // TOOLKIT_COMPONENTS_MAINTENANCESERVICE_MAINTENANCESERVICE_H_ diff --git a/toolkit/components/maintenanceservice/maintenanceservice.rc b/toolkit/components/maintenanceservice/maintenanceservice.rc new file mode 100644 index 0000000000..e3731efb2a --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.rc @@ -0,0 +1,79 @@ +/* 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/. */ + +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winresrc.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN +END +#endif // APSTUDIO_INVOKED + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winresrc.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/toolkit/components/maintenanceservice/moz.build b/toolkit/components/maintenanceservice/moz.build new file mode 100644 index 0000000000..ee9a106259 --- /dev/null +++ b/toolkit/components/maintenanceservice/moz.build @@ -0,0 +1,62 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Program("maintenanceservice") + +SOURCES += [ + "maintenanceservice.cpp", + "servicebase.cpp", + "serviceinstall.cpp", + "workmonitor.cpp", +] + +USE_LIBS += [ + "updatecommon", +] + +if CONFIG["DISABLE_UPDATER_AUTHENTICODE_CHECK"]: + DEFINES["DISABLE_UPDATER_AUTHENTICODE_CHECK"] = True + +DEFINES["UNICODE"] = True +DEFINES["_UNICODE"] = True +DEFINES["NS_NO_XPCOM"] = True + +# Pick up nsWindowsRestart.cpp +LOCAL_INCLUDES += [ + "/toolkit/mozapps/update/common", + "/toolkit/xre", +] + +USE_STATIC_LIBS = True + +if CONFIG["CC_TYPE"] == "clang-cl": + WIN32_EXE_LDFLAGS += ["-ENTRY:wmainCRTStartup"] + +if CONFIG["OS_TARGET"] == "WINNT" and CONFIG["CC_TYPE"] in ("gcc", "clang"): + # This allows us to use wmain as the entry point on mingw + LDFLAGS += [ + "-municode", + ] + +RCINCLUDE = "maintenanceservice.rc" + +DisableStlWrapping() + +OS_LIBS += [ + "comctl32", + "ws2_32", + "shell32", + "shlwapi", + "user32", + "userenv", + "uuid", +] + +if CONFIG["ENABLE_TESTS"]: + DIRS += ["tests/gtest"] + +with Files("**"): + BUG_COMPONENT = ("Toolkit", "Application Update") diff --git a/toolkit/components/maintenanceservice/resource.h b/toolkit/components/maintenanceservice/resource.h new file mode 100644 index 0000000000..fcc79dd9ae --- /dev/null +++ b/toolkit/components/maintenanceservice/resource.h @@ -0,0 +1,20 @@ +/* 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/. */ + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by updater.rc +// +#define IDI_DIALOG 1003 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +# ifndef APSTUDIO_READONLY_SYMBOLS +# define _APS_NEXT_RESOURCE_VALUE 102 +# define _APS_NEXT_COMMAND_VALUE 40001 +# define _APS_NEXT_CONTROL_VALUE 1003 +# define _APS_NEXT_SYMED_VALUE 101 +# endif +#endif diff --git a/toolkit/components/maintenanceservice/servicebase.cpp b/toolkit/components/maintenanceservice/servicebase.cpp new file mode 100644 index 0000000000..d6004234c7 --- /dev/null +++ b/toolkit/components/maintenanceservice/servicebase.cpp @@ -0,0 +1,84 @@ +/* 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 "servicebase.h" +#include "nsWindowsHelpers.h" + +// Shared code between applications and updater.exe +#include "nsWindowsRestart.cpp" + +/** + * Verifies if 2 files are byte for byte equivalent. + * + * @param file1Path The first file to verify. + * @param file2Path The second file to verify. + * @param sameContent Out parameter, TRUE if the files are equal + * @return TRUE If there was no error checking the files. + */ +BOOL VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL& sameContent) { + sameContent = FALSE; + nsAutoHandle file1(CreateFileW(file1Path, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr)); + if (INVALID_HANDLE_VALUE == file1) { + return FALSE; + } + nsAutoHandle file2(CreateFileW(file2Path, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr)); + if (INVALID_HANDLE_VALUE == file2) { + return FALSE; + } + + DWORD fileSize1 = GetFileSize(file1, nullptr); + DWORD fileSize2 = GetFileSize(file2, nullptr); + if (INVALID_FILE_SIZE == fileSize1 || INVALID_FILE_SIZE == fileSize2) { + return FALSE; + } + + if (fileSize1 != fileSize2) { + // sameContent is already set to FALSE + return TRUE; + } + + char buf1[COMPARE_BLOCKSIZE]; + char buf2[COMPARE_BLOCKSIZE]; + DWORD numBlocks = fileSize1 / COMPARE_BLOCKSIZE; + DWORD leftOver = fileSize1 % COMPARE_BLOCKSIZE; + DWORD readAmount; + for (DWORD i = 0; i < numBlocks; i++) { + if (!ReadFile(file1, buf1, COMPARE_BLOCKSIZE, &readAmount, nullptr) || + readAmount != COMPARE_BLOCKSIZE) { + return FALSE; + } + + if (!ReadFile(file2, buf2, COMPARE_BLOCKSIZE, &readAmount, nullptr) || + readAmount != COMPARE_BLOCKSIZE) { + return FALSE; + } + + if (memcmp(buf1, buf2, COMPARE_BLOCKSIZE)) { + // sameContent is already set to FALSE + return TRUE; + } + } + + if (leftOver) { + if (!ReadFile(file1, buf1, leftOver, &readAmount, nullptr) || + readAmount != leftOver) { + return FALSE; + } + + if (!ReadFile(file2, buf2, leftOver, &readAmount, nullptr) || + readAmount != leftOver) { + return FALSE; + } + + if (memcmp(buf1, buf2, leftOver)) { + // sameContent is already set to FALSE + return TRUE; + } + } + + sameContent = TRUE; + return TRUE; +} diff --git a/toolkit/components/maintenanceservice/servicebase.h b/toolkit/components/maintenanceservice/servicebase.h new file mode 100644 index 0000000000..5ba3689a22 --- /dev/null +++ b/toolkit/components/maintenanceservice/servicebase.h @@ -0,0 +1,22 @@ +/* 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 +#include "updatecommon.h" + +BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra); +BOOL VerifySameFiles(LPCWSTR file1Path, LPCWSTR file2Path, BOOL& sameContent); + +// 32KiB for comparing files at a time seems reasonable. +// The bigger the better for speed, but this will be used +// on the stack so I don't want it to be too big. +#define COMPARE_BLOCKSIZE 32768 + +// The following string resource value is used to uniquely identify the signed +// Mozilla application as an updater. Before the maintenance service will +// execute the updater it must have this updater identity string in its string +// table. No other signed Mozilla product will have this string table value. +#define UPDATER_IDENTITY_STRING \ + "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49" +#define IDS_UPDATER_IDENTITY 1006 diff --git a/toolkit/components/maintenanceservice/serviceinstall.cpp b/toolkit/components/maintenanceservice/serviceinstall.cpp new file mode 100644 index 0000000000..c59a74b94a --- /dev/null +++ b/toolkit/components/maintenanceservice/serviceinstall.cpp @@ -0,0 +1,760 @@ +/* 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 +#include +#include +#include + +// Used for DNLEN and UNLEN +#include + +#include +#include "mozilla/UniquePtr.h" + +#include "serviceinstall.h" +#include "servicebase.h" +#include "updatehelper.h" +#include "shellapi.h" +#include "readstrings.h" +#include "updatererrors.h" +#include "commonupdatedir.h" + +#pragma comment(lib, "version.lib") + +// This uninstall key is defined originally in maintenanceservice_installer.nsi +#define MAINT_UNINSTALL_KEY \ + L"Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\MozillaMaintenan" \ + L"ceService" + +static BOOL UpdateUninstallerVersionString(LPWSTR versionString) { + HKEY uninstallKey; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, MAINT_UNINSTALL_KEY, 0, + KEY_WRITE | KEY_WOW64_32KEY, + &uninstallKey) != ERROR_SUCCESS) { + return FALSE; + } + + LONG rv = RegSetValueExW(uninstallKey, L"DisplayVersion", 0, REG_SZ, + reinterpret_cast(versionString), + (wcslen(versionString) + 1) * sizeof(WCHAR)); + RegCloseKey(uninstallKey); + return rv == ERROR_SUCCESS; +} + +/** + * A wrapper function to read strings for the maintenance service. + * + * @param path The path of the ini file to read from + * @param results The maintenance service strings that were read + * @return OK on success + */ +static int ReadMaintenanceServiceStrings( + LPCWSTR path, MaintenanceServiceStringTable* results) { + // Read in the maintenance service description string if specified. + const unsigned int kNumStrings = 1; + const char* kServiceKeys = "MozillaMaintenanceDescription\0"; + mozilla::UniquePtr serviceString; + int result = ReadStrings(path, kServiceKeys, kNumStrings, &serviceString); + if (result != OK) { + results->serviceDescription = mozilla::MakeUnique(1); + results->serviceDescription.get()[0] = '\0'; + } + results->serviceDescription.swap(serviceString); + return result; +} + +/** + * Obtains the version number from the specified PE file's version information + * Version Format: A.B.C.D (Example 10.0.0.300) + * + * @param path The path of the file to check the version on + * @param A The first part of the version number + * @param B The second part of the version number + * @param C The third part of the version number + * @param D The fourth part of the version number + * @return TRUE if successful + */ +static BOOL GetVersionNumberFromPath(LPWSTR path, DWORD& A, DWORD& B, DWORD& C, + DWORD& D) { + DWORD fileVersionInfoSize = GetFileVersionInfoSizeW(path, 0); + mozilla::UniquePtr fileVersionInfo(new char[fileVersionInfoSize]); + if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize, + fileVersionInfo.get())) { + LOG_WARN( + ("Could not obtain file info of old service. (%lu)", GetLastError())); + return FALSE; + } + + VS_FIXEDFILEINFO* fixedFileInfo = + reinterpret_cast(fileVersionInfo.get()); + UINT size; + if (!VerQueryValueW(fileVersionInfo.get(), L"\\", + reinterpret_cast(&fixedFileInfo), &size)) { + LOG_WARN(("Could not query file version info of old service. (%lu)", + GetLastError())); + return FALSE; + } + + A = HIWORD(fixedFileInfo->dwFileVersionMS); + B = LOWORD(fixedFileInfo->dwFileVersionMS); + C = HIWORD(fixedFileInfo->dwFileVersionLS); + D = LOWORD(fixedFileInfo->dwFileVersionLS); + return TRUE; +} + +/** + * Updates the service description with what is stored in updater.ini + * at the same path as the currently executing module binary. + * + * @param serviceHandle A handle to an opened service with + * SERVICE_CHANGE_CONFIG access right + * @param TRUE on succcess. + */ +BOOL UpdateServiceDescription(SC_HANDLE serviceHandle) { + WCHAR updaterINIPath[MAX_PATH + 1]; + if (!GetModuleFileNameW(nullptr, updaterINIPath, + sizeof(updaterINIPath) / sizeof(updaterINIPath[0]))) { + LOG_WARN( + ("Could not obtain module filename when attempting to " + "modify service description. (%lu)", + GetLastError())); + return FALSE; + } + + if (!PathRemoveFileSpecW(updaterINIPath)) { + LOG_WARN( + ("Could not remove file spec when attempting to " + "modify service description. (%lu)", + GetLastError())); + return FALSE; + } + + if (!PathAppendSafe(updaterINIPath, L"updater.ini")) { + LOG_WARN( + ("Could not append updater.ini filename when attempting to " + "modify service description. (%lu)", + GetLastError())); + return FALSE; + } + + if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) { + LOG_WARN( + ("updater.ini file does not exist, will not modify " + "service description. (%lu)", + GetLastError())); + return FALSE; + } + + MaintenanceServiceStringTable serviceStrings; + int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings); + if (rv != OK || !strlen(serviceStrings.serviceDescription.get())) { + LOG_WARN( + ("updater.ini file does not contain a maintenance " + "service description.")); + return FALSE; + } + + int bufferSize = MultiByteToWideChar( + CP_UTF8, 0, serviceStrings.serviceDescription.get(), -1, nullptr, 0); + mozilla::UniquePtr serviceDescription = + mozilla::MakeUnique(bufferSize); + if (!MultiByteToWideChar(CP_UTF8, 0, serviceStrings.serviceDescription.get(), + -1, serviceDescription.get(), bufferSize)) { + LOG_WARN(("Could not convert description to wide string format. (%lu)", + GetLastError())); + return FALSE; + } + + SERVICE_DESCRIPTIONW descriptionConfig; + descriptionConfig.lpDescription = serviceDescription.get(); + if (!ChangeServiceConfig2W(serviceHandle, SERVICE_CONFIG_DESCRIPTION, + &descriptionConfig)) { + LOG_WARN(("Could not change service config. (%lu)", GetLastError())); + return FALSE; + } + + LOG(("The service description was updated successfully.")); + return TRUE; +} + +/** + * Determines if the MozillaMaintenance service path needs to be updated + * and fixes it if it is wrong. + * + * @param service A handle to the service to fix. + * @param currentServicePath The current (possibly wrong) path that is used. + * @param servicePathWasWrong Out parameter set to TRUE if a fix was needed. + * @return TRUE if the service path is now correct. + */ +BOOL FixServicePath(SC_HANDLE service, LPCWSTR currentServicePath, + BOOL& servicePathWasWrong) { + // When we originally upgraded the MozillaMaintenance service we + // would uninstall the service on each upgrade. This had an + // intermittent error which could cause the service to use the file + // maintenanceservice_tmp.exe as the install path. Only a small number + // of Nightly users would be affected by this, but we check for this + // state here and fix the user if they are affected. + // + // We also fix the path in the case of the path not being quoted. + size_t currentServicePathLen = wcslen(currentServicePath); + bool doesServiceHaveCorrectPath = + currentServicePathLen > 2 && + !wcsstr(currentServicePath, L"maintenanceservice_tmp.exe") && + currentServicePath[0] == L'\"' && + currentServicePath[currentServicePathLen - 1] == L'\"'; + + if (doesServiceHaveCorrectPath) { + LOG(("The MozillaMaintenance service path is correct.")); + servicePathWasWrong = FALSE; + return TRUE; + } + // This is a recoverable situation so not logging as a warning + LOG(("The MozillaMaintenance path is NOT correct. It was: %ls", + currentServicePath)); + + servicePathWasWrong = TRUE; + WCHAR fixedPath[MAX_PATH + 1] = {L'\0'}; + wcsncpy(fixedPath, currentServicePath, MAX_PATH); + PathUnquoteSpacesW(fixedPath); + if (!PathRemoveFileSpecW(fixedPath)) { + LOG_WARN(("Couldn't remove file spec. (%lu)", GetLastError())); + return FALSE; + } + if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) { + LOG_WARN(("Couldn't append file spec. (%lu)", GetLastError())); + return FALSE; + } + PathQuoteSpacesW(fixedPath); + + if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_NO_CHANGE, + SERVICE_NO_CHANGE, fixedPath, nullptr, nullptr, + nullptr, nullptr, nullptr, nullptr)) { + LOG_WARN(("Could not fix service path. (%lu)", GetLastError())); + return FALSE; + } + + LOG(("Fixed service path to: %ls.", fixedPath)); + return TRUE; +} + +/** + * Installs or upgrades the SVC_NAME service. + * If an existing service is already installed, we replace it with the + * currently running process. + * + * @param action The action to perform. + * @return TRUE if the service was installed/upgraded + */ +BOOL SvcInstall(SvcInstallAction action) { + // Get a handle to the local computer SCM database with full access rights. + nsAutoServiceHandle schSCManager( + OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS)); + if (!schSCManager) { + LOG_WARN(("Could not open service manager. (%lu)", GetLastError())); + return FALSE; + } + + WCHAR newServiceBinaryPath[MAX_PATH + 1]; + if (!GetModuleFileNameW( + nullptr, newServiceBinaryPath, + sizeof(newServiceBinaryPath) / sizeof(newServiceBinaryPath[0]))) { + LOG_WARN( + ("Could not obtain module filename when attempting to " + "install service. (%lu)", + GetLastError())); + return FALSE; + } + + // Check if we already have the service installed. + nsAutoServiceHandle schService( + OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS)); + DWORD lastError = GetLastError(); + if (!schService && ERROR_SERVICE_DOES_NOT_EXIST != lastError) { + // The service exists but we couldn't open it + LOG_WARN(("Could not open service. (%lu)", GetLastError())); + return FALSE; + } + + if (schService) { + // The service exists but it may not have the correct permissions. + // This could happen if the permissions were not set correctly originally + // or have been changed after the installation. This will reset the + // permissions back to allow limited user accounts. + if (!SetUserAccessServiceDACL(schService)) { + LOG_WARN( + ("Could not reset security ACE on service handle. It might not be " + "possible to start the service. This error should never " + "happen. (%lu)", + GetLastError())); + } + + // The service exists and we opened it + DWORD bytesNeeded; + if (!QueryServiceConfigW(schService, nullptr, 0, &bytesNeeded) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + LOG_WARN( + ("Could not determine buffer size for query service config. (%lu)", + GetLastError())); + return FALSE; + } + + // Get the service config information, in particular we want the binary + // path of the service. + mozilla::UniquePtr serviceConfigBuffer(new char[bytesNeeded]); + if (!QueryServiceConfigW( + schService, + reinterpret_cast(serviceConfigBuffer.get()), + bytesNeeded, &bytesNeeded)) { + LOG_WARN(("Could open service but could not query service config. (%lu)", + GetLastError())); + return FALSE; + } + QUERY_SERVICE_CONFIGW& serviceConfig = + *reinterpret_cast(serviceConfigBuffer.get()); + + // Check if we need to fix the service path + BOOL servicePathWasWrong; + static BOOL alreadyCheckedFixServicePath = FALSE; + if (!alreadyCheckedFixServicePath) { + if (!FixServicePath(schService, serviceConfig.lpBinaryPathName, + servicePathWasWrong)) { + LOG_WARN( + ("Could not fix service path. This should never happen. (%lu)", + GetLastError())); + // True is returned because the service is pointing to + // maintenanceservice_tmp.exe so it actually was upgraded to the + // newest installed service. + return TRUE; + } else if (servicePathWasWrong) { + // Now that the path is fixed we should re-attempt the install. + // This current process' image path is maintenanceservice_tmp.exe. + // The service used to point to maintenanceservice_tmp.exe. + // The service was just fixed to point to maintenanceservice.exe. + // Re-attempting an install from scratch will work as normal. + alreadyCheckedFixServicePath = TRUE; + LOG(("Restarting install action: %d", action)); + return SvcInstall(action); + } + } + + // Ensure the service path is not quoted. We own this memory and know it to + // be large enough for the quoted path, so it is large enough for the + // unquoted path. This function cannot fail. + PathUnquoteSpacesW(serviceConfig.lpBinaryPathName); + + // Obtain the existing maintenanceservice file's version number and + // the new file's version number. Versions are in the format of + // A.B.C.D. + DWORD existingA, existingB, existingC, existingD; + DWORD newA, newB, newC, newD; + BOOL obtainedExistingVersionInfo = + GetVersionNumberFromPath(serviceConfig.lpBinaryPathName, existingA, + existingB, existingC, existingD); + if (!GetVersionNumberFromPath(newServiceBinaryPath, newA, newB, newC, + newD)) { + LOG_WARN(("Could not obtain version number from new path")); + return FALSE; + } + + // Check if we need to replace the old binary with the new one + // If we couldn't get the old version info then we assume we should + // replace it. + if (ForceInstallSvc == action || !obtainedExistingVersionInfo || + (existingA < newA) || (existingA == newA && existingB < newB) || + (existingA == newA && existingB == newB && existingC < newC) || + (existingA == newA && existingB == newB && existingC == newC && + existingD < newD)) { + // We have a newer updater, so update the description from the INI file. + UpdateServiceDescription(schService); + + schService.reset(); + if (!StopService()) { + return FALSE; + } + + if (!wcscmp(newServiceBinaryPath, serviceConfig.lpBinaryPathName)) { + LOG( + ("File is already in the correct location, no action needed for " + "upgrade. The path is: \"%ls\"", + newServiceBinaryPath)); + return TRUE; + } + + BOOL result = TRUE; + + // Attempt to copy the new binary over top the existing binary. + // If there is an error we try to move it out of the way and then + // copy it in. First try the safest / easiest way to overwrite the file. + if (!CopyFileW(newServiceBinaryPath, serviceConfig.lpBinaryPathName, + FALSE)) { + LOG_WARN( + ("Could not overwrite old service binary file. " + "This should never happen, but if it does the next " + "upgrade will fix it, the service is not a critical " + "component that needs to be installed for upgrades " + "to work. (%lu)", + GetLastError())); + + // We rename the last 3 filename chars in an unsafe way. Manually + // verify there are more than 3 chars for safe failure in MoveFileExW. + const size_t len = wcslen(serviceConfig.lpBinaryPathName); + if (len > 3) { + // Calculate the temp file path that we're moving the file to. This + // is the same as the proper service path but with a .old extension. + LPWSTR oldServiceBinaryTempPath = new WCHAR[len + 1]; + memset(oldServiceBinaryTempPath, 0, (len + 1) * sizeof(WCHAR)); + wcsncpy(oldServiceBinaryTempPath, serviceConfig.lpBinaryPathName, + len); + // Rename the last 3 chars to 'old' + wcsncpy(oldServiceBinaryTempPath + len - 3, L"old", 3); + + // Move the current (old) service file to the temp path. + if (MoveFileExW(serviceConfig.lpBinaryPathName, + oldServiceBinaryTempPath, + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { + // The old binary is moved out of the way, copy in the new one. + if (!CopyFileW(newServiceBinaryPath, serviceConfig.lpBinaryPathName, + FALSE)) { + // It is best to leave the old service binary in this condition. + LOG_WARN( + ("The new service binary could not be copied in." + " The service will not be upgraded.")); + result = FALSE; + } else { + LOG( + ("The new service binary was copied in by first moving the" + " old one out of the way.")); + } + + // Attempt to get rid of the old service temp path. + if (DeleteFileW(oldServiceBinaryTempPath)) { + LOG(("The old temp service path was deleted: %ls.", + oldServiceBinaryTempPath)); + } else { + // The old temp path could not be removed. It will be removed + // the next time the user can't copy the binary in or on + // uninstall. + LOG_WARN(("The old temp service path was not deleted.")); + } + } else { + // It is best to leave the old service binary in this condition. + LOG_WARN( + ("Could not move old service file out of the way from:" + " \"%ls\" to \"%ls\". Service will not be upgraded. (%lu)", + serviceConfig.lpBinaryPathName, oldServiceBinaryTempPath, + GetLastError())); + result = FALSE; + } + delete[] oldServiceBinaryTempPath; + } else { + // It is best to leave the old service binary in this condition. + LOG_WARN( + ("Service binary path was less than 3, service will" + " not be updated. This should never happen.")); + result = FALSE; + } + } else { + WCHAR versionStr[128] = {L'\0'}; + swprintf(versionStr, 128, L"%d.%d.%d.%d", newA, newB, newC, newD); + if (!UpdateUninstallerVersionString(versionStr)) { + LOG(("The uninstaller version string could not be updated.")); + } + LOG(("The new service binary was copied in.")); + } + + // We made a copy of ourselves to the existing location. + // The tmp file (the process of which we are executing right now) will be + // left over. Attempt to delete the file on the next reboot. + if (MoveFileExW(newServiceBinaryPath, nullptr, + MOVEFILE_DELAY_UNTIL_REBOOT)) { + LOG(("Deleting the old file path on the next reboot: %ls.", + newServiceBinaryPath)); + } else { + LOG_WARN(("Call to delete the old file path failed: %ls.", + newServiceBinaryPath)); + } + + return result; + } + + // We don't need to copy ourselves to the existing location. + // The tmp file (the process of which we are executing right now) will be + // left over. Attempt to delete the file on the next reboot. + MoveFileExW(newServiceBinaryPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT); + + // nothing to do, we already have a newer service installed + return TRUE; + } + + // If the service does not exist and we are upgrading, don't install it. + if (UpgradeSvc == action) { + // The service does not exist and we are upgrading, so don't install it + return TRUE; + } + + // Quote the path only if it contains spaces. + PathQuoteSpacesW(newServiceBinaryPath); + // The service does not already exist so create the service as on demand + schService.own(CreateServiceW( + schSCManager, SVC_NAME, SVC_DISPLAY_NAME, SERVICE_ALL_ACCESS, + SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, + newServiceBinaryPath, nullptr, nullptr, nullptr, nullptr, nullptr)); + if (!schService) { + LOG_WARN( + ("Could not create Windows service. " + "This error should never happen since a service install " + "should only be called when elevated. (%lu)", + GetLastError())); + return FALSE; + } + + if (!SetUserAccessServiceDACL(schService)) { + LOG_WARN( + ("Could not set security ACE on service handle, the service will not " + "be able to be started from unelevated processes. " + "This error should never happen. (%lu)", + GetLastError())); + } + + UpdateServiceDescription(schService); + + return TRUE; +} + +/** + * Stops the Maintenance service. + * + * @return TRUE if successful. + */ +BOOL StopService() { + // Get a handle to the local computer SCM database with full access rights. + nsAutoServiceHandle schSCManager( + OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS)); + if (!schSCManager) { + LOG_WARN(("Could not open service manager. (%lu)", GetLastError())); + return FALSE; + } + + // Open the service + nsAutoServiceHandle schService( + OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS)); + if (!schService) { + LOG_WARN(("Could not open service. (%lu)", GetLastError())); + return FALSE; + } + + LOG(("Sending stop request...")); + SERVICE_STATUS status; + SetLastError(ERROR_SUCCESS); + if (!ControlService(schService, SERVICE_CONTROL_STOP, &status) && + GetLastError() != ERROR_SERVICE_NOT_ACTIVE) { + LOG_WARN(("Error sending stop request. (%lu)", GetLastError())); + } + + schSCManager.reset(); + schService.reset(); + + LOG(("Waiting for service stop...")); + DWORD lastState = WaitForServiceStop(SVC_NAME, 30); + + // The service can be in a stopped state but the exe still in use + // so make sure the process is really gone before proceeding + WaitForProcessExit(L"maintenanceservice.exe", 30); + LOG(("Done waiting for service stop, last service state: %lu", lastState)); + + return lastState == SERVICE_STOPPED; +} + +/** + * Uninstalls the Maintenance service. + * + * @return TRUE if successful. + */ +BOOL SvcUninstall() { + // Get a handle to the local computer SCM database with full access rights. + nsAutoServiceHandle schSCManager( + OpenSCManager(nullptr, nullptr, SC_MANAGER_ALL_ACCESS)); + if (!schSCManager) { + LOG_WARN(("Could not open service manager. (%lu)", GetLastError())); + return FALSE; + } + + // Open the service + nsAutoServiceHandle schService( + OpenServiceW(schSCManager, SVC_NAME, SERVICE_ALL_ACCESS)); + if (!schService) { + LOG_WARN(("Could not open service. (%lu)", GetLastError())); + return FALSE; + } + + // Stop the service so it deletes faster and so the uninstaller + // can actually delete its EXE. + DWORD totalWaitTime = 0; + SERVICE_STATUS status; + static const int maxWaitTime = 1000 * 60; // Never wait more than a minute + if (ControlService(schService, SERVICE_CONTROL_STOP, &status)) { + do { + Sleep(status.dwWaitHint); + totalWaitTime += (status.dwWaitHint + 10); + if (status.dwCurrentState == SERVICE_STOPPED) { + break; + } else if (totalWaitTime > maxWaitTime) { + break; + } + } while (QueryServiceStatus(schService, &status)); + } + + // Delete the service or mark it for deletion + BOOL deleted = DeleteService(schService); + if (!deleted) { + deleted = (GetLastError() == ERROR_SERVICE_MARKED_FOR_DELETE); + } + + return deleted; +} + +/** + * Sets the access control list for user access for the specified service. + * + * @param hService The service to set the access control list on + * @return TRUE if successful + */ +BOOL SetUserAccessServiceDACL(SC_HANDLE hService) { + PACL pNewAcl = nullptr; + PSECURITY_DESCRIPTOR psd = nullptr; + DWORD lastError = SetUserAccessServiceDACL(hService, pNewAcl, psd); + if (pNewAcl) { + LocalFree((HLOCAL)pNewAcl); + } + if (psd) { + LocalFree((LPVOID)psd); + } + return ERROR_SUCCESS == lastError; +} + +/** + * Sets the access control list for user access for the specified service. + * + * @param hService The service to set the access control list on + * @param pNewAcl The out param ACL which should be freed by caller + * @param psd out param security descriptor, should be freed by caller + * @return ERROR_SUCCESS if successful + */ +DWORD +SetUserAccessServiceDACL(SC_HANDLE hService, PACL& pNewAcl, + PSECURITY_DESCRIPTOR psd) { + // Get the current security descriptor needed size + DWORD needed = 0; + if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &psd, 0, + &needed)) { + if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + LOG_WARN(("Could not query service object security size. (%lu)", + GetLastError())); + return GetLastError(); + } + + DWORD size = needed; + psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size); + if (!psd) { + LOG_WARN( + ("Could not allocate security descriptor. (%lu)", GetLastError())); + return ERROR_INSUFFICIENT_BUFFER; + } + + // Get the actual security descriptor now + if (!QueryServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, psd, + size, &needed)) { + LOG_WARN( + ("Could not allocate security descriptor. (%lu)", GetLastError())); + return GetLastError(); + } + } + + // Get the current DACL from the security descriptor. + PACL pacl = nullptr; + BOOL bDaclPresent = FALSE; + BOOL bDaclDefaulted = FALSE; + if (!GetSecurityDescriptorDacl(psd, &bDaclPresent, &pacl, &bDaclDefaulted)) { + LOG_WARN(("Could not obtain DACL. (%lu)", GetLastError())); + return GetLastError(); + } + + PSID sidBuiltinUsers; + DWORD SIDSize = SECURITY_MAX_SID_SIZE; + sidBuiltinUsers = LocalAlloc(LMEM_FIXED, SIDSize); + if (!sidBuiltinUsers) { + LOG_WARN(("Could not allocate SID memory. (%lu)", GetLastError())); + return GetLastError(); + } + UniqueSidPtr uniqueSidBuiltinUsers(sidBuiltinUsers); + + if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sidBuiltinUsers, + &SIDSize)) { + DWORD lastError = GetLastError(); + LOG_WARN(("Could not create BI\\Users SID. (%lu)", lastError)); + return lastError; + } + + PSID sidInteractive; + SIDSize = SECURITY_MAX_SID_SIZE; + sidInteractive = LocalAlloc(LMEM_FIXED, SIDSize); + if (!sidInteractive) { + LOG_WARN(("Could not allocate SID memory. (%lu)", GetLastError())); + return GetLastError(); + } + UniqueSidPtr uniqueSidInteractive(sidInteractive); + + if (!CreateWellKnownSid(WinInteractiveSid, nullptr, sidInteractive, + &SIDSize)) { + DWORD lastError = GetLastError(); + LOG_WARN(("Could not create Interactive SID. (%lu)", lastError)); + return lastError; + } + + const size_t eaCount = 2; + EXPLICIT_ACCESS ea[eaCount]; + ZeroMemory(ea, sizeof(ea)); + ea[0].grfAccessMode = REVOKE_ACCESS; + ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea[0].Trustee.TrusteeType = TRUSTEE_IS_GROUP; + ea[0].Trustee.ptstrName = static_cast(sidBuiltinUsers); + ea[1].grfAccessPermissions = SERVICE_START | SERVICE_STOP | GENERIC_READ; + ea[1].grfAccessMode = SET_ACCESS; + ea[1].grfInheritance = NO_INHERITANCE; + ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID; + ea[1].Trustee.TrusteeType = TRUSTEE_IS_GROUP; + ea[1].Trustee.ptstrName = static_cast(sidInteractive); + + DWORD lastError = SetEntriesInAclW(eaCount, ea, pacl, &pNewAcl); + if (ERROR_SUCCESS != lastError) { + LOG_WARN(("Could not set entries in ACL. (%lu)", lastError)); + return lastError; + } + + // Initialize a new security descriptor. + SECURITY_DESCRIPTOR sd; + if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { + LOG_WARN( + ("Could not initialize security descriptor. (%lu)", GetLastError())); + return GetLastError(); + } + + // Set the new DACL in the security descriptor. + if (!SetSecurityDescriptorDacl(&sd, TRUE, pNewAcl, FALSE)) { + LOG_WARN( + ("Could not set security descriptor DACL. (%lu)", GetLastError())); + return GetLastError(); + } + + // Set the new security descriptor for the service object. + if (!SetServiceObjectSecurity(hService, DACL_SECURITY_INFORMATION, &sd)) { + LOG_WARN(("Could not set object security. (%lu)", GetLastError())); + return GetLastError(); + } + + // Woohoo, raise the roof + LOG(("User access was set successfully on the service.")); + return ERROR_SUCCESS; +} diff --git a/toolkit/components/maintenanceservice/serviceinstall.h b/toolkit/components/maintenanceservice/serviceinstall.h new file mode 100644 index 0000000000..735bebd67f --- /dev/null +++ b/toolkit/components/maintenanceservice/serviceinstall.h @@ -0,0 +1,19 @@ +/* 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 "readstrings.h" + +#define SVC_DISPLAY_NAME L"Mozilla Maintenance Service" + +enum SvcInstallAction { UpgradeSvc, InstallSvc, ForceInstallSvc }; +BOOL SvcInstall(SvcInstallAction action); +BOOL SvcUninstall(); +BOOL StopService(); +BOOL SetUserAccessServiceDACL(SC_HANDLE hService); +DWORD SetUserAccessServiceDACL(SC_HANDLE hService, PACL& pNewAcl, + PSECURITY_DESCRIPTOR psd); + +struct MaintenanceServiceStringTable { + mozilla::UniquePtr serviceDescription; +}; diff --git a/toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp b/toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp new file mode 100644 index 0000000000..beff8e8e47 --- /dev/null +++ b/toolkit/components/maintenanceservice/tests/gtest/ServiceStartInteractiveOnly.cpp @@ -0,0 +1,52 @@ +#include "gtest/gtest.h" + +#include +#include +#include + +#include "nsWindowsHelpers.h" + +TEST(MaintenanceServiceTest, ServiceStartInteractiveOnly) +{ + // First, make a restricted token that excludes the Interactive group. + SID_AND_ATTRIBUTES sid; + DWORD SIDSize = SECURITY_MAX_SID_SIZE; + sid.Sid = LocalAlloc(LMEM_FIXED, SIDSize); + // Automatically free the SID when we are done with it. + UniqueSidPtr uniqueSid(sid.Sid); + ASSERT_TRUE(sid.Sid); + + BOOL success = + CreateWellKnownSid(WinInteractiveSid, nullptr, sid.Sid, &SIDSize); + ASSERT_TRUE(success); + + HANDLE primaryToken; + success = + OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &primaryToken); + // Automatically close the token when we are done with it. + nsAutoHandle uniquePrimaryToken(primaryToken); + ASSERT_TRUE(success); + + HANDLE restrictedToken; + success = CreateRestrictedToken(primaryToken, 0, 1, &sid, 0, nullptr, 0, + nullptr, &restrictedToken); + // Automatically close the token when we are done with it. + nsAutoHandle uniqueRestrictedToken(restrictedToken); + ASSERT_TRUE(success); + + success = ImpersonateLoggedOnUser(restrictedToken); + ASSERT_TRUE(success); + + SC_HANDLE scmHandle = + OpenSCManagerW(L"127.0.0.1", nullptr, SC_MANAGER_CONNECT); + // Automatically close the SCM when we are done with it. + nsAutoServiceHandle uniqueScmHandle(scmHandle); + ASSERT_TRUE(scmHandle); + + SC_HANDLE serviceHandle = + OpenServiceW(scmHandle, L"MozillaMaintenance", SERVICE_START); + // Automatically close the SCM when we are done with it. + nsAutoServiceHandle uniqueServiceHandle(serviceHandle); + ASSERT_FALSE(serviceHandle); + ASSERT_EQ(GetLastError(), static_cast(ERROR_ACCESS_DENIED)); +} diff --git a/toolkit/components/maintenanceservice/tests/gtest/moz.build b/toolkit/components/maintenanceservice/tests/gtest/moz.build new file mode 100644 index 0000000000..201f67a69d --- /dev/null +++ b/toolkit/components/maintenanceservice/tests/gtest/moz.build @@ -0,0 +1,20 @@ +# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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 https://mozilla.org/MPL/2.0/. + +Library("MaintenanceServiceTest") + +UNIFIED_SOURCES += [ + "ServiceStartInteractiveOnly.cpp", +] + +DisableStlWrapping() + +OS_LIBS += [ + "advapi32", + "kernel32", +] + +FINAL_LIBRARY = "xul-gtest" diff --git a/toolkit/components/maintenanceservice/workmonitor.cpp b/toolkit/components/maintenanceservice/workmonitor.cpp new file mode 100644 index 0000000000..96a098d547 --- /dev/null +++ b/toolkit/components/maintenanceservice/workmonitor.cpp @@ -0,0 +1,812 @@ +/* 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 +#include +#include +#include +#include + +#ifndef __MINGW32__ +# pragma comment(lib, "wtsapi32.lib") +# pragma comment(lib, "userenv.lib") +# pragma comment(lib, "shlwapi.lib") +# pragma comment(lib, "ole32.lib") +# pragma comment(lib, "rpcrt4.lib") +#endif + +#include "mozilla/CmdLineAndEnvUtils.h" +#include "nsWindowsHelpers.h" +#include "mozilla/UniquePtr.h" + +using mozilla::UniquePtr; + +#include "workmonitor.h" +#include "serviceinstall.h" +#include "servicebase.h" +#include "registrycertificates.h" +#include "uachelper.h" +#include "updatehelper.h" +#include "pathhash.h" +#include "updatererrors.h" +#include "commonupdatedir.h" + +// Wait 15 minutes for an update operation to run at most. +// Updates usually take less than a minute so this seems like a +// significantly large and safe amount of time to wait. +static const int TIME_TO_WAIT_ON_UPDATER = 15 * 60 * 1000; +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName); +BOOL DoesFallbackKeyExist(); + +/* + * Reads the secure update status file and sets isApplying to true if the status + * is set to applying. + * + * @param patchDirPath + * The update patch directory path + * @param isApplying Out parameter for specifying if the status + * is set to applying or not. + * @return TRUE if the information was filled. + */ +static BOOL IsStatusApplying(LPCWSTR patchDirPath, BOOL& isApplying) { + isApplying = FALSE; + WCHAR statusFilePath[MAX_PATH + 1] = {L'\0'}; + if (!GetSecureOutputFilePath(patchDirPath, L".status", statusFilePath)) { + LOG_WARN(("Could not get path for the secure update status file")); + return FALSE; + } + + nsAutoHandle statusFile( + CreateFileW(statusFilePath, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, 0, nullptr)); + + if (INVALID_HANDLE_VALUE == statusFile) { + LOG_WARN(("Could not open update.status file")); + return FALSE; + } + + char buf[32] = {0}; + DWORD read; + if (!ReadFile(statusFile, buf, sizeof(buf), &read, nullptr)) { + LOG_WARN(("Could not read from update.status file")); + return FALSE; + } + + const char kApplying[] = "applying"; + isApplying = strncmp(buf, kApplying, sizeof(kApplying) - 1) == 0; + return TRUE; +} + +/** + * Determines whether we're staging an update. + * + * @param argc The argc value normally sent to updater.exe + * @param argv The argv value normally sent to updater.exe + * @return boolean True if we're staging an update + */ +static bool IsUpdateBeingStaged(int argc, LPWSTR* argv) { + // PID will be set to -1 if we're supposed to stage an update. + return (argc == 4 && !wcscmp(argv[3], L"-1")) || + (argc == 5 && !wcscmp(argv[4], L"-1")); +} + +/** + * Determines whether the param only contains digits. + * + * @param str The string to check + * @param boolean True if the param only contains digits + */ +static bool IsDigits(WCHAR* str) { + while (*str) { + if (!iswdigit(*str++)) { + return FALSE; + } + } + return TRUE; +} + +/** + * Determines whether the command line contains just the directory to apply the + * update to (old command line) or if it contains the installation directory and + * the directory to apply the update to. + * + * @param argc The argc value normally sent to updater.exe + * @param argv The argv value normally sent to updater.exe + * @param boolean True if the command line contains just the directory to apply + * the update to + */ +static bool IsOldCommandline(int argc, LPWSTR* argv) { + return (argc == 4 && !wcscmp(argv[3], L"-1")) || + (argc >= 4 && (wcsstr(argv[3], L"/replace") || IsDigits(argv[3]))); +} + +/** + * Gets the installation directory from the arguments passed to updater.exe. + * + * @param argcTmp The argc value normally sent to updater.exe + * @param argvTmp The argv value normally sent to updater.exe + * @param aResultDir Buffer to hold the installation directory. + */ +static BOOL GetInstallationDir(int argcTmp, LPWSTR* argvTmp, + WCHAR aResultDir[MAX_PATH + 1]) { + int index = 3; + if (IsOldCommandline(argcTmp, argvTmp)) { + index = 2; + } + + if (argcTmp < index) { + return FALSE; + } + + wcsncpy(aResultDir, argvTmp[2], MAX_PATH); + WCHAR* backSlash = wcsrchr(aResultDir, L'\\'); + // Make sure that the path does not include trailing backslashes + if (backSlash && (backSlash[1] == L'\0')) { + *backSlash = L'\0'; + } + + // The new command line's argv[2] is always the installation directory. + if (index == 2) { + bool backgroundUpdate = IsUpdateBeingStaged(argcTmp, argvTmp); + bool replaceRequest = (argcTmp >= 4 && wcsstr(argvTmp[3], L"/replace")); + if (backgroundUpdate || replaceRequest) { + return PathRemoveFileSpecW(aResultDir); + } + } + return TRUE; +} + +/** + * Runs an update process as the service using the SYSTEM account. + * + * @param argc The number of arguments in argv + * @param argv The arguments normally passed to updater.exe + * argv[0] must be the path to updater.exe + * @param processStarted Set to TRUE if the process was started. + * @return TRUE if the update process was run had a return code of 0. + */ +BOOL StartUpdateProcess(int argc, LPWSTR* argv, LPCWSTR installDir, + BOOL& processStarted) { + processStarted = FALSE; + + LOG(("Starting update process as the service in session 0.")); + STARTUPINFOW si; + PROCESS_INFORMATION pi; + + ZeroMemory(&si, sizeof(si)); + ZeroMemory(&pi, sizeof(pi)); + si.cb = sizeof(si); + si.lpDesktop = const_cast(L"winsta0\\Default"); // -Wwritable-strings + + // The updater command line is of the form: + // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]] + auto cmdLine = mozilla::MakeCommandLine(argc, argv); + + int index = 3; + if (IsOldCommandline(argc, argv)) { + index = 2; + } + + // If we're about to start the update process from session 0, + // then we should not show a GUI. This only really needs to be done + // on Vista and higher, but it's better to keep everything consistent + // across all OS if it's of no harm. + if (argc >= index) { + // Setting the desktop to blank will ensure no GUI is displayed + si.lpDesktop = const_cast(L""); // -Wwritable-strings + si.dwFlags |= STARTF_USESHOWWINDOW; + si.wShowWindow = SW_HIDE; + } + + // Add an env var for MOZ_USING_SERVICE so the updater.exe can + // do anything special that it needs to do for service updates. + // Search in updater.cpp for more info on MOZ_USING_SERVICE. + putenv(const_cast("MOZ_USING_SERVICE=1")); + + LOG(("Starting service with cmdline: %ls", cmdLine.get())); + processStarted = + CreateProcessW(argv[0], cmdLine.get(), nullptr, nullptr, FALSE, + CREATE_DEFAULT_ERROR_MODE, nullptr, nullptr, &si, &pi); + + BOOL updateWasSuccessful = FALSE; + if (processStarted) { + BOOL processTerminated = FALSE; + BOOL noProcessExitCode = FALSE; + // Wait for the updater process to finish + LOG(("Process was started... waiting on result.")); + DWORD waitRes = WaitForSingleObject(pi.hProcess, TIME_TO_WAIT_ON_UPDATER); + if (WAIT_TIMEOUT == waitRes) { + // We waited a long period of time for updater.exe and it never finished + // so kill it. + TerminateProcess(pi.hProcess, 1); + processTerminated = TRUE; + } else { + // Check the return code of updater.exe to make sure we get 0 + DWORD returnCode; + if (GetExitCodeProcess(pi.hProcess, &returnCode)) { + LOG(("Process finished with return code %lu.", returnCode)); + // updater returns 0 if successful. + updateWasSuccessful = (returnCode == 0); + } else { + LOG_WARN(("Process finished but could not obtain return code.")); + noProcessExitCode = TRUE; + } + } + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + // Check just in case updater.exe didn't change the status from + // applying. If this is the case we report an error. + BOOL isApplying = FALSE; + if (IsStatusApplying(argv[1], isApplying) && isApplying) { + if (updateWasSuccessful) { + LOG( + ("update.status is still applying even though update was " + "successful.")); + if (!WriteStatusFailure(argv[1], SERVICE_STILL_APPLYING_ON_SUCCESS)) { + LOG_WARN( + ("Could not write update.status still applying on " + "success error.")); + } + // Since we still had applying we know updater.exe didn't do its + // job correctly. + updateWasSuccessful = FALSE; + } else { + LOG_WARN( + ("update.status is still applying and update was not successful.")); + int failcode = SERVICE_STILL_APPLYING_ON_FAILURE; + if (noProcessExitCode) { + failcode = SERVICE_STILL_APPLYING_NO_EXIT_CODE; + } else if (processTerminated) { + failcode = SERVICE_STILL_APPLYING_TERMINATED; + } + if (!WriteStatusFailure(argv[1], failcode)) { + LOG_WARN( + ("Could not write update.status still applying on " + "failure error.")); + } + } + } + } else { + DWORD lastError = GetLastError(); + LOG_WARN( + ("Could not create process as current user, " + "updaterPath: %ls; cmdLine: %ls. (%lu)", + argv[0], cmdLine.get(), lastError)); + } + + // Empty value on putenv is how you remove an env variable in Windows + putenv(const_cast("MOZ_USING_SERVICE=")); + + return updateWasSuccessful; +} + +/** + * Validates a file as an official updater. + * + * @param updater Path to the updater to validate + * @param installDir Path to the application installation + * being updated + * @param updateDir Update applyTo direcotry, + * where logs will be written + * + * @return true if updater is the path to a valid updater + */ +static bool UpdaterIsValid(LPWSTR updater, LPWSTR installDir, + LPWSTR updateDir) { + // Make sure the path to the updater to use for the update is local. + // We do this check to make sure that file locking is available for + // race condition security checks. + BOOL isLocal = FALSE; + if (!IsLocalFile(updater, isLocal) || !isLocal) { + LOG_WARN(("Filesystem in path %ls is not supported (%lu)", updater, + GetLastError())); + if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_NOT_FIXED_DRIVE)) { + LOG_WARN(("Could not write update.status service update failure. (%lu)", + GetLastError())); + } + return false; + } + + nsAutoHandle noWriteLock(CreateFileW(updater, GENERIC_READ, FILE_SHARE_READ, + nullptr, OPEN_EXISTING, 0, nullptr)); + if (INVALID_HANDLE_VALUE == noWriteLock) { + LOG_WARN(("Could not set no write sharing access on file. (%lu)", + GetLastError())); + if (!WriteStatusFailure(updateDir, SERVICE_COULD_NOT_LOCK_UPDATER)) { + LOG_WARN(("Could not write update.status service update failure. (%lu)", + GetLastError())); + } + return false; + } + + // Verify that the updater.exe that we are executing is the same + // as the one in the installation directory which we are updating. + // The installation dir that we are installing to is installDir. + WCHAR installDirUpdater[MAX_PATH + 1] = {L'\0'}; + wcsncpy(installDirUpdater, installDir, MAX_PATH); + if (!PathAppendSafe(installDirUpdater, L"updater.exe")) { + LOG_WARN(("Install directory updater could not be determined.")); + return false; + } + + BOOL updaterIsCorrect; + if (!VerifySameFiles(updater, installDirUpdater, updaterIsCorrect)) { + LOG_WARN( + ("Error checking if the updaters are the same.\n" + "Path 1: %ls\nPath 2: %ls", + updater, installDirUpdater)); + return false; + } + + if (!updaterIsCorrect) { + LOG_WARN( + ("The updaters do not match, updater will not run.\n" + "Path 1: %ls\nPath 2: %ls", + updater, installDirUpdater)); + if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_COMPARE_ERROR)) { + LOG_WARN(("Could not write update.status updater compare failure.")); + } + return false; + } + + LOG( + ("updater.exe was compared successfully to the installation directory" + " updater.exe.")); + + // Check to make sure the updater.exe module has the unique updater identity. + // This is a security measure to make sure that the signed executable that + // we will run is actually an updater. + bool result = true; + HMODULE updaterModule = + LoadLibraryEx(updater, nullptr, LOAD_LIBRARY_AS_DATAFILE); + if (!updaterModule) { + LOG_WARN(("updater.exe module could not be loaded. (%lu)", GetLastError())); + result = false; + } else { + char updaterIdentity[64]; + if (!LoadStringA(updaterModule, IDS_UPDATER_IDENTITY, updaterIdentity, + sizeof(updaterIdentity))) { + LOG_WARN( + ("The updater.exe application does not contain the Mozilla" + " updater identity.")); + result = false; + } + + if (strcmp(updaterIdentity, UPDATER_IDENTITY_STRING)) { + LOG_WARN(("The updater.exe identity string is not valid.")); + result = false; + } + FreeLibrary(updaterModule); + } + + if (result) { + LOG( + ("The updater.exe application contains the Mozilla" + " updater identity.")); + } else { + if (!WriteStatusFailure(updateDir, SERVICE_UPDATER_IDENTITY_ERROR)) { + LOG_WARN(("Could not write update.status no updater identity.")); + } + return false; + } + +#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK + return DoesBinaryMatchAllowedCertificates(installDir, updater); +#else + return true; +#endif +} + +/** + * Processes a software update command + * + * @param argc The number of arguments in argv + * @param argv The arguments normally passed to updater.exe + * argv[0] must be the path to updater.exe + * + * @return TRUE if the update was successful. + */ +BOOL ProcessSoftwareUpdateCommand(DWORD argc, LPWSTR* argv) { + BOOL result = TRUE; + if (argc < 3) { + LOG_WARN( + ("Not enough command line parameters specified. " + "Updating update.status.")); + + // We can only update update.status if argv[1] exists. argv[1] is + // the directory where the update.status file exists. + if (argc < 2 || + !WriteStatusFailure(argv[1], SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS)) { + LOG_WARN(("Could not write update.status service update failure. (%lu)", + GetLastError())); + } + return FALSE; + } + + WCHAR installDir[MAX_PATH + 1] = {L'\0'}; + if (!GetInstallationDir(argc, argv, installDir)) { + LOG_WARN(("Could not get the installation directory")); + if (!WriteStatusFailure(argv[1], SERVICE_INSTALLDIR_ERROR)) { + LOG_WARN( + ("Could not write update.status for GetInstallationDir failure.")); + } + return FALSE; + } + + if (UpdaterIsValid(argv[0], installDir, argv[1])) { + BOOL updateProcessWasStarted = FALSE; + if (StartUpdateProcess(argc, argv, installDir, updateProcessWasStarted)) { + LOG(("updater.exe was launched and run successfully!")); + LogFlush(); + + // Don't attempt to update the service when the update is being staged. + if (!IsUpdateBeingStaged(argc, argv)) { + // We might not execute code after StartServiceUpdate because + // the service installer will stop the service if it is running. + StartServiceUpdate(installDir); + } + } else { + result = FALSE; + LOG_WARN(("Error running update process. Updating update.status (%lu)", + GetLastError())); + LogFlush(); + + // If the update process was started, then updater.exe is responsible for + // setting the failure code. If it could not be started then we do the + // work. We set an error instead of directly setting status pending + // so that the app.update.service.errors pref can be updated when + // the callback app restarts. + if (!updateProcessWasStarted) { + if (!WriteStatusFailure(argv[1], + SERVICE_UPDATER_COULD_NOT_BE_STARTED)) { + LOG_WARN( + ("Could not write update.status service update failure. (%lu)", + GetLastError())); + } + } + } + } else { + result = FALSE; + LOG_WARN( + ("Could not start process due to certificate check error on " + "updater.exe. Updating update.status. (%lu)", + GetLastError())); + + // When there is a certificate check error on the updater.exe application, + // we want to write out the error. + if (!WriteStatusFailure(argv[1], SERVICE_UPDATER_SIGN_ERROR)) { + LOG_WARN(("Could not write pending state to update.status. (%lu)", + GetLastError())); + } + } + + return result; +} + +/** + * Obtains the updater path alongside a subdir of the service binary. + * The purpose of this function is to return a path that is likely high + * integrity and therefore more safe to execute code from. + * + * @param serviceUpdaterPath Out parameter for the path where the updater + * should be copied to. + * @return TRUE if a file path was obtained. + */ +BOOL GetSecureUpdaterPath(WCHAR serviceUpdaterPath[MAX_PATH + 1]) { + if (!GetModuleFileNameW(nullptr, serviceUpdaterPath, MAX_PATH)) { + LOG_WARN( + ("Could not obtain module filename when attempting to " + "use a secure updater path. (%lu)", + GetLastError())); + return FALSE; + } + + if (!PathRemoveFileSpecW(serviceUpdaterPath)) { + LOG_WARN( + ("Couldn't remove file spec when attempting to use a secure " + "updater path. (%lu)", + GetLastError())); + return FALSE; + } + + if (!PathAppendSafe(serviceUpdaterPath, L"update")) { + LOG_WARN( + ("Couldn't append file spec when attempting to use a secure " + "updater path. (%lu)", + GetLastError())); + return FALSE; + } + + CreateDirectoryW(serviceUpdaterPath, nullptr); + + if (!PathAppendSafe(serviceUpdaterPath, L"updater.exe")) { + LOG_WARN( + ("Couldn't append file spec when attempting to use a secure " + "updater path. (%lu)", + GetLastError())); + return FALSE; + } + + return TRUE; +} + +/** + * Deletes the passed in updater path and the associated updater.ini file. + * + * @param serviceUpdaterPath The path to delete. + * @return TRUE if a file was deleted. + */ +BOOL DeleteSecureUpdater(WCHAR serviceUpdaterPath[MAX_PATH + 1]) { + BOOL result = FALSE; + if (serviceUpdaterPath[0]) { + result = DeleteFileW(serviceUpdaterPath); + if (!result && GetLastError() != ERROR_PATH_NOT_FOUND && + GetLastError() != ERROR_FILE_NOT_FOUND) { + LOG_WARN(("Could not delete service updater path: '%ls'.", + serviceUpdaterPath)); + } + + WCHAR updaterINIPath[MAX_PATH + 1] = {L'\0'}; + if (PathGetSiblingFilePath(updaterINIPath, serviceUpdaterPath, + L"updater.ini")) { + result = DeleteFileW(updaterINIPath); + if (!result && GetLastError() != ERROR_PATH_NOT_FOUND && + GetLastError() != ERROR_FILE_NOT_FOUND) { + LOG_WARN(("Could not delete service updater INI path: '%ls'.", + updaterINIPath)); + } + } + } + return result; +} + +/** + * Executes a service command. + * + * @param argc The number of arguments in argv + * @param argv The service command line arguments, argv[0] is automatically + * included by Windows, argv[1] is unused but hardcoded to + * "MozillaMaintenance", and argv[2] is the service command. + * + * @return FALSE if there was an error executing the service command. + */ +BOOL ExecuteServiceCommand(int argc, LPWSTR* argv) { + if (argc < 3) { + LOG_WARN( + ("Not enough command line arguments to execute a service command")); + return FALSE; + } + + // The tests work by making sure the log has changed, so we put a + // unique ID in the log. + WCHAR uuidString[MAX_PATH + 1] = {L'\0'}; + if (GetUUIDString(uuidString)) { + LOG(("Executing service command %ls, ID: %ls", argv[2], uuidString)); + } else { + // The ID is only used by tests, so failure to allocate it isn't fatal. + LOG(("Executing service command %ls", argv[2])); + } + + BOOL result = FALSE; + if (!lstrcmpi(argv[2], L"software-update")) { + // This check is also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (argc <= 4 || !IsValidFullPath(argv[4])) { + // Since the status file is written to the patch directory and the patch + // directory is invalid don't write the status file. + LOG_WARN(("The patch directory path is not valid for this application.")); + return FALSE; + } + + // The patch directory path must end with updates\0 to use the maintenance + // service. + size_t fullPathLen = NS_tstrlen(argv[4]); + size_t relPathLen = NS_tstrlen(PATCH_DIR_PATH); + if (relPathLen > fullPathLen) { + LOG_WARN( + ("The patch directory path length is not valid for this " + "application.")); + return FALSE; + } + + if (_wcsnicmp(argv[4] + fullPathLen - relPathLen, PATCH_DIR_PATH, + relPathLen) != 0) { + LOG_WARN( + ("The patch directory path subdirectory is not valid for this " + "application.")); + return FALSE; + } + + // Remove the secure output files so it is easier to determine when new + // files are created in the unelevated updater. + RemoveSecureOutputFiles(argv[4]); + + // Create a new secure ID for this update. + if (!WriteSecureIDFile(argv[4])) { + LOG_WARN(("Unable to write to secure ID file.")); + return FALSE; + } + + // This check is also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (argc <= 5 || !IsValidFullPath(argv[5]) + // This build flag is used as a handy proxy to tell when we're a build made + // for local testing, because there isn't much other reason to set it. +#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK + || !IsProgramFilesPath(argv[5]) +#endif + ) { + LOG_WARN( + ("The install directory path is not valid for this application.")); + if (!WriteStatusFailure(argv[4], + SERVICE_INVALID_INSTALL_DIR_PATH_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + if (!IsOldCommandline(argc - 3, argv + 3)) { + // This check is also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (argc <= 6 || !IsValidFullPath(argv[6])) { + LOG_WARN( + ("The working directory path is not valid for this application.")); + if (!WriteStatusFailure(argv[4], + SERVICE_INVALID_WORKING_DIR_PATH_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + // These checks are also performed in updater.cpp and is performed here + // as well since the maintenance service can be called directly. + if (_wcsnicmp(argv[6], argv[5], MAX_PATH) != 0) { + if (argc <= 7 || + (wcscmp(argv[7], L"-1") != 0 && !wcsstr(argv[7], L"/replace"))) { + LOG_WARN( + ("Installation directory and working directory must be the " + "same for non-staged updates. Exiting.")); + if (!WriteStatusFailure(argv[4], SERVICE_INVALID_APPLYTO_DIR_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + NS_tchar workingDirParent[MAX_PATH]; + NS_tsnprintf(workingDirParent, + sizeof(workingDirParent) / sizeof(workingDirParent[0]), + NS_T("%s"), argv[6]); + if (!PathRemoveFileSpecW(workingDirParent)) { + LOG_WARN( + ("Couldn't remove file spec when attempting to verify the " + "working directory path. (%lu)", + GetLastError())); + if (!WriteStatusFailure(argv[4], REMOVE_FILE_SPEC_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + if (_wcsnicmp(workingDirParent, argv[5], MAX_PATH) != 0) { + LOG_WARN( + ("The apply-to directory must be the same as or " + "a child of the installation directory! Exiting.")); + if (!WriteStatusFailure(argv[4], + SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + } + } + + // Use the passed in command line arguments for the update, except for the + // path to updater.exe. We always look for updater.exe in the installation + // directory, then we copy that updater.exe to a the directory of the + // MozillaMaintenance service so that a low integrity process cannot + // replace the updater.exe at any point and use that for the update. + // It also makes DLL injection attacks harder. + WCHAR installDir[MAX_PATH + 1] = {L'\0'}; + if (!GetInstallationDir(argc - 3, argv + 3, installDir)) { + LOG_WARN(("Could not get the installation directory")); + if (!WriteStatusFailure(argv[4], SERVICE_INSTALLDIR_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + + if (!DoesFallbackKeyExist()) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (CalculateRegistryPathFromFilePath(installDir, + maintenanceServiceKey)) { + LOG(("Checking for Maintenance Service registry. key: '%ls'", + maintenanceServiceKey)); + HKEY baseKey = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, + &baseKey) != ERROR_SUCCESS) { + LOG_WARN(("The maintenance service registry key does not exist.")); + if (!WriteStatusFailure(argv[4], SERVICE_INSTALL_DIR_REG_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + RegCloseKey(baseKey); + } else { + if (!WriteStatusFailure(argv[4], SERVICE_CALC_REG_PATH_ERROR)) { + LOG_WARN(("Could not write update.status for previous failure.")); + } + return FALSE; + } + } + + WCHAR installDirUpdater[MAX_PATH + 1] = {L'\0'}; + wcsncpy(installDirUpdater, installDir, MAX_PATH); + if (!PathAppendSafe(installDirUpdater, L"updater.exe")) { + LOG_WARN(("Install directory updater could not be determined.")); + result = FALSE; + } + + result = UpdaterIsValid(installDirUpdater, installDir, argv[4]); + + WCHAR secureUpdaterPath[MAX_PATH + 1] = {L'\0'}; + if (result) { + result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging + } + if (result) { + LOG( + ("Passed in path: '%ls' (ignored); " + "Install dir has: '%ls'; " + "Using this path for updating: '%ls'.", + argv[3], installDirUpdater, secureUpdaterPath)); + DeleteSecureUpdater(secureUpdaterPath); + result = CopyFileW(installDirUpdater, secureUpdaterPath, FALSE); + } + + if (!result) { + LOG_WARN( + ("Could not copy path to secure location. (%lu)", GetLastError())); + if (!WriteStatusFailure(argv[4], SERVICE_COULD_NOT_COPY_UPDATER)) { + LOG_WARN( + ("Could not write update.status could not copy updater error")); + } + } else { + // We obtained the path and copied it successfully, update the path to + // use for the service update. + argv[3] = secureUpdaterPath; + + WCHAR installDirUpdaterINIPath[MAX_PATH + 1] = {L'\0'}; + WCHAR secureUpdaterINIPath[MAX_PATH + 1] = {L'\0'}; + if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath, + L"updater.ini") && + PathGetSiblingFilePath(installDirUpdaterINIPath, installDirUpdater, + L"updater.ini")) { + // This is non fatal if it fails there is no real harm + if (!CopyFileW(installDirUpdaterINIPath, secureUpdaterINIPath, FALSE)) { + LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%lu)", + installDirUpdaterINIPath, secureUpdaterINIPath, + GetLastError())); + } + } + + result = ProcessSoftwareUpdateCommand(argc - 3, argv + 3); + DeleteSecureUpdater(secureUpdaterPath); + } + + // We might not reach here if the service install succeeded + // because the service self updates itself and the service + // installer will stop the service. + LOG(("Service command %ls complete.", argv[2])); + } else { + LOG_WARN(("Service command not recognized: %ls.", argv[2])); + // result is already set to FALSE + } + + LOG(("service command %ls complete with result: %ls.", argv[1], + (result ? L"Success" : L"Failure"))); + return result; +} diff --git a/toolkit/components/maintenanceservice/workmonitor.h b/toolkit/components/maintenanceservice/workmonitor.h new file mode 100644 index 0000000000..8fc2e51a7d --- /dev/null +++ b/toolkit/components/maintenanceservice/workmonitor.h @@ -0,0 +1,5 @@ +/* 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/. */ + +BOOL ExecuteServiceCommand(int argc, LPWSTR* argv); -- cgit v1.2.3