chore: detect browser reuse based on the connection headers (#18230)
This commit is contained in:
parent
7ae447ea0f
commit
5b1e4e08a5
|
|
@ -54,7 +54,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
|||
path = options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`;
|
||||
|
||||
// 2. Start the server
|
||||
const server = new PlaywrightServer('use-pre-launched-browser', { path, maxConcurrentConnections: Infinity, maxIncomingConnections: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
||||
const server = new PlaywrightServer({ path, maxConnections: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
||||
const wsEndpoint = await server.listen(options.port);
|
||||
|
||||
// 3. Return the BrowserServer interface
|
||||
|
|
|
|||
|
|
@ -49,10 +49,8 @@ export function runDriver() {
|
|||
};
|
||||
}
|
||||
|
||||
export async function runServer(port: number | undefined, path = '/', maxClients = Infinity, enableSocksProxy = true, reuseBrowser = false) {
|
||||
const maxIncomingConnections = maxClients;
|
||||
const maxConcurrentConnections = reuseBrowser ? 1 : maxClients;
|
||||
const server = new PlaywrightServer(reuseBrowser ? 'reuse-browser' : 'auto', { path, maxIncomingConnections, maxConcurrentConnections, enableSocksProxy });
|
||||
export async function runServer(port: number | undefined, path = '/', maxConnections = Infinity, enableSocksProxy = true, reuseBrowser = false) {
|
||||
const server = new PlaywrightServer({ path, maxConnections, enableSocksProxy });
|
||||
const wsEndpoint = await server.listen(port);
|
||||
process.on('exit', () => server.close().catch(console.error));
|
||||
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
||||
|
|
@ -86,7 +84,6 @@ class ProtocolHandler {
|
|||
this._controller = playwright.debugController;
|
||||
this._controller.setAutoCloseAllowed(true);
|
||||
this._controller.setTrackHierarcy(true);
|
||||
this._controller.setReuseBrowser(true);
|
||||
this._controller.on(DebugController.Events.BrowsersChanged, browsers => {
|
||||
process.send!({ method: 'browsersChanged', params: { browsers } });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
|
|||
const log = debug(`pw:grid:worker:${workerId}`);
|
||||
log('created');
|
||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
||||
new PlaywrightConnection(Promise.resolve(), 'auto', ws, false, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
||||
new PlaywrightConnection(Promise.resolve(), 'launch-browser', ws, { enableSocksProxy: true, browserName, launchOptions: {} }, { playwright: null, browser: null }, log, async () => {
|
||||
log('exiting process');
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
// Meanwhile, try to gracefully close all browsers.
|
||||
|
|
|
|||
|
|
@ -350,10 +350,6 @@ scheme.DebugControllerSetTrackHierarchyParams = tObject({
|
|||
enabled: tBoolean,
|
||||
});
|
||||
scheme.DebugControllerSetTrackHierarchyResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerSetReuseBrowserParams = tObject({
|
||||
enabled: tBoolean,
|
||||
});
|
||||
scheme.DebugControllerSetReuseBrowserResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerResetForReuseParams = tOptional(tObject({}));
|
||||
scheme.DebugControllerResetForReuseResult = tOptional(tObject({}));
|
||||
scheme.DebugControllerNavigateAllParams = tObject({
|
||||
|
|
|
|||
|
|
@ -21,11 +21,12 @@ import { Browser } from '../server/browser';
|
|||
import { serverSideCallMetadata } from '../server/instrumentation';
|
||||
import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||
import { SocksProxy } from '../common/socksProxy';
|
||||
import type { Mode } from './playwrightServer';
|
||||
import { assert } from '../utils';
|
||||
import type { LaunchOptions } from '../server/types';
|
||||
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
|
||||
|
||||
export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser';
|
||||
|
||||
type Options = {
|
||||
enableSocksProxy: boolean,
|
||||
browserName: string | null,
|
||||
|
|
@ -48,13 +49,13 @@ export class PlaywrightConnection {
|
|||
private _options: Options;
|
||||
private _root: DispatcherScope;
|
||||
|
||||
constructor(lock: Promise<void>, mode: Mode, ws: WebSocket, isDebugControllerClient: boolean, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||
constructor(lock: Promise<void>, clientType: ClientType, ws: WebSocket, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||
this._ws = ws;
|
||||
this._preLaunched = preLaunched;
|
||||
this._options = options;
|
||||
if (mode === 'reuse-browser' || mode === 'use-pre-launched-browser')
|
||||
if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser')
|
||||
assert(preLaunched.playwright);
|
||||
if (mode === 'use-pre-launched-browser')
|
||||
if (clientType === 'pre-launched-browser')
|
||||
assert(preLaunched.browser);
|
||||
this._onClose = onClose;
|
||||
this._debugLog = log;
|
||||
|
|
@ -73,19 +74,21 @@ export class PlaywrightConnection {
|
|||
ws.on('close', () => this._onDisconnect());
|
||||
ws.on('error', error => this._onDisconnect(error));
|
||||
|
||||
if (isDebugControllerClient) {
|
||||
if (clientType === 'controller') {
|
||||
this._root = this._initDebugControllerMode();
|
||||
return;
|
||||
}
|
||||
|
||||
this._root = new RootDispatcher(this._dispatcherConnection, async scope => {
|
||||
if (mode === 'reuse-browser')
|
||||
if (clientType === 'reuse-browser')
|
||||
return await this._initReuseBrowsersMode(scope);
|
||||
if (mode === 'use-pre-launched-browser')
|
||||
if (clientType === 'pre-launched-browser')
|
||||
return await this._initPreLaunchedBrowserMode(scope);
|
||||
if (!options.browserName)
|
||||
if (clientType === 'launch-browser')
|
||||
return await this._initLaunchBrowserMode(scope);
|
||||
if (clientType === 'playwright')
|
||||
return await this._initPlaywrightConnectMode(scope);
|
||||
return await this._initLaunchBrowserMode(scope);
|
||||
throw new Error('Unsupported client type: ' + clientType);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ import type { Browser } from '../server/browser';
|
|||
import type { Playwright } from '../server/playwright';
|
||||
import { createPlaywright } from '../server/playwright';
|
||||
import { PlaywrightConnection } from './playwrightConnection';
|
||||
import { assert } from '../utils';
|
||||
import type { ClientType } from './playwrightConnection';
|
||||
import type { LaunchOptions } from '../server/types';
|
||||
import { ManualPromise } from '../utils/manualPromise';
|
||||
|
||||
|
|
@ -35,13 +35,9 @@ function newLogger() {
|
|||
return (message: string) => debugLog(`[id=${id}] ${message}`);
|
||||
}
|
||||
|
||||
// TODO: replace 'reuse-browser' with 'allow-reuse' in 1.27.
|
||||
export type Mode = 'use-pre-launched-browser' | 'reuse-browser' | 'auto';
|
||||
|
||||
type ServerOptions = {
|
||||
path: string;
|
||||
maxIncomingConnections: number;
|
||||
maxConcurrentConnections: number;
|
||||
maxConnections: number;
|
||||
enableSocksProxy: boolean;
|
||||
preLaunchedBrowser?: Browser
|
||||
};
|
||||
|
|
@ -49,16 +45,12 @@ type ServerOptions = {
|
|||
export class PlaywrightServer {
|
||||
private _preLaunchedPlaywright: Playwright | null = null;
|
||||
private _wsServer: WebSocketServer | undefined;
|
||||
private _mode: Mode;
|
||||
private _options: ServerOptions;
|
||||
|
||||
constructor(mode: Mode, options: ServerOptions) {
|
||||
this._mode = mode;
|
||||
constructor(options: ServerOptions) {
|
||||
this._options = options;
|
||||
if (mode === 'use-pre-launched-browser') {
|
||||
assert(options.preLaunchedBrowser);
|
||||
if (options.preLaunchedBrowser)
|
||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
||||
}
|
||||
}
|
||||
|
||||
preLaunchedPlaywright(): Playwright {
|
||||
|
|
@ -95,13 +87,10 @@ export class PlaywrightServer {
|
|||
debugLog('Listening at ' + wsEndpoint);
|
||||
|
||||
this._wsServer = new wsServer({ server, path: this._options.path });
|
||||
const browserSemaphore = new Semaphore(this._options.maxConcurrentConnections);
|
||||
const browserSemaphore = new Semaphore(this._options.maxConnections);
|
||||
const controllerSemaphore = new Semaphore(1);
|
||||
const reuseBrowserSemaphore = new Semaphore(1);
|
||||
this._wsServer.on('connection', (ws, request) => {
|
||||
if (browserSemaphore.requested() >= this._options.maxIncomingConnections) {
|
||||
ws.close(1013, 'Playwright Server is busy');
|
||||
return;
|
||||
}
|
||||
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;
|
||||
|
|
@ -119,26 +108,27 @@ export class PlaywrightServer {
|
|||
const log = newLogger();
|
||||
log(`serving connection: ${request.url}`);
|
||||
const isDebugControllerClient = !!request.headers['x-playwright-debug-controller'];
|
||||
const semaphore = isDebugControllerClient ? controllerSemaphore : browserSemaphore;
|
||||
const shouldReuseBrowser = !!request.headers['x-playwright-reuse-context'];
|
||||
const semaphore = isDebugControllerClient ? controllerSemaphore : (shouldReuseBrowser ? reuseBrowserSemaphore : browserSemaphore);
|
||||
|
||||
// If we started in the legacy reuse-browser mode, create this._preLaunchedPlaywright.
|
||||
// If we get a reuse-controller request, create this._preLaunchedPlaywright.
|
||||
if (isDebugControllerClient || (this._mode === 'reuse-browser') && !this._preLaunchedPlaywright)
|
||||
if (isDebugControllerClient || shouldReuseBrowser)
|
||||
this.preLaunchedPlaywright();
|
||||
|
||||
// If we have a playwright to reuse, consult controller for reuse mode.
|
||||
let mode = this._mode;
|
||||
if (mode === 'auto' && this._preLaunchedPlaywright?.debugController.reuseBrowser())
|
||||
mode = 'reuse-browser';
|
||||
|
||||
if (mode === 'reuse-browser')
|
||||
semaphore.setMax(1);
|
||||
else
|
||||
semaphore.setMax(this._options.maxConcurrentConnections);
|
||||
let clientType: ClientType = 'playwright';
|
||||
if (isDebugControllerClient)
|
||||
clientType = 'controller';
|
||||
else if (shouldReuseBrowser)
|
||||
clientType = 'reuse-browser';
|
||||
else if (this._options.preLaunchedBrowser)
|
||||
clientType = 'pre-launched-browser';
|
||||
else if (browserName)
|
||||
clientType = 'launch-browser';
|
||||
|
||||
const connection = new PlaywrightConnection(
|
||||
semaphore.aquire(),
|
||||
mode, ws, isDebugControllerClient,
|
||||
clientType, ws,
|
||||
{ enableSocksProxy, browserName, launchOptions },
|
||||
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
||||
log, () => semaphore.release());
|
||||
|
|
@ -192,10 +182,6 @@ export class Semaphore {
|
|||
return lock;
|
||||
}
|
||||
|
||||
requested() {
|
||||
return this._aquired + this._queue.length;
|
||||
}
|
||||
|
||||
release() {
|
||||
--this._aquired;
|
||||
this._flush();
|
||||
|
|
|
|||
|
|
@ -55,7 +55,6 @@ export class DebugController extends SdkObject {
|
|||
dispose() {
|
||||
this.setTrackHierarcy(false);
|
||||
this.setAutoCloseAllowed(false);
|
||||
this.setReuseBrowser(false);
|
||||
}
|
||||
|
||||
setTrackHierarcy(enabled: boolean) {
|
||||
|
|
@ -72,14 +71,6 @@ export class DebugController extends SdkObject {
|
|||
}
|
||||
}
|
||||
|
||||
reuseBrowser(): boolean {
|
||||
return this._reuseBrowser;
|
||||
}
|
||||
|
||||
setReuseBrowser(enabled: boolean) {
|
||||
this._reuseBrowser = enabled;
|
||||
}
|
||||
|
||||
async resetForReuse() {
|
||||
const contexts = new Set<BrowserContext>();
|
||||
for (const page of this._playwright.allPages())
|
||||
|
|
|
|||
|
|
@ -40,10 +40,6 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
|
|||
this._object.setTrackHierarcy(params.enabled);
|
||||
}
|
||||
|
||||
async setReuseBrowser(params: channels.DebugControllerSetReuseBrowserParams) {
|
||||
this._object.setReuseBrowser(params.enabled);
|
||||
}
|
||||
|
||||
async resetForReuse() {
|
||||
await this._object.resetForReuse();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,10 +72,22 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
|||
headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
|
||||
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
|
||||
launchOptions: [{}, { scope: 'worker', option: true }],
|
||||
connectOptions: [process.env.PW_TEST_CONNECT_WS_ENDPOINT ? {
|
||||
wsEndpoint: process.env.PW_TEST_CONNECT_WS_ENDPOINT,
|
||||
headers: process.env.PW_TEST_CONNECT_HEADERS ? JSON.parse(process.env.PW_TEST_CONNECT_HEADERS) : undefined,
|
||||
} : undefined, { scope: 'worker', option: true }],
|
||||
connectOptions: [({}, use) => {
|
||||
const wsEndpoint = process.env.PW_TEST_CONNECT_WS_ENDPOINT;
|
||||
if (!wsEndpoint)
|
||||
return use(undefined);
|
||||
let headers = process.env.PW_TEST_CONNECT_HEADERS ? JSON.parse(process.env.PW_TEST_CONNECT_HEADERS) : undefined;
|
||||
if (process.env.PW_TEST_REUSE_CONTEXT) {
|
||||
headers = {
|
||||
...headers,
|
||||
'x-playwright-reuse-context': '1',
|
||||
};
|
||||
}
|
||||
return use({
|
||||
wsEndpoint,
|
||||
headers
|
||||
});
|
||||
}, { scope: 'worker', option: true }],
|
||||
screenshot: ['off', { scope: 'worker', option: true }],
|
||||
video: ['off', { scope: 'worker', option: true }],
|
||||
trace: ['off', { scope: 'worker', option: true }],
|
||||
|
|
|
|||
|
|
@ -599,7 +599,6 @@ export interface DebugControllerEventTarget {
|
|||
export interface DebugControllerChannel extends DebugControllerEventTarget, Channel {
|
||||
_type_DebugController: boolean;
|
||||
setTrackHierarchy(params: DebugControllerSetTrackHierarchyParams, metadata?: Metadata): Promise<DebugControllerSetTrackHierarchyResult>;
|
||||
setReuseBrowser(params: DebugControllerSetReuseBrowserParams, metadata?: Metadata): Promise<DebugControllerSetReuseBrowserResult>;
|
||||
resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>;
|
||||
navigateAll(params: DebugControllerNavigateAllParams, metadata?: Metadata): Promise<DebugControllerNavigateAllResult>;
|
||||
setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: Metadata): Promise<DebugControllerSetRecorderModeResult>;
|
||||
|
|
@ -629,13 +628,6 @@ export type DebugControllerSetTrackHierarchyOptions = {
|
|||
|
||||
};
|
||||
export type DebugControllerSetTrackHierarchyResult = void;
|
||||
export type DebugControllerSetReuseBrowserParams = {
|
||||
enabled: boolean,
|
||||
};
|
||||
export type DebugControllerSetReuseBrowserOptions = {
|
||||
|
||||
};
|
||||
export type DebugControllerSetReuseBrowserResult = void;
|
||||
export type DebugControllerResetForReuseParams = {};
|
||||
export type DebugControllerResetForReuseOptions = {};
|
||||
export type DebugControllerResetForReuseResult = void;
|
||||
|
|
|
|||
|
|
@ -664,10 +664,6 @@ DebugController:
|
|||
parameters:
|
||||
enabled: boolean
|
||||
|
||||
setReuseBrowser:
|
||||
parameters:
|
||||
enabled: boolean
|
||||
|
||||
resetForReuse:
|
||||
|
||||
navigateAll:
|
||||
|
|
|
|||
Loading…
Reference in a new issue