chore(protocol): do client hello instead of server hello (#8019)

This commit is contained in:
Max Schmitt 2021-08-19 17:31:14 +02:00 committed by GitHub
parent 166851e7d8
commit ddcdb6d413
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 106 additions and 54 deletions

View file

@ -17,7 +17,7 @@
import { LaunchServerOptions, Logger } from './client/types'; import { LaunchServerOptions, Logger } from './client/types';
import { Browser } from './server/browser'; import { Browser } from './server/browser';
import { EventEmitter } from 'ws'; import { EventEmitter } from 'ws';
import { Dispatcher, DispatcherScope } from './dispatchers/dispatcher'; import { Dispatcher, DispatcherConnection, DispatcherScope, Root } from './dispatchers/dispatcher';
import { BrowserContextDispatcher } from './dispatchers/browserContextDispatcher'; import { BrowserContextDispatcher } from './dispatchers/browserContextDispatcher';
import * as channels from './protocol/channels'; import * as channels from './protocol/channels';
import { BrowserServerLauncher, BrowserServer } from './client/browserType'; import { BrowserServerLauncher, BrowserServer } from './client/browserType';
@ -80,18 +80,21 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
return browserServer; return browserServer;
} }
private async _onConnect(playwright: Playwright, browser: Browser, scope: DispatcherScope, forceDisconnect: () => void) { private async _onConnect(playwright: Playwright, browser: Browser, connection: DispatcherConnection, forceDisconnect: () => void) {
const selectors = new Selectors(); let browserDispatcher: ConnectedBrowserDispatcher | undefined;
const selectorsDispatcher = new SelectorsDispatcher(scope, selectors); new Root(connection, async (scope: DispatcherScope): Promise<PlaywrightDispatcher> => {
const browserDispatcher = new ConnectedBrowserDispatcher(scope, browser, selectors); const selectors = new Selectors();
browser.on(Browser.Events.Disconnected, () => { const selectorsDispatcher = new SelectorsDispatcher(scope, selectors);
// Underlying browser did close for some reason - force disconnect the client. browserDispatcher = new ConnectedBrowserDispatcher(scope, browser, selectors);
forceDisconnect(); 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 () => { return () => {
// Cleanup contexts upon disconnect. // Cleanup contexts upon disconnect.
browserDispatcher.cleanupContexts().catch(e => {}); browserDispatcher?.cleanupContexts().catch(e => {});
}; };
} }
} }

View file

@ -20,7 +20,7 @@ import fs from 'fs';
import * as playwright from '../..'; import * as playwright from '../..';
import { BrowserType } from '../client/browserType'; import { BrowserType } from '../client/browserType';
import { LaunchServerOptions } from '../client/types'; import { LaunchServerOptions } from '../client/types';
import { DispatcherConnection } from '../dispatchers/dispatcher'; import { DispatcherConnection, Root } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { Transport } from '../protocol/transport'; import { Transport } from '../protocol/transport';
import { PlaywrightServer, PlaywrightServerOptions } from '../remote/playwrightServer'; import { PlaywrightServer, PlaywrightServerOptions } from '../remote/playwrightServer';
@ -34,6 +34,10 @@ export function printApiJson() {
export function runDriver() { export function runDriver() {
const dispatcherConnection = new DispatcherConnection(); 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); const transport = new Transport(process.stdout, process.stdin);
transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message)); transport.onmessage = message => dispatcherConnection.dispatch(JSON.parse(message));
dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message)); dispatcherConnection.onmessage = message => transport.send(JSON.stringify(message));
@ -46,9 +50,6 @@ export function runDriver() {
await gracefullyCloseAll(); await gracefullyCloseAll();
process.exit(0); process.exit(0);
}; };
const playwright = createPlaywright();
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright);
} }
export async function runServer(port: number | undefined, configFile?: string) { export async function runServer(port: number | undefined, configFile?: string) {

View file

@ -28,7 +28,6 @@ import { envObjectToArray } from './clientHelper';
import { assert, headersObjectToArray, makeWaitForNextTask, getUserAgent } from '../utils/utils'; import { assert, headersObjectToArray, makeWaitForNextTask, getUserAgent } from '../utils/utils';
import { kBrowserClosedError } from '../utils/errors'; import { kBrowserClosedError } from '../utils/errors';
import * as api from '../../types/types'; import * as api from '../../types/types';
import type { Playwright } from './playwright';
export interface BrowserServerLauncher { export interface BrowserServerLauncher {
launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>; launchServer(options?: LaunchServerOptions): Promise<api.BrowserServer>;
@ -182,7 +181,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
reject(new Error(`WebSocket server disconnected (${event.code}) ${event.reason}`)); reject(new Error(`WebSocket server disconnected (${event.code}) ${event.reason}`));
}; };
ws.addEventListener('close', prematureCloseListener); ws.addEventListener('close', prematureCloseListener);
const playwright = await connection.waitForObjectWithKnownName('Playwright') as Playwright; const playwright = await connection.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) { if (!playwright._initializer.preLaunchedBrowser) {
reject(new Error('Malformed endpoint. Did you use launchServer method?')); reject(new Error('Malformed endpoint. Did you use launchServer method?'));

View file

@ -40,9 +40,15 @@ import { ParsedStackTrace } from '../utils/stackTrace';
import { Artifact } from './artifact'; import { Artifact } from './artifact';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
class Root extends ChannelOwner<channels.Channel, {}> { class Root extends ChannelOwner<channels.RootChannel, {}> {
constructor(connection: Connection) { constructor(connection: Connection) {
super(connection, '', '', {}); super(connection, 'Root', '', {});
}
async initialize(): Promise<Playwright> {
return Playwright.from((await this._channel.initialize({
language: 'javascript',
})).playwright);
} }
} }
@ -52,7 +58,7 @@ export class Connection extends EventEmitter {
onmessage = (message: object): void => {}; onmessage = (message: object): void => {};
private _lastId = 0; private _lastId = 0;
private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void, metadata: channels.Metadata }>(); private _callbacks = new Map<number, { resolve: (a: any) => void, reject: (a: Error) => void, metadata: channels.Metadata }>();
private _rootObject: ChannelOwner; private _rootObject: Root;
private _disconnectedErrorMessage: string | undefined; private _disconnectedErrorMessage: string | undefined;
private _onClose?: () => void; private _onClose?: () => void;
@ -62,10 +68,8 @@ export class Connection extends EventEmitter {
this._onClose = onClose; this._onClose = onClose;
} }
async waitForObjectWithKnownName(guid: string): Promise<any> { async initializePlaywright(): Promise<Playwright> {
if (this._objects.has(guid)) return await this._rootObject.initialize();
return this._objects.get(guid)!;
return new Promise(f => this._waitingForObject.set(guid, f));
} }
pendingProtocolCalls(): channels.Metadata[] { pendingProtocolCalls(): channels.Metadata[] {

View file

@ -65,6 +65,10 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
this._channel.on('incomingSocksSocket', ({socket}) => SocksSocket.from(socket)); this._channel.on('incomingSocksSocket', ({socket}) => SocksSocket.from(socket));
} }
static from(channel: channels.PlaywrightChannel): Playwright {
return (channel as any)._object;
}
async _enablePortForwarding(ports: number[]) { async _enablePortForwarding(ports: number[]) {
this._forwardPorts = ports; this._forwardPorts = ports;
await this._channel.setForwardedPorts({ports}); await this._channel.setForwardedPorts({ports});

View file

@ -23,6 +23,7 @@ import { tOptional } from '../protocol/validatorPrimitives';
import { kBrowserOrContextClosedError } from '../utils/errors'; import { kBrowserOrContextClosedError } from '../utils/errors';
import { CallMetadata, SdkObject } from '../server/instrumentation'; import { CallMetadata, SdkObject } from '../server/instrumentation';
import { rewriteErrorMessage } from '../utils/stackTrace'; import { rewriteErrorMessage } from '../utils/stackTrace';
import type { PlaywrightDispatcher } from './playwrightDispatcher';
export const dispatcherSymbol = Symbol('dispatcher'); export const dispatcherSymbol = Symbol('dispatcher');
@ -121,15 +122,25 @@ export class Dispatcher<Type extends { guid: string }, Initializer> extends Even
} }
export type DispatcherScope = Dispatcher<any, any>; export type DispatcherScope = Dispatcher<any, any>;
class Root extends Dispatcher<{ guid: '' }, {}> { export class Root extends Dispatcher<{ guid: '' }, {}> {
constructor(connection: DispatcherConnection) { private _initialized = false;
super(connection, { guid: '' }, '', {}, true);
constructor(connection: DispatcherConnection, private readonly createPlaywright?: (scope: DispatcherScope) => Promise<PlaywrightDispatcher>) {
super(connection, { guid: '' }, 'Root', {}, true);
}
async initialize(params: { language?: string }): Promise<channels.RootInitializeResult> {
assert(this.createPlaywright);
assert(!this._initialized);
this._initialized = true;
return {
playwright: await this.createPlaywright(this),
};
} }
} }
export class DispatcherConnection { export class DispatcherConnection {
readonly _dispatchers = new Map<string, Dispatcher<any, any>>(); readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
private _rootDispatcher: Root;
onmessage = (message: object) => {}; onmessage = (message: object) => {};
private _validateParams: (type: string, method: string, params: any) => any; private _validateParams: (type: string, method: string, params: any) => any;
private _validateMetadata: (metadata: any) => { stack?: channels.StackFrame[] }; private _validateMetadata: (metadata: any) => { stack?: channels.StackFrame[] };
@ -157,8 +168,6 @@ export class DispatcherConnection {
} }
constructor() { constructor() {
this._rootDispatcher = new Root(this);
const tChannel = (name: string): Validator => { const tChannel = (name: string): Validator => {
return (arg: any, path: string) => { return (arg: any, path: string) => {
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') { if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
@ -185,10 +194,6 @@ export class DispatcherConnection {
}; };
} }
rootDispatcher(): Dispatcher<any, any> {
return this._rootDispatcher;
}
async dispatch(message: object) { async dispatch(message: object) {
const { id, guid, method, params, metadata } = message as any; const { id, guid, method, params, metadata } = message as any;
const dispatcher = this._dispatchers.get(guid); const dispatcher = this._dispatchers.get(guid);
@ -197,7 +202,8 @@ export class DispatcherConnection {
return; return;
} }
if (method === 'debugScopeState') { if (method === 'debugScopeState') {
this.onmessage({ id, result: this._rootDispatcher._debugScopeState() }); const rootDispatcher = this._dispatchers.get('')!;
this.onmessage({ id, result: rootDispatcher._debugScopeState() });
return; return;
} }

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { DispatcherConnection } from './dispatchers/dispatcher'; import { DispatcherConnection, Root } from './dispatchers/dispatcher';
import { createPlaywright } from './server/playwright'; import { createPlaywright } from './server/playwright';
import type { Playwright as PlaywrightAPI } from './client/playwright'; import type { Playwright as PlaywrightAPI } from './client/playwright';
import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
@ -31,8 +31,10 @@ function setupInProcess(): PlaywrightAPI {
dispatcherConnection.onmessage = message => clientConnection.dispatch(message); dispatcherConnection.onmessage = message => clientConnection.dispatch(message);
clientConnection.onmessage = message => dispatcherConnection.dispatch(message); clientConnection.onmessage = message => dispatcherConnection.dispatch(message);
const rootScope = new Root(dispatcherConnection);
// Initialize Playwright channel. // Initialize Playwright channel.
new PlaywrightDispatcher(dispatcherConnection.rootDispatcher(), playwright); new PlaywrightDispatcher(rootScope, playwright);
const playwrightAPI = clientConnection.getObjectWithKnownName('Playwright') as PlaywrightAPI; const playwrightAPI = clientConnection.getObjectWithKnownName('Playwright') as PlaywrightAPI;
playwrightAPI.chromium._serverLauncher = new BrowserServerLauncherImpl('chromium'); playwrightAPI.chromium._serverLauncher = new BrowserServerLauncherImpl('chromium');
playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox'); playwrightAPI.firefox._serverLauncher = new BrowserServerLauncherImpl('firefox');

View file

@ -52,7 +52,7 @@ class PlaywrightClient {
transport.onmessage = message => connection.dispatch(JSON.parse(message)); transport.onmessage = message => connection.dispatch(JSON.parse(message));
this._closePromise = new Promise(f => transport.onclose = f); this._closePromise = new Promise(f => transport.onclose = f);
this._playwright = connection.waitForObjectWithKnownName('Playwright'); this._playwright = connection.initializePlaywright();
} }
async stop() { async stop() {

View file

@ -152,6 +152,21 @@ export type InterceptedResponse = {
}[], }[],
}; };
// ----------- Root -----------
export type RootInitializer = {};
export interface RootChannel extends Channel {
initialize(params: RootInitializeParams, metadata?: Metadata): Promise<RootInitializeResult>;
}
export type RootInitializeParams = {
language: string,
};
export type RootInitializeOptions = {
};
export type RootInitializeResult = {
playwright: PlaywrightChannel,
};
// ----------- Playwright ----------- // ----------- Playwright -----------
export type PlaywrightInitializer = { export type PlaywrightInitializer = {
chromium: BrowserTypeChannel, chromium: BrowserTypeChannel,

View file

@ -325,6 +325,16 @@ ContextOptions:
path: string path: string
strictSelectors: boolean? strictSelectors: boolean?
Root:
type: interface
commands:
initialize:
parameters:
language: string
returns:
playwright: Playwright
Playwright: Playwright:
type: interface type: interface

View file

@ -149,6 +149,9 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
value: tString, value: tString,
})), })),
}); });
scheme.RootInitializeParams = tObject({
language: tString,
});
scheme.PlaywrightSetForwardedPortsParams = tObject({ scheme.PlaywrightSetForwardedPortsParams = tObject({
ports: tArray(tNumber), ports: tArray(tNumber),
}); });

