packets.js (12982B)
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 /** 8 * Packets contain read / write functionality for the different packet types 9 * supported by the debugging protocol, so that a transport can focus on 10 * delivery and queue management without worrying too much about the specific 11 * packet types. 12 * 13 * They are intended to be "one use only", so a new packet should be 14 * instantiated for each incoming or outgoing packet. 15 * 16 * A complete Packet type should expose at least the following: 17 * * read(stream, scriptableStream) 18 * Called when the input stream has data to read 19 * * write(stream) 20 * Called when the output stream is ready to write 21 * * get done() 22 * Returns true once the packet is done being read / written 23 * * destroy() 24 * Called to clean up at the end of use 25 */ 26 27 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js"); 28 const { dumpn, dumpv } = DevToolsUtils; 29 const flags = require("resource://devtools/shared/flags.js"); 30 const StreamUtils = require("resource://devtools/shared/transport/stream-utils.js"); 31 32 DevToolsUtils.defineLazyGetter(this, "unicodeConverter", () => { 33 // eslint-disable-next-line no-shadow 34 const unicodeConverter = Cc[ 35 "@mozilla.org/intl/scriptableunicodeconverter" 36 ].createInstance(Ci.nsIScriptableUnicodeConverter); 37 unicodeConverter.charset = "UTF-8"; 38 return unicodeConverter; 39 }); 40 41 // The transport's previous check ensured the header length did not exceed 20 42 // characters. Here, we opt for the somewhat smaller, but still large limit of 43 // 1 TiB. 44 const PACKET_LENGTH_MAX = Math.pow(2, 40); 45 46 /** 47 * A generic Packet processing object (extended by two subtypes below). 48 */ 49 class Packet { 50 constructor(transport) { 51 this._transport = transport; 52 this._length = 0; 53 } 54 /** 55 * Attempt to initialize a new Packet based on the incoming packet header we've 56 * received so far. We try each of the types in succession, trying JSON packets 57 * first since they are much more common. 58 * 59 * @param {string} header 60 * The packet header string to attempt parsing. 61 * @param {DebuggerTransport} transport 62 * The transport instance that will own the packet. 63 * @return Packet 64 * The parsed packet of the matching type, or null if no types matched. 65 */ 66 static fromHeader(header, transport) { 67 return ( 68 JSONPacket.fromHeader(header, transport) || 69 BulkPacket.fromHeader(header, transport) 70 ); 71 } 72 get length() { 73 return this._length; 74 } 75 76 set length(length) { 77 if (length > PACKET_LENGTH_MAX) { 78 throw Error( 79 "Packet length " + 80 length + 81 " exceeds the max length of " + 82 PACKET_LENGTH_MAX 83 ); 84 } 85 this._length = length; 86 } 87 88 destroy() { 89 this._transport = null; 90 } 91 } 92 93 exports.Packet = Packet; 94 95 /** 96 * With a JSON packet (the typical packet type sent via the transport), data is 97 * transferred as a JSON packet serialized into a string, with the string length 98 * prepended to the packet, followed by a colon ([length]:[packet]). The 99 * contents of the JSON packet are specified in the Remote Debugging Protocol 100 * specification. 101 * 102 */ 103 class JSONPacket extends Packet { 104 /** 105 * @param {DebuggerTransport} transport 106 * The transport instance that will own the packet. 107 */ 108 constructor(transport) { 109 super(transport); 110 111 this._data = ""; 112 this._done = false; 113 } 114 /** 115 * Attempt to initialize a new JSONPacket based on the incoming packet header 116 * we've received so far. 117 * 118 * @param {string} header 119 * The packet header string to attempt parsing. 120 * @param {DebuggerTransport} transport 121 * The transport instance that will own the packet. 122 * @return {JSONPacket} 123 * The parsed packet, or null if it's not a match. 124 */ 125 static fromHeader(header, transport) { 126 const match = this.HEADER_PATTERN.exec(header); 127 128 if (!match) { 129 return null; 130 } 131 132 dumpv("Header matches JSON packet"); 133 const packet = new JSONPacket(transport); 134 packet.length = +match[1]; 135 return packet; 136 } 137 138 static HEADER_PATTERN = /^(\d+):$/; 139 140 /** 141 * Gets the object (not the serialized string) being read or written. 142 */ 143 get object() { 144 return this._object; 145 } 146 /** 147 * Sets the object to be sent when write() is called. 148 */ 149 set object(object) { 150 this._object = object; 151 const data = JSON.stringify(object); 152 this._data = unicodeConverter.ConvertFromUnicode(data); 153 this.length = this._data.length; 154 } 155 156 read(stream, scriptableStream) { 157 dumpv("Reading JSON packet"); 158 159 // Read in more packet data. 160 this._readData(stream, scriptableStream); 161 162 if (!this.done) { 163 // Don't have a complete packet yet. 164 return; 165 } 166 167 let json = this._data; 168 try { 169 json = unicodeConverter.ConvertToUnicode(json); 170 this._object = JSON.parse(json); 171 } catch (e) { 172 const msg = 173 "Error parsing incoming packet: " + 174 json + 175 " (" + 176 e + 177 " - " + 178 e.stack + 179 ")"; 180 console.error(msg); 181 dumpn(msg); 182 return; 183 } 184 185 this._transport._onJSONObjectReady(this._object); 186 } 187 188 _readData(stream, scriptableStream) { 189 if (flags.wantVerbose) { 190 dumpv( 191 "Reading JSON data: _l: " + 192 this.length + 193 " dL: " + 194 this._data.length + 195 " sA: " + 196 stream.available() 197 ); 198 } 199 const bytesToRead = Math.min( 200 this.length - this._data.length, 201 stream.available() 202 ); 203 this._data += scriptableStream.readBytes(bytesToRead); 204 this._done = this._data.length === this.length; 205 } 206 207 write(stream) { 208 dumpv("Writing JSON packet"); 209 210 if (this._outgoing === undefined) { 211 // Format the serialized packet to a buffer 212 this._outgoing = this.length + ":" + this._data; 213 } 214 215 const written = stream.write(this._outgoing, this._outgoing.length); 216 this._outgoing = this._outgoing.slice(written); 217 this._done = !this._outgoing.length; 218 } 219 220 get done() { 221 return this._done; 222 } 223 224 toString() { 225 return JSON.stringify(this._object, null, 2); 226 } 227 } 228 229 exports.JSONPacket = JSONPacket; 230 231 /** 232 * With a bulk packet, data is transferred by temporarily handing over the 233 * transport's input or output stream to the application layer for writing data 234 * directly. This can be much faster for large data sets, and avoids various 235 * stages of copies and data duplication inherent in the JSON packet type. The 236 * bulk packet looks like: 237 * 238 * bulk [actor] [type] [length]:[data] 239 * 240 * The interpretation of the data portion depends on the kind of actor and the 241 * packet's type. See the Remote Debugging Protocol Stream Transport spec for 242 * more details. 243 * 244 */ 245 class BulkPacket extends Packet { 246 /** 247 * @param {DebuggerTransport} transport 248 * The transport instance that will own the packet. 249 */ 250 constructor(transport) { 251 super(transport); 252 253 this._done = false; 254 let _resolve; 255 this._readyForWriting = new Promise(resolve => { 256 _resolve = resolve; 257 }); 258 this._readyForWriting.resolve = _resolve; 259 } 260 261 /** 262 * Attempt to initialize a new BulkPacket based on the incoming packet header 263 * we've received so far. 264 * 265 * @param {string} header 266 * The packet header string to attempt parsing. 267 * @param {DebuggerTransport} transport 268 * The transport instance that will own the packet. 269 * @return {BulkPacket} 270 * The parsed packet, or null if it's not a match. 271 */ 272 static fromHeader = function (header, transport) { 273 const match = this.HEADER_PATTERN.exec(header); 274 275 if (!match) { 276 return null; 277 } 278 279 dumpv("Header matches bulk packet"); 280 const packet = new BulkPacket(transport); 281 packet.header = { 282 actor: match[1], 283 type: match[2], 284 length: +match[3], 285 }; 286 return packet; 287 }; 288 289 static HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/; 290 291 read(stream) { 292 dumpv("Reading bulk packet, handing off input stream"); 293 294 // Temporarily pause monitoring of the input stream 295 this._transport.pauseIncoming(); 296 297 new Promise(resolve => { 298 this._transport._onBulkReadReady({ 299 actor: this.actor, 300 type: this.type, 301 length: this.length, 302 copyTo: output => { 303 dumpv("CT length: " + this.length); 304 const copying = StreamUtils.copyStream(stream, output, this.length); 305 resolve(copying); 306 return copying; 307 }, 308 copyToBuffer: outputBuffer => { 309 if (outputBuffer.byteLength !== this.length) { 310 throw new Error( 311 `In copyToBuffer, the output buffer needs to have the same length as the data to read. ${outputBuffer.byteLength} !== ${this.length}` 312 ); 313 } 314 dumpv("CT length: " + this.length); 315 const copying = StreamUtils.copyAsyncStreamToArrayBuffer( 316 stream, 317 outputBuffer 318 ); 319 resolve(copying); 320 return copying; 321 }, 322 stream, 323 done: resolve, 324 }); 325 // Await the result of reading from the stream 326 }).then(() => { 327 dumpv("onReadDone called, ending bulk mode"); 328 this._done = true; 329 this._transport.resumeIncoming(); 330 }, this._transport.close); 331 332 // Ensure this is only done once 333 this.read = () => { 334 throw new Error("Tried to read() a BulkPacket's stream multiple times."); 335 }; 336 } 337 338 write(stream) { 339 dumpv("Writing bulk packet"); 340 341 if (this._outgoingHeader === undefined) { 342 dumpv("Serializing bulk packet header"); 343 // Format the serialized packet header to a buffer 344 this._outgoingHeader = 345 "bulk " + this.actor + " " + this.type + " " + this.length + ":"; 346 } 347 348 // Write the header, or whatever's left of it to write. 349 if (this._outgoingHeader.length) { 350 dumpv("Writing bulk packet header"); 351 const written = stream.write( 352 this._outgoingHeader, 353 this._outgoingHeader.length 354 ); 355 this._outgoingHeader = this._outgoingHeader.slice(written); 356 return; 357 } 358 359 dumpv("Handing off output stream"); 360 361 // Temporarily pause the monitoring of the output stream 362 this._transport.pauseOutgoing(); 363 364 new Promise(resolve => { 365 this._readyForWriting.resolve({ 366 copyFrom: input => { 367 dumpv("CF length: " + this.length); 368 const copying = StreamUtils.copyStream(input, stream, this.length); 369 resolve(copying); 370 return copying; 371 }, 372 copyFromBuffer: inputBuffer => { 373 if (inputBuffer.byteLength !== this.length) { 374 throw new Error( 375 `In copyFromBuffer, the input buffer needs to have the same length as the data to write. ${inputBuffer.byteLength} !== ${this.length}` 376 ); 377 } 378 dumpv("CF length: " + this.length); 379 const copying = StreamUtils.copyArrayBufferToAsyncStream( 380 inputBuffer, 381 stream 382 ); 383 resolve(copying); 384 return copying; 385 }, 386 stream, 387 done: resolve, 388 }); 389 // Await the result of writing to the stream 390 }).then(() => { 391 dumpv("onWriteDone called, ending bulk mode"); 392 this._done = true; 393 this._transport.resumeOutgoing(); 394 }, this._transport.close); 395 396 // Ensure this is only done once 397 this.write = () => { 398 throw new Error("Tried to write() a BulkPacket's stream multiple times."); 399 }; 400 } 401 402 get streamReadyForWriting() { 403 return this._readyForWriting; 404 } 405 406 get header() { 407 return { 408 actor: this.actor, 409 type: this.type, 410 length: this.length, 411 }; 412 } 413 414 set header(header) { 415 this.actor = header.actor; 416 this.type = header.type; 417 this.length = header.length; 418 } 419 420 get done() { 421 return this._done; 422 } 423 424 toString() { 425 return "Bulk: " + JSON.stringify(this.header, null, 2); 426 } 427 } 428 429 exports.BulkPacket = BulkPacket; 430 431 /** 432 * RawPacket is used to test the transport's error handling of malformed 433 * packets, by writing data directly onto the stream. 434 */ 435 class RawPacket extends Packet { 436 /** 437 * @param {DebuggerTransport} transport 438 * The transport instance that will own the packet. 439 * @param {string} data 440 * The raw string to send out onto the stream. 441 */ 442 constructor(transport, data) { 443 super(transport); 444 this._data = data; 445 this.length = data.length; 446 this._done = false; 447 } 448 read() { 449 // This hasn't yet been needed for testing. 450 throw Error("Not implmented."); 451 } 452 write(stream) { 453 const written = stream.write(this._data, this._data.length); 454 this._data = this._data.slice(written); 455 this._done = !this._data.length; 456 } 457 get done() { 458 return this._done; 459 } 460 } 461 462 exports.RawPacket = RawPacket;