| // 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" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "sync" |
| "syscall" |
| "time" |
| |
| "v.io/x/lib/cmdline" |
| "v.io/x/lib/envvar" |
| ) |
| |
| // 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). |
| // Sequence is not thread safe. It also good practice to use a new |
| // instance of a Sequence in defer's. |
| // |
| // 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 addition 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. |
| // |
| // Errors returned by Sequence augment those returned by the underlying |
| // packages with details of the exact call that generated those errors. |
| // This means that it is not possible to test directly for errors from |
| // those packages. The GetOriginalError function can be used to obtain |
| // the error from the underlying package, or the IsTimeout, IsNotExists etc |
| // functions can be used on the wrapped error. The ExitCode method |
| // is also provided to convert to the exit codes expected by the |
| // v.io/x/lib/cmdline package which is often used in conjunction with |
| // Sequence. |
| type Sequence struct { |
| // NOTE: we use a struct as the return value of all |
| // Sequence methods to ensure that code of the form: |
| // if err := s.WriteFile(); err != nil {...} |
| // does not compile. |
| *sequence |
| } |
| |
| type sequence struct { |
| r *executor |
| 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 |
| verbosity *bool |
| dryRun *bool |
| cmdDir string |
| timeout time.Duration |
| serializedWriterLock sync.Mutex |
| } |
| |
| // NewSequence creates an instance of Sequence with default values for its |
| // environment, stdin, stderr, stdout and other supported options. |
| // If the environment parameter is nil or empty then the current value of |
| // os.Environ() will be used instead. |
| func NewSequence(env map[string]string, stdin io.Reader, stdout, stderr io.Writer, color, dryRun, verbose bool) Sequence { |
| if len(env) == 0 { |
| env = envvar.SliceToMap(os.Environ()) |
| } |
| s := Sequence{ |
| &sequence{ |
| r: newExecutor(env, stdin, stdout, stderr, color, dryRun, verbose), |
| defaultStdin: stdin, |
| }, |
| } |
| s.defaultStdout, s.defaultStderr = s.serializeWriter(stdout), s.serializeWriter(stderr) |
| return s |
| } |
| |
| // RunOpts returns the values of dryRun and verbose that were used to |
| // create this sequence. |
| func (s Sequence) RunOpts() (dryRun bool, verbose bool) { |
| opts := s.getOpts() |
| return opts.dryRun, opts.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. 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 |
| } |
| |
| // SetEnv arranges for the next call to Run, Call, Start or Last to use the supplied |
| // environment variables. These will be cleared and not used for any calls |
| // to Run, Call or Last beyond the next one. |
| func (s Sequence) SetEnv(env map[string]string) Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.env = env |
| return s |
| } |
| |
| // Env arranges for the next call to Run, Call, Start or Last to use |
| // the result of merging the supplied environment variables with those |
| // specified when the sequence was created or with those set by the most |
| // recent call to the Env method. These 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 |
| } |
| if s.env != nil { |
| s.env = envvar.MergeMaps(s.env, env) |
| } else { |
| e := s.getOpts().env |
| s.env = envvar.MergeMaps(e, env) |
| } |
| return s |
| } |
| |
| // Verbosity arranges for the next call to Run, Call, Start or Last to use the |
| // specified verbosity. This will be cleared and not used for any calls |
| // to Run, Call or Last beyond the next one. |
| func (s Sequence) Verbose(verbosity bool) Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.verbosity = &verbosity |
| return s |
| } |
| |
| // Dir sets the working directory for the next subprocess that is created |
| // via Run, Call, Start or Last to the supplied parameter. This is the only |
| // way to safely set the working directory of a command when multiple threads |
| // are used. |
| func (s Sequence) Dir(dir string) Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.cmdDir = dir |
| return s |
| } |
| |
| // DryRun arranges for the next call to Run, Call, Start or Last to use the |
| // specified dry run value. This will be cleared and not used for any calls |
| // to Run, Call or Last beyond the next one. |
| func (s Sequence) DryRun(dryRun bool) Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.dryRun = &dryRun |
| 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, Start 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 |
| } |
| |
| type wrappedError struct { |
| oe, we error |
| } |
| |
| func (ie *wrappedError) Error() string { |
| return ie.we.Error() |
| } |
| |
| // Error returns the error, if any, stored in the Sequence. |
| func (s Sequence) Error() error { |
| if s.err != nil && len(s.caller) > 0 { |
| return &wrappedError{oe: s.err, we: fmt.Errorf("%s: %v", s.caller, s.err)} |
| } |
| return s.err |
| } |
| |
| // TranslateExitCode translates errors from the "os/exec" package that |
| // contain exit codes into cmdline.ErrExitCode errors. |
| func TranslateExitCode(err error) error { |
| return translateExitCode(GetOriginalError(err)) |
| } |
| |
| func translateExitCode(err error) error { |
| if exit, ok := err.(*exec.ExitError); ok { |
| if wait, ok := exit.Sys().(syscall.WaitStatus); ok { |
| if status := wait.ExitStatus(); wait.Exited() && status != 0 { |
| return cmdline.ErrExitCode(status) |
| } |
| } |
| } |
| return err |
| } |
| |
| // GetOriginalError gets the original error wrapped in the supplied err. |
| // If the given err has not been wrapped by Sequence, then the supplied error |
| // is returned. |
| func GetOriginalError(err error) error { |
| if we, ok := err.(*wrappedError); ok { |
| return we.oe |
| } |
| return err |
| } |
| |
| // IsExist returns a boolean indicating whether the error is known |
| // to report that a file or directory already exists. |
| func IsExist(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return os.IsExist(we.oe) |
| } |
| return os.IsExist(err) |
| } |
| |
| // IsNotExist returns a boolean indicating whether the error is known |
| // to report that a file or directory does not exist. |
| func IsNotExist(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return os.IsNotExist(we.oe) |
| } |
| return os.IsNotExist(err) |
| } |
| |
| // IsPermission returns a boolean indicating whether the error is known |
| // to report that permission is denied. |
| func IsPermission(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return os.IsPermission(we.oe) |
| } |
| return os.IsPermission(err) |
| } |
| |
| // IsTimeout returns a boolean indicating whether the error is a result of |
| // a timeout. |
| func IsTimeout(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return we.oe == commandTimedOutErr |
| } |
| return err == commandTimedOutErr |
| } |
| |
| 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) |
| } |
| |
| // reset all state except s.err |
| func (s Sequence) reset() { |
| s.stdin, s.stdout, s.stderr, s.env = nil, nil, nil, nil |
| s.opts, s.verbosity, s.dryRun = nil, nil, nil |
| s.cmdDir = "" |
| s.reading = false |
| s.timeout = 0 |
| } |
| |
| func cleanup(p1, p2 *io.PipeWriter, stdinCh, stderrCh chan error) error { |
| p1.Close() |
| p2.Close() |
| 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(logdir bool, from string, to io.Writer) { |
| if fi, err := os.Open(from); err == nil { |
| io.Copy(to, fi) |
| fi.Close() |
| } |
| if !logdir { |
| return |
| } |
| if wd, err := os.Getwd(); err == nil { |
| fmt.Fprintf(to, "Current Directory: %v\n", wd) |
| } |
| } |
| |
| type sharedLockWriter struct { |
| mu *sync.Mutex |
| f io.Writer |
| } |
| |
| func (lw *sharedLockWriter) Write(d []byte) (int, error) { |
| lw.mu.Lock() |
| defer lw.mu.Unlock() |
| return lw.f.Write(d) |
| } |
| |
| func (s Sequence) serializeWriter(a io.Writer) io.Writer { |
| if a != nil { |
| return &sharedLockWriter{&s.serializedWriterLock, a} |
| } |
| return nil |
| } |
| |
| // isWriterTTY returns whether the io.Writer w is an os.File pointing at a tty. |
| func isWriterTTY(w io.Writer) (isTTY bool) { |
| _, isOsFile := w.(*os.File) |
| if isOsFile { |
| stat, _ := os.Stdin.Stat() |
| isTTY = (stat.Mode() & (os.ModeDevice | os.ModeCharDevice)) == (os.ModeDevice | os.ModeCharDevice) |
| } |
| return isTTY |
| } |
| |
| func (s Sequence) initAndDefer(h *Handle) func() { |
| if s.stdout == nil && s.stderr == nil { |
| fout, err := ioutil.TempFile("", "seq") |
| if err != nil { |
| return func() {} |
| } |
| opts := s.getOpts() |
| opts.stdout, opts.stderr = s.serializeWriter(fout), s.serializeWriter(fout) |
| if len(s.env) > 0 { |
| opts.env = s.env |
| } |
| if s.reading { |
| opts.stdin = s.stdin |
| } |
| if s.verbosity != nil { |
| opts.verbose = *s.verbosity |
| } |
| if s.dryRun != nil { |
| opts.dryRun = *s.dryRun |
| } |
| opts.dir = s.cmdDir |
| s.setOpts(opts) |
| if h != nil { |
| return func() { |
| h.stderr = useIfNotNil(s.defaultStderr, os.Stderr) |
| h.filename = fout.Name() |
| h.doneErr = nil |
| fout.Close() |
| } |
| } |
| return func() { |
| filename := fout.Name() |
| fout.Close() |
| defer func() { os.Remove(filename); s.opts = nil }() |
| if s.err != nil { |
| writeOutput(true, filename, useIfNotNil(s.defaultStderr, os.Stderr)) |
| } |
| if opts.verbose && s.defaultStderr != s.defaultStdout { |
| writeOutput(false, filename, useIfNotNil(s.defaultStdout, os.Stdout)) |
| } |
| } |
| } |
| opts := s.getOpts() |
| rStdout, wStdout := io.Pipe() |
| rStderr, wStderr := io.Pipe() |
| // If the requested stdout/stderr is a raw os.File pointing at a tty, |
| // use that directly. This allows the invocation of tools |
| // like editors, which expect stdout/stderr to be ttys. |
| if isWriterTTY(s.stdout) { |
| opts.stdout = s.stdout |
| } else { |
| opts.stdout = wStdout |
| } |
| if isWriterTTY(s.stderr) { |
| opts.stderr = s.stderr |
| } else { |
| opts.stderr = wStderr |
| } |
| if len(s.env) > 0 { |
| opts.env = s.env |
| } |
| if s.reading { |
| opts.stdin = s.stdin |
| } |
| var stdinCh, stderrCh chan error |
| stdout, stderr := s.serializeWriter(s.stdout), s.serializeWriter(s.stderr) |
| if stdout != nil { |
| stdinCh = make(chan error) |
| go copy(stdout, rStdout, stdinCh) |
| } else { |
| opts.stdout = s.defaultStdout |
| } |
| if stderr != nil { |
| stderrCh = make(chan error) |
| go copy(stderr, rStderr, stderrCh) |
| } else { |
| opts.stderr = s.defaultStderr |
| } |
| if s.verbosity != nil { |
| opts.verbose = *s.verbosity |
| } |
| if s.dryRun != nil { |
| opts.dryRun = *s.dryRun |
| } |
| opts.dir = s.cmdDir |
| s.setOpts(opts) |
| if h != nil { |
| return func() { |
| h.filename = "" |
| h.doneErr = cleanup(wStdout, wStderr, stdinCh, stderrCh) |
| } |
| } |
| return func() { |
| if err := cleanup(wStdout, wStderr, stdinCh, stderrCh); err != nil && s.err == nil { |
| // If we haven't already encountered an error and we fail to |
| // cleanup then record the error from the cleanup |
| s.err = err |
| } |
| // reset does not affect s.err |
| s.reset() |
| } |
| } |
| |
| func fmtStringArgs(args ...string) string { |
| if len(args) == 0 { |
| return "" |
| } |
| var out bytes.Buffer |
| for _, a := range args { |
| fmt.Fprintf(&out, ", %q", a) |
| } |
| return out.String() |
| } |
| |
| // 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(nil)() |
| s.setError(s.r.run(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.Done() |
| defer s.initAndDefer(nil)() |
| s.setError(s.r.run(s.timeout, s.getOpts(), path, args...), fmt.Sprintf("Last(%q%s)", path, fmtStringArgs(args...))) |
| return s.Error() |
| } |
| |
| // 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(nil)() |
| s.setError(s.r.function(s.getOpts(), fn, format, args...), fmt.Sprintf(format, args...)) |
| return s |
| } |
| |
| // Handle represents a command running in the background. |
| type Handle struct { |
| stdout, stderr io.Writer |
| doneErr error |
| filename string |
| deferFn func() |
| cmd *exec.Cmd |
| } |
| |
| // Kill terminates the currently running background process. |
| func (h *Handle) Kill() error { |
| return h.cmd.Process.Kill() |
| } |
| |
| // Pid returns the pid of the running process. |
| func (h *Handle) Pid() int { |
| return h.cmd.Process.Pid |
| } |
| |
| func (h *Handle) Signal(sig os.Signal) error { |
| return h.cmd.Process.Signal(sig) |
| } |
| |
| // Wait waits for the currently running background process to terminate. |
| func (h *Handle) Wait() error { |
| err := h.cmd.Wait() |
| h.deferFn() |
| if len(h.filename) > 0 { |
| if err != nil { |
| writeOutput(true, h.filename, h.stderr) |
| } |
| os.Remove(h.filename) |
| return err |
| } |
| return h.doneErr |
| } |
| |
| // Start runs the given command as a subprocess in background and returns |
| // a handle that can be used to kill and/or wait for that background process. |
| // Start is a terminating function. |
| func (s Sequence) Start(path string, args ...string) (*Handle, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| h := &Handle{} |
| h.deferFn = s.initAndDefer(h) |
| cmd, err := s.r.start(s.timeout, s.getOpts(), path, args...) |
| h.cmd = cmd |
| s.setError(err, fmt.Sprintf("Start(%q%s)", path, fmtStringArgs(args...))) |
| return h, s.Error() |
| } |
| |
| // 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 |
| } |
| opts := s.getOpts() |
| if s.verbosity != nil { |
| opts.verbose = *s.verbosity |
| } |
| s.r.output(opts, output) |
| return s |
| } |
| |
| // Fprintf calls fmt.Fprintf. |
| func (s Sequence) Fprintf(f io.Writer, format string, args ...interface{}) Sequence { |
| if s.err != nil { |
| return s |
| } |
| fmt.Fprintf(f, format, args...) |
| 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 |
| err := s.r.alwaysRun(func() error { |
| return os.Chdir(cwd) |
| }, fmt.Sprintf("sequence done popd %q", cwd)) |
| if 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 |
| } |
| |
| // 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) |
| err = s.r.alwaysRun(func() error { |
| return os.Chdir(dir) |
| }, fmt.Sprintf("pushd %q", dir)) |
| s.setError(err, "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] |
| err := s.r.alwaysRun(func() error { |
| return os.Chdir(last) |
| }, fmt.Sprintf("popd %q", last)) |
| s.setError(err, "Popd() -> "+last) |
| 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 |
| } |
| err := s.r.alwaysRun(func() error { |
| return os.Chdir(dir) |
| }, fmt.Sprintf("cd %q", dir)) |
| s.setError(err, "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 |
| } |
| err := s.r.call(func() error { return os.Chmod(dir, mode) }, fmt.Sprintf("chmod %v %q", mode, dir)) |
| 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 |
| } |
| err := s.r.call(func() error { return os.MkdirAll(dir, mode) }, fmt.Sprintf("mkdir -p %q", dir)) |
| s.setError(err, 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 |
| } |
| err := s.r.call(func() error { return os.RemoveAll(dir) }, fmt.Sprintf("rm -rf %q", dir)) |
| s.setError(err, 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 |
| } |
| err := s.r.call(func() error { return os.Remove(file) }, fmt.Sprintf("rm %q", file)) |
| s.setError(err, 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 |
| } |
| err := s.r.call(func() error { |
| if err := os.Rename(src, dst); err != nil { |
| // Check if the rename operation failed |
| // because the source and destination are |
| // located on different mount points. |
| linkErr, ok := err.(*os.LinkError) |
| if !ok { |
| return err |
| } |
| errno, ok := linkErr.Err.(syscall.Errno) |
| if !ok || errno != syscall.EXDEV { |
| return err |
| } |
| // Fall back to a non-atomic rename. |
| cmd := exec.Command("mv", src, dst) |
| return cmd.Run() |
| } |
| return nil |
| }, fmt.Sprintf("mv %q %q", src, dst)) |
| s.setError(err, 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 |
| } |
| err := s.r.call(func() error { return os.Symlink(src, dst) }, fmt.Sprintf("ln -s %q %q", src, dst)) |
| s.setError(err, fmt.Sprintf("Symlink(%s, %s)", src, dst)) |
| return s |
| } |
| |
| // 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) (f *os.File, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.call(func() error { |
| f, err = os.Open(name) |
| return err |
| }, fmt.Sprintf("open %q", name)) |
| s.setError(err, fmt.Sprintf("Open(%s)", name)) |
| err = s.Done() |
| return |
| } |
| |
| // 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) (f *os.File, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.call(func() error { |
| f, err = os.OpenFile(name, flag, perm) |
| return err |
| }, fmt.Sprintf("open file %q", name)) |
| s.setError(err, fmt.Sprintf("OpenFile(%s, 0x%x, %s)", name, flag, perm)) |
| err = s.Done() |
| return |
| } |
| |
| // 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) (f *os.File, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.call(func() error { |
| var err error |
| f, err = os.Create(name) |
| return err |
| }, fmt.Sprintf("create %q", name)) |
| s.setError(err, fmt.Sprintf("Create(%s)", name)) |
| err = s.Done() |
| return |
| } |
| |
| // 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) (fi []os.FileInfo, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.alwaysRun(func() error { |
| fi, err = ioutil.ReadDir(dirname) |
| return err |
| }, fmt.Sprintf("ls %q", dirname)) |
| s.setError(err, fmt.Sprintf("ReadDir(%s)", dirname)) |
| err = s.Done() |
| return |
| } |
| |
| // 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) (bytes []byte, err error) { |
| |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.alwaysRun(func() error { |
| bytes, err = ioutil.ReadFile(filename) |
| return err |
| }, fmt.Sprintf("read %q", filename)) |
| s.setError(err, fmt.Sprintf("ReadFile(%s)", filename)) |
| err = s.Done() |
| return |
| } |
| |
| // 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.call(func() error { |
| return ioutil.WriteFile(filename, data, perm) |
| }, fmt.Sprintf("write %q", filename)) |
| 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 io.Writer, src io.Reader) (n int64, err error) { |
| if s.err != nil { |
| return 0, s.Done() |
| } |
| s.r.call(func() error { |
| n, err = io.Copy(dst, src) |
| return err |
| }, "io.copy") |
| s.setError(err, fmt.Sprintf("Copy(%s, %s)", dst, src)) |
| err = s.Done() |
| return |
| } |
| |
| // 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) (fi os.FileInfo, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.alwaysRun(func() error { |
| fi, err = os.Stat(name) |
| return err |
| }, fmt.Sprintf("stat %q", name)) |
| s.setError(err, fmt.Sprintf("Stat(%s)", name)) |
| err = s.Done() |
| return |
| } |
| |
| // Lstat is a wrapper around os.Lstat that handles options such as |
| // "verbose" or "dry run". Lstat is a terminating function. |
| func (s Sequence) Lstat(name string) (fi os.FileInfo, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| s.r.alwaysRun(func() error { |
| fi, err = os.Lstat(name) |
| return err |
| }, fmt.Sprintf("lstat %q", name)) |
| s.setError(err, fmt.Sprintf("Lstat(%s)", name)) |
| err = s.Done() |
| return |
| } |
| |
| // Readlink is a wrapper around os.Readlink that handles options such as |
| // "verbose" or "dry run". Lstat is a terminating function. |
| func (s Sequence) Readlink(name string) (link string, err error) { |
| if s.err != nil { |
| return "", s.Done() |
| } |
| s.r.alwaysRun(func() error { |
| link, err = os.Readlink(name) |
| return err |
| }, fmt.Sprintf("readlink %q", name)) |
| s.setError(err, fmt.Sprintf("Readlink(%s)", name)) |
| err = s.Done() |
| return |
| } |
| |
| // 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) (tmpDir string, err error) { |
| if s.err != nil { |
| return "", s.Done() |
| } |
| if dir == "" { |
| dir = os.Getenv("TMPDIR") |
| } |
| tmpDir = filepath.Join(dir, prefix+"XXXXXX") |
| s.r.call(func() error { |
| tmpDir, err = ioutil.TempDir(dir, prefix) |
| return err |
| }, fmt.Sprintf("mkdir -p %q", tmpDir)) |
| s.setError(err, fmt.Sprintf("TempDir(%s,%s)", dir, prefix)) |
| err = s.Done() |
| return |
| } |
| |
| // TempFile is a wrapper around ioutil.TempFile that handles options |
| // such as "verbose" or "dry run". |
| func (s Sequence) TempFile(dir, prefix string) (f *os.File, err error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| if dir == "" { |
| dir = os.Getenv("TMPDIR") |
| } |
| s.r.call(func() error { |
| f, err = ioutil.TempFile(dir, prefix) |
| return err |
| }, fmt.Sprintf("tempFile %q %q", dir, prefix)) |
| s.setError(err, fmt.Sprintf("TempFile(%s,%s)", dir, prefix)) |
| err = s.Done() |
| return |
| } |
| |
| // IsDir is a wrapper around os.Stat with appropriate logging that |
| // returns true of dirname exists and is a directory. |
| // IsDir is a terminating function. |
| func (s Sequence) IsDir(dirname string) (bool, error) { |
| if s.err != nil { |
| return false, s.Done() |
| } |
| var fileInfo os.FileInfo |
| var err error |
| err = s.r.alwaysRun(func() error { |
| fileInfo, err = os.Stat(dirname) |
| return err |
| }, fmt.Sprintf("isdir %q", dirname)) |
| if IsNotExist(err) { |
| return false, nil |
| } |
| if err != nil { |
| return false, err |
| } |
| s.setError(err, fmt.Sprintf("IsDir(%s)", dirname)) |
| return fileInfo.IsDir(), s.Done() |
| } |
| |
| // IsFile is a wrapper around os.Stat with appropriate logging that |
| // returns true if file exists and is not a directory. |
| // IsFile is a terminating function. |
| func (s Sequence) IsFile(file string) (bool, error) { |
| if s.err != nil { |
| return false, s.Done() |
| } |
| var fileInfo os.FileInfo |
| var err error |
| err = s.r.alwaysRun(func() error { |
| fileInfo, err = os.Stat(file) |
| return err |
| }, fmt.Sprintf("isfile %q", file)) |
| if IsNotExist(err) { |
| return false, nil |
| } |
| if err != nil { |
| return false, err |
| } |
| s.setError(err, fmt.Sprintf("IsFile(%s)", file)) |
| return !fileInfo.IsDir(), s.Done() |
| } |
| |
| // AssertDirExists asserts that the specified directory exists with appropriate |
| // logging. |
| func (s Sequence) AssertDirExists(dirname string) Sequence { |
| if s.err != nil { |
| return s |
| } |
| isdir, err := s.IsDir(dirname) |
| if !isdir && err == nil { |
| err = os.ErrNotExist |
| } |
| s.setError(err, fmt.Sprintf("AssertDirExists(%s)", dirname)) |
| return s |
| } |
| |
| // AssertFileExists asserts that the specified file exists with appropriate |
| // logging. |
| func (s Sequence) AssertFileExists(file string) Sequence { |
| if s.err != nil { |
| return s |
| } |
| isfile, err := s.IsFile(file) |
| if !isfile && err == nil { |
| err = os.ErrNotExist |
| } |
| s.setError(err, fmt.Sprintf("AssertFileExists(%s)", file)) |
| return s |
| } |
| |
| func copy(to io.Writer, from io.Reader, ch chan error) { |
| _, err := io.Copy(to, from) |
| ch <- err |
| } |