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  }