Skip to content
Closed
109 changes: 100 additions & 9 deletions apps/desktop/src/backend/DesktopBackendConfiguration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,21 @@ describe("DesktopBackendConfiguration", () => {
const entryPath = path.join(baseDir, "app.asar.unpacked/apps/server/dist/bin.mjs");
yield* fileSystem.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fileSystem.writeFileString(entryPath, "");
const archiveHash = "a".repeat(64);
yield* fileSystem.writeFileString(
path.join(baseDir, "wsl-runtime.tar.gz.sha256"),
archiveHash,
);

const observedDistros: Array<string | null> = [];
const observedWindowsConversions: string[] = [];
const observedRuntimePreparations: Array<{
readonly distro: string | null;
readonly windowsArchivePath: string;
readonly archiveHash: string;
}> = [];
const observedNodePtyRoots: string[] = [];
const linuxRepoRoot = `/home/test/.cache/t3code/desktop-wsl-runtime/sha256-${archiveHash}`;
const config = yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
return yield* configuration.resolveWsl({ port: 5000, distro: null });
Expand All @@ -193,12 +206,22 @@ 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, windowsArchivePath, preparedArchiveHash) => {
observedDistros.push(distro);
return Option.some("/repo/apps/server/dist/bin.mjs");
observedRuntimePreparations.push({
distro,
windowsArchivePath,
archiveHash: preparedArchiveHash,
});
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 +244,73 @@ 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",
windowsArchivePath: path.join(baseDir, "wsl-runtime.tar.gz"),
archiveHash,
},
]);
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)),
);

it.effect("resolveWsl falls back to the mounted runtime when native staging fails", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-backend-config-test-",
});
const entryPath = path.join(baseDir, "app.asar.unpacked/apps/server/dist/bin.mjs");
yield* fileSystem.makeDirectory(path.dirname(entryPath), { recursive: true });
yield* fileSystem.writeFileString(entryPath, "");
yield* fileSystem.writeFileString(
path.join(baseDir, "wsl-runtime.tar.gz.sha256"),
`${"b".repeat(64)}\n`,
);

const observedNodePtyRoots: string[] = [];
const config = yield* Effect.gen(function* () {
const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration;
return yield* configuration.resolveWsl({ port: 5001, distro: "Ubuntu" });
}).pipe(
Effect.provide(
DesktopBackendConfiguration.layer.pipe(
Layer.provideMerge(serverExposureLayer),
Layer.provideMerge(DesktopAppSettings.layerTest()),
Layer.provideMerge(
DesktopWslEnvironment.layerTest({
isAvailable: true,
distros: [{ name: "Ubuntu", isDefault: true, version: 2 }],
preparePackagedRuntime: () => ({
ok: false,
reason: "archive extraction failed",
}),
windowsToWslPath: () => Option.some("/mnt/c/t3code"),
ensureNodePty: (_distro, root) => {
observedNodePtyRoots.push(root);
return { ok: true, nodePath: "/usr/bin/node", resolvedPath: "/usr/bin:/bin" };
},
getDistroIp: () => Option.none(),
}),
),
Layer.provideMerge(
makeEnvironmentLayer(baseDir, {
appPath: baseDir,
platform: "win32",
resourcesPath: baseDir,
}),
),
),
),
);

assert.deepEqual(observedNodePtyRoots, ["/mnt/c/t3code"]);
assert.include(config.args, "/mnt/c/t3code/apps/server/dist/bin.mjs");
assert.isTrue(Option.isNone(config.preflightFailure));
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);
Expand All @@ -234,12 +324,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 +346,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
78 changes: 67 additions & 11 deletions apps/desktop/src/backend/DesktopBackendConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
import * as DesktopServerExposure from "./DesktopServerExposure.ts";
import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts";
import {
parseWslRuntimeArchiveHash,
WSL_RUNTIME_ARCHIVE_FILENAME,
WSL_RUNTIME_ARCHIVE_HASH_FILENAME,
} from "@t3tools/shared/wslRuntimeArchive";

export class DesktopBackendObservabilitySettingsReadError extends Schema.TaggedErrorClass<DesktopBackendObservabilitySettingsReadError>()(
"DesktopBackendObservabilitySettingsReadError",
Expand Down Expand Up @@ -238,6 +243,11 @@ const WSL_TRANSIENT_PREFLIGHT_RETRY_LIMIT = 12;

const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(function* (input: {
readonly distro: string | null;
readonly isPackaged: boolean;
readonly packagedRuntime: {
readonly windowsArchivePath: string;
readonly archiveHash: string;
} | null;
readonly windowsEntryPath: string;
readonly windowsRepoRoot: string;
readonly allowBuild: boolean;
Expand Down Expand Up @@ -299,16 +309,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 | null = null;
if (input.isPackaged && input.packagedRuntime !== null) {
const prepared = yield* wslEnv.preparePackagedRuntime(
runningDistro,
input.packagedRuntime.windowsArchivePath,
input.packagedRuntime.archiveHash,
);
if (!prepared.ok) {
yield* Effect.logWarning(
`Could not prepare the Linux-native WSL runtime; using the packaged runtime from its Windows mount instead. ${prepared.reason}`,
);
} else {
linuxRepoRoot = prepared.linuxRepoRoot;
}
}

if (linuxRepoRoot === null) {
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 +354,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 @@ -428,6 +458,7 @@ const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl
> {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment;
const fileSystem = yield* FileSystem.FileSystem;

// Bind to 0.0.0.0 inside WSL so the backend is reachable both via
// WSL2's automatic localhost forwarding (wslhost: Windows 127.0.0.1
Expand Down Expand Up @@ -469,15 +500,40 @@ 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 runtimeArchivePath = environment.path.join(
environment.resourcesPath,
WSL_RUNTIME_ARCHIVE_FILENAME,
);
const runtimeArchiveHashPath = environment.path.join(
environment.resourcesPath,
WSL_RUNTIME_ARCHIVE_HASH_FILENAME,
);
const runtimeArchiveHash = environment.isPackaged
? yield* fileSystem.readFileString(runtimeArchiveHashPath).pipe(
Effect.map(parseWslRuntimeArchiveHash),
Effect.orElseSucceed(() => null),
)
: null;
if (environment.isPackaged && runtimeArchiveHash === null) {
yield* Effect.logWarning(
"The packaged WSL runtime archive identity is missing or invalid; using the existing Windows-mounted runtime.",
);
}

const preflight = yield* runWslPreflight({
distro: input.distro,
isPackaged: environment.isPackaged,
packagedRuntime:
environment.isPackaged && runtimeArchiveHash !== null
? { windowsArchivePath: runtimeArchivePath, archiveHash: runtimeArchiveHash }
: null,
windowsEntryPath: wslEntryPath,
windowsRepoRoot: wslAppRoot,
// Packaged builds ship a prebuilt Linux node-pty (built on Linux in CI and
Expand Down
Loading
Loading