| OLD | NEW |
| (Empty) | |
| 1 /* |
| 2 A wrapper around the os/exec package that supports timeouts and testing. |
| 3 |
| 4 Example usage: |
| 5 |
| 6 Simple command with argument: |
| 7 err := Run(&Command{ |
| 8 Name: "touch", |
| 9 Args: []string{file}, |
| 10 }) |
| 11 |
| 12 More complicated example: |
| 13 output := bytes.Buffer{} |
| 14 err := Run(&Command{ |
| 15 Name: "make", |
| 16 Args: []string{"all"}, |
| 17 // Set environment: |
| 18 Env: []string{fmt.Sprintf("GOPATH=%s", projectGoPath)}, |
| 19 // Set working directory: |
| 20 Dir: projectDir, |
| 21 // Capture output: |
| 22 CombinedOutput: &output, |
| 23 // Set a timeout: |
| 24 Timeout: 10*time.Minute, |
| 25 }) |
| 26 |
| 27 Inject a Run function for testing: |
| 28 var actualCommand *Command |
| 29 SetRunForTesting(func(command *Command) error { |
| 30 actualCommand = command |
| 31 return nil |
| 32 }) |
| 33 defer SetRunForTesting(DefaultRun) |
| 34 TestCodeCallingRun() |
| 35 expect.Equal(t, "touch", actualCommand.Name) |
| 36 expect.Equal(t, 1, len(actualCommand.Args)) |
| 37 expect.Equal(t, file, actualCommand.Args[0]) |
| 38 */ |
| 39 package exec |
| 40 |
| 41 import ( |
| 42 "bytes" |
| 43 "fmt" |
| 44 "io" |
| 45 "os" |
| 46 osexec "os/exec" |
| 47 "strings" |
| 48 "time" |
| 49 |
| 50 "github.com/skia-dev/glog" |
| 51 ) |
| 52 |
| 53 // WriteLog implements the io.Writer interface and writes to the given log funct
ion. |
| 54 type WriteLog struct { |
| 55 LogFunc func(format string, args ...interface{}) |
| 56 } |
| 57 |
| 58 func (wl WriteLog) Write(p []byte) (n int, err error) { |
| 59 wl.LogFunc("%s", string(p)) |
| 60 return len(p), nil |
| 61 } |
| 62 |
| 63 var ( |
| 64 WriteInfoLog = WriteLog{LogFunc: glog.Infof} |
| 65 WriteErrorLog = WriteLog{LogFunc: glog.Errorf} |
| 66 ) |
| 67 |
| 68 type Command struct { |
| 69 // Name of the command, as passed to osexec.Command. Can be the path to
a binary or the |
| 70 // name of a command that osexec.Lookpath can find. |
| 71 Name string |
| 72 // Arguments of the command, not including Name. |
| 73 Args []string |
| 74 // The environment of the process. If nil, the current process's environ
ment is used. |
| 75 Env []string |
| 76 // If Env is non-nil, adds the current process's PATH to Env. |
| 77 InheritPath bool |
| 78 // The working directory of the command. If nil, runs in the current pro
cess's current |
| 79 // directory. |
| 80 Dir string |
| 81 // See docs for osexec.Cmd.Stdin. |
| 82 Stdin io.Reader |
| 83 // If true, duplicates stdout of the command to WriteInfoLog. |
| 84 LogStdout bool |
| 85 // Sends the stdout of the command to this Writer, e.g. os.File or bytes
.Buffer. |
| 86 Stdout io.Writer |
| 87 // If true, duplicates stderr of the command to WriteErrorLog. |
| 88 LogStderr bool |
| 89 // Sends the stderr of the command to this Writer, e.g. os.File or bytes
.Buffer. |
| 90 Stderr io.Writer |
| 91 // Sends the combined stdout and stderr of the command to this Writer, i
n addition to |
| 92 // Stdout and Stderr. Only one goroutine will write at a time. Note: the
Go runtime seems to |
| 93 // combine stdout and stderr into one stream as long as LogStdout and Lo
gStderr are false |
| 94 // and Stdout and Stderr are nil. Otherwise, the stdout and stderr of th
e command could be |
| 95 // arbitrarily reordered when written to CombinedOutput. |
| 96 CombinedOutput io.Writer |
| 97 // Time limit to wait for the command to finish. (Starts when Wait is ca
lled.) No limit if |
| 98 // not specified. |
| 99 Timeout time.Duration |
| 100 } |
| 101 |
| 102 // Divides commandLine at spaces; treats the first token as the program name and
the other tokens |
| 103 // as arguments. Note: don't expect this function to do anything smart with quot
es or escaped |
| 104 // spaces. |
| 105 func ParseCommand(commandLine string) Command { |
| 106 programAndArgs := strings.Split(commandLine, " ") |
| 107 return Command{Name: programAndArgs[0], Args: programAndArgs[1:]} |
| 108 } |
| 109 |
| 110 // Given io.Writers or nils, return a single writer that writes to all, or nil i
f no non-nil |
| 111 // writers. Does not handle non-nil interface containing a nil value. |
| 112 // http://devs.cloudimmunity.com/gotchas-and-common-mistakes-in-go-golang/index.
html#nil_in_nil_in_vals |
| 113 func squashWriters(writers ...io.Writer) io.Writer { |
| 114 nonNil := []io.Writer{} |
| 115 for _, writer := range writers { |
| 116 if writer != nil { |
| 117 nonNil = append(nonNil, writer) |
| 118 } |
| 119 } |
| 120 switch len(nonNil) { |
| 121 case 0: |
| 122 return nil |
| 123 case 1: |
| 124 return nonNil[0] |
| 125 default: |
| 126 return io.MultiWriter(nonNil...) |
| 127 } |
| 128 } |
| 129 |
| 130 func createCmd(command *Command) *osexec.Cmd { |
| 131 cmd := osexec.Command(command.Name, command.Args...) |
| 132 if len(command.Env) != 0 { |
| 133 cmd.Env = command.Env |
| 134 if command.InheritPath { |
| 135 cmd.Env = append(cmd.Env, "PATH="+os.Getenv("PATH")) |
| 136 } |
| 137 } |
| 138 cmd.Dir = command.Dir |
| 139 cmd.Stdin = command.Stdin |
| 140 var stdoutLog io.Writer |
| 141 if command.LogStdout { |
| 142 stdoutLog = WriteInfoLog |
| 143 } |
| 144 cmd.Stdout = squashWriters(stdoutLog, command.Stdout, command.CombinedOu
tput) |
| 145 var stderrLog io.Writer |
| 146 if command.LogStderr { |
| 147 stderrLog = WriteErrorLog |
| 148 } |
| 149 cmd.Stderr = squashWriters(stderrLog, command.Stderr, command.CombinedOu
tput) |
| 150 return cmd |
| 151 } |
| 152 |
| 153 func start(cmd *osexec.Cmd) error { |
| 154 if len(cmd.Env) == 0 { |
| 155 glog.Infof("Executing %s", strings.Join(cmd.Args, " ")) |
| 156 } else { |
| 157 glog.Infof("Executing %s with env %s", |
| 158 strings.Join(cmd.Args, " "), strings.Join(cmd.Env, " ")) |
| 159 } |
| 160 err := cmd.Start() |
| 161 if err != nil { |
| 162 glog.Errorf("Unable to start command %s: %s", strings.Join(cmd.A
rgs, " "), err) |
| 163 } |
| 164 return err |
| 165 } |
| 166 |
| 167 func waitSimple(cmd *osexec.Cmd) error { |
| 168 err := cmd.Wait() |
| 169 if err != nil { |
| 170 glog.Errorf("Command exited with %s: %s", err, strings.Join(cmd.
Args, " ")) |
| 171 } |
| 172 return err |
| 173 } |
| 174 |
| 175 func wait(command *Command, cmd *osexec.Cmd) error { |
| 176 if command.Timeout == 0 { |
| 177 return waitSimple(cmd) |
| 178 } |
| 179 done := make(chan error) |
| 180 go func() { |
| 181 done <- cmd.Wait() |
| 182 }() |
| 183 select { |
| 184 case <-time.After(command.Timeout): |
| 185 if err := cmd.Process.Kill(); err != nil { |
| 186 return fmt.Errorf("Failed to kill timed out process: %s"
, err) |
| 187 } |
| 188 <-done // allow goroutine to exit |
| 189 glog.Errorf("Command killed since it took longer than %f secs",
command.Timeout.Seconds()) |
| 190 return fmt.Errorf("Command killed since it took longer than %f s
ecs", command.Timeout.Seconds()) |
| 191 case err := <-done: |
| 192 if err != nil { |
| 193 glog.Errorf("Command exited with %s: %s", err, strings.J
oin(cmd.Args, " ")) |
| 194 } |
| 195 return err |
| 196 } |
| 197 } |
| 198 |
| 199 // Default value of Run. |
| 200 func DefaultRun(command *Command) error { |
| 201 cmd := createCmd(command) |
| 202 if err := start(cmd); err != nil { |
| 203 return err |
| 204 } |
| 205 return wait(command, cmd) |
| 206 } |
| 207 |
| 208 // Run runs command and waits for it to finish. If any failure, returns non-nil.
If a timeout was |
| 209 // specified, returns an error once the command has exceeded that timeout. |
| 210 var Run func(command *Command) error = DefaultRun |
| 211 |
| 212 // SetRunForTesting replaces the Run function with a test version so that comman
ds don't actually |
| 213 // run. |
| 214 func SetRunForTesting(testRun func(command *Command) error) { |
| 215 Run = testRun |
| 216 } |
| 217 |
| 218 // Run method is convenience for Run(command). |
| 219 func (command *Command) Run() error { |
| 220 return Run(command) |
| 221 } |
| 222 |
| 223 // RunSimple executes the given command line string; the command being run is ex
pected to not care |
| 224 // what its current working directory is. Returns the combined stdout and stderr
. May also return |
| 225 // an error if the command exited with a non-zero status or there is any other e
rror. |
| 226 func RunSimple(commandLine string) (string, error) { |
| 227 command := ParseCommand(commandLine) |
| 228 output := bytes.Buffer{} |
| 229 command.CombinedOutput = &output |
| 230 err := Run(&command) |
| 231 result := string(output.Bytes()) |
| 232 glog.Infof("StdOut + StdErr: %s\n", result) |
| 233 return result, err |
| 234 } |
| OLD | NEW |