Bug 1204156 - allow system add-ons to install and update without restart r?aswan draft
authorRobert Helmer <rhelmer@mozilla.com>
Thu, 25 Aug 2016 10:01:43 -0700
changeset 436982 bc9328a03cc4887acdb08b5aa81a6ee7ffe50e0b
parent 436738 336759fad4621dfcd0a3293840edbed67018accd
child 536505 4ecc55e49f21cf48c9a4295e3bc254edc6d8b231
push id35270
push userrhelmer@mozilla.com
push dateThu, 10 Nov 2016 04:24:04 +0000
reviewersaswan
bugs1204156
milestone52.0a1
Bug 1204156 - allow system add-ons to install and update without restart r?aswan MozReview-Commit-ID: 3RuYfR2wGIg
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_failed_update.xpi
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js
toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -301,17 +301,38 @@ var AddonTestUtils = {
       dirEntries.close();
 
       try {
         appDirForAddons.remove(true);
       } catch (ex) {
         testScope.do_print(`Got exception removing addon app dir: ${ex}`);
       }
 
-      var testDir = this.profileDir.clone();
+      // ensure no leftover files in the system addon upgrade location
+      let featuresDir = this.profileDir.clone();
+      featuresDir.append("features");
+      // upgrade directories will be in UUID folders under features/
+      let systemAddonDirs = [];
+      if (featuresDir.exists()) {
+        let featuresDirEntries = featuresDir.directoryEntries
+                                            .QueryInterface(Ci.nsIDirectoryEnumerator);
+        while (featuresDirEntries.hasMoreElements()) {
+          let entry = featuresDirEntries.getNext();
+          entry.QueryInterface(Components.interfaces.nsIFile);
+          systemAddonDirs.push(entry);
+        }
+
+        systemAddonDirs.map(dir => {
+          dir.append("stage");
+          pathShouldntExist(dir);
+        });
+      }
+
+      // ensure no leftover files in the user addon location
+      let testDir = this.profileDir.clone();
       testDir.append("extensions");
       testDir.append("trash");
       pathShouldntExist(testDir);
 
       testDir.leafName = "staged";
       pathShouldntExist(testDir);
 
       return this.promiseShutdownManager();
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3233,46 +3233,25 @@ this.XPIProvider = {
       }
 
       if (!systemAddonLocation.isValidAddon(item.addon))
         return false;
 
       return true;
     }
 
