diff options
author | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
---|---|---|
committer | Matt A. Tobin <email@mattatobin.com> | 2018-02-10 02:51:36 -0500 |
commit | 37d5300335d81cecbecc99812747a657588c63eb (patch) | |
tree | 765efa3b6a56bb715d9813a8697473e120436278 /toolkit/jetpack/sdk/remote | |
parent | b2bdac20c02b12f2057b9ef70b0a946113a00e00 (diff) | |
parent | 4fb11cd5966461bccc3ed1599b808237be6b0de9 (diff) | |
download | UXP-37d5300335d81cecbecc99812747a657588c63eb.tar UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.gz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.lz UXP-37d5300335d81cecbecc99812747a657588c63eb.tar.xz UXP-37d5300335d81cecbecc99812747a657588c63eb.zip |
Merge branch 'ext-work'
Diffstat (limited to 'toolkit/jetpack/sdk/remote')
-rw-r--r-- | toolkit/jetpack/sdk/remote/child.js | 284 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/remote/core.js | 8 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/remote/parent.js | 338 | ||||
-rw-r--r-- | toolkit/jetpack/sdk/remote/utils.js | 39 |
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; |