gecko-dev/addon-sdk/source/lib/sdk/remote/parent.js

339 строки
10 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/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;