veyron/mgmt/lib/exec: move this into veyron/lib/exec and merge veyron/lib/config with it.

Change-Id: I8c5b2d1f1a4a9d3c48b6e76e1fe5e43c93416c0d
diff --git a/lib/exec/child.go b/lib/exec/child.go
new file mode 100644
index 0000000..72055e4
--- /dev/null
+++ b/lib/exec/child.go
@@ -0,0 +1,117 @@
+package exec
+
+import (
+	"encoding/binary"
+	"errors"
+	"io"
+	"os"
+	"sync"
+)
+
+var (
+	ErrNoVersion          = errors.New(versionVariable + " environment variable missing")
+	ErrUnsupportedVersion = errors.New("Unsupported version of veyron/lib/exec request by " + versionVariable + " environment variable")
+)
+
+type ChildHandle struct {
+	// Config is passed down from the parent.
+	Config Config
+	// Secret is a secret passed to the child by its parent via a
+	// trusted channel.
+	Secret string
+	// statusPipe is a pipe that is used to notify the parent that the child
+	// process has started successfully. It is meant to be invoked by the
+	// veyron framework to notify the parent that the child process has
+	// successfully started.
+	statusPipe *os.File
+}
+
+var (
+	childHandle    *ChildHandle
+	childHandleErr error
+	once           sync.Once
+)
+
+// fileOffset accounts for the file descriptors that are always passed
+// to the child by the parent: stderr, stdin, stdout, data read, and
+// status write. Any extra files added by the client will follow
+// fileOffset.
+const fileOffset = 5
+
+// GetChildHandle returns a ChildHandle that can be used to signal
+// that the child is 'ready' (by calling SetReady) to its parent or to
+// retrieve data securely passed to this process by its parent. For
+// instance, a secret intended to create a secure communication
+// channels and or authentication.
+//
+// If the child is relying on exec.Cmd.ExtraFiles then its first file
+// descriptor will not be 3, but will be offset by extra files added
+// by the framework. The developer should use the NewExtraFile method
+// to robustly get their extra files with the correct offset applied.
+func GetChildHandle() (*ChildHandle, error) {
+	once.Do(func() {
+		childHandle, childHandleErr = createChildHandle()
+	})
+	return childHandle, childHandleErr
+}
+
+// SetReady writes a 'ready' status to its parent.
+func (c *ChildHandle) SetReady() error {
+	_, err := c.statusPipe.Write([]byte(readyStatus))
+	c.statusPipe.Close()
+	return err
+}
+
+// NewExtraFile creates a new file handle for the i-th file descriptor after
+// discounting stdout, stderr, stdin and the files reserved by the framework for
+// its own purposes.
+func (c *ChildHandle) NewExtraFile(i uintptr, name string) *os.File {
+	return os.NewFile(i+fileOffset, name)
+}
+
+func createChildHandle() (*ChildHandle, error) {
+	switch os.Getenv(versionVariable) {
+	case "":
+		return nil, ErrNoVersion
+	case version1:
+		// TODO(cnicolaou): need to use major.minor.build format for
+		// version #s.
+	default:
+		return nil, ErrUnsupportedVersion
+	}
+	dataPipe := os.NewFile(3, "data_rd")
+	serializedConfig, err := decodeString(dataPipe)
+	if err != nil {
+		return nil, err
+	}
+	cfg := NewConfig()
+	if err := cfg.MergeFrom(serializedConfig); err != nil {
+		return nil, err
+	}
+	secret, err := decodeString(dataPipe)
+	if err != nil {
+		return nil, err
+	}
+	childHandle = &ChildHandle{
+		Config:     cfg,
+		Secret:     secret,
+		statusPipe: os.NewFile(4, "status_wr"),
+	}
+	return childHandle, nil
+}
+
+func decodeString(r io.Reader) (string, error) {
+	var l int64 = 0
+	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
+		return "", err
+	}
+	var data []byte = make([]byte, l)
+	if n, err := r.Read(data); err != nil || int64(n) != l {
+		if err != nil {
+			return "", err
+		} else {
+			return "", errors.New("partial read")
+		}
+	}
+	return string(data), nil
+}
diff --git a/lib/exec/config.go b/lib/exec/config.go
new file mode 100644
index 0000000..1045d34
--- /dev/null
+++ b/lib/exec/config.go
@@ -0,0 +1,90 @@
+package exec
+
+import (
+	"bytes"
+	"strings"
+	"sync"
+
+	"veyron.io/veyron/veyron2/verror"
+	"veyron.io/veyron/veyron2/vom"
+)
+
+var ErrKeyNotFound = verror.NoExistf("config key not found")
+
+// Config defines a simple key-value configuration.  Keys and values are
+// strings, and a key can have exactly one value.  The client is responsible for
+// encoding structured values, or multiple values, in the provided string.
+//
+// Config data can come from several sources:
+// - passed from parent process to child process through pipe;
+// - using environment variables or flags;
+// - via the neighborhood-based config service;
+// - by RPCs using the Config idl;
+// - manually, by calling the Set method.
+//
+// This interface makes no assumptions about the source of the configuration,
+// but provides a unified API for accessing it.
+type Config interface {
+	// Set sets the value for the key.  If the key already exists in the
+	// config, its value is overwritten.
+	Set(key, value string)
+	// Get returns the value for the key.  If the key doesn't exist in the
+	// config, Get returns an error.
+	Get(key string) (string, error)
+	// Serialize serializes the config to a string.
+	Serialize() (string, error)
+	// MergeFrom deserializes config information from a string created using
+	// Serialize(), and merges this information into the config, updating
+	// values for keys that already exist and creating new key-value pairs
+	// for keys that don't.
+	MergeFrom(string) error
+}
+
+type cfg struct {
+	sync.RWMutex
+	m map[string]string
+}
+
+// New creates a new empty config.
+func NewConfig() Config {
+	return &cfg{m: make(map[string]string)}
+}
+
+func (c cfg) Set(key, value string) {
+	c.Lock()
+	defer c.Unlock()
+	c.m[key] = value
+}
+
+func (c cfg) Get(key string) (string, error) {
+	c.RLock()
+	defer c.RUnlock()
+	v, ok := c.m[key]
+	if !ok {
+		return "", ErrKeyNotFound
+	}
+	return v, nil
+}
+
+func (c cfg) Serialize() (string, error) {
+	var buf bytes.Buffer
+	c.RLock()
+	defer c.RUnlock()
+	if err := vom.NewEncoder(&buf).Encode(c.m); err != nil {
+		return "", err
+	}
+	return buf.String(), nil
+}
+
+func (c cfg) MergeFrom(serialized string) error {
+	var newM map[string]string
+	if err := vom.NewDecoder(strings.NewReader(serialized)).Decode(&newM); err != nil {
+		return err
+	}
+	c.Lock()
+	defer c.Unlock()
+	for k, v := range newM {
+		c.m[k] = v
+	}
+	return nil
+}
diff --git a/lib/exec/config_test.go b/lib/exec/config_test.go
new file mode 100644
index 0000000..dbb3475
--- /dev/null
+++ b/lib/exec/config_test.go
@@ -0,0 +1,65 @@
+package exec
+
+import (
+	"testing"
+)
+
+func checkPresent(t *testing.T, c Config, k, wantV string) {
+	if v, err := c.Get(k); err != nil {
+		t.Errorf("Expected value %q for key %q, got error %v instead", wantV, k, err)
+	} else if v != wantV {
+		t.Errorf("Expected value %q for key %q, got %q instead", wantV, k, v)
+	}
+}
+
+func checkAbsent(t *testing.T, c Config, k string) {
+	if v, err := c.Get(k); err != ErrKeyNotFound {
+		t.Errorf("Expected (\"\", %v) for key %q, got (%q, %v) instead", ErrKeyNotFound, k, v, err)
+	}
+}
+
+// TestConfig checks that Set and Get work as expected.
+func TestConfig(t *testing.T) {
+	c := NewConfig()
+	c.Set("foo", "bar")
+	checkPresent(t, c, "foo", "bar")
+	checkAbsent(t, c, "food")
+	c.Set("foo", "baz")
+	checkPresent(t, c, "foo", "baz")
+}
+
+// TestSerialize checks that serializing the config and merging from a
+// serialized config work as expected.
+func TestSerialize(t *testing.T) {
+	c := NewConfig()
+	c.Set("k1", "v1")
+	c.Set("k2", "v2")
+	s, err := c.Serialize()
+	if err != nil {
+		t.Fatalf("Failed to serialize: %v", err)
+	}
+	readC := NewConfig()
+	if err := readC.MergeFrom(s); err != nil {
+		t.Fatalf("Failed to deserialize: %v", err)
+	}
+	checkPresent(t, readC, "k1", "v1")
+	checkPresent(t, readC, "k2", "v2")
+
+	readC.Set("k2", "newv2") // This should be overwritten by the next merge.
+	checkPresent(t, readC, "k2", "newv2")
+	readC.Set("k3", "v3") // This should survive the next merge.
+
+	c.Set("k1", "newv1") // This should overwrite v1 in the next merge.
+	c.Set("k4", "v4")    // This should be added following the next merge.
+	s, err = c.Serialize()
+	if err != nil {
+		t.Fatalf("Failed to serialize: %v", err)
+	}
+	if err := readC.MergeFrom(s); err != nil {
+		t.Fatalf("Failed to deserialize: %v", err)
+	}
+	checkPresent(t, readC, "k1", "newv1")
+	checkPresent(t, readC, "k2", "v2")
+	checkPresent(t, readC, "k3", "v3")
+	checkPresent(t, readC, "k4", "v4")
+}
diff --git a/lib/exec/doc.go b/lib/exec/doc.go
new file mode 100644
index 0000000..953965a
--- /dev/null
+++ b/lib/exec/doc.go
@@ -0,0 +1,13 @@
+// Package exec implements simple process creation and rendezvous, including
+// sharing a secret with, and passing arbitrary configuration to, the newly
+// created process.
+//
+// Once a parent starts a child process it can use WaitForReady to wait
+// for the child to reach its 'Ready' state. Operations are provided to wait
+// for the child to terminate, and to terminate the child cleaning up any state
+// associated with it.
+//
+// A child process uses the GetChildHandle function to complete the initial
+// authentication handshake. The child must call SetReady to indicate that it is
+// fully initialized and ready for whatever purpose it is intended to fulfill.
+package exec
diff --git a/lib/exec/example_test.go b/lib/exec/example_test.go
new file mode 100644
index 0000000..c66adcc
--- /dev/null
+++ b/lib/exec/example_test.go
@@ -0,0 +1,38 @@
+package exec
+
+import (
+	"log"
+	"os/exec"
+	"time"
+)
+
+func ExampleChildHandle() {
+	ch, _ := GetChildHandle()
+	// Initalize the app/service, access the secret shared with the
+	// child by its parent
+	_ = ch.Secret
+	ch.SetReady()
+	// Do work
+}
+
+func ExampleParentHandle() {
+	cmd := exec.Command("/bin/hostname")
+	ph := NewParentHandle(cmd, SecretOpt("secret"))
+
+	// Start the child process.
+	if err := ph.Start(); err != nil {
+		log.Printf("failed to start child: %s\n", err)
+		return
+	}
+
+	// Wait for the child to become ready.
+	if err := ph.WaitForReady(time.Second); err != nil {
+		log.Printf("failed to start child: %s\n", err)
+		return
+	}
+
+	// Wait for the child to exit giving it an hour to do it's work.
+	if err := ph.Wait(time.Hour); err != nil {
+		log.Printf("wait or child failed %s\n", err)
+	}
+}
diff --git a/lib/exec/exec_test.go b/lib/exec/exec_test.go
new file mode 100644
index 0000000..0984521
--- /dev/null
+++ b/lib/exec/exec_test.go
@@ -0,0 +1,522 @@
+package exec_test
+
+import (
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"os/exec"
+	"sync"
+	"testing"
+	"time"
+
+	vexec "veyron.io/veyron/veyron/lib/exec"
+	// Use mock timekeeper to avoid actually sleeping during the test.
+	"veyron.io/veyron/veyron/runtimes/google/testing/timekeeper"
+)
+
+// We always expect there to be exactly three open file descriptors
+// when the test starts out: STDIN, STDOUT, and STDERR. This
+// assumption is tested in init below, and in the rare cases where it
+// is wrong, we bail out.
+const baselineOpenFiles = 3
+
+func init() {
+	if os.Getenv("GO_WANT_HELPER_PROCESS_EXEC") == "1" {
+		return
+	}
+	if want, got := baselineOpenFiles, openFiles(); want != got {
+		message := `Test expected to start with %d open files, found %d instead.
+This can happen if parent process has any open file descriptors,
+e.g. pipes, that are being inherited.`
+		panic(fmt.Errorf(message, want, got))
+	}
+}
+
+// These tests need to run a subprocess and we reuse this same test
+// binary to do so. A fake test 'TestHelperProcess' contains the code
+// we need to run in the child and we simply run this same binary with
+// a test.run= arg that runs just that test. This idea was taken from
+// the tests for os/exec.
+func helperCommand(s ...string) *exec.Cmd {
+	cs := []string{"-test.run=TestHelperProcess", "--"}
+	cs = append(cs, s...)
+	cmd := exec.Command(os.Args[0], cs...)
+	cmd.Env = append([]string{"GO_WANT_HELPER_PROCESS_EXEC=1"}, os.Environ()...)
+	return cmd
+}
+
+func openFiles() int {
+	f, err := os.Open("/dev/null")
+	if err != nil {
+		panic("Failed to open /dev/null\n")
+	}
+	n := f.Fd()
+	f.Close()
+	return int(n)
+}
+
+func clean(t *testing.T, ph ...*vexec.ParentHandle) {
+	for _, p := range ph {
+		alreadyClean := !p.Exists()
+		p.Clean()
+		if !alreadyClean && p.Exists() {
+			t.Errorf("child process left behind even after calling Clean")
+		}
+	}
+	if want, got := baselineOpenFiles, openFiles(); want != got {
+		t.Errorf("Leaking file descriptors: expect %d, got %d", want, got)
+	}
+}
+
+func read(ch chan bool, r io.Reader, m string) {
+	buf := make([]byte, 4096*4)
+	n, err := r.Read(buf)
+	if err != nil {
+		log.Printf("failed to read message: error %s, expecting '%s'\n",
+			err, m)
+		ch <- false
+		return
+	}
+	g := string(buf[:n])
+	b := g == m
+	if !b {
+		log.Printf("read '%s', not '%s'\n", g, m)
+	}
+	ch <- b
+}
+
+func expectMessage(r io.Reader, m string) bool {
+	ch := make(chan bool, 1)
+	go read(ch, r, m)
+	select {
+	case b := <-ch:
+		return b
+	case <-time.After(5 * time.Second):
+		log.Printf("expectMessage: timeout\n")
+		return false
+	}
+	panic("unreachable")
+}
+
+func TestConfigExchange(t *testing.T) {
+	cmd := helperCommand("testConfig")
+	stderr, _ := cmd.StderrPipe()
+	cfg := vexec.NewConfig()
+	cfg.Set("foo", "bar")
+	ph := vexec.NewParentHandle(cmd, vexec.ConfigOpt{cfg})
+	err := ph.Start()
+	if err != nil {
+		t.Fatalf("testConfig: start: %v", err)
+	}
+	serialized, err := cfg.Serialize()
+	if err != nil {
+		t.Fatalf("testConfig: failed to serialize config: %v", err)
+	}
+	if !expectMessage(stderr, serialized) {
+		t.Errorf("unexpected output from child")
+	} else {
+		if err = cmd.Wait(); err != nil {
+			t.Errorf("testConfig: wait: %v", err)
+		}
+	}
+	clean(t, ph)
+}
+
+func TestSecretExchange(t *testing.T) {
+	cmd := helperCommand("testSecret")
+	stderr, _ := cmd.StderrPipe()
+	ph := vexec.NewParentHandle(cmd, vexec.SecretOpt("dummy_secret"))
+	err := ph.Start()
+	if err != nil {
+		t.Fatalf("testSecretTest: start: %v", err)
+	}
+	if !expectMessage(stderr, "dummy_secret") {
+		t.Errorf("unexpected output from child")
+	} else {
+		if err = cmd.Wait(); err != nil {
+			t.Errorf("testSecretTest: wait: %v", err)
+		}
+	}
+	clean(t, ph)
+}
+
+func TestNoVersion(t *testing.T) {
+	// Make sure that Init correctly tests for the presence of VEXEC_VERSION
+	_, err := vexec.GetChildHandle()
+	if err != vexec.ErrNoVersion {
+		t.Errorf("Should be missing Version")
+	}
+}
+
+func waitForReady(t *testing.T, cmd *exec.Cmd, name string, delay int, ph *vexec.ParentHandle) error {
+	err := ph.Start()
+	if err != nil {
+		t.Fatalf("%s: start: %v", name, err)
+		return err
+	}
+	return ph.WaitForReady(time.Duration(delay) * time.Second)
+}
+
+func readyHelperCmd(t *testing.T, cmd *exec.Cmd, name, result string) *vexec.ParentHandle {
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		t.Fatalf("%s: failed to get stderr pipe\n", name)
+	}
+	ph := vexec.NewParentHandle(cmd)
+	if err := waitForReady(t, cmd, name, 4, ph); err != nil {
+		t.Errorf("%s: WaitForReady: %v (%v)", name, err, ph)
+		return nil
+	}
+	if !expectMessage(stderr, result) {
+		t.Errorf("%s: failed to read '%s' from child\n", name, result)
+	}
+	return ph
+}
+
+func readyHelper(t *testing.T, name, test, result string) *vexec.ParentHandle {
+	cmd := helperCommand(test)
+	return readyHelperCmd(t, cmd, name, result)
+}
+
+func testMany(t *testing.T, name, test, result string) []*vexec.ParentHandle {
+	nprocs := 10
+	ph := make([]*vexec.ParentHandle, nprocs)
+	cmd := make([]*exec.Cmd, nprocs)
+	stderr := make([]io.ReadCloser, nprocs)
+	controlReaders := make([]io.ReadCloser, nprocs)
+	var done sync.WaitGroup
+	for i := 0; i < nprocs; i++ {
+		cmd[i] = helperCommand(test)
+		// The control pipe is used to signal the child when to wake up.
+		controlRead, controlWrite, err := os.Pipe()
+		if err != nil {
+			t.Errorf("Failed to create control pipe: %v", err)
+			return nil
+		}
+		controlReaders[i] = controlRead
+		cmd[i].ExtraFiles = append(cmd[i].ExtraFiles, controlRead)
+		stderr[i], _ = cmd[i].StderrPipe()
+		tk := timekeeper.NewManualTime()
+		ph[i] = vexec.NewParentHandle(cmd[i], vexec.TimeKeeperOpt{tk})
+		done.Add(1)
+		go func() {
+			// For simulated slow children, wait until the parent
+			// starts waiting, and then wake up the child.
+			if test == "testReadySlow" {
+				<-tk.Requests()
+				tk.AdvanceTime(3 * time.Second)
+				if _, err = controlWrite.Write([]byte("wake")); err != nil {
+					t.Errorf("Failed to write to control pipe: %v", err)
+				}
+			}
+			controlWrite.Close()
+			done.Done()
+		}()
+		if err := ph[i].Start(); err != nil {
+			t.Errorf("%s: Failed to start child %d: %s\n", name, i, err)
+		}
+	}
+	for i := 0; i < nprocs; i++ {
+		if err := ph[i].WaitForReady(5 * time.Second); err != nil {
+			t.Errorf("%s: Failed to wait for child %d: %s\n", name, i, err)
+		}
+		controlReaders[i].Close()
+	}
+	for i := 0; i < nprocs; i++ {
+		if !expectMessage(stderr[i], result) {
+			t.Errorf("%s: Failed to read message from child %d\n", name, i)
+		}
+	}
+	done.Wait()
+	return ph
+}
+
+func TestToReadyMany(t *testing.T) {
+	clean(t, testMany(t, "TestToReadyMany", "testReady", ".")...)
+}
+
+func TestToReadySlowMany(t *testing.T) {
+	clean(t, testMany(t, "TestToReadySlowMany", "testReadySlow", "..")...)
+}
+
+func TestToReady(t *testing.T) {
+	ph := readyHelper(t, "TestToReady", "testReady", ".")
+	clean(t, ph)
+}
+
+func TestNeverReady(t *testing.T) {
+	name := "testNeverReady"
+	result := "never ready"
+	cmd := helperCommand(name)
+	stderr, _ := cmd.StderrPipe()
+	ph := vexec.NewParentHandle(cmd)
+	err := waitForReady(t, cmd, name, 1, ph)
+	if err != vexec.ErrTimeout {
+		t.Errorf("Failed to get timeout: got %v\n", err)
+	} else {
+		// block waiting for error from child
+		if !expectMessage(stderr, result) {
+			t.Errorf("%s: failed to read '%s' from child\n", name, result)
+		}
+	}
+	clean(t, ph)
+}
+
+func TestTooSlowToReady(t *testing.T) {
+	name := "testTooSlowToReady"
+	result := "write status_wr: broken pipe"
+	cmd := helperCommand(name)
+	// The control pipe is used to signal the child when to wake up.
+	controlRead, controlWrite, err := os.Pipe()
+	if err != nil {
+		t.Errorf("Failed to create control pipe: %v", err)
+		return
+	}
+	cmd.ExtraFiles = append(cmd.ExtraFiles, controlRead)
+	stderr, _ := cmd.StderrPipe()
+	tk := timekeeper.NewManualTime()
+	ph := vexec.NewParentHandle(cmd, vexec.TimeKeeperOpt{tk})
+	defer clean(t, ph)
+	defer controlWrite.Close()
+	defer controlRead.Close()
+	// Wait for the parent to start waiting, then simulate a timeout.
+	go func() {
+		toWait := <-tk.Requests()
+		tk.AdvanceTime(toWait)
+	}()
+	err = waitForReady(t, cmd, name, 1, ph)
+	if err != vexec.ErrTimeout {
+		t.Errorf("Failed to get timeout: got %v\n", err)
+	} else {
+		// After the parent timed out, wake up the child and let it
+		// proceed.
+		if _, err = controlWrite.Write([]byte("wake")); err != nil {
+			t.Errorf("Failed to write to control pipe: %v", err)
+		} else {
+			// block waiting for error from child
+			if !expectMessage(stderr, result) {
+				t.Errorf("%s: failed to read '%s' from child\n", name, result)
+			}
+		}
+	}
+}
+
+func TestToReadySlow(t *testing.T) {
+	name := "TestToReadySlow"
+	cmd := helperCommand("testReadySlow")
+	// The control pipe is used to signal the child when to wake up.
+	controlRead, controlWrite, err := os.Pipe()
+	if err != nil {
+		t.Errorf("Failed to create control pipe: %v", err)
+		return
+	}
+	cmd.ExtraFiles = append(cmd.ExtraFiles, controlRead)
+	stderr, err := cmd.StderrPipe()
+	if err != nil {
+		t.Fatalf("%s: failed to get stderr pipe", name)
+	}
+	tk := timekeeper.NewManualTime()
+	ph := vexec.NewParentHandle(cmd, vexec.TimeKeeperOpt{tk})
+	defer clean(t, ph)
+	defer controlWrite.Close()
+	defer controlRead.Close()
+	// Wait for the parent to start waiting, simulate a short wait (but not
+	// enough to timeout the parent), then wake up the child.
+	go func() {
+		<-tk.Requests()
+		tk.AdvanceTime(2 * time.Second)
+		if _, err = controlWrite.Write([]byte("wake")); err != nil {
+			t.Errorf("Failed to write to control pipe: %v", err)
+		}
+	}()
+	if err := waitForReady(t, cmd, name, 4, ph); err != nil {
+		t.Errorf("%s: WaitForReady: %v (%v)", name, err, ph)
+		return
+	}
+	// After the child has replied, simulate a timeout on the server by
+	// advacing the time more; at this point, however, the timeout should no
+	// longer occur since the child already replied.
+	tk.AdvanceTime(2 * time.Second)
+	if result := ".."; !expectMessage(stderr, result) {
+		t.Errorf("%s: failed to read '%s' from child\n", name, result)
+	}
+}
+
+func TestToCompletion(t *testing.T) {
+	ph := readyHelper(t, "TestToCompletion", "testSuccess", "...ok")
+	e := ph.Wait(time.Second)
+	if e != nil {
+		t.Errorf("Wait failed: err %s\n", e)
+	}
+	clean(t, ph)
+}
+
+func TestToCompletionError(t *testing.T) {
+	ph := readyHelper(t, "TestToCompletionError", "testError", "...err")
+	e := ph.Wait(time.Second)
+	if e == nil {
+		t.Errorf("Wait failed: err %s\n", e)
+	}
+	clean(t, ph)
+}
+
+func TestExtraFiles(t *testing.T) {
+	cmd := helperCommand("testExtraFiles")
+	rd, wr, err := os.Pipe()
+	if err != nil {
+		t.Fatalf("Failed to create pipe: %s\n", err)
+	}
+	cmd.ExtraFiles = append(cmd.ExtraFiles, rd)
+	msg := "hello there..."
+	fmt.Fprintf(wr, msg)
+	ph := readyHelperCmd(t, cmd, "TestExtraFiles", "child: "+msg)
+	if ph == nil {
+		t.Fatalf("Failed to get vexec.ParentHandle\n")
+	}
+	e := ph.Wait(1 * time.Second)
+	if e != nil {
+		t.Errorf("Wait failed: err %s\n", e)
+	}
+	rd.Close()
+	wr.Close()
+	clean(t, ph)
+}
+
+// TestHelperProcess isn't a real test; it's used as a helper process
+// for the other tests.
+func TestHelperProcess(*testing.T) {
+	// Return immediately if this is not run as the child helper
+	// for the other tests.
+	if os.Getenv("GO_WANT_HELPER_PROCESS_EXEC") != "1" {
+		return
+	}
+	defer os.Exit(0)
+
+	// Write errors to stderr or using log. since the parent
+	// process is reading stderr.
+	args := os.Args
+	for len(args) > 0 {
+		if args[0] == "--" {
+			args = args[1:]
+			break
+		}
+		args = args[1:]
+	}
+
+	if len(args) == 0 {
+		log.Fatal(os.Stderr, "No command\n")
+	}
+
+	cmd, args := args[0], args[1:]
+
+	switch cmd {
+	case "testNeverReady":
+		_, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatal(os.Stderr, "%s\n", err)
+		}
+		fmt.Fprintf(os.Stderr, "never ready")
+	case "testTooSlowToReady":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatal(os.Stderr, "%s\n", err)
+		}
+		// Wait for the parent to tell us when it's ok to proceed.
+		controlPipe := ch.NewExtraFile(0, "control_rd")
+		for {
+			buf := make([]byte, 100)
+			n, err := controlPipe.Read(buf)
+			if err != nil {
+				log.Fatal(os.Stderr, "%s", err)
+			}
+			if n > 0 {
+				break
+			}
+		}
+		// SetReady should return an error since the parent has
+		// timed out on us and we'd be writing to a closed pipe.
+		if err := ch.SetReady(); err != nil {
+			fmt.Fprintf(os.Stderr, "%s", err)
+		} else {
+			fmt.Fprintf(os.Stderr, "didn't get the expected error")
+		}
+		os.Exit(0)
+	case "testReady":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatal(os.Stderr, "%s", err)
+		}
+		ch.SetReady()
+		fmt.Fprintf(os.Stderr, ".")
+	case "testReadySlow":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatal(os.Stderr, "%s", err)
+		}
+		// Wait for the parent to tell us when it's ok to proceed.
+		controlPipe := ch.NewExtraFile(0, "control_rd")
+		for {
+			buf := make([]byte, 100)
+			n, err := controlPipe.Read(buf)
+			if err != nil {
+				log.Fatal(os.Stderr, "%s", err)
+			}
+			if n > 0 {
+				break
+			}
+		}
+		ch.SetReady()
+		fmt.Fprintf(os.Stderr, "..")
+	case "testSuccess", "testError":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatal(os.Stderr, "%s\n", err)
+		}
+		ch.SetReady()
+		rc := make(chan int)
+		go func() {
+			if cmd == "testError" {
+				fmt.Fprintf(os.Stderr, "...err")
+				rc <- 1
+			} else {
+				fmt.Fprintf(os.Stderr, "...ok")
+				rc <- 0
+			}
+		}()
+		r := <-rc
+		os.Exit(r)
+	case "testConfig":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatalf("%v", err)
+		} else {
+			serialized, err := ch.Config.Serialize()
+			if err != nil {
+				log.Fatalf("%v", err)
+			}
+			fmt.Fprintf(os.Stderr, "%s", serialized)
+		}
+	case "testSecret":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatalf("%s", err)
+		} else {
+			fmt.Fprintf(os.Stderr, "%s", ch.Secret)
+		}
+	case "testExtraFiles":
+		ch, err := vexec.GetChildHandle()
+		if err != nil {
+			log.Fatal("error.... %s\n", err)
+		}
+		err = ch.SetReady()
+		rd := ch.NewExtraFile(0, "read")
+		buf := make([]byte, 1024)
+		if n, err := rd.Read(buf); err != nil {
+			fmt.Fprintf(os.Stderr, "child: error %s\n", err)
+		} else {
+			fmt.Fprintf(os.Stderr, "child: %s", string(buf[:n]))
+		}
+		os.Exit(0)
+	}
+}
diff --git a/lib/exec/parent.go b/lib/exec/parent.go
new file mode 100644
index 0000000..ad93af1
--- /dev/null
+++ b/lib/exec/parent.go
@@ -0,0 +1,249 @@
+package exec
+
+import (
+	"encoding/binary"
+	"errors"
+	"io"
+	"os"
+	"os/exec"
+	"syscall"
+	"time"
+
+	// TODO(cnicolaou): move timekeeper out of runtimes
+	"veyron.io/veyron/veyron/runtimes/google/lib/timekeeper"
+
+	"veyron.io/veyron/veyron2/vlog"
+)
+
+var (
+	ErrAuthTimeout    = errors.New("timout in auth handshake")
+	ErrTimeout        = errors.New("timeout waiting for child")
+	ErrSecretTooLarge = errors.New("secret is too large")
+)
+
+// A ParentHandle is the Parent process' means of managing a single child.
+type ParentHandle struct {
+	c           *exec.Cmd
+	config      Config
+	secret      string
+	statusRead  *os.File
+	statusWrite *os.File
+	tk          timekeeper.TimeKeeper
+}
+
+// ParentHandleOpt is an option for NewParentHandle.
+type ParentHandleOpt interface {
+	// ExecParentHandleOpt is a signature 'dummy' method for the
+	// interface.
+	ExecParentHandleOpt()
+}
+
+// ConfigOpt can be used to seed the parent handle with a
+// config to be passed to the child.
+type ConfigOpt struct {
+	Config
+}
+
+// ExecParentHandleOpt makes ConfigOpt an instance of
+// ParentHandleOpt.
+func (ConfigOpt) ExecParentHandleOpt() {}
+
+// SecretOpt can be used to seed the parent handle with a custom secret.
+type SecretOpt string
+
+// ExecParentHandleOpt makes SecretOpt an instance of ParentHandleOpt.
+func (SecretOpt) ExecParentHandleOpt() {}
+
+// TimeKeeperOpt can be used to seed the parent handle with a custom timekeeper.
+type TimeKeeperOpt struct {
+	timekeeper.TimeKeeper
+}
+
+// ExecParentHandleOpt makes TimeKeeperOpt an instance of ParentHandleOpt.
+func (TimeKeeperOpt) ExecParentHandleOpt() {}
+
+// NewParentHandle creates a ParentHandle for the child process represented by
+// an instance of exec.Cmd.
+func NewParentHandle(c *exec.Cmd, opts ...ParentHandleOpt) *ParentHandle {
+	c.Env = append(c.Env, versionVariable+"="+version1)
+	cfg, secret := NewConfig(), ""
+	tk := timekeeper.RealTime()
+	for _, opt := range opts {
+		switch v := opt.(type) {
+		case ConfigOpt:
+			cfg = v
+		case SecretOpt:
+			secret = string(v)
+		case TimeKeeperOpt:
+			tk = v
+		default:
+			vlog.Errorf("Unrecognized parent option: %v", v)
+		}
+	}
+	return &ParentHandle{
+		c:      c,
+		config: cfg,
+		secret: secret,
+		tk:     tk,
+	}
+}
+
+// Start starts the child process, sharing a secret with it and
+// setting up a communication channel over which to read its status.
+func (p *ParentHandle) Start() error {
+	// Create anonymous pipe for communicating data between the child
+	// and the parent.
+	dataRead, dataWrite, err := os.Pipe()
+	if err != nil {
+		return err
+	}
+	defer dataRead.Close()
+	defer dataWrite.Close()
+	statusRead, statusWrite, err := os.Pipe()
+	if err != nil {
+		return err
+	}
+	p.statusRead = statusRead
+	p.statusWrite = statusWrite
+	// Add the parent-child pipes to cmd.ExtraFiles, offsetting all
+	// existing file descriptors accordingly.
+	extraFiles := make([]*os.File, len(p.c.ExtraFiles)+2)
+	extraFiles[0] = dataRead
+	extraFiles[1] = statusWrite
+	for i, _ := range p.c.ExtraFiles {
+		extraFiles[i+2] = p.c.ExtraFiles[i]
+	}
+	p.c.ExtraFiles = extraFiles
+	// Start the child process.
+	if err := p.c.Start(); err != nil {
+		p.statusWrite.Close()
+		p.statusRead.Close()
+		return err
+	}
+	// Pass data to the child using a pipe.
+	serializedConfig, err := p.config.Serialize()
+	if err != nil {
+		return err
+	}
+	if err := encodeString(dataWrite, serializedConfig); err != nil {
+		p.statusWrite.Close()
+		p.statusRead.Close()
+		return err
+	}
+	if err := encodeString(dataWrite, p.secret); err != nil {
+		p.statusWrite.Close()
+		p.statusRead.Close()
+		return err
+	}
+	return nil
+}
+
+func waitForStatus(c chan string, e chan error, r *os.File) {
+	buf := make([]byte, 100)
+	n, err := r.Read(buf)
+	if err != nil {
+		e <- err
+	} else {
+		c <- string(buf[:n])
+	}
+	r.Close()
+	close(c)
+	close(e)
+}
+
+// WaitForReady will wait for the child process to become ready.
+func (p *ParentHandle) WaitForReady(timeout time.Duration) error {
+	defer p.statusWrite.Close()
+	c := make(chan string, 1)
+	e := make(chan error, 1)
+	go waitForStatus(c, e, p.statusRead)
+	for {
+		select {
+		case err := <-e:
+			return err
+		case st := <-c:
+			if st == readyStatus {
+				return nil
+			}
+		case <-p.tk.After(timeout):
+			// Make sure that the read in waitForStatus
+			// returns now.
+			p.statusWrite.Write([]byte("quit"))
+			return ErrTimeout
+		}
+	}
+	panic("unreachable")
+}
+
+// Wait will wait for the child process to terminate of its own accord.
+// It returns nil if the process exited cleanly with an exit status of 0,
+// any other exit code or error will result in an appropriate error return
+func (p *ParentHandle) Wait(timeout time.Duration) error {
+	c := make(chan error, 1)
+	go func() {
+		c <- p.c.Wait()
+		close(c)
+	}()
+	// If timeout is zero time.After will panic; we handle zero specially
+	// to mean infinite timeout.
+	if timeout > 0 {
+		select {
+		case <-p.tk.After(timeout):
+			return ErrTimeout
+		case err := <-c:
+			return err
+		}
+	} else {
+		return <-c
+	}
+	panic("unreachable")
+}
+
+// Pid returns the pid of the child, 0 if the child process doesn't exist
+func (p *ParentHandle) Pid() int {
+	if p.c.Process != nil {
+		return p.c.Process.Pid
+	}
+	return 0
+}
+
+// Exists returns true if the child process exists and can be signal'ed
+func (p *ParentHandle) Exists() bool {
+	if p.c.Process != nil {
+		return syscall.Kill(p.c.Process.Pid, 0) == nil
+	}
+	return false
+}
+
+// Kill kills the child process.
+func (p *ParentHandle) Kill() error {
+	return p.c.Process.Kill()
+}
+
+// Signal sends the given signal to the child process.
+func (p *ParentHandle) Signal(sig syscall.Signal) error {
+	return syscall.Kill(p.c.Process.Pid, sig)
+}
+
+// Clean will clean up state, including killing the child process.
+func (p *ParentHandle) Clean() error {
+	if err := p.Kill(); err != nil {
+		return err
+	}
+	return p.c.Wait()
+}
+
+func encodeString(w io.Writer, data string) error {
+	l := len(data)
+	if err := binary.Write(w, binary.BigEndian, int64(l)); err != nil {
+		return err
+	}
+	if n, err := w.Write([]byte(data)); err != nil || n != l {
+		if err != nil {
+			return err
+		} else {
+			return errors.New("partial write")
+		}
+	}
+	return nil
+}
diff --git a/lib/exec/shared.go b/lib/exec/shared.go
new file mode 100644
index 0000000..936d2bf
--- /dev/null
+++ b/lib/exec/shared.go
@@ -0,0 +1,8 @@
+package exec
+
+const (
+	version1        = "1.0.0"
+	readyStatus     = "ready"
+	initStatus      = "init"
+	versionVariable = "VEYRON_EXEC_VERSION"
+)
diff --git a/lib/exec/util.go b/lib/exec/util.go
new file mode 100644
index 0000000..8ecbeb2
--- /dev/null
+++ b/lib/exec/util.go
@@ -0,0 +1,30 @@
+package exec
+
+import (
+	"errors"
+	"strings"
+)
+
+// Getenv retrieves the value of the given variable from the given
+// slice of environment variable assignments.
+func Getenv(env []string, name string) (string, error) {
+	for _, v := range env {
+		if strings.HasPrefix(v, name+"=") {
+			return strings.TrimPrefix(v, name+"="), nil
+		}
+	}
+	return "", errors.New("not found")
+}
+
+// Setenv updates / adds the value assignment for the given variable
+// in the given slice of environment variable assigments.
+func Setenv(env []string, name, value string) []string {
+	newValue := name + "=" + value
+	for i, v := range env {
+		if strings.HasPrefix(v, name+"=") {
+			env[i] = newValue
+			return env
+		}
+	}
+	return append(env, newValue)
+}
diff --git a/lib/exec/util_test.go b/lib/exec/util_test.go
new file mode 100644
index 0000000..a247524
--- /dev/null
+++ b/lib/exec/util_test.go
@@ -0,0 +1,34 @@
+package exec
+
+import (
+	"testing"
+)
+
+func TestEnv(t *testing.T) {
+	env := make([]string, 0)
+	env = Setenv(env, "NAME", "VALUE1")
+	if expected, got := 1, len(env); expected != got {
+		t.Fatalf("Unexpected length of environment variable slice: expected %d, got %d", expected, got)
+	}
+	if expected, got := "NAME=VALUE1", env[0]; expected != got {
+		t.Fatalf("Unexpected element in the environment variable slice: expected %d, got %d", expected, got)
+	}
+	env = Setenv(env, "NAME", "VALUE2")
+	if expected, got := 1, len(env); expected != got {
+		t.Fatalf("Unexpected length of environment variable slice: expected %d, got %d", expected, got)
+	}
+	if expected, got := "NAME=VALUE2", env[0]; expected != got {
+		t.Fatalf("Unexpected element in the environment variable slice: expected %d, got %d", expected, got)
+	}
+	value, err := Getenv(env, "NAME")
+	if err != nil {
+		t.Fatalf("Unexpected error when looking up environment variable value: %v", err)
+	}
+	if expected, got := "VALUE2", value; expected != got {
+		t.Fatalf("Unexpected value of an environment variable: expected %d, got %d", expected, got)
+	}
+	value, err = Getenv(env, "NONAME")
+	if err == nil {
+		t.Fatalf("Expected error when looking up environment variable value, got none", value)
+	}
+}