fix(desktop-wsl): skip sidecar spawn when distro first-run is incomplete

Reading HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss tells us the DefaultUid for every registered distro without touching wsl.exe. On a freshly installed Ubuntu-24.04 the 'Create a default UNIX user account' prompt never ran and DefaultUid stays 0; every wsl.exe -d <distro> ... invocation in that state silently blocks on stdin forever, even with --user root. checkWslDistroFirstRun reads the registry via powershell, and spawnWslSidecar throws a human-readable error if the distro still needs setup, so the controller marks the server as failed with a clear message instead of hanging. Also emits 'wsl sidecar starting' to main.log for visibility.
This commit is contained in:
LukeParkerDev 2026-04-17 15:29:50 +10:00
parent 6fc5f342dd
commit 16ada93dd4
3 changed files with 78 additions and 1 deletions

View file

@ -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<void> }
@ -83,6 +83,23 @@ export async function spawnWslSidecar(
distro: string,
opts: { onLine?: (line: WslCommandLine) => void; healthTimeoutMs?: number } = {},
): Promise<WslSidecar> {
// 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 <distro> ... 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}`)

View file

@ -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)

View file

@ -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 <distro> 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<WslRegistryDistro[]> {
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<WslFirstRunCheck> {
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)
}