chore: extract ws server util (#29247)
This commit is contained in:
parent
aeafd44726
commit
aff6cf3c83
|
|
@ -17,7 +17,7 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import type * as channels from '@protocol/channels';
|
||||
import { maybeFindValidator, ValidationError, type ValidatorContext } from '../protocol/validator';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { ExpectZone } from '../utils/stackTrace';
|
||||
import { captureRawStack, captureLibraryStackTrace, stringifyStackFrames } from '../utils/stackTrace';
|
||||
import { isUnderTest } from '../utils';
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { Electron, ElectronApplication } from './electron';
|
|||
import type * as channels from '@protocol/channels';
|
||||
import { Stream } from './stream';
|
||||
import { WritableStream } from './writableStream';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { SelectorsOwner } from './selectors';
|
||||
import { Android, AndroidSocket, AndroidDevice } from './android';
|
||||
import { Artifact } from './artifact';
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { BrowserContext } from './browserContext';
|
||||
import type { LocalUtils } from './localUtils';
|
||||
import type { Route } from './network';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
import EventEmitter from 'events';
|
||||
import type { AddressInfo } from 'net';
|
||||
import net from 'net';
|
||||
import { debugLogger } from './debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { createSocket } from '../utils/happy-eyeballs';
|
||||
import { assert, createGuid, } from '../utils';
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import { AndroidDevice } from '../server/android/android';
|
|||
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
|
||||
import { startProfiling, stopProfiling } from '../utils';
|
||||
import { monotonicTime } from '../utils';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
|
||||
export type ClientType = 'controller' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser-or-android';
|
||||
|
||||
|
|
|
|||
|
|
@ -14,24 +14,18 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { wsServer } from '../utilsBundle';
|
||||
import type { WebSocketServer } from '../utilsBundle';
|
||||
import type http from 'http';
|
||||
import type { Browser } from '../server/browser';
|
||||
import type { Playwright } from '../server/playwright';
|
||||
import { createPlaywright } from '../server/playwright';
|
||||
import { PlaywrightConnection } from './playwrightConnection';
|
||||
import type { ClientType } from './playwrightConnection';
|
||||
import type { LaunchOptions } from '../server/types';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { Semaphore } from '../utils/semaphore';
|
||||
import type { AndroidDevice } from '../server/android/android';
|
||||
import type { SocksProxy } from '../common/socksProxy';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { createHttpServer, userAgentVersionMatchesErrorMessage } from '../utils';
|
||||
import { perMessageDeflate } from '../server/transport';
|
||||
|
||||
let lastConnectionId = 0;
|
||||
const kConnectionSymbol = Symbol('kConnection');
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import { userAgentVersionMatchesErrorMessage } from '../utils';
|
||||
import { WSServer } from '../utils/wsServer';
|
||||
|
||||
type ServerOptions = {
|
||||
path: string;
|
||||
|
|
@ -44,9 +38,8 @@ type ServerOptions = {
|
|||
|
||||
export class PlaywrightServer {
|
||||
private _preLaunchedPlaywright: Playwright | undefined;
|
||||
private _wsServer: WebSocketServer | undefined;
|
||||
private _server: http.Server | undefined;
|
||||
private _options: ServerOptions;
|
||||
private _wsServer: WSServer;
|
||||
|
||||
constructor(options: ServerOptions) {
|
||||
this._options = options;
|
||||
|
|
@ -54,183 +47,85 @@ export class PlaywrightServer {
|
|||
this._preLaunchedPlaywright = options.preLaunchedBrowser.attribution.playwright;
|
||||
if (options.preLaunchedAndroidDevice)
|
||||
this._preLaunchedPlaywright = options.preLaunchedAndroidDevice._android.attribution.playwright;
|
||||
}
|
||||
|
||||
async listen(port: number = 0, hostname?: string): Promise<string> {
|
||||
debugLogger.log('server', `Server started at ${new Date()}`);
|
||||
|
||||
const server = createHttpServer((request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (request.method === 'GET' && request.url === '/json') {
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
response.end(JSON.stringify({
|
||||
wsEndpointPath: this._options.path,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
response.end('Running');
|
||||
});
|
||||
server.on('error', error => debugLogger.log('server', String(error)));
|
||||
this._server = server;
|
||||
|
||||
const wsEndpoint = await new Promise<string>((resolve, reject) => {
|
||||
server.listen(port, hostname, () => {
|
||||
const address = server.address();
|
||||
if (!address) {
|
||||
reject(new Error('Could not bind server socket'));
|
||||
return;
|
||||
}
|
||||
const wsEndpoint = typeof address === 'string' ? `${address}${this._options.path}` : `ws://${hostname || 'localhost'}:${address.port}${this._options.path}`;
|
||||
resolve(wsEndpoint);
|
||||
}).on('error', reject);
|
||||
});
|
||||
|
||||
debugLogger.log('server', 'Listening at ' + wsEndpoint);
|
||||
this._wsServer = new wsServer({
|
||||
noServer: true,
|
||||
perMessageDeflate,
|
||||
});
|
||||
const browserSemaphore = new Semaphore(this._options.maxConnections);
|
||||
const controllerSemaphore = new Semaphore(1);
|
||||
const reuseBrowserSemaphore = new Semaphore(1);
|
||||
if (process.env.PWTEST_SERVER_WS_HEADERS) {
|
||||
this._wsServer.on('headers', (headers, request) => {
|
||||
headers.push(process.env.PWTEST_SERVER_WS_HEADERS!);
|
||||
});
|
||||
}
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const pathname = new URL('http://localhost' + request.url!).pathname;
|
||||
if (pathname !== this._options.path) {
|
||||
socket.write(`HTTP/${request.httpVersion} 400 Bad Request\r\n\r\n`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
|
||||
if (uaError) {
|
||||
socket.write(`HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
this._wsServer = new WSServer({
|
||||
onUpgrade: (request, socket) => {
|
||||
const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
|
||||
if (uaError)
|
||||
return { error: `HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}` };
|
||||
},
|
||||
|
||||
this._wsServer?.handleUpgrade(request, socket, head, ws => this._wsServer?.emit('connection', ws, request));
|
||||
onHeaders: headers => {
|
||||
if (process.env.PWTEST_SERVER_WS_HEADERS)
|
||||
headers.push(process.env.PWTEST_SERVER_WS_HEADERS!);
|
||||
},
|
||||
|
||||
onConnection: (request, url, ws, id) => {
|
||||
const browserHeader = request.headers['x-playwright-browser'];
|
||||
const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
|
||||
const proxyHeader = request.headers['x-playwright-proxy'];
|
||||
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
|
||||
|
||||
const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
|
||||
const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader;
|
||||
const launchOptionsParam = url.searchParams.get('launch-options');
|
||||
let launchOptions: LaunchOptions = {};
|
||||
try {
|
||||
launchOptions = JSON.parse(launchOptionsParam || launchOptionsHeaderValue);
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
// Instantiate playwright for the extension modes.
|
||||
const isExtension = this._options.mode === 'extension';
|
||||
if (isExtension) {
|
||||
if (!this._preLaunchedPlaywright)
|
||||
this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
|
||||
}
|
||||
|
||||
let clientType: ClientType = 'launch-browser';
|
||||
let semaphore: Semaphore = browserSemaphore;
|
||||
if (isExtension && url.searchParams.has('debug-controller')) {
|
||||
clientType = 'controller';
|
||||
semaphore = controllerSemaphore;
|
||||
} else if (isExtension) {
|
||||
clientType = 'reuse-browser';
|
||||
semaphore = reuseBrowserSemaphore;
|
||||
} else if (this._options.mode === 'launchServer') {
|
||||
clientType = 'pre-launched-browser-or-android';
|
||||
semaphore = browserSemaphore;
|
||||
}
|
||||
|
||||
return new PlaywrightConnection(
|
||||
semaphore.acquire(),
|
||||
clientType, ws,
|
||||
{ socksProxyPattern: proxyValue, browserName, launchOptions },
|
||||
{
|
||||
playwright: this._preLaunchedPlaywright,
|
||||
browser: this._options.preLaunchedBrowser,
|
||||
androidDevice: this._options.preLaunchedAndroidDevice,
|
||||
socksProxy: this._options.preLaunchedSocksProxy,
|
||||
},
|
||||
id, () => semaphore.release());
|
||||
},
|
||||
|
||||
onClose: async () => {
|
||||
debugLogger.log('server', 'closing browsers');
|
||||
if (this._preLaunchedPlaywright)
|
||||
await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' })));
|
||||
debugLogger.log('server', 'closed browsers');
|
||||
}
|
||||
});
|
||||
this._wsServer.on('connection', (ws, request) => {
|
||||
debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions);
|
||||
const url = new URL('http://localhost' + (request.url || ''));
|
||||
const browserHeader = request.headers['x-playwright-browser'];
|
||||
const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
|
||||
const proxyHeader = request.headers['x-playwright-proxy'];
|
||||
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
|
||||
}
|
||||
|
||||
const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
|
||||
const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader;
|
||||
const launchOptionsParam = url.searchParams.get('launch-options');
|
||||
let launchOptions: LaunchOptions = {};
|
||||
try {
|
||||
launchOptions = JSON.parse(launchOptionsParam || launchOptionsHeaderValue);
|
||||
} catch (e) {
|
||||
}
|
||||
|
||||
const id = String(++lastConnectionId);
|
||||
debugLogger.log('server', `[${id}] serving connection: ${request.url}`);
|
||||
|
||||
// Instantiate playwright for the extension modes.
|
||||
const isExtension = this._options.mode === 'extension';
|
||||
if (isExtension) {
|
||||
if (!this._preLaunchedPlaywright)
|
||||
this._preLaunchedPlaywright = createPlaywright({ sdkLanguage: 'javascript', isServer: true });
|
||||
}
|
||||
|
||||
let clientType: ClientType = 'launch-browser';
|
||||
let semaphore: Semaphore = browserSemaphore;
|
||||
if (isExtension && url.searchParams.has('debug-controller')) {
|
||||
clientType = 'controller';
|
||||
semaphore = controllerSemaphore;
|
||||
} else if (isExtension) {
|
||||
clientType = 'reuse-browser';
|
||||
semaphore = reuseBrowserSemaphore;
|
||||
} else if (this._options.mode === 'launchServer') {
|
||||
clientType = 'pre-launched-browser-or-android';
|
||||
semaphore = browserSemaphore;
|
||||
}
|
||||
|
||||
const connection = new PlaywrightConnection(
|
||||
semaphore.acquire(),
|
||||
clientType, ws,
|
||||
{ socksProxyPattern: proxyValue, browserName, launchOptions },
|
||||
{
|
||||
playwright: this._preLaunchedPlaywright,
|
||||
browser: this._options.preLaunchedBrowser,
|
||||
androidDevice: this._options.preLaunchedAndroidDevice,
|
||||
socksProxy: this._options.preLaunchedSocksProxy,
|
||||
},
|
||||
id, () => semaphore.release());
|
||||
(ws as any)[kConnectionSymbol] = connection;
|
||||
});
|
||||
|
||||
return wsEndpoint;
|
||||
async listen(port: number = 0, hostname?: string): Promise<string> {
|
||||
return this._wsServer.listen(port, hostname, this._options.path);
|
||||
}
|
||||
|
||||
async close() {
|
||||
const server = this._wsServer;
|
||||
if (!server)
|
||||
return;
|
||||
debugLogger.log('server', 'closing websocket server');
|
||||
const waitForClose = new Promise(f => server.close(f));
|
||||
// First disconnect all remaining clients.
|
||||
await Promise.all(Array.from(server.clients).map(async ws => {
|
||||
const connection = (ws as any)[kConnectionSymbol] as PlaywrightConnection | undefined;
|
||||
if (connection)
|
||||
await connection.close();
|
||||
try {
|
||||
ws.terminate();
|
||||
} catch (e) {
|
||||
}
|
||||
}));
|
||||
await waitForClose;
|
||||
debugLogger.log('server', 'closing http server');
|
||||
if (this._server)
|
||||
await new Promise(f => this._server!.close(f));
|
||||
this._wsServer = undefined;
|
||||
this._server = undefined;
|
||||
debugLogger.log('server', 'closed server');
|
||||
|
||||
debugLogger.log('server', 'closing browsers');
|
||||
if (this._preLaunchedPlaywright)
|
||||
await Promise.all(this._preLaunchedPlaywright.allBrowsers().map(browser => browser.close({ reason: 'Playwright Server stopped' })));
|
||||
debugLogger.log('server', 'closed browsers');
|
||||
}
|
||||
}
|
||||
|
||||
export class Semaphore {
|
||||
private _max: number;
|
||||
private _acquired = 0;
|
||||
private _queue: ManualPromise[] = [];
|
||||
|
||||
constructor(max: number) {
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
setMax(max: number) {
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
acquire(): Promise<void> {
|
||||
const lock = new ManualPromise();
|
||||
this._queue.push(lock);
|
||||
this._flush();
|
||||
return lock;
|
||||
}
|
||||
|
||||
release() {
|
||||
--this._acquired;
|
||||
this._flush();
|
||||
}
|
||||
|
||||
private _flush() {
|
||||
while (this._acquired < this._max && this._queue.length) {
|
||||
++this._acquired;
|
||||
this._queue.shift()!.resolve();
|
||||
}
|
||||
await this._wsServer.close();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ import { ProgressController } from '../progress';
|
|||
import { CRBrowser } from '../chromium/crBrowser';
|
||||
import { helper } from '../helper';
|
||||
import { PipeTransport } from '../../protocol/transport';
|
||||
import { RecentLogsCollector } from '../../common/debugLogger';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
||||
import { TimeoutSettings } from '../../common/timeoutSettings';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import { Page } from './page';
|
|||
import { Download } from './download';
|
||||
import type { ProxySettings } from './types';
|
||||
import type { ChildProcess } from 'child_process';
|
||||
import type { RecentLogsCollector } from '../common/debugLogger';
|
||||
import type { RecentLogsCollector } from '../utils/debugLogger';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import { Artifact } from './artifact';
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ import { DEFAULT_TIMEOUT, TimeoutSettings } from '../common/timeoutSettings';
|
|||
import { debugMode } from '../utils';
|
||||
import { existsAsync } from '../utils/fileUtils';
|
||||
import { helper } from './helper';
|
||||
import { RecentLogsCollector } from '../common/debugLogger';
|
||||
import { RecentLogsCollector } from '../utils/debugLogger';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import { SdkObject } from './instrumentation';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ import { getUserAgent } from '../../utils/userAgent';
|
|||
import { wrapInASCIIBox } from '../../utils/ascii';
|
||||
import { debugMode, headersArrayToObject, headersObjectToArray, } from '../../utils';
|
||||
import { removeFolders } from '../../utils/fileUtils';
|
||||
import { RecentLogsCollector } from '../../common/debugLogger';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import type { Progress } from '../progress';
|
||||
import { ProgressController } from '../progress';
|
||||
import { TimeoutSettings } from '../../common/timeoutSettings';
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { type RegisteredListener, assert, eventsHelper } from '../../utils';
|
|||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import type { Protocol } from './protocol';
|
||||
import { EventEmitter } from 'events';
|
||||
import type { RecentLogsCollector } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import type { ProtocolLogger } from '../types';
|
||||
import { helper } from '../helper';
|
||||
import { ProtocolError } from '../protocolError';
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ import type { BrowserOptions, BrowserProcess } from '../browser';
|
|||
import type { Playwright } from '../playwright';
|
||||
import type * as childProcess from 'child_process';
|
||||
import * as readline from 'readline';
|
||||
import { RecentLogsCollector } from '../../common/debugLogger';
|
||||
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { serverSideCallMetadata, SdkObject } from '../instrumentation';
|
||||
import type * as channels from '@protocol/channels';
|
||||
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@
|
|||
import { EventEmitter } from 'events';
|
||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import type { Protocol } from './protocol';
|
||||
import type { RecentLogsCollector } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import type { ProtocolLogger } from '../types';
|
||||
import { helper } from '../helper';
|
||||
import { ProtocolError } from '../protocolError';
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { FFNetworkManager } from './ffNetworkManager';
|
|||
import type { Protocol } from './protocol';
|
||||
import type { Progress } from '../progress';
|
||||
import { splitErrorMessage } from '../../utils/stackTrace';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { TargetClosedError } from '../errors';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import type { Progress } from './progress';
|
|||
import { ProgressController } from './progress';
|
||||
import { LongStandingScope, assert, constructURLBasedOnBaseURL, makeWaitForNextTask, monotonicTime } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import { serverSideCallMetadata, SdkObject } from './instrumentation';
|
||||
import type { InjectedScript, ElementStateWithoutStable, FrameExpectParams } from './injected/injectedScript';
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
import type { EventEmitter } from 'events';
|
||||
import type * as types from './types';
|
||||
import type { Progress } from './progress';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { RegisteredListener } from '../utils/eventsHelper';
|
||||
import { eventsHelper } from '../utils/eventsHelper';
|
||||
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ import type { Progress } from './progress';
|
|||
import { ProgressController } from './progress';
|
||||
import { LongStandingScope, assert, isError } from '../utils';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { ImageComparatorOptions } from '../utils/comparators';
|
||||
import { getComparator } from '../utils/comparators';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from './transport';
|
||||
import { makeWaitForNextTask } from '../utils';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
|
||||
export class PipeTransport implements ConnectionTransport {
|
||||
private _pipeRead: NodeJS.ReadableStream;
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ import { Selectors } from './selectors';
|
|||
import { WebKit } from './webkit/webkit';
|
||||
import type { CallMetadata } from './instrumentation';
|
||||
import { createInstrumentation, SdkObject } from './instrumentation';
|
||||
import { debugLogger } from '../common/debugLogger';
|
||||
import { debugLogger } from '../utils/debugLogger';
|
||||
import type { Page } from './page';
|
||||
import { DebugController } from './debugController';
|
||||
import type { Language } from '../utils/isomorphic/locatorGenerators';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
import { TimeoutError } from './errors';
|
||||
import { assert, monotonicTime } from '../utils';
|
||||
import type { LogName } from '../common/debugLogger';
|
||||
import type { LogName } from '../utils/debugLogger';
|
||||
import type { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||
import type { ElementHandle } from './dom';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ import os from 'os';
|
|||
import path from 'path';
|
||||
import childProcess from 'child_process';
|
||||
import { existsAsync } from '../../utils/fileUtils';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { colors, progress as ProgressBar } from '../../utilsBundle';
|
||||
import { browserDirectoryToMarkerFilePath } from '.';
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ import { transformCommandsForRoot, dockerVersion, readDockerVersionSync } from '
|
|||
import { installDependenciesLinux, installDependenciesWindows, validateDependenciesLinux, validateDependenciesWindows } from './dependencies';
|
||||
import { downloadBrowserWithProgressBar, logPolitely } from './browserFetcher';
|
||||
export { writeDockerVersion } from './dependencies';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
|
||||
const PACKAGE_PATH = path.join(__dirname, '..', '..', '..');
|
||||
const BIN_PATH = path.join(__dirname, '..', '..', '..', 'bin');
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { BrowserContext } from '../../browserContext';
|
|||
import { Page } from '../../page';
|
||||
import type { RegisteredListener } from '../../../utils/eventsHelper';
|
||||
import { eventsHelper } from '../../../utils/eventsHelper';
|
||||
import { debugLogger } from '../../../common/debugLogger';
|
||||
import { debugLogger } from '../../../utils/debugLogger';
|
||||
import type { Frame } from '../../frames';
|
||||
import type { SnapshotData } from './snapshotterInjected';
|
||||
import { frameSnapshotStreamer } from './snapshotterInjected';
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@ import { EventEmitter } from 'events';
|
|||
import { assert } from '../../utils';
|
||||
import type { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
|
||||
import type { Protocol } from './protocol';
|
||||
import type { RecentLogsCollector } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import type { RecentLogsCollector } from '../../utils/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import type { ProtocolLogger } from '../types';
|
||||
import { helper } from '../helper';
|
||||
import { ProtocolError } from '../protocolError';
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './wkInput';
|
|||
import { WKInterceptableRequest, WKRouteImpl } from './wkInterceptableRequest';
|
||||
import { WKProvisionalPage } from './wkProvisionalPage';
|
||||
import { WKWorkers } from './wkWorkers';
|
||||
import { debugLogger } from '../../common/debugLogger';
|
||||
import { debugLogger } from '../../utils/debugLogger';
|
||||
import { ManualPromise } from '../../utils/manualPromise';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { TargetClosedError } from '../errors';
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export * from './network';
|
|||
export * from './processLauncher';
|
||||
export * from './profiler';
|
||||
export * from './rtti';
|
||||
export * from './semaphore';
|
||||
export * from './spawnAsync';
|
||||
export * from './stackTrace';
|
||||
export * from './task';
|
||||
|
|
|
|||
50
packages/playwright-core/src/utils/semaphore.ts
Normal file
50
packages/playwright-core/src/utils/semaphore.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ManualPromise } from './manualPromise';
|
||||
|
||||
export class Semaphore {
|
||||
private _max: number;
|
||||
private _acquired = 0;
|
||||
private _queue: ManualPromise[] = [];
|
||||
|
||||
constructor(max: number) {
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
setMax(max: number) {
|
||||
this._max = max;
|
||||
}
|
||||
|
||||
acquire(): Promise<void> {
|
||||
const lock = new ManualPromise();
|
||||
this._queue.push(lock);
|
||||
this._flush();
|
||||
return lock;
|
||||
}
|
||||
|
||||
release() {
|
||||
--this._acquired;
|
||||
this._flush();
|
||||
}
|
||||
|
||||
private _flush() {
|
||||
while (this._acquired < this._max && this._queue.length) {
|
||||
++this._acquired;
|
||||
this._queue.shift()!.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
146
packages/playwright-core/src/utils/wsServer.ts
Normal file
146
packages/playwright-core/src/utils/wsServer.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Copyright (c) Microsoft Corporation.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type http from 'http';
|
||||
import type stream from 'stream';
|
||||
import { createHttpServer } from '../utils';
|
||||
import type { WebSocketServer, WebSocket } from '../utilsBundle';
|
||||
import { wsServer } from '../utilsBundle';
|
||||
import { debugLogger } from './debugLogger';
|
||||
|
||||
let lastConnectionId = 0;
|
||||
const kConnectionSymbol = Symbol('kConnection');
|
||||
|
||||
export const perMessageDeflate = {
|
||||
zlibDeflateOptions: {
|
||||
level: 3,
|
||||
},
|
||||
zlibInflateOptions: {
|
||||
chunkSize: 10 * 1024
|
||||
},
|
||||
threshold: 10 * 1024,
|
||||
};
|
||||
|
||||
export type WSConnection = {
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
|
||||
export type WSServerDelegate = {
|
||||
onHeaders: (headers: string[]) => void;
|
||||
onUpgrade: (request: http.IncomingMessage, socket: stream.Duplex) => { error: string } | undefined;
|
||||
onConnection: (request: http.IncomingMessage, url: URL, ws: WebSocket, id: string) => WSConnection;
|
||||
onClose(): Promise<void>;
|
||||
};
|
||||
|
||||
export class WSServer {
|
||||
private _wsServer: WebSocketServer | undefined;
|
||||
server: http.Server | undefined;
|
||||
private _delegate: WSServerDelegate;
|
||||
|
||||
constructor(delegate: WSServerDelegate) {
|
||||
this._delegate = delegate;
|
||||
}
|
||||
|
||||
async listen(port: number = 0, hostname: string | undefined, path: string): Promise<string> {
|
||||
debugLogger.log('server', `Server started at ${new Date()}`);
|
||||
|
||||
const server = createHttpServer((request: http.IncomingMessage, response: http.ServerResponse) => {
|
||||
if (request.method === 'GET' && request.url === '/json') {
|
||||
response.setHeader('Content-Type', 'application/json');
|
||||
response.end(JSON.stringify({
|
||||
wsEndpointPath: path,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
response.end('Running');
|
||||
});
|
||||
server.on('error', error => debugLogger.log('server', String(error)));
|
||||
this.server = server;
|
||||
|
||||
const wsEndpoint = await new Promise<string>((resolve, reject) => {
|
||||
server.listen(port, hostname, () => {
|
||||
const address = server.address();
|
||||
if (!address) {
|
||||
reject(new Error('Could not bind server socket'));
|
||||
return;
|
||||
}
|
||||
const wsEndpoint = typeof address === 'string' ? `${address}${path}` : `ws://${hostname || 'localhost'}:${address.port}${path}`;
|
||||
resolve(wsEndpoint);
|
||||
}).on('error', reject);
|
||||
});
|
||||
|
||||
debugLogger.log('server', 'Listening at ' + wsEndpoint);
|
||||
|
||||
this._wsServer = new wsServer({
|
||||
noServer: true,
|
||||
perMessageDeflate,
|
||||
});
|
||||
|
||||
this._wsServer.on('headers', headers => this._delegate.onHeaders(headers));
|
||||
|
||||
server.on('upgrade', (request, socket, head) => {
|
||||
const pathname = new URL('http://localhost' + request.url!).pathname;
|
||||
if (pathname !== path) {
|
||||
socket.write(`HTTP/${request.httpVersion} 400 Bad Request\r\n\r\n`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
const upgradeResult = this._delegate.onUpgrade(request, socket);
|
||||
if (upgradeResult) {
|
||||
socket.write(upgradeResult.error);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
this._wsServer?.handleUpgrade(request, socket, head, ws => this._wsServer?.emit('connection', ws, request));
|
||||
});
|
||||
|
||||
this._wsServer.on('connection', (ws, request) => {
|
||||
debugLogger.log('server', 'Connected client ws.extension=' + ws.extensions);
|
||||
const url = new URL('http://localhost' + (request.url || ''));
|
||||
const id = String(++lastConnectionId);
|
||||
debugLogger.log('server', `[${id}] serving connection: ${request.url}`);
|
||||
const connection = this._delegate.onConnection(request, url, ws, id);
|
||||
(ws as any)[kConnectionSymbol] = connection;
|
||||
});
|
||||
|
||||
return wsEndpoint;
|
||||
}
|
||||
|
||||
async close() {
|
||||
const server = this._wsServer;
|
||||
if (!server)
|
||||
return;
|
||||
debugLogger.log('server', 'closing websocket server');
|
||||
const waitForClose = new Promise(f => server.close(f));
|
||||
// First disconnect all remaining clients.
|
||||
await Promise.all(Array.from(server.clients).map(async ws => {
|
||||
const connection = (ws as any)[kConnectionSymbol] as WSConnection | undefined;
|
||||
if (connection)
|
||||
await connection.close();
|
||||
try {
|
||||
ws.terminate();
|
||||
} catch (e) {
|
||||
}
|
||||
}));
|
||||
await waitForClose;
|
||||
debugLogger.log('server', 'closing http server');
|
||||
if (this.server)
|
||||
await new Promise(f => this.server!.close(f));
|
||||
this._wsServer = undefined;
|
||||
this.server = undefined;
|
||||
debugLogger.log('server', 'closed server');
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue