github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/pkg/execctl/cmd.go (about) 1 package execctl 2 3 import ( 4 "errors" 5 "io" 6 "os" 7 "os/exec" 8 "strings" 9 "sync" 10 11 "github.com/Benchkram/bob/pkg/ctl" 12 "github.com/Benchkram/bob/pkg/usererror" 13 ) 14 15 var ( 16 ErrCmdAlreadyStarted = errors.New("cmd already started") 17 ) 18 19 // Command allows to execute a command and interact with it in real-time 20 // type Command interface { 21 // Start() error 22 // Stop() error 23 // Restart() error 24 25 // Shutdown() error 26 // Done() <-chan struct{} 27 28 // Stdout() io.Reader 29 // Stderr() io.Reader 30 // Stdin() io.Writer 31 // } 32 33 // assert Cmd implements the Command interface 34 var _ ctl.Command = (*Cmd)(nil) 35 36 // Cmd allows to control a process started through os.Exec with additional start, stop and restart capabilities, and 37 // provides readers/writers for the command's outputs and input, respectively. 38 type Cmd struct { 39 mux sync.Mutex 40 cmd *exec.Cmd 41 name string 42 exe string 43 args []string 44 stdout pipe 45 stderr pipe 46 stdin pipe 47 running bool 48 interrupted bool 49 err chan error 50 lastErr error 51 } 52 53 type pipe struct { 54 r *os.File 55 w *os.File 56 } 57 58 // NewCmd creates a new Cmd, ready to be started 59 func NewCmd(name string, exe string, args ...string) (c *Cmd, err error) { 60 c = &Cmd{ 61 name: name, 62 exe: exe, 63 args: args, 64 err: make(chan error, 1), 65 } 66 67 // create pipes for stdout, stderr and stdin 68 c.stdout.r, c.stdout.w, err = os.Pipe() 69 if err != nil { 70 return nil, err 71 } 72 73 c.stderr.r, c.stderr.w, err = os.Pipe() 74 if err != nil { 75 return nil, err 76 } 77 78 c.stdin.r, c.stdin.w, err = os.Pipe() 79 if err != nil { 80 return nil, err 81 } 82 83 return c, nil 84 } 85 86 func (c *Cmd) Name() string { 87 c.mux.Lock() 88 defer c.mux.Unlock() 89 90 return c.name 91 } 92 93 func (c *Cmd) Running() bool { 94 c.mux.Lock() 95 defer c.mux.Unlock() 96 97 return c.running 98 } 99 100 // Start starts the command if it's not already running. It will be a noop if it is. 101 // It also spins up a goroutine that will receive any error occurred during the command's exit. 102 func (c *Cmd) Start() error { 103 c.mux.Lock() 104 defer c.mux.Unlock() 105 106 if c.running { 107 return nil 108 } 109 110 c.running = true 111 c.interrupted = false 112 c.lastErr = nil 113 114 // create the command with the found executable and the its args 115 cmd := exec.Command(c.exe, c.args...) 116 c.cmd = cmd 117 118 // assign the pipes to the command 119 c.cmd.Stdout = c.stdout.w 120 c.cmd.Stderr = c.stderr.w 121 c.cmd.Stdin = c.stdin.r 122 123 // start the command 124 err := c.cmd.Start() 125 if err != nil { 126 return usererror.Wrapm(err, "Command execution failed") 127 } 128 129 go func() { 130 err = cmd.Wait() 131 132 c.err <- err 133 134 c.mux.Lock() 135 136 c.running = false 137 138 c.mux.Unlock() 139 }() 140 141 return nil 142 } 143 144 // Stop stops the running command with an os.Interrupt signal. It does not return an error if the command has 145 // already exited gracefully. 146 func (c *Cmd) Stop() error { 147 err := c.stop() 148 if err != nil { 149 return err 150 } 151 152 return c.Wait() 153 } 154 155 // Restart first interrupts the command if it's already running, and then re-runs the command. 156 func (c *Cmd) Restart() error { 157 err := c.stop() 158 if err != nil { 159 return err 160 } 161 162 err = c.Wait() 163 if err != nil { 164 return err 165 } 166 167 return c.Start() 168 } 169 170 // Stdout returns a reader to the command's stdout. The reader will return an io.EOF error if the command exits. 171 func (c *Cmd) Stdout() io.Reader { 172 c.mux.Lock() 173 defer c.mux.Unlock() 174 175 return c.stdout.r 176 } 177 178 // Stderr returns a reader to the command's stderr. The reader will return an io.EOF error if the command exits. 179 func (c *Cmd) Stderr() io.Reader { 180 c.mux.Lock() 181 defer c.mux.Unlock() 182 183 return c.stderr.r 184 } 185 186 // Stdin returns a writer to the command's stdin. The writer will be closed if the command has exited by the time this 187 // function is called. 188 func (c *Cmd) Stdin() io.Writer { 189 c.mux.Lock() 190 defer c.mux.Unlock() 191 192 return c.stdin.w 193 } 194 195 // Wait awaits for the command to stop running (either to gracefully exit or be interrupted). 196 // If the command has already finished when Wait is invoked, it returns the error that was returned when the command 197 // exited, if any. 198 func (c *Cmd) Wait() error { 199 c.mux.Lock() 200 201 running := c.running 202 errChan := c.err 203 lastErr := c.lastErr 204 interrupted := c.interrupted 205 206 if !running { 207 c.mux.Unlock() 208 209 return lastErr 210 } 211 212 c.mux.Unlock() 213 214 err := <-errChan 215 216 c.mux.Lock() 217 218 if err != nil && interrupted && strings.Contains(err.Error(), "signal: interrupt") { 219 err = nil 220 } else if err != nil { 221 c.lastErr = err 222 } 223 224 c.mux.Unlock() 225 226 return err 227 } 228 229 // stop requests for the command to stop, if it has already started. 230 func (c *Cmd) stop() error { 231 c.mux.Lock() 232 233 running := c.running 234 cmd := c.cmd 235 c.interrupted = true 236 237 c.mux.Unlock() 238 239 if !running { 240 return nil 241 } 242 243 if cmd != nil && cmd.Process != nil { 244 // send an interrupt signal to the command 245 err := cmd.Process.Signal(os.Interrupt) 246 if err != nil && !strings.Contains(err.Error(), "os: process already finished") { 247 return err 248 } 249 } 250 251 return nil 252 } 253 254 // Shutdown stops the cmd 255 func (c *Cmd) Shutdown() error { 256 return c.Stop() 257 } 258 259 func (c *Cmd) Done() <-chan struct{} { 260 done := make(chan struct{}) 261 go func() { 262 _ = c.Wait() 263 close(done) 264 }() 265 return done 266 }