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