Skip to content

Commit

Permalink
Robust keyboard finding with udev and respond to removed/added keyboards
Browse files Browse the repository at this point in the history
  • Loading branch information
AethanFoot committed Mar 8, 2024
1 parent d9be9ff commit cc8d3d3
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 89 deletions.
32 changes: 7 additions & 25 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions lefthk-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ description = "A hotkey daemon for Adventurers"

[dependencies]
evdev-rs = { version = "0.6.1", features = ["serde"] }
input = "0.8"
mio = "0.8.0"
mio = "0.8.11"
nix = { version = "0.27.1", features = ["fs", "signal"] }
signal-hook = "0.3.4"
thiserror = "1.0.30"
Expand All @@ -23,6 +22,7 @@ tokio = { version = "1.14.0", features = [
"sync",
"time",
] }
udev = {version = "0.8.0", features = ["mio"] }
xdg = "2.4.0"
ron = "0.8.0"
serde = { version = "1.0.145", features = ["derive"] }
Expand Down
175 changes: 113 additions & 62 deletions lefthk-core/src/evdev.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
use evdev_rs::{Device, DeviceWrapper, InputEvent, ReadFlag, ReadStatus};
use input::event::{DeviceEvent, EventTrait};
use input::{Event, Libinput, LibinputInterface};
use nix::libc::{O_RDWR, O_WRONLY};
use std::ffi::OsStr;
use std::fs::{File, OpenOptions};
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::os::fd::AsRawFd;
use std::path::PathBuf;
use std::{collections::HashMap, ffi::OsStr};
use tokio::sync::{mpsc, oneshot};
use tokio::time::Duration;

use crate::errors::{self, LeftError};

#[derive(Debug)]
pub enum Task {
KeyboardEvent(InputEvent),
KeyboardAdded(String),
KeyboardAdded(PathBuf),
KeyboardRemoved(PathBuf),
}

pub struct EvDev {
pub task_receiver: mpsc::Receiver<Task>,
task_transmitter: mpsc::Sender<Task>,
task_guards: Vec<oneshot::Receiver<()>>,
task_guards: HashMap<PathBuf, oneshot::Receiver<()>>,
_keyboard_watcher: KeyboardWatcher,
}

impl Default for EvDev {
fn default() -> Self {
let (task_transmitter, task_receiver) = mpsc::channel(100);
let (task_transmitter, task_receiver) = mpsc::channel(128);

let keyboard_watcher = KeyboardWatcher::new(task_transmitter.clone());

let task_guards: Vec<oneshot::Receiver<()>> = vec![];
let task_guards: HashMap<PathBuf, oneshot::Receiver<()>> = HashMap::new();

let devices = find_keyboards();

Expand All @@ -41,17 +36,22 @@ impl Default for EvDev {
task_guards,
_keyboard_watcher: keyboard_watcher,
};
for device in devices {
evdev.add_device(device);
match devices {
Some(devices) => {
for device in devices {
evdev.add_device(device);
}
}
None => tracing::warn!("No devices found on intialization."),
}

evdev
}
}

impl EvDev {
pub fn add_device(&mut self, path: PathBuf) {
if let Some(device) = device_with_path(path) {
tracing::info!("Adding device with path: {:?}", path);
if let Some(device) = device_with_path(path.clone()) {
let (guard, task_guard) = oneshot::channel();
let transmitter = self.task_transmitter.clone();
const SERVER: mio::Token = mio::Token(0);
Expand All @@ -63,15 +63,15 @@ impl EvDev {
SERVER,
mio::Interest::READABLE,
));
let timeout = Duration::from_millis(100);

tokio::task::spawn(async move {
loop {
if guard.is_closed() {
println!("Bye");
return;
}

if let Err(err) = poll.poll(&mut events, Some(timeout)) {
if let Err(err) = poll.poll(&mut events, None) {
tracing::warn!("Evdev device poll failed with {:?}", err);
continue;
}
Expand All @@ -81,60 +81,43 @@ impl EvDev {
Ok((ReadStatus::Success, event)) => {
transmitter.send(Task::KeyboardEvent(event)).await.unwrap();
}
Err(_) => println!("Boo"),
Err(_) => break,
_ => {}
}
}
}
});
self.task_guards.push(task_guard);
self.task_guards.insert(path, task_guard);
}
}
}

struct Interface;

impl LibinputInterface for Interface {
fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
OpenOptions::new()
.custom_flags(flags)
.read((flags != 0) | (flags & O_RDWR != 0))
.write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0))
.open(path)
.map(|file| file.into())
.map_err(|err| err.raw_os_error().unwrap())
}
fn close_restricted(&mut self, fd: OwnedFd) {
let _ = File::from(fd);
pub fn remove_device(&mut self, path: PathBuf) {
tracing::info!("Removing device with path: {:?}", path);
drop(self.task_guards.remove(&path));
}
}

