Connection.ts (7920B)
1 /** 2 * @license 3 * Copyright 2017 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {Protocol} from 'devtools-protocol'; 8 import type {ProtocolMapping} from 'devtools-protocol/types/protocol-mapping.js'; 9 10 import type {CommandOptions} from '../api/CDPSession.js'; 11 import { 12 CDPSessionEvent, 13 type CDPSession, 14 type CDPSessionEvents, 15 } from '../api/CDPSession.js'; 16 import {CallbackRegistry} from '../common/CallbackRegistry.js'; 17 import type {ConnectionTransport} from '../common/ConnectionTransport.js'; 18 import {debug} from '../common/Debug.js'; 19 import {TargetCloseError} from '../common/Errors.js'; 20 import {EventEmitter} from '../common/EventEmitter.js'; 21 import {createProtocolErrorMessage} from '../util/ErrorLike.js'; 22 23 import {CdpCDPSession} from './CdpSession.js'; 24 25 const debugProtocolSend = debug('puppeteer:protocol:SEND ►'); 26 const debugProtocolReceive = debug('puppeteer:protocol:RECV ◀'); 27 28 /** 29 * @public 30 */ 31 export class Connection extends EventEmitter<CDPSessionEvents> { 32 #url: string; 33 #transport: ConnectionTransport; 34 #delay: number; 35 #timeout: number; 36 #sessions = new Map<string, CdpCDPSession>(); 37 #closed = false; 38 #manuallyAttached = new Set<string>(); 39 #callbacks: CallbackRegistry; 40 #rawErrors = false; 41 42 constructor( 43 url: string, 44 transport: ConnectionTransport, 45 delay = 0, 46 timeout?: number, 47 rawErrors = false, 48 ) { 49 super(); 50 this.#rawErrors = rawErrors; 51 this.#callbacks = new CallbackRegistry(); 52 this.#url = url; 53 this.#delay = delay; 54 this.#timeout = timeout ?? 180_000; 55 56 this.#transport = transport; 57 this.#transport.onmessage = this.onMessage.bind(this); 58 this.#transport.onclose = this.#onClose.bind(this); 59 } 60 61 static fromSession(session: CDPSession): Connection | undefined { 62 return session.connection(); 63 } 64 65 /** 66 * @internal 67 */ 68 get delay(): number { 69 return this.#delay; 70 } 71 72 get timeout(): number { 73 return this.#timeout; 74 } 75 76 /** 77 * @internal 78 */ 79 get _closed(): boolean { 80 return this.#closed; 81 } 82 83 /** 84 * @internal 85 */ 86 get _sessions(): Map<string, CdpCDPSession> { 87 return this.#sessions; 88 } 89 90 /** 91 * @internal 92 */ 93 _session(sessionId: string): CdpCDPSession | null { 94 return this.#sessions.get(sessionId) || null; 95 } 96 97 /** 98 * @param sessionId - The session id 99 * @returns The current CDP session if it exists 100 */ 101 session(sessionId: string): CDPSession | null { 102 return this._session(sessionId); 103 } 104 105 url(): string { 106 return this.#url; 107 } 108 109 send<T extends keyof ProtocolMapping.Commands>( 110 method: T, 111 params?: ProtocolMapping.Commands[T]['paramsType'][0], 112 options?: CommandOptions, 113 ): Promise<ProtocolMapping.Commands[T]['returnType']> { 114 // There is only ever 1 param arg passed, but the Protocol defines it as an 115 // array of 0 or 1 items See this comment: 116 // https://github.com/ChromeDevTools/devtools-protocol/pull/113#issuecomment-412603285 117 // which explains why the protocol defines the params this way for better 118 // type-inference. 119 // So now we check if there are any params or not and deal with them accordingly. 120 return this._rawSend(this.#callbacks, method, params, undefined, options); 121 } 122 123 /** 124 * @internal 125 */ 126 _rawSend<T extends keyof ProtocolMapping.Commands>( 127 callbacks: CallbackRegistry, 128 method: T, 129 params: ProtocolMapping.Commands[T]['paramsType'][0], 130 sessionId?: string, 131 options?: CommandOptions, 132 ): Promise<ProtocolMapping.Commands[T]['returnType']> { 133 if (this.#closed) { 134 return Promise.reject(new Error('Protocol error: Connection closed.')); 135 } 136 return callbacks.create(method, options?.timeout ?? this.#timeout, id => { 137 const stringifiedMessage = JSON.stringify({ 138 method, 139 params, 140 id, 141 sessionId, 142 }); 143 debugProtocolSend(stringifiedMessage); 144 this.#transport.send(stringifiedMessage); 145 }) as Promise<ProtocolMapping.Commands[T]['returnType']>; 146 } 147 148 /** 149 * @internal 150 */ 151 async closeBrowser(): Promise<void> { 152 await this.send('Browser.close'); 153 } 154 155 /** 156 * @internal 157 */ 158 protected async onMessage(message: string): Promise<void> { 159 if (this.#delay) { 160 await new Promise(r => { 161 return setTimeout(r, this.#delay); 162 }); 163 } 164 debugProtocolReceive(message); 165 const object = JSON.parse(message); 166 if (object.method === 'Target.attachedToTarget') { 167 const sessionId = object.params.sessionId; 168 const session = new CdpCDPSession( 169 this, 170 object.params.targetInfo.type, 171 sessionId, 172 object.sessionId, 173 this.#rawErrors, 174 ); 175 this.#sessions.set(sessionId, session); 176 this.emit(CDPSessionEvent.SessionAttached, session); 177 const parentSession = this.#sessions.get(object.sessionId); 178 if (parentSession) { 179 parentSession.emit(CDPSessionEvent.SessionAttached, session); 180 } 181 } else if (object.method === 'Target.detachedFromTarget') { 182 const session = this.#sessions.get(object.params.sessionId); 183 if (session) { 184 session.onClosed(); 185 this.#sessions.delete(object.params.sessionId); 186 this.emit(CDPSessionEvent.SessionDetached, session); 187 const parentSession = this.#sessions.get(object.sessionId); 188 if (parentSession) { 189 parentSession.emit(CDPSessionEvent.SessionDetached, session); 190 } 191 } 192 } 193 if (object.sessionId) { 194 const session = this.#sessions.get(object.sessionId); 195 if (session) { 196 session.onMessage(object); 197 } 198 } else if (object.id) { 199 if (object.error) { 200 if (this.#rawErrors) { 201 this.#callbacks.rejectRaw(object.id, object.error); 202 } else { 203 this.#callbacks.reject( 204 object.id, 205 createProtocolErrorMessage(object), 206 object.error.message, 207 ); 208 } 209 } else { 210 this.#callbacks.resolve(object.id, object.result); 211 } 212 } else { 213 this.emit(object.method, object.params); 214 } 215 } 216 217 #onClose(): void { 218 if (this.#closed) { 219 return; 220 } 221 this.#closed = true; 222 this.#transport.onmessage = undefined; 223 this.#transport.onclose = undefined; 224 this.#callbacks.clear(); 225 for (const session of this.#sessions.values()) { 226 session.onClosed(); 227 } 228 this.#sessions.clear(); 229 this.emit(CDPSessionEvent.Disconnected, undefined); 230 } 231 232 dispose(): void { 233 this.#onClose(); 234 this.#transport.close(); 235 } 236 237 /** 238 * @internal 239 */ 240 isAutoAttached(targetId: string): boolean { 241 return !this.#manuallyAttached.has(targetId); 242 } 243 244 /** 245 * @internal 246 */ 247 async _createSession( 248 targetInfo: {targetId: string}, 249 isAutoAttachEmulated = true, 250 ): Promise<CdpCDPSession> { 251 if (!isAutoAttachEmulated) { 252 this.#manuallyAttached.add(targetInfo.targetId); 253 } 254 const {sessionId} = await this.send('Target.attachToTarget', { 255 targetId: targetInfo.targetId, 256 flatten: true, 257 }); 258 this.#manuallyAttached.delete(targetInfo.targetId); 259 const session = this.#sessions.get(sessionId); 260 if (!session) { 261 throw new Error('CDPSession creation failed.'); 262 } 263 return session; 264 } 265 266 /** 267 * @param targetInfo - The target info 268 * @returns The CDP session that is created 269 */ 270 async createSession( 271 targetInfo: Protocol.Target.TargetInfo, 272 ): Promise<CDPSession> { 273 return await this._createSession(targetInfo, false); 274 } 275 276 /** 277 * @internal 278 */ 279 getPendingProtocolErrors(): Error[] { 280 const result: Error[] = []; 281 result.push(...this.#callbacks.getPendingProtocolErrors()); 282 for (const session of this.#sessions.values()) { 283 result.push(...session.getPendingProtocolErrors()); 284 } 285 return result; 286 } 287 } 288 289 /** 290 * @internal 291 */ 292 export function isTargetClosedError(error: Error): boolean { 293 return error instanceof TargetCloseError; 294 }