|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript 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 |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 // TODO: Get rid of this code once the marionette server loads transport.js as |
|
8 // an SDK module (see bug 1000814) |
|
9 (function (factory) { // Module boilerplate |
|
10 if (this.module && module.id.indexOf("transport") >= 0) { // require |
|
11 factory(require, exports); |
|
12 } else { // loadSubScript |
|
13 if (this.require) { |
|
14 factory(require, this); |
|
15 } else { |
|
16 const Cu = Components.utils; |
|
17 const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); |
|
18 factory(devtools.require, this); |
|
19 } |
|
20 } |
|
21 }).call(this, function (require, exports) { |
|
22 |
|
23 "use strict"; |
|
24 |
|
25 const { Cc, Ci, Cr, Cu } = require("chrome"); |
|
26 const Services = require("Services"); |
|
27 const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); |
|
28 const { dumpn } = DevToolsUtils; |
|
29 |
|
30 Cu.import("resource://gre/modules/NetUtil.jsm"); |
|
31 |
|
32 /** |
|
33 * An adapter that handles data transfers between the debugger client and |
|
34 * server. It can work with both nsIPipe and nsIServerSocket transports so |
|
35 * long as the properly created input and output streams are specified. |
|
36 * (However, for intra-process connections, LocalDebuggerTransport, below, |
|
37 * is more efficient than using an nsIPipe pair with DebuggerTransport.) |
|
38 * |
|
39 * @param aInput nsIInputStream |
|
40 * The input stream. |
|
41 * @param aOutput nsIAsyncOutputStream |
|
42 * The output stream. |
|
43 * |
|
44 * Given a DebuggerTransport instance dt: |
|
45 * 1) Set dt.hooks to a packet handler object (described below). |
|
46 * 2) Call dt.ready() to begin watching for input packets. |
|
47 * 3) Call dt.send() to send packets as you please, and handle incoming |
|
48 * packets passed to hook.onPacket. |
|
49 * 4) Call dt.close() to close the connection, and disengage from the event |
|
50 * loop. |
|
51 * |
|
52 * A packet handler is an object with two methods: |
|
53 * |
|
54 * - onPacket(packet) - called when we have received a complete packet. |
|
55 * |Packet| is the parsed form of the packet --- a JavaScript value, not |
|
56 * a JSON-syntax string. |
|
57 * |
|
58 * - onClosed(status) - called when the connection is closed. |Status| is |
|
59 * an nsresult, of the sort passed to nsIRequestObserver. |
|
60 * |
|
61 * Data is transferred as a JSON packet serialized into a string, with the |
|
62 * string length prepended to the packet, followed by a colon |
|
63 * ([length]:[packet]). The contents of the JSON packet are specified in |
|
64 * the Remote Debugging Protocol specification. |
|
65 */ |
|
66 function DebuggerTransport(aInput, aOutput) |
|
67 { |
|
68 this._input = aInput; |
|
69 this._output = aOutput; |
|
70 |
|
71 this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] |
|
72 .createInstance(Ci.nsIScriptableUnicodeConverter); |
|
73 this._converter.charset = "UTF-8"; |
|
74 |
|
75 this._outgoing = ""; |
|
76 this._incoming = ""; |
|
77 |
|
78 this.hooks = null; |
|
79 } |
|
80 |
|
81 DebuggerTransport.prototype = { |
|
82 /** |
|
83 * Transmit a packet. |
|
84 * |
|
85 * This method returns immediately, without waiting for the entire |
|
86 * packet to be transmitted, registering event handlers as needed to |
|
87 * transmit the entire packet. Packets are transmitted in the order |
|
88 * they are passed to this method. |
|
89 */ |
|
90 send: function DT_send(aPacket) { |
|
91 let data = JSON.stringify(aPacket); |
|
92 data = this._converter.ConvertFromUnicode(data); |
|
93 data = data.length + ':' + data; |
|
94 this._outgoing += data; |
|
95 this._flushOutgoing(); |
|
96 }, |
|
97 |
|
98 /** |
|
99 * Close the transport. |
|
100 */ |
|
101 close: function DT_close() { |
|
102 this._input.close(); |
|
103 this._output.close(); |
|
104 }, |
|
105 |
|
106 /** |
|
107 * Flush the outgoing stream. |
|
108 */ |
|
109 _flushOutgoing: function DT_flushOutgoing() { |
|
110 if (this._outgoing.length > 0) { |
|
111 var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); |
|
112 this._output.asyncWait(this, 0, 0, threadManager.currentThread); |
|
113 } |
|
114 }, |
|
115 |
|
116 onOutputStreamReady: |
|
117 DevToolsUtils.makeInfallible(function DT_onOutputStreamReady(aStream) { |
|
118 let written = 0; |
|
119 try { |
|
120 written = aStream.write(this._outgoing, this._outgoing.length); |
|
121 } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED) { |
|
122 dumpn("Connection closed."); |
|
123 this.close(); |
|
124 return; |
|
125 } |
|
126 this._outgoing = this._outgoing.slice(written); |
|
127 this._flushOutgoing(); |
|
128 }, "DebuggerTransport.prototype.onOutputStreamReady"), |
|
129 |
|
130 /** |
|
131 * Initialize the input stream for reading. Once this method has been |
|
132 * called, we watch for packets on the input stream, and pass them to |
|
133 * this.hook.onPacket. |
|
134 */ |
|
135 ready: function DT_ready() { |
|
136 let pump = Cc["@mozilla.org/network/input-stream-pump;1"] |
|
137 .createInstance(Ci.nsIInputStreamPump); |
|
138 pump.init(this._input, -1, -1, 0, 0, false); |
|
139 pump.asyncRead(this, null); |
|
140 }, |
|
141 |
|
142 // nsIStreamListener |
|
143 onStartRequest: |
|
144 DevToolsUtils.makeInfallible(function DT_onStartRequest(aRequest, aContext) {}, |
|
145 "DebuggerTransport.prototype.onStartRequest"), |
|
146 |
|
147 onStopRequest: |
|
148 DevToolsUtils.makeInfallible(function DT_onStopRequest(aRequest, aContext, aStatus) { |
|
149 this.close(); |
|
150 if (this.hooks) { |
|
151 this.hooks.onClosed(aStatus); |
|
152 this.hooks = null; |
|
153 } |
|
154 }, "DebuggerTransport.prototype.onStopRequest"), |
|
155 |
|
156 onDataAvailable: |
|
157 DevToolsUtils.makeInfallible(function DT_onDataAvailable(aRequest, aContext, |
|
158 aStream, aOffset, aCount) { |
|
159 this._incoming += NetUtil.readInputStreamToString(aStream, |
|
160 aStream.available()); |
|
161 while (this._processIncoming()) {}; |
|
162 }, "DebuggerTransport.prototype.onDataAvailable"), |
|
163 |
|
164 /** |
|
165 * Process incoming packets. Returns true if a packet has been received, either |
|
166 * if it was properly parsed or not. Returns false if the incoming stream does |
|
167 * not contain a full packet yet. After a proper packet is parsed, the dispatch |
|
168 * handler DebuggerTransport.hooks.onPacket is called with the packet as a |
|
169 * parameter. |
|
170 */ |
|
171 _processIncoming: function DT__processIncoming() { |
|
172 // Well this is ugly. |
|
173 let sep = this._incoming.indexOf(':'); |
|
174 if (sep < 0) { |
|
175 // Incoming packet length is too big anyway - drop the connection. |
|
176 if (this._incoming.length > 20) { |
|
177 this.close(); |
|
178 } |
|
179 |
|
180 return false; |
|
181 } |
|
182 |
|
183 let count = this._incoming.substring(0, sep); |
|
184 // Check for a positive number with no garbage afterwards. |
|
185 if (!/^[0-9]+$/.exec(count)) { |
|
186 this.close(); |
|
187 return false; |
|
188 } |
|
189 |
|
190 count = +count; |
|
191 if (this._incoming.length - (sep + 1) < count) { |
|
192 // Don't have a complete request yet. |
|
193 return false; |
|
194 } |
|
195 |
|
196 // We have a complete request, pluck it out of the data and parse it. |
|
197 this._incoming = this._incoming.substring(sep + 1); |
|
198 let packet = this._incoming.substring(0, count); |
|
199 this._incoming = this._incoming.substring(count); |
|
200 |
|
201 try { |
|
202 packet = this._converter.ConvertToUnicode(packet); |
|
203 var parsed = JSON.parse(packet); |
|
204 } catch(e) { |
|
205 let msg = "Error parsing incoming packet: " + packet + " (" + e + " - " + e.stack + ")"; |
|
206 if (Cu.reportError) { |
|
207 Cu.reportError(msg); |
|
208 } |
|
209 dump(msg + "\n"); |
|
210 return true; |
|
211 } |
|
212 |
|
213 if (dumpn.wantLogging) { |
|
214 dumpn("Got: " + JSON.stringify(parsed, null, 2)); |
|
215 } |
|
216 let self = this; |
|
217 Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(function() { |
|
218 // Ensure the hooks are still around by the time this runs (they will go |
|
219 // away when the transport is closed). |
|
220 if (self.hooks) { |
|
221 self.hooks.onPacket(parsed); |
|
222 } |
|
223 }, "DebuggerTransport instance's this.hooks.onPacket"), 0); |
|
224 |
|
225 return true; |
|
226 } |
|
227 } |
|
228 |
|
229 exports.DebuggerTransport = DebuggerTransport; |
|
230 |
|
231 /** |
|
232 * An adapter that handles data transfers between the debugger client and |
|
233 * server when they both run in the same process. It presents the same API as |
|
234 * DebuggerTransport, but instead of transmitting serialized messages across a |
|
235 * connection it merely calls the packet dispatcher of the other side. |
|
236 * |
|
237 * @param aOther LocalDebuggerTransport |
|
238 * The other endpoint for this debugger connection. |
|
239 * |
|
240 * @see DebuggerTransport |
|
241 */ |
|
242 function LocalDebuggerTransport(aOther) |
|
243 { |
|
244 this.other = aOther; |
|
245 this.hooks = null; |
|
246 |
|
247 /* |
|
248 * A packet number, shared between this and this.other. This isn't used |
|
249 * by the protocol at all, but it makes the packet traces a lot easier to |
|
250 * follow. |
|
251 */ |
|
252 this._serial = this.other ? this.other._serial : { count: 0 }; |
|
253 } |
|
254 |
|
255 LocalDebuggerTransport.prototype = { |
|
256 /** |
|
257 * Transmit a message by directly calling the onPacket handler of the other |
|
258 * endpoint. |
|
259 */ |
|
260 send: function LDT_send(aPacket) { |
|
261 let serial = this._serial.count++; |
|
262 if (dumpn.wantLogging) { |
|
263 /* Check 'from' first, as 'echo' packets have both. */ |
|
264 if (aPacket.from) { |
|
265 dumpn("Packet " + serial + " sent from " + uneval(aPacket.from)); |
|
266 } else if (aPacket.to) { |
|
267 dumpn("Packet " + serial + " sent to " + uneval(aPacket.to)); |
|
268 } |
|
269 } |
|
270 this._deepFreeze(aPacket); |
|
271 let other = this.other; |
|
272 if (other) { |
|
273 Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(function() { |
|
274 // Avoid the cost of JSON.stringify() when logging is disabled. |
|
275 if (dumpn.wantLogging) { |
|
276 dumpn("Received packet " + serial + ": " + JSON.stringify(aPacket, null, 2)); |
|
277 } |
|
278 if (other.hooks) { |
|
279 other.hooks.onPacket(aPacket); |
|
280 } |
|
281 }, "LocalDebuggerTransport instance's this.other.hooks.onPacket"), 0); |
|
282 } |
|
283 }, |
|
284 |
|
285 /** |
|
286 * Close the transport. |
|
287 */ |
|
288 close: function LDT_close() { |
|
289 if (this.other) { |
|
290 // Remove the reference to the other endpoint before calling close(), to |
|
291 // avoid infinite recursion. |
|
292 let other = this.other; |
|
293 this.other = null; |
|
294 other.close(); |
|
295 } |
|
296 if (this.hooks) { |
|
297 try { |
|
298 this.hooks.onClosed(); |
|
299 } catch(ex) { |
|
300 Cu.reportError(ex); |
|
301 } |
|
302 this.hooks = null; |
|
303 } |
|
304 }, |
|
305 |
|
306 /** |
|
307 * An empty method for emulating the DebuggerTransport API. |
|
308 */ |
|
309 ready: function LDT_ready() {}, |
|
310 |
|
311 /** |
|
312 * Helper function that makes an object fully immutable. |
|
313 */ |
|
314 _deepFreeze: function LDT_deepFreeze(aObject) { |
|
315 Object.freeze(aObject); |
|
316 for (let prop in aObject) { |
|
317 // Freeze the properties that are objects, not on the prototype, and not |
|
318 // already frozen. Note that this might leave an unfrozen reference |
|
319 // somewhere in the object if there is an already frozen object containing |
|
320 // an unfrozen object. |
|
321 if (aObject.hasOwnProperty(prop) && typeof aObject === "object" && |
|
322 !Object.isFrozen(aObject)) { |
|
323 this._deepFreeze(o[prop]); |
|
324 } |
|
325 } |
|
326 } |
|
327 }; |
|
328 |
|
329 exports.LocalDebuggerTransport = LocalDebuggerTransport; |
|
330 |
|
331 /** |
|
332 * A transport for the debugging protocol that uses nsIMessageSenders to |
|
333 * exchange packets with servers running in child processes. |
|
334 * |
|
335 * In the parent process, |aSender| should be the nsIMessageSender for the |
|
336 * child process. In a child process, |aSender| should be the child process |
|
337 * message manager, which sends packets to the parent. |
|
338 * |
|
339 * aPrefix is a string included in the message names, to distinguish |
|
340 * multiple servers running in the same child process. |
|
341 * |
|
342 * This transport exchanges messages named 'debug:<prefix>:packet', where |
|
343 * <prefix> is |aPrefix|, whose data is the protocol packet. |
|
344 */ |
|
345 function ChildDebuggerTransport(aSender, aPrefix) { |
|
346 this._sender = aSender.QueryInterface(Ci.nsIMessageSender); |
|
347 this._messageName = "debug:" + aPrefix + ":packet"; |
|
348 } |
|
349 |
|
350 /* |
|
351 * To avoid confusion, we use 'message' to mean something that |
|
352 * nsIMessageSender conveys, and 'packet' to mean a remote debugging |
|
353 * protocol packet. |
|
354 */ |
|
355 ChildDebuggerTransport.prototype = { |
|
356 constructor: ChildDebuggerTransport, |
|
357 |
|
358 hooks: null, |
|
359 |
|
360 ready: function () { |
|
361 this._sender.addMessageListener(this._messageName, this); |
|
362 }, |
|
363 |
|
364 close: function () { |
|
365 this._sender.removeMessageListener(this._messageName, this); |
|
366 this.hooks.onClosed(); |
|
367 }, |
|
368 |
|
369 receiveMessage: function ({data}) { |
|
370 this.hooks.onPacket(data); |
|
371 }, |
|
372 |
|
373 send: function (packet) { |
|
374 this._sender.sendAsyncMessage(this._messageName, packet); |
|
375 } |
|
376 }; |
|
377 |
|
378 exports.ChildDebuggerTransport = ChildDebuggerTransport; |
|
379 |
|
380 }); |