From 9070c4e8360b7b3c3e17f12cfd685874b97311dc Mon Sep 17 00:00:00 2001 From: Johannes Leupolz Date: Thu, 26 Mar 2026 21:19:40 +0000 Subject: [PATCH] First draft of the test tool test-scenarios that should make it easier to test new scenarios in case we have an issue. --- vuinputd-tests/Cargo.toml | 4 + vuinputd-tests/src/bin/test-scenarios.rs | 77 ++++ vuinputd-tests/src/devices/keyboard.rs | 342 ++++++++++++++++++ vuinputd-tests/src/devices/mod.rs | 38 ++ vuinputd-tests/src/devices/mouse.rs | 114 ++++++ vuinputd-tests/src/devices/utils.rs | 143 ++++++++ vuinputd-tests/src/ipc.rs | 2 + vuinputd-tests/src/lib.rs | 2 + .../src/scenarios/basic_keyboard.rs | 40 ++ vuinputd-tests/src/scenarios/basic_mouse.rs | 40 ++ .../src/scenarios/basic_ps4_gamepad.rs | 40 ++ .../src/scenarios/basic_xbox_gamepad.rs | 41 +++ vuinputd-tests/src/scenarios/mod.rs | 33 ++ 13 files changed, 916 insertions(+) create mode 100644 vuinputd-tests/src/bin/test-scenarios.rs create mode 100644 vuinputd-tests/src/devices/keyboard.rs create mode 100644 vuinputd-tests/src/devices/mod.rs create mode 100644 vuinputd-tests/src/devices/mouse.rs create mode 100644 vuinputd-tests/src/devices/utils.rs create mode 100644 vuinputd-tests/src/scenarios/basic_keyboard.rs create mode 100644 vuinputd-tests/src/scenarios/basic_mouse.rs create mode 100644 vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs create mode 100644 vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs create mode 100644 vuinputd-tests/src/scenarios/mod.rs diff --git a/vuinputd-tests/Cargo.toml b/vuinputd-tests/Cargo.toml index 5352278..d53c2cb 100644 --- a/vuinputd-tests/Cargo.toml +++ b/vuinputd-tests/Cargo.toml @@ -12,6 +12,9 @@ name = "test-keyboard" [[bin]] name = "test-ok" +[[bin]] +name = "test-scenarios" + [dependencies] uinput-ioctls = { path = "../uinput-ioctls" } nix = { version = "0.30", features = ["ioctl","socket","signal"] } # ioctl & libc bindings @@ -24,6 +27,7 @@ serde_json = "1.0" [features] requires-privileges = [] +requires-rootless = [] requires-uinput = [] requires-bwrap = [] requires-podman = [] \ No newline at end of file diff --git a/vuinputd-tests/src/bin/test-scenarios.rs b/vuinputd-tests/src/bin/test-scenarios.rs new file mode 100644 index 0000000..e9e41a7 --- /dev/null +++ b/vuinputd-tests/src/bin/test-scenarios.rs @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use clap::{Parser, Subcommand}; +use vuinputd_tests::scenarios::{ + ScenarioArgs, basic_keyboard::BasicKeyboard, basic_mouse::BasicMouse, basic_ps4_gamepad::BasicPs4Gamepad, basic_xbox_gamepad::BasicXboxGamepad/* + reuse_keyboard::ReuseKeyboard, reuse_xbox_gamepad::ReuseXboxGamepad, + ScenarioArgs, stress_keyboard::StressKeyboard, stress_xbox_gamepad::StressXboxGamepad, */ +}; + +#[derive(Parser)] +#[command(name = "test-scenarios")] +#[command(about = "Test scenarios for vuinputd", long_about = None)] +struct Cli { + /// Run scenarios in IPC mode (communicate with vuinputd daemon) + #[arg(short, long, default_value_t = false)] + ipc: bool, + + /// Path to uinput device + #[arg(short, long, default_value = "/dev/uinput")] + dev_path: String, + + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Basic keyboard test + BasicKeyboard, + + /// Basic mouse test + BasicMouse, + + /// Basic PS4 gamepad test + BasicPs4Gamepad, + + /// Basic Xbox gamepad test + BasicXboxGamepad, + + /* + /// Reuse keyboard test (create, destroy, recreate) + ReuseKeyboard, + + /// Reuse Xbox gamepad test (create, destroy, recreate) + ReuseXboxGamepad, + + /// Stress test for keyboard (3000 events) + StressKeyboard, + + /// Stress test for Xbox gamepad (3000 events) + StressXboxGamepad, + */ +} + +fn main() -> Result<(), std::io::Error> { + let cli = Cli::parse(); + + let args = ScenarioArgs { + ipc: cli.ipc, + dev_path: Some(cli.dev_path), + }; + + match cli.command { + Commands::BasicKeyboard => BasicKeyboard::run(&args), + Commands::BasicMouse => BasicMouse::run(&args), + Commands::BasicPs4Gamepad => BasicPs4Gamepad::run(&args), + Commands::BasicXboxGamepad => BasicXboxGamepad::run(&args), + /* + Commands::ReuseKeyboard => ReuseKeyboard::run(&args), + Commands::ReuseXboxGamepad => ReuseXboxGamepad::run(&args), + Commands::StressKeyboard => StressKeyboard::run(&args), + Commands::StressXboxGamepad => StressXboxGamepad::run(&args), + */ + } +} diff --git a/vuinputd-tests/src/devices/keyboard.rs b/vuinputd-tests/src/devices/keyboard.rs new file mode 100644 index 0000000..bb384da --- /dev/null +++ b/vuinputd-tests/src/devices/keyboard.rs @@ -0,0 +1,342 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use super::Device; +use std::{ffi::CStr, fs::File}; +use std::io; +use uinput_ioctls::*; + +/// Key codes. Those are used by udev to recognize a device as a keyboard. +pub const KEY_ESC: u16 = 1; +pub const KEY_1: u16 = 2; +pub const KEY_2: u16 = 3; +pub const KEY_3: u16 = 4; +pub const KEY_4: u16 = 5; +pub const KEY_5: u16 = 6; +pub const KEY_6: u16 = 7; +pub const KEY_7: u16 = 8; +pub const KEY_8: u16 = 9; +pub const KEY_9: u16 = 10; +pub const KEY_0: u16 = 11; +pub const KEY_MINUS: u16 = 12; +pub const KEY_EQUAL: u16 = 13; +pub const KEY_BACKSPACE: u16 = 14; +pub const KEY_TAB: u16 = 15; +pub const KEY_Q: u16 = 16; +pub const KEY_W: u16 = 17; +pub const KEY_E: u16 = 18; +pub const KEY_R: u16 = 19; +pub const KEY_T: u16 = 20; +pub const KEY_Y: u16 = 21; +pub const KEY_U: u16 = 22; +pub const KEY_I: u16 = 23; +pub const KEY_O: u16 = 24; +pub const KEY_P: u16 = 25; +pub const KEY_LEFTBRACE: u16 = 26; +pub const KEY_RIGHTBRACE: u16 = 27; +pub const KEY_ENTER: u16 = 28; +pub const KEY_LEFTCTRL: u16 = 29; +pub const KEY_A: u16 = 30; +pub const KEY_S: u16 = 31; + +/// Space and other common keys +pub const KEY_D: u16 = 32; +pub const KEY_F: u16 = 33; +pub const KEY_G: u16 = 34; +pub const KEY_H: u16 = 35; +pub const KEY_J: u16 = 36; +pub const KEY_K: u16 = 37; +pub const KEY_L: u16 = 38; +pub const KEY_SEMICOLON: u16 = 39; +pub const KEY_APOSTROPHE: u16 = 40; +pub const KEY_GRAVE: u16 = 41; +pub const KEY_LEFTSHIFT: u16 = 42; +pub const KEY_BACKSLASH: u16 = 43; +pub const KEY_Z: u16 = 44; +pub const KEY_X: u16 = 45; +pub const KEY_C: u16 = 46; +pub const KEY_V: u16 = 47; +pub const KEY_B: u16 = 48; +pub const KEY_N: u16 = 49; +pub const KEY_M: u16 = 50; +pub const KEY_COMMA: u16 = 51; +pub const KEY_DOT: u16 = 52; +pub const KEY_SLASH: u16 = 53; +pub const KEY_RIGHTSHIFT: u16 = 54; +pub const KEY_KPASTERISK: u16 = 55; +pub const KEY_LEFTALT: u16 = 56; +pub const KEY_SPACE: u16 = 57; +pub const KEY_CAPSLOCK: u16 = 58; + +/// Function keys +pub const KEY_F1: u16 = 59; +pub const KEY_F2: u16 = 60; +pub const KEY_F3: u16 = 61; +pub const KEY_F4: u16 = 62; +pub const KEY_F5: u16 = 63; +pub const KEY_F6: u16 = 64; +pub const KEY_F7: u16 = 65; +pub const KEY_F8: u16 = 66; +pub const KEY_F9: u16 = 67; +pub const KEY_F10: u16 = 68; +pub const KEY_NUMLOCK: u16 = 69; +pub const KEY_SCROLLLOCK: u16 = 70; +pub const KEY_KP7: u16 = 71; +pub const KEY_KP8: u16 = 72; +pub const KEY_KP9: u16 = 73; +pub const KEY_KPMINUS: u16 = 74; +pub const KEY_KP4: u16 = 75; +pub const KEY_KP5: u16 = 76; +pub const KEY_KP6: u16 = 77; +pub const KEY_KPPLUS: u16 = 78; +pub const KEY_KP1: u16 = 79; +pub const KEY_KP2: u16 = 80; +pub const KEY_KP3: u16 = 81; +pub const KEY_KP0: u16 = 82; +pub const KEY_KPDOT: u16 = 83; + +/// Arrow keys and navigation +pub const KEY_ZENKAKUHANKAKU: u16 = 85; +pub const KEY_102ND: u16 = 86; +pub const KEY_F11: u16 = 87; +pub const KEY_F12: u16 = 88; +pub const KEY_RO: u16 = 89; +pub const KEY_KATAKANA: u16 = 90; +pub const KEY_HIRAGANA: u16 = 91; +pub const KEY_HENKAN: u16 = 92; +pub const KEY_KATAKANAHIRAGANA: u16 = 93; +pub const KEY_MUHENKAN: u16 = 94; +pub const KEY_KPJPCOMMA: u16 = 95; +pub const KEY_KPENTER: u16 = 96; +pub const KEY_RIGHTCTRL: u16 = 97; +pub const KEY_KPSLASH: u16 = 98; +pub const KEY_SYSRQ: u16 = 99; +pub const KEY_RIGHTALT: u16 = 100; +pub const KEY_LINEFEED: u16 = 101; +pub const KEY_HOME: u16 = 102; +pub const KEY_UP: u16 = 103; +pub const KEY_PAGEUP: u16 = 104; +pub const KEY_LEFT: u16 = 105; +pub const KEY_RIGHT: u16 = 106; +pub const KEY_END: u16 = 107; +pub const KEY_DOWN: u16 = 108; +pub const KEY_PAGEDOWN: u16 = 109; +pub const KEY_INSERT: u16 = 110; +pub const KEY_DELETE: u16 = 111; + + +/// Configure a full 101-key standard keyboard +unsafe fn set_standard_keyboard_keys(fd: i32) -> Result<(), std::io::Error> { + // We need to set more bits so that systemd recognizes a keyboard as a keyboard. + // At least the first 32 bits are ESC, numbers, and Q to D, except KEY_RESERVED need to be considered. + // udev-builtin-input_id.c consideres the mask = 0xFFFFFFFE + + // EV_KEY + ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?; + + // All standard keys (1..101+) + let all_keys = [ + // Modifier + main keys + KEY_ESC, + KEY_1, + KEY_2, + KEY_3, + KEY_4, + KEY_5, + KEY_6, + KEY_7, + KEY_8, + KEY_9, + KEY_0, + KEY_MINUS, + KEY_EQUAL, + KEY_BACKSPACE, + KEY_TAB, + KEY_Q, + KEY_W, + KEY_E, + KEY_R, + KEY_T, + KEY_Y, + KEY_U, + KEY_I, + KEY_O, + KEY_P, + KEY_LEFTBRACE, + KEY_RIGHTBRACE, + KEY_ENTER, + KEY_LEFTCTRL, + KEY_A, + KEY_S, + KEY_D, + KEY_F, + KEY_G, + KEY_H, + KEY_J, + KEY_K, + KEY_L, + KEY_SEMICOLON, + KEY_APOSTROPHE, + KEY_GRAVE, + KEY_LEFTSHIFT, + KEY_BACKSLASH, + KEY_Z, + KEY_X, + KEY_C, + KEY_V, + KEY_B, + KEY_N, + KEY_M, + KEY_COMMA, + KEY_DOT, + KEY_SLASH, + KEY_RIGHTSHIFT, + KEY_KPASTERISK, + KEY_LEFTALT, + KEY_SPACE, + KEY_CAPSLOCK, + // Function keys + KEY_F1, + KEY_F2, + KEY_F3, + KEY_F4, + KEY_F5, + KEY_F6, + KEY_F7, + KEY_F8, + KEY_F9, + KEY_F10, + KEY_F11, + KEY_F12, + KEY_NUMLOCK, + KEY_SCROLLLOCK, + // Keypad + KEY_KP7, + KEY_KP8, + KEY_KP9, + KEY_KPMINUS, + KEY_KP4, + KEY_KP5, + KEY_KP6, + KEY_KPPLUS, + KEY_KP1, + KEY_KP2, + KEY_KP3, + KEY_KP0, + KEY_KPDOT, + KEY_KPENTER, + KEY_KPSLASH, + KEY_KPJPCOMMA, + // Arrows / navigation + KEY_HOME, + KEY_UP, + KEY_PAGEUP, + KEY_LEFT, + KEY_RIGHT, + KEY_END, + KEY_DOWN, + KEY_PAGEDOWN, + KEY_INSERT, + KEY_DELETE, + KEY_RIGHTCTRL, + KEY_RIGHTALT, + // Optional Japanese / additional keys + KEY_ZENKAKUHANKAKU, + KEY_102ND, + KEY_RO, + KEY_KATAKANA, + KEY_HIRAGANA, + KEY_HENKAN, + KEY_KATAKANAHIRAGANA, + KEY_MUHENKAN, + KEY_LINEFEED, + KEY_SYSRQ, + ]; + + for &key in all_keys.iter() { + ui_set_keybit(fd, key.try_into().unwrap())?; + } + + Ok(()) +} + +pub struct KeyboardDevice; + +impl Device for KeyboardDevice { + fn name() -> &'static str { + "Keyboard" + } + + fn get_event_device(sysname: &str) -> Result { + super::utils::fetch_device_node(sysname) + .and_then(|devnode| File::open(&devnode)) + } + + fn setup(device:Option<&str>,name: &str) -> Result { + let fd = super::utils::open_uinput(device)?; + unsafe { set_standard_keyboard_keys(fd) }?; + + unsafe { + let mut usetup: libc::uinput_setup = std::mem::zeroed(); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xbeef; + usetup.id.product = 0xdead; + + let name_cstr = CString::new(name).unwrap(); + let name_ptr = usetup.name.as_mut_ptr() as *mut c_char; + std::ptr::copy_nonoverlapping( + name_cstr.as_ptr(), + name_ptr, + name_cstr.to_bytes_with_nul().len(), + ); + + let usetup_ptr = &mut usetup as *mut libc::uinput_setup; + ui_dev_setup(fd, usetup_ptr).map_err(|e| { + eprintln!("ui_dev_setup failed: {:?}", e); + e + })?; + } + + Ok(fd) + } + + fn create(fd: i32) -> Result { + unsafe { + ui_dev_create(fd).map_err(|e| { + eprintln!("ui_dev_create failed: {:?}", e); + e + })?; + + let mut resultbuf: [c_char; 64] = [0; 64]; + ui_get_sysname(fd, resultbuf.as_mut_slice()).map_err(|e| { + eprintln!("ui_get_sysname failed: {:?}", e); + e + })?; + + let sysname = format!( + "{}{}", + SYS_INPUT_DIR, + CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy() + ); + + Ok(sysname) + } + } + + fn destroy(fd: i32) { + unsafe { + ui_dev_destroy(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_destroy failed: {:?}", e); + std::process::exit(1); + }); + close(fd); + } + } +} + +use libc::{c_char, close}; +use std::ffi::CString; + +const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/"; +const BUS_USB: u16 = 0x03; diff --git a/vuinputd-tests/src/devices/mod.rs b/vuinputd-tests/src/devices/mod.rs new file mode 100644 index 0000000..43ffd5c --- /dev/null +++ b/vuinputd-tests/src/devices/mod.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::fs::File; +use std::io; + +pub mod keyboard; +pub mod mouse; +pub mod ps4_gamepad; +pub mod xbox_gamepad; +pub mod utils; + +pub use keyboard::KeyboardDevice; +pub use mouse::MouseDevice; +pub use ps4_gamepad::Ps4GamepadDevice; +pub use xbox_gamepad::XboxGamepadDevice; + +// Constants (same numeric values as in linux headers) +pub const EV_SYN: u16 = 0x00; +pub const EV_KEY: u16 = 0x01; +pub const EV_REL: u16 = 0x02; +pub const EV_ABS: u16 = 0x03; + +/// Trait for input devices +pub trait Device { + fn name() -> &'static str; + fn get_event_device(sysname: &str) -> Result; + + /// Phase 1: open uinput, configure keys, call ui_dev_setup + fn setup(device:Option<&str>,name: &str) -> Result; + + /// Phase 2: call ui_dev_create, get sysname and devnode + fn create(fd: i32) -> Result; + + /// Phase 3: call ui_dev_destroy and close fd + fn destroy(fd: i32); +} \ No newline at end of file diff --git a/vuinputd-tests/src/devices/mouse.rs b/vuinputd-tests/src/devices/mouse.rs new file mode 100644 index 0000000..5861bd3 --- /dev/null +++ b/vuinputd-tests/src/devices/mouse.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use super::{ Device}; +use std::{ffi::CStr, fs::File}; +use std::io; +use libc::c_int; +use uinput_ioctls::*; + +// Mouse codes +pub const BTN_LEFT: u16 = 272; +pub const BTN_RIGHT: u16 = 273; +pub const BTN_MIDDLE: u16 = 274; +pub const REL_X: u16 = 0; +pub const REL_Y: u16 = 1; + +/// Setup mouse device +unsafe fn setup_mouse(fd: c_int) -> io::Result<()> { + // EV_SYN + ui_set_evbit(fd, super::EV_SYN.try_into().unwrap())?; + // EV_KEY + ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?; + ui_set_keybit(fd, BTN_LEFT.try_into().unwrap())?; + ui_set_keybit(fd, BTN_RIGHT.try_into().unwrap())?; + ui_set_keybit(fd, BTN_MIDDLE.try_into().unwrap())?; + // EV_REL + ui_set_evbit(fd, super::EV_REL.try_into().unwrap())?; + ui_set_relbit(fd, REL_X.try_into().unwrap())?; + ui_set_relbit(fd, REL_Y.try_into().unwrap())?; + + Ok(()) +} + + +pub struct MouseDevice; + +impl Device for MouseDevice { + fn name() -> &'static str { + "Mouse" + } + + fn get_event_device(sysname: &str) -> Result { + super::utils::fetch_device_node(sysname) + .and_then(|devnode| File::open(&devnode)) + } + + fn setup(device: Option<&str>, name: &str) -> Result { + let fd = super::utils::open_uinput(device)?; + unsafe { setup_mouse(fd) }?; + + unsafe { + let mut usetup: libc::uinput_setup = std::mem::zeroed(); + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xbeef; + usetup.id.product = 0xdead; + + let name_cstr = CString::new(name).unwrap(); + let name_ptr = usetup.name.as_mut_ptr() as *mut c_char; + std::ptr::copy_nonoverlapping( + name_cstr.as_ptr(), + name_ptr, + name_cstr.to_bytes_with_nul().len(), + ); + + let usetup_ptr = &mut usetup as *mut libc::uinput_setup; + ui_dev_setup(fd, usetup_ptr).map_err(|e| { + eprintln!("ui_dev_setup failed: {:?}", e); + e + })?; + } + + Ok(fd) + } + + fn create(fd: i32) -> Result { + unsafe { + ui_dev_create(fd).map_err(|e| { + eprintln!("ui_dev_create failed: {:?}", e); + e + })?; + + let mut resultbuf: [c_char; 64] = [0; 64]; + ui_get_sysname(fd, resultbuf.as_mut_slice()).map_err(|e| { + eprintln!("ui_get_sysname failed: {:?}", e); + e + })?; + + let sysname = format!( + "{}{}", + SYS_INPUT_DIR, + CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy() + ); + + Ok(sysname) + } + } + + fn destroy(fd: i32) { + unsafe { + ui_dev_destroy(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_destroy failed: {:?}", e); + std::process::exit(1); + }); + close(fd); + } + } +} + +use libc::{c_char, close}; +use std::ffi::CString; + +const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/"; +const BUS_USB: u16 = 0x03; diff --git a/vuinputd-tests/src/devices/utils.rs b/vuinputd-tests/src/devices/utils.rs new file mode 100644 index 0000000..33234d8 --- /dev/null +++ b/vuinputd-tests/src/devices/utils.rs @@ -0,0 +1,143 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use libc::{c_int, close, open, write, O_NONBLOCK, O_WRONLY}; +use libc::{input_event, timespec, uinput_setup, CLOCK_MONOTONIC}; +use std::ffi::{CStr, CString}; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, ErrorKind}; +use std::mem::{self, size_of, zeroed}; +use std::os::fd::AsRawFd; +use std::os::raw::{c_char, c_void}; +use std::ptr; +pub use uinput_ioctls::*; +use crate::test_log::{LoggedInputEvent, TestLog}; + +// Constants (same numeric values as in linux headers) +const EV_SYN: u16 = 0x00; +const EV_KEY: u16 = 0x01; +const SYN_REPORT: u16 = 0; +const BUS_USB: u16 = 0x03; + + +pub fn emit(fd: c_int, ev_type: u16, code: u16, val: i32) -> io::Result<()> { + // libc's input_event struct layout: + // struct input_event { + // struct timeval time; + // __u16 type; + // __u16 code; + // __s32 value; + // }; + // + // libc provides input_event as `libc::input_event` on Linux. + let mut ie: libc::input_event = unsafe { zeroed() }; + + // time fields are ignored by kernel for synthetic events - set zero + ie.time.tv_sec = 0; + ie.time.tv_usec = 0; + + ie.type_ = ev_type; // note: in libc the field is `type_` + ie.code = code; + ie.value = val; + + // write the struct to the uinput fd + let buf_ptr = &ie as *const libc::input_event as *const c_void; + let bytes = size_of::(); + + //println!("write to {} {} {} {} ",fd,ev_type,code,val); + let written = unsafe { write(fd, buf_ptr, bytes) }; + //println!("written"); + if written as usize != bytes { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +// Note that before we can read, a SYN needs to be sent. Thus combine it. +pub fn emit_read_and_log( + emit_to: c_int, + read_from: &File, + ev_type: u16, + code: u16, + val: i32, +) -> io::Result { + let (time_sent_sec, time_sent_nsec) = monotonic_time(); + emit(emit_to, ev_type, code, val)?; + emit(emit_to, EV_SYN, SYN_REPORT, 0)?; + let input_event_recv = read_event(&read_from).unwrap(); + let _syn_recv = read_event(&read_from).unwrap(); + let (time_recv_sec, time_recv_nsec) = monotonic_time(); + let duration_usec = + (time_recv_sec - time_sent_sec) * 1_000_000 + (time_recv_nsec - time_sent_nsec) / 1000; + let send_and_receive_match = input_event_recv.type_ == ev_type + && input_event_recv.code == code + && input_event_recv.value == val; + + Ok(LoggedInputEvent { + tv_sec: time_sent_sec, + tv_nsec: time_sent_nsec, + duration_usec: duration_usec, + type_: ev_type, + code: code, + value: val, + send_and_receive_match: send_and_receive_match, + }) +} + +pub fn fetch_device_node(path: &str) -> io::Result { + println!("Read dir {}", &path); + for entry in fs::read_dir(path)? { + let entry = entry?; // propagate per-entry errors + if let Some(name) = entry.file_name().to_str() { + if name.starts_with("event") { + return Ok(format!("/dev/input/{}", name)); + } + } + } + // If no device is found, return an error + Err(io::Error::new(ErrorKind::NotFound, "no device found")) +} + +pub fn read_event(event_dev: &File) -> io::Result { + let mut ev: input_event = unsafe { mem::zeroed() }; + let ret = unsafe { + libc::read( + event_dev.as_raw_fd(), + &mut ev as *mut _ as *mut c_void, + mem::size_of::(), + ) + }; + if ret as usize != mem::size_of::() { + return Err(io::Error::last_os_error()); + } + Ok(ev) +} + +pub fn monotonic_time() -> (i64, i64) { + let mut ts = timespec { + tv_sec: 0, + tv_nsec: 0, + }; + + unsafe { + libc::clock_gettime(CLOCK_MONOTONIC, &mut ts); + } + (ts.tv_sec, ts.tv_nsec) +} + + +pub fn open_uinput(device:Option<&str>) -> io::Result { + let device = match device { + Some(dev_path) => dev_path, + _ => "/dev/uinput", + }; + + let path = CString::new(device).unwrap(); + let fd = unsafe { open(path.as_ptr(), O_WRONLY | O_NONBLOCK) }; + if fd < 0 { + eprintln!("error opening uinput"); + return Err(io::Error::last_os_error()); + } + Ok(fd) +} \ No newline at end of file diff --git a/vuinputd-tests/src/ipc.rs b/vuinputd-tests/src/ipc.rs index d7c7208..165fced 100644 --- a/vuinputd-tests/src/ipc.rs +++ b/vuinputd-tests/src/ipc.rs @@ -2,6 +2,8 @@ // // Author: Johannes Leupolz +// TODO: Use https://varlink.org/ which also supports bridges over ssh, which is nice + use std::{ io, os::{ diff --git a/vuinputd-tests/src/lib.rs b/vuinputd-tests/src/lib.rs index 65112a5..c3afdc5 100644 --- a/vuinputd-tests/src/lib.rs +++ b/vuinputd-tests/src/lib.rs @@ -2,6 +2,8 @@ // // Author: Johannes Leupolz +pub mod devices; +pub mod scenarios; pub mod bwrap; pub mod ipc; pub mod podman; diff --git a/vuinputd-tests/src/scenarios/basic_keyboard.rs b/vuinputd-tests/src/scenarios/basic_keyboard.rs new file mode 100644 index 0000000..149873f --- /dev/null +++ b/vuinputd-tests/src/scenarios/basic_keyboard.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::thread; +use std::time::Duration; + +use crate::devices::keyboard::KeyboardDevice; +use crate::scenarios::ScenarioArgs; +use crate::devices::{Device, utils}; +use crate::test_log::{TestLog}; + +const KEY_SPACE: u16 = 57; + +pub struct BasicKeyboard; + +impl BasicKeyboard { + pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> { + let device = args.dev_path.clone().unwrap_or_else(|| "/dev/uinput".to_string()); + let fd = KeyboardDevice::setup(Some(&device),"Example Keyboard")?; + let sysname = KeyboardDevice::create(fd)?; + eprintln!("sysname: {}", sysname); + + thread::sleep(Duration::from_secs(1)); + + let event_device = std::fs::OpenOptions::new() + .read(true) + .open(&utils::fetch_device_node(&sysname)?)?; + + let ev1 = utils::emit_read_and_log(fd, &event_device, 0x01, KEY_SPACE, 1)?; + let ev2 = utils::emit_read_and_log(fd, &event_device, 0x01, KEY_SPACE, 0)?; + + let eventlog = TestLog { events: vec![ev1, ev2] }; + let serialized = serde_json::to_string(&eventlog).unwrap(); + println!("Event log: {}", serialized); + + KeyboardDevice::destroy(fd); + Ok(()) + } +} \ No newline at end of file diff --git a/vuinputd-tests/src/scenarios/basic_mouse.rs b/vuinputd-tests/src/scenarios/basic_mouse.rs new file mode 100644 index 0000000..3a46d40 --- /dev/null +++ b/vuinputd-tests/src/scenarios/basic_mouse.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz +use std::thread; +use std::time::Duration; + +use crate::devices::mouse::MouseDevice; +use crate::scenarios::ScenarioArgs; +use crate::devices::{Device, utils}; +use crate::test_log::{LoggedInputEvent, TestLog}; + +const BTN_LEFT: u16 = 272; + +pub struct BasicMouse; + +impl BasicMouse { + pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> { + let device = args.dev_path.clone().unwrap_or_else(|| "/dev/uinput".to_string()); + + let fd = MouseDevice::setup(Some(&device), "Example Mouse")?; + let sysname = MouseDevice::create(fd)?; + eprintln!("sysname: {}", sysname); + + thread::sleep(Duration::from_secs(1)); + + let event_device = std::fs::OpenOptions::new() + .read(true) + .open(&utils::fetch_device_node(&sysname)?)?; + + let ev1 = utils::emit_read_and_log(fd, &event_device, 0x01, BTN_LEFT, 1)?; + let ev2 = utils::emit_read_and_log(fd, &event_device, 0x01, BTN_LEFT, 0)?; + + let eventlog = TestLog { events: vec![ev1, ev2] }; + let serialized = serde_json::to_string(&eventlog).unwrap(); + println!("Event log: {}", serialized); + + MouseDevice::destroy(fd); + Ok(()) + } +} diff --git a/vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs b/vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs new file mode 100644 index 0000000..8952dd3 --- /dev/null +++ b/vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz +use std::thread; +use std::time::Duration; + +use crate::devices::ps4_gamepad::Ps4GamepadDevice; +use crate::scenarios::ScenarioArgs; +use crate::devices::{Device, utils}; +use crate::test_log::{LoggedInputEvent, TestLog}; + +const BTN_SOUTH: u16 = 304; + +pub struct BasicPs4Gamepad; + +impl BasicPs4Gamepad { + pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> { + let device = args.dev_path.clone().unwrap_or_else(|| "/dev/uinput".to_string()); + + let fd = Ps4GamepadDevice::setup(Some(&device), "PS4 Gamepad")?; + let sysname = Ps4GamepadDevice::create(fd)?; + eprintln!("sysname: {}", sysname); + + thread::sleep(Duration::from_secs(1)); + + let event_device = std::fs::OpenOptions::new() + .read(true) + .open(&utils::fetch_device_node(&sysname)?)?; + + let ev1 = utils::emit_read_and_log(fd, &event_device, 0x01, BTN_SOUTH, 1)?; + let ev2 = utils::emit_read_and_log(fd, &event_device, 0x01, BTN_SOUTH, 0)?; + + let eventlog = TestLog { events: vec![ev1, ev2] }; + let serialized = serde_json::to_string(&eventlog).unwrap(); + println!("Event log: {}", serialized); + + Ps4GamepadDevice::destroy(fd); + Ok(()) + } +} \ No newline at end of file diff --git a/vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs b/vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs new file mode 100644 index 0000000..76dfb6e --- /dev/null +++ b/vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use std::thread; +use std::time::Duration; + +use crate::devices::xbox_gamepad::XboxGamepadDevice; +use crate::scenarios::ScenarioArgs; +use crate::devices::{Device, utils}; +use crate::test_log::{LoggedInputEvent, TestLog}; + +const BTN_A: u16 = 304; + +pub struct BasicXboxGamepad; + +impl BasicXboxGamepad { + pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> { + let device = args.dev_path.clone().unwrap_or_else(|| "/dev/uinput".to_string()); + + let fd = XboxGamepadDevice::setup(Some(&device), "Xbox Gamepad")?; + let sysname = XboxGamepadDevice::create(fd)?; + eprintln!("sysname: {}", sysname); + + thread::sleep(Duration::from_secs(1)); + + let event_device = std::fs::OpenOptions::new() + .read(true) + .open(&utils::fetch_device_node(&sysname)?)?; + + let ev1 = utils::emit_read_and_log(fd, &event_device, 0x01, BTN_A, 1)?; + let ev2 = utils::emit_read_and_log(fd, &event_device, 0x01, BTN_A, 0)?; + + let eventlog = TestLog { events: vec![ev1, ev2] }; + let serialized = serde_json::to_string(&eventlog).unwrap(); + println!("Event log: {}", serialized); + + XboxGamepadDevice::destroy(fd); + Ok(()) + } +} diff --git a/vuinputd-tests/src/scenarios/mod.rs b/vuinputd-tests/src/scenarios/mod.rs new file mode 100644 index 0000000..1e82bd8 --- /dev/null +++ b/vuinputd-tests/src/scenarios/mod.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +pub mod basic_keyboard; +pub mod basic_mouse; +pub mod basic_ps4_gamepad; +pub mod basic_xbox_gamepad; +/* +pub mod reuse_keyboard; +pub mod reuse_xbox_gamepad; +pub mod stress_keyboard; +pub mod stress_xbox_gamepad; +*/ + +// Re-exports for type checking +pub use basic_keyboard::BasicKeyboard; +pub use basic_mouse::BasicMouse; +pub use basic_ps4_gamepad::BasicPs4Gamepad; +pub use basic_xbox_gamepad::BasicXboxGamepad; +/* +pub use reuse_keyboard::ReuseKeyboard; +pub use reuse_xbox_gamepad::ReuseXboxGamepad; +pub use stress_keyboard::StressKeyboard; +pub use stress_xbox_gamepad::StressXboxGamepad; + */ + +/// Common scenario arguments passed from CLI +#[derive(Debug, Clone)] +pub struct ScenarioArgs { + pub ipc: bool, + pub dev_path: Option, +}