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 433040 5c07026569a099d4b4ee7a2bba5a5f926ebde9ca
parent 427220 48071b5d0cb211139be1ce7af508861cc79ac442
child 535789 6a24b5cde260aff1715aa99cbf0449372465e7bf
push id34463
push userrhelmer@mozilla.com
push dateThu, 03 Nov 2016 02:31:36 +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/AddonManager.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_ignore.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore/bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore/install.rdf
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2.xpi
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2/bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2/install.rdf
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/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -2285,34 +2285,30 @@ var AddonManagerInternal = {
      logger.debug(`Registering upgrade listener for ${addonId}`);
      this.upgradeListeners.set(addonId, aCallback);
    });
   },
 
   /**
    * Removes an UpgradeListener if the listener is registered.
    *
-   * @param  aInstanceID
-   *         The instance ID of the addon to remove
+   * @param  aID
+   *         The addon ID of the addon to remove
    */
-  removeUpgradeListener: function(aInstanceID) {
-    if (!aInstanceID || typeof aInstanceID != "symbol")
-      throw Components.Exception("aInstanceID must be a symbol",
+  removeUpgradeListener: function(aID) {
+    if (!aID || typeof aID != "string") {
+      throw Components.Exception("aID must be a non-empty string",
                                  Cr.NS_ERROR_INVALID_ARG);
-
-    this.getAddonByInstanceID(aInstanceID).then(addon => {
-      if (!addon) {
-        throw Error("No addon for instanceID:", aInstanceID.toString());
-      }
-      if (this.upgradeListeners.has(addon.id)) {
-        this.upgradeListeners.delete(addon.id);
-      } else {
-        throw Error("No upgrade listener registered for addon ID:", addon.id);
-      }
-    });
+    }
+
+    if (this.upgradeListeners.has(aID)) {
+      this.upgradeListeners.delete(aID);
+    } else {
+      throw Error("No upgrade listener registered for addon ID:", aID);
+    }
   },
 
   /**
    * Installs a temporary add-on from a local file or directory.
    * @param  aFile
    *         An nsIFile for the file or directory of the add-on to be
    *         temporarily installed.
    * @return a Promise that rejects if the add-on is not a valid restartless
@@ -3171,16 +3167,20 @@ this.AddonManagerPrivate = {
 
   hasUpgradeListener: function(aId) {
     return AddonManagerInternal.upgradeListeners.has(aId);
   },
 
   getUpgradeListener: function(aId) {
     return AddonManagerInternal.upgradeListeners.get(aId);
   },
+
+  removeUpgradeListener: function(aId) {
+    AddonManagerInternal.removeUpgradeListener(aId);
+  },
 };
 
 /**
  * This is the public API that UI and developers should be calling. All methods
  * just forward to AddonManagerInternal.
  */
 this.AddonManager = {
   // Constants for the AddonInstall.state property
@@ -3547,20 +3547,16 @@ this.AddonManager = {
   getUpgradeListener: function(aId) {
     return AddonManagerInternal.upgradeListeners.get(aId);
   },
 
   addUpgradeListener: function(aInstanceID, aCallback) {
     AddonManagerInternal.addUpgradeListener(aInstanceID, aCallback);
   },
 
-  removeUpgradeListener: function(aInstanceID) {
-    AddonManagerInternal.removeUpgradeListener(aInstanceID);
-  },
-
   addAddonListener: function(aListener) {
     AddonManagerInternal.addAddonListener(aListener);
   },
 
   removeAddonListener: function(aListener) {
     AddonManagerInternal.removeAddonListener(aListener);
   },
 
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3237,46 +3237,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*() {
@@ -3370,18 +3349,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) {
@@ -3511,17 +3491,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);
 
@@ -5459,34 +5439,41 @@ AddonInstall.prototype = {
   progress: null,
   maxProgress: null,
 
   /**
    * Initialises this install to be a staged install waiting to be applied
    *
    * @param  aManifest
    *         The cached manifest for the staged install
-   */
-  initStagedInstall: function(aManifest) {
+   * @param  aCallback
+   *         A callback to call with the resulting install.
+   */
+  initStagedInstall: function(aManifest, aCallback) {
     this.name = aManifest.name;
     this.type = aManifest.type;
     this.version = aManifest.version;
     this.icons = aManifest.icons;
     this.releaseNotesURI = aManifest.releaseNotesURI ?
                            NetUtil.newURI(aManifest.releaseNotesURI) :
-                           null
+                           null;
     this.sourceURI = aManifest.sourceURI ?
                      NetUtil.newURI(aManifest.sourceURI) :
                      null;
-    this.file = null;
+    this.file = this.sourceURI;
+    this._sourceBundle = this.sourceURI;
     this.addon = aManifest;
-
+    this.addon.sourceURI = this.sourceURI;
+
+    this.alreadyStaged = true;
     this.state = AddonManager.STATE_INSTALLED;
 
     XPIProvider.installs.push(this);
+
+    aCallback(this);
   },
 
   /**
    * Initialises this install to be an install from a local file.
    *
    * @param  aCallback
    *         The callback to pass the initialised AddonInstall to
    */
@@ -5778,17 +5765,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
@@ -6338,23 +6327,20 @@ AddonInstall.prototype = {
 
             // 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;
+                    AddonManagerPrivate.removeUpgradeListener(this.addon.id);
                     this.installLocation.releaseStagingDir();
                     this.install();
                     break;
                   default:
                     logger.warn(`${this.addon.id} cannot resume postponed upgrade from state (${this.state})`);
                     break;
                 }
               },
@@ -6406,31 +6392,34 @@ 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);
+      if (!this.alreadyStaged) {
+        yield this.unstageInstall(stagedAddon);
+      }
 
       stagedAddon.append(this.addon.id);
       stagedAddon.leafName = this.addon.id + ".xpi";
 
-      installedUnpacked = yield this.stageInstall(requiresRestart, stagedAddon, isUpgrade);
+      if (!this.alreadyStaged) {
+        installedUnpacked = yield this.stageInstall(requiresRestart, stagedAddon, isUpgrade);
+      }
 
       if (requiresRestart) {
         this.state = AddonManager.STATE_INSTALLED;
         AddonManagerPrivate.callInstallListeners("onInstallEnded",
                                                  this.listeners, this.wrapper,
                                                  this.addon.wrapper);
       }
       else {
@@ -6529,17 +6518,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",
@@ -6565,19 +6555,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
@@ -6631,46 +6621,52 @@ AddonInstall.prototype = {
       return this;
     }
 
     return this.badCertHandler.getInterface(iid);
   }
 }
 
 /**
- * Creates a new AddonInstall for an already staged install. Used when
- * installing the staged install failed for some reason.
+ * Creates a new AddonInstall for an already staged install.
  *
+ * @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
+ * @param  aCallback
+ *         A callback to call with the resulting install.
  */
-AddonInstall.createStagedInstall = function(aInstallLocation, aDir, aManifest) {
+AddonInstall.createStagedInstall = function(aInstallLocation, aDir, aManifest, aCallback = undefined) {
   let url = Services.io.newFileURI(aDir);
 
   let install = new AddonInstall(aInstallLocation, aDir);
-  install.initStagedInstall(aManifest);
+  install.initStagedInstall(aManifest, aCallback);
 };
 
 /**
- * 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);
   }
 };
 
@@ -8543,55 +8539,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)
@@ -8602,16 +8694,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)
@@ -8681,17 +8776,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*() {
@@ -8744,77 +8855,303 @@ 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.
+    let oldAddons = new Set(Object.keys(addonSet.addons));
+    let newAddons = new Set(aAddons);
+    var difference = new Set([...oldAddons].filter(x => !newAddons.has(x)));
+
+    if (difference.size > 0) {
+      for (let addonID of difference.values()) {
+        let addon = yield new Promise(resolve => AddonManager.getAddonByID(addonID, resolve));
+        if (addon) {
+          addon.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 checkPostponed = (addon) => {
+      let result = false;
+      if (AddonManagerPrivate.hasUpgradeListener(addon.id)) {
+        result = true;
+      }
+      return result;
+    }
+
+    let installAddon = Task.async(function*(addon) {
+      let install = yield new Promise(resolve => {
+        AddonInstall.createInstall(resolve, addon._sourceBundle, this);
+      });
+      // Make the new install own its temporary file.
+      install.ownsTempFile = true;
+      install.install();
+    });
+
+    let removeAddon = Task.async(function*(addon) {
+      if (addon) {
+        addon.uninstall();
+      }
     });
 
     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
+        }
+      }
+
+      this._saveAddonSet(state);
+
+      let blockers = aAddons.filter(
+        addon => AddonManagerPrivate.hasUpgradeListener(addon.id)
+      );
+
+      if (blockers) {
+        yield this.postponeAddonSet(aAddons);
+      } else {
+        yield waitForAllPromises(aAddons.map(installAddon));
+      }
     }
     catch (e) {
+      // roll back to built-in set.
+      let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_DEFAULTS];
+      let states = XPIStates.getLocation(systemAddonLocation.name);
+      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;
+  }),
+
+  /**
+   * Postpone all add-ons in a set. If any have upgrade listeners, provide
+   * them a callback with which to resume the install of the set.
+   *
+   * @param {Array} aAddons - An array of add-ons to check for upgrade listeners.
+   */
+  postponeAddonSet: Task.async(function*(aAddons) {
+    let postponeAddon = Task.async(function*(addon) {
+      // If an upgrade listener is registered for this add-on, pass control
+      // over the upgrade to the add-on.
+      let install = yield new Promise(resolve => {
+        let location = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+        AddonInstall.createInstall(resolve, addon._sourceBundle, location);
+      });
+
+      logger.info(`system add-on ${addon.id} has an upgrade listener, postponing upgrade set until restart`);
+
+      install.state = AddonManager.STATE_POSTPONED;
+
+      let stagingDir = install.installLocation.getStagingDir();
+      let stagedAddon = stagingDir.clone();
+
+      yield install.installLocation.requestStagingDir();
+      yield install.unstageInstall(stagedAddon);
+
+      stagedAddon.append(install.addon.id);
+      stagedAddon.leafName = install.addon.id + ".xpi";
+
+      yield install.stageInstall(true, stagedAddon, true);
+
+      AddonManagerPrivate.callInstallListeners("onInstallPostponed",
+                                               install.listeners, install.wrapper)
+
+      if (AddonManagerPrivate.hasUpgradeListener(addon.id)) {
+        // upgrade has been staged for restart, if it has an upgrade listener
+        // then provide it a way to resume.
+        let callback = AddonManagerPrivate.getUpgradeListener(install.addon.id);
+        callback({
+          version: install.version,
+          install: () => {
+            switch (install.state) {
+              case AddonManager.STATE_POSTPONED:
+                logger.info(`${install.addon.id} has resumed a previously postponed upgrade set`);
+                AddonManagerPrivate.removeUpgradeListener(install.addon.id);
+                install.installLocation.releaseStagingDir();
+                install.installLocation.resumeAddonSet();
+                break;
+              default:
+                logger.warn(`${install.addon.id} cannot resume postponed upgrade from state (${install.state})`);
+                break;
+            }
+          },
+        });
+      }
+    });
+
+    let blockers = aAddons.filter(
+      addon => AddonManagerPrivate.hasUpgradeListener(addon.id)
+    );
+
+    return blockers;
   }),
+
+ /**
+  * Resumes upgrade of a previously-delayed add-on set.
+  */
+  resumeAddonSet: Task.async(function*() {
+    let checkPostponed = (addon) => {
+      let result = false;
+      if (AddonManagerPrivate.hasUpgradeListener(addon.id)) {
+        result = true;
+      }
+      return result;
+    }
+
+    let resumeAddon = Task.async(function*(id) {
+      let existingAddonID = id;
+
+      let location = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+      let stagingDir = location.getStagingDir()
+      let stagedAddon = stagingDir.clone();
+      stagedAddon.append(`${id}.xpi`);
+
+      let addon = syncLoadManifestFromFile(stagedAddon, location);
+      addon.sourceURI = stagedAddon;
+
+      let install = yield new Promise(resolve => {
+        AddonInstall.createStagedInstall(location, stagingDir, addon, resolve);
+      });
+
+      install.state = AddonManager.STATE_DOWNLOADED;
+      install.install();
+    });
+
+    let addonSet = this._loadAddonSet();
+    let addonIDs = Object.keys(addonSet.addons);
+
+    let blockers = yield waitForAllPromises(addonIDs.map(checkPostponed));
+    if (blockers.some(a => a)) {
+      logger.warn("Attempted to resume system add-on install but upgrade blockers are still present");
+    } else {
+      yield waitForAllPromises(addonIDs.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..06a81e260e7319fd6dedc09e18884b398e2988ee
GIT binary patch
literal 1097
zc$^FHW@Zs#U|`^2Xitpr?2Y&syOxoGL57)ufrmkcAt^t<q`0Igu|O}YI5dQlftfd!
zFTxCnODnh;7+Jmom4bCoo#>x`+kofj`+p)W{H@F^-)ddkmwK|Xc+5(8+i*ALXp#``
zri_3P=KK@o^K+h`56fwvs}tOBAN1aLW!{N}!t>b;uRT}$w>d>~@$v(rbsAl^$$^vk
z#A-kG{7KXL^0PdC`QqOxzvllHUVC!wl;G+o+{!CYR5qT>*;c0|Zd}n2a>0A$#Z}vQ
zdc&GhMcE@o{#=Ruxo4UDv8buS?=Sy0PWbrck5Q!F>6cYoBhSS@d;0A3pJMmhLY?Aa
zzSn2ZUJ-gNt~2!xhqB5|*6>b6_H<V<_SJ?4TK;(~_WqthA`d!VSKK@#_|W5E7Khx!
zk48zZ=Qs>yCKU4AR+xKEF)Lu=mhRASMUGMpONAfX?oE2@Ce{1lSMKH;278Yd=bz86
zyRG%^h|sL>(W(rBOJ}LAGpS%*wk~jX#iranrq(a2KQ(`Py}0`aTjVXBxAP^pN1nMP
zc;~L8{w`5=vjg%IHP`Mps}7TzevI4d$(z{Q5}OP(+&+Bxc!t5^y575V!|&^AUhP+R
zkoJ|?Qj}nIXwFOvZ`p&qyuF|7tTb7!d+2Xl!mMHWciLimmWw4N^_-tpalV_M`JX8O
z9_2HDQGW370i_6FJYHpDVBiKud1hX5Nn%cpUQtRKIK~Ztv2B4F<E@iU=N&Q-aQ*(1
z%Y02UbFR3H;ufXU>%Q^PDU%p=eyUanEr^d#_6TxWUG;osb?x(>Ip@CxP4M5)mu=eX
ze%NH5XyBCnQ@9^lpYnaIyXd6G-M3|j@4gMV%`5AxCOoU}WyPEuRdaVVJ8koyB7H>A
za$dq2hq#KR{~kGQ_;A3&_r1o6#nl48Eu20H#iZ_Zv}a4yzWcx=Z_TN5>w2`?eL7r>
zj$VIoVRxUW<%yo_t=1MSJJ&dFIK1i7B@TxQhxNtw#l=;V%C8xhKiufNes6ix)N3y;
z7zHoLRmk`iw0u&pL*ANo2X^lCPd^dPRCQYZ?~5-7d?!t4y~=fR@~V@@3fhuJlT>OO
zl7zahGkD&!5!LKp^5pInp2joh4=8GTEI!wHCXYeBS8z|#jg7H-d)AizzBBt&u46}i
z*o}|HUoYl;V!0!(*P3(x+I#;)`!1YwUC+jSD&Ap{cB$Xr{kn_t1H2iT<d|{gNC^fI
z0CE|YG=f;D`I8lrKha_e*+k3;M>g>@kV1)mR!H<?7|6;7(!>OWi-Gh6W)Kemj&i(U
new file mode 100644
index 0000000000000000000000000000000000000000..9f38cb3f81409db54a132af2a62ff24811a3cf0c
GIT binary patch
literal 730
zc$^FHW@Zs#U|`^2*pe9GdEiykMH?Wm8;E%rWEhh2^Gk|LiV_R-vWi1PI2o8zV)-Ic
zfw;7Sn}Lz#D<cB~Shu(KS#Q0QXU+$oIHRrSdFJFr4^JJ>D}fqbzB=d5Uk+KZ?x|Pv
zN`_P7Q>TbM2%Z=iHEB}(ymoibOgX=9h5)z=)&pI@9T{j51vK_569WS`&;^-!#U+V3
zIeJAYX<!FL0xgfkbU^E*(|Lys1YE!W<T76)+`c7Np!Gu6lvwlH+=-rS8=g#m<JnkO
z`&mhIQRv>!XLjx@cJ~u^*F2#faeV8n-<J(OiO+n#RR3q!71^iH{>fcbnIgy6{-HoR
zYxm;IH*?$;&p1|?a60#aAJ<L8fV@Z5ODxwKFjg*{`m3A!;4St?8oL%NNPj)D{w;@R
zaY5@-w%3Lnw_k8QuS*TRv|4DLjFBSKNs;?ahoi1qEL9MWOMCZ3;K?qQgMP1_yp<Uq
zGG?Fq`^7i2o&S<uxmbJPZR`8&35!jwwwy`H(9W2-(IjA#(23rwtQOnX+?D*)+YtE4
z{r+pY9h^*?Hbp5`2B}uErC2(<H6@q?_&9yL)uxhPsx`^n^W<(#Nv6nejnV7HzOtzr
z?Pjhqar@BpaKhW7htF?qIKNo)j$F%+^j7D(b#}kb`tClE{7@+6c=6wT%*PK^>Q$LW
zc>fbRvU^c*)c@OOezOI5Gcw6B<4PhD3?KlE1%@S!AQozZVTB|Zw75bx5i`n>O>6>^
aC<%ZSk^nFaWMu<sVgka&Kw6Or!~+1WC<V9x
new file mode 100644
index 0000000000000000000000000000000000000000..9c968cab7fdf679c30c3049f190882de33234b7b
GIT binary patch
literal 1113
zc$^FHW@Zs#U|`^2aE*)dygbe6_&!Dkh8AW91|9|(hNS%blH!u0!~(sn;?NLI2Ihyq
z1R@>-acKoN10%~<pi;2zsX^Y^cMJsf{SN1`W04m7J#T{3Y&8Loj0GZ9JvnD@d8PD9
z_LfDS`uDx>ZLLXW)r*sxOg^9eK0{i{jY~o$%yUlv)XVkLl+t^q>^ORQ*E@;VjX9qr
zUcWyfeLZPS+@lH8!*8Cg|5m?k+M&gq%be|!Qc8K}E<aY05Tl%&A=t7&Jzud`R=V5a
zEcdoal_D?dbjok-^jj$E&KT2?v`2r9;N~C+Va+KYvhADMA34gGZ*aX4G%G;)khoIk
zp|qMM4_YLuWcN0yoeU8d(4Ar4qNedu@KIjI34i6eLe>9%mi#}m<>KcxFLqjO4)W8<
ztU0g69H8Ot<#V8Cv61)UYYICO9bSKVQoh=C|DqM5*-M{)owfDq+C;WBRl@soo6pHj
zObvgsnceH_zwEd}M_JFfK0lpma9}n^&sKJG`4|zN6GwM%6BTLa@!IjP<Um6(M?r1R
zi>$hv(=V6vzqtBnX++gB-8)9Y;*|;($%iwNLgt4i7W+KTh}y@H+8uc+`0@w#K7mX>
zCj+i6YG*7i1PNSAkh;7tt^W0#|NdIySt9SwXy3~#Y1Ci%BOswPX6+}1f~3&2-}ck=
zzj^B99%jE?zRL6AeC=P-0q`_%IX24k{@cBu!+{Zdo{52h8<++%^NLFnb8_^GQqsUl
zfDah`e3(g~b<*j)W&;7&?=`L7*N(Jo>sD}znwoaqH~#jiM$e>^`Zp)HeR-X&;IMd$
z7Pt4l;`Qn0I&vbt9qkTUt-Go;$M<o;Om@d_F>5;3Yi#Ez?@(L%rNF|6|9Wul_7b!C
z=QHPAQ0dIs+Ic5XakcVOwM2(m>v@+coZl2wU#J-I+d=5Kxk$?7c`f^76(bqfZMm~R
zzInsedykdwL{FU;q#4dPQ($6BSN?&8{-$${CTOlNDlg&qw3z8&-@iKzd!6%(>}RKK
z`OLRdxa8f;+fnuMJlSh6E=*N(-`#%A*sIz59cxi?Dd#!cj7*77TN?~Nx!b;w+tH=8
zX;YM9Wr%1cQwpoI+oFt|1v6UyELBxmKd(!KSFO@m)cJ$B|829Z+JgelY1<pVALEEK
zk~wfL{BHJPwd~+svK$}J|6{9Ickj1p*_Ooxc`2pq@0T45eDQpdc=OhC@eWPg<!*oF
zXS3fA@MdI^W5$(3B^W>e$Yog42x6h;S5`=VMT;n86EWi(*+dqg2ukd;LSi4oKvp)8
OCMF<U45Sw@gLnYK@W74$
new file mode 100644
index 0000000000000000000000000000000000000000..e5358ffa709048c06262f0540e3ce5c8e3616754
GIT binary patch
literal 729
zc$^FHW@Zs#U|`^2NR5l~Jn*XNq79JO4a7VQG7L%i`6b0AMTrG^S;e6toD9rzzXc*p
z{s}~sR&X;gvV3J^U;yj();{a4ck;~n;1g%G^*qm<yy)Sn<9Q`e!^>Ca-1*BPE7m>r
zYF^23N_^@Ru?N8u1EVHQil5i+?wKj)*Ub<BcR>Qs1-m=F{Ud?Ko@ZiU;0C%NGq1QL
zF(*f_C?yT-0AZj5j4>V1I_Y#?vw?u?_nKDkYe!nPbt^bUO-;M*8-M#0=j0=k;y1NB
z{rYM;!C~<hEpG39#p}}Fb>!UiaI`yUweG6Y9N)(UGua)##jNRAud$uudxzT6E$d!q
z*4kbzb5H;LbIvhN-$^H?-98~JvPmr{Pl#>FU#n!UPiM5hii<au@~afqI6H28=lSBc
z;3UxpX49m<Bnxc2CaQPn)T%Epyml6yXz5T1uHoj}yZY^vMxV-=yPKUS$g6UceRwYR
zMz-q2-<(+&=gHO_zfft^yZhElJBIAF7Z;|gxko!5@44h~(o4ZSEdRhxc3ocQePRYt
z|5mX7WeyK?2@BI!>0hPd->@d(goj8kugK&H`%5@auJ(8BGE=KGR(1X$K7U*GmBNDw
z&S~Wh-;Z(ZOSE9Ij{oW@ZFX};@9k!R|8Md+&R_jqn7i5Tt;U?@HI^^8+gflf-}&Nb
zg6vQ42@jgit@xk(`GItRHzSiAGp-aO!2kllNMKmf2x6h87gk7mL5nJ66ER~P*~BIw
ai4y;;kod<ikd+Oji3tc718D^&5Dx$t-Ugun
new file mode 100644
index 0000000000000000000000000000000000000000..1ee3e74072f5f53fe899b8156e5bd2c091b1b448
GIT binary patch
literal 1129
zc$^FHW@Zs#U|`^2m>d-0IjOhO@eCsaLk%+n0}q1?LsEWzNpVS0Vu4;(acBr91M^cS
zzK8%<zKGHaZU#n{uRx_>-BW`O=G`{n+520YL!OJB<IV3bzSpyII5wC$-e%mrv&&Uo
zD&^wj6}xUusF%-GKP#5gJ}<}fy{(<yr?y$sWUTlqQ<9zYp1iU2wq@(ucc|QT<Hrb-
zC5w*+*es|w6K(rbopI@8YSCx6e_O9T$(tH1{l&d^iITX)y7bLYV|UItD7vJl(YrV7
z?Wr4CM<ZtSD)-C(YM)^K<#NR8qEEBGd~&FtV{bF9`+WK7-<3Oe&98rz%GNq>Qhy1f
zdTEKMbWuXf(luvY;(QM-=6a&`A$z`xD`QjMlR|b`C-;k49gGi^1=+u?+N+vA!zSnA
zW07kUtLHdhS>UpAT6DOg!PlTYt{l75e7EUmnjE^k{c6(Fmy!S41?GL4to`wc<+kVR
zgc`2Dh+Zl1sX?dgjThg|%J;!5rt;oM=T;E=uypG(Bb~oJjt6GE60tfdc#Xqe$*nNn
z+n+B!f{Wj+B`-_j%#&!*X>*0|>sH-MeK}$NmYp+}3h&s_z*w?N#+UhzY2nhZ*UKJH
zJ*KZ_VD;suWG+{vjG*w2s`?8R$)DdO->*J@oBzJ;f+K|jkJm2edv<T~`5^a#RlIAt
z9D@>?z7}3O-ne9Oz^4Dn&4<i3Ej#x&xi!x4;@|S~b<6?q6yXyZ;aNK8@ApVxG+$(5
zVBiL(h|Ij=lEj=Gy`q#faDw>n&KCi~XbGZq(&@ZI1_G|%e{z|xX?DJ)*Wr|LRb*v%
z-mhCqlX((O>fdy3tg97nS8?64s(jwg>*eQ8-29`!`jh4Lj<C%IMtz4X<ydZdZ&WCC
z-x;~X)kJgY@4t0>_RD4OUO4$>iPz&6HP4jQrBC{}ZYl=liLfoPyf>rC=Cjw|qpb=5
z8ddu8HBKC}<$7)5w23JubLYV-o<m_(?VAhIL#v9G##p8}xlGdB$JV}ewdW~^WqVHD
zy{LF1m9<HBdd=c3t=4No-=$}{MHlAV1U@(2^1k$Y+=fe1vQcS8TdY6KT6ksk6F&v>
zu;~x(G*4?&jOUclssGaat#P%668CD+N$#N&(;dPLQdGLy7>=quSuZzn&()A685SpZ
zuasb_{ItXFBzN<@_A_}5?|TLJq}-T1SAX~Fue)t#T*`Iqs296&x%lhFw;PYjay@q{
zd+}=f+5(M|;)~q}_k0qc@W5&A#b4*29W)Q{W@M6M#+73w7(f8XWmwV(Vxi_=R!IIu
liz;LjF=HIrL?NIEO8m1z;vd66RyL3(CLml4q&F~wcmRp&)$#xU
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore/bootstrap.js
@@ -0,0 +1,26 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_ignore@tests.mozilla.org";
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+  Services.prefs.setBoolPref(TEST_IGNORE_PREF, false);
+
+  // explicitly ignore update, will be queued for next restart
+  if (data.hasOwnProperty("instanceID") && data.instanceID) {
+    AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+      Services.prefs.setBoolPref(TEST_IGNORE_PREF, true);
+    });
+  } else {
+    throw Error("no instanceID passed to bootstrap startup");
+  }
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>system_delay_ignore@tests.mozilla.org</em:id>
+    <em:version>1.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <!-- Front End MetaData -->
+    <em:name>System Test Delay Update Ignore</em:name>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>5</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
new file mode 100644
index 0000000000000000000000000000000000000000..26deb5bc44b8e2253380a410e2308797608faca8
GIT binary patch
literal 731
zc$^FHW@Zs#U|`^2xEvJWdEiykMH?Wm8;E%rWEhh2^Gk|LiV_R-vWi1PI2o8@o%teK
zfw;7Sn}Lz#D<cB~Shu(KS#Q0QXU+$oIHRrSdFJFr4^JJ>D}fqbzB=d5Uk+KZ?x|Pv
zN`_P7Q>TbM2%Z=iHEB}(ymoibOgX=9h5)z=f<q%b1J2Le8x1t}A`=4xH_!!{dBr7(
zIXQYoDQRE_@Okh>fH0Z^S|^>(J7gf>`u!)D`5NK&EwKWv7glKoiQoOQ)wPW$;iUde
zXU@OB)Y?>Bx2!6kXL-H+-HD%TrZv?xmBy^jwCI~$5XR#8FX)`-<-k0tJV9T#mv#I5
zzhB?DP1<UnkD7^R<}9~uQSR>+Ot?C+Xt9C9+mDBMp3Dq;rLV{GnSY6Ak+b8rSAjQf
z2u_;*AZ(g+&M}3YmE{(^<zcDnp~t2=tGTGme3V^qV@1?*nUrIz56Q|j#e^u_NIYon
z6}{tdTakTiaaBw8L(S5O$2#AaewT~L>gCflX1yg_k*4u=${o%p$6kqAd|$9M@x*i{
zwv+OIAN)8FDl&2BRj!kZSDi3c(3Uh3a&2x*66y+<pSZ_#rN_-@D({yiaXdMB{HvcF
zPnE3EZpJ<4ZXZMs+ui)RX?N8A)3?^#5o`I8-dJ0;&d#q|TK+L#Yw?7uv46jD@U`aY
zRT<7uujdqsxnP<5FGsM3Il!BdNsbv;8j)ZC0bn#RENKL>P*V&mB*mab7P5($agJ<a
c6Ocqn0j!V|fMFmj8%Prq5H1GNN=zUg0NVcn<p2Nx
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2/bootstrap.js
@@ -0,0 +1,4 @@
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+     xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+  <Description about="urn:mozilla:install-manifest">
+    <em:id>system_delay_ignore@tests.mozilla.org</em:id>
+    <em:version>2.0</em:version>
+    <em:bootstrap>true</em:bootstrap>
+    <em:multiprocessCompatible>true</em:multiprocessCompatible>
+
+    <!-- Front End MetaData -->
+    <em:name>System Test Delay Update Ignore</em:name>
+
+    <em:targetApplication>
+      <Description>
+        <em:id>xpcshell@tests.mozilla.org</em:id>
+        <em:minVersion>1</em:minVersion>
+        <em:maxVersion>5</em:maxVersion>
+      </Description>
+    </em:targetApplication>
+
+  </Description>
+</RDF>
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
@@ -1340,8 +1340,73 @@ var {promiseWriteProxyFileToDir} = Addon
 
 function writeProxyFileToDir(aDir, aAddon, aId) {
   awaitPromise(promiseWriteProxyFileToDir(aDir, aAddon, aId));
 
   let file = aDir.clone();
   file.append(aId);
   return file
 }
+
+function* serve_system_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_system_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_system_addons(xml) {
+  do_print("Triggering full add-on update check.");
+
+  yield serve_system_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_system_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;
+}
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js
@@ -0,0 +1,333 @@
+/* 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.
+
+// FIXME sign the test addons, or this will fail on beta/release
+Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+
+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 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() {
+  return new Promise((resolve, reject) => {
+    let listener = {
+      onInstallFailed: () => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not have failed");
+      },
+      onInstallEnded: () => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not have ended");
+      },
+      onInstallPostponed: (install) => {
+        AddonManager.removeInstallListener(listener);
+        ok(true, "extension should be postponed")
+        resolve();
+      }
+    };
+
+    AddonManager.addInstallListener(listener);
+  });
+}
+
+function promiseInstallEnded() {
+  return new Promise((resolve, reject) => {
+    let listener = {
+      onInstallFailed: () => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not have failed");
+      },
+      onInstallEnded: () => {
+        AddonManager.removeInstallListener(listener);
+        ok(true, "extension installation should have ended")
+        resolve();
+      },
+      onInstallPostponed: (install) => {
+        AddonManager.removeInstallListener(listener);
+        reject("extension installation should not be postponed");      }
+    };
+
+    AddonManager.addInstallListener(listener);
+  });
+}
+
+// add-on registers upgrade listener, and ignores update.
+add_task(function*() {
+
+  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: "system1@tests.mozilla.org", version: "2.0", path: "system1_2.xpi" },
+  ];
+
+  let installStatus = promiseInstallPostponed();
+  yield install_system_addons(yield build_system_xml(updateList));
+  yield installStatus;
+
+  // addon upgrade has been delayed.
+  let addon_postponed = yield promiseAddonByID(IGNORE_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("system1@tests.mozilla.org");
+  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("system1@tests.mozilla.org");
+  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*() {
+  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: "system1@tests.mozilla.org", version: "2.0", path: "system1_2.xpi" },
+  ];
+
+  let installStatus = promiseInstallPostponed();
+
+  yield install_system_addons(yield build_system_xml(updateList));
+  yield installStatus;
+
+  // upgrade is initially postponed.
+  let addon_postponed = yield promiseAddonByID(COMPLETE_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 Complete");
+  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 delayed as well.
+  addon_postponed = yield promiseAddonByID("system1@tests.mozilla.org");
+  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");
+
+  installStatus = promiseInstallEnded();
+  yield installStatus;
+
+  // addon upgrade has been allowed
+  let 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");
+
+  installStatus = promiseInstallEnded();
+  yield installStatus;
+
+  // other upgrades in the set are allowed as well
+  addon_allowed = yield promiseAddonByID("system1@tests.mozilla.org");
+  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");
+
+  // other addons in the set are allowed as well.
+  addon_upgraded = yield promiseAddonByID("system1@tests.mozilla.org");
+  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*() {
+
+  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 addon = yield promiseAddonByID(DEFER_ID);
+  do_check_neq(addon, null);
+  do_check_eq(addon.version, "1.0");
+  do_check_eq(addon.name, "System Test Delay Update Defer");
+  do_check_true(addon.isCompatible);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_eq(addon.type, "extension");
+
+  let updateList = [
+    { id: DEFER_ID, version: "2.0", path: "system_delay_defer_2.xpi" },
+    { id: "system1@tests.mozilla.org", version: "2.0", path: "system1_2.xpi" },
+  ];
+
+  let installStatus = promiseInstallPostponed();
+
+  yield install_system_addons(yield build_system_xml(updateList));
+  yield installStatus;
+
+  // 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("system1@tests.mozilla.org");
+  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");
+
+  // add-on will not allow upgrade until fake event fires
+  AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+  installStatus = promiseInstallEnded();
+  yield installStatus;
+
+  // 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");
+
+  installStatus = promiseInstallEnded();
+  yield installStatus;
+
+  // other addons in the set are allowed as well.
+  addon_allowed = yield promiseAddonByID("system1@tests.mozilla.org");
+  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();
+
+  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("system1@tests.mozilla.org");
+  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();
+});
--- a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -45,18 +45,16 @@ 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);
 
       // 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());
@@ -368,8 +366,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_install.xpi");
+  file.copyTo(updatesDir, "system_failed_install@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_install@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 install_system_addons(yield build_system_xml(test.updateList));
     }
 
     if (test.fails) {
       do_throw("Expected this test to fail");
     }
   }
   catch (e) {
     if (!test.fails) {
@@ -665,17 +604,17 @@ 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 update_all_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));
 
   yield verify_state(TEST_CONDITIONS.blank.initialState, [
     {isUpgrade: false, version: null},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: true, version: "2.0"},
@@ -686,17 +625,17 @@ 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 update_all_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));
   Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
 
   yield verify_state(TEST_CONDITIONS.blank.initialState);
 
   yield promiseShutdownManager();
@@ -704,49 +643,49 @@ add_task(function* test_app_update_disab
 
 // 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 update_all_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));
   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 install_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));
 
   // 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 install_system_addons(yield build_system_xml([
     { id: "system1@tests.mozilla.org", version: "1.0", path: "system1_1.xpi" },
     { id: "system2@tests.mozilla.org", version: "1.0", path: "system2_1.xpi" }
   ]));
 
   // 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"},
@@ -758,17 +697,17 @@ 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 install_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
   ]));
 
   // 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");
 
@@ -777,17 +716,17 @@ add_task(function* test_match_current() 
   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 install_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "missing.xpi" },
     { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
   ]));
 
   yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
     {isUpgrade: false, version: "1.0"},
     {isUpgrade: true, version: "2.0"},
     {isUpgrade: false, version: null},
@@ -797,53 +736,53 @@ add_task(function* test_no_download() {
 
   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 install_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
   ]));
 
-  yield install_system_addons(yield build_xml([
+  yield install_system_addons(yield build_system_xml([
     { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" },
     { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
   ]));
 
   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 install_system_addons(yield build_system_xml([
     { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
     { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
   ]));
 
   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 install_system_addons(yield build_system_xml(null));
 
   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]