Skip to content
Open
43 changes: 34 additions & 9 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ describe("DesktopBackendConfiguration", () => {
yield* fileSystem.writeFileString(entryPath, "");

const observedDistros: Array<string | null> = [];
const observedWindowsConversions: string[] = [];
const observedRuntimePreparations: Array<{
readonly distro: string | null;
readonly windowsRepoRoot: string;
readonly runtimeId: string;
}> = [];
const observedNodePtyRoots: string[] = [];
const linuxRepoRoot = "/home/test/.cache/t3code/desktop-wsl-runtime/current";
const config = yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
return yield* configuration.resolveWsl({ port: 5000, distro: null });
Expand All @@ -193,12 +201,18 @@ describe("DesktopBackendConfiguration", () => {
{ name: "Debian", isDefault: false, version: 2 },
{ name: "Ubuntu", isDefault: true, version: 2 },
],
windowsToWslPath: (distro) => {
windowsToWslPath: (_distro, windowsPath) => {
observedWindowsConversions.push(windowsPath);
return Option.some("/mnt/c/t3code");
},
preparePackagedRuntime: (distro, windowsRepoRoot, runtimeId) => {
observedDistros.push(distro);
return Option.some("/repo/apps/server/dist/bin.mjs");
observedRuntimePreparations.push({ distro, windowsRepoRoot, runtimeId });
return { ok: true, linuxRepoRoot };
},
ensureNodePty: (distro) => {
ensureNodePty: (distro, root) => {
observedDistros.push(distro);
observedNodePtyRoots.push(root);
return { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" };
},
getDistroIp: (distro) => {
Expand All @@ -221,6 +235,16 @@ describe("DesktopBackendConfiguration", () => {
assert.equal(config.runningDistro, "Ubuntu");
assert.deepEqual(config.args.slice(0, 2), ["-d", "Ubuntu"]);
assert.deepEqual(observedDistros, ["Ubuntu", "Ubuntu", "Ubuntu"]);
assert.deepEqual(observedWindowsConversions, []);
assert.deepEqual(observedRuntimePreparations, [
{
distro: "Ubuntu",
windowsRepoRoot: path.join(baseDir, "app.asar.unpacked"),
runtimeId: "1.2.3-x64",
},
]);
assert.deepEqual(observedNodePtyRoots, [linuxRepoRoot]);
assert.include(config.args, `${linuxRepoRoot}/apps/server/dist/bin.mjs`);
assert.isTrue(Option.isNone(config.preflightFailure));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand All @@ -234,12 +258,14 @@ describe("DesktopBackendConfiguration", () => {
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-config-test-",
});
const entryPath = path.join(baseDir, "app.asar.unpacked/apps/server/dist/bin.mjs");
const dirname = path.join(baseDir, "apps/desktop/src");
const entryPath = path.join(baseDir, "apps/server/dist/bin.mjs");
yield* fileSystem.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fileSystem.writeFileString(entryPath, "");

const nodePath = "/home/test user's/.nvm/versions/node/v22.0.0/bin/node";
const linuxEntryPath = "/tmp/t3 code's launch/entry file.mjs";
const linuxRepoRoot = "/tmp/t3 code's launch";
const linuxEntryPath = `${linuxRepoRoot}/apps/server/dist/bin.mjs`;
const resolvedPath = "/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin";
const devServerUrl = "http://127.0.0.1:5733/dev%20assets/?label=hello%20world";
const config = yield* Effect.gen(function* () {
Expand All @@ -254,18 +280,17 @@ describe("DesktopBackendConfiguration", () => {
DesktopWslEnvironment.layerTest({
isAvailable: true,
distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],
windowsToWslPath: () => Option.some(linuxEntryPath),
windowsToWslPath: () => Option.some(linuxRepoRoot),
ensureNodePty: () => ({ ok: true, nodePath, resolvedPath }),
getDistroIp: () => Option.some("172.27.0.99"),
}),
),
Layer.provideMerge(
makeEnvironmentLayer(baseDir, {
appPath: baseDir,
dirname,
devServerUrl,
isPackaged: true,
isPackaged: false,
platform: "win32",
resourcesPath: baseDir,
}),
),
),
Expand Down
47 changes: 36 additions & 11 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,8 @@ const WSL_TRANSIENT_PREFLIGHT_RETRY_LIMIT = 12;

const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(function* (input: {
readonly distro: string | null;
readonly isPackaged: boolean;
readonly runtimeId: string;
readonly windowsEntryPath: string;
readonly windowsRepoRoot: string;
readonly allowBuild: boolean;
Expand Down Expand Up @@ -299,16 +301,36 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f
} as const;
}

const linuxEntry = yield* wslEnv.windowsToWslPath(runningDistro, input.windowsEntryPath);
if (Option.isNone(linuxEntry)) {
return {
_tag: "Failed",
reason: `wslpath conversion failed for ${input.windowsEntryPath}`,
fatal: false,
} as const;
let linuxRepoRoot: string;
if (input.isPackaged) {
const prepared = yield* wslEnv.preparePackagedRuntime(
runningDistro,
input.windowsRepoRoot,
input.runtimeId,
);
if (!prepared.ok) {
return {
_tag: "Failed",
reason: prepared.reason,
fatal: prepared.fatal,
...(prepared.retryLimit === undefined ? {} : { retryLimit: prepared.retryLimit }),
} as const;
}
linuxRepoRoot = prepared.linuxRepoRoot;
} else {
const convertedRepoRoot = yield* wslEnv.windowsToWslPath(runningDistro, input.windowsRepoRoot);
if (Option.isNone(convertedRepoRoot)) {
return {
_tag: "Failed",
reason: `wslpath conversion failed for ${input.windowsRepoRoot}`,
fatal: false,
} as const;
}
linuxRepoRoot = convertedRepoRoot.value;
}

const nodePtyResult = yield* wslEnv.ensureNodePty(runningDistro, input.windowsRepoRoot, {
const linuxEntryPath = `${linuxRepoRoot.replace(/\/+$/, "")}/apps/server/dist/bin.mjs`;
const nodePtyResult = yield* wslEnv.ensureNodePty(runningDistro, linuxRepoRoot, {
allowBuild: input.allowBuild,
nodeEngineRange: serverPackageJson.engines.node,
});
Expand All @@ -324,7 +346,7 @@ const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(f
return {
_tag: "Ready",
runningDistro,
linuxEntryPath: linuxEntry.value,
linuxEntryPath,
nodePath: nodePtyResult.nodePath,
resolvedPath: nodePtyResult.resolvedPath,
} as const;
Expand Down Expand Up @@ -469,15 +491,18 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
// ELECTRON_RUN_AS_NODE (asar-aware), but the WSL backend launches plain
// `wsl.exe -- node`, which can't read inside an asar. electron-builder unpacks
// the server bundle + node-pty (see asarUnpack in build-desktop-artifact.ts)
// to the app.asar.unpacked sibling, so point WSL there. In dev appRoot is
// already a real directory, so this is a no-op.
// to the app.asar.unpacked sibling. That directory is the packaged staging
// source; preflight copies it to WSL's native filesystem and launches there.
// In dev appRoot is already a real checkout, so preflight uses it in place.
const wslAppRoot = environment.isPackaged
? environment.path.join(environment.resourcesPath, "app.asar.unpacked")
: environment.appRoot;
const wslEntryPath = environment.path.join(wslAppRoot, "apps/server/dist/bin.mjs");

const preflight = yield* runWslPreflight({
distro: input.distro,
isPackaged: environment.isPackaged,
runtimeId: `${environment.appVersion}-${environment.processArch}`,
windowsEntryPath: wslEntryPath,
windowsRepoRoot: wslAppRoot,
// Packaged builds ship a prebuilt Linux node-pty (built on Linux in CI and
Expand Down
73 changes: 73 additions & 0 deletions apps/desktop/src/wsl/DesktopWslEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,19 @@ import * as TestClock from "effect/testing/TestClock";
import { ChildProcessSpawner } from "effect/unstable/process";

import {
buildPackagedRuntimeStageScript,
buildWslNodeEnvPreamble,
DesktopWslDistroListError,
formatMissingToolsReason,
formatNodePtyProbeFailureReason,
formatPackagedRuntimeStageFailure,
formatWslShellTransportFailureReason,
parseNodePath,
parseNodeVersion,
parseResolvedPath,
parseToolchainReport,
probeWslDistros,
WSL_SCRIPT_SHELL_ARGS,
} from "./DesktopWslEnvironment.ts";

const encoder = new TextEncoder();
Expand Down Expand Up @@ -110,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");
Expand All @@ -125,6 +134,70 @@ describe("buildWslNodeEnvPreamble", () => {
});
});

describe("buildPackagedRuntimeStageScript", () => {
it("checks the Linux-native cache before converting or reading the mounted source", () => {
const script = buildPackagedRuntimeStageScript(
"C:\\Program Files\\T3 Code\\resources\\app.asar.unpacked",
"1.2.3-x64",
);
const cacheHit = script.indexOf('if [ "$(cat "$manifest_path"');
const sourceConversion = script.indexOf("wslpath -u");

expect(cacheHit).toBeGreaterThanOrEqual(0);
expect(sourceConversion).toBeGreaterThan(cacheHit);
expect(script.slice(cacheHit, sourceConversion)).toContain(
'runtimeRoot:%s\\n\' "$current_dir"',
);
expect(script.slice(cacheHit, sourceConversion)).toContain("exit 0");
});

it("falls back from an XDG cache on a Windows-mounted filesystem", () => {
const script = buildPackagedRuntimeStageScript(
"C:\\Program Files\\T3 Code\\resources\\app.asar.unpacked",
"1.2.3-x64",
);
const xdgSelection = script.indexOf('cache_home="$XDG_CACHE_HOME"');
const filesystemProbe = script.indexOf('findmnt -T "$cache_home"');
const windowsMountFallback = script.indexOf("9p|drvfs|plan9|virtio-plan9|virtiofs");
const runtimeBase = script.indexOf('runtime_base="$cache_home/t3code/desktop-wsl-runtime"');

expect(filesystemProbe).toBeGreaterThan(xdgSelection);
expect(windowsMountFallback).toBeGreaterThan(filesystemProbe);
expect(runtimeBase).toBeGreaterThan(windowsMountFallback);
expect(script.slice(filesystemProbe, runtimeBase)).toContain(
'cache_home="${HOME:?WSL home directory is unavailable}/.cache"',
);
});

it("serializes cache misses and rechecks the cache before reading the mounted source", () => {
const script = buildPackagedRuntimeStageScript(
"C:\\Program Files\\T3 Code\\resources\\app.asar.unpacked",
"1.2.3-x64",
);
const firstCacheCheck = script.indexOf('if [ "$(cat "$manifest_path"');
const lock = script.indexOf("flock -x 9");
const secondCacheCheck = script.indexOf('if [ "$(cat "$manifest_path"', firstCacheCheck + 1);
const sourceConversion = script.indexOf("wslpath -u");

expect(lock).toBeGreaterThan(firstCacheCheck);
expect(secondCacheCheck).toBeGreaterThan(lock);
expect(sourceConversion).toBeGreaterThan(secondCacheCheck);
expect(script).toContain('if ! source_root=$(wslpath -u "$windows_repo_root"); then');
expect(script.slice(sourceConversion)).toContain("exit 5");
});
});

describe("formatPackagedRuntimeStageFailure", () => {
it("keeps a packaged wslpath failure retryable while WSL starts", () => {
expect(formatPackagedRuntimeStageFailure(5, "wslpath conversion failed")).toEqual({
ok: false,
reason: "wslpath conversion failed",
fatal: false,
retryLimit: 12,
});
});
});

describe("parseToolchainReport", () => {
it("returns no missing tools and no node version on empty output", () => {
expect(parseToolchainReport("")).toEqual({ missingTools: [], nodeVersion: null });
Expand Down
Loading
Loading