Bug 1068087: Add a simple mechanism for content pages to communicate with chrome. r=mconley draft
authorDave Townsend <dtownsend@oxymoronical.com>
Tue, 10 Mar 2015 09:19:17 -0700
changeset 249418 f765e9c3bf554a921955f4c7dfe806cfc89fdc7e
parent 249417 e3e2dfaf9037a0bf5e98a102d1fa6e824602508c
child 249419 02e6199e5e8d416067317612270132fe791fd2f8
push id975
push userdtownsend@mozilla.com
push dateTue, 10 Mar 2015 20:43:12 +0000
reviewersmconley
bugs1068087
milestone39.0a1
Bug 1068087: Add a simple mechanism for content pages to communicate with chrome. r=mconley
browser/base/content/tabbrowser.xml
toolkit/components/processsingleton/MainProcessSingleton.js
toolkit/content/jar.mn
toolkit/content/process-content.js
toolkit/content/widgets/browser.xml
toolkit/modules/RemotePageManager.jsm
toolkit/modules/Services.jsm
toolkit/modules/moz.build
toolkit/modules/tests/browser/browser.ini
toolkit/modules/tests/browser/browser_RemotePageManager.js
toolkit/modules/tests/browser/testremotepagemanager.html
--- a/browser/base/content/tabbrowser.xml
+++ b/browser/base/content/tabbrowser.xml
@@ -2470,22 +2470,16 @@
             let tabListener = this.mTabListeners[index];
             let ourBrowser = this.getBrowserForTab(aOurTab);
             ourBrowser.webProgress.removeProgressListener(filter);
             filter.removeProgressListener(tabListener);
 
             // Make sure to unregister any open URIs.
             this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
 
-            // Give others a chance to swap state.
-            let event = new CustomEvent("SwapDocShells", {"detail": aOtherBrowser});
-            ourBrowser.dispatchEvent(event);
-            event = new CustomEvent("SwapDocShells", {"detail": ourBrowser});
-            aOtherBrowser.dispatchEvent(event);
-
             // Swap the docshells
             ourBrowser.swapDocShells(aOtherBrowser);
 
             // Restore the progress listener
             this.mTabListeners[index] = tabListener =
               this.mTabProgressListener(aOurTab, ourBrowser, false);
 
             const notifyAll = Ci.nsIWebProgress.NOTIFY_ALL;
--- a/toolkit/components/processsingleton/MainProcessSingleton.js
+++ b/toolkit/components/processsingleton/MainProcessSingleton.js
@@ -4,27 +4,26 @@
 
 "use strict";
 
 const { utils: Cu, interfaces: Ci, classes: Cc, results: Cr } = Components;
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
-                                   "@mozilla.org/parentprocessmessagemanager;1",
-                                   "nsIMessageListenerManager");
-
-XPCOMUtils.defineLazyServiceGetter(this, "globalmm",
-                                   "@mozilla.org/globalmessagemanager;1",
-                                   "nsIMessageBroadcaster");
-
 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
                                   "resource://gre/modules/NetUtil.jsm");
 
