Bug 1546627 - Prevent Firefox from prematurely showing "Restart to Update" on startup r=rstrong
authorKirk Steuber <ksteuber@mozilla.com>
Fri, 10 May 2019 19:06:34 +0000
changeset 532269 8096d130b1346ba0c6bc10450de6f86196f3f768
parent 532268 c6dbde54689d42c29e2ff47fee4972bbd2433840
child 532270 a5fe44cee7723c38287dd5e4f5d04d335260bcb5
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersrstrong
bugs1546627
milestone68.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1546627 - Prevent Firefox from prematurely showing "Restart to Update" on startup r=rstrong Differential Revision: https://phabricator.services.mozilla.com/D30520
browser/base/content/aboutDialog-appUpdater.js
--- a/browser/base/content/aboutDialog-appUpdater.js
+++ b/browser/base/content/aboutDialog-appUpdater.js
@@ -45,17 +45,17 @@ function appUpdater(options = {}) {
   this.updateDeck = document.getElementById("updateDeck");
   this.promiseAutoUpdateSetting = null;
 
   // Hide the update deck when the update window is already open and it's not
   // already applied, to avoid syncing issues between them. Applied updates
   // don't have any information to sync between the windows as they both just
   // show the "Restart to continue"-type button.
   if (Services.wm.getMostRecentWindow("Update:Wizard") &&
-      !this.isApplied) {
+      !this.isReadyForRestart) {
     this.updateDeck.hidden = true;
     return;
   }
 
   this.bundle = Services.strings.
                 createBundle("chrome://browser/locale/browser.properties");
 
   let manualURL = Services.urlFormatter.formatURLPref("app.update.url.manual");
@@ -64,88 +64,126 @@ function appUpdater(options = {}) {
   manualLink.href = manualURL;
   document.getElementById("failedLink").href = manualURL;
 
   if (this.updateDisabledByPolicy) {
     this.selectPanel("policyDisabled");
     return;
   }
 
-  if (this.isPending || this.isApplied) {
+  if (this.isReadyForRestart) {
     this.selectPanel("apply");
     return;
   }
 
   if (this.aus.isOtherInstanceHandlingUpdates) {
     this.selectPanel("otherInstanceHandlingUpdates");
     return;
   }
 
   if (this.isDownloading) {
     this.startDownload();
     // selectPanel("downloading") is called from setupDownloadingUI().
     return;
   }
 
+  if (this.isStaging) {
+    this.waitForUpdateToStage();
+    // selectPanel("applying"); is called from 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();
 }
 
 appUpdater.prototype =
 {
   // true when there is an update check in progress.
   isChecking: false,
 
-  // true when there is an update already staged / ready to be applied.
+  // 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 installed in the background.
+  // 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 backgroundUpdateEnabled() {
+  get updateStagingEnabled() {
     return !this.updateDisabledByPolicy &&
-           gAppUpdater.aus.canStageUpdates;
+           this.aus.canStageUpdates;
   },
 
   /**
    * Sets the panel of the updateDeck.
    *
    * @param  aChildID
    *         The id of the deck's child to select, e.g. "apply".
    */
@@ -195,17 +233,17 @@ appUpdater.prototype =
     // after checking, onCheckComplete() is called
   },
 
   /**
    * Handles oncommand for the "Restart to Update" button
    * which is presented after the download has been downloaded.
    */
   buttonRestartAfterDownload() {
-    if (!this.isPending && !this.isApplied) {
+    if (!this.isReadyForRestart) {
       return;
     }
 
     gAppUpdater.selectPanel("restarting");
 
     // Notify all windows that an application quit has been requested.
     let cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
                      createInstance(Ci.nsISupportsPRBool);
@@ -284,16 +322,28 @@ appUpdater.prototype =
 
     /**
      * See nsISupports.idl
      */
     QueryInterface: ChromeUtils.generateQI(["nsIUpdateCheckListener"]),
   },
 
   /**
+   * Shows the applying UI until the update has finished staging
+   */
+  waitForUpdateToStage() {
+    if (!this.update)
+      this.update = this.um.activeUpdate;
+    this.update.QueryInterface(Ci.nsIWritablePropertyBag);
+    this.update.setProperty("foregroundDownload", "true");
+    this.selectPanel("applying");
+    this.updateUIWhenStagingComplete();
+  },
+
+  /**
    * 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");
 
@@ -346,42 +396,19 @@ appUpdater.prototype =
       // 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.removeDownloadListener();
-      if (this.backgroundUpdateEnabled) {
+      if (this.updateStagingEnabled) {
         this.selectPanel("applying");
-        let self = this;
-        Services.obs.addObserver(function 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.
-            self.selectPanel("apply");
-          } else if (status == "failed") {
-            // Background update has failed, let's show the UI responsible for
-            // prompting the user to update manually.
-            self.selectPanel("downloadFailed");
-          } else if (status == "downloading") {
-            // We've fallen back to downloading the full update because the
-            // partial update failed to get staged in the background.
-            // Therefore we need to keep our observer.
-            self.setupDownloadingUI();
-            return;
-          }
-          Services.obs.removeObserver(observer, "update-staged");
-        }, "update-staged");
+        this.updateUIWhenStagingComplete();
       } else {
         this.selectPanel("apply");
       }
       break;
     default:
       this.removeDownloadListener();
       this.selectPanel("downloadFailed");
       break;
@@ -398,12 +425,46 @@ appUpdater.prototype =
    * See nsIProgressEventSink.idl
    */
   onProgress(aRequest, aContext, aProgress, aProgressMax) {
     this.downloadStatus.textContent =
       DownloadUtils.getTransferTotal(aProgress, aProgressMax);
   },
 
   /**
+   * This function registers an observer that watches for the staging process
+   * to complete. Once it does, it updates the UI 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.
+   */
+  updateUIWhenStagingComplete() {
+    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.selectPanel("apply");
+      } else if (status == "failed") {
+        // Background update has failed, let's show the UI responsible for
+        // prompting the user to update manually.
+        this.selectPanel("downloadFailed");
+      } 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.setupDownloadingUI();
+        return;
+      }
+      Services.obs.removeObserver(observer, "update-staged");
+    };
+    Services.obs.addObserver(observer, "update-staged");
+  },
+
+  /**
    * See nsISupports.idl
    */
   QueryInterface: ChromeUtils.generateQI(["nsIProgressEventSink", "nsIRequestObserver"]),
 };