veyron/lib/exec: adding support for passing callback endpoint and
nonce to the child process

Change-Id: Ia161ac70bac47f4f3d06de792a21391825ea2dc3
diff --git a/lib/exec/child.go b/lib/exec/child.go
index 676beb2..f200751 100644
--- a/lib/exec/child.go
+++ b/lib/exec/child.go
@@ -1,54 +1,81 @@
 package exec
 
 import (
+	"encoding/binary"
 	"errors"
+	"io"
 	"os"
 )
 
 var (
 	ErrNoVersion          = errors.New(versionVariable + " environment variable missing")
-	ErrUnsupportedVersion = errors.New("Unsupported version of veyron/runtimes/google/lib/exec request by " + versionVariable + " environment variable")
+	ErrUnsupportedVersion = errors.New("Unsupported version of veyron/lib/exec request by " + versionVariable + " environment variable")
 )
 
 type ChildHandle struct {
-	// A Secret passed to the child by its parent via a trusted channel
-	Secret     string
+	// Endpoint is a callback endpoint that can be use to notify the
+	// parent that the child has started up successfully via the
+	// Callback() RPC.
+	Endpoint string
+	// ID is a callback ID that can be used by a parent to identify this
+	// child when the child invokes the Callback() RPC using the
+	// callback endpoint.
+	ID string
+	// 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. Unlike the Callback()
+	// RPC, which is to be invoked by the application to notify the
+	// parent that the application is "ready", the statusPipe is to be
+	// invoked by the veyron framework to notify the parent that the
+	// child process has successfully started.
 	statusPipe *os.File
 }
 
-// fileOffset accounts for the file descriptors that are always passed to the
-// child by the parent: stderr, stdin, stdout, token read, and status write.
-// Any extra files added by the client will follow fileOffset.
+// 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
 
 // NewChildHandle creates a new ChildHandle that can be used to signal
 // that the child is 'ready' (by calling SetReady) to its parent. The
-// value of the ChildHandle's Secret securely passed to it by the parent; this
-// is intended for subsequent use to create a secure communication channels
-// and or authentication.
+// value of the ChildHandle's Secret securely passed to it by the
+// parent; this is intended for subsequent use 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.
+// 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 NewChildHandle() (*ChildHandle, error) {
 	switch os.Getenv(versionVariable) {
 	case "":
 		return nil, ErrNoVersion
 	case version1:
-		// TODO(cnicolaou): need to use major.minor.build format for version #s
+		// TODO(cnicolaou): need to use major.minor.build format for
+		// version #s.
 	default:
 		return nil, ErrUnsupportedVersion
 	}
-	tokenPipe := os.NewFile(3, "token_rd")
-
-	buf := make([]byte, MaxSecretSize)
-	n, err := tokenPipe.Read(buf)
+	dataPipe := os.NewFile(3, "data_rd")
+	endpoint, err := readData(dataPipe)
+	if err != nil {
+		return nil, err
+	}
+	id, err := readData(dataPipe)
+	if err != nil {
+		return nil, err
+	}
+	secret, err := readData(dataPipe)
 	if err != nil {
 		return nil, err
 	}
 	c := &ChildHandle{
-		Secret:     string(buf[:n]),
+		Endpoint:   endpoint,
+		ID:         id,
+		Secret:     secret,
 		statusPipe: os.NewFile(4, "status_wr"),
 	}
 	return c, nil
@@ -67,3 +94,19 @@
 func (c *ChildHandle) NewExtraFile(i uintptr, name string) *os.File {
 	return os.NewFile(i+fileOffset, name)
 }
