toolkit/devtools/server/transport.js

changeset 0
6474c204b198
     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 +});

mercurial