Bug 1461062: Refactor bootstrap lifecycle management to be somewhat maintainable. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 11 May 2018 20:40:31 -0700
changeset 795016 dc655b111d4c28db532285f0d4649573c56d333e
parent 794430 7bdfa97fda86b4b47acf74103c67fbcfa2ae108b
child 795017 06b5593511f72633c21fe785974184e476ca1ce5
push id109833
push usermaglione.k@gmail.com
push dateMon, 14 May 2018 21:02:44 +0000
reviewersaswan
bugs1461062
milestone62.0a1
Bug 1461062: Refactor bootstrap lifecycle management to be somewhat maintainable. r?aswan MozReview-Commit-ID: 8OQhjqxzKYP
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIInstall.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -1975,17 +1975,17 @@ this.XPIDatabase = {
     if (!aAddon.isPlatformCompatible) {
       logger.warn(`Add-on ${aAddon.id} is not compatible with platform.`);
       return false;
     }
 
     if (aAddon.dependencies.length) {
       let isActive = id => {
         let active = XPIProvider.activeAddons.get(id);
-        return active && !active.disable;
+        return active && !active._pendingDisable;
       };
 
       if (aAddon.dependencies.some(id => !isActive(id)))
         return false;
     }
 
     if (this.isDisabledLegacy(aAddon)) {
       logger.warn(`disabling legacy extension ${aAddon.id}`);
@@ -2311,26 +2311,22 @@ this.XPIDatabase = {
       if (isDisabled) {
         AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
       } else {
         AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
       }
 
       this.updateAddonActive(aAddon, !isDisabled);
 
+      let bootstrap = XPIInternal.BootstrapScope.get(aAddon);
       if (isDisabled) {
-        if (XPIProvider.activeAddons.has(aAddon.id)) {
-          XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
-                                          BOOTSTRAP_REASONS.ADDON_DISABLE);
-          XPIProvider.unloadBootstrapScope(aAddon.id);
-        }
+        bootstrap.disable();
         AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
       } else {
-        XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
-                                        BOOTSTRAP_REASONS.ADDON_ENABLE);
+        bootstrap.startup(BOOTSTRAP_REASONS.ADDON_ENABLE);
         AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
       }
     }
 
     // Notify any other providers that a new theme has been enabled
     if (isTheme(aAddon.type)) {
       if (!isDisabled) {
         AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type);
@@ -2861,17 +2857,17 @@ this.XPIDatabaseReconcile = {
                       addonStates.get(addon));
 
       this.applyStartupChange(addon, previousVisible.get(id), xpiState);
       previousVisible.delete(id);
     }
 
     for (let [id, addon] of previousVisible) {
       if (addonExists(addon)) {
-        this.callBootstrapUninstall(addon, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+        XPIInternal.BootstrapScope.get(addon).uninstall();
       }
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
       XPIStates.removeAddon(addon.location, id);
 
       addon.visible = false;
       addon.active = false;
     }
     if (previousVisible.size) {
@@ -2927,34 +2923,26 @@ this.XPIDatabaseReconcile = {
 
     let isActive = !currentAddon.disabled;
     let wasActive = previousAddon ? previousAddon.active : currentAddon.active;
 
     if (previousAddon) {
       if (previousAddon !== currentAddon) {
         AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_CHANGED, id);
 
-        let installReason = Services.vc.compare(previousAddon.version, currentAddon.version) < 0 ?
-                            BOOTSTRAP_REASONS.ADDON_UPGRADE :
-                            BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
-
         if (previousAddon._installLocation &&
             previousAddon._sourceBundle.exists() &&
             !previousAddon._sourceBundle.equals(currentAddon._sourceBundle)) {
-          this.callBootstrapUninstall(previousAddon, installReason,
-                                      { newVersion: currentAddon.version });
+          XPIInternal.BootstrapScope.get(previousAddon).update(
+            currentAddon);
+        } else {
+          let reason = XPIInstall.newVersionReason(previousAddon.version, currentAddon.version);
+          XPIInternal.BootstrapScope.get(currentAddon).install(
+            reason, false, {oldVersion: previousAddon.version});
         }
-
-        XPIInstall.flushChromeCaches();
-
-        let file = currentAddon._sourceBundle.clone();
-        XPIProvider.callBootstrapMethod(currentAddon, file, "install", installReason,
-                                        { oldVersion: previousAddon.version });
-        if (currentAddon.disabled)
-          XPIProvider.unloadBootstrapScope(currentAddon.id);
       }
 
       if (isActive != wasActive) {
         let change = isActive ? AddonManager.STARTUP_CHANGE_ENABLED
                               : AddonManager.STARTUP_CHANGE_DISABLED;
         AddonManagerPrivate.addStartupChange(change, id);
       }
     } else if (xpiState && xpiState.wasRestored) {
@@ -2969,23 +2957,16 @@ this.XPIDatabaseReconcile = {
         // If the add-on is softblocked then assume it is softDisabled
         if (currentAddon.blocklistState == Services.blocklist.STATE_SOFTBLOCKED)
           currentAddon.softDisabled = true;
         else
           currentAddon.userDisabled = true;
       }
     } else {
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id);
-      XPIProvider.callBootstrapMethod(currentAddon, currentAddon._sourceBundle,
-                                      "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
-      if (!isActive)
-        XPIProvider.unloadBootstrapScope(currentAddon.id);
+      let scope = XPIInternal.BootstrapScope.get(currentAddon);
+      scope.install();
     }
 
     XPIDatabase.makeAddonVisible(currentAddon);
     currentAddon.active = isActive;
   },
-
-  callBootstrapUninstall(addon, reason, extraArgs) {
-    XPIProvider.callBootstrapMethod(addon, addon._sourceBundle, "uninstall", reason, extraArgs);
-    XPIProvider.unloadBootstrapScope(addon.id);
-  },
 };
