/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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 }); }); // 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 = { __exposedProps__: { onmessage: "rw", postMessage: "r", close: "r", toString: "r" }, __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 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); }); }