Path: blob/main/src/vs/platform/browserView/electron-main/browserViewDebugger.ts
13397 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Emitter } from '../../../base/common/event.js';6import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';7import { ILogService } from '../../log/common/log.js';8import { CDPEvent, CDPTargetInfo, ICDPConnection } from '../common/cdp/types.js';9import { BrowserView } from './browserView.js';1011/**12* CDP transport for a browser view, backed by the Electron debugger.13*14* Manages:15* - Electron debugger lifecycle (attach/detach)16* - Session registry and event routing17* - Auto-attach via `Target.setAutoAttach` (flatten=true)18*19* All CDP sessions on this WebContents (root + child sessions) live in a20* single flat registry here. Target-level abstractions (sub-target21* discovery, {@link ICDPTarget} contract) are handled by22* {@link BrowserViewCDPTarget} which wraps this class.23*/24export class BrowserViewDebugger extends Disposable {2526private readonly _sessions = this._register(new DisposableMap<string, DebugSession>());27private readonly _onSessionCreated = this._register(new Emitter<{ session: ICDPConnection; waitingForDebugger: boolean }>());28readonly onSessionCreated = this._onSessionCreated.event;2930/**31* Target IDs discovered via `Target.attachedToTarget`. Consumed by32* {@link BrowserViewCDPTarget} to create sub-target handles.33*/34private readonly _knownTargets = new Map<string, CDPTargetInfo>();35get knownTargets(): ReadonlyMap<string, CDPTargetInfo> { return this._knownTargets; }3637private readonly _onTargetDiscovered = this._register(new Emitter<CDPTargetInfo>());38/** Fired when a new targetId is seen in an attachedToTarget event. */39readonly onTargetDiscovered = this._onTargetDiscovered.event;4041private readonly _onTargetDestroyed = this._register(new Emitter<string>());42/** Fired when a targetId is removed via a targetDestroyed event. */43readonly onTargetDestroyed = this._onTargetDestroyed.event;4445private readonly _onTargetInfoChanged = this._register(new Emitter<CDPTargetInfo>());46/** Fired when targetInfo for a known target changes (e.g. title/url update). */47readonly onTargetInfoChanged = this._onTargetInfoChanged.event;4849/** Whether any attached debugger session has paused JavaScript execution. */50private _isPaused = false;51get isPaused(): boolean { return this._isPaused; }5253private readonly _messageHandler: (event: Electron.Event, method: string, params: unknown, sessionId?: string) => void;54private readonly _electronDebugger: Electron.Debugger;55private _targetId: string | undefined;5657constructor(58private readonly view: BrowserView,59readonly logService: ILogService60) {61super();6263this._electronDebugger = view.webContents.debugger;6465this._messageHandler = (_event: Electron.Event, method: string, params: unknown, sessionId?: string) => {66this.routeCDPEvent(method, params, sessionId);67};68}6970/**71* Attach to this debugger.72* Attach to a target by its targetId, returning the session.73* Works for both the root page and sub-targets.74*/75async attach(): Promise<ICDPConnection> {76if (!this._targetId) {77const targetInfo = await this.getTargetInfo();78this._targetId = targetInfo.targetId;79}80return this.attachToTarget(this._targetId);81}8283async attachToTarget(targetId: string): Promise<ICDPConnection> {84this.ensureAttached();85const result = await this._electronDebugger.sendCommand('Target.attachToTarget', {86targetId,87flatten: true88}) as { sessionId: string };8990if (!this._sessions.has(result.sessionId)) {91throw new Error(`Failed to attach to target ${targetId}`);92}9394return this._sessions.get(result.sessionId)!;95}9697async getTargetInfo(): Promise<CDPTargetInfo> {98this.ensureAttached();99const result = await this._electronDebugger.sendCommand('Target.getTargetInfo') as { targetInfo: CDPTargetInfo };100return result.targetInfo;101}102103/**104* Send a CDP command. Handles Electron-specific workarounds in a single place.105*/106sendCommand(method: string, params?: unknown, sessionId?: string): Promise<unknown> {107// This crashes Electron. Don't pass it through.108if (method === 'Emulation.setDeviceMetricsOverride') {109return Promise.resolve({});110}111112this.ensureAttached();113const resultPromise = this._electronDebugger.sendCommand(method, params, sessionId);114115// Electron overrides dialog behavior — manually dismiss open dialogs.116if (method === 'Page.handleJavaScriptDialog') {117this.view.webContents.emit('-cancel-dialogs');118}119120return resultPromise;121}122123private ensureAttached(): void {124if (this._electronDebugger.isAttached()) {125return;126}127128this._electronDebugger.on('message', this._messageHandler);129this._electronDebugger.attach('1.3');130131// We use auto-attach to discover descendent targets.132// Regular target discovery doesn't provide ancestor information for workers,133// And we have to filter to avoid including targets from other pages or VS Code internals.134void this._electronDebugger.sendCommand('Target.setAutoAttach', {135autoAttach: true,136flatten: true,137waitForDebuggerOnStart: false138});139// We still set discoverTargets so we get target info updates.140void this._electronDebugger.sendCommand('Target.setDiscoverTargets', {141discover: true142});143}144145private detachElectronDebugger(): void {146try {147if (this.view.webContents.isDestroyed() || !this._electronDebugger.isAttached()) {148return;149}150151this._electronDebugger.removeListener('message', this._messageHandler);152this._electronDebugger.detach();153} catch {154// WebContents may already be destroyed or in an inconsistent state155}156}157158/**159* Route a CDP event from the Electron debugger.160*/161private routeCDPEvent(method: string, params: unknown, sessionId?: string): void {162if (method === 'Target.attachedToTarget') {163const p = params as { sessionId: string; targetInfo: CDPTargetInfo; waitingForDebugger: boolean };164this.registerSession(p.sessionId, p.targetInfo, p.waitingForDebugger, sessionId);165} else if (method === 'Target.detachedFromTarget') {166const p = params as { sessionId: string };167this._sessions.deleteAndDispose(p.sessionId);168} else if (method === 'Target.targetDestroyed') {169const p = params as { targetId: string };170this.destroyTarget(p.targetId);171} else if (method === 'Target.targetInfoChanged' && !sessionId) {172const p = params as { targetInfo: CDPTargetInfo };173if (this._knownTargets.has(p.targetInfo.targetId)) {174this._knownTargets.set(p.targetInfo.targetId, p.targetInfo);175this._onTargetInfoChanged.fire(p.targetInfo);176}177} else if (method === 'Debugger.paused') {178this._isPaused = true;179} else if (method === 'Debugger.resumed') {180this._isPaused = false;181}182183const session = sessionId ? this._sessions.get(sessionId) : undefined;184if (session) {185session.emitEvent({ method, params, sessionId });186}187}188189/**190* A target was destroyed by the Electron debugger.191* Dispose all sessions belonging to that target before firing the192* lifecycle event so that listeners never observe stale sessions.193*/194private destroyTarget(targetId: string): void {195const toDispose: string[] = [];196for (const [sessionId, session] of this._sessions) {197if (session.targetId === targetId) {198toDispose.push(sessionId);199}200}201for (const sessionId of toDispose) {202this._sessions.deleteAndDispose(sessionId);203}204205if (this._knownTargets.delete(targetId)) {206this._onTargetDestroyed.fire(targetId);207}208}209210private registerSession(sessionId: string, targetInfo: CDPTargetInfo, waitingForDebugger: boolean, parentSessionId: string | undefined): DebugSession {211if (!this._knownTargets.has(targetInfo.targetId) && targetInfo.targetId !== this._targetId) {212this._knownTargets.set(targetInfo.targetId, targetInfo);213this._onTargetDiscovered.fire(targetInfo);214}215216if (this._sessions.has(sessionId)) {217return this._sessions.get(sessionId)!;218}219220const session = new DebugSession(parentSessionId, sessionId, targetInfo.targetId, this);221this._sessions.set(sessionId, session);222session.onClose(() => this._sessions.deleteAndDispose(sessionId));223224this._onSessionCreated.fire({ session, waitingForDebugger });225226return session;227}228229override dispose(): void {230this.detachElectronDebugger();231super.dispose();232}233}234235/**236* A CDP session backed by the Electron debugger.237*238* Pure plumbing — holds a sessionId, emits events, and delegates239* commands to the root {@link BrowserViewDebugger}.240*/241class DebugSession extends Disposable implements ICDPConnection {242private readonly _onEvent = this._register(new Emitter<CDPEvent>());243readonly onEvent = this._onEvent.event;244readonly emitEvent = (event: CDPEvent) => this._onEvent.fire(event);245246private readonly _onClose = this._register(new Emitter<void>());247readonly onClose = this._onClose.event;248249private _isDisposed = false;250251constructor(252public readonly parentSessionId: string | undefined,253public readonly sessionId: string,254public readonly targetId: string,255private readonly _debugger: BrowserViewDebugger,256) {257super();258}259260async sendCommand(method: string, params?: unknown): Promise<unknown> {261return this._debugger.sendCommand(method, params, this.sessionId);262}263264override dispose(): void {265if (this._isDisposed) {266return;267}268this._isDisposed = true;269270this._onClose.fire();271super.dispose();272}273}274275276