| // 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 ( |
| "io" |
| "os" |
| "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). |
| // |
| // Modifier methods are provided that influence the behaviour of the |
| // next invocation of the Run method to set timeouts (Timeout) and to |
| // capture output (Capture), an additional modifier method (Opts) is |
| // provided the set the options for the remaining methods and is generally |
| // used to control logging. |
| // For example, the following will result in a timeout error. |
| // |
| // err := s.Timed(time.Second).Run("sleep","10").Done() |
| // |
| // A sequence of commands must be terminated with a call to a 'terminating' |
| // method. The simplest is the Done method 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 == "foo" |
| // |
| // 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 |
| stdout, stderr io.Writer |
| opts *Opts |
| 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 to write its stdout and stderr |
| // output to the supplied io.Writers. This will be cleared and not used for |
| // any calls to Run 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 to use the supplied options. |
| // They will be cleared and not used for any calls to Run 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 to be subject to the specified |
| // timeout. The timeout will be cleared and not used any calls to Run 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 { |
| return s.err |
| } |
| |
| func (s *Sequence) setOpts(opts Opts) { |
| s.opts = &opts |
| } |
| |
| 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 { |
| return func() {} |
| } |
| 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 |
| } |
| } |
| } |
| |
| // 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.err = s.r.command(s.timeout, s.GetOpts(), path, args...) |
| return s |
| } |
| |
| // 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.err = s.r.FunctionWithOpts(s.GetOpts(), fn, format, 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.err = s.r.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.err = err |
| } |
| 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.err = s.r.MkdirAll(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.err = s.r.RemoveAll(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.err = s.r.Remove(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.err = s.r.Rename(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.err = s.r.Symlink(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. Done is a terminating function. |
| func (s *Sequence) Done() error { |
| err := s.err |
| s.err = nil |
| s.reset() |
| return err |
| } |
| |
| // 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.err = err |
| 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.err = err |
| 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.err = err |
| return data, 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.err = err |
| 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.err = err |
| return name, s.Done() |
| } |
| |
| // IsDir is a wrapper around os.Stat with appropriate logging. |
| // IsDir is a terminating function. |
| func (s *Sequence) IsDir(name string) (bool, error) { |
| if s.err != nil { |
| return false, s.Done() |
| } |
| t, err := s.r.IsDir(name) |
| s.err = err |
| 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 |
| } |