evdevil/enumerate.rs
1//! Device enumeration.
2//!
3//! Applications can choose whether they are only interested in the currently plugged-in devices
4//! (via [`enumerate`]), or whether they also want to receive any devices that will be hot-plugged
5//! in later (via [`enumerate_hotplug`]).
6//!
7//! Device enumeration is always blocking, and cannot be made non-blocking or `async`.
8//! For interactive applications, it is recommended to perform device enumeration in a dedicated
9//! thread.
10
11use std::{
12 cmp,
13 collections::HashMap,
14 fs::{self, ReadDir},
15 io,
16 os::unix::fs::FileTypeExt as _,
17 path::PathBuf,
18 thread,
19 time::Duration,
20 vec,
21};
22
23use crate::{Evdev, hotplug::HotplugMonitor};
24
25/// Enumerates all currently plugged-in [`Evdev`] devices.
26///
27/// Performing enumeration can block for a significant amount of time while opening the *evdev*
28/// device files. In user-facing applications, it is recommended to perform enumeration in a
29/// background thread.
30///
31/// # Examples
32///
33/// ```
34/// use evdevil::enumerate;
35///
36/// for res in enumerate()? {
37/// let (path, evdev) = res?;
38/// println!("{}: {}", path.display(), evdev.name()?);
39/// }
40/// # Ok::<_, std::io::Error>(())
41/// ```
42pub fn enumerate() -> io::Result<Enumerate> {
43 Ok(Enumerate {
44 read_dir: fs::read_dir("/dev/input")?,
45 })
46}
47
48/// Enumerates all currently plugged-in [`Evdev`] devices, and future hotplugged devices.
49///
50/// The returned iterator will first yield the devices currently present on the system (like
51/// [`enumerate`]), and then blocks until new devices are plugged into the system (using
52/// [`HotplugMonitor`]).
53///
54/// This allows an application to process a single stream of [`Evdev`]s to both open an already
55/// plugged-in device on startup, but also to react to hot-plugged devices automatically, which is
56/// typically the desired UX of applications.
57///
58/// If opening the [`HotplugMonitor`] fails, this will degrade gracefully and only yield the
59/// currently plugged-in devices.
60///
61/// # Examples
62///
63/// ```no_run
64/// use evdevil::enumerate_hotplug;
65///
66/// for res in enumerate_hotplug()? {
67/// let (path, evdev) = res?;
68/// println!("{}: {}", path.display(), evdev.name()?);
69/// }
70/// # Ok::<_, std::io::Error>(())
71/// ```
72pub fn enumerate_hotplug() -> io::Result<EnumerateHotplug> {
73 EnumerateHotplug::new()
74}
75
76/// Iterator over evdev devices on the system.
77///
78/// Returned by [`enumerate`].
79///
80/// If a device is plugged into the system after [`enumerate`] has been called, it is unspecified
81/// whether [`Enumerate`] will yield the new device.
82#[derive(Debug)]
83pub struct Enumerate {
84 read_dir: ReadDir,
85}
86
87impl Iterator for Enumerate {
88 type Item = io::Result<(PathBuf, Evdev)>;
89
90 fn next(&mut self) -> Option<Self::Item> {
91 loop {
92 let entry = match self.read_dir.next()? {
93 Ok(ent) => ent,
94 Err(e) => return Some(Err(e)),
95 };
96
97 // Valid evdev devices are named `eventN`. `/dev/input` also contains some other
98 // devices like `/dev/input/mouseN` that we have to skip.
99 if !entry.file_name().as_encoded_bytes().starts_with(b"event") {
100 continue;
101 }
102
103 let path = entry.path();
104 let mkerr = |ioerr: io::Error| -> io::Error {
105 io::Error::new(
106 ioerr.kind(),
107 format!("failed to access '{}': {}", path.display(), ioerr),
108 )
109 };
110
111 let ty = match entry.file_type() {
112 Ok(ty) => ty,
113 Err(e) => return Some(Err(mkerr(e))),
114 };
115 if !ty.is_char_device() {
116 continue;
117 }
118
119 match Evdev::open_unchecked(&path) {
120 Ok(dev) => return Some(Ok((path, dev))),
121 // If a device is unplugged in the middle of enumeration (before it can be opened),
122 // skip it, since yielding this error to the application is pretty useless.
123 Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
124 Err(e) => return Some(Err(e)),
125 }
126 }
127 }
128}
129
130/// Enumerates all current devices, and future hotplugged devices.
131///
132/// Returned by [`enumerate_hotplug`].
133#[derive(Debug)]
134pub struct EnumerateHotplug {
135 to_yield: vec::IntoIter<io::Result<(PathBuf, Evdev)>>,
136
137 monitor: Option<HotplugMonitor>,
138 delay_ms: u32,
139}
140
141const INITIAL_DELAY: u32 = 250;
142const MAX_DELAY: u32 = 8000;
143
144impl EnumerateHotplug {
145 fn new() -> io::Result<Self> {
146 // The hotplug monitor has to be opened first, to ensure that devices plugged in during
147 // enumeration are not lost.
148 let monitor = match HotplugMonitor::new() {
149 Ok(m) => Some(m),
150 Err(e) => {
151 log::warn!("couldn't open hotplug monitor: {e}; device hotplug will not work");
152 None
153 }
154 };
155
156 // If a device is plugged in during enumeration, it may be yielded twice: once from the
157 // `readdir`-based enumeration, and once from the hotplug event.
158 // To prevent that, we collect all `readdir` devices into a collection, and then drain all
159 // pending hotplug events, ignoring those that belong to devices that we've already
160 // collected (and that haven't been unplugged and replugged).
161 // The resulting collection of devices is then yielded to the application, followed by any
162 // hotplug events that arrive after the `readdir` enumeration is complete.
163
164 let mut results = Vec::new();
165 let mut path_map = HashMap::new();
166 for res in enumerate()? {
167 match res {
168 Ok((path, evdev)) => {
169 let index = results.len();
170 results.push(Ok((path.clone(), evdev)));
171 path_map.insert(path, index);
172 }
173 Err(e) => results.push(Err(e)),
174 }
175 }
176 if cfg!(test) {
177 thread::sleep(Duration::from_millis(500));
178 }
179
180 if let Some(mon) = &monitor {
181 mon.set_nonblocking(true)?;
182
183 for res in mon {
184 let Ok(event) = res else {
185 break;
186 };
187
188 match path_map.get(event.path()) {
189 Some(&i) => {
190 match &results[i] {
191 Ok((path, evdev)) if evdev.driver_version().is_ok() => {
192 // This device is still plugged in. Ignore this `HotplugEvent`.
193 log::debug!("device at `{}` still present", path.display());
194 continue;
195 }
196 _ => {
197 // Try opening the device.
198 log::debug!(
199 "device at `{}` unplugged or errored; reopening",
200 event.path().display()
201 );
202 results[i] = event.open().map(|evdev| (event.into_path(), evdev));
203 }
204 }
205 }
206 None => {
207 // This is a device path we haven't seen before, so it's a newly plugged-in
208 // device.
209 log::debug!(
210 "found new device during enumeration: {}",
211 event.path().display()
212 );
213 let index = results.len();
214 let res = event
215 .open()
216 .map(|evdev| (event.path().to_path_buf(), evdev));
217 results.push(res);
218 path_map.insert(event.into_path(), index);
219 }
220 }
221 }
222
223 mon.set_nonblocking(false)?;
224 }
225
226 Ok(Self {
227 to_yield: results.into_iter(),
228 monitor,
229 delay_ms: INITIAL_DELAY,
230 })
231 }
232}
233
234impl Iterator for EnumerateHotplug {
235 type Item = io::Result<(PathBuf, Evdev)>;
236
237 fn next(&mut self) -> Option<Self::Item> {
238 if let Some(res) = self.to_yield.next() {
239 return Some(res);
240 }
241
242 let mon = match &mut self.monitor {
243 Some(mon) => mon,
244 None => loop {
245 // The connection to the hotplug monitor was broken. Back off and try to reconnect.
246 thread::sleep(Duration::from_millis(self.delay_ms.into()));
247 self.delay_ms = cmp::min(self.delay_ms * 2, MAX_DELAY);
248 match HotplugMonitor::new() {
249 Ok(mon) => {
250 #[cfg(test)]
251 mon.set_nonblocking(true).unwrap();
252
253 break self.monitor.insert(mon);
254 }
255 Err(e) => log::warn!("hotplug monitor reconnect failed: {e}"),
256 }
257 },
258 };
259
260 match mon.iter().next()? {
261 Ok(event) => {
262 let res = event.open().map(|dev| (event.into_path(), dev));
263 Some(res)
264 }
265 Err(e) => {
266 // If there's an error trying to receive a hotplug event, treat the socket
267 // as broken and reconnect next time the iterator is advanced.
268 self.monitor = None;
269 Some(Err(e))
270 }
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use crate::{event::Key, uinput::UinputDevice};
278
279 use super::*;
280
281 #[test]
282 fn hotplug_reconnect() {
283 let mut e = EnumerateHotplug {
284 to_yield: Vec::new().into_iter(),
285 monitor: None,
286 delay_ms: 25,
287 };
288
289 e.next(); // may be `None` or `Some` if an event arrived
290 assert!(e.monitor.is_some());
291 }
292
293 #[test]
294 fn hotplug_enumerate() {
295 if !fs::exists("/dev/uinput").unwrap() {
296 eprintln!("`/dev/uinput` doesn't exist, probably running under QEMU");
297 return;
298 }
299
300 env_logger::builder()
301 .filter_module(env!("CARGO_PKG_NAME"), log::LevelFilter::Debug)
302 .init();
303
304 let h = thread::spawn(|| -> io::Result<()> {
305 thread::sleep(Duration::from_millis(5));
306 let _uinput = UinputDevice::builder()?
307 .with_keys([Key::BTN_LEFT])?
308 .build(&format!("@@@hotplugtest-early"))?;
309 thread::sleep(Duration::from_millis(1000));
310 Ok(())
311 });
312
313 let iter = enumerate_hotplug().unwrap();
314 drop(iter);
315
316 h.join().unwrap().unwrap();
317 }
318}