summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2025-10-16 14:39:36 -0500
committerGitHub <[email protected]>2025-10-16 14:39:36 -0500
commitfc18fc8a08e703a54553e714344e638673b2d313 (patch)
tree5a732573260c47965e665543ca2997011c10a11c /packages
parent74747887786e5ee48a311ee5b59a42d3a23beecc (diff)
downloadopencode-fc18fc8a08e703a54553e714344e638673b2d313.tar.gz
opencode-fc18fc8a08e703a54553e714344e638673b2d313.zip
fix: bash hangs & orphans (#3225)
Diffstat (limited to 'packages')
-rw-r--r--packages/opencode/src/tool/bash.ts110
1 files changed, 84 insertions, 26 deletions
diff --git a/packages/opencode/src/tool/bash.ts b/packages/opencode/src/tool/bash.ts
index d4e8ce85a..8263e9305 100644
--- a/packages/opencode/src/tool/bash.ts
+++ b/packages/opencode/src/tool/bash.ts
@@ -14,6 +14,7 @@ import { Agent } from "../agent/agent"
const MAX_OUTPUT_LENGTH = 30_000
const DEFAULT_TIMEOUT = 1 * 60 * 1000
const MAX_TIMEOUT = 10 * 60 * 1000
+const SIGKILL_TIMEOUT_MS = 200
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,
cwd: Instance.directory,
- signal: ctx.abort,
stdio: ["ignore", "pipe", "pipe"],
- timeout,
+ detached: process.platform !== "win32",
})
let output = ""
@@ -163,38 +168,87 @@ export const BashTool = Tool.define("bash", {
},
})
- process.stdout?.on("data", (chunk) => {
+ const append = (chunk: Buffer) => {
output += chunk.toString()
ctx.metadata({
metadata: {
- output: output,
+ output,
description: params.description,
},
})
- })
+ }
- process.stderr?.on("data", (chunk) => {
- output += chunk.toString()
- ctx.metadata({
- metadata: {
- output: output,
- description: params.description,
- },
- })
- })
+ proc.stdout?.on("data", append)
+ proc.stderr?.on("data", append)
+
+ let timedOut = false
+ let aborted = false
+ let exited = false
- await new Promise<void>((resolve) => {
- process.on("close", () => {
+ 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()
})
- })
- ctx.metadata({
- metadata: {
- output: output,
- exit: process.exitCode,
- description: params.description,
- },
+ proc.once("error", (error) => {
+ exited = true
+ cleanup()
+ reject(error)
+ })
})
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)"
}
- if (process.signalCode === "SIGTERM" && params.timeout) {
+ if (timedOut) {
output += `\n\n(Command timed out after ${timeout} ms)`
}
+ if (aborted) {
+ output += "\n\n(Command was aborted)"
+ }
+
return {
title: params.command,
metadata: {
output,
- exit: process.exitCode,
+ exit: proc.exitCode,
description: params.description,
},
output,