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 }