--- a/toolkit/mozapps/extensions/internal/XPIInstall.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIInstall.jsm
@@ -1763,101 +1763,67 @@ class AddonInstall {
 
       stagedAddon.append(`${this.addon.id}.xpi`);
 
       await this.stageInstall(false, stagedAddon, isUpgrade);
 
       // The install is completed so it should be removed from the active list
       XPIProvider.removeActiveInstall(this);
 
-      // Deactivate and remove the old add-on as necessary
-      let reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
-      let callUpdate = false;
-      if (this.existingAddon) {
-        if (Services.vc.compare(this.existingAddon.version, this.addon.version) < 0)
-          reason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
-        else
-          reason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
-
-        callUpdate = isWebExtension(this.addon.type) && isWebExtension(this.existingAddon.type);
-
-        let file = this.existingAddon._sourceBundle;
-        if (this.existingAddon.active) {
-          XPIProvider.callBootstrapMethod(this.existingAddon, file,
-                                          "shutdown", reason,
-                                          { newVersion: this.addon.version });
-        }
-
-        if (!callUpdate) {
-          XPIProvider.callBootstrapMethod(this.existingAddon, file,
-                                          "uninstall", reason,
-                                          { newVersion: this.addon.version });
-        }
-        XPIProvider.unloadBootstrapScope(this.existingAddon.id);
-        flushChromeCaches();
-
-        if (!isUpgrade && this.existingAddon.active) {
+      let install = () => {
+        if (this.existingAddon && this.existingAddon.active && !isUpgrade) {
           XPIDatabase.updateAddonActive(this.existingAddon, false);
         }
-      }
-
-      // Install the new add-on into its final location
-      let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
-      let file = this.installLocation.installAddon({
-        id: this.addon.id,
-        source: stagedAddon,
-        existingAddonID
-      });
-
-      // Update the metadata in the database
-      this.addon._sourceBundle = file;
-      this.addon.visible = true;
-
-      if (isUpgrade) {
-        this.addon =  XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
-                                                      file.path);
-        let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
-        if (state) {
-          state.syncWithDB(this.addon, true);
+
+        // Install the new add-on into its final location
+        let existingAddonID = this.existingAddon ? this.existingAddon.id : null;
+        let file = this.installLocation.installAddon({
+          id: this.addon.id,
+          source: stagedAddon,
+          existingAddonID
+        });
+
+        // Update the metadata in the database
+        this.addon._sourceBundle = file;
+        this.addon.visible = true;
+
+        if (isUpgrade) {
+          this.addon =  XPIDatabase.updateAddonMetadata(this.existingAddon, this.addon,
+                                                        file.path);
+          let state = XPIStates.getAddon(this.installLocation.name, this.addon.id);
+          if (state) {
+            state.syncWithDB(this.addon, true);
+          } else {
+            logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon);
+          }
         } else {
-          logger.warn("Unexpected missing XPI state for add-on ${id}", this.addon);
+          this.addon.active = (this.addon.visible && !this.addon.disabled);
+          this.addon = XPIDatabase.addAddonMetadata(this.addon, file.path);
+          XPIStates.addAddon(this.addon);
+          this.addon.installDate = this.addon.updateDate;
+          XPIDatabase.saveChanges();
         }
+        XPIStates.save();
+
+        AddonManagerPrivate.callAddonListeners("onInstalled",
+                                               this.addon.wrapper);
+
+        logger.debug(`Install of ${this.sourceURI.spec} completed.`);
+        this.state = AddonManager.STATE_INSTALLED;
+        this._callInstallListeners("onInstallEnded", this.addon.wrapper);
+      };
+
+      if (this.existingAddon) {
+        XPIInternal.BootstrapScope.get(this.existingAddon).update(
+          this.addon, !this.addon.disabled, install);
       } else {
-        this.addon.active = (this.addon.visible && !this.addon.disabled);
-        this.addon = XPIDatabase.addAddonMetadata(this.addon, file.path);
-        XPIStates.addAddon(this.addon);
-        this.addon.installDate = this.addon.updateDate;
-        XPIDatabase.saveChanges();
-      }
-      XPIStates.save();
-
-      let extraParams = {};
-      if (this.existingAddon) {
-        extraParams.oldVersion = this.existingAddon.version;
+        install();
+        XPIInternal.BootstrapScope.get(this.addon).install(undefined, true);
       }
 
-      let method = callUpdate ? "update" : "install";
-      XPIProvider.callBootstrapMethod(this.addon, file, method,
-                                      reason, extraParams);
-
-      AddonManagerPrivate.callAddonListeners("onInstalled",
-                                             this.addon.wrapper);
-
-      logger.debug("Install of " + this.sourceURI.spec + " completed.");
-      this.state = AddonManager.STATE_INSTALLED;
-      this._callInstallListeners("onInstallEnded", this.addon.wrapper);
-
-      if (this.addon.active) {
-        XPIProvider.callBootstrapMethod(this.addon, file, "startup",
-                                        reason, extraParams);
-      } else {
-        // XXX this makes it dangerous to do some things in onInstallEnded
-        // listeners because important cleanup hasn't been done yet
-        XPIProvider.unloadBootstrapScope(this.addon.id);
-      }
       XPIDatabase.recordAddonTelemetry(this.addon);
 
       // Notify providers that a new theme has been enabled.
       if (isTheme(this.addon.type) && this.addon.active)
         AddonManagerPrivate.notifyAddonChanged(this.addon.id, this.addon.type);
     })().catch((e) => {
       logger.warn(`Failed to install ${this.file.path} from ${this.sourceURI.spec} to ${stagedAddon.path}`, e);
 
@@ -3544,52 +3510,41 @@ var XPIInstall = {
 
     if (XPIDatabase.mustSign(addon.type) &&
         addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
       throw new Error(`Refusing to install staged add-on ${id} with signed state ${addon.signedState}`);
     }
 
     addon.importMetadata(metadata);
 
-    var oldBootstrap = null;
     logger.debug(`Processing install of ${id} in ${location.name}`);
     let existingAddon = XPIStates.findAddon(id);
-    if (existingAddon && existingAddon.bootstrapped) {
+    if (existingAddon) {
       try {
         var file = existingAddon.file;
         if (file.exists()) {
-          oldBootstrap = existingAddon;
-
-          // We'll be replacing a currently active bootstrapped add-on so
-          // call its uninstall method
-          let newVersion = addon.version;
-          let oldVersion = existingAddon;
-          let uninstallReason = newVersionReason(oldVersion, newVersion);
-
-          XPIProvider.callBootstrapMethod(existingAddon,
-                                          file, "uninstall", uninstallReason,
-                                          { newVersion });
-          XPIProvider.unloadBootstrapScope(id);
-          flushChromeCaches();
+          let newVersion = existingAddon.version;
+          let reason = newVersionReason(existingAddon.version, newVersion);
+
+          XPIInternal.get(existingAddon).uninstall(reason, {newVersion});
         }
       } catch (e) {
         Cu.reportError(e);
       }
     }
 
     try {
       addon._sourceBundle = location.installAddon({
         id, source, existingAddonID: id,
       });
       XPIStates.addAddon(addon);
     } catch (e) {
-      if (oldBootstrap) {
+      if (existingAddon) {
         // Re-install the old add-on
-        XPIProvider.callBootstrapMethod(oldBootstrap, existingAddon, "install",
-                                        BOOTSTRAP_REASONS.ADDON_INSTALL);
+        XPIInternal.get(existingAddon).install();
       }
       throw e;
     }
 
     return addon;
   },
 
   async updateSystemAddons() {
@@ -3879,81 +3834,55 @@ var XPIInstall = {
         }
         if (app.maxVersion) {
           message += ` add-on maxVersion: ${app.maxVersion}.`;
         }
       }
       throw new Error(message);
     }
 
-    let installReason = BOOTSTRAP_REASONS.ADDON_INSTALL;
     let oldAddon = await XPIDatabase.getVisibleAddonForID(addon.id);
-    let callUpdate = false;
 
     let extraParams = {};
     extraParams.temporarilyInstalled = true;
+
+    let install = () => {
+      addon.state = AddonManager.STATE_INSTALLED;
+      logger.debug(`Install of temporary addon in ${aFile.path} completed.`);
+      addon.visible = true;
+      addon.enabled = true;
+      addon.active = true;
+      // WebExtension themes are installed as disabled, fix that here.
+      addon.userDisabled = false;
+
+      addon = XPIDatabase.addAddonMetadata(addon, addon._sourceBundle.path);
+
+      XPIStates.addAddon(addon);
+      XPIDatabase.saveChanges();
+      XPIStates.save();
+    };
+
     if (oldAddon) {
-      logger.warn("Addon with ID " + oldAddon.id + " already installed,"
-                  + " older version will be disabled");
+      logger.warn(`Addon with ID ${oldAddon.id} already installed, ` +
+                  "older version will be disabled");
 
       addon.installDate = oldAddon.installDate;
 
-      let existingAddonID = oldAddon.id;
-      let existingAddon = oldAddon._sourceBundle;
-
-      // We'll be replacing a currently active bootstrapped add-on so
-      // call its uninstall method
-      let newVersion = addon.version;
-      let oldVersion = oldAddon.version;
-
-      installReason = newVersionReason(oldVersion, newVersion);
-      let uninstallReason = installReason;
-
-      extraParams.newVersion = newVersion;
-      extraParams.oldVersion = oldVersion;
-
-      callUpdate = isWebExtension(oldAddon.type) && isWebExtension(addon.type);
-
-      if (oldAddon.active) {
-        XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
-                                        "shutdown", uninstallReason,
-                                        extraParams);
-      }
-
-      if (!callUpdate) {
-        XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
-                                        "uninstall", uninstallReason, extraParams);
-      }
-      XPIProvider.unloadBootstrapScope(existingAddonID);
-      flushChromeCaches();
+      XPIInternal.BootstrapScope.get(oldAddon).update(
+        addon, true, install);
     } else {
       addon.installDate = Date.now();
+
+      install();
+      let bootstrap = XPIInternal.BootstrapScope.get(addon);
+      bootstrap.install(undefined, true, {temporarilyInstalled: true});
     }
 
-    let file = addon._sourceBundle;
-
-    let method = callUpdate ? "update" : "install";
-    XPIProvider.callBootstrapMethod(addon, file, method, installReason, extraParams);
-    addon.state = AddonManager.STATE_INSTALLED;
-    logger.debug("Install of temporary addon in " + aFile.path + " completed.");
-    addon.visible = true;
-    addon.enabled = true;
-    addon.active = true;
-    // WebExtension themes are installed as disabled, fix that here.
-    addon.userDisabled = false;
-
-    addon = XPIDatabase.addAddonMetadata(addon, file.path);
-
-    XPIStates.addAddon(addon);
-    XPIDatabase.saveChanges();
-    XPIStates.save();
-
     AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper,
                                            false);
-    XPIProvider.callBootstrapMethod(addon, file, "startup", installReason, extraParams);
     AddonManagerPrivate.callInstallListeners("onExternalInstall",
                                              null, addon.wrapper,
                                              oldAddon ? oldAddon.wrapper : null,
                                              false);
     AddonManagerPrivate.callAddonListeners("onInstalled", addon.wrapper);
 
     // Notify providers that a new theme has been enabled.
     if (isTheme(addon.type))
@@ -4026,72 +3955,56 @@ var XPIInstall = {
     let wrapper = aAddon.wrapper;
 
     // If the add-on wasn't already pending uninstall then notify listeners.
     if (!wasPending) {
       AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper,
                                              !!aForcePending);
     }
 
-    let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
-    let callUpdate = false;
     let existingAddon = XPIStates.findAddon(aAddon.id, loc =>
       loc.name != aAddon._installLocation.name);
-    if (existingAddon) {
-      reason = newVersionReason(aAddon.version, existingAddon.version);
-      callUpdate = isWebExtension(aAddon.type) && isWebExtension(existingAddon.type);
-    }
-
+
+    let bootstrap = XPIInternal.BootstrapScope.get(aAddon);
     if (!aForcePending) {
-      if (aAddon.active) {
-        XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
-                                        reason);
-      }
-
-      if (!callUpdate) {
-        XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "uninstall",
-                                        reason);
+      let existing;
+      if (existingAddon) {
+        existing = await XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name);
       }
-      XPIStates.disableAddon(aAddon.id);
-      XPIProvider.unloadBootstrapScope(aAddon.id);
-      flushChromeCaches();
-
-      aAddon._installLocation.uninstallAddon(aAddon.id);
-      XPIDatabase.removeAddonMetadata(aAddon);
-      XPIStates.removeAddon(aAddon.location, aAddon.id);
-      AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
-
-      if (existingAddon) {
-        let existing = await XPIDatabase.getAddonInLocation(aAddon.id, existingAddon.location.name);
-        XPIDatabase.makeAddonVisible(existing);
-
-        let wrappedAddon = existing.wrapper;
-        AddonManagerPrivate.callAddonListeners("onInstalling", wrappedAddon, false);
-
-        if (!existing.disabled) {
-          XPIDatabase.updateAddonActive(existing, true);
+
+      let uninstall = () => {
+        XPIStates.disableAddon(aAddon.id);
+
+        aAddon._installLocation.uninstallAddon(aAddon.id);
+        XPIDatabase.removeAddonMetadata(aAddon);
+        XPIStates.removeAddon(aAddon.location, aAddon.id);
+        AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
+
+        if (existing) {
+          XPIDatabase.makeAddonVisible(existing);
+          AddonManagerPrivate.callAddonListeners("onInstalling", existing.wrapper, false);
+
+          if (!existing.disabled) {
+            XPIDatabase.updateAddonActive(existing, true);
+          }
         }
-
-        let method = callUpdate ? "update" : "install";
-        XPIProvider.callBootstrapMethod(existing, existing._sourceBundle,
-                                        method, reason);
-
-        if (existing.active) {
-          XPIProvider.callBootstrapMethod(existing, existing._sourceBundle,
-                                          "startup", reason);
-        } else {
-          XPIProvider.unloadBootstrapScope(existing.id);
-        }
-
-        AddonManagerPrivate.callAddonListeners("onInstalled", wrappedAddon);
+      };
+
+      if (existing) {
+        bootstrap.update(existing, !existing.disabled, uninstall);
+
+        AddonManagerPrivate.callAddonListeners("onInstalled", existing.wrapper);
+      } else {
+        XPIStates.removeAddon(aAddon.location, aAddon.id);
+        bootstrap.uninstall();
+        uninstall();
       }
     } else if (aAddon.active) {
-      XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown", reason);
       XPIStates.disableAddon(aAddon.id);
-      XPIProvider.unloadBootstrapScope(aAddon.id);
+      bootstrap.shutdown(BOOTSTRAP_REASONS.ADDON_UNINSTALL);
       XPIDatabase.updateAddonActive(aAddon, false);
     }
 
     // Notify any other providers that a new theme has been enabled
     if (isTheme(aAddon.type) && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(null, aAddon.type);
   },
 
@@ -4122,18 +4035,17 @@ var XPIInstall = {
 
     Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
 
     // TODO hide hidden add-ons (bug 557710)
     let wrapper = aAddon.wrapper;
     AddonManagerPrivate.callAddonListeners("onOperationCancelled", wrapper);
 
     if (!aAddon.disabled) {
-      XPIProvider.callBootstrapMethod(aAddon, aAddon._sourceBundle, "startup",
-                                      BOOTSTRAP_REASONS.ADDON_INSTALL);
+      XPIInternal.BootstrapScope.get(aAddon).startup(BOOTSTRAP_REASONS.ADDON_INSTALL);
       XPIDatabase.updateAddonActive(aAddon, true);
     }
 
     // Notify any other providers that this theme is now enabled again.
     if (isTheme(aAddon.type) && aAddon.active)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, false);
   },
 
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -45,18 +45,16 @@ XPCOMUtils.defineLazyModuleGetters(this,
   XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   verifyBundleSignedState: "resource://gre/modules/addons/XPIInstall.jsm",
 });
 
 XPCOMUtils.defineLazyServiceGetter(this, "aomStartup",
                                    "@mozilla.org/addons/addon-manager-startup;1",
                                    "amIAddonManagerStartup");
 
-Cu.importGlobalProperties(["URL"]);
-
 const nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
                                        "initWithPath");
 
 const PREF_DB_SCHEMA                  = "extensions.databaseSchema";
 const PREF_XPI_STATE                  = "extensions.xpiState";
 const PREF_BOOTSTRAP_ADDONS           = "extensions.bootstrappedAddons";
 const PREF_PENDING_OPERATIONS         = "extensions.pendingOperations";
 const PREF_EM_ENABLED_SCOPES          = "extensions.enabledScopes";
