diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index c0ef57ae993a..e2c7874e5bb2 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -437,7 +437,6 @@ describe("DesktopBackendConfiguration", () => { runtimeId: string; sha256: string; }> = []; - const observedProbeRoots: string[] = []; let legacyCleanupCount = 0; const linuxAppRoot = "/home/test/.t3/wsl-runtime/1.2.3-x64"; const resolvedPath = "/home/test/.local/bin:/usr/bin:/bin"; @@ -456,11 +455,7 @@ describe("DesktopBackendConfiguration", () => { runtimeId: archive.runtimeId, sha256: archive.sha256, }); - return { ok: true, linuxAppRoot }; - }, - probeRuntime: (_distro, root) => { - observedProbeRoots.push(root); - return { ok: true, resolvedPath }; + return { ok: true, linuxAppRoot, resolvedPath }; }, ensureNodePty: () => { throw new Error("the staged runtime must not probe for node-pty"); @@ -480,7 +475,6 @@ describe("DesktopBackendConfiguration", () => { sha256: archiveHash, }, ]); - assert.deepEqual(observedProbeRoots, [linuxAppRoot]); assert.equal( config.entryPath, path.join(baseDir, "server.asar/apps/server/dist/bin.mjs"), @@ -513,7 +507,11 @@ describe("DesktopBackendConfiguration", () => { wsl: () => ({ prepareRuntime: (_distro, archive) => { observedRuntimeIds.push(archive.runtimeId); - return { ok: true, linuxAppRoot: `/runtime/${archive.runtimeId}` }; + return { + ok: true, + linuxAppRoot: `/runtime/${archive.runtimeId}`, + resolvedPath: "/usr/bin:/bin", + }; }, ensureNodePty: () => ({ ok: true, @@ -572,25 +570,18 @@ describe("DesktopBackendConfiguration", () => { ); }); - it.effect("resolveWsl retires a staged runtime whose executable does not start", () => { + it.effect("resolveWsl falls back when runtime validation fails", () => { const archiveHash = "c".repeat(64); const stagedAppRoot = `/home/test/.t3/wsl-runtime/sha256-${archiveHash}`; - const observedProbeRoots: string[] = []; const observedNodePtyRoots: string[] = []; - const invalidatedRuntimeIds: string[] = []; return withPackagedWslHarness( { archiveHash, wsl: () => ({ - prepareRuntime: () => ({ ok: true, linuxAppRoot: stagedAppRoot }), - invalidateRuntime: (_distro, runtimeId) => - Effect.sync(() => { - invalidatedRuntimeIds.push(runtimeId); - }), - probeRuntime: (_distro, root) => { - observedProbeRoots.push(root); - return { ok: false, reason: `${root}/t3 --version failed (exit 127)` }; - }, + prepareRuntime: () => ({ + ok: false, + reason: `${stagedAppRoot}/t3 --help failed (exit 127)`, + }), ensureNodePty: (_distro, root) => { observedNodePtyRoots.push(root); return { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" }; @@ -602,7 +593,6 @@ describe("DesktopBackendConfiguration", () => { const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; const config = yield* configuration.resolveWsl({ port: 5000, distro: "Ubuntu" }); - assert.deepEqual(observedProbeRoots, [stagedAppRoot]); assert.deepEqual(observedNodePtyRoots, [mountedAppRoot]); assert.include(config.args, "/usr/bin/node"); assert.include(config.args, `${mountedAppRoot}/apps/server/dist/bin.mjs`); @@ -610,24 +600,16 @@ describe("DesktopBackendConfiguration", () => { assert.equal(config.entryPath, mountedEntryPath); assert.isUndefined(config.wslRuntimeId); assert.isTrue(Option.isNone(config.preflightFailure)); - assert.deepEqual(invalidatedRuntimeIds, [`sha256-${archiveHash}`]); }), ); }); - it.effect("resolveWsl keeps the staged runtime when the mounted tree fails too", () => { - const stagedAppRoot = "/home/test/.t3/wsl-runtime/cache"; - const invalidatedRuntimeIds: string[] = []; + it.effect("resolveWsl reports runtime validation failure when the mounted tree fails too", () => { return withPackagedWslHarness( { archiveHash: "d".repeat(64), wsl: () => ({ - prepareRuntime: () => ({ ok: true, linuxAppRoot: stagedAppRoot }), - invalidateRuntime: (_distro, runtimeId) => - Effect.sync(() => { - invalidatedRuntimeIds.push(runtimeId); - }), - probeRuntime: () => ({ + prepareRuntime: () => ({ ok: false, reason: "unsupported CPU architecture or incompatible system libraries", }), @@ -646,24 +628,16 @@ describe("DesktopBackendConfiguration", () => { assert.isTrue(failure.fatal); assert.include(failure.reason, "unsupported CPU architecture"); - assert.deepEqual(invalidatedRuntimeIds, []); }), ); }); it.effect("resolveWsl keeps WSL retryable when the mounted fallback fails transiently", () => { - const stagedAppRoot = "/home/test/.t3/wsl-runtime/cache"; - const invalidatedRuntimeIds: string[] = []; return withPackagedWslHarness( { archiveHash: "f".repeat(64), wsl: () => ({ - prepareRuntime: () => ({ ok: true, linuxAppRoot: stagedAppRoot }), - invalidateRuntime: (_distro, runtimeId) => - Effect.sync(() => { - invalidatedRuntimeIds.push(runtimeId); - }), - probeRuntime: () => ({ ok: false, reason: "t3 --version failed (exit 1)" }), + prepareRuntime: () => ({ ok: false, reason: "t3 --version failed (exit 1)" }), ensureNodePty: () => ({ ok: false, reason: "WSL backend preflight timed out while probing for Node.js.", @@ -680,7 +654,6 @@ describe("DesktopBackendConfiguration", () => { assert.isFalse(failure.fatal); assert.equal(failure.retryLimit, 12); assert.include(failure.reason, "timed out"); - assert.deepEqual(invalidatedRuntimeIds, []); }), ); }); diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index d9ff98279718..e1e13e042254 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -386,7 +386,7 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f : ({ ok: true, windowsEntryPath, linuxAppRoot: mountedAppRoot.value } as const); }); - let stagedFailure: { readonly runtimeId: string; readonly reason: string } | undefined; + let stagedFailure: { readonly reason: string } | undefined; const failedStaged = (failure: { readonly reason: string }) => ({ _tag: "Failed", @@ -397,26 +397,17 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f if (input.runtimeArchive !== null) { const runtime = yield* wslEnv.prepareRuntime(runningDistro, input.runtimeArchive); if (runtime.ok) { - // The staged runtime supplies its own Node and node-pty. Provider PATH - // discovery must not require either dependency for runtime readiness. - const stagedProbe = yield* wslEnv.probeRuntime(runningDistro, runtime.linuxAppRoot); - if (stagedProbe.ok) { - yield* wslServerTree.cleanupLegacy; - return { - _tag: "Ready", - runningDistro, - windowsEntryPath: environment.backendEntryPath, - runtime: { kind: "executable", entryPath: `${runtime.linuxAppRoot}/t3` }, - resolvedPath: stagedProbe.resolvedPath, - runtimeId: input.runtimeArchive.runtimeId, - } as const; - } - yield* Effect.logWarning( - "The staged WSL runtime did not start; retrying from the mounted server tree.", - { reason: stagedProbe.reason }, - ); - stagedFailure = { runtimeId: input.runtimeArchive.runtimeId, reason: stagedProbe.reason }; + yield* wslServerTree.cleanupLegacy; + return { + _tag: "Ready", + runningDistro, + windowsEntryPath: environment.backendEntryPath, + runtime: { kind: "executable", entryPath: `${runtime.linuxAppRoot}/t3` }, + resolvedPath: runtime.resolvedPath, + runtimeId: input.runtimeArchive.runtimeId, + } as const; } else { + stagedFailure = { reason: runtime.reason }; yield* Effect.logWarning( "Could not stage the WSL runtime; launching from the mounted server tree instead.", { reason: runtime.reason }, @@ -440,16 +431,12 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f // Substituting the staged verdict for a transient mounted failure would // turn a retryable failure into a fatal one, ending the WSL attempt (and, // in wsl-only mode, persisting Windows) before the slow /mnt path had a - // chance to answer and clear the bad cache. + // chance to answer. return stagedFailure && nodePtyResult.fatal ? failedStaged(stagedFailure) : failedNodePty(nodePtyResult); } - if (stagedFailure) { - yield* wslEnv.invalidateRuntime(runningDistro, stagedFailure.runtimeId); - } - return { _tag: "Ready", runningDistro, diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts index 0224eca440ea..3b6780892ece 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts @@ -12,10 +12,9 @@ import * as TestClock from "effect/testing/TestClock"; import { ChildProcessSpawner } from "effect/process"; import { + buildWslProviderPathScript, buildWslRuntimeInstallScript, - buildWslRuntimeInvalidateScript, buildWslRuntimePruneScript, - buildWslRuntimeProbeScript, DesktopWslDistroListError, formatMissingToolsReason, parseNodePath, @@ -165,7 +164,6 @@ describe("WSL runtime cache", () => { (id: string) => buildWslRuntimeInstallScript("/runtime.tar.gz", id, "b".repeat(64)), ], ["prune", buildWslRuntimePruneScript], - ["invalidate", buildWslRuntimeInvalidateScript], ] as const)("sanitizes cache ids in the %s script", (_, buildScript) => { const runtimeId = "1.2.3/x64; touch /tmp/nope"; const script = buildScript(runtimeId); @@ -182,7 +180,7 @@ describe("WSL runtime cache", () => { expect(script).toContain('runtime_parent="$HOME/.t3/wsl-runtime"'); expect(script).toContain(' [ -f "$ready_marker" ] &&'); - expect(script).toContain(' runtime_entry_runs "$runtime_root" &&'); + expect(script).toContain(' runtime_entry_runs "$runtime_root" &'); expect(script).toContain("if runtime_is_ready; then"); expect(script).not.toContain("bin.mjs"); expect(script).not.toContain("node-pty"); @@ -197,7 +195,7 @@ describe("WSL runtime cache", () => { expect(script).toContain( "tar -xzf '/mnt/c/Program Files/T3 Code/wsl-runtime.tar.gz' -C \"$runtime_tmp\" --strip-components=1", ); - expect(script).toContain('if ! runtime_entry_runs "$runtime_tmp"; then'); + expect(script).toContain('if ! wait "$runtime_check_pid"; then'); expect(script).toContain('mv -T "$runtime_tmp" "$runtime_root"'); expect(script).not.toContain('rm -rf "$runtime_root"'); @@ -262,7 +260,7 @@ describe("WSL runtime cache", () => { "b".repeat(64), ); - expect(script).toContain(' [ -x "$1/t3" ] && "$1/t3" --version >/dev/null 2>&1'); + expect(script).toContain(' [ -x "$1/t3" ] && "$1/t3" --help >/dev/null 2>&1'); const entryCheckDefined = script.indexOf("runtime_entry_runs() {"); const readinessDefined = script.indexOf("runtime_is_ready() {"); @@ -280,10 +278,8 @@ describe("WSL runtime cache", () => { ); expect(script).toContain(` sha256sum "$1/t3" 2>/dev/null | cut -d ' ' -f 1`); - expect(script).toContain( - ' [ "$recorded_entry_digest" = "$(runtime_server_entry_digest "$runtime_root")" ]', - ); - expect(script).toContain(' [ -n "$recorded_entry_digest" ] &&'); + expect(script).toContain(' [ "$recorded_entry_digest" = "$current_entry_digest" ]'); + expect(script).toContain(' [ -n "$recorded_entry_digest" ] || return 1'); expect(script).toContain( `printf '%s\\n' "$installed_entry_digest" > "$runtime_tmp/.t3code-wsl-runtime-ready"`, ); @@ -306,7 +302,7 @@ describe("WSL runtime cache", () => { "b".repeat(64), ); - expect(script).toContain('if ! runtime_entry_runs "$runtime_tmp"; then'); + expect(script).toContain('if ! wait "$runtime_check_pid"; then'); const payloadValidated = script.indexOf('runtime_entry_runs "$runtime_tmp"'); const markerWritten = script.indexOf('> "$runtime_tmp/.t3code-wsl-runtime-ready"'); @@ -357,13 +353,6 @@ describe("WSL runtime cache", () => { ); expect(script).toContain('find "$scratch" -maxdepth 0 -mmin +120'); }); - - it("invalidates a cache by dropping its ready marker, not the tree", () => { - const script = buildWslRuntimeInvalidateScript("1.2.3/x64"); - - expect(script).toContain('rm -f "$HOME/.t3/wsl-runtime/1.2.3_x64/.t3code-wsl-runtime-ready"'); - expect(script).not.toContain("rm -rf"); - }); }); describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed)", () => { @@ -374,14 +363,14 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed fixtures.length = 0; }); - const createFixture = () => { + const createFixture = (serverEntrySource = SERVER_ENTRY_SOURCE) => { const result = runShell( [ "set -eu", "work=$(mktemp -d)", 'stage="$work/stage/t3-0.0.0-linux-x64"', 'mkdir -p "$stage/node_modules/node-pty/build/Release" "$work/home"', - `printf '%s' ${sh(SERVER_ENTRY_SOURCE)} > "$stage/t3"`, + `printf '%s' ${sh(serverEntrySource)} > "$stage/t3"`, 'chmod +x "$stage/t3"', `printf '%s' 'pty-native-payload' > "$stage/node_modules/node-pty/build/Release/pty.node"`, `tar -czf "$work/wsl-runtime.tar.gz" -C "$work/stage" t3-0.0.0-linux-x64`, @@ -431,7 +420,7 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed " }", ' eval "$2"', "); }", - buildWslRuntimeProbeScript(fixture.runtimeRoot), + buildWslProviderPathScript(), ].join("\n"), ); @@ -476,7 +465,7 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed [ "bash() { return 127; }", 'export PATH="/fixture/bin:/usr/bin:/bin"', - buildWslRuntimeProbeScript(fixture.runtimeRoot), + buildWslProviderPathScript(), ].join("\n"), ); @@ -508,6 +497,57 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed expect(restored.stdout).toBe(SERVER_ENTRY_SOURCE); }); + it("repairs a cache whose version prints but whose CLI cannot load", () => { + const fixture = createFixture(); + expect(fixture.install().status).toBe(0); + const brokenEntry = '#!/bin/sh\n[ "$1" = "--version" ] && echo "t3 v0.0.0" && exit 0\nexit 1\n'; + const broken = runShell( + [ + "set -eu", + `printf '%s' ${sh(brokenEntry)} > ${sh(fixture.serverEntry)}`, + `sha256sum ${sh(fixture.serverEntry)} | cut -d ' ' -f 1 > ${sh(`${fixture.runtimeRoot}/.t3code-wsl-runtime-ready`)}`, + ].join("\n"), + ); + expect(broken.status, broken.stderr).toBe(0); + + const repaired = fixture.install(); + + expect(repaired.status, repaired.stderr).toBe(0); + expect(parseWslRuntimeRoot(repaired.stdout)).toBe(fixture.runtimeRoot); + expect(runShell(`cat ${sh(fixture.serverEntry)}`).stdout).toBe(SERVER_ENTRY_SOURCE); + }); + + it("does not promote an archive when its CLI validation fails", () => { + const fixture = createFixture("#!/bin/sh\nexit 23\n"); + + const installed = fixture.install(); + + expect(installed.status).not.toBe(0); + expect(installed.stderr).toContain("does not contain a working t3 executable"); + expect(parseWslRuntimeRoot(installed.stdout)).toBeNull(); + expect(runShell(`[ ! -e ${sh(fixture.runtimeRoot)} ]`).status).toBe(0); + }); + + it("does not promote a working executable whose digest cannot be read", () => { + const fixture = createFixture(); + const installed = runShell( + [ + "set -eu", + "real_sha=$(command -v sha256sum)", + `mkdir -p ${sh(`${fixture.work}/bin`)}`, + `printf '#!/bin/sh\\ncase "$1" in */t3) exit 1;; esac\\nexec %s "$@"\\n' "$real_sha" > ${sh(`${fixture.work}/bin/sha256sum`)}`, + `chmod +x ${sh(`${fixture.work}/bin/sha256sum`)}`, + `export PATH=${sh(`${fixture.work}/bin`)}:$PATH`, + fixture.installScript(), + ].join("\n"), + ); + + expect(installed.status).not.toBe(0); + expect(installed.stderr).toContain("Could not hash the WSL runtime server entry"); + expect(parseWslRuntimeRoot(installed.stdout)).toBeNull(); + expect(runShell(`[ ! -e ${sh(fixture.runtimeRoot)} ]`).status).toBe(0); + }); + it("falls back instead of launching a corrupted cache it cannot reinstall", () => { const fixture = createFixture(); expect(fixture.install().status).toBe(0); diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.ts index 7e00f1b40ad1..b8c1f5ce22b0 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.ts @@ -24,7 +24,6 @@ const TOOLCHAIN_TIMEOUT = Duration.seconds(10); const BUILD_TIMEOUT = Duration.minutes(5); const RUNTIME_INSTALL_TIMEOUT = Duration.minutes(2); const RUNTIME_PRUNE_TIMEOUT = Duration.seconds(30); -const RUNTIME_INVALIDATE_TIMEOUT = Duration.seconds(15); const USER_HOME_TIMEOUT = Duration.seconds(5); const TOOLCHAIN_TRANSPORT_RETRY_LIMIT = 12; const BUILD_TRANSPORT_RETRY_LIMIT = 2; @@ -44,6 +43,7 @@ export type PrepareWslRuntimeResult = | { readonly ok: true; readonly linuxAppRoot: string; + readonly resolvedPath: string; } | { readonly ok: false; @@ -63,16 +63,6 @@ export type EnsureWslNodePtyResult = readonly retryLimit?: number; }; -export type ProbeWslRuntimeResult = - | { - readonly ok: true; - readonly resolvedPath: string; - } - | { - readonly ok: false; - readonly reason: string; - }; - export class DesktopWslDistroListError extends Schema.TaggedError()( "DesktopWslDistroListError", { reason: Schema.String }, @@ -110,11 +100,6 @@ export class DesktopWslEnvironment extends Context.Service< archive: WslRuntimeArchive, ) => Effect.Effect; readonly pruneRuntimes: (distro: string | null, runtimeId: string) => Effect.Effect; - readonly invalidateRuntime: (distro: string | null, runtimeId: string) => Effect.Effect; - readonly probeRuntime: ( - distro: string | null, - linuxAppRoot: string, - ) => Effect.Effect; readonly ensureNodePty: ( distro: string | null, linuxAppRoot: string, @@ -274,7 +259,8 @@ export const buildWslRuntimeInstallScript = ( `runtime_root="$runtime_parent/${safeRuntimeId}"`, `ready_marker="$runtime_root/${WSL_RUNTIME_READY_MARKER}"`, "runtime_entry_runs() {", - ' [ -x "$1/t3" ] && "$1/t3" --version >/dev/null 2>&1', + // Load the full CLI so missing runtime dependencies cannot pass cache validation. + ' [ -x "$1/t3" ] && "$1/t3" --help >/dev/null 2>&1', "}", // Hashing the entry is what tells a working cache from one whose `t3` was // swapped or half-written after install: the file is still there and may @@ -284,11 +270,14 @@ export const buildWslRuntimeInstallScript = ( ` sha256sum "$1/t3" 2>/dev/null | cut -d ' ' -f 1`, "}", "runtime_is_ready() {", - ' [ -f "$ready_marker" ] &&', - ' runtime_entry_runs "$runtime_root" &&', - ` recorded_entry_digest=$(tr -d '[:space:]' < "$ready_marker" 2>/dev/null) &&`, - ' [ -n "$recorded_entry_digest" ] &&', - ' [ "$recorded_entry_digest" = "$(runtime_server_entry_digest "$runtime_root")" ]', + ' [ -f "$ready_marker" ] && [ -x "$runtime_root/t3" ] || return 1', + ` recorded_entry_digest=$(tr -d '[:space:]' < "$ready_marker" 2>/dev/null) || return 1`, + ' [ -n "$recorded_entry_digest" ] || return 1', + ' runtime_entry_runs "$runtime_root" &', + " runtime_check_pid=$!", + ' current_entry_digest=$(runtime_server_entry_digest "$runtime_root")', + ' wait "$runtime_check_pid" || return 1', + ' [ "$recorded_entry_digest" = "$current_entry_digest" ]', "}", 'mkdir -p "$runtime_parent"', `runtime_lock="$runtime_parent/.${safeRuntimeId}.install.lock"`, @@ -336,11 +325,13 @@ export const buildWslRuntimeInstallScript = ( 'cleanup_runtime_install() { rm -rf "$runtime_tmp"; }', "trap cleanup_runtime_install EXIT", `tar -xzf ${shellQuote(linuxArchivePath)} -C "$runtime_tmp" --strip-components=1`, - 'if ! runtime_entry_runs "$runtime_tmp"; then', + 'runtime_entry_runs "$runtime_tmp" &', + "runtime_check_pid=$!", + 'installed_entry_digest=$(runtime_server_entry_digest "$runtime_tmp")', + 'if ! wait "$runtime_check_pid"; then', " printf 'WSL runtime archive does not contain a working t3 executable\\n' >&2", " exit 1", "fi", - 'installed_entry_digest=$(runtime_server_entry_digest "$runtime_tmp")', 'if [ -z "$installed_entry_digest" ]; then', " printf 'Could not hash the WSL runtime server entry\\n' >&2", " exit 1", @@ -416,18 +407,6 @@ export const buildWslRuntimePruneScript = (runtimeId: string): string => { ].join("\n"); }; -// Readiness is decided inside the install script, so a cached tree that passes -// there but fails the launch-time probe (a distro whose glibc the executable -// needs, a tree copied from another machine) would stay ready forever and fail -// on every launch. -export const buildWslRuntimeInvalidateScript = (runtimeId: string): string => { - const safeRuntimeId = sanitizeWslRuntimeId(runtimeId); - return [ - "set -eu", - `rm -f "$HOME/.t3/wsl-runtime/${safeRuntimeId}/${WSL_RUNTIME_READY_MARKER}"`, - ].join("\n"); -}; - export const parseWslRuntimeRoot = (stdout: string): string | null => { const prefix = "runtimeRoot:"; const line = stdout.split("\n").find((candidate) => candidate.startsWith(prefix)); @@ -479,13 +458,9 @@ if (!candidates.some((candidate) => fs.existsSync(candidate))) process.exit(${NO require("node-pty"); NODE`; -// Provider CLIs may still need version-managed Node, so resolve it before -// capturing PATH without requiring it for runtime readiness. -export const buildWslRuntimeProbeScript = (linuxAppRoot: string) => - [ - `bash -lc ${shellQuote(`${buildWslNodeEnvPreamble()}${RESOLVED_PATH_LINE}`)} 2>/dev/null || ${RESOLVED_PATH_LINE}`, - `${shellQuote(`${linuxAppRoot}/t3`)} --version >/dev/null 2>&1`, - ].join("\n"); +// The standalone runtime supplies Node, but provider CLIs may need the user's PATH. +export const buildWslProviderPathScript = () => + `bash -lc ${shellQuote(`${buildWslNodeEnvPreamble()}${RESOLVED_PATH_LINE}`)} 2>/dev/null || ${RESOLVED_PATH_LINE}`; const TOOLCHAIN_CHECK_SCRIPT = [ "for tool in node make g++ python3; do", @@ -598,43 +573,6 @@ export const formatMissingToolsReason = ( return `WSL distro is missing required tools: ${issues.join(", ")}. Install ${remediations.join(" and ")}, then retry.`; }; -const probeWslRuntimeImpl = ( - distro: string | null, - linuxAppRoot: string, -): Effect.Effect => - Effect.gen(function* () { - const probe = yield* runWslShell( - distro, - buildWslRuntimeProbeScript(linuxAppRoot), - PROBE_TIMEOUT, - { - resolveNode: false, - }, - ); - const transportFailureReason = formatWslShellTransportFailureReason( - probe.transportFailure, - "the staged runtime", - ); - if (transportFailureReason !== null) { - return { ok: false, reason: transportFailureReason } as const; - } - if (probe.exitCode !== 0) { - const trimmedTail = probe.stderr.trim().slice(-500); - return { - ok: false, - reason: `${linuxAppRoot}/t3 --version failed (exit ${probe.exitCode})${trimmedTail ? `: ${trimmedTail}` : ""}`, - } as const; - } - const resolvedPath = parseResolvedPath(probe.stdout); - if (resolvedPath === null) { - return { - ok: false, - reason: "WSL login-shell PATH could not be resolved during backend preflight.", - } as const; - } - return { ok: true, resolvedPath } as const; - }); - const ensureNodePtyImpl = ( distro: string | null, linuxRepoRoot: string, @@ -817,11 +755,21 @@ const prepareWslRuntimeImpl = Effect.fn("desktop.wsl.prepareRuntimeImpl")(functi } as const; } - const install = yield* runWslShell( - distro, - buildWslRuntimeInstallScript(linuxArchivePath.value, archive.runtimeId, archive.sha256), - RUNTIME_INSTALL_TIMEOUT, - { resolveNode: false }, + // PATH discovery is independent of the standalone runtime. Keep its shorter + // timeout while installing/validating the runtime in parallel. + const { install, pathProbe } = yield* Effect.all( + { + install: runWslShell( + distro, + buildWslRuntimeInstallScript(linuxArchivePath.value, archive.runtimeId, archive.sha256), + RUNTIME_INSTALL_TIMEOUT, + { resolveNode: false }, + ), + pathProbe: runWslShell(distro, buildWslProviderPathScript(), PROBE_TIMEOUT, { + resolveNode: false, + }), + }, + { concurrency: 2 }, ); if (install.transportFailure !== null) { return { @@ -840,13 +788,26 @@ const prepareWslRuntimeImpl = Effect.fn("desktop.wsl.prepareRuntimeImpl")(functi } as const; } + const pathFailure = formatWslShellTransportFailureReason( + pathProbe.transportFailure, + "the provider PATH", + ); + if (pathFailure !== null) return { ok: false, reason: pathFailure }; + if (pathProbe.exitCode !== 0) { + return { + ok: false, + reason: `WSL provider PATH discovery failed (exit ${pathProbe.exitCode}).`, + }; + } const linuxAppRoot = parseWslRuntimeRoot(install.stdout); - return linuxAppRoot === null - ? { - ok: false, - reason: "WSL runtime installation completed without reporting its cache path.", - } - : { ok: true, linuxAppRoot }; + const resolvedPath = parseResolvedPath(pathProbe.stdout); + if (linuxAppRoot === null || resolvedPath === null) { + return { + ok: false, + reason: "WSL runtime preparation did not report its cache path and provider PATH.", + }; + } + return { ok: true, linuxAppRoot, resolvedPath }; }); const pruneWslRuntimesImpl = Effect.fn("desktop.wsl.pruneRuntimesImpl")(function* ( @@ -869,26 +830,6 @@ const pruneWslRuntimesImpl = Effect.fn("desktop.wsl.pruneRuntimesImpl")(function }); }); -const invalidateWslRuntimeImpl = Effect.fn("desktop.wsl.invalidateRuntimeImpl")(function* ( - distro: string | null, - runtimeId: string, -): Effect.fn.Return { - const result = yield* runWslShell( - distro, - buildWslRuntimeInvalidateScript(runtimeId), - RUNTIME_INVALIDATE_TIMEOUT, - { resolveNode: false }, - ); - if (result.transportFailure === null && result.exitCode === 0) return; - - const detail = `${result.stdout}${result.stderr}`.trim().slice(-500); - yield* Effect.logWarning("Could not invalidate the staged WSL runtime cache.", { - distro, - runtimeId, - detail: detail || `exit ${result.exitCode}`, - }); -}); - export const probeWslDistros: Effect.Effect< readonly WslDistro[], DesktopWslDistroListError, @@ -1082,8 +1023,6 @@ export interface DesktopWslEnvironmentTestStub { archive: WslRuntimeArchive, ) => PrepareWslRuntimeResult; readonly pruneRuntimes?: (distro: string | null, runtimeId: string) => Effect.Effect; - readonly invalidateRuntime?: (distro: string | null, runtimeId: string) => Effect.Effect; - readonly probeRuntime?: (distro: string | null, linuxAppRoot: string) => ProbeWslRuntimeResult; readonly ensureNodePty?: ( distro: string | null, linuxAppRoot: string, @@ -1113,12 +1052,6 @@ export const layerTest = (stub: DesktopWslEnvironmentTestStub = {}) => { }, ), pruneRuntimes: (distro, runtimeId) => stub.pruneRuntimes?.(distro, runtimeId) ?? Effect.void, - invalidateRuntime: (distro, runtimeId) => - stub.invalidateRuntime?.(distro, runtimeId) ?? Effect.void, - probeRuntime: (distro, linuxAppRoot) => - Effect.succeed( - stub.probeRuntime?.(distro, linuxAppRoot) ?? { ok: true, resolvedPath: "/usr/bin:/bin" }, - ), ensureNodePty: (distro, linuxAppRoot, options) => Effect.succeed( stub.ensureNodePty?.(distro, linuxAppRoot, options) ?? { @@ -1199,14 +1132,6 @@ export const layer = Layer.effect( provideSpawner(pruneWslRuntimesImpl(distro, runtimeId)).pipe( Effect.withSpan("desktop.wsl.pruneRuntimes"), ), - invalidateRuntime: (distro, runtimeId) => - provideSpawner(invalidateWslRuntimeImpl(distro, runtimeId)).pipe( - Effect.withSpan("desktop.wsl.invalidateRuntime"), - ), - probeRuntime: (distro, linuxAppRoot) => - provideSpawner(probeWslRuntimeImpl(distro, linuxAppRoot)).pipe( - Effect.withSpan("desktop.wsl.probeRuntime"), - ), ensureNodePty: (distro, linuxAppRoot, options) => provideSpawner(ensureNodePtyImpl(distro, linuxAppRoot, options)).pipe( Effect.withSpan("desktop.wsl.ensureNodePty"),