Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 1 | package modules |
| 2 | |
| 3 | import ( |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 4 | "flag" |
| 5 | "fmt" |
| 6 | "io" |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 7 | "os" |
| 8 | "os/exec" |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 9 | "regexp" |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 10 | "strings" |
| 11 | "sync" |
| 12 | "time" |
| 13 | |
Jiri Simsa | 519c507 | 2014-09-17 21:37:57 -0700 | [diff] [blame] | 14 | "veyron.io/veyron/veyron2/vlog" |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 15 | |
Cosmos Nicolaou | 486d349 | 2014-09-30 22:21:20 -0700 | [diff] [blame] | 16 | vexec "veyron.io/veyron/veyron/lib/exec" |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 17 | ) |
| 18 | |
| 19 | // execHandle implements both the command and Handle interfaces. |
| 20 | type execHandle struct { |
| 21 | mu sync.Mutex |
| 22 | cmd *exec.Cmd |
| 23 | entryPoint string |
| 24 | handle *vexec.ParentHandle |
Cosmos Nicolaou | 66afced | 2014-09-15 22:12:43 -0700 | [diff] [blame] | 25 | sh *Shell |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 26 | stderr *os.File |
Cosmos Nicolaou | 9ca249d | 2014-09-18 15:07:12 -0700 | [diff] [blame] | 27 | stdout io.ReadCloser |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 28 | stdin io.WriteCloser |
| 29 | } |
| 30 | |
| 31 | func 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 Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 55 | // IsTestHelperProces returns true if it is called in via |
| 56 | // -run=TestHelperProcess which normally only ever happens for subprocesses |
| 57 | // run from tests. |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 58 | func IsTestHelperProcess() bool { |
| 59 | runFlag := flag.Lookup("test.run") |
| 60 | if runFlag == nil { |
| 61 | return false |
| 62 | } |
| 63 | return runFlag.Value.String() == "TestHelperProcess" |
| 64 | } |
| 65 | |
| 66 | func newExecHandle(entryPoint string) command { |
| 67 | return &execHandle{entryPoint: entryPoint} |
| 68 | } |
| 69 | |
Cosmos Nicolaou | 9ca249d | 2014-09-18 15:07:12 -0700 | [diff] [blame] | 70 | func (eh *execHandle) Stdout() io.Reader { |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 71 | eh.mu.Lock() |
| 72 | defer eh.mu.Unlock() |
| 73 | return eh.stdout |
| 74 | } |
| 75 | |
| 76 | func (eh *execHandle) Stderr() io.Reader { |
| 77 | eh.mu.Lock() |
| 78 | defer eh.mu.Unlock() |
| 79 | return eh.stderr |
| 80 | } |
| 81 | |
Cosmos Nicolaou | 66afced | 2014-09-15 22:12:43 -0700 | [diff] [blame] | 82 | func (eh *execHandle) Stdin() io.Writer { |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 83 | eh.mu.Lock() |
| 84 | defer eh.mu.Unlock() |
| 85 | return eh.stdin |
| 86 | } |
| 87 | |
Cosmos Nicolaou | 66afced | 2014-09-15 22:12:43 -0700 | [diff] [blame] | 88 | func (eh *execHandle) CloseStdin() { |
| 89 | eh.mu.Lock() |
| 90 | eh.stdin.Close() |
| 91 | eh.mu.Unlock() |
| 92 | } |
| 93 | |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 94 | func 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 | } |
| 110 | func (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 Nicolaou | e664f3f | 2014-10-20 17:40:05 -0700 | [diff] [blame^] | 120 | func (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 Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 127 | func (eh *execHandle) start(sh *Shell, args ...string) (Handle, error) { |
| 128 | eh.mu.Lock() |
| 129 | defer eh.mu.Unlock() |
Cosmos Nicolaou | 66afced | 2014-09-15 22:12:43 -0700 | [diff] [blame] | 130 | eh.sh = sh |
Cosmos Nicolaou | 59496fe | 2014-10-14 11:21:05 -0700 | [diff] [blame] | 131 | // 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 Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 135 | cmd := exec.Command(os.Args[0], newargs...) |
Cosmos Nicolaou | e664f3f | 2014-10-20 17:40:05 -0700 | [diff] [blame^] | 136 | cmd.Env = append(sh.MergedEnv(), eh.entryPoint) |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 137 | fname := strings.TrimPrefix(eh.entryPoint, ShellEntryPoint+"=") |
| 138 | stderr, err := newLogfile(strings.TrimLeft(fname, "-\n\t ")) |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 139 | 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 Nicolaou | 9ca249d | 2014-09-18 15:07:12 -0700 | [diff] [blame] | 153 | eh.stdout = stdout |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 154 | 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 Rosencrantz | 281729f | 2014-09-19 10:45:53 -0700 | [diff] [blame] | 161 | err = handle.WaitForReady(10 * time.Second) |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 162 | return eh, err |
| 163 | } |
| 164 | |
Cosmos Nicolaou | cc58172 | 2014-10-07 12:45:39 -0700 | [diff] [blame] | 165 | func (eh *execHandle) Pid() int { |
| 166 | return eh.cmd.Process.Pid |
| 167 | } |
| 168 | |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 169 | func (eh *execHandle) Shutdown(stdout, stderr io.Writer) error { |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 170 | eh.mu.Lock() |
| 171 | defer eh.mu.Unlock() |
| 172 | eh.stdin.Close() |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 173 | logFile := eh.stderr.Name() |
Cosmos Nicolaou | e664f3f | 2014-10-20 17:40:05 -0700 | [diff] [blame^] | 174 | defer eh.sh.Forget(eh) |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 175 | |
| 176 | defer func() { |
| 177 | os.Remove(logFile) |
| 178 | }() |
| 179 | |
| 180 | if stdout == nil && stderr == nil { |
| 181 | return eh.cmd.Wait() |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 182 | } |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 183 | // 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 Nicolaou | e664f3f | 2014-10-20 17:40:05 -0700 | [diff] [blame^] | 192 | 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 Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 200 | } |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 201 | return procErr |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 202 | } |
| 203 | |
Cosmos Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 204 | const ShellEntryPoint = "VEYRON_SHELL_HELPER_PROCESS_ENTRY_POINT" |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 205 | |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 206 | func RegisterChild(name, help string, main Main) { |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 207 | child.Lock() |
| 208 | defer child.Unlock() |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 209 | child.mains[name] = &childEntryPoint{main, help} |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 210 | } |
| 211 | |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 212 | // DispatchInTest will execute the requested subproccess command from within |
| 213 | // a unit test run as a subprocess. |
| 214 | func 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 Nicolaou | cc58172 | 2014-10-07 12:45:39 -0700 | [diff] [blame] | 225 | // Dispatch will execute the requested subprocess command from a within a |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 226 | // a subprocess that is not a unit test. |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 227 | func Dispatch() error { |
Cosmos Nicolaou | bbae388 | 2014-10-02 22:58:19 -0700 | [diff] [blame] | 228 | if IsTestHelperProcess() { |
| 229 | return fmt.Errorf("use DispatchInTest in unittests") |
| 230 | } |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 231 | return child.dispatch() |
| 232 | } |
| 233 | |
| 234 | func (child *childRegistrar) hasCommand(name string) bool { |
| 235 | child.Lock() |
| 236 | _, present := child.mains[name] |
| 237 | child.Unlock() |
| 238 | return present |
| 239 | } |
| 240 | |
| 241 | func (child *childRegistrar) dispatch() error { |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 242 | 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 Nicolaou | 9c9918d | 2014-09-23 08:45:56 -0700 | [diff] [blame] | 247 | command := os.Getenv(ShellEntryPoint) |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 248 | if len(command) == 0 { |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 249 | err := fmt.Errorf("Failed to find entrypoint %q", ShellEntryPoint) |
| 250 | if ch != nil { |
| 251 | ch.SetFailed(err) |
| 252 | } |
| 253 | return err |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 254 | } |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 255 | |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 256 | child.Lock() |
| 257 | m := child.mains[command] |
| 258 | child.Unlock() |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 259 | |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 260 | if m == nil { |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 261 | err := fmt.Errorf("Shell command %q not registered", command) |
| 262 | if ch != nil { |
| 263 | ch.SetFailed(err) |
| 264 | } |
| 265 | return err |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 266 | } |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 267 | |
| 268 | if ch != nil { |
| 269 | ch.SetReady() |
| 270 | } |
| 271 | |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 272 | 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 Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 282 | |
Cosmos Nicolaou | 59496fe | 2014-10-14 11:21:05 -0700 | [diff] [blame] | 283 | args := append([]string{command}, flag.Args()...) |
| 284 | return m.fn(os.Stdin, os.Stdout, os.Stderr, osEnvironMap(), args...) |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 285 | } |
| 286 | |
| 287 | func (child *childRegistrar) addSubprocesses(sh *Shell, pattern string) error { |
| 288 | re, err := regexp.Compile(pattern) |
| 289 | if err != nil { |
| 290 | return err |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 291 | } |
Cosmos Nicolaou | 1e78ccc | 2014-10-09 08:10:26 -0700 | [diff] [blame] | 292 | 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. |
| 309 | func AddRegisteredSubprocesses(sh *Shell, pattern string) error { |
| 310 | return child.addSubprocesses(sh, pattern) |
Cosmos Nicolaou | 6261384 | 2014-08-25 21:57:37 -0700 | [diff] [blame] | 311 | } |
Cosmos Nicolaou | addf483 | 2014-09-10 21:36:54 -0700 | [diff] [blame] | 312 | |
| 313 | // WaitForEof returns when a read on its io.Reader parameter returns io.EOF |
| 314 | func WaitForEOF(stdin io.Reader) { |
| 315 | buf := [1024]byte{} |
| 316 | for { |
| 317 | if _, err := stdin.Read(buf[:]); err == io.EOF { |
| 318 | return |
| 319 | } |
| 320 | } |
| 321 | } |