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 db73b51e0d
commit 65104ade92
8 changed files with 255 additions and 79 deletions

View File

@@ -46,6 +46,12 @@ func (e *Engine) Run(ctx context.Context, downloads []Download) []Result {
go func(i int, d Download) {
defer wg.Done()
// Track immediately so a download still queued for a concurrency slot
// is visible to the progress reporter as Waiting, rather than hidden
// until it starts running.
e.track(d)
defer e.untrack(d)
// Acquire a concurrency slot, or bail if we're shutting down
// before this download ever started.
select {
@@ -56,9 +62,6 @@ func (e *Engine) Run(ctx context.Context, downloads []Download) []Result {
}
defer func() { <-slots }()
e.track(d)
defer e.untrack(d)
err := d.Run(ctx)
results <- Result{Name: d.Name(), Index: i, Err: err, Stat: d.Stat()}
}(i, d)
@@ -74,7 +77,8 @@ func (e *Engine) Run(ctx context.Context, downloads []Download) []Result {
return out
}
// Snapshot returns a Stat for every currently running download.
// Snapshot returns a Stat for every tracked download — those running plus those
// still queued for a concurrency slot (reported as Waiting).
func (e *Engine) Snapshot() []Stat {
e.mu.Lock()
defer e.mu.Unlock()