From ab1b832e723c05c5cc4aff12d494e1d1470eda00 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 19 Oct 2026 10:48:37 +0000 Subject: [PATCH] fix(desktop): preserve WSL runtime installation errors Rebuilt from #11901 after the organization-owned fork rejected maintainer push access. Co-authored-by: Kalven Schraut --- .../DesktopBackendConfiguration.test.ts | 30 +++++++++++++++++++ .../backend/DesktopBackendConfiguration.ts | 5 ++-- .../src/wsl/DesktopWslEnvironment.test.ts | 20 ++++++++++--- apps/desktop/src/wsl/DesktopWslEnvironment.ts | 4 +-- 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index c0ef57ae993a..ec92473d80a3 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -551,6 +551,7 @@ describe("DesktopBackendConfiguration", () => { archiveHash: "b".repeat(64), wsl: () => ({ prepareRuntime: () => ({ ok: false, reason: "archive is corrupt" }), + invalidateRuntime: () => Effect.die("failed staging must not invalidate a runtime"), ensureNodePty: (_distro, root) => { observedNodePtyRoots.push(root); return { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" }; @@ -615,6 +616,35 @@ describe("DesktopBackendConfiguration", () => { ); }); + it.effect("resolveWsl preserves the staging error when the mounted fallback is fatal", () => + withPackagedWslHarness( + { + archiveHash: "b".repeat(64), + wsl: () => ({ + prepareRuntime: () => ({ + ok: false, + reason: + "t3: error while loading shared libraries: libatomic.so.1: cannot open shared object file", + }), + ensureNodePty: () => ({ + ok: false, + reason: "the packaged Linux node-pty binary was not included", + fatal: true, + }), + }), + }, + () => + Effect.gen(function* () { + const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; + const config = yield* configuration.resolveWsl({ port: 5000, distro: "Ubuntu" }); + const failure = Option.getOrThrow(config.preflightFailure); + + assert.isTrue(failure.fatal); + assert.include(failure.reason, "libatomic.so.1"); + }), + ), + ); + it.effect("resolveWsl keeps the staged runtime when the mounted tree fails too", () => { const stagedAppRoot = "/home/test/.t3/wsl-runtime/cache"; const invalidatedRuntimeIds: string[] = []; diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index d9ff98279718..77a90e0512aa 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 runtimeId?: string; readonly reason: string } | undefined; const failedStaged = (failure: { readonly reason: string }) => ({ _tag: "Failed", @@ -417,6 +417,7 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f ); stagedFailure = { runtimeId: input.runtimeArchive.runtimeId, reason: stagedProbe.reason }; } else { + stagedFailure = { reason: runtime.reason }; yield* Effect.logWarning( "Could not stage the WSL runtime; launching from the mounted server tree instead.", { reason: runtime.reason }, @@ -446,7 +447,7 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f : failedNodePty(nodePtyResult); } - if (stagedFailure) { + if (stagedFailure?.runtimeId) { yield* wslEnv.invalidateRuntime(runningDistro, stagedFailure.runtimeId); } diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts index 0224eca440ea..40dcf180dcac 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts @@ -182,7 +182,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" 2>/dev/null &&'); expect(script).toContain("if runtime_is_ready; then"); expect(script).not.toContain("bin.mjs"); expect(script).not.toContain("node-pty"); @@ -262,7 +262,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" --version >/dev/null'); const entryCheckDefined = script.indexOf("runtime_entry_runs() {"); const readinessDefined = script.indexOf("runtime_is_ready() {"); @@ -374,14 +374,14 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed fixtures.length = 0; }); - const createFixture = () => { + const createFixture = (serverEntry = 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(serverEntry)} > "$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`, @@ -495,6 +495,18 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed expect(parseWslRuntimeRoot(warm.stdout)).toBe(fixture.runtimeRoot); }); + it("retains the loader error when a bundled executable needs a missing system library", () => { + const fixture = createFixture( + '#!/bin/sh\nprintf "error while loading shared libraries: libatomic.so.1: cannot open shared object file\\n" >&2\nexit 127\n', + ); + + const result = fixture.install(); + + expect(result.status).not.toBe(0); + expect(result.stderr).toContain("libatomic.so.1"); + expect(parseWslRuntimeRoot(result.stdout)).toBeNull(); + }); + it("reinstalls a cache whose executable was truncated", () => { 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..128525796ac9 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.ts @@ -274,7 +274,7 @@ 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', + ' [ -x "$1/t3" ] && "$1/t3" --version >/dev/null', "}", // 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 @@ -285,7 +285,7 @@ export const buildWslRuntimeInstallScript = ( "}", "runtime_is_ready() {", ' [ -f "$ready_marker" ] &&', - ' runtime_entry_runs "$runtime_root" &&', + ' runtime_entry_runs "$runtime_root" 2>/dev/null &&', ` recorded_entry_digest=$(tr -d '[:space:]' < "$ready_marker" 2>/dev/null) &&`, ' [ -n "$recorded_entry_digest" ] &&', ' [ "$recorded_entry_digest" = "$(runtime_server_entry_digest "$runtime_root")" ]',