use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::thread::{self, JoinHandle};
use flexaudio_core::backend::{CaptureBackend, RawSink};
use flexaudio_core::types::{Error, Result};
use crate::common::{translate_pid_to_object, FALLBACK_FORMAT};
use crate::tap::{build_tap_chain, TapChain, TapKind};
pub struct MacSystemBackend {
exclude_self: bool,
device_id: Option<String>,
stop_flag: Arc<AtomicBool>,
handle: Option<JoinHandle<()>>,
native: (u32, u16),
}
impl MacSystemBackend {
pub fn new(exclude_self: bool, device_id: Option<String>) -> Self {
Self {
exclude_self,
device_id,
stop_flag: Arc::new(AtomicBool::new(false)),
handle: None,
native: FALLBACK_FORMAT,
}
}
}
impl Default for MacSystemBackend {
fn default() -> Self {
Self::new(false, None)
}
}
impl CaptureBackend for MacSystemBackend {
fn native_format(&self) -> (u32, u16) {
self.native
}
fn start(&mut self, sink: RawSink) -> Result<()> {
if self.handle.is_some() {
return Ok(());
}
crate::version::ensure_process_tap_supported()?;
self.stop_flag.store(false, Ordering::SeqCst);
let stop_flag = self.stop_flag.clone();
let (ready_tx, ready_rx) = mpsc::channel::<Result<()>>();
let exclude_self = self.exclude_self;
let device_id = self.device_id.clone();
let handle = thread::Builder::new()
.name("flexaudio-macos-system".into())
.spawn(move || {
let kind = if exclude_self {
match translate_pid_to_object(std::process::id() as i32) {
Ok(0) => TapKind::ExcludeProcesses(Vec::new()),
Ok(self_object_id) => TapKind::ExcludeProcesses(vec![self_object_id]),
Err(e) => {
let _ = ready_tx.send(Err(e));
return;
}
}
} else if let Some(name) = device_id {
match crate::devices::uid_for_device_name(&name) {
Some(uid) => TapKind::ExcludeProcessesOnDevice {
ids: Vec::new(),
device_uid: uid,
},
None => {
let _ = ready_tx.send(Err(Error::DeviceNotFound));
return;
}
}
} else {
TapKind::ExcludeProcesses(Vec::new())
};
run_tap_thread(kind, sink, stop_flag, ready_tx);
})
.map_err(|e| Error::Backend(format!("spawn macos system thread: {e}")))?;
match ready_rx.recv() {
Ok(Ok(())) => {
self.handle = Some(handle);
Ok(())
}
Ok(Err(e)) => {
self.stop_flag.store(false, Ordering::SeqCst);
let _ = handle.join();
Err(e)
}
Err(_) => {
self.stop_flag.store(false, Ordering::SeqCst);
let _ = handle.join();
Err(Error::Backend(
"macos system thread exited before reporting readiness".into(),
))
}
}
}
fn stop(&mut self) {
self.stop_flag.store(true, Ordering::SeqCst);
if let Some(h) = self.handle.take() {
let _ = h.join();
}
}
}
impl Drop for MacSystemBackend {
fn drop(&mut self) {
self.stop();
}
}
pub(crate) fn run_tap_thread(
kind: TapKind,
sink: RawSink,
stop_flag: Arc<AtomicBool>,
ready_tx: mpsc::Sender<Result<()>>,
) {
let label = match &kind {
TapKind::IncludeProcesses(_) => "flexaudio-process-tap",
TapKind::ExcludeProcesses(_) => "flexaudio-system-tap",
TapKind::ExcludeProcessesOnDevice { .. } => "flexaudio-system-device-tap",
};
let chain: TapChain = match unsafe { build_tap_chain(kind, label, sink) } {
Ok(c) => c,
Err(e) => {
let _ = ready_tx.send(Err(e));
return;
}
};
if ready_tx.send(Ok(())).is_err() {
drop(chain);
return;
}
while !stop_flag.load(Ordering::SeqCst) {
thread::park_timeout(std::time::Duration::from_millis(100));
}
drop(chain);
}
#[cfg(test)]
mod tests {
use super::*;
use flexaudio_core::raw_ring;
#[test]
fn new_and_native_format_do_not_panic() {
let backend = MacSystemBackend::new(false, None);
let (rate, channels) = backend.native_format();
assert!(rate > 0);
assert!(channels > 0);
}
#[test]
fn start_then_stop_tolerates_failure() {
let mut backend = MacSystemBackend::new(false, None);
let (rate, channels) = backend.native_format();
let cap = (rate as usize * channels as usize).max(1);
let (prod, _cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
match backend.start(sink) {
Ok(()) => {
backend.stop();
backend.stop(); }
Err(_e) => { }
}
}
#[test]
fn new_exclude_self_start_then_stop_tolerates_failure() {
let mut backend = MacSystemBackend::new(true, None);
let (rate, channels) = backend.native_format();
assert!(rate > 0);
assert!(channels > 0);
let cap = (rate as usize * channels as usize).max(1);
let (prod, _cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
match backend.start(sink) {
Ok(()) => {
backend.stop();
backend.stop(); }
Err(_e) => { }
}
}
#[test]
fn new_with_unknown_device_id_returns_device_not_found() {
let mut backend =
MacSystemBackend::new(false, Some("flexaudio-no-such-output-device-xyzzy".into()));
let (rate, channels) = backend.native_format();
let cap = (rate as usize * channels as usize).max(1);
let (prod, _cons) = raw_ring(cap);
let sink = RawSink::new(prod, rate, channels);
match backend.start(sink) {
Err(Error::DeviceNotFound) | Err(Error::UnsupportedOsVersion) => {}
other => panic!("expected DeviceNotFound or UnsupportedOsVersion, got {other:?}"),
}
}
}