feat(reuse): retain different browser types when reusing (#16269)

This commit is contained in:
Pavel Feldman 2022-08-04 15:04:00 -07:00 committed by GitHub
parent b3d30a808f
commit 3aa5710b49
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 30 additions and 43 deletions

View file

@ -30,16 +30,16 @@ export function launchGridAgent(agentId: string, gridURL: string, runId: string
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerAgent?` + params.toString()); const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerAgent?` + params.toString());
ws.on('message', (message: string) => { ws.on('message', (message: string) => {
log('worker requested ' + message); log('worker requested ' + message);
const { workerId, browserAlias } = JSON.parse(message); const { workerId, browserName } = JSON.parse(message);
if (!workerId) { if (!workerId) {
log('workerId not specified'); log('workerId not specified');
return; return;
} }
if (!browserAlias) { if (!browserName) {
log('browserAlias not specified'); log('browserName not specified');
return; return;
} }
fork(require.resolve('./gridBrowserWorker.js'), [gridURL, agentId, workerId, browserAlias], { detached: true }); fork(require.resolve('./gridBrowserWorker.js'), [gridURL, agentId, workerId, browserName], { detached: true });
}); });
ws.on('close', () => process.exit(0)); ws.on('close', () => process.exit(0));
} }

View file

@ -19,11 +19,11 @@ import { ws as WebSocket } from '../utilsBundle';
import { PlaywrightConnection } from '../remote/playwrightConnection'; import { PlaywrightConnection } from '../remote/playwrightConnection';
import { gracefullyCloseAll } from '../utils/processLauncher'; import { gracefullyCloseAll } from '../utils/processLauncher';
function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: string, browserAlias: string) { function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: string, browserName: string) {
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, { enableSocksProxy: true, browserAlias, launchOptions: {} }, { playwright: null, browser: null }, log, async () => { new PlaywrightConnection(Promise.resolve(), 'auto', 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

@ -64,8 +64,7 @@ const WSErrors = {
type GridWorkerParams = { type GridWorkerParams = {
browserAlias?: string; browserName?: string;
headless?: boolean;
}; };
class GridWorker extends EventEmitter { class GridWorker extends EventEmitter {
@ -284,8 +283,7 @@ export class GridServer {
} }
agent.createWorker(ws, { agent.createWorker(ws, {
browserAlias: request.headers['x-playwright-browser'] as string | undefined, browserName: request.headers['x-playwright-browser'] as string,
headless: request.headers['x-playwright-headless'] !== '0',
}); });
return; return;
} }

View file

@ -15,12 +15,11 @@
*/ */
import type { WebSocket } from '../utilsBundle'; import type { WebSocket } from '../utilsBundle';
import type { Playwright, DispatcherScope, Executable } from '../server'; import type { Playwright, DispatcherScope } from '../server';
import { createPlaywright, DispatcherConnection, Root, PlaywrightDispatcher } from '../server'; import { createPlaywright, DispatcherConnection, Root, PlaywrightDispatcher } from '../server';
import { Browser } from '../server/browser'; 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 { registry } from '../server';
import { SocksProxy } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy';
import type { Mode } from './playwrightServer'; import type { Mode } from './playwrightServer';
import { assert } from '../utils'; import { assert } from '../utils';
@ -28,7 +27,7 @@ import type { LaunchOptions } from '../server/types';
type Options = { type Options = {
enableSocksProxy: boolean, enableSocksProxy: boolean,
browserAlias: string | null, browserName: string | null,
launchOptions: LaunchOptions, launchOptions: LaunchOptions,
}; };
@ -78,7 +77,7 @@ export class PlaywrightConnection {
return await this._initReuseBrowsersMode(scope); return await this._initReuseBrowsersMode(scope);
if (mode === 'use-pre-launched-browser') if (mode === 'use-pre-launched-browser')
return await this._initPreLaunchedBrowserMode(scope); return await this._initPreLaunchedBrowserMode(scope);
if (!options.browserAlias) if (!options.browserName)
return await this._initPlaywrightConnectMode(scope); return await this._initPlaywrightConnectMode(scope);
return await this._initLaunchBrowserMode(scope); return await this._initLaunchBrowserMode(scope);
}); });
@ -95,15 +94,11 @@ export class PlaywrightConnection {
} }
private async _initLaunchBrowserMode(scope: DispatcherScope) { private async _initLaunchBrowserMode(scope: DispatcherScope) {
this._debugLog(`engaged launch mode for "${this._options.browserAlias}"`); this._debugLog(`engaged launch mode for "${this._options.browserName}"`);
const executable = this._executableForBrowerAlias(this._options.browserAlias!);
const playwright = createPlaywright('javascript'); const playwright = createPlaywright('javascript');
const socksProxy = this._options.enableSocksProxy ? await this._enableSocksProxy(playwright) : undefined; const socksProxy = this._options.enableSocksProxy ? await this._enableSocksProxy(playwright) : undefined;
const browser = await playwright[executable.browserName!].launch(serverSideCallMetadata(), { const browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), this._options.launchOptions);
channel: executable.type === 'browser' ? undefined : executable.name,
headless: this._options.launchOptions?.headless,
});
// Close the browser on disconnect. // Close the browser on disconnect.
// TODO: it is technically possible to launch more browsers over protocol. // TODO: it is technically possible to launch more browsers over protocol.
@ -132,20 +127,26 @@ export class PlaywrightConnection {
} }
private async _initReuseBrowsersMode(scope: DispatcherScope) { private async _initReuseBrowsersMode(scope: DispatcherScope) {
this._debugLog(`engaged reuse browsers mode for ${this._options.browserAlias}`); this._debugLog(`engaged reuse browsers mode for ${this._options.browserName}`);
const executable = this._executableForBrowerAlias(this._options.browserAlias!);
const playwright = this._preLaunched.playwright!; const playwright = this._preLaunched.playwright!;
const requestedOptions = launchOptionsHash(this._options.launchOptions); const requestedOptions = launchOptionsHash(this._options.launchOptions);
let browser = playwright.allBrowsers().find(b => { let browser = playwright.allBrowsers().find(b => {
if (b.options.name !== this._options.browserName)
return false;
const existingOptions = launchOptionsHash(b.options.originalLaunchOptions); const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
return existingOptions === requestedOptions; return existingOptions === requestedOptions;
}); });
const remaining = playwright.allBrowsers().filter(b => b !== browser);
for (const r of remaining) // Close remaining browsers of this type+channel. Keep different browser types for the speed.
await r.close(); for (const b of playwright.allBrowsers()) {
if (b === browser)
continue;
if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel)
await b.close();
}
if (!browser) { if (!browser) {
browser = await playwright[executable.browserName!].launch(serverSideCallMetadata(), { browser = await playwright[this._options.browserName as 'chromium'].launch(serverSideCallMetadata(), {
...this._options.launchOptions, ...this._options.launchOptions,
headless: false, headless: false,
}); });
@ -187,13 +188,6 @@ export class PlaywrightConnection {
} catch (e) { } catch (e) {
} }
} }
private _executableForBrowerAlias(browserAlias: string): Executable {
const executable = registry.findExecutable(browserAlias);
if (!executable || !executable.browserName)
throw new Error(`Unsupported browser "${browserAlias}`);
return executable;
}
} }
function launchOptionsHash(options: LaunchOptions) { function launchOptionsHash(options: LaunchOptions) {

View file

@ -120,7 +120,7 @@ export class PlaywrightServer {
} }
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 browserAlias = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null; const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
const proxyHeader = request.headers['x-playwright-proxy']; const proxyHeader = request.headers['x-playwright-proxy'];
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader); const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
const enableSocksProxy = this._options.enableSocksProxy && proxyValue === '*'; const enableSocksProxy = this._options.enableSocksProxy && proxyValue === '*';
@ -132,17 +132,12 @@ export class PlaywrightServer {
} catch (e) { } catch (e) {
} }
const headlessHeader = request.headers['x-playwright-headless'];
const headlessValue = url.searchParams.get('headless') || (Array.isArray(headlessHeader) ? headlessHeader[0] : headlessHeader);
if (headlessValue && headlessValue !== '0')
launchOptions.headless = true;
const log = newLogger(); const log = newLogger();
log(`serving connection: ${request.url}`); log(`serving connection: ${request.url}`);
const connection = new PlaywrightConnection( const connection = new PlaywrightConnection(
semaphore.aquire(), semaphore.aquire(),
this._mode, ws, this._mode, ws,
{ enableSocksProxy, browserAlias, 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());
(ws as any)[kConnectionSymbol] = connection; (ws as any)[kConnectionSymbol] = connection;

View file

@ -115,8 +115,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`); throw new Error(`Unexpected browserName "${browserName}", must be one of "chromium", "firefox" or "webkit"`);
const browser = await playwright[browserName].connect(connectOptions.wsEndpoint, { const browser = await playwright[browserName].connect(connectOptions.wsEndpoint, {
headers: { headers: {
'x-playwright-browser': channel || browserName, 'x-playwright-browser': browserName,
'x-playwright-headless': headless ? '1' : '0',
'x-playwright-launch-options': JSON.stringify(launchOptions), 'x-playwright-launch-options': JSON.stringify(launchOptions),
...connectOptions.headers, ...connectOptions.headers,
}, },

View file

@ -66,7 +66,8 @@ const it = contextTest.extend<{ pageFactory: (redirectPortForTest?: number) => P
const server = new OutOfProcessPlaywrightServer(0, 3200 + testInfo.workerIndex); const server = new OutOfProcessPlaywrightServer(0, 3200 + testInfo.workerIndex);
playwrightServers.push(server); playwrightServers.push(server);
const browser = await browserType.connect({ const browser = await browserType.connect({
wsEndpoint: await server.wsEndpoint() + '?proxy=*&browser=' + (channel || browserName), wsEndpoint: await server.wsEndpoint() + '?proxy=*&browser=' + browserName,
headers: { 'x-playwright-launch-options': JSON.stringify({ channel }) },
__testHookRedirectPortForwarding: redirectPortForTest, __testHookRedirectPortForwarding: redirectPortForTest,
} as any); } as any);
browsers.push(browser); browsers.push(browser);