int NS_main()

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);
}