github.com/golazy/golazy@v0.0.7-0.20221012133820-968fe65a0b65/lazydev/runner/runner.go (about) 1 // Package runner run a restart a program on signals 2 package runner 3 4 import ( 5 "bytes" 6 "errors" 7 "os/exec" 8 "strings" 9 "syscall" 10 "time" 11 ) 12 13 // Options holds the runner options 14 type Options struct { 15 KillWaitPeriod time.Duration // Time wait for a proces to die before callking kill 16 ReadyString []string // ReadyString is the string that Runner looks for in the command output to send EventReady 17 } 18 19 // DefaultRunnerOptions are used when no RunnerOptions are passed 20 var DefaultRunnerOptions = &Options{ 21 KillWaitPeriod: time.Second, 22 ReadyString: []string{"Listening", "Started", "Ready"}, 23 } 24 25 // EventStart is fired whenever Start is called 26 type EventStart struct { 27 // Err display any error that happen during start 28 // If Err is nil you can expect an EventStarted event 29 Err error 30 } 31 32 // EventStop is fired whenever Stop is called 33 type EventStop struct { 34 // Err display any error that happen during stop 35 Err error 36 } 37 38 // EventSignal is fired whenever 39 type EventSignal struct { 40 // Err display any error that happen during stop 41 Err error 42 } 43 44 // EventReady is fired whenever the command outputs the string Listening 45 type EventReady struct { 46 Data string // Data contains the block that triggered the EventReady 47 } 48 49 // EventRestart is fired whenever Restart is called 50 type EventRestart struct { 51 // Err display any error that happen during stop 52 // If Err is nil you can expect an EventStarted event 53 Err error 54 } 55 56 // EventStopped is fired whenever the process exits 57 type EventStopped struct { 58 Output []string // Holds the command output up to MaxOutputSize 59 ExitCode int // ExitCode holds the exit code 60 RunTime time.Duration 61 } 62 63 // EventStarted is fired whenever the subprocess is started 64 type EventStarted struct { 65 Command string 66 } 67 68 // Runner is an command runner that produces events on start/stop and restart 69 type Runner struct { 70 Events <-chan (interface{}) // Events will be fired here. The channel is not expected to be closed. 71 options Options 72 cmd *exec.Cmd 73 e chan (interface{}) 74 startCmd chan (chan (error)) 75 restartCmd chan (chan (error)) 76 stopCmd chan (chan (error)) 77 closeCmd chan (chan (error)) 78 signalCmd chan (struct { 79 Signal syscall.Signal 80 errC chan (error) 81 }) 82 closed bool 83 } 84 85 // Close stop all the internal goroutines 86 // After Close is called the runner can't be used anymore 87 func (r *Runner) Close() error { 88 if r.closed { 89 return nil 90 } 91 errC := make(chan (error)) 92 r.closeCmd <- errC 93 return <-errC 94 } 95 96 // Start starts the command 97 // If the command is already running it returns ErrRunning 98 func (r *Runner) Start() error { 99 if r.closed { 100 return ErrRunnerClosed 101 } 102 errC := make(chan (error)) 103 r.startCmd <- errC 104 return <-errC 105 } 106 107 // Restart restart the process by calling Stop and then Restart. If the process is not runing it will be the same as calling Start 108 func (r *Runner) Restart() error { 109 if r.closed { 110 return ErrRunnerClosed 111 } 112 errC := make(chan (error)) 113 r.restartCmd <- errC 114 return <-errC 115 } 116 117 // Stop stops the process. 118 // It will send an interrupt signal to the process. 119 // If after KillWaitPeriod the process is still alive, it will send a kill signal 120 func (r *Runner) Stop() error { 121 if r.closed { 122 return ErrRunnerClosed 123 } 124 errC := make(chan (error)) 125 r.stopCmd <- errC 126 return <-errC 127 } 128 129 // Signal sends a signal to the process. 130 // If the process is not running it returns ErrNotRunning 131 func (r *Runner) Signal(s syscall.Signal) error { 132 if r.closed { 133 return ErrRunnerClosed 134 } 135 errC := make(chan (error)) 136 r.signalCmd <- struct { 137 Signal syscall.Signal 138 errC chan error 139 }{s, errC} 140 return <-errC 141 } 142 143 // New creates a new runner for the given command 144 // if options is nil, New will use DefaultRunnerOptions 145 func New(cmd *exec.Cmd, options *Options) *Runner { 146 147 e := make(chan (interface{}), 1024) 148 if options == nil { 149 options = DefaultRunnerOptions 150 } 151 r := &Runner{ 152 Events: e, 153 options: *options, 154 cmd: cmd, 155 e: e, 156 startCmd: make(chan (chan (error))), 157 restartCmd: make(chan (chan (error))), 158 stopCmd: make(chan (chan (error))), 159 closeCmd: make(chan (chan (error))), 160 closed: false, 161 } 162 go r.loop() 163 return r 164 } 165 166 var ( 167 // ErrRunning is the return error in the Start method 168 ErrRunning = errors.New("Program is already running") 169 // ErrCantKill is returned by Restart and Stop in case the process can't be killed 170 ErrCantKill = errors.New("Process is still alive after sending the kill signal") 171 // ErrNotRunning is retuned by the Stop and Signal command when the program is not running 172 ErrNotRunning = errors.New("Process is not running") 173 // ErrRunnerClosed is returned by any method when the runner is closed 174 ErrRunnerClosed = errors.New("Runner is closed") 175 ) 176 177 func (r *Runner) loop() { 178 running := false 179 var done chan (int) = nil 180 var io chan ([]byte) = nil 181 var readyEventSent bool 182 var output []string 183 var startTime time.Time 184 var cmd *exec.Cmd 185 186 signal := func(sig syscall.Signal) error { 187 if cmd == nil || cmd.Process == nil { 188 return ErrNotRunning 189 } 190 syscall.Kill(-cmd.Process.Pid, sig) 191 err := cmd.Process.Signal(sig) 192 return err 193 194 } 195 196 start := func() error { 197 startTime = time.Now() 198 io = make(chan ([]byte)) 199 cmd = &exec.Cmd{ 200 Path: r.cmd.Path, 201 Args: r.cmd.Args, 202 Env: r.cmd.Env, 203 Dir: r.cmd.Dir, 204 Stdin: r.cmd.Stdin, 205 Stdout: channelWriter(io), 206 Stderr: channelWriter(io), 207 ExtraFiles: r.cmd.ExtraFiles, 208 SysProcAttr: &syscall.SysProcAttr{Setpgid: true}, 209 } 210 211 output = make([]string, 0, 1024) 212 readyEventSent = false 213 214 if err := cmd.Start(); err != nil { 215 return err 216 } 217 running = true 218 r.e <- EventStarted{ 219 Command: strings.Join(append([]string{cmd.Path}, cmd.Args...), " "), 220 } 221 222 // Wait for it to stop 223 done = make(chan (int)) 224 go func() { 225 err := cmd.Wait() 226 if exitError, ok := err.(*exec.ExitError); ok { 227 done <- exitError.ExitCode() 228 return 229 } 230 done <- cmd.ProcessState.ExitCode() 231 }() 232 233 return nil 234 } 235 236 checkReadyEvent := func(data []byte) { 237 if !readyEventSent { 238 for _, readyString := range r.options.ReadyString { 239 if bytes.Contains(data, []byte(readyString)) { 240 r.e <- EventReady{string(data)} 241 readyEventSent = true 242 } 243 } 244 } 245 } 246 247 buf := []byte{} 248 processIO := func(data []byte) { 249 if r.cmd.Stdout != nil { 250 r.cmd.Stdout.Write(data) 251 } 252 buf := append(buf, data...) 253 if len(buf) == 0 { 254 return 255 } 256 checkReadyEvent(buf) 257 lines := strings.Split(string(buf), "\n") 258 output = append(output, lines[0:len(lines)-1]...) 259 buf = []byte(lines[len(lines)-1]) 260 } 261 262 handleExit := func(statusCode int) { 263 if len(buf) != 0 { 264 output = append(output, string(buf)) 265 } 266 running = false 267 done = nil 268 r.e <- EventStopped{ 269 Output: output, 270 ExitCode: statusCode, 271 RunTime: time.Since(startTime), 272 } 273 } 274 275 stop := func() error { 276 signal(syscall.SIGINT) 277 278 wait := time.After(time.Duration(r.options.KillWaitPeriod)) 279 var kill <-chan (time.Time) = nil 280 281 for running { 282 select { 283 case exitCode := <-done: 284 handleExit(exitCode) 285 return nil 286 case data := <-io: 287 processIO(data) 288 case <-wait: 289 signal(syscall.SIGKILL) 290 kill = time.After(time.Duration(r.options.KillWaitPeriod)) 291 case <-kill: 292 return ErrCantKill 293 } 294 } 295 return nil 296 } 297 298 for { 299 select { 300 case errC := <-r.startCmd: 301 if running { 302 errC <- ErrRunning 303 r.e <- EventStart{Err: ErrRunning} 304 continue 305 } 306 err := start() 307 r.e <- EventStart{Err: err} 308 errC <- err 309 310 case errC := <-r.restartCmd: 311 if running { 312 err := stop() 313 if err != nil { 314 r.e <- EventRestart{Err: err} 315 errC <- err 316 continue 317 } 318 } 319 err := start() 320 r.e <- EventRestart{Err: err} 321 errC <- err 322 case errC := <-r.stopCmd: 323 if !running { 324 errC <- ErrNotRunning 325 r.e <- EventStop{Err: ErrNotRunning} 326 continue 327 } 328 err := stop() 329 r.e <- EventStop{Err: err} 330 errC <- err 331 332 case args := <-r.signalCmd: 333 args.errC <- signal(args.Signal) 334 case errC := <-r.closeCmd: 335 if running { 336 errC <- stop() 337 } 338 errC <- nil 339 return 340 case exitCode := <-done: 341 handleExit(exitCode) 342 case data := <-io: 343 processIO(data) 344 } 345 } 346 } 347 348 type channelWriter chan ([]byte) 349 350 func (cw channelWriter) Write(data []byte) (int, error) { 351 cw <- data 352 return len(data), nil 353 }