mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-22 11:55:46 +00:00
feat: add Electron local server probe actions
This commit is contained in:
@@ -253,6 +253,8 @@ registerIpcHandlers({
|
||||
},
|
||||
getLocalServerState: () => localServer.getState(),
|
||||
setLocalServerConfig: (config) => localServer.setConfig(config),
|
||||
runLocalServerStep: (step) => localServer.runStep(step),
|
||||
cancelLocalServerJob: () => localServer.cancelJob(),
|
||||
onLocalServerEvent: (listener) => localServer.subscribe(listener),
|
||||
getDefaultServerUrl: () => getDefaultServerUrl(),
|
||||
setDefaultServerUrl: (url) => setDefaultServerUrl(url),
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
LocalServerConfig,
|
||||
LocalServerEvent,
|
||||
LocalServerState,
|
||||
LocalServerStep,
|
||||
ServerReadyData,
|
||||
SqliteMigrationProgress,
|
||||
TitlebarTheme,
|
||||
@@ -24,6 +25,8 @@ type Deps = {
|
||||
awaitInitialization: (sendStep: (step: InitStep) => void) => Promise<ServerReadyData>
|
||||
getLocalServerState: () => Promise<LocalServerState> | LocalServerState
|
||||
setLocalServerConfig: (config: LocalServerConfig) => Promise<void> | void
|
||||
runLocalServerStep: (step: LocalServerStep) => Promise<void> | void
|
||||
cancelLocalServerJob: () => Promise<void> | void
|
||||
onLocalServerEvent: (listener: (event: LocalServerEvent) => void) => () => void
|
||||
getDefaultServerUrl: () => Promise<string | null> | string | null
|
||||
setDefaultServerUrl: (url: string | null) => Promise<void> | void
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import type { LocalServerConfig, LocalServerEvent, LocalServerState, LocalServerStep } from "../preload/types"
|
||||
import type {
|
||||
LocalServerConfig,
|
||||
LocalServerDistroCheck,
|
||||
LocalServerEvent,
|
||||
LocalServerState,
|
||||
LocalServerStep,
|
||||
} from "../preload/types"
|
||||
import { LOCAL_SERVER_KEY } from "./constants"
|
||||
import { store } from "./store"
|
||||
import { listInstalledWslDistros, listOnlineWslDistros, probeWslDistro, probeWslRuntime } from "./wsl"
|
||||
|
||||
export function defaultLocalServerConfig(): LocalServerConfig {
|
||||
return {
|
||||
@@ -21,6 +28,7 @@ export function defaultLocalServerConfig(): LocalServerConfig {
|
||||
export function createLocalServerController() {
|
||||
let state = toState(readLocalServerConfig())
|
||||
const listeners = new Set<(event: LocalServerEvent) => void>()
|
||||
let jobAbort: AbortController | undefined
|
||||
|
||||
const emit = (event: LocalServerEvent) => {
|
||||
for (const listener of listeners) listener(event)
|
||||
@@ -47,6 +55,101 @@ export function createLocalServerController() {
|
||||
listeners.add(listener)
|
||||
return () => listeners.delete(listener)
|
||||
},
|
||||
async runStep(step: LocalServerStep) {
|
||||
jobAbort?.abort()
|
||||
const abort = new AbortController()
|
||||
jobAbort = abort
|
||||
update({
|
||||
...state,
|
||||
job: { step, startedAt: Date.now() },
|
||||
status: { kind: "running", step },
|
||||
})
|
||||
|
||||
try {
|
||||
if (step === "wsl") {
|
||||
const wsl = await probeWslRuntime({ signal: abort.signal })
|
||||
if (jobAbort !== abort) return
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: wsl.available
|
||||
? { kind: "ready" }
|
||||
: { kind: "failed", step, message: wsl.error ?? "WSL is unavailable" },
|
||||
checks: {
|
||||
...state.checks,
|
||||
wsl,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (step === "distro") {
|
||||
const [installedResult, onlineResult] = await Promise.allSettled([
|
||||
listInstalledWslDistros({ signal: abort.signal }),
|
||||
listOnlineWslDistros({ signal: abort.signal }),
|
||||
])
|
||||
if (jobAbort !== abort) return
|
||||
|
||||
const installed = installedResult.status === "fulfilled" ? installedResult.value : []
|
||||
const online = onlineResult.status === "fulfilled" ? onlineResult.value : []
|
||||
const selected = state.config.distro
|
||||
? await probeWslDistro(state.config.distro, { signal: abort.signal })
|
||||
: null
|
||||
if (jobAbort !== abort) return
|
||||
|
||||
const error = distroError(state.config.distro, installed, selected, installedResult, onlineResult)
|
||||
const distro: LocalServerDistroCheck = {
|
||||
installed,
|
||||
online,
|
||||
selected,
|
||||
error,
|
||||
}
|
||||
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: error ? { kind: "failed", step, message: error } : { kind: "ready" },
|
||||
checks: {
|
||||
...state.checks,
|
||||
distro,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "idle" },
|
||||
})
|
||||
} catch (error) {
|
||||
if (jobAbort !== abort) return
|
||||
if (error instanceof Error && error.name === "AbortError") {
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "idle" },
|
||||
})
|
||||
return
|
||||
}
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "failed", step, message: error instanceof Error ? error.message : String(error) },
|
||||
})
|
||||
} finally {
|
||||
if (jobAbort === abort) jobAbort = undefined
|
||||
}
|
||||
},
|
||||
cancelJob() {
|
||||
jobAbort?.abort()
|
||||
jobAbort = undefined
|
||||
update({
|
||||
...state,
|
||||
job: null,
|
||||
status: { kind: "idle" },
|
||||
})
|
||||
},
|
||||
setRuntime(runtime: LocalServerState["runtime"]) {
|
||||
update({
|
||||
...state,
|
||||
@@ -72,6 +175,7 @@ function toState(config: LocalServerConfig, current?: LocalServerState): LocalSe
|
||||
runtime: current?.runtime ?? windowsRuntime(),
|
||||
status: current?.status ?? { kind: "idle" },
|
||||
job: current?.job ?? null,
|
||||
checks: current?.checks ?? { wsl: null, distro: null },
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,3 +244,23 @@ function windowsRuntime(): LocalServerState["runtime"] {
|
||||
distro: null,
|
||||
}
|
||||
}
|
||||
|
||||
function distroError(
|
||||
configured: string | null,
|
||||
installed: LocalServerDistroCheck["installed"],
|
||||
selected: LocalServerDistroCheck["selected"],
|
||||
installedResult: PromiseSettledResult<LocalServerDistroCheck["installed"]>,
|
||||
onlineResult: PromiseSettledResult<LocalServerDistroCheck["online"]>,
|
||||
) {
|
||||
if (installedResult.status === "rejected") {
|
||||
return installedResult.reason instanceof Error ? installedResult.reason.message : String(installedResult.reason)
|
||||
}
|
||||
if (onlineResult.status === "rejected") {
|
||||
return onlineResult.reason instanceof Error ? onlineResult.reason.message : String(onlineResult.reason)
|
||||
}
|
||||
if (configured && !installed.find((item) => item.name === configured)) {
|
||||
return `Selected distro is not installed: ${configured}`
|
||||
}
|
||||
if (selected?.error) return selected.error
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
import { spawn } from "node:child_process"
|
||||
import type {
|
||||
LocalServerDistroProbe,
|
||||
LocalServerInstalledDistro,
|
||||
LocalServerOnlineDistro,
|
||||
LocalServerWslCheck,
|
||||
} from "../preload/types"
|
||||
|
||||
export type WslCommandLine = {
|
||||
stream: "stdout" | "stderr"
|
||||
@@ -12,35 +18,6 @@ export type WslCommandResult = {
|
||||
stderr: string
|
||||
}
|
||||
|
||||
export type WslRuntimeProbe = {
|
||||
available: boolean
|
||||
version: string | null
|
||||
status: string | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
export type WslInstalledDistro = {
|
||||
name: string
|
||||
state: string | null
|
||||
version: number | null
|
||||
isDefault: boolean
|
||||
}
|
||||
|
||||
export type WslOnlineDistro = {
|
||||
name: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export type WslDistroProbe = {
|
||||
name: string
|
||||
canExecute: boolean
|
||||
hasBash: boolean
|
||||
hasCurl: boolean
|
||||
username: string | null
|
||||
isRoot: boolean | null
|
||||
error: string | null
|
||||
}
|
||||
|
||||
type RunWslOptions = {
|
||||
onLine?: (line: WslCommandLine) => void
|
||||
signal?: AbortSignal
|
||||
@@ -113,8 +90,8 @@ export function runWslBash(script: string, distro?: string | null, opts?: RunWsl
|
||||
return runWslInDistro(["bash", "-lc", script], distro, opts)
|
||||
}
|
||||
|
||||
export async function probeWslRuntime(): Promise<WslRuntimeProbe> {
|
||||
const version = await runWsl(["--version"]).catch((error) => ({
|
||||
export async function probeWslRuntime(opts?: RunWslOptions): Promise<LocalServerWslCheck> {
|
||||
const version = await runWsl(["--version"], opts).catch((error) => ({
|
||||
code: 1,
|
||||
signal: null,
|
||||
stdout: "",
|
||||
@@ -130,7 +107,7 @@ export async function probeWslRuntime(): Promise<WslRuntimeProbe> {
|
||||
}
|
||||
}
|
||||
|
||||
const status = await runWsl(["--status"]).catch(() => undefined)
|
||||
const status = await runWsl(["--status"], opts).catch(() => undefined)
|
||||
return {
|
||||
available: true,
|
||||
version: firstLine(version.stdout),
|
||||
@@ -139,16 +116,16 @@ export async function probeWslRuntime(): Promise<WslRuntimeProbe> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function listInstalledWslDistros() {
|
||||
const result = await runWsl(["--list", "--verbose"])
|
||||
export async function listInstalledWslDistros(opts?: RunWslOptions) {
|
||||
const result = await runWsl(["--list", "--verbose"], opts)
|
||||
if (result.code !== 0) {
|
||||
throw new Error(summarize(result.stderr || result.stdout) || "Failed to list installed WSL distros")
|
||||
}
|
||||
return parseInstalledDistros(result.stdout)
|
||||
}
|
||||
|
||||
export async function listOnlineWslDistros() {
|
||||
const result = await runWsl(["--list", "--online"])
|
||||
export async function listOnlineWslDistros(opts?: RunWslOptions) {
|
||||
const result = await runWsl(["--list", "--online"], opts)
|
||||
if (result.code !== 0) {
|
||||
throw new Error(summarize(result.stderr || result.stdout) || "Failed to list online WSL distros")
|
||||
}
|
||||
@@ -163,8 +140,8 @@ export async function installWslDistro(name: string, opts?: RunWslOptions) {
|
||||
return runWsl(["--install", "-d", name, "--web-download", "--no-launch"], opts)
|
||||
}
|
||||
|
||||
export async function probeWslDistro(name: string): Promise<WslDistroProbe> {
|
||||
const executable = await runWslInDistro(["/bin/true"], name).catch((error) => ({
|
||||
export async function probeWslDistro(name: string, opts?: RunWslOptions): Promise<LocalServerDistroProbe> {
|
||||
const executable = await runWslInDistro(["/bin/true"], name, opts).catch((error) => ({
|
||||
code: 1,
|
||||
signal: null,
|
||||
stdout: "",
|
||||
@@ -183,9 +160,9 @@ export async function probeWslDistro(name: string): Promise<WslDistroProbe> {
|
||||
}
|
||||
|
||||
const [bash, curl, user] = await Promise.all([
|
||||
runWslSh("command -v bash >/dev/null && printf yes || printf no", name),
|
||||
runWslSh("command -v curl >/dev/null && printf yes || printf no", name),
|
||||
runWslSh("id -un 2>/dev/null || true", name),
|
||||
runWslSh("command -v bash >/dev/null && printf yes || printf no", name, opts),
|
||||
runWslSh("command -v curl >/dev/null && printf yes || printf no", name, opts),
|
||||
runWslSh("id -un 2>/dev/null || true", name, opts),
|
||||
])
|
||||
|
||||
const username = summarize(user.stdout)
|
||||
@@ -228,7 +205,7 @@ function parseInstalledDistros(output: string) {
|
||||
state: state || null,
|
||||
version: Number.isNaN(Number.parseInt(version, 10)) ? null : Number.parseInt(version, 10),
|
||||
isDefault: marker === "*",
|
||||
} satisfies WslInstalledDistro,
|
||||
} satisfies LocalServerInstalledDistro,
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -241,7 +218,7 @@ function parseOnlineDistros(output: string) {
|
||||
if (!match) return []
|
||||
const [, name, label] = match
|
||||
if (/^name$/i.test(name)) return []
|
||||
return [{ name, label: label.trim() } satisfies WslOnlineDistro]
|
||||
return [{ name, label: label.trim() } satisfies LocalServerOnlineDistro]
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ const api: ElectronAPI = {
|
||||
localServer: {
|
||||
getState: () => ipcRenderer.invoke("local-server-get-state"),
|
||||
setConfig: (config) => ipcRenderer.invoke("local-server-set-config", config),
|
||||
runStep: (step) => ipcRenderer.invoke("local-server-run-step", step),
|
||||
cancelJob: () => ipcRenderer.invoke("local-server-cancel-job"),
|
||||
subscribe: (cb) => {
|
||||
const handler = (_: unknown, event: LocalServerEvent) => cb(event)
|
||||
ipcRenderer.on("local-server-event", handler)
|
||||
|
||||
@@ -14,6 +14,37 @@ export type LocalServerMismatchAcknowledgement = {
|
||||
path: string
|
||||
version: string
|
||||
}
|
||||
export type LocalServerWslCheck = {
|
||||
available: boolean
|
||||
version: string | null
|
||||
status: string | null
|
||||
error: string | null
|
||||
}
|
||||
export type LocalServerInstalledDistro = {
|
||||
name: string
|
||||
state: string | null
|
||||
version: number | null
|
||||
isDefault: boolean
|
||||
}
|
||||
export type LocalServerOnlineDistro = {
|
||||
name: string
|
||||
label: string
|
||||
}
|
||||
export type LocalServerDistroProbe = {
|
||||
name: string
|
||||
canExecute: boolean
|
||||
hasBash: boolean
|
||||
hasCurl: boolean
|
||||
username: string | null
|
||||
isRoot: boolean | null
|
||||
error: string | null
|
||||
}
|
||||
export type LocalServerDistroCheck = {
|
||||
installed: LocalServerInstalledDistro[]
|
||||
online: LocalServerOnlineDistro[]
|
||||
selected: LocalServerDistroProbe | null
|
||||
error: string | null
|
||||
}
|
||||
export type LocalServerConfig = {
|
||||
mode: LocalServerMode
|
||||
distro: string | null
|
||||
@@ -41,6 +72,10 @@ export type LocalServerState = {
|
||||
}
|
||||
status: LocalServerStatus
|
||||
job: { step: LocalServerStep | null; startedAt: number } | null
|
||||
checks: {
|
||||
wsl: LocalServerWslCheck | null
|
||||
distro: LocalServerDistroCheck | null
|
||||
}
|
||||
}
|
||||
export type LocalServerEvent = {
|
||||
type: "state"
|
||||
@@ -49,6 +84,8 @@ export type LocalServerEvent = {
|
||||
export type LocalServerAPI = {
|
||||
getState: () => Promise<LocalServerState>
|
||||
setConfig: (config: LocalServerConfig) => Promise<void>
|
||||
runStep: (step: LocalServerStep) => Promise<void>
|
||||
cancelJob: () => Promise<void>
|
||||
subscribe: (cb: (event: LocalServerEvent) => void) => () => void
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user