1use std::collections::HashMap;
9use std::collections::HashSet;
10use std::ffi::CString;
11use std::io;
12use std::mem::size_of;
13use std::os::fd::AsRawFd;
14use std::os::fd::FromRawFd;
15use std::os::fd::OwnedFd;
16use std::os::fd::RawFd;
17use std::os::unix::ffi::OsStrExt;
18use std::path::Path;
19use std::path::PathBuf;
20
21use evdev::Device;
22use evdev::EventSummary;
23use evdev::InputEvent;
24use evdev::KeyCode;
25use kbd::device::DeviceInfo;
26use kbd::key::Key;
27use kbd::key_state::KeyTransition;
28
29use crate::convert::EvdevKeyCodeExt;
30use crate::forwarder::VIRTUAL_DEVICE_NAME;
31
32pub const INPUT_DIRECTORY: &str = "/dev/input";
37const HOTPLUG_BUFFER_SIZE: usize = 4096;
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum DeviceGrabMode {
42 Shared,
44 Exclusive,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq)]
49pub(crate) struct HotplugFsEvent {
50 pub(crate) mask: u32,
51 pub(crate) device_name: String,
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub(crate) enum HotplugPathChange {
56 Added(PathBuf),
57 Removed(PathBuf),
58 Unchanged,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub(crate) enum DiscoveryOutcome {
63 Keyboard,
64 NotKeyboard,
65 Skip,
66}
67
68#[derive(Debug)]
69struct ManagedDevice {
70 path: PathBuf,
71 device: Device,
72 info: DeviceInfo,
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct DeviceKeyEvent {
81 pub device_fd: RawFd,
83 pub key: Key,
85 pub transition: KeyTransition,
87}
88
89#[derive(Debug)]
95pub struct PollResult {
96 pub key_events: Vec<DeviceKeyEvent>,
98 pub disconnected_devices: Vec<RawFd>,
101}
102
103#[derive(Debug)]
118pub struct DeviceManager {
119 input_dir: PathBuf,
120 grab_mode: DeviceGrabMode,
121 inotify_fd: Option<OwnedFd>,
122 devices: HashMap<RawFd, ManagedDevice>,
123 poll_fds: Vec<RawFd>,
124}
125
126impl Default for DeviceManager {
127 fn default() -> Self {
128 Self::new(Path::new(INPUT_DIRECTORY), DeviceGrabMode::Shared)
129 }
130}
131
132impl DeviceManager {
133 #[must_use]
142 pub fn new(input_dir: &Path, grab_mode: DeviceGrabMode) -> Self {
143 let mut manager = Self {
144 input_dir: input_dir.to_path_buf(),
145 grab_mode,
146 inotify_fd: initialize_inotify(input_dir).ok(),
147 devices: HashMap::new(),
148 poll_fds: Vec::new(),
149 };
150
151 manager.discover_existing_devices();
152 manager.rebuild_poll_fds();
153 manager
154 }
155
156 fn discover_existing_devices(&mut self) {
157 let discover_result =
158 discover_devices_in_dir_with(&self.input_dir, DiscoveryOutcome::probe);
159
160 if let Ok(paths) = discover_result {
161 for path in paths {
162 self.add_device_path(&path);
163 }
164 }
165 }
166
167 fn add_device_path(&mut self, path: &Path) {
168 if self.devices.values().any(|device| device.path == path) {
169 return;
170 }
171
172 let open_result = ManagedDevice::open(path, self.grab_mode);
173 let Some(device) = open_result.ok().flatten() else {
174 return;
175 };
176
177 let fd = device.device.as_raw_fd();
178 self.devices.insert(fd, device);
179 self.rebuild_poll_fds();
180 }
181
182 fn remove_device_fd(&mut self, fd: RawFd) -> bool {
183 if self.devices.remove(&fd).is_some() {
184 self.rebuild_poll_fds();
185 true
186 } else {
187 false
188 }
189 }
190
191 fn remove_device_path(&mut self, path: &Path) -> Option<RawFd> {
192 let fd = self
193 .devices
194 .iter()
195 .find_map(|(&fd, device)| (device.path == path).then_some(fd))?;
196
197 if self.remove_device_fd(fd) {
198 Some(fd)
199 } else {
200 None
201 }
202 }
203
204 fn rebuild_poll_fds(&mut self) {
205 self.poll_fds.clear();
206
207 if let Some(inotify_fd) = self.inotify_fd.as_ref() {
208 self.poll_fds.push(inotify_fd.as_raw_fd());
209 }
210
211 let mut device_fds: Vec<_> = self.devices.keys().copied().collect();
212 device_fds.sort_unstable();
213 self.poll_fds.extend(device_fds);
214 }
215
216 fn process_hotplug_events(&mut self, disconnected: &mut Vec<RawFd>) {
217 let Some(inotify_fd) = self.inotify_fd.as_ref().map(AsRawFd::as_raw_fd) else {
218 return;
219 };
220
221 let mut buffer = [0_u8; HOTPLUG_BUFFER_SIZE];
222 let mut known_paths: HashSet<PathBuf> = self
223 .devices
224 .values()
225 .map(|device| device.path.clone())
226 .collect();
227
228 loop {
229 let read_result = unsafe {
232 libc::read(
233 inotify_fd,
234 (&raw mut buffer).cast::<libc::c_void>(),
235 buffer.len(),
236 )
237 };
238
239 if read_result < 0 {
240 let error = io::Error::last_os_error();
241 if error.kind() == io::ErrorKind::Interrupted {
242 continue;
243 }
244 if error.kind() == io::ErrorKind::WouldBlock {
245 break;
246 }
247 break;
248 }
249
250 if read_result == 0 {
251 break;
252 }
253
254 let bytes_read = usize::try_from(read_result).unwrap_or(0);
255 for event in parse_hotplug_events(&buffer, bytes_read) {
256 match event.classify_change(&mut known_paths, &self.input_dir) {
257 HotplugPathChange::Added(path) => {
258 self.add_device_path(&path);
259 }
260 HotplugPathChange::Removed(path) => {
261 if let Some(fd) = self.remove_device_path(&path) {
262 disconnected.push(fd);
263 }
264 }
265 HotplugPathChange::Unchanged => {}
266 }
267 }
268 }
269 }
270
271 fn process_device_fd(
272 &mut self,
273 fd: RawFd,
274 revents: i16,
275 collected_events: &mut Vec<DeviceKeyEvent>,
276 disconnected: &mut Vec<RawFd>,
277 ) {
278 if (revents & (libc::POLLERR | libc::POLLHUP | libc::POLLNVAL)) != 0 {
279 if self.remove_device_fd(fd) {
280 disconnected.push(fd);
281 }
282 return;
283 }
284
285 if (revents & libc::POLLIN) == 0 {
286 return;
287 }
288
289 let Some(device) = self.devices.get_mut(&fd) else {
290 return;
291 };
292
293 match device.device.read_key_events() {
294 Ok(events) => {
295 for event in events {
296 collected_events.push(DeviceKeyEvent {
297 device_fd: fd,
298 key: event.key,
299 transition: event.transition,
300 });
301 }
302 }
303 Err(error) if should_drop_device(&error) => {
304 if self.remove_device_fd(fd) {
305 disconnected.push(fd);
306 }
307 }
308 Err(_) => {}
309 }
310 }
311
312 #[must_use]
317 pub fn poll_fds(&self) -> &[RawFd] {
318 &self.poll_fds
319 }
320
321 #[must_use]
325 pub fn device_info(&self, fd: RawFd) -> Option<&DeviceInfo> {
326 self.devices.get(&fd).map(|managed| &managed.info)
327 }
328
329 pub fn process_polled_events(&mut self, polled_fds: &[libc::pollfd]) -> PollResult {
334 let mut key_events = Vec::new();
335 let mut disconnected_devices = Vec::new();
336
337 let ready_fds: Vec<_> = polled_fds
338 .iter()
339 .filter(|pollfd| pollfd.revents != 0)
340 .map(|pollfd| (pollfd.fd, pollfd.revents))
341 .collect();
342
343 for (fd, revents) in ready_fds {
344 if self
345 .inotify_fd
346 .as_ref()
347 .is_some_and(|inotify_fd| inotify_fd.as_raw_fd() == fd)
348 {
349 self.process_hotplug_events(&mut disconnected_devices);
350 } else {
351 self.process_device_fd(fd, revents, &mut key_events, &mut disconnected_devices);
352 }
353 }
354
355 PollResult {
356 key_events,
357 disconnected_devices,
358 }
359 }
360}
361
362fn initialize_inotify(input_dir: &Path) -> io::Result<OwnedFd> {
363 let raw_fd = unsafe { libc::inotify_init1(libc::IN_NONBLOCK | libc::IN_CLOEXEC) };
365 if raw_fd < 0 {
366 return Err(io::Error::last_os_error());
367 }
368
369 let fd = unsafe { OwnedFd::from_raw_fd(raw_fd) };
371 let path_cstr = CString::new(input_dir.as_os_str().as_bytes()).map_err(|_| {
372 io::Error::new(
373 io::ErrorKind::InvalidInput,
374 "input directory contained interior NUL byte",
375 )
376 })?;
377
378 let watch_result = unsafe {
381 libc::inotify_add_watch(
382 fd.as_raw_fd(),
383 path_cstr.as_ptr(),
384 libc::IN_CREATE
385 | libc::IN_DELETE
386 | libc::IN_MOVED_FROM
387 | libc::IN_MOVED_TO
388 | libc::IN_DELETE_SELF
389 | libc::IN_MOVE_SELF,
390 )
391 };
392
393 if watch_result < 0 {
394 return Err(io::Error::last_os_error());
395 }
396
397 Ok(fd)
398}
399
400impl DiscoveryOutcome {
401 fn probe(path: &Path) -> Self {
402 let Ok(device) = Device::open(path) else {
403 return Self::Skip;
404 };
405
406 if device.is_virtual_forwarder() {
407 return Self::Skip;
408 }
409
410 if device.is_keyboard() {
411 Self::Keyboard
412 } else {
413 Self::NotKeyboard
414 }
415 }
416}
417
418impl ManagedDevice {
419 fn open(path: &Path, grab_mode: DeviceGrabMode) -> io::Result<Option<Self>> {
420 let mut device = Device::open(path)?;
421
422 if !device.is_keyboard() {
423 return Ok(None);
424 }
425
426 if device.is_virtual_forwarder() {
427 return Ok(None);
428 }
429
430 let input_id = device.input_id();
431 let info = DeviceInfo::new(
432 device.name().unwrap_or(""),
433 input_id.vendor(),
434 input_id.product(),
435 );
436
437 if matches!(grab_mode, DeviceGrabMode::Exclusive) {
438 device.grab()?;
439 }
440
441 device.set_nonblocking(true)?;
442
443 Ok(Some(Self {
444 path: path.to_path_buf(),
445 device,
446 info,
447 }))
448 }
449}
450
451trait DeviceExt {
452 fn is_virtual_forwarder(&self) -> bool;
457
458 fn is_keyboard(&self) -> bool;
460
461 fn read_key_events(&mut self) -> io::Result<Vec<ObservedKeyEvent>>;
463}
464
465impl DeviceExt for Device {
466 fn is_virtual_forwarder(&self) -> bool {
467 self.name().is_some_and(|name| name == VIRTUAL_DEVICE_NAME)
468 }
469
470 fn is_keyboard(&self) -> bool {
471 self.supported_keys().is_some_and(|supported_keys| {
472 supported_keys.contains(KeyCode::KEY_A)
473 && supported_keys.contains(KeyCode::KEY_Z)
474 && supported_keys.contains(KeyCode::KEY_ENTER)
475 })
476 }
477
478 fn read_key_events(&mut self) -> io::Result<Vec<ObservedKeyEvent>> {
479 let mut events = Vec::new();
480
481 for event in self.fetch_events()? {
482 if let Some(observed) = ObservedKeyEvent::from_input_event(event) {
483 events.push(observed);
484 }
485 }
486
487 Ok(events)
488 }
489}
490
491pub(crate) fn discover_devices_in_dir_with<F>(
492 input_dir: &Path,
493 mut classify: F,
494) -> io::Result<Vec<PathBuf>>
495where
496 F: FnMut(&Path) -> DiscoveryOutcome,
497{
498 let mut device_paths = Vec::new();
499
500 for entry_result in std::fs::read_dir(input_dir)? {
501 let Ok(entry) = entry_result else {
502 continue;
503 };
504
505 let path = entry.path();
506 let Some(name) = path.file_name().and_then(|candidate| candidate.to_str()) else {
507 continue;
508 };
509
510 if !name.starts_with("event") {
511 continue;
512 }
513
514 if matches!(classify(&path), DiscoveryOutcome::Keyboard) {
515 device_paths.push(path);
516 }
517 }
518
519 device_paths.sort_unstable();
520 Ok(device_paths)
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq)]
524struct ObservedKeyEvent {
525 key: Key,
526 transition: KeyTransition,
527}
528
529impl ObservedKeyEvent {
530 fn from_input_event(event: InputEvent) -> Option<Self> {
531 match event.destructure() {
532 EventSummary::Key(_, key_code, value) => {
533 let transition = key_transition(value)?;
534 let key = key_code.to_key();
535 if key == Key::UNIDENTIFIED {
536 return None;
537 }
538 Some(Self { key, transition })
539 }
540 _ => None,
541 }
542 }
543}
544
545fn key_transition(value: i32) -> Option<KeyTransition> {
546 match value {
547 1 => Some(KeyTransition::Press),
548 0 => Some(KeyTransition::Release),
549 2 => Some(KeyTransition::Repeat),
550 _ => None,
551 }
552}
553
554fn should_drop_device(error: &io::Error) -> bool {
555 error.raw_os_error() == Some(libc::ENODEV)
556 || error.kind() == io::ErrorKind::NotFound
557 || error.kind() == io::ErrorKind::UnexpectedEof
558}
559
560impl HotplugFsEvent {
561 pub fn classify_change(
562 &self,
563 known_paths: &mut HashSet<PathBuf>,
564 input_dir: &Path,
565 ) -> HotplugPathChange {
566 if !self.device_name.starts_with("event") {
567 return HotplugPathChange::Unchanged;
568 }
569
570 let path = input_dir.join(&self.device_name);
571
572 if self.mask & (libc::IN_CREATE | libc::IN_MOVED_TO) != 0
573 && known_paths.insert(path.clone())
574 {
575 return HotplugPathChange::Added(path);
576 }
577
578 if self.mask
579 & (libc::IN_DELETE | libc::IN_MOVED_FROM | libc::IN_DELETE_SELF | libc::IN_MOVE_SELF)
580 != 0
581 {
582 known_paths.remove(&path);
583 return HotplugPathChange::Removed(path);
584 }
585
586 HotplugPathChange::Unchanged
587 }
588}
589
590#[must_use]
591pub(crate) fn parse_hotplug_events(buffer: &[u8], bytes_read: usize) -> Vec<HotplugFsEvent> {
592 let mut events = Vec::new();
593 let mut offset = 0_usize;
594
595 while offset + size_of::<libc::inotify_event>() <= bytes_read {
596 #[allow(clippy::cast_ptr_alignment)]
597 let event_ptr = buffer[offset..].as_ptr().cast::<libc::inotify_event>();
598 let event = unsafe { std::ptr::read_unaligned(event_ptr) };
602
603 let name_start = offset + size_of::<libc::inotify_event>();
604 let Some(name_end) = name_start.checked_add(event.len as usize) else {
605 break;
606 };
607
608 let device_name = if event.len > 0 && name_end <= bytes_read {
609 parse_hotplug_name(&buffer[name_start..name_end])
610 } else {
611 String::new()
612 };
613
614 events.push(HotplugFsEvent {
615 mask: event.mask,
616 device_name,
617 });
618
619 let Some(next_offset) =
620 offset.checked_add(size_of::<libc::inotify_event>() + event.len as usize)
621 else {
622 break;
623 };
624
625 offset = next_offset;
626 }
627
628 events
629}
630
631fn parse_hotplug_name(name_bytes: &[u8]) -> String {
632 let name_end = name_bytes
633 .iter()
634 .position(|&byte| byte == 0)
635 .unwrap_or(name_bytes.len());
636
637 String::from_utf8_lossy(&name_bytes[..name_end]).into_owned()
638}
639
640#[cfg(test)]
641mod tests {
642 use std::path::Path;
643 use std::time::SystemTime;
644 use std::time::UNIX_EPOCH;
645
646 use evdev::EventType;
647 use evdev::InputEvent;
648 use evdev::KeyCode;
649 use kbd::key::Key;
650 use kbd::key_state::KeyTransition;
651
652 use super::DiscoveryOutcome;
653 use super::HotplugFsEvent;
654 use super::HotplugPathChange;
655 use super::ObservedKeyEvent;
656 use super::discover_devices_in_dir_with;
657 use super::parse_hotplug_events;
658 use crate::forwarder::VIRTUAL_DEVICE_NAME;
659
660 #[test]
661 fn discover_event_devices_ignores_non_event_entries_and_non_keyboards() {
662 let temp = unique_test_dir();
663
664 std::fs::create_dir_all(&temp).expect("temp dir should be created");
665 std::fs::File::create(temp.join("event0"))
666 .expect("event0 should be created for discovery test");
667 std::fs::File::create(temp.join("event1"))
668 .expect("event1 should be created for discovery test");
669 std::fs::File::create(temp.join("mouse0"))
670 .expect("mouse0 should be created for discovery test");
671
672 let keyboards = discover_devices_in_dir_with(&temp, |path| {
673 match path.file_name().and_then(|name| name.to_str()) {
674 Some("event0") => DiscoveryOutcome::Keyboard,
675 Some("event1") => DiscoveryOutcome::NotKeyboard,
676 _ => DiscoveryOutcome::Skip,
677 }
678 })
679 .expect("discovery should succeed for temp dir");
680
681 assert_eq!(keyboards, vec![temp.join("event0")]);
682
683 std::fs::remove_dir_all(temp).expect("temp dir should be removed");
684 }
685
686 #[test]
687 fn parse_hotplug_events_extracts_device_names() {
688 let mut buffer = Vec::new();
689
690 append_inotify_event(&mut buffer, libc::IN_CREATE, "event3");
691 append_inotify_event(&mut buffer, libc::IN_DELETE, "mouse0");
692
693 let events = parse_hotplug_events(&buffer, buffer.len());
694 assert_eq!(
695 events,
696 vec![
697 HotplugFsEvent {
698 mask: libc::IN_CREATE,
699 device_name: "event3".into(),
700 },
701 HotplugFsEvent {
702 mask: libc::IN_DELETE,
703 device_name: "mouse0".into(),
704 },
705 ]
706 );
707 }
708
709 #[test]
710 fn classify_hotplug_change_distinguishes_add_remove_and_ignore() {
711 let mut known_paths = std::collections::HashSet::new();
712 let input_dir = Path::new("/dev/input");
713
714 let add_event = HotplugFsEvent {
715 mask: libc::IN_CREATE,
716 device_name: "event7".into(),
717 };
718 let added = add_event.classify_change(&mut known_paths, input_dir);
719 assert_eq!(added, HotplugPathChange::Added(input_dir.join("event7")));
720
721 let remove_event = HotplugFsEvent {
722 mask: libc::IN_DELETE,
723 device_name: "event7".into(),
724 };
725 let removed = remove_event.classify_change(&mut known_paths, input_dir);
726 assert_eq!(
727 removed,
728 HotplugPathChange::Removed(input_dir.join("event7"))
729 );
730
731 let ignored = HotplugFsEvent {
732 mask: libc::IN_CREATE,
733 device_name: "js0".into(),
734 }
735 .classify_change(&mut known_paths, input_dir);
736 assert_eq!(ignored, HotplugPathChange::Unchanged);
737 }
738
739 #[test]
740 fn virtual_forwarder_name_is_detected() {
741 let is_forwarder = |name: &str| name == VIRTUAL_DEVICE_NAME;
742
743 assert!(is_forwarder(VIRTUAL_DEVICE_NAME));
744 assert!(!is_forwarder("AT Translated Set 2 keyboard"));
745 assert!(!is_forwarder("Logitech USB Keyboard"));
746 assert!(!is_forwarder(""));
747 }
748
749 #[test]
750 fn key_input_events_are_converted_to_domain_keys() {
751 let press = ObservedKeyEvent::from_input_event(InputEvent::new(
752 EventType::KEY.0,
753 KeyCode::KEY_C.0,
754 1,
755 ));
756 assert_eq!(
757 press,
758 Some(ObservedKeyEvent {
759 key: Key::C,
760 transition: KeyTransition::Press,
761 })
762 );
763
764 let release = ObservedKeyEvent::from_input_event(InputEvent::new(
765 EventType::KEY.0,
766 KeyCode::KEY_C.0,
767 0,
768 ));
769 assert_eq!(
770 release,
771 Some(ObservedKeyEvent {
772 key: Key::C,
773 transition: KeyTransition::Release,
774 })
775 );
776
777 let repeat = ObservedKeyEvent::from_input_event(InputEvent::new(
778 EventType::KEY.0,
779 KeyCode::KEY_C.0,
780 2,
781 ));
782 assert_eq!(
783 repeat,
784 Some(ObservedKeyEvent {
785 key: Key::C,
786 transition: KeyTransition::Repeat,
787 })
788 );
789
790 let ignored = ObservedKeyEvent::from_input_event(InputEvent::new(
791 EventType::KEY.0,
792 KeyCode::new(1023).0,
793 1,
794 ));
795 assert_eq!(ignored, None);
796 }
797
798 fn unique_test_dir() -> std::path::PathBuf {
799 let nanos = SystemTime::now()
800 .duration_since(UNIX_EPOCH)
801 .expect("system clock should be after unix epoch")
802 .as_nanos();
803
804 std::env::temp_dir().join(format!(
805 "kbd-discovery-test-{}-{}",
806 std::process::id(),
807 nanos
808 ))
809 }
810
811 fn append_inotify_event(buffer: &mut Vec<u8>, mask: u32, name: &str) {
812 let mut name_bytes = name.as_bytes().to_vec();
813 name_bytes.push(0);
814
815 let event = libc::inotify_event {
816 wd: 1,
817 mask,
818 cookie: 0,
819 len: u32::try_from(name_bytes.len()).expect("name should fit in u32"),
820 };
821
822 let event_bytes = unsafe {
824 std::slice::from_raw_parts(
825 (&raw const event).cast::<u8>(),
826 std::mem::size_of::<libc::inotify_event>(),
827 )
828 };
829
830 buffer.extend_from_slice(event_bytes);
831 buffer.extend_from_slice(&name_bytes);
832 }
833}