148 lines
3.4 KiB
Go
148 lines
3.4 KiB
Go
//go:build unix
|
|
|
|
package process
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"os/exec"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type customSignal struct{}
|
|
|
|
func (customSignal) String() string { return "custom" }
|
|
func (customSignal) Signal() {}
|
|
|
|
// NOTE: Run these tests with -race in CI for signal/process safety.
|
|
|
|
func TestConfigureProcessForSignals(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd := exec.Command("sh", "-c", "sleep 0.1")
|
|
configureProcessForSignals(cmd)
|
|
|
|
if cmd.SysProcAttr == nil {
|
|
t.Fatal("SysProcAttr is nil")
|
|
}
|
|
if !cmd.SysProcAttr.Setpgid {
|
|
t.Fatal("Setpgid = false, want true")
|
|
}
|
|
}
|
|
|
|
func TestSignalProcess_NilSafe(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if err := SignalProcess(nil, syscall.SIGTERM); err != nil {
|
|
t.Fatalf("SignalProcess(nil) error = %v, want nil", err)
|
|
}
|
|
|
|
cmd := &exec.Cmd{}
|
|
if err := SignalProcess(cmd, syscall.SIGTERM); err != nil {
|
|
t.Fatalf("SignalProcess(cmd without process) error = %v, want nil", err)
|
|
}
|
|
|
|
cmd.Process = &os.Process{Pid: 0}
|
|
if err := SignalProcess(cmd, syscall.SIGTERM); err != nil {
|
|
t.Fatalf("SignalProcess(pid<=0) error = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestSignalProcess_ESRCHIsTreatedAsSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd := &exec.Cmd{Process: &os.Process{Pid: 999999}}
|
|
if err := SignalProcess(cmd, syscall.SIGTERM); err != nil {
|
|
t.Fatalf("SignalProcess() error = %v, want nil when process group not found", err)
|
|
}
|
|
}
|
|
|
|
func TestSignalProcess_UsesFallbackOnKillError(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd := exec.Command("sh", "-c", "sleep 5")
|
|
configureProcessForSignals(cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start command error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
})
|
|
|
|
// Invalid signal causes syscall.Kill to fail with EINVAL, then fallback to cmd.Process.Signal.
|
|
err := SignalProcess(cmd, syscall.Signal(9999))
|
|
if err == nil {
|
|
t.Fatal("SignalProcess() error = nil, want non-nil for invalid signal")
|
|
}
|
|
if !(errors.Is(err, syscall.EINVAL) || errors.Is(err, os.ErrProcessDone)) {
|
|
// OS/process timing can vary; ensure we at least failed predictably.
|
|
t.Fatalf("SignalProcess() unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSignalProcess_NonSyscallSignalUsesProcessSignal(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cmd := exec.Command("sh", "-c", "sleep 1")
|
|
configureProcessForSignals(cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start command error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
})
|
|
|
|
err := SignalProcess(cmd, customSignal{})
|
|
if err == nil {
|
|
t.Fatal("SignalProcess(custom signal) error = nil, want non-nil")
|
|
}
|
|
if errors.Is(err, os.ErrProcessDone) {
|
|
return
|
|
}
|
|
if msg := err.Error(); msg == "" {
|
|
t.Fatalf("unexpected empty error for custom signal: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestProcessAlive(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if ProcessAlive(nil) {
|
|
t.Fatal("ProcessAlive(nil) = true, want false")
|
|
}
|
|
if ProcessAlive(&exec.Cmd{}) {
|
|
t.Fatal("ProcessAlive(cmd without process) = true, want false")
|
|
}
|
|
|
|
cmd := exec.Command("sh", "-c", "sleep 0.2")
|
|
configureProcessForSignals(cmd)
|
|
if err := cmd.Start(); err != nil {
|
|
t.Fatalf("start command error = %v", err)
|
|
}
|
|
|
|
if !ProcessAlive(cmd) {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
t.Fatal("ProcessAlive(running) = false, want true")
|
|
}
|
|
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
|
|
deadline := time.After(time.Second)
|
|
for {
|
|
if !ProcessAlive(cmd) {
|
|
return
|
|
}
|
|
select {
|
|
case <-deadline:
|
|
t.Fatal("ProcessAlive(exited) stayed true")
|
|
default:
|
|
}
|
|
}
|
|
}
|