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 { WebSocketConnection } from "chrome://remote/content/shared/WebSocketConnection.sys.mjs";
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",
14 "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
15 quit: "chrome://remote/content/shared/Browser.sys.mjs",
16 RemoteAgent: "chrome://remote/content/components/RemoteAgent.sys.mjs",
17 WEBDRIVER_CLASSIC_CAPABILITIES:
18 "chrome://remote/content/shared/webdriver/Capabilities.sys.mjs",
21 ChromeUtils.defineLazyGetter(lazy, "logger", () =>
22 lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
25 export class WebDriverBiDiConnection extends WebSocketConnection {
27 * @param {WebSocket} webSocket
28 * The WebSocket server connection to wrap.
29 * @param {Connection} httpdConnection
30 * Reference to the httpd.js's connection needed for clean-up.
32 constructor(webSocket, httpdConnection) {
33 super(webSocket, httpdConnection);
35 // Each connection has only a single associated WebDriver session.
40 * Perform required steps to end the session.
43 // TODO Bug 1838269. Implement session ending logic
44 // for the case of classic + bidi session.
45 // We currently only support one session, see Bug 1720707.
46 lazy.RemoteAgent.webDriverBiDi.deleteSession();
50 * Register a new WebDriver Session to forward the messages to.
52 * @param {Session} session
53 * The WebDriverSession to register.
55 registerSession(session) {
57 throw new lazy.error.UnknownError(
58 "A WebDriver session has already been set"
62 this.session = session;
64 `Connection ${this.id} attached to session ${session.id}`
69 * Unregister the already set WebDriver session.
76 this.session.removeConnection(this);
81 * Send an error back to the WebDriver BiDi client.
84 * Id of the packet which lead to an error.
86 * Error object with `status`, `message` and `stack` attributes.
89 const webDriverError = lazy.error.wrap(err);
94 error: webDriverError.status,
95 message: webDriverError.message,
96 stacktrace: webDriverError.stack,
101 * Send an event coming from a module to the WebDriver BiDi client.
103 * @param {string} method
104 * The event name. This is composed by a module name, a dot character
105 * followed by the event name, e.g. `log.entryAdded`.
106 * @param {object} params
107 * A JSON-serializable object, which is the payload of this event.
109 sendEvent(method, params) {
110 this.send({ type: "event", method, params });
112 if (Services.profiler?.IsActive()) {
113 ChromeUtils.addProfilerMarker(
115 { category: "Remote-Protocol" },
122 * Send the result of a call to a module's method back to the
123 * WebDriver BiDi client.
126 * The request id being sent by the client to call the module's method.
127 * @param {object} result
128 * A JSON-serializable object, which is the actual result.
130 sendResult(id, result) {
131 result = typeof result !== "undefined" ? result : {};
132 this.send({ type: "success", id, result });
135 observe(subject, topic) {
137 case "quit-application-requested":
146 * Called by the `transport` when the connection is closed.
148 onConnectionClose() {
149 this.unregisterSession();
151 super.onConnectionClose();
155 * Receive a packet from the WebSocket layer.
157 * This packet is sent by a WebDriver BiDi client and is meant to execute
158 * a particular method on a given module.
160 * @param {object} packet
161 * JSON-serializable object sent by the client
163 async onPacket(packet) {
164 super.onPacket(packet);
166 const { id, method, params } = packet;
167 const startTime = Cu.now();
170 // First check for mandatory field in the command packet
171 lazy.assert.positiveInteger(id, "id: unsigned integer value expected");
172 lazy.assert.string(method, "method: string value expected");
173 lazy.assert.object(params, "params: object value expected");
175 // Extract the module and the command name out of `method` attribute
176 const { module, command } = splitMethod(method);
179 // Handle static commands first
180 if (module === "session" && command === "new") {
181 const processedCapabilities = lazy.processCapabilities(params);
183 result = await lazy.RemoteAgent.webDriverBiDi.createSession(
184 processedCapabilities,
188 // Since in Capabilities class we setup default values also for capabilities which are
189 // not relevant for bidi, we want to remove them from the payload before returning to a client.
190 result.capabilities = Array.from(result.capabilities.entries()).reduce(
191 (object, [key, value]) => {
192 if (!lazy.WEBDRIVER_CLASSIC_CAPABILITIES.includes(key)) {
200 } else if (module === "session" && command === "status") {
201 result = lazy.RemoteAgent.webDriverBiDi.getSessionReadinessStatus();
203 lazy.assert.session(this.session);
205 // Bug 1741854 - Workaround to deny internal methods to be called
206 if (command.startsWith("_")) {
207 throw new lazy.error.UnknownCommandError(method);
210 // Finally, instruct the session to execute the command
211 result = await this.session.execute(module, command, params);
214 this.sendResult(id, result);
217 if (module === "session" && command === "end") {
220 // Close the browser.
221 // TODO Bug 1842018. Refactor this part to return the response
222 // when the quitting of the browser is finished.
223 else if (module === "browser" && command === "close") {
224 // Register handler to run WebDriver BiDi specific shutdown code.
225 Services.obs.addObserver(this, "quit-application-requested");
227 // TODO Bug 1836282. Add as the third argument "moz:windowless" capability
228 // from the session, when this capability is supported by Webdriver BiDi.
229 await lazy.quit(["eForceQuit"], false);
231 Services.obs.removeObserver(this, "quit-application-requested");
234 this.sendError(id, e);
237 if (Services.profiler?.IsActive()) {
238 ChromeUtils.addProfilerMarker(
240 { startTime, category: "Remote-Protocol" },
248 * Splits a WebDriver BiDi method into module and command components.
250 * @param {string} method
251 * Name of the method to split, e.g. "session.subscribe".
253 * @returns {Object<string, string>}
254 * Object with the module ("session") and command ("subscribe")
257 export function splitMethod(method) {
258 const parts = method.split(".");
260 if (parts.length != 2 || !parts[0].length || !parts[1].length) {
261 throw new TypeError(`Invalid method format: '${method}'`);