src/vs/server/node/agentHostChannel.ts
304 LOC · 195 covered · 109 uncovered · 40 ranges · 5 concepts · 5 introducers · 3 tests
File neighbourhood
The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.
Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file
In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.
Graph controls are ready.
Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.
/*---------------------------------------------------------------------------------------------
agentHostChannel.ts ×20
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// Server-side IPC channel that proxies the agent host protocol from a
// renderer to the agent host process running on the server. For each
// renderer client (identified by IPC context) the channel opens its own
// AHP-over-WebSocket connection upstream and pipes raw JSON frames.
//
// The renderer-side counterpart is `AgentHostIpcChannelTransport` in
// `src/vs/platform/agentHost/browser/`. Together they reuse the existing
// `RemoteAgentHostProtocolClient` over IPC instead of a raw WebSocket.
import { Emitter, Event } from '../../base/common/event.js';
import { Disposable, IDisposable } from '../../base/common/lifecycle.js';
import { connectionTokenQueryName } from '../../base/common/network.js';
import { IPCServer, IServerChannel } from '../../base/parts/ipc/common/ipc.js';
import { ILogService } from '../../platform/log/common/log.js';
import type * as wsTypes from 'ws';
import type * as netTypes from 'net';
const agentHostProxyUnavailableMessage = 'Agent host proxy is not available because no upstream agent host endpoint was configured.';
/**
* Endpoint description for the upstream agent host. One of `port` or
* `socketPath` must be set, matching how `setWebSocketConfig` is called
* for `NodeAgentHostStarter`.
*/
export interface IAgentHostUpstreamEndpoint {
readonly host?: string;
readonly port?: string;
readonly socketPath?: string;
readonly connectionToken?: string;
}
/**
* Lazy-loaded `ws` module. Imported once on first connection so renderers
* that never touch the agent host don't pay the cost.
*/
let _wsModule: typeof wsTypes | undefined;
async function loadWs(): Promise<typeof wsTypes> {
return _wsModule ??= await import('ws');
}
let _netModule: typeof netTypes | undefined;
async function loadNet(): Promise<typeof netTypes> {
return _netModule ??= await import('net');
}
/**
* One upstream connection to the agent host, owned by a single renderer
* client. The default implementation wraps a `ws.WebSocket`; tests inject
* a custom factory via {@link AgentHostChannel.upstreamFactory}.
*/
export interface IUpstreamConnection extends IDisposable {
readonly onFrame: Event<string>;
readonly onClose: Event<void>;
connect(): Promise<void>;
send(frame: string): void;
}
export type UpstreamConnectionFactory = (endpoint: IAgentHostUpstreamEndpoint) => IUpstreamConnection;
/**
* IPC channel registered when the remote server has no agent host upstream.
* Keeping the channel present lets renderers fail explicitly without making
* the IPC layer report `Unknown channel: agentHostProxy`.
*/
export class UnavailableAgentHostChannel<TContext> implements IServerChannel<TContext> {
listen<T>(_ctx: TContext, event: string): Event<T> {
case 'frame':
case 'close':
return Event.None;
}
throw new Error(`Invalid listen: ${event}`);
call<T>(_ctx: TContext, command: string): Promise<T> {
case 'connect':
return Promise.reject(new Error(agentHostProxyUnavailableMessage));
case 'send':
case 'close':
return Promise.resolve(undefined as T);
}
return Promise.reject(new Error(`Invalid call: ${command}`));
/**
* Default upstream factory: opens an AHP WebSocket to the local agent host.
*/
const defaultUpstreamFactory = (logService: ILogService): UpstreamConnectionFactory =>
(endpoint) => new WebSocketUpstreamConnection(endpoint, logService);
class WebSocketUpstreamConnection extends Disposable implements IUpstreamConnection {
private readonly _onFrame = this._register(new Emitter<string>());
readonly onFrame: Event<string> = this._onFrame.event;
private readonly _onClose = this._register(new Emitter<void>());
readonly onClose: Event<void> = this._onClose.event;
private _ws: wsTypes.WebSocket | undefined;
private _connectPromise: Promise<void> | undefined;
private _closeFired = false;
constructor(
private readonly _endpoint: IAgentHostUpstreamEndpoint,
private readonly _logService: ILogService,
) {
super();
}
connect(): Promise<void> {
if (this._store.isDisposed) {
return Promise.reject(new Error('UpstreamConnection is disposed'));
}
return this._connectPromise ??= this._doConnect();
}
private async _doConnect(): Promise<void> {
const ws = await loadWs();
const url = this._buildUrl();
const wsOptions = await this._buildWsOptions();
this._logService.info(`[AgentHostChannel] Opening upstream to ${this._endpoint.socketPath ?? url}`);
const socket = new ws.WebSocket(url, wsOptions);
this._ws = socket;
return new Promise<void>((resolve, reject) => {
const onOpen = () => {
cleanup();
this._logService.trace('[AgentHostChannel] Upstream open');
socket.on('message', (data: Buffer | string) => {
const text = typeof data === 'string' ? data : data.toString('utf-8');
this._onFrame.fire(text);
});
socket.on('close', () => this._fireClose());
socket.on('error', err => {
this._logService.warn('[AgentHostChannel] Upstream error', err);
this._fireClose();
});
resolve();
};
const onError = (err: Error) => {
cleanup();
this._logService.warn('[AgentHostChannel] Upstream connection failed', err);
this._fireClose();
reject(err);
};
const onClose = () => {
cleanup();
this._fireClose();
reject(new Error('Upstream closed before connect'));
};
const cleanup = () => {
socket.removeListener('open', onOpen);
socket.removeListener('error', onError);
socket.removeListener('close', onClose);
};
socket.on('open', onOpen);
socket.on('error', onError);
socket.on('close', onClose);
});
}
send(frame: string): void {
const ws = this._ws;
if (!ws || ws.readyState !== ws.OPEN) {
this._logService.warn('[AgentHostChannel] Drop send: upstream not open');
this._fireClose();
return;
}
ws.send(frame);
}
override dispose(): void {
this._ws?.close();
this._fireClose();
super.dispose();
}
private _fireClose(): void {
if (this._closeFired) {
return;
}
this._closeFired = true;
this._onClose.fire();
}
private _buildUrl(): string {
const host = this._endpoint.host ?? 'localhost';
const port = this._endpoint.port ?? '0';
let url = `ws://${host}:${port}`;
if (this._endpoint.connectionToken) {
url += `?${connectionTokenQueryName}=${encodeURIComponent(this._endpoint.connectionToken)}`;
}
return url;
}
private async _buildWsOptions(): Promise<wsTypes.ClientOptions | undefined> {
if (!this._endpoint.socketPath) {
return undefined;
}
const net = await loadNet();
const socketPath = this._endpoint.socketPath;
// Note: `createConnection` shape required by `ws` differs slightly
// across versions; we cast through `unknown` to match the local typings.
const createConnection = (() => net.createConnection(socketPath)) as unknown as wsTypes.ClientOptions['createConnection'];
return { createConnection } satisfies wsTypes.ClientOptions;
}
/**
* IPC channel that proxies the agent host protocol. One channel instance
* serves all renderer clients; per-context state is tracked in `_perCtx`.
*/
export class AgentHostChannel<TContext> extends Disposable implements IServerChannel<TContext> {
private readonly _perCtx = new Map<TContext, IUpstreamConnection>();
private readonly _upstreamFactory: UpstreamConnectionFactory;
constructor(
private readonly _endpoint: IAgentHostUpstreamEndpoint,
private readonly _logService: ILogService,
upstreamFactory?: UpstreamConnectionFactory,
) {
super();
this._upstreamFactory = upstreamFactory ?? defaultUpstreamFactory(_logService);
this._register(ipcServer.onDidRemoveConnection(c => this._disposeCtx(c.ctx as unknown as TContext)));
}
listen<T>(ctx: TContext, event: string): Event<T> {
switch (event) {
case 'frame': return conn.onFrame as Event<unknown> as Event<T>;
case 'close': return conn.onClose as Event<unknown> as Event<T>;
}
throw new Error(`Invalid listen: ${event}`);
async call<T>(ctx: TContext, command: string, arg?: unknown): Promise<T> {
switch (command) {
case 'connect':
this._logService.info(`[AgentHostChannel] Renderer ctx=${String(ctx)} requested connect to upstream`);
await conn.connect();
return undefined as T;
case 'send':
throw new Error('send: arg must be a string frame');
}
return undefined as T;
this._disposeCtx(ctx);
return undefined as T;
throw new Error(`Invalid call: ${command}`);
override dispose(): void {
}
super.dispose();
}
private _getOrCreate(ctx: TContext): IUpstreamConnection {
if (!conn) {
conn = this._upstreamFactory(this._endpoint);
this._perCtx.set(ctx, conn);
// If the upstream closes on its own (e.g. agent host restart or
// connection drop), evict it from the cache so the next
// `connect()` call creates a fresh upstream rather than
// returning the stuck-closed one.
const sub = conn.onClose(() => {
sub.dispose();
if (this._perCtx.get(ctx) === conn) {
}
}
return conn;
}
private _disposeCtx(ctx: TContext): void {
if (conn) {
this._perCtx.delete(ctx);
conn.dispose();
}
}