lib: replace most uses of modules pkg

(mostly device mgr)

MultiPart: 1/2

Change-Id: Ib3f19287dc721d06f9dafbf92179153067987713
diff --git a/gosh/.api b/gosh/.api
index e99f9da..687f358 100644
--- a/gosh/.api
+++ b/gosh/.api
@@ -1,6 +1,6 @@
 pkg gosh, func Call(string, ...interface{}) error
-pkg gosh, func MaybeRunFnAndExit()
-pkg gosh, func MaybeWatchParent()
+pkg gosh, func InitChildMain()
+pkg gosh, func InitMain()
 pkg gosh, func NewBufferedPipe() io.ReadWriteCloser
 pkg gosh, func NewShell(Opts) *Shell
 pkg gosh, func NopWriteCloser(io.Writer) io.WriteCloser
@@ -8,7 +8,6 @@
 pkg gosh, func Run(func() int) int
 pkg gosh, func SendReady()
 pkg gosh, func SendVars(map[string]string)
-pkg gosh, func WatchParent()
 pkg gosh, method (*Cmd) AddStderrWriter(io.WriteCloser)
 pkg gosh, method (*Cmd) AddStdoutWriter(io.WriteCloser)
 pkg gosh, method (*Cmd) AwaitReady()
@@ -45,7 +44,9 @@
 pkg gosh, type Cmd struct
 pkg gosh, type Cmd struct, Args []string
 pkg gosh, type Cmd struct, Err error
+pkg gosh, type Cmd struct, ExitAfter time.Duration
 pkg gosh, type Cmd struct, ExitErrorIsOk bool
+pkg gosh, type Cmd struct, IgnoreParentExit bool
 pkg gosh, type Cmd struct, OutputDir string
 pkg gosh, type Cmd struct, Path string
 pkg gosh, type Cmd struct, PropagateOutput bool
diff --git a/gosh/child.go b/gosh/child.go
index 50725ee..95c47ef 100644
--- a/gosh/child.go
+++ b/gosh/child.go
@@ -44,26 +44,38 @@
 	send(msg{Type: typeVars, Vars: vars})
 }
 
