Browser.ts (12103B)
1 /** 2 * @license 3 * Copyright 2017 Google Inc. 4 * SPDX-License-Identifier: Apache-2.0 5 */ 6 7 import type {ChildProcess} from 'node:child_process'; 8 9 import type {Protocol} from 'devtools-protocol'; 10 11 import type {DebugInfo} from '../api/Browser.js'; 12 import { 13 Browser as BrowserBase, 14 BrowserEvent, 15 type BrowserCloseCallback, 16 type BrowserContextOptions, 17 type IsPageTargetCallback, 18 type TargetFilterCallback, 19 } from '../api/Browser.js'; 20 import {BrowserContextEvent} from '../api/BrowserContext.js'; 21 import {CDPSessionEvent} from '../api/CDPSession.js'; 22 import type {Page} from '../api/Page.js'; 23 import type {Target} from '../api/Target.js'; 24 import type {DownloadBehavior} from '../common/DownloadBehavior.js'; 25 import type {Viewport} from '../common/Viewport.js'; 26 27 import {CdpBrowserContext} from './BrowserContext.js'; 28 import type {CdpCDPSession} from './CdpSession.js'; 29 import type {Connection} from './Connection.js'; 30 import { 31 DevToolsTarget, 32 InitializationStatus, 33 OtherTarget, 34 PageTarget, 35 WorkerTarget, 36 type CdpTarget, 37 } from './Target.js'; 38 import {TargetManagerEvent} from './TargetManageEvents.js'; 39 import {TargetManager} from './TargetManager.js'; 40 41 /** 42 * @internal 43 */ 44 export class CdpBrowser extends BrowserBase { 45 readonly protocol = 'cdp'; 46 47 static async _create( 48 connection: Connection, 49 contextIds: string[], 50 acceptInsecureCerts: boolean, 51 defaultViewport?: Viewport | null, 52 downloadBehavior?: DownloadBehavior, 53 process?: ChildProcess, 54 closeCallback?: BrowserCloseCallback, 55 targetFilterCallback?: TargetFilterCallback, 56 isPageTargetCallback?: IsPageTargetCallback, 57 waitForInitiallyDiscoveredTargets = true, 58 ): Promise<CdpBrowser> { 59 const browser = new CdpBrowser( 60 connection, 61 contextIds, 62 defaultViewport, 63 process, 64 closeCallback, 65 targetFilterCallback, 66 isPageTargetCallback, 67 waitForInitiallyDiscoveredTargets, 68 ); 69 if (acceptInsecureCerts) { 70 await connection.send('Security.setIgnoreCertificateErrors', { 71 ignore: true, 72 }); 73 } 74 await browser._attach(downloadBehavior); 75 return browser; 76 } 77 #defaultViewport?: Viewport | null; 78 #process?: ChildProcess; 79 #connection: Connection; 80 #closeCallback: BrowserCloseCallback; 81 #targetFilterCallback: TargetFilterCallback; 82 #isPageTargetCallback!: IsPageTargetCallback; 83 #defaultContext: CdpBrowserContext; 84 #contexts = new Map<string, CdpBrowserContext>(); 85 #targetManager: TargetManager; 86 87 constructor( 88 connection: Connection, 89 contextIds: string[], 90 defaultViewport?: Viewport | null, 91 process?: ChildProcess, 92 closeCallback?: BrowserCloseCallback, 93 targetFilterCallback?: TargetFilterCallback, 94 isPageTargetCallback?: IsPageTargetCallback, 95 waitForInitiallyDiscoveredTargets = true, 96 ) { 97 super(); 98 this.#defaultViewport = defaultViewport; 99 this.#process = process; 100 this.#connection = connection; 101 this.#closeCallback = closeCallback || (() => {}); 102 this.#targetFilterCallback = 103 targetFilterCallback || 104 (() => { 105 return true; 106 }); 107 this.#setIsPageTargetCallback(isPageTargetCallback); 108 this.#targetManager = new TargetManager( 109 connection, 110 this.#createTarget, 111 this.#targetFilterCallback, 112 waitForInitiallyDiscoveredTargets, 113 ); 114 this.#defaultContext = new CdpBrowserContext(this.#connection, this); 115 for (const contextId of contextIds) { 116 this.#contexts.set( 117 contextId, 118 new CdpBrowserContext(this.#connection, this, contextId), 119 ); 120 } 121 } 122 123 #emitDisconnected = () => { 124 this.emit(BrowserEvent.Disconnected, undefined); 125 }; 126 127 async _attach(downloadBehavior: DownloadBehavior | undefined): Promise<void> { 128 this.#connection.on(CDPSessionEvent.Disconnected, this.#emitDisconnected); 129 if (downloadBehavior) { 130 await this.#defaultContext.setDownloadBehavior(downloadBehavior); 131 } 132 this.#targetManager.on( 133 TargetManagerEvent.TargetAvailable, 134 this.#onAttachedToTarget, 135 ); 136 this.#targetManager.on( 137 TargetManagerEvent.TargetGone, 138 this.#onDetachedFromTarget, 139 ); 140 this.#targetManager.on( 141 TargetManagerEvent.TargetChanged, 142 this.#onTargetChanged, 143 ); 144 this.#targetManager.on( 145 TargetManagerEvent.TargetDiscovered, 146 this.#onTargetDiscovered, 147 ); 148 await this.#targetManager.initialize(); 149 } 150 151 _detach(): void { 152 this.#connection.off(CDPSessionEvent.Disconnected, this.#emitDisconnected); 153 this.#targetManager.off( 154 TargetManagerEvent.TargetAvailable, 155 this.#onAttachedToTarget, 156 ); 157 this.#targetManager.off( 158 TargetManagerEvent.TargetGone, 159 this.#onDetachedFromTarget, 160 ); 161 this.#targetManager.off( 162 TargetManagerEvent.TargetChanged, 163 this.#onTargetChanged, 164 ); 165 this.#targetManager.off( 166 TargetManagerEvent.TargetDiscovered, 167 this.#onTargetDiscovered, 168 ); 169 } 170 171 override process(): ChildProcess | null { 172 return this.#process ?? null; 173 } 174 175 _targetManager(): TargetManager { 176 return this.#targetManager; 177 } 178 179 #setIsPageTargetCallback(isPageTargetCallback?: IsPageTargetCallback): void { 180 this.#isPageTargetCallback = 181 isPageTargetCallback || 182 ((target: Target): boolean => { 183 return ( 184 target.type() === 'page' || 185 target.type() === 'background_page' || 186 target.type() === 'webview' 187 ); 188 }); 189 } 190 191 _getIsPageTargetCallback(): IsPageTargetCallback | undefined { 192 return this.#isPageTargetCallback; 193 } 194 195 override async createBrowserContext( 196 options: BrowserContextOptions = {}, 197 ): Promise<CdpBrowserContext> { 198 const {proxyServer, proxyBypassList, downloadBehavior} = options; 199 200 const {browserContextId} = await this.#connection.send( 201 'Target.createBrowserContext', 202 { 203 proxyServer, 204 proxyBypassList: proxyBypassList && proxyBypassList.join(','), 205 }, 206 ); 207 const context = new CdpBrowserContext( 208 this.#connection, 209 this, 210 browserContextId, 211 ); 212 if (downloadBehavior) { 213 await context.setDownloadBehavior(downloadBehavior); 214 } 215 this.#contexts.set(browserContextId, context); 216 return context; 217 } 218 219 override browserContexts(): CdpBrowserContext[] { 220 return [this.#defaultContext, ...Array.from(this.#contexts.values())]; 221 } 222 223 override defaultBrowserContext(): CdpBrowserContext { 224 return this.#defaultContext; 225 } 226 227 async _disposeContext(contextId?: string): Promise<void> { 228 if (!contextId) { 229 return; 230 } 231 await this.#connection.send('Target.disposeBrowserContext', { 232 browserContextId: contextId, 233 }); 234 this.#contexts.delete(contextId); 235 } 236 237 #createTarget = ( 238 targetInfo: Protocol.Target.TargetInfo, 239 session?: CdpCDPSession, 240 ) => { 241 const {browserContextId} = targetInfo; 242 const context = 243 browserContextId && this.#contexts.has(browserContextId) 244 ? this.#contexts.get(browserContextId) 245 : this.#defaultContext; 246 247 if (!context) { 248 throw new Error('Missing browser context'); 249 } 250 251 const createSession = (isAutoAttachEmulated: boolean) => { 252 return this.#connection._createSession(targetInfo, isAutoAttachEmulated); 253 }; 254 const otherTarget = new OtherTarget( 255 targetInfo, 256 session, 257 context, 258 this.#targetManager, 259 createSession, 260 ); 261 if (targetInfo.url?.startsWith('devtools://')) { 262 return new DevToolsTarget( 263 targetInfo, 264 session, 265 context, 266 this.#targetManager, 267 createSession, 268 this.#defaultViewport ?? null, 269 ); 270 } 271 if (this.#isPageTargetCallback(otherTarget)) { 272 return new PageTarget( 273 targetInfo, 274 session, 275 context, 276 this.#targetManager, 277 createSession, 278 this.#defaultViewport ?? null, 279 ); 280 } 281 if ( 282 targetInfo.type === 'service_worker' || 283 targetInfo.type === 'shared_worker' 284 ) { 285 return new WorkerTarget( 286 targetInfo, 287 session, 288 context, 289 this.#targetManager, 290 createSession, 291 ); 292 } 293 return otherTarget; 294 }; 295 296 #onAttachedToTarget = async (target: CdpTarget) => { 297 if ( 298 target._isTargetExposed() && 299 (await target._initializedDeferred.valueOrThrow()) === 300 InitializationStatus.SUCCESS 301 ) { 302 this.emit(BrowserEvent.TargetCreated, target); 303 target.browserContext().emit(BrowserContextEvent.TargetCreated, target); 304 } 305 }; 306 307 #onDetachedFromTarget = async (target: CdpTarget): Promise<void> => { 308 target._initializedDeferred.resolve(InitializationStatus.ABORTED); 309 target._isClosedDeferred.resolve(); 310 if ( 311 target._isTargetExposed() && 312 (await target._initializedDeferred.valueOrThrow()) === 313 InitializationStatus.SUCCESS 314 ) { 315 this.emit(BrowserEvent.TargetDestroyed, target); 316 target.browserContext().emit(BrowserContextEvent.TargetDestroyed, target); 317 } 318 }; 319 320 #onTargetChanged = ({target}: {target: CdpTarget}): void => { 321 this.emit(BrowserEvent.TargetChanged, target); 322 target.browserContext().emit(BrowserContextEvent.TargetChanged, target); 323 }; 324 325 #onTargetDiscovered = (targetInfo: Protocol.Target.TargetInfo): void => { 326 this.emit(BrowserEvent.TargetDiscovered, targetInfo); 327 }; 328 329 override wsEndpoint(): string { 330 return this.#connection.url(); 331 } 332 333 override async newPage(): Promise<Page> { 334 return await this.#defaultContext.newPage(); 335 } 336 337 async _createPageInContext(contextId?: string): Promise<Page> { 338 const {targetId} = await this.#connection.send('Target.createTarget', { 339 url: 'about:blank', 340 browserContextId: contextId || undefined, 341 }); 342 const target = (await this.waitForTarget(t => { 343 return (t as CdpTarget)._targetId === targetId; 344 })) as CdpTarget; 345 if (!target) { 346 throw new Error(`Missing target for page (id = ${targetId})`); 347 } 348 const initialized = 349 (await target._initializedDeferred.valueOrThrow()) === 350 InitializationStatus.SUCCESS; 351 if (!initialized) { 352 throw new Error(`Failed to create target for page (id = ${targetId})`); 353 } 354 const page = await target.page(); 355 if (!page) { 356 throw new Error( 357 `Failed to create a page for context (id = ${contextId})`, 358 ); 359 } 360 return page; 361 } 362 363 override async installExtension(path: string): Promise<string> { 364 const {id} = await this.#connection.send('Extensions.loadUnpacked', {path}); 365 return id; 366 } 367 368 override uninstallExtension(id: string): Promise<void> { 369 return this.#connection.send('Extensions.uninstall', {id}); 370 } 371 372 override targets(): CdpTarget[] { 373 return Array.from( 374 this.#targetManager.getAvailableTargets().values(), 375 ).filter(target => { 376 return ( 377 target._isTargetExposed() && 378 target._initializedDeferred.value() === InitializationStatus.SUCCESS 379 ); 380 }); 381 } 382 383 override target(): CdpTarget { 384 const browserTarget = this.targets().find(target => { 385 return target.type() === 'browser'; 386 }); 387 if (!browserTarget) { 388 throw new Error('Browser target is not found'); 389 } 390 return browserTarget; 391 } 392 393 override async version(): Promise<string> { 394 const version = await this.#getVersion(); 395 return version.product; 396 } 397 398 override async userAgent(): Promise<string> { 399 const version = await this.#getVersion(); 400 return version.userAgent; 401 } 402 403 override async close(): Promise<void> { 404 await this.#closeCallback.call(null); 405 await this.disconnect(); 406 } 407 408 override disconnect(): Promise<void> { 409 this.#targetManager.dispose(); 410 this.#connection.dispose(); 411 this._detach(); 412 return Promise.resolve(); 413 } 414 415 override get connected(): boolean { 416 return !this.#connection._closed; 417 } 418 419 #getVersion(): Promise<Protocol.Browser.GetVersionResponse> { 420 return this.#connection.send('Browser.getVersion'); 421 } 422 423 override get debugInfo(): DebugInfo { 424 return { 425 pendingProtocolErrors: this.#connection.getPendingProtocolErrors(), 426 }; 427 } 428 }