authorgravatar for 73941998+UtkarshUsername@users.noreply.github.comUtkarsh Patil <73941998+UtkarshUsername@users.noreply.github.com> 2026-07-18 17:24:13+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-07-18 11:54:13+00:00
logbf76535fe4da71d8de7b8bd5ffa0d2086b7af8d0
tree2a7d8411ef87d1b20567e989928ead9b2c31bbd7
parent5c66110e269cc5a852763e5f06a81f2a763d3181
signature Signed by PGP key B5690EEEBB952194

fix(desktop): Validate WSL node version against engine range after probe success (#3621)

Co-authored-by: Julius Marminge <julius0216@outlook.com>

2 files changed, 66 insertions(+), 4 deletions(-)

apps/desktop/src/wsl/DesktopWslEnvironment.test.ts+24
......@@ -16,6 +16,7 @@ import {
1616 formatNodePtyProbeFailureReason,
1717 formatWslShellTransportFailureReason,
1818 parseNodePath,
19 parseNodeVersion,
1920 parseResolvedPath,
2021 parseToolchainReport,
2122 probeWslDistros,
......@@ -173,6 +174,29 @@ describe("parseNodePath", () => {
173174 });
174175});
175176
177describe("parseNodeVersion", () => {
178 it("extracts the node version from a nodeVersion: line", () => {
179 expect(parseNodeVersion("nodeVersion:24.10.0")).toBe("24.10.0");
180 });
181
182 it("returns null when the version value is empty", () => {
183 expect(parseNodeVersion("nodeVersion:")).toBeNull();
184 });
185
186 it("returns null when there is no nodeVersion line at all", () => {
187 expect(parseNodeVersion("nodePath:/usr/bin/node\nresolvedPath:/usr/bin")).toBeNull();
188 });
189
190 it("ignores surrounding noise and trims whitespace", () => {
191 const stdout = [
192 "some preamble noise",
193 " nodeVersion:22.16.0 ",
194 "nodePath:/usr/bin/node",
195 ].join("\n");
196 expect(parseNodeVersion(stdout)).toBe("22.16.0");
197 });
198});
199
176200describe("parseResolvedPath", () => {
177201 it("preserves spaces and apostrophes in the resolved login-shell PATH", () => {
178202 const resolvedPath = "/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin";
apps/desktop/src/wsl/DesktopWslEnvironment.ts+42-4
......@@ -32,7 +32,11 @@ export interface EnsureWslNodePtyOptions {
3232}
3333
3434export type EnsureWslNodePtyResult =
35 | { readonly ok: true; readonly nodePath: string; readonly resolvedPath: string }
35 | {
36 readonly ok: true;
37 readonly nodePath: string;
38 readonly resolvedPath: string;
39 }
3640 | {
3741 readonly ok: false;
3842 readonly reason: string;
......@@ -222,6 +226,7 @@ export const formatNodePtyProbeFailureReason = (exitCode: number): string | null
222226const NODE_PTY_PROBE_SCRIPT = (
223227 linuxServerDir: string,
224228) => `printf 'nodePath:%s\\n' "$(command -v node 2>/dev/null)"
229printf 'nodeVersion:%s\\n' "$(node -p 'process.versions.node' 2>/dev/null)"
225230printf 'resolvedPath:%s\\n' "$PATH"
226231cd ${shellQuote(linuxServerDir)} && node <<'NODE' >/dev/null 2>&1
227232// The server bundle externalizes its deps to node_modules, and the WSL Node
......@@ -318,6 +323,16 @@ export const parseNodePath = (stdout: string): string | null => {
318323 return path ?? null;
319324};
320325
326export const parseNodeVersion = (stdout: string): string | null => {
327 const version = stdout
328 .split("\n")
329 .map((line) => line.trim())
330 .filter((line) => line.startsWith("nodeVersion:"))
331 .map((line) => line.slice("nodeVersion:".length).trim())
332 .find((value) => value.length > 0);
333 return version ?? null;
334};
335
321336// Captures the login-shell PATH after the shared resolver has loaded version
322337// managers. Preserve the value byte-for-byte apart from a Windows-style CR so
323338// paths containing spaces or apostrophes can be forwarded as one env argv.
......@@ -404,7 +419,11 @@ const ensureNodePtyImpl = (
404419
405420 const transportFailureReason = formatWslShellTransportFailureReason(probe.transportFailure);
406421 if (transportFailureReason !== null) {
407 return { ok: false, reason: transportFailureReason, fatal: false } as const;
422 return {
423 ok: false,
424 reason: transportFailureReason,
425 fatal: false,
426 } as const;
408427 }
409428
410429 // No node at all, even after the shared resolver repaired PATH. Surface
......@@ -457,12 +476,31 @@ const ensureNodePtyImpl = (
457476 } as const;
458477 }
459478
460 if (probe.exitCode === 0) return { ok: true, nodePath, resolvedPath } as const;
479 if (probe.exitCode === 0) {
480 const rawVersion = parseNodeVersion(probe.stdout);
481 if (
482 rawVersion !== null &&
483 options.nodeEngineRange &&
484 !satisfiesSemverRange(rawVersion, options.nodeEngineRange.trim())
485 ) {
486 const range = options.nodeEngineRange.trim();
487 return {
488 ok: false,
489 reason: `WSL Node.js ${rawVersion} does not satisfy the server's required engine range (${range}). Install a compatible version, and restart the desktop app.`,
490 fatal: true,
491 } as const;
492 }
493 return { ok: true, nodePath, resolvedPath } as const;
494 }
461495
462496 if (options.allowBuild !== true) {
463497 const packagedProbeFailure = formatNodePtyProbeFailureReason(probe.exitCode);
464498 if (packagedProbeFailure !== null) {
465 return { ok: false, reason: packagedProbeFailure, fatal: true } as const;
499 return {
500 ok: false,
501 reason: packagedProbeFailure,
502 fatal: true,
503 } as const;
466504 }
467505 }
468506