/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
import { AUSTLMY } from "resource://gre/modules/UpdateTelemetry.sys.mjs";

import {
  Bits,
  BitsRequest,
  BitsUnknownError,
  BitsVerificationError,
} from "resource://gre/modules/Bits.sys.mjs";
import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
  DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
  TorConnect: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  TorConnectStage: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  TorConnectTopics: "moz-src:///toolkit/modules/TorConnect.sys.mjs",
  UpdateLog: "resource://gre/modules/UpdateLog.sys.mjs",
  UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
  WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",
});

XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "AUS",
  "@mozilla.org/updates/update-service;1",
  Ci.nsIApplicationUpdateService
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UM",
  "@mozilla.org/updates/update-manager;1",
  Ci.nsIUpdateManager
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "CheckSvc",
  "@mozilla.org/updates/update-checker;1",
  Ci.nsIUpdateChecker
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UpdateServiceStub",
  "@mozilla.org/updates/update-service-stub;1",
  Ci.nsIApplicationUpdateServiceStub
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UpdateMutex",
  "@mozilla.org/updates/update-mutex;1",
  Ci.nsIUpdateMutex
);

const UPDATESERVICE_CID = Components.ID(
  "{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"
);

const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath";
const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
const PREF_APP_UPDATE_BACKGROUND_ALLOWDOWNLOADSWITHOUTBITS =
  "app.update.background.allowDownloadsWithoutBITS";
const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled";
const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED =
  "app.update.checkOnlyInstance.enabled";
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL =
  "app.update.checkOnlyInstance.interval";
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT =
  "app.update.checkOnlyInstance.timeout";
const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version";
const PREF_APP_UPDATE_ELEVATE_ATTEMPTS = "app.update.elevate.attempts";
const PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS = "app.update.elevate.maxAttempts";
const PREF_APP_UPDATE_LOCKEDOUT_COUNT = "app.update.lockedOut.count";
const PREF_APP_UPDATE_LOCKEDOUT_DEBOUNCETIME =
  "app.update.lockedOut.debounceTimeMs";
const PREF_APP_UPDATE_LOCKEDOUT_MAXCOUNT = "app.update.lockedOut.maxCount";
const PREF_APP_UPDATE_LOCKEDOUT_MAXAGE = "app.update.lockedOut.maxAgeMs";
const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled";
const PREF_APP_UPDATE_LANGPACK_TIMEOUT = "app.update.langpack.timeout";
const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
const PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED =
  "app.update.noWindowAutoRestart.enabled";
const PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS =
  "app.update.noWindowAutoRestart.delayMs";
const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors";
const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors";
const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout";
const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
const PREF_NETWORK_PROXY_TYPE = "network.proxy.type";

const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update";
const URI_UPDATES_PROPERTIES =
  "chrome://mozapps/locale/update/updates.properties";

const KEY_EXECUTABLE = "XREExeF";
const KEY_UPDROOT = "UpdRootD";
const KEY_OLD_UPDROOT = "OldUpdRootD";

const DIR_UPDATES = "updates";
const DIR_UPDATE_READY = "0";
const DIR_UPDATE_DOWNLOADING = "downloading";

const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
const FILE_BACKUP_UPDATE_ELEVATED_LOG = "backup-update-elevated.log";
const FILE_LAST_UPDATE_LOG = "last-update.log";
const FILE_LAST_UPDATE_ELEVATED_LOG = "last-update-elevated.log";
const FILE_UPDATES_XML = "updates.xml";
const FILE_UPDATE_LOG = "update.log";
const FILE_UPDATE_ELEVATED_LOG = "update-elevated.log";
const FILE_UPDATE_MAR = "update.mar";
const FILE_UPDATE_STATUS = "update.status";
const FILE_UPDATE_TEST = "update.test";
const FILE_UPDATE_VERSION = "update.version";

const STATE_NONE = "null";
const STATE_DOWNLOADING = "downloading";
const STATE_PENDING = "pending";
const STATE_PENDING_SERVICE = "pending-service";
const STATE_PENDING_ELEVATE = "pending-elevate";
const STATE_APPLYING = "applying";
const STATE_APPLIED = "applied";
const STATE_APPLIED_SERVICE = "applied-service";
const STATE_SUCCEEDED = "succeeded";
const STATE_DOWNLOAD_FAILED = "download-failed";
const STATE_FAILED = "failed";

// BITS will keep retrying a download after transient errors, unless this much
// time has passed since there has been download progress.
// Similarly to ...POLL_RATE_MS below, we are much more aggressive when the user
// is watching the download progress.
const BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS = 3600; // 1 hour
const BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS = 5;

// These value control how frequently we get updates from the BITS client on
// the progress made downloading. The difference between the two is that the
// active interval is the one used when the user is watching. The idle interval
// is the one used when no one is watching.
const BITS_IDLE_POLL_RATE_MS = 1000;
const BITS_ACTIVE_POLL_RATE_MS = 200;

// The number of update attempts when a write error occurs during an attempt
const MAX_TOTAL_INSTALL_ATTEMPTS = 2;

// The values below used by this code are from common/updatererrors.h
const WRITE_ERROR = 7;
const ELEVATION_CANCELED = 9;
const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24;
const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25;
const SERVICE_UPDATER_SIGN_ERROR = 26;
const SERVICE_UPDATER_COMPARE_ERROR = 27;
const SERVICE_UPDATER_IDENTITY_ERROR = 28;
const SERVICE_STILL_APPLYING_ON_SUCCESS = 29;
const SERVICE_STILL_APPLYING_ON_FAILURE = 30;
const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31;
const SERVICE_COULD_NOT_LOCK_UPDATER = 32;
const SERVICE_INSTALLDIR_ERROR = 33;
const WRITE_ERROR_ACCESS_DENIED = 35;
const WRITE_ERROR_CALLBACK_APP = 37;
const UNEXPECTED_STAGING_ERROR = 43;
const DELETE_ERROR_STAGING_LOCK_FILE = 44;
const SERVICE_COULD_NOT_COPY_UPDATER = 49;
const SERVICE_STILL_APPLYING_TERMINATED = 50;
const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51;
const SERVICE_COULD_NOT_IMPERSONATE = 58;
const WRITE_ERROR_FILE_COPY = 61;
const WRITE_ERROR_DELETE_FILE = 62;
const WRITE_ERROR_OPEN_PATCH_FILE = 63;
const WRITE_ERROR_PATCH_FILE = 64;
const WRITE_ERROR_APPLY_DIR_PATH = 65;
const WRITE_ERROR_CALLBACK_PATH = 66;
const WRITE_ERROR_FILE_ACCESS_DENIED = 67;
const WRITE_ERROR_DIR_ACCESS_DENIED = 68;
const WRITE_ERROR_DELETE_BACKUP = 69;
const WRITE_ERROR_EXTRACT = 70;

// Error codes 80 through 99 are reserved for UpdateService.sys.mjs and are not
// defined in common/updatererrors.h
const ERR_UPDATER_CRASHED = 89;
const ERR_OLDER_VERSION_OR_SAME_BUILD = 90;
const ERR_UPDATE_STATE_NONE = 91;
const ERR_CHANNEL_CHANGE = 92;
const INVALID_UPDATER_STATE_CODE = 98;
const INVALID_UPDATER_STATUS_CODE = 99;

const SILENT_UPDATE_NEEDED_ELEVATION_ERROR = 105;
const BACKGROUND_TASK_SHARING_VIOLATION = 106;

// Array of write errors to simplify checks for write errors
const WRITE_ERRORS = [
  WRITE_ERROR,
  WRITE_ERROR_ACCESS_DENIED,
  WRITE_ERROR_CALLBACK_APP,
  WRITE_ERROR_FILE_COPY,
  WRITE_ERROR_DELETE_FILE,
  WRITE_ERROR_OPEN_PATCH_FILE,
  WRITE_ERROR_PATCH_FILE,
  WRITE_ERROR_APPLY_DIR_PATH,
  WRITE_ERROR_CALLBACK_PATH,
  WRITE_ERROR_FILE_ACCESS_DENIED,
  WRITE_ERROR_DIR_ACCESS_DENIED,
  WRITE_ERROR_DELETE_BACKUP,
  WRITE_ERROR_EXTRACT,
];

// Array of write errors to simplify checks for service errors
const SERVICE_ERRORS = [
  SERVICE_UPDATER_COULD_NOT_BE_STARTED,
  SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS,
  SERVICE_UPDATER_SIGN_ERROR,
  SERVICE_UPDATER_COMPARE_ERROR,
  SERVICE_UPDATER_IDENTITY_ERROR,
  SERVICE_STILL_APPLYING_ON_SUCCESS,
  SERVICE_STILL_APPLYING_ON_FAILURE,
  SERVICE_UPDATER_NOT_FIXED_DRIVE,
  SERVICE_COULD_NOT_LOCK_UPDATER,
  SERVICE_INSTALLDIR_ERROR,
  SERVICE_COULD_NOT_COPY_UPDATER,
  SERVICE_STILL_APPLYING_TERMINATED,
  SERVICE_STILL_APPLYING_NO_EXIT_CODE,
  SERVICE_COULD_NOT_IMPERSONATE,
];

// Custom update error codes
const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
const NETWORK_ERROR_OFFLINE = 111;
const PROXY_SERVER_CONNECTION_REFUSED = 2152398920;

// Error codes should be < 1000. Errors above 1000 represent http status codes
const HTTP_ERROR_OFFSET = 1000;

// The is an HRESULT error that may be returned from the BITS interface
// indicating that access was denied. Often, this error code is returned when
// attempting to access a job created by a different user.
const HRESULT_E_ACCESSDENIED = -2147024891;

// HRESULT for HTTP 406 defined in bitsmsg.rs as:
// pub const BG_E_HTTP_ERROR_406: DWORD = 0x80190196;
// Represented in JavaScript as signed 32-bit integer
const BG_E_HTTP_ERROR_406 = -2145844842;

const DOWNLOAD_CHUNK_SIZE = 300000; // bytes

// The number of consecutive failures when updating using the service before
// setting the app.update.service.enabled preference to false.
const DEFAULT_SERVICE_MAX_ERRORS = 10;

// The number of consecutive socket errors to allow before falling back to
// downloading a different MAR file or failing if already downloading the full.
const DEFAULT_SOCKET_MAX_ERRORS = 10;

// The number of milliseconds to wait before retrying a connection error.
const DEFAULT_SOCKET_RETRYTIMEOUT = 2000;

// Default maximum number of elevation cancelations per update version before
// giving up.
const DEFAULT_CANCELATIONS_OSX_MAX = 3;

// The interval for the update xml write deferred task.
const XML_SAVER_INTERVAL_MS = 200;

// How long after a patch has downloaded should we wait for language packs to
// update before proceeding anyway.
const LANGPACK_UPDATE_DEFAULT_TIMEOUT = 300000;

// Interval between rechecks for other instances after the initial check finds
// at least one other instance.
const ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes

// Wait this long after detecting that another instance is running (having been
// polling that entire time) before giving up and applying the update anyway.
const ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS = 6 * 60 * 60 * 1000; // 6 hours

// The other instance check timeout can be overridden via a pref, but we limit
// that value to this so that the pref can't effectively disable the feature.
const ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS = 2 * 24 * 60 * 60 * 1000; // 2 days

// Values to use when polling for staging. See `pollForStagingEnd` for more
// details.
const STAGING_POLLING_MIN_INTERVAL_MS = 15 * 1000; // 15 seconds
const STAGING_POLLING_MAX_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
const STAGING_POLLING_ATTEMPTS_PER_INTERVAL = 5;
const STAGING_POLLING_MAX_DURATION_MS = 1 * 60 * 60 * 1000; // 1 hour

// This value will be set to true if it appears that BITS is being used by
// another user to download updates. We don't really want two users using BITS
// at once. Computers with many users (ex: a school computer), should not end
// up with dozens of BITS jobs.
var gBITSInUseByAnotherUser = false;
// The update service can be invoked as part of a standalone headless background
// task.  In this context, when the background task kicks off an update
// download, we don't want it to move on to staging. As soon as the download has
// kicked off, the task begins shutting down and, even if the the download
// completes incredibly quickly, we don't want staging to begin while we are
// shutting down. That isn't a well tested scenario and it's possible that it
// could leave us in a bad state.
let gOnlyDownloadUpdatesThisSession = false;
// This will be the backing for `nsIApplicationUpdateService.currentState`
var gUpdateState = Ci.nsIApplicationUpdateService.STATE_IDLE;

let gLastLockoutDebouncedAt = 0;

/**
 * Simple container and constructor for a Promise and its resolve function.
 */
class SelfContainedPromise {
  constructor() {
    this.promise = new Promise(resolve => {
      this.resolve = resolve;
    });
  }
}

// This will contain a `SelfContainedPromise` that will be used to back
// `nsIApplicationUpdateService.stateTransition`.
var gStateTransitionPromise = new SelfContainedPromise();

ChromeUtils.defineLazyGetter(
  lazy,
  "gUpdateBundle",
  function aus_gUpdateBundle() {
    return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
  }
);

function resetIsBackgroundTaskMode() {
  /**
   * gIsBackgroundTaskMode will be true if Firefox is currently running as a
   * background task. Otherwise it will be false.
   */
  ChromeUtils.defineLazyGetter(
    lazy,
    "gIsBackgroundTaskMode",
    function aus_gCurrentlyRunningAsBackgroundTask() {
      if (!("@mozilla.org/backgroundtasks;1" in Cc)) {
        return false;
      }
      const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
        Ci.nsIBackgroundTasks
      );
      if (!bts) {
        return false;
      }
      return bts.isBackgroundTaskMode;
    }
  );
}
resetIsBackgroundTaskMode();

// Exported for testing only.
export function testResetIsBackgroundTaskMode() {
  resetIsBackgroundTaskMode();
}

/**
 * Changes `nsIApplicationUpdateService.currentState` and causes
 * `nsIApplicationUpdateService.stateTransition` to resolve.
 */
function transitionState(newState) {
  if (newState == gUpdateState) {
    LOG("transitionState - Not transitioning state because it isn't changing.");
    return;
  }
  LOG(
    `transitionState - "${lazy.AUS.getStateName(gUpdateState)}" -> ` +
      `"${lazy.AUS.getStateName(newState)}".`
  );
  gUpdateState = newState;
  // Assign the new Promise before we resolve the old one just to make sure that
  // anything that runs as a result of `resolve` doesn't end up waiting on the
  // Promise that already resolved.
  let oldStateTransitionPromise = gStateTransitionPromise;
  gStateTransitionPromise = new SelfContainedPromise();
  oldStateTransitionPromise.resolve();
}

/**
 * When a plain JS object is passed through xpconnect the other side sees a
 * wrapped version of the object instead of the real object. Since these two
 * objects are different they act as different keys for Map and WeakMap. However
 * xpconnect gives us a way to get the underlying JS object from the wrapper so
 * this function returns the JS object regardless of whether passed the JS
 * object or its wrapper for use in places where it is unclear which one you
 * have.
 */
function unwrap(obj) {
  return obj.wrappedJSObject ?? obj;
}

/**
 * When an update starts to download (and if the feature is enabled) the add-ons
 * manager starts downloading updated language packs for the new application
 * version. A promise is used to track whether those updates are complete so the
 * front-end is only notified that an application update is ready once the
 * language pack updates have been staged.
 *
 * In order to be able to access that promise from various places in the update
 * service they are cached in this map using the nsIUpdate object as a weak
 * owner. Note that the key should always be the result of calling the above
 * unwrap function on the nsIUpdate to ensure a consistent object is used as the
 * key.
 *
 * When the language packs finish staging the nsIUpdate entriy is removed from
 * this map so if the entry is still there then language pack updates are in
 * progress.
 */
const LangPackUpdates = new WeakMap();

/**
 * When we're polling to see if other running instances of the application have
 * exited, there's no need to ever start polling again in parallel. To prevent
 * doing that, we keep track of the promise that resolves when polling completes
 * and return that if a second simultaneous poll is requested, so that the
 * multiple callers end up waiting for the same promise to resolve.
 */
let gOtherInstancePollPromise;

/**
 * Query the update sync manager to see if another instance of this same
 * installation of this application is currently running, under the context of
 * any operating system user (not just the current one).
 * This function immediately returns the current, instantaneous status of any
 * other instances.
 *
 * @return true if at least one other instance is running, false if not
 */
function isOtherInstanceRunning() {
  const checkEnabled = Services.prefs.getBoolPref(
    PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED,
    true
  );
  if (!checkEnabled) {
    LOG("isOtherInstanceRunning - disabled by pref, skipping check");
    return false;
  }

  try {
    let syncManager = Cc[
      "@mozilla.org/updates/update-sync-manager;1"
    ].getService(Ci.nsIUpdateSyncManager);
    return syncManager.isOtherInstanceRunning();
  } catch (ex) {
    LOG(`isOtherInstanceRunning - sync manager failed with exception: ${ex}`);
    return false;
  }
}

/**
 * Query the update sync manager to see if another instance of this same
 * installation of this application is currently running, under the context of
 * any operating system user (not just the one running this instance).
 * This function polls for the status of other instances continually
 * (asynchronously) until either none exist or a timeout expires.
 *
 * @return a Promise that resolves with false if at any point during polling no
 *         other instances can be found, or resolves with true if the timeout
 *         expires when other instances are still running
 */
function waitForOtherInstances() {
  // If we're already in the middle of a poll, reuse it rather than start again.
  if (gOtherInstancePollPromise) {
    return gOtherInstancePollPromise;
  }

  let timeout = Services.prefs.getIntPref(
    PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT,
    ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS
  );

  // return immediately if timeout value is invalid.
  if (timeout <= 0) {
    return Promise.resolve(isOtherInstanceRunning());
  }

  // Don't allow the pref to set a super high timeout and break this feature.
  if (timeout > ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS) {
    timeout = ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS;
  }

  let interval = Services.prefs.getIntPref(
    PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL,
    ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS
  );

  if (interval <= 0) {
    interval = ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS;
  }

  // Don't allow an interval longer than the timeout.
  interval = Math.min(interval, timeout);

  let iterations = 0;
  const maxIterations = Math.ceil(timeout / interval);

  gOtherInstancePollPromise = new Promise(function (resolve) {
    let poll = function () {
      iterations++;
      if (!isOtherInstanceRunning()) {
        LOG("waitForOtherInstances - no other instances found, exiting");
        resolve(false);
        gOtherInstancePollPromise = undefined;
      } else if (iterations >= maxIterations) {
        LOG(
          "waitForOtherInstances - timeout expired while other instances " +
            "are still running"
        );
        resolve(true);
        gOtherInstancePollPromise = undefined;
      } else if (iterations + 1 == maxIterations && timeout % interval != 0) {
        // In case timeout isn't a multiple of interval, set the next timeout
        // for the remainder of the time rather than for the usual interval.
        lazy.setTimeout(poll, timeout % interval);
      } else {
        lazy.setTimeout(poll, interval);
      }
    };

    LOG("waitForOtherInstances - beginning polling");
    poll();
  });

  return gOtherInstancePollPromise;
}

/**
 * Tests to make sure that we can write to a given directory.
 *
 * @param updateTestFile a test file in the directory that needs to be tested.
 * @param createDirectory whether a test directory should be created.
 * @throws if we don't have right access to the directory.
 */
function testWriteAccess(updateTestFile, createDirectory) {
  const NORMAL_FILE_TYPE = Ci.nsIFile.NORMAL_FILE_TYPE;
  const DIRECTORY_TYPE = Ci.nsIFile.DIRECTORY_TYPE;
  if (updateTestFile.exists()) {
    updateTestFile.remove(false);
  }
  updateTestFile.create(
    createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE,
    createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE
  );
  updateTestFile.remove(false);
}

/**
 * Tests whether or not the current instance has the update mutex. Tries to
 * acquire it if it is not held currently.
 *
 * @return true if this instance now holds the update mutex or was already
 *         holding
 */
function hasUpdateMutex() {
  return lazy.UpdateMutex.tryLock();
}

/**
 * Determines whether or not all descendants of a directory are writeable.
 * Note: Does not check the root directory itself for writeability.
 *
 * @return true if all descendants are writeable, false otherwise
 */
function areDirectoryEntriesWriteable(aDir) {
  let items = aDir.directoryEntries;
  while (items.hasMoreElements()) {
    let item = items.nextFile;
    if (!item.isWritable()) {
      LOG("areDirectoryEntriesWriteable - unable to write to " + item.path);
      return false;
    }
    if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) {
      return false;
    }
  }
  return true;
}

/**
 * OSX only function to determine if the user requires elevation to be able to
 * write to the application bundle.
 *
 * @return true if elevation is required, false otherwise
 */
function getElevationRequired() {
  if (AppConstants.BASE_BROWSER_UPDATE) {
    // To avoid potential security holes associated with running the updater
    // process with elevated privileges, Tor Browser does not support elevation.
    return false;
  }
  if (AppConstants.platform != "macosx") {
    return false;
  }

  try {
    // Recursively check that the application bundle (and its descendants) can
    // be written to.
    LOG(
      "getElevationRequired - recursively testing write access on " +
        getInstallDirRoot().path
    );
    if (
      !getInstallDirRoot().isWritable() ||
      !areDirectoryEntriesWriteable(getInstallDirRoot())
    ) {
      LOG(
        "getElevationRequired - unable to write to application bundle, " +
          "elevation required"
      );
      return true;
    }
  } catch (ex) {
    LOG(
      "getElevationRequired - unable to write to application bundle, " +
        "elevation required. Exception: " +
        ex
    );
    return true;
  }
  LOG(
    "getElevationRequired - able to write to application bundle, elevation " +
      "not required"
  );
  return false;
}

/**
 * A promise that resolves when language packs are downloading or if no language
 * packs are being downloaded.
 */
function promiseLangPacksUpdated(update) {
  let promise = LangPackUpdates.get(unwrap(update));
  if (promise) {
    LOG(
      "promiseLangPacksUpdated - waiting for language pack updates to stage."
    );
    return promise;
  }

  // In case callers rely on a promise just return an already resolved promise.
  return Promise.resolve();
}

/*
 * See nsIUpdateService.idl
 */
function isAppBaseDirWritable() {
  let appDirTestFile = "";

  try {
    appDirTestFile = getAppBaseDir();
    appDirTestFile.append(FILE_UPDATE_TEST);
  } catch (e) {
    LOG(
      "isAppBaseDirWritable - Base directory or test path could not be " +
        `determined: ${e}`
    );
    return false;
  }

  try {
    LOG(
      `isAppBaseDirWritable - testing write access for ${appDirTestFile.path}`
    );

    if (appDirTestFile.exists()) {
      appDirTestFile.remove(false);
    }
    // if we're unable to create the test file this will throw an exception:
    appDirTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
    appDirTestFile.remove(false);
    LOG(`isAppBaseDirWritable - Path is writable: ${appDirTestFile.path}`);
    return true;
  } catch (e) {
    LOG(
      `isAppBaseDirWritable - Path '${appDirTestFile.path}' ` +
        `is not writable: ${e}`
    );
  }
  // No write access to the installation directory
  return false;
}

/**
 * Determines whether or not an update can be applied. This is always true on
 * Windows when the service is used. On Mac OS X and Linux, if the user has
 * write access to the update directory this will return true because on OSX we
 * offer users the option to perform an elevated update when necessary and on
 * Linux the update directory is located in the application directory.
 *
 * @return true if an update can be applied, false otherwise
 */
function getCanApplyUpdates() {
  try {
    // Check if it is possible to write to the update directory so clients won't
    // repeatedly try to apply an update without the ability to complete the
    // update process which requires write access to the update directory.
    let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
    LOG("getCanApplyUpdates - testing write access " + updateTestFile.path);
    testWriteAccess(updateTestFile, false);
  } catch (e) {
    LOG(
      "getCanApplyUpdates - unable to apply updates without write " +
        "access to the update directory. Exception: " +
        e
    );
    return false;
  }

  if (
    !AppConstants.BASE_BROWSER_UPDATE &&
    (AppConstants.platform == "macosx" || AppConstants.platform == "win")
  ) {
    LOG(
      "getCanApplyUpdates - bypass the write since elevation can be used " +
        "on macOS and Windows"
    );
    return true;
  }

  if (!isAppBaseDirWritable()) {
    LOG(
      "getCanApplyUpdates - unable to apply updates, because the base " +
        "directory is not writable."
    );
    return false;
  }

  LOG("getCanApplyUpdates - able to apply updates");
  return true;
}

/**
 * Whether or not the application can stage an update for the current session.
 * These checks are only performed once per session due to using a lazy getter.
 *
 * @return true if updates can be staged for this session.
 */
ChromeUtils.defineLazyGetter(
  lazy,
  "gCanStageUpdatesSession",
  function aus_gCSUS() {
    if (getElevationRequired()) {
      LOG(
        "gCanStageUpdatesSession - unable to stage updates because elevation " +
          "is required."
      );
      return false;
    }

    try {
      let updateTestFile;
      if (AppConstants.platform == "macosx") {
        updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
      } else {
        updateTestFile = getInstallDirRoot();
        updateTestFile.append(FILE_UPDATE_TEST);
      }
      LOG(
        "gCanStageUpdatesSession - testing write access " + updateTestFile.path
      );
      testWriteAccess(updateTestFile, true);
      if (AppConstants.platform != "macosx") {
        // On all platforms except Mac, we need to test the parent directory as
        // well, as we need to be able to move files in that directory during the
        // replacing step.
        updateTestFile = getInstallDirRoot().parent;
        updateTestFile.append(FILE_UPDATE_TEST);
        LOG(
          "gCanStageUpdatesSession - testing write access " +
            updateTestFile.path
        );
        updateTestFile.createUnique(
          Ci.nsIFile.DIRECTORY_TYPE,
          FileUtils.PERMS_DIRECTORY
        );
        updateTestFile.remove(false);
      }
    } catch (e) {
      LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + e);
      // No write privileges
      return false;
    }

    LOG("gCanStageUpdatesSession - able to stage updates");
    return true;
  }
);

/**
 * Whether or not the application can stage an update.
 *
 * @param {boolean} [transient] Whether transient factors such as the update
 *        mutex should be considered.
 * @return true if updates can be staged.
 */
function getCanStageUpdates(transient = true) {
  // If staging updates are disabled, then just bail out!
  if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) {
    LOG(
      "getCanStageUpdates - staging updates is disabled by preference " +
        PREF_APP_UPDATE_STAGING_ENABLED
    );
    return false;
  }

  if (AppConstants.platform == "win" && shouldUseService()) {
    // No need to perform directory write checks, the maintenance service will
    // be able to write to all directories.
    LOG("getCanStageUpdates - able to stage updates using the service");
    return true;
  }

  if (transient && !hasUpdateMutex()) {
    LOG(
      "getCanStageUpdates - unable to apply updates because another " +
        "instance of the application is already handling updates for this " +
        "installation."
    );
    return false;
  }

  return lazy.gCanStageUpdatesSession;
}

/**
 * Whether or not the application can use BITS to download updates.
 *
 * @param {boolean} [transient] Whether transient factors such as the update
 *        mutex should be considered.
 * @return A string with one of these values:
 *           CanUseBits
 *           NoBits_NotWindows
 *           NoBits_FeatureOff
 *           NoBits_Pref
 *           NoBits_Proxy
 *           NoBits_OtherUser
 *         These strings are directly compatible with the categories for the
 *         Glean.update.canUseBitsExternal and Glean.update.canUseBitsNotify glean
 *         metrics. If this function is made to return other values, they should
 *         also be added to the labels lists for those probes in Histograms.json
 *         and metrics.yaml.
 */
function getCanUseBits(transient = true) {
  if (AppConstants.platform != "win") {
    LOG("getCanUseBits - Not using BITS because this is not Windows");
    return "NoBits_NotWindows";
  }
  if (!AppConstants.MOZ_BITS_DOWNLOAD) {
    LOG("getCanUseBits - Not using BITS because the feature is disabled");
    return "NoBits_FeatureOff";
  }

  if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, true)) {
    LOG("getCanUseBits - Not using BITS. Disabled by pref.");
    return "NoBits_Pref";
  }
  // Firefox support for passing proxies to BITS is still rudimentary.
  // For now, disable BITS support on configurations that are not using the
  // standard system proxy.
  let defaultProxy = Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM;
  if (
    Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) !=
      defaultProxy &&
    !Cu.isInAutomation
  ) {
    LOG("getCanUseBits - Not using BITS because of proxy usage");
    return "NoBits_Proxy";
  }
  if (transient && gBITSInUseByAnotherUser) {
    LOG("getCanUseBits - Not using BITS. Already in use by another user");
    return "NoBits_OtherUser";
  }
  LOG("getCanUseBits - BITS can be used to download updates");
  return "CanUseBits";
}

/**
 * Logs a string to the error console. If enabled, also logs to the update
 * messages file.
 *
 * @param   string
 *          The string to write to the error console.
 */
function LOG(string) {
  lazy.UpdateLog.logPrefixedString("AUS:SVC", string);
}

/**
 * Gets the specified directory at the specified hierarchy under the
 * update root directory and creates it if it doesn't exist.
 *
 * @param   pathArray
 *          An array of path components to locate beneath the directory
 *          specified by |key|
 * @return  nsIFile object for the location specified.
 */
