BidiOverCdp.ts (5410B)
1 /** 2 * @license 3 * Copyright 2023 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import * as BidiMapper from 'chromium-bidi/lib/cjs/bidiMapper/BidiMapper.js'; 8 import type * as Bidi from 'chromium-bidi/lib/cjs/protocol/protocol.js'; 9 import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; 10 11 import type {CDPEvents, CDPSession} from '../api/CDPSession.js'; 12 import type {Connection as CdpConnection} from '../cdp/Connection.js'; 13 import {debug} from '../common/Debug.js'; 14 import {TargetCloseError} from '../common/Errors.js'; 15 import type {Handler} from '../common/EventEmitter.js'; 16 17 import {BidiConnection} from './Connection.js'; 18 19 const bidiServerLogger = (prefix: string, ...args: unknown[]): void => { 20 debug(`bidi:${prefix}`)(args); 21 }; 22 23 /** 24 * @internal 25 */ 26 export async function connectBidiOverCdp( 27 cdp: CdpConnection, 28 ): Promise<BidiConnection> { 29 const transportBiDi = new NoOpTransport(); 30 const cdpConnectionAdapter = new CdpConnectionAdapter(cdp); 31 const pptrTransport = { 32 send(message: string): void { 33 // Forwards a BiDi command sent by Puppeteer to the input of the BidiServer. 34 transportBiDi.emitMessage(JSON.parse(message)); 35 }, 36 close(): void { 37 bidiServer.close(); 38 cdpConnectionAdapter.close(); 39 cdp.dispose(); 40 }, 41 onmessage(_message: string): void { 42 // The method is overridden by the Connection. 43 }, 44 }; 45 transportBiDi.on('bidiResponse', (message: object) => { 46 // Forwards a BiDi event sent by BidiServer to Puppeteer. 47 pptrTransport.onmessage(JSON.stringify(message)); 48 }); 49 const pptrBiDiConnection = new BidiConnection( 50 cdp.url(), 51 pptrTransport, 52 cdp.delay, 53 cdp.timeout, 54 ); 55 const bidiServer = await BidiMapper.BidiServer.createAndStart( 56 transportBiDi, 57 cdpConnectionAdapter, 58 cdpConnectionAdapter.browserClient(), 59 /* selfTargetId= */ '', 60 undefined, 61 bidiServerLogger, 62 ); 63 return pptrBiDiConnection; 64 } 65 66 /** 67 * Manages CDPSessions for BidiServer. 68 * @internal 69 */ 70 class CdpConnectionAdapter { 71 #cdp: CdpConnection; 72 #adapters = new Map<CDPSession, CDPClientAdapter<CDPSession>>(); 73 #browserCdpConnection: CDPClientAdapter<CdpConnection>; 74 75 constructor(cdp: CdpConnection) { 76 this.#cdp = cdp; 77 this.#browserCdpConnection = new CDPClientAdapter(cdp); 78 } 79 80 browserClient(): CDPClientAdapter<CdpConnection> { 81 return this.#browserCdpConnection; 82 } 83 84 getCdpClient(id: string) { 85 const session = this.#cdp.session(id); 86 if (!session) { 87 throw new Error(`Unknown CDP session with id ${id}`); 88 } 89 if (!this.#adapters.has(session)) { 90 const adapter = new CDPClientAdapter( 91 session, 92 id, 93 this.#browserCdpConnection, 94 ); 95 this.#adapters.set(session, adapter); 96 return adapter; 97 } 98 return this.#adapters.get(session)!; 99 } 100 101 close() { 102 this.#browserCdpConnection.close(); 103 for (const adapter of this.#adapters.values()) { 104 adapter.close(); 105 } 106 } 107 } 108 109 /** 110 * Wrapper on top of CDPSession/CDPConnection to satisfy CDP interface that 111 * BidiServer needs. 112 * 113 * @internal 114 */ 115 class CDPClientAdapter<T extends CDPSession | CdpConnection> 116 extends BidiMapper.EventEmitter<CDPEvents> 117 implements BidiMapper.CdpClient 118 { 119 #closed = false; 120 #client: T; 121 sessionId: string | undefined = undefined; 122 #browserClient?: BidiMapper.CdpClient; 123 124 constructor( 125 client: T, 126 sessionId?: string, 127 browserClient?: BidiMapper.CdpClient, 128 ) { 129 super(); 130 this.#client = client; 131 this.sessionId = sessionId; 132 this.#browserClient = browserClient; 133 this.#client.on('*', this.#forwardMessage as Handler<any>); 134 } 135 136 browserClient(): BidiMapper.CdpClient { 137 return this.#browserClient!; 138 } 139 140 #forwardMessage = <T extends keyof CDPEvents>( 141 method: T, 142 event: CDPEvents[T], 143 ) => { 144 this.emit(method, event); 145 }; 146 147 async sendCommand<T extends keyof ProtocolMapping.Commands>( 148 method: T, 149 ...params: ProtocolMapping.Commands[T]['paramsType'] 150 ): Promise<ProtocolMapping.Commands[T]['returnType']> { 151 if (this.#closed) { 152 return; 153 } 154 try { 155 return await this.#client.send(method, ...params); 156 } catch (err) { 157 if (this.#closed) { 158 return; 159 } 160 throw err; 161 } 162 } 163 164 close() { 165 this.#client.off('*', this.#forwardMessage as Handler<any>); 166 this.#closed = true; 167 } 168 169 isCloseError(error: unknown): boolean { 170 return error instanceof TargetCloseError; 171 } 172 } 173 174 /** 175 * This transport is given to the BiDi server instance and allows Puppeteer 176 * to send and receive commands to the BiDiServer. 177 * @internal 178 */ 179 class NoOpTransport 180 extends BidiMapper.EventEmitter<{ 181 bidiResponse: Bidi.ChromiumBidi.Message; 182 }> 183 implements BidiMapper.BidiTransport 184 { 185 #onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void = 186 async (_m: Bidi.ChromiumBidi.Command): Promise<void> => { 187 return; 188 }; 189 190 emitMessage(message: Bidi.ChromiumBidi.Command) { 191 void this.#onMessage(message); 192 } 193 194 setOnMessage( 195 onMessage: (message: Bidi.ChromiumBidi.Command) => Promise<void> | void, 196 ): void { 197 this.#onMessage = onMessage; 198 } 199 200 async sendMessage(message: Bidi.ChromiumBidi.Message): Promise<void> { 201 this.emit('bidiResponse', message); 202 } 203 204 close() { 205 this.#onMessage = async (_m: Bidi.ChromiumBidi.Command): Promise<void> => { 206 return; 207 }; 208 } 209 }