ref: Change test/modules registration mechanism.

Previously modules registration and usage looked like this:

func foo(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
...
}

func init() {
modules.RegisterChild("foo", "", foo)
}

func TestFoo(t *testing.T) {
sh, err := modules.NewShell(...)
h, err := sh.Start("foo", nil, ...)
...
}

The new modules registration and usage looks like this:

var foo = modules.Register(func(env *modules.Env, args ...string) error {
...
}, "foo")

func TestFoo(t *testing.T) {
sh, err := modules.NewShell(...)
h, err := sh.Start(nil, foo, ...)
...
}

The main change is that Register now returns a modules.Program,
which is typically captured in a global variable, and is used as
the argument to Shell.Start.  This makes it easy to write the
registration manually, and we also have a more obvious linkage
between program registration and the Start call through the
program variable, rather than using strings.

Since registration was annoying to write manually, we used to
have 'v23 test generate' detect the functions and automatically
add the modules.RegisterChild call.  With the new mechanism, the
registration is simple to write manually, so 'v23 test generate'
has been simplified to remove the detection logic.

In fact the Program returned by modules.Register now must be
captured, so that it can be passed to Shell.Start; this forces
the linkage between Register and Start to be obvious.

Also removed the modules Help mechanism, since it wasn't being
used, and has questionable utility.  In its place, added logic to
dump all registered programs when program lookups fail.

MultiPart: 3/5

Change-Id: I6442c6959a4cb27fc1515f6ea14a4018ceb9f6b8
diff --git a/cmd/principal/v23_test.go b/cmd/principal/v23_test.go
index dddc89a..842bc1f 100644
--- a/cmd/principal/v23_test.go
+++ b/cmd/principal/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/cmd/servicerunner/main.go b/cmd/servicerunner/main.go
index ba2cda6..d0695fa 100644
--- a/cmd/servicerunner/main.go
+++ b/cmd/servicerunner/main.go
@@ -17,7 +17,6 @@
 import (
 	"encoding/json"
 	"fmt"
-	"io"
 	"os"
 	"strings"
 	"time"
@@ -46,12 +45,8 @@
 	wsprlib.OverrideCaveatValidation()
 	cmdServiceRunner.Flags.IntVar(&port, "port", 8124, "Port for wspr to listen on.")
 	cmdServiceRunner.Flags.StringVar(&identd, "identd", "", "Name of wspr identd server.")
-	modules.RegisterChild("rootMT", ``, rootMT)
-	modules.RegisterChild(wsprdCommand, modules.Usage(&cmdServiceRunner.Flags), startWSPR)
 }
 
-const wsprdCommand = "wsprd"
-
 func main() {
 	cmdline.HideGlobalFlagsExcept()
 	cmdline.Main(cmdServiceRunner)
@@ -68,7 +63,7 @@
 `,
 }
 
-func rootMT(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var rootMT = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -88,13 +83,13 @@
 	if err := server.ServeDispatcher("", mt); err != nil {
 		return fmt.Errorf("root failed: %s", err)
 	}
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	for _, ep := range eps {
-		fmt.Fprintf(stdout, "MT_NAME=%s\n", ep.Name())
+		fmt.Fprintf(env.Stdout, "MT_NAME=%s\n", ep.Name())
 	}
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "rootMT")
 
 // updateVars captures the vars from the given Handle's stdout and adds them to
 // the given vars map, overwriting existing entries.
@@ -130,7 +125,7 @@
 	// The dispatch to modules children must occur after the call to cmdline.Main
 	// (which calls cmdline.Parse), so that servicerunner flags are registered on
 	// the global flag.CommandLine.
-	if modules.IsModulesChildProcess() {
+	if modules.IsChildProcess() {
 		return modules.Dispatch()
 	}
 
@@ -146,7 +141,7 @@
 	}
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 
-	h, err := sh.Start("rootMT", nil, "--v23.tcp.protocol=ws", "--v23.tcp.address=127.0.0.1:0")
+	h, err := sh.Start(nil, rootMT, "--v23.tcp.protocol=ws", "--v23.tcp.address=127.0.0.1:0")
 	if err != nil {
 		return err
 	}
@@ -164,7 +159,7 @@
 	defer proxyShutdown()
 	vars["PROXY_NAME"] = proxyEndpoint.Name()
 
-	h, err = sh.Start(wsprdCommand, nil, "--v23.tcp.protocol=ws", "--v23.tcp.address=127.0.0.1:0", "--v23.proxy=test/proxy", "--identd=test/identd")
+	h, err = sh.Start(nil, wsprd, "--v23.tcp.protocol=ws", "--v23.tcp.address=127.0.0.1:0", "--v23.proxy=test/proxy", "--identd=test/identd")
 	if err != nil {
 		return err
 	}
@@ -172,7 +167,7 @@
 		return err
 	}
 
-	h, err = sh.Start(identitylib.TestIdentitydCommand, nil, "--v23.tcp.protocol=ws", "--v23.tcp.address=127.0.0.1:0", "--v23.proxy=test/proxy", "--http-addr=localhost:0")
+	h, err = sh.Start(nil, identitylib.TestIdentityd, "--v23.tcp.protocol=ws", "--v23.tcp.address=127.0.0.1:0", "--v23.proxy=test/proxy", "--http-addr=localhost:0")
 	if err != nil {
 		return err
 	}
@@ -190,7 +185,7 @@
 	return nil
 }
 
-func startWSPR(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var wsprd = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -203,7 +198,7 @@
 		proxy.Serve()
 	}()
 
-	fmt.Fprintf(stdout, "WSPR_ADDR=%s\n", addr)
-	modules.WaitForEOF(stdin)
+	fmt.Fprintf(env.Stdout, "WSPR_ADDR=%s\n", addr)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "wsprd")
diff --git a/cmd/vdl/v23_internal_test.go b/cmd/vdl/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/cmd/vdl/v23_internal_test.go
+++ b/cmd/vdl/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/cmd/vrpc/v23_internal_test.go b/cmd/vrpc/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/cmd/vrpc/v23_internal_test.go
+++ b/cmd/vrpc/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/cmd/vrun/v23_test.go b/cmd/vrun/v23_test.go
index dd6a542..ece399a 100644
--- a/cmd/vrun/v23_test.go
+++ b/cmd/vrun/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/examples/rps/rpsbot/impl_test.go b/examples/rps/rpsbot/impl_test.go
index de82e86..831f067 100644
--- a/examples/rps/rpsbot/impl_test.go
+++ b/examples/rps/rpsbot/impl_test.go
@@ -29,7 +29,7 @@
 
 //go:generate v23 test generate
 
-func rootMT(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var rootMT = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -49,13 +49,13 @@
 	if err := server.ServeDispatcher("", mt); err != nil {
 		return fmt.Errorf("root failed: %s", err)
 	}
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	for _, ep := range eps {
-		fmt.Fprintf(stdout, "MT_NAME=%s\n", ep.Name())
+		fmt.Fprintf(env.Stdout, "MT_NAME=%s\n", ep.Name())
 	}
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "rootMT")
 
 var spec = rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}}
 
@@ -96,7 +96,7 @@
 		t.Fatalf("Could not create shell: %v", err)
 	}
 	defer sh.Cleanup(os.Stdout, os.Stderr)
-	h, err := sh.Start("rootMT", nil, "--v23.tcp.address=127.0.0.1:0")
+	h, err := sh.Start(nil, rootMT, "--v23.tcp.address=127.0.0.1:0")
 	if err != nil {
 		if h != nil {
 			h.Shutdown(nil, os.Stderr)
diff --git a/examples/rps/rpsbot/v23_internal_test.go b/examples/rps/rpsbot/v23_internal_test.go
index e8fff94..a80e0ec 100644
--- a/examples/rps/rpsbot/v23_internal_test.go
+++ b/examples/rps/rpsbot/v23_internal_test.go
@@ -4,27 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("rootMT", ``, rootMT)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/examples/tunnel/tunneld/v23_test.go b/examples/tunnel/tunneld/v23_test.go
index c2a1b48..99c37d9 100644
--- a/examples/tunnel/tunneld/v23_test.go
+++ b/examples/tunnel/tunneld/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/lib/security/securityflag/flag_test.go b/lib/security/securityflag/flag_test.go
index 29cff52..04ca9ff 100644
--- a/lib/security/securityflag/flag_test.go
+++ b/lib/security/securityflag/flag_test.go
@@ -8,7 +8,6 @@
 	"bytes"
 	"flag"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"reflect"
@@ -45,21 +44,21 @@
 	return a
 }
 
-func tamFromFlag(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var permFromFlag = modules.Register(func(env *modules.Env, args ...string) error {
 	nfargs := flag.CommandLine.Args()
 	tam, err := PermissionsFromFlag()
 	if err != nil {
-		fmt.Fprintf(stdout, "PermissionsFromFlag() failed: %v", err)
+		fmt.Fprintf(env.Stdout, "PermissionsFromFlag() failed: %v", err)
 		return nil
 	}
 	got := auth(access.PermissionsAuthorizer(tam, access.TypicalTagType()))
 	want := expectedAuthorizer[nfargs[0]]
 	if !reflect.DeepEqual(got, want) {
-		fmt.Fprintf(stdout, "args %#v\n", args)
-		fmt.Fprintf(stdout, "AuthorizerFromFlags() got Authorizer: %v, want: %v", got, want)
+		fmt.Fprintf(env.Stdout, "args %#v\n", args)
+		fmt.Fprintf(env.Stdout, "AuthorizerFromFlags() got Authorizer: %v, want: %v", got, want)
 	}
 	return nil
-}
+}, "permFromFlag")
 
 func writePermissionsToFile(perms access.Permissions) (string, error) {
 	f, err := ioutil.TempFile("", "permissions")
@@ -88,30 +87,29 @@
 	defer os.Remove(filename)
 
 	testdata := []struct {
-		cmd   string
+		prog  modules.Program
 		flags []string
 		auth  string
 	}{
 		{
-			cmd:   "tamFromFlag",
+			prog:  permFromFlag,
 			flags: []string{"--v23.permissions.file", "runtime:" + filename},
 			auth:  "perms2",
 		},
 		{
-			cmd:   "tamFromFlag",
+			prog:  permFromFlag,
 			flags: []string{"--v23.permissions.literal", "{}"},
 			auth:  "empty",
 		},
 		{
-			cmd:   "tamFromFlag",
+			prog:  permFromFlag,
 			flags: []string{"--v23.permissions.literal", `{"Read": {"In":["v23/alice/$", "v23/bob"]}, "Write": {"In":["v23/alice/$"]}}`},
 			auth:  "perms2",
 		},
 	}
-
 	for _, td := range testdata {
 		fp := append(td.flags, td.auth)
-		h, err := sh.Start(td.cmd, nil, fp...)
+		h, err := sh.Start(nil, td.prog, fp...)
 		if err != nil {
 			t.Errorf("unexpected error: %s", err)
 		}
diff --git a/lib/security/securityflag/v23_internal_test.go b/lib/security/securityflag/v23_internal_test.go
index d4e2d18..38cf5e0 100644
--- a/lib/security/securityflag/v23_internal_test.go
+++ b/lib/security/securityflag/v23_internal_test.go
@@ -4,27 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package securityflag
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("tamFromFlag", ``, tamFromFlag)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/lib/signals/signals_test.go b/lib/signals/signals_test.go
index fc2f40e..aa0ed90 100644
--- a/lib/signals/signals_test.go
+++ b/lib/signals/signals_test.go
@@ -55,32 +55,32 @@
 	<-closeStopLoop
 }
 
-func handleDefaults(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	program(stdin, stdout)
+var handleDefaults = modules.Register(func(env *modules.Env, args ...string) error {
+	program(env.Stdin, env.Stdout)
 	return nil
-}
+}, "handleDefaults")
 
-func handleCustom(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	program(stdin, stdout, syscall.SIGABRT)
+var handleCustom = modules.Register(func(env *modules.Env, args ...string) error {
+	program(env.Stdin, env.Stdout, syscall.SIGABRT)
 	return nil
-}
+}, "handleCustom")
 
-func handleCustomWithStop(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	program(stdin, stdout, STOP, syscall.SIGABRT, syscall.SIGHUP)
+var handleCustomWithStop = modules.Register(func(env *modules.Env, args ...string) error {
+	program(env.Stdin, env.Stdout, STOP, syscall.SIGABRT, syscall.SIGHUP)
 	return nil
-}
+}, "handleCustomWithStop")
 
-func handleDefaultsIgnoreChan(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var handleDefaultsIgnoreChan = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
 	closeStopLoop := make(chan struct{})
-	go stopLoop(v23.GetAppCycle(ctx).Stop, stdin, closeStopLoop)
+	go stopLoop(v23.GetAppCycle(ctx).Stop, env.Stdin, closeStopLoop)
 	ShutdownOnSignals(ctx)
-	fmt.Fprintf(stdout, "ready\n")
+	fmt.Fprintf(env.Stdout, "ready\n")
 	<-closeStopLoop
 	return nil
-}
+}, "handleDefaultsIgnoreChan")
 
 func isSignalInSet(sig os.Signal, set []os.Signal) bool {
 	for _, s := range set {
@@ -103,12 +103,12 @@
 	}
 }
 
-func newShell(t *testing.T, ctx *context.T, command string) (*modules.Shell, modules.Handle) {
+func newShell(t *testing.T, ctx *context.T, prog modules.Program) (*modules.Shell, modules.Handle) {
 	sh, err := modules.NewShell(ctx, nil, testing.Verbose(), t)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
-	handle, err := sh.Start(command, nil)
+	handle, err := sh.Start(nil, prog)
 	if err != nil {
 		sh.Cleanup(os.Stderr, os.Stderr)
 		t.Fatalf("unexpected error: %s", err)
@@ -123,7 +123,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaults")
+	sh, h := newShell(t, ctx, handleDefaults)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	checkSignalIsDefault(t, syscall.SIGINT)
@@ -139,7 +139,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaults")
+	sh, h := newShell(t, ctx, handleDefaults)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	fmt.Fprintf(h.Stdin(), "stop\n")
@@ -156,7 +156,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleCustomWithStop")
+	sh, h := newShell(t, ctx, handleCustomWithStop)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	fmt.Fprintf(h.Stdin(), "stop\n")
@@ -180,7 +180,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleCustom")
+	sh, h := newShell(t, ctx, handleCustom)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	fmt.Fprintf(h.Stdin(), "stop\n")
@@ -194,7 +194,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaults")
+	sh, h := newShell(t, ctx, handleDefaults)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	checkSignalIsDefault(t, syscall.SIGTERM)
@@ -212,7 +212,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaults")
+	sh, h := newShell(t, ctx, handleDefaults)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	checkSignalIsDefault(t, syscall.SIGTERM)
@@ -229,7 +229,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaults")
+	sh, h := newShell(t, ctx, handleDefaults)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	fmt.Fprintf(h.Stdin(), "stop\n")
@@ -244,7 +244,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaults")
+	sh, h := newShell(t, ctx, handleDefaults)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	checkSignalIsNotDefault(t, syscall.SIGABRT)
@@ -260,7 +260,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleDefaultsIgnoreChan")
+	sh, h := newShell(t, ctx, handleDefaultsIgnoreChan)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	// Even if we ignore the channel that ShutdownOnSignals returns,
@@ -278,7 +278,7 @@
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
-	sh, h := newShell(t, ctx, "handleCustom")
+	sh, h := newShell(t, ctx, handleCustom)
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	h.Expect("ready")
 	checkSignalIsNotDefault(t, syscall.SIGABRT)
@@ -297,7 +297,7 @@
 
 	for _, signal := range []syscall.Signal{syscall.SIGABRT, syscall.SIGHUP} {
 		ctx, _ := vtrace.WithNewTrace(rootCtx)
-		sh, h := newShell(t, ctx, "handleCustomWithStop")
+		sh, h := newShell(t, ctx, handleCustomWithStop)
 		h.Expect("ready")
 		checkSignalIsNotDefault(t, signal)
 		syscall.Kill(h.Pid(), signal)
@@ -367,7 +367,7 @@
 	sh.SetConfigKey(mgmt.ParentNameConfigKey, configServiceName)
 	sh.SetConfigKey(mgmt.ProtocolConfigKey, "tcp")
 	sh.SetConfigKey(mgmt.AddressConfigKey, "127.0.0.1:0")
-	h, err := sh.Start("handleDefaults", nil)
+	h, err := sh.Start(nil, handleDefaults)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/lib/signals/v23_internal_test.go b/lib/signals/v23_internal_test.go
index 4d2767f..e047d2e 100644
--- a/lib/signals/v23_internal_test.go
+++ b/lib/signals/v23_internal_test.go
@@ -4,30 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package signals
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("handleDefaults", ``, handleDefaults)
-	modules.RegisterChild("handleCustom", ``, handleCustom)
-	modules.RegisterChild("handleCustomWithStop", ``, handleCustomWithStop)
-	modules.RegisterChild("handleDefaultsIgnoreChan", ``, handleDefaultsIgnoreChan)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/runtime/internal/lib/deque/v23_internal_test.go b/runtime/internal/lib/deque/v23_internal_test.go
index 5091465..e8e5310 100644
--- a/runtime/internal/lib/deque/v23_internal_test.go
+++ b/runtime/internal/lib/deque/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package deque
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/lib/pcqueue/v23_internal_test.go b/runtime/internal/lib/pcqueue/v23_internal_test.go
index 6475bd5..40cedde 100644
--- a/runtime/internal/lib/pcqueue/v23_internal_test.go
+++ b/runtime/internal/lib/pcqueue/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package pcqueue
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/lib/publisher/v23_internal_test.go b/runtime/internal/lib/publisher/v23_internal_test.go
index 399d8d5..569e177 100644
--- a/runtime/internal/lib/publisher/v23_internal_test.go
+++ b/runtime/internal/lib/publisher/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package publisher
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/lib/sync/v23_internal_test.go b/runtime/internal/lib/sync/v23_internal_test.go
index 008ca9f..16f69e9 100644
--- a/runtime/internal/lib/sync/v23_internal_test.go
+++ b/runtime/internal/lib/sync/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package sync
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/lib/upcqueue/v23_internal_test.go b/runtime/internal/lib/upcqueue/v23_internal_test.go
index af1b3c8..794f10f 100644
--- a/runtime/internal/lib/upcqueue/v23_internal_test.go
+++ b/runtime/internal/lib/upcqueue/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package upcqueue
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/lib/websocket/v23_internal_test.go b/runtime/internal/lib/websocket/v23_internal_test.go
index 2051301..a77fac4 100644
--- a/runtime/internal/lib/websocket/v23_internal_test.go
+++ b/runtime/internal/lib/websocket/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package websocket
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/naming/namespace/v23_internal_test.go b/runtime/internal/naming/namespace/v23_internal_test.go
index 15b3504..cf82452 100644
--- a/runtime/internal/naming/namespace/v23_internal_test.go
+++ b/runtime/internal/naming/namespace/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package namespace
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/rpc/resolve_test.go b/runtime/internal/rpc/resolve_test.go
index 73ace16..08b400a 100644
--- a/runtime/internal/rpc/resolve_test.go
+++ b/runtime/internal/rpc/resolve_test.go
@@ -7,7 +7,6 @@
 import (
 	"flag"
 	"fmt"
-	"io"
 	"os"
 	"testing"
 	"time"
@@ -66,7 +65,7 @@
 	fake.InjectRuntime(runtime, ctx, shutdown)
 }
 
-func rootMountTable(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var rootMT = modules.Register(func(env *modules.Env, args ...string) error {
 	setupRuntime()
 	ctx, shutdown := v23.Init()
 	defer shutdown()
@@ -88,16 +87,16 @@
 	if err := server.ServeDispatcher(mp, mt); err != nil {
 		return fmt.Errorf("root failed: %s", err)
 	}
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	for _, ep := range eps {
-		fmt.Fprintf(stdout, "MT_NAME=%s\n", ep.Name())
+		fmt.Fprintf(env.Stdout, "MT_NAME=%s\n", ep.Name())
 	}
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "rootMT")
 
 func startMT(t *testing.T, sh *modules.Shell) string {
-	h, err := sh.Start("rootMountTable", nil)
+	h, err := sh.Start(nil, rootMT)
 	if err != nil {
 		t.Fatalf("unexpected error for root mt: %s", err)
 	}
diff --git a/runtime/internal/rpc/stream/manager/manager_test.go b/runtime/internal/rpc/stream/manager/manager_test.go
index 2c20fcb..b2a5cad 100644
--- a/runtime/internal/rpc/stream/manager/manager_test.go
+++ b/runtime/internal/rpc/stream/manager/manager_test.go
@@ -37,11 +37,6 @@
 	"v.io/x/ref/test/testutil"
 )
 
-func init() {
-	modules.RegisterChild("runServer", "", runServer)
-	modules.RegisterChild("runRLimitedServer", "", runRLimitedServer)
-}
-
 // We write our own TestMain here instead of relying on v23 test generate because
 // we need to set runtime.GOMAXPROCS.
 func TestMain(m *testing.M) {
@@ -51,13 +46,7 @@
 	// condition that occurs when closing the server; also, using 1 cpu
 	// introduces less variance in the behavior of the test.
 	runtime.GOMAXPROCS(1)
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
 
@@ -676,7 +665,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(nil, nil)
-	h, err := sh.Start("runServer", nil, protocol, "127.0.0.1:0")
+	h, err := sh.Start(nil, runServer, protocol, "127.0.0.1:0")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -697,7 +686,7 @@
 		t.Fatal("Expected client.Dial to fail since server is dead")
 	}
 
-	h, err = sh.Start("runServer", nil, protocol, ep.Addr().String())
+	h, err = sh.Start(nil, runServer, protocol, ep.Addr().String())
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -714,34 +703,36 @@
 	}
 }
 
-func runServer(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var runServer = modules.Register(runServerFunc, "runServer")
+
+func runServerFunc(env *modules.Env, args ...string) error {
 	server := InternalNew(naming.FixedRoutingID(0x55555555))
 	principal := testutil.NewPrincipal("test")
 	_, ep, err := server.Listen(args[0], args[1], principal, principal.BlessingStore().Default())
 	if err != nil {
-		fmt.Fprintln(stderr, err)
+		fmt.Fprintln(env.Stderr, err)
 		return err
 	}
-	fmt.Fprintf(stdout, "ENDPOINT=%v\n", ep)
+	fmt.Fprintf(env.Stdout, "ENDPOINT=%v\n", ep)
 	// Live forever (till the process is explicitly killed)
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
 }
 
-func runRLimitedServer(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var runRLimitedServer = modules.Register(func(env *modules.Env, args ...string) error {
 	var rlimit syscall.Rlimit
 	if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit); err != nil {
-		fmt.Fprintln(stderr, err)
+		fmt.Fprintln(env.Stderr, err)
 		return err
 	}
 	rlimit.Cur = 9
 	if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlimit); err != nil {
-		fmt.Fprintln(stderr, err)
+		fmt.Fprintln(env.Stderr, err)
 		return err
 	}
-	fmt.Fprintf(stdout, "RLIMIT_NOFILE=%d\n", rlimit.Cur)
-	return runServer(stdin, stdout, stderr, env, args...)
-}
+	fmt.Fprintf(env.Stdout, "RLIMIT_NOFILE=%d\n", rlimit.Cur)
+	return runServerFunc(env, args...)
+}, "runRLimitedServer")
 
 func readLine(f stream.Flow) (string, error) {
 	var result bytes.Buffer
@@ -873,7 +864,7 @@
 		t.Fatal(err)
 	}
 	defer sh.Cleanup(nil, nil)
-	h, err := sh.Start("runRLimitedServer", nil, "--logtostderr=true", "tcp", "127.0.0.1:0")
+	h, err := sh.Start(nil, runRLimitedServer, "--logtostderr=true", "tcp", "127.0.0.1:0")
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/runtime/internal/rpc/stream/proxy/v23_internal_test.go b/runtime/internal/rpc/stream/proxy/v23_internal_test.go
index 84bea54..73903ec 100644
--- a/runtime/internal/rpc/stream/proxy/v23_internal_test.go
+++ b/runtime/internal/rpc/stream/proxy/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package proxy
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/rpc/stream/vc/v23_internal_test.go b/runtime/internal/rpc/stream/vc/v23_internal_test.go
index 945d8c4..a48d6f6 100644
--- a/runtime/internal/rpc/stream/vc/v23_internal_test.go
+++ b/runtime/internal/rpc/stream/vc/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package vc
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/rpc/stream/vif/v23_internal_test.go b/runtime/internal/rpc/stream/vif/v23_internal_test.go
index 161553c..70e47de 100644
--- a/runtime/internal/rpc/stream/vif/v23_internal_test.go
+++ b/runtime/internal/rpc/stream/vif/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package vif
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/rpc/test/client_test.go b/runtime/internal/rpc/test/client_test.go
index a1e9013..2f0157f 100644
--- a/runtime/internal/rpc/test/client_test.go
+++ b/runtime/internal/rpc/test/client_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"net"
 	"os"
 	"path/filepath"
@@ -38,15 +37,15 @@
 
 //go:generate v23 test generate .
 
-func rootMT(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var rootMT = modules.Register(func(env *modules.Env, args ...string) error {
 	seclevel := options.SecurityConfidential
 	if len(args) == 1 && args[0] == "nosec" {
 		seclevel = options.SecurityNone
 	}
-	return runRootMT(stdin, stdout, stderr, seclevel, env, args...)
-}
+	return runRootMT(seclevel, env, args...)
+}, "rootMT")
 
-func runRootMT(stdin io.Reader, stdout, stderr io.Writer, seclevel options.SecurityLevel, env map[string]string, args ...string) error {
+func runRootMT(seclevel options.SecurityLevel, env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -66,11 +65,11 @@
 	if err := server.ServeDispatcher("", mt); err != nil {
 		return fmt.Errorf("root failed: %s", err)
 	}
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	for _, ep := range eps {
-		fmt.Fprintf(stdout, "MT_NAME=%s\n", ep.Name())
+		fmt.Fprintf(env.Stdout, "MT_NAME=%s\n", ep.Name())
 	}
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
 }
 
@@ -100,7 +99,7 @@
 	return nil
 }
 
-func echoServer(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var echoServer = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -118,15 +117,15 @@
 	if err := server.ServeDispatcher(mp, disp); err != nil {
 		return err
 	}
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	for _, ep := range eps {
-		fmt.Fprintf(stdout, "NAME=%s\n", ep.Name())
+		fmt.Fprintf(env.Stdout, "NAME=%s\n", ep.Name())
 	}
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "echoServer")
 
-func echoClient(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var echoClient = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -138,10 +137,10 @@
 		if err := client.Call(ctx, name, "Echo", []interface{}{a}, []interface{}{&r}); err != nil {
 			return err
 		}
-		fmt.Fprintf(stdout, r)
+		fmt.Fprintf(env.Stdout, r)
 	}
 	return nil
-}
+}, "echoClient")
 
 func newCtx() (*context.T, v23.Shutdown) {
 	ctx, shutdown := test.InitForTest()
@@ -154,7 +153,7 @@
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
-	root, err := sh.Start("rootMT", nil, args...)
+	root, err := sh.Start(nil, rootMT, args...)
 	if err != nil {
 		t.Fatalf("unexpected error for root mt: %s", err)
 	}
@@ -174,7 +173,7 @@
 }
 
 func runClient(t *testing.T, sh *modules.Shell) error {
-	clt, err := sh.Start("echoClient", nil, "echoServer", "a message")
+	clt, err := sh.Start(nil, echoClient, "echoServer", "a message")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -202,7 +201,7 @@
 
 	sh, fn := runMountTable(t, ctx)
 	defer fn()
-	srv, err := sh.Start("echoServer", nil, "echoServer", "echoServer")
+	srv, err := sh.Start(nil, echoServer, "echoServer", "echoServer")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -508,7 +507,7 @@
 	logErr("client does not trust server", err)
 }
 
-func childPing(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var childPing = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
@@ -518,9 +517,9 @@
 	if err := v23.GetClient(ctx).Call(ctx, name, "Ping", nil, []interface{}{&got}); err != nil {
 		fmt.Errorf("unexpected error: %s", err)
 	}
-	fmt.Fprintf(stdout, "RESULT=%s\n", got)
+	fmt.Fprintf(env.Stdout, "RESULT=%s\n", got)
 	return nil
-}
+}, "childPing")
 
 func initServer(t *testing.T, ctx *context.T, opts ...rpc.ServerOpt) (string, func()) {
 	server, err := v23.NewServer(ctx, opts...)
@@ -654,7 +653,7 @@
 	// backoff of some minutes.
 	startServer := func() {
 		time.Sleep(100 * time.Millisecond)
-		srv, _ := sh.Start("echoServer", nil, "message", name)
+		srv, _ := sh.Start(nil, echoServer, "message", name)
 		s := expect.NewSession(t, srv.Stdout(), time.Minute)
 		s.ExpectVar("PID")
 		s.ExpectVar("NAME")
@@ -686,7 +685,7 @@
 	name, fn := initServer(t, ctx)
 	defer fn()
 
-	srv, err := sh.Start("childPing", nil, name)
+	srv, err := sh.Start(nil, childPing, name)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -814,7 +813,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(os.Stderr, os.Stderr)
-	server, err := sh.Start("echoServer", nil, "--v23.tcp.address=127.0.0.1:0", "mymessage", "")
+	server, err := sh.Start(nil, echoServer, "--v23.tcp.address=127.0.0.1:0", "mymessage", "")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -852,7 +851,7 @@
 	// Resurrect the server with the same address, verify client
 	// re-establishes the connection. This is racy if another
 	// process grabs the port.
-	server, err = sh.Start("echoServer", nil, "--v23.tcp.address="+ep.Address, "mymessage again", "")
+	server, err = sh.Start(nil, echoServer, "--v23.tcp.address="+ep.Address, "mymessage again", "")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/runtime/internal/rpc/test/proxy_test.go b/runtime/internal/rpc/test/proxy_test.go
index 05e1c36..9a6c7b8 100644
--- a/runtime/internal/rpc/test/proxy_test.go
+++ b/runtime/internal/rpc/test/proxy_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"reflect"
 	"sort"
@@ -49,7 +48,7 @@
 	return ctx, shutdown
 }
 
-func proxyServer(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var proxyServer = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -58,11 +57,11 @@
 	listenSpec := rpc.ListenSpec{Addrs: rpc.ListenAddrs{{"tcp", "127.0.0.1:0"}}}
 	proxyShutdown, proxyEp, err := proxy.New(ctx, listenSpec, security.AllowEveryone())
 	if err != nil {
-		fmt.Fprintf(stderr, "%s\n", verror.DebugString(err))
+		fmt.Fprintf(env.Stderr, "%s\n", verror.DebugString(err))
 		return err
 	}
 	defer proxyShutdown()
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	if expected > 0 {
 		pub := publisher.New(ctx, v23.GetNamespace(ctx), time.Minute)
 		defer pub.WaitForStop()
@@ -84,11 +83,11 @@
 			time.Sleep(delay)
 		}
 	}
-	fmt.Fprintf(stdout, "PROXY_NAME=%s\n", proxyEp.Name())
-	modules.WaitForEOF(stdin)
-	fmt.Fprintf(stdout, "DONE\n")
+	fmt.Fprintf(env.Stdout, "PROXY_NAME=%s\n", proxyEp.Name())
+	modules.WaitForEOF(env.Stdin)
+	fmt.Fprintf(env.Stdout, "DONE\n")
 	return nil
-}
+}, "")
 
 type testServer struct{}
 
@@ -121,7 +120,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	h.sh = sh
-	p, err := sh.Start("proxyServer", nil, args...)
+	p, err := sh.Start(nil, proxyServer, args...)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/runtime/internal/rpc/test/v23_internal_test.go b/runtime/internal/rpc/test/v23_internal_test.go
index 0862cd2..d7274da 100644
--- a/runtime/internal/rpc/test/v23_internal_test.go
+++ b/runtime/internal/rpc/test/v23_internal_test.go
@@ -4,31 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("rootMT", ``, rootMT)
-	modules.RegisterChild("echoServer", ``, echoServer)
-	modules.RegisterChild("echoClient", ``, echoClient)
-	modules.RegisterChild("childPing", ``, childPing)
-	modules.RegisterChild("proxyServer", ``, proxyServer)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/runtime/internal/rpc/v23_test.go b/runtime/internal/rpc/v23_test.go
index b874fc4..fac4210 100644
--- a/runtime/internal/rpc/v23_test.go
+++ b/runtime/internal/rpc/v23_test.go
@@ -4,27 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package rpc_test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("rootMountTable", ``, rootMountTable)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/runtime/internal/rt/mgmt_test.go b/runtime/internal/rt/mgmt_test.go
index 73860e5..428b733 100644
--- a/runtime/internal/rt/mgmt_test.go
+++ b/runtime/internal/rt/mgmt_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"reflect"
 	"strings"
@@ -30,12 +29,6 @@
 
 //go:generate v23 test generate
 
-const (
-	noWaitersCmd = "noWaiters"
-	forceStopCmd = "forceStop"
-	appCmd       = "app"
-)
-
 // TestBasic verifies that the basic plumbing works: LocalStop calls result in
 // stop messages being sent on the channel passed to WaitForStop.
 func TestBasic(t *testing.T) {
@@ -103,17 +96,17 @@
 	}
 }
 
-func noWaiters(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var noWaiters = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
 	m := v23.GetAppCycle(ctx)
-	fmt.Fprintf(stdout, "ready\n")
-	modules.WaitForEOF(stdin)
+	fmt.Fprintf(env.Stdout, "ready\n")
+	modules.WaitForEOF(env.Stdin)
 	m.Stop()
 	os.Exit(42) // This should not be reached.
 	return nil
-}
+}, "noWaiters")
 
 // TestNoWaiters verifies that the child process exits in the absence of any
 // wait channel being registered with its runtime.
@@ -123,7 +116,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(os.Stderr, os.Stderr)
-	h, err := sh.Start(noWaitersCmd, nil)
+	h, err := sh.Start(nil, noWaiters)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -134,18 +127,18 @@
 	}
 }
 
-func forceStop(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var forceStop = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
 	m := v23.GetAppCycle(ctx)
-	fmt.Fprintf(stdout, "ready\n")
-	modules.WaitForEOF(stdin)
+	fmt.Fprintf(env.Stdout, "ready\n")
+	modules.WaitForEOF(env.Stdin)
 	m.WaitForStop(make(chan string, 1))
 	m.ForceStop()
 	os.Exit(42) // This should not be reached.
 	return nil
-}
+}, "forceStop")
 
 // TestForceStop verifies that ForceStop causes the child process to exit
 // immediately.
@@ -155,7 +148,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(os.Stderr, os.Stderr)
-	h, err := sh.Start(forceStopCmd, nil)
+	h, err := sh.Start(nil, forceStop)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -250,21 +243,21 @@
 	}
 }
 
-func app(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var app = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
 	m := v23.GetAppCycle(ctx)
 	ch := make(chan string, 1)
 	m.WaitForStop(ch)
-	fmt.Fprintf(stdout, "Got %s\n", <-ch)
+	fmt.Fprintf(env.Stdout, "Got %s\n", <-ch)
 	m.AdvanceGoal(10)
-	fmt.Fprintf(stdout, "Doing some work\n")
+	fmt.Fprintf(env.Stdout, "Doing some work\n")
 	m.AdvanceProgress(2)
-	fmt.Fprintf(stdout, "Doing some more work\n")
+	fmt.Fprintf(env.Stdout, "Doing some more work\n")
 	m.AdvanceProgress(5)
 	return nil
-}
+}, "app")
 
 type configServer struct {
 	ch chan<- string
@@ -306,7 +299,7 @@
 	sh.SetConfigKey(mgmt.ParentNameConfigKey, configServiceName)
 	sh.SetConfigKey(mgmt.ProtocolConfigKey, "tcp")
 	sh.SetConfigKey(mgmt.AddressConfigKey, "127.0.0.1:0")
-	h, err := sh.Start("app", nil)
+	h, err := sh.Start(nil, app)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/runtime/internal/rt/rt_test.go b/runtime/internal/rt/rt_test.go
index 3e53548..2b49ef7 100644
--- a/runtime/internal/rt/rt_test.go
+++ b/runtime/internal/rt/rt_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"regexp"
@@ -53,17 +52,17 @@
 	}
 }
 
-func child(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var child = modules.Register(func(env *modules.Env, args ...string) error {
 	_, shutdown := test.InitForTest()
 	defer shutdown()
 
 	logger := vlog.Log
 	vlog.Infof("%s\n", logger)
-	fmt.Fprintf(stdout, "%s\n", logger)
-	modules.WaitForEOF(stdin)
-	fmt.Fprintf(stdout, "done\n")
+	fmt.Fprintf(env.Stdout, "%s\n", logger)
+	modules.WaitForEOF(env.Stdin)
+	fmt.Fprintf(env.Stdout, "done\n")
 	return nil
-}
+}, "child")
 
 func TestInitArgs(t *testing.T) {
 	sh, err := modules.NewShell(nil, nil, testing.Verbose(), t)
@@ -71,7 +70,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(os.Stderr, os.Stderr)
-	h, err := sh.Start("child", nil, "--logtostderr=true", "--vmodule=*=3", "--", "foobar")
+	h, err := sh.Start(nil, child, "--logtostderr=true", "--vmodule=*=3", "--", "foobar")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -121,7 +120,7 @@
 	return dir
 }
 
-func principal(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var principal = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
@@ -129,13 +128,13 @@
 	if err := validatePrincipal(p); err != nil {
 		return err
 	}
-	fmt.Fprintf(stdout, "DEFAULT_BLESSING=%s\n", defaultBlessing(p))
+	fmt.Fprintf(env.Stdout, "DEFAULT_BLESSING=%s\n", defaultBlessing(p))
 	return nil
-}
+}, "principal")
 
 // Runner runs a principal as a subprocess and reports back with its
 // own security info and it's childs.
-func runner(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var runner = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
@@ -143,18 +142,18 @@
 	if err := validatePrincipal(p); err != nil {
 		return err
 	}
-	fmt.Fprintf(stdout, "RUNNER_DEFAULT_BLESSING=%v\n", defaultBlessing(p))
+	fmt.Fprintf(env.Stdout, "RUNNER_DEFAULT_BLESSING=%v\n", defaultBlessing(p))
 	sh, err := modules.NewShell(ctx, p, false, nil)
 	if err != nil {
 		return err
 	}
-	if _, err := sh.Start("principal", nil, args...); err != nil {
+	if _, err := sh.Start(nil, principal, args...); err != nil {
 		return err
 	}
 	// Cleanup copies the output of sh to these Writers.
-	sh.Cleanup(stdout, stderr)
+	sh.Cleanup(env.Stdout, env.Stderr)
 	return nil
-}
+}, "runner")
 
 func createCredentialsInDir(t *testing.T, dir string, blessing string) {
 	principal, err := vsecurity.CreatePersistentPrincipal(dir, nil)
@@ -186,7 +185,7 @@
 	// directory supplied by the environment.
 	credEnv := []string{ref.EnvCredentials + "=" + cdir}
 
-	h, err := sh.Start("runner", credEnv)
+	h, err := sh.Start(credEnv, runner)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -209,7 +208,7 @@
 func TestPrincipalInit(t *testing.T) {
 	// Collect the process' public key and error status
 	collect := func(sh *modules.Shell, env []string, args ...string) string {
-		h, err := sh.Start("principal", env, args...)
+		h, err := sh.Start(env, principal, args...)
 		if err != nil {
 			t.Fatalf("unexpected error: %s", err)
 		}
diff --git a/runtime/internal/rt/shutdown_servers_test.go b/runtime/internal/rt/shutdown_servers_test.go
index 1a05535..c0713e5 100644
--- a/runtime/internal/rt/shutdown_servers_test.go
+++ b/runtime/internal/rt/shutdown_servers_test.go
@@ -23,11 +23,6 @@
 	"v.io/x/ref/test/modules"
 )
 
-func init() {
-	modules.RegisterChild("simpleServerProgram", "", simpleServerProgram)
-	modules.RegisterChild("complexServerProgram", "", complexServerProgram)
-}
-
 type dummy struct{}
 
 func (*dummy) Echo(*context.T, rpc.ServerCall) error { return nil }
@@ -73,7 +68,7 @@
 // complex server application (with several servers, a mix of interruptible
 // and blocking cleanup, and parallel and sequential cleanup execution).
 // For a more typical server, see simpleServerProgram.
-func complexServerProgram(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var complexServerProgram = modules.Register(func(env *modules.Env, args ...string) error {
 	// Initialize the runtime.  This is boilerplate.
 	ctx, shutdown := test.InitForTest()
 	// shutdown is optional, but it's a good idea to clean up, especially
@@ -84,7 +79,7 @@
 	// commands from the parent process to simulate Stop and
 	// RemoteStop commands that would normally be issued from
 	// application code.
-	defer remoteCmdLoop(ctx, stdin)()
+	defer remoteCmdLoop(ctx, env.Stdin)()
 
 	// Create a couple servers, and start serving.
 	server1 := makeServer(ctx)
@@ -118,9 +113,9 @@
 		case sig := <-sigChan:
 			// If the developer wants to take different actions
 			// depending on the type of signal, they can do it here.
-			fmt.Fprintln(stdout, "Received signal", sig)
+			fmt.Fprintln(env.Stdout, "Received signal", sig)
 		case stop := <-stopChan:
-			fmt.Fprintln(stdout, "Stop", stop)
+			fmt.Fprintln(env.Stdout, "Stop", stop)
 		}
 		// This commences the cleanup stage.
 		done.Done()
@@ -138,7 +133,7 @@
 	// This communicates to the parent test driver process in our unit test
 	// that this server is ready and waiting on signals or stop commands.
 	// It's purely an artifact of our test setup.
-	fmt.Fprintln(stdout, "Ready")
+	fmt.Fprintln(env.Stdout, "Ready")
 
 	// Wait for shutdown.
 	done.Wait()
@@ -174,7 +169,7 @@
 	parallelCleanup.Add(1)
 	blocking.Add(1)
 	go func() {
-		fmt.Fprintln(stdout, "Parallel blocking cleanup1")
+		fmt.Fprintln(env.Stdout, "Parallel blocking cleanup1")
 		blocking.Done()
 		parallelCleanup.Done()
 	}()
@@ -182,40 +177,40 @@
 	parallelCleanup.Add(1)
 	blocking.Add(1)
 	go func() {
-		fmt.Fprintln(stdout, "Parallel blocking cleanup2")
+		fmt.Fprintln(env.Stdout, "Parallel blocking cleanup2")
 		blocking.Done()
 		parallelCleanup.Done()
 	}()
 
 	parallelCleanup.Add(1)
 	go func() {
-		fmt.Fprintln(stdout, "Parallel interruptible cleanup1")
+		fmt.Fprintln(env.Stdout, "Parallel interruptible cleanup1")
 		parallelCleanup.Done()
 	}()
 
 	parallelCleanup.Add(1)
 	go func() {
-		fmt.Fprintln(stdout, "Parallel interruptible cleanup2")
+		fmt.Fprintln(env.Stdout, "Parallel interruptible cleanup2")
 		parallelCleanup.Done()
 	}()
 
 	// Simulate two sequential cleanup steps, one blocking and one
 	// interruptible.
-	fmt.Fprintln(stdout, "Sequential blocking cleanup")
+	fmt.Fprintln(env.Stdout, "Sequential blocking cleanup")
 	blocking.Wait()
 	close(blockingCh)
 
-	fmt.Fprintln(stdout, "Sequential interruptible cleanup")
+	fmt.Fprintln(env.Stdout, "Sequential interruptible cleanup")
 
 	parallelCleanup.Wait()
 	return nil
-}
+}, "complexServerProgram")
 
 // simpleServerProgram demonstrates the recommended way to write a typical
 // simple server application (with one server and a clean shutdown triggered by
 // a signal or a stop command).  For an example of something more involved, see
 // complexServerProgram.
-func simpleServerProgram(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var simpleServerProgram = modules.Register(func(env *modules.Env, args ...string) error {
 	// Initialize the runtime.  This is boilerplate.
 	ctx, shutdown := test.InitForTest()
 	// Calling shutdown is optional, but it's a good idea to clean up, especially
@@ -230,7 +225,7 @@
 	// commands from the parent process to simulate Stop and
 	// RemoteStop commands that would normally be issued from
 	// application code.
-	defer remoteCmdLoop(ctx, stdin)()
+	defer remoteCmdLoop(ctx, env.Stdin)()
 
 	// Create a server, and start serving.
 	server := makeServer(ctx)
@@ -246,17 +241,17 @@
 	// This communicates to the parent test driver process in our unit test
 	// that this server is ready and waiting on signals or stop commands.
 	// It's purely an artifact of our test setup.
-	fmt.Fprintln(stdout, "Ready")
+	fmt.Fprintln(env.Stdout, "Ready")
 
 	// Use defer for anything that should still execute even if a panic
 	// occurs.
-	defer fmt.Fprintln(stdout, "Deferred cleanup")
+	defer fmt.Fprintln(env.Stdout, "Deferred cleanup")
 
 	// Wait for shutdown.
 	sig := <-waiter
 	// The developer could take different actions depending on the type of
 	// signal.
-	fmt.Fprintln(stdout, "Received signal", sig)
+	fmt.Fprintln(env.Stdout, "Received signal", sig)
 
 	// Cleanup code starts here.  Alternatively, these steps could be
 	// invoked through defer, but we list them here to make the order of
@@ -268,7 +263,7 @@
 	// Note, this will not execute in cases of forced shutdown
 	// (e.g. SIGSTOP), when the process calls os.Exit (e.g. via log.Fatal),
 	// or when a panic occurs.
-	fmt.Fprintln(stdout, "Interruptible cleanup")
+	fmt.Fprintln(env.Stdout, "Interruptible cleanup")
 
 	return nil
-}
+}, "simpleServerProgram")
diff --git a/runtime/internal/rt/shutdown_test.go b/runtime/internal/rt/shutdown_test.go
index 85798b3..efb7aff 100644
--- a/runtime/internal/rt/shutdown_test.go
+++ b/runtime/internal/rt/shutdown_test.go
@@ -39,7 +39,7 @@
 func TestSimpleServerSignal(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("simpleServerProgram", nil)
+	h, _ := sh.Start(nil, simpleServerProgram)
 	h.Expect("Ready")
 	syscall.Kill(h.Pid(), syscall.SIGINT)
 	h.Expect("Received signal interrupt")
@@ -54,7 +54,7 @@
 func TestSimpleServerLocalStop(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("simpleServerProgram", nil)
+	h, _ := sh.Start(nil, simpleServerProgram)
 	h.Expect("Ready")
 	fmt.Fprintln(h.Stdin(), "stop")
 	h.Expect(fmt.Sprintf("Received signal %s", v23.LocalStop))
@@ -70,7 +70,7 @@
 func TestSimpleServerDoubleSignal(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("simpleServerProgram", nil)
+	h, _ := sh.Start(nil, simpleServerProgram)
 	h.Expect("Ready")
 	syscall.Kill(h.Pid(), syscall.SIGINT)
 	h.Expect("Received signal interrupt")
@@ -89,7 +89,7 @@
 func TestSimpleServerLocalForceStop(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("simpleServerProgram", nil)
+	h, _ := sh.Start(nil, simpleServerProgram)
 	h.Expect("Ready")
 	fmt.Fprintln(h.Stdin(), "forcestop")
 	h.Expect("straight exit")
@@ -107,7 +107,7 @@
 func TestSimpleServerKill(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("simpleServerProgram", nil)
+	h, _ := sh.Start(nil, simpleServerProgram)
 	h.Expect("Ready")
 	syscall.Kill(h.Pid(), syscall.SIGKILL)
 	err := h.Shutdown(os.Stdout, cstderr)
@@ -126,7 +126,7 @@
 func TestComplexServerSignal(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("complexServerProgram", nil)
+	h, _ := sh.Start(nil, complexServerProgram)
 	h.Expect("Ready")
 	syscall.Kill(h.Pid(), syscall.SIGINT)
 	h.Expect("Received signal interrupt")
@@ -147,7 +147,7 @@
 func TestComplexServerLocalStop(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("complexServerProgram", nil)
+	h, _ := sh.Start(nil, complexServerProgram)
 	h.Expect("Ready")
 
 	fmt.Fprintln(h.Stdin(), "stop")
@@ -173,7 +173,7 @@
 func TestComplexServerDoubleSignal(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("complexServerProgram", nil)
+	h, _ := sh.Start(nil, complexServerProgram)
 	h.Expect("Ready")
 	syscall.Kill(h.Pid(), syscall.SIGINT)
 	h.Expect("Received signal interrupt")
@@ -196,7 +196,7 @@
 func TestComplexServerLocalForceStop(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("complexServerProgram", nil)
+	h, _ := sh.Start(nil, complexServerProgram)
 	h.Expect("Ready")
 	fmt.Fprintln(h.Stdin(), "forcestop")
 	h.Expect("straight exit")
@@ -214,7 +214,7 @@
 func TestComplexServerKill(t *testing.T) {
 	sh := newShell(t)
 	defer sh.Cleanup(os.Stdout, cstderr)
-	h, _ := sh.Start("complexServerProgram", nil)
+	h, _ := sh.Start(nil, complexServerProgram)
 	h.Expect("Ready")
 	syscall.Kill(h.Pid(), syscall.SIGKILL)
 	err := h.Shutdown(os.Stdout, cstderr)
diff --git a/runtime/internal/rt/signal_test.go b/runtime/internal/rt/signal_test.go
index d84455b..7027071 100644
--- a/runtime/internal/rt/signal_test.go
+++ b/runtime/internal/rt/signal_test.go
@@ -18,11 +18,6 @@
 	"v.io/x/ref/test/modules"
 )
 
-func init() {
-	modules.RegisterChild("withRuntime", "", withRuntime)
-	modules.RegisterChild("withoutRuntime", "", withoutRuntime)
-}
-
 func simpleEchoProgram(stdin io.Reader, stdout io.Writer) {
 	fmt.Fprintf(stdout, "ready\n")
 	scanner := bufio.NewScanner(stdin)
@@ -32,18 +27,18 @@
 	modules.WaitForEOF(stdin)
 }
 
-func withRuntime(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var withRuntime = modules.Register(func(env *modules.Env, args ...string) error {
 	_, shutdown := test.InitForTest()
 	defer shutdown()
 
-	simpleEchoProgram(stdin, stdout)
+	simpleEchoProgram(env.Stdin, env.Stdout)
 	return nil
-}
+}, "withRuntime")
 
-func withoutRuntime(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	simpleEchoProgram(stdin, stdout)
+var withoutRuntime = modules.Register(func(env *modules.Env, args ...string) error {
+	simpleEchoProgram(env.Stdin, env.Stdout)
 	return nil
-}
+}, "withoutRuntime")
 
 func TestWithRuntime(t *testing.T) {
 	sh, err := modules.NewShell(nil, nil, testing.Verbose(), t)
@@ -51,7 +46,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(os.Stderr, os.Stderr)
-	h, err := sh.Start("withRuntime", nil)
+	h, err := sh.Start(nil, withRuntime)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -72,7 +67,7 @@
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	opts := sh.DefaultStartOpts()
 	opts.ShutdownTimeout = 5 * time.Second
-	h, err := sh.StartWithOpts(opts, nil, "withoutRuntime")
+	h, err := sh.StartWithOpts(opts, nil, withoutRuntime)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
diff --git a/runtime/internal/rt/v23_test.go b/runtime/internal/rt/v23_test.go
index 774f3c3..2145611 100644
--- a/runtime/internal/rt/v23_test.go
+++ b/runtime/internal/rt/v23_test.go
@@ -4,43 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package rt_test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("noWaiters", ``, noWaiters)
-	modules.RegisterChild("forceStop", ``, forceStop)
-	modules.RegisterChild("app", ``, app)
-	modules.RegisterChild("child", ``, child)
-	modules.RegisterChild("principal", ``, principal)
-	modules.RegisterChild("runner", `Runner runs a principal as a subprocess and reports back with its
-own security info and it's childs.`, runner)
-	modules.RegisterChild("complexServerProgram", `complexServerProgram demonstrates the recommended way to write a more
-complex server application (with several servers, a mix of interruptible
-and blocking cleanup, and parallel and sequential cleanup execution).
-For a more typical server, see simpleServerProgram.`, complexServerProgram)
-	modules.RegisterChild("simpleServerProgram", `simpleServerProgram demonstrates the recommended way to write a typical
-simple server application (with one server and a clean shutdown triggered by
-a signal or a stop command).  For an example of something more involved, see
-complexServerProgram.`, simpleServerProgram)
-	modules.RegisterChild("withRuntime", ``, withRuntime)
-	modules.RegisterChild("withoutRuntime", ``, withoutRuntime)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/runtime/internal/testing/concurrency/v23_internal_test.go b/runtime/internal/testing/concurrency/v23_internal_test.go
index e2d51cf..be5e421 100644
--- a/runtime/internal/testing/concurrency/v23_internal_test.go
+++ b/runtime/internal/testing/concurrency/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package concurrency
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/runtime/internal/testing/mocks/mocknet/v23_internal_test.go b/runtime/internal/testing/mocks/mocknet/v23_internal_test.go
index bc46709..ec445eb 100644
--- a/runtime/internal/testing/mocks/mocknet/v23_internal_test.go
+++ b/runtime/internal/testing/mocks/mocknet/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package mocknet
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/agent/agentlib/agent_test.go b/services/agent/agentlib/agent_test.go
index 93621a9..65b6f3d 100644
--- a/services/agent/agentlib/agent_test.go
+++ b/services/agent/agentlib/agent_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"reflect"
@@ -43,15 +42,15 @@
 
 //go:generate v23 test generate
 
-func getPrincipalAndHang(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var getPrincipalAndHang = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
 	p := v23.GetPrincipal(ctx)
-	fmt.Fprintf(stdout, "DEFAULT_BLESSING=%s\n", p.BlessingStore().Default())
-	ioutil.ReadAll(stdin)
+	fmt.Fprintf(env.Stdout, "DEFAULT_BLESSING=%s\n", p.BlessingStore().Default())
+	ioutil.ReadAll(env.Stdin)
 	return nil
-}
+}, "getPrincipalAndHang")
 
 func newAgent(ctx *context.T, endpoint string, cached bool) (security.Principal, error) {
 	ep, err := v23.NewEndpoint(endpoint)
@@ -165,7 +164,7 @@
 		t.Fatal(err)
 	}
 	// The child process will connect to the agent
-	h, err := sh.Start("getPrincipalAndHang", nil)
+	h, err := sh.Start(nil, getPrincipalAndHang)
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git a/services/agent/agentlib/v23_test.go b/services/agent/agentlib/v23_test.go
index 3e4061b..334393a 100644
--- a/services/agent/agentlib/v23_test.go
+++ b/services/agent/agentlib/v23_test.go
@@ -4,29 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package agentlib_test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-import "v.io/x/ref/test/v23tests"
-
-func init() {
-	modules.RegisterChild("getPrincipalAndHang", ``, getPrincipalAndHang)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/agent/vbecome/v23_test.go b/services/agent/vbecome/v23_test.go
index 35ff567..69f2726 100644
--- a/services/agent/vbecome/v23_test.go
+++ b/services/agent/vbecome/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/application/application/v23_internal_test.go b/services/application/application/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/services/application/application/v23_internal_test.go
+++ b/services/application/application/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/application/applicationd/perms_test.go b/services/application/applicationd/perms_test.go
index e787080..720b64b 100644
--- a/services/application/applicationd/perms_test.go
+++ b/services/application/applicationd/perms_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"reflect"
 	"syscall"
@@ -19,22 +18,18 @@
 	"v.io/v23/services/application"
 	"v.io/v23/verror"
 	"v.io/x/lib/vlog"
-
 	"v.io/x/ref/lib/signals"
 	appd "v.io/x/ref/services/application/applicationd"
 	"v.io/x/ref/services/internal/servicetest"
 	"v.io/x/ref/services/repository"
 	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
 	"v.io/x/ref/test/testutil"
 )
 
 //go:generate v23 test generate
 
-const (
-	repoCmd = "appRepository"
-)
-
-func appRepository(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var appRepository = modules.Register(func(env *modules.Env, args ...string) error {
 	if len(args) < 2 {
 		vlog.Fatalf("repository expected at least name and store arguments and optionally Permissions flags per PermissionsFromFlag")
 	}
@@ -46,7 +41,7 @@
 
 	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
 
-	defer fmt.Fprintf(stdout, "%v terminating\n", publishName)
+	defer fmt.Fprintf(env.Stdout, "%v terminating\n", publishName)
 	defer vlog.VI(1).Infof("%v terminating", publishName)
 	server, endpoint := servicetest.NewServer(ctx)
 	defer server.Stop()
@@ -62,11 +57,11 @@
 		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
 	}
 
-	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "ready:%d\n", os.Getpid())
 	<-signals.ShutdownOnSignals(ctx)
 
 	return nil
-}
+}, "appRepository")
 
 func TestApplicationUpdatePermissions(t *testing.T) {
 	ctx, shutdown := test.InitForTest()
@@ -88,7 +83,7 @@
 	storedir, cleanup := servicetest.SetupRootDir(t, "application")
 	defer cleanup()
 
-	nmh := servicetest.RunCommand(t, sh, nil, repoCmd, "repo", storedir)
+	nmh := servicetest.RunCommand(t, sh, nil, appRepository, "repo", storedir)
 	pid := servicetest.ReadPID(t, nmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -241,7 +236,7 @@
 		t.Fatal(err)
 	}
 
-	nmh := servicetest.RunCommand(t, sh, nil, repoCmd, "repo", storedir)
+	nmh := servicetest.RunCommand(t, sh, nil, appRepository, "repo", storedir)
 	pid := servicetest.ReadPID(t, nmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
diff --git a/services/application/applicationd/v23_test.go b/services/application/applicationd/v23_test.go
index f032617..fff5ec8 100644
--- a/services/application/applicationd/v23_test.go
+++ b/services/application/applicationd/v23_test.go
@@ -4,29 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-import "v.io/x/ref/test/v23tests"
-
-func init() {
-	modules.RegisterChild("appRepository", ``, appRepository)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/binary/binary/v23_internal_test.go b/services/binary/binary/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/services/binary/binary/v23_internal_test.go
+++ b/services/binary/binary/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/binary/binaryd/v23_test.go b/services/binary/binaryd/v23_test.go
index b7aae18..5c12adf 100644
--- a/services/binary/binaryd/v23_test.go
+++ b/services/binary/binaryd/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/build/build/v23_internal_test.go b/services/build/build/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/services/build/build/v23_internal_test.go
+++ b/services/build/build/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/build/buildd/v23_test.go b/services/build/buildd/v23_test.go
index 6ec6ef0..dbfb5ac 100644
--- a/services/build/buildd/v23_test.go
+++ b/services/build/buildd/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/debug/debug/v23_test.go b/services/debug/debug/v23_test.go
index b9cd380..007a5fd 100644
--- a/services/debug/debug/v23_test.go
+++ b/services/debug/debug/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/debug/debuglib/v23_internal_test.go b/services/debug/debuglib/v23_internal_test.go
index cdbaeda..77006f9 100644
--- a/services/debug/debuglib/v23_internal_test.go
+++ b/services/debug/debuglib/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package debuglib
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/device/device/v23_internal_test.go b/services/device/device/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/services/device/device/v23_internal_test.go
+++ b/services/device/device/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/device/internal/impl/applife/app_life_test.go b/services/device/internal/impl/applife/app_life_test.go
index 8484495..efdef51 100644
--- a/services/device/internal/impl/applife/app_life_test.go
+++ b/services/device/internal/impl/applife/app_life_test.go
@@ -78,7 +78,7 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
 
@@ -89,7 +89,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.AppCmd, "google naps", 0, 0, fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.App, "google naps", 0, 0, fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
 
 	// Install the app.  The config-specified flag value for testFlagName
 	// should override the value specified in the envelope above, and the
@@ -185,12 +185,12 @@
 	utiltest.UpdateAppExpectError(t, ctx, appID, impl.ErrUpdateNoOp.ID)
 
 	// Updating the installation should not work with a mismatched title.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "bogus", 0, 0)
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "bogus", 0, 0)
 
 	utiltest.UpdateAppExpectError(t, ctx, appID, impl.ErrAppTitleMismatch.ID)
 
 	// Create a second version of the app and update the app to it.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.AppCmd, "google naps", 0, 0, "appV2")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-val-envelope"}, utiltest.App, "google naps", 0, 0, "appV2")
 
 	utiltest.UpdateApp(t, ctx, appID)
 
@@ -303,7 +303,7 @@
 	// cleanly Do this by installing, instantiating, running, and killing
 	// hangingApp, which sleeps (rather than exits) after being asked to
 	// Stop()
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.HangingAppCmd, "hanging ap", 0, 0, "hAppV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.HangingApp, "hanging ap", 0, 0, "hAppV1")
 	hAppID := utiltest.InstallApp(t, ctx)
 	hInstanceID := utiltest.LaunchApp(t, ctx, hAppID)
 	hangingPid := pingCh.WaitForPingArgs(t).Pid
diff --git a/services/device/internal/impl/applife/instance_reaping_test.go b/services/device/internal/impl/applife/instance_reaping_test.go
index dfe3592..47ca75c 100644
--- a/services/device/internal/impl/applife/instance_reaping_test.go
+++ b/services/device/internal/impl/applife/instance_reaping_test.go
@@ -23,7 +23,7 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
 
@@ -34,7 +34,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "appV1")
 
 	// Install the app.  The config-specified flag value for testFlagName
 	// should override the value specified in the envelope above.
diff --git a/services/device/internal/impl/daemonreap/daemon_reaping_test.go b/services/device/internal/impl/daemonreap/daemon_reaping_test.go
index 84d0602..03dd6d7 100644
--- a/services/device/internal/impl/daemonreap/daemon_reaping_test.go
+++ b/services/device/internal/impl/daemonreap/daemon_reaping_test.go
@@ -24,7 +24,7 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
 
@@ -35,7 +35,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for a first version of the app that will be restarted once.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 1, 10*time.Second, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 1, 10*time.Second, "appV1")
 	appID := utiltest.InstallApp(t, ctx)
 
 	// Start an instance of the app.
diff --git a/services/device/internal/impl/daemonreap/instance_reaping_kill_test.go b/services/device/internal/impl/daemonreap/instance_reaping_kill_test.go
index c04f8b0..9d234dc 100644
--- a/services/device/internal/impl/daemonreap/instance_reaping_kill_test.go
+++ b/services/device/internal/impl/daemonreap/instance_reaping_kill_test.go
@@ -32,7 +32,7 @@
 	defer os.RemoveAll(dmCreds)
 	dmEnv := []string{fmt.Sprintf("%v=%v", ref.EnvCredentials, dmCreds)}
 
-	dmh := servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
 
@@ -42,7 +42,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "appV1")
 
 	// Install the app.
 	appID := utiltest.InstallApp(t, ctx)
@@ -77,7 +77,7 @@
 	}
 
 	// Run another device manager to replace the dead one.
-	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "dm", 1) // Verify the device manager has published itself.
 
diff --git a/services/device/internal/impl/globsuid/glob_test.go b/services/device/internal/impl/globsuid/glob_test.go
index 30fdc23..1d6a04d 100644
--- a/services/device/internal/impl/globsuid/glob_test.go
+++ b/services/device/internal/impl/globsuid/glob_test.go
@@ -36,7 +36,7 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -45,7 +45,7 @@
 	defer cleanup()
 
 	// Create the envelope for the first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "appV1")
 
 	// Device must be claimed before applications can be installed.
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
diff --git a/services/device/internal/impl/globsuid/signature_match_test.go b/services/device/internal/impl/globsuid/signature_match_test.go
index ded825f..aedb942 100644
--- a/services/device/internal/impl/globsuid/signature_match_test.go
+++ b/services/device/internal/impl/globsuid/signature_match_test.go
@@ -73,7 +73,7 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
diff --git a/services/device/internal/impl/globsuid/suid_test.go b/services/device/internal/impl/globsuid/suid_test.go
index 12afe73..313b720 100644
--- a/services/device/internal/impl/globsuid/suid_test.go
+++ b/services/device/internal/impl/globsuid/suid_test.go
@@ -67,7 +67,7 @@
 	// Create a script wrapping the test target that implements suidhelper.
 	helperPath := utiltest.GenerateSuidHelperScript(t, root)
 
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "-mocksetuid", "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "-mocksetuid", "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 	defer utiltest.VerifyNoRunningProcesses(t)
@@ -82,7 +82,7 @@
 	defer cleanup()
 
 	// Create an envelope for a first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-var"}, utiltest.AppCmd, "google naps", 0, 0, fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, []string{utiltest.TestEnvVarName + "=env-var"}, utiltest.App, "google naps", 0, 0, fmt.Sprintf("--%s=flag-val-envelope", utiltest.TestFlagName), "appV1")
 
 	// Install and start the app as root/self.
 	appID := utiltest.InstallApp(t, selfCtx)
diff --git a/services/device/internal/impl/impl_test.go b/services/device/internal/impl/impl_test.go
index 8e21f14..ef302f3 100644
--- a/services/device/internal/impl/impl_test.go
+++ b/services/device/internal/impl/impl_test.go
@@ -108,7 +108,7 @@
 	defer os.RemoveAll(dmCreds)
 	dmEnv := []string{fmt.Sprintf("%v=%v", ref.EnvCredentials, dmCreds)}
 	dmArgs := []string{"factoryDM", root, "unused_helper", utiltest.MockApplicationRepoName, currLink}
-	args, env := sh.CommandEnvelope(utiltest.DeviceManagerCmd, dmEnv, dmArgs...)
+	args, env := sh.ProgramEnvelope(dmEnv, utiltest.DeviceManager, dmArgs...)
 	scriptPathFactory := generateDeviceManagerScript(t, root, args, env)
 
 	if err := os.Symlink(scriptPathFactory, currLink); err != nil {
@@ -126,7 +126,7 @@
 	// demonstrates that the initial device manager could be running by hand
 	// as long as the right initial configuration is passed into the device
 	// manager implementation.
-	dmh := servicetest.RunCommand(t, sh, dmPauseBeforeStopEnv, utiltest.DeviceManagerCmd, dmArgs...)
+	dmh := servicetest.RunCommand(t, sh, dmPauseBeforeStopEnv, utiltest.DeviceManager, dmArgs...)
 	defer func() {
 		syscall.Kill(dmh.Pid(), syscall.SIGINT)
 		utiltest.VerifyNoRunningProcesses(t)
@@ -142,7 +142,7 @@
 	}
 
 	// Simulate an invalid envelope in the application repository.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmPauseBeforeStopEnv, utiltest.DeviceManagerCmd, "bogus", 0, 0, dmArgs...)
+	*envelope = utiltest.EnvelopeFromShell(sh, dmPauseBeforeStopEnv, utiltest.DeviceManager, "bogus", 0, 0, dmArgs...)
 
 	utiltest.UpdateDeviceExpectError(t, ctx, "factoryDM", impl.ErrAppTitleMismatch.ID)
 	utiltest.RevertDeviceExpectError(t, ctx, "factoryDM", impl.ErrUpdateNoOp.ID)
@@ -150,7 +150,7 @@
 	// Set up a second version of the device manager. The information in the
 	// envelope will be used by the device manager to stage the next
 	// version.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerCmd, application.DeviceManagerTitle, 0, 0, "v2DM")
+	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManager, application.DeviceManagerTitle, 0, 0, "v2DM")
 	utiltest.UpdateDevice(t, ctx, "factoryDM")
 
 	// Current link should have been updated to point to v2.
@@ -179,7 +179,7 @@
 	// relaunch it from the current link.
 	utiltest.ResolveExpectNotFound(t, ctx, "v2DM") // Ensure a clean slate.
 
-	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScriptCmd, currLink)
+	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScript, currLink)
 
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "v2DM", 1) // Current link should have been launching v2.
@@ -195,7 +195,7 @@
 	// Try issuing an update with a binary that has a different major version
 	// number. It should fail.
 	utiltest.ResolveExpectNotFound(t, ctx, "v2.5DM") // Ensure a clean slate.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerV10Cmd, application.DeviceManagerTitle, 0, 0, "v2.5DM")
+	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerV10, application.DeviceManagerTitle, 0, 0, "v2.5DM")
 	utiltest.UpdateDeviceExpectError(t, ctx, "v2DM", impl.ErrOperationFailed.ID)
 
 	if evalLink() != scriptPathV2 {
@@ -203,7 +203,7 @@
 	}
 
 	// Create a third version of the device manager and issue an update.
-	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManagerCmd, application.DeviceManagerTitle, 0, 0, "v3DM")
+	*envelope = utiltest.EnvelopeFromShell(sh, dmEnv, utiltest.DeviceManager, application.DeviceManagerTitle, 0, 0, "v3DM")
 	utiltest.UpdateDevice(t, ctx, "v2DM")
 
 	scriptPathV3 := evalLink()
@@ -221,7 +221,7 @@
 	// Re-lanuch the device manager from current link.  We instruct the
 	// device manager to pause before stopping its server, so that we can
 	// verify that a second revert fails while a revert is in progress.
-	dmh = servicetest.RunCommand(t, sh, dmPauseBeforeStopEnv, utiltest.ExecScriptCmd, currLink)
+	dmh = servicetest.RunCommand(t, sh, dmPauseBeforeStopEnv, utiltest.ExecScript, currLink)
 
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "v3DM", 1) // Current link should have been launching v3.
@@ -243,7 +243,7 @@
 
 	utiltest.ResolveExpectNotFound(t, ctx, "v2DM") // Ensure a clean slate.
 
-	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScriptCmd, currLink)
+	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScript, currLink)
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "v2DM", 1) // Current link should have been launching v2.
 
@@ -258,7 +258,7 @@
 
 	utiltest.ResolveExpectNotFound(t, ctx, "factoryDM") // Ensure a clean slate.
 
-	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScriptCmd, currLink)
+	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScript, currLink)
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "factoryDM", 1) // Current link should have been launching factory version.
 	utiltest.ShutdownDevice(t, ctx, "factoryDM")
@@ -267,7 +267,7 @@
 
 	// Re-launch the device manager, to exercise the behavior of Stop.
 	utiltest.ResolveExpectNotFound(t, ctx, "factoryDM") // Ensure a clean slate.
-	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScriptCmd, currLink)
+	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.ExecScript, currLink)
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "factoryDM", 1)
 	utiltest.KillDevice(t, ctx, "factoryDM")
@@ -310,7 +310,7 @@
 	// Create an 'envelope' for the device manager that we can pass to the
 	// installer, to ensure that the device manager that the installer
 	// configures can run.
-	dmargs, dmenv := sh.CommandEnvelope(utiltest.DeviceManagerCmd, nil, "dm")
+	dmargs, dmenv := sh.ProgramEnvelope(nil, utiltest.DeviceManager, "dm")
 	dmDir := filepath.Join(testDir, "dm")
 	// TODO(caprita): Add test logic when initMode = true.
 	singleUser, sessionMode, initMode := true, true, false
@@ -404,7 +404,7 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 	defer utiltest.VerifyNoRunningProcesses(t)
@@ -414,7 +414,7 @@
 	defer cleanup()
 
 	// Create the envelope for the first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "appV1")
 	envelope.Packages = map[string]application.SignedFile{
 		"test": application.SignedFile{
 			File: "realbin/testpkg",
@@ -519,7 +519,7 @@
 		v23.GetPrincipal(c).AddToRoots(v23.GetPrincipal(ctx).BlessingStore().Default())
 	}
 
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 	defer utiltest.VerifyNoRunningProcesses(t)
diff --git a/services/device/internal/impl/perms/debug_perms_test.go b/services/device/internal/impl/perms/debug_perms_test.go
index 296c3b8..77576cf 100644
--- a/services/device/internal/impl/perms/debug_perms_test.go
+++ b/services/device/internal/impl/perms/debug_perms_test.go
@@ -44,7 +44,7 @@
 	defer cleanup()
 
 	// Set up the device manager.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
 
@@ -62,7 +62,7 @@
 	// TODO(rjkroege): Set AccessLists here that conflict with the one provided by the device
 	// manager and show that the one set here is overridden.
 	// Create the envelope for the first version of the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "appV1")
 
 	// Install the app.
 	appID := utiltest.InstallApp(t, ctx)
@@ -200,7 +200,7 @@
 	}
 
 	// Set up the device manager.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "--log_dir="+extraLogDir, "dm", root, helperPath, "unused", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "--log_dir="+extraLogDir, "dm", root, helperPath, "unused", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 
 	// Make some users.
diff --git a/services/device/internal/impl/perms/perms_test.go b/services/device/internal/impl/perms/perms_test.go
index 69fe3c1..166de49 100644
--- a/services/device/internal/impl/perms/perms_test.go
+++ b/services/device/internal/impl/perms/perms_test.go
@@ -55,11 +55,11 @@
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
 	pairingToken := "abcxyz"
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link", pairingToken)
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link", pairingToken)
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "trapp")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "trapp")
 
 	claimantCtx := utiltest.CtxWithNewPrincipal(t, ctx, idp, "claimant")
 	octx, err := v23.WithPrincipal(ctx, testutil.NewPrincipal("other"))
@@ -136,13 +136,13 @@
 
 	// Set up the device manager.  Since we won't do device manager updates,
 	// don't worry about its application envelope and current link.
-	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManagerCmd, "dm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, nil, utiltest.DeviceManager, "dm", root, "unused_helper", "unused_app_repo_name", "unused_curr_link")
 	pid := servicetest.ReadPID(t, dmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 	defer utiltest.VerifyNoRunningProcesses(t)
 
 	// Create an envelope for an app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0)
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0)
 
 	// On an unclaimed device manager, there will be no AccessLists.
 	if _, _, err := device.DeviceClient("claimable").GetPermissions(selfCtx); err == nil {
diff --git a/services/device/internal/impl/reaping/instance_reaping_test.go b/services/device/internal/impl/reaping/instance_reaping_test.go
index 81dd2ac..d7b360a 100644
--- a/services/device/internal/impl/reaping/instance_reaping_test.go
+++ b/services/device/internal/impl/reaping/instance_reaping_test.go
@@ -33,7 +33,7 @@
 	defer os.RemoveAll(dmCreds)
 	dmEnv := []string{fmt.Sprintf("%v=%v", ref.EnvCredentials, dmCreds), fmt.Sprintf("%v=%v", impl.AppcycleReconciliation, "1")}
 
-	dmh := servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh := servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.ClaimDevice(t, ctx, "claimable", "dm", "mydevice", utiltest.NoPairingToken)
 
@@ -43,7 +43,7 @@
 	utiltest.Resolve(t, ctx, "pingserver", 1)
 
 	// Create an envelope for the app.
-	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.AppCmd, "google naps", 0, 0, "appV1")
+	*envelope = utiltest.EnvelopeFromShell(sh, nil, utiltest.App, "google naps", 0, 0, "appV1")
 
 	// Install the app.
 	appID := utiltest.InstallApp(t, ctx)
@@ -78,7 +78,7 @@
 	}
 
 	// Run another device manager to replace the dead one.
-	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManagerCmd, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
+	dmh = servicetest.RunCommand(t, sh, dmEnv, utiltest.DeviceManager, "dm", root, helperPath, "unused_app_repo_name", "unused_curr_link")
 	servicetest.ReadPID(t, dmh)
 	utiltest.Resolve(t, ctx, "dm", 1) // Verify the device manager has published itself.
 
diff --git a/services/device/internal/impl/utiltest/app.go b/services/device/internal/impl/utiltest/app.go
index 47d88a4..bc194c9 100644
--- a/services/device/internal/impl/utiltest/app.go
+++ b/services/device/internal/impl/utiltest/app.go
@@ -8,7 +8,6 @@
 	"encoding/json"
 	"flag"
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -22,13 +21,12 @@
 	"v.io/v23/naming"
 	"v.io/v23/rpc"
 	"v.io/v23/security"
-
 	"v.io/x/lib/vlog"
-
 	"v.io/x/ref/lib/signals"
 	"v.io/x/ref/services/device/internal/suid"
 	"v.io/x/ref/services/internal/servicetest"
 	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
 	"v.io/x/ref/test/testutil"
 )
 
@@ -113,8 +111,10 @@
 	return content, nil
 }
 
-// app is a test application. It pings the invoking device manager with state information.
-func app(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+// App is a test application. It pings the invoking device manager with state information.
+var App = modules.Register(appFunc, "App")
+
+func appFunc(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
@@ -193,9 +193,10 @@
 	}
 }
 
-// Same as app, except that it does not exit properly after being stopped
-func hangingApp(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	err := app(stdin, stdout, stderr, env, args...)
+// HangingApp is the same as App, except that it does not exit properly after
+// being stopped.
+var HangingApp = modules.Register(func(env *modules.Env, args ...string) error {
+	err := appFunc(env, args...)
 	time.Sleep(24 * time.Hour)
 	return err
-}
+}, "HangingApp")
diff --git a/services/device/internal/impl/utiltest/helpers.go b/services/device/internal/impl/utiltest/helpers.go
index d461eb2..498c05b 100644
--- a/services/device/internal/impl/utiltest/helpers.go
+++ b/services/device/internal/impl/utiltest/helpers.go
@@ -69,8 +69,8 @@
 	}
 }
 
