Skip to content

Commit

Permalink
Send TERM to exec processes before sending KILL signal (influxdata#6302)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielnelson authored and Mathieu Lecarme committed Apr 17, 2020
1 parent cc7ca99 commit de57258
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 49 deletions.
30 changes: 30 additions & 0 deletions internal/exec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package internal

import (
"bytes"
"os/exec"
"time"
)

// CombinedOutputTimeout runs the given command with the given timeout and
// returns the combined output of stdout and stderr.
// If the command times out, it attempts to kill the process.
func CombinedOutputTimeout(c *exec.Cmd, timeout time.Duration) ([]byte, error) {
var b bytes.Buffer
c.Stdout = &b
c.Stderr = &b
if err := c.Start(); err != nil {
return nil, err
}
err := WaitTimeout(c, timeout)
return b.Bytes(), err
}

// RunTimeout runs the given command with the given timeout.
// If the command times out, it attempts to kill the process.
func RunTimeout(c *exec.Cmd, timeout time.Duration) error {
if err := c.Start(); err != nil {
return err
}
return WaitTimeout(c, timeout)
}
58 changes: 58 additions & 0 deletions internal/exec_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// +build !windows

package internal

import (
"log"
"os/exec"
"syscall"
"time"
)

// KillGrace is the amount of time we allow a process to shutdown before
// sending a SIGKILL.
const KillGrace = 5 * time.Second

// WaitTimeout waits for the given command to finish with a timeout.
// It assumes the command has already been started.
// If the command times out, it attempts to kill the process.
func WaitTimeout(c *exec.Cmd, timeout time.Duration) error {
var kill *time.Timer
term := time.AfterFunc(timeout, func() {
err := c.Process.Signal(syscall.SIGTERM)
if err != nil {
log.Printf("E! [agent] Error terminating process: %s", err)
return
}

kill = time.AfterFunc(KillGrace, func() {
err := c.Process.Kill()
if err != nil {
log.Printf("E! [agent] Error killing process: %s", err)
return
}
})
})

err := c.Wait()

// Shutdown all timers
if kill != nil {
kill.Stop()
}
termSent := !term.Stop()

// If the process exited without error treat it as success. This allows a
// process to do a clean shutdown on signal.
if err == nil {
return nil
}

// If SIGTERM was sent then treat any process error as a timeout.
if termSent {
return TimeoutErr
}

// Otherwise there was an error unrelated to termination.
return err
}
41 changes: 41 additions & 0 deletions internal/exec_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// +build windows

package internal

import (
"log"
"os/exec"
"time"
)

// WaitTimeout waits for the given command to finish with a timeout.
// It assumes the command has already been started.
// If the command times out, it attempts to kill the process.
func WaitTimeout(c *exec.Cmd, timeout time.Duration) error {
timer := time.AfterFunc(timeout, func() {
err := c.Process.Kill()
if err != nil {
log.Printf("E! [agent] Error killing process: %s", err)
return
}
})

err := c.Wait()

// Shutdown all timers
termSent := !timer.Stop()

// If the process exited without error treat it as success. This allows a
// process to do a clean shutdown on signal.
if err == nil {
return nil
}

// If SIGTERM was sent then treat any process error as a timeout.
if termSent {
return TimeoutErr
}

// Otherwise there was an error unrelated to termination.
return err
}
49 changes: 0 additions & 49 deletions internal/internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"errors"
"fmt"
"io"
"log"
"math"
"math/big"
"os"
Expand Down Expand Up @@ -200,54 +199,6 @@ func SnakeCase(in string) string {
return string(out)
}

// CombinedOutputTimeout runs the given command with the given timeout and
// returns the combined output of stdout and stderr.
// If the command times out, it attempts to kill the process.
func CombinedOutputTimeout(c *exec.Cmd, timeout time.Duration) ([]byte, error) {
var b bytes.Buffer
c.Stdout = &b
c.Stderr = &b
if err := c.Start(); err != nil {
return nil, err
}
err := WaitTimeout(c, timeout)
return b.Bytes(), err
}

// RunTimeout runs the given command with the given timeout.
// If the command times out, it attempts to kill the process.
func RunTimeout(c *exec.Cmd, timeout time.Duration) error {
if err := c.Start(); err != nil {
return err
}
return WaitTimeout(c, timeout)
}

// WaitTimeout waits for the given command to finish with a timeout.
// It assumes the command has already been started.
// If the command times out, it attempts to kill the process.
func WaitTimeout(c *exec.Cmd, timeout time.Duration) error {
timer := time.AfterFunc(timeout, func() {
err := c.Process.Kill()
if err != nil {
log.Printf("E! [agent] Error killing process: %s", err)
return
}
})

err := c.Wait()
if err == nil {
timer.Stop()
return nil
}

if !timer.Stop() {
return TimeoutErr
}

return err
}

// RandomSleep will sleep for a random amount of time up to max.
// If the shutdown channel is closed, it will return before it has finished
// sleeping.
Expand Down

0 comments on commit de57258

Please sign in to comment.