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}`; path = options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`;
// 2. Start the server // 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); const wsEndpoint = await server.listen(options.port);
// 3. Return the BrowserServer interface // 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) { export async function runServer(port: number | undefined, path = '/', maxConnections = Infinity, enableSocksProxy = true, reuseBrowser = false) {
const maxIncomingConnections = maxClients; const server = new PlaywrightServer({ path, maxConnections, enableSocksProxy });
const maxConcurrentConnections = reuseBrowser ? 1 : maxClients;
const server = new PlaywrightServer(reuseBrowser ? 'reuse-browser' : 'auto', { path, maxIncomingConnections, maxConcurrentConnections, enableSocksProxy });
const wsEndpoint = await server.listen(port); const wsEndpoint = await server.listen(port);
process.on('exit', () => server.close().catch(console.error)); process.on('exit', () => server.close().catch(console.error));
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
@ -86,7 +84,6 @@ class ProtocolHandler {
this._controller = playwright.debugController; this._controller = playwright.debugController;
this._controller.setAutoCloseAllowed(true); this._controller.setAutoCloseAllowed(true);
this._controller.setTrackHierarcy(true); this._controller.setTrackHierarcy(true);
this._controller.setReuseBrowser(true);
this._controller.on(DebugController.Events.BrowsersChanged, browsers => { this._controller.on(DebugController.Events.BrowsersChanged, browsers => {
process.send!({ method: 'browsersChanged', params: { 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}`); const log = debug(`pw:grid:worker:${workerId}`);
log('created'); log('created');
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`); 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'); log('exiting process');
setTimeout(() => process.exit(0), 30000); setTimeout(() => process.exit(0), 30000);
// Meanwhile, try to gracefully close all browsers. // Meanwhile, try to gracefully close all browsers.

View file

@ -350,10 +350,6 @@ scheme.DebugControllerSetTrackHierarchyParams = tObject({
enabled: tBoolean, enabled: tBoolean,
}); });
scheme.DebugControllerSetTrackHierarchyResult = tOptional(tObject({})); scheme.DebugControllerSetTrackHierarchyResult = tOptional(tObject({}));
scheme.DebugControllerSetReuseBrowserParams = tObject({
enabled: tBoolean,
});
scheme.DebugControllerSetReuseBrowserResult = tOptional(tObject({}));
scheme.DebugControllerResetForReuseParams = tOptional(tObject({})); scheme.DebugControllerResetForReuseParams = tOptional(tObject({}));
scheme.DebugControllerResetForReuseResult = tOptional(tObject({})); scheme.DebugControllerResetForReuseResult = tOptional(tObject({}));
scheme.DebugControllerNavigateAllParams = tObject({ scheme.DebugControllerNavigateAllParams = tObject({

View file

@ -21,11 +21,12 @@ import { Browser } from '../server/browser';
import { serverSideCallMetadata } from '../server/instrumentation'; import { serverSideCallMetadata } from '../server/instrumentation';
import { gracefullyCloseAll } from '../utils/processLauncher'; import { gracefullyCloseAll } from '../utils/processLauncher';
import { SocksProxy } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy';
import type { Mode } from './playwrightServer';
import { assert } from '../utils'; import { assert } from '../utils';
import type { LaunchOptions } from '../server/types'; import type { LaunchOptions } from '../server/types';
import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher'; import { DebugControllerDispatcher } from '../server/dispatchers/debugControllerDispatcher';
export type ClientType = 'controller' | 'playwright' | 'launch-browser' | 'reuse-browser' | 'pre-launched-browser';
type Options = { type Options = {
enableSocksProxy: boolean, enableSocksProxy: boolean,
browserName: string | null, browserName: string | null,
@ -48,13 +49,13 @@ export class PlaywrightConnection {
private _options: Options; private _options: Options;
private _root: DispatcherScope; 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._ws = ws;
this._preLaunched = preLaunched; this._preLaunched = preLaunched;
this._options = options; this._options = options;
if (mode === 'reuse-browser' || mode === 'use-pre-launched-browser') if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser')
assert(preLaunched.playwright); assert(preLaunched.playwright);
if (mode === 'use-pre-launched-browser') if (clientType === 'pre-launched-browser')
assert(preLaunched.browser); assert(preLaunched.browser);
this._onClose = onClose; this._onClose = onClose;
this._debugLog = log; this._debugLog = log;
@ -73,19 +74,21 @@ export class PlaywrightConnection {
ws.on('close', () => this._onDisconnect()); ws.on('close', () => this._onDisconnect());
ws.on('error', error => this._onDisconnect(error)); ws.on('error', error => this._onDisconnect(error));
if (isDebugControllerClient) { if (clientType === 'controller') {
this._root = this._initDebugControllerMode(); this._root = this._initDebugControllerMode();
return; return;
} }
this._root = new RootDispatcher(this._dispatcherConnection, async scope => { this._root = new RootDispatcher(this._dispatcherConnection, async scope => {
if (mode === 'reuse-browser') if (clientType === 'reuse-browser')
return await this._initReuseBrowsersMode(scope); return await this._initReuseBrowsersMode(scope);
if (mode === 'use-pre-launched-browser') if (clientType === 'pre-launched-browser')
return await this._initPreLaunchedBrowserMode(scope); 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._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 type { Playwright } from '../server/playwright';
import { createPlaywright } from '../server/playwright'; import { createPlaywright } from '../server/playwright';
import { PlaywrightConnection } from './playwrightConnection'; import { PlaywrightConnection } from './playwrightConnection';
import { assert } from '../utils'; import type { ClientType } from './playwrightConnection';
import type { LaunchOptions } from '../server/types'; import type { LaunchOptions } from '../server/types';
import { ManualPromise } from '../utils/manualPromise'; import { ManualPromise } from '../utils/manualPromise';
@ -35,13 +35,9 @@ function newLogger() {
return (message: string) => debugLog(`[id=${id}] ${message}`); 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 = { type ServerOptions = {
path: string; path: string;
maxIncomingConnections: number; maxConnections: number;
maxConcurrentConnections: number;
enableSocksProxy: boolean; enableSocksProxy: boolean;
preLaunchedBrowser?: Browser preLaunchedBrowser?: Browser
}; };
@ -49,16 +45,12 @@ type ServerOptions = {
export class PlaywrightServer { export class PlaywrightServer {
private _preLaunchedPlaywright: Playwright | null = null; private _preLaunchedPlaywright: Playwright | null = null;
private _wsServer: WebSocketServer | undefined; private _wsServer: WebSocketServer | undefined;
private _mode: Mode;
private _options: ServerOptions; private _options: ServerOptions;
constructor(mode: Mode, options: ServerOptions) { constructor(options: ServerOptions) {
this._mode = mode;
this._options = options; this._options = options;
if (mode === 'use-pre-launched-browser') { if (options.preLaunchedBrowser)
assert(options.preLaunchedBrowser);
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright; this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
}
} }
preLaunchedPlaywright(): Playwright { preLaunchedPlaywright(): Playwright {
@ -95,13 +87,10 @@ export class PlaywrightServer {
debugLog('Listening at ' + wsEndpoint); debugLog('Listening at ' + wsEndpoint);
this._wsServer = new wsServer({ server, path: this._options.path }); 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 controllerSemaphore = new Semaphore(1);
const reuseBrowserSemaphore = new Semaphore(1);
this._wsServer.on('connection', (ws, request) => { 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 url = new URL('http://localhost' + (request.url || ''));
const browserHeader = request.headers['x-playwright-browser']; const browserHeader = request.headers['x-playwright-browser'];
const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null; const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
@ -119,26 +108,27 @@ export class PlaywrightServer {
const log = newLogger(); const log = newLogger();
log(`serving connection: ${request.url}`); log(`serving connection: ${request.url}`);
const isDebugControllerClient = !!request.headers['x-playwright-debug-controller']; 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 started in the legacy reuse-browser mode, create this._preLaunchedPlaywright.
// If we get a reuse-controller request, 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(); this.preLaunchedPlaywright();
// If we have a playwright to reuse, consult controller for reuse mode. let clientType: ClientType = 'playwright';
let mode = this._mode; if (isDebugControllerClient)
if (mode === 'auto' && this._preLaunchedPlaywright?.debugController.reuseBrowser()) clientType = 'controller';
mode = 'reuse-browser'; else if (shouldReuseBrowser)
clientType = 'reuse-browser';
if (mode === 'reuse-browser') else if (this._options.preLaunchedBrowser)
semaphore.setMax(1); clientType = 'pre-launched-browser';
else else if (browserName)
semaphore.setMax(this._options.maxConcurrentConnections); clientType = 'launch-browser';
const connection = new PlaywrightConnection( const connection = new PlaywrightConnection(
semaphore.aquire(), semaphore.aquire(),
mode, ws, isDebugControllerClient, clientType, ws,
{ enableSocksProxy, browserName, launchOptions }, { enableSocksProxy, browserName, launchOptions },
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null }, { playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
log, () => semaphore.release()); log, () => semaphore.release());
@ -192,10 +182,6 @@ export class Semaphore {
return lock; return lock;
} }
requested() {
return this._aquired + this._queue.length;
}
release() { release() {
--this._aquired; --this._aquired;
this._flush(); this._flush();

View file

@ -55,7 +55,6 @@ export class DebugController extends SdkObject {
dispose() { dispose() {
this.setTrackHierarcy(false); this.setTrackHierarcy(false);
this.setAutoCloseAllowed(false); this.setAutoCloseAllowed(false);
this.setReuseBrowser(false);
} }
setTrackHierarcy(enabled: boolean) { 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() { async resetForReuse() {
const contexts = new Set<BrowserContext>(); const contexts = new Set<BrowserContext>();
for (const page of this._playwright.allPages()) 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); this._object.setTrackHierarcy(params.enabled);
} }
async setReuseBrowser(params: channels.DebugControllerSetReuseBrowserParams) {
this._object.setReuseBrowser(params.enabled);
}
async resetForReuse() { async resetForReuse() {
await this._object.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 }], headless: [({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true }],
channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }], channel: [({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true }],
launchOptions: [{}, { scope: 'worker', option: true }], launchOptions: [{}, { scope: 'worker', option: true }],
connectOptions: [process.env.PW_TEST_CONNECT_WS_ENDPOINT ? { connectOptions: [({}, use) => {
wsEndpoint: process.env.PW_TEST_CONNECT_WS_ENDPOINT, const wsEndpoint = process.env.PW_TEST_CONNECT_WS_ENDPOINT;
headers: process.env.PW_TEST_CONNECT_HEADERS ? JSON.parse(process.env.PW_TEST_CONNECT_HEADERS) : undefined, if (!wsEndpoint)
} : undefined, { scope: 'worker', option: true }], 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 }], screenshot: ['off', { scope: 'worker', option: true }],
video: ['off', { scope: 'worker', option: true }], video: ['off', { scope: 'worker', option: true }],
trace: ['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 { export interface DebugControllerChannel extends DebugControllerEventTarget, Channel {
_type_DebugController: boolean; _type_DebugController: boolean;
setTrackHierarchy(params: DebugControllerSetTrackHierarchyParams, metadata?: Metadata): Promise<DebugControllerSetTrackHierarchyResult>; setTrackHierarchy(params: DebugControllerSetTrackHierarchyParams, metadata?: Metadata): Promise<DebugControllerSetTrackHierarchyResult>;
setReuseBrowser(params: DebugControllerSetReuseBrowserParams, metadata?: Metadata): Promise<DebugControllerSetReuseBrowserResult>;
resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>; resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>;
navigateAll(params: DebugControllerNavigateAllParams, metadata?: Metadata): Promise<DebugControllerNavigateAllResult>; navigateAll(params: DebugControllerNavigateAllParams, metadata?: Metadata): Promise<DebugControllerNavigateAllResult>;
setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: Metadata): Promise<DebugControllerSetRecorderModeResult>; setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: Metadata): Promise<DebugControllerSetRecorderModeResult>;
@ -629,13 +628,6 @@ export type DebugControllerSetTrackHierarchyOptions = {
}; };
export type DebugControllerSetTrackHierarchyResult = void; export type DebugControllerSetTrackHierarchyResult = void;
export type DebugControllerSetReuseBrowserParams = {
enabled: boolean,
};
export type DebugControllerSetReuseBrowserOptions = {
};
export type DebugControllerSetReuseBrowserResult = void;
export type DebugControllerResetForReuseParams = {}; export type DebugControllerResetForReuseParams = {};
export type DebugControllerResetForReuseOptions = {}; export type DebugControllerResetForReuseOptions = {};
export type DebugControllerResetForReuseResult = void; export type DebugControllerResetForReuseResult = void;

View file

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