forked from TrueCloudLab/restic
restic-from-command: use standard behavior when no output and exit code 0 from command
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.
This commit is contained in:
parent
c0ca54dc8a
commit
37a312e505
2 changed files with 62 additions and 22 deletions
|
@ -579,13 +579,13 @@ func TestStdinFromCommand(t *testing.T) {
|
||||||
StdinFilename: "stdin",
|
StdinFilename: "stdin",
|
||||||
}
|
}
|
||||||
|
|
||||||
testRunBackup(t, filepath.Dir(env.testdata), []string{"ls"}, opts, env.gopts)
|
testRunBackup(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; print('something'); sys.exit(0)"}, opts, env.gopts)
|
||||||
testListSnapshots(t, env.gopts, 1)
|
testListSnapshots(t, env.gopts, 1)
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStdinFromCommandFailNoOutput(t *testing.T) {
|
func TestStdinFromCommandNoOutput(t *testing.T) {
|
||||||
env, cleanup := withTestEnvironment(t)
|
env, cleanup := withTestEnvironment(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
@ -595,10 +595,9 @@ func TestStdinFromCommandFailNoOutput(t *testing.T) {
|
||||||
StdinFilename: "stdin",
|
StdinFilename: "stdin",
|
||||||
}
|
}
|
||||||
|
|
||||||
err := testRunBackupAssumeFailure(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; sys.exit(1)"}, opts, env.gopts)
|
err := testRunBackupAssumeFailure(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; sys.exit(0)"}, opts, env.gopts)
|
||||||
rtest.Assert(t, err != nil, "Expected error while backing up")
|
rtest.Assert(t, err != nil && err.Error() == "at least one source file could not be read", "No data error expected")
|
||||||
|
testListSnapshots(t, env.gopts, 1)
|
||||||
testListSnapshots(t, env.gopts, 0)
|
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
@ -620,3 +619,21 @@ func TestStdinFromCommandFailExitCode(t *testing.T) {
|
||||||
|
|
||||||
testRunCheck(t, env.gopts)
|
testRunCheck(t, env.gopts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStdinFromCommandFailNoOutputAndExitCode(t *testing.T) {
|
||||||
|
env, cleanup := withTestEnvironment(t)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
testSetupBackupData(t, env)
|
||||||
|
opts := BackupOptions{
|
||||||
|
StdinCommand: true,
|
||||||
|
StdinFilename: "stdin",
|
||||||
|
}
|
||||||
|
|
||||||
|
err := testRunBackupAssumeFailure(t, filepath.Dir(env.testdata), []string{"python", "-c", "import sys; sys.exit(1)"}, opts, env.gopts)
|
||||||
|
rtest.Assert(t, err != nil, "Expected error while backing up")
|
||||||
|
|
||||||
|
testListSnapshots(t, env.gopts, 0)
|
||||||
|
|
||||||
|
testRunCheck(t, env.gopts)
|
||||||
|
}
|
||||||
|
|
|
@ -13,31 +13,54 @@ type ReadCloserCommand struct {
|
||||||
Cmd *exec.Cmd
|
Cmd *exec.Cmd
|
||||||
Stdout io.ReadCloser
|
Stdout io.ReadCloser
|
||||||
|
|
||||||
bytesRead bool
|
// 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.
|
// Read populate the array with data from the process stdout.
|
||||||
func (fp *ReadCloserCommand) Read(p []byte) (int, error) {
|
func (fp *ReadCloserCommand) Read(p []byte) (int, error) {
|
||||||
// We may encounter two different error conditions here:
|
if fp.alreadyClosedReadErr != nil {
|
||||||
// - EOF with no bytes read: the program terminated prematurely, so we send
|
return 0, fp.alreadyClosedReadErr
|
||||||
// a fatal error to cancel the snapshot;
|
|
||||||
// - an error that is not EOF: something bad happened, we need to abort the
|
|
||||||
// snapshot.
|
|
||||||
b, err := fp.Stdout.Read(p)
|
|
||||||
if b == 0 && errors.Is(err, io.EOF) && !fp.bytesRead {
|
|
||||||
// The command terminated with no output at all. Raise a fatal error.
|
|
||||||
return 0, errors.Fatalf("command terminated with no output")
|
|
||||||
} else if err != nil && !errors.Is(err, io.EOF) {
|
|
||||||
// The command terminated with an error that is not EOF. Raise a fatal
|
|
||||||
// error.
|
|
||||||
return 0, errors.Fatal(err.Error())
|
|
||||||
} else if b > 0 {
|
|
||||||
fp.bytesRead = true
|
|
||||||
}
|
}
|
||||||
|
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
|
return b, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fp *ReadCloserCommand) Close() error {
|
func (fp *ReadCloserCommand) Close() error {
|
||||||
|
if fp.waitHandled {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// No need to close fp.Stdout as Wait() closes all pipes.
|
// No need to close fp.Stdout as Wait() closes all pipes.
|
||||||
err := fp.Cmd.Wait()
|
err := fp.Cmd.Wait()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in a new issue