diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts index 145b82e3948f..b9682350d901 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.test.ts @@ -22,6 +22,7 @@ import { parseResolvedPath, parseToolchainReport, probeWslDistros, + WSL_SCRIPT_SHELL_ARGS, } from "./DesktopWslEnvironment.ts"; const encoder = new TextEncoder(); @@ -112,6 +113,12 @@ describe("formatWslShellTransportFailureReason", () => { }); }); +describe("WSL scripted shell transport", () => { + it("loads login state then executes stdin in a non-login child", () => { + expect(WSL_SCRIPT_SHELL_ARGS).toEqual(["--", "bash", "-l", "-c", "exec bash -s"]); + }); +}); + describe("buildWslNodeEnvPreamble", () => { it("passes the required Node engine range into the shared resolver", () => { const preamble = buildWslNodeEnvPreamble("^22.16 || ^23.11 || >=24.10"); diff --git a/apps/desktop/src/wsl/DesktopWslEnvironment.ts b/apps/desktop/src/wsl/DesktopWslEnvironment.ts index 96b382e41f77..c060fa7e3f66 100644 --- a/apps/desktop/src/wsl/DesktopWslEnvironment.ts +++ b/apps/desktop/src/wsl/DesktopWslEnvironment.ts @@ -113,6 +113,10 @@ export class DesktopWslEnvironment extends Context.Service< const buildDistroArgs = (distro: string | null): ReadonlyArray => distro ? ["-d", distro] : []; +// Load the user's login profile, then execute the scripted stdin in a +// non-login child so the profile's logout hook cannot rewrite its exit code. +export const WSL_SCRIPT_SHELL_ARGS = ["--", "bash", "-l", "-c", "exec bash -s"] as const; + const concatChunks = (arrays: ReadonlyArray): Uint8Array => { let totalLength = 0; for (const arr of arrays) totalLength += arr.byteLength; @@ -174,11 +178,13 @@ const runWslScript = ( timeout: Duration.Duration, ): Effect.Effect => { const spawner = ChildProcessSpawner.ChildProcessSpawner; - // -l picks up profile-managed shell state. -s makes bash read the script - // from stdin so wsl.exe never has to re-escape the script as an argument. + // Load profile-managed shell state in a login shell, then replace it with a + // non-login child that reads stdin. This preserves exported profile state + // without running ~/.bash_logout, whose status can rewrite a successful + // scripted launch when bash exits. const command = ChildProcess.make( "wsl.exe", - [...buildDistroArgs(distro), "--", "bash", "-l", "-s"], + [...buildDistroArgs(distro), ...WSL_SCRIPT_SHELL_ARGS], { stdin: Stream.encodeText(Stream.make(bashScript)), stdout: "pipe",