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/filepath"
"runtime"
"strconv"
"strings"
"time"
@ -545,15 +544,7 @@ func runBackup(opts BackupOptions, gopts GlobalOptions, term *termstatus.Termina
}()
gopts.stdout, gopts.stderr = p.Stdout(), p.Stderr()
if s, ok := os.LookupEnv("RESTIC_PROGRESS_FPS"); ok {
fps, err := strconv.Atoi(s)
if err == nil && fps >= 1 {
if fps > 60 {
fps = 60
}
p.SetMinUpdatePause(time.Second / time.Duration(fps))
}
}
p.SetMinUpdatePause(calculateProgressInterval())
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"
)
// 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.
func newProgressMax(show bool, max uint64, description string) *progress.Counter {
if !show {
return nil
}
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)
}
}
interval := calculateProgressInterval()
return progress.New(interval, func(v uint64, d time.Duration, final bool) {
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
-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
``--quiet`` is set. When running from a non-interactive console progress
reporting will be limited to once every 10 seconds to not fill your
logs. Use ``backup`` with the quiet flag ``-q`` or ``--quiet`` to skip
the initial scan of the source directory, this may shorten the backup
time needed for large directories.
reporting is disabled by default to not fill your logs. For interactive
and non-interactive consoles the environment variable ``RESTIC_PROGRESS_FPS``
can be used to control the frequency of progress reporting. Use for example
``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
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
-----------

View File

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

View File

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

View File

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

View File

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

View File

@ -78,6 +78,11 @@ func New(wr io.Writer, errWriter io.Writer, disableStatus bool) *Terminal {
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
// ctx is cancelled, the status lines are cleanly removed.
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)
}
case <-t.status:
// discard status lines
case stat := <-t.status:
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
}
width, _, err := terminal.GetSize(int(t.fd))
if err != nil || width <= 0 {
// use 80 columns by default
width = 80
// only truncate interactive status output
var width int
if t.canUpdateStatus {
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
for i, line := range lines {
line = strings.TrimRight(line, "\n")
line = truncate(line, width-2) + "\n"
lines[i] = line
if width > 0 {
line = truncate(line, width-2)
}
lines[i] = line + "\n"
}
// make sure the last line does not have a line break