diff options
author | Pale Moon <git-repo@palemoon.org> | 2016-10-13 09:28:23 +0200 |
---|---|---|
committer | Pale Moon <git-repo@palemoon.org> | 2016-10-13 09:43:49 +0200 |
commit | f39a7473bb52ebfd9a6571ab6c2ac7f6ba0a71ea (patch) | |
tree | db92f2994eaac281d2d0962412dfc69dd6009d13 | |
parent | ae4c8cc4559be4c8820b20111339ef702e5b4dd7 (diff) | |
download | palemoon-gre-f39a7473bb52ebfd9a6571ab6c2ac7f6ba0a71ea.tar.gz |
Revert "Remove the maintenance service (take 2)."
This reverts commit 009fdf9eb83e29aceec430ccc6c1132e32b23185.
Tag #492.
61 files changed, 5633 insertions, 31 deletions
diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index 35f7a1816..c9aae3841 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -806,6 +806,13 @@ bin/libfreebl_32int64_3.so #endif #endif +; [MaintenanceService] +; +#ifdef MOZ_MAINTENANCE_SERVICE +@BINPATH@/maintenanceservice.exe +@BINPATH@/maintenanceservice_installer.exe +#endif + @RESPATH@/components/DataStore.manifest @RESPATH@/components/DataStoreImpl.js @RESPATH@/components/dom_datastore.xpt diff --git a/browser/installer/windows/nsis/installer.nsi b/browser/installer/windows/nsis/installer.nsi index b5bd62605..8afccb91b 100644 --- a/browser/installer/windows/nsis/installer.nsi +++ b/browser/installer/windows/nsis/installer.nsi @@ -1018,6 +1018,25 @@ Function .onInit WriteINIStr "$PLUGINSDIR\shortcuts.ini" "Field 4" State "1" ${EndUnless} + ; Setup the components.ini file for the Components Page + WriteINIStr "$PLUGINSDIR\components.ini" "Settings" NumFields "2" + + WriteINIStr "$PLUGINSDIR\components.ini" "Field 1" Type "label" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 1" Text "$(OPTIONAL_COMPONENTS_DESC)" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 1" Left "0" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 1" Right "-1" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 1" Top "5" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 1" Bottom "25" + + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Type "checkbox" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Text "$(MAINTENANCE_SERVICE_CHECKBOX_DESC)" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Left "0" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Right "-1" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Top "27" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Bottom "37" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" State "1" + WriteINIStr "$PLUGINSDIR\components.ini" "Field 2" Flags "GROUP" + ; There must always be a core directory. ${GetSize} "$EXEDIR\core\" "/S=0K" $R5 $R7 $R8 SectionSetSize ${APP_IDX} $R5 diff --git a/configure.in b/configure.in index 8fb2d0017..242a955ca 100644 --- a/configure.in +++ b/configure.in @@ -6184,6 +6184,23 @@ if test -n "$MOZ_SIGN_CMD"; then fi dnl ======================================================== +dnl Maintenance Service +dnl ======================================================== + +MOZ_ARG_ENABLE_BOOL(maintenance-service, +[ --enable-maintenance-service Enable building of maintenanceservice], + MOZ_MAINTENANCE_SERVICE=1, + MOZ_MAINTENANCE_SERVICE= ) + +if test -n "$MOZ_MAINTENANCE_SERVICE"; then + if test "$OS_ARCH" = "WINNT"; then + AC_DEFINE(MOZ_MAINTENANCE_SERVICE) + else + AC_MSG_ERROR([Can only build with --enable-maintenance-service with a Windows target]) + fi +fi + +dnl ======================================================== dnl Bundled fonts on desktop platforms dnl ======================================================== @@ -8244,6 +8261,7 @@ AC_SUBST(ACCESSIBILITY) AC_SUBST(MOZ_SPELLCHECK) AC_SUBST(MOZ_ANDROID_APZ) AC_SUBST(MOZ_ANDROID_ANR_REPORTER) +AC_SUBST(MOZ_MAINTENANCE_SERVICE) AC_SUBST(MOZ_VERIFY_MAR_SIGNATURE) AC_SUBST(MOZ_ENABLE_SIGNMAR) AC_SUBST(MOZ_UPDATER) diff --git a/toolkit/components/maintenanceservice/Makefile.in b/toolkit/components/maintenanceservice/Makefile.in new file mode 100644 index 000000000..32f2dff91 --- /dev/null +++ b/toolkit/components/maintenanceservice/Makefile.in @@ -0,0 +1,15 @@ +# 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/. + +DIST_PROGRAM = maintenanceservice$(BIN_SUFFIX) + +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 000000000..9e831dc9c --- /dev/null +++ b/toolkit/components/maintenanceservice/bootstrapinstaller/maintenanceservice_installer.nsi @@ -0,0 +1,278 @@ +# 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 + +; The commands inside this ifdef require NSIS 3.0a2 or greater so the ifdef can +; be removed after we require NSIS 3.0a2 or greater. +!ifdef NSIS_PACKEDVERSION + Unicode true + ManifestSupportedOS all + ManifestDPIAware true +!endif + +!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 slaves 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" +!undef BrandFullName +!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 + ; On Windows 2000 we do not install the maintenance service. + ; We won't run this installer from the parent installer, but just in case + ; someone tries to execute it on Windows 2000... + ${Unless} ${AtLeastWinXP} + 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 "")' + +; The commands inside this ifndef are needed prior to NSIS 3.0a2 and can be +; removed after we require NSIS 3.0a2 or greater. +!ifndef NSIS_PACKEDVERSION + ${If} ${AtLeastWinVista} + System::Call 'user32::SetProcessDPIAware()' + ${EndIf} +!endif + + 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} + 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} + 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} + SetRegView 64 + ${EndIf} + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "Installed" + DeleteRegValue HKLM "Software\Mozilla\MaintenanceService" "FFPrefetchDisabled" + DeleteRegKey HKLM "${FallbackKey}\" + ${If} ${RunningX64} + SetRegView lastused + ${EndIf} +SectionEnd + diff --git a/toolkit/components/maintenanceservice/maintenanceservice.cpp b/toolkit/components/maintenanceservice/maintenanceservice.cpp new file mode 100644 index 000000000..e79df071f --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.cpp @@ -0,0 +1,386 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <shlwapi.h> +#include <stdio.h> +#include <wchar.h> +#include <shlobj.h> + +#include "serviceinstall.h" +#include "maintenanceservice.h" +#include "servicebase.h" +#include "workmonitor.h" +#include "uachelper.h" +#include "updatehelper.h" + +// Link w/ subsystem window so we don't get a console when executing +// this binary through the installer. +#pragma comment(linker, "/SUBSYSTEM:windows") + +SERVICE_STATUS gSvcStatus = { 0 }; +SERVICE_STATUS_HANDLE gSvcStatusHandle = nullptr; +HANDLE gWorkDoneEvent = nullptr; +HANDLE gThread = nullptr; +bool gServiceControlStopping = false; + +// logs are pretty small, about 20 lines, so 10 seems reasonable. +#define LOGS_TO_KEEP 10 + +BOOL GetLogDirectoryPath(WCHAR *path); + +int +wmain(int argc, WCHAR **argv) +{ + // If command-line parameter is "install", install the service + // or upgrade if already installed + // If command line parameter is "forceinstall", install the service + // even if it is older than what is already installed. + // If command-line parameter is "upgrade", upgrade the service + // but do not install it if it is not already installed. + // If command line parameter is "uninstall", uninstall the service. + // Otherwise, the service is probably being started by the SCM. + bool forceInstall = !lstrcmpi(argv[1], L"forceinstall"); + if (!lstrcmpi(argv[1], L"install") || forceInstall) { + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + LogInit(updatePath, L"maintenanceservice-install.log"); + } + + SvcInstallAction action = InstallSvc; + if (forceInstall) { + action = ForceInstallSvc; + LOG(("Installing service with force specified...")); + } else { + LOG(("Installing service...")); + } + + bool ret = SvcInstall(action); + if (!ret) { + LOG_WARN(("Could not install service. (%d)", GetLastError())); + LogFinish(); + return 1; + } + + LOG(("The service was installed successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"upgrade")) { + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + LogInit(updatePath, L"maintenanceservice-install.log"); + } + + LOG(("Upgrading service if installed...")); + if (!SvcInstall(UpgradeSvc)) { + LOG_WARN(("Could not upgrade service. (%d)", GetLastError())); + LogFinish(); + return 1; + } + + LOG(("The service was upgraded successfully")); + LogFinish(); + return 0; + } + + if (!lstrcmpi(argv[1], L"uninstall")) { + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + LogInit(updatePath, L"maintenanceservice-uninstall.log"); + } + LOG(("Uninstalling service...")); + if (!SvcUninstall()) { + LOG_WARN(("Could not uninstall service. (%d)", GetLastError())); + LogFinish(); + return 1; + } + LOG(("The service was uninstalled successfully")); + LogFinish(); + return 0; + } + + SERVICE_TABLE_ENTRYW DispatchTable[] = { + { SVC_NAME, (LPSERVICE_MAIN_FUNCTIONW) SvcMain }, + { nullptr, nullptr } + }; + + // This call returns when the service has stopped. + // The process should simply terminate when the call returns. + if (!StartServiceCtrlDispatcherW(DispatchTable)) { + LOG_WARN(("StartServiceCtrlDispatcher failed. (%d)", GetLastError())); + } + + return 0; +} + +/** + * Obtains the base path where logs should be stored + * + * @param path The out buffer for the backup log path of size MAX_PATH + 1 + * @return TRUE if successful. + */ +BOOL +GetLogDirectoryPath(WCHAR *path) +{ + if (!GetModuleFileNameW(nullptr, path, MAX_PATH)) { + return FALSE; + } + + if (!PathRemoveFileSpecW(path)) { + return FALSE; + } + + if (!PathAppendSafe(path, L"logs")) { + return FALSE; + } + CreateDirectoryW(path, nullptr); + return TRUE; +} + +/** + * Calculated a backup path based on the log number. + * + * @param path The out buffer to store the log path of size MAX_PATH + 1 + * @param basePath The base directory where the calculated path should go + * @param logNumber The log number, 0 == updater.log + * @return TRUE if successful. + */ +BOOL +GetBackupLogPath(LPWSTR path, LPCWSTR basePath, int logNumber) +{ + WCHAR logName[64] = { L'\0' }; + wcsncpy(path, basePath, sizeof(logName) / sizeof(logName[0]) - 1); + if (logNumber <= 0) { + swprintf(logName, sizeof(logName) / sizeof(logName[0]), + L"maintenanceservice.log"); + } else { + swprintf(logName, sizeof(logName) / sizeof(logName[0]), + L"maintenanceservice-%d.log", logNumber); + } + return PathAppendSafe(path, logName); +} + +/** + * Moves the old log files out of the way before a new one is written. + * If you for example keep 3 logs, then this function will do: + * updater2.log -> updater3.log + * updater1.log -> updater2.log + * updater.log -> updater1.log + * Which clears room for a new updater.log in the basePath directory + * + * @param basePath The base directory path where log files are stored + * @param numLogsToKeep The number of logs to keep + */ +void +BackupOldLogs(LPCWSTR basePath, int numLogsToKeep) +{ + WCHAR oldPath[MAX_PATH + 1]; + WCHAR newPath[MAX_PATH + 1]; + for (int i = numLogsToKeep; i >= 1; i--) { + if (!GetBackupLogPath(oldPath, basePath, i -1)) { + continue; + } + + if (!GetBackupLogPath(newPath, basePath, i)) { + continue; + } + + if (!MoveFileExW(oldPath, newPath, MOVEFILE_REPLACE_EXISTING)) { + continue; + } + } +} + +/** + * Ensures the service is shutdown once all work is complete. + * There is an issue on XP SP2 and below where the service can hang + * in a stop pending state even though the SCM is notified of a stopped + * state. Control *should* be returned to StartServiceCtrlDispatcher from the + * call to SetServiceStatus on a stopped state in the wmain thread. + * Sometimes this is not the case though. This thread will terminate the process + * if it has been 5 seconds after all work is done and the process is still not + * terminated. This thread is only started once a stopped state was sent to the + * SCM. The stop pending hang can be reproduced intermittently even if you set + * a stopped state dirctly and never set a stop pending state. It is safe to + * forcefully terminate the process ourselves since all work is done once we + * start this thread. +*/ +DWORD WINAPI +EnsureProcessTerminatedThread(LPVOID) +{ + Sleep(5000); + exit(0); + return 0; +} + +void +StartTerminationThread() +{ + // If the process does not self terminate like it should, this thread + // will terminate the process after 5 seconds. + HANDLE thread = CreateThread(nullptr, 0, EnsureProcessTerminatedThread, + nullptr, 0, nullptr); + if (thread) { + CloseHandle(thread); + } +} + +/** + * Main entry point when running as a service. + */ +void WINAPI +SvcMain(DWORD argc, LPWSTR *argv) +{ + // Setup logging, and backup the old logs + WCHAR updatePath[MAX_PATH + 1]; + if (GetLogDirectoryPath(updatePath)) { + BackupOldLogs(updatePath, LOGS_TO_KEEP); + LogInit(updatePath, L"maintenanceservice.log"); + } + + // Disable every privilege we don't need. Processes started using + // CreateProcess will use the same token as this process. + UACHelper::DisablePrivileges(nullptr); + + // Register the handler function for the service + gSvcStatusHandle = RegisterServiceCtrlHandlerW(SVC_NAME, SvcCtrlHandler); + if (!gSvcStatusHandle) { + LOG_WARN(("RegisterServiceCtrlHandler failed. (%d)", GetLastError())); + ExecuteServiceCommand(argc, argv); + LogFinish(); + exit(1); + } + + // These values will be re-used later in calls involving gSvcStatus + gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + gSvcStatus.dwServiceSpecificExitCode = 0; + + // Report initial status to the SCM + ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000); + + // This event will be used to tell the SvcCtrlHandler when the work is + // done for when a stop comamnd is manually issued. + gWorkDoneEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (!gWorkDoneEvent) { + ReportSvcStatus(SERVICE_STOPPED, 1, 0); + StartTerminationThread(); + return; + } + + // Initialization complete and we're about to start working on + // the actual command. Report the service state as running to the SCM. + ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0); + + // The service command was executed, stop logging and set an event + // to indicate the work is done in case someone is waiting on a + // service stop operation. + ExecuteServiceCommand(argc, argv); + LogFinish(); + + SetEvent(gWorkDoneEvent); + + // If we aren't already in a stopping state then tell the SCM we're stopped + // now. If we are already in a stopping state then the SERVICE_STOPPED state + // will be set by the SvcCtrlHandler. + if (!gServiceControlStopping) { + ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); + StartTerminationThread(); + } +} + +/** + * Sets the current service status and reports it to the SCM. + * + * @param currentState The current state (see SERVICE_STATUS) + * @param exitCode The system error code + * @param waitHint Estimated time for pending operation in milliseconds + */ +void +ReportSvcStatus(DWORD currentState, + DWORD exitCode, + DWORD waitHint) +{ + static DWORD dwCheckPoint = 1; + + gSvcStatus.dwCurrentState = currentState; + gSvcStatus.dwWin32ExitCode = exitCode; + gSvcStatus.dwWaitHint = waitHint; + + if (SERVICE_START_PENDING == currentState || + SERVICE_STOP_PENDING == currentState) { + gSvcStatus.dwControlsAccepted = 0; + } else { + gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP | + SERVICE_ACCEPT_SHUTDOWN; + } + + if ((SERVICE_RUNNING == currentState) || + (SERVICE_STOPPED == currentState)) { + gSvcStatus.dwCheckPoint = 0; + } else { + gSvcStatus.dwCheckPoint = dwCheckPoint++; + } + + // Report the status of the service to the SCM. + SetServiceStatus(gSvcStatusHandle, &gSvcStatus); +} + +/** + * Since the SvcCtrlHandler should only spend at most 30 seconds before + * returning, this function does the service stop work for the SvcCtrlHandler. +*/ +DWORD WINAPI +StopServiceAndWaitForCommandThread(LPVOID) +{ + do { + ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000); + } while(WaitForSingleObject(gWorkDoneEvent, 100) == WAIT_TIMEOUT); + CloseHandle(gWorkDoneEvent); + gWorkDoneEvent = nullptr; + ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0); + StartTerminationThread(); + return 0; +} + +/** + * Called by SCM whenever a control code is sent to the service + * using the ControlService function. + */ +void WINAPI +SvcCtrlHandler(DWORD dwCtrl) +{ + // After a SERVICE_CONTROL_STOP there should be no more commands sent to + // the SvcCtrlHandler. + if (gServiceControlStopping) { + return; + } + + // Handle the requested control code. + switch(dwCtrl) { + case SERVICE_CONTROL_SHUTDOWN: + case SERVICE_CONTROL_STOP: { + gServiceControlStopping = true; + ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 1000); + + // The SvcCtrlHandler thread should not spend more than 30 seconds in + // shutdown so we spawn a new thread for stopping the service + HANDLE thread = CreateThread(nullptr, 0, + StopServiceAndWaitForCommandThread, + nullptr, 0, nullptr); + if (thread) { + CloseHandle(thread); + } else { + // Couldn't start the thread so just call the stop ourselves. + // If it happens to take longer than 30 seconds the caller will + // get an error. + StopServiceAndWaitForCommandThread(nullptr); + } + } + break; + default: + break; + } +} diff --git a/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest new file mode 100644 index 000000000..cb317c47d --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.exe.manifest @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> +<assemblyIdentity + version="1.0.0.0" + processorArchitecture="*" + name="MaintenanceService" + type="win32" +/> +<description>MaintenanceService</description> +<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3"> + <ms_asmv3:security> + <ms_asmv3:requestedPrivileges> + <ms_asmv3:requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> + </ms_asmv3:requestedPrivileges> + </ms_asmv3:security> +</ms_asmv3:trustInfo> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> + </application> + </compatibility> +</assembly> diff --git a/toolkit/components/maintenanceservice/maintenanceservice.h b/toolkit/components/maintenanceservice/maintenanceservice.h new file mode 100644 index 000000000..9e02914a0 --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.h @@ -0,0 +1,10 @@ +/* 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/. */ + +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); diff --git a/toolkit/components/maintenanceservice/maintenanceservice.rc b/toolkit/components/maintenanceservice/maintenanceservice.rc new file mode 100644 index 000000000..ddd3e942b --- /dev/null +++ b/toolkit/components/maintenanceservice/maintenanceservice.rc @@ -0,0 +1,86 @@ +/* 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 + +///////////////////////////////////////////////////////////////////////////// +// +// RT_MANIFEST +// + +1 RT_MANIFEST "maintenanceservice.exe.manifest" + +///////////////////////////////////////////////////////////////////////////// +// +// 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 000000000..21990b5ab --- /dev/null +++ b/toolkit/components/maintenanceservice/moz.build @@ -0,0 +1,53 @@ +# -*- 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 http://mozilla.org/MPL/2.0/. + +Program('maintenanceservice') + +SOURCES += [ + 'maintenanceservice.cpp', + 'servicebase.cpp', + 'serviceinstall.cpp', + 'workmonitor.cpp', +] + +if CONFIG['OS_ARCH'] == 'WINNT': + USE_LIBS += [ + 'updatecommon-standalone', + ] +else: + USE_LIBS += [ + 'updatecommon', + ] + +# For debugging purposes only +#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['_MSC_VER']: + WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup'] + +RCINCLUDE = 'maintenanceservice.rc' + +DISABLE_STL_WRAPPING = True + +OS_LIBS += [ + 'comctl32', + 'ws2_32', + 'shell32', +] + +FAIL_ON_WARNINGS = True diff --git a/toolkit/components/maintenanceservice/resource.h b/toolkit/components/maintenanceservice/resource.h new file mode 100644 index 000000000..45619457c --- /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 000000000..a858c4537 --- /dev/null +++ b/toolkit/components/maintenanceservice/servicebase.cpp @@ -0,0 +1,86 @@ +/* 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 000000000..dfe04ed29 --- /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 <windows.h> +#include "updatelogging.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 000000000..fda3e7bbd --- /dev/null +++ b/toolkit/components/maintenanceservice/serviceinstall.cpp @@ -0,0 +1,733 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include <windows.h> +#include <aclapi.h> +#include <stdlib.h> +#include <shlwapi.h> + +// Used for DNLEN and UNLEN +#include <lm.h> + +#include <nsWindowsHelpers.h> +#include "mozilla/UniquePtr.h" + +#include "serviceinstall.h" +#include "servicebase.h" +#include "updatehelper.h" +#include "shellapi.h" +#include "readstrings.h" +#include "errors.h" + +#pragma comment(lib, "version.lib") + +/** + * 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"; + char serviceStrings[kNumStrings][MAX_TEXT_LEN]; + int result = ReadStrings(path, kServiceKeys, + kNumStrings, serviceStrings); + if (result != OK) { + serviceStrings[0][0] = '\0'; + } + strncpy(results->serviceDescription, + serviceStrings[0], MAX_TEXT_LEN - 1); + results->serviceDescription[MAX_TEXT_LEN - 1] = '\0'; + 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<char[]> fileVersionInfo(new char[fileVersionInfoSize]); + if (!GetFileVersionInfoW(path, 0, fileVersionInfoSize, + fileVersionInfo.get())) { + LOG_WARN(("Could not obtain file info of old service. (%d)", + GetLastError())); + return FALSE; + } + + VS_FIXEDFILEINFO *fixedFileInfo = + reinterpret_cast<VS_FIXEDFILEINFO *>(fileVersionInfo.get()); + UINT size; + if (!VerQueryValueW(fileVersionInfo.get(), L"\\", + reinterpret_cast<LPVOID*>(&fixedFileInfo), &size)) { + LOG_WARN(("Could not query file version info of old service. (%d)", + 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. (%d)", GetLastError())); + return FALSE; + } + + if (!PathRemoveFileSpecW(updaterINIPath)) { + LOG_WARN(("Could not remove file spec when attempting to " + "modify service description. (%d)", GetLastError())); + return FALSE; + } + + if (!PathAppendSafe(updaterINIPath, L"updater.ini")) { + LOG_WARN(("Could not append updater.ini filename when attempting to " + "modify service description. (%d)", GetLastError())); + return FALSE; + } + + if (GetFileAttributesW(updaterINIPath) == INVALID_FILE_ATTRIBUTES) { + LOG_WARN(("updater.ini file does not exist, will not modify " + "service description. (%d)", GetLastError())); + return FALSE; + } + + MaintenanceServiceStringTable serviceStrings; + int rv = ReadMaintenanceServiceStrings(updaterINIPath, &serviceStrings); + if (rv != OK || !strlen(serviceStrings.serviceDescription)) { + LOG_WARN(("updater.ini file does not contain a maintenance " + "service description.")); + return FALSE; + } + + WCHAR serviceDescription[MAX_TEXT_LEN]; + if (!MultiByteToWideChar(CP_UTF8, 0, + serviceStrings.serviceDescription, -1, + serviceDescription, + sizeof(serviceDescription) / + sizeof(serviceDescription[0]))) { + LOG_WARN(("Could not convert description to wide string format. (%d)", + GetLastError())); + return FALSE; + } + + SERVICE_DESCRIPTIONW descriptionConfig; + descriptionConfig.lpDescription = serviceDescription; + if (!ChangeServiceConfig2W(serviceHandle, + SERVICE_CONFIG_DESCRIPTION, + &descriptionConfig)) { + LOG_WARN(("Could not change service config. (%d)", 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. (%d)", GetLastError())); + return FALSE; + } + if (!PathAppendSafe(fixedPath, L"maintenanceservice.exe")) { + LOG_WARN(("Couldn't append file spec. (%d)", 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. (%d)", 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. (%d)", 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. (%d)", + 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. (%d)", 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. (%d)", 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. (%d)", + GetLastError())); + return FALSE; + } + + // Get the service config information, in particular we want the binary + // path of the service. + mozilla::UniquePtr<char[]> serviceConfigBuffer(new char[bytesNeeded]); + if (!QueryServiceConfigW(schService, + reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()), + bytesNeeded, &bytesNeeded)) { + LOG_WARN(("Could open service but could not query service config. (%d)", + GetLastError())); + return FALSE; + } + QUERY_SERVICE_CONFIGW &serviceConfig = + *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(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. (%d)", + 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. (%d)", 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. (%d)", + 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 { + 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. (%d)", 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. (%d)", + 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. (%d)", GetLastError())); + return FALSE; + } + + // Open the service + nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME, + SERVICE_ALL_ACCESS)); + if (!schService) { + LOG_WARN(("Could not open service. (%d)", 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. (%d)", 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: %d", 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. (%d)", GetLastError())); + return FALSE; + } + + // Open the service + nsAutoServiceHandle schService(OpenServiceW(schSCManager, SVC_NAME, + SERVICE_ALL_ACCESS)); + if (!schService) { + LOG_WARN(("Could not open service. (%d)", 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. (%d)", + GetLastError())); + return GetLastError(); + } + + DWORD size = needed; + psd = (PSECURITY_DESCRIPTOR)LocalAlloc(LPTR, size); + if (!psd) { + LOG_WARN(("Could not allocate security descriptor. (%d)", + 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. (%d)", + 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. (%d)", GetLastError())); + return GetLastError(); + } + + PSID sid; + DWORD SIDSize = SECURITY_MAX_SID_SIZE; + sid = LocalAlloc(LMEM_FIXED, SIDSize); + if (!sid) { + LOG_WARN(("Could not allocate SID memory. (%d)", GetLastError())); + return GetLastError(); + } + + if (!CreateWellKnownSid(WinBuiltinUsersSid, nullptr, sid, &SIDSize)) { + DWORD lastError = GetLastError(); + LOG_WARN(("Could not create well known SID. (%d)", lastError)); + LocalFree(sid); + return lastError; + } + + // Lookup the account name, the function fails if you don't pass in + // a buffer for the domain name but it's not used since we're using + // the built in account Sid. + SID_NAME_USE accountType; + WCHAR accountName[UNLEN + 1] = { L'\0' }; + WCHAR domainName[DNLEN + 1] = { L'\0' }; + DWORD accountNameSize = UNLEN + 1; + DWORD domainNameSize = DNLEN + 1; + if (!LookupAccountSidW(nullptr, sid, accountName, + &accountNameSize, + domainName, &domainNameSize, &accountType)) { + LOG_WARN(("Could not lookup account Sid, will try Users. (%d)", + GetLastError())); + wcsncpy(accountName, L"Users", UNLEN); + } + + // We already have the group name so we can get rid of the SID + FreeSid(sid); + sid = nullptr; + + // Build the ACE, BuildExplicitAccessWithName cannot fail so it is not logged. + EXPLICIT_ACCESS ea; + BuildExplicitAccessWithNameW(&ea, accountName, + SERVICE_START | SERVICE_STOP | GENERIC_READ, + SET_ACCESS, NO_INHERITANCE); + DWORD lastError = SetEntriesInAclW(1, (PEXPLICIT_ACCESS)&ea, pacl, &pNewAcl); + if (ERROR_SUCCESS != lastError) { + LOG_WARN(("Could not set entries in ACL. (%d)", lastError)); + return lastError; + } + + // Initialize a new security descriptor. + SECURITY_DESCRIPTOR sd; + if (!InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION)) { + LOG_WARN(("Could not initialize security descriptor. (%d)", + 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. (%d)", + 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. (%d)", + 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 000000000..d8532a968 --- /dev/null +++ b/toolkit/components/maintenanceservice/serviceinstall.h @@ -0,0 +1,21 @@ +/* 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 +{ + char serviceDescription[MAX_TEXT_LEN]; +}; + diff --git a/toolkit/components/maintenanceservice/workmonitor.cpp b/toolkit/components/maintenanceservice/workmonitor.cpp new file mode 100644 index 000000000..39044a6c4 --- /dev/null +++ b/toolkit/components/maintenanceservice/workmonitor.cpp @@ -0,0 +1,635 @@ +/* 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 <shlobj.h> +#include <shlwapi.h> +#include <wtsapi32.h> +#include <userenv.h> +#include <shellapi.h> + +#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") + +#include "nsWindowsHelpers.h" + +#include "workmonitor.h" +#include "serviceinstall.h" +#include "servicebase.h" +#include "registrycertificates.h" +#include "uachelper.h" +#include "updatehelper.h" +#include "errors.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; +wchar_t* MakeCommandLine(int argc, wchar_t** argv); +BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode); +BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer, LPCWSTR siblingFilePath, + LPCWSTR newFileName); + +/* + * Read the update.status file and sets isApplying to true if + * the status is set to applying. + * + * @param updateDirPath The directory where update.status is stored + * @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 updateDirPath, BOOL &isApplying) +{ + isApplying = FALSE; + WCHAR updateStatusFilePath[MAX_PATH + 1] = {L'\0'}; + wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH); + if (!PathAppendSafe(updateStatusFilePath, L"update.status")) { + LOG_WARN(("Could not append path for update.status file")); + return FALSE; + } + + nsAutoHandle statusFile(CreateFileW(updateStatusFilePath, 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; + } + + LOG(("updater.exe returned status: %s", buf)); + + 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) +{ + LOG(("Starting update process as the service in session 0.")); + STARTUPINFO si = {0}; + si.cb = sizeof(STARTUPINFO); + si.lpDesktop = L"winsta0\\Default"; + PROCESS_INFORMATION pi = {0}; + + // The updater command line is of the form: + // updater.exe update-dir apply [wait-pid [callback-dir callback-path args]] + LPWSTR cmdLine = 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 = L""; + 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<char*>("MOZ_USING_SERVICE=1")); + LOG(("Starting service with cmdline: %ls", cmdLine)); + processStarted = CreateProcessW(argv[0], cmdLine, + nullptr, nullptr, FALSE, + CREATE_DEFAULT_ERROR_MODE, + nullptr, + nullptr, &si, &pi); + + BOOL updateWasSuccessful = FALSE; + if (processStarted) { + // 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); + } 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 %d.", returnCode)); + // updater returns 0 if successful. + updateWasSuccessful = (returnCode == 0); + } else { + LOG_WARN(("Process finished but could not obtain return code.")); + } + } + 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 know 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.")); + if (!WriteStatusFailure(argv[1], + SERVICE_STILL_APPLYING_ON_FAILURE)) { + LOG_WARN(("Could not write update.status still applying on" + " success error.")); + } + } + } + } else { + DWORD lastError = GetLastError(); + LOG_WARN(("Could not create process as current user, " + "updaterPath: %ls; cmdLine: %ls. (%d)", + argv[0], cmdLine, lastError)); + } + // Empty value on putenv is how you remove an env variable in Windows + putenv(const_cast<char*>("MOZ_USING_SERVICE=")); + + free(cmdLine); + return updateWasSuccessful; +} + +/** + * 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. (%d)", + 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; + } + + // 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(argv[0], isLocal) || !isLocal) { + LOG_WARN(("Filesystem in path %ls is not supported (%d)", + argv[0], GetLastError())); + if (!WriteStatusFailure(argv[1], + SERVICE_UPDATER_NOT_FIXED_DRIVE)) { + LOG_WARN(("Could not write update.status service update failure. (%d)", + GetLastError())); + } + return FALSE; + } + + nsAutoHandle noWriteLock(CreateFileW(argv[0], 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. (%d)", + GetLastError())); + if (!WriteStatusFailure(argv[1], + SERVICE_COULD_NOT_LOCK_UPDATER)) { + LOG_WARN(("Could not write update.status service update failure. (%d)", + 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.")); + result = FALSE; + } + + BOOL updaterIsCorrect; + if (result && !VerifySameFiles(argv[0], installDirUpdater, + updaterIsCorrect)) { + LOG_WARN(("Error checking if the updaters are the same.\n" + "Path 1: %ls\nPath 2: %ls", argv[0], installDirUpdater)); + result = FALSE; + } + + if (result && !updaterIsCorrect) { + LOG_WARN(("The updaters do not match, updater will not run.\n" + "Path 1: %ls\nPath 2: %ls", argv[0], installDirUpdater)); + result = FALSE; + } + + if (result) { + LOG(("updater.exe was compared successfully to the installation directory" + " updater.exe.")); + } else { + if (!WriteStatusFailure(argv[1], + SERVICE_UPDATER_COMPARE_ERROR)) { + LOG_WARN(("Could not write update.status updater compare failure.")); + } + return FALSE; + } + + // 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. + HMODULE updaterModule = LoadLibraryEx(argv[0], nullptr, + LOAD_LIBRARY_AS_DATAFILE); + if (!updaterModule) { + LOG_WARN(("updater.exe module could not be loaded. (%d)", 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(argv[1], + SERVICE_UPDATER_IDENTITY_ERROR)) { + LOG_WARN(("Could not write update.status no updater identity.")); + } + return TRUE; + } + + // Check for updater.exe sign problems + BOOL updaterSignProblem = FALSE; +#ifndef DISABLE_UPDATER_AUTHENTICODE_CHECK + updaterSignProblem = !DoesBinaryMatchAllowedCertificates(installDir, + argv[0]); +#endif + + // Only proceed with the update if we have no signing problems + if (!updaterSignProblem) { + 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 (%d)", + 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. (%d)", + GetLastError())); + } + } + } + } else { + result = FALSE; + LOG_WARN(("Could not start process due to certificate check error on " + "updater.exe. Updating update.status. (%d)", 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. (%d)", + 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. (%d)", GetLastError())); + return FALSE; + } + + if (!PathRemoveFileSpecW(serviceUpdaterPath)) { + LOG_WARN(("Couldn't remove file spec when attempting to use a secure " + "updater path. (%d)", GetLastError())); + return FALSE; + } + + if (!PathAppendSafe(serviceUpdaterPath, L"update")) { + LOG_WARN(("Couldn't append file spec when attempting to use a secure " + "updater path. (%d)", 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. (%d)", 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] and argv[1] + * and automatically included by Windows. 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. + RPC_WSTR guidString = RPC_WSTR(L""); + GUID guid; + HRESULT hr = CoCreateGuid(&guid); + if (SUCCEEDED(hr)) { + UuidToString(&guid, &guidString); + } + LOG(("Executing service command %ls, ID: %ls", + argv[2], reinterpret_cast<LPCWSTR>(guidString))); + RpcStringFree(&guidString); + + BOOL result = FALSE; + if (!lstrcmpi(argv[2], L"software-update")) { + + // Use the passed in command line arguments for the update, except for the + // path to updater.exe. We copy 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. + LPWSTR oldUpdaterPath = argv[3]; + WCHAR secureUpdaterPath[MAX_PATH + 1] = { L'\0' }; + result = GetSecureUpdaterPath(secureUpdaterPath); // Does its own logging + if (result) { + LOG(("Passed in path: '%ls'; Using this path for updating: '%ls'.", + oldUpdaterPath, secureUpdaterPath)); + DeleteSecureUpdater(secureUpdaterPath); + result = CopyFileW(oldUpdaterPath, secureUpdaterPath, FALSE); + } + + if (!result) { + LOG_WARN(("Could not copy path to secure location. (%d)", + GetLastError())); + if (argc > 4 && !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 oldUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; + WCHAR secureUpdaterINIPath[MAX_PATH + 1] = { L'\0' }; + if (PathGetSiblingFilePath(secureUpdaterINIPath, secureUpdaterPath, + L"updater.ini") && + PathGetSiblingFilePath(oldUpdaterINIPath, oldUpdaterPath, + L"updater.ini")) { + // This is non fatal if it fails there is no real harm + if (!CopyFileW(oldUpdaterINIPath, secureUpdaterINIPath, FALSE)) { + LOG_WARN(("Could not copy updater.ini from: '%ls' to '%ls'. (%d)", + oldUpdaterINIPath, 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 TRUE; +} diff --git a/toolkit/components/maintenanceservice/workmonitor.h b/toolkit/components/maintenanceservice/workmonitor.h new file mode 100644 index 000000000..ac4cd679b --- /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); diff --git a/toolkit/moz.build b/toolkit/moz.build index f5f2e288f..fd0f50dcc 100644 --- a/toolkit/moz.build +++ b/toolkit/moz.build @@ -25,6 +25,9 @@ DIRS += [ DIRS += ['mozapps/update'] +if CONFIG['MOZ_MAINTENANCE_SERVICE']: + DIRS += ['components/maintenanceservice'] + DIRS += ['xre'] if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('gtk2', 'gtk3', 'qt'): diff --git a/toolkit/mozapps/installer/windows/nsis/makensis.mk b/toolkit/mozapps/installer/windows/nsis/makensis.mk index 121819f6a..49aeca5cf 100644 --- a/toolkit/mozapps/installer/windows/nsis/makensis.mk +++ b/toolkit/mozapps/installer/windows/nsis/makensis.mk @@ -75,3 +75,10 @@ uninstaller:: cd $(CONFIG_DIR) && $(MAKENSISU) uninstaller.nsi $(NSINSTALL) -D $(DIST)/bin/uninstall cp $(CONFIG_DIR)/helper.exe $(DIST)/bin/uninstall + +ifdef MOZ_MAINTENANCE_SERVICE +maintenanceservice_installer:: + cd $(CONFIG_DIR) && $(MAKENSISU) maintenanceservice_installer.nsi + $(NSINSTALL) -D $(DIST)/bin/ + cp $(CONFIG_DIR)/maintenanceservice_installer.exe $(DIST)/bin +endif
\ No newline at end of file diff --git a/toolkit/mozapps/update/moz.build b/toolkit/mozapps/update/moz.build index 94de7d42a..136b3c91e 100644 --- a/toolkit/mozapps/update/moz.build +++ b/toolkit/mozapps/update/moz.build @@ -5,11 +5,18 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android': - if CONFIG['MOZ_UPDATER']: - DIRS += ['common', 'updater'] + if CONFIG['MOZ_UPDATER'] or CONFIG['MOZ_MAINTENANCE_SERVICE']: + # If only the maintenance service is installed and not + # the updater, then the maintenance service may still be + # used for other things. We need to build update/common + # which the maintenance service uses. + DIRS += ['common'] if CONFIG['OS_ARCH'] == 'WINNT': DIRS += ['common-standalone'] + if CONFIG['MOZ_UPDATER']: + DIRS += ['updater'] + XPIDL_MODULE = 'update' XPCSHELL_TESTS_MANIFESTS += ['tests/unit_timermanager/xpcshell.ini'] diff --git a/toolkit/mozapps/update/nsUpdateService.js b/toolkit/mozapps/update/nsUpdateService.js index 61de54caf..977a1aa61 100644 --- a/toolkit/mozapps/update/nsUpdateService.js +++ b/toolkit/mozapps/update/nsUpdateService.js @@ -53,6 +53,9 @@ const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled"; const PREF_APP_UPDATE_URL = "app.update.url"; const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details"; const PREF_APP_UPDATE_URL_OVERRIDE = "app.update.url.override"; +const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled"; +const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors"; +const PREF_APP_UPDATE_SERVICE_MAX_ERRORS = "app.update.service.maxErrors"; const PREF_APP_UPDATE_SOCKET_ERRORS = "app.update.socket.maxErrors"; const PREF_APP_UPDATE_RETRY_TIMEOUT = "app.update.socket.retryTimeout"; @@ -167,6 +170,10 @@ const DOWNLOAD_FOREGROUND_INTERVAL = 0; const UPDATE_WINDOW_NAME = "Update:Wizard"; +// The number of consecutive failures when updating using the service before +// setting the app.update.service.enabled preference to false. +const DEFAULT_SERVICE_MAX_ERRORS = 10; + // The number of consecutive socket errors to allow before falling back to // downloading a different MAR file or failing if already downloading the full. const DEFAULT_SOCKET_MAX_ERRORS = 10; @@ -715,6 +722,15 @@ function getCanStageUpdates() { } #endif +#ifdef MOZ_WIDGET_GONK + // For Gonk, the updater will remount the /system partition to move staged + // files into place. + if (getPref("getBoolPref", PREF_APP_UPDATE_SERVICE_ENABLED, false)) { + LOG("getCanStageUpdates - able to stage updates because this is gonk"); + return true; + } +#endif + if (!hasUpdateMutex()) { LOG("getCanStageUpdates - unable to apply updates because another " + "instance of the application is already handling updates for this " + @@ -1143,7 +1159,12 @@ function releaseSDCardMountLock() { * @return true if the service should be used for updates. */ function shouldUseService() { +#ifdef MOZ_MAINTENANCE_SERVICE + return getPref("getBoolPref", + PREF_APP_UPDATE_SERVICE_ENABLED, false); +#else return false; +#endif } /** @@ -1496,6 +1517,24 @@ function handleUpdateFailure(update, errorCode) { update.errorCode == SERVICE_COULD_NOT_COPY_UPDATER || update.errorCode == SERVICE_INSTALLDIR_ERROR) { + var failCount = getPref("getIntPref", + PREF_APP_UPDATE_SERVICE_ERRORS, 0); + var maxFail = getPref("getIntPref", + PREF_APP_UPDATE_SERVICE_MAX_ERRORS, + DEFAULT_SERVICE_MAX_ERRORS); + + // As a safety, when the service reaches maximum failures, it will + // disable itself and fallback to using the normal update mechanism + // without the service. + if (failCount >= maxFail) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false); + Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS); + } else { + failCount++; + Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, + failCount); + } + writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING); try { Services.telemetry.getHistogramById("UPDATER_SERVICE_ERROR_CODE"). @@ -2579,6 +2618,14 @@ UpdateService.prototype = { this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_STAGING_ENABLED, "UPDATER_STAGE_ENABLED"); +#ifdef XP_WIN + this._sendBoolPrefTelemetryPing(PREF_APP_UPDATE_SERVICE_ENABLED, + "UPDATER_SERVICE_ENABLED"); + this._sendIntPrefTelemetryPing(PREF_APP_UPDATE_SERVICE_ERRORS, + "UPDATER_SERVICE_ERRORS"); + this._sendServiceInstalledTelemetryPing(); +#endif + this._checkForBackgroundUpdates(true); }, diff --git a/toolkit/mozapps/update/tests/Makefile.in b/toolkit/mozapps/update/tests/Makefile.in index d3cd98c94..b9310233d 100644 --- a/toolkit/mozapps/update/tests/Makefile.in +++ b/toolkit/mozapps/update/tests/Makefile.in @@ -23,6 +23,13 @@ base-updater-head_TARGET := misc base-updater-head_DEST := $(XPCSHELLTESTROOT)/unit_base_updater base-updater-head_FILES := $(XPCSHELLTESTROOT)/unit_aus_update/head_update.js +ifdef MOZ_MAINTENANCE_SERVICE +INSTALL_TARGETS += service-updater-head +service-updater-head_TARGET := misc +service-updater-head_DEST := $(XPCSHELLTESTROOT)/unit_service_updater +service-updater-head_FILES := $(XPCSHELLTESTROOT)/unit_aus_update/head_update.js +endif # MOZ_MAINTENANCE_SERVICE + ifndef MOZ_PROFILE_GENERATE ifdef COMPILE_ENVIRONMENT INSTALL_TARGETS += xpcshell-test-helper diff --git a/toolkit/mozapps/update/tests/moz.build b/toolkit/mozapps/update/tests/moz.build index 15373e75a..9081e5be4 100644 --- a/toolkit/mozapps/update/tests/moz.build +++ b/toolkit/mozapps/update/tests/moz.build @@ -12,6 +12,9 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android': MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini'] XPCSHELL_TESTS_MANIFESTS += ['unit_base_updater/xpcshell.ini'] + if CONFIG['MOZ_MAINTENANCE_SERVICE']: + XPCSHELL_TESTS_MANIFESTS += ['unit_service_updater/xpcshell.ini'] + SimplePrograms([ 'TestAUSHelper', 'TestAUSReadStrings', @@ -43,6 +46,9 @@ for var in ('MOZ_APP_NAME', 'MOZ_APP_BASENAME', 'MOZ_APP_DISPLAYNAME', DEFINES['NS_NO_XPCOM'] = True +if CONFIG['MOZ_MAINTENANCE_SERVICE']: + DEFINES['MOZ_MAINTENANCE_SERVICE'] = CONFIG['MOZ_MAINTENANCE_SERVICE'] + # For debugging purposes only #DEFINES['DISABLE_UPDATER_AUTHENTICODE_CHECK'] = True diff --git a/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js new file mode 100644 index 000000000..ffa207ebb --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js @@ -0,0 +1,33 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Bootstrap the tests using the service by installing our own version of the service */ + +function run_test() { + if (!shouldRunServiceTest(true)) { + return; + } + + setupTestCommon(); + // We don't actually care if the MAR has any data, we only care about the + // application return code and update.status result. + gTestFiles = gTestFilesCommon; + gTestDirs = []; + setupUpdaterTest(FILE_COMPLETE_MAR); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED, false); +} + +function checkUpdateFinished() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + + // We need to check the service log even though this is a bootstrap + // because the app bin could be in use by this test by the time the next + // test runs. + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js new file mode 100644 index 000000000..869c3634b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +function run_test() { + if (MOZ_APP_NAME == "xulrunner") { + logTestInfo("Unable to run this test on xulrunner"); + return; + } + + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + createUpdaterINI(false); + + if (IS_WIN) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true); + } + + let channel = Services.prefs.getCharPref(PREF_APP_UPDATE_CHANNEL); + let patches = getLocalPatchString(null, null, null, null, null, "true", + STATE_PENDING_SVC); + let updates = getLocalUpdateString(patches, null, null, null, null, null, + null, null, null, null, null, null, + null, "true", channel); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile(getAppVersion()); + writeStatusFile(STATE_PENDING_SVC); + + reloadUpdateManagerData(); + do_check_true(!!gUpdateManager.activeUpdate); + + lockDirectory(getAppBaseDir()); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + stageUpdate(); +} + +function end_test() { + resetEnvironment(); +} + +/** + * Checks if staging the update has failed. + */ +function checkUpdateApplied() { + // Don't proceed until the update has failed, and reset to pending. + if (gUpdateManager.activeUpdate.state != STATE_PENDING) { + if (++gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for update to equal: " + + STATE_PENDING + + ", current state: " + gUpdateManager.activeUpdate.state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + do_timeout(TEST_CHECK_TIMEOUT, finishTest); +} + +function finishTest() { + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + do_check_eq(readStatusState(), STATE_PENDING); + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + unlockDirectory(getAppBaseDir()); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js new file mode 100644 index 000000000..ba063244e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js @@ -0,0 +1,320 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +Components.utils.import("resource://gre/modules/ctypes.jsm"); + +function run_test() { + if (MOZ_APP_NAME == "xulrunner") { + logTestInfo("Unable to run this test on xulrunner"); + return; + } + + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(true); + + if (IS_WIN) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true); + } + + let channel = Services.prefs.getCharPref(PREF_APP_UPDATE_CHANNEL); + let patches = getLocalPatchString(null, null, null, null, null, "true", + STATE_PENDING_SVC); + let updates = getLocalUpdateString(patches, null, null, null, null, null, + null, null, null, null, null, null, + null, "true", channel); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile(getAppVersion()); + writeStatusFile(STATE_PENDING_SVC); + + reloadUpdateManagerData(); + do_check_true(!!gUpdateManager.activeUpdate); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + stageUpdate(); +} + +function customLaunchAppToApplyUpdate() { + logTestInfo("start - locking installation directory"); + const LPCWSTR = ctypes.char16_t.ptr; + const DWORD = ctypes.uint32_t; + const LPVOID = ctypes.voidptr_t; + const GENERIC_READ = 0x80000000; + const FILE_SHARE_READ = 1; + const FILE_SHARE_WRITE = 2; + const OPEN_EXISTING = 3; + const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + const INVALID_HANDLE_VALUE = LPVOID(0xffffffff); + let kernel32 = ctypes.open("kernel32"); + let CreateFile = kernel32.declare("CreateFileW", ctypes.default_abi, + LPVOID, LPCWSTR, DWORD, DWORD, + LPVOID, DWORD, DWORD, LPVOID); + gHandle = CreateFile(getAppBaseDir().path, GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, LPVOID(0), + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, LPVOID(0)); + do_check_neq(gHandle.toString(), INVALID_HANDLE_VALUE.toString()); + kernel32.close(); + logTestInfo("finish - locking installation directory"); +} + +/** + * Checks if the update has finished being staged. + */ +function checkUpdateApplied() { + gTimeoutRuns++; + // Don't proceed until the active update's state is the expected value. + if (gUpdateManager.activeUpdate.state != STATE_APPLIED_SVC) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for update to equal: " + + STATE_APPLIED_SVC + + ", current state: " + gUpdateManager.activeUpdate.state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + // Don't proceed until the update's status state is the expected value. + let state = readStatusState(); + if (state != STATE_APPLIED_SVC) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "status state to equal: " + + STATE_APPLIED_SVC + + ", current status state: " + state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + // Don't proceed until the last update log has been created. + let log; + if (IS_WIN || IS_MACOSX) { + log = getUpdatesDir(); + } else { + log = getStageDirFile(null, true); + log.append(DIR_UPDATES); + } + log.append(FILE_LAST_LOG); + if (!log.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update log " + + "to be created. Path: " + log.path); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + + log = getUpdatesPatchDir(); + log.append(FILE_UPDATE_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_LAST_LOG); + if (IS_WIN || IS_MACOSX) { + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + } else { + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + } + + log = getUpdatesDir(); + log.append(FILE_BACKUP_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + let updatesDir = getStageDirFile(DIR_UPDATES + "/0", true); + logTestInfo("testing " + updatesDir.path + " shouldn't exist"); + do_check_false(updatesDir.exists()); + + log = getStageDirFile(DIR_UPDATES + "/0/" + FILE_UPDATE_LOG, true); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getStageDirFile(DIR_UPDATES + "/" + FILE_LAST_LOG, true); + if (IS_WIN || IS_MACOSX) { + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + } else { + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + } + + log = getStageDirFile(DIR_UPDATES + "/" + FILE_BACKUP_LOG, true); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + // Switch the application to the staged application that was updated by + // launching the application. + do_timeout(TEST_CHECK_TIMEOUT, launchAppToApplyUpdate); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateFinished() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateFinished; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateFinished(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateFinished); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateFinished() { + gTimeoutRuns++; + // Don't proceed until the update's status state is the expected value. + let state = readStatusState(); + if (state != STATE_SUCCEEDED) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "status state to equal: " + + STATE_SUCCEEDED + + ", current status state: " + state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + } + return; + } + + // Don't proceed until the application was switched out with the staged update + // successfully. + let updatedDir = getStageDirFile(null, true); + if (updatedDir.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded while waiting for updated dir to not exist. Path: " + + updatedDir.path); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + } + return; + } + + if (IS_WIN) { + // Don't proceed until the updater binary is no longer in use. + let updater = getUpdatesPatchDir(); + updater.append(FILE_UPDATER_BIN); + if (updater.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded while waiting for updater binary to no longer be " + + "in use"); + } else { + try { + updater.remove(false); + } catch (e) { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + return; + } + } + } + } + +// Added to make this test pass on esr38 + if (IS_WIN) { + // Don't proceed until the tobedeleted dir no longer exists. + let toBeDeletedDir = getApplyDirFile(DIR_TOBEDELETED, true); + if (toBeDeletedDir.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded while waiting for the tobedeleted dir to no " + + "longer exist"); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + return; + } + } + } + + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// let running = getPostUpdateFile(".running"); +// logTestInfo("checking that the post update process running file exists. " + +// "Path: " + running.path); +// do_check_true(running.exists()); +// } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackAppLog(); + + standardInit(); + + let update = gUpdateManager.getUpdateAt(0); + do_check_eq(update.state, STATE_SUCCEEDED); + + let updatesDir = getUpdatesPatchDir(); + logTestInfo("testing " + updatesDir.path + " should exist"); + do_check_true(updatesDir.exists()); + + let log = getUpdatesPatchDir(); + log.append(FILE_UPDATE_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_LAST_LOG); + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_BACKUP_LOG); + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js new file mode 100644 index 000000000..a4188fa3f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js @@ -0,0 +1,286 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +function run_test() { + if (MOZ_APP_NAME == "xulrunner") { + logTestInfo("Unable to run this test on xulrunner"); + return; + } + + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(false); + + if (IS_WIN) { + Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true); + } + + let channel = Services.prefs.getCharPref(PREF_APP_UPDATE_CHANNEL); + let patches = getLocalPatchString(null, null, null, null, null, "true", + STATE_PENDING_SVC); + let updates = getLocalUpdateString(patches, null, null, null, null, null, + null, null, null, null, null, null, + null, "true", channel); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile(getAppVersion()); + writeStatusFile(STATE_PENDING_SVC); + + reloadUpdateManagerData(); + do_check_true(!!gUpdateManager.activeUpdate); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + stageUpdate(); +} + +/** + * Checks if the update has finished being staged. + */ +function checkUpdateApplied() { + gTimeoutRuns++; + // Don't proceed until the active update's state is the expected value. + if (gUpdateManager.activeUpdate.state != STATE_APPLIED_SVC) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for update to equal: " + + STATE_APPLIED_SVC + + ", current state: " + gUpdateManager.activeUpdate.state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + // Don't proceed until the update's status state is the expected value. + let state = readStatusState(); + if (state != STATE_APPLIED_SVC) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "status state to equal: " + + STATE_APPLIED_SVC + + ", current status state: " + state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + // Don't proceed until the last update log has been created. + let log; + if (IS_WIN || IS_MACOSX) { + log = getUpdatesDir(); + } else { + log = getStageDirFile(null, true); + log.append(DIR_UPDATES); + } + log.append(FILE_LAST_LOG); + if (!log.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update log " + + "to be created. Path: " + log.path); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateApplied); + } + return; + } + + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + + log = getUpdatesPatchDir(); + log.append(FILE_UPDATE_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_LAST_LOG); + if (IS_WIN || IS_MACOSX) { + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + } else { + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + } + + log = getUpdatesDir(); + log.append(FILE_BACKUP_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + let updatesDir = getStageDirFile(DIR_UPDATES + "/0", true); + logTestInfo("testing " + updatesDir.path + " shouldn't exist"); + do_check_false(updatesDir.exists()); + + log = getStageDirFile(DIR_UPDATES + "/0/" + FILE_UPDATE_LOG, true); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getStageDirFile(DIR_UPDATES + "/" + FILE_LAST_LOG, true); + if (IS_WIN || IS_MACOSX) { + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + } else { + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + } + + log = getStageDirFile(DIR_UPDATES + "/" + FILE_BACKUP_LOG, true); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + // Switch the application to the staged application that was updated by + // launching the application. + do_timeout(TEST_CHECK_TIMEOUT, launchAppToApplyUpdate); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateFinished() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateApplied; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateApplied(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateApplied); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateApplied() { + gTimeoutRuns++; + // Don't proceed until the update's status state is the expected value. + let state = readStatusState(); + if (state != STATE_SUCCEEDED) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "status state to equal: " + STATE_SUCCEEDED + + ", current status state: " + state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + } + return; + } + + // Don't proceed until the application was switched out with the staged update + // successfully. + let updatedDir = getStageDirFile(null, true); + if (updatedDir.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded while waiting for updated dir to not exist. Path: " + + updatedDir.path); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + } + return; + } + + if (IS_WIN) { + // Don't proceed until the updater binary is no longer in use. + let updater = getUpdatesPatchDir(); + updater.append(FILE_UPDATER_BIN); + if (updater.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded while waiting for updater binary to no longer be " + + "in use"); + } else { + try { + updater.remove(false); + } catch (e) { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + return; + } + } + } + } + + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// let running = getPostUpdateFile(".running"); +// logTestInfo("checking that the post update process running file exists. " + +// "Path: " + running.path); +// do_check_true(running.exists()); +// } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + gSwitchApp = true; + checkUpdateLogContents(); + gSwitchApp = false; + checkCallbackAppLog(); + + standardInit(); + + let update = gUpdateManager.getUpdateAt(0); + do_check_eq(update.state, STATE_SUCCEEDED); + + let updatesDir = getUpdatesPatchDir(); + logTestInfo("testing " + updatesDir.path + " should exist"); + do_check_true(updatesDir.exists()); + + let log = getUpdatesPatchDir(); + log.append(FILE_UPDATE_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_LAST_LOG); + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_BACKUP_LOG); + if (IS_WIN || IS_MACOSX) { + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + } else { + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + } + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js new file mode 100644 index 000000000..a6749bb7e --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js @@ -0,0 +1,160 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/** + * Test applying an update by staging an update and launching an application to + * apply it. + */ + +function run_test() { + if (MOZ_APP_NAME == "xulrunner") { + logTestInfo("Unable to run this test on xulrunner"); + return; + } + + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + let channel = Services.prefs.getCharPref(PREF_APP_UPDATE_CHANNEL); + let patches = getLocalPatchString(null, null, null, null, null, "true", + STATE_PENDING); + let updates = getLocalUpdateString(patches, null, null, null, null, null, + null, null, null, null, null, null, + null, "true", channel); + writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true); + writeVersionFile(getAppVersion()); + writeStatusFile(STATE_PENDING_SVC); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateFinished() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateFinished; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateFinished(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateFinished); + +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateFinished() { + gTimeoutRuns++; + // Don't proceed until the update's status state is the expected value. + let state = readStatusState(); + if (state != STATE_SUCCEEDED) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " + + "status state to equal: " + STATE_SUCCEEDED + + ", current status state: " + state); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + } + return; + } + + // Don't proceed until the update log has been created. + let log = getUpdatesPatchDir(); + log.append(FILE_UPDATE_LOG); + if (!log.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update log " + + "to be created. Path: " + log.path); + } else { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + } + return; + } + + if (IS_WIN) { + // Don't proceed until the updater binary is no longer in use. + let updater = getUpdatesPatchDir(); + updater.append(FILE_UPDATER_BIN); + if (updater.exists()) { + if (gTimeoutRuns > MAX_TIMEOUT_RUNS) { + do_throw("Exceeded while waiting for updater binary to no longer be " + + "in use"); + } else { + try { + updater.remove(false); + } catch (e) { + do_timeout(TEST_CHECK_TIMEOUT, checkUpdateFinished); + return; + } + } + } + } + + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackAppLog(); + + standardInit(); + + let update = gUpdateManager.getUpdateAt(0); + do_check_eq(update.state, STATE_SUCCEEDED); + + let updatesPatchDir = getUpdatesPatchDir(); + logTestInfo("testing " + updatesPatchDir.path + " should exist"); + do_check_true(updatesPatchDir.exists()); + + log = getUpdatesPatchDir(); + log.append(FILE_UPDATE_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_LAST_LOG); + logTestInfo("testing " + log.path + " should exist"); + do_check_true(log.exists()); + + log = getUpdatesDir(); + log.append(FILE_BACKUP_LOG); + logTestInfo("testing " + log.path + " shouldn't exist"); + do_check_false(log.exists()); + + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseFallbackStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseFallbackStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..dc3dcc810 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseFallbackStageFailureCompleteSvc_win.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply failure fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Launch the callback helper application so it is in use during the update. + let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let callbackAppProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + callbackAppProcess.init(callbackApp); + callbackAppProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..1d5d3c516 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file staged patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Launch the callback helper application so it is in use during the update. + let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let callbackAppProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + callbackAppProcess.init(callbackApp); + callbackAppProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js new file mode 100644 index 000000000..552fd2d95 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Application in use complete MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Launch the callback helper application so it is in use during the update. + let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile); + callbackApp.permissions = PERMS_DIRECTORY; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let callbackAppProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + callbackAppProcess.init(callbackApp); + callbackAppProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let applyToDir = getApplyDirFile(); + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + applyToDir.lastModifiedTime = yesterday; + } + + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js new file mode 100644 index 000000000..c698b885b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file staged patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + + gCallbackBinFile = "exe0.exe"; + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(0, STATE_SUCCEEDED); +} + +function checkUpdateApplied() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js new file mode 100644 index 000000000..ab1d753e4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js @@ -0,0 +1,37 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file staged patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR); + + gCallbackBinFile = "exe0.exe"; + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(0, STATE_SUCCEEDED); +} + +function checkUpdateApplied() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js new file mode 100644 index 000000000..c09ac63fc --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Replace app binary complete MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + + gCallbackBinFile = "exe0.exe"; + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js new file mode 100644 index 000000000..639743f3c --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Patch app binary partial MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR); + + gCallbackBinFile = "exe0.exe"; + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js new file mode 100644 index 000000000..525ded0aa --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js @@ -0,0 +1,63 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Failure Test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + createUpdaterINI(); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on all updates since + // the precomplete file in the root of the bundle is renamed, etc. (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function checkUpdateFinished() { + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_PARTIAL_FAILURE); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseFallbackStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseFallbackStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..2fa86e798 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseFallbackStageFailureCompleteSvc_win.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file staged patch apply failure fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Launch an existing file so it is in use during the update. + let fileInUseBin = getApplyDirFile(gTestFiles[13].relPathDir + + gTestFiles[13].fileName); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseFallbackStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseFallbackStageFailurePartialSvc_win.js new file mode 100644 index 000000000..1463df65b --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseFallbackStageFailurePartialSvc_win.js @@ -0,0 +1,55 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file staged patch apply failure fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + // Launch an existing file so it is in use during the update. + let fileInUseBin = getApplyDirFile(gTestFiles[11].relPathDir + + gTestFiles[11].fileName); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..f8c7a6a58 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file staged patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Launch an existing file so it is in use during the update. + let fileInUseBin = getApplyDirFile(gTestFiles[13].relPathDir + + gTestFiles[13].fileName); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js new file mode 100644 index 000000000..a52aa937d --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file staged patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + // Launch an existing file so it is in use during the update. + let fileInUseBin = getApplyDirFile(gTestFiles[11].relPathDir + + gTestFiles[11].fileName); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js new file mode 100644 index 000000000..a535ac0c6 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use complete MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Launch an existing file so it is in use during the update. + let fileInUseBin = getApplyDirFile(gTestFiles[13].relPathDir + + gTestFiles[13].fileName); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js new file mode 100644 index 000000000..68ffdff58 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use partial MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR); + + // Launch an existing file so it is in use during the update. + let fileInUseBin = getApplyDirFile(gTestFiles[11].relPathDir + + gTestFiles[11].fileName); + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js new file mode 100644 index 000000000..ed22b1d48 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = gTestFiles[3].relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + gTestFiles[3].fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let lockFileProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + lockFileProcess.init(helperBin); + lockFileProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js new file mode 100644 index 000000000..d4562a9e2 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = gTestFiles[2].relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + gTestFiles[2].fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let lockFileProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + lockFileProcess.init(helperBin); + lockFileProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_UNABLE_OPEN_DEST); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFallbackStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFallbackStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..7612db387 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFallbackStageFailureCompleteSvc_win.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file staged patch apply failure fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = gTestFiles[3].relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + gTestFiles[3].fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let lockFileProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + lockFileProcess.init(helperBin); + lockFileProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFallbackStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFallbackStageFailurePartialSvc_win.js new file mode 100644 index 000000000..366bb3632 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFallbackStageFailurePartialSvc_win.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file staged patch apply failure fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = gTestFiles[2].relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + gTestFiles[2].fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let lockFileProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + lockFileProcess.init(helperBin); + lockFileProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..4600150eb --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked complete MAR file staged patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = gTestFiles[3].relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + gTestFiles[3].fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let lockFileProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + lockFileProcess.init(helperBin); + lockFileProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js new file mode 100644 index 000000000..6b318375a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File locked partial MAR file staged patch apply failure test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + // Exclusively lock an existing file so it is in use during the update. + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let helperDestDir = getApplyDirFile(DIR_RESOURCES); + helperBin.copyTo(helperDestDir, FILE_HELPER_BIN); + helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN); + // Strip off the first two directories so the path has to be from the helper's + // working directory. + let lockFileRelPath = gTestFiles[2].relPathDir.split("/"); + if (IS_MACOSX) { + lockFileRelPath = lockFileRelPath.slice(2); + } + lockFileRelPath = lockFileRelPath.join("/") + "/" + gTestFiles[2].fileName; + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT, lockFileRelPath]; + let lockFileProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + lockFileProcess.init(helperBin); + lockFileProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseFallbackStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseFallbackStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..f6729e06a --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseFallbackStageFailureCompleteSvc_win.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file staged patch apply failure + fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + let fileInUseBin = getApplyDirFile(gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0]); + // Remove the empty file created for the test so the helper application can + // replace it. + fileInUseBin.remove(false); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseDir = getApplyDirFile(gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0]); + helperBin.copyTo(fileInUseDir, gTestDirs[4].subDirFiles[0]); + + // Launch an existing file so it is in use during the update. + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseFallbackStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseFallbackStageFailurePartialSvc_win.js new file mode 100644 index 000000000..176b55ba5 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseFallbackStageFailurePartialSvc_win.js @@ -0,0 +1,64 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file staged patch apply failure + fallback test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + let fileInUseBin = getApplyDirFile(gTestDirs[2].relPathDir + + gTestDirs[2].files[0]); + // Remove the empty file created for the test so the helper application can + // replace it. + fileInUseBin.remove(false); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseDir = getApplyDirFile(gTestDirs[2].relPathDir); + helperBin.copyTo(fileInUseDir, gTestDirs[2].files[0]); + + // Launch an existing file so it is in use during the update. + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + runUpdate(1, STATE_PENDING); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, false, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js new file mode 100644 index 000000000..a63644cfa --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file staged patch apply failure + test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_COMPLETE_MAR); + + let fileInUseBin = getApplyDirFile(gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0]); + // Remove the empty file created for the test so the helper application can + // replace it. + fileInUseBin.remove(false); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseDir = getApplyDirFile(gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0]); + helperBin.copyTo(fileInUseDir, gTestDirs[4].subDirFiles[0]); + + // Launch an existing file so it is in use during the update. + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js new file mode 100644 index 000000000..cc8067c8f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js @@ -0,0 +1,65 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file staged patch apply failure + test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + let fileInUseBin = getApplyDirFile(gTestDirs[2].relPathDir + + gTestDirs[2].files[0]); + // Remove the empty file created for the test so the helper application can + // replace it. + fileInUseBin.remove(false); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseDir = getApplyDirFile(gTestDirs[2].relPathDir); + helperBin.copyTo(fileInUseDir, gTestDirs[2].files[0]); + + // Launch an existing file so it is in use during the update. + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + gDisableReplaceFallback = true; + runUpdate(1, STATE_FAILED_WRITE_ERROR); +} + +function checkUpdateApplied() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContains(ERR_RENAME_FILE); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js new file mode 100644 index 000000000..3d042a672 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js @@ -0,0 +1,56 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir complete MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + + let fileInUseBin = getApplyDirFile(gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0] + + gTestDirs[4].subDirFiles[0]); + // Remove the empty file created for the test so the helper application can + // replace it. + fileInUseBin.remove(false); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseDir = getApplyDirFile(gTestDirs[4].relPathDir + + gTestDirs[4].subDirs[0]); + helperBin.copyTo(fileInUseDir, gTestDirs[4].subDirFiles[0]); + + // Launch an existing file so it is in use during the update. + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js new file mode 100644 index 000000000..b62c2af4f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js @@ -0,0 +1,54 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* File in use inside removed dir partial MAR file patch apply success test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR); + + let fileInUseBin = getApplyDirFile(gTestDirs[2].relPathDir + + gTestDirs[2].files[0]); + // Remove the empty file created for the test so the helper application can + // replace it. + fileInUseBin.remove(false); + + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let fileInUseDir = getApplyDirFile(gTestDirs[2].relPathDir); + helperBin.copyTo(fileInUseDir, gTestDirs[2].files[0]); + + // Launch an existing file so it is in use during the update. + let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s", + HELPER_SLEEP_TIMEOUT]; + let fileInUseProcess = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + fileInUseProcess.init(fileInUseBin); + fileInUseProcess.run(false, args, args.length); + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + do_timeout(TEST_HELPER_TIMEOUT, waitForHelperSleep); +} + +function doUpdate() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +function checkUpdateFinished() { + setupHelperFinish(); +} + +function checkUpdate() { + checkFilesAfterUpdateSuccess(getApplyDirFile, false, true); + checkUpdateLogContains(ERR_BACKUP_DISCARD); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js new file mode 100644 index 000000000..71500f69f --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js @@ -0,0 +1,55 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Failure Test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[11].originalFile = "partial.png"; + gTestDirs = gTestDirsPartialSuccess; + setTestFilesAndDirsForFailure(); + setupUpdaterTest(FILE_PARTIAL_MAR); + + createUpdaterINI(true); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on all updates since + // the precomplete file in the root of the bundle is renamed, etc. (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_FAILED); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function checkUpdateFinished() { + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + checkFilesAfterUpdateFailure(getApplyDirFile, true, false); + checkUpdateLogContents(LOG_PARTIAL_FAILURE); + waitForFilesInUse(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js new file mode 100644 index 000000000..5dbc0fca9 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js @@ -0,0 +1,160 @@ +/* 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/. + */ + +/* General Complete MAR File Staged Patch Apply Test */ + +function run_test() { + if (!shouldRunServiceTest(false, true)) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(false); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + // Don't test symlinks on Mac OS X in this test since it tends to timeout. + // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js + if (IS_UNIX && !IS_MACOSX) { + removeSymlink(); + createSymlink(); + do_register_cleanup(removeSymlink); + gTestFiles.splice(gTestFiles.length - 3, 0, + { + description : "Readable symlink", + fileName : "link", + relPathDir : DIR_RESOURCES, + originalContents : "test", + compareContents : "test", + originalFile : null, + compareFile : null, + originalPerms : 0o666, + comparePerms : 0o666 + }); + } + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + do_timeout(TEST_CHECK_TIMEOUT, function() { + runUpdate(0, STATE_SUCCEEDED); + }); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateApplied() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateApplied; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateApplied(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateApplied); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateApplied() { + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// let running = getPostUpdateFile(".running"); +// logTestInfo("checking that the post update process running file exists. " + +// "Path: " + running.path); +// do_check_true(running.exists()); +// } + + if (IS_UNIX && !IS_MACOSX) { + checkSymlink(); + } + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackAppLog(); +} + +function runHelperProcess(args) { + let helperBin = getTestDirFile(FILE_HELPER_BIN); + let process = AUS_Cc["@mozilla.org/process/util;1"]. + createInstance(AUS_Ci.nsIProcess); + process.init(helperBin); + logTestInfo("Running " + helperBin.path + " " + args.join(" ")); + process.run(true, args, args.length); + do_check_eq(process.exitValue, 0); +} + +function createSymlink() { + let args = ["setup-symlink", "moz-foo", "moz-bar", "target", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link"]; + runHelperProcess(args); + getApplyDirFile(DIR_RESOURCES + "link", false).permissions = 0o666; + + args = ["setup-symlink", "moz-foo2", "moz-bar2", "target2", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link2", "change-perm"]; + runHelperProcess(args); +} + +function removeSymlink() { + let args = ["remove-symlink", "moz-foo", "moz-bar", "target", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link"]; + runHelperProcess(args); + args = ["remove-symlink", "moz-foo2", "moz-bar2", "target2", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link2"]; + runHelperProcess(args); +} + +function checkSymlink() { + let args = ["check-symlink", + getApplyDirFile().path + "/" + DIR_RESOURCES + "link"]; + runHelperProcess(args); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js new file mode 100644 index 000000000..272c468e4 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js @@ -0,0 +1,101 @@ +/* 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/. + */ + +/* General Partial MAR File Staged Patch Apply Test */ + +function run_test() { + if (!shouldRunServiceTest(false, true)) { + return; + } + + gStageUpdate = true; + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 2].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(false); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on all updates since + // the precomplete file in the root of the bundle is renamed, etc. (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_APPLIED); +} + +function checkUpdateFinished() { + checkFilesAfterUpdateSuccess(getStageDirFile, true, false); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + + if (IS_WIN || IS_MACOSX) { + let running = getPostUpdateFile(".running"); + logTestInfo("checking that the post update process running file doesn't " + + "exist. Path: " + running.path); + do_check_false(running.exists()); + } + + // Switch the application to the staged application that was updated. + gStageUpdate = false; + gSwitchApp = true; + do_timeout(TEST_CHECK_TIMEOUT, function() { + runUpdate(0, STATE_SUCCEEDED); + }); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateApplied() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateApplied; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateApplied(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateApplied); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateApplied() { + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// let running = getPostUpdateFile(".running"); +// logTestInfo("checking that the post update process running file exists. " + +// "Path: " + running.path); +// do_check_true(running.exists()); +// } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + checkCallbackAppLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js new file mode 100644 index 000000000..c6eafcaaf --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js @@ -0,0 +1,70 @@ +/* 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/. + */ + +/* General Complete MAR File Patch Apply Test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesCompleteSuccess; + gTestDirs = gTestDirsCompleteSuccess; + setupUpdaterTest(FILE_COMPLETE_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on a successful + // update (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateFinished() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateFinished; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateFinished(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateFinished); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateFinished() { + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_COMPLETE_SUCCESS); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js new file mode 100644 index 000000000..c241f67ec --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js @@ -0,0 +1,76 @@ +/* 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/. + */ + +/* General Partial MAR File Patch Apply Test */ + +function run_test() { + if (!shouldRunServiceTest()) { + return; + } + + setupTestCommon(); + gTestFiles = gTestFilesPartialSuccess; + gTestFiles[gTestFiles.length - 1].originalContents = null; + gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 1].comparePerms = 0o644; + gTestFiles[gTestFiles.length - 2].originalContents = null; + gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n"; + gTestFiles[gTestFiles.length - 2].comparePerms = 0o644; + gTestDirs = gTestDirsPartialSuccess; + setupUpdaterTest(FILE_PARTIAL_MAR); + +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// createUpdaterINI(true); + + // For Mac OS X set the last modified time for the root directory to a date in + // the past to test that the last modified time is updated on all updates since + // the precomplete file in the root of the bundle is renamed, etc. (bug 600098). + if (IS_MACOSX) { + let now = Date.now(); + let yesterday = now - (1000 * 60 * 60 * 24); + let applyToDir = getApplyDirFile(); + applyToDir.lastModifiedTime = yesterday; + } + + setupAppFilesAsync(); +} + +function setupAppFilesFinished() { + runUpdateUsingService(STATE_PENDING_SVC, STATE_SUCCEEDED); +} + +/** + * Checks if the post update binary was properly launched for the platforms that + * support launching post update process. + */ +function checkUpdateFinished() { +// This is commented out on mozilla-esr38 since it doesn't have the test updater +// if (IS_WIN || IS_MACOSX) { +// gCheckFunc = finishCheckUpdateFinished; +// checkPostUpdateAppLog(); +// } else { +// finishCheckUpdateFinished(); +// } + do_timeout(TEST_HELPER_TIMEOUT, finishCheckUpdateFinished); +} + +/** + * Checks if the update has finished and if it has finished performs checks for + * the test. + */ +function finishCheckUpdateFinished() { + if (IS_MACOSX) { + logTestInfo("testing last modified time on the apply to directory has " + + "changed after a successful update (bug 600098)"); + let now = Date.now(); + let applyToDir = getApplyDirFile(); + let timeDiff = Math.abs(applyToDir.lastModifiedTime - now); + do_check_true(timeDiff < MAC_MAX_TIME_DIFFERENCE); + } + + checkFilesAfterUpdateSuccess(getApplyDirFile, false, false); + checkUpdateLogContents(LOG_PARTIAL_SUCCESS); + checkCallbackServiceLog(); +} diff --git a/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini new file mode 100644 index 000000000..43b24c783 --- /dev/null +++ b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini @@ -0,0 +1,83 @@ +; 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/. + +; Tests that require the updater binary and the maintenance service. + +[DEFAULT] +head = head_update.js +tail = +generated-files = head_update.js + +[bootstrapSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marSuccessCompleteSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marSuccessPartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFailurePartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageSuccessCompleteSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageSuccessPartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marStageFailurePartialSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppSuccessCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppSuccessPartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppStageSuccessCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marCallbackAppStageSuccessPartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppInUseSuccessCompleteSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppInUseStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppInUseFallbackStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedFallbackStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileLockedFallbackStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseSuccessCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseSuccessPartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseSuccessCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseSuccessPartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseFallbackStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marFileInUseFallbackStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseFallbackStageFailureCompleteSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marRMRFDirFileInUseFallbackStageFailurePartialSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyDirLockedStageFailureSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateSuccessSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. +[marAppApplyUpdateStageSuccessSvc.js] +run-sequentially = Uses the Mozilla Maintenance Service. diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp index 766d278bc..1a5a17f36 100644 --- a/toolkit/mozapps/update/updater/updater.cpp +++ b/toolkit/mozapps/update/updater/updater.cpp @@ -291,6 +291,7 @@ static ArchiveReader gArchiveReader; static bool gSucceeded = false; static bool sStagedUpdate = false; static bool sReplaceRequest = false; +static bool sUsingService = false; static bool sIsOSUpdate = false; #ifdef XP_WIN @@ -1823,7 +1824,8 @@ IsUpdateFromMetro(int argc, NS_tchar **argv) /** * Launch the post update application (helper.exe). It takes in the path of the - * callback application to calculate the path of helper.exe. + * callback application to calculate the path of helper.exe. For service updates + * this is called from both the system account and the current user account. * * @param installationDir The path to the callback application binary. * @param updateInfoDir The directory where update info is stored. @@ -1876,6 +1878,14 @@ LaunchWinPostProcess(const WCHAR *installationDir, return false; } +// TEST_UPDATER is not available on esr38 +//#if !defined(TEST_UPDATER) + if (sUsingService && + !DoesBinaryMatchAllowedCertificates(installationDir, exefullpath)) { + return false; + } +//#endif + WCHAR dlogFile[MAX_PATH + 1]; if (!PathGetSiblingFilePath(dlogFile, exefullpath, L"uninstall.update")) { return false; @@ -1899,7 +1909,8 @@ LaunchWinPostProcess(const WCHAR *installationDir, wcsncpy(cmdline, dummyArg, len); wcscat(cmdline, exearg); - if (!_wcsnicmp(exeasync, L"false", 6) || + if (sUsingService || + !_wcsnicmp(exeasync, L"false", 6) || !_wcsnicmp(exeasync, L"0", 2)) { async = false; } @@ -1939,7 +1950,8 @@ LaunchWinPostProcess(const WCHAR *installationDir, static void LaunchCallbackApp(const NS_tchar *workingDir, int argc, - NS_tchar **argv) + NS_tchar **argv, + bool usingService) { putenv(const_cast<char*>("NO_EM_RESTART=")); putenv(const_cast<char*>("MOZ_LAUNCHED_CHILD=1")); @@ -1956,15 +1968,19 @@ LaunchCallbackApp(const NS_tchar *workingDir, #elif defined(XP_MACOSX) LaunchChild(argc, argv); #elif defined(XP_WIN) + // Do not allow the callback to run when running an update through the + // service as session 0. The unelevated updater.exe will do the launching. + if (!usingService) { #if defined(MOZ_METRO) - // If our callback application is the default metro browser, then - // launch it now. - if (IsUpdateFromMetro(argc, argv)) { - LaunchDefaultMetroBrowser(); - return; - } + // If our callback application is the default metro browser, then + // launch it now. + if (IsUpdateFromMetro(argc, argv)) { + LaunchDefaultMetroBrowser(); + return; + } #endif - WinLaunchChild(argv[0], argc, argv, nullptr); + WinLaunchChild(argv[0], argc, argv, nullptr); + } #else # warning "Need implementaton of LaunchCallbackApp" #endif @@ -2033,6 +2049,40 @@ WriteStatusFile(int status) WriteStatusFile(text); } +#ifdef MOZ_MAINTENANCE_SERVICE +/* + * Read the update.status file and sets isPendingService to true if + * the status is set to pending-service. + * + * @param isPendingService Out parameter for specifying if the status + * is set to pending-service or not. + * @return true if the information was retrieved and it is pending + * or pending-service. +*/ +static bool +IsUpdateStatusPendingService() +{ + NS_tchar filename[MAXPATHLEN]; + NS_tsnprintf(filename, sizeof(filename)/sizeof(filename[0]), + NS_T("%s/update.status"), gPatchDirPath); + + AutoFile file(NS_tfopen(filename, NS_T("rb"))); + if (file == nullptr) + return false; + + char buf[32] = { 0 }; + fread(buf, sizeof(buf), 1, file); + + const char kPendingService[] = "pending-service"; + const char kAppliedService[] = "applied-service"; + + return (strncmp(buf, kPendingService, + sizeof(kPendingService) - 1) == 0) || + (strncmp(buf, kAppliedService, + sizeof(kAppliedService) - 1) == 0); +} +#endif + #ifdef XP_WIN /* * Read the update.status file and sets isSuccess to true if @@ -2223,6 +2273,17 @@ ProcessReplaceRequest() return 0; } +#ifdef XP_WIN +static void +WaitForServiceFinishThread(void *param) +{ + // We wait at most 10 minutes, we already waited 5 seconds previously + // before deciding to show this UI. + WaitForServiceStop(SVC_NAME, 595); + QuitProgressUI(); +} +#endif + #ifdef MOZ_VERIFY_MAR_SIGNATURE /** * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from update-settings.ini @@ -2392,7 +2453,7 @@ UpdateThreadFunc(void *param) // The only special thing which we should do here is to remove the // staged directory as it won't be useful any more. ensure_remove_recursive(gWorkingDirPath); - WriteStatusFile("pending"); + WriteStatusFile(sUsingService ? "pending-service" : "pending"); putenv(const_cast<char*>("MOZ_PROCESS_UPDATES=")); // We need to use --process-updates again in the tests reportRealResults = false; // pretend success } @@ -2474,6 +2535,21 @@ int NS_main(int argc, NS_tchar **argv) } #ifdef XP_WIN + bool useService = false; + bool testOnlyFallbackKeyExists = false; + bool noServiceFallback = getenv("MOZ_NO_SERVICE_FALLBACK") != nullptr; + putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK=")); + + // We never want the service to be used unless we build with + // the maintenance service. +#ifdef MOZ_MAINTENANCE_SERVICE + useService = IsUpdateStatusPendingService(); + // Our tests run with a different apply directory for each test. + // We use this registry key on our test slaves to store the + // allowed name/issuers. + testOnlyFallbackKeyExists = DoesFallbackKeyExist(); +#endif + // Remove everything except close window from the context menu { HKEY hkApp = nullptr; @@ -2660,11 +2736,21 @@ int NS_main(int argc, NS_tchar **argv) const int callbackIndex = 6; #if defined(XP_WIN) + sUsingService = getenv("MOZ_USING_SERVICE") != nullptr; + putenv(const_cast<char*>("MOZ_USING_SERVICE=")); + // lastFallbackError keeps track of the last error for the service not being + // used, in case of an error when fallback is not enabled we write the + // error to the update.status file. + // When fallback is disabled (MOZ_NO_SERVICE_FALLBACK does not exist) then + // we will instead fallback to not using the service and display a UAC prompt. + int lastFallbackError = FALLBACKKEY_UNKNOWN_ERROR; + // Launch a second instance of the updater with the runas verb on Windows // when write access is denied to the installation directory. HANDLE updateLockFileHandle = INVALID_HANDLE_VALUE; NS_tchar elevatedLockFilePath[MAXPATHLEN] = {NS_T('\0')}; - if (argc > callbackIndex || sStagedUpdate || sReplaceRequest) { + if (!sUsingService && + (argc > callbackIndex || sStagedUpdate || sReplaceRequest)) { NS_tchar updateLockFilePath[MAXPATHLEN]; if (sStagedUpdate) { // When staging an update, the lock file is: @@ -2722,17 +2808,20 @@ int NS_main(int argc, NS_tchar **argv) bool startedFromUnelevatedUpdater = GetFileAttributesW(elevatedLockFilePath) != INVALID_FILE_ATTRIBUTES; - // If we're running from an elevated updater that was started from an - // unelevated updater, then we drop the permissions here. We do not drop - // the permissions on the originally called updater because we use its - // token to start the callback application. + // If we're running from the service, then we were started with the same + // token as the service so the permissions are already dropped. If we're + // running from an elevated updater that was started from an unelevated + // updater, then we drop the permissions here. We do not drop the + // permissions on the originally called updater because we use its token + // to start the callback application. if (startedFromUnelevatedUpdater) { // Disable every privilege we don't need. Processes started using // CreateProcess will use the same token as this process. UACHelper::DisablePrivileges(nullptr); } - if (updateLockFileHandle == INVALID_HANDLE_VALUE) { + if (updateLockFileHandle == INVALID_HANDLE_VALUE || + (useService && testOnlyFallbackKeyExists && noServiceFallback)) { if (!_waccess(elevatedLockFilePath, F_OK) && NS_tremove(elevatedLockFilePath) != 0) { fprintf(stderr, "Unable to create elevated lock file! Exiting\n"); @@ -2759,10 +2848,102 @@ int NS_main(int argc, NS_tchar **argv) return 1; } - // When staging an update, make sure that the UAC prompt is not shown! - // In this case, just set the status to pending and the update will - // be applied during the next startup. - if (sStagedUpdate) { + // Make sure the path to the updater to use for the update is on local. + // We do this check to make sure that file locking is available for + // race condition security checks. + if (useService) { + BOOL isLocal = FALSE; + useService = IsLocalFile(argv[0], isLocal) && isLocal; + } + + // If we have unprompted elevation we should NOT use the service + // for the update. Service updates happen with the SYSTEM account + // which has more privs than we need to update with. + // Windows 8 provides a user interface so users can configure this + // behavior and it can be configured in the registry in all Windows + // versions that support UAC. + if (useService) { + BOOL unpromptedElevation; + if (IsUnpromptedElevation(unpromptedElevation)) { + useService = !unpromptedElevation; + } + } + + // Make sure the service registry entries for the instsallation path + // are available. If not don't use the service. + if (useService) { + WCHAR maintenanceServiceKey[MAX_PATH + 1]; + if (CalculateRegistryPathFromFilePath(gInstallDirPath, maintenanceServiceKey)) { + HKEY baseKey = nullptr; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, + maintenanceServiceKey, 0, + KEY_READ | KEY_WOW64_64KEY, + &baseKey) == ERROR_SUCCESS) { + RegCloseKey(baseKey); + } else { + useService = testOnlyFallbackKeyExists; + if (!useService) { + lastFallbackError = FALLBACKKEY_NOKEY_ERROR; + } + } + } else { + useService = false; + lastFallbackError = FALLBACKKEY_REGPATH_ERROR; + } + } + + // Originally we used to write "pending" to update.status before + // launching the service command. This is no longer needed now + // since the service command is launched from updater.exe. If anything + // fails in between, we can fall back to using the normal update process + // on our own. + + // If we still want to use the service try to launch the service + // comamnd for the update. + if (useService) { + // If the update couldn't be started, then set useService to false so + // we do the update the old way. + DWORD ret = LaunchServiceSoftwareUpdateCommand(argc, (LPCWSTR *)argv); + useService = (ret == ERROR_SUCCESS); + // If the command was launched then wait for the service to be done. + if (useService) { + bool showProgressUI = false; + // Never show the progress UI when staging updates. + if (!sStagedUpdate) { + // We need to call this separately instead of allowing ShowProgressUI + // to initialize the strings because the service will move the + // ini file out of the way when running updater. + showProgressUI = !InitProgressUIStrings(); + } + + // Wait for the service to stop for 5 seconds. If the service + // has still not stopped then show an indeterminate progress bar. + DWORD lastState = WaitForServiceStop(SVC_NAME, 5); + if (lastState != SERVICE_STOPPED) { + Thread t1; + if (t1.Run(WaitForServiceFinishThread, nullptr) == 0 && + showProgressUI) { + ShowProgressUI(true, false); + } + t1.Join(); + } + + lastState = WaitForServiceStop(SVC_NAME, 1); + if (lastState != SERVICE_STOPPED) { + // If the service doesn't stop after 10 minutes there is + // something seriously wrong. + lastFallbackError = FALLBACKKEY_SERVICE_NO_STOP_ERROR; + useService = false; + } + } else { + lastFallbackError = FALLBACKKEY_LAUNCH_ERROR; + } + } + + // If the service can't be used when staging an update, make sure that + // the UAC prompt is not shown! In this case, just set the status to + // pending and the update will be applied during the next startup. + if (!useService && sStagedUpdate) { if (updateLockFileHandle != INVALID_HANDLE_VALUE) { CloseHandle(updateLockFileHandle); } @@ -2770,7 +2951,32 @@ int NS_main(int argc, NS_tchar **argv) return 0; } - if (updateLockFileHandle == INVALID_HANDLE_VALUE) { + // If we started the service command, and it finished, check the + // update.status file to make sure it succeeded, and if it did + // we need to manually start the PostUpdate process from the + // current user's session of this unelevated updater.exe the + // current process is running as. + // Note that we don't need to do this if we're just staging the update, + // as the PostUpdate step runs when performing the replacing in that case. + if (useService && !sStagedUpdate) { + bool updateStatusSucceeded = false; + if (IsUpdateStatusSucceeded(updateStatusSucceeded) && + updateStatusSucceeded) { + if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { + fprintf(stderr, "The post update process which runs as the user" + " for service update could not be launched."); + } + } + } + + // If we didn't want to use the service at all, or if an update was + // already happening, or launching the service command failed, then + // launch the elevated updater.exe as we do without the service. + // We don't launch the elevated updater in the case that we did have + // write access all along because in that case the only reason we're + // using the service is because we are testing. + if (!useService && !noServiceFallback && + updateLockFileHandle == INVALID_HANDLE_VALUE) { SHELLEXECUTEINFO sinfo; memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO)); sinfo.cbSize = sizeof(SHELLEXECUTEINFO); @@ -2796,16 +3002,33 @@ int NS_main(int argc, NS_tchar **argv) if (argc > callbackIndex) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex); + argv + callbackIndex, sUsingService); } CloseHandle(elevatedFileHandle); - if (INVALID_HANDLE_VALUE == updateLockFileHandle) { - // We ran the elevated updater.exe. + if (!useService && !noServiceFallback && + INVALID_HANDLE_VALUE == updateLockFileHandle) { + // We didn't use the service and we did run the elevated updater.exe. // The elevated updater.exe is responsible for writing out the // update.status file. return 0; + } else if(useService) { + // The service command was launched. The service is responsible for + // writing out the update.status file. + if (updateLockFileHandle != INVALID_HANDLE_VALUE) { + CloseHandle(updateLockFileHandle); + } + return 0; + } else { + // Otherwise the service command was not launched at all. + // We are only reaching this code path because we had write access + // all along to the directory and a fallback key existed, and we + // have fallback disabled (MOZ_NO_SERVICE_FALLBACK env var exists). + // We only currently use this env var from XPCShell tests. + CloseHandle(updateLockFileHandle); + WriteStatusFile(lastFallbackError); + return 0; } } } @@ -2886,7 +3109,7 @@ int NS_main(int argc, NS_tchar **argv) EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); if (argc > callbackIndex) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex); + argv + callbackIndex, sUsingService); } return 1; } @@ -2939,7 +3162,8 @@ int NS_main(int argc, NS_tchar **argv) if (argc > callbackIndex) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex); + argv + callbackIndex, + sUsingService); } return 1; } @@ -3019,7 +3243,8 @@ int NS_main(int argc, NS_tchar **argv) EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1); LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex); + argv + callbackIndex, + sUsingService); return 1; } LOG(("NS_main: callback app file in use, continuing without " \ @@ -3134,6 +3359,17 @@ int NS_main(int argc, NS_tchar **argv) if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) { fprintf(stderr, "The post update process was not launched"); } + + // The service update will only be executed if it is already installed. + // For first time installs of the service, the install will happen from + // the PostUpdate process. We do the service update process here + // because it's possible we are updating with updater.exe without the + // service if the service failed to apply the update. We want to update + // the service to a newer version in that case. If we are not running + // through the service, then MOZ_USING_SERVICE will not exist. + if (!sUsingService) { + StartServiceUpdate(gInstallDirPath); + } } EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 0); #endif /* XP_WIN */ @@ -3146,7 +3382,8 @@ int NS_main(int argc, NS_tchar **argv) if (getenv("MOZ_PROCESS_UPDATES") == nullptr) { LaunchCallbackApp(argv[5], argc - callbackIndex, - argv + callbackIndex); + argv + callbackIndex, + sUsingService); } } |