diff -r 000000000000 -r 6474c204b198 toolkit/devtools/server/transport.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/toolkit/devtools/server/transport.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,380 @@ +/* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// TODO: Get rid of this code once the marionette server loads transport.js as +// an SDK module (see bug 1000814) +(function (factory) { // Module boilerplate + if (this.module && module.id.indexOf("transport") >= 0) { // require + factory(require, exports); + } else { // loadSubScript + if (this.require) { + factory(require, this); + } else { + const Cu = Components.utils; + const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + factory(devtools.require, this); + } + } +}).call(this, function (require, exports) { + +"use strict"; + +const { Cc, Ci, Cr, Cu } = require("chrome"); +const Services = require("Services"); +const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); +const { dumpn } = DevToolsUtils; + +Cu.import("resource://gre/modules/NetUtil.jsm"); + +/** + * An adapter that handles data transfers between the debugger client and + * server. It can work with both nsIPipe and nsIServerSocket transports so + * long as the properly created input and output streams are specified. + * (However, for intra-process connections, LocalDebuggerTransport, below, + * is more efficient than using an nsIPipe pair with DebuggerTransport.) + * + * @param aInput nsIInputStream + * The input stream. + * @param aOutput nsIAsyncOutputStream + * The output stream. + * + * Given a DebuggerTransport instance dt: + * 1) Set dt.hooks to a packet handler object (described below). + * 2) Call dt.ready() to begin watching for input packets. + * 3) Call dt.send() to send packets as you please, and handle incoming + * packets passed to hook.onPacket. + * 4) Call dt.close() to close the connection, and disengage from the event + * loop. + * + * A packet handler is an object with two methods: + * + * - onPacket(packet) - called when we have received a complete packet. + * |Packet| is the parsed form of the packet --- a JavaScript value, not + * a JSON-syntax string. + * + * - onClosed(status) - called when the connection is closed. |Status| is + * an nsresult, of the sort passed to nsIRequestObserver. + * + * Data is transferred as a JSON packet serialized into a string, with the + * string length prepended to the packet, followed by a colon + * ([length]:[packet]). The contents of the JSON packet are specified in + * the Remote Debugging Protocol specification. + */ +function DebuggerTransport(aInput, aOutput) +{ + this._input = aInput; + this._output = aOutput; + + this._converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"] + .createInstance(Ci.nsIScriptableUnicodeConverter); + this._converter.charset = "UTF-8"; + + this._outgoing = ""; + this._incoming = ""; + + this.hooks = null; +} + +DebuggerTransport.prototype = { + /** + * Transmit a packet. + * + * This method returns immediately, without waiting for the entire + * packet to be transmitted, registering event handlers as needed to + * transmit the entire packet. Packets are transmitted in the order + * they are passed to this method. + */ + send: function DT_send(aPacket) { + let data = JSON.stringify(aPacket); + data = this._converter.ConvertFromUnicode(data); + data = data.length + ':' + data; + this._outgoing += data; + this._flushOutgoing(); + }, + + /** + * Close the transport. + */ + close: function DT_close() { + this._input.close(); + this._output.close(); + }, + + /** + * Flush the outgoing stream. + */ + _flushOutgoing: function DT_flushOutgoing() { + if (this._outgoing.length > 0) { + var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); + this._output.asyncWait(this, 0, 0, threadManager.currentThread); + } + }, + + onOutputStreamReady: + DevToolsUtils.makeInfallible(function DT_onOutputStreamReady(aStream) { + let written = 0; + try { + written = aStream.write(this._outgoing, this._outgoing.length); + } catch(e if e.result == Cr.NS_BASE_STREAM_CLOSED) { + dumpn("Connection closed."); + this.close(); + return; + } + this._outgoing = this._outgoing.slice(written); + this._flushOutgoing(); + }, "DebuggerTransport.prototype.onOutputStreamReady"), + + /** + * Initialize the input stream for reading. Once this method has been + * called, we watch for packets on the input stream, and pass them to + * this.hook.onPacket. + */ + ready: function DT_ready() { + let pump = Cc["@mozilla.org/network/input-stream-pump;1"] + .createInstance(Ci.nsIInputStreamPump); + pump.init(this._input, -1, -1, 0, 0, false); + pump.asyncRead(this, null); + }, + + // nsIStreamListener + onStartRequest: + DevToolsUtils.makeInfallible(function DT_onStartRequest(aRequest, aContext) {}, + "DebuggerTransport.prototype.onStartRequest"), + + onStopRequest: + DevToolsUtils.makeInfallible(function DT_onStopRequest(aRequest, aContext, aStatus) { + this.close(); + if (this.hooks) { + this.hooks.onClosed(aStatus); + this.hooks = null; + } + }, "DebuggerTransport.prototype.onStopRequest"), + + onDataAvailable: + DevToolsUtils.makeInfallible(function DT_onDataAvailable(aRequest, aContext, + aStream, aOffset, aCount) { + this._incoming += NetUtil.readInputStreamToString(aStream, + aStream.available()); + while (this._processIncoming()) {}; + }, "DebuggerTransport.prototype.onDataAvailable"), + + /** + * Process incoming packets. Returns true if a packet has been received, either + * if it was properly parsed or not. Returns false if the incoming stream does + * not contain a full packet yet. After a proper packet is parsed, the dispatch + * handler DebuggerTransport.hooks.onPacket is called with the packet as a + * parameter. + */ + _processIncoming: function DT__processIncoming() { + // Well this is ugly. + let sep = this._incoming.indexOf(':'); + if (sep < 0) { + // Incoming packet length is too big anyway - drop the connection. + if (this._incoming.length > 20) { + this.close(); + } + + return false; + } + + let count = this._incoming.substring(0, sep); + // Check for a positive number with no garbage afterwards. + if (!/^[0-9]+$/.exec(count)) { + this.close(); + return false; + } + + count = +count; + if (this._incoming.length - (sep + 1) < count) { + // Don't have a complete request yet. + return false; + } + + // We have a complete request, pluck it out of the data and parse it. + this._incoming = this._incoming.substring(sep + 1); + let packet = this._incoming.substring(0, count); + this._incoming = this._incoming.substring(count); + + try { + packet = this._converter.ConvertToUnicode(packet); + var parsed = JSON.parse(packet); + } catch(e) { + let msg = "Error parsing incoming packet: " + packet + " (" + e + " - " + e.stack + ")"; + if (Cu.reportError) { + Cu.reportError(msg); + } + dump(msg + "\n"); + return true; + } + + if (dumpn.wantLogging) { + dumpn("Got: " + JSON.stringify(parsed, null, 2)); + } + let self = this; + Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(function() { + // Ensure the hooks are still around by the time this runs (they will go + // away when the transport is closed). + if (self.hooks) { + self.hooks.onPacket(parsed); + } + }, "DebuggerTransport instance's this.hooks.onPacket"), 0); + + return true; + } +} + +exports.DebuggerTransport = DebuggerTransport; + +/** + * An adapter that handles data transfers between the debugger client and + * server when they both run in the same process. It presents the same API as + * DebuggerTransport, but instead of transmitting serialized messages across a + * connection it merely calls the packet dispatcher of the other side. + * + * @param aOther LocalDebuggerTransport + * The other endpoint for this debugger connection. + * + * @see DebuggerTransport + */ +function LocalDebuggerTransport(aOther) +{ + this.other = aOther; + this.hooks = null; + + /* + * A packet number, shared between this and this.other. This isn't used + * by the protocol at all, but it makes the packet traces a lot easier to + * follow. + */ + this._serial = this.other ? this.other._serial : { count: 0 }; +} + +LocalDebuggerTransport.prototype = { + /** + * Transmit a message by directly calling the onPacket handler of the other + * endpoint. + */ + send: function LDT_send(aPacket) { + let serial = this._serial.count++; + if (dumpn.wantLogging) { + /* Check 'from' first, as 'echo' packets have both. */ + if (aPacket.from) { + dumpn("Packet " + serial + " sent from " + uneval(aPacket.from)); + } else if (aPacket.to) { + dumpn("Packet " + serial + " sent to " + uneval(aPacket.to)); + } + } + this._deepFreeze(aPacket); + let other = this.other; + if (other) { + Services.tm.currentThread.dispatch(DevToolsUtils.makeInfallible(function() { + // Avoid the cost of JSON.stringify() when logging is disabled. + if (dumpn.wantLogging) { + dumpn("Received packet " + serial + ": " + JSON.stringify(aPacket, null, 2)); + } + if (other.hooks) { + other.hooks.onPacket(aPacket); + } + }, "LocalDebuggerTransport instance's this.other.hooks.onPacket"), 0); + } + }, + + /** + * Close the transport. + */ + close: function LDT_close() { + if (this.other) { + // Remove the reference to the other endpoint before calling close(), to + // avoid infinite recursion. + let other = this.other; + this.other = null; + other.close(); + } + if (this.hooks) { + try { + this.hooks.onClosed(); + } catch(ex) { + Cu.reportError(ex); + } + this.hooks = null; + } + }, + + /** + * An empty method for emulating the DebuggerTransport API. + */ + ready: function LDT_ready() {}, + + /** + * Helper function that makes an object fully immutable. + */ + _deepFreeze: function LDT_deepFreeze(aObject) { + Object.freeze(aObject); + for (let prop in aObject) { + // Freeze the properties that are objects, not on the prototype, and not + // already frozen. Note that this might leave an unfrozen reference + // somewhere in the object if there is an already frozen object containing + // an unfrozen object. + if (aObject.hasOwnProperty(prop) && typeof aObject === "object" && + !Object.isFrozen(aObject)) { + this._deepFreeze(o[prop]); + } + } + } +}; + +exports.LocalDebuggerTransport = LocalDebuggerTransport; + +/** + * A transport for the debugging protocol that uses nsIMessageSenders to + * exchange packets with servers running in child processes. + * + * In the parent process, |aSender| should be the nsIMessageSender for the + * child process. In a child process, |aSender| should be the child process + * message manager, which sends packets to the parent. + * + * aPrefix is a string included in the message names, to distinguish + * multiple servers running in the same child process. + * + * This transport exchanges messages named 'debug::packet', where + * is |aPrefix|, whose data is the protocol packet. + */ +function ChildDebuggerTransport(aSender, aPrefix) { + this._sender = aSender.QueryInterface(Ci.nsIMessageSender); + this._messageName = "debug:" + aPrefix + ":packet"; +} + +/* + * To avoid confusion, we use 'message' to mean something that + * nsIMessageSender conveys, and 'packet' to mean a remote debugging + * protocol packet. + */ +ChildDebuggerTransport.prototype = { + constructor: ChildDebuggerTransport, + + hooks: null, + + ready: function () { + this._sender.addMessageListener(this._messageName, this); + }, + + close: function () { + this._sender.removeMessageListener(this._messageName, this); + this.hooks.onClosed(); + }, + + receiveMessage: function ({data}) { + this.hooks.onPacket(data); + }, + + send: function (packet) { + this._sender.sendAsyncMessage(this._messageName, packet); + } +}; + +exports.ChildDebuggerTransport = ChildDebuggerTransport; + +});