F4: real-sink discovery fallback closes the cold-boot race
Codex flagged a real-but-rare race in `try_bind_default_metadata`:
the daemon installs the metadata listener then immediately writes
`default.audio.sink = headroom-processed`, relying on PipeWire to
deliver the prior value to the listener before our write. In
practice this works (pw-metadata replays current state to a
freshly-installed listener), but two failure modes leak through:
1. **Prior daemon left the world dirty.** If a previous daemon
run set default to `headroom-processed` and didn't restore
before exiting, the listener replays "headroom-processed" —
`on_metadata_property` recognises that as our own promotion
and returns early. `real_sink.name` is never captured.
Bypass routes log "no real sink known" forever.
2. **No replay event.** If the listener doesn't fire for any
reason — broken PipeWire, an event-bus hiccup,
pipewire-rs's `add_listener_local` racing with our write —
same outcome.
Fix: instead of trying to win the listener race
(pipewire-rs has no synchronous metadata getter, and `Core::sync`
needs an async done-callback we don't have plumbing for), make
`try_capture_real_sink` self-heal. When `real_sink.name` is still
`None` and we see *any* non-processed `Audio/Sink` on the registry,
adopt it as the fallback real sink. A subsequent
`default.audio.sink` event will refine the choice via the existing
`adopt_new_real_sink` path if WP picks something else.
This is a belt-and-braces patch — the listener path stays the
primary mechanism, the fallback only kicks in when that path
hasn't produced a name. In steady-state (the common case where
listener replay works) it changes nothing.
Verified
Cold start with PipeWire's `default.audio.sink` set to the
Mbox: daemon logs `preferred_real_sink updated sink=Mbox`
via the listener path; the fallback's
`adopting first available Audio/Sink as fallback` log does
not fire. No regression for the steady state.
188 tests pass; clippy clean at -D warnings --all-targets.
This commit is contained in:
parent
0e718abe27
commit
04a005e1cd
1 changed files with 20 additions and 1 deletions
|
|
@ -496,6 +496,17 @@ impl RoutingState {
|
|||
/// `sinks_by_name`. If the captured name matches the active
|
||||
/// `preferred_real_sink`, populate `real_sink.node_id` so `status`
|
||||
/// reports it and downstream ops can route by id.
|
||||
///
|
||||
/// **Cold-boot fallback (F4).** If we haven't captured a real-sink
|
||||
/// name yet — either because the metadata listener's initial
|
||||
/// replay hasn't fired, or because `default.audio.sink` was
|
||||
/// already `headroom-processed` from a prior daemon run that
|
||||
/// didn't clean up — adopt the first non-processed Audio/Sink we
|
||||
/// see as the real sink. This avoids the failure mode Codex
|
||||
/// flagged where `real_sink.name = None` indefinitely and bypass
|
||||
/// routes log "no real sink known" forever. A subsequent
|
||||
/// `default.audio.sink` event will refine the choice via
|
||||
/// `adopt_new_real_sink` if the user/WP picks a different one.
|
||||
fn try_capture_real_sink(&mut self, global: &GlobalObject<&DictRef>) {
|
||||
let Some(props) = &global.props else { return };
|
||||
let dict: &DictRef = props;
|
||||
|
|
@ -510,7 +521,15 @@ impl RoutingState {
|
|||
}
|
||||
self.sinks_by_name.insert(name.to_owned(), global.id);
|
||||
let mut s = self.daemon.lock();
|
||||
if s.real_sink.name.as_deref() == Some(name) && s.real_sink.node_id != Some(global.id) {
|
||||
if s.real_sink.name.is_none() {
|
||||
tracing::info!(
|
||||
node_id = global.id,
|
||||
name,
|
||||
"no preferred_real_sink yet; adopting first available Audio/Sink as fallback"
|
||||
);
|
||||
s.real_sink.name = Some(name.to_owned());
|
||||
s.real_sink.node_id = Some(global.id);
|
||||
} else if s.real_sink.name.as_deref() == Some(name) && s.real_sink.node_id != Some(global.id) {
|
||||
tracing::info!(
|
||||
node_id = global.id,
|
||||
name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue