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