diff options
| author | 2023-04-28 01:36:55 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:55 +0800 | |
| commit | fc8c5fdce62fb229202659408798a7b6c98f6e8b (patch) | |
| tree | 7554f80e50de4af6fd255afa7c21bcdd58a7af34 /cli/internal/process | |
| parent | dd84b9d64fb98746a230cd24233ff50a562c39c9 (diff) | |
| download | HydroRoll-fc8c5fdce62fb229202659408798a7b6c98f6e8b.tar.gz HydroRoll-fc8c5fdce62fb229202659408798a7b6c98f6e8b.zip | |
Diffstat (limited to 'cli/internal/process')
| -rw-r--r-- | cli/internal/process/child.go | 406 | ||||
| -rw-r--r-- | cli/internal/process/child_nix_test.go | 190 | ||||
| -rw-r--r-- | cli/internal/process/child_test.go | 193 | ||||
| -rw-r--r-- | cli/internal/process/manager.go | 120 | ||||
| -rw-r--r-- | cli/internal/process/manager_test.go | 94 | ||||
| -rw-r--r-- | cli/internal/process/sys_nix.go | 23 | ||||
| -rw-r--r-- | cli/internal/process/sys_windows.go | 17 |
7 files changed, 0 insertions, 1043 deletions
diff --git a/cli/internal/process/child.go b/cli/internal/process/child.go deleted file mode 100644 index 1c3e6e7..0000000 --- a/cli/internal/process/child.go +++ /dev/null @@ -1,406 +0,0 @@ -package process - -/** - * Code in this file is based on the source code at - * https://github.com/hashicorp/consul-template/tree/3ea7d99ad8eff17897e0d63dac86d74770170bb8/child/child.go - * - * Major changes include removing the ability to restart a child process, - * requiring a fully-formed exec.Cmd to be passed in, and including cmd.Dir - * in the description of a child process. - */ - -import ( - "errors" - "fmt" - "math/rand" - "os" - "os/exec" - "strings" - "sync" - "syscall" - "time" - - "github.com/hashicorp/go-hclog" -) - -func init() { - // Seed the default rand Source with current time to produce better random - // numbers used with splay - rand.Seed(time.Now().UnixNano()) -} - -var ( - // ErrMissingCommand is the error returned when no command is specified - // to run. - ErrMissingCommand = errors.New("missing command") - - // ExitCodeOK is the default OK exit code. - ExitCodeOK = 0 - - // ExitCodeError is the default error code returned when the child exits with - // an error without a more specific code. - ExitCodeError = 127 -) - -// Child is a wrapper around a child process which can be used to send signals -// and manage the processes' lifecycle. -type Child struct { - sync.RWMutex - - timeout time.Duration - - killSignal os.Signal - killTimeout time.Duration - - splay time.Duration - - // cmd is the actual child process under management. - cmd *exec.Cmd - - // exitCh is the channel where the processes exit will be returned. - exitCh chan int - - // stopLock is the mutex to lock when stopping. stopCh is the circuit breaker - // to force-terminate any waiting splays to kill the process now. stopped is - // a boolean that tells us if we have previously been stopped. - stopLock sync.RWMutex - stopCh chan struct{} - stopped bool - - // whether to set process group id or not (default on) - setpgid bool - - Label string - - logger hclog.Logger -} - -// NewInput is input to the NewChild function. -type NewInput struct { - // Cmd is the unstarted, preconfigured command to run - Cmd *exec.Cmd - - // Timeout is the maximum amount of time to allow the command to execute. If - // set to 0, the command is permitted to run infinitely. - Timeout time.Duration - - // KillSignal is the signal to send to gracefully kill this process. This - // value may be nil. - KillSignal os.Signal - - // KillTimeout is the amount of time to wait for the process to gracefully - // terminate before force-killing. - KillTimeout time.Duration - - // Splay is the maximum random amount of time to wait before sending signals. - // This option helps reduce the thundering herd problem by effectively - // sleeping for a random amount of time before sending the signal. This - // prevents multiple processes from all signaling at the same time. This value - // may be zero (which disables the splay entirely). - Splay time.Duration - - // Logger receives debug log lines about the process state and transitions - Logger hclog.Logger -} - -// New creates a new child process for management with high-level APIs for -// sending signals to the child process, restarting the child process, and -// gracefully terminating the child process. -func newChild(i NewInput) (*Child, error) { - // exec.Command prepends the command to be run to the arguments list, so - // we only need the arguments here, it will include the command itself. - label := fmt.Sprintf("(%v) %v", i.Cmd.Dir, strings.Join(i.Cmd.Args, " ")) - child := &Child{ - cmd: i.Cmd, - timeout: i.Timeout, - killSignal: i.KillSignal, - killTimeout: i.KillTimeout, - splay: i.Splay, - stopCh: make(chan struct{}, 1), - setpgid: true, - Label: label, - logger: i.Logger.Named(label), - } - - return child, nil -} - -// ExitCh returns the current exit channel for this child process. This channel -// may change if the process is restarted, so implementers must not cache this -// value. -func (c *Child) ExitCh() <-chan int { - c.RLock() - defer c.RUnlock() - return c.exitCh -} - -// Pid returns the pid of the child process. If no child process exists, 0 is -// returned. -func (c *Child) Pid() int { - c.RLock() - defer c.RUnlock() - return c.pid() -} - -// Command returns the human-formatted command with arguments. -func (c *Child) Command() string { - return c.Label -} - -// Start starts and begins execution of the child process. A buffered channel -// is returned which is where the command's exit code will be returned upon -// exit. Any errors that occur prior to starting the command will be returned -// as the second error argument, but any errors returned by the command after -// execution will be returned as a non-zero value over the exit code channel. -func (c *Child) Start() error { - // log.Printf("[INFO] (child) spawning: %s", c.Command()) - c.Lock() - defer c.Unlock() - return c.start() -} - -// Signal sends the signal to the child process, returning any errors that -// occur. -func (c *Child) Signal(s os.Signal) error { - c.logger.Debug("receiving signal %q", s.String()) - c.RLock() - defer c.RUnlock() - return c.signal(s) -} - -// Kill sends the kill signal to the child process and waits for successful -// termination. If no kill signal is defined, the process is killed with the -// most aggressive kill signal. If the process does not gracefully stop within -// the provided KillTimeout, the process is force-killed. If a splay was -// provided, this function will sleep for a random period of time between 0 and -// the provided splay value to reduce the thundering herd problem. This function -// does not return any errors because it guarantees the process will be dead by -// the return of the function call. -func (c *Child) Kill() { - c.logger.Debug("killing process") - c.Lock() - defer c.Unlock() - c.kill(false) -} - -// Stop behaves almost identical to Kill except it suppresses future processes -// from being started by this child and it prevents the killing of the child -// process from sending its value back up the exit channel. This is useful -// when doing a graceful shutdown of an application. -func (c *Child) Stop() { - c.internalStop(false) -} - -// StopImmediately behaves almost identical to Stop except it does not wait -// for any random splay if configured. This is used for performing a fast -// shutdown of consul-template and its children when a kill signal is received. -func (c *Child) StopImmediately() { - c.internalStop(true) -} - -func (c *Child) internalStop(immediately bool) { - c.Lock() - defer c.Unlock() - - c.stopLock.Lock() - defer c.stopLock.Unlock() - if c.stopped { - return - } - c.kill(immediately) - close(c.stopCh) - c.stopped = true -} - -func (c *Child) start() error { - setSetpgid(c.cmd, c.setpgid) - if err := c.cmd.Start(); err != nil { - return err - } - - // Create a new exitCh so that previously invoked commands (if any) don't - // cause us to exit, and start a goroutine to wait for that process to end. - exitCh := make(chan int, 1) - go func() { - var code int - // It's possible that kill is called before we even - // manage to get here. Make sure we still have a valid - // cmd before waiting on it. - c.RLock() - var cmd = c.cmd - c.RUnlock() - var err error - if cmd != nil { - err = cmd.Wait() - } - if err == nil { - code = ExitCodeOK - } else { - code = ExitCodeError - if exiterr, ok := err.(*exec.ExitError); ok { - if status, ok := exiterr.Sys().(syscall.WaitStatus); ok { - code = status.ExitStatus() - } - } - } - - // If the child is in the process of killing, do not send a response back - // down the exit channel. - c.stopLock.RLock() - defer c.stopLock.RUnlock() - if !c.stopped { - select { - case <-c.stopCh: - case exitCh <- code: - } - } - - close(exitCh) - }() - - c.exitCh = exitCh - - // If a timeout was given, start the timer to wait for the child to exit - if c.timeout != 0 { - select { - case code := <-exitCh: - if code != 0 { - return fmt.Errorf( - "command exited with a non-zero exit status:\n"+ - "\n"+ - " %s\n"+ - "\n"+ - "This is assumed to be a failure. Please ensure the command\n"+ - "exits with a zero exit status.", - c.Command(), - ) - } - case <-time.After(c.timeout): - // Force-kill the process - c.stopLock.Lock() - defer c.stopLock.Unlock() - if c.cmd != nil && c.cmd.Process != nil { - c.cmd.Process.Kill() - } - - return fmt.Errorf( - "command did not exit within %q:\n"+ - "\n"+ - " %s\n"+ - "\n"+ - "Commands must exit in a timely manner in order for processing to\n"+ - "continue. Consider using a process supervisor or utilizing the\n"+ - "built-in exec mode instead.", - c.timeout, - c.Command(), - ) - } - } - - return nil -} - -func (c *Child) pid() int { - if !c.running() { - return 0 - } - return c.cmd.Process.Pid -} - -func (c *Child) signal(s os.Signal) error { - if !c.running() { - return nil - } - - sig, ok := s.(syscall.Signal) - if !ok { - return fmt.Errorf("bad signal: %s", s) - } - pid := c.cmd.Process.Pid - if c.setpgid { - // kill takes negative pid to indicate that you want to use gpid - pid = -(pid) - } - // cross platform way to signal process/process group - p, err := os.FindProcess(pid) - if err != nil { - return err - } - return p.Signal(sig) -} - -// kill sends the signal to kill the process using the configured signal -// if set, else the default system signal -func (c *Child) kill(immediately bool) { - - if !c.running() { - c.logger.Debug("Kill() called but process dead; not waiting for splay.") - return - } else if immediately { - c.logger.Debug("Kill() called but performing immediate shutdown; not waiting for splay.") - } else { - c.logger.Debug("Kill(%v) called", immediately) - select { - case <-c.stopCh: - case <-c.randomSplay(): - } - } - - var exited bool - defer func() { - if !exited { - c.logger.Debug("PKill") - c.cmd.Process.Kill() - } - c.cmd = nil - }() - - if c.killSignal == nil { - return - } - - if err := c.signal(c.killSignal); err != nil { - c.logger.Debug("Kill failed: %s", err) - if processNotFoundErr(err) { - exited = true // checked in defer - } - return - } - - killCh := make(chan struct{}, 1) - go func() { - defer close(killCh) - c.cmd.Process.Wait() - }() - - select { - case <-c.stopCh: - case <-killCh: - exited = true - case <-time.After(c.killTimeout): - c.logger.Debug("timeout") - } -} - -func (c *Child) running() bool { - select { - case <-c.exitCh: - return false - default: - } - return c.cmd != nil && c.cmd.Process != nil -} - -func (c *Child) randomSplay() <-chan time.Time { - if c.splay == 0 { - return time.After(0) - } - - ns := c.splay.Nanoseconds() - offset := rand.Int63n(ns) - t := time.Duration(offset) - - c.logger.Debug("waiting %.2fs for random splay", t.Seconds()) - - return time.After(t) -} diff --git a/cli/internal/process/child_nix_test.go b/cli/internal/process/child_nix_test.go deleted file mode 100644 index 7311d18..0000000 --- a/cli/internal/process/child_nix_test.go +++ /dev/null @@ -1,190 +0,0 @@ -//go:build !windows -// +build !windows - -package process - -/** - * Code in this file is based on the source code at - * https://github.com/hashicorp/consul-template/tree/3ea7d99ad8eff17897e0d63dac86d74770170bb8/child/child_test.go - * - * Tests in this file use signals or pgid features not available on windows - */ - -import ( - "os/exec" - "syscall" - "testing" - "time" - - "github.com/hashicorp/go-gatedio" -) - -func TestSignal(t *testing.T) { - - c := testChild(t) - cmd := exec.Command("sh", "-c", "trap 'echo one; exit' USR1; while true; do sleep 0.2; done") - c.cmd = cmd - - out := gatedio.NewByteBuffer() - c.cmd.Stdout = out - - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - // For some reason bash doesn't start immediately - time.Sleep(fileWaitSleepDelay) - - if err := c.Signal(syscall.SIGUSR1); err != nil { - t.Fatal(err) - } - - // Give time for the file to flush - time.Sleep(fileWaitSleepDelay) - - expected := "one\n" - if out.String() != expected { - t.Errorf("expected %q to be %q", out.String(), expected) - } -} - -func TestStop_childAlreadyDead(t *testing.T) { - c := testChild(t) - c.cmd = exec.Command("sh", "-c", "exit 1") - c.splay = 100 * time.Second - c.killSignal = syscall.SIGTERM - - if err := c.Start(); err != nil { - t.Fatal(err) - } - - // For some reason bash doesn't start immediately - time.Sleep(fileWaitSleepDelay) - - killStartTime := time.Now() - c.Stop() - killEndTime := time.Now() - - if killEndTime.Sub(killStartTime) > fileWaitSleepDelay { - t.Error("expected not to wait for splay") - } -} - -func TestSignal_noProcess(t *testing.T) { - - c := testChild(t) - if err := c.Signal(syscall.SIGUSR1); err != nil { - // Just assert there is no error - t.Fatal(err) - } -} - -func TestKill_signal(t *testing.T) { - - c := testChild(t) - cmd := exec.Command("sh", "-c", "trap 'echo one; exit' USR1; while true; do sleep 0.2; done") - c.killSignal = syscall.SIGUSR1 - - out := gatedio.NewByteBuffer() - cmd.Stdout = out - c.cmd = cmd - - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - // For some reason bash doesn't start immediately - time.Sleep(fileWaitSleepDelay) - - c.Kill() - - // Give time for the file to flush - time.Sleep(fileWaitSleepDelay) - - expected := "one\n" - if out.String() != expected { - t.Errorf("expected %q to be %q", out.String(), expected) - } -} - -func TestKill_noProcess(t *testing.T) { - c := testChild(t) - c.killSignal = syscall.SIGUSR1 - c.Kill() -} - -func TestStop_noWaitForSplay(t *testing.T) { - c := testChild(t) - c.cmd = exec.Command("sh", "-c", "trap 'echo one; exit' USR1; while true; do sleep 0.2; done") - c.splay = 100 * time.Second - c.killSignal = syscall.SIGUSR1 - - out := gatedio.NewByteBuffer() - c.cmd.Stdout = out - - if err := c.Start(); err != nil { - t.Fatal(err) - } - - // For some reason bash doesn't start immediately - time.Sleep(fileWaitSleepDelay) - - killStartTime := time.Now() - c.StopImmediately() - killEndTime := time.Now() - - expected := "one\n" - if out.String() != expected { - t.Errorf("expected %q to be %q", out.String(), expected) - } - - if killEndTime.Sub(killStartTime) > fileWaitSleepDelay { - t.Error("expected not to wait for splay") - } -} - -func TestSetpgid(t *testing.T) { - t.Run("true", func(t *testing.T) { - c := testChild(t) - c.cmd = exec.Command("sh", "-c", "while true; do sleep 0.2; done") - // default, but to be explicit for the test - c.setpgid = true - - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - // when setpgid is true, the pid and gpid should be the same - gpid, err := syscall.Getpgid(c.Pid()) - if err != nil { - t.Fatal("Getpgid error:", err) - } - - if c.Pid() != gpid { - t.Fatal("pid and gpid should match") - } - }) - t.Run("false", func(t *testing.T) { - c := testChild(t) - c.cmd = exec.Command("sh", "-c", "while true; do sleep 0.2; done") - c.setpgid = false - - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - // when setpgid is true, the pid and gpid should be the same - gpid, err := syscall.Getpgid(c.Pid()) - if err != nil { - t.Fatal("Getpgid error:", err) - } - - if c.Pid() == gpid { - t.Fatal("pid and gpid should NOT match") - } - }) -} diff --git a/cli/internal/process/child_test.go b/cli/internal/process/child_test.go deleted file mode 100644 index 63dee22..0000000 --- a/cli/internal/process/child_test.go +++ /dev/null @@ -1,193 +0,0 @@ -package process - -/** - * Code in this file is based on the source code at - * https://github.com/hashicorp/consul-template/tree/3ea7d99ad8eff17897e0d63dac86d74770170bb8/child/child_test.go - * - * Major changes include supporting api changes in child.go and removing - * tests for reloading, which was removed in child.go - */ - -import ( - "io/ioutil" - "os" - "os/exec" - "strings" - "testing" - "time" - - "github.com/hashicorp/go-gatedio" - "github.com/hashicorp/go-hclog" -) - -const fileWaitSleepDelay = 150 * time.Millisecond - -func testChild(t *testing.T) *Child { - cmd := exec.Command("echo", "hello", "world") - cmd.Stdout = ioutil.Discard - cmd.Stderr = ioutil.Discard - c, err := newChild(NewInput{ - Cmd: cmd, - KillSignal: os.Kill, - KillTimeout: 2 * time.Second, - Splay: 0 * time.Second, - Logger: hclog.Default(), - }) - if err != nil { - t.Fatal(err) - } - return c -} - -func TestNew(t *testing.T) { - - stdin := gatedio.NewByteBuffer() - stdout := gatedio.NewByteBuffer() - stderr := gatedio.NewByteBuffer() - command := "echo" - args := []string{"hello", "world"} - env := []string{"a=b", "c=d"} - killSignal := os.Kill - killTimeout := fileWaitSleepDelay - splay := fileWaitSleepDelay - - cmd := exec.Command(command, args...) - cmd.Stdin = stdin - cmd.Stderr = stderr - cmd.Stdout = stdout - cmd.Env = env - c, err := newChild(NewInput{ - Cmd: cmd, - KillSignal: killSignal, - KillTimeout: killTimeout, - Splay: splay, - Logger: hclog.Default(), - }) - if err != nil { - t.Fatal(err) - } - - if c.killSignal != killSignal { - t.Errorf("expected %q to be %q", c.killSignal, killSignal) - } - - if c.killTimeout != killTimeout { - t.Errorf("expected %q to be %q", c.killTimeout, killTimeout) - } - - if c.splay != splay { - t.Errorf("expected %q to be %q", c.splay, splay) - } - - if c.stopCh == nil { - t.Errorf("expected %#v to be", c.stopCh) - } -} - -func TestExitCh_noProcess(t *testing.T) { - - c := testChild(t) - ch := c.ExitCh() - if ch != nil { - t.Errorf("expected %#v to be nil", ch) - } -} - -func TestExitCh(t *testing.T) { - - c := testChild(t) - if err := c.Start(); err != nil { - t.Fatal(err) - } - println("Started") - defer c.Stop() - - ch := c.ExitCh() - if ch == nil { - t.Error("expected ch to exist") - } -} - -func TestPid_noProcess(t *testing.T) { - - c := testChild(t) - pid := c.Pid() - if pid != 0 { - t.Errorf("expected %q to be 0", pid) - } -} - -func TestPid(t *testing.T) { - - c := testChild(t) - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - pid := c.Pid() - if pid == 0 { - t.Error("expected pid to not be 0") - } -} - -func TestStart(t *testing.T) { - - c := testChild(t) - - // Set our own reader and writer so we can verify they are wired to the child. - stdin := gatedio.NewByteBuffer() - stdout := gatedio.NewByteBuffer() - stderr := gatedio.NewByteBuffer() - // Custom env and command - env := []string{"a=b", "c=d"} - cmd := exec.Command("env") - cmd.Stdin = stdin - cmd.Stdout = stdout - cmd.Stderr = stderr - cmd.Env = env - c.cmd = cmd - - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - select { - case <-c.ExitCh(): - case <-time.After(fileWaitSleepDelay): - t.Fatal("process should have exited") - } - - output := stdout.String() - for _, envVar := range env { - if !strings.Contains(output, envVar) { - t.Errorf("expected to find %q in %q", envVar, output) - } - } -} - -func TestKill_noSignal(t *testing.T) { - - c := testChild(t) - c.cmd = exec.Command("sh", "-c", "while true; do sleep 0.2; done") - c.killTimeout = 20 * time.Millisecond - c.killSignal = nil - - if err := c.Start(); err != nil { - t.Fatal(err) - } - defer c.Stop() - - // For some reason bash doesn't start immediately - time.Sleep(fileWaitSleepDelay) - - c.Kill() - - // Give time for the file to flush - time.Sleep(fileWaitSleepDelay) - - if c.cmd != nil { - t.Errorf("expected cmd to be nil") - } -} diff --git a/cli/internal/process/manager.go b/cli/internal/process/manager.go deleted file mode 100644 index 0488a29..0000000 --- a/cli/internal/process/manager.go +++ /dev/null @@ -1,120 +0,0 @@ -package process - -import ( - "errors" - "fmt" - "os" - "os/exec" - "sync" - "time" - - "github.com/hashicorp/go-hclog" -) - -// ErrClosing is returned when the process manager is in the process of closing, -// meaning that no more child processes can be Exec'd, and existing, non-failed -// child processes will be stopped with this error. -var ErrClosing = errors.New("process manager is already closing") - -// ChildExit is returned when a child process exits with a non-zero exit code -type ChildExit struct { - ExitCode int - Command string -} - -func (ce *ChildExit) Error() string { - return fmt.Sprintf("command %s exited (%d)", ce.Command, ce.ExitCode) -} - -// Manager tracks all of the child processes that have been spawned -type Manager struct { - done bool - children map[*Child]struct{} - mu sync.Mutex - doneCh chan struct{} - logger hclog.Logger -} - -// NewManager creates a new properly-initialized Manager instance -func NewManager(logger hclog.Logger) *Manager { - return &Manager{ - children: make(map[*Child]struct{}), - doneCh: make(chan struct{}), - logger: logger, - } -} - -// Exec spawns a child process to run the given command, then blocks -// until it completes. Returns a nil error if the child process finished -// successfully, ErrClosing if the manager closed during execution, and -// a ChildExit error if the child process exited with a non-zero exit code. -func (m *Manager) Exec(cmd *exec.Cmd) error { - m.mu.Lock() - if m.done { - m.mu.Unlock() - return ErrClosing - } - - child, err := newChild(NewInput{ - Cmd: cmd, - // Run forever by default - Timeout: 0, - // When it's time to exit, give a 10 second timeout - KillTimeout: 10 * time.Second, - // Send SIGINT to stop children - KillSignal: os.Interrupt, - Logger: m.logger, - }) - if err != nil { - return err - } - - m.children[child] = struct{}{} - m.mu.Unlock() - err = child.Start() - if err != nil { - m.mu.Lock() - delete(m.children, child) - m.mu.Unlock() - return err - } - err = nil - exitCode, ok := <-child.ExitCh() - if !ok { - err = ErrClosing - } else if exitCode != ExitCodeOK { - err = &ChildExit{ - ExitCode: exitCode, - Command: child.Command(), - } - } - - m.mu.Lock() - delete(m.children, child) - m.mu.Unlock() - return err -} - -// Close sends SIGINT to all child processes if it hasn't been done yet, -// and in either case blocks until they all exit or timeout -func (m *Manager) Close() { - m.mu.Lock() - if m.done { - m.mu.Unlock() - <-m.doneCh - return - } - wg := sync.WaitGroup{} - m.done = true - for child := range m.children { - child := child - wg.Add(1) - go func() { - child.Stop() - wg.Done() - }() - } - m.mu.Unlock() - wg.Wait() - close(m.doneCh) -} diff --git a/cli/internal/process/manager_test.go b/cli/internal/process/manager_test.go deleted file mode 100644 index fb40ffa..0000000 --- a/cli/internal/process/manager_test.go +++ /dev/null @@ -1,94 +0,0 @@ -package process - -import ( - "errors" - "os/exec" - "sync" - "testing" - "time" - - "github.com/hashicorp/go-gatedio" - "github.com/hashicorp/go-hclog" -) - -func newManager() *Manager { - return NewManager(hclog.Default()) -} - -func TestExec_simple(t *testing.T) { - mgr := newManager() - - out := gatedio.NewByteBuffer() - cmd := exec.Command("env") - cmd.Stdout = out - - err := mgr.Exec(cmd) - if err != nil { - t.Errorf("expected %q to be nil", err) - } - - output := out.String() - if output == "" { - t.Error("expected output from running 'env', got empty string") - } -} - -func TestClose(t *testing.T) { - mgr := newManager() - - wg := sync.WaitGroup{} - tasks := 4 - errors := make([]error, tasks) - start := time.Now() - for i := 0; i < tasks; i++ { - wg.Add(1) - go func(index int) { - cmd := exec.Command("sleep", "0.5") - err := mgr.Exec(cmd) - if err != nil { - errors[index] = err - } - wg.Done() - }(i) - } - // let processes kick off - time.Sleep(50 * time.Millisecond) - mgr.Close() - end := time.Now() - wg.Wait() - duration := end.Sub(start) - if duration >= 500*time.Millisecond { - t.Errorf("expected to close, total time was %q", duration) - } - for _, err := range errors { - if err != ErrClosing { - t.Errorf("expected manager closing error, found %q", err) - } - } -} - -func TestClose_alreadyClosed(t *testing.T) { - mgr := newManager() - mgr.Close() - - // repeated closing does not error - mgr.Close() - - err := mgr.Exec(exec.Command("sleep", "1")) - if err != ErrClosing { - t.Errorf("expected manager closing error, found %q", err) - } -} - -func TestExitCode(t *testing.T) { - mgr := newManager() - - err := mgr.Exec(exec.Command("ls", "doesnotexist")) - exitErr := &ChildExit{} - if !errors.As(err, &exitErr) { - t.Errorf("expected a ChildExit err, got %q", err) - } - if exitErr.ExitCode == 0 { - t.Error("expected non-zero exit code , got 0") - } -} diff --git a/cli/internal/process/sys_nix.go b/cli/internal/process/sys_nix.go deleted file mode 100644 index 0e6c003..0000000 --- a/cli/internal/process/sys_nix.go +++ /dev/null @@ -1,23 +0,0 @@ -//go:build !windows -// +build !windows - -package process - -/** - * Code in this file is based on the source code at - * https://github.com/hashicorp/consul-template/tree/3ea7d99ad8eff17897e0d63dac86d74770170bb8/child/sys_nix.go - */ - -import ( - "os/exec" - "syscall" -) - -func setSetpgid(cmd *exec.Cmd, value bool) { - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: value} -} - -func processNotFoundErr(err error) bool { - // ESRCH == no such process, ie. already exited - return err == syscall.ESRCH -} diff --git a/cli/internal/process/sys_windows.go b/cli/internal/process/sys_windows.go deleted file mode 100644 index c626c22..0000000 --- a/cli/internal/process/sys_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build windows -// +build windows - -package process - -/** - * Code in this file is based on the source code at - * https://github.com/hashicorp/consul-template/tree/3ea7d99ad8eff17897e0d63dac86d74770170bb8/child/sys_windows.go - */ - -import "os/exec" - -func setSetpgid(cmd *exec.Cmd, value bool) {} - -func processNotFoundErr(err error) bool { - return false -} |