-// WatchParent starts a goroutine that periodically checks whether the parent
-// process has exited and, if so, kills the current process.
-func WatchParent() {
-	go func() {
-		for {
-			if os.Getppid() == 1 {
-				log.Fatal("parent process has exited")
-			}
-			time.Sleep(time.Second)
+// watchParent periodically checks whether the parent process has exited and, if
+// so, kills the current process. Meant to be run in a goroutine.
+func watchParent() {
+	for {
+		if os.Getppid() == 1 {
+			log.Fatal("gosh: parent process has exited")
 		}
-	}()
+		time.Sleep(time.Second)
+	}
 }
 
-// MaybeWatchParent calls WatchParent iff this process was spawned by a
-// gosh.Shell in the parent process.
-func MaybeWatchParent() {
-	if os.Getenv(envSpawnedByShell) != "" {
-		// Our child processes should see envSpawnedByShell iff they were spawned by
-		// a gosh.Shell in this process.
-		os.Unsetenv(envSpawnedByShell)
-		WatchParent()
+// exitAfter kills the current process once the given duration has elapsed.
+// Meant to be run in a goroutine.
+func exitAfter(d time.Duration) {
+	time.Sleep(d)
+	log.Fatalf("gosh: timed out after %v", d)
+}
+
+// InitChildMain must be called early on in main() of child processes. It spawns
+// goroutines to kill the current process when certain conditions are met, per
+// Cmd.IgnoreParentExit and Cmd.ExitAfter.
+func InitChildMain() {
+	if os.Getenv(envWatchParent) != "" {
+		os.Unsetenv(envWatchParent)
+		go watchParent()
+	}
+	if os.Getenv(envExitAfter) != "" {
+		d, err := time.ParseDuration(envExitAfter)
+		if err != nil {
+			panic(err)
+		}
+		os.Unsetenv(envExitAfter)
+		go exitAfter(d)
 	}
 }
diff --git a/gosh/cmd.go b/gosh/cmd.go
index e80eff0..6ed9682 100644
--- a/gosh/cmd.go
+++ b/gosh/cmd.go
@@ -36,8 +36,19 @@
 	Path string
 	// Vars is the map of env vars for this Cmd.
 	Vars map[string]string
-	// Args is the list of args for this Cmd, not including the path.
+	// Args is the list of args for this Cmd, starting with the resolved path.
+	// Note, we set Args[0] to the resolved path (rather than the user-specified
+	// name) so that a command started by Shell can reliably determine the path to
+	// its executable.
 	Args []string
+	// IgnoreParentExit, if true, makes it so the child process does not exit when
+	// its parent exits. Only takes effect if the child process was spawned via
+	// Shell.Fn or Shell.Main, or explicitly calls InitChildMain.
+	IgnoreParentExit bool
+	// ExitAfter, if non-zero, specifies that the child process should exit after
+	// the given duration has elapsed. Only takes effect if the child process was
+	// spawned via Shell.Fn or Shell.Main, or explicitly calls InitChildMain.
+	ExitAfter time.Duration
 	// PropagateOutput is inherited from Shell.Opts.PropagateChildOutput.
 	PropagateOutput bool
 	// OutputDir is inherited from Shell.Opts.ChildOutputDir.
@@ -228,7 +239,7 @@
 	c := &Cmd{
 		Path:     path,
 		Vars:     vars,
-		Args:     args,
+		Args:     append([]string{path}, args...),
 		sh:       sh,
 		c:        &exec.Cmd{},
 		cond:     sync.NewCond(&sync.Mutex{}),
@@ -397,16 +408,15 @@
 }
 
 func (c *Cmd) clone() (*Cmd, error) {
-	vars := make(map[string]string, len(c.Vars))
-	for k, v := range c.Vars {
-		vars[k] = v
-	}
+	vars := copyMap(c.Vars)
 	args := make([]string, len(c.Args))
 	copy(args, c.Args)
-	res, err := newCmdInternal(c.sh, vars, c.Path, args)
+	res, err := newCmdInternal(c.sh, vars, c.Path, args[1:])
 	if err != nil {
 		return nil, err
 	}
+	res.IgnoreParentExit = c.IgnoreParentExit
+	res.ExitAfter = c.ExitAfter
 	res.PropagateOutput = c.PropagateOutput
 	res.OutputDir = c.OutputDir
 	res.ExitErrorIsOk = c.ExitErrorIsOk
@@ -475,7 +485,7 @@
 }
 
 // TODO(sadovsky): Maybe wrap every child process with a "supervisor" process
-// that calls WatchParent().
+// that calls InitChildMain.
 
 func (c *Cmd) start() error {
 	if c.calledStart {
@@ -491,8 +501,19 @@
 	}
 	// Configure the command.
 	c.c.Path = c.Path
-	c.c.Env = mapToSlice(c.Vars)
-	c.c.Args = append([]string{c.Path}, c.Args...)
+	vars := copyMap(c.Vars)
+	if c.IgnoreParentExit {
+		delete(vars, envWatchParent)
+	} else {
+		vars[envWatchParent] = "1"
+	}
+	if c.ExitAfter == 0 {
+		delete(vars, envExitAfter)
+	} else {
+		vars[envExitAfter] = c.ExitAfter.String()
+	}
+	c.c.Env = mapToSlice(vars)
+	c.c.Args = c.Args
 	if c.Stdin != "" {
 		if c.stdinWriteCloser != nil {
 			return errors.New("gosh: cannot both set Stdin and call StdinPipe")
diff --git a/gosh/env_util.go b/gosh/env_util.go
index 3e7b815..a9966d7 100644
--- a/gosh/env_util.go
+++ b/gosh/env_util.go
@@ -67,3 +67,8 @@
 	}
 	return res
 }
+
+// copyMap returns a copy of the given map.
+func copyMap(m map[string]string) map[string]string {
+	return mergeMaps(m)
+}
diff --git a/gosh/internal/gosh_example/main.go b/gosh/internal/gosh_example/main.go
index bde8f86..d79d680 100644
--- a/gosh/internal/gosh_example/main.go
+++ b/gosh/internal/gosh_example/main.go
@@ -59,7 +59,7 @@
 }
 
 func main() {
-	gosh.MaybeRunFnAndExit()
+	gosh.InitMain()
 	ExampleCmds()
 	ExampleFns()
 	ExampleShellMain()
diff --git a/gosh/internal/gosh_example_client/main.go b/gosh/internal/gosh_example_client/main.go
index 6962d22..1568da5 100644
--- a/gosh/internal/gosh_example_client/main.go
+++ b/gosh/internal/gosh_example_client/main.go
@@ -14,7 +14,7 @@
 var addr = flag.String("addr", "localhost:8080", "server addr")
 
 func main() {
-	gosh.MaybeWatchParent()
+	gosh.InitChildMain()
 	flag.Parse()
 	lib.Get(*addr)
 }
diff --git a/gosh/internal/gosh_example_server/main.go b/gosh/internal/gosh_example_server/main.go
index f7d96c2..00968a0 100644
--- a/gosh/internal/gosh_example_server/main.go
+++ b/gosh/internal/gosh_example_server/main.go
@@ -10,6 +10,6 @@
 )
 
 func main() {
-	gosh.MaybeWatchParent()
+	gosh.InitChildMain()
 	lib.Serve()
 }
diff --git a/gosh/shell.go b/gosh/shell.go
index 02ee145..37d3174 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -20,6 +20,7 @@
 	"log"
 	"math/rand"
 	"os"
+	"os/exec"
 	"os/signal"
 	"path"
 	"path/filepath"
@@ -31,16 +32,25 @@
 const (
 	envBinDir         = "GOSH_BIN_DIR"
 	envChildOutputDir = "GOSH_CHILD_OUTPUT_DIR"
+	envExitAfter      = "GOSH_EXIT_AFTER"
 	envInvocation     = "GOSH_INVOCATION"
-	envSpawnedByShell = "GOSH_SPAWNED_BY_SHELL"
+	envWatchParent    = "GOSH_WATCH_PARENT"
 )
 
 var (
-	errAlreadyCalledCleanup        = errors.New("gosh: already called Shell.Cleanup")
-	errDidNotCallMaybeRunFnAndExit = errors.New("gosh: did not call Shell.MaybeRunFnAndExit")
-	errDidNotCallNewShell          = errors.New("gosh: did not call gosh.NewShell")
+	errAlreadyCalledCleanup = errors.New("gosh: already called Shell.Cleanup")
+	errDidNotCallInitMain   = errors.New("gosh: did not call gosh.InitMain")
+	errDidNotCallNewShell   = errors.New("gosh: did not call gosh.NewShell")
 )
 
+// TODO(sadovsky):
+// - Eliminate Shell.Main, since it's easy enough to use Shell.Fn and set the
+//   returned Cmd's Args.
+// - Rename Shell.Fn to Shell.FuncCmd, Fn to Func, Register to RegisterFunc, and
+//   Call to CallFunc.
+// - Eliminate gosh.Run.
+// - Revisit whether to have Cmd.Shutdown.
+
 // Shell represents a shell. Not thread-safe.
 type Shell struct {
 	// Err is the most recent error from this Shell or any of its Cmds (may be
@@ -288,7 +298,8 @@
 	if vars == nil {
 		vars = make(map[string]string)
 	}
-	vars[envSpawnedByShell] = "1"
+	// TODO(sadovsky): Copy os.Environ() into sh.Vars in NewShell, and clear
+	// envWatchParent and envExitAfter if they're set.
 	c, err := newCmd(sh, mergeMaps(sliceToMap(os.Environ()), sh.Vars, vars), name, append(args, sh.Args...)...)
 	if err != nil {
 		return nil, err
@@ -298,25 +309,34 @@
 	return c, nil
 }
 
+var executablePath = os.Args[0]
+
+func init() {
+	// If exec.LookPath fails, hope for the best.
+	if lp, err := exec.LookPath(executablePath); err != nil {
+		executablePath = lp
+	}
+}
+
 func (sh *Shell) fn(fn *Fn, args ...interface{}) (*Cmd, error) {
-	// Safeguard against the developer forgetting to call MaybeRunFnAndExit, which
-	// could lead to infinite recursion.
-	if !calledMaybeRunFnAndExit {
-		return nil, errDidNotCallMaybeRunFnAndExit
+	// Safeguard against the developer forgetting to call InitMain, which could
+	// lead to infinite recursion.
+	if !calledInitMain {
+		return nil, errDidNotCallInitMain
 	}
 	b, err := encInvocation(fn.name, args...)
 	if err != nil {
 		return nil, err
 	}
 	vars := map[string]string{envInvocation: string(b)}
-	return sh.cmd(vars, os.Args[0])
+	return sh.cmd(vars, executablePath)
 }
 
 func (sh *Shell) main(fn *Fn, args ...string) (*Cmd, error) {
-	// Safeguard against the developer forgetting to call MaybeRunFnAndExit, which
-	// could lead to infinite recursion.
-	if !calledMaybeRunFnAndExit {
-		return nil, errDidNotCallMaybeRunFnAndExit
+	// Safeguard against the developer forgetting to call InitMain, which could
+	// lead to infinite recursion.
+	if !calledInitMain {
+		return nil, errDidNotCallInitMain
 	}
 	// Check that fn has the required signature.
 	t := fn.value.Type()
@@ -328,7 +348,7 @@
 		return nil, err
 	}
 	vars := map[string]string{envInvocation: string(b)}
-	return sh.cmd(vars, os.Args[0], args...)
+	return sh.cmd(vars, executablePath, args...)
 }
 
 func (sh *Shell) wait() error {
@@ -534,22 +554,19 @@
 ////////////////////////////////////////
 // Public utilities
 
-var calledMaybeRunFnAndExit = false
+var calledInitMain = false
 
-// MaybeRunFnAndExit must be called first thing in main() or TestMain(), before
-// flags are parsed. In the parent process, it returns immediately with no
-// effect. In a child process for a Shell.Fn() or Shell.Main() command, it runs
-// the specified function, then exits.
-func MaybeRunFnAndExit() {
-	calledMaybeRunFnAndExit = true
+// InitMain must be called early on in main(), before flags are parsed. In the
+// parent process, it returns immediately with no effect. In a child process for
+// a Shell.Fn or Shell.Main command, it runs the specified function, then exits.
+func InitMain() {
+	calledInitMain = true
 	s := os.Getenv(envInvocation)
 	if s == "" {
 		return
 	}
 	os.Unsetenv(envInvocation)
-	// Call MaybeWatchParent rather than WatchParent so that envSpawnedByShell
-	// gets cleared.
-	MaybeWatchParent()
+	InitChildMain()
 	name, args, err := decInvocation(s)
 	if err != nil {
 		log.Fatal(err)
@@ -560,10 +577,10 @@
 	os.Exit(0)
 }
 
-// Run calls MaybeRunFnAndExit(), then returns run(). Exported so that TestMain
-// functions can simply call os.Exit(gosh.Run(m.Run)).
+// Run calls InitMain, then returns run(). Exported so that TestMain functions
+// can simply call os.Exit(gosh.Run(m.Run)).
 func Run(run func() int) int {
-	MaybeRunFnAndExit()
+	InitMain()
 	return run()
 }