mod keycodes;
use std::collections::HashSet;
use std::os::fd::AsRawFd;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};
use crossbeam_channel::Sender;
use evdev::{Device, EventSummary, KeyCode};
use nix::fcntl::{FcntlArg, OFlag, fcntl};
use nix::libc;
use crate::log;
use crate::{Error, Event, EventKind, tap::TapBuilder};
const KEYBOARD_PROBE: KeyCode = KeyCode::KEY_A;
#[derive(Debug)]
pub(crate) struct ShutdownGuard {
running: Arc<AtomicBool>,
thread: Option<JoinHandle<()>>,
}
impl Drop for ShutdownGuard {
fn drop(&mut self) {
log::debug!("keytap: stopping Linux evdev tap");
self.running.store(false, Ordering::Relaxed);
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}
pub(crate) fn start(tx: Sender<Event>, cfg: &TapBuilder) -> Result<ShutdownGuard, Error> {
log::debug!("keytap: starting Linux evdev tap");
let keyboards = find_keyboards()?;
if keyboards.is_empty() {
return Err(Error::NoDevices);
}
log::debug!("keytap: opened {} keyboard device(s)", keyboards.len());
let paths = keyboards
.iter()
.map(|(p, _)| p.clone())
.collect::<HashSet<_>>();
let devices: Vec<Device> = keyboards.into_iter().map(|(_, d)| d).collect();
set_nonblocking(&devices)?;
let running = Arc::new(AtomicBool::new(true));
let running_worker = running.clone();
let hotplug_interval = cfg.linux_hotplug_interval;
let thread = thread::Builder::new()
.name("keytap-linux-evdev".into())
.spawn(move || {
run_worker(devices, paths, tx, running_worker, hotplug_interval);
})
.map_err(|e| Error::TapFailed(format!("spawn evdev worker: {e}")))?;
Ok(ShutdownGuard {
running,
thread: Some(thread),
})
}
fn run_worker(
mut devices: Vec<Device>,
mut known_paths: HashSet<PathBuf>,
tx: Sender<Event>,
running: Arc<AtomicBool>,
hotplug_interval: Duration,
) {
let mut last_hotplug = Instant::now();
while running.load(Ordering::Relaxed) {
if last_hotplug.elapsed() >= hotplug_interval {
adopt_new_keyboards(&mut devices, &mut known_paths);
last_hotplug = Instant::now();
}
for dev in devices.iter_mut() {
match dev.fetch_events() {
Ok(events) => {
for ev in events {
if let EventSummary::Key(_, code, value) = ev.destructure() {
let key = keycodes::key_from_code(code.0);
let kind = match value {
0 => EventKind::KeyUp(key),
1 => EventKind::KeyDown(key),
2 => EventKind::KeyRepeat(key),
_ => continue,
};
if tx
.try_send(Event {
time: Instant::now(),
kind,
})
.is_err()
{
log::trace!("keytap: channel full — dropping event");
}
}
}
}
Err(e) => {
let code = e.raw_os_error();
if code != Some(libc::EAGAIN) && code != Some(libc::EWOULDBLOCK) {
}
}
}
}
thread::sleep(Duration::from_millis(10));
}
}
fn find_keyboards() -> Result<Vec<(PathBuf, Device)>, Error> {
let mut out = Vec::new();
let entries = std::fs::read_dir("/dev/input").map_err(Error::Io)?;
let mut saw_event_node = false;
let mut any_permission_denied = false;
for entry in entries.flatten() {
let path = entry.path();
if !is_event_node(&path) {
continue;
}
saw_event_node = true;
match Device::open(&path) {
Ok(device) => {
if is_keyboard(&device) {
out.push((path, device));
}
}
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
any_permission_denied = true;
}
Err(_) => {
}
}
}
if out.is_empty() && saw_event_node && any_permission_denied {
return Err(Error::PermissionDenied);
}
Ok(out)
}
fn is_event_node(path: &Path) -> bool {
path.file_name()
.and_then(|n| n.to_str())
.map(|n| n.starts_with("event"))
.unwrap_or(false)
}
fn is_keyboard(device: &Device) -> bool {
device
.supported_keys()
.map(|keys| keys.contains(KEYBOARD_PROBE))
.unwrap_or(false)
}
fn set_nonblocking(devices: &[Device]) -> Result<(), Error> {
for dev in devices {
let fd = dev.as_raw_fd();
let raw = fcntl(fd, FcntlArg::F_GETFL).map_err(io_from_nix)?;
let flags = OFlag::from_bits_truncate(raw) | OFlag::O_NONBLOCK;
fcntl(fd, FcntlArg::F_SETFL(flags)).map_err(io_from_nix)?;
}
Ok(())
}
fn io_from_nix(e: nix::errno::Errno) -> Error {
Error::Io(std::io::Error::from_raw_os_error(e as i32))
}
fn adopt_new_keyboards(devices: &mut Vec<Device>, known: &mut HashSet<PathBuf>) {
let Ok(entries) = std::fs::read_dir("/dev/input") else {
return;
};
let mut new_devices = Vec::new();
for entry in entries.flatten() {
let path = entry.path();
if !is_event_node(&path) || known.contains(&path) {
continue;
}
if let Ok(device) = Device::open(&path) {
if is_keyboard(&device) {
new_devices.push((path, device));
}
}
}
if new_devices.is_empty() {
return;
}
log::debug!(
"keytap: adopting {} new keyboard device(s) via hotplug",
new_devices.len()
);
thread::sleep(Duration::from_millis(100));
for (path, device) in new_devices {
if set_nonblocking(std::slice::from_ref(&device)).is_ok() {
known.insert(path);
devices.push(device);
}
}
}