-
Notifications
You must be signed in to change notification settings - Fork 31
/
command.go
241 lines (220 loc) · 6.56 KB
/
command.go
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
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
package main
import (
"crypto/md5"
"errors"
"fmt"
"os"
"os/exec"
"path"
"strings"
"syscall"
)
// CommandDelimiter is used when demarcating boundaries between
// functions
const CommandDelimiter = "───────────────────────────────────────────"
// CommandProcessStartSymbol is the fancy symbol we use to denote
// the start of a command
const CommandProcessStartSymbol = "►"
// CommandProcessStopSymbol is the fancy symbol we use to denote
// the end of a command
const CommandProcessStopSymbol = "■"
// ICommand is the interface for the Command class
type ICommand interface {
// runs the command
Run()
// gets the id of the command
GetID() string
// get a pointer to the status channel
GetStatus() *chan error
// checks if the command is still running
IsRunning() bool
// checks if the command is valid
IsValid() error
// tells command to exit nicely
SendInterrupt()
}
// InitCommand is for creating a new Command
func InitCommand(config *CommandConfig) *Command {
commandHash := fmt.Sprintf("%x", md5.Sum([]byte(fmt.Sprintf("%s%v", config.Application, config.Arguments))))
command := &Command{
id: commandHash[:6],
}
command.config = config
command.logger = InitLogger(&LoggerConfig{
Name: "command",
Format: "production",
Level: config.LogLevel,
AdditionalFields: &map[string]interface{}{
"submodule": path.Base(fmt.Sprintf("%s", config.Application)),
},
})
return command
}
// CommandConfig configures Command
type CommandConfig struct {
Application string
Arguments []string
Directory string
Environment []string
LogLevel LogLevel
}
// Command is the atomic command to run
type Command struct {
id string
signal chan os.Signal
status chan error
run chan error
terminated chan error
config *CommandConfig
cmd *exec.Cmd
logger *Logger
started bool
reported bool
stopped bool
}
// GetID returns the command's ID, used for the execution group
// to report the running command
func (command *Command) GetID() string {
return command.id
}
// GetStatus returns the command's status channel for the execution
// group to know when the command has terminated
func (command *Command) GetStatus() *chan error {
return &command.status
}
// IsRunning allows callers to check if the command is running,
// the logic is tied into the Run()
func (command *Command) IsRunning() bool {
return command.started && !command.stopped
}
// IsValid does some sanity checks on the provided
// application before we try to run it
func (command *Command) IsValid() error {
application := command.config.Application
if len(application) == 0 {
return errors.New("no application was specified")
}
if path.IsAbs(application) {
if _, err := os.Lstat(application); err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("application at '%s' could not be found", application)
}
return err
}
}
if _, err := exec.LookPath(application); err != nil {
return err
}
return nil
}
// Run executes the command
func (command *Command) Run() {
command.logger.Tracef("command[%s] is starting", command.id)
command.handleInitialisation()
go command.handleStart()
go command.handleProcessLifecycle()
select {
case terminateCommand := <-command.terminated:
command.handleStopped(terminateCommand)
}
}
// SendInterrupt sends SIGINT to the command
func (command *Command) SendInterrupt() {
command.logger.Tracef("SIGINT received by command %s", command.id)
command.logger.Tracef("command[%v] status: %v/%v, msg: SIGINT >>> %v", command.id, command.started, command.terminated, &command.signal)
command.signal <- syscall.SIGINT
}
func (command *Command) handleInitialisation() {
if command.config == nil {
panic("command.config needs to be defined before initialisation can be done")
}
command.signal = make(chan os.Signal, 0)
command.status = make(chan error, 0)
command.run = make(chan error, 0)
command.terminated = make(chan error, 0)
command.started = false
command.reported = false
command.stopped = false
command.cmd = exec.Command(
command.config.Application,
command.config.Arguments...,
)
command.cmd.Dir = command.config.Directory
command.cmd.Env = command.config.Environment
for _, envvar := range os.Environ() {
command.cmd.Env = append(command.cmd.Env, envvar)
}
// command.cmd.Env = append(command.config.Environment, "GOCACHE=on")
command.cmd.Stderr = os.Stderr
command.cmd.Stdout = os.Stdout
}
// handleProcessExited handles the exit status being sent by the process
func (command *Command) handleProcessExited(status error) error {
command.logger.Tracef("process status: %v", command.cmd.ProcessState)
command.terminated <- status
return nil
}
func (command *Command) handleProcessLifecycle() error {
for {
select {
case signal := <-command.signal: // caller -> Command: shut down please
return command.handleSignalReceived(signal)
case cmdRunStatus := <-command.run: // process -> Command: i'm done here
return command.handleProcessExited(cmdRunStatus)
default: // just run
command.handleProcessReporting()
}
}
}
// handleProcessReporting handles the CLI reporting after a process
// has its PID reported
func (command *Command) handleProcessReporting() {
if !command.reported {
if command.cmd.Process != nil {
command.logger.Infof(
"'%v'\n%s %s pid:%v id:%s %s",
strings.Join(command.config.Arguments, "', '"),
CommandProcessStartSymbol,
CommandDelimiter,
command.cmd.Process.Pid,
command.id,
CommandProcessStartSymbol,
)
command.reported = true
}
}
}
// handleSignalReceived handles the signal received by the caller
func (command *Command) handleSignalReceived(signal os.Signal) error {
command.logger.Tracef("caller sent signal %v", signal)
command.terminated <- errors.New(signal.String())
if err := command.cmd.Process.Signal(signal); err != nil {
command.logger.Warn(err)
return err
}
return nil
}
// handleStart starts the process
func (command *Command) handleStart() {
command.started = true
command.run <- command.cmd.Run()
}
// handleStopped processes the end of a command as reported
// by (*exec.Cmd).Run or (*exec.Cmd).Wait
func (command *Command) handleStopped(terminateCommand error) {
command.logger.Tracef("command[%s] is exiting (%v)", command.id, terminateCommand)
pid := -1
if command.cmd.Process != nil {
pid = command.cmd.Process.Pid
}
command.logger.Infof(
"\n%s %s pid:%v id:%s %s",
CommandProcessStopSymbol,
CommandDelimiter,
pid,
command.id,
CommandProcessStopSymbol,
)
command.stopped = true
command.status <- terminateCommand
}