toolkit/devtools/server/transport.js

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:4a1eaac91eeb
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 });

mercurial