chore: print the launch error message to console (#2304)

This commit is contained in:
Pavel Feldman 2020-05-20 00:10:10 -07:00 committed by GitHub
parent e658a3e48a
commit e558f0516b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 198 additions and 111 deletions

View file

@ -41,24 +41,32 @@ export function logError(logger: InnerLogger): (error: Error) => void {
} }
export class RootLogger implements InnerLogger { export class RootLogger implements InnerLogger {
private _userSink: Logger | undefined; private _logger = new MultiplexingLogger();
private _debugSink: DebugLoggerSink;
constructor(userSink: Logger | undefined) { constructor(userSink: Logger | undefined) {
this._userSink = userSink; if (userSink)
this._debugSink = new DebugLoggerSink(); this._logger.add('user', userSink);
this._logger.add('debug', new DebugLogger());
} }
_isLogEnabled(log: Log): boolean { _isLogEnabled(log: Log): boolean {
return (this._userSink && this._userSink.isEnabled(log.name, log.severity || 'info')) || return this._logger.isEnabled(log.name, log.severity || 'info');
this._debugSink.isEnabled(log.name, log.severity || 'info');
} }
_log(log: Log, message: string | Error, ...args: any[]) { _log(log: Log, message: string | Error, ...args: any[]) {
if (this._userSink && this._userSink.isEnabled(log.name, log.severity || 'info')) if (this._logger.isEnabled(log.name, log.severity || 'info'))
this._userSink.log(log.name, log.severity || 'info', message, args, log.color ? { color: log.color } : {}); this._logger.log(log.name, log.severity || 'info', message, args, log.color ? { color: log.color } : {});
if (this._debugSink.isEnabled(log.name, log.severity || 'info')) }
this._debugSink.log(log.name, log.severity || 'info', message, args, log.color ? { color: log.color } : {});
startLaunchRecording() {
this._logger.add(`launch`, new RecordingLogger('browser'));
}
stopLaunchRecording(): string {
const logger = this._logger.remove(`launch`) as RecordingLogger;
if (logger)
return logger.recording();
return '';
} }
} }
@ -72,7 +80,55 @@ const colorMap = new Map<string, number>([
['reset', 0], ['reset', 0],
]); ]);
class DebugLoggerSink implements Logger { class MultiplexingLogger implements Logger {
private _loggers = new Map<string, Logger>();
add(id: string, logger: Logger) {
this._loggers.set(id, logger);
}
remove(id: string): Logger | undefined {
const logger = this._loggers.get(id);
this._loggers.delete(id);
return logger;
}
isEnabled(name: string, severity: LoggerSeverity): boolean {
for (const logger of this._loggers.values()) {
if (logger.isEnabled(name, severity))
return true;
}
return false;
}
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
for (const logger of this._loggers.values())
logger.log(name, severity, message, args, hints);
}
}
export class RecordingLogger implements Logger {
private _prefix: string;
private _recording: string[] = [];
constructor(prefix: string) {
this._prefix = prefix;
}
isEnabled(name: string, severity: LoggerSeverity): boolean {
return name.startsWith(this._prefix);
}
log(name: string, severity: LoggerSeverity, message: string | Error, args: any[], hints: { color?: string }) {
this._recording.push(String(message));
}
recording(): string {
return this._recording.join('\n');
}
}
class DebugLogger implements Logger {
private _debuggers = new Map<string, debug.IDebugger>(); private _debuggers = new Map<string, debug.IDebugger>();
isEnabled(name: string, severity: LoggerSeverity): boolean { isEnabled(name: string, severity: LoggerSeverity): boolean {

View file

@ -17,6 +17,9 @@
import { ChildProcess, execSync } from 'child_process'; import { ChildProcess, execSync } from 'child_process';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { helper } from '../helper'; import { helper } from '../helper';
import { RootLogger } from '../logger';
import { TimeoutSettings } from '../timeoutSettings';
import { LaunchOptionsBase } from './browserType';
export class WebSocketWrapper { export class WebSocketWrapper {
readonly wsEndpoint: string; readonly wsEndpoint: string;
@ -48,19 +51,32 @@ export class WebSocketWrapper {
} }
export class BrowserServer extends EventEmitter { export class BrowserServer extends EventEmitter {
private _process: ChildProcess; private _process: ChildProcess | undefined;
private _gracefullyClose: () => Promise<void>; private _gracefullyClose: (() => Promise<void>) | undefined;
private _webSocketWrapper: WebSocketWrapper | null; private _webSocketWrapper: WebSocketWrapper | null = null;
private _launchOptions: LaunchOptionsBase;
readonly _logger: RootLogger;
readonly _launchDeadline: number;
constructor(process: ChildProcess, gracefullyClose: () => Promise<void>, webSocketWrapper: WebSocketWrapper | null) { constructor(options: LaunchOptionsBase) {
super(); super();
this._launchOptions = options;
this._logger = new RootLogger(options.logger);
this._launchDeadline = TimeoutSettings.computeDeadline(typeof options.timeout === 'number' ? options.timeout : 30000);
}
_initialize(process: ChildProcess, gracefullyClose: () => Promise<void>, webSocketWrapper: WebSocketWrapper | null) {
this._process = process; this._process = process;
this._gracefullyClose = gracefullyClose; this._gracefullyClose = gracefullyClose;
this._webSocketWrapper = webSocketWrapper; this._webSocketWrapper = webSocketWrapper;
} }
_isInitialized(): boolean {
return !!this._process;
}
process(): ChildProcess { process(): ChildProcess {
return this._process; return this._process!;
} }
wsEndpoint(): string { wsEndpoint(): string {
@ -68,12 +84,12 @@ export class BrowserServer extends EventEmitter {
} }
kill() { kill() {
if (this._process.pid && !this._process.killed) { if (this._process!.pid && !this._process!.killed) {
try { try {
if (process.platform === 'win32') if (process.platform === 'win32')
execSync(`taskkill /pid ${this._process.pid} /T /F`); execSync(`taskkill /pid ${this._process!.pid} /T /F`);
else else
process.kill(-this._process.pid, 'SIGKILL'); process.kill(-this._process!.pid, 'SIGKILL');
} catch (e) { } catch (e) {
// the process might have already stopped // the process might have already stopped
} }
@ -81,7 +97,7 @@ export class BrowserServer extends EventEmitter {
} }
async close(): Promise<void> { async close(): Promise<void> {
await this._gracefullyClose(); await this._gracefullyClose!();
} }
async _checkLeaks(): Promise<void> { async _checkLeaks(): Promise<void> {
@ -89,19 +105,28 @@ export class BrowserServer extends EventEmitter {
await this._webSocketWrapper.checkLeaks(); await this._webSocketWrapper.checkLeaks();
} }
async _initializeOrClose<T>(deadline: number, init: () => Promise<T>): Promise<T> { async _initializeOrClose<T>(init: () => Promise<T>): Promise<T> {
try { try {
const result = await helper.waitWithDeadline(init(), 'the browser to launch', deadline, 'pw:browser*'); let promise: Promise<T>;
if ((this._launchOptions as any).__testHookBeforeCreateBrowser)
promise = (this._launchOptions as any).__testHookBeforeCreateBrowser().then(init);
else
promise = init();
const result = await helper.waitWithDeadline(promise, 'the browser to launch', this._launchDeadline, 'pw:browser*');
this._logger.stopLaunchRecording();
return result; return result;
} catch (e) { } catch (e) {
await this._closeOrKill(deadline); e.message += '\n=============== Process output during launch: ===============\n' +
this._logger.stopLaunchRecording() +
'\n=============================================================';
await this._closeOrKill();
throw e; throw e;
} }
} }
async _closeOrKill(deadline: number): Promise<void> { private async _closeOrKill(): Promise<void> {
try { try {
await helper.waitWithDeadline(this.close(), '', deadline, ''); // The error message is ignored. await helper.waitWithDeadline(this.close(), '', this._launchDeadline, ''); // The error message is ignored.
} catch (ignored) { } catch (ignored) {
this.kill(); this.kill();
} }

View file

@ -25,7 +25,7 @@ export type BrowserArgOptions = {
devtools?: boolean, devtools?: boolean,
}; };
type LaunchOptionsBase = BrowserArgOptions & { export type LaunchOptionsBase = BrowserArgOptions & {
executablePath?: string, executablePath?: string,
ignoreDefaultArgs?: boolean | string[], ignoreDefaultArgs?: boolean | string[],
handleSIGINT?: boolean, handleSIGINT?: boolean,

View file

@ -19,7 +19,7 @@ import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
import * as util from 'util'; import * as util from 'util';
import { helper, assert, isDebugMode } from '../helper'; import { helper, assert, isDebugMode, debugAssert } from '../helper';
import { CRBrowser } from '../chromium/crBrowser'; import { CRBrowser } from '../chromium/crBrowser';
import * as ws from 'ws'; import * as ws from 'ws';
import { launchProcess } from './processLauncher'; import { launchProcess } from './processLauncher';
@ -33,7 +33,6 @@ import { ConnectionTransport, ProtocolRequest, WebSocketTransport } from '../tra
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { InnerLogger, logError, RootLogger } from '../logger'; import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { TimeoutSettings } from '../timeoutSettings';
import { CRDevTools } from '../chromium/crDevTools'; import { CRDevTools } from '../chromium/crDevTools';
export class Chromium extends AbstractBrowserType<CRBrowser> { export class Chromium extends AbstractBrowserType<CRBrowser> {
@ -51,12 +50,9 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
async launch(options: LaunchOptions = {}): Promise<CRBrowser> { async launch(options: LaunchOptions = {}): Promise<CRBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const { timeout = 30000 } = options; const browserServer = new BrowserServer(options);
const deadline = TimeoutSettings.computeDeadline(timeout); const { transport, downloadsPath } = await this._launchServer(options, 'local', browserServer);
const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local'); return await browserServer._initializeOrClose(async () => {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
let devtools = this._devtools; let devtools = this._devtools;
if ((options as any).__testHookForDevTools) { if ((options as any).__testHookForDevTools) {
devtools = this._createDevTools(); devtools = this._createDevTools();
@ -65,7 +61,7 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
return await CRBrowser.connect(transport!, { return await CRBrowser.connect(transport!, {
slowMo: options.slowMo, slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless, headful: !processBrowserArgOptions(options).headless,
logger, logger: browserServer._logger,
downloadsPath, downloadsPath,
ownedServer: browserServer, ownedServer: browserServer,
}, devtools); }, devtools);
@ -73,20 +69,19 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
} }
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
return (await this._launchServer(options, 'server')).browserServer; const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
} }
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const { timeout = 30000 } = options; const browserServer = new BrowserServer(options);
const deadline = TimeoutSettings.computeDeadline(timeout); const { transport, downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
const { transport, browserServer, downloadsPath, logger } = await this._launchServer(options, 'persistent', userDataDir); return await browserServer._initializeOrClose(async () => {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await CRBrowser.connect(transport!, { const browser = await CRBrowser.connect(transport!, {
slowMo: options.slowMo, slowMo: options.slowMo,
persistent: true, persistent: true,
logger, logger: browserServer._logger,
downloadsPath, downloadsPath,
headful: !processBrowserArgOptions(options).headless, headful: !processBrowserArgOptions(options).headless,
ownedServer: browserServer ownedServer: browserServer
@ -98,7 +93,7 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
}); });
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: InnerLogger }> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -110,7 +105,7 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
port = 0, port = 0,
} = options; } = options;
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.logger); const logger = browserServer._logger;
let temporaryUserDataDir: string | null = null; let temporaryUserDataDir: string | null = null;
if (!userDataDir) { if (!userDataDir) {
@ -136,7 +131,6 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
// Note: it is important to define these variables before launchProcess, so that we don't get // Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong. // "Cannot access 'browserServer' before initialization" if something went wrong.
let transport: PipeTransport | undefined = undefined; let transport: PipeTransport | undefined = undefined;
let browserServer: BrowserServer | undefined = undefined;
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({ const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
executablePath: chromeExecutable, executablePath: chromeExecutable,
args: chromeArguments, args: chromeArguments,
@ -148,7 +142,7 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
pipe: true, pipe: true,
tempDir: temporaryUserDataDir || undefined, tempDir: temporaryUserDataDir || undefined,
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {
assert(browserServer); debugAssert(browserServer._isInitialized());
// We try to gracefully close to prevent crash reporting and core dumps. // We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since // Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId. // our connection ignores kBrowserCloseMessageId.
@ -157,15 +151,14 @@ export class Chromium extends AbstractBrowserType<CRBrowser> {
t.send(message); t.send(message);
}, },
onkill: (exitCode, signal) => { onkill: (exitCode, signal) => {
if (browserServer) browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
}, },
}); });
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4], logger); transport = new PipeTransport(stdio[3], stdio[4], logger);
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null); browserServer._initialize(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port) : null);
return { browserServer, transport, downloadsPath, logger }; return { transport, downloadsPath };
} }
async connect(options: ConnectOptions): Promise<CRBrowser> { async connect(options: ConnectOptions): Promise<CRBrowser> {

View file

@ -167,9 +167,8 @@ export class Electron {
handleSIGINT = true, handleSIGINT = true,
handleSIGTERM = true, handleSIGTERM = true,
handleSIGHUP = true, handleSIGHUP = true,
timeout = 30000,
} = options; } = options;
const deadline = TimeoutSettings.computeDeadline(timeout); const browserServer = new BrowserServer(options);
let app: ElectronApplication | undefined = undefined; let app: ElectronApplication | undefined = undefined;
@ -193,6 +192,7 @@ export class Electron {
}, },
}); });
const deadline = browserServer._launchDeadline;
const timeoutError = new TimeoutError(`Timed out while trying to connect to Electron!`); const timeoutError = new TimeoutError(`Timed out while trying to connect to Electron!`);
const nodeMatch = await waitForLine(launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/, helper.timeUntilDeadline(deadline), timeoutError); const nodeMatch = await waitForLine(launchedProcess, launchedProcess.stderr, /^Debugger listening on (ws:\/\/.*)$/, helper.timeUntilDeadline(deadline), timeoutError);
const nodeConnection = await WebSocketTransport.connect(nodeMatch[1], transport => { const nodeConnection = await WebSocketTransport.connect(nodeMatch[1], transport => {
@ -203,7 +203,7 @@ export class Electron {
const chromeTransport = await WebSocketTransport.connect(chromeMatch[1], transport => { const chromeTransport = await WebSocketTransport.connect(chromeMatch[1], transport => {
return transport; return transport;
}, logger); }, logger);
const browserServer = new BrowserServer(launchedProcess, gracefullyClose, null); browserServer._initialize(launchedProcess, gracefullyClose, null);
const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: true, viewport: null, ownedServer: browserServer, downloadsPath: '' }); const browser = await CRBrowser.connect(chromeTransport, { headful: true, logger, persistent: true, viewport: null, ownedServer: browserServer, downloadsPath: '' });
app = new ElectronApplication(logger, browser, nodeConnection); app = new ElectronApplication(logger, browser, nodeConnection);
await app._init(); await app._init();

View file

@ -26,14 +26,13 @@ import { TimeoutError } from '../errors';
import { Events } from '../events'; import { Events } from '../events';
import { FFBrowser } from '../firefox/ffBrowser'; import { FFBrowser } from '../firefox/ffBrowser';
import { kBrowserCloseMessageId } from '../firefox/ffConnection'; import { kBrowserCloseMessageId } from '../firefox/ffConnection';
import { helper, assert } from '../helper'; import { helper, assert, debugAssert } from '../helper';
import { BrowserServer, WebSocketWrapper } from './browserServer'; import { BrowserServer, WebSocketWrapper } from './browserServer';
import { BrowserArgOptions, LaunchOptions, LaunchServerOptions, ConnectOptions, AbstractBrowserType, processBrowserArgOptions } from './browserType'; import { BrowserArgOptions, LaunchOptions, LaunchServerOptions, ConnectOptions, AbstractBrowserType, processBrowserArgOptions } from './browserType';
import { launchProcess, waitForLine } from './processLauncher'; import { launchProcess, waitForLine } from './processLauncher';
import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport'; import { ConnectionTransport, SequenceNumberMixer, WebSocketTransport } from '../transport';
import { RootLogger, InnerLogger, logError } from '../logger'; import { RootLogger, InnerLogger, logError } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { TimeoutSettings } from '../timeoutSettings';
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
@ -44,46 +43,42 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
async launch(options: LaunchOptions = {}): Promise<FFBrowser> { async launch(options: LaunchOptions = {}): Promise<FFBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const { timeout = 30000 } = options; const browserServer = new BrowserServer(options);
const deadline = TimeoutSettings.computeDeadline(timeout); const { downloadsPath } = await this._launchServer(options, 'local', browserServer);
const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'local'); return await browserServer._initializeOrClose(async () => {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => { const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, { return FFBrowser.connect(transport, {
slowMo: options.slowMo, slowMo: options.slowMo,
logger, logger: browserServer._logger,
downloadsPath, downloadsPath,
headful: !processBrowserArgOptions(options).headless, headful: !processBrowserArgOptions(options).headless,
ownedServer: browserServer, ownedServer: browserServer,
}); });
}, logger); }, browserServer._logger);
return browser; return browser;
}); });
} }
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
return (await this._launchServer(options, 'server')).browserServer; const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
} }
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const { timeout = 30000 } = options; const browserServer = new BrowserServer(options);
const deadline = TimeoutSettings.computeDeadline(timeout); const { downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
const { browserServer, downloadsPath, logger } = await this._launchServer(options, 'persistent', userDataDir); return await browserServer._initializeOrClose(async () => {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => { const browser = await WebSocketTransport.connect(browserServer.wsEndpoint()!, transport => {
return FFBrowser.connect(transport, { return FFBrowser.connect(transport, {
slowMo: options.slowMo, slowMo: options.slowMo,
logger, logger: browserServer._logger,
persistent: true, persistent: true,
downloadsPath, downloadsPath,
ownedServer: browserServer, ownedServer: browserServer,
headful: !processBrowserArgOptions(options).headless, headful: !processBrowserArgOptions(options).headless,
}); });
}, logger); }, browserServer._logger);
const context = browser._defaultContext!; const context = browser._defaultContext!;
if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs)) if (!options.ignoreDefaultArgs || Array.isArray(options.ignoreDefaultArgs))
await context._loadDefaultContext(); await context._loadDefaultContext();
@ -91,7 +86,7 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
}); });
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, downloadsPath: string, logger: InnerLogger }> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ downloadsPath: string }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -104,7 +99,7 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
port = 0, port = 0,
} = options; } = options;
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.logger); const logger = browserServer._logger;
let temporaryProfileDir = null; let temporaryProfileDir = null;
if (!userDataDir) { if (!userDataDir) {
@ -126,7 +121,6 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
// Note: it is important to define these variables before launchProcess, so that we don't get // Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong. // "Cannot access 'browserServer' before initialization" if something went wrong.
let browserServer: BrowserServer | undefined = undefined;
let browserWSEndpoint: string | undefined = undefined; let browserWSEndpoint: string | undefined = undefined;
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({ const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
executablePath: firefoxExecutable, executablePath: firefoxExecutable,
@ -143,15 +137,14 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
pipe: false, pipe: false,
tempDir: temporaryProfileDir || undefined, tempDir: temporaryProfileDir || undefined,
attemptToGracefullyClose: async () => { attemptToGracefullyClose: async () => {
assert(browserServer); debugAssert(browserServer._isInitialized());
// We try to gracefully close to prevent crash reporting and core dumps. // We try to gracefully close to prevent crash reporting and core dumps.
const transport = await WebSocketTransport.connect(browserWSEndpoint!, async transport => transport); const transport = await WebSocketTransport.connect(browserWSEndpoint!, async transport => transport);
const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId }; const message = { method: 'Browser.close', params: {}, id: kBrowserCloseMessageId };
await transport.send(message); await transport.send(message);
}, },
onkill: (exitCode, signal) => { onkill: (exitCode, signal) => {
if (browserServer) browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
}, },
}); });
@ -163,8 +156,8 @@ export class Firefox extends AbstractBrowserType<FFBrowser> {
(await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, logger, port), logger)) : (await WebSocketTransport.connect(innerEndpoint, t => wrapTransportWithWebSocket(t, logger, port), logger)) :
new WebSocketWrapper(innerEndpoint, []); new WebSocketWrapper(innerEndpoint, []);
browserWSEndpoint = webSocketWrapper.wsEndpoint; browserWSEndpoint = webSocketWrapper.wsEndpoint;
browserServer = new BrowserServer(launchedProcess, gracefullyClose, webSocketWrapper); browserServer._initialize(launchedProcess, gracefullyClose, webSocketWrapper);
return { browserServer, downloadsPath, logger }; return { downloadsPath };
} }
async connect(options: ConnectOptions): Promise<FFBrowser> { async connect(options: ConnectOptions): Promise<FFBrowser> {

View file

@ -16,7 +16,7 @@
*/ */
import * as childProcess from 'child_process'; import * as childProcess from 'child_process';
import { Log, InnerLogger } from '../logger'; import { Log, RootLogger } from '../logger';
import * as fs from 'fs'; import * as fs from 'fs';
import * as os from 'os'; import * as os from 'os';
import * as path from 'path'; import * as path from 'path';
@ -44,7 +44,6 @@ const browserStdErrLog: Log = {
severity: 'warning' severity: 'warning'
}; };
export type LaunchProcessOptions = { export type LaunchProcessOptions = {
executablePath: string, executablePath: string,
args: string[], args: string[],
@ -62,7 +61,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>,
onkill: (exitCode: number | null, signal: string | null) => void, onkill: (exitCode: number | null, signal: string | null) => void,
logger: InnerLogger, logger: RootLogger,
}; };
type LaunchResult = { type LaunchResult = {
@ -74,6 +73,7 @@ type LaunchResult = {
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> { export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
const logger = options.logger; const logger = options.logger;
const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe']; const stdio: ('ignore' | 'pipe')[] = options.pipe ? ['ignore', 'pipe', 'pipe', 'pipe', 'pipe'] : ['ignore', 'pipe', 'pipe'];
logger.startLaunchRecording();
logger._log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`); logger._log(browserLog, `<launching> ${options.executablePath} ${options.args.join(' ')}`);
const spawnedProcess = childProcess.spawn( const spawnedProcess = childProcess.spawn(
options.executablePath, options.executablePath,

View file

@ -33,7 +33,6 @@ import { Events } from '../events';
import { BrowserContext } from '../browserContext'; import { BrowserContext } from '../browserContext';
import { InnerLogger, logError, RootLogger } from '../logger'; import { InnerLogger, logError, RootLogger } from '../logger';
import { BrowserDescriptor } from '../install/browserPaths'; import { BrowserDescriptor } from '../install/browserPaths';
import { TimeoutSettings } from '../timeoutSettings';
export class WebKit extends AbstractBrowserType<WKBrowser> { export class WebKit extends AbstractBrowserType<WKBrowser> {
constructor(packagePath: string, browser: BrowserDescriptor) { constructor(packagePath: string, browser: BrowserDescriptor) {
@ -42,16 +41,13 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
async launch(options: LaunchOptions = {}): Promise<WKBrowser> { async launch(options: LaunchOptions = {}): Promise<WKBrowser> {
assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead');
const { timeout = 30000 } = options; const browserServer = new BrowserServer(options);
const deadline = TimeoutSettings.computeDeadline(timeout); const { transport, downloadsPath } = await this._launchServer(options, 'local', browserServer);
const { browserServer, transport, downloadsPath, logger } = await this._launchServer(options, 'local'); return await browserServer._initializeOrClose(async () => {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
return await WKBrowser.connect(transport!, { return await WKBrowser.connect(transport!, {
slowMo: options.slowMo, slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless, headful: !processBrowserArgOptions(options).headless,
logger, logger: browserServer._logger,
downloadsPath, downloadsPath,
ownedServer: browserServer ownedServer: browserServer
}); });
@ -59,20 +55,19 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
} }
async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> { async launchServer(options: LaunchServerOptions = {}): Promise<BrowserServer> {
return (await this._launchServer(options, 'server')).browserServer; const browserServer = new BrowserServer(options);
await this._launchServer(options, 'server', browserServer);
return browserServer;
} }
async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> { async launchPersistentContext(userDataDir: string, options: LaunchOptions = {}): Promise<BrowserContext> {
const { timeout = 30000 } = options; const browserServer = new BrowserServer(options);
const deadline = TimeoutSettings.computeDeadline(timeout); const { transport, downloadsPath } = await this._launchServer(options, 'persistent', browserServer, userDataDir);
const { transport, browserServer, logger, downloadsPath } = await this._launchServer(options, 'persistent', userDataDir); return await browserServer._initializeOrClose(async () => {
return await browserServer._initializeOrClose(deadline, async () => {
if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser();
const browser = await WKBrowser.connect(transport!, { const browser = await WKBrowser.connect(transport!, {
slowMo: options.slowMo, slowMo: options.slowMo,
headful: !processBrowserArgOptions(options).headless, headful: !processBrowserArgOptions(options).headless,
logger, logger: browserServer._logger,
persistent: true, persistent: true,
downloadsPath, downloadsPath,
ownedServer: browserServer ownedServer: browserServer
@ -84,7 +79,7 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
}); });
} }
private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, userDataDir?: string): Promise<{ browserServer: BrowserServer, transport?: ConnectionTransport, downloadsPath: string, logger: InnerLogger }> { private async _launchServer(options: LaunchServerOptions, launchType: LaunchType, browserServer: BrowserServer, userDataDir?: string): Promise<{ transport?: ConnectionTransport, downloadsPath: string, logger: RootLogger }> {
const { const {
ignoreDefaultArgs = false, ignoreDefaultArgs = false,
args = [], args = [],
@ -96,7 +91,7 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
port = 0, port = 0,
} = options; } = options;
assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.'); assert(!port || launchType === 'server', 'Cannot specify a port without launching as a server.');
const logger = new RootLogger(options.logger); const logger = browserServer._logger;
let temporaryUserDataDir: string | null = null; let temporaryUserDataDir: string | null = null;
if (!userDataDir) { if (!userDataDir) {
@ -119,7 +114,6 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
// Note: it is important to define these variables before launchProcess, so that we don't get // Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong. // "Cannot access 'browserServer' before initialization" if something went wrong.
let transport: ConnectionTransport | undefined = undefined; let transport: ConnectionTransport | undefined = undefined;
let browserServer: BrowserServer | undefined = undefined;
const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({ const { launchedProcess, gracefullyClose, downloadsPath } = await launchProcess({
executablePath: webkitExecutable, executablePath: webkitExecutable,
args: webkitArguments, args: webkitArguments,
@ -135,18 +129,17 @@ export class WebKit extends AbstractBrowserType<WKBrowser> {
// We try to gracefully close to prevent crash reporting and core dumps. // We try to gracefully close to prevent crash reporting and core dumps.
// Note that it's fine to reuse the pipe transport, since // Note that it's fine to reuse the pipe transport, since
// our connection ignores kBrowserCloseMessageId. // our connection ignores kBrowserCloseMessageId.
await transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId}); transport.send({method: 'Playwright.close', params: {}, id: kBrowserCloseMessageId});
}, },
onkill: (exitCode, signal) => { onkill: (exitCode, signal) => {
if (browserServer) browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
browserServer.emit(Events.BrowserServer.Close, exitCode, signal);
}, },
}); });
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4], logger); transport = new PipeTransport(stdio[3], stdio[4], logger);
browserServer = new BrowserServer(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port || 0) : null); browserServer._initialize(launchedProcess, gracefullyClose, launchType === 'server' ? wrapTransportWithWebSocket(transport, logger, port || 0) : null);
return { browserServer, transport, downloadsPath, logger }; return { transport, downloadsPath, logger };
} }
async connect(options: ConnectOptions): Promise<WKBrowser> { async connect(options: ConnectOptions): Promise<WKBrowser> {

View file

@ -39,6 +39,7 @@ export class WKBrowser extends BrowserBase {
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> { static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<WKBrowser> {
const browser = new WKBrowser(SlowMoTransport.wrap(transport, options.slowMo), options); const browser = new WKBrowser(SlowMoTransport.wrap(transport, options.slowMo), options);
// TODO: add Playwright.enable to test connection.
return browser; return browser;
} }

View file

@ -54,7 +54,7 @@ describe('Playwright', function() {
it('should handle timeout', async({browserType, defaultBrowserOptions}) => { it('should handle timeout', async({browserType, defaultBrowserOptions}) => {
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) }; const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launch(options).catch(e => e); const error = await browserType.launch(options).catch(e => e);
expect(error.message).toBe('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.'); expect(error.message).toContain('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.');
}); });
it('should handle exception', async({browserType, defaultBrowserOptions}) => { it('should handle exception', async({browserType, defaultBrowserOptions}) => {
const e = new Error('Dummy'); const e = new Error('Dummy');
@ -62,6 +62,12 @@ describe('Playwright', function() {
const error = await browserType.launch(options).catch(e => e); const error = await browserType.launch(options).catch(e => e);
expect(error).toBe(e); expect(error).toBe(e);
}); });
it('should report launch log', async({browserType, defaultBrowserOptions}) => {
const e = new Error('Dummy');
const options = { ...defaultBrowserOptions, __testHookBeforeCreateBrowser: () => { throw e; }, timeout: 9000 };
const error = await browserType.launch(options).catch(e => e);
expect(error.message).toContain('<launching>');
});
}); });
describe('browserType.launchPersistentContext', function() { describe('browserType.launchPersistentContext', function() {
@ -102,7 +108,7 @@ describe('Playwright', function() {
const userDataDir = await makeUserDataDir(); const userDataDir = await makeUserDataDir();
const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) }; const options = { ...defaultBrowserOptions, timeout: 5000, __testHookBeforeCreateBrowser: () => new Promise(f => setTimeout(f, 6000)) };
const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e); const error = await browserType.launchPersistentContext(userDataDir, options).catch(e => e);
expect(error.message).toBe('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.'); expect(error.message).toContain('Waiting for the browser to launch failed: timeout exceeded. Re-run with the DEBUG=pw:browser* env variable to see the debug log.');
await removeUserDataDir(userDataDir); await removeUserDataDir(userDataDir);
}); });
it('should handle exception', async({browserType, defaultBrowserOptions}) => { it('should handle exception', async({browserType, defaultBrowserOptions}) => {

View file

@ -213,7 +213,7 @@ function compareDocumentations(actual, expected) {
let expectedName = expected.name; let expectedName = expected.name;
for (const replacer of tsReplacers) for (const replacer of tsReplacers)
expectedName = expectedName.replace(...replacer); expectedName = expectedName.replace(...replacer);
if (expectedName !== actualName) if (normalizeType(expectedName) !== normalizeType(actualName))
errors.push(`${source} ${actualName} != ${expectedName}`); errors.push(`${source} ${actualName} != ${expectedName}`);
if (actual.name === 'boolean' || actual.name === 'string') if (actual.name === 'boolean' || actual.name === 'string')
return; return;
@ -304,3 +304,23 @@ function diff(actual, expected) {
} }
} }
function normalizeType(type) {
let nesting = 0;
const result = [];
let word = '';
for (const c of type) {
if (c === '<') {
++nesting;
} else if (c === '>') {
--nesting;
}
if (c === '|' && !nesting) {
result.push(word);
word = '';
} else {
word += c;
}
}
result.sort();
return result.join('|');
}