httpdl: fix progress-reporter race on name/out; harden mirror & resume validation

Findings from a Rob-Pike-lens review (bugs/races/network), each verified
against the code before fixing:

- httpdl: name/out are now atomic.Pointer[string] -- the engine publishes a
  download to the reporter before Run resolves the name, so Stat raced the
  write (bt already did this)
- httpdl: a malformed --proxy fails loudly instead of silently bypassing it
- httpdl: a 206 must carry a matching Content-Range; a 200 in segmented mode is
  fatal so it fails over instead of burning the retry budget
- httpdl: single-stream mirror failover validates the range before appending;
  ErrTooSlow only when the error is a real ctx cancellation
- httpdl: idle guard tracks progress by timestamp (no Reset/Stop race, no
  sticky fired flag)
- httpdl/control: reject a resume file whose segments don't tile [0,total)
- bt: clamp the listen-port range; verify on-disk data before choosing pieces
  under --check-integrity
- cli: reject size overflow; show --seed-time=MIN; clamp --select-file range
- progress/engine/main: clamp ETA against int64 overflow; show queued
  downloads as waiting; join the reporter on exit instead of a 20ms sleep
This commit is contained in:
2026-06-20 23:28:26 +09:00
parent c4c03dde60
commit e3505855c1
8 changed files with 255 additions and 79 deletions

View File

@@ -153,8 +153,7 @@ func (r *Reporter) lineOne(s download.Stat) string {
fmt.Fprintf(&b, " UL:%s", speed(ul, r.human))
}
if !seeding && s.Total > 0 && dl > 0 {
eta := time.Duration(float64(s.Total-s.Completed)/float64(dl)) * time.Second
fmt.Fprintf(&b, " ETA:%s", secfmt(eta))
fmt.Fprintf(&b, " ETA:%s", secfmt(etaDuration(s.Total-s.Completed, dl)))
}
b.WriteByte(']')
return b.String()
@@ -215,16 +214,23 @@ func (r *Reporter) summary(stats []download.Stat) string {
return ""
}
var totDL, totUL int64
stalled := 0
stalled, waiting := 0, 0
for _, s := range stats {
dl, ul := r.rates(s)
totDL += dl
totUL += ul
if s.Status == download.Waiting {
waiting++
continue
}
if r.isStalled(s, dl) {
stalled++
}
}
line := fmt.Sprintf("%d active DL:%s UL:%s", len(stats), speed(totDL, r.human), speed(totUL, r.human))
line := fmt.Sprintf("%d active DL:%s UL:%s", len(stats)-waiting, speed(totDL, r.human), speed(totUL, r.human))
if waiting > 0 {
line += fmt.Sprintf(" (%d waiting)", waiting)
}
if stalled > 0 {
line += fmt.Sprintf(" (%d stalled)", stalled)
}
@@ -245,6 +251,8 @@ func (r *Reporter) tableRow(s download.Stat) string {
// upload rate, or a "stalled" flag).
func (r *Reporter) rowMetric(s download.Stat, dl, ul int64) (metric, tail string) {
switch s.Status {
case download.Waiting:
return "queued", ""
case download.Seeding:
return "seeding", "UL:" + speed(ul, r.human)
case download.Complete:
@@ -255,8 +263,7 @@ func (r *Reporter) rowMetric(s download.Stat, dl, ul int64) (metric, tail string
metric = "DL:" + speed(dl, r.human)
switch {
case dl > 0 && s.Total > 0:
eta := time.Duration(float64(s.Total-s.Completed)/float64(dl)) * time.Second
tail = secfmt(eta)
tail = secfmt(etaDuration(s.Total-s.Completed, dl))
case r.isStalled(s, dl):
tail = "stalled"
}
@@ -264,6 +271,20 @@ func (r *Reporter) rowMetric(s download.Stat, dl, ul int64) (metric, tail string
}
}
// maxETASeconds caps an ETA before it becomes a Duration: a near-stalled download
// yields an astronomically large seconds value, and time.Duration(secs) *
// time.Second would overflow int64 and wrap to a bogus tiny ETA. secfmt renders
// anything past 99h as "--", so clamping to 100h loses nothing.
const maxETASeconds = 100 * 3600
func etaDuration(remaining, dl int64) time.Duration {
secs := float64(remaining) / float64(dl)
if secs > maxETASeconds {
secs = maxETASeconds
}
return time.Duration(secs) * time.Second
}
// isStalled reports an active download that has gone a while with no new bytes,
// so the table can flag it rather than leave the eye to read DL:0B. A download
// too young to have a rate yet is not stalled.