/* -*- indent-tabs-mode: nil; js-indent-level: 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.call(this, require, exports); // } else { // loadSubScript // if (this.require) { // factory.call(this, require, this); // } else { // const Cu = Components.utils; // const { require } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); // factory.call(this, require, this); // } // } // }).call(this, function (require, exports) { "use strict"; // const { Cc, Ci, Cr, Cu, CC } = require("chrome"); // const Services = require("Services"); // const DevToolsUtils = require("devtools/toolkit/DevToolsUtils"); // const { dumpn, dumpv } = DevToolsUtils; // const StreamUtils = require("devtools/toolkit/transport/stream-utils"); // const { Packet, JSONPacket, BulkPacket } = // require("devtools/toolkit/transport/packets"); // const promise = require("promise"); // const EventEmitter = require("devtools/toolkit/event-emitter"); // DevToolsUtils.defineLazyGetter(this, "Pipe", () => { // return CC("@mozilla.org/pipe;1", "nsIPipe", "init"); // }); // DevToolsUtils.defineLazyGetter(this, "ScriptableInputStream", () => { // return CC("@mozilla.org/scriptableinputstream;1", // "nsIScriptableInputStream", "init"); // }); const PACKET_HEADER_MAX = 200; /** * 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 input nsIAsyncInputStream * The input stream. * @param output 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() / dt.startBulkSend() to send packets. * 4) Call dt.close() to close the connection, and disengage from the event * loop. * * A packet handler is an object with the following 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. * * - onBulkPacket(packet) - called when we have switched to bulk packet * receiving mode. |packet| is an object containing: * * actor: Name of actor that will receive the packet * * type: Name of actor's method that should be called on receipt * * length: Size of the data to be read * * stream: This input stream should only be used directly if you can ensure * that you will read exactly |length| bytes and will not close the * stream when reading is complete * * done: If you use the stream directly (instead of |copyTo| below), you * must signal completion by resolving / rejecting this deferred. * If it's rejected, the transport will be closed. If an Error is * supplied as a rejection value, it will be logged via |dumpn|. * If you do use |copyTo|, resolving is taken care of for you when * copying completes. * * copyTo: A helper function for getting your data out of the stream that * meets the stream handling requirements above, and has the * following signature: * @param output nsIAsyncOutputStream * The stream to copy to. * @return Promise * The promise is resolved when copying completes or rejected if any * (unexpected) errors occur. * This object also emits "progress" events for each chunk that is * copied. See stream-utils.js. * * - onClosed(reason) - called when the connection is closed. |reason| is * an optional nsresult or object, typically passed when the transport is * closed due to some error in a underlying stream. * * See ./packets.js and the Remote Debugging Protocol specification for more * details on the format of these packets. */ function DebuggerTransport(input, output) { // EventEmitter.decorate(this); this._input = input; // this._scriptableInput = new ScriptableInputStream(input); this._output = output; // The current incoming (possibly partial) header, which will determine which // type of Packet |_incoming| below will become. this._incomingHeader = ""; // The current incoming Packet object // this._incoming = null; this._incoming = ""; // A queue of outgoing Packet objects // this._outgoing = []; this._outgoing = ""; this.hooks = null; this.active = false; this._incomingEnabled = true; this._outgoingEnabled = true; this.close = this.close.bind(this); } DebuggerTransport.prototype = { /** * Transmit an object as a JSON 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(object) { // this.emit("send", object); // let packet = new JSONPacket(this); // packet.object = object; // this._outgoing.push(packet); let data = JSON.stringify(object); let data_for_len = DevToolsUtils.utf16to8(data); this._outgoing = data_for_len.length + ':' + data; this._flushOutgoing(); }, /** * Transmit streaming data via a bulk packet. * * This method initiates the bulk send process by queuing up the header data. * The caller receives eventual access to a stream for writing. * * N.B.: Do *not* attempt to close the stream handed to you, as it will * continue to be used by this transport afterwards. Most users should * instead use the provided |copyFrom| function instead. * * @param header Object * This is modeled after the format of JSON packets above, but does not * actually contain the data, but is instead just a routing header: * * actor: Name of actor that will receive the packet * * type: Name of actor's method that should be called on receipt * * length: Size of the data to be sent * @return Promise * The promise will be resolved when you are allowed to write to the * stream with an object containing: * * stream: This output stream should only be used directly if * you can ensure that you will write exactly |length| * bytes and will not close the stream when writing is * complete * * done: If you use the stream directly (instead of |copyFrom| * below), you must signal completion by resolving / * rejecting this deferred. If it's rejected, the * transport will be closed. If an Error is supplied as * a rejection value, it will be logged via |dumpn|. If * you do use |copyFrom|, resolving is taken care of for * you when copying completes. * * copyFrom: A helper function for getting your data onto the * stream that meets the stream handling requirements * above, and has the following signature: * @param input nsIAsyncInputStream * The stream to copy from. * @return Promise * The promise is resolved when copying completes or * rejected if any (unexpected) errors occur. * This object also emits "progress" events for each chunk * that is copied. See stream-utils.js. */ startBulkSend: function(header) { this.emit("startBulkSend", header); let packet = new BulkPacket(this); packet.header = header; this._outgoing.push(packet); this._flushOutgoing(); return packet.streamReadyForWriting; }, /** * Close the transport. * @param reason nsresult / object (optional) * The status code or error message that corresponds to the reason for * closing the transport (likely because a stream closed or failed). */ close: function(reason) { this.emit("onClosed", reason); this.active = false; this._input.close(); this._scriptableInput.close(); this._output.close(); this._destroyIncoming(); this._destroyAllOutgoing(); if (this.hooks) { this.hooks.onClosed(reason); this.hooks = null; } if (reason) { dumpn("Transport closed: " + DevToolsUtils.safeErrorString(reason)); } else { dumpn("Transport closed."); } }, /** * The currently outgoing packet (at the top of the queue). */ get _currentOutgoing() { return this._outgoing[0]; }, /** * Flush data to the outgoing stream. Waits until the output stream notifies * us that it is ready to be written to (via onOutputStreamReady). */ _flushOutgoing: function() { if (!this._outgoingEnabled || this._outgoing.length === 0) { return; } // If the top of the packet queue has nothing more to send, remove it. if (this._currentOutgoing.done) { this._finishCurrentOutgoing(); } if (this._outgoing.length > 0) { // var threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); // this._output.asyncWait(this, 0, 0, threadManager.currentThread); // log('send: ' + this._outgoing); _bufferWrite(this._outgoing); } }, /** * Pause this transport's attempts to write to the output stream. This is * used when we've temporarily handed off our output stream for writing bulk * data. */ pauseOutgoing: function() { this._outgoingEnabled = false; }, /** * Resume this transport's attempts to write to the output stream. */ resumeOutgoing: function() { this._outgoingEnabled = true; this._flushOutgoing(); }, // nsIOutputStreamCallback /** * This is called when the output stream is ready for more data to be written. * The current outgoing packet will attempt to write some amount of data, but * may not complete. */ onOutputStreamReady: DevToolsUtils.makeInfallible(function(stream) { if (!this._outgoingEnabled || this._outgoing.length === 0) { return; } try { this._currentOutgoing.write(stream); } catch(e) { if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) { this.close(e.result); return; } else { throw e; } } this._flushOutgoing(); }, "DebuggerTransport.prototype.onOutputStreamReady"), /** * Remove the current outgoing packet from the queue upon completion. */ _finishCurrentOutgoing: function() { if (this._currentOutgoing) { this._currentOutgoing.destroy(); this._outgoing.shift(); } }, /** * Clear the entire outgoing queue. */ _destroyAllOutgoing: function() { for (let packet of this._outgoing) { packet.destroy(); } this._outgoing = []; }, /** * Initialize the input stream for reading. Once this method has been called, * we watch for packets on the input stream, and pass them to the appropriate * handlers via this.hooks. */ ready: function() { this.active = true; this._waitForIncoming(); }, /** * Asks the input stream to notify us (via onInputStreamReady) when it is * ready for reading. */ _waitForIncoming: function() { // if (this._incomingEnabled) { // let threadManager = Cc["@mozilla.org/thread-manager;1"].getService(); // this._input.asyncWait(this, 0, 0, threadManager.currentThread); // } }, /** * Pause this transport's attempts to read from the input stream. This is * used when we've temporarily handed off our input stream for reading bulk * data. */ pauseIncoming: function() { this._incomingEnabled = false; }, /** * Resume this transport's attempts to read from the input stream. */ resumeIncoming: function() { this._incomingEnabled = true; this._flushIncoming(); this._waitForIncoming(); }, // nsIInputStreamCallback /** * Called when the stream is either readable or closed. */ onInputStreamReady: DevToolsUtils.makeInfallible(function(stream) { try { while(stream.available() && this._incomingEnabled && this._processIncoming(stream, stream.available())) {} this._waitForIncoming(); } catch(e) { if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK) { this.close(e.result); } else { throw e; } } }, "DebuggerTransport.prototype.onInputStreamReady"), /** * Process the incoming data. Will create a new currently incoming Packet if * needed. Tells the incoming Packet to read as much data as it can, but * reading may not complete. The Packet signals that its data is ready for * delivery by calling one of this transport's _on*Ready methods (see * ./packets.js and the _on*Ready methods below). * @return boolean * Whether incoming stream processing should continue for any * remaining data. */ _processIncoming: function(stream, count) { dumpv("Data available: " + count); if (!count) { dumpv("Nothing to read, skipping"); return false; } try { if (!this._incoming) { dumpv("Creating a new packet from incoming"); if (!this._readHeader(stream)) { return false; // Not enough data to read packet type } // Attempt to create a new Packet by trying to parse each possible // header pattern. this._incoming = Packet.fromHeader(this._incomingHeader, this); if (!this._incoming) { throw new Error("No packet types for header: " + this._incomingHeader); } } if (!this._incoming.done) { // We have an incomplete packet, keep reading it. dumpv("Existing packet incomplete, keep reading"); this._incoming.read(stream, this._scriptableInput); } } catch(e) { let msg = "Error reading incoming packet: (" + e + " - " + e.stack + ")"; dumpn(msg); // Now in an invalid state, shut down the transport. this.close(); return false; } if (!this._incoming.done) { // Still not complete, we'll wait for more data. dumpv("Packet not done, wait for more"); return true; } // Ready for next packet this._flushIncoming(); return true; }, /** * Read as far as we can into the incoming data, attempting to build up a * complete packet header (which terminates with ":"). We'll only read up to * PACKET_HEADER_MAX characters. * @return boolean * True if we now have a complete header. */ _readHeader: function() { let amountToRead = PACKET_HEADER_MAX - this._incomingHeader.length; this._incomingHeader += StreamUtils.delimitedRead(this._scriptableInput, ":", amountToRead); if (dumpv.wantVerbose) { dumpv("Header read: " + this._incomingHeader); } if (this._incomingHeader.endsWith(":")) { if (dumpv.wantVerbose) { dumpv("Found packet header successfully: " + this._incomingHeader); } return true; } if (this._incomingHeader.length >= PACKET_HEADER_MAX) { throw new Error("Failed to parse packet header!"); } // Not enough data yet. return false; }, /** * If the incoming packet is done, log it as needed and clear the buffer. */ _flushIncoming: function() { if (!this._incoming.done) { return; } if (dumpn.wantLogging) { dumpn("Got: " + this._incoming); } this._destroyIncoming(); }, /** * Handler triggered by an incoming JSONPacket completing it's |read| method. * Delivers the packet to this.hooks.onPacket. */ _onJSONObjectReady: function(object) { DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => { // Ensure the transport is still alive by the time this runs. if (this.active) { this.emit("onPacket", object); this.hooks.onPacket(object); } }, "DebuggerTransport instance's this.hooks.onPacket")); }, /** * Handler triggered by an incoming BulkPacket entering the |read| phase for * the stream portion of the packet. Delivers info about the incoming * streaming data to this.hooks.onBulkPacket. See the main comment on the * transport at the top of this file for more details. */ _onBulkReadReady: function(...args) { DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => { // Ensure the transport is still alive by the time this runs. if (this.active) { this.emit("onBulkPacket", ...args); this.hooks.onBulkPacket(...args); } }, "DebuggerTransport instance's this.hooks.onBulkPacket")); }, /** * Remove all handlers and references related to the current incoming packet, * either because it is now complete or because the transport is closing. */ _destroyIncoming: function() { if (this._incoming) { this._incoming.destroy(); } this._incomingHeader = ""; this._incoming = null; } }; // 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 other LocalDebuggerTransport * The other endpoint for this debugger connection. * * @see DebuggerTransport */ function LocalDebuggerTransport(other) { // EventEmitter.decorate(this); this.other = other; 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 }; this.close = this.close.bind(this); } LocalDebuggerTransport.prototype = { /** * Transmit a message by directly calling the onPacket handler of the other * endpoint. */ send: function(packet) { // this.emit("send", packet); let serial = this._serial.count++; // if (dumpn.wantLogging) { // /* Check 'from' first, as 'echo' packets have both. */ // if (packet.from) { // dumpn("Packet " + serial + " sent from " + uneval(packet.from)); // } else if (packet.to) { // dumpn("Packet " + serial + " sent to " + uneval(packet.to)); // } // } this._deepFreeze(packet); let other = this.other; if (other && other.hooks) { log("Received packet " + serial + ": " + JSON.stringify(packet, null, 2)); other.hooks.onPacket(packet); } // if (other) { // DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => { // // Avoid the cost of JSON.stringify() when logging is disabled. // if (dumpn.wantLogging) { // dumpn("Received packet " + serial + ": " + JSON.stringify(packet, null, 2)); // } // if (other.hooks) { // other.emit("onPacket", packet); // other.hooks.onPacket(packet); // } // }, "LocalDebuggerTransport instance's this.other.hooks.onPacket")); // } }, /** * Send a streaming bulk packet directly to the onBulkPacket handler of the * other endpoint. * * This case is much simpler than the full DebuggerTransport, since there is * no primary stream we have to worry about managing while we hand it off to * others temporarily. Instead, we can just make a single use pipe and be * done with it. */ startBulkSend: function({actor, type, length}) { this.emit("startBulkSend", {actor, type, length}); let serial = this._serial.count++; dumpn("Sent bulk packet " + serial + " for actor " + actor); if (!this.other) { return; } let pipe = new Pipe(true, true, 0, 0, null); DevToolsUtils.executeSoon(DevToolsUtils.makeInfallible(() => { dumpn("Received bulk packet " + serial); if (!this.other.hooks) { return; } // Receiver let deferred = promise.defer(); let packet = { actor: actor, type: type, length: length, copyTo: (output) => { let copying = StreamUtils.copyStream(pipe.inputStream, output, length); deferred.resolve(copying); return copying; }, stream: pipe.inputStream, done: deferred }; this.other.emit("onBulkPacket", packet); this.other.hooks.onBulkPacket(packet); // Await the result of reading from the stream deferred.promise.then(() => pipe.inputStream.close(), this.close); }, "LocalDebuggerTransport instance's this.other.hooks.onBulkPacket")); // Sender let sendDeferred = promise.defer(); // The remote transport is not capable of resolving immediately here, so we // shouldn't be able to either. DevToolsUtils.executeSoon(() => { let copyDeferred = promise.defer(); sendDeferred.resolve({ copyFrom: (input) => { let copying = StreamUtils.copyStream(input, pipe.outputStream, length); copyDeferred.resolve(copying); return copying; }, stream: pipe.outputStream, done: copyDeferred }); // Await the result of writing to the stream copyDeferred.promise.then(() => pipe.outputStream.close(), this.close); }); return sendDeferred.promise; }, /** * Close the transport. */ close: function() { this.emit("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() {}, /** * Helper function that makes an object fully immutable. */ _deepFreeze: function(object) { Object.freeze(object); for (let prop in object) { // 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 (object.hasOwnProperty(prop) && typeof object === "object" && !Object.isFrozen(object)) { 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, |sender| should be the nsIMessageSender for the * child process. In a child process, |sender| should be the child process * message manager, which sends packets to the parent. * * |prefix| 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 |prefix|, whose data is the protocol packet. */ // function ChildDebuggerTransport(sender, prefix) { // EventEmitter.decorate(this); // this._sender = sender.QueryInterface(Ci.nsIMessageSender); // this._messageName = "debug:" + prefix + ":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.emit("onClosed"); // this.hooks.onClosed(); // }, // receiveMessage: function ({data}) { // this.emit("onPacket", data); // this.hooks.onPacket(data); // }, // send: function (packet) { // this.emit("send", packet); // this._sender.sendAsyncMessage(this._messageName, packet); // }, // startBulkSend: function() { // throw new Error("Can't send bulk data to child processes."); // } // }; // exports.ChildDebuggerTransport = ChildDebuggerTransport; // // WorkerDebuggerTransport is defined differently depending on whether we are // // on the main thread or a worker thread. In the former case, we are required // // by the devtools loader, and isWorker will be false. Otherwise, we are // // required by the worker loader, and isWorker will be true. // // // // Each worker debugger supports only a single connection to the main thread. // // However, its theoretically possible for multiple servers to connect to the // // same worker. Consequently, each transport has a connection id, to allow // // messages from multiple connections to be multiplexed on a single channel. // if (!this.isWorker) { // (function () { // Main thread // /** // * A transport that uses a WorkerDebugger to send packets from the main // * thread to a worker thread. // */ // function WorkerDebuggerTransport(dbg, id) { // this._dbg = dbg; // this._id = id; // this.onMessage = this._onMessage.bind(this); // } // WorkerDebuggerTransport.prototype = { // constructor: WorkerDebuggerTransport, // ready: function () { // this._dbg.addListener(this); // }, // close: function () { // this._dbg.removeListener(this); // if (this.hooks) { // this.hooks.onClosed(); // } // }, // send: function (packet) { // this._dbg.postMessage(JSON.stringify({ // type: "message", // id: this._id, // message: packet // })); // }, // startBulkSend: function () { // throw new Error("Can't send bulk data from worker threads!"); // }, // _onMessage: function (message) { // let packet = JSON.parse(message); // if (packet.type !== "message" || packet.id !== this._id) { // return; // } // if (this.hooks) { // this.hooks.onPacket(packet.message); // } // } // }; // exports.WorkerDebuggerTransport = WorkerDebuggerTransport; // }).call(this); // } else { // (function () { // Worker thread // /* // * A transport that uses a WorkerDebuggerGlobalScope to send packets from a // * worker thread to the main thread. // */ // function WorkerDebuggerTransport(scope, id) { // this._scope = scope; // this._id = id; // this._onMessage = this._onMessage.bind(this); // } // WorkerDebuggerTransport.prototype = { // constructor: WorkerDebuggerTransport, // ready: function () { // this._scope.addEventListener("message", this._onMessage); // }, // close: function () { // this._scope.removeEventListener("message", this._onMessage); // if (this.hooks) { // this.hooks.onClosed(); // } // }, // send: function (packet) { // this._scope.postMessage(JSON.stringify({ // type: "message", // id: this._id, // message: packet // })); // }, // startBulkSend: function () { // throw new Error("Can't send bulk data from worker threads!"); // }, // _onMessage: function (event) { // let packet = JSON.parse(event.data); // if (packet.type !== "message" || packet.id !== this._id) { // return; // } // if (this.hooks) { // this.hooks.onPacket(packet.message); // } // } // }; // exports.WorkerDebuggerTransport = WorkerDebuggerTransport; // }).call(this); // } // });