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 { 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<PlaywrightDispatcher> => {
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 => {});
};
}
}

View file

@ -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) {

View file

@ -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<api.BrowserServer>;
@ -182,7 +181,7 @@ export class BrowserType extends ChannelOwner<channels.BrowserTypeChannel, chann
reject(new Error(`WebSocket server disconnected (${event.code}) ${event.reason}`));
};
ws.addEventListener('close', prematureCloseListener);
const playwright = await connection.waitForObjectWithKnownName('Playwright') as Playwright;
const playwright = await connection.initializePlaywright();
if (!playwright._initializer.preLaunchedBrowser) {
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 { EventEmitter } from 'events';
class Root extends ChannelOwner<channels.Channel, {}> {
class Root extends ChannelOwner<channels.RootChannel, {}> {
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 => {};
private _lastId = 0;
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 _onClose?: () => void;
@ -62,10 +68,8 @@ export class Connection extends EventEmitter {
this._onClose = onClose;
}
async waitForObjectWithKnownName(guid: string): Promise<any> {
if (this._objects.has(guid))
return this._objects.get(guid)!;
return new Promise(f => this._waitingForObject.set(guid, f));
async initializePlaywright(): Promise<Playwright> {
return await this._rootObject.initialize();
}
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));
}
static from(channel: channels.PlaywrightChannel): Playwright {
return (channel as any)._object;
}
async _enablePortForwarding(ports: number[]) {
this._forwardPorts = ports;
await this._channel.setForwardedPorts({ports});

View file

@ -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<Type extends { guid: string }, Initializer> extends Even
}
export type DispatcherScope = Dispatcher<any, any>;
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<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 {
readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
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<any, any> {
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;
}

View file

@ -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');

View file

@ -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() {

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 -----------
export type PlaywrightInitializer = {
chromium: BrowserTypeChannel,

View file

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

View file

@ -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),
});

View file

@ -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<PlaywrightClient>(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<PlaywrightClient>((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 {

View file

@ -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<PlaywrightDispatcher> => {
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;

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);
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