Implemented device reuse. This is relevant for Steam.

This commit is contained in:
Johannes Leupolz 2025-11-08 22:06:19 +00:00
parent a01c20d94f
commit f98c89ab06
4 changed files with 261 additions and 10 deletions

View file

@ -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.

View file

@ -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

View file

@ -0,0 +1,239 @@
// SPDX-License-Identifier: MIT
//
// Author: Johannes Leupolz <dev@leupolz.eu>
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::<libc::input_event>();
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<String> = 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(())
}

View file

@ -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);
}