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