Test for absolute mouse device added

This commit is contained in:
Johannes Leupolz 2026-04-06 21:22:15 +00:00
parent 0fda223d4e
commit 65802bfa63
9 changed files with 287 additions and 34 deletions

View file

@ -5,10 +5,7 @@
use clap::{Parser, Subcommand};
use vuinputd_tests::scenarios::{
basic_keyboard::BasicKeyboard, basic_mouse::BasicMouse, basic_ps4_gamepad::BasicPs4Gamepad,
basic_xbox_gamepad::BasicXboxGamepad,
ff_xbox_gamepad::FfXboxGamepad, /*
reuse_keyboard::ReuseKeyboard, reuse_xbox_gamepad::ReuseXboxGamepad,
ScenarioArgs, stress_keyboard::StressKeyboard, stress_xbox_gamepad::StressXboxGamepad, */
basic_xbox_gamepad::BasicXboxGamepad, ff_xbox_gamepad::FfXboxGamepad, BasicMouseAbsolute,
ScenarioArgs,
};
@ -36,6 +33,9 @@ enum Commands {
/// Basic mouse test
BasicMouse,
/// Basic mouse (absolute) test
BasicMouseAbsolute,
/// Basic PS4 gamepad test
BasicPs4Gamepad,
@ -70,6 +70,7 @@ fn main() -> Result<(), std::io::Error> {
match cli.command {
Commands::BasicKeyboard => BasicKeyboard::run(&args),
Commands::BasicMouse => BasicMouse::run(&args),
Commands::BasicMouseAbsolute => BasicMouseAbsolute::run(&args),
Commands::BasicPs4Gamepad => BasicPs4Gamepad::run(&args),
Commands::BasicXboxGamepad => BasicXboxGamepad::run(&args),
Commands::FfXboxGamepad => FfXboxGamepad::run(&args),

View file

@ -23,6 +23,16 @@ pub const SYN_REPORT: u16 = 0;
pub const BUS_USB: u16 = 0x03;
pub const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/";
// Absolute Axes
pub const ABS_X: u16 = 0x00;
pub const ABS_Y: u16 = 0x01;
pub const ABS_Z: u16 = 0x02;
pub const ABS_RX: u16 = 0x03;
pub const ABS_RY: u16 = 0x04;
pub const ABS_RZ: u16 = 0x05;
pub const ABS_HAT0X: u16 = 0x10;
pub const ABS_HAT0Y: u16 = 0x11;
/// Struct holding device state
pub struct DeviceState {
pub uinput_fd: i32,
@ -99,7 +109,8 @@ pub trait Device: Sized {
val: i32,
) -> io::Result<LoggedInputEvent> {
let event_device_fd = self.get_event_device()?;
let event = emit_read_and_log(event_device_fd, self.uinput_fd(), ev_type, code, val, false)?;
let event =
emit_read_and_log(event_device_fd, self.uinput_fd(), ev_type, code, val, false)?;
self.state_mut().events.push(event.clone());
Ok(event)
}

View file

@ -5,6 +5,7 @@
pub mod device_base;
pub mod keyboard;
pub mod mouse;
pub mod mouse_absolute;
pub mod ps4_gamepad;
pub mod utils;
pub mod xbox_gamepad;
@ -14,6 +15,7 @@ pub use device_base::Device;
pub use device_base::DeviceState;
pub use keyboard::KeyboardDevice;
pub use mouse::MouseDevice;
pub use mouse_absolute::MouseAbsoluteDevice;
pub use ps4_gamepad::Ps4GamepadDevice;
pub use xbox_gamepad::XboxGamepadDevice;

View file

@ -0,0 +1,161 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
use crate::devices::{
device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB},
utils::{ABS_X, ABS_Y},
};
use libc::{c_int, close, input_absinfo, open, uinput_abs_setup, INPUT_PROP_DIRECT};
use std::io;
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;
// non linux constants used this way in inputtino
pub const ABS_MAX_WIDTH: i32 = 19200;
pub const ABS_MAX_HEIGHT: i32 = 12000;
/// Setup absolute mouse device
unsafe fn setup_mouse_absolute(fd: c_int) -> io::Result<()> {
// EV_SYN (implicitly handled by libevdev, but required manually for uinput)
ui_set_evbit(fd, super::EV_SYN.try_into().unwrap())?;
// INPUT_PROP_DIRECT
ui_set_propbit(fd, INPUT_PROP_DIRECT.try_into().unwrap())?;
// EV_KEY
ui_set_evbit(fd, super::EV_KEY.try_into().unwrap())?;
ui_set_keybit(fd, BTN_LEFT.try_into().unwrap())?;
// EV_ABS
ui_set_evbit(fd, super::EV_ABS.try_into().unwrap())?;
ui_set_absbit(fd, ABS_X.try_into().unwrap())?;
ui_set_absbit(fd, ABS_Y.try_into().unwrap())?;
// Setup absolute axis parameters (min, max, fuzz, flat, resolution)
let abs_x_setup = uinput_abs_setup {
code: ABS_X,
absinfo: input_absinfo {
value: 0,
minimum: 0,
maximum: ABS_MAX_WIDTH,
fuzz: 1,
flat: 0,
resolution: 28,
},
};
ui_abs_setup(fd, &abs_x_setup).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("ui_abs_setup X failed: {:?}", e),
)
})?;
let abs_y_setup = uinput_abs_setup {
code: ABS_Y,
absinfo: input_absinfo {
value: 0,
minimum: 0,
maximum: ABS_MAX_HEIGHT,
fuzz: 1,
flat: 0,
resolution: 28,
},
};
ui_abs_setup(fd, &abs_y_setup).map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("ui_abs_setup Y failed: {:?}", e),
)
})?;
Ok(())
}
pub struct MouseAbsoluteDevice {
state: DeviceState,
}
impl Device for MouseAbsoluteDevice {
fn name() -> &'static str {
"Mouse Absolute"
}
fn state(&self) -> &DeviceState {
&self.state
}
fn state_mut(&mut self) -> &mut DeviceState {
&mut self.state
}
fn get_event_device(&self) -> Result<c_int, io::Error> {
Ok(self.state.event_device_fd)
}
fn create(device: Option<&str>, name: &str) -> Result<Self, io::Error> {
let fd = open_uinput(device)?;
unsafe { setup_mouse_absolute(fd)? };
let temp_device = MouseAbsoluteDevice {
state: DeviceState {
uinput_fd: fd,
sysname: String::new(),
device_name: name.to_string(),
event_device_node: String::new(),
event_device_fd: -1,
events: Vec::new(),
},
};
temp_device.setup_device(name, 0xbeef, 0xdead, BUS_USB, 0)?;
unsafe {
ui_dev_create(fd).map_err(|e| {
eprintln!("ui_dev_create failed: {:?}", e);
e
})?;
}
let sysname = temp_device.get_sysname()?;
let event_device_node = fetch_device_node(&sysname)?;
let event_device_fd = unsafe {
open(
event_device_node.as_ptr() as *const i8,
libc::O_RDONLY | libc::O_NONBLOCK,
)
};
if event_device_fd < 0 {
return Err(io::Error::last_os_error());
}
Ok(MouseAbsoluteDevice {
state: DeviceState {
uinput_fd: fd,
sysname,
device_name: name.to_string(),
event_device_node,
event_device_fd,
events: Vec::new(),
},
})
}
fn destroy(self) {
unsafe {
ui_dev_destroy(self.state.uinput_fd).unwrap_or_else(|e| {
eprintln!("ui_dev_destroy failed: {:?}", e);
std::process::exit(1);
});
close(self.state.uinput_fd);
close(self.state.event_device_fd);
}
}
}

View file

@ -2,13 +2,24 @@
//
// Author: Johannes Leupolz <dev@leupolz.eu>
use crate::devices::device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB};
use crate::devices::{
device_base::{fetch_device_node, open_uinput, Device, DeviceState, BUS_USB},
utils::{ABS_HAT0X, ABS_HAT0Y, ABS_RX, ABS_RY, ABS_RZ, ABS_X, ABS_Y, ABS_Z},
};
use libc::{c_int, close, ff_effect, input_event, open, uinput_ff_upload, EAGAIN};
use nix::{ioctl_write_int, ioctl_write_ptr, poll::{PollFd, PollFlags, PollTimeout, poll}};
use nix::{
ioctl_write_int, ioctl_write_ptr,
poll::{poll, PollFd, PollFlags, PollTimeout},
};
use std::{
io, os::fd::BorrowedFd, sync::{
Arc, atomic::{AtomicBool, Ordering}
}, thread, time::Duration
io,
os::fd::BorrowedFd,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::Duration,
};
use uinput_ioctls::*;
@ -28,16 +39,6 @@ pub const BTN_MODE: u16 = 0x13c;
pub const BTN_THUMBL: u16 = 0x13d;
pub const BTN_THUMBR: u16 = 0x13e;
// Absolute Axes
pub const ABS_X: u16 = 0x00;
pub const ABS_Y: u16 = 0x01;
pub const ABS_Z: u16 = 0x02;
pub const ABS_RX: u16 = 0x03;
pub const ABS_RY: u16 = 0x04;
pub const ABS_RZ: u16 = 0x05;
pub const ABS_HAT0X: u16 = 0x10;
pub const ABS_HAT0Y: u16 = 0x11;
// Force Feedback
// https://github.com/torvalds/linux/blob/master/include/uapi/linux/input.h
pub const FF_RUMBLE: u16 = 0x50;
@ -288,7 +289,7 @@ impl Device for XboxGamepadDevice {
}
impl XboxGamepadDevice {
pub fn read_process_ff_event_from_uinput(&self, shutdown: Arc<AtomicBool>,use_poll:bool) {
pub fn read_process_ff_event_from_uinput(&self, shutdown: Arc<AtomicBool>, use_poll: bool) {
// Copy the i32 file descriptor so we can move it into the thread safely
let fd = self.state().uinput_fd;
@ -296,10 +297,10 @@ impl XboxGamepadDevice {
// Buffer for the raw bytes
let mut buffer = [0u8; 256];
let mut pollfds = [
PollFd::new(unsafe { BorrowedFd::borrow_raw(fd) }, PollFlags::POLLIN),
];
let mut pollfds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(fd) },
PollFlags::POLLIN,
)];
loop {
if shutdown.load(Ordering::SeqCst) {
@ -348,11 +349,12 @@ impl XboxGamepadDevice {
println!("effect type: {}", upload.effect.type_);
ui_end_ff_upload(fd, ptr).unwrap();
};
}
else {
println!("event: {} {} {}",input_event.type_,input_event.code,input_event.value);
crate::devices::utils::emit(fd, input_event.type_,input_event.code,input_event.value).unwrap();
} else {
println!(
"event: {} {} {}",
input_event.type_, input_event.code, input_event.value
);
//crate::devices::utils::emit(fd, input_event.type_,input_event.code,input_event.value).unwrap();
}
} else {
println!("Read {} bytes", result);

View file

@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
use std::thread;
use std::time::Duration;
use crate::devices::{Device, MouseAbsoluteDevice, EV_KEY};
use crate::scenarios::ScenarioArgs;
use crate::test_log::{LoggedInputEvent, TestLog};
const BTN_LEFT: u16 = 272;
pub struct BasicMouseAbsolute;
impl BasicMouseAbsolute {
pub fn run(args: &ScenarioArgs) -> Result<(), std::io::Error> {
let device = args
.dev_path
.clone()
.unwrap_or_else(|| "/dev/uinput".to_string());
let mut mouse = MouseAbsoluteDevice::create(Some(&device), "Example Mouse (absolute)")?;
eprintln!("sysname: {}", mouse.sysname());
thread::sleep(Duration::from_secs(1));
let _ev1 = mouse.emit_read_and_log(EV_KEY, BTN_LEFT, 1)?;
let _ev2 = mouse.emit_read_and_log(EV_KEY, BTN_LEFT, 0)?;
let eventlog = TestLog {
events: mouse.event_log().to_vec(),
};
let serialized = serde_json::to_string(&eventlog).unwrap();
println!("Event log: {}", serialized);
MouseAbsoluteDevice::destroy(mouse);
Ok(())
}
}

View file

@ -41,7 +41,7 @@ impl FfXboxGamepad {
// ensure uploaded effect gets processed
let shutdown = Arc::new(AtomicBool::new(false));
gamepad.read_process_ff_event_from_uinput(shutdown.clone(),false);
gamepad.read_process_ff_event_from_uinput(shutdown.clone(), false);
// Upload effect via ioctl
let effect_id = upload_effect(gamepad.state().event_device_fd, &mut effect)?;

View file

@ -4,6 +4,7 @@
pub mod basic_keyboard;
pub mod basic_mouse;
pub mod basic_mouse_absolute;
pub mod basic_ps4_gamepad;
pub mod basic_xbox_gamepad;
pub mod ff_xbox_gamepad;
@ -17,6 +18,7 @@ pub mod stress_xbox_gamepad;
// Re-exports for type checking
pub use basic_keyboard::BasicKeyboard;
pub use basic_mouse::BasicMouse;
pub use basic_mouse_absolute::BasicMouseAbsolute;
pub use basic_ps4_gamepad::BasicPs4Gamepad;
pub use basic_xbox_gamepad::BasicXboxGamepad;
pub use ff_xbox_gamepad::FfXboxGamepad;

View file

@ -194,7 +194,6 @@ fn test_keyboard_in_container_with_vuinput_placement_on_host() {
assert!(out.status.success());
}
#[ignore = "not implemented yet"]
#[cfg(all(
feature = "requires-privileges",
@ -221,7 +220,7 @@ fn test_gamepad_with_ff_in_container() {
.expect("failed to create IPC");
let out = builder
.command(test_scenarios, &["--ipc","ff-xbox-gamepad"])
.command(test_scenarios, &["--ipc", "ff-xbox-gamepad"])
.run()
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
@ -230,4 +229,40 @@ fn test_gamepad_with_ff_in_container() {
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
assert!(out.status.success());
}
}
#[ignore = "not implemented yet"]
#[cfg(all(
feature = "requires-privileges",
feature = "requires-uinput",
feature = "requires-bwrap"
))]
#[test]
fn test_mouse_absolute_in_container() {
let _guard: run_vuinputd::VuinputdGuard = run_vuinputd::ensure_vuinputd_running(&[]);
let test_scenarios = env!("CARGO_BIN_EXE_test-scenarios");
let (builder, _ipc) = bwrap::BwrapBuilder::new()
.unshare_net()
.ro_bind("/", "/")
.tmpfs("/tmp")
// dev needs to be writable for the new devices
.dev()
// run needs to be writable for the udev devices
.tmpfs("/run")
.dev_bind("/dev/vuinput-test", "/dev/uinput")
.die_with_parent()
.with_ipc()
.expect("failed to create IPC");
let out = builder
.command(test_scenarios, &["--ipc", "basic-mouse-absolute"])
.run()
.unwrap_or_else(|e| panic!("failed to run bwrap!: {e}"));
println!("Output");
println!("stdout: {}", str::from_utf8(&out.stdout).unwrap());
println!("stderr: {}", str::from_utf8(&out.stderr).unwrap());
assert!(out.status.success());
}