mirror of
				https://github.com/restic/restic.git
				synced 2025-11-03 23:00:59 +00:00 
			
		
		
		
	termstatus: centralize OutputIsTerminal checks
This commit is contained in:
		
							parent
							
								
									c745e4221e
								
							
						
					
					
						commit
						1ae2d08d1b
					
				
					 14 changed files with 85 additions and 68 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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{}) {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	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,11 +431,9 @@ 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",
 | 
			
		||||
		printer.PT("found %d old cache directories in %v, run `restic cache --cleanup` to remove them",
 | 
			
		||||
			len(oldCacheDirs), c.Base)
 | 
			
		||||
	}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return s, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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...))
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,3 +28,7 @@ func (m *MockTerminal) CanUpdateStatus() bool {
 | 
			
		|||
func (m *MockTerminal) OutputRaw() io.Writer {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (m *MockTerminal) OutputIsTerminal() bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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...)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,7 @@ type Terminal struct {
 | 
			
		|||
	errWriter        io.Writer
 | 
			
		||||
	msg              chan message
 | 
			
		||||
	status           chan status
 | 
			
		||||
	outputIsTerminal bool
 | 
			
		||||
	canUpdateStatus  bool
 | 
			
		||||
	lastStatusLen    int
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -65,13 +66,18 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
 | 
			
		|||
		return t
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if d, ok := wr.(fder); ok && terminal.CanUpdateStatus(d.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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue