fix(doctor): add timeouts to subprocess spawns to prevent exit code 137
Doctor checks spawned subprocesses (gh, opencode, sg) without timeouts, causing the process to hang indefinitely if any binary was stuck. The OS would then SIGKILL the process (exit code 137). - Add spawnWithTimeout utility with 10s per-spawn timeout - Add 30s overall doctor command timeout with graceful error message - Update all doctor check subprocess calls to use timeouts
This commit is contained in:
@@ -3,7 +3,7 @@ import { createRequire } from "node:module"
|
||||
import { dirname, join } from "node:path"
|
||||
|
||||
import type { DependencyInfo } from "../types"
|
||||
import { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide"
|
||||
import { spawnWithTimeout } from "../spawn-with-timeout"
|
||||
|
||||
async function checkBinaryExists(binary: string): Promise<{ exists: boolean; path: string | null }> {
|
||||
try {
|
||||
@@ -19,16 +19,12 @@ async function checkBinaryExists(binary: string): Promise<{ exists: boolean; pat
|
||||
|
||||
async function getBinaryVersion(binary: string): Promise<string | null> {
|
||||
try {
|
||||
const proc = spawnWithWindowsHide([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||
const output = await new Response(proc.stdout).text()
|
||||
await proc.exited
|
||||
if (proc.exitCode === 0) {
|
||||
return output.trim().split("\n")[0]
|
||||
}
|
||||
const result = await spawnWithTimeout([binary, "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||
if (result.timedOut || result.exitCode !== 0) return null
|
||||
return result.stdout.trim().split("\n")[0] ?? null
|
||||
} catch {
|
||||
// intentionally empty - version unavailable
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export async function checkAstGrepCli(): Promise<DependencyInfo> {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { existsSync } from "node:fs"
|
||||
import { homedir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide"
|
||||
import { spawnWithTimeout } from "../spawn-with-timeout"
|
||||
|
||||
import { OPENCODE_BINARIES } from "../constants"
|
||||
|
||||
@@ -111,12 +111,9 @@ export async function getOpenCodeVersion(
|
||||
): Promise<string | null> {
|
||||
try {
|
||||
const command = buildVersionCommand(binaryPath, platform)
|
||||
const processResult = spawnWithWindowsHide(command, { stdout: "pipe", stderr: "pipe" })
|
||||
const output = await new Response(processResult.stdout).text()
|
||||
await processResult.exited
|
||||
|
||||
if (processResult.exitCode !== 0) return null
|
||||
return output.trim() || null
|
||||
const result = await spawnWithTimeout(command, { stdout: "pipe", stderr: "pipe" })
|
||||
if (result.timedOut || result.exitCode !== 0) return null
|
||||
return result.stdout.trim() || null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawnWithWindowsHide } from "../../../shared/spawn-with-windows-hide"
|
||||
import { spawnWithTimeout } from "../spawn-with-timeout"
|
||||
|
||||
export interface GhCliInfo {
|
||||
installed: boolean
|
||||
@@ -21,13 +21,11 @@ async function checkBinaryExists(binary: string): Promise<{ exists: boolean; pat
|
||||
|
||||
async function getGhVersion(): Promise<string | null> {
|
||||
try {
|
||||
const processResult = spawnWithWindowsHide(["gh", "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||
const output = await new Response(processResult.stdout).text()
|
||||
await processResult.exited
|
||||
if (processResult.exitCode !== 0) return null
|
||||
const result = await spawnWithTimeout(["gh", "--version"], { stdout: "pipe", stderr: "pipe" })
|
||||
if (result.timedOut || result.exitCode !== 0) return null
|
||||
|
||||
const matchedVersion = output.match(/gh version (\S+)/)
|
||||
return matchedVersion?.[1] ?? output.trim().split("\n")[0] ?? null
|
||||
const matchedVersion = result.stdout.match(/gh version (\S+)/)
|
||||
return matchedVersion?.[1] ?? result.stdout.trim().split("\n")[0] ?? null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
@@ -40,18 +38,17 @@ async function getGhAuthStatus(): Promise<{
|
||||
error: string | null
|
||||
}> {
|
||||
try {
|
||||
const processResult = spawnWithWindowsHide(["gh", "auth", "status"], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1" },
|
||||
})
|
||||
const result = await spawnWithTimeout(
|
||||
["gh", "auth", "status"],
|
||||
{ stdout: "pipe", stderr: "pipe", env: { ...process.env, GH_NO_UPDATE_NOTIFIER: "1" } }
|
||||
)
|
||||
|
||||
const stdout = await new Response(processResult.stdout).text()
|
||||
const stderr = await new Response(processResult.stderr).text()
|
||||
await processResult.exited
|
||||
if (result.timedOut) {
|
||||
return { authenticated: false, username: null, scopes: [], error: "gh auth status timed out" }
|
||||
}
|
||||
|
||||
const output = stderr || stdout
|
||||
if (processResult.exitCode === 0) {
|
||||
const output = result.stdout
|
||||
if (result.exitCode === 0) {
|
||||
const usernameMatch = output.match(/Logged in to github\.com account (\S+)/)
|
||||
const scopesMatch = output.match(/Token scopes?:\s*(.+)/i)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user