github.com/joey-fossa/fossa-cli@v0.7.34-0.20190708193710-569f1e8679f0/exec/run.go (about)

     1  package exec
     2  
     3  import (
     4  	"bytes"
     5  	"os"
     6  	"os/exec"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/apex/log"
    11  	"github.com/pkg/errors"
    12  )
    13  
    14  // Cmd represents a single command. If Name and Argv are set, this is treated as
    15  // an executable. If Command is set, this is treated as a shell command.
    16  type Cmd struct {
    17  	Name    string   // Executable name.
    18  	Argv    []string // Executable arguments.
    19  	Command string   // Shell command.
    20  	Dir     string   // The Command's working directory.
    21  
    22  	Timeout string // Specifies the amount of time a command is allowed to run.
    23  	Retries int    // Amount of times a command can be retried.
    24  
    25  	// If neither Env nor WithEnv are set, the environment is inherited from os.Environ().
    26  	Env     map[string]string // If set, the command's environment is _set_ to Env.
    27  	WithEnv map[string]string // If set, the command's environment is _added_ to WithEnv.
    28  }
    29  
    30  // Run executes a `Cmd`, retries the specified amount, and checks for command timeout if specified.
    31  func Run(cmd Cmd) (string, string, error) {
    32  	var stdout, stderr string
    33  	var err error
    34  
    35  	for i := 0; i <= cmd.Retries; i++ {
    36  		if cmd.Timeout != "" {
    37  			stdout, stderr, err = runWithTimeout(cmd)
    38  		} else {
    39  			log.WithFields(log.Fields{
    40  				"name": cmd.Name,
    41  				"argv": cmd.Argv,
    42  			}).Debug("called Run")
    43  
    44  			xc, stderrBuf := BuildExec(cmd)
    45  
    46  			log.WithFields(log.Fields{
    47  				"dir": xc.Dir,
    48  				"env": xc.Env,
    49  			}).Debug("executing command")
    50  
    51  			var stdoutBuf []byte
    52  			stdoutBuf, err = xc.Output()
    53  			stdout = string(stdoutBuf)
    54  			stderr = stderrBuf.String()
    55  
    56  			log.WithFields(log.Fields{
    57  				"stdout": stdout,
    58  				"stderr": stderr,
    59  			}).Debug("done running")
    60  		}
    61  
    62  		if err == nil {
    63  			break
    64  		}
    65  	}
    66  
    67  	return stdout, stderr, err
    68  }
    69  
    70  // RunTimeout executes a `Cmd` and waits to see if it times out.
    71  func runWithTimeout(cmd Cmd) (string, string, error) {
    72  	log.WithFields(log.Fields{
    73  		"name": cmd.Name,
    74  		"argv": cmd.Argv,
    75  	}).Debug("called Start")
    76  
    77  	xc, stderr := BuildExec(cmd)
    78  	var stdout strings.Builder
    79  	xc.Stdout = &stdout
    80  
    81  	err := xc.Start()
    82  	if err != nil {
    83  		return "", "", errors.Wrap(err, "error starting command")
    84  	}
    85  
    86  	log.WithFields(log.Fields{
    87  		"dir": xc.Dir,
    88  		"env": xc.Env,
    89  	}).Debug("executing command")
    90  
    91  	done := make(chan error)
    92  	go func() {
    93  		done <- xc.Wait()
    94  	}()
    95  
    96  	timeout, err := time.ParseDuration((cmd.Timeout))
    97  	if err != nil {
    98  		return "", "", errors.Wrap(err, "unable to determine timeout value")
    99  	}
   100  
   101  	select {
   102  	case <-time.After(timeout):
   103  		err := xc.Process.Kill()
   104  		if err != nil {
   105  			return "", "", errors.Wrapf(err, "error killing the process")
   106  		}
   107  
   108  		return "", "", errors.Errorf("operation timed out running `%s %s` after %s", cmd.Name, cmd.Argv, cmd.Timeout)
   109  	case err := <-done:
   110  		if err != nil {
   111  			return "", "", errors.Wrap(err, "error waiting for command to finish")
   112  		}
   113  
   114  		log.WithFields(log.Fields{
   115  			"stdout": stdout.String(),
   116  			"stderr": stderr.String(),
   117  		}).Debug("done running")
   118  		return stdout.String(), stderr.String(), nil
   119  	}
   120  
   121  }
   122  
   123  func toEnv(env map[string]string) []string {
   124  	var out []string
   125  	for key, val := range env {
   126  		out = append(out, key+"="+val)
   127  	}
   128  	return out
   129  }
   130  
   131  // BuildExec translates FOSSA exec structures into standard library exec
   132  // commands.
   133  func BuildExec(cmd Cmd) (*exec.Cmd, bytes.Buffer) {
   134  	var stderr bytes.Buffer
   135  	xc := exec.Command(cmd.Name, cmd.Argv...)
   136  	xc.Stderr = &stderr
   137  
   138  	if cmd.Dir != "" {
   139  		xc.Dir = cmd.Dir
   140  	}
   141  
   142  	if cmd.Env != nil {
   143  		xc.Env = toEnv(cmd.Env)
   144  	} else if cmd.WithEnv != nil {
   145  		xc.Env = append(xc.Env, toEnv(cmd.WithEnv)...)
   146  		xc.Env = append(xc.Env, os.Environ()...)
   147  	} else {
   148  		xc.Env = os.Environ()
   149  	}
   150  
   151  	return xc, stderr
   152  }