fix(launchServer): stream protocol logs into options.logger (#4403)

fix(launchServer): stream protocol logs into options.logger

This has regressed in v1.4.
This commit is contained in:
Dmitry Gozman 2020-11-11 15:12:10 -08:00 committed by GitHub
parent cb4fef1497
commit 138680f93c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 80 additions and 28 deletions

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { LaunchServerOptions } from './client/types'; import { LaunchServerOptions, Logger } from './client/types';
import { BrowserType } from './server/browserType'; import { BrowserType } from './server/browserType';
import * as ws from 'ws'; import * as ws from 'ws';
import * as fs from 'fs'; import * as fs from 'fs';
@ -32,6 +32,7 @@ import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher';
import { Selectors } from './server/selectors'; import { Selectors } from './server/selectors';
import { BrowserContext, Video } from './server/browserContext'; import { BrowserContext, Video } from './server/browserContext';
import { StreamDispatcher } from './dispatchers/streamDispatcher'; import { StreamDispatcher } from './dispatchers/streamDispatcher';
import { ProtocolLogger } from './server/types';
export class BrowserServerLauncherImpl implements BrowserServerLauncher { export class BrowserServerLauncherImpl implements BrowserServerLauncher {
private _browserType: BrowserType; private _browserType: BrowserType;
@ -46,7 +47,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined, ignoreDefaultArgs: Array.isArray(options.ignoreDefaultArgs) ? options.ignoreDefaultArgs : undefined,
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs), ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
env: options.env ? envObjectToArray(options.env) : undefined, env: options.env ? envObjectToArray(options.env) : undefined,
}); }, toProtocolLogger(options.logger));
return new BrowserServerImpl(browser, options.port); return new BrowserServerImpl(browser, options.port);
} }
} }
@ -192,3 +193,10 @@ class ConnectedBrowser extends BrowserDispatcher {
}); });
} }
} }
function toProtocolLogger(logger: Logger | undefined): ProtocolLogger | undefined {
return logger ? (direction: 'send' | 'receive', message: object) => {
if (logger.isEnabled('protocol', 'verbose'))
logger.log('protocol', 'verbose', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message), [], {});
} : undefined;
}

View File

@ -36,6 +36,7 @@ export type BrowserOptions = types.UIOptions & {
persistent?: types.BrowserContextOptions, // Undefined means no persistent context. persistent?: types.BrowserContextOptions, // Undefined means no persistent context.
browserProcess: BrowserProcess, browserProcess: BrowserProcess,
proxy?: ProxySettings, proxy?: ProxySettings,
protocolLogger: types.ProtocolLogger,
}; };
export abstract class Browser extends EventEmitter { export abstract class Browser extends EventEmitter {

View File

@ -29,6 +29,7 @@ import * as types from './types';
import { TimeoutSettings } from '../utils/timeoutSettings'; 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';
const mkdirAsync = util.promisify(fs.mkdir); const mkdirAsync = util.promisify(fs.mkdir);
const mkdtempAsync = util.promisify(fs.mkdtemp); const mkdtempAsync = util.promisify(fs.mkdtemp);
@ -57,12 +58,12 @@ export abstract class BrowserType {
return this._name; return this._name;
} }
async launch(options: types.LaunchOptions = {}): Promise<Browser> { async launch(options: types.LaunchOptions, protocolLogger?: types.ProtocolLogger): Promise<Browser> {
options = validateLaunchOptions(options); options = validateLaunchOptions(options);
const controller = new ProgressController(); const controller = new ProgressController();
controller.setLogName('browser'); controller.setLogName('browser');
const browser = await controller.run(progress => { const browser = await controller.run(progress => {
return this._innerLaunch(progress, options, undefined).catch(e => { throw this._rewriteStartupError(e); }); return this._innerLaunch(progress, options, undefined, helper.debugProtocolLogger(protocolLogger)).catch(e => { throw this._rewriteStartupError(e); });
}, TimeoutSettings.timeout(options)); }, TimeoutSettings.timeout(options));
return browser; return browser;
} }
@ -73,12 +74,12 @@ export abstract class BrowserType {
const controller = new ProgressController(); const controller = new ProgressController();
controller.setLogName('browser'); controller.setLogName('browser');
const browser = await controller.run(progress => { const browser = await controller.run(progress => {
return this._innerLaunch(progress, options, persistent, userDataDir).catch(e => { throw this._rewriteStartupError(e); }); return this._innerLaunch(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupError(e); });
}, TimeoutSettings.timeout(options)); }, TimeoutSettings.timeout(options));
return browser._defaultContext!; return browser._defaultContext!;
} }
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, 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 { browserProcess, downloadsPath, transport } = await this._launchProcess(progress, options, !!persistent, userDataDir);
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
@ -91,6 +92,7 @@ export abstract class BrowserType {
downloadsPath, downloadsPath,
browserProcess, browserProcess,
proxy: options.proxy, proxy: options.proxy,
protocolLogger,
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);

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); const connection = new CRConnection(transport, options.protocolLogger);
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

@ -21,6 +21,7 @@ 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 } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('ConnectionEvents.Disconnected') Disconnected: Symbol('ConnectionEvents.Disconnected')
@ -34,12 +35,14 @@ export class CRConnection extends EventEmitter {
private _lastId = 0; private _lastId = 0;
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;
readonly rootSession: CRSession; readonly rootSession: CRSession;
_closed = false; _closed = false;
constructor(transport: ConnectionTransport) { constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger) {
super(); super();
this._transport = transport; this._transport = transport;
this._protocolLogger = protocolLogger;
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', '');
@ -59,15 +62,13 @@ export class CRConnection extends EventEmitter {
const message: ProtocolRequest = { id, method, params }; const message: ProtocolRequest = { id, method, params };
if (sessionId) if (sessionId)
message.sessionId = sessionId; message.sessionId = sessionId;
if (debugLogger.isEnabled('protocol')) this._protocolLogger('send', message);
debugLogger.log('protocol', 'SEND ► ' + JSON.stringify(message));
this._transport.send(message); this._transport.send(message);
return id; return id;
} }
async _onMessage(message: ProtocolResponse) { async _onMessage(message: ProtocolResponse) {
if (debugLogger.isEnabled('protocol')) this._protocolLogger('receive', message);
debugLogger.log('protocol', '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.method === 'Target.attachedToTarget') { if (message.method === 'Target.attachedToTarget') {

View File

@ -27,7 +27,7 @@ import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
import { AdbBackend } from './backendAdb'; import { AdbBackend } from './backendAdb';
export class Clank extends BrowserType { export class Clank extends BrowserType {
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, 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;
if ((options as any).__testHookBeforeCreateBrowser) if ((options as any).__testHookBeforeCreateBrowser)
await (options as any).__testHookBeforeCreateBrowser(); await (options as any).__testHookBeforeCreateBrowser();
@ -47,6 +47,7 @@ export class Clank extends BrowserType {
downloadsPath: undefined, downloadsPath: undefined,
browserProcess: new ClankBrowserProcess(device, adbBrowser), browserProcess: new ClankBrowserProcess(device, adbBrowser),
proxy: options.proxy, proxy: options.proxy,
protocolLogger,
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);

View File

@ -29,7 +29,7 @@ import type {BrowserWindow} from 'electron';
import { Progress, ProgressController, runAbortableTask } from '../progress'; import { Progress, ProgressController, runAbortableTask } from '../progress';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { helper } from '../helper'; import { helper } from '../helper';
import { 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';
@ -174,7 +174,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); const nodeConnection = new CRConnection(nodeTransport, helper.debugProtocolLogger());
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]);
@ -184,7 +184,14 @@ export class Electron {
close: gracefullyClose, close: gracefullyClose,
kill kill
}; };
const browser = await CRBrowser.connect(chromeTransport, { name: 'electron', headful: true, persistent: { noDefaultViewport: true }, browserProcess }); const browserOptions: BrowserOptions = {
name: 'electron',
headful: true,
persistent: { noDefaultViewport: true },
browserProcess,
protocolLogger: helper.debugProtocolLogger(),
};
const browser = await CRBrowser.connect(chromeTransport, browserOptions);
app = new ElectronApplication(browser, nodeConnection); app = new ElectronApplication(browser, nodeConnection);
await app._init(); await app._init();
return app; return app;

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); const connection = new FFConnection(transport, options.protocolLogger);
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

@ -21,6 +21,7 @@ import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../trans
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 } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types';
export const ConnectionEvents = { export const ConnectionEvents = {
Disconnected: Symbol('Disconnected'), Disconnected: Symbol('Disconnected'),
@ -34,6 +35,7 @@ export class FFConnection extends EventEmitter {
private _lastId: number; private _lastId: number;
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;
readonly _sessions: Map<string, FFSession>; readonly _sessions: Map<string, FFSession>;
_closed: boolean; _closed: boolean;
@ -43,9 +45,10 @@ 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) { constructor(transport: ConnectionTransport, protocolLogger: ProtocolLogger) {
super(); super();
this._transport = transport; this._transport = transport;
this._protocolLogger = protocolLogger;
this._lastId = 0; this._lastId = 0;
this._callbacks = new Map(); this._callbacks = new Map();
@ -77,14 +80,12 @@ export class FFConnection extends EventEmitter {
} }
_rawSend(message: ProtocolRequest) { _rawSend(message: ProtocolRequest) {
if (debugLogger.isEnabled('protocol')) this._protocolLogger('send', message);
debugLogger.log('protocol', 'SEND ► ' + JSON.stringify(message));
this._transport.send(message); this._transport.send(message);
} }
async _onMessage(message: ProtocolResponse) { async _onMessage(message: ProtocolResponse) {
if (debugLogger.isEnabled('protocol')) this._protocolLogger('receive', message);
debugLogger.log('protocol', '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.sessionId) { if (message.sessionId) {

View File

@ -20,6 +20,7 @@ import * as removeFolder from 'rimraf';
import * as util from 'util'; import * as util from 'util';
import * as types from './types'; import * as types from './types';
import { Progress } from './progress'; import { Progress } from './progress';
import { debugLogger } from '../utils/debugLogger';
const removeFolderAsync = util.promisify(removeFolder); const removeFolderAsync = util.promisify(removeFolder);
@ -110,6 +111,15 @@ class Helper {
static millisToRoundishMillis(value: number): number { static millisToRoundishMillis(value: number): number {
return ((value * 1000) | 0) / 1000; return ((value * 1000) | 0) / 1000;
} }
static debugProtocolLogger(protocolLogger?: types.ProtocolLogger): types.ProtocolLogger {
return (direction: 'send' | 'receive', message: object) => {
if (protocolLogger)
protocolLogger(direction, message);
if (debugLogger.isEnabled('protocol'))
debugLogger.log('protocol', (direction === 'send' ? 'SEND ► ' : '◀ RECV ') + JSON.stringify(message));
};
}
} }
export const helper = Helper; export const helper = Helper;

View File

@ -275,6 +275,8 @@ export type LaunchOptions = LaunchOptionsBase & UIOptions & {
}; };
export type LaunchPersistentOptions = LaunchOptionsBase & BrowserContextOptions; export type LaunchPersistentOptions = LaunchOptionsBase & BrowserContextOptions;
export type ProtocolLogger = (direction: 'send' | 'receive', message: object) => void;
export type SerializedAXNode = { export type SerializedAXNode = {
role: string, role: string,
name: string, name: string,

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)); this._connection = new WKConnection(transport, this._onDisconnect.bind(this), options.protocolLogger);
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)),

View File

@ -21,6 +21,7 @@ import { ConnectionTransport, ProtocolRequest, ProtocolResponse } from '../trans
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 } from '../../utils/debugLogger';
import { ProtocolLogger } from '../types';
// 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.
@ -34,15 +35,17 @@ export type PageProxyMessageReceivedPayload = { pageProxyId: string, message: an
export class WKConnection { export class WKConnection {
private readonly _transport: ConnectionTransport; private readonly _transport: ConnectionTransport;
private readonly _onDisconnect: () => void; private readonly _onDisconnect: () => void;
private readonly _protocolLogger: ProtocolLogger;
private _lastId = 0; private _lastId = 0;
private _closed = false; private _closed = false;
readonly browserSession: WKSession; readonly browserSession: WKSession;
constructor(transport: ConnectionTransport, onDisconnect: () => void) { constructor(transport: ConnectionTransport, onDisconnect: () => void, protocolLogger: ProtocolLogger) {
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.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);
}); });
@ -53,14 +56,12 @@ export class WKConnection {
} }
rawSend(message: ProtocolRequest) { rawSend(message: ProtocolRequest) {
if (debugLogger.isEnabled('protocol')) this._protocolLogger('send', message);
debugLogger.log('protocol', 'SEND ► ' + JSON.stringify(message));
this._transport.send(message); this._transport.send(message);
} }
private _dispatchMessage(message: ProtocolResponse) { private _dispatchMessage(message: ProtocolResponse) {
if (debugLogger.isEnabled('protocol')) this._protocolLogger('receive', message);
debugLogger.log('protocol', '◀ RECV ' + JSON.stringify(message));
if (message.id === kBrowserCloseMessageId) if (message.id === kBrowserCloseMessageId)
return; return;
if (message.pageProxyId) { if (message.pageProxyId) {

View File

@ -62,4 +62,22 @@ describe('lauch server', (suite, { wire }) => {
expect(result['exitCode']).toBe(0); expect(result['exitCode']).toBe(0);
expect(result['signal']).toBe(null); expect(result['signal']).toBe(null);
}); });
it('should log protocol', async ({browserType, browserOptions}) => {
const logs: string[] = [];
const logger = {
isEnabled(name: string) {
return true;
},
log(name: string, severity: string, message: string) {
logs.push(`${name}:${severity}:${message}`);
}
};
const browserServer = await browserType.launchServer({ ...browserOptions, logger });
await browserServer.close();
expect(logs.some(log => log.startsWith('protocol:verbose:SEND ►'))).toBe(true);
expect(logs.some(log => log.startsWith('protocol:verbose:◀ RECV'))).toBe(true);
});
}); });