Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(395)

Side by Side Diff: go/exec/exec.go

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

Powered by Google App Engine
This is Rietveld 408576698