feat(cli): upgrade command, rich status, and version cache
Some checks failed
Release / release (push) Failing after 0s
Release / docker (push) Has been skipped
Release / virustotal (push) Failing after 0s

- Replace `upgrade` stub with real command (alias for `self-update`)
- Also register `update` as alias: `unarr update` works too
- Rewrite `status` to show full config, disk usage, daemon state, and
  update availability with colored sections
- Add version check cache (1h TTL) so `status` is instant on repeat runs
- Guard against division by zero on empty filesystems
- Guard against negative durations from clock skew
- Guard against stale PID via heartbeat recency check (2 min)
- Add comprehensive test coverage across agent, engine, upgrade, usenet,
  arr, library, mediaserver, and UI packages
- Improve Makefile coverage target to exclude cmd/ glue code
- Fix stream handler resource cleanup and ffprobe error handling
This commit is contained in:
Deivid Soto 2026-03-31 22:05:43 +02:00
parent 01d62ffa13
commit 3e0f3a5a64
33 changed files with 7084 additions and 65 deletions

View file

@ -24,6 +24,20 @@ var streamRegistry = struct {
servers: make(map[string]*engine.StreamServer),
}
// cancelAllStreams cancels all active stream tasks and servers (only 1 stream at a time).
func cancelAllStreams() {
streamRegistry.mu.Lock()
for taskID, cancel := range streamRegistry.cancels {
cancel()
delete(streamRegistry.cancels, taskID)
}
for taskID, srv := range streamRegistry.servers {
srv.Shutdown(context.Background())
delete(streamRegistry.servers, taskID)
}
streamRegistry.mu.Unlock()
}
// cancelStreamTask cancels a running stream task and shuts down any stream server.
func cancelStreamTask(taskID string) {
streamRegistry.mu.Lock()
@ -94,7 +108,7 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
}
// 4. Start HTTP server
srv := engine.NewStreamServer(eng, 0)
srv := engine.NewStreamServer(eng, cfg.Download.StreamPort)
streamURL, err := srv.Start(ctx)
if err != nil {
task.ErrorMessage = "start HTTP server: " + err.Error()
@ -107,17 +121,27 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
task.StreamURL = streamURL
log.Printf("[%s] stream ready: %s", at.ID[:8], streamURL)
// 6. Progress loop
// 6. Unified progress + idle timeout loop
eng.StartProgressLoop(ctx)
ticker := time.NewTicker(3 * time.Second)
defer ticker.Stop()
progressTicker := time.NewTicker(3 * time.Second)
defer progressTicker.Stop()
idleCheck := time.NewTicker(60 * time.Second)
defer idleCheck.Stop()
completed := false
for {
select {
case <-ctx.Done():
log.Printf("[%s] stream stopped", at.ID[:8])
return
case <-ticker.C:
case <-idleCheck.C:
if srv.IdleSince() > 30*time.Minute {
log.Printf("[%s] stream idle timeout (30m no HTTP requests), shutting down", at.ID[:8])
return
}
case <-progressTicker.C:
p := eng.Progress()
task.UpdateProgress(engine.Progress{
DownloadedBytes: p.DownloadedBytes,
@ -129,7 +153,7 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
})
// Terminal progress
if p.TotalBytes > 0 {
if !completed && p.TotalBytes > 0 {
pct := int(float64(p.DownloadedBytes) / float64(p.TotalBytes) * 100)
fmt.Fprintf(os.Stderr, "\r[%s] %d%% — %s/%s @ %s/s peers:%d seeds:%d",
at.ID[:8], pct,
@ -137,20 +161,11 @@ func handleStreamTask(parentCtx context.Context, at agent.Task, reporter *engine
p.Peers, p.Seeds)
}
if p.DownloadedBytes >= p.TotalBytes && p.TotalBytes > 0 {
fmt.Fprint(os.Stderr, "\r\033[2K") // clear progress line
if !completed && p.DownloadedBytes >= p.TotalBytes && p.TotalBytes > 0 {
fmt.Fprint(os.Stderr, "\r\033[2K")
task.Transition(engine.StatusCompleted)
log.Printf("[%s] stream download complete, server stays up for 30m or until cancelled", at.ID[:8])
// Keep HTTP server running so the player can finish reading.
// Auto-shutdown after 30 minutes of idle to prevent resource leaks.
idleTimer := time.NewTimer(30 * time.Minute)
defer idleTimer.Stop()
select {
case <-ctx.Done():
case <-idleTimer.C:
log.Printf("[%s] stream idle timeout (30m), shutting down", at.ID[:8])
}
return
log.Printf("[%s] stream download complete, server stays up until idle (30m)", at.ID[:8])
completed = true
}
}
}