fix(host): contain capture owners and fail closed
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.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
//! 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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user