Set a timeout for a command
Stop a command that takes too long with exec.CommandContext and context.WithTimeout. In Go 1.19 a shell's child can keep Output() waiting past the deadline.
A program that runs an external command must not wait for it forever, and a deadline on the context should be enough.
package main
import (
"context"
"fmt"
"os/exec"
"time"
)
func run(args ...string) {
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
start := time.Now()
out, err := exec.CommandContext(ctx, args[0], args[1:]...).Output()
fmt.Printf("%v took %.1fs err=%v out=%q\n", args, time.Since(start).Seconds(), err, out)
}
func main() {
run("sleep", "3")
run("sh", "-c", "sleep 3; echo done")
}go run main.go[sleep 3] took 0.5s err=signal: killed out=""
[sh -c sleep 3; echo done] took 3.0s err=signal: killed out=""CommandContext kills the process with SIGKILL when the context ends, so the first call returns after half a second with a "signal: killed" error. The second call kills only sh, and its child sleep keeps the output pipe open, so Output waits for end of file until the child exits, which takes the full three seconds. This is described in the os/exec documentation. Go 1.20 added the WaitDelay field to bound that wait.
To stop the whole tree on Go 1.19, start the command in its own process group with SysProcAttr{Setpgid: true} and kill the negative group ID, or run it with exec sleep 3 so there is no separate child. A command with its output sent to a file instead of a pipe does not have the wait problem. Windows has no process groups in this sense.