// Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // Package audio contains the shared, init-independent audio retry policy. package audio import ( "context" "errors" "io" "net" "os" "os/exec" "syscall" "time" ) const RecoveryTimeout = 20 * time.Second // Temporary excludes permission, executable and protocol/configuration errors. func Temporary(err error) bool { if errors.Is(err, context.Canceled) { return false } var executableError *exec.Error var pathError *os.PathError if errors.As(err, &executableError) || errors.As(err, &pathError) && pathError.Op == "fork/exec" { return false } var networkError net.Error return errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, syscall.ENOENT) || errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, syscall.ECONNRESET) || errors.Is(err, syscall.EPIPE) || errors.Is(err, context.DeadlineExceeded) || errors.As(err, &networkError) && networkError.Timeout() } // Retry never sleeps or performs I/O on the input loop. Callers supply a // deadline and must make each attempt honor its context. func Retry(ctx context.Context, attempt func(context.Context) error, retryable func(error) bool) error { delay := 100 * time.Millisecond for { if err := ctx.Err(); err != nil { return err } err := attempt(ctx) if err == nil || !retryable(err) { return err } timer := time.NewTimer(delay) select { case <-ctx.Done(): timer.Stop() return errors.Join(ctx.Err(), err) case <-timer.C: } delay = min(2*delay, time.Second) } }