mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-30 03:54:59 +00:00
feat: branch Electron local startup by runtime
This commit is contained in:
parent
482dc3a15d
commit
5aa544179d
2 changed files with 109 additions and 30 deletions
|
|
@ -42,15 +42,14 @@ import { createLocalServerController } from "./local-server"
|
|||
import { initLogging } from "./logging"
|
||||
import { parseMarkdown } from "./markdown"
|
||||
import { createMenu } from "./menu"
|
||||
import { getDefaultServerUrl, setDefaultServerUrl, spawnLocalServer } from "./server"
|
||||
import { getDefaultServerUrl, setDefaultServerUrl, spawnLocalServer, spawnWslLocalServer } from "./server"
|
||||
import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows"
|
||||
import type { Server } from "virtual:opencode-server"
|
||||
|
||||
const initEmitter = new EventEmitter()
|
||||
let initStep: InitStep = { phase: "server_waiting" }
|
||||
|
||||
let mainWindow: BrowserWindow | null = null
|
||||
let server: Server.Listener | null = null
|
||||
let server: { stop(): void } | null = null
|
||||
const loadingComplete = defer<void>()
|
||||
|
||||
const pendingDeepLinks: string[] = []
|
||||
|
|
@ -140,26 +139,47 @@ async function initialize() {
|
|||
const hostname = "127.0.0.1"
|
||||
const url = `http://${hostname}:${port}`
|
||||
const password = randomUUID()
|
||||
const config = localServer.getState().config
|
||||
const runtime =
|
||||
config.mode === "wsl" && config.distro
|
||||
? {
|
||||
key: `local:wsl:${config.distro}`,
|
||||
mode: "wsl" as const,
|
||||
distro: config.distro,
|
||||
}
|
||||
: {
|
||||
key: "local:windows",
|
||||
mode: "windows" as const,
|
||||
distro: null,
|
||||
}
|
||||
|
||||
logger.log("spawning sidecar", { url })
|
||||
localServer.setRuntime({ key: "local:windows", mode: "windows", distro: null })
|
||||
localServer.setRuntime(runtime)
|
||||
localServer.setStatus({ kind: "running", step: null })
|
||||
const { listener, health } = await spawnLocalServer(hostname, port, password).catch((error) => {
|
||||
localServer.setStatus({
|
||||
kind: "failed",
|
||||
step: null,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
throw error
|
||||
})
|
||||
server = listener
|
||||
const runtime = localServer.getState().runtime
|
||||
serverReady.resolve({
|
||||
url,
|
||||
username: "opencode",
|
||||
password,
|
||||
local: runtime,
|
||||
})
|
||||
const startup = await (async () => {
|
||||
try {
|
||||
if (runtime.mode === "wsl") {
|
||||
if (!runtime.distro) throw new Error("No WSL distro selected")
|
||||
return spawnWslLocalServer(runtime.distro, port, password)
|
||||
}
|
||||
return spawnLocalServer(hostname, port, password)
|
||||
} catch (error) {
|
||||
localServer.setStatus({
|
||||
kind: "failed",
|
||||
step: null,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
logger.error("local server startup failed", error)
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
server = startup?.listener ?? null
|
||||
|
||||
const loadingTask = (async () => {
|
||||
logger.log("sidecar connection started", { url })
|
||||
|
|
@ -175,23 +195,25 @@ async function initialize() {
|
|||
await sqliteDone?.promise
|
||||
}
|
||||
|
||||
await Promise.race([
|
||||
health.wait,
|
||||
delay(30_000).then(() => {
|
||||
throw new Error("Sidecar health check timed out")
|
||||
}),
|
||||
])
|
||||
.then(() => {
|
||||
localServer.setStatus({ kind: "ready" })
|
||||
})
|
||||
.catch((error) => {
|
||||
localServer.setStatus({
|
||||
kind: "failed",
|
||||
step: null,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
if (startup) {
|
||||
await Promise.race([
|
||||
startup.health.wait,
|
||||
delay(30_000).then(() => {
|
||||
throw new Error("Sidecar health check timed out")
|
||||
}),
|
||||
])
|
||||
.then(() => {
|
||||
localServer.setStatus({ kind: "ready" })
|
||||
})
|
||||
logger.error("sidecar health check failed", error)
|
||||
})
|
||||
.catch((error) => {
|
||||
localServer.setStatus({
|
||||
kind: "failed",
|
||||
step: null,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
logger.error("sidecar health check failed", error)
|
||||
})
|
||||
}
|
||||
|
||||
logger.log("loading task finished")
|
||||
})()
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { spawn } from "node:child_process"
|
||||
import { app } from "electron"
|
||||
import { DEFAULT_SERVER_URL_KEY } from "./constants"
|
||||
import { getUserShell, loadShellEnv } from "./shell-env"
|
||||
import { getStore } from "./store"
|
||||
import { wslArgs } from "./wsl"
|
||||
|
||||
export type HealthCheck = { wait: Promise<void> }
|
||||
|
||||
|
|
@ -46,6 +48,57 @@ export async function spawnLocalServer(hostname: string, port: number, password:
|
|||
return { listener, health: { wait } }
|
||||
}
|
||||
|
||||
export async function spawnWslLocalServer(distro: string, port: number, password: string) {
|
||||
const script = [
|
||||
"set -e",
|
||||
"OPENCODE_EXPERIMENTAL_ICON_DISCOVERY=true",
|
||||
"OPENCODE_EXPERIMENTAL_FILEWATCHER=true",
|
||||
"OPENCODE_CLIENT=desktop",
|
||||
`OPENCODE_SERVER_USERNAME=${shellEscape("opencode")}`,
|
||||
`OPENCODE_SERVER_PASSWORD=${shellEscape(password)}`,
|
||||
'XDG_STATE_HOME="$HOME/.local/state"',
|
||||
`exec opencode --print-logs --log-level WARN serve --hostname 0.0.0.0 --port ${port}`,
|
||||
].join(" ")
|
||||
|
||||
const child = spawn("wsl", wslArgs(["bash", "-lc", script], distro), {
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
windowsHide: true,
|
||||
})
|
||||
child.stdout.setEncoding("utf8")
|
||||
child.stderr.setEncoding("utf8")
|
||||
|
||||
const exit = new Promise<never>((_, reject) => {
|
||||
child.once("error", reject)
|
||||
child.once("exit", (code, signal) => {
|
||||
reject(
|
||||
new Error(
|
||||
`WSL local server exited before becoming healthy (code=${code ?? "null"} signal=${signal ?? "null"})`,
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const wait = Promise.race([
|
||||
(async () => {
|
||||
const url = `http://127.0.0.1:${port}`
|
||||
while (true) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
if (await checkHealth(url, password)) return
|
||||
}
|
||||
})(),
|
||||
exit,
|
||||
])
|
||||
|
||||
return {
|
||||
listener: {
|
||||
stop() {
|
||||
child.kill()
|
||||
},
|
||||
},
|
||||
health: { wait },
|
||||
}
|
||||
}
|
||||
|
||||
function prepareServerEnv(password: string) {
|
||||
const shell = process.platform === "win32" ? null : getUserShell()
|
||||
const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {}
|
||||
|
|
@ -62,6 +115,10 @@ function prepareServerEnv(password: string) {
|
|||
Object.assign(process.env, env)
|
||||
}
|
||||
|
||||
function shellEscape(value: string) {
|
||||
return `'${value.replace(/'/g, `'"'"'`)}'`
|
||||
}
|
||||
|
||||
export async function checkHealth(url: string, password?: string | null): Promise<boolean> {
|
||||
let healthUrl: URL
|
||||
try {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue