use std::{
cmp,
collections::HashMap,
fs::{self, ReadDir},
io,
os::unix::fs::FileTypeExt as _,
path::PathBuf,
thread,
time::Duration,
vec,
};
use crate::{Evdev, hotplug::HotplugMonitor};
pub fn enumerate() -> io::Result<Enumerate> {
Ok(Enumerate {
read_dir: fs::read_dir("/dev/input")?,
})
}
pub fn enumerate_hotplug() -> io::Result<EnumerateHotplug> {
EnumerateHotplug::new()
}
#[derive(Debug)]
pub struct Enumerate {
read_dir: ReadDir,
}
impl Iterator for Enumerate {
type Item = io::Result<(PathBuf, Evdev)>;
fn next(&mut self) -> Option<Self::Item> {
loop {
let entry = match self.read_dir.next()? {
Ok(ent) => ent,
Err(e) => return Some(Err(e)),
};
if !entry.file_name().as_encoded_bytes().starts_with(b"event") {
continue;
}
let path = entry.path();
let mkerr = |ioerr: io::Error| -> io::Error {
io::Error::new(
ioerr.kind(),
format!("failed to access '{}': {}", path.display(), ioerr),
)
};
let ty = match entry.file_type() {
Ok(ty) => ty,
Err(e) => return Some(Err(mkerr(e))),
};
if !ty.is_char_device() {
continue;
}
match Evdev::open_unchecked(&path) {
Ok(dev) => return Some(Ok((path, dev))),
Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
Err(e) => return Some(Err(e)),
}
}
}
}
#[derive(Debug)]
pub struct EnumerateHotplug {
to_yield: vec::IntoIter<io::Result<(PathBuf, Evdev)>>,
monitor: Option<HotplugMonitor>,
delay_ms: u32,
}
const INITIAL_DELAY: u32 = 250;
const MAX_DELAY: u32 = 8000;
impl EnumerateHotplug {
fn new() -> io::Result<Self> {
let monitor = match HotplugMonitor::new() {
Ok(m) => Some(m),
Err(e) => {
log::warn!("couldn't open hotplug monitor: {e}; device hotplug will not work");
None
}
};
let mut results = Vec::new();
let mut path_map = HashMap::new();
for res in enumerate()? {
match res {
Ok((path, evdev)) => {
let index = results.len();
results.push(Ok((path.clone(), evdev)));
path_map.insert(path, index);
}
Err(e) => results.push(Err(e)),
}
}
if cfg!(test) {
thread::sleep(Duration::from_millis(500));
}
if let Some(mon) = &monitor {
mon.set_nonblocking(true)?;
for res in mon {
let Ok(event) = res else {
break;
};
match path_map.get(event.path()) {
Some(&i) => {
match &results[i] {
Ok((path, evdev)) if evdev.driver_version().is_ok() => {
log::debug!("device at `{}` still present", path.display());
continue;
}
_ => {
log::debug!(
"device at `{}` unplugged or errored; reopening",
event.path().display()
);
results[i] = event.open().map(|evdev| (event.into_path(), evdev));
}
}
}
None => {
log::debug!(
"found new device during enumeration: {}",
event.path().display()
);
let index = results.len();
let res = event
.open()
.map(|evdev| (event.path().to_path_buf(), evdev));
results.push(res);
path_map.insert(event.into_path(), index);
}
}
}
mon.set_nonblocking(false)?;
}
Ok(Self {
to_yield: results.into_iter(),
monitor,
delay_ms: INITIAL_DELAY,
})
}
}
impl Iterator for EnumerateHotplug {
type Item = io::Result<(PathBuf, Evdev)>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(res) = self.to_yield.next() {
return Some(res);
}
let mon = match &mut self.monitor {
Some(mon) => mon,
None => loop {
thread::sleep(Duration::from_millis(self.delay_ms.into()));
self.delay_ms = cmp::min(self.delay_ms * 2, MAX_DELAY);
match HotplugMonitor::new() {
Ok(mon) => {
#[cfg(test)]
mon.set_nonblocking(true).unwrap();
break self.monitor.insert(mon);
}
Err(e) => log::warn!("hotplug monitor reconnect failed: {e}"),
}
},
};
match mon.iter().next()? {
Ok(event) => {
let res = event.open().map(|dev| (event.into_path(), dev));
Some(res)
}
Err(e) => {
self.monitor = None;
Some(Err(e))
}
}
}
}
#[cfg(test)]
mod tests {
use crate::{event::Key, uinput::UinputDevice};
use super::*;
#[test]
fn hotplug_reconnect() {
let mut e = EnumerateHotplug {
to_yield: Vec::new().into_iter(),
monitor: None,
delay_ms: 25,
};
e.next(); assert!(e.monitor.is_some());
}
#[test]
fn hotplug_enumerate() {
if !fs::exists("/dev/uinput").unwrap() {
eprintln!("`/dev/uinput` doesn't exist, probably running under QEMU");
return;
}
env_logger::builder()
.filter_module(env!("CARGO_PKG_NAME"), log::LevelFilter::Debug)
.init();
let h = thread::spawn(|| -> io::Result<()> {
thread::sleep(Duration::from_millis(5));
let _uinput = UinputDevice::builder()?
.with_keys([Key::BTN_LEFT])?
.build(&format!("@@@hotplugtest-early"))?;
thread::sleep(Duration::from_millis(1000));
Ok(())
});
let iter = enumerate_hotplug().unwrap();
drop(iter);
h.join().unwrap().unwrap();
}
}