-    try {
-      if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) {
-        throw new Error("Rejecting updated system add-on set that either could not " +
-                        "be downloaded or contained unusable add-ons.");
-      }
-
-      // Install into the install location
-      logger.info("Installing new system add-on set");
-      yield systemAddonLocation.installAddonSet(Array.from(addonList.values())
-        .map(a => a.addon));
-
-      // Bug 1204156: Switch to the new system add-ons without requiring a restart
-    }
-    finally {
-      // Delete the temporary files
-      logger.info("Deleting temporary files");
-      for (let item of addonList.values()) {
-        // If this item downloaded delete the temporary file.
-        if (item.path) {
-          try {
-            yield OS.File.remove(item.path);
-          }
-          catch (e) {
-            logger.warn(`Failed to remove temporary file ${item.path}.`, e);
-          }
-        }
-      }
-
-      yield systemAddonLocation.cleanDirectories();
-    }
+    if (!Array.from(addonList.values()).every(item => item.path && item.addon && validateAddon(item))) {
+      throw new Error("Rejecting updated system add-on set that either could not " +
+                      "be downloaded or contained unusable add-ons.");
+    }
+
+    // Install into the install location
+    logger.info("Installing new system add-on set");
+    yield systemAddonLocation.installAddonSet(Array.from(addonList.values())
+      .map(a => a.addon));
   }),
 
   /**
    * Verifies that all installed add-ons are still correctly signed.
    */
   verifySignatures: function() {
     XPIDatabase.getAddonList(a => true, (addons) => {
       Task.spawn(function*() {
@@ -3366,18 +3345,19 @@ this.XPIProvider = {
    *         of passing through updated compatibility information
    * @return true if an add-on was installed or uninstalled
    */
   processPendingFileChanges: function(aManifests) {
     let changed = false;
     for (let location of this.installLocations) {
       aManifests[location.name] = {};
       // We can't install or uninstall anything in locked locations
-      if (location.locked)
+      if (location.locked) {
         continue;
+      }
 
       let stagingDir = location.getStagingDir();
 
       try {
         if (!stagingDir || !stagingDir.exists() || !stagingDir.isDirectory())
           continue;
       }
       catch (e) {
@@ -3507,17 +3487,17 @@ this.XPIProvider = {
 
             // Pass this through to addMetadata so it knows this add-on was
             // likely installed through the UI
             aManifests[location.name][id] = addon;
           }
           catch (e) {
             // If some data can't be recovered from the cached metadata then it
             // is unlikely to be a problem big enough to justify throwing away
-            // the install, just log and error and continue
+            // the install, just log an error and continue
             logger.error("Unable to read metadata from " + jsonfile.path, e);
           }
           finally {
             fis.close();
           }
         }
         seenFiles.push(jsonfile.leafName);
 
@@ -5779,17 +5759,19 @@ AddonInstall.prototype = {
       return;
 
     // Create new AddonInstall instances for every remaining file
     if (!this.linkedInstalls)
       this.linkedInstalls = [];
 
     for (let { entryName, file } of aFiles) {
       logger.debug("Creating linked install from " + entryName);
-      let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
+      let install = yield new Promise(
+        resolve => AddonInstall.createInstall(resolve, file)
+      );
 
       // Make the new install own its temporary file
       install.ownsTempFile = true;
 
       this.linkedInstalls.push(install);
 
       // If one of the internal XPIs was multipackage then move its linked
       // installs to the outer install
@@ -6313,59 +6295,23 @@ AddonInstall.prototype = {
                                                    this.wrapper)) {
         // If a listener changed our state then do not proceed with the install
         if (this.state != AddonManager.STATE_DOWNLOADED)
           return;
 
         // If an upgrade listener is registered for this add-on, pass control
         // over the upgrade to the add-on.
         if (AddonManagerPrivate.hasUpgradeListener(this.addon.id)) {
-          logger.info(`${this.addon.id} has an upgrade listener, postponing until restart`);
-          this.state = AddonManager.STATE_POSTPONED;
-
-          let stagingDir = this.installLocation.getStagingDir();
-          let stagedAddon = stagingDir.clone();
-
-          Task.spawn((function*() {
-            yield this.installLocation.requestStagingDir();
-
-            yield this.unstageInstall(stagedAddon);
-
-            stagedAddon.append(this.addon.id);
-            stagedAddon.leafName = this.addon.id + ".xpi";
-
-            yield this.stageInstall(true, stagedAddon, true);
-
-            AddonManagerPrivate.callInstallListeners("onInstallPostponed",
-                                                     this.listeners, this.wrapper)
-
-            // upgrade has been staged for restart, notify the add-on and give
-            // it a way to resume.
-            let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
-            callback({
-              version: this.version,
-              install: () => {
-                switch (this.state) {
-                  case AddonManager.STATE_INSTALLED:
-                    // this addon has already been installed, nothing to do
-                    logger.warn(`${this.addon.id} tried to resume postponed upgrade, but it's already installed`);
-                    break;
-                  case AddonManager.STATE_POSTPONED:
-                    logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
-                    this.state = AddonManager.STATE_DOWNLOADED;
-                    this.installLocation.releaseStagingDir();
-                    this.install();
-                    break;
-                  default:
-                    logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
-                    break;
-                }
-              },
-            });
-          }).bind(this));
+          logger.info(`add-on ${this.addon.id} has an upgrade listener, postponing upgrade until restart`);
+          let resumeFn = () => {
+            logger.info(`${this.addon.id} has resumed a previously postponed upgrade`);
+            this.state = AddonManager.STATE_DOWNLOADED;
+            this.install();
+          }
+          this.postpone(resumeFn);
         } else {
           // no upgrade listener present, so proceed with normal install
           this.install();
           if (this.linkedInstalls) {
             for (let install of this.linkedInstalls) {
               if (install.state == AddonManager.STATE_DOWNLOADED)
                 install.install();
             }
@@ -6407,18 +6353,17 @@ AddonInstall.prototype = {
                     this.existingAddon._installLocation == this.installLocation;
     let requiresRestart = XPIProvider.installRequiresRestart(this.addon);
 
     logger.debug("Starting install of " + this.addon.id + " from " + this.sourceURI.spec);
     AddonManagerPrivate.callAddonListeners("onInstalling",
                                            this.addon.wrapper,
                                            requiresRestart);
 
-    let stagingDir = this.installLocation.getStagingDir();
-    let stagedAddon = stagingDir.clone();
+    let stagedAddon = this.installLocation.getStagingDir();
 
     Task.spawn((function*() {
       let installedUnpacked = 0;
 
       yield this.installLocation.requestStagingDir();
 
       // remove any previously staged files
       yield this.unstageInstall(stagedAddon);
@@ -6530,17 +6475,18 @@ AddonInstall.prototype = {
             // listeners because important cleanup hasn't been done yet
             XPIProvider.unloadBootstrapScope(this.addon.id);
           }
         }
         XPIProvider.setTelemetry(this.addon.id, "unpacked", installedUnpacked);
         recordAddonTelemetry(this.addon);
       }
     }).bind(this)).then(null, (e) => {
-      logger.warn("Failed to install " + this.file.path + " from " + this.sourceURI.spec + " to " + stagedAddon.path, e);
+      logger.warn(`Failed to install ${this.file.path} from ${this.sourceURI.spec} to ${stagedAddon.path}`, e);
+
       if (stagedAddon.exists())
         recursiveRemove(stagedAddon);
       this.state = AddonManager.STATE_INSTALL_FAILED;
       this.error = AddonManager.ERROR_FILE_ACCESS;
       XPIProvider.removeActiveInstall(this);
       AddonManagerPrivate.callAddonListeners("onOperationCancelled",
                                              this.addon.wrapper);
       AddonManagerPrivate.callInstallListeners("onInstallFailed",
@@ -6566,19 +6512,19 @@ AddonInstall.prototype = {
       logger.debug("Addon " + this.addon.id + " will be installed as " +
           "an unpacked directory");
       stagedAddon.leafName = this.addon.id;
       yield OS.File.makeDir(stagedAddon.path);
       yield ZipUtils.extractFilesAsync(this.file, stagedAddon);
       installedUnpacked = 1;
     }
     else {
-      logger.debug("Addon " + this.addon.id + " will be installed as " +
-          "a packed xpi");
+      logger.debug(`Addon ${this.addon.id} will be installed as a packed xpi`);
       stagedAddon.leafName = this.addon.id + ".xpi";
+
       yield OS.File.copy(this.file.path, stagedAddon.path);
     }
 
     if (restartRequired) {
       // Point the add-on to its extracted files as the xpi may get deleted
       this.addon._sourceBundle = stagedAddon;
 
       // Cache the AddonInternal as it may have updated compatibility info
@@ -6628,50 +6574,104 @@ AddonInstall.prototype = {
 
       return prompt;
     }
     else if (iid.equals(Ci.nsIChannelEventSink)) {
       return this;
     }
 
     return this.badCertHandler.getInterface(iid);
+  },
+
+  /**
+    * Postone a pending update, until restart or until the add-on resumes.
+    *
+    * @param {Function} resumeFn - a function for the add-on to run
+    *                                    when resuming.
+    */
+  postpone: Task.async(function*(resumeFn) {
+    this.state = AddonManager.STATE_POSTPONED;
+
+    let stagingDir = this.installLocation.getStagingDir();
+    let stagedAddon = stagingDir.clone();
+
+    yield this.installLocation.requestStagingDir();
+    yield this.unstageInstall(stagedAddon);
+
+    stagedAddon.append(this.addon.id);
+    stagedAddon.leafName = this.addon.id + ".xpi";
+
+    yield this.stageInstall(true, stagedAddon, true);
+
+    AddonManagerPrivate.callInstallListeners("onInstallPostponed",
+                                             this.listeners, this.wrapper)
+
+    // upgrade has been staged for restart, provide a way for it to call the
+    // resume function.
+    let callback = AddonManagerPrivate.getUpgradeListener(this.addon.id);
+    if (callback) {
+      callback({
+        version: this.version,
+        install: () => {
+          switch (this.state) {
+            case AddonManager.STATE_POSTPONED:
+              if (resumeFn) {
+                resumeFn();
+              }
+              break;
+            default:
+              logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
+              break;
+          }
+        },
+      });
+    }
+    // Release the staging directory lock, but since the staging dir is populated
+    // it will not be removed until resumed or installed by restart.
+    // See also cleanStagingDir()
+    this.installLocation.releaseStagingDir();
   }
-}
+)}
 
 /**
  * Creates a new AddonInstall for an already staged install. Used when
  * installing the staged install failed for some reason.
- *
+ * @param  aInstallLocation
+ *         The install location holding the staged install.
  * @param  aDir
  *         The directory holding the staged install
  * @param  aManifest
  *         The cached manifest for the install
  */
 AddonInstall.createStagedInstall = function(aInstallLocation, aDir, aManifest) {
   let url = Services.io.newFileURI(aDir);
 
   let install = new AddonInstall(aInstallLocation, aDir);
+
   install.initStagedInstall(aManifest);
 };
 
 /**
- * Creates a new AddonInstall to install an add-on from a local file. Installs
- * always go into the profile install location.
+ * Creates a new AddonInstall to install an add-on from a local file.
  *
  * @param  aCallback
  *         The callback to pass the new AddonInstall to
  * @param  aFile
  *         The file to install
+ * @param  aLocation
+ *         The location to install to
  */
-AddonInstall.createInstall = function(aCallback, aFile) {
-  let location = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+AddonInstall.createInstall = function(aCallback, aFile, aLocation = undefined) {
+  if (!aLocation) {
+    aLocation = XPIProvider.installLocationsByName[KEY_APP_PROFILE];
+  }
   let url = Services.io.newFileURI(aFile);
 
   try {
-    let install = new AddonInstall(location, url);
+    let install = new AddonInstall(aLocation, url);
     install.initLocalInstall(aCallback);
   }
   catch (e) {
     logger.error("Error creating install", e);
     makeSafe(aCallback)(null);
   }
 };
 
@@ -7339,18 +7339,22 @@ AddonInternal.prototype = {
         permissions |= AddonManager.PERM_CAN_DISABLE;
       }
     }
 
     // Add-ons that are in locked install locations, or are pending uninstall
     // cannot be upgraded or uninstalled
     if (!this._installLocation.locked && !this.pendingUninstall) {
       // Experiments cannot be upgraded.
+      // System add-on upgrades are triggered through a different mechanism (see updateSystemAddons())
+      let isSystem = (this._installLocation.name == KEY_APP_SYSTEM_DEFAULTS ||
+                      this._installLocation.name == KEY_APP_SYSTEM_ADDONS);
       // Add-ons that are installed by a file link cannot be upgraded.
-      if (this.type != "experiment" && !this._installLocation.isLinkedAddon(this.id)) {
+      if (this.type != "experiment" &&
+          !this._installLocation.isLinkedAddon(this.id) && !isSystem) {
         permissions |= AddonManager.PERM_CAN_UPGRADE;
       }
 
       permissions |= AddonManager.PERM_CAN_UNINSTALL;
     }
 
     return permissions;
   },
@@ -8544,55 +8548,151 @@ Object.assign(MutableDirectoryInstallLoc
       }
     }
 
     delete this._IDToFileMap[aId];
   },
 });
 
 /**
- * An object which identifies a directory install location for system add-ons.
- * The location consists of a directory which contains the add-ons installed in
- * the location.
+ * An object which identifies a directory install location for system add-ons
+ * upgrades.
+ *
+ * The location consists of a directory which contains the add-ons installed.
  *
  * @param  aName
  *         The string identifier for the install location
  * @param  aDirectory
  *         The nsIFile directory for the install location
  * @param  aScope
  *         The scope of add-ons installed in this location
  * @param  aResetSet
  *         True to throw away the current add-on set
  */
 function SystemAddonInstallLocation(aName, aDirectory, aScope, aResetSet) {
   this._baseDir = aDirectory;
   this._nextDir = null;
 
+  this._stagingDirLock = 0;
+
   if (aResetSet)
     this.resetAddonSet();
 
   this._addonSet = this._loadAddonSet();
 
   this._directory = null;
   if (this._addonSet.directory) {
     this._directory = aDirectory.clone();
     this._directory.append(this._addonSet.directory);
     logger.info("SystemAddonInstallLocation scanning directory " + this._directory.path);
   }
   else {
     logger.info("SystemAddonInstallLocation directory is missing");
   }
 
   DirectoryInstallLocation.call(this, aName, this._directory, aScope);
-  this.locked = true;
+  this.locked = false;
 }
 
 SystemAddonInstallLocation.prototype = Object.create(DirectoryInstallLocation.prototype);
 Object.assign(SystemAddonInstallLocation.prototype, {
   /**
+   * Removes the specified files or directories in the staging directory and
+   * then if the staging directory is empty attempts to remove it.
+   *
+   * @param  aLeafNames
+   *         An array of file or directory to remove from the directory, the
+   *         array may be empty
+   */
+  cleanStagingDir: function(aLeafNames = []) {
+    let dir = this.getStagingDir();
+
+    for (let name of aLeafNames) {
+      let file = dir.clone();
+      file.append(name);
+      recursiveRemove(file);
+    }
+
+    if (this._stagingDirLock > 0)
+      return;
+
+    let dirEntries = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
+    try {
+      if (dirEntries.nextFile)
+        return;
+    }
+    finally {
+      dirEntries.close();
+    }
+
+    try {
+      setFilePermissions(dir, FileUtils.PERMS_DIRECTORY);
+      dir.remove(false);
+    }
+    catch (e) {
+      logger.warn("Failed to remove staging dir", e);
+      // Failing to remove the staging directory is ignorable
+    }
+  },
+
+  /**
+   * Gets the staging directory to put add-ons that are pending install and
+   * uninstall into.
+   *
+   * @return {nsIFile} - staging directory for system add-on upgrades.
+   */
+  getStagingDir: function() {
+    this._addonSet = this._loadAddonSet();
+    let dir = null;
+    if (this._addonSet.directory) {
+      this._directory = this._baseDir.clone();
+      this._directory.append(this._addonSet.directory);
+      dir = this._directory.clone();
+      dir.append(DIR_STAGE);
+    }
+    else {
+      logger.info("SystemAddonInstallLocation directory is missing");
+    }
+
+    return dir;
+  },
+
+  requestStagingDir: function() {
+    this._stagingDirLock++;
+    if (this._stagingDirPromise)
+      return this._stagingDirPromise;
+
+    this._addonSet = this._loadAddonSet();
+    if (this._addonSet.directory) {
+      this._directory = this._baseDir.clone();
+      this._directory.append(this._addonSet.directory);
+    }
+
+    OS.File.makeDir(this._directory.path);
+    let stagepath = OS.Path.join(this._directory.path, DIR_STAGE);
+    return this._stagingDirPromise = OS.File.makeDir(stagepath).then(null, (e) => {
+      if (e instanceof OS.File.Error && e.becauseExists)
+        return;
+      logger.error("Failed to create staging directory", e);
+      throw e;
+    });
+  },
+
+  releaseStagingDir: function() {
+    this._stagingDirLock--;
+
+    if (this._stagingDirLock == 0) {
+      this._stagingDirPromise = null;
+      this.cleanStagingDir();
+    }
+
+    return Promise.resolve();
+  },
+
+  /**
    * Reads the current set of system add-ons
    */
   _loadAddonSet: function() {
     try {
       let setStr = Preferences.get(PREF_SYSTEM_ADDON_SET, null);
       if (setStr) {
         let addonSet = JSON.parse(setStr);
         if ((typeof addonSet == "object") && addonSet.schema == 1)
@@ -8603,16 +8703,19 @@ Object.assign(SystemAddonInstallLocation
       logger.error("Malformed system add-on set, resetting.");
     }
 
     return { schema: 1, addons: {} };
   },
 
   /**
    * Saves the current set of system add-ons
+   *
+   * @param {Object} aAddonSet - object containing schema, directory and set
+   *                 of system add-on IDs and versions.
    */
   _saveAddonSet: function(aAddonSet) {
     Preferences.set(PREF_SYSTEM_ADDON_SET, JSON.stringify(aAddonSet));
   },
 
   getAddonLocations: function() {
     // Updated system add-ons are ignored in safe mode
     if (Services.appinfo.inSafeMode)
@@ -8682,17 +8785,33 @@ Object.assign(SystemAddonInstallLocation
 
     return true;
   },
 
   /**
    * Resets the add-on set so on the next startup the default set will be used.
    */
   resetAddonSet: function() {
-    this._saveAddonSet({ schema: 1, addons: {} });
+
+    if (this._addonSet) {
+      logger.info("Removing all system add-on upgrades.");
+
+      // remove everything from the pref first, if uninstall
+      // fails then at least they will not be re-activated on
+      // next restart.
+      this._saveAddonSet({ schema: 1, addons: {} });
+
+      for (let id of Object.keys(this._addonSet.addons)) {
+        AddonManager.getAddonByID(id, addon => {
+          if (addon) {
+            addon.uninstall();
+          }
+        });
+      }
+    }
   },
 
   /**
    * Removes any directories not currently in use or pending use after a
    * restart. Any errors that happen here don't really matter as we'll attempt
    * to cleanup again next time.
    */
   cleanDirectories: Task.async(function*() {
@@ -8745,77 +8864,225 @@ Object.assign(SystemAddonInstallLocation
     }
     finally {
       iterator.close();
     }
   }),
 
   /**
    * Installs a new set of system add-ons into the location and updates the
-   * add-on set in prefs. We wait to switch state until a restart.
+   * add-on set in prefs.
+   *
+   * @param {Array} aAddons - An array of addons to install.
    */
   installAddonSet: Task.async(function*(aAddons) {
     // Make sure the base dir exists
     yield OS.File.makeDir(this._baseDir.path, { ignoreExisting: true });
 
+    let addonSet = this._loadAddonSet();
+
+    // Remove any add-ons that are no longer part of the set.
+    for (let addonID of Object.keys(addonSet.addons)) {
+      if (!aAddons.includes(addonID)) {
+        AddonManager.getAddonByID(addonID, a => a.uninstall());
+      }
+    }
+
     let newDir = this._baseDir.clone();
 
     let uuidGen = Cc["@mozilla.org/uuid-generator;1"].
                   getService(Ci.nsIUUIDGenerator);
     newDir.append("blank");
 
     while (true) {
       newDir.leafName = uuidGen.generateUUID().toString();
 
       try {
         yield OS.File.makeDir(newDir.path, { ignoreExisting: false });
         break;
       }
       catch (e) {
-        // Directory already exists, pick another
-      }
-    }
-
-    let copyAddon = Task.async(function*(addon) {
-      let target = OS.Path.join(newDir.path, addon.id + ".xpi");
-      logger.info(`Copying ${addon.id} from ${addon._sourceBundle.path} to ${target}.`);
-      try {
-        yield OS.File.copy(addon._sourceBundle.path, target);
-      }
-      catch (e) {
-        logger.error(`Failed to copy ${addon.id} from ${addon._sourceBundle.path} to ${target}.`, e);
-        throw e;
-      }
-      addon._sourceBundle = new nsIFile(target);
+        logger.debug("Could not create new system add-on updates dir, retrying", e);
+      }
+    }
+
+    // Record the new upgrade directory.
+    let state = { schema: 1, directory: newDir.leafName, addons: {} };
+    this._saveAddonSet(state);
+
+    this._nextDir = newDir;
+    let location = this;
+
+    let installs = [];
+    for (let addon of aAddons) {
+      let install = yield new Promise(resolve => {
+        AddonInstall.createInstall(resolve, addon._sourceBundle, location);
+      });
+      installs.push(install);
+    }
+
+    let installAddon = Task.async(function*(install) {
+      // Make the new install own its temporary file.
+      install.ownsTempFile = true;
+      install.install();
     });
 
+    let postponeAddon = Task.async(function*(install) {
+      let resumeFn;
+      if (AddonManagerPrivate.hasUpgradeListener(install.addon.id)) {
+        logger.info(`system add-on ${install.addon.id} has an upgrade listener, postponing upgrade set until restart`);
+        resumeFn = () => {
+          logger.info(`${install.addon.id} has resumed a previously postponed addon set`);
+          install.installLocation.resumeAddonSet(installs);
+        }
+      }
+      yield install.postpone(resumeFn);
+    });
+
+    let previousState;
+
     try {
-      yield waitForAllPromises(aAddons.map(copyAddon));
+      // All add-ons in position, create the new state and store it in prefs
+      state = { schema: 1, directory: newDir.leafName, addons: {} };
+      for (let addon of aAddons) {
+        state.addons[addon.id] = {
+          version: addon.version
+        }
+      }
+
+      previousState = this._loadAddonSet();
+      this._saveAddonSet(state);
+
+      let blockers = aAddons.filter(
+        addon => AddonManagerPrivate.hasUpgradeListener(addon.id)
+      );
+
+      if (blockers.length > 0) {
+        yield waitForAllPromises(installs.map(postponeAddon));
+      } else {
+        yield waitForAllPromises(installs.map(installAddon));
+      }
     }
     catch (e) {
+      // Roll back to previous upgrade set (if present) on restart.
+      if (previousState) {
+        this._saveAddonSet(previousState);
+      }
+      // Otherwise, roll back to built-in set on restart.
+      // TODO try to do these restartlessly
+      this.resetAddonSet();
+
       try {
         yield OS.File.removeDir(newDir.path, { ignorePermissions: true });
       }
       catch (e) {
-        logger.warn(`Failed to remove new system add-on directory ${newDir.path}.`, e);
+        logger.warn(`Failed to remove failed system add-on directory ${newDir.path}.`, e);
       }
       throw e;
     }
-
-    // All add-ons in position, create the new state and store it in prefs
-    let state = { schema: 1, directory: newDir.leafName, addons: {} };
-    for (let addon of aAddons) {
-      state.addons[addon.id] = {
-        version: addon.version
-      }
-    }
-
-    this._saveAddonSet(state);
-    this._nextDir = newDir;
+  }),
+
+ /**
+  * Resumes upgrade of a previously-delayed add-on set.
+  */
+  resumeAddonSet: Task.async(function*(installs) {
+    let resumeAddon = Task.async(function*(install) {
+      install.state = AddonManager.STATE_DOWNLOADED;
+      install.installLocation.releaseStagingDir();
+      install.install();
+    });
+
+    let addonSet = this._loadAddonSet();
+    let addonIDs = Object.keys(addonSet.addons);
+
+    let blockers = installs.filter(
+      install => AddonManagerPrivate.hasUpgradeListener(install.addon.id)
+    );
+
+    if (blockers.length > 1) {
+      logger.warn("Attempted to resume system add-on install but upgrade blockers are still present");
+    } else {
+      yield waitForAllPromises(installs.map(resumeAddon));
+    }
   }),
+
+  /**
+   * Returns a directory that is normally on the same filesystem as the rest of
+   * the install location and can be used for temporarily storing files during
+   * safe move operations. Calling this method will delete the existing trash
+   * directory and its contents.
+   *
+   * @return an nsIFile
+   */
+  getTrashDir: function() {
+    let trashDir = this._directory.clone();
+    trashDir.append(DIR_TRASH);
+    let trashDirExists = trashDir.exists();
+    try {
+      if (trashDirExists)
+        recursiveRemove(trashDir);
+      trashDirExists = false;
+    } catch (e) {
+      logger.warn("Failed to remove trash directory", e);
+    }
+    if (!trashDirExists)
+      trashDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+    return trashDir;
+  },
+
+  /**
+   * Installs an add-on into the install location.
+   *
+   * @param  id
+   *         The ID of the add-on to install
+   * @param  source
+   *         The source nsIFile to install from
+   * @return an nsIFile indicating where the add-on was installed to
+   */
+  installAddon: function({id, source}) {
+    let trashDir = this.getTrashDir();
+    let transaction = new SafeInstallOperation();
+
+    // If any of these operations fails the finally block will clean up the
+    // temporary directory
+    try {
+      if (source.isFile()) {
+        flushJarCache(source);
+      }
+
+      transaction.moveUnder(source, this._directory);
+    }
+    finally {
+      // It isn't ideal if this cleanup fails but it isn't worth rolling back
+      // the install because of it.
+      try {
+        recursiveRemove(trashDir);
+      }
+      catch (e) {
+        logger.warn("Failed to remove trash directory when installing " + id, e);
+      }
+    }
+
+    let newFile = this._directory.clone();
+    newFile.append(source.leafName);
+
+    try {
+      newFile.lastModifiedTime = Date.now();
+    } catch (e)  {
+      logger.warn("failed to set lastModifiedTime on " + newFile.path, e);
+    }
+    this._IDToFileMap[id] = newFile;
+    XPIProvider._addURIMapping(id, newFile);
+
+    return newFile;
+  },
+
+  // old system add-on upgrade dirs get automatically removed
+  uninstallAddon: (aAddon) => {},
 });
 
 /**
  * An object which identifies an install location for temporary add-ons.
  */
 const TemporaryInstallLocation = {
   locked: false,
   name: KEY_APP_TEMPORARY,
new file mode 100644
index 0000000000000000000000000000000000000000..94d9e47d26d53493af0d28743c99b57da89645d4
GIT binary patch
literal 5090
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
F{{Ty?F|hyu
new file mode 100644
index 0000000000000000000000000000000000000000..28c8561c625eb42d84f71fa05af9b1e24ab66fe5
GIT binary patch
literal 4706
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
new file mode 100644
index 0000000000000000000000000000000000000000..daf55c0d4c63146e174adf5b02c71fba6b3444fb
GIT binary patch
literal 5095
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
Jc9egt{{fXDE^hz;
new file mode 100644
index 0000000000000000000000000000000000000000..75cacbbc87b5ea6e1d798a20e83d47d751ad97e6
GIT binary patch
literal 4701
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
bKM|lLf6J(rDh}@N0GzvHfA{SY$#3sp<riu-
new file mode 100644
index 0000000000000000000000000000000000000000..2eb6b7fc962df47b8dcc162cbab18f0d5550543f
GIT binary patch
literal 5117
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
new file mode 100644
index 0000000000000000000000000000000000000000..fb588b3e049bc6860dae3d8c791f734c0717a1b6
GIT binary patch
literal 4716
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
new file mode 100644
index 0000000000000000000000000000000000000000..7a5eb265d34734e31bb2d6434a57b2e32b91a26e
GIT binary patch
literal 5098
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
MU4B37LG)+zU!USFQUCw|
new file mode 100644
index 0000000000000000000000000000000000000000..dc674935509ce2bf957ecf8e1eca74c4698ed353
GIT binary patch
literal 4707
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
new file mode 100644
index 0000000000000000000000000000000000000000..3c673ac2e4fc4f8a0fa1f2688fa3be66b4d999c3
GIT binary patch
literal 735
zc$^FHW@Zs#U|`^2xRw>=x!-wvpf8Y@1jIZHG7L%i`6b0AMTrG^S;e6toD9s91q35@
z0&!^tHv=QfS4IW~u<p}mwDmmCoV<7;WW}?f0F9Fxr}V=MbhS?DUkW(Y;-NY%<=J!1
z6csV;C8}x9mMqiAj4HlbH1%1kQpvR<#+k0FOIZW_+%i4A>MG@0*ck%gPI?P;lIWk=
zOZ|ZMoMd8P;08J=Gq1QLF(*f_C?yT-qP;-Z?8S6Z>!h=OhYbW;-q&$Wo)^2Ek+1iX
zh|9_&jocnF+UAAP-v1`=Rn+wNe`UfM>iFc@&HS?Ru#?kzC;gagt=|*pvN=;mx431~
z_N!9cMc;~FWIV8J%C%D4wfW1By?vQuH~+H5LQkidoyz}KG@82n_H6m^lKa?{)!gfi
z;+?$2q*MxP+;>{-R1dV~dz^E$_F+qF8Sj~G%bHw6Ea%RPU3o!n#uOJr-LgB*udRGf
z?eKD)`Fk<@Ix}lE6@BycZ;Vmr{?D0xkxzcj^E;K&)>nRgXUOSg<nOyF6LXZ=*o%3o
z7Uv<W;DYz_dp7YL3Hx;Mle=swZyv|N>~xa{>9LQ(`ErgnBsv{qoTWK$fBS?AS*J(S
z^-^BQpUPO!xYM@XJ$tE!1W%=2|DEi^ywcG>nE!vzaZX!k|Lb+BqWeRk6yNpt%O7Zb
zslK@SDAP}GffCpBEq}v{zwiclGcw6B<4PwI3?Kjud4?s8AQoz>VTGg`wD>_b5i^>R
eO`HrQQ6ilc66qKQva*3RF#+LXAg#s(;sF4oZ~w3W
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1274,8 +1274,73 @@ function callback_soon(aFunction) {
 
 function writeProxyFileToDir(aDir, aAddon, aId) {
   awaitPromise(promiseWriteProxyFileToDir(aDir, aAddon, aId));
 
   let file = aDir.clone();
   file.append(aId);
   return file
 }
+
+function* serveSystemUpdate(xml, perform_update, testserver) {
+  testserver.registerPathHandler("/data/update.xml", (request, response) => {
+    response.write(xml);
+  });
+
+  try {
+    yield perform_update();
+  }
+  finally {
+    testserver.registerPathHandler("/data/update.xml", null);
+  }
+}
+
+// Runs an update check making it use the passed in xml string. Uses the direct
+// call to the update function so we get rejections on failure.
+function* installSystemAddons(xml, testserver) {
+  do_print("Triggering system add-on update check.");
+
+  yield serveSystemUpdate(xml, function*() {
+    let { XPIProvider } = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+    yield XPIProvider.updateSystemAddons();
+  }, testserver);
+}
+
+// Runs a full add-on update check which will in some cases do a system add-on
+// update check. Always succeeds.
+function* updateAllSystemAddons(xml, testserver) {
+  do_print("Triggering full add-on update check.");
+
+  yield serveSystemUpdate(xml, function() {
+    return new Promise(resolve => {
+      Services.obs.addObserver(function() {
+        Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+        resolve();
+      }, "addons-background-update-complete", false);
+
+      // Trigger the background update timer handler
+      gInternalManager.notify(null);
+    });
+  }, testserver);
+}
+
+// Builds an update.xml file for an update check based on the data passed.
+function* buildSystemAddonUpdates(addons, root) {
+  let xml = `<?xml version="1.0" encoding="UTF-8"?>\n\n<updates>\n`;
+  if (addons) {
+    xml += `  <addons>\n`;
+    for (let addon of addons) {
+      xml += `    <addon id="${addon.id}" URL="${root + addon.path}" version="${addon.version}"`;
+      if (addon.size)
+        xml += ` size="${addon.size}"`;
+      if (addon.hashFunction)
+        xml += ` hashFunction="${addon.hashFunction}"`;
+      if (addon.hashValue)
+        xml += ` hashValue="${addon.hashValue}"`;
+      xml += `/>\n`;
+    }
+    xml += `  </addons>\n`;
+  }
+  xml += `</updates>\n`;
+
+  return xml;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js
@@ -0,0 +1,461 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that delaying a system add-on update works.
+
+Components.utils.import("resource://testing-common/httpd.js");
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const tempdir = gTmpD.clone();
+
+const PREF_SYSTEM_ADDON_SET           = "extensions.systemAddonSet";
+const PREF_SYSTEM_ADDON_UPDATE_URL    = "extensions.systemAddon.update.url";
+
+const IGNORE_ID = "system_delay_ignore@tests.mozilla.org";
+const COMPLETE_ID = "system_delay_complete@tests.mozilla.org";
+const DEFER_ID = "system_delay_defer@tests.mozilla.org";
+const DEFER_ALSO_ID = "system_delay_defer_also@tests.mozilla.org";
+const NORMAL_ID = "system1@tests.mozilla.org";
+
+
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"], true);
+registerDirectory("XREAppFeat", distroDir);
+
+let testserver = new HttpServer();
+testserver.registerDirectory("/data/", do_get_file("data/system_addons"));
+testserver.start();
+let root = `${testserver.identity.primaryScheme}://${testserver.identity.primaryHost}:${testserver.identity.primaryPort}/data/`;
+Services.prefs.setCharPref(PREF_SYSTEM_ADDON_UPDATE_URL, root + "update.xml");
+
+
+// Note that we would normally use BootstrapMonitor but it currently requires
+// the objects in `data` to be serializable, and we need a real reference to the
+// `instanceID` symbol to test.
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function promiseInstallPostponed(addonID1, addonID2) {
+  return new Promise((resolve, reject) => {
+    let seen = [];
+    let listener = {
+      onInstallFailed: () => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not have failed");
+      },
+      onInstallEnded: (install) => {
+        AddonManager.removeInstallListener(listener);
+        reject(`extension installation should not have ended for ${install.addon.id}`);
+      },
+      onInstallPostponed: (install) => {
+        seen.push(install.addon.id);
+        if (seen.includes(addonID1) && seen.includes(addonID2)) {
+          AddonManager.removeInstallListener(listener);
+          resolve();
+        }
+      }
+    };
+
+    AddonManager.addInstallListener(listener);
+  });
+}
+
+function promiseInstallResumed(addonID1, addonID2) {
+  return new Promise((resolve, reject) => {
+    let seenPostponed = [];
+    let seenEnded = [];
+    let listener = {
+      onInstallFailed: () => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not have failed");
+      },
+      onInstallEnded: (install) => {
+        seenEnded.push(install.addon.id);
+        if ((seenEnded.includes(addonID1) && seenEnded.includes(addonID2)) &&
+            (seenPostponed.includes(addonID1) && seenPostponed.includes(addonID2))) {
+          AddonManager.removeInstallListener(listener);
+          resolve();
+        }
+      },
+      onInstallPostponed: (install) => {
+        seenPostponed.push(install.addon.id);
+      }
+    };
+
+    AddonManager.addInstallListener(listener);
+  });
+}
+
+function promiseInstallDeferred(addonID1, addonID2) {
+  return new Promise((resolve, reject) => {
+    let seenEnded = [];
+    let listener = {
+      onInstallFailed: () => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not have failed");
+      },
+      onInstallEnded: (install) => {
+        seenEnded.push(install.addon.id);
+        if (seenEnded.includes(addonID1) && seenEnded.includes(addonID2)) {
+          AddonManager.removeInstallListener(listener);
+          resolve();
+        }
+      },
+      onInstallPostponed: (install) => {
+        AddonManager.removeInstallListener(listener);
+        reject(`extension installation should not have been postponed for ${install.addon.id}`);
+      }
+    };
+
+    AddonManager.addInstallListener(listener);
+  });
+}
+
+
+// add-on registers upgrade listener, and ignores update.
+add_task(function*() {
+  // discard system addon updates
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+  do_get_file("data/system_addons/system_delay_ignore.xpi").copyTo(distroDir, "system_delay_ignore@tests.mozilla.org.xpi");
+  do_get_file("data/system_addons/system1_1.xpi").copyTo(distroDir, "system1@tests.mozilla.org.xpi");
+
+  startupManager();
+  let updateList = [
+    { id: IGNORE_ID, version: "2.0", path: "system_delay_ignore_2.xpi" },
+    { id: NORMAL_ID, version: "2.0", path: "system1_2.xpi" },
+  ];
+
+  let postponed = promiseInstallPostponed(IGNORE_ID, NORMAL_ID);
+  yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+  yield postponed;
+
+  // addon upgrade has been delayed.
+  let addon_postponed = yield promiseAddonByID(IGNORE_ID, NORMAL_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Test Delay Update Ignore");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+  do_check_true(Services.prefs.getBoolPref(TEST_IGNORE_PREF));
+
+  // other addons in the set are delayed as well.
+  addon_postponed = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Add-on 1");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  // restarting allows upgrades to proceed
+  yield promiseRestartManager();
+
+  let addon_upgraded = yield promiseAddonByID(IGNORE_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Test Delay Update Ignore");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  addon_upgraded = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Add-on 1");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  yield shutdownManager();
+});
+
+// add-on registers upgrade listener, and allows update.
+add_task(function*() {
+  // discard system addon updates
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+  do_get_file("data/system_addons/system_delay_complete.xpi").copyTo(distroDir, "system_delay_complete@tests.mozilla.org.xpi");
+  do_get_file("data/system_addons/system1_1.xpi").copyTo(distroDir, "system1@tests.mozilla.org.xpi");
+
+  startupManager();
+
+  let updateList = [
+    { id: COMPLETE_ID, version: "2.0", path: "system_delay_complete_2.xpi" },
+    { id: NORMAL_ID, version: "2.0", path: "system1_2.xpi" },
+  ];
+
+  // initial state
+  let addon_allowed = yield promiseAddonByID(COMPLETE_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "1.0");
+  do_check_eq(addon_allowed.name, "System Test Delay Update Complete");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  addon_allowed = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "1.0");
+  do_check_eq(addon_allowed.name, "System Add-on 1");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  let resumed = promiseInstallResumed(COMPLETE_ID, NORMAL_ID);
+  yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+
+  // update is initially postponed, then resumed
+  yield resumed;
+
+  // addon upgrade has been allowed
+  addon_allowed = yield promiseAddonByID(COMPLETE_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "2.0");
+  do_check_eq(addon_allowed.name, "System Test Delay Update Complete");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  // other upgrades in the set are allowed as well
+  addon_allowed = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "2.0");
+  do_check_eq(addon_allowed.name, "System Add-on 1");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  // restarting changes nothing
+  yield promiseRestartManager();
+
+  let addon_upgraded = yield promiseAddonByID(COMPLETE_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Test Delay Update Complete");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  addon_upgraded = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Add-on 1");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  yield shutdownManager();
+});
+
+// add-on registers upgrade listener, initially defers update then allows upgrade
+add_task(function*() {
+  // discard system addon updates
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+  do_get_file("data/system_addons/system_delay_defer.xpi").copyTo(distroDir, "system_delay_defer@tests.mozilla.org.xpi");
+  do_get_file("data/system_addons/system1_1.xpi").copyTo(distroDir, "system1@tests.mozilla.org.xpi");
+
+  startupManager();
+
+  let updateList = [
+    { id: DEFER_ID, version: "2.0", path: "system_delay_defer_2.xpi" },
+    { id: NORMAL_ID, version: "2.0", path: "system1_2.xpi" },
+  ];
+
+  let postponed = promiseInstallPostponed(DEFER_ID, NORMAL_ID);
+  yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+  yield postponed;
+
+  // upgrade is initially postponed
+  let addon_postponed = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Test Delay Update Defer");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  // other addons in the set are postponed as well.
+  addon_postponed = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Add-on 1");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  let deferred = promiseInstallDeferred(DEFER_ID, NORMAL_ID);
+  // add-on will not allow upgrade until fake event fires
+  AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+  yield deferred;
+
+  // addon upgrade has been allowed
+  let addon_allowed = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "2.0");
+  do_check_eq(addon_allowed.name, "System Test Delay Update Defer");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  // other addons in the set are allowed as well.
+  addon_allowed = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "2.0");
+  do_check_eq(addon_allowed.name, "System Add-on 1");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  // restarting changes nothing
+  yield promiseRestartManager();
+
+  let addon_upgraded = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Test Delay Update Defer");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  addon_upgraded = yield promiseAddonByID(NORMAL_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Add-on 1");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  yield shutdownManager();
+});
+
+// multiple add-ons registers upgrade listeners, initially defers then each unblock in turn.
+add_task(function*() {
+  // discard system addon updates.
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+  do_get_file("data/system_addons/system_delay_defer.xpi").copyTo(distroDir, "system_delay_defer@tests.mozilla.org.xpi");
+  do_get_file("data/system_addons/system_delay_defer_also.xpi").copyTo(distroDir, "system_delay_defer_also@tests.mozilla.org.xpi");
+
+  startupManager();
+
+  let updateList = [
+    { id: DEFER_ID, version: "2.0", path: "system_delay_defer_2.xpi" },
+    { id: DEFER_ALSO_ID, version: "2.0", path: "system_delay_defer_also_2.xpi" },
+  ];
+
+  let postponed = promiseInstallPostponed(DEFER_ID, DEFER_ALSO_ID);
+  yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+  yield postponed;
+
+  // upgrade is initially postponed
+  let addon_postponed = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Test Delay Update Defer");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  // other addons in the set are postponed as well.
+  addon_postponed = yield promiseAddonByID(DEFER_ALSO_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Test Delay Update Defer Also");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  let deferred = promiseInstallDeferred(DEFER_ID, DEFER_ALSO_ID);
+  // add-on will not allow upgrade until fake event fires
+  AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+  // Upgrade blockers still present.
+  addon_postponed = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Test Delay Update Defer");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  addon_postponed = yield promiseAddonByID(DEFER_ALSO_ID);
+  do_check_neq(addon_postponed, null);
+  do_check_eq(addon_postponed.version, "1.0");
+  do_check_eq(addon_postponed.name, "System Test Delay Update Defer Also");
+  do_check_true(addon_postponed.isCompatible);
+  do_check_false(addon_postponed.appDisabled);
+  do_check_true(addon_postponed.isActive);
+  do_check_eq(addon_postponed.type, "extension");
+
+  AddonManagerPrivate.callAddonListeners("onOtherFakeEvent");
+
+  yield deferred;
+
+  // addon upgrade has been allowed
+  let addon_allowed = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_allowed, null);
+  do_check_eq(addon_allowed.version, "2.0");
+  do_check_eq(addon_allowed.name, "System Test Delay Update Defer");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  // other addons in the set are allowed as well.
+  addon_allowed = yield promiseAddonByID(DEFER_ALSO_ID);
+  do_check_neq(addon_allowed, null);
+  // do_check_eq(addon_allowed.version, "2.0");
+  do_check_eq(addon_allowed.name, "System Test Delay Update Defer Also");
+  do_check_true(addon_allowed.isCompatible);
+  do_check_false(addon_allowed.appDisabled);
+  do_check_true(addon_allowed.isActive);
+  do_check_eq(addon_allowed.type, "extension");
+
+  // restarting changes nothing
+  yield promiseRestartManager();
+
+  let addon_upgraded = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Test Delay Update Defer");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  addon_upgraded = yield promiseAddonByID(DEFER_ALSO_ID);
+  do_check_neq(addon_upgraded, null);
+  do_check_eq(addon_upgraded.version, "2.0");
+  do_check_eq(addon_upgraded.name, "System Test Delay Update Defer Also");
+  do_check_true(addon_upgraded.isCompatible);
+  do_check_false(addon_upgraded.appDisabled);
+  do_check_true(addon_upgraded.isActive);
+  do_check_eq(addon_upgraded.type, "extension");
+
+  yield shutdownManager();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -45,20 +45,24 @@ function* check_installed(conditions) {
     let expectedDir = isUpgrade ? updatesDir : distroDir;
 
     if (version) {
       // Add-on should be installed
       do_check_neq(addon, null);
       do_check_eq(addon.version, version);
       do_check_true(addon.isActive);
       do_check_false(addon.foreignInstall);
-      do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE));
-      do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
       do_check_true(addon.hidden);
       do_check_true(addon.isSystem);
+      do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE));
+      if (isUpgrade) {
+        do_check_true(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
+      } else {
+        do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
+      }
 
       // Verify the add-ons file is in the right place
       let file = expectedDir.clone();
       file.append(id + ".xpi");
       do_check_true(file.exists());
       do_check_true(file.isFile());
 
       let uri = addon.getResourceURI(null);
@@ -368,8 +372,47 @@ add_task(function* test_bad_app_cert() {
       { isUpgrade: false, version: null },
       { isUpgrade: false, version: "1.0" },
   ];
 
   yield check_installed(conditions);
 
   yield promiseShutdownManager();
 });
+
+// A failed upgrade should revert to the default set.
+add_task(function* test_updated() {
+  // Create a random dir to install into
+  let dirname = makeUUID();
+  FileUtils.getDir("ProfD", ["features", dirname], true);
+  updatesDir.append(dirname);
+
+  // Copy in the system add-ons
+  let file = do_get_file("data/system_addons/system2_2.xpi");
+  file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi");
+  file = do_get_file("data/system_addons/system_failed_update.xpi");
+  file.copyTo(updatesDir, "system_failed_update@tests.mozilla.org.xpi");
+
+  // Inject it into the system set
+  let addonSet = {
+    schema: 1,
+    directory: updatesDir.leafName,
+    addons: {
+      "system2@tests.mozilla.org": {
+        version: "2.0"
+      },
+      "system_failed_update@tests.mozilla.org": {
+        version: "1.0"
+      },
+    }
+  };
+  Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet));
+
+  startupManager(false);
+
+  let conditions = [
+      { isUpgrade: false, version: "1.0" },
+  ];
+
+  yield check_installed(conditions);
+
+  yield promiseShutdownManager();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
@@ -76,81 +76,16 @@ var root = testserver.identity.primarySc
 Services.prefs.setCharPref(PREF_SYSTEM_ADDON_UPDATE_URL, root + "update.xml");
 
 function makeUUID() {
   let uuidGen = AM_Cc["@mozilla.org/uuid-generator;1"].
                 getService(AM_Ci.nsIUUIDGenerator);
   return uuidGen.generateUUID().toString();
 }
 
-function* serve_update(xml, perform_update) {
-  testserver.registerPathHandler("/data/update.xml", (request, response) => {
-    response.write(xml);
-  });
-
-  try {
-    yield perform_update();
-  }
-  finally {
-    testserver.registerPathHandler("/data/update.xml", null);
-  }
-}
-
-// Runs an update check making it use the passed in xml string. Uses the direct
-// call to the update function so we get rejections on failure.
-function* install_system_addons(xml) {
-  do_print("Triggering system add-on update check.");
-
-  yield serve_update(xml, function*() {
-    let { XPIProvider } = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
-    yield XPIProvider.updateSystemAddons();
-  });
-}
-
-// Runs a full add-on update check which will in some cases do a system add-on
-// update check. Always succeeds.
-function* update_all_addons(xml) {
-  do_print("Triggering full add-on update check.");
-
-  yield serve_update(xml, function() {
-    return new Promise(resolve => {
-      Services.obs.addObserver(function() {
-        Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
-
-        resolve();
-      }, "addons-background-update-complete", false);
-
-      // Trigger the background update timer handler
-      gInternalManager.notify(null);
-    });
-  });
-}
-
-// Builds an update.xml file for an update check based on the data passed.
-function* build_xml(addons) {
-  let xml = `<?xml version="1.0" encoding="UTF-8"?>\n\n<updates>\n`;
-  if (addons) {
-    xml += `  <addons>\n`;
-    for (let addon of addons) {
-      xml += `    <addon id="${addon.id}" URL="${root + addon.path}" version="${addon.version}"`;
-      if (addon.size)
-        xml += ` size="${addon.size}"`;
-      if (addon.hashFunction)
-        xml += ` hashFunction="${addon.hashFunction}"`;
-      if (addon.hashValue)
-        xml += ` hashValue="${addon.hashValue}"`;
-      xml += `/>\n`;
-    }
-    xml += `  </addons>\n`;
-  }
-  xml += `</updates>\n`;
-
-  return xml;
-}
-
 function* check_installed(conditions) {
   for (let i = 0; i < conditions.length; i++) {
     let condition = conditions[i];
     let id = "system" + (i + 1) + "@tests.mozilla.org";
     let addon = yield promiseAddonByID(id);
 
     if (!("isUpgrade" in condition) || !("version" in condition)) {
       throw Error("condition must contain isUpgrade and version");
@@ -582,17 +517,17 @@ function* setup_conditions(setup) {
 
   startupManager(false);
 
   // Make sure the initial state is correct
   do_print("Checking initial state.");
   yield check_installed(setup.initialState);
 }
 
-function* verify_state(initialState, finalState = undefined) {
+function* verify_state(initialState, finalState = undefined, alreadyUpgraded = false) {
   let expectedDirs = 0;
 
   // If the initial state was using the profile set then that directory will
   // still exist.
 
   if (initialState.some(a => a.isUpgrade)) {
     expectedDirs++;
   }
@@ -600,23 +535,27 @@ function* verify_state(initialState, fin
   if (finalState == undefined) {
     finalState = initialState;
   }
   else if (finalState.some(a => a.isUpgrade)) {
     // If the new state is using the profile then that directory will exist.
     expectedDirs++;
   }
 
+  // Since upgrades are restartless now, the previous update dir hasn't been removed.
+  if (alreadyUpgraded) {
+    expectedDirs++;
+  }
+
   do_print("Checking final state.");
 
   let dirs = yield get_directories();
   do_check_eq(dirs.length, expectedDirs);
 
-  // Bug 1204156: Currently switching to the new state requires a restart
-  // yield check_installed(...finalState);
+  yield check_installed(...finalState);
 
   // Check that the new state is active after a restart
   yield promiseRestartManager();
   yield check_installed(finalState);
 }
 
 function* exec_test(setupName, testName) {
   let setup = TEST_CONDITIONS[setupName];
@@ -624,17 +563,17 @@ function* exec_test(setupName, testName)
 
   yield setup_conditions(setup);
 
   try {
     if ("test" in test) {
       yield test.test();
     }
     else {
-      yield install_system_addons(yield build_xml(test.updateList));
+      yield installSystemAddons(yield buildSystemAddonUpdates(test.updateList, root), testserver);
     }
 
     if (test.fails) {
       do_throw("Expected this test to fail");
     }
   }
   catch (e) {
     if (!test.fails) {
@@ -665,20 +604,20 @@ add_task(function*() {
 });
 
 // Some custom tests
 // Test that the update check is performed as part of the regular add-on update
 // check
 add_task(function* test_addon_update() {
   yield setup_conditions(TEST_CONDITIONS.blank);
 
-  yield update_all_addons(yield build_xml([
+  yield updateAllSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
-  ]));
+  ], root), testserver);
 
   yield verify_state(TEST_CONDITIONS.blank.initialState, [
     {isUpgrade: false, version: null},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: false, version: null},
     {isUpgrade: false, version: null}
   ]);
@@ -686,70 +625,70 @@ add_task(function* test_addon_update() {
   yield promiseShutdownManager();
 });
 
 // Disabling app updates should block system add-on updates
 add_task(function* test_app_update_disabled() {
   yield setup_conditions(TEST_CONDITIONS.blank);
 
   Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
-  yield update_all_addons(yield build_xml([
+  yield updateAllSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
-  ]));
+  ], root), testserver);
   Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
 
   yield verify_state(TEST_CONDITIONS.blank.initialState);
 
   yield promiseShutdownManager();
 });
 
 // Safe mode should block system add-on updates
 add_task(function* test_safe_mode() {
   gAppInfo.inSafeMode = true;
 
   yield setup_conditions(TEST_CONDITIONS.blank);
 
   Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
-  yield update_all_addons(yield build_xml([
+  yield updateAllSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
-  ]));
+  ], root), testserver);
   Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
 
   yield verify_state(TEST_CONDITIONS.blank.initialState);
 
   yield promiseShutdownManager();
 
   gAppInfo.inSafeMode = false;
 });
 
 // Tests that a set that matches the default set does nothing
 add_task(function* test_match_default() {
   yield setup_conditions(TEST_CONDITIONS.withAppSet);
 
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
-  ]));
+  ], root), testserver);
 
   // Shouldn't have installed an updated set
   yield verify_state(TEST_CONDITIONS.withAppSet.initialState);
 
   yield promiseShutdownManager();
 });
 
 // Tests that a set that matches the hidden default set works
 add_task(function* test_match_default_revert() {
   yield setup_conditions(TEST_CONDITIONS.withBothSets);
 
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system1@tests.mozilla.org", version: "1.0", path: "system1_1.xpi" },
     { id: "system2@tests.mozilla.org", version: "1.0", path: "system2_1.xpi" }
-  ]));
+  ], root), testserver);
 
   // This should revert to the default set instead of installing new versions
   // into an updated set.
   yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
     {isUpgrade: false, version: "1.0"},
     {isUpgrade: false, version: "1.0"},
     {isUpgrade: false, version: null},
     {isUpgrade: false, version: null},
