diff --git a/src/browserServerImpl.ts b/src/browserServerImpl.ts index a589b695b1..54aa1b30f1 100644 --- a/src/browserServerImpl.ts +++ b/src/browserServerImpl.ts @@ -17,7 +17,7 @@ import { LaunchServerOptions, Logger } from './client/types'; import { Browser } from './server/browser'; import { EventEmitter } from 'ws'; -import { Dispatcher, DispatcherScope } from './dispatchers/dispatcher'; +import { Dispatcher, DispatcherConnection, DispatcherScope, Root } from './dispatchers/dispatcher'; import { BrowserContextDispatcher } from './dispatchers/browserContextDispatcher'; import * as channels from './protocol/channels'; import { BrowserServerLauncher, BrowserServer } from './client/browserType'; @@ -80,18 +80,21 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { return browserServer; } - private async _onConnect(playwright: Playwright, browser: Browser, scope: DispatcherScope, forceDisconnect: () => void) { - const selectors = new Selectors(); - const selectorsDispatcher = new SelectorsDispatcher(scope, selectors); - const browserDispatcher = new ConnectedBrowserDispatcher(scope, browser, selectors); - browser.on(Browser.Events.Disconnected, () => { - // Underlying browser did close for some reason - force disconnect the client. - forceDisconnect(); + private async _onConnect(playwright: Playwright, browser: Browser, connection: DispatcherConnection, forceDisconnect: () => void) { + let browserDispatcher: ConnectedBrowserDispatcher | undefined; + new Root(connection, async (scope: DispatcherScope): Promise => { + const selectors = new Selectors(); + const selectorsDispatcher = new SelectorsDispatcher(scope, selectors); + browserDispatcher = new ConnectedBrowserDispatcher(scope, browser, selectors); + browser.on(Browser.Events.Disconnected, () => { + // Underlying browser did close for some reason - force disconnect the client. + forceDisconnect(); + }); + return new PlaywrightDispatcher(scope, playwright, selectorsDispatcher, browserDispatcher); }); - new PlaywrightDispatcher(scope, playwright, selectorsDispatcher, browserDispatcher); return () => { // Cleanup contexts upon disconnect. - browserDispatcher.cleanupContexts().catch(e => {}); + browserDispatcher?.cleanupContexts().catch(e => {}); }; } } diff --git a/src/cli/driver.ts b/src/cli/driver.ts index b55de4ba31..0b8eec62ee 100644 --- a/src/cli/driver.ts +++ b/src/cli/driver.ts @@ -20,7 +20,7 @@ import fs from 'fs'; import * as playwright from '../..'; import { BrowserType } from '../client/browserType'; import { LaunchServerOptions } from '../client/types'; -import { DispatcherConnection } from '../dispatchers/dispatcher'; +import { DispatcherConnection, Root } from '../dispatchers/dispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { Transport } from '../protocol/transport'; import { PlaywrightServer, PlaywrightServerOptions } from '../remote/playwrightServer'; @@ -34,6 +34,10 @@ export function printApiJson() { export function runDriver() { const dispatcherConnection = new DispatcherConnection(); + new Root(dispatcherConnection, async rootScope => { + const playwright = createPlaywright(); + return new PlaywrightDispatcher(rootScope, playwright); + }); const transport = new Transport(process.stdout, process.stdin); transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message)); dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message)); @@ -46,9 +50,6 @@ export function runDriver() { await gracefullyCloseAll(); process.exit(0); }; - - const playwright = createPlaywright(); - new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright); } export async function runServer(port: number | undefined, configFile?: string) { diff --git a/src/client/browserType.ts b/src/client/browserType.ts index 4fdf13c61c..17282dfc66 100644 --- a/src/client/browserType.ts +++ b/src/client/browserType.ts @@ -28,7 +28,6 @@ import { envObjectToArray } from './clientHelper'; import { assert, headersObjectToArray, makeWaitForNextTask, getUserAgent } from '../utils/utils'; import { kBrowserClosedError } from '../utils/errors'; import * as api from '../../types/types'; -import type { Playwright } from './playwright'; export interface BrowserServerLauncher { launchServer(options?: LaunchServerOptions): Promise; @@ -182,7 +181,7 @@ export class BrowserType extends ChannelOwner { +class Root extends ChannelOwner { constructor(connection: Connection) { - super(connection, '', '', {}); + super(connection, 'Root', '', {}); + } + + async initialize(): Promise { + return Playwright.from((await this._channel.initialize({ + language: 'javascript', + })).playwright); } } @@ -52,7 +58,7 @@ export class Connection extends EventEmitter { onmessage = (message: object): void => {}; private _lastId = 0; private _callbacks = new Map void, reject: (a: Error) => void, metadata: channels.Metadata }>(); - private _rootObject: ChannelOwner; + private _rootObject: Root; private _disconnectedErrorMessage: string | undefined; private _onClose?: () => void; @@ -62,10 +68,8 @@ export class Connection extends EventEmitter { this._onClose = onClose; } - async waitForObjectWithKnownName(guid: string): Promise { - if (this._objects.has(guid)) - return this._objects.get(guid)!; - return new Promise(f => this._waitingForObject.set(guid, f)); + async initializePlaywright(): Promise { + return await this._rootObject.initialize(); } pendingProtocolCalls(): channels.Metadata[] { diff --git a/src/client/playwright.ts b/src/client/playwright.ts index 061696a41d..0e2025ea8d 100644 --- a/src/client/playwright.ts +++ b/src/client/playwright.ts @@ -65,6 +65,10 @@ export class Playwright extends ChannelOwner SocksSocket.from(socket)); } + static from(channel: channels.PlaywrightChannel): Playwright { + return (channel as any)._object; + } + async _enablePortForwarding(ports: number[]) { this._forwardPorts = ports; await this._channel.setForwardedPorts({ports}); diff --git a/src/dispatchers/dispatcher.ts b/src/dispatchers/dispatcher.ts index 518e30a5a2..c294424e13 100644 --- a/src/dispatchers/dispatcher.ts +++ b/src/dispatchers/dispatcher.ts @@ -23,6 +23,7 @@ import { tOptional } from '../protocol/validatorPrimitives'; import { kBrowserOrContextClosedError } from '../utils/errors'; import { CallMetadata, SdkObject } from '../server/instrumentation'; import { rewriteErrorMessage } from '../utils/stackTrace'; +import type { PlaywrightDispatcher } from './playwrightDispatcher'; export const dispatcherSymbol = Symbol('dispatcher'); @@ -121,15 +122,25 @@ export class Dispatcher extends Even } export type DispatcherScope = Dispatcher; -class Root extends Dispatcher<{ guid: '' }, {}> { - constructor(connection: DispatcherConnection) { - super(connection, { guid: '' }, '', {}, true); +export class Root extends Dispatcher<{ guid: '' }, {}> { + private _initialized = false; + + constructor(connection: DispatcherConnection, private readonly createPlaywright?: (scope: DispatcherScope) => Promise) { + super(connection, { guid: '' }, 'Root', {}, true); + } + + async initialize(params: { language?: string }): Promise { + assert(this.createPlaywright); + assert(!this._initialized); + this._initialized = true; + return { + playwright: await this.createPlaywright(this), + }; } } export class DispatcherConnection { readonly _dispatchers = new Map>(); - private _rootDispatcher: Root; onmessage = (message: object) => {}; private _validateParams: (type: string, method: string, params: any) => any; private _validateMetadata: (metadata: any) => { stack?: channels.StackFrame[] }; @@ -157,8 +168,6 @@ export class DispatcherConnection { } constructor() { - this._rootDispatcher = new Root(this); - const tChannel = (name: string): Validator => { return (arg: any, path: string) => { if (arg && typeof arg === 'object' && typeof arg.guid === 'string') { @@ -185,10 +194,6 @@ export class DispatcherConnection { }; } - rootDispatcher(): Dispatcher { - return this._rootDispatcher; - } - async dispatch(message: object) { const { id, guid, method, params, metadata } = message as any; const dispatcher = this._dispatchers.get(guid); @@ -197,7 +202,8 @@ export class DispatcherConnection { return; } if (method === 'debugScopeState') { - this.onmessage({ id, result: this._rootDispatcher._debugScopeState() }); + const rootDispatcher = this._dispatchers.get('')!; + this.onmessage({ id, result: rootDispatcher._debugScopeState() }); return; } diff --git a/src/inprocess.ts b/src/inprocess.ts index e7ff0c3b1e..9bd5b41fc8 100644 --- a/src/inprocess.ts +++ b/src/inprocess.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { DispatcherConnection } from './dispatchers/dispatcher'; +import { DispatcherConnection, Root } from './dispatchers/dispatcher'; import { createPlaywright } from './server/playwright'; import type { Playwright as PlaywrightAPI } from './client/playwright'; import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher'; @@ -31,8 +31,10 @@ function setupInProcess(): PlaywrightAPI { dispatcherConnection.onmessage = message => clientConnection.dispatch(message); clientConnection.onmessage = message => dispatcherConnection.dispatch(message); + const rootScope = new Root(dispatcherConnection); + // Initialize Playwright channel. - new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright); + new PlaywrightDispatcher(rootScope, playwright); const playwrightAPI = clientConnection.getObjectWithKnownName('Playwright') as PlaywrightAPI; playwrightAPI.chromium._serverLauncher = new BrowserServerLauncherImpl('chromium'); playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox'); diff --git a/src/outofprocess.ts b/src/outofprocess.ts index ff5edb0c91..b283c8e578 100644 --- a/src/outofprocess.ts +++ b/src/outofprocess.ts @@ -52,7 +52,7 @@ class PlaywrightClient { transport.onmessage = message => connection.dispatch(JSON.parse(message)); this._closePromise = new Promise(f => transport.onclose = f); - this._playwright = connection.waitForObjectWithKnownName('Playwright'); + this._playwright = connection.initializePlaywright(); } async stop() { diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index de06908f9b..1d087454e1 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -152,6 +152,21 @@ export type InterceptedResponse = { }[], }; +// ----------- Root ----------- +export type RootInitializer = {}; +export interface RootChannel extends Channel { + initialize(params: RootInitializeParams, metadata?: Metadata): Promise; +} +export type RootInitializeParams = { + language: string, +}; +export type RootInitializeOptions = { + +}; +export type RootInitializeResult = { + playwright: PlaywrightChannel, +}; + // ----------- Playwright ----------- export type PlaywrightInitializer = { chromium: BrowserTypeChannel, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index b1926e77f2..e7b30818b4 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -325,6 +325,16 @@ ContextOptions: path: string strictSelectors: boolean? +Root: + type: interface + + commands: + + initialize: + parameters: + language: string + returns: + playwright: Playwright Playwright: type: interface diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index a42463e709..517031a073 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -149,6 +149,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme { value: tString, })), }); + scheme.RootInitializeParams = tObject({ + language: tString, + }); scheme.PlaywrightSetForwardedPortsParams = tObject({ ports: tArray(tNumber), }); diff --git a/src/remote/playwrightClient.ts b/src/remote/playwrightClient.ts index 535c46e16e..3f18bce230 100644 --- a/src/remote/playwrightClient.ts +++ b/src/remote/playwrightClient.ts @@ -37,11 +37,13 @@ export class PlaywrightClient { ws.on('message', message => connection.dispatch(JSON.parse(message.toString()))); const errorPromise = new Promise((_, reject) => ws.on('error', error => reject(error))); const closePromise = new Promise((_, reject) => ws.on('close', () => reject(new Error('Connection closed')))); - const playwrightClientPromise = new Promise(async (resolve, reject) => { - const playwright = await connection.waitForObjectWithKnownName('Playwright') as Playwright; - if (forwardPorts) - await playwright._enablePortForwarding(forwardPorts).catch(reject); - resolve(new PlaywrightClient(playwright, ws)); + const playwrightClientPromise = new Promise((resolve, reject) => { + ws.on('open', async () => { + const playwright = await connection.initializePlaywright(); + if (forwardPorts) + await playwright._enablePortForwarding(forwardPorts).catch(reject); + resolve(new PlaywrightClient(playwright, ws)); + }); }); let timer: NodeJS.Timeout; try { diff --git a/src/remote/playwrightServer.ts b/src/remote/playwrightServer.ts index 386634bc43..0e254d8ce1 100644 --- a/src/remote/playwrightServer.ts +++ b/src/remote/playwrightServer.ts @@ -17,9 +17,9 @@ import debug from 'debug'; import * as http from 'http'; import * as ws from 'ws'; -import { DispatcherConnection, DispatcherScope } from '../dispatchers/dispatcher'; +import { DispatcherConnection, Root } from '../dispatchers/dispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; -import { createPlaywright } from '../server/playwright'; +import { createPlaywright, Playwright } from '../server/playwright'; import { gracefullyCloseAll } from '../utils/processLauncher'; const debugLog = debug('pw:server'); @@ -27,7 +27,7 @@ const debugLog = debug('pw:server'); export interface PlaywrightServerDelegate { path: string; allowMultipleClients: boolean; - onConnect(rootScope: DispatcherScope, forceDisconnect: () => void): Promise<() => any>; + onConnect(connection: DispatcherConnection, forceDisconnect: () => void): Promise<() => any>; onClose: () => any; } @@ -49,15 +49,18 @@ export class PlaywrightServer { path: '/ws', allowMultipleClients: false, onClose: cleanup, - onConnect: async (rootScope: DispatcherScope) => { - const playwright = createPlaywright(); - if (acceptForwardedPorts) - await playwright._enablePortForwarding(); - new PlaywrightDispatcher(rootScope, playwright); + onConnect: async (connection: DispatcherConnection) => { + let playwright: Playwright | undefined; + new Root(connection, async (rootScope): Promise => { + playwright = createPlaywright(); + if (acceptForwardedPorts) + await playwright._enablePortForwarding(); + return new PlaywrightDispatcher(rootScope, playwright); + }); return () => { cleanup(); - playwright._disablePortForwarding(); - playwright.selectors.unregisterAll(); + playwright?._disablePortForwarding(); + playwright?.selectors.unregisterAll(); onDisconnect?.(); }; }, @@ -105,7 +108,6 @@ export class PlaywrightServer { }); const forceDisconnect = () => socket.close(); - const scope = connection.rootDispatcher(); let onDisconnect = () => {}; const disconnected = () => { this._clientsCount--; @@ -121,7 +123,7 @@ export class PlaywrightServer { debugLog('Client error ' + error); disconnected(); }); - onDisconnect = await this._delegate.onConnect(scope, forceDisconnect); + onDisconnect = await this._delegate.onConnect(connection, forceDisconnect); }); return wsEndpoint; diff --git a/tests/browsertype-connect.spec.ts b/tests/browsertype-connect.spec.ts index 1b290c293c..12caa547e4 100644 --- a/tests/browsertype-connect.spec.ts +++ b/tests/browsertype-connect.spec.ts @@ -57,6 +57,7 @@ test('should be able to connect two browsers at the same time', async ({browserT expect(browser1.contexts().length).toBe(1); await browser1.close(); + expect(browser2.contexts().length).toBe(1); const page2 = await browser2.newPage(); expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work