Bug 1316780: Part 1 - Proxy extension events between the parent and child process. r?aswan
MozReview-Commit-ID: 6820Fzoks8n
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -559,23 +559,31 @@ this.ExtensionData = class {
this.localeData.selectedLocale = locale;
return results[0];
}.bind(this));
}
};
let _browserUpdated = false;
+let nextId = 0;
+
+const PROXIED_EVENTS = new Set(["test-harness-message"]);
+
// We create one instance of this class per extension. |addonData|
// comes directly from bootstrap.js when initializing.
this.Extension = class extends ExtensionData {
constructor(addonData, startupReason) {
super(addonData.resourceURI);
this.uuid = UUIDMap.get(addonData.id);
+ this.instanceId = nextId++;
+
+ this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+ Services.ppmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
if (addonData.cleanupFile) {
Services.obs.addObserver(this, "xpcom-shutdown", false);
this.cleanupFile = addonData.cleanupFile || null;
delete addonData.cleanupFile;
}
this.addonData = addonData;
@@ -622,22 +630,32 @@ this.Extension = class extends Extension
on(hook, f) {
return this.emitter.on(hook, f);
}
off(hook, f) {
return this.emitter.off(hook, f);
}
- emit(...args) {
- return this.emitter.emit(...args);
+ emit(event, ...args) {
+ if (PROXIED_EVENTS.has(event)) {
+ Services.ppmm.broadcastAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
+ }
+
+ return this.emitter.emit(event, ...args);
+ }
+
+ receiveMessage({name, data}) {
+ if (name === this.MESSAGE_EMIT_EVENT) {
+ this.emitter.emit(data.event, ...data.args);
+ }
}
testMessage(...args) {
- Management.emit("test-message", this, ...args);
+ this.emit("test-harness-message", ...args);
}
createPrincipal(uri = this.baseURI) {
return Services.scriptSecurityManager.createCodebasePrincipal(
uri, {addonId: this.id});
}
// Checks that the given URL is a child of our baseURI.
@@ -669,16 +687,17 @@ this.Extension = class extends Extension
// Representation of the extension to send to content
// processes. This should include anything the content process might
// need.
serialize() {
return {
id: this.id,
uuid: this.uuid,
+ instanceId: this.instanceId,
manifest: this.manifest,
resourceURL: this.addonData.resourceURI.spec,
baseURL: this.baseURI.spec,
content_scripts: this.manifest.content_scripts || [], // eslint-disable-line camelcase
webAccessibleResources: this.webAccessibleResources.serialize(),
whiteListedHosts: this.whiteListedHosts.serialize(),
localeData: this.localeData.serialize(),
permissions: this.permissions,
@@ -825,16 +844,19 @@ this.Extension = class extends Extension
// child processes (including the parent) to flush their JAR
// caches. These caches may keep the file open.
file.remove(false);
});
}
shutdown() {
this.hasShutdown = true;
+
+ Services.ppmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
if (!this.manifest) {
ExtensionManagement.shutdownExtension(this.uuid);
this.cleanupGeneratedFile();
return;
}
GlobalManager.uninit(this);
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -44,16 +44,17 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "WebNavigationFrames",
"resource://gre/modules/WebNavigationFrames.jsm");
Cu.import("resource://gre/modules/ExtensionChild.jsm");
Cu.import("resource://gre/modules/ExtensionCommon.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
+ EventEmitter,
LocaleData,
defineLazyGetter,
flushJarCache,
getInnerWindowID,
promiseDocumentReady,
runSafeSyncWithoutClone,
} = ExtensionUtils;
@@ -758,65 +759,86 @@ DocumentManager = {
for (let context of contexts.values()) {
context.triggerScripts(when);
}
}
},
};
// Represents a browser extension in the content process.
-function BrowserExtensionContent(data) {
- this.id = data.id;
- this.uuid = data.uuid;
- this.data = data;
- this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
- this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
- this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
- this.permissions = data.permissions;
- this.principal = data.principal;
+class BrowserExtensionContent extends EventEmitter {
+ constructor(data) {
+ super();
+
+ this.id = data.id;
+ this.uuid = data.uuid;
+ this.data = data;
+ this.instanceId = data.instanceId;
- this.localeData = new LocaleData(data.localeData);
+ this.MESSAGE_EMIT_EVENT = `Extension:EmitEvent:${this.instanceId}`;
+ Services.cpmm.addMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
+ this.scripts = data.content_scripts.map(scriptData => new Script(this, scriptData));
+ this.webAccessibleResources = new MatchGlobs(data.webAccessibleResources);
+ this.whiteListedHosts = new MatchPattern(data.whiteListedHosts);
+ this.permissions = data.permissions;
+ this.principal = data.principal;
- this.manifest = data.manifest;
- this.baseURI = Services.io.newURI(data.baseURL, null, null);
+ this.localeData = new LocaleData(data.localeData);
+
+ this.manifest = data.manifest;
+ this.baseURI = Services.io.newURI(data.baseURL, null, null);
- // Only used in addon processes.
- this.views = new Set();
+ // Only used in addon processes.
+ this.views = new Set();
- let uri = Services.io.newURI(data.resourceURL, null, null);
+ let uri = Services.io.newURI(data.resourceURL, null, null);
- if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
- // Extension.jsm takes care of this in the parent.
- ExtensionManagement.startupExtension(this.uuid, uri, this);
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ // Extension.jsm takes care of this in the parent.
+ ExtensionManagement.startupExtension(this.uuid, uri, this);
+ }
}
-}
-BrowserExtensionContent.prototype = {
shutdown() {
+ Services.cpmm.removeMessageListener(this.MESSAGE_EMIT_EVENT, this);
+
if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
ExtensionManagement.shutdownExtension(this.uuid);
}
- },
+ }
+
+ emit(event, ...args) {
+ Services.cpmm.sendAsyncMessage(this.MESSAGE_EMIT_EVENT, {event, args});
+
+ super.emit(event, ...args);
+ }
+
+ receiveMessage({name, data}) {
+ if (name === this.MESSAGE_EMIT_EVENT) {
+ super.emit(data.event, ...data.args);
+ }
+ }
localizeMessage(...args) {
return this.localeData.localizeMessage(...args);
- },
+ }
localize(...args) {
return this.localeData.localize(...args);
- },
+ }
hasPermission(perm) {
let match = /^manifest:(.*)/.exec(perm);
if (match) {
return this.manifest[match[1]] != null;
}
return this.permissions.has(perm);
- },
-};
+ }
+}
ExtensionManager = {
// Map[extensionId, BrowserExtensionContent]
extensions: new Map(),
init() {
Schemas.init();
ExtensionChild.initOnce();
@@ -838,26 +860,31 @@ ExtensionManager = {
return this.extensions.get(extensionId);
},
receiveMessage({name, data}) {
let extension;
switch (name) {
case "Extension:Startup": {
extension = new BrowserExtensionContent(data);
+
this.extensions.set(data.id, extension);
+
DocumentManager.startupExtension(data.id);
+
Services.cpmm.sendAsyncMessage("Extension:StartupComplete");
break;
}
case "Extension:Shutdown": {
extension = this.extensions.get(data.id);
extension.shutdown();
+
DocumentManager.shutdownExtension(data.id);
+
this.extensions.delete(data.id);
break;
}
case "Extension:FlushJarCache": {
let nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
"initWithPath");
let file = new nsIFile(data.path);
--- a/toolkit/components/extensions/ext-test.js
+++ b/toolkit/components/extensions/ext-test.js
@@ -1,35 +1,15 @@
"use strict";
Components.utils.import("resource://gre/modules/ExtensionUtils.jsm");
var {
EventManager,
} = ExtensionUtils;
-// WeakMap[Extension -> Set(callback)]
-var messageHandlers = new WeakMap();
-
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("startup", (type, extension) => {
- messageHandlers.set(extension, new Set());
-});
-
-extensions.on("shutdown", (type, extension) => {
- messageHandlers.delete(extension);
-});
-
-extensions.on("test-message", (type, extension, ...args) => {
- let handlers = messageHandlers.get(extension);
- for (let handler of handlers) {
- handler(...args);
- }
-});
-/* eslint-enable mozilla/balanced-listeners */
-
function makeTestAPI(context) {
let {extension} = context;
return {
test: {
sendMessage: function(...args) {
extension.emit("test-message", ...args);
},
@@ -67,20 +47,22 @@ function makeTestAPI(context) {
actual += "";
if (!equal && expected === actual) {
actual += " (different)";
}
extension.emit("test-eq", equal, String(msg), expected, actual);
},
onMessage: new EventManager(context, "test.onMessage", fire => {
- let handlers = messageHandlers.get(extension);
- handlers.add(fire);
+ let handler = (event, ...args) => {
+ context.runSafe(fire, ...args);
+ };
+ extension.on("test-harness-message", handler);
return () => {
- handlers.delete(fire);
+ extension.off("test-harness-message", handler);
};
}).api(),
},
};
}
extensions.registerSchemaAPI("test", "addon_parent", makeTestAPI);
extensions.registerSchemaAPI("test", "content_parent", makeTestAPI);