diff --git a/src/cli/config-manager/opencode-binary.test.ts b/src/cli/config-manager/opencode-binary.test.ts index 99113da9a..27171db5b 100644 --- a/src/cli/config-manager/opencode-binary.test.ts +++ b/src/cli/config-manager/opencode-binary.test.ts @@ -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() }) diff --git a/src/cli/config-manager/opencode-binary.ts b/src/cli/config-manager/opencode-binary.ts index ff650da3c..79e4ee542 100644 --- a/src/cli/config-manager/opencode-binary.ts +++ b/src/cli/config-manager/opencode-binary.ts @@ -23,15 +23,16 @@ async function findOpenCodeBinaryWithVersion(): Promise | null = null - const timedExitCode = await Promise.race([ - proc.exited, - new Promise((resolve) => { + let killGraceTimer: ReturnType | 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((resolve) => { - setTimeout(() => { - resolve("") + if (timedExitResult.type === "timeout") { + void outputPromise.catch(() => {}) + continue + } + + if (killGraceTimer) { + clearTimeout(killGraceTimer) + } + + let outputTimer: ReturnType | 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 } }