--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -732,16 +732,26 @@ function isUsableAddon(aAddon) {
return true;
if (AddonManager.checkUpdateSecurity && !aAddon.providesUpdatesSecurely)
return false;
if (!aAddon.isPlatformCompatible)
return false;
+ if (aAddon.dependencies.length) {
+ let isActive = id => {
+ let active = XPIProvider.activeAddons.get(id);
+ return active && !active.disable;
+ };
+
+ if (aAddon.dependencies.some(id => !isActive(id)))
+ return false;
+ }
+
if (AddonManager.checkCompatibility) {
if (!aAddon.isCompatible)
return false;
}
else {
let app = aAddon.matchingTargetApplication;
if (!app)
return false;
@@ -772,16 +782,17 @@ function EM_R(aProperty) {
function createAddonDetails(id, aAddon) {
return {
id: id || aAddon.id,
type: aAddon.type,
version: aAddon.version,
multiprocessCompatible: aAddon.multiprocessCompatible,
runInSafeMode: aAddon.runInSafeMode,
+ dependencies: aAddon.dependencies,
};
}
/**
* Converts an internal add-on type to the type presented through the API.
*
* @param aType
* The internal add-on type
@@ -1153,16 +1164,25 @@ function loadManifestFromRDF(aUri, aStre
let targets = ds.GetTargets(root, EM_R("localized"), true);
while (targets.hasMoreElements()) {
let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
let locale = readLocale(ds, target, false, seenLocales);
if (locale)
addon.locales.push(locale);
}
+ let dependencies = new Set();
+ targets = ds.GetTargets(root, EM_R("dependency"), true);
+ while (targets.hasMoreElements()) {
+ let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
+ let id = getRDFProperty(ds, target, "id");
+ dependencies.add(id);
+ }
+ addon.dependencies = Object.freeze(Array.from(dependencies));
+
let seenApplications = [];
addon.targetApplications = [];
targets = ds.GetTargets(root, EM_R("targetApplication"), true);
while (targets.hasMoreElements()) {
let target = targets.getNext().QueryInterface(Ci.nsIRDFResource);
let targetAppInfo = {};
for (let prop of PROP_TARGETAPP) {
targetAppInfo[prop] = getRDFProperty(ds, target, prop);
@@ -2409,16 +2429,44 @@ this.XPIProvider = {
_telemetryDetails: {},
// A Map from an add-on install to its ID
_addonFileMap: new Map(),
// Flag to know if ToolboxProcess.jsm has already been loaded by someone or not
_toolboxProcessLoaded: false,
// Have we started shutting down bootstrap add-ons?
_closing: false,
+ sortBootstrappedAddons: function() {
+ let addons = {};
+
+ // Sort the list of IDs so that the ordering is deterministic.
+ for (let id of Object.keys(this.bootstrappedAddons).sort()) {
+ addons[id] = Object.assign({id}, this.bootstrappedAddons[id]);
+ }
+
+ let res = new Set();
+ let seen = new Set();
+
+ let add = addon => {
+ seen.add(addon.id);
+
+ for (let id of addon.dependencies || []) {
+ if (id in addons && !seen.has(id)) {
+ add(addons[id]);
+ }
+ }
+
+ res.add(addon.id);
+ }
+
+ Object.values(addons).forEach(add);
+
+ return Array.from(res, id => addons[id]);
+ },
+
/*
* Set a value in the telemetry hash for a given ID
*/
setTelemetry: function(aId, aName, aValue) {
if (!this._telemetryDetails[aId])
this._telemetryDetails[aId] = {};
this._telemetryDetails[aId][aName] = aValue;
},
@@ -2745,66 +2793,67 @@ this.XPIProvider = {
Services.appinfo.annotateCrashReport("EMCheckCompatibility",
AddonManager.checkCompatibility);
} catch (e) { }
this.addAddonsToCrashReporter();
}
try {
AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_begin");
- for (let id in this.bootstrappedAddons) {
+
+ for (let addon of this.sortBootstrappedAddons()) {
try {
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.persistentDescriptor = this.bootstrappedAddons[id].descriptor;
+ file.persistentDescriptor = addon.descriptor;
let reason = BOOTSTRAP_REASONS.APP_STARTUP;
// Eventually set INSTALLED reason when a bootstrap addon
// is dropped in profile folder and automatically installed
if (AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_INSTALLED)
- .indexOf(id) !== -1)
+ .indexOf(addon.id) !== -1)
reason = BOOTSTRAP_REASONS.ADDON_INSTALL;
- this.callBootstrapMethod(createAddonDetails(id, this.bootstrappedAddons[id]),
+ this.callBootstrapMethod(createAddonDetails(addon.id, addon),
file, "startup", reason);
}
catch (e) {
- logger.error("Failed to load bootstrap addon " + id + " from " +
- this.bootstrappedAddons[id].descriptor, e);
+ logger.error("Failed to load bootstrap addon " + addon.id + " from " +
+ addon.descriptor, e);
}
}
AddonManagerPrivate.recordTimestamp("XPI_bootstrap_addons_end");
}
catch (e) {
logger.error("bootstrap startup failed", e);
AddonManagerPrivate.recordException("XPI-BOOTSTRAP", "startup failed", e);
}
// Let these shutdown a little earlier when they still have access to most
// of XPCOM
Services.obs.addObserver({
observe: function(aSubject, aTopic, aData) {
XPIProvider._closing = true;
- for (let id in XPIProvider.bootstrappedAddons) {
+ for (let addon of XPIProvider.sortBootstrappedAddons().reverse()) {
// If no scope has been loaded for this add-on then there is no need
// to shut it down (should only happen when a bootstrapped add-on is
// pending enable)
- if (!XPIProvider.activeAddons.has(id))
+ if (!XPIProvider.activeAddons.has(addon.id))
continue;
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
- file.persistentDescriptor = XPIProvider.bootstrappedAddons[id].descriptor;
- let addon = createAddonDetails(id, XPIProvider.bootstrappedAddons[id]);
+ file.persistentDescriptor = addon.descriptor;
+ let addonDetails = createAddonDetails(addon.id, addon);
// If the add-on was pending disable then shut it down and remove it
// from the persisted data.
- if (XPIProvider.bootstrappedAddons[id].disable) {
- XPIProvider.callBootstrapMethod(addon, file, "shutdown",
+ if (addon.disable) {
+ XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown",
BOOTSTRAP_REASONS.ADDON_DISABLE);
- delete XPIProvider.bootstrappedAddons[id];
+ delete XPIProvider.bootstrappedAddons[addon.id];
}
else {
- XPIProvider.callBootstrapMethod(addon, file, "shutdown",
+ XPIProvider.callBootstrapMethod(addonDetails, file, "shutdown",
BOOTSTRAP_REASONS.APP_SHUTDOWN);
}
}
Services.obs.removeObserver(this, "quit-application-granted");
}
}, "quit-application-granted", false);
// Detect final-ui-startup for telemetry reporting
@@ -3588,16 +3637,21 @@ this.XPIProvider = {
* Imports the xpinstall permissions from preferences into the permissions
* manager for the user to change later.
*/
importPermissions: function() {
PermissionsUtils.importFromPrefs(PREF_XPI_PERMISSIONS_BRANCH,
XPI_PERMISSION);
},
+ getDependentAddons: function(aAddon) {
+ return Array.from(XPIDatabase.getAddons())
+ .filter(addon => addon.dependencies.includes(aAddon.id));
+ },
+
/**
* Checks for any changes that have occurred since the last time the
* application was launched.
*
* @param aAppChanged
* A tri-state value. Undefined means the current profile was created
* for this session, true means the profile already existed but was
* last used with an application with a different version number,
@@ -4558,27 +4612,31 @@ this.XPIProvider = {
* @param aVersion
* The add-on's version
* @param aType
* The type for the add-on
* @param aMultiprocessCompatible
* Boolean indicating whether the add-on is compatible with electrolysis.
* @param aRunInSafeMode
* Boolean indicating whether the add-on can run in safe mode.
+ * @param aDependencies
+ * An array of add-on IDs on which this add-on depends.
* @return a JavaScript scope
*/
loadBootstrapScope: function(aId, aFile, aVersion, aType,
- aMultiprocessCompatible, aRunInSafeMode) {
+ aMultiprocessCompatible, aRunInSafeMode,
+ aDependencies) {
// Mark the add-on as active for the crash reporter before loading
this.bootstrappedAddons[aId] = {
version: aVersion,
type: aType,
descriptor: aFile.persistentDescriptor,
multiprocessCompatible: aMultiprocessCompatible,
runInSafeMode: aRunInSafeMode,
+ dependencies: aDependencies,
};
this.persistBootstrappedAddons();
this.addAddonsToCrashReporter();
this.activeAddons.set(aId, {
debugGlobal: null,
safeWrapper: null,
bootstrapScope: null,
@@ -4722,17 +4780,17 @@ this.XPIProvider = {
}
try {
// Load the scope if it hasn't already been loaded
let activeAddon = this.activeAddons.get(aAddon.id);
if (!activeAddon) {
this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type,
aAddon.multiprocessCompatible || false,
- runInSafeMode);
+ runInSafeMode, aAddon.dependencies);
activeAddon = this.activeAddons.get(aAddon.id);
}
if (aMethod == "startup" || aMethod == "shutdown") {
if (!aExtraParams) {
aExtraParams = {};
}
aExtraParams["instanceID"] = this.activeAddons.get(aAddon.id).instanceID;
@@ -4752,16 +4810,25 @@ this.XPIProvider = {
// That will be logged below.
}
if (!method) {
logger.warn("Add-on " + aAddon.id + " is missing bootstrap method " + aMethod);
return;
}
+ // Extensions are automatically deinitialized in the correct order at shutdown.
+ if (aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
+ activeAddon.disable = true;
+ for (let addon of this.getDependentAddons(aAddon)) {
+ if (addon.active)
+ this.updateAddonDisabledState(addon);
+ }
+ }
+
let params = {
id: aAddon.id,
version: aAddon.version,
installPath: aFile.clone(),
resourceURI: getURIForResourceInFile(aFile, "")
};
if (aExtraParams) {
@@ -4775,16 +4842,22 @@ this.XPIProvider = {
try {
method(params, aReason);
}
catch (e) {
logger.warn("Exception running bootstrap method " + aMethod + " on " + aAddon.id, e);
}
}
finally {
+ // Extensions are automatically initialized in the correct order at startup.
+ if (aMethod == "startup" && aReason != BOOTSTRAP_REASONS.APP_STARTUP) {
+ for (let addon of this.getDependentAddons(aAddon))
+ this.updateAddonDisabledState(addon);
+ }
+
if (CHROME_TYPES.has(aAddon.type) && aMethod == "shutdown" && aReason != BOOTSTRAP_REASONS.APP_SHUTDOWN) {
logger.debug("Removing manifest for " + aFile.path);
Components.manager.removeBootstrappedManifestLocation(aFile);
let manifest = getURIForResourceInFile(aFile, "chrome.manifest");
for (let line of ChromeManifestParser.parseSync(manifest)) {
if (line.type == "resource") {
ResProtocolHandler.setSubstitution(line.args[0], null);
@@ -4885,16 +4958,17 @@ this.XPIProvider = {
else {
needsRestart = this.enableRequiresRestart(aAddon);
AddonManagerPrivate.callAddonListeners("onEnabling", wrapper,
needsRestart);
}
if (!needsRestart) {
XPIDatabase.updateAddonActive(aAddon, !isDisabled);
+
if (isDisabled) {
if (aAddon.bootstrap) {
this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
BOOTSTRAP_REASONS.ADDON_DISABLE);
this.unloadBootstrapScope(aAddon.id);
}
AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
}
@@ -4914,16 +4988,17 @@ this.XPIProvider = {
}
else {
this.bootstrappedAddons[aAddon.id] = {
version: aAddon.version,
type: aAddon.type,
descriptor: aAddon._sourceBundle.persistentDescriptor,
multiprocessCompatible: aAddon.multiprocessCompatible,
runInSafeMode: canRunInSafeMode(aAddon),
+ dependencies: aAddon.dependencies,
};
this.persistBootstrappedAddons();
}
}
}
// Sync with XPIStates.
let xpiState = XPIStates.getAddon(aAddon.location, aAddon.id);
@@ -6793,16 +6868,17 @@ AddonInternal.prototype = {
userDisabled: false,
appDisabled: false,
softDisabled: false,
sourceURI: null,
releaseNotesURI: null,
foreignInstall: false,
seen: true,
skinnable: false,
+ dependencies: Object.freeze([]),
get selectedLocale() {
if (this._selectedLocale)
return this._selectedLocale;
let locale = Locale.findClosestLocale(this.locales);
this._selectedLocale = locale ? locale : this.defaultLocale;
return this._selectedLocale;
},
@@ -7564,17 +7640,17 @@ function defineAddonWrapperProperty(name
get: getter,
enumerable: true,
});
}
["id", "syncGUID", "version", "isCompatible", "isPlatformCompatible",
"providesUpdatesSecurely", "blocklistState", "blocklistURL", "appDisabled",
"softDisabled", "skinnable", "size", "foreignInstall", "hasBinaryComponents",
- "strictCompatibility", "compatibilityOverrides", "updateURL",
+ "strictCompatibility", "compatibilityOverrides", "updateURL", "dependencies",
"getDataDirectory", "multiprocessCompatible", "signedState"].forEach(function(aProp) {
defineAddonWrapperProperty(aProp, function() {
let addon = addonFor(this);
return (aProp in addon) ? addon[aProp] : undefined;
});
});
["fullDescription", "developerComments", "eula", "supportURL",
--- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -81,17 +81,17 @@ const PROP_JSON_FIELDS = ["id", "syncGUI
"optionsType", "aboutURL", "icons", "iconURL", "icon64URL",
"defaultLocale", "visible", "active", "userDisabled",
"appDisabled", "pendingUninstall", "descriptor", "installDate",
"updateDate", "applyBackgroundUpdates", "bootstrap",
"skinnable", "size", "sourceURI", "releaseNotesURI",
"softDisabled", "foreignInstall", "hasBinaryComponents",
"strictCompatibility", "locales", "targetApplications",
"targetPlatforms", "multiprocessCompatible", "signedState",
- "seen"];
+ "seen", "dependencies"];
// Properties that should be migrated where possible from an old database. These
// shouldn't include properties that can be read directly from install.rdf files
// or calculated
const DB_MIGRATE_METADATA= ["installDate", "userDisabled", "softDisabled",
"sourceURI", "applyBackgroundUpdates",
"releaseNotesURI", "foreignInstall", "syncGUID"];
@@ -325,16 +325,20 @@ function copyRowProperties(aRow, aProper
* @param aLoaded
* Addon data fields loaded from JSON or the addon manifest.
*/
function DBAddonInternal(aLoaded) {
AddonInternal.call(this);
copyProperties(aLoaded, PROP_JSON_FIELDS, this);
+ if (!this.dependencies)
+ this.dependencies = [];
+ Object.freeze(this.dependencies);
+
if (aLoaded._installLocation) {
this._installLocation = aLoaded._installLocation;
this.location = aLoaded._installLocation.name;
}
else if (aLoaded.location) {
this._installLocation = XPIProvider.installLocationsByName[this.location];
}
@@ -2147,16 +2151,17 @@ this.XPIDatabaseReconcile = {
// Make sure the bootstrap information is up to date for this ID
if (currentAddon.bootstrap && currentAddon.active) {
XPIProvider.bootstrappedAddons[id] = {
version: currentAddon.version,
type: currentAddon.type,
descriptor: currentAddon._sourceBundle.persistentDescriptor,
multiprocessCompatible: currentAddon.multiprocessCompatible,
runInSafeMode: canRunInSafeMode(currentAddon),
+ dependencies: currentAddon.dependencies,
};
}
if (currentAddon.active && currentAddon.internalName == XPIProvider.selectedSkin)
sawActiveTheme = true;
}
// Pass over the set of previously visible add-ons that have now gone away
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1,16 +1,18 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
var AM_Cc = Components.classes;
var AM_Ci = Components.interfaces;
var AM_Cu = Components.utils;
+AM_Cu.importGlobalProperties(["TextEncoder"]);
+
const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
@@ -1022,16 +1024,22 @@ function createInstallRDF(aData) {
rdf += "<em:locale>" + escapeXML(aLocaleName) + "</em:locale>\n";
});
}
rdf += writeLocaleStrings(aLocalized);
rdf += "</Description></em:localized>\n";
});
}
+ if ("dependencies" in aData) {
+ aData.dependencies.forEach(function(aDependency) {
+ rdf += `<em:dependency><Description em:id="${escapeXML(aDependency)}"/></em:dependency>\n`;
+ });
+ }
+
rdf += "</Description>\n</RDF>\n";
return rdf;
}
/**
* Writes an install.rdf manifest into a directory using the properties passed
* in a JS object. The objects should contain a property for each property to
* appear in the RDF. The object may contain an array of objects with id,
@@ -1184,64 +1192,93 @@ function writeInstallRDFToXPI(aData, aDi
var file = aDir.clone();
file.append(id + ".xpi");
writeInstallRDFToXPIFile(aData, file, aExtraFile);
return file;
}
/**
+ * Writes the given data to a file in the given zip file.
+ *
+ * @param aFile
+ * The zip file to write to.
+ * @param aFiles
+ * An object containing filenames and the data to write to the
+ * corresponding paths in the zip file.
+ * @param aFlags
+ * Additional flags to open the file with.
+ */
+function writeFilesToZip(aFile, aFiles, aFlags = 0) {
+ var zipW = AM_Cc["@mozilla.org/zipwriter;1"].createInstance(AM_Ci.nsIZipWriter);
+ zipW.open(aFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | aFlags);
+
+ for (let path of Object.keys(aFiles)) {
+ let data = aFiles[path];
+ if (!(data instanceof ArrayBuffer)) {
+ data = new TextEncoder("utf-8").encode(data).buffer;
+ }
+
+ let stream = AM_Cc["@mozilla.org/io/arraybuffer-input-stream;1"]
+ .createInstance(AM_Ci.nsIArrayBufferInputStream);
+ stream.setData(data, 0, data.byteLength);
+
+ // Note these files are being created in the XPI archive with date "0" which is 1970-01-01.
+ zipW.addEntryStream(path, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+ stream, false);
+ }
+
+ zipW.close();
+}
+
+/**
* Writes an install.rdf manifest into an XPI file using the properties passed
* in a JS object. The objects should contain a property for each property to
* appear in the RDF. The object may contain an array of objects with id,
* minVersion and maxVersion in the targetApplications property to give target
* application compatibility.
*
* @param aData
* The object holding data about the add-on
* @param aFile
* The XPI file to write to. Any existing file will be overwritten
* @param aExtraFile
* An optional dummy file to create in the extension
*/
function writeInstallRDFToXPIFile(aData, aFile, aExtraFile) {
- var rdf = createInstallRDF(aData);
- var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
- createInstance(AM_Ci.nsIStringInputStream);
- stream.setData(rdf, -1);
- var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
- createInstance(AM_Ci.nsIZipWriter);
- zipW.open(aFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE);
- // Note these files are being created in the XPI archive with date "0" which is 1970-01-01.
- zipW.addEntryStream("install.rdf", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
- stream, false);
- if (aExtraFile)
- zipW.addEntryStream(aExtraFile, 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
- stream, false);
- zipW.close();
+ let files = {
+ "install.rdf": createInstallRDF(aData),
+ };
+
+ if (typeof aExtraFile == "object")
+ Object.assign(files, aExtraFile);
+ else if (aExtraFile)
+ files[aExtraFile] = "";
+
+ writeFilesToZip(aFile, files, FileUtils.MODE_TRUNCATE);
}
var temp_xpis = [];
/**
* Creates an XPI file for some manifest data in the temporary directory and
* returns the nsIFile for it. The file will be deleted when the test completes.
*
* @param aData
* The object holding data about the add-on
* @return A file pointing to the created XPI file
*/
-function createTempXPIFile(aData) {
+function createTempXPIFile(aData, aExtraFile) {
var file = gTmpD.clone();
file.append("foo.xpi");
do {
file.leafName = Math.floor(Math.random() * 1000000) + ".xpi";
} while (file.exists());
temp_xpis.push(file);
- writeInstallRDFToXPIFile(aData, file);
+ writeInstallRDFToXPIFile(aData, file, aExtraFile);
return file;
}
/**
* Creates an XPI file for some WebExtension data in the temporary directory and
* returns the nsIFile for it. The file will be deleted when the test completes.
*
* @param aData
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+startupManager();
+
+const BOOTSTRAP = String.raw`
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ function startup(data) {
+ Services.obs.notifyObservers(null, "test-addon-bootstrap-startup", data.id);
+ }
+ function shutdown(data) {
+ Services.obs.notifyObservers(null, "test-addon-bootstrap-shutdown", data.id);
+ }
+ function install() {}
+ function uninstall() {}
+`;
+
+const ADDONS = [
+ {
+ id: "addon1@dependency-test.mozilla.org",
+ dependencies: ["addon2@dependency-test.mozilla.org"],
+ },
+ {
+ id: "addon2@dependency-test.mozilla.org",
+ dependencies: ["addon3@dependency-test.mozilla.org"],
+ },
+ {
+ id: "addon3@dependency-test.mozilla.org",
+ },
+ {
+ id: "addon4@dependency-test.mozilla.org",
+ },
+ {
+ id: "addon5@dependency-test.mozilla.org",
+ dependencies: ["addon2@dependency-test.mozilla.org"],
+ },
+];
+
+let addonFiles = [];
+
+let events = [];
+add_task(function* setup() {
+ let startupObserver = (subject, topic, data) => {
+ events.push(["startup", data]);
+ };
+ let shutdownObserver = (subject, topic, data) => {
+ events.push(["shutdown", data]);
+ };
+
+ Services.obs.addObserver(startupObserver, "test-addon-bootstrap-startup", false);
+ Services.obs.addObserver(shutdownObserver, "test-addon-bootstrap-shutdown", false);
+ do_register_cleanup(() => {
+ Services.obs.removeObserver(startupObserver, "test-addon-bootstrap-startup");
+ Services.obs.removeObserver(shutdownObserver, "test-addon-bootstrap-shutdown");
+ });
+
+ for (let addon of ADDONS) {
+ Object.assign(addon, {
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1",
+ }],
+ version: "1.0",
+ name: addon.id,
+ bootstrap: true,
+ });
+
+ addonFiles.push(createTempXPIFile(addon, {"bootstrap.js": BOOTSTRAP}));
+
+ let dir = AM_Cc["@mozilla.org/file/local;1"].createInstance(AM_Ci.nsIFile);
+ dir.initWithPath("/home/kris");
+ // addonFiles[addonFiles.length - 1].copyTo(dir, null);
+ }
+});
+
+add_task(function*() {
+ deepEqual(events, [], "Should have no events");
+
+ yield promiseInstallAllFiles([addonFiles[3]]);
+
+ deepEqual(events, [
+ ["startup", ADDONS[3].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseInstallAllFiles([addonFiles[0]]);
+ deepEqual(events, [], "Should have no events");
+
+ yield promiseInstallAllFiles([addonFiles[1]]);
+ deepEqual(events, [], "Should have no events");
+
+ yield promiseInstallAllFiles([addonFiles[2]]);
+
+ deepEqual(events, [
+ ["startup", ADDONS[2].id],
+ ["startup", ADDONS[1].id],
+ ["startup", ADDONS[0].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseInstallAllFiles([addonFiles[2]]);
+
+ deepEqual(events, [
+ ["shutdown", ADDONS[0].id],
+ ["shutdown", ADDONS[1].id],
+ ["shutdown", ADDONS[2].id],
+
+ ["startup", ADDONS[2].id],
+ ["startup", ADDONS[1].id],
+ ["startup", ADDONS[0].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseInstallAllFiles([addonFiles[4]]);
+
+ deepEqual(events, [
+ ["startup", ADDONS[4].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseRestartManager();
+
+ deepEqual(events, [
+ ["shutdown", ADDONS[4].id],
+ ["shutdown", ADDONS[3].id],
+ ["shutdown", ADDONS[0].id],
+ ["shutdown", ADDONS[1].id],
+ ["shutdown", ADDONS[2].id],
+
+ ["startup", ADDONS[2].id],
+ ["startup", ADDONS[1].id],
+ ["startup", ADDONS[0].id],
+ ["startup", ADDONS[3].id],
+ ["startup", ADDONS[4].id],
+ ]);
+});
+
--- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -32,11 +32,12 @@ skip-if = appname != "firefox"
[test_system_reset.js]
[test_XPIcancel.js]
[test_XPIStates.js]
[test_temporary.js]
[test_proxies.js]
[test_proxy.js]
[test_pass_symbol.js]
[test_delay_update.js]
+[test_dependencies.js]
[include:xpcshell-shared.ini]