Bug 1798767 - Port bug 1797888: Use AppUpdater.jsm from toolkit instead of the forked copy. r=mkmelin
Differential Revision: https://phabricator.services.mozilla.com/D161719 --HG-- extra : amend_source : d9d309ec934498aa64e873ee775e111946129726
This commit is contained in:
Родитель
fa3770b473
Коммит
5f16ac55bb
|
@ -16,7 +16,7 @@ ChromeUtils.defineESModuleGetters(this, {
|
|||
});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
AppUpdater: "resource:///modules/AppUpdater.jsm",
|
||||
AppUpdater: "resource://gre/modules/AppUpdater.jsm",
|
||||
DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
|
||||
});
|
||||
|
||||
|
|
|
@ -1,943 +0,0 @@
|
|||
/* 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/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
var EXPORTED_SYMBOLS = ["AppUpdater"];
|
||||
|
||||
var { XPCOMUtils } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
||||
);
|
||||
|
||||
var gLogfileOutputStream;
|
||||
|
||||
const { AppConstants } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/AppConstants.sys.mjs"
|
||||
);
|
||||
const { FileUtils } = ChromeUtils.importESModule(
|
||||
"resource://gre/modules/FileUtils.sys.mjs"
|
||||
);
|
||||
const PREF_APP_UPDATE_LOG = "app.update.log";
|
||||
const PREF_APP_UPDATE_LOG_FILE = "app.update.log.file";
|
||||
const KEY_PROFILE_DIR = "ProfD";
|
||||
const FILE_UPDATE_MESSAGES = "update_messages.log";
|
||||
const lazy = {};
|
||||
ChromeUtils.defineESModuleGetters(lazy, {
|
||||
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
|
||||
});
|
||||
XPCOMUtils.defineLazyGetter(lazy, "gLogEnabled", function() {
|
||||
return (
|
||||
Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
|
||||
Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false)
|
||||
);
|
||||
});
|
||||
XPCOMUtils.defineLazyGetter(lazy, "gLogfileEnabled", function() {
|
||||
return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
|
||||
});
|
||||
|
||||
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
|
||||
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
|
||||
|
||||
class AbortError extends Error {
|
||||
constructor(...params) {
|
||||
super(...params);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `AbortablePromise`s automatically add themselves to this set on construction
|
||||
* and remove themselves when they settle.
|
||||
*/
|
||||
var gPendingAbortablePromises = new Set();
|
||||
|
||||
/**
|
||||
* Creates a Promise that can be resolved immediately with an abort method.
|
||||
*
|
||||
* Note that the underlying Promise will probably still run to completion since
|
||||
* there isn't any general way to abort Promises. So if it is possible to abort
|
||||
* the operation instead or in addition to using this class, that is preferable.
|
||||
*/
|
||||
class AbortablePromise {
|
||||
#abortFn;
|
||||
#promise;
|
||||
#hasCompleted = false;
|
||||
|
||||
constructor(promise) {
|
||||
let abortPromise = new Promise((resolve, reject) => {
|
||||
this.#abortFn = () => reject(new AbortError());
|
||||
});
|
||||
this.#promise = Promise.race([promise, abortPromise]);
|
||||
this.#promise = this.#promise.finally(() => {
|
||||
this.#hasCompleted = true;
|
||||
gPendingAbortablePromises.delete(this);
|
||||
});
|
||||
gPendingAbortablePromises.add(this);
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.#hasCompleted) {
|
||||
return;
|
||||
}
|
||||
this.#abortFn();
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be `await`ed on to get the result of the `AbortablePromise`. It
|
||||
* will resolve with the value that the Promise provided to the constructor
|
||||
* resolves with.
|
||||
*/
|
||||
get promise() {
|
||||
return this.#promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will be `true` if the Promise provided to the constructor has resolved or
|
||||
* `abort()` has been called. Otherwise `false`.
|
||||
*/
|
||||
get hasCompleted() {
|
||||
return this.#hasCompleted;
|
||||
}
|
||||
}
|
||||
|
||||
function makeAbortable(promise) {
|
||||
let abortable = new AbortablePromise(promise);
|
||||
return abortable.promise;
|
||||
}
|
||||
|
||||
function abortAllPromises() {
|
||||
for (const promise of gPendingAbortablePromises) {
|
||||
promise.abort();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class checks for app updates in the foreground. It has several public
|
||||
* methods for checking for updates, downloading updates, stopping the current
|
||||
* update, and getting the current update status. It can also register
|
||||
* listeners that will be called back as different stages of updates occur.
|
||||
*/
|
||||
class AppUpdater {
|
||||
#listeners = new Set();
|
||||
#status = AppUpdater.STATUS.NEVER_CHECKED;
|
||||
// This will basically be set to `true` when `AppUpdater.check` is called and
|
||||
// back to `false` right before it returns.
|
||||
// It is also set to `true` during an update swap and back to `false` when the
|
||||
// swap completes.
|
||||
#updateBusy = false;
|
||||
// When settings require that the user be asked for permission to download
|
||||
// updates and we have an update to download, we will assign a function.
|
||||
// Calling this function allows the download to proceed.
|
||||
#permissionToDownloadGivenFn = null;
|
||||
#_update = null;
|
||||
// Keeps track of if we have an `update-swap` listener connected. We only
|
||||
// connect it when the status is `READY_TO_RESTART`, but we can't use that to
|
||||
// tell if its connected because we might be in the middle of an update swap
|
||||
// in which case the status will have temporarily changed.
|
||||
#swapListenerConnected = false;
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
this.QueryInterface = ChromeUtils.generateQI([
|
||||
"nsIObserver",
|
||||
"nsISupportsWeakReference",
|
||||
]);
|
||||
|
||||
// This one call observes PREF_APP_UPDATE_LOG and PREF_APP_UPDATE_LOG_FILE
|
||||
Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this, /* ownWeak */ true);
|
||||
} catch (e) {
|
||||
this.#onException(e);
|
||||
}
|
||||
}
|
||||
|
||||
#onException(exception) {
|
||||
try {
|
||||
this.#update = null;
|
||||
|
||||
if (this.#swapListenerConnected) {
|
||||
LOG("AppUpdater:#onException - Removing update-swap listener");
|
||||
Services.obs.removeObserver(this, "update-swap");
|
||||
this.#swapListenerConnected = false;
|
||||
}
|
||||
|
||||
if (exception instanceof AbortError) {
|
||||
// This should be where we end up if `AppUpdater.stop()` is called while
|
||||
// `AppUpdater.check` is running or during an update swap.
|
||||
LOG(
|
||||
"AppUpdater:#onException - Caught AbortError. Setting status " +
|
||||
"NEVER_CHECKED"
|
||||
);
|
||||
this.#setStatus(AppUpdater.STATUS.NEVER_CHECKED);
|
||||
} else {
|
||||
LOG(
|
||||
"AppUpdater:#onException - Exception caught. Setting status " +
|
||||
"INTERNAL_ERROR"
|
||||
);
|
||||
console.error(exception);
|
||||
this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR);
|
||||
}
|
||||
} catch (e) {
|
||||
LOG(
|
||||
"AppUpdater:#onException - Caught additional exception while " +
|
||||
"handling previous exception"
|
||||
);
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be accessed by consumers to inspect the update that is being
|
||||
* prepared for installation. It will always be null if `AppUpdater.check`
|
||||
* hasn't been called yet. `AppUpdater.check` will set it to an instance of
|
||||
* nsIUpdate once there is one available. This may be immediate, if an update
|
||||
* is already downloading or has been downloaded. It may be delayed if an
|
||||
* update check needs to be performed first. It also may remain null if
|
||||
* Thunderbird is up to date or if the update check fails.
|
||||
*
|
||||
* Regarding the difference between `AppUpdater.update`, `AppUpdater.#update`,
|
||||
* and `AppUpdater.#_update`:
|
||||
* - `AppUpdater.update` and `AppUpdater.#update` are effectively identical
|
||||
* except that `AppUpdater.update` is readonly since it should not be
|
||||
* changed from outside this class.
|
||||
* - `AppUpdater.#_update` should only ever be modified by the setter for
|
||||
* `AppUpdater.#update` in order to ensure that the "foregroundDownload"
|
||||
* property is set on assignment.
|
||||
* The quick and easy rule for using these is to always use `#update`
|
||||
* internally and (of course) always use `update` externally.
|
||||
*/
|
||||
get update() {
|
||||
return this.#update;
|
||||
}
|
||||
|
||||
get #update() {
|
||||
return this.#_update;
|
||||
}
|
||||
|
||||
set #update(update) {
|
||||
this.#_update = update;
|
||||
if (this.#_update) {
|
||||
this.#_update.QueryInterface(Ci.nsIWritablePropertyBag);
|
||||
this.#_update.setProperty("foregroundDownload", "true");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The main entry point for checking for updates. As different stages of the
|
||||
* check and possible subsequent update occur, the updater's status is set and
|
||||
* listeners are called.
|
||||
*
|
||||
* Note that this is the correct entry point, regardless of the current state
|
||||
* of the updater. Although the function name suggests that this function will
|
||||
* start an update check, it will only do that if we aren't already in the
|
||||
* update process. Otherwise, it will simply monitor the update process,
|
||||
* update its own status, and call listeners.
|
||||
*
|
||||
* This function is async and will resolve when the update is ready to
|
||||
* install, or a failure state is reached.
|
||||
* However, most callers probably don't actually want to track its progress by
|
||||
* awaiting on this function. More likely, it is desired to kick this function
|
||||
* off without awaiting and add a listener via addListener. This allows the
|
||||
* caller to see when the updater is checking for an update, downloading it,
|
||||
* etc rather than just knowing "now it's running" and "now it's done".
|
||||
*
|
||||
* Note that calling this function while this instance is already performing
|
||||
* or monitoring an update check/download will have no effect. In other words,
|
||||
* it is only really necessary/useful to call this function when the status is
|
||||
* `NEVER_CHECKED` or `NO_UPDATES_FOUND`.
|
||||
*/
|
||||
async check() {
|
||||
try {
|
||||
// We don't want to end up with multiple instances of the same `async`
|
||||
// functions waiting on the same events, so if we are already busy going
|
||||
// through the update state, don't enter this function. This must not
|
||||
// be in the try/catch that sets #updateBusy to false in its finally
|
||||
// block.
|
||||
if (this.#updateBusy) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.#onException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.#updateBusy = true;
|
||||
this.#update = null;
|
||||
|
||||
if (this.#swapListenerConnected) {
|
||||
LOG("AppUpdater:check - Removing update-swap listener");
|
||||
Services.obs.removeObserver(this, "update-swap");
|
||||
this.#swapListenerConnected = false;
|
||||
}
|
||||
|
||||
if (!AppConstants.MOZ_UPDATER || this.#updateDisabledByPackage) {
|
||||
LOG(
|
||||
"AppUpdater:check -" +
|
||||
"AppConstants.MOZ_UPDATER=" +
|
||||
AppConstants.MOZ_UPDATER +
|
||||
"this.#updateDisabledByPackage: " +
|
||||
this.#updateDisabledByPackage
|
||||
);
|
||||
this.#setStatus(AppUpdater.STATUS.NO_UPDATER);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.aus.disabled) {
|
||||
LOG("AppUpdater:check - AUS disabled");
|
||||
this.#setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY);
|
||||
return;
|
||||
}
|
||||
|
||||
let updateState = this.aus.currentState;
|
||||
let stateName = this.aus.getStateName(updateState);
|
||||
LOG(`AppUpdater:check - currentState=${stateName}`);
|
||||
|
||||
if (updateState == Ci.nsIApplicationUpdateService.STATE_PENDING) {
|
||||
LOG("AppUpdater:check - ready for restart");
|
||||
this.#onReadyToRestart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.aus.isOtherInstanceHandlingUpdates) {
|
||||
LOG("AppUpdater:check - this.aus.isOtherInstanceHandlingUpdates");
|
||||
this.#setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES);
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateState == Ci.nsIApplicationUpdateService.STATE_DOWNLOADING) {
|
||||
LOG("AppUpdater:check - downloading");
|
||||
this.#update = this.um.downloadingUpdate;
|
||||
await this.#downloadUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
if (updateState == Ci.nsIApplicationUpdateService.STATE_STAGING) {
|
||||
LOG("AppUpdater:check - staging");
|
||||
this.#update = this.um.readyUpdate;
|
||||
await this.#awaitStagingComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear prefs that could prevent a user from discovering available updates.
|
||||
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);
|
||||
}
|
||||
this.#setStatus(AppUpdater.STATUS.CHECKING);
|
||||
LOG("AppUpdater:check - starting update check");
|
||||
let check = this.checker.checkForUpdates(this.checker.FOREGROUND_CHECK);
|
||||
let result;
|
||||
try {
|
||||
result = await makeAbortable(check.result);
|
||||
} catch (e) {
|
||||
// If we are aborting, stop the update check on our way out.
|
||||
if (e instanceof AbortError) {
|
||||
this.checker.stopCheck(check.id);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (!result.checksAllowed) {
|
||||
// This shouldn't happen. The cases where this can happen should be
|
||||
// handled specifically, above.
|
||||
LOG("AppUpdater:check - !checksAllowed; INTERNAL_ERROR");
|
||||
this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.succeeded) {
|
||||
// Errors in the update check are treated as no updates found. If the
|
||||
// update check fails repeatedly without a success the user will be
|
||||
// notified with the normal app update user interface so this is safe.
|
||||
LOG("AppUpdater:check - Update check failed; NO_UPDATES_FOUND");
|
||||
this.#setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
LOG("AppUpdater:check - Update check succeeded");
|
||||
this.#update = this.aus.selectUpdate(result.updates);
|
||||
if (!this.#update) {
|
||||
LOG("AppUpdater:check - result: NO_UPDATES_FOUND");
|
||||
this.#setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#update.unsupported) {
|
||||
LOG("AppUpdater:check - result: UNSUPPORTED SYSTEM");
|
||||
this.#setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.aus.canApplyUpdates) {
|
||||
LOG("AppUpdater:check - result: MANUAL_UPDATE");
|
||||
this.#setStatus(AppUpdater.STATUS.MANUAL_UPDATE);
|
||||
return;
|
||||
}
|
||||
|
||||
let updateAuto = await makeAbortable(
|
||||
lazy.UpdateUtils.getAppUpdateAutoEnabled()
|
||||
);
|
||||
if (!updateAuto || this.aus.manualUpdateOnly) {
|
||||
LOG(
|
||||
"AppUpdater:check - Need to wait for user approval to start the " +
|
||||
"download."
|
||||
);
|
||||
|
||||
let downloadPermissionPromise = new Promise(resolve => {
|
||||
this.#permissionToDownloadGivenFn = resolve;
|
||||
});
|
||||
// There are other interfaces through which the user can start the
|
||||
// download, so we want to listen both for permission, and for the
|
||||
// download to independently start.
|
||||
let downloadStartPromise = Promise.race([
|
||||
downloadPermissionPromise,
|
||||
this.aus.stateTransition,
|
||||
]);
|
||||
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL);
|
||||
|
||||
await makeAbortable(downloadStartPromise);
|
||||
LOG("AppUpdater:check - Got user approval. Proceeding with download");
|
||||
// If we resolved because of `aus.stateTransition`, we may actually be
|
||||
// downloading a different update now.
|
||||
if (this.um.downloadingUpdate) {
|
||||
this.#update = this.um.downloadingUpdate;
|
||||
}
|
||||
} else {
|
||||
LOG(
|
||||
"AppUpdater:check - updateAuto is active and " +
|
||||
"manualUpdateOnlydateOnly is inactive. Start the download."
|
||||
);
|
||||
}
|
||||
await this.#downloadUpdate();
|
||||
} catch (e) {
|
||||
this.#onException(e);
|
||||
} finally {
|
||||
this.#updateBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This only has an effect if the status is `DOWNLOAD_AND_INSTALL`.This
|
||||
* indicates that the user has configured Firefox not to download updates
|
||||
* without permission, and we are waiting the user's permission.
|
||||
* This function should be called if and only if the user's permission was
|
||||
* given as it will allow the update download to proceed.
|
||||
*/
|
||||
allowUpdateDownload() {
|
||||
if (this.#permissionToDownloadGivenFn) {
|
||||
this.#permissionToDownloadGivenFn();
|
||||
}
|
||||
}
|
||||
|
||||
// true if updating is disabled because we're running in an app package.
|
||||
// This is distinct from aus.disabled because we need to avoid
|
||||
// messages being shown to the user about an "administrator" handling
|
||||
// updates; packaged apps may be getting updated by an administrator or they
|
||||
// may not be, and we don't have a good way to tell the difference from here,
|
||||
// so we err to the side of less confusion for unmanaged users.
|
||||
get #updateDisabledByPackage() {
|
||||
return Services.sysinfo.getProperty("isPackagedApp");
|
||||
}
|
||||
|
||||
// true when updating in background is enabled.
|
||||
get #updateStagingEnabled() {
|
||||
LOG(
|
||||
"AppUpdater:#updateStagingEnabled" +
|
||||
"canStageUpdates: " +
|
||||
this.aus.canStageUpdates
|
||||
);
|
||||
return (
|
||||
!this.aus.disabled &&
|
||||
!this.#updateDisabledByPackage &&
|
||||
this.aus.canStageUpdates
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads an update mar or connects to an in-progress download.
|
||||
* Doesn't resolve until the update is ready to install, or a failure state
|
||||
* is reached.
|
||||
*/
|
||||
async #downloadUpdate() {
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOADING);
|
||||
|
||||
let success = await this.aus.downloadUpdate(this.#update, false);
|
||||
if (!success) {
|
||||
LOG("AppUpdater:#downloadUpdate - downloadUpdate failed.");
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.#awaitDownloadComplete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Listens for a download to complete.
|
||||
* Doesn't resolve until the update is ready to install, or a failure state
|
||||
* is reached.
|
||||
*/
|
||||
async #awaitDownloadComplete() {
|
||||
let updateState = this.aus.currentState;
|
||||
if (
|
||||
updateState != Ci.nsIApplicationUpdateService.STATE_DOWNLOADING &&
|
||||
updateState != Ci.nsIApplicationUpdateService.STATE_SWAP
|
||||
) {
|
||||
throw new Error(
|
||||
"AppUpdater:#awaitDownloadComplete invoked in unexpected state: " +
|
||||
this.aus.getStateName(updateState)
|
||||
);
|
||||
}
|
||||
|
||||
// We may already be in the `DOWNLOADING` state, depending on how we entered
|
||||
// this function. But we actually want to alert the listeners again even if
|
||||
// we are because `this.update.selectedPatch` is null early in the
|
||||
// downloading state, but it should be set by now and listeners may want to
|
||||
// update UI based on that.
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOADING);
|
||||
|
||||
const updateDownloadProgress = (progress, progressMax) => {
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOADING, progress, progressMax);
|
||||
};
|
||||
|
||||
const progressObserver = {
|
||||
onStartRequest(aRequest) {
|
||||
LOG(
|
||||
`AppUpdater:#awaitDownloadComplete.observer.onStartRequest - ` +
|
||||
`aRequest: ${aRequest}`
|
||||
);
|
||||
},
|
||||
|
||||
onStatus(aRequest, aStatus, aStatusArg) {
|
||||
LOG(
|
||||
`AppUpdater:#awaitDownloadComplete.observer.onStatus ` +
|
||||
`- aRequest: ${aRequest}, aStatus: ${aStatus}, ` +
|
||||
`aStatusArg: ${aStatusArg}`
|
||||
);
|
||||
},
|
||||
|
||||
onProgress(aRequest, aProgress, aProgressMax) {
|
||||
LOG(
|
||||
`AppUpdater:#awaitDownloadComplete.observer.onProgress ` +
|
||||
`- aRequest: ${aRequest}, aProgress: ${aProgress}, ` +
|
||||
`aProgressMax: ${aProgressMax}`
|
||||
);
|
||||
updateDownloadProgress(aProgress, aProgressMax);
|
||||
},
|
||||
|
||||
onStopRequest(aRequest, aStatusCode) {
|
||||
LOG(
|
||||
`AppUpdater:#awaitDownloadComplete.observer.onStopRequest ` +
|
||||
`- aRequest: ${aRequest}, aStatusCode: ${aStatusCode}`
|
||||
);
|
||||
},
|
||||
|
||||
QueryInterface: ChromeUtils.generateQI([
|
||||
"nsIProgressEventSink",
|
||||
"nsIRequestObserver",
|
||||
]),
|
||||
};
|
||||
|
||||
let listenForProgress =
|
||||
updateState == Ci.nsIApplicationUpdateService.STATE_DOWNLOADING;
|
||||
|
||||
if (listenForProgress) {
|
||||
this.aus.addDownloadListener(progressObserver);
|
||||
LOG("AppUpdater:#awaitDownloadComplete - Registered download listener");
|
||||
}
|
||||
|
||||
LOG("AppUpdater:#awaitDownloadComplete - Waiting for state transition.");
|
||||
try {
|
||||
await makeAbortable(this.aus.stateTransition);
|
||||
} finally {
|
||||
if (listenForProgress) {
|
||||
this.aus.removeDownloadListener(progressObserver);
|
||||
LOG("AppUpdater:#awaitDownloadComplete - Download listener removed");
|
||||
}
|
||||
}
|
||||
|
||||
updateState = this.aus.currentState;
|
||||
LOG(
|
||||
"AppUpdater:#awaitDownloadComplete - State transition seen. New state: " +
|
||||
this.aus.getStateName(updateState)
|
||||
);
|
||||
|
||||
switch (updateState) {
|
||||
case Ci.nsIApplicationUpdateService.STATE_IDLE:
|
||||
LOG(
|
||||
"AppUpdater:#awaitDownloadComplete - Setting status DOWNLOAD_FAILED."
|
||||
);
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
||||
break;
|
||||
case Ci.nsIApplicationUpdateService.STATE_STAGING:
|
||||
LOG("AppUpdater:#awaitDownloadComplete - awaiting staging completion.");
|
||||
await this.#awaitStagingComplete();
|
||||
break;
|
||||
case Ci.nsIApplicationUpdateService.STATE_PENDING:
|
||||
LOG("AppUpdater:#awaitDownloadComplete - ready to restart.");
|
||||
this.#onReadyToRestart();
|
||||
break;
|
||||
default:
|
||||
LOG(
|
||||
"AppUpdater:#awaitDownloadComplete - Setting status INTERNAL_ERROR."
|
||||
);
|
||||
this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This function registers an observer that watches for the staging process
|
||||
* to complete. Once it does, it sets the status to either request that the
|
||||
* user restarts to install the update on success, request that the user
|
||||
* manually download and install the newer version, or automatically download
|
||||
* a complete update if applicable.
|
||||
* Doesn't resolve until the update is ready to install, or a failure state
|
||||
* is reached.
|
||||
*/
|
||||
async #awaitStagingComplete() {
|
||||
let updateState = this.aus.currentState;
|
||||
if (updateState != Ci.nsIApplicationUpdateService.STATE_STAGING) {
|
||||
throw new Error(
|
||||
"AppUpdater:#awaitStagingComplete invoked in unexpected state: " +
|
||||
this.aus.getStateName(updateState)
|
||||
);
|
||||
}
|
||||
|
||||
LOG("AppUpdater:#awaitStagingComplete - Setting status STAGING.");
|
||||
this.#setStatus(AppUpdater.STATUS.STAGING);
|
||||
|
||||
LOG("AppUpdater:#awaitStagingComplete - Waiting for state transition.");
|
||||
await makeAbortable(this.aus.stateTransition);
|
||||
|
||||
updateState = this.aus.currentState;
|
||||
LOG(
|
||||
"AppUpdater:#awaitStagingComplete - State transition seen. New state: " +
|
||||
this.aus.getStateName(updateState)
|
||||
);
|
||||
|
||||
switch (updateState) {
|
||||
case Ci.nsIApplicationUpdateService.STATE_PENDING:
|
||||
LOG("AppUpdater:#awaitStagingComplete - ready for restart");
|
||||
this.#onReadyToRestart();
|
||||
break;
|
||||
case Ci.nsIApplicationUpdateService.STATE_IDLE:
|
||||
LOG("AppUpdater:#awaitStagingComplete - DOWNLOAD_FAILED");
|
||||
this.#setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
|
||||
break;
|
||||
case Ci.nsIApplicationUpdateService.STATE_DOWNLOADING:
|
||||
// We've fallen back to downloading the complete update because the
|
||||
// partial update failed to be staged. Return to the downloading stage.
|
||||
LOG(
|
||||
"AppUpdater:#awaitStagingComplete - Partial update must have " +
|
||||
"failed to stage. Downloading complete update."
|
||||
);
|
||||
await this.#awaitDownloadComplete();
|
||||
break;
|
||||
default:
|
||||
LOG(
|
||||
"AppUpdater:#awaitStagingComplete - Setting status INTERNAL_ERROR."
|
||||
);
|
||||
this.#setStatus(AppUpdater.STATUS.INTERNAL_ERROR);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#onReadyToRestart() {
|
||||
let updateState = this.aus.currentState;
|
||||
if (updateState != Ci.nsIApplicationUpdateService.STATE_PENDING) {
|
||||
throw new Error(
|
||||
"AppUpdater:#onReadyToRestart invoked in unexpected state: " +
|
||||
this.aus.getStateName(updateState)
|
||||
);
|
||||
}
|
||||
|
||||
LOG("AppUpdater:#onReadyToRestart - Setting status READY_FOR_RESTART.");
|
||||
if (this.#swapListenerConnected) {
|
||||
LOG(
|
||||
"AppUpdater:#onReadyToRestart - update-swap listener already attached"
|
||||
);
|
||||
} else {
|
||||
this.#swapListenerConnected = true;
|
||||
LOG("AppUpdater:#onReadyToRestart - Attaching update-swap listener");
|
||||
Services.obs.addObserver(this, "update-swap", /* ownsWeak */ true);
|
||||
}
|
||||
this.#setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the current check for updates and any ongoing download.
|
||||
*
|
||||
* If this is called before `AppUpdater.check()` is called or after it
|
||||
* resolves, this should have no effect. If this is called while `check()` is
|
||||
* still running, `AppUpdater` will return to the NEVER_CHECKED state. We
|
||||
* don't really want to leave it in any of the intermediary states after we
|
||||
* have disconnected all the listeners that would allow those states to ever
|
||||
* change.
|
||||
*/
|
||||
stop() {
|
||||
LOG("AppUpdater:stop called");
|
||||
if (this.#swapListenerConnected) {
|
||||
LOG("AppUpdater:stop - Removing update-swap listener");
|
||||
Services.obs.removeObserver(this, "update-swap");
|
||||
this.#swapListenerConnected = false;
|
||||
}
|
||||
abortAllPromises();
|
||||
}
|
||||
|
||||
/**
|
||||
* {AppUpdater.STATUS} The status of the current check or update.
|
||||
*
|
||||
* Note that until AppUpdater.check has been called, this will always be set
|
||||
* to NEVER_CHECKED.
|
||||
*/
|
||||
get status() {
|
||||
return this.#status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener function that will be called back on status changes as
|
||||
* different stages of updates occur. The function will be called without
|
||||
* arguments for most status changes; see the comments around the STATUS value
|
||||
* definitions below. This is safe to call multiple times with the same
|
||||
* function. It will be added only once.
|
||||
*
|
||||
* @param {function} listener
|
||||
* The listener function to add.
|
||||
*/
|
||||
addListener(listener) {
|
||||
this.#listeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener. This is safe to call multiple times with the same
|
||||
* function, or with a function that was never added.
|
||||
*
|
||||
* @param {function} listener
|
||||
* The listener function to remove.
|
||||
*/
|
||||
removeListener(listener) {
|
||||
this.#listeners.delete(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the updater's current status and calls listeners.
|
||||
*
|
||||
* @param {AppUpdater.STATUS} status
|
||||
* The new updater status.
|
||||
* @param {*} listenerArgs
|
||||
* Arguments to pass to listeners.
|
||||
*/
|
||||
#setStatus(status, ...listenerArgs) {
|
||||
this.#status = status;
|
||||
for (let listener of this.#listeners) {
|
||||
listener(status, ...listenerArgs);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
observe(subject, topic, status) {
|
||||
LOG(
|
||||
"AppUpdater:observe " +
|
||||
"- subject: " +
|
||||
subject +
|
||||
", topic: " +
|
||||
topic +
|
||||
", status: " +
|
||||
status
|
||||
);
|
||||
switch (topic) {
|
||||
case "update-swap":
|
||||
// This is asynchronous, but we don't really want to wait for it in this
|
||||
// observer.
|
||||
this.#handleUpdateSwap();
|
||||
break;
|
||||
case "nsPref:changed":
|
||||
if (
|
||||
status == PREF_APP_UPDATE_LOG ||
|
||||
status == PREF_APP_UPDATE_LOG_FILE
|
||||
) {
|
||||
lazy.gLogEnabled; // Assigning this before it is lazy-loaded is an error.
|
||||
lazy.gLogEnabled =
|
||||
Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false) ||
|
||||
Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG_FILE, false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async #handleUpdateSwap() {
|
||||
try {
|
||||
// This must not be in the try/catch that sets #updateBusy to `false` in
|
||||
// its finally block.
|
||||
// There really shouldn't be any way to enter this function when
|
||||
// `#updateBusy` is `true`. But let's just be safe because we don't want
|
||||
// to ever end up with two things running at once.
|
||||
if (this.#updateBusy) {
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
this.#onException(e);
|
||||
}
|
||||
|
||||
try {
|
||||
this.#updateBusy = true;
|
||||
|
||||
// During an update swap, the new update will initially be stored in
|
||||
// `downloadingUpdate`. Part way through, it will be moved into
|
||||
// `readyUpdate` and `downloadingUpdate` will be set to `null`.
|
||||
this.#update = this.um.downloadingUpdate;
|
||||
if (!this.#update) {
|
||||
this.#update = this.um.readyUpdate;
|
||||
}
|
||||
|
||||
await this.#awaitDownloadComplete();
|
||||
} catch (e) {
|
||||
this.#onException(e);
|
||||
} finally {
|
||||
this.#updateBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
AppUpdater.prototype,
|
||||
"aus",
|
||||
"@mozilla.org/updates/update-service;1",
|
||||
"nsIApplicationUpdateService"
|
||||
);
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
AppUpdater.prototype,
|
||||
"checker",
|
||||
"@mozilla.org/updates/update-checker;1",
|
||||
"nsIUpdateChecker"
|
||||
);
|
||||
XPCOMUtils.defineLazyServiceGetter(
|
||||
AppUpdater.prototype,
|
||||
"um",
|
||||
"@mozilla.org/updates/update-manager;1",
|
||||
"nsIUpdateManager"
|
||||
);
|
||||
|
||||
AppUpdater.STATUS = {
|
||||
// Updates are allowed and there's no downloaded or staged update, but the
|
||||
// AppUpdater hasn't checked for updates yet, so it doesn't know more than
|
||||
// that.
|
||||
NEVER_CHECKED: 0,
|
||||
|
||||
// The updater isn't available (AppConstants.MOZ_UPDATER is falsey).
|
||||
NO_UPDATER: 1,
|
||||
|
||||
// "appUpdate" is not allowed by policy.
|
||||
UPDATE_DISABLED_BY_POLICY: 2,
|
||||
|
||||
// Another app instance is handling updates.
|
||||
OTHER_INSTANCE_HANDLING_UPDATES: 3,
|
||||
|
||||
// There's an update, but it's not supported on this system.
|
||||
UNSUPPORTED_SYSTEM: 4,
|
||||
|
||||
// The user must apply updates manually.
|
||||
MANUAL_UPDATE: 5,
|
||||
|
||||
// The AppUpdater is checking for updates.
|
||||
CHECKING: 6,
|
||||
|
||||
// The AppUpdater checked for updates and none were found.
|
||||
NO_UPDATES_FOUND: 7,
|
||||
|
||||
// The AppUpdater is downloading an update. Listeners are notified of this
|
||||
// status as a download starts. They are also notified on download progress,
|
||||
// and in that case they are passed two arguments: the current download
|
||||
// progress and the total download size.
|
||||
DOWNLOADING: 8,
|
||||
|
||||
// The AppUpdater tried to download an update but it failed.
|
||||
DOWNLOAD_FAILED: 9,
|
||||
|
||||
// There's an update available, but the user wants us to ask them to download
|
||||
// and install it.
|
||||
DOWNLOAD_AND_INSTALL: 10,
|
||||
|
||||
// An update is staging.
|
||||
STAGING: 11,
|
||||
|
||||
// An update is downloaded and staged and will be applied on restart.
|
||||
READY_FOR_RESTART: 12,
|
||||
|
||||
// Essential components of the updater are failing and preventing us from
|
||||
// updating.
|
||||
INTERNAL_ERROR: 13,
|
||||
|
||||
/**
|
||||
* Is the given `status` a terminal state in the update state machine?
|
||||
*
|
||||
* A terminal state means that the `check()` method has completed.
|
||||
*
|
||||
* N.b.: `DOWNLOAD_AND_INSTALL` is not considered terminal because the normal
|
||||
* flow is that Firefox will show UI prompting the user to install, and when
|
||||
* the user interacts, the `check()` method will continue through the update
|
||||
* state machine.
|
||||
*
|
||||
* @returns {boolean} `true` if `status` is terminal.
|
||||
*/
|
||||
isTerminalStatus(status) {
|
||||
return ![
|
||||
AppUpdater.STATUS.CHECKING,
|
||||
AppUpdater.STATUS.DOWNLOAD_AND_INSTALL,
|
||||
AppUpdater.STATUS.DOWNLOADING,
|
||||
AppUpdater.STATUS.NEVER_CHECKED,
|
||||
AppUpdater.STATUS.STAGING,
|
||||
].includes(status);
|
||||
},
|
||||
|
||||
/**
|
||||
* Turn the given `status` into a string for debugging.
|
||||
*
|
||||
* @returns {?string} representation of given numerical `status`.
|
||||
*/
|
||||
debugStringFor(status) {
|
||||
for (let [k, v] of Object.entries(AppUpdater.STATUS)) {
|
||||
if (v == status) {
|
||||
return k;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs a string to the error console. If enabled, also logs to the update
|
||||
* messages file.
|
||||
* @param string
|
||||
* The string to write to the error console.
|
||||
*/
|
||||
function LOG(string) {
|
||||
if (lazy.gLogEnabled) {
|
||||
dump("*** AUS:AUM " + string + "\n");
|
||||
if (!Cu.isInAutomation) {
|
||||
Services.console.logStringMessage("AUS:AUM " + string);
|
||||
}
|
||||
|
||||
if (lazy.gLogfileEnabled) {
|
||||
if (!gLogfileOutputStream) {
|
||||
let logfile = Services.dirsvc.get(KEY_PROFILE_DIR, Ci.nsIFile);
|
||||
logfile.append(FILE_UPDATE_MESSAGES);
|
||||
gLogfileOutputStream = FileUtils.openAtomicFileOutputStream(logfile);
|
||||
}
|
||||
|
||||
try {
|
||||
let encoded = new TextEncoder().encode(string + "\n");
|
||||
gLogfileOutputStream.write(encoded, encoded.length);
|
||||
gLogfileOutputStream.flush();
|
||||
} catch (e) {
|
||||
dump("*** AUS:AUM Unable to write to messages file: " + e + "\n");
|
||||
Services.console.logStringMessage(
|
||||
"AUS:AUM Unable to write to messages file: " + e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
EXTRA_JS_MODULES += [
|
||||
"AppUpdater.jsm",
|
||||
"AttachmentChecker.jsm",
|
||||
"BrowserWindowTracker.jsm",
|
||||
"ConversationOpener.jsm",
|
||||
|
|
Загрузка…
Ссылка в новой задаче