forked from mirrors/gecko-dev
		
	 b204bf50aa
			
		
	
	
		b204bf50aa
		
	
	
	
	
		
			
			Differential Revision: https://phabricator.services.mozilla.com/D68235 --HG-- extra : moz-landing-system : lando
		
			
				
	
	
		
			508 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			508 lines
		
	
	
	
		
			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, 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 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,
 | |
| };
 |