|
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 */ |
|
7 |
|
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 */ |
|
14 |
|
15 "use strict"; |
|
16 |
|
17 const {classes: Cc, interfaces: Ci, utils: Cu} = Components; |
|
18 |
|
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"); |
|
23 |
|
24 XPCOMUtils.defineLazyModuleGetter(this, "SocialService", |
|
25 "resource://gre/modules/SocialService.jsm"); |
|
26 |
|
27 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; |
|
28 const HTML_NS = "http://www.w3.org/1999/xhtml"; |
|
29 |
|
30 this.EXPORTED_SYMBOLS = ["getFrameWorkerHandle"]; |
|
31 |
|
32 var workerCache = {}; // keyed by URL. |
|
33 var _nextPortId = 1; |
|
34 |
|
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"); |
|
42 |
|
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 }; |
|
51 |
|
52 existingWorker = workerCache[url] = new _Worker(browserPromise, options); |
|
53 } |
|
54 |
|
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 }; |
|
66 |
|
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 }); |
|
78 |
|
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 } |
|
88 |
|
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 } |
|
103 |
|
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 } |
|
112 |
|
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 }; |
|
139 |
|
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 } |
|
148 |
|
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", |
|
158 |
|
159 _dopost: function(data) { |
|
160 this._browserPromise.then(browser => { |
|
161 browser.messageManager.sendAsyncMessage("frameworker:port-message", data); |
|
162 }); |
|
163 }, |
|
164 |
|
165 _onerror: function(err) { |
|
166 Cu.reportError("FrameWorker: Port " + this + " handler failed: " + err + "\n" + err.stack); |
|
167 }, |
|
168 |
|
169 _JSONParse: function(data) { |
|
170 if (this._clientWindow) { |
|
171 return XPCNativeWrapper.unwrap(this._clientWindow).JSON.parse(data); |
|
172 } |
|
173 return JSON.parse(data); |
|
174 }, |
|
175 |
|
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 } |
|
189 |
|
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"); |
|
203 |
|
204 iframe.contentDocument.documentElement.appendChild(browser); |
|
205 deferred.resolve(browser); |
|
206 }, true); |
|
207 hiddenDoc.documentElement.appendChild(iframe); |
|
208 return deferred.promise; |
|
209 } |
|
210 |
|
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 } |