1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/components/social/FrameWorker.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,219 @@ 1.4 +/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ts=2 et sw=2 tw=80: */ 1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.8 + * You can obtain one at http://mozilla.org/MPL/2.0/. 1.9 + */ 1.10 + 1.11 +/* 1.12 + * This is an implementation of a "Shared Worker" using a remote browser 1.13 + * in the hidden DOM window. This is the implementation that lives in the 1.14 + * "chrome process". See FrameWorkerContent for code that lives in the 1.15 + * "content" process and which sets up a sandbox for the worker. 1.16 + */ 1.17 + 1.18 +"use strict"; 1.19 + 1.20 +const {classes: Cc, interfaces: Ci, utils: Cu} = Components; 1.21 + 1.22 +Cu.import("resource://gre/modules/Services.jsm"); 1.23 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.24 +Cu.import("resource://gre/modules/MessagePortBase.jsm"); 1.25 +Cu.import("resource://gre/modules/Promise.jsm"); 1.26 + 1.27 +XPCOMUtils.defineLazyModuleGetter(this, "SocialService", 1.28 + "resource://gre/modules/SocialService.jsm"); 1.29 + 1.30 +const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; 1.31 +const HTML_NS = "http://www.w3.org/1999/xhtml"; 1.32 + 1.33 +this.EXPORTED_SYMBOLS = ["getFrameWorkerHandle"]; 1.34 + 1.35 +var workerCache = {}; // keyed by URL. 1.36 +var _nextPortId = 1; 1.37 + 1.38 +// Retrieves a reference to a WorkerHandle associated with a FrameWorker and a 1.39 +// new ClientPort. 1.40 +this.getFrameWorkerHandle = 1.41 + function getFrameWorkerHandle(url, clientWindow, name, origin, exposeLocalStorage = false) { 1.42 + // prevent data/about urls - see bug 891516 1.43 + if (['http', 'https'].indexOf(Services.io.newURI(url, null, null).scheme) < 0) 1.44 + throw new Error("getFrameWorkerHandle requires http/https urls"); 1.45 + 1.46 + // See if we already have a worker with this URL. 1.47 + let existingWorker = workerCache[url]; 1.48 + if (!existingWorker) { 1.49 + // create a remote browser and _Worker object - this will message the 1.50 + // remote browser to do the content side of things. 1.51 + let browserPromise = makeRemoteBrowser(); 1.52 + let options = { url: url, name: name, origin: origin, 1.53 + exposeLocalStorage: exposeLocalStorage }; 1.54 + 1.55 + existingWorker = workerCache[url] = new _Worker(browserPromise, options); 1.56 + } 1.57 + 1.58 + // message the content so it can establish a new connection with the worker. 1.59 + let portid = _nextPortId++; 1.60 + existingWorker.browserPromise.then(browser => { 1.61 + browser.messageManager.sendAsyncMessage("frameworker:connect", 1.62 + { portId: portid }); 1.63 + }); 1.64 + // return the pseudo worker object. 1.65 + let port = new ParentPort(portid, existingWorker.browserPromise, clientWindow); 1.66 + existingWorker.ports.set(portid, port); 1.67 + return new WorkerHandle(port, existingWorker); 1.68 +}; 1.69 + 1.70 +// A "_Worker" is an internal representation of a worker. It's never returned 1.71 +// directly to consumers. 1.72 +function _Worker(browserPromise, options) { 1.73 + this.browserPromise = browserPromise; 1.74 + this.options = options; 1.75 + this.ports = new Map(); 1.76 + browserPromise.then(browser => { 1.77 + browser.addEventListener("oop-browser-crashed", () => { 1.78 + Cu.reportError("FrameWorker remote process crashed"); 1.79 + notifyWorkerError(options.origin); 1.80 + }); 1.81 + 1.82 + let mm = browser.messageManager; 1.83 + // execute the content script and send the message to bootstrap the content 1.84 + // side of the world. 1.85 + mm.loadFrameScript("resource://gre/modules/FrameWorkerContent.js", true); 1.86 + mm.sendAsyncMessage("frameworker:init", this.options); 1.87 + mm.addMessageListener("frameworker:port-message", this); 1.88 + mm.addMessageListener("frameworker:notify-worker-error", this); 1.89 + }); 1.90 +} 1.91 + 1.92 +_Worker.prototype = { 1.93 + // Message handler. 1.94 + receiveMessage: function(msg) { 1.95 + switch (msg.name) { 1.96 + case "frameworker:port-message": 1.97 + let port = this.ports.get(msg.data.portId); 1.98 + port._onmessage(msg.data.data); 1.99 + break; 1.100 + case "frameworker:notify-worker-error": 1.101 + notifyWorkerError(msg.data.origin); 1.102 + break; 1.103 + } 1.104 + } 1.105 +} 1.106 + 1.107 +// This WorkerHandle is exposed to consumers - it has the new port instance 1.108 +// the consumer uses to communicate with the worker. 1.109 +// public methods/properties on WorkerHandle should conform to the SharedWorker 1.110 +// api - currently that's just .port and .terminate() 1.111 +function WorkerHandle(port, worker) { 1.112 + this.port = port; 1.113 + this._worker = worker; 1.114 +} 1.115 + 1.116 +WorkerHandle.prototype = { 1.117 + // A method to terminate the worker. The worker spec doesn't define a 1.118 + // callback to be made in the worker when this happens, so we just kill the 1.119 + // browser element. 1.120 + terminate: function terminate() { 1.121 + let url = this._worker.options.url; 1.122 + if (!(url in workerCache)) { 1.123 + // terminating an already terminated worker - ignore it 1.124 + return; 1.125 + } 1.126 + delete workerCache[url]; 1.127 + // close all the ports we have handed out. 1.128 + for (let [portid, port] of this._worker.ports) { 1.129 + port.close(); 1.130 + } 1.131 + this._worker.ports.clear(); 1.132 + this._worker.ports = null; 1.133 + this._worker.browserPromise.then(browser => { 1.134 + let iframe = browser.ownerDocument.defaultView.frameElement; 1.135 + iframe.parentNode.removeChild(iframe); 1.136 + }); 1.137 + // wipe things out just incase other reference have snuck out somehow... 1.138 + this._worker.browserPromise = null; 1.139 + this._worker = null; 1.140 + } 1.141 +}; 1.142 + 1.143 +// The port that lives in the parent chrome process. The other end of this 1.144 +// port is the "client" port in the content process, which itself is just a 1.145 +// shim which shuttles messages to/from the worker itself. 1.146 +function ParentPort(portid, browserPromise, clientWindow) { 1.147 + this._clientWindow = clientWindow; 1.148 + this._browserPromise = browserPromise; 1.149 + AbstractPort.call(this, portid); 1.150 +} 1.151 + 1.152 +ParentPort.prototype = { 1.153 + __exposedProps__: { 1.154 + onmessage: "rw", 1.155 + postMessage: "r", 1.156 + close: "r", 1.157 + toString: "r" 1.158 + }, 1.159 + __proto__: AbstractPort.prototype, 1.160 + _portType: "parent", 1.161 + 1.162 + _dopost: function(data) { 1.163 + this._browserPromise.then(browser => { 1.164 + browser.messageManager.sendAsyncMessage("frameworker:port-message", data); 1.165 + }); 1.166 + }, 1.167 + 1.168 + _onerror: function(err) { 1.169 + Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack); 1.170 + }, 1.171 + 1.172 + _JSONParse: function(data) { 1.173 + if (this._clientWindow) { 1.174 + return XPCNativeWrapper.unwrap(this._clientWindow).JSON.parse(data); 1.175 + } 1.176 + return JSON.parse(data); 1.177 + }, 1.178 + 1.179 + close: function() { 1.180 + if (this._closed) { 1.181 + return; // already closed. 1.182 + } 1.183 + // a leaky abstraction due to the worker spec not specifying how the 1.184 + // other end of a port knows it is closing. 1.185 + this.postMessage({topic: "social.port-closing"}); 1.186 + AbstractPort.prototype.close.call(this); 1.187 + this._clientWindow = null; 1.188 + // this._pendingMessagesOutgoing should still be drained, as a closed 1.189 + // port will still get "entangled" quickly enough to deliver the messages. 1.190 + } 1.191 +} 1.192 + 1.193 +// Make the <browser remote="true"> element that hosts the worker. 1.194 +function makeRemoteBrowser() { 1.195 + let deferred = Promise.defer(); 1.196 + let hiddenDoc = Services.appShell.hiddenDOMWindow.document; 1.197 + // Create a HTML iframe with a chrome URL, then this can host the browser. 1.198 + let iframe = hiddenDoc.createElementNS(HTML_NS, "iframe"); 1.199 + iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml"); 1.200 + iframe.addEventListener("load", function onLoad() { 1.201 + iframe.removeEventListener("load", onLoad, true); 1.202 + let browser = iframe.contentDocument.createElementNS(XUL_NS, "browser"); 1.203 + browser.setAttribute("type", "content"); 1.204 + browser.setAttribute("disableglobalhistory", "true"); 1.205 + browser.setAttribute("remote", "true"); 1.206 + 1.207 + iframe.contentDocument.documentElement.appendChild(browser); 1.208 + deferred.resolve(browser); 1.209 + }, true); 1.210 + hiddenDoc.documentElement.appendChild(iframe); 1.211 + return deferred.promise; 1.212 +} 1.213 + 1.214 +function notifyWorkerError(origin) { 1.215 + // Try to retrieve the worker's associated provider, if it has one, to set its 1.216 + // error state. 1.217 + SocialService.getProvider(origin, function (provider) { 1.218 + if (provider) 1.219 + provider.errorState = "frameworker-error"; 1.220 + Services.obs.notifyObservers(null, "social:frameworker-error", origin); 1.221 + }); 1.222 +}