| 1 | import type { GuestDebugger } from "./guestView.js"; |
| 2 | import { traceBrowserCommand } from "./diagnosticContext.js"; |
| 3 | |
| 4 | export const DEBUGGER_IDLE_MS = 1500; |
| 5 | interface SharedResource { dispose(transportAvailable: boolean): void } |
| 6 | interface Lease { users: number; pending: number; owned: boolean; resources: Map<object, SharedResource>; timer?: ReturnType<typeof setTimeout> } |
| 7 | export type DebuggerSender = (method: string, params?: unknown, sessionId?: string) => Promise<unknown>; |
| 8 | export type DebuggerRelease = (() => void) & { send: DebuggerSender; owned: boolean; shared<T extends SharedResource>(key: object, create: () => T): T }; |
| 9 | const leases = new WeakMap<GuestDebugger, Lease>(); |
| 10 | |
| 11 | function idle(debuggerAPI: GuestDebugger, lease: Lease): void { |
| 12 | if (leases.get(debuggerAPI) !== lease || lease.users || lease.pending) return; |
| 13 | clearTimeout(lease.timer); |
| 14 | // Immediate root detach/re-attach between observe and input can strand |
| 15 | // Chromium's OOPIF isolated-world creation. Keep adjacent calls together. |
| 16 | lease.timer = setTimeout(() => { |
| 17 | if (leases.get(debuggerAPI) === lease && !lease.users && !lease.pending) disposeDebugger(debuggerAPI); |
| 18 | }, DEBUGGER_IDLE_MS); |
| 19 | lease.timer.unref?.(); |
| 20 | } |
| 21 | export function acquireDebugger(debuggerAPI: GuestDebugger): DebuggerRelease { |
| 22 | let lease = leases.get(debuggerAPI); |
| 23 | if (lease && !debuggerAPI.isAttached()) { disposeDebugger(debuggerAPI); lease = undefined; } |
| 24 | if (!lease) { |
| 25 | const owned = !debuggerAPI.isAttached(); |
| 26 | if (owned) debuggerAPI.attach("1.3"); |
| 27 | lease = { users: 0, pending: 0, owned, resources: new Map() }; |
| 28 | leases.set(debuggerAPI, lease); |
| 29 | } |
| 30 | clearTimeout(lease.timer); |
| 31 | lease.users++; |
| 32 | let released = false; |
| 33 | const release = () => { |
| 34 | if (released) return; |
| 35 | released = true; |
| 36 | lease.users--; |
| 37 | idle(debuggerAPI, lease); |
| 38 | }; |
| 39 | return Object.assign(release, { owned: lease.owned, send: (method: string, params?: unknown, sessionId?: string) => sendDebuggerCommand(debuggerAPI, method, params, sessionId, lease), shared: <T extends SharedResource>(key: object, create: () => T): T => { |
| 40 | if (leases.get(debuggerAPI) !== lease) throw new Error("browser debugger lease is unavailable"); |
| 41 | let resource = lease.resources.get(key); |
| 42 | if (!resource) { resource = create(); lease.resources.set(key, resource); } |
| 43 | return resource as T; |
| 44 | } }); |
| 45 | } |
| 46 | |
| 47 | // Count the underlying command, not its deadline-limited waiter. Timing out a |
| 48 | // waiter must not detach a connection with a reply still in flight. |
| 49 | export async function sendDebuggerCommand(debuggerAPI: GuestDebugger, method: string, params?: unknown, sessionId?: string, expected?: Lease): Promise<unknown> { |
| 50 | const lease = leases.get(debuggerAPI); |
| 51 | if (!lease || expected && lease !== expected || !debuggerAPI.isAttached()) throw new Error("browser debugger lease is unavailable"); |
| 52 | clearTimeout(lease.timer); |
| 53 | lease.pending++; |
| 54 | const started = performance.now(); |
| 55 | const target = sessionId ? "child" : "root"; |
| 56 | traceBrowserCommand(method, target, "cdp_started", 0); |
| 57 | try { |
| 58 | const result = await debuggerAPI.sendCommand(method, params, sessionId); |
| 59 | traceBrowserCommand(method, target, "cdp_completed", Math.round(performance.now() - started)); |
| 60 | return result; |
| 61 | } catch (error) { |
| 62 | traceBrowserCommand(method, target, "cdp_failed", Math.round(performance.now() - started)); |
| 63 | throw error; |
| 64 | } |
| 65 | finally { lease.pending--; idle(debuggerAPI, lease); } |
| 66 | } |
| 67 | |
| 68 | // Run before native destruction/crash recovery, while the debugger is still |
| 69 | // accessible. Late completions must not touch a replacement lease. |
| 70 | export function disposeDebugger(debuggerAPI: GuestDebugger, detach = true): void { |
| 71 | const lease = leases.get(debuggerAPI); |
| 72 | if (!lease) return; |
| 73 | leases.delete(debuggerAPI); |
| 74 | clearTimeout(lease.timer); |
| 75 | for (const resource of lease.resources.values()) resource.dispose(detach); |
| 76 | lease.resources.clear(); |
| 77 | if (detach && lease.owned && debuggerAPI.isAttached()) debuggerAPI.detach(); |
| 78 | } |
| 79 |