use flexaudio_core::types::DeviceEvent;
trait DeviceWatchBackend: Send {
fn poll_event(&mut self) -> Option<DeviceEvent>;
fn stop(&mut self);
}
pub struct DeviceWatcher {
inner: Box<dyn DeviceWatchBackend>,
}
impl DeviceWatcher {
pub fn poll_event(&mut self) -> Option<DeviceEvent> {
self.inner.poll_event()
}
pub fn stop(&mut self) {
self.inner.stop();
}
}
impl Drop for DeviceWatcher {
fn drop(&mut self) {
self.stop();
}
}
#[cfg(any(test, not(target_os = "linux")))]
struct NoopWatcher;
#[cfg(any(test, not(target_os = "linux")))]
impl DeviceWatchBackend for NoopWatcher {
fn poll_event(&mut self) -> Option<DeviceEvent> {
None
}
fn stop(&mut self) {}
}
#[cfg(target_os = "linux")]
impl DeviceWatchBackend for flexaudio_os_linux::PwDeviceWatcher {
fn poll_event(&mut self) -> Option<DeviceEvent> {
flexaudio_os_linux::PwDeviceWatcher::poll_event(self)
}
fn stop(&mut self) {
flexaudio_os_linux::PwDeviceWatcher::stop(self)
}
}
pub(crate) fn watch_devices() -> flexaudio_core::types::Result<DeviceWatcher> {
#[cfg(target_os = "linux")]
{
watcher_from_start(
flexaudio_os_linux::PwDeviceWatcher::start()
.map(|watcher| Box::new(watcher) as Box<dyn DeviceWatchBackend>),
)
}
#[cfg(not(target_os = "linux"))]
{
watcher_from_start(Ok(Box::new(NoopWatcher)))
}
}
fn watcher_from_start(
start: flexaudio_core::Result<Box<dyn DeviceWatchBackend>>,
) -> flexaudio_core::Result<DeviceWatcher> {
start.map(|inner| DeviceWatcher { inner })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn watcher_is_send() {
fn assert_send<T: Send>() {}
assert_send::<DeviceWatcher>();
}
#[test]
fn noop_watcher_yields_nothing() {
let mut w = DeviceWatcher {
inner: Box::new(NoopWatcher),
};
assert!(w.poll_event().is_none());
assert!(w.poll_event().is_none());
w.stop();
w.stop();
assert!(w.poll_event().is_none());
}
#[test]
fn watcher_start_failure_returns_error() {
let error = flexaudio_core::Error::Backend("unavailable watcher".into());
assert!(
matches!(watcher_from_start(Err(error.clone())), Err(observed) if observed == error)
);
}
}
#[cfg(test)]
mod repro_tests {
use super::*;
#[test]
#[cfg(target_os = "linux")]
fn repro_p2_watcher_start_error() {
if std::env::var_os("FLEXAUDIO_REPRO_WATCHER_CHILD").is_none() {
let directory = std::env::temp_dir()
.join(format!("flexaudio-repro-watcher-{}", std::process::id()));
std::fs::create_dir(&directory).unwrap();
let output = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"device_watcher::repro_tests::repro_p2_watcher_start_error",
"--exact",
"--include-ignored",
"--nocapture",
])
.env("FLEXAUDIO_REPRO_WATCHER_CHILD", "1")
.env("XDG_RUNTIME_DIR", &directory)
.env("PIPEWIRE_RUNTIME_DIR", &directory)
.env("PIPEWIRE_REMOTE", "absent-repro-server")
.output()
.unwrap();
std::fs::remove_dir(directory).unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(output.status.success(), "{stdout}\n{stderr}");
return;
}
let original = flexaudio_os_linux::PwDeviceWatcher::start()
.err()
.expect("fixture must fail native watcher startup");
let result = watch_devices();
assert!(
result.is_err(),
"F26: native startup error={original}; facade returned Ok(NoopWatcher)"
);
}
#[test]
fn repro_p2_watcher_start_error_control() {
let mut watcher = DeviceWatcher {
inner: Box::new(NoopWatcher),
};
assert!(watcher.poll_event().is_none());
watcher.stop();
}
}