From f98c89ab062734d730f722523f16dbd128d4f9b0 Mon Sep 17 00:00:00 2001 From: Johannes Leupolz Date: Sat, 8 Nov 2025 22:06:19 +0000 Subject: [PATCH] Implemented device reuse. This is relevant for Steam. --- README.md | 5 + vuinput-examples/Cargo.toml | 3 + vuinput-examples/src/bin/mouse-reuse.rs | 239 ++++++++++++++++++++++++ vuinputd/src/main.rs | 24 ++- 4 files changed, 261 insertions(+), 10 deletions(-) create mode 100644 vuinput-examples/src/bin/mouse-reuse.rs diff --git a/README.md b/README.md index 0562dd5..4b70dcf 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,11 @@ It reliably demonstrates the core concept — exposing `/dev/uinput` devices ins ### ✅ Goals for Production Readiness +* [ ] **Device reuse:** + Ensure the daemon handles device reuse properly. This is used by steam. + +* [ ] **Delete devices in container only once:** + * [ ] **Error handling and recovery:** Ensure the daemon gracefully handles container shutdowns, device races, and failed mounts without leaks or undefined states. diff --git a/vuinput-examples/Cargo.toml b/vuinput-examples/Cargo.toml index 242d494..25ce1dc 100644 --- a/vuinput-examples/Cargo.toml +++ b/vuinput-examples/Cargo.toml @@ -9,6 +9,9 @@ name = "keyboard-advanced" [[bin]] name = "mouse-advanced" +[[bin]] +name = "mouse-reuse" + [dependencies] uinput-ioctls = { path = "../uinput-ioctls" } nix = { version = "0.30", features = ["ioctl"] } # ioctl & libc bindings diff --git a/vuinput-examples/src/bin/mouse-reuse.rs b/vuinput-examples/src/bin/mouse-reuse.rs new file mode 100644 index 0000000..7b32db3 --- /dev/null +++ b/vuinput-examples/src/bin/mouse-reuse.rs @@ -0,0 +1,239 @@ +// SPDX-License-Identifier: MIT +// +// Author: Johannes Leupolz + +use libc::uinput_setup; +use libc::{c_int, close, open, write, O_NONBLOCK, O_WRONLY}; +use std::ffi::{CStr, CString}; +use std::io; +use std::mem::{size_of, zeroed}; +use std::os::raw::{c_char, c_void}; +use std::ptr; +use std::thread::sleep; +use std::time::Duration; +pub use uinput_ioctls::*; + +// Constants (same numeric values as in linux headers) +const EV_SYN: i32 = 0x00; +const EV_KEY: i32 = 0x01; +const EV_REL: i32 = 2; +const BTN_LEFT: i32 = 272; +const REL_X: i32 = 0; +const REL_Y: i32 = 1; +const SYN_REPORT: i32 = 0; +const BUS_USB: u16 = 0x03; + +/// + + +fn emit(fd: c_int, ev_type: i32, code: i32, 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; + + // input_event fields: type and code are u16 in C; value is i32 + ie.type_ = ev_type as u16; // note: in libc the field is `type_` + ie.code = code as u16; + ie.value = val as i32; + + // write the struct to the uinput fd + let buf_ptr = &ie as *const libc::input_event as *const c_void; + let bytes = size_of::(); + + let written = unsafe { write(fd, buf_ptr, bytes) }; + if written as usize != bytes { + return Err(io::Error::last_os_error()); + } + Ok(()) +} + +fn main() -> io::Result<()> { + // open device - matches: open("/dev/uinput-test", O_WRONLY | O_NONBLOCK); + + let args: Vec = std::env::args().collect(); + let device= + match args.len() { + 2 => args[1].clone(), + _ => "/dev/uinput".to_string() + }; + + 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()); + } + + // In your snippet you supplied value.into() to the wrappers. The wrappers may accept different types. + // We follow your earlier usage pattern: + unsafe { + let mut version_of_uinput = 0; + let pversion_of_uinput = std::ptr::from_mut(&mut version_of_uinput); + eprintln!("ioctl UI_GET_VERSION request"); + ui_get_version(fd, pversion_of_uinput).unwrap_or_else(|e| { + eprintln!("ui_get_version failed: {:?}", e); + std::process::exit(1); + }); + eprintln!("ioctl UI_GET_VERSION {}", version_of_uinput); + + ui_set_evbit(fd, EV_KEY.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_evbit(EV_KEY) failed: {:?}", e); + std::process::exit(1); + }); + + ui_set_keybit(fd, BTN_LEFT.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_keybit(BTN_LEFT) failed: {:?}", e); + std::process::exit(1); + }); + + + ui_set_evbit(fd, EV_REL.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_evbit(EV_REL) failed: {:?}", e); + std::process::exit(1); + }); + + + ui_set_relbit(fd, REL_X.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_relbit(REL_X) failed: {:?}", e); + std::process::exit(1); + }); + + + ui_set_relbit(fd, REL_Y.try_into().unwrap()).unwrap_or_else(|e| { + eprintln!("ui_set_relbit(REL_Y) failed: {:?}", e); + std::process::exit(1); + }); + } + + // Prepare uinput_setup struct + let mut usetup: uinput_setup = unsafe { zeroed() }; + + // Fill id and name fields + // `id` has bustype, vendor, product fields (types may vary slightly by libc version) + // set bustype/vendor/product as in C example + // Note: make sure the fields exist as below in your libc version; adapt if names differ. + usetup.id.bustype = BUS_USB; + usetup.id.vendor = 0xbeef; + usetup.id.product = 0xdead; + + // Copy device name into the C char array in the struct + let name = CString::new("Example device").unwrap(); + // uinput_setup::name is usually [c_char; UINPUT_MAX_NAME_SIZE] + unsafe { + // Fill with zeros first (already zeroed by zeroed()) then copy bytes + let name_ptr = usetup.name.as_mut_ptr() as *mut c_char; + ptr::copy_nonoverlapping(name.as_ptr(), name_ptr, name.to_bytes_with_nul().len()); + } + + // Call IOCTLs to setup and create the device + // Assuming your wrappers accept (fd, ptr_to_usetup) etc. + // We'll pass pointer to usetup + let usetup_ptr = &mut usetup as *mut uinput_setup; + unsafe { + eprintln!("ui_dev_setup first time"); + ui_dev_setup(fd, usetup_ptr).unwrap_or_else(|e| { + eprintln!("ui_dev_setup failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + ui_dev_create(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_create failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + let mut resultbuf: [c_char; 64] = [0; 64]; + ui_get_sysname(fd, &mut resultbuf).unwrap(); + let sysname = CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy(); + eprintln!("sysname: {}", sysname); + + // Sleep 1 second to allow userspace to detect the device (same as C example) + sleep(Duration::from_secs(1)); + + eprintln!("emit some input"); + for n in 1..10 { + if n % 10 <= 5 { + emit(fd, EV_REL, REL_X, 5)?; + emit(fd, EV_REL, REL_Y, 5)?; + } else { + emit(fd, EV_REL, REL_X, -5)?; + emit(fd, EV_REL, REL_Y, -5)?; + } + emit(fd, EV_SYN, SYN_REPORT, 0)?; + sleep(Duration::from_millis(300)); + } + + // Give userspace time to read events + sleep(Duration::from_secs(5)); + + eprintln!("ui_dev_destroy"); + // Destroy device and close fd + ui_dev_destroy(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_destroy failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + // Give userspace time to read events + sleep(Duration::from_secs(5)); + + // Reuse device + + eprintln!("reuse device"); + ui_dev_setup(fd, usetup_ptr).unwrap_or_else(|e| { + eprintln!("ui_dev_setup failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + ui_dev_create(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_create a second time failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + eprintln!("emit some input"); + for n in 1..10 { + if n % 10 <= 5 { + emit(fd, EV_REL, REL_X, 5)?; + emit(fd, EV_REL, REL_Y, 5)?; + } else { + emit(fd, EV_REL, REL_X, -5)?; + emit(fd, EV_REL, REL_Y, -5)?; + } + emit(fd, EV_SYN, SYN_REPORT, 0)?; + sleep(Duration::from_millis(300)); + } + + + // Give userspace time to read events + sleep(Duration::from_secs(5)); + + eprintln!("ui_dev_destroy"); + // Destroy device and close fd + ui_dev_destroy(fd).unwrap_or_else(|e| { + eprintln!("ui_dev_destroy failed: {:?}", e); + close(fd); + std::process::exit(1); + }); + + // Give userspace time to read events + sleep(Duration::from_secs(2)); + + close(fd); + } + + Ok(()) +} diff --git a/vuinputd/src/main.rs b/vuinputd/src/main.rs index cf65695..9e08fea 100644 --- a/vuinputd/src/main.rs +++ b/vuinputd/src/main.rs @@ -278,7 +278,7 @@ unsafe extern "C" fn vuinput_write( fuse_lowlevel::fuse_reply_write(_req, _size); } Err(e) => { - debug!("error writing to uinput: {e:?}"); + debug!("fh {}: error writing to uinput: {e:?}",fh); fuse_lowlevel::fuse_reply_err(_req, EIO); } } @@ -290,12 +290,16 @@ unsafe extern "C" fn vuinput_release( ) { let fh = &(*_fi).fh; let vuinput_state_mutex = remove_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap(); - - let mut vuinput_state = vuinput_state_mutex.lock().unwrap(); - // remove device in container, if the request was really from another namespace - if ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { - let input_device = vuinput_state.input_device.as_ref().unwrap(); + let mut vuinput_state = vuinput_state_mutex.lock().unwrap(); + let input_device = vuinput_state.input_device.take();; + + // Remove device in container, if the request was really from another namespace + // Only do this in case it has not already been done by the ioctl UI_DEV_DESTROY + // this here is relevant if the process was killed and didn't have the chance to send the + // ioctl UI_DEV_DESTROY. + if input_device.is_some() && ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { + let input_device = input_device.unwrap(); let remove_job=RemoveFromContainerJob::new(vuinput_state.ns_of_requestor.clone(),input_device.devnode.clone(),input_device.syspath.clone(),input_device.major,input_device.minor); JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job)); } @@ -303,7 +307,7 @@ unsafe extern "C" fn vuinput_release( drop(vuinput_state); debug!( - "{}: references left before releasing device {} (expected is 1)", + "fh {}: references left before releasing device {} (expected is 1)", fh, Arc::strong_count(&vuinput_state_mutex) ); @@ -474,15 +478,15 @@ unsafe extern "C" fn vuinput_ioctl( } UI_DEV_DESTROY => { debug!("fh {}: ioctl UI_DEV_DESTROY", fh); + let input_device = vuinput_state.input_device.take(); // Remove device in container, if the request was really from another namespace - if ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { - let input_device = vuinput_state.input_device.as_ref().unwrap(); + if input_device.is_some() && ! SELF_NAMESPACES.get().unwrap().equal_mnt_and_net(&vuinput_state.ns_of_requestor) { + let input_device = input_device.unwrap(); let remove_job=RemoveFromContainerJob::new(vuinput_state.ns_of_requestor.clone(),input_device.devnode.clone(),input_device.syspath.clone(),input_device.major,input_device.minor); JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job)); } - ui_dev_destroy(fd).unwrap(); fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0); }