Bug 1458308 - Move app.update.auto to be stored in the update directory draft
authorKirk Steuber <ksteuber@mozilla.com>
Mon, 09 Jul 2018 11:27:03 -0700
changeset 829191 bbd4561e14d30b21f81fc286e78f951dd1e06755
parent 828129 8def30df2e40f81246eaaa8277a0225f35538256
child 829192 dde7f7c1dd4d09f3c306e0d2220af37a22b621e1
push id118747
push userbmo:ksteuber@mozilla.com
push dateTue, 14 Aug 2018 21:08:44 +0000
bugs1458308
milestone63.0a1
Bug 1458308 - Move app.update.auto to be stored in the update directory This patch additionally includes support for automatic migration of the pref from its old location to its new location. This patch does not fix telemetry reporting of app.update.auto - that will be addressed in another patch in the same series. MozReview-Commit-ID: KjX1mmGVB8M
browser/app/profile/firefox.js
browser/base/content/aboutDialog-appUpdater.js
toolkit/mozapps/update/nsIUpdateService.idl
toolkit/mozapps/update/nsUpdateService.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -129,20 +129,16 @@ pref("app.update.link.updateManualWhatsN
 // How many times we should let downloads fail before prompting the user to
 // download a fresh installer.
 pref("app.update.download.promptMaxAttempts", 2);
 
 // How many times we should let an elevation prompt fail before prompting the user to
 // download a fresh installer.
 pref("app.update.elevation.promptMaxAttempts", 2);
 
-// If set to true, the Update Service will automatically download updates if the
-// user can apply updates.
-pref("app.update.auto", true);
-
 // If set to true, the Update Service will present no UI for any event.
 pref("app.update.silent", false);
 
 // app.update.badgeWaitTime is in branding section
 
 // If set to true, the Update Service will apply updates in the background
 // when it finishes downloading them. Disabled in bug 1397562.
 pref("app.update.staging.enabled", false);
--- a/browser/base/content/aboutDialog-appUpdater.js
+++ b/browser/base/content/aboutDialog-appUpdater.js
@@ -35,16 +35,18 @@ function appUpdater(options = {}) {
                                      "@mozilla.org/updates/update-checker;1",
                                      "nsIUpdateChecker");
   XPCOMUtils.defineLazyServiceGetter(this, "um",
                                      "@mozilla.org/updates/update-manager;1",
                                      "nsIUpdateManager");
 
   this.options = options;
   this.updateDeck = document.getElementById("updateDeck");
+  this.promiseAutoUpdatePref = null;
+  this.autoUpdateCachedVal = 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.updateDeck.hidden = true;
@@ -76,16 +78,19 @@ function appUpdater(options = {}) {
   }
 
   if (this.isDownloading) {
     this.startDownload();
     // selectPanel("downloading") is called from setupDownloadingUI().
     return;
   }
 
+  // We might need this pref value later, so start loading it from the disk now.
+  this.promiseAutoUpdatePref = this.aus.autoUpdateIsEnabled();
+
   // 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();
 }
 
@@ -131,22 +136,29 @@ appUpdater.prototype =
   },
 
   // true when updating in background is enabled.
   get backgroundUpdateEnabled() {
     return !this.updateDisabledByPolicy &&
            gAppUpdater.aus.canStageUpdates;
   },
 
-  // true when updating is automatic.
-  get updateAuto() {
-    try {
-      return Services.prefs.getBoolPref("app.update.auto");
-    } catch (e) { }
-    return true; // Firefox default is true
+  // Returns a promise that resolves to true when updating is automatic.
+  // Since the pref value needs to be read from a file, we do some caching here.
+  updateAuto() {
+    if (this.autoUpdateCachedVal != null) {
+      return Promise.resolve(this.autoUpdateCachedVal);
+    }
+    if (this.promiseAutoUpdatePref == null) {
+      this.promiseAutoUpdatePref = this.aus.autoUpdateIsEnabled();
+    }
+    return this.promiseAutoUpdatePref.then(enabled => {
+      this.autoUpdateCachedVal = enabled;
+      return enabled;
+    });
   },
 
   /**
    * Sets the panel of the updateDeck.
    *
    * @param  aChildID
    *         The id of the deck's child to select, e.g. "apply".
    */
@@ -255,20 +267,23 @@ appUpdater.prototype =
         return;
       }
 
       if (!gAppUpdater.aus.canApplyUpdates) {
         gAppUpdater.selectPanel("manualUpdate");
         return;
       }
 
-      if (gAppUpdater.updateAuto) // automatically download and install
-        gAppUpdater.startDownload();
-      else // ask
-        gAppUpdater.selectPanel("downloadAndInstall");
+      gAppUpdater.updateAuto().then(updateAuto => {
+        if (updateAuto) { // automatically download and install
+          gAppUpdater.startDownload();
+        } else { // ask
+          gAppUpdater.selectPanel("downloadAndInstall");
+        }
+      });
     },
 
     /**
      * 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
--- a/toolkit/mozapps/update/nsIUpdateService.idl
+++ b/toolkit/mozapps/update/nsIUpdateService.idl
@@ -358,16 +358,44 @@ interface nsIApplicationUpdateService : 
   /**
    * Whether or not the Update Service can check for updates. This is a function
    * of whether or not application update is disabled by the application and the
    * platform the application is running on.
    */
   readonly attribute boolean canCheckForUpdates;
 
   /**
+   * Determines whether or not the Update Service automatically downloads and
+   * installs updates. This corresponds to whether or not the user has selected
+   * "Automatically install updates" in about:preferences.
+   *
+   * Note: This preference is stored in a file on the disk, so this operation
+   *       involves reading that file.
+   *
+   * @return A Promise that resolves with a boolean.
+   */
+  jsval autoUpdateIsEnabled();
+
+  /**
+   * Sets the preference for whether or not the Update Service automatically
+   * downloads and installs updates. This effectively selects between the
+   * "Automatically install updates" and "Check for updates but let you choose
+   * to install them" options in about:preferences.
+   *
+   * Note: This preferences is stored in a file on disk, so this operation
+   *       involves writing to that file.
+   *
+   * @return A Promise that resolves when the file has been written. If the file
+   *         was written successfully, the Promise resolves with the boolean
+   *         value writtent. If the file was not written successfully, the
+   *         Promise will reject with an I/O error.
+   */
+  jsval setAutoUpdateEnabled(in boolean enabled);
+
+  /**
    * Whether or not the installation requires elevation. Currently only
    * implemented on OSX, returns false on other platforms.
    */
   readonly attribute boolean elevationRequired;
 
   /**
    * Whether or not the Update Service can download and install updates.
    * On Windows, this is a function of whether or not the maintenance service
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -7,23 +7,24 @@
 "use strict";
 
 ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm", this);
 ChromeUtils.import("resource://gre/modules/FileUtils.jsm", this);
 ChromeUtils.import("resource://gre/modules/Services.jsm", this);
 ChromeUtils.import("resource://gre/modules/ctypes.jsm", this);
 ChromeUtils.import("resource://gre/modules/UpdateTelemetry.jsm", this);
 ChromeUtils.import("resource://gre/modules/AppConstants.jsm", this);
+ChromeUtils.import("resource://gre/modules/osfile.jsm", this);
 XPCOMUtils.defineLazyGlobalGetters(this, ["DOMParser", "XMLHttpRequest"]);
 
 const UPDATESERVICE_CID = Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}");
 const UPDATESERVICE_CONTRACTID = "@mozilla.org/updates/update-service;1";
 
 const PREF_APP_UPDATE_ALTWINDOWTYPE        = "app.update.altwindowtype";
-const PREF_APP_UPDATE_AUTO                 = "app.update.auto";
+const UNMIGRATED_PREF_APP_UPDATE_AUTO      = "app.update.auto";
 const PREF_APP_UPDATE_BACKGROUNDINTERVAL   = "app.update.download.backgroundInterval";
 const PREF_APP_UPDATE_BACKGROUNDERRORS     = "app.update.backgroundErrors";
 const PREF_APP_UPDATE_BACKGROUNDMAXERRORS  = "app.update.backgroundMaxErrors";
 const PREF_APP_UPDATE_CANCELATIONS         = "app.update.cancelations";
 const PREF_APP_UPDATE_CANCELATIONS_OSX     = "app.update.cancelations.osx";
 const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
 const PREF_APP_UPDATE_DOORHANGER           = "app.update.doorhanger";
 const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS    = "app.update.download.attempts";
@@ -42,16 +43,24 @@ const PREF_APP_UPDATE_SERVICE_ENABLED   
 const PREF_APP_UPDATE_SERVICE_ERRORS       = "app.update.service.errors";
 const PREF_APP_UPDATE_SERVICE_MAXERRORS    = "app.update.service.maxErrors";
 const PREF_APP_UPDATE_SILENT               = "app.update.silent";
 const PREF_APP_UPDATE_SOCKET_MAXERRORS     = "app.update.socket.maxErrors";
 const PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT  = "app.update.socket.retryTimeout";
 const PREF_APP_UPDATE_STAGING_ENABLED      = "app.update.staging.enabled";
 const PREF_APP_UPDATE_URL                  = "app.update.url";
 const PREF_APP_UPDATE_URL_DETAILS          = "app.update.url.details";
+// Note that although this pref has the same format as those above, it is very
+// different. It is not stored as part of the user's prefs. Instead it is stored
+// in the file indicated by FILE_UPDATE_PREFS, which will be located in the
+// update directory. This allows it to be accessible from all Firefox profiles
+// and from the Background Update Agent.
+const PREF_APP_UPDATE_AUTO                 = "app.update.auto";
+// The default value for the above pref
+const DEFAULT_VALUE_APP_UPDATE_AUTO        = true;
 
 const URI_BRAND_PROPERTIES      = "chrome://branding/locale/brand.properties";
 const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul";
 const URI_UPDATE_NS             = "http://www.mozilla.org/2005/app-update";
 const URI_UPDATE_PROMPT_DIALOG  = "chrome://mozapps/content/update/updates.xul";
 const URI_UPDATES_PROPERTIES    = "chrome://mozapps/locale/update/updates.properties";
 
 const KEY_UPDROOT         = "UpdRootD";
@@ -63,16 +72,17 @@ const FILE_ACTIVE_UPDATE_XML = "active-u
 const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
 const FILE_LAST_UPDATE_LOG   = "last-update.log";
 const FILE_UPDATES_XML       = "updates.xml";
 const FILE_UPDATE_LOG        = "update.log";
 const FILE_UPDATE_MAR        = "update.mar";
 const FILE_UPDATE_STATUS     = "update.status";
 const FILE_UPDATE_TEST       = "update.test";
 const FILE_UPDATE_VERSION    = "update.version";
+const FILE_UPDATE_PREFS      = "prefs.json";
 
 const STATE_NONE            = "null";
 const STATE_DOWNLOADING     = "downloading";
 const STATE_PENDING         = "pending";
 const STATE_PENDING_SERVICE = "pending-service";
 const STATE_PENDING_ELEVATE = "pending-elevate";
 const STATE_APPLYING        = "applying";
 const STATE_APPLIED         = "applied";
@@ -207,16 +217,18 @@ ChromeUtils.defineModuleGetter(this, "De
 XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
   return Services.prefs.getBoolPref(PREF_APP_UPDATE_LOG, false);
 });
 
 XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() {
   return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
 });
 
+XPCOMUtils.defineLazyGetter(this, "gTextDecoder", () => new TextDecoder());
+
 /**
  * Tests to make sure that we can write to a given directory.
  *
  * @param updateTestFile a test file in the directory that needs to be tested.
  * @param createDirectory whether a test directory should be created.
  * @throws if we don't have right access to the directory.
  */
 function testWriteAccess(updateTestFile, createDirectory) {
@@ -2341,16 +2353,105 @@ UpdateService.prototype = {
           "unable to acquire update mutex");
       return false;
     }
 
     LOG("UpdateService.canCheckForUpdates - able to check for updates");
     return true;
   },
 
