Path: blob/main/src/vs/platform/browserView/common/cdp/proxy.ts
13401 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 } from '../../../../base/common/lifecycle.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { generateUuid } from '../../../../base/common/uuid.js';8import { ICDPTarget, CDPRequest, CDPResponse, CDPEvent, CDPError, CDPErrorCode, CDPServerError, CDPMethodNotFoundError, CDPInvalidParamsError, ICDPConnection, ICDPBrowserTarget } from './types.js';910/**11* CDP protocol handler for browser-level connections.12* Manages Browser.* and Target.* domains, routes page-level commands13* to the appropriate attached session by sessionId.14*/15export class CDPBrowserProxy extends Disposable implements ICDPConnection {16readonly sessionId = `browser-session-${generateUuid()}`;17get targetId() {18return this.browserTarget.targetInfo.targetId;19}2021// Browser session state22private _isAttachedToBrowserTarget = false;23private _autoAttach = false;24private _discover = false;2526/**27* All sessions known to this proxy, keyed by sessionId.28* Includes sessions from explicit attach, proxy auto-attach,29* and client auto-attach children.30*/31private readonly _sessions = this._register(new DisposableMap<string, ICDPConnection>());32private readonly _targets = this._register(new DisposableMap<string, ICDPTarget>());3334// Only auto-attach once per target.35private readonly _autoAttachments = new WeakSet<ICDPTarget>();3637// CDP method handlers map38private readonly _handlers = new Map<string, (params: unknown, sessionId?: string) => Promise<object> | object>([39// Browser.* methods (https://chromedevtools.github.io/devtools-protocol/tot/Browser/)40['Browser.addPrivacySandboxCoordinatorKeyConfig', () => ({})],41['Browser.addPrivacySandboxEnrollmentOverride', () => ({})],42['Browser.close', () => ({})],43['Browser.getVersion', () => this.browserTarget.getVersion()],44['Browser.resetPermissions', () => ({})],45['Browser.getWindowForTarget', (p, s) => this.handleBrowserGetWindowForTarget(p as { targetId?: string; sessionId?: string }, s)],46['Browser.setDownloadBehavior', () => ({})],47['Browser.setWindowBounds', () => ({})],48// Target.* methods (https://chromedevtools.github.io/devtools-protocol/tot/Target/)49['Target.activateTarget', (p) => this.handleTargetActivateTarget(p as { targetId: string })],50['Target.attachToTarget', (p) => this.handleTargetAttachToTarget(p as { targetId: string; flatten?: boolean })],51['Target.closeTarget', (p) => this.handleTargetCloseTarget(p as { targetId: string })],52['Target.createBrowserContext', () => this.handleTargetCreateBrowserContext()],53['Target.createTarget', (p) => this.handleTargetCreateTarget(p as { url?: string; browserContextId?: string })],54['Target.detachFromTarget', (p) => this.handleTargetDetachFromTarget(p as { sessionId: string })],55['Target.disposeBrowserContext', (p) => this.handleTargetDisposeBrowserContext(p as { browserContextId: string })],56['Target.getBrowserContexts', () => this.handleTargetGetBrowserContexts()],57['Target.getTargets', () => this.handleTargetGetTargets()],58['Target.setAutoAttach', (p, s) => this.handleTargetSetAutoAttach(p as { autoAttach?: boolean; flatten?: boolean }, s)],59['Target.setDiscoverTargets', (p) => this.handleTargetSetDiscoverTargets(p as { discover?: boolean })],60['Target.attachToBrowserTarget', () => this.handleTargetAttachToBrowserTarget()],61['Target.getTargetInfo', (p) => this.handleTargetGetTargetInfo(p as { targetId?: string } | undefined)],62]);6364constructor(65private readonly browserTarget: ICDPBrowserTarget,66) {67super();68}6970registerTarget(target: ICDPTarget): void {71const targetInfo = target.targetInfo;72if (this._targets.has(targetInfo.targetId)) {73return;74}75this._targets.set(targetInfo.targetId, target);7677if (this._discover) {78this.sendEvent('Target.targetCreated', {79targetInfo: target.targetInfo,80});81}82if (this._autoAttach && !this._autoAttachments.has(target)) {83this._autoAttachments.add(target);84void target.attach();85}8687target.onClose(() => {88this._targets.deleteAndDispose(targetInfo.targetId);89if (this._discover) {90this.sendEvent('Target.targetDestroyed', { targetId: targetInfo.targetId });91}92});9394target.onTargetInfoChanged(info => {95if (this._discover) {96this.sendEvent('Target.targetInfoChanged', { targetInfo: info });97}98});99100for (const [, session] of target.sessions) {101this.registerSession(session, false);102}103target.onSessionCreated(({ session, waitingForDebugger }) => {104this.registerSession(session, waitingForDebugger);105});106}107108notifySessionCreated(session: ICDPConnection, waitingForDebugger: boolean): void {109if (this._sessions.has(session.sessionId)) {110return; // We already know about it.111}112if (!session.parentSessionId) {113return; // Created globally -- we don't care about it.114}115if (!this._sessions.has(session.parentSessionId)) {116return; // Not from one of our sessions -- ignore it.117}118const target = this._targets.get(session.targetId);119if (!target) {120return; // Target isn't known -- ignore it.121}122target.notifySessionCreated(session, waitingForDebugger);123}124125private registerSession(session: ICDPConnection, waitingForDebugger: boolean): void {126if (this._sessions.has(session.sessionId)) {127return;128}129this._sessions.set(session.sessionId, session);130131const target = this._targets.get(session.targetId);132if (!target) {133throw new CDPServerError(`Unable to resolve target for session ${session.sessionId}`);134}135136this.sendEvent('Target.attachedToTarget', {137sessionId: session.sessionId,138targetInfo: target.targetInfo,139waitingForDebugger140}, session.parentSessionId);141142// Forward non-Target events from the session to the external client.143// Target domain events are suppressed — the proxy emits its own144// lifecycle events (attachedToTarget, detachedFromTarget, etc.)145// via registerSession / onClose / sendEvent.146session.onEvent(event => {147if (event.method.startsWith('Target.')) {148return;149}150this.sendEvent(event.method, event.params, event.sessionId ?? session.sessionId);151});152153session.onClose(() => {154this._sessions.deleteAndDispose(session.sessionId);155156this.sendEvent('Target.detachedFromTarget', {157sessionId: session.sessionId,158targetId: session.targetId159}, session.parentSessionId);160});161}162163/** Send a browser-level event to the client */164private sendEvent(method: string, params: unknown, sessionId?: string): void {165sessionId ||= (this._isAttachedToBrowserTarget ? this.sessionId : undefined);166this._onMessage.fire({ method, params, sessionId });167this._onEvent.fire({ method, params, sessionId });168}169170// #region Public API171172// Events to external clients173private readonly _onEvent = this._register(new Emitter<CDPEvent>());174readonly onEvent: Event<CDPEvent> = this._onEvent.event;175private readonly _onClose = this._register(new Emitter<void>());176readonly onClose: Event<void> = this._onClose.event;177private readonly _onMessage = this._register(new Emitter<CDPResponse | CDPEvent>());178readonly onMessage: Event<CDPResponse | CDPEvent> = this._onMessage.event;179180/**181* Send a CDP command and await the result.182* Browser-level handlers (Browser.*, Target.*) are checked first.183* Other commands are routed to the page session identified by sessionId.184*/185async sendCommand(method: string, params: unknown = {}, sessionId?: string): Promise<unknown> {186try {187// Browser-level command handling188if (189!sessionId ||190sessionId === this.sessionId ||191method.startsWith('Browser.') ||192method.startsWith('Target.')193) {194const handler = this._handlers.get(method);195if (!handler) {196throw new CDPMethodNotFoundError(method);197}198return await handler(params, sessionId);199}200201const connection = this._sessions.get(sessionId);202if (!connection) {203throw new CDPServerError(`Session not found: ${sessionId}`);204}205206const result = await connection.sendCommand(method, params);207return result ?? {};208} catch (error) {209if (error instanceof CDPError) {210throw error;211}212throw new CDPServerError(error instanceof Error ? error.message : 'Unknown error');213}214}215216/**217* Accept a CDP request from a message-based transport (WebSocket, IPC, etc.), route it,218* and deliver the response or error via {@link onMessage}.219*/220async sendMessage({ id, method, params, sessionId }: CDPRequest): Promise<void> {221return this.sendCommand(method, params, sessionId)222.then(result => {223this._onMessage.fire({ id, result, sessionId });224})225.catch((error: Error) => {226this._onMessage.fire({227id,228error: {229code: error instanceof CDPError ? error.code : CDPErrorCode.ServerError,230message: error.message || 'Unknown error'231},232sessionId233});234});235}236237// #endregion238239// #region CDP Commands240241private handleBrowserGetWindowForTarget({ targetId }: { targetId?: string }, sessionId?: string) {242const resolvedTargetId = (sessionId && this._sessions.get(sessionId)?.targetId) ?? targetId;243if (!resolvedTargetId) {244throw new CDPServerError('Unable to resolve target');245}246247const target = this._targets.get(resolvedTargetId);248if (!target) {249throw new CDPServerError('Unable to resolve target');250}251252return this.browserTarget.getWindowForTarget(target);253}254255private handleTargetGetBrowserContexts() {256return { browserContextIds: this.browserTarget.getBrowserContexts() };257}258259private async handleTargetCreateBrowserContext() {260const browserContextId = await this.browserTarget.createBrowserContext();261return { browserContextId };262}263264private async handleTargetDisposeBrowserContext({ browserContextId }: { browserContextId: string }) {265await this.browserTarget.disposeBrowserContext(browserContextId);266return {};267}268269private handleTargetAttachToBrowserTarget() {270this.sendEvent('Target.attachedToTarget', {271sessionId: this.sessionId,272targetInfo: this.browserTarget.targetInfo,273waitingForDebugger: false274});275this._isAttachedToBrowserTarget = true;276return { sessionId: this.sessionId };277}278279private handleTargetActivateTarget({ targetId }: { targetId: string }) {280const target = this._targets.get(targetId);281if (!target) {282throw new CDPServerError('Unable to resolve target');283}284return this.browserTarget.activateTarget(target);285}286287private async handleTargetSetAutoAttach(params: { autoAttach?: boolean; flatten?: boolean }, sessionId?: string) {288if (sessionId && sessionId !== this.sessionId) {289const connection = this._sessions.get(sessionId);290if (!connection) {291throw new CDPServerError(`Session not found: ${sessionId}`);292}293return connection.sendCommand('Target.setAutoAttach', params);294}295296if (!params.flatten) {297throw new CDPInvalidParamsError('This implementation only supports auto-attach with flatten=true');298}299300// Proxy-level auto-attach: attach to new targets as they are registered.301this._autoAttach = params.autoAttach ?? false;302303return {};304}305306private async handleTargetSetDiscoverTargets({ discover = false }: { discover?: boolean }) {307if (discover !== this._discover) {308this._discover = discover;309310if (this._discover) {311// Announce all existing targets312for (const target of this._targets.values()) {313this.sendEvent('Target.targetCreated', { targetInfo: target.targetInfo });314}315}316}317318return {};319}320321private async handleTargetGetTargets() {322return { targetInfos: Array.from(this._targets.values()).map(target => target.targetInfo) };323}324325private async handleTargetGetTargetInfo({ targetId }: { targetId?: string } = {}) {326if (!targetId) {327// No targetId specified -- return info about the browser target itself328return { targetInfo: this.browserTarget.targetInfo };329}330331const target = this._targets.get(targetId);332if (!target) {333throw new CDPServerError('Unable to resolve target');334}335return { targetInfo: target.targetInfo };336}337338private async handleTargetAttachToTarget({ targetId, flatten }: { targetId: string; flatten?: boolean }) {339if (!flatten) {340throw new CDPInvalidParamsError('This implementation only supports attachToTarget with flatten=true');341}342343const target = this._targets.get(targetId);344if (!target) {345throw new CDPServerError('Unable to resolve target');346}347const connection = await target.attach();348return { sessionId: connection.sessionId };349}350351private async handleTargetDetachFromTarget({ sessionId }: { sessionId: string }) {352const connection = this._sessions.get(sessionId);353if (!connection) {354throw new CDPServerError(`Session not found: ${sessionId}`);355}356357connection.dispose();358return {};359}360361private async handleTargetCreateTarget({ url, browserContextId }: { url?: string; browserContextId?: string }) {362const target = await this.browserTarget.createTarget(url || 'about:blank', browserContextId);363this.registerTarget(target);364365// Playwright expects the attachment to happen before createTarget returns.366if (this._autoAttach && !this._autoAttachments.has(target)) {367this._autoAttachments.add(target);368await target.attach();369}370371return { targetId: target.targetInfo.targetId };372}373374private async handleTargetCloseTarget({ targetId }: { targetId: string }) {375try {376const target = this._targets.get(targetId);377if (!target) {378throw new CDPServerError('Unable to resolve target');379}380await this.browserTarget.closeTarget(target);381return { success: true };382} catch {383return { success: false };384}385}386387// #endregion388}389390391