/* 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" ); this.QueryInterface = ChromeUtils.generateQI([ "nsIObserver", "nsIProgressEventSink", "nsIRequestObserver", "nsISupportsWeakReference", ]); Services.obs.addObserver(this, "update-swap", /* ownsWeak */ 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. */ 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.readyUpdate && (this.um.readyUpdate.state == "pending" || this.um.readyUpdate.state == "pending-service" || this.um.readyUpdate.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.readyUpdate && (this.um.readyUpdate.state == "applied" || this.um.readyUpdate.state == "applied-service") ); } get isStaging() { if (!this.updateStagingEnabled) { return false; } let errorCode; if (this.update) { errorCode = this.update.errorCode; } else if (this.um.readyUpdate) { errorCode = this.um.readyUpdate.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.readyUpdate) { errorCode = this.um.readyUpdate.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.downloadingUpdate && this.um.downloadingUpdate.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.readyUpdate; } 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.downloadingUpdate; } this.update.QueryInterface(Ci.nsIWritablePropertyBag); this.update.setProperty("foregroundDownload", "true"); let success = this.aus.downloadUpdate(this.update, false); if (!success) { 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) { // It could be that another instance was started during the download, // and if that happened, then we actually should not advance to the // STAGING status because the staging process isn't really happening // until that instance exits (or we time out waiting). if (this.aus.isOtherInstanceHandlingUpdates) { this._setStatus(AppUpdater.OTHER_INSTANCE_HANDLING_UPDATES); } else { this._setStatus(AppUpdater.STATUS.STAGING); } // But we should register the staging observer in either case, because // if we do time out waiting for the other instance to exit, then // staging really will start at that point. this._awaitStagingComplete(); } else { this._awaitDownloadComplete(); } break; default: this.aus.removeDownloadListener(this); this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED); break; } } /** * See nsIProgressEventSink.idl */ onStatus(aRequest, aStatus, aStatusArg) {} /** * See nsIProgressEventSink.idl */ onProgress(aRequest, aProgress, aProgressMax) { this._setStatus(AppUpdater.STATUS.DOWNLOADING, aProgress, aProgressMax); } /** * This function registers an observer that watches for the download * to complete. Once it does, it updates the status accordingly. */ _awaitDownloadComplete() { let observer = (aSubject, aTopic, aData) => { // Update the UI when the download is finished this._setStatus(AppUpdater.STATUS.READY_FOR_RESTART); Services.obs.removeObserver(observer, "update-downloaded"); }; Services.obs.addObserver(observer, "update-downloaded"); } /** * 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 switch (aTopic) { case "update-staged": 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; } break; case "update-error": this._setStatus(AppUpdater.STATUS.DOWNLOAD_FAILED); break; } Services.obs.removeObserver(observer, "update-staged"); Services.obs.removeObserver(observer, "update-error"); }; Services.obs.addObserver(observer, "update-staged"); Services.obs.addObserver(observer, "update-error"); } /** * 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; } observe(subject, topic, status) { switch (topic) { case "update-swap": this._handleUpdateSwap(); break; } } _handleUpdateSwap() { // This function exists to deal with the fact that we support handling 2 // updates at once: a ready update and a downloading update. But AppUpdater // only ever really considers a single update at a time. // We see an update swap just when the downloading update has finished // downloading and is being swapped into UpdateManager.readyUpdate. At this // point, we are in one of two states. Either: // a) The update that is being swapped in is the update that this // AppUpdater has already been tracking, or // b) We've been tracking the ready update. Now that the downloading // update is about to be swapped into the place of the ready update, we // need to switch over to tracking the new update. if ( this._status == AppUpdater.STATUS.DOWNLOADING || this._status == AppUpdater.STATUS.STAGING ) { // We are already tracking the correct update. return; } if (this.updateStagingEnabled) { this._setStatus(AppUpdater.STATUS.STAGING); this._awaitStagingComplete(); } else { this._setStatus(AppUpdater.STATUS.DOWNLOADING); this._awaitDownloadComplete(); } } } 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, };