#![allow(unsafe_code)]
use std::io;
use std::os::fd::{AsRawFd, RawFd};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use udev::MonitorBuilder;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitResult {
Readable,
Timeout,
Interrupted,
}
#[derive(Debug, Clone)]
pub struct WatcherConfig {
pub subsystems: Vec<String>,
}
impl Default for WatcherConfig {
fn default() -> Self {
WatcherConfig {
subsystems: vec!["usb".into(), "typec".into()],
}
}
}
pub struct Watcher {
socket: udev::MonitorSocket,
}
impl Watcher {
pub fn new() -> io::Result<Self> {
Self::with_config(&WatcherConfig::default())
}
pub fn with_config(config: &WatcherConfig) -> io::Result<Self> {
let mut builder = MonitorBuilder::new()?;
for s in &config.subsystems {
builder = builder.match_subsystem(s)?;
}
let socket = builder.listen()?;
Ok(Watcher { socket })
}
pub fn fd(&self) -> RawFd {
self.socket.as_raw_fd()
}
pub fn drain(&mut self) -> usize {
self.socket.iter().count()
}
pub fn wait(&self, timeout: Option<Duration>) -> WaitResult {
let timeout_ms: i32 = match timeout {
Some(d) => d.as_millis().min(i32::MAX as u128) as i32,
None => -1,
};
let mut pfd = libc::pollfd {
fd: self.fd(),
events: libc::POLLIN,
revents: 0,
};
let r = unsafe { libc::poll(&mut pfd, 1, timeout_ms) };
if r < 0 {
WaitResult::Interrupted
} else if r == 0 {
WaitResult::Timeout
} else if (pfd.revents & libc::POLLIN) != 0 {
WaitResult::Readable
} else {
WaitResult::Timeout
}
}
}
static GLOBAL_RUNNING: AtomicBool = AtomicBool::new(true);
extern "C" fn on_signal(_: libc::c_int) {
GLOBAL_RUNNING.store(false, Ordering::SeqCst);
}
pub fn install_default_signal_handlers() {
GLOBAL_RUNNING.store(true, Ordering::SeqCst);
unsafe {
libc::signal(libc::SIGINT, on_signal as *const () as usize);
libc::signal(libc::SIGTERM, on_signal as *const () as usize);
}
}
pub fn run_loop<F>(debounce: Duration, mut on_refresh: F) -> io::Result<()>
where
F: FnMut(RefreshReason) -> io::Result<()>,
{
install_default_signal_handlers();
let mut watcher = Watcher::new()?;
on_refresh(RefreshReason::Initial)?;
let mut dirty: Option<Instant> = None;
while GLOBAL_RUNNING.load(Ordering::SeqCst) {
let timeout = dirty.map(|deadline| deadline.saturating_duration_since(Instant::now()));
match watcher.wait(timeout) {
WaitResult::Readable => {
watcher.drain();
dirty = Some(Instant::now() + debounce);
}
WaitResult::Timeout => {}
WaitResult::Interrupted => continue,
}
if let Some(deadline) = dirty {
if Instant::now() >= deadline {
on_refresh(RefreshReason::Hotplug)?;
dirty = None;
}
}
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RefreshReason {
Initial,
Hotplug,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn watcher_config_defaults_to_usb_and_typec() {
let cfg = WatcherConfig::default();
assert!(cfg.subsystems.iter().any(|s| s == "usb"));
assert!(cfg.subsystems.iter().any(|s| s == "typec"));
}
#[test]
#[cfg(target_os = "linux")]
fn watcher_can_open_and_close() {
match Watcher::new() {
Ok(w) => {
assert!(w.fd() >= 0);
}
Err(e) => {
eprintln!("Watcher::new failed (libudev unavailable?): {e}");
}
}
}
}