1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
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)
}
|