From 81f8d473df96995795cb7c160b08f44c7f940b00 Mon Sep 17 00:00:00 2001 From: Enrico204 Date: Mon, 28 Aug 2023 07:53:17 +0200 Subject: [PATCH] restic-from-command: abort snapshot on non-zero exit codes --- cmd/restic/cmd_backup.go | 10 ++++++++- internal/fs/fs_reader_command.go | 37 +++++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index af736d13b..69a68ccab 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -634,6 +634,8 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter wg.Go(func() error { return sc.Scan(cancelCtx, targets) }) } + snapshotCtx, cancelSnapshot := context.WithCancel(ctx) + arch := archiver.New(repo, targetFS, archiver.Options{ReadConcurrency: backupOptions.ReadConcurrency}) arch.SelectByName = selectByNameFilter arch.Select = selectFilter @@ -641,6 +643,11 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter success := true arch.Error = func(item string, err error) error { success = false + // If we receive a fatal error during the execution of the snapshot, + // we abort the snapshot. + if errors.IsFatal(err) { + cancelSnapshot() + } return progressReporter.Error(item, err) } arch.CompleteItem = progressReporter.CompleteItem @@ -668,7 +675,8 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter if !gopts.JSON { progressPrinter.V("start backup on %v", targets) } - _, id, err := arch.Snapshot(ctx, targets, snapshotOpts) + _, id, err := arch.Snapshot(snapshotCtx, targets, snapshotOpts) + cancelSnapshot() // cleanly shutdown all running goroutines cancel() diff --git a/internal/fs/fs_reader_command.go b/internal/fs/fs_reader_command.go index f35454c7f..e456ee965 100644 --- a/internal/fs/fs_reader_command.go +++ b/internal/fs/fs_reader_command.go @@ -1,6 +1,7 @@ package fs import ( + "github.com/restic/restic/internal/errors" "io" "os/exec" ) @@ -11,13 +12,43 @@ import ( type ReadCloserCommand struct { Cmd *exec.Cmd Stdout io.ReadCloser + + bytesRead bool } -func (fp *ReadCloserCommand) Read(p []byte) (n int, err error) { - return fp.Stdout.Read(p) +// Read populate the array with data from the process stdout. +func (fp *ReadCloserCommand) Read(p []byte) (int, error) { + // We may encounter two different error conditions here: + // - EOF with no bytes read: the program terminated prematurely, so we send + // 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 + } + return b, err } func (fp *ReadCloserCommand) Close() error { // No need to close fp.Stdout as Wait() closes all pipes. - return fp.Cmd.Wait() + 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 }