bug 1361102 - update the application update xml files asynchronously for writes and deletions. r=dothayer
authorRobert Strong <robert.bugzilla@gmail.com>
Fri, 08 Sep 2017 10:38:31 -0700
changeset 429346 a18f510ef12e97729f0fb315071e44afd41132ee
parent 429345 8e00076e0122afc990a7fe1bba6006f4692ac92c
child 429347 814b441b2bad6ac477225918bf1c7f6034c3ed82
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdothayer
bugs1361102
milestone57.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 1361102 - update the application update xml files asynchronously for writes and deletions. r=dothayer
toolkit/mozapps/update/nsUpdateService.js
--- a/toolkit/mozapps/update/nsUpdateService.js
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -186,23 +186,30 @@ const APPID_TO_TOPIC = {
   // SeaMonkey
   "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored",
   // Thunderbird
   "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done",
   // Instantbird
   "{33cb9019-c295-46dd-be21-8c4936574bee}": "xul-window-visible",
 };
 
+// A var is used for the delay so tests can set a smaller value.
+var gSaveUpdateXMLDelay = 2000;
 var gUpdateMutexHandle = null;
 
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
-
 XPCOMUtils.defineLazyModuleGetter(this, "WindowsRegistry",
                                   "resource://gre/modules/WindowsRegistry.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
+                                  "resource://gre/modules/AsyncShutdown.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
+                                  "resource://gre/modules/DeferredTask.jsm");
 
 XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
   return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
 });
 
 XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() {
   return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
 });
