lib: gosh/v23test: address gosh TODOs, update v23 code
MultiPart: 3/5
Change-Id: I349048b7014210fb9c45f814f247d601fac7ab7b
diff --git a/gosh/.api b/gosh/.api
index 3206ab5..472dccb 100644
--- a/gosh/.api
+++ b/gosh/.api
@@ -8,16 +8,20 @@
pkg gosh, func SendReady()
pkg gosh, func SendVars(map[string]string)
pkg gosh, func WatchParent()
+pkg gosh, method (*Cmd) AddStderrWriter(io.Writer)
+pkg gosh, method (*Cmd) AddStdoutWriter(io.Writer)
pkg gosh, method (*Cmd) AwaitReady()
pkg gosh, method (*Cmd) AwaitVars(...string) map[string]string
-pkg gosh, method (*Cmd) CombinedOutput() string
-pkg gosh, method (*Cmd) Output() (string, string)
+pkg gosh, method (*Cmd) Clone() *Cmd
pkg gosh, method (*Cmd) Process() *os.Process
pkg gosh, method (*Cmd) Run()
pkg gosh, method (*Cmd) Shutdown(os.Signal)
pkg gosh, method (*Cmd) Start()
pkg gosh, method (*Cmd) StderrPipe() io.Reader
+pkg gosh, method (*Cmd) StdinPipe() io.WriteCloser
+pkg gosh, method (*Cmd) Stdout() string
pkg gosh, method (*Cmd) StdoutPipe() io.Reader
+pkg gosh, method (*Cmd) StdoutStderr() (string, string)
pkg gosh, method (*Cmd) Wait()
pkg gosh, method (*Fn) Call(...interface{}) error
pkg gosh, method (*Shell) AddToCleanup(func())
@@ -39,16 +43,17 @@
pkg gosh, type Cmd struct, Err error
pkg gosh, type Cmd struct, ExitErrorIsOk bool
pkg gosh, type Cmd struct, OutputDir string
-pkg gosh, type Cmd struct, Stdin io.Reader
-pkg gosh, type Cmd struct, SuppressOutput bool
+pkg gosh, type Cmd struct, Path string
+pkg gosh, type Cmd struct, PropagateOutput bool
+pkg gosh, type Cmd struct, Stdin string
pkg gosh, type Cmd struct, Vars map[string]string
pkg gosh, type Fn struct
pkg gosh, type Opts struct
pkg gosh, type Opts struct, BinDir string
pkg gosh, type Opts struct, ChildOutputDir string
-pkg gosh, type Opts struct, Errorf func(string, ...interface{})
+pkg gosh, type Opts struct, Fatalf func(string, ...interface{})
pkg gosh, type Opts struct, Logf func(string, ...interface{})
-pkg gosh, type Opts struct, SuppressChildOutput bool
+pkg gosh, type Opts struct, PropagateChildOutput bool
pkg gosh, type Shell struct
pkg gosh, type Shell struct, Args []string
pkg gosh, type Shell struct, Err error
diff --git a/gosh/buffered_pipe.go b/gosh/buffered_pipe.go
index 0cae34f..b7cd44b 100644
--- a/gosh/buffered_pipe.go
+++ b/gosh/buffered_pipe.go
@@ -6,15 +6,14 @@
import (
"bytes"
- "errors"
"io"
"sync"
)
type bufferedPipe struct {
- cond *sync.Cond
- buf bytes.Buffer
- err error
+ cond *sync.Cond
+ buf bytes.Buffer
+ closed bool
}
// NewBufferedPipe returns a new thread-safe pipe backed by an unbounded
@@ -29,11 +28,12 @@
p.cond.L.Lock()
defer p.cond.L.Unlock()
for {
+ // Read any remaining data before checking whether the pipe is closed.
if p.buf.Len() > 0 {
return p.buf.Read(d)
}
- if p.err != nil {
- return 0, p.err
+ if p.closed {
+ return 0, io.EOF
}
p.cond.Wait()
}
@@ -43,8 +43,8 @@
func (p *bufferedPipe) Write(d []byte) (n int, err error) {
p.cond.L.Lock()
defer p.cond.L.Unlock()
- if p.err != nil {
- return 0, errors.New("write on closed pipe")
+ if p.closed {
+ return 0, io.ErrClosedPipe
}
defer p.cond.Signal()
return p.buf.Write(d)
@@ -54,9 +54,9 @@
func (p *bufferedPipe) Close() error {
p.cond.L.Lock()
defer p.cond.L.Unlock()
- if p.err == nil {
+ if !p.closed {
defer p.cond.Signal()
- p.err = io.EOF
+ p.closed = true
}
return nil
}
diff --git a/gosh/buffered_pipe_test.go b/gosh/buffered_pipe_test.go
new file mode 100644
index 0000000..593f77b
--- /dev/null
+++ b/gosh/buffered_pipe_test.go
@@ -0,0 +1,18 @@
+// 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_test
+
+import (
+ "testing"
+
+ "v.io/x/lib/gosh"
+)
+
+func TestReadAfterClose(t *testing.T) {
+ p := gosh.NewBufferedPipe()
+ p.Write([]byte("foo"))
+ p.Close()
+ eq(t, toString(t, p), "foo")
+}
diff --git a/gosh/cmd.go b/gosh/cmd.go
index 19ccdfb..9c69d01 100644
--- a/gosh/cmd.go
+++ b/gosh/cmd.go
@@ -13,6 +13,7 @@
"os"
"os/exec"
"path/filepath"
+ "strings"
"sync"
"time"
)
@@ -28,50 +29,58 @@
type Cmd struct {
// Err is the most recent error from this Cmd (may be nil).
Err error
+ // Path is the path of the command to run.
+ 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.
Args []string
- // Stdin specifies this Cmd's stdin. See comments in exec.Cmd for detailed
- // semantics.
- Stdin io.Reader
- // SuppressOutput is inherited from Shell.Opts.SuppressChildOutput.
- SuppressOutput bool
+ // PropagateOutput is inherited from Shell.Opts.PropagateChildOutput.
+ PropagateOutput bool
// OutputDir is inherited from Shell.Opts.ChildOutputDir.
OutputDir string
// ExitErrorIsOk specifies whether an *exec.ExitError should be reported via
// Shell.HandleError.
ExitErrorIsOk bool
+ // Stdin is a string to write to the child's stdin.
+ Stdin string
// Internal state.
- sh *Shell
- c *exec.Cmd
- name string
- calledWait bool
- stdoutWriters []io.Writer
- stderrWriters []io.Writer
- closeAfterWait []io.Closer
- condReady *sync.Cond
- recvReady bool // protected by condReady.L
- condVars *sync.Cond
- recvVars map[string]string // protected by condVars.L
+ sh *Shell
+ c *exec.Cmd
+ stdinWriteCloser io.WriteCloser // from exec.Cmd.StdinPipe
+ calledStart bool
+ calledWait bool
+ waitChan chan error
+ started bool // protected by sh.cleanupMu
+ exitedMu sync.Mutex
+ exited bool // protected by exitedMu
+ stdoutWriters []io.Writer
+ stderrWriters []io.Writer
+ closers []io.Closer
+ condReady *sync.Cond
+ recvReady bool // protected by condReady.L
+ condVars *sync.Cond
+ recvVars map[string]string // protected by condVars.L
}
-// TODO(sadovsky):
-// - Change "Errorf" to "Fatalf" everywhere.
-// - Drop "CombinedOutput", change "Output" to "StdoutStderr", and add "Stdout".
-// - Add Cmd.Clone method that returns a new Cmd with the same configuration.
-// - Add Cmd.StdinPipe method that creates a new bufferedPipe, returns a
-// WriteCloser, and stores a ReadCloser. Cmd.StdinPipe cannot be called more
-// than once.
-// - In Cmd.Start, start a goroutine that monitors the started process and
-// closes the stdinPipe ReadCloser (if it's not already closed) when the
-// process exits.
-// - Make it so awaitReady and awaitVars return an error if/when our goroutine
-// detects that the process has exited.
-// - Add unit test for piping from one Cmd's stdout to another's stdin. It
-// should be possible to wait on just the last Cmd.
-// - Add unit test to demonstrate that Cmd.Wait returns immediately for a
-// process that blocks on non-nil stdin if Cmd.StdinPipe was never called.
+// Clone returns a new Cmd with a copy of this Cmd's configuration.
+func (c *Cmd) Clone() *Cmd {
+ c.sh.Ok()
+ res, err := c.clone()
+ c.handleError(err)
+ return res
+}
+
+// StdinPipe returns a thread-safe WriteCloser backed by a buffered pipe for the
+// command's stdin. The returned WriteCloser will be closed when the process
+// exits. Must be called before Start. It is safe to call StdinPipe multiple
+// times; calls after the first return the pipe created by the first call.
+func (c *Cmd) StdinPipe() io.WriteCloser {
+ c.sh.Ok()
+ res, err := c.stdinPipe()
+ c.handleError(err)
+ return res
+}
// StdoutPipe returns a Reader backed by a buffered pipe for the command's
// stdout. Must be called before Start. May be called more than once; each
@@ -93,6 +102,22 @@
return res
}
+// AddStdoutWriter configures this Cmd to tee the child's stdout to the given
+// Writer. If this Writer is a Closer and is not os.Stdout or os.Stderr, it will
+// be closed when the process exits.
+func (c *Cmd) AddStdoutWriter(w io.Writer) {
+ c.sh.Ok()
+ c.handleError(c.addStdoutWriter(w))
+}
+
+// AddStderrWriter configures this Cmd to tee the child's stderr to the given
+// Writer. If this Writer is a Closer and is not os.Stdout or os.Stderr, it will
+// be closed when the process exits.
+func (c *Cmd) AddStderrWriter(w io.Writer) {
+ c.sh.Ok()
+ c.handleError(c.addStderrWriter(w))
+}
+
// Start starts the command.
func (c *Cmd) Start() {
c.sh.Ok()
@@ -136,24 +161,23 @@
c.handleError(c.run())
}
-// Output calls Start followed by Wait, then returns the command's stdout and
-// stderr.
-func (c *Cmd) Output() (string, string) {
+// Stdout calls Start followed by Wait, then returns the command's stdout.
+func (c *Cmd) Stdout() string {
c.sh.Ok()
- stdout, stderr, err := c.output()
- c.handleError(err)
- return stdout, stderr
-}
-
-// CombinedOutput calls Start followed by Wait, then returns the command's
-// combined stdout and stderr.
-func (c *Cmd) CombinedOutput() string {
- c.sh.Ok()
- res, err := c.combinedOutput()
+ res, err := c.stdout()
c.handleError(err)
return res
}
+// StdoutStderr calls Start followed by Wait, then returns the command's stdout
+// and stderr.
+func (c *Cmd) StdoutStderr() (string, string) {
+ c.sh.Ok()
+ stdout, stderr, err := c.stdoutStderr()
+ c.handleError(err)
+ return stdout, stderr
+}
+
// Process returns the underlying process handle for the command.
func (c *Cmd) Process() *os.Process {
c.sh.Ok()
@@ -165,20 +189,14 @@
////////////////////////////////////////
// Internals
-func newCmd(sh *Shell, vars map[string]string, name string, args ...string) (*Cmd, error) {
- // Mimics https://golang.org/src/os/exec/exec.go Command.
- if filepath.Base(name) == name {
- if lp, err := exec.LookPath(name); err != nil {
- return nil, err
- } else {
- name = lp
- }
- }
+func newCmdInternal(sh *Shell, vars map[string]string, path string, args []string) (*Cmd, error) {
c := &Cmd{
+ Path: path,
Vars: vars,
Args: args,
sh: sh,
- name: name,
+ c: &exec.Cmd{},
+ waitChan: make(chan error, 1),
condReady: sync.NewCond(&sync.Mutex{}),
condVars: sync.NewCond(&sync.Mutex{}),
recvVars: map[string]string{},
@@ -193,28 +211,60 @@
return c, nil
}
-func (c *Cmd) handleError(err error) {
- c.Err = err
- if c.ExitErrorIsOk {
- if _, ok := err.(*exec.ExitError); ok {
- return
+func newCmd(sh *Shell, vars map[string]string, name string, args ...string) (*Cmd, error) {
+ // Mimics https://golang.org/src/os/exec/exec.go Command.
+ if filepath.Base(name) == name {
+ if lp, err := exec.LookPath(name); err != nil {
+ return nil, err
+ } else {
+ name = lp
}
}
- c.sh.HandleError(err)
+ return newCmdInternal(sh, vars, name, args)
}
-func (c *Cmd) calledStart() bool {
- return c.c != nil
+func (c *Cmd) errorIsOk(err error) bool {
+ if c.ExitErrorIsOk {
+ if _, ok := err.(*exec.ExitError); ok {
+ return true
+ }
+ }
+ return err == nil
}
-func closeAll(closers []io.Closer) {
- for _, c := range closers {
- c.Close()
+func (c *Cmd) handleError(err error) {
+ c.Err = err
+ if !c.errorIsOk(err) {
+ c.sh.HandleError(err)
}
}
-func addWriter(writers *[]io.Writer, w io.Writer) {
+func (c *Cmd) addWriter(writers *[]io.Writer, w io.Writer) {
*writers = append(*writers, w)
+ // Check for os.Stdout and os.Stderr so that we don't close these when a
+ // single command exits. This technique isn't foolproof (since, for example,
+ // os.Stdout may be wrapped in another WriteCloser), but in practice it should
+ // be adequate.
+ if w != os.Stdout && w != os.Stderr {
+ if wc, ok := w.(io.Closer); ok {
+ c.closers = append(c.closers, wc)
+ }
+ }
+}
+
+func (c *Cmd) closeClosers() {
+ for _, c := range c.closers {
+ c.Close() // best-effort; ignore returned error
+ }
+}
+
+func (c *Cmd) isRunning() bool {
+ if !c.started {
+ return false
+ }
+ c.exitedMu.Lock()
+ defer c.exitedMu.Unlock()
+ return !c.exited
}
// recvWriter listens for gosh messages from a child process.
@@ -269,55 +319,100 @@
var writers *[]io.Writer
if f == os.Stdout {
writers = &c.stdoutWriters
+ c.addWriter(writers, &recvWriter{c: c})
} else {
writers = &c.stderrWriters
}
- if !c.SuppressOutput {
- addWriter(writers, f)
+ if c.PropagateOutput {
+ c.addWriter(writers, f)
}
if c.OutputDir != "" {
suffix := "stderr"
if f == os.Stdout {
suffix = "stdout"
}
- name := filepath.Join(c.OutputDir, filepath.Base(c.name)+"."+t+"."+suffix)
+ name := filepath.Join(c.OutputDir, filepath.Base(c.Path)+"."+t+"."+suffix)
file, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
if err != nil {
return nil, err
}
- addWriter(writers, file)
- c.closeAfterWait = append(c.closeAfterWait, file)
- }
- if f == os.Stdout {
- addWriter(writers, &recvWriter{c: c})
+ c.addWriter(writers, file)
}
return io.MultiWriter(*writers...), nil
}
+func (c *Cmd) clone() (*Cmd, error) {
+ vars := make(map[string]string, len(c.Vars))
+ for k, v := range c.Vars {
+ vars[k] = v
+ }
+ args := make([]string, len(c.Args))
+ copy(args, c.Args)
+ res, err := newCmdInternal(c.sh, vars, c.Path, args)
+ if err != nil {
+ return nil, err
+ }
+ res.PropagateOutput = c.PropagateOutput
+ res.OutputDir = c.OutputDir
+ res.ExitErrorIsOk = c.ExitErrorIsOk
+ res.Stdin = c.Stdin
+ return res, nil
+}
+
+func (c *Cmd) stdinPipe() (io.WriteCloser, error) {
+ if c.calledStart {
+ return nil, errAlreadyCalledStart
+ }
+ if c.stdinWriteCloser != nil {
+ return c.stdinWriteCloser, nil
+ }
+ var err error
+ c.stdinWriteCloser, err = c.c.StdinPipe()
+ return c.stdinWriteCloser, err
+}
+
func (c *Cmd) stdoutPipe() (io.Reader, error) {
- if c.calledStart() {
+ if c.calledStart {
return nil, errAlreadyCalledStart
}
p := NewBufferedPipe()
- addWriter(&c.stdoutWriters, p)
- c.closeAfterWait = append(c.closeAfterWait, p)
+ c.addWriter(&c.stdoutWriters, p)
return p, nil
}
func (c *Cmd) stderrPipe() (io.Reader, error) {
- if c.calledStart() {
+ if c.calledStart {
return nil, errAlreadyCalledStart
}
p := NewBufferedPipe()
- addWriter(&c.stderrWriters, p)
- c.closeAfterWait = append(c.closeAfterWait, p)
+ c.addWriter(&c.stderrWriters, p)
return p, nil
}
-func (c *Cmd) start() error {
- if c.calledStart() {
+func (c *Cmd) addStdoutWriter(w io.Writer) error {
+ if c.calledStart {
return errAlreadyCalledStart
}
+ c.addWriter(&c.stdoutWriters, w)
+ return nil
+}
+
+func (c *Cmd) addStderrWriter(w io.Writer) error {
+ if c.calledStart {
+ return errAlreadyCalledStart
+ }
+ c.addWriter(&c.stderrWriters, w)
+ return nil
+}
+
+// TODO(sadovsky): Maybe wrap every child process with a "supervisor" process
+// that calls WatchParent().
+
+func (c *Cmd) start() error {
+ if c.calledStart {
+ return errAlreadyCalledStart
+ }
+ c.calledStart = true
// Protect against Cmd.start() writing to c.c.Process concurrently with
// signal-triggered Shell.cleanup() reading from it.
c.sh.cleanupMu.Lock()
@@ -325,9 +420,16 @@
if c.sh.calledCleanup {
return errAlreadyCalledCleanup
}
- c.c = exec.Command(c.name, c.Args...)
+ // Configure the command.
+ c.c.Path = c.Path
c.c.Env = mapToSlice(c.Vars)
- c.c.Stdin = c.Stdin
+ c.c.Args = append([]string{c.Path}, c.Args...)
+ if c.Stdin != "" {
+ if c.stdinWriteCloser != nil {
+ return errors.New("gosh: cannot both set Stdin and call StdinPipe")
+ }
+ c.c.Stdin = strings.NewReader(c.Stdin)
+ }
t := time.Now().Format("20060102.150405.000000")
var err error
if c.c.Stdout, err = c.initMultiWriter(os.Stdout, t); err != nil {
@@ -336,19 +438,38 @@
if c.c.Stderr, err = c.initMultiWriter(os.Stderr, t); err != nil {
return err
}
- // TODO(sadovsky): Maybe wrap every child process with a "supervisor" process
- // that calls WatchParent().
+ // Start the command.
err = c.c.Start()
if err != nil {
- closeAll(c.closeAfterWait)
+ c.exitedMu.Lock()
+ c.exited = true
+ c.exitedMu.Unlock()
+ c.closeClosers()
+ c.waitChan <- errors.New("gosh: start failed")
+ return err
}
- return err
+ c.started = true
+ // Spawn a "waiter" goroutine that calls exec.Cmd.Wait and thus waits for the
+ // process to exit. Calling exec.Cmd.Wait here rather than in gosh.Cmd.Wait
+ // ensures that the child process is reaped once it exits. Note, gosh.Cmd.wait
+ // blocks on waitChan.
+ go func() {
+ err := c.c.Wait()
+ c.exitedMu.Lock()
+ c.exited = true
+ c.exitedMu.Unlock()
+ c.closeClosers()
+ c.waitChan <- err
+ }()
+ return nil
}
-// TODO(sadovsky): Maybe add timeouts for Cmd.{awaitReady,awaitVars,wait}.
+// TODO(sadovsky): Make it so Cmd.{awaitReady,awaitVars} return an error if/when
+// we detect that the process has exited. Also, maybe add optional timeouts for
+// Cmd.{awaitReady,awaitVars,wait}.
func (c *Cmd) awaitReady() error {
- if !c.calledStart() {
+ if !c.started {
return errDidNotCallStart
} else if c.calledWait {
return errAlreadyCalledWait
@@ -363,7 +484,7 @@
}
func (c *Cmd) awaitVars(keys ...string) (map[string]string, error) {
- if !c.calledStart() {
+ if !c.started {
return nil, errDidNotCallStart
} else if c.calledWait {
return nil, errAlreadyCalledWait
@@ -392,21 +513,27 @@
}
func (c *Cmd) wait() error {
- if !c.calledStart() {
+ if !c.started {
return errDidNotCallStart
} else if c.calledWait {
return errAlreadyCalledWait
}
c.calledWait = true
- err := c.c.Wait()
- closeAll(c.closeAfterWait)
- return err
+ return <-c.waitChan
}
func (c *Cmd) shutdown(sig os.Signal) error {
- if !c.calledStart() {
+ if !c.started {
return errDidNotCallStart
}
+ // TODO(sadovsky): There's a race condition here and in
+ // Shell.terminateRunningCmds. If our Process.Wait returns immediately before
+ // we call Process.Signal, Process.Signal will return an error, "os: process
+ // already finished". Should we add Cmd.Signal and Cmd.Kill methods that
+ // special-case for this error message?
+ if !c.isRunning() {
+ return nil
+ }
if err := c.c.Process.Signal(sig); err != nil {
return err
}
@@ -425,41 +552,29 @@
return c.wait()
}
-func (c *Cmd) output() (string, string, error) {
+func (c *Cmd) stdout() (string, error) {
+ if c.calledStart {
+ return "", errAlreadyCalledStart
+ }
+ var stdout bytes.Buffer
+ c.addWriter(&c.stdoutWriters, &stdout)
+ err := c.run()
+ return stdout.String(), err
+}
+
+func (c *Cmd) stdoutStderr() (string, string, error) {
+ if c.calledStart {
+ return "", "", errAlreadyCalledStart
+ }
var stdout, stderr bytes.Buffer
- addWriter(&c.stdoutWriters, &stdout)
- addWriter(&c.stderrWriters, &stderr)
+ c.addWriter(&c.stdoutWriters, &stdout)
+ c.addWriter(&c.stderrWriters, &stderr)
err := c.run()
return stdout.String(), stderr.String(), err
}
-type threadSafeBuffer struct {
- mu sync.Mutex
- buf bytes.Buffer
-}
-
-func (b *threadSafeBuffer) Write(p []byte) (int, error) {
- b.mu.Lock()
- defer b.mu.Unlock()
- return b.buf.Write(p)
-}
-
-func (b *threadSafeBuffer) String() string {
- b.mu.Lock()
- defer b.mu.Unlock()
- return b.buf.String()
-}
-
-func (c *Cmd) combinedOutput() (string, error) {
- buf := &threadSafeBuffer{}
- addWriter(&c.stdoutWriters, buf)
- addWriter(&c.stderrWriters, buf)
- err := c.run()
- return buf.String(), err
-}
-
func (c *Cmd) process() (*os.Process, error) {
- if !c.calledStart() {
+ if !c.started {
return nil, errDidNotCallStart
}
return c.c.Process, nil
diff --git a/gosh/internal/gosh_example/main.go b/gosh/internal/gosh_example/main.go
index 51d4754..67fa15c 100644
--- a/gosh/internal/gosh_example/main.go
+++ b/gosh/internal/gosh_example/main.go
@@ -12,7 +12,7 @@
)
func ExampleCmds() {
- sh := gosh.NewShell(gosh.Opts{SuppressChildOutput: true})
+ sh := gosh.NewShell(gosh.Opts{})
defer sh.Cleanup()
// Start server.
@@ -26,37 +26,36 @@
// Run client.
binPath = sh.BuildGoPkg("v.io/x/lib/gosh/internal/gosh_example_client")
c = sh.Cmd(binPath, "-addr="+addr)
- stdout, _ := c.Output()
- fmt.Print(stdout)
+ fmt.Print(c.Stdout())
}
var (
- get = gosh.Register("get", lib.Get)
- serve = gosh.Register("serve", lib.Serve)
+ getFn = gosh.Register("get", lib.Get)
+ serveFn = gosh.Register("serve", lib.Serve)
)
func ExampleFns() {
- sh := gosh.NewShell(gosh.Opts{SuppressChildOutput: true})
+ sh := gosh.NewShell(gosh.Opts{})
defer sh.Cleanup()
// Start server.
- c := sh.Fn(serve)
+ c := sh.Fn(serveFn)
c.Start()
c.AwaitReady()
addr := c.AwaitVars("Addr")["Addr"]
fmt.Println(addr)
// Run client.
- c = sh.Fn(get, addr)
- stdout, _ := c.Output()
- fmt.Print(stdout)
+ c = sh.Fn(getFn, addr)
+ fmt.Print(c.Stdout())
}
func ExampleShellMain() {
sh := gosh.NewShell(gosh.Opts{})
defer sh.Cleanup()
- stdout, _ := sh.Main(lib.HelloWorldMain).Output()
- fmt.Print(stdout)
+
+ c := sh.Main(lib.HelloWorldMain)
+ fmt.Print(c.Stdout())
}
func main() {
diff --git a/gosh/registry.go b/gosh/registry.go
index e2511eb..139a4f1 100644
--- a/gosh/registry.go
+++ b/gosh/registry.go
@@ -10,6 +10,7 @@
"bytes"
"encoding/base64"
"encoding/gob"
+ "errors"
"fmt"
"reflect"
)
@@ -29,20 +30,20 @@
// unique across the dependency graph; 'fni' must be a function that accepts
// gob-encodable arguments and returns an error or nothing.
func Register(name string, fni interface{}) *Fn {
- // TODO(sadovsky): Switch to using len(fns) as name, and maybe drop the name
- // argument, if it turns out that initialization order is deterministic.
+ // TODO(sadovsky): Include len(fns) in registered name if it turns out that
+ // initialization order is deterministic.
if _, ok := fns[name]; ok {
- panic(fmt.Errorf("%s: already registered", name))
+ panic(fmt.Errorf("gosh: %q is already registered", name))
}
v := reflect.ValueOf(fni)
t := v.Type()
if t.Kind() != reflect.Func {
- panic(fmt.Errorf("%s: not a function: %v", name, t.Kind()))
+ panic(fmt.Errorf("gosh: %q is not a function: %v", name, t.Kind()))
}
if t.NumOut() > 1 || t.NumOut() == 1 && t.Out(0) != errorType {
- panic(fmt.Errorf("%s: function must return an error or nothing: %v", name, t))
+ panic(fmt.Errorf("gosh: %q must return an error or nothing: %v", name, t))
}
- // Register the function's args with gob. Needed because Shell.Fn() takes
+ // Register the function's args with gob. Needed because Shell.Fn takes
// interface{} arguments.
for i := 0; i < t.NumIn(); i++ {
// Note: Clients are responsible for registering any concrete types stored
@@ -59,11 +60,11 @@
// Call calls the named function, which must have been registered.
func Call(name string, args ...interface{}) error {
- if fn, ok := fns[name]; !ok {
- return fmt.Errorf("unknown function: %s", name)
- } else {
- return fn.Call(args...)
+ fn, err := getFn(name)
+ if err != nil {
+ return err
}
+ return fn.Call(args...)
}
// Call calls the function fn with the input arguments args.
@@ -77,11 +78,7 @@
} else {
// Client passed nil; construct the zero value for this argument based on
// the function signature.
- at := t.In(i)
- if t.IsVariadic() && i == t.NumIn()-1 {
- at = at.Elem()
- }
- av = reflect.Zero(at)
+ av = reflect.Zero(argType(t, i))
}
in = append(in, av)
}
@@ -92,6 +89,52 @@
return nil
}
+// getFn returns the named function.
+func getFn(name string) (*Fn, error) {
+ fn, ok := fns[name]
+ if !ok {
+ return nil, fmt.Errorf("gosh: unknown function %q", name)
+ }
+ return fn, nil
+}
+
+// argType returns the type of the nth argument to a function of type t.
+func argType(t reflect.Type, n int) reflect.Type {
+ if !t.IsVariadic() || n < t.NumIn()-1 {
+ return t.In(n)
+ }
+ return t.In(t.NumIn() - 1).Elem()
+}
+
+// checkCall checks that the named function exists and can be called with the
+// given arguments. Modeled after the implementation of reflect.Value.call.
+func checkCall(name string, args ...interface{}) error {
+ fn, err := getFn(name)
+ if err != nil {
+ return err
+ }
+ t := fn.value.Type()
+ n := t.NumIn()
+ if t.IsVariadic() {
+ n--
+ }
+ if len(args) < n {
+ return errors.New("gosh: too few input arguments")
+ }
+ if !t.IsVariadic() && len(args) > n {
+ return errors.New("gosh: too many input arguments")
+ }
+ for i, arg := range args {
+ if arg == nil {
+ continue
+ }
+ if at, et := reflect.ValueOf(arg).Type(), argType(t, i); !at.AssignableTo(et) {
+ return fmt.Errorf("gosh: cannot use %s as type %s", at, et)
+ }
+ }
+ return nil
+}
+
////////////////////////////////////////
// invocation
@@ -102,10 +145,13 @@
// encInvocation encodes an invocation.
func encInvocation(name string, args ...interface{}) (string, error) {
+ if err := checkCall(name, args...); err != nil {
+ return "", err
+ }
inv := invocation{Name: name, Args: args}
buf := &bytes.Buffer{}
if err := gob.NewEncoder(buf).Encode(inv); err != nil {
- return "", fmt.Errorf("failed to encode invocation: %v", err)
+ return "", fmt.Errorf("gosh: failed to encode invocation: %v", err)
}
// Base64-encode the gob-encoded bytes so that the result can be used as an
// env var value.
@@ -120,7 +166,7 @@
err = gob.NewDecoder(bytes.NewReader(b)).Decode(&inv)
}
if err != nil {
- return "", nil, fmt.Errorf("failed to decode invocation: %v", err)
+ return "", nil, fmt.Errorf("gosh: failed to decode invocation: %v", err)
}
return inv.Name, inv.Args, nil
}
diff --git a/gosh/shell.go b/gosh/shell.go
index dbe8fbb..04705ee 100644
--- a/gosh/shell.go
+++ b/gosh/shell.go
@@ -38,7 +38,6 @@
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")
- errShellErrIsNotNil = errors.New("gosh: Shell.Err is not nil")
)
// Shell represents a shell. Not thread-safe.
@@ -65,15 +64,15 @@
// Opts configures Shell.
type Opts struct {
- // Errorf is called whenever an error is encountered.
+ // Fatalf is called whenever an error is encountered.
// If not specified, defaults to panic(fmt.Sprintf(format, v...)).
- Errorf func(format string, v ...interface{})
+ Fatalf func(format string, v ...interface{})
// Logf is called to log things.
// If not specified, defaults to log.Printf(format, v...).
Logf func(format string, v ...interface{})
// Child stdout and stderr are propagated up to the parent's stdout and stderr
- // iff SuppressChildOutput is false.
- SuppressChildOutput bool
+ // iff PropagateChildOutput is true.
+ PropagateChildOutput bool
// If specified, each child's stdout and stderr streams are also piped to
// files in this directory.
// If not specified, defaults to GOSH_CHILD_OUTPUT_DIR.
@@ -90,12 +89,12 @@
return sh
}
-// HandleError sets sh.Err. If err is not nil, it also calls sh.Opts.Errorf.
+// HandleError sets sh.Err. If err is not nil, it also calls sh.Opts.Fatalf.
func (sh *Shell) HandleError(err error) {
sh.Ok()
sh.Err = err
- if err != nil && sh.Opts.Errorf != nil {
- sh.Opts.Errorf("%v", err)
+ if err != nil && sh.Opts.Fatalf != nil {
+ sh.Opts.Fatalf("%v", err)
}
}
@@ -210,7 +209,7 @@
}
// Panic on incorrect usage of Shell.
if sh.Err != nil {
- panic(errShellErrIsNotNil)
+ panic(fmt.Errorf("gosh: Shell.Err is not nil: %v", sh.Err))
}
sh.cleanupMu.Lock()
defer sh.cleanupMu.Unlock()
@@ -232,11 +231,11 @@
}()
}
-// Note: On error, newShell returns a *Shell with Opts.Errorf initialized to
+// Note: On error, newShell returns a *Shell with Opts.Fatalf initialized to
// simplify things for the caller.
func newShell(opts Opts) (*Shell, error) {
- if opts.Errorf == nil {
- opts.Errorf = func(format string, v ...interface{}) {
+ if opts.Fatalf == nil {
+ opts.Fatalf = func(format string, v ...interface{}) {
panic(fmt.Sprintf(format, v...))
}
}
@@ -263,15 +262,6 @@
}
}
}
- // Set this process's PGID to its PID so that its child processes can be
- // identified reliably.
- // http://man7.org/linux/man-pages/man2/setpgid.2.html
- // TODO(sadovsky): Is there any way to reliably kill all spawned subprocesses
- // without modifying external state?
- if err := syscall.Setpgid(0, 0); err != nil {
- sh.cleanup()
- return sh, err
- }
// Call sh.cleanup() if needed when a termination signal is received.
onTerminationSignal(func(sig os.Signal) {
sh.logf("Received signal: %v\n", sig)
@@ -302,7 +292,7 @@
if err != nil {
return nil, err
}
- c.SuppressOutput = sh.Opts.SuppressChildOutput
+ c.PropagateOutput = sh.Opts.PropagateChildOutput
c.OutputDir = sh.Opts.ChildOutputDir
return c, nil
}
@@ -330,7 +320,7 @@
// Check that fn has the required signature.
t := fn.value.Type()
if t.NumIn() != 0 || t.NumOut() != 0 {
- return nil, errors.New("main function must have no input or output parameters")
+ return nil, errors.New("gosh: main function must have no input or output parameters")
}
b, err := encInvocation(fn.name)
if err != nil {
@@ -345,14 +335,12 @@
// need not hold cleanupMu when accessing sh.cmds below.
var res error
for _, c := range sh.cmds {
- if !c.calledStart() || c.calledWait {
+ if !c.started || c.calledWait {
continue
}
- if err := c.wait(); err != nil {
- sh.logf("Cmd.Wait() failed: %v\n", err)
- if res == nil {
- res = err
- }
+ if err := c.wait(); !c.errorIsOk(err) {
+ sh.logf("%s (PID %d) failed: %v\n", c.Path, c.c.Process.Pid, err)
+ res = err
}
}
return res
@@ -392,7 +380,7 @@
if err != nil {
return "", err
}
- c.SuppressOutput = true
+ c.PropagateOutput = false
if err := c.run(); err != nil {
return "", err
}
@@ -444,7 +432,7 @@
func (sh *Shell) popd() error {
if len(sh.dirStack) == 0 {
- return errors.New("dir stack is empty")
+ return errors.New("gosh: dir stack is empty")
}
dir := sh.dirStack[len(sh.dirStack)-1]
if err := os.Chdir(dir); err != nil {
@@ -468,24 +456,21 @@
func (sh *Shell) forEachRunningCmd(fn func(*Cmd)) bool {
anyRunning := false
for _, c := range sh.cmds {
- if !c.calledStart() || c.c.Process == nil {
- continue // not started
- }
- if pgid, err := syscall.Getpgid(c.c.Process.Pid); err != nil || pgid != os.Getpid() {
- continue // not our child
- }
- anyRunning = true
- if fn != nil {
- fn(c)
+ if c.isRunning() {
+ anyRunning = true
+ if fn != nil {
+ fn(c)
+ }
}
}
return anyRunning
}
-// Note: It is safe to run Shell.terminateRunningCmds() concurrently with
-// Cmd.wait(). In particular, Shell.terminateRunningCmds() only reads
-// c.c.Process.{Pid,Path} and calls c.c.Process.{Signal,Kill}, all of which are
-// thread-safe with Cmd.wait().
+// Note: It is safe to run Shell.terminateRunningCmds concurrently with the
+// waiter goroutine and with Cmd.wait. In particular, Shell.terminateRunningCmds
+// only reads c.c.Process.Pid and calls c.c.Process.{Signal,Kill}, all of which
+// are thread-safe with the waiter goroutine and with Cmd.wait.
+// TODO(sadovsky): Not quite true. See TODO in Cmd.shutdown.
func (sh *Shell) terminateRunningCmds() {
// Try SIGINT first; if that doesn't work, use SIGKILL.
anyRunning := sh.forEachRunningCmd(func(c *Cmd) {
@@ -493,11 +478,11 @@
sh.logf("%d.Signal(os.Interrupt) failed: %v\n", c.c.Process.Pid, err)
}
})
- // If any child is still running, wait for 50ms.
+ // If any child is still running, wait for 100ms.
if anyRunning {
- time.Sleep(50 * time.Millisecond)
+ time.Sleep(100 * time.Millisecond)
anyRunning = sh.forEachRunningCmd(func(c *Cmd) {
- sh.logf("%s (PID %d) did not die\n", c.c.Path, c.c.Process.Pid)
+ sh.logf("%s (PID %d) did not die\n", c.Path, c.c.Process.Pid)
})
}
// If any child is still running, wait for another second, then send SIGKILL
@@ -515,14 +500,8 @@
func (sh *Shell) cleanup() {
sh.calledCleanup = true
- // Terminate all children that are still running. Note, newShell() calls
- // syscall.Setpgid().
- pgid, pid := syscall.Getpgrp(), os.Getpid()
- if pgid != pid {
- sh.logf("PGID (%d) != PID (%d); skipping subprocess termination\n", pgid, pid)
- } else {
- sh.terminateRunningCmds()
- }
+ // Terminate all children that are still running.
+ sh.terminateRunningCmds()
// Close and delete all temporary files.
for _, tempFile := range sh.tempFiles {
name := tempFile.Name()
diff --git a/gosh/shell_test.go b/gosh/shell_test.go
index 89a9555..a6df636 100644
--- a/gosh/shell_test.go
+++ b/gosh/shell_test.go
@@ -5,14 +5,14 @@
package gosh_test
// TODO(sadovsky): Add more tests:
-// - variadic function registration and invocation
// - effects of Shell.Cleanup
-// - Cmd.{Wait,Run}
-// - Shell.{Args,Wait,Rename,MakeTempFile,MakeTempDir}
+// - Cmd.Clone
+// - Shell.{Vars,Args,Rename,MakeTempFile,MakeTempDir}
// - Opts (including defaulting behavior)
// - {,Maybe}WatchParent
import (
+ "bufio"
"errors"
"fmt"
"io"
@@ -64,16 +64,71 @@
}
}
-func makeErrorf(t *testing.T) func(string, ...interface{}) {
+func toString(t *testing.T, r io.Reader) string {
+ b, err := ioutil.ReadAll(r)
+ ok(t, err)
+ return string(b)
+}
+
+func makeFatalf(t *testing.T) func(string, ...interface{}) {
return func(format string, v ...interface{}) {
debug.PrintStack()
t.Fatalf(format, v...)
}
}
-func TestPushdPopd(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+////////////////////////////////////////
+// Simple functions
+
+// Simplified versions of various Unix commands.
+var (
+ catFn = gosh.Register("cat", func() {
+ io.Copy(os.Stdout, os.Stdin)
+ })
+ echoFn = gosh.Register("echo", func() {
+ fmt.Println(os.Args[1])
+ })
+ readFn = gosh.Register("read", func() {
+ bufio.NewReader(os.Stdin).ReadString('\n')
+ })
+)
+
+// Functions with parameters.
+var (
+ exitFn = gosh.Register("exit", func(code int) {
+ os.Exit(code)
+ })
+ sleepFn = gosh.Register("sleep", func(d time.Duration, code int) {
+ time.Sleep(d)
+ os.Exit(code)
+ })
+ printFn = gosh.Register("print", func(v ...interface{}) {
+ fmt.Print(v...)
+ })
+ printfFn = gosh.Register("printf", func(format string, v ...interface{}) {
+ fmt.Printf(format, v...)
+ })
+)
+
+////////////////////////////////////////
+// Tests
+
+func TestCustomFatalf(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
+
+ var calledFatalf bool
+ sh.Opts.Fatalf = func(string, ...interface{}) { calledFatalf = true }
+ sh.HandleError(fakeError)
+ // Note, our deferred sh.Cleanup() should succeed despite this error.
+ nok(t, sh.Err)
+ eq(t, calledFatalf, true)
+}
+
+func TestPushdPopd(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+ defer sh.Cleanup()
+
startDir, err := os.Getwd()
ok(t, err)
parentDir := filepath.Dir(startDir)
@@ -95,35 +150,39 @@
ok(t, err)
eq(t, cwd, startDir)
// The next sh.Popd() will fail.
- var calledErrorf bool
- sh.Opts.Errorf = func(string, ...interface{}) { calledErrorf = true }
+ sh.Opts.Fatalf = func(string, ...interface{}) {}
sh.Popd()
- // Note, our deferred sh.Cleanup() should succeed despite this error.
nok(t, sh.Err)
- eq(t, calledErrorf, true)
+}
+
+func evalSymlinks(t *testing.T, dir string) string {
+ var err error
+ dir, err = filepath.EvalSymlinks(dir)
+ ok(t, err)
+ return dir
+}
+
+func getwdEvalSymlinks(t *testing.T) string {
+ dir, err := os.Getwd()
+ ok(t, err)
+ return evalSymlinks(t, dir)
}
func TestPushdNoPopdCleanup(t *testing.T) {
- startDir, err := os.Getwd()
- ok(t, err)
-
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ startDir := getwdEvalSymlinks(t)
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
tmpDir := sh.MakeTempDir()
sh.Pushd(tmpDir)
- cwd, err := os.Getwd()
- ok(t, err)
- eq(t, cwd, tmpDir)
+ eq(t, getwdEvalSymlinks(t), evalSymlinks(t, tmpDir))
// There is no matching popd; the cwd is tmpDir, which is deleted by Cleanup.
// Cleanup needs to put us back in startDir, otherwise all subsequent Pushd
// calls will fail.
sh.Cleanup()
- cwd, err = os.Getwd()
- ok(t, err)
- eq(t, cwd, startDir)
+ eq(t, getwdEvalSymlinks(t), startDir)
}
func TestCmds(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
// Start server.
@@ -137,61 +196,178 @@
// Run client.
binPath = sh.BuildGoPkg("v.io/x/lib/gosh/internal/gosh_example_client")
c = sh.Cmd(binPath, "-addr="+addr)
- stdout, _ := c.Output()
- eq(t, stdout, "Hello, world!\n")
+ eq(t, c.Stdout(), "Hello, world!\n")
}
var (
- get = gosh.Register("get", lib.Get)
- serve = gosh.Register("serve", lib.Serve)
+ getFn = gosh.Register("get", lib.Get)
+ serveFn = gosh.Register("serve", lib.Serve)
)
func TestFns(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
// Start server.
- c := sh.Fn(serve)
+ c := sh.Fn(serveFn)
c.Start()
c.AwaitReady()
addr := c.AwaitVars("Addr")["Addr"]
neq(t, addr, "")
// Run client.
- c = sh.Fn(get, addr)
- stdout, _ := c.Output()
- eq(t, stdout, "Hello, world!\n")
+ c = sh.Fn(getFn, addr)
+ eq(t, c.Stdout(), "Hello, world!\n")
}
func TestShellMain(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
- stdout, _ := sh.Main(lib.HelloWorldMain).Output()
- eq(t, stdout, "Hello, world!\n")
+
+ c := sh.Main(lib.HelloWorldMain)
+ eq(t, c.Stdout(), "Hello, world!\n")
}
-var write = gosh.Register("write", func(stdout, stderr bool) error {
- tenMs := 10 * time.Millisecond
+// Functions designed for TestRegistry.
+var (
+ printIntsFn = gosh.Register("printInts", func(v ...int) {
+ var vi []interface{}
+ for _, x := range v {
+ vi = append(vi, x)
+ }
+ fmt.Print(vi...)
+ })
+ printfIntsFn = gosh.Register("printfInts", func(format string, v ...int) {
+ var vi []interface{}
+ for _, x := range v {
+ vi = append(vi, x)
+ }
+ fmt.Printf(format, vi...)
+ })
+)
+
+// Tests function signature-checking and execution.
+func TestRegistry(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+ defer sh.Cleanup()
+
+ // Variadic functions. Non-variadic functions are sufficiently covered in
+ // other tests.
+ eq(t, sh.Fn(printFn).Stdout(), "")
+ eq(t, sh.Fn(printFn, 0).Stdout(), "0")
+ eq(t, sh.Fn(printFn, 0, "foo").Stdout(), "0foo")
+ eq(t, sh.Fn(printfFn, "").Stdout(), "")
+ eq(t, sh.Fn(printfFn, "%v", 0).Stdout(), "0")
+ eq(t, sh.Fn(printfFn, "%v%v", 0, "foo").Stdout(), "0foo")
+ eq(t, sh.Fn(printIntsFn, 1, 2).Stdout(), "1 2")
+ eq(t, sh.Fn(printfIntsFn, "%v %v", 1, 2).Stdout(), "1 2")
+
+ // Error cases.
+ sh.Opts.Fatalf = func(string, ...interface{}) {}
+ reset := func() {
+ nok(t, sh.Err)
+ sh.Err = nil
+ }
+
+ // Too few arguments.
+ sh.Fn(exitFn)
+ reset()
+ sh.Fn(sleepFn, time.Second)
+ reset()
+ sh.Fn(printfFn)
+ reset()
+
+ // Too many arguments.
+ sh.Fn(exitFn, 0, 0)
+ reset()
+ sh.Fn(sleepFn, time.Second, 0, 0)
+ reset()
+
+ // Wrong argument types.
+ sh.Fn(exitFn, "foo")
+ reset()
+ sh.Fn(sleepFn, 0, 0)
+ reset()
+ sh.Fn(printfFn, 0)
+ reset()
+ sh.Fn(printfFn, 0, 0)
+ reset()
+
+ // Wrong variadic argument types.
+ sh.Fn(printIntsFn, 0.5)
+ reset()
+ sh.Fn(printIntsFn, 0, 0.5)
+ reset()
+ sh.Fn(printfIntsFn, "%v", 0.5)
+ reset()
+ sh.Fn(printfIntsFn, "%v", 0, 0.5)
+ reset()
+
+ // Unsupported argument types.
+ var p *int
+ sh.Fn(printFn, p)
+ reset()
+ sh.Fn(printfFn, "%v", p)
+ reset()
+}
+
+func TestStdin(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+ defer sh.Cleanup()
+
+ c := sh.Main(catFn)
+ c.Stdin = "foo\n"
+ // We set c.Stdin and did not call c.StdinPipe(), so stdin should close and
+ // cat should exit immediately.
+ eq(t, c.Stdout(), "foo\n")
+
+ c = sh.Main(catFn)
+ c.StdinPipe().Write([]byte("foo\n"))
+ // The "cat" command only exits when stdin is closed, so we must explicitly
+ // close the stdin pipe. Note, it's safe to call c.StdinPipe multiple times.
+ c.StdinPipe().Close()
+ eq(t, c.Stdout(), "foo\n")
+
+ c = sh.Main(readFn)
+ c.StdinPipe().Write([]byte("foo\n"))
+ // The "read" command exits when it sees a newline, so Cmd.Wait (and thus
+ // Cmd.Run) should return immediately; it should not be necessary to close the
+ // stdin pipe.
+ c.Run()
+
+ c = sh.Main(catFn)
+ // No stdin, so cat should exit immediately.
+ eq(t, c.Stdout(), "")
+
+ // It's an error (detected at command start time) to both set c.Stdin and call
+ // c.StdinPipe. Note, this indirectly tests that Shell.Cleanup works even if
+ // some Cmd.Start failed.
+ c = sh.Main(catFn)
+ c.Stdin = "foo"
+ c.StdinPipe().Write([]byte("bar"))
+ c.StdinPipe().Close()
+ sh.Opts.Fatalf = func(string, ...interface{}) {}
+ c.Start()
+ nok(t, sh.Err)
+}
+
+var writeFn = gosh.Register("write", func(stdout, stderr bool) error {
if stdout {
- time.Sleep(tenMs)
if _, err := os.Stdout.Write([]byte("A")); err != nil {
return err
}
}
if stderr {
- time.Sleep(tenMs)
if _, err := os.Stderr.Write([]byte("B")); err != nil {
return err
}
}
if stdout {
- time.Sleep(tenMs)
if _, err := os.Stdout.Write([]byte("A")); err != nil {
return err
}
}
if stderr {
- time.Sleep(tenMs)
if _, err := os.Stderr.Write([]byte("B")); err != nil {
return err
}
@@ -199,92 +375,169 @@
return nil
})
-func toString(r io.Reader) string {
- if b, err := ioutil.ReadAll(r); err != nil {
- panic(err)
- } else {
- return string(b)
- }
-}
-
func TestStdoutStderr(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
// Write to stdout only.
- c := sh.Fn(write, true, false)
+ c := sh.Fn(writeFn, true, false)
stdoutPipe, stderrPipe := c.StdoutPipe(), c.StderrPipe()
- eq(t, c.CombinedOutput(), "AA")
- eq(t, toString(stdoutPipe), "AA")
- eq(t, toString(stderrPipe), "")
- stdout, stderr := sh.Fn(write, true, false).Output()
+ stdout, stderr := c.StdoutStderr()
eq(t, stdout, "AA")
eq(t, stderr, "")
+ eq(t, toString(t, stdoutPipe), "AA")
+ eq(t, toString(t, stderrPipe), "")
// Write to stderr only.
- c = sh.Fn(write, false, true)
+ c = sh.Fn(writeFn, false, true)
stdoutPipe, stderrPipe = c.StdoutPipe(), c.StderrPipe()
- eq(t, c.CombinedOutput(), "BB")
- eq(t, toString(stdoutPipe), "")
- eq(t, toString(stderrPipe), "BB")
- stdout, stderr = sh.Fn(write, false, true).Output()
+ stdout, stderr = c.StdoutStderr()
eq(t, stdout, "")
eq(t, stderr, "BB")
+ eq(t, toString(t, stdoutPipe), "")
+ eq(t, toString(t, stderrPipe), "BB")
// Write to both stdout and stderr.
- c = sh.Fn(write, true, true)
+ c = sh.Fn(writeFn, true, true)
stdoutPipe, stderrPipe = c.StdoutPipe(), c.StderrPipe()
- eq(t, c.CombinedOutput(), "ABAB")
- eq(t, toString(stdoutPipe), "AA")
- eq(t, toString(stderrPipe), "BB")
- stdout, stderr = sh.Fn(write, true, true).Output()
+ stdout, stderr = c.StdoutStderr()
eq(t, stdout, "AA")
eq(t, stderr, "BB")
+ eq(t, toString(t, stdoutPipe), "AA")
+ eq(t, toString(t, stderrPipe), "BB")
}
-var sleep = gosh.Register("sleep", func(d time.Duration) {
- time.Sleep(d)
+var writeMoreFn = gosh.Register("writeMore", func() {
+ sh := gosh.NewShell(gosh.Opts{})
+ defer sh.Cleanup()
+
+ c := sh.Fn(writeFn, true, true)
+ c.AddStdoutWriter(os.Stdout)
+ c.AddStderrWriter(os.Stderr)
+ c.Run()
+
+ fmt.Fprint(os.Stdout, " stdout done")
+ fmt.Fprint(os.Stderr, " stderr done")
})
+// Tests that it's safe to add os.Stdout and os.Stderr as writers.
+func TestAddWriters(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+ defer sh.Cleanup()
+
+ stdout, stderr := sh.Fn(writeMoreFn).StdoutStderr()
+ eq(t, stdout, "AA stdout done")
+ eq(t, stderr, "BB stderr done")
+}
+
+// Tests piping from one Cmd's stdout/stderr to another's stdin. It should be
+// possible to wait on just the last Cmd.
+func TestPiping(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+ defer sh.Cleanup()
+
+ echo := sh.Main(echoFn, "foo")
+ cat := sh.Main(catFn)
+ echo.AddStdoutWriter(cat.StdinPipe())
+ echo.Start()
+ eq(t, cat.Stdout(), "foo\n")
+
+ // This time, pipe both stdout and stderr to cat's stdin.
+ c := sh.Fn(writeFn, true, true)
+ cat = sh.Main(catFn)
+ c.AddStdoutWriter(cat.StdinPipe())
+ c.AddStderrWriter(cat.StdinPipe())
+ c.Start()
+ // Note, we can't assume any particular ordering of stdout and stderr, so we
+ // simply check the length of the combined output.
+ eq(t, len(cat.Stdout()), 4)
+}
+
func TestShutdown(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
for _, d := range []time.Duration{0, time.Second} {
for _, s := range []os.Signal{os.Interrupt, os.Kill} {
fmt.Println(d, s)
- c := sh.Fn(sleep, d)
+ c := sh.Fn(sleepFn, d, 0)
c.Start()
+ // Wait for a bit to allow the zero-sleep commands to exit, to test that
+ // Shutdown succeeds for an exited process and to avoid the race condition
+ // in Cmd.Shutdown.
time.Sleep(10 * time.Millisecond)
c.Shutdown(s)
}
}
}
-var exit = gosh.Register("exit", func(code int) {
- os.Exit(code)
-})
+func TestShellWait(t *testing.T) {
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
+ defer sh.Cleanup()
+
+ d0 := time.Duration(0)
+ d200 := 200 * time.Millisecond
+
+ c0 := sh.Fn(sleepFn, d0, 0) // not started
+ c1 := sh.Fn(sleepFn, d0, 0) // failed to start
+ c2 := sh.Fn(sleepFn, d200, 0) // running and will succeed
+ c3 := sh.Fn(sleepFn, d200, 1) // running and will fail
+ c4 := sh.Fn(sleepFn, d0, 0) // succeeded
+ c5 := sh.Fn(sleepFn, d0, 0) // succeeded, called wait
+ c6 := sh.Fn(sleepFn, d0, 1) // failed
+ c7 := sh.Fn(sleepFn, d0, 1) // failed, called wait
+
+ c3.ExitErrorIsOk = true
+ c6.ExitErrorIsOk = true
+ c7.ExitErrorIsOk = true
+
+ // Configure the "failed to start" command.
+ c1.StdinPipe()
+ c1.Stdin = "foo"
+ sh.Opts.Fatalf = func(string, ...interface{}) {}
+ c1.Start()
+ nok(t, sh.Err)
+ sh.Err = nil
+ sh.Opts.Fatalf = makeFatalf(t)
+
+ // Start commands, call wait.
+ for _, c := range []*gosh.Cmd{c2, c3, c4, c5, c6, c7} {
+ c.Start()
+ }
+ c5.Wait()
+ c7.Wait()
+ sh.Wait()
+
+ // It should be possible to run existing unstarted commands, and to create and
+ // run new commands, after calling Shell.Wait.
+ c0.Run()
+ sh.Fn(sleepFn, d0, 0).Run()
+ sh.Fn(sleepFn, d0, 0).Start()
+
+ // Call Shell.Wait again.
+ sh.Wait()
+}
func TestExitErrorIsOk(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
defer sh.Cleanup()
// Exit code 0 is not an error.
- c := sh.Fn(exit, 0)
+ c := sh.Fn(exitFn, 0)
c.Run()
ok(t, c.Err)
ok(t, sh.Err)
// Exit code 1 is an error.
- c = sh.Fn(exit, 1)
+ c = sh.Fn(exitFn, 1)
c.ExitErrorIsOk = true
c.Run()
nok(t, c.Err)
ok(t, sh.Err)
// If ExitErrorIsOk is false, exit code 1 triggers sh.HandleError.
- sh.Opts.Errorf = func(string, ...interface{}) {}
- c = sh.Fn(exit, 1)
+ c = sh.Fn(exitFn, 1)
+ sh.Opts.Fatalf = func(string, ...interface{}) {}
c.Run()
nok(t, c.Err)
nok(t, sh.Err)
@@ -298,14 +551,14 @@
sh.Ok()
}()
func() { // errShellErrIsNotNil
- sh := gosh.NewShell(gosh.Opts{Errorf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
defer sh.Cleanup()
sh.Err = fakeError
defer func() { neq(t, recover(), nil) }()
sh.Ok()
}()
func() { // errAlreadyCalledCleanup
- sh := gosh.NewShell(gosh.Opts{Errorf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
sh.Cleanup()
defer func() { neq(t, recover(), nil) }()
sh.Ok()
@@ -320,14 +573,14 @@
sh.HandleError(fakeError)
}()
func() { // errShellErrIsNotNil
- sh := gosh.NewShell(gosh.Opts{Errorf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
defer sh.Cleanup()
sh.Err = fakeError
defer func() { neq(t, recover(), nil) }()
sh.HandleError(fakeError)
}()
func() { // errAlreadyCalledCleanup
- sh := gosh.NewShell(gosh.Opts{Errorf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Logf})
sh.Cleanup()
defer func() { neq(t, recover(), nil) }()
sh.HandleError(fakeError)
@@ -345,14 +598,14 @@
// Tests that sh.Cleanup succeeds even if sh.Err is not nil.
func TestCleanupAfterError(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
sh.Err = fakeError
sh.Cleanup()
}
// Tests that sh.Cleanup can be called multiple times.
func TestMultipleCleanup(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: makeErrorf(t), Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: makeFatalf(t), Logf: t.Logf})
sh.Cleanup()
sh.Cleanup()
}
diff --git a/vlog/flags_test.go b/vlog/flags_test.go
index 575504e..f241165 100644
--- a/vlog/flags_test.go
+++ b/vlog/flags_test.go
@@ -42,7 +42,7 @@
})
func TestFlags(t *testing.T) {
- sh := gosh.NewShell(gosh.Opts{Errorf: t.Fatalf, Logf: t.Logf})
+ sh := gosh.NewShell(gosh.Opts{Fatalf: t.Fatalf, Logf: t.Logf})
defer sh.Cleanup()
sh.Fn(child).Run()
}