blob: 5ca6f09d8d55c7384c3f5359de7db24aa83c74bc [file] [log] [blame]
package modules
import (
"flag"
"fmt"
"io"
"os"
"os/exec"
"regexp"
"strings"
"sync"
"time"
"veyron.io/veyron/veyron2/vlog"
vexec "veyron.io/veyron/veyron/lib/exec"
)
// execHandle implements both the command and Handle interfaces.
type execHandle struct {
mu sync.Mutex
cmd *exec.Cmd
entryPoint string
handle *vexec.ParentHandle
sh *Shell
stderr *os.File
stdout io.ReadCloser
stdin io.WriteCloser
}
func testFlags() []string {
var fl []string
// pass logging flags to any subprocesses
for fname, fval := range vlog.Log.ExplicitlySetFlags() {
fl = append(fl, "--"+fname+"="+fval)
}
timeout := flag.Lookup("test.timeout")
if timeout == nil {
// not a go test binary
return fl
}
// must be a go test binary
fl = append(fl, "-test.run=TestHelperProcess")
val := timeout.Value.(flag.Getter).Get().(time.Duration)
if val.String() != timeout.DefValue {
// use supplied command value for subprocesses
fl = append(fl, "--test.timeout="+timeout.Value.String())
} else {
// translate default value into 1m for subproccesses
fl = append(fl, "--test.timeout=1m")
}
return fl
}
// IsTestHelperProces returns true if it is called in via
// -run=TestHelperProcess which normally only ever happens for subprocesses
// run from tests.
func IsTestHelperProcess() bool {
runFlag := flag.Lookup("test.run")
if runFlag == nil {
return false
}
return runFlag.Value.String() == "TestHelperProcess"
}
func newExecHandle(entryPoint string) command {
return &execHandle{entryPoint: entryPoint}
}
func (eh *execHandle) Stdout() io.Reader {
eh.mu.Lock()
defer eh.mu.Unlock()
return eh.stdout
}
func (eh *execHandle) Stderr() io.Reader {
eh.mu.Lock()
defer eh.mu.Unlock()
return eh.stderr
}
func (eh *execHandle) Stdin() io.Writer {
eh.mu.Lock()
defer eh.mu.Unlock()
return eh.stdin
}
func (eh *execHandle) CloseStdin() {
eh.mu.Lock()
eh.stdin.Close()
eh.mu.Unlock()
}
// mergeOSEnv returns a slice contained the merged set of environment
// variables from the OS environment and those in this Shell, preferring
// values in the Shell environment over those found in the OS environment.
func (sh *Shell) mergeOSEnvSlice() []string {
merged := sh.mergeOSEnv()
env := []string{}
for k, v := range merged {
env = append(env, k+"="+v)
}
return env
}
func osEnvironMap() map[string]string {
m := make(map[string]string)
for _, osv := range os.Environ() {
if len(osv) == 0 {
continue
}
parts := strings.SplitN(osv, "=", 2)
key := parts[0]
if len(parts) == 2 {
m[key] = parts[1]
} else {
m[key] = ""
}
}
return m
}
func (sh *Shell) mergeOSEnv() map[string]string {
merged := osEnvironMap()
sh.mu.Lock()
for k, v := range sh.env {
merged[k] = v
}
sh.mu.Unlock()
return merged
}
func (eh *execHandle) start(sh *Shell, args ...string) (Handle, error) {
eh.mu.Lock()
defer eh.mu.Unlock()
eh.sh = sh
newargs := append(testFlags(), args...)
cmd := exec.Command(os.Args[0], newargs...)
cmd.Env = append(sh.mergeOSEnvSlice(), eh.entryPoint)
fname := strings.TrimPrefix(eh.entryPoint, ShellEntryPoint+"=")
stderr, err := newLogfile(strings.TrimLeft(fname, "-\n\t "))
if err != nil {
return nil, err
}
cmd.Stderr = stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
handle := vexec.NewParentHandle(cmd)
eh.stdout = stdout
eh.stderr = stderr
eh.stdin = stdin
eh.handle = handle
eh.cmd = cmd
if err := handle.Start(); err != nil {
return nil, err
}
err = handle.WaitForReady(10 * time.Second)
return eh, err
}
func (eh *execHandle) Pid() int {
return eh.cmd.Process.Pid
}
func (eh *execHandle) Shutdown(stdout, stderr io.Writer) error {
eh.mu.Lock()
defer eh.mu.Unlock()
eh.stdin.Close()
logFile := eh.stderr.Name()
defer eh.sh.forget(eh)
defer func() {
os.Remove(logFile)
}()
if stdout == nil && stderr == nil {
return eh.cmd.Wait()
}
// Read from stdin before waiting for the child process to ensure
// that we get to read all of its output.
readTo(eh.stdout, stdout)
procErr := eh.cmd.Wait()
// Stderr is buffered to a file, so we can safely read it after we
// wait for the process.
eh.stderr.Close()
stderrFile, err := os.Open(logFile)
if err != nil {
fmt.Fprintf(os.Stderr, "failed to open %q: %s", logFile, err)
return procErr
}
readTo(stderrFile, stderr)
stderrFile.Close()
return procErr
}
const ShellEntryPoint = "VEYRON_SHELL_HELPER_PROCESS_ENTRY_POINT"
func RegisterChild(name, help string, main Main) {
child.Lock()
defer child.Unlock()
child.mains[name] = &childEntryPoint{main, help}
}
// DispatchInTest will execute the requested subproccess command from within
// a unit test run as a subprocess.
func DispatchInTest() {
if !IsTestHelperProcess() {
return
}
if err := child.dispatch(); err != nil {
fmt.Fprintf(os.Stderr, "Failed: %s\n", err)
os.Exit(1)
}
os.Exit(0)
}
// Dispatch will execute the requested subprocess command from a within a
// a subprocess that is not a unit test.
func Dispatch() error {
if IsTestHelperProcess() {
return fmt.Errorf("use DispatchInTest in unittests")
}
return child.dispatch()
}
func (child *childRegistrar) hasCommand(name string) bool {
child.Lock()
_, present := child.mains[name]
child.Unlock()
return present
}
func (child *childRegistrar) dispatch() error {
ch, _ := vexec.GetChildHandle()
// Only signal that the child is ready or failed if we successfully get
// a child handle. We most likely failed to get a child handle
// because the subprocess was run directly from the command line.
command := os.Getenv(ShellEntryPoint)
if len(command) == 0 {
err := fmt.Errorf("Failed to find entrypoint %q", ShellEntryPoint)
if ch != nil {
ch.SetFailed(err)
}
return err
}
child.Lock()
m := child.mains[command]
child.Unlock()
if m == nil {
err := fmt.Errorf("Shell command %q not registered", command)
if ch != nil {
ch.SetFailed(err)
}
return err
}
if ch != nil {
ch.SetReady()
}
go func(pid int) {
for {
_, err := os.FindProcess(pid)
if err != nil {
fmt.Fprintf(os.Stderr, "Looks like our parent exited: %v", err)
os.Exit(1)
}
time.Sleep(time.Second)
}
}(os.Getppid())
return m.fn(os.Stdin, os.Stdout, os.Stderr, osEnvironMap(), flag.Args()...)
}
func (child *childRegistrar) addSubprocesses(sh *Shell, pattern string) error {
re, err := regexp.Compile(pattern)
if err != nil {
return err
}
child.Lock()
defer child.Unlock()
found := false
for name, subproc := range child.mains {
if re.MatchString(name) {
sh.addSubprocess(name, subproc.help)
found = true
}
}
if !found {
return fmt.Errorf("patterh %q failed to match any registered commands", pattern)
}
return nil
}
// AddRegisteredSubprocesses adds any commands that match the regexp pattern
// to the supplied shell.
func AddRegisteredSubprocesses(sh *Shell, pattern string) error {
return child.addSubprocesses(sh, pattern)
}
// WaitForEof returns when a read on its io.Reader parameter returns io.EOF
func WaitForEOF(stdin io.Reader) {
buf := [1024]byte{}
for {
if _, err := stdin.Read(buf[:]); err == io.EOF {
return
}
}
}