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)
+ }
+}