/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import type WebSocket from 'ws'; import * as cp from 'child_process'; import { Emitter, Event } from '../../../base/common/event.js'; import { Disposable, toDisposable } from '../../../base/common/lifecycle.js'; import { removeAnsiEscapeCodes } from '../../../base/common/strings.js'; import { generateUuid } from '../../../base/common/uuid.js'; import { localize } from '../../../nls.js'; import { ILogService } from '../../log/common/log.js'; import { IProductService } from '../../product/common/productService.js'; import type { IRelayMessage } from '../common/relayTransport.js'; import { IWSLRemoteAgentHostMainService, type IWSLAgentHostConfig, type IWSLConnectProgress, type IWSLConnectResult, type IWSLDistro, } from '../common/wslRemoteAgentHost.js'; import { redactToken, resolveRemotePlatform } from './sshRemoteAgentHostHelpers.js'; import { composeAgentHostBootstrapScript, decodeWslOutput, extractAgentHostWebSocketURL, getWslExePath, isWSLSupported, parseRunningDistros, parseWslListVerbose, runWslCommand, validateDistroName, } from './wslRemoteAgentHostHelpers.js'; const LOG_PREFIX = '[WSLRemoteAgentHost]'; /** Max time to wait for `code agent host` inside the distro to print its `ws://` URL. */ const AGENT_HOST_READY_TIMEOUT_MS = 60_000; /** Max time to wait for the host-side WebSocket to complete its handshake. */ const WEBSOCKET_OPEN_TIMEOUT_MS = 30_000; /** Max stdout/stderr lines kept buffered for diagnostic context on failure. */ const OUTPUT_BUFFER_LINES = 50; interface IWSLConnection { readonly connectionId: string; readonly distro: string; readonly name: string; readonly address: string; readonly connectionToken: string | undefined; readonly child: cp.ChildProcess; readonly ws: WebSocket; } export class WSLRemoteAgentHostMainService extends Disposable implements IWSLRemoteAgentHostMainService { declare readonly _serviceBrand: undefined; private readonly _onDidChangeConnections = this._register(new Emitter()); readonly onDidChangeConnections: Event = this._onDidChangeConnections.event; private readonly _onDidCloseConnection = this._register(new Emitter()); readonly onDidCloseConnection: Event = this._onDidCloseConnection.event; private readonly _onDidReportConnectProgress = this._register(new Emitter()); readonly onDidReportConnectProgress: Event = this._onDidReportConnectProgress.event; private readonly _onDidRelayMessage = this._register(new Emitter()); readonly onDidRelayMessage: Event = this._onDidRelayMessage.event; private readonly _onDidRelayClose = this._register(new Emitter()); readonly onDidRelayClose: Event = this._onDidRelayClose.event; private readonly _connections = new Map(); private readonly _distroToConnectionId = new Map(); private _nativeRequire: NodeJS.Require | undefined; constructor( @ILogService private readonly _logService: ILogService, @IProductService private readonly _productService: IProductService, ) { super(); this._register(toDisposable(() => { for (const id of [...this._connections.keys()]) { this._closeConnection(id); } })); } private get _quality(): string { return this._productService.quality || 'insider'; } private get _serverDataFolderName(): string { const value = this._productService.serverDataFolderName; if (!value) { throw new Error(`${LOG_PREFIX} productService.serverDataFolderName is required`); } return value; } private get _commit(): string | undefined { return this._productService.commit; } /** Lazily load `require` so the `ws` native module is only resolved at runtime. */ private async _getNativeRequire(): Promise { if (!this._nativeRequire) { const nodeModule = await import('node:module'); this._nativeRequire = nodeModule.createRequire(import.meta.url); } return this._nativeRequire; } async isWSLAvailable(): Promise { return isWSLSupported(); } async listDistros(): Promise { try { // Run both probes in parallel so we can overlay the locale-free // running set on the verbose parse (the `STATE` column from // `--verbose` is localized by Windows and reads "Stopped" for // every distro on non-English hosts). const [verbose, running] = await Promise.all([ runWslCommand(['--list', '--verbose']), runWslCommand(['--list', '--running', '--quiet']), ]); if (verbose.exitCode !== 0) { this._logService.info(`${LOG_PREFIX} wsl --list --verbose exited ${verbose.exitCode}: ${verbose.stderr.trim()}`); return []; } const parsed = parseWslListVerbose(verbose.stdout); if (running.exitCode !== 0) { return parsed; } const runningSet = new Set(parseRunningDistros(running.stdout)); return parsed.map(d => ({ ...d, isRunning: runningSet.has(d.name) })); } catch (err) { this._logService.warn(`${LOG_PREFIX} listDistros failed`, err); return []; } } async listRunningDistros(): Promise { try { const result = await runWslCommand(['--list', '--running', '--quiet']); if (result.exitCode !== 0) { return []; } return parseRunningDistros(result.stdout); } catch (err) { this._logService.warn(`${LOG_PREFIX} listRunningDistros failed`, err); return []; } } async connect(config: IWSLAgentHostConfig): Promise { const distro = validateDistroName(config.distro); // Idempotent: a second `connect` for an already-live distro returns // the existing connection so the renderer-side `_setupConnection` // reuses its handle (it dedupes by `connectionId`). Picking // "WSL..." → same distro should be a no-op, not an error. const existingId = this._distroToConnectionId.get(distro); if (existingId) { const existing = this._connections.get(existingId); if (existing) { return { connectionId: existing.connectionId, address: existing.address, distro: existing.distro, name: existing.name, connectionToken: existing.connectionToken, }; } } const connectionKey = `wsl:${distro}`; const reportProgress = (message: string) => { this._onDidReportConnectProgress.fire({ connectionKey, message }); }; reportProgress(localize('wslProgressDetectingPlatform', "Detecting platform in {0}...", distro)); const { os: targetOs, arch: targetArch } = await this._resolvePlatform(distro); reportProgress(localize('wslProgressPreparingCLI', "Preparing CLI in {0}...", distro)); const script = composeAgentHostBootstrapScript({ serverDataFolderName: this._serverDataFolderName, quality: this._quality, commit: this._commit, os: targetOs, arch: targetArch, remoteAgentHostCommand: config.remoteAgentHostCommand, }); this._logService.info(`${LOG_PREFIX} Spawning agent host in WSL distro '${distro}'`); this._logService.trace(`${LOG_PREFIX} bootstrap script: ${script}`); // `-e bash -lc