fix: bash hangs & orphans (#3225)

This commit is contained in:
Aiden Cline
2025-10-16 14:39:36 -05:00
committed by GitHub
parent 7474788778
commit fc18fc8a08

View File

@@ -14,6 +14,7 @@ import { Agent } from "../agent/agent"
const MAX_OUTPUT_LENGTH = 30_000 const MAX_OUTPUT_LENGTH = 30_000
const DEFAULT_TIMEOUT = 1 * 60 * 1000 const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000 const MAX_TIMEOUT = 10 * 60 * 1000
const SIGKILL_TIMEOUT_MS = 200
const log = Log.create({ service: "bash-tool" }) const log = Log.create({ service: "bash-tool" })
@@ -145,12 +146,16 @@ export const BashTool = Tool.define("bash", {
}) })
} }
const process = spawn(params.command, { const pause = (ms: number) =>
new Promise<void>((resolve) => {
setTimeout(resolve, ms)
})
const proc = spawn(params.command, {
shell: true, shell: true,
cwd: Instance.directory, cwd: Instance.directory,
signal: ctx.abort,
stdio: ["ignore", "pipe", "pipe"], stdio: ["ignore", "pipe", "pipe"],
timeout, detached: process.platform !== "win32",
}) })
let output = "" let output = ""
@@ -163,38 +168,87 @@ export const BashTool = Tool.define("bash", {
}, },
}) })
process.stdout?.on("data", (chunk) => { const append = (chunk: Buffer) => {
output += chunk.toString() output += chunk.toString()
ctx.metadata({ ctx.metadata({
metadata: { metadata: {
output: output, output,
description: params.description, description: params.description,
}, },
}) })
}) }
process.stderr?.on("data", (chunk) => { proc.stdout?.on("data", append)
output += chunk.toString() proc.stderr?.on("data", append)
ctx.metadata({
metadata: {
output: output,
description: params.description,
},
})
})
await new Promise<void>((resolve) => { let timedOut = false
process.on("close", () => { let aborted = false
let exited = false
const killTree = async () => {
const pid = proc.pid
if (!pid || exited) {
return
}
if (process.platform === "win32") {
await new Promise<void>((resolve) => {
const killer = spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { stdio: "ignore" })
killer.once("exit", resolve)
killer.once("error", resolve)
})
return
}
try {
process.kill(-pid, "SIGTERM")
await pause(SIGKILL_TIMEOUT_MS)
if (!exited) {
process.kill(-pid, "SIGKILL")
}
} catch (_e) {
proc.kill("SIGTERM")
await pause(SIGKILL_TIMEOUT_MS)
if (!exited) {
proc.kill("SIGKILL")
}
}
}
if (ctx.abort.aborted) {
aborted = true
await killTree()
}
const abortHandler = () => {
aborted = true
void killTree()
}
ctx.abort.addEventListener("abort", abortHandler, { once: true })
const timeoutTimer = setTimeout(() => {
timedOut = true
void killTree()
}, timeout)
await new Promise<void>((resolve, reject) => {
const cleanup = () => {
clearTimeout(timeoutTimer)
ctx.abort.removeEventListener("abort", abortHandler)
}
proc.once("exit", () => {
exited = true
cleanup()
resolve() resolve()
}) })
})
ctx.metadata({ proc.once("error", (error) => {
metadata: { exited = true
output: output, cleanup()
exit: process.exitCode, reject(error)
description: params.description, })
},
}) })
if (output.length > MAX_OUTPUT_LENGTH) { if (output.length > MAX_OUTPUT_LENGTH) {
@@ -202,15 +256,19 @@ export const BashTool = Tool.define("bash", {
output += "\n\n(Output was truncated due to length limit)" output += "\n\n(Output was truncated due to length limit)"
} }
if (process.signalCode === "SIGTERM" && params.timeout) { if (timedOut) {
output += `\n\n(Command timed out after ${timeout} ms)` output += `\n\n(Command timed out after ${timeout} ms)`
} }
if (aborted) {
output += "\n\n(Command was aborted)"
}
return { return {
title: params.command, title: params.command,
metadata: { metadata: {
output, output,
exit: process.exitCode, exit: proc.exitCode,
description: params.description, description: params.description,
}, },
output, output,