@@ -159,20 +157,16 @@ const BOOTSTRAP_REASONS = {
 // externally
 const TYPE_ALIASES = {
   "webextension": "extension",
   "webextension-dictionary": "dictionary",
   "webextension-langpack": "locale",
   "webextension-theme": "theme",
 };
 
-const CHROME_TYPES = new Set([
-  "extension",
-]);
-
 const SIGNED_TYPES = new Set([
   "extension",
   "webextension",
   "webextension-langpack",
   "webextension-theme",
 ]);
 
 const ALL_EXTERNAL_TYPES = new Set([
@@ -1248,16 +1242,406 @@ var XPIStates = {
     logger.debug(`Disabling XPIState for ${aId}`);
     let state = this.findAddon(aId);
     if (state) {
       state.enabled = false;
     }
   },
 };
 
+/**
+ * A helper class to manage the lifetime of and interaction with
+ * bootstrap scopes for an add-on.
+ *
+ * @param {Object} addon
+ *        The add-on which owns this scope. Should be either an
+ *        AddonInternal or XPIState object.
+ */
+class BootstrapScope {
+  constructor(addon) {
+    if (!addon.id || !addon.version || !addon.type) {
+      throw new Error("Addon must include an id, version, and type");
+    }
+
+    this.addon = addon;
+    this.instanceID = null;
+    this.scope = null;
+    this.started = false;
+  }
+
+  /**
+   * Returns a BootstrapScope object for the given add-on. If an active
+   * scope exists, it is returned. Otherwise a new one is created.
+   *
+   * @param {Object} addon
+   *        The add-on which owns this scope, as accepted by the
+   *        constructor.
+   * @returns {BootstrapScope}
+   */
+  static get(addon) {
+    let scope = XPIProvider.activeAddons.get(addon.id);
+    if (!scope) {
+      scope = new this(addon);
+    }
+    return scope;
+  }
+
+  get file() {
+    return this.addon.file || this.addon._sourceBundle;
+  }
+
+  get runInSafeMode() {
+    return "runInSafeMode" in this.addon ? this.addon.runInSafeMode : canRunInSafeMode(this.addon);
+  }
+
+  /**
+   * Calls a bootstrap method for an add-on.
+   *
+   * @param {string} aMethod
+   *        The name of the bootstrap method to call
+   * @param {integer} aReason
+   *        The reason flag to pass to the bootstrap's startup method
+   * @param {Object} [aExtraParams = {}]
+   *        An object of additional key/value pairs to pass to the method in
+   *        the params argument
+   */
+  callBootstrapMethod(aMethod, aReason, aExtraParams = {}) {
+    let {addon, runInSafeMode} = this;
+    if (Services.appinfo.inSafeMode && !runInSafeMode)
+      return;
+
+    let timeStart = new Date();
+    if (addon.type == "extension" && aMethod == "startup") {
+      logger.debug(`Registering manifest for ${this.file.path}`);
+      Components.manager.addBootstrappedManifestLocation(this.file);
+    }
+
+    try {
+      if (!this.scope) {
+        this.loadBootstrapScope(aReason);
+      }
+
+      if (aMethod == "startup" || aMethod == "shutdown") {
+        aExtraParams.instanceID = this.instanceID;
+      }
+
+      let method = undefined;
+      let {scope} = this;
+      try {
+        method = scope[aMethod] || Cu.evalInSandbox(`${aMethod};`, scope);
+      } catch (e) {
+        // An exception will be caught if the expected method is not defined.
+        // That will be logged below.
+      }
+
+      if (aMethod == "startup") {
+        this.started = true;
+      } else if (aMethod == "shutdown") {
+        this.started = false;
+
+        // Extensions are automatically deinitialized in the correct order at shutdown.
+        if (aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+          this._pendingDisable = true;
+          for (let addon of XPIProvider.getDependentAddons(this.addon)) {
+            if (addon.active)
+              XPIDatabase.updateAddonDisabledState(addon);
+          }
+        }
+      }
+
+      let installLocation = addon._installLocation || null;
+      let params = {
+        id: addon.id,
+        version: addon.version,
+        installPath: this.file.clone(),
+        resourceURI: getURIForResourceInFile(this.file, ""),
+        signedState: addon.signedState,
+        temporarilyInstalled: installLocation == TemporaryInstallLocation,
+        builtIn: installLocation instanceof BuiltInInstallLocation,
+      };
+
+      if (aMethod == "startup" && addon.startupData) {
+        params.startupData = addon.startupData;
+      }
+
+      Object.assign(params, aExtraParams);
+
+      if (addon.hasEmbeddedWebExtension) {
+        let reason = Object.keys(BOOTSTRAP_REASONS).find(
+          key => BOOTSTRAP_REASONS[key] == aReason
+        );
+
+        if (aMethod == "startup") {
+          const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor(params);
+          params.webExtension = {
+            startup: () => webExtension.startup(reason),
+          };
+        } else if (aMethod == "shutdown") {
+          LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown(reason);
+        }
+      }
+
+      if (!method) {
+        logger.warn(`Add-on ${addon.id} is missing bootstrap method ${aMethod}`);
+      } else {
+        logger.debug(`Calling bootstrap method ${aMethod} on ${addon.id} version ${addon.version}`);
+
+        let result;
+        try {
+          result = method.call(scope, params, aReason);
+        } catch (e) {
+          logger.warn(`Exception running bootstrap method ${aMethod} on ${addon.id}`, e);
+        }
+
+        if (aMethod == "startup") {
+          this.startupPromise = Promise.resolve(result);
+          this.startupPromise.catch(Cu.reportError);
+        }
+      }
+    } finally {
+      // Extensions are automatically initialized in the correct order at startup.
+      if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) {
+        for (let addon of XPIProvider.getDependentAddons(this.addon)) {
+          XPIDatabase.updateAddonDisabledState(addon);
+        }
+      }
+
+      if (addon.type == "extension" && aMethod == "shutdown" &&
+          aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+        logger.debug(`Removing manifest for ${this.file.path}`);
+        Components.manager.removeBootstrappedManifestLocation(this.file);
+      }
+      XPIProvider.setTelemetry(addon.id, `${aMethod}_MS`, new Date() - timeStart);
+    }
+  }
+
+  /**
+   * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
+   * values as constants in the scope.
+   *
+   * @param {integer?} [aReason]
+   *        The reason this bootstrap is being loaded, as passed to a
+   *        bootstrap method.
+   */
+  loadBootstrapScope(aReason) {
+    this.instanceID = Symbol(this.addon.id);
+    this._pendingDisable = false;
+
+    XPIProvider.activeAddons.set(this.addon.id, this);
+
+    // Mark the add-on as active for the crash reporter before loading.
+    // But not at app startup, since we'll already have added all of our
+    // annotations before starting any loads.
+    if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
+      XPIProvider.addAddonsToCrashReporter();
+    }
+
+    logger.debug(`Loading bootstrap scope from ${this.file.path}`);
+
+    let principal = Services.scriptSecurityManager.getSystemPrincipal();
+
+    if (!this.file.exists()) {
+      this.scope =
+        new Cu.Sandbox(principal, { sandboxName: this.file.path,
+                                    addonId: this.addon.id,
+                                    wantGlobalProperties: ["ChromeUtils"],
+                                    metadata: { addonID: this.addon.id } });
+      logger.error(`Attempted to load bootstrap scope from missing directory ${this.file.path}`);
+      return;
+    }
+
+    if (isWebExtension(this.addon.type)) {
+      this.scope = Extension.getBootstrapScope(this.addon.id, this.file);
+    } else if (this.addon.type === "webextension-langpack") {
+      this.scope = Langpack.getBootstrapScope(this.addon.id, this.file);
+    } else if (this.addon.type === "webextension-dictionary") {
+      this.scope = Dictionary.getBootstrapScope(this.addon.id, this.file);
+    } else {
+      let uri = getURIForResourceInFile(this.file, "bootstrap.js").spec;
+
+      this.scope =
+        new Cu.Sandbox(principal, { sandboxName: uri,
+                                    addonId: this.addon.id,
+                                    wantGlobalProperties: ["ChromeUtils"],
+                                    metadata: { addonID: this.addon.id, URI: uri } });
+
+      try {
+        // Copy the reason values from the global object into the bootstrap scope.
+        for (let name in BOOTSTRAP_REASONS)
+          this.scope[name] = BOOTSTRAP_REASONS[name];
+
+        // Add other stuff that extensions want.
+        Object.assign(this.scope, {Worker, ChromeWorker});
+
+        // Define a console for the add-on
+        XPCOMUtils.defineLazyGetter(
+          this.scope, "console",
+          () => new ConsoleAPI({ consoleID: `addon/${this.addon.id}` }));
+
+        this.scope.__SCRIPT_URI_SPEC__ = uri;
+        Services.scriptloader.loadSubScript(uri, this.scope);
+      } catch (e) {
+        logger.warn(`Error loading bootstrap.js for ${this.addon.id}`, e);
+      }
+    }
+
+    // Notify the BrowserToolboxProcess that a new addon has been loaded.
+    let wrappedJSObject = { id: this.addon.id, options: { global: this.scope }};
+    Services.obs.notifyObservers({ wrappedJSObject }, "toolbox-update-addon-options");
+  }
+
+  /**
+   * Unloads a bootstrap scope by dropping all references to it and then
+   * updating the list of active add-ons with the crash reporter.
+   */
+  unloadBootstrapScope() {
+    XPIProvider.activeAddons.delete(this.addon.id);
+    XPIProvider.addAddonsToCrashReporter();
+
+    this.scope = null;
+    this.startupPromise = null;
+    this.instanceID = null;
+
+    // Notify the BrowserToolboxProcess that an addon has been unloaded.
+    let wrappedJSObject = { id: this.addon.id, options: { global: null }};
+    Services.obs.notifyObservers({ wrappedJSObject }, "toolbox-update-addon-options");
+  }
+
+  /**
+   * Calls the bootstrap scope's startup method, with the given reason
+   * and extra parameters.
+   *
+   * @param {integer} reason
+   *        The reason code for the startup call.
+   * @param {Object} [aExtraParams]
+   *        Optional extra parameters to pass to the bootstrap method.
+   */
+  startup(reason, aExtraParams) {
+    this.callBootstrapMethod("startup", reason, aExtraParams);
+  }
+
+  /**
+   * Calls the bootstrap scope's shutdown method, with the given reason
+   * and extra parameters.
+   *
+   * @param {integer} reason
+   *        The reason code for the shutdown call.
+   * @param {Object} [aExtraParams]
+   *        Optional extra parameters to pass to the bootstrap method.
+   */
+  shutdown(reason, aExtraParams) {
+    this.callBootstrapMethod("shutdown", reason, aExtraParams);
+  }
+
+  /**
+   * If the add-on is already running, calls its "shutdown" method, and
+   * unloads its bootstrap scope.
+   *
+   * @param {integer} reason
+   *        The reason code for the shutdown call.
+   * @param {Object} [aExtraParams]
+   *        Optional extra parameters to pass to the bootstrap method.
+   */
+  disable() {
+    if (this.started) {
+      this.shutdown(BOOTSTRAP_REASONS.ADDON_DISABLE);
+      this.unloadBootstrapScope();
+    }
+  }
+
+  /**
+   * Calls the bootstrap scope's install method, and optionally its
+   * startup method.
+   *
+   * @param {integer} reason
+   *        The reason code for the calls.
+   * @param {boolean} [startup = false]
+   *        If true, and the add-on is active, calls its startup method
+   *        after its install method.
+   * @param {Object} [extraArgs]
+   *        Optional extra parameters to pass to the bootstrap method.
+   */
+  install(reason = BOOTSTRAP_REASONS.ADDON_INSTALL, startup, extraArgs) {
+    this._install(reason, false, startup, extraArgs);
+  }
+
+  _install(reason, callUpdate, startup, extraArgs) {
+    if (callUpdate) {
+      this.callBootstrapMethod("update", reason, extraArgs);
+    } else {
+      this.callBootstrapMethod("install", reason, extraArgs);
+    }
+
+    if (startup && this.addon.active) {
+      this.startup(reason, extraArgs);
+    } else if (this.addon.disabled) {
+      this.unloadBootstrapScope();
+    }
+  }
+
+  /**
+   * Calls the bootstrap scope's uninstall method, and unloads its
+   * bootstrap scope. If the extension is already running, its shutdown
+   * method is called before its uninstall method.
+   *
+   * @param {integer} reason
+   *        The reason code for the calls.
+   * @param {Object} [extraArgs]
+   *        Optional extra parameters to pass to the bootstrap method.
+   */
+  uninstall(reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL, extraArgs) {
+    this._uninstall(reason, false, extraArgs);
+  }
+
+  _uninstall(reason, callUpdate, extraArgs) {
+    if (this.started) {
+      this.shutdown(reason, extraArgs);
+    }
+    if (!callUpdate) {
+      this.callBootstrapMethod("uninstall", reason, extraArgs);
+    }
+    this.unloadBootstrapScope();
+    XPIInstall.flushChromeCaches();
+  }
+
+  /**
+   * Calls the appropriate sequence of shutdown, uninstall, update,
+   * startup, and install methods for updating the current scope's
+   * add-on to the given new add-on, depending on the current state of
+   * the scope.
+   *
+   * @param {Object} newAddon
+   *        The new add-on which is being installed, as expected by the
+   *        constructor.
+   * @param {boolean} [startup = false]
+   *        If true, and the new add-on is enabled, calls its startup
+   *        method as its final operation.
+   * @param {function} [updateCallback]
+   *        An optional callback function to call between uninstalling
+   *        the old add-on and installing the new one. This callback
+   *        should update any database state which is necessary for the
+   *        startup of the new add-on.
+   */
+  update(newAddon, startup = false, updateCallback) {
+    let reason = XPIInstall.newVersionReason(this.addon.version, newAddon.version);
+    let extraArgs = {oldVersion: this.addon.version, newVersion: newAddon.version};
+
+    let callUpdate = isWebExtension(this.addon.type) && isWebExtension(newAddon.type);
+
+    this._uninstall(reason, callUpdate, extraArgs);
+
+    if (updateCallback) {
+      updateCallback();
+    }
+
+    this.addon = newAddon;
+    this._install(reason, callUpdate, startup, extraArgs);
+  }
+}
+
 var XPIProvider = {
   get name() {
     return "XPIProvider";
   },
 
   BOOTSTRAP_REASONS: Object.freeze(BOOTSTRAP_REASONS),
 
   // An array of known install locations
@@ -1595,70 +1979,65 @@ var XPIProvider = {
           }
           try {
             let reason = BOOTSTRAP_REASONS.APP_STARTUP;
             // Eventually set INSTALLED reason when a bootstrap addon
             // is dropped in profile folder and automatically installed
             if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
                             .includes(addon.id))
               reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
-            this.callBootstrapMethod(addon, addon.file, "startup", reason);
+            BootstrapScope.get(addon).startup(reason);
           } catch (e) {
             logger.error("Failed to load bootstrap addon " + addon.id + " from " +
                          addon.descriptor, e);
           }
         }
         AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
       } catch (e) {
         logger.error("bootstrap startup failed", e);
         AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e);
       }
 
       // Let these shutdown a little earlier when they still have access to most
       // of XPCOM
