import * as NodeOS from "node:os"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings";import * as Context from "effect/Context";import * as Crypto from "effect/Crypto";import * as Effect from "effect/Effect";import * as Encoding from "effect/Encoding";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as PlatformError from "effect/PlatformError";import * as Schema from "effect/Schema";import * as SynchronizedRef from "effect/SynchronizedRef"; import serverPackageJson from "../../../server/package.json" with { type: "json" }; import * as DesktopBackendManager from "./DesktopBackendManager.ts";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"; export class DesktopBackendObservabilitySettingsReadError extends Schema.TaggedErrorClass<DesktopBackendObservabilitySettingsReadError>()( "DesktopBackendObservabilitySettingsReadError", { settingsPath: Schema.String, cause: Schema.Defect(), },) { override get message(): string { return `Failed to read persisted backend observability settings at ${this.settingsPath}.`; }} export class DesktopBackendConfiguration extends Context.Service< DesktopBackendConfiguration, { // Build the Windows-native primary backend's start config. Reads the // primary's port/host/exposure from DesktopServerExposure. Can fail // with PlatformError because bootstrap token generation now uses // crypto.randomBytes under the hood (post Effect 4 migration). readonly resolvePrimary: Effect.Effect< DesktopBackendManager.DesktopBackendStartConfig, PlatformError.PlatformError >; // Build a WSL backend start config for the given distro on the given // port. The WSL backend is always loopback-only (the primary owns LAN // exposure when the user opts in), so this takes the port directly and // hardcodes 127.0.0.1. Distro=null means "WSL default distro" and is // forwarded to wsl.exe with no -d flag. readonly resolveWsl: (input: { readonly port: number; readonly distro: string | null; }) => Effect.Effect< DesktopBackendManager.DesktopBackendStartConfig, PlatformError.PlatformError >; // The renderer-facing label for the primary instance, derived from the // same decision resolvePrimary makes (including the WSL-availability // fall-back to Windows), so the env switcher can't show "WSL" for a // backend that actually resolved to Windows. readonly resolvePrimaryLabel: Effect.Effect<string>; }>()("@t3tools/desktop/backend/DesktopBackendConfiguration") {} interface BackendObservabilitySettings { readonly otlpTracesUrl: Option.Option<string>; readonly otlpMetricsUrl: Option.Option<string>;} const emptyBackendObservabilitySettings: BackendObservabilitySettings = { otlpTracesUrl: Option.none(), otlpMetricsUrl: Option.none(),}; const DESKTOP_BACKEND_ENV_NAMES = [ "T3CODE_PORT", "T3CODE_MODE", "T3CODE_NO_BROWSER", "T3CODE_HOST", "T3CODE_DESKTOP_WS_URL", "T3CODE_DESKTOP_LAN_ACCESS", "T3CODE_DESKTOP_LAN_HOST", "T3CODE_DESKTOP_HTTPS_ENDPOINTS", "T3CODE_TAILSCALE_SERVE", "T3CODE_TAILSCALE_SERVE_PORT",] as const; // Sensitive env vars that the WSL backend needs but Windows process.env won't// forward across the wsl.exe boundary without WSLENV. The dev-server URL is// handled separately via a `--dev-url` CLI flag because WSLENV translation of// URL-shaped values (colons / slashes) is unreliable.const WSL_FORWARDED_ENV_NAMES = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] as const; const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const backendChildEnvPatch = (): Record<string, string | undefined> => Object.fromEntries(DESKTOP_BACKEND_ENV_NAMES.map((name) => [name, undefined])); const getWslEnvEntryName = (entry: string): string => { const slashIndex = entry.indexOf("/"); return slashIndex === -1 ? entry : entry.slice(0, slashIndex);}; const mergeWslEnv = ( existingWslEnv: string | undefined, forwardedEnvNames: ReadonlyArray<string>,): string | undefined => { const existing = existingWslEnv?.trim() ?? ""; // Names already declared, so we don't forward a duplicate. We parse the // existing value only for this membership test — the string itself is // preserved verbatim below rather than re-serialized. const seenNames = new Set( existing .split(":") .map((entry) => getWslEnvEntryName(entry.trim())) .filter((name) => name.length > 0), ); const additions = forwardedEnvNames.filter((name) => !seenNames.has(name)); // Preserve the user's WSLENV exactly as Windows handed it to us — empty // "::" segments and duplicate entries are harmless no-ops to WSL and not // ours to normalize — and only append the secrets we need to forward // across the wsl.exe boundary. const parts = [existing, ...additions].filter((part) => part.length > 0); return parts.length > 0 ? parts.join(":") : undefined;}; const logBackendObservabilitySettingsReadFailure = ( settingsPath: string, cause: PlatformError.PlatformError,) => { const error = new DesktopBackendObservabilitySettingsReadError({ settingsPath, cause }); return Effect.logWarning(error).pipe( Effect.annotateLogs({ component: "desktop-backend-configuration", error, }), );}; const readPersistedBackendObservabilitySettings = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const environment = yield* DesktopEnvironment.DesktopEnvironment; const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe( Effect.map(Option.some), Effect.catchTags({ PlatformError: (cause) => cause.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : logBackendObservabilitySettingsReadFailure(environment.serverSettingsPath, cause).pipe( Effect.as(Option.none()), ), }), ); if (Option.isNone(raw)) { return emptyBackendObservabilitySettings; } const parsed = parsePersistedServerObservabilitySettings(raw.value); return { otlpTracesUrl: Option.fromNullishOr(parsed.otlpTracesUrl), otlpMetricsUrl: Option.fromNullishOr(parsed.otlpMetricsUrl), };}); interface SharedBootstrapInput { readonly bootstrapToken: string; readonly observabilitySettings: BackendObservabilitySettings;} interface WslPreflightSuccess { readonly _tag: "Ready"; readonly runningDistro: string; readonly linuxEntryPath: string; // Absolute path to the node binary the preflight validated after the shared // remote resolver repaired PATH. The launch must use this exact path so it // doesn't fall through to a different/old node than the one node-pty was // built against. readonly nodePath: string; // PATH captured from the same login shell after the shared resolver loaded // version managers. The launch forwards this value directly without a shell. readonly resolvedPath: string;} interface WslPreflightFailure { readonly _tag: "Failed"; readonly reason: string; // Fatal: the WSL distro is misconfigured (no node, wrong version, missing // build tools) and retrying won't help — surface it and (wsl-only) fall back // to Windows. Non-fatal: transient (WSL not ready yet, wslpath while it // boots), with a bounded window for self-healing before fallback. readonly fatal: boolean; readonly retryLimit?: number;} const WSL_TRANSIENT_PREFLIGHT_RETRY_LIMIT = 12; const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(function* (input: { readonly distro: string | null; readonly windowsEntryPath: string; readonly windowsRepoRoot: string; readonly allowBuild: boolean;}): Effect.fn.Return< WslPreflightSuccess | WslPreflightFailure, never, DesktopWslEnvironment.DesktopWslEnvironment | FileSystem.FileSystem> { const wslEnv = yield* DesktopWslEnvironment.DesktopWslEnvironment; const fileSystem = yield* FileSystem.FileSystem; const wslAvailable = yield* wslEnv.isAvailable; if (!wslAvailable) { return { _tag: "Failed", reason: "WSL is not available on this system", fatal: false, } as const; } const distroProbe = yield* wslEnv.probeDistros.pipe( Effect.map((distros) => ({ _tag: "Success", distros }) as const), Effect.catch((error) => Effect.succeed({ _tag: "Failure", error } as const)), ); if (distroProbe._tag === "Failure") { return { _tag: "Failed", reason: `Unable to list WSL distributions: ${distroProbe.error.message}`, fatal: false, } as const; } const installedDistros = distroProbe.distros; const runningDistro = input.distro ? (installedDistros.find( (installed) => installed.name.toLowerCase() === input.distro?.toLowerCase(), )?.name ?? null) : (installedDistros.find((installed) => installed.isDefault)?.name ?? null); if (runningDistro === null) { return { _tag: "Failed", reason: input.distro ? `WSL distro is not installed: ${input.distro}` : installedDistros.length === 0 ? "WSL has no installed distributions" : "WSL has no default distribution", fatal: true, } as const; } const entryExists = yield* fileSystem .exists(input.windowsEntryPath) .pipe(Effect.orElseSucceed(() => false)); if (!entryExists) { return { _tag: "Failed", reason: `missing server entry at ${input.windowsEntryPath}`, fatal: true, } 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; } const nodePtyResult = yield* wslEnv.ensureNodePty(runningDistro, input.windowsRepoRoot, { allowBuild: input.allowBuild, nodeEngineRange: serverPackageJson.engines.node, }); if (!nodePtyResult.ok) { return { _tag: "Failed", reason: `WSL node-pty unavailable: ${nodePtyResult.reason}`, fatal: nodePtyResult.fatal, ...(nodePtyResult.retryLimit === undefined ? {} : { retryLimit: nodePtyResult.retryLimit }), } as const; } return { _tag: "Ready", runningDistro, linuxEntryPath: linuxEntry.value, nodePath: nodePtyResult.nodePath, resolvedPath: nodePtyResult.resolvedPath, } as const;}); // True when the given IPv4 belongs to a Windows-side network// interface. In WSL2 mirrored mode the distro's eth0 IP equals the// host's, which is the signature we use to detect that mode and// switch the renderer URL to loopback.const isLocalHostIpv4 = (ip: string): boolean => { const interfaces = NodeOS.networkInterfaces(); for (const list of Object.values(interfaces)) { if (!list) continue; for (const entry of list) { // os.networkInterfaces() reports IPv4 `family` as the string "IPv4" on // the Node build Electron ships (41 / Node 22, verified), but some Node // builds report the numeric 4. Normalize to a string so a future runtime // bump can't silently break mirrored-mode detection and leave the // renderer pointed at the distro IP instead of loopback. const family = String(entry.family); if ((family === "IPv4" || family === "4") && entry.address === ip) return true; } } return false;}; const buildObservabilityFragment = (observabilitySettings: BackendObservabilitySettings) => ({ ...Option.match(observabilitySettings.otlpTracesUrl, { onNone: () => ({}), onSome: (otlpTracesUrl) => ({ otlpTracesUrl }), }), ...Option.match(observabilitySettings.otlpMetricsUrl, { onNone: () => ({}), onSome: (otlpMetricsUrl) => ({ otlpMetricsUrl }), }),}); const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolvePrimary")( function* ( input: SharedBootstrapInput, ): Effect.fn.Return< DesktopBackendManager.DesktopBackendStartConfig, never, DesktopEnvironment.DesktopEnvironment | DesktopServerExposure.DesktopServerExposure > { const environment = yield* DesktopEnvironment.DesktopEnvironment; const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const backendExposure = yield* serverExposure.backendConfig; const bootstrap = { mode: "desktop" as const, noBrowser: true, port: backendExposure.port, t3Home: environment.baseDir, host: backendExposure.bindHost, desktopBootstrapToken: input.bootstrapToken, tailscaleServeEnabled: backendExposure.tailscaleServeEnabled, tailscaleServePort: backendExposure.tailscaleServePort, ...buildObservabilityFragment(input.observabilitySettings), }; return { executablePath: process.execPath, args: [environment.backendEntryPath, "--bootstrap-fd", "3"], entryPath: environment.backendEntryPath, cwd: environment.backendCwd, env: { ...backendChildEnvPatch(), ELECTRON_RUN_AS_NODE: "1", }, // Primary wants process.env (PATH, dev-runner's T3CODE_HOME, etc.). extendEnv: true, bootstrap, bootstrapDelivery: "fd3", httpBaseUrl: backendExposure.httpBaseUrl, captureOutput: true, preflightFailure: Option.none(), } satisfies DesktopBackendManager.DesktopBackendStartConfig; },); const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl")(function* ( input: SharedBootstrapInput & { readonly port: number; readonly distro: string | null; },): Effect.fn.Return< DesktopBackendManager.DesktopBackendStartConfig, never, | DesktopEnvironment.DesktopEnvironment | DesktopWslEnvironment.DesktopWslEnvironment | FileSystem.FileSystem> { const environment = yield* DesktopEnvironment.DesktopEnvironment; const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment; // 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 // -> WSL 127.0.0.1) AND via the distro's eth0 IP directly from // Windows. wslhost forwarding is unreliable on some Windows hosts: // the desktop's readiness probe and the renderer's saved-env-style // fetch both saw "Failed to fetch" when the backend only bound to // 127.0.0.1 inside WSL. Binding to 0.0.0.0 plus advertising the // WSL IP as the renderer-visible URL avoids that dependency. // Security-wise this is acceptable for the local-only WSL backend: // the network it exposes on is the WSL-vEthernet network, not the // LAN; the primary owns LAN exposure when the user opts in. const wslBindHost = "0.0.0.0"; const bootstrap = { mode: "desktop" as const, noBrowser: true, port: input.port, // Omit t3Home so the Linux backend uses its own home dir instead of // the Windows-side baseDir (which would be a /mnt/c path and share // the SQLite file with the primary). host: wslBindHost, desktopBootstrapToken: input.bootstrapToken, // PortSchema rejects 0, so when tailscale serve is disabled we still // need a valid number in this slot. The backend reads tailscaleServePort // only when tailscaleServeEnabled is true, so the actual value here is // inert. tailscaleServeEnabled: false, tailscaleServePort: 443, ...buildObservabilityFragment(input.observabilitySettings), }; // In packaged builds environment.appRoot is .../resources/app.asar — an // archive FILE. The Windows primary reads its entry through // 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. 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, windowsEntryPath: wslEntryPath, windowsRepoRoot: wslAppRoot, // Packaged builds ship a prebuilt Linux node-pty (built on Linux in CI and // attached to the Windows artifact — see build-desktop-artifact.ts), so the // WSL backend never needs a compiler, node-gyp, or network on first launch. // Compiling from source is a dev-only convenience: a checkout has no shipped // prebuilt, and developers have the toolchain. In packaged builds we instead // surface a clear diagnostic if the prebuilt can't load (unsupported // arch/distro), rather than silently dropping into a fragile runtime build. allowBuild: !environment.isPackaged, }); // Every operation after preflight uses the same concrete distro. In // default-tracking mode this closes the race where the system default // changes between probing and spawning the backend. const runningDistro = preflight._tag === "Ready" ? preflight.runningDistro : null; const distroForConfig = runningDistro ?? input.distro; // Resolve the selected distro's IPv4 address. In mirrored mode the distro // reports a host interface, so use loopback instead; a failed probe also // falls back to loopback and preserves the previous behavior. const distroIp = yield* wslEnvironment.getDistroIp(distroForConfig); const usesSharedNetworkStack = Option.match(distroIp, { onNone: () => false, onSome: (ip) => isLocalHostIpv4(ip), }); const rendererHost = usesSharedNetworkStack ? "127.0.0.1" : Option.getOrElse(distroIp, () => "127.0.0.1"); const httpBaseUrl = new URL(`http://${rendererHost}:${input.port}`); const distroArgs = distroForConfig ? ["-d", distroForConfig] : []; const forwardedEnv: Record<string, string> = {}; const forwardedEnvNames: string[] = []; for (const name of WSL_FORWARDED_ENV_NAMES) { const value = process.env[name]; if (value !== undefined && value.length > 0) { forwardedEnv[name] = value; forwardedEnvNames.push(name); } } // Build an explicit copy of process.env minus T3CODE_HOME (dev-runner // exports the Windows-side base dir for the primary; if it leaks into // the WSL backend the Linux side ends up sharing C:\Users\...\.t3 via // /mnt/c, which means both backends read/write the same database and // their env-ids collide). const parentEnvWithoutT3Home: Record<string, string | undefined> = {}; for (const [key, value] of Object.entries(process.env)) { if (key === "T3CODE_HOME") continue; parentEnvWithoutT3Home[key] = value; } const wslEnv = mergeWslEnv(parentEnvWithoutT3Home.WSLENV, forwardedEnvNames); const baseConfig = { executablePath: "wsl.exe", entryPath: wslEntryPath, cwd: environment.backendCwd, env: { ...parentEnvWithoutT3Home, ...backendChildEnvPatch(), ...forwardedEnv, ...(wslEnv !== undefined ? { WSLENV: wslEnv } : {}), }, // env is already a complete process.env minus T3CODE_HOME; pass it // verbatim instead of letting the spawner re-merge process.env on top. extendEnv: false, bootstrap, bootstrapDelivery: "stdin" as const, httpBaseUrl,Lines 1–500 of 686. Load more
import * as NodeOS from "node:os"; import { parsePersistedServerObservabilitySettings } from "@t3tools/shared/serverSettings"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Encoding from "effect/Encoding"; import * as FileSystem from "effect/FileSystem"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as PlatformError from "effect/PlatformError"; import * as Schema from "effect/Schema"; import * as SynchronizedRef from "effect/SynchronizedRef"; import serverPackageJson from "../../../server/package.json" with { type: "json" }; import * as DesktopBackendManager from "./DesktopBackendManager.ts"; 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"; export class DesktopBackendObservabilitySettingsReadError extends Schema.TaggedErrorClass<DesktopBackendObservabilitySettingsReadError>()( "DesktopBackendObservabilitySettingsReadError", { settingsPath: Schema.String, cause: Schema.Defect(), }, ) { override get message(): string { return `Failed to read persisted backend observability settings at ${this.settingsPath}.`; } } export class DesktopBackendConfiguration extends Context.Service< DesktopBackendConfiguration, { // Build the Windows-native primary backend's start config. Reads the // primary's port/host/exposure from DesktopServerExposure. Can fail // with PlatformError because bootstrap token generation now uses // crypto.randomBytes under the hood (post Effect 4 migration). readonly resolvePrimary: Effect.Effect< DesktopBackendManager.DesktopBackendStartConfig, PlatformError.PlatformError >; // Build a WSL backend start config for the given distro on the given // port. The WSL backend is always loopback-only (the primary owns LAN // exposure when the user opts in), so this takes the port directly and // hardcodes 127.0.0.1. Distro=null means "WSL default distro" and is // forwarded to wsl.exe with no -d flag. readonly resolveWsl: (input: { readonly port: number; readonly distro: string | null; }) => Effect.Effect< DesktopBackendManager.DesktopBackendStartConfig, PlatformError.PlatformError >; // The renderer-facing label for the primary instance, derived from the // same decision resolvePrimary makes (including the WSL-availability // fall-back to Windows), so the env switcher can't show "WSL" for a // backend that actually resolved to Windows. readonly resolvePrimaryLabel: Effect.Effect<string>; } >()("@t3tools/desktop/backend/DesktopBackendConfiguration") {} interface BackendObservabilitySettings { readonly otlpTracesUrl: Option.Option<string>; readonly otlpMetricsUrl: Option.Option<string>; } const emptyBackendObservabilitySettings: BackendObservabilitySettings = { otlpTracesUrl: Option.none(), otlpMetricsUrl: Option.none(), }; const DESKTOP_BACKEND_ENV_NAMES = [ "T3CODE_PORT", "T3CODE_MODE", "T3CODE_NO_BROWSER", "T3CODE_HOST", "T3CODE_DESKTOP_WS_URL", "T3CODE_DESKTOP_LAN_ACCESS", "T3CODE_DESKTOP_LAN_HOST", "T3CODE_DESKTOP_HTTPS_ENDPOINTS", "T3CODE_TAILSCALE_SERVE", "T3CODE_TAILSCALE_SERVE_PORT", ] as const; // Sensitive env vars that the WSL backend needs but Windows process.env won't // forward across the wsl.exe boundary without WSLENV. The dev-server URL is // handled separately via a `--dev-url` CLI flag because WSLENV translation of // URL-shaped values (colons / slashes) is unreliable. const WSL_FORWARDED_ENV_NAMES = ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"] as const; const WSL_SERVER_SYSTEM_PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"; const backendChildEnvPatch = (): Record<string, string | undefined> => Object.fromEntries(DESKTOP_BACKEND_ENV_NAMES.map((name) => [name, undefined])); const getWslEnvEntryName = (entry: string): string => { const slashIndex = entry.indexOf("/"); return slashIndex === -1 ? entry : entry.slice(0, slashIndex); }; const mergeWslEnv = ( existingWslEnv: string | undefined, forwardedEnvNames: ReadonlyArray<string>, ): string | undefined => { const existing = existingWslEnv?.trim() ?? ""; // Names already declared, so we don't forward a duplicate. We parse the // existing value only for this membership test — the string itself is // preserved verbatim below rather than re-serialized. const seenNames = new Set( existing .split(":") .map((entry) => getWslEnvEntryName(entry.trim())) .filter((name) => name.length > 0), ); const additions = forwardedEnvNames.filter((name) => !seenNames.has(name)); // Preserve the user's WSLENV exactly as Windows handed it to us — empty // "::" segments and duplicate entries are harmless no-ops to WSL and not // ours to normalize — and only append the secrets we need to forward // across the wsl.exe boundary. const parts = [existing, ...additions].filter((part) => part.length > 0); return parts.length > 0 ? parts.join(":") : undefined; }; const logBackendObservabilitySettingsReadFailure = ( settingsPath: string, cause: PlatformError.PlatformError, ) => { const error = new DesktopBackendObservabilitySettingsReadError({ settingsPath, cause }); return Effect.logWarning(error).pipe( Effect.annotateLogs({ component: "desktop-backend-configuration", error, }), ); }; const readPersistedBackendObservabilitySettings = Effect.gen(function* () { const fileSystem = yield* FileSystem.FileSystem; const environment = yield* DesktopEnvironment.DesktopEnvironment; const raw = yield* fileSystem.readFileString(environment.serverSettingsPath).pipe( Effect.map(Option.some), Effect.catchTags({ PlatformError: (cause) => cause.reason._tag === "NotFound" ? Effect.succeed(Option.none()) : logBackendObservabilitySettingsReadFailure(environment.serverSettingsPath, cause).pipe( Effect.as(Option.none()), ), }), ); if (Option.isNone(raw)) { return emptyBackendObservabilitySettings; } const parsed = parsePersistedServerObservabilitySettings(raw.value); return { otlpTracesUrl: Option.fromNullishOr(parsed.otlpTracesUrl), otlpMetricsUrl: Option.fromNullishOr(parsed.otlpMetricsUrl), }; }); interface SharedBootstrapInput { readonly bootstrapToken: string; readonly observabilitySettings: BackendObservabilitySettings; } interface WslPreflightSuccess { readonly _tag: "Ready"; readonly runningDistro: string; readonly linuxEntryPath: string; // Absolute path to the node binary the preflight validated after the shared // remote resolver repaired PATH. The launch must use this exact path so it // doesn't fall through to a different/old node than the one node-pty was // built against. readonly nodePath: string; // PATH captured from the same login shell after the shared resolver loaded // version managers. The launch forwards this value directly without a shell. readonly resolvedPath: string; } interface WslPreflightFailure { readonly _tag: "Failed"; readonly reason: string; // Fatal: the WSL distro is misconfigured (no node, wrong version, missing // build tools) and retrying won't help — surface it and (wsl-only) fall back // to Windows. Non-fatal: transient (WSL not ready yet, wslpath while it // boots), with a bounded window for self-healing before fallback. readonly fatal: boolean; readonly retryLimit?: number; } const WSL_TRANSIENT_PREFLIGHT_RETRY_LIMIT = 12; const runWslPreflight = Effect.fn("desktop.backendConfiguration.wslPreflight")(function* (input: { readonly distro: string | null; readonly windowsEntryPath: string; readonly windowsRepoRoot: string; readonly allowBuild: boolean; }): Effect.fn.Return< WslPreflightSuccess | WslPreflightFailure, never, DesktopWslEnvironment.DesktopWslEnvironment | FileSystem.FileSystem > { const wslEnv = yield* DesktopWslEnvironment.DesktopWslEnvironment; const fileSystem = yield* FileSystem.FileSystem; const wslAvailable = yield* wslEnv.isAvailable; if (!wslAvailable) { return { _tag: "Failed", reason: "WSL is not available on this system", fatal: false, } as const; } const distroProbe = yield* wslEnv.probeDistros.pipe( Effect.map((distros) => ({ _tag: "Success", distros }) as const), Effect.catch((error) => Effect.succeed({ _tag: "Failure", error } as const)), ); if (distroProbe._tag === "Failure") { return { _tag: "Failed", reason: `Unable to list WSL distributions: ${distroProbe.error.message}`, fatal: false, } as const; } const installedDistros = distroProbe.distros; const runningDistro = input.distro ? (installedDistros.find( (installed) => installed.name.toLowerCase() === input.distro?.toLowerCase(), )?.name ?? null) : (installedDistros.find((installed) => installed.isDefault)?.name ?? null); if (runningDistro === null) { return { _tag: "Failed", reason: input.distro ? `WSL distro is not installed: ${input.distro}` : installedDistros.length === 0 ? "WSL has no installed distributions" : "WSL has no default distribution", fatal: true, } as const; } const entryExists = yield* fileSystem .exists(input.windowsEntryPath) .pipe(Effect.orElseSucceed(() => false)); if (!entryExists) { return { _tag: "Failed", reason: `missing server entry at ${input.windowsEntryPath}`, fatal: true, } 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; } const nodePtyResult = yield* wslEnv.ensureNodePty(runningDistro, input.windowsRepoRoot, { allowBuild: input.allowBuild, nodeEngineRange: serverPackageJson.engines.node, }); if (!nodePtyResult.ok) { return { _tag: "Failed", reason: `WSL node-pty unavailable: ${nodePtyResult.reason}`, fatal: nodePtyResult.fatal, ...(nodePtyResult.retryLimit === undefined ? {} : { retryLimit: nodePtyResult.retryLimit }), } as const; } return { _tag: "Ready", runningDistro, linuxEntryPath: linuxEntry.value, nodePath: nodePtyResult.nodePath, resolvedPath: nodePtyResult.resolvedPath, } as const; }); // True when the given IPv4 belongs to a Windows-side network // interface. In WSL2 mirrored mode the distro's eth0 IP equals the // host's, which is the signature we use to detect that mode and // switch the renderer URL to loopback. const isLocalHostIpv4 = (ip: string): boolean => { const interfaces = NodeOS.networkInterfaces(); for (const list of Object.values(interfaces)) { if (!list) continue; for (const entry of list) { // os.networkInterfaces() reports IPv4 `family` as the string "IPv4" on // the Node build Electron ships (41 / Node 22, verified), but some Node // builds report the numeric 4. Normalize to a string so a future runtime // bump can't silently break mirrored-mode detection and leave the // renderer pointed at the distro IP instead of loopback. const family = String(entry.family); if ((family === "IPv4" || family === "4") && entry.address === ip) return true; } } return false; }; const buildObservabilityFragment = (observabilitySettings: BackendObservabilitySettings) => ({ ...Option.match(observabilitySettings.otlpTracesUrl, { onNone: () => ({}), onSome: (otlpTracesUrl) => ({ otlpTracesUrl }), }), ...Option.match(observabilitySettings.otlpMetricsUrl, { onNone: () => ({}), onSome: (otlpMetricsUrl) => ({ otlpMetricsUrl }), }), }); const resolvePrimaryStartConfig = Effect.fn("desktop.backendConfiguration.resolvePrimary")( function* ( input: SharedBootstrapInput, ): Effect.fn.Return< DesktopBackendManager.DesktopBackendStartConfig, never, DesktopEnvironment.DesktopEnvironment | DesktopServerExposure.DesktopServerExposure > { const environment = yield* DesktopEnvironment.DesktopEnvironment; const serverExposure = yield* DesktopServerExposure.DesktopServerExposure; const backendExposure = yield* serverExposure.backendConfig; const bootstrap = { mode: "desktop" as const, noBrowser: true, port: backendExposure.port, t3Home: environment.baseDir, host: backendExposure.bindHost, desktopBootstrapToken: input.bootstrapToken, tailscaleServeEnabled: backendExposure.tailscaleServeEnabled, tailscaleServePort: backendExposure.tailscaleServePort, ...buildObservabilityFragment(input.observabilitySettings), }; return { executablePath: process.execPath, args: [environment.backendEntryPath, "--bootstrap-fd", "3"], entryPath: environment.backendEntryPath, cwd: environment.backendCwd, env: { ...backendChildEnvPatch(), ELECTRON_RUN_AS_NODE: "1", }, // Primary wants process.env (PATH, dev-runner's T3CODE_HOME, etc.). extendEnv: true, bootstrap, bootstrapDelivery: "fd3", httpBaseUrl: backendExposure.httpBaseUrl, captureOutput: true, preflightFailure: Option.none(), } satisfies DesktopBackendManager.DesktopBackendStartConfig; }, ); const resolveWslStartConfig = Effect.fn("desktop.backendConfiguration.resolveWsl")(function* ( input: SharedBootstrapInput & { readonly port: number; readonly distro: string | null; }, ): Effect.fn.Return< DesktopBackendManager.DesktopBackendStartConfig, never, | DesktopEnvironment.DesktopEnvironment | DesktopWslEnvironment.DesktopWslEnvironment | FileSystem.FileSystem > { const environment = yield* DesktopEnvironment.DesktopEnvironment; const wslEnvironment = yield* DesktopWslEnvironment.DesktopWslEnvironment; // 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 // -> WSL 127.0.0.1) AND via the distro's eth0 IP directly from // Windows. wslhost forwarding is unreliable on some Windows hosts: // the desktop's readiness probe and the renderer's saved-env-style // fetch both saw "Failed to fetch" when the backend only bound to // 127.0.0.1 inside WSL. Binding to 0.0.0.0 plus advertising the // WSL IP as the renderer-visible URL avoids that dependency. // Security-wise this is acceptable for the local-only WSL backend: // the network it exposes on is the WSL-vEthernet network, not the // LAN; the primary owns LAN exposure when the user opts in. const wslBindHost = "0.0.0.0"; const bootstrap = { mode: "desktop" as const, noBrowser: true, port: input.port, // Omit t3Home so the Linux backend uses its own home dir instead of // the Windows-side baseDir (which would be a /mnt/c path and share // the SQLite file with the primary). host: wslBindHost, desktopBootstrapToken: input.bootstrapToken, // PortSchema rejects 0, so when tailscale serve is disabled we still // need a valid number in this slot. The backend reads tailscaleServePort // only when tailscaleServeEnabled is true, so the actual value here is // inert. tailscaleServeEnabled: false, tailscaleServePort: 443, ...buildObservabilityFragment(input.observabilitySettings), }; // In packaged builds environment.appRoot is .../resources/app.asar — an // archive FILE. The Windows primary reads its entry through // 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. 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, windowsEntryPath: wslEntryPath, windowsRepoRoot: wslAppRoot, // Packaged builds ship a prebuilt Linux node-pty (built on Linux in CI and // attached to the Windows artifact — see build-desktop-artifact.ts), so the // WSL backend never needs a compiler, node-gyp, or network on first launch. // Compiling from source is a dev-only convenience: a checkout has no shipped // prebuilt, and developers have the toolchain. In packaged builds we instead // surface a clear diagnostic if the prebuilt can't load (unsupported // arch/distro), rather than silently dropping into a fragile runtime build. allowBuild: !environment.isPackaged, }); // Every operation after preflight uses the same concrete distro. In // default-tracking mode this closes the race where the system default // changes between probing and spawning the backend. const runningDistro = preflight._tag === "Ready" ? preflight.runningDistro : null; const distroForConfig = runningDistro ?? input.distro; // Resolve the selected distro's IPv4 address. In mirrored mode the distro // reports a host interface, so use loopback instead; a failed probe also // falls back to loopback and preserves the previous behavior. const distroIp = yield* wslEnvironment.getDistroIp(distroForConfig); const usesSharedNetworkStack = Option.match(distroIp, { onNone: () => false, onSome: (ip) => isLocalHostIpv4(ip), }); const rendererHost = usesSharedNetworkStack ? "127.0.0.1" : Option.getOrElse(distroIp, () => "127.0.0.1"); const httpBaseUrl = new URL(`http://${rendererHost}:${input.port}`); const distroArgs = distroForConfig ? ["-d", distroForConfig] : []; const forwardedEnv: Record<string, string> = {}; const forwardedEnvNames: string[] = []; for (const name of WSL_FORWARDED_ENV_NAMES) { const value = process.env[name]; if (value !== undefined && value.length > 0) { forwardedEnv[name] = value; forwardedEnvNames.push(name); } } // Build an explicit copy of process.env minus T3CODE_HOME (dev-runner // exports the Windows-side base dir for the primary; if it leaks into // the WSL backend the Linux side ends up sharing C:\Users\...\.t3 via // /mnt/c, which means both backends read/write the same database and // their env-ids collide). const parentEnvWithoutT3Home: Record<string, string | undefined> = {}; for (const [key, value] of Object.entries(process.env)) { if (key === "T3CODE_HOME") continue; parentEnvWithoutT3Home[key] = value; } const wslEnv = mergeWslEnv(parentEnvWithoutT3Home.WSLENV, forwardedEnvNames); const baseConfig = { executablePath: "wsl.exe", entryPath: wslEntryPath, cwd: environment.backendCwd, env: { ...parentEnvWithoutT3Home, ...backendChildEnvPatch(), ...forwardedEnv, ...(wslEnv !== undefined ? { WSLENV: wslEnv } : {}), }, // env is already a complete process.env minus T3CODE_HOME; pass it // verbatim instead of letting the spawner re-merge process.env on top. extendEnv: false, bootstrap, bootstrapDelivery: "stdin" as const, httpBaseUrl,