diff --git a/src/browserServerImpl.ts b/src/browserServerImpl.ts index 2eb0f8f3d7..3097cf5deb 100644 --- a/src/browserServerImpl.ts +++ b/src/browserServerImpl.ts @@ -31,8 +31,9 @@ import { createGuid } from './utils/utils'; import { SelectorsDispatcher } from './dispatchers/selectorsDispatcher'; import { Selectors } from './server/selectors'; import { BrowserContext, Video } from './server/browserContext'; -import { StreamDispatcher } from './dispatchers/streamDispatcher'; +import { StreamDispatcher, StreamWrapper } from './dispatchers/streamDispatcher'; import { ProtocolLogger } from './server/types'; +import { SdkObject } from './server/sdkObject'; export class BrowserServerLauncherImpl implements BrowserServerLauncher { private _browserType: BrowserType; @@ -118,7 +119,7 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer { connection.dispatch(JSON.parse(Buffer.from(message).toString())); }); socket.on('error', () => {}); - const selectors = new Selectors(); + const selectors = new Selectors(this._browser.options.rootSdkObject); const scope = connection.rootDispatcher(); const remoteBrowser = new RemoteBrowserDispatcher(scope, this._browser, selectors); socket.on('close', () => { @@ -130,12 +131,12 @@ export class BrowserServerImpl extends EventEmitter implements BrowserServer { } } -class RemoteBrowserDispatcher extends Dispatcher<{}, channels.RemoteBrowserInitializer> implements channels.PlaywrightChannel { +class RemoteBrowserDispatcher extends Dispatcher implements channels.PlaywrightChannel { readonly connectedBrowser: ConnectedBrowser; constructor(scope: DispatcherScope, browser: Browser, selectors: Selectors) { const connectedBrowser = new ConnectedBrowser(scope, browser, selectors); - super(scope, {}, 'RemoteBrowser', { + super(scope, browser, 'RemoteBrowser', { selectors: new SelectorsDispatcher(scope, selectors), browser: connectedBrowser, }, false, 'remoteBrowser'); @@ -188,7 +189,7 @@ class ConnectedBrowser extends BrowserDispatcher { video._waitForCallbackOnFinish(async () => { const readable = fs.createReadStream(video._path); await new Promise(f => readable.on('readable', f)); - const stream = new StreamDispatcher(this._remoteBrowser!._scope, readable); + const stream = new StreamDispatcher(this._remoteBrowser!._scope, new StreamWrapper(this._object, readable)); this._remoteBrowser!._dispatchEvent('video', { stream, context: contextDispatcher, diff --git a/src/client/channelOwner.ts b/src/client/channelOwner.ts index d776b4a131..71a9ffd916 100644 --- a/src/client/channelOwner.ts +++ b/src/client/channelOwner.ts @@ -35,6 +35,7 @@ export abstract class ChannelOwner(object: any | null): Di return object ? lookupDispatcher(object) : undefined; } -export class Dispatcher extends EventEmitter implements channels.Channel { +export type CallMetadata = channels.Metadata & { + object: SdkObject; + type: string; + method: string; + params: any; +}; + +export class Dispatcher extends EventEmitter implements channels.Channel { private _connection: DispatcherConnection; private _isScope: boolean; // Parent is always "isScope". @@ -112,10 +120,9 @@ export class Dispatcher extends EventEmitter implements chann } export type DispatcherScope = Dispatcher; - -class Root extends Dispatcher<{}, {}> { +class Root extends Dispatcher { constructor(connection: DispatcherConnection) { - super(connection, {}, '', {}, true, ''); + super(connection, new SdkObject(null), '', {}, true, ''); } } @@ -178,7 +185,14 @@ export class DispatcherConnection { const validated = this._validateParams(dispatcher._type, method, params); if (typeof (dispatcher as any)[method] !== 'function') throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`); - const result = await (dispatcher as any)[method](validated, this._validateMetadata(metadata)); + const callMetadata: CallMetadata = { + ...this._validateMetadata(metadata).stack, + object: dispatcher._object, + type: dispatcher._type, + method, + params, + }; + const result = await (dispatcher as any)[method](validated, callMetadata); this.onmessage({ id, result: this._replaceDispatchersWithGuids(result) }); } catch (e) { this.onmessage({ id, error: serializeError(e) }); diff --git a/src/dispatchers/downloadDispatcher.ts b/src/dispatchers/downloadDispatcher.ts index 4e0e0d5e9f..96ca7813ce 100644 --- a/src/dispatchers/downloadDispatcher.ts +++ b/src/dispatchers/downloadDispatcher.ts @@ -17,7 +17,7 @@ import { Download } from '../server/download'; import * as channels from '../protocol/channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; -import { StreamDispatcher } from './streamDispatcher'; +import { StreamDispatcher, StreamWrapper } from './streamDispatcher'; import * as fs from 'fs'; import * as util from 'util'; import { mkdirIfNeeded } from '../utils/utils'; @@ -65,7 +65,7 @@ export class DownloadDispatcher extends Dispatcher readable.on('readable', f)); - const stream = new StreamDispatcher(this._scope, readable); + const stream = new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable)); // Resolve with a stream, so that client starts saving the data. resolve({ stream }); // Block the download until the stream is consumed. @@ -87,7 +87,7 @@ export class DownloadDispatcher extends Dispatcher readable.on('readable', f)); - return { stream: new StreamDispatcher(this._scope, readable) }; + return { stream: new StreamDispatcher(this._scope, new StreamWrapper(this._object, readable)) }; } async failure(): Promise { diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 0cfbc0851e..99386eb105 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -31,6 +31,7 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche import { FileChooser } from '../server/fileChooser'; import { CRCoverage } from '../server/chromium/crCoverage'; import { JSHandle } from '../server/javascript'; +import { SdkObject } from '../server/sdkObject'; export class PageDispatcher extends Dispatcher implements channels.PageChannel { private _page: Page; @@ -264,13 +265,13 @@ export class WorkerDispatcher extends Dispatcher implements channels.BindingCallChannel { +export class BindingCallDispatcher extends Dispatcher implements channels.BindingCallChannel { private _resolve: ((arg: any) => void) | undefined; private _reject: ((error: any) => void) | undefined; private _promise: Promise; constructor(scope: DispatcherScope, name: string, needsHandle: boolean, source: { context: BrowserContext, page: Page, frame: Frame }, args: any[]) { - super(scope, {}, 'BindingCall', { + super(scope, new SdkObject(null), 'BindingCall', { frame: lookupDispatcher(source.frame), name, args: needsHandle ? undefined : args.map(serializeResult), diff --git a/src/dispatchers/streamDispatcher.ts b/src/dispatchers/streamDispatcher.ts index 4e0d777ada..1495f0e5e5 100644 --- a/src/dispatchers/streamDispatcher.ts +++ b/src/dispatchers/streamDispatcher.ts @@ -17,18 +17,27 @@ import * as channels from '../protocol/channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; import * as stream from 'stream'; +import { SdkObject } from '../server/sdkObject'; -export class StreamDispatcher extends Dispatcher implements channels.StreamChannel { - constructor(scope: DispatcherScope, stream: stream.Readable) { +export class StreamWrapper extends SdkObject { + readonly stream: stream.Readable; + constructor(parentObject: SdkObject, stream: stream.Readable) { + super(parentObject); + this.stream = stream; + } +} + +export class StreamDispatcher extends Dispatcher implements channels.StreamChannel { + constructor(scope: DispatcherScope, stream: StreamWrapper) { super(scope, stream, 'Stream', {}); } async read(params: channels.StreamReadParams): Promise { - const buffer = this._object.read(Math.min(this._object.readableLength, params.size || this._object.readableLength)); + const buffer = this._object.stream.read(Math.min(this._object.stream.readableLength, params.size || this._object.stream.readableLength)); return { binary: buffer ? buffer.toString('base64') : '' }; } async close() { - this._object.destroy(); + this._object.stream.destroy(); } } diff --git a/src/server/android/android.ts b/src/server/android/android.ts index e80fa81a9f..0557f9d2ef 100644 --- a/src/server/android/android.ts +++ b/src/server/android/android.ts @@ -32,11 +32,12 @@ import { RecentLogsCollector } from '../../utils/debugLogger'; import { TimeoutSettings } from '../../utils/timeoutSettings'; import { AndroidWebView } from '../../protocol/channels'; import { CRPage } from '../chromium/crPage'; +import { SdkObject } from '../sdkObject'; const readFileAsync = util.promisify(fs.readFile); export interface Backend { - devices(): Promise; + devices(owner: SdkObject): Promise; } export interface DeviceBackend { @@ -44,22 +45,23 @@ export interface DeviceBackend { status: string; close(): Promise; init(): Promise; - runCommand(command: string): Promise; - open(command: string): Promise; + runCommand(owner: SdkObject, command: string): Promise; + open(owner: SdkObject, command: string): Promise; } -export interface SocketBackend extends EventEmitter { +export interface SocketBackend extends SdkObject { write(data: Buffer): Promise; close(): Promise; } -export class Android { +export class Android extends SdkObject { private _backend: Backend; private _devices = new Map(); readonly _timeoutSettings: TimeoutSettings; readonly _playwrightOptions: PlaywrightOptions; constructor(backend: Backend, playwrightOptions: PlaywrightOptions) { + super(playwrightOptions.rootSdkObject); this._backend = backend; this._playwrightOptions = playwrightOptions; this._timeoutSettings = new TimeoutSettings(); @@ -70,7 +72,7 @@ export class Android { } async devices(): Promise { - const devices = (await this._backend.devices()).filter(d => d.status === 'device'); + const devices = (await this._backend.devices(this)).filter(d => d.status === 'device'); const newSerials = new Set(); for (const d of devices) { newSerials.add(d.serial); @@ -91,7 +93,7 @@ export class Android { } } -export class AndroidDevice extends EventEmitter { +export class AndroidDevice extends SdkObject { readonly _backend: DeviceBackend; readonly model: string; readonly serial: string; @@ -113,8 +115,7 @@ export class AndroidDevice extends EventEmitter { private _isClosed = false; constructor(android: Android, backend: DeviceBackend, model: string) { - super(); - this.setMaxListeners(0); + super(android); this._android = android; this._backend = backend; this.model = model; @@ -124,7 +125,7 @@ export class AndroidDevice extends EventEmitter { static async create(android: Android, backend: DeviceBackend): Promise { await backend.init(); - const model = await backend.runCommand('shell:getprop ro.product.model'); + const model = await backend.runCommand(android, 'shell:getprop ro.product.model'); const device = new AndroidDevice(android, backend, model.toString().trim()); await device._init(); return device; @@ -143,17 +144,17 @@ export class AndroidDevice extends EventEmitter { } async shell(command: string): Promise { - const result = await this._backend.runCommand(`shell:${command}`); + const result = await this._backend.runCommand(this, `shell:${command}`); await this._refreshWebViews(); return result; } async open(command: string): Promise { - return await this._backend.open(`${command}`); + return await this._backend.open(this, `${command}`); } async screenshot(): Promise { - return await this._backend.runCommand(`shell:screencap -p`); + return await this._backend.runCommand(this, `shell:screencap -p`); } private async _driver(): Promise { @@ -198,7 +199,7 @@ export class AndroidDevice extends EventEmitter { debug('pw:android')(`Polling the socket localabstract:${socketName}`); while (!socket) { try { - socket = await this._backend.open(`localabstract:${socketName}`); + socket = await this._backend.open(this, `localabstract:${socketName}`); } catch (e) { await new Promise(f => setTimeout(f, 250)); } @@ -234,13 +235,13 @@ export class AndroidDevice extends EventEmitter { async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise { debug('pw:android')('Force-stopping', pkg); - await this._backend.runCommand(`shell:am force-stop ${pkg}`); + await this._backend.runCommand(this, `shell:am force-stop ${pkg}`); const socketName = 'playwright-' + createGuid(); const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`; debug('pw:android')('Starting', pkg, commandLine); - await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); - await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`); + await this._backend.runCommand(this, `shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`); + await this._backend.runCommand(this, `shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`); return await this._connectToBrowser(socketName, options); } @@ -295,7 +296,7 @@ export class AndroidDevice extends EventEmitter { async installApk(content: Buffer, options?: { args?: string[] }): Promise { const args = options && options.args ? options.args : ['-r', '-t', '-S']; debug('pw:android')('Opening install socket'); - const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`); + const installSocket = await this._backend.open(this, `shell:cmd package install ${args.join(' ')} ${content.length}`); debug('pw:android')('Writing driver bytes: ' + content.length); await installSocket.write(content); const success = await new Promise(f => installSocket.on('data', f)); @@ -304,7 +305,7 @@ export class AndroidDevice extends EventEmitter { } async push(content: Buffer, path: string, mode = 0o644): Promise { - const socket = await this._backend.open(`sync:`); + const socket = await this._backend.open(this, `sync:`); const sendHeader = async (command: string, length: number) => { const buffer = Buffer.alloc(command.length + 4); buffer.write(command, 0); @@ -328,7 +329,7 @@ export class AndroidDevice extends EventEmitter { } private async _refreshWebViews() { - const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n'); + const sockets = (await this._backend.runCommand(this, `shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n'); if (this._isClosed) return; @@ -344,7 +345,7 @@ export class AndroidDevice extends EventEmitter { if (this._webViews.has(pid)) continue; - const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n'); + const procs = (await this._backend.runCommand(this, `shell:ps -A | grep ${pid}`)).toString().split('\n'); if (this._isClosed) return; let pkg = ''; diff --git a/src/server/android/backendAdb.ts b/src/server/android/backendAdb.ts index 8709087da3..b73bb3566d 100644 --- a/src/server/android/backendAdb.ts +++ b/src/server/android/backendAdb.ts @@ -17,12 +17,12 @@ import * as assert from 'assert'; import * as debug from 'debug'; import * as net from 'net'; -import { EventEmitter } from 'ws'; +import { SdkObject } from '../sdkObject'; import { Backend, DeviceBackend, SocketBackend } from './android'; export class AdbBackend implements Backend { - async devices(): Promise { - const result = await runCommand('host:devices'); + async devices(sdkObject: SdkObject): Promise { + const result = await runCommand(sdkObject, 'host:devices'); const lines = result.toString().trim().split('\n'); return lines.map(line => { const [serial, status] = line.trim().split('\t'); @@ -46,20 +46,20 @@ class AdbDevice implements DeviceBackend { async close() { } - runCommand(command: string): Promise { - return runCommand(command, this.serial); + runCommand(sdkObject: SdkObject, command: string): Promise { + return runCommand(sdkObject, command, this.serial); } - async open(command: string): Promise { - const result = await open(command, this.serial); + async open(sdkObject: SdkObject, command: string): Promise { + const result = await open(sdkObject, command, this.serial); result.becomeSocket(); return result; } } -async function runCommand(command: string, serial?: string): Promise { +async function runCommand(sdkObject: SdkObject, command: string, serial?: string): Promise { debug('pw:adb:runCommand')(command, serial); - const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); + const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 })); if (serial) { await socket.write(encodeMessage(`host:transport:${serial}`)); const status = await socket.read(4); @@ -79,8 +79,8 @@ async function runCommand(command: string, serial?: string): Promise { return commandOutput; } -async function open(command: string, serial?: string): Promise { - const socket = new BufferedSocketWrapper(command, net.createConnection({ port: 5037 })); +async function open(sdkObject: SdkObject, command: string, serial?: string): Promise { + const socket = new BufferedSocketWrapper(sdkObject, command, net.createConnection({ port: 5037 })); if (serial) { await socket.write(encodeMessage(`host:transport:${serial}`)); const status = await socket.read(4); @@ -98,7 +98,7 @@ function encodeMessage(message: string): Buffer { return Buffer.from(lenHex + message); } -class BufferedSocketWrapper extends EventEmitter implements SocketBackend { +class BufferedSocketWrapper extends SdkObject implements SocketBackend { private _socket: net.Socket; private _buffer = Buffer.from([]); private _isSocket = false; @@ -107,9 +107,8 @@ class BufferedSocketWrapper extends EventEmitter implements SocketBackend { private _isClosed = false; private _command: string; - constructor(command: string, socket: net.Socket) { - super(); - this.setMaxListeners(0); + constructor(parent: SdkObject, command: string, socket: net.Socket) { + super(parent); this._command = command; this._socket = socket; this._connectPromise = new Promise(f => this._socket.on('connect', f)); diff --git a/src/server/browser.ts b/src/server/browser.ts index 39de1a4415..a51e7b4104 100644 --- a/src/server/browser.ts +++ b/src/server/browser.ts @@ -17,12 +17,13 @@ import * as types from './types'; import { BrowserContext, ContextListener, Video } from './browserContext'; import { Page } from './page'; -import { EventEmitter } from 'events'; import { Download } from './download'; import { ProxySettings } from './types'; import { ChildProcess } from 'child_process'; import { RecentLogsCollector } from '../utils/debugLogger'; import * as registry from '../utils/registry'; +import { SdkObject } from './sdkObject'; +import { Selectors } from './selectors'; export interface BrowserProcess { onclose: ((exitCode: number | null, signal: string | null) => void) | undefined; @@ -34,7 +35,10 @@ export interface BrowserProcess { export type PlaywrightOptions = { contextListeners: ContextListener[], registry: registry.Registry, - isInternal: boolean + isInternal: boolean, + rootSdkObject: SdkObject, + // FIXME, this is suspicious + selectors: Selectors }; export type BrowserOptions = PlaywrightOptions & { @@ -50,7 +54,7 @@ export type BrowserOptions = PlaywrightOptions & { slowMo?: number, }; -export abstract class Browser extends EventEmitter { +export abstract class Browser extends SdkObject { static Events = { Disconnected: 'disconnected', }; @@ -62,8 +66,8 @@ export abstract class Browser extends EventEmitter { readonly _idToVideo = new Map(); constructor(options: BrowserOptions) { - super(); - this.setMaxListeners(0); + super(options.rootSdkObject); + this.attribution.browser = this; this.options = options; } diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index cbc03f4dab..5089450631 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -15,7 +15,6 @@ * limitations under the License. */ -import { EventEmitter } from 'events'; import { TimeoutSettings } from '../utils/timeoutSettings'; import { mkdirIfNeeded } from '../utils/utils'; import { Browser, BrowserOptions } from './browser'; @@ -26,9 +25,10 @@ import { helper } from './helper'; import * as network from './network'; import { Page, PageBinding, PageDelegate } from './page'; import { Progress, ProgressController, ProgressResult } from './progress'; -import { Selectors, serverSelectors } from './selectors'; +import { Selectors } from './selectors'; import * as types from './types'; import * as path from 'path'; +import { SdkObject } from './sdkObject'; export class Video { readonly _videoId: string; @@ -94,7 +94,7 @@ export interface ContextListener { onContextDidDestroy(context: BrowserContext): Promise; } -export abstract class BrowserContext extends EventEmitter { +export abstract class BrowserContext extends SdkObject { static Events = { Close: 'close', Page: 'page', @@ -122,8 +122,8 @@ export abstract class BrowserContext extends EventEmitter { terminalSize: { rows?: number, columns?: number } = {}; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { - super(); - this.setMaxListeners(0); + super(browser); + this.attribution.context = this; this._browser = browser; this._options = options; this._browserContextId = browserContextId; @@ -135,8 +135,8 @@ export abstract class BrowserContext extends EventEmitter { this._selectors = selectors; } - selectors() { - return this._selectors || serverSelectors; + selectors(): Selectors { + return this._selectors || this._browser.options.selectors; } async _initialize() { diff --git a/src/server/browserType.ts b/src/server/browserType.ts index a4908dcdbc..2b87f07f93 100644 --- a/src/server/browserType.ts +++ b/src/server/browserType.ts @@ -31,18 +31,21 @@ import { validateHostRequirements } from './validateDependencies'; import { isDebugMode } from '../utils/utils'; import { helper } from './helper'; import { RecentLogsCollector } from '../utils/debugLogger'; +import { SdkObject } from './sdkObject'; const mkdirAsync = util.promisify(fs.mkdir); const mkdtempAsync = util.promisify(fs.mkdtemp); const existsAsync = (path: string): Promise => new Promise(resolve => fs.stat(path, err => resolve(!err))); const DOWNLOADS_FOLDER = path.join(os.tmpdir(), 'playwright_downloads-'); -export abstract class BrowserType { +export abstract class BrowserType extends SdkObject { private _name: registry.BrowserName; readonly _registry: registry.Registry; readonly _playwrightOptions: PlaywrightOptions; constructor(browserName: registry.BrowserName, playwrightOptions: PlaywrightOptions) { + super(playwrightOptions.rootSdkObject); + this.attribution.browserType = this; this._playwrightOptions = playwrightOptions; this._name = browserName; this._registry = playwrightOptions.registry; diff --git a/src/server/chromium/crBrowser.ts b/src/server/chromium/crBrowser.ts index aab3e7866c..794f987339 100644 --- a/src/server/chromium/crBrowser.ts +++ b/src/server/chromium/crBrowser.ts @@ -263,7 +263,7 @@ class CRServiceWorker extends Worker { readonly _browserContext: CRBrowserContext; constructor(browserContext: CRBrowserContext, session: CRSession, url: string) { - super(url); + super(browserContext, url); this._browserContext = browserContext; session.once('Runtime.executionContextCreated', event => { this._createExecutionContext(new CRExecutionContext(session, event.context)); diff --git a/src/server/chromium/crConnection.ts b/src/server/chromium/crConnection.ts index c590a3acce..1c3dde03c8 100644 --- a/src/server/chromium/crConnection.ts +++ b/src/server/chromium/crConnection.ts @@ -23,6 +23,7 @@ import { rewriteErrorMessage } from '../../utils/stackTrace'; import { debugLogger, RecentLogsCollector } from '../../utils/debugLogger'; import { ProtocolLogger } from '../types'; import { helper } from '../helper'; +import { SdkObject } from '../sdkObject'; export const ConnectionEvents = { Disconnected: Symbol('ConnectionEvents.Disconnected') @@ -123,7 +124,7 @@ export const CRSessionEvents = { Disconnected: Symbol('Events.CDPSession.Disconnected') }; -export class CRSession extends EventEmitter { +export class CRSession extends SdkObject { _connection: CRConnection | null; _eventListener?: (method: string, params?: Object) => void; private readonly _callbacks = new Map void, reject: (e: Error) => void, error: Error, method: string}>(); @@ -139,8 +140,7 @@ export class CRSession extends EventEmitter { once: (event: T, listener: (payload: T extends symbol ? any : Protocol.Events[T extends keyof Protocol.Events ? T : never]) => void) => this; constructor(connection: CRConnection, rootSessionId: string, targetType: string, sessionId: string) { - super(); - this.setMaxListeners(0); + super(null); this._connection = connection; this._rootSessionId = rootSessionId; this._targetType = targetType; diff --git a/src/server/chromium/crPage.ts b/src/server/chromium/crPage.ts index ae3d8e660e..b5911c2d11 100644 --- a/src/server/chromium/crPage.ts +++ b/src/server/chromium/crPage.ts @@ -639,7 +639,7 @@ class FrameSession { } const url = event.targetInfo.url; - const worker = new Worker(url); + const worker = new Worker(this._page, url); this._page._addWorker(event.sessionId, worker); session.once('Runtime.executionContextCreated', async event => { worker._createExecutionContext(new CRExecutionContext(session, event.context)); @@ -759,7 +759,7 @@ class FrameSession { lineNumber: lineNumber || 0, columnNumber: 0, }; - this._page.emit(Page.Events.Console, new ConsoleMessage(level, text, [], location)); + this._page.emit(Page.Events.Console, new ConsoleMessage(this._page, level, text, [], location)); } } diff --git a/src/server/console.ts b/src/server/console.ts index 08b8367df9..f4a2ad66a6 100644 --- a/src/server/console.ts +++ b/src/server/console.ts @@ -15,15 +15,17 @@ */ import * as js from './javascript'; +import { SdkObject } from './sdkObject'; import { ConsoleMessageLocation } from './types'; -export class ConsoleMessage { +export class ConsoleMessage extends SdkObject { private _type: string; private _text?: string; private _args: js.JSHandle[]; private _location: ConsoleMessageLocation; - constructor(type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { + constructor(parent: SdkObject, type: string, text: string | undefined, args: js.JSHandle[], location?: ConsoleMessageLocation) { + super(parent); this._type = type; this._text = text; this._args = args; diff --git a/src/server/dialog.ts b/src/server/dialog.ts index 89067b0241..f10cc1b239 100644 --- a/src/server/dialog.ts +++ b/src/server/dialog.ts @@ -17,12 +17,13 @@ import { assert } from '../utils/utils'; import { Page } from './page'; +import { SdkObject } from './sdkObject'; type OnHandle = (accept: boolean, promptText?: string) => Promise; export type DialogType = 'alert' | 'beforeunload' | 'confirm' | 'prompt'; -export class Dialog { +export class Dialog extends SdkObject { private _page: Page; private _type: string; private _message: string; @@ -31,6 +32,7 @@ export class Dialog { private _defaultValue: string; constructor(page: Page, type: string, message: string, onHandle: OnHandle, defaultValue?: string) { + super(page); this._page = page; this._type = type; this._message = message; diff --git a/src/server/dom.ts b/src/server/dom.ts index 99adfd0df9..405e1684d2 100644 --- a/src/server/dom.ts +++ b/src/server/dom.ts @@ -31,7 +31,7 @@ export class FrameExecutionContext extends js.ExecutionContext { readonly world: types.World | null; constructor(delegate: js.ExecutionContextDelegate, frame: frames.Frame, world: types.World|null) { - super(delegate); + super(frame, delegate); this.frame = frame; this.world = world; } diff --git a/src/server/download.ts b/src/server/download.ts index 687283bce1..1cbf7a2c47 100644 --- a/src/server/download.ts +++ b/src/server/download.ts @@ -19,10 +19,11 @@ import * as fs from 'fs'; import * as util from 'util'; import { Page } from './page'; import { assert } from '../utils/utils'; +import { SdkObject } from './sdkObject'; type SaveCallback = (localPath: string, error?: string) => Promise; -export class Download { +export class Download extends SdkObject { private _downloadsPath: string; private _uuid: string; private _finishedCallback: () => void; @@ -37,6 +38,7 @@ export class Download { private _suggestedFilename: string | undefined; constructor(page: Page, downloadsPath: string, uuid: string, url: string, suggestedFilename?: string) { + super(page); this._page = page; this._downloadsPath = downloadsPath; this._uuid = uuid; diff --git a/src/server/electron/electron.ts b/src/server/electron/electron.ts index 98082e693f..dace10389d 100644 --- a/src/server/electron/electron.ts +++ b/src/server/electron/electron.ts @@ -27,12 +27,12 @@ import { launchProcess, envArrayToObject } from '../processLauncher'; import { BrowserContext } from '../browserContext'; import type {BrowserWindow} from 'electron'; import { Progress, ProgressController, runAbortableTask } from '../progress'; -import { EventEmitter } from 'events'; import { helper } from '../helper'; import { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser'; import * as childProcess from 'child_process'; import * as readline from 'readline'; import { RecentLogsCollector } from '../../utils/debugLogger'; +import { SdkObject } from '../sdkObject'; export type ElectronLaunchOptionsBase = { executablePath?: string, @@ -47,7 +47,7 @@ export interface ElectronPage extends Page { _browserWindowId: number; } -export class ElectronApplication extends EventEmitter { +export class ElectronApplication extends SdkObject { static Events = { Close: 'close', Window: 'window', @@ -62,9 +62,8 @@ export class ElectronApplication extends EventEmitter { private _lastWindowId = 0; readonly _timeoutSettings = new TimeoutSettings(); - constructor(browser: CRBrowser, nodeConnection: CRConnection) { - super(); - this.setMaxListeners(0); + constructor(parent: SdkObject, browser: CRBrowser, nodeConnection: CRConnection) { + super(parent); this._browserContext = browser._defaultContext as CRBrowserContext; this._browserContext.on(BrowserContext.Events.Close, () => { // Emit application closed after context closed. @@ -115,17 +114,18 @@ export class ElectronApplication extends EventEmitter { async _init() { this._nodeSession.on('Runtime.executionContextCreated', (event: any) => { if (event.context.auxData && event.context.auxData.isDefault) - this._nodeExecutionContext = new js.ExecutionContext(new CRExecutionContext(this._nodeSession, event.context)); + this._nodeExecutionContext = new js.ExecutionContext(this, new CRExecutionContext(this._nodeSession, event.context)); }); await this._nodeSession.send('Runtime.enable', {}).catch(e => {}); this._nodeElectronHandle = await js.evaluate(this._nodeExecutionContext!, false /* returnByValue */, `process.mainModule.require('electron')`); } } -export class Electron { +export class Electron extends SdkObject { private _playwrightOptions: PlaywrightOptions; constructor(playwrightOptions: PlaywrightOptions) { + super(playwrightOptions.rootSdkObject); this._playwrightOptions = playwrightOptions; } @@ -187,7 +187,7 @@ export class Electron { browserLogsCollector, }; const browser = await CRBrowser.connect(chromeTransport, browserOptions); - app = new ElectronApplication(browser, nodeConnection); + app = new ElectronApplication(this, browser, nodeConnection); await app._init(); return app; }, TimeoutSettings.timeout(options)); diff --git a/src/server/firefox/ffPage.ts b/src/server/firefox/ffPage.ts index 9abda52421..46352eaa1b 100644 --- a/src/server/firefox/ffPage.ts +++ b/src/server/firefox/ffPage.ts @@ -244,7 +244,7 @@ export class FFPage implements PageDelegate { async _onWorkerCreated(event: Protocol.Page.workerCreatedPayload) { const workerId = event.workerId; - const worker = new Worker(event.url); + const worker = new Worker(this._page, event.url); const workerSession = new FFSession(this._session._connection, 'worker', workerId, (message: any) => { this._session.send('Page.sendMessageToWorker', { frameId: event.frameId, diff --git a/src/server/frames.ts b/src/server/frames.ts index aa9a2a4a38..73bff8e193 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -24,9 +24,9 @@ import { Page } from './page'; import * as types from './types'; import { BrowserContext } from './browserContext'; import { Progress, ProgressController, runAbortableTask } from './progress'; -import { EventEmitter } from 'events'; import { assert, makeWaitForNextTask } from '../utils/utils'; import { debugLogger } from '../utils/debugLogger'; +import { SdkObject } from './sdkObject'; type ContextData = { contextPromise: Promise; @@ -342,7 +342,7 @@ export class FrameManager { } onWebSocketCreated(requestId: string, url: string) { - const ws = new network.WebSocket(url); + const ws = new network.WebSocket(this._page, url); this._webSockets.set(requestId, ws); } @@ -386,7 +386,7 @@ export class FrameManager { } } -export class Frame extends EventEmitter { +export class Frame extends SdkObject { static Events = { Navigation: 'navigation', AddLifecycle: 'addlifecycle', @@ -412,8 +412,8 @@ export class Frame extends EventEmitter { private _detachedCallback = () => {}; constructor(page: Page, id: string, parentFrame: Frame | null) { - super(); - this.setMaxListeners(0); + super(page); + this.attribution.frame = this; this._id = id; this._page = page; this._parentFrame = parentFrame; diff --git a/src/server/javascript.ts b/src/server/javascript.ts index 7f92670d71..c014131de8 100644 --- a/src/server/javascript.ts +++ b/src/server/javascript.ts @@ -18,6 +18,7 @@ import * as dom from './dom'; import * as utilityScriptSource from '../generated/utilityScriptSource'; import { serializeAsCallArgument } from './common/utilityScriptSerializers'; import type UtilityScript from './injected/utilityScript'; +import { SdkObject } from './sdkObject'; type ObjectId = string; export type RemoteObject = { @@ -49,11 +50,12 @@ export interface ExecutionContextDelegate { releaseHandle(handle: JSHandle): Promise; } -export class ExecutionContext { +export class ExecutionContext extends SdkObject { readonly _delegate: ExecutionContextDelegate; private _utilityScriptPromise: Promise | undefined; - constructor(delegate: ExecutionContextDelegate) { + constructor(parent: SdkObject, delegate: ExecutionContextDelegate) { + super(parent); this._delegate = delegate; } @@ -82,7 +84,7 @@ export class ExecutionContext { } } -export class JSHandle { +export class JSHandle extends SdkObject { readonly _context: ExecutionContext; _disposed = false; readonly _objectId: ObjectId | undefined; @@ -92,6 +94,7 @@ export class JSHandle { private _previewCallback: ((preview: string) => void) | undefined; constructor(context: ExecutionContext, type: string, objectId?: ObjectId, value?: any) { + super(context); this._context = context; this._objectId = objectId; this._value = value; diff --git a/src/server/network.ts b/src/server/network.ts index d70a677f06..22038fa866 100644 --- a/src/server/network.ts +++ b/src/server/network.ts @@ -17,7 +17,7 @@ import * as frames from './frames'; import * as types from './types'; import { assert } from '../utils/utils'; -import { EventEmitter } from 'events'; +import { SdkObject } from './sdkObject'; export function filterCookies(cookies: types.NetworkCookie[], urls: string[]): types.NetworkCookie[] { const parsedURLs = urls.map(s => new URL(s)); @@ -78,7 +78,7 @@ export function stripFragmentFromUrl(url: string): string { return url.substring(0, url.indexOf('#')); } -export class Request { +export class Request extends SdkObject { readonly _routeDelegate: RouteDelegate | null; private _response: Response | null = null; private _redirectedFrom: Request | null; @@ -99,6 +99,7 @@ export class Request { constructor(routeDelegate: RouteDelegate | null, frame: frames.Frame, redirectedFrom: Request | null, documentId: string | undefined, url: string, resourceType: string, method: string, postData: Buffer | null, headers: types.HeadersArray) { + super(frame); assert(!url.startsWith('data:'), 'Data urls should not fire requests'); assert(!(routeDelegate && redirectedFrom), 'Should not be able to intercept redirects'); this._routeDelegate = routeDelegate; @@ -203,12 +204,13 @@ export class Request { } } -export class Route { +export class Route extends SdkObject { private readonly _request: Request; private readonly _delegate: RouteDelegate; private _handled = false; constructor(request: Request, delegate: RouteDelegate) { + super(request.frame()); this._request = request; this._delegate = delegate; } @@ -261,7 +263,7 @@ export type ResourceTiming = { responseStart: number; }; -export class Response { +export class Response extends SdkObject { private _request: Request; private _contentPromise: Promise | null = null; _finishedPromise: Promise<{ error?: string }>; @@ -275,6 +277,7 @@ export class Response { private _timing: ResourceTiming; constructor(request: Request, status: number, statusText: string, headers: types.HeadersArray, timing: ResourceTiming, getResponseBodyCallback: GetResponseBodyCallback) { + super(request.frame()); this._request = request; this._timing = timing; this._status = status; @@ -343,7 +346,7 @@ export class Response { } } -export class WebSocket extends EventEmitter { +export class WebSocket extends SdkObject { private _url: string; static Events = { @@ -353,9 +356,8 @@ export class WebSocket extends EventEmitter { FrameSent: 'framesent', }; - constructor(url: string) { - super(); - this.setMaxListeners(0); + constructor(parent: SdkObject, url: string) { + super(parent); this._url = url; } diff --git a/src/server/page.ts b/src/server/page.ts index b7a630858c..5827bf1d49 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -26,12 +26,12 @@ import * as types from './types'; import { BrowserContext, Video } from './browserContext'; import { ConsoleMessage } from './console'; import * as accessibility from './accessibility'; -import { EventEmitter } from 'events'; import { FileChooser } from './fileChooser'; import { ProgressController, runAbortableTask } from './progress'; import { assert, isError } from '../utils/utils'; import { debugLogger } from '../utils/debugLogger'; import { Selectors } from './selectors'; +import { SdkObject } from './sdkObject'; export interface PageDelegate { readonly rawMouse: input.RawMouse; @@ -92,7 +92,7 @@ type PageState = { extraHTTPHeaders: types.HeadersArray | null; }; -export class Page extends EventEmitter { +export class Page extends SdkObject { static Events = { Close: 'close', Crash: 'crash', @@ -149,8 +149,8 @@ export class Page extends EventEmitter { _video: Video | null = null; constructor(delegate: PageDelegate, browserContext: BrowserContext) { - super(); - this.setMaxListeners(0); + super(browserContext); + this.attribution.page = this; this._delegate = delegate; this._closedCallback = () => {}; this._closedPromise = new Promise(f => this._closedCallback = f); @@ -288,7 +288,7 @@ export class Page extends EventEmitter { } _addConsoleMessage(type: string, args: js.JSHandle[], location: types.ConsoleMessageLocation, text?: string) { - const message = new ConsoleMessage(type, text, args, location); + const message = new ConsoleMessage(this, type, text, args, location); const intercepted = this._frameManager.interceptConsoleMessage(message); if (intercepted || !this.listenerCount(Page.Events.Console)) args.forEach(arg => arg.dispose()); @@ -502,7 +502,7 @@ export class Page extends EventEmitter { } } -export class Worker extends EventEmitter { +export class Worker extends SdkObject { static Events = { Close: 'close', }; @@ -512,16 +512,15 @@ export class Worker extends EventEmitter { private _executionContextCallback: (value: js.ExecutionContext) => void; _existingExecutionContext: js.ExecutionContext | null = null; - constructor(url: string) { - super(); - this.setMaxListeners(0); + constructor(parent: SdkObject, url: string) { + super(parent); this._url = url; this._executionContextCallback = () => {}; this._executionContextPromise = new Promise(x => this._executionContextCallback = x); } _createExecutionContext(delegate: js.ExecutionContextDelegate) { - this._existingExecutionContext = new js.ExecutionContext(delegate); + this._existingExecutionContext = new js.ExecutionContext(this, delegate); this._executionContextCallback(this._existingExecutionContext); } diff --git a/src/server/playwright.ts b/src/server/playwright.ts index b9779026d1..6e7a9b1b18 100644 --- a/src/server/playwright.ts +++ b/src/server/playwright.ts @@ -22,14 +22,15 @@ import { PlaywrightOptions } from './browser'; import { Chromium } from './chromium/chromium'; import { Electron } from './electron/electron'; import { Firefox } from './firefox/firefox'; -import { serverSelectors } from './selectors'; +import { Selectors } from './selectors'; import { HarTracer } from './supplements/har/harTracer'; import { InspectorController } from './supplements/inspectorController'; import { WebKit } from './webkit/webkit'; import { Registry } from '../utils/registry'; +import { SdkObject } from './sdkObject'; -export class Playwright { - readonly selectors = serverSelectors; +export class Playwright extends SdkObject { + readonly selectors: Selectors; readonly chromium: Chromium; readonly android: Android; readonly electron: Electron; @@ -38,6 +39,8 @@ export class Playwright { readonly options: PlaywrightOptions; constructor(isInternal: boolean) { + super(null); + this.selectors = new Selectors(this); this.options = { isInternal, registry: new Registry(path.join(__dirname, '..', '..')), @@ -46,7 +49,9 @@ export class Playwright { new InspectorController(), new Tracer(), new HarTracer() - ] + ], + rootSdkObject: this, + selectors: this.selectors }; this.chromium = new Chromium(this.options); this.firefox = new Firefox(this.options); diff --git a/src/server/sdkObject.ts b/src/server/sdkObject.ts new file mode 100644 index 0000000000..28d6e44daf --- /dev/null +++ b/src/server/sdkObject.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) Microsoft Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { EventEmitter } from 'events'; +import type { Browser } from './browser'; +import type { BrowserContext } from './browserContext'; +import type { BrowserType } from './browserType'; +import type { Frame } from './frames'; +import type { Page } from './page'; + +export type Attribution = { + browserType?: BrowserType; + browser?: Browser; + context?: BrowserContext; + page?: Page; + frame?: Frame; +}; + +export class SdkObject extends EventEmitter { + attribution: Attribution; + constructor(parent: SdkObject | null) { + super(); + this.setMaxListeners(0); + this.attribution = { ...parent?.attribution }; + } +} diff --git a/src/server/selectors.ts b/src/server/selectors.ts index f241ff00a8..3b785b6ae8 100644 --- a/src/server/selectors.ts +++ b/src/server/selectors.ts @@ -19,6 +19,7 @@ import * as frames from './frames'; import * as js from './javascript'; import * as types from './types'; import { ParsedSelector, parseSelector } from './common/selectorParser'; +import { SdkObject } from './sdkObject'; export type SelectorInfo = { parsed: ParsedSelector, @@ -26,11 +27,12 @@ export type SelectorInfo = { selector: string, }; -export class Selectors { +export class Selectors extends SdkObject { readonly _builtinEngines: Set; readonly _engines: Map; - constructor() { + constructor(parent: SdkObject) { + super(parent); // Note: keep in sync with InjectedScript class. this._builtinEngines = new Set([ 'css', 'css:light', @@ -134,4 +136,6 @@ export class Selectors { } } -export const serverSelectors = new Selectors(); +export function serverSelectors(parent: SdkObject) { + return new Selectors(parent); +} diff --git a/src/server/validateDependencies.ts b/src/server/validateDependencies.ts index 1075416d82..8e6017c810 100644 --- a/src/server/validateDependencies.ts +++ b/src/server/validateDependencies.ts @@ -189,7 +189,7 @@ function isSharedLib(basename: string) { async function executablesOrSharedLibraries(directoryPath: string): Promise { const allPaths = (await readdirAsync(directoryPath)).map(file => path.resolve(directoryPath, file)); const allStats = await Promise.all(allPaths.map(aPath => statAsync(aPath))); - const filePaths = allPaths.filter((aPath, index) => allStats[index].isFile()); + const filePaths = allPaths.filter((aPath, index) => (allStats[index] as any).isFile()); const executablersOrLibraries = (await Promise.all(filePaths.map(async filePath => { const basename = path.basename(filePath).toLowerCase(); diff --git a/src/server/webkit/wkWorkers.ts b/src/server/webkit/wkWorkers.ts index 4208b8872c..d8ba71f0f6 100644 --- a/src/server/webkit/wkWorkers.ts +++ b/src/server/webkit/wkWorkers.ts @@ -35,7 +35,7 @@ export class WKWorkers { this.clear(); this._sessionListeners = [ helper.addEventListener(session, 'Worker.workerCreated', (event: Protocol.Worker.workerCreatedPayload) => { - const worker = new Worker(event.url); + const worker = new Worker(this._page, event.url); const workerSession = new WKSession(session.connection, event.workerId, 'Most likely the worker has been closed.', (message: any) => { session.send('Worker.sendMessageToWorker', { workerId: event.workerId, diff --git a/tsconfig.json b/tsconfig.json index 8e42ac765a..5f593d648c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "ESNext", + "target": "es2018", "module": "commonjs", "lib": ["esnext", "dom", "DOM.Iterable"], "sourceMap": true,