Bug 1117858: Use JSONFile to persist the XPIStates database. draft
authorDave Townsend <dtownsend@oxymoronical.com>
Mon, 06 Mar 2017 14:43:21 -0800
changeset 496181 e1e5001ac6415d2174b62fc0a628732505a6cfde
parent 494215 fde4cd0aed6deccd27113ad6519ad31d5f27564a
child 496206 d675739e12dd06bb86fa1912f42375fe05b2a1a0
push id48549
push userdtownsend@mozilla.com
push dateThu, 09 Mar 2017 22:39:21 +0000
bugs1117858
milestone54.0a1
Bug 1117858: Use JSONFile to persist the XPIStates database. MozReview-Commit-ID: 2yTOKHZpJla
toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/internal/XPIProviderUtils.js
toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
toolkit/mozapps/extensions/test/xpcshell/test_isReady.js
--- a/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -71,24 +71,37 @@ const ZipWriter = Components.Constructor
 // We need some internal bits of AddonManager
 var AMscope = Cu.import("resource://gre/modules/AddonManager.jsm", {});
 var {AddonManager, AddonManagerPrivate} = AMscope;
 
 
 // Mock out AddonManager's reference to the AsyncShutdown module so we can shut
 // down AddonManager from the test
 var MockAsyncShutdown = {
-  hook: null,
+  hooks: [],
+  statuses: [],
   status: null,
+
+  async callHooks() {
+    for (let i = 0; i < this.hooks.length; i++) {
+      this.status = this.statuses[i];
+      await this.hooks[i]();
+    }
+
+    this.hooks = [];
+    this.statuses = [];
+  },
+
   profileBeforeChange: {
     addBlocker(name, blocker, options) {
-      MockAsyncShutdown.hook = blocker;
-      MockAsyncShutdown.status = options.fetchState;
+      MockAsyncShutdown.hooks.push(blocker);
+      MockAsyncShutdown.statuses.push(options ? options.fetchState : null);
     }
   },
+
   // We can use the real Barrier
   Barrier: AsyncShutdown.Barrier,
 };
 
 AMscope.AsyncShutdown = MockAsyncShutdown;
 
 
 /**
@@ -540,17 +553,17 @@ var AddonTestUtils = {
     return Promise.resolve();
   },
 
   promiseShutdownManager() {
     if (!this.addonIntegrationService)
       return Promise.resolve(false);
 
     Services.obs.notifyObservers(null, "quit-application-granted", null);
-    return MockAsyncShutdown.hook()
+    return MockAsyncShutdown.callHooks()
       .then(() => {
         this.emit("addon-manager-shutdown");
 
         this.addonIntegrationService = null;
 
         // Load the add-ons list as it was after application shutdown
         this.loadAddonsList();
 
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -12,17 +12,18 @@ const Cu = Components.utils;
 this.EXPORTED_SYMBOLS = ["XPIProvider"];
 
 const CONSTANTS = {};
 Cu.import("resource://gre/modules/addons/AddonConstants.jsm", CONSTANTS);
 const { ADDON_SIGNING, REQUIRE_SIGNING } = CONSTANTS
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/AddonManager.jsm");
+const { AsyncShutdown, AddonManager, AddonManagerPrivate } =
+  Cu.import("resource://gre/modules/AddonManager.jsm", {});
 Cu.import("resource://gre/modules/Preferences.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
                                   "resource://gre/modules/addons/AddonRepository.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "ChromeManifestParser",
                                   "resource://gre/modules/ChromeManifestParser.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
                                   "resource://gre/modules/LightweightThemeManager.jsm");
@@ -55,16 +56,18 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
                                   "resource://gre/modules/UpdateUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
                                   "resource://gre/modules/AppConstants.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "isAddonPartOfE10SRollout",
                                   "resource://gre/modules/addons/E10SAddonsRollout.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "LegacyExtensionsUtils",
                                   "resource://gre/modules/LegacyExtensionsUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+                                  "resource://gre/modules/JSONFile.jsm");
 
 XPCOMUtils.defineLazyServiceGetter(this, "Blocklist",
                                    "@mozilla.org/extensions/blocklist;1",
                                    Ci.nsIBlocklistService);
 XPCOMUtils.defineLazyServiceGetter(this,
                                    "ChromeRegistry",
                                    "@mozilla.org/chrome/chrome-registry;1",
                                    "nsIChromeRegistry");
@@ -153,16 +156,17 @@ const DIR_SYSTEM_ADDONS               = 
 const DIR_STAGE                       = "staged";
 const DIR_TRASH                       = "trash";
 
 const FILE_DATABASE                   = "extensions.json";
 const FILE_OLD_CACHE                  = "extensions.cache";
 const FILE_RDF_MANIFEST               = "install.rdf";
 const FILE_WEB_MANIFEST               = "manifest.json";
 const FILE_XPI_ADDONS_LIST            = "extensions.ini";
+const FILE_XPI_STATE                  = "xpiState.json";
 
 const KEY_PROFILEDIR                  = "ProfD";
 const KEY_ADDON_APP_DIR               = "XREAddonAppDir";
 const KEY_TEMPDIR                     = "TmpD";
 const KEY_APP_DISTRIBUTION            = "XREAppDist";
 const KEY_APP_FEATURES                = "XREAppFeat";
 
 const KEY_APP_PROFILE                 = "app-profile";
@@ -2117,44 +2121,44 @@ function recordAddonTelemetry(aAddon) {
   }
 }
 
 /**
  * The on-disk state of an individual XPI, created from an Object
  * as stored in the 'extensions.xpiState' pref.
  */
 function XPIState(saved) {
-  for (let [short, long] of XPIState.prototype.fields) {
-    if (short in saved) {
-      this[long] = saved[short];
+  for (let field of XPIState.prototype.fields) {
+    if (field in saved) {
+      this[field] = saved[field];
     }
   }
 }
 
 XPIState.prototype = {
-  fields: [["d", "descriptor"],
-           ["e", "enabled"],
-           ["v", "version"],
-           ["st", "scanTime"],
-           ["mt", "manifestTime"]],
+  fields: ["descriptor",
+           "enabled",
+           "version",
+           "scanTime",
+           "manifestTime"],
   /**
    * Return the last modified time, based on enabled/disabled
    */
   get mtime() {
     if (!this.enabled && ("manifestTime" in this) && this.manifestTime > this.scanTime) {
       return this.manifestTime;
     }
     return this.scanTime;
   },
 
   toJSON() {
     let json = {};
-    for (let [short, long] of XPIState.prototype.fields) {
-      if (long in this) {
-        json[short] = this[long];
+    for (let field of XPIState.prototype.fields) {
+      if (field in this) {
+        json[field] = this[field];
       }
     }
     return json;
   },
 
   /**
    * Update the last modified time for an add-on on disk.
    * @param aFile: nsIFile path of the add-on.
@@ -2209,17 +2213,16 @@ XPIState.prototype = {
 
     return changed;
   },
 
   /**
    * Update the XPIState to match an XPIDatabase entry; if 'enabled' is changed to true,
    * update the last-modified time. This should probably be made async, but for now we
    * don't want to maintain parallel sync and async versions of the scan.
-   * Caller is responsible for doing XPIStates.save() if necessary.
    * @param aDBAddon The DBAddonInternal for this add-on.
    * @param aUpdated The add-on was updated, so we must record new modified time.
    */
   syncWithDB(aDBAddon, aUpdated = false) {
     logger.debug("Updating XPIState for " + JSON.stringify(aDBAddon));
     // If the add-on changes from disabled to enabled, we should re-check the modified time.
     // If this is a newly found add-on, it won't have an 'enabled' field but we
     // did a full recursive scan in that case, so we don't need to do it again.
@@ -2230,228 +2233,211 @@ XPIState.prototype = {
     // XXX Eventually also copy bootstrap, etc.
     if (aUpdated || mustGetMod) {
       this.getModTime(new nsIFile(this.descriptor), aDBAddon.id);
       if (this.scanTime != aDBAddon.updateDate) {
         aDBAddon.updateDate = this.scanTime;
         XPIDatabase.saveChanges();
       }
     }
+
+    XPIStates.cache.saveSoon();
   },
 };
 
-// Constructor for an ES6 Map that knows how to convert itself into a
-// regular object for toJSON().
-function SerializableMap() {
-  let m = new Map();
-  m.toJSON = function() {
-    let out = {}
-    for (let [key, val] of m) {
-      out[key] = val;
-    }
-    return out;
-  };
-  return m;
-}
-
 /**
  * Keeps track of the state of XPI add-ons on the file system.
  */
 this.XPIStates = {
-  // Map(location name -> Map(add-on ID -> XPIState))
-  db: null,
-
-  get size() {
-    if (!this.db) {
-      return 0;
-    }
-    let count = 0;
-    for (let location of this.db.values()) {
-      count += location.size;
-    }
-    return count;
+  cache: new JSONFile({
+    path: OS.Path.join(OS.Constants.Path.profileDir, FILE_XPI_STATE),
+    saveDelayMS: 10000,
+    finalizeAt: AsyncShutdown.profileBeforeChange,
+    dataPostProcessor(data) {
+      for (let location of Object.values(data)) {
+        for (let id of Object.keys(location)) {
+          location[id] = new XPIState(location[id]);
+        }
+      }
+      return data;
+    }
+  }),
+
+  get hasAddons() {
+    for (let location of Object.values(this.cache.data)) {
+      if (Object.values(location).length > 0)
+        return true;
+    }
+
+    return false;
   },
 
   /**
    * Load extension state data from preferences.
    */
   loadExtensionState() {
-    let state = {};
-
     // Clear out old directory state cache.
     Preferences.reset(PREF_INSTALL_CACHE);
-
-    let cache = Preferences.get(PREF_XPI_STATE, "{}");
-    try {
-      state = JSON.parse(cache);
-    } catch (e) {
-      logger.warn("Error parsing extensions.xpiState ${state}: ${error}",
-          {state: cache, error: e});
-    }
-    logger.debug("Loaded add-on state from prefs: ${}", state);
-    return state;
+    Preferences.reset(PREF_XPI_STATE);
+
+    this.cache.ensureDataReady();
+
+    logger.debug(`Loaded add-on state: ${JSON.stringify(this.cache.data)}`);
+    return this.cache.data;
   },
 
   /**
    * Walk through all install locations, highest priority first,
    * comparing the on-disk state of extensions to what is stored in prefs.
    * @return true if anything has changed.
    */
   getInstallState() {
     let oldState = this.loadExtensionState();
     let changed = false;
-    this.db = new SerializableMap();
+    let db = {};
 
     for (let location of XPIProvider.installLocations) {
       // The list of add-on like file/directory names in the install location.
       let addons = location.getAddonLocations();
       // The results of scanning this location.
-      let foundAddons = new SerializableMap();
+      let foundAddons = {};
 
       // What our old state thinks should be in this location.
       let locState = {};
       if (location.name in oldState) {
         locState = oldState[location.name];
         // We've seen this location.
         delete oldState[location.name];
       }
 
       for (let [id, file] of addons) {
         if (!(id in locState)) {
           logger.debug("New add-on ${id} in ${location}", {id, location: location.name});
-          let xpiState = new XPIState({d: file.persistentDescriptor});
+          let xpiState = new XPIState({descriptor: file.persistentDescriptor});
           changed = xpiState.getModTime(file, id) || changed;
-          foundAddons.set(id, xpiState);
+          foundAddons[id] = xpiState;
         } else {
-          let xpiState = new XPIState(locState[id]);
+          let xpiState = locState[id];
           // We found this add-on in the file system
           delete locState[id];
 
-          changed = xpiState.getModTime(file, id) || changed;
+          let addonChanged = xpiState.getModTime(file, id);
 
           if (file.persistentDescriptor != xpiState.descriptor) {
             xpiState.descriptor = file.persistentDescriptor;
-            changed = true;
+            addonChanged = true;
           }
-          if (changed) {
+
+          if (addonChanged) {
             logger.debug("Changed add-on ${id} in ${location}", {id, location: location.name});
+            changed = true;
           } else {
             logger.debug("Existing add-on ${id} in ${location}", {id, location: location.name});
           }
-          foundAddons.set(id, xpiState);
+          foundAddons[id] = xpiState;
         }
         XPIProvider.setTelemetry(id, "location", location.name);
       }
 
       // Anything left behind in oldState was removed from the file system.
       if (Object.keys(locState).length) {
         changed = true;
       }
       // If we found anything, add this location to our database.
-      if (foundAddons.size != 0) {
-        this.db.set(location.name, foundAddons);
+      if (Object.keys(foundAddons)) {
+        db[location.name] = foundAddons;
       }
     }
 
     // If there's anything left in oldState, an install location that held add-ons
     // was removed from the browser configuration.
     if (Object.keys(oldState).length) {
       changed = true;
     }
 
-    logger.debug("getInstallState changed: ${rv}, state: ${state}",
-        {rv: changed, state: this.db});
+    logger.debug(`getInstallState changed: ${changed}, state: ${JSON.stringify(db)}`);
+    this.cache.data = db;
+    this.cache.saveSoon();
     return changed;
   },
 
   /**
    * Get the Map of XPI states for a particular location.
    * @param aLocation The name of the install location.
-   * @return Map (id -> XPIState) or null if there are no add-ons in the location.
+   * @return Object (id -> XPIState) or null if there are no add-ons in the location.
    */
   getLocation(aLocation) {
-    return this.db.get(aLocation);
+    return this.cache.data[aLocation];
   },
 
   /**
    * Get the XPI state for a specific add-on in a location.
    * If the state is not in our cache, return null.
    * @param aLocation The name of the location where the add-on is installed.
    * @param aId       The add-on ID
    * @return The XPIState entry for the add-on, or null.
    */
   getAddon(aLocation, aId) {
-    let location = this.db.get(aLocation);
+    let location = this.cache.data[aLocation];
     if (!location) {
       return null;
     }
-    return location.get(aId);
+    return location[aId];
   },
 
   /**
    * Find the highest priority location of an add-on by ID and return the
    * location and the XPIState.
    * @param aId   The add-on ID
    * @return [locationName, XPIState] if the add-on is found, [undefined, undefined]
    *         if the add-on is not found.
    */
   findAddon(aId) {
     // Fortunately the Map iterator returns in order of insertion, which is
     // also our highest -> lowest priority order.
-    for (let [name, location] of this.db) {
-      if (location.has(aId)) {
-        return [name, location.get(aId)];
+    for (let [name, location] of Object.entries(this.cache.data)) {
+      if (aId in location) {
+        return [name, location[aId]];
       }
     }
     return [undefined, undefined];
   },
 
   /**
    * Add a new XPIState for an add-on and synchronize it with the DBAddonInternal.
    * @param aAddon DBAddonInternal for the new add-on.
    */
   addAddon(aAddon) {
-    let location = this.db.get(aAddon.location);
-    if (!location) {
+    if (!(aAddon.location in this.cache.data)) {
       // First add-on in this location.
-      location = new SerializableMap();
-      this.db.set(aAddon.location, location);
+      this.cache.data[aAddon.location] = {};
     }
     logger.debug("XPIStates adding add-on ${id} in ${location}: ${descriptor}", aAddon);
-    let xpiState = new XPIState({d: aAddon.descriptor});
-    location.set(aAddon.id, xpiState);
+    let xpiState = new XPIState({descriptor: aAddon.descriptor});
+    this.cache.data[aAddon.location][aAddon.id] = xpiState;
     xpiState.syncWithDB(aAddon, true);
     XPIProvider.setTelemetry(aAddon.id, "location", aAddon.location);
   },
 
   /**
-   * Save the current state of installed add-ons.
-   * XXX this *totally* should be a .json file using DeferredSave...
-   */
-  save() {
-    let cache = JSON.stringify(this.db);
-    Services.prefs.setCharPref(PREF_XPI_STATE, cache);
-  },
-
-  /**
    * Remove the XPIState for an add-on and save the new state.
    * @param aLocation  The name of the add-on location.
    * @param aId        The ID of the add-on.
    */
   removeAddon(aLocation, aId) {
     logger.debug("Removing XPIState for " + aLocation + ":" + aId);
-    let location = this.db.get(aLocation);
+    let location = this.cache.data[aLocation];
     if (!location) {
       return;
     }
-    location.delete(aId);
-    if (location.size == 0) {
-      this.db.delete(aLocation);
-    }
-    this.save();
+    delete location[aId];
+    if (Object.keys(location).length == 0) {
+      delete this.cache.data[aLocation];
+    }
+    this.cache.saveSoon();
   },
 };
 
 this.XPIProvider = {
   get name() {
     return "XPIProvider";
   },
 
@@ -3736,45 +3722,43 @@ this.XPIProvider = {
     let telemetryCaptureTime = Cu.now();
     let installChanged = XPIStates.getInstallState();
     let telemetry = Services.telemetry;
     telemetry.getHistogramById("CHECK_ADDONS_MODIFIED_MS").add(Math.round(Cu.now() - telemetryCaptureTime));
     if (installChanged) {
       updateReasons.push("directoryState");
     }
 
-    let haveAnyAddons = (XPIStates.size > 0);
-
     // If the schema appears to have changed then we should update the database
     if (DB_SCHEMA != Preferences.get(PREF_DB_SCHEMA, 0)) {
       // If we don't have any add-ons, just update the pref, since we don't need to
       // write the database
-      if (!haveAnyAddons) {
+      if (!XPIStates.hasAddons) {
         logger.debug("Empty XPI database, setting schema version preference to " + DB_SCHEMA);
         Services.prefs.setIntPref(PREF_DB_SCHEMA, DB_SCHEMA);
       } else {
         updateReasons.push("schemaChanged");
       }
     }
 
     // If the database doesn't exist and there are add-ons installed then we
     // must update the database however if there are no add-ons then there is
     // no need to update the database.
     let dbFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_DATABASE], true);
-    if (!dbFile.exists() && haveAnyAddons) {
+    if (!dbFile.exists() && XPIStates.hasAddons) {
       updateReasons.push("needNewDatabase");
     }
 
     // XXX This will go away when we fold bootstrappedAddons into XPIStates.
     if (updateReasons.length == 0) {
       let bootstrapDescriptors = new Set(Object.keys(this.bootstrappedAddons)
         .map(b => this.bootstrappedAddons[b].descriptor));
 
-      for (let location of XPIStates.db.values()) {
-        for (let state of location.values()) {
+      for (let location of Object.values(XPIStates.cache.data)) {
+        for (let state of Object.values(location)) {
           bootstrapDescriptors.delete(state.descriptor);
         }
       }
 
       if (bootstrapDescriptors.size > 0) {
         logger.warn("Bootstrap state is invalid (missing add-ons: "
             + Array.from(bootstrapDescriptors).join(", ") + ")");
         updateReasons.push("missingBootstrapAddon");
@@ -3836,17 +3820,17 @@ this.XPIProvider = {
     } catch (e) {
       logger.error("Error during startup file checks", e);
     }
 
     // Check that the add-ons list still exists
     let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
                                        true);
     // the addons list file should exist if and only if we have add-ons installed
-    if (addonsList.exists() != haveAnyAddons) {
+    if (addonsList.exists() != XPIStates.hasAddons) {
       logger.debug("Add-ons list is invalid, rebuilding");
       XPIDatabase.writeAddonsList();
     }
 
     return false;
   },
 
   /**
@@ -4111,17 +4095,16 @@ this.XPIProvider = {
     addon.visible = true;
     addon.enabled = true;
     addon.active = true;
 
     addon = XPIDatabase.addAddonMetadata(addon, file.persistentDescriptor);
 
     XPIStates.addAddon(addon);
     XPIDatabase.saveChanges();
-    XPIStates.save();
 
     AddonManagerPrivate.callAddonListeners("onInstalling", addon.wrapper,
                                            false);
     XPIProvider.callBootstrapMethod(addon, file, "startup",
                                     BOOTSTRAP_REASONS.ADDON_INSTALL);
     AddonManagerPrivate.callInstallListeners("onExternalInstall",
                                              null, addon.wrapper,
                                              oldAddon ? oldAddon.wrapper : null,
@@ -5107,17 +5090,16 @@ this.XPIProvider = {
         }
       }
     }
 
     // Sync with XPIStates.
     let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
     if (xpiState) {
       xpiState.syncWithDB(aAddon);
-      XPIStates.save();
     } else {
       // There should always be an xpiState
       logger.warn("No XPIState for ${id} in ${location}", aAddon);
     }
 
     // Notify any other providers that a new theme has been enabled
     if (isTheme(aAddon.type) && !isDisabled)
       AddonManagerPrivate.notifyAddonChanged(aAddon.id, aAddon.type, needsRestart);
@@ -5179,17 +5161,17 @@ this.XPIProvider = {
 
       XPIDatabase.setAddonProperties(aAddon, {
         pendingUninstall: true
       });
       Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
       let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
       if (xpiState) {
         xpiState.enabled = false;
-        XPIStates.save();
+        XPIStates.cache.saveSoon();
       } else {
         logger.warn("Can't find XPI state while uninstalling ${id} from ${location}", aAddon);
       }
     }
 
     // If the add-on is not visible then there is no need to notify listeners.
     if (!aAddon.visible)
       return;
@@ -5834,17 +5816,16 @@ class AddonInstall {
           }
         } else {
           this.addon.active = (this.addon.visible && !this.addon.disabled);
           this.addon = XPIDatabase.addAddonMetadata(this.addon, file.persistentDescriptor);
           XPIStates.addAddon(this.addon);
           this.addon.installDate = this.addon.updateDate;
           XPIDatabase.saveChanges();
         }
-        XPIStates.save();
 
         let extraParams = {};
         if (this.existingAddon) {
           extraParams.oldVersion = this.existingAddon.version;
         }
 
         if (this.addon.bootstrap) {
           XPIProvider.callBootstrapMethod(this.addon, file, "install",
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -760,17 +760,17 @@ this.XPIDatabase = {
    *         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) {
+    if (!XPIStates.hasAddons) {
       // 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)
@@ -1909,17 +1909,17 @@ this.XPIDatabaseReconcile = {
       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);
+          let xpiState = states && states[id];
           if (xpiState) {
             // Here the add-on was present in the database and on disk
             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) {
@@ -1972,17 +1972,17 @@ this.XPIDatabaseReconcile = {
       // 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) {
+        for (let [id, xpiState] of Object.entries(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);
@@ -2169,17 +2169,16 @@ this.XPIDatabaseReconcile = {
 
     // Finally update XPIStates to match everything
     for (let [locationName, locationAddonMap] of currentAddons) {
       for (let [id, addon] of locationAddonMap) {
         let xpiState = XPIStates.getAddon(locationName, id);
         xpiState.syncWithDB(addon);
       }
     }
-    XPIStates.save();
 
     XPIProvider.persistBootstrappedAddons();
 
     // Clear out any cached migration data.
     XPIDatabase.migrateData = null;
     XPIDatabase.saveChanges();
 
     return true;
--- a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
@@ -89,18 +89,16 @@ var lastTimestamp = Date.now();
  * @param aChange True if we should notice the change, False if we shouldn't.
  */
 function checkChange(XS, aPath, aChange) {
   do_check_true(aPath.exists());
   lastTimestamp += 10000;
   do_print("Touching file " + aPath.path + " with " + lastTimestamp);
   aPath.lastModifiedTime = lastTimestamp;
   do_check_eq(XS.getInstallState(), aChange);
-  // Save the pref so we don't detect this change again
-  XS.save();
 }
 
 // Get a reference to the XPIState (loaded by startupManager) so we can unit test it.
 function getXS() {
   let XPI = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
   return XPI.XPIStates;
 }
 
@@ -120,20 +118,20 @@ add_task(function* detect_touches() {
   let XS = getXS();
 
   // Should be no changes detected here, because everything should start out up-to-date.
   do_check_false(XS.getInstallState());
 
   let states = XS.getLocation("app-profile");
 
   // State should correctly reflect enabled/disabled
-  do_check_true(states.get("packed-enabled@tests.mozilla.org").enabled);
-  do_check_false(states.get("packed-disabled@tests.mozilla.org").enabled);
-  do_check_true(states.get("unpacked-enabled@tests.mozilla.org").enabled);
-  do_check_false(states.get("unpacked-disabled@tests.mozilla.org").enabled);
+  do_check_true(states["packed-enabled@tests.mozilla.org"].enabled);
+  do_check_false(states["packed-disabled@tests.mozilla.org"].enabled);
+  do_check_true(states["unpacked-enabled@tests.mozilla.org"].enabled);
+  do_check_false(states["unpacked-disabled@tests.mozilla.org"].enabled);
 
   // Touch various files and make sure the change is detected.
 
   // We notice that a packed XPI is touched for an enabled add-on.
   let peFile = profileDir.clone();
   peFile.append("packed-enabled@tests.mozilla.org.xpi");
   checkChange(XS, peFile, true);
 
@@ -183,17 +181,17 @@ add_task(function* detect_touches() {
 add_task(function* uninstall_bootstrap() {
   let [pe, /* pd, ue, ud */] = yield promiseAddonsByIDs([
          "packed-enabled@tests.mozilla.org",
          "packed-disabled@tests.mozilla.org",
          "unpacked-enabled@tests.mozilla.org",
          "unpacked-disabled@tests.mozilla.org"
          ]);
   pe.uninstall();
-  let xpiState = Services.prefs.getCharPref("extensions.xpiState");
+  let xpiState = JSON.stringify(getXS().cache.data);
   do_check_false(xpiState.includes("\"packed-enabled@tests.mozilla.org\""));
 });
 
 /*
  * Installing a restartless add-on should immediately add it to XPIState
  */
 add_task(function* install_bootstrap() {
   let XS = getXS();
--- a/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js
@@ -24,19 +24,17 @@ add_task(function* () {
   equal(AddonManager.isReady, true, "isReady should be true after startup");
   equal(gotStartupEvent, true, "Should have seen onStartup event after startup");
   equal(gotShutdownEvent, false, "Should not have seen onShutdown event before shutdown");
 
   gotStartupEvent = false;
   gotShutdownEvent = false;
 
   do_print("Shutting down manager...");
-  let shutdownPromise = promiseShutdownManager();
-  equal(AddonManager.isReady, false, "isReady should be false when shutdown commences");
-  yield shutdownPromise;
+  yield promiseShutdownManager();
 
   equal(AddonManager.isReady, false, "isReady should be false after shutdown");
   equal(gotStartupEvent, false, "Should not have seen onStartup event after shutdown");
   equal(gotShutdownEvent, true, "Should have seen onShutdown event after shutdown");
 
   AddonManager.addManagerListener(listener);
   gotStartupEvent = false;
   gotShutdownEvent = false;