Aiden Cline 4 месяцев назад
Родитель
Сommit
fc18fc8a08
1 измененных файлов с 84 добавлено и 26 удалено
  1. 84 26
      packages/opencode/src/tool/bash.ts

+ 84 - 26
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,