+
+func readData(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/example_test.go b/lib/exec/example_test.go
index 8e828ca..e5eeb5a 100644
--- a/lib/exec/example_test.go
+++ b/lib/exec/example_test.go
@@ -17,7 +17,7 @@
 
 func ExampleParentHandle() {
 	cmd := exec.Command("/bin/hostname")
-	ph := NewParentHandle(cmd, "secret")
+	ph := NewParentHandle(cmd, SecretOpt("secret"))
 
 	// Start the child process.
 	if err := ph.Start(); err != nil {
diff --git a/lib/exec/exec_test.go b/lib/exec/exec_test.go
index d84ab8b..4da8b5b 100644
--- a/lib/exec/exec_test.go
+++ b/lib/exec/exec_test.go
@@ -15,9 +15,10 @@
 	"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's wrong, we bail out.
+// 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() {
@@ -25,14 +26,18 @@
 		return
 	}
 	if want, got := baselineOpenFiles, openFiles(); want != got {
-		panic(fmt.Errorf("Test expected to start with %d open files, found %d instead.\nThis can happen if parent process has any open file descriptors, e.g. pipes, that are being inherited.", 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.
+// 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...)
@@ -94,19 +99,55 @@
 	panic("unreachable")
 }
 
-func TestAuthExchange(t *testing.T) {
-	cmd := helperCommand("testAuth")
+func TestEndpointExchange(t *testing.T) {
+	cmd := helperCommand("testEndpoint")
 	stderr, _ := cmd.StderrPipe()
-	ph := vexec.NewParentHandle(cmd, "dummy_secret")
+	ph := vexec.NewParentHandle(cmd, vexec.CallbackEndpointOpt("dummy_endpoint"))
 	err := ph.Start()
 	if err != nil {
-		t.Fatalf("testAuthTest: start: %v", err)
+		t.Fatalf("testEndpointTest: start: %v", err)
+	}
+	if !expectMessage(stderr, "dummy_endpoint") {
+		t.Errorf("unexpected output from child")
+	} else {
+		if err = cmd.Wait(); err != nil {
+			t.Errorf("testEndpointTest: wait: %v", err)
+		}
+	}
+	clean(t, ph)
+}
+
+func TestIDExchange(t *testing.T) {
+	cmd := helperCommand("testID")
+	stderr, _ := cmd.StderrPipe()
+	ph := vexec.NewParentHandle(cmd, vexec.CallbackIDOpt("dummy_id"))
+	err := ph.Start()
+	if err != nil {
+		t.Fatalf("testIDTest: start: %v", err)
+	}
+	if !expectMessage(stderr, "dummy_id") {
+		t.Errorf("unexpected output from child")
+	} else {
+		if err = cmd.Wait(); err != nil {
+			t.Errorf("testIDTest: 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("testAuthTest: wait: %v", err)
+			t.Errorf("testSecretTest: wait: %v", err)
 		}
 	}
 	clean(t, ph)
@@ -134,7 +175,7 @@
 	if err != nil {
 		t.Fatalf("%s: failed to get stderr pipe\n", name)
 	}
-	ph := vexec.NewParentHandle(cmd, "")
+	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
@@ -169,7 +210,7 @@
 		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})
+		ph[i] = vexec.NewParentHandle(cmd[i], vexec.TimeKeeperOpt{tk})
 		done.Add(1)
 		go func() {
 			// For simulated slow children, wait until the parent
@@ -221,7 +262,7 @@
 	result := "never ready"
 	cmd := helperCommand(name)
 	stderr, _ := cmd.StderrPipe()
-	ph := vexec.NewParentHandle(cmd, "")
+	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)
@@ -247,7 +288,7 @@
 	cmd.ExtraFiles = append(cmd.ExtraFiles, controlRead)
 	stderr, _ := cmd.StderrPipe()
 	tk := timekeeper.NewManualTime()
-	ph := vexec.NewParentHandle(cmd, "", vexec.TimeKeeperOpt{tk})
+	ph := vexec.NewParentHandle(cmd, vexec.TimeKeeperOpt{tk})
 	defer clean(t, ph)
 	defer controlWrite.Close()
 	defer controlRead.Close()
@@ -288,7 +329,7 @@
 		t.Fatalf("%s: failed to get stderr pipe", name)
 	}
 	tk := timekeeper.NewManualTime()
-	ph := vexec.NewParentHandle(cmd, "", vexec.TimeKeeperOpt{tk})
+	ph := vexec.NewParentHandle(cmd, vexec.TimeKeeperOpt{tk})
 	defer clean(t, ph)
 	defer controlWrite.Close()
 	defer controlRead.Close()
@@ -457,7 +498,21 @@
 		}()
 		r := <-rc
 		os.Exit(r)
-	case "testAuth":
+	case "testEndpoint":
+		ch, err := vexec.NewChildHandle()
+		if err != nil {
+			log.Fatalf("%v", err)
+		} else {
+			fmt.Fprintf(os.Stderr, "%s", ch.Endpoint)
+		}
+	case "testID":
+		ch, err := vexec.NewChildHandle()
+		if err != nil {
+			log.Fatalf("%s", err)
+		} else {
+			fmt.Fprintf(os.Stderr, "%s", ch.ID)
+		}
+	case "testSecret":
 		ch, err := vexec.NewChildHandle()
 		if err != nil {
 			log.Fatalf("%s", err)
diff --git a/lib/exec/parent.go b/lib/exec/parent.go
index 72a730a..2a77924 100644
--- a/lib/exec/parent.go
+++ b/lib/exec/parent.go
@@ -1,7 +1,9 @@
 package exec
 
 import (
+	"encoding/binary"
 	"errors"
+	"io"
 	"os"
 	"os/exec"
 	"syscall"
@@ -23,6 +25,8 @@
 // A ParentHandle is the Parent process' means of managing a single child.
 type ParentHandle struct {
 	c           *exec.Cmd
+	endpoint    string
+	id          string
 	secret      string
 	statusRead  *os.File
 	statusWrite *os.File
@@ -31,10 +35,33 @@
 
 // ParentHandleOpt is an option for NewParentHandle.
 type ParentHandleOpt interface {
-	// ExecParentHandleOpt is a signature 'dummy' method for the interface.
+	// ExecParentHandleOpt is a signature 'dummy' method for the
+	// interface.
 	ExecParentHandleOpt()
 }
 
+// CallbackEndpointOpt can be used to seed the parent handle with a
+// custom callback endpoint.
+type CallbackEndpointOpt string
+
+// ExecParentHandleOpt makes CallbackEndpointOpt an instance of
+// ParentHandleOpt.
+func (cno CallbackEndpointOpt) ExecParentHandleOpt() {}
+
+// CallbackIDOpt can be used to seed the parent handle with a
+// custom callback ID.
+type CallbackIDOpt string
+
+// ExecParentHandleOpt makes CallbackIDOpt an instance of
+// ParentHandleOpt.
+func (cno CallbackIDOpt) 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 (so SecretOpt) ExecParentHandleOpt() {}
+
 // TimeKeeperOpt can be used to seed the parent handle with a custom timekeeper.
 type TimeKeeperOpt struct {
 	timekeeper.TimeKeeper
@@ -45,69 +72,82 @@
 
 // NewParentHandle creates a ParentHandle for the child process represented by
 // an instance of exec.Cmd.
-func NewParentHandle(c *exec.Cmd, secret string, opts ...ParentHandleOpt) *ParentHandle {
+func NewParentHandle(c *exec.Cmd, opts ...ParentHandleOpt) *ParentHandle {
 	c.Env = append(c.Env, versionVariable+"="+version1)
-	if len(secret) == 0 {
-		secret = emptySecret
-	}
-	var tk timekeeper.TimeKeeper
+	endpoint, id, secret := emptyEndpoint, emptyID, emptySecret
+	tk := timekeeper.RealTime()
 	for _, opt := range opts {
 		switch v := opt.(type) {
+		case CallbackEndpointOpt:
+			endpoint = string(v)
+		case CallbackIDOpt:
+			id = string(v)
+		case SecretOpt:
+			secret = string(v)
 		case TimeKeeperOpt:
 			tk = v
 		default:
 			vlog.Errorf("Unrecognized parent option: %v", v)
 		}
 	}
-	if tk == nil {
-		tk = timekeeper.RealTime()
-	}
 	return &ParentHandle{
-		c:      c,
-		secret: secret,
-		tk:     tk,
+		c:        c,
+		endpoint: endpoint,
+		id:       id,
+		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 {
-	if len(p.secret) > MaxSecretSize {
-		return ErrSecretTooLarge
-	}
 	if parent.BlackboxTest(p.c.Env) {
 		if err := parent.InitBlackboxParent(p.c); err != nil {
 			return err
 		}
 	}
-	tokenRead, tokenWrite, err := os.Pipe()
+	// Create anonymous pipe for communicating data between the child
+	// and the parent.
+	dataRead, dataWrite, err := os.Pipe()
 	if err != nil {
 		return err
 	}
-	defer tokenWrite.Close()
-	defer tokenRead.Close()
-
+	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] = tokenRead
+	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
 	}
-
-	if _, err = tokenWrite.Write([]byte(p.secret)); err != nil {
+	// Pass data to the child using a pipe.
+	if err := writeData(dataWrite, p.endpoint); err != nil {
+		p.statusWrite.Close()
+		p.statusRead.Close()
+		return err
+	}
+	if err := writeData(dataWrite, p.id); err != nil {
+		p.statusWrite.Close()
+		p.statusRead.Close()
+		return err
+	}
+	if err := writeData(dataWrite, p.secret); err != nil {
 		p.statusWrite.Close()
 		p.statusRead.Close()
 		return err
@@ -209,3 +249,18 @@
 	}
 	return p.c.Wait()
 }
+
+func writeData(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
index 0ab1417..cf31731 100644
--- a/lib/exec/shared.go
+++ b/lib/exec/shared.go
@@ -1,12 +1,12 @@
 package exec
 
 const (
-	MaxSecretSize = 8192
-
 	version1        = "1.0.0"
 	readyStatus     = "ready"
 	initStatus      = "init"
 	versionVariable = "VEYRON_EXEC_VERSION"
 
-	emptySecret = "emptySecret"
+	emptyEndpoint = "emptyEndpoint"
+	emptyID       = "emptyID"
+	emptySecret   = "emptySecret"
 )
diff --git a/services/mgmt/node/impl/invoker.go b/services/mgmt/node/impl/invoker.go
index 83cfc66..a50c6ba 100644
--- a/services/mgmt/node/impl/invoker.go
+++ b/services/mgmt/node/impl/invoker.go
@@ -360,7 +360,7 @@
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
 
-	handle := vexec.NewParentHandle(cmd, "")
+	handle := vexec.NewParentHandle(cmd)
 	if err := handle.Start(); err != nil {
 		vlog.Errorf("Start() failed: %v", err)
 		return errOperationFailed