Skip to content

Commit be16ce4

Browse files
authored
feat(errors): append recent browser logs when browser disconnects (#4625)
1 parent e1e000d commit be16ce4

File tree

16 files changed

+110
-48
lines changed

16 files changed

+110
-48
lines changed

src/server/browser.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { EventEmitter } from 'events';
2121
import { Download } from './download';
2222
import { ProxySettings } from './types';
2323
import { ChildProcess } from 'child_process';
24+
import { RecentLogsCollector } from '../utils/debugLogger';
2425

2526
export interface BrowserProcess {
2627
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
@@ -37,6 +38,7 @@ export type BrowserOptions = types.UIOptions & {
3738
browserProcess: BrowserProcess,
3839
proxy?: ProxySettings,
3940
protocolLogger: types.ProtocolLogger,
41+
browserLogsCollector: RecentLogsCollector,
4042
};
4143

4244
export abstract class Browser extends EventEmitter {

src/server/browserType.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { TimeoutSettings } from '../utils/timeoutSettings';
3030
import { validateHostRequirements } from './validateDependencies';
3131
import { isDebugMode } from '../utils/utils';
3232
import { helper } from './helper';
33+
import { RecentLogsCollector } from '../utils/debugLogger';
3334

3435
const mkdirAsync = util.promisify(fs.mkdir);
3536
const mkdtempAsync = util.promisify(fs.mkdtemp);
@@ -81,7 +82,8 @@ export abstract class BrowserType {
8182

8283
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise<Browser> {
8384
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
84-
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir);
85+
const browserLogsCollector = new RecentLogsCollector();
86+
const { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, userDataDir);
8587
if ((options as any).__testHookBeforeCreateBrowser)
8688
await (options as any).__testHookBeforeCreateBrowser();
8789
const browserOptions: BrowserOptions = {
@@ -93,6 +95,7 @@ export abstract class BrowserType {
9395
browserProcess,
9496
proxy: options.proxy,
9597
protocolLogger,
98+
browserLogsCollector,
9699
};
97100
if (persistent)
98101
validateBrowserContextOptions(persistent, browserOptions);
@@ -104,7 +107,7 @@ export abstract class BrowserType {
104107
return browser;
105108
}
106109

107-
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> {
110+
private async _launchProcess(progress: Progress, options: types.LaunchOptions, isPersistent: boolean, browserLogsCollector: RecentLogsCollector, userDataDir?: string): Promise<{ browserProcess: BrowserProcess, downloadsPath: string, transport: ConnectionTransport }> {
108111
const {
109112
ignoreDefaultArgs,
110113
ignoreAllDefaultArgs,
@@ -172,7 +175,10 @@ export abstract class BrowserType {
172175
handleSIGINT,
173176
handleSIGTERM,
174177
handleSIGHUP,
175-
progress,
178+
log: (message: string) => {
179+
progress.log(message);
180+
browserLogsCollector.log(message);
181+
},
176182
stdio: 'pipe',
177183
tempDirectories,
178184
attemptToGracefullyClose: async () => {

src/server/chromium/crBrowser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export class CRBrowser extends Browser {
4646
private _tracingClient: CRSession | undefined;
4747

4848
static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise<CRBrowser> {
49-
const connection = new CRConnection(transport, options.protocolLogger);
49+
const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector);
5050
const browser = new CRBrowser(connection, options);
5151
browser._devtools = devtools;
5252
const session = connection.rootSession;

src/server/chromium/crConnection.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../trans
2020
import { Protocol } from './protocol';
2121
import { EventEmitter } from 'events';
2222
import { rewriteErrorMessage } from '../../utils/stackTrace';
23-
import { debugLogger } from '../../utils/debugLogger';
23+
import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
2424
import { ProtocolLogger } from '../types';
25+
import { helper } from '../helper';
2526

2627
export const ConnectionEvents = {
2728
Disconnected: Symbol('ConnectionEvents.Disconnected')
@@ -36,13 +37,15 @@ export class CRConnection extends EventEmitter {
3637
private readonly _transport: ConnectionTransport;
3738
private readonly _sessions = new Map<string, CRSession>();
3839
private readonly _protocolLogger: ProtocolLogger;
40+
private readonly _browserLogsCollector: RecentLogsCollector;
3941
readonly rootSession: CRSession;
4042
_closed = false;
4143

42-
constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger) {
44+
constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
4345
super();
4446
this._transport = transport;
4547
this._protocolLogger = protocolLogger;
48+
this._browserLogsCollector = browserLogsCollector;
4649
this._transport.onmessage = this._onMessage.bind(this);
4750
this._transport.onclose = this._onClose.bind(this);
4851
this.rootSession = new CRSession(this, '', 'browser', '');
@@ -79,7 +82,7 @@ export class CRConnection extends EventEmitter {
7982
} else if (message.method === 'Target.detachedFromTarget') {
8083
const session = this._sessions.get(message.params.sessionId);
8184
if (session) {
82-
session._onClosed();
85+
session._onClosed(undefined);
8386
this._sessions.delete(message.params.sessionId);
8487
}
8588
}
@@ -92,8 +95,9 @@ export class CRConnection extends EventEmitter {
9295
this._closed = true;
9396
this._transport.onmessage = undefined;
9497
this._transport.onclose = undefined;
98+
const browserDisconnectedLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs());
9599
for (const session of this._sessions.values())
96-
session._onClosed();
100+
session._onClosed(browserDisconnectedLogs);
97101
this._sessions.clear();
98102
Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected));
99103
}
@@ -126,6 +130,7 @@ export class CRSession extends EventEmitter {
126130
private readonly _sessionId: string;
127131
private readonly _rootSessionId: string;
128132
private _crashed: boolean = false;
133+
private _browserDisconnectedLogs: string | undefined;
129134
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;
130135
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;
131136
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 {
156161
): Promise<Protocol.CommandReturnValues[T]> {
157162
if (this._crashed)
158163
throw new Error('Target crashed');
164+
if (this._browserDisconnectedLogs !== undefined)
165+
throw new Error(`Protocol error (${method}): Browser closed.` + this._browserDisconnectedLogs);
159166
if (!this._connection)
160167
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
161168
const id = this._connection._rawSend(this._sessionId, method, params);
@@ -195,9 +202,11 @@ export class CRSession extends EventEmitter {
195202
await rootSession.send('Target.detachFromTarget', { sessionId: this._sessionId });
196203
}
197204

198-
_onClosed() {
205+
_onClosed(browserDisconnectedLogs: string | undefined) {
206+
this._browserDisconnectedLogs = browserDisconnectedLogs;
207+
const errorMessage = browserDisconnectedLogs !== undefined ? 'Browser closed.' + browserDisconnectedLogs : 'Target closed.';
199208
for (const callback of this._callbacks.values())
200-
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`));
209+
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): ` + errorMessage));
201210
this._callbacks.clear();
202211
this._connection = null;
203212
Promise.resolve().then(() => this.emit(CRSessionEvents.Disconnected));

src/server/chromium/videoRecorder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export class VideoRecorder {
6565
executablePath,
6666
args,
6767
stdio: 'stdin',
68-
progress,
68+
log: (message: string) => progress.log(message),
6969
tempDirectories: [],
7070
attemptToGracefullyClose: async () => {
7171
progress.log('Closing stdin...');

src/server/clank/clank.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { Env } from '../processLauncher';
2525
import { CRBrowser } from '../chromium/crBrowser';
2626
import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
2727
import { AdbBackend } from './backendAdb';
28+
import { RecentLogsCollector } from '../../utils/debugLogger';
2829

2930
export class Clank extends BrowserType {
3031
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 {
4849
browserProcess: new ClankBrowserProcess(device, adbBrowser),
4950
proxy: options.proxy,
5051
protocolLogger,
52+
browserLogsCollector: new RecentLogsCollector(),
5153
};
5254
if (persistent)
5355
validateBrowserContextOptions(persistent, browserOptions);

src/server/electron/electron.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { helper } from '../helper';
3232
import { BrowserOptions, BrowserProcess } from '../browser';
3333
import * as childProcess from 'child_process';
3434
import * as readline from 'readline';
35+
import { RecentLogsCollector } from '../../utils/debugLogger';
3536

3637
export type ElectronLaunchOptionsBase = {
3738
args?: string[],
@@ -157,14 +158,18 @@ export class Electron {
157158
electronArguments.push('--no-sandbox');
158159
}
159160

161+
const browserLogsCollector = new RecentLogsCollector();
160162
const { launchedProcess, gracefullyClose, kill } = await launchProcess({
161163
executablePath,
162164
args: electronArguments,
163165
env: options.env ? envArrayToObject(options.env) : process.env,
164166
handleSIGINT,
165167
handleSIGTERM,
166168
handleSIGHUP,
167-
progress,
169+
log: (message: string) => {
170+
progress.log(message);
171+
browserLogsCollector.log(message);
172+
},
168173
stdio: 'pipe',
169174
cwd: options.cwd,
170175
tempDirectories: [],
@@ -174,7 +179,7 @@ export class Electron {
174179

175180
const nodeMatch = await waitForLine(progress, launchedProcess, /^Debugger listening on (ws:\/\/.*)$/);
176181
const nodeTransport = await WebSocketTransport.connect(progress, nodeMatch[1]);
177-
const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger());
182+
const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger(), browserLogsCollector);
178183

179184
const chromeMatch = await waitForLine(progress, launchedProcess, /^DevTools listening on (ws:\/\/.*)$/);
180185
const chromeTransport = await WebSocketTransport.connect(progress, chromeMatch[1]);
@@ -190,6 +195,7 @@ export class Electron {
190195
persistent: { noDefaultViewport: true },
191196
browserProcess,
192197
protocolLogger: helper.debugProtocolLogger(),
198+
browserLogsCollector,
193199
};
194200
const browser = await CRBrowser.connect(chromeTransport, browserOptions);
195201
app = new ElectronApplication(browser, nodeConnection);

src/server/firefox/ffBrowser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export class FFBrowser extends Browser {
3333
private _version = '';
3434

3535
static async connect(transport: ConnectionTransport, options: BrowserOptions): Promise<FFBrowser> {
36-
const connection = new FFConnection(transport, options.protocolLogger);
36+
const connection = new FFConnection(transport, options.protocolLogger, options.browserLogsCollector);
3737
const browser = new FFBrowser(connection, options);
3838
const promises: Promise<any>[] = [
3939
connection.send('Browser.enable', { attachToDefaultContext: !!options.persistent }),

src/server/firefox/ffConnection.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ import { assert } from '../../utils/utils';
2020
import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../transport';
2121
import { Protocol } from './protocol';
2222
import { rewriteErrorMessage } from '../../utils/stackTrace';
23-
import { debugLogger } from '../../utils/debugLogger';
23+
import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
2424
import { ProtocolLogger } from '../types';
25+
import { helper } from '../helper';
2526

2627
export const ConnectionEvents = {
2728
Disconnected: Symbol('Disconnected'),
@@ -36,6 +37,7 @@ export class FFConnection extends EventEmitter {
3637
private _callbacks: Map<number, {resolve: Function, reject: Function, error: Error, method: string}>;
3738
private _transport: ConnectionTransport;
3839
private readonly _protocolLogger: ProtocolLogger;
40+
private readonly _browserLogsCollector: RecentLogsCollector;
3941
readonly _sessions: Map<string, FFSession>;
4042
_closed: boolean;
4143

@@ -45,10 +47,11 @@ export class FFConnection extends EventEmitter {
4547
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;
4648
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;
4749

48-
constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger) {
50+
constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger, browserLogsCollector: RecentLogsCollector) {
4951
super();
5052
this._transport = transport;
5153
this._protocolLogger = protocolLogger;
54+
this._browserLogsCollector = browserLogsCollector;
5255
this._lastId = 0;
5356
this._callbacks = new Map();
5457

@@ -68,6 +71,7 @@ export class FFConnection extends EventEmitter {
6871
method: T,
6972
params?: Protocol.CommandParameters[T]
7073
): Promise<Protocol.CommandReturnValues[T]> {
74+
this._checkClosed(method);
7175
const id = this.nextMessageId();
7276
this._rawSend({id, method, params});
7377
return new Promise((resolve, reject) => {
@@ -79,6 +83,11 @@ export class FFConnection extends EventEmitter {
7983
return ++this._lastId;
8084
}
8185

86+
_checkClosed(method: string) {
87+
if (this._closed)
88+
throw new Error(`Protocol error (${method}): Browser closed.` + helper.formatBrowserLogs(this._browserLogsCollector.recentLogs()));
89+
}
90+
8291
_rawSend(message: ProtocolRequest) {
8392
this._protocolLogger('send', message);
8493
this._transport.send(message);
@@ -111,12 +120,13 @@ export class FFConnection extends EventEmitter {
111120
this._closed = true;
112121
this._transport.onmessage = undefined;
113122
this._transport.onclose = undefined;
114-
for (const callback of this._callbacks.values())
115-
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`));
116-
this._callbacks.clear();
123+
const formattedBrowserLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs());
117124
for (const session of this._sessions.values())
118-
session.dispose();
125+
session.dispose(formattedBrowserLogs);
119126
this._sessions.clear();
127+
for (const callback of this._callbacks.values())
128+
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Browser closed.` + formattedBrowserLogs));
129+
this._callbacks.clear();
120130
Promise.resolve().then(() => this.emit(ConnectionEvents.Disconnected));
121131
}
122132

@@ -175,6 +185,7 @@ export class FFSession extends EventEmitter {
175185
): Promise<Protocol.CommandReturnValues[T]> {
176186
if (this._crashed)
177187
throw new Error('Page crashed');
188+
this._connection._checkClosed(method);
178189
if (this._disposed)
179190
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
180191
const id = this._connection.nextMessageId();
@@ -202,9 +213,9 @@ export class FFSession extends EventEmitter {
202213
}
203214
}
204215

205-
dispose() {
216+
dispose(formattedBrowserLogs?: string) {
206217
for (const callback of this._callbacks.values())
207-
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`));
218+
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.` + formattedBrowserLogs));
208219
this._callbacks.clear();
209220
this._disposed = true;
210221
this._connection._sessions.delete(this._sessionId);

src/server/helper.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@ class Helper {
120120
debugLogger.log('protocol', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message));
121121
};
122122
}
123+
124+
static formatBrowserLogs(logs: string[]) {
125+
if (!logs.length)
126+
return '';
127+
return '\n' + '='.repeat(20) + ' Browser output: ' + '='.repeat(20) + '\n' + logs.join('\n');
128+
}
123129
}
124130

125131
export const helper = Helper;

0 commit comments

Comments
 (0)