Bug 1931425 - Limit how often moz-label's #setStyles runs r=reusable-components-revie...
[gecko.git] / remote / cdp / CDPConnection.sys.mjs
blob093aeda944a7ca873feaf0e62263019569801a35
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     lazy.logger.warn(
44       `Support for the Chrome DevTools Protocol (CDP) in Firefox will be deprecated after Firefox 128 (ESR) ` +
45         `and will be removed in a later release. CDP users should consider migrating ` +
46         `to WebDriver BiDi. See https://bugzilla.mozilla.org/show_bug.cgi?id=1872254`
47     );
49     // CDP is not compatible with Fission by default, check the appropriate
50     // preferences are set to ensure compatibility.
51     if (
52       Services.prefs.getIntPref("fission.webContentIsolationStrategy") !== 0 ||
53       Services.prefs.getBoolPref("fission.bfcacheInParent")
54     ) {
55       lazy.logger.warn(
56         `Invalid browser preferences for CDP. Set "fission.webContentIsolationStrategy"` +
57           `to 0 and "fission.bfcacheInParent" to false before Firefox starts.`
58       );
59     }
61     if (!session.id) {
62       if (this.defaultSession) {
63         throw new Error(
64           "Default session is already set on Connection, " +
65             "can't register another one."
66         );
67       }
68       this.defaultSession = session;
69     }
71     this.sessions.set(session.id, session);
72   }
74   /**
75    * Send an error back to the CDP client.
76    *
77    * @param {number} id
78    *     Id of the packet which lead to an error.
79    * @param {Error} err
80    *     Error object with `message` and `stack` attributes.
81    * @param {string=} sessionId
82    *     Id of the session used to send this packet. Falls back to the
83    *     default session if not specified.
84    */
85   sendError(id, err, sessionId) {
86     const error = {
87       message: err.message,
88       data: err.stack,
89     };
91     this.send({ id, error, sessionId });
92   }
94   /**
95    * Send an event coming from a Domain to the CDP client.
96    *
97    * @param {string} method
98    *     The event name. This is composed by a domain name, a dot character
99    *     followed by the event name, e.g. `Target.targetCreated`.
100    * @param {object} params
101    *     A JSON-serializable object, which is the payload of this event.
102    * @param {string=} sessionId
103    *     The sessionId from which this packet is emitted. Falls back to the
104    *     default session if not specified.
105    */
106   sendEvent(method, params, sessionId) {
107     this.send({ method, params, sessionId });
109     if (Services.profiler?.IsActive()) {
110       ChromeUtils.addProfilerMarker(
111         "CDP: Event",
112         { category: "Remote-Protocol" },
113         method
114       );
115     }
117     // When a client attaches to a secondary target via
118     // `Target.attachToTarget`, we should emit an event back with the
119     // result including the `sessionId` attribute of this secondary target's
120     // session. `Target.attachToTarget` creates the secondary session and
121     // returns the session ID.
122     if (sessionId) {
123       // receivedMessageFromTarget is expected to send a raw CDP packet
124       // in the `message` property and it to be already serialized to a
125       // string
126       this.send({
127         method: "Target.receivedMessageFromTarget",
128         params: { sessionId, message: JSON.stringify({ method, params }) },
129       });
130     }
131   }
133   /**
134    * Interpret a given CDP packet for a given Session.
135    *
136    * @param {string} sessionId
137    *     ID of the session for which we should execute a command.
138    * @param {string} message
139    *     The stringified JSON payload of the CDP packet, which is about
140    *     executing a Domain's function.
141    */
142   sendMessageToTarget(sessionId, message) {
143     const session = this.sessions.get(sessionId);
144     if (!session) {
145       throw new Error(`Session '${sessionId}' doesn't exist.`);
146     }
147     // `message` is received from `Target.sendMessageToTarget` where the
148     // message attribute is a stringified JSON payload which represent a CDP
149     // packet.
150     const packet = JSON.parse(message);
152     // The CDP packet sent by the client shouldn't have a sessionId attribute
153     // as it is passed as another argument of `Target.sendMessageToTarget`.
154     // Set it here in order to reuse the codepath of flatten session, where
155     // the client sends CDP packets with a `sessionId` attribute instead
156     // of going through the old and probably deprecated
157     // `Target.sendMessageToTarget` API.
158     packet.sessionId = sessionId;
159     this.onPacket(packet);
160   }
162   /**
163    * Send the result of a call to a Domain's function back to the CDP client.
164    *
165    * @param {number} id
166    *     The request id being sent by the client to call the domain's method.
167    * @param {object} result
168    *     A JSON-serializable object, which is the actual result.
169    * @param {string=} sessionId
170    *     The sessionId from which this packet is emitted. Falls back to the
171    *     default session if not specified.
172    */
173   sendResult(id, result, sessionId) {
174     result = typeof result != "undefined" ? result : {};
175     this.send({ id, result, sessionId });
177     // When a client attaches to a secondary target via
178     // `Target.attachToTarget`, and it executes a command via
179     // `Target.sendMessageToTarget`, we should emit an event back with the
180     // result including the `sessionId` attribute of this secondary target's
181     // session. `Target.attachToTarget` creates the secondary session and
182     // returns the session ID.
183     if (sessionId) {
184       // receivedMessageFromTarget is expected to send a raw CDP packet
185       // in the `message` property and it to be already serialized to a
186       // string
187       this.send({
188         method: "Target.receivedMessageFromTarget",
189         params: { sessionId, message: JSON.stringify({ id, result }) },
190       });
191     }
192   }
194   // Transport hooks
196   /**
197    * Called by the `transport` when the connection is closed.
198    */
199   onConnectionClose() {
200     // Cleanup all the registered sessions.
201     for (const session of this.sessions.values()) {
202       session.destructor();
203     }
204     this.sessions.clear();
206     super.onConnectionClose();
207   }
209   /**
210    * Receive a packet from the WebSocket layer.
211    *
212    * This packet is sent by a CDP client and is meant to execute
213    * a particular function on a given Domain.
214    *
215    * @param {object} packet
216    *        JSON-serializable object sent by the client.
217    */
218   async onPacket(packet) {
219     super.onPacket(packet);
221     const { id, method, params, sessionId } = packet;
222     const startTime = Cu.now();
224     try {
225       // First check for mandatory field in the packets
226       if (typeof id == "undefined") {
227         throw new TypeError("Message missing 'id' field");
228       }
229       if (typeof method == "undefined") {
230         throw new TypeError("Message missing 'method' field");
231       }
233       // Extract the domain name and the method name out of `method` attribute
234       const { domain, command } = splitMethod(method);
236       // If a `sessionId` field is passed, retrieve the session to which we
237       // should forward this packet. Otherwise send it to the default session.
238       let session;
239       if (!sessionId) {
240         if (!this.defaultSession) {
241           throw new Error("Connection is missing a default Session.");
242         }
243         session = this.defaultSession;
244       } else {
245         session = this.sessions.get(sessionId);
246         if (!session) {
247           throw new Error(`Session '${sessionId}' doesn't exists.`);
248         }
249       }
251       // Bug 1600317 - Workaround to deny internal methods to be called
252       if (command.startsWith("_")) {
253         throw new lazy.UnknownMethodError(command);
254       }
256       // Finally, instruct the targeted session to execute the command
257       const result = await session.execute(id, domain, command, params);
258       this.sendResult(id, result, sessionId);
259     } catch (e) {
260       this.sendError(id, e, packet.sessionId);
261     }
263     if (Services.profiler?.IsActive()) {
264       ChromeUtils.addProfilerMarker(
265         "CDP: Command",
266         { startTime, category: "Remote-Protocol" },
267         `${method} (${id})`
268       );
269     }
270   }
274  * Splits a CDP method into domain and command components.
276  * @param {string} method
277  *     Name of the method to split, e.g. "Browser.getVersion".
279  * @returns {Record<string, string>}
280  *     Object with the domain ("Browser") and command ("getVersion")
281  *     as properties.
282  */
283 export function splitMethod(method) {
284   const parts = method.split(".");
286   if (parts.length != 2 || !parts[0].length || !parts[1].length) {
287     throw new TypeError(`Invalid method format: '${method}'`);
288   }
290   return {
291     domain: parts[0],
292     command: parts[1],
293   };