forked from mirrors/gecko-dev
In order to do this without initializing the update system this early, we move the functionality to the stub, add an XPCOM interface for it, and convert it to new-style class using the `class` keyword. Differential Revision: https://phabricator.services.mozilla.com/D209128
7311 lines
237 KiB
JavaScript
7311 lines
237 KiB
JavaScript
/* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
import { AUSTLMY } from "resource://gre/modules/UpdateTelemetry.sys.mjs";
|
|
|
|
import {
|
|
Bits,
|
|
BitsRequest,
|
|
BitsUnknownError,
|
|
BitsVerificationError,
|
|
} from "resource://gre/modules/Bits.sys.mjs";
|
|
import { FileUtils } from "resource://gre/modules/FileUtils.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
|
|
DeferredTask: "resource://gre/modules/DeferredTask.sys.mjs",
|
|
UpdateLog: "resource://gre/modules/UpdateLog.sys.mjs",
|
|
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
|
|
WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
|
|
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"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"UpdateServiceStub",
|
|
"@mozilla.org/updates/update-service-stub;1",
|
|
"nsIApplicationUpdateServiceStub"
|
|
);
|
|
|
|
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_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_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
|
|
const PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED =
|
|
"app.update.noWindowAutoRestart.enabled";
|
|
const PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS =
|
|
"app.update.noWindowAutoRestart.delayMs";
|
|
const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
|
|
const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
|
|
const PREF_APP_UPDATE_SERVICE_ERRORS = "app.update.service.errors";
|
|
const PREF_APP_UPDATE_SERVICE_MAXERRORS = "app.update.service.maxErrors";
|
|
const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
|
|
const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT = "app.update.socket.retryTimeout";
|
|
const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
|
|
const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
|
|
const PREF_NETWORK_PROXY_TYPE = "network.proxy.type";
|
|
|
|
const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
|
|
const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update";
|
|
const URI_UPDATES_PROPERTIES =
|
|
"chrome://mozapps/locale/update/updates.properties";
|
|
|
|
const KEY_EXECUTABLE = "XREExeF";
|
|
const KEY_UPDROOT = "UpdRootD";
|
|
const KEY_OLD_UPDROOT = "OldUpdRootD";
|
|
|
|
const DIR_UPDATES = "updates";
|
|
const DIR_UPDATE_READY = "0";
|
|
const DIR_UPDATE_DOWNLOADING = "downloading";
|
|
|
|
const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
|
|
const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
|
|
const FILE_BACKUP_UPDATE_ELEVATED_LOG = "backup-update-elevated.log";
|
|
const FILE_BT_RESULT = "bt.result";
|
|
const FILE_LAST_UPDATE_LOG = "last-update.log";
|
|
const FILE_LAST_UPDATE_ELEVATED_LOG = "last-update-elevated.log";
|
|
const FILE_UPDATES_XML = "updates.xml";
|
|
const FILE_UPDATE_LOG = "update.log";
|
|
const FILE_UPDATE_ELEVATED_LOG = "update-elevated.log";
|
|
const FILE_UPDATE_MAR = "update.mar";
|
|
const FILE_UPDATE_STATUS = "update.status";
|
|
const FILE_UPDATE_TEST = "update.test";
|
|
const FILE_UPDATE_VERSION = "update.version";
|
|
|
|
const STATE_NONE = "null";
|
|
const STATE_DOWNLOADING = "downloading";
|
|
const STATE_PENDING = "pending";
|
|
const STATE_PENDING_SERVICE = "pending-service";
|
|
const STATE_PENDING_ELEVATE = "pending-elevate";
|
|
const STATE_APPLYING = "applying";
|
|
const STATE_APPLIED = "applied";
|
|
const STATE_APPLIED_SERVICE = "applied-service";
|
|
const STATE_SUCCEEDED = "succeeded";
|
|
const STATE_DOWNLOAD_FAILED = "download-failed";
|
|
const STATE_FAILED = "failed";
|
|
|
|
// BITS will keep retrying a download after transient errors, unless this much
|
|
// time has passed since there has been download progress.
|
|
// Similarly to ...POLL_RATE_MS below, we are much more aggressive when the user
|
|
// is watching the download progress.
|
|
const BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS = 3600; // 1 hour
|
|
const BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS = 5;
|
|
|
|
// These value control how frequently we get updates from the BITS client on
|
|
// the progress made downloading. The difference between the two is that the
|
|
// active interval is the one used when the user is watching. The idle interval
|
|
// is the one used when no one is watching.
|
|
const BITS_IDLE_POLL_RATE_MS = 1000;
|
|
const BITS_ACTIVE_POLL_RATE_MS = 200;
|
|
|
|
// The values below used by this code are from common/updatererrors.h
|
|
const WRITE_ERROR = 7;
|
|
const ELEVATION_CANCELED = 9;
|
|
const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24;
|
|
const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25;
|
|
const SERVICE_UPDATER_SIGN_ERROR = 26;
|
|
const SERVICE_UPDATER_COMPARE_ERROR = 27;
|
|
const SERVICE_UPDATER_IDENTITY_ERROR = 28;
|
|
const SERVICE_STILL_APPLYING_ON_SUCCESS = 29;
|
|
const SERVICE_STILL_APPLYING_ON_FAILURE = 30;
|
|
const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31;
|
|
const SERVICE_COULD_NOT_LOCK_UPDATER = 32;
|
|
const SERVICE_INSTALLDIR_ERROR = 33;
|
|
const WRITE_ERROR_ACCESS_DENIED = 35;
|
|
const WRITE_ERROR_CALLBACK_APP = 37;
|
|
const UNEXPECTED_STAGING_ERROR = 43;
|
|
const DELETE_ERROR_STAGING_LOCK_FILE = 44;
|
|
const SERVICE_COULD_NOT_COPY_UPDATER = 49;
|
|
const SERVICE_STILL_APPLYING_TERMINATED = 50;
|
|
const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51;
|
|
const SERVICE_COULD_NOT_IMPERSONATE = 58;
|
|
const WRITE_ERROR_FILE_COPY = 61;
|
|
const WRITE_ERROR_DELETE_FILE = 62;
|
|
const WRITE_ERROR_OPEN_PATCH_FILE = 63;
|
|
const WRITE_ERROR_PATCH_FILE = 64;
|
|
const WRITE_ERROR_APPLY_DIR_PATH = 65;
|
|
const WRITE_ERROR_CALLBACK_PATH = 66;
|
|
const WRITE_ERROR_FILE_ACCESS_DENIED = 67;
|
|
const WRITE_ERROR_DIR_ACCESS_DENIED = 68;
|
|
const WRITE_ERROR_DELETE_BACKUP = 69;
|
|
const WRITE_ERROR_EXTRACT = 70;
|
|
|
|
// Error codes 80 through 99 are reserved for UpdateService.sys.mjs and are not
|
|
// defined in common/updatererrors.h
|
|
const ERR_UPDATER_CRASHED = 89;
|
|
const ERR_OLDER_VERSION_OR_SAME_BUILD = 90;
|
|
const ERR_UPDATE_STATE_NONE = 91;
|
|
const ERR_CHANNEL_CHANGE = 92;
|
|
const INVALID_UPDATER_STATE_CODE = 98;
|
|
const INVALID_UPDATER_STATUS_CODE = 99;
|
|
|
|
const SILENT_UPDATE_NEEDED_ELEVATION_ERROR = 105;
|
|
const 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;
|
|
|
|
// 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 value will be set to true if it appears that BITS is being used by
|
|
// another user to download updates. We don't really want two users using BITS
|
|
// at once. Computers with many users (ex: a school computer), should not end
|
|
// up with dozens of BITS jobs.
|
|
var gBITSInUseByAnotherUser = false;
|
|
// The update service can be invoked as part of a standalone headless background
|
|
// task. In this context, when the background task kicks off an update
|
|
// download, we don't want it to move on to staging. As soon as the download has
|
|
// kicked off, the task begins shutting down and, even if the the download
|
|
// completes incredibly quickly, we don't want staging to begin while we are
|
|
// shutting down. That isn't a well tested scenario and it's possible that it
|
|
// could leave us in a bad state.
|
|
let gOnlyDownloadUpdatesThisSession = false;
|
|
// This will be the backing for `nsIApplicationUpdateService.currentState`
|
|
var gUpdateState = Ci.nsIApplicationUpdateService.STATE_IDLE;
|
|
|
|
/**
|
|
* Simple container and constructor for a Promise and its resolve function.
|
|
*/
|
|
class SelfContainedPromise {
|
|
constructor() {
|
|
this.promise = new Promise(resolve => {
|
|
this.resolve = resolve;
|
|
});
|
|
}
|
|
}
|
|
|
|
// This will contain a `SelfContainedPromise` that will be used to back
|
|
// `nsIApplicationUpdateService.stateTransition`.
|
|
var gStateTransitionPromise = new SelfContainedPromise();
|
|
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gUpdateBundle",
|
|
function aus_gUpdateBundle() {
|
|
return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
|
|
}
|
|
);
|
|
|
|
/**
|
|
* gIsBackgroundTaskMode will be true if Firefox is currently running as a
|
|
* background task. Otherwise it will be false.
|
|
*/
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gIsBackgroundTaskMode",
|
|
function aus_gCurrentlyRunningAsBackgroundTask() {
|
|
if (!("@mozilla.org/backgroundtasks;1" in Cc)) {
|
|
return false;
|
|
}
|
|
const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
|
|
Ci.nsIBackgroundTasks
|
|
);
|
|
if (!bts) {
|
|
return false;
|
|
}
|
|
return bts.isBackgroundTaskMode;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Changes `nsIApplicationUpdateService.currentState` and causes
|
|
* `nsIApplicationUpdateService.stateTransition` to resolve.
|
|
*/
|
|
function transitionState(newState) {
|
|
if (newState == gUpdateState) {
|
|
LOG("transitionState - Not transitioning state because it isn't changing.");
|
|
return;
|
|
}
|
|
LOG(
|
|
`transitionState - "${lazy.AUS.getStateName(gUpdateState)}" -> ` +
|
|
`"${lazy.AUS.getStateName(newState)}".`
|
|
);
|
|
gUpdateState = newState;
|
|
// Assign the new Promise before we resolve the old one just to make sure that
|
|
// anything that runs as a result of `resolve` doesn't end up waiting on the
|
|
// Promise that already resolved.
|
|
let oldStateTransitionPromise = gStateTransitionPromise;
|
|
gStateTransitionPromise = new SelfContainedPromise();
|
|
oldStateTransitionPromise.resolve();
|
|
}
|
|
|
|
/**
|
|
* When a plain JS object is passed through xpconnect the other side sees a
|
|
* wrapped version of the object instead of the real object. Since these two
|
|
* objects are different they act as different keys for Map and WeakMap. However
|
|
* xpconnect gives us a way to get the underlying JS object from the wrapper so
|
|
* this function returns the JS object regardless of whether passed the JS
|
|
* object or its wrapper for use in places where it is unclear which one you
|
|
* have.
|
|
*/
|
|
function unwrap(obj) {
|
|
return obj.wrappedJSObject ?? obj;
|
|
}
|
|
|
|
/**
|
|
* When an update starts to download (and if the feature is enabled) the add-ons
|
|
* manager starts downloading updated language packs for the new application
|
|
* version. A promise is used to track whether those updates are complete so the
|
|
* front-end is only notified that an application update is ready once the
|
|
* language pack updates have been staged.
|
|
*
|
|
* In order to be able to access that promise from various places in the update
|
|
* service they are cached in this map using the nsIUpdate object as a weak
|
|
* owner. Note that the key should always be the result of calling the above
|
|
* unwrap function on the nsIUpdate to ensure a consistent object is used as the
|
|
* key.
|
|
*
|
|
* When the language packs finish staging the nsIUpdate entriy is removed from
|
|
* this map so if the entry is still there then language pack updates are in
|
|
* progress.
|
|
*/
|
|
const LangPackUpdates = new WeakMap();
|
|
|
|
/**
|
|
* When we're polling to see if other running instances of the application have
|
|
* exited, there's no need to ever start polling again in parallel. To prevent
|
|
* doing that, we keep track of the promise that resolves when polling completes
|
|
* and return that if a second simultaneous poll is requested, so that the
|
|
* multiple callers end up waiting for the same promise to resolve.
|
|
*/
|
|
let gOtherInstancePollPromise;
|
|
|
|
/**
|
|
* Query the update sync manager to see if another instance of this same
|
|
* installation of this application is currently running, under the context of
|
|
* any operating system user (not just the current one).
|
|
* This function immediately returns the current, instantaneous status of any
|
|
* other instances.
|
|
*
|
|
* @return true if at least one other instance is running, false if not
|
|
*/
|
|
function isOtherInstanceRunning() {
|
|
const checkEnabled = Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED,
|
|
true
|
|
);
|
|
if (!checkEnabled) {
|
|
LOG("isOtherInstanceRunning - disabled by pref, skipping check");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
let syncManager = Cc[
|
|
"@mozilla.org/updates/update-sync-manager;1"
|
|
].getService(Ci.nsIUpdateSyncManager);
|
|
return syncManager.isOtherInstanceRunning();
|
|
} catch (ex) {
|
|
LOG(`isOtherInstanceRunning - sync manager failed with exception: ${ex}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query the update sync manager to see if another instance of this same
|
|
* installation of this application is currently running, under the context of
|
|
* any operating system user (not just the one running this instance).
|
|
* This function polls for the status of other instances continually
|
|
* (asynchronously) until either none exist or a timeout expires.
|
|
*
|
|
* @return a Promise that resolves with false if at any point during polling no
|
|
* other instances can be found, or resolves with true if the timeout
|
|
* expires when other instances are still running
|
|
*/
|
|
function waitForOtherInstances() {
|
|
// If we're already in the middle of a poll, reuse it rather than start again.
|
|
if (gOtherInstancePollPromise) {
|
|
return gOtherInstancePollPromise;
|
|
}
|
|
|
|
let timeout = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT,
|
|
ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS
|
|
);
|
|
// 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) {
|
|
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);
|
|
|
|
var data = new TextEncoder().encode(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();
|
|
}
|
|
|
|
/*
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
function isAppBaseDirWritable() {
|
|
let appDirTestFile = "";
|
|
|
|
try {
|
|
appDirTestFile = getAppBaseDir();
|
|
appDirTestFile.append(FILE_UPDATE_TEST);
|
|
} catch (e) {
|
|
LOG(
|
|
"isAppBaseDirWritable - Base directory or test path could not be " +
|
|
`determined: ${e}`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
LOG(
|
|
`isAppBaseDirWritable - testing write access for ${appDirTestFile.path}`
|
|
);
|
|
|
|
if (appDirTestFile.exists()) {
|
|
appDirTestFile.remove(false);
|
|
}
|
|
// if we're unable to create the test file this will throw an exception:
|
|
appDirTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
|
appDirTestFile.remove(false);
|
|
LOG(`isAppBaseDirWritable - Path is writable: ${appDirTestFile.path}`);
|
|
return true;
|
|
} catch (e) {
|
|
LOG(
|
|
`isAppBaseDirWritable - Path '${appDirTestFile.path}' ` +
|
|
`is not writable: ${e}`
|
|
);
|
|
}
|
|
// No write access to the installation directory
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not an update can be applied. This is always true on
|
|
* Windows when the service is used. On Mac OS X and Linux, if the user has
|
|
* write access to the update directory this will return true because on OSX we
|
|
* offer users the option to perform an elevated update when necessary and on
|
|
* Linux the update directory is located in the application directory.
|
|
*
|
|
* @return true if an update can be applied, false otherwise
|
|
*/
|
|
function getCanApplyUpdates() {
|
|
try {
|
|
// Check if it is possible to write to the update directory so clients won't
|
|
// repeatedly try to apply an update without the ability to complete the
|
|
// update process which requires write access to the update directory.
|
|
let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
|
|
LOG("getCanApplyUpdates - testing write access " + updateTestFile.path);
|
|
testWriteAccess(updateTestFile, false);
|
|
} catch (e) {
|
|
LOG(
|
|
"getCanApplyUpdates - unable to apply updates without write " +
|
|
"access to the update directory. Exception: " +
|
|
e
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (AppConstants.platform == "macosx") {
|
|
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 && !isAppBaseDirWritable) {
|
|
LOG(
|
|
"getCanApplyUpdates - unable to apply updates, because the base " +
|
|
"directory is not writable."
|
|
);
|
|
}
|
|
}
|
|
} 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.
|
|
*/
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gCanStageUpdatesSession",
|
|
function aus_gCSUS() {
|
|
if (getElevationRequired()) {
|
|
LOG(
|
|
"gCanStageUpdatesSession - unable to stage updates because elevation " +
|
|
"is required."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
let updateTestFile;
|
|
if (AppConstants.platform == "macosx") {
|
|
updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
|
|
} else {
|
|
updateTestFile = getInstallDirRoot();
|
|
updateTestFile.append(FILE_UPDATE_TEST);
|
|
}
|
|
LOG(
|
|
"gCanStageUpdatesSession - testing write access " + updateTestFile.path
|
|
);
|
|
testWriteAccess(updateTestFile, true);
|
|
if (AppConstants.platform != "macosx") {
|
|
// On all platforms except Mac, we need to test the parent directory as
|
|
// well, as we need to be able to move files in that directory during the
|
|
// replacing step.
|
|
updateTestFile = getInstallDirRoot().parent;
|
|
updateTestFile.append(FILE_UPDATE_TEST);
|
|
LOG(
|
|
"gCanStageUpdatesSession - testing write access " +
|
|
updateTestFile.path
|
|
);
|
|
updateTestFile.createUnique(
|
|
Ci.nsIFile.DIRECTORY_TYPE,
|
|
FileUtils.PERMS_DIRECTORY
|
|
);
|
|
updateTestFile.remove(false);
|
|
}
|
|
} catch (e) {
|
|
LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + e);
|
|
// No write privileges
|
|
return false;
|
|
}
|
|
|
|
LOG("gCanStageUpdatesSession - able to stage updates");
|
|
return true;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Whether or not the application can stage an update.
|
|
*
|
|
* @param {boolean} [transient] Whether transient factors such as the update
|
|
* mutex should be considered.
|
|
* @return true if updates can be staged.
|
|
*/
|
|
function getCanStageUpdates(transient = true) {
|
|
// If staging updates are disabled, then just bail out!
|
|
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) {
|
|
LOG(
|
|
"getCanStageUpdates - staging updates is disabled by preference " +
|
|
PREF_APP_UPDATE_STAGING_ENABLED
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (AppConstants.platform == "win" && shouldUseService()) {
|
|
// No need to perform directory write checks, the maintenance service will
|
|
// be able to write to all directories.
|
|
LOG("getCanStageUpdates - able to stage updates using the service");
|
|
return true;
|
|
}
|
|
|
|
if (transient && !hasUpdateMutex()) {
|
|
LOG(
|
|
"getCanStageUpdates - unable to apply updates because another " +
|
|
"instance of the application is already handling updates for this " +
|
|
"installation."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return lazy.gCanStageUpdatesSession;
|
|
}
|
|
|
|
/*
|
|
* Whether or not the application can use BITS to download updates.
|
|
*
|
|
* @param {boolean} [transient] Whether transient factors such as the update
|
|
* mutex should be considered.
|
|
* @return A string with one of these values:
|
|
* CanUseBits
|
|
* NoBits_NotWindows
|
|
* NoBits_FeatureOff
|
|
* NoBits_Pref
|
|
* NoBits_Proxy
|
|
* NoBits_OtherUser
|
|
* These strings are directly compatible with the categories for
|
|
* UPDATE_CAN_USE_BITS_EXTERNAL and UPDATE_CAN_USE_BITS_NOTIFY telemetry
|
|
* probes. If this function is made to return other values, they should
|
|
* also be added to the labels lists for those probes in Histograms.json
|
|
*/
|
|
function getCanUseBits(transient = true) {
|
|
if (AppConstants.platform != "win") {
|
|
LOG("getCanUseBits - Not using BITS because this is not Windows");
|
|
return "NoBits_NotWindows";
|
|
}
|
|
if (!AppConstants.MOZ_BITS_DOWNLOAD) {
|
|
LOG("getCanUseBits - Not using BITS because the feature is disabled");
|
|
return "NoBits_FeatureOff";
|
|
}
|
|
|
|
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, true)) {
|
|
LOG("getCanUseBits - Not using BITS. Disabled by pref.");
|
|
return "NoBits_Pref";
|
|
}
|
|
// Firefox support for passing proxies to BITS is still rudimentary.
|
|
// For now, disable BITS support on configurations that are not using the
|
|
// standard system proxy.
|
|
let defaultProxy = Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM;
|
|
if (
|
|
Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) !=
|
|
defaultProxy &&
|
|
!Cu.isInAutomation
|
|
) {
|
|
LOG("getCanUseBits - Not using BITS because of proxy usage");
|
|
return "NoBits_Proxy";
|
|
}
|
|
if (transient && gBITSInUseByAnotherUser) {
|
|
LOG("getCanUseBits - Not using BITS. Already in use by another user");
|
|
return "NoBits_OtherUser";
|
|
}
|
|
LOG("getCanUseBits - BITS can be used to download updates");
|
|
return "CanUseBits";
|
|
}
|
|
|
|
/**
|
|
* Logs a string to the error console. If enabled, also logs to the update
|
|
* messages file.
|
|
* @param string
|
|
* The string to write to the error console.
|
|
*/
|
|
function LOG(string) {
|
|
lazy.UpdateLog.logPrefixedString("AUS:SVC", string);
|
|
}
|
|
|
|
/**
|
|
* Gets the specified directory at the specified hierarchy under the
|
|
* update root directory and creates it if it doesn't exist.
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|
|
|
* @return nsIFile object for the location specified.
|
|
*/
|
|
function getUpdateDirCreate(pathArray) {
|
|
if (Cu.isInAutomation) {
|
|
// This allows tests to use an alternate updates directory so they can test
|
|
// startup behavior.
|
|
const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
|
|
const PREF_TEST_ROOT = "mochitest.testRoot";
|
|
let alternatePath = Services.prefs.getCharPref(
|
|
PREF_APP_UPDATE_ALTUPDATEDIRPATH,
|
|
null
|
|
);
|
|
if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
|
|
let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT);
|
|
let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length);
|
|
if (AppConstants.platform == "win") {
|
|
relativePath = relativePath.replace(/\//g, "\\");
|
|
}
|
|
alternatePath = testRoot + relativePath;
|
|
let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
|
|
Ci.nsIFile
|
|
);
|
|
updateDir.initWithPath(alternatePath);
|
|
for (let i = 0; i < pathArray.length; ++i) {
|
|
updateDir.append(pathArray[i]);
|
|
}
|
|
return updateDir;
|
|
}
|
|
}
|
|
|
|
let dir = FileUtils.getDir(KEY_UPDROOT, pathArray);
|
|
try {
|
|
dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
|
} catch (ex) {
|
|
if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
|
|
throw ex;
|
|
}
|
|
// Ignore the exception due to a directory that already exists.
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Gets the application base directory.
|
|
*
|
|
* @return nsIFile object for the application base directory.
|
|
*/
|
|
function getAppBaseDir() {
|
|
return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
|
|
}
|
|
|
|
/**
|
|
* Gets the root of the installation directory which is the application
|
|
* bundle directory on Mac OS X and the location of the application binary
|
|
* on all other platforms.
|
|
*
|
|
* @return nsIFile object for the directory
|
|
*/
|
|
function getInstallDirRoot() {
|
|
let dir = getAppBaseDir();
|
|
if (AppConstants.platform == "macosx") {
|
|
// On macOS, the executable is stored under Contents/MacOS.
|
|
dir = dir.parent.parent;
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Gets the file at the specified hierarchy under the update root directory.
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|. The last item in this array must be the
|
|
* leaf name of a file.
|
|
* @return nsIFile object for the file specified. The file is NOT created
|
|
* if it does not exist, however all required directories along
|
|
* the way are.
|
|
*/
|
|
function getUpdateFile(pathArray) {
|
|
let file = getUpdateDirCreate(pathArray.slice(0, -1));
|
|
file.append(pathArray[pathArray.length - 1]);
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* This function is designed to let us slightly clean up the mapping between
|
|
* strings and error codes. So that instead of having:
|
|
* check_error-2147500036=Connection aborted
|
|
* check_error-2152398850=Connection aborted
|
|
* We can have:
|
|
* check_error-connection_aborted=Connection aborted
|
|
* And map both of those error codes to it.
|
|
*/
|
|
function maybeMapErrorCode(code) {
|
|
switch (code) {
|
|
case Cr.NS_BINDING_ABORTED:
|
|
case Cr.NS_ERROR_ABORT:
|
|
return "connection_aborted";
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Returns human readable status text from the updates.properties bundle
|
|
* based on an error code
|
|
* @param code
|
|
* The error code to look up human readable status text for
|
|
* @param defaultCode
|
|
* The default code to look up should human readable status text
|
|
* not exist for |code|
|
|
* @return A human readable status text string
|
|
*/
|
|
function getStatusTextFromCode(code, defaultCode) {
|
|
code = maybeMapErrorCode(code);
|
|
|
|
let reason;
|
|
try {
|
|
reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + code);
|
|
LOG(
|
|
"getStatusTextFromCode - transfer error: " + reason + ", code: " + code
|
|
);
|
|
} catch (e) {
|
|
defaultCode = maybeMapErrorCode(defaultCode);
|
|
|
|
// Use the default reason
|
|
reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
|
|
LOG(
|
|
"getStatusTextFromCode - transfer error: " +
|
|
reason +
|
|
", default code: " +
|
|
defaultCode
|
|
);
|
|
}
|
|
return reason;
|
|
}
|
|
|
|
/**
|
|
* Get the Ready Update directory. This is the directory that an update
|
|
* should reside in after download has completed but before it has been
|
|
* installed and cleaned up.
|
|
* @return The ready updates directory, as a nsIFile object
|
|
*/
|
|
function getReadyUpdateDir() {
|
|
return getUpdateDirCreate([DIR_UPDATES, DIR_UPDATE_READY]);
|
|
}
|
|
|
|
/**
|
|
* Get the Downloading Update directory. This is the directory that an update
|
|
* should reside in during download. Once download is completed, it will be
|
|
* moved to the Ready Update directory.
|
|
* @return The downloading update directory, as a nsIFile object
|
|
*/
|
|
function getDownloadingUpdateDir() {
|
|
return getUpdateDirCreate([DIR_UPDATES, DIR_UPDATE_DOWNLOADING]);
|
|
}
|
|
|
|
/**
|
|
* Reads the update state from the update.status file in the specified
|
|
* directory.
|
|
* @param dir
|
|
* The dir to look for an update.status file in
|
|
* @return The status value of the update.
|
|
*/
|
|
function readStatusFile(dir) {
|
|
let statusFile = dir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
let status = readStringFromFile(statusFile) || STATE_NONE;
|
|
LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* 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 files for debugging purposes.
|
|
// Make sure to keep the pairs of logs (ex "last-update.log" and
|
|
// "last-update-elevated.log") together. We don't want to skip moving
|
|
// "last-update-elevated.log" just because there isn't an
|
|
// "update-elevated.log" to take its place.
|
|
let updateLogFile = updateDir.clone();
|
|
updateLogFile.append(FILE_UPDATE_LOG);
|
|
let updateElevatedLogFile = updateDir.clone();
|
|
updateElevatedLogFile.append(FILE_UPDATE_ELEVATED_LOG);
|
|
if (updateLogFile.exists() || updateElevatedLogFile.exists()) {
|
|
const overwriteOrRemoveBackupLog = (log, shouldOverwrite, backupName) => {
|
|
if (shouldOverwrite) {
|
|
try {
|
|
log.moveTo(dir, backupName);
|
|
} catch (e) {
|
|
LOG(
|
|
`cleanUpReadyUpdateDir - failed to rename file '${log.path}' to ` +
|
|
`'${backupName}': ${e.result}`
|
|
);
|
|
}
|
|
} else {
|
|
// If we don't have a file to overwrite this one, make sure we remove
|
|
// it anyways to prevent log pairs from getting mismatched.
|
|
let backupLogFile = dir.clone();
|
|
backupLogFile.append(backupName);
|
|
try {
|
|
backupLogFile.remove(false);
|
|
} catch (e) {
|
|
if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
LOG(
|
|
`cleanUpReadyUpdateDir - failed to remove file ` +
|
|
`'${backupLogFile.path}': ${e.result}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let dir = updateDir.parent;
|
|
let logFile = dir.clone();
|
|
logFile.append(FILE_LAST_UPDATE_LOG);
|
|
const logFileExists = logFile.exists();
|
|
let elevatedLogFile = dir.clone();
|
|
elevatedLogFile.append(FILE_LAST_UPDATE_ELEVATED_LOG);
|
|
const elevatedLogFileExists = elevatedLogFile.exists();
|
|
if (logFileExists || elevatedLogFileExists) {
|
|
overwriteOrRemoveBackupLog(
|
|
logFile,
|
|
logFileExists,
|
|
FILE_BACKUP_UPDATE_LOG
|
|
);
|
|
overwriteOrRemoveBackupLog(
|
|
elevatedLogFile,
|
|
elevatedLogFileExists,
|
|
FILE_BACKUP_UPDATE_ELEVATED_LOG
|
|
);
|
|
}
|
|
|
|
overwriteOrRemoveBackupLog(updateLogFile, true, FILE_LAST_UPDATE_LOG);
|
|
overwriteOrRemoveBackupLog(
|
|
updateElevatedLogFile,
|
|
true,
|
|
FILE_LAST_UPDATE_ELEVATED_LOG
|
|
);
|
|
}
|
|
|
|
if (aRemovePatchFiles) {
|
|
let dirEntries;
|
|
try {
|
|
dirEntries = updateDir.directoryEntries;
|
|
} catch (ex) {
|
|
// If if doesn't exist, our job is already done.
|
|
if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
return;
|
|
}
|
|
throw ex;
|
|
}
|
|
while (dirEntries.hasMoreElements()) {
|
|
let file = dirEntries.nextFile;
|
|
// Now, recursively remove this file. The recursive removal is needed for
|
|
// Mac OSX because this directory will contain a copy of updater.app,
|
|
// which is itself a directory and the MozUpdater directory on platforms
|
|
// other than Windows.
|
|
try {
|
|
file.remove(true);
|
|
} catch (e) {
|
|
LOG("cleanUpReadyUpdateDir - failed to remove file " + file.path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the contents of the update download directory.
|
|
*
|
|
*/
|
|
function cleanUpDownloadingUpdateDir() {
|
|
let updateDir;
|
|
try {
|
|
updateDir = getDownloadingUpdateDir();
|
|
} catch (e) {
|
|
LOG(
|
|
"cleanUpDownloadUpdatesDir - unable to get the updates patch " +
|
|
"directory. Exception: " +
|
|
e
|
|
);
|
|
return;
|
|
}
|
|
|
|
let dirEntries;
|
|
try {
|
|
dirEntries = updateDir.directoryEntries;
|
|
} catch (ex) {
|
|
// If if doesn't exist, our job is already done.
|
|
if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
return;
|
|
}
|
|
throw ex;
|
|
}
|
|
while (dirEntries.hasMoreElements()) {
|
|
let file = dirEntries.nextFile;
|
|
// Now, recursively remove this file.
|
|
try {
|
|
file.remove(true);
|
|
} catch (e) {
|
|
LOG("cleanUpDownloadUpdatesDir - failed to remove file " + file.path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up the updates list and the directory that contains the update that
|
|
* is ready to be installed.
|
|
*
|
|
* Note - This function causes a state transition to either STATE_DOWNLOADING
|
|
* or STATE_NONE, depending on whether an update download is in progress.
|
|
*/
|
|
function cleanupReadyUpdate() {
|
|
// Move the update from the Active Update list into the Past Updates list.
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
LOG("cleanupReadyUpdate - Clearing readyUpdate");
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
|
|
lazy.UM.internal.readyUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
let readyUpdateDir = getReadyUpdateDir();
|
|
let shouldSetDownloadingStatus =
|
|
lazy.UM.internal.downloadingUpdate ||
|
|
readStatusFile(readyUpdateDir) == STATE_DOWNLOADING;
|
|
|
|
// Now trash the ready update directory, since we're done with it
|
|
cleanUpReadyUpdateDir();
|
|
|
|
// We need to handle two similar cases here.
|
|
// The first is where we clean up the ready updates directory while we are in
|
|
// the downloading state. In this case, we remove the update.status file that
|
|
// says we are downloading, even though we should remain in that state.
|
|
// The second case is when we clean up a ready update, but there is also a
|
|
// downloading update (in which case the update status file's state will
|
|
// reflect the state of the ready update, not the downloading one). In that
|
|
// case, instead of reverting to STATE_NONE (which is what we do by removing
|
|
// the status file), we should set our state to downloading.
|
|
if (shouldSetDownloadingStatus) {
|
|
LOG("cleanupReadyUpdate - Transitioning back to downloading state.");
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
|
|
writeStatusFile(readyUpdateDir, STATE_DOWNLOADING);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up updates list and the directory that the currently downloading update
|
|
* is downloaded to.
|
|
*
|
|
* Note - This function may cause a state transition. If the current state is
|
|
* STATE_DOWNLOADING, this will cause it to change to STATE_NONE.
|
|
*/
|
|
function cleanupDownloadingUpdate() {
|
|
// Move the update from the Active Update list into the Past Updates list.
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
LOG("cleanupDownloadingUpdate - Clearing downloadingUpdate.");
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
|
|
lazy.UM.internal.downloadingUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
// Now trash the update download directory, since we're done with it
|
|
cleanUpDownloadingUpdateDir();
|
|
|
|
// If the update status file says we are downloading, we should remove that
|
|
// too, since we aren't doing that anymore.
|
|
let readyUpdateDir = getReadyUpdateDir();
|
|
let status = readStatusFile(readyUpdateDir);
|
|
if (status == STATE_DOWNLOADING) {
|
|
let statusFile = readyUpdateDir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
statusFile.remove();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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.internal.readyUpdate) {
|
|
LOG("cleanupActiveUpdates - Clearing readyUpdate");
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
|
|
lazy.UM.internal.readyUpdate = null;
|
|
}
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
LOG("cleanupActiveUpdates - Clearing downloadingUpdate.");
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
|
|
lazy.UM.internal.downloadingUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
// Now trash both active update directories, since we're done with them
|
|
cleanUpReadyUpdateDir();
|
|
cleanUpDownloadingUpdateDir();
|
|
}
|
|
|
|
/**
|
|
* Writes a string of text to a file. A newline will be appended to the data
|
|
* written to the file. This function only works with ASCII text.
|
|
* @param file An nsIFile indicating what file to write to.
|
|
* @param text A string containing the text to write to the file.
|
|
* @return true on success, false on failure.
|
|
*/
|
|
function writeStringToFile(file, text) {
|
|
try {
|
|
let fos = FileUtils.openSafeFileOutputStream(file);
|
|
text += "\n";
|
|
fos.write(text, text.length);
|
|
FileUtils.closeSafeFileOutputStream(fos);
|
|
} catch (e) {
|
|
LOG(`writeStringToFile - Failed to write to file: "${file}". Error: ${e}"`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function readStringFromInputStream(inputStream) {
|
|
var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
|
|
Ci.nsIScriptableInputStream
|
|
);
|
|
sis.init(inputStream);
|
|
var text = sis.read(sis.available());
|
|
sis.close();
|
|
if (text && text[text.length - 1] == "\n") {
|
|
text = text.slice(0, -1);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Reads a string of text from a file. A trailing newline will be removed
|
|
* before the result is returned. This function only works with ASCII text.
|
|
*/
|
|
function readStringFromFile(file) {
|
|
if (!file.exists()) {
|
|
LOG("readStringFromFile - file doesn't exist: " + file.path);
|
|
return null;
|
|
}
|
|
var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
|
|
Ci.nsIFileInputStream
|
|
);
|
|
fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
|
|
return readStringFromInputStream(fis);
|
|
}
|
|
|
|
/**
|
|
* Attempts to recover from an update error. If successful, `true` will be
|
|
* returned and AUS.currentState will be transitioned.
|
|
*/
|
|
function handleUpdateFailure(update) {
|
|
if (WRITE_ERRORS.includes(update.errorCode)) {
|
|
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.internal.downloadingUpdate || lazy.UM.internal.readyUpdate;
|
|
if (!update) {
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - Unable to find an update to fall " +
|
|
"back to."
|
|
);
|
|
return;
|
|
}
|
|
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - Cleaning up active updates in " +
|
|
"preparation of falling back to complete update."
|
|
);
|
|
await lazy.AUS.internal.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 result = await lazy.AUS.internal.downloadUpdate(update);
|
|
if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_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.sys.mjs
|
|
default:
|
|
stateCode = 1;
|
|
}
|
|
|
|
if (parts.length > 1) {
|
|
let statusErrorCode = INVALID_UPDATER_STATE_CODE;
|
|
if (parts[0] == STATE_FAILED) {
|
|
statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
|
|
}
|
|
AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode);
|
|
}
|
|
}
|
|
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.internal.readyUpdate ||
|
|
!lazy.UM.internal.readyUpdate.appVersion ||
|
|
!lazy.UM.internal.readyUpdate.buildID
|
|
) {
|
|
return false;
|
|
}
|
|
return updateIsAtLeastAsOldAs(
|
|
update,
|
|
lazy.UM.internal.readyUpdate.appVersion,
|
|
lazy.UM.internal.readyUpdate.buildID
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This function determines whether the error represented by the passed error
|
|
* code could potentially be recovered from or bypassed by updating without
|
|
* using the Maintenance Service (i.e. by showing a UAC prompt).
|
|
* We don't really want to show a UAC prompt, but it's preferable over the
|
|
* manual update doorhanger. So this function effectively distinguishes between
|
|
* which of those we should do if update staging failed. (The updater
|
|
* automatically falls back if the Maintenance Services fails, so this function
|
|
* doesn't handle that case)
|
|
*
|
|
* @param An integer error code from the update.status file. Should be one of
|
|
* the codes enumerated in updatererrors.h.
|
|
* @returns true if the code represents a Maintenance Service specific error.
|
|
* Otherwise, false.
|
|
*/
|
|
function isServiceSpecificErrorCode(errorCode) {
|
|
return (
|
|
(errorCode >= 24 && errorCode <= 33) || (errorCode >= 49 && errorCode <= 58)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This function determines whether the error represented by the passed error
|
|
* code is the result of the updater failing to allocate memory. This is
|
|
* relevant when staging because, since Firefox is also running, we may not be
|
|
* able to allocate much memory. Thus, if we fail to stage an update, we may
|
|
* succeed at updating without staging.
|
|
*
|
|
* @param An integer error code from the update.status file. Should be one of
|
|
* the codes enumerated in updatererrors.h.
|
|
* @returns true if the code represents a memory allocation error.
|
|
* Otherwise, false.
|
|
*/
|
|
function isMemoryAllocationErrorCode(errorCode) {
|
|
return errorCode >= 10 && errorCode <= 14;
|
|
}
|
|
|
|
/**
|
|
* Normally when staging, `nsUpdateProcessor::WaitForProcess` waits for the
|
|
* staging process to complete by watching for its PID to terminate.
|
|
* However, there are less ideal situations. Notably, we might start the browser
|
|
* and find that update staging appears to already be in-progress. If that
|
|
* happens, we really want to pick up the update process from STATE_STAGING,
|
|
* but we don't really have any way of keeping an eye on the staging process
|
|
* other than to just poll the status file.
|
|
*
|
|
* Like `nsUpdateProcessor`, this calls `nsIUpdateManager.refreshUpdateStatus`
|
|
* after polling completes (regardless of result).
|
|
*
|
|
* It is also important to keep in mind that the updater might have crashed
|
|
* during staging, meaning that the status file will never change, no matter how
|
|
* long we keep polling. So we need to set an upper bound on how long we are
|
|
* willing to poll for.
|
|
*
|
|
* There are three situations that we want to avoid.
|
|
* (1) We don't want to set the poll interval too long. A user might be watching
|
|
* the user interface and waiting to restart to install the update. A long poll
|
|
* interval will cause them to have to wait longer than necessary. Especially
|
|
* since the expected total staging time is not that long.
|
|
* (2) We don't want to give up polling too early and give up on an update that
|
|
* will ultimately succeed.
|
|
* (3) We don't want to use a rapid polling interval over a long duration.
|
|
*
|
|
* To avoid these situations, we will start with a short polling interval, but
|
|
* will increase it the longer that we have to wait. Then if we hit the upper
|
|
* bound of polling, we will give up.
|
|
*/
|
|
function pollForStagingEnd() {
|
|
let pollingIntervalMs = STAGING_POLLING_MIN_INTERVAL_MS;
|
|
// Number of times to poll before increasing the polling interval.
|
|
let pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL;
|
|
let timeElapsedMs = 0;
|
|
|
|
let pollingFn = () => {
|
|
pollAttemptsAtIntervalRemaining -= 1;
|
|
// This isn't a perfectly accurate way of keeping time, but it does nicely
|
|
// sidestep dealing with issues of (non)monotonic time.
|
|
timeElapsedMs += pollingIntervalMs;
|
|
|
|
if (timeElapsedMs >= STAGING_POLLING_MAX_DURATION_MS) {
|
|
lazy.UM.internal.refreshUpdateStatus();
|
|
return;
|
|
}
|
|
|
|
if (readStatusFile(getReadyUpdateDir()) != STATE_APPLYING) {
|
|
lazy.UM.internal.refreshUpdateStatus();
|
|
return;
|
|
}
|
|
|
|
if (pollAttemptsAtIntervalRemaining <= 0) {
|
|
pollingIntervalMs = Math.min(
|
|
pollingIntervalMs * 2,
|
|
STAGING_POLLING_MAX_INTERVAL_MS
|
|
);
|
|
pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL;
|
|
}
|
|
|
|
lazy.setTimeout(pollingFn, pollingIntervalMs);
|
|
};
|
|
|
|
lazy.setTimeout(pollingFn, pollingIntervalMs);
|
|
}
|
|
|
|
class UpdatePatch {
|
|
// nsIUpdatePatch attribute names used to prevent nsIWritablePropertyBag from
|
|
// over writing nsIUpdatePatch attributes.
|
|
_attrNames = [
|
|
"errorCode",
|
|
"finalURL",
|
|
"selected",
|
|
"size",
|
|
"state",
|
|
"type",
|
|
"URL",
|
|
];
|
|
|
|
/**
|
|
* @param patch
|
|
* A <patch> element to initialize this object with
|
|
* @throws if patch has a size of 0
|
|
* @constructor
|
|
*/
|
|
constructor(patch) {
|
|
this._properties = {};
|
|
this.errorCode = 0;
|
|
this.finalURL = null;
|
|
this.state = STATE_NONE;
|
|
|
|
for (let i = 0; i < patch.attributes.length; ++i) {
|
|
var attr = patch.attributes.item(i);
|
|
// If an undefined value is saved to the xml file it will be a string when
|
|
// it is read from the xml file.
|
|
if (attr.value == "undefined") {
|
|
continue;
|
|
}
|
|
switch (attr.name) {
|
|
case "xmlns":
|
|
// Don't save the XML namespace.
|
|
break;
|
|
case "selected":
|
|
this.selected = attr.value == "true";
|
|
break;
|
|
case "size":
|
|
if (0 == parseInt(attr.value)) {
|
|
LOG("UpdatePatch:init - 0-sized patch!");
|
|
throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
|
|
}
|
|
this[attr.name] = attr.value;
|
|
break;
|
|
case "errorCode":
|
|
if (attr.value) {
|
|
let val = parseInt(attr.value);
|
|
// This will evaluate to false if the value is 0 but that's ok since
|
|
// this.errorCode is set to the default of 0 above.
|
|
if (val) {
|
|
this.errorCode = val;
|
|
}
|
|
}
|
|
break;
|
|
case "finalURL":
|
|
case "state":
|
|
case "type":
|
|
case "URL":
|
|
this[attr.name] = attr.value;
|
|
break;
|
|
default:
|
|
if (!this._attrNames.includes(attr.name)) {
|
|
// Set nsIPropertyBag properties that were read from the xml file.
|
|
this.setProperty(attr.name, attr.value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
serialize(updates) {
|
|
var patch = updates.createElementNS(URI_UPDATE_NS, "patch");
|
|
patch.setAttribute("size", this.size);
|
|
patch.setAttribute("type", this.type);
|
|
patch.setAttribute("URL", this.URL);
|
|
// Don't write an errorCode if it evaluates to false since 0 is the same as
|
|
// no error code.
|
|
if (this.errorCode) {
|
|
patch.setAttribute("errorCode", this.errorCode);
|
|
}
|
|
// finalURL is not available until after the download has started
|
|
if (this.finalURL) {
|
|
patch.setAttribute("finalURL", this.finalURL);
|
|
}
|
|
// The selected patch is the only patch that should have this attribute.
|
|
if (this.selected) {
|
|
patch.setAttribute("selected", this.selected);
|
|
}
|
|
if (this.state != STATE_NONE) {
|
|
patch.setAttribute("state", this.state);
|
|
}
|
|
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
patch.setAttribute(name, value.data);
|
|
}
|
|
}
|
|
return patch;
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
setProperty(name, value) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdatePatch) " +
|
|
"when calling method: [nsIWritablePropertyBag::setProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
this._properties[name] = { data: value, present: true };
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
deleteProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdatePatch) " +
|
|
"when calling method: [nsIWritablePropertyBag::deleteProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties) {
|
|
this._properties[name].present = false;
|
|
} else {
|
|
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
*
|
|
* Note: this only contains the nsIPropertyBag name / value pairs and not the
|
|
* nsIUpdatePatch name / value pairs.
|
|
*/
|
|
get enumerator() {
|
|
return this.enumerate();
|
|
}
|
|
|
|
*enumerate() {
|
|
// An nsISupportsInterfacePointer is used so creating an array using
|
|
// Array.from will retain the QueryInterface for nsIProperty.
|
|
let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
|
|
Ci.nsISupportsInterfacePointer
|
|
);
|
|
let qi = ChromeUtils.generateQI([Ci.nsIProperty]);
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
// The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
|
|
// elements are nsIProperty objects. Calling QueryInterface for
|
|
// nsIProperty on the object doesn't return to the caller an object that
|
|
// is already queried to nsIProperty but do it just in case it is fixed
|
|
// at some point.
|
|
ip.data = { name, value: value.data, QueryInterface: qi };
|
|
yield ip.data.QueryInterface(Ci.nsIProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
*
|
|
* Note: returns null instead of throwing when the property doesn't exist to
|
|
* simplify code and to silence warnings in debug builds.
|
|
*/
|
|
getProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdatePatch) " +
|
|
"when calling method: [nsIWritablePropertyBag::getProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties && this._properties[name].present) {
|
|
return this._properties[name].data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIUpdatePatch,
|
|
Ci.nsIPropertyBag,
|
|
Ci.nsIWritablePropertyBag,
|
|
]);
|
|
}
|
|
|
|
class Update {
|
|
// nsIUpdate attribute names used to prevent nsIWritablePropertyBag from over
|
|
// writing nsIUpdate attributes.
|
|
_attrNames = [
|
|
"appVersion",
|
|
"buildID",
|
|
"channel",
|
|
"detailsURL",
|
|
"displayVersion",
|
|
"elevationFailure",
|
|
"errorCode",
|
|
"installDate",
|
|
"isCompleteUpdate",
|
|
"name",
|
|
"previousAppVersion",
|
|
"promptWaitTime",
|
|
"serviceURL",
|
|
"state",
|
|
"statusText",
|
|
"type",
|
|
"unsupported",
|
|
];
|
|
|
|
/**
|
|
* Implements nsIUpdate
|
|
* @param update
|
|
* An <update> element to initialize this object with
|
|
* @throws if the update contains no patches
|
|
* @constructor
|
|
*/
|
|
constructor(update) {
|
|
this._patches = [];
|
|
this._properties = {};
|
|
this.isCompleteUpdate = false;
|
|
this.channel = "default";
|
|
this.promptWaitTime = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_PROMPTWAITTIME,
|
|
43200
|
|
);
|
|
this.unsupported = false;
|
|
|
|
// Null <update>, assume this is a message container and do no
|
|
// further initialization
|
|
if (!update) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < update.childNodes.length; ++i) {
|
|
let patchElement = update.childNodes.item(i);
|
|
if (
|
|
patchElement.nodeType != patchElement.ELEMENT_NODE ||
|
|
patchElement.localName != "patch"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let patch;
|
|
try {
|
|
patch = new UpdatePatch(patchElement);
|
|
} catch (e) {
|
|
continue;
|
|
}
|
|
this._patches.push(patch);
|
|
}
|
|
|
|
if (!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,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
getPatchAt(index) {
|
|
return this._patches[index];
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*
|
|
* We use a copy of the state cached on this object in |_state| only when
|
|
* there is no selected patch, i.e. in the case when we could not load
|
|
* active updates from the update manager for some reason but still have
|
|
* the update.status file to work with.
|
|
*/
|
|
_state = "";
|
|
get state() {
|
|
if (this.selectedPatch) {
|
|
return this.selectedPatch.state;
|
|
}
|
|
return this._state;
|
|
}
|
|
set state(state) {
|
|
if (this.selectedPatch) {
|
|
this.selectedPatch.state = state;
|
|
}
|
|
this._state = state;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*
|
|
* We use a copy of the errorCode cached on this object in |_errorCode| only
|
|
* when there is no selected patch, i.e. in the case when we could not load
|
|
* active updates from the update manager for some reason but still have
|
|
* the update.status file to work with.
|
|
*/
|
|
_errorCode = 0;
|
|
get errorCode() {
|
|
if (this.selectedPatch) {
|
|
return this.selectedPatch.errorCode;
|
|
}
|
|
return this._errorCode;
|
|
}
|
|
set errorCode(errorCode) {
|
|
if (this.selectedPatch) {
|
|
this.selectedPatch.errorCode = errorCode;
|
|
}
|
|
this._errorCode = errorCode;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get selectedPatch() {
|
|
for (let i = 0; i < this.patchCount; ++i) {
|
|
if (this._patches[i].selected) {
|
|
return this._patches[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
serialize(updates) {
|
|
// If appVersion isn't defined just return null. This happens when cleaning
|
|
// up invalid updates (e.g. incorrect channel).
|
|
if (!this.appVersion) {
|
|
return null;
|
|
}
|
|
let update = updates.createElementNS(URI_UPDATE_NS, "update");
|
|
update.setAttribute("appVersion", this.appVersion);
|
|
update.setAttribute("buildID", this.buildID);
|
|
update.setAttribute("channel", this.channel);
|
|
update.setAttribute("detailsURL", this.detailsURL);
|
|
update.setAttribute("displayVersion", this.displayVersion);
|
|
update.setAttribute("installDate", this.installDate);
|
|
update.setAttribute("isCompleteUpdate", this.isCompleteUpdate);
|
|
update.setAttribute("name", this.name);
|
|
update.setAttribute("previousAppVersion", this.previousAppVersion);
|
|
update.setAttribute("promptWaitTime", this.promptWaitTime);
|
|
update.setAttribute("serviceURL", this.serviceURL);
|
|
update.setAttribute("type", this.type);
|
|
|
|
if (this.statusText) {
|
|
update.setAttribute("statusText", this.statusText);
|
|
}
|
|
if (this.unsupported) {
|
|
update.setAttribute("unsupported", this.unsupported);
|
|
}
|
|
if (this.elevationFailure) {
|
|
update.setAttribute("elevationFailure", this.elevationFailure);
|
|
}
|
|
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
update.setAttribute(name, value.data);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < this.patchCount; ++i) {
|
|
update.appendChild(this.getPatchAt(i).serialize(updates));
|
|
}
|
|
|
|
updates.documentElement.appendChild(update);
|
|
return update;
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
setProperty(name, value) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdate) " +
|
|
"when calling method: [nsIWritablePropertyBag::setProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
this._properties[name] = { data: value, present: true };
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
deleteProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdate) " +
|
|
"when calling method: [nsIWritablePropertyBag::deleteProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties) {
|
|
this._properties[name].present = false;
|
|
} else {
|
|
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
*
|
|
* Note: this only contains the nsIPropertyBag name value / pairs and not the
|
|
* nsIUpdate name / value pairs.
|
|
*/
|
|
get enumerator() {
|
|
return this.enumerate();
|
|
}
|
|
|
|
*enumerate() {
|
|
// An nsISupportsInterfacePointer is used so creating an array using
|
|
// Array.from will retain the QueryInterface for nsIProperty.
|
|
let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
|
|
Ci.nsISupportsInterfacePointer
|
|
);
|
|
let qi = ChromeUtils.generateQI([Ci.nsIProperty]);
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
// The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
|
|
// elements are nsIProperty objects. Calling QueryInterface for
|
|
// nsIProperty on the object doesn't return to the caller an object that
|
|
// is already queried to nsIProperty but do it just in case it is fixed
|
|
// at some point.
|
|
ip.data = { name, value: value.data, QueryInterface: qi };
|
|
yield ip.data.QueryInterface(Ci.nsIProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
* Note: returns null instead of throwing when the property doesn't exist to
|
|
* simplify code and to silence warnings in debug builds.
|
|
*/
|
|
getProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdate) " +
|
|
"when calling method: [nsIWritablePropertyBag::getProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties && this._properties[name].present) {
|
|
return this._properties[name].data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIUpdate,
|
|
Ci.nsIPropertyBag,
|
|
Ci.nsIWritablePropertyBag,
|
|
]);
|
|
}
|
|
|
|
export class UpdateService {
|
|
#initPromise;
|
|
|
|
/**
|
|
* The downloader we are using to download updates. There is only ever one of
|
|
* these.
|
|
*/
|
|
_downloader = null;
|
|
|
|
/**
|
|
* Whether or not the service registered the "online" observer.
|
|
*/
|
|
_registeredOnlineObserver = false;
|
|
|
|
/**
|
|
* The current number of consecutive socket errors
|
|
*/
|
|
_consecutiveSocketErrors = 0;
|
|
|
|
/**
|
|
* A timer used to retry socket errors
|
|
*/
|
|
_retryTimer = null;
|
|
|
|
/**
|
|
* Whether or not a background update check was initiated by the
|
|
* application update timer notification.
|
|
*/
|
|
_isNotify = true;
|
|
|
|
/**
|
|
* UpdateService
|
|
* A Service for managing the discovery and installation of software updates.
|
|
* @constructor
|
|
*/
|
|
constructor() {
|
|
LOG("Creating UpdateService");
|
|
// The observor notification to shut down the service must be before
|
|
// profile-before-change since nsIUpdateManager uses profile-before-change
|
|
// to shutdown and write the update xml files.
|
|
Services.obs.addObserver(this, "quit-application");
|
|
lazy.UpdateLog.addConfigChangeListener(() => {
|
|
this._logStatus();
|
|
});
|
|
|
|
this._logStatus();
|
|
|
|
this.internal = {
|
|
init: async () => this.#init(),
|
|
downloadUpdate: async update => this.#downloadUpdate(update),
|
|
postUpdateProcessing: async () => this._postUpdateProcessing(),
|
|
stopDownload: async () => this.#stopDownload(),
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
Ci.nsIApplicationUpdateServiceInternal,
|
|
]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async init() {
|
|
await lazy.UpdateServiceStub.initUpdate();
|
|
}
|
|
|
|
/**
|
|
* See nsIApplicationUpdateServiceInternal.init in nsIUpdateService.idl
|
|
*/
|
|
async #init() {
|
|
if (!this.#initPromise) {
|
|
this.#initPromise = this._postUpdateProcessing();
|
|
}
|
|
|
|
await this.#initPromise;
|
|
}
|
|
|
|
/**
|
|
* Handle Observer Service notifications
|
|
* @param subject
|
|
* The subject of the notification
|
|
* @param topic
|
|
* The notification name
|
|
* @param data
|
|
* Additional data
|
|
*/
|
|
async observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "network:offline-status-changed":
|
|
await this._offlineStatusChanged(data);
|
|
break;
|
|
case "quit-application":
|
|
Services.obs.removeObserver(this, topic);
|
|
|
|
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();
|
|
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.
|
|
*
|
|
* This will only be called during `UpdateService` initialization and should
|
|
* not be called again (with possible exceptions in testing).
|
|
*/
|
|
/* eslint-disable-next-line complexity */
|
|
async _postUpdateProcessing() {
|
|
if (!this.canCheckForUpdates) {
|
|
LOG(
|
|
"UpdateService:_postUpdateProcessing - unable to check for " +
|
|
"updates... returning early"
|
|
);
|
|
return;
|
|
}
|
|
const readyUpdateDir = getReadyUpdateDir();
|
|
let status = readStatusFile(readyUpdateDir);
|
|
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.internal.readyUpdate) {
|
|
updates.push(lazy.UM.internal.readyUpdate);
|
|
}
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
updates.push(lazy.UM.internal.downloadingUpdate);
|
|
}
|
|
|
|
if (status == STATE_NONE) {
|
|
// A status of STATE_NONE in _postUpdateProcessing means that the there
|
|
// isn't an update in progress. Let's make sure we initialize into a good
|
|
// state.
|
|
// Under some edgecases such as Windows system restore the
|
|
// active-update.xml will contain a pending update without the status
|
|
// file. We need to deal with that situation gracefully.
|
|
LOG("UpdateService:_postUpdateProcessing - Initial cleanup");
|
|
|
|
const statusFile = readyUpdateDir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
if (statusFile.exists()) {
|
|
// This file existing but not having a state in it is unexpected. In
|
|
// addition to putting things in a good state, put a null update in the
|
|
// update history if we didn't start up with any.
|
|
LOG("UpdateService:_postUpdateProcessing - Status file is empty?");
|
|
|
|
if (!updates.length) {
|
|
updates.push(new Update(null));
|
|
}
|
|
}
|
|
|
|
// There shouldn't be any updates when the update status is null. Mark any
|
|
// updates that are in there as having failed.
|
|
if (updates.length) {
|
|
for (let update of updates) {
|
|
update.state = STATE_FAILED;
|
|
update.errorCode = ERR_UPDATE_STATE_NONE;
|
|
update.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("statusFailed");
|
|
}
|
|
let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE;
|
|
pingStateAndStatusCodes(updates[0], true, newStatus);
|
|
}
|
|
|
|
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.internal.readyUpdate
|
|
? lazy.UM.internal.readyUpdate.channel
|
|
: lazy.UM.internal.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.internal.readyUpdate,
|
|
Services.appinfo.version,
|
|
Services.appinfo.appBuildID
|
|
)
|
|
) {
|
|
tooOldUpdate = lazy.UM.internal.readyUpdate;
|
|
} else if (
|
|
updateIsAtLeastAsOldAs(
|
|
lazy.UM.internal.downloadingUpdate,
|
|
Services.appinfo.version,
|
|
Services.appinfo.appBuildID
|
|
)
|
|
) {
|
|
tooOldUpdate = lazy.UM.internal.downloadingUpdate;
|
|
}
|
|
if (tooOldUpdate) {
|
|
LOG(
|
|
"UpdateService:_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.internal.downloadingUpdate
|
|
: lazy.UM.internal.readyUpdate,
|
|
true,
|
|
status
|
|
);
|
|
if (lazy.UM.internal.downloadingUpdate || status == STATE_DOWNLOADING) {
|
|
if (status == STATE_SUCCEEDED) {
|
|
// If we successfully installed an update while we were downloading
|
|
// another update, the downloading update is now a partial MAR for
|
|
// a version that is no longer installed. We know that it's a partial
|
|
// MAR without checking because we currently only download partial MARs
|
|
// when an update has already been downloaded.
|
|
LOG(
|
|
"UpdateService:_postUpdateProcessing - removing downloading patch " +
|
|
"because we installed a different patch before it finished" +
|
|
"downloading."
|
|
);
|
|
cleanupDownloadingUpdate();
|
|
} else {
|
|
// Attempt to resume download
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
LOG(
|
|
"UpdateService:_postUpdateProcessing - resuming patch found in " +
|
|
"downloading state"
|
|
);
|
|
let result = await this.#downloadUpdate(
|
|
lazy.UM.internal.downloadingUpdate
|
|
);
|
|
if (
|
|
result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS &&
|
|
result !=
|
|
Ci.nsIApplicationUpdateService
|
|
.DOWNLOAD_FAILURE_CANNOT_RESUME_IN_BACKGROUND
|
|
) {
|
|
LOG(
|
|
"UpdateService:_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.internal.readyUpdate;
|
|
|
|
if (status == STATE_APPLYING) {
|
|
// This indicates that the background updater service is in either of the
|
|
// following two states:
|
|
// 1. It is in the process of applying an update in the background, and
|
|
// we just happen to be racing against that.
|
|
// 2. It has failed to apply an update for some reason, and we hit this
|
|
// case because the updater process has set the update status to
|
|
// applying, but has never finished.
|
|
// In order to differentiate between these two states, we look at the
|
|
// state field of the update object. If it's "pending", then we know
|
|
// that this is the first time we're hitting this case, so we switch
|
|
// that state to "applying" and we just wait and hope for the best.
|
|
// If it's "applying", we know that we've already been here once, so
|
|
// we really want to start from a clean state.
|
|
if (
|
|
update &&
|
|
(update.state == STATE_PENDING || update.state == STATE_PENDING_SERVICE)
|
|
) {
|
|
LOG(
|
|
"UpdateService:_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.internal.readyUpdate) {
|
|
LOG(
|
|
"UpdateService:_postUpdateProcessing - Assigning successful update " +
|
|
"readyUpdate before cleaning it up."
|
|
);
|
|
lazy.UM.internal.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() {
|
|
if (this._registeredOnlineObserver) {
|
|
LOG(
|
|
"UpdateService:_registerOnlineObserver - observer already registered"
|
|
);
|
|
return;
|
|
}
|
|
|
|
LOG(
|
|
"UpdateService:_registerOnlineObserver - waiting for the network to " +
|
|
"be online, then forcing another check"
|
|
);
|
|
|
|
Services.obs.addObserver(this, "network:offline-status-changed");
|
|
this._registeredOnlineObserver = true;
|
|
}
|
|
|
|
/**
|
|
* Called from the network:offline-status-changed observer.
|
|
*/
|
|
async _offlineStatusChanged(status) {
|
|
if (status !== "online") {
|
|
return;
|
|
}
|
|
|
|
Services.obs.removeObserver(this, "network:offline-status-changed");
|
|
this._registeredOnlineObserver = false;
|
|
|
|
LOG(
|
|
"UpdateService:_offlineStatusChanged - network is online, forcing " +
|
|
"another background check"
|
|
);
|
|
|
|
// the background checker is contained in notify
|
|
await this._attemptResume();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async onCheckComplete(result) {
|
|
if (result.succeeded) {
|
|
await this._selectAndInstallUpdate(result.updates);
|
|
return;
|
|
}
|
|
|
|
if (!result.checksAllowed) {
|
|
LOG("UpdateService:onCheckComplete - checks not allowed");
|
|
return;
|
|
}
|
|
|
|
// On failure, result.updates is guaranteed to have exactly one update
|
|
// containing error information.
|
|
let update = result.updates[0];
|
|
|
|
LOG(
|
|
"UpdateService:onCheckComplete - error during background update. error " +
|
|
"code: " +
|
|
update.errorCode +
|
|
", status text: " +
|
|
update.statusText
|
|
);
|
|
|
|
if (update.errorCode == NETWORK_ERROR_OFFLINE) {
|
|
// Register an online observer to try again
|
|
this._registerOnlineObserver();
|
|
if (this._pingSuffix) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Send the error code to telemetry
|
|
AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode);
|
|
update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES;
|
|
let errCount = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_BACKGROUNDERRORS,
|
|
0
|
|
);
|
|
|
|
// If we already have an update ready, we don't want to worry the user over
|
|
// update check failures. As far as the user knows, the update status is
|
|
// the status of the ready update. We don't want to confuse them by saying
|
|
// that an update check failed.
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
LOG(
|
|
"UpdateService:onCheckComplete - Ignoring error because another " +
|
|
"update is ready."
|
|
);
|
|
return;
|
|
}
|
|
|
|
errCount++;
|
|
Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
|
|
// Don't allow the preference to set a value greater than 20 for max errors.
|
|
let maxErrors = Math.min(
|
|
Services.prefs.getIntPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10),
|
|
20
|
|
);
|
|
|
|
if (errCount >= maxErrors) {
|
|
LOG(
|
|
"UpdateService:onCheckComplete - notifying observers of error. " +
|
|
"topic: update-error, status: check-attempts-exceeded"
|
|
);
|
|
Services.obs.notifyObservers(
|
|
update,
|
|
"update-error",
|
|
"check-attempts-exceeded"
|
|
);
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
|
|
} else {
|
|
LOG(
|
|
"UpdateService:onCheckComplete - notifying observers of error. " +
|
|
"topic: update-error, status: check-attempt-failed"
|
|
);
|
|
Services.obs.notifyObservers(
|
|
update,
|
|
"update-error",
|
|
"check-attempt-failed"
|
|
);
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when a connection should be resumed
|
|
*/
|
|
async _attemptResume() {
|
|
LOG("UpdateService:_attemptResume");
|
|
// If a download is in progress and we aren't already downloading it, then
|
|
// resume it.
|
|
if (this.isDownloading) {
|
|
// There is nothing to resume. We are already downloading.
|
|
LOG("UpdateService:_attemptResume - already downloading.");
|
|
return;
|
|
}
|
|
if (
|
|
this._downloader &&
|
|
this._downloader._patch &&
|
|
this._downloader._patch.state == STATE_DOWNLOADING &&
|
|
this._downloader._update
|
|
) {
|
|
LOG(
|
|
"UpdateService:_attemptResume - _patch.state: " +
|
|
this._downloader._patch.state
|
|
);
|
|
let result = await this.#downloadUpdate(this._downloader._update);
|
|
LOG("UpdateService:_attemptResume - downloadUpdate result: " + result);
|
|
if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
|
|
LOG(
|
|
"UpdateService:_attemptResume - Resuming download failed. Cleaning " +
|
|
"up downloading update."
|
|
);
|
|
cleanupDownloadingUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Kick off an update check
|
|
(async () => {
|
|
let check = lazy.CheckSvc.internal.checkForUpdates(
|
|
lazy.CheckSvc.BACKGROUND_CHECK
|
|
);
|
|
await this.onCheckComplete(await check.result);
|
|
})();
|
|
}
|
|
|
|
/**
|
|
* Notified when a timer fires
|
|
* @param _timer
|
|
* The timer that fired
|
|
*/
|
|
async notify(_timer) {
|
|
await this._checkForBackgroundUpdates(true);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async checkForBackgroundUpdates() {
|
|
return this._checkForBackgroundUpdates(false);
|
|
}
|
|
|
|
// The suffix used for background update check telemetry histogram ID's.
|
|
get _pingSuffix() {
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
// Once an update has been downloaded, all later updates will be reported
|
|
// to telemetry as subsequent updates. We move the first update into
|
|
// readyUpdate as soon as the download is complete, so any update checks
|
|
// after readyUpdate is no longer null are subsequent update checks.
|
|
return AUSTLMY.SUBSEQUENT;
|
|
}
|
|
return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL;
|
|
}
|
|
|
|
/**
|
|
* Checks for updates in the background.
|
|
* @param isNotify
|
|
* Whether or not a background update check was initiated by the
|
|
* application update timer notification.
|
|
*/
|
|
async _checkForBackgroundUpdates(isNotify) {
|
|
await this.init();
|
|
|
|
if (!this.disabled && AppConstants.NIGHTLY_BUILD) {
|
|
// Scalar ID: update.suppress_prompts
|
|
AUSTLMY.pingSuppressPrompts();
|
|
}
|
|
if (this.disabled || this.manualUpdateOnly) {
|
|
// Return immediately if we are disabled by policy. Otherwise, just the
|
|
// telemetry we try to collect below can potentially trigger a restart
|
|
// prompt if the update directory isn't writable. And we shouldn't be
|
|
// telling the user about update failures if update is disabled.
|
|
// See Bug 1599590.
|
|
// Note that we exit unconditionally here if we are only doing manual
|
|
// update checks, because manual update checking uses a completely
|
|
// different code path (AppUpdater.sys.mjs creates its own nsIUpdateChecker),
|
|
// bypassing this function completely.
|
|
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.internal.readyUpdate &&
|
|
lazy.UM.internal.readyUpdate.selectedPatch &&
|
|
lazy.UM.internal.readyUpdate.selectedPatch.type == "complete"
|
|
) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
|
|
return false;
|
|
}
|
|
|
|
// If we start downloading an update while the readyUpdate is staging, we
|
|
// run the risk of eventually wanting to overwrite readyUpdate with the
|
|
// downloadingUpdate while the readyUpdate is still staging. Then we would
|
|
// have to have a weird intermediate state where the downloadingUpdate has
|
|
// finished downloading, but can't be moved yet. It's simpler to just not
|
|
// start a new update if the old one is still staging.
|
|
if (this.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
|
|
return false;
|
|
}
|
|
|
|
// Asynchronously kick off update checking
|
|
(async () => {
|
|
let validUpdateURL = true;
|
|
try {
|
|
await lazy.CheckSvc.getUpdateURL(lazy.CheckSvc.BACKGROUND_CHECK);
|
|
} catch (e) {
|
|
validUpdateURL = false;
|
|
}
|
|
|
|
// The following checks are done here so they can be differentiated from
|
|
// foreground checks.
|
|
if (!lazy.UpdateUtils.OSVersion) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
|
|
} else if (!lazy.UpdateUtils.ABI) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI);
|
|
} else if (!validUpdateURL) {
|
|
AUSTLMY.pingCheckCode(
|
|
this._pingSuffix,
|
|
AUSTLMY.CHK_INVALID_DEFAULT_URL
|
|
);
|
|
} else if (!hasUpdateMutex()) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
|
|
} else if (isOtherInstanceRunning()) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OTHER_INSTANCE);
|
|
} else if (!this.canCheckForUpdates) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
|
|
}
|
|
|
|
let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK);
|
|
await this.onCheckComplete(await check.result);
|
|
})();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determine the update from the specified updates that should be offered.
|
|
* If both valid major and minor updates are available the minor update will
|
|
* be offered.
|
|
* @param updates
|
|
* An array of available nsIUpdate items
|
|
* @return The nsIUpdate to offer.
|
|
*/
|
|
#selectUpdate(updates) {
|
|
if (!updates.length) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND);
|
|
return null;
|
|
}
|
|
|
|
// The ping for unsupported is sent after the call to showPrompt.
|
|
if (updates.length == 1 && updates[0].unsupported) {
|
|
return updates[0];
|
|
}
|
|
|
|
// Choose the newest of the available minor and major updates.
|
|
var majorUpdate = null;
|
|
var minorUpdate = null;
|
|
var vc = Services.vc;
|
|
let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND;
|
|
|
|
for (const update of updates) {
|
|
// Ignore updates for older versions of the application and updates for
|
|
// the same version of the application with the same build ID.
|
|
if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping update because the " +
|
|
"update's application version is not greater than the current " +
|
|
"application version"
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
|
|
continue;
|
|
}
|
|
|
|
if (updateIsAtLeastAsOldAsReadyUpdate(update)) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping update because the " +
|
|
"update's application version is not greater than that of the " +
|
|
"currently downloaded update"
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
|
|
continue;
|
|
}
|
|
|
|
if (lazy.UM.internal.readyUpdate && !getPatchOfType(update, "partial")) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping update because no partial " +
|
|
"patch is available and an update has already been downloaded."
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_NO_PARTIAL_PATCH;
|
|
continue;
|
|
}
|
|
|
|
switch (update.type) {
|
|
case "major":
|
|
if (!majorUpdate) {
|
|
majorUpdate = update;
|
|
} else if (
|
|
vc.compare(majorUpdate.appVersion, update.appVersion) <= 0
|
|
) {
|
|
majorUpdate = update;
|
|
}
|
|
break;
|
|
case "minor":
|
|
if (!minorUpdate) {
|
|
minorUpdate = update;
|
|
} else if (
|
|
vc.compare(minorUpdate.appVersion, update.appVersion) <= 0
|
|
) {
|
|
minorUpdate = update;
|
|
}
|
|
break;
|
|
default:
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping unknown update type: " +
|
|
update.type
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let update = minorUpdate || majorUpdate;
|
|
if (AppConstants.platform == "macosx" && update) {
|
|
if (getElevationRequired()) {
|
|
let installAttemptVersion = Services.prefs.getCharPref(
|
|
PREF_APP_UPDATE_ELEVATE_VERSION,
|
|
null
|
|
);
|
|
if (vc.compare(installAttemptVersion, update.appVersion) != 0) {
|
|
Services.prefs.setCharPref(
|
|
PREF_APP_UPDATE_ELEVATE_VERSION,
|
|
update.appVersion
|
|
);
|
|
if (
|
|
Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)
|
|
) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
|
|
}
|
|
} else {
|
|
let numCancels = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX,
|
|
0
|
|
);
|
|
let rejectedVersion = Services.prefs.getCharPref(
|
|
PREF_APP_UPDATE_ELEVATE_NEVER,
|
|
""
|
|
);
|
|
let maxCancels = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
|
|
DEFAULT_CANCELATIONS_OSX_MAX
|
|
);
|
|
if (numCancels >= maxCancels) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - the user requires elevation to " +
|
|
"install this update, but the user has exceeded the max " +
|
|
"number of elevation attempts."
|
|
);
|
|
update.elevationFailure = true;
|
|
AUSTLMY.pingCheckCode(
|
|
this._pingSuffix,
|
|
AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION
|
|
);
|
|
} else if (vc.compare(rejectedVersion, update.appVersion) == 0) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - the user requires elevation to " +
|
|
"install this update, but elevation is disabled for this " +
|
|
"version."
|
|
);
|
|
update.elevationFailure = true;
|
|
AUSTLMY.pingCheckCode(
|
|
this._pingSuffix,
|
|
AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION
|
|
);
|
|
} else {
|
|
LOG(
|
|
"UpdateService:selectUpdate - the user requires elevation to " +
|
|
"install the update."
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Clear elevation-related prefs since they no longer apply (the user
|
|
// may have gained write access to the Firefox directory or an update
|
|
// was executed with a different profile).
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
|
|
}
|
|
}
|
|
} else if (!update) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode);
|
|
}
|
|
|
|
return update;
|
|
}
|
|
|
|
/*
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async selectUpdate(updates) {
|
|
await this.init();
|
|
return this.#selectUpdate(updates);
|
|
}
|
|
|
|
/**
|
|
* Determine which of the specified updates should be installed and begin the
|
|
* download/installation process or notify the user about the update.
|
|
* @param updates
|
|
* An array of available updates
|
|
*/
|
|
async _selectAndInstallUpdate(updates) {
|
|
// Return early if there's an active update. The user is already aware and
|
|
// is downloading or performed some user action to prevent notification.
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE);
|
|
return;
|
|
}
|
|
|
|
if (this.disabled) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - not prompting because " +
|
|
"update is disabled"
|
|
);
|
|
return;
|
|
}
|
|
|
|
var update = this.#selectUpdate(updates);
|
|
if (!update || update.elevationFailure) {
|
|
return;
|
|
}
|
|
|
|
if (update.unsupported) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - update not supported for " +
|
|
"this system. Notifying observers. topic: update-available, " +
|
|
"status: unsupported"
|
|
);
|
|
Services.obs.notifyObservers(update, "update-available", "unsupported");
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
|
|
return;
|
|
}
|
|
|
|
if (!getCanApplyUpdates()) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - the user is unable to " +
|
|
"apply updates... prompting. Notifying observers. " +
|
|
"topic: update-available, status: cant-apply"
|
|
);
|
|
Services.obs.notifyObservers(null, "update-available", "cant-apply");
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* From this point on there are two possible outcomes:
|
|
* 1. download and install the update automatically
|
|
* 2. notify the user about the availability of an update
|
|
*
|
|
* Notes:
|
|
* a) if the app.update.auto preference is false then automatic download and
|
|
* install is disabled and the user will be notified.
|
|
*
|
|
* If the update when it is first read does not have an appVersion attribute
|
|
* the following deprecated behavior will occur:
|
|
* Update Type Outcome
|
|
* Major Notify
|
|
* Minor Auto Install
|
|
*/
|
|
let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled();
|
|
if (!updateAuto) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - prompting because silent " +
|
|
"install is disabled. Notifying observers. topic: update-available, " +
|
|
"status: show-prompt"
|
|
);
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
|
|
Services.obs.notifyObservers(update, "update-available", "show-prompt");
|
|
return;
|
|
}
|
|
|
|
LOG("UpdateService:_selectAndInstallUpdate - download the update");
|
|
let result = await this.#downloadUpdate(update);
|
|
if (
|
|
result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS &&
|
|
!this.isDownloading
|
|
) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - Failed to start downloading " +
|
|
"update. Cleaning up downloading update."
|
|
);
|
|
cleanupDownloadingUpdate();
|
|
}
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get isAppBaseDirWritable() {
|
|
return isAppBaseDirWritable();
|
|
}
|
|
|
|
get disabledForTesting() {
|
|
return lazy.UpdateServiceStub.updateDisabledForTesting;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get disabled() {
|
|
return lazy.UpdateServiceStub.updateDisabled;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get manualUpdateOnly() {
|
|
return (
|
|
Services.policies && !Services.policies.isAllowed("autoAppUpdateChecking")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyCheckForUpdates() {
|
|
if (this.disabled) {
|
|
LOG(
|
|
"UpdateService.canUsuallyCheckForUpdates - unable to automatically check " +
|
|
"for updates, the option has been disabled by the administrator."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// If we don't know the binary platform we're updating, we can't update.
|
|
if (!lazy.UpdateUtils.ABI) {
|
|
LOG(
|
|
"UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
|
|
"unknown ABI"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// If we don't know the OS version we're updating, we can't update.
|
|
if (!lazy.UpdateUtils.OSVersion) {
|
|
LOG(
|
|
"UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
|
|
"unknown OS version"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
LOG("UpdateService.canUsuallyCheckForUpdates - able to check for updates");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canCheckForUpdates() {
|
|
if (!this.canUsuallyCheckForUpdates) {
|
|
return false;
|
|
}
|
|
|
|
if (!hasUpdateMutex()) {
|
|
LOG(
|
|
"UpdateService.canCheckForUpdates - unable to check for updates, " +
|
|
"unable to acquire update mutex"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (isOtherInstanceRunning()) {
|
|
// This doesn't block update checks, but we will have to wait until either
|
|
// the other instance is gone or we time out waiting for it.
|
|
LOG(
|
|
"UpdateService.canCheckForUpdates - another instance is holding the " +
|
|
"lock, will need to wait for it prior to checking for updates"
|
|
);
|
|
}
|
|
|
|
LOG("UpdateService.canCheckForUpdates - able to check for updates");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get elevationRequired() {
|
|
return getElevationRequired();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyApplyUpdates() {
|
|
return getCanApplyUpdates();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canApplyUpdates() {
|
|
return (
|
|
this.canUsuallyApplyUpdates &&
|
|
hasUpdateMutex() &&
|
|
!isOtherInstanceRunning()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyStageUpdates() {
|
|
return getCanStageUpdates(false);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canStageUpdates() {
|
|
return getCanStageUpdates();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyUseBits() {
|
|
return getCanUseBits(false) == "CanUseBits";
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUseBits() {
|
|
return getCanUseBits() == "CanUseBits";
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get isOtherInstanceHandlingUpdates() {
|
|
return !hasUpdateMutex() || isOtherInstanceRunning();
|
|
}
|
|
|
|
/**
|
|
* A set of download listeners to be notified by this._downloader when it
|
|
* receives nsIRequestObserver or nsIProgressEventSink method calls.
|
|
*
|
|
* These are stored on the UpdateService rather than on the Downloader,
|
|
* because they ought to persist across multiple Downloader instances.
|
|
*/
|
|
_downloadListeners = new Set();
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
addDownloadListener(listener) {
|
|
let oldSize = this._downloadListeners.size;
|
|
this._downloadListeners.add(listener);
|
|
|
|
if (this._downloadListeners.size == oldSize) {
|
|
LOG(
|
|
"UpdateService:addDownloadListener - Warning: Didn't add duplicate " +
|
|
"listener"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this._downloader) {
|
|
this._downloader.onDownloadListenerAdded();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
removeDownloadListener(listener) {
|
|
let elementRemoved = this._downloadListeners.delete(listener);
|
|
|
|
if (!elementRemoved) {
|
|
LOG(
|
|
"UpdateService:removeDownloadListener - Warning: Didn't remove " +
|
|
"non-existent listener"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this._downloader) {
|
|
this._downloader.onDownloadListenerRemoved();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean indicating whether there are any download listeners
|
|
*/
|
|
get hasDownloadListeners() {
|
|
return !!this._downloadListeners.length;
|
|
}
|
|
|
|
/*
|
|
* Calls the provided function once with each download listener that is
|
|
* currently registered.
|
|
*/
|
|
forEachDownloadListener(fn) {
|
|
// Make a shallow copy in case listeners remove themselves.
|
|
let listeners = new Set(this._downloadListeners);
|
|
listeners.forEach(fn);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async downloadUpdate(update) {
|
|
await this.init();
|
|
return this.#downloadUpdate(update);
|
|
}
|
|
|
|
async #downloadUpdate(update) {
|
|
if (!update) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
|
|
}
|
|
|
|
// Don't download the update if the update's version is less than the
|
|
// current application's version or the update's version is the same as the
|
|
// application's version and the build ID is the same as the application's
|
|
// build ID. If we already have an update ready, we want to apply those
|
|
// same checks against the version of the ready update, so that we don't
|
|
// download an update that isn't newer than the one we already have.
|
|
if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
|
|
LOG(
|
|
"UpdateService:downloadUpdate - Skipping download of update since " +
|
|
"it is for an earlier or same application version and build ID.\n" +
|
|
"current application version: " +
|
|
Services.appinfo.version +
|
|
"\n" +
|
|
"update application version : " +
|
|
update.appVersion +
|
|
"\n" +
|
|
"current build ID: " +
|
|
Services.appinfo.appBuildID +
|
|
"\n" +
|
|
"update build ID : " +
|
|
update.buildID
|
|
);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
|
|
}
|
|
if (updateIsAtLeastAsOldAsReadyUpdate(update)) {
|
|
LOG(
|
|
"UpdateService:downloadUpdate - not downloading update because the " +
|
|
"update that's already been downloaded is the same version or " +
|
|
"newer.\n" +
|
|
"currently downloaded update application version: " +
|
|
lazy.UM.internal.readyUpdate.appVersion +
|
|
"\n" +
|
|
"available update application version : " +
|
|
update.appVersion +
|
|
"\n" +
|
|
"currently downloaded update build ID: " +
|
|
lazy.UM.internal.readyUpdate.buildID +
|
|
"\n" +
|
|
"available update build ID : " +
|
|
update.buildID
|
|
);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
|
|
}
|
|
|
|
// If a download request is in progress vs. a download ready to resume
|
|
if (this.isDownloading) {
|
|
if (update.isCompleteUpdate == this._downloader.isCompleteUpdate) {
|
|
LOG(
|
|
"UpdateService:downloadUpdate - no support for downloading more " +
|
|
"than one update at a time"
|
|
);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS;
|
|
}
|
|
this._downloader.cancel();
|
|
}
|
|
this._downloader = new Downloader(this);
|
|
return this._downloader.downloadUpdate(update);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async stopDownload() {
|
|
await this.init();
|
|
return this.#stopDownload();
|
|
}
|
|
|
|
async #stopDownload() {
|
|
if (this.isDownloading) {
|
|
await this._downloader.cancel();
|
|
} else if (this._retryTimer) {
|
|
// Download status is still considered as 'downloading' during retry.
|
|
// We need to cancel both retry and download at this stage.
|
|
this._retryTimer.cancel();
|
|
this._retryTimer = null;
|
|
if (this._downloader) {
|
|
await this._downloader.cancel();
|
|
}
|
|
}
|
|
if (this._downloader) {
|
|
await this._downloader.cleanup();
|
|
}
|
|
this._downloader = null;
|
|
}
|
|
|
|
/**
|
|
* Note that this is different from checking if `currentState` is
|
|
* `STATE_DOWNLOADING` because if we are downloading a second update, this
|
|
* will be `true` while `currentState` will be `STATE_PENDING`.
|
|
*/
|
|
get isDownloading() {
|
|
return this._downloader && this._downloader.isBusy;
|
|
}
|
|
|
|
_logStatus() {
|
|
if (!lazy.UpdateLog.enabled) {
|
|
return;
|
|
}
|
|
if (this.disabled) {
|
|
LOG("Current UpdateService status: disabled");
|
|
// Return early if UpdateService is disabled by policy. Otherwise some of
|
|
// the getters we call to display status information may discover that the
|
|
// update directory is not writable, which automatically results in the
|
|
// permissions being fixed. Which we shouldn't really be doing if update
|
|
// is disabled by policy.
|
|
return;
|
|
}
|
|
LOG("Logging current UpdateService status:");
|
|
// These getters print their own logging
|
|
this.canCheckForUpdates;
|
|
this.canApplyUpdates;
|
|
this.canStageUpdates;
|
|
LOG("Elevation required: " + this.elevationRequired);
|
|
LOG(
|
|
"Other instance of the application currently running: " +
|
|
this.isOtherInstanceHandlingUpdates
|
|
);
|
|
LOG("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([
|
|
Ci.nsIApplicationUpdateService,
|
|
Ci.nsITimerCallback,
|
|
Ci.nsIObserver,
|
|
]);
|
|
}
|
|
|
|
export class UpdateManager {
|
|
/**
|
|
* The nsIUpdate object for the update that has been downloaded.
|
|
*/
|
|
_readyUpdate = null;
|
|
|
|
/**
|
|
* The nsIUpdate object for the update currently being downloaded.
|
|
*/
|
|
_downloadingUpdate = null;
|
|
|
|
/**
|
|
* Whether the update history stored in _updates has changed since it was
|
|
* loaded.
|
|
*/
|
|
_updatesDirty = false;
|
|
|
|
/**
|
|
* The backing for `nsIUpdateManager.updateInstalledAtStartup`.
|
|
*/
|
|
#updateInstalledAtStartup = null;
|
|
|
|
/**
|
|
* A service to manage active and past updates.
|
|
* @constructor
|
|
*/
|
|
constructor() {
|
|
// 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}"`);
|
|
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;
|
|
} else if (status == STATE_SUCCEEDED && this._readyUpdate) {
|
|
this.#updateInstalledAtStartup = this._readyUpdate;
|
|
}
|
|
}
|
|
|
|
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
|
|
);
|
|
}
|
|
LOG(
|
|
"UpdateManager:UpdateManager - Initialized updateInstalledAtStartup to " +
|
|
this.#updateInstalledAtStartup
|
|
);
|
|
|
|
this.internal = {
|
|
reload: skipFiles => this.#reload(skipFiles),
|
|
getHistory: () => this.#getHistory(),
|
|
addUpdateToHistory: update => this.#addUpdateToHistory(update),
|
|
refreshUpdateStatus: async () => this.#refreshUpdateStatus(),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateManagerInternal]),
|
|
};
|
|
|
|
Object.defineProperty(this.internal, "readyUpdate", {
|
|
get: () => this._readyUpdate,
|
|
set: update => {
|
|
this._readyUpdate = update;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(this.internal, "downloadingUpdate", {
|
|
get: () => this._downloadingUpdate,
|
|
set: update => {
|
|
this._downloadingUpdate = update;
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* See `nsIUpdateManagerInternal.reload` in nsIUpdateService.idl
|
|
*/
|
|
#reload(skipFiles) {
|
|
if (!Cu.isInAutomation) {
|
|
return;
|
|
}
|
|
LOG("UpdateManager:#reload - Reloading update data.");
|
|
if (this._updatesXMLSaver) {
|
|
this._updatesXMLSaver.disarm();
|
|
}
|
|
|
|
let updates = [];
|
|
this._updatesDirty = true;
|
|
this._readyUpdate = null;
|
|
this._downloadingUpdate = null;
|
|
this.#updateInstalledAtStartup = null;
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
if (!skipFiles) {
|
|
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:#reload - 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);
|
|
}
|
|
if (status == STATE_SUCCEEDED && this._readyUpdate) {
|
|
this.#updateInstalledAtStartup = this._readyUpdate;
|
|
}
|
|
}
|
|
updates = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
|
|
}
|
|
this._updatesCache = updates;
|
|
|
|
LOG(
|
|
"UpdateManager:#reload - Reloaded downloadingUpdate as " +
|
|
this._downloadingUpdate
|
|
);
|
|
if (this._downloadingUpdate) {
|
|
LOG(
|
|
"UpdateManager:#reload - Reloaded downloadingUpdate state as " +
|
|
this._downloadingUpdate.state
|
|
);
|
|
}
|
|
LOG("UpdateManager:#reload - Reloaded readyUpdate as " + this._readyUpdate);
|
|
if (this._readyUpdate) {
|
|
LOG(
|
|
"UpdateManager:#reload - Reloaded readyUpdate state as " +
|
|
this._readyUpdate.state
|
|
);
|
|
}
|
|
LOG(
|
|
"UpdateManager:UpdateManager - Reloaded updateInstalledAtStartup as " +
|
|
this.#updateInstalledAtStartup
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Loads an updates.xml formatted file into an array of nsIUpdate items.
|
|
* @param fileName
|
|
* The file name in the updates directory to load.
|
|
* @return The array of nsIUpdate items held in the file.
|
|
*/
|
|
_loadXMLFileIntoArray(fileName) {
|
|
let updates = [];
|
|
let file = getUpdateFile([fileName]);
|
|
if (!file.exists()) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - XML file does not exist. " +
|
|
"path: " +
|
|
file.path
|
|
);
|
|
return updates;
|
|
}
|
|
|
|
// Open the active-update.xml file with both read and write access so
|
|
// opening it will fail if it isn't possible to also write to the file. When
|
|
// opening it fails it means that it isn't possible to update and the code
|
|
// below will return early without loading the active-update.xml. This will
|
|
// also make it so notifications to update manually will still be shown.
|
|
let mode =
|
|
fileName == FILE_ACTIVE_UPDATE_XML
|
|
? FileUtils.MODE_RDWR
|
|
: FileUtils.MODE_RDONLY;
|
|
let fileStream = Cc[
|
|
"@mozilla.org/network/file-input-stream;1"
|
|
].createInstance(Ci.nsIFileInputStream);
|
|
try {
|
|
fileStream.init(file, mode, FileUtils.PERMS_FILE, 0);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - error initializing file " +
|
|
"stream. Exception: " +
|
|
e
|
|
);
|
|
return updates;
|
|
}
|
|
try {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromStream(
|
|
fileStream,
|
|
"UTF-8",
|
|
fileStream.available(),
|
|
"text/xml"
|
|
);
|
|
|
|
var updateCount = doc.documentElement.childNodes.length;
|
|
for (var i = 0; i < updateCount; ++i) {
|
|
var updateElement = doc.documentElement.childNodes.item(i);
|
|
if (
|
|
updateElement.nodeType != updateElement.ELEMENT_NODE ||
|
|
updateElement.localName != "update"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let update;
|
|
try {
|
|
update = new Update(updateElement);
|
|
} catch (e) {
|
|
LOG("UpdateManager:_loadXMLFileIntoArray - invalid update");
|
|
continue;
|
|
}
|
|
updates.push(update);
|
|
}
|
|
} catch (ex) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - error constructing update " +
|
|
"list. Exception: " +
|
|
ex
|
|
);
|
|
}
|
|
fileStream.close();
|
|
if (!updates.length) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - update xml file " +
|
|
fileName +
|
|
" exists but doesn't contain any updates"
|
|
);
|
|
// The file exists but doesn't contain any updates so remove it.
|
|
try {
|
|
file.remove(false);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - error removing " +
|
|
fileName +
|
|
" file. Exception: " +
|
|
e
|
|
);
|
|
}
|
|
}
|
|
return updates;
|
|
}
|
|
|
|
#getHistory() {
|
|
const history = this._getUpdates();
|
|
return [...history];
|
|
}
|
|
|
|
/**
|
|
* Loads the update history from the updates.xml file into a cache.
|
|
*/
|
|
_getUpdates() {
|
|
if (!this._updatesCache) {
|
|
this._updatesCache = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
|
|
}
|
|
return this._updatesCache;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getHistory() {
|
|
await lazy.AUS.init();
|
|
return lazy.UM.internal.getHistory();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getReadyUpdate() {
|
|
await lazy.AUS.init();
|
|
return this._readyUpdate;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getDownloadingUpdate() {
|
|
await lazy.AUS.init();
|
|
return this._downloadingUpdate;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get updateInstalledAtStartup() {
|
|
return this.#updateInstalledAtStartup;
|
|
}
|
|
|
|
#addUpdateToHistory(aUpdate) {
|
|
this._updatesDirty = true;
|
|
let updates = this._getUpdates();
|
|
updates.unshift(aUpdate);
|
|
// Limit the update history to 10 updates.
|
|
updates.splice(10);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async addUpdateToHistory(aUpdate) {
|
|
await lazy.AUS.init();
|
|
this.#addUpdateToHistory(aUpdate);
|
|
}
|
|
|
|
/**
|
|
* Serializes an array of updates to an XML file or removes the file if the
|
|
* array length is 0.
|
|
* @param updates
|
|
* An array of nsIUpdate objects
|
|
* @param fileName
|
|
* The file name in the updates directory to write to.
|
|
* @return true on success, false on error
|
|
*/
|
|
async _writeUpdatesToXMLFile(updates, fileName) {
|
|
let file;
|
|
try {
|
|
file = getUpdateFile([fileName]);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_writeUpdatesToXMLFile - Unable to get XML file - " +
|
|
"Exception: " +
|
|
e
|
|
);
|
|
return false;
|
|
}
|
|
if (!updates.length) {
|
|
LOG(
|
|
"UpdateManager:_writeUpdatesToXMLFile - no updates to write. " +
|
|
"removing file: " +
|
|
file.path
|
|
);
|
|
try {
|
|
await IOUtils.remove(file.path);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_writeUpdatesToXMLFile - Delete file exception: " + e
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const EMPTY_UPDATES_DOCUMENT_OPEN =
|
|
'<?xml version="1.0"?><updates xmlns="' + URI_UPDATE_NS + '">';
|
|
const EMPTY_UPDATES_DOCUMENT_CLOSE = "</updates>";
|
|
try {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromString(
|
|
EMPTY_UPDATES_DOCUMENT_OPEN + EMPTY_UPDATES_DOCUMENT_CLOSE,
|
|
"text/xml"
|
|
);
|
|
|
|
for (var i = 0; i < updates.length; ++i) {
|
|
doc.documentElement.appendChild(updates[i].serialize(doc));
|
|
}
|
|
|
|
var xml =
|
|
EMPTY_UPDATES_DOCUMENT_OPEN +
|
|
doc.documentElement.innerHTML +
|
|
EMPTY_UPDATES_DOCUMENT_CLOSE;
|
|
// If the destination file existed and is removed while the following is
|
|
// being performed the copy of the tmp file to the destination file will
|
|
// fail.
|
|
await IOUtils.writeUTF8(file.path, xml, {
|
|
tmpPath: file.path + ".tmp",
|
|
});
|
|
await IOUtils.setPermissions(file.path, FileUtils.PERMS_FILE);
|
|
} catch (e) {
|
|
LOG("UpdateManager:_writeUpdatesToXMLFile - Exception: " + e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_updatesXMLSaver = null;
|
|
_updatesXMLSaverCallback = null;
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
saveUpdates() {
|
|
if (!this._updatesXMLSaver) {
|
|
this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize();
|
|
|
|
this._updatesXMLSaver = new lazy.DeferredTask(
|
|
() => this._saveUpdatesXML(),
|
|
XML_SAVER_INTERVAL_MS
|
|
);
|
|
lazy.AsyncShutdown.profileBeforeChange.addBlocker(
|
|
"UpdateManager: writing update xml data",
|
|
this._updatesXMLSaverCallback
|
|
);
|
|
} else {
|
|
this._updatesXMLSaver.disarm();
|
|
}
|
|
|
|
this._updatesXMLSaver.arm();
|
|
}
|
|
|
|
/**
|
|
* Saves the active-updates.xml and updates.xml when the updates history has
|
|
* been modified files.
|
|
*/
|
|
_saveUpdatesXML() {
|
|
// This mechanism for how we store the updates might seem a bit odd, since,
|
|
// if only one update is stored, we don't know if it's the ready update or
|
|
// the downloading update. However, we can determine which it is by reading
|
|
// update.status. If we read STATE_DOWNLOADING, it must be a downloading
|
|
// update and otherwise it's a ready update. This method has the additional
|
|
// advantage of requiring no migration from when we used to only store a
|
|
// single active update.
|
|
let updates = [];
|
|
if (this._readyUpdate) {
|
|
updates.push(this._readyUpdate);
|
|
}
|
|
if (this._downloadingUpdate) {
|
|
updates.push(this._downloadingUpdate);
|
|
}
|
|
|
|
// The active update stored in the active-update.xml file will change during
|
|
// the lifetime of an active update and the file should always be updated
|
|
// when saveUpdates is called.
|
|
let promises = [];
|
|
promises[0] = this._writeUpdatesToXMLFile(updates, FILE_ACTIVE_UPDATE_XML);
|
|
// The update history stored in the updates.xml file should only need to be
|
|
// updated when an active update has been added to it in which case
|
|
// |_updatesDirty| will be true.
|
|
if (this._updatesDirty) {
|
|
this._updatesDirty = false;
|
|
promises[1] = this._writeUpdatesToXMLFile(
|
|
this._getUpdates(),
|
|
FILE_UPDATES_XML
|
|
);
|
|
}
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async refreshUpdateStatus() {
|
|
return this.#refreshUpdateStatus();
|
|
}
|
|
|
|
async #refreshUpdateStatus() {
|
|
try {
|
|
LOG("UpdateManager:refreshUpdateStatus - Staging done.");
|
|
|
|
await lazy.AUS.init();
|
|
|
|
var update = this._readyUpdate;
|
|
if (!update) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Missing ready update?");
|
|
return;
|
|
}
|
|
|
|
var status = readStatusFile(getReadyUpdateDir());
|
|
pingStateAndStatusCodes(update, false, status);
|
|
LOG(`UpdateManager:refreshUpdateStatus - status = ${status}`);
|
|
|
|
let parts = status.split(":");
|
|
update.state = parts[0];
|
|
if (update.state == STATE_APPLYING) {
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - Staging appears to have crashed."
|
|
);
|
|
update.state = STATE_FAILED;
|
|
update.errorCode = ERR_UPDATER_CRASHED;
|
|
} else if (update.state == STATE_FAILED) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Staging failed.");
|
|
if (parts[1]) {
|
|
update.errorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
|
|
} else {
|
|
update.errorCode = INVALID_UPDATER_STATUS_CODE;
|
|
}
|
|
}
|
|
|
|
// Rotate the update logs so the update log isn't removed if a complete
|
|
// update is downloaded. By passing false the patch directory won't be
|
|
// removed.
|
|
cleanUpReadyUpdateDir(false);
|
|
|
|
if (update.state == STATE_FAILED) {
|
|
let isMemError = isMemoryAllocationErrorCode(update.errorCode);
|
|
if (
|
|
update.errorCode == DELETE_ERROR_STAGING_LOCK_FILE ||
|
|
update.errorCode == UNEXPECTED_STAGING_ERROR ||
|
|
isMemError
|
|
) {
|
|
update.state = getBestPendingState();
|
|
writeStatusFile(getReadyUpdateDir(), update.state);
|
|
if (isMemError) {
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Updater failed to ` +
|
|
`allocate enough memory to successfully stage. Setting ` +
|
|
`status to "${update.state}"`
|
|
);
|
|
} else {
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Unexpected staging error. ` +
|
|
`Setting status to "${update.state}"`
|
|
);
|
|
}
|
|
} else if (isServiceSpecificErrorCode(update.errorCode)) {
|
|
// Sometimes when staging, we might encounter an error that is
|
|
// specific to the Maintenance Service. If this happens, we should try
|
|
// to update without the Service.
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Encountered service ` +
|
|
`specific error code: ${update.errorCode}. Will try installing ` +
|
|
`update without the Maintenance Service. Setting state to pending`
|
|
);
|
|
update.state = STATE_PENDING;
|
|
writeStatusFile(getReadyUpdateDir(), update.state);
|
|
} else {
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - Attempting handleUpdateFailure"
|
|
);
|
|
if (!handleUpdateFailure(update)) {
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - handleUpdateFailure " +
|
|
"failed. Attempting to fall back to complete update."
|
|
);
|
|
await handleFallbackToCompleteUpdate();
|
|
}
|
|
}
|
|
}
|
|
if (update.state == STATE_APPLIED && shouldUseService()) {
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Staging successful. ` +
|
|
`Setting status to "${STATE_APPLIED_SERVICE}"`
|
|
);
|
|
writeStatusFile(
|
|
getReadyUpdateDir(),
|
|
(update.state = STATE_APPLIED_SERVICE)
|
|
);
|
|
}
|
|
|
|
// Now that the active update's properties have been updated write the
|
|
// active-update.xml to disk. Since there have been no changes to the
|
|
// update history the updates.xml will not be written to disk.
|
|
this.saveUpdates();
|
|
|
|
// Send an observer notification which the app update doorhanger uses to
|
|
// display a restart notification after any langpacks have staged.
|
|
await promiseLangPacksUpdated(update);
|
|
|
|
if (
|
|
update.state == STATE_APPLIED ||
|
|
update.state == STATE_APPLIED_SERVICE ||
|
|
update.state == STATE_PENDING ||
|
|
update.state == STATE_PENDING_SERVICE ||
|
|
update.state == STATE_PENDING_ELEVATE
|
|
) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_PENDING");
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
}
|
|
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - Notifying observers that " +
|
|
"the update was staged. topic: update-staged, status: " +
|
|
update.state
|
|
);
|
|
Services.obs.notifyObservers(update, "update-staged", update.state);
|
|
} finally {
|
|
// This function being called is the one thing that tells us that staging
|
|
// is done so be very sure that we don't exit it leaving the current
|
|
// state at STATE_STAGING.
|
|
// The only cases where we haven't already done a state transition are
|
|
// error cases, so if another state isn't set, assume that we hit an error
|
|
// and aborted the update.
|
|
if (
|
|
lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING
|
|
) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_IDLE");
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
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
|
|
*/
|
|
async cleanupDownloadingUpdate() {
|
|
LOG(
|
|
"UpdateManager:cleanupDownloadingUpdate - cleaning up downloading update."
|
|
);
|
|
await lazy.AUS.init();
|
|
cleanupDownloadingUpdate();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async cleanupReadyUpdate() {
|
|
LOG("UpdateManager:cleanupReadyUpdate - cleaning up ready update.");
|
|
await lazy.AUS.init();
|
|
cleanupReadyUpdate();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async cleanupActiveUpdates() {
|
|
LOG("UpdateManager:cleanupActiveUpdates - cleaning up active updates.");
|
|
await lazy.AUS.init();
|
|
cleanupActiveUpdates();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async doInstallCleanup() {
|
|
LOG("UpdateManager:doInstallCleanup - cleaning up");
|
|
await lazy.AUS.init();
|
|
|
|
let completionPromises = [];
|
|
|
|
const delete_or_log = path =>
|
|
IOUtils.remove(path).catch(ex =>
|
|
console.error(`Failed to delete ${path}`, ex)
|
|
);
|
|
|
|
for (const key of [KEY_OLD_UPDROOT, KEY_UPDROOT]) {
|
|
const root = Services.dirsvc.get(key, Ci.nsIFile);
|
|
|
|
const activeUpdateXml = root.clone();
|
|
activeUpdateXml.append(FILE_ACTIVE_UPDATE_XML);
|
|
completionPromises.push(delete_or_log(activeUpdateXml.path));
|
|
|
|
const downloadingMar = root.clone();
|
|
downloadingMar.append(DIR_UPDATES);
|
|
downloadingMar.append(DIR_UPDATE_DOWNLOADING);
|
|
downloadingMar.append(FILE_UPDATE_MAR);
|
|
completionPromises.push(delete_or_log(downloadingMar.path));
|
|
|
|
const readyDir = root.clone();
|
|
readyDir.append(DIR_UPDATES);
|
|
readyDir.append(DIR_UPDATE_READY);
|
|
const readyMar = readyDir.clone();
|
|
readyMar.append(FILE_UPDATE_MAR);
|
|
completionPromises.push(delete_or_log(readyMar.path));
|
|
const readyStatus = readyDir.clone();
|
|
readyStatus.append(FILE_UPDATE_STATUS);
|
|
completionPromises.push(delete_or_log(readyStatus.path));
|
|
const versionFile = readyDir.clone();
|
|
versionFile.append(FILE_UPDATE_VERSION);
|
|
completionPromises.push(delete_or_log(versionFile.path));
|
|
}
|
|
|
|
return Promise.allSettled(completionPromises);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async doUninstallCleanup() {
|
|
LOG("UpdateManager:doUninstallCleanup - cleaning up.");
|
|
await lazy.AUS.init();
|
|
let completionPromises = [];
|
|
|
|
completionPromises.push(
|
|
IOUtils.remove(Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile).path, {
|
|
recursive: true,
|
|
}).catch(ex => console.error("Failed to remove update directory", ex))
|
|
);
|
|
completionPromises.push(
|
|
IOUtils.remove(Services.dirsvc.get(KEY_OLD_UPDROOT, Ci.nsIFile).path, {
|
|
recursive: true,
|
|
}).catch(ex => console.error("Failed to remove old update directory", ex))
|
|
);
|
|
|
|
return Promise.allSettled(completionPromises);
|
|
}
|
|
|
|
classID = Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}");
|
|
QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateManager]);
|
|
}
|
|
|
|
/**
|
|
* CheckerService
|
|
* Provides an interface for checking for new updates. When more checks are
|
|
* made while an equivalent check is already in-progress, they will be coalesced
|
|
* into a single update check request.
|
|
*/
|
|
export class CheckerService {
|
|
#nextUpdateCheckId = 1;
|
|
|
|
// Most of the update checking data is looked up via a "request key". This
|
|
// allows us to lookup the request key for a particular check id, since
|
|
// multiple checks can correspond to a single request.
|
|
// When a check is cancelled or completed, it will be removed from this
|
|
// object.
|
|
#requestKeyByCheckId = {};
|
|
|
|
// This object will relate request keys to update check data objects. The
|
|
// format of the update check data objects is defined by
|
|
// #makeUpdateCheckDataObject, below.
|
|
// When an update request is cancelled (by all of the corresponding update
|
|
// checks being cancelled) or completed, its key will be removed from this
|
|
// object.
|
|
#updateCheckData = {};
|
|
|
|
constructor() {
|
|
this.internal = {
|
|
checkForUpdates: checkType => this.#checkForUpdates(checkType, true),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckerInternal]),
|
|
};
|
|
}
|
|
|
|
#makeUpdateCheckDataObject(type, promise) {
|
|
return { type, promise, request: null };
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the passed parameter is one of the valid enumerated
|
|
* values that indicates a type of update check.
|
|
*/
|
|
#validUpdateCheckType(checkType) {
|
|
return [
|
|
Ci.nsIUpdateChecker.BACKGROUND_CHECK,
|
|
Ci.nsIUpdateChecker.FOREGROUND_CHECK,
|
|
].includes(checkType);
|
|
}
|
|
|
|
#getCanMigrate() {
|
|
if (AppConstants.platform != "win") {
|
|
return false;
|
|
}
|
|
|
|
// The first element of the array is whether the build target is 32 or 64
|
|
// bit and the third element of the array is whether the client's Windows OS
|
|
// system processor is 32 or 64 bit.
|
|
let aryABI = lazy.UpdateUtils.ABI.split("-");
|
|
if (aryABI[0] != "x86" || aryABI[2] != "x64") {
|
|
return false;
|
|
}
|
|
|
|
let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
|
Ci.nsIWindowsRegKey
|
|
);
|
|
|
|
let regPath =
|
|
"SOFTWARE\\Mozilla\\" + Services.appinfo.name + "\\32to64DidMigrate";
|
|
let regValHKCU = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_CURRENT_USER,
|
|
regPath,
|
|
"Never",
|
|
wrk.WOW64_32
|
|
);
|
|
let regValHKLM = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_LOCAL_MACHINE,
|
|
regPath,
|
|
"Never",
|
|
wrk.WOW64_32
|
|
);
|
|
// The Never registry key value allows configuring a system to never migrate
|
|
// any of the installations.
|
|
if (regValHKCU === 1 || regValHKLM === 1) {
|
|
LOG(
|
|
"CheckerService:#getCanMigrate - all installations should not be " +
|
|
"migrated"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
let appBaseDirPath = getAppBaseDir().path;
|
|
regValHKCU = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_CURRENT_USER,
|
|
regPath,
|
|
appBaseDirPath,
|
|
wrk.WOW64_32
|
|
);
|
|
regValHKLM = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_LOCAL_MACHINE,
|
|
regPath,
|
|
appBaseDirPath,
|
|
wrk.WOW64_32
|
|
);
|
|
// When the registry value is 1 for the installation directory path value
|
|
// name then the installation has already been migrated once or the system
|
|
// was configured to not migrate that installation.
|
|
if (regValHKCU === 1 || regValHKLM === 1) {
|
|
LOG(
|
|
"CheckerService:#getCanMigrate - this installation should not be " +
|
|
"migrated"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// When the registry value is 0 for the installation directory path value
|
|
// name then the installation has updated to Firefox 56 and can be migrated.
|
|
if (regValHKCU === 0 || regValHKLM === 0) {
|
|
LOG("CheckerService:#getCanMigrate - this installation can be migrated");
|
|
return true;
|
|
}
|
|
|
|
LOG(
|
|
"CheckerService:#getCanMigrate - no registry entries for this " +
|
|
"installation"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getUpdateURL(checkType) {
|
|
LOG("CheckerService:getUpdateURL - checkType: " + checkType);
|
|
if (!this.#validUpdateCheckType(checkType)) {
|
|
LOG("CheckerService:getUpdateURL - Invalid checkType");
|
|
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let url = Services.appinfo.updateURL;
|
|
let updatePin;
|
|
|
|
if (Services.policies) {
|
|
let policies = Services.policies.getActivePolicies();
|
|
if (policies) {
|
|
if ("AppUpdateURL" in policies) {
|
|
url = policies.AppUpdateURL.toString();
|
|
}
|
|
if ("AppUpdatePin" in policies) {
|
|
updatePin = policies.AppUpdatePin;
|
|
|
|
// Scalar ID: update.version_pin
|
|
AUSTLMY.pingPinPolicy(updatePin);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!url) {
|
|
LOG("CheckerService:getUpdateURL - update URL not defined");
|
|
return null;
|
|
}
|
|
|
|
url = await lazy.UpdateUtils.formatUpdateURL(url);
|
|
|
|
if (checkType == Ci.nsIUpdateChecker.FOREGROUND_CHECK) {
|
|
url += (url.includes("?") ? "&" : "?") + "force=1";
|
|
}
|
|
|
|
if (this.#getCanMigrate()) {
|
|
url += (url.includes("?") ? "&" : "?") + "mig64=1";
|
|
}
|
|
|
|
if (updatePin) {
|
|
url +=
|
|
(url.includes("?") ? "&" : "?") +
|
|
"pin=" +
|
|
encodeURIComponent(updatePin);
|
|
}
|
|
|
|
LOG("CheckerService:getUpdateURL - update URL: " + url);
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
checkForUpdates(checkType) {
|
|
return this.#checkForUpdates(checkType, false);
|
|
}
|
|
|
|
#checkForUpdates(checkType, internal) {
|
|
// Note that we should run update initialization if it was invoked
|
|
// externally (i.e. `internal == false`). But initialization is async and
|
|
// the asynchronous work of this function happens in `this.#updateCheck`, so
|
|
// we will delay initialization until then.
|
|
|
|
LOG("CheckerService:checkForUpdates - checkType: " + checkType);
|
|
if (!this.#validUpdateCheckType(checkType)) {
|
|
LOG("CheckerService:checkForUpdates - Invalid checkType");
|
|
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let checkId = this.#nextUpdateCheckId;
|
|
this.#nextUpdateCheckId += 1;
|
|
|
|
// `checkType == FOREGROUND_CHECK` can override `canCheckForUpdates`. But
|
|
// nothing should override enterprise policies.
|
|
if (lazy.AUS.disabled) {
|
|
LOG("CheckerService:checkForUpdates - disabled by policy");
|
|
return this.#getChecksNotAllowedObject(checkId);
|
|
}
|
|
if (
|
|
checkType == Ci.nsIUpdateChecker.BACKGROUND_CHECK &&
|
|
!lazy.AUS.canCheckForUpdates
|
|
) {
|
|
LOG("CheckerService:checkForUpdates - !canCheckForUpdates");
|
|
return this.#getChecksNotAllowedObject(checkId);
|
|
}
|
|
|
|
// We want to combine simultaneous requests, but only ones that are
|
|
// equivalent. If, say, one of them uses the force parameter and one
|
|
// doesn't, we want those two requests to remain separate. This key will
|
|
// allow us to map equivalent requests together. It is also the key that we
|
|
// use to lookup the update check data in this.#updateCheckData.
|
|
let requestKey = checkType;
|
|
|
|
if (requestKey in this.#updateCheckData) {
|
|
LOG(
|
|
`CheckerService:checkForUpdates - Connecting check id ${checkId} to ` +
|
|
`existing check request.`
|
|
);
|
|
} else {
|
|
LOG(
|
|
`CheckerService:checkForUpdates - Making new check request for check ` +
|
|
`id ${checkId}.`
|
|
);
|
|
this.#updateCheckData[requestKey] = this.#makeUpdateCheckDataObject(
|
|
checkType,
|
|
this.#updateCheck(checkType, requestKey, internal)
|
|
);
|
|
}
|
|
|
|
this.#requestKeyByCheckId[checkId] = requestKey;
|
|
|
|
return {
|
|
id: checkId,
|
|
result: this.#updateCheckData[requestKey].promise,
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]),
|
|
};
|
|
}
|
|
|
|
#getChecksNotAllowedObject(checkId) {
|
|
return {
|
|
id: checkId,
|
|
result: Promise.resolve(
|
|
Object.freeze({
|
|
checksAllowed: false,
|
|
succeeded: false,
|
|
request: null,
|
|
updates: [],
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
|
|
})
|
|
),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]),
|
|
};
|
|
}
|
|
|
|
async #updateCheck(checkType, requestKey, internal) {
|
|
if (!internal) {
|
|
await lazy.AUS.init();
|
|
}
|
|
|
|
await waitForOtherInstances();
|
|
|
|
let url;
|
|
try {
|
|
url = await this.getUpdateURL(checkType);
|
|
} catch (ex) {}
|
|
|
|
if (!url) {
|
|
LOG("CheckerService:#updateCheck - !url");
|
|
return this.#getCheckFailedObject("update_url_not_available");
|
|
}
|
|
|
|
let request = new XMLHttpRequest();
|
|
request.open("GET", url, true);
|
|
// Prevent the request from reading from the cache.
|
|
request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
|
|
// Prevent the request from writing to the cache.
|
|
request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
|
|
// Disable cutting edge features, like TLS 1.3, where middleboxes might
|
|
// brick us
|
|
request.channel.QueryInterface(
|
|
Ci.nsIHttpChannelInternal
|
|
).beConservative = true;
|
|
|
|
request.overrideMimeType("text/xml");
|
|
// The Cache-Control header is only interpreted by proxies and the
|
|
// final destination. It does not help if a resource is already
|
|
// cached locally.
|
|
request.setRequestHeader("Cache-Control", "no-cache");
|
|
// HTTP/1.0 servers might not implement Cache-Control and
|
|
// might only implement Pragma: no-cache
|
|
request.setRequestHeader("Pragma", "no-cache");
|
|
|
|
const UPDATE_CHECK_LOAD_SUCCESS = 1;
|
|
const UPDATE_CHECK_LOAD_ERROR = 2;
|
|
const UPDATE_CHECK_CANCELLED = 3;
|
|
|
|
let result = await new Promise(resolve => {
|
|
// It's important that nothing potentially asynchronous happens between
|
|
// checking if the request has been cancelled and starting the request.
|
|
// If an update check cancellation happens before dispatching the request
|
|
// and we end up dispatching it anyways, we will never call cancel on the
|
|
// request later and the cancellation effectively won't happen.
|
|
if (!(requestKey in this.#updateCheckData)) {
|
|
LOG(
|
|
"CheckerService:#updateCheck - check was cancelled before request " +
|
|
"was able to start"
|
|
);
|
|
resolve(UPDATE_CHECK_CANCELLED);
|
|
return;
|
|
}
|
|
|
|
let onLoad = _event => {
|
|
request.removeEventListener("load", onLoad);
|
|
LOG("CheckerService:#updateCheck - request got 'load' event");
|
|
resolve(UPDATE_CHECK_LOAD_SUCCESS);
|
|
};
|
|
request.addEventListener("load", onLoad);
|
|
let onError = _event => {
|
|
request.removeEventListener("error", onLoad);
|
|
LOG("CheckerService:#updateCheck - request got 'error' event");
|
|
resolve(UPDATE_CHECK_LOAD_ERROR);
|
|
};
|
|
request.addEventListener("error", onError);
|
|
|
|
LOG("CheckerService:#updateCheck - sending request to: " + url);
|
|
request.send(null);
|
|
this.#updateCheckData[requestKey].request = request;
|
|
});
|
|
|
|
// Remove all entries for this request key. This marks the request and the
|
|
// associated check ids as no longer in-progress.
|
|
delete this.#updateCheckData[requestKey];
|
|
for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
|
|
if (this.#requestKeyByCheckId[checkId] == requestKey) {
|
|
delete this.#requestKeyByCheckId[checkId];
|
|
}
|
|
}
|
|
|
|
if (result == UPDATE_CHECK_CANCELLED) {
|
|
return this.#getCheckFailedObject(Cr.NS_BINDING_ABORTED);
|
|
}
|
|
|
|
if (result == UPDATE_CHECK_LOAD_ERROR) {
|
|
let status = this.#getChannelStatus(request);
|
|
LOG("CheckerService:#updateCheck - Failed. request.status: " + status);
|
|
|
|
// Set MitM pref.
|
|
try {
|
|
let secInfo = request.channel.securityInfo;
|
|
if (secInfo.serverCert && secInfo.serverCert.issuerName) {
|
|
Services.prefs.setStringPref(
|
|
"security.pki.mitm_canary_issuer",
|
|
secInfo.serverCert.issuerName
|
|
);
|
|
}
|
|
} catch (e) {
|
|
LOG("CheckerService:#updateCheck - Getting secInfo failed.");
|
|
}
|
|
|
|
return this.#getCheckFailedObject(status, 404, request);
|
|
}
|
|
|
|
LOG("CheckerService:#updateCheck - request completed downloading document");
|
|
Services.prefs.clearUserPref("security.pki.mitm_canary_issuer");
|
|
// Check whether there is a mitm, i.e. check whether the root cert is
|
|
// built-in or not.
|
|
try {
|
|
let sslStatus = request.channel.securityInfo;
|
|
if (sslStatus) {
|
|
Services.prefs.setBoolPref(
|
|
"security.pki.mitm_detected",
|
|
!sslStatus.isBuiltCertChainRootBuiltInRoot
|
|
);
|
|
}
|
|
} catch (e) {
|
|
LOG("CheckerService:#updateCheck - Getting sslStatus failed.");
|
|
}
|
|
|
|
let updates;
|
|
try {
|
|
// Analyze the resulting DOM and determine the set of updates.
|
|
updates = this.#parseUpdates(request);
|
|
} catch (e) {
|
|
LOG(
|
|
"CheckerService:#updateCheck - there was a problem checking for " +
|
|
"updates. Exception: " +
|
|
e
|
|
);
|
|
let status = this.#getChannelStatus(request);
|
|
// If we can't find an error string specific to this status code,
|
|
// just use the 200 message from above, which means everything
|
|
// "looks" fine but there was probably an XML error or a bogus file.
|
|
return this.#getCheckFailedObject(status, 200, request);
|
|
}
|
|
|
|
LOG(
|
|
"CheckerService:#updateCheck - number of updates available: " +
|
|
updates.length
|
|
);
|
|
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
|
|
}
|
|
|
|
return Object.freeze({
|
|
checksAllowed: true,
|
|
succeeded: true,
|
|
request,
|
|
updates,
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param errorCode
|
|
* The error code to include in the return value. If possible, we
|
|
* will get the update status text based on this error code.
|
|
* @param defaultCode
|
|
* Optional. The error code to use to get the status text if there
|
|
* isn't status text available for `errorCode`.
|
|
* @param request
|
|
* The XMLHttpRequest used to check for updates. Or null, if one was
|
|
* never constructed.
|
|
* @returns An nsIUpdateCheckResult object indicating an error, using the
|
|
* error data passed to this function.
|
|
*/
|
|
#getCheckFailedObject(
|
|
errorCode,
|
|
defaultCode = Cr.NS_BINDING_FAILED,
|
|
request = null
|
|
) {
|
|
let update = new Update(null);
|
|
update.errorCode = errorCode;
|
|
update.statusText = getStatusTextFromCode(errorCode, defaultCode);
|
|
|
|
if (errorCode == Cr.NS_ERROR_OFFLINE) {
|
|
// We use a separate constant here because nsIUpdate.errorCode is signed
|
|
update.errorCode = NETWORK_ERROR_OFFLINE;
|
|
} else if (this.#isHttpStatusCode(errorCode)) {
|
|
update.errorCode = HTTP_ERROR_OFFSET + errorCode;
|
|
}
|
|
|
|
return Object.freeze({
|
|
checksAllowed: true,
|
|
succeeded: false,
|
|
request,
|
|
updates: [update],
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the status code for the XMLHttpRequest
|
|
*/
|
|
#getChannelStatus(request) {
|
|
var status = 0;
|
|
try {
|
|
status = request.status;
|
|
} catch (e) {}
|
|
|
|
if (status == 0) {
|
|
status = request.channel.QueryInterface(Ci.nsIRequest).status;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
#isHttpStatusCode(status) {
|
|
return status >= 100 && status <= 599;
|
|
}
|
|
|
|
/**
|
|
* @param request
|
|
* The XMLHttpRequest that successfully loaded the update XML.
|
|
* @returns An array of 0 or more nsIUpdate objects describing the available
|
|
* updates.
|
|
* @throws If the XML document element node name is not updates.
|
|
*/
|
|
#parseUpdates(request) {
|
|
let updatesElement = request.responseXML.documentElement;
|
|
if (!updatesElement) {
|
|
LOG("CheckerService:#parseUpdates - empty updates document?!");
|
|
return [];
|
|
}
|
|
|
|
if (updatesElement.nodeName != "updates") {
|
|
LOG("CheckerService:#parseUpdates - unexpected node name!");
|
|
throw new Error(
|
|
"Unexpected node name, expected: updates, got: " +
|
|
updatesElement.nodeName
|
|
);
|
|
}
|
|
|
|
let updates = [];
|
|
for (const updateElement of updatesElement.childNodes) {
|
|
if (
|
|
updateElement.nodeType != updateElement.ELEMENT_NODE ||
|
|
updateElement.localName != "update"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let update;
|
|
try {
|
|
update = new Update(updateElement);
|
|
} catch (e) {
|
|
LOG("CheckerService:#parseUpdates - invalid <update/>, ignoring...");
|
|
continue;
|
|
}
|
|
update.serviceURL = request.responseURL;
|
|
update.channel = lazy.UpdateUtils.UpdateChannel;
|
|
updates.push(update);
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
stopCheck(checkId) {
|
|
if (!(checkId in this.#requestKeyByCheckId)) {
|
|
LOG(`CheckerService:stopCheck - Non-existent check id ${checkId}`);
|
|
return;
|
|
}
|
|
LOG(`CheckerService:stopCheck - Cancelling check id ${checkId}`);
|
|
let requestKey = this.#requestKeyByCheckId[checkId];
|
|
delete this.#requestKeyByCheckId[checkId];
|
|
if (Object.values(this.#requestKeyByCheckId).includes(requestKey)) {
|
|
LOG(
|
|
`CheckerService:stopCheck - Not actually cancelling request because ` +
|
|
`other check id's depend on it.`
|
|
);
|
|
} else {
|
|
LOG(
|
|
`CheckerService:stopCheck - This is the last check using this ` +
|
|
`request. Cancelling the request now.`
|
|
);
|
|
let request = this.#updateCheckData[requestKey].request;
|
|
delete this.#updateCheckData[requestKey];
|
|
if (request) {
|
|
LOG(`CheckerService:stopCheck - Aborting XMLHttpRequest`);
|
|
request.abort();
|
|
} else {
|
|
LOG(
|
|
`CheckerService:stopCheck - Not aborting XMLHttpRequest. It ` +
|
|
`doesn't appear to have started yet.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
stopAllChecks() {
|
|
LOG("CheckerService:stopAllChecks - stopping all checks.");
|
|
for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
|
|
this.stopCheck(checkId);
|
|
}
|
|
}
|
|
|
|
classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}");
|
|
QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateChecker]);
|
|
}
|
|
|
|
class Downloader {
|
|
/**
|
|
* The nsIUpdatePatch that we are downloading
|
|
*/
|
|
_patch = null;
|
|
|
|
/**
|
|
* The nsIUpdate that we are downloading
|
|
*/
|
|
_update = null;
|
|
|
|
/**
|
|
* The nsIRequest object handling the download.
|
|
*/
|
|
_request = null;
|
|
|
|
/**
|
|
* Whether or not the update being downloaded is a complete replacement of
|
|
* the user's existing installation or a patch representing the difference
|
|
* between the new version and the previous version.
|
|
*/
|
|
isCompleteUpdate = null;
|
|
|
|
/**
|
|
* We get the nsIRequest from nsIBITS asynchronously. When downloadUpdate has
|
|
* been called, but this._request is not yet valid, _pendingRequest will be
|
|
* a promise that will resolve when this._request has been set.
|
|
*/
|
|
_pendingRequest = null;
|
|
|
|
/**
|
|
* When using BITS, cancel actions happen asynchronously. This variable
|
|
* keeps track of any cancel action that is in-progress.
|
|
* If the cancel action fails, this will be set back to null so that the
|
|
* action can be attempted again. But if the cancel action succeeds, the
|
|
* resolved promise will remain stored in this variable to prevent cancel
|
|
* from being called twice (which, for BITS, is an error).
|
|
*/
|
|
_cancelPromise = null;
|
|
|
|
/**
|
|
* BITS receives progress notifications slowly, unless a user is watching.
|
|
* This tracks what frequency notifications are happening at.
|
|
*
|
|
* This is needed because BITS downloads are started asynchronously.
|
|
* Specifically, this is needed to prevent a situation where the download is
|
|
* still starting (Downloader._pendingRequest has not resolved) when the first
|
|
* observer registers itself. Without this variable, there is no way of
|
|
* knowing whether the download was started as Active or Idle and, therefore,
|
|
* we don't know if we need to start Active mode when _pendingRequest
|
|
* resolves.
|
|
*/
|
|
_bitsActiveNotifications = false;
|
|
|
|
/**
|
|
* This is a function that when called will stop the update process from
|
|
* waiting for language pack updates. This is for safety to ensure that a
|
|
* problem in the add-ons manager doesn't delay updates by much.
|
|
*/
|
|
_langPackTimeout = null;
|
|
|
|
/**
|
|
* If gOnlyDownloadUpdatesThisSession is true, we prevent the update process
|
|
* from progressing past the downloading stage. If the download finishes,
|
|
* pretend that it hasn't in order to keep the current update in the
|
|
* "downloading" state.
|
|
*/
|
|
_pretendingDownloadIsNotDone = false;
|
|
|
|
/**
|
|
* Manages the download of updates
|
|
* @param background
|
|
* Whether or not this downloader is operating in background
|
|
* update mode.
|
|
* @param updateService
|
|
* The update service that created this downloader.
|
|
* @constructor
|
|
*/
|
|
constructor(updateService) {
|
|
LOG("Creating Downloader");
|
|
this.updateService = updateService;
|
|
}
|
|
|
|
/**
|
|
* Cancels the active download.
|
|
*
|
|
* For a BITS download, this will cancel and remove the download job. For
|
|
* an nsIIncrementalDownload, this will stop the download, but leaves the
|
|
* data around to allow the transfer to be resumed later.
|
|
*/
|
|
async cancel(cancelError) {
|
|
LOG("Downloader: cancel");
|
|
if (cancelError === undefined) {
|
|
cancelError = Cr.NS_BINDING_ABORTED;
|
|
}
|
|
if (this.usingBits) {
|
|
// If a cancel action is already in progress, just return when that
|
|
// promise resolved. Trying to cancel the same request twice is an error.
|
|
if (this._cancelPromise) {
|
|
await this._cancelPromise;
|
|
return;
|
|
}
|
|
|
|
if (this._pendingRequest) {
|
|
await this._pendingRequest;
|
|
}
|
|
if (this._patch.getProperty("bitsId") != null) {
|
|
// Make sure that we don't try to resume this download after it was
|
|
// cancelled.
|
|
this._patch.deleteProperty("bitsId");
|
|
}
|
|
try {
|
|
this._cancelPromise = this._request.cancelAsync(cancelError);
|
|
await this._cancelPromise;
|
|
} catch (e) {
|
|
// On success, we will not set the cancel promise to null because
|
|
// we want to prevent two cancellations of the same request. But
|
|
// retrying after a failed cancel is not an error, so we will set the
|
|
// cancel promise to null in the failure case.
|
|
this._cancelPromise = null;
|
|
throw e;
|
|
}
|
|
} else if (this._request && this._request instanceof Ci.nsIRequest) {
|
|
// Normally, cancelling an nsIIncrementalDownload results in it stopping
|
|
// the download but leaving the downloaded data so that we can resume the
|
|
// download later. If we've already finished the download, there is no
|
|
// transfer to stop.
|
|
// Note that this differs from the BITS case. Cancelling a BITS job, even
|
|
// when the transfer has completed, results in all data being deleted.
|
|
// Therefore, even if the transfer has completed, cancelling a BITS job
|
|
// has effects that we must not skip.
|
|
if (this._pretendingDownloadIsNotDone) {
|
|
LOG(
|
|
"Downloader: cancel - Ignoring cancel request of finished download"
|
|
);
|
|
} else {
|
|
this._request.cancel(cancelError);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Verify the downloaded file. We assume that the download is complete at
|
|
* this point.
|
|
*/
|
|
_verifyDownload() {
|
|
LOG("Downloader:_verifyDownload called");
|
|
if (!this._request) {
|
|
AUSTLMY.pingDownloadCode(
|
|
this.isCompleteUpdate,
|
|
AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST
|
|
);
|
|
return false;
|
|
}
|
|
|
|
let destination = getDownloadingUpdateDir();
|
|
destination.append(FILE_UPDATE_MAR);
|
|
|
|
// Ensure that the file size matches the expected file size.
|
|
if (destination.fileSize != this._patch.size) {
|
|
LOG("Downloader:_verifyDownload downloaded size != expected size.");
|
|
AUSTLMY.pingDownloadCode(
|
|
this.isCompleteUpdate,
|
|
AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL
|
|
);
|
|
return false;
|
|
}
|
|
|
|
LOG("Downloader:_verifyDownload downloaded size == expected size.");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Select the patch to use given the current state of updateDir and the given
|
|
* set of update patches.
|
|
* @param update
|
|
* A nsIUpdate object to select a patch from
|
|
* @return A nsIUpdatePatch object to download
|
|
*/
|
|
_selectPatch(update) {
|
|
// Given an update to download, we will always try to download the patch
|
|
// for a partial update over the patch for a full update.
|
|
|
|
// Look to see if any of the patches in the Update object has been
|
|
// pre-selected for download, otherwise we must figure out which one
|
|
// to select ourselves.
|
|
var selectedPatch = update.selectedPatch;
|
|
|
|
var state = selectedPatch ? selectedPatch.state : STATE_NONE;
|
|
|
|
// If this is a patch that we know about, then select it. If it is a patch
|
|
// that we do not know about, then remove it and use our default logic.
|
|
var useComplete = false;
|
|
if (selectedPatch) {
|
|
LOG(
|
|
"Downloader:_selectPatch - found existing patch with state: " + state
|
|
);
|
|
if (state == STATE_DOWNLOADING) {
|
|
LOG("Downloader:_selectPatch - resuming download");
|
|
return selectedPatch;
|
|
}
|
|
if (
|
|
state == STATE_PENDING ||
|
|
state == STATE_PENDING_SERVICE ||
|
|
state == STATE_PENDING_ELEVATE ||
|
|
state == STATE_APPLIED ||
|
|
state == STATE_APPLIED_SERVICE
|
|
) {
|
|
LOG("Downloader:_selectPatch - already downloaded");
|
|
return null;
|
|
}
|
|
|
|
// When downloading the patch failed using BITS, there hasn't been an
|
|
// attempt to download the patch using the internal application download
|
|
// mechanism, and an attempt to stage or apply the patch hasn't failed
|
|
// which indicates that a different patch should be downloaded since
|
|
// re-downloading the same patch with the internal application download
|
|
// mechanism will likely also fail when trying to stage or apply it then
|
|
// try to download the same patch using the internal application download
|
|
// mechanism.
|
|
selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
if (
|
|
selectedPatch.getProperty("bitsResult") != null &&
|
|
selectedPatch.getProperty("internalResult") == null &&
|
|
!selectedPatch.errorCode
|
|
) {
|
|
LOG(
|
|
"Downloader:_selectPatch - Falling back to non-BITS download " +
|
|
"mechanism for the same patch due to existing BITS result: " +
|
|
selectedPatch.getProperty("bitsResult")
|
|
);
|
|
return selectedPatch;
|
|
}
|
|
|
|
if (update && selectedPatch.type == "complete") {
|
|
// This is a pretty fatal error. Just bail.
|
|
LOG("Downloader:_selectPatch - failed to apply complete patch!");
|
|
cleanupDownloadingUpdate();
|
|
return null;
|
|
}
|
|
|
|
// Something went wrong when we tried to apply the previous patch.
|
|
// Try the complete patch next time.
|
|
useComplete = true;
|
|
selectedPatch = null;
|
|
}
|
|
|
|
// If we were not able to discover an update from a previous download, we
|
|
// select the best patch from the given set.
|
|
var partialPatch = getPatchOfType(update, "partial");
|
|
if (!useComplete) {
|
|
selectedPatch = partialPatch;
|
|
}
|
|
if (!selectedPatch) {
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
// If we already have a ready update, we download partials only.
|
|
LOG(
|
|
"Downloader:_selectPatch - not selecting a complete patch because " +
|
|
"this is not the first download of the session"
|
|
);
|
|
return null;
|
|
}
|
|
|
|
if (partialPatch) {
|
|
partialPatch.selected = false;
|
|
}
|
|
selectedPatch = getPatchOfType(update, "complete");
|
|
}
|
|
|
|
// if update only contains a partial patch, selectedPatch == null here if
|
|
// the partial patch has been attempted and fails and we're trying to get a
|
|
// complete patch
|
|
if (selectedPatch) {
|
|
selectedPatch.selected = true;
|
|
update.isCompleteUpdate = selectedPatch.type == "complete";
|
|
}
|
|
|
|
LOG(
|
|
"Downloader:_selectPatch - Patch selected. Assigning update to " +
|
|
"downloadingUpdate."
|
|
);
|
|
lazy.UM.internal.downloadingUpdate = update;
|
|
|
|
return selectedPatch;
|
|
}
|
|
|
|
/**
|
|
* Whether or not the user wants to be notified that an update is being
|
|
* downloaded.
|
|
*/
|
|
get _notifyDuringDownload() {
|
|
return Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD,
|
|
false
|
|
);
|
|
}
|
|
|
|
_notifyDownloadStatusObservers() {
|
|
if (this._notifyDuringDownload) {
|
|
let status = this.updateService.isDownloading ? "downloading" : "idle";
|
|
Services.obs.notifyObservers(this._update, "update-downloading", status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether or not we are currently downloading something.
|
|
*/
|
|
get isBusy() {
|
|
return this._request != null || this._pendingRequest != null;
|
|
}
|
|
|
|
get usingBits() {
|
|
return this._pendingRequest != null || this._request instanceof BitsRequest;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the specified patch can be downloaded with BITS.
|
|
*/
|
|
_canUseBits(patch) {
|
|
if (getCanUseBits() != "CanUseBits") {
|
|
// This will have printed its own logging. No need to print more.
|
|
return false;
|
|
}
|
|
// Regardless of success or failure, don't download the same patch with BITS
|
|
// twice.
|
|
if (patch.getProperty("bitsResult") != null) {
|
|
LOG(
|
|
"Downloader:_canUseBits - Not using BITS because it was already tried"
|
|
);
|
|
return false;
|
|
}
|
|
LOG("Downloader:_canUseBits - Patch is able to use BITS download");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Instruct the add-ons manager to start downloading language pack updates in
|
|
* preparation for the current update.
|
|
*/
|
|
_startLangPackUpdates() {
|
|
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, false)) {
|
|
return;
|
|
}
|
|
|
|
// A promise that we can resolve at some point to time out the language pack
|
|
// update process.
|
|
let timeoutPromise = new Promise(resolve => {
|
|
this._langPackTimeout = resolve;
|
|
});
|
|
|
|
let update = unwrap(this._update);
|
|
|
|
let existing = LangPackUpdates.get(update);
|
|
if (existing) {
|
|
// We have already started staging lang packs for this update, no need to
|
|
// do it again.
|
|
return;
|
|
}
|
|
|
|
// Note that we don't care about success or failure here, either way we will
|
|
// continue with the update process.
|
|
let langPackPromise = lazy.AddonManager.stageLangpacksForAppUpdate(
|
|
update.appVersion,
|
|
update.appVersion
|
|
)
|
|
.catch(error => {
|
|
LOG(
|
|
`Add-ons manager threw exception while updating language packs: ${error}`
|
|
);
|
|
})
|
|
.finally(() => {
|
|
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.
|
|
*/
|
|
async downloadUpdate(update) {
|
|
LOG("Downloader:downloadUpdate");
|
|
if (!update) {
|
|
AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE);
|
|
throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
|
|
}
|
|
|
|
var updateDir = getDownloadingUpdateDir();
|
|
|
|
this._update = update;
|
|
|
|
// This function may return null, which indicates that there are no patches
|
|
// to download.
|
|
this._patch = this._selectPatch(update);
|
|
if (!this._patch) {
|
|
LOG("Downloader:downloadUpdate - no patch to download");
|
|
AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
|
|
}
|
|
// The update and the patch implement nsIWritablePropertyBag. Expose that
|
|
// interface immediately after a patch is assigned so that
|
|
// this.(_patch|_update).(get|set)Property can always safely be called.
|
|
this._update.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
this._patch.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
|
|
if (
|
|
this._update.getProperty("disableBackgroundUpdates") != null &&
|
|
lazy.gIsBackgroundTaskMode
|
|
) {
|
|
LOG(
|
|
"Downloader:downloadUpdate - Background update disabled by update " +
|
|
"advertisement"
|
|
);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
|
|
}
|
|
|
|
this.isCompleteUpdate = this._patch.type == "complete";
|
|
|
|
let canUseBits = false;
|
|
// Allow the advertised update to disable BITS.
|
|
if (this._update.getProperty("disableBITS") != null) {
|
|
LOG(
|
|
"Downloader:downloadUpdate - BITS downloads disabled by update " +
|
|
"advertisement"
|
|
);
|
|
} else {
|
|
canUseBits = this._canUseBits(this._patch);
|
|
}
|
|
|
|
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 Ci.nsIApplicationUpdateService
|
|
.DOWNLOAD_FAILURE_CANNOT_RESUME_IN_BACKGROUND;
|
|
}
|
|
|
|
// The interval is 0 since there is no need to throttle downloads.
|
|
let interval = 0;
|
|
|
|
LOG(
|
|
"Downloader:downloadUpdate - Starting nsIIncrementalDownload with " +
|
|
"url: " +
|
|
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, []);
|
|
try {
|
|
updateRootDir.create(
|
|
Ci.nsIFile.DIRECTORY_TYPE,
|
|
FileUtils.PERMS_DIRECTORY
|
|
);
|
|
} catch (ex) {
|
|
if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
|
|
throw ex;
|
|
}
|
|
// Ignore the exception due to a directory that already exists.
|
|
}
|
|
|
|
let jobName = "MozillaUpdate " + updateRootDir.leafName;
|
|
let updatePath = 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.internal.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 Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS;
|
|
}
|
|
|
|
/**
|
|
* This is run when a download listener is added.
|
|
*/
|
|
onDownloadListenerAdded() {
|
|
// Increase the status update frequency when someone starts listening
|
|
this._maybeStartActiveNotifications();
|
|
}
|
|
|
|
/**
|
|
* This is run when a download listener is removed.
|
|
*/
|
|
onDownloadListenerRemoved() {
|
|
// Decrease the status update frequency when no one is listening
|
|
if (!this.hasDownloadListeners) {
|
|
this._maybeStopActiveNotifications();
|
|
}
|
|
}
|
|
|
|
get hasDownloadListeners() {
|
|
return this.updateService.hasDownloadListeners;
|
|
}
|
|
|
|
/**
|
|
* This speeds up BITS progress notifications in response to a user watching
|
|
* the notifications.
|
|
*/
|
|
async _maybeStartActiveNotifications() {
|
|
if (
|
|
this.usingBits &&
|
|
!this._bitsActiveNotifications &&
|
|
this.hasDownloadListeners &&
|
|
this._request
|
|
) {
|
|
LOG(
|
|
"Downloader:_maybeStartActiveNotifications - Starting active " +
|
|
"notifications"
|
|
);
|
|
this._bitsActiveNotifications = true;
|
|
await Promise.all([
|
|
this._request
|
|
.setNoProgressTimeout(BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS)
|
|
.catch(error => {
|
|
LOG(
|
|
"Downloader:_maybeStartActiveNotifications - Failed to set " +
|
|
"no progress timeout. Error: " +
|
|
error
|
|
);
|
|
}),
|
|
this._request
|
|
.changeMonitorInterval(BITS_ACTIVE_POLL_RATE_MS)
|
|
.catch(error => {
|
|
LOG(
|
|
"Downloader:_maybeStartActiveNotifications - Failed to increase " +
|
|
"status update frequency. Error: " +
|
|
error
|
|
);
|
|
}),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This slows down BITS progress notifications in response to a user no longer
|
|
* watching the notifications.
|
|
*/
|
|
async _maybeStopActiveNotifications() {
|
|
if (
|
|
this.usingBits &&
|
|
this._bitsActiveNotifications &&
|
|
!this.hasDownloadListeners &&
|
|
this._request
|
|
) {
|
|
LOG(
|
|
"Downloader:_maybeStopActiveNotifications - Stopping active " +
|
|
"notifications"
|
|
);
|
|
this._bitsActiveNotifications = false;
|
|
await Promise.all([
|
|
this._request
|
|
.setNoProgressTimeout(BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS)
|
|
.catch(error => {
|
|
LOG(
|
|
"Downloader:_maybeStopActiveNotifications - Failed to set " +
|
|
"no progress timeout: " +
|
|
error
|
|
);
|
|
}),
|
|
this._request
|
|
.changeMonitorInterval(BITS_IDLE_POLL_RATE_MS)
|
|
.catch(error => {
|
|
LOG(
|
|
"Downloader:_maybeStopActiveNotifications - Failed to decrease " +
|
|
"status update frequency: " +
|
|
error
|
|
);
|
|
}),
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* When the async request begins
|
|
* @param request
|
|
* The nsIRequest object for the transfer
|
|
*/
|
|
onStartRequest(request) {
|
|
if (this.usingBits) {
|
|
LOG("Downloader:onStartRequest");
|
|
} else {
|
|
LOG(
|
|
"Downloader:onStartRequest - original URI spec: " +
|
|
request.URI.spec +
|
|
", final URI spec: " +
|
|
request.finalURI.spec
|
|
);
|
|
// Set finalURL in onStartRequest if it is different.
|
|
if (this._patch.finalURL != request.finalURI.spec) {
|
|
this._patch.finalURL = request.finalURI.spec;
|
|
lazy.UM.saveUpdates();
|
|
}
|
|
}
|
|
|
|
this.updateService.forEachDownloadListener(listener => {
|
|
listener.onStartRequest(request);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When new data has been downloaded
|
|
* @param request
|
|
* The nsIRequest object for the transfer
|
|
* @param progress
|
|
* The current number of bytes transferred
|
|
* @param maxProgress
|
|
* The total number of bytes that must be transferred
|
|
*/
|
|
onProgress(request, progress, maxProgress) {
|
|
LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress);
|
|
|
|
if (progress > this._patch.size) {
|
|
LOG(
|
|
"Downloader:onProgress - progress: " +
|
|
progress +
|
|
" is higher than patch size: " +
|
|
this._patch.size
|
|
);
|
|
AUSTLMY.pingDownloadCode(
|
|
this.isCompleteUpdate,
|
|
AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER
|
|
);
|
|
this.cancel(Cr.NS_ERROR_UNEXPECTED);
|
|
return;
|
|
}
|
|
|
|
// Wait until the transfer has started (progress > 0) to verify maxProgress
|
|
// so that we don't check it before it is available (in which case, -1 would
|
|
// have been passed).
|
|
if (progress > 0 && maxProgress != this._patch.size) {
|
|
LOG(
|
|
"Downloader:onProgress - maxProgress: " +
|
|
maxProgress +
|
|
" is not equal to expected patch size: " +
|
|
this._patch.size
|
|
);
|
|
AUSTLMY.pingDownloadCode(
|
|
this.isCompleteUpdate,
|
|
AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL
|
|
);
|
|
this.cancel(Cr.NS_ERROR_UNEXPECTED);
|
|
return;
|
|
}
|
|
|
|
this.updateService.forEachDownloadListener(listener => {
|
|
if (listener instanceof Ci.nsIProgressEventSink) {
|
|
listener.onProgress(request, progress, maxProgress);
|
|
}
|
|
});
|
|
this.updateService._consecutiveSocketErrors = 0;
|
|
}
|
|
|
|
/**
|
|
* When we have new status text
|
|
* @param request
|
|
* The nsIRequest object for the transfer
|
|
* @param status
|
|
* A status code
|
|
* @param statusText
|
|
* Human readable version of |status|
|
|
*/
|
|
onStatus(request, status, statusText) {
|
|
LOG(
|
|
"Downloader:onStatus - status: " + status + ", statusText: " + statusText
|
|
);
|
|
|
|
this.updateService.forEachDownloadListener(listener => {
|
|
if (listener instanceof Ci.nsIProgressEventSink) {
|
|
listener.onStatus(request, status, statusText);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* When data transfer ceases
|
|
* @param request
|
|
* The nsIRequest object for the transfer
|
|
* @param status
|
|
* Status code containing the reason for the cessation.
|
|
*/
|
|
/* eslint-disable-next-line complexity */
|
|
async onStopRequest(request, status) {
|
|
if (gOnlyDownloadUpdatesThisSession) {
|
|
LOG(
|
|
"Downloader:onStopRequest - End of update download detected and " +
|
|
"ignored because we are restricted to update downloads this " +
|
|
"session. We will continue with this update next session."
|
|
);
|
|
// In order to keep the update from progressing past the downloading
|
|
// stage, we will pretend that the download is still going.
|
|
// A lot of this work is done for us by just not setting this._request to
|
|
// null, which usually signals that the transfer has completed.
|
|
this._pretendingDownloadIsNotDone = true;
|
|
// This notification is currently used only for testing.
|
|
Services.obs.notifyObservers(null, "update-download-restriction-hit");
|
|
return;
|
|
}
|
|
|
|
if (!this.usingBits) {
|
|
LOG(
|
|
"Downloader:onStopRequest - downloader: nsIIncrementalDownload, " +
|
|
"original URI spec: " +
|
|
request.URI.spec +
|
|
", final URI spec: " +
|
|
request.finalURI.spec +
|
|
", status: " +
|
|
status
|
|
);
|
|
} else {
|
|
LOG("Downloader:onStopRequest - downloader: BITS, status: " + status);
|
|
}
|
|
|
|
let bitsCompletionError;
|
|
if (this.usingBits) {
|
|
if (Components.isSuccessCode(status)) {
|
|
try {
|
|
await request.complete();
|
|
} catch (e) {
|
|
LOG(
|
|
"Downloader:onStopRequest - Unable to complete BITS download: " + e
|
|
);
|
|
status = Cr.NS_ERROR_FAILURE;
|
|
bitsCompletionError = e;
|
|
}
|
|
} else {
|
|
// BITS jobs that failed to complete should still have cancel called on
|
|
// them to remove the job.
|
|
try {
|
|
await this.cancel();
|
|
} catch (e) {
|
|
// This will fail if the job stopped because it was cancelled.
|
|
// Even if this is a "real" error, there isn't really anything to do
|
|
// about it, and it's not really a big problem. It just means that the
|
|
// BITS job will stay around until it is removed automatically
|
|
// (default of 90 days).
|
|
}
|
|
}
|
|
}
|
|
|
|
var state = this._patch.state;
|
|
var shouldShowPrompt = false;
|
|
var shouldRegisterOnlineObserver = false;
|
|
var shouldRetrySoon = false;
|
|
var deleteActiveUpdate = false;
|
|
let migratedToReadyUpdate = false;
|
|
let nonDownloadFailure = false;
|
|
var retryTimeout = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT,
|
|
DEFAULT_SOCKET_RETRYTIMEOUT
|
|
);
|
|
// Prevent the preference from setting a value greater than 10000.
|
|
retryTimeout = Math.min(retryTimeout, 10000);
|
|
var maxFail = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_SOCKET_MAXERRORS,
|
|
DEFAULT_SOCKET_MAX_ERRORS
|
|
);
|
|
// Prevent the preference from setting a value greater than 20.
|
|
maxFail = Math.min(maxFail, 20);
|
|
LOG(
|
|
"Downloader:onStopRequest - status: " +
|
|
status +
|
|
", " +
|
|
"current fail: " +
|
|
this.updateService._consecutiveSocketErrors +
|
|
", " +
|
|
"max fail: " +
|
|
maxFail +
|
|
", " +
|
|
"retryTimeout: " +
|
|
retryTimeout
|
|
);
|
|
if (Components.isSuccessCode(status)) {
|
|
if (this._verifyDownload()) {
|
|
AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS);
|
|
|
|
LOG(
|
|
"Downloader:onStopRequest - Clearing readyUpdate in preparation of " +
|
|
"moving downloadingUpdate into readyUpdate."
|
|
);
|
|
|
|
// Clear out any old update before we notify anyone about the new one.
|
|
// It will be invalid in a moment anyways when we call
|
|
// `cleanUpReadyUpdateDir()`.
|
|
lazy.UM.internal.readyUpdate = null;
|
|
|
|
// We're about to clobber the ready update so we can replace it with the
|
|
// downloading update that just finished. We need to let observers know
|
|
// about this.
|
|
if (
|
|
lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_PENDING
|
|
) {
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_SWAP);
|
|
}
|
|
Services.obs.notifyObservers(this._update, "update-swap");
|
|
|
|
// Swap the downloading update into the ready update directory.
|
|
cleanUpReadyUpdateDir();
|
|
let downloadedMar = getDownloadingUpdateDir();
|
|
downloadedMar.append(FILE_UPDATE_MAR);
|
|
let readyDir = getReadyUpdateDir();
|
|
try {
|
|
downloadedMar.moveTo(readyDir, FILE_UPDATE_MAR);
|
|
migratedToReadyUpdate = true;
|
|
} catch (e) {
|
|
migratedToReadyUpdate = false;
|
|
}
|
|
|
|
if (migratedToReadyUpdate) {
|
|
AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_SUCCESS);
|
|
state = getBestPendingState();
|
|
shouldShowPrompt = !getCanStageUpdates();
|
|
|
|
// Tell the updater.exe we're ready to apply.
|
|
LOG(
|
|
`Downloader:onStopRequest - Ready to apply. Setting state to ` +
|
|
`"${state}".`
|
|
);
|
|
writeStatusFile(getReadyUpdateDir(), state);
|
|
writeVersionFile(getReadyUpdateDir(), this._update.appVersion);
|
|
this._update.installDate = new Date().getTime();
|
|
this._update.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("installPending");
|
|
Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS, 0);
|
|
} else {
|
|
LOG(
|
|
"Downloader:onStopRequest - failed to move the downloading " +
|
|
"update to the ready update directory."
|
|
);
|
|
AUSTLMY.pingMoveResult(AUSTLMY.MOVE_RESULT_UNKNOWN_FAILURE);
|
|
|
|
state = STATE_DOWNLOAD_FAILED;
|
|
status = Cr.NS_ERROR_FILE_COPY_OR_MOVE_FAILED;
|
|
|
|
const mfCode = "move_failed";
|
|
let message = getStatusTextFromCode(mfCode, mfCode);
|
|
this._update.statusText = message;
|
|
|
|
nonDownloadFailure = true;
|
|
deleteActiveUpdate = true;
|
|
|
|
cleanUpDownloadingUpdateDir();
|
|
}
|
|
} else {
|
|
LOG("Downloader:onStopRequest - download verification failed");
|
|
state = STATE_DOWNLOAD_FAILED;
|
|
status = Cr.NS_ERROR_CORRUPTED_CONTENT;
|
|
|
|
// Yes, this code is a string.
|
|
const vfCode = "verification_failed";
|
|
var message = getStatusTextFromCode(vfCode, vfCode);
|
|
this._update.statusText = message;
|
|
|
|
if (this._update.isCompleteUpdate || this._update.patchCount != 2) {
|
|
LOG("Downloader:onStopRequest - No alternative patch to try");
|
|
deleteActiveUpdate = true;
|
|
}
|
|
|
|
// Destroy the updates directory, since we're done with it.
|
|
cleanUpDownloadingUpdateDir();
|
|
}
|
|
} else if (status == Cr.NS_ERROR_OFFLINE) {
|
|
// Register an online observer to try again.
|
|
// The online observer will continue the incremental download by
|
|
// calling downloadUpdate on the active update which continues
|
|
// downloading the file from where it was.
|
|
LOG("Downloader:onStopRequest - offline, register online observer: true");
|
|
AUSTLMY.pingDownloadCode(
|
|
this.isCompleteUpdate,
|
|
AUSTLMY.DWNLD_RETRY_OFFLINE
|
|
);
|
|
shouldRegisterOnlineObserver = true;
|
|
deleteActiveUpdate = false;
|
|
|
|
// 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.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
|
|
lazy.UM.internal.downloadingUpdate = null;
|
|
} else if (
|
|
lazy.UM.internal.downloadingUpdate &&
|
|
lazy.UM.internal.downloadingUpdate.state != state
|
|
) {
|
|
lazy.UM.internal.downloadingUpdate.state = state;
|
|
}
|
|
if (migratedToReadyUpdate) {
|
|
LOG(
|
|
"Downloader:onStopRequest - Moving downloadingUpdate into readyUpdate"
|
|
);
|
|
lazy.UM.internal.readyUpdate = lazy.UM.internal.downloadingUpdate;
|
|
lazy.UM.internal.downloadingUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
// Only notify listeners about the stopped state if we
|
|
// aren't handling an internal retry.
|
|
if (!shouldRetrySoon && !shouldRegisterOnlineObserver) {
|
|
this.updateService.forEachDownloadListener(listener => {
|
|
listener.onStopRequest(request, status);
|
|
});
|
|
}
|
|
|
|
this._request = null;
|
|
|
|
// This notification must happen after _request is set to null so that
|
|
// the correct this.updateService.isDownloading value is available in
|
|
// _notifyDownloadStatusObservers().
|
|
this._notifyDownloadStatusObservers();
|
|
|
|
if (state == STATE_DOWNLOAD_FAILED) {
|
|
var allFailed = true;
|
|
// Don't bother retrying the download if we got an error that isn't
|
|
// download related.
|
|
if (!nonDownloadFailure) {
|
|
// If we haven't already, attempt to download without BITS
|
|
if (request instanceof BitsRequest) {
|
|
LOG(
|
|
"Downloader:onStopRequest - BITS download failed. Falling back " +
|
|
"to nsIIncrementalDownload"
|
|
);
|
|
let result = await this.downloadUpdate(this._update);
|
|
if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
|
|
LOG(
|
|
"Downloader:onStopRequest - Failed to fall back to " +
|
|
"nsIIncrementalDownload. Cleaning up downloading update."
|
|
);
|
|
cleanupDownloadingUpdate();
|
|
} else {
|
|
allFailed = false;
|
|
}
|
|
}
|
|
|
|
// Check if there is a complete update patch that can be downloaded.
|
|
if (
|
|
allFailed &&
|
|
!this._update.isCompleteUpdate &&
|
|
this._update.patchCount == 2
|
|
) {
|
|
LOG(
|
|
"Downloader:onStopRequest - verification of patch failed, " +
|
|
"downloading complete update patch"
|
|
);
|
|
this._update.isCompleteUpdate = true;
|
|
let result = await this.downloadUpdate(this._update);
|
|
|
|
if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
|
|
LOG(
|
|
"Downloader:onStopRequest - Failed to fall back to complete " +
|
|
"patch. Cleaning up downloading update."
|
|
);
|
|
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.
|
|
*/
|
|
async cleanup() {
|
|
if (this.usingBits) {
|
|
if (this._pendingRequest) {
|
|
await this._pendingRequest;
|
|
}
|
|
this._request.shutdown();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIInterfaceRequestor.idl
|
|
*/
|
|
getInterface(iid) {
|
|
// The network request may require proxy authentication, so provide the
|
|
// default nsIAuthPrompt if requested.
|
|
if (iid.equals(Ci.nsIAuthPrompt)) {
|
|
var prompt =
|
|
Cc["@mozilla.org/network/default-auth-prompt;1"].createInstance();
|
|
return prompt.QueryInterface(iid);
|
|
}
|
|
throw Components.Exception("", Cr.NS_NOINTERFACE);
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIRequestObserver,
|
|
Ci.nsIProgressEventSink,
|
|
Ci.nsIInterfaceRequestor,
|
|
]);
|
|
}
|
|
|
|
// On macOS, all browser windows can be closed without Firefox exiting. If it
|
|
// is left in this state for a while and an update is pending, we should restart
|
|
// Firefox on our own to apply the update. This class will do that
|
|
// automatically.
|
|
class RestartOnLastWindowClosed {
|
|
#enabled = false;
|
|
#hasShutdown = false;
|
|
|
|
#restartTimer = null;
|
|
#restartTimerExpired = false;
|
|
|
|
constructor() {
|
|
this.#maybeEnableOrDisable();
|
|
|
|
Services.prefs.addObserver(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
|
|
this
|
|
);
|
|
Services.obs.addObserver(this, "quit-application");
|
|
}
|
|
|
|
shutdown() {
|
|
LOG("RestartOnLastWindowClosed.shutdown - Shutting down");
|
|
this.#hasShutdown = true;
|
|
|
|
Services.prefs.removeObserver(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
|
|
this
|
|
);
|
|
Services.obs.removeObserver(this, "quit-application");
|
|
|
|
this.#maybeEnableOrDisable();
|
|
}
|
|
|
|
get shouldEnable() {
|
|
if (AppConstants.platform != "macosx") {
|
|
return false;
|
|
}
|
|
if (this.#hasShutdown) {
|
|
return false;
|
|
}
|
|
return Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
|
|
false
|
|
);
|
|
}
|
|
|
|
get enabled() {
|
|
return this.#enabled;
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "nsPref:changed":
|
|
if (data == PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED) {
|
|
this.#maybeEnableOrDisable();
|
|
}
|
|
break;
|
|
case "quit-application":
|
|
this.shutdown();
|
|
break;
|
|
case "domwindowclosed":
|
|
this.#onWindowClose();
|
|
break;
|
|
case "domwindowopened":
|
|
this.#onWindowOpen();
|
|
break;
|
|
case "update-downloaded":
|
|
case "update-staged":
|
|
this.#onUpdateReady(data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Returns true if any windows are open. Otherwise, false.
|
|
#windowsAreOpen() {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const win of Services.wm.getEnumerator(null)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Enables or disables this class's functionality based on the value of
|
|
// this.shouldEnable. Does nothing if the class is already in the right state
|
|
// (i.e. if the class should be enabled and already is, or should be disabled
|
|
// and already is).
|
|
#maybeEnableOrDisable() {
|
|
if (this.shouldEnable) {
|
|
if (this.#enabled) {
|
|
return;
|
|
}
|
|
LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Enabling");
|
|
|
|
Services.obs.addObserver(this, "domwindowclosed");
|
|
Services.obs.addObserver(this, "domwindowopened");
|
|
Services.obs.addObserver(this, "update-downloaded");
|
|
Services.obs.addObserver(this, "update-staged");
|
|
|
|
this.#restartTimer = null;
|
|
this.#restartTimerExpired = false;
|
|
|
|
this.#enabled = true;
|
|
|
|
// Synchronize with external state.
|
|
this.#onWindowClose();
|
|
} else {
|
|
if (!this.#enabled) {
|
|
return;
|
|
}
|
|
LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Disabling");
|
|
|
|
Services.obs.removeObserver(this, "domwindowclosed");
|
|
Services.obs.removeObserver(this, "domwindowopened");
|
|
Services.obs.removeObserver(this, "update-downloaded");
|
|
Services.obs.removeObserver(this, "update-staged");
|
|
|
|
this.#enabled = false;
|
|
|
|
if (this.#restartTimer) {
|
|
this.#restartTimer.cancel();
|
|
}
|
|
this.#restartTimer = null;
|
|
}
|
|
}
|
|
|
|
// Note: Since we keep track of the update state even when this class is
|
|
// disabled, this function will run even in that case.
|
|
#onUpdateReady(updateState) {
|
|
// Note that we do not count pending-elevate as a ready state, because we
|
|
// cannot silently restart in that state.
|
|
if (
|
|
[
|
|
STATE_APPLIED,
|
|
STATE_PENDING,
|
|
STATE_APPLIED_SERVICE,
|
|
STATE_PENDING_SERVICE,
|
|
].includes(updateState)
|
|
) {
|
|
if (this.#enabled) {
|
|
LOG("RestartOnLastWindowClosed.#onUpdateReady - update ready");
|
|
this.#maybeRestartBrowser();
|
|
}
|
|
} else if (this.#enabled) {
|
|
LOG(
|
|
`RestartOnLastWindowClosed.#onUpdateReady - Not counting update as ` +
|
|
`ready because the state is ${updateState}`
|
|
);
|
|
}
|
|
}
|
|
|
|
#onWindowClose() {
|
|
if (!this.#windowsAreOpen()) {
|
|
this.#onLastWindowClose();
|
|
}
|
|
}
|
|
|
|
#onLastWindowClose() {
|
|
if (this.#restartTimer || this.#restartTimerExpired) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#onLastWindowClose - Restart timer is " +
|
|
"either already running or has already expired"
|
|
);
|
|
return;
|
|
}
|
|
|
|
let timeout = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS,
|
|
5 * 60 * 1000
|
|
);
|
|
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#onLastWindowClose - Last window closed. " +
|
|
"Starting restart timer"
|
|
);
|
|
this.#restartTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
this.#restartTimer.initWithCallback(
|
|
() => this.#onRestartTimerExpire(),
|
|
timeout,
|
|
Ci.nsITimer.TYPE_ONE_SHOT
|
|
);
|
|
}
|
|
|
|
#onWindowOpen() {
|
|
if (this.#restartTimer) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#onWindowOpen - Window opened. Cancelling " +
|
|
"restart timer."
|
|
);
|
|
this.#restartTimer.cancel();
|
|
}
|
|
this.#restartTimer = null;
|
|
this.#restartTimerExpired = false;
|
|
}
|
|
|
|
#onRestartTimerExpire() {
|
|
LOG("RestartOnLastWindowClosed.#onRestartTimerExpire - Timer Expired");
|
|
|
|
this.#restartTimer = null;
|
|
this.#restartTimerExpired = true;
|
|
this.#maybeRestartBrowser();
|
|
}
|
|
|
|
#maybeRestartBrowser() {
|
|
if (!this.#restartTimerExpired) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - Still waiting for " +
|
|
"all windows to be closed and restartTimer to expire. " +
|
|
"(not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (lazy.AUS.currentState != Ci.nsIApplicationUpdateService.STATE_PENDING) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - No update ready. " +
|
|
"(not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (getElevationRequired()) {
|
|
// We check for STATE_PENDING_ELEVATE elsewhere, but this is actually
|
|
// different from that because it is technically possible that the user
|
|
// gave permission to elevate, but we haven't actually elevated yet.
|
|
// This is a bit of a corner case. We only call elevationOptedIn() right
|
|
// before we restart to apply the update immediately. But it is possible
|
|
// that something could stop the browser from shutting down.
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - This update will " +
|
|
"require user elevation (not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.#windowsAreOpen()) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - Window " +
|
|
"unexpectedly still open! (not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!this.shouldEnable) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - Unexpectedly " +
|
|
"attempted to restart when RestartOnLastWindowClosed ought to be " +
|
|
"disabled! (not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
LOG("RestartOnLastWindowClosed.#maybeRestartBrowser - Restarting now");
|
|
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();
|