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

View File

@ -30,6 +30,7 @@ import { TimeoutSettings } from '../utils/timeoutSettings';
import { validateHostRequirements } from './validateDependencies';
import { isDebugMode } from '../utils/utils';
import { helper } from './helper';
import { RecentLogsCollector } from '../utils/debugLogger';
const mkdirAsync = util.promisify(fs.mkdir);
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> {
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)
await (options as any).__testHookBeforeCreateBrowser();
const browserOptions: BrowserOptions = {
@ -93,6 +95,7 @@ export abstract class BrowserType {
browserProcess,
proxy: options.proxy,
protocolLogger,
browserLogsCollector,
};
if (persistent)
validateBrowserContextOptions(persistent, browserOptions);
@ -104,7 +107,7 @@ export abstract class BrowserType {
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 {
ignoreDefaultArgs,
ignoreAllDefaultArgs,
@ -172,7 +175,10 @@ export abstract class BrowserType {
handleSIGINT,
handleSIGTERM,
handleSIGHUP,
progress,
log: (message: string) => {
progress.log(message);
browserLogsCollector.log(message);
},
stdio: 'pipe',
tempDirectories,
attemptToGracefullyClose: async () => {

View File

@ -46,7 +46,7 @@ export class CRBrowser extends Browser {
private _tracingClient: CRSession | undefined;
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);
browser._devtools = devtools;
const session = connection.rootSession;

View File

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

View File

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

View File

@ -25,6 +25,7 @@ import { Env } from '../processLauncher';
import { CRBrowser } from '../chromium/crBrowser';
import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
import { AdbBackend } from './backendAdb';
import { RecentLogsCollector } from '../../utils/debugLogger';
export class Clank extends BrowserType {
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),
proxy: options.proxy,
protocolLogger,
browserLogsCollector: new RecentLogsCollector(),
};
if (persistent)
validateBrowserContextOptions(persistent, browserOptions);

View File

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

View File

@ -33,7 +33,7 @@ export class FFBrowser extends Browser {
private _version = '';
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 promises: Promise<any>[] = [
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 { Protocol } from './protocol';
import { rewriteErrorMessage } from '../../utils/stackTrace';
import { debugLogger } from '../../utils/debugLogger';
import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types';
import { helper } from '../helper';
export const ConnectionEvents = {
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 _transport: ConnectionTransport;
private readonly _protocolLogger: ProtocolLogger;
private readonly _browserLogsCollector: RecentLogsCollector;
readonly _sessions: Map<string, FFSession>;
_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;
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();
this._transport = transport;
this._protocolLogger = protocolLogger;
this._browserLogsCollector = browserLogsCollector;
this._lastId = 0;
this._callbacks = new Map();
@ -68,6 +71,7 @@ export class FFConnection extends EventEmitter {
method: T,
params?: Protocol.CommandParameters[T]
): Promise<Protocol.CommandReturnValues[T]> {
this._checkClosed(method);
const id = this.nextMessageId();
this._rawSend({id, method, params});
return new Promise((resolve, reject) => {
@ -79,6 +83,11 @@ export class FFConnection extends EventEmitter {
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) {
this._protocolLogger('send', message);
this._transport.send(message);
@ -111,12 +120,13 @@ export class FFConnection extends EventEmitter {
this._closed = true;
this._transport.onmessage = undefined;
this._transport.onclose = undefined;
for (const callback of this._callbacks.values())
callback.reject(rewriteErrorMessage(callback.error, `Protocol error (${callback.method}): Target closed.`));
this._callbacks.clear();
const formattedBrowserLogs = helper.formatBrowserLogs(this._browserLogsCollector.recentLogs());
for (const session of this._sessions.values())
session.dispose();
session.dispose(formattedBrowserLogs);
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));
}
@ -175,6 +185,7 @@ export class FFSession extends EventEmitter {
): Promise<Protocol.CommandReturnValues[T]> {
if (this._crashed)
throw new Error('Page crashed');
this._connection._checkClosed(method);
if (this._disposed)
throw new Error(`Protocol error (${method}): Session closed. Most likely the ${this._targetType} has been closed.`);
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())
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._disposed = true;
this._connection._sessions.delete(this._sessionId);

View File

@ -120,6 +120,12 @@ class Helper {
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;

View File

@ -19,7 +19,6 @@ import * as childProcess from 'child_process';
import * as readline from 'readline';
import * as removeFolder from 'rimraf';
import { helper } from './helper';
import { Progress } from './progress';
import * as types from './types';
import { isUnderTest } from '../utils/utils';
@ -41,7 +40,7 @@ export type LaunchProcessOptions = {
// Note: attemptToGracefullyClose should reject if it does not close the browser.
attemptToGracefullyClose: () => Promise<any>,
onExit: (exitCode: number | null, signal: string | null) => void,
progress: Progress,
log: (message: string) => void,
};
type LaunchResult = {
@ -65,9 +64,8 @@ if (maxListeners !== 0)
export async function launchProcess(options: LaunchProcessOptions): Promise<LaunchResult> {
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'];
progress.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
options.log(`<launching> ${options.executablePath} ${options.args.join(' ')}`);
const spawnedProcess = childProcess.spawn(
options.executablePath,
options.args,
@ -93,16 +91,16 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
});
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 });
stdout.on('line', (data: string) => {
progress.log('[out] ' + data);
options.log('[out] ' + data);
});
const stderr = readline.createInterface({ input: spawnedProcess.stderr });
stderr.on('line', (data: string) => {
progress.log('[err] ' + data);
options.log('[err] ' + data);
});
let processClosed = false;
@ -111,7 +109,7 @@ export async function launchProcess(options: LaunchProcessOptions): Promise<Laun
let fulfillCleanup = () => {};
const waitForCleanup = new Promise<void>(f => fulfillCleanup = f);
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;
helper.removeEventListeners(listeners);
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.
// In this case, let's forcefully kill the process.
if (gracefullyClosing) {
progress.log(`<forecefully close>`);
options.log(`<forecefully close>`);
killProcess();
await waitForClose; // Ensure the process is dead and we called options.onkill.
return;
}
gracefullyClosing = true;
progress.log(`<gracefully close start>`);
options.log(`<gracefully close start>`);
await options.attemptToGracefullyClose().catch(() => killProcess());
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.
function killProcess() {
progress.log(`<kill>`);
options.log(`<kill>`);
helper.removeEventListeners(listeners);
if (spawnedProcess.pid && !spawnedProcess.killed && !processClosed) {
// Force kill the browser.

View File

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

View File

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

View File

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

View File

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

View File

@ -61,3 +61,20 @@ class 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;
}
}