chore(protocol): do client hello instead of server hello (#8019)
This commit is contained in:
parent
166851e7d8
commit
ddcdb6d413
|
|
@ -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 => {});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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?'));
|
||||
|
|
|
|||
|
|
@ -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[] {
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -325,6 +325,16 @@ ContextOptions:
|
|||
path: string
|
||||
strictSelectors: boolean?
|
||||
|
||||
Root:
|
||||
type: interface
|
||||
|
||||
commands:
|
||||
|
||||
initialize:
|
||||
parameters:
|
||||
language: string
|
||||
returns:
|
||||
playwright: Playwright
|
||||
|
||||
Playwright:
|
||||
type: interface
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue