diff options
| author | 2023-04-28 01:36:44 +0800 | |
|---|---|---|
| committer | 2023-04-28 01:36:44 +0800 | |
| commit | dd84b9d64fb98746a230cd24233ff50a562c39c9 (patch) | |
| tree | b583261ef00b3afe72ec4d6dacb31e57779a6faf /cli/internal/process/manager.go | |
| parent | 0b46fcd72ac34382387b2bcf9095233efbcc52f4 (diff) | |
| download | HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.tar.gz HydroRoll-dd84b9d64fb98746a230cd24233ff50a562c39c9.zip | |
Diffstat (limited to 'cli/internal/process/manager.go')
| -rw-r--r-- | cli/internal/process/manager.go | 120 |
1 files changed, 120 insertions, 0 deletions
diff --git a/cli/internal/process/manager.go b/cli/internal/process/manager.go new file mode 100644 index 0000000..0488a29 --- /dev/null +++ b/cli/internal/process/manager.go @@ -0,0 +1,120 @@ +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) +} |