function getUpdateDirCreate(pathArray) {
  if (Cu.isInAutomation) {
    // This allows tests to use an alternate updates directory so they can test
    // startup behavior.
    const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
    const PREF_TEST_ROOT = "mochitest.testRoot";
    let alternatePath = Services.prefs.getCharPref(
      PREF_APP_UPDATE_ALTUPDATEDIRPATH,
      null
    );
    if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
      let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT);
      let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length);
      if (AppConstants.platform == "win") {
        relativePath = relativePath.replace(/\//g, "\\");
      }
      alternatePath = testRoot + relativePath;
      let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
        Ci.nsIFile
      );
      updateDir.initWithPath(alternatePath);
      for (let i = 0; i < pathArray.length; ++i) {
        updateDir.append(pathArray[i]);
      }
      return updateDir;
    }
  }

  let dir = FileUtils.getDir(KEY_UPDROOT, pathArray);
  try {
    dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
  } catch (ex) {
    if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
      throw ex;
    }
    // Ignore the exception due to a directory that already exists.
  }
  return dir;
}

/**
 * Gets the application base directory.
 *
 * @return  nsIFile object for the application base directory.
 */
function getAppBaseDir() {
  return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
}

/**
 * Gets the root of the installation directory which is the application
 * bundle directory on Mac OS X and the location of the application binary
 * on all other platforms.
 *
 * @return nsIFile object for the directory
 */
function getInstallDirRoot() {
  let dir = getAppBaseDir();
  if (AppConstants.platform == "macosx") {
    // On macOS, the executable is stored under Contents/MacOS.
    dir = dir.parent.parent;
  }
  return dir;
}

/**
 * Gets the file at the specified hierarchy under the update root directory.
 *
 * @param   pathArray
 *          An array of path components to locate beneath the directory
 *          specified by |key|. The last item in this array must be the
 *          leaf name of a file.
 * @return  nsIFile object for the file specified. The file is NOT created
 *          if it does not exist, however all required directories along
 *          the way are.
 */
function getUpdateFile(pathArray) {
  let file = getUpdateDirCreate(pathArray.slice(0, -1));
  file.append(pathArray[pathArray.length - 1]);
  return file;
}

/**
 * This function is designed to let us slightly clean up the mapping between
 * strings and error codes. So that instead of having:
 *   check_error-2147500036=Connection aborted
 *   check_error-2152398850=Connection aborted
 * We can have:
 *   check_error-connection_aborted=Connection aborted
 * And map both of those error codes to it.
 */
function maybeMapErrorCode(code) {
  switch (code) {
    case Cr.NS_BINDING_ABORTED:
    case Cr.NS_ERROR_ABORT:
      return "connection_aborted";
  }
  return code;
}

/**
 * Returns human readable status text from the updates.properties bundle
 * based on an error code
 *
 * @param   code
 *          The error code to look up human readable status text for
 * @param   defaultCode
 *          The default code to look up should human readable status text
 *          not exist for |code|
 * @return  A human readable status text string
 */
function getStatusTextFromCode(code, defaultCode) {
  code = maybeMapErrorCode(code);

  let reason;
  try {
    reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + code);
    LOG(
      "getStatusTextFromCode - transfer error: " + reason + ", code: " + code
    );
  } catch (e) {
    defaultCode = maybeMapErrorCode(defaultCode);

    // Use the default reason
    reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
    LOG(
      "getStatusTextFromCode - transfer error: " +
        reason +
        ", default code: " +
        defaultCode
    );
  }
  return reason;
}

/**
 * Get the Ready Update directory. This is the directory that an update
 * should reside in after download has completed but before it has been
 * installed and cleaned up.
 *
 * @return The ready updates directory, as a nsIFile object
 */
function getReadyUpdateDir() {
  return getUpdateDirCreate([DIR_UPDATES, DIR_UPDATE_READY]);
}

/**
 * Get the Downloading Update directory. This is the directory that an update
 * should reside in during download. Once download is completed, it will be
 * moved to the Ready Update directory.
 *
 * @return The downloading update directory, as a nsIFile object
 */
function getDownloadingUpdateDir() {
  return getUpdateDirCreate([DIR_UPDATES, DIR_UPDATE_DOWNLOADING]);
}

/**
 * If there is a problem with accessing the status file, it may be a transient
 * issue, such as another process checking for updates holding a lock,
 * or it may be a persistent problem that needs attention.
 * This function tries to determine if we should prompt the user to fix the
 * issue.
 *
 * @param   file
 *          An nsIFile object for the file with the issue
 * @param   ex
 *          The Exception object that was thrown when attempting to access the
 *          file.
 */
function onStateAccessFailure(file, ex) {
  LOG("onStateAccessFailure. Ex: " + ex);
  if (
    ex.result == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
    ex.result == Cr.NS_ERROR_FILE_IS_LOCKED
  ) {
    // Looks like we can't access the file. If it hasn't changed in
    // a long time, notify the user that we are persistently unable to update.
    const oneMinMs = 60 * 1000;
    const oneDayMs = 24 * 60 * oneMinMs;
    const now = Date.now();

    // A single update check can attempt to read and write the update state
    // multiple times. We want `lockoutCount` to count the number of times that
    // an update check fails, not the number of times that we fail to access the
    // state. To deal with this, we will implement a debouncing period after
    // each failure.
    let debounceTimeMs = Services.prefs.getIntPref(
      PREF_APP_UPDATE_LOCKEDOUT_DEBOUNCETIME,
      5 * oneMinMs
    );
    debounceTimeMs = Math.min(debounceTimeMs, oneDayMs);
    const debounceEnd = gLastLockoutDebouncedAt + debounceTimeMs;
    if (now < debounceEnd) {
      LOG(`onStateAccessFailure: debounced! (${debounceEnd - now}ms left)`);
      return;
    }
    gLastLockoutDebouncedAt = now;

    // Not really the age, just the interval since last modified.
    const fileAgeMs = now - file.lastModifiedTime;

    let lockoutCount = Services.prefs.getIntPref(
      PREF_APP_UPDATE_LOCKEDOUT_COUNT,
      0
    );
    lockoutCount += 1;
    Services.prefs.setIntPref(PREF_APP_UPDATE_LOCKEDOUT_COUNT, lockoutCount);

    let maxLockoutCount = Services.prefs.getIntPref(
      PREF_APP_UPDATE_LOCKEDOUT_MAXCOUNT,
      4
    );
    maxLockoutCount = Math.min(maxLockoutCount, 20);
    LOG(
      `onStateAccessFailure: lockoutCount is ${lockoutCount}, maxLockoutCount is ${maxLockoutCount}`
    );

    let maxFileAgeMs = Services.prefs.getIntPref(
      PREF_APP_UPDATE_LOCKEDOUT_MAXAGE,
      oneDayMs * 2
    );
    maxFileAgeMs = Math.min(maxFileAgeMs, oneDayMs * 14);
    LOG(
      `onStateAccessFailure: fileAgeMs = ${fileAgeMs} maxFileAgeMs = ${maxFileAgeMs}`
    );

    if (lockoutCount >= maxLockoutCount && fileAgeMs >= maxFileAgeMs) {
      Glean.update.stateWriteFailure.add();
      // Create an empty Update object for messaging.
      let update = new Update(null);
      LOG("onStateAccessFailure: reporting permission issue");
      Services.obs.notifyObservers(update, "update-error", "bad-perms");
      Services.prefs.setIntPref(PREF_APP_UPDATE_LOCKEDOUT_COUNT, 0);
    }
  }
}

function onStateAccessSuccess() {
  LOG("onStateAccessSuccess");
  Services.prefs.setIntPref(PREF_APP_UPDATE_LOCKEDOUT_COUNT, 0);
}

/**
 * Reads the update state from the update.status file in the specified
 * directory.
 *
 * @param   dir
 *          The dir to look for an update.status file in
 * @return  The status value of the update.
 */
function readStatusFile(dir) {
  let statusFile = dir.clone();
  statusFile.append(FILE_UPDATE_STATUS);
  let status = readStringFromFile(statusFile) || STATE_NONE;
  LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
  return status;
}

/**
 * Writes the current update operation/state to a file in the patch
 * directory, indicating to the patching system that operations need
 * to be performed.
 *
 * This function does not throw on errors. It just returns the error.
 *
 * @param   dir
 *          The patch directory where the update.status file should be
 *          written.
 * @param   state
 *          The state value to write.
 * @returns `null` on success or a JS exception on failure.
 */
function writeStatusFile(dir, state) {
  let statusFile = dir.clone();
  statusFile.append(FILE_UPDATE_STATUS);
  try {
    writeStringToFile(statusFile, state);
    LOG("writeStatusFile - status: " + state + ", path: " + statusFile.path);
    try {
      onStateAccessSuccess();
    } catch {}
  } catch (ex) {
    LOG("writeStatusFile failed: " + ex);
    onStateAccessFailure(statusFile, ex);
    return ex;
  }
  return null;
}

/**
 * Writes the update's application version to a file in the patch directory. If
 * the update doesn't provide application version information via the
 * appVersion attribute the string "null" will be written to the file.
 * This value is compared during startup (in nsUpdateDriver.cpp) to determine if
 * the update should be applied. Note that this won't provide protection from
 * downgrade of the application for the nightly user case where the application
 * version doesn't change.
 *
 * This function fails silently.
 *
 * @param   dir
 *          The patch directory where the update.version file should be
 *          written.
 * @param   version
 *          The version value to write. Will be the string "null" when the
 *          update doesn't provide the appVersion attribute in the update xml.
 */
function writeVersionFile(dir, version) {
  let versionFile = dir.clone();
  versionFile.append(FILE_UPDATE_VERSION);
  try {
    writeStringToFile(versionFile, version);
  } catch (ex) {
    LOG("writeVersionFile failed: " + ex);
  }
}

/**
 * Determines if the service should be used to attempt an update
 * or not.
 *
 * @return  true if the service should be used for updates.
 */
function shouldUseService() {
  // This function will return true if the mantenance service should be used if
  // all of the following conditions are met:
  // 1) This build was done with the maintenance service enabled
  // 2) The maintenance service is installed
  // 3) The pref for using the service is enabled
  if (
    !AppConstants.MOZ_MAINTENANCE_SERVICE ||
    !isServiceInstalled() ||
    !Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false)
  ) {
    LOG("shouldUseService - returning false");
    return false;
  }

  LOG("shouldUseService - returning true");
  return true;
}

/**
 * Determines if the service is is installed.
 *
 * @return  true if the service is installed.
 */
function isServiceInstalled() {
  if (!AppConstants.MOZ_MAINTENANCE_SERVICE || AppConstants.platform != "win") {
    LOG("isServiceInstalled - returning false");
    return false;
  }

  let installed = 0;
  try {
    let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
      Ci.nsIWindowsRegKey
    );
    wrk.open(
      wrk.ROOT_KEY_LOCAL_MACHINE,
      "SOFTWARE\\Mozilla\\MaintenanceService",
      wrk.ACCESS_READ | wrk.WOW64_64
    );
    installed = wrk.readIntValue("Installed");
    wrk.close();
  } catch (e) {}
  installed = installed == 1; // convert to bool
  LOG("isServiceInstalled - returning " + installed);
  return installed;
}

/**
 * Gets the appropriate pending update state. Returns STATE_PENDING_SERVICE,
 * STATE_PENDING_ELEVATE, or STATE_PENDING.
 */
function getBestPendingState() {
  if (shouldUseService()) {
    return STATE_PENDING_SERVICE;
  } else if (getElevationRequired()) {
    return STATE_PENDING_ELEVATE;
  }
  return STATE_PENDING;
}

/**
 * Removes the contents of the ready update directory and rotates the update
 * logs when present. If the update.log exists in the patch directory this will
 * move the last-update.log if it exists to backup-update.log in the parent
 * directory of the patch directory and then move the update.log in the patch
 * directory to last-update.log in the parent directory of the patch directory.
 *
 * @param aRemovePatchFiles (optional, defaults to true)
 *        When true the update's patch directory contents are removed.
 */
function cleanUpReadyUpdateDir(aRemovePatchFiles = true) {
  let updateDir;
  try {
    updateDir = getReadyUpdateDir();
  } catch (e) {
    LOG(
      "cleanUpReadyUpdateDir - unable to get the updates patch directory. " +
        "Exception: " +
        e
    );
    return;
  }

  // Preserve the last update log files for debugging purposes.
  // Make sure to keep the pairs of logs (ex "last-update.log" and
  // "last-update-elevated.log") together. We don't want to skip moving
  // "last-update-elevated.log" just because there isn't an
  // "update-elevated.log" to take its place.
  let updateLogFile = updateDir.clone();
  updateLogFile.append(FILE_UPDATE_LOG);
  let updateElevatedLogFile = updateDir.clone();
  updateElevatedLogFile.append(FILE_UPDATE_ELEVATED_LOG);
  if (updateLogFile.exists() || updateElevatedLogFile.exists()) {
    const overwriteOrRemoveBackupLog = (log, shouldOverwrite, backupName) => {
      if (shouldOverwrite) {
        try {
          log.moveTo(dir, backupName);
        } catch (e) {
          LOG(
            `cleanUpReadyUpdateDir - failed to rename file '${log.path}' to ` +
              `'${backupName}': ${e.result}`
          );
        }
      } else {
        // If we don't have a file to overwrite this one, make sure we remove
        // it anyways to prevent log pairs from getting mismatched.
        let backupLogFile = dir.clone();
        backupLogFile.append(backupName);
        try {
          backupLogFile.remove(false);
        } catch (e) {
          if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
            LOG(
              `cleanUpReadyUpdateDir - failed to remove file ` +
                `'${backupLogFile.path}': ${e.result}`
            );
          }
        }
      }
    };

    let dir = updateDir.parent;
    let logFile = dir.clone();
    logFile.append(FILE_LAST_UPDATE_LOG);
    const logFileExists = logFile.exists();
    let elevatedLogFile = dir.clone();
    elevatedLogFile.append(FILE_LAST_UPDATE_ELEVATED_LOG);
    const elevatedLogFileExists = elevatedLogFile.exists();
    if (logFileExists || elevatedLogFileExists) {
      overwriteOrRemoveBackupLog(
        logFile,
        logFileExists,
        FILE_BACKUP_UPDATE_LOG
      );
      overwriteOrRemoveBackupLog(
        elevatedLogFile,
        elevatedLogFileExists,
        FILE_BACKUP_UPDATE_ELEVATED_LOG
      );
    }

    overwriteOrRemoveBackupLog(updateLogFile, true, FILE_LAST_UPDATE_LOG);
    overwriteOrRemoveBackupLog(
      updateElevatedLogFile,
      true,
      FILE_LAST_UPDATE_ELEVATED_LOG
    );
  }

  if (aRemovePatchFiles) {
    let dirEntries;
    try {
      dirEntries = updateDir.directoryEntries;
    } catch (ex) {
      // If if doesn't exist, our job is already done.
      if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
        return;
      }
      throw ex;
    }
    while (dirEntries.hasMoreElements()) {
      let file = dirEntries.nextFile;
      // Now, recursively remove this file.  The recursive removal is needed for
      // Mac OSX because this directory will contain a copy of updater.app,
      // which is itself a directory and the MozUpdater directory on platforms
      // other than Windows.
      try {
        file.remove(true);
      } catch (e) {
        LOG("cleanUpReadyUpdateDir - failed to remove file " + file.path);
      }
    }
  }
}

/**
 * Removes the contents of the update download directory.
 *
 */
function cleanUpDownloadingUpdateDir() {
  let updateDir;
  try {
    updateDir = getDownloadingUpdateDir();
  } catch (e) {
    LOG(
      "cleanUpDownloadUpdatesDir - unable to get the updates patch " +
        "directory. Exception: " +
        e
    );
    return;
  }

  let dirEntries;
  try {
    dirEntries = updateDir.directoryEntries;
  } catch (ex) {
    // If if doesn't exist, our job is already done.
    if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
      return;
    }
    throw ex;
  }
  while (dirEntries.hasMoreElements()) {
    let file = dirEntries.nextFile;
    // Now, recursively remove this file.
    try {
      file.remove(true);
    } catch (e) {
      LOG("cleanUpDownloadUpdatesDir - failed to remove file " + file.path);
    }
  }
}

/**
 * Clean up the updates list and the directory that contains the update that
 * is ready to be installed.
 *
 * Note - This function causes a state transition to either STATE_DOWNLOADING
 *        or STATE_NONE, depending on whether an update download is in progress.
 */
function cleanupReadyUpdate() {
  // Move the update from the Active Update list into the Past Updates list.
  if (lazy.UM.internal.readyUpdate) {
    LOG("cleanupReadyUpdate - Clearing readyUpdate");
    lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
    lazy.UM.internal.readyUpdate = null;
  }
  lazy.UM.saveUpdates();

  let readyUpdateDir = getReadyUpdateDir();
  let shouldSetDownloadingStatus =
    lazy.UM.internal.downloadingUpdate ||
    readStatusFile(readyUpdateDir) == STATE_DOWNLOADING;

  // Now trash the ready update directory, since we're done with it
  cleanUpReadyUpdateDir();

  // We need to handle two similar cases here.
  // The first is where we clean up the ready updates directory while we are in
  // the downloading state. In this case, we remove the update.status file that
  // says we are downloading, even though we should remain in that state.
  // The second case is when we clean up a ready update, but there is also a
  // downloading update (in which case the update status file's state will
  // reflect the state of the ready update, not the downloading one). In that
  // case, instead of reverting to STATE_NONE (which is what we do by removing
  // the status file), we should set our state to downloading.
  if (shouldSetDownloadingStatus) {
    LOG("cleanupReadyUpdate - Transitioning back to downloading state.");
    transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
    writeStatusFile(readyUpdateDir, STATE_DOWNLOADING);
  }
}

/**
 * Clean up updates list and the directory that the currently downloading update
 * is downloaded to.
 *
 * Note - This function may cause a state transition. If the current state is
 *        STATE_DOWNLOADING, this will cause it to change to STATE_NONE.
 */
async function cleanupDownloadingUpdate() {
  // Move the update from the Active Update list into the Past Updates list.
  if (lazy.UM.internal.downloadingUpdate) {
    LOG("cleanupDownloadingUpdate - Clearing downloadingUpdate.");
    await lazy.AUS.wrappedJSObject.cancelDownloadingUpdate();
    lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
    lazy.UM.internal.downloadingUpdate = null;
  }
  lazy.UM.saveUpdates();

  // Now trash the update download directory, since we're done with it
  cleanUpDownloadingUpdateDir();

  // If the update state says we are downloading, we should change that
  // too, since we aren't doing that anymore.
  if (
    lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_DOWNLOADING
  ) {
    let statusFile = getReadyUpdateDir().clone();
    statusFile.append(FILE_UPDATE_STATUS);
    statusFile.remove(false);
    transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
  }
}

/**
 * Clean up updates list, the ready update directory, and the downloading update
 * directory.
 *
 * This is more efficient than calling
 *   cleanupReadyUpdate();
 *   cleanupDownloadingUpdate();
 * because those need some special handling of the update status file to make
 * sure that, for example, cleaning up a ready update doesn't make us forget
 * that we are downloading an update. When we cleanup both updates, we don't
 * need to worry about things like that.
 *
 * Note - This function causes a state transition to STATE_NONE.
 */
async function cleanupActiveUpdates() {
  // Move the update from the Active Update list into the Past Updates list.
  if (lazy.UM.internal.readyUpdate) {
    LOG("cleanupActiveUpdates - Clearing readyUpdate");
    lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
    lazy.UM.internal.readyUpdate = null;
  }
  if (lazy.UM.internal.downloadingUpdate) {
    LOG("cleanupActiveUpdates - Clearing downloadingUpdate.");
    await lazy.AUS.wrappedJSObject.cancelDownloadingUpdate();
    lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
    lazy.UM.internal.downloadingUpdate = null;
  }
  lazy.UM.saveUpdates();

  // Now trash both active update directories, since we're done with them
  cleanUpReadyUpdateDir();
  cleanUpDownloadingUpdateDir();
}

/**
 * Writes a string of text to a file.  A newline will be appended to the data
 * written to the file.  This function only works with ASCII text.
 *
 * @param file An nsIFile indicating what file to write to.
 * @param text A string containing the text to write to the file.
 * @throws Errors from file stream will be propagated.
 */
function writeStringToFile(file, text) {
  let fos = FileUtils.openSafeFileOutputStream(file);
  text += "\n";
  fos.write(text, text.length);
  // Don't use `FileUtils.closeSafeFileOutputStream` because it swallows errors,
  // which we don't want to do here.
  try {
    fos.QueryInterface(Ci.nsISafeOutputStream);
    fos.finish();
  } finally {
    fos.close();
  }
}

function readStringFromInputStream(inputStream) {
  var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
    Ci.nsIScriptableInputStream
  );
  sis.init(inputStream);
  var text = sis.read(sis.available());
  sis.close();
  if (text && text[text.length - 1] == "\n") {
    text = text.slice(0, -1);
  }
  return text;
}

/**
 * Reads a string of text from a file.  A trailing newline will be removed
 * before the result is returned.  This function only works with ASCII text.
 */
function readStringFromFile(file) {
  if (!file.exists()) {
    LOG("readStringFromFile - file doesn't exist: " + file.path);
    return null;
  }
  var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
    Ci.nsIFileInputStream
  );
  fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
  return readStringFromInputStream(fis);
}

/**
 * Attempts to recover from an update error. If successful, `true` will be
 * returned and AUS.currentState will be transitioned.
 */
function handleUpdateFailure(update) {
  if (WRITE_ERRORS.includes(update.errorCode)) {
    let nextState = getBestPendingState();

    // Check how many install attempts we have with this patch
    let totalInstallAttempts =
      update.selectedPatch
        .QueryInterface(Ci.nsIWritablePropertyBag)
        .getProperty("numTotalInstallAttempts") ?? 0;
    // Out of retries, unable to handle the update failure here
    if (totalInstallAttempts >= MAX_TOTAL_INSTALL_ATTEMPTS) {
      return false;
    }

    LOG(
      "handleUpdateFailure - Failure is a write error. Setting state to " +
        nextState
    );
    writeStatusFile(getReadyUpdateDir(), (update.state = nextState));
    transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
    return true;
  }

  if (update.errorCode == BACKGROUND_TASK_SHARING_VIOLATION) {
    let newState = getBestPendingState();
    LOG(
      "handleUpdateFailure - witnessed BACKGROUND_TASK_SHARING_VIOLATION, setting state to " +
        newState
    );
    writeStatusFile(getReadyUpdateDir(), (update.state = newState));
    transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
    return true;
  }

  if (update.errorCode == SILENT_UPDATE_NEEDED_ELEVATION_ERROR) {
    // There's no need to count attempts and escalate: it's expected that the
    // background update task will try to update and fail due to required
    // elevation repeatedly if, for example, the maintenance service is not
    // available (or not functioning) and the installation requires privileges
    // to update.

    let bestState = getBestPendingState();
    LOG(
      "handleUpdateFailure - witnessed SILENT_UPDATE_NEEDED_ELEVATION_ERROR, " +
        "returning to " +
        bestState
    );
    writeStatusFile(getReadyUpdateDir(), (update.state = bestState));

    transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
    // Return true to indicate a recoverable error.
    return true;
  }

  if (update.errorCode == ELEVATION_CANCELED) {
    let elevationAttempts = Services.prefs.getIntPref(
      PREF_APP_UPDATE_ELEVATE_ATTEMPTS,
      0
    );
    elevationAttempts++;
    Services.prefs.setIntPref(
      PREF_APP_UPDATE_ELEVATE_ATTEMPTS,
      elevationAttempts
    );
    let maxAttempts = Math.min(
      Services.prefs.getIntPref(PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS, 2),
      10
    );

    if (elevationAttempts > maxAttempts) {
      LOG(
        "handleUpdateFailure - notifying observers of error. " +
          "topic: update-error, status: elevation-attempts-exceeded"
      );
      Services.obs.notifyObservers(
        update,
        "update-error",
        "elevation-attempts-exceeded"
      );
    } else {
      LOG(
        "handleUpdateFailure - notifying observers of error. " +
          "topic: update-error, status: elevation-attempt-failed"
      );
      Services.obs.notifyObservers(
        update,
        "update-error",
        "elevation-attempt-failed"
      );
    }

    let cancelations = Services.prefs.getIntPref(
      PREF_APP_UPDATE_CANCELATIONS,
      0
    );
    cancelations++;
    Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations);
    if (AppConstants.platform == "macosx" && AppConstants.BASE_BROWSER_UPDATE) {
      cleanupActiveUpdates();
      update.statusText =
        lazy.gUpdateBundle.GetStringFromName("elevationFailure");
    } else if (AppConstants.platform == "macosx") {
      let osxCancelations = Services.prefs.getIntPref(
        PREF_APP_UPDATE_CANCELATIONS_OSX,
        0
      );
      osxCancelations++;
      Services.prefs.setIntPref(
        PREF_APP_UPDATE_CANCELATIONS_OSX,
        osxCancelations
      );
      let maxCancels = Services.prefs.getIntPref(
        PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
        DEFAULT_CANCELATIONS_OSX_MAX
      );
      // Prevent the preference from setting a value greater than 5.
      maxCancels = Math.min(maxCancels, 5);
      if (osxCancelations >= maxCancels) {
        LOG(
          "handleUpdateFailure - Too many OSX cancellations. Cleaning up " +
            "ready update."
        );
        cleanupReadyUpdate();
        return false;
      }
      LOG(
        `handleUpdateFailure - OSX cancellation. Trying again by setting ` +
          `status to "${STATE_PENDING_ELEVATE}".`
      );
      writeStatusFile(
        getReadyUpdateDir(),
        (update.state = STATE_PENDING_ELEVATE)
      );
      update.statusText =
        lazy.gUpdateBundle.GetStringFromName("elevationFailure");
    } else {
      const nextState = getBestPendingState();
      LOG(
        `handleUpdateFailure - Failure because elevation was cancelled. ` +
          `Setting status to ${nextState}.`
      );
      writeStatusFile(getReadyUpdateDir(), (update.state = nextState));
    }
    transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
    return true;
  }

  if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) {
    Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS);
  }
  if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
    Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
  }

  if (SERVICE_ERRORS.includes(update.errorCode)) {
    var failCount = Services.prefs.getIntPref(
      PREF_APP_UPDATE_SERVICE_ERRORS,
      0
    );
    var maxFail = Services.prefs.getIntPref(
      PREF_APP_UPDATE_SERVICE_MAXERRORS,
      DEFAULT_SERVICE_MAX_ERRORS
    );
    // Prevent the preference from setting a value greater than 10.
    maxFail = Math.min(maxFail, 10);
    // As a safety, when the service reaches maximum failures, it will
    // disable itself and fallback to using the normal update mechanism
    // without the service.
    if (failCount >= maxFail) {
      Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false);
      Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS);
    } else {
      failCount++;
      Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, failCount);
    }

    LOG(
      "handleUpdateFailure - Got a service error. Try to update without the " +
        "service by setting the state to pending."
    );
    writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING));
    transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
    return true;
  }

  if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) {
    Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS);
  }

  return false;
}

/**
 * Return the first UpdatePatch with the given type.
 *
 * @param   update
 *          A nsIUpdate object to search through for a patch of the desired
 *          type.
 * @param   patch_type
 *          The type of the patch ("complete" or "partial")
 * @return  A nsIUpdatePatch object matching the type specified
 */
function getPatchOfType(update, patch_type) {
  for (var i = 0; i < update.patchCount; ++i) {
    var patch = update.getPatchAt(i);
    if (patch && patch.type == patch_type) {
      return patch;
    }
  }
  return null;
}

/**
 * Fall back to downloading a complete update in case an update has failed.
 *
 * This will transition `AUS.currentState` to `STATE_DOWNLOADING` if there is
 * another patch to download, or `STATE_IDLE` if there is not.
 */
async function handleFallbackToCompleteUpdate() {
  // If we failed to install an update, we need to fall back to a complete
  // update. If the install directory has been modified, more partial updates
  // will fail for the same reason. Since we only download partial updates
  // while there is already an update downloaded, we don't have to check the
  // downloading update, we can be confident that we are not downloading the
  // right thing at the moment.

  // The downloading update will be newer than the ready update, so use that
  // update, if it exists.
  let update =
    lazy.UM.internal.downloadingUpdate || lazy.UM.internal.readyUpdate;
  if (!update) {
    LOG(
      "handleFallbackToCompleteUpdate - Unable to find an update to fall " +
        "back to."
    );
    return;
  }

  LOG(
    "handleFallbackToCompleteUpdate - Cleaning up active updates in " +
      "preparation of falling back to complete update."
  );
  await cleanupActiveUpdates();

  if (!update.selectedPatch) {
    // If we don't have a partial patch selected but a partial is available,
    // _selectPatch() will download that instead of the complete patch.
    let patch = getPatchOfType(update, "partial");
    if (patch) {
      patch.selected = true;
    }
  }

  update.statusText = lazy.gUpdateBundle.GetStringFromName("patchApplyFailure");
  var oldType = update.selectedPatch ? update.selectedPatch.type : "complete";
  if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) {
    // Partial patch application failed, try downloading the complete
    // update in the background instead.
    LOG(
      "handleFallbackToCompleteUpdate - install of partial patch " +
        "failed, downloading complete patch"
    );
    var result = await lazy.AUS.internal.downloadUpdate(update);
    if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
      LOG(
        "handleFallbackToCompleteUpdate - Starting complete patch download " +
          "failed. Cleaning up downloading patch."
      );
      await cleanupDownloadingUpdate();
    }
  } else {
    LOG(
      "handleFallbackToCompleteUpdate - install of complete or " +
        "only one patch offered failed. Notifying observers. topic: " +
        "update-error, status: unknown, " +
        "update.patchCount: " +
        update.patchCount +
        ", " +
        "oldType: " +
        oldType
    );
    transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
    Services.obs.notifyObservers(update, "update-error", "unknown");
  }
}

