1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/devtools/server/transport.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,380 @@ 1.4 +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 1.5 +/* vim: set ft=javascript 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 1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.9 + 1.10 +// TODO: Get rid of this code once the marionette server loads transport.js as 1.11 +// an SDK module (see bug 1000814) 1.12 +(function (factory) { // Module boilerplate 1.13 + if (this.module && module.id.indexOf("transport") >= 0) { // require 1.14 + factory(require, exports); 1.15 + } else { // loadSubScript 1.16 + if (this.require) { 1.17 + factory(require, this); 1.18 + } else { 1.19 + const Cu = Components.utils; 1.20 + const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 1.21 + factory(devtools.require, this); 1.22 + } 1.23 + } 1.24 +}).call(this, function (require, exports) { 1.25 + 1.26 +"use strict"; 1.27 + 1.28 +const { Cc, Ci, Cr, Cu } = require("chrome"); 1.29 +const Services = require("Services"); 1.30 +const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); 1.31 +const { dumpn } = DevToolsUtils; 1.32 + 1.33 +Cu.import("resource://gre/modules/NetUtil.jsm"); 1.34 + 1.35 +/** 1.36 + * An adapter that handles data transfers between the debugger client and 1.37 + * server. It can work with both nsIPipe and nsIServerSocket transports so 1.38 + * long as the properly created input and output streams are specified. 1.39 + * (However, for intra-process connections, LocalDebuggerTransport, below, 1.40 + * is more efficient than using an nsIPipe pair with DebuggerTransport.) 1.41 + * 1.42 + * @param aInput nsIInputStream 1.43 + * The input stream. 1.44 + * @param aOutput nsIAsyncOutputStream 1.45 + * The output stream. 1.46 + * 1.47 + * Given a DebuggerTransport instance dt: 1.48 + * 1) Set dt.hooks to a packet handler object (described below). 1.49 + * 2) Call dt.ready() to begin watching for input packets. 1.50 + * 3) Call dt.send() to send packets as you please, and handle incoming 1.51 + * packets passed to hook.onPacket. 1.52 + * 4) Call dt.close() to close the connection, and disengage from the event 1.53 + * loop. 1.54 + * 1.55 + * A packet handler is an object with two methods: 1.56 + * 1.57 + * - onPacket(packet) - called when we have received a complete packet. 1.58 + * |Packet| is the parsed form of the packet --- a JavaScript value, not 1.59 + * a JSON-syntax string. 1.60 + * 1.61 + * - onClosed(status) - called when the connection is closed. |Status| is 1.62 + * an nsresult, of the sort passed to nsIRequestObserver. 1.63 + * 1.64 + * Data is transferred as a JSON packet serialized into a string, with the 1.65 + * string length prepended to the packet, followed by a colon 1.66 + * ([length]:[packet]). The contents of the JSON packet are specified in 1.67 + * the Remote Debugging Protocol specification. 1.68 + */ 1.69 +function DebuggerTransport(aInput, aOutput) 1.70 +{ 1.71 + this._input = aInput; 1.72 + this._output = aOutput; 1.73 + 1.74 + this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] 1.75 + .createInstance(Ci.nsIScriptableUnicodeConverter); 1.76 + this._converter.charset = "UTF-8"; 1.77 + 1.78 + this._outgoing = ""; 1.79 + this._incoming = ""; 1.80 + 1.81 + this.hooks = null; 1.82 +} 1.83 + 1.84 +DebuggerTransport.prototype = { 1.85 + /** 1.86 + * Transmit a packet. 1.87 + * 1.88 + * This method returns immediately, without waiting for the entire 1.89 + * packet to be transmitted, registering event handlers as needed to 1.90 + * transmit the entire packet. Packets are transmitted in the order 1.91 + * they are passed to this method. 1.92 + */ 1.93 + send: function DT_send(aPacket) { 1.94 + let data = JSON.stringify(aPacket); 1.95 + data = this._converter.ConvertFromUnicode(data); 1.96 + data = data.length + ':' + data; 1.97 + this._outgoing += data; 1.98 + this._flushOutgoing(); 1.99 + }, 1.100 + 1.101 + /** 1.102 + * Close the transport. 1.103 + */ 1.104 + close: function DT_close() { 1.105 + this._input.close(); 1.106 + this._output.close(); 1.107 + }, 1.108 + 1.109 + /** 1.110 + * Flush the outgoing stream. 1.111 + */ 1.112 + _flushOutgoing: function DT_flushOutgoing() { 1.113 + if (this._outgoing.length > 0) { 1.114 + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); 1.115 + this._output.asyncWait(this, 0, 0, threadManager.currentThread); 1.116 + } 1.117 + }, 1.118 + 1.119 + onOutputStreamReady: 1.120 + DevToolsUtils.makeInfallible(function DT_onOutputStreamReady(aStream) { 1.121 + let written = 0; 1.122 + try { 1.123 + written = aStream.write(this._outgoing, this._outgoing.length); 1.124 + } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED) { 1.125 + dumpn("Connection closed."); 1.126 + this.close(); 1.127 + return; 1.128 + } 1.129 + this._outgoing = this._outgoing.slice(written); 1.130 + this._flushOutgoing(); 1.131 + }, "DebuggerTransport.prototype.onOutputStreamReady"), 1.132 + 1.133 + /** 1.134 + * Initialize the input stream for reading. Once this method has been 1.135 + * called, we watch for packets on the input stream, and pass them to 1.136 + * this.hook.onPacket. 1.137 + */ 1.138 + ready: function DT_ready() { 1.139 + let pump = Cc["@mozilla.org/network/input-stream-pump;1"] 1.140 + .createInstance(Ci.nsIInputStreamPump); 1.141 + pump.init(this._input, -1, -1, 0, 0, false); 1.142 + pump.asyncRead(this, null); 1.143 + }, 1.144 + 1.145 + // nsIStreamListener 1.146 + onStartRequest: 1.147 + DevToolsUtils.makeInfallible(function DT_onStartRequest(aRequest, aContext) {}, 1.148 + "DebuggerTransport.prototype.onStartRequest"), 1.149 + 1.150 + onStopRequest: 1.151 + DevToolsUtils.makeInfallible(function DT_onStopRequest(aRequest, aContext, aStatus) { 1.152 + this.close(); 1.153 + if (this.hooks) { 1.154 + this.hooks.onClosed(aStatus); 1.155 + this.hooks = null; 1.156 + } 1.157 + }, "DebuggerTransport.prototype.onStopRequest"), 1.158 + 1.159 + onDataAvailable: 1.160 + DevToolsUtils.makeInfallible(function DT_onDataAvailable(aRequest, aContext, 1.161 + aStream, aOffset, aCount) { 1.162 + this._incoming += NetUtil.readInputStreamToString(aStream, 1.163 + aStream.available()); 1.164 + while (this._processIncoming()) {}; 1.165 + }, "DebuggerTransport.prototype.onDataAvailable"), 1.166 + 1.167 + /** 1.168 + * Process incoming packets. Returns true if a packet has been received, either 1.169 + * if it was properly parsed or not. Returns false if the incoming stream does 1.170 + * not contain a full packet yet. After a proper packet is parsed, the dispatch 1.171 + * handler DebuggerTransport.hooks.onPacket is called with the packet as a 1.172 + * parameter. 1.173 + */ 1.174 + _processIncoming: function DT__processIncoming() { 1.175 + // Well this is ugly. 1.176 + let sep = this._incoming.indexOf(':'); 1.177 + if (sep < 0) { 1.178 + // Incoming packet length is too big anyway - drop the connection. 1.179 + if (this._incoming.length > 20) { 1.180 + this.close(); 1.181 + } 1.182 + 1.183 + return false; 1.184 + } 1.185 + 1.186 + let count = this._incoming.substring(0, sep); 1.187 + // Check for a positive number with no garbage afterwards. 1.188 + if (!/^[0-9]+$/.exec(count)) { 1.189 + this.close(); 1.190 + return false; 1.191 + } 1.192 + 1.193 + count = +count; 1.194 + if (this._incoming.length - (sep + 1) < count) { 1.195 + // Don't have a complete request yet. 1.196 + return false; 1.197 + } 1.198 + 1.199 + // We have a complete request, pluck it out of the data and parse it. 1.200 + this._incoming = this._incoming.substring(sep + 1); 1.201 + let packet = this._incoming.substring(0, count); 1.202 + this._incoming = this._incoming.substring(count); 1.203 + 1.204 + try { 1.205 + packet = this._converter.ConvertToUnicode(packet); 1.206 + var parsed = JSON.parse(packet); 1.207 + } catch(e) { 1.208 + let msg = "Error parsing incoming packet: " + packet + " (" + e + " - " + e.stack + ")"; 1.209 + if (Cu.reportError) { 1.210 + Cu.reportError(msg); 1.211 + } 1.212 + dump(msg + "\n"); 1.213 + return true; 1.214 + } 1.215 + 1.216 + if (dumpn.wantLogging) { 1.217 + dumpn("Got: " + JSON.stringify(parsed, null, 2)); 1.218 + } 1.219 + let self = this; 1.220 + Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(function() { 1.221 + // Ensure the hooks are still around by the time this runs (they will go 1.222 + // away when the transport is closed). 1.223 + if (self.hooks) { 1.224 + self.hooks.onPacket(parsed); 1.225 + } 1.226 + }, "DebuggerTransport instance's this.hooks.onPacket"), 0); 1.227 + 1.228 + return true; 1.229 + } 1.230 +} 1.231 + 1.232 +exports.DebuggerTransport = DebuggerTransport; 1.233 + 1.234 +/** 1.235 + * An adapter that handles data transfers between the debugger client and 1.236 + * server when they both run in the same process. It presents the same API as 1.237 + * DebuggerTransport, but instead of transmitting serialized messages across a 1.238 + * connection it merely calls the packet dispatcher of the other side. 1.239 + * 1.240 + * @param aOther LocalDebuggerTransport 1.241 + * The other endpoint for this debugger connection. 1.242 + * 1.243 + * @see DebuggerTransport 1.244 + */ 1.245 +function LocalDebuggerTransport(aOther) 1.246 +{ 1.247 + this.other = aOther; 1.248 + this.hooks = null; 1.249 + 1.250 + /* 1.251 + * A packet number, shared between this and this.other. This isn't used 1.252 + * by the protocol at all, but it makes the packet traces a lot easier to 1.253 + * follow. 1.254 + */ 1.255 + this._serial = this.other ? this.other._serial : { count: 0 }; 1.256 +} 1.257 + 1.258 +LocalDebuggerTransport.prototype = { 1.259 + /** 1.260 + * Transmit a message by directly calling the onPacket handler of the other 1.261 + * endpoint. 1.262 + */ 1.263 + send: function LDT_send(aPacket) { 1.264 + let serial = this._serial.count++; 1.265 + if (dumpn.wantLogging) { 1.266 + /* Check 'from' first, as 'echo' packets have both. */ 1.267 + if (aPacket.from) { 1.268 + dumpn("Packet " + serial + " sent from " + uneval(aPacket.from)); 1.269 + } else if (aPacket.to) { 1.270 + dumpn("Packet " + serial + " sent to " + uneval(aPacket.to)); 1.271 + } 1.272 + } 1.273 + this._deepFreeze(aPacket); 1.274 + let other = this.other; 1.275 + if (other) { 1.276 + Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(function() { 1.277 + // Avoid the cost of JSON.stringify() when logging is disabled. 1.278 + if (dumpn.wantLogging) { 1.279 + dumpn("Received packet " + serial + ": " + JSON.stringify(aPacket, null, 2)); 1.280 + } 1.281 + if (other.hooks) { 1.282 + other.hooks.onPacket(aPacket); 1.283 + } 1.284 + }, "LocalDebuggerTransport instance's this.other.hooks.onPacket"), 0); 1.285 + } 1.286 + }, 1.287 + 1.288 + /** 1.289 + * Close the transport. 1.290 + */ 1.291 + close: function LDT_close() { 1.292 + if (this.other) { 1.293 + // Remove the reference to the other endpoint before calling close(), to 1.294 + // avoid infinite recursion. 1.295 + let other = this.other; 1.296 + this.other = null; 1.297 + other.close(); 1.298 + } 1.299 + if (this.hooks) { 1.300 + try { 1.301 + this.hooks.onClosed(); 1.302 + } catch(ex) { 1.303 + Cu.reportError(ex); 1.304 + } 1.305 + this.hooks = null; 1.306 + } 1.307 + }, 1.308 + 1.309 + /** 1.310 + * An empty method for emulating the DebuggerTransport API. 1.311 + */ 1.312 + ready: function LDT_ready() {}, 1.313 + 1.314 + /** 1.315 + * Helper function that makes an object fully immutable. 1.316 + */ 1.317 + _deepFreeze: function LDT_deepFreeze(aObject) { 1.318 + Object.freeze(aObject); 1.319 + for (let prop in aObject) { 1.320 + // Freeze the properties that are objects, not on the prototype, and not 1.321 + // already frozen. Note that this might leave an unfrozen reference 1.322 + // somewhere in the object if there is an already frozen object containing 1.323 + // an unfrozen object. 1.324 + if (aObject.hasOwnProperty(prop) && typeof aObject === "object" && 1.325 + !Object.isFrozen(aObject)) { 1.326 + this._deepFreeze(o[prop]); 1.327 + } 1.328 + } 1.329 + } 1.330 +}; 1.331 + 1.332 +exports.LocalDebuggerTransport = LocalDebuggerTransport; 1.333 + 1.334 +/** 1.335 + * A transport for the debugging protocol that uses nsIMessageSenders to 1.336 + * exchange packets with servers running in child processes. 1.337 + * 1.338 + * In the parent process, |aSender| should be the nsIMessageSender for the 1.339 + * child process. In a child process, |aSender| should be the child process 1.340 + * message manager, which sends packets to the parent. 1.341 + * 1.342 + * aPrefix is a string included in the message names, to distinguish 1.343 + * multiple servers running in the same child process. 1.344 + * 1.345 + * This transport exchanges messages named 'debug:<prefix>:packet', where 1.346 + * <prefix> is |aPrefix|, whose data is the protocol packet. 1.347 + */ 1.348 +function ChildDebuggerTransport(aSender, aPrefix) { 1.349 + this._sender = aSender.QueryInterface(Ci.nsIMessageSender); 1.350 + this._messageName = "debug:" + aPrefix + ":packet"; 1.351 +} 1.352 + 1.353 +/* 1.354 + * To avoid confusion, we use 'message' to mean something that 1.355 + * nsIMessageSender conveys, and 'packet' to mean a remote debugging 1.356 + * protocol packet. 1.357 + */ 1.358 +ChildDebuggerTransport.prototype = { 1.359 + constructor: ChildDebuggerTransport, 1.360 + 1.361 + hooks: null, 1.362 + 1.363 + ready: function () { 1.364 + this._sender.addMessageListener(this._messageName, this); 1.365 + }, 1.366 + 1.367 + close: function () { 1.368 + this._sender.removeMessageListener(this._messageName, this); 1.369 + this.hooks.onClosed(); 1.370 + }, 1.371 + 1.372 + receiveMessage: function ({data}) { 1.373 + this.hooks.onPacket(data); 1.374 + }, 1.375 + 1.376 + send: function (packet) { 1.377 + this._sender.sendAsyncMessage(this._messageName, packet); 1.378 + } 1.379 +}; 1.380 + 1.381 +exports.ChildDebuggerTransport = ChildDebuggerTransport; 1.382 + 1.383 +});