2020-06-25 16:05:36 -07:00
|
|
|
/**
|
|
|
|
* Copyright (c) Microsoft Corporation.
|
|
|
|
*
|
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
|
* You may obtain a copy of the License at
|
|
|
|
*
|
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
*
|
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
* See the License for the specific language governing permissions and
|
|
|
|
* limitations under the License.
|
|
|
|
*/
|
|
|
|
|
|
|
|
import { EventEmitter } from 'events';
|
2020-08-24 14:48:03 -07:00
|
|
|
import * as channels from '../protocol/channels';
|
|
|
|
import { serializeError } from '../protocol/serializers';
|
|
|
|
import { createScheme, Validator, ValidationError } from '../protocol/validator';
|
2021-04-20 23:03:56 -07:00
|
|
|
import { assert, debugAssert, isUnderTest, monotonicTime } from '../utils/utils';
|
2020-09-10 19:25:44 -07:00
|
|
|
import { tOptional } from '../protocol/validatorPrimitives';
|
2020-09-30 21:17:30 -07:00
|
|
|
import { kBrowserOrContextClosedError } from '../utils/errors';
|
2021-02-10 21:55:46 -08:00
|
|
|
import { CallMetadata, SdkObject } from '../server/instrumentation';
|
|
|
|
import { rewriteErrorMessage } from '../utils/stackTrace';
|
2021-08-19 17:31:14 +02:00
|
|
|
import type { PlaywrightDispatcher } from './playwrightDispatcher';
|
2020-06-25 16:05:36 -07:00
|
|
|
|
2020-06-30 21:30:39 -07:00
|
|
|
export const dispatcherSymbol = Symbol('dispatcher');
|
|
|
|
|
|
|
|
export function lookupDispatcher<DispatcherType>(object: any): DispatcherType {
|
|
|
|
const result = object[dispatcherSymbol];
|
|
|
|
debugAssert(result);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function existingDispatcher<DispatcherType>(object: any): DispatcherType {
|
|
|
|
return object[dispatcherSymbol];
|
|
|
|
}
|
|
|
|
|
2020-07-20 17:38:06 -07:00
|
|
|
export function lookupNullableDispatcher<DispatcherType>(object: any | null): DispatcherType | undefined {
|
|
|
|
return object ? lookupDispatcher(object) : undefined;
|
2020-06-30 21:30:39 -07:00
|
|
|
}
|
|
|
|
|
2021-04-20 23:03:56 -07:00
|
|
|
export class Dispatcher<Type extends { guid: string }, Initializer> extends EventEmitter implements channels.Channel {
|
2020-07-10 16:24:11 -07:00
|
|
|
private _connection: DispatcherConnection;
|
|
|
|
private _isScope: boolean;
|
|
|
|
// Parent is always "isScope".
|
|
|
|
private _parent: Dispatcher<any, any> | undefined;
|
|
|
|
// Only "isScope" channel owners have registered dispatchers inside.
|
|
|
|
private _dispatchers = new Map<string, Dispatcher<any, any>>();
|
2020-07-27 10:21:39 -07:00
|
|
|
private _disposed = false;
|
2020-07-10 16:24:11 -07:00
|
|
|
|
2020-06-25 16:05:36 -07:00
|
|
|
readonly _guid: string;
|
|
|
|
readonly _type: string;
|
2020-07-10 16:24:11 -07:00
|
|
|
readonly _scope: Dispatcher<any, any>;
|
2020-06-29 18:58:09 -07:00
|
|
|
_object: Type;
|
2020-06-25 16:05:36 -07:00
|
|
|
|
2021-04-20 23:03:56 -07:00
|
|
|
constructor(parent: Dispatcher<any, any> | DispatcherConnection, object: Type, type: string, initializer: Initializer, isScope?: boolean) {
|
2020-06-25 16:05:36 -07:00
|
|
|
super();
|
2020-07-10 16:24:11 -07:00
|
|
|
|
|
|
|
this._connection = parent instanceof DispatcherConnection ? parent : parent._connection;
|
|
|
|
this._isScope = !!isScope;
|
|
|
|
this._parent = parent instanceof DispatcherConnection ? undefined : parent;
|
|
|
|
this._scope = isScope ? this : this._parent!;
|
|
|
|
|
2021-04-20 23:03:56 -07:00
|
|
|
const guid = object.guid;
|
2020-07-10 16:24:11 -07:00
|
|
|
assert(!this._connection._dispatchers.has(guid));
|
|
|
|
this._connection._dispatchers.set(guid, this);
|
|
|
|
if (this._parent) {
|
|
|
|
assert(!this._parent._dispatchers.has(guid));
|
|
|
|
this._parent._dispatchers.set(guid, this);
|
|
|
|
}
|
|
|
|
|
2020-06-25 16:05:36 -07:00
|
|
|
this._type = type;
|
|
|
|
this._guid = guid;
|
|
|
|
this._object = object;
|
2020-07-10 16:24:11 -07:00
|
|
|
|
2020-06-30 21:30:39 -07:00
|
|
|
(object as any)[dispatcherSymbol] = this;
|
2020-07-10 16:24:11 -07:00
|
|
|
if (this._parent)
|
2021-06-30 17:56:48 -07:00
|
|
|
this._connection.sendMessageToClient(this._parent._guid, type, '__create__', { type, initializer, guid }, this._parent._object);
|
2020-06-25 16:05:36 -07:00
|
|
|
}
|
|
|
|
|
2020-06-26 17:24:21 -07:00
|
|
|
_dispatchEvent(method: string, params: Dispatcher<any, any> | any = {}) {
|
2020-09-10 16:20:12 -07:00
|
|
|
if (this._disposed) {
|
|
|
|
if (isUnderTest())
|
|
|
|
throw new Error(`${this._guid} is sending "${method}" event after being disposed`);
|
|
|
|
// Just ignore this event outside of tests.
|
|
|
|
return;
|
|
|
|
}
|
2021-04-23 09:28:18 -07:00
|
|
|
const sdkObject = this._object instanceof SdkObject ? this._object : undefined;
|
|
|
|
this._connection.sendMessageToClient(this._guid, this._type, method, params, sdkObject);
|
2020-06-30 22:21:17 -07:00
|
|
|
}
|
|
|
|
|
2020-07-10 16:24:11 -07:00
|
|
|
_dispose() {
|
2020-07-27 10:21:39 -07:00
|
|
|
assert(!this._disposed);
|
2021-04-05 11:50:28 -07:00
|
|
|
this._disposed = true;
|
2020-07-01 13:55:29 -07:00
|
|
|
|
2020-07-10 16:24:11 -07:00
|
|
|
// Clean up from parent and connection.
|
|
|
|
if (this._parent)
|
|
|
|
this._parent._dispatchers.delete(this._guid);
|
|
|
|
this._connection._dispatchers.delete(this._guid);
|
2020-07-01 13:55:29 -07:00
|
|
|
|
2020-07-10 16:24:11 -07:00
|
|
|
// Dispose all children.
|
2020-07-27 10:21:39 -07:00
|
|
|
for (const dispatcher of [...this._dispatchers.values()])
|
|
|
|
dispatcher._dispose();
|
2020-07-10 16:24:11 -07:00
|
|
|
this._dispatchers.clear();
|
2020-07-27 10:21:39 -07:00
|
|
|
|
|
|
|
if (this._isScope)
|
2021-04-23 09:28:18 -07:00
|
|
|
this._connection.sendMessageToClient(this._guid, this._type, '__dispose__', {});
|
2020-06-30 22:21:17 -07:00
|
|
|
}
|
2020-06-25 16:05:36 -07:00
|
|
|
|
2020-07-10 16:24:11 -07:00
|
|
|
_debugScopeState(): any {
|
|
|
|
return {
|
|
|
|
_guid: this._guid,
|
2020-07-27 10:21:39 -07:00
|
|
|
objects: Array.from(this._dispatchers.values()).map(o => o._debugScopeState()),
|
2020-07-10 16:24:11 -07:00
|
|
|
};
|
2020-06-30 22:21:17 -07:00
|
|
|
}
|
2021-02-13 20:31:06 -08:00
|
|
|
|
|
|
|
async waitForEventInfo(): Promise<void> {
|
|
|
|
// Instrumentation takes care of this.
|
|
|
|
}
|
2020-07-10 16:24:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export type DispatcherScope = Dispatcher<any, any>;
|
2021-08-19 17:31:14 +02:00
|
|
|
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),
|
|
|
|
};
|
2020-07-01 13:55:29 -07:00
|
|
|
}
|
2020-06-30 22:21:17 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
export class DispatcherConnection {
|
|
|
|
readonly _dispatchers = new Map<string, Dispatcher<any, any>>();
|
2020-07-13 08:31:20 -07:00
|
|
|
onmessage = (message: object) => {};
|
2020-07-24 15:16:33 -07:00
|
|
|
private _validateParams: (type: string, method: string, params: any) => any;
|
2021-07-02 16:45:09 -07:00
|
|
|
private _validateMetadata: (metadata: any) => { stack?: channels.StackFrame[] };
|
2021-04-18 17:02:34 -10:00
|
|
|
private _waitOperations = new Map<string, CallMetadata>();
|
2020-06-30 22:21:17 -07:00
|
|
|
|
2021-04-23 09:28:18 -07:00
|
|
|
sendMessageToClient(guid: string, type: string, method: string, params: any, sdkObject?: SdkObject) {
|
|
|
|
params = this._replaceDispatchersWithGuids(params);
|
|
|
|
if (sdkObject) {
|
|
|
|
const eventMetadata: CallMetadata = {
|
|
|
|
id: `event@${++lastEventId}`,
|
|
|
|
objectId: sdkObject?.guid,
|
2021-06-30 17:56:48 -07:00
|
|
|
pageId: sdkObject?.attribution?.page?.guid,
|
|
|
|
frameId: sdkObject?.attribution?.frame?.guid,
|
2021-04-23 09:28:18 -07:00
|
|
|
startTime: monotonicTime(),
|
|
|
|
endTime: 0,
|
|
|
|
type,
|
|
|
|
method,
|
|
|
|
params: params || {},
|
|
|
|
log: [],
|
|
|
|
snapshots: []
|
|
|
|
};
|
2021-06-30 17:56:48 -07:00
|
|
|
sdkObject.instrumentation?.onEvent(sdkObject, eventMetadata);
|
2021-04-23 09:28:18 -07:00
|
|
|
}
|
|
|
|
this.onmessage({ guid, method, params });
|
2020-06-25 16:05:36 -07:00
|
|
|
}
|
|
|
|
|
2020-07-01 13:55:29 -07:00
|
|
|
constructor() {
|
2020-07-24 15:16:33 -07:00
|
|
|
const tChannel = (name: string): Validator => {
|
|
|
|
return (arg: any, path: string) => {
|
|
|
|
if (arg && typeof arg === 'object' && typeof arg.guid === 'string') {
|
|
|
|
const guid = arg.guid;
|
|
|
|
const dispatcher = this._dispatchers.get(guid);
|
|
|
|
if (!dispatcher)
|
|
|
|
throw new ValidationError(`${path}: no object with guid ${guid}`);
|
|
|
|
if (name !== '*' && dispatcher._type !== name)
|
|
|
|
throw new ValidationError(`${path}: object with guid ${guid} has type ${dispatcher._type}, expected ${name}`);
|
|
|
|
return dispatcher;
|
|
|
|
}
|
|
|
|
throw new ValidationError(`${path}: expected ${name}`);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
const scheme = createScheme(tChannel);
|
|
|
|
this._validateParams = (type: string, method: string, params: any): any => {
|
|
|
|
const name = type + method[0].toUpperCase() + method.substring(1) + 'Params';
|
|
|
|
if (!scheme[name])
|
2020-08-05 20:44:32 +02:00
|
|
|
throw new ValidationError(`Unknown scheme for ${type}.${method}`);
|
2020-07-24 15:16:33 -07:00
|
|
|
return scheme[name](params, '');
|
|
|
|
};
|
2020-09-10 19:25:44 -07:00
|
|
|
this._validateMetadata = (metadata: any): any => {
|
|
|
|
return tOptional(scheme['Metadata'])(metadata, '');
|
|
|
|
};
|
2020-07-01 13:55:29 -07:00
|
|
|
}
|
|
|
|
|
2020-07-13 08:31:20 -07:00
|
|
|
async dispatch(message: object) {
|
2020-09-10 19:25:44 -07:00
|
|
|
const { id, guid, method, params, metadata } = message as any;
|
2020-06-30 22:21:17 -07:00
|
|
|
const dispatcher = this._dispatchers.get(guid);
|
|
|
|
if (!dispatcher) {
|
2020-09-30 21:17:30 -07:00
|
|
|
this.onmessage({ id, error: serializeError(new Error(kBrowserOrContextClosedError)) });
|
2020-06-30 22:21:17 -07:00
|
|
|
return;
|
|
|
|
}
|
2020-07-01 13:55:29 -07:00
|
|
|
if (method === 'debugScopeState') {
|
2021-08-19 17:31:14 +02:00
|
|
|
const rootDispatcher = this._dispatchers.get('')!;
|
|
|
|
this.onmessage({ id, result: rootDispatcher._debugScopeState() });
|
2020-07-01 13:55:29 -07:00
|
|
|
return;
|
|
|
|
}
|
2021-02-10 21:55:46 -08:00
|
|
|
|
|
|
|
let validParams: any;
|
|
|
|
let validMetadata: channels.Metadata;
|
2020-06-27 11:10:07 -07:00
|
|
|
try {
|
2021-02-10 21:55:46 -08:00
|
|
|
validParams = this._validateParams(dispatcher._type, method, params);
|
|
|
|
validMetadata = this._validateMetadata(metadata);
|
2021-01-04 13:54:55 -08:00
|
|
|
if (typeof (dispatcher as any)[method] !== 'function')
|
|
|
|
throw new Error(`Mismatching dispatcher: "${dispatcher._type}" does not implement "${method}"`);
|
2021-02-10 21:55:46 -08:00
|
|
|
} catch (e) {
|
|
|
|
this.onmessage({ id, error: serializeError(e) });
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-03-01 12:20:04 -08:00
|
|
|
const sdkObject = dispatcher._object instanceof SdkObject ? dispatcher._object : undefined;
|
2021-04-29 09:28:19 -07:00
|
|
|
const callMetadata: CallMetadata = {
|
2021-04-23 09:28:18 -07:00
|
|
|
id: `call@${id}`,
|
2021-02-10 21:55:46 -08:00
|
|
|
...validMetadata,
|
2021-04-23 09:28:18 -07:00
|
|
|
objectId: sdkObject?.guid,
|
2021-06-30 17:56:48 -07:00
|
|
|
pageId: sdkObject?.attribution?.page?.guid,
|
|
|
|
frameId: sdkObject?.attribution?.frame?.guid,
|
2021-02-10 21:55:46 -08:00
|
|
|
startTime: monotonicTime(),
|
|
|
|
endTime: 0,
|
|
|
|
type: dispatcher._type,
|
|
|
|
method,
|
2021-04-08 22:59:05 +08:00
|
|
|
params: params || {},
|
2021-02-10 21:55:46 -08:00
|
|
|
log: [],
|
2021-04-23 09:28:18 -07:00
|
|
|
snapshots: []
|
2021-02-10 21:55:46 -08:00
|
|
|
};
|
|
|
|
|
2021-04-29 09:28:19 -07:00
|
|
|
if (sdkObject && params?.info?.waitId) {
|
|
|
|
// Process logs for waitForNavigation/waitForLoadState
|
|
|
|
const info = params.info;
|
|
|
|
switch (info.phase) {
|
|
|
|
case 'before': {
|
|
|
|
this._waitOperations.set(info.waitId, callMetadata);
|
|
|
|
await sdkObject.instrumentation.onBeforeCall(sdkObject, callMetadata);
|
|
|
|
return;
|
|
|
|
} case 'log': {
|
|
|
|
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
|
|
|
originalMetadata.log.push(info.message);
|
|
|
|
sdkObject.instrumentation.onCallLog('api', info.message, sdkObject, originalMetadata);
|
|
|
|
return;
|
|
|
|
} case 'after': {
|
|
|
|
const originalMetadata = this._waitOperations.get(info.waitId)!;
|
|
|
|
originalMetadata.endTime = monotonicTime();
|
2021-07-02 14:33:38 -07:00
|
|
|
originalMetadata.error = info.error ? { error: { name: 'Error', message: info.error } } : undefined;
|
2021-04-29 09:28:19 -07:00
|
|
|
this._waitOperations.delete(info.waitId);
|
|
|
|
await sdkObject.instrumentation.onAfterCall(sdkObject, originalMetadata);
|
|
|
|
return;
|
2021-04-18 17:02:34 -10:00
|
|
|
}
|
|
|
|
}
|
2021-04-29 09:28:19 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let error: any;
|
|
|
|
await sdkObject?.instrumentation.onBeforeCall(sdkObject, callMetadata);
|
|
|
|
try {
|
2021-07-02 14:33:38 -07:00
|
|
|
const result = await (dispatcher as any)[method](validParams, callMetadata);
|
|
|
|
callMetadata.result = this._replaceDispatchersWithGuids(result);
|
2020-06-27 11:10:07 -07:00
|
|
|
} catch (e) {
|
2021-02-10 21:55:46 -08:00
|
|
|
// Dispatching error
|
2021-07-02 14:33:38 -07:00
|
|
|
// We want original, unmodified error in metadata.
|
|
|
|
callMetadata.error = serializeError(e);
|
2021-02-10 21:55:46 -08:00
|
|
|
if (callMetadata.log.length)
|
2021-06-28 13:27:38 -07:00
|
|
|
rewriteErrorMessage(e, e.message + formatLogRecording(callMetadata.log));
|
2021-04-29 09:28:19 -07:00
|
|
|
error = serializeError(e);
|
2021-02-10 21:55:46 -08:00
|
|
|
} finally {
|
2021-02-17 22:10:13 -08:00
|
|
|
callMetadata.endTime = monotonicTime();
|
2021-04-29 09:28:19 -07:00
|
|
|
await sdkObject?.instrumentation.onAfterCall(sdkObject, callMetadata);
|
2020-06-27 11:10:07 -07:00
|
|
|
}
|
2021-04-29 09:28:19 -07:00
|
|
|
|
2021-07-02 14:33:38 -07:00
|
|
|
if (callMetadata.error)
|
|
|
|
this.onmessage({ id, error: error });
|
2021-04-29 09:28:19 -07:00
|
|
|
else
|
2021-07-02 14:33:38 -07:00
|
|
|
this.onmessage({ id, result: callMetadata.result });
|
2020-06-25 16:05:36 -07:00
|
|
|
}
|
|
|
|
|
2020-11-02 13:06:54 -08:00
|
|
|
private _replaceDispatchersWithGuids(payload: any): any {
|
2020-06-25 16:05:36 -07:00
|
|
|
if (!payload)
|
|
|
|
return payload;
|
2020-11-02 13:06:54 -08:00
|
|
|
if (payload instanceof Dispatcher)
|
2020-06-25 16:05:36 -07:00
|
|
|
return { guid: payload._guid };
|
|
|
|
if (Array.isArray(payload))
|
2020-11-02 13:06:54 -08:00
|
|
|
return payload.map(p => this._replaceDispatchersWithGuids(p));
|
2020-06-26 17:24:21 -07:00
|
|
|
if (typeof payload === 'object') {
|
|
|
|
const result: any = {};
|
|
|
|
for (const key of Object.keys(payload))
|
2020-11-02 13:06:54 -08:00
|
|
|
result[key] = this._replaceDispatchersWithGuids(payload[key]);
|
2020-06-26 17:24:21 -07:00
|
|
|
return result;
|
|
|
|
}
|
2020-06-25 16:05:36 -07:00
|
|
|
return payload;
|
|
|
|
}
|
|
|
|
}
|
2021-02-10 21:55:46 -08:00
|
|
|
|
|
|
|
function formatLogRecording(log: string[]): string {
|
|
|
|
if (!log.length)
|
|
|
|
return '';
|
|
|
|
const header = ` logs `;
|
|
|
|
const headerLength = 60;
|
|
|
|
const leftLength = (headerLength - header.length) / 2;
|
|
|
|
const rightLength = headerLength - header.length - leftLength;
|
|
|
|
return `\n${'='.repeat(leftLength)}${header}${'='.repeat(rightLength)}\n${log.join('\n')}\n${'='.repeat(headerLength)}`;
|
|
|
|
}
|
2021-04-23 09:28:18 -07:00
|
|
|
|
|
|
|
let lastEventId = 0;
|