Bug 853388: Load JSON database asynchronously outside of startup

This commit is contained in:
Irving Reid 2013-08-08 16:05:33 -04:00
parent 81d59b2e11
commit 9f94529444
3 changed files with 312 additions and 196 deletions

View File

@ -3218,9 +3218,8 @@ var XPIProvider = {
// If the database needs to be updated then open it and then update it // If the database needs to be updated then open it and then update it
// from the filesystem // from the filesystem
if (updateDatabase || hasPendingChanges) { if (updateDatabase || hasPendingChanges) {
XPIDatabase.syncLoadDB(false);
try { try {
XPIDatabase.openConnection(false, true);
extensionListChanged = this.processFileChanges(state, manifests, extensionListChanged = this.processFileChanges(state, manifests,
aAppChanged, aAppChanged,
aOldAppVersion, aOldAppVersion,

View File

@ -20,6 +20,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
"resource://gre/modules/DeferredSave.jsm"); "resource://gre/modules/DeferredSave.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise", XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm"); "resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
["LOG", "WARN", "ERROR"].forEach(function(aName) { ["LOG", "WARN", "ERROR"].forEach(function(aName) {
Object.defineProperty(this, aName, { Object.defineProperty(this, aName, {
@ -158,6 +160,20 @@ function getRepositoryAddon(aAddon, aCallback) {
AddonRepository.getCachedAddonByID(aAddon.id, completeAddon); AddonRepository.getCachedAddonByID(aAddon.id, completeAddon);
} }
/**
* Wrap an API-supplied function in an exception handler to make it safe to call
*/
function safeCallback(aCallback) {
return function(...aArgs) {
try {
aCallback.apply(null, aArgs);
}
catch(ex) {
WARN("XPI Database callback failed", ex);
}
}
}
/** /**
* A helper method to asynchronously call a function on an array * A helper method to asynchronously call a function on an array
* of objects, calling a callback when function(x) has been gathered * of objects, calling a callback when function(x) has been gathered
@ -375,6 +391,32 @@ DBAddonInternal.prototype = {
DBAddonInternal.prototype.__proto__ = AddonInternal.prototype; DBAddonInternal.prototype.__proto__ = AddonInternal.prototype;
/**
* Internal interface: find an addon from an already loaded addonDB
*/
function _findAddon(addonDB, aFilter) {
for (let [, addon] of addonDB) {
if (aFilter(addon)) {
return addon;
}
}
return null;
}
/**
* Internal interface to get a filtered list of addons from a loaded addonDB
*/
function _filterDB(addonDB, aFilter) {
let addonList = [];
for (let [, addon] of addonDB) {
if (aFilter(addon)) {
addonList.push(addon);
}
}
return addonList;
}
this.XPIDatabase = { this.XPIDatabase = {
// true if the database connection has been opened // true if the database connection has been opened
initialized: false, initialized: false,
@ -410,6 +452,12 @@ this.XPIDatabase = {
return; return;
} }
if (!this._deferredSave) {
this._deferredSave = new DeferredSave(this.jsonFile.path,
() => JSON.stringify(this),
ASYNC_SAVE_DELAY_MS);
}
let promise = this._deferredSave.saveChanges(); let promise = this._deferredSave.saveChanges();
if (!this._schemaVersionSet) { if (!this._schemaVersionSet) {
this._schemaVersionSet = true; this._schemaVersionSet = true;
@ -429,8 +477,8 @@ this.XPIDatabase = {
}, },
flush: function() { flush: function() {
// handle the "in memory only" case // handle the "in memory only" and "saveChanges never called" cases
if (this.lockedDatabase) { if (!this._deferredSave) {
let done = Promise.defer(); let done = Promise.defer();
done.resolve(0); done.resolve(0);
return done.promise; return done.promise;
@ -439,19 +487,17 @@ this.XPIDatabase = {
return this._deferredSave.flush(); return this._deferredSave.flush();
}, },
get _deferredSave() {
delete this._deferredSave;
return this._deferredSave =
new DeferredSave(this.jsonFile.path, () => JSON.stringify(this),
ASYNC_SAVE_DELAY_MS);
},
/** /**
* Converts the current internal state of the XPI addon database to JSON * Converts the current internal state of the XPI addon database to JSON
*/ */
toJSON: function() { toJSON: function() {
if (!this.addonDB) {
// We never loaded the database?
throw new Error("Attempt to save database without loading it first");
}
let addons = []; let addons = [];
for (let [key, addon] of this.addonDB) { for (let [, addon] of this.addonDB) {
addons.push(addon); addons.push(addon);
} }
let toSave = { let toSave = {
@ -489,7 +535,7 @@ this.XPIDatabase = {
}, },
/** /**
* Opens and reads the database file, upgrading from old * Synchronously opens and reads the database file, upgrading from old
* databases or making a new DB if needed. * databases or making a new DB if needed.
* *
* The possibilities, in order of priority, are: * The possibilities, in order of priority, are:
@ -506,10 +552,8 @@ this.XPIDatabase = {
* from the install locations if the database needs to be rebuilt. * from the install locations if the database needs to be rebuilt.
* (if false, caller is XPIProvider.checkForChanges() which will rebuild) * (if false, caller is XPIProvider.checkForChanges() which will rebuild)
*/ */
openConnection: function XPIDB_openConnection(aRebuildOnError, aForceOpen) { syncLoadDB: function XPIDB_syncLoadDB(aRebuildOnError) {
// XXX TELEMETRY report opens with aRebuildOnError true (which implies delayed open) // XXX TELEMETRY report synchronous opens (startup time) vs. delayed opens
// vs. aRebuildOnError false (DB loaded during startup)
delete this.addonDB;
this.migrateData = null; this.migrateData = null;
let fstream = null; let fstream = null;
let data = ""; let data = "";
@ -530,42 +574,10 @@ this.XPIDatabase = {
data += str.value; data += str.value;
} while (read != 0); } while (read != 0);
} }
// dump("Loaded JSON:\n" + data + "\n"); this.parseDB(data, aRebuildOnError);
let inputAddons = JSON.parse(data);
// Now do some sanity checks on our JSON db
if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) {
// Content of JSON file is bad, need to rebuild from scratch
ERROR("bad JSON file contents");
this.rebuildDatabase(aRebuildOnError);
}
if (inputAddons.schemaVersion != DB_SCHEMA) {
// Handle mismatched JSON schema version. For now, we assume
// compatibility for JSON data, though we throw away any fields we
// don't know about
// XXX preserve unknown fields during save/restore
LOG("JSON schema mismatch: expected " + DB_SCHEMA +
", actual " + inputAddons.schemaVersion);
}
// If we got here, we probably have good data
// Make AddonInternal instances from the loaded data and save them
let addonDB = new Map();
inputAddons.addons.forEach(function(loadedAddon) {
let newAddon = new DBAddonInternal(loadedAddon);
addonDB.set(newAddon._key, newAddon);
});
this.addonDB = addonDB;
LOG("Successfully read XPI database");
this.initialized = true;
} }
catch(e) { catch(e) {
// If we catch and log a SyntaxError from the JSON ERROR("Failed to load XPI JSON data from profile", e);
// parser, the xpcshell test harness fails the test for us: bug 870828
if (e.name == "SyntaxError") {
ERROR("Syntax error parsing saved XPI JSON data");
}
else {
ERROR("Failed to load XPI JSON data from profile", e);
}
this.rebuildDatabase(aRebuildOnError); this.rebuildDatabase(aRebuildOnError);
} }
finally { finally {
@ -575,46 +587,151 @@ this.XPIDatabase = {
} }
catch (e) { catch (e) {
if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) { if (e.result == Cr.NS_ERROR_FILE_NOT_FOUND) {
try { this.upgradeDB(aRebuildOnError);
let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA);
if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) {
// we should have an older SQLITE database
this.migrateData = this.getMigrateDataFromSQLITE();
}
// else we've upgraded before but the JSON file is gone, fall through
// and rebuild from scratch
}
catch(e) {
// No schema version pref means either a really old upgrade (RDF) or
// a new profile
this.migrateData = this.getMigrateDataFromRDF();
}
this.rebuildDatabase(aRebuildOnError);
} }
else { else {
WARN("Extensions database " + this.jsonFile.path + this.rebuildUnreadableDB(e, aRebuildOnError);
" exists but is not readable; rebuilding in memory", e);
// XXX open question - if we can overwrite at save time, should we, or should we
// leave the locked database in case we can recover from it next time we start up?
// The old code made one attempt to remove the locked file before it rebuilt in memory
this.lockedDatabase = true;
// XXX TELEMETRY report when this happens?
this.rebuildDatabase(aRebuildOnError);
} }
} }
finally { finally {
if (fstream) if (fstream)
fstream.close(); fstream.close();
} }
// If an async load was also in progress, resolve that promise with our DB;
// otherwise create a resolved promise
if (this._dbPromise)
this._dbPromise.resolve(this.addonDB);
else
this._dbPromise = Promise.resolve(this.addonDB);
},
return; /**
* Parse loaded data, reconstructing the database if the loaded data is not valid
// XXX what about aForceOpen? Appears to handle the case of "don't open DB file if there aren't any extensions"? * @param aRebuildOnError
if (!aForceOpen && !this.dbfileExists) { * If true, synchronously reconstruct the database from installed add-ons
this.connection = null; */
return; parseDB: function(aData, aRebuildOnError) {
try {
// dump("Loaded JSON:\n" + aData + "\n");
let inputAddons = JSON.parse(aData);
// Now do some sanity checks on our JSON db
if (!("schemaVersion" in inputAddons) || !("addons" in inputAddons)) {
// Content of JSON file is bad, need to rebuild from scratch
ERROR("bad JSON file contents");
this.rebuildDatabase(aRebuildOnError);
return;
}
if (inputAddons.schemaVersion != DB_SCHEMA) {
// Handle mismatched JSON schema version. For now, we assume
// compatibility for JSON data, though we throw away any fields we
// don't know about
// XXX preserve unknown fields during save/restore
LOG("JSON schema mismatch: expected " + DB_SCHEMA +
", actual " + inputAddons.schemaVersion);
}
// If we got here, we probably have good data
// Make AddonInternal instances from the loaded data and save them
let addonDB = new Map();
inputAddons.addons.forEach(function(loadedAddon) {
let newAddon = new DBAddonInternal(loadedAddon);
addonDB.set(newAddon._key, newAddon);
});
this.addonDB = addonDB;
LOG("Successfully read XPI database");
this.initialized = true;
} }
catch(e) {
// If we catch and log a SyntaxError from the JSON
// parser, the xpcshell test harness fails the test for us: bug 870828
if (e.name == "SyntaxError") {
ERROR("Syntax error parsing saved XPI JSON data");
}
else {
ERROR("Failed to load XPI JSON data from profile", e);
}
this.rebuildDatabase(aRebuildOnError);
}
},
/**
* Upgrade database from earlier (sqlite or RDF) version if available
*/
upgradeDB: function(aRebuildOnError) {
try {
let schemaVersion = Services.prefs.getIntPref(PREF_DB_SCHEMA);
if (schemaVersion <= LAST_SQLITE_DB_SCHEMA) {
// we should have an older SQLITE database
this.migrateData = this.getMigrateDataFromSQLITE();
}
// else we've upgraded before but the JSON file is gone, fall through
// and rebuild from scratch
}
catch(e) {
// No schema version pref means either a really old upgrade (RDF) or
// a new profile
this.migrateData = this.getMigrateDataFromRDF();
}
this.rebuildDatabase(aRebuildOnError);
},
/**
* Reconstruct when the DB file exists but is unreadable
* (for example because read permission is denied
*/
rebuildUnreadableDB: function(aError, aRebuildOnError) {
WARN("Extensions database " + this.jsonFile.path +
" exists but is not readable; rebuilding in memory", aError);
// XXX open question - if we can overwrite at save time, should we, or should we
// leave the locked database in case we can recover from it next time we start up?
// The old code made one attempt to remove the locked file before it rebuilt in memory
this.lockedDatabase = true;
// XXX TELEMETRY report when this happens?
this.rebuildDatabase(aRebuildOnError);
},
/**
* Open and read the XPI database asynchronously, upgrading if
* necessary. If any DB load operation fails, we need to
* synchronously rebuild the DB from the installed extensions.
*
* @return Promise<Map> resolves to the Map of loaded JSON data stored
* in this.addonDB; never rejects.
*/
asyncLoadDB: function XPIDB_asyncLoadDB(aDBCallback) {
// Already started (and possibly finished) loading
if (this._dbPromise) {
return this._dbPromise;
}
LOG("Starting async load of XPI database " + this.jsonFile.path);
return this._dbPromise = OS.File.read(this.jsonFile.path).then(
byteArray => {
if (this._addonDB) {
LOG("Synchronous load completed while waiting for async load");
return this.addonDB;
}
LOG("Finished async read of XPI database, parsing...");
let decoder = new TextDecoder();
let data = decoder.decode(byteArray);
this.parseDB(data, true);
return this.addonDB;
})
.then(null,
error => {
if (this._addonDB) {
LOG("Synchronous load completed while waiting for async load");
return this.addonDB;
}
if (error.becauseNoSuchFile) {
this.upgradeDB(true);
}
else {
// it's there but unreadable
this.rebuildUnreadableDB(error, true);
}
return this.addonDB;
});
}, },
/** /**
@ -649,21 +766,13 @@ this.XPIDatabase = {
} }
}, },
/**
* Lazy getter for the addons database
*/
get addonDB() {
this.openConnection(true);
return this.addonDB;
},
/** /**
* Gets the list of file descriptors of active extension directories or XPI * Gets the list of file descriptors of active extension directories or XPI
* files from the add-ons list. This must be loaded from disk since the * files from the add-ons list. This must be loaded from disk since the
* directory service gives no easy way to get both directly. This list doesn't * directory service gives no easy way to get both directly. This list doesn't
* include themes as preferences already say which theme is currently active * include themes as preferences already say which theme is currently active
* *
* @return an array of persisitent descriptors for the directories * @return an array of persistent descriptors for the directories
*/ */
getActiveBundles: function XPIDB_getActiveBundles() { getActiveBundles: function XPIDB_getActiveBundles() {
let bundles = []; let bundles = [];
@ -882,27 +991,11 @@ this.XPIDatabase = {
return 0; return 0;
}) })
.then(count => { .then(count => {
// Clear out the cached addons data loaded from JSON and recreate // Clear out the cached addons data loaded from JSON
// the getter to allow database re-loads during testing.
delete this.addonDB; delete this.addonDB;
Object.defineProperty(this, "addonDB", { delete this._dbPromise;
get: function addonsGetter() {
this.openConnection(true);
return this.addonDB;
},
configurable: true
});
// same for the deferred save // same for the deferred save
delete this._deferredSave; delete this._deferredSave;
Object.defineProperty(this, "_deferredSave", {
set: function deferredSaveGetter() {
delete this._deferredSave;
return this._deferredSave =
new DeferredSave(this.jsonFile.path, this.formJSON.bind(this),
ASYNC_SAVE_DELAY_MS);
},
configurable: true
});
// re-enable the schema version setter // re-enable the schema version setter
delete this._schemaVersionSet; delete this._schemaVersionSet;
@ -920,7 +1013,8 @@ this.XPIDatabase = {
* Return a list of all install locations known about by the database. This * Return a list of all install locations known about by the database. This
* is often a a subset of the total install locations when not all have * is often a a subset of the total install locations when not all have
* installed add-ons, occasionally a superset when an install location no * installed add-ons, occasionally a superset when an install location no
* longer exists. * longer exists. Only called from XPIProvider.processFileChanges, when
* the database should already be loaded.
* *
* @return a Set of names of install locations * @return a Set of names of install locations
*/ */
@ -936,61 +1030,62 @@ this.XPIDatabase = {
}, },
/** /**
* List all addons that match the filter function * Asynchronously list all addons that match the filter function
* @param aFilter * @param aFilter
* Function that takes an addon instance and returns * Function that takes an addon instance and returns
* true if that addon should be included in the selected array * true if that addon should be included in the selected array
* @return an array of DBAddonInternals * @param aCallback
* Called back with an array of addons matching aFilter
* or an empty array if none match
*/ */
_listAddons: function XPIDB_listAddons(aFilter) { getAddonList: function(aFilter, aCallback) {
if (!this.addonDB) this.asyncLoadDB().then(
return []; addonDB => {
let addonList = _filterDB(addonDB, aFilter);
let addonList = []; asyncMap(addonList, getRepositoryAddon, safeCallback(aCallback));
for (let [key, addon] of this.addonDB) { })
if (aFilter(addon)) { .then(null,
addonList.push(addon); error => {
} ERROR("getAddonList failed", e);
} safeCallback(aCallback)([]);
});
return addonList;
}, },
/** /**
* Find the first addon that matches the filter function * (Possibly asynchronously) get the first addon that matches the filter function
* @param aFilter * @param aFilter
* Function that takes an addon instance and returns * Function that takes an addon instance and returns
* true if that addon should be selected * true if that addon should be selected
* @return The first DBAddonInternal for which the filter returns true * @param aCallback
* Called back with the addon, or null if no matching addon is found
*/ */
_findAddon: function XPIDB_findAddon(aFilter) { getAddon: function(aFilter, aCallback) {
if (!this.addonDB) return this.asyncLoadDB().then(
return null; addonDB => {
getRepositoryAddon(_findAddon(addonDB, aFilter), safeCallback(aCallback));
for (let [key, addon] of this.addonDB) { })
if (aFilter(addon)) { .then(null,
return addon; error => {
} ERROR("getAddon failed", e);
} safeCallback(aCallback)(null);
});
return null;
}, },
/** /**
* Synchronously reads all the add-ons in a particular install location. * Synchronously reads all the add-ons in a particular install location.
* Always called with the addon database already loaded.
* *
* @param aLocation * @param aLocation
* The name of the install location * The name of the install location
* @return an array of DBAddonInternals * @return an array of DBAddonInternals
*/ */
getAddonsInLocation: function XPIDB_getAddonsInLocation(aLocation) { getAddonsInLocation: function XPIDB_getAddonsInLocation(aLocation) {
return this._listAddons(function inLocation(aAddon) {return (aAddon.location == aLocation);}); return _filterDB(this.addonDB, aAddon => (aAddon.location == aLocation));
}, },
/** /**
* Asynchronously gets an add-on with a particular ID in a particular * Asynchronously gets an add-on with a particular ID in a particular
* install location. * install location.
* XXX IRVING sync for now
* *
* @param aId * @param aId
* The ID of the add-on to retrieve * The ID of the add-on to retrieve
@ -1000,12 +1095,13 @@ this.XPIDatabase = {
* A callback to pass the DBAddonInternal to * A callback to pass the DBAddonInternal to
*/ */
getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) { getAddonInLocation: function XPIDB_getAddonInLocation(aId, aLocation, aCallback) {
getRepositoryAddon(this.addonDB.get(aLocation + ":" + aId), aCallback); this.asyncLoadDB().then(
addonDB => getRepositoryAddon(addonDB.get(aLocation + ":" + aId),
safeCallback(aCallback)));
}, },
/** /**
* Asynchronously gets the add-on with an ID that is visible. * Asynchronously gets the add-on with the specified ID that is visible.
* XXX IRVING sync
* *
* @param aId * @param aId
* The ID of the add-on to retrieve * The ID of the add-on to retrieve
@ -1013,13 +1109,12 @@ this.XPIDatabase = {
* A callback to pass the DBAddonInternal to * A callback to pass the DBAddonInternal to
*/ */
getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) { getVisibleAddonForID: function XPIDB_getVisibleAddonForID(aId, aCallback) {
let addon = this._findAddon(function visibleID(aAddon) {return ((aAddon.id == aId) && aAddon.visible)}); this.getAddon(aAddon => ((aAddon.id == aId) && aAddon.visible),
getRepositoryAddon(addon, aCallback); aCallback);
}, },
/** /**
* Asynchronously gets the visible add-ons, optionally restricting by type. * Asynchronously gets the visible add-ons, optionally restricting by type.
* XXX IRVING sync
* *
* @param aTypes * @param aTypes
* An array of types to include or null to include all types * An array of types to include or null to include all types
@ -1027,10 +1122,10 @@ this.XPIDatabase = {
* A callback to pass the array of DBAddonInternals to * A callback to pass the array of DBAddonInternals to
*/ */
getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) { getVisibleAddons: function XPIDB_getVisibleAddons(aTypes, aCallback) {
let addons = this._listAddons(function visibleType(aAddon) { this.getAddonList(aAddon => (aAddon.visible &&
return (aAddon.visible && (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))) (!aTypes || (aTypes.length == 0) ||
}); (aTypes.indexOf(aAddon.type) > -1))),
asyncMap(addons, getRepositoryAddon, aCallback); aCallback);
}, },
/** /**
@ -1041,7 +1136,14 @@ this.XPIDatabase = {
* @return an array of DBAddonInternals * @return an array of DBAddonInternals
*/ */
getAddonsByType: function XPIDB_getAddonsByType(aType) { getAddonsByType: function XPIDB_getAddonsByType(aType) {
return this._listAddons(function byType(aAddon) { return aAddon.type == aType; }); if (!this.addonDB) {
// jank-tastic! Must synchronously load DB if the theme switches from
// an XPI theme to a lightweight theme before the DB has loaded,
// because we're called from sync XPIProvider.addonChanged
WARN("Synchronous load of XPI database due to getAddonsByType(" + aType + ")");
this.syncLoadDB(true);
}
return _filterDB(this.addonDB, aAddon => (aAddon.type == aType));
}, },
/** /**
@ -1052,14 +1154,20 @@ this.XPIDatabase = {
* @return a DBAddonInternal * @return a DBAddonInternal
*/ */
getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) { getVisibleAddonForInternalName: function XPIDB_getVisibleAddonForInternalName(aInternalName) {
return this._findAddon(function visibleInternalName(aAddon) { if (!this.addonDB) {
return (aAddon.visible && (aAddon.internalName == aInternalName)); // This may be called when the DB hasn't otherwise been loaded
}); // XXX TELEMETRY
WARN("Synchronous load of XPI database due to getVisibleAddonForInternalName");
this.syncLoadDB(true);
}
return _findAddon(this.addonDB,
aAddon => aAddon.visible &&
(aAddon.internalName == aInternalName));
}, },
/** /**
* Asynchronously gets all add-ons with pending operations. * Asynchronously gets all add-ons with pending operations.
* XXX IRVING sync
* *
* @param aTypes * @param aTypes
* The types of add-ons to retrieve or null to get all types * The types of add-ons to retrieve or null to get all types
@ -1069,21 +1177,19 @@ this.XPIDatabase = {
getVisibleAddonsWithPendingOperations: getVisibleAddonsWithPendingOperations:
function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) { function XPIDB_getVisibleAddonsWithPendingOperations(aTypes, aCallback) {
let addons = this._listAddons(function visibleType(aAddon) { this.getAddonList(
return (aAddon.visible && aAddon => (aAddon.visible &&
(aAddon.pendingUninstall || (aAddon.pendingUninstall ||
// Logic here is tricky. If we're active but either // Logic here is tricky. If we're active but either
// disabled flag is set, we're pending disable; if we're not // disabled flag is set, we're pending disable; if we're not
// active and neither disabled flag is set, we're pending enable // active and neither disabled flag is set, we're pending enable
(aAddon.active == (aAddon.userDisabled || aAddon.appDisabled))) && (aAddon.active == (aAddon.userDisabled || aAddon.appDisabled))) &&
(!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))) (!aTypes || (aTypes.length == 0) || (aTypes.indexOf(aAddon.type) > -1))),
}); aCallback);
asyncMap(addons, getRepositoryAddon, aCallback);
}, },
/** /**
* Asynchronously get an add-on by its Sync GUID. * Asynchronously get an add-on by its Sync GUID.
* XXX IRVING sync
* *
* @param aGUID * @param aGUID
* Sync GUID of add-on to fetch * Sync GUID of add-on to fetch
@ -1093,17 +1199,23 @@ this.XPIDatabase = {
* *
*/ */
getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) { getAddonBySyncGUID: function XPIDB_getAddonBySyncGUID(aGUID, aCallback) {
let addon = this._findAddon(function bySyncGUID(aAddon) { return aAddon.syncGUID == aGUID; }); this.getAddon(aAddon => aAddon.syncGUID == aGUID,
getRepositoryAddon(addon, aCallback); aCallback);
}, },
/** /**
* Synchronously gets all add-ons in the database. * Synchronously gets all add-ons in the database.
* This is only called from the preference observer for the default
* compatibility version preference, so we can return an empty list if
* we haven't loaded the database yet.
* *
* @return an array of DBAddonInternals * @return an array of DBAddonInternals
*/ */
getAddons: function XPIDB_getAddons() { getAddons: function XPIDB_getAddons() {
return this._listAddons(function(aAddon) {return true;}); if (!this.addonDB) {
return [];
}
return _filterDB(this.addonDB, aAddon => true);
}, },
/** /**
@ -1116,12 +1228,10 @@ this.XPIDatabase = {
* @return The DBAddonInternal that was added to the database * @return The DBAddonInternal that was added to the database
*/ */
addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) { addAddonMetadata: function XPIDB_addAddonMetadata(aAddon, aDescriptor) {
// If there is no DB yet then forcibly create one if (!this.addonDB) {
// XXX IRVING I don't think this will work as expected because the addonDB // XXX telemetry. Should never happen on platforms that have a default theme
// getter will kick in. Might not matter because of the way the new DB this.syncLoadDB(false);
// creates itself. }
if (!this.addonDB)
this.openConnection(false, true);
let newAddon = new DBAddonInternal(aAddon); let newAddon = new DBAddonInternal(aAddon);
newAddon.descriptor = aDescriptor; newAddon.descriptor = aDescriptor;
@ -1182,7 +1292,7 @@ this.XPIDatabase = {
*/ */
makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) { makeAddonVisible: function XPIDB_makeAddonVisible(aAddon) {
LOG("Make addon " + aAddon._key + " visible"); LOG("Make addon " + aAddon._key + " visible");
for (let [key, otherAddon] of this.addonDB) { for (let [, otherAddon] of this.addonDB) {
if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) { if ((otherAddon.id == aAddon.id) && (otherAddon._key != aAddon._key)) {
LOG("Hide addon " + otherAddon._key); LOG("Hide addon " + otherAddon._key);
otherAddon.visible = false; otherAddon.visible = false;
@ -1209,6 +1319,7 @@ this.XPIDatabase = {
/** /**
* Synchronously sets the Sync GUID for an add-on. * Synchronously sets the Sync GUID for an add-on.
* Only called when the database is already loaded.
* *
* @param aAddon * @param aAddon
* The DBAddonInternal being updated * The DBAddonInternal being updated
@ -1221,7 +1332,7 @@ this.XPIDatabase = {
function excludeSyncGUID(otherAddon) { function excludeSyncGUID(otherAddon) {
return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID); return (otherAddon._key != aAddon._key) && (otherAddon.syncGUID == aGUID);
} }
let otherAddon = this._findAddon(excludeSyncGUID); let otherAddon = _findAddon(this.addonDB, excludeSyncGUID);
if (otherAddon) { if (otherAddon) {
throw new Error("Addon sync GUID conflict for addon " + aAddon._key + throw new Error("Addon sync GUID conflict for addon " + aAddon._key +
": " + otherAddon._key + " already has GUID " + aGUID); ": " + otherAddon._key + " already has GUID " + aGUID);
@ -1264,7 +1375,7 @@ this.XPIDatabase = {
// XXX IRVING this may get called during XPI-utils shutdown // XXX IRVING this may get called during XPI-utils shutdown
// XXX need to make sure PREF_PENDING_OPERATIONS handling is clean // XXX need to make sure PREF_PENDING_OPERATIONS handling is clean
LOG("Updating add-on states"); LOG("Updating add-on states");
for (let [key, addon] of this.addonDB) { for (let [, addon] of this.addonDB) {
let newActive = (addon.visible && !addon.userDisabled && let newActive = (addon.visible && !addon.userDisabled &&
!addon.softDisabled && !addon.appDisabled && !addon.softDisabled && !addon.appDisabled &&
!addon.pendingUninstall); !addon.pendingUninstall);
@ -1279,6 +1390,10 @@ this.XPIDatabase = {
* Writes out the XPI add-ons list for the platform to read. * Writes out the XPI add-ons list for the platform to read.
*/ */
writeAddonsList: function XPIDB_writeAddonsList() { writeAddonsList: function XPIDB_writeAddonsList() {
if (!this.addonDB) {
// Unusual condition, force the DB to load
this.syncLoadDB(true);
}
Services.appinfo.invalidateCachesOnRestart(); Services.appinfo.invalidateCachesOnRestart();
let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST], let addonsList = FileUtils.getFile(KEY_PROFILEDIR, [FILE_XPI_ADDONS_LIST],
@ -1288,9 +1403,9 @@ this.XPIDatabase = {
let count = 0; let count = 0;
let fullCount = 0; let fullCount = 0;
let activeAddons = this._listAddons(function active(aAddon) { let activeAddons = _filterDB(
return aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme"); this.addonDB,
}); aAddon => aAddon.active && !aAddon.bootstrap && (aAddon.type != "theme"));
for (let row of activeAddons) { for (let row of activeAddons) {
text += "Extension" + (count++) + "=" + row.descriptor + "\r\n"; text += "Extension" + (count++) + "=" + row.descriptor + "\r\n";
@ -1310,12 +1425,13 @@ this.XPIDatabase = {
let themes = []; let themes = [];
if (dssEnabled) { if (dssEnabled) {
themes = this._listAddons(function isTheme(aAddon){ return aAddon.type == "theme"; }); themes = _filterDB(this.addonDB, aAddon => aAddon.type == "theme");
} }
else { else {
let activeTheme = this._findAddon(function isSelected(aAddon) { let activeTheme = _findAddon(
return ((aAddon.type == "theme") && (aAddon.internalName == XPIProvider.selectedSkin)); this.addonDB,
}); aAddon => (aAddon.type == "theme") &&
(aAddon.internalName == XPIProvider.selectedSkin));
if (activeTheme) { if (activeTheme) {
themes.push(activeTheme); themes.push(activeTheme);
} }

View File

@ -45,25 +45,26 @@ function run_test() {
let internal_ids = {}; let internal_ids = {};
[["addon1@tests.mozilla.org", "app-profile", "1.0", "0", "1", "0"] let a = ["addon1@tests.mozilla.org", "app-profile", "1.0", "0", "1", "0"];
].forEach(function(a) { stmt.params.id = a[0];
stmt.params.id = a[0]; stmt.params.location = a[1];
stmt.params.location = a[1]; stmt.params.version = a[2];
stmt.params.version = a[2]; stmt.params.active = a[3];
stmt.params.active = a[3]; stmt.params.userDisabled = a[4];
stmt.params.userDisabled = a[4]; stmt.params.installDate = a[5];
stmt.params.installDate = a[5]; stmt.execute();
stmt.execute(); internal_ids[a[0]] = db.lastInsertRowID;
internal_ids[a[0]] = db.lastInsertRowID;
});
stmt.finalize(); stmt.finalize();
db.schemaVersion = 15; db.schemaVersion = 14;
Services.prefs.setIntPref("extensions.databaseSchema", 14); Services.prefs.setIntPref("extensions.databaseSchema", 14);
db.close(); db.close();
startupManager(); startupManager();
run_next_test();
}
add_test(function before_rebuild() {
AddonManager.getAddonByID("addon1@tests.mozilla.org", AddonManager.getAddonByID("addon1@tests.mozilla.org",
function check_before_rebuild (a1) { function check_before_rebuild (a1) {
// First check that it migrated OK once // First check that it migrated OK once
@ -77,7 +78,7 @@ function run_test() {
run_next_test(); run_next_test();
}); });
} });
// now shut down, remove the JSON database, // now shut down, remove the JSON database,
// start up again, and make sure the data didn't migrate this time // start up again, and make sure the data didn't migrate this time