@@ -1568,17 +1575,20 @@ const UpdateServiceFactory = {
 
 /**
  * UpdateService
  * A Service for managing the discovery and installation of software updates.
  * @constructor
  */
 function UpdateService() {
   LOG("Creating UpdateService");
-  Services.obs.addObserver(this, "xpcom-shutdown");
+  // The observor notification to shut down the service must be before
+  // profile-before-change since nsIUpdateManager uses profile-before-change
+  // to shutdown and write the update xml files.
+  Services.obs.addObserver(this, "quit-application");
   Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this);
 }
 
 UpdateService.prototype = {
   /**
    * The downloader we are using to download updates. There is only ever one of
    * these.
    */
@@ -1647,17 +1657,17 @@ UpdateService.prototype = {
       case "network:offline-status-changed":
         this._offlineStatusChanged(data);
         break;
       case "nsPref:changed":
         if (data == PREF_APP_UPDATE_LOG) {
           gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
         }
         break;
-      case "xpcom-shutdown":
+      case "quit-application":
         Services.obs.removeObserver(this, topic);
         Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this);
 
         if (AppConstants.platform == "win" && gUpdateMutexHandle) {
           // If we hold the update mutex, let it go!
           // The OS would clean this up sometime after shutdown,
           // but that would have no guarantee on timing.
           closeHandle(gUpdateMutexHandle);
@@ -2506,20 +2516,26 @@ UpdateService.prototype = {
                                          Ci.nsIObserver])
 };
 
 /**
  * A service to manage active and past updates.
  * @constructor
  */
 function UpdateManager() {
-  // Ensure the active update file is loaded and that the active update is set
-  // in the update manager if there is an existing active update.
+  if (Services.appinfo.ID == "xpcshell@tests.mozilla.org") {
+    // Use a smaller value for xpcshell tests so they don't have to wait as
+    // long for the files to be saved.
+    gSaveUpdateXMLDelay = 0;
+  }
+
+  // Load the active-update.xml file to see if there is an active update.
   let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML);
   if (activeUpdates.length > 0) {
+    // Set the active update directly on the var used to cache the value.
     this._activeUpdate = activeUpdates[0];
     // This check is performed here since UpdateService:_postUpdateProcessing
     // won't be called when there isn't an update.status file.
     if (readStatusFile(getUpdatesDir()) == STATE_NONE) {
       // Under some edgecases such as Windows system restore the
       // active-update.xml will contain a pending update without the status
       // file. To recover from this situation clean the updates dir and move
       // the active update to the update history.
@@ -2549,21 +2565,31 @@ UpdateManager.prototype = {
   _updatesDirty: false,
 
   /**
    * See nsIObserver.idl
    */
   observe: function UM_observe(subject, topic, data) {
     // Hack to be able to run and cleanup tests by reloading the update data.
     if (topic == "um-reload-update-data") {
+      if (this._updatesXMLSaver) {
+        this._updatesXMLSaver.disarm();
+        AsyncShutdown.profileBeforeChange.removeBlocker(this._updatesXMLSaverCallback);
+        this._updatesXMLSaver = null;
+        this._updatesXMLSaverCallback = null;
+      }
+      // Set the write delay to 0 for mochitest-chrome and mochitest-browser
+      // tests.
+      gSaveUpdateXMLDelay = 0;
       this._activeUpdate = null;
       let activeUpdates = this._loadXMLFileIntoArray(FILE_ACTIVE_UPDATE_XML);
       if (activeUpdates.length > 0) {
         this._activeUpdate = activeUpdates[0];
       }
+      this._updatesDirty = true;
       delete this._updates;
       let updates = this._loadXMLFileIntoArray(FILE_UPDATES_XML);
       Object.defineProperty(this, "_updates", {
         value: updates,
         writable: true,
         configurable: true,
         enumerable: true
       });
@@ -2612,16 +2638,27 @@ UpdateManager.prototype = {
         }
         updates.push(update);
       }
     } catch (ex) {
       LOG("UpdateManager:_loadXMLFileIntoArray - error constructing update " +
           "list. Exception: " + ex);
     }
     fileStream.close();
+    if (updates.length == 0) {
+      LOG("UpdateManager:_loadXMLFileIntoArray - update xml file " + fileName +
+          " exists but doesn't contain any updates");
+      // The file exists but doesn't contain any updates so remove it.
+      try {
+        file.remove(false);
+      } catch (e) {
+        LOG("UpdateManager:_loadXMLFileIntoArray - error removing " + fileName +
+            " file. Exception: " + e);
+      }
+    }
     return updates;
   },
 
   /**
    * Loads the update history from the updates.xml file and then replaces
    * _updates with an array of all previously downloaded and installed updates
    * so the file is only read once.
    */
@@ -2672,71 +2709,86 @@ UpdateManager.prototype = {
   /**
    * Serializes an array of updates to an XML file or removes the file if the
    * array length is 0.
    * @param   updates
    *          An array of nsIUpdate objects
    * @param   fileName
    *          The file name in the updates directory to write to.
    */
-  _writeUpdatesToXMLFile: function UM__writeUpdatesToXMLFile(updates, fileName) {
+  _writeUpdatesToXMLFile: async function UM__writeUpdatesToXMLFile(updates, fileName) {
     let file = getUpdateFile([fileName]);
     if (updates.length == 0) {
       LOG("UpdateManager:_writeUpdatesToXMLFile - no updates to write. " +
           "removing file: " + file.path);
-      try {
-        file.remove(false);
-      } catch (e) {
-      }
+      await OS.File.remove(file.path, {ignoreAbsent: true});
       return;
     }
 
-    var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
-              createInstance(Ci.nsIFileOutputStream);
-    var modeFlags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
-                    FileUtils.MODE_TRUNCATE;
-    if (!file.exists()) {
-      file.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
-    }
-    fos.init(file, modeFlags, FileUtils.PERMS_FILE, 0);
-
+    const EMPTY_UPDATES_DOCUMENT_OPEN = "<?xml version=\"1.0\"?><updates xmlns=\"http://www.mozilla.org/2005/app-update\">";
+    const EMPTY_UPDATES_DOCUMENT_CLOSE = "</updates>";
     try {
       var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
                    createInstance(Ci.nsIDOMParser);
-      const EMPTY_UPDATES_DOCUMENT = "<?xml version=\"1.0\"?><updates xmlns=\"http://www.mozilla.org/2005/app-update\"></updates>";
-      var doc = parser.parseFromString(EMPTY_UPDATES_DOCUMENT, "text/xml");
+      var doc = parser.parseFromString(EMPTY_UPDATES_DOCUMENT_OPEN + EMPTY_UPDATES_DOCUMENT_CLOSE, "text/xml");
 
       for (var i = 0; i < updates.length; ++i) {
         doc.documentElement.appendChild(updates[i].serialize(doc));
       }
 
-      var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
-                       createInstance(Ci.nsIDOMSerializer);
-      serializer.serializeToStream(doc.documentElement, fos, null);
+      var xml = EMPTY_UPDATES_DOCUMENT_OPEN + doc.documentElement.innerHTML +
+                EMPTY_UPDATES_DOCUMENT_CLOSE;
+      await OS.File.writeAtomic(file.path, xml, {encoding: "utf-8",
+                                                 tmpPath: file.path + ".tmp"});
+      await OS.File.setPermissions(file.path, {unixMode: FileUtils.PERMS_FILE});
     } catch (e) {
+      LOG("UpdateManager:_writeUpdatesToXMLFile - Exception: " + e);
     }
-
-    FileUtils.closeSafeFileOutputStream(fos);
   },
 
+  _updatesXMLSaver: null,
+  _updatesXMLSaverCallback: null,
   /**
    * See nsIUpdateService.idl
    */
   saveUpdates: function UM_saveUpdates() {
+    if (!this._updatesXMLSaver) {
+      this._updatesXMLSaverCallback = () => this._updatesXMLSaver.finalize();
+
+      this._updatesXMLSaver = new DeferredTask(() => this._saveUpdatesXML(),
+                                               gSaveUpdateXMLDelay);
+      AsyncShutdown.profileBeforeChange.addBlocker(
+        "UpdateManager: writing update xml data", this._updatesXMLSaverCallback);
+    } else {
+      this._updatesXMLSaver.disarm();
+    }
+
+    this._updatesXMLSaver.arm();
+  },
+
+  /**
+   * Saves the active-updates.xml and updates.xml when the updates history has
+   * been modified files.
+   */
+  _saveUpdatesXML: function UM__saveUpdatesXML() {
+    AsyncShutdown.profileBeforeChange.removeBlocker(this._updatesXMLSaverCallback);
+    this._updatesXMLSaver = null;
+    this._updatesXMLSaverCallback = null;
+
     // The active update stored in the active-update.xml file will change during
-    // the lifetime of an active update and should always be updated when
-    // saveUpdates is called.
+    // the lifetime of an active update and the file should always be updated
+    // when saveUpdates is called.
     this._writeUpdatesToXMLFile(this._activeUpdate ? [this._activeUpdate] : [],
                                 FILE_ACTIVE_UPDATE_XML);
     // The update history stored in the updates.xml file should only need to be
     // updated when an active update has been added to it in which case
     // |_updatesDirty| will be true.
     if (this._updatesDirty) {
       this._updatesDirty = false;
-      this._writeUpdatesToXMLFile(this._updates.slice(0, 10), FILE_UPDATES_XML);
+      this._writeUpdatesToXMLFile(this._updates, FILE_UPDATES_XML);
     }
   },
 
   /**
    * See nsIUpdateService.idl
    */
   refreshUpdateStatus: function UM_refreshUpdateStatus() {
     var update = this._activeUpdate;