diff --git a/cmd/restic/cmd_backup.go b/cmd/restic/cmd_backup.go index fe3ace448..09dbab183 100644 --- a/cmd/restic/cmd_backup.go +++ b/cmd/restic/cmd_backup.go @@ -523,7 +523,7 @@ func runBackup(ctx context.Context, opts BackupOptions, gopts GlobalOptions, ter progressPrinter = backup.NewTextProgress(term, gopts.verbosity) } progressReporter := backup.NewProgress(progressPrinter, - calculateProgressInterval(!gopts.Quiet, gopts.JSON)) + calculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) defer progressReporter.Done() // rejectByNameFuncs collect functions that can reject items from the backup based on path only diff --git a/cmd/restic/cmd_check.go b/cmd/restic/cmd_check.go index 8b9937b69..5fddcfc0c 100644 --- a/cmd/restic/cmd_check.go +++ b/cmd/restic/cmd_check.go @@ -540,5 +540,6 @@ func (p *jsonErrorPrinter) E(msg string, args ...interface{}) { } func (*jsonErrorPrinter) S(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) P(_ string, _ ...interface{}) {} +func (*jsonErrorPrinter) PT(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) V(_ string, _ ...interface{}) {} func (*jsonErrorPrinter) VV(_ string, _ ...interface{}) {} diff --git a/cmd/restic/cmd_dump.go b/cmd/restic/cmd_dump.go index 6a2a2cce8..2cd15c06f 100644 --- a/cmd/restic/cmd_dump.go +++ b/cmd/restic/cmd_dump.go @@ -11,7 +11,6 @@ import ( "github.com/restic/restic/internal/dump" "github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/restic" - "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/spf13/cobra" @@ -180,7 +179,7 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] } outputFileWriter := term.OutputRaw() - canWriteArchiveFunc := checkStdoutArchive + canWriteArchiveFunc := checkStdoutArchive(term) if opts.Target != "" { file, err := os.Create(opts.Target) @@ -204,9 +203,9 @@ func runDump(ctx context.Context, opts DumpOptions, gopts GlobalOptions, args [] return nil } -func checkStdoutArchive() error { - if terminal.StdoutIsTerminal() { - return fmt.Errorf("stdout is the terminal, please redirect output") +func checkStdoutArchive(term ui.Terminal) func() error { + if term.OutputIsTerminal() { + return func() error { return fmt.Errorf("stdout is the terminal, please redirect output") } } - return nil + return func() error { return nil } } diff --git a/cmd/restic/cmd_generate.go b/cmd/restic/cmd_generate.go index 328b937ad..477910507 100644 --- a/cmd/restic/cmd_generate.go +++ b/cmd/restic/cmd_generate.go @@ -6,7 +6,6 @@ import ( "time" "github.com/restic/restic/internal/errors" - "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" "github.com/spf13/cobra" @@ -76,9 +75,7 @@ func writeManpages(root *cobra.Command, dir string, printer progress.Printer) er } func writeCompletion(filename string, shell string, generate func(w io.Writer) error, printer progress.Printer, gopts GlobalOptions) (err error) { - if terminal.StdoutIsTerminal() { - printer.P("writing %s completion file to %v", shell, filename) - } + printer.PT("writing %s completion file to %v", shell, filename) var outWriter io.Writer if filename != "-" { var outFile *os.File diff --git a/cmd/restic/cmd_restore.go b/cmd/restic/cmd_restore.go index 888226c8e..d0bf76d85 100644 --- a/cmd/restic/cmd_restore.go +++ b/cmd/restic/cmd_restore.go @@ -165,7 +165,7 @@ func runRestore(ctx context.Context, opts RestoreOptions, gopts GlobalOptions, printer = restoreui.NewTextProgress(term, gopts.verbosity) } - progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON)) + progress := restoreui.NewProgress(printer, calculateProgressInterval(!gopts.Quiet, gopts.JSON, term.CanUpdateStatus())) res := restorer.NewRestorer(repo, sn, restorer.Options{ DryRun: opts.DryRun, Sparse: opts.Sparse, diff --git a/cmd/restic/cmd_tag_integration_test.go b/cmd/restic/cmd_tag_integration_test.go index 5d58f89c4..cbb08c5bf 100644 --- a/cmd/restic/cmd_tag_integration_test.go +++ b/cmd/restic/cmd_tag_integration_test.go @@ -6,10 +6,13 @@ import ( "github.com/restic/restic/internal/restic" rtest "github.com/restic/restic/internal/test" + "github.com/restic/restic/internal/ui" ) func testRunTag(t testing.TB, opts TagOptions, gopts GlobalOptions) { - rtest.OK(t, runTag(context.TODO(), opts, gopts, nil, []string{})) + rtest.OK(t, withTermStatus(gopts, func(ctx context.Context, term ui.Terminal) error { + return runTag(context.TODO(), opts, gopts, term, []string{}) + })) } func TestTag(t *testing.T) { diff --git a/cmd/restic/global.go b/cmd/restic/global.go index 391e1edf6..e3419fedc 100644 --- a/cmd/restic/global.go +++ b/cmd/restic/global.go @@ -267,9 +267,7 @@ func ReadPassword(ctx context.Context, opts GlobalOptions, prompt string, printe if terminal.StdinIsTerminal() { password, err = terminal.ReadPassword(ctx, os.Stdin, os.Stderr, prompt) } else { - if terminal.StdoutIsTerminal() { - printer.P("reading repository password from stdin") - } + printer.PT("reading repository password from stdin") password, err = readPassword(os.Stdin) } @@ -385,19 +383,15 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr return nil, errors.Fatalf("%s", err) } - if terminal.StdoutIsTerminal() && !opts.JSON { - id := s.Config().ID - if len(id) > 8 { - id = id[:8] - } - if !opts.JSON { - extra := "" - if s.Config().Version >= 2 { - extra = ", compression level " + opts.Compression.String() - } - printer.P("repository %v opened (version %v%s)", id, s.Config().Version, extra) - } + id := s.Config().ID + if len(id) > 8 { + id = id[:8] } + extra := "" + if s.Config().Version >= 2 { + extra = ", compression level " + opts.Compression.String() + } + printer.PT("repository %v opened (version %v%s)", id, s.Config().Version, extra) if opts.NoCache { return s, nil @@ -409,8 +403,8 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr return s, nil } - if c.Created && !opts.JSON && terminal.StdoutIsTerminal() { - printer.P("created new cache in %v", c.Base) + if c.Created { + printer.PT("created new cache in %v", c.Base) } // start using the cache @@ -428,9 +422,7 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr // cleanup old cache dirs if instructed to do so if opts.CleanupCache { - if terminal.StdoutIsTerminal() && !opts.JSON { - printer.P("removing %d old cache dirs from %v", len(oldCacheDirs), c.Base) - } + printer.PT("removing %d old cache dirs from %v", len(oldCacheDirs), c.Base) for _, item := range oldCacheDirs { dir := filepath.Join(c.Base, item.Name()) err = os.RemoveAll(dir) @@ -439,10 +431,8 @@ func OpenRepository(ctx context.Context, opts GlobalOptions, printer progress.Pr } } } else { - if terminal.StdoutIsTerminal() { - printer.P("found %d old cache directories in %v, run `restic cache --cleanup` to remove them", - len(oldCacheDirs), c.Base) - } + printer.PT("found %d old cache directories in %v, run `restic cache --cleanup` to remove them", + len(oldCacheDirs), c.Base) } return s, nil diff --git a/cmd/restic/progress.go b/cmd/restic/progress.go index 37ba0e623..f72a052ae 100644 --- a/cmd/restic/progress.go +++ b/cmd/restic/progress.go @@ -6,7 +6,6 @@ import ( "strconv" "time" - "github.com/restic/restic/internal/terminal" "github.com/restic/restic/internal/ui" "github.com/restic/restic/internal/ui/progress" ) @@ -14,7 +13,7 @@ import ( // calculateProgressInterval returns the interval configured via RESTIC_PROGRESS_FPS // or if unset returns an interval for 60fps on interactive terminals and 0 (=disabled) // for non-interactive terminals or when run using the --quiet flag -func calculateProgressInterval(show bool, json bool) time.Duration { +func calculateProgressInterval(show bool, json bool, canUpdateStatus bool) time.Duration { interval := time.Second / 60 fps, err := strconv.ParseFloat(os.Getenv("RESTIC_PROGRESS_FPS"), 64) if err == nil && fps > 0 { @@ -22,7 +21,7 @@ func calculateProgressInterval(show bool, json bool) time.Duration { fps = 60 } interval = time.Duration(float64(time.Second) / fps) - } else if !json && !terminal.StdoutCanUpdateStatus() || !show { + } else if !json && !canUpdateStatus || !show { interval = 0 } return interval @@ -33,7 +32,7 @@ func newTerminalProgressMax(show bool, max uint64, description string, term ui.T if !show { return nil } - interval := calculateProgressInterval(show, false) + interval := calculateProgressInterval(show, false, term.CanUpdateStatus()) return progress.NewCounter(interval, max, func(v uint64, max uint64, d time.Duration, final bool) { var status string @@ -65,7 +64,7 @@ func (t *terminalProgressPrinter) NewCounter(description string) *progress.Count } func (t *terminalProgressPrinter) NewCounterTerminalOnly(description string) *progress.Counter { - return newTerminalProgressMax(t.show && terminal.StdoutIsTerminal(), 0, description, t.term) + return newTerminalProgressMax(t.show && t.term.OutputIsTerminal(), 0, description, t.term) } func newTerminalProgressPrinter(json bool, verbosity uint, term ui.Terminal) progress.Printer { diff --git a/internal/terminal/stdio.go b/internal/terminal/stdio.go index 70e465ccb..1ee33e025 100644 --- a/internal/terminal/stdio.go +++ b/internal/terminal/stdio.go @@ -10,18 +10,11 @@ func StdinIsTerminal() bool { return term.IsTerminal(int(os.Stdin.Fd())) } -func StdoutIsTerminal() bool { +func OutputIsTerminal(fd uintptr) bool { // mintty on windows can use pipes which behave like a posix terminal, - // but which are not a terminal handle - return term.IsTerminal(int(os.Stdout.Fd())) || StdoutCanUpdateStatus() -} - -func StdoutCanUpdateStatus() bool { - return CanUpdateStatus(os.Stdout.Fd()) -} - -func StdoutWidth() int { - return Width(os.Stdout.Fd()) + // but which are not a terminal handle. Thus also check `CanUpdateStatus`, + // which is able to detect such pipes. + return term.IsTerminal(int(fd)) || CanUpdateStatus(fd) } func Width(fd uintptr) int { diff --git a/internal/ui/message.go b/internal/ui/message.go index 612fd72a2..d186c3859 100644 --- a/internal/ui/message.go +++ b/internal/ui/message.go @@ -30,8 +30,17 @@ func (m *Message) S(msg string, args ...interface{}) { m.term.Print(fmt.Sprintf(msg, args...)) } -// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), -// this is used for normal messages which are not errors. +// PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified) +// and stdout points to a terminal. +// This is used for informational messages. +func (m *Message) PT(msg string, args ...interface{}) { + if m.term.OutputIsTerminal() && m.v >= 1 { + m.term.Print(fmt.Sprintf(msg, args...)) + } +} + +// P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified). +// This is used for normal messages which are not errors. func (m *Message) P(msg string, args ...interface{}) { if m.v >= 1 { m.term.Print(fmt.Sprintf(msg, args...)) diff --git a/internal/ui/mock.go b/internal/ui/mock.go index 36452f4be..fc5488792 100644 --- a/internal/ui/mock.go +++ b/internal/ui/mock.go @@ -28,3 +28,7 @@ func (m *MockTerminal) CanUpdateStatus() bool { func (m *MockTerminal) OutputRaw() io.Writer { return nil } + +func (m *MockTerminal) OutputIsTerminal() bool { + return true +} diff --git a/internal/ui/progress/printer.go b/internal/ui/progress/printer.go index 37d81f4d6..edcf7256b 100644 --- a/internal/ui/progress/printer.go +++ b/internal/ui/progress/printer.go @@ -19,6 +19,10 @@ type Printer interface { // that are not errors. The message is even printed if --quiet is specified. // Appends a newline if not present. S(msg string, args ...interface{}) + // PT prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified) + // and stdout points to a terminal. + // This is used for informational messages. + PT(msg string, args ...interface{}) // P prints a message if verbosity >= 1 (neither --quiet nor --verbose is specified), // this is used for normal messages which are not errors. Appends a newline if not present. P(msg string, args ...interface{}) @@ -47,6 +51,8 @@ func (*NoopPrinter) E(_ string, _ ...interface{}) {} func (*NoopPrinter) S(_ string, _ ...interface{}) {} +func (*NoopPrinter) PT(_ string, _ ...interface{}) {} + func (*NoopPrinter) P(_ string, _ ...interface{}) {} func (*NoopPrinter) V(_ string, _ ...interface{}) {} @@ -82,6 +88,10 @@ func (p *TestPrinter) S(msg string, args ...interface{}) { p.t.Logf("stdout: "+msg, args...) } +func (p *TestPrinter) PT(msg string, args ...interface{}) { + p.t.Logf("stdout(terminal): "+msg, args...) +} + func (p *TestPrinter) P(msg string, args ...interface{}) { p.t.Logf("print: "+msg, args...) } diff --git a/internal/ui/terminal.go b/internal/ui/terminal.go index 262f1bcf7..845e36508 100644 --- a/internal/ui/terminal.go +++ b/internal/ui/terminal.go @@ -17,4 +17,5 @@ type Terminal interface { // other option. Must not be used in combination with Print, Error, SetStatus // or any other method that writes to the terminal. OutputRaw() io.Writer + OutputIsTerminal() bool } diff --git a/internal/ui/termstatus/status.go b/internal/ui/termstatus/status.go index dc47c880a..f1cbd7ef4 100644 --- a/internal/ui/termstatus/status.go +++ b/internal/ui/termstatus/status.go @@ -16,13 +16,14 @@ var _ ui.Terminal = &Terminal{} // updated. When the output is redirected to a file, the status lines are not // printed. type Terminal struct { - wr io.Writer - fd uintptr - errWriter io.Writer - msg chan message - status chan status - canUpdateStatus bool - lastStatusLen int + wr io.Writer + fd uintptr + errWriter io.Writer + msg chan message + status chan status + outputIsTerminal bool + canUpdateStatus bool + lastStatusLen int // will be closed when the goroutine which runs Run() terminates, so it'll // yield a default value immediately @@ -65,12 +66,17 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal { return t } - if d, ok := wr.(fder); ok && terminal.CanUpdateStatus(d.Fd()) { - // only use the fancy status code when we're running on a real terminal. - t.canUpdateStatus = true - t.fd = d.Fd() - t.clearCurrentLine = terminal.ClearCurrentLine(t.fd) - t.moveCursorUp = terminal.MoveCursorUp(t.fd) + if d, ok := wr.(fder); ok { + if terminal.CanUpdateStatus(d.Fd()) { + // only use the fancy status code when we're running on a real terminal. + t.canUpdateStatus = true + t.fd = d.Fd() + t.clearCurrentLine = terminal.ClearCurrentLine(t.fd) + t.moveCursorUp = terminal.MoveCursorUp(t.fd) + } + if terminal.OutputIsTerminal(d.Fd()) { + t.outputIsTerminal = true + } } return t @@ -88,6 +94,11 @@ func (t *Terminal) OutputRaw() io.Writer { return t.wr } +// OutputIsTerminal returns whether the output is a terminal. +func (t *Terminal) OutputIsTerminal() bool { + return t.outputIsTerminal +} + // Run updates the screen. It should be run in a separate goroutine. When // ctx is cancelled, the status lines are cleanly removed. func (t *Terminal) Run(ctx context.Context) {