mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-02 15:15:23 +00:00
216 lines
7.8 KiB
JavaScript
216 lines
7.8 KiB
JavaScript
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
/* 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/.
|
|
*/
|
|
|
|
/*
|
|
* This is an implementation of a "Shared Worker" using a remote browser
|
|
* in the hidden DOM window. This is the implementation that lives in the
|
|
* "chrome process". See FrameWorkerContent for code that lives in the
|
|
* "content" process and which sets up a sandbox for the worker.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
|
|
|
Cu.import("resource://gre/modules/Services.jsm");
|
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
Cu.import("resource://gre/modules/MessagePortBase.jsm");
|
|
Cu.import("resource://gre/modules/Promise.jsm");
|
|
|
|
XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
|
|
"resource://gre/modules/SocialService.jsm");
|
|
|
|
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
|
const HTML_NS = "http://www.w3.org/1999/xhtml";
|
|
|
|
this.EXPORTED_SYMBOLS = ["getFrameWorkerHandle"];
|
|
|
|
var workerCache = {}; // keyed by URL.
|
|
var _nextPortId = 1;
|
|
|
|
// Retrieves a reference to a WorkerHandle associated with a FrameWorker and a
|
|
// new ClientPort.
|
|
this.getFrameWorkerHandle =
|
|
function getFrameWorkerHandle(url, clientWindow, name, origin, exposeLocalStorage = false) {
|
|
// prevent data/about urls - see bug 891516
|
|
if (['http', 'https'].indexOf(Services.io.newURI(url, null, null).scheme) < 0)
|
|
throw new Error("getFrameWorkerHandle requires http/https urls");
|
|
|
|
// See if we already have a worker with this URL.
|
|
let existingWorker = workerCache[url];
|
|
if (!existingWorker) {
|
|
// create a remote browser and _Worker object - this will message the
|
|
// remote browser to do the content side of things.
|
|
let browserPromise = makeRemoteBrowser();
|
|
let options = { url: url, name: name, origin: origin,
|
|
exposeLocalStorage: exposeLocalStorage };
|
|
|
|
existingWorker = workerCache[url] = new _Worker(browserPromise, options);
|
|
}
|
|
|
|
// message the content so it can establish a new connection with the worker.
|
|
let portid = _nextPortId++;
|
|
existingWorker.browserPromise.then(browser => {
|
|
browser.messageManager.sendAsyncMessage("frameworker:connect",
|
|
{ portId: portid });
|
|
}).then(null, (ex) => {
|
|
Cu.reportError("Could not send frameworker:connect: " + ex);
|
|
});
|
|
// return the pseudo worker object.
|
|
let port = new ParentPort(portid, existingWorker.browserPromise, clientWindow);
|
|
existingWorker.ports.set(portid, port);
|
|
return new WorkerHandle(port, existingWorker);
|
|
};
|
|
|
|
// A "_Worker" is an internal representation of a worker. It's never returned
|
|
// directly to consumers.
|
|
function _Worker(browserPromise, options) {
|
|
this.browserPromise = browserPromise;
|
|
this.options = options;
|
|
this.ports = new Map();
|
|
browserPromise.then(browser => {
|
|
browser.addEventListener("oop-browser-crashed", () => {
|
|
Cu.reportError("FrameWorker remote process crashed");
|
|
notifyWorkerError(options.origin);
|
|
});
|
|
|
|
let mm = browser.messageManager;
|
|
// execute the content script and send the message to bootstrap the content
|
|
// side of the world.
|
|
mm.loadFrameScript("resource://gre/modules/FrameWorkerContent.js", true);
|
|
mm.sendAsyncMessage("frameworker:init", this.options);
|
|
mm.addMessageListener("frameworker:port-message", this);
|
|
mm.addMessageListener("frameworker:notify-worker-error", this);
|
|
});
|
|
}
|
|
|
|
_Worker.prototype = {
|
|
// Message handler.
|
|
receiveMessage: function(msg) {
|
|
switch (msg.name) {
|
|
case "frameworker:port-message":
|
|
let port = this.ports.get(msg.data.portId);
|
|
port._onmessage(msg.data.data);
|
|
break;
|
|
case "frameworker:notify-worker-error":
|
|
notifyWorkerError(msg.data.origin);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// This WorkerHandle is exposed to consumers - it has the new port instance
|
|
// the consumer uses to communicate with the worker.
|
|
// public methods/properties on WorkerHandle should conform to the SharedWorker
|
|
// api - currently that's just .port and .terminate()
|
|
function WorkerHandle(port, worker) {
|
|
this.port = port;
|
|
this._worker = worker;
|
|
}
|
|
|
|
WorkerHandle.prototype = {
|
|
// A method to terminate the worker. The worker spec doesn't define a
|
|
// callback to be made in the worker when this happens, so we just kill the
|
|
// browser element.
|
|
terminate: function terminate() {
|
|
let url = this._worker.options.url;
|
|
if (!(url in workerCache)) {
|
|
// terminating an already terminated worker - ignore it
|
|
return;
|
|
}
|
|
delete workerCache[url];
|
|
// close all the ports we have handed out.
|
|
for (let [portid, port] of this._worker.ports) {
|
|
port.close();
|
|
}
|
|
this._worker.ports.clear();
|
|
this._worker.ports = null;
|
|
this._worker.browserPromise.then(browser => {
|
|
let iframe = browser.ownerDocument.defaultView.frameElement;
|
|
iframe.parentNode.removeChild(iframe);
|
|
});
|
|
// wipe things out just incase other reference have snuck out somehow...
|
|
this._worker.browserPromise = null;
|
|
this._worker = null;
|
|
}
|
|
};
|
|
|
|
// The port that lives in the parent chrome process. The other end of this
|
|
// port is the "client" port in the content process, which itself is just a
|
|
// shim which shuttles messages to/from the worker itself.
|
|
function ParentPort(portid, browserPromise, clientWindow) {
|
|
this._clientWindow = clientWindow;
|
|
this._browserPromise = browserPromise;
|
|
AbstractPort.call(this, portid);
|
|
}
|
|
|
|
ParentPort.prototype = {
|
|
__proto__: AbstractPort.prototype,
|
|
_portType: "parent",
|
|
|
|
_dopost: function(data) {
|
|
this._browserPromise.then(browser => {
|
|
browser.messageManager.sendAsyncMessage("frameworker:port-message", data);
|
|
});
|
|
},
|
|
|
|
_onerror: function(err) {
|
|
Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack);
|
|
},
|
|
|
|
_JSONParse: function(data) {
|
|
if (this._clientWindow) {
|
|
return XPCNativeWrapper.unwrap(this._clientWindow).JSON.parse(data);
|
|
}
|
|
return JSON.parse(data);
|
|
},
|
|
|
|
close: function() {
|
|
if (this._closed) {
|
|
return; // already closed.
|
|
}
|
|
// a leaky abstraction due to the worker spec not specifying how the
|
|
// other end of a port knows it is closing.
|
|
this.postMessage({topic: "social.port-closing"});
|
|
AbstractPort.prototype.close.call(this);
|
|
this._clientWindow = null;
|
|
// this._pendingMessagesOutgoing should still be drained, as a closed
|
|
// port will still get "entangled" quickly enough to deliver the messages.
|
|
}
|
|
}
|
|
|
|
// Make the <browser remote="true"> element that hosts the worker.
|
|
function makeRemoteBrowser() {
|
|
let deferred = Promise.defer();
|
|
let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
|
|
// Create a HTML iframe with a chrome URL, then this can host the browser.
|
|
let iframe = hiddenDoc.createElementNS(HTML_NS, "iframe");
|
|
iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
|
|
iframe.addEventListener("load", function onLoad() {
|
|
iframe.removeEventListener("load", onLoad, true);
|
|
let browser = iframe.contentDocument.createElementNS(XUL_NS, "browser");
|
|
browser.setAttribute("type", "content");
|
|
browser.setAttribute("disableglobalhistory", "true");
|
|
browser.setAttribute("remote", "true");
|
|
|
|
iframe.contentDocument.documentElement.appendChild(browser);
|
|
deferred.resolve(browser);
|
|
}, true);
|
|
hiddenDoc.documentElement.appendChild(iframe);
|
|
return deferred.promise;
|
|
}
|
|
|
|
function notifyWorkerError(origin) {
|
|
// Try to retrieve the worker's associated provider, if it has one, to set its
|
|
// error state.
|
|
SocialService.getProvider(origin, function (provider) {
|
|
if (provider)
|
|
provider.errorState = "frameworker-error";
|
|
Services.obs.notifyObservers(null, "social:frameworker-error", origin);
|
|
});
|
|
}
|