veyron/lib/modules: introduce sh.StartExternalCommand

This CL splits up the shell.Start function into shell.Start and
shell.StartExternalCommand. This allows users (such as the e2e tests) to
run commands that have not been pre-registered. The shell will not
attempt to run these commands in the envelope.

Change-Id: I920b6172e6750aa3e6de4c031d5dd5e3fef95936
diff --git a/lib/modules/core/core.go b/lib/modules/core/core.go
index c2919fa..c937287 100644
--- a/lib/modules/core/core.go
+++ b/lib/modules/core/core.go
@@ -47,9 +47,6 @@
 // echoClient <name> <text>
 //    invoke <name>.Echo(<text>)
 //
-// exec <command> [args...]
-//    executes the given command with the given arguments
-//
 // proxyd <names>...
 //    runs a proxy server
 package core
@@ -67,5 +64,4 @@
 	ProxyServerCommand   = "proxyd"
 	WSPRCommand          = "wsprd"
 	TestIdentitydCommand = "test_identityd"
-	ExecCommand          = "exec"
 )
diff --git a/lib/modules/core/core_test.go b/lib/modules/core/core_test.go
index e5071c2..15a7c6a 100644
--- a/lib/modules/core/core_test.go
+++ b/lib/modules/core/core_test.go
@@ -202,7 +202,7 @@
 func TestExec(t *testing.T) {
 	sh, cleanup := newShell(t)
 	defer cleanup()
-	h, err := sh.Start(core.ExecCommand, nil, []string{"/bin/sh", "-c", "echo hello world"}...)
+	h, err := sh.StartExternalCommand(nil, []string{"/bin/sh", "-c", "echo hello world"}...)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -218,7 +218,7 @@
 func TestExecWithEnv(t *testing.T) {
 	sh, cleanup := newShell(t)
 	defer cleanup()
-	h, err := sh.Start(core.ExecCommand, []string{"BLAH=hello world"}, "/bin/sh", "-c", "printenv BLAH")
+	h, err := sh.StartExternalCommand([]string{"BLAH=hello world"}, "/bin/sh", "-c", "printenv BLAH")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/lib/modules/core/exec.go b/lib/modules/core/exec.go
deleted file mode 100644
index be41d0d..0000000
--- a/lib/modules/core/exec.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package core
-
-import (
-	"io"
-	"syscall"
-
-	"v.io/core/veyron/lib/modules"
-)
-
-func init() {
-	modules.RegisterChild(ExecCommand, "", execCommand)
-}
-
-func execCommand(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	envSlice := []string{}
-	for key, value := range env {
-		envSlice = append(envSlice, key+"="+value)
-	}
-	return syscall.Exec(args[1], args[1:], envSlice)
-}
diff --git a/lib/modules/exec.go b/lib/modules/exec.go
index b7e77c0..547ac0d 100644
--- a/lib/modules/exec.go
+++ b/lib/modules/exec.go
@@ -70,6 +70,10 @@
 	return &execHandle{name: name, entryPoint: shellEntryPoint + "=" + name, procErrCh: make(chan error, 1)}
 }
 
+func newExecHandleForExternalCommand(name string) command {
+	return &execHandle{name: name, procErrCh: make(chan error, 1)}
+}
+
 func (eh *execHandle) Stdout() io.Reader {
 	eh.mu.Lock()
 	defer eh.mu.Unlock()
@@ -114,8 +118,16 @@
 	eh.mu.Lock()
 	defer eh.mu.Unlock()
 	eh.sh = sh
-	newargs, newenv := eh.envelope(sh, env, args[1:]...)
-	cmd := exec.Command(os.Args[0], newargs[1:]...)
+	cmdPath := args[0]
+	newargs, newenv := args, env
+
+	// If an entry point is specified, use the envelope execution environment.
+	if eh.entryPoint != "" {
+		cmdPath = os.Args[0]
+		newargs, newenv = eh.envelope(sh, env, args[1:]...)
+	}
+
+	cmd := exec.Command(cmdPath, newargs[1:]...)
 	cmd.Env = newenv
 	stderr, err := newLogfile("stderr", eh.name)
 	if err != nil {
@@ -160,7 +172,6 @@
 		return nil, err
 	}
 	vlog.VI(1).Infof("Started: %q, pid %d", eh.name, cmd.Process.Pid)
-	err = handle.WaitForReady(sh.startTimeout)
 	go func() {
 		eh.procErrCh <- eh.handle.Wait(0)
 		// It's now safe to close eh.stdout, since Wait only returns
@@ -170,7 +181,7 @@
 		eh.stdout.Close()
 	}()
 
-	return eh, err
+	return eh, nil
 }
 
 func (eh *execHandle) Pid() int {
@@ -215,3 +226,7 @@
 
 	return procErr
 }
+
+func (eh *execHandle) WaitForReady(timeout time.Duration) error {
+	return eh.handle.WaitForReady(timeout)
+}
diff --git a/lib/modules/func.go b/lib/modules/func.go
index e7372e6..5c5bc4c 100644
--- a/lib/modules/func.go
+++ b/lib/modules/func.go
@@ -5,6 +5,7 @@
 	"io"
 	"os"
 	"sync"
+	"time"
 
 	"v.io/core/veyron2/vlog"
 )