function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
  let patchType = AUSTLMY.PATCH_UNKNOWN;
  if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) {
    if (aUpdate.selectedPatch.type == "complete") {
      patchType = AUSTLMY.PATCH_COMPLETE;
    } else if (aUpdate.selectedPatch.type == "partial") {
      patchType = AUSTLMY.PATCH_PARTIAL;
    }
  }

  let suffix = patchType + (aStartup ? AUSTLMY.STARTUP : AUSTLMY.STAGE);
  let stateCode = 0;
  let parts = aStatus.split(":");
  if (parts.length) {
    switch (parts[0]) {
      case STATE_NONE:
        stateCode = 2;
        break;
      case STATE_DOWNLOADING:
        stateCode = 3;
        break;
      case STATE_PENDING:
        stateCode = 4;
        break;
      case STATE_PENDING_SERVICE:
        stateCode = 5;
        break;
      case STATE_APPLYING:
        stateCode = 6;
        break;
      case STATE_APPLIED:
        stateCode = 7;
        break;
      case STATE_APPLIED_SERVICE:
        stateCode = 9;
        break;
      case STATE_SUCCEEDED:
        stateCode = 10;
        break;
      case STATE_DOWNLOAD_FAILED:
        stateCode = 11;
        break;
      case STATE_FAILED:
        stateCode = 12;
        break;
      case STATE_PENDING_ELEVATE:
        stateCode = 13;
        break;
      // Note: Do not use stateCode 14 here. It is defined in
      // UpdateTelemetry.sys.mjs
      default:
        stateCode = 1;
    }

    if (parts.length > 1) {
      let statusErrorCode = INVALID_UPDATER_STATE_CODE;
      if (parts[0] == STATE_FAILED) {
        statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
      }
      AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode);
    }
  }
  AUSTLMY.pingStateCode(suffix, stateCode);
}

/**
 * This returns true if the passed update is the same version or older than the
 * version and build ID values passed. Otherwise it returns false.
 */
function updateIsAtLeastAsOldAs(update, version, buildID) {
  if (!update || !update.appVersion || !update.buildID) {
    return false;
  }
  let versionComparison = Services.vc.compare(update.appVersion, version);
  return (
    versionComparison < 0 ||
    (versionComparison == 0 && update.buildID == buildID)
  );
}

/**
 * This returns the current version of the browser to use to check updates.
 */
function getCompatVersion() {
  return AppConstants.BASE_BROWSER_VERSION
    ? AppConstants.BASE_BROWSER_VERSION
    : Services.appinfo.version;
}

/**
 * This returns true if the passed update is the same version or older than
 * currently installed Firefox version.
 */
function updateIsAtLeastAsOldAsCurrentVersion(update) {
  return updateIsAtLeastAsOldAs(
    update,
    getCompatVersion(),
    Services.appinfo.appBuildID
  );
}

/**
 * This returns true if the passed update is the same version or older than
 * the update that we have already downloaded (UpdateManager.readyUpdate).
 * Returns false if no update has already been downloaded.
 */
function updateIsAtLeastAsOldAsReadyUpdate(update) {
  if (
    !lazy.UM.internal.readyUpdate ||
    !lazy.UM.internal.readyUpdate.appVersion ||
    !lazy.UM.internal.readyUpdate.buildID
  ) {
    return false;
  }
  return updateIsAtLeastAsOldAs(
    update,
    lazy.UM.internal.readyUpdate.appVersion,
    lazy.UM.internal.readyUpdate.buildID
  );
}

/**
 * This function determines whether the error represented by the passed error
 * code could potentially be recovered from or bypassed by updating without
 * using the Maintenance Service (i.e. by showing a UAC prompt).
 * We don't really want to show a UAC prompt, but it's preferable over the
 * manual update doorhanger. So this function effectively distinguishes between
 * which of those we should do if update staging failed. (The updater
 * automatically falls back if the Maintenance Services fails, so this function
 * doesn't handle that case)
 *
 * @param   An integer error code from the update.status file. Should be one of
 *          the codes enumerated in updatererrors.h.
 * @returns true if the code represents a Maintenance Service specific error.
 *          Otherwise, false.
 */
function isServiceSpecificErrorCode(errorCode) {
  return (
    (errorCode >= 24 && errorCode <= 33) || (errorCode >= 49 && errorCode <= 58)
  );
}

/**
 * This function determines whether the error represented by the passed error
 * code is the result of the updater failing to allocate memory. This is
 * relevant when staging because, since Firefox is also running, we may not be
 * able to allocate much memory. Thus, if we fail to stage an update, we may
 * succeed at updating without staging.
 *
 * @param   An integer error code from the update.status file. Should be one of
 *          the codes enumerated in updatererrors.h.
 * @returns true if the code represents a memory allocation error.
 *          Otherwise, false.
 */
function isMemoryAllocationErrorCode(errorCode) {
  return errorCode >= 10 && errorCode <= 14;
}

/**
 * Normally when staging, `nsUpdateProcessor::WaitForProcess` waits for the
 * staging process to complete by watching for its PID to terminate.
 * However, there are less ideal situations. Notably, we might start the browser
 * and find that update staging appears to already be in-progress. If that
 * happens, we really want to pick up the update process from STATE_STAGING,
 * but we don't really have any way of keeping an eye on the staging process
 * other than to just poll the status file.
 *
 * Like `nsUpdateProcessor`, this calls `nsIUpdateManager.refreshUpdateStatus`
 * after polling completes (regardless of result).
 *
 * It is also important to keep in mind that the updater might have crashed
 * during staging, meaning that the status file will never change, no matter how
 * long we keep polling. So we need to set an upper bound on how long we are
 * willing to poll for.
 *
 * There are three situations that we want to avoid.
 * (1) We don't want to set the poll interval too long. A user might be watching
 * the user interface and waiting to restart to install the update. A long poll
 * interval will cause them to have to wait longer than necessary. Especially
 * since the expected total staging time is not that long.
 * (2) We don't want to give up polling too early and give up on an update that
 * will ultimately succeed.
 * (3) We don't want to use a rapid polling interval over a long duration.
 *
 * To avoid these situations, we will start with a short polling interval, but
 * will increase it the longer that we have to wait. Then if we hit the upper
 * bound of polling, we will give up.
 */
function pollForStagingEnd() {
  let pollingIntervalMs = STAGING_POLLING_MIN_INTERVAL_MS;
  // Number of times to poll before increasing the polling interval.
  let pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL;
  let timeElapsedMs = 0;

  let pollingFn = () => {
    pollAttemptsAtIntervalRemaining -= 1;
    // This isn't a perfectly accurate way of keeping time, but it does nicely
    // sidestep dealing with issues of (non)monotonic time.
    timeElapsedMs += pollingIntervalMs;

    if (timeElapsedMs >= STAGING_POLLING_MAX_DURATION_MS) {
      lazy.UM.internal.refreshUpdateStatus();
      return;
    }

    if (readStatusFile(getReadyUpdateDir()) != STATE_APPLYING) {
      lazy.UM.internal.refreshUpdateStatus();
      return;
    }

    if (pollAttemptsAtIntervalRemaining <= 0) {
      pollingIntervalMs = Math.min(
        pollingIntervalMs * 2,
        STAGING_POLLING_MAX_INTERVAL_MS
      );
      pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL;
    }

    lazy.setTimeout(pollingFn, pollingIntervalMs);
  };

  lazy.setTimeout(pollingFn, pollingIntervalMs);
}

class UpdatePatch {
  // nsIUpdatePatch attribute names used to prevent nsIWritablePropertyBag from
  // over writing nsIUpdatePatch attributes.
  _attrNames = [
    "errorCode",
    "finalURL",
    "selected",
    "size",
    "state",
    "type",
    "URL",
  ];

  /**
   * @param   patch
   *          A <patch> element to initialize this object with
   * @throws if patch has a size of 0
   * @class
   */
  constructor(patch) {
    this._properties = {};
    this.errorCode = 0;
    this.finalURL = null;
    this.state = STATE_NONE;

    for (let i = 0; i < patch.attributes.length; ++i) {
      var attr = patch.attributes.item(i);
      // If an undefined value is saved to the xml file it will be a string when
      // it is read from the xml file.
      if (attr.value == "undefined") {
        continue;
      }
      switch (attr.name) {
        case "xmlns":
          // Don't save the XML namespace.
          break;
        case "selected":
          this.selected = attr.value == "true";
          break;
        case "size":
          if (0 == parseInt(attr.value)) {
            LOG("UpdatePatch:init - 0-sized patch!");
            throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
          }
          this[attr.name] = attr.value;
          break;
        case "errorCode":
          if (attr.value) {
            let val = parseInt(attr.value);
            // This will evaluate to false if the value is 0 but that's ok since
            // this.errorCode is set to the default of 0 above.
            if (val) {
              this.errorCode = val;
            }
          }
          break;
        case "finalURL":
        case "state":
        case "type":
        case "URL":
          this[attr.name] = attr.value;
          break;
        default:
          if (!this._attrNames.includes(attr.name)) {
            // Set nsIPropertyBag properties that were read from the xml file.
            this.setProperty(attr.name, attr.value);
          }
          break;
      }
    }
  }

  /**
   * See nsIUpdateService.idl
   */
  serialize(updates) {
    var patch = updates.createElementNS(URI_UPDATE_NS, "patch");
    patch.setAttribute("size", this.size);
    patch.setAttribute("type", this.type);
    patch.setAttribute("URL", this.URL);
    // Don't write an errorCode if it evaluates to false since 0 is the same as
    // no error code.
    if (this.errorCode) {
      patch.setAttribute("errorCode", this.errorCode);
    }
    // finalURL is not available until after the download has started
    if (this.finalURL) {
      patch.setAttribute("finalURL", this.finalURL);
    }
    // The selected patch is the only patch that should have this attribute.
    if (this.selected) {
      patch.setAttribute("selected", this.selected);
    }
    if (this.state != STATE_NONE) {
      patch.setAttribute("state", this.state);
    }

    for (let [name, value] of Object.entries(this._properties)) {
      if (value.present && !this._attrNames.includes(name)) {
        patch.setAttribute(name, value.data);
      }
    }
    return patch;
  }

  /**
   * See nsIWritablePropertyBag.idl
   */
  setProperty(name, value) {
    if (this._attrNames.includes(name)) {
      throw Components.Exception(
        "Illegal value '" +
          name +
          "' (attribute exists on nsIUpdatePatch) " +
          "when calling method: [nsIWritablePropertyBag::setProperty]",
        Cr.NS_ERROR_ILLEGAL_VALUE
      );
    }
    this._properties[name] = { data: value, present: true };
  }

  /**
   * See nsIWritablePropertyBag.idl
   */
  deleteProperty(name) {
    if (this._attrNames.includes(name)) {
      throw Components.Exception(
        "Illegal value '" +
          name +
          "' (attribute exists on nsIUpdatePatch) " +
          "when calling method: [nsIWritablePropertyBag::deleteProperty]",
        Cr.NS_ERROR_ILLEGAL_VALUE
      );
    }
    if (name in this._properties) {
      this._properties[name].present = false;
    } else {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }
  }

  /**
   * See nsIPropertyBag.idl
   *
   * Note: this only contains the nsIPropertyBag name / value pairs and not the
   *       nsIUpdatePatch name / value pairs.
   */
  get enumerator() {
    return this.enumerate();
  }

  *enumerate() {
    // An nsISupportsInterfacePointer is used so creating an array using
    // Array.from will retain the QueryInterface for nsIProperty.
    let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
      Ci.nsISupportsInterfacePointer
    );
    let qi = ChromeUtils.generateQI([Ci.nsIProperty]);
    for (let [name, value] of Object.entries(this._properties)) {
      if (value.present && !this._attrNames.includes(name)) {
        // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
        // elements are nsIProperty objects. Calling QueryInterface for
        // nsIProperty on the object doesn't return to the caller an object that
        // is already queried to nsIProperty but do it just in case it is fixed
        // at some point.
        ip.data = { name, value: value.data, QueryInterface: qi };
        yield ip.data.QueryInterface(Ci.nsIProperty);
      }
    }
  }

  /**
   * See nsIPropertyBag.idl
   *
   * Note: returns null instead of throwing when the property doesn't exist to
   *       simplify code and to silence warnings in debug builds.
   */
  getProperty(name) {
    if (this._attrNames.includes(name)) {
      throw Components.Exception(
        "Illegal value '" +
          name +
          "' (attribute exists on nsIUpdatePatch) " +
          "when calling method: [nsIWritablePropertyBag::getProperty]",
        Cr.NS_ERROR_ILLEGAL_VALUE
      );
    }
    if (name in this._properties && this._properties[name].present) {
      return this._properties[name].data;
    }
    return null;
  }

  QueryInterface = ChromeUtils.generateQI([
    Ci.nsIUpdatePatch,
    Ci.nsIPropertyBag,
    Ci.nsIWritablePropertyBag,
  ]);
}

class Update {
  // nsIUpdate attribute names used to prevent nsIWritablePropertyBag from over
  // writing nsIUpdate attributes.
  _attrNames = [
    "appVersion",
    "buildID",
    "channel",
    "detailsURL",
    "displayVersion",
    "elevationFailure",
    "errorCode",
    "installDate",
    "isCompleteUpdate",
    "name",
    "previousAppVersion",
    "promptWaitTime",
    "serviceURL",
    "state",
    "statusText",
    "type",
    "unsupported",
    "platformVersion",
  ];

  /**
   * Implements nsIUpdate
   *
   * @param   update
   *          An <update> element to initialize this object with
   * @throws if the update contains no patches
   * @class
   */
  constructor(update) {
    this._patches = [];
    this._properties = {};
    this.isCompleteUpdate = false;
    this.channel = "default";
    this.promptWaitTime = Services.prefs.getIntPref(
      PREF_APP_UPDATE_PROMPTWAITTIME,
      43200
    );
    this.unsupported = false;

    // Null <update>, assume this is a message container and do no
    // further initialization
    if (!update) {
      return;
    }

    for (let i = 0; i < update.childNodes.length; ++i) {
      let patchElement = update.childNodes.item(i);
      if (
        patchElement.nodeType != patchElement.ELEMENT_NODE ||
        patchElement.localName != "patch"
      ) {
        continue;
      }

      let patch;
      try {
        patch = new UpdatePatch(patchElement);
      } catch (e) {
        continue;
      }
      this._patches.push(patch);
    }

    if (update.hasAttribute("unsupported")) {
      this.unsupported = "true" == update.getAttribute("unsupported");
    } else if (update.hasAttribute("minSupportedOSVersion")) {
      let minOSVersion = update.getAttribute("minSupportedOSVersion");
      try {
        let osVersion = Services.sysinfo.getProperty("version");
        this.unsupported = Services.vc.compare(osVersion, minOSVersion) < 0;
      } catch (e) {}
    }
    if (
      !this.unsupported &&
      update.hasAttribute("minSupportedInstructionSet")
    ) {
      let minInstructionSet = update.getAttribute("minSupportedInstructionSet");
      if (
        ["MMX", "SSE", "SSE2", "SSE3", "SSE4A", "SSE4_1", "SSE4_2"].includes(
          minInstructionSet
        )
      ) {
        try {
          this.unsupported = !Services.sysinfo.getProperty(
            "has" + minInstructionSet
          );
        } catch (e) {}
      }
    }

    if (!this._patches.length && !this.unsupported) {
      throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
    }

    // Set the installDate value with the current time. If the update has an
    // installDate attribute this will be replaced with that value if it doesn't
    // equal 0.
    this.installDate = new Date().getTime();
    this.patchCount = this._patches.length;

    for (let i = 0; i < update.attributes.length; ++i) {
      let attr = update.attributes.item(i);
      if (attr.name == "xmlns" || attr.value == "undefined") {
        // Don't save the XML namespace or undefined values.
        // If an undefined value is saved to the xml file it will be a string when
        // it is read from the xml file.
        continue;
      } else if (attr.name == "detailsURL") {
        this.detailsURL = attr.value;
      } else if (attr.name == "installDate" && attr.value) {
        let val = parseInt(attr.value);
        if (val) {
          this.installDate = val;
        }
      } else if (attr.name == "errorCode" && attr.value) {
        let val = parseInt(attr.value);
        if (val) {
          // Set the value of |_errorCode| instead of |errorCode| since
          // selectedPatch won't be available at this point and normally the
          // nsIUpdatePatch will provide the errorCode.
          this._errorCode = val;
        }
      } else if (attr.name == "isCompleteUpdate") {
        this.isCompleteUpdate = attr.value == "true";
      } else if (attr.name == "promptWaitTime") {
        if (!isNaN(attr.value)) {
          this.promptWaitTime = parseInt(attr.value);
        }
      } else if (attr.name != "unsupported") {
        switch (attr.name) {
          case "appVersion":
          case "buildID":
          case "channel":
          case "displayVersion":
          case "elevationFailure":
          case "name":
          case "previousAppVersion":
          case "serviceURL":
          case "statusText":
          case "type":
          case "platformVersion":
            this[attr.name] = attr.value;
            break;
          default:
            if (!this._attrNames.includes(attr.name)) {
              // Set nsIPropertyBag properties that were read from the xml file.
              this.setProperty(attr.name, attr.value);
            }
            break;
        }
      }
    }

    if (!this.previousAppVersion) {
      this.previousAppVersion = getCompatVersion();
    }

    if (!this.elevationFailure) {
      this.elevationFailure = false;
    }

    if (this.unsupported && update.hasAttribute("unsupportedURL")) {
      // Override the detailsURL with the dedicated link for the EOL.
      // Otherwise it will point to the release blog post for the wrong version.
      // See tor-browser#44668.
      this.detailsURL = update.getAttribute("unsupportedURL");
    }

    if (!this.detailsURL) {
      try {
        // Try using a default details URL supplied by the distribution
        // if the update XML does not supply one.
        this.detailsURL = Services.urlFormatter.formatURLPref(
          PREF_APP_UPDATE_URL_DETAILS
        );
      } catch (e) {
        this.detailsURL = "";
      }
    }

    if (!this.displayVersion) {
      this.displayVersion = this.appVersion;
    }

    if (!this.name) {
      // When the update doesn't provide a name fallback to using
      // "<App Name> <Update App Version>"
      let brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES);
      let appName = brandBundle.GetStringFromName("brandShortName");
      this.name = lazy.gUpdateBundle.formatStringFromName("updateName", [
        appName,
        this.displayVersion,
      ]);
    }
  }

  /**
   * See nsIUpdateService.idl
   */
  getPatchAt(index) {
    return this._patches[index];
  }

  /**
   * See nsIUpdateService.idl
   *
   * We use a copy of the state cached on this object in |_state| only when
   * there is no selected patch, i.e. in the case when we could not load
   * active updates from the update manager for some reason but still have
   * the update.status file to work with.
   */
  _state = "";
  get state() {
    if (this.selectedPatch) {
      return this.selectedPatch.state;
    }
    return this._state;
  }
  set state(state) {
    if (this.selectedPatch) {
      this.selectedPatch.state = state;
    }
    this._state = state;
  }

  /**
   * See nsIUpdateService.idl
   *
   * We use a copy of the errorCode cached on this object in |_errorCode| only
   * when there is no selected patch, i.e. in the case when we could not load
   * active updates from the update manager for some reason but still have
   * the update.status file to work with.
   */
  _errorCode = 0;
  get errorCode() {
    if (this.selectedPatch) {
      return this.selectedPatch.errorCode;
    }
    return this._errorCode;
  }
  set errorCode(errorCode) {
    if (this.selectedPatch) {
      this.selectedPatch.errorCode = errorCode;
    }
    this._errorCode = errorCode;
  }

  /**
   * See nsIUpdateService.idl
   */
  get selectedPatch() {
    for (let i = 0; i < this.patchCount; ++i) {
      if (this._patches[i].selected) {
        return this._patches[i];
      }
    }
    return null;
  }

  /**
   * See nsIUpdateService.idl
   */
  serialize(updates) {
    // If appVersion isn't defined just return null. This happens when cleaning
    // up invalid updates (e.g. incorrect channel).
    if (!this.appVersion) {
      return null;
    }
    let update = updates.createElementNS(URI_UPDATE_NS, "update");
    update.setAttribute("appVersion", this.appVersion);
    update.setAttribute("buildID", this.buildID);
    update.setAttribute("channel", this.channel);
    update.setAttribute("detailsURL", this.detailsURL);
    update.setAttribute("displayVersion", this.displayVersion);
    update.setAttribute("platformVersion", this.platformVersion);
    update.setAttribute("installDate", this.installDate);
    update.setAttribute("isCompleteUpdate", this.isCompleteUpdate);
    update.setAttribute("name", this.name);
    update.setAttribute("previousAppVersion", this.previousAppVersion);
    update.setAttribute("promptWaitTime", this.promptWaitTime);
    update.setAttribute("serviceURL", this.serviceURL);
    update.setAttribute("type", this.type);

    if (this.statusText) {
      update.setAttribute("statusText", this.statusText);
    }
    if (this.unsupported) {
      update.setAttribute("unsupported", this.unsupported);
    }
    if (this.elevationFailure) {
      update.setAttribute("elevationFailure", this.elevationFailure);
    }

    for (let [name, value] of Object.entries(this._properties)) {
      if (value.present && !this._attrNames.includes(name)) {
        update.setAttribute(name, value.data);
      }
    }

    for (let i = 0; i < this.patchCount; ++i) {
      update.appendChild(this.getPatchAt(i).serialize(updates));
    }

    updates.documentElement.appendChild(update);
    return update;
  }

  /**
   * See nsIWritablePropertyBag.idl
   */
  setProperty(name, value) {
    if (this._attrNames.includes(name)) {
      throw Components.Exception(
        "Illegal value '" +
          name +
          "' (attribute exists on nsIUpdate) " +
          "when calling method: [nsIWritablePropertyBag::setProperty]",
        Cr.NS_ERROR_ILLEGAL_VALUE
      );
    }
    this._properties[name] = { data: value, present: true };
  }

  /**
   * See nsIWritablePropertyBag.idl
   */
  deleteProperty(name) {
    if (this._attrNames.includes(name)) {
      throw Components.Exception(
        "Illegal value '" +
          name +
          "' (attribute exists on nsIUpdate) " +
          "when calling method: [nsIWritablePropertyBag::deleteProperty]",
        Cr.NS_ERROR_ILLEGAL_VALUE
      );
    }
    if (name in this._properties) {
      this._properties[name].present = false;
    } else {
      throw Components.Exception("", Cr.NS_ERROR_FAILURE);
    }
  }

  /**
   * See nsIPropertyBag.idl
   *
   * Note: this only contains the nsIPropertyBag name value / pairs and not the
   *       nsIUpdate name / value pairs.
   */
  get enumerator() {
    return this.enumerate();
  }

  *enumerate() {
    // An nsISupportsInterfacePointer is used so creating an array using
    // Array.from will retain the QueryInterface for nsIProperty.
    let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
      Ci.nsISupportsInterfacePointer
    );
    let qi = ChromeUtils.generateQI([Ci.nsIProperty]);
    for (let [name, value] of Object.entries(this._properties)) {
      if (value.present && !this._attrNames.includes(name)) {
        // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
        // elements are nsIProperty objects. Calling QueryInterface for
        // nsIProperty on the object doesn't return to the caller an object that
        // is already queried to nsIProperty but do it just in case it is fixed
        // at some point.
        ip.data = { name, value: value.data, QueryInterface: qi };
        yield ip.data.QueryInterface(Ci.nsIProperty);
      }
    }
  }

  /**
   * See nsIPropertyBag.idl
   * Note: returns null instead of throwing when the property doesn't exist to
   *       simplify code and to silence warnings in debug builds.
   */
  getProperty(name) {
    if (this._attrNames.includes(name)) {
      throw Components.Exception(
        "Illegal value '" +
          name +
          "' (attribute exists on nsIUpdate) " +
          "when calling method: [nsIWritablePropertyBag::getProperty]",
        Cr.NS_ERROR_ILLEGAL_VALUE
      );
    }
    if (name in this._properties && this._properties[name].present) {
      return this._properties[name].data;
    }
    return null;
  }

  QueryInterface = ChromeUtils.generateQI([
    Ci.nsIUpdate,
    Ci.nsIPropertyBag,
    Ci.nsIWritablePropertyBag,
  ]);
}

export class UpdateService {
  #initPromise;

  /**
   * The downloader we are using to download updates. There is only ever one of
   * these.
   */
  _downloader = null;

  /**
   * Whether or not the service registered the "online" observer.
   */
  _registeredOnlineObserver = false;

  /**
   * The current number of consecutive socket errors
   */
  _consecutiveSocketErrors = 0;

  /**
   * A timer used to retry socket errors
   */
  _retryTimer = null;

  /**
   * Whether or not a background update check was initiated by the
   * application update timer notification.
   */
  _isNotify = true;

  /**
   * UpdateService
   * A Service for managing the discovery and installation of software updates.
   *
   * @class
   */
  constructor() {
    LOG("Creating UpdateService");
    // The observor notification to shut down the service must be before
    // profile-before-change since nsIUpdateManager uses profile-before-change
    // to shutdown and write the update xml files.
    Services.obs.addObserver(this, "quit-application");
    lazy.UpdateLog.addConfigChangeListener(() => {
      this._logStatus();
    });

    this._logStatus();

    this.internal = {
      init: async (force = false) => this.#init(force),
      downloadUpdate: async update => this.#downloadUpdate(update),
      stopDownload: async () => this.#stopDownload(),
      QueryInterface: ChromeUtils.generateQI([
        Ci.nsIApplicationUpdateServiceInternal,
      ]),
    };
  }

  /**
   * See nsIUpdateService.idl
   */
  async init() {
    await lazy.UpdateServiceStub.initUpdate();
  }

  /**
   * See nsIApplicationUpdateServiceInternal.init in nsIUpdateService.idl
   * and `#asyncInit`.
   */
  async #init(force = false) {
    if (force) {
      return this.#asyncInit();
    }
    if (!this.#initPromise) {
      this.#initPromise = this.#asyncInit();
    }

