| // 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, unless an error |
| // is encountered, in which case the output from the command that failed (both |
| // stdout and stderr) is written to the stderr io.Writer specified via |
| // NewSequence. In addition, in verbose mode, command execution logging |
| // is written to the stdout an 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 (Timed), to |
| // capture output (Capture), input (Read) and to set the environment (Env). |
| // For example, the following will result in a timeout error. |
| // |
| // err := s.Timeout(time.Second).Run("sleep","10").Done() |
| // err := s.Timeout(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.Capture(o, nil).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. |
| type Sequence struct { |
| r *Run |
| err error |
| caller string |
| stdout, stderr io.Writer |
| stdin io.Reader |
| reading bool |
| env map[string]string |
| opts *Opts |
| defaultStdin io.Reader |
| defaultStdout, defaultStderr io.Writer |
| 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), |
| defaultStdin: stdin, |
| defaultStdout: stdout, |
| defaultStderr: stderr, |
| } |
| } |
| |
| // 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. Specifying nil for |
| // a writer will result in using the the corresponding io.Writer supplied |
| // to NewSequence. ioutil.Discard should be used to discard output. |
| func (s *Sequence) Capture(stdout, stderr io.Writer) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.stdout, s.stderr = stdout, stderr |
| return s |
| } |
| |
| // Read arranges for the next call to Run or Last to read from the supplied |
| // io.Reader. This will be cleared and not used for any calls to Run or Last |
| // beyond the next one. Specifying nil will result in reading from os.DevNull. |
| func (s *Sequence) Read(stdin io.Reader) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.reading = true |
| s.stdin = stdin |
| return s |
| } |
| |
| // Env arranges for the next call to Run, Call or Last to use the supplied |
| // environment variables. This will be cleared and not used for any calls |
| // to Run, Call or Last beyond the next one. |
| func (s *Sequence) Env(env map[string]string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.env = env |
| return s |
| } |
| |
| // internal getOpts that doesn't override stdin, stdout, stderr |
| func (s *Sequence) getOpts() Opts { |
| var opts Opts |
| if s.opts != nil { |
| opts = *s.opts |
| } else { |
| opts = s.r.Opts() |
| } |
| return opts |
| } |
| |
| // 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. It has no effect for calls to Call. |
| func (s *Sequence) Timeout(timeout time.Duration) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.timeout = timeout |
| return s |
| } |
| |
| func (s *Sequence) setOpts(opts Opts) { |
| s.opts = &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 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.stdin, s.stdout, s.stderr, s.env, s.opts = nil, nil, nil, nil, nil |
| s.reading = false |
| 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 useIfNotNil(a, b io.Writer) io.Writer { |
| if a != nil { |
| return a |
| } |
| return b |
| } |
| |
| func writeOutput(from string, to io.Writer) { |
| if fi, err := os.Open(from); err == nil { |
| io.Copy(to, fi) |
| fi.Close() |
| } |
| if wd, err := os.Getwd(); err == nil { |
| fmt.Fprintf(to, "Current Directory: %v\n", wd) |
| } |
| } |
| |
| func (s *Sequence) initAndDefer() func() { |
| if s.stdout == nil && s.stderr == nil { |
| fout, err := ioutil.TempFile("", "seq") |
| if err != nil { |
| return func() {} |
| } |
| opts := s.getOpts() |
| opts.Stdout = fout |
| opts.Stderr = fout |
| opts.Env = s.env |
| if s.reading { |
| opts.Stdin = s.stdin |
| } |
| s.setOpts(opts) |
| return func() { |
| filename := fout.Name() |
| fout.Close() |
| defer func() { os.Remove(filename); s.opts = nil }() |
| if s.err != nil { |
| writeOutput(filename, useIfNotNil(s.defaultStderr, os.Stderr)) |
| } |
| if opts.Verbose && s.defaultStderr != s.defaultStdout { |
| writeOutput(filename, useIfNotNil(s.defaultStdout, os.Stdout)) |
| } |
| } |
| } |
| opts := s.getOpts() |
| rStdin, wStdin := io.Pipe() |
| rStderr, wStderr := io.Pipe() |
| opts.Stdout = wStdin |
| opts.Stderr = wStderr |
| opts.Env = s.env |
| if s.reading { |
| opts.Stdin = s.stdin |
| } |
| var stdinCh, stderrCh chan error |
| if s.stdout != nil { |
| stdinCh = make(chan error) |
| go copy(s.stdout, rStdin, stdinCh) |
| } else { |
| opts.Stdout = s.defaultStdout |
| } |
| if s.stderr != nil { |
| stderrCh = make(chan error) |
| go copy(s.stderr, rStderr, stderrCh) |
| } else { |
| opts.Stderr = s.defaultStderr |
| } |
| s.setOpts(opts) |
| 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 |
| } |