bug 1378459 parts 2 through N: implement persistent event listeners
MozReview-Commit-ID: IUbaydJVKPc
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -1156,16 +1156,17 @@ class Extension extends ExtensionData {
if (addonData.cleanupFile) {
Services.obs.addObserver(this, "xpcom-shutdown");
this.cleanupFile = addonData.cleanupFile || null;
delete addonData.cleanupFile;
}
this.addonData = addonData;
+ this.startupData = addonData.startupData || {};
this.startupReason = startupReason;
if (["ADDON_UPGRADE", "ADDON_DOWNGRADE"].includes(startupReason)) {
StartupCache.clearAddonData(addonData.id);
}
this.remote = !WebExtensionPolicy.isExtensionProcess;
@@ -1350,16 +1351,20 @@ class Extension extends ExtensionData {
this.addonData.temporarilyInstalled));
}
get experimentsAllowed() {
return (AddonSettings.ALLOW_LEGACY_EXTENSIONS ||
this.isPrivileged);
}
+ saveStartupData() {
+ AddonManagerPrivate.setStartupData(this.id, this.startupData);
+ }
+
async _parseManifest() {
let manifest = await super.parseManifest();
if (manifest && manifest.permissions.has("mozillaAddons") &&
!this.isPrivileged) {
Cu.reportError(`Stripping mozillaAddons permission from ${this.id}`);
manifest.permissions.delete("mozillaAddons");
let i = manifest.manifest.permissions.indexOf("mozillaAddons");
if (i >= 0) {
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -18,16 +18,17 @@ var EXPORTED_SYMBOLS = ["ExtensionCommon
Cu.importGlobalProperties(["fetch"]);
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetters(this, {
ConsoleAPI: "resource://gre/modules/Console.jsm",
MessageChannel: "resource://gre/modules/MessageChannel.jsm",
+ ObjectUtils: "resource://gre/modules/ObjectUtils.jsm",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
Schemas: "resource://gre/modules/Schemas.jsm",
SchemaRoot: "resource://gre/modules/Schemas.jsm",
});
XPCOMUtils.defineLazyServiceGetter(this, "styleSheetService",
"@mozilla.org/content/style-sheet-service;1",
"nsIStyleSheetService");
@@ -1766,23 +1767,101 @@ class EventManager {
* An object representing the extension instance using this event.
* @param {string} params.name
* A name used only for debugging.
* @param {functon} params.register
* A function called whenever a new listener is added.
* @param {boolean} [params.inputHandling=false]
* If true, the "handling user input" flag is set while handlers
* for this event are executing.
+ * @param {object} [params.persistent]
+ * Details for persistent event listeners
+ * @param {string} params.persistent.module
+ * The name of the module in which this event is defined.
+ * @param {string} params.persistent.event
+ * The name of this event.
*/
- constructor({context, name, register, inputHandling = false}) {
+ constructor({context, name, register, inputHandling = false, persistent = null}) {
this.context = context;
this.name = name;
this.register = register;
+ this.inputHandling = inputHandling;
+ this.persistent = persistent;
+
+ // XXX DEBUG only? XXX validate that persistent has exactly module and event
+ if (this.persistent && context.envType !== "addon_parent") {
+ throw new Error("Persistent event managers can only be created for addon_parent");
+ }
+
this.unregister = new Map();
- this.inputHandling = inputHandling;
+ }
+
+ // Set up "primed" event listeners for any saved event listeners
+ // in the provided extension's startup data.
+ // XXX write a better comment
+ static primeListeners(extension) {
+ extension.primedEvents = {};
+
+ let listeners = extension.startupData.persistentListeners;
+ if (!listeners) {
+ return;
+ }
+
+ for (let [module, entry] of Object.entries(listeners)) {
+ extension.primedEvents[module] = {};
+ let api = extension.apiManager.getAPI(module, extension, "addon_parent");
+ for (let [event, paramlists] of Object.entries(entry)) {
+ dump(`have saved listener for ${module}.${event}\n`);
+
+ extension.primedEvents[module][event] = new Set(paramlists.map(params => {
+ let info = {params};
+
+ let wakeup = (...args) => {
+ return new Promise((resolve, reject) => {
+ if (!info.pendingEvents) {
+ info.pendingEvents = [];
+ }
+ info.pendingEvents.push({args, resolve, reject});
+
+ extension.emit("start-background-page");
+ });
+ };
+
+ let fire = {
+ sync: wakeup,
+ async: wakeup,
+ };
+
+ let {unregister, setFire} = api.primeListener(extension, event, fire, params);
+ Object.assign(info, {unregister, setFire});
+ return info;
+ }));
+ }
+ }
+ }
+
+ // Remove any primed listeners created from primeListeners()
+ // that were not re-created.
+ // This function is called after the background page is started and
+ // all event listeners have been registered.
+ // XXX write a better comment
+ static clearPrimedListeners(extension) {
+ for (let entry1 of Object.values(extension.primedEvents)) {
+ for (let entry2 of Object.values(entry1)) {
+ for (let info of entry2) {
+ if (info.pendingEvents) {
+ for (let evt of info.pendingEvents) {
+ evt.reject(new Error("listener not re-registered"));
+ }
+ }
+ // XXX remove from startupData
+ info.unregister();
+ }
+ }
+ }
}
addListener(callback, ...args) {
if (this.unregister.has(callback)) {
return;
}
let shouldFire = () => {
@@ -1819,19 +1898,77 @@ class EventManager {
return Promise.resolve().then(() => {
if (shouldFire()) {
return this.context.applySafeWithoutClone(callback, args);
}
});
},
};
- let unregister = this.register(fire, ...args);
+ let {extension} = this.context;
+
+ let unregister = null;
+ let recordStartupData = false;
+
+ // If this is a persistent event, check for a listener that was already
+ // created during startup. If there is one, use it and don't create a
+ // new one.
+ if (this.persistent) {
+ recordStartupData = true;
+ let {module, event} = this.persistent;
+ if (extension.primedEvents &&
+ extension.primedEvents[module] &&
+ extension.primedEvents[module][event]) {
+ let listeners = extension.primedEvents[module][event];
+ for (let listener of listeners) {
+ if (ObjectUtils.deepEqual(args, listener.params)) {
+ listener.setFire(fire);
+ unregister = listener.unregister;
+
+ if (listener.pendingEvents) {
+ for (let evt of listener.pendingEvents) {
+ evt.resolve(fire.async(...evt.args));
+ }
+ }
+
+ listeners.delete(listener);
+ recordStartupData = false;
+ break;
+ }
+ }
+ }
+ }
+
+ if (!unregister) {
+ unregister = this.register(fire, ...args);
+ }
+
this.unregister.set(callback, unregister);
this.context.callOnClose(this);
+
+ // If this is a new listener for a persistent event, record
+ // the details for subsequent startups.
+ if (recordStartupData) {
+ let {module, event} = this.persistent;
+
+ if (!extension.startupData.persistentListeners) {
+ extension.startupData.persistentListeners = {};
+ }
+ if (!extension.startupData.persistentListeners[module]) {
+ extension.startupData.persistentListeners[module] = {};
+ }
+ if (!extension.startupData.persistentListeners[module][event]) {
+ extension.startupData.persistentListeners[module][event] = [];
+ }
+ extension.startupData.persistentListeners[module][event].push(args);
+
+ dump(`set startupData to ${JSON.stringify(extension.startupData)}\n`);
+
+ extension.saveStartupData();
+ }
}
removeListener(callback) {
if (!this.unregister.has(callback)) {
return;
}
let unregister = this.unregister.get(callback);
@@ -1839,16 +1976,18 @@ class EventManager {
try {
unregister();
} catch (e) {
Cu.reportError(e);
}
if (this.unregister.size == 0) {
this.context.forgetOnClose(this);
}
+
+ // XXX if persistent, remove from startupData
}
hasListener(callback) {
return this.unregister.has(callback);
}
revoke() {
for (let callback of this.unregister.keys()) {
--- a/toolkit/components/extensions/ext-backgroundPage.js
+++ b/toolkit/components/extensions/ext-backgroundPage.js
@@ -20,16 +20,17 @@ class BackgroundPage extends HiddenExten
if (this.page) {
this.url = this.extension.baseURI.resolve(this.page);
} else if (this.isGenerated) {
this.url = this.extension.baseURI.resolve("_generated_background_page.html");
}
}
async build() {
+ dump(`building background page for ${this.extension.id}\n`);
TelemetryStopwatch.start("WEBEXT_BACKGROUND_PAGE_LOAD_MS", this);
await this.createBrowserElement();
this.extension._backgroundPageFrameLoader = this.browser.frameLoader;
extensions.emit("extension-browser-inserted", this.browser);
this.browser.loadURIWithFlags(this.url, {triggeringPrincipal: this.extension.principal});
@@ -49,19 +50,31 @@ class BackgroundPage extends HiddenExten
shutdown() {
this.extension._backgroundPageFrameLoader = null;
super.shutdown();
}
}
this.backgroundPage = class extends ExtensionAPI {
onManifestEntry(entryName) {
- let {manifest} = this.extension;
+ let {extension} = this;
+ let {manifest} = extension;
+
+ this.bgPage = new BackgroundPage(extension, manifest.background);
+ if (extension.startupReason != "APP_STARTUP") {
+ this.bgPage.build();
+ return;
+ }
- this.bgPage = new BackgroundPage(this.extension, manifest.background);
+ EventManager.primeListeners(extension);
- return this.bgPage.build();
+ extension.once("start-background-page", async () => {
+ await this.bgPage.build();
+ EventManager.clearPrimedListeners(extension);
+ });
+
+ // XXX listen for an appropriate startup event and start background pages
}
onShutdown() {
this.bgPage.shutdown();
}
};
--- a/toolkit/components/extensions/ext-webRequest.js
+++ b/toolkit/components/extensions/ext-webRequest.js
@@ -5,105 +5,123 @@
// This file expectes tabTracker to be defined in the global scope (e.g.
// by ext-utils.js).
/* global tabTracker */
ChromeUtils.defineModuleGetter(this, "WebRequest",
"resource://gre/modules/WebRequest.jsm");
-// EventManager-like class specifically for WebRequest. Inherits from
-// EventManager. Takes care of converting |details| parameter
-// when invoking listeners.
-function WebRequestEventManager(context, eventName) {
- let name = `webRequest.${eventName}`;
- let register = (fire, filter, info) => {
- let listener = data => {
- let browserData = {tabId: -1, windowId: -1};
- if (data.browser) {
- browserData = tabTracker.getBrowserData(data.browser);
- }
- if (filter.tabId != null && browserData.tabId != filter.tabId) {
- return;
- }
- if (filter.windowId != null && browserData.windowId != filter.windowId) {
- return;
- }
+// The guts of a WebRequest event handler. Takes care of converting
+// |details| parameter when invoking listeners.
+function registerEvent(extension, eventName, fire, filter, info, tabParent = null) {
+ let listener = data => {
+ let browserData = {tabId: -1, windowId: -1};
+ if (data.browser) {
+ browserData = tabTracker.getBrowserData(data.browser);
+ }
+ if (filter.tabId != null && browserData.tabId != filter.tabId) {
+ return;
+ }
+ if (filter.windowId != null && browserData.windowId != filter.windowId) {
+ return;
+ }
+
+ let event = data.serialize(eventName);
+ event.tabId = browserData.tabId;
+
+ return fire.sync(event);
+ };
+
+ let filter2 = {};
+ if (filter.urls) {
+ let perms = new MatchPatternSet([...extension.whiteListedHosts.patterns,
+ ...extension.optionalOrigins.patterns]);
- let event = data.serialize(eventName);
- event.tabId = browserData.tabId;
-
- return fire.sync(event);
- };
+ filter2.urls = new MatchPatternSet(filter.urls);
- let filter2 = {};
- if (filter.urls) {
- let perms = new MatchPatternSet([...context.extension.whiteListedHosts.patterns,
- ...context.extension.optionalOrigins.patterns]);
+ if (!perms.overlapsAll(filter2.urls)) {
+ Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions.");
+ }
+ }
+ if (filter.types) {
+ filter2.types = filter.types;
+ }
+ if (filter.tabId) {
+ filter2.tabId = filter.tabId;
+ }
+ if (filter.windowId) {
+ filter2.windowId = filter.windowId;
+ }
- filter2.urls = new MatchPatternSet(filter.urls);
+ let blockingAllowed = extension.hasPermission("webRequestBlocking");
- if (!perms.overlapsAll(filter2.urls)) {
- Cu.reportError("The webRequest.addListener filter doesn't overlap with host permissions.");
+ let info2 = [];
+ if (info) {
+ for (let desc of info) {
+ if (desc == "blocking" && !blockingAllowed) {
+ Cu.reportError("Using webRequest.addListener with the blocking option " +
+ "requires the 'webRequestBlocking' permission.");
+ } else {
+ info2.push(desc);
}
}
- if (filter.types) {
- filter2.types = filter.types;
- }
- if (filter.tabId) {
- filter2.tabId = filter.tabId;
- }
- if (filter.windowId) {
- filter2.windowId = filter.windowId;
- }
-
- let blockingAllowed = context.extension.hasPermission("webRequestBlocking");
+ }
- let info2 = [];
- if (info) {
- for (let desc of info) {
- if (desc == "blocking" && !blockingAllowed) {
- Cu.reportError("Using webRequest.addListener with the blocking option " +
- "requires the 'webRequestBlocking' permission.");
- } else {
- info2.push(desc);
- }
- }
- }
-
- let listenerDetails = {
- addonId: context.extension.id,
- extension: context.extension.policy,
- blockingAllowed,
- tabParent: context.xulBrowser.frameLoader.tabParent,
- };
-
- WebRequest[eventName].addListener(
- listener, filter2, info2,
- listenerDetails);
- return () => {
- WebRequest[eventName].removeListener(listener);
- };
+ let listenerDetails = {
+ addonId: extension.id,
+ extension: extension.policy,
+ blockingAllowed,
};
- return new EventManager({context, name, register}).api();
+ if (tabParent) {
+ listenerDetails.tabParent = tabParent;
+ }
+
+ WebRequest[eventName].addListener(
+ listener, filter2, info2,
+ listenerDetails);
+
+ return {
+ unregister: () => { WebRequest[eventName].removeListener(listener); },
+ setFire: _fire => { fire = _fire; },
+ };
+}
+
+function makeWebRequestEvent(context, name) {
+ return new EventManager({
+ context,
+ name: `webRequest.${name}`,
+ persistent: {
+ module: "webRequest",
+ event: name,
+ },
+ register: (fire, filter, info) => {
+ return registerEvent(context.extension, name, fire, filter, info,
+ context.xulBrowser.frameLoader.tabParent).unregister;
+ },
+ }).api();
}
this.webRequest = class extends ExtensionAPI {
+ primeListener(extension, event, fire, params) {
+ return registerEvent(extension, event, fire, ...params);
+ }
+
getAPI(context) {
return {
webRequest: {
- onBeforeRequest: WebRequestEventManager(context, "onBeforeRequest"),
- onBeforeSendHeaders: WebRequestEventManager(context, "onBeforeSendHeaders"),
- onSendHeaders: WebRequestEventManager(context, "onSendHeaders"),
- onHeadersReceived: WebRequestEventManager(context, "onHeadersReceived"),
- onAuthRequired: WebRequestEventManager(context, "onAuthRequired"),
- onBeforeRedirect: WebRequestEventManager(context, "onBeforeRedirect"),
- onResponseStarted: WebRequestEventManager(context, "onResponseStarted"),
- onErrorOccurred: WebRequestEventManager(context, "onErrorOccurred"),
- onCompleted: WebRequestEventManager(context, "onCompleted"),
+ onBeforeRequest: makeWebRequestEvent(context, "onBeforeRequest"),
+ onBeforeSendHeaders: makeWebRequestEvent(context, "onBeforeSendHeaders"),
+ onSendHeaders: makeWebRequestEvent(context, "onSendHeaders"),
+ onHeadersReceived: makeWebRequestEvent(context, "onHeadersReceived"),
+ onAuthRequired: makeWebRequestEvent(context, "onAuthRequired"),
+ onBeforeRedirect: makeWebRequestEvent(context, "onBeforeRedirect"),
+ onResponseStarted: makeWebRequestEvent(context, "onResponseStarted"),
+ onErrorOccurred: makeWebRequestEvent(context, "onErrorOccurred"),
+ onCompleted: makeWebRequestEvent(context, "onCompleted"),
handlerBehaviorChanged: function() {
// TODO: Flush all caches.
},
},
};
}
};
--- a/toolkit/mozapps/extensions/AddonManager.jsm
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -3137,16 +3137,28 @@ var AddonManagerPrivate = {
return AddonManagerInternal._getProviderByName("XPIProvider")
.isTemporaryInstallID(extensionId);
},
isDBLoaded() {
let provider = AddonManagerInternal._getProviderByName("XPIProvider");
return provider ? provider.isDBLoaded : false;
},
+
+ /**
+ * XXX document me
+ */
+ setStartupData(aID, aData) {
+ if (!gStarted)
+ throw Components.Exception("AddonManager is not initialized",
+ Cr.NS_ERROR_NOT_INITIALIZED);
+
+ return AddonManagerInternal._getProviderByName("XPIProvider")
+ .setStartupData(aID, aData);
+ },
};
/**
* This is the public API that UI and developers should be calling. All methods
* just forward to AddonManagerInternal.
* @class
*/
var AddonManager = {
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -3619,16 +3619,26 @@ var XPIProvider = {
// Notify providers that a new theme has been enabled.
if (isTheme(addon.type))
AddonManagerPrivate.notifyAddonChanged(addon.id, addon.type, false);
return addon.wrapper;
},
/**
+ * XXX document me
+ */
+ setStartupData(aID, aData) {
+ dump(`write startup data ${JSON.stringify(aData)} for ${aID}\n`);
+ let state = XPIStates.findAddon(aID);
+ state.startupData = aData;
+ XPIStates.save();
+ },
+
+ /**
* Returns an Addon corresponding to an instance ID.
* @param aInstanceID
* An Addon Instance ID
* @return {Promise}
* @resolves The found Addon or null if no such add-on exists.
* @rejects Never
* @throws if the aInstanceID argument is not specified
*/