//go:build linux || darwin package subprocess import ( "errors" "fmt" "os" "os/exec" "syscall" "time" ) const processGroupPollInterval = 10 * time.Millisecond type unixProcessTree struct { processGroupID int } func newOwnedProcessTree() (ownedProcessTree, error) { return &unixProcessTree{}, nil } func (tree *unixProcessTree) Start(cmd *exec.Cmd) error { cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} if err := cmd.Start(); err != nil { return err } tree.processGroupID = cmd.Process.Pid return nil } func (tree *unixProcessTree) TerminateGracefully() error { return tree.signal(syscall.SIGTERM) } func (tree *unixProcessTree) TerminateForcefully() error { return tree.signal(syscall.SIGKILL) } func (tree *unixProcessTree) Dispose() error { hasMembers, err := tree.hasMembers() if err != nil || !hasMembers { return err } cleanupErr := tree.TerminateGracefully() empty, waitErr := tree.waitUntilEmpty(gracefulTerminationWait) cleanupErr = joinErrors(cleanupErr, waitErr) if empty { return cleanupErr } cleanupErr = joinErrors(cleanupErr, tree.TerminateForcefully()) empty, waitErr = tree.waitUntilEmpty(forcefulTerminationWait) cleanupErr = joinErrors(cleanupErr, waitErr) if !empty { cleanupErr = joinErrors(cleanupErr, fmt.Errorf("owned subprocess group did not exit within %s after forceful termination", forcefulTerminationWait)) } return cleanupErr } func (tree *unixProcessTree) signal(signal syscall.Signal) error { if tree.processGroupID <= 0 { return nil } err := syscall.Kill(-tree.processGroupID, signal) if errors.Is(err, syscall.ESRCH) || errors.Is(err, os.ErrProcessDone) { return nil } return err } func (tree *unixProcessTree) hasMembers() (bool, error) { if tree.processGroupID <= 0 { return false, nil } err := syscall.Kill(-tree.processGroupID, 0) if err == nil || errors.Is(err, syscall.EPERM) { return true, nil } if errors.Is(err, syscall.ESRCH) || errors.Is(err, os.ErrProcessDone) { return false, nil } return false, fmt.Errorf("inspect owned subprocess group: %w", err) } func (tree *unixProcessTree) waitUntilEmpty(timeout time.Duration) (bool, error) { deadline := time.Now().Add(timeout) for { hasMembers, err := tree.hasMembers() if err != nil || !hasMembers { return !hasMembers, err } remaining := time.Until(deadline) if remaining <= 0 { return false, nil } if remaining > processGroupPollInterval { remaining = processGroupPollInterval } time.Sleep(remaining) } }