fn find_keyboards() -> Vec<PathBuf> {
let mut context = Libinput::new_with_udev(Interface);
context.udev_assign_seat("seat0").unwrap();
context.dispatch().unwrap();
fn find_keyboards() -> Option<Vec<PathBuf>> {
let mut devices = vec![];
for event in &mut context {
if let Event::Device(DeviceEvent::Added(_)) = &event {
unsafe {
if let Some(device) = event.device().udev_device() {
let is_keyboard = device
.property_value("ID_INPUT_KEYBOARD")
.unwrap_or(OsStr::new("0"))
== "1"
&& device
.property_value("ID_INPUT_MOUSE")
.unwrap_or(OsStr::new("0"))
== "0";
if is_keyboard {
let path = device.property_value("DEVNAME").unwrap_or(OsStr::new(""));
devices.push(PathBuf::from(path))
}
}
let mut enumerator = udev::Enumerator::new().ok()?;
enumerator.match_is_initialized().ok()?;
enumerator.match_subsystem("input").ok()?;
let enum_devices = enumerator.scan_devices().ok()?;
for device in enum_devices {
if let Some(devnode) = device.devnode() {
let is_keyboard = device
.property_value("ID_INPUT_KEYBOARD")
.unwrap_or(OsStr::new("0"))
== "1"
&& device
.property_value("ID_INPUT_MOUSE")
.unwrap_or(OsStr::new("0"))
== "0";
if is_keyboard {
devices.push(PathBuf::from(devnode));
}
}
}
devices
Some(devices)
}

fn device_with_path(path: PathBuf) -> Option<Device> {
Expand All @@ -153,9 +136,77 @@ struct KeyboardWatcher {
}

impl KeyboardWatcher {
pub fn new(_task_transmitter: mpsc::Sender<Task>) -> Self {
let (_guard, task_guard) = oneshot::channel();
tokio::task::spawn(async move {});
pub fn new(task_transmitter: mpsc::Sender<Task>) -> Self {
let (guard, task_guard) = oneshot::channel();

tokio::task::spawn_blocking(move || {
let mut socket = udev::MonitorBuilder::new()
.expect("Failed to create monitor")
.match_subsystem("input")
.expect("Failed to match subsystem")
.listen()
.expect("Failed to listen");
const SERVER: mio::Token = mio::Token(0);
let mut poll = mio::Poll::new().expect("Failed to create poll");
let mut events = mio::Events::with_capacity(1);
poll.registry()
.register(&mut socket, SERVER, mio::Interest::READABLE)
.expect("Failed to register");
loop {
if guard.is_closed() {
println!("Bye");
return;
}
if let Err(err) = poll.poll(&mut events, None) {
tracing::warn!("KeyboardWatcher poll failed with {:?}", err);
continue;
}

for e in socket.iter() {
let device = e.device();
let is_keyboard = device
.property_value("ID_INPUT_KEYBOARD")
.unwrap_or(OsStr::new("0"))
== "1"
&& device
.property_value("ID_INPUT_MOUSE")
.unwrap_or(OsStr::new("0"))
== "0";
if is_keyboard {
let path = device
.property_value("DEVNAME")
.unwrap_or(OsStr::new(""))
.to_owned();
if path.is_empty() {
continue;
}
match e.event_type() {
udev::EventType::Add => {
if let Err(err) = task_transmitter
.try_send(Task::KeyboardAdded(PathBuf::from(path)))
{
tracing::warn!(
"Failed to send keyboard added event: {:?}",
err
);
}
}
udev::EventType::Remove => {
if let Err(err) = task_transmitter
.try_send(Task::KeyboardRemoved(PathBuf::from(path)))
{
tracing::warn!(
"Failed to send keyboard removed event: {:?}",
err
);
}
}
_ => {}
}
}
}
}
});
Self {
_task_guard: task_guard,
}
Expand Down
3 changes: 3 additions & 0 deletions lefthk-core/src/worker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ impl Worker {
Task::KeyboardAdded(path) => {
self.evdev.add_device(path.into());
}
Task::KeyboardRemoved(path) => {
self.evdev.remove_device(path.into());
}
}
}
Some(command) = pipe.get_next_command() => {
Expand Down

0 comments on commit cc8d3d3

Please # to comment.