diff options
| -rw-r--r-- | cmds/evemu-record/Android.bp | 13 | ||||
| -rw-r--r-- | cmds/evemu-record/OWNERS | 1 | ||||
| -rw-r--r-- | cmds/evemu-record/evdev.rs | 299 | ||||
| -rw-r--r-- | cmds/evemu-record/main.rs | 193 |
4 files changed, 506 insertions, 0 deletions
diff --git a/cmds/evemu-record/Android.bp b/cmds/evemu-record/Android.bp new file mode 100644 index 0000000000..1edacec384 --- /dev/null +++ b/cmds/evemu-record/Android.bp @@ -0,0 +1,13 @@ +package { + default_applicable_licenses: ["frameworks_native_license"], +} + +rust_binary { + name: "evemu-record", + srcs: ["main.rs"], + rustlibs: [ + "libclap", + "liblibc", + "libnix", + ], +} diff --git a/cmds/evemu-record/OWNERS b/cmds/evemu-record/OWNERS new file mode 100644 index 0000000000..c88bfe97ca --- /dev/null +++ b/cmds/evemu-record/OWNERS @@ -0,0 +1 @@ +include platform/frameworks/base:/INPUT_OWNERS diff --git a/cmds/evemu-record/evdev.rs b/cmds/evemu-record/evdev.rs new file mode 100644 index 0000000000..35feea1930 --- /dev/null +++ b/cmds/evemu-record/evdev.rs @@ -0,0 +1,299 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! Wrappers for the Linux evdev APIs. + +use std::fs::File; +use std::io; +use std::mem; +use std::os::fd::{AsRawFd, OwnedFd}; +use std::path::Path; + +use libc::c_int; +use nix::sys::time::TimeVal; + +pub const SYN_CNT: usize = 0x10; +pub const KEY_CNT: usize = 0x300; +pub const REL_CNT: usize = 0x10; +pub const ABS_CNT: usize = 0x40; +pub const MSC_CNT: usize = 0x08; +pub const SW_CNT: usize = 0x11; +pub const LED_CNT: usize = 0x10; +pub const SND_CNT: usize = 0x08; +pub const REP_CNT: usize = 0x02; + +// Disable naming warnings, as these are supposed to match the EV_ constants in input-event-codes.h. +#[allow(non_camel_case_types)] +// Some of these types aren't referenced for evemu purposes, but are included for completeness. +#[allow(dead_code)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u16)] +pub enum EventType { + SYN = 0x00, + KEY = 0x01, + REL = 0x02, + ABS = 0x03, + MSC = 0x04, + SW = 0x05, + LED = 0x11, + SND = 0x12, + REP = 0x14, + FF = 0x15, + PWR = 0x16, + FF_STATUS = 0x17, +} + +impl EventType { + fn code_count(&self) -> usize { + match self { + EventType::SYN => SYN_CNT, + EventType::KEY => KEY_CNT, + EventType::REL => REL_CNT, + EventType::ABS => ABS_CNT, + EventType::MSC => MSC_CNT, + EventType::SW => SW_CNT, + EventType::LED => LED_CNT, + EventType::SND => SND_CNT, + EventType::REP => REP_CNT, + _ => { + panic!("Event type {self:?} does not have a defined code count."); + } + } + } +} + +pub const EVENT_TYPES_WITH_BITMAPS: [EventType; 7] = [ + EventType::KEY, + EventType::REL, + EventType::ABS, + EventType::MSC, + EventType::SW, + EventType::LED, + EventType::SND, +]; + +const INPUT_PROP_CNT: usize = 32; + +/// The `ioctl_*!` macros create public functions by default, so this module makes them private. +mod ioctl { + use nix::{ioctl_read, ioctl_read_buf}; + + ioctl_read!(eviocgid, b'E', 0x02, super::DeviceId); + ioctl_read_buf!(eviocgname, b'E', 0x06, u8); + ioctl_read_buf!(eviocgprop, b'E', 0x09, u8); +} + +#[derive(Default)] +#[repr(C)] +pub struct DeviceId { + pub bus_type: u16, + pub vendor: u16, + pub product: u16, + pub version: u16, +} + +#[derive(Default)] +#[repr(C)] +pub struct AbsoluteAxisInfo { + pub value: i32, + pub minimum: i32, + pub maximum: i32, + pub fuzz: i32, + pub flat: i32, + pub resolution: i32, +} + +#[repr(C)] +pub struct InputEvent { + pub time: TimeVal, + pub type_: u16, + pub code: u16, + pub value: i32, +} + +impl InputEvent { + pub fn offset_time_by(&self, offset: TimeVal) -> InputEvent { + InputEvent { time: self.time - offset, ..*self } + } +} + +impl Default for InputEvent { + fn default() -> Self { + InputEvent { time: TimeVal::new(0, 0), type_: 0, code: 0, value: 0 } + } +} + +/// An object representing an input device using Linux's evdev protocol. +pub struct Device { + fd: OwnedFd, +} + +/// # Safety +/// +/// `ioctl` must be safe to call with the given file descriptor and a pointer to a buffer of +/// `initial_buf_size` `u8`s. +unsafe fn buf_from_ioctl( + ioctl: unsafe fn(c_int, &mut [u8]) -> nix::Result<c_int>, + fd: &OwnedFd, + initial_buf_size: usize, +) -> Result<Vec<u8>, nix::errno::Errno> { + let mut buf = vec![0; initial_buf_size]; + // SAFETY: + // Here we're relying on the safety guarantees for `ioctl` made by the caller. + match unsafe { ioctl(fd.as_raw_fd(), buf.as_mut_slice()) } { + Ok(len) if len < 0 => { + panic!("ioctl returned invalid length {len}"); + } + Ok(len) => { + buf.truncate(len as usize); + Ok(buf) + } + Err(err) => Err(err), + } +} + +impl Device { + /// Opens a device from the evdev node at the given path. + pub fn open(path: &Path) -> io::Result<Device> { + Ok(Device { fd: OwnedFd::from(File::open(path)?) }) + } + + /// Returns the name of the device, as set by the relevant kernel driver. + pub fn name(&self) -> Result<String, nix::errno::Errno> { + // There's no official maximum length for evdev device names. The Linux HID driver + // currently supports names of at most 151 bytes (128 from the device plus a suffix of up + // to 23 bytes). 256 seems to be the buffer size most commonly used in evdev bindings, so + // we use it here. + // + // SAFETY: + // We know that fd is a valid file descriptor as it comes from a File that we have open. + // + // The ioctl_read_buf macro prevents the retrieved data from overflowing the buffer created + // by buf_from_ioctl by passing in the size to the ioctl, meaning that the kernel's + // str_to_user function will truncate the string to that length. + let mut buf = unsafe { buf_from_ioctl(ioctl::eviocgname, &self.fd, 256)? }; + assert!(!buf.is_empty(), "buf is too short for an empty null-terminated string"); + assert_eq!(buf.pop().unwrap(), 0, "buf is not a null-terminated string"); + Ok(String::from_utf8_lossy(buf.as_slice()).into_owned()) + } + + pub fn ids(&self) -> Result<DeviceId, nix::errno::Errno> { + let mut ids = DeviceId::default(); + // SAFETY: + // We know that fd is a valid file descriptor as it comes from a File that we have open. + // + // We know that the pointer to ids is valid because we just allocated it. + unsafe { ioctl::eviocgid(self.fd.as_raw_fd(), &mut ids) }.map(|_| ids) + } + + pub fn properties_bitmap(&self) -> Result<Vec<u8>, nix::errno::Errno> { + let buf_size = (INPUT_PROP_CNT + 7) / 8; + // SAFETY: + // We know that fd is a valid file descriptor as it comes from a File that we have open. + // + // The ioctl_read_buf macro prevents the retrieved data from overflowing the buffer created + // by buf_from_ioctl by passing in the size to the ioctl, meaning that the kernel's + // str_to_user function will truncate the string to that length. + unsafe { buf_from_ioctl(ioctl::eviocgprop, &self.fd, buf_size) } + } + + pub fn bitmap_for_event_type(&self, event_type: EventType) -> nix::Result<Vec<u8>> { + let buf_size = (event_type.code_count() + 7) / 8; + let mut buf = vec![0; buf_size]; + + // The EVIOCGBIT ioctl can't be bound using ioctl_read_buf! like the others, since it uses + // part of its ioctl code as an additional parameter, for the event type. Hence this unsafe + // block is a manual expansion of ioctl_read_buf!. + // + // SAFETY: + // We know that fd is a valid file descriptor as it comes from a File that we have open. + // + // We prevent the retrieved data from overflowing buf by passing in the size of buf to the + // ioctl, meaning that the kernel's str_to_user function will truncate the string to that + // length. We also panic if the ioctl returns a length longer than buf, hopefully before the + // overflow can do any damage. + match nix::errno::Errno::result(unsafe { + libc::ioctl( + self.fd.as_raw_fd(), + nix::request_code_read!(b'E', 0x20 + event_type as u16, buf.len()) + as nix::sys::ioctl::ioctl_num_type, + buf.as_mut_ptr(), + ) + }) { + Ok(len) if len < 0 => { + panic!("EVIOCGBIT returned invalid length {len} for event type {event_type:?}"); + } + Ok(len) => { + buf.truncate(len as usize); + Ok(buf) + } + Err(err) => Err(err), + } + } + + pub fn supported_axes_of_type(&self, event_type: EventType) -> nix::Result<Vec<u16>> { + let mut axes = Vec::new(); + for (i, byte_ref) in self.bitmap_for_event_type(event_type)?.iter().enumerate() { + let mut byte = *byte_ref; + for j in 0..8 { + if byte & 1 == 1 { + axes.push((i * 8 + j) as u16); + } + byte >>= 1; + } + } + Ok(axes) + } + + pub fn absolute_axis_info(&self, axis: u16) -> nix::Result<AbsoluteAxisInfo> { + let mut info = AbsoluteAxisInfo::default(); + // The EVIOCGABS ioctl can't be bound using ioctl_read! since it uses part of its ioctl code + // as an additional parameter, for the axis code. Hence this unsafe block is a manual + // expansion of ioctl_read!. + // + // SAFETY: + // We know that fd is a valid file descriptor as it comes from a File that we have open. + // + // We know that the pointer to info is valid because we just allocated it. + nix::errno::Errno::result(unsafe { + nix::libc::ioctl( + self.fd.as_raw_fd(), + nix::request_code_read!(b'E', 0x40 + axis, mem::size_of::<AbsoluteAxisInfo>()), + &mut info, + ) + }) + .map(|_| info) + } + + pub fn read_event(&self) -> nix::Result<InputEvent> { + let mut event = InputEvent::default(); + + // SAFETY: + // We know that fd is a valid file descriptor as it comes from a File that we have open. + // + // We know that the pointer to event is valid because we just allocated it, and that the + // data structures match up because InputEvent is repr(C) and all its members are repr(C) + // or primitives that support all representations without niches. + nix::errno::Errno::result(unsafe { + libc::read( + self.fd.as_raw_fd(), + &mut event as *mut _ as *mut std::ffi::c_void, + mem::size_of::<InputEvent>(), + ) + }) + .map(|_| event) + } +} diff --git a/cmds/evemu-record/main.rs b/cmds/evemu-record/main.rs new file mode 100644 index 0000000000..6f5deb96b2 --- /dev/null +++ b/cmds/evemu-record/main.rs @@ -0,0 +1,193 @@ +/* + * Copyright 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +//! A Rust implementation of the evemu-record command from the [FreeDesktop evemu suite][evemu] of +//! tools. +//! +//! [evemu]: https://gitlab.freedesktop.org/libevdev/evemu + +use std::cmp; +use std::error::Error; +use std::fs; +use std::io; +use std::io::{BufRead, Write}; +use std::path::PathBuf; + +use clap::Parser; +use nix::sys::time::TimeVal; + +mod evdev; + +/// Records evdev events from an input device in a format compatible with the FreeDesktop evemu +/// library. +#[derive(Parser, Debug)] +struct Args { + /// The path to the input device to record. If omitted, offers a list of devices to choose from. + device: Option<PathBuf>, + /// The file to save the recording to. Defaults to standard output. + output_file: Option<PathBuf>, +} + +fn get_choice(max: u32) -> u32 { + fn read_u32() -> Result<u32, std::num::ParseIntError> { + io::stdin().lock().lines().next().unwrap().unwrap().parse::<u32>() + } + let mut choice = read_u32(); + while choice.is_err() || choice.clone().unwrap() > max { + eprint!("Enter a number between 0 and {max} inclusive: "); + choice = read_u32(); + } + choice.unwrap() +} + +fn pick_input_device() -> Result<PathBuf, io::Error> { + eprintln!("Available devices:"); + let mut entries = + fs::read_dir("/dev/input")?.filter_map(|entry| entry.ok()).collect::<Vec<_>>(); + entries.sort_by_key(|entry| entry.path()); + let mut highest_number = 0; + for entry in entries { + let path = entry.path(); + let file_name = path.file_name().unwrap().to_str().unwrap(); + if path.is_dir() || !file_name.starts_with("event") { + continue; + } + let number = file_name.strip_prefix("event").unwrap().parse::<u32>(); + if number.is_err() { + continue; + } + let number = number.unwrap(); + match evdev::Device::open(path.as_path()) { + Ok(dev) => { + highest_number = cmp::max(highest_number, number); + eprintln!( + "{}:\t{}", + path.display(), + dev.name().unwrap_or("[could not read name]".to_string()), + ); + } + Err(_) => { + eprintln!("Couldn't open {}", path.display()); + } + } + } + eprint!("Select the device event number [0-{highest_number}]: "); + let choice = get_choice(highest_number); + Ok(PathBuf::from(format!("/dev/input/event{choice}"))) +} + +fn print_device_description( + device: &evdev::Device, + output: &mut impl Write, +) -> Result<(), Box<dyn Error>> { + // TODO(b/302297266): report LED and SW states, then bump the version to EVEMU 1.3. + writeln!(output, "# EVEMU 1.2")?; + writeln!(output, "N: {}", device.name()?)?; + + let ids = device.ids()?; + writeln!( + output, + "I: {:04x} {:04x} {:04x} {:04x}", + ids.bus_type, ids.vendor, ids.product, ids.version, + )?; + + fn print_in_8_byte_chunks( + output: &mut impl Write, + prefix: &str, + data: &Vec<u8>, + ) -> Result<(), io::Error> { + for (i, byte) in data.iter().enumerate() { + if i % 8 == 0 { + write!(output, "{prefix}")?; + } + write!(output, " {:02x}", byte)?; + if (i + 1) % 8 == 0 { + writeln!(output)?; + } + } + if data.len() % 8 != 0 { + for _ in (data.len() % 8)..8 { + write!(output, " 00")?; + } + writeln!(output)?; + } + Ok(()) + } + + let props = device.properties_bitmap()?; + print_in_8_byte_chunks(output, "P:", &props)?; + + // The SYN event type can't be queried through the EVIOCGBIT ioctl, so just hard-code it to + // SYN_REPORT, SYN_CONFIG, and SYN_DROPPED. + writeln!(output, "B: 00 0b 00 00 00 00 00 00 00")?; + for event_type in evdev::EVENT_TYPES_WITH_BITMAPS { + let bits = device.bitmap_for_event_type(event_type)?; + print_in_8_byte_chunks(output, format!("B: {:02x}", event_type as u16).as_str(), &bits)?; + } + + for axis in device.supported_axes_of_type(evdev::EventType::ABS)? { + let info = device.absolute_axis_info(axis)?; + writeln!( + output, + "A: {axis:02x} {} {} {} {} {}", + info.minimum, info.maximum, info.fuzz, info.flat, info.resolution + )?; + } + Ok(()) +} + +fn print_events(device: &evdev::Device, output: &mut impl Write) -> Result<(), Box<dyn Error>> { + fn print_event(output: &mut impl Write, event: &evdev::InputEvent) -> Result<(), io::Error> { + // TODO(b/302297266): Translate events into human-readable names and add those as comments. + writeln!( + output, + "E: {}.{:06} {:04x} {:04x} {:04}", + event.time.tv_sec(), + event.time.tv_usec(), + event.type_, + event.code, + event.value, + )?; + Ok(()) + } + let event = device.read_event()?; + // Due to a bug in the C implementation of evemu-play [0] that has since become part of the API, + // the timestamp of the first event in a recording shouldn't be exactly 0.0 seconds, so offset + // it by 1µs. + // + // [0]: https://gitlab.freedesktop.org/libevdev/evemu/-/commit/eba96a4d2be7260b5843e65c4b99c8b06a1f4c9d + let start_time = event.time - TimeVal::new(0, 1); + print_event(output, &event.offset_time_by(start_time))?; + loop { + let event = device.read_event()?; + print_event(output, &event.offset_time_by(start_time))?; + } +} + +fn main() -> Result<(), Box<dyn Error>> { + let args = Args::parse(); + + let device_path = args.device.unwrap_or_else(|| pick_input_device().unwrap()); + + let device = evdev::Device::open(device_path.as_path())?; + let mut output = match args.output_file { + Some(path) => Box::new(fs::File::create(path)?) as Box<dyn Write>, + None => Box::new(io::stdout().lock()), + }; + print_device_description(&device, &mut output)?; + print_events(&device, &mut output)?; + Ok(()) +} |