Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


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

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

[ Dauer der Verarbeitung: 0.9 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge