mirror of
https://github.com/restic/restic.git
synced 2025-11-02 22:30: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)
|
||||
}
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue