Bug 1866798 - Add QA tests r=taskgraph-reviewers,releng-reviewers,mboldan,benchatt...
[gecko.git] / remote / webdriver-bidi / WebDriverBiDi.sys.mjs
blob00503ca2f629ed7eec1f6d2d95f413fa36a9228c
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 const lazy = {};
7 ChromeUtils.defineESModuleGetters(lazy, {
8   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
9   Log: "chrome://remote/content/shared/Log.sys.mjs",
10   WebDriverNewSessionHandler:
11     "chrome://remote/content/webdriver-bidi/NewSessionHandler.sys.mjs",
12   WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
13 });
15 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
16   lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
18 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
20 /**
21  * Entry class for the WebDriver BiDi support.
22  *
23  * @see https://w3c.github.io/webdriver-bidi
24  */
25 export class WebDriverBiDi {
26   /**
27    * Creates a new instance of the WebDriverBiDi class.
28    *
29    * @param {RemoteAgent} agent
30    *     Reference to the Remote Agent instance.
31    */
32   constructor(agent) {
33     this.agent = agent;
34     this._running = false;
36     this._session = null;
37     this._sessionlessConnections = new Set();
38   }
40   get address() {
41     return `ws://${this.agent.host}:${this.agent.port}`;
42   }
44   get session() {
45     return this._session;
46   }
48   /**
49    * Add a new connection that is not yet attached to a WebDriver session.
50    *
51    * @param {WebDriverBiDiConnection} connection
52    *     The connection without an accociated WebDriver session.
53    */
54   addSessionlessConnection(connection) {
55     this._sessionlessConnections.add(connection);
56   }
58   /**
59    * Create a new WebDriver session.
60    *
61    * @param {Object<string, *>=} capabilities
62    *     JSON Object containing any of the recognised capabilities as listed
63    *     on the `WebDriverSession` class.
64    *
65    * @param {WebDriverBiDiConnection=} sessionlessConnection
66    *     Optional connection that is not yet accociated with a WebDriver
67    *     session, and has to be associated with the new WebDriver session.
68    *
69    * @returns {Object<string, Capabilities>}
70    *     Object containing the current session ID, and all its capabilities.
71    *
72    * @throws {SessionNotCreatedError}
73    *     If, for whatever reason, a session could not be created.
74    */
75   async createSession(capabilities, sessionlessConnection) {
76     if (this.session) {
77       throw new lazy.error.SessionNotCreatedError(
78         "Maximum number of active sessions"
79       );
80     }
82     const session = new lazy.WebDriverSession(
83       capabilities,
84       sessionlessConnection
85     );
87     // When the Remote Agent is listening, and a BiDi WebSocket connection
88     // has been requested, register a path handler for the session.
89     let webSocketUrl = null;
90     if (
91       this.agent.running &&
92       (session.capabilities.get("webSocketUrl") || sessionlessConnection)
93     ) {
94       // Creating a WebDriver BiDi session too early can cause issues with
95       // clients in not being able to find any available browsing context.
96       // Also when closing the application while it's still starting up can
97       // cause shutdown hangs. As such WebDriver BiDi will return a new session
98       // once the initial application window has finished initializing.
99       lazy.logger.debug(`Waiting for initial application window`);
100       await this.agent.browserStartupFinished;
102       this.agent.server.registerPathHandler(session.path, session);
103       webSocketUrl = `${this.address}${session.path}`;
105       lazy.logger.debug(`Registered session handler: ${session.path}`);
107       if (sessionlessConnection) {
108         // Remove temporary session-less connection
109         this._sessionlessConnections.delete(sessionlessConnection);
110       }
111     }
113     // Also update the webSocketUrl capability to contain the session URL if
114     // a path handler has been registered. Otherwise set its value to null.
115     session.capabilities.set("webSocketUrl", webSocketUrl);
117     this._session = session;
119     return {
120       sessionId: this.session.id,
121       capabilities: this.session.capabilities,
122     };
123   }
125   /**
126    * Delete the current WebDriver session.
127    */
128   deleteSession() {
129     if (!this.session) {
130       return;
131     }
133     // When the Remote Agent is listening, and a BiDi WebSocket is active,
134     // unregister the path handler for the session.
135     if (this.agent.running && this.session.capabilities.get("webSocketUrl")) {
136       this.agent.server.registerPathHandler(this.session.path, null);
137       lazy.logger.debug(`Unregistered session handler: ${this.session.path}`);
138     }
140     this.session.destroy();
141     this._session = null;
142   }
144   /**
145    * Retrieve the readiness state of the remote end, regarding the creation of
146    * new WebDriverBiDi sessions.
147    *
148    * See https://w3c.github.io/webdriver-bidi/#command-session-status
149    *
150    * @returns {object}
151    *     The readiness state.
152    */
153   getSessionReadinessStatus() {
154     if (this.session) {
155       // We currently only support one session, see Bug 1720707.
156       return {
157         ready: false,
158         message: "Session already started",
159       };
160     }
162     return {
163       ready: true,
164       message: "",
165     };
166   }
168   /**
169    * Starts the WebDriver BiDi support.
170    */
171   async start() {
172     if (this._running) {
173       return;
174     }
176     this._running = true;
178     // Install a HTTP handler for direct WebDriver BiDi connection requests.
179     this.agent.server.registerPathHandler(
180       "/session",
181       new lazy.WebDriverNewSessionHandler(this)
182     );
184     Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`);
186     // Write WebSocket connection details to the WebDriverBiDiServer.json file
187     // located within the application's profile.
188     this._bidiServerPath = PathUtils.join(
189       PathUtils.profileDir,
190       "WebDriverBiDiServer.json"
191     );
193     const data = {
194       ws_host: this.agent.host,
195       ws_port: this.agent.port,
196     };
198     try {
199       await IOUtils.write(
200         this._bidiServerPath,
201         lazy.textEncoder.encode(JSON.stringify(data, undefined, "  "))
202       );
203     } catch (e) {
204       lazy.logger.warn(
205         `Failed to create ${this._bidiServerPath} (${e.message})`
206       );
207     }
208   }
210   /**
211    * Stops the WebDriver BiDi support.
212    */
213   async stop() {
214     if (!this._running) {
215       return;
216     }
218     try {
219       await IOUtils.remove(this._bidiServerPath);
220     } catch (e) {
221       lazy.logger.warn(
222         `Failed to remove ${this._bidiServerPath} (${e.message})`
223       );
224     }
226     try {
227       // Close open session
228       this.deleteSession();
229       this.agent.server.registerPathHandler("/session", null);
231       // Close all open session-less connections
232       this._sessionlessConnections.forEach(connection => connection.close());
233       this._sessionlessConnections.clear();
234     } catch (e) {
235       lazy.logger.error("Failed to stop protocol", e);
236     } finally {
237       this._running = false;
238     }
239   }