fix(desktop): few WSL bugs (#31095)
This commit is contained in:
@@ -509,11 +509,16 @@ export function useServerManagementController(options: { onSelect?: () => void;
|
|||||||
resetEdit()
|
resetEdit()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function handleRemove(url: ServerConnection.Key) {
|
async function handleRemove(key: ServerConnection.Key) {
|
||||||
tabs.removeServer(url)
|
try {
|
||||||
server.remove(url)
|
if (key.startsWith("wsl:")) await platform.wslServers?.removeServer(key)
|
||||||
if ((await platform.getDefaultServer?.()) === url) {
|
tabs.removeServer(key)
|
||||||
void platform.setDefaultServer?.(null)
|
server.remove(key)
|
||||||
|
if ((await platform.getDefaultServer?.()) === key) {
|
||||||
|
await setDefault(null)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
showRequestError(language, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,17 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
if (!distro) return null
|
if (!distro) return null
|
||||||
return current()?.opencodeChecks[distro] ?? null
|
return current()?.opencodeChecks[distro] ?? null
|
||||||
})
|
})
|
||||||
|
const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
|
||||||
|
const distroReady = createMemo(() => {
|
||||||
|
const probe = selectedProbe()
|
||||||
|
if (!probe || !selectedDistro()) return false
|
||||||
|
if (selectedInstalled()?.version === 1) return false
|
||||||
|
return probe.canExecute && probe.hasBash && probe.hasCurl
|
||||||
|
})
|
||||||
|
const opencodeReady = createMemo(() => {
|
||||||
|
const check = opencodeCheck()
|
||||||
|
return !!check?.resolvedPath && !check.error
|
||||||
|
})
|
||||||
const distroWarningProbe = createMemo(() => {
|
const distroWarningProbe = createMemo(() => {
|
||||||
const probe = selectedProbe()
|
const probe = selectedProbe()
|
||||||
if (!probe) return null
|
if (!probe) return null
|
||||||
@@ -106,17 +117,6 @@ export function DialogAddWslServer(props: DialogWslServerProps = {}) {
|
|||||||
const job = current()?.job
|
const job = current()?.job
|
||||||
return job?.kind === "install-opencode" && job.distro === selectedDistro()
|
return job?.kind === "install-opencode" && job.distro === selectedDistro()
|
||||||
})
|
})
|
||||||
const wslReady = createMemo(() => !!current()?.runtime?.available && !current()?.pendingRestart)
|
|
||||||
const distroReady = createMemo(() => {
|
|
||||||
const probe = selectedProbe()
|
|
||||||
if (!probe || !selectedDistro()) return false
|
|
||||||
if (selectedInstalled()?.version === 1) return false
|
|
||||||
return probe.canExecute && probe.hasBash && probe.hasCurl
|
|
||||||
})
|
|
||||||
const opencodeReady = createMemo(() => {
|
|
||||||
const check = opencodeCheck()
|
|
||||||
return !!check?.resolvedPath && !check.error
|
|
||||||
})
|
|
||||||
const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
|
const allReady = createMemo(() => wslReady() && distroReady() && opencodeReady())
|
||||||
const addDisabled = createMemo(() => {
|
const addDisabled = createMemo(() => {
|
||||||
const job = current()?.job
|
const job = current()?.job
|
||||||
|
|||||||
@@ -76,11 +76,7 @@ export function WslServerSettings(props: {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
const remove = (key: ServerConnection.Key) => {
|
const remove = (key: ServerConnection.Key) => {
|
||||||
if (!api) return
|
request.mutate(() => props.controller.handleRemove(key))
|
||||||
request.mutate(async () => {
|
|
||||||
await api.removeServer(key)
|
|
||||||
await props.controller.handleRemove(key)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -4,8 +4,4 @@ type Channel = "dev" | "beta" | "prod"
|
|||||||
const raw = import.meta.env.OPENCODE_CHANNEL
|
const raw = import.meta.env.OPENCODE_CHANNEL
|
||||||
export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev"
|
export const CHANNEL: Channel = raw === "dev" || raw === "beta" || raw === "prod" ? raw : "dev"
|
||||||
|
|
||||||
export const SETTINGS_STORE = "opencode.settings"
|
|
||||||
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
|
|
||||||
export const WSL_SERVERS_KEY = "wslServers"
|
|
||||||
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
|
|
||||||
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
|
export const UPDATER_ENABLED = app.isPackaged && CHANNEL !== "dev"
|
||||||
|
|||||||
@@ -145,9 +145,11 @@ const main = Effect.gen(function* () {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
logger: {
|
||||||
log: (message, meta) => logger.log(message, meta),
|
log: (message, meta) => logger.log(message, meta),
|
||||||
error: (message, meta) => logger.error(message, meta),
|
error: (message, meta) => logger.error(message, meta),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
const stopSidecars = async () => {
|
const stopSidecars = async () => {
|
||||||
await killSidecar()
|
await killSidecar()
|
||||||
@@ -327,7 +329,9 @@ const main = Effect.gen(function* () {
|
|||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error))
|
void wslServers.initialize().catch((error) => logger.error("wsl server initialization failed", error))
|
||||||
|
}
|
||||||
|
|
||||||
yield* Effect.promise(() => health.wait).pipe(
|
yield* Effect.promise(() => health.wait).pipe(
|
||||||
Effect.timeout("30 seconds"),
|
Effect.timeout("30 seconds"),
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ import { dirname, join } from "node:path"
|
|||||||
import { fileURLToPath } from "node:url"
|
import { fileURLToPath } from "node:url"
|
||||||
import { app, utilityProcess } from "electron"
|
import { app, utilityProcess } from "electron"
|
||||||
import type { Details } from "electron"
|
import type { Details } from "electron"
|
||||||
import { DEFAULT_SERVER_URL_KEY } from "./constants"
|
import { getLogger } from "./logging"
|
||||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||||
import { getStore } from "./store"
|
import { getStore } from "./store"
|
||||||
|
import { DEFAULT_SERVER_URL_KEY } from "./store-keys"
|
||||||
|
|
||||||
export type HealthCheck = { wait: Promise<void> }
|
export type HealthCheck = { wait: Promise<void> }
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export function setDefaultServerUrl(url: string | null) {
|
|||||||
export function preferAppEnv(userDataPath: string) {
|
export function preferAppEnv(userDataPath: string) {
|
||||||
const shell = process.platform === "win32" ? null : getUserShell()
|
const shell = process.platform === "win32" ? null : getUserShell()
|
||||||
Object.assign(process.env, {
|
Object.assign(process.env, {
|
||||||
...(shell ? loadShellEnv(shell) : null),
|
...(shell ? loadShellEnv(shell, getLogger()) : null),
|
||||||
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true",
|
||||||
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
OPENCODE_EXPERIMENTAL_FILEWATCHER: "true",
|
||||||
OPENCODE_CLIENT: "desktop",
|
OPENCODE_CLIENT: "desktop",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import { spawnSync } from "node:child_process"
|
import { spawnSync } from "node:child_process"
|
||||||
import { userInfo } from "node:os"
|
import { userInfo } from "node:os"
|
||||||
import { basename } from "node:path"
|
import { basename } from "node:path"
|
||||||
import { getLogger } from "./logging"
|
|
||||||
|
|
||||||
const TIMEOUT = 5_000
|
const TIMEOUT = 5_000
|
||||||
|
|
||||||
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
|
type Probe = { type: "Loaded"; value: Record<string, string> } | { type: "Timeout" } | { type: "Unavailable" }
|
||||||
|
type ShellEnvLogger = {
|
||||||
|
log: (message: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveUserShell(envShell: string | undefined, loginShell: string | null | undefined) {
|
export function resolveUserShell(envShell: string | undefined, loginShell: string | null | undefined) {
|
||||||
const resolvedLoginShell = loginShell && loginShell !== "unknown" ? loginShell : undefined
|
const resolvedLoginShell = loginShell && loginShell !== "unknown" ? loginShell : undefined
|
||||||
@@ -65,8 +67,7 @@ export function isNushell(shell: string) {
|
|||||||
return name === "nu" || name === "nu.exe" || raw.endsWith("\\nu.exe")
|
return name === "nu" || name === "nu.exe" || raw.endsWith("\\nu.exe")
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadShellEnv(shell: string) {
|
export function loadShellEnv(shell: string, logger: ShellEnvLogger) {
|
||||||
const logger = getLogger()
|
|
||||||
if (isNushell(shell)) {
|
if (isNushell(shell)) {
|
||||||
logger.log(`[server] Skipping shell env probe for nushell: ${shell}`)
|
logger.log(`[server] Skipping shell env probe for nushell: ${shell}`)
|
||||||
return null
|
return null
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const SETTINGS_STORE = "opencode.settings"
|
||||||
|
export const DEFAULT_SERVER_URL_KEY = "defaultServerUrl"
|
||||||
|
export const WSL_SERVERS_KEY = "wslServers"
|
||||||
|
export const PINCH_ZOOM_ENABLED_KEY = "pinchZoomEnabled"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import Store from "electron-store"
|
import Store from "electron-store"
|
||||||
import { app } from "electron"
|
import electron from "electron"
|
||||||
|
|
||||||
import { SETTINGS_STORE } from "./constants"
|
import { SETTINGS_STORE } from "./store-keys"
|
||||||
|
|
||||||
const cache = new Map<string, Store>()
|
const cache = new Map<string, Store>()
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ export function getStore(name = SETTINGS_STORE) {
|
|||||||
if (cached) return cached
|
if (cached) return cached
|
||||||
const next = new Store({
|
const next = new Store({
|
||||||
name,
|
name,
|
||||||
cwd: app.getPath("userData"),
|
cwd: electron.app.getPath("userData"),
|
||||||
fileExtension: "",
|
fileExtension: "",
|
||||||
accessPropertiesByDotNotation: false,
|
accessPropertiesByDotNotation: false,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { app, BrowserWindow, dialog, net, nativeImage, nativeTheme, protocol } f
|
|||||||
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path"
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||||
import type { TitlebarTheme } from "../preload/types"
|
import type { TitlebarTheme } from "../preload/types"
|
||||||
import { PINCH_ZOOM_ENABLED_KEY } from "./constants"
|
|
||||||
import { exportDebugLogs, write as writeLog } from "./logging"
|
import { exportDebugLogs, write as writeLog } from "./logging"
|
||||||
import { getStore } from "./store"
|
import { getStore } from "./store"
|
||||||
|
import { PINCH_ZOOM_ENABLED_KEY } from "./store-keys"
|
||||||
import { createUnresponsiveSampler } from "./unresponsive"
|
import { createUnresponsiveSampler } from "./unresponsive"
|
||||||
|
|
||||||
const root = dirname(fileURLToPath(import.meta.url))
|
const root = dirname(fileURLToPath(import.meta.url))
|
||||||
|
|||||||
@@ -2,8 +2,14 @@ import { app, ipcMain } from "electron"
|
|||||||
import type { IpcMainInvokeEvent } from "electron"
|
import type { IpcMainInvokeEvent } from "electron"
|
||||||
import type { WslServersController } from "./servers"
|
import type { WslServersController } from "./servers"
|
||||||
import { requireWslIpcString } from "./policy"
|
import { requireWslIpcString } from "./policy"
|
||||||
|
import type { WslServersState } from "../../preload/types"
|
||||||
|
|
||||||
export function registerWslIpcHandlers(controller: WslServersController) {
|
export function registerWslIpcHandlers(controller: WslServersController) {
|
||||||
|
if (process.platform !== "win32") {
|
||||||
|
registerUnavailableWslIpcHandlers()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const subscriptions = new Map<number, () => void>()
|
const subscriptions = new Map<number, () => void>()
|
||||||
const unsubscribe = (id: number) => {
|
const unsubscribe = (id: number) => {
|
||||||
const off = subscriptions.get(id)
|
const off = subscriptions.get(id)
|
||||||
@@ -62,3 +68,40 @@ export function registerWslIpcHandlers(controller: WslServersController) {
|
|||||||
controller.startServer(requireWslIpcString("server id", id)),
|
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,
|
pollWslHealth,
|
||||||
wslServerIdsToStartOnInitialize,
|
wslServerIdsToStartOnInitialize,
|
||||||
} from "./startup"
|
} from "./startup"
|
||||||
|
import { createWslServersController, type WslServerConfig } from "./servers"
|
||||||
|
|
||||||
|
let persistedServers: WslServerConfig[] = []
|
||||||
|
let releaseOpencodeResolve: (() => void) | undefined
|
||||||
|
|
||||||
test("starts every configured WSL server on initialization", () => {
|
test("starts every configured WSL server on initialization", () => {
|
||||||
expect(
|
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: false, version: null, error: "WSL unavailable" })).toBe(true)
|
||||||
expect(pendingRestartAfterWslInstall({ available: true, version: "WSL version: 2.6.1", error: null })).toBe(false)
|
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"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import type {
|
|||||||
WslServersEvent,
|
WslServersEvent,
|
||||||
WslServersState,
|
WslServersState,
|
||||||
} from "../../preload/types"
|
} from "../../preload/types"
|
||||||
import { WSL_SERVERS_KEY } from "../constants"
|
import { WSL_SERVERS_KEY } from "../store-keys"
|
||||||
import { getStore } from "../store"
|
import { getStore } from "../store"
|
||||||
import { expectOpencodeVersion, pendingRestartAfterWslInstall, wslServerIdsToStartOnInitialize } from "./startup"
|
import { expectOpencodeVersion, pendingRestartAfterWslInstall, wslServerIdsToStartOnInitialize } from "./startup"
|
||||||
import { clearWslDistroState, wslServerIdToRestart } from "./policy"
|
import { clearWslDistroState, wslServerIdToRestart } from "./policy"
|
||||||
@@ -43,18 +43,33 @@ type ControllerLogger = {
|
|||||||
error: (message: string, meta?: unknown) => void
|
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 type WslServersController = ReturnType<typeof createWslServersController>
|
||||||
|
|
||||||
export function wslServerIdForDistro(distro: string) {
|
export function wslServerIdForDistro(distro: string) {
|
||||||
return `wsl:${distro}`
|
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()
|
let state: WslServersState = initialState()
|
||||||
const listeners = new Set<(event: WslServersEvent) => void>()
|
const listeners = new Set<(event: WslServersEvent) => void>()
|
||||||
const sidecars = new Map<string, RunningSidecar>()
|
const sidecars = new Map<string, RunningSidecar>()
|
||||||
const startAttempts = new Map<string, number>()
|
const startAttempts = new Map<string, number>()
|
||||||
let jobAbort: AbortController | undefined
|
let jobAbort: AbortController | undefined
|
||||||
|
const logger = options?.logger
|
||||||
|
const readServers = options?.readServers ?? readPersistedServers
|
||||||
|
const writeServers = options?.writeServers ?? writePersistedServers
|
||||||
|
|
||||||
const emit = () => {
|
const emit = () => {
|
||||||
for (const listener of listeners) listener({ type: "state", state })
|
for (const listener of listeners) listener({ type: "state", state })
|
||||||
@@ -66,7 +81,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const persistServers = (servers: WslServerConfig[]) => {
|
const persistServers = (servers: WslServerConfig[]) => {
|
||||||
getStore().set(WSL_SERVERS_KEY, { servers })
|
writeServers(servers)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateServer = (id: string, update: (item: WslServerItem) => WslServerItem) => {
|
const updateServer = (id: string, update: (item: WslServerItem) => WslServerItem) => {
|
||||||
@@ -89,7 +104,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const refreshFromStore = () => {
|
const refreshFromStore = () => {
|
||||||
const persisted = readPersistedServers()
|
const persisted = readServers()
|
||||||
const items: WslServerItem[] = persisted.map((config) => {
|
const items: WslServerItem[] = persisted.map((config) => {
|
||||||
const existing = state.servers.find((item) => item.config.id === config.id)
|
const existing = state.servers.find((item) => item.config.id === config.id)
|
||||||
return {
|
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 refreshOpencodeCheck = async (distro: string, opts?: { signal?: AbortSignal }) => {
|
||||||
const resolved = await resolveWslOpencode(distro, opts)
|
setOpencodeCheck(distro, await checkOpencode(distro, opts))
|
||||||
const version = resolved ? await readWslCommandVersion(resolved, distro, opts) : null
|
}
|
||||||
setOpencodeCheck(distro, opencodeCheck(distro, resolved, version, appVersion))
|
|
||||||
|
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 }) => {
|
const refreshDistroLists = async (opts: { signal?: AbortSignal }) => {
|
||||||
@@ -170,10 +225,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
|||||||
setRuntime(id, { kind: "failed", message })
|
setRuntime(id, { kind: "failed", message })
|
||||||
logger?.error("wsl sidecar exited", { id, distro: item.config.distro, code, signal })
|
logger?.error("wsl sidecar exited", { id, distro: item.config.distro, code, signal })
|
||||||
})
|
})
|
||||||
void refreshOpencodeCheck(item.config.distro).catch((error) => {
|
refreshOpencodeCheckBackground(id, item.config.distro)
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
|
||||||
logger?.error("wsl opencode check failed", { id, distro: item.config.distro, message })
|
|
||||||
})
|
|
||||||
logger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url })
|
logger?.log("wsl sidecar ready", { id, distro: item.config.distro, url: sidecar.url })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
@@ -225,6 +277,7 @@ export function createWslServersController(appVersion: string, spawnSidecar: Spa
|
|||||||
|
|
||||||
async initialize() {
|
async initialize() {
|
||||||
refreshFromStore()
|
refreshFromStore()
|
||||||
|
void refreshOpencodeChecks()
|
||||||
for (const id of wslServerIdsToStartOnInitialize(state.servers.map((item) => item.config))) void startServer(id)
|
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,
|
id,
|
||||||
distro,
|
distro,
|
||||||
}
|
}
|
||||||
persistServers([...readPersistedServers(), config])
|
persistServers([...readServers(), config])
|
||||||
setState({
|
setState({
|
||||||
servers: [...state.servers, { config, runtime: { kind: "starting" } }],
|
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
|
const distro = state.servers.find((item) => item.config.id === id)?.config.distro
|
||||||
invalidateStartAttempt(id)
|
invalidateStartAttempt(id)
|
||||||
await stopServerInternal(id)
|
await stopServerInternal(id)
|
||||||
const remaining = readPersistedServers().filter((item) => item.id !== id)
|
const remaining = readServers().filter((item) => item.id !== id)
|
||||||
persistServers(remaining)
|
persistServers(remaining)
|
||||||
setState({
|
setState({
|
||||||
servers: state.servers.filter((item) => item.config.id !== id),
|
servers: state.servers.filter((item) => item.config.id !== id),
|
||||||
@@ -371,6 +424,10 @@ function readPersistedServers(): WslServerConfig[] {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writePersistedServers(servers: WslServerConfig[]) {
|
||||||
|
getStore().set(WSL_SERVERS_KEY, { servers })
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePersistedServer(value: unknown): WslServerConfig[] {
|
function normalizePersistedServer(value: unknown): WslServerConfig[] {
|
||||||
if (!value || typeof value !== "object") return []
|
if (!value || typeof value !== "object") return []
|
||||||
const record = value as Record<string, unknown>
|
const record = value as Record<string, unknown>
|
||||||
|
|||||||
Reference in New Issue
Block a user