use crate::capture_failures::CaptureCallbacks;
use crate::capture_proxy::{CoreOutcome, ProxyLifecycle};
use crate::link_error::{CoreFailure, LinkAttempt};
use crate::owner::BackendEvents;
use crate::readiness::Readiness;
use crate::{capture_error, capture_failures, link_error, owner::push_backend_event};
use flexaudio_core::{Event, Result};
use std::rc::Rc;
#[derive(Clone)]
pub(crate) struct CaptureControl {
lifecycle: Rc<ProxyLifecycle>,
callbacks: CaptureCallbacks,
}
impl CaptureControl {
pub(crate) fn new(lifecycle: Rc<ProxyLifecycle>, callbacks: CaptureCallbacks) -> Self {
Self {
lifecycle,
callbacks,
}
}
pub(crate) fn error(&self, failure: CoreFailure) {
self.callbacks.run(|| {
let outcome = self.lifecycle.handle_error(failure);
self.callbacks
.readiness
.set_control_pending(self.lifecycle.has_pending_errors());
self.apply(outcome);
});
}
pub(crate) fn barrier(
&self,
sequence: i32,
mut retire: impl FnMut(&LinkAttempt) -> Result<()>,
reconcile: impl FnOnce() -> Result<()>,
) -> bool {
let released = self.lifecycle.acknowledge(sequence);
let outcomes = self.lifecycle.resolve_errors(sequence);
let resolved = !outcomes.is_empty();
for outcome in outcomes {
let attempt = outcome.attempt.clone();
if self.apply(outcome) {
return true;
}
if let Some(attempt) = attempt {
if let Err(error) = retire(&attempt) {
self.callbacks.fail(error);
return true;
}
}
}
if released || resolved {
if reconcile().is_err() {
return true;
}
}
self.callbacks
.readiness
.set_control_pending(self.lifecycle.has_pending_errors());
false
}
fn apply(&self, outcome: CoreOutcome) -> bool {
apply_core_outcome(
outcome,
&self.callbacks.readiness,
&self.callbacks.events,
|| self.callbacks.quit(),
)
}
}
pub(crate) fn apply_core_outcome(
outcome: CoreOutcome,
readiness: &Readiness,
events: &BackendEvents,
quit: impl FnOnce(),
) -> bool {
let disposition = match outcome.decision {
link_error::Decision::Continue => return false,
link_error::Decision::Recoverable(disposition) => disposition,
link_error::Decision::Terminate => capture_error::CoreErrorDisposition::Fatal,
};
let phase = if readiness.is_reported() {
capture_error::CapturePhase::Running
} else {
capture_error::CapturePhase::Setup
};
let failure = outcome.failure;
let error = capture_error::core_error(
failure.id,
failure.sequence,
failure.status,
&failure.message,
phase,
disposition,
);
if outcome.decision == link_error::Decision::Terminate {
capture_failures::report_control_failure(readiness, events, error);
quit();
true
} else {
push_backend_event(events, Event::RecoverableError { error });
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{link_error::CoreFailure, owner::poll_backend_event, proxy_release::RequestWindow};
use std::{
cell::Cell,
collections::VecDeque,
sync::{mpsc, Arc, Mutex},
};
struct Fixture {
control: CaptureControl,
lifecycle: Rc<ProxyLifecycle>,
readiness: Rc<Readiness>,
result: mpsc::Receiver<Result<()>>,
events: BackendEvents,
stopped: Rc<Cell<bool>>,
}
impl Fixture {
fn new() -> Self {
let sequence = Cell::new(100);
Self::with_checkpoint(move || {
let next = sequence.get();
sequence.set(next + 1);
Ok(next)
})
}
fn with_checkpoint(checkpoint: impl Fn() -> Result<i32> + 'static) -> Self {
let (tx, result) = mpsc::channel();
let readiness = Rc::new(Readiness::new(tx, true));
readiness.set_target_present(true);
readiness.registry_synchronized();
let events = Arc::new(Mutex::new(VecDeque::new()));
let stopped = Rc::new(Cell::new(false));
let stopped_for_callback = stopped.clone();
let callbacks = CaptureCallbacks::new(readiness.clone(), events.clone(), move || {
stopped_for_callback.set(true)
});
let lifecycle = ProxyLifecycle::with_checkpoint(callbacks.clone(), checkpoint);
for id in [51, 52, 53, 61, 62] {
lifecycle.observe(id);
}
lifecycle
.link_attempt(
5,
RequestWindow {
before: 20,
after: 22,
},
51,
52,
61,
62,
)
.expect("owned link attempt");
let control = CaptureControl::new(lifecycle.clone(), callbacks);
Self {
control,
lifecycle,
readiness,
result,
events,
stopped,
}
}
fn error(&self, status: i32) {
self.control.error(CoreFailure {
id: 5,
sequence: 21,
status,
message: "link-factory: unknown output port 52".into(),
});
}
}
#[test]
fn verified_last_source_disappearance_starts_silent_before_deadline() {
for node_survives in [false, true] {
let owner = Fixture::new();
owner.error(-22);
assert!(owner.lifecycle.has_pending_errors());
assert_eq!(owner.result.try_recv(), Err(mpsc::TryRecvError::Empty));
assert!(poll_backend_event(&owner.events).is_none());
owner.lifecycle.remove(52);
owner.lifecycle.remove(53);
if !node_survives {
owner.lifecycle.remove(51);
}
let retired = Cell::new(0);
assert!(!owner.control.barrier(
100,
|_| {
retired.set(retired.get() + 1);
Ok(())
},
|| {
owner.readiness.set_target_present(
node_survives && owner.lifecycle.source_requires_negotiation(51, false),
);
Ok(())
}
));
assert!(
!owner.readiness.deadline_expired(),
"verified source disappearance must not time out"
);
assert_eq!(owner.result.try_recv(), Ok(Ok(())));
assert!(!owner.stopped.get());
assert_eq!(retired.get(), 1);
assert!(!owner.lifecycle.has_pending_errors());
let Some(Event::RecoverableError { error }) = poll_backend_event(&owner.events) else {
panic!("expected recovery");
};
assert_eq!(error.diagnostic_details(), ["PipeWire core error: id=5 sequence=21 status=-22; link-factory: unknown output port 52"]);
assert!(poll_backend_event(&owner.events).is_none());
}
}
#[test]
fn surviving_link_target_terminates_with_original_error_after_barrier() {
let owner = Fixture::new();
owner.error(-22);
assert!(!owner.control.barrier(
99,
|_| panic!("unrelated barrier"),
|| panic!("unrelated barrier")
));
assert!(!owner.stopped.get());
assert!(owner.control.barrier(
100,
|_| panic!("fatal route must not retire before termination"),
|| panic!("fatal route must not reconcile")
));
assert!(owner.stopped.get());
assert!(owner.result.try_recv().expect("startup failure").is_err());
let Some(Event::TerminalError { error }) = poll_backend_event(&owner.events) else {
panic!("expected terminal failure");
};
assert_eq!(error.diagnostic_details(), ["PipeWire core error: id=5 sequence=21 status=-22; link-factory: unknown output port 52"]);
assert!(poll_backend_event(&owner.events).is_none());
}
#[test]
fn permission_failure_terminates_without_scheduling_verification() {
let owner = Fixture::new();
owner.error(-13);
assert!(owner.stopped.get());
assert!(!owner.lifecycle.has_pending_errors());
assert!(owner.result.try_recv().expect("startup failure").is_err());
assert!(matches!(
poll_backend_event(&owner.events),
Some(Event::TerminalError { .. })
));
}
#[test]
fn a_surviving_neighbor_still_requires_negotiation_after_benign_disappearance() {
let owner = Fixture::new();
owner.lifecycle.observe(71);
owner.error(-22);
owner.lifecycle.remove(52);
owner.lifecycle.remove(53);
assert!(!owner.control.barrier(
100,
|_| Ok(()),
|| {
let dormant = owner.lifecycle.source_requires_negotiation(51, false);
let neighbor = owner.lifecycle.source_requires_negotiation(71, true);
assert!(!dormant);
assert!(neighbor);
owner.readiness.set_target_present(dormant || neighbor);
Ok(())
}
));
assert_eq!(owner.result.try_recv(), Err(mpsc::TryRecvError::Empty));
assert!(matches!(
poll_backend_event(&owner.events),
Some(Event::RecoverableError { .. })
));
assert!(owner.readiness.deadline_expired());
assert!(owner
.result
.try_recv()
.expect("negotiation timeout")
.is_err());
}
#[test]
fn returning_ports_and_reused_node_ids_clear_verified_dormancy() {
let owner = Fixture::new();
owner.error(-22);
owner.lifecycle.remove(52);
owner.lifecycle.remove(53);
owner.control.barrier(100, |_| Ok(()), || Ok(()));
assert!(!owner.lifecycle.source_requires_negotiation(51, false));
owner.lifecycle.source_port_available(51);
assert!(owner.lifecycle.source_requires_negotiation(51, false));
let owner = Fixture::new();
owner.error(-22);
owner.lifecycle.remove(52);
owner.control.barrier(100, |_| Ok(()), || Ok(()));
assert!(owner.lifecycle.source_requires_negotiation(51, true));
assert!(owner.lifecycle.source_requires_negotiation(51, false));
let owner = Fixture::new();
owner.error(-22);
owner.lifecycle.remove(52);
owner.lifecycle.remove(53);
owner.control.barrier(100, |_| Ok(()), || Ok(()));
owner.lifecycle.remove(51);
owner.lifecycle.observe(51);
assert!(owner.lifecycle.source_requires_negotiation(51, false));
}
#[test]
fn callback_panic_is_contained_and_terminates_through_the_shared_owner() {
let owner = Fixture::new();
owner
.control
.callbacks
.run(|| panic!("synthetic callback panic"));
assert!(owner.stopped.get());
assert!(owner.result.try_recv().expect("callback failure").is_err());
assert!(matches!(
poll_backend_event(&owner.events),
Some(Event::TerminalError { .. })
));
}
#[test]
fn verification_sync_failure_is_terminal_and_retains_the_original_tuple() {
let owner = Fixture::with_checkpoint(|| {
Err(flexaudio_core::Error::Backend(
"synthetic sync failure".into(),
))
});
owner.error(-22);
assert!(owner.stopped.get());
assert!(!owner.lifecycle.has_pending_errors());
assert!(owner.result.try_recv().expect("startup failure").is_err());
let Some(Event::TerminalError { error }) = poll_backend_event(&owner.events) else {
panic!("expected terminal failure");
};
assert_eq!(error.diagnostic_details(), ["PipeWire core error: id=5 sequence=21 status=-22; link-factory: unknown output port 52"]);
}
}