From d4d79e17433858d27846774274e38d907621b184 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Sat, 9 May 2026 22:02:22 +0300 Subject: Harden external process and network calls - Add cmd.WaitDelay (10-15s) to ffprobe, ffmpeg remux, and thumbnail generation. - Add bounded retries with exponential backoff to FFProber.Probe (max 3 attempts). - Inject *http.Client into podcast ParseFeed, replacing gofeed's default client. - Update all callers (podcast service, tests, integration tests) to pass client. - Ensure go test ./... -race -cover passes. --- internal/probe/probe.go | 65 ++++++++++++++++++++++++++++++++++++++++++++++--- internal/probe/remux.go | 4 +++ 2 files changed, 65 insertions(+), 4 deletions(-) (limited to 'internal/probe') diff --git a/internal/probe/probe.go b/internal/probe/probe.go index e94d2e5..a3fce78 100644 --- a/internal/probe/probe.go +++ b/internal/probe/probe.go @@ -4,31 +4,87 @@ package probe import ( "context" "encoding/json" + "errors" "fmt" "os" "os/exec" "strconv" + "time" "codeberg.org/snonux/player/internal/mediatype" "codeberg.org/snonux/player/internal/model" "github.com/rwcarlsen/goexif/exif" ) +const ( + defaultProbeWaitDelay = 10 * time.Second + defaultProbeMaxRetries = 2 + defaultProbeRetryDelay = 500 * time.Millisecond +) + // Prober extracts metadata from a media file. type Prober interface { Probe(ctx context.Context, path string) (*model.Metadata, error) } // FFProber wraps the ffprobe command-line tool. -type FFProber struct{} +type FFProber struct { + maxRetries int + retryDelay time.Duration + waitDelay time.Duration +} -// NewFFProber creates a new FFProber. +// NewFFProber creates a new FFProber with bounded retries and a process wait delay. func NewFFProber() *FFProber { - return &FFProber{} + return &FFProber{ + maxRetries: defaultProbeMaxRetries, + retryDelay: defaultProbeRetryDelay, + waitDelay: defaultProbeWaitDelay, + } } -// Probe runs ffprobe against the given path and parses the resulting JSON. +// Probe runs ffprobe against the given path with retries and parses the resulting JSON. func (f *FFProber) Probe(ctx context.Context, path string) (*model.Metadata, error) { + var lastErr error + attempts := f.maxRetries + 1 + if attempts <= 0 { + attempts = 1 + } + + for i := 0; i < attempts; i++ { + if err := ctx.Err(); err != nil { + return nil, err + } + + meta, err := f.probeOnce(ctx, path) + if err == nil { + return meta, nil + } + lastErr = err + + // Don't retry on context cancellation. + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + break + } + + if i < attempts-1 { + delay := f.retryDelay * time.Duration(1< maxDelay { + delay = maxDelay + } + select { + case <-time.After(delay): + case <-ctx.Done(): + return nil, ctx.Err() + } + } + } + return nil, lastErr +} + +// probeOnce performs a single ffprobe invocation. +func (f *FFProber) probeOnce(ctx context.Context, path string) (*model.Metadata, error) { cmd := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_format", @@ -36,6 +92,7 @@ func (f *FFProber) Probe(ctx context.Context, path string) (*model.Metadata, err "-of", "json", path, ) + cmd.WaitDelay = f.waitDelay out, err := cmd.Output() if err != nil { if exitErr, ok := err.(*exec.ExitError); ok && len(exitErr.Stderr) > 0 { diff --git a/internal/probe/remux.go b/internal/probe/remux.go index 35e87d3..07870f8 100644 --- a/internal/probe/remux.go +++ b/internal/probe/remux.go @@ -8,6 +8,7 @@ import ( "log/slog" "os" "os/exec" + "time" ) // Remuxer remuxes media on-the-fly to a browser-friendly container. @@ -23,6 +24,8 @@ func NewFFRemuxer() *FFRemuxer { return &FFRemuxer{} } +const remuxWaitDelay = 10 * time.Second + // Remux runs ffmpeg to copy video/audio streams into a fragmented MP4 // suitable for streaming to a browser. func (f *FFRemuxer) Remux(ctx context.Context, inputPath string, w io.Writer) error { @@ -43,6 +46,7 @@ func (f *FFRemuxer) Remux(ctx context.Context, inputPath string, w io.Writer) er "pipe:1", ) cmd.Stderr = os.Stderr + cmd.WaitDelay = remuxWaitDelay stdout, err := cmd.StdoutPipe() if err != nil { return fmt.Errorf("remux stdout pipe: %w", err) -- cgit v1.2.3