-func EnvelopeFromShell(sh *modules.Shell, env []string, cmd, title string, retries int, window time.Duration, args ...string) application.Envelope {
-	args, nenv := sh.CommandEnvelope(cmd, env, args...)
+func EnvelopeFromShell(sh *modules.Shell, env []string, prog modules.Program, title string, retries int, window time.Duration, args ...string) application.Envelope {
+	args, nenv := sh.ProgramEnvelope(env, prog, args...)
 	return application.Envelope{
 		Title: title,
 		Args:  args[1:],
diff --git a/services/device/internal/impl/utiltest/modules.go b/services/device/internal/impl/utiltest/modules.go
index 96d6f6b..13656e3 100644
--- a/services/device/internal/impl/utiltest/modules.go
+++ b/services/device/internal/impl/utiltest/modules.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"os"
 	goexec "os/exec"
 	"strings"
@@ -31,48 +30,42 @@
 	RedirectEnv    = "DEVICE_MANAGER_DONT_REDIRECT_STDOUT_STDERR"
 	TestEnvVarName = "V23_RANDOM_ENV_VALUE"
 	NoPairingToken = ""
-
-	// Modules names.
-	ExecScriptCmd       = "execScript"
-	DeviceManagerCmd    = "deviceManager"
-	DeviceManagerV10Cmd = "deviceManagerV10" // deviceManager with a different major version number
-	AppCmd              = "app"
-	HangingAppCmd       = "hangingApp"
 )
 
-// execScript launches the script passed as argument.
-func execScript(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+// ExecScript launches the script passed as argument.
+var ExecScript = modules.Register(func(env *modules.Env, args ...string) error {
 	if want, got := 1, len(args); want != got {
-		vlog.Fatalf("execScript expected %d arguments, got %d instead", want, got)
+		vlog.Fatalf("ExecScript expected %d arguments, got %d instead", want, got)
 	}
 	script := args[0]
 	osenv := []string{RedirectEnv + "=1"}
-	if env["PAUSE_BEFORE_STOP"] == "1" {
+	if env.Vars["PAUSE_BEFORE_STOP"] == "1" {
 		osenv = append(osenv, "PAUSE_BEFORE_STOP=1")
 	}
 
 	cmd := goexec.Cmd{
 		Path:   script,
 		Env:    osenv,
-		Stdin:  stdin,
-		Stderr: stderr,
-		Stdout: stdout,
+		Stdin:  env.Stdin,
+		Stderr: env.Stderr,
+		Stdout: env.Stdout,
 	}
-
 	return cmd.Run()
-}
+}, "ExecScript")
 
-// deviceManager sets up a device manager server.  It accepts the name to
+// DeviceManager sets up a device manager server.  It accepts the name to
 // publish the server under as an argument.  Additional arguments can optionally
 // specify device manager config settings.
-func deviceManager(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var DeviceManager = modules.Register(deviceManagerFunc, "DeviceManager")
+
+func deviceManagerFunc(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	if len(args) == 0 {
 		vlog.Fatalf("deviceManager expected at least an argument")
 	}
 	publishName := args[0]
 	args = args[1:]
-	defer fmt.Fprintf(stdout, "%v terminated\n", publishName)
+	defer fmt.Fprintf(env.Stdout, "%v terminated\n", publishName)
 	defer vlog.VI(1).Infof("%v terminated", publishName)
 	defer shutdown()
 	v23.GetNamespace(ctx).CacheCtl(naming.DisableCache(true))
@@ -134,11 +127,11 @@
 	}
 	// Manually mount the claimable service in the 'global' mounttable.
 	v23.GetNamespace(ctx).Mount(ctx, "claimable", claimableName, 0)
-	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "ready:%d\n", os.Getpid())
 
 	<-shutdownChan
-	if val, present := env["PAUSE_BEFORE_STOP"]; present && val == "1" {
-		modules.WaitForEOF(stdin)
+	if val, present := env.Vars["PAUSE_BEFORE_STOP"]; present && val == "1" {
+		modules.WaitForEOF(env.Stdin)
 	}
 	// TODO(ashankar): Figure out a way to incorporate this check in the test.
 	// if impl.DispatcherLeaking(dispatcher) {
@@ -148,15 +141,15 @@
 }
 
 // This is the same as DeviceManager above, except that it has a different major version number
-func deviceManagerV10(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var DeviceManagerV10 = modules.Register(func(env *modules.Env, args ...string) error {
 	impl.CurrentVersion = impl.Version{10, 0} // Set the version number to 10.0
-	return deviceManager(stdin, stdout, stderr, env, args...)
-}
+	return deviceManagerFunc(env, args...)
+}, "DeviceManagerV10")
 
 func TestMainImpl(m *testing.M) {
 	test.Init()
 	isSuidHelper := len(os.Getenv("V23_SUIDHELPER_TEST")) > 0
-	if modules.IsModulesChildProcess() && !isSuidHelper {
+	if modules.IsChildProcess() && !isSuidHelper {
 		if err := modules.Dispatch(); err != nil {
 			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
 			os.Exit(1)
@@ -177,13 +170,3 @@
 		vlog.Fatalf("Failed to Run() setuidhelper: %v", err)
 	}
 }
-
-func init() {
-	modules.RegisterChild("execScript", `execScript launches the script passed as argument.`, execScript)
-	modules.RegisterChild("deviceManager", `deviceManager sets up a device manager server.  It accepts the name to
-publish the server under as an argument.  Additional arguments can optionally
- specify device manager config settings.`, deviceManager)
-	modules.RegisterChild("deviceManagerV10", `This is the same as deviceManager above, except that it has a different major version number`, deviceManagerV10)
-	modules.RegisterChild("app", ``, app)
-	modules.RegisterChild("hangingApp", `Same as app, except that it does not exit properly after being stopped`, hangingApp)
-}
diff --git a/services/device/mgmt_v23_test.go b/services/device/mgmt_v23_test.go
index c07c9da..61d2ada 100644
--- a/services/device/mgmt_v23_test.go
+++ b/services/device/mgmt_v23_test.go
@@ -126,7 +126,7 @@
 		// Create those credentials and options to use to setup the
 		// binaries with them.
 		aliceCreds, _ = i.Shell().NewChildCredentials("alice")
-		aliceOpts     = i.Shell().DefaultStartOpts().ExternalCommand().WithCustomCredentials(aliceCreds)
+		aliceOpts     = i.Shell().DefaultStartOpts().ExternalProgram().WithCustomCredentials(aliceCreds)
 
 		// Build all the command-line tools and set them up to run as alice.
 		// applicationd/binaryd servers will be run by alice too.
diff --git a/services/device/v23_test.go b/services/device/v23_test.go
index 3eeab2f..0bb141e 100644
--- a/services/device/v23_test.go
+++ b/services/device/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package device_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/identity/identityd/v23_test.go b/services/identity/identityd/v23_test.go
index 13c2816..17cc37d 100644
--- a/services/identity/identityd/v23_test.go
+++ b/services/identity/identityd/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/identity/identitylib/test_identityd.go b/services/identity/identitylib/test_identityd.go
index 03cdf0a..a633bbd 100644
--- a/services/identity/identitylib/test_identityd.go
+++ b/services/identity/identitylib/test_identityd.go
@@ -9,7 +9,6 @@
 import (
 	"flag"
 	"fmt"
-	"io"
 	"net"
 	"strconv"
 	"time"
@@ -32,15 +31,7 @@
 	tlsConfig        = flag.CommandLine.String("tls-config", "", "Comma-separated list of TLS certificate and private key files. This must be provided.")
 )
 
-const (
-	TestIdentitydCommand = "test_identityd"
-)
-
-func init() {
-	modules.RegisterChild(TestIdentitydCommand, modules.Usage(flag.CommandLine), startTestIdentityd)
-}
-
-func startTestIdentityd(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var TestIdentityd = modules.Register(func(env *modules.Env, args ...string) error {
 	// Duration to use for tls cert and blessing duration.
 	duration := 365 * 24 * time.Hour
 
@@ -101,12 +92,12 @@
 
 	_, eps, externalHttpAddress := s.Listen(ctx, &l, *externalHttpAddr, *httpAddr, *tlsConfig)
 
-	fmt.Fprintf(stdout, "TEST_IDENTITYD_NAME=%s\n", eps[0])
-	fmt.Fprintf(stdout, "TEST_IDENTITYD_HTTP_ADDR=%s\n", externalHttpAddress)
+	fmt.Fprintf(env.Stdout, "TEST_IDENTITYD_NAME=%s\n", eps[0])
+	fmt.Fprintf(env.Stdout, "TEST_IDENTITYD_HTTP_ADDR=%s\n", externalHttpAddress)
 
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "TestIdentityd")
 
 func freePort() string {
 	l, _ := net.Listen("tcp", ":0")
diff --git a/services/identity/internal/revocation/v23_internal_test.go b/services/identity/internal/revocation/v23_internal_test.go
index ed4c9f9..d53fea3 100644
--- a/services/identity/internal/revocation/v23_internal_test.go
+++ b/services/identity/internal/revocation/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package revocation
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/internal/binarylib/perms_test.go b/services/internal/binarylib/perms_test.go
index 59de734..0b2e978 100644
--- a/services/internal/binarylib/perms_test.go
+++ b/services/internal/binarylib/perms_test.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"reflect"
 	"syscall"
@@ -25,16 +24,13 @@
 	"v.io/x/ref/services/internal/binarylib"
 	"v.io/x/ref/services/internal/servicetest"
 	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
 	"v.io/x/ref/test/testutil"
 )
 
 //go:generate v23 test generate
 
-const (
-	binaryCmd = "binaryd"
-)
-
-func binaryd(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var binaryd = modules.Register(func(env *modules.Env, args ...string) error {
 	if len(args) < 2 {
 		vlog.Fatalf("binaryd expected at least name and store arguments and optionally AccessList flags per PermissionsFromFlag")
 	}
@@ -43,7 +39,7 @@
 
 	ctx, shutdown := test.InitForTest()
 
-	defer fmt.Fprintf(stdout, "%v terminating\n", publishName)
+	defer fmt.Fprintf(env.Stdout, "%v terminating\n", publishName)
 	defer vlog.VI(1).Infof("%v terminating", publishName)
 	defer shutdown()
 
@@ -64,11 +60,11 @@
 		vlog.Fatalf("Serve(%v) failed: %v", publishName, err)
 	}
 
-	fmt.Fprintf(stdout, "ready:%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "ready:%d\n", os.Getpid())
 	<-signals.ShutdownOnSignals(ctx)
 
 	return nil
-}
+}, "binaryd")
 
 func b(name string) repository.BinaryClientStub {
 	return repository.BinaryClient(name)
@@ -112,7 +108,7 @@
 	defer cleanup()
 	prepDirectory(t, storedir)
 
-	nmh := servicetest.RunCommand(t, sh, nil, binaryCmd, "bini", storedir)
+	nmh := servicetest.RunCommand(t, sh, nil, binaryd, "bini", storedir)
 	pid := servicetest.ReadPID(t, nmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -169,7 +165,7 @@
 		t.Fatalf("WithPrincipal() failed: %v", err)
 	}
 
-	nmh := servicetest.RunCommand(t, sh, nil, binaryCmd, "bini", storedir)
+	nmh := servicetest.RunCommand(t, sh, nil, binaryd, "bini", storedir)
 	pid := servicetest.ReadPID(t, nmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
@@ -449,7 +445,7 @@
 		t.Fatalf("otherPrincipal.AddToRoots() failed: %v", err)
 	}
 
-	nmh := servicetest.RunCommand(t, sh, nil, binaryCmd, "bini", storedir)
+	nmh := servicetest.RunCommand(t, sh, nil, binaryd, "bini", storedir)
 	pid := servicetest.ReadPID(t, nmh)
 	defer syscall.Kill(pid, syscall.SIGINT)
 
diff --git a/services/internal/binarylib/v23_test.go b/services/internal/binarylib/v23_test.go
index 06b7552..9e2e792 100644
--- a/services/internal/binarylib/v23_test.go
+++ b/services/internal/binarylib/v23_test.go
@@ -4,27 +4,19 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package binarylib_test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-
-func init() {
-	modules.RegisterChild("binaryd", ``, binaryd)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
diff --git a/services/internal/logreaderlib/v23_internal_test.go b/services/internal/logreaderlib/v23_internal_test.go
index bcdf9fb..c0b54bf 100644
--- a/services/internal/logreaderlib/v23_internal_test.go
+++ b/services/internal/logreaderlib/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package logreaderlib
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/internal/pproflib/v23_internal_test.go b/services/internal/pproflib/v23_internal_test.go
index 266140e..2b18e2d 100644
--- a/services/internal/pproflib/v23_internal_test.go
+++ b/services/internal/pproflib/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package pproflib
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/internal/servicetest/modules.go b/services/internal/servicetest/modules.go
index 7954346..e69c624 100644
--- a/services/internal/servicetest/modules.go
+++ b/services/internal/servicetest/modules.go
@@ -6,7 +6,6 @@
 
 import (
 	"fmt"
-	"io"
 	"io/ioutil"
 	"os"
 	"path/filepath"
@@ -33,11 +32,7 @@
 	preserveWorkspaceEnv = "V23_TEST_PRESERVE_WORKSPACE"
 )
 
-func init() {
-	modules.RegisterChild("rootMT", ``, rootMT)
-}
-
-func rootMT(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var rootMT = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -57,17 +52,17 @@
 	if err := server.ServeDispatcher("", mt); err != nil {
 		return fmt.Errorf("root failed: %s", err)
 	}
-	fmt.Fprintf(stdout, "PID=%d\n", os.Getpid())
+	fmt.Fprintf(env.Stdout, "PID=%d\n", os.Getpid())
 	for _, ep := range eps {
-		fmt.Fprintf(stdout, "MT_NAME=%s\n", ep.Name())
+		fmt.Fprintf(env.Stdout, "MT_NAME=%s\n", ep.Name())
 	}
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "rootMT")
 
 // startRootMT sets up a root mount table for tests.
 func startRootMT(t *testing.T, sh *modules.Shell) (string, modules.Handle) {
-	h, err := sh.Start("rootMT", nil, "--v23.tcp.address=127.0.0.1:0")
+	h, err := sh.Start(nil, rootMT, "--v23.tcp.address=127.0.0.1:0")
 	if err != nil {
 		t.Fatalf("failed to start root mount table: %s", err)
 	}
@@ -123,10 +118,10 @@
 }
 
 // RunCommand runs a modules command.
-func RunCommand(t *testing.T, sh *modules.Shell, env []string, cmd string, args ...string) modules.Handle {
-	h, err := sh.StartWithOpts(sh.DefaultStartOpts(), env, cmd, args...)
+func RunCommand(t *testing.T, sh *modules.Shell, env []string, prog modules.Program, args ...string) modules.Handle {
+	h, err := sh.StartWithOpts(sh.DefaultStartOpts(), env, prog, args...)
 	if err != nil {
-		t.Fatalf(testutil.FormatLogLine(2, "failed to start %q: %s", cmd, err))
+		t.Fatalf(testutil.FormatLogLine(2, "failed to start %q: %s", prog, err))
 		return nil
 	}
 	h.SetVerbosity(testing.Verbose())
diff --git a/services/internal/statslib/v23_internal_test.go b/services/internal/statslib/v23_internal_test.go
index 8f6c7c9..b34a92f 100644
--- a/services/internal/statslib/v23_internal_test.go
+++ b/services/internal/statslib/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package statslib
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/internal/vtracelib/v23_internal_test.go b/services/internal/vtracelib/v23_internal_test.go
index 386c81f..2229a8d 100644
--- a/services/internal/vtracelib/v23_internal_test.go
+++ b/services/internal/vtracelib/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package vtracelib
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/mounttable/mounttabled/v23_test.go b/services/mounttable/mounttabled/v23_test.go
index 0d570e3..b88590f 100644
--- a/services/mounttable/mounttabled/v23_test.go
+++ b/services/mounttable/mounttabled/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/mounttable/mounttablelib/v23_internal_test.go b/services/mounttable/mounttablelib/v23_internal_test.go
index 1d210b4..82b7beb 100644
--- a/services/mounttable/mounttablelib/v23_internal_test.go
+++ b/services/mounttable/mounttablelib/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package mounttablelib
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/profile/profile/v23_internal_test.go b/services/profile/profile/v23_internal_test.go
index dcd0029..ae59080 100644
--- a/services/profile/profile/v23_internal_test.go
+++ b/services/profile/profile/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/profile/profiled/v23_test.go b/services/profile/profiled/v23_test.go
index cb53f83..9df2772 100644
--- a/services/profile/profiled/v23_test.go
+++ b/services/profile/profiled/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/proxy/proxyd/proxyd_v23_test.go b/services/proxy/proxyd/proxyd_v23_test.go
index e58c90d..167ea42 100644
--- a/services/proxy/proxyd/proxyd_v23_test.go
+++ b/services/proxy/proxyd/proxyd_v23_test.go
@@ -6,13 +6,11 @@
 
 import (
 	"fmt"
-	"io"
 
 	"v.io/v23"
 	"v.io/v23/context"
 	"v.io/v23/rpc"
 	"v.io/v23/security"
-
 	"v.io/x/ref/test/modules"
 	"v.io/x/ref/test/v23tests"
 )
@@ -20,18 +18,11 @@
 //go:generate v23 test generate
 
 const (
-	serverCmd   = "server"
-	clientCmd   = "client"
 	proxyName   = "proxy"    // Name which the proxy mounts itself at
 	serverName  = "server"   // Name which the server mounts itself at
 	responseVar = "RESPONSE" // Name of the variable used by client program to output the response
 )
 
-func init() {
-	modules.RegisterChild(serverCmd, "server", runServer)
-	modules.RegisterChild(clientCmd, "client", runClient)
-}
-
 func V23TestProxyd(t *v23tests.T) {
 	v23tests.RunRootMT(t, "--v23.tcp.address=127.0.0.1:0")
 	var (
@@ -47,14 +38,14 @@
 	if _, err := t.Shell().StartWithOpts(
 		t.Shell().DefaultStartOpts().WithCustomCredentials(serverCreds),
 		nil,
-		serverCmd); err != nil {
+		runServer); err != nil {
 		t.Fatal(err)
 	}
 	// Run the client.
 	client, err := t.Shell().StartWithOpts(
 		t.Shell().DefaultStartOpts().WithCustomCredentials(clientCreds),
 		nil,
-		clientCmd)
+		runClient)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -63,7 +54,7 @@
 	}
 }
 
-func runServer(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var runServer = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -79,11 +70,11 @@
 		return err
 	}
 
-	modules.WaitForEOF(stdin)
+	modules.WaitForEOF(env.Stdin)
 	return nil
-}
+}, "runServer")
 
-func runClient(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var runClient = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := v23.Init()
 	defer shutdown()
 
@@ -95,9 +86,9 @@
 	if err := call.Finish(&response); err != nil {
 		return err
 	}
-	fmt.Fprintf(stdout, "%v=%v\n", responseVar, response)
+	fmt.Fprintf(env.Stdout, "%v=%v\n", responseVar, response)
 	return nil
-}
+}, "runClient")
 
 type service struct{}
 
diff --git a/services/proxy/proxyd/v23_test.go b/services/proxy/proxyd/v23_test.go
index 436b072..98cd476 100644
--- a/services/proxy/proxyd/v23_test.go
+++ b/services/proxy/proxyd/v23_test.go
@@ -4,30 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package main_test
 
-import "fmt"
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/modules"
-import "v.io/x/ref/test/v23tests"
-
-func init() {
-	modules.RegisterChild("runServer", ``, runServer)
-	modules.RegisterChild("runClient", ``, runClient)
-}
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/services/wspr/internal/app/v23_internal_test.go b/services/wspr/internal/app/v23_internal_test.go
index 9ae5683..70d6982 100644
--- a/services/wspr/internal/app/v23_internal_test.go
+++ b/services/wspr/internal/app/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package app
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/services/wspr/internal/browspr/v23_internal_test.go b/services/wspr/internal/browspr/v23_internal_test.go
index 9b21db0..6c94e49 100644
--- a/services/wspr/internal/browspr/v23_internal_test.go
+++ b/services/wspr/internal/browspr/v23_internal_test.go
@@ -4,12 +4,15 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package browspr
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
+	"v.io/x/ref/test"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
diff --git a/test/hello/v23_test.go b/test/hello/v23_test.go
index 515e1cd..6364cfb 100644
--- a/test/hello/v23_test.go
+++ b/test/hello/v23_test.go
@@ -4,16 +4,21 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package hello_test
 
-import "testing"
-import "os"
+import (
+	"os"
+	"testing"
 
-import "v.io/x/ref/test"
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test"
+	"v.io/x/ref/test/modules"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestMain(m *testing.M) {
 	test.Init()
+	modules.DispatchAndExitIfChild()
 	cleanup := v23tests.UseSharedBinDir()
 	r := m.Run()
 	cleanup()
diff --git a/test/modules/examples_test.go b/test/modules/examples_test.go
index 8b3113b..b5b66b1 100644
--- a/test/modules/examples_test.go
+++ b/test/modules/examples_test.go
@@ -6,52 +6,48 @@
 
 import (
 	"fmt"
-	"io"
 	"os"
 
 	"v.io/x/ref/test"
 	"v.io/x/ref/test/modules"
 )
 
-func init() {
-	modules.RegisterChild("echo", "<args>...", echo)
-}
-
-func echo(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var Echo = modules.Register(func(env *modules.Env, args ...string) error {
 	for i, a := range args {
-		fmt.Fprintf(stdout, "%d: %s\n", i, a)
+		fmt.Fprintf(env.Stdout, "%d: %s\n", i, a)
 	}
 	return nil
-}
+}, "echo")
 
 func ExampleDispatch() {
-	ctx, shutdown := test.InitForTest()
-	defer shutdown()
-	if modules.IsModulesChildProcess() {
-		// Child process. Dispatch will invoke the 'echo' command
+	if modules.IsChildProcess() {
+		// Child process dispatches to the echo program.
 		if err := modules.Dispatch(); err != nil {
-			panic(fmt.Sprintf("unexpected error: %s", err))
+			panic(err)
 		}
 		return
 	}
-	// Parent process.
+	// Parent process spawns the echo program.
+	ctx, shutdown := test.InitForTest()
+	defer shutdown()
 	sh, _ := modules.NewShell(ctx, nil, false, nil)
 	defer sh.Cleanup(nil, nil)
-	h, _ := sh.Start("echo", nil, "a", "b")
+	h, _ := sh.Start(nil, Echo, "a", "b")
 	h.Shutdown(os.Stdout, os.Stderr)
 	// Output:
 	// 0: a
 	// 1: b
 }
 
-func ExampleDispatchAndExit() {
+func ExampleDispatchAndExitIfChild() {
+	// Child process dispatches to the echo program.
+	modules.DispatchAndExitIfChild()
+	// Parent process spawns the echo program.
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
-	// DispatchAndExit will call os.Exit(0) when executed within the child.
-	modules.DispatchAndExit()
 	sh, _ := modules.NewShell(ctx, nil, false, nil)
 	defer sh.Cleanup(nil, nil)
-	h, _ := sh.Start("echo", nil, "c", "d")
+	h, _ := sh.Start(nil, Echo, "c", "d")
 	h.Shutdown(os.Stdout, os.Stderr)
 	// Output:
 	// 0: c
diff --git a/test/modules/exec.go b/test/modules/exec.go
index 63e0b94..c85a2c9 100644
--- a/test/modules/exec.go
+++ b/test/modules/exec.go
@@ -21,20 +21,21 @@
 	"v.io/x/ref/test/expect"
 )
 
-// execHandle implements both the command and Handle interfaces.
+// execHandle implements both the Handle interface.
 type execHandle struct {
 	*expect.Session
-	mu        sync.Mutex
-	cmd       *exec.Cmd
-	name      string
-	handle    *vexec.ParentHandle
-	sh        *Shell
-	stderr    *os.File
-	stdout    io.ReadCloser
-	stdin     io.WriteCloser
-	procErrCh chan error
-	opts      *StartOpts
-	external  bool
+	mu         sync.Mutex
+	cmd        *exec.Cmd
+	entryPoint string
+	desc       string
+	handle     *vexec.ParentHandle
+	sh         *Shell
+	stderr     *os.File
+	stdout     io.ReadCloser
+	stdin      io.WriteCloser
+	procErrCh  chan error
+	opts       *StartOpts
+	external   bool
 }
 
 func testFlags() []string {
@@ -51,7 +52,7 @@
 	// must be a go test binary
 	val := timeout.Value.(flag.Getter).Get().(time.Duration)
 	if val.String() != timeout.DefValue {
-		// use supplied command value for subprocesses
+		// use supplied value for subprocesses
 		fl = append(fl, "--test.timeout="+timeout.Value.String())
 	} else {
 		// translate default value into 3m for subproccesses.  The
@@ -62,12 +63,12 @@
 	return fl
 }
 
-func newExecHandle(name string) *execHandle {
-	return &execHandle{name: name, procErrCh: make(chan error, 1)}
+func newExecHandle(entry, desc string) *execHandle {
+	return &execHandle{entryPoint: entry, desc: desc, procErrCh: make(chan error, 1)}
 }
 
-func newExecHandleForExternalCommand(name string) *execHandle {
-	return &execHandle{name: name, procErrCh: make(chan error, 1), external: true}
+func newExecHandleExternal(prog string) *execHandle {
+	return &execHandle{entryPoint: prog, desc: prog, procErrCh: make(chan error, 1), external: true}
 }
 
 func (eh *execHandle) Stdout() io.Reader {
@@ -96,7 +97,7 @@
 
 func (eh *execHandle) envelope(sh *Shell, env []string, args []string) ([]string, []string) {
 	if eh.external {
-		newargs := append([]string{eh.name}, args...)
+		newargs := append([]string{eh.entryPoint}, args...)
 		newenv := envvar.SliceToMap(env)
 		delete(newenv, shellEntryPoint)
 		return newargs, envvar.MapToSlice(newenv)
@@ -104,7 +105,7 @@
 	newargs := append([]string{os.Args[0]}, testFlags()...)
 	newargs = append(newargs, args...)
 	newenv := envvar.SliceToMap(env)
-	newenv[shellEntryPoint] = eh.name
+	newenv[shellEntryPoint] = eh.entryPoint
 	return newargs, envvar.MapToSlice(newenv)
 }
 
@@ -117,7 +118,7 @@
 	cmd := exec.Command(args[0], args[1:]...)
 	cmd.Env = env
 
-	stderr, err := newLogfile("stderr", eh.name)
+	stderr, err := newLogfile("stderr", eh.entryPoint)
 	if err != nil {
 		return nil, err
 	}
@@ -163,7 +164,7 @@
 		execOpts = append(execOpts, vexec.ConfigOpt{Config: config})
 	}
 
-	// TODO(cnicolaou): for external commands, vexec should either not be
+	// TODO(cnicolaou): for external programs, vexec should either not be
 	// used or it should taken an option to not use its protocol, and in
 	// particular to share secrets with children.
 	handle := vexec.NewParentHandle(cmd, execOpts...)
@@ -171,9 +172,9 @@
 	eh.stderr = stderr
 	eh.handle = handle
 	eh.cmd = cmd
-	vlog.VI(1).Infof("Start: %q stderr: %s", eh.name, stderr.Name())
-	vlog.VI(1).Infof("Start: %q args: %v", eh.name, cmd.Args)
-	vlog.VI(2).Infof("Start: %q env: %v", eh.name, cmd.Env)
+	vlog.VI(1).Infof("Start: %q stderr: %s", eh.desc, stderr.Name())
+	vlog.VI(1).Infof("Start: %q args: %v", eh.desc, cmd.Args)
+	vlog.VI(2).Infof("Start: %q env: %v", eh.desc, cmd.Env)
 	if err := handle.Start(); err != nil {
 		// The child process failed to start, either because of some setup
 		// error (e.g. creating pipes for it to use), or a bad binary etc.
@@ -194,7 +195,7 @@
 			return eh, err
 		}
 	}
-	vlog.VI(1).Infof("Started: %q, pid %d", eh.name, cmd.Process.Pid)
+	vlog.VI(1).Infof("Started: %q, pid %d", eh.desc, cmd.Process.Pid)
 	go func() {
 		eh.procErrCh <- eh.handle.Wait(0)
 		// It's now safe to close eh.stdout, since Wait only returns
@@ -215,8 +216,8 @@
 func (eh *execHandle) Shutdown(stdout, stderr io.Writer) error {
 	eh.mu.Lock()
 	defer eh.mu.Unlock()
-	vlog.VI(1).Infof("Shutdown: %q", eh.name)
-	defer vlog.VI(1).Infof("Shutdown: %q [DONE]", eh.name)
+	vlog.VI(1).Infof("Shutdown: %q", eh.desc)
+	defer vlog.VI(1).Infof("Shutdown: %q [DONE]", eh.desc)
 	if eh.stdin != nil {
 		eh.stdin.Close()
 	}
diff --git a/test/modules/modules_test.go b/test/modules/modules_test.go
index b0ce9e4..183aec6 100644
--- a/test/modules/modules_test.go
+++ b/test/modules/modules_test.go
@@ -32,98 +32,85 @@
 	_ "v.io/x/ref/runtime/factories/generic"
 )
 
-func init() {
-	modules.RegisterChild("envtest", "envtest: <variables to print>...", PrintFromEnv)
-	modules.RegisterChild("printenv", "printenv", PrintEnv)
-	modules.RegisterChild("printblessing", "printblessing", PrintBlessing)
-	modules.RegisterChild("echos", "[args]*", Echo)
-	modules.RegisterChild("errortestChild", "", ErrorMain)
-	modules.RegisterChild("ignores_stdin", "", ignoresStdin)
-	modules.RegisterChild("pipeEcho", "", pipeEcho)
-	modules.RegisterChild("lifo", "", lifo)
-}
-
-// We must call Testmain ourselves because using v23 test generate
+// We must call TestMain ourselves because using v23 test generate
 // creates an import cycle for this package.
 func TestMain(m *testing.M) {
 	test.Init()
-	if modules.IsModulesChildProcess() {
-		if err := modules.Dispatch(); err != nil {
-			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
-			os.Exit(1)
-		}
-		return
-	}
+	modules.DispatchAndExitIfChild()
 	os.Exit(m.Run())
 }
 
-func ignoresStdin(io.Reader, io.Writer, io.Writer, map[string]string, ...string) error {
+var ignoreStdin = modules.Register(func(*modules.Env, ...string) error {
 	<-time.After(time.Minute)
 	return nil
-}
+}, "ignoreStdin")
 
-func Echo(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var echo = modules.Register(func(env *modules.Env, args ...string) error {
 	for _, a := range args {
-		fmt.Fprintf(stdout, "stdout: %s\n", a)
-		fmt.Fprintf(stderr, "stderr: %s\n", a)
+		fmt.Fprintf(env.Stdout, "stdout: %s\n", a)
+		fmt.Fprintf(env.Stderr, "stderr: %s\n", a)
 	}
 	return nil
-}
+}, "Echo")
 
-func pipeEcho(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	scanner := bufio.NewScanner(stdin)
+var pipeEcho = modules.Register(pipeEchoFunc, "pipeEcho")
+
+func pipeEchoFunc(env *modules.Env, args ...string) error {
+	scanner := bufio.NewScanner(env.Stdin)
 	for scanner.Scan() {
-		fmt.Fprintf(stdout, "%p: %s\n", pipeEcho, scanner.Text())
+		fmt.Fprintf(env.Stdout, "%p: %s\n", pipeEchoFunc, scanner.Text())
 	}
 	return nil
 }
 
-func lifo(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
-	scanner := bufio.NewScanner(stdin)
+var lifo = modules.Register(lifoFunc, "lifo")
+
+func lifoFunc(env *modules.Env, args ...string) error {
+	scanner := bufio.NewScanner(env.Stdin)
 	scanner.Scan()
 	msg := scanner.Text()
-	modules.WaitForEOF(stdin)
-	fmt.Fprintf(stdout, "%p: %s\n", lifo, msg)
+	modules.WaitForEOF(env.Stdin)
+	fmt.Fprintf(env.Stdout, "%p: %s\n", lifoFunc, msg)
 	return nil
 }
 
-func PrintBlessing(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var printBlessing = modules.Register(func(env *modules.Env, args ...string) error {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
 	blessing := v23.GetPrincipal(ctx).BlessingStore().Default()
-	fmt.Fprintf(stdout, "%s", blessing)
+	fmt.Fprintf(env.Stdout, "%s", blessing)
 	return nil
-}
+}, "printBlessing")
 
-func PrintFromEnv(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var envTest = modules.Register(func(env *modules.Env, args ...string) error {
 	for _, a := range args {
-		if v := env[a]; len(v) > 0 {
-			fmt.Fprintf(stdout, "%s\n", a+"="+v)
+		if v := env.Vars[a]; len(v) > 0 {
+			fmt.Fprintf(env.Stdout, "%s\n", a+"="+v)
 		} else {
-			fmt.Fprintf(stderr, "missing %s\n", a)
+			fmt.Fprintf(env.Stderr, "missing %s\n", a)
 		}
 	}
-	modules.WaitForEOF(stdin)
-	fmt.Fprintf(stdout, "done\n")
+	modules.WaitForEOF(env.Stdin)
+	fmt.Fprintf(env.Stdout, "done\n")
 	return nil
-}
+}, "envTest")
 
 const printEnvArgPrefix = "PRINTENV_ARG="
 
-func PrintEnv(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var printEnv = modules.Register(func(env *modules.Env, args ...string) error {
 	for _, a := range args {
-		fmt.Fprintf(stdout, "%s%s\n", printEnvArgPrefix, a)
+		fmt.Fprintf(env.Stdout, "%s%s\n", printEnvArgPrefix, a)
 	}
-	for k, v := range env {
-		fmt.Fprintf(stdout, "%q\n", k+"="+v)
+	for k, v := range env.Vars {
+		fmt.Fprintf(env.Stdout, "%q\n", k+"="+v)
 	}
 	return nil
-}
+}, "printEnv")
 
-func ErrorMain(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var errorMain = modules.Register(func(env *modules.Env, args ...string) error {
 	return fmt.Errorf("an error")
-}
+}, "errorMain")
 
 func waitForInput(scanner *bufio.Scanner) bool {
 	ch := make(chan struct{})
@@ -139,8 +126,8 @@
 	}
 }
 
-func testCommand(t *testing.T, sh *modules.Shell, name, key, val string) {
-	h, err := sh.Start(name, nil, key)
+func testProgram(t *testing.T, sh *modules.Shell, prog modules.Program, key, val string) {
+	h, err := sh.Start(nil, prog, key)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -180,7 +167,7 @@
 }
 
 func getBlessing(t *testing.T, sh *modules.Shell, env ...string) string {
-	h, err := sh.Start("printblessing", env)
+	h, err := sh.Start(env, printBlessing)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -193,7 +180,7 @@
 }
 
 func getCustomBlessing(t *testing.T, sh *modules.Shell, creds *modules.CustomCredentials) string {
-	h, err := sh.StartWithOpts(sh.DefaultStartOpts().WithCustomCredentials(creds), nil, "printblessing")
+	h, err := sh.StartWithOpts(sh.DefaultStartOpts().WithCustomCredentials(creds), nil, printBlessing)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -216,7 +203,7 @@
 	defer sh.Cleanup(nil, nil)
 	key, val := "simpleVar", "foo & bar"
 	sh.SetVar(key, val)
-	testCommand(t, sh, "envtest", key, val)
+	testProgram(t, sh, envTest, key, val)
 }
 
 func TestAgent(t *testing.T) {
@@ -367,8 +354,8 @@
 	defer sh.Cleanup(os.Stderr, os.Stderr)
 	key, val := "simpleVar", "foo & bar"
 	sh.SetVar(key, val)
-	testCommand(t, sh, "envtest", key, val)
-	_, err = sh.Start("non-existent-command", nil, "random", "args")
+	testProgram(t, sh, envTest, key, val)
+	_, err = sh.Start(nil, modules.Program("non-existent-program"), "random", "args")
 	if err == nil {
 		fmt.Fprintf(os.Stderr, "Failed: %v\n", err)
 		t.Fatalf("expected error")
@@ -384,7 +371,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(nil, nil)
-	h, err := sh.Start("errortestChild", nil)
+	h, err := sh.Start(nil, errorMain)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -393,10 +380,10 @@
 	}
 }
 
-func testShutdown(t *testing.T, sh *modules.Shell, command string, isfunc bool) {
+func testShutdown(t *testing.T, sh *modules.Shell, prog modules.Program) {
 	result := ""
 	args := []string{"a", "b c", "ddd"}
-	if _, err := sh.Start(command, nil, args...); err != nil {
+	if _, err := sh.Start(nil, prog, args...); err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
 	var stdoutBuf bytes.Buffer
@@ -410,9 +397,7 @@
 	if got, want := stdoutBuf.String(), stdoutOutput+result; got != want {
 		t.Errorf("got %q want %q", got, want)
 	}
-	if !isfunc {
-		stderrBuf.ReadString('\n') // Skip past the random # generator output
-	}
+	stderrBuf.ReadString('\n') // Skip past the random # generator output
 	if got, want := stderrBuf.String(), stderrOutput; got != want {
 		t.Errorf("got %q want %q", got, want)
 	}
@@ -427,13 +412,13 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(nil, nil)
-	testShutdown(t, sh, "echos", false)
+	testShutdown(t, sh, echo)
 }
 
-// TestShutdownSubprocessIgnoresStdin verifies that Shutdown doesn't wait
+// TestShutdownSubprocessIgnoreStdin verifies that Shutdown doesn't wait
 // forever if a child does not die upon closing stdin; but instead times out and
 // returns an appropriate error.
-func TestShutdownSubprocessIgnoresStdin(t *testing.T) {
+func TestShutdownSubprocessIgnoreStdin(t *testing.T) {
 	ctx, shutdown := test.InitForTest()
 	defer shutdown()
 
@@ -443,7 +428,7 @@
 	}
 	opts := sh.DefaultStartOpts()
 	opts.ShutdownTimeout = time.Second
-	h, err := sh.StartWithOpts(opts, nil, "ignores_stdin")
+	h, err := sh.StartWithOpts(opts, nil, ignoreStdin)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -472,7 +457,7 @@
 	}
 	opts := sh.DefaultStartOpts()
 	opts.ShutdownTimeout = time.Second
-	h, err := sh.StartWithOpts(opts, nil, "ignores_stdin")
+	h, err := sh.StartWithOpts(opts, nil, ignoreStdin)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -519,7 +504,7 @@
 	sh.SetVar("a", "1")
 	sh.SetVar("b", "2")
 	args := []string{"oh", "ah"}
-	h, err := sh.Start("printenv", nil, args...)
+	h, err := sh.Start(nil, printEnv, args...)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -533,7 +518,7 @@
 			childEnv = append(childEnv, o)
 		}
 	}
-	shArgs, shEnv := sh.CommandEnvelope("printenv", nil, args...)
+	shArgs, shEnv := sh.ProgramEnvelope(nil, printEnv, args...)
 	for i, ev := range shEnv {
 		shEnv[i] = fmt.Sprintf("%q", ev)
 	}
@@ -575,7 +560,7 @@
 	os.Setenv("a", "wrong, should be 1")
 	sh.SetVar("b", "2 also wrong")
 	os.Setenv("b", "wrong, should be 2")
-	h, err := sh.Start("printenv", []string{"b=2"})
+	h, err := sh.Start([]string{"b=2"}, printEnv)
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -604,7 +589,7 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	defer sh.Cleanup(nil, nil)
-	h, err := sh.StartWithOpts(sh.DefaultStartOpts().NoExecCommand(), nil, "/bin/echo", "hello", "world")
+	h, err := sh.StartWithOpts(sh.DefaultStartOpts().NoExecProgram(), nil, modules.Program("/bin/echo"), "hello", "world")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -626,7 +611,7 @@
 	defer sh.Cleanup(nil, nil)
 	cookie := strconv.Itoa(rand.Int())
 	sh.SetConfigKey("cookie", cookie)
-	h, err := sh.StartWithOpts(sh.DefaultStartOpts().ExternalCommand(), nil, os.Args[0], "--test.run=TestExternalTestHelper")
+	h, err := sh.StartWithOpts(sh.DefaultStartOpts().ExternalProgram(), nil, modules.Program(os.Args[0]), "--test.run=TestExternalTestHelper")
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
@@ -669,7 +654,7 @@
 	}
 	opts := sh.DefaultStartOpts()
 	opts.Stdin = r
-	h, err := sh.StartWithOpts(opts, nil, "pipeEcho")
+	h, err := sh.StartWithOpts(opts, nil, pipeEcho)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -682,8 +667,8 @@
 
 	scanner := bufio.NewScanner(h.Stdout())
 	want := []string{
-		fmt.Sprintf("%p: hello world", pipeEcho),
-		fmt.Sprintf("%p: %s", pipeEcho, cookie),
+		fmt.Sprintf("%p: hello world", pipeEchoFunc),
+		fmt.Sprintf("%p: %s", pipeEchoFunc, cookie),
 	}
 	i := 0
 	for scanner.Scan() {
@@ -712,7 +697,7 @@
 
 	cases := []string{"a", "b", "c"}
 	for _, msg := range cases {
-		h, err := sh.Start("lifo", nil)
+		h, err := sh.Start(nil, lifo)
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -728,7 +713,7 @@
 	}
 	sort.Sort(sort.Reverse(sort.StringSlice(cases)))
 	for i, msg := range cases {
-		if got, want := lines[i], fmt.Sprintf("%p: %s", lifo, msg); got != want {
+		if got, want := lines[i], fmt.Sprintf("%p: %s", lifoFunc, msg); got != want {
 			t.Fatalf("got %v, want %v", got, want)
 		}
 	}
@@ -789,13 +774,13 @@
 		t.Fatalf("unexpected error: %s", err)
 	}
 	opts := sh.DefaultStartOpts()
-	opts = opts.NoExecCommand()
+	opts = opts.NoExecProgram()
 	creds, err := sh.NewCustomCredentials()
 	if err != nil {
 		t.Fatalf("unexpected error: %s", err)
 	}
 	opts = opts.WithCustomCredentials(creds)
-	h, err := sh.StartWithOpts(opts, nil, "echos", "a")
+	h, err := sh.StartWithOpts(opts, nil, echo, "a")
 
 	if got, want := err, modules.ErrNoExecAndCustomCreds; got != want {
 		t.Fatalf("got %v, want %v", got, want)
diff --git a/test/modules/registry.go b/test/modules/registry.go
index 877d1ba..ed4c871 100644
--- a/test/modules/registry.go
+++ b/test/modules/registry.go
@@ -10,125 +10,109 @@
 	"io"
 	"io/ioutil"
 	"os"
-	"strings"
-	"sync"
+	"path/filepath"
+	"runtime"
+	"strconv"
 	"time"
 
-	"v.io/x/lib/envvar"
 	"v.io/x/lib/vlog"
-
 	vexec "v.io/x/ref/lib/exec"
 )
 
-type commandDesc struct {
-	factory func() *execHandle
+// Program is a symbolic representation of a registered Main function.
+type Program string
+
+type programInfo struct {
 	main    Main
-	help    string
+	factory func() *execHandle
 }
 
-type cmdRegistry struct {
-	sync.Mutex
-	cmds map[string]*commandDesc
+type programRegistry struct {
+	programs []*programInfo
 }
 
-var registry = &cmdRegistry{cmds: make(map[string]*commandDesc)}
+var registry = new(programRegistry)
 
-func (r *cmdRegistry) addCommand(name, help string, factory func() *execHandle, main Main) {
-	r.Lock()
-	defer r.Unlock()
-	r.cmds[name] = &commandDesc{factory, main, help}
+func (r *programRegistry) addProgram(main Main, description string) Program {
+	prog := strconv.Itoa(len(r.programs))
+	factory := func() *execHandle { return newExecHandle(prog, description) }
+	r.programs = append(r.programs, &programInfo{main, factory})
+	return Program(prog)
 }
 
-func (r *cmdRegistry) getCommand(name string) *commandDesc {
-	r.Lock()
-	defer r.Unlock()
-	return r.cmds[name]
+func (r *programRegistry) getProgram(prog Program) *programInfo {
+	index, err := strconv.Atoi(string(prog))
+	if err != nil || index < 0 || index >= len(r.programs) {
+		return nil
+	}
+	return r.programs[index]
 }
 
-func (r *cmdRegistry) getExternalCommand(name string) *commandDesc {
-	h := newExecHandleForExternalCommand(name)
-	return &commandDesc{
+func (r *programRegistry) getExternalProgram(prog Program) *programInfo {
+	h := newExecHandleExternal(string(prog))
+	return &programInfo{
 		factory: func() *execHandle { return h },
 	}
 }
 
-// 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) {
-	factory := func() *execHandle { return newExecHandle(name) }
-	registry.addCommand(name, help, factory, main)
+func (r *programRegistry) String() string {
+	var s string
+	for _, info := range r.programs {
+		h := info.factory()
+		s += fmt.Sprintf("%s: %s\n", h.entryPoint, h.desc)
+	}
+	return s
 }
 
-// Help returns the help message for the specified command, or a list
-// of all commands if the command parameter is an empty string.
-func Help(command string) string {
-	return registry.help(command)
+// Register adds a new program to the registry that will be run as a subprocess.
+// It must be called before Dispatch is called, typically from an init function.
+func Register(main Main, description string) Program {
+	if _, file, line, ok := runtime.Caller(1); ok {
+		description = fmt.Sprintf("%s:%d %s", shortFile(file), line, description)
+	}
+	return registry.addProgram(main, description)
 }
 
-func (r *cmdRegistry) help(command string) string {
-	r.Lock()
-	defer r.Unlock()
-	if len(command) == 0 {
-		h := ""
-		for c, _ := range r.cmds {
-			h += c + ", "
-		}
-		return strings.TrimRight(h, ", ")
+// shortFile returns the last 3 components of the given file name.
+func shortFile(file string) string {
+	var short string
+	for i := 0; i < 3; i++ {
+		short = filepath.Join(filepath.Base(file), short)
+		file = filepath.Dir(file)
 	}
-	if c := r.cmds[command]; c != nil {
-		return command + ": " + c.help
-	}
-	return ""
+	return short
 }
 
 const shellEntryPoint = "V23_SHELL_HELPER_PROCESS_ENTRY_POINT"
 
-// IsModulesChildProcess returns true if this process was started by
-// the modules package.
-func IsModulesChildProcess() bool {
+// IsChildProcess returns true if this process was started by the modules
+// package.
+func IsChildProcess() bool {
 	return os.Getenv(shellEntryPoint) != ""
 }
 
-// Dispatch will execute the requested subprocess command from a within a
-// a subprocess. It will return without an error if it is executed by a
-// process that does not specify an entry point in its environment.
-//
-// func main() {
-//     if modules.IsModulesChildProcess() {
-//         if err := modules.Dispatch(); err != nil {
-//             panic("error")
-//         }
-//         eturn
-//     }
-//     parent code...
-//
+// Dispatch executes the requested subprocess program from within a subprocess.
+// Returns an error if it is executed by a process that does not specify an
+// entry point in its environment.
 func Dispatch() error {
-	if !IsModulesChildProcess() {
-		return nil
-	}
 	return registry.dispatch()
 }
 
-// DispatchAndExit is like Dispatch except that it will call os.Exit(0)
-// when executed within a child process and the command succeeds, or panic
-// on encountering an error.
-//
-// func main() {
-//     modules.DispatchAndExit()
-//     parent code...
-//
-func DispatchAndExit() {
-	if !IsModulesChildProcess() {
-		return
+// DispatchAndExitIfChild is a convenience function with three possible results:
+//   * os.Exit(0) if called within a child process, and the dispatch succeeds.
+//   * os.Exit(1) if called within a child process, and the dispatch fails.
+//   * return with no side-effects, if not called within a child process.
+func DispatchAndExitIfChild() {
+	if IsChildProcess() {
+		if err := Dispatch(); err != nil {
+			fmt.Fprintf(os.Stderr, "modules.Dispatch failed: %v\n", err)
+			os.Exit(1)
+		}
+		os.Exit(0)
 	}
-	if err := registry.dispatch(); err != nil {
-		panic(fmt.Sprintf("unexpected error: %s", err))
-	}
-	os.Exit(0)
 }
 
-func (r *cmdRegistry) dispatch() error {
+func (r *programRegistry) dispatch() error {
 	ch, err := vexec.GetChildHandle()
 	if err != nil {
 		// This is for debugging only. It's perfectly reasonable for this
@@ -140,18 +124,18 @@
 	// Only signal that the child is ready or failed if we successfully get
 	// a child handle. We most likely failed to get a child handle
 	// because the subprocess was run directly from the command line.
-	command := os.Getenv(shellEntryPoint)
-	if len(command) == 0 {
-		err := fmt.Errorf("Failed to find entrypoint %q", command)
+	prog := os.Getenv(shellEntryPoint)
+	if prog == "" {
+		err := fmt.Errorf("Failed to find entrypoint %q", prog)
 		if ch != nil {
 			ch.SetFailed(err)
 		}
 		return err
 	}
 
-	m := registry.getCommand(command)
+	m := registry.getProgram(Program(prog))
 	if m == nil {
-		err := fmt.Errorf("%s: not registered", command)
+		err := fmt.Errorf("%s: not registered\n%s", prog, registry.String())
 		if ch != nil {
 			ch.SetFailed(err)
 		}
@@ -173,7 +157,7 @@
 	}(os.Getppid())
 
 	flag.Parse()
-	return m.main(os.Stdin, os.Stdout, os.Stderr, envvar.SliceToMap(os.Environ()), flag.Args()...)
+	return m.main(EnvFromOS(), flag.Args()...)
 }
 
 // WaitForEOF returns when a read on its io.Reader parameter returns io.EOF
diff --git a/test/modules/shell.go b/test/modules/shell.go
index ca4c53d..34401c8 100644
--- a/test/modules/shell.go
+++ b/test/modules/shell.go
@@ -4,17 +4,17 @@
 
 // Package modules implements a mechanism for running commonly used services as
 // subprocesses, and client functionality for accessing those services.  Such
-// services and functions are collectively called 'commands' and are managed by
+// services and functions are collectively called 'programs' and are managed by
 // a 'Registry'. The Shell is analagous to the UNIX shell and maintains a key,
 // value store of environment variables and config settings that are accessible
-// to the commands that it hosts. Simple variable expansion is supported.
+// to the programs that it hosts. Simple variable expansion is supported.
 //
-// Three types of 'commands' may be invoked via a Shell:
+// Three types of 'programs' may be invoked via a Shell:
 //
-//   1) Functions of type Shell.Main as subprocesses via fork/exec.
-//   2) Arbitrary non-Vanadium commands available on the underlying operating
+//   1) Functions of type Main as subprocesses via fork/exec.
+//   2) Arbitrary non-Vanadium programs available on the underlying operating
 //      system such as '/bin/cp', 'bash' etc.
-//   3) Arbitrary Vanadium commands available on the underlying operating system
+//   3) Arbitrary Vanadium programs available on the underlying operating system
 //      such as precompiled Vanadium services.
 //
 // The first type requires that the function to be executed is compiled into the
@@ -22,8 +22,8 @@
 // a single, per-process, registry.
 //
 // The second two types allow for arbitrary binaries to be executed. The
-// distinction between a Vanadium and non-Vanadium command is that the Vanadium
-// command implements the protocol used by v.io/x/ref/lib/exec package to
+// distinction between a Vanadium and non-Vanadium program is that the Vanadium
+// program implements the protocol used by v.io/x/ref/lib/exec package to
 // synchronise between the parent and child processes and to share information
 // such as the ConfigKey key,value store supported by the Shell, a shared
 // secret, shared file descriptors etc.
@@ -36,23 +36,20 @@
 //
 // The registry provides the following functions:
 //
-//   RegisterChild: generally called from an init function to register a
-//     shell.Main to be executed in a subprocess by fork/exec'ing the calling
-//     process.
-//   Dispatch: which must be called in the child process to lookup the requested
-//     function in the registry and to invoke it. This will typically be called
-//     from a TestMain. modules.IsModulesChildProcess can be used to determine
-//     if the calling process is a child started via this package.
+//   Register: registers a Main function to be executed in a subprocess,
+//     the returned Program is typically assigned to a global variable.
+//   Dispatch: must be called in the child process to lookup and invoke the
+//     requested function.  Typically called from TestMain.
 //
-// The v23 tool can automate generation of TestMain and calls to RegisterChild.
-// Adding the comment below to a test file will generate the appropriate code.
+// The v23 tool can automate generation of TestMain.  Adding the comment below
+// to a test file will generate the appropriate code.
 //
 //   //go:generate v23 test generate .
 //
 // Use 'v23 test generate --help' to get a complete explanation.
 //
-// In all cases commands are started by invoking the StartWithOpts method on the
-// Shell with the name of the command to run. An instance of the Handle
+// In all cases programs are started by invoking the StartWithOpts method on the
+// Shell with the name of the program to run. An instance of the Handle
 // interface is returned which can be used to interact with the function or
 // subprocess, and in particular to read/write data from/to it using io channels
 // that follow the stdin, stdout, stderr convention. The StartOpts struct is
@@ -61,20 +58,19 @@
 // StartOpts and for common uses of StartWithOpts.
 //
 // Each successful call to StartWithOpts returns a handle representing the
-// running command. This handle can be used to gain access to that command's
+// running program. This handle can be used to gain access to that program's
 // stdin, stdout, stderr and to request or synchronize with its termination via
 // the Shutdown method. The Shutdown method can optionally be used to read any
-// remaining output from the commands stdout and stderr.  The Shell maintains a
+// remaining output from the programs stdout and stderr.  The Shell maintains a
 // record of all such handles and will call Shutdown on them in LIFO order when
 // the Shell's Cleanup method is called.
 //
-// A simple protocol must be followed by all modules.Main commands, in
-// particular, they should wait for their stdin stream to be closed before
-// exiting. The caller can then coordinate with any command by writing to that
-// stdin stream and reading responses from the stdout stream, and it can close
-// stdin when it's ready for the command to exit using the CloseStdin method on
-// the command's handle. Any binary or script that follows this protocol can be
-// used as well.
+// A simple protocol must be followed by all programs, in particular, they
+// should wait for their stdin stream to be closed before exiting. The caller
+// can then coordinate with any program by writing to that stdin stream and
+// reading responses from the stdout stream, and it can close stdin when it's
+// ready for the program to exit using the CloseStdin method on the program's
+// handle. Any binary or script that follows this protocol can be used as well.
 //
 // By default, every Shell created by NewShell starts a security agent to manage
 // principals for child processes. These default credentials can be overridden
@@ -82,12 +78,12 @@
 // environment provided as a parameter to the StartWithOpts method. It is also
 // possible to specify custom credentials via StartOpts.
 //
-// Interacting with Commands
+// Interacting with Programs
 //
 // Handle.Stdout(), Stdin(), Stderr():
 //
 // StartWithOpts returns a Handle which can be used to interact with the running
-// command. In particular, its Stdin() and Stdout() methods give access to the
+// program. In particular, its Stdin() and Stdout() methods give access to the
 // running process' corresponding stdin and stdout and hence can be used to
 // communicate with it. Stderr is handled differently and is configured so that
 // the child's stderr is written to a log file rather than a pipe. This is in
@@ -96,9 +92,9 @@
 //
 // Handle.Shutdown(stdout, stderr io.Writer):
 //
-// The Shutdown method is used to gracefully shutdown a command and to
+// The Shutdown method is used to gracefully shutdown a program and to
 // synchronise with its termination. In particular, Shutdown can be used to read
-// any unread output from the command's stdout and stderr. Note that since
+// any unread output from the program's stdout and stderr. Note that since
 // Stderr is buffered to a file, Shutdown is able to return the entire contents
 // of that file. This is useful for debugging misbehaving/crashing child
 // processes.
@@ -107,32 +103,32 @@
 //
 // The Shell keeps track of all Handles that it has issued and in particular if
 // Shutdown (or Forget) have not been called, it will call Shutdown for each
-// such Handle in LIFO order. This ensures that all commands will be Shutdown
+// such Handle in LIFO order. This ensures that all programs will be Shutdown
 // even if the developer does not explicitly take care to do so for every
 // invocation.
 //
 // Pipes:
 //
-// StartWithOpts allows the caller to pass an io.Reader to the command
+// StartWithOpts allows the caller to pass an io.Reader to the program
 // (StartOpts.Stdin) for it to read from, rather than creating a new pipe
-// internally. This makes it possible to connect the output of one command to
+// internally. This makes it possible to connect the output of one program to
 // the input of another directly.
 //
 // Command Line Arguments:
 //
 // The arguments passed in calls to Start are appended to any system required
 // ones (e.g. for propagating test timeouts, verbosity etc) and the child
-// process will call the command with the result of flag.Args(). In this way the
+// process will call the program with the result of flag.Args(). In this way the
 // caller can provide flags used by libraries in the child process as well as
-// those specific to the command and the command will only receive the args
+// those specific to the program and the program will only receive the args
 // specific to it. The usual "--" convention can be used to override this
 // default behaviour.
 //
 // Caveats:
 //
-// Handle.Shutdown assumes that the child command/process will terminate when
+// Handle.Shutdown assumes that the child program/process will terminate when
 // its stdin stream is closed. This assumption is unlikely to be valid for
-// 'external' commands (e.g. /bin/cp) and in these cases Kill or some other
+// 'external' programs (e.g. /bin/cp) and in these cases Kill or some other
 // application specific mechanism will need to be used.
 package modules
 
@@ -172,7 +168,7 @@
 	ExecProtocol:    true,
 }
 
-// Shell represents the context within which commands are run.
+// Shell represents the context within which programs are run.
 type Shell struct {
 	mu               sync.Mutex
 	env              map[string]string
@@ -350,22 +346,30 @@
 	return creds, nil
 }
 
-type Main func(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error
-
-// String returns a string representation of the Shell, which is a
-// list of the commands currently available in the shell.
-func (sh *Shell) String() string {
-	return registry.help("")
+// Env represents the environment for Main functions.
+type Env struct {
+	Stdin  io.Reader
+	Stdout io.Writer
+	Stderr io.Writer
+	Vars   map[string]string // Environment variables
 }
 
-// Help returns the help message for the specified command.
-func (sh *Shell) Help(command string) string {
-	return registry.help(command)
+// EnvFromOS returns a new Env based on the underlying OS.
+func EnvFromOS() *Env {
+	return &Env{
+		Stdin:  os.Stdin,
+		Stdout: os.Stdout,
+		Stderr: os.Stderr,
+		Vars:   envvar.SliceToMap(os.Environ()),
+	}
 }
 
+// Main describes the entry-point function type for registered programs.
+type Main func(env *Env, args ...string) error
+
 // Start is shorthand for StartWithOpts(sh.DefaultStartOpts(), ...)
-func (sh *Shell) Start(name string, env []string, args ...string) (Handle, error) {
-	return sh.StartWithOpts(sh.DefaultStartOpts(), env, name, args...)
+func (sh *Shell) Start(env []string, prog Program, args ...string) (Handle, error) {
+	return sh.StartWithOpts(sh.DefaultStartOpts(), env, prog, args...)
 }
 
 // StartOpts represents the options that can be passed to the
@@ -397,11 +401,11 @@
 	Credentials *CustomCredentials
 	// ExecProtocol indicates whether the child process is expected to
 	// implement the v.io/x.ref/lib/exec parent/child protocol.
-	// It should be set to false when running non-vanadium commands
+	// It should be set to false when running non-vanadium programs
 	// (e.g. /bin/cp).
 	ExecProtocol bool
-	// External indicates if the command is an external process rather than
-	// a Shell.Main function.
+	// External indicates if the program is an external process rather than
+	// a Main function.
 	External bool
 	// StartTimeout specifies the amount of time to wait for the
 	// child process to signal its correct intialization for Vanadium
@@ -451,37 +455,37 @@
 	return opts
 }
 
-// NoExecCommand returns a copy of opts with the External option
+// NoExecProgram returns a copy of opts with the External option
 // enabled and ExecProtocol disabled.
-func (opts StartOpts) NoExecCommand() StartOpts {
+func (opts StartOpts) NoExecProgram() StartOpts {
 	opts.External = true
 	opts.ExecProtocol = false
 	return opts
 }
 
-// ExternalCommand returns a copy of StartOpts with the
+// ExternalProgram returns a copy of StartOpts with the
 // External option enabled.
-func (opts StartOpts) ExternalCommand() StartOpts {
+func (opts StartOpts) ExternalProgram() StartOpts {
 	opts.External = true
 	return opts
 }
 
 var (
-	ErrNotRegistered        = errors.New("command not registered")
+	ErrNotRegistered        = errors.New("program not registered")
 	ErrNoExecAndCustomCreds = errors.New("ExecProtocol set to false but this invocation is attempting to use custome credentials")
 )
 
-// StartWithOpts starts the specified command according to the supplied
+// StartWithOpts starts the specified program according to the supplied
 // StartOpts and returns a Handle which can be used for interacting with
-// that command.
+// that program.
 //
-// The environment variables for the command are set by merging variables
+// The environment variables for the program are set by merging variables
 // from the OS environment, those in this Shell and those provided as a
 // parameter to it. In general, it prefers values from its parameter over
 // those from the Shell, over those from the OS. However, the VeyronCredentials
 // and agent FdEnvVar variables will never use the value from the Shell or OS.
 //
-// If the shell is managing principals, the command is configured to
+// If the shell is managing principals, the program is configured to
 // connect to the shell's agent. Custom credentials may be specified
 // via StartOpts. If the shell is not managing principals, set
 // the VeyronCredentials environment variable in the 'env' parameter.
@@ -489,7 +493,7 @@
 // The Shell tracks all of the Handles that it creates so that it can shut
 // them down when asked to. The returned Handle may be non-nil even when an
 // error is returned, in which case it may be used to retrieve any output
-// from the failed command.
+// from the failed program.
 //
 // StartWithOpts will return a valid handle for errors that occur during the
 // child processes startup process. It is thus possible to call Shutdown
@@ -504,16 +508,16 @@
 //        }
 //        t.Fatal(err)
 //    }
-func (sh *Shell) StartWithOpts(opts StartOpts, env []string, name string, args ...string) (Handle, error) {
+func (sh *Shell) StartWithOpts(opts StartOpts, env []string, prog Program, args ...string) (Handle, error) {
 	var err error
 	if opts.Error != nil {
 		return nil, opts.Error
 	}
 
-	var desc *commandDesc
+	var info *programInfo
 	if opts.External {
-		desc = registry.getExternalCommand(name)
-	} else if desc = registry.getCommand(name); desc == nil {
+		info = registry.getExternalProgram(prog)
+	} else if info = registry.getProgram(prog); info == nil {
 		return nil, ErrNotRegistered
 	}
 
@@ -536,8 +540,8 @@
 		}
 	}
 
-	handle := desc.factory()
-	h, err := handle.start(sh, p, &opts, sh.setupCommandEnv(env), sh.expand(args))
+	handle := info.factory()
+	h, err := handle.start(sh, p, &opts, sh.setupProgramEnv(env), sh.expand(args))
 	if err != nil {
 		return h, err
 	}
@@ -548,19 +552,19 @@
 	return h, nil
 }
 
-// CommandEnvelope returns the command line and environment that would be used
+// ProgramEnvelope returns the command line and environment that would be used
 // for running the subprocess if it were started with the specifed arguments.
-func (sh *Shell) CommandEnvelope(name string, env []string, args ...string) ([]string, []string) {
-	desc := registry.getCommand(name)
-	if desc == nil {
+func (sh *Shell) ProgramEnvelope(env []string, prog Program, args ...string) ([]string, []string) {
+	info := registry.getProgram(prog)
+	if info == nil {
 		return []string{}, []string{}
 	}
-	return desc.factory().envelope(sh, sh.setupCommandEnv(env), sh.expand(args))
+	return info.factory().envelope(sh, sh.setupProgramEnv(env), sh.expand(args))
 }
 
 // Forget tells the Shell to stop tracking the supplied Handle. This is
 // generally used when the application wants to control the order that
-// commands are shutdown in.
+// programs are shutdown in.
 func (sh *Shell) Forget(h Handle) {
 	sh.mu.Lock()
 	if handle, ok := h.(*execHandle); ok {
@@ -670,7 +674,7 @@
 	var err error
 	for i := len(handles); i > 0; i-- {
 		h := handles[i-1]
-		writeMsg("---- Cleanup calling Shutdown on command %q\n", h.name)
+		writeMsg("---- Cleanup calling Shutdown on program %q\n", h.desc)
 		cerr := h.Shutdown(stdout, stderr)
 		if cerr != nil {
 			err = cerr
@@ -682,7 +686,7 @@
 				return ": error: " + err.Error()
 			}
 		}
-		writeMsg("---- Shutdown on command %q%s\n", h.name, fn())
+		writeMsg("---- Shutdown on program %q%s\n", h.desc, fn())
 	}
 
 	if sh.cancelCtx != nil {
@@ -697,7 +701,7 @@
 	return err
 }
 
-func (sh *Shell) setupCommandEnv(env []string) []string {
+func (sh *Shell) setupProgramEnv(env []string) []string {
 	osmap := envvar.SliceToMap(os.Environ())
 	evmap := envvar.SliceToMap(env)
 
@@ -731,34 +735,34 @@
 	Error() error
 }
 
-// Handle represents a running command.
+// Handle represents a running program.
 type Handle interface {
 	ExpectSession
 
-	// Stdout returns a reader to the running command's stdout stream.
+	// Stdout returns a reader to the running program's stdout stream.
 	Stdout() io.Reader
 
-	// Stderr returns a reader to the running command's stderr
+	// Stderr returns a reader to the running program's stderr
 	// stream.
 	Stderr() io.Reader
 
-	// Stdin returns a writer to the running command's stdin. The
-	// convention is for commands to wait for stdin to be closed before
+	// Stdin returns a writer to the running program's stdin. The
+	// convention is for programs to wait for stdin to be closed before
 	// they exit, thus the caller should close stdin when it wants the
-	// command to exit cleanly.
+	// program to exit cleanly.
 	Stdin() io.Writer
 
 	// CloseStdin closes stdin in a manner that avoids a data race
 	// between any current readers on it.
 	CloseStdin()
 
-	// Shutdown closes the Stdin for the command and then reads output
-	// from the command's stdout until it encounters EOF, waits for
-	// the command to complete and then reads all of its stderr output.
+	// Shutdown closes the Stdin for the program and then reads output
+	// from the program's stdout until it encounters EOF, waits for
+	// the program to complete and then reads all of its stderr output.
 	// The stdout and stderr contents are written to the corresponding
 	// io.Writers if they are non-nil, otherwise the content is discarded.
 	Shutdown(stdout, stderr io.Writer) error
 
-	// Pid returns the pid of the process running the command
+	// Pid returns the pid of the process running the program
 	Pid() int
 }
diff --git a/test/modules/util.go b/test/modules/util.go
index db463a1..642abfe 100644
--- a/test/modules/util.go
+++ b/test/modules/util.go
@@ -5,13 +5,11 @@
 package modules
 
 import (
-	"flag"
 	"fmt"
 	"hash/adler32"
 	"io"
 	"io/ioutil"
 	"os"
-	"strings"
 
 	"v.io/v23/security"
 	"v.io/x/lib/vlog"
@@ -60,19 +58,3 @@
 	}
 	return p, nil
 }
-
-// Usage generates a usage string based on the flags in a flagset.
-func Usage(fs *flag.FlagSet) string {
-	res := []string{}
-	fs.VisitAll(func(f *flag.Flag) {
-		format := "  -%s=%s: %s"
-		if getter, ok := f.Value.(flag.Getter); ok {
-			if _, ok := getter.Get().(string); ok {
-				// put quotes on the value
-				format = "  -%s=%q: %s"
-			}
-		}
-		res = append(res, fmt.Sprintf(format, f.Name, f.DefValue, f.Usage))
-	})
-	return strings.Join(res, "\n") + "\n"
-}
diff --git a/test/v23tests/binary.go b/test/v23tests/binary.go
index c4dd0cc..ec770e3 100644
--- a/test/v23tests/binary.go
+++ b/test/v23tests/binary.go
@@ -11,7 +11,6 @@
 	"strings"
 
 	"v.io/x/lib/vlog"
-
 	"v.io/x/ref/test/modules"
 )
 
@@ -65,7 +64,7 @@
 		opts.Credentials, opts.Error = b.env.shell.NewChildCredentials("child")
 	}
 	opts.ExpectTesting = b.env.TB
-	handle, err := b.env.shell.StartWithOpts(opts, b.envVars, b.Path(), args...)
+	handle, err := b.env.shell.StartWithOpts(opts, b.envVars, modules.Program(b.Path()), args...)
 	if err != nil {
 		if handle != nil {
 			vlog.Infof("%s: start failed", Caller(skip+1))
diff --git a/test/v23tests/internal/v23_test.go b/test/v23tests/internal/v23_test.go
index 87514d1..aad6a7a 100644
--- a/test/v23tests/internal/v23_test.go
+++ b/test/v23tests/internal/v23_test.go
@@ -4,11 +4,14 @@
 
 // This file was auto-generated via go generate.
 // DO NOT UPDATE MANUALLY
+
 package internal_test
 
-import "testing"
+import (
+	"testing"
 
-import "v.io/x/ref/test/v23tests"
+	"v.io/x/ref/test/v23tests"
+)
 
 func TestV23One(t *testing.T) {
 	v23tests.RunTest(t, V23TestOne)
diff --git a/test/v23tests/invocation.go b/test/v23tests/invocation.go
index 8d5f3cf..63ddc75 100644
--- a/test/v23tests/invocation.go
+++ b/test/v23tests/invocation.go
@@ -11,7 +11,6 @@
 	"syscall"
 
 	"v.io/x/lib/vlog"
-
 	"v.io/x/ref/test/modules"
 )
 
diff --git a/test/v23tests/v23tests.go b/test/v23tests/v23tests.go
index 4976dd1..91e1afa 100644
--- a/test/v23tests/v23tests.go
+++ b/test/v23tests/v23tests.go
@@ -419,7 +419,7 @@
 		env:     t,
 		envVars: nil,
 		path:    path,
-		opts:    t.shell.DefaultStartOpts().NoExecCommand(),
+		opts:    t.shell.DefaultStartOpts().NoExecProgram(),
 	}
 }
 
@@ -449,7 +449,7 @@
 // be used via BuildV23Pkg.
 func (t *T) BuildV23Pkg(pkg string, flags ...string) *Binary {
 	b := t.buildPkg(pkg, flags...)
-	b.opts = t.shell.DefaultStartOpts().ExternalCommand()
+	b.opts = t.shell.DefaultStartOpts().ExternalProgram()
 	return b
 }
 
@@ -474,7 +474,7 @@
 		env:     t,
 		envVars: nil,
 		path:    built_path,
-		opts:    t.shell.DefaultStartOpts().NoExecCommand(),
+		opts:    t.shell.DefaultStartOpts().NoExecProgram(),
 	}
 	t.builtBinaries[pkg] = binary
 	return binary
diff --git a/test/v23tests/v23tests_test.go b/test/v23tests/v23tests_test.go
index 76e16be..63efeb9 100644
--- a/test/v23tests/v23tests_test.go
+++ b/test/v23tests/v23tests_test.go
@@ -8,7 +8,6 @@
 	"bytes"
 	"crypto/sha1"
 	"fmt"
-	"io"
 	"os"
 	"regexp"
 	"strings"
@@ -101,23 +100,26 @@
 var globalT *testing.T
 
 func TestHelperProcess(t *testing.T) {
-	globalT = t
-	modules.Dispatch()
+	if modules.IsChildProcess() {
+		globalT = t
+		if err := modules.Dispatch(); err != nil {
+			t.Errorf("modules.Dispatch failed: %v", err)
+		}
+	}
 }
 
-func RunIntegrationTestInChild(stdin io.Reader, stdout, stderr io.Writer, env map[string]string, args ...string) error {
+var RunIntegrationTestInChild = modules.Register(func(env *modules.Env, args ...string) error {
 	v23tests.RunTest(globalT, IntegrationTestInChild)
 	return nil
-}
+}, "RunIntegrationTestInChild")
 
 func init() {
 	test.Init()
-	modules.RegisterChild("RunIntegrationTestInChild", "", RunIntegrationTestInChild)
 }
 
 func TestDeferHandling(t *testing.T) {
 	sh, _ := modules.NewShell(nil, nil, testing.Verbose(), t)
-	child, err := sh.Start("RunIntegrationTestInChild", nil, "--test.run=TestHelperProcess", "--v23.tests")
+	child, err := sh.Start(nil, RunIntegrationTestInChild, "--test.run=TestHelperProcess", "--v23.tests")
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -294,7 +296,7 @@
 		msg := recover().(string)
 		// this, and the tests below are intended to ensure that line #s
 		// are captured and reported correctly.
-		if got, want := msg, "v23tests_test.go:304"; !strings.Contains(got, want) {
+		if got, want := msg, "v23tests_test.go:306"; !strings.Contains(got, want) {
 			t.Fatalf("%q does not contain %q", got, want)
 		}
 		if got, want := msg, "fork/exec /bin/echox: no such file or directory"; !strings.Contains(got, want) {
@@ -316,7 +318,7 @@
 	sh.SetDefaultStartOpts(opts)
 	defer func() {
 		msg := recover().(string)
-		if got, want := msg, "v23tests_test.go:326"; !strings.Contains(got, want) {
+		if got, want := msg, "v23tests_test.go:328"; !strings.Contains(got, want) {
 			t.Fatalf("%q does not contain %q", got, want)
 		}
 		if got, want := msg, "StartWithOpts"; !strings.Contains(got, want) {
@@ -340,7 +342,7 @@
 		if iterations == 0 {
 			t.Fatalf("our sleeper didn't get to run")
 		}
-		if got, want := recover().(string), "v23tests_test.go:347: timed out"; !strings.Contains(got, want) {
+		if got, want := recover().(string), "v23tests_test.go:349: timed out"; !strings.Contains(got, want) {
 			t.Fatalf("%q does not contain %q", got, want)
 		}
 	}()
@@ -362,7 +364,7 @@
 		if iterations != 0 {
 			t.Fatalf("our sleeper got to run")
 		}
-		if got, want := recover().(string), "v23tests_test.go:369: timed out"; !strings.Contains(got, want) {
+		if got, want := recover().(string), "v23tests_test.go:371: timed out"; !strings.Contains(got, want) {
 			t.Fatalf("%q does not contain %q", got, want)
 		}
 	}()