Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / devtools / shared / transport / packets.js
blobe4b85697562793656b97034c572ebcb56396ed26
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.
13 * They are intended to be "one use only", so a new packet should be
14 * instantiated for each incoming or outgoing packet.
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
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");
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 });
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);
46 /**
47 * A generic Packet processing object (extended by two subtypes below).
49 function Packet(transport) {
50 this._transport = transport;
51 this._length = 0;
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 * @param header string
59 * The packet header string to attempt parsing.
60 * @param transport DebuggerTransport
61 * The transport instance that will own the packet.
62 * @return Packet
63 * The parsed packet of the matching type, or null if no types matched.
65 Packet.fromHeader = function (header, transport) {
66 return (
67 JSONPacket.fromHeader(header, transport) ||
68 BulkPacket.fromHeader(header, transport)
72 Packet.prototype = {
73 get length() {
74 return this._length;
77 set length(length) {
78 if (length > PACKET_LENGTH_MAX) {
79 throw Error(
80 "Packet length " +
81 length +
82 " exceeds the max length of " +
83 PACKET_LENGTH_MAX
86 this._length = length;
89 destroy() {
90 this._transport = null;
94 exports.Packet = Packet;
96 /**
97 * With a JSON packet (the typical packet type sent via the transport), data is
98 * transferred as a JSON packet serialized into a string, with the string length
99 * prepended to the packet, followed by a colon ([length]:[packet]). The
100 * contents of the JSON packet are specified in the Remote Debugging Protocol
101 * specification.
102 * @param transport DebuggerTransport
103 * The transport instance that will own the packet.
105 function JSONPacket(transport) {
106 Packet.call(this, transport);
107 this._data = "";
108 this._done = false;
112 * Attempt to initialize a new JSONPacket based on the incoming packet header
113 * we've received so far.
114 * @param header string
115 * The packet header string to attempt parsing.
116 * @param transport DebuggerTransport
117 * The transport instance that will own the packet.
118 * @return JSONPacket
119 * The parsed packet, or null if it's not a match.
121 JSONPacket.fromHeader = function (header, transport) {
122 const match = this.HEADER_PATTERN.exec(header);
124 if (!match) {
125 return null;
128 dumpv("Header matches JSON packet");
129 const packet = new JSONPacket(transport);
130 packet.length = +match[1];
131 return packet;
134 JSONPacket.HEADER_PATTERN = /^(\d+):$/;
136 JSONPacket.prototype = Object.create(Packet.prototype);
138 Object.defineProperty(JSONPacket.prototype, "object", {
140 * Gets the object (not the serialized string) being read or written.
142 get() {
143 return this._object;
147 * Sets the object to be sent when write() is called.
149 set(object) {
150 this._object = object;
151 const data = JSON.stringify(object);
152 this._data = unicodeConverter.ConvertFromUnicode(data);
153 this.length = this._data.length;
157 JSONPacket.prototype.read = function (stream, scriptableStream) {
158 dumpv("Reading JSON packet");
160 // Read in more packet data.
161 this._readData(stream, scriptableStream);
163 if (!this.done) {
164 // Don't have a complete packet yet.
165 return;
168 let json = this._data;
169 try {
170 json = unicodeConverter.ConvertToUnicode(json);
171 this._object = JSON.parse(json);
172 } catch (e) {
173 const msg =
174 "Error parsing incoming packet: " +
175 json +
176 " (" +
178 " - " +
179 e.stack +
180 ")";
181 console.error(msg);
182 dumpn(msg);
183 return;
186 this._transport._onJSONObjectReady(this._object);
189 JSONPacket.prototype._readData = function (stream, scriptableStream) {
190 if (flags.wantVerbose) {
191 dumpv(
192 "Reading JSON data: _l: " +
193 this.length +
194 " dL: " +
195 this._data.length +
196 " sA: " +
197 stream.available()
200 const bytesToRead = Math.min(
201 this.length - this._data.length,
202 stream.available()
204 this._data += scriptableStream.readBytes(bytesToRead);
205 this._done = this._data.length === this.length;
208 JSONPacket.prototype.write = function (stream) {
209 dumpv("Writing JSON packet");
211 if (this._outgoing === undefined) {
212 // Format the serialized packet to a buffer
213 this._outgoing = this.length + ":" + this._data;
216 const written = stream.write(this._outgoing, this._outgoing.length);
217 this._outgoing = this._outgoing.slice(written);
218 this._done = !this._outgoing.length;
221 Object.defineProperty(JSONPacket.prototype, "done", {
222 get() {
223 return this._done;
227 JSONPacket.prototype.toString = function () {
228 return JSON.stringify(this._object, null, 2);
231 exports.JSONPacket = JSONPacket;
234 * With a bulk packet, data is transferred by temporarily handing over the
235 * transport's input or output stream to the application layer for writing data
236 * directly. This can be much faster for large data sets, and avoids various
237 * stages of copies and data duplication inherent in the JSON packet type. The
238 * bulk packet looks like:
240 * bulk [actor] [type] [length]:[data]
242 * The interpretation of the data portion depends on the kind of actor and the
243 * packet's type. See the Remote Debugging Protocol Stream Transport spec for
244 * more details.
245 * @param transport DebuggerTransport
246 * The transport instance that will own the packet.
248 function BulkPacket(transport) {
249 Packet.call(this, transport);
250 this._done = false;
251 let _resolve;
252 this._readyForWriting = new Promise(resolve => {
253 _resolve = resolve;
255 this._readyForWriting.resolve = _resolve;
259 * Attempt to initialize a new BulkPacket based on the incoming packet header
260 * we've received so far.
261 * @param header string
262 * The packet header string to attempt parsing.
263 * @param transport DebuggerTransport
264 * The transport instance that will own the packet.
265 * @return BulkPacket
266 * The parsed packet, or null if it's not a match.
268 BulkPacket.fromHeader = function (header, transport) {
269 const match = this.HEADER_PATTERN.exec(header);
271 if (!match) {
272 return null;
275 dumpv("Header matches bulk packet");
276 const packet = new BulkPacket(transport);
277 packet.header = {
278 actor: match[1],
279 type: match[2],
280 length: +match[3],
282 return packet;
285 BulkPacket.HEADER_PATTERN = /^bulk ([^: ]+) ([^: ]+) (\d+):$/;
287 BulkPacket.prototype = Object.create(Packet.prototype);
289 BulkPacket.prototype.read = function (stream) {
290 dumpv("Reading bulk packet, handing off input stream");
292 // Temporarily pause monitoring of the input stream
293 this._transport.pauseIncoming();
295 new Promise(resolve => {
296 this._transport._onBulkReadReady({
297 actor: this.actor,
298 type: this.type,
299 length: this.length,
300 copyTo: output => {
301 dumpv("CT length: " + this.length);
302 const copying = StreamUtils.copyStream(stream, output, this.length);
303 resolve(copying);
304 return copying;
306 stream,
307 done: resolve,
309 // Await the result of reading from the stream
310 }).then(() => {
311 dumpv("onReadDone called, ending bulk mode");
312 this._done = true;
313 this._transport.resumeIncoming();
314 }, this._transport.close);
316 // Ensure this is only done once
317 this.read = () => {
318 throw new Error("Tried to read() a BulkPacket's stream multiple times.");
322 BulkPacket.prototype.write = function (stream) {
323 dumpv("Writing bulk packet");
325 if (this._outgoingHeader === undefined) {
326 dumpv("Serializing bulk packet header");
327 // Format the serialized packet header to a buffer
328 this._outgoingHeader =
329 "bulk " + this.actor + " " + this.type + " " + this.length + ":";
332 // Write the header, or whatever's left of it to write.
333 if (this._outgoingHeader.length) {
334 dumpv("Writing bulk packet header");
335 const written = stream.write(
336 this._outgoingHeader,
337 this._outgoingHeader.length
339 this._outgoingHeader = this._outgoingHeader.slice(written);
340 return;
343 dumpv("Handing off output stream");
345 // Temporarily pause the monitoring of the output stream
346 this._transport.pauseOutgoing();
348 new Promise(resolve => {
349 this._readyForWriting.resolve({
350 copyFrom: input => {
351 dumpv("CF length: " + this.length);
352 const copying = StreamUtils.copyStream(input, stream, this.length);
353 resolve(copying);
354 return copying;
356 stream,
357 done: resolve,
359 // Await the result of writing to the stream
360 }).then(() => {
361 dumpv("onWriteDone called, ending bulk mode");
362 this._done = true;
363 this._transport.resumeOutgoing();
364 }, this._transport.close);
366 // Ensure this is only done once
367 this.write = () => {
368 throw new Error("Tried to write() a BulkPacket's stream multiple times.");
372 Object.defineProperty(BulkPacket.prototype, "streamReadyForWriting", {
373 get() {
374 return this._readyForWriting;
378 Object.defineProperty(BulkPacket.prototype, "header", {
379 get() {
380 return {
381 actor: this.actor,
382 type: this.type,
383 length: this.length,
387 set(header) {
388 this.actor = header.actor;
389 this.type = header.type;
390 this.length = header.length;
394 Object.defineProperty(BulkPacket.prototype, "done", {
395 get() {
396 return this._done;
400 BulkPacket.prototype.toString = function () {
401 return "Bulk: " + JSON.stringify(this.header, null, 2);
404 exports.BulkPacket = BulkPacket;
407 * RawPacket is used to test the transport's error handling of malformed
408 * packets, by writing data directly onto the stream.
409 * @param transport DebuggerTransport
410 * The transport instance that will own the packet.
411 * @param data string
412 * The raw string to send out onto the stream.
414 function RawPacket(transport, data) {
415 Packet.call(this, transport);
416 this._data = data;
417 this.length = data.length;
418 this._done = false;
421 RawPacket.prototype = Object.create(Packet.prototype);
423 RawPacket.prototype.read = function () {
424 // This hasn't yet been needed for testing.
425 throw Error("Not implmented.");
428 RawPacket.prototype.write = function (stream) {
429 const written = stream.write(this._data, this._data.length);
430 this._data = this._data.slice(written);
431 this._done = !this._data.length;
434 Object.defineProperty(RawPacket.prototype, "done", {
435 get() {
436 return this._done;
440 exports.RawPacket = RawPacket;