Bug 1640914 [wpt PR 23771] - Python 3: port tests in resource-timing, a=testonly
[gecko.git] / testing / marionette / packets.js
blobeb90baab36232d9aa78e5c94fd953d5d8fc9837f
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/. */
5 "use strict";
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  */
27 const { StreamUtils } = ChromeUtils.import(
28   "chrome://marionette/content/stream-utils.js"
31 const unicodeConverter = Cc[
32   "@mozilla.org/intl/scriptableunicodeconverter"
33 ].createInstance(Ci.nsIScriptableUnicodeConverter);
34 unicodeConverter.charset = "UTF-8";
36 const defer = function() {
37   let deferred = {
38     promise: new Promise((resolve, reject) => {
39       deferred.resolve = resolve;
40       deferred.reject = reject;
41     }),
42   };
43   return deferred;
46 this.EXPORTED_SYMBOLS = ["RawPacket", "Packet", "JSONPacket", "BulkPacket"];
48 // The transport's previous check ensured the header length did not
49 // exceed 20 characters.  Here, we opt for the somewhat smaller, but still
50 // large limit of 1 TiB.
51 const PACKET_LENGTH_MAX = Math.pow(2, 40);
53 /**
54  * A generic Packet processing object (extended by two subtypes below).
55  *
56  * @class
57  */
58 function Packet(transport) {
59   this._transport = transport;
60   this._length = 0;
63 /**
64  * Attempt to initialize a new Packet based on the incoming packet header
65  * we've received so far.  We try each of the types in succession, trying
66  * JSON packets first since they are much more common.
67  *
68  * @param {string} header
69  *     Packet header string to attempt parsing.
70  * @param {DebuggerTransport} transport
71  *     Transport instance that will own the packet.
72  *
73  * @return {Packet}
74  *     Parsed packet of the matching type, or null if no types matched.
75  */
76 Packet.fromHeader = function(header, transport) {
77   return (
78     JSONPacket.fromHeader(header, transport) ||
79     BulkPacket.fromHeader(header, transport)
80   );
83 Packet.prototype = {
84   get length() {
85     return this._length;
86   },
88   set length(length) {
89     if (length > PACKET_LENGTH_MAX) {
90       throw new Error(
91         "Packet length " +
92           length +
93           " exceeds the max length of " +
94           PACKET_LENGTH_MAX
95       );
96     }
97     this._length = length;
98   },
100   destroy() {
101     this._transport = null;
102   },
106  * With a JSON packet (the typical packet type sent via the transport),
107  * data is transferred as a JSON packet serialized into a string,
108  * with the string length prepended to the packet, followed by a colon
109  * ([length]:[packet]). The contents of the JSON packet are specified in
110  * the Remote Debugging Protocol specification.
112  * @param {DebuggerTransport} transport
113  *     Transport instance that will own the packet.
114  */
115 function JSONPacket(transport) {
116   Packet.call(this, transport);
117   this._data = "";
118   this._done = false;
122  * Attempt to initialize a new JSONPacket based on the incoming packet
123  * header we've received so far.
125  * @param {string} header
126  *     Packet header string to attempt parsing.
127  * @param {DebuggerTransport} transport
128  *     Transport instance that will own the packet.
130  * @return {JSONPacket}
131  *     Parsed packet, or null if it's not a match.
132  */
133 JSONPacket.fromHeader = function(header, transport) {
134   let match = this.HEADER_PATTERN.exec(header);
136   if (!match) {
137     return null;
138   }
140   let packet = new JSONPacket(transport);
141   packet.length = +match[1];
142   return packet;
145 JSONPacket.HEADER_PATTERN = /^(\d+):$/;
147 JSONPacket.prototype = Object.create(Packet.prototype);
149 Object.defineProperty(JSONPacket.prototype, "object", {
150   /**
151    * Gets the object (not the serialized string) being read or written.
152    */
153   get() {
154     return this._object;
155   },
157   /**
158    * Sets the object to be sent when write() is called.
159    */
160   set(object) {
161     this._object = object;
162     let data = JSON.stringify(object);
163     this._data = unicodeConverter.ConvertFromUnicode(data);
164     this.length = this._data.length;
165   },
168 JSONPacket.prototype.read = function(stream, scriptableStream) {
169   // Read in more packet data.
170   this._readData(stream, scriptableStream);
172   if (!this.done) {
173     // Don't have a complete packet yet.
174     return;
175   }
177   let json = this._data;
178   try {
179     json = unicodeConverter.ConvertToUnicode(json);
180     this._object = JSON.parse(json);
181   } catch (e) {
182     let msg =
183       "Error parsing incoming packet: " +
184       json +
185       " (" +
186       e +
187       " - " +
188       e.stack +
189       ")";
190     console.error(msg);
191     dump(msg + "\n");
192     return;
193   }
195   this._transport._onJSONObjectReady(this._object);
198 JSONPacket.prototype._readData = function(stream, scriptableStream) {
199   let 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;
207 JSONPacket.prototype.write = function(stream) {
208   if (this._outgoing === undefined) {
209     // Format the serialized packet to a buffer
210     this._outgoing = this.length + ":" + this._data;
211   }
213   let written = stream.write(this._outgoing, this._outgoing.length);
214   this._outgoing = this._outgoing.slice(written);
215   this._done = !this._outgoing.length;
218 Object.defineProperty(JSONPacket.prototype, "done", {
219   get() {
220     return this._done;
221   },
224 JSONPacket.prototype.toString = function() {
225   return JSON.stringify(this._object, null, 2);
229  * With a bulk packet, data is transferred by temporarily handing over
230  * the transport's input or output stream to the application layer for
231  * writing data directly.  This can be much faster for large data sets,
232  * and avoids various stages of copies and data duplication inherent in
233  * the JSON packet type.  The bulk packet looks like:
235  *     bulk [actor] [type] [length]:[data]
237  * The interpretation of the data portion depends on the kind of actor and
238  * the packet's type.  See the Remote Debugging Protocol Stream Transport
239  * spec for more details.
241  * @param {DebuggerTransport} transport
242  *     Transport instance that will own the packet.
243  */
244 function BulkPacket(transport) {
245   Packet.call(this, transport);
246   this._done = false;
247   this._readyForWriting = defer();
251  * Attempt to initialize a new BulkPacket based on the incoming packet
252  * header we've received so far.
254  * @param {string} header
255  *     Packet header string to attempt parsing.
256  * @param {DebuggerTransport} transport
257  *     Transport instance that will own the packet.
259  * @return {BulkPacket}
260  *     Parsed packet, or null if it's not a match.
261  */
262 BulkPacket.fromHeader = function(header, transport) {
263   let match = this.HEADER_PATTERN.exec(header);
265   if (!match) {
266     return null;
267   }
269   let packet = new BulkPacket(transport);
270   packet.header = {
271     actor: match[1],
272     type: match[2],
273     length: +match[3],
274   };
275   return packet;
278 BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
280 BulkPacket.prototype = Object.create(Packet.prototype);
282 BulkPacket.prototype.read = function(stream) {
283   // Temporarily pause monitoring of the input stream
284   this._transport.pauseIncoming();
286   let deferred = defer();
288   this._transport._onBulkReadReady({
289     actor: this.actor,
290     type: this.type,
291     length: this.length,
292     copyTo: output => {
293       let copying = StreamUtils.copyStream(stream, output, this.length);
294       deferred.resolve(copying);
295       return copying;
296     },
297     stream,
298     done: deferred,
299   });
301   // Await the result of reading from the stream
302   deferred.promise.then(() => {
303     this._done = true;
304     this._transport.resumeIncoming();
305   }, this._transport.close);
307   // Ensure this is only done once
308   this.read = () => {
309     throw new Error("Tried to read() a BulkPacket's stream multiple times.");
310   };
313 BulkPacket.prototype.write = function(stream) {
314   if (this._outgoingHeader === undefined) {
315     // Format the serialized packet header to a buffer
316     this._outgoingHeader =
317       "bulk " + this.actor + " " + this.type + " " + this.length + ":";
318   }
320   // Write the header, or whatever's left of it to write.
321   if (this._outgoingHeader.length) {
322     let written = stream.write(
323       this._outgoingHeader,
324       this._outgoingHeader.length
325     );
326     this._outgoingHeader = this._outgoingHeader.slice(written);
327     return;
328   }
330   // Temporarily pause the monitoring of the output stream
331   this._transport.pauseOutgoing();
333   let deferred = defer();
335   this._readyForWriting.resolve({
336     copyFrom: input => {
337       let copying = StreamUtils.copyStream(input, stream, this.length);
338       deferred.resolve(copying);
339       return copying;
340     },
341     stream,
342     done: deferred,
343   });
345   // Await the result of writing to the stream
346   deferred.promise.then(() => {
347     this._done = true;
348     this._transport.resumeOutgoing();
349   }, this._transport.close);
351   // Ensure this is only done once
352   this.write = () => {
353     throw new Error("Tried to write() a BulkPacket's stream multiple times.");
354   };
357 Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
358   get() {
359     return this._readyForWriting.promise;
360   },
363 Object.defineProperty(BulkPacket.prototype, "header", {
364   get() {
365     return {
366       actor: this.actor,
367       type: this.type,
368       length: this.length,
369     };
370   },
372   set(header) {
373     this.actor = header.actor;
374     this.type = header.type;
375     this.length = header.length;
376   },
379 Object.defineProperty(BulkPacket.prototype, "done", {
380   get() {
381     return this._done;
382   },
385 BulkPacket.prototype.toString = function() {
386   return "Bulk: " + JSON.stringify(this.header, null, 2);
390  * RawPacket is used to test the transport's error handling of malformed
391  * packets, by writing data directly onto the stream.
392  * @param transport DebuggerTransport
393  *        The transport instance that will own the packet.
394  * @param data string
395  *        The raw string to send out onto the stream.
396  */
397 function RawPacket(transport, data) {
398   Packet.call(this, transport);
399   this._data = data;
400   this.length = data.length;
401   this._done = false;
404 RawPacket.prototype = Object.create(Packet.prototype);
406 RawPacket.prototype.read = function() {
407   // this has not yet been needed for testing
408   throw new Error("Not implemented");
411 RawPacket.prototype.write = function(stream) {
412   let written = stream.write(this._data, this._data.length);
413   this._data = this._data.slice(written);
414   this._done = !this._data.length;
417 Object.defineProperty(RawPacket.prototype, "done", {
418   get() {
419     return this._done;
420   },