+// Temporary workaround for bug 1141661
+function convertURL(url) {
+  let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"].
+                       getService(Ci.nsIChromeRegistry);
+  return chromeRegistry.convertChromeURL(Services.io.newURI(url, null, null)).spec;
+}
+
 function MainProcessSingleton() {}
 MainProcessSingleton.prototype = {
   classID: Components.ID("{0636a680-45cb-11e4-916c-0800200c9a66}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   logConsoleMessage: function(message) {
     let logMsg = message.data;
@@ -80,23 +79,24 @@ MainProcessSingleton.prototype = {
 
   observe: function(subject, topic, data) {
     switch (topic) {
     case "app-startup": {
       Services.obs.addObserver(this, "xpcom-shutdown", false);
 
       // Load this script early so that console.* is initialized
       // before other frame scripts.
-      globalmm.loadFrameScript("chrome://global/content/browser-content.js", true);
-      ppmm.addMessageListener("Console:Log", this.logConsoleMessage);
-      globalmm.addMessageListener("Search:AddEngine", this.addSearchEngine);
+      Services.mm.loadFrameScript("chrome://global/content/browser-content.js", true);
+      Services.ppmm.loadProcessScript(convertURL("chrome://global/content/process-content.js"), true);
+      Services.ppmm.addMessageListener("Console:Log", this.logConsoleMessage);
+      Services.mm.addMessageListener("Search:AddEngine", this.addSearchEngine);
       break;
     }
 
     case "xpcom-shutdown":
-      ppmm.removeMessageListener("Console:Log", this.logConsoleMessage);
-      globalmm.removeMessageListener("Search:AddEngine", this.addSearchEngine);
+      Services.ppmm.removeMessageListener("Console:Log", this.logConsoleMessage);
+      Services.mm.removeMessageListener("Search:AddEngine", this.addSearchEngine);
       break;
     }
   },
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([MainProcessSingleton]);
--- a/toolkit/content/jar.mn
+++ b/toolkit/content/jar.mn
@@ -43,16 +43,17 @@ toolkit.jar:
 *+ content/global/editMenuOverlay.xul         (editMenuOverlay.xul)
    content/global/finddialog.js               (finddialog.js)
 *+ content/global/finddialog.xul              (finddialog.xul)
    content/global/findUtils.js                (findUtils.js)
    content/global/filepicker.properties       (filepicker.properties)
 *+ content/global/globalOverlay.js            (globalOverlay.js)
 +  content/global/mozilla.xhtml               (mozilla.xhtml)
    content/global/nsDragAndDrop.js            (nsDragAndDrop.js)
+   content/global/process-content.js          (process-content.js)
    content/global/resetProfile.css            (resetProfile.css)
    content/global/resetProfile.js             (resetProfile.js)
    content/global/resetProfile.xul            (resetProfile.xul)
    content/global/resetProfileProgress.xul    (resetProfileProgress.xul)
    content/global/select-child.js             (select-child.js)
    content/global/treeUtils.js                (treeUtils.js)
    content/global/viewZoomOverlay.js          (viewZoomOverlay.js)
 *+ content/global/bindings/autocomplete.xml    (widgets/autocomplete.xml)
new file mode 100644
--- /dev/null
+++ b/toolkit/content/process-content.js
@@ -0,0 +1,12 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+let { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+// Creates a new PageListener for this process. This will listen for page loads
+// and for those that match URLs provided by the parent process will set up
+// a dedicated message port and notify the parent process.
+Cu.import("resource://gre/modules/RemotePageManager.jsm");
--- a/toolkit/content/widgets/browser.xml
+++ b/toolkit/content/widgets/browser.xml
@@ -1080,16 +1080,22 @@
 
       <method name="swapDocShells">
         <parameter name="aOtherBrowser"/>
         <body>
         <![CDATA[
           if (this.isRemoteBrowser != aOtherBrowser.isRemoteBrowser)
             throw new Error("Can only swap docshells between browsers in the same process.");
 
+          // Give others a chance to swap state.
+          let event = new CustomEvent("SwapDocShells", {"detail": aOtherBrowser});
+          this.dispatchEvent(event);
+          event = new CustomEvent("SwapDocShells", {"detail": this});
+          aOtherBrowser.dispatchEvent(event);
+
           // We need to swap fields that are tied to our docshell or related to
           // the loaded page
           // Fields which are built as a result of notifactions (pageshow/hide,
           // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
           // because these notifications are dispatched again once the docshells
           // are swapped.
           var fieldsToSwap = [
             "_docShell",
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/RemotePageManager.jsm
@@ -0,0 +1,521 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["RemotePages", "RemotePageManager", "PageListener"];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function MessageListener() {
+  this.listeners = new Map();
+}
+
+MessageListener.prototype = {
+  keys: function() {
+    return this.listeners.keys();
+  },
+
+  has: function(name) {
+    return this.listeners.has(name);
+  },
+
+  callListeners: function(message) {
+    let listeners = this.listeners.get(message.name);
+    if (!listeners) {
+      return;
+    }
+
+    for (let listener of listeners.values()) {
+      try {
+        listener(message);
+      }
+      catch (e) {
+        Cu.reportError(e);
+      }
+    }
+  },
+
+  addMessageListener: function(name, callback) {
+    if (!this.listeners.has(name))
+      this.listeners.set(name, new Set([callback]));
+    else
+      this.listeners.get(name).add(callback);
+  },
+
+  removeMessageListener: function(name, callback) {
+    if (!this.listeners.has(name))
+      return;
+
+    this.listeners.get(name).delete(callback);
+  },
+}
+
+
+/**
+ * Creates a RemotePages object which listens for new remote pages of a
+ * particular URL. A "RemotePage:Init" message will be dispatched to this object
+ * for every page loaded. Message listeners added to this object receive
+ * messages from all loaded pages from the requested url.
+ */
+this.RemotePages = function(url) {
+  this.url = url;
+  this.messagePorts = new Set();
+  this.listener = new MessageListener();
+  this.destroyed = false;
+
+  RemotePageManager.addRemotePageListener(url, this.portCreated.bind(this));
+  this.portMessageReceived = this.portMessageReceived.bind(this);
+}
+
+RemotePages.prototype = {
+  url: null,
+  messagePorts: null,
+  listener: null,
+  destroyed: null,
+
+  destroy: function() {
+    RemotePageManager.removeRemotePageListener(this.url);
+
+    for (let port of this.messagePorts.values()) {
+      this.removeMessagePort(port);
+    }
+
+    this.messagePorts = null;
+    this.listener = null;
+    this.destroyed = true;
+  },
+
+  // Called when a page matching the url has loaded in a frame.
+  portCreated: function(port) {
+    this.messagePorts.add(port);
+
+    port.addMessageListener("RemotePage:Unload", this.portMessageReceived);
+
+    for (let name of this.listener.keys()) {
+      this.registerPortListener(port, name);
+    }
+
+    this.listener.callListeners({ target: port, name: "RemotePage:Init" });
+  },
+
+  // A message has been received from one of the pages
+  portMessageReceived: function(message) {
+    this.listener.callListeners(message);
+
+    if (message.name == "RemotePage:Unload")
+      this.removeMessagePort(message.target);
+  },
+
+  // A page has closed
+  removeMessagePort: function(port) {
+    for (let name of this.listener.keys()) {
+      port.removeMessageListener(name, this.portMessageReceived);
+    }
+
+    port.removeMessageListener("RemotePage:Unload", this.portMessageReceived);
+    this.messagePorts.delete(port);
+  },
+
+  registerPortListener: function(port, name) {
+    port.addMessageListener(name, this.portMessageReceived);
+  },
+
+  // Sends a message to all known pages
+  sendAsyncMessage: function(name, data = null) {
+    for (let port of this.messagePorts.values()) {
+      port.sendAsyncMessage(name, data);
+    }
+  },
+
+  addMessageListener: function(name, callback) {
+    if (this.destroyed) {
+      throw new Error("RemotePages has been destroyed");
+    }
+
+    if (!this.listener.has(name)) {
+      for (let port of this.messagePorts.values()) {
+        this.registerPortListener(port, name)
+      }
+    }
+
+    this.listener.addMessageListener(name, callback);
+  },
+
+  removeMessageListener: function(name, callback) {
+    if (this.destroyed) {
+      throw new Error("RemotePages has been destroyed");
+    }
+
+    this.listener.removeMessageListener(name, callback);
+  },
+};
+
+
+// Only exposes the public properties of the MessagePort
+function publicMessagePort(port) {
+  let properties = ["addMessageListener", "removeMessageListener",
+                    "sendAsyncMessage", "destroy"];
+
+  let clean = {};
+  for (let property of properties) {
+    clean[property] = port[property].bind(port);
+  }
+
+  if (port instanceof ChromeMessagePort) {
+    Object.defineProperty(clean, "browser", {
+      get: function() {
+        return port.browser;
+      }
+    });
+  }
+
+  return clean;
+}
+
+
+/*
+ * A message port sits on each side of the process boundary for every remote
+ * page. Each has a port ID that is unique to the message manager it talks
+ * through.
+ *
+ * We roughly implement the same contract as nsIMessageSender and
+ * nsIMessageListenerManager
+ */
+function MessagePort(messageManager, portID) {
+  this.messageManager = messageManager;
+  this.portID = portID;
+  this.destroyed = false;
+  this.listener = new MessageListener();
+
+  this.message = this.message.bind(this);
+  this.messageManager.addMessageListener("RemotePage:Message", this.message);
+}
+
+MessagePort.prototype = {
+  messageManager: null,
+  portID: null,
+  destroyed: null,
+  listener: null,
+  _browser: null,
+  remotePort: null,
+
+  // Called when the message manager used to connect to the other process has
+  // changed, i.e. when a tab is detached.
+  swapMessageManager: function(messageManager) {
+    this.messageManager.removeMessageListener("RemotePage:Message", this.message);
+
+    this.messageManager = messageManager;
+
+    this.messageManager.addMessageListener("RemotePage:Message", this.message);
+  },
+
+  /* Adds a listener for messages. Many callbacks can be registered for the
+   * same message if necessary. An attempt to register the same callback for the
+   * same message twice will be ignored. When called the callback is passed an
+   * object with these properties:
+   *   target: This message port
+   *   name:   The message name
+   *   data:   Any data sent with the message
+   */
+  addMessageListener: function(name, callback) {
+    if (this.destroyed) {
+      throw new Error("Message port has been destroyed");
+    }
+
+    this.listener.addMessageListener(name, callback);
+  },
+
+  /*
+   * Removes a listener for messages.
+   */
+  removeMessageListener: function(name, callback) {
+    if (this.destroyed) {
+      throw new Error("Message port has been destroyed");
+    }
+
+    this.listener.removeMessageListener(name, callback);
+  },
+
+  // Sends a message asynchronously to the other process
+  sendAsyncMessage: function(name, data = null) {
+    if (this.destroyed) {
+      throw new Error("Message port has been destroyed");
+    }
+
+    this.messageManager.sendAsyncMessage("RemotePage:Message", {
+      portID: this.portID,
+      name: name,
+      data: data,
+    });
+  },
+
+  // Called to destroy this port
+  destroy: function() {
+    try {
+      // This can fail in the child process if the tab has already been closed
+      this.messageManager.removeMessageListener("RemotePage:Message", this.message);
+    }
+    catch (e) { }
+    this.messageManager = null;
+    this.destroyed = true;
+    this.portID = null;
+    this.listener = null;
+  },
+};
+
+
+// The chome side of a message port
+function ChromeMessagePort(browser, portID) {
+  MessagePort.call(this, browser.messageManager, portID);
+
+  this._browser = browser;
+  this._permanentKey = browser.permanentKey;
+
+  Services.obs.addObserver(this, "message-manager-disconnect", false);
+  this.publicPort = publicMessagePort(this);
+
+  this.swapBrowsers = this.swapBrowsers.bind(this);
+  this._browser.addEventListener("SwapDocShells", this.swapBrowsers, false);
+}
+
+ChromeMessagePort.prototype = Object.create(MessagePort.prototype);
+
+Object.defineProperty(ChromeMessagePort.prototype, "browser", {
+  get: function() {
+    return this._browser;
+  }
+});
+
+// Called when the docshell is being swapped with another browser. We have to
+// update to use the new browser's message manager
+ChromeMessagePort.prototype.swapBrowsers = function({ detail: newBrowser }) {
+  // We can see this event for the new browser before the swap completes so
+  // check that the browser we're tracking has our permanentKey.
+  if (this._browser.permanentKey != this._permanentKey)
+    return;
+
+  this._browser.removeEventListener("SwapDocShells", this.swapBrowsers, false);
+
+  this._browser = newBrowser;
+  this.swapMessageManager(newBrowser.messageManager);
+
+  this._browser.addEventListener("SwapDocShells", this.swapBrowsers, false);
+}
+
+// Called when a message manager has been disconnected indicating that the
+// tab has closed or crashed
+ChromeMessagePort.prototype.observe = function(messageManager) {
+  if (messageManager != this.messageManager)
+    return;
+
+  this.listener.callListeners({
+    target: this.publicPort,
+    name: "RemotePage:Unload",
+    data: null,
+  });
+  this.destroy();
+};
+
+// Called when a message is received from the message manager. This could
+// have come from any port in the message manager so verify the port ID.
+ChromeMessagePort.prototype.message = function({ data: messagedata }) {
+  if (this.destroyed || (messagedata.portID != this.portID)) {
+    return;
+  }
+
+  let message = {
+    target: this.publicPort,
+    name: messagedata.name,
+    data: messagedata.data,
+  };
+  this.listener.callListeners(message);
+
+  if (messagedata.name == "RemotePage:Unload")
+    this.destroy();
+};
+
+ChromeMessagePort.prototype.destroy = function() {
+  this._browser.removeEventListener("SwapDocShells", this.swapBrowsers, false);
+  this._browser = null;
+  Services.obs.removeObserver(this, "message-manager-disconnect");
+  MessagePort.prototype.destroy.call(this);
+};
+
+
+// The content side of a message port
+function ChildMessagePort(contentFrame, window) {
+  let portID = Services.appinfo.processID + ":" + ChildMessagePort.prototype.nextPortID++;
+  MessagePort.call(this, contentFrame, portID);
+
+  this.window = window;
+
+  // Add functionality to the content page
+  Cu.exportFunction(this.sendAsyncMessage.bind(this), window, {
+    defineAs: "sendAsyncMessage",
+  });
+  Cu.exportFunction(this.addMessageListener.bind(this), window, {
+    defineAs: "addMessageListener",
+    allowCallbacks: true,
+  });
+  Cu.exportFunction(this.removeMessageListener.bind(this), window, {
+    defineAs: "removeMessageListener",
+    allowCallbacks: true,
+  });
+
+  // Send a message for load events
+  let loadListener = () => {
+    this.sendAsyncMessage("RemotePage:Load");
+    window.removeEventListener("load", loadListener, false);
+  };
+  window.addEventListener("load", loadListener, false);
+
+  // Destroy the port when the window is unloaded
+  window.addEventListener("unload", () => {
+    try {
+      this.sendAsyncMessage("RemotePage:Unload");
+    }
+    catch (e) {
+      // If the tab has been closed the frame message manager has already been
+      // destroyed
+    }
+    this.destroy();
+  }, false);
+
+  // Tell the main process to set up its side of the message pipe.
+  this.messageManager.sendAsyncMessage("RemotePage:InitPort", {
+    portID: portID,
+    url: window.location.toString(),
+  });
+}
+
+ChildMessagePort.prototype = Object.create(MessagePort.prototype);
+
+ChildMessagePort.prototype.nextPortID = 0;
+
+// Called when a message is received from the message manager. This could
+// have come from any port in the message manager so verify the port ID.
+ChildMessagePort.prototype.message = function({ data: messagedata }) {
+  if (this.destroyed || (messagedata.portID != this.portID)) {
+    return;
+  }
+
+  let message = {
+    name: messagedata.name,
+    data: messagedata.data,
+  };
+  this.listener.callListeners(Cu.cloneInto(message, this.window));
+};
+
+ChildMessagePort.prototype.destroy = function() {
+  this.window = null;
+  MessagePort.prototype.destroy.call(this);
+}
+
+// Allows callers to register to connect to specific content pages. Registration
+// is done through the addRemotePageListener method
+let RemotePageManagerInternal = {
+  // The currently registered remote pages
+  pages: new Map(),
+
+  // Initialises all the needed listeners
+  init: function() {
+    Services.ppmm.addMessageListener("RemotePage:InitListener", this.initListener.bind(this));
+    Services.mm.addMessageListener("RemotePage:InitPort", this.initPort.bind(this));
+  },
+
+  // Registers interest in a remote page. A callback is called with a port for
+  // the new page when loading begins (i.e. the page hasn't actually loaded yet).
+  // Only one callback can be registered per URL.
+  addRemotePageListener: function(url, callback) {
+    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
+      throw new Error("RemotePageManager can only be used in the main process.");
+
+    if (this.pages.has(url)) {
+      throw new Error("Remote page already registered: " + url);
+    }
+
+    this.pages.set(url, callback);
+
+    // Notify all the frame scripts of the new registration
+    Services.ppmm.broadcastAsyncMessage("RemotePage:Register", { urls: [url] });
+  },
+
+  // Removes any interest in a remote page.
+  removeRemotePageListener: function(url) {
+    if (Services.appinfo.processType != Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
+      throw new Error("RemotePageManager can only be used in the main process.");
+
+    if (!this.pages.has(url)) {
+      throw new Error("Remote page is not registered: " + url);
+    }
+
+    // Notify all the frame scripts of the removed registration
+    Services.ppmm.broadcastAsyncMessage("RemotePage:Unregister", { urls: [url] });
+    this.pages.delete(url);
+  },
+
+  // A listener is requesting the list of currently registered urls
+  initListener: function({ target: messageManager }) {
+    messageManager.sendAsyncMessage("RemotePage:Register", { urls: [u for (u of this.pages.keys())] })
+  },
+
+  // A remote page has been created and a port is ready in the content side
+  initPort: function({ target: browser, data: { url, portID } }) {
+    let callback = this.pages.get(url);
+    if (!callback) {
+      Cu.reportError("Unexpected remote page load: " + url);
+      return;
+    }
+
+    let port = new ChromeMessagePort(browser, portID);
+    callback(port.publicPort);
+  }
+};
+
+if (Services.appinfo.processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT)
+  RemotePageManagerInternal.init();
+
+// The public API for the above object
+this.RemotePageManager = {
+  addRemotePageListener: RemotePageManagerInternal.addRemotePageListener.bind(RemotePageManagerInternal),
+  removeRemotePageListener: RemotePageManagerInternal.removeRemotePageListener.bind(RemotePageManagerInternal),
+};
+
+// Listen for pages in any process we're loaded in
+let registeredURLs = new Set();
+
+let observer = (window) => {
+  let url = window.location.toString();
+  if (!registeredURLs.has(url))
+    return;
+
+  // Get the frame message manager for this window so we can associate this
+  // page with a browser element
+  let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDocShell)
+                             .QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIContentFrameMessageManager);
+  // Set up the child side of the message port
+  let port = new ChildMessagePort(messageManager, window);
+};
+Services.obs.addObserver(observer, "chrome-document-global-created", false);
+Services.obs.addObserver(observer, "content-document-global-created", false);
+
+// A message from chrome telling us what pages to listen for
+Services.cpmm.addMessageListener("RemotePage:Register", ({ data }) => {
+  for (let url of data.urls)
+    registeredURLs.add(url);
+});
+
+// A message from chrome telling us what pages to stop listening for
+Services.cpmm.addMessageListener("RemotePage:Unregister", ({ data }) => {
+  for (let url of data.urls)
+    registeredURLs.delete(url);
+});
+
+Services.cpmm.sendAsyncMessage("RemotePage:InitListener");
--- a/toolkit/modules/Services.jsm
+++ b/toolkit/modules/Services.jsm
@@ -40,23 +40,36 @@ XPCOMUtils.defineLazyGetter(Services, "d
 XPCOMUtils.defineLazyGetter(Services, "crashmanager", () => {
   let ns = {};
   Components.utils.import("resource://gre/modules/CrashManager.jsm", ns);
 
   return ns.CrashManager.Singleton;
 });
 #endif
 
+XPCOMUtils.defineLazyGetter(Services, "mm", () => {
+  return Cc["@mozilla.org/globalmessagemanager;1"]
+           .getService(Ci.nsIMessageBroadcaster)
+           .QueryInterface(Ci.nsIFrameScriptLoader);
+});
+
+XPCOMUtils.defineLazyGetter(Services, "ppmm", () => {
+  return Cc["@mozilla.org/parentprocessmessagemanager;1"]
+           .getService(Ci.nsIMessageBroadcaster)
+           .QueryInterface(Ci.nsIProcessScriptLoader);
+});
+
 let initTable = [
 #ifdef MOZ_WIDGET_ANDROID
   ["androidBridge", "@mozilla.org/android/bridge;1", "nsIAndroidBridge"],
 #endif
   ["appShell", "@mozilla.org/appshell/appShellService;1", "nsIAppShellService"],
   ["cache", "@mozilla.org/network/cache-service;1", "nsICacheService"],
   ["cache2", "@mozilla.org/netwerk/cache-storage-service;1", "nsICacheStorageService"],
+  ["cpmm", "@mozilla.org/childprocessmessagemanager;1", "nsIMessageSender"],
   ["console", "@mozilla.org/consoleservice;1", "nsIConsoleService"],
   ["contentPrefs", "@mozilla.org/content-pref/service;1", "nsIContentPrefService"],
   ["cookies", "@mozilla.org/cookiemanager;1", "nsICookieManager2"],
   ["downloads", "@mozilla.org/download-manager;1", "nsIDownloadManager"],
   ["droppedLinkHandler", "@mozilla.org/content/dropped-link-handler;1", "nsIDroppedLinkHandler"],
   ["eTLD", "@mozilla.org/network/effective-tld-service;1", "nsIEffectiveTLDService"],
   ["io", "@mozilla.org/network/io-service;1", "nsIIOService2"],
   ["locale", "@mozilla.org/intl/nslocaleservice;1", "nsILocaleService"],
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -38,16 +38,17 @@ EXTRA_JS_MODULES += [
     'PrivateBrowsingUtils.jsm',
     'ProfileAge.jsm',
     'Promise-backend.js',
     'Promise.jsm',
     'PromiseUtils.jsm',
     'PropertyListUtils.jsm',
     'RemoteController.jsm',
     'RemoteFinder.jsm',
+    'RemotePageManager.jsm',
     'RemoteSecurityUI.jsm',
     'RemoteWebNavigation.jsm',
     'RemoteWebProgress.jsm',
     'secondscreen/SimpleServiceDiscovery.jsm',
     'SelectContentHelper.jsm',
     'SelectParentHelper.jsm',
     'sessionstore/FormData.jsm',
     'sessionstore/ScrollPosition.jsm',
--- a/toolkit/modules/tests/browser/browser.ini
+++ b/toolkit/modules/tests/browser/browser.ini
@@ -1,14 +1,16 @@
 [DEFAULT]
 support-files =
   dummy_page.html
   metadata_*.html
+  testremotepagemanager.html
 
 [browser_Battery.js]
 [browser_Deprecated.js]
 [browser_Finder.js]
 skip-if = e10s # Bug ?????? - test already uses content scripts, but still fails only under e10s.
 [browser_Geometry.js]
 [browser_InlineSpellChecker.js]
 [browser_PageMetadata.js]
+[browser_RemotePageManager.js]
 [browser_RemoteWebNavigation.js]
 [browser_Troubleshoot.js]
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/browser/browser_RemotePageManager.js
@@ -0,0 +1,379 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_URL = "http://www.example.com/browser/toolkit/modules/tests/browser/testremotepagemanager.html";
+
+let { RemotePages, RemotePageManager } = Cu.import("resource://gre/modules/RemotePageManager.jsm", {});
+
+function failOnMessage(message) {
+  ok(false, "Should not have seen message " + message.name);
+}
+
+function waitForMessage(port, message, expectedPort = port) {
+  return new Promise((resolve) => {
+    function listener(message) {
+      is(message.target, expectedPort, "Message should be from the right port.");
+
+      port.removeMessageListener(listener);
+      resolve(message);
+    }
+
+    port.addMessageListener(message, listener);
+  });
+}
+
+function waitForPort(url, createTab = true) {
+  return new Promise((resolve) => {
+    RemotePageManager.addRemotePageListener(url, (port) => {
+      RemotePageManager.removeRemotePageListener(url);
+
+      waitForMessage(port, "RemotePage:Load").then(() => resolve(port));
+    });
+
+    if (createTab)
+      gBrowser.selectedTab = gBrowser.addTab(url);
+  });
+}
+
+function waitForPage(pages) {
+  return new Promise((resolve) => {
+    function listener({ target }) {
+      pages.removeMessageListener("RemotePage:Init", listener);
+
+      waitForMessage(target, "RemotePage:Load").then(() => resolve(target));
+    }
+
+    pages.addMessageListener("RemotePage:Init", listener);
+    gBrowser.selectedTab = gBrowser.addTab(TEST_URL);
+  });
+}
+
+// Test that opening a page creates a port, sends the load event and then
+// navigating to a new page sends the unload event. Going back should create a
+// new port
+add_task(function* init_navigate() {
+  let port = yield waitForPort(TEST_URL);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  let loaded = new Promise(resolve => {
+    function listener() {
+      gBrowser.selectedBrowser.removeEventListener("load", listener, true);
+      resolve();
+    }
+    gBrowser.selectedBrowser.addEventListener("load", listener, true);
+    gBrowser.loadURI("about:blank");
+  });
+
+  yield waitForMessage(port, "RemotePage:Unload");
+
+  // Port should be destroyed now
+  try {
+    port.addMessageListener("Foo", failOnMessage);
+    ok(false, "Should have seen exception");
+  }
+  catch (e) {
+    ok(true, "Should have seen exception");
+  }
+
+  try {
+    port.sendAsyncMessage("Foo");
+    ok(false, "Should have seen exception");
+  }
+  catch (e) {
+    ok(true, "Should have seen exception");
+  }
+
+  yield loaded;
+
+  gBrowser.goBack();
+  port = yield waitForPort(TEST_URL, false);
+
+  port.sendAsyncMessage("Ping2");
+  let message = yield waitForMessage(port, "Pong2");
+  port.destroy();
+
+  gBrowser.removeCurrentTab();
+});
+
+// Test that opening a page creates a port, sends the load event and then
+// closing the tab sends the unload event
+add_task(function* init_close() {
+  let port = yield waitForPort(TEST_URL);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+  gBrowser.removeCurrentTab();
+  yield unloadPromise;
+
+  // Port should be destroyed now
+  try {
+    port.addMessageListener("Foo", failOnMessage);
+    ok(false, "Should have seen exception");
+  }
+  catch (e) {
+    ok(true, "Should have seen exception");
+  }
+
+  try {
+    port.sendAsyncMessage("Foo");
+    ok(false, "Should have seen exception");
+  }
+  catch (e) {
+    ok(true, "Should have seen exception");
+  }
+});
+
+// Tests that we can send messages to individual pages even when more than one
+// is open
+add_task(function* multiple_ports() {
+  let port1 = yield waitForPort(TEST_URL);
+  is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  let port2 = yield waitForPort(TEST_URL);
+  is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  port2.addMessageListener("Pong", failOnMessage);
+  port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
+  let message = yield waitForMessage(port1, "Pong");
+  port2.removeMessageListener("Pong", failOnMessage);
+  is(message.data.str, "foobar", "String should pass through");
+  is(message.data.counter, 1, "Counter should be incremented");
+
+  port1.addMessageListener("Pong", failOnMessage);
+  port2.sendAsyncMessage("Ping", { str: "foobaz", counter: 5 });
+  message = yield waitForMessage(port2, "Pong");
+  port1.removeMessageListener("Pong", failOnMessage);
+  is(message.data.str, "foobaz", "String should pass through");
+  is(message.data.counter, 6, "Counter should be incremented");
+
+  let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
+  gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
+  yield unloadPromise;
+
+  try {
+    port2.addMessageListener("Pong", failOnMessage);
+    ok(false, "Should not have been able to add a new message listener to a destroyed port.");
+  }
+  catch (e) {
+    ok(true, "Should not have been able to add a new message listener to a destroyed port.");
+  }
+
+  port1.sendAsyncMessage("Ping", { str: "foobar", counter: 0 });
+  message = yield waitForMessage(port1, "Pong");
+  is(message.data.str, "foobar", "String should pass through");
+  is(message.data.counter, 1, "Counter should be incremented");
+
+  unloadPromise = waitForMessage(port1, "RemotePage:Unload");
+  gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
+  yield unloadPromise;
+});
+
+// Tests that swapping browser docshells doesn't break the ports
+add_task(function* browser_switch() {
+  let port1 = yield waitForPort(TEST_URL);
+  is(port1.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+  let browser1 = gBrowser.selectedBrowser;
+  port1.sendAsyncMessage("SetCookie", { value: "om nom" });
+
+  let port2 = yield waitForPort(TEST_URL);
+  is(port2.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+  let browser2 = gBrowser.selectedBrowser;
+  port2.sendAsyncMessage("SetCookie", { value: "om nom nom" });
+
+  port2.addMessageListener("Cookie", failOnMessage);
+  port1.sendAsyncMessage("GetCookie");
+  let message = yield waitForMessage(port1, "Cookie");
+  port2.removeMessageListener("Cookie", failOnMessage);
+  is(message.data.value, "om nom", "Should have the right cookie");
+
+  port1.addMessageListener("Cookie", failOnMessage);
+  port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+  message = yield waitForMessage(port2, "Cookie");
+  port1.removeMessageListener("Cookie", failOnMessage);
+  is(message.data.value, "om nom nom", "Should have the right cookie");
+
+  browser1.swapDocShells(browser2);
+  is(port1.browser, browser2, "Should have noticed the swap");
+  is(port2.browser, browser1, "Should have noticed the swap");
+
+  // Cookies should have stayed the same
+  port2.addMessageListener("Cookie", failOnMessage);
+  port1.sendAsyncMessage("GetCookie");
+  message = yield waitForMessage(port1, "Cookie");
+  port2.removeMessageListener("Cookie", failOnMessage);
+  is(message.data.value, "om nom", "Should have the right cookie");
+
+  port1.addMessageListener("Cookie", failOnMessage);
+  port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+  message = yield waitForMessage(port2, "Cookie");
+  port1.removeMessageListener("Cookie", failOnMessage);
+  is(message.data.value, "om nom nom", "Should have the right cookie");
+
+  browser1.swapDocShells(browser2);
+  is(port1.browser, browser1, "Should have noticed the swap");
+  is(port2.browser, browser2, "Should have noticed the swap");
+
+  // Cookies should have stayed the same
+  port2.addMessageListener("Cookie", failOnMessage);
+  port1.sendAsyncMessage("GetCookie");
+  message = yield waitForMessage(port1, "Cookie");
+  port2.removeMessageListener("Cookie", failOnMessage);
+  is(message.data.value, "om nom", "Should have the right cookie");
+
+  port1.addMessageListener("Cookie", failOnMessage);
+  port2.sendAsyncMessage("GetCookie", { str: "foobaz", counter: 5 });
+  message = yield waitForMessage(port2, "Cookie");
+  port1.removeMessageListener("Cookie", failOnMessage);
+  is(message.data.value, "om nom nom", "Should have the right cookie");
+
+  let unloadPromise = waitForMessage(port2, "RemotePage:Unload");
+  gBrowser.removeTab(gBrowser.getTabForBrowser(browser2));
+  yield unloadPromise;
+
+  unloadPromise = waitForMessage(port1, "RemotePage:Unload");
+  gBrowser.removeTab(gBrowser.getTabForBrowser(browser1));
+  yield unloadPromise;
+});
+
+// Tests that removeMessageListener in chrome works
+add_task(function* remove_chrome_listener() {
+  let port = yield waitForPort(TEST_URL);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  // This relies on messages sent arriving in the same order. Pong will be
+  // sent back before Pong2 so if removeMessageListener fails the test will fail
+  port.addMessageListener("Pong", failOnMessage);
+  port.removeMessageListener("Pong", failOnMessage);
+  port.sendAsyncMessage("Ping", { str: "remove_listener", counter: 27 });
+  port.sendAsyncMessage("Ping2");
+  yield waitForMessage(port, "Pong2");
+
+  let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+  gBrowser.removeCurrentTab();
+  yield unloadPromise;
+});
+
+// Tests that removeMessageListener in content works
+add_task(function* remove_content_listener() {
+  let port = yield waitForPort(TEST_URL);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  // This relies on messages sent arriving in the same order. Pong3 would be
+  // sent back before Pong2 so if removeMessageListener fails the test will fail
+  port.addMessageListener("Pong3", failOnMessage);
+  port.sendAsyncMessage("Ping3");
+  port.sendAsyncMessage("Ping2");
+  yield waitForMessage(port, "Pong2");
+
+  let unloadPromise = waitForMessage(port, "RemotePage:Unload");
+  gBrowser.removeCurrentTab();
+  yield unloadPromise;
+});
+
+// Test RemotePages works
+add_task(function* remote_pages_basic() {
+  let pages = new RemotePages(TEST_URL);
+  let port = yield waitForPage(pages);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  // Listening to global messages should work
+  let unloadPromise = waitForMessage(pages, "RemotePage:Unload", port);
+  gBrowser.removeCurrentTab();
+  yield unloadPromise;
+
+  pages.destroy();
+
+  // RemotePages should be destroyed now
+  try {
+    pages.addMessageListener("Foo", failOnMessage);
+    ok(false, "Should have seen exception");
+  }
+  catch (e) {
+    ok(true, "Should have seen exception");
+  }
+
+  try {
+    pages.sendAsyncMessage("Foo");
+    ok(false, "Should have seen exception");
+  }
+  catch (e) {
+    ok(true, "Should have seen exception");
+  }
+});
+
+// Test sending messages to all remote pages works
+add_task(function* remote_pages_multiple() {
+  let pages = new RemotePages(TEST_URL);
+  let port1 = yield waitForPage(pages);
+  let port2 = yield waitForPage(pages);
+
+  let pongPorts = [];
+  yield new Promise((resolve) => {
+    function listener({ name, target, data }) {
+      is(name, "Pong", "Should have seen the right response.");
+      is(data.str, "remote_pages", "String should pass through");
+      is(data.counter, 43, "Counter should be incremented");
+      pongPorts.push(target);
+      if (pongPorts.length == 2)
+        resolve();
+    }
+
+    pages.addMessageListener("Pong", listener);
+    pages.sendAsyncMessage("Ping", { str: "remote_pages", counter: 42 });
+  });
+
+  // We don't make any guarantees about which order messages are sent to known
+  // pages so the pongs could have come back in any order.
+  isnot(pongPorts[0], pongPorts[1], "Should have received pongs from different ports");
+  ok(pongPorts.indexOf(port1) >= 0, "Should have seen a pong from port1");
+  ok(pongPorts.indexOf(port2) >= 0, "Should have seen a pong from port2");
+
+  // After destroy we should see no messages
+  pages.addMessageListener("RemotePage:Unload", failOnMessage);
+  pages.destroy();
+
+  gBrowser.removeTab(gBrowser.getTabForBrowser(port1.browser));
+  gBrowser.removeTab(gBrowser.getTabForBrowser(port2.browser));
+});
+
+// Test sending various types of data across the boundary
+add_task(function* send_data() {
+  let port = yield waitForPort(TEST_URL);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  let data = {
+    integer: 45,
+    real: 45.78,
+    str: "foobar",
+    array: [1, 2, 3, 5, 27]
+  };
+
+  port.sendAsyncMessage("SendData", data);
+  let message = yield waitForMessage(port, "ReceivedData");
+
+  ok(message.data.result, message.data.status);
+
+  gBrowser.removeCurrentTab();
+});
+
+// Test sending an object of data across the boundary
+add_task(function* send_data2() {
+  let port = yield waitForPort(TEST_URL);
+  is(port.browser, gBrowser.selectedBrowser, "Port is for the correct browser");
+
+  let data = {
+    integer: 45,
+    real: 45.78,
+    str: "foobar",
+    array: [1, 2, 3, 5, 27]
+  };
+
+  port.sendAsyncMessage("SendData2", {data});
+  let message = yield waitForMessage(port, "ReceivedData2");
+
+  ok(message.data.result, message.data.status);
+
+  gBrowser.removeCurrentTab();
+});
+
new file mode 100644
--- /dev/null
+++ b/toolkit/modules/tests/browser/testremotepagemanager.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+
+<html>
+<head>
+<script type="text/javascript">
+addMessageListener("Ping", function(message) {
+  sendAsyncMessage("Pong", {
+    str: message.data.str,
+    counter: message.data.counter + 1
+  });
+});
+
+addMessageListener("Ping2", function(message) {
+  sendAsyncMessage("Pong2", message.data);
+});
+
+function neverCalled() {
+  sendAsyncMessage("Pong3");
+}
+addMessageListener("Pong3", neverCalled);
+removeMessageListener("Pong3", neverCalled);
+
+function testData(data) {
+  var response = {
+    result: true,
+    status: "All data correctly received"
+  }
+
+  function compare(prop, expected) {
+    if (uneval(data[prop]) == uneval(expected))
+      return;
+    if (response.result)
+      response.status = "";
+    response.result = false;
+    response.status += "Property " + prop + " should have been " + expected + " but was " + data[prop] + "\n";
+  }
+
+  compare("integer", 45);
+  compare("real", 45.78);
+  compare("str", "foobar");
+  compare("array", [1, 2, 3, 5, 27]);
+
+  return response;
+}
+
+addMessageListener("SendData", function(message) {
+  sendAsyncMessage("ReceivedData", testData(message.data));
+});
+
+addMessageListener("SendData2", function(message) {
+  sendAsyncMessage("ReceivedData2", testData(message.data.data));
+});
+
+var cookie = "nom";
+addMessageListener("SetCookie", function(message) {
+  cookie = message.data.value;
+});
+
+addMessageListener("GetCookie", function(message) {
+  sendAsyncMessage("Cookie", { value: cookie });
+});
+</script>
+</head>
+<body>
+</body>
+</html>