WebDriverBiDi.sys.mjs (8289B)
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/. */ 4 5 const lazy = {}; 6 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 }); 18 19 ChromeUtils.defineLazyGetter(lazy, "logger", () => 20 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI) 21 ); 22 ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder()); 23 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 // Enables race-cache-with-network, which avoids issues with requests 29 // intercepted in the responseStarted phase. Without this preference, any 30 // subsequent request to the same URL as a suspended request hangs as well. 31 // Bug 1966494: should allow to unblock subsequent request, but might do so 32 // with a timer, slowing down tests. Should be reconsidered once fixed. 33 ["network.http.rcwn.enabled", true], 34 ]); 35 36 /** 37 * Entry class for the WebDriver BiDi support. 38 * 39 * @see https://w3c.github.io/webdriver-bidi 40 */ 41 export class WebDriverBiDi { 42 #agent; 43 #bidiServerPath; 44 #running; 45 #session; 46 #sessionlessConnections; 47 48 /** 49 * Creates a new instance of the WebDriverBiDi class. 50 * 51 * @param {RemoteAgent} agent 52 * Reference to the Remote Agent instance. 53 */ 54 constructor(agent) { 55 this.#agent = agent; 56 this.#running = false; 57 58 this.#bidiServerPath; 59 this.#session = null; 60 this.#sessionlessConnections = new Set(); 61 } 62 63 get address() { 64 return `ws://${this.#agent.host}:${this.#agent.port}`; 65 } 66 67 get session() { 68 return this.#session; 69 } 70 71 #newSessionAlgorithm(session, flags) { 72 if (!this.#agent.running) { 73 // With the Remote Agent not running WebDriver BiDi is not supported. 74 return; 75 } 76 77 if (flags.has(lazy.WebDriverSession.SESSION_FLAG_BIDI)) { 78 // It's already a WebDriver BiDi session. 79 return; 80 } 81 82 const webSocketUrl = session.capabilities.get("webSocketUrl"); 83 if (webSocketUrl === undefined) { 84 return; 85 } 86 87 // Start listening for BiDi connections. 88 this.#agent.server.registerPathHandler(session.path, session); 89 lazy.logger.debug(`Registered session handler: ${session.path}`); 90 91 session.capabilities.set("webSocketUrl", `${this.address}${session.path}`); 92 93 session.bidi = true; 94 flags.add("bidi"); 95 } 96 97 /** 98 * Add a new connection that is not yet attached to a WebDriver session. 99 * 100 * @param {WebDriverBiDiConnection} connection 101 * The connection without an associated WebDriver session. 102 */ 103 addSessionlessConnection(connection) { 104 this.#sessionlessConnections.add(connection); 105 } 106 107 /** 108 * Create a new WebDriver session. 109 * 110 * @param {Record<string, *>=} capabilities 111 * JSON Object containing any of the recognised capabilities as listed 112 * on the `WebDriverSession` class. 113 * @param {Set} flags 114 * Session configuration flags. 115 * @param {WebDriverBiDiConnection=} sessionlessConnection 116 * Optional connection that is not yet associated with a WebDriver 117 * session, and has to be associated with the new WebDriver session. 118 * 119 * @returns {Record<string, Capabilities>} 120 * Object containing the current session ID, and all its capabilities. 121 * 122 * @throws {SessionNotCreatedError} 123 * If, for whatever reason, a session could not be created. 124 */ 125 async createSession(capabilities, flags, sessionlessConnection) { 126 if (this.#session) { 127 throw new lazy.error.SessionNotCreatedError( 128 "Maximum number of active sessions" 129 ); 130 } 131 132 this.#session = new lazy.WebDriverSession( 133 capabilities, 134 flags, 135 sessionlessConnection 136 ); 137 138 // Run new session steps for WebDriver BiDi. 139 this.#newSessionAlgorithm(this.#session, flags); 140 141 if (sessionlessConnection) { 142 // Connection is now registered with a WebDriver session 143 this.#sessionlessConnections.delete(sessionlessConnection); 144 } 145 146 if (this.#session.bidi) { 147 // Creating a WebDriver BiDi session too early can cause issues with 148 // clients in not being able to find any available browsing context. 149 // Also when closing the application while it's still starting up can 150 // cause shutdown hangs. As such WebDriver BiDi will return a new session 151 // once the initial application window has finished initializing. 152 lazy.logger.debug(`Waiting for initial application window`); 153 await this.#agent.browserStartupFinished; 154 } 155 156 return { 157 sessionId: this.#session.id, 158 capabilities: this.#session.capabilities, 159 }; 160 } 161 162 /** 163 * Delete the current WebDriver session. 164 */ 165 deleteSession() { 166 if (!this.#session) { 167 return; 168 } 169 170 // When the Remote Agent is listening, and a BiDi WebSocket is active, 171 // unregister the path handler for the session. 172 if (this.#agent.running && this.#session.capabilities.get("webSocketUrl")) { 173 this.#agent.server.registerPathHandler(this.#session.path, null); 174 lazy.logger.debug(`Unregistered session handler: ${this.#session.path}`); 175 } 176 177 // For multiple session check first if the last session was closed. 178 lazy.cleanupCacheBypassState(); 179 180 this.#session.destroy(); 181 this.#session = null; 182 } 183 184 /** 185 * Retrieve the readiness state of the remote end, regarding the creation of 186 * new WebDriverBiDi sessions. 187 * 188 * See https://w3c.github.io/webdriver-bidi/#command-session-status 189 * 190 * @returns {object} 191 * The readiness state. 192 */ 193 getSessionReadinessStatus() { 194 if (this.#session) { 195 // We currently only support one session, see Bug 1720707. 196 return { 197 ready: false, 198 message: "Session already started", 199 }; 200 } 201 202 return { 203 ready: true, 204 message: "", 205 }; 206 } 207 208 /** 209 * Starts the WebDriver BiDi support. 210 */ 211 async start() { 212 if (this.#running) { 213 return; 214 } 215 216 this.#running = true; 217 218 lazy.RecommendedPreferences.applyPreferences(RECOMMENDED_PREFS); 219 220 // Install a HTTP handler for direct WebDriver BiDi connection requests. 221 this.#agent.server.registerPathHandler( 222 "/session", 223 new lazy.WebDriverNewSessionHandler(this) 224 ); 225 226 Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`); 227 228 try { 229 // Write WebSocket connection details to the WebDriverBiDiServer.json file 230 // located within the application's profile. 231 this.#bidiServerPath = PathUtils.join( 232 PathUtils.profileDir, 233 "WebDriverBiDiServer.json" 234 ); 235 236 const data = { 237 ws_host: this.#agent.host, 238 ws_port: this.#agent.port, 239 }; 240 241 await IOUtils.write( 242 this.#bidiServerPath, 243 lazy.textEncoder.encode(JSON.stringify(data, undefined, " ")) 244 ); 245 } catch (e) { 246 lazy.logger.warn( 247 `Failed to create ${this.#bidiServerPath} (${e.message})` 248 ); 249 } 250 } 251 252 /** 253 * Stops the WebDriver BiDi support. 254 */ 255 async stop() { 256 if (!this.#running) { 257 return; 258 } 259 260 try { 261 await IOUtils.remove(this.#bidiServerPath); 262 } catch (e) { 263 lazy.logger.warn( 264 `Failed to remove ${this.#bidiServerPath} (${e.message})` 265 ); 266 } 267 268 try { 269 // Close open session 270 this.deleteSession(); 271 this.#agent.server.registerPathHandler("/session", null); 272 273 // Close all open session-less connections 274 this.#sessionlessConnections.forEach(connection => connection.close()); 275 this.#sessionlessConnections.clear(); 276 } catch (e) { 277 lazy.logger.error("Failed to stop protocol", e); 278 } finally { 279 this.#running = false; 280 } 281 } 282 }