    return this.#initPromise;
  }

  /**
   * Handle Observer Service notifications
   *
   * @param   subject
   *          The subject of the notification
   * @param   topic
   *          The notification name
   * @param   data
   *          Additional data
   */
  async observe(subject, topic, data) {
    switch (topic) {
      case "network:offline-status-changed":
        await this._offlineStatusChanged(data);
        break;
      case lazy.TorConnectTopics.BootstrapComplete:
        this._bootstrapComplete();
        break;
      case "quit-application":
        Services.obs.removeObserver(this, topic);

        if (lazy.UpdateMutex.isLocked()) {
          // If we hold the update mutex, let it go!
          // The OS would clean this up sometime after shutdown,
          // but that would have no guarantee on timing.
          lazy.UpdateMutex.unlock();
        }
        if (this._retryTimer) {
          this._retryTimer.cancel();
        }

        // When downloading an update with nsIIncrementalDownload the download
        // is stopped when the quit-application observer notification is
        // received and networking hasn't started to shutdown. The download will
        // be resumed the next time the application starts. Downloads using
        // Windows BITS are not stopped since they don't require Firefox to be
        // running to perform the download.
        if (this._downloader) {
          if (this._downloader.usingBits) {
            await this._downloader.cleanup();
          } else {
            // stopDownload() calls _downloader.cleanup()
            await this.#stopDownload();
          }
        }
        // Prevent leaking the downloader (bug 454964)
        this._downloader = null;
        // In case any update checks are in progress.
        lazy.CheckSvc.stopAllChecks();
        break;
      case "test-unlock-update-mutex":
        if (Cu.isInAutomation) {
          if (lazy.UpdateMutex.isLocked()) {
            LOG("UpdateService:observe - releasing update mutex for testing");
            lazy.UpdateMutex.unlock();
          }
        }
        break;
    }
  }

  /**
   * This is effectively the initialization function for `UpdateService`.
   *
   * Bug 1900717 - This currently returns immediately if we don't hold the
   *               update mutex when we initialize and it never runs again, even
   *               if we eventually do get the update mutex.
   *
   * The initialization process basically involves doing any of these steps that
   * are currently relevant:
   *
   * Make sure we are in a good state
   *   Reset things and clean up if information in different files conflicts.
   *
   * Show the update elevation dialog
   *   When we need to elevate our privileges to update on macOS, we show a
   *   dialog to ask the user first. We show that now and if they accept it,
   *   they are updated on the next application launch.
   *
   * Post update processing
   *   Move installed updates into the history. Cleanup.
   *
   * Resume in-progress download
   *   For BITS, this really just means connecting to the BITS manager.
   *   For the internal downloader, we actually need to start the download
   *   again.
   *
   * Error fallback
   *   If an in-progress update failed in some way, we try to fall back. This
   *   could include retrying the installation differently next time,
   *   downloading a new MAR, or asking the user to download and run a new
   *   installer.
   */
  /* eslint-disable-next-line complexity */
  async #asyncInit() {
    // This check also ensures we have the update mutex
    if (!this.canCheckForUpdates) {
      LOG(
        "UpdateService:#asyncInit - unable to check for " +
          "updates... returning early"
      );
      return;
    }
    const readyUpdateDir = getReadyUpdateDir();
    let status = readStatusFile(readyUpdateDir);
    let statusParts = status.split(":");
    status = statusParts[0];
    LOG(`UpdateService:#asyncInit - status = "${status}"`);
    if (!this.canUsuallyApplyUpdates) {
      LOG(
        "UpdateService:#asyncInit - unable to apply " +
          "updates... returning early"
      );
      // If the update is present in the update directory somehow,
      // it would prevent us from notifying the user of further updates.
      LOG("UpdateService:#asyncInit - Cleaning up active updates.");
      await cleanupActiveUpdates();
      return;
    }

    let updates = [];
    if (lazy.UM.internal.readyUpdate) {
      updates.push(lazy.UM.internal.readyUpdate);
    }
    if (lazy.UM.internal.downloadingUpdate) {
      updates.push(lazy.UM.internal.downloadingUpdate);
    }

    // Validate the update state. It's too complicated to recover much of
    // anything if the state isn't already correct. And blowing away all the
    // update state is a good way to make sure we always start the update loop
    // in a reasonably consistent state. If we do this, we can return early
    // since the rest of the function deals with further validation and handling
    // of update success and failure.
    const resetUpdateState = async (missingUpdateObject = false) => {
      LOG("UpdateService:#asyncInit - Resetting update state");

      // If we are clearing away some update data and we don't actually have a
      // corresponding update object to write into the update history, just
      // make an empty one.
      if (missingUpdateObject && !updates.length) {
        if (!updates.length) {
          updates.push(new Update(null));
        }
      }

      // We are about to clean up any updates that we have, so if we do have
      // any, mark them as having failed.
      if (updates.length) {
        for (let update of updates) {
          update.state = STATE_FAILED;
          update.errorCode = ERR_UPDATE_STATE_NONE;
          update.statusText =
            lazy.gUpdateBundle.GetStringFromName("statusFailed");
        }
        let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE;
        pingStateAndStatusCodes(updates[0], true, newStatus);
      }

      await cleanupActiveUpdates();
      transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
    };

    switch (status) {
      case STATE_DOWNLOADING:
        if (!lazy.UM.internal.downloadingUpdate) {
          LOG("UpdateService:#asyncInit - Missing downloading update object");
          await resetUpdateState(true);
          return;
        } else if (lazy.UM.internal.readyUpdate) {
          // It isn't valid to have a ready update while in the downloading
          // state. We could just clear out the ready update and continue with
          // the downloading update, but if the state is inconsistent, we'd
          // prefer to start from scratch rather than try to rescue it
          LOG("UpdateService:#asyncInit - Unexpected ready update object");
          await resetUpdateState(true);
          return;
        }

        // Note that we don't check for an existing downloading update MAR
        // because there are good states where it does not exist (ex. BITS or
        // the transfer just hasn't started yet).
        break;
      case STATE_PENDING:
      case STATE_PENDING_SERVICE:
      case STATE_PENDING_ELEVATE:
      case STATE_APPLYING:
      case STATE_APPLIED:
      case STATE_APPLIED_SERVICE: {
        let readyMarFile = readyUpdateDir.clone();
        readyMarFile.append(FILE_UPDATE_MAR);

        if (!lazy.UM.internal.readyUpdate) {
          LOG("UpdateService:#asyncInit - Missing ready update object");
          await resetUpdateState(true);
          return;
        } else if (!readyMarFile.exists()) {
          LOG("UpdateService:#asyncInit - Missing mar file");
          await resetUpdateState();
          return;
        }
        break;
      }
      case STATE_SUCCEEDED:
      case STATE_FAILED:
        // There is more handing and validation to be done in this state, so
        // we never want to return early here or lose any of the available state
        // information, even if it is inconsistent.
        break;
      case STATE_DOWNLOAD_FAILED:
        // This is an odd state to start up in since we usually handle this
        // situation right away. We'll just clean this state up since the risk
        // of whatever state is still hanging around interfering with update
        // seems higher than the possible benefit of being able to salvage some
        // partial transfer.
        await resetUpdateState(!lazy.UM.internal.downloadingUpdate);
        return;
      case STATE_NONE: {
        const statusFile = readyUpdateDir.clone();
        statusFile.append(FILE_UPDATE_STATUS);
        // This file existing but not having a state in it is unexpected.
        const statusFileExists = statusFile.exists();
        if (statusFileExists) {
          LOG("UpdateService:#asyncInit - Status file is empty?");
        }
        await resetUpdateState(statusFileExists);
        return;
      }
      default:
        LOG(
          `UpdateService:#asyncInit - Unexpected state! ${status}) - assuming no valid updates`
        );
        await resetUpdateState();
        return;
    }

    let channelChanged = updatesToCheck => {
      for (let update of updatesToCheck) {
        if (update.channel != lazy.UpdateUtils.UpdateChannel) {
          return true;
        }
      }
      return false;
    };
    if (channelChanged(updates)) {
      let channel = lazy.UM.internal.readyUpdate
        ? lazy.UM.internal.readyUpdate.channel
        : lazy.UM.internal.downloadingUpdate.channel;
      LOG(
        "UpdateService:#asyncInit - update channel is " +
          "different than application's channel, removing update. update " +
          "channel: " +
          channel +
          ", expected channel: " +
          lazy.UpdateUtils.UpdateChannel
      );
      // User switched channels, clear out the old active updates and remove
      // partial downloads
      for (let update of updates) {
        update.state = STATE_FAILED;
        update.errorCode = ERR_CHANNEL_CHANGE;
        update.statusText =
          lazy.gUpdateBundle.GetStringFromName("statusFailed");
      }
      let newStatus = STATE_FAILED + ": " + ERR_CHANNEL_CHANGE;
      pingStateAndStatusCodes(updates[0], true, newStatus);
      await cleanupActiveUpdates();
      return;
    }

    // Handle the case when the update is the same or older than the current
    // version and nsUpdateDriver.cpp skipped updating due to the version being
    // older than the current version. This also handles the general case when
    // an update is for an older version or the same version and same build ID.
    if (
      status == STATE_PENDING ||
      status == STATE_PENDING_SERVICE ||
      status == STATE_APPLIED ||
      status == STATE_APPLIED_SERVICE ||
      status == STATE_PENDING_ELEVATE ||
      status == STATE_DOWNLOADING
    ) {
      let tooOldUpdate;
      if (
        updateIsAtLeastAsOldAs(
          lazy.UM.internal.readyUpdate,
          Services.appinfo.version,
          Services.appinfo.appBuildID
        )
      ) {
        tooOldUpdate = lazy.UM.internal.readyUpdate;
      } else if (
        updateIsAtLeastAsOldAs(
          lazy.UM.internal.downloadingUpdate,
          Services.appinfo.version,
          Services.appinfo.appBuildID
        )
      ) {
        tooOldUpdate = lazy.UM.internal.downloadingUpdate;
      }
      if (tooOldUpdate) {
        LOG(
          "UpdateService:#asyncInit - removing update for older " +
            "application version or same application version with same build " +
            "ID. update application version: " +
            tooOldUpdate.appVersion +
            ", " +
            "application version: " +
            Services.appinfo.version +
            ", update " +
            "build ID: " +
            tooOldUpdate.buildID +
            ", application build ID: " +
            Services.appinfo.appBuildID
        );
        tooOldUpdate.state = STATE_FAILED;
        tooOldUpdate.statusText =
          lazy.gUpdateBundle.GetStringFromName("statusFailed");
        tooOldUpdate.errorCode = ERR_OLDER_VERSION_OR_SAME_BUILD;
        // This could be split out to report telemetry for each case.
        let newStatus = STATE_FAILED + ": " + ERR_OLDER_VERSION_OR_SAME_BUILD;
        pingStateAndStatusCodes(tooOldUpdate, true, newStatus);
        // Cleanup both updates regardless of which one is too old. It's
        // exceedingly unlikely that a user could end up in a state where one
        // update is acceptable and the other isn't. And it makes this function
        // considerably more complex to try to deal with that possibility.
        await cleanupActiveUpdates();
        return;
      }
    }

    pingStateAndStatusCodes(
      status == STATE_DOWNLOADING
        ? lazy.UM.internal.downloadingUpdate
        : lazy.UM.internal.readyUpdate,
      true,
      status
    );
    if (lazy.UM.internal.downloadingUpdate || status == STATE_DOWNLOADING) {
      if (status == STATE_SUCCEEDED) {
        // If we successfully installed an update while we were downloading
        // another update, the downloading update is now a partial MAR for
        // a version that is no longer installed. We know that it's a partial
        // MAR without checking because we currently only download partial MARs
        // when an update has already been downloaded.
        LOG(
          "UpdateService:#asyncInit - removing downloading patch " +
            "because we installed a different patch before it finished" +
            "downloading."
        );
        await cleanupDownloadingUpdate();
      } else {
        // Attempt to resume download
        if (lazy.UM.internal.downloadingUpdate) {
          LOG(
            "UpdateService:#asyncInit - resuming patch found in " +
              "downloading state"
          );
          let result = await this.#downloadUpdate(
            lazy.UM.internal.downloadingUpdate
          );
          if (
            result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS &&
            result !=
              Ci.nsIApplicationUpdateService
                .DOWNLOAD_FAILURE_CANNOT_RESUME_IN_BACKGROUND
          ) {
            LOG(
              "UpdateService:#asyncInit - Failed to resume patch. " +
                "Cleaning up downloading update."
            );
            await cleanupDownloadingUpdate();
          }
        } else {
          LOG(
            "UpdateService:#asyncInit - Warning: found " +
              "downloading state, but no downloading patch. Cleaning up " +
              "active updates."
          );
          // Put ourselves back in a good state.
          await cleanupActiveUpdates();
        }
        if (status == STATE_DOWNLOADING) {
          // Done dealing with the downloading update, and there is no ready
          // update, so return early.
          return;
        }
      }
    }

    let update = lazy.UM.internal.readyUpdate;

    if (status == STATE_APPLYING) {
      // This indicates that the background updater service is in either of the
      // following two states:
      // 1. It is in the process of applying an update in the background, and
      //    we just happen to be racing against that.
      // 2. It has failed to apply an update for some reason, and we hit this
      //    case because the updater process has set the update status to
      //    applying, but has never finished.
      // In order to differentiate between these two states, we look at the
      // state field of the update object.  If it's "pending", then we know
      // that this is the first time we're hitting this case, so we switch
      // that state to "applying" and we just wait and hope for the best.
      // If it's "applying", we know that we've already been here once, so
      // we really want to start from a clean state.
      if (
        update &&
        (update.state == STATE_PENDING || update.state == STATE_PENDING_SERVICE)
      ) {
        LOG(
          "UpdateService:#asyncInit - patch found in applying " +
            "state for the first time"
        );
        update.state = STATE_APPLYING;
        lazy.UM.saveUpdates();
        transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING);
        pollForStagingEnd();
      } else {
        // We get here even if we don't have an update object
        LOG(
          "UpdateService:#asyncInit - patch found in applying " +
            "state for the second time. Cleaning up ready update."
        );
        cleanupReadyUpdate();
      }
      return;
    }

    if (!update) {
      if (status != STATE_SUCCEEDED) {
        LOG(
          "UpdateService:#asyncInit - previous patch failed " +
            "and no patch available. Cleaning up ready update."
        );
        cleanupReadyUpdate();
        return;
      }
      LOG(
        "UpdateService:#asyncInit - Update data missing. Creating " +
          "an empty update object."
      );
      update = new Update(null);
    }

    update.state = status;
    LOG(
      `UpdateService:#asyncInit - Setting update's state from ` +
        `the status file (="${update.state}")`
    );
    if (update.state == STATE_FAILED && statusParts[1]) {
      update.errorCode = parseInt(statusParts[1]);
      LOG(
        `UpdateService:#asyncInit - Setting update's errorCode ` +
          `from the status file (="${update.errorCode}")`
      );
    }

    if (status != STATE_SUCCEEDED) {
      // If there are new update logs, rotate them to ensure none are ever
      // overwritten. By passing `false` the patch directory won't be removed.
      cleanUpReadyUpdateDir(false);
    }

    // Track number of installation attempts for this patch
    if (
      update.selectedPatch &&
      [STATE_SUCCEEDED, STATE_FAILED].includes(status)
    ) {
      let totalInstallAttempts =
        update.selectedPatch
          .QueryInterface(Ci.nsIWritablePropertyBag)
          .getProperty("numTotalInstallAttempts") ?? 0;

      update.selectedPatch
        .QueryInterface(Ci.nsIWritablePropertyBag)
        .setProperty("numTotalInstallAttempts", totalInstallAttempts + 1);
    }

    if (status == STATE_SUCCEEDED) {
      if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) {
        Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS);
      }
      update.statusText =
        lazy.gUpdateBundle.GetStringFromName("installSuccess");

      // The only time that update is not a reference to readyUpdate is when
      // readyUpdate is null.
      if (!lazy.UM.internal.readyUpdate) {
        LOG(
          "UpdateService:#asyncInit - Assigning successful update " +
            "readyUpdate before cleaning it up."
        );
        lazy.UM.internal.readyUpdate = update;
      }

      // Done with this update. Clean it up.
      LOG("UpdateService:#asyncInit - Cleaning up successful ready update.");
      cleanupReadyUpdate();

      Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
    } else if (status == STATE_PENDING_ELEVATE) {
      // In case the active-update.xml file is deleted.
      if (!update) {
        LOG(
          "UpdateService:#asyncInit - status is pending-elevate " +
            "but there isn't a ready update, removing update"
        );
        cleanupReadyUpdate();
      } else {
        transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
        if (Services.startup.wasSilentlyStarted) {
          // This check _should_ be unnecessary since we should not silently
          // restart if state == pending-elevate. But the update elevation
          // dialog is a way that we could potentially show UI on startup, even
          // with no windows open. Which we really do not want to do on a silent
          // restart.
          // So this is defense in depth.
          LOG(
            "UpdateService:#asyncInit - status is " +
              "pending-elevate, but this is a silent startup, so the " +
              "elevation window has been suppressed."
          );
        } else {
          LOG(
            "UpdateService:#asyncInit - status is " +
              "pending-elevate. Showing Update elevation dialog."
          );
          let uri = "chrome://mozapps/content/update/updateElevation.xhtml";
          let features =
            "chrome,centerscreen,resizable=no,titlebar,toolbar=no,dialog=no";

          // The following timeout is intended to make the elevation dialog
          // appear on top of any browser windows after startup. In the past,
          // this dialog would frequently be displayed first, then getting
          // obscured by browser windows. The timeout period is arbitrary and
          // may be adjusted, but this seemed to work well during initial
          // testing. See bug 1273536 for more info.
          lazy.setTimeout(() => {
            Services.ww.openWindow(
              null,
              uri,
              "Update:Elevation",
              features,
              null
            );
          }, 2000);
        }
      }
    } else if (
      status == STATE_PENDING ||
      status == STATE_PENDING_SERVICE ||
      status == STATE_APPLIED ||
      status == STATE_APPLIED_SERVICE
    ) {
      // We started up with an update already pending. Usually we apply updates
      // at startup, but there are some cases where we do not. Most likely, we
      // will just end up installing this update the next time we start up, but
      // we should make sure that the pending update looks valid since it may
      // prevent us from downloading a new one in the meantime.
      LOG("UpdateService:#asyncInit - Verifying existing pending update.");
      // The only things the updater binary really needs to update are
      // `update.status` (which we already read `status` from) and `update.mar`.
      let marFile = readyUpdateDir.clone();
      marFile.append(FILE_UPDATE_MAR);
      if (!marFile.exists()) {
        LOG("UpdateService:#asyncInit - Cleaning up missing pending update.");
        cleanupReadyUpdate();
      }
    } else {
      // If there was an I/O error it is assumed that the patch is not invalid
      // and it is set to pending so an attempt to apply it again will happen
      // when the application is restarted.
      if (update.state == STATE_FAILED && update.errorCode) {
        LOG("UpdateService:#asyncInit - Attempting handleUpdateFailure");
        if (handleUpdateFailure(update)) {
          LOG("UpdateService:#asyncInit - handleUpdateFailure success.");
          return;
        }
      }

      LOG(
        "UpdateService:#asyncInit - Attempting to fall back to a " +
          "complete update."
      );
      // Something went wrong with the patch application process.
      await handleFallbackToCompleteUpdate();
    }
  }

  /**
   * Register an observer when the network comes online, so we can short-circuit
   * the app.update.interval when there isn't connectivity
   */
  _registerOnlineObserver() {
    if (this._registeredOnlineObserver) {
      LOG(
        "UpdateService:_registerOnlineObserver - observer already registered"
      );
      return;
    }

    LOG(
      "UpdateService:_registerOnlineObserver - waiting for the network to " +
        "be online, then forcing another check"
    );

    Services.obs.addObserver(this, "network:offline-status-changed");
    this._registeredOnlineObserver = true;
  }

  /**
   * Called from the network:offline-status-changed observer.
   */
  async _offlineStatusChanged(status) {
    if (status !== "online") {
      return;
    }

    Services.obs.removeObserver(this, "network:offline-status-changed");
    this._registeredOnlineObserver = false;

    LOG(
      "UpdateService:_offlineStatusChanged - network is online, forcing " +
        "another background check"
    );

    // the background checker is contained in notify
    await this._attemptResume();
  }

  /**
   * Whether we should wait for the Tor bootstrap event to try again because we
   * are not yet bootstrapped.
   *
   * If the browser does not monitor the Tor connection state, this will return
   * `false` since the event will never fire.
   *
   * @returns {boolean} - Whether we should wait for the bootstrap event.
   */
  _shouldRegisterBootstrapObserver() {
    return (
      lazy.TorConnect.enabled &&
      lazy.TorConnect.stageName !== lazy.TorConnectStage.Bootstrapped
    );
  }

  /**
   * Wait for the Tor connection to be bootstrapped before trying again.
   */
  _registerBootstrapObserver() {
    // Double check that we haven't since become bootstrapped between the call
    // to _shouldRegisterBootstrapObserver and _registerBootstrapObserver, which
    // may be delayed. If so, we do not wait for the BootstrapComplete signal
    // since it has already fired.
    if (lazy.TorConnect.stageName === lazy.TorConnectStage.Bootstrapped) {
      LOG("UpdateService:_registerBootstrapObserver - already bootstrapped");
      this._bootstrapComplete();
      return;
    }

    if (this._registeredBootstrapObserver) {
      LOG(
        "UpdateService:_registerBootstrapObserver - observer already registered"
      );
      return;
    }

    LOG(
      "UpdateService:_registerBootstrapObserver - waiting for tor bootstrap " +
        "to be complete, then forcing another check"
    );

    Services.obs.addObserver(this, lazy.TorConnectTopics.BootstrapComplete);
    this._registeredBootstrapObserver = true;
  }

  /**
   * Called when the Tor connection becomes bootstrapped. This will trigger a
   * retry.
   */
  _bootstrapComplete() {
    if (this._registeredBootstrapObserver) {
      Services.obs.removeObserver(
        this,
        lazy.TorConnectTopics.BootstrapComplete
      );
      this._registeredBootstrapObserver = false;
    }

    LOG(
      "UpdateService:_bootstrapComplete - bootstrapping complete, forcing " +
        "another background check"
    );

    this._attemptResume();
  }

  /**
   * See nsIUpdateService.idl
   */
  async onCheckComplete(result) {
    if (result.succeeded) {
      await this._selectAndInstallUpdate(result.updates);
      return;
    }

    if (!result.checksAllowed) {
      LOG("UpdateService:onCheckComplete - checks not allowed");
      return;
    }

    // On failure, result.updates is guaranteed to have exactly one update
    // containing error information.
    let update = result.updates[0];

    LOG(
      "UpdateService:onCheckComplete - error during background update. error " +
        "code: " +
        update.errorCode +
        ", status text: " +
        update.statusText
    );

    if (update.errorCode == NETWORK_ERROR_OFFLINE) {
      // Register an online observer to try again
      this._registerOnlineObserver();
      if (this._pingSuffix) {
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
      }
      return;
    } else if (
      update.errorCode === PROXY_SERVER_CONNECTION_REFUSED &&
      this._shouldRegisterBootstrapObserver()
    ) {
      // Register boostrap observer to try again, but only when we own the
      // tor process.
      this._registerBootstrapObserver();
      return;
    }

    // Send the error code to telemetry
    AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode);
    update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES;
    let errCount = Services.prefs.getIntPref(
      PREF_APP_UPDATE_BACKGROUNDERRORS,
      0
    );

    // If we already have an update ready, we don't want to worry the user over
    // update check failures. As far as the user knows, the update status is
    // the status of the ready update. We don't want to confuse them by saying
    // that an update check failed.
    if (lazy.UM.internal.readyUpdate) {
      LOG(
        "UpdateService:onCheckComplete - Ignoring error because another " +
          "update is ready."
      );
      return;
    }

    errCount++;
    Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
    // Don't allow the preference to set a value greater than 20 for max errors.
    let maxErrors = Math.min(
      Services.prefs.getIntPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10),
      20
    );

    if (errCount >= maxErrors) {
      LOG(
        "UpdateService:onCheckComplete - notifying observers of error. " +
          "topic: update-error, status: check-attempts-exceeded"
      );
      Services.obs.notifyObservers(
        update,
        "update-error",
        "check-attempts-exceeded"
      );
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
    } else {
      LOG(
        "UpdateService:onCheckComplete - notifying observers of error. " +
          "topic: update-error, status: check-attempt-failed"
      );
      Services.obs.notifyObservers(
        update,
        "update-error",
        "check-attempt-failed"
      );
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
    }
  }

  /**
   * Called when a connection should be resumed
   */
  async _attemptResume() {
    LOG("UpdateService:_attemptResume");
    // If a download is in progress and we aren't already downloading it, then
    // resume it.
    if (this.isDownloading) {
      // There is nothing to resume. We are already downloading.
      LOG("UpdateService:_attemptResume - already downloading.");
      return;
    }
    if (
      this._downloader &&
      this._downloader._patch &&
      this._downloader._patch.state == STATE_DOWNLOADING &&
      this._downloader._update
    ) {
      LOG(
        "UpdateService:_attemptResume - _patch.state: " +
          this._downloader._patch.state
      );
      let result = await this.#downloadUpdate(this._downloader._update);
      LOG("UpdateService:_attemptResume - downloadUpdate result: " + result);
      if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
        LOG(
          "UpdateService:_attemptResume - Resuming download failed. Cleaning " +
            "up downloading update."
        );
        await cleanupDownloadingUpdate();
      }
      return;
    }

    // Kick off an update check
    (async () => {
      let check = lazy.CheckSvc.internal.checkForUpdates(
        lazy.CheckSvc.BACKGROUND_CHECK
      );
      await this.onCheckComplete(await check.result);
    })();
  }

  /**
   * Notified when a timer fires
   *
   * @param   _timer
   *          The timer that fired
   */
  async notify(_timer) {
    await this._checkForBackgroundUpdates(true);
  }

  /**
   * See nsIUpdateService.idl
   */
  async checkForBackgroundUpdates() {
    return this._checkForBackgroundUpdates(false);
  }

  // The suffix used for background update check glean metric names.
  get _pingSuffix() {
    if (lazy.UM.internal.readyUpdate) {
      // Once an update has been downloaded, all later updates will be reported
      // to telemetry as subsequent updates. We move the first update into
      // readyUpdate as soon as the download is complete, so any update checks
      // after readyUpdate is no longer null are subsequent update checks.
      return AUSTLMY.SUBSEQUENT;
    }
    return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL;
  }

  /**
   * Checks for updates in the background.
   *
   * @param   isNotify
   *          Whether or not a background update check was initiated by the
   *          application update timer notification.
   */
  async _checkForBackgroundUpdates(isNotify) {
    await this.init();

    if (!this.disabled && AppConstants.NIGHTLY_BUILD) {
      // Scalar ID: update.suppress_prompts
      AUSTLMY.pingSuppressPrompts();
    }
    if (this.disabled || this.manualUpdateOnly) {
      // Return immediately if we are disabled by policy. Otherwise, just the
      // telemetry we try to collect below can potentially trigger a restart
      // prompt if the update directory isn't writable. And we shouldn't be
      // telling the user about update failures if update is disabled.
      // See Bug 1599590.
      // Note that we exit unconditionally here if we are only doing manual
      // update checks, because manual update checking uses a completely
      // different code path (AppUpdater.sys.mjs creates its own nsIUpdateChecker),
      // bypassing this function completely.
      if (!this.disabledForTesting) {
        // This can cause some problems for tests that aren't setup properly for
        // this.
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
      }
      return false;
    }

    this._isNotify = isNotify;

    // Glean metric names:
    // Glean.update.pingCountExternal
    // Glean.update.pingCountNotify
    // Glean.update.pingCountSubsequent
    Glean.update["pingCount" + this._pingSuffix].add();

    // Glean metric names:
    // Glean.update.unableToApplyExternal
    // Glean.update.unableToApplyNotify
    // Glean.update.unableToApplySubsequent
    if (!getCanApplyUpdates()) {
      Glean.update["unableToApply" + this._pingSuffix].add();
    }

    // Glean metric names:
    // Glean.update.cannotStageExternal
    // Glean.update.cannotStageNotify
    // Glean.update.cannotStageSubsequent
    if (!getCanApplyUpdates()) {
      Glean.update["cannotStage" + this._pingSuffix].add();
    }
    if (AppConstants.platform == "win") {
      // labeled counter metric names:
      // Glean.update.canUseBitsExternal
      // Glean.update.canUseBitsNotify
      // Glean.update.canUseBitsSubsequent
      Glean.update["canUseBits" + this._pingSuffix][getCanUseBits()].add();
    }
    // Glean metric names:
    // Glean.update.invalidLastupdatetimeExternal
    // Glean.update.invalidLastupdatetimeNotify
    // Glean.update.invalidLastupdatetimeSubsequent
    // Glean.update.lastNotifyIntervalDaysExternal
    // Glean.update.lastNotifyIntervalDaysNotify
    // Glean.update.lastNotifyIntervalDaysSubsequent
    AUSTLMY.pingLastUpdateTime(this._pingSuffix);
    // Glean metric names:
    // Glean.update.notPrefUpdateAutoExternal
    // Glean.update.notPrefUpdateAutoNotify
    // Glean.update.notPrefUpdateAutoSubsequent
    lazy.UpdateUtils.getAppUpdateAutoEnabled().then(enabled => {
      if (!enabled) {
        Glean.update["notPrefUpdateAuto" + this._pingSuffix].add();
      }
    });
    // Glean metric names:
    // Glean.update.notPrefUpdateStagingEnabledExternal
    // Glean.update.notPrefUpdateStagingEnabledNotify
    // Glean.update.notPrefUpdateStagingEnabledSubsequent
    AUSTLMY.pingBoolPref(
      Glean.update["notPrefUpdateStagingEnabled" + this._pingSuffix],
      PREF_APP_UPDATE_STAGING_ENABLED,
      true,
      true
    );
    if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
      // Glean metric names:
      // Glean.update.prefUpdateCancelationsExternal
      // Glean.update.prefUpdateCancelationsNotify
      // Glean.update.prefUpdateCancelationsSubsequent
      AUSTLMY.pingIntPref(
        Glean.update["prefUpdateCancelations" + this._pingSuffix],
        PREF_APP_UPDATE_CANCELATIONS,
        0,
        0
      );
    }
    if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
      // Glean metric names:
      // Glean.update.notPrefUpdateServiceEnabledExternal
      // Glean.update.notPrefUpdateServiceEnabledNotify
      // Glean.update.notPrefUpdateServiceEnabledSubsequent
      AUSTLMY.pingBoolPref(
        Glean.update["notPrefUpdateServiceEnabled" + this._pingSuffix],
        PREF_APP_UPDATE_SERVICE_ENABLED,
        true
      );
      // Glean metric names:
      // Glean.update.prefServiceErrorsExternal
      // Glean.update.prefServiceErrorsNotify
      // Glean.update.prefServiceErrorsSubsequent
      AUSTLMY.pingIntPref(
        Glean.update["prefServiceErrors" + this._pingSuffix],
        PREF_APP_UPDATE_SERVICE_ERRORS,
        0,
        0
      );
      if (AppConstants.platform == "win") {
        // Glean metric names:
        // Glean.update.serviceInstalledExternal
        // Glean.update.serviceInstalledNotify
        // Glean.update.serviceInstalledSubsequent
        // Glean.update.serviceManuallyUninstalledExternal
        // Glean.update.serviceManuallyUninstalledNotify
        // Glean.update.serviceManuallyUninstalledSubsequent
        AUSTLMY.pingServiceInstallStatus(
          this._pingSuffix,
          isServiceInstalled()
        );
      }
    }

    // If a download is in progress or the patch has been staged do nothing.
    if (this.isDownloading) {
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING);
      return false;
    }

    // Once we have downloaded a complete update, do not download further
    // updates until the complete update is installed. This is important,
    // because if we fall back from a partial update to a complete update,
    // it might be because of changes to the patch directory (which would cause
    // a failure to apply any partial MAR). So we really don't want to replace
    // a downloaded complete update with a downloaded partial update. And we
    // do not currently download complete updates if there is already a
    // readyUpdate available.
    if (
      lazy.UM.internal.readyUpdate &&
      lazy.UM.internal.readyUpdate.selectedPatch &&
      lazy.UM.internal.readyUpdate.selectedPatch.type == "complete"
    ) {
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
      return false;
    }

    // If we start downloading an update while the readyUpdate is staging, we
    // run the risk of eventually wanting to overwrite readyUpdate with the
    // downloadingUpdate while the readyUpdate is still staging. Then we would
    // have to have a weird intermediate state where the downloadingUpdate has
    // finished downloading, but can't be moved yet. It's simpler to just not
    // start a new update if the old one is still staging.
    if (this.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) {
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
      return false;
    }

    // Asynchronously kick off update checking
    (async () => {
      let validUpdateURL = true;
      try {
        await lazy.CheckSvc.getUpdateURL(lazy.CheckSvc.BACKGROUND_CHECK);
      } catch (e) {
        validUpdateURL = false;
      }

      // The following checks are done here so they can be differentiated from
      // foreground checks.
      if (!lazy.UpdateUtils.OSVersion) {
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
      } else if (!lazy.UpdateUtils.ABI) {
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI);
      } else if (!validUpdateURL) {
        AUSTLMY.pingCheckCode(
          this._pingSuffix,
          AUSTLMY.CHK_INVALID_DEFAULT_URL
        );
      } else if (!hasUpdateMutex()) {
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
      } else if (isOtherInstanceRunning()) {
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OTHER_INSTANCE);
      } else if (!this.canCheckForUpdates) {
        AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
      }

      let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK);
      await this.onCheckComplete(await check.result);
    })();

    return true;
  }

  /**
   * Determine the update from the specified updates that should be offered.
   * If both valid major and minor updates are available the minor update will
   * be offered.
   *
   * @param   updates
   *          An array of available nsIUpdate items
   * @return  The nsIUpdate to offer.
   */
  #selectUpdate(updates) {
    if (!updates.length) {
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND);
      return null;
    }

    // The ping for unsupported is sent after the call to showPrompt.
    if (updates.length == 1 && updates[0].unsupported) {
      return updates[0];
    }

    // Choose the newest of the available minor and major updates.
    var majorUpdate = null;
    var minorUpdate = null;
    var vc = Services.vc;
    let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND;

    for (const update of updates) {
      // Ignore updates for older versions of the application and updates for
      // the same version of the application with the same build ID.
      if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
        LOG(
          "UpdateService:selectUpdate - skipping update because the " +
            "update's application version is not greater than the current " +
            "application version"
        );
        lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
        continue;
      }

      if (updateIsAtLeastAsOldAsReadyUpdate(update)) {
        LOG(
          "UpdateService:selectUpdate - skipping update because the " +
            "update's application version is not greater than that of the " +
            "currently downloaded update"
        );
        lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
        continue;
      }

      if (lazy.UM.internal.readyUpdate && !getPatchOfType(update, "partial")) {
        LOG(
          "UpdateService:selectUpdate - skipping update because no partial " +
            "patch is available and an update has already been downloaded."
        );
        lastCheckCode = AUSTLMY.CHK_NO_PARTIAL_PATCH;
        continue;
      }

      switch (update.type) {
        case "major":
          if (!majorUpdate || majorUpdate.unsupported) {
            majorUpdate = update;
          } else if (
            !update.unsupported &&
            vc.compare(majorUpdate.appVersion, update.appVersion) <= 0
          ) {
            majorUpdate = update;
          }
          break;
        case "minor":
          if (!minorUpdate || minorUpdate.unsupported) {
            minorUpdate = update;
          } else if (
            !update.unsupported &&
            vc.compare(minorUpdate.appVersion, update.appVersion) <= 0
          ) {
            minorUpdate = update;
          }
          break;
        default:
          LOG(
            "UpdateService:selectUpdate - skipping unknown update type: " +
              update.type
          );
          lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE;
          break;
      }
    }

    let update = minorUpdate || majorUpdate;
    if (AppConstants.platform == "macosx" && update) {
      if (getElevationRequired()) {
        let installAttemptVersion = Services.prefs.getCharPref(
          PREF_APP_UPDATE_ELEVATE_VERSION,
          null
        );
        if (vc.compare(installAttemptVersion, update.appVersion) != 0) {
          Services.prefs.setCharPref(
            PREF_APP_UPDATE_ELEVATE_VERSION,
            update.appVersion
          );
          if (
            Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)
          ) {
            Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
          }
          if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
            Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
          }
        } else {
          let numCancels = Services.prefs.getIntPref(
            PREF_APP_UPDATE_CANCELATIONS_OSX,
            0
          );
          let rejectedVersion = Services.prefs.getCharPref(
            PREF_APP_UPDATE_ELEVATE_NEVER,
            ""
          );
          let maxCancels = Services.prefs.getIntPref(
            PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
            DEFAULT_CANCELATIONS_OSX_MAX
          );
          if (numCancels >= maxCancels) {
            LOG(
              "UpdateService:selectUpdate - the user requires elevation to " +
                "install this update, but the user has exceeded the max " +
                "number of elevation attempts."
            );
            update.elevationFailure = true;
            AUSTLMY.pingCheckCode(
              this._pingSuffix,
              AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION
            );
          } else if (vc.compare(rejectedVersion, update.appVersion) == 0) {
            LOG(
              "UpdateService:selectUpdate - the user requires elevation to " +
                "install this update, but elevation is disabled for this " +
                "version."
            );
            update.elevationFailure = true;
            AUSTLMY.pingCheckCode(
              this._pingSuffix,
              AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION
            );
          } else {
            LOG(
              "UpdateService:selectUpdate - the user requires elevation to " +
                "install the update."
            );
          }
        }
      } else {
        // Clear elevation-related prefs since they no longer apply (the user
        // may have gained write access to the Firefox directory or an update
        // was executed with a different profile).
        if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) {
          Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION);
        }
        if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
          Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
        }
        if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
          Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
        }
      }
    } else if (!update) {
      AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode);
    }

    return update;
  }

  /*
   * See nsIUpdateService.idl
   */
  async selectUpdate(updates) {
    await this.init();
    return this.#selectUpdate(updates);
  }

  /**
   * Determine which of the specified updates should be installed and begin the
   * download/installation process or notify the user about the update.
   *
   * @param   updates
   *          An array of available updates
   */
  async _selectAndInstallUpdate(updates) {
    // Return early if there's an active update. The user is already aware and
    // is downloading or performed some user action to prevent notification.
    if (lazy.UM.internal.downloadingUpdate) {
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE);
      return;
    }

    if (this.disabled) {
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
      LOG(
        "UpdateService:_selectAndInstallUpdate - not prompting because " +
          "update is disabled"
      );
      return;
    }

    var update = this.#selectUpdate(updates);
    if (!update || update.elevationFailure) {
      return;
    }

    if (update.unsupported) {
      LOG(
        "UpdateService:_selectAndInstallUpdate - update not supported for " +
          "this system. Notifying observers. topic: update-available, " +
          "status: unsupported"
      );
      Services.obs.notifyObservers(update, "update-available", "unsupported");
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
      return;
    }

    if (!getCanApplyUpdates()) {
      LOG(
        "UpdateService:_selectAndInstallUpdate - the user is unable to " +
          "apply updates... prompting. Notifying observers. " +
          "topic: update-available, status: cant-apply"
      );
      Services.obs.notifyObservers(null, "update-available", "cant-apply");
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
      return;
    }

    /**
     * From this point on there are two possible outcomes:
     * 1. download and install the update automatically
     * 2. notify the user about the availability of an update
     *
     * Notes:
     * a) if the app.update.auto preference is false then automatic download and
     *    install is disabled and the user will be notified.
     *
     * If the update when it is first read does not have an appVersion attribute
     * the following deprecated behavior will occur:
     * Update Type   Outcome
     * Major         Notify
     * Minor         Auto Install
     */
    let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled();
    if (!updateAuto) {
      LOG(
        "UpdateService:_selectAndInstallUpdate - prompting because silent " +
          "install is disabled. Notifying observers. topic: update-available, " +
          "status: show-prompt"
      );
      AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
      Services.obs.notifyObservers(update, "update-available", "show-prompt");
      return;
    }

    LOG("UpdateService:_selectAndInstallUpdate - download the update");
    let result = await this.#downloadUpdate(update);
    if (
      result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS &&
      !this.isDownloading
    ) {
      LOG(
        "UpdateService:_selectAndInstallUpdate - Failed to start downloading " +
          "update. Cleaning up downloading update."
      );
      await cleanupDownloadingUpdate();
    }
    AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE);
  }

  /**
   * See nsIUpdateService.idl
   */
  get isAppBaseDirWritable() {
    return isAppBaseDirWritable();
  }

  get disabledForTesting() {
    return lazy.UpdateServiceStub.updateDisabledForTesting;
  }

  /**
   * See nsIUpdateService.idl
   */
  get disabled() {
    return lazy.UpdateServiceStub.updateDisabled;
  }

  /**
   * See nsIUpdateService.idl
   */
  get manualUpdateOnly() {
    return (
      Services.policies && !Services.policies.isAllowed("autoAppUpdateChecking")
    );
  }

  /**
   * See nsIUpdateService.idl
   */
  get canUsuallyCheckForUpdates() {
    if (this.disabled) {
      LOG(
        "UpdateService.canUsuallyCheckForUpdates - unable to automatically check " +
          "for updates, the option has been disabled by the administrator."
      );
      return false;
    }

    // If we don't know the binary platform we're updating, we can't update.
    if (!lazy.UpdateUtils.ABI) {
      LOG(
        "UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
          "unknown ABI"
      );
      return false;
    }

    // If we don't know the OS version we're updating, we can't update.
    if (!lazy.UpdateUtils.OSVersion) {
      LOG(
        "UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
          "unknown OS version"
      );
      return false;
    }

    LOG("UpdateService.canUsuallyCheckForUpdates - able to check for updates");
    return true;
  }

  /**
   * See nsIUpdateService.idl
   */
  get canCheckForUpdates() {
    if (!this.canUsuallyCheckForUpdates) {
      return false;
    }

    if (!hasUpdateMutex()) {
      LOG(
        "UpdateService.canCheckForUpdates - unable to check for updates, " +
          "unable to acquire update mutex"
      );
      return false;
    }

    if (isOtherInstanceRunning()) {
      // This doesn't block update checks, but we will have to wait until either
      // the other instance is gone or we time out waiting for it.
      LOG(
        "UpdateService.canCheckForUpdates - another instance is holding the " +
          "lock, will need to wait for it prior to checking for updates"
      );
    }

    LOG("UpdateService.canCheckForUpdates - able to check for updates");
    return true;
  }

  /**
   * See nsIUpdateService.idl
   */
  get elevationRequired() {
    return getElevationRequired();
  }

  /**
   * See nsIUpdateService.idl
   */
  get canUsuallyApplyUpdates() {
    return getCanApplyUpdates();
  }

  /**
   * See nsIUpdateService.idl
   */
  get canApplyUpdates() {
    return (
      this.canUsuallyApplyUpdates &&
      hasUpdateMutex() &&
      !isOtherInstanceRunning()
    );
  }

  /**
   * See nsIUpdateService.idl
   */
  get canUsuallyStageUpdates() {
    return getCanStageUpdates(false);
  }

  /**
   * See nsIUpdateService.idl
   */
  get canStageUpdates() {
    return getCanStageUpdates();
  }

  /**
   * See nsIUpdateService.idl
   */
  get canUsuallyUseBits() {
    return getCanUseBits(false) == "CanUseBits";
  }

  /**
   * See nsIUpdateService.idl
   */
  get canUseBits() {
    return getCanUseBits() == "CanUseBits";
  }

  /**
   * See nsIUpdateService.idl
   */
  get isOtherInstanceHandlingUpdates() {
    return !hasUpdateMutex() || isOtherInstanceRunning();
  }

  /**
   * A set of download listeners to be notified by this._downloader when it
   * receives nsIRequestObserver or nsIProgressEventSink method calls.
   *
   * These are stored on the UpdateService rather than on the Downloader,
   * because they ought to persist across multiple Downloader instances.
   */
  _downloadListeners = new Set();

  /**
   * See nsIUpdateService.idl
   */
  addDownloadListener(listener) {
    let oldSize = this._downloadListeners.size;
    this._downloadListeners.add(listener);

    if (this._downloadListeners.size == oldSize) {
      LOG(
        "UpdateService:addDownloadListener - Warning: Didn't add duplicate " +
          "listener"
      );
      return;
    }

    if (this._downloader) {
      this._downloader.onDownloadListenerAdded();
    }
  }

  /**
   * See nsIUpdateService.idl
   */
  removeDownloadListener(listener) {
    let elementRemoved = this._downloadListeners.delete(listener);

    if (!elementRemoved) {
      LOG(
        "UpdateService:removeDownloadListener - Warning: Didn't remove " +
          "non-existent listener"
      );
      return;
    }

    if (this._downloader) {
      this._downloader.onDownloadListenerRemoved();
    }
  }

  /**
   * Returns a boolean indicating whether there are any download listeners
   */
  get hasDownloadListeners() {
    return !!this._downloadListeners.length;
  }

  /*
   * Calls the provided function once with each download listener that is
   * currently registered.
   */
  forEachDownloadListener(fn) {
    // Make a shallow copy in case listeners remove themselves.
    let listeners = new Set(this._downloadListeners);
    listeners.forEach(fn);
  }

  /**
   * See nsIUpdateService.idl
   */
  async downloadUpdate(update) {
    await this.init();
    return this.#downloadUpdate(update);
  }

  async #downloadUpdate(update) {
    if (!update) {
      throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
    }

    // Don't download the update if the update's version is less than the
    // current application's version or the update's version is the same as the
    // application's version and the build ID is the same as the application's
    // build ID. If we already have an update ready, we want to apply those
    // same checks against the version of the ready update, so that we don't
    // download an update that isn't newer than the one we already have.
    if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
      LOG(
        "UpdateService:downloadUpdate - Skipping download of update since " +
          "it is for an earlier or same application version and build ID.\n" +
          "current application version: " +
          getCompatVersion() +
          "\n" +
          "update application version : " +
          update.appVersion +
          "\n" +
          "current build ID: " +
          Services.appinfo.appBuildID +
          "\n" +
          "update build ID : " +
          update.buildID
      );
      return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
    }
    if (updateIsAtLeastAsOldAsReadyUpdate(update)) {
      LOG(
        "UpdateService:downloadUpdate - not downloading update because the " +
          "update that's already been downloaded is the same version or " +
          "newer.\n" +
          "currently downloaded update application version: " +
          lazy.UM.internal.readyUpdate.appVersion +
          "\n" +
          "available update application version : " +
          update.appVersion +
          "\n" +
          "currently downloaded update build ID: " +
          lazy.UM.internal.readyUpdate.buildID +
          "\n" +
          "available update build ID : " +
          update.buildID
      );
      return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
    }

    // If a download request is in progress vs. a download ready to resume
    if (this.isDownloading) {
      if (update.isCompleteUpdate == this._downloader.isCompleteUpdate) {
        LOG(
          "UpdateService:downloadUpdate - no support for downloading more " +
            "than one update at a time"
        );
        return Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS;
      }
      this._downloader.cancel();
    }
    this._downloader = new Downloader(this);
    return this._downloader.downloadUpdate(update);
  }

  /**
   * See nsIUpdateService.idl
   */
  async stopDownload() {
    await this.init();
    return this.#stopDownload();
  }

  async #stopDownload() {
    if (this.isDownloading) {
      await this._downloader.cancel();
    } else if (this._retryTimer) {
      // Download status is still considered as 'downloading' during retry.
      // We need to cancel both retry and download at this stage.
      this._retryTimer.cancel();
      this._retryTimer = null;
      if (this._downloader) {
        await this._downloader.cancel();
      }
    }
    if (this._downloader) {
      await this._downloader.cleanup();
    }
    this._downloader = null;
  }

  /**
   * Note that this is different from checking if `currentState` is
   * `STATE_DOWNLOADING` because if we are downloading a second update, this
   * will be `true` while `currentState` will be `STATE_PENDING`.
   */
  get isDownloading() {
    return this._downloader && this._downloader.isBusy;
  }

  _logStatus() {
    if (!lazy.UpdateLog.enabled) {
      return;
    }
    if (this.disabled) {
      LOG("Current UpdateService status: disabled");
      // Return early if UpdateService is disabled by policy. Otherwise some of
      // the getters we call to display status information may discover that the
      // update directory is not writable, which automatically results in the
      // permissions being fixed. Which we shouldn't really be doing if update
      // is disabled by policy.
      return;
    }
    LOG("Logging current UpdateService status:");
    // These getters print their own logging
    this.canCheckForUpdates;
    this.canApplyUpdates;
    this.canStageUpdates;
    LOG("Elevation required: " + this.elevationRequired);
    LOG(
      "Other instance of the application currently running: " +
        this.isOtherInstanceHandlingUpdates
    );
    LOG("Current update state: " + this.getStateName(gUpdateState));
    LOG("Downloading: " + !!this.isDownloading);
    if (this._downloader && this._downloader.isBusy) {
      LOG("Downloading complete update: " + this._downloader.isCompleteUpdate);
      LOG("Downloader using BITS: " + this._downloader.usingBits);
      if (this._downloader._patch) {
        // This will print its own logging
        this._downloader._canUseBits(this._downloader._patch);

        // Downloader calls QueryInterface(Ci.nsIWritablePropertyBag) on
        // its _patch member as soon as it is assigned, so no need to do so
        // again here.
        let bitsResult = this._downloader._patch.getProperty("bitsResult");
        if (bitsResult != null) {
          LOG("Patch BITS result: " + bitsResult);
        }
        let internalResult =
          this._downloader._patch.getProperty("internalResult");
        if (internalResult != null) {
          LOG("Patch nsIIncrementalDownload result: " + internalResult);
        }
      }
    }
    LOG("End of UpdateService status");
  }

  /**
   * See nsIUpdateService.idl
   */
  get onlyDownloadUpdatesThisSession() {
    return gOnlyDownloadUpdatesThisSession;
  }

  /**
   * See nsIUpdateService.idl
   */
  set onlyDownloadUpdatesThisSession(newValue) {
    gOnlyDownloadUpdatesThisSession = newValue;
  }

  /**
   * See nsIUpdateService.idl
   */
  getStateName(state) {
    switch (state) {
      case Ci.nsIApplicationUpdateService.STATE_IDLE:
        return "STATE_IDLE";
      case Ci.nsIApplicationUpdateService.STATE_DOWNLOADING:
        return "STATE_DOWNLOADING";
      case Ci.nsIApplicationUpdateService.STATE_STAGING:
        return "STATE_STAGING";
      case Ci.nsIApplicationUpdateService.STATE_PENDING:
        return "STATE_PENDING";
      case Ci.nsIApplicationUpdateService.STATE_SWAP:
        return "STATE_SWAP";
    }
    return `[unknown update state: ${state}]`;
  }

  /**
   * See nsIUpdateService.idl
   */
  get currentState() {
    return gUpdateState;
  }

  /**
   * See nsIUpdateService.idl
   */
  get stateTransition() {
    return gStateTransitionPromise.promise;
  }

  /**
   * Either starts a BITS transfer job or connects to an existing one.
   * When starting a job, it starts it with a name and path that make sense for
   * an update MAR download.
   *
   * @param  parameters
   *         A parameters object must be passed, in which `bitsId` or `url` must
   *         be specified. If `bitsId` is specified (and not null), this will
   *         connect to an existing transfer.
   *           activeListeners
   *             If `true`, this option specifies that there are active
   *             listeners, so the faster "active" progress update polling rate
   *             should be used.
   *           bitsId
   *             The ID of the job to connect to. If this is not passed, a new
   *             transfer will be started.
   *           observer
   *             If specified, should be an instance of `nsIRequestObserver`
   *             and, optionally, `nsIProgressEventSink`. This will be connected
   *             to the `BitsRequest` that is returned.
   *           url
   *             The URL to download.
   *           extraHeaders
   *             String of extra headers to include, in the format accepted by
   *             `IBackgroundCopyJobHttpOptions::SetCustomHeaders`: separated by
   *             `\r\n`, terminated by an additional `\r\n`.
   * @return Promise<BitsRequest>
   *         Returns a request object
   * @throws BitsError
   *         On failure to connect to the BITS job.
   */
  async makeBitsRequest({
    activeListeners = false,
    bitsId,
    observer,
    url,
    extraHeaders,
  }) {
    let noProgressTimeout = BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS;
    let monitorInterval = BITS_IDLE_POLL_RATE_MS;
    // The monitor's timeout should be much greater than the longest monitor
    // poll interval. If the timeout is too short, delay in the pipe to the
    // update agent might cause BITS to falsely report an error, causing an
    // unnecessary fallback to nsIIncrementalDownload.
    let monitorTimeout = Math.max(10 * monitorInterval, 10 * 60 * 1000);
    if (activeListeners) {
      noProgressTimeout = BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS;
      monitorInterval = BITS_ACTIVE_POLL_RATE_MS;
    }

    let updateRootDir = FileUtils.getDir(KEY_UPDROOT, []);
    try {
      updateRootDir.create(
        Ci.nsIFile.DIRECTORY_TYPE,
        FileUtils.PERMS_DIRECTORY
      );
    } catch (ex) {
      if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
        throw ex;
      }
      // Ignore the exception due to a directory that already exists.
    }

    let jobName = "MozillaUpdate " + updateRootDir.leafName;
    let updatePath = getDownloadingUpdateDir().path;
    if (!Bits.initialized) {
      Bits.init(jobName, updatePath, monitorTimeout);
    }

    if (bitsId) {
      LOG(
        "UpdateService:makeBitsRequest - Connecting to in-progress download. " +
          "BITS ID: " +
          bitsId
      );

      return Bits.monitorDownload(bitsId, monitorInterval, observer, null);
    }

    LOG(
      "UpdateService:makeBitsRequest - Starting BITS download with url: " +
        url +
        ", updateDir: " +
        updatePath +
        ", filename: " +
        FILE_UPDATE_MAR
    );

    return Bits.startDownload(
      url,
      FILE_UPDATE_MAR,
      Ci.nsIBits.PROXY_PRECONFIG,
      noProgressTimeout,
      monitorInterval,
      extraHeaders,
      observer,
      null
    );
  }

  /**
   * Get rid of a downloading update. This is generally done before cleaning it
   * up.
   *
   * Connects to the in-progress BITS job in order to cancel it, if necessary.
   * This is generally only necessary when cancelling a job at startup,
   * otherwise we would have connected to it already.
   *
   * If there is not an in-progress download, this has no effect.
   */
  async cancelDownloadingUpdate() {
    if (this.isDownloading) {
      LOG(
        "UpdateService:cancelDownloadingUpdate - Job is connected already. Stopping."
      );
      await this.internal.stopDownload();
      return;
    }

    // If we didn't return above, either we haven't connected to the BITS job
    // yet or there is no BITS job and nothing to cancel.

    if (!lazy.UM.internal.downloadingUpdate) {
      LOG(
        "UpdateService:cancelDownloadingUpdate - Not cleaning up BITS Job. No update."
      );
      return;
    }
    const patch = lazy.UM.internal.downloadingUpdate.selectedPatch;
    if (!patch || !patch.QueryInterface(Ci.nsIWritablePropertyBag)) {
      LOG(
        "UpdateService:cancelDownloadingUpdate - Not cleaning up BITS Job. No patch."
      );
      return;
    }
    const bitsId = patch.getProperty("bitsId");
    if (!bitsId) {
      LOG(
        "UpdateService:cancelDownloadingUpdate - Not cleaning up BITS Job. No BITS ID."
      );
      return;
    }
    // If `!this.isDownloading`, we are not connected to the BITS request, which
    // we need to do to stop a BITS download.
    try {
      const request = await this.makeBitsRequest({ bitsId });
      await request.cancelAsync();
    } catch (ex) {
      LOG(
        `UpdateService:cancelDownloadingUpdate - Failed to clean up BITS Job ${bitsId}: ${ex}`
      );
      return;
    }
    LOG(
      `UpdateService:cancelDownloadingUpdate - Successfully cleaned up BITS Job ${bitsId}`
    );
  }

  classID = UPDATESERVICE_CID;

  QueryInterface = ChromeUtils.generateQI([
    Ci.nsIApplicationUpdateService,
    Ci.nsITimerCallback,
    Ci.nsIObserver,
  ]);
}

export class UpdateManager {
  /**
   * The nsIUpdate object for the update that has been downloaded.
   */
  _readyUpdate = null;

  /**
   * The nsIUpdate object for the update currently being downloaded.
   */
  _downloadingUpdate = null;

  /**
   * Whether the update history stored in _updates has changed since it was
   * loaded.
   */
  _updatesDirty = false;

  /**
   * The backing for `nsIUpdateManager.updateInstalledAtStartup`.
   */
  #updateInstalledAtStartup = null;

  /**
   * A service to manage active and past updates.
   *
   * @class
   */
  constructor() {
    this.internal = {
      reload: async skipFiles => this.#reload(skipFiles),
      getHistory: () => this.#getHistory(),
      addUpdateToHistory: update => this.#addUpdateToHistory(update),
      refreshUpdateStatus: async () => this.#refreshUpdateStatus(),
      QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateManagerInternal]),
    };

    Object.defineProperty(this.internal, "readyUpdate", {
      get: () => this._readyUpdate,
      set: update => {
        this._readyUpdate = update;
      },
    });

    Object.defineProperty(this.internal, "downloadingUpdate", {
      get: () => this._downloadingUpdate,
      set: update => {
        this._downloadingUpdate = update;
      },
    });
  }

  /**
   * See `nsIUpdateManagerInternal.reload` in nsIUpdateService.idl
   */
  async #reload(skipFiles) {
    LOG("UpdateManager:#reload - Reloading update data.");
    if (this._updatesXMLSaver) {
      this._updatesXMLSaver.disarm();
    }

    let updates = [];
    this._updatesDirty = skipFiles;
    this._readyUpdate = null;
    this._downloadingUpdate = null;
    this.#updateInstalledAtStartup = null;
    transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
    if (!skipFiles) {
      // Load the active-update.xml file to see if there is an active update.
      let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML);
      if (activeUpdates.length) {
        const status = readStatusFile(getReadyUpdateDir());

        // If there are two updates, the first one is the ready update.
        // If there is only 1 update, we don't know which is which. We use the
        // state to figure it out.
        if (activeUpdates.length > 1) {
          this._readyUpdate = activeUpdates[0];
          this._downloadingUpdate = activeUpdates[1];
          if (activeUpdates.length > 2) {
            LOG(
              "UpdateManager:#reload - Warning: Ignoring additional (>2) " +
                "unexpected active updates"
            );
          }
        } else if (
          status == STATE_DOWNLOADING ||
          activeUpdates[0].state == STATE_DOWNLOADING
        ) {
          this._downloadingUpdate = activeUpdates[0];
          transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
        } else {
          this._readyUpdate = activeUpdates[0];
        }

        LOG(`UpdateManager:#reload - Got status = ${status}`);
        if (
          [
            STATE_PENDING,
            STATE_PENDING_SERVICE,
            STATE_PENDING_ELEVATE,
            STATE_APPLIED,
            STATE_APPLIED_SERVICE,
          ].includes(status)
        ) {
          transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
        }
        if (status == STATE_SUCCEEDED && this._readyUpdate) {
          this.#updateInstalledAtStartup = this._readyUpdate;
          // Bug 1889785 - When deciding whether or not to show a What's New Page, we
          // rely on both `Services.appinfo.platformVersion` and this value. But Balrog
          // doesn't guarantee that the value that it sends will match appinfo.
          // We synchronize the values here so they are consistent.
          this.#updateInstalledAtStartup.platformVersion =
            Services.appinfo.platformVersion;
          this.saveUpdates();
        }
      }
      updates = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
    }
    this._updatesCache = updates;

    LOG(
      "UpdateManager:#reload - Reloaded downloadingUpdate as " +
        this._downloadingUpdate
    );
    if (this._downloadingUpdate) {
      LOG(
        "UpdateManager:#reload - Reloaded downloadingUpdate state as " +
          this._downloadingUpdate.state
      );
    }
    LOG("UpdateManager:#reload - Reloaded readyUpdate as " + this._readyUpdate);
    if (this._readyUpdate) {
      LOG(
        "UpdateManager:#reload - Reloaded readyUpdate state as " +
          this._readyUpdate.state
      );
    }
    LOG(
      "UpdateManager:UpdateManager - Reloaded updateInstalledAtStartup as  " +
        this.#updateInstalledAtStartup
    );
  }

  /**
   * Loads an updates.xml formatted file into an array of nsIUpdate items.
   *
   * @param   fileName
   *          The file name in the updates directory to load.
   * @return  The array of nsIUpdate items held in the file.
   */
  _loadXMLFileIntoArray(fileName) {
    let updates = [];
    let file = getUpdateFile([fileName]);
    if (!file.exists()) {
      LOG(
        "UpdateManager:_loadXMLFileIntoArray - XML file does not exist. " +
          "path: " +
          file.path
      );
      return updates;
    }

    // Open the active-update.xml file with both read and write access so
    // opening it will fail if it isn't possible to also write to the file. When
    // opening it fails it means that it isn't possible to update and the code
    // below will return early without loading the active-update.xml. This will
    // also make it so notifications to update manually will still be shown.
    let mode =
      fileName == FILE_ACTIVE_UPDATE_XML
        ? FileUtils.MODE_RDWR
        : FileUtils.MODE_RDONLY;
    let fileStream = Cc[
      "@mozilla.org/network/file-input-stream;1"
    ].createInstance(Ci.nsIFileInputStream);
    try {
      fileStream.init(file, mode, FileUtils.PERMS_FILE, 0);
    } catch (e) {
      LOG(
        "UpdateManager:_loadXMLFileIntoArray - error initializing file " +
          "stream. Exception: " +
          e
      );
      return updates;
    }
    try {
      var parser = new DOMParser();
      var doc = parser.parseFromStream(
        fileStream,
        "UTF-8",
        fileStream.available(),
        "text/xml"
      );

      var updateCount = doc.documentElement.childNodes.length;
      for (var i = 0; i < updateCount; ++i) {
        var updateElement = doc.documentElement.childNodes.item(i);
        if (
          updateElement.nodeType != updateElement.ELEMENT_NODE ||
          updateElement.localName != "update"
        ) {
          continue;
        }

        let update;
        try {
          update = new Update(updateElement);
        } catch (e) {
          LOG("UpdateManager:_loadXMLFileIntoArray - invalid update");
          continue;
        }
        updates.push(update);
      }
    } catch (ex) {
      LOG(
        "UpdateManager:_loadXMLFileIntoArray - error constructing update " +
          "list. Exception: " +
          ex
      );
    }
    fileStream.close();
    if (!updates.length) {
      LOG(
        "UpdateManager:_loadXMLFileIntoArray - update xml file " +
          fileName +
          " exists but doesn't contain any updates"
      );
      // The file exists but doesn't contain any updates so remove it.
      try {
        file.remove(false);
      } catch (e) {
        LOG(
          "UpdateManager:_loadXMLFileIntoArray - error removing " +
            fileName +
            " file. Exception: " +
            e
        );
      }
    }
    return updates;
  }

  #getHistory() {
    const history = this._getUpdates();
    return [...history];
  }

  /**
   * Loads the update history from the updates.xml file into a cache.
   */
  _getUpdates() {
    if (!this._updatesCache) {
      this._updatesCache = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
    }
    return this._updatesCache;
  }

  /**
   * See nsIUpdateService.idl
   */
  async getHistory() {
    await lazy.AUS.init();
    return lazy.UM.internal.getHistory();
  }

  /**
   * See nsIUpdateService.idl
   */
  async getReadyUpdate() {
    await lazy.AUS.init();
    return this._readyUpdate;
  }

  /**
   * See nsIUpdateService.idl
   */
  async getDownloadingUpdate() {
    await lazy.AUS.init();
    return this._downloadingUpdate;
  }

  /**
   * See nsIUpdateService.idl
   */
  async updateInstalledAtStartup() {
    await lazy.AUS.init();
    return this.#updateInstalledAtStartup;
  }

  /**
   * See nsIUpdateService.idl
   */
  async lastUpdateInstalled() {
    await lazy.AUS.init();
    if (this.#updateInstalledAtStartup) {
      return this.#updateInstalledAtStartup;
    }
    return this._getUpdates().find(u => u.state == STATE_SUCCEEDED) ?? null;
  }

  #addUpdateToHistory(aUpdate) {
    this._updatesDirty = true;
    let updates = this._getUpdates();
    updates.unshift(aUpdate);
    // Limit the update history to 10 updates.
    updates.splice(10);
  }

  /**
   * See nsIUpdateService.idl
   */
  async addUpdateToHistory(aUpdate) {
    await lazy.AUS.init();
    this.#addUpdateToHistory(aUpdate);
  }

  /**
   * Serializes an array of updates to an XML file or removes the file if the
   * array length is 0.
   *
   * @param   updates
   *          An array of nsIUpdate objects
   * @param   fileName
   *          The file name in the updates directory to write to.
   * @return  true on success, false on error
   */
  async _writeUpdatesToXMLFile(updates, fileName) {
    let file;
    try {
      file = getUpdateFile([fileName]);
    } catch (e) {
      LOG(
        "UpdateManager:_writeUpdatesToXMLFile - Unable to get XML file - " +
          "Exception: " +
          e
      );
      return false;
    }
    if (!updates.length) {
      LOG(
        "UpdateManager:_writeUpdatesToXMLFile - no updates to write. " +
          "removing file: " +
          file.path
      );
      try {
        await IOUtils.remove(file.path);
      } catch (e) {
        LOG(
          "UpdateManager:_writeUpdatesToXMLFile - Delete file exception: " + e
        );
        return false;
      }
      return true;
    }

    const EMPTY_UPDATES_DOCUMENT_OPEN =
      '<?xml version="1.0"?><updates xmlns="' + URI_UPDATE_NS + '">';
    const EMPTY_UPDATES_DOCUMENT_CLOSE = "</updates>";
    try {
      var parser = new DOMParser();
      var doc = parser.parseFromString(
        EMPTY_UPDATES_DOCUMENT_OPEN + EMPTY_UPDATES_DOCUMENT_CLOSE,
        "text/xml"
      );

      for (var i = 0; i < updates.length; ++i) {
        doc.documentElement.appendChild(updates[i].serialize(doc));
      }

      var xml =
        EMPTY_UPDATES_DOCUMENT_OPEN +
        doc.documentElement.innerHTML +
        EMPTY_UPDATES_DOCUMENT_CLOSE;
      // If the destination file existed and is removed while the following is
      // being performed the copy of the tmp file to the destination file will
      // fail.
      await IOUtils.writeUTF8(file.path, xml, {
        tmpPath: file.path + ".tmp",
      });
      await IOUtils.setPermissions(file.path, FileUtils.PERMS_FILE);
    } catch (e) {
      LOG("UpdateManager:_writeUpdatesToXMLFile - Exception: " + e);
      return false;
    }
    return true;
  }

  _updatesXMLSaver = null;
  _updatesXMLSaverCallback = null;
  /**
   * See nsIUpdateService.idl
   */
  saveUpdates() {
    if (!this._updatesXMLSaver) {
      this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize();

      this._updatesXMLSaver = new lazy.DeferredTask(
        () => this._saveUpdatesXML(),
        XML_SAVER_INTERVAL_MS
      );
      lazy.AsyncShutdown.profileBeforeChange.addBlocker(
        "UpdateManager: writing update xml data",
        this._updatesXMLSaverCallback
      );
    } else {
      this._updatesXMLSaver.disarm();
    }

    this._updatesXMLSaver.arm();
  }

  /**
   * Saves the active-updates.xml and updates.xml when the updates history has
   * been modified files.
   */
  _saveUpdatesXML() {
    // This mechanism for how we store the updates might seem a bit odd, since,
    // if only one update is stored, we don't know if it's the ready update or
    // the downloading update. However, we can determine which it is by reading
    // update.status. If we read STATE_DOWNLOADING, it must be a downloading
    // update and otherwise it's a ready update. This method has the additional
    // advantage of requiring no migration from when we used to only store a
    // single active update.
    let updates = [];
    if (this._readyUpdate) {
      updates.push(this._readyUpdate);
    }
    if (this._downloadingUpdate) {
      updates.push(this._downloadingUpdate);
    }

    // The active update stored in the active-update.xml file will change during
    // the lifetime of an active update and the file should always be updated
    // when saveUpdates is called.
    let promises = [];
    promises[0] = this._writeUpdatesToXMLFile(updates, FILE_ACTIVE_UPDATE_XML);
    // The update history stored in the updates.xml file should only need to be
    // updated when an active update has been added to it in which case
    // |_updatesDirty| will be true.
    if (this._updatesDirty) {
      this._updatesDirty = false;
      promises[1] = this._writeUpdatesToXMLFile(
        this._getUpdates(),
        FILE_UPDATES_XML
      );
    }
    return Promise.all(promises);
  }

  /**
   * See nsIUpdateService.idl
   */
  async refreshUpdateStatus() {
    return this.#refreshUpdateStatus();
  }

  async #refreshUpdateStatus() {
    try {
      LOG("UpdateManager:refreshUpdateStatus - Staging done.");

      await lazy.AUS.init();

      var update = this._readyUpdate;
      if (!update) {
        LOG("UpdateManager:refreshUpdateStatus - Missing ready update?");
        return;
      }

      var status = readStatusFile(getReadyUpdateDir());
      pingStateAndStatusCodes(update, false, status);
      LOG(`UpdateManager:refreshUpdateStatus - status = ${status}`);

      let parts = status.split(":");
      update.state = parts[0];
      if (update.state == STATE_APPLYING) {
        LOG(
          "UpdateManager:refreshUpdateStatus - Staging appears to have crashed."
        );
        update.state = STATE_FAILED;
        update.errorCode = ERR_UPDATER_CRASHED;
      } else if (update.state == STATE_FAILED) {
        LOG("UpdateManager:refreshUpdateStatus - Staging failed.");
        if (parts[1]) {
          update.errorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
        } else {
          update.errorCode = INVALID_UPDATER_STATUS_CODE;
        }
      }

      // Rotate the update logs so the update log isn't removed if a complete
      // update is downloaded. By passing false the patch directory won't be
      // removed.
      cleanUpReadyUpdateDir(false);

      if (update.state == STATE_FAILED) {
        let isMemError = isMemoryAllocationErrorCode(update.errorCode);
        if (
          update.errorCode == DELETE_ERROR_STAGING_LOCK_FILE ||
          update.errorCode == UNEXPECTED_STAGING_ERROR ||
          isMemError
        ) {
          update.state = getBestPendingState();
          writeStatusFile(getReadyUpdateDir(), update.state);
          if (isMemError) {
            LOG(
              `UpdateManager:refreshUpdateStatus - Updater failed to ` +
                `allocate enough memory to successfully stage. Setting ` +
                `status to "${update.state}"`
            );
          } else {
            LOG(
              `UpdateManager:refreshUpdateStatus - Unexpected staging error. ` +
                `Setting status to "${update.state}"`
            );
          }
        } else if (isServiceSpecificErrorCode(update.errorCode)) {
          // Sometimes when staging, we might encounter an error that is
          // specific to the Maintenance Service. If this happens, we should try
          // to update without the Service.
          LOG(
            `UpdateManager:refreshUpdateStatus - Encountered service ` +
              `specific error code: ${update.errorCode}. Will try installing ` +
              `update without the Maintenance Service. Setting state to pending`
          );
          update.state = STATE_PENDING;
          writeStatusFile(getReadyUpdateDir(), update.state);
        } else {
          LOG(
            "UpdateManager:refreshUpdateStatus - Attempting handleUpdateFailure"
          );
          if (!handleUpdateFailure(update)) {
            LOG(
              "UpdateManager:refreshUpdateStatus - handleUpdateFailure " +
                "failed. Attempting to fall back to complete update."
            );
            await handleFallbackToCompleteUpdate();
          }
        }
      }
      if (update.state == STATE_APPLIED && shouldUseService()) {
        LOG(
          `UpdateManager:refreshUpdateStatus - Staging successful. ` +
            `Setting status to "${STATE_APPLIED_SERVICE}"`
        );
        writeStatusFile(
          getReadyUpdateDir(),
          (update.state = STATE_APPLIED_SERVICE)
        );
      }

      // Now that the active update's properties have been updated write the
      // active-update.xml to disk. Since there have been no changes to the
      // update history the updates.xml will not be written to disk.
      this.saveUpdates();

      // Send an observer notification which the app update doorhanger uses to
      // display a restart notification after any langpacks have staged.
      await promiseLangPacksUpdated(update);

      if (
        update.state == STATE_APPLIED ||
        update.state == STATE_APPLIED_SERVICE ||
        update.state == STATE_PENDING ||
        update.state == STATE_PENDING_SERVICE ||
        update.state == STATE_PENDING_ELEVATE
      ) {
        LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_PENDING");
        transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
      }

      LOG(
        "UpdateManager:refreshUpdateStatus - Notifying observers that " +
          "the update was staged. topic: update-staged, status: " +
          update.state
      );
      Services.obs.notifyObservers(update, "update-staged", update.state);
    } finally {
      // This function being called is the one thing that tells us that staging
      // is done so be very sure that we don't exit it leaving the current
      // state at STATE_STAGING.
      // The only cases where we haven't already done a state transition are
      // error cases, so if another state isn't set, assume that we hit an error
      // and aborted the update.
      if (
        lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING
      ) {
        LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_IDLE");
        transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
      }
    }
  }

  /**
   * See nsIUpdateService.idl
   */
  async elevationOptedIn() {
    // The user has been been made aware that the update requires elevation.
    await lazy.AUS.init();
    let update = this._readyUpdate;
    if (!update) {
      return;
    }
    let status = readStatusFile(getReadyUpdateDir());
    let parts = status.split(":");
    update.state = parts[0];
    if (update.state == STATE_PENDING_ELEVATE) {
      LOG("UpdateManager:elevationOptedIn - Setting state to pending.");
      // Proceed with the pending update.
      // Note: STATE_PENDING_ELEVATE stands for "pending user's approval to
      // proceed with an elevated update". As long as we see this state, we will
      // notify the user of the availability of an update that requires
      // elevation. |elevationOptedIn| (this function) is called when the user
      // gives us approval to proceed, so we want to switch to STATE_PENDING.
      // The updater then detects whether or not elevation is required and
      // displays the elevation prompt if necessary. This last step does not
      // depend on the state in the status file.
      writeStatusFile(getReadyUpdateDir(), STATE_PENDING);
    } else {
      LOG("UpdateManager:elevationOptedIn - Not in pending-elevate state.");
    }
  }

  /**
   * See nsIUpdateService.idl
   */
  async cleanupDownloadingUpdate() {
    LOG(
      "UpdateManager:cleanupDownloadingUpdate - cleaning up downloading update."
    );
    await lazy.AUS.init();
    await cleanupDownloadingUpdate();
  }

  /**
   * See nsIUpdateService.idl
   */
  async cleanupReadyUpdate() {
    LOG("UpdateManager:cleanupReadyUpdate - cleaning up ready update.");
    await lazy.AUS.init();
    cleanupReadyUpdate();
  }

  /**
   * See nsIUpdateService.idl
   */
  async cleanupActiveUpdates() {
    LOG("UpdateManager:cleanupActiveUpdates - cleaning up active updates.");
    await lazy.AUS.init();
    await cleanupActiveUpdates();
  }

  /**
   * See nsIUpdateService.idl
   */
  async doInstallCleanup() {
    LOG("UpdateManager:doInstallCleanup - cleaning up");
    await lazy.AUS.init();

    let completionPromises = [];

    const delete_or_log = path =>
      IOUtils.remove(path).catch(ex =>
        console.error(`Failed to delete ${path}`, ex)
      );

    for (const key of [KEY_OLD_UPDROOT, KEY_UPDROOT]) {
      const root = Services.dirsvc.get(key, Ci.nsIFile);

      const activeUpdateXml = root.clone();
      activeUpdateXml.append(FILE_ACTIVE_UPDATE_XML);
      completionPromises.push(delete_or_log(activeUpdateXml.path));

      const downloadingMar = root.clone();
      downloadingMar.append(DIR_UPDATES);
      downloadingMar.append(DIR_UPDATE_DOWNLOADING);
      downloadingMar.append(FILE_UPDATE_MAR);
      completionPromises.push(delete_or_log(downloadingMar.path));

      const readyDir = root.clone();
      readyDir.append(DIR_UPDATES);
      readyDir.append(DIR_UPDATE_READY);
      const readyMar = readyDir.clone();
      readyMar.append(FILE_UPDATE_MAR);
      completionPromises.push(delete_or_log(readyMar.path));
      const readyStatus = readyDir.clone();
      readyStatus.append(FILE_UPDATE_STATUS);
      completionPromises.push(delete_or_log(readyStatus.path));
      const versionFile = readyDir.clone();
      versionFile.append(FILE_UPDATE_VERSION);
      completionPromises.push(delete_or_log(versionFile.path));
    }

    return Promise.allSettled(completionPromises);
  }

  /**
   * See nsIUpdateService.idl
   */
  async doUninstallCleanup() {
    LOG("UpdateManager:doUninstallCleanup - cleaning up.");
    await lazy.AUS.init();
    let completionPromises = [];

    completionPromises.push(
      IOUtils.remove(Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile).path, {
        recursive: true,
      }).catch(ex => console.error("Failed to remove update directory", ex))
    );
    completionPromises.push(
      IOUtils.remove(Services.dirsvc.get(KEY_OLD_UPDROOT, Ci.nsIFile).path, {
        recursive: true,
      }).catch(ex => console.error("Failed to remove old update directory", ex))
    );

    return Promise.allSettled(completionPromises);
  }

  classID = Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}");
  QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateManager]);
}

