Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 14 additions & 41 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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");
Expand All @@ -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"),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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" };
Expand All @@ -602,32 +593,23 @@ 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`);
assert.notInclude(config.args, `${stagedAppRoot}/t3`);
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",
}),
Expand All @@ -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.",
Expand All @@ -680,7 +654,6 @@ describe("DesktopBackendConfiguration", () => {
assert.isFalse(failure.fatal);
assert.equal(failure.retryLimit, 12);
assert.include(failure.reason, "timed out");
assert.deepEqual(invalidatedRuntimeIds, []);
}),
);
});
Expand Down
37 changes: 12 additions & 25 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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 },
Expand All @@ -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,
Expand Down
84 changes: 62 additions & 22 deletions apps/desktop/src/wsl/DesktopWslEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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");
Expand All @@ -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"');

Expand Down Expand Up @@ -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() {");
Expand All @@ -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"`,
);
Expand All @@ -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"');
Expand Down Expand Up @@ -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)", () => {
Expand All @@ -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`,
Expand Down Expand Up @@ -431,7 +420,7 @@ describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed
" }",
' eval "$2"',
"); }",
buildWslRuntimeProbeScript(fixture.runtimeRoot),
buildWslProviderPathScript(),
].join("\n"),
);

Expand Down Expand Up @@ -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"),
);

Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading