зеркало из https://github.com/mozilla/gecko-dev.git
Bug 911621 - Ensure that XPI and AddonRepository JSON is completely written before shutdown. r=Unfocused
This commit is contained in:
Родитель
916c266495
Коммит
13d84531de
|
@ -40,12 +40,16 @@ var PREF_EM_CHECK_COMPATIBILITY;
|
|||
|
||||
const TOOLKIT_ID = "toolkit@mozilla.org";
|
||||
|
||||
const SHUTDOWN_EVENT = "profile-before-change";
|
||||
|
||||
const VALID_TYPES_REGEXP = /^[\w\-]+$/;
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/AsyncShutdown.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository",
|
||||
"resource://gre/modules/AddonRepository.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "CertUtils", function certUtilsLazyGetter() {
|
||||
let certUtils = {};
|
||||
|
@ -453,8 +457,6 @@ var AddonManagerInternal = {
|
|||
|
||||
this.recordTimestamp("AMI_startup_begin");
|
||||
|
||||
Services.obs.addObserver(this, SHUTDOWN_EVENT, false);
|
||||
|
||||
let appChanged = undefined;
|
||||
|
||||
let oldAppVersion = null;
|
||||
|
@ -547,6 +549,10 @@ var AddonManagerInternal = {
|
|||
}
|
||||
}
|
||||
|
||||
// Register our shutdown handler with the AsyncShutdown manager
|
||||
AsyncShutdown.profileBeforeChange.addBlocker("AddonManager: shutting down providers",
|
||||
this.shutdown.bind(this));
|
||||
|
||||
// Once we start calling providers we must allow all normal methods to work.
|
||||
gStarted = true;
|
||||
|
||||
|
@ -678,13 +684,58 @@ var AddonManagerInternal = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Calls a method on all registered providers, if the provider implements
|
||||
* the method. The called method is expected to return a promise, and
|
||||
* callProvidersAsync returns a promise that resolves when every provider
|
||||
* method has either resolved or rejected. Rejection reasons are logged
|
||||
* but otherwise ignored. Return values are ignored. Any parameters after the
|
||||
* method parameter are passed to the provider's method.
|
||||
*
|
||||
* @param aMethod
|
||||
* The method name to call
|
||||
* @see callProvider
|
||||
*/
|
||||
callProvidersAsync: function AMI_callProviders(aMethod, ...aArgs) {
|
||||
if (!aMethod || typeof aMethod != "string")
|
||||
throw Components.Exception("aMethod must be a non-empty string",
|
||||
Cr.NS_ERROR_INVALID_ARG);
|
||||
|
||||
let allProviders = [];
|
||||
|
||||
let providers = this.providers.slice(0);
|
||||
for (let provider of providers) {
|
||||
try {
|
||||
if (aMethod in provider) {
|
||||
// Resolve a new promise with the result of the method, to handle both
|
||||
// methods that return values (or nothing) and methods that return promises.
|
||||
let providerResult = provider[aMethod].apply(provider, aArgs);
|
||||
let nextPromise = Promise.resolve(providerResult);
|
||||
// Log and swallow the errors from methods that do return promises.
|
||||
nextPromise = nextPromise.then(
|
||||
null,
|
||||
e => ERROR("Exception calling provider " + aMethod, e));
|
||||
allProviders.push(nextPromise);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
ERROR("Exception calling provider " + aMethod, e);
|
||||
}
|
||||
}
|
||||
// Because we use promise.then to catch and log all errors above, Promise.all()
|
||||
// will never exit early because of a rejection.
|
||||
return Promise.all(allProviders);
|
||||
},
|
||||
|
||||
/**
|
||||
* Shuts down the addon manager and all registered providers, this must clean
|
||||
* up everything in order for automated tests to fake restarts.
|
||||
* @return Promise{null} that resolves when all providers and dependent modules
|
||||
* have finished shutting down
|
||||
*/
|
||||
shutdown: function AMI_shutdown() {
|
||||
LOG("shutdown");
|
||||
Services.obs.removeObserver(this, SHUTDOWN_EVENT);
|
||||
// Clean up listeners
|
||||
Services.prefs.removeObserver(PREF_EM_CHECK_COMPATIBILITY, this);
|
||||
Services.prefs.removeObserver(PREF_EM_STRICT_COMPATIBILITY, this);
|
||||
Services.prefs.removeObserver(PREF_EM_CHECK_UPDATE_SECURITY, this);
|
||||
|
@ -692,11 +743,22 @@ var AddonManagerInternal = {
|
|||
Services.prefs.removeObserver(PREF_EM_AUTOUPDATE_DEFAULT, this);
|
||||
Services.prefs.removeObserver(PREF_EM_HOTFIX_ID, this);
|
||||
|
||||
// Always clean up listeners, but only shutdown providers if they've been
|
||||
// started.
|
||||
if (gStarted)
|
||||
this.callProviders("shutdown");
|
||||
// Only shut down providers if they've been started. Shut down
|
||||
// AddonRepository after providers (if any).
|
||||
let shuttingDown = null;
|
||||
if (gStarted) {
|
||||
shuttingDown = this.callProvidersAsync("shutdown")
|
||||
.then(null,
|
||||
err => ERROR("Failure during async provider shutdown", err))
|
||||
.then(() => AddonRepository.shutdown());
|
||||
}
|
||||
else {
|
||||
shuttingDown = AddonRepository.shutdown();
|
||||
}
|
||||
|
||||
shuttingDown.then(val => LOG("Async provider shutdown done"),
|
||||
err => ERROR("Failure during AddonRepository shutdown", err))
|
||||
.then(() => {
|
||||
this.managerListeners.splice(0, this.managerListeners.length);
|
||||
this.installListeners.splice(0, this.installListeners.length);
|
||||
this.addonListeners.splice(0, this.addonListeners.length);
|
||||
|
@ -705,6 +767,8 @@ var AddonManagerInternal = {
|
|||
delete this.startupChanges[type];
|
||||
gStarted = false;
|
||||
gStartupComplete = false;
|
||||
});
|
||||
return shuttingDown;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -713,11 +777,6 @@ var AddonManagerInternal = {
|
|||
* @see nsIObserver
|
||||
*/
|
||||
observe: function AMI_observe(aSubject, aTopic, aData) {
|
||||
if (aTopic == SHUTDOWN_EVENT) {
|
||||
this.shutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (aData) {
|
||||
case PREF_EM_CHECK_COMPATIBILITY: {
|
||||
let oldValue = gCheckCompatibility;
|
||||
|
@ -910,7 +969,6 @@ var AddonManagerInternal = {
|
|||
|
||||
if (this.updateEnabled) {
|
||||
let scope = {};
|
||||
Components.utils.import("resource://gre/modules/AddonRepository.jsm", scope);
|
||||
Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", scope);
|
||||
scope.LightweightThemeManager.updateCurrentTheme();
|
||||
|
||||
|
@ -921,7 +979,7 @@ var AddonManagerInternal = {
|
|||
|
||||
// Repopulate repository cache first, to ensure compatibility overrides
|
||||
// are up to date before checking for addon updates.
|
||||
scope.AddonRepository.backgroundUpdateCheck(
|
||||
AddonRepository.backgroundUpdateCheck(
|
||||
ids, function BUC_backgroundUpdateCheckCallback() {
|
||||
AddonManagerInternal.updateAddonRepositoryData(
|
||||
function BUC_updateAddonCallback() {
|
||||
|
|
|
@ -23,6 +23,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "DeferredSave",
|
|||
"resource://gre/modules/DeferredSave.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AddonRepository_SQLiteMigrator",
|
||||
"resource://gre/modules/AddonRepository_SQLiteMigrator.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
|
||||
this.EXPORTED_SYMBOLS = [ "AddonRepository" ];
|
||||
|
||||
|
@ -525,34 +527,17 @@ this.AddonRepository = {
|
|||
// Maximum number of results to return
|
||||
_maxResults: null,
|
||||
|
||||
/**
|
||||
* Initialize AddonRepository.
|
||||
*/
|
||||
initialize: function AddonRepo_initialize() {
|
||||
Services.obs.addObserver(this, "xpcom-shutdown", false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Observe xpcom-shutdown notification, so we can shutdown cleanly.
|
||||
*/
|
||||
observe: function AddonRepo_observe(aSubject, aTopic, aData) {
|
||||
if (aTopic == "xpcom-shutdown") {
|
||||
Services.obs.removeObserver(this, "xpcom-shutdown");
|
||||
this.shutdown();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Shut down AddonRepository
|
||||
* return: promise{integer} resolves with the result of flushing
|
||||
* the AddonRepository database
|
||||
*/
|
||||
shutdown: function AddonRepo_shutdown() {
|
||||
this.cancelSearch();
|
||||
|
||||
this._addons = null;
|
||||
this._pendingCallbacks = null;
|
||||
AddonDatabase.shutdown(function shutdown_databaseShutdown() {
|
||||
Services.obs.notifyObservers(null, "addon-repository-shutdown", null);
|
||||
});
|
||||
return AddonDatabase.shutdown(false);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1514,7 +1499,6 @@ this.AddonRepository = {
|
|||
}
|
||||
|
||||
};
|
||||
AddonRepository.initialize();
|
||||
|
||||
var AddonDatabase = {
|
||||
// true if the database connection has been opened
|
||||
|
@ -1644,13 +1628,11 @@ var AddonDatabase = {
|
|||
* An optional boolean to skip flushing data to disk. Useful
|
||||
* when the database is going to be deleted afterwards.
|
||||
*/
|
||||
shutdown: function AD_shutdown(aCallback, aSkipFlush) {
|
||||
shutdown: function AD_shutdown(aSkipFlush) {
|
||||
this.databaseOk = true;
|
||||
aCallback = aCallback || function() {};
|
||||
|
||||
if (!this.initialized) {
|
||||
aCallback();
|
||||
return;
|
||||
return Promise.resolve(0);
|
||||
}
|
||||
|
||||
this.initialized = false;
|
||||
|
@ -1660,9 +1642,9 @@ var AddonDatabase = {
|
|||
});
|
||||
|
||||
if (aSkipFlush) {
|
||||
aCallback();
|
||||
return Promise.resolve(0);
|
||||
} else {
|
||||
this.Writer.flush().then(aCallback, aCallback);
|
||||
return this.Writer.flush();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1676,13 +1658,14 @@ var AddonDatabase = {
|
|||
delete: function AD_delete(aCallback) {
|
||||
this.DB = BLANK_DB();
|
||||
|
||||
this.Writer.flush().then(null, () => {}).then(() => {
|
||||
this.shutdown(() => {
|
||||
let promise = OS.File.remove(this.jsonFile.path, {});
|
||||
if (aCallback)
|
||||
promise.then(aCallback, aCallback);
|
||||
}, true);
|
||||
});
|
||||
this.Writer.flush()
|
||||
.then(null, () => {})
|
||||
// shutdown(true) never rejects
|
||||
.then(() => this.shutdown(true))
|
||||
.then(() => OS.File.remove(this.jsonFile.path, {}))
|
||||
.then(null, error => ERROR("Unable to delete Addon Repository file " +
|
||||
this.jsonFile.path, error))
|
||||
.then(aCallback);
|
||||
},
|
||||
|
||||
toJSON: function AD_toJSON() {
|
||||
|
|
|
@ -1685,6 +1685,8 @@ var XPIProvider = {
|
|||
this.installs = [];
|
||||
this.installLocations = [];
|
||||
this.installLocationsByName = {};
|
||||
// Hook for tests to detect when saving database at shutdown time fails
|
||||
this._shutdownError = null;
|
||||
|
||||
AddonManagerPrivate.recordTimestamp("XPI_startup_begin");
|
||||
|
||||
|
@ -1873,6 +1875,9 @@ var XPIProvider = {
|
|||
|
||||
/**
|
||||
* Shuts down the database and releases all references.
|
||||
* Return: Promise{integer} resolves / rejects with the result of
|
||||
* flushing the XPI Database if it was loaded,
|
||||
* 0 otherwise.
|
||||
*/
|
||||
shutdown: function XPI_shutdown() {
|
||||
LOG("shutdown");
|
||||
|
@ -1903,10 +1908,19 @@ var XPIProvider = {
|
|||
delete this._uriMappings;
|
||||
|
||||
if (gLazyObjectsLoaded) {
|
||||
XPIDatabase.shutdown(function shutdownCallback(saveError) {
|
||||
let done = XPIDatabase.shutdown();
|
||||
done.then(
|
||||
ret => {
|
||||
LOG("Notifying XPI shutdown observers");
|
||||
Services.obs.notifyObservers(null, "xpi-provider-shutdown", saveError);
|
||||
});
|
||||
Services.obs.notifyObservers(null, "xpi-provider-shutdown", null);
|
||||
},
|
||||
err => {
|
||||
LOG("Notifying XPI shutdown observers");
|
||||
this._shutdownError = err;
|
||||
Services.obs.notifyObservers(null, "xpi-provider-shutdown", err);
|
||||
}
|
||||
);
|
||||
return done;
|
||||
}
|
||||
else {
|
||||
LOG("Notifying XPI shutdown observers");
|
||||
|
@ -3190,6 +3204,9 @@ var XPIProvider = {
|
|||
// active state of add-ons but didn't commit them properly (normally due
|
||||
// to the application crashing)
|
||||
let hasPendingChanges = Prefs.getBoolPref(PREF_PENDING_OPERATIONS);
|
||||
if (hasPendingChanges) {
|
||||
updateReasons.push("hasPendingChanges");
|
||||
}
|
||||
|
||||
// If the schema appears to have changed then we should update the database
|
||||
if (DB_SCHEMA != Prefs.getIntPref(PREF_DB_SCHEMA, 0)) {
|
||||
|
@ -3249,9 +3266,6 @@ var XPIProvider = {
|
|||
let extensionListChanged = false;
|
||||
// If the database needs to be updated then open it and then update it
|
||||
// from the filesystem
|
||||
if (hasPendingChanges) {
|
||||
updateReasons.push("hasPendingChanges");
|
||||
}
|
||||
if (updateReasons.length > 0) {
|
||||
AddonManagerPrivate.recordSimpleMeasure("XPIDB_startup_load_reasons", updateReasons);
|
||||
XPIDatabase.syncLoadDB(false);
|
||||
|
|
|
@ -978,8 +978,11 @@ this.XPIDatabase = {
|
|||
|
||||
/**
|
||||
* Shuts down the database connection and releases all cached objects.
|
||||
* Return: Promise{integer} resolves / rejects with the result of the DB
|
||||
* flush after the database is flushed and
|
||||
* all cleanup is done
|
||||
*/
|
||||
shutdown: function XPIDB_shutdown(aCallback) {
|
||||
shutdown: function XPIDB_shutdown() {
|
||||
LOG("shutdown");
|
||||
if (this.initialized) {
|
||||
// If our last database I/O had an error, try one last time to save.
|
||||
|
@ -997,21 +1000,17 @@ this.XPIDatabase = {
|
|||
"XPIDB_saves_late", this._deferredSave.dirty ? 1 : 0);
|
||||
}
|
||||
|
||||
// Make sure any pending writes of the DB are complete, and we
|
||||
// finish cleaning up, and then call back
|
||||
this.flush()
|
||||
.then(null, error => {
|
||||
// Return a promise that any pending writes of the DB are complete and we
|
||||
// are finished cleaning up
|
||||
let flushPromise = this.flush();
|
||||
flushPromise.then(null, error => {
|
||||
ERROR("Flush of XPI database failed", error);
|
||||
AddonManagerPrivate.recordSimpleMeasure("XPIDB_shutdownFlush_failed", 1);
|
||||
return 0;
|
||||
})
|
||||
.then(count => {
|
||||
// If our last attempt to read or write the DB failed, force a new
|
||||
// extensions.ini to be written to disk on the next startup
|
||||
let lastSaveFailed = this.lastError;
|
||||
if (lastSaveFailed)
|
||||
Services.prefs.setBoolPref(PREF_PENDING_OPERATIONS, true);
|
||||
|
||||
})
|
||||
.then(count => {
|
||||
// Clear out the cached addons data loaded from JSON
|
||||
delete this.addonDB;
|
||||
delete this._dbPromise;
|
||||
|
@ -1019,15 +1018,10 @@ this.XPIDatabase = {
|
|||
delete this._deferredSave;
|
||||
// re-enable the schema version setter
|
||||
delete this._schemaVersionSet;
|
||||
|
||||
if (aCallback)
|
||||
aCallback(lastSaveFailed);
|
||||
});
|
||||
return flushPromise;
|
||||
}
|
||||
else {
|
||||
if (aCallback)
|
||||
aCallback(null);
|
||||
}
|
||||
return Promise.resolve(0);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1382,6 +1376,13 @@ this.XPIDatabase = {
|
|||
* Synchronously calculates and updates all the active flags in the database.
|
||||
*/
|
||||
updateActiveAddons: function XPIDB_updateActiveAddons() {
|
||||
if (!this.addonDB) {
|
||||
WARN("updateActiveAddons called when DB isn't loaded");
|
||||
// force the DB to load
|
||||
AddonManagerPrivate.recordSimpleMeasure("XPIDB_lateOpen_updateActive",
|
||||
XPIProvider.runPhase);
|
||||
this.syncLoadDB(true);
|
||||
}
|
||||
LOG("Updating add-on states");
|
||||
for (let [, addon] of this.addonDB) {
|
||||
let newActive = (addon.visible && !addon.userDisabled &&
|
||||
|
|
|
@ -400,29 +400,16 @@ function shutdownManager() {
|
|||
if (!gInternalManager)
|
||||
return;
|
||||
|
||||
let xpiShutdown = false;
|
||||
Services.obs.addObserver({
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
xpiShutdown = true;
|
||||
gXPISaveError = aData;
|
||||
Services.obs.removeObserver(this, "xpi-provider-shutdown");
|
||||
}
|
||||
}, "xpi-provider-shutdown", false);
|
||||
|
||||
let repositoryShutdown = false;
|
||||
Services.obs.addObserver({
|
||||
observe: function(aSubject, aTopic, aData) {
|
||||
repositoryShutdown = true;
|
||||
Services.obs.removeObserver(this, "addon-repository-shutdown");
|
||||
}
|
||||
}, "addon-repository-shutdown", false);
|
||||
let shutdownDone = false;
|
||||
|
||||
Services.obs.notifyObservers(null, "quit-application-granted", null);
|
||||
let scope = Components.utils.import("resource://gre/modules/AddonManager.jsm");
|
||||
scope.AddonManagerInternal.shutdown();
|
||||
gInternalManager = null;
|
||||
scope.AddonManagerInternal.shutdown()
|
||||
.then(
|
||||
() => shutdownDone = true,
|
||||
err => shutdownDone = true);
|
||||
|
||||
AddonRepository.shutdown();
|
||||
gInternalManager = null;
|
||||
|
||||
// Load the add-ons list as it was after application shutdown
|
||||
loadAddonsList();
|
||||
|
@ -433,13 +420,16 @@ function shutdownManager() {
|
|||
let thr = Services.tm.mainThread;
|
||||
|
||||
// Wait until we observe the shutdown notifications
|
||||
while (!repositoryShutdown || !xpiShutdown) {
|
||||
while (!shutdownDone) {
|
||||
thr.processNextEvent(true);
|
||||
}
|
||||
|
||||
// Force the XPIProvider provider to reload to better
|
||||
// simulate real-world usage.
|
||||
scope = Components.utils.import("resource://gre/modules/XPIProvider.jsm");
|
||||
// This would be cleaner if I could get it as the rejection reason from
|
||||
// the AddonManagerInternal.shutdown() promise
|
||||
gXPISaveError = scope.XPIProvider._shutdownError;
|
||||
AddonManagerPrivate.unregisterProvider(scope.XPIProvider);
|
||||
Components.utils.unload("resource://gre/modules/XPIProvider.jsm");
|
||||
}
|
||||
|
|
|
@ -78,9 +78,19 @@ function run_test() {
|
|||
db.schemaVersion = 1;
|
||||
db.close();
|
||||
|
||||
Services.obs.addObserver({
|
||||
observe: function () {
|
||||
Services.obs.removeObserver(this, "addon-repository-shutdown");
|
||||
|
||||
Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
|
||||
AddonRepository.getCachedAddonByID("test1@tests.mozilla.org", function (aAddon) {
|
||||
do_check_neq(aAddon, null);
|
||||
do_check_eq(aAddon.screenshots.length, 1);
|
||||
do_check_true(aAddon.screenshots[0].width === null);
|
||||
do_check_true(aAddon.screenshots[0].height === null);
|
||||
do_check_true(aAddon.screenshots[0].thumbnailWidth === null);
|
||||
do_check_true(aAddon.screenshots[0].thumbnailHeight === null);
|
||||
do_check_eq(aAddon.iconURL, undefined);
|
||||
do_check_eq(JSON.stringify(aAddon.icons), "{}");
|
||||
AddonRepository.shutdown().then(
|
||||
function checkAfterRepoShutdown() {
|
||||
// Check the DB schema has changed once AddonRepository has freed it.
|
||||
db = AM_Cc["@mozilla.org/storage/service;1"].
|
||||
getService(AM_Ci.mozIStorageService).
|
||||
|
@ -110,19 +120,8 @@ function run_test() {
|
|||
|
||||
db.close();
|
||||
do_test_finished();
|
||||
}
|
||||
}, "addon-repository-shutdown", null);
|
||||
|
||||
Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
|
||||
AddonRepository.getCachedAddonByID("test1@tests.mozilla.org", function (aAddon) {
|
||||
do_check_neq(aAddon, null);
|
||||
do_check_eq(aAddon.screenshots.length, 1);
|
||||
do_check_true(aAddon.screenshots[0].width === null);
|
||||
do_check_true(aAddon.screenshots[0].height === null);
|
||||
do_check_true(aAddon.screenshots[0].thumbnailWidth === null);
|
||||
do_check_true(aAddon.screenshots[0].thumbnailHeight === null);
|
||||
do_check_eq(aAddon.iconURL, undefined);
|
||||
do_check_eq(JSON.stringify(aAddon.icons), "{}");
|
||||
AddonRepository.shutdown();
|
||||
},
|
||||
do_report_unexpected_exception
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче