fix(server): tidy up BrowserServer.close methods (#426)

This commit is contained in:
Dmitry Gozman 2020-01-08 13:55:38 -08:00 committed by GitHub
parent cd02dd8f88
commit 28bad69093
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 105 additions and 97 deletions

View file

@ -235,8 +235,9 @@ export class CRBrowser extends browser.Browser {
}
async close() {
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f));
await this._connection.rootSession.send('Browser.close');
this.disconnect();
await disconnected;
}
browserTarget(): CRTarget {

View file

@ -149,7 +149,9 @@ export class FFBrowser extends browser.Browser {
async close() {
helper.removeEventListeners(this._eventListeners);
const disconnected = new Promise(f => this._connection.once(ConnectionEvents.Disconnected, f));
await this._connection.send('Browser.close');
await disconnected;
}
_createBrowserContext(browserContextId: string | null, options: BrowserContextOptions): BrowserContext {

View file

@ -57,10 +57,12 @@ export type LaunchOptions = ChromeArgOptions & SlowMoOptions & {
export class CRBrowserServer {
private _process: ChildProcess;
private _gracefullyClose: () => Promise<void>;
private _connectOptions: CRConnectOptions;
constructor(process: ChildProcess, connectOptions: CRConnectOptions) {
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, connectOptions: CRConnectOptions) {
this._process = process;
this._gracefullyClose = gracefullyClose;
this._connectOptions = connectOptions;
}
@ -81,10 +83,7 @@ export class CRBrowserServer {
}
async close(): Promise<void> {
const transport = await createTransport(this._connectOptions);
const connection = new CRConnection(transport);
await connection.rootSession.send('Browser.close');
connection.dispose();
await this._gracefullyClose();
}
}
@ -151,7 +150,7 @@ export class CRPlaywright {
const usePipe = chromeArguments.includes('--remote-debugging-pipe');
const launchedProcess = await launchProcess({
const { launchedProcess, gracefullyClose } = await launchProcess({
executablePath: chromeExecutable,
args: chromeArguments,
env,
@ -160,33 +159,31 @@ export class CRPlaywright {
handleSIGHUP,
dumpio,
pipe: usePipe,
tempDir: temporaryUserDataDir
}, () => {
if (temporaryUserDataDir || !server)
return Promise.reject();
return server.close();
tempDir: temporaryUserDataDir,
attemptToGracefullyClose: async () => {
if (!connectOptions)
return Promise.reject();
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since
// our connection is tolerant to unknown responses.
const transport = await createTransport(connectOptions);
const connection = new CRConnection(transport);
connection.rootSession.send('Browser.close');
},
});
let server: CRBrowserServer | undefined;
try {
let connectOptions: CRConnectOptions | undefined;
let browserWSEndpoint: string = '';
if (!usePipe) {
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${this._revision}`);
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
browserWSEndpoint = match[1];
connectOptions = { browserWSEndpoint, slowMo };
} else {
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
connectOptions = { slowMo, transport };
}
server = new CRBrowserServer(launchedProcess, connectOptions);
return server;
} catch (e) {
if (server)
await server.close();
throw e;
let connectOptions: CRConnectOptions | undefined;
if (!usePipe) {
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Chrome! The only Chrome revision guaranteed to work is r${this._revision}`);
const match = await waitForLine(launchedProcess, launchedProcess.stderr, /^DevTools listening on (ws:\/\/.*)$/, timeout, timeoutError);
const browserWSEndpoint = match[1];
connectOptions = { browserWSEndpoint, slowMo };
} else {
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
connectOptions = { slowMo, transport };
}
return new CRBrowserServer(launchedProcess, gracefullyClose, connectOptions);
}
async connect(options: CRConnectOptions): Promise<CRBrowser> {

View file

@ -33,10 +33,12 @@ import { assert } from '../helper';
export class FFBrowserServer {
private _process: ChildProcess;
private _gracefullyClose: () => Promise<void>;
private _connectOptions: FFConnectOptions;
constructor(process: ChildProcess, connectOptions: FFConnectOptions) {
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, connectOptions: FFConnectOptions) {
this._process = process;
this._gracefullyClose = gracefullyClose;
this._connectOptions = connectOptions;
}
@ -57,10 +59,7 @@ export class FFBrowserServer {
}
async close(): Promise<void> {
const transport = await createTransport(this._connectOptions);
const connection = new FFConnection(transport);
await connection.send('Browser.close');
connection.dispose();
await this._gracefullyClose();
}
}
@ -123,7 +122,10 @@ export class FFPlaywright {
throw new Error(missingText);
firefoxExecutable = executablePath;
}
const launchedProcess = await launchProcess({
let connectOptions: FFConnectOptions | undefined = undefined;
const { launchedProcess, gracefullyClose } = await launchProcess({
executablePath: firefoxExecutable,
args: firefoxArguments,
env: os.platform() === 'linux' ? {
@ -136,25 +138,24 @@ export class FFPlaywright {
handleSIGHUP,
dumpio,
pipe: false,
tempDir: temporaryProfileDir
}, () => {
if (temporaryProfileDir || !server)
return Promise.reject();
server.close();
tempDir: temporaryProfileDir,
attemptToGracefullyClose: async () => {
if (!connectOptions)
return Promise.reject();
// We try to gracefully close to prevent crash reporting and core dumps.
// Note that we don't support pipe yet, so there is no issue
// with reusing the same connection - we can always create a new one.
const transport = await createTransport(connectOptions);
const connection = new FFConnection(transport);
connection.send('Browser.close');
},
});
let server: FFBrowserServer | undefined;
try {
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
const url = match[1];
server = new FFBrowserServer(launchedProcess, { browserWSEndpoint: url, slowMo });
return server;
} catch (e) {
if (server)
await server.close();
throw e;
}
const timeoutError = new TimeoutError(`Timed out after ${timeout} ms while trying to connect to Firefox!`);
const match = await waitForLine(launchedProcess, launchedProcess.stdout, /^Juggler listening on (ws:\/\/.*)$/, timeout, timeoutError);
const url = match[1];
connectOptions = { browserWSEndpoint: url, slowMo };
return new FFBrowserServer(launchedProcess, gracefullyClose, connectOptions);
}
async connect(options: FFConnectOptions): Promise<FFBrowser> {

View file

@ -36,9 +36,14 @@ export type LaunchProcessOptions = {
dumpio?: boolean,
pipe?: boolean,
tempDir?: string,
// Note: attemptToGracefullyClose should reject if it does not close the browser.
attemptToGracefullyClose: () => Promise<any>,
};
export async function launchProcess(options: LaunchProcessOptions, attemptToGracefullyClose: () => Promise<any>): Promise<childProcess.ChildProcess> {
type LaunchResult = { launchedProcess: childProcess.ChildProcess, gracefullyClose: () => Promise<void> };
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
let stdio: ('ignore' | 'pipe')[] = ['pipe', 'pipe', 'pipe'];
if (options.pipe) {
if (options.dumpio)
@ -61,7 +66,7 @@ export async function launchProcess(options: LaunchProcessOptions, attemptToGrac
if (!spawnedProcess.pid) {
let reject: (e: Error) => void;
const result = new Promise<childProcess.ChildProcess>((f, r) => reject = r);
const result = new Promise<LaunchResult>((f, r) => reject = r);
spawnedProcess.once('error', error => {
reject(new Error('Failed to launch browser: ' + error));
});
@ -96,12 +101,20 @@ export async function launchProcess(options: LaunchProcessOptions, attemptToGrac
listeners.push(helper.addEventListener(process, 'SIGTERM', gracefullyClose));
if (options.handleSIGHUP)
listeners.push(helper.addEventListener(process, 'SIGHUP', gracefullyClose));
return spawnedProcess;
let gracefullyClosing = false;
return { launchedProcess: spawnedProcess, gracefullyClose };
async function gracefullyClose(): Promise<void> {
helper.removeEventListeners(listeners);
attemptToGracefullyClose().catch(() => killProcess());
// We keep listeners until we are done, to handle 'exit' and 'SIGINT' while
// asynchronously closing to prevent zombie processes. This might introduce
// reentrancy to this function.
if (gracefullyClosing)
return;
gracefullyClosing = true;
options.attemptToGracefullyClose().catch(() => killProcess());
// TODO: forcefully kill the process after some timeout.
await waitForProcessToClose;
helper.removeEventListeners(listeners);
}
// This method has to be sync to be used as 'exit' event handler.

View file

@ -19,9 +19,8 @@ import { BrowserFetcher, BrowserFetcherOptions, OnProgressCallback, BrowserFetch
import { DeviceDescriptors } from '../deviceDescriptors';
import * as Errors from '../errors';
import * as types from '../types';
import { WKBrowser } from '../webkit/wkBrowser';
import { WKConnectOptions, createTransport } from '../webkit/wkBrowser';
import { WKConnection } from '../webkit/wkConnection';
import { WKBrowser, createTransport } from '../webkit/wkBrowser';
import { WKConnectOptions } from '../webkit/wkBrowser';
import { execSync, ChildProcess } from 'child_process';
import { PipeTransport } from './pipeTransport';
import { launchProcess } from './processLauncher';
@ -29,6 +28,7 @@ import * as path from 'path';
import * as util from 'util';
import * as os from 'os';
import { assert } from '../helper';
import { kBrowserCloseMessageId } from '../webkit/wkConnection';
export type LaunchOptions = {
ignoreDefaultArgs?: boolean,
@ -46,10 +46,12 @@ export type LaunchOptions = {
export class WKBrowserServer {
private _process: ChildProcess;
private _gracefullyClose: () => Promise<void>;
private _connectOptions: WKConnectOptions;
constructor(process: ChildProcess, connectOptions: WKConnectOptions) {
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, connectOptions: WKConnectOptions) {
this._process = process;
this._gracefullyClose = gracefullyClose;
this._connectOptions = connectOptions;
}
@ -66,10 +68,7 @@ export class WKBrowserServer {
}
async close(): Promise<void> {
const transport = await createTransport(this._connectOptions);
const connection = WKConnection.from(transport);
await connection.send('Browser.close');
connection.dispose();
await this._gracefullyClose();
}
}
@ -125,7 +124,9 @@ export class WKPlaywright {
if (process.platform === 'darwin' && options.headless !== false)
webkitArguments.push('--headless');
const launchedProcess = await launchProcess({
let connectOptions: WKConnectOptions | undefined = undefined;
const { launchedProcess, gracefullyClose } = await launchProcess({
executablePath: webkitExecutable,
args: webkitArguments,
env,
@ -134,23 +135,20 @@ export class WKPlaywright {
handleSIGHUP,
dumpio,
pipe: true,
tempDir: null
}, () => {
if (!server)
return Promise.reject();
server.close();
tempDir: null,
attemptToGracefullyClose: async () => {
if (!connectOptions)
return Promise.reject();
// We try to gracefully close to prevent crash reporting and core dumps.
const transport = await createTransport(connectOptions);
const message = JSON.stringify({method: 'Browser.close', params: {}, id: kBrowserCloseMessageId});
transport.send(message);
},
});
let server: WKBrowserServer | undefined;
try {
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
server = new WKBrowserServer(launchedProcess, { transport, slowMo });
return server;
} catch (e) {
if (server)
await server.close();
throw e;
}
const transport = new PipeTransport(launchedProcess.stdio[3] as NodeJS.WritableStream, launchedProcess.stdio[4] as NodeJS.ReadableStream);
connectOptions = { transport, slowMo };
return new WKBrowserServer(launchedProcess, gracefullyClose, connectOptions);
}
executablePath(): string {

View file

@ -22,6 +22,7 @@ import * as network from '../network';
import { Page } from '../page';
import { ConnectionTransport, SlowMoTransport } from '../transport';
import * as types from '../types';
import { Events } from '../events';
import { Protocol } from './protocol';
import { WKConnection, WKConnectionEvents, WKPageProxySession } from './wkConnection';
import { WKPageProxy } from './wkPageProxy';
@ -51,7 +52,8 @@ export class WKBrowser extends browser.Browser {
constructor(transport: ConnectionTransport) {
super();
this._connection = WKConnection.from(transport);
this._connection = new WKConnection(transport);
this._connection.on(WKConnectionEvents.Disconnected, () => this.emit(Events.Browser.Disconnected));
this._defaultContext = this._createBrowserContext(undefined, {});
@ -127,7 +129,9 @@ export class WKBrowser extends browser.Browser {
async close() {
helper.removeEventListeners(this._eventListeners);
const disconnected = new Promise(f => this._connection.once(WKConnectionEvents.Disconnected, f));
await this._connection.send('Browser.close');
await disconnected;
}
_createBrowserContext(browserContextId: string | undefined, options: BrowserContextOptions): BrowserContext {

View file

@ -24,6 +24,7 @@ const debugProtocol = platform.debug('playwright:protocol');
const debugWrappedMessage = platform.debug('wrapped');
export const WKConnectionEvents = {
Disconnected: Symbol('Disconnected'),
PageProxyCreated: Symbol('ConnectionEvents.PageProxyCreated'),
PageProxyDestroyed: Symbol('Connection.PageProxyDestroyed')
};
@ -34,7 +35,7 @@ export const WKPageProxySessionEvents = {
DidCommitProvisionalTarget: Symbol('PageProxyEvents.DidCommitProvisionalTarget'),
};
const kConnectionSymbol = Symbol();
export const kBrowserCloseMessageId = -9999;
export class WKConnection extends platform.EventEmitter {
private _lastId = 0;
@ -44,15 +45,6 @@ export class WKConnection extends platform.EventEmitter {
private _closed = false;
static from(transport: ConnectionTransport): WKConnection {
let connection = (transport as any)[kConnectionSymbol];
if (!connection) {
connection = new WKConnection(transport);
(transport as any)[kConnectionSymbol] = connection;
}
return connection;
}
constructor(transport: ConnectionTransport) {
super();
this._transport = transport;
@ -96,7 +88,7 @@ export class WKConnection extends platform.EventEmitter {
callback.reject(createProtocolError(callback.error, callback.method, object));
else
callback.resolve(object.result);
} else {
} else if (object.id !== kBrowserCloseMessageId) {
assert(this._closed, 'Received response for unknown callback: ' + object.id);
}
} else {
@ -135,6 +127,7 @@ export class WKConnection extends platform.EventEmitter {
for (const pageProxySession of this._pageProxySessions.values())
pageProxySession.dispose();
this._pageProxySessions.clear();
this.emit(WKConnectionEvents.Disconnected);
}
dispose() {

View file

@ -27,7 +27,6 @@ import { WKWorkers } from './wkWorkers';
import { Page, PageDelegate, Coverage } from '../page';
import { Protocol } from './protocol';
import * as dialog from '../dialog';
import { WKBrowser } from './wkBrowser';
import { BrowserContext } from '../browserContext';
import { RawMouseImpl, RawKeyboardImpl } from './wkInput';
import * as types from '../types';

View file

@ -34,7 +34,7 @@ module.exports.describe = function({testRunner, defaultBrowserOptions, playwrigh
let output = '';
res.stdout.on('data', data => {
output += data;
if (output.indexOf('\n'))
if (output.indexOf('\n') !== -1)
wsEndPointCallback(output.substring(0, output.indexOf('\n')));
});
const browser = await playwright.connect({ browserWSEndpoint: await wsEndPointPromise });