diff options
Diffstat (limited to 'remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts')
-rw-r--r-- | remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts new file mode 100644 index 0000000000..ace35a52b0 --- /dev/null +++ b/remote/test/puppeteer/packages/puppeteer-core/src/bidi/BidiOverCdp.ts @@ -0,0 +1,209 @@ +/** + * @license + * Copyright 2023 Google Inc. + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js'; +import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; +import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; + +import type {CDPEvents, CDPSession} from '../api/CDPSession.js'; +import type {Connection as CdpConnection} from '../cdp/Connection.js'; +import {debug} from '../common/Debug.js'; +import {TargetCloseError} from '../common/Errors.js'; +import type {Handler} from '../common/EventEmitter.js'; + +import {BidiConnection} from './Connection.js'; + +const bidiServerLogger = (prefix: string, ...args: unknown[]): void => { + debug(`bidi:${prefix}`)(args); +}; + +/** + * @internal + */ +export async function connectBidiOverCdp( + cdp: CdpConnection, + // TODO: replace with `BidiMapper.MapperOptions`, once it's exported in + // https://github.com/puppeteer/puppeteer/pull/11415. + options: {acceptInsecureCerts: boolean} +): Promise<BidiConnection> { + const transportBiDi = new NoOpTransport(); + const cdpConnectionAdapter = new CdpConnectionAdapter(cdp); + const pptrTransport = { + send(message: string): void { + // Forwards a BiDi command sent by Puppeteer to the input of the BidiServer. + transportBiDi.emitMessage(JSON.parse(message)); + }, + close(): void { + bidiServer.close(); + cdpConnectionAdapter.close(); + cdp.dispose(); + }, + onmessage(_message: string): void { + // The method is overridden by the Connection. + }, + }; + transportBiDi.on('bidiResponse', (message: object) => { + // Forwards a BiDi event sent by BidiServer to Puppeteer. + pptrTransport.onmessage(JSON.stringify(message)); + }); + const pptrBiDiConnection = new BidiConnection(cdp.url(), pptrTransport); + const bidiServer = await BidiMapper.BidiServer.createAndStart( + transportBiDi, + cdpConnectionAdapter, + // TODO: most likely need a little bit of refactoring + cdpConnectionAdapter.browserClient(), + '', + options, + undefined, + bidiServerLogger + ); + return pptrBiDiConnection; +} + +/** + * Manages CDPSessions for BidiServer. + * @internal + */ +class CdpConnectionAdapter { + #cdp: CdpConnection; + #adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>(); + #browserCdpConnection: CDPClientAdapter<CdpConnection>; + + constructor(cdp: CdpConnection) { + this.#cdp = cdp; + this.#browserCdpConnection = new CDPClientAdapter(cdp); + } + + browserClient(): CDPClientAdapter<CdpConnection> { + return this.#browserCdpConnection; + } + + getCdpClient(id: string) { + const session = this.#cdp.session(id); + if (!session) { + throw new Error(`Unknown CDP session with id ${id}`); + } + if (!this.#adapters.has(session)) { + const adapter = new CDPClientAdapter( + session, + id, + this.#browserCdpConnection + ); + this.#adapters.set(session, adapter); + return adapter; + } + return this.#adapters.get(session)!; + } + + close() { + this.#browserCdpConnection.close(); + for (const adapter of this.#adapters.values()) { + adapter.close(); + } + } +} + +/** + * Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that + * BidiServer needs. + * + * @internal + */ +class CDPClientAdapter<T extends CDPSession | CdpConnection> + extends BidiMapper.EventEmitter<CDPEvents> + implements BidiMapper.CdpClient +{ + #closed = false; + #client: T; + sessionId: string | undefined = undefined; + #browserClient?: BidiMapper.CdpClient; + + constructor( + client: T, + sessionId?: string, + browserClient?: BidiMapper.CdpClient + ) { + super(); + this.#client = client; + this.sessionId = sessionId; + this.#browserClient = browserClient; + this.#client.on('*', this.#forwardMessage as Handler<any>); + } + + browserClient(): BidiMapper.CdpClient { + return this.#browserClient!; + } + + #forwardMessage = <T extends keyof CDPEvents>( + method: T, + event: CDPEvents[T] + ) => { + this.emit(method, event); + }; + + async sendCommand<T extends keyof ProtocolMapping.Commands>( + method: T, + ...params: ProtocolMapping.Commands[T]['paramsType'] + ): Promise<ProtocolMapping.Commands[T]['returnType']> { + if (this.#closed) { + return; + } + try { + return await this.#client.send(method, ...params); + } catch (err) { + if (this.#closed) { + return; + } + throw err; + } + } + + close() { + this.#client.off('*', this.#forwardMessage as Handler<any>); + this.#closed = true; + } + + isCloseError(error: unknown): boolean { + return error instanceof TargetCloseError; + } +} + +/** + * This transport is given to the BiDi server instance and allows Puppeteer + * to send and receive commands to the BiDiServer. + * @internal + */ +class NoOpTransport + extends BidiMapper.EventEmitter<{ + bidiResponse: Bidi.ChromiumBidi.Message; + }> + implements BidiMapper.BidiTransport +{ + #onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void = + async (_m: Bidi.ChromiumBidi.Command): Promise<void> => { + return; + }; + + emitMessage(message: Bidi.ChromiumBidi.Command) { + void this.#onMessage(message); + } + + setOnMessage( + onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void + ): void { + this.#onMessage = onMessage; + } + + async sendMessage(message: Bidi.ChromiumBidi.Message): Promise<void> { + this.emit('bidiResponse', message); + } + + close() { + this.#onMessage = async (_m: Bidi.ChromiumBidi.Command): Promise<void> => { + return; + }; + } +} |