Files
oh-my-opencode/src/cli/config-manager/opencode-binary.ts
T
YeonGyu-Kim 5d1c8718d7 fix(installer): bound outputPromise wait after kill to prevent indirect hang
After SIGTERM/SIGKILL escalation, the stdout stream may not close immediately on all platforms. The unconditional await on outputPromise could then hang indefinitely, defeating the bounded process lifetime guarantee. Race outputPromise against a short follow-up timeout to ensure getOpenCodeVersion always returns within a bounded time.

Refs #3766
2026-05-11 09:24:42 +09:00

73 lines
2.2 KiB
TypeScript

import { extractSemverFromOutput } from "../../shared/extract-semver"
import type { OpenCodeBinaryType } from "../../shared/opencode-config-dir-types"
import { spawnWithWindowsHide } from "../../shared/spawn-with-windows-hide"
import { initConfigContext } from "./config-context"
const OPENCODE_BINARIES = ["opencode", "opencode-desktop"] as const
const OPENCODE_VERSION_CHECK_TIMEOUT_MS = 1500
const OPENCODE_VERSION_KILL_GRACE_MS = 200
const OPENCODE_OUTPUT_WAIT_TIMEOUT_MS = 200
interface OpenCodeBinaryResult {
binary: OpenCodeBinaryType
version: string
}
async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | null> {
for (const binary of OPENCODE_BINARIES) {
try {
const proc = spawnWithWindowsHide([binary, "--version"], {
stdout: "pipe",
stderr: "pipe",
})
const outputPromise = new Response(proc.stdout).text()
let killTimer: ReturnType<typeof setTimeout> | null = null
const timedExitCode = await Promise.race([
proc.exited,
new Promise<number>((resolve) => {
killTimer = setTimeout(() => {
proc.kill("SIGTERM")
setTimeout(() => {
proc.kill("SIGKILL")
}, OPENCODE_VERSION_KILL_GRACE_MS)
resolve(1)
}, OPENCODE_VERSION_CHECK_TIMEOUT_MS)
}),
])
if (killTimer) {
clearTimeout(killTimer)
}
const output = await Promise.race([
outputPromise,
new Promise<string>((resolve) => {
setTimeout(() => {
resolve("")
}, OPENCODE_OUTPUT_WAIT_TIMEOUT_MS)
}),
]).catch(() => "")
if (timedExitCode === 0 && proc.exitCode === 0) {
const version = extractSemverFromOutput(output) ?? output.trim()
initConfigContext(binary, version)
return { binary, version }
}
} catch {
continue
}
}
return null
}
export async function isOpenCodeInstalled(): Promise<boolean> {
const result = await findOpenCodeBinaryWithVersion()
return result !== null
}
export async function getOpenCodeVersion(): Promise<string | null> {
const result = await findOpenCodeBinaryWithVersion()
return result?.version ?? null
}