chore(rpc): move classes around, fix tests, respect dispatcher scopes (#2784)

This commit is contained in:
Pavel Feldman 2020-06-30 22:21:17 -07:00 committed by GitHub
parent 87516cb3a3
commit 95538e73e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 130 additions and 81 deletions

View File

@ -55,7 +55,6 @@ export interface BrowserChannel extends Channel {
close(): Promise<void>;
newContext(params: { options?: types.BrowserContextOptions }): Promise<BrowserContextChannel>;
newPage(params: { options?: types.BrowserContextOptions }): Promise<PageChannel>;
}
export type BrowserInitializer = {};

View File

@ -16,7 +16,7 @@
import * as childProcess from 'child_process';
import * as path from 'path';
import { Connection } from './connection';
import { Connection } from './client/connection';
import { Transport } from './transport';
(async () => {
@ -24,7 +24,7 @@ import { Transport } from './transport';
const transport = new Transport(spawnedProcess.stdin, spawnedProcess.stdout);
const connection = new Connection();
connection.onmessage = message => transport.send(message);
transport.onmessage = message => connection.send(message);
transport.onmessage = message => connection.dispatch(message);
const chromium = await connection.waitForObjectWithKnownName('chromium');
const browser = await chromium.launch({ headless: false });

View File

@ -19,12 +19,13 @@ import { BrowserChannel, BrowserInitializer } from '../channels';
import { BrowserContext } from './browserContext';
import { Page } from './page';
import { ChannelOwner } from './channelOwner';
import { Connection } from '../connection';
import { Connection } from './connection';
import { Events } from '../../events';
export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
readonly _contexts = new Set<BrowserContext>();
private _isConnected = true;
private _isClosedOrClosing = false;
static from(browser: BrowserChannel): Browser {
@ -40,6 +41,7 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
channel.on('close', () => {
this._isConnected = false;
this.emit(Events.Browser.Disconnected);
this._isClosedOrClosing = true;
});
}
@ -69,6 +71,9 @@ export class Browser extends ChannelOwner<BrowserChannel, BrowserInitializer> {
}
async close(): Promise<void> {
if (this._isClosedOrClosing)
return;
this._isClosedOrClosing = true;
await this._channel.close();
}
}

View File

@ -23,7 +23,7 @@ import { BrowserContextChannel, BrowserContextInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { helper } from '../../helper';
import { Browser } from './browser';
import { Connection } from '../connection';
import { Connection } from './connection';
import { Events } from '../../events';
import { TimeoutSettings } from '../../timeoutSettings';

View File

@ -16,7 +16,7 @@
import { ChildProcess } from 'child_process';
import { BrowserServerChannel, BrowserServerInitializer } from '../channels';
import { Connection } from '../connection';
import { Connection } from './connection';
import { ChannelOwner } from './channelOwner';
import { Events } from '../../events';

View File

@ -19,7 +19,7 @@ import { BrowserTypeChannel, BrowserTypeInitializer } from '../channels';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { ChannelOwner } from './channelOwner';
import { Connection } from '../connection';
import { Connection } from './connection';
import { BrowserServer } from './browserServer';
export class BrowserType extends ChannelOwner<BrowserTypeChannel, BrowserTypeInitializer> {

View File

@ -16,7 +16,7 @@
import { EventEmitter } from 'events';
import { Channel } from '../channels';
import { Connection } from '../connection';
import { Connection } from './connection';
export abstract class ChannelOwner<T extends Channel, Initializer> extends EventEmitter {
readonly _channel: T;

View File

@ -15,23 +15,23 @@
*/
import { EventEmitter } from 'ws';
import { Browser } from './client/browser';
import { BrowserContext } from './client/browserContext';
import { BrowserType } from './client/browserType';
import { ChannelOwner } from './client/channelOwner';
import { ElementHandle } from './client/elementHandle';
import { Frame } from './client/frame';
import { JSHandle } from './client/jsHandle';
import { Request, Response, Route } from './client/network';
import { Page, BindingCall } from './client/page';
import { Worker } from './client/worker';
import { Browser } from './browser';
import { BrowserContext } from './browserContext';
import { BrowserType } from './browserType';
import { ChannelOwner } from './channelOwner';
import { ElementHandle } from './elementHandle';
import { Frame } from './frame';
import { JSHandle } from './jsHandle';
import { Request, Response, Route } from './network';
import { Page, BindingCall } from './page';
import { Worker } from './worker';
import debug = require('debug');
import { Channel } from './channels';
import { ConsoleMessage } from './client/consoleMessage';
import { Dialog } from './client/dialog';
import { Download } from './client/download';
import { parseError } from './serializers';
import { BrowserServer } from './client/browserServer';
import { Channel } from '../channels';
import { ConsoleMessage } from './consoleMessage';
import { Dialog } from './dialog';
import { Download } from './download';
import { parseError } from '../serializers';
import { BrowserServer } from './browserServer';
export class Connection {
private _channels = new Map<string, Channel>();
@ -122,7 +122,7 @@ export class Connection {
return new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject }));
}
send(message: string) {
dispatch(message: string) {
const parsedMessage = JSON.parse(message);
const { id, guid, method, params, result, error } = parsedMessage;
if (id) {

View File

@ -19,7 +19,7 @@ import { ConsoleMessageLocation } from '../../types';
import { JSHandle } from './jsHandle';
import { ConsoleMessageChannel, ConsoleMessageInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { Connection } from '../connection';
import { Connection } from './connection';
export class ConsoleMessage extends ChannelOwner<ConsoleMessageChannel, ConsoleMessageInitializer> {
static from(request: ConsoleMessageChannel): ConsoleMessage {

View File

@ -15,7 +15,7 @@
*/
import { DialogChannel, DialogInitializer } from '../channels';
import { Connection } from '../connection';
import { Connection } from './connection';
import { ChannelOwner } from './channelOwner';
export class Dialog extends ChannelOwner<DialogChannel, DialogInitializer> {

View File

@ -16,7 +16,7 @@
import * as fs from 'fs';
import { DownloadChannel, DownloadInitializer } from '../channels';
import { Connection } from '../connection';
import { Connection } from './connection';
import { ChannelOwner } from './channelOwner';
import { Readable } from 'stream';

View File

@ -18,7 +18,7 @@ import * as types from '../../types';
import { ElementHandleChannel, JSHandleInitializer } from '../channels';
import { Frame } from './frame';
import { FuncOn, JSHandle, serializeArgument, parseResult } from './jsHandle';
import { Connection } from '../connection';
import { Connection } from './connection';
export class ElementHandle<T extends Node = Node> extends JSHandle<T> {
readonly _elementChannel: ElementHandleChannel;

View File

@ -25,7 +25,7 @@ import { JSHandle, Func1, FuncOn, SmartHandle, serializeArgument, parseResult }
import * as network from './network';
import { Response } from './network';
import { Page } from './page';
import { Connection } from '../connection';
import { Connection } from './connection';
import { normalizeFilePayloads } from '../serializers';
export type GotoOptions = types.NavigateOptions & {

View File

@ -17,7 +17,7 @@
import { JSHandleChannel, JSHandleInitializer } from '../channels';
import { ElementHandle } from './elementHandle';
import { ChannelOwner } from './channelOwner';
import { Connection } from '../connection';
import { Connection } from './connection';
import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers';
type NoHandles<Arg> = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles<Arg[Key]> } : Arg);

View File

@ -19,7 +19,7 @@ import * as types from '../../types';
import { RequestChannel, ResponseChannel, RouteChannel, RequestInitializer, ResponseInitializer, RouteInitializer } from '../channels';
import { ChannelOwner } from './channelOwner';
import { Frame } from './frame';
import { Connection } from '../connection';
import { Connection } from './connection';
import { normalizeFulfillParameters } from '../serializers';
export type NetworkCookie = {

View File

@ -22,7 +22,7 @@ import { assert, assertMaxArguments, helper, Listener } from '../../helper';
import { TimeoutSettings } from '../../timeoutSettings';
import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, Channel, PageChannel, PageInitializer } from '../channels';
import { Connection } from '../connection';
import { Connection } from './connection';
import { parseError, serializeError } from '../serializers';
import { Accessibility } from './accessibility';
import { BrowserContext } from './browserContext';

View File

@ -17,7 +17,7 @@
import { Events } from '../../events';
import { assertMaxArguments } from '../../helper';
import { WorkerChannel, WorkerInitializer } from '../channels';
import { Connection } from '../connection';
import { Connection } from './connection';
import { ChannelOwner } from './channelOwner';
import { Func1, JSHandle, parseResult, serializeArgument, SmartHandle } from './jsHandle';
import { Page } from './page';

View File

@ -15,15 +15,16 @@
*/
import { Transport } from './transport';
import { DispatcherScope } from './dispatcher';
import { DispatcherConnection } from './server/dispatcher';
import { Playwright } from '../server/playwright';
import { BrowserTypeDispatcher } from './server/browserTypeDispatcher';
const dispatcherScope = new DispatcherScope();
const dispatcherConnection = new DispatcherConnection();
const transport = new Transport(process.stdout, process.stdin);
transport.onmessage = message => dispatcherScope.send(message);
dispatcherScope.onmessage = message => transport.send(message);
transport.onmessage = message => dispatcherConnection.dispatch(message);
dispatcherConnection.onmessage = message => transport.send(message);
const dispatcherScope = dispatcherConnection.createScope();
const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']);
new BrowserTypeDispatcher(dispatcherScope, playwright.chromium!);
new BrowserTypeDispatcher(dispatcherScope, playwright.firefox!);

View File

@ -17,7 +17,7 @@
import * as types from '../../types';
import { BrowserContextBase, BrowserContext } from '../../browserContext';
import { Events } from '../../events';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, lookupDispatcher } from '../dispatcher';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, lookupDispatcher } from './dispatcher';
import { PageDispatcher, BindingCallDispatcher } from './pageDispatcher';
import { PageChannel, BrowserContextChannel, BrowserContextInitializer } from '../channels';
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
@ -34,6 +34,7 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, Browser
context.on(Events.BrowserContext.Page, page => this._dispatchEvent('page', PageDispatcher.from(this._scope, page)));
context.on(Events.BrowserContext.Close, () => {
this._dispatchEvent('close');
scope.dispose();
});
}

View File

@ -14,27 +14,25 @@
* limitations under the License.
*/
import { BrowserBase, Browser } from '../../browser';
import { Browser, BrowserBase } from '../../browser';
import { BrowserContextBase } from '../../browserContext';
import * as types from '../../types';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { BrowserChannel, BrowserContextChannel, PageChannel, BrowserInitializer } from '../channels';
import { Dispatcher, DispatcherScope, lookupDispatcher } from '../dispatcher';
import { PageDispatcher } from './pageDispatcher';
import { Events } from '../../events';
import * as types from '../../types';
import { BrowserChannel, BrowserContextChannel, BrowserInitializer } from '../channels';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
export class BrowserDispatcher extends Dispatcher<Browser, BrowserInitializer> implements BrowserChannel {
constructor(scope: DispatcherScope, browser: BrowserBase) {
super(scope, browser, 'browser', {});
browser.on(Events.Browser.Disconnected, () => this._dispatchEvent('close'));
browser.on(Events.Browser.Disconnected, () => {
this._dispatchEvent('close');
scope.dispose();
});
}
async newContext(params: { options?: types.BrowserContextOptions }): Promise<BrowserContextChannel> {
return new BrowserContextDispatcher(this._scope, await this._object.newContext(params.options) as BrowserContextBase);
}
async newPage(params: { options?: types.BrowserContextOptions }): Promise<PageChannel> {
return lookupDispatcher<PageDispatcher>(await this._object.newPage(params.options))!;
return new BrowserContextDispatcher(this._scope.createChild(), await this._object.newContext(params.options) as BrowserContextBase);
}
async close(): Promise<void> {

View File

@ -16,7 +16,7 @@
import { BrowserServer } from '../../server/browserServer';
import { BrowserServerChannel, BrowserServerInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { Events } from '../../events';
export class BrowserServerDispatcher extends Dispatcher<BrowserServer, BrowserServerInitializer> implements BrowserServerChannel {

View File

@ -19,7 +19,7 @@ import { BrowserTypeBase, BrowserType } from '../../server/browserType';
import * as types from '../../types';
import { BrowserDispatcher } from './browserDispatcher';
import { BrowserChannel, BrowserTypeChannel, BrowserContextChannel, BrowserTypeInitializer, BrowserServerChannel } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { BrowserContextBase } from '../../browserContext';
import { BrowserContextDispatcher } from './browserContextDispatcher';
import { BrowserServerDispatcher } from './browserServerDispatcher';
@ -34,12 +34,12 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
async launch(params: { options?: types.LaunchOptions }): Promise<BrowserChannel> {
const browser = await this._object.launch(params.options || undefined);
return new BrowserDispatcher(this._scope, browser as BrowserBase);
return new BrowserDispatcher(this._scope.createChild(), browser as BrowserBase);
}
async launchPersistentContext(params: { userDataDir: string, options?: types.LaunchOptions & types.BrowserContextOptions }): Promise<BrowserContextChannel> {
const browserContext = await this._object.launchPersistentContext(params.userDataDir, params.options);
return new BrowserContextDispatcher(this._scope, browserContext as BrowserContextBase);
return new BrowserContextDispatcher(this._scope.createChild(), browserContext as BrowserContextBase);
}
async launchServer(params: { options?: types.LaunchServerOptions }): Promise<BrowserServerChannel> {
@ -48,6 +48,6 @@ export class BrowserTypeDispatcher extends Dispatcher<BrowserType, BrowserTypeIn
async connect(params: { options: types.ConnectOptions }): Promise<BrowserChannel> {
const browser = await this._object.connect(params.options);
return new BrowserDispatcher(this._scope, browser as BrowserBase);
return new BrowserDispatcher(this._scope.createChild(), browser as BrowserBase);
}
}

View File

@ -16,7 +16,7 @@
import { ConsoleMessage } from '../../console';
import { ConsoleMessageChannel, ConsoleMessageInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { createHandle } from './elementHandlerDispatcher';
export class ConsoleMessageDispatcher extends Dispatcher<ConsoleMessage, ConsoleMessageInitializer> implements ConsoleMessageChannel {

View File

@ -16,7 +16,7 @@
import { Dialog } from '../../dialog';
import { DialogChannel, DialogInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
export class DialogDispatcher extends Dispatcher<Dialog, DialogInitializer> implements DialogChannel {
constructor(scope: DispatcherScope, dialog: Dialog) {

View File

@ -15,9 +15,9 @@
*/
import { EventEmitter } from 'events';
import { helper, debugAssert } from '../helper';
import { Channel } from './channels';
import { serializeError } from './serializers';
import { helper, debugAssert } from '../../helper';
import { Channel } from '../channels';
import { serializeError } from '../serializers';
export const dispatcherSymbol = Symbol('dispatcher');
@ -47,7 +47,7 @@ export class Dispatcher<Type, Initializer> extends EventEmitter implements Chann
this._guid = guid;
this._object = object;
this._scope = scope;
scope.dispatchers.set(this._guid, this);
scope.bind(this._guid, this);
(object as any)[dispatcherSymbol] = this;
this._scope.sendMessageToClient(this._guid, '__create__', { type, initializer });
}
@ -58,17 +58,62 @@ export class Dispatcher<Type, Initializer> extends EventEmitter implements Chann
}
export class DispatcherScope {
readonly dispatchers = new Map<string, Dispatcher<any, any>>();
onmessage = (message: string) => {};
private _connection: DispatcherConnection;
private _dispatchers = new Map<string, Dispatcher<any, any>>();
private _parent: DispatcherScope | undefined;
private _childScopes = new Set<DispatcherScope>();
constructor(connection: DispatcherConnection, parent?: DispatcherScope) {
this._connection = connection;
this._parent = parent;
if (parent)
parent._childScopes.add(this);
}
createChild(): DispatcherScope {
return new DispatcherScope(this._connection, this);
}
bind(guid: string, arg: Dispatcher<any, any>) {
this._dispatchers.set(guid, arg);
this._connection._dispatchers.set(guid, arg);
}
dispose() {
for (const child of [...this._childScopes])
child.dispose();
this._childScopes.clear();
for (const guid of this._dispatchers.keys())
this._connection._dispatchers.delete(guid);
if (this._parent)
this._parent._childScopes.delete(this);
}
async sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
this._connection._sendMessageToClient(guid, method, params);
}
}
export class DispatcherConnection {
readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
onmessage = (message: string) => {};
async _sendMessageToClient(guid: string, method: string, params: any): Promise<any> {
this.onmessage(JSON.stringify({ guid, method, params: this._replaceDispatchersWithGuids(params) }));
}
async send(message: string) {
createScope(): DispatcherScope {
return new DispatcherScope(this);
}
async dispatch(message: string) {
const parsedMessage = JSON.parse(message);
const { id, guid, method, params } = parsedMessage;
const dispatcher = this.dispatchers.get(guid)!;
const dispatcher = this._dispatchers.get(guid);
if (!dispatcher) {
this.onmessage(JSON.stringify({ id, error: serializeError(new Error('Target browser or context has been closed')) }));
return;
}
try {
const result = await (dispatcher as any)[method](this._replaceGuidsWithDispatchers(params));
this.onmessage(JSON.stringify({ id, result: this._replaceDispatchersWithGuids(result) }));
@ -101,8 +146,8 @@ export class DispatcherScope {
return payload;
if (Array.isArray(payload))
return payload.map(p => this._replaceGuidsWithDispatchers(p));
if (payload.guid && this.dispatchers.has(payload.guid))
return this.dispatchers.get(payload.guid);
if (payload.guid && this._dispatchers.has(payload.guid))
return this._dispatchers.get(payload.guid);
// TODO: send base64
if (payload instanceof Buffer)
return payload;

View File

@ -16,7 +16,7 @@
import { Download } from '../../download';
import { DownloadChannel, DownloadInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
export class DownloadDispatcher extends Dispatcher<Download, DownloadInitializer> implements DownloadChannel {
constructor(scope: DispatcherScope, download: Download) {

View File

@ -18,7 +18,7 @@ import { ElementHandle } from '../../dom';
import * as js from '../../javascript';
import * as types from '../../types';
import { ElementHandleChannel, FrameChannel } from '../channels';
import { DispatcherScope, lookupNullableDispatcher } from '../dispatcher';
import { DispatcherScope, lookupNullableDispatcher } from './dispatcher';
import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher';
import { FrameDispatcher } from './frameDispatcher';

View File

@ -17,7 +17,7 @@
import { Frame } from '../../frames';
import * as types from '../../types';
import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel } from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from '../dispatcher';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { convertSelectOptionValues, ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher';
import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { ResponseDispatcher } from './networkDispatchers';

View File

@ -16,7 +16,7 @@
import * as js from '../../javascript';
import { JSHandleChannel, JSHandleInitializer } from '../channels';
import { Dispatcher, DispatcherScope } from '../dispatcher';
import { Dispatcher, DispatcherScope } from './dispatcher';
import { parseEvaluationResultValue, serializeAsCallArgument } from '../../common/utilityScriptSerializers';
import { createHandle } from './elementHandlerDispatcher';

View File

@ -17,7 +17,7 @@
import { Request, Response, Route } from '../../network';
import * as types from '../../types';
import { RequestChannel, ResponseChannel, RouteChannel, ResponseInitializer, RequestInitializer, RouteInitializer, Binary } from '../channels';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from '../dispatcher';
import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { FrameDispatcher } from './frameDispatcher';
export class RequestDispatcher extends Dispatcher<Request, RequestInitializer> implements RequestChannel {

View File

@ -21,7 +21,7 @@ import { Request } from '../../network';
import { Page, Worker } from '../../page';
import * as types from '../../types';
import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary } from '../channels';
import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher, existingDispatcher } from '../dispatcher';
import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher, existingDispatcher } from './dispatcher';
import { parseError, serializeError } from '../serializers';
import { ConsoleMessageDispatcher } from './consoleMessageDispatcher';
import { DialogDispatcher } from './dialogDispatcher';

View File

@ -20,8 +20,8 @@ const fs = require('fs');
const path = require('path');
const rm = require('rimraf').sync;
const {TestServer} = require('../utils/testserver/');
const { DispatcherScope } = require('../lib/rpc/dispatcher');
const { Connection } = require('../lib/rpc/connection');
const { DispatcherConnection } = require('../lib/rpc/server/dispatcher');
const { Connection } = require('../lib/rpc/client/connection');
const { BrowserTypeDispatcher } = require('../lib/rpc/server/browserTypeDispatcher');
class ServerEnvironment {
@ -172,17 +172,17 @@ class BrowserTypeEnvironment {
// Channel substitute
let overridenBrowserType = this._browserType;
if (process.env.PWCHANNEL) {
const dispatcherScope = new DispatcherScope();
const dispatcherConnection = new DispatcherConnection();
const connection = new Connection();
dispatcherScope.onmessage = async message => {
setImmediate(() => connection.send(message));
dispatcherConnection.onmessage = async message => {
setImmediate(() => connection.dispatch(message));
};
connection.onmessage = async message => {
const result = await dispatcherScope.send(message);
const result = await dispatcherConnection.dispatch(message);
await new Promise(f => setImmediate(f));
return result;
};
new BrowserTypeDispatcher(dispatcherScope, this._browserType);
new BrowserTypeDispatcher(dispatcherConnection.createScope(), this._browserType);
overridenBrowserType = await connection.waitForObjectWithKnownName(this._browserType.name());
}
state.browserType = overridenBrowserType;