summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/UpdateService.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/mozapps/update/UpdateService.sys.mjs')
-rw-r--r--toolkit/mozapps/update/UpdateService.sys.mjs7229
1 files changed, 7229 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/UpdateService.sys.mjs b/toolkit/mozapps/update/UpdateService.sys.mjs
new file mode 100644
index 0000000000..5e9caf090e
--- /dev/null
+++ b/toolkit/mozapps/update/UpdateService.sys.mjs
@@ -0,0 +1,7229 @@
+/* -*- 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",
+ CertUtils: "resource://gre/modules/CertUtils.sys.mjs",
+ DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+ WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
+ ctypes: "resource://gre/modules/ctypes.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"
+);
+
+if (AppConstants.ENABLE_WEBDRIVER) {
+ XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "Marionette",
+ "@mozilla.org/remote/marionette;1",
+ "nsIMarionette"
+ );
+
+ XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "RemoteAgent",
+ "@mozilla.org/remote/agent;1",
+ "nsIRemoteAgent"
+ );
+} else {
+ lazy.Marionette = { running: false };
+ lazy.RemoteAgent = { running: false };
+}
+
+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_BITS_ENABLED = "app.update.BITS.enabled";
+const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
+const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
+const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
+const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED =
+ "app.update.checkOnlyInstance.enabled";
+const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL =
+ "app.update.checkOnlyInstance.interval";
+const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT =
+ "app.update.checkOnlyInstance.timeout";
+const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting";
+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_LANGPACK_ENABLED = "app.update.langpack.enabled";
+const PREF_APP_UPDATE_LANGPACK_TIMEOUT = "app.update.langpack.timeout";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file";
+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_PROFILE_DIR = "ProfD";
+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_BT_RESULT = "bt.result";
+const FILE_LAST_UPDATE_LOG = "last-update.log";
+const FILE_UPDATES_XML = "updates.xml";
+const FILE_UPDATE_LOG = "update.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_MESSAGES = "update_messages.log";
+
+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 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.jsm 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 WRITE_ERROR_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,
+ WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION,
+];
+
+// 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;
+
+// This maps app IDs to their respective notification topic which signals when
+// the application's user interface has been displayed.
+const APPID_TO_TOPIC = {
+ // Firefox
+ "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored",
+ // SeaMonkey
+ "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored",
+ // Thunderbird
+ "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done",
+};
+
+// The interval for the update xml write deferred task.
+const XML_SAVER_INTERVAL_MS = 200;
+
+// How long after a patch has downloaded should we wait for language packs to
+// update before proceeding anyway.
+const LANGPACK_UPDATE_DEFAULT_TIMEOUT = 300000;
+
+// Interval between rechecks for other instances after the initial check finds
+// at least one other instance.
+const ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
+
+// Wait this long after detecting that another instance is running (having been
+// polling that entire time) before giving up and applying the update anyway.
+const ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS = 6 * 60 * 60 * 1000; // 6 hours
+
+// The other instance check timeout can be overridden via a pref, but we limit
+// that value to this so that the pref can't effectively disable the feature.
+const ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS = 2 * 24 * 60 * 60 * 1000; // 2 days
+
+// Values to use when polling for staging. See `pollForStagingEnd` for more
+// details.
+const STAGING_POLLING_MIN_INTERVAL_MS = 15 * 1000; // 15 seconds
+const STAGING_POLLING_MAX_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
+const STAGING_POLLING_ATTEMPTS_PER_INTERVAL = 5;
+const STAGING_POLLING_MAX_DURATION_MS = 1 * 60 * 60 * 1000; // 1 hour
+
+var gUpdateMutexHandle = null;
+// This is the file stream used for the log file.
+var gLogfileOutputStream;
+// 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();
+
+XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function aus_gLogEnabled() {
+ return (
+ Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
+ Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false)
+ );
+});
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "gLogfileEnabled",
+ function aus_gLogfileEnabled() {
+ return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
+ }
+);
+
+XPCOMUtils.defineLazyGetter(
+ lazy,
+ "gUpdateBundle",
+ function aus_gUpdateBundle() {
+ return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
+ }
+);
+
+/**
+ * gIsBackgroundTaskMode will be true if Firefox is currently running as a
+ * background task. Otherwise it will be false.
+ */
+XPCOMUtils.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;
+ }
+);
+
+/**
+ * Changes `nsIApplicationUpdateService.currentState` and causes
+ * `nsIApplicationUpdateService.stateTransition` to resolve.
+ */
+function transitionState(newState) {
+ if (newState == gUpdateState) {
+ LOG("transitionState - Not transitioning state because it isn't changing.");
+ return;
+ }
+ LOG(
+ `transitionState - "${lazy.AUS.getStateName(gUpdateState)}" -> ` +
+ `"${lazy.AUS.getStateName(newState)}".`
+ );
+ gUpdateState = newState;
+ // Assign the new Promise before we resolve the old one just to make sure that
+ // anything that runs as a result of `resolve` doesn't end up waiting on the
+ // Promise that already resolved.
+ let oldStateTransitionPromise = gStateTransitionPromise;
+ gStateTransitionPromise = new SelfContainedPromise();
+ oldStateTransitionPromise.resolve();
+}
+
+/**
+ * When a plain JS object is passed through xpconnect the other side sees a
+ * wrapped version of the object instead of the real object. Since these two
+ * objects are different they act as different keys for Map and WeakMap. However
+ * xpconnect gives us a way to get the underlying JS object from the wrapper so
+ * this function returns the JS object regardless of whether passed the JS
+ * object or its wrapper for use in places where it is unclear which one you
+ * have.
+ */
+function unwrap(obj) {
+ return obj.wrappedJSObject ?? obj;
+}
+
+/**
+ * When an update starts to download (and if the feature is enabled) the add-ons
+ * manager starts downloading updated language packs for the new application
+ * version. A promise is used to track whether those updates are complete so the
+ * front-end is only notified that an application update is ready once the
+ * language pack updates have been staged.
+ *
+ * In order to be able to access that promise from various places in the update
+ * service they are cached in this map using the nsIUpdate object as a weak
+ * owner. Note that the key should always be the result of calling the above
+ * unwrap function on the nsIUpdate to ensure a consistent object is used as the
+ * key.
+ *
+ * When the language packs finish staging the nsIUpdate entriy is removed from
+ * this map so if the entry is still there then language pack updates are in
+ * progress.
+ */
+const LangPackUpdates = new WeakMap();
+
+/**
+ * When we're polling to see if other running instances of the application have
+ * exited, there's no need to ever start polling again in parallel. To prevent
+ * doing that, we keep track of the promise that resolves when polling completes
+ * and return that if a second simultaneous poll is requested, so that the
+ * multiple callers end up waiting for the same promise to resolve.
+ */
+let gOtherInstancePollPromise;
+
+/**
+ * Query the update sync manager to see if another instance of this same
+ * installation of this application is currently running, under the context of
+ * any operating system user (not just the current one).
+ * This function immediately returns the current, instantaneous status of any
+ * other instances.
+ *
+ * @return true if at least one other instance is running, false if not
+ */
+function isOtherInstanceRunning(callback) {
+ const checkEnabled = Services.prefs.getBoolPref(
+ PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED,
+ true
+ );
+ if (!checkEnabled) {
+ LOG("isOtherInstanceRunning - disabled by pref, skipping check");
+ return false;
+ }
+
+ try {
+ let syncManager = Cc[
+ "@mozilla.org/updates/update-sync-manager;1"
+ ].getService(Ci.nsIUpdateSyncManager);
+ return syncManager.isOtherInstanceRunning();
+ } catch (ex) {
+ LOG(`isOtherInstanceRunning - sync manager failed with exception: ${ex}`);
+ return false;
+ }
+}
+
+/**
+ * Query the update sync manager to see if another instance of this same
+ * installation of this application is currently running, under the context of
+ * any operating system user (not just the one running this instance).
+ * This function polls for the status of other instances continually
+ * (asynchronously) until either none exist or a timeout expires.
+ *
+ * @return a Promise that resolves with false if at any point during polling no
+ * other instances can be found, or resolves with true if the timeout
+ * expires when other instances are still running
+ */
+function waitForOtherInstances() {
+ // If we're already in the middle of a poll, reuse it rather than start again.
+ if (gOtherInstancePollPromise) {
+ return gOtherInstancePollPromise;
+ }
+
+ let timeout = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT,
+ ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS
+ );
+ // Don't allow the pref to set a super high timeout and break this feature.
+ if (timeout > ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS) {
+ timeout = ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS;
+ }
+
+ let interval = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL,
+ ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS
+ );
+ // Don't allow an interval longer than the timeout.
+ interval = Math.min(interval, timeout);
+
+ let iterations = 0;
+ const maxIterations = Math.ceil(timeout / interval);
+
+ gOtherInstancePollPromise = new Promise(function (resolve, reject) {
+ let poll = function () {
+ iterations++;
+ if (!isOtherInstanceRunning()) {
+ LOG("waitForOtherInstances - no other instances found, exiting");
+ resolve(false);
+ gOtherInstancePollPromise = undefined;
+ } else if (iterations >= maxIterations) {
+ LOG(
+ "waitForOtherInstances - timeout expired while other instances " +
+ "are still running"
+ );
+ resolve(true);
+ gOtherInstancePollPromise = undefined;
+ } else if (iterations + 1 == maxIterations && timeout % interval != 0) {
+ // In case timeout isn't a multiple of interval, set the next timeout
+ // for the remainder of the time rather than for the usual interval.
+ lazy.setTimeout(poll, timeout % interval);
+ } else {
+ lazy.setTimeout(poll, interval);
+ }
+ };
+
+ LOG("waitForOtherInstances - beginning polling");
+ poll();
+ });
+
+ return gOtherInstancePollPromise;
+}
+
+/**
+ * Tests to make sure that we can write to a given directory.
+ *
+ * @param updateTestFile a test file in the directory that needs to be tested.
+ * @param createDirectory whether a test directory should be created.
+ * @throws if we don't have right access to the directory.
+ */
+function testWriteAccess(updateTestFile, createDirectory) {
+ const NORMAL_FILE_TYPE = Ci.nsIFile.NORMAL_FILE_TYPE;
+ const DIRECTORY_TYPE = Ci.nsIFile.DIRECTORY_TYPE;
+ if (updateTestFile.exists()) {
+ updateTestFile.remove(false);
+ }
+ updateTestFile.create(
+ createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE,
+ createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE
+ );
+ updateTestFile.remove(false);
+}
+
+/**
+ * Windows only function that closes a Win32 handle.
+ *
+ * @param handle The handle to close
+ */
+function closeHandle(handle) {
+ if (handle) {
+ let lib = lazy.ctypes.open("kernel32.dll");
+ let CloseHandle = lib.declare(
+ "CloseHandle",
+ lazy.ctypes.winapi_abi,
+ lazy.ctypes.int32_t /* success */,
+ lazy.ctypes.void_t.ptr
+ ); /* handle */
+ CloseHandle(handle);
+ lib.close();
+ }
+}
+
+/**
+ * Windows only function that creates a mutex.
+ *
+ * @param aName
+ * The name for the mutex.
+ * @param aAllowExisting
+ * If false the function will close the handle and return null.
+ * @return The Win32 handle to the mutex.
+ */
+function createMutex(aName, aAllowExisting = true) {
+ if (AppConstants.platform != "win") {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ const INITIAL_OWN = 1;
+ const ERROR_ALREADY_EXISTS = 0xb7;
+ let lib = lazy.ctypes.open("kernel32.dll");
+ let CreateMutexW = lib.declare(
+ "CreateMutexW",
+ lazy.ctypes.winapi_abi,
+ lazy.ctypes.void_t.ptr /* return handle */,
+ lazy.ctypes.void_t.ptr /* security attributes */,
+ lazy.ctypes.int32_t /* initial owner */,
+ lazy.ctypes.char16_t.ptr
+ ); /* name */
+
+ let handle = CreateMutexW(null, INITIAL_OWN, aName);
+ let alreadyExists = lazy.ctypes.winLastError == ERROR_ALREADY_EXISTS;
+ if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) {
+ closeHandle(handle);
+ handle = null;
+ }
+ lib.close();
+
+ if (handle && handle.isNull()) {
+ handle = null;
+ }
+
+ return handle;
+}
+
+/**
+ * Windows only function that determines a unique mutex name for the
+ * installation.
+ *
+ * @param aGlobal
+ * true if the function should return a global mutex. A global mutex is
+ * valid across different sessions.
+ * @return Global mutex path
+ */
+function getPerInstallationMutexName(aGlobal = true) {
+ if (AppConstants.platform != "win") {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA1);
+
+ let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile);
+
+ let converter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var data = converter.convertToByteArray(exeFile.path.toLowerCase());
+
+ hasher.update(data, data.length);
+ return (
+ (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true)
+ );
+}
+
+/**
+ * Whether or not the current instance has the update mutex. The update mutex
+ * gives protection against 2 applications from the same installation updating:
+ * 1) Running multiple profiles from the same installation path
+ * 2) Two applications running in 2 different user sessions from the same path
+ *
+ * @return true if this instance holds the update mutex
+ */
+function hasUpdateMutex() {
+ if (AppConstants.platform != "win") {
+ return true;
+ }
+ if (!gUpdateMutexHandle) {
+ gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false);
+ }
+ return !!gUpdateMutexHandle;
+}
+
+/**
+ * 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();
+}
+
+/**
+ * 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") {
+ LOG(
+ "getCanApplyUpdates - bypass the write since elevation can be used " +
+ "on Mac OS X"
+ );
+ return true;
+ }
+
+ if (shouldUseService()) {
+ LOG(
+ "getCanApplyUpdates - bypass the write checks because the Windows " +
+ "Maintenance Service can be used"
+ );
+ return true;
+ }
+
+ try {
+ if (AppConstants.platform == "win") {
+ // On Windows when the maintenance service isn't used updates can still be
+ // performed in a location requiring admin privileges by the client
+ // accepting a UAC prompt from an elevation request made by the updater.
+ // Whether the client can elevate (e.g. has a split token) is determined
+ // in nsXULAppInfo::GetUserCanElevate which is located in nsAppRunner.cpp.
+ let userCanElevate = Services.appinfo.QueryInterface(
+ Ci.nsIWinAppHelper
+ ).userCanElevate;
+ if (lazy.gIsBackgroundTaskMode) {
+ LOG(
+ "getCanApplyUpdates - in background task mode, assuming user can't elevate"
+ );
+ userCanElevate = false;
+ }
+ if (!userCanElevate) {
+ // if we're unable to create the test file this will throw an exception.
+ let appDirTestFile = getAppBaseDir();
+ appDirTestFile.append(FILE_UPDATE_TEST);
+ LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path);
+ if (appDirTestFile.exists()) {
+ appDirTestFile.remove(false);
+ }
+ appDirTestFile.create(
+ Ci.nsIFile.NORMAL_FILE_TYPE,
+ FileUtils.PERMS_FILE
+ );
+ appDirTestFile.remove(false);
+ }
+ }
+ } catch (e) {
+ LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e);
+ // No write access to the installation directory
+ 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.
+ */
+XPCOMUtils.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) {
+ if (lazy.gLogEnabled) {
+ dump("*** AUS:SVC " + string + "\n");
+ if (!Cu.isInAutomation) {
+ Services.console.logStringMessage("AUS:SVC " + string);
+ }
+
+ if (lazy.gLogfileEnabled) {
+ if (!gLogfileOutputStream) {
+ let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
+ logfile.append(FILE_UPDATE_MESSAGES);
+ gLogfileOutputStream = FileUtils.openAtomicFileOutputStream(logfile);
+ }
+
+ try {
+ let encoded = new TextEncoder().encode(string + "\n");
+ gLogfileOutputStream.write(encoded, encoded.length);
+ gLogfileOutputStream.flush();
+ } catch (e) {
+ dump("*** AUS:SVC Unable to write to messages file: " + e + "\n");
+ Services.console.logStringMessage(
+ "AUS:SVC Unable to write to messages file: " + e
+ );
+ }
+ }
+ }
+}
+
+/**
+ * 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;
+ }
+ }
+
+ return FileUtils.getDir(KEY_UPDROOT, pathArray, true);
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Reads the binary transparency result file from the given directory.
+ * Removes the file if it is present (so don't call this twice and expect a
+ * result the second time).
+ * @param dir
+ * The dir to look for an update.bt file in
+ * @return A error code from verifying binary transparency information or null
+ * if the file was not present (indicating there was no error).
+ */
+function readBinaryTransparencyResult(dir) {
+ let binaryTransparencyResultFile = dir.clone();
+ binaryTransparencyResultFile.append(FILE_BT_RESULT);
+ let result = readStringFromFile(binaryTransparencyResultFile);
+ LOG(
+ "readBinaryTransparencyResult - result: " +
+ result +
+ ", path: " +
+ binaryTransparencyResultFile.path
+ );
+ // If result is non-null, the file exists. We should remove it to avoid
+ // double-reporting this result.
+ if (result) {
+ binaryTransparencyResultFile.remove(false);
+ }
+ return result;
+}
+
+/**
+ * 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 file for debugging purposes.
+ let updateLogFile = updateDir.clone();
+ updateLogFile.append(FILE_UPDATE_LOG);
+ if (updateLogFile.exists()) {
+ let dir = updateDir.parent;
+ let logFile = dir.clone();
+ logFile.append(FILE_LAST_UPDATE_LOG);
+ if (logFile.exists()) {
+ try {
+ logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG);
+ } catch (e) {
+ LOG(
+ "cleanUpReadyUpdateDir - failed to rename file " +
+ logFile.path +
+ " to " +
+ FILE_BACKUP_UPDATE_LOG
+ );
+ }
+ }
+
+ try {
+ updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG);
+ } catch (e) {
+ LOG(
+ "cleanUpReadyUpdateDir - failed to rename file " +
+ updateLogFile.path +
+ " to " +
+ FILE_LAST_UPDATE_LOG
+ );
+ }
+ }
+
+ if (aRemovePatchFiles) {
+ let dirEntries = updateDir.directoryEntries;
+ 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 = updateDir.directoryEntries;
+ 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.readyUpdate) {
+ LOG("cleanupReadyUpdate - Clearing readyUpdate");
+ lazy.UM.addUpdateToHistory(lazy.UM.readyUpdate);
+ lazy.UM.readyUpdate = null;
+ }
+ lazy.UM.saveUpdates();
+
+ let readyUpdateDir = getReadyUpdateDir();
+ let shouldSetDownloadingStatus =
+ lazy.UM.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.
+ */
+function cleanupDownloadingUpdate() {
+ // Move the update from the Active Update list into the Past Updates list.
+ if (lazy.UM.downloadingUpdate) {
+ LOG("cleanupDownloadingUpdate - Clearing downloadingUpdate.");
+ lazy.UM.addUpdateToHistory(lazy.UM.downloadingUpdate);
+ lazy.UM.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();
+ }
+}
+
+/**
+ * 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.
+ */
+function cleanupActiveUpdates() {
+ // Move the update from the Active Update list into the Past Updates list.
+ if (lazy.UM.readyUpdate) {
+ LOG("cleanupActiveUpdates - Clearing readyUpdate");
+ lazy.UM.addUpdateToHistory(lazy.UM.readyUpdate);
+ lazy.UM.readyUpdate = null;
+ }
+ if (lazy.UM.downloadingUpdate) {
+ LOG("cleanupActiveUpdates - Clearing downloadingUpdate.");
+ lazy.UM.addUpdateToHistory(lazy.UM.downloadingUpdate);
+ lazy.UM.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)) {
+ LOG(
+ "handleUpdateFailure - Failure is a write error. Setting state to pending"
+ );
+ writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING));
+ 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 {
+ LOG(
+ "handleUpdateFailure - Failure because elevation was cancelled. " +
+ "again by setting status to pending."
+ );
+ writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING));
+ }
+ 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.downloadingUpdate || lazy.UM.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 lazy.AUS.stopDownload();
+ 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 success = await lazy.AUS.downloadUpdate(update);
+ if (!success) {
+ LOG(
+ "handleFallbackToCompleteUpdate - Starting complete patch download " +
+ "failed. Cleaning up downloading patch."
+ );
+ 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.jsm
+ 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);
+ }
+ }
+ let binaryTransparencyResult = readBinaryTransparencyResult(
+ getReadyUpdateDir()
+ );
+ if (binaryTransparencyResult) {
+ AUSTLMY.pingBinaryTransparencyResult(
+ suffix,
+ parseInt(binaryTransparencyResult)
+ );
+ }
+ 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.readyUpdate ||
+ !lazy.UM.readyUpdate.appVersion ||
+ !lazy.UM.readyUpdate.buildID
+ ) {
+ return false;
+ }
+ return updateIsAtLeastAsOldAs(
+ update,
+ lazy.UM.readyUpdate.appVersion,
+ lazy.UM.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.refreshUpdateStatus();
+ return;
+ }
+
+ if (readStatusFile(getReadyUpdateDir()) != STATE_APPLYING) {
+ lazy.UM.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);
+}
+
+/**
+ * Update Patch
+ * @param patch
+ * A <patch> element to initialize this object with
+ * @throws if patch has a size of 0
+ * @constructor
+ */
+function UpdatePatch(patch) {
+ this._properties = {};
+ this.errorCode = 0;
+ this.finalURL = null;
+ this.state = STATE_NONE;
+
+ for (let i = 0; i < patch.attributes.length; ++i) {
+ var attr = patch.attributes.item(i);
+ // If an undefined value is saved to the xml file it will be a string when
+ // it is read from the xml file.
+ if (attr.value == "undefined") {
+ continue;
+ }
+ switch (attr.name) {
+ case "xmlns":
+ // Don't save the XML namespace.
+ break;
+ case "selected":
+ this.selected = attr.value == "true";
+ break;
+ case "size":
+ if (0 == parseInt(attr.value)) {
+ LOG("UpdatePatch:init - 0-sized patch!");
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+ this[attr.name] = attr.value;
+ break;
+ case "errorCode":
+ if (attr.value) {
+ let val = parseInt(attr.value);
+ // This will evaluate to false if the value is 0 but that's ok since
+ // this.errorCode is set to the default of 0 above.
+ if (val) {
+ this.errorCode = val;
+ }
+ }
+ break;
+ case "finalURL":
+ case "state":
+ case "type":
+ case "URL":
+ this[attr.name] = attr.value;
+ break;
+ default:
+ if (!this._attrNames.includes(attr.name)) {
+ // Set nsIPropertyBag properties that were read from the xml file.
+ this.setProperty(attr.name, attr.value);
+ }
+ break;
+ }
+ }
+}
+UpdatePatch.prototype = {
+ // nsIUpdatePatch attribute names used to prevent nsIWritablePropertyBag from
+ // over writing nsIUpdatePatch attributes.
+ _attrNames: [
+ "errorCode",
+ "finalURL",
+ "selected",
+ "size",
+ "state",
+ "type",
+ "URL",
+ ],
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ serialize: function UpdatePatch_serialize(updates) {
+ var patch = updates.createElementNS(URI_UPDATE_NS, "patch");
+ patch.setAttribute("size", this.size);
+ patch.setAttribute("type", this.type);
+ patch.setAttribute("URL", this.URL);
+ // Don't write an errorCode if it evaluates to false since 0 is the same as
+ // no error code.
+ if (this.errorCode) {
+ patch.setAttribute("errorCode", this.errorCode);
+ }
+ // finalURL is not available until after the download has started
+ if (this.finalURL) {
+ patch.setAttribute("finalURL", this.finalURL);
+ }
+ // The selected patch is the only patch that should have this attribute.
+ if (this.selected) {
+ patch.setAttribute("selected", this.selected);
+ }
+ if (this.state != STATE_NONE) {
+ patch.setAttribute("state", this.state);
+ }
+
+ for (let [name, value] of Object.entries(this._properties)) {
+ if (value.present && !this._attrNames.includes(name)) {
+ patch.setAttribute(name, value.data);
+ }
+ }
+ return patch;
+ },
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ setProperty: function UpdatePatch_setProperty(name, value) {
+ if (this._attrNames.includes(name)) {
+ throw Components.Exception(
+ "Illegal value '" +
+ name +
+ "' (attribute exists on nsIUpdatePatch) " +
+ "when calling method: [nsIWritablePropertyBag::setProperty]",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ this._properties[name] = { data: value, present: true };
+ },
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ deleteProperty: function UpdatePatch_deleteProperty(name) {
+ if (this._attrNames.includes(name)) {
+ throw Components.Exception(
+ "Illegal value '" +
+ name +
+ "' (attribute exists on nsIUpdatePatch) " +
+ "when calling method: [nsIWritablePropertyBag::deleteProperty]",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ if (name in this._properties) {
+ this._properties[name].present = false;
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ *
+ * Note: this only contains the nsIPropertyBag name / value pairs and not the
+ * nsIUpdatePatch name / value pairs.
+ */
+ get enumerator() {
+ return this.enumerate();
+ },
+
+ *enumerate() {
+ // An nsISupportsInterfacePointer is used so creating an array using
+ // Array.from will retain the QueryInterface for nsIProperty.
+ let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
+ Ci.nsISupportsInterfacePointer
+ );
+ let qi = ChromeUtils.generateQI(["nsIProperty"]);
+ for (let [name, value] of Object.entries(this._properties)) {
+ if (value.present && !this._attrNames.includes(name)) {
+ // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
+ // elements are nsIProperty objects. Calling QueryInterface for
+ // nsIProperty on the object doesn't return to the caller an object that
+ // is already queried to nsIProperty but do it just in case it is fixed
+ // at some point.
+ ip.data = { name, value: value.data, QueryInterface: qi };
+ yield ip.data.QueryInterface(Ci.nsIProperty);
+ }
+ }
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ *
+ * Note: returns null instead of throwing when the property doesn't exist to
+ * simplify code and to silence warnings in debug builds.
+ */
+ getProperty: function UpdatePatch_getProperty(name) {
+ if (this._attrNames.includes(name)) {
+ throw Components.Exception(
+ "Illegal value '" +
+ name +
+ "' (attribute exists on nsIUpdatePatch) " +
+ "when calling method: [nsIWritablePropertyBag::getProperty]",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ if (name in this._properties && this._properties[name].present) {
+ return this._properties[name].data;
+ }
+ return null;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIUpdatePatch",
+ "nsIPropertyBag",
+ "nsIWritablePropertyBag",
+ ]),
+};
+
+/**
+ * Update
+ * Implements nsIUpdate
+ * @param update
+ * An <update> element to initialize this object with
+ * @throws if the update contains no patches
+ * @constructor
+ */
+function Update(update) {
+ this._patches = [];
+ this._properties = {};
+ this.isCompleteUpdate = false;
+ this.channel = "default";
+ this.promptWaitTime = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_PROMPTWAITTIME,
+ 43200
+ );
+ this.unsupported = false;
+
+ // Null <update>, assume this is a message container and do no
+ // further initialization
+ if (!update) {
+ return;
+ }
+
+ for (let i = 0; i < update.childNodes.length; ++i) {
+ let patchElement = update.childNodes.item(i);
+ if (
+ patchElement.nodeType != patchElement.ELEMENT_NODE ||
+ patchElement.localName != "patch"
+ ) {
+ continue;
+ }
+
+ let patch;
+ try {
+ patch = new UpdatePatch(patchElement);
+ } catch (e) {
+ continue;
+ }
+ this._patches.push(patch);
+ }
+
+ if (!this._patches.length && !update.hasAttribute("unsupported")) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ // Set the installDate value with the current time. If the update has an
+ // installDate attribute this will be replaced with that value if it doesn't
+ // equal 0.
+ this.installDate = new Date().getTime();
+ this.patchCount = this._patches.length;
+
+ for (let i = 0; i < update.attributes.length; ++i) {
+ let attr = update.attributes.item(i);
+ if (attr.name == "xmlns" || attr.value == "undefined") {
+ // Don't save the XML namespace or undefined values.
+ // If an undefined value is saved to the xml file it will be a string when
+ // it is read from the xml file.
+ continue;
+ } else if (attr.name == "detailsURL") {
+ this.detailsURL = attr.value;
+ } else if (attr.name == "installDate" && attr.value) {
+ let val = parseInt(attr.value);
+ if (val) {
+ this.installDate = val;
+ }
+ } else if (attr.name == "errorCode" && attr.value) {
+ let val = parseInt(attr.value);
+ if (val) {
+ // Set the value of |_errorCode| instead of |errorCode| since
+ // selectedPatch won't be available at this point and normally the
+ // nsIUpdatePatch will provide the errorCode.
+ this._errorCode = val;
+ }
+ } else if (attr.name == "isCompleteUpdate") {
+ this.isCompleteUpdate = attr.value == "true";
+ } else if (attr.name == "promptWaitTime") {
+ if (!isNaN(attr.value)) {
+ this.promptWaitTime = parseInt(attr.value);
+ }
+ } else if (attr.name == "unsupported") {
+ this.unsupported = attr.value == "true";
+ } else {
+ switch (attr.name) {
+ case "appVersion":
+ case "buildID":
+ case "channel":
+ case "displayVersion":
+ case "elevationFailure":
+ case "name":
+ case "previousAppVersion":
+ case "serviceURL":
+ case "statusText":
+ case "type":
+ this[attr.name] = attr.value;
+ break;
+ default:
+ if (!this._attrNames.includes(attr.name)) {
+ // Set nsIPropertyBag properties that were read from the xml file.
+ this.setProperty(attr.name, attr.value);
+ }
+ break;
+ }
+ }
+ }
+
+ if (!this.previousAppVersion) {
+ this.previousAppVersion = Services.appinfo.version;
+ }
+
+ if (!this.elevationFailure) {
+ this.elevationFailure = false;
+ }
+
+ if (!this.detailsURL) {
+ try {
+ // Try using a default details URL supplied by the distribution
+ // if the update XML does not supply one.
+ this.detailsURL = Services.urlFormatter.formatURLPref(
+ PREF_APP_UPDATE_URL_DETAILS
+ );
+ } catch (e) {
+ this.detailsURL = "";
+ }
+ }
+
+ if (!this.displayVersion) {
+ this.displayVersion = this.appVersion;
+ }
+
+ if (!this.name) {
+ // When the update doesn't provide a name fallback to using
+ // "<App Name> <Update App Version>"
+ let brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES);
+ let appName = brandBundle.GetStringFromName("brandShortName");
+ this.name = lazy.gUpdateBundle.formatStringFromName("updateName", [
+ appName,
+ this.displayVersion,
+ ]);
+ }
+}
+Update.prototype = {
+ // nsIUpdate attribute names used to prevent nsIWritablePropertyBag from over
+ // writing nsIUpdate attributes.
+ _attrNames: [
+ "appVersion",
+ "buildID",
+ "channel",
+ "detailsURL",
+ "displayVersion",
+ "elevationFailure",
+ "errorCode",
+ "installDate",
+ "isCompleteUpdate",
+ "name",
+ "previousAppVersion",
+ "promptWaitTime",
+ "serviceURL",
+ "state",
+ "statusText",
+ "type",
+ "unsupported",
+ ],
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getPatchAt: function Update_getPatchAt(index) {
+ return this._patches[index];
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ *
+ * We use a copy of the state cached on this object in |_state| only when
+ * there is no selected patch, i.e. in the case when we could not load
+ * active updates from the update manager for some reason but still have
+ * the update.status file to work with.
+ */
+ _state: "",
+ get state() {
+ if (this.selectedPatch) {
+ return this.selectedPatch.state;
+ }
+ return this._state;
+ },
+ set state(state) {
+ if (this.selectedPatch) {
+ this.selectedPatch.state = state;
+ }
+ this._state = state;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ *
+ * We use a copy of the errorCode cached on this object in |_errorCode| only
+ * when there is no selected patch, i.e. in the case when we could not load
+ * active updates from the update manager for some reason but still have
+ * the update.status file to work with.
+ */
+ _errorCode: 0,
+ get errorCode() {
+ if (this.selectedPatch) {
+ return this.selectedPatch.errorCode;
+ }
+ return this._errorCode;
+ },
+ set errorCode(errorCode) {
+ if (this.selectedPatch) {
+ this.selectedPatch.errorCode = errorCode;
+ }
+ this._errorCode = errorCode;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get selectedPatch() {
+ for (let i = 0; i < this.patchCount; ++i) {
+ if (this._patches[i].selected) {
+ return this._patches[i];
+ }
+ }
+ return null;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ serialize: function Update_serialize(updates) {
+ // If appVersion isn't defined just return null. This happens when cleaning
+ // up invalid updates (e.g. incorrect channel).
+ if (!this.appVersion) {
+ return null;
+ }
+ let update = updates.createElementNS(URI_UPDATE_NS, "update");
+ update.setAttribute("appVersion", this.appVersion);
+ update.setAttribute("buildID", this.buildID);
+ update.setAttribute("channel", this.channel);
+ update.setAttribute("detailsURL", this.detailsURL);
+ update.setAttribute("displayVersion", this.displayVersion);
+ update.setAttribute("installDate", this.installDate);
+ update.setAttribute("isCompleteUpdate", this.isCompleteUpdate);
+ update.setAttribute("name", this.name);
+ update.setAttribute("previousAppVersion", this.previousAppVersion);
+ update.setAttribute("promptWaitTime", this.promptWaitTime);
+ update.setAttribute("serviceURL", this.serviceURL);
+ update.setAttribute("type", this.type);
+
+ if (this.statusText) {
+ update.setAttribute("statusText", this.statusText);
+ }
+ if (this.unsupported) {
+ update.setAttribute("unsupported", this.unsupported);
+ }
+ if (this.elevationFailure) {
+ update.setAttribute("elevationFailure", this.elevationFailure);
+ }
+
+ for (let [name, value] of Object.entries(this._properties)) {
+ if (value.present && !this._attrNames.includes(name)) {
+ update.setAttribute(name, value.data);
+ }
+ }
+
+ for (let i = 0; i < this.patchCount; ++i) {
+ update.appendChild(this.getPatchAt(i).serialize(updates));
+ }
+
+ updates.documentElement.appendChild(update);
+ return update;
+ },
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ setProperty: function Update_setProperty(name, value) {
+ if (this._attrNames.includes(name)) {
+ throw Components.Exception(
+ "Illegal value '" +
+ name +
+ "' (attribute exists on nsIUpdate) " +
+ "when calling method: [nsIWritablePropertyBag::setProperty]",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ this._properties[name] = { data: value, present: true };
+ },
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ deleteProperty: function Update_deleteProperty(name) {
+ if (this._attrNames.includes(name)) {
+ throw Components.Exception(
+ "Illegal value '" +
+ name +
+ "' (attribute exists on nsIUpdate) " +
+ "when calling method: [nsIWritablePropertyBag::deleteProperty]",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ if (name in this._properties) {
+ this._properties[name].present = false;
+ } else {
+ throw Components.Exception("", Cr.NS_ERROR_FAILURE);
+ }
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ *
+ * Note: this only contains the nsIPropertyBag name value / pairs and not the
+ * nsIUpdate name / value pairs.
+ */
+ get enumerator() {
+ return this.enumerate();
+ },
+
+ *enumerate() {
+ // An nsISupportsInterfacePointer is used so creating an array using
+ // Array.from will retain the QueryInterface for nsIProperty.
+ let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
+ Ci.nsISupportsInterfacePointer
+ );
+ let qi = ChromeUtils.generateQI(["nsIProperty"]);
+ for (let [name, value] of Object.entries(this._properties)) {
+ if (value.present && !this._attrNames.includes(name)) {
+ // The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
+ // elements are nsIProperty objects. Calling QueryInterface for
+ // nsIProperty on the object doesn't return to the caller an object that
+ // is already queried to nsIProperty but do it just in case it is fixed
+ // at some point.
+ ip.data = { name, value: value.data, QueryInterface: qi };
+ yield ip.data.QueryInterface(Ci.nsIProperty);
+ }
+ }
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ * Note: returns null instead of throwing when the property doesn't exist to
+ * simplify code and to silence warnings in debug builds.
+ */
+ getProperty: function Update_getProperty(name) {
+ if (this._attrNames.includes(name)) {
+ throw Components.Exception(
+ "Illegal value '" +
+ name +
+ "' (attribute exists on nsIUpdate) " +
+ "when calling method: [nsIWritablePropertyBag::getProperty]",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ if (name in this._properties && this._properties[name].present) {
+ return this._properties[name].data;
+ }
+ return null;
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIUpdate",
+ "nsIPropertyBag",
+ "nsIWritablePropertyBag",
+ ]),
+};
+
+/**
+ * UpdateService
+ * A Service for managing the discovery and installation of software updates.
+ * @constructor
+ */
+export function UpdateService() {
+ LOG("Creating UpdateService");
+ // The observor notification to shut down the service must be before
+ // profile-before-change since nsIUpdateManager uses profile-before-change
+ // to shutdown and write the update xml files.
+ Services.obs.addObserver(this, "quit-application");
+ // This one call observes PREF_APP_UPDATE_LOG and PREF_APP_UPDATE_LOG_FILE
+ Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this);
+
+ this._logStatus();
+}
+
+UpdateService.prototype = {
+ /**
+ * The downloader we are using to download updates. There is only ever one of
+ * these.
+ */
+ _downloader: null,
+
+ /**
+ * Whether or not the service registered the "online" observer.
+ */
+ _registeredOnlineObserver: false,
+
+ /**
+ * The current number of consecutive socket errors
+ */
+ _consecutiveSocketErrors: 0,
+
+ /**
+ * A timer used to retry socket errors
+ */
+ _retryTimer: null,
+
+ /**
+ * Whether or not a background update check was initiated by the
+ * application update timer notification.
+ */
+ _isNotify: true,
+
+ /**
+ * Handle Observer Service notifications
+ * @param subject
+ * The subject of the notification
+ * @param topic
+ * The notification name
+ * @param data
+ * Additional data
+ */
+ observe: async function AUS_observe(subject, topic, data) {
+ switch (topic) {
+ case "post-update-processing":
+ // This pref was not cleared out of profiles after it stopped being used
+ // (Bug 1420514), so clear it out on the next update to avoid confusion
+ // regarding its use.
+ Services.prefs.clearUserPref("app.update.enabled");
+ Services.prefs.clearUserPref("app.update.BITS.inTrialGroup");
+
+ // Background tasks do not notify any delayed startup notifications.
+ if (
+ !lazy.gIsBackgroundTaskMode &&
+ Services.appinfo.ID in APPID_TO_TOPIC
+ ) {
+ // Delay post-update processing to ensure that possible update
+ // dialogs are shown in front of the app window, if possible.
+ // See bug 311614.
+ Services.obs.addObserver(this, APPID_TO_TOPIC[Services.appinfo.ID]);
+ break;
+ }
+ // intentional fallthrough
+ case "sessionstore-windows-restored":
+ case "mail-startup-done":
+ // Background tasks do not notify any delayed startup notifications.
+ if (
+ !lazy.gIsBackgroundTaskMode &&
+ Services.appinfo.ID in APPID_TO_TOPIC
+ ) {
+ Services.obs.removeObserver(
+ this,
+ APPID_TO_TOPIC[Services.appinfo.ID]
+ );
+ }
+ // intentional fallthrough
+ case "test-post-update-processing":
+ // Clean up any extant updates
+ await this._postUpdateProcessing();
+ break;
+ case "network:offline-status-changed":
+ await this._offlineStatusChanged(data);
+ break;
+ case "nsPref:changed":
+ if (data == PREF_APP_UPDATE_LOG || data == PREF_APP_UPDATE_LOG_FILE) {
+ lazy.gLogEnabled; // Assigning this before it is lazy-loaded is an error.
+ lazy.gLogEnabled =
+ Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
+ Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
+ }
+ if (data == PREF_APP_UPDATE_LOG_FILE) {
+ lazy.gLogfileEnabled; // Assigning this before it is lazy-loaded is an
+ // error.
+ lazy.gLogfileEnabled = Services.prefs.getBoolPref(
+ PREF_APP_UPDATE_LOG_FILE,
+ false
+ );
+ if (lazy.gLogfileEnabled) {
+ this._logStatus();
+ }
+ }
+ break;
+ case "quit-application":
+ Services.obs.removeObserver(this, topic);
+ Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this);
+
+ if (AppConstants.platform == "win" && gUpdateMutexHandle) {
+ // If we hold the update mutex, let it go!
+ // The OS would clean this up sometime after shutdown,
+ // but that would have no guarantee on timing.
+ closeHandle(gUpdateMutexHandle);
+ gUpdateMutexHandle = null;
+ }
+ if (this._retryTimer) {
+ this._retryTimer.cancel();
+ }
+
+ // When downloading an update with nsIIncrementalDownload the download
+ // is stopped when the quit-application observer notification is
+ // received and networking hasn't started to shutdown. The download will
+ // be resumed the next time the application starts. Downloads using
+ // Windows BITS are not stopped since they don't require Firefox to be
+ // running to perform the download.
+ if (this._downloader) {
+ if (this._downloader.usingBits) {
+ await this._downloader.cleanup();
+ } else {
+ // stopDownload() calls _downloader.cleanup()
+ await this.stopDownload();
+ }
+ }
+ // Prevent leaking the downloader (bug 454964)
+ this._downloader = null;
+ // In case any update checks are in progress.
+ lazy.CheckSvc.stopAllChecks();
+
+ if (gLogfileOutputStream) {
+ gLogfileOutputStream.close();
+ }
+ break;
+ case "test-close-handle-update-mutex":
+ if (Cu.isInAutomation) {
+ if (AppConstants.platform == "win" && gUpdateMutexHandle) {
+ LOG("UpdateService:observe - closing mutex handle for testing");
+ closeHandle(gUpdateMutexHandle);
+ gUpdateMutexHandle = null;
+ }
+ }
+ break;
+ }
+ },
+
+ /**
+ * The following needs to happen during the post-update-processing
+ * notification from nsUpdateServiceStub.js:
+ * 1. post update processing
+ * 2. resume of a download that was in progress during a previous session
+ * 3. start of a complete update download after the failure to apply a partial
+ * update
+ */
+
+ /**
+ * Perform post-processing on updates lingering in the updates directory
+ * from a previous application session - either report install failures (and
+ * optionally attempt to fetch a different version if appropriate) or
+ * notify the user of install success.
+ */
+ /* eslint-disable-next-line complexity */
+ _postUpdateProcessing: async function AUS__postUpdateProcessing() {
+ if (this.disabled) {
+ // This function is a point when we can potentially enter the update
+ // system, even with update disabled. Make sure that we do not continue
+ // because update code can have side effects that are visible to the user
+ // and give the impression that updates are enabled. For example, if we
+ // can't write to the update directory, we might complain to the user that
+ // update is broken and they should reinstall.
+ return;
+ }
+ if (!this.canCheckForUpdates) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - unable to check for " +
+ "updates... returning early"
+ );
+ return;
+ }
+ let status = readStatusFile(getReadyUpdateDir());
+ LOG(`UpdateService:_postUpdateProcessing - status = "${status}"`);
+
+ if (!this.canApplyUpdates) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - unable to apply " +
+ "updates... returning early"
+ );
+ if (hasUpdateMutex()) {
+ // If the update is present in the update directory somehow,
+ // it would prevent us from notifying the user of further updates.
+ LOG(
+ "UpdateService:_postUpdateProcessing - Cleaning up active updates."
+ );
+ cleanupActiveUpdates();
+ }
+ return;
+ }
+
+ let updates = [];
+ if (lazy.UM.readyUpdate) {
+ updates.push(lazy.UM.readyUpdate);
+ }
+ if (lazy.UM.downloadingUpdate) {
+ updates.push(lazy.UM.downloadingUpdate);
+ }
+
+ if (status == STATE_NONE) {
+ // A status of STATE_NONE in _postUpdateProcessing means that the
+ // update.status file is present but there isn't an update in progress.
+ // This isn't an expected state, so if we find ourselves in it, we want
+ // to just clean things up to go back to a good state.
+ LOG(
+ "UpdateService:_postUpdateProcessing - Cleaning up unexpected state."
+ );
+ if (!updates.length) {
+ updates.push(new Update(null));
+ }
+ for (let update of updates) {
+ update.state = STATE_FAILED;
+ update.errorCode = ERR_UPDATE_STATE_NONE;
+ update.statusText =
+ lazy.gUpdateBundle.GetStringFromName("statusFailed");
+ }
+ let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE;
+ pingStateAndStatusCodes(updates[0], true, newStatus);
+ cleanupActiveUpdates();
+ return;
+ }
+
+ let channelChanged = updates => {
+ for (let update of updates) {
+ if (update.channel != lazy.UpdateUtils.UpdateChannel) {
+ return true;
+ }
+ }
+ return false;
+ };
+ if (channelChanged(updates)) {
+ let channel = lazy.UM.readyUpdate
+ ? lazy.UM.readyUpdate.channel
+ : lazy.UM.downloadingUpdate.channel;
+ LOG(
+ "UpdateService:_postUpdateProcessing - update channel is " +
+ "different than application's channel, removing update. update " +
+ "channel: " +
+ channel +
+ ", expected channel: " +
+ lazy.UpdateUtils.UpdateChannel
+ );
+ // User switched channels, clear out the old active updates and remove
+ // partial downloads
+ for (let update of updates) {
+ update.state = STATE_FAILED;
+ update.errorCode = ERR_CHANNEL_CHANGE;
+ update.statusText =
+ lazy.gUpdateBundle.GetStringFromName("statusFailed");
+ }
+ let newStatus = STATE_FAILED + ": " + ERR_CHANNEL_CHANGE;
+ pingStateAndStatusCodes(updates[0], true, newStatus);
+ cleanupActiveUpdates();
+ return;
+ }
+
+ // Handle the case when the update is the same or older than the current
+ // version and nsUpdateDriver.cpp skipped updating due to the version being
+ // older than the current version. This also handles the general case when
+ // an update is for an older version or the same version and same build ID.
+ if (
+ status == STATE_PENDING ||
+ status == STATE_PENDING_SERVICE ||
+ status == STATE_APPLIED ||
+ status == STATE_APPLIED_SERVICE ||
+ status == STATE_PENDING_ELEVATE ||
+ status == STATE_DOWNLOADING
+ ) {
+ let tooOldUpdate;
+ if (
+ updateIsAtLeastAsOldAs(
+ lazy.UM.readyUpdate,
+ Services.appinfo.version,
+ Services.appinfo.appBuildID
+ )
+ ) {
+ tooOldUpdate = lazy.UM.readyUpdate;
+ } else if (
+ updateIsAtLeastAsOldAs(
+ lazy.UM.downloadingUpdate,
+ Services.appinfo.version,
+ Services.appinfo.appBuildID
+ )
+ ) {
+ tooOldUpdate = lazy.UM.downloadingUpdate;
+ }
+ if (tooOldUpdate) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - removing update for older " +
+ "application version or same application version with same build " +
+ "ID. update application version: " +
+ tooOldUpdate.appVersion +
+ ", " +
+ "application version: " +
+ Services.appinfo.version +
+ ", update " +
+ "build ID: " +
+ tooOldUpdate.buildID +
+ ", application build ID: " +
+ Services.appinfo.appBuildID
+ );
+ tooOldUpdate.state = STATE_FAILED;
+ tooOldUpdate.statusText =
+ lazy.gUpdateBundle.GetStringFromName("statusFailed");
+ tooOldUpdate.errorCode = ERR_OLDER_VERSION_OR_SAME_BUILD;
+ // This could be split out to report telemetry for each case.
+ let newStatus = STATE_FAILED + ": " + ERR_OLDER_VERSION_OR_SAME_BUILD;
+ pingStateAndStatusCodes(tooOldUpdate, true, newStatus);
+ // Cleanup both updates regardless of which one is too old. It's
+ // exceedingly unlikely that a user could end up in a state where one
+ // update is acceptable and the other isn't. And it makes this function
+ // considerably more complex to try to deal with that possibility.
+ cleanupActiveUpdates();
+ return;
+ }
+ }
+
+ pingStateAndStatusCodes(
+ status == STATE_DOWNLOADING
+ ? lazy.UM.downloadingUpdate
+ : lazy.UM.readyUpdate,
+ true,
+ status
+ );
+ if (lazy.UM.downloadingUpdate || status == STATE_DOWNLOADING) {
+ if (status == STATE_SUCCEEDED) {
+ // If we successfully installed an update while we were downloading
+ // another update, the downloading update is now a partial MAR for
+ // a version that is no longer installed. We know that it's a partial
+ // MAR without checking because we currently only download partial MARs
+ // when an update has already been downloaded.
+ LOG(
+ "UpdateService:_postUpdateProcessing - removing downloading patch " +
+ "because we installed a different patch before it finished" +
+ "downloading."
+ );
+ cleanupDownloadingUpdate();
+ } else {
+ // Attempt to resume download
+ if (lazy.UM.downloadingUpdate) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - resuming patch found in " +
+ "downloading state"
+ );
+ let success = await this.downloadUpdate(lazy.UM.downloadingUpdate);
+ if (!success) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - Failed to resume patch. " +
+ "Cleaning up downloading update."
+ );
+ cleanupDownloadingUpdate();
+ }
+ } else {
+ LOG(
+ "UpdateService:_postUpdateProcessing - Warning: found " +
+ "downloading state, but no downloading patch. Cleaning up " +
+ "active updates."
+ );
+ // Put ourselves back in a good state.
+ cleanupActiveUpdates();
+ }
+ if (status == STATE_DOWNLOADING) {
+ // Done dealing with the downloading update, and there is no ready
+ // update, so return early.
+ return;
+ }
+ }
+ }
+
+ let update = lazy.UM.readyUpdate;
+
+ if (status == STATE_APPLYING) {
+ // This indicates that the background updater service is in either of the
+ // following two states:
+ // 1. It is in the process of applying an update in the background, and
+ // we just happen to be racing against that.
+ // 2. It has failed to apply an update for some reason, and we hit this
+ // case because the updater process has set the update status to
+ // applying, but has never finished.
+ // In order to differentiate between these two states, we look at the
+ // state field of the update object. If it's "pending", then we know
+ // that this is the first time we're hitting this case, so we switch
+ // that state to "applying" and we just wait and hope for the best.
+ // If it's "applying", we know that we've already been here once, so
+ // we really want to start from a clean state.
+ if (
+ update &&
+ (update.state == STATE_PENDING || update.state == STATE_PENDING_SERVICE)
+ ) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - patch found in applying " +
+ "state for the first time"
+ );
+ update.state = STATE_APPLYING;
+ lazy.UM.saveUpdates();
+ transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING);
+ pollForStagingEnd();
+ } else {
+ // We get here even if we don't have an update object
+ LOG(
+ "UpdateService:_postUpdateProcessing - patch found in applying " +
+ "state for the second time. Cleaning up ready update."
+ );
+ cleanupReadyUpdate();
+ }
+ return;
+ }
+
+ if (!update) {
+ if (status != STATE_SUCCEEDED) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - previous patch failed " +
+ "and no patch available. Cleaning up ready update."
+ );
+ cleanupReadyUpdate();
+ return;
+ }
+ LOG(
+ "UpdateService:_postUpdateProcessing - Update data missing. Creating " +
+ "an empty update object."
+ );
+ update = new Update(null);
+ }
+
+ let parts = status.split(":");
+ update.state = parts[0];
+ LOG(
+ `UpdateService:_postUpdateProcessing - Setting update's state from ` +
+ `the status file (="${update.state}")`
+ );
+ if (update.state == STATE_FAILED && parts[1]) {
+ update.errorCode = parseInt(parts[1]);
+ LOG(
+ `UpdateService:_postUpdateProcessing - Setting update's errorCode ` +
+ `from the status file (="${update.errorCode}")`
+ );
+ }
+
+ if (status != STATE_SUCCEEDED) {
+ // Rotate the update logs so the update log isn't removed. By passing
+ // false the patch directory won't be removed.
+ cleanUpReadyUpdateDir(false);
+ }
+
+ if (status == STATE_SUCCEEDED) {
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS);
+ }
+ update.statusText =
+ lazy.gUpdateBundle.GetStringFromName("installSuccess");
+
+ // The only time that update is not a reference to readyUpdate is when
+ // readyUpdate is null.
+ if (!lazy.UM.readyUpdate) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - Assigning successful update " +
+ "readyUpdate before cleaning it up."
+ );
+ lazy.UM.readyUpdate = update;
+ }
+
+ // Done with this update. Clean it up.
+ LOG(
+ "UpdateService:_postUpdateProcessing - Cleaning up successful ready " +
+ "update."
+ );
+ cleanupReadyUpdate();
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
+ } else if (status == STATE_PENDING_ELEVATE) {
+ // In case the active-update.xml file is deleted.
+ if (!update) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - status is pending-elevate " +
+ "but there isn't a ready update, removing update"
+ );
+ cleanupReadyUpdate();
+ } else {
+ transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
+ if (Services.startup.wasSilentlyStarted) {
+ // This check _should_ be unnecessary since we should not silently
+ // restart if state == pending-elevate. But the update elevation
+ // dialog is a way that we could potentially show UI on startup, even
+ // with no windows open. Which we really do not want to do on a silent
+ // restart.
+ // So this is defense in depth.
+ LOG(
+ "UpdateService:_postUpdateProcessing - status is " +
+ "pending-elevate, but this is a silent startup, so the " +
+ "elevation window has been suppressed."
+ );
+ } else {
+ LOG(
+ "UpdateService:_postUpdateProcessing - status is " +
+ "pending-elevate. Showing Update elevation dialog."
+ );
+ let uri = "chrome://mozapps/content/update/updateElevation.xhtml";
+ let features =
+ "chrome,centerscreen,resizable=no,titlebar,toolbar=no,dialog=no";
+ Services.ww.openWindow(null, uri, "Update:Elevation", features, null);
+ }
+ }
+ } else {
+ // If there was an I/O error it is assumed that the patch is not invalid
+ // and it is set to pending so an attempt to apply it again will happen
+ // when the application is restarted.
+ if (update.state == STATE_FAILED && update.errorCode) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - Attempting handleUpdateFailure"
+ );
+ if (handleUpdateFailure(update)) {
+ LOG(
+ "UpdateService:_postUpdateProcessing - handleUpdateFailure success."
+ );
+ return;
+ }
+ }
+
+ LOG(
+ "UpdateService:_postUpdateProcessing - Attempting to fall back to a " +
+ "complete update."
+ );
+ // Something went wrong with the patch application process.
+ await handleFallbackToCompleteUpdate();
+ }
+ },
+
+ /**
+ * Register an observer when the network comes online, so we can short-circuit
+ * the app.update.interval when there isn't connectivity
+ */
+ _registerOnlineObserver: function AUS__registerOnlineObserver() {
+ if (this._registeredOnlineObserver) {
+ LOG(
+ "UpdateService:_registerOnlineObserver - observer already registered"
+ );
+ return;
+ }
+
+ LOG(
+ "UpdateService:_registerOnlineObserver - waiting for the network to " +
+ "be online, then forcing another check"
+ );
+
+ Services.obs.addObserver(this, "network:offline-status-changed");
+ this._registeredOnlineObserver = true;
+ },
+
+ /**
+ * Called from the network:offline-status-changed observer.
+ */
+ _offlineStatusChanged: async function AUS__offlineStatusChanged(status) {
+ if (status !== "online") {
+ return;
+ }
+
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+ this._registeredOnlineObserver = false;
+
+ LOG(
+ "UpdateService:_offlineStatusChanged - network is online, forcing " +
+ "another background check"
+ );
+
+ // the background checker is contained in notify
+ await this._attemptResume();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ onCheckComplete: async function AUS_onCheckComplete(result) {
+ if (result.succeeded) {
+ await this._selectAndInstallUpdate(result.updates);
+ return;
+ }
+
+ if (!result.checksAllowed) {
+ LOG("UpdateService:onCheckComplete - checks not allowed");
+ return;
+ }
+
+ // On failure, result.updates is guaranteed to have exactly one update
+ // containing error information.
+ let update = result.updates[0];
+
+ LOG(
+ "UpdateService:onCheckComplete - error during background update. error " +
+ "code: " +
+ update.errorCode +
+ ", status text: " +
+ update.statusText
+ );
+
+ if (update.errorCode == NETWORK_ERROR_OFFLINE) {
+ // Register an online observer to try again
+ this._registerOnlineObserver();
+ if (this._pingSuffix) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
+ }
+ return;
+ }
+
+ // Send the error code to telemetry
+ AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode);
+ update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES;
+ let errCount = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_BACKGROUNDERRORS,
+ 0
+ );
+
+ // If we already have an update ready, we don't want to worry the user over
+ // update check failures. As far as the user knows, the update status is
+ // the status of the ready update. We don't want to confuse them by saying
+ // that an update check failed.
+ if (lazy.UM.readyUpdate) {
+ LOG(
+ "UpdateService:onCheckComplete - Ignoring error because another " +
+ "update is ready."
+ );
+ return;
+ }
+
+ errCount++;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
+ // Don't allow the preference to set a value greater than 20 for max errors.
+ let maxErrors = Math.min(
+ Services.prefs.getIntPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10),
+ 20
+ );
+
+ if (errCount >= maxErrors) {
+ LOG(
+ "UpdateService:onCheckComplete - notifying observers of error. " +
+ "topic: update-error, status: check-attempts-exceeded"
+ );
+ Services.obs.notifyObservers(
+ update,
+ "update-error",
+ "check-attempts-exceeded"
+ );
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
+ } else {
+ LOG(
+ "UpdateService:onCheckComplete - notifying observers of error. " +
+ "topic: update-error, status: check-attempt-failed"
+ );
+ Services.obs.notifyObservers(
+ update,
+ "update-error",
+ "check-attempt-failed"
+ );
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
+ }
+ },
+
+ /**
+ * Called when a connection should be resumed
+ */
+ _attemptResume: async function AUS_attemptResume() {
+ LOG("UpdateService:_attemptResume");
+ // If a download is in progress and we aren't already downloading it, then
+ // resume it.
+ if (this.isDownloading) {
+ // There is nothing to resume. We are already downloading.
+ LOG("UpdateService:_attemptResume - already downloading.");
+ return;
+ }
+ if (
+ this._downloader &&
+ this._downloader._patch &&
+ this._downloader._patch.state == STATE_DOWNLOADING &&
+ this._downloader._update
+ ) {
+ LOG(
+ "UpdateService:_attemptResume - _patch.state: " +
+ this._downloader._patch.state
+ );
+ let success = await this.downloadUpdate(this._downloader._update);
+ LOG("UpdateService:_attemptResume - downloadUpdate success: " + success);
+ if (!success) {
+ LOG(
+ "UpdateService:_attemptResume - Resuming download failed. Cleaning " +
+ "up downloading update."
+ );
+ cleanupDownloadingUpdate();
+ }
+ return;
+ }
+
+ // Kick off an update check
+ (async () => {
+ let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK);
+ await this.onCheckComplete(await check.result);
+ })();
+ },
+
+ /**
+ * Notified when a timer fires
+ * @param timer
+ * The timer that fired
+ */
+ notify: function AUS_notify(timer) {
+ this._checkForBackgroundUpdates(true);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() {
+ return this._checkForBackgroundUpdates(false);
+ },
+
+ // The suffix used for background update check telemetry histogram ID's.
+ get _pingSuffix() {
+ if (lazy.UM.readyUpdate) {
+ // Once an update has been downloaded, all later updates will be reported
+ // to telemetry as subsequent updates. We move the first update into
+ // readyUpdate as soon as the download is complete, so any update checks
+ // after readyUpdate is no longer null are subsequent update checks.
+ return AUSTLMY.SUBSEQUENT;
+ }
+ return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL;
+ },
+
+ /**
+ * Checks for updates in the background.
+ * @param isNotify
+ * Whether or not a background update check was initiated by the
+ * application update timer notification.
+ */
+ _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates(
+ isNotify
+ ) {
+ if (!this.disabled && AppConstants.NIGHTLY_BUILD) {
+ // Scalar ID: update.suppress_prompts
+ AUSTLMY.pingSuppressPrompts();
+ }
+ if (this.disabled || this.manualUpdateOnly) {
+ // Return immediately if we are disabled by policy. Otherwise, just the
+ // telemetry we try to collect below can potentially trigger a restart
+ // prompt if the update directory isn't writable. And we shouldn't be
+ // telling the user about update failures if update is disabled.
+ // See Bug 1599590.
+ // Note that we exit unconditionally here if we are only doing manual
+ // update checks, because manual update checking uses a completely
+ // different code path (AppUpdater.jsm creates its own nsIUpdateChecker),
+ // bypassing this function completely.
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
+ return false;
+ }
+
+ this._isNotify = isNotify;
+
+ // Histogram IDs:
+ // UPDATE_PING_COUNT_EXTERNAL
+ // UPDATE_PING_COUNT_NOTIFY
+ // UPDATE_PING_COUNT_SUBSEQUENT
+ AUSTLMY.pingGeneric("UPDATE_PING_COUNT_" + this._pingSuffix, true, false);
+
+ // Histogram IDs:
+ // UPDATE_UNABLE_TO_APPLY_EXTERNAL
+ // UPDATE_UNABLE_TO_APPLY_NOTIFY
+ // UPDATE_UNABLE_TO_APPLY_SUBSEQUENT
+ AUSTLMY.pingGeneric(
+ "UPDATE_UNABLE_TO_APPLY_" + this._pingSuffix,
+ getCanApplyUpdates(),
+ true
+ );
+ // Histogram IDs:
+ // UPDATE_CANNOT_STAGE_EXTERNAL
+ // UPDATE_CANNOT_STAGE_NOTIFY
+ // UPDATE_CANNOT_STAGE_SUBSEQUENT
+ AUSTLMY.pingGeneric(
+ "UPDATE_CANNOT_STAGE_" + this._pingSuffix,
+ getCanStageUpdates(),
+ true
+ );
+ if (AppConstants.platform == "win") {
+ // Histogram IDs:
+ // UPDATE_CAN_USE_BITS_EXTERNAL
+ // UPDATE_CAN_USE_BITS_NOTIFY
+ // UPDATE_CAN_USE_BITS_SUBSEQUENT
+ AUSTLMY.pingGeneric(
+ "UPDATE_CAN_USE_BITS_" + this._pingSuffix,
+ getCanUseBits()
+ );
+ }
+ // Histogram IDs:
+ // UPDATE_INVALID_LASTUPDATETIME_EXTERNAL
+ // UPDATE_INVALID_LASTUPDATETIME_NOTIFY
+ // UPDATE_INVALID_LASTUPDATETIME_SUBSEQUENT
+ // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL
+ // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY
+ // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_SUBSEQUENT
+ AUSTLMY.pingLastUpdateTime(this._pingSuffix);
+ // Histogram IDs:
+ // UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL
+ // UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY
+ // UPDATE_NOT_PREF_UPDATE_AUTO_SUBSEQUENT
+ lazy.UpdateUtils.getAppUpdateAutoEnabled().then(enabled => {
+ AUSTLMY.pingGeneric(
+ "UPDATE_NOT_PREF_UPDATE_AUTO_" + this._pingSuffix,
+ enabled,
+ true
+ );
+ });
+ // Histogram IDs:
+ // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL
+ // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY
+ // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_SUBSEQUENT
+ AUSTLMY.pingBoolPref(
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_" + this._pingSuffix,
+ PREF_APP_UPDATE_STAGING_ENABLED,
+ true,
+ true
+ );
+ if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
+ // Histogram IDs:
+ // UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL
+ // UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY
+ // UPDATE_PREF_UPDATE_CANCELATIONS_SUBSEQUENT
+ AUSTLMY.pingIntPref(
+ "UPDATE_PREF_UPDATE_CANCELATIONS_" + this._pingSuffix,
+ PREF_APP_UPDATE_CANCELATIONS,
+ 0,
+ 0
+ );
+ }
+ if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
+ // Histogram IDs:
+ // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL
+ // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY
+ // UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_SUBSEQUENT
+ AUSTLMY.pingBoolPref(
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_" + this._pingSuffix,
+ PREF_APP_UPDATE_SERVICE_ENABLED,
+ true
+ );
+ // Histogram IDs:
+ // UPDATE_PREF_SERVICE_ERRORS_EXTERNAL
+ // UPDATE_PREF_SERVICE_ERRORS_NOTIFY
+ // UPDATE_PREF_SERVICE_ERRORS_SUBSEQUENT
+ AUSTLMY.pingIntPref(
+ "UPDATE_PREF_SERVICE_ERRORS_" + this._pingSuffix,
+ PREF_APP_UPDATE_SERVICE_ERRORS,
+ 0,
+ 0
+ );
+ if (AppConstants.platform == "win") {
+ // Histogram IDs:
+ // UPDATE_SERVICE_INSTALLED_EXTERNAL
+ // UPDATE_SERVICE_INSTALLED_NOTIFY
+ // UPDATE_SERVICE_INSTALLED_SUBSEQUENT
+ // UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL
+ // UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY
+ // UPDATE_SERVICE_MANUALLY_UNINSTALLED_SUBSEQUENT
+ AUSTLMY.pingServiceInstallStatus(
+ this._pingSuffix,
+ isServiceInstalled()
+ );
+ }
+ }
+
+ // If a download is in progress or the patch has been staged do nothing.
+ if (this.isDownloading) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING);
+ return false;
+ }
+
+ // Once we have downloaded a complete update, do not download further
+ // updates until the complete update is installed. This is important,
+ // because if we fall back from a partial update to a complete update,
+ // it might be because of changes to the patch directory (which would cause
+ // a failure to apply any partial MAR). So we really don't want to replace
+ // a downloaded complete update with a downloaded partial update. And we
+ // do not currently download complete updates if there is already a
+ // readyUpdate available.
+ if (
+ lazy.UM.readyUpdate &&
+ lazy.UM.readyUpdate.selectedPatch &&
+ lazy.UM.readyUpdate.selectedPatch.type == "complete"
+ ) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
+ return false;
+ }
+
+ // If we start downloading an update while the readyUpdate is staging, we
+ // run the risk of eventually wanting to overwrite readyUpdate with the
+ // downloadingUpdate while the readyUpdate is still staging. Then we would
+ // have to have a weird intermediate state where the downloadingUpdate has
+ // finished downloading, but can't be moved yet. It's simpler to just not
+ // start a new update if the old one is still staging.
+ if (this.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
+ return false;
+ }
+
+ // Asynchronously kick off update checking
+ (async () => {
+ let validUpdateURL = true;
+ try {
+ await lazy.CheckSvc.getUpdateURL(lazy.CheckSvc.BACKGROUND_CHECK);
+ } catch (e) {
+ validUpdateURL = false;
+ }
+
+ // The following checks are done here so they can be differentiated from
+ // foreground checks.
+ if (!lazy.UpdateUtils.OSVersion) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
+ } else if (!lazy.UpdateUtils.ABI) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI);
+ } else if (!validUpdateURL) {
+ AUSTLMY.pingCheckCode(
+ this._pingSuffix,
+ AUSTLMY.CHK_INVALID_DEFAULT_URL
+ );
+ } else if (!hasUpdateMutex()) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
+ } else if (isOtherInstanceRunning()) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OTHER_INSTANCE);
+ } else if (!this.canCheckForUpdates) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
+ }
+
+ let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK);
+ await this.onCheckComplete(await check.result);
+ })();
+
+ return true;
+ },
+
+ /**
+ * Determine the update from the specified updates that should be offered.
+ * If both valid major and minor updates are available the minor update will
+ * be offered.
+ * @param updates
+ * An array of available nsIUpdate items
+ * @return The nsIUpdate to offer.
+ */
+ selectUpdate: function AUS_selectUpdate(updates) {
+ if (!updates.length) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND);
+ return null;
+ }
+
+ // The ping for unsupported is sent after the call to showPrompt.
+ if (updates.length == 1 && updates[0].unsupported) {
+ return updates[0];
+ }
+
+ // Choose the newest of the available minor and major updates.
+ var majorUpdate = null;
+ var minorUpdate = null;
+ var vc = Services.vc;
+ let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND;
+
+ updates.forEach(function (aUpdate) {
+ // Ignore updates for older versions of the application and updates for
+ // the same version of the application with the same build ID.
+ if (updateIsAtLeastAsOldAsCurrentVersion(aUpdate)) {
+ LOG(
+ "UpdateService:selectUpdate - skipping update because the " +
+ "update's application version is not greater than the current " +
+ "application version"
+ );
+ lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
+ return;
+ }
+
+ if (updateIsAtLeastAsOldAsReadyUpdate(aUpdate)) {
+ LOG(
+ "UpdateService:selectUpdate - skipping update because the " +
+ "update's application version is not greater than that of the " +
+ "currently downloaded update"
+ );
+ lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
+ return;
+ }
+
+ if (lazy.UM.readyUpdate && !getPatchOfType(aUpdate, "partial")) {
+ LOG(
+ "UpdateService:selectUpdate - skipping update because no partial " +
+ "patch is available and an update has already been downloaded."
+ );
+ lastCheckCode = AUSTLMY.CHK_NO_PARTIAL_PATCH;
+ return;
+ }
+
+ switch (aUpdate.type) {
+ case "major":
+ if (!majorUpdate) {
+ majorUpdate = aUpdate;
+ } else if (
+ vc.compare(majorUpdate.appVersion, aUpdate.appVersion) <= 0
+ ) {
+ majorUpdate = aUpdate;
+ }
+ break;
+ case "minor":
+ if (!minorUpdate) {
+ minorUpdate = aUpdate;
+ } else if (
+ vc.compare(minorUpdate.appVersion, aUpdate.appVersion) <= 0
+ ) {
+ minorUpdate = aUpdate;
+ }
+ break;
+ default:
+ LOG(
+ "UpdateService:selectUpdate - skipping unknown update type: " +
+ aUpdate.type
+ );
+ lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE;
+ break;
+ }
+ });
+
+ let update = minorUpdate || majorUpdate;
+ if (AppConstants.platform == "macosx" && update) {
+ if (getElevationRequired()) {
+ let installAttemptVersion = Services.prefs.getCharPref(
+ PREF_APP_UPDATE_ELEVATE_VERSION,
+ null
+ );
+ if (vc.compare(installAttemptVersion, update.appVersion) != 0) {
+ Services.prefs.setCharPref(
+ PREF_APP_UPDATE_ELEVATE_VERSION,
+ update.appVersion
+ );
+ if (
+ Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)
+ ) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
+ }
+ } else {
+ let numCancels = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_CANCELATIONS_OSX,
+ 0
+ );
+ let rejectedVersion = Services.prefs.getCharPref(
+ PREF_APP_UPDATE_ELEVATE_NEVER,
+ ""
+ );
+ let maxCancels = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
+ DEFAULT_CANCELATIONS_OSX_MAX
+ );
+ if (numCancels >= maxCancels) {
+ LOG(
+ "UpdateService:selectUpdate - the user requires elevation to " +
+ "install this update, but the user has exceeded the max " +
+ "number of elevation attempts."
+ );
+ update.elevationFailure = true;
+ AUSTLMY.pingCheckCode(
+ this._pingSuffix,
+ AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION
+ );
+ } else if (vc.compare(rejectedVersion, update.appVersion) == 0) {
+ LOG(
+ "UpdateService:selectUpdate - the user requires elevation to " +
+ "install this update, but elevation is disabled for this " +
+ "version."
+ );
+ update.elevationFailure = true;
+ AUSTLMY.pingCheckCode(
+ this._pingSuffix,
+ AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION
+ );
+ } else {
+ LOG(
+ "UpdateService:selectUpdate - the user requires elevation to " +
+ "install the update."
+ );
+ }
+ }
+ } else {
+ // Clear elevation-related prefs since they no longer apply (the user
+ // may have gained write access to the Firefox directory or an update
+ // was executed with a different profile).
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
+ }
+ }
+ } else if (!update) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode);
+ }
+
+ return update;
+ },
+
+ /**
+ * Determine which of the specified updates should be installed and begin the
+ * download/installation process or notify the user about the update.
+ * @param updates
+ * An array of available updates
+ */
+ _selectAndInstallUpdate: async function AUS__selectAndInstallUpdate(updates) {
+ // Return early if there's an active update. The user is already aware and
+ // is downloading or performed some user action to prevent notification.
+ if (lazy.UM.downloadingUpdate) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE);
+ return;
+ }
+
+ if (this.disabled) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
+ LOG(
+ "UpdateService:_selectAndInstallUpdate - not prompting because " +
+ "update is disabled"
+ );
+ return;
+ }
+
+ var update = this.selectUpdate(updates);
+ if (!update || update.elevationFailure) {
+ return;
+ }
+
+ if (update.unsupported) {
+ LOG(
+ "UpdateService:_selectAndInstallUpdate - update not supported for " +
+ "this system. Notifying observers. topic: update-available, " +
+ "status: unsupported"
+ );
+ Services.obs.notifyObservers(update, "update-available", "unsupported");
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
+ return;
+ }
+
+ if (!getCanApplyUpdates()) {
+ LOG(
+ "UpdateService:_selectAndInstallUpdate - the user is unable to " +
+ "apply updates... prompting. Notifying observers. " +
+ "topic: update-available, status: cant-apply"
+ );
+ Services.obs.notifyObservers(null, "update-available", "cant-apply");
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
+ return;
+ }
+
+ /**
+ * From this point on there are two possible outcomes:
+ * 1. download and install the update automatically
+ * 2. notify the user about the availability of an update
+ *
+ * Notes:
+ * a) if the app.update.auto preference is false then automatic download and
+ * install is disabled and the user will be notified.
+ *
+ * If the update when it is first read does not have an appVersion attribute
+ * the following deprecated behavior will occur:
+ * Update Type Outcome
+ * Major Notify
+ * Minor Auto Install
+ */
+ let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled();
+ if (!updateAuto) {
+ LOG(
+ "UpdateService:_selectAndInstallUpdate - prompting because silent " +
+ "install is disabled. Notifying observers. topic: update-available, " +
+ "status: show-prompt"
+ );
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
+ Services.obs.notifyObservers(update, "update-available", "show-prompt");
+ return;
+ }
+
+ LOG("UpdateService:_selectAndInstallUpdate - download the update");
+ let success = await this.downloadUpdate(update);
+ if (!success && !this.isDownloading) {
+ LOG(
+ "UpdateService:_selectAndInstallUpdate - Failed to start downloading " +
+ "update. Cleaning up downloading update."
+ );
+ cleanupDownloadingUpdate();
+ }
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE);
+ },
+
+ get disabledForTesting() {
+ return (
+ (Cu.isInAutomation ||
+ lazy.Marionette.running ||
+ lazy.RemoteAgent.running) &&
+ Services.prefs.getBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false)
+ );
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get disabled() {
+ return (
+ (Services.policies && !Services.policies.isAllowed("appUpdate")) ||
+ this.disabledForTesting ||
+ Services.sysinfo.getProperty("isPackagedApp")
+ );
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get manualUpdateOnly() {
+ return (
+ Services.policies && !Services.policies.isAllowed("autoAppUpdateChecking")
+ );
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canUsuallyCheckForUpdates() {
+ if (this.disabled) {
+ LOG(
+ "UpdateService.canUsuallyCheckForUpdates - unable to automatically check " +
+ "for updates, the option has been disabled by the administrator."
+ );
+ return false;
+ }
+
+ // If we don't know the binary platform we're updating, we can't update.
+ if (!lazy.UpdateUtils.ABI) {
+ LOG(
+ "UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
+ "unknown ABI"
+ );
+ return false;
+ }
+
+ // If we don't know the OS version we're updating, we can't update.
+ if (!lazy.UpdateUtils.OSVersion) {
+ LOG(
+ "UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
+ "unknown OS version"
+ );
+ return false;
+ }
+
+ LOG("UpdateService.canUsuallyCheckForUpdates - able to check for updates");
+ return true;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canCheckForUpdates() {
+ if (!this.canUsuallyCheckForUpdates) {
+ return false;
+ }
+
+ if (!hasUpdateMutex()) {
+ LOG(
+ "UpdateService.canCheckForUpdates - unable to check for updates, " +
+ "unable to acquire update mutex"
+ );
+ return false;
+ }
+
+ if (isOtherInstanceRunning()) {
+ // This doesn't block update checks, but we will have to wait until either
+ // the other instance is gone or we time out waiting for it.
+ LOG(
+ "UpdateService.canCheckForUpdates - another instance is holding the " +
+ "lock, will need to wait for it prior to checking for updates"
+ );
+ }
+
+ LOG("UpdateService.canCheckForUpdates - able to check for updates");
+ return true;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get elevationRequired() {
+ return getElevationRequired();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canUsuallyApplyUpdates() {
+ return getCanApplyUpdates();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canApplyUpdates() {
+ return (
+ this.canUsuallyApplyUpdates &&
+ hasUpdateMutex() &&
+ !isOtherInstanceRunning()
+ );
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canUsuallyStageUpdates() {
+ return getCanStageUpdates(false);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canStageUpdates() {
+ return getCanStageUpdates();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canUsuallyUseBits() {
+ return getCanUseBits(false) == "CanUseBits";
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canUseBits() {
+ return getCanUseBits() == "CanUseBits";
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get isOtherInstanceHandlingUpdates() {
+ return !hasUpdateMutex() || isOtherInstanceRunning();
+ },
+
+ /**
+ * A set of download listeners to be notified by this._downloader when it
+ * receives nsIRequestObserver or nsIProgressEventSink method calls.
+ *
+ * These are stored on the UpdateService rather than on the Downloader,
+ * because they ought to persist across multiple Downloader instances.
+ */
+ _downloadListeners: new Set(),
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ addDownloadListener: function AUS_addDownloadListener(listener) {
+ let oldSize = this._downloadListeners.size;
+ this._downloadListeners.add(listener);
+
+ if (this._downloadListeners.size == oldSize) {
+ LOG(
+ "UpdateService:addDownloadListener - Warning: Didn't add duplicate " +
+ "listener"
+ );
+ return;
+ }
+
+ if (this._downloader) {
+ this._downloader.onDownloadListenerAdded();
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ removeDownloadListener: function AUS_removeDownloadListener(listener) {
+ let elementRemoved = this._downloadListeners.delete(listener);
+
+ if (!elementRemoved) {
+ LOG(
+ "UpdateService:removeDownloadListener - Warning: Didn't remove " +
+ "non-existent listener"
+ );
+ return;
+ }
+
+ if (this._downloader) {
+ this._downloader.onDownloadListenerRemoved();
+ }
+ },
+
+ /**
+ * Returns a boolean indicating whether there are any download listeners
+ */
+ get hasDownloadListeners() {
+ return !!this._downloadListeners.length;
+ },
+
+ /*
+ * Calls the provided function once with each download listener that is
+ * currently registered.
+ */
+ forEachDownloadListener: function AUS_forEachDownloadListener(fn) {
+ // Make a shallow copy in case listeners remove themselves.
+ let listeners = new Set(this._downloadListeners);
+ listeners.forEach(fn);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ downloadUpdate: async function AUS_downloadUpdate(update) {
+ if (!update) {
+ throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ // Don't download the update if the update's version is less than the
+ // current application's version or the update's version is the same as the
+ // application's version and the build ID is the same as the application's
+ // build ID. If we already have an update ready, we want to apply those
+ // same checks against the version of the ready update, so that we don't
+ // download an update that isn't newer than the one we already have.
+ if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
+ LOG(
+ "UpdateService:downloadUpdate - Skipping download of update since " +
+ "it is for an earlier or same application version and build ID.\n" +
+ "current application version: " +
+ Services.appinfo.version +
+ "\n" +
+ "update application version : " +
+ update.appVersion +
+ "\n" +
+ "current build ID: " +
+ Services.appinfo.appBuildID +
+ "\n" +
+ "update build ID : " +
+ update.buildID
+ );
+ return false;
+ }
+ if (updateIsAtLeastAsOldAsReadyUpdate(update)) {
+ LOG(
+ "UpdateService:downloadUpdate - not downloading update because the " +
+ "update that's already been downloaded is the same version or " +
+ "newer.\n" +
+ "currently downloaded update application version: " +
+ lazy.UM.readyUpdate.appVersion +
+ "\n" +
+ "available update application version : " +
+ update.appVersion +
+ "\n" +
+ "currently downloaded update build ID: " +
+ lazy.UM.readyUpdate.buildID +
+ "\n" +
+ "available update build ID : " +
+ update.buildID
+ );
+ return false;
+ }
+
+ // If a download request is in progress vs. a download ready to resume
+ if (this.isDownloading) {
+ if (update.isCompleteUpdate == this._downloader.isCompleteUpdate) {
+ LOG(
+ "UpdateService:downloadUpdate - no support for downloading more " +
+ "than one update at a time"
+ );
+ return true;
+ }
+ this._downloader.cancel();
+ }
+ this._downloader = new Downloader(this);
+ return this._downloader.downloadUpdate(update);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ stopDownload: async function AUS_stopDownload() {
+ if (this.isDownloading) {
+ await this._downloader.cancel();
+ } else if (this._retryTimer) {
+ // Download status is still considered as 'downloading' during retry.
+ // We need to cancel both retry and download at this stage.
+ this._retryTimer.cancel();
+ this._retryTimer = null;
+ if (this._downloader) {
+ await this._downloader.cancel();
+ }
+ }
+ if (this._downloader) {
+ await this._downloader.cleanup();
+ }
+ this._downloader = null;
+ },
+
+ /**
+ * Note that this is different from checking if `currentState` is
+ * `STATE_DOWNLOADING` because if we are downloading a second update, this
+ * will be `true` while `currentState` will be `STATE_PENDING`.
+ */
+ get isDownloading() {
+ return this._downloader && this._downloader.isBusy;
+ },
+
+ _logStatus: function AUS__logStatus() {
+ if (!lazy.gLogEnabled) {
+ return;
+ }
+ if (this.disabled) {
+ LOG("Current UpdateService status: disabled");
+ // Return early if UpdateService is disabled by policy. Otherwise some of
+ // the getters we call to display status information may discover that the
+ // update directory is not writable, which automatically results in the
+ // permissions being fixed. Which we shouldn't really be doing if update
+ // is disabled by policy.
+ return;
+ }
+ LOG("Logging current UpdateService status:");
+ // These getters print their own logging
+ this.canCheckForUpdates;
+ this.canApplyUpdates;
+ this.canStageUpdates;
+ LOG("Elevation required: " + this.elevationRequired);
+ LOG(
+ "Other instance of the application currently running: " +
+ this.isOtherInstanceHandlingUpdates
+ );
+ LOG("Downloading: " + !!this.isDownloading);
+ if (this._downloader && this._downloader.isBusy) {
+ LOG("Downloading complete update: " + this._downloader.isCompleteUpdate);
+ LOG("Downloader using BITS: " + this._downloader.usingBits);
+ if (this._downloader._patch) {
+ // This will print its own logging
+ this._downloader._canUseBits(this._downloader._patch);
+
+ // Downloader calls QueryInterface(Ci.nsIWritablePropertyBag) on
+ // its _patch member as soon as it is assigned, so no need to do so
+ // again here.
+ let bitsResult = this._downloader._patch.getProperty("bitsResult");
+ if (bitsResult != null) {
+ LOG("Patch BITS result: " + bitsResult);
+ }
+ let internalResult =
+ this._downloader._patch.getProperty("internalResult");
+ if (internalResult != null) {
+ LOG("Patch nsIIncrementalDownload result: " + internalResult);
+ }
+ }
+ }
+ LOG("End of UpdateService status");
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get onlyDownloadUpdatesThisSession() {
+ return gOnlyDownloadUpdatesThisSession;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ set onlyDownloadUpdatesThisSession(newValue) {
+ gOnlyDownloadUpdatesThisSession = newValue;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getStateName(state) {
+ switch (state) {
+ case Ci.nsIApplicationUpdateService.STATE_IDLE:
+ return "STATE_IDLE";
+ case Ci.nsIApplicationUpdateService.STATE_DOWNLOADING:
+ return "STATE_DOWNLOADING";
+ case Ci.nsIApplicationUpdateService.STATE_STAGING:
+ return "STATE_STAGING";
+ case Ci.nsIApplicationUpdateService.STATE_PENDING:
+ return "STATE_PENDING";
+ case Ci.nsIApplicationUpdateService.STATE_SWAP:
+ return "STATE_SWAP";
+ }
+ return `[unknown update state: ${state}]`;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get currentState() {
+ return gUpdateState;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get stateTransition() {
+ return gStateTransitionPromise.promise;
+ },
+
+ classID: UPDATESERVICE_CID,
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIApplicationUpdateService",
+ "nsITimerCallback",
+ "nsIObserver",
+ ]),
+};
+
+/**
+ * A service to manage active and past updates.
+ * @constructor
+ */
+export function UpdateManager() {
+ // Load the active-update.xml file to see if there is an active update.
+ let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML);
+ if (activeUpdates.length) {
+ // Set the active update directly on the var used to cache the value.
+ this._readyUpdate = activeUpdates[0];
+ if (activeUpdates.length >= 2) {
+ this._downloadingUpdate = activeUpdates[1];
+ }
+ let status = readStatusFile(getReadyUpdateDir());
+ LOG(`UpdateManager:UpdateManager - status = "${status}"`);
+ // This check is performed here since UpdateService:_postUpdateProcessing
+ // won't be called when there isn't an update.status file.
+ if (status == STATE_NONE) {
+ // Under some edgecases such as Windows system restore the
+ // active-update.xml will contain a pending update without the status
+ // file. To recover from this situation clean the updates dir and move
+ // the active update to the update history.
+ LOG(
+ "UpdateManager:UpdateManager - Found update data with no status " +
+ "file. Cleaning up..."
+ );
+ this._readyUpdate.state = STATE_FAILED;
+ this._readyUpdate.errorCode = ERR_UPDATE_STATE_NONE;
+ this._readyUpdate.statusText =
+ lazy.gUpdateBundle.GetStringFromName("statusFailed");
+ let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE;
+ pingStateAndStatusCodes(this._readyUpdate, true, newStatus);
+ this.addUpdateToHistory(this._readyUpdate);
+ this._readyUpdate = null;
+ this.saveUpdates();
+ cleanUpReadyUpdateDir();
+ cleanUpDownloadingUpdateDir();
+ } else if (status == STATE_DOWNLOADING) {
+ // The first update we read out of activeUpdates may not be the ready
+ // update, it may be the downloading update.
+ if (this._downloadingUpdate) {
+ // If the first update we read is a downloading update, it's
+ // unexpected to have read another active update. That would seem to
+ // indicate that we were downloading two updates at once, which we don't
+ // do.
+ LOG(
+ "UpdateManager:UpdateManager - Warning: Found and discarded a " +
+ "second downloading update."
+ );
+ }
+ this._downloadingUpdate = this._readyUpdate;
+ this._readyUpdate = null;
+ }
+ }
+
+ LOG(
+ "UpdateManager:UpdateManager - Initialized downloadingUpdate to " +
+ this._downloadingUpdate
+ );
+ if (this._downloadingUpdate) {
+ LOG(
+ "UpdateManager:UpdateManager - Initialized downloadingUpdate state to " +
+ this._downloadingUpdate.state
+ );
+ }
+ LOG(
+ "UpdateManager:UpdateManager - Initialized readyUpdate to " +
+ this._readyUpdate
+ );
+ if (this._readyUpdate) {
+ LOG(
+ "UpdateManager:UpdateManager - Initialized readyUpdate state to " +
+ this._readyUpdate.state
+ );
+ }
+}
+
+UpdateManager.prototype = {
+ /**
+ * The nsIUpdate object for the update that has been downloaded.
+ */
+ _readyUpdate: null,
+
+ /**
+ * The nsIUpdate object for the update currently being downloaded.
+ */
+ _downloadingUpdate: null,
+
+ /**
+ * Whether the update history stored in _updates has changed since it was
+ * loaded.
+ */
+ _updatesDirty: false,
+
+ /**
+ * See nsIObserver.idl
+ */
+ observe: function UM_observe(subject, topic, data) {
+ // Hack to be able to run and cleanup tests by reloading the update data.
+ if (topic == "um-reload-update-data") {
+ if (!Cu.isInAutomation) {
+ return;
+ }
+ LOG("UpdateManager:observe - Reloading update data.");
+ if (this._updatesXMLSaver) {
+ this._updatesXMLSaver.disarm();
+ }
+
+ let updates = [];
+ this._updatesDirty = true;
+ this._readyUpdate = null;
+ this._downloadingUpdate = null;
+ transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
+ if (data != "skip-files") {
+ let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML);
+ if (activeUpdates.length) {
+ this._readyUpdate = activeUpdates[0];
+ if (activeUpdates.length >= 2) {
+ this._downloadingUpdate = activeUpdates[1];
+ }
+ let status = readStatusFile(getReadyUpdateDir());
+ LOG(`UpdateManager:observe - Got status = ${status}`);
+ if (status == STATE_DOWNLOADING) {
+ this._downloadingUpdate = this._readyUpdate;
+ this._readyUpdate = null;
+ transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
+ } else if (
+ [
+ STATE_PENDING,
+ STATE_PENDING_SERVICE,
+ STATE_PENDING_ELEVATE,
+ STATE_APPLIED,
+ STATE_APPLIED_SERVICE,
+ ].includes(status)
+ ) {
+ transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
+ }
+ }
+ updates = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
+ }
+ this._updatesCache = updates;
+
+ LOG(
+ "UpdateManager:observe - Reloaded downloadingUpdate as " +
+ this._downloadingUpdate
+ );
+ if (this._downloadingUpdate) {
+ LOG(
+ "UpdateManager:observe - Reloaded downloadingUpdate state as " +
+ this._downloadingUpdate.state
+ );
+ }
+ LOG(
+ "UpdateManager:observe - Reloaded readyUpdate as " + this._readyUpdate
+ );
+ if (this._readyUpdate) {
+ LOG(
+ "UpdateManager:observe - Reloaded readyUpdate state as " +
+ this._readyUpdate.state
+ );
+ }
+ }
+ },
+
+ /**
+ * Loads an updates.xml formatted file into an array of nsIUpdate items.
+ * @param fileName
+ * The file name in the updates directory to load.
+ * @return The array of nsIUpdate items held in the file.
+ */
+ _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(fileName) {
+ let updates = [];
+ let file = getUpdateFile([fileName]);
+ if (!file.exists()) {
+ LOG(
+ "UpdateManager:_loadXMLFileIntoArray - XML file does not exist. " +
+ "path: " +
+ file.path
+ );
+ return updates;
+ }
+
+ // Open the active-update.xml file with both read and write access so
+ // opening it will fail if it isn't possible to also write to the file. When
+ // opening it fails it means that it isn't possible to update and the code
+ // below will return early without loading the active-update.xml. This will
+ // also make it so notifications to update manually will still be shown.
+ let mode =
+ fileName == FILE_ACTIVE_UPDATE_XML
+ ? FileUtils.MODE_RDWR
+ : FileUtils.MODE_RDONLY;
+ let fileStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ try {
+ fileStream.init(file, mode, FileUtils.PERMS_FILE, 0);
+ } catch (e) {
+ LOG(
+ "UpdateManager:_loadXMLFileIntoArray - error initializing file " +
+ "stream. Exception: " +
+ e
+ );
+ return updates;
+ }
+ try {
+ var parser = new DOMParser();
+ var doc = parser.parseFromStream(
+ fileStream,
+ "UTF-8",
+ fileStream.available(),
+ "text/xml"
+ );
+
+ var updateCount = doc.documentElement.childNodes.length;
+ for (var i = 0; i < updateCount; ++i) {
+ var updateElement = doc.documentElement.childNodes.item(i);
+ if (
+ updateElement.nodeType != updateElement.ELEMENT_NODE ||
+ updateElement.localName != "update"
+ ) {
+ continue;
+ }
+
+ let update;
+ try {
+ update = new Update(updateElement);
+ } catch (e) {
+ LOG("UpdateManager:_loadXMLFileIntoArray - invalid update");
+ continue;
+ }
+ updates.push(update);
+ }
+ } catch (ex) {
+ LOG(
+ "UpdateManager:_loadXMLFileIntoArray - error constructing update " +
+ "list. Exception: " +
+ ex
+ );
+ }
+ fileStream.close();
+ if (!updates.length) {
+ LOG(
+ "UpdateManager:_loadXMLFileIntoArray - update xml file " +
+ fileName +
+ " exists but doesn't contain any updates"
+ );
+ // The file exists but doesn't contain any updates so remove it.
+ try {
+ file.remove(false);
+ } catch (e) {
+ LOG(
+ "UpdateManager:_loadXMLFileIntoArray - error removing " +
+ fileName +
+ " file. Exception: " +
+ e
+ );
+ }
+ }
+ return updates;
+ },
+
+ /**
+ * Loads the update history from the updates.xml file into a cache.
+ */
+ _getUpdates() {
+ if (!this._updatesCache) {
+ this._updatesCache = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
+ }
+ return this._updatesCache;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getUpdateAt: function UM_getUpdateAt(aIndex) {
+ return this._getUpdates()[aIndex];
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getUpdateCount() {
+ return this._getUpdates().length;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get readyUpdate() {
+ return this._readyUpdate;
+ },
+ set readyUpdate(aUpdate) {
+ this._readyUpdate = aUpdate;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get downloadingUpdate() {
+ return this._downloadingUpdate;
+ },
+ set downloadingUpdate(aUpdate) {
+ this._downloadingUpdate = aUpdate;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ addUpdateToHistory(aUpdate) {
+ this._updatesDirty = true;
+ let updates = this._getUpdates();
+ updates.unshift(aUpdate);
+ // Limit the update history to 10 updates.
+ updates.splice(10);
+ },
+
+ /**
+ * Serializes an array of updates to an XML file or removes the file if the
+ * array length is 0.
+ * @param updates
+ * An array of nsIUpdate objects
+ * @param fileName
+ * The file name in the updates directory to write to.
+ * @return true on success, false on error
+ */
+ _writeUpdatesToXMLFile: async function UM__writeUpdatesToXMLFile(
+ updates,
+ fileName
+ ) {
+ let file;
+ try {
+ file = getUpdateFile([fileName]);
+ } catch (e) {
+ LOG(
+ "UpdateManager:_writeUpdatesToXMLFile - Unable to get XML file - " +
+ "Exception: " +
+ e
+ );
+ return false;
+ }
+ if (!updates.length) {
+ LOG(
+ "UpdateManager:_writeUpdatesToXMLFile - no updates to write. " +
+ "removing file: " +
+ file.path
+ );
+ try {
+ await IOUtils.remove(file.path);
+ } catch (e) {
+ LOG(
+ "UpdateManager:_writeUpdatesToXMLFile - Delete file exception: " + e
+ );
+ return false;
+ }
+ return true;
+ }
+
+ const EMPTY_UPDATES_DOCUMENT_OPEN =
+ '<?xml version="1.0"?><updates xmlns="' + URI_UPDATE_NS + '">';
+ const EMPTY_UPDATES_DOCUMENT_CLOSE = "</updates>";
+ try {
+ var parser = new DOMParser();
+ var doc = parser.parseFromString(
+ EMPTY_UPDATES_DOCUMENT_OPEN + EMPTY_UPDATES_DOCUMENT_CLOSE,
+ "text/xml"
+ );
+
+ for (var i = 0; i < updates.length; ++i) {
+ doc.documentElement.appendChild(updates[i].serialize(doc));
+ }
+
+ var xml =
+ EMPTY_UPDATES_DOCUMENT_OPEN +
+ doc.documentElement.innerHTML +
+ EMPTY_UPDATES_DOCUMENT_CLOSE;
+ // If the destination file existed and is removed while the following is
+ // being performed the copy of the tmp file to the destination file will
+ // fail.
+ await IOUtils.writeUTF8(file.path, xml, {
+ tmpPath: file.path + ".tmp",
+ });
+ await IOUtils.setPermissions(file.path, FileUtils.PERMS_FILE);
+ } catch (e) {
+ LOG("UpdateManager:_writeUpdatesToXMLFile - Exception: " + e);
+ return false;
+ }
+ return true;
+ },
+
+ _updatesXMLSaver: null,
+ _updatesXMLSaverCallback: null,
+ /**
+ * See nsIUpdateService.idl
+ */
+ saveUpdates: function UM_saveUpdates() {
+ if (!this._updatesXMLSaver) {
+ this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize();
+
+ this._updatesXMLSaver = new lazy.DeferredTask(
+ () => this._saveUpdatesXML(),
+ XML_SAVER_INTERVAL_MS
+ );
+ lazy.AsyncShutdown.profileBeforeChange.addBlocker(
+ "UpdateManager: writing update xml data",
+ this._updatesXMLSaverCallback
+ );
+ } else {
+ this._updatesXMLSaver.disarm();
+ }
+
+ this._updatesXMLSaver.arm();
+ },
+
+ /**
+ * Saves the active-updates.xml and updates.xml when the updates history has
+ * been modified files.
+ */
+ _saveUpdatesXML: function UM__saveUpdatesXML() {
+ // This mechanism for how we store the updates might seem a bit odd, since,
+ // if only one update is stored, we don't know if it's the ready update or
+ // the downloading update. However, we can determine which it is by reading
+ // update.status. If we read STATE_DOWNLOADING, it must be a downloading
+ // update and otherwise it's a ready update. This method has the additional
+ // advantage of requiring no migration from when we used to only store a
+ // single active update.
+ let updates = [];
+ if (this._readyUpdate) {
+ updates.push(this._readyUpdate);
+ }
+ if (this._downloadingUpdate) {
+ updates.push(this._downloadingUpdate);
+ }
+
+ // The active update stored in the active-update.xml file will change during
+ // the lifetime of an active update and the file should always be updated
+ // when saveUpdates is called.
+ let promises = [];
+ promises[0] = this._writeUpdatesToXMLFile(updates, FILE_ACTIVE_UPDATE_XML);
+ // The update history stored in the updates.xml file should only need to be
+ // updated when an active update has been added to it in which case
+ // |_updatesDirty| will be true.
+ if (this._updatesDirty) {
+ this._updatesDirty = false;
+ promises[1] = this._writeUpdatesToXMLFile(
+ this._getUpdates(),
+ FILE_UPDATES_XML
+ );
+ }
+ return Promise.all(promises);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ refreshUpdateStatus: async function UM_refreshUpdateStatus() {
+ try {
+ LOG("UpdateManager:refreshUpdateStatus - Staging done.");
+
+ var update = this._readyUpdate;
+ if (!update) {
+ LOG("UpdateManager:refreshUpdateStatus - Missing ready update?");
+ return;
+ }
+
+ var status = readStatusFile(getReadyUpdateDir());
+ pingStateAndStatusCodes(update, false, status);
+ LOG(`UpdateManager:refreshUpdateStatus - status = ${status}`);
+
+ let parts = status.split(":");
+ update.state = parts[0];
+ if (update.state == STATE_APPLYING) {
+ LOG(
+ "UpdateManager:refreshUpdateStatus - Staging appears to have crashed."
+ );
+ update.state = STATE_FAILED;
+ update.errorCode = ERR_UPDATER_CRASHED;
+ } else if (update.state == STATE_FAILED) {
+ LOG("UpdateManager:refreshUpdateStatus - Staging failed.");
+ if (parts[1]) {
+ update.errorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
+ } else {
+ update.errorCode = INVALID_UPDATER_STATUS_CODE;
+ }
+ }
+
+ // Rotate the update logs so the update log isn't removed if a complete
+ // update is downloaded. By passing false the patch directory won't be
+ // removed.
+ cleanUpReadyUpdateDir(false);
+
+ if (update.state == STATE_FAILED) {
+ let isMemError = isMemoryAllocationErrorCode(update.errorCode);
+ if (
+ update.errorCode == DELETE_ERROR_STAGING_LOCK_FILE ||
+ update.errorCode == UNEXPECTED_STAGING_ERROR ||
+ isMemError
+ ) {
+ update.state = getBestPendingState();
+ writeStatusFile(getReadyUpdateDir(), update.state);
+ if (isMemError) {
+ LOG(
+ `UpdateManager:refreshUpdateStatus - Updater failed to ` +
+ `allocate enough memory to successfully stage. Setting ` +
+ `status to "${update.state}"`
+ );
+ } else {
+ LOG(
+ `UpdateManager:refreshUpdateStatus - Unexpected staging error. ` +
+ `Setting status to "${update.state}"`
+ );
+ }
+ } else if (isServiceSpecificErrorCode(update.errorCode)) {
+ // Sometimes when staging, we might encounter an error that is
+ // specific to the Maintenance Service. If this happens, we should try
+ // to update without the Service.
+ LOG(
+ `UpdateManager:refreshUpdateStatus - Encountered service ` +
+ `specific error code: ${update.errorCode}. Will try installing ` +
+ `update without the Maintenance Service. Setting state to pending`
+ );
+ update.state = STATE_PENDING;
+ writeStatusFile(getReadyUpdateDir(), update.state);
+ } else {
+ LOG(
+ "UpdateManager:refreshUpdateStatus - Attempting handleUpdateFailure"
+ );
+ if (!handleUpdateFailure(update)) {
+ LOG(
+ "UpdateManager:refreshUpdateStatus - handleUpdateFailure " +
+ "failed. Attempting to fall back to complete update."
+ );
+ await handleFallbackToCompleteUpdate();
+ }
+ }
+ }
+ if (update.state == STATE_APPLIED && shouldUseService()) {
+ LOG(
+ `UpdateManager:refreshUpdateStatus - Staging successful. ` +
+ `Setting status to "${STATE_APPLIED_SERVICE}"`
+ );
+ writeStatusFile(
+ getReadyUpdateDir(),
+ (update.state = STATE_APPLIED_SERVICE)
+ );
+ }
+
+ // Now that the active update's properties have been updated write the
+ // active-update.xml to disk. Since there have been no changes to the
+ // update history the updates.xml will not be written to disk.
+ this.saveUpdates();
+
+ // Send an observer notification which the app update doorhanger uses to
+ // display a restart notification after any langpacks have staged.
+ await promiseLangPacksUpdated(update);
+
+ if (
+ update.state == STATE_APPLIED ||
+ update.state == STATE_APPLIED_SERVICE ||
+ update.state == STATE_PENDING ||
+ update.state == STATE_PENDING_SERVICE ||
+ update.state == STATE_PENDING_ELEVATE
+ ) {
+ LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_PENDING");
+ transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
+ }
+
+ LOG(
+ "UpdateManager:refreshUpdateStatus - Notifying observers that " +
+ "the update was staged. topic: update-staged, status: " +
+ update.state
+ );
+ Services.obs.notifyObservers(update, "update-staged", update.state);
+ } finally {
+ // This function being called is the one thing that tells us that staging
+ // is done so be very sure that we don't exit it leaving the current
+ // state at STATE_STAGING.
+ // The only cases where we haven't already done a state transition are
+ // error cases, so if another state isn't set, assume that we hit an error
+ // and aborted the update.
+ if (
+ lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING
+ ) {
+ LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_IDLE");
+ transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
+ }
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ elevationOptedIn: function UM_elevationOptedIn() {
+ // The user has been been made aware that the update requires elevation.
+ let update = this._readyUpdate;
+ if (!update) {
+ return;
+ }
+ let status = readStatusFile(getReadyUpdateDir());
+ let parts = status.split(":");
+ update.state = parts[0];
+ if (update.state == STATE_PENDING_ELEVATE) {
+ LOG("UpdateManager:elevationOptedIn - Setting state to pending.");
+ // Proceed with the pending update.
+ // Note: STATE_PENDING_ELEVATE stands for "pending user's approval to
+ // proceed with an elevated update". As long as we see this state, we will
+ // notify the user of the availability of an update that requires
+ // elevation. |elevationOptedIn| (this function) is called when the user
+ // gives us approval to proceed, so we want to switch to STATE_PENDING.
+ // The updater then detects whether or not elevation is required and
+ // displays the elevation prompt if necessary. This last step does not
+ // depend on the state in the status file.
+ writeStatusFile(getReadyUpdateDir(), STATE_PENDING);
+ } else {
+ LOG("UpdateManager:elevationOptedIn - Not in pending-elevate state.");
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ cleanupDownloadingUpdate: function UM_cleanupDownloadingUpdate() {
+ LOG(
+ "UpdateManager:cleanupDownloadingUpdate - cleaning up downloading update."
+ );
+ cleanupDownloadingUpdate();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ cleanupReadyUpdate: function UM_cleanupReadyUpdate() {
+ LOG("UpdateManager:cleanupReadyUpdate - cleaning up ready update.");
+ cleanupReadyUpdate();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ doInstallCleanup: async function UM_doInstallCleanup(isUninstall) {
+ LOG("UpdateManager:doInstallCleanup - cleaning up");
+ let completionPromises = [];
+
+ const delete_or_log = path =>
+ IOUtils.remove(path).catch(ex =>
+ console.error(`Failed to delete ${path}`, ex)
+ );
+
+ for (const key of [KEY_OLD_UPDROOT, KEY_UPDROOT]) {
+ const root = Services.dirsvc.get(key, Ci.nsIFile);
+
+ const activeUpdateXml = root.clone();
+ activeUpdateXml.append(FILE_ACTIVE_UPDATE_XML);
+ completionPromises.push(delete_or_log(activeUpdateXml.path));
+
+ const downloadingMar = root.clone();
+ downloadingMar.append(DIR_UPDATES);
+ downloadingMar.append(DIR_UPDATE_DOWNLOADING);
+ downloadingMar.append(FILE_UPDATE_MAR);
+ completionPromises.push(delete_or_log(downloadingMar.path));
+
+ const readyDir = root.clone();
+ readyDir.append(DIR_UPDATES);
+ readyDir.append(DIR_UPDATE_READY);
+ const readyMar = readyDir.clone();
+ readyMar.append(FILE_UPDATE_MAR);
+ completionPromises.push(delete_or_log(readyMar.path));
+ const readyStatus = readyDir.clone();
+ readyStatus.append(FILE_UPDATE_STATUS);
+ completionPromises.push(delete_or_log(readyStatus.path));
+ const versionFile = readyDir.clone();
+ versionFile.append(FILE_UPDATE_VERSION);
+ completionPromises.push(delete_or_log(versionFile.path));
+ }
+
+ return Promise.allSettled(completionPromises);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ doUninstallCleanup: async function UM_doUninstallCleanup(isUninstall) {
+ LOG("UpdateManager:doUninstallCleanup - cleaning up.");
+ let completionPromises = [];
+
+ completionPromises.push(
+ IOUtils.remove(Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile).path, {
+ recursive: true,
+ }).catch(ex => console.error("Failed to remove update directory", ex))
+ );
+ completionPromises.push(
+ IOUtils.remove(Services.dirsvc.get(KEY_OLD_UPDROOT, Ci.nsIFile).path, {
+ recursive: true,
+ }).catch(ex => console.error("Failed to remove old update directory", ex))
+ );
+
+ return Promise.allSettled(completionPromises);
+ },
+
+ classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"),
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateManager", "nsIObserver"]),
+};
+
+/**
+ * CheckerService
+ * Provides an interface for checking for new updates. When more checks are
+ * made while an equivalent check is already in-progress, they will be coalesced
+ * into a single update check request.
+ */
+export class CheckerService {
+ #nextUpdateCheckId = 1;
+
+ // Most of the update checking data is looked up via a "request key". This
+ // allows us to lookup the request key for a particular check id, since
+ // multiple checks can correspond to a single request.
+ // When a check is cancelled or completed, it will be removed from this
+ // object.
+ #requestKeyByCheckId = {};
+
+ // This object will relate request keys to update check data objects. The
+ // format of the update check data objects is defined by
+ // #makeUpdateCheckDataObject, below.
+ // When an update request is cancelled (by all of the corresponding update
+ // checks being cancelled) or completed, its key will be removed from this
+ // object.
+ #updateCheckData = {};
+
+ #makeUpdateCheckDataObject(type, promise) {
+ return { type, promise, request: null };
+ }
+
+ /**
+ * Indicates whether the passed parameter is one of the valid enumerated
+ * values that indicates a type of update check.
+ */
+ #validUpdateCheckType(checkType) {
+ return [
+ Ci.nsIUpdateChecker.BACKGROUND_CHECK,
+ Ci.nsIUpdateChecker.FOREGROUND_CHECK,
+ ].includes(checkType);
+ }
+
+ #getCanMigrate() {
+ if (AppConstants.platform != "win") {
+ return false;
+ }
+
+ // The first element of the array is whether the build target is 32 or 64
+ // bit and the third element of the array is whether the client's Windows OS
+ // system processor is 32 or 64 bit.
+ let aryABI = lazy.UpdateUtils.ABI.split("-");
+ if (aryABI[0] != "x86" || aryABI[2] != "x64") {
+ return false;
+ }
+
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ let regPath =
+ "SOFTWARE\\Mozilla\\" + Services.appinfo.name + "\\32to64DidMigrate";
+ let regValHKCU = lazy.WindowsRegistry.readRegKey(
+ wrk.ROOT_KEY_CURRENT_USER,
+ regPath,
+ "Never",
+ wrk.WOW64_32
+ );
+ let regValHKLM = lazy.WindowsRegistry.readRegKey(
+ wrk.ROOT_KEY_LOCAL_MACHINE,
+ regPath,
+ "Never",
+ wrk.WOW64_32
+ );
+ // The Never registry key value allows configuring a system to never migrate
+ // any of the installations.
+ if (regValHKCU === 1 || regValHKLM === 1) {
+ LOG(
+ "CheckerService:#getCanMigrate - all installations should not be " +
+ "migrated"
+ );
+ return false;
+ }
+
+ let appBaseDirPath = getAppBaseDir().path;
+ regValHKCU = lazy.WindowsRegistry.readRegKey(
+ wrk.ROOT_KEY_CURRENT_USER,
+ regPath,
+ appBaseDirPath,
+ wrk.WOW64_32
+ );
+ regValHKLM = lazy.WindowsRegistry.readRegKey(
+ wrk.ROOT_KEY_LOCAL_MACHINE,
+ regPath,
+ appBaseDirPath,
+ wrk.WOW64_32
+ );
+ // When the registry value is 1 for the installation directory path value
+ // name then the installation has already been migrated once or the system
+ // was configured to not migrate that installation.
+ if (regValHKCU === 1 || regValHKLM === 1) {
+ LOG(
+ "CheckerService:#getCanMigrate - this installation should not be " +
+ "migrated"
+ );
+ return false;
+ }
+
+ // When the registry value is 0 for the installation directory path value
+ // name then the installation has updated to Firefox 56 and can be migrated.
+ if (regValHKCU === 0 || regValHKLM === 0) {
+ LOG("CheckerService:#getCanMigrate - this installation can be migrated");
+ return true;
+ }
+
+ LOG(
+ "CheckerService:#getCanMigrate - no registry entries for this " +
+ "installation"
+ );
+ return false;
+ }
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ async getUpdateURL(checkType) {
+ LOG("CheckerService:getUpdateURL - checkType: " + checkType);
+ if (!this.#validUpdateCheckType(checkType)) {
+ LOG("CheckerService:getUpdateURL - Invalid checkType");
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let url = Services.appinfo.updateURL;
+ let updatePin;
+
+ if (Services.policies) {
+ let policies = Services.policies.getActivePolicies();
+ if (policies) {
+ if ("AppUpdateURL" in policies) {
+ url = policies.AppUpdateURL.toString();
+ }
+ if ("AppUpdatePin" in policies) {
+ updatePin = policies.AppUpdatePin;
+
+ // Scalar ID: update.version_pin
+ AUSTLMY.pingPinPolicy(updatePin);
+ }
+ }
+ }
+
+ if (!url) {
+ LOG("CheckerService:getUpdateURL - update URL not defined");
+ return null;
+ }
+
+ url = await lazy.UpdateUtils.formatUpdateURL(url);
+
+ if (checkType == Ci.nsIUpdateChecker.FOREGROUND_CHECK) {
+ url += (url.includes("?") ? "&" : "?") + "force=1";
+ }
+
+ if (this.#getCanMigrate()) {
+ url += (url.includes("?") ? "&" : "?") + "mig64=1";
+ }
+
+ if (updatePin) {
+ url +=
+ (url.includes("?") ? "&" : "?") +
+ "pin=" +
+ encodeURIComponent(updatePin);
+ }
+
+ LOG("CheckerService:getUpdateURL - update URL: " + url);
+ return url;
+ }
+
+ /**
+ * See nsIUpdateService.idl
+ */
+
+ checkForUpdates(checkType) {
+ LOG("CheckerService:checkForUpdates - checkType: " + checkType);
+ if (!this.#validUpdateCheckType(checkType)) {
+ LOG("CheckerService:checkForUpdates - Invalid checkType");
+ throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ let checkId = this.#nextUpdateCheckId;
+ this.#nextUpdateCheckId += 1;
+
+ // `checkType == FOREGROUND_CHECK`` can override `canCheckForUpdates`. But
+ // nothing should override enterprise policies.
+ if (lazy.AUS.disabled) {
+ LOG("CheckerService:checkForUpdates - disabled by policy");
+ return this.#getChecksNotAllowedObject(checkId);
+ }
+ if (
+ checkType == Ci.nsIUpdateChecker.BACKGROUND_CHECK &&
+ !lazy.AUS.canCheckForUpdates
+ ) {
+ LOG("CheckerService:checkForUpdates - !canCheckForUpdates");
+ return this.#getChecksNotAllowedObject(checkId);
+ }
+
+ // We want to combine simultaneous requests, but only ones that are
+ // equivalent. If, say, one of them uses the force parameter and one
+ // doesn't, we want those two requests to remain separate. This key will
+ // allow us to map equivalent requests together. It is also the key that we
+ // use to lookup the update check data in this.#updateCheckData.
+ let requestKey = checkType;
+
+ if (requestKey in this.#updateCheckData) {
+ LOG(
+ `CheckerService:checkForUpdates - Connecting check id ${checkId} to ` +
+ `existing check request.`
+ );
+ } else {
+ LOG(
+ `CheckerService:checkForUpdates - Making new check request for check ` +
+ `id ${checkId}.`
+ );
+ this.#updateCheckData[requestKey] = this.#makeUpdateCheckDataObject(
+ checkType,
+ this.#updateCheck(checkType, requestKey)
+ );
+ }
+
+ this.#requestKeyByCheckId[checkId] = requestKey;
+
+ return {
+ id: checkId,
+ result: this.#updateCheckData[requestKey].promise,
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]),
+ };
+ }
+
+ #getChecksNotAllowedObject(checkId) {
+ return {
+ id: checkId,
+ result: Promise.resolve(
+ Object.freeze({
+ checksAllowed: false,
+ succeeded: false,
+ request: null,
+ updates: [],
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]),
+ })
+ ),
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheck"]),
+ };
+ }
+
+ async #updateCheck(checkType, requestKey) {
+ await waitForOtherInstances();
+
+ let url;
+ try {
+ url = await this.getUpdateURL(checkType);
+ } catch (ex) {}
+
+ if (!url) {
+ LOG("CheckerService:#updateCheck - !url");
+ return this.#getCheckFailedObject("update_url_not_available");
+ }
+
+ let request = new XMLHttpRequest();
+ request.open("GET", url, true);
+ request.channel.notificationCallbacks = new lazy.CertUtils.BadCertHandler(
+ false
+ );
+ // Prevent the request from reading from the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ // Disable cutting edge features, like TLS 1.3, where middleboxes might
+ // brick us
+ request.channel.QueryInterface(
+ Ci.nsIHttpChannelInternal
+ ).beConservative = true;
+
+ request.overrideMimeType("text/xml");
+ // The Cache-Control header is only interpreted by proxies and the
+ // final destination. It does not help if a resource is already
+ // cached locally.
+ request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ request.setRequestHeader("Pragma", "no-cache");
+
+ const UPDATE_CHECK_LOAD_SUCCESS = 1;
+ const UPDATE_CHECK_LOAD_ERROR = 2;
+ const UPDATE_CHECK_CANCELLED = 3;
+
+ let result = await new Promise(resolve => {
+ // It's important that nothing potentially asynchronous happens between
+ // checking if the request has been cancelled and starting the request.
+ // If an update check cancellation happens before dispatching the request
+ // and we end up dispatching it anyways, we will never call cancel on the
+ // request later and the cancellation effectively won't happen.
+ if (!(requestKey in this.#updateCheckData)) {
+ LOG(
+ "CheckerService:#updateCheck - check was cancelled before request " +
+ "was able to start"
+ );
+ resolve(UPDATE_CHECK_CANCELLED);
+ return;
+ }
+
+ let onLoad = event => {
+ request.removeEventListener("load", onLoad);
+ LOG("CheckerService:#updateCheck - request got 'load' event");
+ resolve(UPDATE_CHECK_LOAD_SUCCESS);
+ };
+ request.addEventListener("load", onLoad);
+ let onError = event => {
+ request.removeEventListener("error", onLoad);
+ LOG("CheckerService:#updateCheck - request got 'error' event");
+ resolve(UPDATE_CHECK_LOAD_ERROR);
+ };
+ request.addEventListener("error", onError);
+
+ LOG("CheckerService:#updateCheck - sending request to: " + url);
+ request.send(null);
+ this.#updateCheckData[requestKey].request = request;
+ });
+
+ // Remove all entries for this request key. This marks the request and the
+ // associated check ids as no longer in-progress.
+ delete this.#updateCheckData[requestKey];
+ for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
+ if (this.#requestKeyByCheckId[checkId] == requestKey) {
+ delete this.#requestKeyByCheckId[checkId];
+ }
+ }
+
+ if (result == UPDATE_CHECK_CANCELLED) {
+ return this.#getCheckFailedObject(Cr.NS_BINDING_ABORTED);
+ }
+
+ if (result == UPDATE_CHECK_LOAD_ERROR) {
+ let status = this.#getChannelStatus(request);
+ LOG("CheckerService:#updateCheck - Failed. request.status: " + status);
+
+ // Set MitM pref.
+ try {
+ let secInfo = request.channel.securityInfo;
+ if (secInfo.serverCert && secInfo.serverCert.issuerName) {
+ Services.prefs.setStringPref(
+ "security.pki.mitm_canary_issuer",
+ secInfo.serverCert.issuerName
+ );
+ }
+ } catch (e) {
+ LOG("CheckerService:#updateCheck - Getting secInfo failed.");
+ }
+
+ return this.#getCheckFailedObject(status, 404, request);
+ }
+
+ LOG("CheckerService:#updateCheck - request completed downloading document");
+ Services.prefs.clearUserPref("security.pki.mitm_canary_issuer");
+ // Check whether there is a mitm, i.e. check whether the root cert is
+ // built-in or not.
+ try {
+ let sslStatus = request.channel.securityInfo;
+ if (sslStatus && sslStatus.succeededCertChain) {
+ let rootCert = null;
+ // The root cert is the last cert in the chain.
+ if (sslStatus.succeededCertChain.length) {
+ rootCert =
+ sslStatus.succeededCertChain[
+ sslStatus.succeededCertChain.length - 1
+ ];
+ }
+ if (rootCert) {
+ Services.prefs.setBoolPref(
+ "security.pki.mitm_detected",
+ !rootCert.isBuiltInRoot
+ );
+ }
+ }
+ } catch (e) {
+ LOG("CheckerService:#updateCheck - Getting sslStatus failed.");
+ }
+
+ let updates;
+ try {
+ // Analyze the resulting DOM and determine the set of updates.
+ updates = this.#parseUpdates(request);
+ } catch (e) {
+ LOG(
+ "CheckerService:#updateCheck - there was a problem checking for " +
+ "updates. Exception: " +
+ e
+ );
+ let status = this.#getChannelStatus(request);
+ // If we can't find an error string specific to this status code,
+ // just use the 200 message from above, which means everything
+ // "looks" fine but there was probably an XML error or a bogus file.
+ return this.#getCheckFailedObject(status, 200, request);
+ }
+
+ LOG(
+ "CheckerService:#updateCheck - number of updates available: " +
+ updates.length
+ );
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
+ }
+
+ return Object.freeze({
+ checksAllowed: true,
+ succeeded: true,
+ request,
+ updates,
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]),
+ });
+ }
+
+ /**
+ * @param errorCode
+ * The error code to include in the return value. If possible, we
+ * will get the update status text based on this error code.
+ * @param defaultCode
+ * Optional. The error code to use to get the status text if there
+ * isn't status text available for `errorCode`.
+ * @param request
+ * The XMLHttpRequest used to check for updates. Or null, if one was
+ * never constructed.
+ * @returns An nsIUpdateCheckResult object indicating an error, using the
+ * error data passed to this function.
+ */
+ #getCheckFailedObject(
+ errorCode,
+ defaultCode = Cr.NS_BINDING_FAILED,
+ request = null
+ ) {
+ let update = new Update(null);
+ update.errorCode = errorCode;
+ update.statusText = getStatusTextFromCode(errorCode, defaultCode);
+
+ if (errorCode == Cr.NS_ERROR_OFFLINE) {
+ // We use a separate constant here because nsIUpdate.errorCode is signed
+ update.errorCode = NETWORK_ERROR_OFFLINE;
+ } else if (this.#isHttpStatusCode(errorCode)) {
+ update.errorCode = HTTP_ERROR_OFFSET + errorCode;
+ }
+
+ return Object.freeze({
+ checksAllowed: true,
+ succeeded: false,
+ request,
+ updates: [update],
+ QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckResult"]),
+ });
+ }
+
+ /**
+ * Returns the status code for the XMLHttpRequest
+ */
+ #getChannelStatus(request) {
+ var status = 0;
+ try {
+ status = request.status;
+ } catch (e) {}
+
+ if (status == 0) {
+ status = request.channel.QueryInterface(Ci.nsIRequest).status;
+ }
+ return status;
+ }
+
+ #isHttpStatusCode(status) {
+ return status >= 100 && status <= 599;
+ }
+
+ /**
+ * @param request
+ * The XMLHttpRequest that successfully loaded the update XML.
+ * @returns An array of 0 or more nsIUpdate objects describing the available
+ * updates.
+ * @throws If the XML document element node name is not updates.
+ */
+ #parseUpdates(request) {
+ let updatesElement = request.responseXML.documentElement;
+ if (!updatesElement) {
+ LOG("CheckerService:#parseUpdates - empty updates document?!");
+ return [];
+ }
+
+ if (updatesElement.nodeName != "updates") {
+ LOG("CheckerService:#parseUpdates - unexpected node name!");
+ throw new Error(
+ "Unexpected node name, expected: updates, got: " +
+ updatesElement.nodeName
+ );
+ }
+
+ let updates = [];
+ for (const updateElement of updatesElement.childNodes) {
+ if (
+ updateElement.nodeType != updateElement.ELEMENT_NODE ||
+ updateElement.localName != "update"
+ ) {
+ continue;
+ }
+
+ let update;
+ try {
+ update = new Update(updateElement);
+ } catch (e) {
+ LOG("CheckerService:#parseUpdates - invalid <update/>, ignoring...");
+ continue;
+ }
+ update.serviceURL = request.responseURL;
+ update.channel = lazy.UpdateUtils.UpdateChannel;
+ updates.push(update);
+ }
+
+ return updates;
+ }
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ stopCheck(checkId) {
+ if (!(checkId in this.#requestKeyByCheckId)) {
+ LOG(`CheckerService:stopCheck - Non-existent check id ${checkId}`);
+ return;
+ }
+ LOG(`CheckerService:stopCheck - Cancelling check id ${checkId}`);
+ let requestKey = this.#requestKeyByCheckId[checkId];
+ delete this.#requestKeyByCheckId[checkId];
+ if (Object.values(this.#requestKeyByCheckId).includes(requestKey)) {
+ LOG(
+ `CheckerService:stopCheck - Not actually cancelling request because ` +
+ `other check id's depend on it.`
+ );
+ } else {
+ LOG(
+ `CheckerService:stopCheck - This is the last check using this ` +
+ `request. Cancelling the request now.`
+ );
+ let request = this.#updateCheckData[requestKey].request;
+ delete this.#updateCheckData[requestKey];
+ if (request) {
+ LOG(`CheckerService:stopCheck - Aborting XMLHttpRequest`);
+ request.abort();
+ } else {
+ LOG(
+ `CheckerService:stopCheck - Not aborting XMLHttpRequest. It ` +
+ `doesn't appear to have started yet.`
+ );
+ }
+ }
+ }
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ stopAllChecks() {
+ LOG("CheckerService:stopAllChecks - stopping all checks.");
+ for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
+ this.stopCheck(checkId);
+ }
+ }
+
+ classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}");
+ QueryInterface = ChromeUtils.generateQI(["nsIUpdateChecker"]);
+}
+
+/**
+ * Manages the download of updates
+ * @param background
+ * Whether or not this downloader is operating in background
+ * update mode.
+ * @param updateService
+ * The update service that created this downloader.
+ * @constructor
+ */
+function Downloader(updateService) {
+ LOG("Creating Downloader");
+ this.updateService = updateService;
+}
+Downloader.prototype = {
+ /**
+ * The nsIUpdatePatch that we are downloading
+ */
+ _patch: null,
+
+ /**
+ * The nsIUpdate that we are downloading
+ */
+ _update: null,
+
+ /**
+ * The nsIRequest object handling the download.
+ */
+ _request: null,
+
+ /**
+ * Whether or not the update being downloaded is a complete replacement of
+ * the user's existing installation or a patch representing the difference
+ * between the new version and the previous version.
+ */
+ isCompleteUpdate: null,
+
+ /**
+ * We get the nsIRequest from nsIBITS asynchronously. When downloadUpdate has
+ * been called, but this._request is not yet valid, _pendingRequest will be
+ * a promise that will resolve when this._request has been set.
+ */
+ _pendingRequest: null,
+
+ /**
+ * When using BITS, cancel actions happen asynchronously. This variable
+ * keeps track of any cancel action that is in-progress.
+ * If the cancel action fails, this will be set back to null so that the
+ * action can be attempted again. But if the cancel action succeeds, the
+ * resolved promise will remain stored in this variable to prevent cancel
+ * from being called twice (which, for BITS, is an error).
+ */
+ _cancelPromise: null,
+
+ /**
+ * BITS receives progress notifications slowly, unless a user is watching.
+ * This tracks what frequency notifications are happening at.
+ *
+ * This is needed because BITS downloads are started asynchronously.
+ * Specifically, this is needed to prevent a situation where the download is
+ * still starting (Downloader._pendingRequest has not resolved) when the first
+ * observer registers itself. Without this variable, there is no way of
+ * knowing whether the download was started as Active or Idle and, therefore,
+ * we don't know if we need to start Active mode when _pendingRequest
+ * resolves.
+ */
+ _bitsActiveNotifications: false,
+
+ /**
+ * This is a function that when called will stop the update process from
+ * waiting for language pack updates. This is for safety to ensure that a
+ * problem in the add-ons manager doesn't delay updates by much.
+ */
+ _langPackTimeout: null,
+
+ /**
+ * If gOnlyDownloadUpdatesThisSession is true, we prevent the update process
+ * from progressing past the downloading stage. If the download finishes,
+ * pretend that it hasn't in order to keep the current update in the
+ * "downloading" state.
+ */
+ _pretendingDownloadIsNotDone: false,
+
+ /**
+ * Cancels the active download.
+ *
+ * For a BITS download, this will cancel and remove the download job. For
+ * an nsIIncrementalDownload, this will stop the download, but leaves the
+ * data around to allow the transfer to be resumed later.
+ */
+ cancel: async function Downloader_cancel(cancelError) {
+ LOG("Downloader: cancel");
+ if (cancelError === undefined) {
+ cancelError = Cr.NS_BINDING_ABORTED;
+ }
+ if (this.usingBits) {
+ // If a cancel action is already in progress, just return when that
+ // promise resolved. Trying to cancel the same request twice is an error.
+ if (this._cancelPromise) {
+ await this._cancelPromise;
+ return;
+ }
+
+ if (this._pendingRequest) {
+ await this._pendingRequest;
+ }
+ if (this._patch.getProperty("bitsId") != null) {
+ // Make sure that we don't try to resume this download after it was
+ // cancelled.
+ this._patch.deleteProperty("bitsId");
+ }
+ try {
+ this._cancelPromise = this._request.cancelAsync(cancelError);
+ await this._cancelPromise;
+ } catch (e) {
+ // On success, we will not set the cancel promise to null because
+ // we want to prevent two cancellations of the same request. But
+ // retrying after a failed cancel is not an error, so we will set the
+ // cancel promise to null in the failure case.
+ this._cancelPromise = null;
+ throw e;
+ }
+ } else if (this._request && this._request instanceof Ci.nsIRequest) {
+ // Normally, cancelling an nsIIncrementalDownload results in it stopping
+ // the download but leaving the downloaded data so that we can resume the
+ // download later. If we've already finished the download, there is no
+ // transfer to stop.
+ // Note that this differs from the BITS case. Cancelling a BITS job, even
+ // when the transfer has completed, results in all data being deleted.
+ // Therefore, even if the transfer has completed, cancelling a BITS job
+ // has effects that we must not skip.
+ if (this._pretendingDownloadIsNotDone) {
+ LOG(
+ "Downloader: cancel - Ignoring cancel request of finished download"
+ );
+ } else {
+ this._request.cancel(cancelError);
+ }
+ }
+ },
+
+ /**
+ * Verify the downloaded file. We assume that the download is complete at
+ * this point.
+ */
+ _verifyDownload: function Downloader__verifyDownload() {
+ LOG("Downloader:_verifyDownload called");
+ if (!this._request) {
+ AUSTLMY.pingDownloadCode(
+ this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST
+ );
+ return false;
+ }
+
+ let destination = getDownloadingUpdateDir();
+ destination.append(FILE_UPDATE_MAR);
+
+ // Ensure that the file size matches the expected file size.
+ if (destination.fileSize != this._patch.size) {
+ LOG("Downloader:_verifyDownload downloaded size != expected size.");
+ AUSTLMY.pingDownloadCode(
+ this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL
+ );
+ return false;
+ }
+
+ LOG("Downloader:_verifyDownload downloaded size == expected size.");
+ return true;
+ },
+
+ /**
+ * Select the patch to use given the current state of updateDir and the given
+ * set of update patches.
+ * @param update
+ * A nsIUpdate object to select a patch from
+ * @param updateDir
+ * A nsIFile representing the update directory
+ * @return A nsIUpdatePatch object to download
+ */
+ _selectPatch: function Downloader__selectPatch(update, updateDir) {
+ // Given an update to download, we will always try to download the patch
+ // for a partial update over the patch for a full update.
+
+ // Look to see if any of the patches in the Update object has been
+ // pre-selected for download, otherwise we must figure out which one
+ // to select ourselves.
+ var selectedPatch = update.selectedPatch;
+
+ var state = selectedPatch ? selectedPatch.state : STATE_NONE;
+
+ // If this is a patch that we know about, then select it. If it is a patch
+ // that we do not know about, then remove it and use our default logic.
+ var useComplete = false;
+ if (selectedPatch) {
+ LOG(
+ "Downloader:_selectPatch - found existing patch with state: " + state
+ );
+ if (state == STATE_DOWNLOADING) {
+ LOG("Downloader:_selectPatch - resuming download");
+ return selectedPatch;
+ }
+ if (
+ state == STATE_PENDING ||
+ state == STATE_PENDING_SERVICE ||
+ state == STATE_PENDING_ELEVATE ||
+ state == STATE_APPLIED ||
+ state == STATE_APPLIED_SERVICE
+ ) {
+ LOG("Downloader:_selectPatch - already downloaded");
+ return null;
+ }
+
+ // When downloading the patch failed using BITS, there hasn't been an
+ // attempt to download the patch using the internal application download
+ // mechanism, and an attempt to stage or apply the patch hasn't failed
+ // which indicates that a different patch should be downloaded since
+ // re-downloading the same patch with the internal application download
+ // mechanism will likely also fail when trying to stage or apply it then
+ // try to download the same patch using the internal application download
+ // mechanism.
+ selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag);
+ if (
+ selectedPatch.getProperty("bitsResult") != null &&
+ selectedPatch.getProperty("internalResult") == null &&
+ !selectedPatch.errorCode
+ ) {
+ LOG(
+ "Downloader:_selectPatch - Falling back to non-BITS download " +
+ "mechanism for the same patch due to existing BITS result: " +
+ selectedPatch.getProperty("bitsResult")
+ );
+ return selectedPatch;
+ }
+
+ if (update && selectedPatch.type == "complete") {
+ // This is a pretty fatal error. Just bail.
+ LOG("Downloader:_selectPatch - failed to apply complete patch!");
+ cleanupDownloadingUpdate();
+ return null;
+ }
+
+ // Something went wrong when we tried to apply the previous patch.
+ // Try the complete patch next time.
+ useComplete = true;
+ selectedPatch = null;
+ }
+
+ // If we were not able to discover an update from a previous download, we
+ // select the best patch from the given set.
+ var partialPatch = getPatchOfType(update, "partial");
+ if (!useComplete) {
+ selectedPatch = partialPatch;
+ }
+ if (!selectedPatch) {
+ if (lazy.UM.readyUpdate) {
+ // If we already have a ready update, we download partials only.
+ LOG(
+ "Downloader:_selectPatch - not selecting a complete patch because " +
+ "this is not the first download of the session"
+ );
+ return null;
+ }
+
+ if (partialPatch) {
+ partialPatch.selected = false;
+ }
+ selectedPatch = getPatchOfType(update, "complete");
+ }
+
+ // if update only contains a partial patch, selectedPatch == null here if
+ // the partial patch has been attempted and fails and we're trying to get a
+ // complete patch
+ if (selectedPatch) {
+ selectedPatch.selected = true;
+ update.isCompleteUpdate = selectedPatch.type == "complete";
+ }
+
+ LOG(
+ "Downloader:_selectPatch - Patch selected. Assigning update to " +
+ "downloadingUpdate."
+ );
+ lazy.UM.downloadingUpdate = update;
+
+ return selectedPatch;
+ },
+
+ /**
+ * Whether or not the user wants to be notified that an update is being
+ * downloaded.
+ */
+ get _notifyDuringDownload() {
+ return Services.prefs.getBoolPref(
+ PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD,
+ false
+ );
+ },
+
+ _notifyDownloadStatusObservers:
+ function Downloader_notifyDownloadStatusObservers() {
+ if (this._notifyDuringDownload) {
+ let status = this.updateService.isDownloading ? "downloading" : "idle";
+ Services.obs.notifyObservers(
+ this._update,
+ "update-downloading",
+ status
+ );
+ }
+ },
+
+ /**
+ * Whether or not we are currently downloading something.
+ */
+ get isBusy() {
+ return this._request != null || this._pendingRequest != null;
+ },
+
+ get usingBits() {
+ return this._pendingRequest != null || this._request instanceof BitsRequest;
+ },
+
+ /**
+ * Returns true if the specified patch can be downloaded with BITS.
+ */
+ _canUseBits: function Downloader__canUseBits(patch) {
+ if (getCanUseBits() != "CanUseBits") {
+ // This will have printed its own logging. No need to print more.
+ return false;
+ }
+ // Regardless of success or failure, don't download the same patch with BITS
+ // twice.
+ if (patch.getProperty("bitsResult") != null) {
+ LOG(
+ "Downloader:_canUseBits - Not using BITS because it was already tried"
+ );
+ return false;
+ }
+ LOG("Downloader:_canUseBits - Patch is able to use BITS download");
+ return true;
+ },
+
+ /**
+ * Instruct the add-ons manager to start downloading language pack updates in
+ * preparation for the current update.
+ */
+ _startLangPackUpdates: function Downloader__startLangPackUpdates() {
+ if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, false)) {
+ return;
+ }
+
+ // A promise that we can resolve at some point to time out the language pack
+ // update process.
+ let timeoutPromise = new Promise(resolve => {
+ this._langPackTimeout = resolve;
+ });
+
+ let update = unwrap(this._update);
+
+ let existing = LangPackUpdates.get(update);
+ if (existing) {
+ // We have already started staging lang packs for this update, no need to
+ // do it again.
+ return;
+ }
+
+ // Note that we don't care about success or failure here, either way we will
+ // continue with the update process.
+ let langPackPromise = lazy.AddonManager.stageLangpacksForAppUpdate(
+ update.appVersion,
+ update.appVersion
+ )
+ .catch(error => {
+ LOG(
+ `Add-ons manager threw exception while updating language packs: ${error}`
+ );
+ })
+ .finally(() => {
+ this._langPackTimeout = null;
+
+ if (TelemetryStopwatch.running("UPDATE_LANGPACK_OVERTIME", update)) {
+ TelemetryStopwatch.finish("UPDATE_LANGPACK_OVERTIME", update);
+ }
+ });
+
+ LangPackUpdates.set(
+ update,
+ Promise.race([langPackPromise, timeoutPromise])
+ );
+ },
+
+ /**
+ * Download and stage the given update.
+ * @param update
+ * A nsIUpdate object to download a patch for. Cannot be null.
+ */
+ downloadUpdate: async function Downloader_downloadUpdate(update) {
+ LOG("UpdateService:downloadUpdate");
+ if (!update) {
+ AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE);
+ throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
+ }
+
+ var updateDir = getDownloadingUpdateDir();
+
+ this._update = update;
+
+ // This function may return null, which indicates that there are no patches
+ // to download.
+ this._patch = this._selectPatch(update, updateDir);
+ if (!this._patch) {
+ LOG("Downloader:downloadUpdate - no patch to download");
+ AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH);
+ return false;
+ }
+ // The update and the patch implement nsIWritablePropertyBag. Expose that
+ // interface immediately after a patch is assigned so that
+ // this.(_patch|_update).(get|set)Property can always safely be called.
+ this._update.QueryInterface(Ci.nsIWritablePropertyBag);
+ this._patch.QueryInterface(Ci.nsIWritablePropertyBag);
+
+ if (
+ this._update.getProperty("disableBackgroundUpdates") != null &&
+ lazy.gIsBackgroundTaskMode
+ ) {
+ LOG(
+ "Downloader:downloadUpdate - Background update disabled by update " +
+ "advertisement"
+ );
+ return false;
+ }
+
+ this.isCompleteUpdate = this._patch.type == "complete";
+
+ let canUseBits = false;
+ // Allow the advertised update to disable BITS.
+ if (this._update.getProperty("disableBITS") != null) {
+ LOG(
+ "Downloader:downloadUpdate - BITS downloads disabled by update " +
+ "advertisement"
+ );
+ } else {
+ canUseBits = this._canUseBits(this._patch);
+ }
+
+ if (!canUseBits) {
+ this._pendingRequest = null;
+
+ let patchFile = updateDir.clone();
+ patchFile.append(FILE_UPDATE_MAR);
+
+ if (lazy.gIsBackgroundTaskMode) {
+ // We don't normally run a background update if we can't use BITS, but
+ // this branch is possible because we do fall back from BITS failures by
+ // attempting an internal download.
+ // If this happens, we are just going to need to wait for interactive
+ // Firefox to download the update. We don't, however, want to be in the
+ // "downloading" state when interactive Firefox runs because we want to
+ // download the newest update available which, at that point, may not be
+ // the one that we are currently trying to download.
+ // However, we can't just unconditionally clobber the current update
+ // because interactive Firefox might already be part way through an
+ // internal update download, and we definitely don't want to interrupt
+ // that.
+ let readyUpdateDir = getReadyUpdateDir();
+ let status = readStatusFile(readyUpdateDir);
+ // nsIIncrementalDownload doesn't use an intermediate download location
+ // for partially downloaded files. If we have started an update
+ // download with it, it will be available at its ultimate location.
+ if (!(status == STATE_DOWNLOADING && patchFile.exists())) {
+ LOG(
+ "Downloader:downloadUpdate - Can't download with internal " +
+ "downloader from a background task. Cleaning up downloading " +
+ "update."
+ );
+ cleanupDownloadingUpdate();
+ }
+ return false;
+ }
+
+ // The interval is 0 since there is no need to throttle downloads.
+ let interval = 0;
+
+ LOG(
+ "Downloader:downloadUpdate - Starting nsIIncrementalDownload with " +
+ "url: " +
+ this._patch.URL +
+ ", path: " +
+ patchFile.path +
+ ", interval: " +
+ interval
+ );
+ let uri = Services.io.newURI(this._patch.URL);
+
+ this._request = Cc[
+ "@mozilla.org/network/incremental-download;1"
+ ].createInstance(Ci.nsIIncrementalDownload);
+ this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval);
+ this._request.start(this, null);
+ } else {
+ let noProgressTimeout = BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS;
+ let monitorInterval = BITS_IDLE_POLL_RATE_MS;
+ this._bitsActiveNotifications = false;
+ // The monitor's timeout should be much greater than the longest monitor
+ // poll interval. If the timeout is too short, delay in the pipe to the
+ // update agent might cause BITS to falsely report an error, causing an
+ // unnecessary fallback to nsIIncrementalDownload.
+ let monitorTimeout = Math.max(10 * monitorInterval, 10 * 60 * 1000);
+ if (this.hasDownloadListeners) {
+ noProgressTimeout = BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS;
+ monitorInterval = BITS_ACTIVE_POLL_RATE_MS;
+ this._bitsActiveNotifications = true;
+ }
+
+ let updateRootDir = FileUtils.getDir(KEY_UPDROOT, [], true);
+ let jobName = "MozillaUpdate " + updateRootDir.leafName;
+ let updatePath = updateDir.path;
+ if (!Bits.initialized) {
+ Bits.init(jobName, updatePath, monitorTimeout);
+ }
+
+ this._cancelPromise = null;
+
+ let bitsId = this._patch.getProperty("bitsId");
+ if (bitsId) {
+ LOG(
+ "Downloader:downloadUpdate - Connecting to in-progress download. " +
+ "BITS ID: " +
+ bitsId
+ );
+
+ this._pendingRequest = Bits.monitorDownload(
+ bitsId,
+ monitorInterval,
+ this,
+ null
+ );
+ } else {
+ LOG(
+ "Downloader:downloadUpdate - Starting BITS download with url: " +
+ this._patch.URL +
+ ", updateDir: " +
+ updatePath +
+ ", filename: " +
+ FILE_UPDATE_MAR
+ );
+
+ this._pendingRequest = Bits.startDownload(
+ this._patch.URL,
+ FILE_UPDATE_MAR,
+ Ci.nsIBits.PROXY_PRECONFIG,
+ noProgressTimeout,
+ monitorInterval,
+ this,
+ null
+ );
+ }
+ let request;
+ try {
+ request = await this._pendingRequest;
+ } catch (error) {
+ if (
+ (error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_GET_BITS_JOB ||
+ error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_CONNECT_TO_BCM) &&
+ error.action == Ci.nsIBits.ERROR_ACTION_MONITOR_DOWNLOAD &&
+ error.stage == Ci.nsIBits.ERROR_STAGE_BITS_CLIENT &&
+ error.codeType == Ci.nsIBits.ERROR_CODE_TYPE_HRESULT &&
+ error.code == HRESULT_E_ACCESSDENIED
+ ) {
+ LOG(
+ "Downloader:downloadUpdate - Failed to connect to existing " +
+ "BITS job. It is likely owned by another user."
+ );
+ // This isn't really a failure code since the BITS job may be working
+ // just fine on another account, so convert this to a code that
+ // indicates that. This will make it easier to identify in telemetry.
+ error.type = Ci.nsIBits.ERROR_TYPE_ACCESS_DENIED_EXPECTED;
+ error.codeType = Ci.nsIBits.ERROR_CODE_TYPE_NONE;
+ error.code = null;
+ // When we detect this situation, disable BITS until Firefox shuts
+ // down. There are a couple of reasons for this. First, without any
+ // kind of flag, we enter an infinite loop here where we keep trying
+ // BITS over and over again (normally setting bitsResult prevents
+ // this, but we don't know the result of the BITS job, so we don't
+ // want to set that). Second, since we are trying to update, this
+ // process must have the update mutex. We don't ever give up the
+ // update mutex, so even if the other user starts Firefox, they will
+ // not complete the BITS job while this Firefox instance is around.
+ gBITSInUseByAnotherUser = true;
+ } else {
+ this._patch.setProperty("bitsResult", Cr.NS_ERROR_FAILURE);
+ lazy.UM.saveUpdates();
+
+ LOG(
+ "Downloader:downloadUpdate - Failed to start to BITS job. " +
+ "Error: " +
+ error
+ );
+ }
+
+ this._pendingRequest = null;
+
+ AUSTLMY.pingBitsError(this.isCompleteUpdate, error);
+
+ // Try download again with nsIIncrementalDownload
+ return this.downloadUpdate(this._update);
+ }
+
+ this._request = request;
+ this._patch.setProperty("bitsId", request.bitsId);
+
+ LOG(
+ "Downloader:downloadUpdate - BITS download running. BITS ID: " +
+ request.bitsId
+ );
+
+ if (this.hasDownloadListeners) {
+ this._maybeStartActiveNotifications();
+ } else {
+ this._maybeStopActiveNotifications();
+ }
+
+ lazy.UM.saveUpdates();
+ this._pendingRequest = null;
+ }
+
+ if (!lazy.UM.readyUpdate) {
+ LOG("Downloader:downloadUpdate - Setting status to downloading");
+ writeStatusFile(getReadyUpdateDir(), STATE_DOWNLOADING);
+ }
+ if (this._patch.state != STATE_DOWNLOADING) {
+ LOG("Downloader:downloadUpdate - Setting state to downloading");
+ this._patch.state = STATE_DOWNLOADING;
+ lazy.UM.saveUpdates();
+ }
+
+ // If we are downloading a second update, we don't change the state until
+ // STATE_SWAP.
+ if (lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING) {
+ LOG(
+ "Downloader:downloadUpdate - not setting state because download is " +
+ "already pending."
+ );
+ } else {
+ LOG(
+ "Downloader:downloadUpdate - setting currentState to STATE_DOWNLOADING"
+ );
+ transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
+ }
+
+ this._startLangPackUpdates();
+
+ this._notifyDownloadStatusObservers();
+
+ return true;
+ },
+
+ /**
+ * This is run when a download listener is added.
+ */
+ onDownloadListenerAdded: function Downloader_onDownloadListenerAdded() {
+ // Increase the status update frequency when someone starts listening
+ this._maybeStartActiveNotifications();
+ },
+
+ /**
+ * This is run when a download listener is removed.
+ */
+ onDownloadListenerRemoved: function Downloader_onDownloadListenerRemoved() {
+ // Decrease the status update frequency when no one is listening
+ if (!this.hasDownloadListeners) {
+ this._maybeStopActiveNotifications();
+ }
+ },
+
+ get hasDownloadListeners() {
+ return this.updateService.hasDownloadListeners;
+ },
+
+ /**
+ * This speeds up BITS progress notifications in response to a user watching
+ * the notifications.
+ */
+ _maybeStartActiveNotifications:
+ async function Downloader__maybeStartActiveNotifications() {
+ if (
+ this.usingBits &&
+ !this._bitsActiveNotifications &&
+ this.hasDownloadListeners &&
+ this._request
+ ) {
+ LOG(
+ "Downloader:_maybeStartActiveNotifications - Starting active " +
+ "notifications"
+ );
+ this._bitsActiveNotifications = true;
+ await Promise.all([
+ this._request
+ .setNoProgressTimeout(BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS)
+ .catch(error => {
+ LOG(
+ "Downloader:_maybeStartActiveNotifications - Failed to set " +
+ "no progress timeout. Error: " +
+ error
+ );
+ }),
+ this._request
+ .changeMonitorInterval(BITS_ACTIVE_POLL_RATE_MS)
+ .catch(error => {
+ LOG(
+ "Downloader:_maybeStartActiveNotifications - Failed to increase " +
+ "status update frequency. Error: " +
+ error
+ );
+ }),
+ ]);
+ }
+ },
+
+ /**
+ * This slows down BITS progress notifications in response to a user no longer
+ * watching the notifications.
+ */
+ _maybeStopActiveNotifications:
+ async function Downloader__maybeStopActiveNotifications() {
+ if (
+ this.usingBits &&
+ this._bitsActiveNotifications &&
+ !this.hasDownloadListeners &&
+ this._request
+ ) {
+ LOG(
+ "Downloader:_maybeStopActiveNotifications - Stopping active " +
+ "notifications"
+ );
+ this._bitsActiveNotifications = false;
+ await Promise.all([
+ this._request
+ .setNoProgressTimeout(BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS)
+ .catch(error => {
+ LOG(
+ "Downloader:_maybeStopActiveNotifications - Failed to set " +
+ "no progress timeout: " +
+ error
+ );
+ }),
+ this._request
+ .changeMonitorInterval(BITS_IDLE_POLL_RATE_MS)
+ .catch(error => {
+ LOG(
+ "Downloader:_maybeStopActiveNotifications - Failed to decrease " +
+ "status update frequency: " +
+ error
+ );
+ }),
+ ]);
+ }
+ },
+
+ /**
+ * When the async request begins
+ * @param request
+ * The nsIRequest object for the transfer
+ */
+ onStartRequest: function Downloader_onStartRequest(request) {
+ if (this.usingBits) {
+ LOG("Downloader:onStartRequest");
+ } else {
+ LOG(
+ "Downloader:onStartRequest - original URI spec: " +
+ request.URI.spec +
+ ", final URI spec: " +
+ request.finalURI.spec
+ );
+ // Set finalURL in onStartRequest if it is different.
+ if (this._patch.finalURL != request.finalURI.spec) {
+ this._patch.finalURL = request.finalURI.spec;
+ lazy.UM.saveUpdates();
+ }
+ }
+
+ this.updateService.forEachDownloadListener(listener => {
+ listener.onStartRequest(request);
+ });
+ },
+
+ /**
+ * When new data has been downloaded
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param progress
+ * The current number of bytes transferred
+ * @param maxProgress
+ * The total number of bytes that must be transferred
+ */
+ onProgress: function Downloader_onProgress(request, progress, maxProgress) {
+ LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress);
+
+ if (progress > this._patch.size) {
+ LOG(
+ "Downloader:onProgress - progress: " +
+ progress +
+ " is higher than patch size: " +
+ this._patch.size
+ );
+ AUSTLMY.pingDownloadCode(
+ this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER
+ );
+ this.cancel(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ // Wait until the transfer has started (progress > 0) to verify maxProgress
+ // so that we don't check it before it is available (in which case, -1 would
+ // have been passed).
+ if (progress > 0 && maxProgress != this._patch.size) {
+ LOG(
+ "Downloader:onProgress - maxProgress: " +
+ maxProgress +
+ " is not equal to expected patch size: " +
+ this._patch.size
+ );
+ AUSTLMY.pingDownloadCode(
+ this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL
+ );
+ this.cancel(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ this.updateService.forEachDownloadListener(listener => {
+ if (listener instanceof Ci.nsIProgressEventSink) {
+ listener.onProgress(request, progress, maxProgress);
+ }
+ });
+ this.updateService._consecutiveSocketErrors = 0;
+ },
+
+ /**
+ * When we have new status text
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param status
+ * A status code
+ * @param statusText
+ * Human readable version of |status|
+ */
+ onStatus: function Downloader_onStatus(request, status, statusText) {
+ LOG(
+ "Downloader:onStatus - status: " + status + ", statusText: " + statusText
+ );
+
+ this.updateService.forEachDownloadListener(listener => {
+ if (listener instanceof Ci.nsIProgressEventSink) {
+ listener.onStatus(request, status, statusText);
+ }
+ });
+ },
+
+ /**
+ * When data transfer ceases
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param status
+ * Status code containing the reason for the cessation.
+ */
+ /* eslint-disable-next-line complexity */
+ onStopRequest: async function Downloader_onStopRequest(request, status) {
+ if (gOnlyDownloadUpdatesThisSession) {
+ LOG(
+ "Downloader:onStopRequest - End of update download detected and " +
+ "ignored because we are restricted to update downloads this " +
+ "session. We will continue with this update next session."
+ );
+ // In order to keep the update from progressing past the downloading
+ // stage, we will pretend that the download is still going.
+ // A lot of this work is done for us by just not setting this._request to
+ // null, which usually signals that the transfer has completed.
+ this._pretendingDownloadIsNotDone = true;
+ // This notification is currently used only for testing.
+ Services.obs.notifyObservers(null, "update-download-restriction-hit");
+ return;
+ }
+
+ if (!this.usingBits) {
+ LOG(
+ "Downloader:onStopRequest - downloader: nsIIncrementalDownload, " +
+ "original URI spec: " +
+ request.URI.spec +
+ ", final URI spec: " +
+ request.finalURI.spec +
+ ", status: " +
+ status
+ );
+ } else {
+ LOG("Downloader:onStopRequest - downloader: BITS, status: " + status);
+ }
+
+ let bitsCompletionError;
+ if (this.usingBits) {
+ if (Components.isSuccessCode(status)) {
+ try {
+ await request.complete();
+ } catch (e) {
+ LOG(
+ "Downloader:onStopRequest - Unable to complete BITS download: " + e
+ );
+ status = Cr.NS_ERROR_FAILURE;
+ bitsCompletionError = e;
+ }
+ } else {
+ // BITS jobs that failed to complete should still have cancel called on
+ // them to remove the job.
+ try {
+ await this.cancel();
+ } catch (e) {
+ // This will fail if the job stopped because it was cancelled.
+ // Even if this is a "real" error, there isn't really anything to do
+ // about it, and it's not really a big problem. It just means that the
+ // BITS job will stay around until it is removed automatically
+ // (default of 90 days).
+ }
+ }
+ }
+
+ var state = this._patch.state;
+ var shouldShowPrompt = false;
+ var shouldRegisterOnlineObserver = false;
+ var shouldRetrySoon = false;
+ var deleteActiveUpdate = false;
+ let migratedToReadyUpdate = false;
+ let nonDownloadFailure = false;
+ var retryTimeout = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT,
+ DEFAULT_SOCKET_RETRYTIMEOUT
+ );
+ // Prevent the preference from setting a value greater than 10000.
+ retryTimeout = Math.min(retryTimeout, 10000);
+ var maxFail = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_SOCKET_MAXERRORS,
+ DEFAULT_SOCKET_MAX_ERRORS
+ );
+ // Prevent the preference from setting a value greater than 20.
+ maxFail = Math.min(maxFail, 20);
+ LOG(
+ "Downloader:onStopRequest - status: " +
+ status +
+ ", " +
+ "current fail: " +
+ this.updateService._consecutiveSocketErrors +
+ ", " +
+ "max fail: " +
+ maxFail +
+ ", " +
+ "retryTimeout: " +
+ retryTimeout
+ );
+ if (Components.isSuccessCode(status)) {
+ if (this._verifyDownload()) {
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS);
+
+ LOG(
+ "Downloader:onStopRequest - Clearing readyUpdate in preparation of " +
+ "moving downloadingUpdate into readyUpdate."
+ );
+
+ // Clear out any old update before we notify anyone about the new one.
+ // It will be invalid in a moment anyways when we call
+ // `cleanUpReadyUpdateDir()`.
+ lazy.UM.readyUpdate = null;
+
+ // We're about to clobber the ready update so we can replace it with the
+ // downloading update that just finished. We need to let observers know
+ // about this.
+ if (
+ lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING
+ ) {
+ transitionState(Ci.nsIApplicationUpdateService.STATE_SWAP);
+ }
+ Services.obs.notifyObservers(this._update, "update-swap");
+
+ // Swap the downloading update into the ready update directory.
+ cleanUpReadyUpdateDir();
+ let downloadedMar = getDownloadingUpdateDir();
+ downloadedMar.append(FILE_UPDATE_MAR);
+ let readyDir = getReadyUpdateDir();
+ try {
+ downloadedMar.moveTo(readyDir, FILE_UPDATE_MAR);
+ migratedToReadyUpdate = true;
+ } catch (e) {
+ migratedToReadyUpdate = false;
+ }
+
+ if (migratedToReadyUpdate) {
+ AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_SUCCESS);
+ state = getBestPendingState();
+ shouldShowPrompt = !getCanStageUpdates();
+
+ // Tell the updater.exe we're ready to apply.
+ LOG(
+ `Downloader:onStopRequest - Ready to apply. Setting state to ` +
+ `"${state}".`
+ );
+ writeStatusFile(getReadyUpdateDir(), state);
+ writeVersionFile(getReadyUpdateDir(), this._update.appVersion);
+ this._update.installDate = new Date().getTime();
+ this._update.statusText =
+ lazy.gUpdateBundle.GetStringFromName("installPending");
+ Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
+ } else {
+ LOG(
+ "Downloader:onStopRequest - failed to move the downloading " +
+ "update to the ready update directory."
+ );
+ AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_UNKNOWN_FAILURE);
+
+ state = STATE_DOWNLOAD_FAILED;
+ status = Cr.NS_ERROR_FILE_COPY_OR_MOVE_FAILED;
+
+ const mfCode = "move_failed";
+ let message = getStatusTextFromCode(mfCode, mfCode);
+ this._update.statusText = message;
+
+ nonDownloadFailure = true;
+ deleteActiveUpdate = true;
+
+ cleanUpDownloadingUpdateDir();
+ }
+ } else {
+ LOG("Downloader:onStopRequest - download verification failed");
+ state = STATE_DOWNLOAD_FAILED;
+ status = Cr.NS_ERROR_CORRUPTED_CONTENT;
+
+ // Yes, this code is a string.
+ const vfCode = "verification_failed";
+ var message = getStatusTextFromCode(vfCode, vfCode);
+ this._update.statusText = message;
+
+ if (this._update.isCompleteUpdate || this._update.patchCount != 2) {
+ LOG("Downloader:onStopRequest - No alternative patch to try");
+ deleteActiveUpdate = true;
+ }
+
+ // Destroy the updates directory, since we're done with it.
+ cleanUpDownloadingUpdateDir();
+ }
+ } else if (status == Cr.NS_ERROR_OFFLINE) {
+ // Register an online observer to try again.
+ // The online observer will continue the incremental download by
+ // calling downloadUpdate on the active update which continues
+ // downloading the file from where it was.
+ LOG("Downloader:onStopRequest - offline, register online observer: true");
+ AUSTLMY.pingDownloadCode(
+ this.isCompleteUpdate,
+ AUSTLMY.DWNLD_RETRY_OFFLINE
+ );
+ shouldRegisterOnlineObserver = true;
+ deleteActiveUpdate = false;
+
+ // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED,
+ // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned
+ // when disconnecting the internet while a download of a MAR is in
+ // progress. There may be others but I have not encountered them during
+ // testing.
+ } else if (
+ (status == Cr.NS_ERROR_NET_TIMEOUT ||
+ status == Cr.NS_ERROR_CONNECTION_REFUSED ||
+ status == Cr.NS_ERROR_NET_RESET ||
+ status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) &&
+ this.updateService._consecutiveSocketErrors < maxFail
+ ) {
+ LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true");
+ let dwnldCode = AUSTLMY.DWNLD_RETRY_CONNECTION_REFUSED;
+ if (status == Cr.NS_ERROR_NET_TIMEOUT) {
+ dwnldCode = AUSTLMY.DWNLD_RETRY_NET_TIMEOUT;
+ } else if (status == Cr.NS_ERROR_NET_RESET) {
+ dwnldCode = AUSTLMY.DWNLD_RETRY_NET_RESET;
+ } else if (status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
+ dwnldCode = AUSTLMY.DWNLD_ERR_DOCUMENT_NOT_CACHED;
+ }
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode);
+ shouldRetrySoon = true;
+ deleteActiveUpdate = false;
+ } else if (status != Cr.NS_BINDING_ABORTED && status != Cr.NS_ERROR_ABORT) {
+ if (
+ status == Cr.NS_ERROR_FILE_ACCESS_DENIED ||
+ status == Cr.NS_ERROR_FILE_READ_ONLY
+ ) {
+ LOG("Downloader:onStopRequest - permission error");
+ nonDownloadFailure = true;
+ } else {
+ LOG("Downloader:onStopRequest - non-verification failure");
+ }
+
+ let dwnldCode = AUSTLMY.DWNLD_ERR_BINDING_ABORTED;
+ if (status == Cr.NS_ERROR_ABORT) {
+ dwnldCode = AUSTLMY.DWNLD_ERR_ABORT;
+ }
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode);
+
+ // Some sort of other failure, log this in the |statusText| property
+ state = STATE_DOWNLOAD_FAILED;
+
+ // XXXben - if |request| (The Incremental Download) provided a means
+ // for accessing the http channel we could do more here.
+
+ this._update.statusText = getStatusTextFromCode(
+ status,
+ Cr.NS_BINDING_FAILED
+ );
+
+ // Destroy the updates directory, since we're done with it.
+ cleanUpDownloadingUpdateDir();
+
+ deleteActiveUpdate = true;
+ }
+ if (!this.usingBits) {
+ LOG(`Downloader:onStopRequest - Setting internalResult to ${status}`);
+ this._patch.setProperty("internalResult", status);
+ } else {
+ LOG(`Downloader:onStopRequest - Setting bitsResult to ${status}`);
+ this._patch.setProperty("bitsResult", status);
+
+ // If we failed when using BITS, we want to override the retry decision
+ // since we need to retry with nsIncrementalDownload before we give up.
+ // However, if the download was cancelled, don't retry. If the transfer
+ // was cancelled, we don't want it to restart on its own.
+ if (
+ !Components.isSuccessCode(status) &&
+ status != Cr.NS_BINDING_ABORTED &&
+ status != Cr.NS_ERROR_ABORT
+ ) {
+ deleteActiveUpdate = false;
+ shouldRetrySoon = true;
+ }
+
+ // Send BITS Telemetry
+ if (Components.isSuccessCode(status)) {
+ AUSTLMY.pingBitsSuccess(this.isCompleteUpdate);
+ } else {
+ let error;
+ if (bitsCompletionError) {
+ error = bitsCompletionError;
+ } else if (status == Cr.NS_ERROR_CORRUPTED_CONTENT) {
+ error = new BitsVerificationError();
+ } else {
+ error = request.transferError;
+ if (!error) {
+ error = new BitsUnknownError();
+ }
+ }
+ AUSTLMY.pingBitsError(this.isCompleteUpdate, error);
+ }
+ }
+
+ LOG("Downloader:onStopRequest - setting state to: " + state);
+ if (this._patch.state != state) {
+ this._patch.state = state;
+ }
+ if (deleteActiveUpdate) {
+ LOG("Downloader:onStopRequest - Clearing downloadingUpdate.");
+ this._update.installDate = new Date().getTime();
+ lazy.UM.addUpdateToHistory(lazy.UM.downloadingUpdate);
+ lazy.UM.downloadingUpdate = null;
+ } else if (
+ lazy.UM.downloadingUpdate &&
+ lazy.UM.downloadingUpdate.state != state
+ ) {
+ lazy.UM.downloadingUpdate.state = state;
+ }
+ if (migratedToReadyUpdate) {
+ LOG(
+ "Downloader:onStopRequest - Moving downloadingUpdate into readyUpdate"
+ );
+ lazy.UM.readyUpdate = lazy.UM.downloadingUpdate;
+ lazy.UM.downloadingUpdate = null;
+ }
+ lazy.UM.saveUpdates();
+
+ // Only notify listeners about the stopped state if we
+ // aren't handling an internal retry.
+ if (!shouldRetrySoon && !shouldRegisterOnlineObserver) {
+ this.updateService.forEachDownloadListener(listener => {
+ listener.onStopRequest(request, status);
+ });
+ }
+
+ this._request = null;
+
+ // This notification must happen after _request is set to null so that
+ // the correct this.updateService.isDownloading value is available in
+ // _notifyDownloadStatusObservers().
+ this._notifyDownloadStatusObservers();
+
+ if (state == STATE_DOWNLOAD_FAILED) {
+ var allFailed = true;
+ // Don't bother retrying the download if we got an error that isn't
+ // download related.
+ if (!nonDownloadFailure) {
+ // If we haven't already, attempt to download without BITS
+ if (request instanceof BitsRequest) {
+ LOG(
+ "Downloader:onStopRequest - BITS download failed. Falling back " +
+ "to nsIIncrementalDownload"
+ );
+ let success = await this.downloadUpdate(this._update);
+ if (!success) {
+ LOG(
+ "Downloader:onStopRequest - Failed to fall back to " +
+ "nsIIncrementalDownload. Cleaning up downloading update."
+ );
+ cleanupDownloadingUpdate();
+ } else {
+ allFailed = false;
+ }
+ }
+
+ // Check if there is a complete update patch that can be downloaded.
+ if (
+ allFailed &&
+ !this._update.isCompleteUpdate &&
+ this._update.patchCount == 2
+ ) {
+ LOG(
+ "Downloader:onStopRequest - verification of patch failed, " +
+ "downloading complete update patch"
+ );
+ this._update.isCompleteUpdate = true;
+ let success = await this.downloadUpdate(this._update);
+
+ if (!success) {
+ LOG(
+ "Downloader:onStopRequest - Failed to fall back to complete " +
+ "patch. Cleaning up downloading update."
+ );
+ cleanupDownloadingUpdate();
+ } else {
+ allFailed = false;
+ }
+ }
+ }
+
+ if (allFailed) {
+ let downloadAttempts = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS,
+ 0
+ );
+ downloadAttempts++;
+ Services.prefs.setIntPref(
+ PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS,
+ downloadAttempts
+ );
+ let maxAttempts = Math.min(
+ Services.prefs.getIntPref(PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2),
+ 10
+ );
+
+ transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
+
+ if (downloadAttempts > maxAttempts) {
+ LOG(
+ "Downloader:onStopRequest - notifying observers of error. " +
+ "topic: update-error, status: download-attempts-exceeded, " +
+ "downloadAttempts: " +
+ downloadAttempts +
+ " " +
+ "maxAttempts: " +
+ maxAttempts
+ );
+ Services.obs.notifyObservers(
+ this._update,
+ "update-error",
+ "download-attempts-exceeded"
+ );
+ } else {
+ this._update.selectedPatch.selected = false;
+ LOG(
+ "Downloader:onStopRequest - notifying observers of error. " +
+ "topic: update-error, status: download-attempt-failed"
+ );
+ Services.obs.notifyObservers(
+ this._update,
+ "update-error",
+ "download-attempt-failed"
+ );
+ }
+ // We don't care about language pack updates now.
+ this._langPackTimeout = null;
+ LangPackUpdates.delete(unwrap(this._update));
+
+ // Prevent leaking the update object (bug 454964).
+ this._update = null;
+
+ // allFailed indicates that we didn't (successfully) call downloadUpdate
+ // to try to download a different MAR. In this case, this Downloader
+ // is no longer being used.
+ this.updateService._downloader = null;
+ }
+ // A complete download has been initiated or the failure was handled.
+ return;
+ }
+
+ // If the download has succeeded or failed, we are done with this Downloader
+ // object. However, in some cases (ex: network disconnection), we will
+ // attempt to resume using this same Downloader.
+ if (state != STATE_DOWNLOADING) {
+ this.updateService._downloader = null;
+ }
+
+ if (
+ state == STATE_PENDING ||
+ state == STATE_PENDING_SERVICE ||
+ state == STATE_PENDING_ELEVATE
+ ) {
+ if (getCanStageUpdates()) {
+ LOG(
+ "Downloader:onStopRequest - attempting to stage update: " +
+ this._update.name
+ );
+ // Stage the update
+ let stagingStarted = true;
+ try {
+ Cc["@mozilla.org/updates/update-processor;1"]
+ .createInstance(Ci.nsIUpdateProcessor)
+ .processUpdate();
+ } catch (e) {
+ LOG(
+ "Downloader:onStopRequest - failed to stage update. Exception: " + e
+ );
+ stagingStarted = false;
+ }
+ if (stagingStarted) {
+ transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING);
+ } else {
+ // Fail gracefully in case the application does not support the update
+ // processor service.
+ shouldShowPrompt = true;
+ }
+ }
+ }
+
+ // If we're still waiting on language pack updates then run a timer to time
+ // out the attempt after an appropriate amount of time.
+ if (this._langPackTimeout) {
+ // Start a timer to measure how much longer it takes for the language
+ // packs to stage.
+ TelemetryStopwatch.start(
+ "UPDATE_LANGPACK_OVERTIME",
+ unwrap(this._update),
+ { inSeconds: true }
+ );
+
+ lazy.setTimeout(
+ this._langPackTimeout,
+ Services.prefs.getIntPref(
+ PREF_APP_UPDATE_LANGPACK_TIMEOUT,
+ LANGPACK_UPDATE_DEFAULT_TIMEOUT
+ )
+ );
+ }
+
+ // Do this after *everything* else, since it will likely cause the app
+ // to shut down.
+ if (shouldShowPrompt) {
+ // Wait for language packs to stage before showing any prompt to restart.
+ let update = this._update;
+ promiseLangPacksUpdated(update).then(() => {
+ LOG(
+ "Downloader:onStopRequest - Notifying observers that " +
+ "an update was downloaded. topic: update-downloaded, status: " +
+ update.state
+ );
+ transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
+ Services.obs.notifyObservers(update, "update-downloaded", update.state);
+ });
+ }
+
+ if (shouldRegisterOnlineObserver) {
+ LOG("Downloader:onStopRequest - Registering online observer");
+ this.updateService._registerOnlineObserver();
+ } else if (shouldRetrySoon) {
+ LOG("Downloader:onStopRequest - Retrying soon");
+ this.updateService._consecutiveSocketErrors++;
+ if (this.updateService._retryTimer) {
+ this.updateService._retryTimer.cancel();
+ }
+ this.updateService._retryTimer = Cc[
+ "@mozilla.org/timer;1"
+ ].createInstance(Ci.nsITimer);
+ this.updateService._retryTimer.initWithCallback(
+ async () => {
+ await this.updateService._attemptResume();
+ },
+ retryTimeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ } else {
+ // Prevent leaking the update object (bug 454964)
+ this._update = null;
+ }
+ },
+
+ /**
+ * This function should be called when shutting down so that resources get
+ * freed properly.
+ */
+ cleanup: async function Downloader_cleanup() {
+ if (this.usingBits) {
+ if (this._pendingRequest) {
+ await this._pendingRequest;
+ }
+ this._request.shutdown();
+ }
+ },
+
+ /**
+ * See nsIInterfaceRequestor.idl
+ */
+ getInterface: function Downloader_getInterface(iid) {
+ // The network request may require proxy authentication, so provide the
+ // default nsIAuthPrompt if requested.
+ if (iid.equals(Ci.nsIAuthPrompt)) {
+ var prompt =
+ Cc["@mozilla.org/network/default-auth-prompt;1"].createInstance();
+ return prompt.QueryInterface(iid);
+ }
+ throw Components.Exception("", Cr.NS_NOINTERFACE);
+ },
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIRequestObserver",
+ "nsIProgressEventSink",
+ "nsIInterfaceRequestor",
+ ]),
+};
+
+// On macOS, all browser windows can be closed without Firefox exiting. If it
+// is left in this state for a while and an update is pending, we should restart
+// Firefox on our own to apply the update. This class will do that
+// automatically.
+class RestartOnLastWindowClosed {
+ #enabled = false;
+ #hasShutdown = false;
+
+ #restartTimer = null;
+ #restartTimerExpired = false;
+
+ constructor() {
+ this.#maybeEnableOrDisable();
+
+ Services.prefs.addObserver(
+ PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
+ this
+ );
+ Services.obs.addObserver(this, "quit-application");
+ }
+
+ shutdown() {
+ LOG("RestartOnLastWindowClosed.shutdown - Shutting down");
+ this.#hasShutdown = true;
+
+ Services.prefs.removeObserver(
+ PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
+ this
+ );
+ Services.obs.removeObserver(this, "quit-application");
+
+ this.#maybeEnableOrDisable();
+ }
+
+ get shouldEnable() {
+ if (AppConstants.platform != "macosx") {
+ return false;
+ }
+ if (this.#hasShutdown) {
+ return false;
+ }
+ return Services.prefs.getBoolPref(
+ PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
+ false
+ );
+ }
+
+ get enabled() {
+ return this.#enabled;
+ }
+
+ observe(subject, topic, data) {
+ switch (topic) {
+ case "nsPref:changed":
+ if (data == PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED) {
+ this.#maybeEnableOrDisable();
+ }
+ break;
+ case "quit-application":
+ this.shutdown();
+ break;
+ case "domwindowclosed":
+ this.#onWindowClose();
+ break;
+ case "domwindowopened":
+ this.#onWindowOpen();
+ break;
+ case "update-downloaded":
+ case "update-staged":
+ this.#onUpdateReady(data);
+ break;
+ }
+ }
+
+ // Returns true if any windows are open. Otherwise, false.
+ #windowsAreOpen() {
+ // eslint-disable-next-line no-unused-vars
+ for (const win of Services.wm.getEnumerator(null)) {
+ return true;
+ }
+ return false;
+ }
+
+ // Enables or disables this class's functionality based on the value of
+ // this.shouldEnable. Does nothing if the class is already in the right state
+ // (i.e. if the class should be enabled and already is, or should be disabled
+ // and already is).
+ #maybeEnableOrDisable() {
+ if (this.shouldEnable) {
+ if (this.#enabled) {
+ return;
+ }
+ LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Enabling");
+
+ Services.obs.addObserver(this, "domwindowclosed");
+ Services.obs.addObserver(this, "domwindowopened");
+ Services.obs.addObserver(this, "update-downloaded");
+ Services.obs.addObserver(this, "update-staged");
+
+ this.#restartTimer = null;
+ this.#restartTimerExpired = false;
+
+ this.#enabled = true;
+
+ // Synchronize with external state.
+ this.#onWindowClose();
+ } else {
+ if (!this.#enabled) {
+ return;
+ }
+ LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Disabling");
+
+ Services.obs.removeObserver(this, "domwindowclosed");
+ Services.obs.removeObserver(this, "domwindowopened");
+ Services.obs.removeObserver(this, "update-downloaded");
+ Services.obs.removeObserver(this, "update-staged");
+
+ this.#enabled = false;
+
+ if (this.#restartTimer) {
+ this.#restartTimer.cancel();
+ }
+ this.#restartTimer = null;
+ }
+ }
+
+ // Note: Since we keep track of the update state even when this class is
+ // disabled, this function will run even in that case.
+ #onUpdateReady(updateState) {
+ // Note that we do not count pending-elevate as a ready state, because we
+ // cannot silently restart in that state.
+ if (
+ [
+ STATE_APPLIED,
+ STATE_PENDING,
+ STATE_APPLIED_SERVICE,
+ STATE_PENDING_SERVICE,
+ ].includes(updateState)
+ ) {
+ if (this.#enabled) {
+ LOG("RestartOnLastWindowClosed.#onUpdateReady - update ready");
+ this.#maybeRestartBrowser();
+ }
+ } else if (this.#enabled) {
+ LOG(
+ `RestartOnLastWindowClosed.#onUpdateReady - Not counting update as ` +
+ `ready because the state is ${updateState}`
+ );
+ }
+ }
+
+ #onWindowClose() {
+ if (!this.#windowsAreOpen()) {
+ this.#onLastWindowClose();
+ }
+ }
+
+ #onLastWindowClose() {
+ if (this.#restartTimer || this.#restartTimerExpired) {
+ LOG(
+ "RestartOnLastWindowClosed.#onLastWindowClose - Restart timer is " +
+ "either already running or has already expired"
+ );
+ return;
+ }
+
+ let timeout = Services.prefs.getIntPref(
+ PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS,
+ 5 * 60 * 1000
+ );
+
+ LOG(
+ "RestartOnLastWindowClosed.#onLastWindowClose - Last window closed. " +
+ "Starting restart timer"
+ );
+ this.#restartTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.#restartTimer.initWithCallback(
+ () => this.#onRestartTimerExpire(),
+ timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ }
+
+ #onWindowOpen() {
+ if (this.#restartTimer) {
+ LOG(
+ "RestartOnLastWindowClosed.#onWindowOpen - Window opened. Cancelling " +
+ "restart timer."
+ );
+ this.#restartTimer.cancel();
+ }
+ this.#restartTimer = null;
+ this.#restartTimerExpired = false;
+ }
+
+ #onRestartTimerExpire() {
+ LOG("RestartOnLastWindowClosed.#onRestartTimerExpire - Timer Expired");
+
+ this.#restartTimer = null;
+ this.#restartTimerExpired = true;
+ this.#maybeRestartBrowser();
+ }
+
+ #maybeRestartBrowser() {
+ if (!this.#restartTimerExpired) {
+ LOG(
+ "RestartOnLastWindowClosed.#maybeRestartBrowser - Still waiting for " +
+ "all windows to be closed and restartTimer to expire. " +
+ "(not restarting)"
+ );
+ return;
+ }
+
+ if (lazy.AUS.currentState != Ci.nsIApplicationUpdateService.STATE_PENDING) {
+ LOG(
+ "RestartOnLastWindowClosed.#maybeRestartBrowser - No update ready. " +
+ "(not restarting)"
+ );
+ return;
+ }
+
+ if (getElevationRequired()) {
+ // We check for STATE_PENDING_ELEVATE elsewhere, but this is actually
+ // different from that because it is technically possible that the user
+ // gave permission to elevate, but we haven't actually elevated yet.
+ // This is a bit of a corner case. We only call elevationOptedIn() right
+ // before we restart to apply the update immediately. But it is possible
+ // that something could stop the browser from shutting down.
+ LOG(
+ "RestartOnLastWindowClosed.#maybeRestartBrowser - This update will " +
+ "require user elevation (not restarting)"
+ );
+ return;
+ }
+
+ if (this.#windowsAreOpen()) {
+ LOG(
+ "RestartOnLastWindowClosed.#maybeRestartBrowser - Window " +
+ "unexpectedly still open! (not restarting)"
+ );
+ return;
+ }
+
+ if (!this.shouldEnable) {
+ LOG(
+ "RestartOnLastWindowClosed.#maybeRestartBrowser - Unexpectedly " +
+ "attempted to restart when RestartOnLastWindowClosed ought to be " +
+ "disabled! (not restarting)"
+ );
+ return;
+ }
+
+ LOG("RestartOnLastWindowClosed.#maybeRestartBrowser - Restarting now");
+ Services.telemetry.scalarAdd("update.no_window_auto_restarts", 1);
+ Services.startup.quit(
+ Ci.nsIAppStartup.eAttemptQuit |
+ Ci.nsIAppStartup.eRestart |
+ Ci.nsIAppStartup.eSilently
+ );
+ }
+}
+// Nothing actually uses this variable at the moment, but let's make sure that
+// we hold the reference to the RestartOnLastWindowClosed instance somewhere.
+// eslint-disable-next-line no-unused-vars
+let restartOnLastWindowClosed = new RestartOnLastWindowClosed();