Files
tubestation/addon-sdk/source/lib/sdk/remote/child.js

275 lines
7.6 KiB
JavaScript

/* 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 } = 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;
process.port.emit = (...args) => {
mm.sendAsyncMessage('sdk/remote/process/message', {
loaderID,
args
});
}
function processMessageReceived({ data }) {
// Ignore messages from other loaders
if (data.loaderID != loaderID)
return;
let [event, ...args] = data.args;
emit(process.port, event, process, ...args);
}
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();
function frameMessageReceived({ data }) {
if (data.loaderID != loaderID)
return;
let [event, ...args] = data.args;
emit(this.port, event, this, ...args);
}
const Frame = Class({
implements: [ Disposable ],
extends: EventTarget,
setup: function(contentFrame) {
// This ID should be unique for this loader across all processes
ns(this).id = runtime.processID + ":" + FRAME_ID++;
ns(this).contentFrame = contentFrame;
ns(this).messageManager = contentFrame;
ns(this).domListeners = [];
tabMap.set(contentFrame.docShell, this);
ns(this).messageReceived = frameMessageReceived.bind(this);
ns(this).messageManager.addMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
this.port = new EventTarget();
this.port.emit = (...args) => {
ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/message', {
loaderID,
args
});
};
ns(this).messageManager.sendAsyncMessage('sdk/remote/frame/attach', {
loaderID,
frameID: ns(this).id,
processID: runtime.processID
});
frames.attachItem(this);
},
dispose: function() {
emit(this, 'detach', this);
for (let listener of ns(this).domListeners)
ns(this).contentFrame.removeEventListener(...listener.args);
ns(this).messageManager.removeMessageListener('sdk/remote/frame/message', ns(this).messageReceived);
tabMap.delete(ns(this).contentFrame.docShell);
ns(this).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 listener = listenerFor(...args);
if (arrayContainsListener(ns(this).domListeners, listener))
return;
listener.registeredCallback = makeFrameEventListener(this, listener.callback);
ns(this).domListeners.push(listener);
ns(this).contentFrame.addEventListener(...listener.args);
},
removeEventListener: function(...args) {
let listener = getListenerFromArray(ns(this).domListeners, listenerFor(...args));
if (!listener)
return;
removeListenerFromArray(ns(this).domListeners, listener);
ns(this).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) {
for (let frame of this) {
if (frame.content == window)
return frame;
}
return 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;