Bug 1280083: Support dependencies for bootstrapped add-ons. r=aswan

MozReview-Commit-ID: ACmsUcKZ2Jp

--HG--
extra : rebase_source : fc7842ff4026ec2b39d1f961253b17cb4f12912e
This commit is contained in:
Kris Maglione 2016-06-16 16:29:31 +01:00
parent ec7c520520
commit 83cfaa2af3
5 changed files with 315 additions and 34 deletions

View File

@ -752,6 +752,16 @@ function isUsableAddon(aAddon) {
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) {
logger.warn(`Add-on ${aAddon.id} is not compatible with application version.`);
@ -797,6 +807,7 @@ function createAddonDetails(id, aAddon) {
version: aAddon.version,
multiprocessCompatible: aAddon.multiprocessCompatible,
runInSafeMode: aAddon.runInSafeMode,
dependencies: aAddon.dependencies,
};
}
@ -1178,6 +1189,15 @@ function loadManifestFromRDF(aUri, aStream) {
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);
@ -2424,6 +2444,45 @@ this.XPIProvider = {
// Have we started shutting down bootstrap add-ons?
_closing: false,
/**
* Returns an array of the add-on values in `bootstrappedAddons`,
* sorted so that all of an add-on's dependencies appear in the array
* before itself.
*
* @returns {Array<object>}
* A sorted array of add-on objects. Each value is a copy of the
* corresponding value in the `bootstrappedAddons` object, with an
* additional `id` property, which corresponds to the key in that
* object, which is the same as the add-ons ID.
*/
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
*/
@ -2762,22 +2821,23 @@ this.XPIProvider = {
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");
@ -2792,26 +2852,26 @@ this.XPIProvider = {
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);
}
}
@ -3603,6 +3663,11 @@ this.XPIProvider = {
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.
@ -4587,10 +4652,13 @@ this.XPIProvider = {
* 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,
@ -4598,6 +4666,7 @@ this.XPIProvider = {
descriptor: aFile.persistentDescriptor,
multiprocessCompatible: aMultiprocessCompatible,
runInSafeMode: aRunInSafeMode,
dependencies: aDependencies,
};
this.persistBootstrappedAddons();
this.addAddonsToCrashReporter();
@ -4753,7 +4822,7 @@ this.XPIProvider = {
if (!activeAddon) {
this.loadBootstrapScope(aAddon.id, aFile, aAddon.version, aAddon.type,
aAddon.multiprocessCompatible || false,
runInSafeMode);
runInSafeMode, aAddon.dependencies);
activeAddon = this.activeAddons.get(aAddon.id);
}
@ -4783,6 +4852,15 @@ this.XPIProvider = {
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,
@ -4806,6 +4884,12 @@ this.XPIProvider = {
}
}
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);
@ -4916,6 +5000,7 @@ this.XPIProvider = {
if (!needsRestart) {
XPIDatabase.updateAddonActive(aAddon, !isDisabled);
if (isDisabled) {
if (aAddon.bootstrap) {
this.callBootstrapMethod(aAddon, aAddon._sourceBundle, "shutdown",
@ -4945,6 +5030,7 @@ this.XPIProvider = {
descriptor: aAddon._sourceBundle.persistentDescriptor,
multiprocessCompatible: aAddon.multiprocessCompatible,
runInSafeMode: canRunInSafeMode(aAddon),
dependencies: aAddon.dependencies,
};
this.persistBootstrappedAddons();
}
@ -6825,6 +6911,14 @@ AddonInternal.prototype = {
seen: true,
skinnable: false,
/**
* @property {Array<string>} dependencies
* An array of bootstrapped add-on IDs on which this add-on depends.
* The add-on will remain appDisabled if any of the dependent
* add-ons is not installed and enabled.
*/
dependencies: Object.freeze([]),
get selectedLocale() {
if (this._selectedLocale)
return this._selectedLocale;
@ -7647,7 +7741,7 @@ function defineAddonWrapperProperty(name, getter) {
["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);

View File

@ -87,7 +87,7 @@ const PROP_JSON_FIELDS = ["id", "syncGUID", "location", "version", "type",
"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
@ -331,6 +331,10 @@ function DBAddonInternal(aLoaded) {
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;
@ -2155,6 +2159,7 @@ this.XPIDatabaseReconcile = {
descriptor: currentAddon._sourceBundle.persistentDescriptor,
multiprocessCompatible: currentAddon.multiprocessCompatible,
runInSafeMode: canRunInSafeMode(currentAddon),
dependencies: currentAddon.dependencies,
};
}

View File

@ -6,6 +6,8 @@ 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}");
@ -1027,6 +1029,12 @@ function createInstallRDF(aData) {
});
}
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;
}
@ -1186,6 +1194,39 @@ function writeInstallRDFToXPI(aData, aDir, aId, 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
@ -1201,20 +1242,16 @@ function writeInstallRDFToXPI(aData, aDir, aId, 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 = [];
@ -1226,7 +1263,7 @@ var temp_xpis = [];
* 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 {
@ -1234,7 +1271,7 @@ function createTempXPIFile(aData) {
} while (file.exists());
temp_xpis.push(file);
writeInstallRDFToXPIFile(aData, file);
writeInstallRDFToXPIFile(aData, file, aExtraFile);
return file;
}

View File

@ -0,0 +1,144 @@
/* 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}));
}
});
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],
]);
});

View File

@ -38,6 +38,7 @@ skip-if = appname != "firefox"
[test_pass_symbol.js]
[test_delay_update.js]
[test_nodisable_hidden.js]
[test_dependencies.js]
[include:xpcshell-shared.ini]