105 lines
2.5 KiB
Go
105 lines
2.5 KiB
Go
//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)
|
|
}
|
|
}
|