feat(app): v2 wsl ui (#34233)
Co-authored-by: LukeParkerDev <10430890+Hona@users.noreply.github.com>
This commit is contained in:
parent
bf18cc971f
commit
4d91f0cf28
20 changed files with 1612 additions and 618 deletions
|
|
@ -1,7 +1,7 @@
|
|||
import { app, ipcMain } from "electron"
|
||||
import type { IpcMainInvokeEvent } from "electron"
|
||||
import type { WslServersController } from "./servers"
|
||||
import { requireWslIpcString } from "./policy"
|
||||
import { requireWslIpcString, requireWslIpcStrings } from "./policy"
|
||||
import type { WslServersState } from "../../preload/types"
|
||||
|
||||
export function registerWslIpcHandlers(controller: WslServersController) {
|
||||
|
|
@ -46,11 +46,8 @@ export function registerWslIpcHandlers(controller: WslServersController) {
|
|||
ipcMain.handle("wsl-servers-install-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.installDistro(requireWslIpcString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-probe-distro", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.probeDistro(requireWslIpcString("distro", name)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-probe-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.probeOpencode(requireWslIpcString("distro", name)),
|
||||
ipcMain.handle("wsl-servers-probe-addable", (_event: IpcMainInvokeEvent, distros: string[]) =>
|
||||
controller.probeAddable(requireWslIpcStrings("distro", distros)),
|
||||
)
|
||||
ipcMain.handle("wsl-servers-install-opencode", (_event: IpcMainInvokeEvent, name: string) =>
|
||||
controller.installOpencode(requireWslIpcString("distro", name)),
|
||||
|
|
@ -97,8 +94,7 @@ function registerUnavailableWslIpcHandlers() {
|
|||
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-probe-addable", unavailable)
|
||||
ipcMain.handle("wsl-servers-install-opencode", unavailable)
|
||||
ipcMain.handle("wsl-servers-open-terminal", unavailable)
|
||||
ipcMain.handle("wsl-servers-add", unavailable)
|
||||
|
|
|
|||
|
|
@ -24,3 +24,10 @@ export function requireWslIpcString(name: string, value: unknown) {
|
|||
if (typeof value === "string" && value.length > 0) return value
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
||||
export function requireWslIpcStrings(name: string, value: unknown) {
|
||||
if (!Array.isArray(value)) throw new Error(`Invalid ${name}`)
|
||||
const values = value.map((item) => requireWslIpcString(name, item))
|
||||
if (values.length > 0) return values
|
||||
throw new Error(`Invalid ${name}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { clearWslDistroState, requireWslIpcString, wslServerIdToRestart, wslTerminalArgs } from "./policy"
|
||||
import { clearWslDistroState, requireWslIpcString, requireWslIpcStrings, wslServerIdToRestart, wslTerminalArgs } from "./policy"
|
||||
import {
|
||||
expectOpencodeVersion,
|
||||
pendingRestartAfterWslInstall,
|
||||
|
|
@ -87,8 +87,10 @@ test("stops health polling when sidecar startup settles", async () => {
|
|||
|
||||
test("validates WSL IPC identifiers at the module boundary", () => {
|
||||
expect(requireWslIpcString("distro", "Debian")).toBe("Debian")
|
||||
expect(requireWslIpcStrings("distro", ["Debian", "Ubuntu"])).toEqual(["Debian", "Ubuntu"])
|
||||
expect(() => requireWslIpcString("distro", "")).toThrow("Invalid distro")
|
||||
expect(() => requireWslIpcString("server id", undefined)).toThrow("Invalid server id")
|
||||
expect(() => requireWslIpcStrings("distro", [])).toThrow("Invalid distro")
|
||||
})
|
||||
|
||||
test("derives a required Windows restart from the post-install runtime probe", () => {
|
||||
|
|
@ -142,6 +144,70 @@ test("ignores stale startup OpenCode checks after removing a WSL server", async
|
|||
expect(controller.getState().opencodeChecks).toEqual({})
|
||||
})
|
||||
|
||||
test("probes addable distros in parallel before checking OpenCode", async () => {
|
||||
persistedServers = []
|
||||
const started: string[] = []
|
||||
const release = new Map<string, () => void>()
|
||||
const opencode: string[] = []
|
||||
const controller = createWslServersController(
|
||||
"1.16.2",
|
||||
async () => new Promise<never>(() => undefined),
|
||||
{
|
||||
...testControllerOptions(),
|
||||
probeDistro: async (distro) => {
|
||||
started.push(distro)
|
||||
await new Promise<void>((resolve) => release.set(distro, resolve))
|
||||
return { name: distro, canExecute: true, hasBash: true, hasCurl: true, error: null }
|
||||
},
|
||||
resolveOpencode: async (distro) => {
|
||||
opencode.push(distro)
|
||||
return "/home/me/.opencode/bin/opencode"
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
const task = controller.probeAddable(["Debian", "Ubuntu"])
|
||||
await waitFor(() => started.length === 2)
|
||||
expect(started).toEqual(["Debian", "Ubuntu"])
|
||||
expect(opencode).toEqual([])
|
||||
release.get("Debian")?.()
|
||||
release.get("Ubuntu")?.()
|
||||
await task
|
||||
|
||||
expect(Object.keys(controller.getState().distroProbes)).toEqual(["Debian", "Ubuntu"])
|
||||
expect(opencode).toEqual(["Debian", "Ubuntu"])
|
||||
expect(Object.keys(controller.getState().opencodeChecks)).toEqual(["Debian", "Ubuntu"])
|
||||
})
|
||||
|
||||
test("does not check OpenCode in addable distros that cannot execute commands", async () => {
|
||||
persistedServers = []
|
||||
const opencode: string[] = []
|
||||
const controller = createWslServersController(
|
||||
"1.16.2",
|
||||
async () => new Promise<never>(() => undefined),
|
||||
{
|
||||
...testControllerOptions(),
|
||||
probeDistro: async (distro) => ({
|
||||
name: distro,
|
||||
canExecute: distro === "Debian",
|
||||
hasBash: distro === "Debian",
|
||||
hasCurl: distro === "Debian",
|
||||
error: distro === "Debian" ? null : "Open Ubuntu once to finish setup",
|
||||
}),
|
||||
resolveOpencode: async (distro) => {
|
||||
opencode.push(distro)
|
||||
return "/home/me/.opencode/bin/opencode"
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
await controller.probeAddable(["Debian", "Ubuntu"])
|
||||
|
||||
expect(Object.keys(controller.getState().distroProbes)).toEqual(["Debian", "Ubuntu"])
|
||||
expect(opencode).toEqual(["Debian"])
|
||||
expect(Object.keys(controller.getState().opencodeChecks)).toEqual(["Debian"])
|
||||
})
|
||||
|
||||
async function waitFor(check: () => boolean) {
|
||||
for (let attempt = 0; attempt < 20; attempt++) {
|
||||
if (check()) return
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ type WslServersControllerOptions = {
|
|||
logger?: ControllerLogger
|
||||
readServers?: () => WslServerConfig[]
|
||||
writeServers?: (servers: WslServerConfig[]) => void
|
||||
probeDistro?: typeof probeWslDistro
|
||||
resolveOpencode?: typeof resolveWslOpencode
|
||||
readCommandVersion?: typeof readWslCommandVersion
|
||||
}
|
||||
|
|
@ -70,6 +71,7 @@ export function createWslServersController(
|
|||
const logger = options?.logger
|
||||
const readServers = options?.readServers ?? readPersistedServers
|
||||
const writeServers = options?.writeServers ?? writePersistedServers
|
||||
const probeDistro = options?.probeDistro ?? probeWslDistro
|
||||
|
||||
const emit = () => {
|
||||
for (const listener of listeners) listener({ type: "state", state })
|
||||
|
|
@ -140,6 +142,28 @@ export function createWslServersController(
|
|||
setOpencodeCheck(distro, await checkOpencode(distro, opts))
|
||||
}
|
||||
|
||||
const probeAddableDistros = async (distros: string[], opts?: { signal?: AbortSignal }) => {
|
||||
const unique = [...new Set(distros)]
|
||||
const distroProbes = await Promise.all(
|
||||
unique
|
||||
.filter((distro) => !state.distroProbes[distro])
|
||||
.map(async (distro) => [distro, await probeDistro(distro, opts)] as const),
|
||||
)
|
||||
if (distroProbes.length) {
|
||||
setState({ distroProbes: { ...state.distroProbes, ...Object.fromEntries(distroProbes) } })
|
||||
}
|
||||
|
||||
const opencodeChecks = await Promise.all(
|
||||
unique
|
||||
.filter((distro) => distroProbeReady(state.distroProbes[distro]))
|
||||
.filter((distro) => !state.opencodeChecks[distro])
|
||||
.map(async (distro) => [distro, await checkOpencode(distro, opts)] as const),
|
||||
)
|
||||
if (opencodeChecks.length) {
|
||||
setState({ opencodeChecks: { ...state.opencodeChecks, ...Object.fromEntries(opencodeChecks) } })
|
||||
}
|
||||
}
|
||||
|
||||
const hasServer = (id: string, distro: string) => {
|
||||
return state.servers.some((item) => item.config.id === id && item.config.distro === distro)
|
||||
}
|
||||
|
|
@ -319,7 +343,7 @@ export function createWslServersController(
|
|||
throw new Error(message)
|
||||
}
|
||||
const distros = await refreshDistroLists({ signal: abort.signal })
|
||||
const probe = await probeWslDistro(name, { signal: abort.signal })
|
||||
const probe = await probeDistro(name, { signal: abort.signal })
|
||||
setState({
|
||||
...distros,
|
||||
distroProbes: { ...state.distroProbes, [name]: probe },
|
||||
|
|
@ -327,16 +351,10 @@ export function createWslServersController(
|
|||
})
|
||||
},
|
||||
|
||||
async probeDistro(name: string) {
|
||||
await runJob({ kind: "probe-distro", distro: name, startedAt: Date.now() }, async (abort) => {
|
||||
const probe = await probeWslDistro(name, { signal: abort.signal })
|
||||
setState({ distroProbes: { ...state.distroProbes, [name]: probe } })
|
||||
})
|
||||
},
|
||||
|
||||
async probeOpencode(name: string) {
|
||||
await runJob({ kind: "probe-opencode", distro: name, startedAt: Date.now() }, async (abort) => {
|
||||
await refreshOpencodeCheck(name, { signal: abort.signal })
|
||||
async probeAddable(distros: string[]) {
|
||||
if (!distros.length) return
|
||||
await runJob({ kind: "probe-addable", distros, startedAt: Date.now() }, async (abort) => {
|
||||
await probeAddableDistros(distros, { signal: abort.signal })
|
||||
})
|
||||
},
|
||||
|
||||
|
|
@ -480,6 +498,10 @@ function opencodeCheck(
|
|||
}
|
||||
}
|
||||
|
||||
function distroProbeReady(probe: WslDistroProbe | undefined) {
|
||||
return !!probe?.canExecute && probe.hasBash && probe.hasCurl
|
||||
}
|
||||
|
||||
function startupFailure(code: number | null, signal: NodeJS.Signals | null) {
|
||||
return `WSL server exited after startup (code=${code ?? "null"} signal=${signal ?? "null"})`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ const api: ElectronAPI = {
|
|||
refreshDistros: () => ipcRenderer.invoke("wsl-servers-refresh-distros"),
|
||||
installWsl: () => ipcRenderer.invoke("wsl-servers-install-wsl"),
|
||||
installDistro: (name) => ipcRenderer.invoke("wsl-servers-install-distro", name),
|
||||
probeDistro: (name) => ipcRenderer.invoke("wsl-servers-probe-distro", name),
|
||||
probeOpencode: (name) => ipcRenderer.invoke("wsl-servers-probe-opencode", name),
|
||||
probeAddable: (distros) => ipcRenderer.invoke("wsl-servers-probe-addable", distros),
|
||||
installOpencode: (name) => ipcRenderer.invoke("wsl-servers-install-opencode", name),
|
||||
openTerminal: (name) => ipcRenderer.invoke("wsl-servers-open-terminal", name),
|
||||
addServer: (distro) => ipcRenderer.invoke("wsl-servers-add", distro),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue