282 lines
8.1 KiB
JavaScript
282 lines
8.1 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
cleanupCacheBypassState:
|
|
"chrome://remote/content/shared/NetworkCacheManager.sys.mjs",
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
RecommendedPreferences:
|
|
"chrome://remote/content/shared/RecommendedPreferences.sys.mjs",
|
|
WebDriverNewSessionHandler:
|
|
"chrome://remote/content/webdriver-bidi/NewSessionHandler.sys.mjs",
|
|
WebDriverSession: "chrome://remote/content/shared/webdriver/Session.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
|
|
lazy.Log.get(lazy.Log.TYPES.WEBDRIVER_BIDI)
|
|
);
|
|
ChromeUtils.defineLazyGetter(lazy, "textEncoder", () => new TextEncoder());
|
|
|
|
const RECOMMENDED_PREFS = new Map([
|
|
// Enables permission isolation by user context.
|
|
// It should be enabled by default in Nightly in the scope of the bug 1641584.
|
|
["permissions.isolateBy.userContext", true],
|
|
// Enables race-cache-with-network, which avoids issues with requests
|
|
// intercepted in the responseStarted phase. Without this preference, any
|
|
// subsequent request to the same URL as a suspended request hangs as well.
|
|
// Bug 1966494: should allow to unblock subsequent request, but might do so
|
|
// with a timer, slowing down tests. Should be reconsidered once fixed.
|
|
["network.http.rcwn.enabled", true],
|
|
]);
|
|
|
|
/**
|
|
* Entry class for the WebDriver BiDi support.
|
|
*
|
|
* @see https://w3c.github.io/webdriver-bidi
|
|
*/
|
|
export class WebDriverBiDi {
|
|
#agent;
|
|
#bidiServerPath;
|
|
#running;
|
|
#session;
|
|
#sessionlessConnections;
|
|
|
|
/**
|
|
* Creates a new instance of the WebDriverBiDi class.
|
|
*
|
|
* @param {RemoteAgent} agent
|
|
* Reference to the Remote Agent instance.
|
|
*/
|
|
constructor(agent) {
|
|
this.#agent = agent;
|
|
this.#running = false;
|
|
|
|
this.#bidiServerPath;
|
|
this.#session = null;
|
|
this.#sessionlessConnections = new Set();
|
|
}
|
|
|
|
get address() {
|
|
return `ws://${this.#agent.host}:${this.#agent.port}`;
|
|
}
|
|
|
|
get session() {
|
|
return this.#session;
|
|
}
|
|
|
|
#newSessionAlgorithm(session, flags) {
|
|
if (!this.#agent.running) {
|
|
// With the Remote Agent not running WebDriver BiDi is not supported.
|
|
return;
|
|
}
|
|
|
|
if (flags.has(lazy.WebDriverSession.SESSION_FLAG_BIDI)) {
|
|
// It's already a WebDriver BiDi session.
|
|
return;
|
|
}
|
|
|
|
const webSocketUrl = session.capabilities.get("webSocketUrl");
|
|
if (webSocketUrl === undefined) {
|
|
return;
|
|
}
|
|
|
|
// Start listening for BiDi connections.
|
|
this.#agent.server.registerPathHandler(session.path, session);
|
|
lazy.logger.debug(`Registered session handler: ${session.path}`);
|
|
|
|
session.capabilities.set("webSocketUrl", `${this.address}${session.path}`);
|
|
|
|
session.bidi = true;
|
|
flags.add("bidi");
|
|
}
|
|
|
|
/**
|
|
* Add a new connection that is not yet attached to a WebDriver session.
|
|
*
|
|
* @param {WebDriverBiDiConnection} connection
|
|
* The connection without an associated WebDriver session.
|
|
*/
|
|
addSessionlessConnection(connection) {
|
|
this.#sessionlessConnections.add(connection);
|
|
}
|
|
|
|
/**
|
|
* Create a new WebDriver session.
|
|
*
|
|
* @param {Record<string, *>=} capabilities
|
|
* JSON Object containing any of the recognised capabilities as listed
|
|
* on the `WebDriverSession` class.
|
|
* @param {Set} flags
|
|
* Session configuration flags.
|
|
* @param {WebDriverBiDiConnection=} sessionlessConnection
|
|
* Optional connection that is not yet associated with a WebDriver
|
|
* session, and has to be associated with the new WebDriver session.
|
|
*
|
|
* @returns {Record<string, Capabilities>}
|
|
* Object containing the current session ID, and all its capabilities.
|
|
*
|
|
* @throws {SessionNotCreatedError}
|
|
* If, for whatever reason, a session could not be created.
|
|
*/
|
|
async createSession(capabilities, flags, sessionlessConnection) {
|
|
if (this.#session) {
|
|
throw new lazy.error.SessionNotCreatedError(
|
|
"Maximum number of active sessions"
|
|
);
|
|
}
|
|
|
|
this.#session = new lazy.WebDriverSession(
|
|
capabilities,
|
|
flags,
|
|
sessionlessConnection
|
|
);
|
|
|
|
// Run new session steps for WebDriver BiDi.
|
|
this.#newSessionAlgorithm(this.#session, flags);
|
|
|
|
if (sessionlessConnection) {
|
|
// Connection is now registered with a WebDriver session
|
|
this.#sessionlessConnections.delete(sessionlessConnection);
|
|
}
|
|
|
|
if (this.#session.bidi) {
|
|
// Creating a WebDriver BiDi session too early can cause issues with
|
|
// clients in not being able to find any available browsing context.
|
|
// Also when closing the application while it's still starting up can
|
|
// cause shutdown hangs. As such WebDriver BiDi will return a new session
|
|
// once the initial application window has finished initializing.
|
|
lazy.logger.debug(`Waiting for initial application window`);
|
|
await this.#agent.browserStartupFinished;
|
|
}
|
|
|
|
return {
|
|
sessionId: this.#session.id,
|
|
capabilities: this.#session.capabilities,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delete the current WebDriver session.
|
|
*/
|
|
deleteSession() {
|
|
if (!this.#session) {
|
|
return;
|
|
}
|
|
|
|
// When the Remote Agent is listening, and a BiDi WebSocket is active,
|
|
// unregister the path handler for the session.
|
|
if (this.#agent.running && this.#session.capabilities.get("webSocketUrl")) {
|
|
this.#agent.server.registerPathHandler(this.#session.path, null);
|
|
lazy.logger.debug(`Unregistered session handler: ${this.#session.path}`);
|
|
}
|
|
|
|
// For multiple session check first if the last session was closed.
|
|
lazy.cleanupCacheBypassState();
|
|
|
|
this.#session.destroy();
|
|
this.#session = null;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the readiness state of the remote end, regarding the creation of
|
|
* new WebDriverBiDi sessions.
|
|
*
|
|
* See https://w3c.github.io/webdriver-bidi/#command-session-status
|
|
*
|
|
* @returns {object}
|
|
* The readiness state.
|
|
*/
|
|
getSessionReadinessStatus() {
|
|
if (this.#session) {
|
|
// We currently only support one session, see Bug 1720707.
|
|
return {
|
|
ready: false,
|
|
message: "Session already started",
|
|
};
|
|
}
|
|
|
|
return {
|
|
ready: true,
|
|
message: "",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Starts the WebDriver BiDi support.
|
|
*/
|
|
async start() {
|
|
if (this.#running) {
|
|
return;
|
|
}
|
|
|
|
this.#running = true;
|
|
|
|
lazy.RecommendedPreferences.applyPreferences(RECOMMENDED_PREFS);
|
|
|
|
// Install a HTTP handler for direct WebDriver BiDi connection requests.
|
|
this.#agent.server.registerPathHandler(
|
|
"/session",
|
|
new lazy.WebDriverNewSessionHandler(this)
|
|
);
|
|
|
|
Cu.printStderr(`WebDriver BiDi listening on ${this.address}\n`);
|
|
|
|
try {
|
|
// Write WebSocket connection details to the WebDriverBiDiServer.json file
|
|
// located within the application's profile.
|
|
this.#bidiServerPath = PathUtils.join(
|
|
PathUtils.profileDir,
|
|
"WebDriverBiDiServer.json"
|
|
);
|
|
|
|
const data = {
|
|
ws_host: this.#agent.host,
|
|
ws_port: this.#agent.port,
|
|
};
|
|
|
|
await IOUtils.write(
|
|
this.#bidiServerPath,
|
|
lazy.textEncoder.encode(JSON.stringify(data, undefined, " "))
|
|
);
|
|
} catch (e) {
|
|
lazy.logger.warn(
|
|
`Failed to create ${this.#bidiServerPath} (${e.message})`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stops the WebDriver BiDi support.
|
|
*/
|
|
async stop() {
|
|
if (!this.#running) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await IOUtils.remove(this.#bidiServerPath);
|
|
} catch (e) {
|
|
lazy.logger.warn(
|
|
`Failed to remove ${this.#bidiServerPath} (${e.message})`
|
|
);
|
|
}
|
|
|
|
try {
|
|
// Close open session
|
|
this.deleteSession();
|
|
this.#agent.server.registerPathHandler("/session", null);
|
|
|
|
// Close all open session-less connections
|
|
this.#sessionlessConnections.forEach(connection => connection.close());
|
|
this.#sessionlessConnections.clear();
|
|
} catch (e) {
|
|
lazy.logger.error("Failed to stop protocol", e);
|
|
} finally {
|
|
this.#running = false;
|
|
}
|
|
}
|
|
}
|