Path: blob/main/src/vs/platform/browserView/electron-main/browserViewGroup.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 { Disposable, DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js';6import { Emitter, Event } from '../../../base/common/event.js';7import { BrowserView } from './browserView.js';8import { ICDPTarget, CDPBrowserVersion, CDPWindowBounds, CDPTargetInfo, ICDPConnection, ICDPBrowserTarget, CDPRequest, CDPResponse, CDPEvent } from '../common/cdp/types.js';9import { CDPBrowserProxy } from '../common/cdp/proxy.js';10import { IBrowserViewGroup, IBrowserViewGroupViewEvent } from '../common/browserViewGroup.js';11import { IBrowserViewOwner } from '../common/browserView.js';12import { IBrowserViewMainService } from './browserViewMainService.js';13import { IProductService } from '../../product/common/productService.js';14import { BrowserSession } from './browserSession.js';15import { generateUuid } from '../../../base/common/uuid.js';16import { BrowserViewCDPTarget } from './browserViewCDPTarget.js';1718/**19* An isolated group of {@link BrowserView} instances exposed as CDP targets.20*21* Each group represents an independent CDP "browser" endpoint22* (`/devtools/browser/{id}`). Different groups can expose different23* subsets of browser views, enabling selective target visibility across24* CDP sessions.25*26* Created via {@link BrowserViewGroupMainService.createGroup}.27*/28export class BrowserViewGroup extends Disposable implements ICDPBrowserTarget, IBrowserViewGroup {2930private readonly views = new Map<string, BrowserView>();31private readonly viewTargets = this._register(new DisposableMap<string, BrowserViewCDPTarget>());3233/** All context IDs known to this group, including those from views added to it. */34private readonly knownContextIds = new Set<string>();35/** Browser context IDs created by this group via {@link createBrowserContext}. */36private readonly ownedContextIds = new Set<string>();3738private readonly _onDidAddView = this._register(new Emitter<IBrowserViewGroupViewEvent>());39readonly onDidAddView: Event<IBrowserViewGroupViewEvent> = this._onDidAddView.event;4041private readonly _onDidRemoveView = this._register(new Emitter<IBrowserViewGroupViewEvent>());42readonly onDidRemoveView: Event<IBrowserViewGroupViewEvent> = this._onDidRemoveView.event;4344private readonly _onDidDestroy = this._register(new Emitter<void>());45readonly onDidDestroy: Event<void> = this._onDidDestroy.event;4647readonly debugger = this._register(new CDPBrowserProxy(this));4849constructor(50readonly id: string,51readonly owner: IBrowserViewOwner,52@IBrowserViewMainService private readonly browserViewMainService: IBrowserViewMainService,53@IProductService private readonly productService: IProductService54) {55super();56}5758get onCDPMessage(): Event<CDPResponse | CDPEvent> {59return this.debugger.onMessage;60}6162sendCDPMessage(msg: CDPRequest): Promise<void> {63return this.debugger.sendMessage(msg);64}6566// #region View management6768/**69* Add a {@link BrowserView} to this group.70* Fires {@link onDidAddView} and registers the view as a CDP target.71* Also subscribes to the view's sub-target events (iframes, workers)72* and bubbles them as group-level target events.73* Automatically removes the view when it closes.74*/75async addView(viewId: string): Promise<void> {76if (this.views.has(viewId)) {77return;78}79const view = this.browserViewMainService.tryGetBrowserView(viewId);80if (!view) {81throw new Error(`Browser view ${viewId} not found`);82}83this.views.set(view.id, view);84this.knownContextIds.add(view.session.id);85this._onDidAddView.fire({ viewId: view.id });8687// Register the close listener before any async work so we never88// miss a close event that fires during the await.89const closeListener = Event.once(view.onDidClose)(() => {90this.removeView(viewId);91});9293const info = await view.debugger.getTargetInfo();9495if (this.views.get(viewId) !== view) {96// View was removed while we were awaiting target info97closeListener.dispose();98return;99}100101// Create a CDP target wrapping the view's debugger transport102const target = new BrowserViewCDPTarget(view, info);103this.viewTargets.set(view.id, target);104105const store = new DisposableStore();106store.add(closeListener);107target.onClose(() => store.dispose());108109this.debugger.registerTarget(target);110111// Register sub-targets of the view112for (const targetInfo of view.debugger.knownTargets.values()) {113this.debugger.registerTarget(new BrowserViewCDPTarget(view, targetInfo));114}115store.add(view.debugger.onTargetDiscovered(targetInfo => {116this.debugger.registerTarget(new BrowserViewCDPTarget(view, targetInfo));117}));118119// Some sessions won't go through the proxy -- e.g. when auto-attaching to workers.120// So we let the proxy know that the session exists, and it decides whether it cares about it.121store.add(view.debugger.onSessionCreated(({ session, waitingForDebugger }) => {122this.debugger.notifySessionCreated(session, waitingForDebugger);123}));124}125126/**127* Remove a {@link BrowserView} from this group.128* Disposes the associated {@link BrowserViewCDPTarget}, which cascades129* destruction to sub-targets and sessions via {@link ICDPTarget.onClose}.130*/131async removeView(viewId: string): Promise<void> {132const view = this.views.get(viewId);133if (view && this.views.delete(viewId)) {134// If no remaining views belong to the view's context, and we don't own the context, remove it from known contexts135if (!this.ownedContextIds.has(view.session.id) && ![...this.views.values()].some(v => v.session.id === view.session.id)) {136this.knownContextIds.delete(view.session.id);137}138this._onDidRemoveView.fire({ viewId: view.id });139this.viewTargets.deleteAndDispose(viewId);140}141}142143// #endregion144145// #region ICDPBrowserTarget implementation146147private readonly _onTargetInfoChanged = this._register(new Emitter<CDPTargetInfo>());148readonly onTargetInfoChanged = this._onTargetInfoChanged.event;149150getVersion(): CDPBrowserVersion {151return {152protocolVersion: '1.3',153product: `${this.productService.nameShort}/${this.productService.version}`,154revision: this.productService.commit || 'unknown',155userAgent: 'Electron',156jsVersion: process.versions.v8157};158}159160getWindowForTarget(target: ICDPTarget): { windowId: number; bounds: CDPWindowBounds } {161if (!(target instanceof BrowserViewCDPTarget)) {162throw new Error('Can only get window for BrowserView targets');163}164165const view = target.view.getWebContentsView();166const viewBounds = view.getBounds();167return {168windowId: this.owner.mainWindowId,169bounds: {170left: viewBounds.x,171top: viewBounds.y,172width: viewBounds.width,173height: viewBounds.height,174windowState: 'normal'175}176};177}178179async attach(): Promise<ICDPConnection> {180return new CDPBrowserProxy(this);181}182183/** Browser target sessions are managed by the CDPBrowserProxy, not tracked here. */184readonly sessions: ReadonlyMap<string, ICDPConnection> = new Map();185readonly onSessionCreated = Event.None;186readonly onClose: Event<void> = this._onDidDestroy.event;187notifySessionCreated(): void { }188189get targetInfo(): CDPTargetInfo {190return {191targetId: this.id,192type: 'browser',193title: this.getVersion().product,194url: '',195attached: true,196canAccessOpener: false197};198}199200async createTarget(url: string, browserContextId?: string): Promise<ICDPTarget> {201if (browserContextId && !this.knownContextIds.has(browserContextId)) {202throw new Error(`Unknown browser context ${browserContextId}`);203}204205const target = await this.browserViewMainService.createTarget(url, this.owner, browserContextId);206if (target instanceof BrowserView) {207await this.addView(target.id);208return this.viewTargets.get(target.id)!;209}210return target;211}212213async activateTarget(target: ICDPTarget): Promise<void> {214if (!(target instanceof BrowserViewCDPTarget)) {215throw new Error('Can only activate BrowserView targets');216}217// TODO@kycutler218}219220async closeTarget(target: ICDPTarget): Promise<boolean> {221if (!(target instanceof BrowserViewCDPTarget)) {222throw new Error('Can only close BrowserView targets');223}224225await this.removeView(target.view.id);226await this.browserViewMainService.destroyBrowserView(target.view.id);227return true;228}229230// Browser context management231232/**233* Returns only the browser context IDs that are visible to this group,234* i.e. contexts used by views currently in the group.235*/236getBrowserContexts(): string[] {237return [...this.knownContextIds];238}239240async createBrowserContext(): Promise<string> {241const browserSession = BrowserSession.getOrCreateEphemeral(generateUuid(), 'cdp-created');242const contextId = browserSession.id;243this.knownContextIds.add(contextId);244this.ownedContextIds.add(contextId);245return contextId;246}247248async disposeBrowserContext(browserContextId: string): Promise<void> {249if (!this.ownedContextIds.has(browserContextId)) {250throw new Error('Can only dispose browser contexts created by this group');251}252253// Snapshot IDs to avoid mutating the map while iterating254const viewIds = [...this.views.entries()]255.filter(([, view]) => view.session.id === browserContextId)256.map(([id]) => id);257258for (const viewId of viewIds) {259await this.removeView(viewId);260await this.browserViewMainService.destroyBrowserView(viewId);261}262263this.knownContextIds.delete(browserContextId);264this.ownedContextIds.delete(browserContextId);265}266267// #endregion268269override dispose(): void {270this._onDidDestroy.fire();271super.dispose();272}273}274275276