mirror of
https://github.com/joleuger/vuinputd.git
synced 2026-07-19 01:15:35 +00:00
First draft of the test tool test-scenarios that should make it easier to test new scenarios in case we have an issue.
This commit is contained in:
parent
b5889e6189
commit
9070c4e836
13 changed files with 916 additions and 0 deletions
|
|
@ -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 = []
|
||||
77
vuinputd-tests/src/bin/test-scenarios.rs
Normal file
77
vuinputd-tests/src/bin/test-scenarios.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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),
|
||||
*/
|
||||
}
|
||||
}
|
||||
342
vuinputd-tests/src/devices/keyboard.rs
Normal file
342
vuinputd-tests/src/devices/keyboard.rs
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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<File, io::Error> {
|
||||
super::utils::fetch_device_node(sysname)
|
||||
.and_then(|devnode| File::open(&devnode))
|
||||
}
|
||||
|
||||
fn setup(device:Option<&str>,name: &str) -> Result<i32, io::Error> {
|
||||
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<String, io::Error> {
|
||||
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;
|
||||
38
vuinputd-tests/src/devices/mod.rs
Normal file
38
vuinputd-tests/src/devices/mod.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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<File, io::Error>;
|
||||
|
||||
/// Phase 1: open uinput, configure keys, call ui_dev_setup
|
||||
fn setup(device:Option<&str>,name: &str) -> Result<i32, io::Error>;
|
||||
|
||||
/// Phase 2: call ui_dev_create, get sysname and devnode
|
||||
fn create(fd: i32) -> Result<String, io::Error>;
|
||||
|
||||
/// Phase 3: call ui_dev_destroy and close fd
|
||||
fn destroy(fd: i32);
|
||||
}
|
||||
114
vuinputd-tests/src/devices/mouse.rs
Normal file
114
vuinputd-tests/src/devices/mouse.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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<File, io::Error> {
|
||||
super::utils::fetch_device_node(sysname)
|
||||
.and_then(|devnode| File::open(&devnode))
|
||||
}
|
||||
|
||||
fn setup(device: Option<&str>, name: &str) -> Result<i32, io::Error> {
|
||||
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<String, io::Error> {
|
||||
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;
|
||||
143
vuinputd-tests/src/devices/utils.rs
Normal file
143
vuinputd-tests/src/devices/utils.rs
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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::<libc::input_event>();
|
||||
|
||||
//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<LoggedInputEvent> {
|
||||
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<String> {
|
||||
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<input_event> {
|
||||
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::<input_event>(),
|
||||
)
|
||||
};
|
||||
if ret as usize != mem::size_of::<input_event>() {
|
||||
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<i32> {
|
||||
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)
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
// TODO: Use https://varlink.org/ which also supports bridges over ssh, which is nice
|
||||
|
||||
use std::{
|
||||
io,
|
||||
os::{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
pub mod devices;
|
||||
pub mod scenarios;
|
||||
pub mod bwrap;
|
||||
pub mod ipc;
|
||||
pub mod podman;
|
||||
|
|
|
|||
40
vuinputd-tests/src/scenarios/basic_keyboard.rs
Normal file
40
vuinputd-tests/src/scenarios/basic_keyboard.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
40
vuinputd-tests/src/scenarios/basic_mouse.rs
Normal file
40
vuinputd-tests/src/scenarios/basic_mouse.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
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(())
|
||||
}
|
||||
}
|
||||
40
vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs
Normal file
40
vuinputd-tests/src/scenarios/basic_ps4_gamepad.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
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(())
|
||||
}
|
||||
}
|
||||
41
vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs
Normal file
41
vuinputd-tests/src/scenarios/basic_xbox_gamepad.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
33
vuinputd-tests/src/scenarios/mod.rs
Normal file
33
vuinputd-tests/src/scenarios/mod.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
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<String>,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue