/** * 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 { 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, WebSocket } from './network'; import { Page, BindingCall } from './page'; import { Worker } from './worker'; import { Dialog } from './dialog'; import { parseError, TargetClosedError } from './errors'; import { CDPSession } from './cdpSession'; import { Playwright } from './playwright'; import { Electron, ElectronApplication } from './electron'; import type * as channels from '@protocol/channels'; import { Stream } from './stream'; import { WritableStream } from './writableStream'; import { debugLogger } from '../utils/debugLogger'; import { SelectorsOwner } from './selectors'; import { Android, AndroidSocket, AndroidDevice } from './android'; import { Artifact } from './artifact'; import { EventEmitter } from 'events'; import { JsonPipe } from './jsonPipe'; import { APIRequestContext } from './fetch'; import { LocalUtils } from './localUtils'; import { Tracing } from './tracing'; import { findValidator, ValidationError, type ValidatorContext } from '../protocol/validator'; import { createInstrumentation } from './clientInstrumentation'; import type { ClientInstrumentation } from './clientInstrumentation'; import { formatCallLog, rewriteErrorMessage } from '../utils'; class Root extends ChannelOwner { constructor(connection: Connection) { super(connection, 'Root', '', {}); } async initialize(): Promise { return Playwright.from((await this._channel.initialize({ sdkLanguage: 'javascript', })).playwright); } } class DummyChannelOwner extends ChannelOwner { } export class Connection extends EventEmitter { readonly _objects = new Map(); onmessage = (message: object): void => {}; private _lastId = 0; private _callbacks = new Map void, reject: (a: Error) => void, apiName: string | undefined, type: string, method: string }>(); private _rootObject: Root; private _closedError: Error | undefined; private _isRemote = false; private _localUtils?: LocalUtils; private _rawBuffers = false; // Some connections allow resolving in-process dispatchers. toImpl: ((client: ChannelOwner) => any) | undefined; private _tracingCount = 0; readonly _instrumentation: ClientInstrumentation; constructor(localUtils: LocalUtils | undefined, instrumentation: ClientInstrumentation | undefined) { super(); this._rootObject = new Root(this); this._localUtils = localUtils; this._instrumentation = instrumentation || createInstrumentation(); } markAsRemote() { this._isRemote = true; } isRemote() { return this._isRemote; } useRawBuffers() { this._rawBuffers = true; } rawBuffers() { return this._rawBuffers; } localUtils(): LocalUtils { return this._localUtils!; } async initializePlaywright(): Promise { return await this._rootObject.initialize(); } getObjectWithKnownName(guid: string): any { return this._objects.get(guid)!; } setIsTracing(isTracing: boolean) { if (isTracing) this._tracingCount++; else this._tracingCount--; } async sendMessageToServer(object: ChannelOwner, method: string, params: any, apiName: string | undefined, frames: channels.StackFrame[], wallTime: number | undefined): Promise { if (this._closedError) throw this._closedError; if (object._wasCollected) throw new Error('The object has been collected to prevent unbounded heap growth.'); const guid = object._guid; const type = object._type; const id = ++this._lastId; const message = { id, guid, method, params }; if (debugLogger.isEnabled('channel')) { // Do not include metadata in debug logs to avoid noise. debugLogger.log('channel', 'SEND> ' + JSON.stringify(message)); } const location = frames[0] ? { file: frames[0].file, line: frames[0].line, column: frames[0].column } : undefined; const metadata: channels.Metadata = { wallTime, apiName, location, internal: !apiName }; if (this._tracingCount && frames && type !== 'LocalUtils') this._localUtils?._channel.addStackToTracingNoReply({ callData: { stack: frames, id } }).catch(() => {}); this.onmessage({ ...message, metadata }); return await new Promise((resolve, reject) => this._callbacks.set(id, { resolve, reject, apiName, type, method })); } dispatch(message: object) { if (this._closedError) return; const { id, guid, method, params, result, error, log } = message as any; if (id) { if (debugLogger.isEnabled('channel')) debugLogger.log('channel', '; const validator = findValidator(type, '', 'Initializer'); initializer = validator(initializer, '', { tChannelImpl: this._tChannelImplFromWire.bind(this), binary: this._rawBuffers ? 'buffer' : 'fromBase64' }); switch (type) { case 'Android': result = new Android(parent, type, guid, initializer); break; case 'AndroidSocket': result = new AndroidSocket(parent, type, guid, initializer); break; case 'AndroidDevice': result = new AndroidDevice(parent, type, guid, initializer); break; case 'APIRequestContext': result = new APIRequestContext(parent, type, guid, initializer); break; case 'Artifact': result = new Artifact(parent, type, guid, initializer); break; case 'BindingCall': result = new BindingCall(parent, type, guid, initializer); break; case 'Browser': result = new Browser(parent, type, guid, initializer); break; case 'BrowserContext': result = new BrowserContext(parent, type, guid, initializer); break; case 'BrowserType': result = new BrowserType(parent, type, guid, initializer); break; case 'CDPSession': result = new CDPSession(parent, type, guid, initializer); break; case 'Dialog': result = new Dialog(parent, type, guid, initializer); break; case 'Electron': result = new Electron(parent, type, guid, initializer); break; case 'ElectronApplication': result = new ElectronApplication(parent, type, guid, initializer); break; case 'ElementHandle': result = new ElementHandle(parent, type, guid, initializer); break; case 'Frame': result = new Frame(parent, type, guid, initializer); break; case 'JSHandle': result = new JSHandle(parent, type, guid, initializer); break; case 'JsonPipe': result = new JsonPipe(parent, type, guid, initializer); break; case 'LocalUtils': result = new LocalUtils(parent, type, guid, initializer); if (!this._localUtils) this._localUtils = result as LocalUtils; break; case 'Page': result = new Page(parent, type, guid, initializer); break; case 'Playwright': result = new Playwright(parent, type, guid, initializer); break; case 'Request': result = new Request(parent, type, guid, initializer); break; case 'Response': result = new Response(parent, type, guid, initializer); break; case 'Route': result = new Route(parent, type, guid, initializer); break; case 'Stream': result = new Stream(parent, type, guid, initializer); break; case 'Selectors': result = new SelectorsOwner(parent, type, guid, initializer); break; case 'SocksSupport': result = new DummyChannelOwner(parent, type, guid, initializer); break; case 'Tracing': result = new Tracing(parent, type, guid, initializer); break; case 'WebSocket': result = new WebSocket(parent, type, guid, initializer); break; case 'Worker': result = new Worker(parent, type, guid, initializer); break; case 'WritableStream': result = new WritableStream(parent, type, guid, initializer); break; default: throw new Error('Missing type ' + type); } return result; } }