Skip to content
shellhacks.dev
Go

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.

2 min read
Summarize in AI

A program that runs an external command must not wait for it forever, and a deadline on the context should be enough.

main.goGo
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")
}
Shell
go run main.go
Text
[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.

Share

More snippets

Go

Exit with a status

Exit with a status using os.Exit(n). It ends the program at once and skips every deferred call, so cleanup in defer is not run before the exit.

1 min read
Go

Read command line flags

Read command line options with the flag package: flag.String, flag.Int and flag.Bool, then flag.Parse(). Parsing stops at the first argument that is not a flag.

2 min read
Go

Raise the scanner line limit

Raise the line limit of bufio.Scanner with Buffer(make([]byte, 0, 64*1024), max). Lines over 64 KB otherwise stop the scan with token too long.

1 min read

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.