@@ -143,3 +144,7 @@
 	fh.mu.Unlock()
 	return funcErr
 }
+
+func (fh *functionHandle) WaitForReady(time.Duration) error {
+	return nil
+}
diff --git a/lib/modules/registry.go b/lib/modules/registry.go
index 7b01a1e..8331f9c 100644
--- a/lib/modules/registry.go
+++ b/lib/modules/registry.go
@@ -39,7 +39,7 @@
 	return r.cmds[name]
 }
 
-// RegisterChild adds a new command to the registery that will be run
+// RegisterChild adds a new command to the registry that will be run
 // as a subprocess. It must be called before Dispatch or DispatchInTest is
 // called, typically from an init function.
 func RegisterChild(name, help string, main Main) {
diff --git a/lib/modules/shell.go b/lib/modules/shell.go
index c80aae7..c6cd898 100644
--- a/lib/modules/shell.go
+++ b/lib/modules/shell.go
@@ -37,6 +37,7 @@
 package modules
 
 import (
+	"errors"
 	"fmt"
 	"io"
 	"io/ioutil"
@@ -213,6 +214,14 @@
 	return registry.help(command)
 }
 
+func (sh *Shell) StartExternalCommand(env []string, args ...string) (Handle, error) {
+	if len(args) == 0 {
+		return nil, errors.New("no arguments specified to StartExternalCommand")
+	}
+	c := newExecHandleForExternalCommand(args[0])
+	return sh.startCommand(c, env, args...)
+}
+
 // Start starts the specified command, it returns a Handle which can be
 // used for interacting with that command.
 //
@@ -236,6 +245,26 @@
 // Commands must have already been registered using RegisterFunction
 // or RegisterChild.
 func (sh *Shell) Start(name string, env []string, args ...string) (Handle, error) {
+	cmd := registry.getCommand(name)
+	if cmd == nil {
+		return nil, fmt.Errorf("%s: not registered", name)
+	}
+	expanded := append([]string{name}, sh.expand(args...)...)
+	c := cmd.factory()
+	h, err := sh.startCommand(c, env, expanded...)
+	if err != nil {
+		// If the error is a timeout, then h can be used to recover
+		// any output from the process.
+		return h, err
+	}
+
+	if err := h.WaitForReady(sh.waitTimeout); err != nil {
+		return h, err
+	}
+	return h, nil
+}
+
+func (sh *Shell) startCommand(c command, env []string, args ...string) (Handle, error) {
 	cenv, err := sh.setupCommandEnv(env)
 	if err != nil {
 		return nil, err
@@ -244,16 +273,10 @@
 	if err != nil {
 		return nil, err
 	}
-	cmd := registry.getCommand(name)
-	if cmd == nil {
-		return nil, fmt.Errorf("%s: not registered", name)
-	}
-	expanded := append([]string{name}, sh.expand(args...)...)
-	h, err := cmd.factory().start(sh, p, cenv, expanded...)
+
+	h, err := c.start(sh, p, cenv, args...)
 	if err != nil {
-		// If the error is a timeout, then h can be used to recover
-		// any output from the process.
-		return h, err
+		return nil, err
 	}
 	sh.mu.Lock()
 	sh.handles[h] = struct{}{}
@@ -459,6 +482,11 @@
 
 	// Pid returns the pid of the process running the command
 	Pid() int
+
+	// WaitForReady waits until the child process signals to us that it is
+	// ready. If this does not occur within the given timeout duration, a
+	// timeout error is returned.
+	WaitForReady(timeout time.Duration) error
 }
 
 // command is used to abstract the implementations of inprocess and subprocess
diff --git a/lib/testutil/integration/util.go b/lib/testutil/integration/util.go
index 46a2a6b..208fad1 100644
--- a/lib/testutil/integration/util.go
+++ b/lib/testutil/integration/util.go
@@ -262,9 +262,9 @@
 		locationString = fmt.Sprintf("(requested at %s:%d) ", filepath.Base(file), line)
 	}
 	b.env.t.Logf("%sstarting %s %s", locationString, b.Path(), strings.Join(args, " "))
-	handle, err := b.env.shell.Start("exec", b.envVars, append([]string{b.Path()}, args...)...)
+	handle, err := b.env.shell.StartExternalCommand(b.envVars, append([]string{b.Path()}, args...)...)
 	if err != nil {
-		b.env.t.Fatalf("Start(%v, %v) failed: %v", b.Path(), strings.Join(args, ", "), err)
+		b.env.t.Fatalf("StartExternalCommand(%v, %v) failed: %v", b.Path(), strings.Join(args, ", "), err)
 	}
 	b.env.t.Logf("started PID %d\n", handle.Pid())
 	return &integrationTestBinaryInvocation{