feat(reuse): account for the browser launch args when reusing the bro… (#16229)

feat(reuse): account for the browser launch args when reusing the browsers
This commit is contained in:
Pavel Feldman 2022-08-03 17:32:29 -07:00 committed by GitHub
parent fb76d62a2b
commit 8eca6339c2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 58 additions and 11 deletions

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('auto', ws, { enableSocksProxy: true, browserAlias, headless: true }, { playwright: null, browser: null }, log, async () => { new PlaywrightConnection('auto', ws, { enableSocksProxy: true, browserAlias, 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

@ -24,11 +24,12 @@ 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';
import type { LaunchOptions } from '../server/types';
type Options = { type Options = {
enableSocksProxy: boolean, enableSocksProxy: boolean,
browserAlias: string | null, browserAlias: string | null,
headless: boolean, launchOptions: LaunchOptions,
}; };
type PreLaunched = { type PreLaunched = {
@ -99,7 +100,7 @@ export class PlaywrightConnection {
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[executable.browserName!].launch(serverSideCallMetadata(), {
channel: executable.type === 'browser' ? undefined : executable.name, channel: executable.type === 'browser' ? undefined : executable.name,
headless: this._options.headless, headless: this._options.launchOptions?.headless,
}); });
// Close the browser on disconnect. // Close the browser on disconnect.
@ -132,15 +133,18 @@ export class PlaywrightConnection {
this._debugLog(`engaged reuse browsers mode for ${this._options.browserAlias}`); this._debugLog(`engaged reuse browsers mode for ${this._options.browserAlias}`);
const executable = this._executableForBrowerAlias(this._options.browserAlias!); const executable = this._executableForBrowerAlias(this._options.browserAlias!);
const playwright = this._preLaunched.playwright!; const playwright = this._preLaunched.playwright!;
const requestedOptions = launchOptionsHash(this._options.launchOptions);
let browser = playwright.allBrowsers().find(b => b.options.name === executable.browserName); let browser = playwright.allBrowsers().find(b => {
const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
return existingOptions === requestedOptions;
});
const remaining = playwright.allBrowsers().filter(b => b !== browser); const remaining = playwright.allBrowsers().filter(b => b !== browser);
for (const r of remaining) for (const r of remaining)
await r.close(); await r.close();
if (!browser) { if (!browser) {
browser = await playwright[executable.browserName!].launch(serverSideCallMetadata(), { browser = await playwright[executable.browserName!].launch(serverSideCallMetadata(), {
channel: executable.type === 'browser' ? undefined : executable.name, ...this._options.launchOptions,
headless: false, headless: false,
}); });
browser.on(Browser.Events.Disconnected, () => { browser.on(Browser.Events.Disconnected, () => {
@ -189,3 +193,28 @@ export class PlaywrightConnection {
return executable; return executable;
} }
} }
function launchOptionsHash(options: LaunchOptions) {
const copy = { ...options };
for (const k of Object.keys(copy)) {
const key = k as keyof LaunchOptions;
if (copy[key] === defaultLaunchOptions[key])
delete copy[key];
}
for (const key of optionsThatAllowBrowserReuse)
delete copy[key];
return JSON.stringify(copy);
}
const defaultLaunchOptions: LaunchOptions = {
ignoreAllDefaultArgs: false,
handleSIGINT: false,
handleSIGTERM: false,
handleSIGHUP: false,
headless: true,
devtools: false,
};
const optionsThatAllowBrowserReuse: (keyof LaunchOptions)[] = [
'headless',
];

View file

@ -23,6 +23,7 @@ import { createPlaywright } from '../server/playwright';
import { PlaywrightConnection } from './playwrightConnection'; import { PlaywrightConnection } from './playwrightConnection';
import { assert } from '../utils'; import { assert } from '../utils';
import { serverSideCallMetadata } from '../server/instrumentation'; import { serverSideCallMetadata } from '../server/instrumentation';
import type { LaunchOptions } from '../server/types';
const debugLog = debug('pw:server'); const debugLog = debug('pw:server');
@ -120,17 +121,28 @@ 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 browserAlias = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
const headlessHeader = request.headers['x-playwright-headless'];
const headlessValue = url.searchParams.get('headless') || (Array.isArray(headlessHeader) ? headlessHeader[0] : headlessHeader);
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 === '*';
const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
let launchOptions: LaunchOptions = {};
try {
launchOptions = JSON.parse(Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader);
} 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;
this._clientsCount++; this._clientsCount++;
const log = newLogger(); const log = newLogger();
log(`serving connection: ${request.url}`); log(`serving connection: ${request.url}`);
const connection = new PlaywrightConnection( const connection = new PlaywrightConnection(
this._mode, ws, this._mode, ws,
{ enableSocksProxy, browserAlias, headless: headlessValue !== '0' }, { enableSocksProxy, browserAlias, launchOptions },
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null }, { playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
log, () => this._clientsCount--); log, () => this._clientsCount--);
(ws as any)[kConnectionSymbol] = connection; (ws as any)[kConnectionSymbol] = connection;

View file

@ -290,7 +290,8 @@ export class AndroidDevice extends SdkObject {
browserProcess: new ClankBrowserProcess(androidBrowser), browserProcess: new ClankBrowserProcess(androidBrowser),
proxy: options.proxy, proxy: options.proxy,
protocolLogger: helper.debugProtocolLogger(), protocolLogger: helper.debugProtocolLogger(),
browserLogsCollector: new RecentLogsCollector() browserLogsCollector: new RecentLogsCollector(),
originalLaunchOptions: {},
}; };
validateBrowserContextOptions(options, browserOptions); validateBrowserContextOptions(options, browserOptions);

View file

@ -57,6 +57,7 @@ export type BrowserOptions = PlaywrightOptions & {
browserLogsCollector: RecentLogsCollector, browserLogsCollector: RecentLogsCollector,
slowMo?: number; slowMo?: number;
wsEndpoint?: string; // Only there when connected over web socket. wsEndpoint?: string; // Only there when connected over web socket.
originalLaunchOptions: types.LaunchOptions;
}; };
export abstract class Browser extends SdkObject { export abstract class Browser extends SdkObject {

View file

@ -123,6 +123,7 @@ export abstract class BrowserType extends SdkObject {
protocolLogger, protocolLogger,
browserLogsCollector, browserLogsCollector,
wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined, wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined,
originalLaunchOptions: options,
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);

View file

@ -114,6 +114,7 @@ export class Chromium extends BrowserType {
// users in normal (launch/launchServer) mode since otherwise connectOverCDP // users in normal (launch/launchServer) mode since otherwise connectOverCDP
// does not work at all with proxies on Windows. // does not work at all with proxies on Windows.
proxy: { server: 'per-context' }, proxy: { server: 'per-context' },
originalLaunchOptions: {},
}; };
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);
progress.throwIfAborted(); progress.throwIfAborted();

View file

@ -226,6 +226,7 @@ export class Electron extends SdkObject {
artifactsDir, artifactsDir,
downloadsPath: artifactsDir, downloadsPath: artifactsDir,
tracesDir: artifactsDir, tracesDir: artifactsDir,
originalLaunchOptions: {},
}; };
validateBrowserContextOptions(contextOptions, browserOptions); validateBrowserContextOptions(contextOptions, browserOptions);
const browser = await CRBrowser.connect(chromeTransport, browserOptions); const browser = await CRBrowser.connect(chromeTransport, browserOptions);

View file

@ -106,7 +106,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
(browserType as any)._defaultLaunchOptions = undefined; (browserType as any)._defaultLaunchOptions = undefined;
}, { scope: 'worker', auto: true }], }, { scope: 'worker', auto: true }],
_connectedBrowser: [async ({ playwright, browserName, channel, headless, connectOptions }, use) => { _connectedBrowser: [async ({ playwright, browserName, channel, headless, connectOptions, launchOptions }, use) => {
if (!connectOptions) { if (!connectOptions) {
await use(undefined); await use(undefined);
return; return;
@ -117,6 +117,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
headers: { headers: {
'x-playwright-browser': channel || browserName, 'x-playwright-browser': channel || browserName,
'x-playwright-headless': headless ? '1' : '0', 'x-playwright-headless': headless ? '1' : '0',
'x-playwright-launch-options': JSON.stringify(launchOptions),
...connectOptions.headers, ...connectOptions.headers,
}, },
timeout: connectOptions.timeout ?? 3 * 60 * 1000, // 3 minutes timeout: connectOptions.timeout ?? 3 * 60 * 1000, // 3 minutes