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