Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserView/electron-main/browserViewDebugger.ts
13397 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Emitter } from '../../../base/common/event.js';
7
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
8
import { ILogService } from '../../log/common/log.js';
9
import { CDPEvent, CDPTargetInfo, ICDPConnection } from '../common/cdp/types.js';
10
import { BrowserView } from './browserView.js';
11
12
/**
13
* CDP transport for a browser view, backed by the Electron debugger.
14
*
15
* Manages:
16
* - Electron debugger lifecycle (attach/detach)
17
* - Session registry and event routing
18
* - Auto-attach via `Target.setAutoAttach` (flatten=true)
19
*
20
* All CDP sessions on this WebContents (root + child sessions) live in a
21
* single flat registry here. Target-level abstractions (sub-target
22
* discovery, {@link ICDPTarget} contract) are handled by
23
* {@link BrowserViewCDPTarget} which wraps this class.
24
*/
25
export class BrowserViewDebugger extends Disposable {
26
27
private readonly _sessions = this._register(new DisposableMap<string, DebugSession>());
28
private readonly _onSessionCreated = this._register(new Emitter<{ session: ICDPConnection; waitingForDebugger: boolean }>());
29
readonly onSessionCreated = this._onSessionCreated.event;
30
31
/**
32
* Target IDs discovered via `Target.attachedToTarget`. Consumed by
33
* {@link BrowserViewCDPTarget} to create sub-target handles.
34
*/
35
private readonly _knownTargets = new Map<string, CDPTargetInfo>();
36
get knownTargets(): ReadonlyMap<string, CDPTargetInfo> { return this._knownTargets; }
37
38
private readonly _onTargetDiscovered = this._register(new Emitter<CDPTargetInfo>());
39
/** Fired when a new targetId is seen in an attachedToTarget event. */
40
readonly onTargetDiscovered = this._onTargetDiscovered.event;
41
42
private readonly _onTargetDestroyed = this._register(new Emitter<string>());
43
/** Fired when a targetId is removed via a targetDestroyed event. */
44
readonly onTargetDestroyed = this._onTargetDestroyed.event;
45
46
private readonly _onTargetInfoChanged = this._register(new Emitter<CDPTargetInfo>());
47
/** Fired when targetInfo for a known target changes (e.g. title/url update). */
48
readonly onTargetInfoChanged = this._onTargetInfoChanged.event;
49
50
/** Whether any attached debugger session has paused JavaScript execution. */
51
private _isPaused = false;
52
get isPaused(): boolean { return this._isPaused; }
53
54
private readonly _messageHandler: (event: Electron.Event, method: string, params: unknown, sessionId?: string) => void;
55
private readonly _electronDebugger: Electron.Debugger;
56
private _targetId: string | undefined;
57
58
constructor(
59
private readonly view: BrowserView,
60
readonly logService: ILogService
61
) {
62
super();
63
64
this._electronDebugger = view.webContents.debugger;
65
66
this._messageHandler = (_event: Electron.Event, method: string, params: unknown, sessionId?: string) => {
67
this.routeCDPEvent(method, params, sessionId);
68
};
69
}
70
71
/**
72
* Attach to this debugger.
73
* Attach to a target by its targetId, returning the session.
74
* Works for both the root page and sub-targets.
75
*/
76
async attach(): Promise<ICDPConnection> {
77
if (!this._targetId) {
78
const targetInfo = await this.getTargetInfo();
79
this._targetId = targetInfo.targetId;
80
}
81
return this.attachToTarget(this._targetId);
82
}
83
84
async attachToTarget(targetId: string): Promise<ICDPConnection> {
85
this.ensureAttached();
86
const result = await this._electronDebugger.sendCommand('Target.attachToTarget', {
87
targetId,
88
flatten: true
89
}) as { sessionId: string };
90
91
if (!this._sessions.has(result.sessionId)) {
92
throw new Error(`Failed to attach to target ${targetId}`);
93
}
94
95
return this._sessions.get(result.sessionId)!;
96
}
97
98
async getTargetInfo(): Promise<CDPTargetInfo> {
99
this.ensureAttached();
100
const result = await this._electronDebugger.sendCommand('Target.getTargetInfo') as { targetInfo: CDPTargetInfo };
101
return result.targetInfo;
102
}
103
104
/**
105
* Send a CDP command. Handles Electron-specific workarounds in a single place.
106
*/
107
sendCommand(method: string, params?: unknown, sessionId?: string): Promise<unknown> {
108
// This crashes Electron. Don't pass it through.
109
if (method === 'Emulation.setDeviceMetricsOverride') {
110
return Promise.resolve({});
111
}
112
113
this.ensureAttached();
114
const resultPromise = this._electronDebugger.sendCommand(method, params, sessionId);
115
116
// Electron overrides dialog behavior — manually dismiss open dialogs.
117
if (method === 'Page.handleJavaScriptDialog') {
118
this.view.webContents.emit('-cancel-dialogs');
119
}
120
121
return resultPromise;
122
}
123
124
private ensureAttached(): void {
125
if (this._electronDebugger.isAttached()) {
126
return;
127
}
128
129
this._electronDebugger.on('message', this._messageHandler);
130
this._electronDebugger.attach('1.3');
131
132
// We use auto-attach to discover descendent targets.
133
// Regular target discovery doesn't provide ancestor information for workers,
134
// And we have to filter to avoid including targets from other pages or VS Code internals.
135
void this._electronDebugger.sendCommand('Target.setAutoAttach', {
136
autoAttach: true,
137
flatten: true,
138
waitForDebuggerOnStart: false
139
});
140
// We still set discoverTargets so we get target info updates.
141
void this._electronDebugger.sendCommand('Target.setDiscoverTargets', {
142
discover: true
143
});
144
}
145
146
private detachElectronDebugger(): void {
147
try {
148
if (this.view.webContents.isDestroyed() || !this._electronDebugger.isAttached()) {
149
return;
150
}
151
152
this._electronDebugger.removeListener('message', this._messageHandler);
153
this._electronDebugger.detach();
154
} catch {
155
// WebContents may already be destroyed or in an inconsistent state
156
}
157
}
158
159
/**
160
* Route a CDP event from the Electron debugger.
161
*/
162
private routeCDPEvent(method: string, params: unknown, sessionId?: string): void {
163
if (method === 'Target.attachedToTarget') {
164
const p = params as { sessionId: string; targetInfo: CDPTargetInfo; waitingForDebugger: boolean };
165
this.registerSession(p.sessionId, p.targetInfo, p.waitingForDebugger, sessionId);
166
} else if (method === 'Target.detachedFromTarget') {
167
const p = params as { sessionId: string };
168
this._sessions.deleteAndDispose(p.sessionId);
169
} else if (method === 'Target.targetDestroyed') {
170
const p = params as { targetId: string };
171
this.destroyTarget(p.targetId);
172
} else if (method === 'Target.targetInfoChanged' && !sessionId) {
173
const p = params as { targetInfo: CDPTargetInfo };
174
if (this._knownTargets.has(p.targetInfo.targetId)) {
175
this._knownTargets.set(p.targetInfo.targetId, p.targetInfo);
176
this._onTargetInfoChanged.fire(p.targetInfo);
177
}
178
} else if (method === 'Debugger.paused') {
179
this._isPaused = true;
180
} else if (method === 'Debugger.resumed') {
181
this._isPaused = false;
182
}
183
184
const session = sessionId ? this._sessions.get(sessionId) : undefined;
185
if (session) {
186
session.emitEvent({ method, params, sessionId });
187
}
188
}
189
190
/**
191
* A target was destroyed by the Electron debugger.
192
* Dispose all sessions belonging to that target before firing the
193
* lifecycle event so that listeners never observe stale sessions.
194
*/
195
private destroyTarget(targetId: string): void {
196
const toDispose: string[] = [];
197
for (const [sessionId, session] of this._sessions) {
198
if (session.targetId === targetId) {
199
toDispose.push(sessionId);
200
}
201
}
202
for (const sessionId of toDispose) {
203
this._sessions.deleteAndDispose(sessionId);
204
}
205
206
if (this._knownTargets.delete(targetId)) {
207
this._onTargetDestroyed.fire(targetId);
208
}
209
}
210
211
private registerSession(sessionId: string, targetInfo: CDPTargetInfo, waitingForDebugger: boolean, parentSessionId: string | undefined): DebugSession {
212
if (!this._knownTargets.has(targetInfo.targetId) && targetInfo.targetId !== this._targetId) {
213
this._knownTargets.set(targetInfo.targetId, targetInfo);
214
this._onTargetDiscovered.fire(targetInfo);
215
}
216
217
if (this._sessions.has(sessionId)) {
218
return this._sessions.get(sessionId)!;
219
}
220
221
const session = new DebugSession(parentSessionId, sessionId, targetInfo.targetId, this);
222
this._sessions.set(sessionId, session);
223
session.onClose(() => this._sessions.deleteAndDispose(sessionId));
224
225
this._onSessionCreated.fire({ session, waitingForDebugger });
226
227
return session;
228
}
229
230
override dispose(): void {
231
this.detachElectronDebugger();
232
super.dispose();
233
}
234
}
235
236
/**
237
* A CDP session backed by the Electron debugger.
238
*
239
* Pure plumbing — holds a sessionId, emits events, and delegates
240
* commands to the root {@link BrowserViewDebugger}.
241
*/
242
class DebugSession extends Disposable implements ICDPConnection {
243
private readonly _onEvent = this._register(new Emitter<CDPEvent>());
244
readonly onEvent = this._onEvent.event;
245
readonly emitEvent = (event: CDPEvent) => this._onEvent.fire(event);
246
247
private readonly _onClose = this._register(new Emitter<void>());
248
readonly onClose = this._onClose.event;
249
250
private _isDisposed = false;
251
252
constructor(
253
public readonly parentSessionId: string | undefined,
254
public readonly sessionId: string,
255
public readonly targetId: string,
256
private readonly _debugger: BrowserViewDebugger,
257
) {
258
super();
259
}
260
261
async sendCommand(method: string, params?: unknown): Promise<unknown> {
262
return this._debugger.sendCommand(method, params, this.sessionId);
263
}
264
265
override dispose(): void {
266
if (this._isDisposed) {
267
return;
268
}
269
this._isDisposed = true;
270
271
this._onClose.fire();
272
super.dispose();
273
}
274
}
275
276