summaryrefslogtreecommitdiffstats
path: root/toolkit/jetpack/sdk/remote
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2018-02-09 06:46:43 -0500
committerMatt A. Tobin <email@mattatobin.com>2018-02-09 06:46:43 -0500
commitac46df8daea09899ce30dc8fd70986e258c746bf (patch)
tree2750d3125fc253fd5b0671e4bd268eff1fd97296 /toolkit/jetpack/sdk/remote
parent8cecf8d5208f3945b35f879bba3015bb1a11bec6 (diff)
downloadUXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.gz
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.lz
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.tar.xz
UXP-ac46df8daea09899ce30dc8fd70986e258c746bf.zip
Move Add-on SDK source to toolkit/jetpack
Diffstat (limited to 'toolkit/jetpack/sdk/remote')
-rw-r--r--toolkit/jetpack/sdk/remote/child.js284
-rw-r--r--toolkit/jetpack/sdk/remote/core.js8
-rw-r--r--toolkit/jetpack/sdk/remote/parent.js338
-rw-r--r--toolkit/jetpack/sdk/remote/utils.js39
4 files changed, 669 insertions, 0 deletions
diff --git a/toolkit/jetpack/sdk/remote/child.js b/toolkit/jetpack/sdk/remote/child.js
new file mode 100644
index 000000000..4ccfa661a
--- /dev/null
+++ b/toolkit/jetpack/sdk/remote/child.js
@@ -0,0 +1,284 @@
+/* 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";
+
+const { isChildLoader } = require('./core');
+if (!isChildLoader)
+ throw new Error("Cannot load sdk/remote/child in a main process loader.");
+
+const { Ci, Cc, Cu } = require('chrome');
+const runtime = require('../system/runtime');
+const { Class } = require('../core/heritage');
+const { Namespace } = require('../core/namespace');
+const { omit } = require('../util/object');
+const { when } = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const { Disposable } = require('../core/disposable');
+const { EventParent } = require('./utils');
+const { addListItem, removeListItem } = require('../util/list');
+
+const loaderID = require('@loader/options').loaderID;
+
+const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+const mm = Cc['@mozilla.org/childprocessmessagemanager;1'].
+ getService(Ci.nsISyncMessageSender);
+
+const ns = Namespace();
+
+const process = {
+ port: new EventTarget(),
+ get id() {
+ return runtime.processID;
+ },
+ get isRemote() {
+ return runtime.processType != MAIN_PROCESS;
+ }
+};
+exports.process = process;
+
+function definePort(obj, name) {
+ obj.port.emit = (event, ...args) => {
+ let manager = ns(obj).messageManager;
+ if (!manager)
+ return;
+
+ manager.sendAsyncMessage(name, { loaderID, event, args });
+ };
+}
+
+function messageReceived({ data, objects }) {
+ // Ignore messages from other loaders
+ if (data.loaderID != loaderID)
+ return;
+
+ let keys = Object.keys(objects);
+ if (keys.length) {
+ // If any objects are CPOWs then ignore this message. We don't want child
+ // processes interracting with CPOWs
+ if (!keys.every(name => !Cu.isCrossProcessWrapper(objects[name])))
+ return;
+
+ data.args.push(objects);
+ }
+
+ emit(this.port, data.event, this, ...data.args);
+}
+
+ns(process).messageManager = mm;
+definePort(process, 'sdk/remote/process/message');
+let processMessageReceived = messageReceived.bind(process);
+mm.addMessageListener('sdk/remote/process/message', processMessageReceived);
+
+when(() => {
+ mm.removeMessageListener('sdk/remote/process/message', processMessageReceived);
+ frames = null;
+});
+
+process.port.on('sdk/remote/require', (process, uri) => {
+ require(uri);
+});
+
+function listenerEquals(a, b) {
+ for (let prop of ["type", "callback", "isCapturing"]) {
+ if (a[prop] != b[prop])
+ return false;
+ }
+ return true;
+}
+
+function listenerFor(type, callback, isCapturing = false) {
+ return {
+ type,
+ callback,
+ isCapturing,
+ registeredCallback: undefined,
+ get args() {
+ return [
+ this.type,
+ this.registeredCallback ? this.registeredCallback : this.callback,
+ this.isCapturing
+ ];
+ }
+ };
+}
+
+function removeListenerFromArray(array, listener) {
+ let index = array.findIndex(l => listenerEquals(l, listener));
+ if (index < 0)
+ return;
+ array.splice(index, 1);
+}
+
+function getListenerFromArray(array, listener) {
+ return array.find(l => listenerEquals(l, listener));
+}
+
+function arrayContainsListener(array, listener) {
+ return !!getListenerFromArray(array, listener);
+}
+
+function makeFrameEventListener(frame, callback) {
+ return callback.bind(frame);
+}
+
+var FRAME_ID = 0;
+var tabMap = new Map();
+
+const Frame = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(contentFrame) {
+ // This ID should be unique for this loader across all processes
+ let priv = ns(this);
+
+ priv.id = runtime.processID + ":" + FRAME_ID++;
+
+ priv.contentFrame = contentFrame;
+ priv.messageManager = contentFrame;
+ priv.domListeners = [];
+
+ tabMap.set(contentFrame.docShell, this);
+
+ priv.messageReceived = messageReceived.bind(this);
+ priv.messageManager.addMessageListener('sdk/remote/frame/message', priv.messageReceived);
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/frame/message');
+
+ priv.messageManager.sendAsyncMessage('sdk/remote/frame/attach', {
+ loaderID,
+ frameID: priv.id,
+ processID: runtime.processID
+ });
+
+ frames.attachItem(this);
+ },
+
+ dispose: function() {
+ let priv = ns(this);
+
+ emit(this, 'detach', this);
+
+ for (let listener of priv.domListeners)
+ priv.contentFrame.removeEventListener(...listener.args);
+
+ priv.messageManager.removeMessageListener('sdk/remote/frame/message', priv.messageReceived);
+ tabMap.delete(priv.contentFrame.docShell);
+ priv.contentFrame = null;
+ },
+
+ get content() {
+ return ns(this).contentFrame.content;
+ },
+
+ get isTab() {
+ let docShell = ns(this).contentFrame.docShell;
+ if (process.isRemote) {
+ // We don't want to roundtrip to the main process to get this property.
+ // This hack relies on the host app having defined webBrowserChrome only
+ // in frames that are part of the tabs. Since only Firefox has remote
+ // processes right now and does this this works.
+ let tabchild = docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsITabChild);
+ return !!tabchild.webBrowserChrome;
+ }
+ else {
+ // This is running in the main process so we can break out to the browser
+ // And check we can find a tab for the browser element directly.
+ let browser = docShell.chromeEventHandler;
+ let tab = require('../tabs/utils').getTabForBrowser(browser);
+ return !!tab;
+ }
+ },
+
+ addEventListener: function(...args) {
+ let priv = ns(this);
+
+ let listener = listenerFor(...args);
+ if (arrayContainsListener(priv.domListeners, listener))
+ return;
+
+ listener.registeredCallback = makeFrameEventListener(this, listener.callback);
+
+ priv.domListeners.push(listener);
+ priv.contentFrame.addEventListener(...listener.args);
+ },
+
+ removeEventListener: function(...args) {
+ let priv = ns(this);
+
+ let listener = getListenerFromArray(priv.domListeners, listenerFor(...args));
+ if (!listener)
+ return;
+
+ removeListenerFromArray(priv.domListeners, listener);
+ priv.contentFrame.removeEventListener(...listener.args);
+ }
+});
+
+const FrameList = Class({
+ implements: [ EventParent, Disposable ],
+ extends: EventTarget,
+ setup: function() {
+ EventParent.prototype.initialize.call(this);
+
+ this.port = new EventTarget();
+ ns(this).domListeners = [];
+
+ this.on('attach', frame => {
+ for (let listener of ns(this).domListeners)
+ frame.addEventListener(...listener.args);
+ });
+ },
+
+ dispose: function() {
+ // The only case where we get destroyed is when the loader is unloaded in
+ // which case each frame will clean up its own event listeners.
+ ns(this).domListeners = null;
+ },
+
+ getFrameForWindow: function(window) {
+ let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell);
+
+ return tabMap.get(docShell) || null;
+ },
+
+ addEventListener: function(...args) {
+ let listener = listenerFor(...args);
+ if (arrayContainsListener(ns(this).domListeners, listener))
+ return;
+
+ ns(this).domListeners.push(listener);
+ for (let frame of this)
+ frame.addEventListener(...listener.args);
+ },
+
+ removeEventListener: function(...args) {
+ let listener = listenerFor(...args);
+ if (!arrayContainsListener(ns(this).domListeners, listener))
+ return;
+
+ removeListenerFromArray(ns(this).domListeners, listener);
+ for (let frame of this)
+ frame.removeEventListener(...listener.args);
+ }
+});
+var frames = exports.frames = new FrameList();
+
+function registerContentFrame(contentFrame) {
+ let frame = new Frame(contentFrame);
+}
+exports.registerContentFrame = registerContentFrame;
+
+function unregisterContentFrame(contentFrame) {
+ let frame = tabMap.get(contentFrame.docShell);
+ if (!frame)
+ return;
+
+ frame.destroy();
+}
+exports.unregisterContentFrame = unregisterContentFrame;
diff --git a/toolkit/jetpack/sdk/remote/core.js b/toolkit/jetpack/sdk/remote/core.js
new file mode 100644
index 000000000..78bb673fd
--- /dev/null
+++ b/toolkit/jetpack/sdk/remote/core.js
@@ -0,0 +1,8 @@
+/* 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";
+
+const options = require("@loader/options");
+
+exports.isChildLoader = options.childLoader;
diff --git a/toolkit/jetpack/sdk/remote/parent.js b/toolkit/jetpack/sdk/remote/parent.js
new file mode 100644
index 000000000..f110fe3f6
--- /dev/null
+++ b/toolkit/jetpack/sdk/remote/parent.js
@@ -0,0 +1,338 @@
+/* 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";
+
+const { isChildLoader } = require('./core');
+if (isChildLoader)
+ throw new Error("Cannot load sdk/remote/parent in a child loader.");
+
+const { Cu, Ci, Cc } = require('chrome');
+const runtime = require('../system/runtime');
+
+const MAIN_PROCESS = Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+if (runtime.processType != MAIN_PROCESS) {
+ throw new Error('Cannot use sdk/remote/parent in a child process.');
+}
+
+const { Class } = require('../core/heritage');
+const { Namespace } = require('../core/namespace');
+const { Disposable } = require('../core/disposable');
+const { omit } = require('../util/object');
+const { when } = require('../system/unload');
+const { EventTarget } = require('../event/target');
+const { emit } = require('../event/core');
+const system = require('../system/events');
+const { EventParent } = require('./utils');
+const options = require('@loader/options');
+const loaderModule = require('toolkit/loader');
+const { getTabForBrowser } = require('../tabs/utils');
+
+const appInfo = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULRuntime);
+
+exports.useRemoteProcesses = appInfo.browserTabsRemoteAutostart;
+
+// Chose the right function for resolving relative a module id
+var moduleResolve;
+if (options.isNative) {
+ moduleResolve = (id, requirer) => loaderModule.nodeResolve(id, requirer, { rootURI: options.rootURI });
+}
+else {
+ moduleResolve = loaderModule.resolve;
+}
+// Build the sorted path mapping structure that resolveURI requires
+var pathMapping = Object.keys(options.paths)
+ .sort((a, b) => b.length - a.length)
+ .map(p => [p, options.paths[p]]);
+
+// Load the scripts in the child processes
+var { getNewLoaderID } = require('../../framescript/FrameScriptManager.jsm');
+var PATH = options.paths[''];
+
+const childOptions = omit(options, ['modules', 'globals', 'resolve', 'load']);
+childOptions.modules = {};
+// @l10n/data is just JSON data and can be safely sent across to the child loader
+try {
+ childOptions.modules["@l10n/data"] = require("@l10n/data");
+}
+catch (e) {
+ // There may be no l10n data
+}
+const loaderID = getNewLoaderID();
+childOptions.loaderID = loaderID;
+childOptions.childLoader = true;
+
+const ppmm = Cc['@mozilla.org/parentprocessmessagemanager;1'].
+ getService(Ci.nsIMessageBroadcaster);
+const gmm = Cc['@mozilla.org/globalmessagemanager;1'].
+ getService(Ci.nsIMessageBroadcaster);
+
+const ns = Namespace();
+
+var processMap = new Map();
+
+function definePort(obj, name) {
+ obj.port.emitCPOW = (event, args, cpows = {}) => {
+ let manager = ns(obj).messageManager;
+ if (!manager)
+ return;
+
+ let method = manager instanceof Ci.nsIMessageBroadcaster ?
+ "broadcastAsyncMessage" : "sendAsyncMessage";
+
+ manager[method](name, { loaderID, event, args }, cpows);
+ };
+
+ obj.port.emit = (event, ...args) => obj.port.emitCPOW(event, args);
+}
+
+function messageReceived({ target, data }) {
+ // Ignore messages from other loaders
+ if (data.loaderID != loaderID)
+ return;
+
+ emit(this.port, data.event, this, ...data.args);
+}
+
+// Process represents a gecko process that can load webpages. Each process
+// contains a number of Frames. This class is used to send and receive messages
+// from a single process.
+const Process = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(id, messageManager, isRemote) {
+ ns(this).id = id;
+ ns(this).isRemote = isRemote;
+ ns(this).messageManager = messageManager;
+ ns(this).messageReceived = messageReceived.bind(this);
+ this.destroy = this.destroy.bind(this);
+ ns(this).messageManager.addMessageListener('sdk/remote/process/message', ns(this).messageReceived);
+ ns(this).messageManager.addMessageListener('child-process-shutdown', this.destroy);
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/process/message');
+
+ // Load any remote modules
+ for (let module of remoteModules.values())
+ this.port.emit('sdk/remote/require', module);
+
+ processMap.set(ns(this).id, this);
+ processes.attachItem(this);
+ },
+
+ dispose: function() {
+ emit(this, 'detach', this);
+ processMap.delete(ns(this).id);
+ ns(this).messageManager.removeMessageListener('sdk/remote/process/message', ns(this).messageReceived);
+ ns(this).messageManager.removeMessageListener('child-process-shutdown', this.destroy);
+ ns(this).messageManager = null;
+ },
+
+ // Returns true if this process is a child process
+ get isRemote() {
+ return ns(this).isRemote;
+ }
+});
+
+// Processes gives an API for enumerating an sending and receiving messages from
+// all processes as well as detecting when a new process starts.
+const Processes = Class({
+ implements: [ EventParent ],
+ extends: EventTarget,
+ initialize: function() {
+ EventParent.prototype.initialize.call(this);
+ ns(this).messageManager = ppmm;
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/process/message');
+ },
+
+ getById: function(id) {
+ return processMap.get(id);
+ }
+});
+var processes = exports.processes = new Processes();
+
+var frameMap = new Map();
+
+function setFrameProcess(frame, process) {
+ ns(frame).process = process;
+ frames.attachItem(frame);
+}
+
+// Frames display webpages in a process. In the main process every Frame is
+// linked with a <browser> or <iframe> element.
+const Frame = Class({
+ implements: [ Disposable ],
+ extends: EventTarget,
+ setup: function(id, node) {
+ ns(this).id = id;
+ ns(this).node = node;
+
+ let frameLoader = node.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ ns(this).messageManager = frameLoader.messageManager;
+
+ ns(this).messageReceived = messageReceived.bind(this);
+ ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/frame/message');
+
+ frameMap.set(ns(this).messageManager, this);
+ },
+
+ dispose: function() {
+ emit(this, 'detach', this);
+ ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
+
+ frameMap.delete(ns(this).messageManager);
+ ns(this).messageManager = null;
+ },
+
+ // Returns the browser or iframe element this frame displays in
+ get frameElement() {
+ return ns(this).node;
+ },
+
+ // Returns the process that this frame loads in
+ get process() {
+ return ns(this).process;
+ },
+
+ // Returns true if this frame is a tab in a main browser window
+ get isTab() {
+ let tab = getTabForBrowser(ns(this).node);
+ return !!tab;
+ }
+});
+
+function managerDisconnected({ subject: manager }) {
+ let frame = frameMap.get(manager);
+ if (frame)
+ frame.destroy();
+}
+system.on('message-manager-disconnect', managerDisconnected);
+
+// Provides an API for enumerating and sending and receiving messages from all
+// Frames
+const FrameList = Class({
+ implements: [ EventParent ],
+ extends: EventTarget,
+ initialize: function() {
+ EventParent.prototype.initialize.call(this);
+ ns(this).messageManager = gmm;
+
+ this.port = new EventTarget();
+ definePort(this, 'sdk/remote/frame/message');
+ },
+
+ // Returns the frame for a browser element
+ getFrameForBrowser: function(browser) {
+ for (let frame of this) {
+ if (frame.frameElement == browser)
+ return frame;
+ }
+ return null;
+ },
+});
+var frames = exports.frames = new FrameList();
+
+// Create the module loader in any existing processes
+ppmm.broadcastAsyncMessage('sdk/remote/process/load', {
+ modulePath: PATH,
+ loaderID,
+ options: childOptions,
+ reason: "broadcast"
+});
+
+// A loader has started in a remote process
+function processLoaderStarted({ target, data }) {
+ if (data.loaderID != loaderID)
+ return;
+
+ if (processMap.has(data.processID)) {
+ console.error("Saw the same process load the same loader twice. This is a bug in the SDK.");
+ return;
+ }
+
+ let process = new Process(data.processID, target, data.isRemote);
+
+ if (pendingFrames.has(data.processID)) {
+ for (let frame of pendingFrames.get(data.processID))
+ setFrameProcess(frame, process);
+ pendingFrames.delete(data.processID);
+ }
+}
+
+// A new process has started
+function processStarted({ target, data: { modulePath } }) {
+ if (modulePath != PATH)
+ return;
+
+ // Have it load a loader if it hasn't already
+ target.sendAsyncMessage('sdk/remote/process/load', {
+ modulePath,
+ loaderID,
+ options: childOptions,
+ reason: "response"
+ });
+}
+
+var pendingFrames = new Map();
+
+// A new frame has been created in the remote process
+function frameAttached({ target, data }) {
+ if (data.loaderID != loaderID)
+ return;
+
+ let frame = new Frame(data.frameID, target);
+
+ let process = processMap.get(data.processID);
+ if (process) {
+ setFrameProcess(frame, process);
+ return;
+ }
+
+ // In some cases frame messages can arrive earlier than process messages
+ // causing us to see a new frame appear before its process. In this case
+ // cache the frame data until we see the process. See bug 1131375.
+ if (!pendingFrames.has(data.processID))
+ pendingFrames.set(data.processID, [frame]);
+ else
+ pendingFrames.get(data.processID).push(frame);
+}
+
+// Wait for new processes and frames
+ppmm.addMessageListener('sdk/remote/process/attach', processLoaderStarted);
+ppmm.addMessageListener('sdk/remote/process/start', processStarted);
+gmm.addMessageListener('sdk/remote/frame/attach', frameAttached);
+
+when(reason => {
+ ppmm.removeMessageListener('sdk/remote/process/attach', processLoaderStarted);
+ ppmm.removeMessageListener('sdk/remote/process/start', processStarted);
+ gmm.removeMessageListener('sdk/remote/frame/attach', frameAttached);
+
+ ppmm.broadcastAsyncMessage('sdk/remote/process/unload', { loaderID, reason });
+});
+
+var remoteModules = new Set();
+
+// Ensures a module is loaded in every child process. It is safe to send
+// messages to this module immediately after calling this.
+// Pass a module to resolve the id relatively.
+function remoteRequire(id, module = null) {
+ // Resolve relative to calling module if passed
+ if (module)
+ id = moduleResolve(id, module.id);
+ let uri = loaderModule.resolveURI(id, pathMapping);
+
+ // Don't reload the same module
+ if (remoteModules.has(uri))
+ return;
+
+ remoteModules.add(uri);
+ processes.port.emit('sdk/remote/require', uri);
+}
+exports.remoteRequire = remoteRequire;
diff --git a/toolkit/jetpack/sdk/remote/utils.js b/toolkit/jetpack/sdk/remote/utils.js
new file mode 100644
index 000000000..5a5e39198
--- /dev/null
+++ b/toolkit/jetpack/sdk/remote/utils.js
@@ -0,0 +1,39 @@
+/* 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";
+
+const { Class } = require('../core/heritage');
+const { List, addListItem, removeListItem } = require('../util/list');
+const { emit } = require('../event/core');
+const { pipe } = require('../event/utils');
+
+// A helper class that maintains a list of EventTargets. Any events emitted
+// to an EventTarget are also emitted by the EventParent. Likewise for an
+// EventTarget's port property.
+const EventParent = Class({
+ implements: [ List ],
+
+ attachItem: function(item) {
+ addListItem(this, item);
+
+ pipe(item.port, this.port);
+ pipe(item, this);
+
+ item.once('detach', () => {
+ removeListItem(this, item);
+ })
+
+ emit(this, 'attach', item);
+ },
+
+ // Calls listener for every object already in the list and every object
+ // subsequently added to the list.
+ forEvery: function(listener) {
+ for (let item of this)
+ listener(item);
+
+ this.on('attach', listener);
+ }
+});
+exports.EventParent = EventParent;