Backed out changeset b71c8c052463 (bug 1943846) for causing mass failures. CLOSED...
[gecko.git] / remote / webdriver-bidi / WebDriverBiDi.sys.mjs
blob639c5f608008f7ca64f3942c71eccb82b914cecb
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   cleanupCacheBypassState:
9     "chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
10   error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
11   Log: "chrome://remote/content/shared/Log.sys.mjs",
12   RecommendedPreferences:
13     "chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
14   WebDriverNewSessionHandler:
15     "chrome://remote/content/webdriver-bidi/NewSessionHandler.sys.mjs",
16   WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
17 });
19 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
20   lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
22 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
24 const RECOMMENDED_PREFS = new Map([
25   // Enables permission isolation by user context.
26   // It should be enabled by default in Nightly in the scope of the bug 1641584.
27   ["permissions.isolateBy.userContext", true],
28 ]);
30 /**
31  * Entry class for the WebDriver BiDi support.
32  *
33  * @see https://w3c.github.io/webdriver-bidi
34  */
35 export class WebDriverBiDi {
36   #agent;
37   #bidiServerPath;
38   #running;
39   #session;
40   #sessionlessConnections;
42   /**
43    * Creates a new instance of the WebDriverBiDi class.
44    *
45    * @param {RemoteAgent} agent
46    *     Reference to the Remote Agent instance.
47    */
48   constructor(agent) {
49     this.#agent = agent;
50     this.#running = false;
52     this.#bidiServerPath;
53     this.#session = null;
54     this.#sessionlessConnections = new Set();
55   }
57   get address() {
58     return `ws://${this.#agent.host}:${this.#agent.port}`;
59   }
61   get session() {
62     return this.#session;
63   }
65   #newSessionAlgorithm(session, flags) {
66     if (!this.#agent.running) {
67       // With the Remote Agent not running WebDriver BiDi is not supported.
68       return;
69     }
71     if (flags.has(lazy.WebDriverSession.SESSION_FLAG_BIDI)) {
72       // It's already a WebDriver BiDi session.
73       return;
74     }
76     const webSocketUrl = session.capabilities.get("webSocketUrl");
77     if (webSocketUrl === undefined) {
78       return;
79     }
81     // Start listening for BiDi connections.
82     this.#agent.server.registerPathHandler(session.path, session);
83     lazy.logger.debug(`Registered session handler: ${session.path}`);
85     session.capabilities.set("webSocketUrl", `${this.address}${session.path}`);
87     session.bidi = true;
88     flags.add("bidi");
89   }
91   /**
92    * Add a new connection that is not yet attached to a WebDriver session.
93    *
94    * @param {WebDriverBiDiConnection} connection
95    *     The connection without an associated WebDriver session.
96    */
97   addSessionlessConnection(connection) {
98     this.#sessionlessConnections.add(connection);
99   }
101   /**
102    * Create a new WebDriver session.
103    *
104    * @param {Record<string, *>=} capabilities
105    *     JSON Object containing any of the recognised capabilities as listed
106    *     on the `WebDriverSession` class.
107    * @param {Set} flags
108    *     Session configuration flags.
109    * @param {WebDriverBiDiConnection=} sessionlessConnection
110    *     Optional connection that is not yet associated with a WebDriver
111    *     session, and has to be associated with the new WebDriver session.
112    *
113    * @returns {Record<string, Capabilities>}
114    *     Object containing the current session ID, and all its capabilities.
115    *
116    * @throws {SessionNotCreatedError}
117    *     If, for whatever reason, a session could not be created.
118    */
119   async createSession(capabilities, flags, sessionlessConnection) {
120     if (this.#session) {
121       throw new lazy.error.SessionNotCreatedError(
122         "Maximum number of active sessions"
123       );
124     }
126     this.#session = new lazy.WebDriverSession(
127       capabilities,
128       flags,
129       sessionlessConnection
130     );
132     // Run new session steps for WebDriver BiDi.
133     this.#newSessionAlgorithm(this.#session, flags);
135     if (sessionlessConnection) {
136       // Connection is now registered with a WebDriver session
137       this.#sessionlessConnections.delete(sessionlessConnection);
138     }
140     if (this.#session.bidi) {
141       // Creating a WebDriver BiDi session too early can cause issues with
142       // clients in not being able to find any available browsing context.
143       // Also when closing the application while it's still starting up can
144       // cause shutdown hangs. As such WebDriver BiDi will return a new session
145       // once the initial application window has finished initializing.
146       lazy.logger.debug(`Waiting for initial application window`);
147       await this.#agent.browserStartupFinished;
148     }
150     return {
151       sessionId: this.#session.id,
152       capabilities: this.#session.capabilities,
153     };
154   }
156   /**
157    * Delete the current WebDriver session.
158    */
159   deleteSession() {
160     if (!this.#session) {
161       return;
162     }
164     // When the Remote Agent is listening, and a BiDi WebSocket is active,
165     // unregister the path handler for the session.
166     if (this.#agent.running && this.#session.capabilities.get("webSocketUrl")) {
167       this.#agent.server.registerPathHandler(this.#session.path, null);
168       lazy.logger.debug(`Unregistered session handler: ${this.#session.path}`);
169     }
171     // For multiple session check first if the last session was closed.
172     lazy.cleanupCacheBypassState();
174     this.#session.destroy();
175     this.#session = null;
176   }
178   /**
179    * Retrieve the readiness state of the remote end, regarding the creation of
180    * new WebDriverBiDi sessions.
181    *
182    * See https://w3c.github.io/webdriver-bidi/#command-session-status
183    *
184    * @returns {object}
185    *     The readiness state.
186    */
187   getSessionReadinessStatus() {
188     if (this.#session) {
189       // We currently only support one session, see Bug 1720707.
190       return {
191         ready: false,
192         message: "Session already started",
193       };
194     }
196     return {
197       ready: true,
198       message: "",
199     };
200   }
202   /**
203    * Starts the WebDriver BiDi support.
204    */
205   async start() {
206     if (this.#running) {
207       return;
208     }
210     this.#running = true;
212     lazy.RecommendedPreferences.applyPreferences(RECOMMENDED_PREFS);
214     // Install a HTTP handler for direct WebDriver BiDi connection requests.
215     this.#agent.server.registerPathHandler(
216       "/session",
217       new lazy.WebDriverNewSessionHandler(this)
218     );
220     Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`);
222     try {
223       // Write WebSocket connection details to the WebDriverBiDiServer.json file
224       // located within the application's profile.
225       this.#bidiServerPath = PathUtils.join(
226         PathUtils.profileDir,
227         "WebDriverBiDiServer.json"
228       );
230       const data = {
231         ws_host: this.#agent.host,
232         ws_port: this.#agent.port,
233       };
235       await IOUtils.write(
236         this.#bidiServerPath,
237         lazy.textEncoder.encode(JSON.stringify(data, undefined, "  "))
238       );
239     } catch (e) {
240       lazy.logger.warn(
241         `Failed to create ${this.#bidiServerPath} (${e.message})`
242       );
243     }
244   }
246   /**
247    * Stops the WebDriver BiDi support.
248    */
249   async stop() {
250     if (!this.#running) {
251       return;
252     }
254     try {
255       await IOUtils.remove(this.#bidiServerPath);
256     } catch (e) {
257       lazy.logger.warn(
258         `Failed to remove ${this.#bidiServerPath} (${e.message})`
259       );
260     }
262     try {
263       // Close open session
264       this.deleteSession();
265       this.#agent.server.registerPathHandler("/session", null);
267       // Close all open session-less connections
268       this.#sessionlessConnections.forEach(connection => connection.close());
269       this.#sessionlessConnections.clear();
270     } catch (e) {
271       lazy.logger.error("Failed to stop protocol", e);
272     } finally {
273       this.#running = false;
274     }
275   }