| ... | @@ -32,7 +32,11 @@ export interface EnsureWslNodePtyOptions { | ... | @@ -32,7 +32,11 @@ export interface EnsureWslNodePtyOptions { |
| 32 | } | 32 | } |
| 33 | | 33 | |
| 34 | export type EnsureWslNodePtyResult = | 34 | export 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 | } |
| 36 | | { | 40 | | { |
| 37 | readonly ok: false; | 41 | readonly ok: false; |
| 38 | readonly reason: string; | 42 | readonly reason: string; |
| ... | @@ -222,6 +226,7 @@ export const formatNodePtyProbeFailureReason = (exitCode: number): string | null | ... | @@ -222,6 +226,7 @@ export const formatNodePtyProbeFailureReason = (exitCode: number): string | null |
| 222 | const NODE_PTY_PROBE_SCRIPT = ( | 226 | const NODE_PTY_PROBE_SCRIPT = ( |
| 223 | linuxServerDir: string, | 227 | linuxServerDir: string, |
| 224 | ) => `printf 'nodePath:%s\\n' "$(command -v node 2>/dev/null)" | 228 | ) => `printf 'nodePath:%s\\n' "$(command -v node 2>/dev/null)" |
| | 229 | printf 'nodeVersion:%s\\n' "$(node -p 'process.versions.node' 2>/dev/null)" |
| 225 | printf 'resolvedPath:%s\\n' "$PATH" | 230 | printf 'resolvedPath:%s\\n' "$PATH" |
| 226 | cd ${shellQuote(linuxServerDir)} && node <<'NODE' >/dev/null 2>&1 | 231 | cd ${shellQuote(linuxServerDir)} && node <<'NODE' >/dev/null 2>&1 |
| 227 | // The server bundle externalizes its deps to node_modules, and the WSL Node | 232 | // The server bundle externalizes its deps to node_modules, and the WSL Node |
| ... | @@ -318,6 +323,16 @@ export const parseNodePath = (stdout: string): string | null => { | ... | @@ -318,6 +323,16 @@ export const parseNodePath = (stdout: string): string | null => { |
| 318 | return path ?? null; | 323 | return path ?? null; |
| 319 | }; | 324 | }; |
| 320 | | 325 | |
| | 326 | export 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 | |
| 321 | // Captures the login-shell PATH after the shared resolver has loaded version | 336 | // Captures the login-shell PATH after the shared resolver has loaded version |
| 322 | // managers. Preserve the value byte-for-byte apart from a Windows-style CR so | 337 | // managers. Preserve the value byte-for-byte apart from a Windows-style CR so |
| 323 | // paths containing spaces or apostrophes can be forwarded as one env argv. | 338 | // paths containing spaces or apostrophes can be forwarded as one env argv. |
| ... | @@ -404,7 +419,11 @@ const ensureNodePtyImpl = ( | ... | @@ -404,7 +419,11 @@ const ensureNodePtyImpl = ( |
| 404 | | 419 | |
| 405 | const transportFailureReason = formatWslShellTransportFailureReason(probe.transportFailure); | 420 | const transportFailureReason = formatWslShellTransportFailureReason(probe.transportFailure); |
| 406 | if (transportFailureReason !== null) { | 421 | 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; |
| 408 | } | 427 | } |
| 409 | | 428 | |
| 410 | // No node at all, even after the shared resolver repaired PATH. Surface | 429 | // No node at all, even after the shared resolver repaired PATH. Surface |
| ... | @@ -457,12 +476,31 @@ const ensureNodePtyImpl = ( | ... | @@ -457,12 +476,31 @@ const ensureNodePtyImpl = ( |
| 457 | } as const; | 476 | } as const; |
| 458 | } | 477 | } |
| 459 | | 478 | |
| 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 | } |
| 461 | | 495 | |
| 462 | if (options.allowBuild !== true) { | 496 | if (options.allowBuild !== true) { |
| 463 | const packagedProbeFailure = formatNodePtyProbeFailureReason(probe.exitCode); | 497 | const packagedProbeFailure = formatNodePtyProbeFailureReason(probe.exitCode); |
| 464 | if (packagedProbeFailure !== null) { | 498 | 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; |
| 466 | } | 504 | } |
| 467 | } | 505 | } |
| 468 | | 506 | |