blob: 13f6633fb4aa04a2524e0226b955ebf4a846de62 [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"
"os/exec"
"path/filepath"
"runtime"
"sync"
"syscall"
"time"
"fuchsia.googlesource.com/jiri/cmdline"
"fuchsia.googlesource.com/jiri/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 and stderr io.Writers configured via NewSequence,
// and never to the stdout specified via Capture if the command succeeded.
//
// 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
// fuchsia.googlesource.com/jiri/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
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, verbose bool) Sequence {
if len(env) == 0 {
env = envvar.SliceToMap(os.Environ())
}
s := Sequence{
&sequence{
r: newExecutor(env, stdin, stdout, stderr, verbose),
defaultStdin: stdin,
},
}
s.defaultStdout, s.defaultStderr = s.serializeWriter(stdout), s.serializeWriter(stderr)
return s
}
// RunOpts returns the value of verbose that was used to
// create this sequence.
func (s Sequence) RunOpts() (verbose bool) {
opts := s.getOpts()
return 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
}
// CaptureAll arranges for all the logs/outputs to write to its supplied io.Writers.
// 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) CaptureAll(stdout, stderr io.Writer) Sequence {
if s.err != nil {
return s
}
s.stdout, s.stderr = stdout, stderr
s.r.opts.stdout, s.r.opts.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
}
// 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 = 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
}
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
}
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.call(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.call(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.call(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".
func (s Sequence) Chdir(dir string) Sequence {
if s.err != nil {
return s
}
err := s.r.call(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".
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".
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".
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".
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".
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".
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". 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". 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"
//. 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 {
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". 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.call(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". 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.call(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".
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".
// 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". 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.call(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". 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.call(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". Lstat is a terminating function.
func (s Sequence) Readlink(name string) (link string, err error) {
if s.err != nil {
return "", s.Done()
}
s.r.call(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". 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".
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.call(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.call(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
}