/**
 * CheckerService
 * Provides an interface for checking for new updates. When more checks are
 * made while an equivalent check is already in-progress, they will be coalesced
 * into a single update check request.
 */
export class CheckerService {
  #nextUpdateCheckId = 1;

  // Most of the update checking data is looked up via a "request key". This
  // allows us to lookup the request key for a particular check id, since
  // multiple checks can correspond to a single request.
  // When a check is cancelled or completed, it will be removed from this
  // object.
  #requestKeyByCheckId = {};

  // This object will relate request keys to update check data objects. The
  // format of the update check data objects is defined by
  // #makeUpdateCheckDataObject, below.
  // When an update request is cancelled (by all of the corresponding update
  // checks being cancelled) or completed, its key will be removed from this
  // object.
  #updateCheckData = {};

  constructor() {
    this.internal = {
      checkForUpdates: checkType => this.#checkForUpdates(checkType, true),
      QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckerInternal]),
    };
  }

  #makeUpdateCheckDataObject(type, promise) {
    return { type, promise, request: null };
  }

  /**
   * Indicates whether the passed parameter is one of the valid enumerated
   * values that indicates a type of update check.
   */
  #validUpdateCheckType(checkType) {
    return [
      Ci.nsIUpdateChecker.BACKGROUND_CHECK,
      Ci.nsIUpdateChecker.FOREGROUND_CHECK,
    ].includes(checkType);
  }

  #getCanMigrate() {
    if (AppConstants.platform != "win" || AppConstants.BASE_BROWSER_UPDATE) {
      return false;
    }

    // The first element of the array is whether the build target is 32 or 64
    // bit and the third element of the array is whether the client's Windows OS
    // system processor is 32 or 64 bit.
    let aryABI = lazy.UpdateUtils.ABI.split("-");
    if (aryABI[0] != "x86" || aryABI[2] != "x64") {
      return false;
    }

    let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
      Ci.nsIWindowsRegKey
    );

    let regPath =
      "SOFTWARE\\Mozilla\\" + Services.appinfo.name + "\\32to64DidMigrate";
    let regValHKCU = lazy.WindowsRegistry.readRegKey(
      wrk.ROOT_KEY_CURRENT_USER,
      regPath,
      "Never",
      wrk.WOW64_32
    );
    let regValHKLM = lazy.WindowsRegistry.readRegKey(
      wrk.ROOT_KEY_LOCAL_MACHINE,
      regPath,
      "Never",
      wrk.WOW64_32
    );
    // The Never registry key value allows configuring a system to never migrate
    // any of the installations.
    if (regValHKCU === 1 || regValHKLM === 1) {
      LOG(
        "CheckerService:#getCanMigrate - all installations should not be " +
          "migrated"
      );
      return false;
    }

    let appBaseDirPath = getAppBaseDir().path;
    regValHKCU = lazy.WindowsRegistry.readRegKey(
      wrk.ROOT_KEY_CURRENT_USER,
      regPath,
      appBaseDirPath,
      wrk.WOW64_32
    );
    regValHKLM = lazy.WindowsRegistry.readRegKey(
      wrk.ROOT_KEY_LOCAL_MACHINE,
      regPath,
      appBaseDirPath,
      wrk.WOW64_32
    );
    // When the registry value is 1 for the installation directory path value
    // name then the installation has already been migrated once or the system
    // was configured to not migrate that installation.
    if (regValHKCU === 1 || regValHKLM === 1) {
      LOG(
        "CheckerService:#getCanMigrate - this installation should not be " +
          "migrated"
      );
      return false;
    }

    // When the registry value is 0 for the installation directory path value
    // name then the installation has updated to Firefox 56 and can be migrated.
    if (regValHKCU === 0 || regValHKLM === 0) {
      LOG("CheckerService:#getCanMigrate - this installation can be migrated");
      return true;
    }

    LOG(
      "CheckerService:#getCanMigrate - no registry entries for this " +
        "installation"
    );
    return false;
  }

  /**
   * See nsIUpdateService.idl
   */
  async getUpdateURL(checkType) {
    LOG("CheckerService:getUpdateURL - checkType: " + checkType);
    if (!this.#validUpdateCheckType(checkType)) {
      LOG("CheckerService:getUpdateURL - Invalid checkType");
      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    }

    let url = Services.appinfo.updateURL;
    let updatePin;

    if (Services.policies) {
      let policies = Services.policies.getActivePolicies();
      if (policies) {
        if ("AppUpdateURL" in policies) {
          url = policies.AppUpdateURL.toString();
        }
        if ("AppUpdatePin" in policies) {
          updatePin = policies.AppUpdatePin;

          // Scalar ID: update.version_pin
          AUSTLMY.pingPinPolicy(updatePin);
        }
      }
    }

    if (!url) {
      LOG("CheckerService:getUpdateURL - update URL not defined");
      return null;
    }

    url = await lazy.UpdateUtils.formatUpdateURL(url);

    if (checkType == Ci.nsIUpdateChecker.FOREGROUND_CHECK) {
      url += (url.includes("?") ? "&" : "?") + "force=1";
    }

    if (this.#getCanMigrate()) {
      url += (url.includes("?") ? "&" : "?") + "mig64=1";
    }

    if (updatePin) {
      url +=
        (url.includes("?") ? "&" : "?") +
        "pin=" +
        encodeURIComponent(updatePin);
    }

    LOG("CheckerService:getUpdateURL - update URL: " + url);
    return url;
  }

  /**
   * See nsIUpdateService.idl
   */
  checkForUpdates(checkType) {
    return this.#checkForUpdates(checkType, false);
  }

  #checkForUpdates(checkType, internal) {
    // Note that we should run update initialization if it was invoked
    // externally (i.e. `internal == false`). But initialization is async and
    // the asynchronous work of this function happens in `this.#updateCheck`, so
    // we will delay initialization until then.

    LOG("CheckerService:checkForUpdates - checkType: " + checkType);
    if (!this.#validUpdateCheckType(checkType)) {
      LOG("CheckerService:checkForUpdates - Invalid checkType");
      throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
    }

    let checkId = this.#nextUpdateCheckId;
    this.#nextUpdateCheckId += 1;

    // `checkType == FOREGROUND_CHECK` can override `canCheckForUpdates`. But
    // nothing should override enterprise policies.
    if (lazy.AUS.disabled) {
      LOG("CheckerService:checkForUpdates - disabled by policy");
      return this.#getChecksNotAllowedObject(checkId);
    }
    if (
      checkType == Ci.nsIUpdateChecker.BACKGROUND_CHECK &&
      !lazy.AUS.canCheckForUpdates
    ) {
      LOG("CheckerService:checkForUpdates - !canCheckForUpdates");
      return this.#getChecksNotAllowedObject(checkId);
    }

    // We want to combine simultaneous requests, but only ones that are
    // equivalent. If, say, one of them uses the force parameter and one
    // doesn't, we want those two requests to remain separate. This key will
    // allow us to map equivalent requests together. It is also the key that we
    // use to lookup the update check data in this.#updateCheckData.
    let requestKey = checkType;

    if (requestKey in this.#updateCheckData) {
      LOG(
        `CheckerService:checkForUpdates - Connecting check id ${checkId} to ` +
          `existing check request.`
      );
    } else {
      LOG(
        `CheckerService:checkForUpdates - Making new check request for check ` +
          `id ${checkId}.`
      );
      this.#updateCheckData[requestKey] = this.#makeUpdateCheckDataObject(
        checkType,
        this.#updateCheck(checkType, requestKey, internal)
      );
    }

    this.#requestKeyByCheckId[checkId] = requestKey;

    return {
      id: checkId,
      result: this.#updateCheckData[requestKey].promise,
      QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]),
    };
  }

  #getChecksNotAllowedObject(checkId) {
    return {
      id: checkId,
      result: Promise.resolve(
        Object.freeze({
          checksAllowed: false,
          succeeded: false,
          request: null,
          updates: [],
          QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
        })
      ),
      QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]),
    };
  }

  async #updateCheck(checkType, requestKey, internal) {
    if (!internal) {
      await lazy.AUS.init();
    }

    await waitForOtherInstances();

    let url;
    try {
      url = await this.getUpdateURL(checkType);
    } catch (ex) {}

    if (!url) {
      LOG("CheckerService:#updateCheck - !url");
      return this.#getCheckFailedObject("update_url_not_available");
    }

    let request = new XMLHttpRequest();
    request.open("GET", url, true);
    // Prevent the request from reading from the cache.
    request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
    // Prevent the request from writing to the cache.
    request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
    // Disable cutting edge features, like TLS 1.3, where middleboxes might
    // brick us
    request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative =
      true;

    request.overrideMimeType("text/xml");
    // The Cache-Control header is only interpreted by proxies and the
    // final destination. It does not help if a resource is already
    // cached locally.
    request.setRequestHeader("Cache-Control", "no-cache");
    // HTTP/1.0 servers might not implement Cache-Control and
    // might only implement Pragma: no-cache
    request.setRequestHeader("Pragma", "no-cache");

    const UPDATE_CHECK_LOAD_SUCCESS = 1;
    const UPDATE_CHECK_LOAD_ERROR = 2;
    const UPDATE_CHECK_CANCELLED = 3;

    let result = await new Promise(resolve => {
      // It's important that nothing potentially asynchronous happens between
      // checking if the request has been cancelled and starting the request.
      // If an update check cancellation happens before dispatching the request
      // and we end up dispatching it anyways, we will never call cancel on the
      // request later and the cancellation effectively won't happen.
      if (!(requestKey in this.#updateCheckData)) {
        LOG(
          "CheckerService:#updateCheck - check was cancelled before request " +
            "was able to start"
        );
        resolve(UPDATE_CHECK_CANCELLED);
        return;
      }

      let onLoad = _event => {
        request.removeEventListener("load", onLoad);
        LOG("CheckerService:#updateCheck - request got 'load' event");
        resolve(UPDATE_CHECK_LOAD_SUCCESS);
      };
      request.addEventListener("load", onLoad);
      let onError = _event => {
        request.removeEventListener("error", onLoad);
        LOG("CheckerService:#updateCheck - request got 'error' event");
        resolve(UPDATE_CHECK_LOAD_ERROR);
      };
      request.addEventListener("error", onError);

      LOG("CheckerService:#updateCheck - sending request to: " + url);
      request.send(null);
      this.#updateCheckData[requestKey].request = request;
    });

    // Remove all entries for this request key. This marks the request and the
    // associated check ids as no longer in-progress.
    delete this.#updateCheckData[requestKey];
    for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
      if (this.#requestKeyByCheckId[checkId] == requestKey) {
        delete this.#requestKeyByCheckId[checkId];
      }
    }

    if (result == UPDATE_CHECK_CANCELLED) {
      return this.#getCheckFailedObject(Cr.NS_BINDING_ABORTED);
    }

    if (result == UPDATE_CHECK_LOAD_ERROR) {
      let status = this.#getChannelStatus(request);
      LOG("CheckerService:#updateCheck - Failed. request.status: " + status);

      // Set MitM pref.
      try {
        let secInfo = request.channel.securityInfo;
        if (
          secInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
          secInfo.serverCert &&
          secInfo.serverCert.issuerName
        ) {
          Services.prefs.setStringPref(
            "security.pki.mitm_canary_issuer",
            secInfo.serverCert.issuerName
          );
        }
      } catch (e) {
        LOG("CheckerService:#updateCheck - Getting secInfo failed.");
      }

      return this.#getCheckFailedObject(status, 404, request);
    }

    LOG("CheckerService:#updateCheck - request completed downloading document");
    Services.prefs.clearUserPref("security.pki.mitm_canary_issuer");
    // Check whether there is a mitm, i.e. check whether the root cert is
    // built-in or not.
    try {
      let sslStatus = request.channel.securityInfo;
      if (sslStatus) {
        Services.prefs.setBoolPref(
          "security.pki.mitm_detected",
          !sslStatus.isBuiltCertChainRootBuiltInRoot
        );
      }
    } catch (e) {
      LOG("CheckerService:#updateCheck - Getting sslStatus failed.");
    }

    let updates;
    try {
      // Analyze the resulting DOM and determine the set of updates.
      updates = this.#parseUpdates(request);
    } catch (e) {
      LOG(
        "CheckerService:#updateCheck - there was a problem checking for " +
          "updates. Exception: " +
          e
      );
      let status = this.#getChannelStatus(request);
      // If we can't find an error string specific to this status code,
      // just use the 200 message from above, which means everything
      // "looks" fine but there was probably an XML error or a bogus file.
      return this.#getCheckFailedObject(status, 200, request);
    }

    LOG(
      "CheckerService:#updateCheck - number of updates available: " +
        updates.length
    );

    if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
      Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
    }

    return Object.freeze({
      checksAllowed: true,
      succeeded: true,
      request,
      updates,
      QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
    });
  }

  /**
   * @param   errorCode
   *          The error code to include in the return value. If possible, we
   *          will get the update status text based on this error code.
   * @param   defaultCode
   *          Optional. The error code to use to get the status text if there
   *          isn't status text available for `errorCode`.
   * @param   request
   *          The XMLHttpRequest used to check for updates. Or null, if one was
   *          never constructed.
   * @returns An nsIUpdateCheckResult object indicating an error, using the
   *          error data passed to this function.
   */
  #getCheckFailedObject(
    errorCode,
    defaultCode = Cr.NS_BINDING_FAILED,
    request = null
  ) {
    let update = new Update(null);
    update.errorCode = errorCode;
    update.statusText = getStatusTextFromCode(errorCode, defaultCode);

    if (errorCode == Cr.NS_ERROR_OFFLINE) {
      // We use a separate constant here because nsIUpdate.errorCode is signed
      update.errorCode = NETWORK_ERROR_OFFLINE;
    } else if (this.#isHttpStatusCode(errorCode)) {
      update.errorCode = HTTP_ERROR_OFFSET + errorCode;
    }

    return Object.freeze({
      checksAllowed: true,
      succeeded: false,
      request,
      updates: [update],
      QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
    });
  }

  /**
   * Returns the status code for the XMLHttpRequest
   */
  #getChannelStatus(request) {
    var status = 0;
    try {
      status = request.status;
    } catch (e) {}

    if (status == 0) {
      status = request.channel.QueryInterface(Ci.nsIRequest).status;
    }
    return status;
  }

  #isHttpStatusCode(status) {
    return status >= 100 && status <= 599;
  }

  /**
   * @param   request
   *          The XMLHttpRequest that successfully loaded the update XML.
   * @returns An array of 0 or more nsIUpdate objects describing the available
   *          updates.
   * @throws  If the XML document element node name is not updates.
   */
  #parseUpdates(request) {
    let updatesElement = request.responseXML.documentElement;
    if (!updatesElement) {
      LOG("CheckerService:#parseUpdates - empty updates document?!");
      return [];
    }

    if (updatesElement.nodeName != "updates") {
      LOG("CheckerService:#parseUpdates - unexpected node name!");
      throw new Error(
        "Unexpected node name, expected: updates, got: " +
          updatesElement.nodeName
      );
    }

    let updates = [];
    for (const updateElement of updatesElement.childNodes) {
      if (
        updateElement.nodeType != updateElement.ELEMENT_NODE ||
        updateElement.localName != "update"
      ) {
        continue;
      }

      let update;
      try {
        update = new Update(updateElement);
      } catch (e) {
        LOG("CheckerService:#parseUpdates - invalid <update/>, ignoring...");
        continue;
      }
      update.serviceURL = request.responseURL;
      update.channel = lazy.UpdateUtils.UpdateChannel;
      updates.push(update);
    }

    return updates;
  }

  /**
   * See nsIUpdateService.idl
   */
  stopCheck(checkId) {
    if (!(checkId in this.#requestKeyByCheckId)) {
      LOG(`CheckerService:stopCheck - Non-existent check id ${checkId}`);
      return;
    }
    LOG(`CheckerService:stopCheck - Cancelling check id ${checkId}`);
    let requestKey = this.#requestKeyByCheckId[checkId];
    delete this.#requestKeyByCheckId[checkId];
    if (Object.values(this.#requestKeyByCheckId).includes(requestKey)) {
      LOG(
        `CheckerService:stopCheck - Not actually cancelling request because ` +
          `other check id's depend on it.`
      );
    } else {
      LOG(
        `CheckerService:stopCheck - This is the last check using this ` +
          `request. Cancelling the request now.`
      );
      let request = this.#updateCheckData[requestKey].request;
      delete this.#updateCheckData[requestKey];
      if (request) {
        LOG(`CheckerService:stopCheck - Aborting XMLHttpRequest`);
        request.abort();
      } else {
        LOG(
          `CheckerService:stopCheck - Not aborting XMLHttpRequest. It ` +
            `doesn't appear to have started yet.`
        );
      }
    }
  }

  /**
   * See nsIUpdateService.idl
   */
  stopAllChecks() {
    LOG("CheckerService:stopAllChecks - stopping all checks.");
    for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
      this.stopCheck(checkId);
    }
  }

  classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}");
  QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateChecker]);
}

