Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
1 /* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/.
6 */
8 /*
9 * This is an implementation of a "Shared Worker" using a remote browser
10 * in the hidden DOM window. This is the implementation that lives in the
11 * "chrome process". See FrameWorkerContent for code that lives in the
12 * "content" process and which sets up a sandbox for the worker.
13 */
15 "use strict";
17 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
19 Cu.import("resource://gre/modules/Services.jsm");
20 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
21 Cu.import("resource://gre/modules/MessagePortBase.jsm");
22 Cu.import("resource://gre/modules/Promise.jsm");
24 XPCOMUtils.defineLazyModuleGetter(this, "SocialService",
25 "resource://gre/modules/SocialService.jsm");
27 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
28 const HTML_NS = "http://www.w3.org/1999/xhtml";
30 this.EXPORTED_SYMBOLS = ["getFrameWorkerHandle"];
32 var workerCache = {}; // keyed by URL.
33 var _nextPortId = 1;
35 // Retrieves a reference to a WorkerHandle associated with a FrameWorker and a
36 // new ClientPort.
37 this.getFrameWorkerHandle =
38 function getFrameWorkerHandle(url, clientWindow, name, origin, exposeLocalStorage = false) {
39 // prevent data/about urls - see bug 891516
40 if (['http', 'https'].indexOf(Services.io.newURI(url, null, null).scheme) < 0)
41 throw new Error("getFrameWorkerHandle requires http/https urls");
43 // See if we already have a worker with this URL.
44 let existingWorker = workerCache[url];
45 if (!existingWorker) {
46 // create a remote browser and _Worker object - this will message the
47 // remote browser to do the content side of things.
48 let browserPromise = makeRemoteBrowser();
49 let options = { url: url, name: name, origin: origin,
50 exposeLocalStorage: exposeLocalStorage };
52 existingWorker = workerCache[url] = new _Worker(browserPromise, options);
53 }
55 // message the content so it can establish a new connection with the worker.
56 let portid = _nextPortId++;
57 existingWorker.browserPromise.then(browser => {
58 browser.messageManager.sendAsyncMessage("frameworker:connect",
59 { portId: portid });
60 });
61 // return the pseudo worker object.
62 let port = new ParentPort(portid, existingWorker.browserPromise, clientWindow);
63 existingWorker.ports.set(portid, port);
64 return new WorkerHandle(port, existingWorker);
65 };
67 // A "_Worker" is an internal representation of a worker. It's never returned
68 // directly to consumers.
69 function _Worker(browserPromise, options) {
70 this.browserPromise = browserPromise;
71 this.options = options;
72 this.ports = new Map();
73 browserPromise.then(browser => {
74 browser.addEventListener("oop-browser-crashed", () => {
75 Cu.reportError("FrameWorker remote process crashed");
76 notifyWorkerError(options.origin);
77 });
79 let mm = browser.messageManager;
80 // execute the content script and send the message to bootstrap the content
81 // side of the world.
82 mm.loadFrameScript("resource://gre/modules/FrameWorkerContent.js", true);
83 mm.sendAsyncMessage("frameworker:init", this.options);
84 mm.addMessageListener("frameworker:port-message", this);
85 mm.addMessageListener("frameworker:notify-worker-error", this);
86 });
87 }
89 _Worker.prototype = {
90 // Message handler.
91 receiveMessage: function(msg) {
92 switch (msg.name) {
93 case "frameworker:port-message":
94 let port = this.ports.get(msg.data.portId);
95 port._onmessage(msg.data.data);
96 break;
97 case "frameworker:notify-worker-error":
98 notifyWorkerError(msg.data.origin);
99 break;
100 }
101 }
102 }
104 // This WorkerHandle is exposed to consumers - it has the new port instance
105 // the consumer uses to communicate with the worker.
106 // public methods/properties on WorkerHandle should conform to the SharedWorker
107 // api - currently that's just .port and .terminate()
108 function WorkerHandle(port, worker) {
109 this.port = port;
110 this._worker = worker;
111 }
113 WorkerHandle.prototype = {
114 // A method to terminate the worker. The worker spec doesn't define a
115 // callback to be made in the worker when this happens, so we just kill the
116 // browser element.
117 terminate: function terminate() {
118 let url = this._worker.options.url;
119 if (!(url in workerCache)) {
120 // terminating an already terminated worker - ignore it
121 return;
122 }
123 delete workerCache[url];
124 // close all the ports we have handed out.
125 for (let [portid, port] of this._worker.ports) {
126 port.close();
127 }
128 this._worker.ports.clear();
129 this._worker.ports = null;
130 this._worker.browserPromise.then(browser => {
131 let iframe = browser.ownerDocument.defaultView.frameElement;
132 iframe.parentNode.removeChild(iframe);
133 });
134 // wipe things out just incase other reference have snuck out somehow...
135 this._worker.browserPromise = null;
136 this._worker = null;
137 }
138 };
140 // The port that lives in the parent chrome process. The other end of this
141 // port is the "client" port in the content process, which itself is just a
142 // shim which shuttles messages to/from the worker itself.
143 function ParentPort(portid, browserPromise, clientWindow) {
144 this._clientWindow = clientWindow;
145 this._browserPromise = browserPromise;
146 AbstractPort.call(this, portid);
147 }
149 ParentPort.prototype = {
150 __exposedProps__: {
151 onmessage: "rw",
152 postMessage: "r",
153 close: "r",
154 toString: "r"
155 },
156 __proto__: AbstractPort.prototype,
157 _portType: "parent",
159 _dopost: function(data) {
160 this._browserPromise.then(browser => {
161 browser.messageManager.sendAsyncMessage("frameworker:port-message", data);
162 });
163 },
165 _onerror: function(err) {
166 Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack);
167 },
169 _JSONParse: function(data) {
170 if (this._clientWindow) {
171 return XPCNativeWrapper.unwrap(this._clientWindow).JSON.parse(data);
172 }
173 return JSON.parse(data);
174 },
176 close: function() {
177 if (this._closed) {
178 return; // already closed.
179 }
180 // a leaky abstraction due to the worker spec not specifying how the
181 // other end of a port knows it is closing.
182 this.postMessage({topic: "social.port-closing"});
183 AbstractPort.prototype.close.call(this);
184 this._clientWindow = null;
185 // this._pendingMessagesOutgoing should still be drained, as a closed
186 // port will still get "entangled" quickly enough to deliver the messages.
187 }
188 }
190 // Make the <browser remote="true"> element that hosts the worker.
191 function makeRemoteBrowser() {
192 let deferred = Promise.defer();
193 let hiddenDoc = Services.appShell.hiddenDOMWindow.document;
194 // Create a HTML iframe with a chrome URL, then this can host the browser.
195 let iframe = hiddenDoc.createElementNS(HTML_NS, "iframe");
196 iframe.setAttribute("src", "chrome://global/content/mozilla.xhtml");
197 iframe.addEventListener("load", function onLoad() {
198 iframe.removeEventListener("load", onLoad, true);
199 let browser = iframe.contentDocument.createElementNS(XUL_NS, "browser");
200 browser.setAttribute("type", "content");
201 browser.setAttribute("disableglobalhistory", "true");
202 browser.setAttribute("remote", "true");
204 iframe.contentDocument.documentElement.appendChild(browser);
205 deferred.resolve(browser);
206 }, true);
207 hiddenDoc.documentElement.appendChild(iframe);
208 return deferred.promise;
209 }
211 function notifyWorkerError(origin) {
212 // Try to retrieve the worker's associated provider, if it has one, to set its
213 // error state.
214 SocialService.getProvider(origin, function (provider) {
215 if (provider)
216 provider.errorState = "frameworker-error";
217 Services.obs.notifyObservers(null, "social:frameworker-error", origin);
218 });
219 }