WebDriverBiDiConnection.sys.mjs (9103B)
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 import { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs"; 6 7 const lazy = {}; 8 9 ChromeUtils.defineESModuleGetters(lazy, { 10 assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", 11 error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", 12 Log: "chrome://remote/content/shared/Log.sys.mjs", 13 pprint: "chrome://remote/content/shared/Format.sys.mjs", 14 processCapabilities: 15 "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs", 16 quit: "chrome://remote/content/shared/Browser.sys.mjs", 17 RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs", 18 WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs", 19 }); 20 21 ChromeUtils.defineLazyGetter(lazy, "logger", () => 22 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI) 23 ); 24 25 export class WebDriverBiDiConnection extends WebSocketConnection { 26 #sessionConfigFlags; 27 28 /** 29 * @param {WebSocket} webSocket 30 * The WebSocket server connection to wrap. 31 * @param {Connection} httpdConnection 32 * Reference to the httpd.js's connection needed for clean-up. 33 */ 34 constructor(webSocket, httpdConnection) { 35 super(webSocket, httpdConnection); 36 37 // Each connection has only a single associated WebDriver session. 38 this.session = null; 39 40 this.#sessionConfigFlags = new Set([ 41 lazy.WebDriverSession.SESSION_FLAG_BIDI, 42 ]); 43 } 44 45 /** 46 * Perform required steps to end the session. 47 */ 48 endSession() { 49 // TODO Bug 1838269. Implement session ending logic 50 // for the case of classic + bidi session. 51 // We currently only support one session, see Bug 1720707. 52 lazy.RemoteAgent.webDriverBiDi.deleteSession(); 53 } 54 55 /** 56 * Register a new WebDriver Session to forward the messages to. 57 * 58 * @param {Session} session 59 * The WebDriverSession to register. 60 */ 61 registerSession(session) { 62 if (this.session) { 63 throw new lazy.error.UnknownError( 64 "A WebDriver session has already been set" 65 ); 66 } 67 68 this.session = session; 69 lazy.logger.debug( 70 `Connection ${this.id} attached to session ${session.id}` 71 ); 72 } 73 74 /** 75 * Unregister the already set WebDriver session. 76 */ 77 unregisterSession() { 78 if (!this.session) { 79 return; 80 } 81 82 this.session.removeConnection(this); 83 this.session = null; 84 } 85 86 /** 87 * Send an error back to the WebDriver BiDi client. 88 * 89 * @param {number} id 90 * Id of the packet which lead to an error. 91 * @param {Error} err 92 * Error object with `status`, `message` and `stack` attributes. 93 */ 94 sendError(id, err) { 95 // Convert specific MessageHandler errors to WebDriver errors 96 let webDriverError; 97 switch (err.name) { 98 case "DiscardedBrowsingContextError": 99 webDriverError = lazy.error.wrap(err, lazy.error.NoSuchFrameError); 100 break; 101 default: 102 webDriverError = lazy.error.wrap(err); 103 } 104 105 this.send({ 106 type: "error", 107 id, 108 error: webDriverError.status, 109 message: webDriverError.message, 110 stacktrace: webDriverError.stack, 111 }); 112 } 113 114 /** 115 * Send an event coming from a module to the WebDriver BiDi client. 116 * 117 * @param {string} method 118 * The event name. This is composed by a module name, a dot character 119 * followed by the event name, e.g. `log.entryAdded`. 120 * @param {object} params 121 * A JSON-serializable object, which is the payload of this event. 122 */ 123 sendEvent(method, params) { 124 this.send({ type: "event", method, params }); 125 126 if (Services.profiler?.IsActive()) { 127 ChromeUtils.addProfilerMarker( 128 "BiDi: Event", 129 { category: "Remote-Protocol" }, 130 method 131 ); 132 } 133 } 134 135 /** 136 * Send the result of a call to a module's method back to the 137 * WebDriver BiDi client. 138 * 139 * @param {number} id 140 * The request id being sent by the client to call the module's method. 141 * @param {object} result 142 * A JSON-serializable object, which is the actual result. 143 */ 144 sendResult(id, result) { 145 result = typeof result !== "undefined" ? result : {}; 146 this.send({ type: "success", id, result }); 147 } 148 149 observe(subject, topic) { 150 switch (topic) { 151 case "quit-application-requested": 152 this.endSession(); 153 break; 154 } 155 } 156 157 // Transport hooks 158 159 /** 160 * Called by the `transport` when the connection is closed. 161 */ 162 onConnectionClose() { 163 this.unregisterSession(); 164 165 super.onConnectionClose(); 166 } 167 168 /** 169 * Receive a packet from the WebSocket layer. 170 * 171 * This packet is sent by a WebDriver BiDi client and is meant to execute 172 * a particular method on a given module. 173 * 174 * @param {object} packet 175 * JSON-serializable object sent by the client 176 */ 177 async onPacket(packet) { 178 super.onPacket(packet); 179 180 const { id, method, params } = packet; 181 const startTime = ChromeUtils.now(); 182 183 try { 184 // First check for mandatory field in the command packet 185 lazy.assert.positiveInteger( 186 id, 187 lazy.pprint`Expected "id" to be a positive integer, got ${id}` 188 ); 189 lazy.assert.string( 190 method, 191 lazy.pprint`Expected "method" to be a string, got ${method}` 192 ); 193 lazy.assert.object( 194 params, 195 lazy.pprint`Expected "params" to be an object, got ${params}` 196 ); 197 198 // Extract the module and the command name out of `method` attribute 199 const { module, command } = splitMethod(method); 200 let result; 201 202 // Handle static commands first 203 if (module === "session" && command === "new") { 204 const processedCapabilities = lazy.processCapabilities(params); 205 206 result = await lazy.RemoteAgent.webDriverBiDi.createSession( 207 processedCapabilities, 208 this.#sessionConfigFlags, 209 this 210 ); 211 212 // Until the spec (see: https://github.com/w3c/webdriver/issues/1834) 213 // is updated to specify what should be the default value for bidi session, 214 // remove this capability from the return value when it's not provided by a client. 215 if (!("unhandledPromptBehavior" in processedCapabilities)) { 216 // We don't want to modify the original `capabilities` field 217 // because it points to an original Capabilities object used by the session. 218 // Since before the result is sent to a client we're going anyway to call 219 // `JSON.stringify` on `result` which will call `toJSON` method recursively, 220 // we can call it already here for the `capabilities` property 221 // to update only the command response object. 222 result.capabilities = result.capabilities.toJSON(); 223 delete result.capabilities.unhandledPromptBehavior; 224 } 225 } else if (module === "session" && command === "status") { 226 result = lazy.RemoteAgent.webDriverBiDi.getSessionReadinessStatus(); 227 } else { 228 lazy.assert.session(this.session); 229 230 // Bug 1741854 - Workaround to deny internal methods to be called 231 if (command.startsWith("_")) { 232 throw new lazy.error.UnknownCommandError(method); 233 } 234 235 // Finally, instruct the session to execute the command 236 result = await this.session.execute(module, command, params); 237 } 238 239 this.sendResult(id, result); 240 241 // Session clean up. 242 if (module === "session" && command === "end") { 243 this.endSession(); 244 } 245 // Close the browser. 246 // TODO Bug 1842018. Refactor this part to return the response 247 // when the quitting of the browser is finished. 248 else if (module === "browser" && command === "close") { 249 // Register handler to run WebDriver BiDi specific shutdown code. 250 Services.obs.addObserver(this, "quit-application-requested"); 251 252 // TODO Bug 1836282. Add as the third argument "moz:windowless" capability 253 // from the session, when this capability is supported by Webdriver BiDi. 254 await lazy.quit(["eForceQuit"], false); 255 256 Services.obs.removeObserver(this, "quit-application-requested"); 257 } 258 } catch (e) { 259 this.sendError(id, e); 260 } 261 262 if (Services.profiler?.IsActive()) { 263 ChromeUtils.addProfilerMarker( 264 "BiDi: Command", 265 { startTime, category: "Remote-Protocol" }, 266 `${method} (${id})` 267 ); 268 } 269 } 270 } 271 272 /** 273 * Splits a WebDriver BiDi method into module and command components. 274 * 275 * @param {string} method 276 * Name of the method to split, e.g. "session.subscribe". 277 * 278 * @returns {Record<string, string>} 279 * Object with the module ("session") and command ("subscribe") 280 * as properties. 281 */ 282 export function splitMethod(method) { 283 const parts = method.split("."); 284 285 if (parts.length != 2 || !parts[0].length || !parts[1].length) { 286 throw new TypeError(`Invalid method format: '${method}'`); 287 } 288 289 return { 290 module: parts[0], 291 command: parts[1], 292 }; 293 }