A big commit with a bunch of node modules so I could run puppeteer for Walmart. Added some todos and Headway's templates.
This commit is contained in:
416
Scripts/node_modules/chromium-bidi/lib/cjs/bidiServer/WebSocketServer.js
generated
vendored
416
Scripts/node_modules/chromium-bidi/lib/cjs/bidiServer/WebSocketServer.js
generated
vendored
@ -53,205 +53,212 @@ const debugInternal = (0, debug_1.default)('bidi:server:internal');
|
||||
const debugSend = (0, debug_1.default)('bidi:server:SEND ▸');
|
||||
const debugRecv = (0, debug_1.default)('bidi:server:RECV ◂');
|
||||
class WebSocketServer {
|
||||
static #sessions = new Map();
|
||||
/**
|
||||
* @param bidiPort Port to start ws server on.
|
||||
* @param channel
|
||||
* @param headless
|
||||
* @param verbose
|
||||
*/
|
||||
static run(bidiPort, channel, headless, verbose) {
|
||||
const server = http_1.default.createServer(async (request, response) => {
|
||||
debugInternal(`${new Date().toString()} Received HTTP ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)}`);
|
||||
if (!request.url) {
|
||||
return response.end(404);
|
||||
}
|
||||
// https://w3c.github.io/webdriver-bidi/#transport, step 2.
|
||||
if (request.url === '/session') {
|
||||
const body = [];
|
||||
request
|
||||
.on('data', (chunk) => {
|
||||
body.push(chunk);
|
||||
})
|
||||
.on('end', () => {
|
||||
const jsonBody = JSON.parse(Buffer.concat(body).toString());
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
const sessionId = (0, uuid_js_1.uuidv4)();
|
||||
const session = {
|
||||
sessionId,
|
||||
// TODO: launch browser instance and set it to the session after WPT
|
||||
// tests clean up is switched to pure BiDi.
|
||||
browserInstancePromise: undefined,
|
||||
sessionOptions: {
|
||||
chromeOptions: this.#getChromeOptions(jsonBody.capabilities, channel, headless),
|
||||
mapperOptions: this.#getMapperOptions(jsonBody.capabilities),
|
||||
verbose,
|
||||
},
|
||||
};
|
||||
this.#sessions.set(sessionId, session);
|
||||
const webSocketUrl = `ws://localhost:${bidiPort}/session/${sessionId}`;
|
||||
debugInternal(`Session created. WebSocket URL: ${JSON.stringify(webSocketUrl)}.`);
|
||||
response.write(JSON.stringify({
|
||||
value: {
|
||||
sessionId,
|
||||
capabilities: {
|
||||
webSocketUrl,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return response.end();
|
||||
});
|
||||
return;
|
||||
}
|
||||
else if (request.url.startsWith('/session')) {
|
||||
debugInternal(`Unknown session command ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url} with payload ${await WebSocketServer.#getHttpRequestPayload(request)}. 200 returned.`);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
response.write(JSON.stringify({
|
||||
value: {},
|
||||
}));
|
||||
}
|
||||
else {
|
||||
debugInternal(`Unknown ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)} with payload ${JSON.stringify(await WebSocketServer.#getHttpRequestPayload(request))}. 404 returned.`);
|
||||
response.writeHead(404);
|
||||
}
|
||||
return response.end();
|
||||
});
|
||||
server.listen(bidiPort, () => {
|
||||
(0, exports.debugInfo)('BiDi server is listening on port', bidiPort);
|
||||
});
|
||||
const wsServer = new websocket.server({
|
||||
httpServer: server,
|
||||
#sessions = new Map();
|
||||
#port;
|
||||
#channel;
|
||||
#headless;
|
||||
#verbose;
|
||||
#server;
|
||||
#wsServer;
|
||||
constructor(port, channel, headless, verbose) {
|
||||
this.#port = port;
|
||||
this.#channel = channel;
|
||||
this.#headless = headless;
|
||||
this.#verbose = verbose;
|
||||
this.#server = http_1.default.createServer(this.#onRequest.bind(this));
|
||||
this.#wsServer = new websocket.server({
|
||||
httpServer: this.#server,
|
||||
autoAcceptConnections: false,
|
||||
});
|
||||
wsServer.on('request', (request) => {
|
||||
// Session is set either by Classic or BiDi commands.
|
||||
let session;
|
||||
const requestSessionId = (request.resource ?? '').split('/').pop();
|
||||
debugInternal(`new WS request received. Path: ${JSON.stringify(request.resourceURL.path)}, sessionId: ${JSON.stringify(requestSessionId)}`);
|
||||
if (requestSessionId !== '' &&
|
||||
requestSessionId !== undefined &&
|
||||
!this.#sessions.has(requestSessionId)) {
|
||||
debugInternal('Unknown session id:', requestSessionId);
|
||||
request.reject();
|
||||
return;
|
||||
}
|
||||
const connection = request.accept();
|
||||
session = this.#sessions.get(requestSessionId ?? '');
|
||||
if (session !== undefined) {
|
||||
// BrowserInstance is created for each new WS connection, even for the
|
||||
// same SessionId. This is because WPT uses a single session for all the
|
||||
// tests, but cleans up tests using WebDriver Classic commands, which is
|
||||
// not implemented in this Mapper runner.
|
||||
// TODO: connect to an existing BrowserInstance instead.
|
||||
const sessionOptions = session.sessionOptions;
|
||||
session.browserInstancePromise = this.#closeBrowserInstanceIfLaunched(session)
|
||||
.then(async () => await this.#launchBrowserInstance(connection, sessionOptions))
|
||||
.catch((e) => {
|
||||
(0, exports.debugInfo)('Error while creating session', e);
|
||||
connection.close(500, 'cannot create browser instance');
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
connection.on('message', async (message) => {
|
||||
// If type is not text, return error.
|
||||
if (message.type !== 'utf8') {
|
||||
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `not supported type (${message.type})`);
|
||||
return;
|
||||
}
|
||||
const plainCommandData = message.utf8Data;
|
||||
if (debugRecv.enabled) {
|
||||
try {
|
||||
debugRecv(JSON.parse(plainCommandData));
|
||||
}
|
||||
catch {
|
||||
debugRecv(plainCommandData);
|
||||
}
|
||||
}
|
||||
// Try to parse the message to handle some of BiDi commands.
|
||||
let parsedCommandData;
|
||||
try {
|
||||
parsedCommandData = JSON.parse(plainCommandData);
|
||||
}
|
||||
catch (e) {
|
||||
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `Cannot parse data as JSON`);
|
||||
return;
|
||||
}
|
||||
// Handle creating new session.
|
||||
if (parsedCommandData.method === 'session.new') {
|
||||
if (session !== undefined) {
|
||||
(0, exports.debugInfo)('WS connection already have an associated session.');
|
||||
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection already have an associated session.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sessionOptions = {
|
||||
chromeOptions: this.#getChromeOptions(parsedCommandData.params?.capabilities, channel, headless),
|
||||
mapperOptions: this.#getMapperOptions(parsedCommandData.params?.capabilities),
|
||||
verbose,
|
||||
};
|
||||
const browserInstance = await this.#launchBrowserInstance(connection, sessionOptions);
|
||||
const sessionId = (0, uuid_js_1.uuidv4)();
|
||||
session = {
|
||||
sessionId,
|
||||
browserInstancePromise: Promise.resolve(browserInstance),
|
||||
sessionOptions,
|
||||
};
|
||||
this.#sessions.set(sessionId, session);
|
||||
}
|
||||
catch (e) {
|
||||
(0, exports.debugInfo)('Error while creating session', e);
|
||||
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, e?.message ?? 'Unknown error');
|
||||
return;
|
||||
}
|
||||
// TODO: extend with capabilities.
|
||||
this.#sendClientMessage({
|
||||
id: parsedCommandData.id,
|
||||
type: 'success',
|
||||
result: {
|
||||
sessionId: session.sessionId,
|
||||
capabilities: {},
|
||||
},
|
||||
}, connection);
|
||||
return;
|
||||
}
|
||||
if (session === undefined) {
|
||||
(0, exports.debugInfo)('Session is not yet initialized.');
|
||||
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Session is not yet initialized.');
|
||||
return;
|
||||
}
|
||||
if (session.browserInstancePromise === undefined) {
|
||||
(0, exports.debugInfo)('Browser instance is not launched.');
|
||||
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Browser instance is not launched.');
|
||||
return;
|
||||
}
|
||||
const browserInstance = await session.browserInstancePromise;
|
||||
// Handle `browser.close` command.
|
||||
if (parsedCommandData.method === 'browser.close') {
|
||||
await browserInstance.close();
|
||||
this.#sendClientMessage({
|
||||
id: parsedCommandData.id,
|
||||
type: 'success',
|
||||
result: {},
|
||||
}, connection);
|
||||
return;
|
||||
}
|
||||
// Forward all other commands to BiDi Mapper.
|
||||
await browserInstance.bidiSession().sendCommand(plainCommandData);
|
||||
});
|
||||
connection.on('close', async () => {
|
||||
debugInternal(`${new Date().toString()} Peer ${connection.remoteAddress} disconnected.`);
|
||||
// TODO: don't close Browser instance to allow re-connecting to the session.
|
||||
await this.#closeBrowserInstanceIfLaunched(session);
|
||||
});
|
||||
this.#wsServer.on('request', this.#onWsRequest.bind(this));
|
||||
this.#server.listen(this.#port, () => {
|
||||
(0, exports.debugInfo)('BiDi server is listening on port', this.#port);
|
||||
});
|
||||
}
|
||||
static async #closeBrowserInstanceIfLaunched(session) {
|
||||
async #onRequest(request, response) {
|
||||
debugInternal(`Received HTTP ${JSON.stringify(request.method)} request for ${JSON.stringify(request.url)}`);
|
||||
if (!request.url) {
|
||||
return response.end(404);
|
||||
}
|
||||
// https://w3c.github.io/webdriver-bidi/#transport, step 2.
|
||||
if (request.url === '/session') {
|
||||
const body = await new Promise((resolve, reject) => {
|
||||
const bodyArray = [];
|
||||
request.on('data', (chunk) => {
|
||||
bodyArray.push(chunk);
|
||||
});
|
||||
request.on('error', reject);
|
||||
request.on('end', () => {
|
||||
resolve(Buffer.concat(bodyArray));
|
||||
});
|
||||
});
|
||||
// https://w3c.github.io/webdriver-bidi/#transport, step 3.
|
||||
const jsonBody = JSON.parse(body.toString());
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
const sessionId = (0, uuid_js_1.uuidv4)();
|
||||
const session = {
|
||||
sessionId,
|
||||
// TODO: launch browser instance and set it to the session after WPT
|
||||
// tests clean up is switched to pure BiDi.
|
||||
browserInstancePromise: undefined,
|
||||
sessionOptions: {
|
||||
chromeOptions: this.#getChromeOptions(jsonBody.capabilities, this.#channel, this.#headless),
|
||||
mapperOptions: this.#getMapperOptions(jsonBody.capabilities),
|
||||
verbose: this.#verbose,
|
||||
},
|
||||
};
|
||||
this.#sessions.set(sessionId, session);
|
||||
const webSocketUrl = `ws://localhost:${this.#port}/session/${sessionId}`;
|
||||
debugInternal(`Session created. WebSocket URL: ${JSON.stringify(webSocketUrl)}.`);
|
||||
response.write(JSON.stringify({
|
||||
value: {
|
||||
sessionId,
|
||||
capabilities: {
|
||||
webSocketUrl,
|
||||
},
|
||||
},
|
||||
}));
|
||||
return response.end();
|
||||
}
|
||||
else if (request.url.startsWith('/session')) {
|
||||
debugInternal(`Unknown session command ${request.method ?? 'UNKNOWN METHOD'} request for ${request.url} with payload ${await this.#getHttpRequestPayload(request)}. 200 returned.`);
|
||||
response.writeHead(200, {
|
||||
'Content-Type': 'application/json;charset=utf-8',
|
||||
'Cache-Control': 'no-cache',
|
||||
});
|
||||
response.write(JSON.stringify({
|
||||
value: {},
|
||||
}));
|
||||
return response.end();
|
||||
}
|
||||
debugInternal(`Unknown ${request.method} request for ${JSON.stringify(request.url)} with payload ${await this.#getHttpRequestPayload(request)}. 404 returned.`);
|
||||
return response.end(404);
|
||||
}
|
||||
#onWsRequest(request) {
|
||||
// Session is set either by Classic or BiDi commands.
|
||||
let session;
|
||||
const requestSessionId = (request.resource ?? '').split('/').pop();
|
||||
debugInternal(`new WS request received. Path: ${JSON.stringify(request.resourceURL.path)}, sessionId: ${JSON.stringify(requestSessionId)}`);
|
||||
if (requestSessionId !== '' &&
|
||||
requestSessionId !== undefined &&
|
||||
!this.#sessions.has(requestSessionId)) {
|
||||
debugInternal('Unknown session id:', requestSessionId);
|
||||
request.reject();
|
||||
return;
|
||||
}
|
||||
const connection = request.accept();
|
||||
session = this.#sessions.get(requestSessionId ?? '');
|
||||
if (session !== undefined) {
|
||||
// BrowserInstance is created for each new WS connection, even for the
|
||||
// same SessionId. This is because WPT uses a single session for all the
|
||||
// tests, but cleans up tests using WebDriver Classic commands, which is
|
||||
// not implemented in this Mapper runner.
|
||||
// TODO: connect to an existing BrowserInstance instead.
|
||||
const sessionOptions = session.sessionOptions;
|
||||
session.browserInstancePromise = this.#closeBrowserInstanceIfLaunched(session)
|
||||
.then(async () => await this.#launchBrowserInstance(connection, sessionOptions))
|
||||
.catch((e) => {
|
||||
(0, exports.debugInfo)('Error while creating session', e);
|
||||
connection.close(500, 'cannot create browser instance');
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
connection.on('message', async (message) => {
|
||||
// If type is not text, return error.
|
||||
if (message.type !== 'utf8') {
|
||||
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `not supported type (${message.type})`);
|
||||
return;
|
||||
}
|
||||
const plainCommandData = message.utf8Data;
|
||||
if (debugRecv.enabled) {
|
||||
try {
|
||||
debugRecv(JSON.parse(plainCommandData));
|
||||
}
|
||||
catch {
|
||||
debugRecv(plainCommandData);
|
||||
}
|
||||
}
|
||||
// Try to parse the message to handle some of BiDi commands.
|
||||
let parsedCommandData;
|
||||
try {
|
||||
parsedCommandData = JSON.parse(plainCommandData);
|
||||
}
|
||||
catch (e) {
|
||||
this.#respondWithError(connection, {}, "invalid argument" /* ErrorCode.InvalidArgument */, `Cannot parse data as JSON`);
|
||||
return;
|
||||
}
|
||||
// Handle creating new session.
|
||||
if (parsedCommandData.method === 'session.new') {
|
||||
if (session !== undefined) {
|
||||
(0, exports.debugInfo)('WS connection already have an associated session.');
|
||||
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, 'WS connection already have an associated session.');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const sessionOptions = {
|
||||
chromeOptions: this.#getChromeOptions(parsedCommandData.params?.capabilities, this.#channel, this.#headless),
|
||||
mapperOptions: this.#getMapperOptions(parsedCommandData.params?.capabilities),
|
||||
verbose: this.#verbose,
|
||||
};
|
||||
const browserInstance = await this.#launchBrowserInstance(connection, sessionOptions);
|
||||
const sessionId = (0, uuid_js_1.uuidv4)();
|
||||
session = {
|
||||
sessionId,
|
||||
browserInstancePromise: Promise.resolve(browserInstance),
|
||||
sessionOptions,
|
||||
};
|
||||
this.#sessions.set(sessionId, session);
|
||||
}
|
||||
catch (e) {
|
||||
(0, exports.debugInfo)('Error while creating session', e);
|
||||
this.#respondWithError(connection, plainCommandData, "session not created" /* ErrorCode.SessionNotCreated */, e?.message ?? 'Unknown error');
|
||||
return;
|
||||
}
|
||||
// TODO: extend with capabilities.
|
||||
this.#sendClientMessage({
|
||||
id: parsedCommandData.id,
|
||||
type: 'success',
|
||||
result: {
|
||||
sessionId: session.sessionId,
|
||||
capabilities: {},
|
||||
},
|
||||
}, connection);
|
||||
return;
|
||||
}
|
||||
if (session === undefined) {
|
||||
(0, exports.debugInfo)('Session is not yet initialized.');
|
||||
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Session is not yet initialized.');
|
||||
return;
|
||||
}
|
||||
if (session.browserInstancePromise === undefined) {
|
||||
(0, exports.debugInfo)('Browser instance is not launched.');
|
||||
this.#respondWithError(connection, plainCommandData, "invalid session id" /* ErrorCode.InvalidSessionId */, 'Browser instance is not launched.');
|
||||
return;
|
||||
}
|
||||
const browserInstance = await session.browserInstancePromise;
|
||||
// Handle `browser.close` command.
|
||||
if (parsedCommandData.method === 'browser.close') {
|
||||
await browserInstance.close();
|
||||
this.#sendClientMessage({
|
||||
id: parsedCommandData.id,
|
||||
type: 'success',
|
||||
result: {},
|
||||
}, connection);
|
||||
return;
|
||||
}
|
||||
// Forward all other commands to BiDi Mapper.
|
||||
await browserInstance.bidiSession().sendCommand(plainCommandData);
|
||||
});
|
||||
connection.on('close', async () => {
|
||||
debugInternal(`Peer ${connection.remoteAddress} disconnected.`);
|
||||
// TODO: don't close Browser instance to allow re-connecting to the session.
|
||||
await this.#closeBrowserInstanceIfLaunched(session);
|
||||
});
|
||||
}
|
||||
async #closeBrowserInstanceIfLaunched(session) {
|
||||
if (session === undefined || session.browserInstancePromise === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -259,11 +266,12 @@ class WebSocketServer {
|
||||
session.browserInstancePromise = undefined;
|
||||
void browserInstance.close();
|
||||
}
|
||||
static #getMapperOptions(capabilities) {
|
||||
#getMapperOptions(capabilities) {
|
||||
const acceptInsecureCerts = capabilities?.alwaysMatch?.acceptInsecureCerts ?? false;
|
||||
return { acceptInsecureCerts };
|
||||
const sharedIdWithFrame = capabilities?.alwaysMatch?.sharedIdWithFrame ?? false;
|
||||
return { acceptInsecureCerts, sharedIdWithFrame };
|
||||
}
|
||||
static #getChromeOptions(capabilities, channel, headless) {
|
||||
#getChromeOptions(capabilities, channel, headless) {
|
||||
const chromeCapabilities = capabilities?.alwaysMatch?.['goog:chromeOptions'];
|
||||
return {
|
||||
chromeArgs: chromeCapabilities?.args ?? [],
|
||||
@ -272,7 +280,7 @@ class WebSocketServer {
|
||||
chromeBinary: chromeCapabilities?.binary ?? undefined,
|
||||
};
|
||||
}
|
||||
static async #launchBrowserInstance(connection, sessionOptions) {
|
||||
async #launchBrowserInstance(connection, sessionOptions) {
|
||||
(0, exports.debugInfo)('Scheduling browser launch...');
|
||||
const browserInstance = await BrowserInstance_js_1.BrowserInstance.run(sessionOptions.chromeOptions, sessionOptions.mapperOptions, sessionOptions.verbose);
|
||||
// Forward messages from BiDi Mapper to the client unconditionally.
|
||||
@ -282,7 +290,7 @@ class WebSocketServer {
|
||||
(0, exports.debugInfo)('Browser is launched!');
|
||||
return browserInstance;
|
||||
}
|
||||
static #sendClientMessageString(message, connection) {
|
||||
#sendClientMessageString(message, connection) {
|
||||
if (debugSend.enabled) {
|
||||
try {
|
||||
debugSend(JSON.parse(message));
|
||||
@ -293,15 +301,15 @@ class WebSocketServer {
|
||||
}
|
||||
connection.sendUTF(message);
|
||||
}
|
||||
static #sendClientMessage(object, connection) {
|
||||
#sendClientMessage(object, connection) {
|
||||
const json = JSON.stringify(object);
|
||||
return this.#sendClientMessageString(json, connection);
|
||||
}
|
||||
static #respondWithError(connection, plainCommandData, errorCode, errorMessage) {
|
||||
#respondWithError(connection, plainCommandData, errorCode, errorMessage) {
|
||||
const errorResponse = this.#getErrorResponse(plainCommandData, errorCode, errorMessage);
|
||||
void this.#sendClientMessage(errorResponse, connection);
|
||||
}
|
||||
static #getErrorResponse(plainCommandData, errorCode, errorMessage) {
|
||||
#getErrorResponse(plainCommandData, errorCode, errorMessage) {
|
||||
// XXX: this is bizarre per spec. We reparse the payload and
|
||||
// extract the ID, regardless of what kind of value it was.
|
||||
let commandId;
|
||||
@ -320,7 +328,7 @@ class WebSocketServer {
|
||||
// XXX: optional stacktrace field.
|
||||
};
|
||||
}
|
||||
static #getHttpRequestPayload(request) {
|
||||
#getHttpRequestPayload(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
request.on('data', (chunk) => {
|
||||
|
||||
Reference in New Issue
Block a user