package process import ( "context" "errors" "os" "os/exec" "runtime" "time" ) type ExecCmd interface { Start() error Wait() error OsProcess() OsProcess } type OsProcess interface { Signal(os.Signal) error Kill() error } type execCmdWrapper struct { *exec.Cmd } func (e *execCmdWrapper) OsProcess() OsProcess { return e.Process } type Process struct { ExecCmd } func NewWithCmd(cmd *exec.Cmd) *Process { return &Process{ExecCmd: &execCmdWrapper{Cmd: cmd}} } func newWithMock(cmd ExecCmd) *Process { return &Process{ExecCmd: cmd} } // WaitOrKill waits for the already-started process by calling its Wait method. // // If the process does not return before ctx is done, WaitOrKill sends it the // given interrupt signal. If killDelay is positive, WaitOrKill waits that // additional period for Wait to return before sending os.Kill. // // Adapted from Go core: // https://github.com/golang/go/blob/8981092d71aee273d27b0e11cf932a34d4d365c1/src/cmd/go/script_test.go#L1131-L1190 func (p *Process) WaitOrKill(ctx context.Context, killDelay time.Duration) error { if p.OsProcess() == nil { return errors.New("WaitOrKill requires a non-nil OsProcess - missing Start call?") } errc := make(chan error) go func() { select { case errc <- nil: return case <-ctx.Done(): } err := p.OsProcess().Signal(stopSignal()) if err == nil { err = ctx.Err() // Report ctx.Err() as the reason we interrupted. } else if err.Error() == "os: process already finished" { errc <- nil return } if killDelay > 0 { timer := time.NewTimer(killDelay) select { // Report ctx.Err() as the reason we interrupted the process... case errc <- ctx.Err(): timer.Stop() return // ...but after killDelay has elapsed, fall back to a stronger signal. case <-timer.C: } // Wait still hasn't returned. // Kill the process harder to make sure that it exits. // // Ignore any error: if cmd.Process has already terminated, we still // want to send ctx.Err() (or the error from the Interrupt call) // to properly attribute the signal that may have terminated it. _ = p.OsProcess().Kill() } errc <- err }() waitErr := p.Wait() if interruptErr := <-errc; interruptErr != nil { return interruptErr } return waitErr } // stopSignal returns the appropriate signal to use to request that a process // stop execution. // // Copied from Go core: // https://github.com/golang/go/blob/8981092d71aee273d27b0e11cf932a34d4d365c1/src/cmd/go/script_test.go#L1119-L1129 func stopSignal() os.Signal { if runtime.GOOS == "windows" { // Per https://golang.org/pkg/os/#Signal, “Interrupt is not implemented on // Windows; using it with os.Process.Signal will return an error.” // Fall back to Kill instead. return os.Kill } return os.Interrupt }