in toolkit/mozapps/update/updater/updater.cpp [3072:4443]
int NS_main(int argc, NS_tchar** argv) {
// We may need to tweak our argument list when we launch the Second Updater
// Invocation (SUI), so we are going to make a copy of our arguments to
// modify.
int suiArgc = argc;
mozilla::UniquePtr<const NS_tchar*[]> suiArgv =
mozilla::MakeUnique<const NS_tchar*[]>(suiArgc);
for (int argIndex = 0; argIndex < suiArgc; argIndex++) {
suiArgv.get()[argIndex] = argv[argIndex];
}
#ifdef MOZ_MAINTENANCE_SERVICE
sUsingService = EnvHasValue("MOZ_USING_SERVICE");
putenv(const_cast<char*>("MOZ_USING_SERVICE="));
#endif
if (argc == 2 && NS_tstrcmp(argv[1], NS_T("--channels-allowed")) == 0) {
#ifdef MOZ_VERIFY_MAR_SIGNATURE
int rv = PopulategMARStrings();
if (rv == OK) {
printf("Channels Allowed: '%s'\n", gMARStrings.MARChannelID.get());
return 0;
}
printf("Error: %d\n", rv);
return 1;
#else
printf("Not Applicable: No support for signature verification\n");
return 0;
#endif
}
// `isDMGInstall` is only ever true for macOS, but we are declaring it here
// to avoid a ton of extra #ifdef's.
bool isDMGInstall = false;
#ifdef XP_MACOSX
if (argc > 2 && NS_tstrcmp(argv[1], NS_T("--openAppBundle")) == 0) {
// We have been asked to open a .app bundle. The path to the .app bundle and
// any command line arguments have been passed to us as arguments after
// "--openAppBundle", so remove the first two arguments and launch the .app
// bundle.
LaunchMacApp(argc - 2, (const char**)argv + 2);
return 0;
}
// We want to control file permissions explicitly, or else we could end up
// corrupting installs for other users on the system. Accordingly, set the
// umask to 0 for all file creations below and reset it on exit. See Bug
// 1337007
mozilla::UniquePtr<UmaskContext> umaskContext(new UmaskContext(0));
#endif
#ifdef XP_WIN
auto isAdmin = mozilla::UserHasAdminPrivileges();
if (isAdmin.isErr()) {
fprintf(stderr,
"Failed to query if the current process has admin privileges.\n");
return 1;
}
auto isLocalSystem = mozilla::UserIsLocalSystem();
if (isLocalSystem.isErr()) {
fprintf(
stderr,
"Failed to query if the current process has LocalSystem privileges.\n");
return 1;
}
#endif
// Indicates that we are running with elevated privileges.
// This is only ever true on macOS and Windows. We don't currently have a
// way of elevating on other platforms.
// Note that this should not be used to determine whether this is the first or
// second invocation of the updater, even though the first invocation will
// _usually_ be unelevated and the second invocation should always be
// elevated. `gInvocation` can be used for that purpose.
bool isElevated =
#ifdef XP_WIN
// While is it technically redundant to check LocalSystem in addition to
// Admin given the former contains privileges of the latter, we have opt
// to verify both. A few reasons for this decision include the off chance
// that the Windows security model changes in the future and weird system
// setups where someone has modified the group lists in surprising ways.
//
// We use this to detect if we were launched from the Maintenance Service
// under LocalSystem or UAC under the user's account, and therefore can
// proceed with an install to `Program Files` or `Program Files(x86)`.
isAdmin.unwrap() || isLocalSystem.unwrap();
#elif defined(XP_MACOSX)
strstr(argv[0], "/Library/PrivilegedHelperTools/org.mozilla.updater") !=
0;
#else
false;
#endif
#ifdef XP_MACOSX
if (isElevated) {
if (!ObtainUpdaterArguments(&argc, &argv, &gMARStrings)) {
// Won't actually get here because ObtainUpdaterArguments will terminate
// the current process on failure.
return 1;
}
}
if (argc == 4 && (strstr(argv[1], "-dmgInstall") != 0)) {
isDMGInstall = true;
if (isElevated) {
PerformInstallationFromDMG(argc, argv);
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
return 0;
}
}
#endif
if (!isDMGInstall) {
// Skip update-related code path for DMG installs.
#if defined(MOZ_VERIFY_MAR_SIGNATURE) && defined(MAR_NSS)
// If using NSS for signature verification, initialize NSS but minimize
// the portion we depend on by avoiding all of the NSS databases.
if (NSS_NoDB_Init(nullptr) != SECSuccess) {
PRErrorCode error = PR_GetError();
fprintf(stderr, "Could not initialize NSS: %s (%d)",
PR_ErrorToName(error), (int)error);
_exit(1);
}
#endif
// To process an update the updater command line must at a minimum have the
// argument version as the first argument, the directory path containing the
// updater.mar file to process as the second argument, the install directory
// as the third argument, the directory to apply the update to as the fourth
// argument, and which updater invocation this is as the fifth argument.
// When the updater is launched by another process, the PID of the parent
// process should be provided in the optional sixth argument and the updater
// will wait on the parent process to exit if the value is non-zero and the
// process is present. This is necessary due to not being able to update
// files that are in use on Windows. The optional seventh argument is the
// callback's working directory and the optional eighth argument is the
// callback path. The callback is the application to launch after updating
// and it will be launched when these arguments are provided whether the
// update was successful or not. All remaining arguments are optional and
// are passed to the callback when it is launched.
if (argc < kWaitPidIndex) {
fprintf(stderr,
"Usage: updater arg-version patch-dir install-dir apply-to-dir "
"which-invocation [wait-pid [callback-working-dir callback-path "
"args...]]\n");
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
return 1;
}
#if defined(TEST_UPDATER) && defined(XP_WIN)
// The tests use nsIProcess to launch the updater and it is simpler for the
// tests to just set an environment variable and have the test updater set
// the current working directory than it is to set the current working
// directory in the test itself.
if (EnvHasValue("CURWORKDIRPATH")) {
const WCHAR* val = _wgetenv(L"CURWORKDIRPATH");
NS_tchdir(val);
}
#endif
gInvocation = getUpdaterInvocationFromArg(argv[kWhichInvocationIndex]);
switch (gInvocation) {
case UpdaterInvocation::Unknown:
fprintf(stderr, "Invalid which-invocation value: " LOG_S "\n",
argv[kWhichInvocationIndex]);
return 1;
case UpdaterInvocation::First:
suiArgv.get()[kWhichInvocationIndex] = secondUpdateInvocationArg;
break;
default:
// There is no good reason we should be launching a third updater, but
// assign something recognizable and unlikely to be used in the future
// to make any bugs here a bit easier to understand.
suiArgv.get()[kWhichInvocationIndex] = NS_T("third???");
break;
}
} else { /* else if (isDMGInstall) */
// We already exited in the other case.
gInvocation = UpdaterInvocation::First;
}
// The directory containing the update information.
NS_tstrncpy(gPatchDirPath, argv[kPatchDirIndex], MAXPATHLEN);
gPatchDirPath[MAXPATHLEN - 1] = NS_T('\0');
if (!isDMGInstall) {
// This check is also performed in workmonitor.cpp since the maintenance
// service can be called directly.
if (!IsValidFullPath(argv[kPatchDirIndex])) {
// Since the status file is written to the patch directory and the patch
// directory is invalid don't write the status file.
fprintf(stderr,
"The patch directory path is not valid for this "
"application (" LOG_S ")\n",
argv[kPatchDirIndex]);
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
return 1;
}
// This check is also performed in workmonitor.cpp since the maintenance
// service can be called directly.
if (!IsValidFullPath(argv[kInstallDirIndex])) {
WriteStatusFile(INVALID_INSTALL_DIR_PATH_ERROR);
fprintf(stderr,
"The install directory path is not valid for this "
"application (" LOG_S ")\n",
argv[kInstallDirIndex]);
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
return 1;
}
} // if (!isDMGInstall)
// The directory we're going to update to.
// We copy this string because we need to remove trailing slashes. The C++
// standard says that it's always safe to write to strings pointed to by argv
// elements, but I don't necessarily believe it.
NS_tstrncpy(gInstallDirPath, argv[kInstallDirIndex], MAXPATHLEN);
gInstallDirPath[MAXPATHLEN - 1] = NS_T('\0');
NS_tchar* slash = NS_tstrrchr(gInstallDirPath, NS_SLASH);
if (slash && !slash[1]) {
*slash = NS_T('\0');
}
#ifdef XP_WIN
bool useService = false;
bool testOnlyFallbackKeyExists = false;
// Prevent the updater from falling back from updating with the Maintenance
// Service to updating without the Service. Used for Service tests.
// This is set below via the MOZ_NO_SERVICE_FALLBACK environment variable.
bool noServiceFallback = false;
// Force the updater to use the Maintenance Service incorrectly, causing it
// to fail. Used to test the mechanism that allows the updater to fall back
// from using the Maintenance Service to updating without it.
// This is set below via the MOZ_FORCE_SERVICE_FALLBACK environment variable.
bool forceServiceFallback = false;
#endif
if (!isDMGInstall) {
#ifdef XP_WIN
// We never want the service to be used unless we build with
// the maintenance service.
# ifdef MOZ_MAINTENANCE_SERVICE
useService = IsUpdateStatusPendingService();
# ifdef TEST_UPDATER
noServiceFallback = EnvHasValue("MOZ_NO_SERVICE_FALLBACK");
putenv(const_cast<char*>("MOZ_NO_SERVICE_FALLBACK="));
forceServiceFallback = EnvHasValue("MOZ_FORCE_SERVICE_FALLBACK");
putenv(const_cast<char*>("MOZ_FORCE_SERVICE_FALLBACK="));
// Our tests run with a different apply directory for each test.
// We use this registry key on our test machines to store the
// allowed name/issuers.
testOnlyFallbackKeyExists = DoesFallbackKeyExist();
# endif
# endif
// Remove everything except close window from the context menu
{
HKEY hkApp = nullptr;
RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications", 0,
nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr,
&hkApp, nullptr);
RegCloseKey(hkApp);
if (RegCreateKeyExW(HKEY_CURRENT_USER,
L"Software\\Classes\\Applications\\updater.exe", 0,
nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr,
&hkApp, nullptr) == ERROR_SUCCESS) {
RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0);
RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0);
RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0);
RegCloseKey(hkApp);
}
}
#endif
} // if (!isDMGInstall)
// If there is a PID specified and it is not '0' then wait for the process to
// exit.
NS_tpid pid = 0;
if (argc > kWaitPidIndex) {
pid = NS_tatoi(argv[kWaitPidIndex]);
if (pid == -1) {
// This is a signal from the parent process that the updater should stage
// the update.
sStagedUpdate = true;
} else if (NS_tstrstr(argv[kWaitPidIndex], NS_T("/replace"))) {
// We're processing a request to replace the application with a staged
// update.
sReplaceRequest = true;
}
}
if (!isDMGInstall) {
// This check is also performed in workmonitor.cpp since the maintenance
// service can be called directly.
if (!IsValidFullPath(argv[kApplyToDirIndex])) {
WriteStatusFile(INVALID_WORKING_DIR_PATH_ERROR);
fprintf(stderr,
"The working directory path is not valid for this "
"application (" LOG_S ")\n",
argv[kApplyToDirIndex]);
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
return 1;
}
// The directory we're going to update to.
// We copy this string because we need to remove trailing slashes. The C++
// standard says that it's always safe to write to strings pointed to by
// argv elements, but I don't necessarily believe it.
NS_tstrncpy(gWorkingDirPath, argv[kApplyToDirIndex], MAXPATHLEN);
gWorkingDirPath[MAXPATHLEN - 1] = NS_T('\0');
slash = NS_tstrrchr(gWorkingDirPath, NS_SLASH);
if (slash && !slash[1]) {
*slash = NS_T('\0');
}
if (argc > kCallbackIndex) {
if (!IsValidFullPath(argv[kCallbackIndex])) {
WriteStatusFile(INVALID_CALLBACK_PATH_ERROR);
fprintf(stderr,
"The callback file path is not valid for this "
"application (" LOG_S ")\n",
argv[kCallbackIndex]);
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
return 1;
}
size_t len = NS_tstrlen(gInstallDirPath);
NS_tchar callbackInstallDir[MAXPATHLEN] = {NS_T('\0')};
NS_tstrncpy(callbackInstallDir, argv[kCallbackIndex], len);
if (NS_tstrcmp(gInstallDirPath, callbackInstallDir) != 0) {
WriteStatusFile(INVALID_CALLBACK_DIR_ERROR);
fprintf(stderr,
"The callback file must be located in the "
"installation directory (" LOG_S ")\n",
argv[kCallbackIndex]);
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
return 1;
}
sUpdateSilently =
ShouldRunSilently(argc - kCallbackIndex, argv + kCallbackIndex);
}
} // if (!isDMGInstall)
if (!sUpdateSilently && !isDMGInstall
#ifdef XP_MACOSX
&& !isElevated
#endif
) {
InitProgressUI(&argc, &argv);
}
#ifdef XP_MACOSX
if (!isElevated &&
(!IsRecursivelyWritable(argv[kInstallDirIndex]) || isDMGInstall)) {
// If the app directory isn't recursively writeable or if this is a DMG
// install, an elevated helper process is required.
if (sUpdateSilently) {
// An elevated update always requires an elevation dialog, so if we are
// updating silently, don't do an elevated update.
// This means that we cannot successfully perform silent updates from
// non-admin accounts on a Mac.
// It also means that we cannot silently perform the first update by an
// admin who was not the installing user. Once the first update has been
// installed, the permissions of the installation directory should be
// changed such that we don't need to elevate in the future.
// Firefox shouldn't actually launch the updater at all in this case. This
// is defense in depth.
WriteStatusFile(SILENT_UPDATE_NEEDED_ELEVATION_ERROR);
fprintf(stderr,
"Skipping update to avoid elevation prompt from silent update.");
} else {
UpdateServerThreadArgs threadArgs;
threadArgs.argc = suiArgc;
threadArgs.argv = suiArgv.get();
threadArgs.marChannelID = gMARStrings.MARChannelID.get();
Thread t1;
if (t1.Run(ServeElevatedUpdateThreadFunc, &threadArgs) == 0) {
// Show an indeterminate progress bar while an elevated update is in
// progress.
if (!isDMGInstall) {
ShowProgressUI(true);
}
}
t1.Join();
}
LaunchCallbackAndPostProcessApps(argc, argv, std::move(umaskContext));
return gSucceeded ? 0 : 1;
}
#endif
#ifdef XP_WIN
HANDLE updateLockFileHandle = INVALID_HANDLE_VALUE;
#endif
if (!isDMGInstall) {
NS_tchar logFilePath[MAXPATHLEN + 1] = {L'\0'};
#ifdef XP_WIN
if (gInvocation == UpdaterInvocation::Second) {
// Remove the secure output files so it is easier to determine when new
// files are created in the unelevated updater.
RemoveSecureOutputFiles(gPatchDirPath);
(void)GetSecureOutputFilePath(gPatchDirPath, L".log", logFilePath);
} else {
NS_tsnprintf(logFilePath, sizeof(logFilePath) / sizeof(logFilePath[0]),
NS_T("%s\\%s"), gPatchDirPath, UpdateLogFilename());
}
#else
NS_tsnprintf(logFilePath, sizeof(logFilePath) / sizeof(logFilePath[0]),
NS_T("%s/%s"), gPatchDirPath, UpdateLogFilename());
#endif
LogInit(logFilePath);
LOG(("sUsingService=%s", sUsingService ? "true" : "false"));
LOG(("sUpdateSilently=%s", sUpdateSilently ? "true" : "false"));
#ifdef XP_WIN
// Note that this is not the final value of useService
LOG(("useService=%s", useService ? "true" : "false"));
#endif
LOG(("isElevated=%s", isElevated ? "true" : "false"));
LOG(("gInvocation=%s", getUpdaterInvocationString(gInvocation)));
if (!WriteStatusFile("applying")) {
LOG(("failed setting status to 'applying'"));
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
output_finish();
return 1;
}
if (sStagedUpdate) {
LOG(("Performing a staged update"));
} else if (sReplaceRequest) {
LOG(("Performing a replace request"));
}
LOG(("PATCH DIRECTORY " LOG_S, gPatchDirPath));
LOG(("INSTALLATION DIRECTORY " LOG_S, gInstallDirPath));
LOG(("WORKING DIRECTORY " LOG_S, gWorkingDirPath));
#if defined(XP_WIN)
// These checks are also performed in workmonitor.cpp since the maintenance
// service can be called directly.
if (_wcsnicmp(gWorkingDirPath, gInstallDirPath, MAX_PATH) != 0) {
if (!sStagedUpdate && !sReplaceRequest) {
WriteStatusFile(INVALID_APPLYTO_DIR_ERROR);
LOG(
("Installation directory and working directory must be the same "
"for non-staged updates. Exiting."));
output_finish();
return 1;
}
NS_tchar workingDirParent[MAX_PATH];
NS_tsnprintf(workingDirParent,
sizeof(workingDirParent) / sizeof(workingDirParent[0]),
NS_T("%s"), gWorkingDirPath);
if (!PathRemoveFileSpecW(workingDirParent)) {
WriteStatusFile(REMOVE_FILE_SPEC_ERROR);
LOG(("Error calling PathRemoveFileSpecW: %lu", GetLastError()));
output_finish();
return 1;
}
if (_wcsnicmp(workingDirParent, gInstallDirPath, MAX_PATH) != 0) {
WriteStatusFile(INVALID_APPLYTO_DIR_STAGED_ERROR);
LOG(
("The apply-to directory must be the same as or "
"a child of the installation directory! Exiting."));
output_finish();
return 1;
}
}
#endif
#ifdef XP_WIN
if (pid > 0) {
HANDLE parent = OpenProcess(SYNCHRONIZE, false, (DWORD)pid);
// May return nullptr if the parent process has already gone away.
// Otherwise, wait for the parent process to exit before starting the
// update.
if (parent) {
DWORD waitTime = PARENT_WAIT;
# ifdef TEST_UPDATER
if (EnvHasValue("MOZ_TEST_SHORTER_WAIT_PID")) {
// Use a shorter time to wait for the PID to exit for the test.
waitTime = 100;
}
# endif
DWORD result = WaitForSingleObject(parent, waitTime);
CloseHandle(parent);
if (result != WAIT_OBJECT_0) {
// Continue to update since the parent application sometimes doesn't
// exit (see bug 1375242) so any fixes to the parent application will
// be applied instead of leaving the client in a broken state.
LOG(("The parent process didn't exit! Continuing with update."));
}
}
}
#endif
#ifdef XP_WIN
if (sReplaceRequest || sStagedUpdate) {
// On Windows, when performing a stage or replace request the current
// working directory for the process must be changed so it isn't locked.
NS_tchar sysDir[MAX_PATH + 1] = {L'\0'};
if (GetSystemDirectoryW(sysDir, MAX_PATH + 1)) {
NS_tchdir(sysDir);
}
}
// 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;
// Check whether a second instance of the updater should be launched by the
// maintenance service or with the 'runas' verb when write access is denied
// to the installation directory.
if (!sUsingService &&
(argc > kCallbackIndex || sStagedUpdate || sReplaceRequest)) {
LOG(("Checking whether elevation is needed"));
NS_tchar updateLockFilePath[MAXPATHLEN];
if (sStagedUpdate) {
// When staging an update, the lock file is:
// <install_dir>\updated.update_in_progress.lock
NS_tsnprintf(updateLockFilePath,
sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]),
NS_T("%s/updated.update_in_progress.lock"),
gInstallDirPath);
} else if (sReplaceRequest) {
// When processing a replace request, the lock file is:
// <install_dir>\..\moz_update_in_progress.lock
NS_tchar installDir[MAXPATHLEN];
NS_tstrcpy(installDir, gInstallDirPath);
NS_tchar* slash = (NS_tchar*)NS_tstrrchr(installDir, NS_SLASH);
*slash = NS_T('\0');
NS_tsnprintf(updateLockFilePath,
sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]),
NS_T("%s\\moz_update_in_progress.lock"), installDir);
} else {
// In the non-staging update case, the lock file is:
// <install_dir>\<app_name>.exe.update_in_progress.lock
NS_tsnprintf(updateLockFilePath,
sizeof(updateLockFilePath) / sizeof(updateLockFilePath[0]),
NS_T("%s.update_in_progress.lock"), argv[kCallbackIndex]);
}
// The update_in_progress.lock file should only exist during an update. In
// case it exists attempt to remove it and exit if that fails to prevent
// simultaneous updates occurring.
if (NS_tremove(updateLockFilePath) && errno != ENOENT) {
// Try to fall back to the old way of doing updates if a staged
// update fails.
if (sReplaceRequest) {
// Note that this could fail, but if it does, there isn't too much we
// can do in order to recover anyways.
WriteStatusFile("pending");
} else if (sStagedUpdate) {
WriteStatusFile(DELETE_ERROR_STAGING_LOCK_FILE);
}
LOG(("Update already in progress! Exiting"));
output_finish();
return 1;
}
// 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 (isElevated) {
// Disable every privilege we don't need. Processes started using
// CreateProcess will use the same token as this process.
UACHelper::DisablePrivileges(nullptr);
}
updateLockFileHandle =
CreateFileW(updateLockFilePath, GENERIC_READ | GENERIC_WRITE, 0,
nullptr, OPEN_ALWAYS, FILE_FLAG_DELETE_ON_CLOSE, nullptr);
if (updateLockFileHandle == INVALID_HANDLE_VALUE) {
LOG(("Failed to open update lock file: %lu", GetLastError()));
} else {
LOG(("Successfully opened lock file"));
}
if (updateLockFileHandle == INVALID_HANDLE_VALUE ||
(useService && testOnlyFallbackKeyExists &&
(noServiceFallback || forceServiceFallback))) {
LOG(("Can't open lock file - seems like we need elevation"));
# ifdef MOZ_MAINTENANCE_SERVICE
// Only invoke the service for installations in Program Files.
// This check is duplicated in workmonitor.cpp because the service can
// be invoked directly without going through the updater.
# ifndef TEST_UPDATER
if (useService) {
useService = IsProgramFilesPath(gInstallDirPath);
LOG(("After checking IsProgramFilesPath, useService=%s",
useService ? "true" : "false"));
}
# endif
// 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;
LOG(("After checking IsLocalFile, useService=%s",
useService ? "true" : "false"));
}
// 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;
LOG(("After checking IsUnpromptedElevation, useService=%s",
useService ? "true" : "false"));
}
}
// Make sure the service registry entries for the installation 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 {
# ifdef TEST_UPDATER
useService = testOnlyFallbackKeyExists;
LOG(("After failing to open maintenanceServiceKey, useService=%s",
useService ? "true" : "false"));
# endif
if (!useService) {
lastFallbackError = FALLBACKKEY_NOKEY_ERROR;
}
}
} else {
useService = false;
lastFallbackError = FALLBACKKEY_REGPATH_ERROR;
LOG(("Can't get registry certificate location. useService=false"));
}
}
// 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
// command for the update.
if (useService) {
// Get the secure ID before trying to update so it is possible to
// determine if the updater or the maintenance service has created a
// new one.
char uuidStringBefore[UUID_LEN] = {'\0'};
bool checkID = GetSecureID(uuidStringBefore);
// Write a catchall service failure status in case it fails without
// changing the status.
WriteStatusFile(SERVICE_UPDATE_STATUS_UNCHANGED);
int serviceArgc = argc;
if (forceServiceFallback && serviceArgc > kPatchDirIndex) {
// To force the service to fail, we can just pass it too few
// arguments. However, we don't want to pass it no arguments,
// because then it won't have enough information to write out the
// update status file telling us that it failed.
serviceArgc = kPatchDirIndex + 1;
}
// If the update couldn't be started, then set useService to false so
// we do the update the old way.
DWORD launchResult = LaunchServiceSoftwareUpdateCommand(
serviceArgc, (LPCWSTR*)suiArgv.get());
useService = (launchResult == ERROR_SUCCESS);
// If the command was launched then wait for the service to be done.
if (useService) {
LOG(("Launched service successfully"));
bool showProgressUI = false;
// Never show the progress UI when staging updates or in a
// background task.
if (!sStagedUpdate && !sUpdateSilently) {
// 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;
LOG(("Service didn't stop after 10 minutes. useService=false"));
} else {
LOG(("Service stop detected."));
// Copy the secure output files if the secure ID has changed.
gCopyOutputFiles = true;
char uuidStringAfter[UUID_LEN] = {'\0'};
if (checkID && GetSecureID(uuidStringAfter) &&
strncmp(uuidStringBefore, uuidStringAfter,
sizeof(uuidStringBefore)) == 0) {
LOG(
("The secure ID hasn't changed after launching the updater "
"using the service"));
gCopyOutputFiles = false;
}
if (gCopyOutputFiles && !sStagedUpdate && !noServiceFallback) {
// If the Maintenance Service fails for a Service-specific
// reason, we ought to fall back to attempting to update
// without the Service.
// However, we need the secure output files to be able to be
// check the error code, and we can't fall back when we are
// staging, because we will need to show a UAC.
bool updateFailed;
mozilla::Maybe<int> maybeErrorCode;
bool success =
IsSecureUpdateStatusFailed(updateFailed, &maybeErrorCode);
if (success && updateFailed && maybeErrorCode.isSome() &&
IsServiceSpecificErrorCode(maybeErrorCode.value())) {
useService = false;
LOG(("Service-specific failure detected. useService=false"));
}
}
}
} else {
LOG(("Launching service failed. useService=false, launchResult=%lu",
launchResult));
lastFallbackError = FALLBACKKEY_LAUNCH_ERROR;
}
}
# endif
// If the service can't be used when staging an update, make sure that
// the UAC prompt is not shown!
if (!useService && sStagedUpdate) {
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
// Set an error so the failure is reported. This will be reset
// to pending so the update can be applied during the next startup,
// see bug 1552853.
WriteStatusFile(UNEXPECTED_STAGING_ERROR);
LOG(
("Non-critical update staging error! Falling back to non-staged "
"updates and exiting"));
output_finish();
// We don't have a callback when staging so we can just exit.
return 0;
}
// If the service can't be used when in a background task, make sure
// that the UAC prompt is not shown!
if (!useService && sUpdateSilently) {
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
// Set an error so we don't get into an update loop when the callback
// runs. This will be reset to pending by handleUpdateFailure in
// UpdateService.sys.mjs.
WriteStatusFile(SILENT_UPDATE_NEEDED_ELEVATION_ERROR);
LOG(("Skipping update to avoid UAC prompt from background task."));
output_finish();
LaunchCallbackApp(argv[kCallbackWorkingDirIndex],
argc - kCallbackIndex, argv + kCallbackIndex,
sUsingService);
return 0;
}
// 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 ||
forceServiceFallback)) {
LOG(("Elevating via a UAC prompt"));
// Get the secure ID before trying to update so it is possible to
// determine if the updater has created a new one.
char uuidStringBefore[UUID_LEN] = {'\0'};
bool checkID = GetSecureID(uuidStringBefore);
// Write a catchall failure status in case it fails without changing
// the status.
WriteStatusFile(UPDATE_STATUS_UNCHANGED);
SHELLEXECUTEINFO sinfo;
memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO));
sinfo.cbSize = sizeof(SHELLEXECUTEINFO);
sinfo.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_FLAG_DDEWAIT |
SEE_MASK_NOCLOSEPROCESS;
sinfo.hwnd = nullptr;
sinfo.lpFile = argv[0];
if (forceServiceFallback) {
// In testing, we don't actually want a UAC prompt. We should
// already have the permissions such that we shouldn't need it.
// And we don't have a good way of accepting the prompt in
// automation.
sinfo.lpVerb = L"open";
// This argument is what lets the updater that we spawn below know
// that it's the second updater invocation. We are going to change
// it so that it doesn't know that it runs as the first updater
// invocation would. Doing this makes this an imperfect test of
// the service fallback functionality because it changes how the
// second updater invocation runs. One of the effects of this is
// that the secure output files will not be used. So that
// functionality won't really be covered by testing. But writing to
// those files would require that the updater run with actual
// elevation, which we have no way to do with in automation.
suiArgv.get()[kWhichInvocationIndex] = firstUpdateInvocationArg;
// We need to let go of the update lock to let the un-elevated
// updater we are about to spawn update.
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
} else {
sinfo.lpVerb = L"runas";
}
sinfo.nShow = SW_SHOWNORMAL;
auto cmdLine =
mozilla::MakeCommandLine(suiArgc - 1, suiArgv.get() + 1);
if (!cmdLine) {
LOG(("Failed to make command line! Exiting"));
output_finish();
return 1;
}
sinfo.lpParameters = cmdLine.get();
LOG(("Using UAC to launch \"%S\"", sinfo.lpParameters));
bool result = ShellExecuteEx(&sinfo);
if (result) {
LOG(("Elevation successful. Waiting for elevated updater to run."));
WaitForSingleObject(sinfo.hProcess, INFINITE);
LOG(("Elevated updater has finished running."));
CloseHandle(sinfo.hProcess);
// Copy the secure output files if the secure ID has changed.
gCopyOutputFiles = true;
char uuidStringAfter[UUID_LEN] = {'\0'};
if (checkID && GetSecureID(uuidStringAfter) &&
strncmp(uuidStringBefore, uuidStringAfter,
sizeof(uuidStringBefore)) == 0) {
LOG(
("The secure ID hasn't changed after launching the updater "
"using runas"));
gCopyOutputFiles = false;
}
} else {
// Don't copy the secure output files if the elevation request was
// canceled since the status file written below is in the patch
// directory. At this point it should already be set to false and
// this is set here to make it clear that it should be false at this
// point and to prevent future changes from regressing this code.
gCopyOutputFiles = false;
WriteStatusFile(ELEVATION_CANCELED);
LOG(("Elevation canceled."));
}
} else {
LOG(("Not showing a UAC prompt."));
LOG(("useService=%s", useService ? "true" : "false"));
LOG(("noServiceFallback=%s", noServiceFallback ? "true" : "false"));
LOG(("updateLockFileHandle%sINVALID_HANDLE_VALUE",
updateLockFileHandle == INVALID_HANDLE_VALUE ? "==" : "!="));
LOG(("forceServiceFallback=%s",
forceServiceFallback ? "true" : "false"));
}
// If we started the elevated updater, and it finished, check the secure
// update status file to make sure that it succeeded, and if it did we
// need to launch the PostUpdate process in the unelevated updater which
// is running in the current user's session. Note that we don't need to
// do this when staging an update since the PostUpdate step runs during
// the replace request.
if (!sStagedUpdate) {
bool updateStatusSucceeded = false;
if (IsSecureUpdateStatusSucceeded(updateStatusSucceeded) &&
updateStatusSucceeded) {
LOG(("Running LaunchWinPostProcess"));
if (!LaunchWinPostProcess(gInstallDirPath, gPatchDirPath)) {
LOG(("Failed to run LaunchWinPostProcess"));
}
} else {
LOG(
("Not running LaunchWinPostProcess because update status is not"
"'succeeded'."));
}
}
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
if (!useService && noServiceFallback) {
// When the service command was not launched at all.
// We should only reach 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.
gCopyOutputFiles = false;
WriteStatusFile(lastFallbackError);
}
// The logging output needs to be finished before launching the callback
// application so the update status file contains the value from the
// secure directory used by the maintenance service and the elevated
// updater.
LOG(("Update complete"));
output_finish();
if (argc > kCallbackIndex) {
LaunchCallbackApp(argv[kCallbackWorkingDirIndex],
argc - kCallbackIndex, argv + kCallbackIndex,
sUsingService);
}
return 0;
// This is the end of the code block for launching another instance of
// the updater using either the maintenance service or with the 'runas'
// verb when the updater doesn't have write access to the installation
// directory.
}
// This is the end of the code block when the updater was not launched by
// the service that checks whether the updater has write access to the
// installation directory.
}
// If we made it this far this is the updater instance that will perform the
// actual update and gCopyOutputFiles will be false (e.g. the default
// value).
LOG(("Going to update via this updater instance."));
#endif
if (sStagedUpdate) {
#ifdef TEST_UPDATER
// This allows testing that the correct UI after an update staging failure
// that falls back to applying the update on startup. It is simulated due
// to the difficulty of creating the conditions for this type of staging
// failure.
if (EnvHasValue("MOZ_TEST_STAGING_ERROR")) {
# ifdef XP_WIN
if (updateLockFileHandle != INVALID_HANDLE_VALUE) {
CloseHandle(updateLockFileHandle);
}
# endif
// WRITE_ERROR is one of the cases where the staging failure falls back
// to applying the update on startup.
WriteStatusFile(WRITE_ERROR);
output_finish();
return 0;
}
#endif
// When staging updates, blow away the old installation directory and
// create it from scratch.
ensure_remove_recursive(gWorkingDirPath);
}
if (!sReplaceRequest) {
// Try to create the destination directory if it doesn't exist
int rv = NS_tmkdir(gWorkingDirPath, 0755);
if (rv != OK && errno != EEXIST) {
#ifdef XP_MACOSX
if (isElevated) {
freeArguments(argc, argv);
CleanupElevatedMacUpdate(true);
}
#endif
output_finish();
return 1;
}
}
#ifdef XP_WIN
NS_tchar applyDirLongPath[MAXPATHLEN];
if (!GetLongPathNameW(
gWorkingDirPath, applyDirLongPath,
sizeof(applyDirLongPath) / sizeof(applyDirLongPath[0]))) {
WriteStatusFile(WRITE_ERROR_APPLY_DIR_PATH);
LOG(("NS_main: unable to find apply to dir: " LOG_S, gWorkingDirPath));
output_finish();
EXIT_IF_SECOND_UPDATER_INSTANCE(updateLockFileHandle, 1);
if (argc > kCallbackIndex) {
LaunchCallbackApp(argv[kCallbackWorkingDirIndex], argc - kCallbackIndex,
argv + kCallbackIndex, sUsingService);
}
return 1;
}
HANDLE callbackFile = INVALID_HANDLE_VALUE;
if (argc > kCallbackIndex) {
// If the callback executable is specified it must exist for a successful
// update. It is important we null out the whole buffer here because
// later we make the assumption that the callback application is inside
// the apply-to dir. If we don't have a fully null'ed out buffer it can
// lead to stack corruption which causes crashes and other problems.
NS_tchar callbackLongPath[MAXPATHLEN];
ZeroMemory(callbackLongPath, sizeof(callbackLongPath));
NS_tchar* targetPath = argv[kCallbackIndex];
NS_tchar buffer[MAXPATHLEN * 2] = {NS_T('\0')};
size_t bufferLeft = MAXPATHLEN * 2;
if (sReplaceRequest) {
// In case of replace requests, we should look for the callback file in
// the destination directory.
size_t commonPrefixLength =
PathCommonPrefixW(argv[kCallbackIndex], gInstallDirPath, nullptr);
NS_tchar* p = buffer;
NS_tstrncpy(p, argv[kCallbackIndex], commonPrefixLength);
p += commonPrefixLength;
bufferLeft -= commonPrefixLength;
NS_tstrncpy(p, gInstallDirPath + commonPrefixLength, bufferLeft);
size_t len = NS_tstrlen(gInstallDirPath + commonPrefixLength);
p += len;
bufferLeft -= len;
*p = NS_T('\\');
++p;
bufferLeft--;
*p = NS_T('\0');
NS_tchar installDir[MAXPATHLEN];
NS_tstrcpy(installDir, gInstallDirPath);
size_t callbackPrefixLength =
PathCommonPrefixW(argv[kCallbackIndex], installDir, nullptr);
NS_tstrncpy(p,
argv[kCallbackIndex] +
std::max(callbackPrefixLength, commonPrefixLength),
bufferLeft);
targetPath = buffer;
}
if (!GetLongPathNameW(
targetPath, callbackLongPath,
sizeof(callbackLongPath) / sizeof(callbackLongPath[0]))) {
WriteStatusFile(WRITE_ERROR_CALLBACK_PATH);
LOG(("NS_main: unable to find callback file: " LOG_S, targetPath));
output_finish();
EXIT_IF_SECOND_UPDATER_INSTANCE(updateLockFileHandle, 1);
if (argc > kCallbackIndex) {
LaunchCallbackApp(argv[kCallbackWorkingDirIndex],
argc - kCallbackIndex, argv + kCallbackIndex,
sUsingService);
}
return 1;
}
// Doing this is only necessary when we're actually applying a patch.
if (!sReplaceRequest) {
int len = NS_tstrlen(applyDirLongPath);
NS_tchar* s = callbackLongPath;
NS_tchar* d = gCallbackRelPath;
// advance to the apply to directory and advance past the trailing
// backslash if present.
s += len;
if (*s == NS_T('\\')) {
++s;
}
// Copy the string and replace backslashes with forward slashes along
// the way.
do {
if (*s == NS_T('\\')) {
*d = NS_T('/');
} else {
*d = *s;
}
++s;
++d;
} while (*s);
*d = NS_T('\0');
++d;
const size_t callbackBackupPathBufSize =
sizeof(gCallbackBackupPath) / sizeof(gCallbackBackupPath[0]);
const int callbackBackupPathLen =
NS_tsnprintf(gCallbackBackupPath, callbackBackupPathBufSize,
NS_T("%s" CALLBACK_BACKUP_EXT), argv[kCallbackIndex]);
if (callbackBackupPathLen < 0 ||
callbackBackupPathLen >=
static_cast<int>(callbackBackupPathBufSize)) {
WriteStatusFile(USAGE_ERROR);
LOG(("NS_main: callback backup path truncated"));
output_finish();
// Don't attempt to launch the callback when the callback path is
// longer than expected.
EXIT_IF_SECOND_UPDATER_INSTANCE(updateLockFileHandle, 1);
return 1;
}
// Make a copy of the callback executable so it can be read when
// patching.
if (!CopyFileW(argv[kCallbackIndex], gCallbackBackupPath, false)) {
DWORD copyFileError = GetLastError();
if (copyFileError == ERROR_ACCESS_DENIED) {
WriteStatusFile(WRITE_ERROR_ACCESS_DENIED);
} else {
WriteStatusFile(WRITE_ERROR_CALLBACK_APP);
}
LOG(("NS_main: failed to copy callback file " LOG_S
" into place at " LOG_S,
argv[kCallbackIndex], gCallbackBackupPath));
output_finish();
EXIT_IF_SECOND_UPDATER_INSTANCE(updateLockFileHandle, 1);
LaunchCallbackApp(argv[kCallbackWorkingDirIndex],
argc - kCallbackIndex, argv + kCallbackIndex,
sUsingService);
return 1;
}
// Since the process may be signaled as exited by WaitForSingleObject
// before the release of the executable image try to lock the main
// executable file multiple times before giving up. If we end up giving
// up, we won't fail the update.
const int max_retries = 10;
int retries = 1;
DWORD lastWriteError = 0;
do {
// By opening a file handle wihout FILE_SHARE_READ to the callback
// executable, the OS will prevent launching the process while it is
// being updated.
callbackFile = CreateFileW(targetPath, DELETE | GENERIC_WRITE,
// allow delete, rename, and write
FILE_SHARE_DELETE | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (callbackFile != INVALID_HANDLE_VALUE) {
break;
}
lastWriteError = GetLastError();
LOG(
("NS_main: callback app file open attempt %d failed. "
"File: " LOG_S ". Last error: %lu",
retries, targetPath, lastWriteError));
Sleep(100);
} while (++retries <= max_retries);
// CreateFileW will fail if the callback executable is already in use.
if (callbackFile == INVALID_HANDLE_VALUE) {
bool proceedWithoutExclusive = true;
// Fail the update if the last error was not a sharing violation.
if (lastWriteError != ERROR_SHARING_VIOLATION) {
LOG((
"NS_main: callback app file in use, failed to exclusively open "
"executable file: " LOG_S,
argv[kCallbackIndex]));
if (lastWriteError == ERROR_ACCESS_DENIED) {
WriteStatusFile(WRITE_ERROR_ACCESS_DENIED);
} else {
WriteStatusFile(WRITE_ERROR_CALLBACK_APP);
}
proceedWithoutExclusive = false;
}
// Fail even on sharing violation from a background task, since a
// background task has a higher risk of interfering with a running
// app. Note that this does not apply when staging (when an exclusive
// lock isn't necessary), as there is no callback.
if (lastWriteError == ERROR_SHARING_VIOLATION && sUpdateSilently) {
LOG((
"NS_main: callback app file in use, failed to exclusively open "
"executable file from background task: " LOG_S,
argv[kCallbackIndex]));
WriteStatusFile(BACKGROUND_TASK_SHARING_VIOLATION);
proceedWithoutExclusive = false;
}
if (!proceedWithoutExclusive) {
if (NS_tremove(gCallbackBackupPath) && errno != ENOENT) {
LOG(
("NS_main: unable to remove backup of callback app file, "
"path: " LOG_S,
gCallbackBackupPath));
}
output_finish();
EXIT_IF_SECOND_UPDATER_INSTANCE(updateLockFileHandle, 1);
LaunchCallbackApp(argv[kCallbackWorkingDirIndex],
argc - kCallbackIndex, argv + kCallbackIndex,
sUsingService);
return 1;
}
LOG(
("NS_main: callback app file in use, continuing without "
"exclusive access for executable file: " LOG_S,
argv[kCallbackIndex]));
}
}
}
// DELETE_DIR is not required when performing a staged update or replace
// request; it can be used during a replace request but then it doesn't
// use gDeleteDirPath.
if (!sStagedUpdate && !sReplaceRequest) {
// The directory to move files that are in use to on Windows. This
// directory will be deleted after the update is finished, on OS reboot
// using MoveFileEx if it contains files that are in use, or by the post
// update process after the update finishes. On Windows when performing a
// normal update (e.g. the update is not a staged update and is not a
// replace request) gWorkingDirPath is the same as gInstallDirPath and
// gWorkingDirPath is used because it is the destination directory.
NS_tsnprintf(gDeleteDirPath,
sizeof(gDeleteDirPath) / sizeof(gDeleteDirPath[0]),
NS_T("%s/%s"), gWorkingDirPath, DELETE_DIR);
if (NS_taccess(gDeleteDirPath, F_OK)) {
NS_tmkdir(gDeleteDirPath, 0755);
}
}
#endif /* XP_WIN */
// Run update process on a background thread. ShowProgressUI may return
// before QuitProgressUI has been called, so wait for UpdateThreadFunc to
// terminate. Avoid showing the progress UI when staging an update, or if
// this is an elevated process on macOS.
Thread t;
if (t.Run(UpdateThreadFunc, nullptr) == 0) {
if (!sStagedUpdate && !sReplaceRequest && !sUpdateSilently
#ifdef XP_MACOSX
&& !isElevated
#endif
) {
ShowProgressUI();
}
}
t.Join();
#ifdef XP_WIN
if (argc > kCallbackIndex && !sReplaceRequest) {
if (callbackFile != INVALID_HANDLE_VALUE) {
CloseHandle(callbackFile);
}
// Remove the copy of the callback executable.
if (NS_tremove(gCallbackBackupPath) && errno != ENOENT) {
LOG(
("NS_main: non-fatal error removing backup of callback app file, "
"path: " LOG_S,
gCallbackBackupPath));
}
}
if (!sStagedUpdate && !sReplaceRequest && _wrmdir(gDeleteDirPath)) {
LOG(("NS_main: unable to remove directory: " LOG_S ", err: %d",
DELETE_DIR, errno));
// The directory probably couldn't be removed due to it containing files
// that are in use and will be removed on OS reboot. The call to remove
// the directory on OS reboot is done after the calls to remove the files
// so the files are removed first on OS reboot since the directory must be
// empty for the directory removal to be successful. The MoveFileEx call
// to remove the directory on OS reboot will fail if the process doesn't
// have write access to the HKEY_LOCAL_MACHINE registry key but this is ok
// since the installer / uninstaller will delete the directory along with
// its contents after an update is applied, on reinstall, and on
// uninstall.
if (MoveFileEx(gDeleteDirPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) {
LOG(("NS_main: directory will be removed on OS reboot: " LOG_S,
DELETE_DIR));
} else {
LOG(
("NS_main: failed to schedule OS reboot removal of "
"directory: " LOG_S,
DELETE_DIR));
}
}
#endif /* XP_WIN */
} // if (!isDMGInstall)
#ifdef XP_MACOSX
if (isElevated) {
SetGroupOwnershipAndPermissions(gInstallDirPath);
freeArguments(argc, argv);
CleanupElevatedMacUpdate(false);
} else if (IsOwnedByGroupAdmin(gInstallDirPath)) {
// If the group ownership of the Firefox .app bundle was set to the "admin"
// group during a previous elevated update, we need to ensure that all files
// in the bundle have group ownership of "admin" as well as write permission
// for the group to not break updates in the future.
SetGroupOwnershipAndPermissions(gInstallDirPath);
}
#endif /* XP_MACOSX */
LOG(("Running LaunchCallbackAndPostProcessApps"));
int retVal = LaunchCallbackAndPostProcessApps(argc, argv
#ifdef XP_WIN
,
updateLockFileHandle
#elif XP_MACOSX
,
std::move(umaskContext)
#endif
);
return retVal ? retVal : (gSucceeded ? 0 : 1);
}