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/. */
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",
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],
31 * Entry class for the WebDriver BiDi support.
33 * @see https://w3c.github.io/webdriver-bidi
35 export class WebDriverBiDi {
40 #sessionlessConnections;
43 * Creates a new instance of the WebDriverBiDi class.
45 * @param {RemoteAgent} agent
46 * Reference to the Remote Agent instance.
50 this.#running = false;
54 this.#sessionlessConnections = new Set();
58 return `ws://${this.#agent.host}:${this.#agent.port}`;
65 #newSessionAlgorithm(session, flags) {
66 if (!this.#agent.running) {
67 // With the Remote Agent not running WebDriver BiDi is not supported.
71 if (flags.has(lazy.WebDriverSession.SESSION_FLAG_BIDI)) {
72 // It's already a WebDriver BiDi session.
76 const webSocketUrl = session.capabilities.get("webSocketUrl");
77 if (webSocketUrl === undefined) {
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}`);
92 * Add a new connection that is not yet attached to a WebDriver session.
94 * @param {WebDriverBiDiConnection} connection
95 * The connection without an associated WebDriver session.
97 addSessionlessConnection(connection) {
98 this.#sessionlessConnections.add(connection);
102 * Create a new WebDriver session.
104 * @param {Record<string, *>=} capabilities
105 * JSON Object containing any of the recognised capabilities as listed
106 * on the `WebDriverSession` class.
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.
113 * @returns {Record<string, Capabilities>}
114 * Object containing the current session ID, and all its capabilities.
116 * @throws {SessionNotCreatedError}
117 * If, for whatever reason, a session could not be created.
119 async createSession(capabilities, flags, sessionlessConnection) {
121 throw new lazy.error.SessionNotCreatedError(
122 "Maximum number of active sessions"
126 this.#session = new lazy.WebDriverSession(
129 sessionlessConnection
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);
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;
151 sessionId: this.#session.id,
152 capabilities: this.#session.capabilities,
157 * Delete the current WebDriver session.
160 if (!this.#session) {
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}`);
171 // For multiple session check first if the last session was closed.
172 lazy.cleanupCacheBypassState();
174 this.#session.destroy();
175 this.#session = null;
179 * Retrieve the readiness state of the remote end, regarding the creation of
180 * new WebDriverBiDi sessions.
182 * See https://w3c.github.io/webdriver-bidi/#command-session-status
185 * The readiness state.
187 getSessionReadinessStatus() {
189 // We currently only support one session, see Bug 1720707.
192 message: "Session already started",
203 * Starts the WebDriver BiDi support.
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(
217 new lazy.WebDriverNewSessionHandler(this)
220 Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`);
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"
231 ws_host: this.#agent.host,
232 ws_port: this.#agent.port,
236 this.#bidiServerPath,
237 lazy.textEncoder.encode(JSON.stringify(data, undefined, " "))
241 `Failed to create ${this.#bidiServerPath} (${e.message})`
247 * Stops the WebDriver BiDi support.
250 if (!this.#running) {
255 await IOUtils.remove(this.#bidiServerPath);
258 `Failed to remove ${this.#bidiServerPath} (${e.message})`
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();
271 lazy.logger.error("Failed to stop protocol", e);
273 this.#running = false;