Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/browserView/electron-main/browserViewGroup.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 { Disposable, DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js';
7
import { Emitter, Event } from '../../../base/common/event.js';
8
import { BrowserView } from './browserView.js';
9
import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget, CDPRequest, CDPResponse, CDPEvent } from '../common/cdp/types.js';
10
import { CDPBrowserProxy } from '../common/cdp/proxy.js';
11
import { IBrowserViewGroup, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js';
12
import { IBrowserViewOwner } from '../common/browserView.js';
13
import { IBrowserViewMainService } from './browserViewMainService.js';
14
import { IProductService } from '../../product/common/productService.js';
15
import { BrowserSession } from './browserSession.js';
16
import { generateUuid } from '../../../base/common/uuid.js';
17
import { BrowserViewCDPTarget } from './browserViewCDPTarget.js';
18
19
/**
20
* An isolated group of {@link BrowserView} instances exposed as CDP targets.
21
*
22
* Each group represents an independent CDP "browser" endpoint
23
* (`/devtools/browser/{id}`). Different groups can expose different
24
* subsets of browser views, enabling selective target visibility across
25
* CDP sessions.
26
*
27
* Created via {@link BrowserViewGroupMainService.createGroup}.
28
*/
29
export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, IBrowserViewGroup {
30
31
private readonly views = new Map<string, BrowserView>();
32
private readonly viewTargets = this._register(new DisposableMap<string, BrowserViewCDPTarget>());
33
34
/** All context IDs known to this group, including those from views added to it. */
35
private readonly knownContextIds = new Set<string>();
36
/** Browser context IDs created by this group via {@link createBrowserContext}. */
37
private readonly ownedContextIds = new Set<string>();
38
39
private readonly _onDidAddView = this._register(new Emitter<IBrowserViewGroupViewEvent>());
40
readonly onDidAddView: Event<IBrowserViewGroupViewEvent> = this._onDidAddView.event;
41
42
private readonly _onDidRemoveView = this._register(new Emitter<IBrowserViewGroupViewEvent>());
43
readonly onDidRemoveView: Event<IBrowserViewGroupViewEvent> = this._onDidRemoveView.event;
44
45
private readonly _onDidDestroy = this._register(new Emitter<void>());
46
readonly onDidDestroy: Event<void> = this._onDidDestroy.event;
47
48
readonly debugger = this._register(new CDPBrowserProxy(this));
49
50
constructor(
51
readonly id: string,
52
readonly owner: IBrowserViewOwner,
53
@IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService,
54
@IProductService private readonly productService: IProductService
55
) {
56
super();
57
}
58
59
get onCDPMessage(): Event<CDPResponse | CDPEvent> {
60
return this.debugger.onMessage;
61
}
62
63
sendCDPMessage(msg: CDPRequest): Promise<void> {
64
return this.debugger.sendMessage(msg);
65
}
66
67
// #region View management
68
69
/**
70
* Add a {@link BrowserView} to this group.
71
* Fires {@link onDidAddView} and registers the view as a CDP target.
72
* Also subscribes to the view's sub-target events (iframes, workers)
73
* and bubbles them as group-level target events.
74
* Automatically removes the view when it closes.
75
*/
76
async addView(viewId: string): Promise<void> {
77
if (this.views.has(viewId)) {
78
return;
79
}
80
const view = this.browserViewMainService.tryGetBrowserView(viewId);
81
if (!view) {
82
throw new Error(`Browser view ${viewId} not found`);
83
}
84
this.views.set(view.id, view);
85
this.knownContextIds.add(view.session.id);
86
this._onDidAddView.fire({ viewId: view.id });
87
88
// Register the close listener before any async work so we never
89
// miss a close event that fires during the await.
90
const closeListener = Event.once(view.onDidClose)(() => {
91
this.removeView(viewId);
92
});
93
94
const info = await view.debugger.getTargetInfo();
95
96
if (this.views.get(viewId) !== view) {
97
// View was removed while we were awaiting target info
98
closeListener.dispose();
99
return;
100
}
101
102
// Create a CDP target wrapping the view's debugger transport
103
const target = new BrowserViewCDPTarget(view, info);
104
this.viewTargets.set(view.id, target);
105
106
const store = new DisposableStore();
107
store.add(closeListener);
108
target.onClose(() => store.dispose());
109
110
this.debugger.registerTarget(target);
111
112
// Register sub-targets of the view
113
for (const targetInfo of view.debugger.knownTargets.values()) {
114
this.debugger.registerTarget(new BrowserViewCDPTarget(view, targetInfo));
115
}
116
store.add(view.debugger.onTargetDiscovered(targetInfo => {
117
this.debugger.registerTarget(new BrowserViewCDPTarget(view, targetInfo));
118
}));
119
120
// Some sessions won't go through the proxy -- e.g. when auto-attaching to workers.
121
// So we let the proxy know that the session exists, and it decides whether it cares about it.
122
store.add(view.debugger.onSessionCreated(({ session, waitingForDebugger }) => {
123
this.debugger.notifySessionCreated(session, waitingForDebugger);
124
}));
125
}
126
127
/**
128
* Remove a {@link BrowserView} from this group.
129
* Disposes the associated {@link BrowserViewCDPTarget}, which cascades
130
* destruction to sub-targets and sessions via {@link ICDPTarget.onClose}.
131
*/
132
async removeView(viewId: string): Promise<void> {
133
const view = this.views.get(viewId);
134
if (view && this.views.delete(viewId)) {
135
// If no remaining views belong to the view's context, and we don't own the context, remove it from known contexts
136
if (!this.ownedContextIds.has(view.session.id) && ![...this.views.values()].some(v => v.session.id === view.session.id)) {
137
this.knownContextIds.delete(view.session.id);
138
}
139
this._onDidRemoveView.fire({ viewId: view.id });
140
this.viewTargets.deleteAndDispose(viewId);
141
}
142
}
143
144
// #endregion
145
146
// #region ICDPBrowserTarget implementation
147
148
private readonly _onTargetInfoChanged = this._register(new Emitter<CDPTargetInfo>());
149
readonly onTargetInfoChanged = this._onTargetInfoChanged.event;
150
151
getVersion(): CDPBrowserVersion {
152
return {
153
protocolVersion: '1.3',
154
product: `${this.productService.nameShort}/${this.productService.version}`,
155
revision: this.productService.commit || 'unknown',
156
userAgent: 'Electron',
157
jsVersion: process.versions.v8
158
};
159
}
160
161
getWindowForTarget(target: ICDPTarget): { windowId: number; bounds: CDPWindowBounds } {
162
if (!(target instanceof BrowserViewCDPTarget)) {
163
throw new Error('Can only get window for BrowserView targets');
164
}
165
166
const view = target.view.getWebContentsView();
167
const viewBounds = view.getBounds();
168
return {
169
windowId: this.owner.mainWindowId,
170
bounds: {
171
left: viewBounds.x,
172
top: viewBounds.y,
173
width: viewBounds.width,
174
height: viewBounds.height,
175
windowState: 'normal'
176
}
177
};
178
}
179
180
async attach(): Promise<ICDPConnection> {
181
return new CDPBrowserProxy(this);
182
}
183
184
/** Browser target sessions are managed by the CDPBrowserProxy, not tracked here. */
185
readonly sessions: ReadonlyMap<string, ICDPConnection> = new Map();
186
readonly onSessionCreated = Event.None;
187
readonly onClose: Event<void> = this._onDidDestroy.event;
188
notifySessionCreated(): void { }
189
190
get targetInfo(): CDPTargetInfo {
191
return {
192
targetId: this.id,
193
type: 'browser',
194
title: this.getVersion().product,
195
url: '',
196
attached: true,
197
canAccessOpener: false
198
};
199
}
200
201
async createTarget(url: string, browserContextId?: string): Promise<ICDPTarget> {
202
if (browserContextId && !this.knownContextIds.has(browserContextId)) {
203
throw new Error(`Unknown browser context ${browserContextId}`);
204
}
205
206
const target = await this.browserViewMainService.createTarget(url, this.owner, browserContextId);
207
if (target instanceof BrowserView) {
208
await this.addView(target.id);
209
return this.viewTargets.get(target.id)!;
210
}
211
return target;
212
}
213
214
async activateTarget(target: ICDPTarget): Promise<void> {
215
if (!(target instanceof BrowserViewCDPTarget)) {
216
throw new Error('Can only activate BrowserView targets');
217
}
218
// TODO@kycutler
219
}
220
221
async closeTarget(target: ICDPTarget): Promise<boolean> {
222
if (!(target instanceof BrowserViewCDPTarget)) {
223
throw new Error('Can only close BrowserView targets');
224
}
225
226
await this.removeView(target.view.id);
227
await this.browserViewMainService.destroyBrowserView(target.view.id);
228
return true;
229
}
230
231
// Browser context management
232
233
/**
234
* Returns only the browser context IDs that are visible to this group,
235
* i.e. contexts used by views currently in the group.
236
*/
237
getBrowserContexts(): string[] {
238
return [...this.knownContextIds];
239
}
240
241
async createBrowserContext(): Promise<string> {
242
const browserSession = BrowserSession.getOrCreateEphemeral(generateUuid(), 'cdp-created');
243
const contextId = browserSession.id;
244
this.knownContextIds.add(contextId);
245
this.ownedContextIds.add(contextId);
246
return contextId;
247
}
248
249
async disposeBrowserContext(browserContextId: string): Promise<void> {
250
if (!this.ownedContextIds.has(browserContextId)) {
251
throw new Error('Can only dispose browser contexts created by this group');
252
}
253
254
// Snapshot IDs to avoid mutating the map while iterating
255
const viewIds = [...this.views.entries()]
256
.filter(([, view]) => view.session.id === browserContextId)
257
.map(([id]) => id);
258
259
for (const viewId of viewIds) {
260
await this.removeView(viewId);
261
await this.browserViewMainService.destroyBrowserView(viewId);
262
}
263
264
this.knownContextIds.delete(browserContextId);
265
this.ownedContextIds.delete(browserContextId);
266
}
267
268
// #endregion
269
270
override dispose(): void {
271
this._onDidDestroy.fire();
272
super.dispose();
273
}
274
}
275
276