Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for sending arbitrary signals to a process #89

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 28 additions & 10 deletions cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand All @@ -303,14 +304,31 @@ 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

// 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
Expand Down
7 changes: 2 additions & 5 deletions cmd_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 2 additions & 5 deletions cmd_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 2 additions & 5 deletions cmd_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
10 changes: 4 additions & 6 deletions cmd_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion cmd_windows_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down