internal/pipewire: finalizers for dangling files
All checks were successful
Test / Create distribution (push) Successful in 27s
Test / Sandbox (race detector) (push) Successful in 41s
Test / Sandbox (push) Successful in 43s
Test / Hpkg (push) Successful in 43s
Test / Hakurei (push) Successful in 47s
Test / Hakurei (race detector) (push) Successful in 3m47s
Test / Flake checks (push) Successful in 1m26s

This makes their handling much less error-prone.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-12-03 03:36:02 +09:00
parent c34439fc5f
commit 054c91879f
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q

View File

@ -16,11 +16,11 @@ package pipewire
import (
"encoding/binary"
"errors"
"fmt"
"io"
"maps"
"net"
"os"
"runtime"
"slices"
"strconv"
@ -102,6 +102,9 @@ type Context struct {
headerFiles int
// Files from the server. This is discarded on every Roundtrip so eventProxy
// implementations must make sure to close them to avoid leaking fds.
//
// These are not automatically set up as [os.File] because it is impossible
// to undo the effects of os.NewFile, which can be inconvenient for some uses.
receivedFiles []int
// Pending footer value for the next outgoing message.
// Newer footers appear to simply replace the existing one.
@ -428,8 +431,8 @@ func (e UnexpectedFilesError) Error() string {
}
// A DanglingFilesError holds onto files that were sent by the server but no [Header]
// accounts for. These must be closed to avoid leaking fds.
type DanglingFilesError []int
// accounts for. These are closed by their finalizers if discarded.
type DanglingFilesError []*os.File
func (e DanglingFilesError) Error() string {
return "received " + strconv.Itoa(len(e)) + " dangling files"
@ -591,17 +594,37 @@ func (ctx *Context) Roundtrip() (err error) {
}
}
var joinError []error
if len(proxyErrors) > 0 {
joinError = append(joinError, proxyErrors)
}
if len(ctx.receivedFiles) < receivedHeaderFiles {
joinError = append(joinError, DanglingFilesError(ctx.receivedFiles[len(ctx.receivedFiles)-receivedHeaderFiles:]))
}
if len(joinError) > 0 {
return errors.Join(joinError...)
// prepared here so finalizers are set up, but should not prevent proxyErrors
// from reaching the caller as those describe the cause of these dangling fds
var danglingFiles DanglingFilesError
if len(ctx.receivedFiles) > receivedHeaderFiles {
danglingFds := ctx.receivedFiles[receivedHeaderFiles:]
// having multiple *os.File with the same fd causes serious problems
slices.Sort(danglingFds)
danglingFds = slices.Compact(danglingFds)
danglingFiles = make(DanglingFilesError, 0, len(danglingFds))
for _, fd := range danglingFds {
// hold these as *os.File so they are closed if this error never reaches the caller,
// or the caller discards or otherwise does not handle this error, to avoid leaking fds
danglingFiles = append(danglingFiles, os.NewFile(uintptr(fd),
"dangling fd "+strconv.Itoa(fd)+" received from PipeWire"))
}
}
// these are checked and made available first since they describe the cause
// of so-called symptoms checked after this point; the symptoms should only
// be made available as a catch-all if these are unavailable
if len(proxyErrors) > 0 {
return proxyErrors
}
// populated early for finalizers
if len(danglingFiles) > 0 {
return danglingFiles
}
// this check must happen after everything else passes
if len(ctx.pendingIds) != 0 {
return UnacknowledgedProxyError(slices.Collect(maps.Keys(ctx.pendingIds)))
}