fix(installer): avoid empty-version success on delayed stdout

This commit is contained in:
YeonGyu-Kim
2026-05-11 09:30:33 +09:00
parent 5d1c8718d7
commit f983b6b19e
2 changed files with 39 additions and 15 deletions
@@ -140,8 +140,8 @@ describe("getOpenCodeVersion (installer)", () => {
})
})
describe("#given quick successful exit #when getOpenCodeVersion #then clears the watchdog timer", () => {
it("avoids timer leak after success", async () => {
describe("#given quick successful exit #when getOpenCodeVersion #then clears active timers", () => {
it("avoids timer leaks after success", async () => {
spawnSpy.mockReturnValue(createProc({ output: { stdout: "1.14.33\n" } }))
const clearTimeoutSpy = spyOn(globalThis, "clearTimeout")
@@ -149,7 +149,7 @@ describe("getOpenCodeVersion (installer)", () => {
const result = await getOpenCodeVersion()
expect(result).toBe("1.14.33")
expect(clearTimeoutSpy).toHaveBeenCalledTimes(1)
expect(clearTimeoutSpy).toHaveBeenCalledTimes(2)
clearTimeoutSpy.mockRestore()
})
+36 -12
View File
@@ -23,15 +23,16 @@ async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | n
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) => {
let killGraceTimer: ReturnType<typeof setTimeout> | null = null
const timedExitResult = await Promise.race([
proc.exited.then((exitCode) => ({ type: "exit" as const, exitCode })),
new Promise<{ type: "timeout" }>((resolve) => {
killTimer = setTimeout(() => {
proc.kill("SIGTERM")
setTimeout(() => {
killGraceTimer = setTimeout(() => {
proc.kill("SIGKILL")
}, OPENCODE_VERSION_KILL_GRACE_MS)
resolve(1)
resolve({ type: "timeout" })
}, OPENCODE_VERSION_CHECK_TIMEOUT_MS)
}),
])
@@ -40,17 +41,40 @@ async function findOpenCodeBinaryWithVersion(): Promise<OpenCodeBinaryResult | n
clearTimeout(killTimer)
}
const output = await Promise.race([
outputPromise,
new Promise<string>((resolve) => {
setTimeout(() => {
resolve("")
if (timedExitResult.type === "timeout") {
void outputPromise.catch(() => {})
continue
}
if (killGraceTimer) {
clearTimeout(killGraceTimer)
}
let outputTimer: ReturnType<typeof setTimeout> | null = null
const outputResult = await Promise.race([
outputPromise.then((output) => ({ type: "output" as const, output })),
new Promise<{ type: "timeout" }>((resolve) => {
outputTimer = setTimeout(() => {
resolve({ type: "timeout" })
}, OPENCODE_OUTPUT_WAIT_TIMEOUT_MS)
}),
]).catch(() => "")
]).catch(() => ({ type: "timeout" as const }))
if (timedExitCode === 0 && proc.exitCode === 0) {
if (outputTimer) {
clearTimeout(outputTimer)
}
if (outputResult.type !== "output") {
continue
}
if (timedExitResult.exitCode === 0 && proc.exitCode === 0) {
const output = outputResult.output
const version = extractSemverFromOutput(output) ?? output.trim()
if (version.length === 0) {
continue
}
initConfigContext(binary, version)
return { binary, version }
}