+  _updateAutoPrefCachedVal: null,
+  // Used for serializing reads and writes of the app.update.auto pref file.
+  // This is especially important for making sure writes don't happen out of
+  // order.
+  _updateAutoIOPromise: Promise.resolve(),
+
+  _readUpdateAutoPref: async function AUS__readUpdateAuto() {
+    let prefFile = getUpdateFile([FILE_UPDATE_PREFS]);
+    let binaryData = await OS.File.read(prefFile.path);
+    let jsonData = gTextDecoder.decode(binaryData);
+    let prefData = JSON.parse(jsonData);
+    return !!prefData[PREF_APP_UPDATE_AUTO];
+  },
+
+  _writeUpdateAutoPref: async function AUS__writeUpdateAutoPref(enabledValue) {
+    enabledValue = !!enabledValue;
+    let prefFile = getUpdateFile([FILE_UPDATE_PREFS]);
+    let prefObject = {[PREF_APP_UPDATE_AUTO]: enabledValue};
+
+    await OS.File.writeAtomic(prefFile.path, JSON.stringify(prefObject));
+    return enabledValue;
+  },
+
+  // If the pref's value changed, notify observers. Returns the new pref value
+  // passed in.d
+  _maybeUpdateAutoPrefChanged: function AUS__maybeUpdateAutoPrefChanged(newValue) {
+    if (newValue != this._updateAutoPrefCachedVal) {
+      this._updateAutoPrefCachedVal = newValue;
+      Services.obs.notifyObservers(null, "auto-update-pref-change",
+                                   newValue.toString());
+    }
+    return newValue;
+  },
+
+  /**
+   * See nsIUpdateService.idl
+   */
+  autoUpdateIsEnabled: function AUS_autoUpdateIsEnabled() {
+    // Justification for the empty catch statement below:
+    // There is only one situation where this promise chain should ever reject,
+    // which is when writing fails and the error is logged and re-thrown. All
+    // other possible exceptions are wrapped in try blocks.
+    let readPromise = this._updateAutoIOPromise.catch(() => {}).then(async () => {
+      try {
+        return await this._readUpdateAutoPref();
+      } catch (error) {
+        LOG("UpdateService.autoUpdateIsEnabled - Unable to read pref file. " +
+            "Going to attempt pref migration. Exception: " + error);
+        let prefValue;
+        try {
+          prefValue = Services.prefs.getBoolPref(
+            UNMIGRATED_PREF_APP_UPDATE_AUTO,
+            DEFAULT_VALUE_APP_UPDATE_AUTO);
+
+          return await this._writeUpdateAutoPref(prefValue);
+        } catch (error) {
+          LOG("UpdateService.autoUpdateIsEnabled - Migration failed. " +
+              "Exception: " + error);
+        }
+        return prefValue;
+      }
+    }).then(this._maybeUpdateAutoPrefChanged.bind(this));
+    this._updateAutoIOPromise = readPromise;
+    return readPromise;
+  },
+
+  /**
+   * See nsIUpdateService.idl
+   */
+  setAutoUpdateEnabled: function AUS_setAutoUpdateEnabled(enabledValue) {
+    // Justification for the empty catch statement below:
+    // There is only one situation where this promise chain should ever reject,
+    // which is when writing fails and the error is logged and re-thrown. All
+    // other possible exceptions are wrapped in try blocks.
+    let writePromise = this._updateAutoIOPromise.catch(() => {}).then(async () => {
+      try {
+        return await this._writeUpdateAutoPref(enabledValue);
+      } catch (error) {
+        LOG("UpdateService.setAutoUpdateEnabled - Pref write failed. " +
+            "Exception: " + error);
+        // After logging, rethrow the error so the caller knows that their pref
+        // set failed.
+        throw error;
+      }
+    }).then(this._maybeUpdateAutoPrefChanged.bind(this));
+    this._updateAutoIOPromise = writePromise;
+    return writePromise;
+  },
+
   /**
    * See nsIUpdateService.idl
    */
   get elevationRequired() {
     return getElevationRequired();
   },
 
   /**