mirror of
https://github.com/aljazceru/opencode.git
synced 2026-01-09 02:44:55 +01:00
fix: bash hangs & orphans (#3225)
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user