gecko-dev/browser/modules/AppUpdater.jsm

509 строки
16 KiB
JavaScript

/* 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.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
AppConstants: "resource://gre/modules/AppConstants.jsm",
Services: "resource://gre/modules/Services.jsm",
UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
});
const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
/**
* 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 {
constructor() {
this._listeners = new Set();
XPCOMUtils.defineLazyServiceGetter(
this,
"aus",
"@mozilla.org/updates/update-service;1",
"nsIApplicationUpdateService"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"checker",
"@mozilla.org/updates/update-checker;1",
"nsIUpdateChecker"
);
XPCOMUtils.defineLazyServiceGetter(
this,
"um",
"@mozilla.org/updates/update-manager;1",
"nsIUpdateManager"
);
}
/**
* 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.
*/
check() {
if (!AppConstants.MOZ_UPDATER) {
this._setStatus(AppUpdater.STATUS.NO_UPDATER);
return;
}
if (this.updateDisabledByPolicy) {
this._setStatus(AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY);
return;
}
if (this.isReadyForRestart) {
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
return;
}
if (this.aus.isOtherInstanceHandlingUpdates) {
this._setStatus(AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES);
return;
}
if (this.isDownloading) {
this.startDownload();
return;
}
if (this.isStaging) {
this._waitForUpdateToStage();
return;
}
// We might need this value later, so start loading it from the disk now.
this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
// That leaves the options
// "Check for updates, but let me choose whether to install them", and
// "Automatically install updates".
// In both cases, we check for updates without asking.
// In the "let me choose" case, we ask before downloading though, in onCheckComplete.
this.checkForUpdates();
}
// true when there is an update ready to be applied on restart or staged.
get isPending() {
if (this.update) {
return (
this.update.state == "pending" ||
this.update.state == "pending-service" ||
this.update.state == "pending-elevate"
);
}
return (
this.um.activeUpdate &&
(this.um.activeUpdate.state == "pending" ||
this.um.activeUpdate.state == "pending-service" ||
this.um.activeUpdate.state == "pending-elevate")
);
}
// true when there is an update already staged.
get isApplied() {
if (this.update) {
return (
this.update.state == "applied" || this.update.state == "applied-service"
);
}
return (
this.um.activeUpdate &&
(this.um.activeUpdate.state == "applied" ||
this.um.activeUpdate.state == "applied-service")
);
}
get isStaging() {
if (!this.updateStagingEnabled) {
return false;
}
let errorCode;
if (this.update) {
errorCode = this.update.errorCode;
} else if (this.um.activeUpdate) {
errorCode = this.um.activeUpdate.errorCode;
}
// If the state is pending and the error code is not 0, staging must have
// failed.
return this.isPending && errorCode == 0;
}
// true when an update ready to restart to finish the update process.
get isReadyForRestart() {
if (this.updateStagingEnabled) {
let errorCode;
if (this.update) {
errorCode = this.update.errorCode;
} else if (this.um.activeUpdate) {
errorCode = this.um.activeUpdate.errorCode;
}
// If the state is pending and the error code is not 0, staging must have
// failed and Firefox should be restarted to try to apply the update
// without staging.
return this.isApplied || (this.isPending && errorCode != 0);
}
return this.isPending;
}
// true when there is an update download in progress.
get isDownloading() {
if (this.update) {
return this.update.state == "downloading";
}
return this.um.activeUpdate && this.um.activeUpdate.state == "downloading";
}
// true when updating has been disabled by enterprise policy
get updateDisabledByPolicy() {
return Services.policies && !Services.policies.isAllowed("appUpdate");
}
// true when updating in background is enabled.
get updateStagingEnabled() {
return !this.updateDisabledByPolicy && this.aus.canStageUpdates;
}
/**
* Check for updates
*/
checkForUpdates() {
// 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);
this.checker.checkForUpdates(this._updateCheckListener, true);
// after checking, onCheckComplete() is called
}
/**
* Implements nsIUpdateCheckListener. The methods implemented by
* nsIUpdateCheckListener are in a different scope from nsIIncrementalDownload
* to make it clear which are used by each interface.
*/
get _updateCheckListener() {
if (!this.__updateCheckListener) {
this.__updateCheckListener = {
/**
* See nsIUpdateService.idl
*/
onCheckComplete: (aRequest, aUpdates) => {
this.update = this.aus.selectUpdate(aUpdates);
if (!this.update) {
this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
return;
}
if (this.update.unsupported) {
this._setStatus(AppUpdater.STATUS.UNSUPPORTED_SYSTEM);
return;
}
if (!this.aus.canApplyUpdates) {
this._setStatus(AppUpdater.STATUS.MANUAL_UPDATE);
return;
}
if (!this.promiseAutoUpdateSetting) {
this.promiseAutoUpdateSetting = UpdateUtils.getAppUpdateAutoEnabled();
}
this.promiseAutoUpdateSetting.then(updateAuto => {
if (updateAuto) {
// automatically download and install
this.startDownload();
} else {
// ask
this._setStatus(AppUpdater.STATUS.DOWNLOAD_AND_INSTALL);
}
});
},
/**
* See nsIUpdateService.idl
*/
onError: (aRequest, aUpdate) => {
// 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.
this._setStatus(AppUpdater.STATUS.NO_UPDATES_FOUND);
},
/**
* See nsISupports.idl
*/
QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
};
}
return this.__updateCheckListener;
}
/**
* Sets the status to STAGING. The status will then be set again when the
* update finishes staging.
*/
_waitForUpdateToStage() {
if (!this.update) {
this.update = this.um.activeUpdate;
}
this.update.QueryInterface(Ci.nsIWritablePropertyBag);
this.update.setProperty("foregroundDownload", "true");
this._setStatus(AppUpdater.STATUS.STAGING);
this._awaitStagingComplete();
}
/**
* Starts the download of an update mar.
*/
startDownload() {
if (!this.update) {
this.update = this.um.activeUpdate;
}
this.update.QueryInterface(Ci.nsIWritablePropertyBag);
this.update.setProperty("foregroundDownload", "true");
let state = this.aus.downloadUpdate(this.update, false);
if (state == "failed") {
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
return;
}
this._setupDownloadListener();
}
/**
* Starts tracking the download.
*/
_setupDownloadListener() {
this._setStatus(AppUpdater.STATUS.DOWNLOADING);
this.aus.addDownloadListener(this);
}
/**
* See nsIRequestObserver.idl
*/
onStartRequest(aRequest) {}
/**
* See nsIRequestObserver.idl
*/
onStopRequest(aRequest, aStatusCode) {
switch (aStatusCode) {
case Cr.NS_ERROR_UNEXPECTED:
if (
this.update.selectedPatch.state == "download-failed" &&
(this.update.isCompleteUpdate || this.update.patchCount != 2)
) {
// Verification error of complete patch, informational text is held in
// the update object.
this.aus.removeDownloadListener(this);
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
break;
}
// Verification failed for a partial patch, complete patch is now
// downloading so return early and do NOT remove the download listener!
break;
case Cr.NS_BINDING_ABORTED:
// Do not remove UI listener since the user may resume downloading again.
break;
case Cr.NS_OK:
this.aus.removeDownloadListener(this);
if (this.updateStagingEnabled) {
this._setStatus(AppUpdater.STATUS.STAGING);
this._awaitStagingComplete();
} else {
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
}
break;
default:
this.aus.removeDownloadListener(this);
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
break;
}
}
/**
* See nsIProgressEventSink.idl
*/
onStatus(aRequest, aContext, aStatus, aStatusArg) {}
/**
* See nsIProgressEventSink.idl
*/
onProgress(aRequest, aContext, aProgress, aProgressMax) {
this._setStatus(AppUpdater.STATUS.DOWNLOADING, aProgress, aProgressMax);
}
/**
* 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.
*/
_awaitStagingComplete() {
let observer = (aSubject, aTopic, aData) => {
// Update the UI when the background updater is finished
let status = aData;
if (
status == "applied" ||
status == "applied-service" ||
status == "pending" ||
status == "pending-service" ||
status == "pending-elevate"
) {
// If the update is successfully applied, or if the updater has
// fallen back to non-staged updates, show the "Restart to Update"
// button.
this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART);
} else if (status == "failed") {
// Background update has failed, let's show the UI responsible for
// prompting the user to update manually.
this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED);
} else if (status == "downloading") {
// We've fallen back to downloading the complete update because the
// partial update failed to get staged in the background.
// Therefore we need to keep our observer.
this._setupDownloadListener();
return;
}
Services.obs.removeObserver(observer, "update-staged");
};
Services.obs.addObserver(observer, "update-staged");
}
/**
* Stops the current check for updates and any ongoing download.
*/
stop() {
this.checker.stopCurrentCheck();
this.aus.removeDownloadListener(this);
}
/**
* {AppUpdater.STATUS} The status of the current check or update.
*/
get status() {
if (!this._status) {
if (!AppConstants.MOZ_UPDATER) {
this._status = AppUpdater.STATUS.NO_UPDATER;
} else if (this.updateDisabledByPolicy) {
this._status = AppUpdater.STATUS.UPDATE_DISABLED_BY_POLICY;
} else if (this.isReadyForRestart) {
this._status = AppUpdater.STATUS.READY_FOR_RESTART;
} else if (this.aus.isOtherInstanceHandlingUpdates) {
this._status = AppUpdater.STATUS.OTHER_INSTANCE_HANDLING_UPDATES;
} else if (this.isDownloading) {
this._status = AppUpdater.STATUS.DOWNLOADING;
} else if (this.isStaging) {
this._status = AppUpdater.STATUS.STAGING;
} else {
this._status = AppUpdater.STATUS.NEVER_CHECKED;
}
}
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;
}
}
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,
};