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