use std::ffi::{CStr, CString, c_char, c_void};
use std::sync::mpsc::{self, Receiver, Sender};
use std::thread;
use objc2_core_foundation::{CFDictionary, CFRetained, CFRunLoop, kCFRunLoopDefaultMode};
use objc2_io_kit::{
IOIteratorNext, IONotificationPort, IONotificationPortRef, IOObjectRelease,
IOServiceAddMatchingNotification, IOServiceMatching, io_iterator_t, kIOMainPortDefault,
kIOMatchedNotification, kIOTerminatedNotification,
};
use crate::iokit::SERVICE_CLASS;
pub(crate) fn watch() -> Receiver<()> {
let (nudged, nudges) = mpsc::channel();
thread::spawn(move || listen(&nudged));
nudges
}
fn listen(nudged: &Sender<()>) {
let port = IONotificationPort::create(unsafe { kIOMainPortDefault });
if port.is_null() {
return;
}
let refcon: *mut c_void = std::ptr::from_ref(nudged).cast_mut().cast();
let armed = [kIOMatchedNotification, kIOTerminatedNotification]
.map(|kind| register(port, kind, refcon));
if let Some(source) = unsafe { IONotificationPort::run_loop_source(port) }
&& let Some(run_loop) = CFRunLoop::current()
{
run_loop.add_source(Some(&source), unsafe { kCFRunLoopDefaultMode });
CFRunLoop::run();
}
for iterator in armed.into_iter().flatten() {
IOObjectRelease(iterator);
}
unsafe { IONotificationPort::destroy(port) };
}
fn register(
port: IONotificationPortRef,
kind: &CStr,
refcon: *mut c_void,
) -> Option<io_iterator_t> {
let class = CString::new(SERVICE_CLASS).expect("class name has no interior nul");
let matching = unsafe { IOServiceMatching(class.as_ptr()) }?;
let matching: CFRetained<CFDictionary> = unsafe { CFRetained::cast_unchecked(matching) };
let mut iterator: io_iterator_t = 0;
let result = unsafe {
IOServiceAddMatchingNotification(
port,
kind.as_ptr().cast_mut().cast::<[c_char; 128]>(),
Some(matching),
Some(changed),
refcon,
&mut iterator,
)
};
if result != 0 || iterator == 0 {
return None;
}
empty(iterator);
Some(iterator)
}
unsafe extern "C-unwind" fn changed(refcon: *mut c_void, iterator: io_iterator_t) {
empty(iterator);
let nudged = unsafe { &*refcon.cast::<Sender<()>>() };
if nudged.send(()).is_err()
&& let Some(run_loop) = CFRunLoop::current()
{
run_loop.stop();
}
}
fn empty(iterator: io_iterator_t) {
loop {
let entry = IOIteratorNext(iterator);
if entry == 0 {
break;
}
IOObjectRelease(entry);
}
}
#[cfg(test)]
mod tests {
use std::sync::mpsc::RecvTimeoutError;
use std::time::Duration;
use super::*;
#[test]
#[ignore = "needs a real machine and a device to connect or disconnect"]
fn a_real_device_change_arrives_as_a_nudge() {
let nudges = watch();
assert_eq!(
nudges.recv_timeout(Duration::from_secs(30)),
Ok(()),
"no notification arrived: connect or disconnect a Magic peripheral"
);
assert_eq!(
nudges.recv_timeout(Duration::from_millis(100)),
Err(RecvTimeoutError::Timeout),
"one change is one nudge, not a stream of them"
);
}
}