-      Services.obs.addObserver({
-        observe(aSubject, aTopic, aData) {
-          XPIProvider.cleanupTemporaryAddons();
-          XPIProvider._closing = true;
-          for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) {
-            // If no scope has been loaded for this add-on then there is no need
-            // to shut it down (should only happen when a bootstrapped add-on is
-            // pending enable)
-            let activeAddon = XPIProvider.activeAddons.get(addon.id);
-            if (!activeAddon || !activeAddon.started) {
-              continue;
+      Services.obs.addObserver(function observer() {
+        XPIProvider.cleanupTemporaryAddons();
+        XPIProvider._closing = true;
+        for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) {
+          // If no scope has been loaded for this add-on then there is no need
+          // to shut it down (should only happen when a bootstrapped add-on is
+          // pending enable)
+          let activeAddon = XPIProvider.activeAddons.get(addon.id);
+          if (!activeAddon || !activeAddon.started) {
+            continue;
+          }
+
+          // If the add-on was pending disable then shut it down and remove it
+          // from the persisted data.
+          let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
+          if (addon._pendingDisable) {
+            reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
+          } else if (addon.location.name == KEY_APP_TEMPORARY) {
+            reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
+            let existing = XPIStates.findAddon(addon.id, loc =>
+              loc.name != TemporaryInstallLocation.name);
+            if (existing) {
+              reason = XPIInstall.newVersionReason(addon.version, existing.version);
             }
-
-            // If the add-on was pending disable then shut it down and remove it
-            // from the persisted data.
-            let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
-            if (addon.disable) {
-              reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
-            } else if (addon.location.name == KEY_APP_TEMPORARY) {
-              reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
-              let existing = XPIStates.findAddon(addon.id, loc =>
-                loc.name != TemporaryInstallLocation.name);
-              if (existing) {
-                reason = XPIInstall.newVersionReason(addon.version, existing.version);
-              }
-            }
-            XPIProvider.callBootstrapMethod(addon, addon.file,
-                                            "shutdown", reason);
           }
-          Services.obs.removeObserver(this, "quit-application-granted");
+          BootstrapScope.get(addon).shutdown(reason);
         }
+        Services.obs.removeObserver(observer, "quit-application-granted");
       }, "quit-application-granted");
 
       // Detect final-ui-startup for telemetry reporting
-      Services.obs.addObserver({
-        observe(aSubject, aTopic, aData) {
-          AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
-          XPIProvider.runPhase = XPI_AFTER_UI_STARTUP;
-          Services.obs.removeObserver(this, "final-ui-startup");
-        }
+      Services.obs.addObserver(function observer() {
+        AddonManagerPrivate.recordTimestamp("XPI_finalUIStartup");
+        XPIProvider.runPhase = XPI_AFTER_UI_STARTUP;
+        Services.obs.removeObserver(observer, "final-ui-startup");
       }, "final-ui-startup");
 
       // If we haven't yet loaded the XPI database, schedule loading it
       // to occur once other important startup work is finished.  We want
       // this to happen relatively quickly after startup so the telemetry
       // environment has complete addon information.
       //
       // Unfortunately we have to use a variety of ways do detect when it
@@ -1666,32 +2045,30 @@ var XPIProvider = {
       // sessionstore-windows-restored.  In a browser toolbox process
       // we wait for the toolbox to show up, based on xul-window-visible
       // and a visible toolbox window.
       // Finally, we have a test-only event called test-load-xpi-database
       // as a temporary workaround for bug 1372845.  The latter can be
       // cleaned up when that bug is resolved.
       if (!this.isDBLoaded) {
         const EVENTS = [ "sessionstore-windows-restored", "xul-window-visible", "test-load-xpi-database" ];
-        let observer = {
-          observe(subject, topic, data) {
-            if (topic == "xul-window-visible" &&
-                !Services.wm.getMostRecentWindow("devtools:toolbox")) {
-              return;
-            }
-
-            for (let event of EVENTS) {
-              Services.obs.removeObserver(observer, event);
-            }
-
-            // It would be nice to defer some of the work here until we
-            // have idle time but we can't yet use requestIdleCallback()
-            // from chrome.  See bug 1358476.
-            XPIDatabase.asyncLoadDB();
-          },
+        let observer = (subject, topic, data) => {
+          if (topic == "xul-window-visible" &&
+              !Services.wm.getMostRecentWindow("devtools:toolbox")) {
+            return;
+          }
+
+          for (let event of EVENTS) {
+            Services.obs.removeObserver(observer, event);
+          }
+
+          // It would be nice to defer some of the work here until we
+          // have idle time but we can't yet use requestIdleCallback()
+          // from chrome.  See bug 1358476.
+          XPIDatabase.asyncLoadDB();
         };
         for (let event of EVENTS) {
           Services.obs.addObserver(observer, event);
         }
       }
 
       AddonManagerPrivate.recordTimestamp("XPI_startup_end");
 
@@ -1756,41 +2133,32 @@ var XPIProvider = {
   },
 
   cleanupTemporaryAddons() {
     let tempLocation = XPIStates.getLocation(TemporaryInstallLocation.name);
     if (tempLocation) {
       for (let [id, addon] of tempLocation.entries()) {
         tempLocation.delete(id);
 
-        let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
-
+        let bootstrap = BootstrapScope.get(addon);
         let existing = XPIStates.findAddon(id, loc => loc != tempLocation);
-        let callUpdate = false;
-        if (existing) {
-          reason = XPIInstall.newVersionReason(addon.version, existing.version);
-          callUpdate = (isWebExtension(addon.type) && isWebExtension(existing.type));
-        }
-
-        this.callBootstrapMethod(addon, addon.file, "shutdown", reason);
-        if (!callUpdate) {
-          this.callBootstrapMethod(addon, addon.file, "uninstall", reason);
-        }
-        this.unloadBootstrapScope(id);
-        TemporaryInstallLocation.uninstallAddon(id);
-        XPIStates.removeAddon(TemporaryInstallLocation.name, id);
+
+        let cleanup = () => {
+          TemporaryInstallLocation.uninstallAddon(id);
+          XPIStates.removeAddon(TemporaryInstallLocation.name, id);
+        };
 
         if (existing) {
-          let newAddon = XPIDatabase.makeAddonLocationVisible(id, existing.location.name);
-
-          let file = new nsIFile(newAddon.path);
-
-          let data = {oldVersion: addon.version};
-          let method = callUpdate ? "update" : "install";
-          this.callBootstrapMethod(newAddon, file, method, reason, data);
+          bootstrap.update(existing, false, () => {
+            cleanup();
+            XPIDatabase.makeAddonLocationVisible(id, existing.location.name);
+          });
+        } else {
+          bootstrap.uninstall();
+          cleanup();
         }
       }
     }
   },
 
   /**
    * Verifies that all installed add-ons are still correctly signed.
    */
@@ -2475,271 +2843,16 @@ var XPIProvider = {
       case PREF_XPI_SIGNATURES_REQUIRED:
       case PREF_LANGPACK_SIGNATURES:
       case PREF_ALLOW_LEGACY:
         this.updateAddonAppDisabledStates();
         break;
       }
     }
   },
-
-  /**
-   * Loads a bootstrapped add-on's bootstrap.js into a sandbox and the reason
-   * values as constants in the scope. This will also add information about the
-   * add-on to the bootstrappedAddons dictionary and notify the crash reporter
-   * that new add-ons have been loaded.
-   *
-   * @param {string} aId
-   *        The add-on's ID
-   * @param {nsIFile} aFile
-   *        The nsIFile for the add-on
-   * @param {string} aVersion
-   *        The add-on's version
-   * @param {string} aType
-   *        The type for the add-on
-   * @param {boolean} aRunInSafeMode
-   *        Boolean indicating whether the add-on can run in safe mode.
-   * @param {string[]} aDependencies
-   *        An array of add-on IDs on which this add-on depends.
-   * @param {boolean} hasEmbeddedWebExtension
-   *        Boolean indicating whether the add-on has an embedded webextension.
-   * @param {integer?} [aReason]
-   *        The reason this bootstrap is being loaded, as passed to a
-   *        bootstrap method.
-   */
-  loadBootstrapScope(aId, aFile, aVersion, aType, aRunInSafeMode, aDependencies,
-                     hasEmbeddedWebExtension, aReason) {
-    this.activeAddons.set(aId, {
-      bootstrapScope: null,
-      // a Symbol passed to this add-on, which it can use to identify itself
-      instanceID: Symbol(aId),
-      started: false,
-    });
-
-    // Mark the add-on as active for the crash reporter before loading.
-    // But not at app startup, since we'll already have added all of our
-    // annotations before starting any loads.
-    if (aReason !== BOOTSTRAP_REASONS.APP_STARTUP) {
-      this.addAddonsToCrashReporter();
-    }
-
-    let activeAddon = this.activeAddons.get(aId);
-
-    logger.debug("Loading bootstrap scope from " + aFile.path);
-
-    let principal = Cc["@mozilla.org/systemprincipal;1"].
-                    createInstance(Ci.nsIPrincipal);
-
-    if (!aFile.exists()) {
-      activeAddon.bootstrapScope =
-        new Cu.Sandbox(principal, { sandboxName: aFile.path,
-                                    addonId: aId,
-                                    wantGlobalProperties: ["ChromeUtils"],
-                                    metadata: { addonID: aId } });
-      logger.error("Attempted to load bootstrap scope from missing directory " + aFile.path);
-      return;
-    }
-
-    if (isWebExtension(aType)) {
-      activeAddon.bootstrapScope = Extension.getBootstrapScope(aId, aFile);
-    } else if (aType === "webextension-langpack") {
-      activeAddon.bootstrapScope = Langpack.getBootstrapScope(aId, aFile);
-    } else if (aType === "webextension-dictionary") {
-      activeAddon.bootstrapScope = Dictionary.getBootstrapScope(aId, aFile);
-    } else {
-      let uri = getURIForResourceInFile(aFile, "bootstrap.js").spec;
-
-      activeAddon.bootstrapScope =
-        new Cu.Sandbox(principal, { sandboxName: uri,
-                                    addonId: aId,
-                                    wantGlobalProperties: ["ChromeUtils"],
-                                    metadata: { addonID: aId, URI: uri } });
-
-      try {
-        // Copy the reason values from the global object into the bootstrap scope.
-        for (let name in BOOTSTRAP_REASONS)
-          activeAddon.bootstrapScope[name] = BOOTSTRAP_REASONS[name];
-
-        // Add other stuff that extensions want.
-        Object.assign(activeAddon.bootstrapScope, {Worker, ChromeWorker});
-
-        // Define a console for the add-on
-        XPCOMUtils.defineLazyGetter(
-          activeAddon.bootstrapScope, "console",
-          () => new ConsoleAPI({ consoleID: "addon/" + aId }));
-
-        activeAddon.bootstrapScope.__SCRIPT_URI_SPEC__ = uri;
-        Services.scriptloader.loadSubScript(uri, activeAddon.bootstrapScope);
-      } catch (e) {
-        logger.warn("Error loading bootstrap.js for " + aId, e);
-      }
-    }
-
-    // Notify the BrowserToolboxProcess that a new addon has been loaded.
-    let wrappedJSObject = { id: aId, options: { global: activeAddon.bootstrapScope }};
-    Services.obs.notifyObservers({ wrappedJSObject }, "toolbox-update-addon-options");
-  },
-
-  /**
-   * Unloads a bootstrap scope by dropping all references to it and then
-   * updating the list of active add-ons with the crash reporter.
-   *
-   * @param {string} aId
-   *        The add-on's ID
-   */
-  unloadBootstrapScope(aId) {
-    this.activeAddons.delete(aId);
-    this.addAddonsToCrashReporter();
-
-    // Notify the BrowserToolboxProcess that an addon has been unloaded.
-    let wrappedJSObject = { id: aId, options: { global: null }};
-    Services.obs.notifyObservers({ wrappedJSObject }, "toolbox-update-addon-options");
-  },
-
-  /**
-   * Calls a bootstrap method for an add-on.
-   *
-   * @param {Object} aAddon
-   *        An object representing the add-on, with `id`, `type` and `version`
-   * @param {nsIFile} aFile
-   *        The nsIFile for the add-on
-   * @param {string} aMethod
-   *        The name of the bootstrap method to call
-   * @param {integer} aReason
-   *        The reason flag to pass to the bootstrap's startup method
-   * @param {Object?} [aExtraParams]
-   *        An object of additional key/value pairs to pass to the method in
-   *        the params argument
-   */
-  callBootstrapMethod(aAddon, aFile, aMethod, aReason, aExtraParams) {
-    if (!aAddon.id || !aAddon.version || !aAddon.type) {
-      throw new Error("aAddon must include an id, version, and type");
-    }
-
-    // Only run in safe mode if allowed to
-    let runInSafeMode = "runInSafeMode" in aAddon ? aAddon.runInSafeMode : canRunInSafeMode(aAddon);
-    if (Services.appinfo.inSafeMode && !runInSafeMode)
-      return;
-
-    let timeStart = new Date();
-    if (CHROME_TYPES.has(aAddon.type) && aMethod == "startup") {
-      logger.debug("Registering manifest for " + aFile.path);
-      Components.manager.addBootstrappedManifestLocation(aFile);
-    }
-
-    try {
-      // Load the scope if it hasn't already been loaded
-      let activeAddon = this.activeAddons.get(aAddon.id);
-      if (!activeAddon) {
-        this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type,
-                                runInSafeMode, aAddon.dependencies,
-                                aAddon.hasEmbeddedWebExtension || false,
-                                aReason);
-        activeAddon = this.activeAddons.get(aAddon.id);
-      }
-
-      if (aMethod == "startup" || aMethod == "shutdown") {
-        if (!aExtraParams) {
-          aExtraParams = {};
-        }
-        aExtraParams.instanceID = this.activeAddons.get(aAddon.id).instanceID;
-      }
-
-      let method = undefined;
-      let scope = activeAddon.bootstrapScope;
-      try {
-        method = scope[aMethod] || Cu.evalInSandbox(`${aMethod};`, scope);
-      } catch (e) {
-        // An exception will be caught if the expected method is not defined.
-        // That will be logged below.
-      }
-
-      if (aMethod == "startup") {
-        activeAddon.started = true;
-      } else if (aMethod == "shutdown") {
-        activeAddon.started = false;
-
-        // Extensions are automatically deinitialized in the correct order at shutdown.
-        if (aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
-          activeAddon.disable = true;
-          for (let addon of this.getDependentAddons(aAddon)) {
-            if (addon.active)
-              XPIDatabase.updateAddonDisabledState(addon);
-          }
-        }
-      }
-
-      let installLocation = aAddon._installLocation || null;
-      let params = {
-        id: aAddon.id,
-        version: aAddon.version,
-        installPath: aFile.clone(),
-        resourceURI: getURIForResourceInFile(aFile, ""),
-        signedState: aAddon.signedState,
-        temporarilyInstalled: installLocation == TemporaryInstallLocation,
-        builtIn: installLocation instanceof BuiltInInstallLocation,
-      };
-
-      if (aMethod == "startup" && aAddon.startupData) {
-        params.startupData = aAddon.startupData;
-      }
-
-      if (aExtraParams) {
-        for (let key in aExtraParams) {
-          params[key] = aExtraParams[key];
-        }
-      }
-
-      if (aAddon.hasEmbeddedWebExtension) {
-        let reason = Object.keys(BOOTSTRAP_REASONS).find(
-          key => BOOTSTRAP_REASONS[key] == aReason
-        );
-
-        if (aMethod == "startup") {
-          const webExtension = LegacyExtensionsUtils.getEmbeddedExtensionFor(params);
-          params.webExtension = {
-            startup: () => webExtension.startup(reason),
-          };
-        } else if (aMethod == "shutdown") {
-          LegacyExtensionsUtils.getEmbeddedExtensionFor(params).shutdown(reason);
-        }
-      }
-
-      if (!method) {
-        logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
-      } else {
-        logger.debug("Calling bootstrap method " + aMethod + " on " + aAddon.id + " version " +
-                     aAddon.version);
-
-        let result;
-        try {
-          result = method.call(scope, params, aReason);
-        } catch (e) {
-          logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
-        }
-
-        if (aMethod == "startup") {
-          activeAddon.startupPromise = Promise.resolve(result);
-          activeAddon.startupPromise.catch(Cu.reportError);
-        }
-      }
-    } finally {
-      // Extensions are automatically initialized in the correct order at startup.
-      if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) {
-        for (let addon of this.getDependentAddons(aAddon))
-          XPIDatabase.updateAddonDisabledState(addon);
-      }
-
-      if (CHROME_TYPES.has(aAddon.type) && aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
-        logger.debug("Removing manifest for " + aFile.path);
-        Components.manager.removeBootstrappedManifestLocation(aFile);
-      }
-      this.setTelemetry(aAddon.id, aMethod + "_MS", new Date() - timeStart);
-    }
-  },
 };
 
 for (let meth of ["cancelUninstallAddon", "getInstallForFile",
                   "getInstallForURL", "installTemporaryAddon",
                   "isInstallAllowed", "isInstallEnabled",
                   "uninstallAddon", "updateSystemAddons"]) {
   XPIProvider[meth] = function() {
     return XPIInstall[meth](...arguments);
@@ -3250,16 +3363,17 @@ class WinRegInstallLocation extends Dire
    */
   isLinkedAddon(aId) {
     return true;
   }
 }
 
 var XPIInternal = {
   BOOTSTRAP_REASONS,
+  BootstrapScope,
   DB_SCHEMA,
   KEY_APP_SYSTEM_ADDONS,
   KEY_APP_SYSTEM_DEFAULTS,
   KEY_APP_TEMPORARY,
   PREF_BRANCH_INSTALLED_ADDON,
   PREF_SYSTEM_ADDON_SET,
   SIGNED_TYPES,
   SystemAddonInstallLocation,
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -241,17 +241,18 @@ this.BootstrapMonitor = {
       this.installPromises.push(resolve);
     });
   },
 
   checkMatches(cached, current) {
     Assert.notEqual(cached, undefined);
     Assert.equal(current.data.version, cached.data.version);
     Assert.equal(current.data.installPath, cached.data.installPath);
-    Assert.equal(current.data.resourceURI, cached.data.resourceURI);
+    Assert.ok(Services.io.newURI(current.data.resourceURI).equals(Services.io.newURI(cached.data.resourceURI)),
+              `Resource URIs match: "${current.data.resourceURI}" == "${cached.data.resourceURI}"`);
   },
 
   checkAddonStarted(id, version = undefined) {
     let started = this.started.get(id);
     Assert.notEqual(started, undefined);
     if (version != undefined)
       Assert.equal(started.data.version, version);
 
--- a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
@@ -979,20 +979,20 @@ add_task(async function test_19() {
   ok(b1.isActive);
   ok(!b1.isSystem);
 
   equal(getShutdownReason(), ADDON_DOWNGRADE);
   equal(getUninstallReason(), ADDON_DOWNGRADE);
   equal(getInstallReason(), ADDON_DOWNGRADE);
   equal(getStartupReason(), ADDON_DOWNGRADE);
 
-  equal(getShutdownNewVersion(), undefined);
-  equal(getUninstallNewVersion(), undefined);
-  equal(getInstallOldVersion(), undefined);
-  equal(getStartupOldVersion(), undefined);
+  equal(getShutdownNewVersion(), "1.0");
+  equal(getUninstallNewVersion(), "1.0");
+  equal(getInstallOldVersion(), "2.0");
+  equal(getStartupOldVersion(), "2.0");
 
   await checkBootstrappedPref();
 });
 
 // Check that a new profile extension detected at startup replaces the non-profile
 // one
 add_task(async function test_20() {
   await promiseShutdownManager();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
@@ -4,17 +4,17 @@
 
 // This verifies that forcing undo for uninstall works
 
 const APP_STARTUP                     = 1;
 const APP_SHUTDOWN                    = 2;
 const ADDON_DISABLE                   = 4;
 const ADDON_INSTALL                   = 5;
 const ADDON_UNINSTALL                 = 6;
-const ADDON_DOWNGRADE                 = 8;
+const ADDON_UPGRADE                   = 7;
 
 const ID = "undouninstall1@tests.mozilla.org";
 const INCOMPAT_ID = "incompatible@tests.mozilla.org";
 
 
 const profileDir = gProfD.clone();
 profileDir.append("extensions");
 
@@ -239,19 +239,19 @@ add_task(async function reinstallAddonAw
   await promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
 
   a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org");
 
   ensure_test_completed();
 
   BootstrapMonitor.checkAddonInstalled(ID, "1.0");
   BootstrapMonitor.checkAddonStarted(ID, "1.0");
-  Assert.equal(getUninstallReason(ID), ADDON_DOWNGRADE);
-  Assert.equal(getInstallReason(ID), ADDON_DOWNGRADE);
-  Assert.equal(getStartupReason(ID), ADDON_DOWNGRADE);
+  Assert.equal(getUninstallReason(ID), ADDON_UPGRADE);
+  Assert.equal(getInstallReason(ID), ADDON_UPGRADE);
+  Assert.equal(getStartupReason(ID), ADDON_UPGRADE);
   Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
   Assert.ok(a1.isActive);
   Assert.ok(!a1.userDisabled);
 
   await promiseShutdownManager();
 
   Assert.equal(getShutdownReason(ID), APP_SHUTDOWN);
 
@@ -464,18 +464,18 @@ add_task(async function reinstallDisable
   await promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
 
   a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org");
 
   ensure_test_completed();
 
   BootstrapMonitor.checkAddonInstalled(ID, "1.0");
   BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
-  Assert.equal(getUninstallReason(ID), ADDON_DOWNGRADE);
-  Assert.equal(getInstallReason(ID), ADDON_DOWNGRADE);
+  Assert.equal(getUninstallReason(ID), ADDON_UPGRADE);
+  Assert.equal(getInstallReason(ID), ADDON_UPGRADE);
   Assert.equal(a1.pendingOperations, AddonManager.PENDING_NONE);
   Assert.ok(!a1.isActive);
   Assert.ok(a1.userDisabled);
 
   await promiseRestartManager();
 
   a1 = await promiseAddonByID("undouninstall1@tests.mozilla.org");