Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserView/common/cdp/proxy.ts
13401 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 { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';
7
import { Emitter, Event } from '../../../../base/common/event.js';
8
import { generateUuid } from '../../../../base/common/uuid.js';
9
import { ICDPTarget, CDPRequest, CDPResponse, CDPEvent, CDPError, CDPErrorCode, CDPServerError, CDPMethodNotFoundError, CDPInvalidParamsError, ICDPConnection, ICDPBrowserTarget } from './types.js';
10
11
/**
12
* CDP protocol handler for browser-level connections.
13
* Manages Browser.* and Target.* domains, routes page-level commands
14
* to the appropriate attached session by sessionId.
15
*/
16
export class CDPBrowserProxy extends Disposable implements ICDPConnection {
17
readonly sessionId = `browser-session-${generateUuid()}`;
18
get targetId() {
19
return this.browserTarget.targetInfo.targetId;
20
}
21
22
// Browser session state
23
private _isAttachedToBrowserTarget = false;
24
private _autoAttach = false;
25
private _discover = false;
26
27
/**
28
* All sessions known to this proxy, keyed by sessionId.
29
* Includes sessions from explicit attach, proxy auto-attach,
30
* and client auto-attach children.
31
*/
32
private readonly _sessions = this._register(new DisposableMap<string, ICDPConnection>());
33
private readonly _targets = this._register(new DisposableMap<string, ICDPTarget>());
34
35
// Only auto-attach once per target.
36
private readonly _autoAttachments = new WeakSet<ICDPTarget>();
37
38
// CDP method handlers map
39
private readonly _handlers = new Map<string, (params: unknown, sessionId?: string) => Promise<object> | object>([
40
// Browser.* methods (https://chromedevtools.github.io/devtools-protocol/tot/Browser/)
41
['Browser.addPrivacySandboxCoordinatorKeyConfig', () => ({})],
42
['Browser.addPrivacySandboxEnrollmentOverride', () => ({})],
43
['Browser.close', () => ({})],
44
['Browser.getVersion', () => this.browserTarget.getVersion()],
45
['Browser.resetPermissions', () => ({})],
46
['Browser.getWindowForTarget', (p, s) => this.handleBrowserGetWindowForTarget(p as { targetId?: string; sessionId?: string }, s)],
47
['Browser.setDownloadBehavior', () => ({})],
48
['Browser.setWindowBounds', () => ({})],
49
// Target.* methods (https://chromedevtools.github.io/devtools-protocol/tot/Target/)
50
['Target.activateTarget', (p) => this.handleTargetActivateTarget(p as { targetId: string })],
51
['Target.attachToTarget', (p) => this.handleTargetAttachToTarget(p as { targetId: string; flatten?: boolean })],
52
['Target.closeTarget', (p) => this.handleTargetCloseTarget(p as { targetId: string })],
53
['Target.createBrowserContext', () => this.handleTargetCreateBrowserContext()],
54
['Target.createTarget', (p) => this.handleTargetCreateTarget(p as { url?: string; browserContextId?: string })],
55
['Target.detachFromTarget', (p) => this.handleTargetDetachFromTarget(p as { sessionId: string })],
56
['Target.disposeBrowserContext', (p) => this.handleTargetDisposeBrowserContext(p as { browserContextId: string })],
57
['Target.getBrowserContexts', () => this.handleTargetGetBrowserContexts()],
58
['Target.getTargets', () => this.handleTargetGetTargets()],
59
['Target.setAutoAttach', (p, s) => this.handleTargetSetAutoAttach(p as { autoAttach?: boolean; flatten?: boolean }, s)],
60
['Target.setDiscoverTargets', (p) => this.handleTargetSetDiscoverTargets(p as { discover?: boolean })],
61
['Target.attachToBrowserTarget', () => this.handleTargetAttachToBrowserTarget()],
62
['Target.getTargetInfo', (p) => this.handleTargetGetTargetInfo(p as { targetId?: string } | undefined)],
63
]);
64
65
constructor(
66
private readonly browserTarget: ICDPBrowserTarget,
67
) {
68
super();
69
}
70
71
registerTarget(target: ICDPTarget): void {
72
const targetInfo = target.targetInfo;
73
if (this._targets.has(targetInfo.targetId)) {
74
return;
75
}
76
this._targets.set(targetInfo.targetId, target);
77
78
if (this._discover) {
79
this.sendEvent('Target.targetCreated', {
80
targetInfo: target.targetInfo,
81
});
82
}
83
if (this._autoAttach && !this._autoAttachments.has(target)) {
84
this._autoAttachments.add(target);
85
void target.attach();
86
}
87
88
target.onClose(() => {
89
this._targets.deleteAndDispose(targetInfo.targetId);
90
if (this._discover) {
91
this.sendEvent('Target.targetDestroyed', { targetId: targetInfo.targetId });
92
}
93
});
94
95
target.onTargetInfoChanged(info => {
96
if (this._discover) {
97
this.sendEvent('Target.targetInfoChanged', { targetInfo: info });
98
}
99
});
100
101
for (const [, session] of target.sessions) {
102
this.registerSession(session, false);
103
}
104
target.onSessionCreated(({ session, waitingForDebugger }) => {
105
this.registerSession(session, waitingForDebugger);
106
});
107
}
108
109
notifySessionCreated(session: ICDPConnection, waitingForDebugger: boolean): void {
110
if (this._sessions.has(session.sessionId)) {
111
return; // We already know about it.
112
}
113
if (!session.parentSessionId) {
114
return; // Created globally -- we don't care about it.
115
}
116
if (!this._sessions.has(session.parentSessionId)) {
117
return; // Not from one of our sessions -- ignore it.
118
}
119
const target = this._targets.get(session.targetId);
120
if (!target) {
121
return; // Target isn't known -- ignore it.
122
}
123
target.notifySessionCreated(session, waitingForDebugger);
124
}
125
126
private registerSession(session: ICDPConnection, waitingForDebugger: boolean): void {
127
if (this._sessions.has(session.sessionId)) {
128
return;
129
}
130
this._sessions.set(session.sessionId, session);
131
132
const target = this._targets.get(session.targetId);
133
if (!target) {
134
throw new CDPServerError(`Unable to resolve target for session ${session.sessionId}`);
135
}
136
137
this.sendEvent('Target.attachedToTarget', {
138
sessionId: session.sessionId,
139
targetInfo: target.targetInfo,
140
waitingForDebugger
141
}, session.parentSessionId);
142
143
// Forward non-Target events from the session to the external client.
144
// Target domain events are suppressed — the proxy emits its own
145
// lifecycle events (attachedToTarget, detachedFromTarget, etc.)
146
// via registerSession / onClose / sendEvent.
147
session.onEvent(event => {
148
if (event.method.startsWith('Target.')) {
149
return;
150
}
151
this.sendEvent(event.method, event.params, event.sessionId ?? session.sessionId);
152
});
153
154
session.onClose(() => {
155
this._sessions.deleteAndDispose(session.sessionId);
156
157
this.sendEvent('Target.detachedFromTarget', {
158
sessionId: session.sessionId,
159
targetId: session.targetId
160
}, session.parentSessionId);
161
});
162
}
163
164
/** Send a browser-level event to the client */
165
private sendEvent(method: string, params: unknown, sessionId?: string): void {
166
sessionId ||= (this._isAttachedToBrowserTarget ? this.sessionId : undefined);
167
this._onMessage.fire({ method, params, sessionId });
168
this._onEvent.fire({ method, params, sessionId });
169
}
170
171
// #region Public API
172
173
// Events to external clients
174
private readonly _onEvent = this._register(new Emitter<CDPEvent>());
175
readonly onEvent: Event<CDPEvent> = this._onEvent.event;
176
private readonly _onClose = this._register(new Emitter<void>());
177
readonly onClose: Event<void> = this._onClose.event;
178
private readonly _onMessage = this._register(new Emitter<CDPResponse | CDPEvent>());
179
readonly onMessage: Event<CDPResponse | CDPEvent> = this._onMessage.event;
180
181
/**
182
* Send a CDP command and await the result.
183
* Browser-level handlers (Browser.*, Target.*) are checked first.
184
* Other commands are routed to the page session identified by sessionId.
185
*/
186
async sendCommand(method: string, params: unknown = {}, sessionId?: string): Promise<unknown> {
187
try {
188
// Browser-level command handling
189
if (
190
!sessionId ||
191
sessionId === this.sessionId ||
192
method.startsWith('Browser.') ||
193
method.startsWith('Target.')
194
) {
195
const handler = this._handlers.get(method);
196
if (!handler) {
197
throw new CDPMethodNotFoundError(method);
198
}
199
return await handler(params, sessionId);
200
}
201
202
const connection = this._sessions.get(sessionId);
203
if (!connection) {
204
throw new CDPServerError(`Session not found: ${sessionId}`);
205
}
206
207
const result = await connection.sendCommand(method, params);
208
return result ?? {};
209
} catch (error) {
210
if (error instanceof CDPError) {
211
throw error;
212
}
213
throw new CDPServerError(error instanceof Error ? error.message : 'Unknown error');
214
}
215
}
216
217
/**
218
* Accept a CDP request from a message-based transport (WebSocket, IPC, etc.), route it,
219
* and deliver the response or error via {@link onMessage}.
220
*/
221
async sendMessage({ id, method, params, sessionId }: CDPRequest): Promise<void> {
222
return this.sendCommand(method, params, sessionId)
223
.then(result => {
224
this._onMessage.fire({ id, result, sessionId });
225
})
226
.catch((error: Error) => {
227
this._onMessage.fire({
228
id,
229
error: {
230
code: error instanceof CDPError ? error.code : CDPErrorCode.ServerError,
231
message: error.message || 'Unknown error'
232
},
233
sessionId
234
});
235
});
236
}
237
238
// #endregion
239
240
// #region CDP Commands
241
242
private handleBrowserGetWindowForTarget({ targetId }: { targetId?: string }, sessionId?: string) {
243
const resolvedTargetId = (sessionId && this._sessions.get(sessionId)?.targetId) ?? targetId;
244
if (!resolvedTargetId) {
245
throw new CDPServerError('Unable to resolve target');
246
}
247
248
const target = this._targets.get(resolvedTargetId);
249
if (!target) {
250
throw new CDPServerError('Unable to resolve target');
251
}
252
253
return this.browserTarget.getWindowForTarget(target);
254
}
255
256
private handleTargetGetBrowserContexts() {
257
return { browserContextIds: this.browserTarget.getBrowserContexts() };
258
}
259
260
private async handleTargetCreateBrowserContext() {
261
const browserContextId = await this.browserTarget.createBrowserContext();
262
return { browserContextId };
263
}
264
265
private async handleTargetDisposeBrowserContext({ browserContextId }: { browserContextId: string }) {
266
await this.browserTarget.disposeBrowserContext(browserContextId);
267
return {};
268
}
269
270
private handleTargetAttachToBrowserTarget() {
271
this.sendEvent('Target.attachedToTarget', {
272
sessionId: this.sessionId,
273
targetInfo: this.browserTarget.targetInfo,
274
waitingForDebugger: false
275
});
276
this._isAttachedToBrowserTarget = true;
277
return { sessionId: this.sessionId };
278
}
279
280
private handleTargetActivateTarget({ targetId }: { targetId: string }) {
281
const target = this._targets.get(targetId);
282
if (!target) {
283
throw new CDPServerError('Unable to resolve target');
284
}
285
return this.browserTarget.activateTarget(target);
286
}
287
288
private async handleTargetSetAutoAttach(params: { autoAttach?: boolean; flatten?: boolean }, sessionId?: string) {
289
if (sessionId && sessionId !== this.sessionId) {
290
const connection = this._sessions.get(sessionId);
291
if (!connection) {
292
throw new CDPServerError(`Session not found: ${sessionId}`);
293
}
294
return connection.sendCommand('Target.setAutoAttach', params);
295
}
296
297
if (!params.flatten) {
298
throw new CDPInvalidParamsError('This implementation only supports auto-attach with flatten=true');
299
}
300
301
// Proxy-level auto-attach: attach to new targets as they are registered.
302
this._autoAttach = params.autoAttach ?? false;
303
304
return {};
305
}
306
307
private async handleTargetSetDiscoverTargets({ discover = false }: { discover?: boolean }) {
308
if (discover !== this._discover) {
309
this._discover = discover;
310
311
if (this._discover) {
312
// Announce all existing targets
313
for (const target of this._targets.values()) {
314
this.sendEvent('Target.targetCreated', { targetInfo: target.targetInfo });
315
}
316
}
317
}
318
319
return {};
320
}
321
322
private async handleTargetGetTargets() {
323
return { targetInfos: Array.from(this._targets.values()).map(target => target.targetInfo) };
324
}
325
326
private async handleTargetGetTargetInfo({ targetId }: { targetId?: string } = {}) {
327
if (!targetId) {
328
// No targetId specified -- return info about the browser target itself
329
return { targetInfo: this.browserTarget.targetInfo };
330
}
331
332
const target = this._targets.get(targetId);
333
if (!target) {
334
throw new CDPServerError('Unable to resolve target');
335
}
336
return { targetInfo: target.targetInfo };
337
}
338
339
private async handleTargetAttachToTarget({ targetId, flatten }: { targetId: string; flatten?: boolean }) {
340
if (!flatten) {
341
throw new CDPInvalidParamsError('This implementation only supports attachToTarget with flatten=true');
342
}
343
344
const target = this._targets.get(targetId);
345
if (!target) {
346
throw new CDPServerError('Unable to resolve target');
347
}
348
const connection = await target.attach();
349
return { sessionId: connection.sessionId };
350
}
351
352
private async handleTargetDetachFromTarget({ sessionId }: { sessionId: string }) {
353
const connection = this._sessions.get(sessionId);
354
if (!connection) {
355
throw new CDPServerError(`Session not found: ${sessionId}`);
356
}
357
358
connection.dispose();
359
return {};
360
}
361
362
private async handleTargetCreateTarget({ url, browserContextId }: { url?: string; browserContextId?: string }) {
363
const target = await this.browserTarget.createTarget(url || 'about:blank', browserContextId);
364
this.registerTarget(target);
365
366
// Playwright expects the attachment to happen before createTarget returns.
367
if (this._autoAttach && !this._autoAttachments.has(target)) {
368
this._autoAttachments.add(target);
369
await target.attach();
370
}
371
372
return { targetId: target.targetInfo.targetId };
373
}
374
375
private async handleTargetCloseTarget({ targetId }: { targetId: string }) {
376
try {
377
const target = this._targets.get(targetId);
378
if (!target) {
379
throw new CDPServerError('Unable to resolve target');
380
}
381
await this.browserTarget.closeTarget(target);
382
return { success: true };
383
} catch {
384
return { success: false };
385
}
386
}
387
388
// #endregion
389
}
390
391