Bug 1869043 add a main thread record of track audio outputs r=padenot
[gecko.git] / remote / webdriver-bidi / WebDriverBiDiConnection.sys.mjs
blob5ec7ff9a06bfc0eb5b8c2c2434f722e5b88b156e
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   assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
11   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
12   Log: "chrome://remote/content/shared/Log.sys.mjs",
13   processCapabilities:
14     "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
15   quit: "chrome://remote/content/shared/Browser.sys.mjs",
16   RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
17   WEBDRIVER_CLASSIC_CAPABILITIES:
18     "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
19 });
21 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
22   lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
25 export class WebDriverBiDiConnection extends WebSocketConnection {
26   /**
27    * @param {WebSocket} webSocket
28    *     The WebSocket server connection to wrap.
29    * @param {Connection} httpdConnection
30    *     Reference to the httpd.js's connection needed for clean-up.
31    */
32   constructor(webSocket, httpdConnection) {
33     super(webSocket, httpdConnection);
35     // Each connection has only a single associated WebDriver session.
36     this.session = null;
37   }
39   /**
40    * Perform required steps to end the session.
41    */
42   endSession() {
43     // TODO Bug 1838269. Implement session ending logic
44     // for the case of classic + bidi session.
45     // We currently only support one session, see Bug 1720707.
46     lazy.RemoteAgent.webDriverBiDi.deleteSession();
47   }
49   /**
50    * Register a new WebDriver Session to forward the messages to.
51    *
52    * @param {Session} session
53    *     The WebDriverSession to register.
54    */
55   registerSession(session) {
56     if (this.session) {
57       throw new lazy.error.UnknownError(
58         "A WebDriver session has already been set"
59       );
60     }
62     this.session = session;
63     lazy.logger.debug(
64       `Connection ${this.id} attached to session ${session.id}`
65     );
66   }
68   /**
69    * Unregister the already set WebDriver session.
70    */
71   unregisterSession() {
72     if (!this.session) {
73       return;
74     }
76     this.session.removeConnection(this);
77     this.session = null;
78   }
80   /**
81    * Send an error back to the WebDriver BiDi client.
82    *
83    * @param {number} id
84    *     Id of the packet which lead to an error.
85    * @param {Error} err
86    *     Error object with `status`, `message` and `stack` attributes.
87    */
88   sendError(id, err) {
89     const webDriverError = lazy.error.wrap(err);
91     this.send({
92       type: "error",
93       id,
94       error: webDriverError.status,
95       message: webDriverError.message,
96       stacktrace: webDriverError.stack,
97     });
98   }
100   /**
101    * Send an event coming from a module to the WebDriver BiDi client.
102    *
103    * @param {string} method
104    *     The event name. This is composed by a module name, a dot character
105    *     followed by the event name, e.g. `log.entryAdded`.
106    * @param {object} params
107    *     A JSON-serializable object, which is the payload of this event.
108    */
109   sendEvent(method, params) {
110     this.send({ type: "event", method, params });
112     if (Services.profiler?.IsActive()) {
113       ChromeUtils.addProfilerMarker(
114         "BiDi: Event",
115         { category: "Remote-Protocol" },
116         method
117       );
118     }
119   }
121   /**
122    * Send the result of a call to a module's method back to the
123    * WebDriver BiDi client.
124    *
125    * @param {number} id
126    *     The request id being sent by the client to call the module's method.
127    * @param {object} result
128    *     A JSON-serializable object, which is the actual result.
129    */
130   sendResult(id, result) {
131     result = typeof result !== "undefined" ? result : {};
132     this.send({ type: "success", id, result });
133   }
135   observe(subject, topic) {
136     switch (topic) {
137       case "quit-application-requested":
138         this.endSession();
139         break;
140     }
141   }
143   // Transport hooks
145   /**
146    * Called by the `transport` when the connection is closed.
147    */
148   onConnectionClose() {
149     this.unregisterSession();
151     super.onConnectionClose();
152   }
154   /**
155    * Receive a packet from the WebSocket layer.
156    *
157    * This packet is sent by a WebDriver BiDi client and is meant to execute
158    * a particular method on a given module.
159    *
160    * @param {object} packet
161    *     JSON-serializable object sent by the client
162    */
163   async onPacket(packet) {
164     super.onPacket(packet);
166     const { id, method, params } = packet;
167     const startTime = Cu.now();
169     try {
170       // First check for mandatory field in the command packet
171       lazy.assert.positiveInteger(id, "id: unsigned integer value expected");
172       lazy.assert.string(method, "method: string value expected");
173       lazy.assert.object(params, "params: object value expected");
175       // Extract the module and the command name out of `method` attribute
176       const { module, command } = splitMethod(method);
177       let result;
179       // Handle static commands first
180       if (module === "session" && command === "new") {
181         const processedCapabilities = lazy.processCapabilities(params);
183         result = await lazy.RemoteAgent.webDriverBiDi.createSession(
184           processedCapabilities,
185           this
186         );
188         // Since in Capabilities class we setup default values also for capabilities which are
189         // not relevant for bidi, we want to remove them from the payload before returning to a client.
190         result.capabilities = Array.from(result.capabilities.entries()).reduce(
191           (object, [key, value]) => {
192             if (!lazy.WEBDRIVER_CLASSIC_CAPABILITIES.includes(key)) {
193               object[key] = value;
194             }
196             return object;
197           },
198           {}
199         );
200       } else if (module === "session" && command === "status") {
201         result = lazy.RemoteAgent.webDriverBiDi.getSessionReadinessStatus();
202       } else {
203         lazy.assert.session(this.session);
205         // Bug 1741854 - Workaround to deny internal methods to be called
206         if (command.startsWith("_")) {
207           throw new lazy.error.UnknownCommandError(method);
208         }
210         // Finally, instruct the session to execute the command
211         result = await this.session.execute(module, command, params);
212       }
214       this.sendResult(id, result);
216       // Session clean up.
217       if (module === "session" && command === "end") {
218         this.endSession();
219       }
220       // Close the browser.
221       // TODO Bug 1842018. Refactor this part to return the response
222       // when the quitting of the browser is finished.
223       else if (module === "browser" && command === "close") {
224         // Register handler to run WebDriver BiDi specific shutdown code.
225         Services.obs.addObserver(this, "quit-application-requested");
227         // TODO Bug 1836282. Add as the third argument "moz:windowless" capability
228         // from the session, when this capability is supported by Webdriver BiDi.
229         await lazy.quit(["eForceQuit"], false);
231         Services.obs.removeObserver(this, "quit-application-requested");
232       }
233     } catch (e) {
234       this.sendError(id, e);
235     }
237     if (Services.profiler?.IsActive()) {
238       ChromeUtils.addProfilerMarker(
239         "BiDi: Command",
240         { startTime, category: "Remote-Protocol" },
241         `${method} (${id})`
242       );
243     }
244   }
248  * Splits a WebDriver BiDi method into module and command components.
250  * @param {string} method
251  *     Name of the method to split, e.g. "session.subscribe".
253  * @returns {Object<string, string>}
254  *     Object with the module ("session") and command ("subscribe")
255  *     as properties.
256  */
257 export function splitMethod(method) {
258   const parts = method.split(".");
260   if (parts.length != 2 || !parts[0].length || !parts[1].length) {
261     throw new TypeError(`Invalid method format: '${method}'`);
262   }
264   return {
265     module: parts[0],
266     command: parts[1],
267   };