/* 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;