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