chore: detect browser reuse based on the connection headers (#18230)

This commit is contained in:
Pavel Feldman 2022-10-20 21:30:37 -04:00 committed by GitHub
parent 7ae447ea0f
commit 5b1e4e08a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 51 additions and 82 deletions

View file

@ -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

View file

@ -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 } });
});

View file

@ -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.

View file

@ -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({

View file

@ -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);
});
}

View file

@ -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();

View file

@ -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())

View file

@ -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();
}

View file

@ -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 }],

View file

@ -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;

View file

@ -664,10 +664,6 @@ DebugController:
parameters:
enabled: boolean
setReuseBrowser:
parameters:
enabled: boolean
resetForReuse:
navigateAll: