mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 08:41:32 -10:00
On timeout the context cancelled only the shell, leaving backgrounded children (e.g. 'sleep 3 & wait') orphaned. Worse, those children inherited the stdout/stderr pipes, so Run() blocked until they exited — far past the timeout. Set Setpgid and a Cancel that SIGKILLs the whole process group on POSIX, plus a WaitDelay backstop so a lingering child can't hang Wait. On Windows, set WaitDelay only (no process groups).
45 lines
1.5 KiB
Go
45 lines
1.5 KiB
Go
//go:build !windows
|
|
|
|
package tools
|
|
|
|
import (
|
|
"errors"
|
|
"os/exec"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// bashWaitDelay bounds how long Wait blocks for the I/O pipes to drain after the
|
|
// process has exited or the context's Cancel has run. Without it, a backgrounded
|
|
// child that inherited the shell's stdout/stderr keeps those pipes open and
|
|
// Run() hangs long past the timeout waiting for EOF. Var (not const) so tests
|
|
// can shorten it.
|
|
var bashWaitDelay = 2 * time.Second
|
|
|
|
// hardenProcessLifetime makes a bash command killable as a single unit so a
|
|
// timeout cannot leak backgrounded children.
|
|
//
|
|
// Setpgid puts the shell into a new process group; since `sh -c` runs without
|
|
// job control, every process it forks (including `&` jobs) inherits that group.
|
|
// On context cancellation we signal the whole group (negative pid) instead of
|
|
// just the shell, so orphaned children die with it. WaitDelay is the backstop:
|
|
// if a child still holds the I/O pipes after the group is killed, Wait gives up
|
|
// rather than blocking forever.
|
|
func hardenProcessLifetime(command *exec.Cmd) {
|
|
if command.SysProcAttr == nil {
|
|
command.SysProcAttr = &syscall.SysProcAttr{}
|
|
}
|
|
command.SysProcAttr.Setpgid = true
|
|
command.WaitDelay = bashWaitDelay
|
|
command.Cancel = func() error {
|
|
if command.Process == nil {
|
|
return nil
|
|
}
|
|
// Negative pid targets the process group led by the shell.
|
|
if err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL); err != nil && !errors.Is(err, syscall.ESRCH) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
}
|