fix(desktop): few WSL bugs (#31095)

This commit is contained in:
Filip
2026-06-08 02:05:54 +00:00
committed by GitHub
parent b1d14acc35
commit 65a3f7f749
13 changed files with 231 additions and 50 deletions
+43
View File
@@ -2,8 +2,14 @@ import { app, ipcMain } from "electron"
import type { IpcMainInvokeEvent } from "electron"
import type { WslServersController } from "./servers"
import { requireWslIpcString } from "./policy"
import type { WslServersState } from "../../preload/types"
export function registerWslIpcHandlers(controller: WslServersController) {
if (process.platform !== "win32") {
registerUnavailableWslIpcHandlers()
return
}
const subscriptions = new Map<number, () => void>()
const unsubscribe = (id: number) => {
const off = subscriptions.get(id)
@@ -62,3 +68,40 @@ export function registerWslIpcHandlers(controller: WslServersController) {
controller.startServer(requireWslIpcString("server id", id)),
)
}
function registerUnavailableWslIpcHandlers() {
const unavailable = () => {
throw new Error("WSL is only available on Windows")
}
const state = (): WslServersState => ({
runtime: {
available: false,
version: null,
error: "WSL is only available on Windows",
},
installed: [],
online: [],
distroProbes: {},
opencodeChecks: {},
pendingRestart: false,
servers: [],
job: null,
})
ipcMain.handle("wsl-servers-subscribe", (event) => {
event.sender.send("wsl-servers-event", { type: "state", state: state() })
})
ipcMain.handle("wsl-servers-unsubscribe", () => undefined)
ipcMain.handle("wsl-servers-get-state", () => state())
ipcMain.handle("wsl-servers-probe-runtime", unavailable)
ipcMain.handle("wsl-servers-refresh-distros", unavailable)
ipcMain.handle("wsl-servers-install-wsl", unavailable)
ipcMain.handle("wsl-servers-install-distro", unavailable)
ipcMain.handle("wsl-servers-probe-distro", unavailable)
ipcMain.handle("wsl-servers-probe-opencode", unavailable)
ipcMain.handle("wsl-servers-install-opencode", unavailable)
ipcMain.handle("wsl-servers-open-terminal", unavailable)
ipcMain.handle("wsl-servers-add", unavailable)
ipcMain.handle("wsl-servers-remove", unavailable)
ipcMain.handle("wsl-servers-start", unavailable)
}
@@ -6,6 +6,10 @@ import {
pollWslHealth,
wslServerIdsToStartOnInitialize,
} from "./startup"
import { createWslServersController, type WslServerConfig } from "./servers"
let persistedServers: WslServerConfig[] = []
let releaseOpencodeResolve: (() => void) | undefined
test("starts every configured WSL server on initialization", () => {
expect(
@@ -91,3 +95,73 @@ test("derives a required Windows restart from the post-install runtime probe", (
expect(pendingRestartAfterWslInstall({ available: false, version: null, error: "WSL unavailable" })).toBe(true)
expect(pendingRestartAfterWslInstall({ available: true, version: "WSL version: 2.6.1", error: null })).toBe(false)
})
test("ignores stale background OpenCode checks after removing a WSL server", async () => {
persistedServers = []
releaseOpencodeResolve = undefined
const controller = createWslServersController(
"1.16.2",
async () => ({
listener: {
stop: () => undefined,
onExit: () => undefined,
},
url: "http://127.0.0.1:4096",
username: "opencode",
password: "secret",
}),
testControllerOptions(),
)
await controller.addServer("Debian")
await waitFor(() => !!releaseOpencodeResolve)
await controller.removeServer("wsl:Debian")
releaseOpencodeResolve?.()
await new Promise((resolve) => setTimeout(resolve, 0))
expect(controller.getState().servers).toEqual([])
expect(controller.getState().opencodeChecks).toEqual({})
})
test("ignores stale startup OpenCode checks after removing a WSL server", async () => {
persistedServers = [{ id: "wsl:Debian", distro: "Debian" }]
releaseOpencodeResolve = undefined
const controller = createWslServersController(
"1.16.2",
async () => new Promise<never>(() => undefined),
testControllerOptions(),
)
await controller.initialize()
await waitFor(() => !!releaseOpencodeResolve)
await controller.removeServer("wsl:Debian")
releaseOpencodeResolve?.()
await new Promise((resolve) => setTimeout(resolve, 0))
expect(controller.getState().servers).toEqual([])
expect(controller.getState().opencodeChecks).toEqual({})
})
async function waitFor(check: () => boolean) {
for (let attempt = 0; attempt < 20; attempt++) {
if (check()) return
await new Promise((resolve) => setTimeout(resolve, 0))
}
throw new Error("Timed out waiting for condition")
}
function testControllerOptions() {
return {
readServers: () => persistedServers,
writeServers: (servers: WslServerConfig[]) => {
persistedServers = servers
},
readCommandVersion: async () => "1.16.2",
resolveOpencode: async () => {
await new Promise<void>((resolve) => {
releaseOpencodeResolve = resolve
})
return "/home/me/.opencode/bin/opencode"
},
}
}
+70 -13
View File
@@ -11,7 +11,7 @@ import type {
WslServersEvent,
WslServersState,
} from "../../preload/types"
import { WSL_SERVERS_KEY } from "../constants"
import { WSL_SERVERS_KEY } from "../store-keys"
import { getStore } from "../store"
import { expectOpencodeVersion, pendingRestartAfterWslInstall, wslServerIdsToStartOnInitialize } from "./startup"
import { clearWslDistroState, wslServerIdToRestart } from "./policy"
@@ -43,18 +43,33 @@ type ControllerLogger = {
error: (message: string, meta?: unknown) => void
}
type WslServersControllerOptions = {
logger?: ControllerLogger
readServers?: () => WslServerConfig[]
writeServers?: (servers: WslServerConfig[]) => void
resolveOpencode?: typeof resolveWslOpencode
readCommandVersion?: typeof readWslCommandVersion
}
export type WslServersController = ReturnType<typeof createWslServersController>
export function wslServerIdForDistro(distro: string) {
return `wsl:${distro}`
}
export function createWslServersController(appVersion: string, spawnSidecar: SpawnSidecar, logger?: ControllerLogger) {
export function createWslServersController(
appVersion: string,
spawnSidecar: SpawnSidecar,
options?: WslServersControllerOptions,
) {
let state: WslServersState = initialState()
const listeners = new Set<(event: WslServersEvent) => void>()
const sidecars = new Map<string, RunningSidecar>()
const startAttempts = new Map<string, number>()
let jobAbort: AbortController | undefined
const logger = options?.logger
const readServers = options?.readServers ?? readPersistedServers
const writeServers = options?.writeServers ?? writePersistedServers
const emit = () => {
for (const listener of listeners) listener({ type: "state", state })
@@ -66,7 +81,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
}
const persistServers = (servers: WslServerConfig[]) => {
getStore().set(WSL_SERVERS_KEY, { servers })
writeServers(servers)
}
const updateServer = (id: string, update: (item: WslServerItem) => WslServerItem) => {
@@ -89,7 +104,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
}
const refreshFromStore = () => {
const persisted = readPersistedServers()
const persisted = readServers()
const items: WslServerItem[] = persisted.map((config) => {
const existing = state.servers.find((item) => item.config.id === config.id)
return {
@@ -113,10 +128,50 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
})
}
const checkOpencode = async (distro: string, opts?: { signal?: AbortSignal }) => {
const resolved = await (options?.resolveOpencode ?? resolveWslOpencode)(distro, opts)
const version = resolved ? await (options?.readCommandVersion ?? readWslCommandVersion)(resolved, distro, opts) : null
return opencodeCheck(distro, resolved, version, appVersion)
}
const refreshOpencodeCheck = async (distro: string, opts?: { signal?: AbortSignal }) => {
const resolved = await resolveWslOpencode(distro, opts)
const version = resolved ? await readWslCommandVersion(resolved, distro, opts) : null
setOpencodeCheck(distro, opencodeCheck(distro, resolved, version, appVersion))
setOpencodeCheck(distro, await checkOpencode(distro, opts))
}
const hasServer = (id: string, distro: string) => {
return state.servers.some((item) => item.config.id === id && item.config.distro === distro)
}
const refreshOpencodeCheckBackground = (id: string, distro: string) => {
void checkOpencode(distro)
.then((check) => {
if (!hasServer(id, distro)) return
setOpencodeCheck(distro, check)
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error)
logger?.error("wsl opencode check failed", { id, distro, message })
})
}
const refreshOpencodeChecks = async () => {
await Promise.all(
state.servers.map((item) =>
checkOpencode(item.config.distro)
.then((check) => {
if (!hasServer(item.config.id, item.config.distro)) return
setOpencodeCheck(item.config.distro, check)
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error)
logger?.error("wsl opencode check failed", {
id: item.config.id,
distro: item.config.distro,
message,
})
}),
),
)
}
const refreshDistroLists = async (opts: { signal?: AbortSignal }) => {
@@ -170,10 +225,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
setRuntime(id, { kind: "failed", message })
logger?.error("wsl sidecar exited", { id, distro: item.config.distro, code, signal })
})
void refreshOpencodeCheck(item.config.distro).catch((error) => {
const message = error instanceof Error ? error.message : String(error)
logger?.error("wsl opencode check failed", { id, distro: item.config.distro, message })
})
refreshOpencodeCheckBackground(id, item.config.distro)
logger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url })
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
@@ -225,6 +277,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
async initialize() {
refreshFromStore()
void refreshOpencodeChecks()
for (const id of wslServerIdsToStartOnInitialize(state.servers.map((item) => item.config))) void startServer(id)
},
@@ -311,7 +364,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
id,
distro,
}
persistServers([...readPersistedServers(), config])
persistServers([...readServers(), config])
setState({
servers: [...state.servers, { config, runtime: { kind: "starting" } }],
})
@@ -323,7 +376,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
const distro = state.servers.find((item) => item.config.id === id)?.config.distro
invalidateStartAttempt(id)
await stopServerInternal(id)
const remaining = readPersistedServers().filter((item) => item.id !== id)
const remaining = readServers().filter((item) => item.id !== id)
persistServers(remaining)
setState({
servers: state.servers.filter((item) => item.config.id !== id),
@@ -371,6 +424,10 @@ function readPersistedServers(): WslServerConfig[] {
return []
}
function writePersistedServers(servers: WslServerConfig[]) {
getStore().set(WSL_SERVERS_KEY, { servers })
}
function normalizePersistedServer(value: unknown): WslServerConfig[] {
if (!value || typeof value !== "object") return []
const record = value as Record<string, unknown>