Bound the libpipewire router shutdown without detaching its OS handle, contain GStreamer and pactl children in parent-bound process groups, and make ownership failures terminal through the capture supervisor.\n\nAdd focused lifecycle tests plus a serialized live router teardown gate.
84 lines
2.4 KiB
Rust
84 lines
2.4 KiB
Rust
//! Terminal health shared by capture components and the host supervisor.
|
|
//!
|
|
//! A capture-side ownership failure is not recoverable inside the same host
|
|
//! process: a wedged PipeWire owner or an unaccounted Pulse module can collide
|
|
//! with the next capture. Health is therefore one-way (`Healthy -> Poisoned`)
|
|
//! and first-fault-wins so a later, less precise teardown symptom cannot erase
|
|
//! the original cause.
|
|
|
|
use std::sync::Arc;
|
|
use tokio::sync::watch;
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub(super) enum State {
|
|
Healthy,
|
|
Poisoned(Arc<str>),
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct Reporter {
|
|
tx: watch::Sender<State>,
|
|
}
|
|
|
|
pub(super) struct Monitor {
|
|
rx: watch::Receiver<State>,
|
|
}
|
|
|
|
pub(super) fn channel() -> (Reporter, Monitor) {
|
|
let (tx, rx) = watch::channel(State::Healthy);
|
|
(Reporter { tx }, Monitor { rx })
|
|
}
|
|
|
|
impl Reporter {
|
|
/// Poison the host exactly once. Returns true for the first fault.
|
|
pub(super) fn poison(&self, reason: impl Into<Arc<str>>) -> bool {
|
|
let reason = reason.into();
|
|
self.tx.send_if_modified(move |state| {
|
|
if matches!(state, State::Healthy) {
|
|
*state = State::Poisoned(reason);
|
|
true
|
|
} else {
|
|
false
|
|
}
|
|
})
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(super) fn fault(&self) -> Option<Arc<str>> {
|
|
match &*self.tx.borrow() {
|
|
State::Healthy => None,
|
|
State::Poisoned(reason) => Some(Arc::clone(reason)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Monitor {
|
|
pub(super) fn fault(&self) -> Option<Arc<str>> {
|
|
match &*self.rx.borrow() {
|
|
State::Healthy => None,
|
|
State::Poisoned(reason) => Some(Arc::clone(reason)),
|
|
}
|
|
}
|
|
|
|
pub(super) async fn changed(&mut self) {
|
|
// The supervisor owns a Reporter for the whole run, so closure is not a
|
|
// normal state. Treat it like a wake and let `fault()` decide.
|
|
let _ = self.rx.changed().await;
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[tokio::test]
|
|
async fn poison_is_terminal_and_first_fault_wins() {
|
|
let (reporter, mut monitor) = channel();
|
|
assert!(reporter.poison("first"));
|
|
monitor.changed().await;
|
|
assert_eq!(monitor.fault().as_deref(), Some("first"));
|
|
assert!(!reporter.poison("second"));
|
|
assert_eq!(reporter.fault().as_deref(), Some("first"));
|
|
}
|
|
}
|