Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 23 additions & 8 deletions packages/opencode/src/shell/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from "path"
import { spawn, type ChildProcess } from "child_process"

const SIGKILL_TIMEOUT_MS = 200
const SIGINT_TIMEOUT_MS = 250

export namespace Shell {
export async function killTree(proc: ChildProcess, opts?: { exited?: () => boolean }): Promise<void> {
Expand All @@ -19,18 +20,32 @@ export namespace Shell {
return
}

try {
const exited = () => opts?.exited?.() === true

const killGroup = async () => {
process.kill(-pid, "SIGINT")
await Bun.sleep(SIGINT_TIMEOUT_MS)
if (exited()) return
process.kill(-pid, "SIGTERM")
await Bun.sleep(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
process.kill(-pid, "SIGKILL")
}
} catch (_e) {
if (exited()) return
process.kill(-pid, "SIGKILL")
}

const killSingle = async () => {
proc.kill("SIGINT")
await Bun.sleep(SIGINT_TIMEOUT_MS)
if (exited()) return
proc.kill("SIGTERM")
await Bun.sleep(SIGKILL_TIMEOUT_MS)
if (!opts?.exited?.()) {
proc.kill("SIGKILL")
}
if (exited()) return
proc.kill("SIGKILL")
}

try {
await killGroup()
} catch (_e) {
await killSingle()
}
}
const BLACKLIST = new Set(["fish", "nu"])
Expand Down
27 changes: 27 additions & 0 deletions packages/opencode/test/tool/bash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,33 @@ describe("tool.bash", () => {
},
})
})

test("abort sends SIGINT before SIGKILL", async () => {
await Instance.provide({
directory: projectRoot,
fn: async () => {
const controller = new AbortController()
const bash = await BashTool.init()
const promise = bash.execute(
{
command: "trap 'echo INT_CAUGHT; exit 0' INT; while true; do sleep 0.05; done",
description: "Loop until interrupted",
timeout: 60_000,
},
{
...ctx,
abort: controller.signal,
},
)

await Bun.sleep(150)
controller.abort()

const result = await promise
expect(result.output).toContain("INT_CAUGHT")
},
})
})
})

describe("tool.bash permissions", () => {
Expand Down