| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package gosh provides facilities for running and managing processes: start |
| // them, wait for them to exit, capture their output streams, pipe messages |
| // between them, terminate them (e.g. on SIGINT), and so on. |
| // |
| // Gosh is meant to be used in situations where one might otherwise be tempted |
| // to write a shell script. (Oh my gosh, no more shell scripts!) |
| // |
| // For usage examples, see shell_test.go and internal/gosh_example/main.go. |
| package gosh |
| |
| import ( |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "log" |
| "math/rand" |
| "os" |
| "os/exec" |
| "os/signal" |
| "path" |
| "path/filepath" |
| "runtime" |
| "runtime/debug" |
| "sync" |
| "syscall" |
| "time" |
| ) |
| |
| const ( |
| envExitAfter = "GOSH_EXIT_AFTER" |
| envInvocation = "GOSH_INVOCATION" |
| envWatchParent = "GOSH_WATCH_PARENT" |
| ) |
| |
| var ( |
| 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") |
| ) |
| |
| // TB is a subset of the testing.TB interface, defined here to avoid depending |
| // on the testing package. |
| type TB interface { |
| FailNow() |
| Logf(format string, args ...interface{}) |
| } |
| |
| // Shell represents a shell. Not thread-safe. |
| type Shell struct { |
| // Err is the most recent error from this Shell or any of its child Cmds (may |
| // be nil). |
| Err error |
| // PropagateChildOutput specifies whether to propagate child stdout and stderr |
| // up to the parent's stdout and stderr. |
| PropagateChildOutput bool |
| // ChildOutputDir, if non-empty, makes it so child stdout and stderr are tee'd |
| // to files in the specified directory. |
| ChildOutputDir string |
| // ContinueOnError specifies whether to invoke TB.FailNow on error, i.e. |
| // whether to panic on error. Users that set ContinueOnError to true should |
| // inspect sh.Err after each Shell method invocation. |
| ContinueOnError bool |
| // Vars is the map of env vars for this Shell. |
| Vars map[string]string |
| // Args is the list of args to append to subsequent command invocations. |
| Args []string |
| // Internal state. |
| calledNewShell bool |
| tb TB |
| cleanupDone chan struct{} |
| cleanupMu sync.Mutex // protects the fields below; held during cleanup |
| calledCleanup bool |
| cmds []*Cmd |
| tempFiles []*os.File |
| tempDirs []string |
| dirStack []string // for pushd/popd |
| cleanupHandlers []func() |
| } |
| |
| // NewShell returns a new Shell. Tests and benchmarks should pass their |
| // testing.TB instance; non-tests should pass nil. |
| func NewShell(tb TB) *Shell { |
| sh, err := newShell(tb) |
| sh.handleError(err) |
| return sh |
| } |
| |
| // HandleError sets sh.Err. If err is not nil and sh.ContinueOnError is false, |
| // it also calls TB.FailNow. |
| func (sh *Shell) HandleError(err error) { |
| sh.HandleErrorWithSkip(err, 2) |
| } |
| |
| // handleError is intended for use by public Shell method implementations. |
| func (sh *Shell) handleError(err error) { |
| sh.HandleErrorWithSkip(err, 3) |
| } |
| |
| // HandleErrorWithSkip is like HandleError, but allows clients to specify the |
| // skip value to pass to runtime.Caller. |
| func (sh *Shell) HandleErrorWithSkip(err error, skip int) { |
| sh.Ok() |
| sh.Err = err |
| if err == nil { |
| return |
| } |
| _, file, line, _ := runtime.Caller(skip) |
| toLog := fmt.Sprintf("%s:%d: %v\n", filepath.Base(file), line, err) |
| if sh.ContinueOnError { |
| sh.tb.Logf(toLog) |
| return |
| } |
| if sh.tb != pkgLevelDefaultTB { |
| sh.tb.Logf(string(debug.Stack())) |
| } |
| // Unfortunately, if FailNow panics, there's no way to make toLog get printed |
| // beneath the stack trace. |
| sh.tb.Logf(toLog) |
| sh.tb.FailNow() |
| } |
| |
| // Cmd returns a Cmd for an invocation of the named program. The given arguments |
| // are passed to the child as command-line arguments. |
| func (sh *Shell) Cmd(name string, args ...string) *Cmd { |
| sh.Ok() |
| res, err := sh.cmd(nil, name, args...) |
| sh.handleError(err) |
| return res |
| } |
| |
| // FuncCmd returns a Cmd for an invocation of the given registered Func. The |
| // given arguments are gob-encoded in the parent process, then gob-decoded in |
| // the child and passed to the Func as parameters. To specify command-line |
| // arguments for the child invocation, append to the returned Cmd's Args. |
| func (sh *Shell) FuncCmd(f *Func, args ...interface{}) *Cmd { |
| sh.Ok() |
| res, err := sh.funcCmd(f, args...) |
| sh.handleError(err) |
| return res |
| } |
| |
| // Wait waits for all commands started by this Shell to exit. |
| func (sh *Shell) Wait() { |
| sh.Ok() |
| sh.handleError(sh.wait()) |
| } |
| |
| // Move moves a file from 'oldpath' to 'newpath'. It first attempts os.Rename; |
| // if that fails, it copies 'oldpath' to 'newpath', then deletes 'oldpath'. |
| // Requires that 'newpath' does not exist, and that the parent directory of |
| // 'newpath' does exist. Currently only supports moving an individual file; |
| // moving a directory is not yet supported. |
| func (sh *Shell) Move(oldpath, newpath string) { |
| sh.Ok() |
| sh.handleError(sh.move(oldpath, newpath)) |
| } |
| |
| // MakeTempFile creates a new temporary file in os.TempDir, opens the file for |
| // reading and writing, and returns the resulting *os.File. |
| func (sh *Shell) MakeTempFile() *os.File { |
| sh.Ok() |
| res, err := sh.makeTempFile() |
| sh.handleError(err) |
| return res |
| } |
| |
| // MakeTempDir creates a new temporary directory in os.TempDir and returns the |
| // path of the new directory. |
| func (sh *Shell) MakeTempDir() string { |
| sh.Ok() |
| res, err := sh.makeTempDir() |
| sh.handleError(err) |
| return res |
| } |
| |
| // Pushd behaves like Bash pushd. |
| func (sh *Shell) Pushd(dir string) { |
| sh.Ok() |
| sh.handleError(sh.pushd(dir)) |
| } |
| |
| // Popd behaves like Bash popd. |
| func (sh *Shell) Popd() { |
| sh.Ok() |
| sh.handleError(sh.popd()) |
| } |
| |
| // AddCleanupHandler registers the given function to be called during cleanup. |
| // Cleanup handlers are called in LIFO order, possibly in a separate goroutine |
| // spawned by gosh. |
| func (sh *Shell) AddCleanupHandler(f func()) { |
| sh.Ok() |
| sh.handleError(sh.addCleanupHandler(f)) |
| } |
| |
| // Cleanup cleans up all resources (child processes, temporary files and |
| // directories) associated with this Shell. It is safe (and recommended) to call |
| // Cleanup after a Shell error. It is also safe to call Cleanup multiple times; |
| // calls after the first return immediately with no effect. Cleanup never calls |
| // HandleError. |
| func (sh *Shell) Cleanup() { |
| if !sh.calledNewShell { |
| panic(errDidNotCallNewShell) |
| } |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if !sh.calledCleanup { |
| sh.cleanup() |
| } |
| } |
| |
| // Ok panics iff this Shell is in a state where it's invalid to call other |
| // methods. This method is public to facilitate Shell wrapping. |
| func (sh *Shell) Ok() { |
| if !sh.calledNewShell { |
| panic(errDidNotCallNewShell) |
| } |
| // Panic on incorrect usage of Shell. |
| if sh.Err != nil { |
| panic(fmt.Errorf("gosh: Shell.Err is not nil: %v", sh.Err)) |
| } |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if sh.calledCleanup { |
| panic(errAlreadyCalledCleanup) |
| } |
| } |
| |
| //////////////////////////////////////// |
| // Internals |
| |
| type defaultTB struct{} |
| |
| func (*defaultTB) FailNow() { |
| panic(nil) |
| } |
| |
| func (*defaultTB) Logf(format string, args ...interface{}) { |
| log.Printf(format, args...) |
| } |
| |
| var pkgLevelDefaultTB *defaultTB = &defaultTB{} |
| |
| func newShell(tb TB) (*Shell, error) { |
| if tb == nil { |
| tb = pkgLevelDefaultTB |
| } |
| // Filter out any gosh env vars coming from outside. |
| shVars := sliceToMap(os.Environ()) |
| for _, key := range []string{envExitAfter, envInvocation, envWatchParent} { |
| delete(shVars, key) |
| } |
| sh := &Shell{ |
| Vars: shVars, |
| calledNewShell: true, |
| tb: tb, |
| cleanupDone: make(chan struct{}), |
| } |
| sh.cleanupOnSignal() |
| return sh, nil |
| } |
| |
| // cleanupOnSignal starts a goroutine that calls cleanup if a termination signal |
| // is received. |
| func (sh *Shell) cleanupOnSignal() { |
| ch := make(chan os.Signal, 1) |
| signal.Notify(ch, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM) |
| go func() { |
| select { |
| case sig := <-ch: |
| // A termination signal was received; the process will exit. |
| sh.tb.Logf("Received signal: %v\n", sig) |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if !sh.calledCleanup { |
| sh.cleanup() |
| } |
| // Note: We hold cleanupMu during os.Exit(1) so that the main goroutine |
| // will not call Shell.Ok() and panic before we exit. |
| os.Exit(1) |
| case <-sh.cleanupDone: |
| // The user called sh.Cleanup; stop listening for signals and exit this |
| // goroutine. |
| } |
| signal.Stop(ch) |
| }() |
| } |
| |
| func (sh *Shell) cmd(vars map[string]string, name string, args ...string) (*Cmd, error) { |
| if vars == nil { |
| vars = make(map[string]string) |
| } |
| c, err := newCmd(sh, mergeMaps(sh.Vars, vars), name, append(args, sh.Args...)...) |
| if err != nil { |
| return nil, err |
| } |
| c.PropagateOutput = sh.PropagateChildOutput |
| c.OutputDir = sh.ChildOutputDir |
| 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) funcCmd(f *Func, args ...interface{}) (*Cmd, error) { |
| // Safeguard against the developer forgetting to call InitMain, which could |
| // lead to infinite recursion. |
| if !calledInitMain { |
| return nil, errDidNotCallInitMain |
| } |
| buf, err := encodeInvocation(f.handle, args...) |
| if err != nil { |
| return nil, err |
| } |
| vars := map[string]string{envInvocation: string(buf)} |
| return sh.cmd(vars, executablePath) |
| } |
| |
| func (sh *Shell) wait() error { |
| // Note: It is illegal to call newCmdInternal (which mutates sh.cmds) |
| // concurrently with Shell.wait, so we need not hold cleanupMu when accessing |
| // sh.cmds below. |
| var res error |
| for _, c := range sh.cmds { |
| if !c.started || c.calledWait { |
| continue |
| } |
| if err := c.wait(); !c.errorIsOk(err) { |
| sh.tb.Logf("%s (PID %d) failed: %v\n", c.Path, c.Pid(), err) |
| res = err |
| } |
| } |
| return res |
| } |
| |
| func copyFile(to, from string) error { |
| fi, err := os.Stat(from) |
| if err != nil { |
| return err |
| } |
| in, err := os.Open(from) |
| if err != nil { |
| return err |
| } |
| defer in.Close() |
| out, err := os.OpenFile(to, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, fi.Mode().Perm()) |
| if err != nil { |
| return err |
| } |
| _, err = io.Copy(out, in) |
| cerr := out.Close() |
| if err != nil { |
| return err |
| } |
| return cerr |
| } |
| |
| func (sh *Shell) move(oldpath, newpath string) error { |
| fi, err := os.Stat(oldpath) |
| if err != nil { |
| return err |
| } |
| if fi.Mode().IsDir() { |
| return errors.New("gosh: moving a directory is not yet supported") |
| } |
| if _, err := os.Stat(newpath); !os.IsNotExist(err) { |
| return errors.New("gosh: destination file must not exist") |
| } |
| if _, err := os.Stat(filepath.Dir(newpath)); err != nil { |
| if os.IsNotExist(err) { |
| return errors.New("gosh: destination file's parent directory must exist") |
| } |
| return err |
| } |
| if err := os.Rename(oldpath, newpath); err == nil { |
| return nil |
| } |
| // Concurrent, same-directory rename operations sometimes fail on certain |
| // systems, so we retry once after a random backoff. |
| time.Sleep(time.Duration(rand.Int63n(1000)) * time.Millisecond) |
| if err := os.Rename(oldpath, newpath); err == nil { |
| return nil |
| } |
| // Try copying the file over. |
| if err := copyFile(newpath, oldpath); err != nil { |
| return err |
| } |
| return os.Remove(oldpath) |
| } |
| |
| func (sh *Shell) makeTempFile() (*os.File, error) { |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if sh.calledCleanup { |
| return nil, errAlreadyCalledCleanup |
| } |
| f, err := ioutil.TempFile("", "") |
| if err != nil { |
| return nil, err |
| } |
| sh.tempFiles = append(sh.tempFiles, f) |
| return f, nil |
| } |
| |
| func (sh *Shell) makeTempDir() (string, error) { |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if sh.calledCleanup { |
| return "", errAlreadyCalledCleanup |
| } |
| name, err := ioutil.TempDir("", "") |
| if err != nil { |
| return "", err |
| } |
| sh.tempDirs = append(sh.tempDirs, name) |
| return name, nil |
| } |
| |
| func (sh *Shell) pushd(dir string) error { |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if sh.calledCleanup { |
| return errAlreadyCalledCleanup |
| } |
| cwd, err := os.Getwd() |
| if err != nil { |
| return err |
| } |
| if err := os.Chdir(dir); err != nil { |
| return err |
| } |
| sh.dirStack = append(sh.dirStack, cwd) |
| return nil |
| } |
| |
| func (sh *Shell) popd() error { |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if sh.calledCleanup { |
| return errAlreadyCalledCleanup |
| } |
| if len(sh.dirStack) == 0 { |
| return errors.New("gosh: dir stack is empty") |
| } |
| dir := sh.dirStack[len(sh.dirStack)-1] |
| if err := os.Chdir(dir); err != nil { |
| return err |
| } |
| sh.dirStack = sh.dirStack[:len(sh.dirStack)-1] |
| return nil |
| } |
| |
| func (sh *Shell) addCleanupHandler(f func()) error { |
| sh.cleanupMu.Lock() |
| defer sh.cleanupMu.Unlock() |
| if sh.calledCleanup { |
| return errAlreadyCalledCleanup |
| } |
| sh.cleanupHandlers = append(sh.cleanupHandlers, f) |
| return nil |
| } |
| |
| // Note: It is safe to run Shell.cleanupRunningCmds concurrently with the waiter |
| // goroutine and with Cmd.wait. In particular, Shell.cleanupRunningCmds only |
| // calls c.{isRunning,Pid}, all of which are thread-safe with the waiter |
| // goroutine and with Cmd.wait. |
| func (sh *Shell) cleanupRunningCmds() { |
| var wg sync.WaitGroup |
| for _, c := range sh.cmds { |
| if !c.started { |
| continue |
| } |
| wg.Add(1) |
| go func(cmd *Cmd) { |
| defer wg.Done() |
| cmd.cleanupProcessGroup() |
| }(c) |
| } |
| wg.Wait() |
| } |
| |
| func (sh *Shell) cleanup() { |
| sh.calledCleanup = true |
| // Clean up all children that are still running. |
| sh.cleanupRunningCmds() |
| // Close and delete all temporary files. |
| for _, tempFile := range sh.tempFiles { |
| name := tempFile.Name() |
| if err := tempFile.Close(); err != nil { |
| sh.tb.Logf("%q.Close() failed: %v\n", name, err) |
| } |
| if err := os.RemoveAll(name); err != nil { |
| sh.tb.Logf("os.RemoveAll(%q) failed: %v\n", name, err) |
| } |
| } |
| // Delete all temporary directories. |
| for _, tempDir := range sh.tempDirs { |
| if err := os.RemoveAll(tempDir); err != nil { |
| sh.tb.Logf("os.RemoveAll(%q) failed: %v\n", tempDir, err) |
| } |
| } |
| // Change back to the top of the dir stack. |
| if len(sh.dirStack) > 0 { |
| dir := sh.dirStack[0] |
| if err := os.Chdir(dir); err != nil { |
| sh.tb.Logf("os.Chdir(%q) failed: %v\n", dir, err) |
| } |
| } |
| // Call cleanup handlers in LIFO order. |
| for i := len(sh.cleanupHandlers) - 1; i >= 0; i-- { |
| sh.cleanupHandlers[i]() |
| } |
| close(sh.cleanupDone) |
| } |
| |
| //////////////////////////////////////// |
| // Public utilities |
| |
| var calledInitMain = false |
| |
| // 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.FuncCmd command, it runs the specified function, then exits. |
| func InitMain() { |
| if calledInitMain { |
| panic("gosh: already called gosh.InitMain") |
| } |
| calledInitMain = true |
| s := os.Getenv(envInvocation) |
| if s == "" { |
| return |
| } |
| os.Unsetenv(envInvocation) |
| InitChildMain() |
| name, args, err := decodeInvocation(s) |
| if err != nil { |
| log.Fatal(err) |
| } |
| if err := callFunc(name, args...); err != nil { |
| log.Fatal(err) |
| } |
| os.Exit(0) |
| } |
| |
| // BuildGoPkg compiles a Go package using the "go build" command and writes the |
| // resulting binary to the given binDir, or to the -o flag location if |
| // specified. If -o is relative, it is interpreted as relative to binDir. If the |
| // binary already exists at the target location, it is not rebuilt. Returns the |
| // absolute path to the binary. |
| func BuildGoPkg(sh *Shell, binDir, pkg string, flags ...string) string { |
| sh.Ok() |
| res, err := buildGoPkg(sh, binDir, pkg, flags...) |
| sh.handleError(err) |
| return res |
| } |
| |
| func extractOutputFlag(flags ...string) (outputFlag string, otherFlags []string, err error) { |
| for i := 0; i < len(flags); i++ { |
| v := flags[i] |
| if v == "-o" || v == "--o" { |
| i++ |
| if i == len(flags) { |
| return "", nil, errors.New("gosh: passed -o without location") |
| } |
| outputFlag = flags[i] |
| } else { |
| otherFlags = append(otherFlags, v) |
| } |
| } |
| return |
| } |
| |
| func buildGoPkg(sh *Shell, binDir, pkg string, flags ...string) (string, error) { |
| outputFlag, flags, err := extractOutputFlag(flags...) |
| if err != nil { |
| return "", err |
| } |
| var binPath string |
| if outputFlag == "" { |
| binPath = filepath.Join(binDir, path.Base(pkg)) |
| } else if filepath.IsAbs(outputFlag) { |
| binPath = outputFlag |
| } else { |
| binPath = filepath.Join(binDir, outputFlag) |
| } |
| // If the binary already exists at the target location, don't rebuild it. |
| if _, err := os.Stat(binPath); err == nil { |
| return binPath, nil |
| } else if !os.IsNotExist(err) { |
| return "", err |
| } |
| // Build binary to tempBinPath (in a fresh temporary directory), then move it |
| // to binPath. |
| tempDir, err := ioutil.TempDir(binDir, "") |
| if err != nil { |
| return "", err |
| } |
| defer os.RemoveAll(tempDir) |
| tempBinPath := filepath.Join(tempDir, path.Base(pkg)) |
| args := []string{"build", "-o", tempBinPath} |
| args = append(args, flags...) |
| args = append(args, pkg) |
| c, err := sh.cmd(nil, "go", args...) |
| if err != nil { |
| return "", err |
| } |
| if err := c.run(); err != nil { |
| return "", err |
| } |
| // Create target directory, if needed. |
| if err := os.MkdirAll(filepath.Dir(binPath), 0700); err != nil { |
| return "", err |
| } |
| if err := sh.move(tempBinPath, binPath); err != nil { |
| return "", err |
| } |
| sh.tb.Logf("Built executable: %s\n", binPath) |
| return binPath, nil |
| } |