diff --git a/Cargo.lock b/Cargo.lock index bd56722..e2a3a72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "bitflags" version = "1.2.1" @@ -18,6 +20,16 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" +[[package]] +name = "epoll" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20df693c700404f7e19d4d6fae6b15215d2913c27955d2b9d6f2c0f537511cd0" +dependencies = [ + "bitflags", + "libc", +] + [[package]] name = "getopts" version = "0.2.21" @@ -94,6 +106,7 @@ dependencies = [ name = "rusty-keys" version = "0.0.3" dependencies = [ + "epoll", "getopts", "inotify", "lazy_static", diff --git a/Cargo.toml b/Cargo.toml index 26346d6..ca1bb86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,4 +32,5 @@ lazy_static = "1.4.0" [target.'cfg(target_os="linux")'.dependencies] libc = "0.2.72" nix = "0.17.0" +epoll = "4.3.1" inotify = { version = "0.8.3", default-features = false, features = [] } diff --git a/src/error.rs b/src/error.rs index 58246da..6defe74 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,15 +3,9 @@ use std::error; use std::ffi; use std::io; -#[cfg(target_os = "linux")] -use std::sync::mpsc; - #[cfg(target_os = "linux")] use nix; -#[cfg(target_os = "linux")] -use libc; - /// UInput error. #[derive(Debug)] pub enum Error { @@ -23,15 +17,16 @@ pub enum Error { Nul(ffi::NulError), Io(io::Error), + + Toml(toml::de::Error), - #[cfg(target_os = "linux")] - Send(mpsc::SendError), - - /// The uinput file could not be found. - NotFound, - + NotAKeyboard, + /// error reading input_event ShortRead, + + /// epoll already added + EpollAlreadyAdded, } impl From for Error { @@ -53,13 +48,6 @@ impl From for Error { } } -#[cfg(target_os = "linux")] -impl From> for Error { - fn from(value: mpsc::SendError) -> Self { - Error::Send(value) - } -} - impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { @@ -69,13 +57,14 @@ impl fmt::Display for Error { &Error::Nul(ref err) => err.fmt(f), &Error::Io(ref err) => err.fmt(f), - - #[cfg(target_os = "linux")] - &Error::Send(ref err) => err.fmt(f), - - &Error::NotFound => f.write_str("Device not found."), + + &Error::Toml(ref err) => err.fmt(f), + + &Error::NotAKeyboard => f.write_str("This device file is not a keyboard"), &Error::ShortRead => f.write_str("Error while reading from device file."), + + &Error::EpollAlreadyAdded => f.write_str("epoll already added, delete first"), } } } diff --git a/src/keymapper.rs b/src/keymapper.rs index 0f70e34..8aa992e 100644 --- a/src/keymapper.rs +++ b/src/keymapper.rs @@ -470,9 +470,5 @@ fn parse_cfg>(path: P) -> Result { let mut f = File::open(path)?; let mut input = String::new(); f.read_to_string(&mut input)?; - //toml::from_str(&input)? - match toml::from_str(&input) { - Ok(toml) => Ok(toml), - Err(_) => Err(Error::NotFound) // todo: something better - } + toml::from_str(&input).map_err(|e| Error::Toml(e)) } diff --git a/src/linux/device/input_device.rs b/src/linux/device/input_device.rs index 41cd7ed..d64dd96 100644 --- a/src/linux/device/input_device.rs +++ b/src/linux/device/input_device.rs @@ -2,54 +2,140 @@ use std::mem; use std::fs::File; use std::io::Read; use std::os::unix::io::AsRawFd; +use std::os::unix::prelude::RawFd; use libc::{input_event, c_int}; -use nix::ioctl_write_ptr; +use nix::{ioctl_write_ptr, ioctl_read_buf}; +use epoll::ControlOptions::{EPOLL_CTL_ADD, EPOLL_CTL_DEL}; +use nix::fcntl::{OFlag, fcntl, FcntlArg}; + use crate::{Error,Result}; +use crate::linux::{EV_KEY, KEY_MAX, NAME, KEY_W, KEY_A, KEY_S, KEY_D}; ioctl_write_ptr!(eviocgrab, b'E', 0x90, c_int); +ioctl_read_buf!(eviocgname, b'E', 0x06, u8); +ioctl_read_buf!(eviocgbit, b'E', 0x20, u8); +ioctl_read_buf!(eviocgbit_ev_key, b'E', 0x20 + EV_KEY, u8); const SIZE_OF_INPUT_EVENT: usize = mem::size_of::(); pub struct InputDevice { device_file: File, - buf: [u8; SIZE_OF_INPUT_EVENT], + grabbed: bool, + epoll_fd: Option, } impl InputDevice { - pub fn open(device_file: &str) -> Result { - let device_file = File::open(device_file)?; + pub fn open>(path: P) -> Result { Ok(InputDevice { - device_file: device_file, - buf: [0u8; SIZE_OF_INPUT_EVENT], + device_file: File::open(path)?, + grabbed: false, + epoll_fd: None, }) } + + pub fn new_input_event_buf() -> [u8; SIZE_OF_INPUT_EVENT] { + [0u8; SIZE_OF_INPUT_EVENT] + } - pub fn read_event(&mut self) -> Result { - let num_bytes = self.device_file.read(&mut self.buf)?; + pub fn read_event(&mut self, buf: &mut [u8; SIZE_OF_INPUT_EVENT]) -> Result { + let num_bytes = self.device_file.read(buf)?; if num_bytes != SIZE_OF_INPUT_EVENT { return Err(Error::ShortRead); } - let event: input_event = unsafe { mem::transmute(self.buf) }; + let event: input_event = unsafe { mem::transmute(*buf) }; Ok(event) } - pub fn grab(&mut self) -> Result<()> { + pub fn valid_keyboard_device(self) -> Result { + use std::os::unix::fs::FileTypeExt; + + // must be a character device + if !self.device_file.metadata()?.file_type().is_char_device() { + return Err(Error::NotAKeyboard); + } + + let raw_fd = self.device_file.as_raw_fd(); + + // does it support EV_KEY + let mut evbit = [0u8; 8]; + unsafe { + eviocgbit(raw_fd, &mut evbit)?; + }; + let evbit = u64::from_ne_bytes(evbit); + if (evbit & (1 << EV_KEY)) == 0 { + return Err(Error::NotAKeyboard); + } + + // does it support all keys WASD ? (yes this is fairly random but probably good enough, could make configuration probably) + let mut key_bits = [0u8; (KEY_MAX as usize / 8) + 1]; + unsafe { + eviocgbit_ev_key(raw_fd, &mut key_bits)?; + }; + let key_unsupported = |key : c_int| (key_bits[key as usize / 8] & (1 << (key % 8))) == 0; + if key_unsupported(KEY_W) || key_unsupported(KEY_A) || key_unsupported(KEY_S) || key_unsupported(KEY_D) { + return Err(Error::NotAKeyboard); + } + + // is it another running copy of rusty-keys ? + let mut name = [0u8; NAME.len()]; + unsafe { + eviocgname(raw_fd, &mut name)? + }; + if NAME.as_bytes() == &name { + return Err(Error::NotAKeyboard); + } + return Ok(self); + } + + pub fn grab(mut self) -> Result { unsafe { eviocgrab(self.device_file.as_raw_fd(), 1 as *const c_int)?; } - Ok(()) + self.grabbed = true; + Ok(self) } pub fn release(&mut self) -> Result<()> { - unsafe { - eviocgrab(self.device_file.as_raw_fd(), 0 as *const c_int)?; + if self.grabbed { + unsafe { + eviocgrab(self.device_file.as_raw_fd(), 0 as *const c_int)?; + } + self.grabbed = false; } Ok(()) } + + pub fn epoll_add(mut self, epoll_fd: RawFd, data: u64) -> Result { + if None != self.epoll_fd { + return Err(Error::EpollAlreadyAdded); + } + let raw_fd = self.device_file.as_raw_fd(); + let flags = unsafe { + // https://github.com/nix-rust/nix/issues/1102 + OFlag::from_bits_unchecked(fcntl(raw_fd, FcntlArg::F_GETFL)?) + }; + fcntl(raw_fd, FcntlArg::F_SETFL(flags | OFlag::O_NONBLOCK))?; + + let epoll_event = epoll::Event::new(epoll::Events::EPOLLIN | epoll::Events::EPOLLET, data); + epoll::ctl(epoll_fd, EPOLL_CTL_ADD, raw_fd, epoll_event)?; + self.epoll_fd = Some(epoll_fd); + Ok(self) + } + + pub fn epoll_del(&mut self) -> Result<&mut Self> { + if let Some(epoll_fd) = self.epoll_fd { + let empty_event = epoll::Event::new(epoll::Events::empty(), 0); + epoll::ctl(epoll_fd, EPOLL_CTL_DEL, self.device_file.as_raw_fd(), empty_event)?; + self.epoll_fd = None; + } + Ok(self) + } } impl Drop for InputDevice { fn drop(&mut self) { + // todo: only release if grabbed self.release().ok(); // ignore any errors here, what could we do anyhow? + self.epoll_del().ok(); } } diff --git a/src/linux/mod.rs b/src/linux/mod.rs index 71bfb03..c4de801 100644 --- a/src/linux/mod.rs +++ b/src/linux/mod.rs @@ -19,9 +19,7 @@ pub fn open>(path: P) -> Result { use libc::input_event; use std::process::exit; -use std::{env, thread}; -use std::sync::mpsc; -use std::sync::mpsc::Sender; +use std::env; const INPUT_FOLDER: &str = "/dev/input/"; @@ -32,7 +30,6 @@ const UP: i32 = 0; use getopts::Options; use inotify::{ - EventMask, Inotify, WatchMask, }; @@ -101,8 +98,6 @@ impl Keyboard for Device { } } - - #[derive(Debug)] struct Config { device_files: Vec, @@ -132,76 +127,107 @@ pub fn main_res() -> Result<()> { let mut key_map = LinuxKeyMaps::from_cfg(&key_map, &config.config_file); //println!("keymaps: {:?}", keymaps); + let mut input_event_buf = InputDevice::new_input_event_buf(); + if config.device_files.len() == 1 { - // shortcut, don't bother with threads - let mut input_device = InputDevice::open(&config.device_files[0])?; - input_device.grab()?; + // shortcut, don't bother with epoll + let mut input_device = InputDevice::open(&config.device_files[0])?.grab()?; loop { - let event = input_device.read_event()?; + let event = input_device.read_event(&mut input_event_buf)?; send_event(&mut key_map, event, &device)? } } else { - // start up some intra thread communication - let (tx, rx) = mpsc::channel(); + let epoll_fd = epoll::create(true)?; + const INOTIFY_DATA: u64 = u64::max_value(); - if config.device_files.len() > 0 { - // we only want to operate on device files sent in then quit - for device_file in config.device_files.iter() { - let device_file = device_file.clone(); - let tx = tx.clone(); - thread::spawn(move || { - let ret = spawn_map_thread(tx, &device_file); - if let Err(e) = ret { - println!("mapping for {} ended due to error: {}", device_file, e); - } - }); - } + let (device_files, mut inotify) = if config.device_files.len() > 0 { + // we operate on exactly the devices sent in and never watch for new devices + (config.device_files.iter().map(|device_file| InputDevice::open(&device_file).expect("device_file does not exist!")).collect(), None) } else { - let tx = tx.clone(); - thread::spawn(move || { - // we want to wait forever starting new threads for any new keyboard devices - let mut inotify = Inotify::init().expect("Failed to initialize inotify"); + use std::os::unix::io::AsRawFd; + // we want to wait forever starting new threads for any new keyboard devices + // there is a slight race condition here, if a keyboard is plugged in between the time we + // enumerate the devices and set up the inotify watch, we'll miss it, doing it the other way + // can bring duplicates though, todo: think about this... + let device_files = get_keyboard_devices(); + let mut inotify = Inotify::init()?; + inotify.add_watch(INPUT_FOLDER, WatchMask::CREATE)?; + let epoll_event = epoll::Event::new(epoll::Events::EPOLLIN | epoll::Events::EPOLLET, INOTIFY_DATA); + epoll::ctl(epoll_fd, epoll::ControlOptions::EPOLL_CTL_ADD, inotify.as_raw_fd(), epoll_event)?; + (device_files, Some(inotify)) + }; + let mut input_devices = Vec::with_capacity(device_files.len()); + for (idx, device_file) in device_files.into_iter().enumerate() { + input_devices.push(Some(device_file.grab()?.epoll_add(epoll_fd, idx as u64)?)); + } - inotify.add_watch(INPUT_FOLDER, WatchMask::CREATE).expect("Failed to add inotify watch"); + let mut epoll_buf = [epoll::Event::new(epoll::Events::empty(), 0); 4]; + let mut inotify_buf = [0u8; 256]; - let device_files = get_keyboard_device_filenames(); - println!("Detected devices: {:?}", device_files); - for device_file in device_files.iter() { - inotify_spawn_thread(&tx, device_file); - } + loop { + let num_events = epoll::wait(epoll_fd, -1, &mut epoll_buf)?; + for event in &epoll_buf[0..num_events] { + let idx = event.data as usize; + if let Some(Some(input_device)) = &mut input_devices.get_mut(idx) { + loop { + match input_device.read_event(&mut input_event_buf) { + Ok(event) => { + //println!("input event: {:?}", event); + send_event(&mut key_map, event, &device)? + } + Err(err) => { + if let Error::Io(ref err) = err { + if err.kind() == std::io::ErrorKind::WouldBlock { + // go back to epoll event loop + break; + } + } + // otherwise it's some other error, don't read anything from this again + println!("input err: {:?}", err); + // remove it from input_devices and drop it + let _ = std::mem::replace(&mut input_devices[idx], None); + if inotify.is_none() { + // if we aren't watching with inotify, and the last device is removed (Vec only has None's in it), exit the program + if input_devices.iter().all(|id| id.is_none()) { + println!("last device went away, exiting..."); + return Ok(()); + } + } + break; + } + } + } + } else if event.data == INOTIFY_DATA { + if let Some(inotify) = &mut inotify { + for event in inotify.read_events(&mut inotify_buf)? { + if let Some(device_file) = event.name.and_then(|name| name.to_str()) { + // check if this is an eligible keyboard device + let mut path = std::path::PathBuf::new(); + path.push(INPUT_FOLDER); + path.push(device_file); + + if let Ok(input_device) = InputDevice::open(path).and_then(|id|id.valid_keyboard_device()) { + println!("starting mapping for new keyboard: {}", device_file); - let mut buffer = [0u8; 4096]; - loop { - let events = inotify.read_events_blocking(&mut buffer); + let idx = input_devices.iter().position(|id| id.is_none()).unwrap_or(input_devices.len()); - if let Ok(events) = events { - for event in events { - if !event.mask.contains(EventMask::ISDIR) { - if let Some(device_file) = event.name.and_then(|name|name.to_str()) { - // check if this is an eligible keyboard device - let mut path = std::path::PathBuf::new(); - path.push(INPUT_FOLDER); - path.push(device_file); + let input_device = input_device.grab()?.epoll_add(epoll_fd, idx as u64)?; - if valid_keyboard_device(path) { - println!("starting mapping thread for: {}", device_file); - inotify_spawn_thread(&tx, device_file.clone()); + if idx == input_devices.len() { + input_devices.push(Some(input_device)); + } else { + // simply replacing None here + let _ = std::mem::replace(&mut input_devices[idx], Some(input_device)); } } } } } } - }); - } - drop(tx); // drop our last one, so when the threads finish, everything stops - // process all events - for event in rx { - send_event(&mut key_map, event, &device)? + } } } - Ok(()) } fn send_event(key_map: &mut LinuxKeyMaps, mut event: input_event, device: &Device) -> Result<()> { @@ -213,28 +239,6 @@ fn send_event(key_map: &mut LinuxKeyMaps, mut event: input_event, device: &Devic Ok(()) } -fn inotify_spawn_thread(tx: &Sender, device_file: &str) { - let mut filename = INPUT_FOLDER.to_string(); - filename.push_str(&device_file); - let tx = tx.clone(); - thread::spawn(move || { - let ret = spawn_map_thread(tx, &filename); - if let Err(e) = ret { - println!("mapping for {} ended due to error: {}", filename, e); - } - }); -} - -fn spawn_map_thread(tx: Sender, device_file: &str) -> Result<()> { - let mut input_device = InputDevice::open(device_file)?; - input_device.grab()?; - - loop { - let event = input_device.read_event()?; - tx.send(event)? - } -} - fn parse_args() -> Config { fn print_usage(program: &str, opts: Options) { let brief = format!("Usage: {} [options] [device_files...]", program); @@ -269,64 +273,13 @@ fn parse_args() -> Config { Config::new(matches.free, config_file) } -nix::ioctl_read_buf!(eviocgname, b'E', 0x06, u8); -nix::ioctl_read_buf!(eviocgbit, b'E', 0x20, u8); -nix::ioctl_read_buf!(eviocgbit_ev_key, b'E', 0x20 + EV_KEY, u8); - -fn valid_keyboard_device_res>(path: P) -> Result { - use std::fs::File; - use std::os::unix::fs::FileTypeExt; - use std::os::unix::io::AsRawFd; - - let device_file = File::open(path)?; - - // must be a character device - if !device_file.metadata()?.file_type().is_char_device() { - return Ok(false); - } - - // does it support EV_KEY - let mut evbit = [0u8; 8]; - unsafe { - eviocgbit(device_file.as_raw_fd(), &mut evbit)?; - }; - let evbit = u64::from_ne_bytes(evbit); - if (evbit & (1 << EV_KEY)) == 0 { - return Ok(false); - } - - // does it support KEY_A ? todo: check other keys ? - let mut key_bits = [0u8; (KEY_MAX as usize / 8) + 1]; - unsafe { - eviocgbit_ev_key(device_file.as_raw_fd(), &mut key_bits)?; - }; - if (key_bits[KEY_A as usize / 8] & (1 << (KEY_A % 8))) == 0 { - return Ok(false); - } - - // is it another running copy of rusty-keys ? - let mut name = [0u8; NAME.len()]; - unsafe { - eviocgname(device_file.as_raw_fd(), &mut name)? - }; - if NAME.as_bytes() == &name { - return Ok(false); - } - return Ok(true); -} - -fn valid_keyboard_device>(path: P) -> bool { - valid_keyboard_device_res(path).unwrap_or(false) -} - -fn get_keyboard_device_filenames() -> Vec { +fn get_keyboard_devices() -> Vec { let mut res = Vec::new(); if let Ok(entries) = std::fs::read_dir(INPUT_FOLDER) { for entry in entries { if let Ok(entry) = entry { - if valid_keyboard_device(entry.path()) { - // these unwrap()'s should not be able to fail if valid_keyboard_device() returns true - res.push(entry.path().file_name().unwrap().to_str().unwrap().to_owned()); + if let Ok(input_device) = InputDevice::open(entry.path()).and_then(|id|id.valid_keyboard_device()) { + res.push(input_device); } } }