Files
orca/src/main/browser/cdp-debugger-lifecycle.ts
T
Neil f3334c1bff Split CDP bridge responsibilities (#17215)
* Split speech session lifecycle

* Split terminal output scheduler pipeline

* Split mobile browser pane modules

* Prune resolved max-lines suppressions

* Split pane tree equalization logic

* Extract mobile troubleshoot screen styles

* Split external automation manager

* Split main window service attachments

* Split hosted review creation checks

* Split automation dispatch event handling

* Split settings navigation metadata

* Split daemon initialization lifecycle

* Split GitLab item dialog

* Split relay dispatcher layers

* Split mobile host screen

* Retarget mobile view settings source test

* Split runtime file client layers

* Split ports panel layers

* Split runtime environments pane layers

* Split local PTY provider responsibilities

* Split CDP bridge responsibilities

* Fix F3-speech for #17123

* Fix F1-cycle for #17131

* Fix F4-navtest for #17157

* Fix F2-allowlist for #17161
2026-08-29 20:18:48 -07:00

108 lines
3.7 KiB
TypeScript

import type { WebContents } from 'electron'
import { ANTI_DETECTION_SCRIPT } from './anti-detection'
import { BrowserError } from './browser-error'
import type { CdpTabState } from './cdp-auxiliary-commands'
import type { CdpCommandSender } from './snapshot-engine'
import type { CdpBridgeState } from './cdp-bridge-state'
import { createCdpDebuggerMessageListener } from './cdp-debugger-events'
export class CdpDebuggerLifecycle {
constructor(private readonly bridgeState: CdpBridgeState) {}
removeDebuggerListeners(guest: WebContents, state: CdpTabState): void {
const detachListener = state.debuggerDetachListener
const messageListener = state.debuggerMessageListener
state.debuggerDetachListener = null
state.debuggerMessageListener = null
if (detachListener) {
try {
guest.debugger.removeListener('detach', detachListener as never)
} catch {
// guest may already be destroyed
}
}
if (messageListener) {
try {
guest.debugger.removeListener('message', messageListener as never)
} catch {
// guest may already be destroyed
}
}
}
async ensureDebuggerAttached(guest: WebContents): Promise<void> {
const tabId = this.bridgeState.resolveTabId(guest.id)
const state = this.bridgeState.getOrCreateTabState(tabId)
if (state.debuggerAttached && guest.debugger.isAttached()) {
return
}
try {
// Why: BrowserManager already attached the debugger; reuse it to avoid "another debugger is already attached."
if (!guest.debugger.isAttached()) {
guest.debugger.attach('1.3')
}
} catch {
throw new BrowserError(
'browser_cdp_error',
'Could not attach debugger. DevTools may already be open for this tab.'
)
}
const sender = this.makeCdpSender(guest)
await sender('Page.enable')
await sender('DOM.enable')
await sender('Network.enable')
// Why: OOPIF iframes are invisible to the parent CDP session; flatten:true gives each a targetable sessionId.
await sender('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: false,
flatten: true
})
// Why: CDP attach exposes automation signals (navigator.webdriver) that Cloudflare checks; override per new document.
await sender('Page.addScriptToEvaluateOnNewDocument', {
source: ANTI_DETECTION_SCRIPT
})
// Why: only remove this bridge's listeners; screencast/proxy sessions share the debugger and own their teardown.
this.removeDebuggerListeners(guest, state)
const detachListener = (): void => {
state.debuggerAttached = false
state.snapshotResult = null
state.iframeSessions.clear()
this.removeDebuggerListeners(guest, state)
}
const messageListener = createCdpDebuggerMessageListener(guest, state)
state.debuggerDetachListener = detachListener
state.debuggerMessageListener = messageListener
guest.debugger.on('detach', detachListener)
guest.debugger.on('message', messageListener)
state.debuggerAttached = true
}
makeCdpSender(guest: WebContents, sessionId?: string): CdpCommandSender {
return (method: string, params?: Record<string, unknown>) => {
const command = guest.debugger.sendCommand(method, params, sessionId) as Promise<unknown>
// Why: Electron's CDP sendCommand can hang on a stale debugger session, so a 10s timeout bounds the RPC.
let timer: ReturnType<typeof setTimeout>
return Promise.race([
command.finally(() => clearTimeout(timer)),
new Promise<never>((_, reject) => {
timer = setTimeout(
() =>
reject(new BrowserError('browser_cdp_error', `CDP command "${method}" timed out`)),
10_000
)
})
])
}
}
}