@@ -758,92 +697,92 @@ add_task(function* test_match_default_re
 
   yield promiseShutdownManager();
 });
 
 // Tests that a set that matches the current set works
 add_task(function* test_match_current() {
   yield setup_conditions(TEST_CONDITIONS.withBothSets);
 
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
-  ]));
+  ], root), testserver);
 
   // This should remain with the current set instead of creating a new copy
   let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET));
   do_check_eq(set.directory, "prefilled");
 
   yield verify_state(TEST_CONDITIONS.withBothSets.initialState);
 
   yield promiseShutdownManager();
 });
 
 // Tests that a set with a minor change doesn't re-download existing files
 add_task(function* test_no_download() {
   yield setup_conditions(TEST_CONDITIONS.withBothSets);
 
   // The missing file here is unneeded since there is a local version already
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "missing.xpi" },
     { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
-  ]));
+  ], root), testserver);
 
   yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
     {isUpgrade: false, version: "1.0"},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: false, version: null},
     {isUpgrade: true, version: "1.0"},
     {isUpgrade: false, version: null}
   ]);
 
   yield promiseShutdownManager();
 });
 
 // Tests that a second update before a restart works
 add_task(function* test_double_update() {
   yield setup_conditions(TEST_CONDITIONS.withAppSet);
 
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
-  ]));
+  ], root), testserver);
 
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" },
     { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
-  ]));
+  ], root), testserver);
 
   yield verify_state(TEST_CONDITIONS.withAppSet.initialState, [
     {isUpgrade: false, version: null},
     {isUpgrade: false, version: "2.0"},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: true, version: "1.0"},
     {isUpgrade: false, version: null}
-  ]);
+  ], true);
 
   yield promiseShutdownManager();
 });
 
 // A second update after a restart will delete the original unused set
 add_task(function* test_update_purges() {
   yield setup_conditions(TEST_CONDITIONS.withBothSets);
 
-  yield install_system_addons(yield build_xml([
+  yield installSystemAddons(yield buildSystemAddonUpdates([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
-  ]));
+  ], root), testserver);
 
   yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
     {isUpgrade: false, version: "1.0"},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: true, version: "1.0"},
     {isUpgrade: false, version: null},
     {isUpgrade: false, version: null}
   ]);
 
-  yield install_system_addons(yield build_xml(null));
+  yield installSystemAddons(yield buildSystemAddonUpdates(null), testserver);
 
   let dirs = yield get_directories();
   do_check_eq(dirs.length, 1);
 
   yield promiseShutdownManager();
 });
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -39,10 +39,11 @@ tags = webextensions
 [test_proxy.js]
 [test_pass_symbol.js]
 [test_delay_update.js]
 [test_nodisable_hidden.js]
 [test_delay_update_webextension.js]
 skip-if = appname == "thunderbird"
 tags = webextensions
 [test_dependencies.js]
+[test_system_delay_update.js]
 
 [include:xpcshell-shared.ini]