Merge pull request #3199 from MichaelEischer/non-interactive-counter

Don't print progress on non-interactive terminals
This commit is contained in:
Alexander Neumann 2021-01-28 10:53:38 +01:00 committed by GitHub
commit a4689eb3b9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 145 additions and 70 deletions

View file

@ -0,0 +1,16 @@
Enhancement: Configurable progress reports for non-interactive terminals
The `backup`, `check` and `prune` commands never printed any progress
reports on non-interactive terminals. This behavior is now configurable
using the `RESTIC_PROGRESS_FPS` environment variable. Use for example a
value of `1` for an update per second or `0.01666` for an update per minute.
The `backup` command now also prints the current progress when restic
receives a `SIGUSR1` signal.
Setting the `RESTIC_PROGRESS_FPS` environment variable or sending a `SIGUSR1`
signal prints a status report even when `--quiet` was specified.
https://github.com/restic/restic/issues/2706
https://github.com/restic/restic/issues/3194
https://github.com/restic/restic/pull/3199

View file

@ -11,7 +11,6 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"time" "time"
@ -545,15 +544,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
}() }()
gopts.stdout, gopts.stderr = p.Stdout(), p.Stderr() gopts.stdout, gopts.stderr = p.Stdout(), p.Stderr()
if s, ok := os.LookupEnv("RESTIC_PROGRESS_FPS"); ok { p.SetMinUpdatePause(calculateProgressInterval())
fps, err := strconv.Atoi(s)
if err == nil && fps >= 1 {
if fps > 60 {
fps = 60
}
p.SetMinUpdatePause(time.Second / time.Duration(fps))
}
}
t.Go(func() error { return p.Run(t.Context(gopts.ctx)) }) t.Go(func() error { return p.Run(t.Context(gopts.ctx)) })

View file

