37a312e505
The behavior of the new option should reflect the behavior of normal backups: when the command exit code is zero and there is no output in the stdout, emit a warning but create the snapshot. This commit fixes the integration tests and the ReadCloserCommand struct.
77 lines
2.4 KiB
Go
77 lines
2.4 KiB
Go
package fs
|
|
|
|
import (
|
|
"github.com/restic/restic/internal/errors"
|
|
"io"
|
|
"os/exec"
|
|
)
|
|
|
|
// ReadCloserCommand wraps an exec.Cmd and its standard output to provide an
|
|
// io.ReadCloser that waits for the command to terminate on Close(), reporting
|
|
// any error in the command.Wait() function back to the Close() caller.
|
|
type ReadCloserCommand struct {
|
|
Cmd *exec.Cmd
|
|
Stdout io.ReadCloser
|
|
|
|
// We should call exec.Wait() once. waitHandled is taking care of storing
|
|
// whether we already called that function in Read() to avoid calling it
|
|
// again in Close().
|
|
waitHandled bool
|
|
|
|
// alreadyClosedReadErr is the error that we should return if we try to
|
|
// read the pipe again after closing. This works around a Read() call that
|
|
// is issued after a previous Read() with `io.EOF` (but some bytes were
|
|
// read in the past).
|
|
alreadyClosedReadErr error
|
|
}
|
|
|
|
// Read populate the array with data from the process stdout.
|
|
func (fp *ReadCloserCommand) Read(p []byte) (int, error) {
|
|
if fp.alreadyClosedReadErr != nil {
|
|
return 0, fp.alreadyClosedReadErr
|
|
}
|
|
b, err := fp.Stdout.Read(p)
|
|
|
|
// If the error is io.EOF, the program terminated. We need to check the
|
|
// exit code here because, if the program terminated with no output, the
|
|
// error in `Close()` is ignored.
|
|
if errors.Is(err, io.EOF) {
|
|
// Check if the command terminated successfully. If not, return the
|
|
// error.
|
|
fp.waitHandled = true
|
|
errw := fp.Cmd.Wait()
|
|
if errw != nil {
|
|
// If we have information about the exit code, let's use it in the
|
|
// error message. Otherwise, send the error message along.
|
|
// In any case, use a fatal error to abort the snapshot.
|
|
var err2 *exec.ExitError
|
|
if errors.As(errw, &err2) {
|
|
err = errors.Fatalf("command terminated with exit code %d", err2.ExitCode())
|
|
} else {
|
|
err = errors.Fatal(errw.Error())
|
|
}
|
|
}
|
|
}
|
|
fp.alreadyClosedReadErr = err
|
|
return b, err
|
|
}
|
|
|
|
func (fp *ReadCloserCommand) Close() error {
|
|
if fp.waitHandled {
|
|
return nil
|
|
}
|
|
|
|
// No need to close fp.Stdout as Wait() closes all pipes.
|
|
err := fp.Cmd.Wait()
|
|
if err != nil {
|
|
// If we have information about the exit code, let's use it in the
|
|
// error message. Otherwise, send the error message along.
|
|
// In any case, use a fatal error to abort the snapshot.
|
|
var err2 *exec.ExitError
|
|
if errors.As(err, &err2) {
|
|
return errors.Fatalf("command terminated with exit code %d", err2.ExitCode())
|
|
}
|
|
return errors.Fatal(err.Error())
|
|
}
|
|
return nil
|
|
}
|