From 47fc40d2e84f2f61eb7639aca0288ee5a070d30c Mon Sep 17 00:00:00 2001 From: Clayton Craft Date: Wed, 27 Jul 2022 12:39:26 -0700 Subject: [PATCH] Add support for sending arbitrary signals to a process This adds a public method, cmd.SendSignal, that allows sending a signal to a process and, optionally, to the entire process group. Internally, terminateProcess is replaced by signalProcess. --- cmd.go | 38 ++++++++++++++++++++++++++---------- cmd_darwin.go | 7 ++----- cmd_freebsd.go | 7 ++----- cmd_linux.go | 7 ++----- cmd_windows.go | 10 ++++------ cmd_windows_internal_test.go | 2 +- 6 files changed, 39 insertions(+), 32 deletions(-) diff --git a/cmd.go b/cmd.go index 653cabc..10d1169 100644 --- a/cmd.go +++ b/cmd.go @@ -275,15 +275,16 @@ func (c *Cmd) StartWithStdin(in io.Reader) <-chan Status { return c.statusChan } -// Stop stops the command by sending its process group a SIGTERM signal. -// Stop is idempotent. Stopping and already stopped command returns nil. -// -// Stop returns ErrNotStarted if called before Start or StartWithStdin. If the -// command is very slow to start, Stop can return ErrNotStarted after calling -// Start or StartWithStdin because this package is still waiting for the system -// to start the process. All other return errors are from the low-level system -// function for process termination. -func (c *Cmd) Stop() error { +// SendSignal sends the given signal to its process group if group is true, +// else the signal is just sent to the parent process. +// SendSignal returns ErrNotStarted if called before Start or StartWithStdin. +// If the command is very slow to start, SendSignal can return ErrNotStarted +// after calling Start or StartWithStdin because this package is still waiting +// for the system to start the process. All other return errors are from the +// low-level system function for process termination. +// Not all signals are supported on all operating systems, refer to os/Signal +// for details. +func (c *Cmd) SendSignal(sig syscall.Signal, group bool) error { c.Lock() defer c.Unlock() @@ -303,6 +304,23 @@ func (c *Cmd) Stop() error { return nil } + pid := c.status.PID + if group { + pid *= -1 + } + + return signalProcess(pid, sig) +} + +// Stop stops the command by sending its process group a SIGTERM signal. +// Stop is idempotent. Stopping and already stopped command returns nil. +// +// Stop returns ErrNotStarted if called before Start or StartWithStdin. If the +// command is very slow to start, Stop can return ErrNotStarted after calling +// Start or StartWithStdin because this package is still waiting for the system +// to start the process. All other return errors are from the low-level system +// function for process termination. +func (c *Cmd) Stop() error { // Flag that command was stopped, it didn't complete. This results in // status.Complete = false c.stopped = true @@ -310,7 +328,7 @@ func (c *Cmd) Stop() error { // Signal the process group (-pid), not just the process, so that the process // and all its children are signaled. Else, child procs can keep running and // keep the stdout/stderr fd open and cause cmd.Wait to hang. - return terminateProcess(c.status.PID) + return c.SendSignal(syscall.SIGTERM, true) } // Status returns the Status of the command at any time. It is safe to call diff --git a/cmd_darwin.go b/cmd_darwin.go index 6d6a960..cc6fd25 100644 --- a/cmd_darwin.go +++ b/cmd_darwin.go @@ -5,11 +5,8 @@ import ( "syscall" ) -func terminateProcess(pid int) error { - // Signal the process group (-pid), not just the process, so that the process - // and all its children are signaled. Else, child procs can keep running and - // keep the stdout/stderr fd open and cause cmd.Wait to hang. - return syscall.Kill(-pid, syscall.SIGTERM) +func signalProcess(pid int, sig syscall.Signal) error { + return syscall.Kill(pid, sig) } func setProcessGroupID(cmd *exec.Cmd) { diff --git a/cmd_freebsd.go b/cmd_freebsd.go index 6d6a960..cc6fd25 100644 --- a/cmd_freebsd.go +++ b/cmd_freebsd.go @@ -5,11 +5,8 @@ import ( "syscall" ) -func terminateProcess(pid int) error { - // Signal the process group (-pid), not just the process, so that the process - // and all its children are signaled. Else, child procs can keep running and - // keep the stdout/stderr fd open and cause cmd.Wait to hang. - return syscall.Kill(-pid, syscall.SIGTERM) +func signalProcess(pid int, sig syscall.Signal) error { + return syscall.Kill(pid, sig) } func setProcessGroupID(cmd *exec.Cmd) { diff --git a/cmd_linux.go b/cmd_linux.go index 6d6a960..cc6fd25 100644 --- a/cmd_linux.go +++ b/cmd_linux.go @@ -5,11 +5,8 @@ import ( "syscall" ) -func terminateProcess(pid int) error { - // Signal the process group (-pid), not just the process, so that the process - // and all its children are signaled. Else, child procs can keep running and - // keep the stdout/stderr fd open and cause cmd.Wait to hang. - return syscall.Kill(-pid, syscall.SIGTERM) +func signalProcess(pid int, sig syscall.Signal) error { + return syscall.Kill(pid, sig) } func setProcessGroupID(cmd *exec.Cmd) { diff --git a/cmd_windows.go b/cmd_windows.go index fca6c2f..2a309d8 100644 --- a/cmd_windows.go +++ b/cmd_windows.go @@ -6,16 +6,14 @@ import ( "syscall" ) -// Stop stops the command by sending its process group a SIGTERM signal. -// Stop is idempotent. An error should only be returned in the rare case that -// Stop is called immediately after the command ends but before Start can -// update its internal state. -func terminateProcess(pid int) error { +// Send the given signal to the process, returns an error if the process isn't +// running. +func signalProcess(pid int, sig syscall.Signal) error { p, err := os.FindProcess(pid) if err != nil { return err } - return p.Kill() + return p.Signal(sig) } func setProcessGroupID(cmd *exec.Cmd) { diff --git a/cmd_windows_internal_test.go b/cmd_windows_internal_test.go index 3ccf899..6c5e5c5 100644 --- a/cmd_windows_internal_test.go +++ b/cmd_windows_internal_test.go @@ -7,7 +7,7 @@ import ( ) func TestTerminateProcess(t *testing.T) { - err := terminateProcess(123) + err := signalProcess(123, syscall.SIGTERM) if err == nil { t.Error("no error, expected one on terminating nonexisting PID") }