local-transport.js (7885B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 8 const { dumpn } = DevToolsUtils; 9 const flags = require("resource://devtools/shared/flags.js"); 10 const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js"); 11 12 loader.lazyGetter(this, "Pipe", () => { 13 return Components.Constructor("@mozilla.org/pipe;1", "nsIPipe", "init"); 14 }); 15 16 /** 17 * An adapter that handles data transfers between the devtools client and 18 * server when they both run in the same process. It presents the same API as 19 * DebuggerTransport, but instead of transmitting serialized messages across a 20 * connection it merely calls the packet dispatcher of the other side. 21 * 22 * @see DebuggerTransport 23 */ 24 25 class LocalDebuggerTransport { 26 /** 27 * @param {LocalDebuggerTransport} other 28 * The other endpoint for this debugger connection. 29 */ 30 constructor(other) { 31 this.other = other; 32 this.hooks = null; 33 34 // A packet number, shared between this and this.other. This isn't used by the 35 // protocol at all, but it makes the packet traces a lot easier to follow. 36 this._serial = this.other ? this.other._serial : { count: 0 }; 37 this.close = this.close.bind(this); 38 } 39 40 /** 41 * Boolean to help identify DevToolsClient instances connected to a LocalDevToolsTransport pipe 42 * and so connected to the same runtime as the frontend. 43 */ 44 isLocalTransport = true; 45 46 /** 47 * Transmit a message by directly calling the onPacket handler of the other 48 * endpoint. 49 */ 50 send(packet) { 51 const serial = this._serial.count++; 52 if (flags.wantLogging) { 53 // Check 'from' first, as 'echo' packets have both. 54 if (packet.from) { 55 dumpn("Packet " + serial + " sent from " + JSON.stringify(packet.from)); 56 } else if (packet.to) { 57 dumpn("Packet " + serial + " sent to " + JSON.stringify(packet.to)); 58 } 59 } 60 this._deepFreeze(packet); 61 const other = this.other; 62 if (other) { 63 DevToolsUtils.executeSoon( 64 DevToolsUtils.makeInfallible(() => { 65 // Avoid the cost of JSON.stringify() when logging is disabled. 66 if (flags.wantLogging) { 67 dumpn( 68 "Received packet " + 69 serial + 70 ": " + 71 JSON.stringify(packet, null, 2) 72 ); 73 } 74 if (other.hooks) { 75 other.hooks.onPacket(packet); 76 } 77 }, "LocalDebuggerTransport instance's this.other.hooks.onPacket") 78 ); 79 } 80 } 81 82 /** 83 * Send a streaming bulk packet directly to the onBulkPacket handler of the 84 * other endpoint. 85 * 86 * This case is much simpler than the full DebuggerTransport, since there is 87 * no primary stream we have to worry about managing while we hand it off to 88 * others temporarily. Instead, we can just make a single use pipe and be 89 * done with it. 90 */ 91 startBulkSend(sentPacket) { 92 const { actor, type, length } = sentPacket; 93 const serial = this._serial.count++; 94 dumpn("Sent bulk packet " + serial + " for actor " + actor); 95 96 if (!this.other) { 97 const error = new Error("startBulkSend: other side of transport missing"); 98 return Promise.reject(error); 99 } 100 101 const pipe = new Pipe(true, true, 0, 0, null); 102 103 DevToolsUtils.executeSoon( 104 DevToolsUtils.makeInfallible(() => { 105 // Avoid the cost of JSON.stringify() when logging is disabled. 106 if (flags.wantLogging) { 107 dumpn( 108 "Received bulk packet " + 109 serial + 110 ": " + 111 JSON.stringify(sentPacket, null, 2) 112 ); 113 } 114 if (!this.other.hooks) { 115 return; 116 } 117 118 // Receiver 119 new Promise(receiverResolve => { 120 const receivedPacket = { 121 actor, 122 type, 123 length, 124 copyTo: output => { 125 const copying = StreamUtils.copyStream( 126 pipe.inputStream, 127 output, 128 length 129 ); 130 receiverResolve(copying); 131 return copying; 132 }, 133 copyToBuffer: outputBuffer => { 134 if (outputBuffer.byteLength !== length) { 135 throw new Error( 136 `In copyToBuffer, the output buffer needs to have the same length as the data to read. ${outputBuffer.byteLength} !== ${length}` 137 ); 138 } 139 const copying = StreamUtils.copyAsyncStreamToArrayBuffer( 140 pipe.inputStream, 141 outputBuffer 142 ); 143 receiverResolve(copying); 144 return copying; 145 }, 146 stream: pipe.inputStream, 147 done: receiverResolve, 148 }; 149 150 this.other.hooks.onBulkPacket(receivedPacket); 151 }) 152 // Await the result of reading from the stream 153 .then(() => pipe.inputStream.close(), this.close); 154 }, "LocalDebuggerTransport instance's this.other.hooks.onBulkPacket") 155 ); 156 157 // Sender 158 return new Promise(senderResolve => { 159 // The remote transport is not capable of resolving immediately here, so we 160 // shouldn't be able to either. 161 DevToolsUtils.executeSoon(() => { 162 return ( 163 new Promise(copyResolve => { 164 senderResolve({ 165 copyFrom: input => { 166 const copying = StreamUtils.copyStream( 167 input, 168 pipe.outputStream, 169 length 170 ); 171 copyResolve(copying); 172 return copying; 173 }, 174 copyFromBuffer: inputBuffer => { 175 if (inputBuffer.byteLength !== length) { 176 throw new Error( 177 `In copyFromBuffer, the input buffer needs to have the same length as the data to write. ${inputBuffer.byteLength} !== ${length}` 178 ); 179 } 180 const copying = StreamUtils.copyArrayBufferToAsyncStream( 181 inputBuffer, 182 pipe.outputStream 183 ); 184 copyResolve(copying); 185 return copying; 186 }, 187 stream: pipe.outputStream, 188 done: copyResolve, 189 }); 190 }) 191 // Await the result of writing to the stream 192 .then(() => pipe.outputStream.close(), this.close) 193 ); 194 }); 195 }); 196 } 197 198 /** 199 * Close the transport. 200 */ 201 close() { 202 if (this.other) { 203 // Remove the reference to the other endpoint before calling close(), to 204 // avoid infinite recursion. 205 const other = this.other; 206 this.other = null; 207 other.close(); 208 } 209 if (this.hooks) { 210 try { 211 if (this.hooks.onTransportClosed) { 212 this.hooks.onTransportClosed(); 213 } 214 } catch (ex) { 215 console.error(ex); 216 } 217 this.hooks = null; 218 } 219 } 220 221 /** 222 * An empty method for emulating the DebuggerTransport API. 223 */ 224 ready() {} 225 226 /** 227 * Helper function that makes an object fully immutable. 228 */ 229 _deepFreeze(object) { 230 Object.freeze(object); 231 for (const prop in object) { 232 // Freeze the properties that are objects, not on the prototype, and not 233 // already frozen. Note that this might leave an unfrozen reference 234 // somewhere in the object if there is an already frozen object containing 235 // an unfrozen object. 236 if ( 237 object.hasOwnProperty(prop) && 238 typeof object === "object" && 239 !Object.isFrozen(object) 240 ) { 241 this._deepFreeze(object[prop]); 242 } 243 } 244 } 245 } 246 247 exports.LocalDebuggerTransport = LocalDebuggerTransport;