blob: 0873c4a67466c48ec5f99e53b94e603939400a88 [file] [log] [blame]
// 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 runutil
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"time"
)
// Sequence provides for convenient chaining of multiple calls to its
// methods to avoid repeated tests for error returns. The usage is:
//
// err := s.Run("echo", "a").Run("echo", "b").Done()
//
// The first method to encounter an error short circuits any following
// methods and the result of that first error is returned by the
// Done method or any of the other 'terminating methods' (see below).
//
// Unless directed to specific stdout and stderr io.Writers using Capture(), the
// stdout and stderr output from the command is discarded, except in verbose
// mode or upon error: when in verbose mode (set either via NewSequence or an
// Opt) or when the command fails, all the command's output (stdout and stderr)
// is written to the stdout io.Writer configured either via NewSequence or an
// Opt. In addition, in verbose mode, command execution logging is written to
// the stdout and stderr io.Writers configured via NewSequence.
//
// Modifier methods are provided that influence the behaviour of the
// next invocation of the Run method to set timeouts (Timeout), to
// capture output (Capture), an set options (Opts).
// For example, the following will result in a timeout error.
//
// err := s.Timed(time.Second).Run("sleep","10").Done()
// err := s.Timed(time.Second).Last("sleep","10")
//
// A sequence of commands must be terminated with a call to a 'terminating'
// method. The simplest are the Done or Last methods used in the examples above,
// but there are other methods which typically return results in addition to
// error, such as ReadFile(filename string) ([]byte, error). Here the usage
// would be:
//
// o.Stdout, _ = os.Create("foo")
// data, err := s.Opts(o).Run("echo","b").ReadFile("foo")
// // data == "b"
//
// Note that terminating functions, even those that take an action, may
// return an error generated by a previous method.
//
// In addtion to Run which will always run a command as a subprocess,
// the Call method will invoke a function. Note that Capture and Timeout
// do not affect such calls, but Opts can be used to control logging.
type Sequence struct {
r *Run
err error
caller string
stdout, stderr io.Writer
opts *Opts
dirs []string
timeout time.Duration
}
// NewSequence creates an instance of Sequence with default values for its
// environment, stdin, stderr, stdout and other supported options.
func NewSequence(env map[string]string, stdin io.Reader, stdout, stderr io.Writer, color, dryRun, verbose bool) *Sequence {
return &Sequence{r: NewRun(env, stdin, stdout, stderr, color, dryRun, verbose)}
}
// Capture arranges for the next call to Run or Last to write its stdout and
// stderr output to the supplied io.Writers. This will be cleared and not used
// for any calls to Run or Last beyond the next one.
func (s *Sequence) Capture(stdout, stderr io.Writer) *Sequence {
if s.err != nil {
return s
}
s.stdout, s.stderr = stdout, stderr
return s
}
// Opts arranges for the next call to Run or Last to use the supplied options.
// This will be cleared and not used for any calls to Run or Last beyond the
// next one.
func (s *Sequence) Opts(opts Opts) *Sequence {
if s.err != nil {
return s
}
s.opts = &opts
return s
}
// Timeout arranges for the next call to Run or Last to be subject to the
// specified timeout. The timeout will be cleared and not used any calls to Run
// or Last beyond the next one.
func (s *Sequence) Timeout(timeout time.Duration) *Sequence {
if s.err != nil {
return s
}
s.timeout = timeout
return s
}
func (s *Sequence) GetOpts() Opts {
if s.opts != nil {
return *s.opts
}
return s.r.Opts()
}
func (s *Sequence) Error() error {
if s.err != nil && len(s.caller) > 0 {
return fmt.Errorf("%s: %v", s.caller, s.err)
}
return s.err
}
func (s *Sequence) setOpts(opts Opts) {
s.opts = &opts
}
func fmtError(depth int, err error, detail string) string {
_, file, line, _ := runtime.Caller(depth + 1)
return fmt.Sprintf("%s:%d: %s", filepath.Base(file), line, detail)
}
func (s *Sequence) setError(err error, detail string) {
if err == nil || s.err != nil {
return
}
s.err = err
s.caller = fmtError(2, err, detail)
}
func (s *Sequence) reset() {
s.stdout, s.stderr, s.opts = nil, nil, nil
s.timeout = 0
}
func (s *Sequence) done(p1, p2 *io.PipeWriter, stdinCh, stderrCh chan error) error {
p1.Close()
p2.Close()
defer s.reset()
if stdinCh != nil {
if err := <-stdinCh; err != nil {
return err
}
}
if stderrCh != nil {
if err := <-stderrCh; err != nil {
return err
}
}
return nil
}
func (s *Sequence) initAndDefer() func() {
if s.stdout == nil && s.stderr == nil {
f, err := ioutil.TempFile("", "seq")
if err != nil {
return func() {}
}
opts := s.GetOpts()
stdout := opts.Stdout
if stdout == nil {
return func() {}
}
opts.Stdout = f
opts.Stderr = f
s.setOpts(opts)
return func() {
filename := f.Name()
f.Close()
if opts.Verbose || s.err != nil {
// TODO(cnicolaou): probably best to stream this out rather
// than buffer the whole file into memory.
if data, err := ioutil.ReadFile(filename); err == nil {
fmt.Fprint(stdout, string(data))
if wd, err := os.Getwd(); err == nil {
fmt.Fprintf(stdout, "Current Directory: %v\n", wd)
}
}
}
os.Remove(filename)
s.opts = nil
}
}
opts := s.GetOpts()
rStdin, wStdin := io.Pipe()
rStderr, wStderr := io.Pipe()
opts.Stdout = wStdin
opts.Stderr = wStderr
s.setOpts(opts)
var stdinCh, stderrCh chan error
if s.stdout != nil {
stdinCh = make(chan error)
go copy(s.stdout, rStdin, stdinCh)
}
if s.stderr != nil {
stderrCh = make(chan error)
go copy(s.stderr, rStderr, stderrCh)
}
return func() {
if err := s.done(wStdin, wStderr, stdinCh, stderrCh); err != nil && s.err == nil {
s.err = err
}
}
}
func fmtArgs(args ...interface{}) string {
if len(args) == 0 {
return ""
}
out := &bytes.Buffer{}
for _, a := range args {
if _, ok := a.(string); ok {
out.WriteString(fmt.Sprintf(" ,%q", a))
} else {
out.WriteString(fmt.Sprintf(" ,%s", a))
}
}
return out.String()
}
func fmtStringArgs(args ...string) string {
if len(args) == 0 {
return ""
}
out := &bytes.Buffer{}
for _, a := range args {
out.WriteString(", \"")
out.WriteString(a)
out.WriteString("\"")
}
return out.String()
}
// Pushd pushes the current directory onto a stack and changes directory
// to the specified one. Calling any terminating function will pop back
// to the first element in the stack on completion of that function.
func (s *Sequence) Pushd(dir string) *Sequence {
cwd, err := os.Getwd()
if err != nil {
s.setError(err, "Pushd("+dir+"): os.Getwd")
return s
}
s.dirs = append(s.dirs, cwd)
s.setError(s.r.Chdir(dir), "Pushd("+dir+")")
return s
}
// Popd popds the last directory from the directory stack and chdir's to it.
// Calling any termination function will pop back to the first element in
// the stack on completion of that function.
func (s *Sequence) Popd() *Sequence {
if s.err != nil {
return s
}
if len(s.dirs) == 0 {
s.setError(fmt.Errorf("directory stack is empty"), "Popd()")
return s
}
last := s.dirs[len(s.dirs)-1]
s.dirs = s.dirs[:len(s.dirs)-1]
s.setError(s.r.Chdir(last), "Popd() -> "+last)
return s
}
// Run runs the given command as a subprocess.
func (s *Sequence) Run(path string, args ...string) *Sequence {
if s.err != nil {
return s
}
defer s.initAndDefer()()
s.setError(s.r.command(s.timeout, s.GetOpts(), path, args...), fmt.Sprintf("Run(%q%s)", path, fmtStringArgs(args...)))
return s
}
// Last runs the given command as a subprocess and returns an error
// immediately terminating the sequence, it is equivalent to
// calling s.Run(path, args...).Done().
func (s *Sequence) Last(path string, args ...string) error {
if s.err != nil {
return s.Done()
}
defer s.initAndDefer()()
s.setError(s.r.command(s.timeout, s.GetOpts(), path, args...), fmt.Sprintf("Last(%q%s)", path, fmtStringArgs(args...)))
return s.Done()
}
// Call runs the given function. Note that Capture and Timeout have no
// effect on invocations of Call, but Opts can control logging.
func (s *Sequence) Call(fn func() error, format string, args ...interface{}) *Sequence {
if s.err != nil {
return s
}
defer s.initAndDefer()()
s.setError(s.r.FunctionWithOpts(s.GetOpts(), fn, format, args...), fmt.Sprintf("Call(%s,%s%s)", fn, format, fmtArgs(args)))
return s
}
// Chdir is a wrapper around os.Chdir that handles options such as
// "verbose" or "dry run".
func (s *Sequence) Chdir(dir string) *Sequence {
if s.err != nil {
return s
}
s.setError(s.r.Chdir(dir), "Chdir("+dir+")")
return s
}
// Chmod is a wrapper around os.Chmod that handles options such as
// "verbose" or "dry run".
func (s *Sequence) Chmod(dir string, mode os.FileMode) *Sequence {
if s.err != nil {
return s
}
if err := s.r.Chmod(dir, mode); err != nil {
s.setError(err, fmt.Sprintf("Chmod(%s, %s)", dir, mode))
}
return s
}
// MkdirAll is a wrapper around os.MkdirAll that handles options such
// as "verbose" or "dry run".
func (s *Sequence) MkdirAll(dir string, mode os.FileMode) *Sequence {
if s.err != nil {
return s
}
s.setError(s.r.MkdirAll(dir, mode), fmt.Sprintf("MkdirAll(%s, %s)", dir, mode))
return s
}
// RemoveAll is a wrapper around os.RemoveAll that handles options
// such as "verbose" or "dry run".
func (s *Sequence) RemoveAll(dir string) *Sequence {
if s.err != nil {
return s
}
s.setError(s.r.RemoveAll(dir), fmt.Sprintf("RemoveAll(%s)", dir))
return s
}
// Remove is a wrapper around os.Remove that handles options
// such as "verbose" or "dry run".
func (s *Sequence) Remove(file string) *Sequence {
if s.err != nil {
return s
}
s.setError(s.r.Remove(file), fmt.Sprintf("Remove(%s)", file))
return s
}
// Rename is a wrapper around os.Rename that handles options such as
// "verbose" or "dry run".
func (s *Sequence) Rename(src, dst string) *Sequence {
if s.err != nil {
return s
}
s.setError(s.r.Rename(src, dst), fmt.Sprintf("Rename(%s, %s)", src, dst))
return s
}
// Symlink is a wrapper around os.Symlink that handles options such as
// "verbose" or "dry run".
func (s *Sequence) Symlink(src, dst string) *Sequence {
if s.err != nil {
return s
}
s.setError(s.r.Symlink(src, dst), fmt.Sprintf("Symlink(%s, %s)", src, dst))
return s
}
// Output logs the given list of lines using the currently in effect verbosity
// as specified by Opts, or the default otherwise.
func (s *Sequence) Output(output []string) *Sequence {
if s.err != nil {
return s
}
s.r.OutputWithOpts(s.GetOpts(), output)
return s
}
// Done returns the error stored in the Sequence and pops back to the first
// entry in the directory stack if Pushd has been called. Done is a terminating
// function. There is no need to ensure that Done is called before returning
// from a function that uses a sequence unless it is necessary to pop the
// stack.
func (s *Sequence) Done() error {
rerr := s.Error()
s.err = nil
s.caller = ""
s.reset()
if len(s.dirs) > 0 {
cwd := s.dirs[0]
s.dirs = nil
if err := s.r.Chdir(cwd); err != nil {
detail := "Done: Chdir(" + cwd + ")"
if rerr == nil {
s.setError(err, detail)
} else {
// In the unlikely event that Chdir fails in addition to an
// earlier error, we append an appropriate error message.
s.err = fmt.Errorf("%v\n%v", rerr, fmtError(1, err, detail))
}
return s.Error()
}
}
return rerr
}
// Open is a wrapper around os.Open that handles options such as
// "verbose" or "dry run". Open is a terminating function.
func (s *Sequence) Open(name string) (*os.File, error) {
if s.err != nil {
return nil, s.Done()
}
f, err := s.r.Open(name)
s.setError(err, fmt.Sprintf("Open(%s)", name))
return f, s.Done()
}
// OpenFile is a wrapper around os.OpenFile that handles options such as
// "verbose" or "dry run". OpenFile is a terminating function.
func (s *Sequence) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) {
if s.err != nil {
return nil, s.Done()
}
f, err := s.r.OpenFile(name, flag, perm)
s.setError(err, fmt.Sprintf("OpenFile(%s, %s, %s)", name, flag, perm))
return f, s.Done()
}
// Create is a wrapper around os.Create that handles options such as "verbose"
// or "dry run". Create is a terminating function.
func (s *Sequence) Create(name string) (*os.File, error) {
if s.err != nil {
return nil, s.Done()
}
f, err := s.r.Create(name)
s.setError(err, fmt.Sprintf("Create(%s)", name))
return f, s.Done()
}
// ReadDir is a wrapper around ioutil.ReadDir that handles options
// such as "verbose" or "dry run". ReadDir is a terminating function.
func (s *Sequence) ReadDir(dirname string) ([]os.FileInfo, error) {
if s.err != nil {
return nil, s.Done()
}
fi, err := s.r.ReadDir(dirname)
s.setError(err, fmt.Sprintf("ReadDir(%s)", dirname))
return fi, s.Done()
}
// ReadFile is a wrapper around ioutil.ReadFile that handles options
// such as "verbose" or "dry run". ReadFile is a terminating function.
func (s *Sequence) ReadFile(filename string) ([]byte, error) {
if s.err != nil {
return nil, s.Done()
}
data, err := s.r.ReadFile(filename)
s.setError(err, fmt.Sprintf("ReadFile(%s)", filename))
return data, s.Done()
}
// WriteFile is a wrapper around ioutil.WriteFile that handles options
// such as "verbose" or "dry run".
func (s *Sequence) WriteFile(filename string, data []byte, perm os.FileMode) *Sequence {
if s.err != nil {
return s
}
err := s.r.WriteFile(filename, data, perm)
s.setError(err, fmt.Sprintf("WriteFile(%s, %10s, %s)", filename, data, perm))
return s
}
// Copy is a wrapper around io.Copy that handles options such as "verbose" or
// "dry run". Copy is a terminating function.
func (s *Sequence) Copy(dst *os.File, src io.Reader) (int64, error) {
if s.err != nil {
return 0, s.Done()
}
n, err := s.r.Copy(dst, src)
s.setError(err, fmt.Sprintf("Copy(%s, %s)", dst, src))
return n, s.Done()
}
// Stat is a wrapper around os.Stat that handles options such as
// "verbose" or "dry run". Stat is a terminating function.
func (s *Sequence) Stat(name string) (os.FileInfo, error) {
if s.err != nil {
return nil, s.Done()
}
fi, err := s.r.Stat(name)
s.setError(err, fmt.Sprintf("Stat(%s)", name))
return fi, s.Done()
}
// TempDir is a wrapper around ioutil.TempDir that handles options
// such as "verbose" or "dry run". TempDir is a terminating function.
func (s *Sequence) TempDir(dir, prefix string) (string, error) {
if s.err != nil {
return "", s.Done()
}
name, err := s.r.TempDir(dir, prefix)
s.setError(err, fmt.Sprintf("TempDir(%s,%s)", dir, prefix))
return name, s.Done()
}
// IsDir is a wrapper around os.Stat with appropriate logging.
// IsDir is a terminating function.
func (s *Sequence) IsDir(dirname string) (bool, error) {
if s.err != nil {
return false, s.Done()
}
t, err := s.r.IsDir(dirname)
s.setError(err, fmt.Sprintf("IsDir(%s)", dirname))
return t, s.Done()
}
// DirExists tests if a directory exists with appropriate logging.
// DirExists is a terminating function.
func (s *Sequence) DirectoryExists(dir string) (bool, error) {
if s.err != nil {
return false, s.Done()
}
isdir, err := s.r.IsDir(dir)
if err != nil {
return false, s.Done()
}
return isdir, s.Done()
}
// FileExists tests if a file exists with appropriate logging.
// FileExists is a terminating function.
func (s *Sequence) FileExists(file string) (bool, error) {
if s.err != nil {
return false, s.Done()
}
_, err := s.r.Stat(file)
return err == nil, s.Done()
}
func copy(to io.Writer, from io.Reader, ch chan error) {
_, err := io.Copy(to, from)
ch <- err
}