fix(audio): give ffprobe its own 45s deadline, a timeout is the file's fault (#49)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 20:56:58 +02:00
co-authored by Claude Sonnet 5.5
parent 6101dd4e2f
commit e8c5ac50bf
+17 -2
View File
@@ -22,8 +22,14 @@ type FFprobe struct {
Bin string Bin string
// MaxOutput bounds how much of ffprobe's stdout is read; zero means DefaultMaxOutput. // MaxOutput bounds how much of ffprobe's stdout is read; zero means DefaultMaxOutput.
MaxOutput int MaxOutput int
// Timeout bounds one probe; zero means DefaultProbeTimeout. A probe is milliseconds of
// work on a real file, so one that runs this long is the FILE's doing (#49).
Timeout time.Duration
} }
// DefaultProbeTimeout is the probe deadline when FFprobe.Timeout is unset.
const DefaultProbeTimeout = 45 * time.Second
// DefaultMaxOutput is far above the narrowed probe output of a legitimate file (a few // DefaultMaxOutput is far above the narrowed probe output of a legitimate file (a few
// hundred bytes) and far below what a hostile one can make ffprobe print. // hundred bytes) and far below what a hostile one can make ffprobe print.
const DefaultMaxOutput = 1 << 20 const DefaultMaxOutput = 1 << 20
@@ -102,7 +108,13 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error)
} }
args := append([]string{"-v", "error"}, inputRestrictions...) args := append([]string{"-v", "error"}, inputRestrictions...)
args = append(args, "-print_format", "json", "-show_entries", probeEntries, path) args = append(args, "-print_format", "json", "-show_entries", probeEntries, path)
cmd := exec.CommandContext(ctx, bin, args...) timeout := f.Timeout
if timeout <= 0 {
timeout = DefaultProbeTimeout
}
probeCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
cmd := exec.CommandContext(probeCtx, bin, args...)
cmd.WaitDelay = killGrace cmd.WaitDelay = killGrace
maxOut := f.MaxOutput maxOut := f.MaxOutput
if maxOut <= 0 { if maxOut <= 0 {
@@ -111,9 +123,12 @@ func (f *FFprobe) Probe(ctx context.Context, path string) (uploads.Probe, error)
stdout, stderr := &capWriter{max: maxOut}, &headWriter{max: maxStderr} stdout, stderr := &capWriter{max: maxOut}, &headWriter{max: maxStderr}
cmd.Stdout, cmd.Stderr = stdout, stderr cmd.Stdout, cmd.Stderr = stdout, stderr
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
if ctxErr := ctx.Err(); ctxErr != nil { if ctxErr := ctx.Err(); ctxErr != nil { // the caller gave up: not a verdict on the file
return uploads.Probe{}, fmt.Errorf("audio: ffprobe: %w", ctxErr) return uploads.Probe{}, fmt.Errorf("audio: ffprobe: %w", ctxErr)
} }
if probeCtx.Err() != nil { // our own deadline passed while the caller was still waiting
return uploads.Probe{}, fmt.Errorf("%w: ffprobe did not finish within %s", uploads.ErrUnreadable, timeout)
}
if errors.Is(err, errOutputCap) { if errors.Is(err, errOutputCap) {
return uploads.Probe{}, fmt.Errorf("%w: ffprobe printed more than %d bytes", uploads.ErrUnreadable, maxOut) return uploads.Probe{}, fmt.Errorf("%w: ffprobe printed more than %d bytes", uploads.ErrUnreadable, maxOut)
} }