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}