Bug 1316780: Part 1 - Proxy extension events between the parent and child process. r?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 10 Nov 2016 18:39:49 -0800
changeset 437583 266991c11385ca8517be3744dd0e3818fa2e1a02
parent 437582 061625130e4c71de83a9f27c3770a650a4a54905
child 437584 36c5a5c4c3f045bd4dbfda9c2fc07328c0b088de
child 437849 9ed0b006c6d3b4d110364a8ffd4c635b52e60b2a
push id35454
push usermaglione.k@gmail.com
push dateFri, 11 Nov 2016 04:34:52 +0000
reviewersaswan
bugs1316780
milestone52.0a1
Bug 1316780: Part 1 - Proxy extension events between the parent and child process. r?aswan MozReview-Commit-ID: 6820Fzoks8n
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/ext-test.js
--- 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);