@ -9,24 +9,29 @@ import (
"github.com/restic/restic/internal/ui/progress" "github.com/restic/restic/internal/ui/progress"
) )
// 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
func calculateProgressInterval() time.Duration {
interval := time.Second / 60
fps, err := strconv.ParseFloat(os.Getenv("RESTIC_PROGRESS_FPS"), 64)
if err == nil && fps > 0 {
if fps > 60 {
fps = 60
}
interval = time.Duration(float64(time.Second) / fps)
} else if !stdoutIsTerminal() {
interval = 0
}
return interval
}
// newProgressMax returns a progress.Counter that prints to stdout. // newProgressMax returns a progress.Counter that prints to stdout.
func newProgressMax(show bool, max uint64, description string) *progress.Counter { func newProgressMax(show bool, max uint64, description string) *progress.Counter {
if !show { if !show {
return nil return nil
} }
interval := calculateProgressInterval()
interval := time.Second / 60
if !stdoutIsTerminal() {
interval = time.Second
} else {
fps, err := strconv.ParseInt(os.Getenv("RESTIC_PROGRESS_FPS"), 10, 64)
if err == nil && fps >= 1 {
if fps > 60 {
fps = 60
}
interval = time.Second / time.Duration(fps)
}
}
return progress.New(interval, func(v uint64, d time.Duration, final bool) { return progress.New(interval, func(v uint64, d time.Duration, final bool) {
status := fmt.Sprintf("[%s] %s %d / %d %s", status := fmt.Sprintf("[%s] %s %d / %d %s",

View file

@ -133,18 +133,21 @@ command:
--tls-client-cert file path to a file containing PEM encoded TLS client certificate and private key --tls-client-cert file path to a file containing PEM encoded TLS client certificate and private key
-v, --verbose n be verbose (specify multiple times or a level using --verbose=n, max level/times is 3) -v, --verbose n be verbose (specify multiple times or a level using --verbose=n, max level/times is 3)
Subcommand that support showing progress information such as ``backup``, Subcommands that support showing progress information such as ``backup``,
``check`` and ``prune`` will do so unless the quiet flag ``-q`` or ``check`` and ``prune`` will do so unless the quiet flag ``-q`` or
``--quiet`` is set. When running from a non-interactive console progress ``--quiet`` is set. When running from a non-interactive console progress
reporting will be limited to once every 10 seconds to not fill your reporting is disabled by default to not fill your logs. For interactive
logs. Use ``backup`` with the quiet flag ``-q`` or ``--quiet`` to skip and non-interactive consoles the environment variable ``RESTIC_PROGRESS_FPS``
the initial scan of the source directory, this may shorten the backup can be used to control the frequency of progress reporting. Use for example
time needed for large directories. ``0.016666`` to only update the progress once per minute.
Additionally on Unix systems if ``restic`` receives a SIGUSR1 signal the Additionally, on Unix systems if ``restic`` receives a SIGUSR1 signal the
current progress will be written to the standard output so you can check up current progress will be written to the standard output so you can check up
on the status at will. on the status at will.
Setting the `RESTIC_PROGRESS_FPS` environment variable or sending a `SIGUSR1`
signal prints a status report even when `--quiet` was specified.
Manage tags Manage tags
----------- -----------

View file

@ -10,6 +10,7 @@ import (
"github.com/restic/restic/internal/archiver" "github.com/restic/restic/internal/archiver"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
"github.com/restic/restic/internal/ui/signals"
"github.com/restic/restic/internal/ui/termstatus" "github.com/restic/restic/internal/ui/termstatus"
) )
@ -31,7 +32,6 @@ type Backup struct {
MinUpdatePause time.Duration MinUpdatePause time.Duration
term *termstatus.Terminal term *termstatus.Terminal
v uint
start time.Time start time.Time
totalBytes uint64 totalBytes uint64
@ -40,7 +40,6 @@ type Backup struct {
processedCh chan counter processedCh chan counter
errCh chan struct{} errCh chan struct{}
workerCh chan fileWorkerMessage workerCh chan fileWorkerMessage
finished chan struct{}
closed chan struct{} closed chan struct{}
summary struct { summary struct {
@ -61,7 +60,6 @@ func NewBackup(term *termstatus.Terminal, verbosity uint) *Backup {
Message: NewMessage(term, verbosity), Message: NewMessage(term, verbosity),
StdioWrapper: NewStdioWrapper(term), StdioWrapper: NewStdioWrapper(term),
term: term, term: term,
v: verbosity,
start: time.Now(), start: time.Now(),
// limit to 60fps by default // limit to 60fps by default
@ -71,7 +69,6 @@ func NewBackup(term *termstatus.Terminal, verbosity uint) *Backup {
processedCh: make(chan counter), processedCh: make(chan counter),
errCh: make(chan struct{}), errCh: make(chan struct{}),
workerCh: make(chan fileWorkerMessage), workerCh: make(chan fileWorkerMessage),
finished: make(chan struct{}),
closed: make(chan struct{}), closed: make(chan struct{}),
} }
} }
@ -89,18 +86,22 @@ func (b *Backup) Run(ctx context.Context) error {
) )
t := time.NewTicker(time.Second) t := time.NewTicker(time.Second)
signalsCh := signals.GetProgressChannel()
defer t.Stop() defer t.Stop()
defer close(b.closed) defer close(b.closed)
// Reset status when finished // Reset status when finished
defer b.term.SetStatus([]string{""}) defer func() {
if b.term.CanUpdateStatus() {
b.term.SetStatus([]string{""})
}
}()
for { for {
forceUpdate := false
select { select {
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case <-b.finished:
started = false
b.term.SetStatus([]string{""})
case t, ok := <-b.totalCh: case t, ok := <-b.totalCh:
if ok { if ok {
total = t total = t
@ -134,10 +135,12 @@ func (b *Backup) Run(ctx context.Context) error {
todo := float64(total.Bytes - processed.Bytes) todo := float64(total.Bytes - processed.Bytes)
secondsRemaining = uint64(secs / float64(processed.Bytes) * todo) secondsRemaining = uint64(secs / float64(processed.Bytes) * todo)
} }
case <-signalsCh:
forceUpdate = true
} }
// limit update frequency // limit update frequency
if time.Since(lastUpdate) < b.MinUpdatePause { if !forceUpdate && (time.Since(lastUpdate) < b.MinUpdatePause || b.MinUpdatePause == 0) {
continue continue
} }
lastUpdate = time.Now() lastUpdate = time.Now()
@ -374,10 +377,8 @@ func (b *Backup) ReportTotal(item string, s archiver.ScanStats) {
// Finish prints the finishing messages. // Finish prints the finishing messages.
func (b *Backup) Finish(snapshotID restic.ID) { func (b *Backup) Finish(snapshotID restic.ID) {
select { // wait for the status update goroutine to shut down
case b.finished <- struct{}{}: <-b.closed
case <-b.closed:
}
b.P("\n") b.P("\n")
b.P("Files: %5d new, %5d changed, %5d unmodified\n", b.summary.Files.New, b.summary.Files.Changed, b.summary.Files.Unchanged) b.P("Files: %5d new, %5d changed, %5d unmodified\n", b.summary.Files.New, b.summary.Files.Changed, b.summary.Files.Unchanged)

View file

@ -1,11 +1,11 @@
package progress package progress
import ( import (
"os"
"sync" "sync"
"time" "time"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/ui/signals"
) )
// A Func is a callback for a Counter. // A Func is a callback for a Counter.
@ -31,17 +31,14 @@ type Counter struct {
// New starts a new Counter. // New starts a new Counter.
func New(interval time.Duration, report Func) *Counter { func New(interval time.Duration, report Func) *Counter {
signals.Once.Do(func() {
signals.ch = make(chan os.Signal, 1)
setupSignals()
})
c := &Counter{ c := &Counter{
report: report, report: report,
start: time.Now(), start: time.Now(),
stopped: make(chan struct{}), stopped: make(chan struct{}),
stop: make(chan struct{}), stop: make(chan struct{}),
tick: time.NewTicker(interval), }
if interval > 0 {
c.tick = time.NewTicker(interval)
} }
go c.run() go c.run()
@ -64,7 +61,9 @@ func (c *Counter) Done() {
if c == nil { if c == nil {
return return
} }
c.tick.Stop() if c.tick != nil {
c.tick.Stop()
}
close(c.stop) close(c.stop)
<-c.stopped // Wait for last progress report. <-c.stopped // Wait for last progress report.
*c = Counter{} // Prevent reuse. *c = Counter{} // Prevent reuse.
@ -85,12 +84,17 @@ func (c *Counter) run() {
c.report(c.get(), time.Since(c.start), true) c.report(c.get(), time.Since(c.start), true)
}() }()
var tick <-chan time.Time
if c.tick != nil {
tick = c.tick.C
}
signalsCh := signals.GetProgressChannel()
for { for {
var now time.Time var now time.Time
select { select {
case now = <-c.tick.C: case now = <-tick:
case sig := <-signals.ch: case sig := <-signalsCh:
debug.Log("Signal received: %v\n", sig) debug.Log("Signal received: %v\n", sig)
now = time.Now() now = time.Now()
case <-c.stop: case <-c.stop:
@ -100,10 +104,3 @@ func (c *Counter) run() {
c.report(c.get(), now.Sub(c.start), false) c.report(c.get(), now.Sub(c.start), false)
} }
} }
// XXX The fact that signals is a single global variable means that only one
// Counter receives each incoming signal.
var signals struct {
ch chan os.Signal
sync.Once
}

View file

@ -53,3 +53,22 @@ func TestCounterNil(t *testing.T) {
c.Add(1) c.Add(1)
c.Done() c.Done()
} }
func TestCounterNoTick(t *testing.T) {
finalSeen := false
otherSeen := false
report := func(value uint64, d time.Duration, final bool) {
if final {
finalSeen = true
} else {
otherSeen = true
}
}
c := progress.New(0, report)
time.Sleep(time.Millisecond)
c.Done()
test.Assert(t, finalSeen, "final call did not happen")
test.Assert(t, !otherSeen, "unexpected status update")
}

View file

@ -0,0 +1,24 @@
package signals
import (
"os"
"sync"
)
// GetProgressChannel returns a channel with which a single listener
// receives each incoming signal.
func GetProgressChannel() <-chan os.Signal {
signals.Once.Do(func() {
signals.ch = make(chan os.Signal, 1)
setupSignals()
})
return signals.ch
}
// XXX The fact that signals is a single global variable means that only one
// listener receives each incoming signal.
var signals struct {
ch chan os.Signal
sync.Once
}

View file

@ -1,6 +1,6 @@
// +build darwin dragonfly freebsd netbsd openbsd // +build darwin dragonfly freebsd netbsd openbsd
package progress package signals
import ( import (
"os/signal" "os/signal"

View file

@ -1,6 +1,6 @@
// +build aix linux solaris // +build aix linux solaris
package progress package signals
import ( import (
"os/signal" "os/signal"

View file

@ -1,3 +1,3 @@
package progress package signals
func setupSignals() {} func setupSignals() {}

View file

@ -78,6 +78,11 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
return t return t
} }
// CanUpdateStatus return whether the status output is updated in place.
func (t *Terminal) CanUpdateStatus() bool {
return t.canUpdateStatus
}
// Run updates the screen. It should be run in a separate goroutine. When // Run updates the screen. It should be run in a separate goroutine. When
// ctx is cancelled, the status lines are cleanly removed. // ctx is cancelled, the status lines are cleanly removed.
func (t *Terminal) Run(ctx context.Context) { func (t *Terminal) Run(ctx context.Context) {
@ -203,8 +208,15 @@ func (t *Terminal) runWithoutStatus(ctx context.Context) {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err) fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
} }
case <-t.status: case stat := <-t.status:
// discard status lines for _, line := range stat.lines {
// ensure that each line ends with newline
withNewline := strings.TrimRight(line, "\n") + "\n"
fmt.Fprint(t.wr, withNewline)
}
if err := t.wr.Flush(); err != nil {
fmt.Fprintf(os.Stderr, "flush failed: %v\n", err)
}
} }
} }
} }
@ -302,17 +314,24 @@ func (t *Terminal) SetStatus(lines []string) {
return return
} }
width, _, err := terminal.GetSize(int(t.fd)) // only truncate interactive status output
if err != nil || width <= 0 { var width int
// use 80 columns by default if t.canUpdateStatus {
width = 80 var err error
width, _, err = terminal.GetSize(int(t.fd))
if err != nil || width <= 0 {
// use 80 columns by default
width = 80
}
} }
// make sure that all lines have a line break and are not too long // make sure that all lines have a line break and are not too long
for i, line := range lines { for i, line := range lines {
line = strings.TrimRight(line, "\n") line = strings.TrimRight(line, "\n")
line = truncate(line, width-2) + "\n" if width > 0 {
lines[i] = line line = truncate(line, width-2)
}
lines[i] = line + "\n"
} }
// make sure the last line does not have a line break // make sure the last line does not have a line break