View file

@ -37,11 +37,13 @@ export class PlaywrightClient {
ws.on('message', message => connection.dispatch(JSON.parse(message.toString()))); ws.on('message', message => connection.dispatch(JSON.parse(message.toString())));
const errorPromise = new Promise((_, reject) => ws.on('error', error => reject(error))); 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 closePromise = new Promise((_, reject) => ws.on('close', () => reject(new Error('Connection closed'))));
const playwrightClientPromise = new Promise<PlaywrightClient>(async (resolve, reject) => { const playwrightClientPromise = new Promise<PlaywrightClient>((resolve, reject) => {
const playwright = await connection.waitForObjectWithKnownName('Playwright') as Playwright; ws.on('open', async () => {
if (forwardPorts) const playwright = await connection.initializePlaywright();
await playwright._enablePortForwarding(forwardPorts).catch(reject); if (forwardPorts)
resolve(new PlaywrightClient(playwright, ws)); await playwright._enablePortForwarding(forwardPorts).catch(reject);
resolve(new PlaywrightClient(playwright, ws));
});
}); });
let timer: NodeJS.Timeout; let timer: NodeJS.Timeout;
try { try {

View file

@ -17,9 +17,9 @@
import debug from 'debug'; import debug from 'debug';
import * as http from 'http'; import * as http from 'http';
import * as ws from 'ws'; import * as ws from 'ws';
import { DispatcherConnection, DispatcherScope } from '../dispatchers/dispatcher'; import { DispatcherConnection, Root } from '../dispatchers/dispatcher';
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher'; import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
import { createPlaywright } from '../server/playwright'; import { createPlaywright, Playwright } from '../server/playwright';
import { gracefullyCloseAll } from '../utils/processLauncher'; import { gracefullyCloseAll } from '../utils/processLauncher';
const debugLog = debug('pw:server'); const debugLog = debug('pw:server');
@ -27,7 +27,7 @@ const debugLog = debug('pw:server');
export interface PlaywrightServerDelegate { export interface PlaywrightServerDelegate {
path: string; path: string;
allowMultipleClients: boolean; allowMultipleClients: boolean;
onConnect(rootScope: DispatcherScope, forceDisconnect: () => void): Promise<() => any>; onConnect(connection: DispatcherConnection, forceDisconnect: () => void): Promise<() => any>;
onClose: () => any; onClose: () => any;
} }
@ -49,15 +49,18 @@ export class PlaywrightServer {
path: '/ws', path: '/ws',
allowMultipleClients: false, allowMultipleClients: false,
onClose: cleanup, onClose: cleanup,
onConnect: async (rootScope: DispatcherScope) => { onConnect: async (connection: DispatcherConnection) => {
const playwright = createPlaywright(); let playwright: Playwright | undefined;
if (acceptForwardedPorts) new Root(connection, async (rootScope): Promise<PlaywrightDispatcher> => {
await playwright._enablePortForwarding(); playwright = createPlaywright();
new PlaywrightDispatcher(rootScope, playwright); if (acceptForwardedPorts)
await playwright._enablePortForwarding();
return new PlaywrightDispatcher(rootScope, playwright);
});
return () => { return () => {
cleanup(); cleanup();
playwright._disablePortForwarding(); playwright?._disablePortForwarding();
playwright.selectors.unregisterAll(); playwright?.selectors.unregisterAll();
onDisconnect?.(); onDisconnect?.();
}; };
}, },
@ -105,7 +108,6 @@ export class PlaywrightServer {
}); });
const forceDisconnect = () => socket.close(); const forceDisconnect = () => socket.close();
const scope = connection.rootDispatcher();
let onDisconnect = () => {}; let onDisconnect = () => {};
const disconnected = () => { const disconnected = () => {
this._clientsCount--; this._clientsCount--;
@ -121,7 +123,7 @@ export class PlaywrightServer {
debugLog('Client error ' + error); debugLog('Client error ' + error);
disconnected(); disconnected();
}); });
onDisconnect = await this._delegate.onConnect(scope, forceDisconnect); onDisconnect = await this._delegate.onConnect(connection, forceDisconnect);
}); });
return wsEndpoint; return wsEndpoint;

View file

@ -57,6 +57,7 @@ test('should be able to connect two browsers at the same time', async ({browserT
expect(browser1.contexts().length).toBe(1); expect(browser1.contexts().length).toBe(1);
await browser1.close(); await browser1.close();
expect(browser2.contexts().length).toBe(1);
const page2 = await browser2.newPage(); const page2 = await browser2.newPage();
expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work expect(await page2.evaluate(() => 7 * 6)).toBe(42); // original browser should still work