//! 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), } #[derive(Clone)] pub(super) struct Reporter { tx: watch::Sender, } pub(super) struct Monitor { rx: watch::Receiver, } 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>) -> 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> { match &*self.tx.borrow() { State::Healthy => None, State::Poisoned(reason) => Some(Arc::clone(reason)), } } } impl Monitor { pub(super) fn fault(&self) -> Option> { 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")); } }