Bug 1460717: Make processFileChanges somewhat maintainable. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 10 May 2018 13:48:08 -0700
changeset 793900 5e71e42f35ee7feba1e65aa95fddbacb46555f73
parent 793763 8d5675fb257b5c75eee7490b998a39e8cef10648
push id109522
push usermaglione.k@gmail.com
push dateThu, 10 May 2018 22:10:14 +0000
reviewersaswan
bugs1460717
milestone62.0a1
Bug 1460717: Make processFileChanges somewhat maintainable. r?aswan MozReview-Commit-ID: 8OFfptAuP7n
toolkit/mozapps/extensions/internal/XPIDatabase.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
--- a/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIDatabase.jsm
@@ -20,16 +20,17 @@ ChromeUtils.import("resource://gre/modul
 
 XPCOMUtils.defineLazyModuleGetters(this, {
   AddonManager: "resource://gre/modules/AddonManager.jsm",
   AddonManagerPrivate: "resource://gre/modules/AddonManager.jsm",
   AddonRepository: "resource://gre/modules/addons/AddonRepository.jsm",
   AddonSettings: "resource://gre/modules/addons/AddonSettings.jsm",
   AppConstants: "resource://gre/modules/AppConstants.jsm",
   DeferredTask: "resource://gre/modules/DeferredTask.jsm",
+  ExtensionUtils: "resource://gre/modules/ExtensionUtils.jsm",
   FileUtils: "resource://gre/modules/FileUtils.jsm",
   OS: "resource://gre/modules/osfile.jsm",
   Services: "resource://gre/modules/Services.jsm",
 
   Blocklist: "resource://gre/modules/Blocklist.jsm",
   UpdateChecker: "resource://gre/modules/addons/XPIInstall.jsm",
   XPIInstall: "resource://gre/modules/addons/XPIInstall.jsm",
   XPIInternal: "resource://gre/modules/addons/XPIProvider.jsm",
@@ -1319,20 +1320,17 @@ function _filterDB(addonDB, aFilter) {
   return Array.from(addonDB.values()).filter(aFilter);
 }
 
 this.XPIDatabase = {
   // true if the database connection has been opened
   initialized: false,
   // The database file
   jsonFile: FileUtils.getFile(KEY_PROFILEDIR, [FILE_JSON_DB], true),
-  // Migration data loaded from an old version of the database.
-  migrateData: null,
-  // Active add-on directories loaded from extensions.ini and prefs at startup.
-  activeBundles: null,
+  rebuildingDatabase: false,
 
   _saveTask: null,
 
   // Saved error object if we fail to read an existing database
   _loadError: null,
 
   // Saved error object if we fail to save the database
   _saveError: null,
@@ -1436,17 +1434,16 @@ this.XPIDatabase = {
    * 8) Nothing at all => build new JSON
    *
    * @param {boolean} aRebuildOnError
    *        A boolean indicating whether add-on information should be loaded
    *        from the install locations if the database needs to be rebuilt.
    *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
    */
   syncLoadDB(aRebuildOnError) {
-    this.migrateData = null;
     let fstream = null;
     let data = "";
     try {
       let readTimer = AddonManagerPrivate.simpleTimer("XPIDB_syncRead_MS");
       logger.debug("Opening XPI database " + this.jsonFile.path);
       fstream = Cc["@mozilla.org/network/file-input-stream;1"].
               createInstance(Ci.nsIFileInputStream);
       fstream.init(this.jsonFile, -1, 0, 0);
@@ -1663,44 +1660,34 @@ this.XPIDatabase = {
     this._dbPromise.then(() => {
       Services.obs.notifyObservers(this.addonDB, "xpi-database-loaded");
     });
 
     return this._dbPromise;
   },
 
   /**
-   * Rebuild the database from addon install directories. If this.migrateData
-   * is available, uses migrated information for settings on the addons found
-   * during rebuild
+   * Rebuild the database from addon install directories.
    *
    * @param {boolean} aRebuildOnError
    *        A boolean indicating whether add-on information should be loaded
    *        from the install locations if the database needs to be rebuilt.
    *        (if false, caller is XPIProvider.checkForChanges() which will rebuild)
    */
   rebuildDatabase(aRebuildOnError) {
     this.addonDB = new Map();
     this.initialized = true;
 
     if (XPIStates.size == 0) {
       // No extensions installed, so we're done
       logger.debug("Rebuilding XPI database with no extensions");
       return;
     }
 
-    // If there is no migration data then load the list of add-on directories
-    // that were active during the last run
-    if (!this.migrateData) {
-      this.activeBundles = Array.from(XPIStates.initialEnabledAddons(),
-                                      addon => addon.path);
-      if (!this.activeBundles.length)
-        this.activeBundles = null;
-    }
-
+    this.rebuildingDatabase = !!aRebuildOnError;
 
     if (aRebuildOnError) {
       logger.warn("Rebuilding add-ons database from installed extensions.");
       try {
         XPIDatabaseReconcile.processFileChanges({}, false);
       } catch (e) {
         logger.error("Failed to rebuild XPI database from installed extensions", e);
       }
@@ -2467,17 +2454,17 @@ this.XPIDatabaseReconcile = {
    */
   addMetadata(aInstallLocation, aId, aAddonState, aNewAddon, aOldAppVersion,
               aOldPlatformVersion) {
     logger.debug("New add-on " + aId + " installed in " + aInstallLocation.name);
 
     // If we had staged data for this add-on or we aren't recovering from a
     // corrupt database and we don't have migration data for this add-on then
     // this must be a new install.
-    let isNewInstall = !!aNewAddon || !XPIDatabase.activeBundles;
+    let isNewInstall = !!aNewAddon || !XPIDatabase.rebuildingDatabase;
 
     // If it's a new install and we haven't yet loaded the manifest then it
     // must be something dropped directly into the install location
     let isDetectedInstall = isNewInstall && !aNewAddon;
 
     // Load the manifest if necessary and sanity check the add-on ID
     try {
       if (!aNewAddon) {
@@ -2674,16 +2661,75 @@ this.XPIDatabaseReconcile = {
     }
 
     aOldAddon.appDisabled = !XPIDatabase.isUsableAddon(aOldAddon);
 
     return aOldAddon;
   },
 
   /**
+   * Updates the databse metadata for an existing add-on during database
+   * reconciliation.
+   *
+   * @param {DBAddonInternal} oldAddon
+   *        The existing database add-on entry.
+   * @param {XPIState} xpiState
+   *        The XPIStates entry for this add-on.
+   * @param {AddonInternal?} newAddon
+   *        The new add-on metadata for the add-on, as loaded from the
+   *        on-disk XPI's manifest, or from a staged update in
+   *        addonStartup.json.
+   * @param {boolean} aUpdateCompatibility
+   *        true to update add-ons appDisabled property when the application
+   *        version has changed
+   * @param {boolean} aSchemaChange
+   *        The schema has changed and all add-on manifests should be re-read.
+   * @returns {AddonInternal?}
+   *        The updated AddonInternal object for the add-on, if one
+   *        could be created.
+   */
+  updateExistingAddon(oldAddon, xpiState, newAddon, aUpdateCompatibility, aSchemaChange) {
+    // Here the add-on was present in the database and on disk
+    XPIDatabase.recordAddonTelemetry(oldAddon);
+
+    let installLocation = oldAddon._installLocation;
+
+    if (xpiState.mtime < oldAddon.updateDate) {
+      XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
+        mtime: xpiState.mtime,
+        oldtime: oldAddon.updateDate
+      });
+    }
+
+    // The add-on has changed if the modification time has changed, if
+    // we have an updated manifest for it, or if the schema version has
+    // changed.
+    //
+    // Also reload the metadata for add-ons in the application directory
+    // when the application version has changed.
+    if (newAddon || oldAddon.updateDate != xpiState.mtime ||
+        (aUpdateCompatibility && (installLocation.name == KEY_APP_GLOBAL ||
+                                  installLocation.name == KEY_APP_SYSTEM_DEFAULTS))) {
+      newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon);
+    } else if (oldAddon.path != xpiState.path) {
+      newAddon = this.updatePath(installLocation, oldAddon, xpiState);
+    } else if (aUpdateCompatibility || aSchemaChange) {
+      // Check compatility when the application version and/or schema
+      // version has changed. A schema change also reloads metadata from
+      // the manifests.
+      newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
+                                          aSchemaChange);
+    } else {
+      // No change
+      newAddon = oldAddon;
+    }
+    return newAddon;
+  },
+
+  /**
    * Compares the add-ons that are currently installed to those that were
    * known to be installed when the application last ran and applies any
    * changes found to the database. Also sends "startupcache-invalidate" signal to
    * observerservice if it detects that data may have changed.
    * Always called after XPIDatabase.jsm and extensions.json have been loaded.
    *
    * @param {Object} aManifests
    *        A dictionary of cached AddonInstalls for add-ons that have been
@@ -2700,320 +2746,243 @@ this.XPIDatabaseReconcile = {
    * @param {boolean} aSchemaChange
    *        The schema has changed and all add-on manifests should be re-read.
    * @returns {boolean}
    *        A boolean indicating if a change requiring flushing the caches was
    *        detected
    */
   processFileChanges(aManifests, aUpdateCompatibility, aOldAppVersion, aOldPlatformVersion,
                      aSchemaChange) {
-    let loadedManifest = (aInstallLocation, aId) => {
-      if (!(aInstallLocation.name in aManifests))
-        return null;
-      if (!(aId in aManifests[aInstallLocation.name]))
-        return null;
-      return aManifests[aInstallLocation.name][aId];
+    let findManifest = (aInstallLocation, aId) => {
+      return (aManifests[aInstallLocation.name] &&
+              aManifests[aInstallLocation.name][aId]) || null;
     };
 
-    // Add-ons loaded from the database can have an uninitialized _sourceBundle
-    // if the path was invalid. Swallow that error and say they don't exist.
-    let exists = (aAddon) => {
-      try {
-        return aAddon._sourceBundle.exists();
-      } catch (e) {
-        if (e.result == Cr.NS_ERROR_NOT_INITIALIZED)
-          return false;
-        throw e;
-      }
-    };
+    let addonExists = addon => addon._sourceBundle.exists();
+
+    let previousAddons = new ExtensionUtils.DefaultMap(() => new Map());
+    let currentAddons = new ExtensionUtils.DefaultMap(() => new Map());
 
     // Get the previous add-ons from the database and put them into maps by location
-    let previousAddons = new Map();
-    for (let a of XPIDatabase.getAddons()) {
-      let locationAddonMap = previousAddons.get(a.location);
-      if (!locationAddonMap) {
-        locationAddonMap = new Map();
-        previousAddons.set(a.location, locationAddonMap);
-      }
-      locationAddonMap.set(a.id, a);
+    for (let addon of XPIDatabase.getAddons()) {
+      previousAddons.get(addon.location).set(addon.id, addon);
     }
 
     // Keep track of add-ons whose blocklist status may have changed. We'll check this
     // after everything else.
     let addonsToCheckAgainstBlocklist = [];
 
     // Build the list of current add-ons into similar maps. When add-ons are still
     // present we re-use the add-on objects from the database and update their
     // details directly
-    let currentAddons = new Map();
+    let addonStates = new Map();
     for (let installLocation of XPIProvider.installLocations) {
-      let locationAddonMap = new Map();
-      currentAddons.set(installLocation.name, locationAddonMap);
+      let locationAddons = currentAddons.get(installLocation.name);
 
       // Get all the on-disk XPI states for this location, and keep track of which
       // ones we see in the database.
-      let states = XPIStates.getLocation(installLocation.name);
-
-      // Iterate through the add-ons installed the last time the application
-      // ran
-      let dbAddons = previousAddons.get(installLocation.name);
-      if (dbAddons) {
-        for (let [id, oldAddon] of dbAddons) {
-          // Check if the add-on is still installed
-          let xpiState = states && states.get(id);
-          if (xpiState) {
-            // Here the add-on was present in the database and on disk
-            XPIDatabase.recordAddonTelemetry(oldAddon);
-
-            // Check if the add-on has been changed outside the XPI provider
-            if (oldAddon.updateDate != xpiState.mtime) {
-              // Did time change in the wrong direction?
-              if (xpiState.mtime < oldAddon.updateDate) {
-                XPIProvider.setTelemetry(oldAddon.id, "olderFile", {
-                  mtime: xpiState.mtime,
-                  oldtime: oldAddon.updateDate
-                });
-              }
-            }
-
-            let oldPath = oldAddon.path || descriptorToPath(oldAddon.descriptor);
-
-            // The add-on has changed if the modification time has changed, if
-            // we have an updated manifest for it, or if the schema version has
-            // changed.
-            //
-            // Also reload the metadata for add-ons in the application directory
-            // when the application version has changed.
-            let newAddon = loadedManifest(installLocation, id);
-            if (newAddon || oldAddon.updateDate != xpiState.mtime ||
-                (aUpdateCompatibility && (installLocation.name == KEY_APP_GLOBAL ||
-                                          installLocation.name == KEY_APP_SYSTEM_DEFAULTS))) {
-              newAddon = this.updateMetadata(installLocation, oldAddon, xpiState, newAddon);
-            } else if (oldPath != xpiState.path) {
-              newAddon = this.updatePath(installLocation, oldAddon, xpiState);
-            } else if (aUpdateCompatibility || aSchemaChange) {
-              // Check compatility when the application version and/or schema
-              // version has changed. A schema change also reloads metadata from
-              // the manifests.
-              newAddon = this.updateCompatibility(installLocation, oldAddon, xpiState,
-                                                  aSchemaChange);
-              // We need to do a blocklist check later, but the add-on may have changed by then.
-              // Avoid storing the current copy and just get one when we need one instead.
-              addonsToCheckAgainstBlocklist.push(newAddon.id);
-            } else {
-              // No change
-              newAddon = oldAddon;
-            }
-
-            if (newAddon)
-              locationAddonMap.set(newAddon.id, newAddon);
-          } else {
-            // The add-on is in the DB, but not in xpiState (and thus not on disk).
-            this.removeMetadata(oldAddon);
+      let states = XPIStates.getLocation(installLocation.name) || new Map();
+      let dbAddons = previousAddons.get(installLocation.name) || new Map();
+      for (let [id, oldAddon] of dbAddons) {
+        // Check if the add-on is still installed
+        let xpiState = states.get(id);
+        if (xpiState) {
+          let newAddon = this.updateExistingAddon(oldAddon, xpiState,
+                                                  findManifest(installLocation, id),
+                                                  aUpdateCompatibility, aSchemaChange);
+          if (newAddon) {
+            locationAddons.set(newAddon.id, newAddon);
+
+            // We need to do a blocklist check later, but the add-on may have changed by then.
+            // Avoid storing the current copy and just get one when we need one instead.
+            addonsToCheckAgainstBlocklist.push(newAddon.id);
           }
+        } else {
+          // The add-on is in the DB, but not in xpiState (and thus not on disk).
+          this.removeMetadata(oldAddon);
         }
       }
 
-      // Any add-on in our current location that we haven't seen needs to
-      // be added to the database.
-      // Get the migration data for this install location so we can include that as
-      // we add, in case this is a database upgrade or rebuild.
-      let locMigrateData = {};
-      if (XPIDatabase.migrateData && installLocation.name in XPIDatabase.migrateData)
-        locMigrateData = XPIDatabase.migrateData[installLocation.name];
-
-      if (states) {
-        for (let [id, xpiState] of states) {
-          if (locationAddonMap.has(id))
-            continue;
-          let migrateData = id in locMigrateData ? locMigrateData[id] : null;
-          let newAddon = loadedManifest(installLocation, id);
-          let addon = this.addMetadata(installLocation, id, xpiState, newAddon,
-                                       aOldAppVersion, aOldPlatformVersion, migrateData);
-          if (addon)
-            locationAddonMap.set(addon.id, addon);
+      for (let [id, xpiState] of states) {
+        if (locationAddons.has(id))
+          continue;
+        let newAddon = findManifest(installLocation, id);
+        let addon = this.addMetadata(installLocation, id, xpiState, newAddon,
+                                     aOldAppVersion, aOldPlatformVersion);
+        if (addon) {
+          locationAddons.set(addon.id, addon);
+          addonStates.set(addon, xpiState);
         }
       }
     }
 
-    // previousAddons may contain locations where the database contains add-ons
-    // but the browser is no longer configured to use that location. The metadata
-    // for those add-ons must be removed from the database.
+    // Remove metadata for any add-ons in install locations that are no
+    // longer supported.
     for (let [locationName, addons] of previousAddons) {
       if (!currentAddons.has(locationName)) {
         for (let oldAddon of addons.values())
           this.removeMetadata(oldAddon);
       }
     }
 
     // Validate the updated system add-ons
-    let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
-    let addons = currentAddons.get(KEY_APP_SYSTEM_ADDONS) || new Map();
-
     let hideLocation;
-
-    if (!systemAddonLocation.isValid(addons)) {
-      // Hide the system add-on updates if any are invalid.
-      logger.info("One or more updated system add-ons invalid, falling back to defaults.");
-      hideLocation = KEY_APP_SYSTEM_ADDONS;
+    {
+      let systemAddonLocation = XPIProvider.installLocationsByName[KEY_APP_SYSTEM_ADDONS];
+      let addons = currentAddons.get(systemAddonLocation.name);
+
+      if (!systemAddonLocation.isValid(addons)) {
+        // Hide the system add-on updates if any are invalid.
+        logger.info("One or more updated system add-ons invalid, falling back to defaults.");
+        hideLocation = systemAddonLocation.name;
+      }
     }
 
+    // Apply startup changes to any currently-visible add-ons, and
+    // uninstall any which were previously visible, but aren't anymore.
     let previousVisible = this.getVisibleAddons(previousAddons);
     let currentVisible = this.flattenByID(currentAddons, hideLocation);
 
-    // Pass over the new set of visible add-ons, record any changes that occurred
-    // during startup and call bootstrap install/uninstall scripts as necessary
-    for (let [id, currentAddon] of currentVisible) {
-      let previousAddon = previousVisible.get(id);
-
-      let isActive = !currentAddon.disabled && !currentAddon.pendingUninstall;
-      let wasActive = previousAddon ? previousAddon.active : currentAddon.active;
-
-      if (!previousAddon) {
-        // If we had a manifest for this add-on it was a staged install and
-        // so wasn't something recovered from a corrupt database
-        let wasStaged = !!loadedManifest(currentAddon._installLocation, id);
-
-        // We might be recovering from a corrupt database, if so use the
-        // list of known active add-ons to update the new add-on
-        if (!wasStaged && XPIDatabase.activeBundles) {
-          isActive = XPIDatabase.activeBundles.includes(currentAddon.path);
-
-          if (currentAddon.type == "webextension-theme")
-            currentAddon.userDisabled = !isActive;
-
-          // If the add-on wasn't active and it isn't already disabled in some way
-          // then it was probably either softDisabled or userDisabled
-          if (!isActive && !currentAddon.disabled) {
-            // 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 {
-          // This is a new install
-          if (currentAddon.foreignInstall)
-            AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id);
-
-          if (currentAddon.bootstrap) {
-            AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id);
-            // Visible bootstrapped add-ons need to have their install method called
-            XPIProvider.callBootstrapMethod(currentAddon, currentAddon._sourceBundle,
-                                            "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
-            if (!isActive)
-              XPIProvider.unloadBootstrapScope(currentAddon.id);
-          }
-        }
-      } else {
-        if (previousAddon !== currentAddon) {
-          // This is an add-on that has changed, either the metadata was reloaded
-          // or the version in a different location has become visible
-          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 the previous add-on was in a different path, bootstrapped
-          // and still exists then call its uninstall method.
-          if (previousAddon.bootstrap && previousAddon._installLocation &&
-              exists(previousAddon) &&
-              currentAddon._sourceBundle.path != previousAddon._sourceBundle.path) {
-
-            XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
-                                            "uninstall", installReason,
-                                            { newVersion: currentAddon.version });
-            XPIProvider.unloadBootstrapScope(previousAddon.id);
-          }
-
-          // Make sure to flush the cache when an old add-on has gone away
-          XPIInstall.flushChromeCaches();
-
-          if (currentAddon.bootstrap) {
-            // Visible bootstrapped add-ons need to have their install method called
-            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);
-        }
-      }
-
-      XPIDatabase.makeAddonVisible(currentAddon);
-      currentAddon.active = isActive;
+    for (let [id, addon] of currentVisible) {
+      // If we have a stored manifest for the add-on, it came from the
+      // startup data cache, and supersedes any previous XPIStates entry.
+      let xpiState = (!findManifest(addon._installLocation, id) &&
+                      addonStates.get(addon));
+
+      this.applyStartupChange(addon, previousVisible.get(id), xpiState);
+      previousVisible.delete(id);
     }
 
-    // Pass over the set of previously visible add-ons that have now gone away
-    // and record the change.
-    for (let [id, previousAddon] of previousVisible) {
-      if (currentVisible.has(id))
-        continue;
-
-      // This add-on vanished
-
-      // If the previous add-on was bootstrapped and still exists then call its
-      // uninstall method.
-      if (previousAddon.bootstrap && exists(previousAddon)) {
-        XPIProvider.callBootstrapMethod(previousAddon, previousAddon._sourceBundle,
-                                        "uninstall", BOOTSTRAP_REASONS.ADDON_UNINSTALL);
-        XPIProvider.unloadBootstrapScope(previousAddon.id);
+    for (let [id, addon] of previousVisible) {
+      if (addon.bootstrap && addonExists(addon)) {
+        this.callBootstrapUninstall(addon, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
       }
       AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_UNINSTALLED, id);
-      XPIStates.removeAddon(previousAddon.location, id);
-
-      // Make sure to flush the cache when an old add-on has gone away
-      XPIInstall.flushChromeCaches();
+      XPIStates.removeAddon(addon.location, id);
+
+      addon.visible = false;
+      addon.active = false;
     }
-
-    // Make sure add-ons from hidden locations are marked invisible and inactive
-    let locationAddonMap = currentAddons.get(hideLocation);
-    if (locationAddonMap) {
-      for (let addon of locationAddonMap.values()) {
-        addon.visible = false;
-        addon.active = false;
-      }
+    if (previousVisible.size) {
+      XPIInstall.flushChromeCaches();
     }
 
     // Finally update XPIStates to match everything
-    for (let [locationName, locationAddonMap] of currentAddons) {
-      for (let [id, addon] of locationAddonMap) {
+    for (let [locationName, locationAddons] of currentAddons) {
+      for (let [id, addon] of locationAddons) {
         let xpiState = XPIStates.getAddon(locationName, id);
         xpiState.syncWithDB(addon);
       }
     }
     XPIStates.save();
-
-    // Clear out any cached migration data.
-    XPIDatabase.migrateData = null;
     XPIDatabase.saveChanges();
-
-    // Do some blocklist checks. These will happen after we've just saved everything,
-    // because they're async and depend on the blocklist loading. When we're done, save
-    // the data if any of the add-ons' blocklist state has changed.
-    AddonManager.shutdown.addBlocker(
-      "Update add-on blocklist state into add-on DB",
-      (async () => {
-        // Avoid querying the AddonManager immediately to give startup a chance
-        // to complete.
-        await Promise.resolve();
-        let addons = await AddonManager.getAddonsByIDs(addonsToCheckAgainstBlocklist);
-        await Promise.all(addons.map(addon => {
-          if (addon) {
-            return addon.updateBlocklistState({updateDatabase: false});
-          }
-          return null;
-        }));
-        XPIDatabase.saveChanges();
-      })().catch(Cu.reportError)
-    );
+    XPIDatabase.rebuildingDatabase = false;
+
+    if (aUpdateCompatibility || aSchemaChange) {
+      // Do some blocklist checks. These will happen after we've just saved everything,
+      // because they're async and depend on the blocklist loading. When we're done, save
+      // the data if any of the add-ons' blocklist state has changed.
+      AddonManager.shutdown.addBlocker(
+        "Update add-on blocklist state into add-on DB",
+        (async () => {
+          // Avoid querying the AddonManager immediately to give startup a chance
+          // to complete.
+          await Promise.resolve();
+
+          let addons = await AddonManager.getAddonsByIDs(addonsToCheckAgainstBlocklist);
+          await Promise.all(addons.map(addon => {
+            return addon && addon.updateBlocklistState({updateDatabase: false});
+          }));
+
+          XPIDatabase.saveChanges();
+        })());
+    }
 
     return true;
   },
+
+  /**
+   * Applies a startup change for the given add-on.
+   *
+   * @param {AddonInternal} currentAddon
+   *        The add-on as it exists in this session.
+   * @param {AddonInternal?} previousAddon
+   *        The add-on as it existed in the previous session.
+   * @param {XPIState?} xpiState
+   *        The XPIState entry for this add-on, if one exists.
+   */
+  applyStartupChange(currentAddon, previousAddon, xpiState) {
+    let {id} = currentAddon;
+
+    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.bootstrap &&
+            previousAddon._installLocation &&
+            previousAddon._sourceBundle.exists() &&
+            !previousAddon._sourceBundle.equals(currentAddon._sourceBundle)) {
+          this.callBootstrapUninstall(previousAddon, installReason,
+                                      { newVersion: currentAddon.version });
+        }
+
+        XPIInstall.flushChromeCaches();
+
+        if (currentAddon.bootstrap) {
+          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) {
+      isActive = xpiState.enabled;
+
+      if (currentAddon.type == "webextension-theme")
+        currentAddon.userDisabled = !isActive;
+
+      // If the add-on wasn't active and it isn't already disabled in some way
+      // then it was probably either softDisabled or userDisabled
+      if (!isActive && !currentAddon.disabled) {
+        // 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 {
+      // This is a new install
+      if (currentAddon.foreignInstall)
+        AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id);
+
+      if (currentAddon.bootstrap) {
+        AddonManagerPrivate.addStartupChange(AddonManager.STARTUP_CHANGE_INSTALLED, id);
+        // Visible bootstrapped add-ons need to have their install method called
+        XPIProvider.callBootstrapMethod(currentAddon, currentAddon._sourceBundle,
+                                        "install", BOOTSTRAP_REASONS.ADDON_INSTALL);
+        if (!isActive)
+          XPIProvider.unloadBootstrapScope(currentAddon.id);
+      }
+    }
+
+    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/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -777,18 +777,23 @@ class XPIStateLocation extends Map {
     this.name = name;
     this.path = path || saved.path || null;
     this.staged = saved.staged || {};
     this.changed = saved.changed || false;
     this.dir = this.path && new nsIFile(this.path);
 
     for (let [id, data] of Object.entries(saved.addons || {})) {
       let xpiState = this._addState(id, data);
-      // Make a note that this state was restored from saved data.
-      xpiState.wasRestored = true;
+
+      // Make a note that this state was restored from saved data. But
+      // only if this location hasn't moved since the last startup,
+      // since that causes problems for new system add-on bundles.
+      if (!path || path == saved.path) {
+        xpiState.wasRestored = true;
+      }
     }
   }
 
   /**
    * Returns a JSON-compatible representation of this location's state
    * data, to be saved to addonStartup.json.
    *
    * @returns {Object}
@@ -1179,28 +1184,16 @@ var XPIStates = {
         if (entry.enabled) {
           yield entry;
         }
       }
     }
   },
 
   /**
-   * Iterates over the list of all add-ons which were initially restored
-   * from the startup state cache.
-   */
-  * initialEnabledAddons() {
-    for (let addon of this.enabledAddons()) {
-      if (addon.wasRestored) {
-        yield addon;
-      }
-    }
-  },
-
-  /**
    * Iterates over all enabled bootstrapped add-ons, in any location.
    */
   * bootstrappedAddons() {
     for (let addon of this.enabledAddons()) {
       if (addon.bootstrapped) {
         yield addon;
       }
     }