Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/toolkit/mozapps/update/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 243 kB image not shown  

Quelle  UpdateService.sys.mjs   Sprache: unbekannt

 
/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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",
  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",
  "nsIApplicationUpdateService"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UM",
  "@mozilla.org/updates/update-manager;1",
  "nsIUpdateManager"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "CheckSvc",
  "@mozilla.org/updates/update-checker;1",
  "nsIUpdateChecker"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UpdateServiceStub",
  "@mozilla.org/updates/update-service-stub;1",
  "nsIApplicationUpdateServiceStub"
);
XPCOMUtils.defineLazyServiceGetter(
  lazy,
  "UpdateMutex",
  "@mozilla.org/updates/update-mutex;1",
  "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_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_INSTALL_LOCKOUT_ENABLED =
  "app.update.multiSessionInstallLockout.enabled";
const PREF_APP_UPDATE_INSTALL_LOCKOUT_TIMEOUT_MS =
  "app.update.multiSessionInstallLockout.timeoutMs";
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 FILE_UPDATE_TIMESTAMP = "update.timestamp";

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;

// 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;

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;

// 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

// Timestamps further than this many milliseconds in the future will be
// considered invalid.
const MAX_UPDATE_INSTALL_LOCKOUT_TIMEOUT_MS = 7 * 24 * 60 * 60 * 1000; // 1 week
const DEFAULT_UPDATE_INSTALL_LOCKOUT_TIMEOUT_MS = 24 * 60 * 60 * 1000; // 1 day

// 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;

/**
 * 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();

/**
 * 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;
  }
}

/**
 * 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.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.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
 *         UPDATE_CAN_USE_BITS_EXTERNAL and UPDATE_CAN_USE_BITS_NOTIFY telemetry
 *         probes. If this function is made to return other values, they should
 *         also be added to the labels lists for those probes in Histograms.json
 */
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]);
}

/**
 * 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.
 * @param   dir
 *          The patch directory where the update.status file should be
 *          written.
 * @param   state
 *          The state value to write.
 */
function writeStatusFile(dir, state) {
  let statusFile = dir.clone();
  statusFile.append(FILE_UPDATE_STATUS);
  writeStringToFile(statusFile, state);
}

/**
 * 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.
 * @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);
  writeStringToFile(versionFile, version);
}

/**
 * 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 status file says we are downloading, we should remove that
  // too, since we aren't doing that anymore.
  let readyUpdateDir = getReadyUpdateDir();
  let status = readStatusFile(readyUpdateDir);
  if (status == STATE_DOWNLOADING) {
    let statusFile = readyUpdateDir.clone();
    statusFile.append(FILE_UPDATE_STATUS);
    statusFile.remove(false);
  }
}

/**
 * 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.
 * @return true on success, false on failure.
 */
function writeStringToFile(file, text) {
  try {
    let fos = FileUtils.openSafeFileOutputStream(file);
    text += "\n";
    fos.write(text, text.length);
    FileUtils.closeSafeFileOutputStream(fos);
  } catch (e) {
    LOG(`writeStringToFile - Failed to write to file: "${file}". Error: ${e}"`);
    return false;
  }
  return true;
}

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") {
      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 true if the passed update is the same version or older than
 * currently installed Firefox version.
 */
function updateIsAtLeastAsOldAsCurrentVersion(update) {
  return updateIsAtLeastAsOldAs(
    update,
    Services.appinfo.version,
    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
   * @constructor
   */
  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":
--> --------------------

--> maximum size reached

--> --------------------

[ zur Elbe Produktseite wechseln0.47Quellennavigators  Analyse erneut starten  ]