util/progresstracking: add Ticker, NewWriter, and CountingWriter

Add three new helpers to the existing progresstracking package:

  - Ticker: spawns a 1 Hz goroutine that calls a report function with
    the current value of an atomic counter and a total. Returns a stop
    function (safe to call multiple times via sync.OnceFunc) that fires
    one final report and blocks until the goroutine exits.

  - NewWriter: wraps an io.Writer and calls onProgress at most once per
    interval with the cumulative byte count.

  - CountingWriter: an io.Writer that atomically counts bytes written,
    for use with Ticker.

These will be used by the appliance flash and OTA update code in
subsequent commits.

Updates #1866

Change-Id: If353cea6506f5351b6fb19bfdb7bc9b78fe7855e
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick
2026-07-01 10:02:05 -07:00
committed by Brad Fitzpatrick
parent d0fcb668d5
commit a8f3c861a4
7 changed files with 122 additions and 111 deletions
+8 -31
View File
@@ -24,6 +24,7 @@ import (
"github.com/bradfitz/monogok/disklayout"
"github.com/diskfs/go-diskfs/backend"
"github.com/diskfs/go-diskfs/filesystem/ext4"
"tailscale.com/util/progresstracking"
)
// gptSecondaryReservedSectors is the number of 512-byte sectors that
@@ -288,39 +289,15 @@ func (m *memBackend) flushTo(f io.WriterAt, baseOffset int64) error {
return nil
}
// startExt4FlushProgress prints "ext4 perm: N MB / M MB (X%)" to
// os.Stderr roughly once a second, plus a final tick on stop. Returns
// a function the caller must invoke when the flush is done.
func startExt4FlushProgress(done *atomic.Int64, total int64) func() {
stopCh := make(chan struct{})
finished := make(chan struct{})
go func() {
defer close(finished)
t := time.NewTicker(time.Second)
defer t.Stop()
report := func() {
d := done.Load()
pct := 0.0
if total > 0 {
pct = float64(d) * 100 / float64(total)
}
fmt.Fprintf(os.Stderr, " ext4 perm: %s / %s (%.1f%%)\n",
humanBytes(d), humanBytes(total), pct)
return progresstracking.Ticker(done.Load, total, func(d, t int64) {
pct := 0.0
if t > 0 {
pct = float64(d) * 100 / float64(t)
}
for {
select {
case <-stopCh:
report()
return
case <-t.C:
report()
}
}
}()
return func() {
close(stopCh)
<-finished
}
fmt.Fprintf(os.Stderr, " ext4 perm: %s / %s (%.1f%%)\n",
humanBytes(d), humanBytes(t), pct)
})
}
func humanBytes(n int64) string {