/* * 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, ValueEnum}; 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, /// The file to save the recording to. Defaults to standard output. output_file: Option, /// The base time that timestamps should be relative to (Android-specific extension) #[arg(long, value_enum, default_value_t = TimestampBase::FirstEvent)] timestamp_base: TimestampBase, } #[derive(Clone, Debug, ValueEnum)] enum TimestampBase { /// The first event received from the device. FirstEvent, /// The Unix epoch (00:00:00 UTC on 1st January 1970), so that all timestamps are Unix /// timestamps. This makes the events in the recording easier to match up with those from other /// log sources. Epoch, } fn get_choice(max: u32) -> u32 { fn read_u32() -> Result { io::stdin().lock().lines().next().unwrap().unwrap().parse::() } 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 { eprintln!("Available devices:"); let mut entries = fs::read_dir("/dev/input")?.filter_map(|entry| entry.ok()).collect::>(); 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::(); 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> { // 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: &[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, timestamp_base: TimestampBase, ) -> Result<(), Box> { 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()?; let start_time = match timestamp_base { // 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 TimestampBase::FirstEvent => event.time - TimeVal::new(0, 1), TimestampBase::Epoch => TimeVal::new(0, 0), }; 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> { 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, None => Box::new(io::stdout().lock()), }; print_device_description(&device, &mut output)?; print_events(&device, &mut output, args.timestamp_base)?; Ok(()) }