Bug 1847397 - Check no post barrier needed when eliding it in UpdateRegExpStatics...
[gecko.git] / remote / cdp / CDPConnection.sys.mjs
blob1d2eb3e77cb12ae6e863ffac75f664db2ce0561d
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 import { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs";
7 const lazy = {};
9 ChromeUtils.defineESModuleGetters(lazy, {
10   Log: "chrome://remote/content/shared/Log.sys.mjs",
11   UnknownMethodError: "chrome://remote/content/cdp/Error.sys.mjs",
12 });
14 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
15   lazy.Log.get(lazy.Log.TYPES.CDP)
18 export class CDPConnection extends WebSocketConnection {
19   /**
20    * @param {WebSocket} webSocket
21    *     The WebSocket server connection to wrap.
22    * @param {Connection} httpdConnection
23    *     Reference to the httpd.js's connection needed for clean-up.
24    */
25   constructor(webSocket, httpdConnection) {
26     super(webSocket, httpdConnection);
28     this.sessions = new Map();
29     this.defaultSession = null;
30   }
32   /**
33    * Register a new Session to forward the messages to.
34    *
35    * A session without any `id` attribute will be considered to be the
36    * default one, to which messages without `sessionId` attribute are
37    * forwarded to. Only one such session can be registered.
38    *
39    * @param {Session} session
40    *     The session to register.
41    */
42   registerSession(session) {
43     // CDP is not compatible with Fission by default, check the appropriate
44     // preferences are set to ensure compatibility.
45     if (
46       Services.prefs.getIntPref("fission.webContentIsolationStrategy") !== 0 ||
47       Services.prefs.getBoolPref("fission.bfcacheInParent")
48     ) {
49       lazy.logger.warn(
50         `Invalid browser preferences for CDP. Set "fission.webContentIsolationStrategy"` +
51           `to 0 and "fission.bfcacheInParent" to false before Firefox starts.`
52       );
53     }
55     if (!session.id) {
56       if (this.defaultSession) {
57         throw new Error(
58           "Default session is already set on Connection, " +
59             "can't register another one."
60         );
61       }
62       this.defaultSession = session;
63     }
65     this.sessions.set(session.id, session);
66   }
68   /**
69    * Send an error back to the CDP client.
70    *
71    * @param {number} id
72    *     Id of the packet which lead to an error.
73    * @param {Error} err
74    *     Error object with `message` and `stack` attributes.
75    * @param {string=} sessionId
76    *     Id of the session used to send this packet. Falls back to the
77    *     default session if not specified.
78    */
79   sendError(id, err, sessionId) {
80     const error = {
81       message: err.message,
82       data: err.stack,
83     };
85     this.send({ id, error, sessionId });
86   }
88   /**
89    * Send an event coming from a Domain to the CDP client.
90    *
91    * @param {string} method
92    *     The event name. This is composed by a domain name, a dot character
93    *     followed by the event name, e.g. `Target.targetCreated`.
94    * @param {object} params
95    *     A JSON-serializable object, which is the payload of this event.
96    * @param {string=} sessionId
97    *     The sessionId from which this packet is emitted. Falls back to the
98    *     default session if not specified.
99    */
100   sendEvent(method, params, sessionId) {
101     this.send({ method, params, sessionId });
103     if (Services.profiler?.IsActive()) {
104       ChromeUtils.addProfilerMarker(
105         "CDP: Event",
106         { category: "Remote-Protocol" },
107         method
108       );
109     }
111     // When a client attaches to a secondary target via
112     // `Target.attachToTarget`, we should emit an event back with the
113     // result including the `sessionId` attribute of this secondary target's
114     // session. `Target.attachToTarget` creates the secondary session and
115     // returns the session ID.
116     if (sessionId) {
117       // receivedMessageFromTarget is expected to send a raw CDP packet
118       // in the `message` property and it to be already serialized to a
119       // string
120       this.send({
121         method: "Target.receivedMessageFromTarget",
122         params: { sessionId, message: JSON.stringify({ method, params }) },
123       });
124     }
125   }
127   /**
128    * Interpret a given CDP packet for a given Session.
129    *
130    * @param {string} sessionId
131    *     ID of the session for which we should execute a command.
132    * @param {string} message
133    *     The stringified JSON payload of the CDP packet, which is about
134    *     executing a Domain's function.
135    */
136   sendMessageToTarget(sessionId, message) {
137     const session = this.sessions.get(sessionId);
138     if (!session) {
139       throw new Error(`Session '${sessionId}' doesn't exist.`);
140     }
141     // `message` is received from `Target.sendMessageToTarget` where the
142     // message attribute is a stringified JSON payload which represent a CDP
143     // packet.
144     const packet = JSON.parse(message);
146     // The CDP packet sent by the client shouldn't have a sessionId attribute
147     // as it is passed as another argument of `Target.sendMessageToTarget`.
148     // Set it here in order to reuse the codepath of flatten session, where
149     // the client sends CDP packets with a `sessionId` attribute instead
150     // of going through the old and probably deprecated
151     // `Target.sendMessageToTarget` API.
152     packet.sessionId = sessionId;
153     this.onPacket(packet);
154   }
156   /**
157    * Send the result of a call to a Domain's function back to the CDP client.
158    *
159    * @param {number} id
160    *     The request id being sent by the client to call the domain's method.
161    * @param {object} result
162    *     A JSON-serializable object, which is the actual result.
163    * @param {string=} sessionId
164    *     The sessionId from which this packet is emitted. Falls back to the
165    *     default session if not specified.
166    */
167   sendResult(id, result, sessionId) {
168     result = typeof result != "undefined" ? result : {};
169     this.send({ id, result, sessionId });
171     // When a client attaches to a secondary target via
172     // `Target.attachToTarget`, and it executes a command via
173     // `Target.sendMessageToTarget`, we should emit an event back with the
174     // result including the `sessionId` attribute of this secondary target's
175     // session. `Target.attachToTarget` creates the secondary session and
176     // returns the session ID.
177     if (sessionId) {
178       // receivedMessageFromTarget is expected to send a raw CDP packet
179       // in the `message` property and it to be already serialized to a
180       // string
181       this.send({
182         method: "Target.receivedMessageFromTarget",
183         params: { sessionId, message: JSON.stringify({ id, result }) },
184       });
185     }
186   }
188   // Transport hooks
190   /**
191    * Called by the `transport` when the connection is closed.
192    */
193   onConnectionClose() {
194     // Cleanup all the registered sessions.
195     for (const session of this.sessions.values()) {
196       session.destructor();
197     }
198     this.sessions.clear();
200     super.onConnectionClose();
201   }
203   /**
204    * Receive a packet from the WebSocket layer.
205    *
206    * This packet is sent by a CDP client and is meant to execute
207    * a particular function on a given Domain.
208    *
209    * @param {object} packet
210    *        JSON-serializable object sent by the client.
211    */
212   async onPacket(packet) {
213     super.onPacket(packet);
215     const { id, method, params, sessionId } = packet;
216     const startTime = Cu.now();
218     try {
219       // First check for mandatory field in the packets
220       if (typeof id == "undefined") {
221         throw new TypeError("Message missing 'id' field");
222       }
223       if (typeof method == "undefined") {
224         throw new TypeError("Message missing 'method' field");
225       }
227       // Extract the domain name and the method name out of `method` attribute
228       const { domain, command } = splitMethod(method);
230       // If a `sessionId` field is passed, retrieve the session to which we
231       // should forward this packet. Otherwise send it to the default session.
232       let session;
233       if (!sessionId) {
234         if (!this.defaultSession) {
235           throw new Error("Connection is missing a default Session.");
236         }
237         session = this.defaultSession;
238       } else {
239         session = this.sessions.get(sessionId);
240         if (!session) {
241           throw new Error(`Session '${sessionId}' doesn't exists.`);
242         }
243       }
245       // Bug 1600317 - Workaround to deny internal methods to be called
246       if (command.startsWith("_")) {
247         throw new lazy.UnknownMethodError(command);
248       }
250       // Finally, instruct the targeted session to execute the command
251       const result = await session.execute(id, domain, command, params);
252       this.sendResult(id, result, sessionId);
253     } catch (e) {
254       this.sendError(id, e, packet.sessionId);
255     }
257     if (Services.profiler?.IsActive()) {
258       ChromeUtils.addProfilerMarker(
259         "CDP: Command",
260         { startTime, category: "Remote-Protocol" },
261         `${method} (${id})`
262       );
263     }
264   }
268  * Splits a CDP method into domain and command components.
270  * @param {string} method
271  *     Name of the method to split, e.g. "Browser.getVersion".
273  * @returns {Object<string, string>}
274  *     Object with the domain ("Browser") and command ("getVersion")
275  *     as properties.
276  */
277 export function splitMethod(method) {
278   const parts = method.split(".");
280   if (parts.length != 2 || !parts[0].length || !parts[1].length) {
281     throw new TypeError(`Invalid method format: '${method}'`);
282   }
284   return {
285     domain: parts[0],
286     command: parts[1],
287   };