feat(errors): append recent browser logs when browser disconnects (#4625)

This commit is contained in:
Dmitry Gozman 2020-12-08 09:35:28 -08:00 committed by GitHub
parent e1e000d264
commit be16ce4bd2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 110 additions and 48 deletions

View file

@ -21,6 +21,7 @@ import { EventEmitter } from 'events';
import { Download } from './download'; import { Download } from './download';
import { ProxySettings } from './types'; import { ProxySettings } from './types';
import { ChildProcess } from 'child_process'; import { ChildProcess } from 'child_process';
import { RecentLogsCollector } from '../utils/debugLogger';
export interface BrowserProcess { export interface BrowserProcess {
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined; onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
@ -37,6 +38,7 @@ export type BrowserOptions = types.UIOptions & {
browserProcess: BrowserProcess, browserProcess: BrowserProcess,
proxy?: ProxySettings, proxy?: ProxySettings,
protocolLogger: types.ProtocolLogger, protocolLogger: types.ProtocolLogger,
browserLogsCollector: RecentLogsCollector,
}; };
export abstract class Browser extends EventEmitter { export abstract class Browser extends EventEmitter {

View file

@ -30,6 +30,7 @@ import { TimeoutSettings } from '../utils/timeoutSettings';
import { validateHostRequirements } from './validateDependencies'; import { validateHostRequirements } from './validateDependencies';
import { isDebugMode } from '../utils/utils'; import { isDebugMode } from '../utils/utils';
import { helper } from './helper'; import { helper } from './helper';
import { RecentLogsCollector } from '../utils/debugLogger';
const mkdirAsync = util.promisify(fs.mkdir); const mkdirAsync = util.promisify(fs.mkdir);
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
@ -81,7 +82,8 @@ export abstract class BrowserType {
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> { async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> {
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined; options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir); const browserLogsCollector = new RecentLogsCollector();
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser(); await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = { const browserOptions: BrowserOptions = {
@ -93,6 +95,7 @@ export abstract class BrowserType {
browserProcess, browserProcess,
proxy: options.proxy, proxy: options.proxy,
protocolLogger, protocolLogger,
browserLogsCollector,
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);
@ -104,7 +107,7 @@ export abstract class BrowserType {
return browser; return browser;
} }
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> { private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, browserLogsCollector: RecentLogsCollector, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> {
const { const {
ignoreDefaultArgs, ignoreDefaultArgs,
ignoreAllDefaultArgs, ignoreAllDefaultArgs,
@ -172,7 +175,10 @@ export abstract class BrowserType {
handleSIGINT, handleSIGINT,
handleSIGTERM, handleSIGTERM,
handleSIGHUP, handleSIGHUP,
progress, log: (message: string) => {
progress.log(message);
browserLogsCollector.log(message);
},
stdio: 'pipe', stdio: 'pipe',
tempDirectories, tempDirectories,
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {

View file

@ -46,7 +46,7 @@ export class CRBrowser extends Browser {
private _tracingClient: CRSession | undefined; private _tracingClient: CRSession | undefined;
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> { static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
const connection = new CRConnection(transport, options.protocolLogger); const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
const browser = new CRBrowser(connection, options); const browser = new CRBrowser(connection, options);
browser._devtools = devtools; browser._devtools = devtools;
const session = connection.rootSession; const session = connection.rootSession;

View file

@ -20,8 +20,9 @@ import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../trans
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../utils/debugLogger'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types'; import { ProtocolLogger } from '../types';
import { helper } from '../helper';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected') Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -36,13 +37,15 @@ export class CRConnection extends EventEmitter {
private readonly _transport: ConnectionTransport; private readonly _transport: ConnectionTransport;
private readonly _sessions = new Map<string, CRSession>(); private readonly _sessions = new Map<string, CRSession>();
private readonly _protocolLogger: ProtocolLogger; private readonly _protocolLogger: ProtocolLogger;
private readonly _browserLogsCollector: RecentLogsCollector;
readonly rootSession: CRSession; readonly rootSession: CRSession;
_closed = false; _closed = false;
constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger) { constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
super(); super();
this._transport = transport; this._transport = transport;
this._protocolLogger = protocolLogger; this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector;
this._transport.onmessage = this._onMessage.bind(this); this._transport.onmessage = this._onMessage.bind(this);
this._transport.onclose = this._onClose.bind(this); this._transport.onclose = this._onClose.bind(this);
this.rootSession = new CRSession(this, '', 'browser', ''); this.rootSession = new CRSession(this, '', 'browser', '');
@ -79,7 +82,7 @@ export class CRConnection extends EventEmitter {
} else if (message.method === 'Target.detachedFromTarget') { } else if (message.method === 'Target.detachedFromTarget') {
const session = this._sessions.get(message.params.sessionId); const session = this._sessions.get(message.params.sessionId);
if (session) { if (session) {
session._onClosed(); session._onClosed(undefined);
this._sessions.delete(message.params.sessionId); this._sessions.delete(message.params.sessionId);
} }
} }
@ -92,8 +95,9 @@ export class CRConnection extends EventEmitter {
this._closed = true; this._closed = true;
this._transport.onmessage = undefined; this._transport.onmessage = undefined;
this._transport.onclose = undefined; this._transport.onclose = undefined;
const browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs());
for (const session of this._sessions.values()) for (const session of this._sessions.values())
session._onClosed(); session._onClosed(browserDisconnectedLogs);
this._sessions.clear(); this._sessions.clear();
Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected));
} }
@ -126,6 +130,7 @@ export class CRSession extends EventEmitter {
private readonly _sessionId: string; private readonly _sessionId: string;
private readonly _rootSessionId: string; private readonly _rootSessionId: string;
private _crashed: boolean = false; private _crashed: boolean = false;
private _browserDisconnectedLogs: string | undefined;
on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; on: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; addListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; off: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
@ -156,6 +161,8 @@ export class CRSession extends EventEmitter {
): Promise<Protocol.CommandReturnValues[T]> { ): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed) if (this._crashed)
throw new Error('Target crashed'); throw new Error('Target crashed');
if (this._browserDisconnectedLogs !== undefined)
throw new Error(`Protocol error (${method}): Browser closed.` + this._browserDisconnectedLogs);
if (!this._connection) if (!this._connection)
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`); throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
const id = this._connection._rawSend(this._sessionId, method, params); const id = this._connection._rawSend(this._sessionId, method, params);
@ -195,9 +202,11 @@ export class CRSession extends EventEmitter {
await rootSession.send('Target.detachFromTarget', { sessionId: this._sessionId }); await rootSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
} }
_onClosed() { _onClosed(browserDisconnectedLogs: string | undefined) {
this._browserDisconnectedLogs = browserDisconnectedLogs;
const errorMessage = browserDisconnectedLogs !== undefined ? 'Browser closed.' + browserDisconnectedLogs : 'Target closed.';
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`)); callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): ` + errorMessage));
this._callbacks.clear(); this._callbacks.clear();
this._connection = null; this._connection = null;
Promise.resolve().then(() => this.emit(CRSessionEvents.Disconnected)); Promise.resolve().then(() => this.emit(CRSessionEvents.Disconnected));

View file

@ -65,7 +65,7 @@ export class VideoRecorder {
executablePath, executablePath,
args, args,
stdio: 'stdin', stdio: 'stdin',
progress, log: (message: string) => progress.log(message),
tempDirectories: [], tempDirectories: [],
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {
progress.log('Closing stdin...'); progress.log('Closing stdin...');

View file

@ -25,6 +25,7 @@ import { Env } from '../processLauncher';
import { CRBrowser } from '../chromium/crBrowser'; import { CRBrowser } from '../chromium/crBrowser';
import { AndroidBrowser, AndroidClient, AndroidDevice } from './android'; import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
import { AdbBackend } from './backendAdb'; import { AdbBackend } from './backendAdb';
import { RecentLogsCollector } from '../../utils/debugLogger';
export class Clank extends BrowserType { export class Clank extends BrowserType {
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> { async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> {
@ -48,6 +49,7 @@ export class Clank extends BrowserType {
browserProcess: new ClankBrowserProcess(device, adbBrowser), browserProcess: new ClankBrowserProcess(device, adbBrowser),
proxy: options.proxy, proxy: options.proxy,
protocolLogger, protocolLogger,
browserLogsCollector: new RecentLogsCollector(),
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);

View file

@ -32,6 +32,7 @@ import { helper } from '../helper';
import { BrowserOptions, BrowserProcess } from '../browser'; import { BrowserOptions, BrowserProcess } from '../browser';
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import * as readline from 'readline'; import * as readline from 'readline';
import { RecentLogsCollector } from '../../utils/debugLogger';
export type ElectronLaunchOptionsBase = { export type ElectronLaunchOptionsBase = {
args?: string[], args?: string[],
@ -157,6 +158,7 @@ export class Electron {
electronArguments.push('--no-sandbox'); electronArguments.push('--no-sandbox');
} }
const browserLogsCollector = new RecentLogsCollector();
const { launchedProcess, gracefullyClose, kill } = await launchProcess({ const { launchedProcess, gracefullyClose, kill } = await launchProcess({
executablePath, executablePath,
args: electronArguments, args: electronArguments,
@ -164,7 +166,10 @@ export class Electron {
handleSIGINT, handleSIGINT,
handleSIGTERM, handleSIGTERM,
handleSIGHUP, handleSIGHUP,
progress, log: (message: string) => {
progress.log(message);
browserLogsCollector.log(message);
},
stdio: 'pipe', stdio: 'pipe',
cwd: options.cwd, cwd: options.cwd,
tempDirectories: [], tempDirectories: [],
@ -174,7 +179,7 @@ export class Electron {
const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/); const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/);
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]); const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger()); const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger(), browserLogsCollector);
const chromeMatch = await waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/); const chromeMatch = await waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/);
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]); const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
@ -190,6 +195,7 @@ export class Electron {
persistent: { noDefaultViewport: true }, persistent: { noDefaultViewport: true },
browserProcess, browserProcess,
protocolLogger: helper.debugProtocolLogger(), protocolLogger: helper.debugProtocolLogger(),
browserLogsCollector,
}; };
const browser = await CRBrowser.connect(chromeTransport, browserOptions); const browser = await CRBrowser.connect(chromeTransport, browserOptions);
app = new ElectronApplication(browser, nodeConnection); app = new ElectronApplication(browser, nodeConnection);

View file

@ -33,7 +33,7 @@ export class FFBrowser extends Browser {
private _version = ''; private _version = '';
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> { static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
const connection = new FFConnection(transport, options.protocolLogger); const connection = new FFConnection(transport, options.protocolLogger, options.browserLogsCollector);
const browser = new FFBrowser(connection, options); const browser = new FFBrowser(connection, options);
const promises: Promise<any>[] = [ const promises: Promise<any>[] = [
connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }), connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }),

View file

@ -20,8 +20,9 @@ import { assert } from '../../utils/utils';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../utils/debugLogger'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types'; import { ProtocolLogger } from '../types';
import { helper } from '../helper';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('Disconnected'), Disconnected: Symbol('Disconnected'),
@ -36,6 +37,7 @@ export class FFConnection extends EventEmitter {
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>; private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
private _transport: ConnectionTransport; private _transport: ConnectionTransport;
private readonly _protocolLogger: ProtocolLogger; private readonly _protocolLogger: ProtocolLogger;
private readonly _browserLogsCollector: RecentLogsCollector;
readonly _sessions: Map<string, FFSession>; readonly _sessions: Map<string, FFSession>;
_closed: boolean; _closed: boolean;
@ -45,10 +47,11 @@ export class FFConnection extends EventEmitter {
removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; removeListener: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; once: <T extends keyof Protocol.Events | symbol>(event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this;
constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger) { constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
super(); super();
this._transport = transport; this._transport = transport;
this._protocolLogger = protocolLogger; this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector;
this._lastId = 0; this._lastId = 0;
this._callbacks = new Map(); this._callbacks = new Map();
@ -68,6 +71,7 @@ export class FFConnection extends EventEmitter {
method: T, method: T,
params?: Protocol.CommandParameters[T] params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> { ): Promise<Protocol.CommandReturnValues[T]> {
this._checkClosed(method);
const id = this.nextMessageId(); const id = this.nextMessageId();
this._rawSend({id, method, params}); this._rawSend({id, method, params});
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -79,6 +83,11 @@ export class FFConnection extends EventEmitter {
return ++this._lastId; return ++this._lastId;
} }
_checkClosed(method: string) {
if (this._closed)
throw new Error(`Protocol error (${method}): Browser closed.` + helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()));
}
_rawSend(message: ProtocolRequest) { _rawSend(message: ProtocolRequest) {
this._protocolLogger('send', message); this._protocolLogger('send', message);
this._transport.send(message); this._transport.send(message);
@ -111,12 +120,13 @@ export class FFConnection extends EventEmitter {
this._closed = true; this._closed = true;
this._transport.onmessage = undefined; this._transport.onmessage = undefined;
this._transport.onclose = undefined; this._transport.onclose = undefined;
for (const callback of this._callbacks.values()) const formattedBrowserLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs());
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
for (const session of this._sessions.values()) for (const session of this._sessions.values())
session.dispose(); session.dispose(formattedBrowserLogs);
this._sessions.clear(); this._sessions.clear();
for (const callback of this._callbacks.values())
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Browser closed.` + formattedBrowserLogs));
this._callbacks.clear();
Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected)); Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected));
} }
@ -175,6 +185,7 @@ export class FFSession extends EventEmitter {
): Promise<Protocol.CommandReturnValues[T]> { ): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed) if (this._crashed)
throw new Error('Page crashed'); throw new Error('Page crashed');
this._connection._checkClosed(method);
if (this._disposed) if (this._disposed)
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`); throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
const id = this._connection.nextMessageId(); const id = this._connection.nextMessageId();
@ -202,9 +213,9 @@ export class FFSession extends EventEmitter {
} }
} }
dispose() { dispose(formattedBrowserLogs?: string) {
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`)); callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.` + formattedBrowserLogs));
this._callbacks.clear(); this._callbacks.clear();
this._disposed = true; this._disposed = true;
this._connection._sessions.delete(this._sessionId); this._connection._sessions.delete(this._sessionId);

View file

@ -120,6 +120,12 @@ class Helper {
debugLogger.log('protocol', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message)); debugLogger.log('protocol', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message));
}; };
} }
static formatBrowserLogs(logs: string[]) {
if (!logs.length)
return '';
return '\n' + '='.repeat(20) + ' Browser output: ' + '='.repeat(20) + '\n' + logs.join('\n');
}
} }
export const helper = Helper; export const helper = Helper;

View file

@ -19,7 +19,6 @@ import * as childProcess from 'child_process';
import * as readline from 'readline'; import * as readline from 'readline';
import * as removeFolder from 'rimraf'; import * as removeFolder from 'rimraf';
import { helper } from './helper'; import { helper } from './helper';
import { Progress } from './progress';
import * as types from './types'; import * as types from './types';
import { isUnderTest } from '../utils/utils'; import { isUnderTest } from '../utils/utils';
@ -41,7 +40,7 @@ export type LaunchProcessOptions = {
// Note: attemptToGracefullyClose should reject if it does not close the browser. // Note: attemptToGracefullyClose should reject if it does not close the browser.
attemptToGracefullyClose: () => Promise<any>, attemptToGracefullyClose: () => Promise<any>,
onExit: (exitCode: number | null, signal: string | null) => void, onExit: (exitCode: number | null, signal: string | null) => void,
progress: Progress, log: (message: string) => void,
}; };
type LaunchResult = { type LaunchResult = {
@ -65,9 +64,8 @@ if (maxListeners !== 0)
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> { export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
const cleanup = () => helper.removeFolders(options.tempDirectories); const cleanup = () => helper.removeFolders(options.tempDirectories);
const progress = options.progress;
const stdio: ('ignore' | 'pipe')[] = options.stdio === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe']; const stdio: ('ignore' | 'pipe')[] = options.stdio === 'pipe' ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['pipe', 'pipe', 'pipe'];
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`); options.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
const spawnedProcess = childProcess.spawn( const spawnedProcess = childProcess.spawn(
options.executablePath, options.executablePath,
options.args, options.args,
@ -93,16 +91,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
}); });
return cleanup().then(() => failedPromise).then(e => Promise.reject(e)); return cleanup().then(() => failedPromise).then(e => Promise.reject(e));
} }
progress.log(`<launched> pid=${spawnedProcess.pid}`); options.log(`<launched> pid=${spawnedProcess.pid}`);
const stdout = readline.createInterface({ input: spawnedProcess.stdout }); const stdout = readline.createInterface({ input: spawnedProcess.stdout });
stdout.on('line', (data: string) => { stdout.on('line', (data: string) => {
progress.log('[out] ' + data); options.log('[out] ' + data);
}); });
const stderr = readline.createInterface({ input: spawnedProcess.stderr }); const stderr = readline.createInterface({ input: spawnedProcess.stderr });
stderr.on('line', (data: string) => { stderr.on('line', (data: string) => {
progress.log('[err] ' + data); options.log('[err] ' + data);
}); });
let processClosed = false; let processClosed = false;
@ -111,7 +109,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
let fulfillCleanup = () => {}; let fulfillCleanup = () => {};
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f); const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
spawnedProcess.once('exit', (exitCode, signal) => { spawnedProcess.once('exit', (exitCode, signal) => {
progress.log(`<process did exit: exitCode=${exitCode}, signal=${signal}>`); options.log(`<process did exit: exitCode=${exitCode}, signal=${signal}>`);
processClosed = true; processClosed = true;
helper.removeEventListeners(listeners); helper.removeEventListeners(listeners);
gracefullyCloseSet.delete(gracefullyClose); gracefullyCloseSet.delete(gracefullyClose);
@ -147,21 +145,21 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
// reentrancy to this function, for example user sends SIGINT second time. // reentrancy to this function, for example user sends SIGINT second time.
// In this case, let's forcefully kill the process. // In this case, let's forcefully kill the process.
if (gracefullyClosing) { if (gracefullyClosing) {
progress.log(`<forecefully close>`); options.log(`<forecefully close>`);
killProcess(); killProcess();
await waitForClose; // Ensure the process is dead and we called options.onkill. await waitForClose; // Ensure the process is dead and we called options.onkill.
return; return;
} }
gracefullyClosing = true; gracefullyClosing = true;
progress.log(`<gracefully close start>`); options.log(`<gracefully close start>`);
await options.attemptToGracefullyClose().catch(() => killProcess()); await options.attemptToGracefullyClose().catch(() => killProcess());
await waitForCleanup; // Ensure the process is dead and we have cleaned up. await waitForCleanup; // Ensure the process is dead and we have cleaned up.
progress.log(`<gracefully close end>`); options.log(`<gracefully close end>`);
} }
// This method has to be sync to be used as 'exit' event handler. // This method has to be sync to be used as 'exit' event handler.
function killProcess() { function killProcess() {
progress.log(`<kill>`); options.log(`<kill>`);
helper.removeEventListeners(listeners); helper.removeEventListeners(listeners);
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) { if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
// Force kill the browser. // Force kill the browser.

View file

@ -52,7 +52,7 @@ export class WKBrowser extends Browser {
constructor(transport: ConnectionTransport, options: BrowserOptions) { constructor(transport: ConnectionTransport, options: BrowserOptions) {
super(options); super(options);
this._connection = new WKConnection(transport, this._onDisconnect.bind(this), options.protocolLogger); this._connection = new WKConnection(transport, this._onDisconnect.bind(this), options.protocolLogger, options.browserLogsCollector);
this._browserSession = this._connection.browserSession; this._browserSession = this._connection.browserSession;
this._eventListeners = [ this._eventListeners = [
helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)), helper.addEventListener(this._browserSession, 'Playwright.pageProxyCreated', this._onPageProxyCreated.bind(this)),
@ -69,7 +69,7 @@ export class WKBrowser extends Browser {
_onDisconnect() { _onDisconnect() {
for (const wkPage of this._wkPages.values()) for (const wkPage of this._wkPages.values())
wkPage.dispose(); wkPage.dispose(true);
this._didClose(); this._didClose();
} }
@ -162,7 +162,7 @@ export class WKBrowser extends Browser {
if (!wkPage) if (!wkPage)
return; return;
wkPage.didClose(); wkPage.didClose();
wkPage.dispose(); wkPage.dispose(false);
this._wkPages.delete(pageProxyId); this._wkPages.delete(pageProxyId);
} }

View file

@ -20,8 +20,9 @@ import { assert } from '../../utils/utils';
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport'; import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
import { Protocol } from './protocol'; import { Protocol } from './protocol';
import { rewriteErrorMessage } from '../../utils/stackTrace'; import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../utils/debugLogger'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types'; import { ProtocolLogger } from '../types';
import { helper } from '../helper';
// WKPlaywright uses this special id to issue Browser.close command which we // WKPlaywright uses this special id to issue Browser.close command which we
// should ignore. // should ignore.
@ -36,16 +37,18 @@ export class WKConnection {
private readonly _transport: ConnectionTransport; private readonly _transport: ConnectionTransport;
private readonly _onDisconnect: () => void; private readonly _onDisconnect: () => void;
private readonly _protocolLogger: ProtocolLogger; private readonly _protocolLogger: ProtocolLogger;
readonly _browserLogsCollector: RecentLogsCollector;
private _lastId = 0; private _lastId = 0;
private _closed = false; private _closed = false;
readonly browserSession: WKSession; readonly browserSession: WKSession;
constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger) { constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
this._transport = transport; this._transport = transport;
this._transport.onmessage = this._dispatchMessage.bind(this); this._transport.onmessage = this._dispatchMessage.bind(this);
this._transport.onclose = this._onClose.bind(this); this._transport.onclose = this._onClose.bind(this);
this._onDisconnect = onDisconnect; this._onDisconnect = onDisconnect;
this._protocolLogger = protocolLogger; this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector;
this.browserSession = new WKSession(this, '', 'Browser has been closed.', (message: any) => { this.browserSession = new WKSession(this, '', 'Browser has been closed.', (message: any) => {
this.rawSend(message); this.rawSend(message);
}); });
@ -76,7 +79,7 @@ export class WKConnection {
this._closed = true; this._closed = true;
this._transport.onmessage = undefined; this._transport.onmessage = undefined;
this._transport.onclose = undefined; this._transport.onclose = undefined;
this.browserSession.dispose(); this.browserSession.dispose(true);
this._onDisconnect(); this._onDisconnect();
} }
@ -148,7 +151,9 @@ export class WKSession extends EventEmitter {
return this._disposed; return this._disposed;
} }
dispose() { dispose(disconnected: boolean) {
if (disconnected)
this.errorText = 'Browser closed.' + helper.formatBrowserLogs(this.connection._browserLogsCollector.recentLogs());
for (const callback of this._callbacks.values()) for (const callback of this._callbacks.values())
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): ${this.errorText}`)); callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): ${this.errorText}`));
this._callbacks.clear(); this._callbacks.clear();

View file

@ -215,11 +215,11 @@ export class WKPage implements PageDelegate {
private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) { private _onTargetDestroyed(event: Protocol.Target.targetDestroyedPayload) {
const { targetId, crashed } = event; const { targetId, crashed } = event;
if (this._provisionalPage && this._provisionalPage._session.sessionId === targetId) { if (this._provisionalPage && this._provisionalPage._session.sessionId === targetId) {
this._provisionalPage._session.dispose(); this._provisionalPage._session.dispose(false);
this._provisionalPage.dispose(); this._provisionalPage.dispose();
this._provisionalPage = null; this._provisionalPage = null;
} else if (this._session.sessionId === targetId) { } else if (this._session.sessionId === targetId) {
this._session.dispose(); this._session.dispose(false);
helper.removeEventListeners(this._sessionListeners); helper.removeEventListeners(this._sessionListeners);
if (crashed) { if (crashed) {
this._session.markAsCrashed(); this._session.markAsCrashed();
@ -232,14 +232,14 @@ export class WKPage implements PageDelegate {
this._page._didClose(); this._page._didClose();
} }
dispose() { dispose(disconnected: boolean) {
this._pageProxySession.dispose(); this._pageProxySession.dispose(disconnected);
helper.removeEventListeners(this._sessionListeners); helper.removeEventListeners(this._sessionListeners);
helper.removeEventListeners(this._eventListeners); helper.removeEventListeners(this._eventListeners);
if (this._session) if (this._session)
this._session.dispose(); this._session.dispose(disconnected);
if (this._provisionalPage) { if (this._provisionalPage) {
this._provisionalPage._session.dispose(); this._provisionalPage._session.dispose(disconnected);
this._provisionalPage.dispose(); this._provisionalPage.dispose();
this._provisionalPage = null; this._provisionalPage = null;
} }

View file

@ -67,7 +67,7 @@ export class WKWorkers {
const workerSession = this._workerSessions.get(event.workerId)!; const workerSession = this._workerSessions.get(event.workerId)!;
if (!workerSession) if (!workerSession)
return; return;
workerSession.dispose(); workerSession.dispose(false);
this._workerSessions.delete(event.workerId); this._workerSessions.delete(event.workerId);
this._page._removeWorker(event.workerId); this._page._removeWorker(event.workerId);
}) })

View file

@ -61,3 +61,20 @@ class DebugLogger {
} }
export const debugLogger = new DebugLogger(); export const debugLogger = new DebugLogger();
const kLogCount = 50;
export class RecentLogsCollector {
private _logs: string[] = [];
log(message: string) {
this._logs.push(message);
if (this._logs.length === kLogCount * 2)
this._logs.splice(0, kLogCount);
}
recentLogs(): string[] {
if (this._logs.length > kLogCount)
return this._logs.slice(-kLogCount);
return this._logs;
}
}