--- 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>