| 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 |