7471 lines
244 KiB
JavaScript
7471 lines
244 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",
|
|
setTimeout: "resource://gre/modules/Timer.sys.mjs",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"AUS",
|
|
"@mozilla.org/updates/update-service;1",
|
|
"nsIApplicationUpdateService"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"UM",
|
|
"@mozilla.org/updates/update-manager;1",
|
|
"nsIUpdateManager"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"CheckSvc",
|
|
"@mozilla.org/updates/update-checker;1",
|
|
"nsIUpdateChecker"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"UpdateServiceStub",
|
|
"@mozilla.org/updates/update-service-stub;1",
|
|
"nsIApplicationUpdateServiceStub"
|
|
);
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"UpdateMutex",
|
|
"@mozilla.org/updates/update-mutex;1",
|
|
"nsIUpdateMutex"
|
|
);
|
|
|
|
const UPDATESERVICE_CID = Components.ID(
|
|
"{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}"
|
|
);
|
|
|
|
const PREF_APP_UPDATE_ALTUPDATEDIRPATH = "app.update.altUpdateDirPath";
|
|
const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
|
|
const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
|
|
const PREF_APP_UPDATE_BACKGROUND_ALLOWDOWNLOADSWITHOUTBITS =
|
|
"app.update.background.allowDownloadsWithoutBITS";
|
|
const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled";
|
|
const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
|
|
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
|
|
const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
|
|
const PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED =
|
|
"app.update.checkOnlyInstance.enabled";
|
|
const PREF_APP_UPDATE_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_LAST_UPDATE_LOG = "last-update.log";
|
|
const FILE_LAST_UPDATE_ELEVATED_LOG = "last-update-elevated.log";
|
|
const FILE_UPDATES_XML = "updates.xml";
|
|
const FILE_UPDATE_LOG = "update.log";
|
|
const FILE_UPDATE_ELEVATED_LOG = "update-elevated.log";
|
|
const FILE_UPDATE_MAR = "update.mar";
|
|
const FILE_UPDATE_STATUS = "update.status";
|
|
const FILE_UPDATE_TEST = "update.test";
|
|
const FILE_UPDATE_VERSION = "update.version";
|
|
|
|
const STATE_NONE = "null";
|
|
const STATE_DOWNLOADING = "downloading";
|
|
const STATE_PENDING = "pending";
|
|
const STATE_PENDING_SERVICE = "pending-service";
|
|
const STATE_PENDING_ELEVATE = "pending-elevate";
|
|
const STATE_APPLYING = "applying";
|
|
const STATE_APPLIED = "applied";
|
|
const STATE_APPLIED_SERVICE = "applied-service";
|
|
const STATE_SUCCEEDED = "succeeded";
|
|
const STATE_DOWNLOAD_FAILED = "download-failed";
|
|
const STATE_FAILED = "failed";
|
|
|
|
// BITS will keep retrying a download after transient errors, unless this much
|
|
// time has passed since there has been download progress.
|
|
// Similarly to ...POLL_RATE_MS below, we are much more aggressive when the user
|
|
// is watching the download progress.
|
|
const BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS = 3600; // 1 hour
|
|
const BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS = 5;
|
|
|
|
// These value control how frequently we get updates from the BITS client on
|
|
// the progress made downloading. The difference between the two is that the
|
|
// active interval is the one used when the user is watching. The idle interval
|
|
// is the one used when no one is watching.
|
|
const BITS_IDLE_POLL_RATE_MS = 1000;
|
|
const BITS_ACTIVE_POLL_RATE_MS = 200;
|
|
|
|
// The number of update attempts when a write error occurs during an attempt
|
|
const MAX_TOTAL_INSTALL_ATTEMPTS = 2;
|
|
|
|
// The values below used by this code are from common/updatererrors.h
|
|
const WRITE_ERROR = 7;
|
|
const ELEVATION_CANCELED = 9;
|
|
const SERVICE_UPDATER_COULD_NOT_BE_STARTED = 24;
|
|
const SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS = 25;
|
|
const SERVICE_UPDATER_SIGN_ERROR = 26;
|
|
const SERVICE_UPDATER_COMPARE_ERROR = 27;
|
|
const SERVICE_UPDATER_IDENTITY_ERROR = 28;
|
|
const SERVICE_STILL_APPLYING_ON_SUCCESS = 29;
|
|
const SERVICE_STILL_APPLYING_ON_FAILURE = 30;
|
|
const SERVICE_UPDATER_NOT_FIXED_DRIVE = 31;
|
|
const SERVICE_COULD_NOT_LOCK_UPDATER = 32;
|
|
const SERVICE_INSTALLDIR_ERROR = 33;
|
|
const WRITE_ERROR_ACCESS_DENIED = 35;
|
|
const WRITE_ERROR_CALLBACK_APP = 37;
|
|
const UNEXPECTED_STAGING_ERROR = 43;
|
|
const DELETE_ERROR_STAGING_LOCK_FILE = 44;
|
|
const SERVICE_COULD_NOT_COPY_UPDATER = 49;
|
|
const SERVICE_STILL_APPLYING_TERMINATED = 50;
|
|
const SERVICE_STILL_APPLYING_NO_EXIT_CODE = 51;
|
|
const SERVICE_COULD_NOT_IMPERSONATE = 58;
|
|
const WRITE_ERROR_FILE_COPY = 61;
|
|
const WRITE_ERROR_DELETE_FILE = 62;
|
|
const WRITE_ERROR_OPEN_PATCH_FILE = 63;
|
|
const WRITE_ERROR_PATCH_FILE = 64;
|
|
const WRITE_ERROR_APPLY_DIR_PATH = 65;
|
|
const WRITE_ERROR_CALLBACK_PATH = 66;
|
|
const WRITE_ERROR_FILE_ACCESS_DENIED = 67;
|
|
const WRITE_ERROR_DIR_ACCESS_DENIED = 68;
|
|
const WRITE_ERROR_DELETE_BACKUP = 69;
|
|
const WRITE_ERROR_EXTRACT = 70;
|
|
|
|
// Error codes 80 through 99 are reserved for UpdateService.sys.mjs and are not
|
|
// defined in common/updatererrors.h
|
|
const ERR_UPDATER_CRASHED = 89;
|
|
const ERR_OLDER_VERSION_OR_SAME_BUILD = 90;
|
|
const ERR_UPDATE_STATE_NONE = 91;
|
|
const ERR_CHANNEL_CHANGE = 92;
|
|
const INVALID_UPDATER_STATE_CODE = 98;
|
|
const INVALID_UPDATER_STATUS_CODE = 99;
|
|
|
|
const SILENT_UPDATE_NEEDED_ELEVATION_ERROR = 105;
|
|
const BACKGROUND_TASK_SHARING_VIOLATION = 106;
|
|
|
|
// Array of write errors to simplify checks for write errors
|
|
const WRITE_ERRORS = [
|
|
WRITE_ERROR,
|
|
WRITE_ERROR_ACCESS_DENIED,
|
|
WRITE_ERROR_CALLBACK_APP,
|
|
WRITE_ERROR_FILE_COPY,
|
|
WRITE_ERROR_DELETE_FILE,
|
|
WRITE_ERROR_OPEN_PATCH_FILE,
|
|
WRITE_ERROR_PATCH_FILE,
|
|
WRITE_ERROR_APPLY_DIR_PATH,
|
|
WRITE_ERROR_CALLBACK_PATH,
|
|
WRITE_ERROR_FILE_ACCESS_DENIED,
|
|
WRITE_ERROR_DIR_ACCESS_DENIED,
|
|
WRITE_ERROR_DELETE_BACKUP,
|
|
WRITE_ERROR_EXTRACT,
|
|
];
|
|
|
|
// Array of write errors to simplify checks for service errors
|
|
const SERVICE_ERRORS = [
|
|
SERVICE_UPDATER_COULD_NOT_BE_STARTED,
|
|
SERVICE_NOT_ENOUGH_COMMAND_LINE_ARGS,
|
|
SERVICE_UPDATER_SIGN_ERROR,
|
|
SERVICE_UPDATER_COMPARE_ERROR,
|
|
SERVICE_UPDATER_IDENTITY_ERROR,
|
|
SERVICE_STILL_APPLYING_ON_SUCCESS,
|
|
SERVICE_STILL_APPLYING_ON_FAILURE,
|
|
SERVICE_UPDATER_NOT_FIXED_DRIVE,
|
|
SERVICE_COULD_NOT_LOCK_UPDATER,
|
|
SERVICE_INSTALLDIR_ERROR,
|
|
SERVICE_COULD_NOT_COPY_UPDATER,
|
|
SERVICE_STILL_APPLYING_TERMINATED,
|
|
SERVICE_STILL_APPLYING_NO_EXIT_CODE,
|
|
SERVICE_COULD_NOT_IMPERSONATE,
|
|
];
|
|
|
|
// Custom update error codes
|
|
const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
|
|
const NETWORK_ERROR_OFFLINE = 111;
|
|
|
|
// 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
|
|
|
|
// This value will be set to true if it appears that BITS is being used by
|
|
// another user to download updates. We don't really want two users using BITS
|
|
// at once. Computers with many users (ex: a school computer), should not end
|
|
// up with dozens of BITS jobs.
|
|
var gBITSInUseByAnotherUser = false;
|
|
// The update service can be invoked as part of a standalone headless background
|
|
// task. In this context, when the background task kicks off an update
|
|
// download, we don't want it to move on to staging. As soon as the download has
|
|
// kicked off, the task begins shutting down and, even if the the download
|
|
// completes incredibly quickly, we don't want staging to begin while we are
|
|
// shutting down. That isn't a well tested scenario and it's possible that it
|
|
// could leave us in a bad state.
|
|
let gOnlyDownloadUpdatesThisSession = false;
|
|
// This will be the backing for `nsIApplicationUpdateService.currentState`
|
|
var gUpdateState = Ci.nsIApplicationUpdateService.STATE_IDLE;
|
|
|
|
/**
|
|
* Simple container and constructor for a Promise and its resolve function.
|
|
*/
|
|
class SelfContainedPromise {
|
|
constructor() {
|
|
this.promise = new Promise(resolve => {
|
|
this.resolve = resolve;
|
|
});
|
|
}
|
|
}
|
|
|
|
// This will contain a `SelfContainedPromise` that will be used to back
|
|
// `nsIApplicationUpdateService.stateTransition`.
|
|
var gStateTransitionPromise = new SelfContainedPromise();
|
|
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gUpdateBundle",
|
|
function aus_gUpdateBundle() {
|
|
return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
|
|
}
|
|
);
|
|
|
|
function resetIsBackgroundTaskMode() {
|
|
/**
|
|
* gIsBackgroundTaskMode will be true if Firefox is currently running as a
|
|
* background task. Otherwise it will be false.
|
|
*/
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gIsBackgroundTaskMode",
|
|
function aus_gCurrentlyRunningAsBackgroundTask() {
|
|
if (!("@mozilla.org/backgroundtasks;1" in Cc)) {
|
|
return false;
|
|
}
|
|
const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
|
|
Ci.nsIBackgroundTasks
|
|
);
|
|
if (!bts) {
|
|
return false;
|
|
}
|
|
return bts.isBackgroundTaskMode;
|
|
}
|
|
);
|
|
}
|
|
resetIsBackgroundTaskMode();
|
|
|
|
// Exported for testing only.
|
|
export function testResetIsBackgroundTaskMode() {
|
|
resetIsBackgroundTaskMode();
|
|
}
|
|
|
|
/**
|
|
* Changes `nsIApplicationUpdateService.currentState` and causes
|
|
* `nsIApplicationUpdateService.stateTransition` to resolve.
|
|
*/
|
|
function transitionState(newState) {
|
|
if (newState == gUpdateState) {
|
|
LOG("transitionState - Not transitioning state because it isn't changing.");
|
|
return;
|
|
}
|
|
LOG(
|
|
`transitionState - "${lazy.AUS.getStateName(gUpdateState)}" -> ` +
|
|
`"${lazy.AUS.getStateName(newState)}".`
|
|
);
|
|
gUpdateState = newState;
|
|
// Assign the new Promise before we resolve the old one just to make sure that
|
|
// anything that runs as a result of `resolve` doesn't end up waiting on the
|
|
// Promise that already resolved.
|
|
let oldStateTransitionPromise = gStateTransitionPromise;
|
|
gStateTransitionPromise = new SelfContainedPromise();
|
|
oldStateTransitionPromise.resolve();
|
|
}
|
|
|
|
/**
|
|
* When a plain JS object is passed through xpconnect the other side sees a
|
|
* wrapped version of the object instead of the real object. Since these two
|
|
* objects are different they act as different keys for Map and WeakMap. However
|
|
* xpconnect gives us a way to get the underlying JS object from the wrapper so
|
|
* this function returns the JS object regardless of whether passed the JS
|
|
* object or its wrapper for use in places where it is unclear which one you
|
|
* have.
|
|
*/
|
|
function unwrap(obj) {
|
|
return obj.wrappedJSObject ?? obj;
|
|
}
|
|
|
|
/**
|
|
* When an update starts to download (and if the feature is enabled) the add-ons
|
|
* manager starts downloading updated language packs for the new application
|
|
* version. A promise is used to track whether those updates are complete so the
|
|
* front-end is only notified that an application update is ready once the
|
|
* language pack updates have been staged.
|
|
*
|
|
* In order to be able to access that promise from various places in the update
|
|
* service they are cached in this map using the nsIUpdate object as a weak
|
|
* owner. Note that the key should always be the result of calling the above
|
|
* unwrap function on the nsIUpdate to ensure a consistent object is used as the
|
|
* key.
|
|
*
|
|
* When the language packs finish staging the nsIUpdate entriy is removed from
|
|
* this map so if the entry is still there then language pack updates are in
|
|
* progress.
|
|
*/
|
|
const LangPackUpdates = new WeakMap();
|
|
|
|
/**
|
|
* When we're polling to see if other running instances of the application have
|
|
* exited, there's no need to ever start polling again in parallel. To prevent
|
|
* doing that, we keep track of the promise that resolves when polling completes
|
|
* and return that if a second simultaneous poll is requested, so that the
|
|
* multiple callers end up waiting for the same promise to resolve.
|
|
*/
|
|
let gOtherInstancePollPromise;
|
|
|
|
/**
|
|
* Query the update sync manager to see if another instance of this same
|
|
* installation of this application is currently running, under the context of
|
|
* any operating system user (not just the current one).
|
|
* This function immediately returns the current, instantaneous status of any
|
|
* other instances.
|
|
*
|
|
* @return true if at least one other instance is running, false if not
|
|
*/
|
|
function isOtherInstanceRunning() {
|
|
const checkEnabled = Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_ENABLED,
|
|
true
|
|
);
|
|
if (!checkEnabled) {
|
|
LOG("isOtherInstanceRunning - disabled by pref, skipping check");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
let syncManager = Cc[
|
|
"@mozilla.org/updates/update-sync-manager;1"
|
|
].getService(Ci.nsIUpdateSyncManager);
|
|
return syncManager.isOtherInstanceRunning();
|
|
} catch (ex) {
|
|
LOG(`isOtherInstanceRunning - sync manager failed with exception: ${ex}`);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Query the update sync manager to see if another instance of this same
|
|
* installation of this application is currently running, under the context of
|
|
* any operating system user (not just the one running this instance).
|
|
* This function polls for the status of other instances continually
|
|
* (asynchronously) until either none exist or a timeout expires.
|
|
*
|
|
* @return a Promise that resolves with false if at any point during polling no
|
|
* other instances can be found, or resolves with true if the timeout
|
|
* expires when other instances are still running
|
|
*/
|
|
function waitForOtherInstances() {
|
|
// If we're already in the middle of a poll, reuse it rather than start again.
|
|
if (gOtherInstancePollPromise) {
|
|
return gOtherInstancePollPromise;
|
|
}
|
|
|
|
let timeout = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_TIMEOUT,
|
|
ONLY_INSTANCE_CHECK_DEFAULT_TIMEOUT_MS
|
|
);
|
|
|
|
// return immediately if timeout value is invalid.
|
|
if (timeout <= 0) {
|
|
return Promise.resolve(isOtherInstanceRunning());
|
|
}
|
|
|
|
// Don't allow the pref to set a super high timeout and break this feature.
|
|
if (timeout > ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS) {
|
|
timeout = ONLY_INSTANCE_CHECK_MAX_TIMEOUT_MS;
|
|
}
|
|
|
|
let interval = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CHECK_ONLY_INSTANCE_INTERVAL,
|
|
ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS
|
|
);
|
|
|
|
if (interval <= 0) {
|
|
interval = ONLY_INSTANCE_CHECK_DEFAULT_POLL_INTERVAL_MS;
|
|
}
|
|
|
|
// Don't allow an interval longer than the timeout.
|
|
interval = Math.min(interval, timeout);
|
|
|
|
let iterations = 0;
|
|
const maxIterations = Math.ceil(timeout / interval);
|
|
|
|
gOtherInstancePollPromise = new Promise(function (resolve) {
|
|
let poll = function () {
|
|
iterations++;
|
|
if (!isOtherInstanceRunning()) {
|
|
LOG("waitForOtherInstances - no other instances found, exiting");
|
|
resolve(false);
|
|
gOtherInstancePollPromise = undefined;
|
|
} else if (iterations >= maxIterations) {
|
|
LOG(
|
|
"waitForOtherInstances - timeout expired while other instances " +
|
|
"are still running"
|
|
);
|
|
resolve(true);
|
|
gOtherInstancePollPromise = undefined;
|
|
} else if (iterations + 1 == maxIterations && timeout % interval != 0) {
|
|
// In case timeout isn't a multiple of interval, set the next timeout
|
|
// for the remainder of the time rather than for the usual interval.
|
|
lazy.setTimeout(poll, timeout % interval);
|
|
} else {
|
|
lazy.setTimeout(poll, interval);
|
|
}
|
|
};
|
|
|
|
LOG("waitForOtherInstances - beginning polling");
|
|
poll();
|
|
});
|
|
|
|
return gOtherInstancePollPromise;
|
|
}
|
|
|
|
/**
|
|
* Tests to make sure that we can write to a given directory.
|
|
*
|
|
* @param updateTestFile a test file in the directory that needs to be tested.
|
|
* @param createDirectory whether a test directory should be created.
|
|
* @throws if we don't have right access to the directory.
|
|
*/
|
|
function testWriteAccess(updateTestFile, createDirectory) {
|
|
const NORMAL_FILE_TYPE = Ci.nsIFile.NORMAL_FILE_TYPE;
|
|
const DIRECTORY_TYPE = Ci.nsIFile.DIRECTORY_TYPE;
|
|
if (updateTestFile.exists()) {
|
|
updateTestFile.remove(false);
|
|
}
|
|
updateTestFile.create(
|
|
createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE,
|
|
createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE
|
|
);
|
|
updateTestFile.remove(false);
|
|
}
|
|
|
|
/**
|
|
* Tests whether or not the current instance has the update mutex. Tries to
|
|
* acquire it if it is not held currently.
|
|
*
|
|
* @return true if this instance now holds the update mutex or was already
|
|
* holding
|
|
*/
|
|
function hasUpdateMutex() {
|
|
return lazy.UpdateMutex.tryLock();
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not all descendants of a directory are writeable.
|
|
* Note: Does not check the root directory itself for writeability.
|
|
*
|
|
* @return true if all descendants are writeable, false otherwise
|
|
*/
|
|
function areDirectoryEntriesWriteable(aDir) {
|
|
let items = aDir.directoryEntries;
|
|
while (items.hasMoreElements()) {
|
|
let item = items.nextFile;
|
|
if (!item.isWritable()) {
|
|
LOG("areDirectoryEntriesWriteable - unable to write to " + item.path);
|
|
return false;
|
|
}
|
|
if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* OSX only function to determine if the user requires elevation to be able to
|
|
* write to the application bundle.
|
|
*
|
|
* @return true if elevation is required, false otherwise
|
|
*/
|
|
function getElevationRequired() {
|
|
if (AppConstants.platform != "macosx") {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// Recursively check that the application bundle (and its descendants) can
|
|
// be written to.
|
|
LOG(
|
|
"getElevationRequired - recursively testing write access on " +
|
|
getInstallDirRoot().path
|
|
);
|
|
if (
|
|
!getInstallDirRoot().isWritable() ||
|
|
!areDirectoryEntriesWriteable(getInstallDirRoot())
|
|
) {
|
|
LOG(
|
|
"getElevationRequired - unable to write to application bundle, " +
|
|
"elevation required"
|
|
);
|
|
return true;
|
|
}
|
|
} catch (ex) {
|
|
LOG(
|
|
"getElevationRequired - unable to write to application bundle, " +
|
|
"elevation required. Exception: " +
|
|
ex
|
|
);
|
|
return true;
|
|
}
|
|
LOG(
|
|
"getElevationRequired - able to write to application bundle, elevation " +
|
|
"not required"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* A promise that resolves when language packs are downloading or if no language
|
|
* packs are being downloaded.
|
|
*/
|
|
function promiseLangPacksUpdated(update) {
|
|
let promise = LangPackUpdates.get(unwrap(update));
|
|
if (promise) {
|
|
LOG(
|
|
"promiseLangPacksUpdated - waiting for language pack updates to stage."
|
|
);
|
|
return promise;
|
|
}
|
|
|
|
// In case callers rely on a promise just return an already resolved promise.
|
|
return Promise.resolve();
|
|
}
|
|
|
|
/*
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
function isAppBaseDirWritable() {
|
|
let appDirTestFile = "";
|
|
|
|
try {
|
|
appDirTestFile = getAppBaseDir();
|
|
appDirTestFile.append(FILE_UPDATE_TEST);
|
|
} catch (e) {
|
|
LOG(
|
|
"isAppBaseDirWritable - Base directory or test path could not be " +
|
|
`determined: ${e}`
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
LOG(
|
|
`isAppBaseDirWritable - testing write access for ${appDirTestFile.path}`
|
|
);
|
|
|
|
if (appDirTestFile.exists()) {
|
|
appDirTestFile.remove(false);
|
|
}
|
|
// if we're unable to create the test file this will throw an exception:
|
|
appDirTestFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
|
|
appDirTestFile.remove(false);
|
|
LOG(`isAppBaseDirWritable - Path is writable: ${appDirTestFile.path}`);
|
|
return true;
|
|
} catch (e) {
|
|
LOG(
|
|
`isAppBaseDirWritable - Path '${appDirTestFile.path}' ` +
|
|
`is not writable: ${e}`
|
|
);
|
|
}
|
|
// No write access to the installation directory
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not an update can be applied. This is always true on
|
|
* Windows when the service is used. On Mac OS X and Linux, if the user has
|
|
* write access to the update directory this will return true because on OSX we
|
|
* offer users the option to perform an elevated update when necessary and on
|
|
* Linux the update directory is located in the application directory.
|
|
*
|
|
* @return true if an update can be applied, false otherwise
|
|
*/
|
|
function getCanApplyUpdates() {
|
|
try {
|
|
// Check if it is possible to write to the update directory so clients won't
|
|
// repeatedly try to apply an update without the ability to complete the
|
|
// update process which requires write access to the update directory.
|
|
let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
|
|
LOG("getCanApplyUpdates - testing write access " + updateTestFile.path);
|
|
testWriteAccess(updateTestFile, false);
|
|
} catch (e) {
|
|
LOG(
|
|
"getCanApplyUpdates - unable to apply updates without write " +
|
|
"access to the update directory. Exception: " +
|
|
e
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (AppConstants.platform == "macosx" || AppConstants.platform == "win") {
|
|
LOG(
|
|
"getCanApplyUpdates - bypass the write since elevation can be used " +
|
|
"on macOS and Windows"
|
|
);
|
|
return true;
|
|
}
|
|
|
|
if (!isAppBaseDirWritable()) {
|
|
LOG(
|
|
"getCanApplyUpdates - unable to apply updates, because the base " +
|
|
"directory is not writable."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
LOG("getCanApplyUpdates - able to apply updates");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Whether or not the application can stage an update for the current session.
|
|
* These checks are only performed once per session due to using a lazy getter.
|
|
*
|
|
* @return true if updates can be staged for this session.
|
|
*/
|
|
ChromeUtils.defineLazyGetter(
|
|
lazy,
|
|
"gCanStageUpdatesSession",
|
|
function aus_gCSUS() {
|
|
if (getElevationRequired()) {
|
|
LOG(
|
|
"gCanStageUpdatesSession - unable to stage updates because elevation " +
|
|
"is required."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
let updateTestFile;
|
|
if (AppConstants.platform == "macosx") {
|
|
updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
|
|
} else {
|
|
updateTestFile = getInstallDirRoot();
|
|
updateTestFile.append(FILE_UPDATE_TEST);
|
|
}
|
|
LOG(
|
|
"gCanStageUpdatesSession - testing write access " + updateTestFile.path
|
|
);
|
|
testWriteAccess(updateTestFile, true);
|
|
if (AppConstants.platform != "macosx") {
|
|
// On all platforms except Mac, we need to test the parent directory as
|
|
// well, as we need to be able to move files in that directory during the
|
|
// replacing step.
|
|
updateTestFile = getInstallDirRoot().parent;
|
|
updateTestFile.append(FILE_UPDATE_TEST);
|
|
LOG(
|
|
"gCanStageUpdatesSession - testing write access " +
|
|
updateTestFile.path
|
|
);
|
|
updateTestFile.createUnique(
|
|
Ci.nsIFile.DIRECTORY_TYPE,
|
|
FileUtils.PERMS_DIRECTORY
|
|
);
|
|
updateTestFile.remove(false);
|
|
}
|
|
} catch (e) {
|
|
LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " + e);
|
|
// No write privileges
|
|
return false;
|
|
}
|
|
|
|
LOG("gCanStageUpdatesSession - able to stage updates");
|
|
return true;
|
|
}
|
|
);
|
|
|
|
/**
|
|
* Whether or not the application can stage an update.
|
|
*
|
|
* @param {boolean} [transient] Whether transient factors such as the update
|
|
* mutex should be considered.
|
|
* @return true if updates can be staged.
|
|
*/
|
|
function getCanStageUpdates(transient = true) {
|
|
// If staging updates are disabled, then just bail out!
|
|
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false)) {
|
|
LOG(
|
|
"getCanStageUpdates - staging updates is disabled by preference " +
|
|
PREF_APP_UPDATE_STAGING_ENABLED
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (AppConstants.platform == "win" && shouldUseService()) {
|
|
// No need to perform directory write checks, the maintenance service will
|
|
// be able to write to all directories.
|
|
LOG("getCanStageUpdates - able to stage updates using the service");
|
|
return true;
|
|
}
|
|
|
|
if (transient && !hasUpdateMutex()) {
|
|
LOG(
|
|
"getCanStageUpdates - unable to apply updates because another " +
|
|
"instance of the application is already handling updates for this " +
|
|
"installation."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return lazy.gCanStageUpdatesSession;
|
|
}
|
|
|
|
/*
|
|
* Whether or not the application can use BITS to download updates.
|
|
*
|
|
* @param {boolean} [transient] Whether transient factors such as the update
|
|
* mutex should be considered.
|
|
* @return A string with one of these values:
|
|
* CanUseBits
|
|
* NoBits_NotWindows
|
|
* NoBits_FeatureOff
|
|
* NoBits_Pref
|
|
* NoBits_Proxy
|
|
* NoBits_OtherUser
|
|
* These strings are directly compatible with the categories for the
|
|
* Glean.update.canUseBitsExternal and Glean.update.canUseBitsNotify glean
|
|
* metrics. If this function is made to return other values, they should
|
|
* also be added to the labels lists for those probes in Histograms.json
|
|
* and metrics.yaml.
|
|
*/
|
|
function getCanUseBits(transient = true) {
|
|
if (AppConstants.platform != "win") {
|
|
LOG("getCanUseBits - Not using BITS because this is not Windows");
|
|
return "NoBits_NotWindows";
|
|
}
|
|
if (!AppConstants.MOZ_BITS_DOWNLOAD) {
|
|
LOG("getCanUseBits - Not using BITS because the feature is disabled");
|
|
return "NoBits_FeatureOff";
|
|
}
|
|
|
|
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_BITS_ENABLED, true)) {
|
|
LOG("getCanUseBits - Not using BITS. Disabled by pref.");
|
|
return "NoBits_Pref";
|
|
}
|
|
// Firefox support for passing proxies to BITS is still rudimentary.
|
|
// For now, disable BITS support on configurations that are not using the
|
|
// standard system proxy.
|
|
let defaultProxy = Ci.nsIProtocolProxyService.PROXYCONFIG_SYSTEM;
|
|
if (
|
|
Services.prefs.getIntPref(PREF_NETWORK_PROXY_TYPE, defaultProxy) !=
|
|
defaultProxy &&
|
|
!Cu.isInAutomation
|
|
) {
|
|
LOG("getCanUseBits - Not using BITS because of proxy usage");
|
|
return "NoBits_Proxy";
|
|
}
|
|
if (transient && gBITSInUseByAnotherUser) {
|
|
LOG("getCanUseBits - Not using BITS. Already in use by another user");
|
|
return "NoBits_OtherUser";
|
|
}
|
|
LOG("getCanUseBits - BITS can be used to download updates");
|
|
return "CanUseBits";
|
|
}
|
|
|
|
/**
|
|
* Logs a string to the error console. If enabled, also logs to the update
|
|
* messages file.
|
|
* @param string
|
|
* The string to write to the error console.
|
|
*/
|
|
function LOG(string) {
|
|
lazy.UpdateLog.logPrefixedString("AUS:SVC", string);
|
|
}
|
|
|
|
/**
|
|
* Gets the specified directory at the specified hierarchy under the
|
|
* update root directory and creates it if it doesn't exist.
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|
|
|
* @return nsIFile object for the location specified.
|
|
*/
|
|
function getUpdateDirCreate(pathArray) {
|
|
if (Cu.isInAutomation) {
|
|
// This allows tests to use an alternate updates directory so they can test
|
|
// startup behavior.
|
|
const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
|
|
const PREF_TEST_ROOT = "mochitest.testRoot";
|
|
let alternatePath = Services.prefs.getCharPref(
|
|
PREF_APP_UPDATE_ALTUPDATEDIRPATH,
|
|
null
|
|
);
|
|
if (alternatePath && alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
|
|
let testRoot = Services.prefs.getCharPref(PREF_TEST_ROOT);
|
|
let relativePath = alternatePath.substring(MAGIC_TEST_ROOT_PREFIX.length);
|
|
if (AppConstants.platform == "win") {
|
|
relativePath = relativePath.replace(/\//g, "\\");
|
|
}
|
|
alternatePath = testRoot + relativePath;
|
|
let updateDir = Cc["@mozilla.org/file/local;1"].createInstance(
|
|
Ci.nsIFile
|
|
);
|
|
updateDir.initWithPath(alternatePath);
|
|
for (let i = 0; i < pathArray.length; ++i) {
|
|
updateDir.append(pathArray[i]);
|
|
}
|
|
return updateDir;
|
|
}
|
|
}
|
|
|
|
let dir = FileUtils.getDir(KEY_UPDROOT, pathArray);
|
|
try {
|
|
dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
|
|
} catch (ex) {
|
|
if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
|
|
throw ex;
|
|
}
|
|
// Ignore the exception due to a directory that already exists.
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Gets the application base directory.
|
|
*
|
|
* @return nsIFile object for the application base directory.
|
|
*/
|
|
function getAppBaseDir() {
|
|
return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
|
|
}
|
|
|
|
/**
|
|
* Gets the root of the installation directory which is the application
|
|
* bundle directory on Mac OS X and the location of the application binary
|
|
* on all other platforms.
|
|
*
|
|
* @return nsIFile object for the directory
|
|
*/
|
|
function getInstallDirRoot() {
|
|
let dir = getAppBaseDir();
|
|
if (AppConstants.platform == "macosx") {
|
|
// On macOS, the executable is stored under Contents/MacOS.
|
|
dir = dir.parent.parent;
|
|
}
|
|
return dir;
|
|
}
|
|
|
|
/**
|
|
* Gets the file at the specified hierarchy under the update root directory.
|
|
* @param pathArray
|
|
* An array of path components to locate beneath the directory
|
|
* specified by |key|. The last item in this array must be the
|
|
* leaf name of a file.
|
|
* @return nsIFile object for the file specified. The file is NOT created
|
|
* if it does not exist, however all required directories along
|
|
* the way are.
|
|
*/
|
|
function getUpdateFile(pathArray) {
|
|
let file = getUpdateDirCreate(pathArray.slice(0, -1));
|
|
file.append(pathArray[pathArray.length - 1]);
|
|
return file;
|
|
}
|
|
|
|
/**
|
|
* This function is designed to let us slightly clean up the mapping between
|
|
* strings and error codes. So that instead of having:
|
|
* check_error-2147500036=Connection aborted
|
|
* check_error-2152398850=Connection aborted
|
|
* We can have:
|
|
* check_error-connection_aborted=Connection aborted
|
|
* And map both of those error codes to it.
|
|
*/
|
|
function maybeMapErrorCode(code) {
|
|
switch (code) {
|
|
case Cr.NS_BINDING_ABORTED:
|
|
case Cr.NS_ERROR_ABORT:
|
|
return "connection_aborted";
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* Returns human readable status text from the updates.properties bundle
|
|
* based on an error code
|
|
* @param code
|
|
* The error code to look up human readable status text for
|
|
* @param defaultCode
|
|
* The default code to look up should human readable status text
|
|
* not exist for |code|
|
|
* @return A human readable status text string
|
|
*/
|
|
function getStatusTextFromCode(code, defaultCode) {
|
|
code = maybeMapErrorCode(code);
|
|
|
|
let reason;
|
|
try {
|
|
reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + code);
|
|
LOG(
|
|
"getStatusTextFromCode - transfer error: " + reason + ", code: " + code
|
|
);
|
|
} catch (e) {
|
|
defaultCode = maybeMapErrorCode(defaultCode);
|
|
|
|
// Use the default reason
|
|
reason = lazy.gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
|
|
LOG(
|
|
"getStatusTextFromCode - transfer error: " +
|
|
reason +
|
|
", default code: " +
|
|
defaultCode
|
|
);
|
|
}
|
|
return reason;
|
|
}
|
|
|
|
/**
|
|
* Get the Ready Update directory. This is the directory that an update
|
|
* should reside in after download has completed but before it has been
|
|
* installed and cleaned up.
|
|
* @return The ready updates directory, as a nsIFile object
|
|
*/
|
|
function getReadyUpdateDir() {
|
|
return getUpdateDirCreate([DIR_UPDATES, DIR_UPDATE_READY]);
|
|
}
|
|
|
|
/**
|
|
* Get the Downloading Update directory. This is the directory that an update
|
|
* should reside in during download. Once download is completed, it will be
|
|
* moved to the Ready Update directory.
|
|
* @return The downloading update directory, as a nsIFile object
|
|
*/
|
|
function getDownloadingUpdateDir() {
|
|
return getUpdateDirCreate([DIR_UPDATES, DIR_UPDATE_DOWNLOADING]);
|
|
}
|
|
|
|
/**
|
|
* Reads the update state from the update.status file in the specified
|
|
* directory.
|
|
* @param dir
|
|
* The dir to look for an update.status file in
|
|
* @return The status value of the update.
|
|
*/
|
|
function readStatusFile(dir) {
|
|
let statusFile = dir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
let status = readStringFromFile(statusFile) || STATE_NONE;
|
|
LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
|
|
return status;
|
|
}
|
|
|
|
/**
|
|
* Writes the current update operation/state to a file in the patch
|
|
* directory, indicating to the patching system that operations need
|
|
* to be performed.
|
|
* @param dir
|
|
* The patch directory where the update.status file should be
|
|
* written.
|
|
* @param state
|
|
* The state value to write.
|
|
*/
|
|
function writeStatusFile(dir, state) {
|
|
let statusFile = dir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
writeStringToFile(statusFile, state);
|
|
}
|
|
|
|
/**
|
|
* Writes the update's application version to a file in the patch directory. If
|
|
* the update doesn't provide application version information via the
|
|
* appVersion attribute the string "null" will be written to the file.
|
|
* This value is compared during startup (in nsUpdateDriver.cpp) to determine if
|
|
* the update should be applied. Note that this won't provide protection from
|
|
* downgrade of the application for the nightly user case where the application
|
|
* version doesn't change.
|
|
* @param dir
|
|
* The patch directory where the update.version file should be
|
|
* written.
|
|
* @param version
|
|
* The version value to write. Will be the string "null" when the
|
|
* update doesn't provide the appVersion attribute in the update xml.
|
|
*/
|
|
function writeVersionFile(dir, version) {
|
|
let versionFile = dir.clone();
|
|
versionFile.append(FILE_UPDATE_VERSION);
|
|
writeStringToFile(versionFile, version);
|
|
}
|
|
|
|
/**
|
|
* Determines if the service should be used to attempt an update
|
|
* or not.
|
|
*
|
|
* @return true if the service should be used for updates.
|
|
*/
|
|
function shouldUseService() {
|
|
// This function will return true if the mantenance service should be used if
|
|
// all of the following conditions are met:
|
|
// 1) This build was done with the maintenance service enabled
|
|
// 2) The maintenance service is installed
|
|
// 3) The pref for using the service is enabled
|
|
if (
|
|
!AppConstants.MOZ_MAINTENANCE_SERVICE ||
|
|
!isServiceInstalled() ||
|
|
!Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false)
|
|
) {
|
|
LOG("shouldUseService - returning false");
|
|
return false;
|
|
}
|
|
|
|
LOG("shouldUseService - returning true");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determines if the service is is installed.
|
|
*
|
|
* @return true if the service is installed.
|
|
*/
|
|
function isServiceInstalled() {
|
|
if (!AppConstants.MOZ_MAINTENANCE_SERVICE || AppConstants.platform != "win") {
|
|
LOG("isServiceInstalled - returning false");
|
|
return false;
|
|
}
|
|
|
|
let installed = 0;
|
|
try {
|
|
let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
|
Ci.nsIWindowsRegKey
|
|
);
|
|
wrk.open(
|
|
wrk.ROOT_KEY_LOCAL_MACHINE,
|
|
"SOFTWARE\\Mozilla\\MaintenanceService",
|
|
wrk.ACCESS_READ | wrk.WOW64_64
|
|
);
|
|
installed = wrk.readIntValue("Installed");
|
|
wrk.close();
|
|
} catch (e) {}
|
|
installed = installed == 1; // convert to bool
|
|
LOG("isServiceInstalled - returning " + installed);
|
|
return installed;
|
|
}
|
|
|
|
/**
|
|
* Gets the appropriate pending update state. Returns STATE_PENDING_SERVICE,
|
|
* STATE_PENDING_ELEVATE, or STATE_PENDING.
|
|
*/
|
|
function getBestPendingState() {
|
|
if (shouldUseService()) {
|
|
return STATE_PENDING_SERVICE;
|
|
} else if (getElevationRequired()) {
|
|
return STATE_PENDING_ELEVATE;
|
|
}
|
|
return STATE_PENDING;
|
|
}
|
|
|
|
/**
|
|
* Removes the contents of the ready update directory and rotates the update
|
|
* logs when present. If the update.log exists in the patch directory this will
|
|
* move the last-update.log if it exists to backup-update.log in the parent
|
|
* directory of the patch directory and then move the update.log in the patch
|
|
* directory to last-update.log in the parent directory of the patch directory.
|
|
*
|
|
* @param aRemovePatchFiles (optional, defaults to true)
|
|
* When true the update's patch directory contents are removed.
|
|
*/
|
|
function cleanUpReadyUpdateDir(aRemovePatchFiles = true) {
|
|
let updateDir;
|
|
try {
|
|
updateDir = getReadyUpdateDir();
|
|
} catch (e) {
|
|
LOG(
|
|
"cleanUpReadyUpdateDir - unable to get the updates patch directory. " +
|
|
"Exception: " +
|
|
e
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Preserve the last update log files for debugging purposes.
|
|
// Make sure to keep the pairs of logs (ex "last-update.log" and
|
|
// "last-update-elevated.log") together. We don't want to skip moving
|
|
// "last-update-elevated.log" just because there isn't an
|
|
// "update-elevated.log" to take its place.
|
|
let updateLogFile = updateDir.clone();
|
|
updateLogFile.append(FILE_UPDATE_LOG);
|
|
let updateElevatedLogFile = updateDir.clone();
|
|
updateElevatedLogFile.append(FILE_UPDATE_ELEVATED_LOG);
|
|
if (updateLogFile.exists() || updateElevatedLogFile.exists()) {
|
|
const overwriteOrRemoveBackupLog = (log, shouldOverwrite, backupName) => {
|
|
if (shouldOverwrite) {
|
|
try {
|
|
log.moveTo(dir, backupName);
|
|
} catch (e) {
|
|
LOG(
|
|
`cleanUpReadyUpdateDir - failed to rename file '${log.path}' to ` +
|
|
`'${backupName}': ${e.result}`
|
|
);
|
|
}
|
|
} else {
|
|
// If we don't have a file to overwrite this one, make sure we remove
|
|
// it anyways to prevent log pairs from getting mismatched.
|
|
let backupLogFile = dir.clone();
|
|
backupLogFile.append(backupName);
|
|
try {
|
|
backupLogFile.remove(false);
|
|
} catch (e) {
|
|
if (e.result != Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
LOG(
|
|
`cleanUpReadyUpdateDir - failed to remove file ` +
|
|
`'${backupLogFile.path}': ${e.result}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
let dir = updateDir.parent;
|
|
let logFile = dir.clone();
|
|
logFile.append(FILE_LAST_UPDATE_LOG);
|
|
const logFileExists = logFile.exists();
|
|
let elevatedLogFile = dir.clone();
|
|
elevatedLogFile.append(FILE_LAST_UPDATE_ELEVATED_LOG);
|
|
const elevatedLogFileExists = elevatedLogFile.exists();
|
|
if (logFileExists || elevatedLogFileExists) {
|
|
overwriteOrRemoveBackupLog(
|
|
logFile,
|
|
logFileExists,
|
|
FILE_BACKUP_UPDATE_LOG
|
|
);
|
|
overwriteOrRemoveBackupLog(
|
|
elevatedLogFile,
|
|
elevatedLogFileExists,
|
|
FILE_BACKUP_UPDATE_ELEVATED_LOG
|
|
);
|
|
}
|
|
|
|
overwriteOrRemoveBackupLog(updateLogFile, true, FILE_LAST_UPDATE_LOG);
|
|
overwriteOrRemoveBackupLog(
|
|
updateElevatedLogFile,
|
|
true,
|
|
FILE_LAST_UPDATE_ELEVATED_LOG
|
|
);
|
|
}
|
|
|
|
if (aRemovePatchFiles) {
|
|
let dirEntries;
|
|
try {
|
|
dirEntries = updateDir.directoryEntries;
|
|
} catch (ex) {
|
|
// If if doesn't exist, our job is already done.
|
|
if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
return;
|
|
}
|
|
throw ex;
|
|
}
|
|
while (dirEntries.hasMoreElements()) {
|
|
let file = dirEntries.nextFile;
|
|
// Now, recursively remove this file. The recursive removal is needed for
|
|
// Mac OSX because this directory will contain a copy of updater.app,
|
|
// which is itself a directory and the MozUpdater directory on platforms
|
|
// other than Windows.
|
|
try {
|
|
file.remove(true);
|
|
} catch (e) {
|
|
LOG("cleanUpReadyUpdateDir - failed to remove file " + file.path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes the contents of the update download directory.
|
|
*
|
|
*/
|
|
function cleanUpDownloadingUpdateDir() {
|
|
let updateDir;
|
|
try {
|
|
updateDir = getDownloadingUpdateDir();
|
|
} catch (e) {
|
|
LOG(
|
|
"cleanUpDownloadUpdatesDir - unable to get the updates patch " +
|
|
"directory. Exception: " +
|
|
e
|
|
);
|
|
return;
|
|
}
|
|
|
|
let dirEntries;
|
|
try {
|
|
dirEntries = updateDir.directoryEntries;
|
|
} catch (ex) {
|
|
// If if doesn't exist, our job is already done.
|
|
if (ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
|
|
return;
|
|
}
|
|
throw ex;
|
|
}
|
|
while (dirEntries.hasMoreElements()) {
|
|
let file = dirEntries.nextFile;
|
|
// Now, recursively remove this file.
|
|
try {
|
|
file.remove(true);
|
|
} catch (e) {
|
|
LOG("cleanUpDownloadUpdatesDir - failed to remove file " + file.path);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up the updates list and the directory that contains the update that
|
|
* is ready to be installed.
|
|
*
|
|
* Note - This function causes a state transition to either STATE_DOWNLOADING
|
|
* or STATE_NONE, depending on whether an update download is in progress.
|
|
*/
|
|
function cleanupReadyUpdate() {
|
|
// Move the update from the Active Update list into the Past Updates list.
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
LOG("cleanupReadyUpdate - Clearing readyUpdate");
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
|
|
lazy.UM.internal.readyUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
let readyUpdateDir = getReadyUpdateDir();
|
|
let shouldSetDownloadingStatus =
|
|
lazy.UM.internal.downloadingUpdate ||
|
|
readStatusFile(readyUpdateDir) == STATE_DOWNLOADING;
|
|
|
|
// Now trash the ready update directory, since we're done with it
|
|
cleanUpReadyUpdateDir();
|
|
|
|
// We need to handle two similar cases here.
|
|
// The first is where we clean up the ready updates directory while we are in
|
|
// the downloading state. In this case, we remove the update.status file that
|
|
// says we are downloading, even though we should remain in that state.
|
|
// The second case is when we clean up a ready update, but there is also a
|
|
// downloading update (in which case the update status file's state will
|
|
// reflect the state of the ready update, not the downloading one). In that
|
|
// case, instead of reverting to STATE_NONE (which is what we do by removing
|
|
// the status file), we should set our state to downloading.
|
|
if (shouldSetDownloadingStatus) {
|
|
LOG("cleanupReadyUpdate - Transitioning back to downloading state.");
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
|
|
writeStatusFile(readyUpdateDir, STATE_DOWNLOADING);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up updates list and the directory that the currently downloading update
|
|
* is downloaded to.
|
|
*
|
|
* Note - This function may cause a state transition. If the current state is
|
|
* STATE_DOWNLOADING, this will cause it to change to STATE_NONE.
|
|
*/
|
|
async function cleanupDownloadingUpdate() {
|
|
// Move the update from the Active Update list into the Past Updates list.
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
LOG("cleanupDownloadingUpdate - Clearing downloadingUpdate.");
|
|
await lazy.AUS.wrappedJSObject.cancelDownloadingUpdate();
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
|
|
lazy.UM.internal.downloadingUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
// Now trash the update download directory, since we're done with it
|
|
cleanUpDownloadingUpdateDir();
|
|
|
|
// If the update status file says we are downloading, we should remove that
|
|
// too, since we aren't doing that anymore.
|
|
let readyUpdateDir = getReadyUpdateDir();
|
|
let status = readStatusFile(readyUpdateDir);
|
|
if (status == STATE_DOWNLOADING) {
|
|
let statusFile = readyUpdateDir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
statusFile.remove(false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clean up updates list, the ready update directory, and the downloading update
|
|
* directory.
|
|
*
|
|
* This is more efficient than calling
|
|
* cleanupReadyUpdate();
|
|
* cleanupDownloadingUpdate();
|
|
* because those need some special handling of the update status file to make
|
|
* sure that, for example, cleaning up a ready update doesn't make us forget
|
|
* that we are downloading an update. When we cleanup both updates, we don't
|
|
* need to worry about things like that.
|
|
*
|
|
* Note - This function causes a state transition to STATE_NONE.
|
|
*/
|
|
async function cleanupActiveUpdates() {
|
|
// Move the update from the Active Update list into the Past Updates list.
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
LOG("cleanupActiveUpdates - Clearing readyUpdate");
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.readyUpdate);
|
|
lazy.UM.internal.readyUpdate = null;
|
|
}
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
LOG("cleanupActiveUpdates - Clearing downloadingUpdate.");
|
|
await lazy.AUS.wrappedJSObject.cancelDownloadingUpdate();
|
|
lazy.UM.internal.addUpdateToHistory(lazy.UM.internal.downloadingUpdate);
|
|
lazy.UM.internal.downloadingUpdate = null;
|
|
}
|
|
lazy.UM.saveUpdates();
|
|
|
|
// Now trash both active update directories, since we're done with them
|
|
cleanUpReadyUpdateDir();
|
|
cleanUpDownloadingUpdateDir();
|
|
}
|
|
|
|
/**
|
|
* Writes a string of text to a file. A newline will be appended to the data
|
|
* written to the file. This function only works with ASCII text.
|
|
* @param file An nsIFile indicating what file to write to.
|
|
* @param text A string containing the text to write to the file.
|
|
* @return true on success, false on failure.
|
|
*/
|
|
function writeStringToFile(file, text) {
|
|
try {
|
|
let fos = FileUtils.openSafeFileOutputStream(file);
|
|
text += "\n";
|
|
fos.write(text, text.length);
|
|
FileUtils.closeSafeFileOutputStream(fos);
|
|
} catch (e) {
|
|
LOG(`writeStringToFile - Failed to write to file: "${file}". Error: ${e}"`);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function readStringFromInputStream(inputStream) {
|
|
var sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
|
|
Ci.nsIScriptableInputStream
|
|
);
|
|
sis.init(inputStream);
|
|
var text = sis.read(sis.available());
|
|
sis.close();
|
|
if (text && text[text.length - 1] == "\n") {
|
|
text = text.slice(0, -1);
|
|
}
|
|
return text;
|
|
}
|
|
|
|
/**
|
|
* Reads a string of text from a file. A trailing newline will be removed
|
|
* before the result is returned. This function only works with ASCII text.
|
|
*/
|
|
function readStringFromFile(file) {
|
|
if (!file.exists()) {
|
|
LOG("readStringFromFile - file doesn't exist: " + file.path);
|
|
return null;
|
|
}
|
|
var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
|
|
Ci.nsIFileInputStream
|
|
);
|
|
fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
|
|
return readStringFromInputStream(fis);
|
|
}
|
|
|
|
/**
|
|
* Attempts to recover from an update error. If successful, `true` will be
|
|
* returned and AUS.currentState will be transitioned.
|
|
*/
|
|
function handleUpdateFailure(update) {
|
|
if (WRITE_ERRORS.includes(update.errorCode)) {
|
|
let nextState = getBestPendingState();
|
|
|
|
// Check how many install attempts we have with this patch
|
|
let totalInstallAttempts =
|
|
update.selectedPatch
|
|
.QueryInterface(Ci.nsIWritablePropertyBag)
|
|
.getProperty("numTotalInstallAttempts") ?? 0;
|
|
// Out of retries, unable to handle the update failure here
|
|
if (totalInstallAttempts >= MAX_TOTAL_INSTALL_ATTEMPTS) {
|
|
return false;
|
|
}
|
|
|
|
LOG(
|
|
"handleUpdateFailure - Failure is a write error. Setting state to " +
|
|
nextState
|
|
);
|
|
writeStatusFile(getReadyUpdateDir(), (update.state = nextState));
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
return true;
|
|
}
|
|
|
|
if (update.errorCode == BACKGROUND_TASK_SHARING_VIOLATION) {
|
|
let newState = getBestPendingState();
|
|
LOG(
|
|
"handleUpdateFailure - witnessed BACKGROUND_TASK_SHARING_VIOLATION, setting state to " +
|
|
newState
|
|
);
|
|
writeStatusFile(getReadyUpdateDir(), (update.state = newState));
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
return true;
|
|
}
|
|
|
|
if (update.errorCode == SILENT_UPDATE_NEEDED_ELEVATION_ERROR) {
|
|
// There's no need to count attempts and escalate: it's expected that the
|
|
// background update task will try to update and fail due to required
|
|
// elevation repeatedly if, for example, the maintenance service is not
|
|
// available (or not functioning) and the installation requires privileges
|
|
// to update.
|
|
|
|
let bestState = getBestPendingState();
|
|
LOG(
|
|
"handleUpdateFailure - witnessed SILENT_UPDATE_NEEDED_ELEVATION_ERROR, " +
|
|
"returning to " +
|
|
bestState
|
|
);
|
|
writeStatusFile(getReadyUpdateDir(), (update.state = bestState));
|
|
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
// Return true to indicate a recoverable error.
|
|
return true;
|
|
}
|
|
|
|
if (update.errorCode == ELEVATION_CANCELED) {
|
|
let elevationAttempts = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_ELEVATE_ATTEMPTS,
|
|
0
|
|
);
|
|
elevationAttempts++;
|
|
Services.prefs.setIntPref(
|
|
PREF_APP_UPDATE_ELEVATE_ATTEMPTS,
|
|
elevationAttempts
|
|
);
|
|
let maxAttempts = Math.min(
|
|
Services.prefs.getIntPref(PREF_APP_UPDATE_ELEVATE_MAXATTEMPTS, 2),
|
|
10
|
|
);
|
|
|
|
if (elevationAttempts > maxAttempts) {
|
|
LOG(
|
|
"handleUpdateFailure - notifying observers of error. " +
|
|
"topic: update-error, status: elevation-attempts-exceeded"
|
|
);
|
|
Services.obs.notifyObservers(
|
|
update,
|
|
"update-error",
|
|
"elevation-attempts-exceeded"
|
|
);
|
|
} else {
|
|
LOG(
|
|
"handleUpdateFailure - notifying observers of error. " +
|
|
"topic: update-error, status: elevation-attempt-failed"
|
|
);
|
|
Services.obs.notifyObservers(
|
|
update,
|
|
"update-error",
|
|
"elevation-attempt-failed"
|
|
);
|
|
}
|
|
|
|
let cancelations = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS,
|
|
0
|
|
);
|
|
cancelations++;
|
|
Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations);
|
|
if (AppConstants.platform == "macosx") {
|
|
let osxCancelations = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX,
|
|
0
|
|
);
|
|
osxCancelations++;
|
|
Services.prefs.setIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX,
|
|
osxCancelations
|
|
);
|
|
let maxCancels = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
|
|
DEFAULT_CANCELATIONS_OSX_MAX
|
|
);
|
|
// Prevent the preference from setting a value greater than 5.
|
|
maxCancels = Math.min(maxCancels, 5);
|
|
if (osxCancelations >= maxCancels) {
|
|
LOG(
|
|
"handleUpdateFailure - Too many OSX cancellations. Cleaning up " +
|
|
"ready update."
|
|
);
|
|
cleanupReadyUpdate();
|
|
return false;
|
|
}
|
|
LOG(
|
|
`handleUpdateFailure - OSX cancellation. Trying again by setting ` +
|
|
`status to "${STATE_PENDING_ELEVATE}".`
|
|
);
|
|
writeStatusFile(
|
|
getReadyUpdateDir(),
|
|
(update.state = STATE_PENDING_ELEVATE)
|
|
);
|
|
update.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("elevationFailure");
|
|
} else {
|
|
const nextState = getBestPendingState();
|
|
LOG(
|
|
`handleUpdateFailure - Failure because elevation was cancelled. ` +
|
|
`Setting status to ${nextState}.`
|
|
);
|
|
writeStatusFile(getReadyUpdateDir(), (update.state = nextState));
|
|
}
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
return true;
|
|
}
|
|
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
|
|
}
|
|
|
|
if (SERVICE_ERRORS.includes(update.errorCode)) {
|
|
var failCount = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_SERVICE_ERRORS,
|
|
0
|
|
);
|
|
var maxFail = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_SERVICE_MAXERRORS,
|
|
DEFAULT_SERVICE_MAX_ERRORS
|
|
);
|
|
// Prevent the preference from setting a value greater than 10.
|
|
maxFail = Math.min(maxFail, 10);
|
|
// As a safety, when the service reaches maximum failures, it will
|
|
// disable itself and fallback to using the normal update mechanism
|
|
// without the service.
|
|
if (failCount >= maxFail) {
|
|
Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false);
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS);
|
|
} else {
|
|
failCount++;
|
|
Services.prefs.setIntPref(PREF_APP_UPDATE_SERVICE_ERRORS, failCount);
|
|
}
|
|
|
|
LOG(
|
|
"handleUpdateFailure - Got a service error. Try to update without the " +
|
|
"service by setting the state to pending."
|
|
);
|
|
writeStatusFile(getReadyUpdateDir(), (update.state = STATE_PENDING));
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
return true;
|
|
}
|
|
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ERRORS)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ERRORS);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Return the first UpdatePatch with the given type.
|
|
* @param update
|
|
* A nsIUpdate object to search through for a patch of the desired
|
|
* type.
|
|
* @param patch_type
|
|
* The type of the patch ("complete" or "partial")
|
|
* @return A nsIUpdatePatch object matching the type specified
|
|
*/
|
|
function getPatchOfType(update, patch_type) {
|
|
for (var i = 0; i < update.patchCount; ++i) {
|
|
var patch = update.getPatchAt(i);
|
|
if (patch && patch.type == patch_type) {
|
|
return patch;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Fall back to downloading a complete update in case an update has failed.
|
|
*
|
|
* This will transition `AUS.currentState` to `STATE_DOWNLOADING` if there is
|
|
* another patch to download, or `STATE_IDLE` if there is not.
|
|
*/
|
|
async function handleFallbackToCompleteUpdate() {
|
|
// If we failed to install an update, we need to fall back to a complete
|
|
// update. If the install directory has been modified, more partial updates
|
|
// will fail for the same reason. Since we only download partial updates
|
|
// while there is already an update downloaded, we don't have to check the
|
|
// downloading update, we can be confident that we are not downloading the
|
|
// right thing at the moment.
|
|
|
|
// The downloading update will be newer than the ready update, so use that
|
|
// update, if it exists.
|
|
let update =
|
|
lazy.UM.internal.downloadingUpdate || lazy.UM.internal.readyUpdate;
|
|
if (!update) {
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - Unable to find an update to fall " +
|
|
"back to."
|
|
);
|
|
return;
|
|
}
|
|
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - Cleaning up active updates in " +
|
|
"preparation of falling back to complete update."
|
|
);
|
|
await cleanupActiveUpdates();
|
|
|
|
if (!update.selectedPatch) {
|
|
// If we don't have a partial patch selected but a partial is available,
|
|
// _selectPatch() will download that instead of the complete patch.
|
|
let patch = getPatchOfType(update, "partial");
|
|
if (patch) {
|
|
patch.selected = true;
|
|
}
|
|
}
|
|
|
|
update.statusText = lazy.gUpdateBundle.GetStringFromName("patchApplyFailure");
|
|
var oldType = update.selectedPatch ? update.selectedPatch.type : "complete";
|
|
if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) {
|
|
// Partial patch application failed, try downloading the complete
|
|
// update in the background instead.
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - install of partial patch " +
|
|
"failed, downloading complete patch"
|
|
);
|
|
var result = await lazy.AUS.internal.downloadUpdate(update);
|
|
if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - Starting complete patch download " +
|
|
"failed. Cleaning up downloading patch."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
}
|
|
} else {
|
|
LOG(
|
|
"handleFallbackToCompleteUpdate - install of complete or " +
|
|
"only one patch offered failed. Notifying observers. topic: " +
|
|
"update-error, status: unknown, " +
|
|
"update.patchCount: " +
|
|
update.patchCount +
|
|
", " +
|
|
"oldType: " +
|
|
oldType
|
|
);
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
Services.obs.notifyObservers(update, "update-error", "unknown");
|
|
}
|
|
}
|
|
|
|
function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
|
|
let patchType = AUSTLMY.PATCH_UNKNOWN;
|
|
if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) {
|
|
if (aUpdate.selectedPatch.type == "complete") {
|
|
patchType = AUSTLMY.PATCH_COMPLETE;
|
|
} else if (aUpdate.selectedPatch.type == "partial") {
|
|
patchType = AUSTLMY.PATCH_PARTIAL;
|
|
}
|
|
}
|
|
|
|
let suffix = patchType + (aStartup ? AUSTLMY.STARTUP : AUSTLMY.STAGE);
|
|
let stateCode = 0;
|
|
let parts = aStatus.split(":");
|
|
if (parts.length) {
|
|
switch (parts[0]) {
|
|
case STATE_NONE:
|
|
stateCode = 2;
|
|
break;
|
|
case STATE_DOWNLOADING:
|
|
stateCode = 3;
|
|
break;
|
|
case STATE_PENDING:
|
|
stateCode = 4;
|
|
break;
|
|
case STATE_PENDING_SERVICE:
|
|
stateCode = 5;
|
|
break;
|
|
case STATE_APPLYING:
|
|
stateCode = 6;
|
|
break;
|
|
case STATE_APPLIED:
|
|
stateCode = 7;
|
|
break;
|
|
case STATE_APPLIED_SERVICE:
|
|
stateCode = 9;
|
|
break;
|
|
case STATE_SUCCEEDED:
|
|
stateCode = 10;
|
|
break;
|
|
case STATE_DOWNLOAD_FAILED:
|
|
stateCode = 11;
|
|
break;
|
|
case STATE_FAILED:
|
|
stateCode = 12;
|
|
break;
|
|
case STATE_PENDING_ELEVATE:
|
|
stateCode = 13;
|
|
break;
|
|
// Note: Do not use stateCode 14 here. It is defined in
|
|
// UpdateTelemetry.sys.mjs
|
|
default:
|
|
stateCode = 1;
|
|
}
|
|
|
|
if (parts.length > 1) {
|
|
let statusErrorCode = INVALID_UPDATER_STATE_CODE;
|
|
if (parts[0] == STATE_FAILED) {
|
|
statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
|
|
}
|
|
AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode);
|
|
}
|
|
}
|
|
AUSTLMY.pingStateCode(suffix, stateCode);
|
|
}
|
|
|
|
/**
|
|
* This returns true if the passed update is the same version or older than the
|
|
* version and build ID values passed. Otherwise it returns false.
|
|
*/
|
|
function updateIsAtLeastAsOldAs(update, version, buildID) {
|
|
if (!update || !update.appVersion || !update.buildID) {
|
|
return false;
|
|
}
|
|
let versionComparison = Services.vc.compare(update.appVersion, version);
|
|
return (
|
|
versionComparison < 0 ||
|
|
(versionComparison == 0 && update.buildID == buildID)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This returns true if the passed update is the same version or older than
|
|
* currently installed Firefox version.
|
|
*/
|
|
function updateIsAtLeastAsOldAsCurrentVersion(update) {
|
|
return updateIsAtLeastAsOldAs(
|
|
update,
|
|
Services.appinfo.version,
|
|
Services.appinfo.appBuildID
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This returns true if the passed update is the same version or older than
|
|
* the update that we have already downloaded (UpdateManager.readyUpdate).
|
|
* Returns false if no update has already been downloaded.
|
|
*/
|
|
function updateIsAtLeastAsOldAsReadyUpdate(update) {
|
|
if (
|
|
!lazy.UM.internal.readyUpdate ||
|
|
!lazy.UM.internal.readyUpdate.appVersion ||
|
|
!lazy.UM.internal.readyUpdate.buildID
|
|
) {
|
|
return false;
|
|
}
|
|
return updateIsAtLeastAsOldAs(
|
|
update,
|
|
lazy.UM.internal.readyUpdate.appVersion,
|
|
lazy.UM.internal.readyUpdate.buildID
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This function determines whether the error represented by the passed error
|
|
* code could potentially be recovered from or bypassed by updating without
|
|
* using the Maintenance Service (i.e. by showing a UAC prompt).
|
|
* We don't really want to show a UAC prompt, but it's preferable over the
|
|
* manual update doorhanger. So this function effectively distinguishes between
|
|
* which of those we should do if update staging failed. (The updater
|
|
* automatically falls back if the Maintenance Services fails, so this function
|
|
* doesn't handle that case)
|
|
*
|
|
* @param An integer error code from the update.status file. Should be one of
|
|
* the codes enumerated in updatererrors.h.
|
|
* @returns true if the code represents a Maintenance Service specific error.
|
|
* Otherwise, false.
|
|
*/
|
|
function isServiceSpecificErrorCode(errorCode) {
|
|
return (
|
|
(errorCode >= 24 && errorCode <= 33) || (errorCode >= 49 && errorCode <= 58)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* This function determines whether the error represented by the passed error
|
|
* code is the result of the updater failing to allocate memory. This is
|
|
* relevant when staging because, since Firefox is also running, we may not be
|
|
* able to allocate much memory. Thus, if we fail to stage an update, we may
|
|
* succeed at updating without staging.
|
|
*
|
|
* @param An integer error code from the update.status file. Should be one of
|
|
* the codes enumerated in updatererrors.h.
|
|
* @returns true if the code represents a memory allocation error.
|
|
* Otherwise, false.
|
|
*/
|
|
function isMemoryAllocationErrorCode(errorCode) {
|
|
return errorCode >= 10 && errorCode <= 14;
|
|
}
|
|
|
|
/**
|
|
* Normally when staging, `nsUpdateProcessor::WaitForProcess` waits for the
|
|
* staging process to complete by watching for its PID to terminate.
|
|
* However, there are less ideal situations. Notably, we might start the browser
|
|
* and find that update staging appears to already be in-progress. If that
|
|
* happens, we really want to pick up the update process from STATE_STAGING,
|
|
* but we don't really have any way of keeping an eye on the staging process
|
|
* other than to just poll the status file.
|
|
*
|
|
* Like `nsUpdateProcessor`, this calls `nsIUpdateManager.refreshUpdateStatus`
|
|
* after polling completes (regardless of result).
|
|
*
|
|
* It is also important to keep in mind that the updater might have crashed
|
|
* during staging, meaning that the status file will never change, no matter how
|
|
* long we keep polling. So we need to set an upper bound on how long we are
|
|
* willing to poll for.
|
|
*
|
|
* There are three situations that we want to avoid.
|
|
* (1) We don't want to set the poll interval too long. A user might be watching
|
|
* the user interface and waiting to restart to install the update. A long poll
|
|
* interval will cause them to have to wait longer than necessary. Especially
|
|
* since the expected total staging time is not that long.
|
|
* (2) We don't want to give up polling too early and give up on an update that
|
|
* will ultimately succeed.
|
|
* (3) We don't want to use a rapid polling interval over a long duration.
|
|
*
|
|
* To avoid these situations, we will start with a short polling interval, but
|
|
* will increase it the longer that we have to wait. Then if we hit the upper
|
|
* bound of polling, we will give up.
|
|
*/
|
|
function pollForStagingEnd() {
|
|
let pollingIntervalMs = STAGING_POLLING_MIN_INTERVAL_MS;
|
|
// Number of times to poll before increasing the polling interval.
|
|
let pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL;
|
|
let timeElapsedMs = 0;
|
|
|
|
let pollingFn = () => {
|
|
pollAttemptsAtIntervalRemaining -= 1;
|
|
// This isn't a perfectly accurate way of keeping time, but it does nicely
|
|
// sidestep dealing with issues of (non)monotonic time.
|
|
timeElapsedMs += pollingIntervalMs;
|
|
|
|
if (timeElapsedMs >= STAGING_POLLING_MAX_DURATION_MS) {
|
|
lazy.UM.internal.refreshUpdateStatus();
|
|
return;
|
|
}
|
|
|
|
if (readStatusFile(getReadyUpdateDir()) != STATE_APPLYING) {
|
|
lazy.UM.internal.refreshUpdateStatus();
|
|
return;
|
|
}
|
|
|
|
if (pollAttemptsAtIntervalRemaining <= 0) {
|
|
pollingIntervalMs = Math.min(
|
|
pollingIntervalMs * 2,
|
|
STAGING_POLLING_MAX_INTERVAL_MS
|
|
);
|
|
pollAttemptsAtIntervalRemaining = STAGING_POLLING_ATTEMPTS_PER_INTERVAL;
|
|
}
|
|
|
|
lazy.setTimeout(pollingFn, pollingIntervalMs);
|
|
};
|
|
|
|
lazy.setTimeout(pollingFn, pollingIntervalMs);
|
|
}
|
|
|
|
class UpdatePatch {
|
|
// nsIUpdatePatch attribute names used to prevent nsIWritablePropertyBag from
|
|
// over writing nsIUpdatePatch attributes.
|
|
_attrNames = [
|
|
"errorCode",
|
|
"finalURL",
|
|
"selected",
|
|
"size",
|
|
"state",
|
|
"type",
|
|
"URL",
|
|
];
|
|
|
|
/**
|
|
* @param patch
|
|
* A <patch> element to initialize this object with
|
|
* @throws if patch has a size of 0
|
|
* @constructor
|
|
*/
|
|
constructor(patch) {
|
|
this._properties = {};
|
|
this.errorCode = 0;
|
|
this.finalURL = null;
|
|
this.state = STATE_NONE;
|
|
|
|
for (let i = 0; i < patch.attributes.length; ++i) {
|
|
var attr = patch.attributes.item(i);
|
|
// If an undefined value is saved to the xml file it will be a string when
|
|
// it is read from the xml file.
|
|
if (attr.value == "undefined") {
|
|
continue;
|
|
}
|
|
switch (attr.name) {
|
|
case "xmlns":
|
|
// Don't save the XML namespace.
|
|
break;
|
|
case "selected":
|
|
this.selected = attr.value == "true";
|
|
break;
|
|
case "size":
|
|
if (0 == parseInt(attr.value)) {
|
|
LOG("UpdatePatch:init - 0-sized patch!");
|
|
throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
|
|
}
|
|
this[attr.name] = attr.value;
|
|
break;
|
|
case "errorCode":
|
|
if (attr.value) {
|
|
let val = parseInt(attr.value);
|
|
// This will evaluate to false if the value is 0 but that's ok since
|
|
// this.errorCode is set to the default of 0 above.
|
|
if (val) {
|
|
this.errorCode = val;
|
|
}
|
|
}
|
|
break;
|
|
case "finalURL":
|
|
case "state":
|
|
case "type":
|
|
case "URL":
|
|
this[attr.name] = attr.value;
|
|
break;
|
|
default:
|
|
if (!this._attrNames.includes(attr.name)) {
|
|
// Set nsIPropertyBag properties that were read from the xml file.
|
|
this.setProperty(attr.name, attr.value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
serialize(updates) {
|
|
var patch = updates.createElementNS(URI_UPDATE_NS, "patch");
|
|
patch.setAttribute("size", this.size);
|
|
patch.setAttribute("type", this.type);
|
|
patch.setAttribute("URL", this.URL);
|
|
// Don't write an errorCode if it evaluates to false since 0 is the same as
|
|
// no error code.
|
|
if (this.errorCode) {
|
|
patch.setAttribute("errorCode", this.errorCode);
|
|
}
|
|
// finalURL is not available until after the download has started
|
|
if (this.finalURL) {
|
|
patch.setAttribute("finalURL", this.finalURL);
|
|
}
|
|
// The selected patch is the only patch that should have this attribute.
|
|
if (this.selected) {
|
|
patch.setAttribute("selected", this.selected);
|
|
}
|
|
if (this.state != STATE_NONE) {
|
|
patch.setAttribute("state", this.state);
|
|
}
|
|
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
patch.setAttribute(name, value.data);
|
|
}
|
|
}
|
|
return patch;
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
setProperty(name, value) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdatePatch) " +
|
|
"when calling method: [nsIWritablePropertyBag::setProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
this._properties[name] = { data: value, present: true };
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
deleteProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdatePatch) " +
|
|
"when calling method: [nsIWritablePropertyBag::deleteProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties) {
|
|
this._properties[name].present = false;
|
|
} else {
|
|
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
*
|
|
* Note: this only contains the nsIPropertyBag name / value pairs and not the
|
|
* nsIUpdatePatch name / value pairs.
|
|
*/
|
|
get enumerator() {
|
|
return this.enumerate();
|
|
}
|
|
|
|
*enumerate() {
|
|
// An nsISupportsInterfacePointer is used so creating an array using
|
|
// Array.from will retain the QueryInterface for nsIProperty.
|
|
let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
|
|
Ci.nsISupportsInterfacePointer
|
|
);
|
|
let qi = ChromeUtils.generateQI([Ci.nsIProperty]);
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
// The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
|
|
// elements are nsIProperty objects. Calling QueryInterface for
|
|
// nsIProperty on the object doesn't return to the caller an object that
|
|
// is already queried to nsIProperty but do it just in case it is fixed
|
|
// at some point.
|
|
ip.data = { name, value: value.data, QueryInterface: qi };
|
|
yield ip.data.QueryInterface(Ci.nsIProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
*
|
|
* Note: returns null instead of throwing when the property doesn't exist to
|
|
* simplify code and to silence warnings in debug builds.
|
|
*/
|
|
getProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdatePatch) " +
|
|
"when calling method: [nsIWritablePropertyBag::getProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties && this._properties[name].present) {
|
|
return this._properties[name].data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIUpdatePatch,
|
|
Ci.nsIPropertyBag,
|
|
Ci.nsIWritablePropertyBag,
|
|
]);
|
|
}
|
|
|
|
class Update {
|
|
// nsIUpdate attribute names used to prevent nsIWritablePropertyBag from over
|
|
// writing nsIUpdate attributes.
|
|
_attrNames = [
|
|
"appVersion",
|
|
"buildID",
|
|
"channel",
|
|
"detailsURL",
|
|
"displayVersion",
|
|
"elevationFailure",
|
|
"errorCode",
|
|
"installDate",
|
|
"isCompleteUpdate",
|
|
"name",
|
|
"previousAppVersion",
|
|
"promptWaitTime",
|
|
"serviceURL",
|
|
"state",
|
|
"statusText",
|
|
"type",
|
|
"unsupported",
|
|
"platformVersion",
|
|
];
|
|
|
|
/**
|
|
* Implements nsIUpdate
|
|
* @param update
|
|
* An <update> element to initialize this object with
|
|
* @throws if the update contains no patches
|
|
* @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":
|
|
case "platformVersion":
|
|
this[attr.name] = attr.value;
|
|
break;
|
|
default:
|
|
if (!this._attrNames.includes(attr.name)) {
|
|
// Set nsIPropertyBag properties that were read from the xml file.
|
|
this.setProperty(attr.name, attr.value);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!this.previousAppVersion) {
|
|
this.previousAppVersion = 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("platformVersion", this.platformVersion);
|
|
update.setAttribute("installDate", this.installDate);
|
|
update.setAttribute("isCompleteUpdate", this.isCompleteUpdate);
|
|
update.setAttribute("name", this.name);
|
|
update.setAttribute("previousAppVersion", this.previousAppVersion);
|
|
update.setAttribute("promptWaitTime", this.promptWaitTime);
|
|
update.setAttribute("serviceURL", this.serviceURL);
|
|
update.setAttribute("type", this.type);
|
|
|
|
if (this.statusText) {
|
|
update.setAttribute("statusText", this.statusText);
|
|
}
|
|
if (this.unsupported) {
|
|
update.setAttribute("unsupported", this.unsupported);
|
|
}
|
|
if (this.elevationFailure) {
|
|
update.setAttribute("elevationFailure", this.elevationFailure);
|
|
}
|
|
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
update.setAttribute(name, value.data);
|
|
}
|
|
}
|
|
|
|
for (let i = 0; i < this.patchCount; ++i) {
|
|
update.appendChild(this.getPatchAt(i).serialize(updates));
|
|
}
|
|
|
|
updates.documentElement.appendChild(update);
|
|
return update;
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
setProperty(name, value) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdate) " +
|
|
"when calling method: [nsIWritablePropertyBag::setProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
this._properties[name] = { data: value, present: true };
|
|
}
|
|
|
|
/**
|
|
* See nsIWritablePropertyBag.idl
|
|
*/
|
|
deleteProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdate) " +
|
|
"when calling method: [nsIWritablePropertyBag::deleteProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties) {
|
|
this._properties[name].present = false;
|
|
} else {
|
|
throw Components.Exception("", Cr.NS_ERROR_FAILURE);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
*
|
|
* Note: this only contains the nsIPropertyBag name value / pairs and not the
|
|
* nsIUpdate name / value pairs.
|
|
*/
|
|
get enumerator() {
|
|
return this.enumerate();
|
|
}
|
|
|
|
*enumerate() {
|
|
// An nsISupportsInterfacePointer is used so creating an array using
|
|
// Array.from will retain the QueryInterface for nsIProperty.
|
|
let ip = Cc["@mozilla.org/supports-interface-pointer;1"].createInstance(
|
|
Ci.nsISupportsInterfacePointer
|
|
);
|
|
let qi = ChromeUtils.generateQI([Ci.nsIProperty]);
|
|
for (let [name, value] of Object.entries(this._properties)) {
|
|
if (value.present && !this._attrNames.includes(name)) {
|
|
// The nsIPropertyBag enumerator returns a nsISimpleEnumerator whose
|
|
// elements are nsIProperty objects. Calling QueryInterface for
|
|
// nsIProperty on the object doesn't return to the caller an object that
|
|
// is already queried to nsIProperty but do it just in case it is fixed
|
|
// at some point.
|
|
ip.data = { name, value: value.data, QueryInterface: qi };
|
|
yield ip.data.QueryInterface(Ci.nsIProperty);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIPropertyBag.idl
|
|
* Note: returns null instead of throwing when the property doesn't exist to
|
|
* simplify code and to silence warnings in debug builds.
|
|
*/
|
|
getProperty(name) {
|
|
if (this._attrNames.includes(name)) {
|
|
throw Components.Exception(
|
|
"Illegal value '" +
|
|
name +
|
|
"' (attribute exists on nsIUpdate) " +
|
|
"when calling method: [nsIWritablePropertyBag::getProperty]",
|
|
Cr.NS_ERROR_ILLEGAL_VALUE
|
|
);
|
|
}
|
|
if (name in this._properties && this._properties[name].present) {
|
|
return this._properties[name].data;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIUpdate,
|
|
Ci.nsIPropertyBag,
|
|
Ci.nsIWritablePropertyBag,
|
|
]);
|
|
}
|
|
|
|
export class UpdateService {
|
|
#initPromise;
|
|
|
|
/**
|
|
* The downloader we are using to download updates. There is only ever one of
|
|
* these.
|
|
*/
|
|
_downloader = null;
|
|
|
|
/**
|
|
* Whether or not the service registered the "online" observer.
|
|
*/
|
|
_registeredOnlineObserver = false;
|
|
|
|
/**
|
|
* The current number of consecutive socket errors
|
|
*/
|
|
_consecutiveSocketErrors = 0;
|
|
|
|
/**
|
|
* A timer used to retry socket errors
|
|
*/
|
|
_retryTimer = null;
|
|
|
|
/**
|
|
* Whether or not a background update check was initiated by the
|
|
* application update timer notification.
|
|
*/
|
|
_isNotify = true;
|
|
|
|
/**
|
|
* UpdateService
|
|
* A Service for managing the discovery and installation of software updates.
|
|
* @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 (force = false) => this.#init(force),
|
|
downloadUpdate: async update => this.#downloadUpdate(update),
|
|
stopDownload: async () => this.#stopDownload(),
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
Ci.nsIApplicationUpdateServiceInternal,
|
|
]),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async init() {
|
|
await lazy.UpdateServiceStub.initUpdate();
|
|
}
|
|
|
|
/**
|
|
* See nsIApplicationUpdateServiceInternal.init in nsIUpdateService.idl
|
|
* and `#asyncInit`.
|
|
*/
|
|
async #init(force = false) {
|
|
if (force) {
|
|
return this.#asyncInit();
|
|
}
|
|
if (!this.#initPromise) {
|
|
this.#initPromise = this.#asyncInit();
|
|
}
|
|
|
|
return this.#initPromise;
|
|
}
|
|
|
|
/**
|
|
* Handle Observer Service notifications
|
|
* @param subject
|
|
* The subject of the notification
|
|
* @param topic
|
|
* The notification name
|
|
* @param data
|
|
* Additional data
|
|
*/
|
|
async observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "network:offline-status-changed":
|
|
await this._offlineStatusChanged(data);
|
|
break;
|
|
case "quit-application":
|
|
Services.obs.removeObserver(this, topic);
|
|
|
|
if (lazy.UpdateMutex.isLocked()) {
|
|
// If we hold the update mutex, let it go!
|
|
// The OS would clean this up sometime after shutdown,
|
|
// but that would have no guarantee on timing.
|
|
lazy.UpdateMutex.unlock();
|
|
}
|
|
if (this._retryTimer) {
|
|
this._retryTimer.cancel();
|
|
}
|
|
|
|
// When downloading an update with nsIIncrementalDownload the download
|
|
// is stopped when the quit-application observer notification is
|
|
// received and networking hasn't started to shutdown. The download will
|
|
// be resumed the next time the application starts. Downloads using
|
|
// Windows BITS are not stopped since they don't require Firefox to be
|
|
// running to perform the download.
|
|
if (this._downloader) {
|
|
if (this._downloader.usingBits) {
|
|
await this._downloader.cleanup();
|
|
} else {
|
|
// stopDownload() calls _downloader.cleanup()
|
|
await this.#stopDownload();
|
|
}
|
|
}
|
|
// Prevent leaking the downloader (bug 454964)
|
|
this._downloader = null;
|
|
// In case any update checks are in progress.
|
|
lazy.CheckSvc.stopAllChecks();
|
|
break;
|
|
case "test-unlock-update-mutex":
|
|
if (Cu.isInAutomation) {
|
|
if (lazy.UpdateMutex.isLocked()) {
|
|
LOG("UpdateService:observe - releasing update mutex for testing");
|
|
lazy.UpdateMutex.unlock();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This is effectively the initialization function for `UpdateService`.
|
|
*
|
|
* Bug 1900717 - This currently returns immediately if we don't hold the
|
|
* update mutex when we initialize and it never runs again, even
|
|
* if we eventually do get the update mutex.
|
|
*
|
|
* The initialization process basically involves doing any of these steps that
|
|
* are currently relevant:
|
|
*
|
|
* Make sure we are in a good state
|
|
* Reset things and clean up if information in different files conflicts.
|
|
*
|
|
* Show the update elevation dialog
|
|
* When we need to elevate our privileges to update on macOS, we show a
|
|
* dialog to ask the user first. We show that now and if they accept it,
|
|
* they are updated on the next application launch.
|
|
*
|
|
* Post update processing
|
|
* Move installed updates into the history. Cleanup.
|
|
*
|
|
* Resume in-progress download
|
|
* For BITS, this really just means connecting to the BITS manager.
|
|
* For the internal downloader, we actually need to start the download
|
|
* again.
|
|
*
|
|
* Error fallback
|
|
* If an in-progress update failed in some way, we try to fall back. This
|
|
* could include retrying the installation differently next time,
|
|
* downloading a new MAR, or asking the user to download and run a new
|
|
* installer.
|
|
*/
|
|
/* eslint-disable-next-line complexity */
|
|
async #asyncInit() {
|
|
// This check also ensures we have the update mutex
|
|
if (!this.canCheckForUpdates) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - unable to check for " +
|
|
"updates... returning early"
|
|
);
|
|
return;
|
|
}
|
|
const readyUpdateDir = getReadyUpdateDir();
|
|
let status = readStatusFile(readyUpdateDir);
|
|
let statusParts = status.split(":");
|
|
status = statusParts[0];
|
|
LOG(`UpdateService:#asyncInit - status = "${status}"`);
|
|
if (!this.canUsuallyApplyUpdates) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - unable to apply " +
|
|
"updates... returning early"
|
|
);
|
|
// If the update is present in the update directory somehow,
|
|
// it would prevent us from notifying the user of further updates.
|
|
LOG("UpdateService:#asyncInit - Cleaning up active updates.");
|
|
await cleanupActiveUpdates();
|
|
return;
|
|
}
|
|
|
|
let updates = [];
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
updates.push(lazy.UM.internal.readyUpdate);
|
|
}
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
updates.push(lazy.UM.internal.downloadingUpdate);
|
|
}
|
|
|
|
// Validate the update state. It's too complicated to recover much of
|
|
// anything if the state isn't already correct. And blowing away all the
|
|
// update state is a good way to make sure we always start the update loop
|
|
// in a reasonably consistent state. If we do this, we can return early
|
|
// since the rest of the function deals with further validation and handling
|
|
// of update success and failure.
|
|
const resetUpdateState = async (missingUpdateObject = false) => {
|
|
LOG("UpdateService:#asyncInit - Resetting update state");
|
|
|
|
// If we are clearing away some update data and we don't actually have a
|
|
// corresponding update object to write into the update history, just
|
|
// make an empty one.
|
|
if (missingUpdateObject && !updates.length) {
|
|
if (!updates.length) {
|
|
updates.push(new Update(null));
|
|
}
|
|
}
|
|
|
|
// We are about to clean up any updates that we have, so if we do have
|
|
// any, mark them as having failed.
|
|
if (updates.length) {
|
|
for (let update of updates) {
|
|
update.state = STATE_FAILED;
|
|
update.errorCode = ERR_UPDATE_STATE_NONE;
|
|
update.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("statusFailed");
|
|
}
|
|
let newStatus = STATE_FAILED + ": " + ERR_UPDATE_STATE_NONE;
|
|
pingStateAndStatusCodes(updates[0], true, newStatus);
|
|
}
|
|
|
|
await cleanupActiveUpdates();
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
};
|
|
|
|
switch (status) {
|
|
case STATE_DOWNLOADING:
|
|
if (!lazy.UM.internal.downloadingUpdate) {
|
|
LOG("UpdateService:#asyncInit - Missing downloading update object");
|
|
await resetUpdateState(true);
|
|
return;
|
|
} else if (lazy.UM.internal.readyUpdate) {
|
|
// It isn't valid to have a ready update while in the downloading
|
|
// state. We could just clear out the ready update and continue with
|
|
// the downloading update, but if the state is inconsistent, we'd
|
|
// prefer to start from scratch rather than try to rescue it
|
|
LOG("UpdateService:#asyncInit - Unexpected ready update object");
|
|
await resetUpdateState(true);
|
|
return;
|
|
}
|
|
|
|
// Note that we don't check for an existing downloading update MAR
|
|
// because there are good states where it does not exist (ex. BITS or
|
|
// the transfer just hasn't started yet).
|
|
break;
|
|
case STATE_PENDING:
|
|
case STATE_PENDING_SERVICE:
|
|
case STATE_PENDING_ELEVATE:
|
|
case STATE_APPLYING:
|
|
case STATE_APPLIED:
|
|
case STATE_APPLIED_SERVICE: {
|
|
let readyMarFile = readyUpdateDir.clone();
|
|
readyMarFile.append(FILE_UPDATE_MAR);
|
|
|
|
if (!lazy.UM.internal.readyUpdate) {
|
|
LOG("UpdateService:#asyncInit - Missing ready update object");
|
|
await resetUpdateState(true);
|
|
return;
|
|
} else if (!readyMarFile.exists()) {
|
|
LOG("UpdateService:#asyncInit - Missing mar file");
|
|
await resetUpdateState();
|
|
return;
|
|
}
|
|
break;
|
|
}
|
|
case STATE_SUCCEEDED:
|
|
case STATE_FAILED:
|
|
// There is more handing and validation to be done in this state, so
|
|
// we never want to return early here or lose any of the available state
|
|
// information, even if it is inconsistent.
|
|
break;
|
|
case STATE_DOWNLOAD_FAILED:
|
|
// This is an odd state to start up in since we usually handle this
|
|
// situation right away. We'll just clean this state up since the risk
|
|
// of whatever state is still hanging around interfering with update
|
|
// seems higher than the possible benefit of being able to salvage some
|
|
// partial transfer.
|
|
await resetUpdateState(!lazy.UM.internal.downloadingUpdate);
|
|
return;
|
|
case STATE_NONE: {
|
|
const statusFile = readyUpdateDir.clone();
|
|
statusFile.append(FILE_UPDATE_STATUS);
|
|
// This file existing but not having a state in it is unexpected.
|
|
const statusFileExists = statusFile.exists();
|
|
if (statusFileExists) {
|
|
LOG("UpdateService:#asyncInit - Status file is empty?");
|
|
}
|
|
await resetUpdateState(statusFileExists);
|
|
return;
|
|
}
|
|
default:
|
|
LOG(
|
|
`UpdateService:#asyncInit - Unexpected state! ${status}) - assuming no valid updates`
|
|
);
|
|
await resetUpdateState();
|
|
return;
|
|
}
|
|
|
|
let channelChanged = updatesToCheck => {
|
|
for (let update of updatesToCheck) {
|
|
if (update.channel != lazy.UpdateUtils.UpdateChannel) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
if (channelChanged(updates)) {
|
|
let channel = lazy.UM.internal.readyUpdate
|
|
? lazy.UM.internal.readyUpdate.channel
|
|
: lazy.UM.internal.downloadingUpdate.channel;
|
|
LOG(
|
|
"UpdateService:#asyncInit - update channel is " +
|
|
"different than application's channel, removing update. update " +
|
|
"channel: " +
|
|
channel +
|
|
", expected channel: " +
|
|
lazy.UpdateUtils.UpdateChannel
|
|
);
|
|
// User switched channels, clear out the old active updates and remove
|
|
// partial downloads
|
|
for (let update of updates) {
|
|
update.state = STATE_FAILED;
|
|
update.errorCode = ERR_CHANNEL_CHANGE;
|
|
update.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("statusFailed");
|
|
}
|
|
let newStatus = STATE_FAILED + ": " + ERR_CHANNEL_CHANGE;
|
|
pingStateAndStatusCodes(updates[0], true, newStatus);
|
|
await cleanupActiveUpdates();
|
|
return;
|
|
}
|
|
|
|
// Handle the case when the update is the same or older than the current
|
|
// version and nsUpdateDriver.cpp skipped updating due to the version being
|
|
// older than the current version. This also handles the general case when
|
|
// an update is for an older version or the same version and same build ID.
|
|
if (
|
|
status == STATE_PENDING ||
|
|
status == STATE_PENDING_SERVICE ||
|
|
status == STATE_APPLIED ||
|
|
status == STATE_APPLIED_SERVICE ||
|
|
status == STATE_PENDING_ELEVATE ||
|
|
status == STATE_DOWNLOADING
|
|
) {
|
|
let tooOldUpdate;
|
|
if (
|
|
updateIsAtLeastAsOldAs(
|
|
lazy.UM.internal.readyUpdate,
|
|
Services.appinfo.version,
|
|
Services.appinfo.appBuildID
|
|
)
|
|
) {
|
|
tooOldUpdate = lazy.UM.internal.readyUpdate;
|
|
} else if (
|
|
updateIsAtLeastAsOldAs(
|
|
lazy.UM.internal.downloadingUpdate,
|
|
Services.appinfo.version,
|
|
Services.appinfo.appBuildID
|
|
)
|
|
) {
|
|
tooOldUpdate = lazy.UM.internal.downloadingUpdate;
|
|
}
|
|
if (tooOldUpdate) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - removing update for older " +
|
|
"application version or same application version with same build " +
|
|
"ID. update application version: " +
|
|
tooOldUpdate.appVersion +
|
|
", " +
|
|
"application version: " +
|
|
Services.appinfo.version +
|
|
", update " +
|
|
"build ID: " +
|
|
tooOldUpdate.buildID +
|
|
", application build ID: " +
|
|
Services.appinfo.appBuildID
|
|
);
|
|
tooOldUpdate.state = STATE_FAILED;
|
|
tooOldUpdate.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("statusFailed");
|
|
tooOldUpdate.errorCode = ERR_OLDER_VERSION_OR_SAME_BUILD;
|
|
// This could be split out to report telemetry for each case.
|
|
let newStatus = STATE_FAILED + ": " + ERR_OLDER_VERSION_OR_SAME_BUILD;
|
|
pingStateAndStatusCodes(tooOldUpdate, true, newStatus);
|
|
// Cleanup both updates regardless of which one is too old. It's
|
|
// exceedingly unlikely that a user could end up in a state where one
|
|
// update is acceptable and the other isn't. And it makes this function
|
|
// considerably more complex to try to deal with that possibility.
|
|
await cleanupActiveUpdates();
|
|
return;
|
|
}
|
|
}
|
|
|
|
pingStateAndStatusCodes(
|
|
status == STATE_DOWNLOADING
|
|
? lazy.UM.internal.downloadingUpdate
|
|
: lazy.UM.internal.readyUpdate,
|
|
true,
|
|
status
|
|
);
|
|
if (lazy.UM.internal.downloadingUpdate || status == STATE_DOWNLOADING) {
|
|
if (status == STATE_SUCCEEDED) {
|
|
// If we successfully installed an update while we were downloading
|
|
// another update, the downloading update is now a partial MAR for
|
|
// a version that is no longer installed. We know that it's a partial
|
|
// MAR without checking because we currently only download partial MARs
|
|
// when an update has already been downloaded.
|
|
LOG(
|
|
"UpdateService:#asyncInit - removing downloading patch " +
|
|
"because we installed a different patch before it finished" +
|
|
"downloading."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
} else {
|
|
// Attempt to resume download
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - resuming patch found in " +
|
|
"downloading state"
|
|
);
|
|
let result = await this.#downloadUpdate(
|
|
lazy.UM.internal.downloadingUpdate
|
|
);
|
|
if (
|
|
result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS &&
|
|
result !=
|
|
Ci.nsIApplicationUpdateService
|
|
.DOWNLOAD_FAILURE_CANNOT_RESUME_IN_BACKGROUND
|
|
) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - Failed to resume patch. " +
|
|
"Cleaning up downloading update."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
}
|
|
} else {
|
|
LOG(
|
|
"UpdateService:#asyncInit - Warning: found " +
|
|
"downloading state, but no downloading patch. Cleaning up " +
|
|
"active updates."
|
|
);
|
|
// Put ourselves back in a good state.
|
|
await cleanupActiveUpdates();
|
|
}
|
|
if (status == STATE_DOWNLOADING) {
|
|
// Done dealing with the downloading update, and there is no ready
|
|
// update, so return early.
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
let update = lazy.UM.internal.readyUpdate;
|
|
|
|
if (status == STATE_APPLYING) {
|
|
// This indicates that the background updater service is in either of the
|
|
// following two states:
|
|
// 1. It is in the process of applying an update in the background, and
|
|
// we just happen to be racing against that.
|
|
// 2. It has failed to apply an update for some reason, and we hit this
|
|
// case because the updater process has set the update status to
|
|
// applying, but has never finished.
|
|
// In order to differentiate between these two states, we look at the
|
|
// state field of the update object. If it's "pending", then we know
|
|
// that this is the first time we're hitting this case, so we switch
|
|
// that state to "applying" and we just wait and hope for the best.
|
|
// If it's "applying", we know that we've already been here once, so
|
|
// we really want to start from a clean state.
|
|
if (
|
|
update &&
|
|
(update.state == STATE_PENDING || update.state == STATE_PENDING_SERVICE)
|
|
) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - patch found in applying " +
|
|
"state for the first time"
|
|
);
|
|
update.state = STATE_APPLYING;
|
|
lazy.UM.saveUpdates();
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING);
|
|
pollForStagingEnd();
|
|
} else {
|
|
// We get here even if we don't have an update object
|
|
LOG(
|
|
"UpdateService:#asyncInit - patch found in applying " +
|
|
"state for the second time. Cleaning up ready update."
|
|
);
|
|
cleanupReadyUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!update) {
|
|
if (status != STATE_SUCCEEDED) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - previous patch failed " +
|
|
"and no patch available. Cleaning up ready update."
|
|
);
|
|
cleanupReadyUpdate();
|
|
return;
|
|
}
|
|
LOG(
|
|
"UpdateService:#asyncInit - Update data missing. Creating " +
|
|
"an empty update object."
|
|
);
|
|
update = new Update(null);
|
|
}
|
|
|
|
update.state = status;
|
|
LOG(
|
|
`UpdateService:#asyncInit - Setting update's state from ` +
|
|
`the status file (="${update.state}")`
|
|
);
|
|
if (update.state == STATE_FAILED && statusParts[1]) {
|
|
update.errorCode = parseInt(statusParts[1]);
|
|
LOG(
|
|
`UpdateService:#asyncInit - Setting update's errorCode ` +
|
|
`from the status file (="${update.errorCode}")`
|
|
);
|
|
}
|
|
|
|
if (status != STATE_SUCCEEDED) {
|
|
// If there are new update logs, rotate them to ensure none are ever
|
|
// overwritten. By passing `false` the patch directory won't be removed.
|
|
cleanUpReadyUpdateDir(false);
|
|
}
|
|
|
|
// Track number of installation attempts for this patch
|
|
if (
|
|
update.selectedPatch &&
|
|
[STATE_SUCCEEDED, STATE_FAILED].includes(status)
|
|
) {
|
|
let totalInstallAttempts =
|
|
update.selectedPatch
|
|
.QueryInterface(Ci.nsIWritablePropertyBag)
|
|
.getProperty("numTotalInstallAttempts") ?? 0;
|
|
|
|
update.selectedPatch
|
|
.QueryInterface(Ci.nsIWritablePropertyBag)
|
|
.setProperty("numTotalInstallAttempts", totalInstallAttempts + 1);
|
|
}
|
|
|
|
if (status == STATE_SUCCEEDED) {
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS);
|
|
}
|
|
update.statusText =
|
|
lazy.gUpdateBundle.GetStringFromName("installSuccess");
|
|
|
|
// The only time that update is not a reference to readyUpdate is when
|
|
// readyUpdate is null.
|
|
if (!lazy.UM.internal.readyUpdate) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - Assigning successful update " +
|
|
"readyUpdate before cleaning it up."
|
|
);
|
|
lazy.UM.internal.readyUpdate = update;
|
|
}
|
|
|
|
// Done with this update. Clean it up.
|
|
LOG("UpdateService:#asyncInit - Cleaning up successful ready update.");
|
|
cleanupReadyUpdate();
|
|
|
|
Services.prefs.setIntPref(PREF_APP_UPDATE_ELEVATE_ATTEMPTS, 0);
|
|
} else if (status == STATE_PENDING_ELEVATE) {
|
|
// In case the active-update.xml file is deleted.
|
|
if (!update) {
|
|
LOG(
|
|
"UpdateService:#asyncInit - status is pending-elevate " +
|
|
"but there isn't a ready update, removing update"
|
|
);
|
|
cleanupReadyUpdate();
|
|
} else {
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
if (Services.startup.wasSilentlyStarted) {
|
|
// This check _should_ be unnecessary since we should not silently
|
|
// restart if state == pending-elevate. But the update elevation
|
|
// dialog is a way that we could potentially show UI on startup, even
|
|
// with no windows open. Which we really do not want to do on a silent
|
|
// restart.
|
|
// So this is defense in depth.
|
|
LOG(
|
|
"UpdateService:#asyncInit - status is " +
|
|
"pending-elevate, but this is a silent startup, so the " +
|
|
"elevation window has been suppressed."
|
|
);
|
|
} else {
|
|
LOG(
|
|
"UpdateService:#asyncInit - status is " +
|
|
"pending-elevate. Showing Update elevation dialog."
|
|
);
|
|
let uri = "chrome://mozapps/content/update/updateElevation.xhtml";
|
|
let features =
|
|
"chrome,centerscreen,resizable=no,titlebar,toolbar=no,dialog=no";
|
|
Services.ww.openWindow(null, uri, "Update:Elevation", features, null);
|
|
}
|
|
}
|
|
} else if (
|
|
status == STATE_PENDING ||
|
|
status == STATE_PENDING_SERVICE ||
|
|
status == STATE_APPLIED ||
|
|
status == STATE_APPLIED_SERVICE
|
|
) {
|
|
// We started up with an update already pending. Usually we apply updates
|
|
// at startup, but there are some cases where we do not. Most likely, we
|
|
// will just end up installing this update the next time we start up, but
|
|
// we should make sure that the pending update looks valid since it may
|
|
// prevent us from downloading a new one in the meantime.
|
|
LOG("UpdateService:#asyncInit - Verifying existing pending update.");
|
|
// The only things the updater binary really needs to update are
|
|
// `update.status` (which we already read `status` from) and `update.mar`.
|
|
let marFile = readyUpdateDir.clone();
|
|
marFile.append(FILE_UPDATE_MAR);
|
|
if (!marFile.exists()) {
|
|
LOG("UpdateService:#asyncInit - Cleaning up missing pending update.");
|
|
cleanupReadyUpdate();
|
|
}
|
|
} else {
|
|
// If there was an I/O error it is assumed that the patch is not invalid
|
|
// and it is set to pending so an attempt to apply it again will happen
|
|
// when the application is restarted.
|
|
if (update.state == STATE_FAILED && update.errorCode) {
|
|
LOG("UpdateService:#asyncInit - Attempting handleUpdateFailure");
|
|
if (handleUpdateFailure(update)) {
|
|
LOG("UpdateService:#asyncInit - handleUpdateFailure success.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
LOG(
|
|
"UpdateService:#asyncInit - Attempting to fall back to a " +
|
|
"complete update."
|
|
);
|
|
// Something went wrong with the patch application process.
|
|
await handleFallbackToCompleteUpdate();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register an observer when the network comes online, so we can short-circuit
|
|
* the app.update.interval when there isn't connectivity
|
|
*/
|
|
_registerOnlineObserver() {
|
|
if (this._registeredOnlineObserver) {
|
|
LOG(
|
|
"UpdateService:_registerOnlineObserver - observer already registered"
|
|
);
|
|
return;
|
|
}
|
|
|
|
LOG(
|
|
"UpdateService:_registerOnlineObserver - waiting for the network to " +
|
|
"be online, then forcing another check"
|
|
);
|
|
|
|
Services.obs.addObserver(this, "network:offline-status-changed");
|
|
this._registeredOnlineObserver = true;
|
|
}
|
|
|
|
/**
|
|
* Called from the network:offline-status-changed observer.
|
|
*/
|
|
async _offlineStatusChanged(status) {
|
|
if (status !== "online") {
|
|
return;
|
|
}
|
|
|
|
Services.obs.removeObserver(this, "network:offline-status-changed");
|
|
this._registeredOnlineObserver = false;
|
|
|
|
LOG(
|
|
"UpdateService:_offlineStatusChanged - network is online, forcing " +
|
|
"another background check"
|
|
);
|
|
|
|
// the background checker is contained in notify
|
|
await this._attemptResume();
|
|
}
|
|
|
|
/**
|
|
* 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."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Kick off an update check
|
|
(async () => {
|
|
let check = lazy.CheckSvc.internal.checkForUpdates(
|
|
lazy.CheckSvc.BACKGROUND_CHECK
|
|
);
|
|
await this.onCheckComplete(await check.result);
|
|
})();
|
|
}
|
|
|
|
/**
|
|
* Notified when a timer fires
|
|
* @param _timer
|
|
* The timer that fired
|
|
*/
|
|
async notify(_timer) {
|
|
await this._checkForBackgroundUpdates(true);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async checkForBackgroundUpdates() {
|
|
return this._checkForBackgroundUpdates(false);
|
|
}
|
|
|
|
// The suffix used for background update check glean metric names.
|
|
get _pingSuffix() {
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
// Once an update has been downloaded, all later updates will be reported
|
|
// to telemetry as subsequent updates. We move the first update into
|
|
// readyUpdate as soon as the download is complete, so any update checks
|
|
// after readyUpdate is no longer null are subsequent update checks.
|
|
return AUSTLMY.SUBSEQUENT;
|
|
}
|
|
return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL;
|
|
}
|
|
|
|
/**
|
|
* Checks for updates in the background.
|
|
* @param isNotify
|
|
* Whether or not a background update check was initiated by the
|
|
* application update timer notification.
|
|
*/
|
|
async _checkForBackgroundUpdates(isNotify) {
|
|
await this.init();
|
|
|
|
if (!this.disabled && AppConstants.NIGHTLY_BUILD) {
|
|
// Scalar ID: update.suppress_prompts
|
|
AUSTLMY.pingSuppressPrompts();
|
|
}
|
|
if (this.disabled || this.manualUpdateOnly) {
|
|
// Return immediately if we are disabled by policy. Otherwise, just the
|
|
// telemetry we try to collect below can potentially trigger a restart
|
|
// prompt if the update directory isn't writable. And we shouldn't be
|
|
// telling the user about update failures if update is disabled.
|
|
// See Bug 1599590.
|
|
// Note that we exit unconditionally here if we are only doing manual
|
|
// update checks, because manual update checking uses a completely
|
|
// different code path (AppUpdater.sys.mjs creates its own nsIUpdateChecker),
|
|
// bypassing this function completely.
|
|
if (!this.disabledForTesting) {
|
|
// This can cause some problems for tests that aren't setup properly for
|
|
// this.
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
this._isNotify = isNotify;
|
|
|
|
// Glean metric names:
|
|
// Glean.update.pingCountExternal
|
|
// Glean.update.pingCountNotify
|
|
// Glean.update.pingCountSubsequent
|
|
Glean.update["pingCount" + this._pingSuffix].add();
|
|
|
|
// Glean metric names:
|
|
// Glean.update.unableToApplyExternal
|
|
// Glean.update.unableToApplyNotify
|
|
// Glean.update.unableToApplySubsequent
|
|
if (!getCanApplyUpdates()) {
|
|
Glean.update["unableToApply" + this._pingSuffix].add();
|
|
}
|
|
|
|
// Glean metric names:
|
|
// Glean.update.cannotStageExternal
|
|
// Glean.update.cannotStageNotify
|
|
// Glean.update.cannotStageSubsequent
|
|
if (!getCanApplyUpdates()) {
|
|
Glean.update["cannotStage" + this._pingSuffix].add();
|
|
}
|
|
if (AppConstants.platform == "win") {
|
|
// labeled counter metric names:
|
|
// Glean.update.canUseBitsExternal
|
|
// Glean.update.canUseBitsNotify
|
|
// Glean.update.canUseBitsSubsequent
|
|
Glean.update["canUseBits" + this._pingSuffix][getCanUseBits()].add();
|
|
}
|
|
// Glean metric names:
|
|
// Glean.update.invalidLastupdatetimeExternal
|
|
// Glean.update.invalidLastupdatetimeNotify
|
|
// Glean.update.invalidLastupdatetimeSubsequent
|
|
// Glean.update.lastNotifyIntervalDaysExternal
|
|
// Glean.update.lastNotifyIntervalDaysNotify
|
|
// Glean.update.lastNotifyIntervalDaysSubsequent
|
|
AUSTLMY.pingLastUpdateTime(this._pingSuffix);
|
|
// Glean metric names:
|
|
// Glean.update.notPrefUpdateAutoExternal
|
|
// Glean.update.notPrefUpdateAutoNotify
|
|
// Glean.update.notPrefUpdateAutoSubsequent
|
|
lazy.UpdateUtils.getAppUpdateAutoEnabled().then(enabled => {
|
|
if (!enabled) {
|
|
Glean.update["notPrefUpdateAuto" + this._pingSuffix].add();
|
|
}
|
|
});
|
|
// Glean metric names:
|
|
// Glean.update.notPrefUpdateStagingEnabledExternal
|
|
// Glean.update.notPrefUpdateStagingEnabledNotify
|
|
// Glean.update.notPrefUpdateStagingEnabledSubsequent
|
|
AUSTLMY.pingBoolPref(
|
|
Glean.update["notPrefUpdateStagingEnabled" + this._pingSuffix],
|
|
PREF_APP_UPDATE_STAGING_ENABLED,
|
|
true,
|
|
true
|
|
);
|
|
if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
|
|
// Glean metric names:
|
|
// Glean.update.prefUpdateCancelationsExternal
|
|
// Glean.update.prefUpdateCancelationsNotify
|
|
// Glean.update.prefUpdateCancelationsSubsequent
|
|
AUSTLMY.pingIntPref(
|
|
Glean.update["prefUpdateCancelations" + this._pingSuffix],
|
|
PREF_APP_UPDATE_CANCELATIONS,
|
|
0,
|
|
0
|
|
);
|
|
}
|
|
if (AppConstants.MOZ_MAINTENANCE_SERVICE) {
|
|
// Glean metric names:
|
|
// Glean.update.notPrefUpdateServiceEnabledExternal
|
|
// Glean.update.notPrefUpdateServiceEnabledNotify
|
|
// Glean.update.notPrefUpdateServiceEnabledSubsequent
|
|
AUSTLMY.pingBoolPref(
|
|
Glean.update["notPrefUpdateServiceEnabled" + this._pingSuffix],
|
|
PREF_APP_UPDATE_SERVICE_ENABLED,
|
|
true
|
|
);
|
|
// Glean metric names:
|
|
// Glean.update.prefServiceErrorsExternal
|
|
// Glean.update.prefServiceErrorsNotify
|
|
// Glean.update.prefServiceErrorsSubsequent
|
|
AUSTLMY.pingIntPref(
|
|
Glean.update["prefServiceErrors" + this._pingSuffix],
|
|
PREF_APP_UPDATE_SERVICE_ERRORS,
|
|
0,
|
|
0
|
|
);
|
|
if (AppConstants.platform == "win") {
|
|
// Glean metric names:
|
|
// Glean.update.serviceInstalledExternal
|
|
// Glean.update.serviceInstalledNotify
|
|
// Glean.update.serviceInstalledSubsequent
|
|
// Glean.update.serviceManuallyUninstalledExternal
|
|
// Glean.update.serviceManuallyUninstalledNotify
|
|
// Glean.update.serviceManuallyUninstalledSubsequent
|
|
AUSTLMY.pingServiceInstallStatus(
|
|
this._pingSuffix,
|
|
isServiceInstalled()
|
|
);
|
|
}
|
|
}
|
|
|
|
// If a download is in progress or the patch has been staged do nothing.
|
|
if (this.isDownloading) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING);
|
|
return false;
|
|
}
|
|
|
|
// Once we have downloaded a complete update, do not download further
|
|
// updates until the complete update is installed. This is important,
|
|
// because if we fall back from a partial update to a complete update,
|
|
// it might be because of changes to the patch directory (which would cause
|
|
// a failure to apply any partial MAR). So we really don't want to replace
|
|
// a downloaded complete update with a downloaded partial update. And we
|
|
// do not currently download complete updates if there is already a
|
|
// readyUpdate available.
|
|
if (
|
|
lazy.UM.internal.readyUpdate &&
|
|
lazy.UM.internal.readyUpdate.selectedPatch &&
|
|
lazy.UM.internal.readyUpdate.selectedPatch.type == "complete"
|
|
) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
|
|
return false;
|
|
}
|
|
|
|
// If we start downloading an update while the readyUpdate is staging, we
|
|
// run the risk of eventually wanting to overwrite readyUpdate with the
|
|
// downloadingUpdate while the readyUpdate is still staging. Then we would
|
|
// have to have a weird intermediate state where the downloadingUpdate has
|
|
// finished downloading, but can't be moved yet. It's simpler to just not
|
|
// start a new update if the old one is still staging.
|
|
if (this.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
|
|
return false;
|
|
}
|
|
|
|
// Asynchronously kick off update checking
|
|
(async () => {
|
|
let validUpdateURL = true;
|
|
try {
|
|
await lazy.CheckSvc.getUpdateURL(lazy.CheckSvc.BACKGROUND_CHECK);
|
|
} catch (e) {
|
|
validUpdateURL = false;
|
|
}
|
|
|
|
// The following checks are done here so they can be differentiated from
|
|
// foreground checks.
|
|
if (!lazy.UpdateUtils.OSVersion) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
|
|
} else if (!lazy.UpdateUtils.ABI) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_ABI);
|
|
} else if (!validUpdateURL) {
|
|
AUSTLMY.pingCheckCode(
|
|
this._pingSuffix,
|
|
AUSTLMY.CHK_INVALID_DEFAULT_URL
|
|
);
|
|
} else if (!hasUpdateMutex()) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
|
|
} else if (isOtherInstanceRunning()) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OTHER_INSTANCE);
|
|
} else if (!this.canCheckForUpdates) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
|
|
}
|
|
|
|
let check = lazy.CheckSvc.checkForUpdates(lazy.CheckSvc.BACKGROUND_CHECK);
|
|
await this.onCheckComplete(await check.result);
|
|
})();
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Determine the update from the specified updates that should be offered.
|
|
* If both valid major and minor updates are available the minor update will
|
|
* be offered.
|
|
* @param updates
|
|
* An array of available nsIUpdate items
|
|
* @return The nsIUpdate to offer.
|
|
*/
|
|
#selectUpdate(updates) {
|
|
if (!updates.length) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND);
|
|
return null;
|
|
}
|
|
|
|
// The ping for unsupported is sent after the call to showPrompt.
|
|
if (updates.length == 1 && updates[0].unsupported) {
|
|
return updates[0];
|
|
}
|
|
|
|
// Choose the newest of the available minor and major updates.
|
|
var majorUpdate = null;
|
|
var minorUpdate = null;
|
|
var vc = Services.vc;
|
|
let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND;
|
|
|
|
for (const update of updates) {
|
|
// Ignore updates for older versions of the application and updates for
|
|
// the same version of the application with the same build ID.
|
|
if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping update because the " +
|
|
"update's application version is not greater than the current " +
|
|
"application version"
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
|
|
continue;
|
|
}
|
|
|
|
if (updateIsAtLeastAsOldAsReadyUpdate(update)) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping update because the " +
|
|
"update's application version is not greater than that of the " +
|
|
"currently downloaded update"
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
|
|
continue;
|
|
}
|
|
|
|
if (lazy.UM.internal.readyUpdate && !getPatchOfType(update, "partial")) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping update because no partial " +
|
|
"patch is available and an update has already been downloaded."
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_NO_PARTIAL_PATCH;
|
|
continue;
|
|
}
|
|
|
|
switch (update.type) {
|
|
case "major":
|
|
if (!majorUpdate || majorUpdate.unsupported) {
|
|
majorUpdate = update;
|
|
} else if (
|
|
!update.unsupported &&
|
|
vc.compare(majorUpdate.appVersion, update.appVersion) <= 0
|
|
) {
|
|
majorUpdate = update;
|
|
}
|
|
break;
|
|
case "minor":
|
|
if (!minorUpdate || minorUpdate.unsupported) {
|
|
minorUpdate = update;
|
|
} else if (
|
|
!update.unsupported &&
|
|
vc.compare(minorUpdate.appVersion, update.appVersion) <= 0
|
|
) {
|
|
minorUpdate = update;
|
|
}
|
|
break;
|
|
default:
|
|
LOG(
|
|
"UpdateService:selectUpdate - skipping unknown update type: " +
|
|
update.type
|
|
);
|
|
lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let update = minorUpdate || majorUpdate;
|
|
if (AppConstants.platform == "macosx" && update) {
|
|
if (getElevationRequired()) {
|
|
let installAttemptVersion = Services.prefs.getCharPref(
|
|
PREF_APP_UPDATE_ELEVATE_VERSION,
|
|
null
|
|
);
|
|
if (vc.compare(installAttemptVersion, update.appVersion) != 0) {
|
|
Services.prefs.setCharPref(
|
|
PREF_APP_UPDATE_ELEVATE_VERSION,
|
|
update.appVersion
|
|
);
|
|
if (
|
|
Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)
|
|
) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
|
|
}
|
|
} else {
|
|
let numCancels = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX,
|
|
0
|
|
);
|
|
let rejectedVersion = Services.prefs.getCharPref(
|
|
PREF_APP_UPDATE_ELEVATE_NEVER,
|
|
""
|
|
);
|
|
let maxCancels = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
|
|
DEFAULT_CANCELATIONS_OSX_MAX
|
|
);
|
|
if (numCancels >= maxCancels) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - the user requires elevation to " +
|
|
"install this update, but the user has exceeded the max " +
|
|
"number of elevation attempts."
|
|
);
|
|
update.elevationFailure = true;
|
|
AUSTLMY.pingCheckCode(
|
|
this._pingSuffix,
|
|
AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION
|
|
);
|
|
} else if (vc.compare(rejectedVersion, update.appVersion) == 0) {
|
|
LOG(
|
|
"UpdateService:selectUpdate - the user requires elevation to " +
|
|
"install this update, but elevation is disabled for this " +
|
|
"version."
|
|
);
|
|
update.elevationFailure = true;
|
|
AUSTLMY.pingCheckCode(
|
|
this._pingSuffix,
|
|
AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION
|
|
);
|
|
} else {
|
|
LOG(
|
|
"UpdateService:selectUpdate - the user requires elevation to " +
|
|
"install the update."
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Clear elevation-related prefs since they no longer apply (the user
|
|
// may have gained write access to the Firefox directory or an update
|
|
// was executed with a different profile).
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
|
|
}
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
|
|
}
|
|
}
|
|
} else if (!update) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode);
|
|
}
|
|
|
|
return update;
|
|
}
|
|
|
|
/*
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async selectUpdate(updates) {
|
|
await this.init();
|
|
return this.#selectUpdate(updates);
|
|
}
|
|
|
|
/**
|
|
* Determine which of the specified updates should be installed and begin the
|
|
* download/installation process or notify the user about the update.
|
|
* @param updates
|
|
* An array of available updates
|
|
*/
|
|
async _selectAndInstallUpdate(updates) {
|
|
// Return early if there's an active update. The user is already aware and
|
|
// is downloading or performed some user action to prevent notification.
|
|
if (lazy.UM.internal.downloadingUpdate) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE);
|
|
return;
|
|
}
|
|
|
|
if (this.disabled) {
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_BY_POLICY);
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - not prompting because " +
|
|
"update is disabled"
|
|
);
|
|
return;
|
|
}
|
|
|
|
var update = this.#selectUpdate(updates);
|
|
if (!update || update.elevationFailure) {
|
|
return;
|
|
}
|
|
|
|
if (update.unsupported) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - update not supported for " +
|
|
"this system. Notifying observers. topic: update-available, " +
|
|
"status: unsupported"
|
|
);
|
|
Services.obs.notifyObservers(update, "update-available", "unsupported");
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
|
|
return;
|
|
}
|
|
|
|
if (!getCanApplyUpdates()) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - the user is unable to " +
|
|
"apply updates... prompting. Notifying observers. " +
|
|
"topic: update-available, status: cant-apply"
|
|
);
|
|
Services.obs.notifyObservers(null, "update-available", "cant-apply");
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* From this point on there are two possible outcomes:
|
|
* 1. download and install the update automatically
|
|
* 2. notify the user about the availability of an update
|
|
*
|
|
* Notes:
|
|
* a) if the app.update.auto preference is false then automatic download and
|
|
* install is disabled and the user will be notified.
|
|
*
|
|
* If the update when it is first read does not have an appVersion attribute
|
|
* the following deprecated behavior will occur:
|
|
* Update Type Outcome
|
|
* Major Notify
|
|
* Minor Auto Install
|
|
*/
|
|
let updateAuto = await lazy.UpdateUtils.getAppUpdateAutoEnabled();
|
|
if (!updateAuto) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - prompting because silent " +
|
|
"install is disabled. Notifying observers. topic: update-available, " +
|
|
"status: show-prompt"
|
|
);
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
|
|
Services.obs.notifyObservers(update, "update-available", "show-prompt");
|
|
return;
|
|
}
|
|
|
|
LOG("UpdateService:_selectAndInstallUpdate - download the update");
|
|
let result = await this.#downloadUpdate(update);
|
|
if (
|
|
result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS &&
|
|
!this.isDownloading
|
|
) {
|
|
LOG(
|
|
"UpdateService:_selectAndInstallUpdate - Failed to start downloading " +
|
|
"update. Cleaning up downloading update."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
}
|
|
AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get isAppBaseDirWritable() {
|
|
return isAppBaseDirWritable();
|
|
}
|
|
|
|
get disabledForTesting() {
|
|
return lazy.UpdateServiceStub.updateDisabledForTesting;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get disabled() {
|
|
return lazy.UpdateServiceStub.updateDisabled;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get manualUpdateOnly() {
|
|
return (
|
|
Services.policies && !Services.policies.isAllowed("autoAppUpdateChecking")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyCheckForUpdates() {
|
|
if (this.disabled) {
|
|
LOG(
|
|
"UpdateService.canUsuallyCheckForUpdates - unable to automatically check " +
|
|
"for updates, the option has been disabled by the administrator."
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// If we don't know the binary platform we're updating, we can't update.
|
|
if (!lazy.UpdateUtils.ABI) {
|
|
LOG(
|
|
"UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
|
|
"unknown ABI"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// If we don't know the OS version we're updating, we can't update.
|
|
if (!lazy.UpdateUtils.OSVersion) {
|
|
LOG(
|
|
"UpdateService.canUsuallyCheckForUpdates - unable to check for updates, " +
|
|
"unknown OS version"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
LOG("UpdateService.canUsuallyCheckForUpdates - able to check for updates");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canCheckForUpdates() {
|
|
if (!this.canUsuallyCheckForUpdates) {
|
|
return false;
|
|
}
|
|
|
|
if (!hasUpdateMutex()) {
|
|
LOG(
|
|
"UpdateService.canCheckForUpdates - unable to check for updates, " +
|
|
"unable to acquire update mutex"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (isOtherInstanceRunning()) {
|
|
// This doesn't block update checks, but we will have to wait until either
|
|
// the other instance is gone or we time out waiting for it.
|
|
LOG(
|
|
"UpdateService.canCheckForUpdates - another instance is holding the " +
|
|
"lock, will need to wait for it prior to checking for updates"
|
|
);
|
|
}
|
|
|
|
LOG("UpdateService.canCheckForUpdates - able to check for updates");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get elevationRequired() {
|
|
return getElevationRequired();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyApplyUpdates() {
|
|
return getCanApplyUpdates();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canApplyUpdates() {
|
|
return (
|
|
this.canUsuallyApplyUpdates &&
|
|
hasUpdateMutex() &&
|
|
!isOtherInstanceRunning()
|
|
);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyStageUpdates() {
|
|
return getCanStageUpdates(false);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canStageUpdates() {
|
|
return getCanStageUpdates();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUsuallyUseBits() {
|
|
return getCanUseBits(false) == "CanUseBits";
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get canUseBits() {
|
|
return getCanUseBits() == "CanUseBits";
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get isOtherInstanceHandlingUpdates() {
|
|
return !hasUpdateMutex() || isOtherInstanceRunning();
|
|
}
|
|
|
|
/**
|
|
* A set of download listeners to be notified by this._downloader when it
|
|
* receives nsIRequestObserver or nsIProgressEventSink method calls.
|
|
*
|
|
* These are stored on the UpdateService rather than on the Downloader,
|
|
* because they ought to persist across multiple Downloader instances.
|
|
*/
|
|
_downloadListeners = new Set();
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
addDownloadListener(listener) {
|
|
let oldSize = this._downloadListeners.size;
|
|
this._downloadListeners.add(listener);
|
|
|
|
if (this._downloadListeners.size == oldSize) {
|
|
LOG(
|
|
"UpdateService:addDownloadListener - Warning: Didn't add duplicate " +
|
|
"listener"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this._downloader) {
|
|
this._downloader.onDownloadListenerAdded();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
removeDownloadListener(listener) {
|
|
let elementRemoved = this._downloadListeners.delete(listener);
|
|
|
|
if (!elementRemoved) {
|
|
LOG(
|
|
"UpdateService:removeDownloadListener - Warning: Didn't remove " +
|
|
"non-existent listener"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this._downloader) {
|
|
this._downloader.onDownloadListenerRemoved();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a boolean indicating whether there are any download listeners
|
|
*/
|
|
get hasDownloadListeners() {
|
|
return !!this._downloadListeners.length;
|
|
}
|
|
|
|
/*
|
|
* Calls the provided function once with each download listener that is
|
|
* currently registered.
|
|
*/
|
|
forEachDownloadListener(fn) {
|
|
// Make a shallow copy in case listeners remove themselves.
|
|
let listeners = new Set(this._downloadListeners);
|
|
listeners.forEach(fn);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async downloadUpdate(update) {
|
|
await this.init();
|
|
return this.#downloadUpdate(update);
|
|
}
|
|
|
|
async #downloadUpdate(update) {
|
|
if (!update) {
|
|
throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
|
|
}
|
|
|
|
// Don't download the update if the update's version is less than the
|
|
// current application's version or the update's version is the same as the
|
|
// application's version and the build ID is the same as the application's
|
|
// build ID. If we already have an update ready, we want to apply those
|
|
// same checks against the version of the ready update, so that we don't
|
|
// download an update that isn't newer than the one we already have.
|
|
if (updateIsAtLeastAsOldAsCurrentVersion(update)) {
|
|
LOG(
|
|
"UpdateService:downloadUpdate - Skipping download of update since " +
|
|
"it is for an earlier or same application version and build ID.\n" +
|
|
"current application version: " +
|
|
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("Current update state: " + this.getStateName(gUpdateState));
|
|
LOG("Downloading: " + !!this.isDownloading);
|
|
if (this._downloader && this._downloader.isBusy) {
|
|
LOG("Downloading complete update: " + this._downloader.isCompleteUpdate);
|
|
LOG("Downloader using BITS: " + this._downloader.usingBits);
|
|
if (this._downloader._patch) {
|
|
// This will print its own logging
|
|
this._downloader._canUseBits(this._downloader._patch);
|
|
|
|
// Downloader calls QueryInterface(Ci.nsIWritablePropertyBag) on
|
|
// its _patch member as soon as it is assigned, so no need to do so
|
|
// again here.
|
|
let bitsResult = this._downloader._patch.getProperty("bitsResult");
|
|
if (bitsResult != null) {
|
|
LOG("Patch BITS result: " + bitsResult);
|
|
}
|
|
let internalResult =
|
|
this._downloader._patch.getProperty("internalResult");
|
|
if (internalResult != null) {
|
|
LOG("Patch nsIIncrementalDownload result: " + internalResult);
|
|
}
|
|
}
|
|
}
|
|
LOG("End of UpdateService status");
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get onlyDownloadUpdatesThisSession() {
|
|
return gOnlyDownloadUpdatesThisSession;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
set onlyDownloadUpdatesThisSession(newValue) {
|
|
gOnlyDownloadUpdatesThisSession = newValue;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
getStateName(state) {
|
|
switch (state) {
|
|
case Ci.nsIApplicationUpdateService.STATE_IDLE:
|
|
return "STATE_IDLE";
|
|
case Ci.nsIApplicationUpdateService.STATE_DOWNLOADING:
|
|
return "STATE_DOWNLOADING";
|
|
case Ci.nsIApplicationUpdateService.STATE_STAGING:
|
|
return "STATE_STAGING";
|
|
case Ci.nsIApplicationUpdateService.STATE_PENDING:
|
|
return "STATE_PENDING";
|
|
case Ci.nsIApplicationUpdateService.STATE_SWAP:
|
|
return "STATE_SWAP";
|
|
}
|
|
return `[unknown update state: ${state}]`;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get currentState() {
|
|
return gUpdateState;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
get stateTransition() {
|
|
return gStateTransitionPromise.promise;
|
|
}
|
|
|
|
/**
|
|
* Either starts a BITS transfer job or connects to an existing one.
|
|
* When starting a job, it starts it with a name and path that make sense for
|
|
* an update MAR download.
|
|
*
|
|
* @param parameters
|
|
* A parameters object must be passed, in which `bitsId` or `url` must
|
|
* be specified. If `bitsId` is specified (and not null), this will
|
|
* connect to an existing transfer.
|
|
* activeListeners
|
|
* If `true`, this option specifies that there are active
|
|
* listeners, so the faster "active" progress update polling rate
|
|
* should be used.
|
|
* bitsId
|
|
* The ID of the job to connect to. If this is not passed, a new
|
|
* transfer will be started.
|
|
* observer
|
|
* If specified, should be an instance of `nsIRequestObserver`
|
|
* and, optionally, `nsIProgressEventSink`. This will be connected
|
|
* to the `BitsRequest` that is returned.
|
|
* url
|
|
* The URL to download.
|
|
* extraHeaders
|
|
* String of extra headers to include, in the format accepted by
|
|
* `IBackgroundCopyJobHttpOptions::SetCustomHeaders`: separated by
|
|
* `\r\n`, terminated by an additional `\r\n`.
|
|
* @return Promise<BitsRequest>
|
|
* Returns a request object
|
|
* @throws BitsError
|
|
* On failure to connect to the BITS job.
|
|
*/
|
|
async makeBitsRequest({
|
|
activeListeners = false,
|
|
bitsId,
|
|
observer,
|
|
url,
|
|
extraHeaders,
|
|
}) {
|
|
let noProgressTimeout = BITS_IDLE_NO_PROGRESS_TIMEOUT_SECS;
|
|
let monitorInterval = BITS_IDLE_POLL_RATE_MS;
|
|
// The monitor's timeout should be much greater than the longest monitor
|
|
// poll interval. If the timeout is too short, delay in the pipe to the
|
|
// update agent might cause BITS to falsely report an error, causing an
|
|
// unnecessary fallback to nsIIncrementalDownload.
|
|
let monitorTimeout = Math.max(10 * monitorInterval, 10 * 60 * 1000);
|
|
if (activeListeners) {
|
|
noProgressTimeout = BITS_ACTIVE_NO_PROGRESS_TIMEOUT_SECS;
|
|
monitorInterval = BITS_ACTIVE_POLL_RATE_MS;
|
|
}
|
|
|
|
let updateRootDir = FileUtils.getDir(KEY_UPDROOT, []);
|
|
try {
|
|
updateRootDir.create(
|
|
Ci.nsIFile.DIRECTORY_TYPE,
|
|
FileUtils.PERMS_DIRECTORY
|
|
);
|
|
} catch (ex) {
|
|
if (ex.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
|
|
throw ex;
|
|
}
|
|
// Ignore the exception due to a directory that already exists.
|
|
}
|
|
|
|
let jobName = "MozillaUpdate " + updateRootDir.leafName;
|
|
let updatePath = getDownloadingUpdateDir().path;
|
|
if (!Bits.initialized) {
|
|
Bits.init(jobName, updatePath, monitorTimeout);
|
|
}
|
|
|
|
if (bitsId) {
|
|
LOG(
|
|
"UpdateService:makeBitsRequest - Connecting to in-progress download. " +
|
|
"BITS ID: " +
|
|
bitsId
|
|
);
|
|
|
|
return Bits.monitorDownload(bitsId, monitorInterval, observer, null);
|
|
}
|
|
|
|
LOG(
|
|
"UpdateService:makeBitsRequest - Starting BITS download with url: " +
|
|
url +
|
|
", updateDir: " +
|
|
updatePath +
|
|
", filename: " +
|
|
FILE_UPDATE_MAR
|
|
);
|
|
|
|
return Bits.startDownload(
|
|
url,
|
|
FILE_UPDATE_MAR,
|
|
Ci.nsIBits.PROXY_PRECONFIG,
|
|
noProgressTimeout,
|
|
monitorInterval,
|
|
extraHeaders,
|
|
observer,
|
|
null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get rid of a downloading update. This is generally done before cleaning it
|
|
* up.
|
|
*
|
|
* Connects to the in-progress BITS job in order to cancel it, if necessary.
|
|
* This is generally only necessary when cancelling a job at startup,
|
|
* otherwise we would have connected to it already.
|
|
*
|
|
* If there is not an in-progress download, this has no effect.
|
|
*/
|
|
async cancelDownloadingUpdate() {
|
|
if (this.isDownloading) {
|
|
LOG(
|
|
"UpdateService:cancelDownloadingUpdate - Job is connected already. Stopping."
|
|
);
|
|
await this.internal.stopDownload();
|
|
return;
|
|
}
|
|
|
|
// If we didn't return above, either we haven't connected to the BITS job
|
|
// yet or there is no BITS job and nothing to cancel.
|
|
|
|
if (!lazy.UM.internal.downloadingUpdate) {
|
|
LOG(
|
|
"UpdateService:cancelDownloadingUpdate - Not cleaning up BITS Job. No update."
|
|
);
|
|
return;
|
|
}
|
|
const patch = lazy.UM.internal.downloadingUpdate.selectedPatch;
|
|
if (!patch || !patch.QueryInterface(Ci.nsIWritablePropertyBag)) {
|
|
LOG(
|
|
"UpdateService:cancelDownloadingUpdate - Not cleaning up BITS Job. No patch."
|
|
);
|
|
return;
|
|
}
|
|
const bitsId = patch.getProperty("bitsId");
|
|
if (!bitsId) {
|
|
LOG(
|
|
"UpdateService:cancelDownloadingUpdate - Not cleaning up BITS Job. No BITS ID."
|
|
);
|
|
return;
|
|
}
|
|
// If `!this.isDownloading`, we are not connected to the BITS request, which
|
|
// we need to do to stop a BITS download.
|
|
try {
|
|
const request = await this.makeBitsRequest({ bitsId });
|
|
await request.cancelAsync();
|
|
} catch (ex) {
|
|
LOG(
|
|
`UpdateService:cancelDownloadingUpdate - Failed to clean up BITS Job ${bitsId}: ${ex}`
|
|
);
|
|
return;
|
|
}
|
|
LOG(
|
|
`UpdateService:cancelDownloadingUpdate - Successfully cleaned up BITS Job ${bitsId}`
|
|
);
|
|
}
|
|
|
|
classID = UPDATESERVICE_CID;
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIApplicationUpdateService,
|
|
Ci.nsITimerCallback,
|
|
Ci.nsIObserver,
|
|
]);
|
|
}
|
|
|
|
export class UpdateManager {
|
|
/**
|
|
* The nsIUpdate object for the update that has been downloaded.
|
|
*/
|
|
_readyUpdate = null;
|
|
|
|
/**
|
|
* The nsIUpdate object for the update currently being downloaded.
|
|
*/
|
|
_downloadingUpdate = null;
|
|
|
|
/**
|
|
* Whether the update history stored in _updates has changed since it was
|
|
* loaded.
|
|
*/
|
|
_updatesDirty = false;
|
|
|
|
/**
|
|
* The backing for `nsIUpdateManager.updateInstalledAtStartup`.
|
|
*/
|
|
#updateInstalledAtStartup = null;
|
|
|
|
/**
|
|
* A service to manage active and past updates.
|
|
* @constructor
|
|
*/
|
|
constructor() {
|
|
this.internal = {
|
|
reload: async skipFiles => this.#reload(skipFiles),
|
|
getHistory: () => this.#getHistory(),
|
|
addUpdateToHistory: update => this.#addUpdateToHistory(update),
|
|
refreshUpdateStatus: async () => this.#refreshUpdateStatus(),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateManagerInternal]),
|
|
};
|
|
|
|
Object.defineProperty(this.internal, "readyUpdate", {
|
|
get: () => this._readyUpdate,
|
|
set: update => {
|
|
this._readyUpdate = update;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(this.internal, "downloadingUpdate", {
|
|
get: () => this._downloadingUpdate,
|
|
set: update => {
|
|
this._downloadingUpdate = update;
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* See `nsIUpdateManagerInternal.reload` in nsIUpdateService.idl
|
|
*/
|
|
async #reload(skipFiles) {
|
|
LOG("UpdateManager:#reload - Reloading update data.");
|
|
if (this._updatesXMLSaver) {
|
|
this._updatesXMLSaver.disarm();
|
|
}
|
|
|
|
let updates = [];
|
|
this._updatesDirty = skipFiles;
|
|
this._readyUpdate = null;
|
|
this._downloadingUpdate = null;
|
|
this.#updateInstalledAtStartup = null;
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
if (!skipFiles) {
|
|
// Load the active-update.xml file to see if there is an active update.
|
|
let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML);
|
|
if (activeUpdates.length) {
|
|
const status = readStatusFile(getReadyUpdateDir());
|
|
|
|
// If there are two updates, the first one is the ready update.
|
|
// If there is only 1 update, we don't know which is which. We use the
|
|
// state to figure it out.
|
|
if (activeUpdates.length > 1) {
|
|
this._readyUpdate = activeUpdates[0];
|
|
this._downloadingUpdate = activeUpdates[1];
|
|
if (activeUpdates.length > 2) {
|
|
LOG(
|
|
"UpdateManager:#reload - Warning: Ignoring additional (>2) " +
|
|
"unexpected active updates"
|
|
);
|
|
}
|
|
} else if (
|
|
status == STATE_DOWNLOADING ||
|
|
activeUpdates[0].state == STATE_DOWNLOADING
|
|
) {
|
|
this._downloadingUpdate = activeUpdates[0];
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_DOWNLOADING);
|
|
} else {
|
|
this._readyUpdate = activeUpdates[0];
|
|
}
|
|
|
|
LOG(`UpdateManager:#reload - Got status = ${status}`);
|
|
if (
|
|
[
|
|
STATE_PENDING,
|
|
STATE_PENDING_SERVICE,
|
|
STATE_PENDING_ELEVATE,
|
|
STATE_APPLIED,
|
|
STATE_APPLIED_SERVICE,
|
|
].includes(status)
|
|
) {
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
}
|
|
if (status == STATE_SUCCEEDED && this._readyUpdate) {
|
|
this.#updateInstalledAtStartup = this._readyUpdate;
|
|
// Bug 1889785 - When deciding whether or not to show a What's New Page, we
|
|
// rely on both `Services.appinfo.platformVersion` and this value. But Balrog
|
|
// doesn't guarantee that the value that it sends will match appinfo.
|
|
// We synchronize the values here so they are consistent.
|
|
this.#updateInstalledAtStartup.platformVersion =
|
|
Services.appinfo.platformVersion;
|
|
this.saveUpdates();
|
|
}
|
|
}
|
|
updates = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
|
|
}
|
|
this._updatesCache = updates;
|
|
|
|
LOG(
|
|
"UpdateManager:#reload - Reloaded downloadingUpdate as " +
|
|
this._downloadingUpdate
|
|
);
|
|
if (this._downloadingUpdate) {
|
|
LOG(
|
|
"UpdateManager:#reload - Reloaded downloadingUpdate state as " +
|
|
this._downloadingUpdate.state
|
|
);
|
|
}
|
|
LOG("UpdateManager:#reload - Reloaded readyUpdate as " + this._readyUpdate);
|
|
if (this._readyUpdate) {
|
|
LOG(
|
|
"UpdateManager:#reload - Reloaded readyUpdate state as " +
|
|
this._readyUpdate.state
|
|
);
|
|
}
|
|
LOG(
|
|
"UpdateManager:UpdateManager - Reloaded updateInstalledAtStartup as " +
|
|
this.#updateInstalledAtStartup
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Loads an updates.xml formatted file into an array of nsIUpdate items.
|
|
* @param fileName
|
|
* The file name in the updates directory to load.
|
|
* @return The array of nsIUpdate items held in the file.
|
|
*/
|
|
_loadXMLFileIntoArray(fileName) {
|
|
let updates = [];
|
|
let file = getUpdateFile([fileName]);
|
|
if (!file.exists()) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - XML file does not exist. " +
|
|
"path: " +
|
|
file.path
|
|
);
|
|
return updates;
|
|
}
|
|
|
|
// Open the active-update.xml file with both read and write access so
|
|
// opening it will fail if it isn't possible to also write to the file. When
|
|
// opening it fails it means that it isn't possible to update and the code
|
|
// below will return early without loading the active-update.xml. This will
|
|
// also make it so notifications to update manually will still be shown.
|
|
let mode =
|
|
fileName == FILE_ACTIVE_UPDATE_XML
|
|
? FileUtils.MODE_RDWR
|
|
: FileUtils.MODE_RDONLY;
|
|
let fileStream = Cc[
|
|
"@mozilla.org/network/file-input-stream;1"
|
|
].createInstance(Ci.nsIFileInputStream);
|
|
try {
|
|
fileStream.init(file, mode, FileUtils.PERMS_FILE, 0);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - error initializing file " +
|
|
"stream. Exception: " +
|
|
e
|
|
);
|
|
return updates;
|
|
}
|
|
try {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromStream(
|
|
fileStream,
|
|
"UTF-8",
|
|
fileStream.available(),
|
|
"text/xml"
|
|
);
|
|
|
|
var updateCount = doc.documentElement.childNodes.length;
|
|
for (var i = 0; i < updateCount; ++i) {
|
|
var updateElement = doc.documentElement.childNodes.item(i);
|
|
if (
|
|
updateElement.nodeType != updateElement.ELEMENT_NODE ||
|
|
updateElement.localName != "update"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let update;
|
|
try {
|
|
update = new Update(updateElement);
|
|
} catch (e) {
|
|
LOG("UpdateManager:_loadXMLFileIntoArray - invalid update");
|
|
continue;
|
|
}
|
|
updates.push(update);
|
|
}
|
|
} catch (ex) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - error constructing update " +
|
|
"list. Exception: " +
|
|
ex
|
|
);
|
|
}
|
|
fileStream.close();
|
|
if (!updates.length) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - update xml file " +
|
|
fileName +
|
|
" exists but doesn't contain any updates"
|
|
);
|
|
// The file exists but doesn't contain any updates so remove it.
|
|
try {
|
|
file.remove(false);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_loadXMLFileIntoArray - error removing " +
|
|
fileName +
|
|
" file. Exception: " +
|
|
e
|
|
);
|
|
}
|
|
}
|
|
return updates;
|
|
}
|
|
|
|
#getHistory() {
|
|
const history = this._getUpdates();
|
|
return [...history];
|
|
}
|
|
|
|
/**
|
|
* Loads the update history from the updates.xml file into a cache.
|
|
*/
|
|
_getUpdates() {
|
|
if (!this._updatesCache) {
|
|
this._updatesCache = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
|
|
}
|
|
return this._updatesCache;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getHistory() {
|
|
await lazy.AUS.init();
|
|
return lazy.UM.internal.getHistory();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getReadyUpdate() {
|
|
await lazy.AUS.init();
|
|
return this._readyUpdate;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getDownloadingUpdate() {
|
|
await lazy.AUS.init();
|
|
return this._downloadingUpdate;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async updateInstalledAtStartup() {
|
|
await lazy.AUS.init();
|
|
return this.#updateInstalledAtStartup;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async lastUpdateInstalled() {
|
|
await lazy.AUS.init();
|
|
if (this.#updateInstalledAtStartup) {
|
|
return this.#updateInstalledAtStartup;
|
|
}
|
|
return this._getUpdates().find(u => u.state == STATE_SUCCEEDED) ?? null;
|
|
}
|
|
|
|
#addUpdateToHistory(aUpdate) {
|
|
this._updatesDirty = true;
|
|
let updates = this._getUpdates();
|
|
updates.unshift(aUpdate);
|
|
// Limit the update history to 10 updates.
|
|
updates.splice(10);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async addUpdateToHistory(aUpdate) {
|
|
await lazy.AUS.init();
|
|
this.#addUpdateToHistory(aUpdate);
|
|
}
|
|
|
|
/**
|
|
* Serializes an array of updates to an XML file or removes the file if the
|
|
* array length is 0.
|
|
* @param updates
|
|
* An array of nsIUpdate objects
|
|
* @param fileName
|
|
* The file name in the updates directory to write to.
|
|
* @return true on success, false on error
|
|
*/
|
|
async _writeUpdatesToXMLFile(updates, fileName) {
|
|
let file;
|
|
try {
|
|
file = getUpdateFile([fileName]);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_writeUpdatesToXMLFile - Unable to get XML file - " +
|
|
"Exception: " +
|
|
e
|
|
);
|
|
return false;
|
|
}
|
|
if (!updates.length) {
|
|
LOG(
|
|
"UpdateManager:_writeUpdatesToXMLFile - no updates to write. " +
|
|
"removing file: " +
|
|
file.path
|
|
);
|
|
try {
|
|
await IOUtils.remove(file.path);
|
|
} catch (e) {
|
|
LOG(
|
|
"UpdateManager:_writeUpdatesToXMLFile - Delete file exception: " + e
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const EMPTY_UPDATES_DOCUMENT_OPEN =
|
|
'<?xml version="1.0"?><updates xmlns="' + URI_UPDATE_NS + '">';
|
|
const EMPTY_UPDATES_DOCUMENT_CLOSE = "</updates>";
|
|
try {
|
|
var parser = new DOMParser();
|
|
var doc = parser.parseFromString(
|
|
EMPTY_UPDATES_DOCUMENT_OPEN + EMPTY_UPDATES_DOCUMENT_CLOSE,
|
|
"text/xml"
|
|
);
|
|
|
|
for (var i = 0; i < updates.length; ++i) {
|
|
doc.documentElement.appendChild(updates[i].serialize(doc));
|
|
}
|
|
|
|
var xml =
|
|
EMPTY_UPDATES_DOCUMENT_OPEN +
|
|
doc.documentElement.innerHTML +
|
|
EMPTY_UPDATES_DOCUMENT_CLOSE;
|
|
// If the destination file existed and is removed while the following is
|
|
// being performed the copy of the tmp file to the destination file will
|
|
// fail.
|
|
await IOUtils.writeUTF8(file.path, xml, {
|
|
tmpPath: file.path + ".tmp",
|
|
});
|
|
await IOUtils.setPermissions(file.path, FileUtils.PERMS_FILE);
|
|
} catch (e) {
|
|
LOG("UpdateManager:_writeUpdatesToXMLFile - Exception: " + e);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
_updatesXMLSaver = null;
|
|
_updatesXMLSaverCallback = null;
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
saveUpdates() {
|
|
if (!this._updatesXMLSaver) {
|
|
this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize();
|
|
|
|
this._updatesXMLSaver = new lazy.DeferredTask(
|
|
() => this._saveUpdatesXML(),
|
|
XML_SAVER_INTERVAL_MS
|
|
);
|
|
lazy.AsyncShutdown.profileBeforeChange.addBlocker(
|
|
"UpdateManager: writing update xml data",
|
|
this._updatesXMLSaverCallback
|
|
);
|
|
} else {
|
|
this._updatesXMLSaver.disarm();
|
|
}
|
|
|
|
this._updatesXMLSaver.arm();
|
|
}
|
|
|
|
/**
|
|
* Saves the active-updates.xml and updates.xml when the updates history has
|
|
* been modified files.
|
|
*/
|
|
_saveUpdatesXML() {
|
|
// This mechanism for how we store the updates might seem a bit odd, since,
|
|
// if only one update is stored, we don't know if it's the ready update or
|
|
// the downloading update. However, we can determine which it is by reading
|
|
// update.status. If we read STATE_DOWNLOADING, it must be a downloading
|
|
// update and otherwise it's a ready update. This method has the additional
|
|
// advantage of requiring no migration from when we used to only store a
|
|
// single active update.
|
|
let updates = [];
|
|
if (this._readyUpdate) {
|
|
updates.push(this._readyUpdate);
|
|
}
|
|
if (this._downloadingUpdate) {
|
|
updates.push(this._downloadingUpdate);
|
|
}
|
|
|
|
// The active update stored in the active-update.xml file will change during
|
|
// the lifetime of an active update and the file should always be updated
|
|
// when saveUpdates is called.
|
|
let promises = [];
|
|
promises[0] = this._writeUpdatesToXMLFile(updates, FILE_ACTIVE_UPDATE_XML);
|
|
// The update history stored in the updates.xml file should only need to be
|
|
// updated when an active update has been added to it in which case
|
|
// |_updatesDirty| will be true.
|
|
if (this._updatesDirty) {
|
|
this._updatesDirty = false;
|
|
promises[1] = this._writeUpdatesToXMLFile(
|
|
this._getUpdates(),
|
|
FILE_UPDATES_XML
|
|
);
|
|
}
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async refreshUpdateStatus() {
|
|
return this.#refreshUpdateStatus();
|
|
}
|
|
|
|
async #refreshUpdateStatus() {
|
|
try {
|
|
LOG("UpdateManager:refreshUpdateStatus - Staging done.");
|
|
|
|
await lazy.AUS.init();
|
|
|
|
var update = this._readyUpdate;
|
|
if (!update) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Missing ready update?");
|
|
return;
|
|
}
|
|
|
|
var status = readStatusFile(getReadyUpdateDir());
|
|
pingStateAndStatusCodes(update, false, status);
|
|
LOG(`UpdateManager:refreshUpdateStatus - status = ${status}`);
|
|
|
|
let parts = status.split(":");
|
|
update.state = parts[0];
|
|
if (update.state == STATE_APPLYING) {
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - Staging appears to have crashed."
|
|
);
|
|
update.state = STATE_FAILED;
|
|
update.errorCode = ERR_UPDATER_CRASHED;
|
|
} else if (update.state == STATE_FAILED) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Staging failed.");
|
|
if (parts[1]) {
|
|
update.errorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
|
|
} else {
|
|
update.errorCode = INVALID_UPDATER_STATUS_CODE;
|
|
}
|
|
}
|
|
|
|
// Rotate the update logs so the update log isn't removed if a complete
|
|
// update is downloaded. By passing false the patch directory won't be
|
|
// removed.
|
|
cleanUpReadyUpdateDir(false);
|
|
|
|
if (update.state == STATE_FAILED) {
|
|
let isMemError = isMemoryAllocationErrorCode(update.errorCode);
|
|
if (
|
|
update.errorCode == DELETE_ERROR_STAGING_LOCK_FILE ||
|
|
update.errorCode == UNEXPECTED_STAGING_ERROR ||
|
|
isMemError
|
|
) {
|
|
update.state = getBestPendingState();
|
|
writeStatusFile(getReadyUpdateDir(), update.state);
|
|
if (isMemError) {
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Updater failed to ` +
|
|
`allocate enough memory to successfully stage. Setting ` +
|
|
`status to "${update.state}"`
|
|
);
|
|
} else {
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Unexpected staging error. ` +
|
|
`Setting status to "${update.state}"`
|
|
);
|
|
}
|
|
} else if (isServiceSpecificErrorCode(update.errorCode)) {
|
|
// Sometimes when staging, we might encounter an error that is
|
|
// specific to the Maintenance Service. If this happens, we should try
|
|
// to update without the Service.
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Encountered service ` +
|
|
`specific error code: ${update.errorCode}. Will try installing ` +
|
|
`update without the Maintenance Service. Setting state to pending`
|
|
);
|
|
update.state = STATE_PENDING;
|
|
writeStatusFile(getReadyUpdateDir(), update.state);
|
|
} else {
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - Attempting handleUpdateFailure"
|
|
);
|
|
if (!handleUpdateFailure(update)) {
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - handleUpdateFailure " +
|
|
"failed. Attempting to fall back to complete update."
|
|
);
|
|
await handleFallbackToCompleteUpdate();
|
|
}
|
|
}
|
|
}
|
|
if (update.state == STATE_APPLIED && shouldUseService()) {
|
|
LOG(
|
|
`UpdateManager:refreshUpdateStatus - Staging successful. ` +
|
|
`Setting status to "${STATE_APPLIED_SERVICE}"`
|
|
);
|
|
writeStatusFile(
|
|
getReadyUpdateDir(),
|
|
(update.state = STATE_APPLIED_SERVICE)
|
|
);
|
|
}
|
|
|
|
// Now that the active update's properties have been updated write the
|
|
// active-update.xml to disk. Since there have been no changes to the
|
|
// update history the updates.xml will not be written to disk.
|
|
this.saveUpdates();
|
|
|
|
// Send an observer notification which the app update doorhanger uses to
|
|
// display a restart notification after any langpacks have staged.
|
|
await promiseLangPacksUpdated(update);
|
|
|
|
if (
|
|
update.state == STATE_APPLIED ||
|
|
update.state == STATE_APPLIED_SERVICE ||
|
|
update.state == STATE_PENDING ||
|
|
update.state == STATE_PENDING_SERVICE ||
|
|
update.state == STATE_PENDING_ELEVATE
|
|
) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_PENDING");
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
}
|
|
|
|
LOG(
|
|
"UpdateManager:refreshUpdateStatus - Notifying observers that " +
|
|
"the update was staged. topic: update-staged, status: " +
|
|
update.state
|
|
);
|
|
Services.obs.notifyObservers(update, "update-staged", update.state);
|
|
} finally {
|
|
// This function being called is the one thing that tells us that staging
|
|
// is done so be very sure that we don't exit it leaving the current
|
|
// state at STATE_STAGING.
|
|
// The only cases where we haven't already done a state transition are
|
|
// error cases, so if another state isn't set, assume that we hit an error
|
|
// and aborted the update.
|
|
if (
|
|
lazy.AUS.currentState == Ci.nsIApplicationUpdateService.STATE_STAGING
|
|
) {
|
|
LOG("UpdateManager:refreshUpdateStatus - Setting state STATE_IDLE");
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async elevationOptedIn() {
|
|
// The user has been been made aware that the update requires elevation.
|
|
await lazy.AUS.init();
|
|
let update = this._readyUpdate;
|
|
if (!update) {
|
|
return;
|
|
}
|
|
let status = readStatusFile(getReadyUpdateDir());
|
|
let parts = status.split(":");
|
|
update.state = parts[0];
|
|
if (update.state == STATE_PENDING_ELEVATE) {
|
|
LOG("UpdateManager:elevationOptedIn - Setting state to pending.");
|
|
// Proceed with the pending update.
|
|
// Note: STATE_PENDING_ELEVATE stands for "pending user's approval to
|
|
// proceed with an elevated update". As long as we see this state, we will
|
|
// notify the user of the availability of an update that requires
|
|
// elevation. |elevationOptedIn| (this function) is called when the user
|
|
// gives us approval to proceed, so we want to switch to STATE_PENDING.
|
|
// The updater then detects whether or not elevation is required and
|
|
// displays the elevation prompt if necessary. This last step does not
|
|
// depend on the state in the status file.
|
|
writeStatusFile(getReadyUpdateDir(), STATE_PENDING);
|
|
} else {
|
|
LOG("UpdateManager:elevationOptedIn - Not in pending-elevate state.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async cleanupDownloadingUpdate() {
|
|
LOG(
|
|
"UpdateManager:cleanupDownloadingUpdate - cleaning up downloading update."
|
|
);
|
|
await lazy.AUS.init();
|
|
await cleanupDownloadingUpdate();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async cleanupReadyUpdate() {
|
|
LOG("UpdateManager:cleanupReadyUpdate - cleaning up ready update.");
|
|
await lazy.AUS.init();
|
|
cleanupReadyUpdate();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async cleanupActiveUpdates() {
|
|
LOG("UpdateManager:cleanupActiveUpdates - cleaning up active updates.");
|
|
await lazy.AUS.init();
|
|
await cleanupActiveUpdates();
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async doInstallCleanup() {
|
|
LOG("UpdateManager:doInstallCleanup - cleaning up");
|
|
await lazy.AUS.init();
|
|
|
|
let completionPromises = [];
|
|
|
|
const delete_or_log = path =>
|
|
IOUtils.remove(path).catch(ex =>
|
|
console.error(`Failed to delete ${path}`, ex)
|
|
);
|
|
|
|
for (const key of [KEY_OLD_UPDROOT, KEY_UPDROOT]) {
|
|
const root = Services.dirsvc.get(key, Ci.nsIFile);
|
|
|
|
const activeUpdateXml = root.clone();
|
|
activeUpdateXml.append(FILE_ACTIVE_UPDATE_XML);
|
|
completionPromises.push(delete_or_log(activeUpdateXml.path));
|
|
|
|
const downloadingMar = root.clone();
|
|
downloadingMar.append(DIR_UPDATES);
|
|
downloadingMar.append(DIR_UPDATE_DOWNLOADING);
|
|
downloadingMar.append(FILE_UPDATE_MAR);
|
|
completionPromises.push(delete_or_log(downloadingMar.path));
|
|
|
|
const readyDir = root.clone();
|
|
readyDir.append(DIR_UPDATES);
|
|
readyDir.append(DIR_UPDATE_READY);
|
|
const readyMar = readyDir.clone();
|
|
readyMar.append(FILE_UPDATE_MAR);
|
|
completionPromises.push(delete_or_log(readyMar.path));
|
|
const readyStatus = readyDir.clone();
|
|
readyStatus.append(FILE_UPDATE_STATUS);
|
|
completionPromises.push(delete_or_log(readyStatus.path));
|
|
const versionFile = readyDir.clone();
|
|
versionFile.append(FILE_UPDATE_VERSION);
|
|
completionPromises.push(delete_or_log(versionFile.path));
|
|
}
|
|
|
|
return Promise.allSettled(completionPromises);
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async doUninstallCleanup() {
|
|
LOG("UpdateManager:doUninstallCleanup - cleaning up.");
|
|
await lazy.AUS.init();
|
|
let completionPromises = [];
|
|
|
|
completionPromises.push(
|
|
IOUtils.remove(Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile).path, {
|
|
recursive: true,
|
|
}).catch(ex => console.error("Failed to remove update directory", ex))
|
|
);
|
|
completionPromises.push(
|
|
IOUtils.remove(Services.dirsvc.get(KEY_OLD_UPDROOT, Ci.nsIFile).path, {
|
|
recursive: true,
|
|
}).catch(ex => console.error("Failed to remove old update directory", ex))
|
|
);
|
|
|
|
return Promise.allSettled(completionPromises);
|
|
}
|
|
|
|
classID = Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}");
|
|
QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateManager]);
|
|
}
|
|
|
|
/**
|
|
* CheckerService
|
|
* Provides an interface for checking for new updates. When more checks are
|
|
* made while an equivalent check is already in-progress, they will be coalesced
|
|
* into a single update check request.
|
|
*/
|
|
export class CheckerService {
|
|
#nextUpdateCheckId = 1;
|
|
|
|
// Most of the update checking data is looked up via a "request key". This
|
|
// allows us to lookup the request key for a particular check id, since
|
|
// multiple checks can correspond to a single request.
|
|
// When a check is cancelled or completed, it will be removed from this
|
|
// object.
|
|
#requestKeyByCheckId = {};
|
|
|
|
// This object will relate request keys to update check data objects. The
|
|
// format of the update check data objects is defined by
|
|
// #makeUpdateCheckDataObject, below.
|
|
// When an update request is cancelled (by all of the corresponding update
|
|
// checks being cancelled) or completed, its key will be removed from this
|
|
// object.
|
|
#updateCheckData = {};
|
|
|
|
constructor() {
|
|
this.internal = {
|
|
checkForUpdates: checkType => this.#checkForUpdates(checkType, true),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckerInternal]),
|
|
};
|
|
}
|
|
|
|
#makeUpdateCheckDataObject(type, promise) {
|
|
return { type, promise, request: null };
|
|
}
|
|
|
|
/**
|
|
* Indicates whether the passed parameter is one of the valid enumerated
|
|
* values that indicates a type of update check.
|
|
*/
|
|
#validUpdateCheckType(checkType) {
|
|
return [
|
|
Ci.nsIUpdateChecker.BACKGROUND_CHECK,
|
|
Ci.nsIUpdateChecker.FOREGROUND_CHECK,
|
|
].includes(checkType);
|
|
}
|
|
|
|
#getCanMigrate() {
|
|
if (AppConstants.platform != "win") {
|
|
return false;
|
|
}
|
|
|
|
// The first element of the array is whether the build target is 32 or 64
|
|
// bit and the third element of the array is whether the client's Windows OS
|
|
// system processor is 32 or 64 bit.
|
|
let aryABI = lazy.UpdateUtils.ABI.split("-");
|
|
if (aryABI[0] != "x86" || aryABI[2] != "x64") {
|
|
return false;
|
|
}
|
|
|
|
let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
|
|
Ci.nsIWindowsRegKey
|
|
);
|
|
|
|
let regPath =
|
|
"SOFTWARE\\Mozilla\\" + Services.appinfo.name + "\\32to64DidMigrate";
|
|
let regValHKCU = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_CURRENT_USER,
|
|
regPath,
|
|
"Never",
|
|
wrk.WOW64_32
|
|
);
|
|
let regValHKLM = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_LOCAL_MACHINE,
|
|
regPath,
|
|
"Never",
|
|
wrk.WOW64_32
|
|
);
|
|
// The Never registry key value allows configuring a system to never migrate
|
|
// any of the installations.
|
|
if (regValHKCU === 1 || regValHKLM === 1) {
|
|
LOG(
|
|
"CheckerService:#getCanMigrate - all installations should not be " +
|
|
"migrated"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
let appBaseDirPath = getAppBaseDir().path;
|
|
regValHKCU = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_CURRENT_USER,
|
|
regPath,
|
|
appBaseDirPath,
|
|
wrk.WOW64_32
|
|
);
|
|
regValHKLM = lazy.WindowsRegistry.readRegKey(
|
|
wrk.ROOT_KEY_LOCAL_MACHINE,
|
|
regPath,
|
|
appBaseDirPath,
|
|
wrk.WOW64_32
|
|
);
|
|
// When the registry value is 1 for the installation directory path value
|
|
// name then the installation has already been migrated once or the system
|
|
// was configured to not migrate that installation.
|
|
if (regValHKCU === 1 || regValHKLM === 1) {
|
|
LOG(
|
|
"CheckerService:#getCanMigrate - this installation should not be " +
|
|
"migrated"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// When the registry value is 0 for the installation directory path value
|
|
// name then the installation has updated to Firefox 56 and can be migrated.
|
|
if (regValHKCU === 0 || regValHKLM === 0) {
|
|
LOG("CheckerService:#getCanMigrate - this installation can be migrated");
|
|
return true;
|
|
}
|
|
|
|
LOG(
|
|
"CheckerService:#getCanMigrate - no registry entries for this " +
|
|
"installation"
|
|
);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
async getUpdateURL(checkType) {
|
|
LOG("CheckerService:getUpdateURL - checkType: " + checkType);
|
|
if (!this.#validUpdateCheckType(checkType)) {
|
|
LOG("CheckerService:getUpdateURL - Invalid checkType");
|
|
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let url = Services.appinfo.updateURL;
|
|
let updatePin;
|
|
|
|
if (Services.policies) {
|
|
let policies = Services.policies.getActivePolicies();
|
|
if (policies) {
|
|
if ("AppUpdateURL" in policies) {
|
|
url = policies.AppUpdateURL.toString();
|
|
}
|
|
if ("AppUpdatePin" in policies) {
|
|
updatePin = policies.AppUpdatePin;
|
|
|
|
// Scalar ID: update.version_pin
|
|
AUSTLMY.pingPinPolicy(updatePin);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!url) {
|
|
LOG("CheckerService:getUpdateURL - update URL not defined");
|
|
return null;
|
|
}
|
|
|
|
url = await lazy.UpdateUtils.formatUpdateURL(url);
|
|
|
|
if (checkType == Ci.nsIUpdateChecker.FOREGROUND_CHECK) {
|
|
url += (url.includes("?") ? "&" : "?") + "force=1";
|
|
}
|
|
|
|
if (this.#getCanMigrate()) {
|
|
url += (url.includes("?") ? "&" : "?") + "mig64=1";
|
|
}
|
|
|
|
if (updatePin) {
|
|
url +=
|
|
(url.includes("?") ? "&" : "?") +
|
|
"pin=" +
|
|
encodeURIComponent(updatePin);
|
|
}
|
|
|
|
LOG("CheckerService:getUpdateURL - update URL: " + url);
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
checkForUpdates(checkType) {
|
|
return this.#checkForUpdates(checkType, false);
|
|
}
|
|
|
|
#checkForUpdates(checkType, internal) {
|
|
// Note that we should run update initialization if it was invoked
|
|
// externally (i.e. `internal == false`). But initialization is async and
|
|
// the asynchronous work of this function happens in `this.#updateCheck`, so
|
|
// we will delay initialization until then.
|
|
|
|
LOG("CheckerService:checkForUpdates - checkType: " + checkType);
|
|
if (!this.#validUpdateCheckType(checkType)) {
|
|
LOG("CheckerService:checkForUpdates - Invalid checkType");
|
|
throw Components.Exception("", Cr.NS_ERROR_INVALID_ARG);
|
|
}
|
|
|
|
let checkId = this.#nextUpdateCheckId;
|
|
this.#nextUpdateCheckId += 1;
|
|
|
|
// `checkType == FOREGROUND_CHECK` can override `canCheckForUpdates`. But
|
|
// nothing should override enterprise policies.
|
|
if (lazy.AUS.disabled) {
|
|
LOG("CheckerService:checkForUpdates - disabled by policy");
|
|
return this.#getChecksNotAllowedObject(checkId);
|
|
}
|
|
if (
|
|
checkType == Ci.nsIUpdateChecker.BACKGROUND_CHECK &&
|
|
!lazy.AUS.canCheckForUpdates
|
|
) {
|
|
LOG("CheckerService:checkForUpdates - !canCheckForUpdates");
|
|
return this.#getChecksNotAllowedObject(checkId);
|
|
}
|
|
|
|
// We want to combine simultaneous requests, but only ones that are
|
|
// equivalent. If, say, one of them uses the force parameter and one
|
|
// doesn't, we want those two requests to remain separate. This key will
|
|
// allow us to map equivalent requests together. It is also the key that we
|
|
// use to lookup the update check data in this.#updateCheckData.
|
|
let requestKey = checkType;
|
|
|
|
if (requestKey in this.#updateCheckData) {
|
|
LOG(
|
|
`CheckerService:checkForUpdates - Connecting check id ${checkId} to ` +
|
|
`existing check request.`
|
|
);
|
|
} else {
|
|
LOG(
|
|
`CheckerService:checkForUpdates - Making new check request for check ` +
|
|
`id ${checkId}.`
|
|
);
|
|
this.#updateCheckData[requestKey] = this.#makeUpdateCheckDataObject(
|
|
checkType,
|
|
this.#updateCheck(checkType, requestKey, internal)
|
|
);
|
|
}
|
|
|
|
this.#requestKeyByCheckId[checkId] = requestKey;
|
|
|
|
return {
|
|
id: checkId,
|
|
result: this.#updateCheckData[requestKey].promise,
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]),
|
|
};
|
|
}
|
|
|
|
#getChecksNotAllowedObject(checkId) {
|
|
return {
|
|
id: checkId,
|
|
result: Promise.resolve(
|
|
Object.freeze({
|
|
checksAllowed: false,
|
|
succeeded: false,
|
|
request: null,
|
|
updates: [],
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
|
|
})
|
|
),
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheck]),
|
|
};
|
|
}
|
|
|
|
async #updateCheck(checkType, requestKey, internal) {
|
|
if (!internal) {
|
|
await lazy.AUS.init();
|
|
}
|
|
|
|
await waitForOtherInstances();
|
|
|
|
let url;
|
|
try {
|
|
url = await this.getUpdateURL(checkType);
|
|
} catch (ex) {}
|
|
|
|
if (!url) {
|
|
LOG("CheckerService:#updateCheck - !url");
|
|
return this.#getCheckFailedObject("update_url_not_available");
|
|
}
|
|
|
|
let request = new XMLHttpRequest();
|
|
request.open("GET", url, true);
|
|
// Prevent the request from reading from the cache.
|
|
request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
|
|
// Prevent the request from writing to the cache.
|
|
request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
|
|
// Disable cutting edge features, like TLS 1.3, where middleboxes might
|
|
// brick us
|
|
request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative =
|
|
true;
|
|
|
|
request.overrideMimeType("text/xml");
|
|
// The Cache-Control header is only interpreted by proxies and the
|
|
// final destination. It does not help if a resource is already
|
|
// cached locally.
|
|
request.setRequestHeader("Cache-Control", "no-cache");
|
|
// HTTP/1.0 servers might not implement Cache-Control and
|
|
// might only implement Pragma: no-cache
|
|
request.setRequestHeader("Pragma", "no-cache");
|
|
|
|
const UPDATE_CHECK_LOAD_SUCCESS = 1;
|
|
const UPDATE_CHECK_LOAD_ERROR = 2;
|
|
const UPDATE_CHECK_CANCELLED = 3;
|
|
|
|
let result = await new Promise(resolve => {
|
|
// It's important that nothing potentially asynchronous happens between
|
|
// checking if the request has been cancelled and starting the request.
|
|
// If an update check cancellation happens before dispatching the request
|
|
// and we end up dispatching it anyways, we will never call cancel on the
|
|
// request later and the cancellation effectively won't happen.
|
|
if (!(requestKey in this.#updateCheckData)) {
|
|
LOG(
|
|
"CheckerService:#updateCheck - check was cancelled before request " +
|
|
"was able to start"
|
|
);
|
|
resolve(UPDATE_CHECK_CANCELLED);
|
|
return;
|
|
}
|
|
|
|
let onLoad = _event => {
|
|
request.removeEventListener("load", onLoad);
|
|
LOG("CheckerService:#updateCheck - request got 'load' event");
|
|
resolve(UPDATE_CHECK_LOAD_SUCCESS);
|
|
};
|
|
request.addEventListener("load", onLoad);
|
|
let onError = _event => {
|
|
request.removeEventListener("error", onLoad);
|
|
LOG("CheckerService:#updateCheck - request got 'error' event");
|
|
resolve(UPDATE_CHECK_LOAD_ERROR);
|
|
};
|
|
request.addEventListener("error", onError);
|
|
|
|
LOG("CheckerService:#updateCheck - sending request to: " + url);
|
|
request.send(null);
|
|
this.#updateCheckData[requestKey].request = request;
|
|
});
|
|
|
|
// Remove all entries for this request key. This marks the request and the
|
|
// associated check ids as no longer in-progress.
|
|
delete this.#updateCheckData[requestKey];
|
|
for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
|
|
if (this.#requestKeyByCheckId[checkId] == requestKey) {
|
|
delete this.#requestKeyByCheckId[checkId];
|
|
}
|
|
}
|
|
|
|
if (result == UPDATE_CHECK_CANCELLED) {
|
|
return this.#getCheckFailedObject(Cr.NS_BINDING_ABORTED);
|
|
}
|
|
|
|
if (result == UPDATE_CHECK_LOAD_ERROR) {
|
|
let status = this.#getChannelStatus(request);
|
|
LOG("CheckerService:#updateCheck - Failed. request.status: " + status);
|
|
|
|
// Set MitM pref.
|
|
try {
|
|
let secInfo = request.channel.securityInfo;
|
|
if (
|
|
secInfo.errorCodeString == "SEC_ERROR_UNKNOWN_ISSUER" &&
|
|
secInfo.serverCert &&
|
|
secInfo.serverCert.issuerName
|
|
) {
|
|
Services.prefs.setStringPref(
|
|
"security.pki.mitm_canary_issuer",
|
|
secInfo.serverCert.issuerName
|
|
);
|
|
}
|
|
} catch (e) {
|
|
LOG("CheckerService:#updateCheck - Getting secInfo failed.");
|
|
}
|
|
|
|
return this.#getCheckFailedObject(status, 404, request);
|
|
}
|
|
|
|
LOG("CheckerService:#updateCheck - request completed downloading document");
|
|
Services.prefs.clearUserPref("security.pki.mitm_canary_issuer");
|
|
// Check whether there is a mitm, i.e. check whether the root cert is
|
|
// built-in or not.
|
|
try {
|
|
let sslStatus = request.channel.securityInfo;
|
|
if (sslStatus) {
|
|
Services.prefs.setBoolPref(
|
|
"security.pki.mitm_detected",
|
|
!sslStatus.isBuiltCertChainRootBuiltInRoot
|
|
);
|
|
}
|
|
} catch (e) {
|
|
LOG("CheckerService:#updateCheck - Getting sslStatus failed.");
|
|
}
|
|
|
|
let updates;
|
|
try {
|
|
// Analyze the resulting DOM and determine the set of updates.
|
|
updates = this.#parseUpdates(request);
|
|
} catch (e) {
|
|
LOG(
|
|
"CheckerService:#updateCheck - there was a problem checking for " +
|
|
"updates. Exception: " +
|
|
e
|
|
);
|
|
let status = this.#getChannelStatus(request);
|
|
// If we can't find an error string specific to this status code,
|
|
// just use the 200 message from above, which means everything
|
|
// "looks" fine but there was probably an XML error or a bogus file.
|
|
return this.#getCheckFailedObject(status, 200, request);
|
|
}
|
|
|
|
LOG(
|
|
"CheckerService:#updateCheck - number of updates available: " +
|
|
updates.length
|
|
);
|
|
|
|
if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
|
|
Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
|
|
}
|
|
|
|
return Object.freeze({
|
|
checksAllowed: true,
|
|
succeeded: true,
|
|
request,
|
|
updates,
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param errorCode
|
|
* The error code to include in the return value. If possible, we
|
|
* will get the update status text based on this error code.
|
|
* @param defaultCode
|
|
* Optional. The error code to use to get the status text if there
|
|
* isn't status text available for `errorCode`.
|
|
* @param request
|
|
* The XMLHttpRequest used to check for updates. Or null, if one was
|
|
* never constructed.
|
|
* @returns An nsIUpdateCheckResult object indicating an error, using the
|
|
* error data passed to this function.
|
|
*/
|
|
#getCheckFailedObject(
|
|
errorCode,
|
|
defaultCode = Cr.NS_BINDING_FAILED,
|
|
request = null
|
|
) {
|
|
let update = new Update(null);
|
|
update.errorCode = errorCode;
|
|
update.statusText = getStatusTextFromCode(errorCode, defaultCode);
|
|
|
|
if (errorCode == Cr.NS_ERROR_OFFLINE) {
|
|
// We use a separate constant here because nsIUpdate.errorCode is signed
|
|
update.errorCode = NETWORK_ERROR_OFFLINE;
|
|
} else if (this.#isHttpStatusCode(errorCode)) {
|
|
update.errorCode = HTTP_ERROR_OFFSET + errorCode;
|
|
}
|
|
|
|
return Object.freeze({
|
|
checksAllowed: true,
|
|
succeeded: false,
|
|
request,
|
|
updates: [update],
|
|
QueryInterface: ChromeUtils.generateQI([Ci.nsIUpdateCheckResult]),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Returns the status code for the XMLHttpRequest
|
|
*/
|
|
#getChannelStatus(request) {
|
|
var status = 0;
|
|
try {
|
|
status = request.status;
|
|
} catch (e) {}
|
|
|
|
if (status == 0) {
|
|
status = request.channel.QueryInterface(Ci.nsIRequest).status;
|
|
}
|
|
return status;
|
|
}
|
|
|
|
#isHttpStatusCode(status) {
|
|
return status >= 100 && status <= 599;
|
|
}
|
|
|
|
/**
|
|
* @param request
|
|
* The XMLHttpRequest that successfully loaded the update XML.
|
|
* @returns An array of 0 or more nsIUpdate objects describing the available
|
|
* updates.
|
|
* @throws If the XML document element node name is not updates.
|
|
*/
|
|
#parseUpdates(request) {
|
|
let updatesElement = request.responseXML.documentElement;
|
|
if (!updatesElement) {
|
|
LOG("CheckerService:#parseUpdates - empty updates document?!");
|
|
return [];
|
|
}
|
|
|
|
if (updatesElement.nodeName != "updates") {
|
|
LOG("CheckerService:#parseUpdates - unexpected node name!");
|
|
throw new Error(
|
|
"Unexpected node name, expected: updates, got: " +
|
|
updatesElement.nodeName
|
|
);
|
|
}
|
|
|
|
let updates = [];
|
|
for (const updateElement of updatesElement.childNodes) {
|
|
if (
|
|
updateElement.nodeType != updateElement.ELEMENT_NODE ||
|
|
updateElement.localName != "update"
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
let update;
|
|
try {
|
|
update = new Update(updateElement);
|
|
} catch (e) {
|
|
LOG("CheckerService:#parseUpdates - invalid <update/>, ignoring...");
|
|
continue;
|
|
}
|
|
update.serviceURL = request.responseURL;
|
|
update.channel = lazy.UpdateUtils.UpdateChannel;
|
|
updates.push(update);
|
|
}
|
|
|
|
return updates;
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
stopCheck(checkId) {
|
|
if (!(checkId in this.#requestKeyByCheckId)) {
|
|
LOG(`CheckerService:stopCheck - Non-existent check id ${checkId}`);
|
|
return;
|
|
}
|
|
LOG(`CheckerService:stopCheck - Cancelling check id ${checkId}`);
|
|
let requestKey = this.#requestKeyByCheckId[checkId];
|
|
delete this.#requestKeyByCheckId[checkId];
|
|
if (Object.values(this.#requestKeyByCheckId).includes(requestKey)) {
|
|
LOG(
|
|
`CheckerService:stopCheck - Not actually cancelling request because ` +
|
|
`other check id's depend on it.`
|
|
);
|
|
} else {
|
|
LOG(
|
|
`CheckerService:stopCheck - This is the last check using this ` +
|
|
`request. Cancelling the request now.`
|
|
);
|
|
let request = this.#updateCheckData[requestKey].request;
|
|
delete this.#updateCheckData[requestKey];
|
|
if (request) {
|
|
LOG(`CheckerService:stopCheck - Aborting XMLHttpRequest`);
|
|
request.abort();
|
|
} else {
|
|
LOG(
|
|
`CheckerService:stopCheck - Not aborting XMLHttpRequest. It ` +
|
|
`doesn't appear to have started yet.`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIUpdateService.idl
|
|
*/
|
|
stopAllChecks() {
|
|
LOG("CheckerService:stopAllChecks - stopping all checks.");
|
|
for (const checkId of Object.keys(this.#requestKeyByCheckId)) {
|
|
this.stopCheck(checkId);
|
|
}
|
|
}
|
|
|
|
classID = Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}");
|
|
QueryInterface = ChromeUtils.generateQI([Ci.nsIUpdateChecker]);
|
|
}
|
|
|
|
class Downloader {
|
|
/**
|
|
* The nsIUpdatePatch that we are downloading
|
|
*/
|
|
_patch = null;
|
|
|
|
/**
|
|
* The nsIUpdate that we are downloading
|
|
*/
|
|
_update = null;
|
|
|
|
/**
|
|
* The nsIRequest object handling the download.
|
|
*/
|
|
_request = null;
|
|
|
|
/**
|
|
* Whether or not the update being downloaded is a complete replacement of
|
|
* the user's existing installation or a patch representing the difference
|
|
* between the new version and the previous version.
|
|
*/
|
|
isCompleteUpdate = null;
|
|
|
|
/**
|
|
* We get the nsIRequest from nsIBITS asynchronously. When downloadUpdate has
|
|
* been called, but this._request is not yet valid, _pendingRequest will be
|
|
* a promise that will resolve when this._request has been set.
|
|
*/
|
|
_pendingRequest = null;
|
|
|
|
/**
|
|
* When using BITS, cancel actions happen asynchronously. This variable
|
|
* keeps track of any cancel action that is in-progress.
|
|
* If the cancel action fails, this will be set back to null so that the
|
|
* action can be attempted again. But if the cancel action succeeds, the
|
|
* resolved promise will remain stored in this variable to prevent cancel
|
|
* from being called twice (which, for BITS, is an error).
|
|
*/
|
|
_cancelPromise = null;
|
|
|
|
/**
|
|
* BITS receives progress notifications slowly, unless a user is watching.
|
|
* This tracks what frequency notifications are happening at.
|
|
*
|
|
* This is needed because BITS downloads are started asynchronously.
|
|
* Specifically, this is needed to prevent a situation where the download is
|
|
* still starting (Downloader._pendingRequest has not resolved) when the first
|
|
* observer registers itself. Without this variable, there is no way of
|
|
* knowing whether the download was started as Active or Idle and, therefore,
|
|
* we don't know if we need to start Active mode when _pendingRequest
|
|
* resolves.
|
|
*/
|
|
_bitsActiveNotifications = false;
|
|
|
|
/**
|
|
* This is a function that when called will stop the update process from
|
|
* waiting for language pack updates. This is for safety to ensure that a
|
|
* problem in the add-ons manager doesn't delay updates by much.
|
|
*/
|
|
_langPackTimeout = null;
|
|
|
|
/**
|
|
* If gOnlyDownloadUpdatesThisSession is true, we prevent the update process
|
|
* from progressing past the downloading stage. If the download finishes,
|
|
* pretend that it hasn't in order to keep the current update in the
|
|
* "downloading" state.
|
|
*/
|
|
_pretendingDownloadIsNotDone = false;
|
|
|
|
/**
|
|
* Manages the download of updates
|
|
* @param background
|
|
* Whether or not this downloader is operating in background
|
|
* update mode.
|
|
* @param updateService
|
|
* The update service that created this downloader.
|
|
* @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
|
|
*/
|
|
async _selectPatch(update) {
|
|
// Given an update to download, we will always try to download the patch
|
|
// for a partial update over the patch for a full update.
|
|
|
|
// Look to see if any of the patches in the Update object has been
|
|
// pre-selected for download, otherwise we must figure out which one
|
|
// to select ourselves.
|
|
var selectedPatch = update.selectedPatch;
|
|
|
|
var state = selectedPatch ? selectedPatch.state : STATE_NONE;
|
|
|
|
// If this is a patch that we know about, then select it. If it is a patch
|
|
// that we do not know about, then remove it and use our default logic.
|
|
var useComplete = false;
|
|
if (selectedPatch) {
|
|
LOG(
|
|
"Downloader:_selectPatch - found existing patch with state: " + state
|
|
);
|
|
if (state == STATE_DOWNLOADING) {
|
|
LOG("Downloader:_selectPatch - resuming download");
|
|
return selectedPatch;
|
|
}
|
|
if (
|
|
state == STATE_PENDING ||
|
|
state == STATE_PENDING_SERVICE ||
|
|
state == STATE_PENDING_ELEVATE ||
|
|
state == STATE_APPLIED ||
|
|
state == STATE_APPLIED_SERVICE
|
|
) {
|
|
LOG("Downloader:_selectPatch - already downloaded");
|
|
return null;
|
|
}
|
|
|
|
// When downloading the patch failed using BITS, there hasn't been an
|
|
// attempt to download the patch using the internal application download
|
|
// mechanism, and an attempt to stage or apply the patch hasn't failed
|
|
// which indicates that a different patch should be downloaded since
|
|
// re-downloading the same patch with the internal application download
|
|
// mechanism will likely also fail when trying to stage or apply it then
|
|
// try to download the same patch using the internal application download
|
|
// mechanism.
|
|
selectedPatch.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
if (
|
|
selectedPatch.getProperty("bitsResult") != null &&
|
|
selectedPatch.getProperty("internalResult") == null &&
|
|
!selectedPatch.errorCode
|
|
) {
|
|
LOG(
|
|
"Downloader:_selectPatch - Falling back to non-BITS download " +
|
|
"mechanism for the same patch due to existing BITS result: " +
|
|
selectedPatch.getProperty("bitsResult")
|
|
);
|
|
return selectedPatch;
|
|
}
|
|
|
|
if (update && selectedPatch.type == "complete") {
|
|
// This is a pretty fatal error. Just bail.
|
|
LOG("Downloader:_selectPatch - failed to apply complete patch!");
|
|
await cleanupDownloadingUpdate();
|
|
return null;
|
|
}
|
|
|
|
// Something went wrong when we tried to apply the previous patch.
|
|
// Try the complete patch next time.
|
|
useComplete = true;
|
|
selectedPatch = null;
|
|
}
|
|
|
|
// If we were not able to discover an update from a previous download, we
|
|
// select the best patch from the given set.
|
|
var partialPatch = getPatchOfType(update, "partial");
|
|
if (!useComplete) {
|
|
selectedPatch = partialPatch;
|
|
}
|
|
if (!selectedPatch) {
|
|
if (lazy.UM.internal.readyUpdate) {
|
|
// If we already have a ready update, we download partials only.
|
|
LOG(
|
|
"Downloader:_selectPatch - not selecting a complete patch because " +
|
|
"this is not the first download of the session"
|
|
);
|
|
return null;
|
|
}
|
|
|
|
if (partialPatch) {
|
|
partialPatch.selected = false;
|
|
}
|
|
selectedPatch = getPatchOfType(update, "complete");
|
|
}
|
|
|
|
// if update only contains a partial patch, selectedPatch == null here if
|
|
// the partial patch has been attempted and fails and we're trying to get a
|
|
// complete patch
|
|
if (selectedPatch) {
|
|
selectedPatch.selected = true;
|
|
update.isCompleteUpdate = selectedPatch.type == "complete";
|
|
}
|
|
|
|
LOG(
|
|
"Downloader:_selectPatch - Patch selected. Assigning update to " +
|
|
"downloadingUpdate."
|
|
);
|
|
lazy.UM.internal.downloadingUpdate = update;
|
|
|
|
return selectedPatch;
|
|
}
|
|
|
|
/**
|
|
* Whether or not the user wants to be notified that an update is being
|
|
* downloaded.
|
|
*/
|
|
get _notifyDuringDownload() {
|
|
return Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD,
|
|
false
|
|
);
|
|
}
|
|
|
|
_notifyDownloadStatusObservers() {
|
|
if (this._notifyDuringDownload) {
|
|
let status = this.updateService.isDownloading ? "downloading" : "idle";
|
|
Services.obs.notifyObservers(this._update, "update-downloading", status);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether or not we are currently downloading something.
|
|
*/
|
|
get isBusy() {
|
|
return this._request != null || this._pendingRequest != null;
|
|
}
|
|
|
|
get usingBits() {
|
|
return this._pendingRequest != null || this._request instanceof BitsRequest;
|
|
}
|
|
|
|
/**
|
|
* Returns true if the specified patch can be downloaded with BITS.
|
|
*/
|
|
_canUseBits(patch) {
|
|
if (getCanUseBits() != "CanUseBits") {
|
|
// This will have printed its own logging. No need to print more.
|
|
return false;
|
|
}
|
|
// Regardless of success or failure, don't download the same patch with BITS
|
|
// twice.
|
|
if (patch.getProperty("bitsResult") != null) {
|
|
LOG(
|
|
"Downloader:_canUseBits - Not using BITS because it was already tried"
|
|
);
|
|
return false;
|
|
}
|
|
LOG("Downloader:_canUseBits - Patch is able to use BITS download");
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Instruct the add-ons manager to start downloading language pack updates in
|
|
* preparation for the current update.
|
|
*/
|
|
_startLangPackUpdates() {
|
|
if (!Services.prefs.getBoolPref(PREF_APP_UPDATE_LANGPACK_ENABLED, false)) {
|
|
return;
|
|
}
|
|
|
|
// A promise that we can resolve at some point to time out the language pack
|
|
// update process.
|
|
let timeoutPromise = new Promise(resolve => {
|
|
this._langPackTimeout = resolve;
|
|
});
|
|
|
|
let update = unwrap(this._update);
|
|
|
|
let existing = LangPackUpdates.get(update);
|
|
if (existing) {
|
|
// We have already started staging lang packs for this update, no need to
|
|
// do it again.
|
|
return;
|
|
}
|
|
|
|
// Note that we don't care about success or failure here, either way we will
|
|
// continue with the update process.
|
|
let langPackPromise = lazy.AddonManager.stageLangpacksForAppUpdate(
|
|
update.appVersion,
|
|
update.appVersion
|
|
)
|
|
.catch(error => {
|
|
LOG(
|
|
`Add-ons manager threw exception while updating language packs: ${error}`
|
|
);
|
|
})
|
|
.finally(() => {
|
|
let timerId = this._langPackTimeout.gleanTimerId;
|
|
this._langPackTimeout = null;
|
|
if (timerId) {
|
|
Glean.update.langpackOvertime.stopAndAccumulate(timerId);
|
|
}
|
|
});
|
|
|
|
LangPackUpdates.set(
|
|
update,
|
|
Promise.race([langPackPromise, timeoutPromise])
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Given a patch URL, return a URL possibly modified with extra query
|
|
* parameters and extra headers. The extras help identify whether this update
|
|
* is driven by a regular browsing Firefox or by a background update task.
|
|
*
|
|
* @param {string} [patchURL] Unmodified patch URL.
|
|
* @return { url, extraHeaders }
|
|
*/
|
|
_maybeWithExtras(patchURL) {
|
|
let shouldAddExtras = true;
|
|
if (AppConstants.MOZ_APP_NAME !== "firefox") {
|
|
shouldAddExtras = false;
|
|
}
|
|
if (Services.policies) {
|
|
let policies = Services.policies.getActivePolicies();
|
|
if (policies) {
|
|
if ("AppUpdateURL" in policies) {
|
|
shouldAddExtras = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!shouldAddExtras) {
|
|
LOG("Downloader:_maybeWithExtras - Not adding extras");
|
|
return { url: patchURL, extraHeaders: "\r\n" };
|
|
}
|
|
|
|
LOG("Downloader:_maybeWithExtras - Adding extras");
|
|
|
|
let modeStr = lazy.gIsBackgroundTaskMode ? "1" : "0";
|
|
let extraHeaders = `X-BackgroundTaskMode: ${modeStr}\r\n`;
|
|
let extraParameters = [["backgroundTaskMode", modeStr]];
|
|
|
|
if (lazy.gIsBackgroundTaskMode) {
|
|
const bts = Cc["@mozilla.org/backgroundtasks;1"].getService(
|
|
Ci.nsIBackgroundTasks
|
|
);
|
|
extraHeaders += `X-BackgroundTaskName: ${bts.backgroundTaskName()}\r\n`;
|
|
extraParameters.push(["backgroundTaskName", bts.backgroundTaskName()]);
|
|
}
|
|
|
|
extraHeaders += "\r\n";
|
|
|
|
let url = patchURL;
|
|
let parsedUrl = URL.parse(url);
|
|
if (parsedUrl) {
|
|
for (let [p, v] of extraParameters) {
|
|
parsedUrl.searchParams.set(p, v);
|
|
}
|
|
url = parsedUrl.href;
|
|
} else {
|
|
LOG("Downloader:_maybeWithExtras - Failed to parse patch URL!");
|
|
}
|
|
|
|
return { url, extraHeaders };
|
|
}
|
|
|
|
/**
|
|
* Download and stage the given update.
|
|
* @param update
|
|
* A nsIUpdate object to download a patch for. Cannot be null.
|
|
*/
|
|
async downloadUpdate(update) {
|
|
LOG("Downloader:downloadUpdate");
|
|
if (!update) {
|
|
AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE);
|
|
throw Components.Exception("", Cr.NS_ERROR_NULL_POINTER);
|
|
}
|
|
|
|
var updateDir = getDownloadingUpdateDir();
|
|
|
|
this._update = update;
|
|
|
|
// This function may return null, which indicates that there are no patches
|
|
// to download.
|
|
this._patch = await this._selectPatch(update);
|
|
if (!this._patch) {
|
|
LOG("Downloader:downloadUpdate - no patch to download");
|
|
AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
|
|
}
|
|
// The update and the patch implement nsIWritablePropertyBag. Expose that
|
|
// interface immediately after a patch is assigned so that
|
|
// this.(_patch|_update).(get|set)Property can always safely be called.
|
|
this._update.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
this._patch.QueryInterface(Ci.nsIWritablePropertyBag);
|
|
|
|
if (
|
|
this._update.getProperty("disableBackgroundUpdates") != null &&
|
|
lazy.gIsBackgroundTaskMode
|
|
) {
|
|
LOG(
|
|
"Downloader:downloadUpdate - Background update disabled by update " +
|
|
"advertisement"
|
|
);
|
|
return Ci.nsIApplicationUpdateService.DOWNLOAD_FAILURE_GENERIC;
|
|
}
|
|
|
|
this.isCompleteUpdate = this._patch.type == "complete";
|
|
|
|
let canUseBits = false;
|
|
// Allow the advertised update to disable BITS.
|
|
if (this._update.getProperty("disableBITS") != null) {
|
|
LOG(
|
|
"Downloader:downloadUpdate - BITS downloads disabled by update " +
|
|
"advertisement"
|
|
);
|
|
} else {
|
|
canUseBits = this._canUseBits(this._patch);
|
|
}
|
|
|
|
// When using Firefox and Mozilla's update server, add extra headers and
|
|
// extra query parameters identifying whether this request is on behalf of a
|
|
// regular browsing profile (0) or a background task (1). This helps
|
|
// understand bandwidth usage of background updates in production.
|
|
let { url, extraHeaders } = this._maybeWithExtras(this._patch.URL);
|
|
|
|
if (!canUseBits) {
|
|
this._pendingRequest = null;
|
|
|
|
let patchFile = updateDir.clone();
|
|
patchFile.append(FILE_UPDATE_MAR);
|
|
|
|
// Background updates generally should not fall back to internal (Necko)
|
|
// downloads: on Windows, they should only use Windows BITS. In
|
|
// automation, this pref allows Necko for testing.
|
|
let allowDownloadsWithoutBITS =
|
|
Cu.isInAutomation &&
|
|
Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_BACKGROUND_ALLOWDOWNLOADSWITHOUTBITS,
|
|
false
|
|
);
|
|
if (lazy.gIsBackgroundTaskMode && !allowDownloadsWithoutBITS) {
|
|
// We don't normally run a background update if we can't use BITS, but
|
|
// this branch is possible because we do fall back from BITS failures by
|
|
// attempting an internal download.
|
|
// If this happens, we are just going to need to wait for interactive
|
|
// Firefox to download the update. We don't, however, want to be in the
|
|
// "downloading" state when interactive Firefox runs because we want to
|
|
// download the newest update available which, at that point, may not be
|
|
// the one that we are currently trying to download.
|
|
// However, we can't just unconditionally clobber the current update
|
|
// because interactive Firefox might already be part way through an
|
|
// internal update download, and we definitely don't want to interrupt
|
|
// that.
|
|
let readyUpdateDir = getReadyUpdateDir();
|
|
let status = readStatusFile(readyUpdateDir);
|
|
// nsIIncrementalDownload doesn't use an intermediate download location
|
|
// for partially downloaded files. If we have started an update
|
|
// download with it, it will be available at its ultimate location.
|
|
if (!(status == STATE_DOWNLOADING && patchFile.exists())) {
|
|
LOG(
|
|
"Downloader:downloadUpdate - Can't download with internal " +
|
|
"downloader from a background task. Cleaning up downloading " +
|
|
"update."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
}
|
|
return Ci.nsIApplicationUpdateService
|
|
.DOWNLOAD_FAILURE_CANNOT_RESUME_IN_BACKGROUND;
|
|
}
|
|
|
|
// The interval is 0 since there is no need to throttle downloads.
|
|
let interval = 0;
|
|
|
|
LOG(
|
|
"Downloader:downloadUpdate - Starting nsIIncrementalDownload with " +
|
|
"url: " +
|
|
url +
|
|
", path: " +
|
|
patchFile.path +
|
|
", interval: " +
|
|
interval
|
|
);
|
|
let uri = Services.io.newURI(url);
|
|
|
|
this._request = Cc[
|
|
"@mozilla.org/network/incremental-download;1"
|
|
].createInstance(Ci.nsIIncrementalDownload);
|
|
|
|
this._request.init(
|
|
uri,
|
|
patchFile,
|
|
DOWNLOAD_CHUNK_SIZE,
|
|
interval,
|
|
extraHeaders
|
|
);
|
|
this._request.start(this, null);
|
|
} else {
|
|
this._bitsActiveNotifications = this.hasDownloadListeners;
|
|
this._cancelPromise = null;
|
|
|
|
this._pendingRequest = this.updateService.makeBitsRequest({
|
|
activeListeners: this.hasDownloadListeners,
|
|
bitsId: this._patch.getProperty("bitsId"),
|
|
observer: this,
|
|
url,
|
|
extraHeaders,
|
|
});
|
|
|
|
let request;
|
|
try {
|
|
request = await this._pendingRequest;
|
|
} catch (error) {
|
|
if (
|
|
(error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_GET_BITS_JOB ||
|
|
error.type == Ci.nsIBits.ERROR_TYPE_FAILED_TO_CONNECT_TO_BCM) &&
|
|
error.action == Ci.nsIBits.ERROR_ACTION_MONITOR_DOWNLOAD &&
|
|
error.stage == Ci.nsIBits.ERROR_STAGE_BITS_CLIENT &&
|
|
error.codeType == Ci.nsIBits.ERROR_CODE_TYPE_HRESULT &&
|
|
error.code == HRESULT_E_ACCESSDENIED
|
|
) {
|
|
LOG(
|
|
"Downloader:downloadUpdate - Failed to connect to existing " +
|
|
"BITS job. It is likely owned by another user."
|
|
);
|
|
// This isn't really a failure code since the BITS job may be working
|
|
// just fine on another account, so convert this to a code that
|
|
// indicates that. This will make it easier to identify in telemetry.
|
|
error.type = Ci.nsIBits.ERROR_TYPE_ACCESS_DENIED_EXPECTED;
|
|
error.codeType = Ci.nsIBits.ERROR_CODE_TYPE_NONE;
|
|
error.code = null;
|
|
// When we detect this situation, disable BITS until Firefox shuts
|
|
// down. There are a couple of reasons for this. First, without any
|
|
// kind of flag, we enter an infinite loop here where we keep trying
|
|
// BITS over and over again (normally setting bitsResult prevents
|
|
// this, but we don't know the result of the BITS job, so we don't
|
|
// want to set that). Second, since we are trying to update, this
|
|
// process must have the update mutex. We don't ever give up the
|
|
// update mutex, so even if the other user starts Firefox, they will
|
|
// not complete the BITS job while this Firefox instance is around.
|
|
gBITSInUseByAnotherUser = true;
|
|
} else {
|
|
this._patch.setProperty("bitsResult", Cr.NS_ERROR_FAILURE);
|
|
lazy.UM.saveUpdates();
|
|
|
|
LOG(
|
|
"Downloader:downloadUpdate - Failed to start to BITS job. " +
|
|
"Error: " +
|
|
error
|
|
);
|
|
}
|
|
|
|
this._pendingRequest = null;
|
|
|
|
AUSTLMY.pingBitsError(this.isCompleteUpdate, error);
|
|
|
|
// Try download again with nsIIncrementalDownload
|
|
return this.downloadUpdate(this._update);
|
|
}
|
|
|
|
this._request = request;
|
|
this._patch.setProperty("bitsId", request.bitsId);
|
|
|
|
LOG(
|
|
"Downloader:downloadUpdate - BITS download running. BITS ID: " +
|
|
request.bitsId
|
|
);
|
|
|
|
if (this.hasDownloadListeners) {
|
|
this._maybeStartActiveNotifications();
|
|
} else {
|
|
this._maybeStopActiveNotifications();
|
|
}
|
|
|
|
lazy.UM.saveUpdates();
|
|
this._pendingRequest = null;
|
|
}
|
|
|
|
if (!lazy.UM.internal.readyUpdate) {
|
|
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."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
} else {
|
|
allFailed = false;
|
|
}
|
|
}
|
|
|
|
// Check if there is a complete update patch that can be downloaded.
|
|
if (
|
|
allFailed &&
|
|
!this._update.isCompleteUpdate &&
|
|
this._update.patchCount == 2
|
|
) {
|
|
LOG(
|
|
"Downloader:onStopRequest - verification of patch failed, " +
|
|
"downloading complete update patch"
|
|
);
|
|
this._update.isCompleteUpdate = true;
|
|
let result = await this.downloadUpdate(this._update);
|
|
|
|
if (result != Ci.nsIApplicationUpdateService.DOWNLOAD_SUCCESS) {
|
|
LOG(
|
|
"Downloader:onStopRequest - Failed to fall back to complete " +
|
|
"patch. Cleaning up downloading update."
|
|
);
|
|
await cleanupDownloadingUpdate();
|
|
} else {
|
|
allFailed = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (allFailed) {
|
|
let downloadAttempts = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS,
|
|
0
|
|
);
|
|
downloadAttempts++;
|
|
Services.prefs.setIntPref(
|
|
PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS,
|
|
downloadAttempts
|
|
);
|
|
let maxAttempts = Math.min(
|
|
Services.prefs.getIntPref(PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS, 2),
|
|
10
|
|
);
|
|
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_IDLE);
|
|
|
|
if (downloadAttempts > maxAttempts) {
|
|
LOG(
|
|
"Downloader:onStopRequest - notifying observers of error. " +
|
|
"topic: update-error, status: download-attempts-exceeded, " +
|
|
"downloadAttempts: " +
|
|
downloadAttempts +
|
|
" " +
|
|
"maxAttempts: " +
|
|
maxAttempts
|
|
);
|
|
Services.obs.notifyObservers(
|
|
this._update,
|
|
"update-error",
|
|
"download-attempts-exceeded"
|
|
);
|
|
} else {
|
|
this._update.selectedPatch.selected = false;
|
|
LOG(
|
|
"Downloader:onStopRequest - notifying observers of error. " +
|
|
"topic: update-error, status: download-attempt-failed"
|
|
);
|
|
Services.obs.notifyObservers(
|
|
this._update,
|
|
"update-error",
|
|
"download-attempt-failed"
|
|
);
|
|
}
|
|
// We don't care about language pack updates now.
|
|
this._langPackTimeout = null;
|
|
LangPackUpdates.delete(unwrap(this._update));
|
|
|
|
// Prevent leaking the update object (bug 454964).
|
|
this._update = null;
|
|
|
|
// allFailed indicates that we didn't (successfully) call downloadUpdate
|
|
// to try to download a different MAR. In this case, this Downloader
|
|
// is no longer being used.
|
|
this.updateService._downloader = null;
|
|
}
|
|
// A complete download has been initiated or the failure was handled.
|
|
return;
|
|
}
|
|
|
|
// If the download has succeeded or failed, we are done with this Downloader
|
|
// object. However, in some cases (ex: network disconnection), we will
|
|
// attempt to resume using this same Downloader.
|
|
if (state != STATE_DOWNLOADING) {
|
|
this.updateService._downloader = null;
|
|
}
|
|
|
|
if (
|
|
state == STATE_PENDING ||
|
|
state == STATE_PENDING_SERVICE ||
|
|
state == STATE_PENDING_ELEVATE
|
|
) {
|
|
if (getCanStageUpdates()) {
|
|
LOG(
|
|
"Downloader:onStopRequest - attempting to stage update: " +
|
|
this._update.name
|
|
);
|
|
// Stage the update
|
|
let stagingStarted = true;
|
|
try {
|
|
Cc["@mozilla.org/updates/update-processor;1"]
|
|
.createInstance(Ci.nsIUpdateProcessor)
|
|
.processUpdate();
|
|
} catch (e) {
|
|
LOG(
|
|
"Downloader:onStopRequest - failed to stage update. Exception: " + e
|
|
);
|
|
stagingStarted = false;
|
|
}
|
|
if (stagingStarted) {
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_STAGING);
|
|
} else {
|
|
// Fail gracefully in case the application does not support the update
|
|
// processor service.
|
|
shouldShowPrompt = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we're still waiting on language pack updates then run a timer to time
|
|
// out the attempt after an appropriate amount of time.
|
|
if (this._langPackTimeout) {
|
|
// Start a timer to measure how much longer it takes for the language
|
|
// packs to stage.
|
|
this._langPackTimeout.gleanTimerId =
|
|
Glean.update.langpackOvertime.start();
|
|
|
|
lazy.setTimeout(
|
|
this._langPackTimeout,
|
|
Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_LANGPACK_TIMEOUT,
|
|
LANGPACK_UPDATE_DEFAULT_TIMEOUT
|
|
)
|
|
);
|
|
}
|
|
|
|
// Do this after *everything* else, since it will likely cause the app
|
|
// to shut down.
|
|
if (shouldShowPrompt) {
|
|
// Wait for language packs to stage before showing any prompt to restart.
|
|
let update = this._update;
|
|
promiseLangPacksUpdated(update).then(() => {
|
|
LOG(
|
|
"Downloader:onStopRequest - Notifying observers that " +
|
|
"an update was downloaded. topic: update-downloaded, status: " +
|
|
update.state
|
|
);
|
|
transitionState(Ci.nsIApplicationUpdateService.STATE_PENDING);
|
|
Services.obs.notifyObservers(update, "update-downloaded", update.state);
|
|
});
|
|
}
|
|
|
|
if (shouldRegisterOnlineObserver) {
|
|
LOG("Downloader:onStopRequest - Registering online observer");
|
|
this.updateService._registerOnlineObserver();
|
|
} else if (shouldRetrySoon) {
|
|
LOG("Downloader:onStopRequest - Retrying soon");
|
|
this.updateService._consecutiveSocketErrors++;
|
|
if (this.updateService._retryTimer) {
|
|
this.updateService._retryTimer.cancel();
|
|
}
|
|
this.updateService._retryTimer = Cc[
|
|
"@mozilla.org/timer;1"
|
|
].createInstance(Ci.nsITimer);
|
|
this.updateService._retryTimer.initWithCallback(
|
|
async () => {
|
|
await this.updateService._attemptResume();
|
|
},
|
|
retryTimeout,
|
|
Ci.nsITimer.TYPE_ONE_SHOT
|
|
);
|
|
} else {
|
|
// Prevent leaking the update object (bug 454964)
|
|
this._update = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function should be called when shutting down so that resources get
|
|
* freed properly.
|
|
*/
|
|
async cleanup() {
|
|
if (this.usingBits) {
|
|
if (this._pendingRequest) {
|
|
await this._pendingRequest;
|
|
}
|
|
this._request.shutdown();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* See nsIInterfaceRequestor.idl
|
|
*/
|
|
getInterface(iid) {
|
|
// The network request may require proxy authentication, so provide the
|
|
// default nsIAuthPrompt if requested.
|
|
if (iid.equals(Ci.nsIAuthPrompt)) {
|
|
var prompt =
|
|
Cc["@mozilla.org/network/default-auth-prompt;1"].createInstance();
|
|
return prompt.QueryInterface(iid);
|
|
}
|
|
throw Components.Exception("", Cr.NS_NOINTERFACE);
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
Ci.nsIRequestObserver,
|
|
Ci.nsIProgressEventSink,
|
|
Ci.nsIInterfaceRequestor,
|
|
]);
|
|
}
|
|
|
|
// On macOS, all browser windows can be closed without Firefox exiting. If it
|
|
// is left in this state for a while and an update is pending, we should restart
|
|
// Firefox on our own to apply the update. This class will do that
|
|
// automatically.
|
|
class RestartOnLastWindowClosed {
|
|
#enabled = false;
|
|
#hasShutdown = false;
|
|
|
|
#restartTimer = null;
|
|
#restartTimerExpired = false;
|
|
|
|
constructor() {
|
|
this.#maybeEnableOrDisable();
|
|
|
|
Services.prefs.addObserver(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
|
|
this
|
|
);
|
|
Services.obs.addObserver(this, "quit-application");
|
|
}
|
|
|
|
shutdown() {
|
|
LOG("RestartOnLastWindowClosed.shutdown - Shutting down");
|
|
this.#hasShutdown = true;
|
|
|
|
Services.prefs.removeObserver(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
|
|
this
|
|
);
|
|
Services.obs.removeObserver(this, "quit-application");
|
|
|
|
this.#maybeEnableOrDisable();
|
|
}
|
|
|
|
get shouldEnable() {
|
|
if (AppConstants.platform != "macosx") {
|
|
return false;
|
|
}
|
|
if (this.#hasShutdown) {
|
|
return false;
|
|
}
|
|
return Services.prefs.getBoolPref(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED,
|
|
false
|
|
);
|
|
}
|
|
|
|
get enabled() {
|
|
return this.#enabled;
|
|
}
|
|
|
|
observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case "nsPref:changed":
|
|
if (data == PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_ENABLED) {
|
|
this.#maybeEnableOrDisable();
|
|
}
|
|
break;
|
|
case "quit-application":
|
|
this.shutdown();
|
|
break;
|
|
case "domwindowclosed":
|
|
this.#onWindowClose();
|
|
break;
|
|
case "domwindowopened":
|
|
this.#onWindowOpen();
|
|
break;
|
|
case "update-downloaded":
|
|
case "update-staged":
|
|
this.#onUpdateReady(data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Returns true if any windows are open. Otherwise, false.
|
|
#windowsAreOpen() {
|
|
// eslint-disable-next-line no-unused-vars
|
|
for (const win of Services.wm.getEnumerator(null)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Enables or disables this class's functionality based on the value of
|
|
// this.shouldEnable. Does nothing if the class is already in the right state
|
|
// (i.e. if the class should be enabled and already is, or should be disabled
|
|
// and already is).
|
|
#maybeEnableOrDisable() {
|
|
if (this.shouldEnable) {
|
|
if (this.#enabled) {
|
|
return;
|
|
}
|
|
LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Enabling");
|
|
|
|
Services.obs.addObserver(this, "domwindowclosed");
|
|
Services.obs.addObserver(this, "domwindowopened");
|
|
Services.obs.addObserver(this, "update-downloaded");
|
|
Services.obs.addObserver(this, "update-staged");
|
|
|
|
this.#restartTimer = null;
|
|
this.#restartTimerExpired = false;
|
|
|
|
this.#enabled = true;
|
|
|
|
// Synchronize with external state.
|
|
this.#onWindowClose();
|
|
} else {
|
|
if (!this.#enabled) {
|
|
return;
|
|
}
|
|
LOG("RestartOnLastWindowClosed.#maybeEnableOrDisable - Disabling");
|
|
|
|
Services.obs.removeObserver(this, "domwindowclosed");
|
|
Services.obs.removeObserver(this, "domwindowopened");
|
|
Services.obs.removeObserver(this, "update-downloaded");
|
|
Services.obs.removeObserver(this, "update-staged");
|
|
|
|
this.#enabled = false;
|
|
|
|
if (this.#restartTimer) {
|
|
this.#restartTimer.cancel();
|
|
}
|
|
this.#restartTimer = null;
|
|
}
|
|
}
|
|
|
|
// Note: Since we keep track of the update state even when this class is
|
|
// disabled, this function will run even in that case.
|
|
#onUpdateReady(updateState) {
|
|
// Note that we do not count pending-elevate as a ready state, because we
|
|
// cannot silently restart in that state.
|
|
if (
|
|
[
|
|
STATE_APPLIED,
|
|
STATE_PENDING,
|
|
STATE_APPLIED_SERVICE,
|
|
STATE_PENDING_SERVICE,
|
|
].includes(updateState)
|
|
) {
|
|
if (this.#enabled) {
|
|
LOG("RestartOnLastWindowClosed.#onUpdateReady - update ready");
|
|
this.#maybeRestartBrowser();
|
|
}
|
|
} else if (this.#enabled) {
|
|
LOG(
|
|
`RestartOnLastWindowClosed.#onUpdateReady - Not counting update as ` +
|
|
`ready because the state is ${updateState}`
|
|
);
|
|
}
|
|
}
|
|
|
|
#onWindowClose() {
|
|
if (!this.#windowsAreOpen()) {
|
|
this.#onLastWindowClose();
|
|
}
|
|
}
|
|
|
|
#onLastWindowClose() {
|
|
if (this.#restartTimer || this.#restartTimerExpired) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#onLastWindowClose - Restart timer is " +
|
|
"either already running or has already expired"
|
|
);
|
|
return;
|
|
}
|
|
|
|
let timeout = Services.prefs.getIntPref(
|
|
PREF_APP_UPDATE_NO_WINDOW_AUTO_RESTART_DELAY_MS,
|
|
5 * 60 * 1000
|
|
);
|
|
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#onLastWindowClose - Last window closed. " +
|
|
"Starting restart timer"
|
|
);
|
|
this.#restartTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
this.#restartTimer.initWithCallback(
|
|
() => this.#onRestartTimerExpire(),
|
|
timeout,
|
|
Ci.nsITimer.TYPE_ONE_SHOT
|
|
);
|
|
}
|
|
|
|
#onWindowOpen() {
|
|
if (this.#restartTimer) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#onWindowOpen - Window opened. Cancelling " +
|
|
"restart timer."
|
|
);
|
|
this.#restartTimer.cancel();
|
|
}
|
|
this.#restartTimer = null;
|
|
this.#restartTimerExpired = false;
|
|
}
|
|
|
|
#onRestartTimerExpire() {
|
|
LOG("RestartOnLastWindowClosed.#onRestartTimerExpire - Timer Expired");
|
|
|
|
this.#restartTimer = null;
|
|
this.#restartTimerExpired = true;
|
|
this.#maybeRestartBrowser();
|
|
}
|
|
|
|
#maybeRestartBrowser() {
|
|
if (!this.#restartTimerExpired) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - Still waiting for " +
|
|
"all windows to be closed and restartTimer to expire. " +
|
|
"(not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (lazy.AUS.currentState != Ci.nsIApplicationUpdateService.STATE_PENDING) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - No update ready. " +
|
|
"(not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (getElevationRequired()) {
|
|
// We check for STATE_PENDING_ELEVATE elsewhere, but this is actually
|
|
// different from that because it is technically possible that the user
|
|
// gave permission to elevate, but we haven't actually elevated yet.
|
|
// This is a bit of a corner case. We only call elevationOptedIn() right
|
|
// before we restart to apply the update immediately. But it is possible
|
|
// that something could stop the browser from shutting down.
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - This update will " +
|
|
"require user elevation (not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.#windowsAreOpen()) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - Window " +
|
|
"unexpectedly still open! (not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (!this.shouldEnable) {
|
|
LOG(
|
|
"RestartOnLastWindowClosed.#maybeRestartBrowser - Unexpectedly " +
|
|
"attempted to restart when RestartOnLastWindowClosed ought to be " +
|
|
"disabled! (not restarting)"
|
|
);
|
|
return;
|
|
}
|
|
|
|
LOG("RestartOnLastWindowClosed.#maybeRestartBrowser - Restarting now");
|
|
Glean.update.noWindowAutoRestarts.add(1);
|
|
Services.startup.quit(
|
|
Ci.nsIAppStartup.eAttemptQuit |
|
|
Ci.nsIAppStartup.eRestart |
|
|
Ci.nsIAppStartup.eSilently
|
|
);
|
|
}
|
|
}
|
|
// Nothing actually uses this variable at the moment, but let's make sure that
|
|
// we hold the reference to the RestartOnLastWindowClosed instance somewhere.
|
|
// eslint-disable-next-line no-unused-vars
|
|
let restartOnLastWindowClosed = new RestartOnLastWindowClosed();
|