diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts index da856cf558..cf2e5e3491 100644 --- a/packages/desktop-electron/src/main/server.ts +++ b/packages/desktop-electron/src/main/server.ts @@ -5,7 +5,7 @@ import { app } from "electron" import { DEFAULT_SERVER_URL_KEY } from "./constants" import { getUserShell, loadShellEnv } from "./shell-env" import { getStore } from "./store" -import { type WslCommandLine, resolveWslOpencode, wslArgs } from "./wsl" +import { checkWslDistroFirstRun, type WslCommandLine, resolveWslOpencode, wslArgs } from "./wsl" export type HealthCheck = { wait: Promise } @@ -83,6 +83,23 @@ export async function spawnWslSidecar( distro: string, opts: { onLine?: (line: WslCommandLine) => void; healthTimeoutMs?: number } = {}, ): Promise { + // Gate on the registry state BEFORE any wsl.exe invocation. If the + // distro still has DefaultUid=0 it means the interactive first-run + // "Create a default UNIX user account" prompt never completed, and + // every wsl.exe -d ... call will silently block on stdin + // forever (we verified: Ubuntu-24.04 hangs on both -- echo and + // --user root -- echo in this state). Fail fast with a clear message + // so the controller can surface it to the user. + const firstRun = await checkWslDistroFirstRun(distro) + if (firstRun.status === "not-installed") { + throw new Error(`WSL distro ${distro} is not installed`) + } + if (firstRun.status === "needs-first-run") { + throw new Error( + `WSL distro ${distro} has not completed first-run setup. Open a terminal and run 'wsl -d ${distro}' to create a default UNIX user, then retry.`, + ) + } + const opencode = await resolveWslOpencode(distro) if (!opencode) throw new Error(`OpenCode is not installed in ${distro}`) diff --git a/packages/desktop-electron/src/main/wsl-servers.ts b/packages/desktop-electron/src/main/wsl-servers.ts index 2757be3a81..e60157fc3f 100644 --- a/packages/desktop-electron/src/main/wsl-servers.ts +++ b/packages/desktop-electron/src/main/wsl-servers.ts @@ -121,6 +121,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa if (!item) return await stopServerInternal(id) setRuntime(id, { kind: "starting" }) + mainLogger?.log("wsl sidecar starting", { id, distro: item.config.distro }) try { const sidecar = await spawnSidecar(item.config.distro) sidecars.set(id, sidecar) diff --git a/packages/desktop-electron/src/main/wsl.ts b/packages/desktop-electron/src/main/wsl.ts index 3ad523b789..8a4b31e1c9 100644 --- a/packages/desktop-electron/src/main/wsl.ts +++ b/packages/desktop-electron/src/main/wsl.ts @@ -159,6 +159,65 @@ export function runWslInDistro(args: string[], distro?: string | null, opts?: Ru return runWsl(wslArgs(args, distro), opts) } +export type WslRegistryDistro = { + name: string + defaultUid: number + state: number + version: number +} + +// Distros that are designed to run as root and don't have a user-level +// first-run setup. Ubuntu/Debian/Kali/etc. all run a first-boot hook that +// prompts for a UNIX username on first invocation; if that never runs, +// wsl.exe -d hangs silently forever. +const ALWAYS_ROOT_DISTROS = new Set(["docker-desktop", "docker-desktop-data"]) + +// Read LXSS metadata from the Windows registry. This never invokes +// wsl.exe, so it is safe to call when wsl.exe itself is wedged. +// DefaultUid === 0 on a user-oriented distro means the first-run +// "Create a default UNIX user account" step never completed. +export async function readWslDistrosFromRegistry(opts?: RunWslOptions): Promise { + const script = [ + "$ErrorActionPreference = 'Stop'", + "$out = @()", + "Get-ChildItem 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss' -ErrorAction SilentlyContinue | ForEach-Object {", + " $name = $_.GetValue('DistributionName')", + " if (-not $name) { return }", + " $out += [PSCustomObject]@{", + " name = $name", + " defaultUid = [int]$_.GetValue('DefaultUid', 0)", + " state = [int]$_.GetValue('State', 0)", + " version = [int]$_.GetValue('Version', 0)", + " }", + "}", + "$out | ConvertTo-Json -Compress", + ].join("; ") + const result = await runPowerShell(script, opts) + if (result.code !== 0) return [] + const text = result.stdout.trim() + if (!text) return [] + try { + const parsed = JSON.parse(text) as WslRegistryDistro | WslRegistryDistro[] + return Array.isArray(parsed) ? parsed : [parsed] + } catch { + return [] + } +} + +export type WslFirstRunCheck = + | { status: "ok" } + | { status: "needs-first-run"; defaultUid: number } + | { status: "not-installed" } + +export async function checkWslDistroFirstRun(distro: string, opts?: RunWslOptions): Promise { + const distros = await readWslDistrosFromRegistry(opts) + const entry = distros.find((d) => d.name === distro) + if (!entry) return { status: "not-installed" } + if (ALWAYS_ROOT_DISTROS.has(entry.name)) return { status: "ok" } + if (entry.defaultUid === 0) return { status: "needs-first-run", defaultUid: entry.defaultUid } + return { status: "ok" } +} + export function runWslSh(script: string, distro?: string | null, opts?: RunWslOptions) { return runWslInDistro(["sh", "-lc", script], distro, opts) }