--- 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",