v.io/jiri@v0.0.0-20160715023856-abfb8b131290/runutil/executor.go (about) 1 // Copyright 2015 The Vanadium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package runutil 6 7 import ( 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "os/exec" 13 "os/signal" 14 "strconv" 15 "strings" 16 "syscall" 17 "time" 18 19 "v.io/x/lib/envvar" 20 "v.io/x/lib/lookpath" 21 ) 22 23 const ( 24 prefix = ">>" 25 ) 26 27 type opts struct { 28 color bool 29 dir string 30 env map[string]string 31 stdin io.Reader 32 stdout io.Writer 33 stderr io.Writer 34 verbose bool 35 } 36 37 type executor struct { 38 indent int 39 opts opts 40 } 41 42 func newExecutor(env map[string]string, stdin io.Reader, stdout, stderr io.Writer, color, verbose bool) *executor { 43 if color { 44 term := os.Getenv("TERM") 45 switch term { 46 case "dumb", "": 47 color = false 48 } 49 } 50 return &executor{ 51 indent: 0, 52 opts: opts{ 53 color: color, 54 env: env, 55 stdin: stdin, 56 stdout: stdout, 57 stderr: stderr, 58 verbose: verbose, 59 }, 60 } 61 } 62 63 var ( 64 commandTimedOutErr = fmt.Errorf("command timed out") 65 ) 66 67 // run run's the command and waits for it to finish 68 func (e *executor) run(timeout time.Duration, opts opts, path string, args ...string) error { 69 _, err := e.execute(true, timeout, opts, path, args...) 70 return err 71 } 72 73 // start start's the command and does not wait for it to finish. 74 func (e *executor) start(timeout time.Duration, opts opts, path string, args ...string) (*exec.Cmd, error) { 75 return e.execute(false, timeout, opts, path, args...) 76 } 77 78 // function runs the given function and logs its outcome using 79 // the given options. 80 func (e *executor) function(opts opts, fn func() error, format string, args ...interface{}) error { 81 e.increaseIndent() 82 defer e.decreaseIndent() 83 e.printf(e.verboseStdout(opts), format, args...) 84 err := fn() 85 e.printf(e.verboseStdout(opts), okOrFailed(err)) 86 return err 87 } 88 89 func okOrFailed(err error) string { 90 if err != nil { 91 return fmt.Sprintf("FAILED: %v", err) 92 } 93 return "OK" 94 } 95 96 func (e *executor) verboseStdout(opts opts) io.Writer { 97 if opts.verbose || e.opts.verbose && (e.opts.stdout != nil) { 98 return e.opts.stdout 99 } 100 return ioutil.Discard 101 } 102 103 func (e *executor) stderrFromOpts(opts opts) io.Writer { 104 if opts.stderr != nil { 105 return opts.stderr 106 } 107 if e.opts.stderr != nil { 108 return e.opts.stderr 109 } 110 return ioutil.Discard 111 } 112 113 // output logs the given list of lines using the given 114 // options. 115 func (e *executor) output(opts opts, output []string) { 116 if opts.verbose { 117 for _, line := range output { 118 e.logLine(line) 119 } 120 } 121 } 122 123 func (e *executor) logLine(line string) { 124 if !strings.HasPrefix(line, prefix) { 125 e.increaseIndent() 126 defer e.decreaseIndent() 127 } 128 e.printf(e.opts.stdout, "%v", line) 129 } 130 131 // call executes the given Go standard library function, 132 // encapsulated as a closure. 133 func (e *executor) call(fn func() error, format string, args ...interface{}) error { 134 return e.function(e.opts, fn, format, args...) 135 } 136 137 // execute executes the binary pointed to by the given path using the given 138 // arguments and options. If the wait flag is set, the function waits for the 139 // completion of the binary and the timeout value can optionally specify for 140 // how long should the function wait before timing out. 141 func (e *executor) execute(wait bool, timeout time.Duration, opts opts, path string, args ...string) (*exec.Cmd, error) { 142 e.increaseIndent() 143 defer e.decreaseIndent() 144 145 // Check if <path> identifies a binary in the PATH environment 146 // variable of the opts.Env. 147 if binary, err := lookpath.Look(opts.env, path); err == nil { 148 // If so, make sure to execute this binary. This step 149 // enables us to "shadow" binaries included in the 150 // PATH environment variable of the host OS (which 151 // would be otherwise used to lookup <path>). 152 // 153 // This mechanism is used instead of modifying the 154 // PATH environment variable of the host OS as the 155 // latter is not thread-safe. 156 path = binary 157 } 158 command := exec.Command(path, args...) 159 command.Dir = opts.dir 160 command.Stdin = opts.stdin 161 command.Stdout = opts.stdout 162 command.Stderr = opts.stderr 163 command.Env = envvar.MapToSlice(opts.env) 164 if out := e.verboseStdout(opts); out != ioutil.Discard { 165 args := []string{} 166 for _, arg := range command.Args { 167 // Quote any arguments that contain '"', ''', '|', or ' '. 168 if strings.IndexAny(arg, "\"' |") != -1 { 169 args = append(args, strconv.Quote(arg)) 170 } else { 171 args = append(args, arg) 172 } 173 } 174 e.printf(out, strings.Replace(strings.Join(args, " "), "%", "%%", -1)) 175 } 176 177 var err error 178 switch { 179 case !wait: 180 err = command.Start() 181 e.printf(e.verboseStdout(opts), okOrFailed(err)) 182 183 case timeout == 0: 184 err = command.Run() 185 e.printf(e.verboseStdout(opts), okOrFailed(err)) 186 default: 187 err = e.timedCommand(timeout, opts, command) 188 // Verbose output handled in timedCommand. 189 } 190 return command, err 191 } 192 193 // timedCommand executes the given command, terminating it forcefully 194 // if it is still running after the given timeout elapses. 195 func (e *executor) timedCommand(timeout time.Duration, opts opts, command *exec.Cmd) error { 196 // Make the process of this command a new process group leader 197 // to facilitate clean up of processes that time out. 198 command.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} 199 // Kill this process group explicitly when receiving SIGTERM 200 // or SIGINT signals. 201 sigchan := make(chan os.Signal, 1) 202 signal.Notify(sigchan, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT) 203 go func() { 204 <-sigchan 205 e.terminateProcessGroup(opts, command) 206 }() 207 if err := command.Start(); err != nil { 208 e.printf(e.verboseStdout(opts), "FAILED: %v", err) 209 return err 210 } 211 done := make(chan error, 1) 212 go func() { 213 done <- command.Wait() 214 }() 215 select { 216 case <-time.After(timeout): 217 // The command has timed out. 218 e.terminateProcessGroup(opts, command) 219 // Allow goroutine to exit. 220 <-done 221 e.printf(e.verboseStdout(opts), "TIMED OUT") 222 return commandTimedOutErr 223 case err := <-done: 224 e.printf(e.verboseStdout(opts), okOrFailed(err)) 225 return err 226 } 227 } 228 229 // terminateProcessGroup sends SIGQUIT followed by SIGKILL to the 230 // process group (the negative value of the process's pid). 231 func (e *executor) terminateProcessGroup(opts opts, command *exec.Cmd) { 232 pid := -command.Process.Pid 233 // Use SIGQUIT in order to get a stack dump of potentially hanging 234 // commands. 235 if err := syscall.Kill(pid, syscall.SIGQUIT); err != nil { 236 e.printf(e.stderrFromOpts(opts), "Kill(%v, %v) failed: %v", pid, syscall.SIGQUIT, err) 237 } 238 e.printf(e.stderrFromOpts(opts), "Waiting for command to exit: %q", command.Args) 239 // Give the process some time to shut down cleanly. 240 for i := 0; i < 50; i++ { 241 if err := syscall.Kill(pid, 0); err != nil { 242 return 243 } 244 time.Sleep(200 * time.Millisecond) 245 } 246 // If it still exists, send SIGKILL to it. 247 if err := syscall.Kill(pid, 0); err == nil { 248 if err := syscall.Kill(-command.Process.Pid, syscall.SIGKILL); err != nil { 249 e.printf(e.stderrFromOpts(opts), "Kill(%v, %v) failed: %v", pid, syscall.SIGKILL, err) 250 } 251 } 252 } 253 254 func (e *executor) decreaseIndent() { 255 e.indent-- 256 } 257 258 func (e *executor) increaseIndent() { 259 e.indent++ 260 } 261 262 func (e *executor) printf(out io.Writer, format string, args ...interface{}) { 263 timestamp := time.Now().Format("15:04:05.00") 264 args = append([]interface{}{timestamp, strings.Repeat(prefix, e.indent)}, args...) 265 fmt.Fprintf(out, "[%s] %v "+format+"\n", args...) 266 }