src/vs/platform/agentHost/node/wslRemoteAgentHostHelpers.ts

323 LOC · 206 covered · 117 uncovered · 37 ranges · 15 concepts · 15 introducers · 10 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.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filewslRemoteAgentHostHelpers.ts ×3 · 6 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×1 · 2 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×3 · 6 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×2 · 4 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×6 · 34 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×3 · 14 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×1 · 2 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×1 · 2 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×1 · 2 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×1 · 1 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×1 · 2 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×2 · 6 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×2 · 7 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×2 · 2 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.ts ×8 · 116 introduced LOCwslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput decodes UTF-16LE input with BOM|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput decodes UTF-16LE input with BOM|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput decodes UTF-8 input|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput decodes UTF-8 input|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput detects null-padded UTF-16LE without BOM via heuristic|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput detects null-padded UTF-16LE without BOM via heuristic|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput returns empty string for empty buffer|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput returns empty string for empty buffer|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput strips UTF-8 BOM|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers decodeWslOutput strips UTF-8 BOM|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseRunningDistros parses a representative running list end-to-end|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseRunningDistros parses a representative running list end-to-end|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseRunningDistros returns empty for empty input|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseRunningDistros returns empty for empty input|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseWslListVerbose parses a representative listing end-to-end|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseWslListVerbose parses a representative listing end-to-end|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseWslListVerbose returns empty for empty input|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseWslListVerbose returns empty for empty input|occurrence=1wslRemoteAgentHostHelper…wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseWslListVerbose tolerates LF-only line endings and missing BOM|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/wslRemoteAgentHostHelpers.test|title=WSL Remote Agent Host Helpers parseWslListVerbose tolerates LF-only line endings and missing BOM|occurrence=1wslRemoteAgentHostHelper…Focused file · src/vs/platform/agentHost/node/wslRemoteAgentHostHelpers.ts · 323 LOCnode/wslRemoteAgentHostH…

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.

1 > /*--------------------------------------------------------------------------------------------- wslRemoteAgentHostHelpers.ts ×8
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 * as cp from 'child_process';
7 > import { join } from '../../../base/common/path.js';
8 > import type { IWSLDistro } from '../common/wslRemoteAgentHost.js';
9 > import {
10 > buildAgentHostBaseCommand,
11 > buildCLIDownloadUrl,
12 > buildCleanupOldCLIsCommand,
13 > extractAgentHostWebSocketURL,
14 > getRemoteCLIBin,
15 > getRemoteCLIDataDir,
16 > getRemoteCLIInstallRoot,
17 > shellEscape,
18 > validateShellToken,
19 > } from './sshRemoteAgentHostHelpers.js';
20 >
21 > export { extractAgentHostWebSocketURL };
22 >
23 > /**
24 > * Locate `wsl.exe`. Prefer the absolute path under `%SystemRoot%\System32`
25 > * (defends against PATH hijacks on Windows hosts where `wsl.exe` is itself
26 > * a security boundary). Falls back to bare `wsl.exe` if `%SystemRoot%` is
27 > * unset so non-Windows hosts still produce a sensible value for testing.
28 > */
29 > export function getWslExePath(): string {
30 const systemRoot = process.env['SystemRoot'];
31 if (!systemRoot) {
32 return 'wsl.exe';
33 }
34 return join(systemRoot, 'System32', 'wsl.exe');
35 }
37 > /**
38 > * Cheap probe for `wsl.exe --status`. Returns false on any non-Windows host,
39 > * on spawn failure (ENOENT), or on non-zero exit. Used by both the platform
40 > * capability check (renderer) and the picker gating logic (UI).
41 > */
42 export async function isWSLSupported(): Promise<boolean> {
43 if (process.platform !== 'win32') {
44 return false;
45 }
46 try {
47 const result = await runWslCommand(['--status'], { timeout: 5_000 });
48 return result.exitCode === 0;
49 } catch {
50 return false;
51 }
52 }
54 > export interface IRunWslCommandOptions {
55 > readonly timeout?: number;
56 > readonly distro?: string;
57 > }
58 >
59 > export interface IRunWslCommandResult {
60 > readonly stdout: string;
61 > readonly stderr: string;
62 > readonly exitCode: number;
63 > }
64 >
65 > /**
66 > * Spawn `wsl.exe` with `WSL_UTF8=1` and capture stdout/stderr. Modern WSL
67 > * builds honor `WSL_UTF8` and emit UTF-8; older builds emit null-padded
68 > * UTF-16LE regardless. We detect the latter by sniffing the first few bytes
69 > * and decode accordingly so a single helper covers both.
70 > *
71 > * Times out after {@link options.timeout} ms (default 30s); on timeout the
72 > * child is hard-killed and the promise rejects.
73 > */
74 > export function runWslCommand(args: readonly string[], options?: IRunWslCommandOptions): Promise<IRunWslCommandResult> {
75 return new Promise((resolve, reject) => {
76 const fullArgs = options?.distro ? ['-d', options.distro, ...args] : [...args];
77 const child = cp.spawn(getWslExePath(), fullArgs, {
78 env: { ...process.env, WSL_UTF8: '1' },
79 windowsHide: true,
80 stdio: ['ignore', 'pipe', 'pipe'],
81 });
82
83 const stdoutChunks: Buffer[] = [];
84 const stderrChunks: Buffer[] = [];
85 let settled = false;
86
87 const timeout = setTimeout(() => {
88 if (settled) {
89 return;
90 }
91 settled = true;
92 try {
93 child.kill('SIGKILL');
94 } catch { /* ignore */ }
95 reject(new Error(`wsl.exe ${fullArgs.join(' ')} timed out after ${options?.timeout ?? 30_000}ms`));
96 }, options?.timeout ?? 30_000);
97
98 child.stdout?.on('data', chunk => stdoutChunks.push(chunk));
99 child.stderr?.on('data', chunk => stderrChunks.push(chunk));
100
101 child.on('error', err => {
102 if (settled) {
103 return;
104 }
105 settled = true;
106 clearTimeout(timeout);
107 reject(err);
108 });
109
110 child.on('close', exitCode => {
111 if (settled) {
112 return;
113 }
114 settled = true;
115 clearTimeout(timeout);
116 resolve({
117 stdout: decodeWslOutput(Buffer.concat(stdoutChunks)),
118 stderr: decodeWslOutput(Buffer.concat(stderrChunks)),
119 exitCode: exitCode ?? -1,
120 });
121 });
122 });
123 }
125 > /**
126 > * Decode a `wsl.exe` output buffer that may be UTF-8 (modern builds with
127 > * `WSL_UTF8=1`) or UTF-16LE (older builds that ignore the env var). Strips
128 > * the BOM in either case.
129 > */
130 > export function decodeWslOutput(buffer: Buffer): string {
131 > if (buffer.length === 0) { wslRemoteAgentHostHelpers.ts ×2
133 > }
134 > // UTF-16LE BOM wslRemoteAgentHostHelpers.ts ×1
135 > if (buffer.length >= 2 && buffer[0] === 0xff && buffer[1] === 0xfe) { wslRemoteAgentHostHelpers.ts ×2
136 > return buffer.toString('utf16le', 2); wslRemoteAgentHostHelpers.ts ×1
137 > }
138 > // Heuristic: WSL UTF-16LE output is ASCII text, so every other byte is wslRemoteAgentHostHelpers.ts ×3
139 > // 0x00. Sample the first 16 bytes (skipping byte 0 in case it's a BOM
140 > // remnant). If a clear majority of odd-indexed bytes are zero, treat as
141 > // UTF-16LE.
142 > const sampleLen = Math.min(buffer.length, 16);
143 > if (sampleLen >= 4) {
144 > let zeros = 0;
145 > let total = 0;
146 > for (let i = 1; i < sampleLen; i += 2) {
147 > total++;
148 > if (buffer[i] === 0) {
150 > }
152 > if (total > 0 && zeros / total >= 0.75) {
153 > let text = buffer.toString('utf16le'); wslRemoteAgentHostHelpers.ts ×3
154 > if (text.charCodeAt(0) === 0xfeff) {
155 text = text.slice(1);
156 }
158 > }
160 > let text = buffer.toString('utf8'); wslRemoteAgentHostHelpers.ts ×2
161 > if (text.charCodeAt(0) === 0xfeff) {
162 > text = text.slice(1); wslRemoteAgentHostHelpers.ts ×1
163 > }
165 > }
167 > /**
168 > * Parse the output of `wsl --list --verbose`. Only WSL 2 distros are
169 > * returned — WSL 1 lacks the kernel features needed to host the agent.
170 > *
171 > * Robust against:
172 > * - BOM, CRLF and lone LF line endings
173 > * - header row with locale-dependent column names
174 > * - the leading `* ` marker on the default distro
175 > * - trailing whitespace and empty lines
176 > */
177 > export function parseWslListVerbose(output: string): IWSLDistro[] {
178 > if (!output) { wslRemoteAgentHostHelpers.ts ×2
180 > }
181 > const stripped = output.charCodeAt(0) === 0xfeff ? output.slice(1) : output; wslRemoteAgentHostHelpers.ts ×2
182 > const lines = stripped.split(/\r\n|\r|\n/);
183 > const distros: IWSLDistro[] = [];
184 > let headerSeen = false;
185 > for (const rawLine of lines) {
186 > const line = rawLine.replace(/\s+$/, ''); wslRemoteAgentHostHelpers.ts ×6
187 > if (!line.trim()) {
189 > }
190 > // The header row starts with `NAME` (possibly preceded by leading wslRemoteAgentHostHelpers.ts ×6
191 > // whitespace where the `* ` marker would otherwise appear). Skip
192 > // it once seen; any subsequent NAME-headed row is data.
193 > if (!headerSeen) {
194 > const upper = line.trim().toUpperCase();
195 > if (upper.startsWith('NAME')) {
196 > headerSeen = true;
197 > continue;
198 > }
199 > }
200 > let working = line;
201 > let isDefault = false;
202 > if (working.startsWith('* ') || working.startsWith('*\t')) {
203 > isDefault = true;
204 > working = working.slice(2);
205 > } else if (working.startsWith(' ') || working.startsWith(' \t')) {
206 > working = working.slice(2); wslRemoteAgentHostHelpers.ts ×3
207 > } else if (working.startsWith(' ')) {
208 working = working.slice(1);
209 }
210 > const columns = working.trim().split(/\s+/); wslRemoteAgentHostHelpers.ts ×6
211 > if (columns.length < 3) {
212 continue;
213 }
214 > const version = parseInt(columns[columns.length - 1], 10); wslRemoteAgentHostHelpers.ts ×6
215 > if (version !== 2) {
217 > }
218 > const state = columns[columns.length - 2]; wslRemoteAgentHostHelpers.ts ×6
219 > const name = columns.slice(0, columns.length - 2).join(' ');
220 > if (!name) {
221 continue;
222 }
223 > distros.push({ wslRemoteAgentHostHelpers.ts ×6
224 > name,
225 > isDefault,
226 > isRunning: state.toLowerCase() === 'running',
227 > version: 2,
228 > });
229 > }
230 > return distros;
231 > }
233 > /**
234 > * Parse the output of `wsl --list --running --quiet`. One distro per line.
235 > */
236 > export function parseRunningDistros(output: string): string[] {
237 > if (!output) { wslRemoteAgentHostHelpers.ts ×2
239 > }
240 > const stripped = output.charCodeAt(0) === 0xfeff ? output.slice(1) : output; wslRemoteAgentHostHelpers.ts ×2
241 > return stripped
242 > .split(/\r\n|\r|\n/)
243 > .map(line => line.trim())
244 > .filter(line => line.length > 0);
245 > }
247 > export interface IComposeAgentHostBootstrapScriptArgs {
248 > readonly serverDataFolderName: string;
249 > readonly quality: string;
250 > readonly commit: string | undefined;
251 > readonly os: string;
252 > readonly arch: string;
253 > /** Dev override; when set, returned verbatim and all CLI bootstrap is skipped. */
254 > readonly remoteAgentHostCommand?: string;
255 > }
256 >
257 > /**
258 > * Compose the bash one-liner passed to `wsl.exe -d <distro> -e bash -lc`.
259 > *
260 > * Reuses the same install layout helpers as Remote-SSH
261 > * ({@link getRemoteCLIBin}, {@link buildCLIDownloadUrl},
262 > * {@link buildCleanupOldCLIsCommand}, {@link buildAgentHostBaseCommand}) so
263 > * a WSL-installed CLI and an SSH-installed CLI share the same
264 > * `~/<serverDataFolderName>/...` files inside the distro.
265 > *
266 > * SSH composes the same operations across multiple sequential `exec` calls
267 > * (so it can branch on lockfile reuse and CLI-install failures); WSL does
268 > * not have a control channel back to the host between commands and so
269 > * collapses the install+launch into one script per spawn. The shared layout
270 > * lives in the helper functions above, not in the composition itself.
271 > */
272 > export function composeAgentHostBootstrapScript(args: IComposeAgentHostBootstrapScriptArgs): string {
273 if (args.remoteAgentHostCommand) {
274 return args.remoteAgentHostCommand;
275 }
276 const installRoot = getRemoteCLIInstallRoot(args.serverDataFolderName);
277 const cliBin = getRemoteCLIBin(args.serverDataFolderName, args.quality, args.commit);
278 const cliDataDir = getRemoteCLIDataDir(args.serverDataFolderName);
279 const url = buildCLIDownloadUrl(args.os, args.arch, args.quality, args.commit);
280 const launch = `exec ${buildAgentHostBaseCommand(cliBin, cliDataDir)}`;
281
282 if (args.commit) {
283 // Pinned-install path. Mirrors SSH's _ensureCLIInstalledPinned: stage
284 // into a same-FS tmpdir for atomic rename, validate +x, then prune
285 // older commit-keyed binaries (keep newest 5). Touch keeps the
286 // reuse-path binary's mtime fresh so it doesn't fall out of that
287 // window.
288 const cleanup = buildCleanupOldCLIsCommand(args.serverDataFolderName, args.quality);
289 const installSteps = [
290 `tmpdir=$(mktemp -d ${installRoot}/.cli-install-XXXXXX)`,
291 `(cd "$tmpdir" && curl -fsSL ${shellEscape(url)} | tar xz)`,
292 `mv "$tmpdir"/* ${cliBin}`,
293 `chmod +x ${cliBin}`,
294 `rm -rf "$tmpdir"`,
295 ].join(' && ');
296 return [
297 `mkdir -p ${installRoot}`,
298 `if [ ! -x ${cliBin} ]; then ${installSteps}; fi`,
299 `touch -- ${cliBin} 2>/dev/null || true`,
300 `(${cleanup}) >/dev/null 2>&1 || true`,
301 launch,
302 ].join(' && ');
303 }
304
305 // Loose dev-build path. Matches SSH's _ensureCLIInstalledLoose: single
306 // non-pinned binary, no retention pruning, install on first miss.
307 const installLoose = `curl -fsSL ${shellEscape(url)} | tar xz -C ${installRoot} && chmod +x ${cliBin}`;
308 return [
309 `mkdir -p ${installRoot}`,
310 `if [ ! -x ${cliBin} ]; then ${installLoose}; fi`,
311 launch,
312 ].join(' && ');
313 }
315 > /**
316 > * Validate that a string is safe to interpolate as a `wsl.exe -d <distro>`
317 > * argument. WSL distro names are user-creatable so they could in principle
318 > * contain spaces or quotes, but every distro shipped through the Store
319 > * matches `[A-Za-z0-9._-]+`. Reject anything else as defense-in-depth.
320 > */
321 > export function validateDistroName(name: string): string {
322 return validateShellToken(name, 'WSL distro name');
323 }