From 83cfaa2af39d8054e28c5d889daddd892a8777c2 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Thu, 16 Jun 2016 16:29:31 +0100 Subject: [PATCH] Bug 1280083: Support dependencies for bootstrapped add-ons. r=aswan MozReview-Commit-ID: ACmsUcKZ2Jp --HG-- extra : rebase_source : fc7842ff4026ec2b39d1f961253b17cb4f12912e --- .../extensions/internal/XPIProvider.jsm | 128 +++++++++++++--- .../extensions/internal/XPIProviderUtils.js | 7 +- .../extensions/test/xpcshell/head_addons.js | 69 +++++++-- .../test/xpcshell/test_dependencies.js | 144 ++++++++++++++++++ .../extensions/test/xpcshell/xpcshell.ini | 1 + 5 files changed, 315 insertions(+), 34 deletions(-) create mode 100644 toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm index 6e203f4e711a..83c79a1460b7 100644 --- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm +++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm @@ -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} + * 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} 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); diff --git a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js index 9f66b7399014..5e7f941697e2 100644 --- a/toolkit/mozapps/extensions/internal/XPIProviderUtils.js +++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js @@ -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, }; } diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js index 0f8be20f45e4..02a816249a35 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js +++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js @@ -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 += `\n`; + }); + } + rdf += "\n\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; } diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js new file mode 100644 index 000000000000..3afc03f84019 --- /dev/null +++ b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js @@ -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], + ]); +}); + diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini index 9995cd06819d..ef3fc5f248e7 100644 --- a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini +++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini @@ -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]