class Downloader {
  /**
   * The nsIUpdatePatch that we are downloading
   */
  _patch = null;

  /**
   * The nsIUpdate that we are downloading
   */
  _update = null;

  /**
   * The nsIRequest object handling the download.
   */
  _request = null;

  /**
   * Whether or not the update being downloaded is a complete replacement of
   * the user's existing installation or a patch representing the difference
   * between the new version and the previous version.
   */
  isCompleteUpdate = null;

  /**
   * We get the nsIRequest from nsIBITS asynchronously. When downloadUpdate has
   * been called, but this._request is not yet valid, _pendingRequest will be
   * a promise that will resolve when this._request has been set.
   */
  _pendingRequest = null;

  /**
   * When using BITS, cancel actions happen asynchronously. This variable
   * keeps track of any cancel action that is in-progress.
   * If the cancel action fails, this will be set back to null so that the
   * action can be attempted again. But if the cancel action succeeds, the
   * resolved promise will remain stored in this variable to prevent cancel
   * from being called twice (which, for BITS, is an error).
   */
  _cancelPromise = null;

  /**
   * BITS receives progress notifications slowly, unless a user is watching.
   * This tracks what frequency notifications are happening at.
   *
   * This is needed because BITS downloads are started asynchronously.
   * Specifically, this is needed to prevent a situation where the download is
   * still starting (Downloader._pendingRequest has not resolved) when the first
   * observer registers itself. Without this variable, there is no way of
   * knowing whether the download was started as Active or Idle and, therefore,
   * we don't know if we need to start Active mode when _pendingRequest
   * resolves.
   */
  _bitsActiveNotifications = false;

  /**
   * This is a function that when called will stop the update process from
   * waiting for language pack updates. This is for safety to ensure that a
   * problem in the add-ons manager doesn't delay updates by much.
   */
  _langPackTimeout = null;

  /**
   * If gOnlyDownloadUpdatesThisSession is true, we prevent the update process
   * from progressing past the downloading stage. If the download finishes,
   * pretend that it hasn't in order to keep the current update in the
   * "downloading" state.
   */
  _pretendingDownloadIsNotDone = false;

  /**
   * Manages the download of updates
   *
   * @param   background
   *          Whether or not this downloader is operating in background
   *          update mode.
   * @param   updateService
   *          The update service that created this downloader.
   * @class
   */
  constructor(updateService) {
    LOG("Creating Downloader");
    this.updateService = updateService;
  }

  /**
   * Cancels the active download.
   *
   * For a BITS download, this will cancel and remove the download job. For
   * an nsIIncrementalDownload, this will stop the download, but leaves the
   * data around to allow the transfer to be resumed later.
   */
  async cancel(cancelError) {
    LOG("Downloader: cancel");
    if (cancelError === undefined) {
      cancelError = Cr.NS_BINDING_ABORTED;
    }
    if (this.usingBits) {
      // If a cancel action is already in progress, just return when that
      // promise resolved. Trying to cancel the same request twice is an error.
      if (this._cancelPromise) {
        await this._cancelPromise;
        return;
      }

      if (this._pendingRequest) {
        await this._pendingRequest;
      }
      if (this._patch.getProperty("bitsId") != null) {
        // Make sure that we don't try to resume this download after it was
        // cancelled.
        this._patch.deleteProperty("bitsId");
      }
      try {
        this._cancelPromise = this._request.cancelAsync(cancelError);
        await this._cancelPromise;
      } catch (e) {
        // On success, we will not set the cancel promise to null because
        // we want to prevent two cancellations of the same request. But
        // retrying after a failed cancel is not an error, so we will set the
        // cancel promise to null in the failure case.
        this._cancelPromise = null;
        throw e;
      }
    } else if (this._request && this._request instanceof Ci.nsIRequest) {
      // Normally, cancelling an nsIIncrementalDownload results in it stopping
      // the download but leaving the downloaded data so that we can resume the
      // download later. If we've already finished the download, there is no
      // transfer to stop.
      // Note that this differs from the BITS case. Cancelling a BITS job, even
      // when the transfer has completed, results in all data being deleted.
      // Therefore, even if the transfer has completed, cancelling a BITS job
      // has effects that we must not skip.
      if (this._pretendingDownloadIsNotDone) {
        LOG(
          "Downloader: cancel - Ignoring cancel request of finished download"
        );
      } else {
        this._request.cancel(cancelError);
      }
    }
  }

  /**
   * Verify the downloaded file.  We assume that the download is complete at
   * this point.
   */
  _verifyDownload() {
    LOG("Downloader:_verifyDownload called");
    if (!this._request) {
      AUSTLMY.pingDownloadCode(
        this.isCompleteUpdate,
        AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST
      );
      return false;
    }

    let destination = getDownloadingUpdateDir();
    destination.append(FILE_UPDATE_MAR);

    // Ensure that the file size matches the expected file size.
    if (destination.fileSize != this._patch.size) {
      LOG("Downloader:_verifyDownload downloaded size != expected size.");
      AUSTLMY.pingDownloadCode(
        this.isCompleteUpdate,
        AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL
      );
      return false;
    }

    LOG("Downloader:_verifyDownload downloaded size == expected size.");
    return true;
  }

  /**
   * Select the patch to use given the current state of updateDir and the given
   * set of update patches.
   *
   * @param   update
   *          A nsIUpdate object to select a patch from
   * @return  A nsIUpdatePatch object to download
   */
  async _selectPatch(update) {
    // Given an update to download, we will always try to download the patch
    // for a partial update over the patch for a full update.

    // Look to see if any of the patches in the Update object has been
    // pre-selected for download, otherwise we must figure out which one
    // to select ourselves.
    var selectedPatch = update.selectedPatch;

    var state = selectedPatch ? selectedPatch.state : STATE_NONE;

    // If this is a patch that we know about, then select it.  If it is a patch
    // that we do not know about, then remove it and use our default logic.
    var useComplete = false;
    if (selectedPatch) {
      LOG(
        "Downloader:_selectPatch - found existing patch with state: " + state
      );
      if (state == STATE_DOWNLOADING) {
        LOG("Downloader:_selectPatch - resuming download");
        return selectedPatch;
      }
      if (
        state == STATE_PENDING ||
        state == STATE_PENDING_SERVICE ||
        state == STATE_PENDING_ELEVATE ||
        state == STATE_APPLIED ||
        state == STATE_APPLIED_SERVICE
      ) {
        LOG("Downloader:_selectPatch - already downloaded");
        return null;
      }

      // When downloading the patch failed using BITS, there hasn't been an
      // attempt to download the patch using the internal application download
      // mechanism, and an attempt to stage or apply the patch hasn't failed
      // which indicates that a different patch should be downloaded since
      // re-downloading the same patch with the internal application download
      // mechanism will likely also fail when trying to stage or apply it then
      // try to download the same patch using the internal application download
      // mechanism.
      selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag);
      if (
        selectedPatch.getProperty("bitsResult") != null &&
        selectedPatch.getProperty("internalResult") == null &&
        !selectedPatch.errorCode
      ) {
        LOG(
          "Downloader:_selectPatch - Falling back to non-BITS download " +
            "mechanism for the same patch due to existing BITS result: " +
            selectedPatch.getProperty("bitsResult")
        );
        return selectedPatch;
      }

      if (update && selectedPatch.type == "complete") {
        // This is a pretty fatal error.  Just bail.
        LOG("Downloader:_selectPatch - failed to apply complete patch!");
        await cleanupDownloadingUpdate();
        return null;
      }

      // Something went wrong when we tried to apply the previous patch.
      // Try the complete patch next time.
      useComplete = true;
      selectedPatch = null;
    }

    // If we were not able to discover an update from a previous download, we
    // select the best patch from the given set.
    var partialPatch = getPatchOfType(update, "partial");
    if (!useComplete) {
      selectedPatch = partialPatch;
    }
    if (!selectedPatch) {
      if (lazy.UM.internal.readyUpdate) {
        // If we already have a ready update, we download partials only.
        LOG(
          "Downloader:_selectPatch - not selecting a complete patch because " +
            "this is not the first download of the session"
        );
        return null;
      }

      if (partialPatch) {
        partialPatch.selected = false;
      }
      selectedPatch = getPatchOfType(update, "complete");
    }

    // if update only contains a partial patch, selectedPatch == null here if
    // the partial patch has been attempted and fails and we're trying to get a
    // complete patch
    if (selectedPatch) {
      selectedPatch.selected = true;
      update.isCompleteUpdate = selectedPatch.type == "complete";
    }

    LOG(
      "Downloader:_selectPatch - Patch selected. Assigning update to " +
        "downloadingUpdate."
    );
    lazy.UM.internal.downloadingUpdate = update;

    return selectedPatch;
  }

  /**
   * Whether or not the user wants to be notified that an update is being
   * downloaded.
   */
  get _notifyDuringDownload() {
    return Services.prefs.getBoolPref(
      PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD,
      false
    );
  }

  _notifyDownloadStatusObservers() {
    if (this._notifyDuringDownload) {
      let status = this.updateService.isDownloading ? "downloading" : "idle";
      Services.obs.notifyObservers(this._update, "update-downloading", status);
    }
  }

  /**
   * Whether or not we are currently downloading something.
   */
  get isBusy() {
    return this._request != null || this._pendingRequest != null;
  }

  get usingBits() {
    return this._pendingRequest != null || this._request instanceof BitsRequest;
  }

  /**
   * Returns true if the specified patch can be downloaded with BITS.
   */
  _canUseBits(patch) {
    if (getCanUseBits() != "CanUseBits") {
      // This will have printed its own logging. No need to print more.
      return false;
    }
    // Regardless of success or failure, don't download the same patch with BITS
    // twice.
    if (patch.getProperty("bitsResult") != null) {
      LOG(
        "Downloader:_canUseBits - Not using BITS because it was already tried"
      );
      return false;
    }
    LOG("Downloader:_canUseBits - Patch is able to use BITS download");
    return true;
  }

  /**
   * Instruct the add-ons manager to start downloading language pack updates in
   * preparation for the current update.
   */
  _startLangPackUpdates() {
    if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, false)) {
      return;
    }

    // A promise that we can resolve at some point to time out the language pack
    // update process.
    let timeoutPromise = new Promise(resolve => {
      this._langPackTimeout = resolve;
    });

    let update = unwrap(this._update);

    let existing = LangPackUpdates.get(update);
    if (existing) {
      // We have already started staging lang packs for this update, no need to
      // do it again.
      return;
    }

    // Note that we don't care about success or failure here, either way we will
    // continue with the update process.
    let langPackPromise = lazy.AddonManager.stageLangpacksForAppUpdate(
      update.appVersion,
      update.appVersion
    )
      .catch(error => {
        LOG(
          `Add-ons manager threw exception while updating language packs: ${error}`
        );
      })
      .finally(() => {
        let timerId = this._langPackTimeout.gleanTimerId;
        this._langPackTimeout = null;
        if (timerId) {
          Glean.update.langpackOvertime.stopAndAccumulate(timerId);
        }
      });

    LangPackUpdates.set(
      update,
      Promise.race([langPackPromise, timeoutPromise])
    );
  }

  /**
   * Given a patch URL, return a URL possibly modified with extra query
   * parameters and extra headers.  The extras help identify whether this update
   * is driven by a regular browsing Firefox or by a background update task.
   *
   * @param {string} [patchURL] Unmodified patch URL.
   * @return { url, extraHeaders }
   */
  _maybeWithExtras(patchURL) {
    let shouldAddExtras = true;
    if (AppConstants.MOZ_APP_NAME !== "firefox") {
      shouldAddExtras = false;
    }
    if (Services.policies) {
      let policies = Services.policies.getActivePolicies();
      if (policies) {
        if ("AppUpdateURL" in policies) {
          shouldAddExtras = false;
        }
      }
    }

    if (!shouldAddExtras) {
      LOG("Downloader:_maybeWithExtras - Not adding extras");
      return { url: patchURL, extraHeaders: "\r\n" };
    }

    LOG("Downloader:_maybeWithExtras - Adding extras");

    let modeStr = lazy.gIsBackgroundTaskMode ? "1" : "0";
    let extraHeaders = `X-BackgroundTaskMode: ${modeStr}\r\n`;
    let extraParameters = [["backgroundTaskMode", modeStr]];

    if (lazy.gIsBackgroundTaskMode) {
      const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
        Ci.nsIBackgroundTasks
      );
      extraHeaders += `X-BackgroundTaskName: ${bts.backgroundTaskName()}\r\n`;
      extraParameters.push(["backgroundTaskName", bts.backgroundTaskName()]);
    }

    extraHeaders += "\r\n";

    let url = patchURL;
    let parsedUrl = URL.parse(url);
    if (parsedUrl) {
      for (let [p, v] of extraParameters) {
        parsedUrl.searchParams.set(p, v);
      }
      url = parsedUrl.href;
    } else {
      LOG("Downloader:_maybeWithExtras - Failed to parse patch URL!");
    }

    return { url, extraHeaders };
  }

  /**
   * Download and stage the given update.
   *
   * @param   update
   *          A nsIUpdate object to download a patch for. Cannot be null.
   */
  async downloadUpdate(update) {
    LOG("Downloader:downloadUpdate");
    if (!update) {
      AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE);
      throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
    }

    var updateDir = getDownloadingUpdateDir();

    this._update = update;

    // This function may return null, which indicates that there are no patches
    // to download.
    this._patch = await this._selectPatch(update);
    if (!this._patch) {
      LOG("Downloader:downloadUpdate - no patch to download");
      AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH);
      return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
    }
    // The update and the patch implement nsIWritablePropertyBag. Expose that
    // interface immediately after a patch is assigned so that
    // this.(_patch|_update).(get|set)Property can always safely be called.
    this._update.QueryInterface(Ci.nsIWritablePropertyBag);
    this._patch.QueryInterface(Ci.nsIWritablePropertyBag);

    if (
      this._update.getProperty("disableBackgroundUpdates") != null &&
      lazy.gIsBackgroundTaskMode
    ) {
      LOG(
        "Downloader:downloadUpdate - Background update disabled by update " +
          "advertisement"
      );
      return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
    }

    this.isCompleteUpdate = this._patch.type == "complete";

    let canUseBits = false;
    // Allow the advertised update to disable BITS.
    if (this._update.getProperty("disableBITS") != null) {
      LOG(
        "Downloader:downloadUpdate - BITS downloads disabled by update " +
          "advertisement"
      );
    } else {
      canUseBits = this._canUseBits(this._patch);
    }

    // When using Firefox and Mozilla's update server, add extra headers and
    // extra query parameters identifying whether this request is on behalf of a
    // regular browsing profile (0) or a background task (1).  This helps
    // understand bandwidth usage of background updates in production.
    let { url, extraHeaders } = this._maybeWithExtras(this._patch.URL);

    if (!canUseBits) {
      this._pendingRequest = null;

      let patchFile = updateDir.clone();
      patchFile.append(FILE_UPDATE_MAR);

      // Background updates generally should not fall back to internal (Necko)
      // downloads: on Windows, they should only use Windows BITS.  In
      // automation, this pref allows Necko for testing.
      let allowDownloadsWithoutBITS =
        Cu.isInAutomation &&
        Services.prefs.getBoolPref(
          PREF_APP_UPDATE_BACKGROUND_ALLOWDOWNLOADSWITHOUTBITS,
          false
        );
      if (lazy.gIsBackgroundTaskMode && !allowDownloadsWithoutBITS) {
        // We don't normally run a background update if we can't use BITS, but
        // this branch is possible because we do fall back from BITS failures by
        // attempting an internal download.
        // If this happens, we are just going to need to wait for interactive
        // Firefox to download the update. We don't, however, want to be in the
        // "downloading" state when interactive Firefox runs because we want to
        // download the newest update available which, at that point, may not be
        // the one that we are currently trying to download.
        // However, we can't just unconditionally clobber the current update
        // because interactive Firefox might already be part way through an
        // internal update download, and we definitely don't want to interrupt
        // that.
        let readyUpdateDir = getReadyUpdateDir();
        let status = readStatusFile(readyUpdateDir);
        // nsIIncrementalDownload doesn't use an intermediate download location
        // for partially downloaded files. If we have started an update
        // download with it, it will be available at its ultimate location.
        if (!(status == STATE_DOWNLOADING && patchFile.exists())) {
          LOG(
            "Downloader:downloadUpdate - Can't download with internal " +
              "downloader from a background task. Cleaning up downloading " +
              "update."
          );
          await cleanupDownloadingUpdate();
        }
        return Ci.nsIApplicationUpdateService
          .DOWNLOAD_FAILURE_CANNOT_RESUME_IN_BACKGROUND;
      }

      // The interval is 0 since there is no need to throttle downloads.
      let interval = 0;

      LOG(
        "Downloader:downloadUpdate - Starting nsIIncrementalDownload with " +
          "url: " +
          url +
          ", path: " +
          patchFile.path +
          ", interval: " +
          interval
      );
      let uri = Services.io.newURI(url);

      this._request = Cc[
        "@mozilla.org/network/incremental-download;1"
      ].createInstance(Ci.nsIIncrementalDownload);

      this._request.init(
        uri,
        patchFile,
        DOWNLOAD_CHUNK_SIZE,
        interval,
        extraHeaders
      );
      this._request.start(this, null);
    } else {
      this._bitsActiveNotifications = this.hasDownloadListeners;
      this._cancelPromise = null;

      this._pendingRequest = this.updateService.makeBitsRequest({
        activeListeners: this.hasDownloadListeners,
        bitsId: this._patch.getProperty("bitsId"),
        observer: this,
        url,
        extraHeaders,
      });

      let request;
      try {
        request = await this._pendingRequest;
      } catch (error) {
        if (
          (error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_GET_BITS_JOB ||
            error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_CONNECT_TO_BCM) &&
          error.action == Ci.nsIBits.ERROR_ACTION_MONITOR_DOWNLOAD &&
          error.stage == Ci.nsIBits.ERROR_STAGE_BITS_CLIENT &&
          error.codeType == Ci.nsIBits.ERROR_CODE_TYPE_HRESULT &&
          error.code == HRESULT_E_ACCESSDENIED
        ) {
          LOG(
            "Downloader:downloadUpdate - Failed to connect to existing " +
              "BITS job. It is likely owned by another user."
          );
          // This isn't really a failure code since the BITS job may be working
          // just fine on another account, so convert this to a code that
          // indicates that. This will make it easier to identify in telemetry.
          error.type = Ci.nsIBits.ERROR_TYPE_ACCESS_DENIED_EXPECTED;
          error.codeType = Ci.nsIBits.ERROR_CODE_TYPE_NONE;
          error.code = null;
          // When we detect this situation, disable BITS until Firefox shuts
          // down. There are a couple of reasons for this. First, without any
          // kind of flag, we enter an infinite loop here where we keep trying
          // BITS over and over again (normally setting bitsResult prevents
          // this, but we don't know the result of the BITS job, so we don't
          // want to set that). Second, since we are trying to update, this
          // process must have the update mutex. We don't ever give up the
          // update mutex, so even if the other user starts Firefox, they will
          // not complete the BITS job while this Firefox instance is around.
          gBITSInUseByAnotherUser = true;
        } else {
          this._patch.setProperty("bitsResult", Cr.NS_ERROR_FAILURE);
          lazy.UM.saveUpdates();

          LOG(
            "Downloader:downloadUpdate - Failed to start to BITS job. " +
              "Error: " +
              error
          );
        }

        this._pendingRequest = null;

        AUSTLMY.pingBitsError(this.isCompleteUpdate, error);

        // Try download again with nsIIncrementalDownload
        return this.downloadUpdate(this._update);
      }

      this._request = request;
      this._patch.setProperty("bitsId", request.bitsId);

      LOG(
        "Downloader:downloadUpdate - BITS download running. BITS ID: " +
          request.bitsId
      );

      if (this.hasDownloadListeners) {
        this._maybeStartActiveNotifications();
      } else {
        this._maybeStopActiveNotifications();
      }

      lazy.UM.saveUpdates();
      this._pendingRequest = null;
    }

    if (!lazy.UM.internal.readyUpdate) {
      const error = writeStatusFile(getReadyUpdateDir(), STATE_DOWNLOADING);
      if (error) {
        LOG("Downloader:downloadUpdate - Failed to set status to downloading");
        await cleanupActiveUpdates();
        return Ci.nsIApplicationUpdateService
          .DOWNLOAD_FAILURE_CANNOT_WRITE_STATE;
      }
      LOG("Downloader:downloadUpdate - Set status to downloading");
    }
    if (this._patch.state != STATE_DOWNLOADING) {
      LOG("Downloader:downloadUpdate - Setting state to downloading");
      this._patch.state = STATE_DOWNLOADING;
      lazy.UM.saveUpdates();
    }

    // If we are downloading a second update, we don't change the state until
    // STATE_SWAP.
    if (lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING) {
      LOG(
        "Downloader:downloadUpdate - not setting state because download is " +
          "already pending."
      );
    } else {
      LOG(
        "Downloader:downloadUpdate - setting currentState to STATE_DOWNLOADING"
      );
      transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
    }

    this._startLangPackUpdates();

    this._notifyDownloadStatusObservers();

    return Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS;
  }

  /**
   * This is run when a download listener is added.
   */
  onDownloadListenerAdded() {
    // Increase the status update frequency when someone starts listening
    this._maybeStartActiveNotifications();
  }

  /**
   * This is run when a download listener is removed.
   */
  onDownloadListenerRemoved() {
    // Decrease the status update frequency when no one is listening
    if (!this.hasDownloadListeners) {
      this._maybeStopActiveNotifications();
    }
  }

  get hasDownloadListeners() {
    return this.updateService.hasDownloadListeners;
  }

  /**
   * This speeds up BITS progress notifications in response to a user watching
   * the notifications.
   */
  async _maybeStartActiveNotifications() {
    if (
      this.usingBits &&
      !this._bitsActiveNotifications &&
      this.hasDownloadListeners &&
      this._request
    ) {
      LOG(
        "Downloader:_maybeStartActiveNotifications - Starting active " +
          "notifications"
      );
      this._bitsActiveNotifications = true;
      await Promise.all([
        this._request
          .setNoProgressTimeout(BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS)
          .catch(error => {
            LOG(
              "Downloader:_maybeStartActiveNotifications - Failed to set " +
                "no progress timeout. Error: " +
                error
            );
          }),
        this._request
          .changeMonitorInterval(BITS_ACTIVE_POLL_RATE_MS)
          .catch(error => {
            LOG(
              "Downloader:_maybeStartActiveNotifications - Failed to increase " +
                "status update frequency. Error: " +
                error
            );
          }),
      ]);
    }
  }

  /**
   * This slows down BITS progress notifications in response to a user no longer
   * watching the notifications.
   */
  async _maybeStopActiveNotifications() {
    if (
      this.usingBits &&
      this._bitsActiveNotifications &&
      !this.hasDownloadListeners &&
      this._request
    ) {
      LOG(
        "Downloader:_maybeStopActiveNotifications - Stopping active " +
          "notifications"
      );
      this._bitsActiveNotifications = false;
      await Promise.all([
        this._request
          .setNoProgressTimeout(BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS)
          .catch(error => {
            LOG(
              "Downloader:_maybeStopActiveNotifications - Failed to set " +
                "no progress timeout: " +
                error
            );
          }),
        this._request
          .changeMonitorInterval(BITS_IDLE_POLL_RATE_MS)
          .catch(error => {
            LOG(
              "Downloader:_maybeStopActiveNotifications - Failed to decrease " +
                "status update frequency: " +
                error
            );
          }),
      ]);
    }
  }

  /**
   * When the async request begins
   *
   * @param   request
   *          The nsIRequest object for the transfer
   */
  onStartRequest(request) {
    if (this.usingBits) {
      LOG("Downloader:onStartRequest");
    } else {
      LOG(
        "Downloader:onStartRequest - original URI spec: " +
          request.URI.spec +
          ", final URI spec: " +
          request.finalURI.spec
      );
      // Set finalURL in onStartRequest if it is different.
      if (this._patch.finalURL != request.finalURI.spec) {
        this._patch.finalURL = request.finalURI.spec;
        lazy.UM.saveUpdates();
      }
    }

    this.updateService.forEachDownloadListener(listener => {
      listener.onStartRequest(request);
    });
  }

  /**
   * When new data has been downloaded
   *
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   progress
   *          The current number of bytes transferred
   * @param   maxProgress
   *          The total number of bytes that must be transferred
   */
  onProgress(request, progress, maxProgress) {
    LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress);

    if (progress > this._patch.size) {
      LOG(
        "Downloader:onProgress - progress: " +
          progress +
          " is higher than patch size: " +
          this._patch.size
      );
      AUSTLMY.pingDownloadCode(
        this.isCompleteUpdate,
        AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER
      );
      this.cancel(Cr.NS_ERROR_UNEXPECTED);
      return;
    }

    // Wait until the transfer has started (progress > 0) to verify maxProgress
    // so that we don't check it before it is available (in which case, -1 would
    // have been passed).
    if (progress > 0 && maxProgress != this._patch.size) {
      LOG(
        "Downloader:onProgress - maxProgress: " +
          maxProgress +
          " is not equal to expected patch size: " +
          this._patch.size
      );
      AUSTLMY.pingDownloadCode(
        this.isCompleteUpdate,
        AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL
      );
      this.cancel(Cr.NS_ERROR_UNEXPECTED);
      return;
    }

    this.updateService.forEachDownloadListener(listener => {
      if (listener instanceof Ci.nsIProgressEventSink) {
        listener.onProgress(request, progress, maxProgress);
      }
    });
    this.updateService._consecutiveSocketErrors = 0;
  }

  /**
   * When we have new status text
   *
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   status
   *          A status code
   * @param   statusText
   *          Human readable version of |status|
   */
  onStatus(request, status, statusText) {
    LOG(
      "Downloader:onStatus - status: " + status + ", statusText: " + statusText
    );

    this.updateService.forEachDownloadListener(listener => {
      if (listener instanceof Ci.nsIProgressEventSink) {
        listener.onStatus(request, status, statusText);
      }
    });
  }

  /**
   * When data transfer ceases
   *
   * @param   request
   *          The nsIRequest object for the transfer
   * @param   status
   *          Status code containing the reason for the cessation.
   */
  /* eslint-disable-next-line complexity */
  async onStopRequest(request, status) {
    if (gOnlyDownloadUpdatesThisSession) {
      LOG(
        "Downloader:onStopRequest - End of update download detected and " +
          "ignored because we are restricted to update downloads this " +
          "session. We will continue with this update next session."
      );
      // In order to keep the update from progressing past the downloading
      // stage, we will pretend that the download is still going.
      // A lot of this work is done for us by just not setting this._request to
      // null, which usually signals that the transfer has completed.
      this._pretendingDownloadIsNotDone = true;
      // This notification is currently used only for testing.
      Services.obs.notifyObservers(null, "update-download-restriction-hit");
      return;
    }

    if (!this.usingBits) {
      LOG(
        "Downloader:onStopRequest - downloader: nsIIncrementalDownload, " +
          "original URI spec: " +
          request.URI.spec +
          ", final URI spec: " +
          request.finalURI.spec +
          ", status: " +
          status
      );
    } else {
      LOG("Downloader:onStopRequest - downloader: BITS, status: " + status);
    }

    let bitsCompletionError;
    if (this.usingBits) {
      if (Components.isSuccessCode(status)) {
        try {
          await request.complete();
        } catch (e) {
          LOG(
            "Downloader:onStopRequest - Unable to complete BITS download: " + e
          );
          status = Cr.NS_ERROR_FAILURE;
          bitsCompletionError = e;
        }
      } else {
        // BITS jobs that failed to complete should still have cancel called on
        // them to remove the job.
        try {
          await this.cancel();
        } catch (e) {
          // This will fail if the job stopped because it was cancelled.
          // Even if this is a "real" error, there isn't really anything to do
          // about it, and it's not really a big problem. It just means that the
          // BITS job will stay around until it is removed automatically
          // (default of 90 days).
        }
      }
    }

    var state = this._patch.state;
    var shouldShowPrompt = false;
    var shouldRegisterOnlineObserver = false;
    var shouldRegisterBootstrapObserver = false;
    var shouldRetrySoon = false;
    var deleteActiveUpdate = false;
    let migratedToReadyUpdate = false;
    let nonDownloadFailure = false;
    var retryTimeout = Services.prefs.getIntPref(
      PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT,
      DEFAULT_SOCKET_RETRYTIMEOUT
    );
    // Prevent the preference from setting a value greater than 10000.
    retryTimeout = Math.min(retryTimeout, 10000);
    var maxFail = Services.prefs.getIntPref(
      PREF_APP_UPDATE_SOCKET_MAXERRORS,
      DEFAULT_SOCKET_MAX_ERRORS
    );
    // Prevent the preference from setting a value greater than 20.
    maxFail = Math.min(maxFail, 20);
    LOG(
      "Downloader:onStopRequest - status: " +
        status +
        ", " +
        "current fail: " +
        this.updateService._consecutiveSocketErrors +
        ", " +
        "max fail: " +
        maxFail +
        ", " +
        "retryTimeout: " +
        retryTimeout
    );
    if (Components.isSuccessCode(status)) {
      if (this._verifyDownload()) {
        AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS);

        LOG(
          "Downloader:onStopRequest - Clearing readyUpdate in preparation of " +
            "moving downloadingUpdate into readyUpdate."
        );

        // Clear out any old update before we notify anyone about the new one.
        // It will be invalid in a moment anyways when we call
        // `cleanUpReadyUpdateDir()`.
        lazy.UM.internal.readyUpdate = null;

        // We're about to clobber the ready update so we can replace it with the
        // downloading update that just finished. We need to let observers know
        // about this.
        if (
          lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING
        ) {
          transitionState(Ci.nsIApplicationUpdateService.STATE_SWAP);
        }
        Services.obs.notifyObservers(this._update, "update-swap");

        // Swap the downloading update into the ready update directory.
        cleanUpReadyUpdateDir();
        let downloadedMar = getDownloadingUpdateDir();
        downloadedMar.append(FILE_UPDATE_MAR);
        let readyDir = getReadyUpdateDir();
        try {
          downloadedMar.moveTo(readyDir, FILE_UPDATE_MAR);
          migratedToReadyUpdate = true;
        } catch (e) {
          migratedToReadyUpdate = false;
        }

        if (migratedToReadyUpdate) {
          AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_SUCCESS);
          state = getBestPendingState();
          shouldShowPrompt = !getCanStageUpdates();

          // Tell the updater.exe we're ready to apply.
          LOG(
            `Downloader:onStopRequest - Ready to apply. Setting state to ` +
              `"${state}".`
          );
          writeStatusFile(getReadyUpdateDir(), state);
          writeVersionFile(getReadyUpdateDir(), this._update.appVersion);
          this._update.installDate = new Date().getTime();
          this._update.statusText =
            lazy.gUpdateBundle.GetStringFromName("installPending");
          Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
        } else {
          LOG(
            "Downloader:onStopRequest - failed to move the downloading " +
              "update to the ready update directory."
          );
          AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_UNKNOWN_FAILURE);

          state = STATE_DOWNLOAD_FAILED;
          status = Cr.NS_ERROR_FILE_COPY_OR_MOVE_FAILED;

          const mfCode = "move_failed";
          let message = getStatusTextFromCode(mfCode, mfCode);
          this._update.statusText = message;

          nonDownloadFailure = true;
          deleteActiveUpdate = true;

          cleanUpDownloadingUpdateDir();
        }
      } else {
        LOG("Downloader:onStopRequest - download verification failed");
        state = STATE_DOWNLOAD_FAILED;
        status = Cr.NS_ERROR_CORRUPTED_CONTENT;

        // Yes, this code is a string.
        const vfCode = "verification_failed";
        var message = getStatusTextFromCode(vfCode, vfCode);
        this._update.statusText = message;

        if (this._update.isCompleteUpdate || this._update.patchCount != 2) {
          LOG("Downloader:onStopRequest - No alternative patch to try");
          deleteActiveUpdate = true;
        }

        // Destroy the updates directory, since we're done with it.
        cleanUpDownloadingUpdateDir();
      }
    } else if (status == Cr.NS_ERROR_OFFLINE) {
      // Register an online observer to try again.
      // The online observer will continue the incremental download by
      // calling downloadUpdate on the active update which continues
      // downloading the file from where it was.
      LOG("Downloader:onStopRequest - offline, register online observer: true");
      AUSTLMY.pingDownloadCode(
        this.isCompleteUpdate,
        AUSTLMY.DWNLD_RETRY_OFFLINE
      );
      shouldRegisterOnlineObserver = true;
      deleteActiveUpdate = false;
    } else if (
      status === PROXY_SERVER_CONNECTION_REFUSED &&
      this.updateService._shouldRegisterBootstrapObserver()
    ) {
      // Register a bootstrap observer to try again.
      // The bootstrap observer will continue the incremental download by
      // calling downloadUpdate on the active update which continues
      // downloading the file from where it was.
      LOG(
        "Downloader:onStopRequest - not bootstrapped, register bootstrap observer: true"
      );
      AUSTLMY.pingDownloadCode(
        this.isCompleteUpdate,
        AUSTLMY.DWNLD_RETRY_OFFLINE
      );
      shouldRegisterBootstrapObserver = true;
      deleteActiveUpdate = false;
      // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED,
      // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned
      // when disconnecting the internet while a download of a MAR is in
      // progress.  There may be others but I have not encountered them during
      // testing.
    } else if (
      (status == Cr.NS_ERROR_NET_TIMEOUT ||
        status == Cr.NS_ERROR_CONNECTION_REFUSED ||
        status == Cr.NS_ERROR_NET_RESET ||
        status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) &&
      this.updateService._consecutiveSocketErrors < maxFail
    ) {
      LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true");
      let dwnldCode = AUSTLMY.DWNLD_RETRY_CONNECTION_REFUSED;
      if (status == Cr.NS_ERROR_NET_TIMEOUT) {
        dwnldCode = AUSTLMY.DWNLD_RETRY_NET_TIMEOUT;
      } else if (status == Cr.NS_ERROR_NET_RESET) {
        dwnldCode = AUSTLMY.DWNLD_RETRY_NET_RESET;
      } else if (status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
        dwnldCode = AUSTLMY.DWNLD_ERR_DOCUMENT_NOT_CACHED;
      }
      AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode);
      shouldRetrySoon = true;
      deleteActiveUpdate = false;
    } else if (status != Cr.NS_BINDING_ABORTED && status != Cr.NS_ERROR_ABORT) {
      if (
        status == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
        status == Cr.NS_ERROR_FILE_READ_ONLY
      ) {
        LOG("Downloader:onStopRequest - permission error");
        nonDownloadFailure = true;
      } else {
        LOG("Downloader:onStopRequest - non-verification failure");
      }

      let dwnldCode = AUSTLMY.DWNLD_ERR_BINDING_ABORTED;
      if (status == Cr.NS_ERROR_ABORT) {
        dwnldCode = AUSTLMY.DWNLD_ERR_ABORT;
      }
      AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode);

      // Some sort of other failure, log this in the |statusText| property
      state = STATE_DOWNLOAD_FAILED;

      // XXXben - if |request| (The Incremental Download) provided a means
      // for accessing the http channel we could do more here.

      this._update.statusText = getStatusTextFromCode(
        status,
        Cr.NS_BINDING_FAILED
      );

      // Destroy the updates directory, since we're done with it.
      cleanUpDownloadingUpdateDir();

      deleteActiveUpdate = true;
    }
    if (!this.usingBits) {
      LOG(`Downloader:onStopRequest - Setting internalResult to ${status}`);
      this._patch.setProperty("internalResult", status);
    } else {
      LOG(`Downloader:onStopRequest - Setting bitsResult to ${status}`);
      this._patch.setProperty("bitsResult", status);

      // If we failed when using BITS, we want to override the retry decision
      // since we need to retry with nsIncrementalDownload before we give up.
      // However, if the download was cancelled, don't retry. If the transfer
      // was cancelled, we don't want it to restart on its own.
      if (
        !Components.isSuccessCode(status) &&
        status != Cr.NS_BINDING_ABORTED &&
        status != Cr.NS_ERROR_ABORT
      ) {
        deleteActiveUpdate = false;
        shouldRetrySoon = true;
      }

      // Send BITS Telemetry
      if (Components.isSuccessCode(status)) {
        AUSTLMY.pingBitsSuccess(this.isCompleteUpdate);
      } else {
        let error;
        if (bitsCompletionError) {
          error = bitsCompletionError;
        } else if (status == Cr.NS_ERROR_CORRUPTED_CONTENT) {
          error = new BitsVerificationError();
        } else {
          error = request.transferError;
          if (!error) {
            error = new BitsUnknownError();
          } else if (
            error.codeType == Ci.nsIBits.ERROR_CODE_TYPE_HRESULT &&
            error.code == BG_E_HTTP_ERROR_406
          ) {
            Glean.update.blocked.add();
          }
        }
        AUSTLMY.pingBitsError(this.isCompleteUpdate, error);
      }
    }

    LOG("Downloader:onStopRequest - setting state to: " + state);
    if (this._patch.state != state) {
      this._patch.state = state;
    }
    if (deleteActiveUpdate) {
      LOG("Downloader:onStopRequest - Clearing downloadingUpdate.");
      this._update.installDate = new Date().getTime();
      lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
      lazy.UM.internal.downloadingUpdate = null;
    } else if (
      lazy.UM.internal.downloadingUpdate &&
      lazy.UM.internal.downloadingUpdate.state != state
    ) {
      lazy.UM.internal.downloadingUpdate.state = state;
    }
    if (migratedToReadyUpdate) {
      LOG(
        "Downloader:onStopRequest - Moving downloadingUpdate into readyUpdate"
      );
      lazy.UM.internal.readyUpdate = lazy.UM.internal.downloadingUpdate;
      lazy.UM.internal.downloadingUpdate = null;
    }
    lazy.UM.saveUpdates();

    // Only notify listeners about the stopped state if we
    // aren't handling an internal retry.
    if (
      !shouldRetrySoon &&
      !shouldRegisterOnlineObserver &&
      !shouldRegisterBootstrapObserver
    ) {
      this.updateService.forEachDownloadListener(listener => {
        listener.onStopRequest(request, status);
      });
    }

    this._request = null;

    // This notification must happen after _request is set to null so that
    // the correct this.updateService.isDownloading value is available in
    // _notifyDownloadStatusObservers().
    this._notifyDownloadStatusObservers();

    if (state == STATE_DOWNLOAD_FAILED) {
      var allFailed = true;
      // Don't bother retrying the download if we got an error that isn't
      // download related.
      if (!nonDownloadFailure) {
        // If we haven't already, attempt to download without BITS
        if (request instanceof BitsRequest) {
          LOG(
            "Downloader:onStopRequest - BITS download failed. Falling back " +
              "to nsIIncrementalDownload"
          );
          let result = await this.downloadUpdate(this._update);
          if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
            LOG(
              "Downloader:onStopRequest - Failed to fall back to " +
                "nsIIncrementalDownload. Cleaning up downloading update."
            );
            await cleanupDownloadingUpdate();
          } else {
            allFailed = false;
          }
        }

        // Check if there is a complete update patch that can be downloaded.
        if (
          allFailed &&
          !this._update.isCompleteUpdate &&
          this._update.patchCount == 2
        ) {
          LOG(
            "Downloader:onStopRequest - verification of patch failed, " +
              "downloading complete update patch"
          );
          this._update.isCompleteUpdate = true;
          let result = await this.downloadUpdate(this._update);

          if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
            LOG(
              "Downloader:onStopRequest - Failed to fall back to complete " +
                "patch. Cleaning up downloading update."
            );
            await cleanupDownloadingUpdate();
          } else {
            allFailed = false;
          }
        }
      }

      if (allFailed) {
        let downloadAttempts = Services.prefs.getIntPref(
          PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS,
          0
        );
        downloadAttempts++;
        Services.prefs.setIntPref(
          PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS,
          downloadAttempts
        );
        let maxAttempts = Math.min(
          Services.prefs.getIntPref(PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2),
          10
        );

        transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);

        if (downloadAttempts > maxAttempts) {
          LOG(
            "Downloader:onStopRequest - notifying observers of error. " +
              "topic: update-error, status: download-attempts-exceeded, " +
              "downloadAttempts: " +
              downloadAttempts +
              " " +
              "maxAttempts: " +
              maxAttempts
          );
          Services.obs.notifyObservers(
            this._update,
            "update-error",
            "download-attempts-exceeded"
          );
        } else {
          this._update.selectedPatch.selected = false;
          LOG(
            "Downloader:onStopRequest - notifying observers of error. " +
              "topic: update-error, status: download-attempt-failed"
          );
          Services.obs.notifyObservers(
            this._update,
            "update-error",
            "download-attempt-failed"
          );
        }
        // We don't care about language pack updates now.
        this._langPackTimeout = null;
        LangPackUpdates.delete(unwrap(this._update));

        // Prevent leaking the update object (bug 454964).
        this._update = null;

        // allFailed indicates that we didn't (successfully) call downloadUpdate
        // to try to download a different MAR. In this case, this Downloader
        // is no longer being used.
        this.updateService._downloader = null;
      }
      // A complete download has been initiated or the failure was handled.
      return;
    }

    // If the download has succeeded or failed, we are done with this Downloader
    // object. However, in some cases (ex: network disconnection), we will
    // attempt to resume using this same Downloader.
    if (state != STATE_DOWNLOADING) {
      this.updateService._downloader = null;
    }

    if (
      state == STATE_PENDING ||
      state == STATE_PENDING_SERVICE ||
      state == STATE_PENDING_ELEVATE
    ) {
      if (getCanStageUpdates()) {
        LOG(
          "Downloader:onStopRequest - attempting to stage update: " +
            this._update.name
        );
        // Stage the update
        let stagingStarted = true;
        try {
          Cc["@mozilla.org/updates/update-processor;1"]
            .createInstance(Ci.nsIUpdateProcessor)
            .processUpdate();
        } catch (e) {
          LOG(
            "Downloader:onStopRequest - failed to stage update. Exception: " + e
          );
          stagingStarted = false;
        }
        if (stagingStarted) {
          transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING);
        } else {
          // Fail gracefully in case the application does not support the update
          // processor service.
          shouldShowPrompt = true;
        }
      }
    }

    // If we're still waiting on language pack updates then run a timer to time
    // out the attempt after an appropriate amount of time.
    if (this._langPackTimeout) {
      // Start a timer to measure how much longer it takes for the language
      // packs to stage.
      this._langPackTimeout.gleanTimerId =
        Glean.update.langpackOvertime.start();

      lazy.setTimeout(
        this._langPackTimeout,
        Services.prefs.getIntPref(
          PREF_APP_UPDATE_LANGPACK_TIMEOUT,
          LANGPACK_UPDATE_DEFAULT_TIMEOUT
        )
      );
    }

    // Do this after *everything* else, since it will likely cause the app
    // to shut down.
    if (shouldShowPrompt) {
      // Wait for language packs to stage before showing any prompt to restart.
      let update = this._update;
      promiseLangPacksUpdated(update).then(() => {
        LOG(
          "Downloader:onStopRequest - Notifying observers that " +
            "an update was downloaded. topic: update-downloaded, status: " +
            update.state
        );
        transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
        Services.obs.notifyObservers(update, "update-downloaded", update.state);
      });
    }

    if (shouldRegisterOnlineObserver) {
      LOG("Downloader:onStopRequest - Registering online observer");
      this.updateService._registerOnlineObserver();
    } else if (shouldRegisterBootstrapObserver) {
      LOG("Downloader:onStopRequest - Registering bootstrap observer");
      this.updateService._registerBootstrapObserver();
    } else if (shouldRetrySoon) {
      LOG("Downloader:onStopRequest - Retrying soon");
      this.updateService._consecutiveSocketErrors++;
      if (this.updateService._retryTimer) {
        this.updateService._retryTimer.cancel();
      }
      this.updateService._retryTimer = Cc[
        "@mozilla.org/timer;1"
      ].createInstance(Ci.nsITimer);
      this.updateService._retryTimer.initWithCallback(
        async () => {
          await this.updateService._attemptResume();
        },
        retryTimeout,
        Ci.nsITimer.TYPE_ONE_SHOT
      );
    } else {
      // Prevent leaking the update object (bug 454964)
      this._update = null;
    }
  }

  /**
   * This function should be called when shutting down so that resources get
   * freed properly.
   */
  async cleanup() {
    if (this.usingBits) {
      if (this._pendingRequest) {
        await this._pendingRequest;
      }
      this._request.shutdown();
    }
  }

  /**
   * See nsIInterfaceRequestor.idl
   */
  getInterface(iid) {
    // The network request may require proxy authentication, so provide the
    // default nsIAuthPrompt if requested.
    if (iid.equals(Ci.nsIAuthPrompt)) {
      var prompt =
        Cc["@mozilla.org/network/default-auth-prompt;1"].createInstance();
      return prompt.QueryInterface(iid);
    }
    throw Components.Exception("", Cr.NS_NOINTERFACE);
  }

  QueryInterface = ChromeUtils.generateQI([
    Ci.nsIRequestObserver,
    Ci.nsIProgressEventSink,
    Ci.nsIInterfaceRequestor,
  ]);
}

// On macOS, all browser windows can be closed without Firefox exiting. If it
// is left in this state for a while and an update is pending, we should restart
// Firefox on our own to apply the update. This class will do that
// automatically.
class RestartOnLastWindowClosed {
  #enabled = false;
  #hasShutdown = false;

  #restartTimer = null;
  #restartTimerExpired = false;

  constructor() {
    this.#maybeEnableOrDisable();

    Services.prefs.addObserver(
      PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
      this
    );
    Services.obs.addObserver(this, "quit-application");
  }

  shutdown() {
    LOG("RestartOnLastWindowClosed.shutdown - Shutting down");
    this.#hasShutdown = true;

    Services.prefs.removeObserver(
      PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
      this
    );
    Services.obs.removeObserver(this, "quit-application");

    this.#maybeEnableOrDisable();
  }

  get shouldEnable() {
    if (AppConstants.platform != "macosx") {
      return false;
    }
    if (this.#hasShutdown) {
      return false;
    }
    return Services.prefs.getBoolPref(
      PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
      false
    );
  }

  get enabled() {
    return this.#enabled;
  }

  observe(subject, topic, data) {
    switch (topic) {
      case "nsPref:changed":
        if (data == PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED) {
          this.#maybeEnableOrDisable();
        }
        break;
      case "quit-application":
        this.shutdown();
        break;
      case "domwindowclosed":
        this.#onWindowClose();
        break;
      case "domwindowopened":
        this.#onWindowOpen();
        break;
      case "update-downloaded":
      case "update-staged":
        this.#onUpdateReady(data);
        break;
    }
  }

  // Returns true if any windows are open. Otherwise, false.
  #windowsAreOpen() {
    // eslint-disable-next-line no-unused-vars
    for (const win of Services.wm.getEnumerator(null)) {
      return true;
    }
    return false;
  }

  // Enables or disables this class's functionality based on the value of
  // this.shouldEnable. Does nothing if the class is already in the right state
  // (i.e. if the class should be enabled and already is, or should be disabled
  // and already is).
  #maybeEnableOrDisable() {
    if (this.shouldEnable) {
      if (this.#enabled) {
        return;
      }
      LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Enabling");

      Services.obs.addObserver(this, "domwindowclosed");
      Services.obs.addObserver(this, "domwindowopened");
      Services.obs.addObserver(this, "update-downloaded");
      Services.obs.addObserver(this, "update-staged");

      this.#restartTimer = null;
      this.#restartTimerExpired = false;

      this.#enabled = true;

      // Synchronize with external state.
      this.#onWindowClose();
    } else {
      if (!this.#enabled) {
        return;
      }
      LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Disabling");

      Services.obs.removeObserver(this, "domwindowclosed");
      Services.obs.removeObserver(this, "domwindowopened");
      Services.obs.removeObserver(this, "update-downloaded");
      Services.obs.removeObserver(this, "update-staged");

      this.#enabled = false;

      if (this.#restartTimer) {
        this.#restartTimer.cancel();
      }
      this.#restartTimer = null;
    }
  }

  // Note: Since we keep track of the update state even when this class is
  //       disabled, this function will run even in that case.
  #onUpdateReady(updateState) {
    // Note that we do not count pending-elevate as a ready state, because we
    // cannot silently restart in that state.
    if (
      [
        STATE_APPLIED,
        STATE_PENDING,
        STATE_APPLIED_SERVICE,
        STATE_PENDING_SERVICE,
      ].includes(updateState)
    ) {
      if (this.#enabled) {
        LOG("RestartOnLastWindowClosed.#onUpdateReady - update ready");
        this.#maybeRestartBrowser();
      }
    } else if (this.#enabled) {
      LOG(
        `RestartOnLastWindowClosed.#onUpdateReady - Not counting update as ` +
          `ready because the state is ${updateState}`
      );
    }
  }

  #onWindowClose() {
    if (!this.#windowsAreOpen()) {
      this.#onLastWindowClose();
    }
  }

  #onLastWindowClose() {
    if (this.#restartTimer || this.#restartTimerExpired) {
      LOG(
        "RestartOnLastWindowClosed.#onLastWindowClose - Restart timer is " +
          "either already running or has already expired"
      );
      return;
    }

    let timeout = Services.prefs.getIntPref(
      PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS,
      5 * 60 * 1000
    );

    LOG(
      "RestartOnLastWindowClosed.#onLastWindowClose - Last window closed. " +
        "Starting restart timer"
    );
    this.#restartTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    this.#restartTimer.initWithCallback(
      () => this.#onRestartTimerExpire(),
      timeout,
      Ci.nsITimer.TYPE_ONE_SHOT
    );
  }

  #onWindowOpen() {
    if (this.#restartTimer) {
      LOG(
        "RestartOnLastWindowClosed.#onWindowOpen - Window opened. Cancelling " +
          "restart timer."
      );
      this.#restartTimer.cancel();
    }
    this.#restartTimer = null;
    this.#restartTimerExpired = false;
  }

  #onRestartTimerExpire() {
    LOG("RestartOnLastWindowClosed.#onRestartTimerExpire - Timer Expired");

    this.#restartTimer = null;
    this.#restartTimerExpired = true;
    this.#maybeRestartBrowser();
  }

  #maybeRestartBrowser() {
    if (!this.#restartTimerExpired) {
      LOG(
        "RestartOnLastWindowClosed.#maybeRestartBrowser - Still waiting for " +
          "all windows to be closed and restartTimer to expire. " +
          "(not restarting)"
      );
      return;
    }

    if (lazy.AUS.currentState != Ci.nsIApplicationUpdateService.STATE_PENDING) {
      LOG(
        "RestartOnLastWindowClosed.#maybeRestartBrowser - No update ready. " +
          "(not restarting)"
      );
      return;
    }

    if (getElevationRequired()) {
      // We check for STATE_PENDING_ELEVATE elsewhere, but this is actually
      // different from that because it is technically possible that the user
      // gave permission to elevate, but we haven't actually elevated yet.
      // This is a bit of a corner case. We only call elevationOptedIn() right
      // before we restart to apply the update immediately. But it is possible
      // that something could stop the browser from shutting down.
      LOG(
        "RestartOnLastWindowClosed.#maybeRestartBrowser - This update will " +
          "require user elevation (not restarting)"
      );
      return;
    }

    if (this.#windowsAreOpen()) {
      LOG(
        "RestartOnLastWindowClosed.#maybeRestartBrowser - Window " +
          "unexpectedly still open! (not restarting)"
      );
      return;
    }

    if (!this.shouldEnable) {
      LOG(
        "RestartOnLastWindowClosed.#maybeRestartBrowser - Unexpectedly " +
          "attempted to restart when RestartOnLastWindowClosed ought to be " +
          "disabled! (not restarting)"
      );
      return;
    }

    LOG("RestartOnLastWindowClosed.#maybeRestartBrowser - Restarting now");
    Glean.update.noWindowAutoRestarts.add(1);
    Services.startup.quit(
      Ci.nsIAppStartup.eAttemptQuit |
        Ci.nsIAppStartup.eRestart |
        Ci.nsIAppStartup.eSilently
    );
  }
}
// Nothing actually uses this variable at the moment, but let's make sure that
// we hold the reference to the RestartOnLastWindowClosed instance somewhere.
// eslint-disable-next-line no-unused-vars
let restartOnLastWindowClosed = new RestartOnLastWindowClosed();
