use super::ffi::{
ABS_CNT, AbsBits, AbsInfo, EV_ABS, EV_KEY, EVENT_SIZE, EVIOCGID_NR, EVIOCGKEY_NR, InputEvent,
InputId, KernelUlong, KeyBits, O_NONBLOCK, decode_events, evioc_read, eviocgabs_nr,
eviocgbit_nr, ioctl, ioctl_request,
};
use super::map::Probe;
use super::{Device, Node, Poller, Source};
use crate::GamepadEvent;
use crate::evdev::EvdevError;
use std::fs::{self, File, OpenOptions};
use std::io::{self, Read};
use std::os::fd::AsRawFd;
use std::os::unix::fs::{DirEntryExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
use std::time::Instant;
const INPUT_DIR: &str = "/dev/input";
const READ_BATCH: usize = 64;
unsafe trait Plain {}
unsafe impl Plain for InputId {}
unsafe impl Plain for AbsInfo {}
unsafe impl<const WORDS: usize> Plain for [KernelUlong; WORDS] {}
fn read_ioctl<T: Plain>(file: &File, nr: u32, out: &mut T) -> io::Result<()> {
let request = ioctl_request(evioc_read(nr, size_of::<T>()));
let result = unsafe {
ioctl(
file.as_raw_fd(),
request,
core::ptr::from_mut(out).cast::<core::ffi::c_void>(),
)
};
if result < 0 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[derive(Debug)]
struct InputDir {
dir: PathBuf,
}
impl Source for InputDir {
type Device = EventNode;
fn scan(&mut self) -> io::Result<Vec<Node>> {
let mut nodes = Vec::new();
for entry in fs::read_dir(&self.dir)? {
let entry = entry?;
if entry.file_name().as_encoded_bytes().starts_with(b"event") {
nodes.push(Node {
path: entry.path(),
ino: entry.ino(),
});
}
}
nodes.sort();
Ok(nodes)
}
fn open(&mut self, path: &Path) -> io::Result<EventNode> {
let file = OpenOptions::new()
.read(true)
.custom_flags(O_NONBLOCK)
.open(path)?;
Ok(EventNode { file })
}
}
#[derive(Debug)]
struct EventNode {
file: File,
}
impl Device for EventNode {
fn probe(&mut self) -> io::Result<Probe> {
let mut probe = Probe {
id: InputId::default(),
keys: KeyBits::EMPTY,
abs: AbsBits::EMPTY,
held: KeyBits::EMPTY,
axes: [AbsInfo::default(); ABS_CNT],
};
read_ioctl(&self.file, EVIOCGID_NR, &mut probe.id)?;
read_ioctl(&self.file, eviocgbit_nr(EV_KEY), &mut probe.keys.0)?;
read_ioctl(&self.file, eviocgbit_nr(EV_ABS), &mut probe.abs.0)?;
read_ioctl(&self.file, EVIOCGKEY_NR, &mut probe.held.0)?;
for code in (0..ABS_CNT as u16).filter(|&code| probe.abs.has(code)) {
read_ioctl(
&self.file,
eviocgabs_nr(code),
&mut probe.axes[usize::from(code)],
)?;
}
Ok(probe)
}
fn read(&mut self, events: &mut Vec<InputEvent>) -> io::Result<()> {
let mut buffer = [0; READ_BATCH * EVENT_SIZE];
loop {
match self.file.read(&mut buffer) {
Ok(read) => {
events.extend(decode_events(&buffer[..read])?);
if read < buffer.len() {
return Ok(());
}
}
Err(error) if error.kind() == io::ErrorKind::WouldBlock => return Ok(()),
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(error) => return Err(error),
}
}
}
}
#[derive(Debug)]
pub struct Evdev {
source: InputDir,
poller: Poller<EventNode>,
}
impl Evdev {
#[must_use]
pub fn new() -> Self {
Self {
source: InputDir {
dir: PathBuf::from(INPUT_DIR),
},
poller: Poller::default(),
}
}
pub fn poll(&mut self, mut emit: impl FnMut(GamepadEvent)) -> Result<(), EvdevError> {
self.poller
.poll(&mut self.source, Instant::now(), &mut emit)
}
}
impl Default for Evdev {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::evdev::ffi::{ABS_Y, EV_SYN, SYN_REPORT};
struct Scratch {
dir: PathBuf,
files: Vec<PathBuf>,
}
impl Scratch {
fn new(name: &str) -> Self {
let dir =
std::env::temp_dir().join(format!("crcbl-evdev-{name}-{}", std::process::id()));
fs::create_dir(&dir).expect("a fresh scratch dir");
Self {
dir,
files: Vec::new(),
}
}
fn file(&mut self, name: &str, bytes: &[u8]) -> PathBuf {
let path = self.dir.join(name);
fs::write(&path, bytes).expect("scratch file written");
self.files.push(path.clone());
path
}
}
impl Drop for Scratch {
fn drop(&mut self) {
for file in &self.files {
fs::remove_file(file).expect("scratch file removed");
}
fs::remove_dir(&self.dir).expect("scratch dir removed");
}
}
#[test]
fn a_scan_lists_only_event_nodes() {
let mut scratch = Scratch::new("scan");
let event1 = scratch.file("event1", b"");
scratch.file("js0", b"");
scratch.file("mouse0", b"");
let event0 = scratch.file("event0", b"");
let mut source = InputDir {
dir: scratch.dir.clone(),
};
let nodes = source.scan().expect("a readable dir");
let paths: Vec<_> = nodes.iter().map(|node| node.path.clone()).collect();
assert_eq!(paths, [event0, event1]);
assert_ne!(nodes[0].ino, nodes[1].ino);
}
#[test]
fn a_non_evdev_node_fails_its_probe_with_enotty() {
const ENOTTY: i32 = 25;
let mut scratch = Scratch::new("probe");
let path = scratch.file("event0", b"");
let mut source = InputDir {
dir: scratch.dir.clone(),
};
let mut node = source.open(&path).expect("a regular file opens");
let error = node.probe().expect_err("a regular file has no EVIOCGID");
assert_eq!(error.raw_os_error(), Some(ENOTTY), "{error}");
let mut events = Vec::new();
let result = Poller::default().poll(&mut source, Instant::now(), &mut |event| {
events.push(event);
});
assert!(
matches!(&result, Err(EvdevError::Open { path: failed, .. }) if *failed == path),
"{result:?}"
);
assert!(events.is_empty());
}
#[test]
fn a_read_decodes_whole_events() {
let mut bytes = Vec::new();
for event in [
InputEvent::new(EV_ABS, ABS_Y, 32767),
InputEvent::new(EV_SYN, SYN_REPORT, 0),
] {
bytes.extend_from_slice(&[0; 2 * size_of::<KernelUlong>()]);
bytes.extend_from_slice(&event.kind.to_ne_bytes());
bytes.extend_from_slice(&event.code.to_ne_bytes());
bytes.extend_from_slice(&event.value.to_ne_bytes());
}
let mut scratch = Scratch::new("read");
let path = scratch.file("event0", &bytes);
let mut node = InputDir {
dir: scratch.dir.clone(),
}
.open(&path)
.expect("a regular file opens");
let mut events = Vec::new();
node.read(&mut events).expect("whole events");
assert_eq!(
events,
[
InputEvent::new(EV_ABS, ABS_Y, 32767),
InputEvent::new(EV_SYN, SYN_REPORT, 0),
]
);
}
}