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:
committed by
Brad Fitzpatrick
parent
d0fcb668d5
commit
a8f3c861a4
@@ -15,9 +15,11 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/clientupdate/distsign"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/progresstracking"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,8 +52,9 @@ func gokrazyUpdateFromURL(ctx context.Context, args GokrazyUpdateArgs) error {
|
||||
tmp.Close()
|
||||
defer os.Remove(tmpName)
|
||||
|
||||
logf("downloading %s", args.URL)
|
||||
if args.AllowUnsigned {
|
||||
if err := downloadUnverified(ctx, args.URL, tmpName); err != nil {
|
||||
if err := downloadUnverified(ctx, logf, args.URL, tmpName); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
@@ -66,6 +69,8 @@ func gokrazyUpdateFromURL(ctx context.Context, args GokrazyUpdateArgs) error {
|
||||
}
|
||||
defer zr.Close()
|
||||
|
||||
logf("download complete")
|
||||
|
||||
gokClient := gokrazyHTTPClient()
|
||||
for _, part := range []struct {
|
||||
name string
|
||||
@@ -75,6 +80,7 @@ func gokrazyUpdateFromURL(ctx context.Context, args GokrazyUpdateArgs) error {
|
||||
{"boot.img", "/update/boot"},
|
||||
{"mbr.img", "/update/mbr"},
|
||||
} {
|
||||
logf("writing %s...", part.name)
|
||||
if err := putGokrazyGAFMember(ctx, gokClient, zr.File, part.name, part.path); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -94,7 +100,7 @@ func gokrazyUpdateFromURL(ctx context.Context, args GokrazyUpdateArgs) error {
|
||||
// downloadUnverified saves the GAF at srcURL to dstPath without verifying
|
||||
// a signature. It is used only when args.AllowUnsigned is set, for tests
|
||||
// that serve the GAF from a fileserver that does not publish distsign.pub.
|
||||
func downloadUnverified(ctx context.Context, srcURL, dstPath string) error {
|
||||
func downloadUnverified(ctx context.Context, logf logger.Logf, srcURL, dstPath string) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", srcURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -111,7 +117,13 @@ func downloadUnverified(ctx context.Context, srcURL, dstPath string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := io.Copy(f, res.Body); err != nil {
|
||||
total := res.ContentLength
|
||||
pw := progresstracking.NewWriter(io.Discard, total, time.Second, func(done int64) {
|
||||
if total > 0 {
|
||||
logf("downloading: %d / %d MB (%.0f%%)", done>>20, total>>20, float64(done)/float64(total)*100)
|
||||
}
|
||||
})
|
||||
if _, err := io.Copy(f, io.TeeReader(res.Body, pw)); err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/progresstracking"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -373,7 +374,10 @@ func (c *Client) download(ctx context.Context, url, dst string, limit int64) ([]
|
||||
return nil, 0, err
|
||||
}
|
||||
defer of.Close()
|
||||
pw := &progressWriter{total: res.ContentLength, logf: c.logf}
|
||||
total := res.ContentLength
|
||||
pw := progresstracking.NewWriter(io.Discard, total, 2*time.Second, func(done int64) {
|
||||
c.logf("Downloaded %v/%v (%.1f%%)", done, total, float64(done)/float64(total)*100)
|
||||
})
|
||||
h := NewPackageHash()
|
||||
n, err := io.Copy(io.MultiWriter(of, h, pw), io.LimitReader(dlRes.Body, limit))
|
||||
if err != nil {
|
||||
@@ -388,31 +392,10 @@ func (c *Client) download(ctx context.Context, url, dst string, limit int64) ([]
|
||||
if err := of.Close(); err != nil {
|
||||
return nil, n, err
|
||||
}
|
||||
pw.print()
|
||||
|
||||
return h.Sum(nil), h.Len(), nil
|
||||
}
|
||||
|
||||
type progressWriter struct {
|
||||
done int64
|
||||
total int64
|
||||
lastPrint time.Time
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
||||
pw.done += int64(len(p))
|
||||
if time.Since(pw.lastPrint) > 2*time.Second {
|
||||
pw.print()
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (pw *progressWriter) print() {
|
||||
pw.lastPrint = time.Now()
|
||||
pw.logf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
||||
}
|
||||
|
||||
func parsePrivateKey(data []byte, typeTag string) (ed25519.PrivateKey, error) {
|
||||
b, rest := pem.Decode(data)
|
||||
if b == nil {
|
||||
|
||||
Reference in New Issue
Block a user