blob: 44ef680dc516db5c0b7641f1fe3bdb6fa9558f4c [file] [log] [blame]
Cosmos Nicolaou62613842014-08-25 21:57:37 -07001package modules
2
3import (
Cosmos Nicolaou62613842014-08-25 21:57:37 -07004 "flag"
5 "fmt"
6 "io"
Cosmos Nicolaou62613842014-08-25 21:57:37 -07007 "os"
8 "os/exec"
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -07009 "regexp"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070010 "strings"
11 "sync"
12 "time"
13
Jiri Simsa519c5072014-09-17 21:37:57 -070014 "veyron.io/veyron/veyron2/vlog"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070015
Cosmos Nicolaou486d3492014-09-30 22:21:20 -070016 vexec "veyron.io/veyron/veyron/lib/exec"
Cosmos Nicolaou62613842014-08-25 21:57:37 -070017)
18
19// execHandle implements both the command and Handle interfaces.
20type execHandle struct {
21 mu sync.Mutex
22 cmd *exec.Cmd
23 entryPoint string
24 handle *vexec.ParentHandle
Cosmos Nicolaou66afced2014-09-15 22:12:43 -070025 sh *Shell
Cosmos Nicolaou62613842014-08-25 21:57:37 -070026 stderr *os.File
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -070027 stdout io.ReadCloser
Cosmos Nicolaou62613842014-08-25 21:57:37 -070028 stdin io.WriteCloser
29}
30
31func testFlags() []string {
32 var fl []string
33 // pass logging flags to any subprocesses
34 for fname, fval := range vlog.Log.ExplicitlySetFlags() {
35 fl = append(fl, "--"+fname+"="+fval)
36 }
37 timeout := flag.Lookup("test.timeout")
38 if timeout == nil {
39 // not a go test binary
40 return fl
41 }
42 // must be a go test binary
43 fl = append(fl, "-test.run=TestHelperProcess")
44 val := timeout.Value.(flag.Getter).Get().(time.Duration)
45 if val.String() != timeout.DefValue {
46 // use supplied command value for subprocesses
47 fl = append(fl, "--test.timeout="+timeout.Value.String())
48 } else {
49 // translate default value into 1m for subproccesses
50 fl = append(fl, "--test.timeout=1m")
51 }
52 return fl
53}
54
Cosmos Nicolaou9c9918d2014-09-23 08:45:56 -070055// IsTestHelperProces returns true if it is called in via
56// -run=TestHelperProcess which normally only ever happens for subprocesses
57// run from tests.
Cosmos Nicolaou62613842014-08-25 21:57:37 -070058func IsTestHelperProcess() bool {
59 runFlag := flag.Lookup("test.run")
60 if runFlag == nil {
61 return false
62 }
63 return runFlag.Value.String() == "TestHelperProcess"
64}
65
66func newExecHandle(entryPoint string) command {
67 return &execHandle{entryPoint: entryPoint}
68}
69
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -070070func (eh *execHandle) Stdout() io.Reader {
Cosmos Nicolaou62613842014-08-25 21:57:37 -070071 eh.mu.Lock()
72 defer eh.mu.Unlock()
73 return eh.stdout
74}
75
76func (eh *execHandle) Stderr() io.Reader {
77 eh.mu.Lock()
78 defer eh.mu.Unlock()
79 return eh.stderr
80}
81
Cosmos Nicolaou66afced2014-09-15 22:12:43 -070082func (eh *execHandle) Stdin() io.Writer {
Cosmos Nicolaou62613842014-08-25 21:57:37 -070083 eh.mu.Lock()
84 defer eh.mu.Unlock()
85 return eh.stdin
86}
87
Cosmos Nicolaou66afced2014-09-15 22:12:43 -070088func (eh *execHandle) CloseStdin() {
89 eh.mu.Lock()
90 eh.stdin.Close()
91 eh.mu.Unlock()
92}
93
Cosmos Nicolaou62613842014-08-25 21:57:37 -070094func osEnvironMap() map[string]string {
95 m := make(map[string]string)
96 for _, osv := range os.Environ() {
97 if len(osv) == 0 {
98 continue
99 }
100 parts := strings.SplitN(osv, "=", 2)
101 key := parts[0]
102 if len(parts) == 2 {
103 m[key] = parts[1]
104 } else {
105 m[key] = ""
106 }
107 }
108 return m
109}
110func (sh *Shell) mergeOSEnv() map[string]string {
111 merged := osEnvironMap()
112 sh.mu.Lock()
113 for k, v := range sh.env {
114 merged[k] = v
115 }
116 sh.mu.Unlock()
117 return merged
118}
119
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700120func (eh *execHandle) envelope(sh *Shell, args ...string) ([]string, []string) {
121 newargs := []string{os.Args[0]}
122 newargs = append(newargs, testFlags()...)
123 newargs = append(newargs, args...)
124 return newargs, append(sh.MergedEnv(), eh.entryPoint)
125}
126
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700127func (eh *execHandle) start(sh *Shell, args ...string) (Handle, error) {
128 eh.mu.Lock()
129 defer eh.mu.Unlock()
Cosmos Nicolaou66afced2014-09-15 22:12:43 -0700130 eh.sh = sh
Cosmos Nicolaou59496fe2014-10-14 11:21:05 -0700131 // Take care to not pass the command line as an arg to the child
132 // process since that'll prevent parsing any subsequent args by
133 // the flag package.
134 newargs := append(testFlags(), args[1:]...)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700135 cmd := exec.Command(os.Args[0], newargs...)
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700136 cmd.Env = append(sh.MergedEnv(), eh.entryPoint)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700137 fname := strings.TrimPrefix(eh.entryPoint, ShellEntryPoint+"=")
138 stderr, err := newLogfile(strings.TrimLeft(fname, "-\n\t "))
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700139 if err != nil {
140 return nil, err
141 }
142 cmd.Stderr = stderr
143 stdout, err := cmd.StdoutPipe()
144 if err != nil {
145 return nil, err
146 }
147 stdin, err := cmd.StdinPipe()
148 if err != nil {
149 return nil, err
150 }
151
152 handle := vexec.NewParentHandle(cmd)
Cosmos Nicolaou9ca249d2014-09-18 15:07:12 -0700153 eh.stdout = stdout
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700154 eh.stderr = stderr
155 eh.stdin = stdin
156 eh.handle = handle
157 eh.cmd = cmd
158 if err := handle.Start(); err != nil {
159 return nil, err
160 }
Matt Rosencrantz281729f2014-09-19 10:45:53 -0700161 err = handle.WaitForReady(10 * time.Second)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700162 return eh, err
163}
164
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700165func (eh *execHandle) Pid() int {
166 return eh.cmd.Process.Pid
167}
168
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700169func (eh *execHandle) Shutdown(stdout, stderr io.Writer) error {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700170 eh.mu.Lock()
171 defer eh.mu.Unlock()
172 eh.stdin.Close()
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700173 logFile := eh.stderr.Name()
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700174 defer eh.sh.Forget(eh)
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700175
176 defer func() {
177 os.Remove(logFile)
178 }()
179
180 if stdout == nil && stderr == nil {
181 return eh.cmd.Wait()
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700182 }
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700183 // Read from stdin before waiting for the child process to ensure
184 // that we get to read all of its output.
185 readTo(eh.stdout, stdout)
186
187 procErr := eh.cmd.Wait()
188
189 // Stderr is buffered to a file, so we can safely read it after we
190 // wait for the process.
191 eh.stderr.Close()
Cosmos Nicolaoue664f3f2014-10-20 17:40:05 -0700192 if stderr != nil {
193 stderrFile, err := os.Open(logFile)
194 if err != nil {
195 fmt.Fprintf(os.Stderr, "failed to open %q: %s\n", logFile, err)
196 return procErr
197 }
198 readTo(stderrFile, stderr)
199 stderrFile.Close()
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700200 }
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700201 return procErr
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700202}
203
Cosmos Nicolaou9c9918d2014-09-23 08:45:56 -0700204const ShellEntryPoint = "VEYRON_SHELL_HELPER_PROCESS_ENTRY_POINT"
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700205
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700206func RegisterChild(name, help string, main Main) {
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700207 child.Lock()
208 defer child.Unlock()
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700209 child.mains[name] = &childEntryPoint{main, help}
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700210}
211
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700212// DispatchInTest will execute the requested subproccess command from within
213// a unit test run as a subprocess.
214func DispatchInTest() {
215 if !IsTestHelperProcess() {
216 return
217 }
218 if err := child.dispatch(); err != nil {
219 fmt.Fprintf(os.Stderr, "Failed: %s\n", err)
220 os.Exit(1)
221 }
222 os.Exit(0)
223}
224
Cosmos Nicolaoucc581722014-10-07 12:45:39 -0700225// Dispatch will execute the requested subprocess command from a within a
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700226// a subprocess that is not a unit test.
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700227func Dispatch() error {
Cosmos Nicolaoubbae3882014-10-02 22:58:19 -0700228 if IsTestHelperProcess() {
229 return fmt.Errorf("use DispatchInTest in unittests")
230 }
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700231 return child.dispatch()
232}
233
234func (child *childRegistrar) hasCommand(name string) bool {
235 child.Lock()
236 _, present := child.mains[name]
237 child.Unlock()
238 return present
239}
240
241func (child *childRegistrar) dispatch() error {
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700242 ch, _ := vexec.GetChildHandle()
243 // Only signal that the child is ready or failed if we successfully get
244 // a child handle. We most likely failed to get a child handle
245 // because the subprocess was run directly from the command line.
246
Cosmos Nicolaou9c9918d2014-09-23 08:45:56 -0700247 command := os.Getenv(ShellEntryPoint)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700248 if len(command) == 0 {
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700249 err := fmt.Errorf("Failed to find entrypoint %q", ShellEntryPoint)
250 if ch != nil {
251 ch.SetFailed(err)
252 }
253 return err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700254 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700255
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700256 child.Lock()
257 m := child.mains[command]
258 child.Unlock()
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700259
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700260 if m == nil {
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700261 err := fmt.Errorf("Shell command %q not registered", command)
262 if ch != nil {
263 ch.SetFailed(err)
264 }
265 return err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700266 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700267
268 if ch != nil {
269 ch.SetReady()
270 }
271
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700272 go func(pid int) {
273 for {
274 _, err := os.FindProcess(pid)
275 if err != nil {
276 fmt.Fprintf(os.Stderr, "Looks like our parent exited: %v", err)
277 os.Exit(1)
278 }
279 time.Sleep(time.Second)
280 }
281 }(os.Getppid())
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700282
Cosmos Nicolaou59496fe2014-10-14 11:21:05 -0700283 args := append([]string{command}, flag.Args()...)
284 return m.fn(os.Stdin, os.Stdout, os.Stderr, osEnvironMap(), args...)
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700285}
286
287func (child *childRegistrar) addSubprocesses(sh *Shell, pattern string) error {
288 re, err := regexp.Compile(pattern)
289 if err != nil {
290 return err
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700291 }
Cosmos Nicolaou1e78ccc2014-10-09 08:10:26 -0700292 child.Lock()
293 defer child.Unlock()
294 found := false
295 for name, subproc := range child.mains {
296 if re.MatchString(name) {
297 sh.addSubprocess(name, subproc.help)
298 found = true
299 }
300 }
301 if !found {
302 return fmt.Errorf("patterh %q failed to match any registered commands", pattern)
303 }
304 return nil
305}
306
307// AddRegisteredSubprocesses adds any commands that match the regexp pattern
308// to the supplied shell.
309func AddRegisteredSubprocesses(sh *Shell, pattern string) error {
310 return child.addSubprocesses(sh, pattern)
Cosmos Nicolaou62613842014-08-25 21:57:37 -0700311}
Cosmos Nicolaouaddf4832014-09-10 21:36:54 -0700312
313// WaitForEof returns when a read on its io.Reader parameter returns io.EOF
314func WaitForEOF(stdin io.Reader) {
315 buf := [1024]byte{}
316 for {
317 if _, err := stdin.Read(buf[:]); err == io.EOF {
318 return
319 }
320 }
321}