Skip to content

Commit 4b312a0

Browse files
committed
bash tool: kill the whole process tree on timeout (process group on Unix, taskkill /T on Windows)
1 parent 41a4dd4 commit 4b312a0

4 files changed

Lines changed: 59 additions & 0 deletions

File tree

‎tools/proc_unix.go‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
//go:build !windows
2+
3+
package tools
4+
5+
import (
6+
"os/exec"
7+
"syscall"
8+
)
9+
10+
// killTreeOnCancel runs the command in its own process group and kills the
11+
// whole group on timeout, so children of `sh -c` don't outlive it.
12+
func killTreeOnCancel(cmd *exec.Cmd) {
13+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
14+
cmd.Cancel = func() error {
15+
return syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
16+
}
17+
}

‎tools/proc_windows.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//go:build windows
2+
3+
package tools
4+
5+
import (
6+
"os/exec"
7+
"strconv"
8+
)
9+
10+
// killTreeOnCancel kills the whole process tree on timeout, so children of
11+
// `cmd /C` don't outlive it (and keep the working directory locked).
12+
func killTreeOnCancel(cmd *exec.Cmd) {
13+
cmd.Cancel = func() error {
14+
kill := exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
15+
if err := kill.Run(); err != nil {
16+
return cmd.Process.Kill()
17+
}
18+
return nil
19+
}
20+
}

‎tools/tools.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ func (Bash) Call(ctx context.Context, tc core.ToolContext, input json.RawMessage
261261
cmd = exec.CommandContext(ctx, "sh", "-c", a.Command)
262262
}
263263
cmd.Dir = tc.Cwd
264+
killTreeOnCancel(cmd)
264265
cmd.WaitDelay = time.Second // don't hang on grandchildren holding the pipes
265266
var stdout, stderr strings.Builder
266267
cmd.Stdout, cmd.Stderr = &stdout, &stderr

‎tools/tree_test.go‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build !windows
2+
3+
package tools
4+
5+
import (
6+
"os/exec"
7+
"strings"
8+
"testing"
9+
"time"
10+
)
11+
12+
func TestBashTimeoutKillsChildren(t *testing.T) {
13+
_, err := call(t, Bash{}, t.TempDir(), `{"command":"sleep 7.123 & sleep 7.123; wait","timeout_secs":1}`)
14+
if err == nil || !strings.Contains(err.Error(), "timed out") {
15+
t.Fatal(err)
16+
}
17+
time.Sleep(200 * time.Millisecond)
18+
if out, _ := exec.Command("pgrep", "-f", "sleep 7.123").Output(); len(out) > 0 {
19+
t.Fatalf("children survived: %s", out)
20+
}
21+
}

0 commit comments

Comments
 (0)