mirror of
https://github.com/joleuger/vuinputd.git
synced 2026-07-18 00:45:07 +00:00
Initial commit
This commit is contained in:
parent
d8b2625ff1
commit
49149e2b48
23 changed files with 3022 additions and 0 deletions
9
Cargo.toml
Normal file
9
Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"uinput-ioctls",
|
||||
"vuinputd",
|
||||
"vuinput-examples",
|
||||
]
|
||||
|
||||
# Optional: force same version for all crates
|
||||
resolver = "2"
|
||||
9
uinput-ioctls/Cargo.toml
Normal file
9
uinput-ioctls/Cargo.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
[package]
|
||||
name = "uinput-ioctls"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
[dependencies]
|
||||
nix = { version = "0.30", features = ["ioctl"] } # ioctl & libc bindings
|
||||
libc = "0.2" # raw system calls
|
||||
68
uinput-ioctls/src/lib.rs
Normal file
68
uinput-ioctls/src/lib.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
use libc::{c_char, c_uint};
|
||||
use libc::{uinput_abs_setup, uinput_ff_erase, uinput_ff_upload, uinput_setup};
|
||||
|
||||
use nix::{
|
||||
ioctl_none, ioctl_read, ioctl_read_buf, ioctl_readwrite, ioctl_write_int, ioctl_write_ptr,
|
||||
request_code_none, request_code_read, request_code_readwrite, request_code_write,
|
||||
};
|
||||
|
||||
pub const UI_DEV_CREATE: u64 = request_code_none!(b'U', 1);
|
||||
pub const UI_DEV_DESTROY: u64 = request_code_none!(b'U', 2);
|
||||
pub const UI_DEV_SETUP: u64 = request_code_write!(b'U', 3, ::std::mem::size_of::<uinput_setup>());
|
||||
pub const UI_ABS_SETUP: u64 =
|
||||
request_code_write!(b'U', 4, ::std::mem::size_of::<uinput_abs_setup>());
|
||||
//pub const UI_ABS_SETUP_WITHOUT_SIZE: u64 = request_code_write!(b'U', 4, 0);
|
||||
|
||||
pub const UI_GET_SYSNAME_WITHOUT_SIZE: u64 = request_code_read!(b'U', 44, 0);
|
||||
//#define UI_GET_SYSNAME(len) _IOC(_IOC_READ, UINPUT_IOCTL_BASE, 44, len)
|
||||
pub const UI_GET_VERSION: u64 = request_code_read!(b'U', 45, ::std::mem::size_of::<c_uint>());
|
||||
|
||||
pub const UI_SET_EVBIT: u64 = request_code_write!(b'U', 100, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_KEYBIT: u64 = request_code_write!(b'U', 101, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_RELBIT: u64 = request_code_write!(b'U', 102, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_ABSBIT: u64 = request_code_write!(b'U', 103, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_MSCBIT: u64 = request_code_write!(b'U', 104, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_LEDBIT: u64 = request_code_write!(b'U', 105, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_SNDBIT: u64 = request_code_write!(b'U', 106, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_FFBIT: u64 = request_code_write!(b'U', 107, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_PHYS: u64 = request_code_write!(b'U', 108, ::std::mem::size_of::<*mut c_char>());
|
||||
pub const UI_SET_SWBIT: u64 = request_code_write!(b'U', 109, std::mem::size_of::<c_uint>());
|
||||
pub const UI_SET_PROPBIT: u64 = request_code_write!(b'U', 110, std::mem::size_of::<c_uint>());
|
||||
|
||||
pub const UI_BEGIN_FF_UPLOAD: u64 =
|
||||
request_code_readwrite!(b'U', 200, ::std::mem::size_of::<uinput_ff_upload>());
|
||||
pub const UI_END_FF_UPLOAD: u64 =
|
||||
request_code_write!(b'U', 201, ::std::mem::size_of::<uinput_ff_upload>());
|
||||
pub const UI_BEGIN_FF_ERASE: u64 =
|
||||
request_code_readwrite!(b'U', 202, ::std::mem::size_of::<uinput_ff_erase>());
|
||||
pub const UI_END_FF_ERASE: u64 =
|
||||
request_code_write!(b'U', 203, ::std::mem::size_of::<uinput_ff_erase>());
|
||||
|
||||
ioctl_none!(ui_dev_create, b'U', 1);
|
||||
ioctl_none!(ui_dev_destroy, b'U', 2);
|
||||
ioctl_write_ptr! {ui_dev_setup, b'U', 3, uinput_setup}
|
||||
ioctl_write_ptr! { ui_abs_setup, b'U', 4, uinput_abs_setup}
|
||||
|
||||
ioctl_read_buf! { ui_get_sysname, b'U', 44, c_char }
|
||||
ioctl_read! { ui_get_version, b'U', 45, c_uint }
|
||||
|
||||
ioctl_write_int!(ui_set_evbit, b'U', 100);
|
||||
ioctl_write_int!(ui_set_keybit, b'U', 101);
|
||||
ioctl_write_int!(ui_set_relbit, b'U', 102);
|
||||
ioctl_write_int!(ui_set_absbit, b'U', 103);
|
||||
ioctl_write_int!(ui_set_mscbit, b'U', 104);
|
||||
ioctl_write_int!(ui_set_ledbit, b'U', 105);
|
||||
ioctl_write_int!(ui_set_sndbit, b'U', 106);
|
||||
ioctl_write_int!(ui_set_ffbit, b'U', 107);
|
||||
ioctl_write_ptr!(ui_set_phys, b'U', 108, *const c_char); // original macro #define UI_SET_PHYS _IOW(UINPUT_IOCTL_BASE, 108, char*)
|
||||
ioctl_write_int!(ui_set_swbit, b'U', 109);
|
||||
ioctl_write_int!(ui_set_propbit, b'U', 110);
|
||||
|
||||
ioctl_readwrite!(ui_begin_ff_upload, b'U', 200, uinput_ff_upload);
|
||||
ioctl_write_ptr!(ui_end_ff_upload, b'U', 201, uinput_ff_upload);
|
||||
ioctl_readwrite!(ui_begin_ff_erase, b'U', 202, uinput_ff_erase);
|
||||
ioctl_write_ptr!(ui_end_ff_erase, b'U', 203, uinput_ff_erase);
|
||||
19
vuinput-examples/Cargo.toml
Normal file
19
vuinput-examples/Cargo.toml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "vuinput-examples"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[[bin]]
|
||||
name = "keyboard-advanced"
|
||||
|
||||
[[bin]]
|
||||
name = "mouse-advanced"
|
||||
|
||||
[dependencies]
|
||||
uinput-ioctls = { path = "../uinput-ioctls" }
|
||||
#fuse = "0.3" # FUSE/ CUSE interface
|
||||
libfuse-sys = { git = "https://github.com/richard-w/libfuse-sys.git", rev = "a9bc85e3c24d44d8577e79759c4ccf0a18050037", features = ["fuse_35","cuse_lowlevel"] }
|
||||
#fuse-backend-rs = "0.13.0"
|
||||
nix = { version = "0.30", features = ["ioctl"] } # ioctl & libc bindings
|
||||
libc = "0.2" # raw system calls
|
||||
libudev = "0.3" # enumerate-udev
|
||||
399
vuinput-examples/src/bin/keyboard-advanced.rs
Normal file
399
vuinput-examples/src/bin/keyboard-advanced.rs
Normal file
|
|
@ -0,0 +1,399 @@
|
|||
// 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 SYN_REPORT: i32 = 0;
|
||||
const BUS_USB: u16 = 0x03;
|
||||
|
||||
/// Key codes. Those are used by udev to recognize a device as a keyboard.
|
||||
const KEY_ESC: i32 = 1;
|
||||
const KEY_1: i32 = 2;
|
||||
const KEY_2: i32 = 3;
|
||||
const KEY_3: i32 = 4;
|
||||
const KEY_4: i32 = 5;
|
||||
const KEY_5: i32 = 6;
|
||||
const KEY_6: i32 = 7;
|
||||
const KEY_7: i32 = 8;
|
||||
const KEY_8: i32 = 9;
|
||||
const KEY_9: i32 = 10;
|
||||
const KEY_0: i32 = 11;
|
||||
const KEY_MINUS: i32 = 12;
|
||||
const KEY_EQUAL: i32 = 13;
|
||||
const KEY_BACKSPACE: i32 = 14;
|
||||
const KEY_TAB: i32 = 15;
|
||||
const KEY_Q: i32 = 16;
|
||||
const KEY_W: i32 = 17;
|
||||
const KEY_E: i32 = 18;
|
||||
const KEY_R: i32 = 19;
|
||||
const KEY_T: i32 = 20;
|
||||
const KEY_Y: i32 = 21;
|
||||
const KEY_U: i32 = 22;
|
||||
const KEY_I: i32 = 23;
|
||||
const KEY_O: i32 = 24;
|
||||
const KEY_P: i32 = 25;
|
||||
const KEY_LEFTBRACE: i32 = 26;
|
||||
const KEY_RIGHTBRACE: i32 = 27;
|
||||
const KEY_ENTER: i32 = 28;
|
||||
const KEY_LEFTCTRL: i32 = 29;
|
||||
const KEY_A: i32 = 30;
|
||||
const KEY_S: i32 = 31;
|
||||
|
||||
/// Space and other common keys
|
||||
const KEY_D: i32 = 32;
|
||||
const KEY_F: i32 = 33;
|
||||
const KEY_G: i32 = 34;
|
||||
const KEY_H: i32 = 35;
|
||||
const KEY_J: i32 = 36;
|
||||
const KEY_K: i32 = 37;
|
||||
const KEY_L: i32 = 38;
|
||||
const KEY_SEMICOLON: i32 = 39;
|
||||
const KEY_APOSTROPHE: i32 = 40;
|
||||
const KEY_GRAVE: i32 = 41;
|
||||
const KEY_LEFTSHIFT: i32 = 42;
|
||||
const KEY_BACKSLASH: i32 = 43;
|
||||
const KEY_Z: i32 = 44;
|
||||
const KEY_X: i32 = 45;
|
||||
const KEY_C: i32 = 46;
|
||||
const KEY_V: i32 = 47;
|
||||
const KEY_B: i32 = 48;
|
||||
const KEY_N: i32 = 49;
|
||||
const KEY_M: i32 = 50;
|
||||
const KEY_COMMA: i32 = 51;
|
||||
const KEY_DOT: i32 = 52;
|
||||
const KEY_SLASH: i32 = 53;
|
||||
const KEY_RIGHTSHIFT: i32 = 54;
|
||||
const KEY_KPASTERISK: i32 = 55;
|
||||
const KEY_LEFTALT: i32 = 56;
|
||||
const KEY_SPACE: i32 = 57;
|
||||
const KEY_CAPSLOCK: i32 = 58;
|
||||
|
||||
/// Function keys
|
||||
const KEY_F1: i32 = 59;
|
||||
const KEY_F2: i32 = 60;
|
||||
const KEY_F3: i32 = 61;
|
||||
const KEY_F4: i32 = 62;
|
||||
const KEY_F5: i32 = 63;
|
||||
const KEY_F6: i32 = 64;
|
||||
const KEY_F7: i32 = 65;
|
||||
const KEY_F8: i32 = 66;
|
||||
const KEY_F9: i32 = 67;
|
||||
const KEY_F10: i32 = 68;
|
||||
const KEY_NUMLOCK: i32 = 69;
|
||||
const KEY_SCROLLLOCK: i32 = 70;
|
||||
const KEY_KP7: i32 = 71;
|
||||
const KEY_KP8: i32 = 72;
|
||||
const KEY_KP9: i32 = 73;
|
||||
const KEY_KPMINUS: i32 = 74;
|
||||
const KEY_KP4: i32 = 75;
|
||||
const KEY_KP5: i32 = 76;
|
||||
const KEY_KP6: i32 = 77;
|
||||
const KEY_KPPLUS: i32 = 78;
|
||||
const KEY_KP1: i32 = 79;
|
||||
const KEY_KP2: i32 = 80;
|
||||
const KEY_KP3: i32 = 81;
|
||||
const KEY_KP0: i32 = 82;
|
||||
const KEY_KPDOT: i32 = 83;
|
||||
|
||||
/// Arrow keys and navigation
|
||||
const KEY_ZENKAKUHANKAKU: i32 = 85;
|
||||
const KEY_102ND: i32 = 86;
|
||||
const KEY_F11: i32 = 87;
|
||||
const KEY_F12: i32 = 88;
|
||||
const KEY_RO: i32 = 89;
|
||||
const KEY_KATAKANA: i32 = 90;
|
||||
const KEY_HIRAGANA: i32 = 91;
|
||||
const KEY_HENKAN: i32 = 92;
|
||||
const KEY_KATAKANAHIRAGANA: i32 = 93;
|
||||
const KEY_MUHENKAN: i32 = 94;
|
||||
const KEY_KPJPCOMMA: i32 = 95;
|
||||
const KEY_KPENTER: i32 = 96;
|
||||
const KEY_RIGHTCTRL: i32 = 97;
|
||||
const KEY_KPSLASH: i32 = 98;
|
||||
const KEY_SYSRQ: i32 = 99;
|
||||
const KEY_RIGHTALT: i32 = 100;
|
||||
const KEY_LINEFEED: i32 = 101;
|
||||
const KEY_HOME: i32 = 102;
|
||||
const KEY_UP: i32 = 103;
|
||||
const KEY_PAGEUP: i32 = 104;
|
||||
const KEY_LEFT: i32 = 105;
|
||||
const KEY_RIGHT: i32 = 106;
|
||||
const KEY_END: i32 = 107;
|
||||
const KEY_DOWN: i32 = 108;
|
||||
const KEY_PAGEDOWN: i32 = 109;
|
||||
const KEY_INSERT: i32 = 110;
|
||||
const KEY_DELETE: i32 = 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, 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(())
|
||||
}
|
||||
|
||||
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 path = CString::new("/dev/uinput-test").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);
|
||||
|
||||
let _ = set_standard_keyboard_keys(fd).unwrap_or_else(|e| {
|
||||
eprintln!("set_standard_keyboard_keys 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 {
|
||||
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: [i8; 64] = [0i8; 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(10));
|
||||
|
||||
// Emit press + syn + release + syn
|
||||
emit(fd, EV_KEY, KEY_SPACE, 1)?;
|
||||
emit(fd, EV_SYN, SYN_REPORT, 0)?;
|
||||
emit(fd, EV_KEY, KEY_SPACE, 0)?;
|
||||
emit(fd, EV_SYN, SYN_REPORT, 0)?;
|
||||
|
||||
// Give userspace time to read events
|
||||
sleep(Duration::from_secs(10));
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
190
vuinput-examples/src/bin/mouse-advanced.rs
Normal file
190
vuinput-examples/src/bin/mouse-advanced.rs
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
// 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 {
|
||||
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: [i8; 64] = [0i8; 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(10));
|
||||
|
||||
for n in 1..3000 {
|
||||
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(10));
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
||||
close(fd);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
28
vuinputd/Cargo.toml
Normal file
28
vuinputd/Cargo.toml
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
[package]
|
||||
name = "vuinputd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Johannes Leupolz <dev@leupolz.eu>"]
|
||||
license = "MIT"
|
||||
description = "Container-safe mediation daemon for /dev/uinput using CUSE."
|
||||
repository = "https://github.com/joleuger/vuinputd"
|
||||
|
||||
[dependencies]
|
||||
uinput-ioctls = { path = "../uinput-ioctls" }
|
||||
#fuse = "0.3" # FUSE/ CUSE interface
|
||||
libfuse-sys = { git = "https://github.com/richard-w/libfuse-sys.git", rev = "a9bc85e3c24d44d8577e79759c4ccf0a18050037", features = ["fuse_35","cuse_lowlevel"] }
|
||||
#fuse-backend-rs = "0.13.0"
|
||||
nix = { version = "0.30", features = ["ioctl","process","sched","fs","event","user","socket","uio"] }
|
||||
libc = "0.2" # raw system calls
|
||||
time = "0.3" # for Timespec in FUSE replies
|
||||
#input-linux-sys = "0.9.0"
|
||||
#linux-raw-sys = {version="0.11.0", features = ["ioctl"]}
|
||||
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
|
||||
env_logger = "0.11.8"
|
||||
libudev = "0.3" # enumerate-udev
|
||||
regex = "1.12.2"
|
||||
async-channel = "2.5.0"
|
||||
futures = "0.3.31"
|
||||
async-io = "2.6.0"
|
||||
async-pidfd = "0.1.5"
|
||||
anyhow = "1.0.100"
|
||||
110
vuinputd/src/container/inject_in_container_job.rs
Normal file
110
vuinputd/src/container/inject_in_container_job.rs
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
use std::{collections::HashMap, future::Future, pin::Pin, time::Duration};
|
||||
|
||||
use async_io::Timer;
|
||||
use async_pidfd::AsyncPidFd;
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
container::{mknod_input_device::ensure_input_device, netlink_message::send_udev_monitor_message_with_properties, runtime_data::{self, ensure_udev_structure, read_udev_data, write_udev_data}}, jobs::job::{Job, JobTarget}, monitor_udev::EVENT_STORE, namespace::{run_in_net_and_mnt_namespace, Namespaces}
|
||||
};
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct InjectInContainerJob {
|
||||
namespaces: Namespaces,
|
||||
target: JobTarget,
|
||||
dev_path: String,
|
||||
sys_path: String,
|
||||
major: u64,
|
||||
minor: u64,
|
||||
}
|
||||
|
||||
impl InjectInContainerJob {
|
||||
pub fn new(namespaces: Namespaces,dev_path: String, sys_path: String, major: u64, minor: u64) -> Self {
|
||||
Self {
|
||||
namespaces: namespaces.clone(),
|
||||
target: JobTarget::Container(namespaces),
|
||||
dev_path: dev_path,
|
||||
sys_path: sys_path,
|
||||
major: major ,
|
||||
minor: minor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Job for InjectInContainerJob {
|
||||
fn desc(&self) -> &str {
|
||||
"Inject input device into container"
|
||||
}
|
||||
|
||||
fn execute_after_cancellation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn create_task(self: &InjectInContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||
Box::pin(self.clone().inject_in_container())
|
||||
}
|
||||
|
||||
fn job_target(&self) -> JobTarget {
|
||||
self.target.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl InjectInContainerJob {
|
||||
async fn inject_in_container(self) {
|
||||
// temporary hack that needs to be replaced. We try 50 times
|
||||
// Should be: Wait for the device to be created, the runtime data to be written and the
|
||||
// netlink message to be sent
|
||||
let mut netlink_data: Option<HashMap<String,String>> = None;
|
||||
let mut runtime_data: Option<String> = None;
|
||||
let mut number_of_attempt = 1;
|
||||
while number_of_attempt<=50 && !(netlink_data.is_some() && runtime_data.is_some()) {
|
||||
|
||||
if netlink_data.is_none() {
|
||||
|
||||
if let Some(netlink_event)=EVENT_STORE.get().unwrap().lock().unwrap().take(&self.sys_path) {
|
||||
if netlink_event.tombstone || netlink_event.remove_data.is_some() {
|
||||
debug!("do nothing, because the device has already been removed in the meantime");
|
||||
return;
|
||||
}
|
||||
netlink_data=netlink_event.add_data;
|
||||
};
|
||||
}
|
||||
if runtime_data.is_none() {
|
||||
runtime_data = read_udev_data(self.major,self.minor).ok();
|
||||
|
||||
}
|
||||
|
||||
number_of_attempt+=1;
|
||||
// wait a maximum of 5 seconds == 50 attempts
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
}
|
||||
if (netlink_data.is_none() || runtime_data.is_none()) {
|
||||
if netlink_data.is_none() {
|
||||
debug!("Give up reading netlink data");
|
||||
}
|
||||
if runtime_data.is_none() {
|
||||
debug!("Give up reading runtime data");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// define for capturing
|
||||
let major = self.major;
|
||||
let minor=self.minor;
|
||||
let runtime_data = runtime_data.unwrap();
|
||||
let netlink_data = netlink_data.unwrap();
|
||||
|
||||
let child_pid = run_in_net_and_mnt_namespace(self.namespaces, Box::new(move || {
|
||||
|
||||
ensure_input_device(self.dev_path.clone(), self.major, self.minor).unwrap();
|
||||
ensure_udev_structure().unwrap();
|
||||
write_udev_data(runtime_data.as_str(), major, minor).unwrap();
|
||||
send_udev_monitor_message_with_properties(netlink_data.clone());
|
||||
|
||||
}))
|
||||
.expect("subprocess should work");
|
||||
let pid_fd = AsyncPidFd::from_pid(child_pid.as_raw()).unwrap();
|
||||
let _exit_info = pid_fd.wait().await.unwrap();
|
||||
|
||||
}
|
||||
}
|
||||
73
vuinputd/src/container/mknod_input_device.rs
Normal file
73
vuinputd/src/container/mknod_input_device.rs
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
use nix::sys::stat::{makedev, mknod, stat, Mode, SFlag};
|
||||
use nix::unistd::{chown, Gid, Uid};
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::os::unix::fs::{MetadataExt, PermissionsExt};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn ensure_input_device(dev_path: String, major: u64, minor: u64) -> Result<(), Box<dyn Error>> {
|
||||
let path = Path::new(&dev_path);
|
||||
let expected_dev = makedev(major, minor);
|
||||
let expected_mode = 0o666;
|
||||
|
||||
// --- Step 1: Ensure node correctness ---
|
||||
let needs_replacement = if path.exists() {
|
||||
match stat(path) {
|
||||
Ok(st) => {
|
||||
let is_char = (st.st_mode & libc::S_IFMT as u32) == libc::S_IFCHR as u32;
|
||||
let dev_ok = st.st_rdev == expected_dev;
|
||||
!(is_char && dev_ok)
|
||||
}
|
||||
Err(_) => true,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if needs_replacement {
|
||||
println!("Replacing {}", dev_path);
|
||||
let _ = fs::remove_file(path);
|
||||
let mode = Mode::from_bits_truncate(expected_mode);
|
||||
mknod(path, SFlag::S_IFCHR, mode, expected_dev)?;
|
||||
} else {
|
||||
println!("{} is already correct device", dev_path);
|
||||
}
|
||||
|
||||
// --- Step 2: Ensure ownership and permissions ---
|
||||
if let Ok(meta) = fs::metadata(path) {
|
||||
let perms = meta.permissions().mode() & 0o777;
|
||||
|
||||
if perms != expected_mode {
|
||||
println!("Fixing mode of {} (was {:o})", dev_path, perms);
|
||||
fs::set_permissions(path, fs::Permissions::from_mode(expected_mode))?;
|
||||
}
|
||||
|
||||
/* TODO: Think about it
|
||||
let expected_uid = 0;
|
||||
let expected_gid = 0;
|
||||
let uid = meta.uid();
|
||||
let gid = meta.gid();
|
||||
if uid != expected_uid || gid != expected_gid {
|
||||
println!(
|
||||
"Fixing ownership of {} (was uid={}, gid={})",
|
||||
dev_path, uid, gid
|
||||
);
|
||||
chown(path, Some(Uid::from_raw(expected_uid)), Some(Gid::from_raw(expected_gid)))?;
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/*
|
||||
fn main() {
|
||||
let name = "/dev/input/example0";
|
||||
let major = 13; // example values
|
||||
let minor = 37;
|
||||
|
||||
if let Err(e) = ensure_input_device(name, major, minor) {
|
||||
eprintln!("Error: {}", e);
|
||||
}
|
||||
}
|
||||
*/
|
||||
5
vuinputd/src/container/mod.rs
Normal file
5
vuinputd/src/container/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod inject_in_container_job;
|
||||
pub mod remove_from_container_job;
|
||||
pub mod mknod_input_device;
|
||||
pub mod runtime_data;
|
||||
pub mod netlink_message;
|
||||
210
vuinputd/src/container/netlink_message.rs
Normal file
210
vuinputd/src/container/netlink_message.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use std::collections::HashMap;
|
||||
use std::mem;
|
||||
use std::os::fd::{AsRawFd, OwnedFd};
|
||||
|
||||
use std::io::{Cursor, IoSlice};
|
||||
|
||||
use log::debug;
|
||||
use nix::sys::socket::{
|
||||
bind, sendmsg, socket, AddressFamily, MsgFlags, NetlinkAddr, SockFlag, SockProtocol, SockType
|
||||
};
|
||||
|
||||
/// Netlink constants
|
||||
pub const UDEV_EVENT_MODE: u32 = 2;
|
||||
pub const UDEV_MONITOR_MAGIC: u32 = 0xfeedcafe;
|
||||
pub const MAX_NETLINK_PAYLOAD: usize = 64 * 1024; // 64 KiB
|
||||
|
||||
// to test, use "udevadm --debug monitor -p"
|
||||
|
||||
// Taken from: https://github.com/systemd/systemd/blob/61afc53924dd3263e7b76b1323a5fe61d589ffd2/src/libsystemd/sd-device/device-monitor.c#L67-L86
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct MonitorNetlinkHeader {
|
||||
pub prefix: [u8; 8],
|
||||
pub magic: u32,
|
||||
pub header_size: u32,
|
||||
pub properties_off: u32,
|
||||
pub properties_len: u32,
|
||||
pub filter_subsystem_hash: u32,
|
||||
pub filter_devtype_hash: u32,
|
||||
pub filter_tag_bloom_hi: u32,
|
||||
pub filter_tag_bloom_lo: u32,
|
||||
}
|
||||
|
||||
impl MonitorNetlinkHeader {
|
||||
pub fn new(properties_len: usize, subsystem: Option<&str>, devtype: Option<&str>) -> Self {
|
||||
let mut prefix = [0u8; 8];
|
||||
// "libudev" plus null: matches original implementation
|
||||
prefix[..7].copy_from_slice(b"libudev");
|
||||
prefix[7] = 0;
|
||||
|
||||
let mut hdr = Self {
|
||||
prefix,
|
||||
magic: UDEV_MONITOR_MAGIC.to_be(),
|
||||
header_size: mem::size_of::<MonitorNetlinkHeader>() as u32,
|
||||
properties_off: mem::size_of::<MonitorNetlinkHeader>() as u32,
|
||||
properties_len: properties_len as u32,
|
||||
filter_subsystem_hash: 0,
|
||||
filter_devtype_hash: 0,
|
||||
filter_tag_bloom_hi: 0,
|
||||
filter_tag_bloom_lo: 0,
|
||||
};
|
||||
|
||||
if let Some(s) = subsystem {
|
||||
hdr.filter_subsystem_hash = string_hash32(s).to_be();
|
||||
}
|
||||
if let Some(d) = devtype {
|
||||
hdr.filter_devtype_hash = string_hash32(d).to_be();
|
||||
}
|
||||
|
||||
hdr
|
||||
}
|
||||
|
||||
/// Serialize header to bytes (safe copy)
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
// repr(C) fixed-size struct -> safe to transmute bytes by copying
|
||||
let ptr = self as *const MonitorNetlinkHeader as *const u8;
|
||||
unsafe { std::slice::from_raw_parts(ptr, mem::size_of::<MonitorNetlinkHeader>()).to_vec() }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn string_hash32(s: &str) -> u32 {
|
||||
// Note: needs to be compatible with https://github.com/systemd/systemd/blob/main/src/libudev/libudev-monitor.c
|
||||
// and https://gitlab.freedesktop.org/libinput/libinput/-/blob/main/src/udev-seat.c?ref_type=heads.
|
||||
// Because in our use case, only subsystem "input" is relevant, we just hard code the values from murmur hash 2.
|
||||
match s {
|
||||
"input" => 3248653424,
|
||||
"" => 0,
|
||||
_ => panic!("uncovered use case")
|
||||
}
|
||||
}
|
||||
|
||||
/// Open netlink socket, bind to groups
|
||||
fn open_netlink(groups: u32) -> Result<OwnedFd, String> {
|
||||
// Domain AF_NETLINK, type SOCK_RAW, protocol NETLINK_KOBJECT_UEVENT
|
||||
let fd = socket(
|
||||
AddressFamily::Netlink,
|
||||
SockType::Raw,
|
||||
SockFlag::empty(),
|
||||
SockProtocol::NetlinkKObjectUEvent,
|
||||
)
|
||||
.map_err(|e| format!("Could not create netlink socket: {}", e))?;
|
||||
|
||||
// pid 0 => the kernel takes care of assigning it.
|
||||
let sockaddr=NetlinkAddr::new(0, groups);
|
||||
let raw_fd= fd.as_raw_fd();
|
||||
|
||||
bind(raw_fd, &sockaddr).map_err(|e| {
|
||||
format!("Could not bind netlink socket: {}", e)
|
||||
})?;
|
||||
|
||||
Ok(fd)
|
||||
}
|
||||
|
||||
/// Send the monitor header + payload over NETLINK_KOBJECT_UEVENT.
|
||||
/// - `payload` should be the raw udev-style `\0` separated key=value bytes (no base64)
|
||||
/// - `subsystem`/`devtype` optionally used to compute filter hashes
|
||||
pub fn send_udev_monitor_message(
|
||||
payload: &[u8],
|
||||
subsystem: Option<&str>,
|
||||
devtype: Option<&str>,
|
||||
groups: u32,
|
||||
) -> Result<(), String> {
|
||||
if payload.len() + mem::size_of::<MonitorNetlinkHeader>() > MAX_NETLINK_PAYLOAD {
|
||||
return Err(format!(
|
||||
"Total payload too large: {} bytes (max {})",
|
||||
payload.len() + mem::size_of::<MonitorNetlinkHeader>(),
|
||||
MAX_NETLINK_PAYLOAD
|
||||
));
|
||||
}
|
||||
|
||||
let header = MonitorNetlinkHeader::new(payload.len(), subsystem, devtype);
|
||||
let header_bytes = header.to_bytes();
|
||||
|
||||
let fd = open_netlink(groups)?;
|
||||
|
||||
// prepare iovecs
|
||||
let iov = [
|
||||
IoSlice::new(&header_bytes),
|
||||
IoSlice::new(payload),
|
||||
];
|
||||
|
||||
// destination sockaddr (NULL nl_pid => kernel / multicast)
|
||||
let sockaddr = NetlinkAddr::new(0, groups);
|
||||
|
||||
let _rc = sendmsg(fd.as_raw_fd(), &iov, &[], MsgFlags::empty(), Some(&sockaddr))
|
||||
.map_err(|e| format!("Could not send message: {}", e));
|
||||
debug!("udev message sent");
|
||||
|
||||
// ensure cleanup
|
||||
drop(fd);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_udev_monitor_message_with_properties(properties:HashMap<String, String>) {
|
||||
let device_name = match properties.get("DEVNAME") {
|
||||
Some(name) => name,
|
||||
None => "unknown device"
|
||||
};
|
||||
debug!("Sending udev message over netlink for {}",device_name);
|
||||
let mut payload:Vec<u8> = Vec::new();
|
||||
for (key,value) in properties.iter() {
|
||||
payload.extend(key.as_bytes());
|
||||
payload.extend("=".as_bytes());
|
||||
payload.extend(value.as_bytes());
|
||||
payload.push(0);
|
||||
}
|
||||
|
||||
send_udev_monitor_message(&payload,Some("input"),None,UDEV_EVENT_MODE).unwrap();
|
||||
}
|
||||
|
||||
// println!("{:02X?}", payload);
|
||||
// 746573743D76616C75650043555252454E545F544147533D3A736561745F7675696E7075743A00544147533D3A736561745F7675696E7075743A00444556504154483D2F646576696365732F7669727475616C2F696E7075742F696E7075743133382F6576656E74390049445F5655494E5055545F4D4F5553453D31004D494E4F523D37330049445F494E5055543D31002E494E5055545F434C4153533D6D6F757365005345514E554D3D3134393231002E484156455F485744425F50524F504552544945533D31004D414A4F523D313300414354494F4E3D6164640049445F53455249414C3D6E6F73657269616C004445564E414D453D2F6465762F696E7075742F6576656E743900555345435F494E495449414C495A45443D31373337373733353034373139320049445F5655494E5055543D310049445F534541543D736561745F7675696E7075740053554253595354454D3D696E70757400
|
||||
// dGVzdD12YWx1ZQBDVVJSRU5UX1RBR1M9OnNlYXRfdnVpbnB1dDoAVEFHUz06c2VhdF92dWlucHV0OgBERVZQQVRIPS9kZXZpY2VzL3ZpcnR1YWwvaW5wdXQvaW5wdXQxMzgvZXZlbnQ5AElEX1ZVSU5QVVRfTU9VU0U9MQBNSU5PUj03MwBJRF9JTlBVVD0xAC5JTlBVVF9DTEFTUz1tb3VzZQBTRVFOVU09MTQ5MjEALkhBVkVfSFdEQl9QUk9QRVJUSUVTPTEATUFKT1I9MTMAQUNUSU9OPWFkZABJRF9TRVJJQUw9bm9zZXJpYWwAREVWTkFNRT0vZGV2L2lucHV0L2V2ZW50OQBVU0VDX0lOSVRJQUxJWkVEPTE3Mzc3NzM1MDQ3MTkyAElEX1ZVSU5QVVQ9MQBJRF9TRUFUPXNlYXRfdnVpbnB1dABTVUJTWVNURU09aW5wdXQA
|
||||
|
||||
|
||||
|
||||
/*
|
||||
UDEV [16427452.069342] add /devices/virtual/input/input97 (input)
|
||||
ACTION=add
|
||||
DEVPATH=/devices/virtual/input/input97
|
||||
SUBSYSTEM=input
|
||||
PRODUCT=3/beef/dead/0
|
||||
NAME="Example device"
|
||||
PROP=0
|
||||
EV=3
|
||||
KEY=ffffffefffff fffffffffffffffe
|
||||
MODALIAS=input:b0003vBEEFpDEADe0000-e0,1,kramlsfw
|
||||
SEQNUM=14498
|
||||
USEC_INITIALIZED=16427452066918
|
||||
ID_VUINPUT_KEYBOARD=1
|
||||
ID_INPUT=1
|
||||
ID_INPUT_KEY=1
|
||||
.INPUT_CLASS=kbd
|
||||
ID_SERIAL=noserial
|
||||
ID_SEAT=seat_vuinput
|
||||
TAGS=:seat:
|
||||
CURRENT_TAGS=:seat:
|
||||
|
||||
UDEV [16427452.089779] add /devices/virtual/input/input97/event9 (input)
|
||||
ACTION=add
|
||||
DEVPATH=/devices/virtual/input/input97/event9
|
||||
SUBSYSTEM=input
|
||||
DEVNAME=/dev/input/event9
|
||||
SEQNUM=14499
|
||||
USEC_INITIALIZED=16427452068006
|
||||
ID_VUINPUT_KEYBOARD=1
|
||||
.HAVE_HWDB_PROPERTIES=1
|
||||
ID_INPUT=1
|
||||
ID_INPUT_KEY=1
|
||||
.INPUT_CLASS=kbd
|
||||
ID_SERIAL=noserial
|
||||
ID_SEAT=seat_vuinput
|
||||
MAJOR=13
|
||||
MINOR=73
|
||||
TAGS=:seat_vuinput:power-switch:
|
||||
CURRENT_TAGS=:seat_vuinput:power-switch:
|
||||
|
||||
|
||||
*/
|
||||
77
vuinputd/src/container/remove_from_container_job.rs
Normal file
77
vuinputd/src/container/remove_from_container_job.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
use std::{collections::HashMap, future::Future, pin::Pin, time::Duration};
|
||||
|
||||
use async_io::Timer;
|
||||
use async_pidfd::AsyncPidFd;
|
||||
use log::debug;
|
||||
|
||||
use crate::{
|
||||
container::{mknod_input_device::ensure_input_device, netlink_message::send_udev_monitor_message_with_properties, runtime_data::{self, ensure_udev_structure, read_udev_data, write_udev_data}}, jobs::job::{Job, JobTarget}, monitor_udev::EVENT_STORE, namespace::{run_in_net_and_mnt_namespace, Namespaces}
|
||||
};
|
||||
|
||||
#[derive(Clone,Debug)]
|
||||
pub struct RemoveFromContainerJob {
|
||||
namespaces: Namespaces,
|
||||
target: JobTarget,
|
||||
sys_path: String,
|
||||
}
|
||||
|
||||
impl RemoveFromContainerJob {
|
||||
pub fn new(namespaces: Namespaces,sys_path: String) -> Self {
|
||||
Self {
|
||||
namespaces: namespaces.clone(),
|
||||
target: JobTarget::Container(namespaces),
|
||||
sys_path: sys_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Job for RemoveFromContainerJob {
|
||||
fn desc(&self) -> &str {
|
||||
"Remove input device from container"
|
||||
}
|
||||
|
||||
fn execute_after_cancellation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn create_task(self: &RemoveFromContainerJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||
Box::pin(self.clone().remove_from_container())
|
||||
}
|
||||
|
||||
fn job_target(&self) -> JobTarget {
|
||||
self.target.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoveFromContainerJob {
|
||||
async fn remove_from_container(self) {
|
||||
// TODO: Here is a race with inject in container.
|
||||
let netlink_event = match EVENT_STORE.get().unwrap().lock().unwrap().take(&self.sys_path) {
|
||||
Some(netlink_event) => netlink_event,
|
||||
None => {
|
||||
debug!("do nothing, because the device has never been announced via netlink");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if netlink_event.tombstone {
|
||||
debug!("do nothing, because the device has already been removed in the meantime");
|
||||
return;
|
||||
}
|
||||
let netlink_data=netlink_event.add_data;
|
||||
|
||||
// define for capturing
|
||||
let mut netlink_data = netlink_data.unwrap().clone();
|
||||
|
||||
let _ = netlink_data.insert("ACTION".to_string(),"remove".to_string());
|
||||
let child_pid = run_in_net_and_mnt_namespace(self.namespaces, Box::new(move || {
|
||||
|
||||
send_udev_monitor_message_with_properties(netlink_data.clone());
|
||||
|
||||
}))
|
||||
.expect("subprocess should work");
|
||||
let pid_fd = AsyncPidFd::from_pid(child_pid.as_raw()).unwrap();
|
||||
let _exit_info = pid_fd.wait().await.unwrap();
|
||||
|
||||
}
|
||||
}
|
||||
109
vuinputd/src/container/runtime_data.rs
Normal file
109
vuinputd/src/container/runtime_data.rs
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
use std::fs::{self, File};
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// Ensure required udev directories and files exist
|
||||
pub fn ensure_udev_structure() -> io::Result<()> {
|
||||
// TODO: this _must_ exist, before a service using libinput is run. The time of device creation might be too late
|
||||
|
||||
|
||||
let data_dir = Path::new("/run/udev/data");
|
||||
let control_file = Path::new("/run/udev/control");
|
||||
|
||||
// Create directory like `mkdir -p`
|
||||
if !data_dir.exists() {
|
||||
fs::create_dir_all(data_dir)?;
|
||||
}
|
||||
|
||||
// Ensure /run/udev/control exists, create empty if not
|
||||
if !control_file.exists() {
|
||||
File::create(control_file)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write udev data entry for a given major/minor number
|
||||
/// - `content` = original udev data text
|
||||
/// - `major`, `minor` = device numbers
|
||||
///
|
||||
/// Performs these transforms:
|
||||
/// - remove all lines containing `ID_SEAT=`
|
||||
/// - remove all lines containing `seat_` references (G:, Q: lines)
|
||||
/// - replace ID_VUINPUT_* with ID_INPUT_*
|
||||
/// - write updated content to `/run/udev/data/c<major>:<minor>`
|
||||
pub fn write_udev_data(content: &str, major: u64, minor: u64) -> io::Result<()> {
|
||||
let mut cleaned = String::new();
|
||||
|
||||
for line in content.lines() {
|
||||
// skip seat-related lines
|
||||
if line.contains("ID_SEAT=") || line.contains("seat_") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// perform replacements
|
||||
let line = line
|
||||
.replace("ID_VUINPUT_KEYBOARD=1", "ID_INPUT_KEYBOARD=1")
|
||||
.replace("ID_VUINPUT_MOUSE=1", "ID_INPUT_MOUSE=1");
|
||||
|
||||
cleaned.push_str(&line);
|
||||
cleaned.push('\n');
|
||||
}
|
||||
|
||||
|
||||
let path = format!("/run/udev/data/c{}:{}", major, minor);
|
||||
let mut file = File::create(&path)?;
|
||||
file.write_all(cleaned.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
pub fn read_udev_data(major: u64, minor: u64) -> io::Result<String> {
|
||||
let path = format!("/run/udev/data/c{}:{}", major, minor);
|
||||
fs::read_to_string(path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_replacement_and_filter() {
|
||||
let input = r#"I:16429403327735
|
||||
E:ID_VUINPUT_KEYBOARD=1
|
||||
E:ID_INPUT=1
|
||||
E:ID_INPUT_KEY=1
|
||||
E:ID_SERIAL=noserial
|
||||
E:ID_SEAT=seat_vuinput
|
||||
G:seat_vuinput
|
||||
G:power-switch
|
||||
Q:seat_vuinput
|
||||
Q:power-switch
|
||||
V:1"#;
|
||||
|
||||
let expected = r#"I:16429403327735
|
||||
E:ID_INPUT_KEYBOARD=1
|
||||
E:ID_INPUT=1
|
||||
E:ID_INPUT_KEY=1
|
||||
E:ID_SERIAL=noserial
|
||||
G:power-switch
|
||||
Q:power-switch
|
||||
V:1
|
||||
"#;
|
||||
|
||||
let mut cleaned = String::new();
|
||||
for line in input.lines() {
|
||||
if line.contains("ID_SEAT=") || line.contains("seat_") {
|
||||
continue;
|
||||
}
|
||||
let line = line
|
||||
.replace("ID_VUINPUT_KEYBOARD=1", "ID_INPUT_KEYBOARD=1")
|
||||
.replace("ID_VUINPUT_MOUSE=1", "ID_INPUT_MOUSE=1");
|
||||
cleaned.push_str(&line);
|
||||
cleaned.push('\n');
|
||||
}
|
||||
|
||||
assert_eq!(cleaned, expected);
|
||||
}
|
||||
}
|
||||
85
vuinputd/src/jobs/closure_job.rs
Normal file
85
vuinputd/src/jobs/closure_job.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use crate::jobs::job::{Dispatcher, Job, JobTarget};
|
||||
|
||||
pub struct ClosureJob {
|
||||
desc: String,
|
||||
execute_after_cancellation: bool,
|
||||
target: JobTarget,
|
||||
task_creator: Box<dyn Fn(JobTarget) -> Pin<Box<dyn Future<Output = ()>>> + Send + 'static>,
|
||||
}
|
||||
|
||||
impl ClosureJob {
|
||||
pub fn new(
|
||||
desc: impl Into<String>,
|
||||
target: JobTarget,
|
||||
execute_after_cancellation: bool,
|
||||
f: Box<
|
||||
dyn Fn(JobTarget) -> Pin<Box<dyn Future<Output = ()>>> // closure returns any future
|
||||
+ Send // the closure itself can be sent across threads
|
||||
+ 'static,
|
||||
>,
|
||||
) -> Self
|
||||
where {
|
||||
Self {
|
||||
desc: desc.into(),
|
||||
execute_after_cancellation,
|
||||
target,
|
||||
task_creator: f,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Job for ClosureJob {
|
||||
fn desc(&self) -> &str {
|
||||
&self.desc
|
||||
}
|
||||
|
||||
fn execute_after_cancellation(&self) -> bool {
|
||||
self.execute_after_cancellation
|
||||
}
|
||||
|
||||
fn create_task(self: &ClosureJob) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||
let creator = &self.task_creator;
|
||||
let target = self.job_target();
|
||||
let task = creator(target);
|
||||
task
|
||||
}
|
||||
|
||||
fn job_target(&self) -> JobTarget {
|
||||
self.target.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Example usage
|
||||
#[allow(dead_code)]
|
||||
pub fn example() {
|
||||
let mut dispatcher = Dispatcher::new();
|
||||
|
||||
// Send a Host job
|
||||
dispatcher.dispatch(Box::new(ClosureJob::new(
|
||||
"Host maintenance",
|
||||
JobTarget::Host,
|
||||
false,
|
||||
Box::new(|target| {
|
||||
Box::pin(async move {
|
||||
println!("Running host job on {:?}", target);
|
||||
})
|
||||
}),
|
||||
)));
|
||||
|
||||
// Sending a Container job works the same
|
||||
// dispatcher.dispatch(Job::new(JobTarget::Container(ns.clone()), "Container task", false, |target| async move {
|
||||
// println!("Running container job for {:?}", target);
|
||||
// }));
|
||||
|
||||
//
|
||||
// JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(ClosureJob::new("Monitor udev events", JobTarget::BackgroundLoop,false,
|
||||
// Box::new(move |_target| Box::pin(monitor_udev::udev_monitor_loop(cancel_token.clone()))))));
|
||||
|
||||
|
||||
// Allow loops to run briefly before dropping all senders -> graceful shutdown
|
||||
dispatcher.close();
|
||||
dispatcher.wait_until_finished();
|
||||
}
|
||||
203
vuinputd/src/jobs/job.rs
Normal file
203
vuinputd/src/jobs/job.rs
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
use async_channel::{Receiver, Sender};
|
||||
use futures::executor::{LocalPool, LocalSpawner};
|
||||
use futures::future::RemoteHandle;
|
||||
use futures::task::LocalSpawnExt;
|
||||
use std::sync::Mutex;
|
||||
use std::thread::{self, JoinHandle};
|
||||
use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
|
||||
|
||||
use crate::namespace::Namespaces;
|
||||
|
||||
// To discuss:
|
||||
// what we handle here, could also be named Task. The decision for job was more or less
|
||||
// because the main goal was to run some short "scripts" that create files etc.
|
||||
// see e.g., https://blog.yoshuawuyts.com/async-cancellation-1/
|
||||
|
||||
/// Represents where a job should run.
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum JobTarget {
|
||||
/// A global or host-wide task.
|
||||
Host,
|
||||
BackgroundLoop,
|
||||
/// A specific container or namespace target.
|
||||
Container(Namespaces),
|
||||
}
|
||||
|
||||
pub trait Job: Send + 'static {
|
||||
/// Free-form description, used for logging or debugging
|
||||
fn desc(&self) -> &str;
|
||||
|
||||
/// Job Target
|
||||
fn job_target(&self) -> JobTarget;
|
||||
|
||||
/// Whether the job should still execute after cancellation
|
||||
fn execute_after_cancellation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Main entry point — creates the future that executes this job
|
||||
fn create_task(self: &Self) -> Pin<Box<dyn Future<Output = ()>>>;
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for dyn Job {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Job")
|
||||
.field("target", &self.job_target())
|
||||
.field("desc", &self.desc())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Central dispatcher that manages per-target async loops.
|
||||
#[derive(Debug)]
|
||||
pub struct Dispatcher {
|
||||
thread_handle: Option<JoinHandle<()>>,
|
||||
tx: Option<Sender<Box<dyn Job>>>,
|
||||
future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>>,
|
||||
}
|
||||
|
||||
impl Dispatcher {
|
||||
/// Create a new dispatcher and return its sender handle.
|
||||
pub fn new() -> Self {
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
|
||||
// Map of active per-target senders.
|
||||
let targets: Arc<Mutex<HashMap<JobTarget, Sender<Box<dyn Job>>>>> =
|
||||
Arc::new(Mutex::new(HashMap::new()));
|
||||
|
||||
let rx_in_thread: Receiver<Box<dyn Job>> = rx.clone();
|
||||
let future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
let future_handles_for_thread = future_handles.clone();
|
||||
// run dispatcher in a dedicated thread
|
||||
let thread_handle = thread::spawn(move || {
|
||||
let mut pool = LocalPool::new();
|
||||
let spawner = pool.spawner();
|
||||
|
||||
let dispatcher_loop_handle = spawner
|
||||
.spawn_local_with_handle(spawn_dispatcher_loop(
|
||||
spawner.clone(),
|
||||
targets,
|
||||
rx_in_thread,
|
||||
future_handles_for_thread.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
future_handles_for_thread
|
||||
.lock()
|
||||
.unwrap()
|
||||
.push(dispatcher_loop_handle);
|
||||
pool.run(); // blocks until all tasks complete
|
||||
});
|
||||
|
||||
Self {
|
||||
thread_handle: Some(thread_handle),
|
||||
tx: Some(tx),
|
||||
future_handles: future_handles,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, job: Box<dyn Job>) {
|
||||
self.tx
|
||||
.as_ref()
|
||||
.expect("Dispatcher already closed")
|
||||
.send_blocking(job)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn close(&mut self) {
|
||||
self.tx = None;
|
||||
self.future_handles.lock().unwrap().clear();
|
||||
}
|
||||
|
||||
pub fn wait_until_finished(&mut self) {
|
||||
self.tx = None;
|
||||
self.future_handles.lock().unwrap().clear();
|
||||
let handle = self.thread_handle.take();
|
||||
handle.unwrap().join().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the dispatcher: listen for incoming jobs and route them to the right loop.
|
||||
async fn spawn_dispatcher_loop(
|
||||
spawner: LocalSpawner,
|
||||
targets: Arc<Mutex<HashMap<JobTarget, Sender<Box<dyn Job>>>>>,
|
||||
rx: Receiver<Box<dyn Job>>,
|
||||
future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>>,
|
||||
) {
|
||||
loop {
|
||||
let received_job = rx.recv().await;
|
||||
match received_job {
|
||||
Ok(job) => {
|
||||
if job.job_target() == JobTarget::BackgroundLoop {
|
||||
// this is a separate loop that just runs in parallel and does not need a queue to be ordered.
|
||||
let background_loop_handle =
|
||||
spawner.spawn_local_with_handle(job.create_task()).unwrap();
|
||||
future_handles.lock().unwrap().push(background_loop_handle);
|
||||
log::info!("Spawned new background loop for {:?}", job.desc());
|
||||
} else {
|
||||
let target = job.job_target();
|
||||
let (tx, newly_created) = get_or_spawn_target_loop(
|
||||
spawner.clone(),
|
||||
targets.clone(),
|
||||
target.clone(),
|
||||
future_handles.clone(),
|
||||
)
|
||||
.await;
|
||||
if newly_created {
|
||||
log::info!("Spawned new loop for {:?}", target);
|
||||
}
|
||||
if let Err(e) = tx.send(job).await {
|
||||
log::warn!("Failed to enqueue job: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_err) => {
|
||||
// channel has been closed
|
||||
log::info!("Channel has been closed {:?}", _err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
log::info!("Global dispatcher shutting down gracefully");
|
||||
}
|
||||
|
||||
/// Get or lazily create a target-specific queue and loop.
|
||||
async fn get_or_spawn_target_loop(
|
||||
spawner: LocalSpawner,
|
||||
targets: Arc<Mutex<HashMap<JobTarget, Sender<Box<dyn Job>>>>>,
|
||||
target: JobTarget,
|
||||
future_handles: Arc<Mutex<Vec<RemoteHandle<()>>>>,
|
||||
) -> (Sender<Box<dyn Job>>, bool) {
|
||||
let mut map = targets.lock().unwrap();
|
||||
if let Some(tx) = map.get(&target) {
|
||||
return (tx.clone(), false);
|
||||
}
|
||||
|
||||
let (tx, rx) = async_channel::unbounded();
|
||||
map.insert(target.clone(), tx.clone());
|
||||
drop(map); // release lock before spawning
|
||||
|
||||
let job_target_loop_handle = spawner
|
||||
.spawn_local_with_handle(job_target_loop(target.clone(), rx))
|
||||
.unwrap();
|
||||
future_handles.lock().unwrap().push(job_target_loop_handle);
|
||||
|
||||
(tx, true)
|
||||
}
|
||||
|
||||
/// The main loop for a single job target (container or host).
|
||||
async fn job_target_loop(target: JobTarget, rx: Receiver<Box<dyn Job>>) {
|
||||
log::info!("Starting loop for {:?}", target);
|
||||
while let Ok(job) = rx.recv().await {
|
||||
log::debug!("Executing job: {}", job.desc());
|
||||
job.create_task().await;
|
||||
}
|
||||
log::info!("Loop for {:?} ended — channel closed", target);
|
||||
}
|
||||
|
||||
/*
|
||||
macro_rules! job {
|
||||
($desc:expr, async move { $($body:tt)* }) => {
|
||||
Box::new(ClosureJob::new($desc, |_: JobTarget| async move { $($body)* }))
|
||||
};
|
||||
}
|
||||
*/
|
||||
106
vuinputd/src/jobs/job_builder.rs
Normal file
106
vuinputd/src/jobs/job_builder.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::time::Duration;
|
||||
use futures::FutureExt; // for .timeout()
|
||||
|
||||
pub struct JobBuilder<J: Job> {
|
||||
inner: J,
|
||||
timeout: Option<Duration>,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
execute_despite_cancellation: bool,
|
||||
log: bool,
|
||||
}
|
||||
|
||||
impl<J: Job> JobBuilder<J> {
|
||||
pub fn new(inner: J) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
timeout: None,
|
||||
cancel_token: None,
|
||||
log: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_timeout(mut self, dur: Duration) -> Self {
|
||||
self.timeout = Some(dur);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_cancellation(mut self, token: Arc<AtomicBool>) -> Self {
|
||||
self.cancel_token = Some(token);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn execute_despite_cancellation(mut self, execute: bool) -> Self {
|
||||
self.execute_despite_cancellation = execute;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_logging(mut self) -> Self {
|
||||
self.log = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> WrappedJob<J> {
|
||||
WrappedJob {
|
||||
inner: self.inner,
|
||||
timeout: self.timeout,
|
||||
cancel_token: self.cancel_token,
|
||||
log: self.log,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WrappedJob<J: Job> {
|
||||
inner: J,
|
||||
timeout: Option<Duration>,
|
||||
cancel_token: Option<Arc<AtomicBool>>,
|
||||
log: bool,
|
||||
}
|
||||
|
||||
impl<J: Job> Job for WrappedJob<J> {
|
||||
fn desc(&self) -> &str {
|
||||
self.inner.desc()
|
||||
}
|
||||
|
||||
fn create_task(self: Box<Self>) -> Pin<Box<dyn Future<Output = ()> + Send>> {
|
||||
Box::pin(async move {
|
||||
let desc = self.inner.desc().to_string();
|
||||
let mut fut = self.inner.create_task();
|
||||
|
||||
if
|
||||
|
||||
// Logging
|
||||
if self.log {
|
||||
println!("[START] {desc}");
|
||||
}
|
||||
|
||||
// Cancellation should work cooperatively
|
||||
if let Some(token) = self.cancel_token.clone() {
|
||||
fut = Box::pin(async move {
|
||||
futures::select! {
|
||||
_ = fut.fuse() => {},
|
||||
_ = async {
|
||||
while !token.load(std::sync::atomic::Ordering::Relaxed) {
|
||||
futures_timer::Delay::new(Duration::from_millis(50)).await;
|
||||
}
|
||||
}.fuse() => {},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Timeout
|
||||
if let Some(dur) = self.timeout {
|
||||
fut = Box::pin(fut.timeout(dur).map(|_| ()));
|
||||
}
|
||||
|
||||
fut.await;
|
||||
|
||||
if self.log {
|
||||
println!("[DONE] {desc}");
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
*/
|
||||
35
vuinputd/src/jobs/mod.rs
Normal file
35
vuinputd/src/jobs/mod.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
//! # Design: Async Per-Container Job Executor (Tokio)
|
||||
//!
|
||||
//! ## Overview
|
||||
//! A scalable, structured design for running async jobs per container.
|
||||
//!
|
||||
//! - Global dispatcher routes jobs to per-container async loops or to global queue.
|
||||
//! - Each container has its own unbounded job queue (no backpressure).
|
||||
//! - Loops are spawned lazily on first job and exit when their sender drops.
|
||||
//! - Graceful shutdown happens automatically (channel close → loop exit).
|
||||
//! - Periodic cleanup removes idle container queues.
|
||||
//!
|
||||
//! ## Async Jobs
|
||||
//! - Each `Job` contains an async closure `task: Box<dyn FnOnce(JobTarget) -> Pin<Box<dyn Future<Output = ()>>> + Send>`
|
||||
//! - This allows full async/await usage inside the job body.
|
||||
//!
|
||||
//!
|
||||
//! +--------------------------------------+
|
||||
//! | Global dispatcher |
|
||||
//! +----------+---------------------------+
|
||||
//! | |
|
||||
//! v v
|
||||
//! +----------+-----------+ +------------+
|
||||
//! | Per-container queues | | Host queue |
|
||||
//! +----+------+----+-----+ +------------+
|
||||
//! | | |
|
||||
//! +----v----+ +---v----+ +---v----+
|
||||
//! | Cont A | | Cont B | | Cont C |
|
||||
//! | loop() | | loop() | | loop() |
|
||||
//! +---------+ +--------+ +--------+
|
||||
|
||||
pub mod closure_job;
|
||||
pub mod job;
|
||||
727
vuinputd/src/main.rs
Normal file
727
vuinputd/src/main.rs
Normal file
|
|
@ -0,0 +1,727 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
// vuinputd: container-safe mediation daemon for /dev/uinput
|
||||
//
|
||||
// - Exposes a fake /dev/uinput inside the container (via CUSE).
|
||||
// - Forwards ioctls + writes to the real /dev/uinput on the host.
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
// TODOS:
|
||||
// preliminary close
|
||||
// remove test
|
||||
// correct char device for vuinput
|
||||
// renaming
|
||||
// use in container
|
||||
// cancellation token
|
||||
// device removal (udev netlink message)
|
||||
// distinguish between cleanup jobs that must not be cancelled and other jobs (especially background jobs)
|
||||
// naming: dev_path vs dev_node. I guess I mean the same.
|
||||
// Send warning, if udev monitor does not exist
|
||||
|
||||
|
||||
// test scenario:
|
||||
// vuinputd
|
||||
// mouse-advanced
|
||||
// ls -lh /dev/input/event9
|
||||
// cat /run/udev/data/c13:73
|
||||
// machinectl shell johannes@gamestreamingserver
|
||||
// systemctl --user stop headless-labwc.service
|
||||
// machinectl shell gamestreamingserver
|
||||
// mkdir -p /run/udev/data/
|
||||
// touch /run/udev/control
|
||||
// mknod /dev/input/event9 c 13 73
|
||||
// vim /run/udev/data/c13:73
|
||||
// machinectl shell johannes@gamestreamingserver
|
||||
// systemctl --user start headless-labwc.service
|
||||
// systemctl --user start wayvnc.service
|
||||
|
||||
|
||||
//libinput backend
|
||||
// seatd
|
||||
//libinput-tools
|
||||
// groups
|
||||
// 777 for input9
|
||||
// char-input to systemd
|
||||
|
||||
use libc::O_CLOEXEC;
|
||||
use libc::{iovec, off_t, size_t, EBADRQC, EIO, ENOENT};
|
||||
use libc::{uinput_abs_setup, uinput_ff_erase, uinput_ff_upload, uinput_setup};
|
||||
use libfuse_sys::cuse_lowlevel;
|
||||
use libfuse_sys::fuse_lowlevel;
|
||||
use log::{debug, error, info, trace};
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::fs;
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::io::{self, ErrorKind};
|
||||
use std::os::fd::AsRawFd;
|
||||
use std::os::raw::{c_char, c_int, c_uint, c_void};
|
||||
use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex, OnceLock, RwLock};
|
||||
use uinput_ioctls::*;
|
||||
|
||||
pub mod namespace;
|
||||
pub mod monitor_udev;
|
||||
use crate::container::inject_in_container_job::InjectInContainerJob;
|
||||
use crate::container::netlink_message;
|
||||
use crate::container::remove_from_container_job::RemoveFromContainerJob;
|
||||
use crate::jobs::closure_job::ClosureJob;
|
||||
use crate::monitor_udev::MonitorBackgroundLoop;
|
||||
use crate::namespace::*;
|
||||
|
||||
pub mod jobs;
|
||||
use crate::jobs::job::*;
|
||||
|
||||
pub mod container;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
struct VuInputDevice {
|
||||
cuse_fh : u64,
|
||||
major : u64,
|
||||
minor : u64,
|
||||
syspath: String,
|
||||
devnode: String,
|
||||
runtime_data: Option<String>,
|
||||
netlink_data: Option<String>
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct VuInputState {
|
||||
file: File,
|
||||
ns_of_requestor: Namespaces,
|
||||
input_device: Option<VuInputDevice>
|
||||
}
|
||||
|
||||
#[derive(Debug,Eq, Hash, PartialEq, Clone)]
|
||||
enum VuFileHandle {
|
||||
Fh(u64)
|
||||
}
|
||||
|
||||
impl VuFileHandle {
|
||||
fn from_fuse_file_info(fi: &fuse_lowlevel::fuse_file_info) -> VuFileHandle {
|
||||
VuFileHandle::Fh(fi.fh)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for VuFileHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
VuFileHandle::Fh(fh) => writeln!(f, "VuFileHandle({:?})",fh)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
static VUINPUT_COUNTER: OnceLock<AtomicU64> = OnceLock::new();
|
||||
static VUINPUT_STATE: OnceLock<RwLock<HashMap<VuFileHandle, Arc<Mutex<VuInputState>>>>> = OnceLock::new();
|
||||
static JOB_DISPATCHER: OnceLock<Mutex<Dispatcher>>= OnceLock::new();
|
||||
static SELF_NAMESPACES: OnceLock<Namespaces>= OnceLock::new();
|
||||
|
||||
|
||||
const SYS_INPUT_DIR: &str = "/sys/devices/virtual/input/";
|
||||
|
||||
fn get_vuinput_state(
|
||||
fh:&VuFileHandle,
|
||||
) -> Result<Arc<Mutex<VuInputState>>, String> {
|
||||
let map = VUINPUT_STATE
|
||||
.get()
|
||||
.ok_or("global not initialized".to_string())?;
|
||||
let guard = map.read().map_err(|e| e.to_string())?;
|
||||
guard
|
||||
.get(&fh)
|
||||
.cloned()
|
||||
.ok_or("handle not opened".to_string())
|
||||
}
|
||||
|
||||
fn get_fresh_filehandle() -> u64 {
|
||||
let ctr = VUINPUT_COUNTER.get().unwrap();
|
||||
ctr.fetch_add(1, Ordering::SeqCst).into()
|
||||
}
|
||||
|
||||
fn insert_vuinput_state(
|
||||
fh:&VuFileHandle,
|
||||
state: VuInputState,
|
||||
) -> Result<(), String> {
|
||||
let map = VUINPUT_STATE
|
||||
.get()
|
||||
.ok_or("global not initialized".to_string())?;
|
||||
let mut guard = map.write().map_err(|e| e.to_string())?;
|
||||
|
||||
if guard.contains_key(&fh) {
|
||||
return Err(format!(
|
||||
"file handle {} already exists. file handles must not be reused!",
|
||||
&fh
|
||||
));
|
||||
}
|
||||
|
||||
let _ = guard.insert(fh.clone(), Arc::new(Mutex::new(state)));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_vuinput_state(
|
||||
fh:&VuFileHandle,
|
||||
) -> Result<Arc<Mutex<VuInputState>>, String> {
|
||||
let map = VUINPUT_STATE
|
||||
.get()
|
||||
.ok_or("global not initialized".to_string())?;
|
||||
let mut guard = map.write().map_err(|e| e.to_string())?;
|
||||
let old_value = guard.remove(&fh).ok_or("fh unknown")?;
|
||||
Ok(old_value)
|
||||
}
|
||||
|
||||
fn fetch_device_node(path: &str) -> io::Result<String> {
|
||||
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"))
|
||||
}
|
||||
|
||||
/// Returns (major, minor) numbers of a device node at `path`
|
||||
fn fetch_major_minor(path: &str) -> io::Result<(u64, u64)> {
|
||||
let metadata = fs::metadata(path)?;
|
||||
|
||||
// Ensure it's a character device
|
||||
if !metadata.file_type().is_char_device() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"Not a character device",
|
||||
));
|
||||
}
|
||||
|
||||
let rdev = metadata.rdev();
|
||||
let major = ((rdev >> 8) & 0xfff) as u64;
|
||||
let minor = ((rdev & 0xff) | ((rdev >> 12) & 0xfff00)) as u64;
|
||||
|
||||
Ok((major, minor))
|
||||
}
|
||||
|
||||
unsafe extern "C" fn vuinput_open(
|
||||
_req: fuse_lowlevel::fuse_req_t,
|
||||
_fi: *mut fuse_lowlevel::fuse_file_info,
|
||||
) {
|
||||
let fh = get_fresh_filehandle();
|
||||
let ctx = fuse_lowlevel::fuse_req_ctx(_req);
|
||||
debug!("fh {}: opened by process id {} (host view)", fh, (*ctx).pid);
|
||||
let namespaces = get_namespaces(Some((*ctx).pid));
|
||||
debug!("fh {}: namespaces {}", fh, namespaces);
|
||||
// namespaces net:4026531840, uts:4026531838, ipc:4026531839, pid:4026531836, pid_for_children:4026531836, user:4026531837, mnt:4026531841, cgroup:4026531835, time:4026531834, time_for_children:4026531834
|
||||
(*_fi).fh = fh;
|
||||
// Open the path in read-only mode, returns `io::Result<File>`
|
||||
let open_vuinput_result = OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
//.custom_flags(O_NONBLOCK)
|
||||
.custom_flags(O_CLOEXEC)
|
||||
.open(Path::new("/dev/uinput"));
|
||||
match open_vuinput_result {
|
||||
Ok(v) => {
|
||||
insert_vuinput_state(
|
||||
&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap()),
|
||||
VuInputState {
|
||||
file: v,
|
||||
ns_of_requestor: namespaces,
|
||||
input_device: None
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
fuse_lowlevel::fuse_reply_open(_req, _fi);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("couldn't open /dev/uinput: {}", e);
|
||||
fuse_lowlevel::fuse_reply_err(_req, ENOENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
unsafe extern "C" fn vuinput_write(
|
||||
_req: fuse_lowlevel::fuse_req_t,
|
||||
_buf: *const c_char,
|
||||
_size: size_t,
|
||||
_off: off_t,
|
||||
_fi: *mut fuse_lowlevel::fuse_file_info,
|
||||
) {
|
||||
assert!(
|
||||
_off == 0,
|
||||
"vuinput_write: offset needs to be 0 but is {}",
|
||||
_off
|
||||
);
|
||||
let slice = std::slice::from_raw_parts(_buf as *const u8, _size);
|
||||
let vuinput_state_mutex = get_vuinput_state(&VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap())).unwrap();
|
||||
let mut vuinput_state = vuinput_state_mutex.lock().unwrap();
|
||||
|
||||
assert!(
|
||||
vuinput_state.input_device.is_some(),
|
||||
"legacy device setup not supported, yet!"
|
||||
);
|
||||
let result = vuinput_state.file.write_all(slice);
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
trace!("wrote {} bytes", _size);
|
||||
fuse_lowlevel::fuse_reply_write(_req, _size);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("error writing to uinput: {e:?}");
|
||||
fuse_lowlevel::fuse_reply_err(_req, EIO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unsafe extern "C" fn vuinput_release(
|
||||
_req: fuse_lowlevel::fuse_req_t,
|
||||
_fi: *mut fuse_lowlevel::fuse_file_info,
|
||||
) {
|
||||
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 remove_job=RemoveFromContainerJob::new(vuinput_state.ns_of_requestor.clone(),vuinput_state.input_device.as_ref().unwrap().syspath.clone());
|
||||
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(remove_job));
|
||||
}
|
||||
|
||||
drop(vuinput_state);
|
||||
|
||||
debug!(
|
||||
"{}: references left before releasing device {} (expected is 1)",
|
||||
fh,
|
||||
Arc::strong_count(&vuinput_state_mutex)
|
||||
);
|
||||
drop(vuinput_state_mutex); // this also closes the file when no other references are open
|
||||
// TODO: maybe also ensure that nothing is left in the containers
|
||||
fuse_lowlevel::fuse_reply_err(_req, 0);
|
||||
}
|
||||
|
||||
unsafe extern "C" fn vuinput_ioctl(
|
||||
_req: fuse_lowlevel::fuse_req_t,
|
||||
_cmd: c_int,
|
||||
_arg: *mut c_void, //note: this is a pointer in the application space and should not be dereferenced at all
|
||||
_fi: *mut fuse_lowlevel::fuse_file_info,
|
||||
_flags: c_uint,
|
||||
_in_buf: *const c_void, // note: this was mapped by the kernel and can be read from
|
||||
_in_bufsz: size_t,
|
||||
_out_bufsz: size_t,
|
||||
) {
|
||||
// fuse_reply_ioctl_retry is only necessary for variable length commands;
|
||||
// see comment "Now check variable-length commands" in uinput.c of the linux kernel.
|
||||
// Those are UI_GET_SYSNAME and UI_ABS_SETUP as of v0.4.
|
||||
|
||||
// ioctl to map are listed on https://www.freedesktop.org/software/libevdev/doc/latest/ioctls.html
|
||||
// https://docs.rs/linux-raw-sys/0.11.0/src/linux_raw_sys/x86_64/ioctl.rs.html#529
|
||||
|
||||
let cmd_u64 = (_cmd as c_uint).into();
|
||||
// normalize the variable length ones
|
||||
let cmd_without_size = cmd_u64 & !(nix::sys::ioctl::SIZEMASK << nix::sys::ioctl::SIZESHIFT);
|
||||
let cmd_normalized = match cmd_without_size {
|
||||
UI_GET_SYSNAME_WITHOUT_SIZE => UI_GET_SYSNAME_WITHOUT_SIZE,
|
||||
//UI_ABS_SETUP => UI_ABS_SETUP_WITHOUT_SIZE,
|
||||
_ => cmd_u64,
|
||||
};
|
||||
let vufh= VuFileHandle::from_fuse_file_info(_fi.as_ref().unwrap());
|
||||
let vuinput_state_mutex = get_vuinput_state(&vufh).unwrap();
|
||||
let fh = &(*_fi).fh;
|
||||
let mut vuinput_state = vuinput_state_mutex.lock().unwrap();
|
||||
|
||||
// ensure for all ioctls that need mapped data, that we have the data correctly mapped
|
||||
match (_in_bufsz, _out_bufsz, cmd_normalized) {
|
||||
(0, _, UI_ABS_SETUP) => {
|
||||
//todo: i guess this needs to be reworked as this is variable size. i guess it is not reachable at all
|
||||
debug!("fh {}: submitting _in_bufsz for UI_ABS_SETUP", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<uinput_abs_setup>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0);
|
||||
return;
|
||||
}
|
||||
(_, 0, UI_GET_SYSNAME_WITHOUT_SIZE) => {
|
||||
let size = (cmd_u64 & nix::sys::ioctl::SIZEMASK) >> nix::sys::ioctl::SIZESHIFT;
|
||||
debug!(
|
||||
"fh {}: submitting _out_bufsz for UI_GET_SYSNAME({}) ",
|
||||
fh, size
|
||||
);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: 64,
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, std::ptr::null(), 0, &iov, 1);
|
||||
return;
|
||||
}
|
||||
(_, 0, UI_GET_VERSION) => {
|
||||
let size = (cmd_u64 & nix::sys::ioctl::SIZEMASK) >> nix::sys::ioctl::SIZESHIFT;
|
||||
debug!(
|
||||
"fh {}: submitting _out_bufsz for UI_GET_VERSION({}) ",
|
||||
fh, size
|
||||
);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: std::mem::size_of::<c_uint>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, std::ptr::null(), 0, &iov, 1);
|
||||
return;
|
||||
}
|
||||
(0, _, UI_DEV_SETUP) => {
|
||||
debug!("fh {}: submitting _in_bufsz for UI_DEV_SETUP", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<uinput_setup>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0);
|
||||
return;
|
||||
}
|
||||
(0, _, UI_SET_PHYS) => {
|
||||
debug!("fh {}: submitting _in_bufsz for UI_SET_PHYS", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<c_char>() * 1024,
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0);
|
||||
return;
|
||||
}
|
||||
(0, _, UI_BEGIN_FF_UPLOAD) => {
|
||||
debug!("fh {}: submitting _in_bufsz for UI_BEGIN_FF_UPLOAD", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<uinput_ff_upload>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, &iov, 1);
|
||||
return;
|
||||
}
|
||||
(0, _, UI_END_FF_UPLOAD) => {
|
||||
debug!("fh {}: submitting _in_bufsz for UI_END_FF_UPLOAD", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<uinput_ff_upload>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0);
|
||||
return;
|
||||
}
|
||||
(0, _, UI_BEGIN_FF_ERASE) => {
|
||||
debug!("fh {}: submitting _in_bufsz for UI_BEGIN_FF_ERASE", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<uinput_ff_erase>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, &iov, 1);
|
||||
return;
|
||||
}
|
||||
(0, _, UI_END_FF_ERASE) => {
|
||||
debug!("fh {}: submitting _in_bufsz for UI_END_FF_ERASE", fh);
|
||||
let iov = iovec {
|
||||
iov_base: _arg,
|
||||
iov_len: ::std::mem::size_of::<uinput_ff_erase>(),
|
||||
};
|
||||
fuse_lowlevel::fuse_reply_ioctl_retry(_req, &iov, 1, std::ptr::null(), 0);
|
||||
return;
|
||||
}
|
||||
_ => {
|
||||
//nothing to map
|
||||
}
|
||||
}
|
||||
|
||||
let fd = vuinput_state.file.as_raw_fd();
|
||||
|
||||
// now we can assume that the data is mapped or it is not required
|
||||
match cmd_normalized {
|
||||
UI_DEV_CREATE => {
|
||||
debug!("fh {}: ioctl UI_DEV_CREATE", fh);
|
||||
ui_dev_create(fd).unwrap();
|
||||
|
||||
let mut resultbuf: [i8; 64] = [0i8; 64];
|
||||
ui_get_sysname(fd, resultbuf.as_mut_slice()).unwrap();
|
||||
let sysname = format!(
|
||||
"{}{}",
|
||||
SYS_INPUT_DIR,
|
||||
CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy()
|
||||
);
|
||||
debug!("fh {}: syspath: {}", fh, sysname);
|
||||
let devnode = fetch_device_node(&sysname).unwrap();
|
||||
debug!("fh {}: devnode: {}", fh, devnode);
|
||||
let (major,minor) = fetch_major_minor(&devnode).unwrap();
|
||||
debug!("fh {}: major: {} minor: {} ", fh, major,minor);
|
||||
vuinput_state.input_device = Some(VuInputDevice {cuse_fh:*fh, major: major, minor: minor, syspath: sysname.clone(), devnode: devnode.clone(), runtime_data: None, netlink_data: None });
|
||||
|
||||
// Create 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 inject_job=InjectInContainerJob::new(vuinput_state.ns_of_requestor.clone(),devnode.clone(),sysname.clone(),major,minor);
|
||||
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(inject_job));
|
||||
}
|
||||
|
||||
// hard code 2 second sleep
|
||||
std::thread::sleep(std::time::Duration::from_secs(2));
|
||||
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_DEV_DESTROY => {
|
||||
debug!("fh {}: ioctl UI_DEV_DESTROY", fh);
|
||||
|
||||
// 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 remove_job=RemoveFromContainerJob::new(vuinput_state.ns_of_requestor.clone(),vuinput_state.input_device.as_ref().unwrap().syspath.clone());
|
||||
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);
|
||||
}
|
||||
UI_DEV_SETUP => {
|
||||
debug!("fh {}: ioctl UI_DEV_SETUP", fh);
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
let setup_ptr = _in_buf as *mut uinput_setup;
|
||||
debug!(
|
||||
"product: {:x} vendor: {:x}",
|
||||
(*setup_ptr).id.product,
|
||||
(*setup_ptr).id.vendor
|
||||
);
|
||||
// replace vendor and product id to the values from sunshine (see inputtino_common.h of sunshine)
|
||||
(*setup_ptr).id.product = 0xdead;
|
||||
(*setup_ptr).id.vendor = 0xbeef;
|
||||
ui_dev_setup(fd, setup_ptr).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_ABS_SETUP => {
|
||||
//todo: i guess this needs to be reworked as this is variable size. i guess it is not reachable at all
|
||||
debug!("fh {}: ioctl UI_ABS_SETUP", fh);
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_GET_SYSNAME_WITHOUT_SIZE => {
|
||||
debug!("fh {}: ioctl UI_GET_SYSNAME {_out_bufsz}", fh);
|
||||
assert!(
|
||||
_out_bufsz == 64,
|
||||
"should have _out_bufsz of length 64 (currently hardcoded)"
|
||||
);
|
||||
let mut resultbuf: [i8; 64] = [0i8; 64];
|
||||
ui_get_sysname(fd, resultbuf.as_mut_slice()).unwrap();
|
||||
let sysname = CStr::from_ptr(resultbuf.as_ptr()).to_string_lossy();
|
||||
debug!("fh {}: sysname: {}", fh, sysname);
|
||||
fuse_lowlevel::fuse_reply_ioctl(
|
||||
_req,
|
||||
0,
|
||||
resultbuf.as_mut_ptr() as *mut c_void,
|
||||
_out_bufsz,
|
||||
);
|
||||
}
|
||||
UI_GET_VERSION => {
|
||||
let mut version_of_kernel = 0;
|
||||
let pversion_of_kernel = std::ptr::from_mut(&mut version_of_kernel);
|
||||
ui_get_version(fd, pversion_of_kernel).unwrap();
|
||||
debug!("fh {}: ioctl UI_GET_VERSION {}", fh, version_of_kernel);
|
||||
let reply_arg = 5;
|
||||
let preply_arg = std::ptr::from_ref(&reply_arg);
|
||||
fuse_lowlevel::fuse_reply_ioctl(
|
||||
_req,
|
||||
0,
|
||||
preply_arg as *const c_void,
|
||||
std::mem::size_of::<c_uint>(),
|
||||
);
|
||||
}
|
||||
UI_SET_EVBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_EVBIT {}", fh, value);
|
||||
ui_set_evbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_KEYBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_KEYBIT {}", fh, value);
|
||||
ui_set_keybit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_RELBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_RELBIT {}", fh, value);
|
||||
ui_set_relbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_ABSBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_ABSBIT {}", fh, value);
|
||||
ui_set_absbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_MSCBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_MSCBIT {}", fh, value);
|
||||
ui_set_mscbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_LEDBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_LEDBIT {}", fh, value);
|
||||
ui_set_ledbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_SNDBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_SNDBIT {}", fh, value);
|
||||
ui_set_sndbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_FFBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_FFBIT {}", fh, value);
|
||||
ui_set_ffbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_PHYS => {
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
debug!("fh {}: ioctl UI_SET_PHYS", fh);
|
||||
// inbuf is actually a *const c_char, but
|
||||
// but the macro to generate ui_set_phys expects a ptr to the actual data structure.
|
||||
let phys = _in_buf as *const *const c_char;
|
||||
ui_set_phys(fd, phys).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_SWBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_SWBIT {}", fh, value);
|
||||
ui_set_swbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_SET_PROPBIT => {
|
||||
let value = _arg as c_uint;
|
||||
debug!("fh {}: ioctl UI_SET_PROPBIT {}", fh, value);
|
||||
ui_set_propbit(fd, value.into()).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_BEGIN_FF_UPLOAD => {
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
debug!("fh {}: ioctl UI_BEGIN_FF_UPLOAD", fh);
|
||||
let ff_upload_ptr = _in_buf as *mut uinput_ff_upload;
|
||||
debug!("request_id: {:x}", (*ff_upload_ptr).request_id);
|
||||
ui_begin_ff_upload(fd, ff_upload_ptr).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, ff_upload_ptr as *mut c_void, _out_bufsz);
|
||||
}
|
||||
UI_END_FF_UPLOAD => {
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
debug!("fh {}: ioctl UI_END_FF_UPLOAD", fh);
|
||||
let ff_upload_ptr = _in_buf as *const uinput_ff_upload;
|
||||
debug!("request_id: {:x}", (*ff_upload_ptr).request_id);
|
||||
ui_end_ff_upload(fd, ff_upload_ptr).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
UI_BEGIN_FF_ERASE => {
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
debug!("fh {}: ioctl UI_BEGIN_FF_ERASE", fh);
|
||||
let ff_erase_ptr = _in_buf as *mut uinput_ff_erase;
|
||||
debug!("request_id: {:x}", (*ff_erase_ptr).request_id);
|
||||
ui_begin_ff_erase(fd, ff_erase_ptr).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, ff_erase_ptr as *mut c_void, _out_bufsz);
|
||||
}
|
||||
UI_END_FF_ERASE => {
|
||||
assert!(_in_bufsz != 0, "should have _in_bufsz");
|
||||
debug!("fh {}: ioctl UI_END_FF_ERASE", fh);
|
||||
let ff_erase_ptr = _in_buf as *const uinput_ff_erase;
|
||||
debug!("request_id: {:x}", (*ff_erase_ptr).request_id);
|
||||
ui_end_ff_erase(fd, ff_erase_ptr).unwrap();
|
||||
fuse_lowlevel::fuse_reply_ioctl(_req, 0, std::ptr::null(), 0);
|
||||
}
|
||||
_ => {
|
||||
debug!("fh {}: ioctl cmd {}", fh, _cmd);
|
||||
fuse_lowlevel::fuse_reply_err(_req, EBADRQC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Instance of cuse_lowlevel_ops with all stubs assigned.
|
||||
// Setting to None leads to e.g. "write error: Function not implemented".
|
||||
// You can find the implementations of the uinput default (open, release ,read, write, poll,
|
||||
// and ioctl) in uinput_fops of uinput.c.
|
||||
// See: https://github.com/torvalds/linux/blob/master/drivers/input/misc/uinput.c,
|
||||
pub fn vuinput_make_cuse_ops() -> cuse_lowlevel::cuse_lowlevel_ops {
|
||||
cuse_lowlevel::cuse_lowlevel_ops {
|
||||
init: None,
|
||||
init_done: None,
|
||||
destroy: None,
|
||||
open: Some(vuinput_open),
|
||||
read: None,
|
||||
write: Some(vuinput_write),
|
||||
flush: None,
|
||||
release: Some(vuinput_release),
|
||||
fsync: None,
|
||||
ioctl: Some(vuinput_ioctl),
|
||||
poll: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
|
||||
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
VUINPUT_STATE.set(RwLock::new(HashMap::new())).unwrap();
|
||||
VUINPUT_COUNTER.set(AtomicU64::new(3)).unwrap();
|
||||
JOB_DISPATCHER.set(Mutex::new(Dispatcher::new())).unwrap();
|
||||
SELF_NAMESPACES.set(get_namespaces(None)).unwrap();
|
||||
JOB_DISPATCHER.get().unwrap().lock().unwrap().dispatch(Box::new(MonitorBackgroundLoop::new()));
|
||||
|
||||
info!("Starting vuinputd");
|
||||
|
||||
let cuse_ops = vuinput_make_cuse_ops();
|
||||
|
||||
let vuinput_devicename = CString::new(format!("DEVNAME=vuinput")).unwrap();
|
||||
|
||||
let mut dev_info_argv: Vec<*const c_char> = vec![
|
||||
vuinput_devicename.as_ptr(), // pointer to the C string
|
||||
std::ptr::null(), // null terminator, often required by C APIs
|
||||
];
|
||||
|
||||
// setting dev_major and dev_minor to 0 leads to a dynamic assignment of the major and minor, very likely beginning with 234:0
|
||||
// see in https://www.kernel.org/doc/Documentation/admin-guide/devices.txt
|
||||
// major 120 is reserved for local/experimental use. I picked minor 414795 with the use
|
||||
// of a random number generator to omit conflicts.
|
||||
let ci = cuse_lowlevel::cuse_info {
|
||||
dev_major: 120,
|
||||
dev_minor: 414795,
|
||||
dev_info_argc: 1,
|
||||
dev_info_argv: dev_info_argv.as_mut_ptr(),
|
||||
flags: cuse_lowlevel::CUSE_UNRESTRICTED_IOCTL,
|
||||
};
|
||||
|
||||
let arg_program_name = CString::new(args[0].clone()).unwrap();
|
||||
let parg_program_name = arg_program_name.into_raw();
|
||||
let arg_foreground = CString::new("-f").unwrap();
|
||||
let parg_foreground = arg_foreground.into_raw();
|
||||
let arg_singlethreaded = CString::new("-s").unwrap();
|
||||
let parg_singlethreaded = arg_singlethreaded.into_raw();
|
||||
let mut stripped_argv: Vec<*mut c_char> = vec![
|
||||
parg_program_name,
|
||||
parg_foreground,
|
||||
parg_singlethreaded,
|
||||
std::ptr::null_mut(), // null terminator, often required by C APIs
|
||||
];
|
||||
|
||||
unsafe {
|
||||
cuse_lowlevel::cuse_lowlevel_main(
|
||||
3,
|
||||
stripped_argv.as_mut_ptr(),
|
||||
&ci,
|
||||
&cuse_ops,
|
||||
std::ptr::null_mut(),
|
||||
);
|
||||
let _reclaim_arg_program_name = CString::from_raw(parg_program_name);
|
||||
let _reclaim_arg_foreground = CString::from_raw(parg_foreground);
|
||||
let _reclaim_arg_foreground = CString::from_raw(parg_singlethreaded);
|
||||
}
|
||||
info!("Stopping vuinputd");
|
||||
JOB_DISPATCHER.get().unwrap().lock().unwrap().close();
|
||||
JOB_DISPATCHER.get().unwrap().lock().unwrap().wait_until_finished();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
343
vuinputd/src/monitor_udev.rs
Normal file
343
vuinputd/src/monitor_udev.rs
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
future::Future,
|
||||
os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex, OnceLock,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use async_io::Async;
|
||||
use libudev::Monitor;
|
||||
use log::debug;
|
||||
use regex::Regex;
|
||||
|
||||
use crate::jobs::job::{Job, JobTarget};
|
||||
|
||||
// === Basic types ===
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EventKind {
|
||||
Add,
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UdevEvent {
|
||||
pub syspath: String,
|
||||
pub seqnum: u64,
|
||||
pub kind: EventKind,
|
||||
pub payload: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Entry {
|
||||
pub syspath: String,
|
||||
pub seqnum: u64,
|
||||
pub add_data: Option<HashMap<String, String>>,
|
||||
pub remove_data: Option<HashMap<String, String>>,
|
||||
pub add_processed: bool,
|
||||
pub tombstone: bool,
|
||||
pub last_update: Instant,
|
||||
}
|
||||
|
||||
// === EventStore ===
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventStore {
|
||||
entries: HashMap<String, Entry>,
|
||||
ttl: Duration,
|
||||
}
|
||||
|
||||
impl EventStore {
|
||||
pub fn new(ttl: Duration) -> Self {
|
||||
Self {
|
||||
entries: HashMap::new(),
|
||||
ttl,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on_event(&mut self, event: UdevEvent) {
|
||||
let now = Instant::now();
|
||||
let e = self
|
||||
.entries
|
||||
.entry(event.syspath.clone())
|
||||
.or_insert_with(|| Entry {
|
||||
syspath: event.syspath.clone(),
|
||||
seqnum: event.seqnum,
|
||||
add_data: None,
|
||||
remove_data: None,
|
||||
add_processed: false,
|
||||
tombstone: false,
|
||||
last_update: now,
|
||||
});
|
||||
|
||||
e.seqnum = event.seqnum;
|
||||
e.last_update = now;
|
||||
e.tombstone = false;
|
||||
|
||||
match event.kind {
|
||||
EventKind::Add => {
|
||||
e.add_data = Some(event.payload);
|
||||
e.add_processed = false;
|
||||
e.remove_data = None;
|
||||
}
|
||||
EventKind::Remove => {
|
||||
e.remove_data = Some(event.payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take(&mut self, syspath: &str) -> Option<Entry> {
|
||||
let e = self.entries.get_mut(syspath)?;
|
||||
|
||||
let result = e.clone();
|
||||
|
||||
if e.tombstone {
|
||||
return Some(result);
|
||||
}
|
||||
|
||||
if !e.add_processed {
|
||||
e.add_processed = true;
|
||||
}
|
||||
if e.remove_data.is_some() {
|
||||
e.tombstone = true;
|
||||
}
|
||||
|
||||
Some(result)
|
||||
}
|
||||
|
||||
pub fn cleanup(&mut self) {
|
||||
let now = Instant::now();
|
||||
self.entries.retain(|_, e| {
|
||||
if e.tombstone {
|
||||
return false;
|
||||
}
|
||||
now.duration_since(e.last_update) < self.ttl
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// === Global store ===
|
||||
|
||||
pub static EVENT_STORE: OnceLock<Arc<Mutex<EventStore>>> = OnceLock::new();
|
||||
|
||||
pub struct MonitorBackgroundLoop {
|
||||
}
|
||||
impl MonitorBackgroundLoop {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Job for MonitorBackgroundLoop {
|
||||
fn desc(&self) -> &str {
|
||||
"Monitor udev events"
|
||||
}
|
||||
|
||||
fn execute_after_cancellation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn create_task(self: &MonitorBackgroundLoop) -> Pin<Box<dyn Future<Output = ()>>> {
|
||||
let cancel_token: Arc<AtomicBool> = Arc::new(AtomicBool::new(false));
|
||||
Box::pin(udev_monitor_loop(cancel_token))
|
||||
}
|
||||
|
||||
fn job_target(&self) -> JobTarget {
|
||||
JobTarget::BackgroundLoop
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn udev_monitor_loop(cancel_token: Arc<AtomicBool>) {
|
||||
// Clone a reference to the shared store which should already be initialized in main.
|
||||
|
||||
// Initialize shared store
|
||||
let store = Arc::new(Mutex::new(EventStore::new(Duration::from_secs(60))));
|
||||
EVENT_STORE.set(store.clone()).unwrap();
|
||||
|
||||
// Create monitor that listens for kernel events.
|
||||
// Use match_subsystem to filter for "input" subsystem as requested.
|
||||
debug!("Monitor started");
|
||||
let mut next_cleanup = Instant::now() + Duration::from_secs(60);
|
||||
|
||||
let context = libudev::Context::new().unwrap();
|
||||
let mut monitor = Monitor::new(&context).unwrap();
|
||||
monitor.match_subsystem("input").unwrap();
|
||||
let mut monitor_socket = monitor.listen().expect("Failed to create udev monitor");
|
||||
|
||||
// Wrap the monitor in a small AsFd adapter
|
||||
struct FdWrap(RawFd);
|
||||
impl AsRawFd for FdWrap {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
impl AsFd for FdWrap {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
// SAFETY: FdWrap owns the fd and lives long enough
|
||||
unsafe { BorrowedFd::borrow_raw(self.0) }
|
||||
}
|
||||
}
|
||||
|
||||
let async_monitor = Async::new(FdWrap(monitor_socket.as_raw_fd())).unwrap();
|
||||
|
||||
let re = Regex::new(r"^/devices/virtual/input/input(\d+)/event(\d+)$").unwrap();
|
||||
|
||||
loop {
|
||||
// check cancel token first
|
||||
if cancel_token.load(Ordering::Relaxed) {
|
||||
debug!("Cancellation requested, shutting down udev monitor thread.");
|
||||
break;
|
||||
}
|
||||
debug!("Waiting for event");
|
||||
async_monitor.readable().await.unwrap();
|
||||
debug!("Event registered");
|
||||
|
||||
if let Some(event) = monitor_socket.receive_event() {
|
||||
let mut properties: HashMap<_, _> = HashMap::new();
|
||||
for property in event.properties() {
|
||||
let key: String = property.name().to_str().unwrap().to_string();
|
||||
let key = match key.as_str() {
|
||||
"ID_VUINPUT_KEYBOARD" => "ID_INPUT_KEYBOARD".to_string(),
|
||||
"ID_VUINPUT_MOUSE" => "ID_INPUT_MOUSE".to_string(),
|
||||
_ => key
|
||||
};
|
||||
|
||||
let value: String = property.value().to_str().unwrap().to_string();
|
||||
if key!="ID_SEAT" {
|
||||
properties.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
let value_of_devpath = properties.get("DEVPATH").unwrap();
|
||||
|
||||
if let Some(caps) = re.captures(value_of_devpath) {
|
||||
// result is something like /devices/virtual/input/input126/event9
|
||||
// println!("devpath {}",value_of_devpath);
|
||||
let input_number: u32 = caps[1].parse().unwrap();
|
||||
// let event_number: u32 = caps[2].parse().unwrap();
|
||||
let syspath = format!("/sys/devices/virtual/input/input{}", input_number);
|
||||
let seqnum: u64 = properties.get("SEQNUM").unwrap().parse().unwrap();
|
||||
let kind = match properties.get("ACTION").unwrap().as_str() {
|
||||
"ADD" => EventKind::Add,
|
||||
"REMOVE" => EventKind::Remove,
|
||||
_ => EventKind::Add,
|
||||
};
|
||||
|
||||
let mut event_store = EVENT_STORE.get().unwrap().lock().unwrap();
|
||||
let udev_event = UdevEvent {
|
||||
syspath: syspath,
|
||||
seqnum: seqnum,
|
||||
kind: kind,
|
||||
payload: properties,
|
||||
};
|
||||
event_store.on_event(udev_event);
|
||||
}
|
||||
}
|
||||
|
||||
if Instant::now() > next_cleanup {
|
||||
next_cleanup = Instant::now() + Duration::from_secs(60);
|
||||
EVENT_STORE.get().unwrap().lock().unwrap().cleanup();
|
||||
}
|
||||
} // loop
|
||||
|
||||
debug!("udev monitor thread exiting.");
|
||||
}
|
||||
|
||||
// === Example threads ===
|
||||
/*
|
||||
fn producer_thread(stop: Arc<AtomicBool>) {
|
||||
let store = EVENT_STORE.get().unwrap().clone();
|
||||
let mut seq = 0u64;
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
// Simulate some udev events
|
||||
let syspath = if seq % 2 == 0 {
|
||||
"/devices/input/event9"
|
||||
} else {
|
||||
"/devices/input/event10"
|
||||
};
|
||||
|
||||
let kind = if seq % 3 == 0 {
|
||||
EventKind::Remove
|
||||
} else {
|
||||
EventKind::Add
|
||||
};
|
||||
|
||||
let event = UdevEvent {
|
||||
syspath: syspath.to_string(),
|
||||
seqnum: seq,
|
||||
kind,
|
||||
payload: format!("payload for seq {seq}"),
|
||||
};
|
||||
|
||||
{
|
||||
let mut guard = store.lock().unwrap();
|
||||
guard.on_event(event);
|
||||
}
|
||||
|
||||
seq += 1;
|
||||
thread::sleep(Duration::from_millis(400));
|
||||
}
|
||||
|
||||
println!("[producer] exiting");
|
||||
}
|
||||
|
||||
fn consumer_thread(stop: Arc<AtomicBool>) {
|
||||
let store = EVENT_STORE.get().unwrap().clone();
|
||||
|
||||
while !stop.load(Ordering::Relaxed) {
|
||||
{
|
||||
let mut guard = store.lock().unwrap();
|
||||
|
||||
// Example: iterate over all known syspaths
|
||||
let syspaths: Vec<String> = guard.entries.keys().cloned().collect();
|
||||
for syspath in syspaths {
|
||||
if let Some(entry) = guard.take(&syspath) {
|
||||
println!("[consumer] Got actionable event: {:?}", entry);
|
||||
}
|
||||
}
|
||||
|
||||
guard.cleanup();
|
||||
}
|
||||
|
||||
thread::sleep(Duration::from_millis(200));
|
||||
}
|
||||
|
||||
println!("[consumer] exiting");
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Initialize shared store
|
||||
let store = Arc::new(Mutex::new(EventStore::new(Duration::from_secs(60))));
|
||||
EVENT_STORE.set(store.clone()).unwrap();
|
||||
|
||||
// Shared stop flag
|
||||
let stop_flag = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let p_stop = stop_flag.clone();
|
||||
let producer = thread::spawn(move || producer_thread(p_stop));
|
||||
|
||||
let c_stop = stop_flag.clone();
|
||||
let consumer = thread::spawn(move || consumer_thread(c_stop));
|
||||
|
||||
// Let it run for 5 seconds
|
||||
thread::sleep(Duration::from_secs(5));
|
||||
stop_flag.store(true, Ordering::Relaxed);
|
||||
|
||||
producer.join().unwrap();
|
||||
consumer.join().unwrap();
|
||||
|
||||
println!("Main exiting");
|
||||
}
|
||||
*/
|
||||
118
vuinputd/src/namespace.rs
Normal file
118
vuinputd/src/namespace.rs
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Author: Johannes Leupolz <dev@leupolz.eu>
|
||||
|
||||
use nix::{
|
||||
sched::{setns, CloneFlags},
|
||||
unistd::{fork, ForkResult, Pid},
|
||||
};
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
os::fd::AsFd,
|
||||
};
|
||||
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Namespaces {
|
||||
pub nspath: String,
|
||||
pub net: Option<u64>,
|
||||
pub uts: Option<u64>,
|
||||
pub ipc: Option<u64>,
|
||||
pub pid: Option<u64>,
|
||||
pub pid_for_children: Option<u64>,
|
||||
pub user: Option<u64>,
|
||||
pub mnt: Option<u64>,
|
||||
pub cgroup: Option<u64>,
|
||||
pub time: Option<u64>,
|
||||
pub time_for_children: Option<u64>,
|
||||
}
|
||||
|
||||
impl Namespaces {
|
||||
pub fn equal_mnt_and_net(&self, other: &Namespaces) -> bool {
|
||||
self.mnt == other.mnt && self.net == other.net
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Namespaces {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
writeln!(f, "Namespaces:")?;
|
||||
writeln!(f, " net: {:?}", self.net)?;
|
||||
writeln!(f, " uts: {:?}", self.uts)?;
|
||||
writeln!(f, " ipc: {:?}", self.ipc)?;
|
||||
writeln!(f, " pid: {:?}", self.pid)?;
|
||||
writeln!(f, " pid_for_children: {:?}", self.pid_for_children)?;
|
||||
writeln!(f, " user: {:?}", self.user)?;
|
||||
writeln!(f, " mnt: {:?}", self.mnt)?;
|
||||
writeln!(f, " cgroup: {:?}", self.cgroup)?;
|
||||
writeln!(f, " time: {:?}", self.time)?;
|
||||
writeln!(f, " time_for_children: {:?}", self.time_for_children)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_namespaces(pid: Option<i32>) -> Namespaces {
|
||||
let pid: String = match pid {
|
||||
Some(pid) => pid.to_string(),
|
||||
None => "self".to_string(),
|
||||
};
|
||||
let nspath = format!("/proc/{}/ns", pid);
|
||||
|
||||
let mut ns = Namespaces {
|
||||
nspath: nspath.clone(),
|
||||
net: None,
|
||||
uts: None,
|
||||
ipc: None,
|
||||
pid: None,
|
||||
pid_for_children: None,
|
||||
user: None,
|
||||
mnt: None,
|
||||
cgroup: None,
|
||||
time: None,
|
||||
time_for_children: None,
|
||||
};
|
||||
|
||||
for entry in fs::read_dir(&nspath).expect("proc not found") {
|
||||
let entry = entry.expect("`msg`");
|
||||
let link = fs::read_link(entry.path()).expect("problem parsing inode");
|
||||
let link_str = link.to_string_lossy();
|
||||
if let (Some(start), Some(end)) = (link_str.find('['), link_str.find(']')) {
|
||||
if let Ok(inode) = link_str[start + 1..end].parse::<u64>() {
|
||||
match entry.file_name().into_string().unwrap_or_default().as_str() {
|
||||
"net" => ns.net = Some(inode),
|
||||
"uts" => ns.uts = Some(inode),
|
||||
"ipc" => ns.ipc = Some(inode),
|
||||
"pid" => ns.pid = Some(inode),
|
||||
"pid_for_children" => ns.pid_for_children = Some(inode),
|
||||
"user" => ns.user = Some(inode),
|
||||
"mnt" => ns.mnt = Some(inode),
|
||||
"cgroup" => ns.cgroup = Some(inode),
|
||||
"time" => ns.time = Some(inode),
|
||||
"time_for_children" => ns.time_for_children = Some(inode),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ns
|
||||
}
|
||||
|
||||
/// Runs a function inside the given network and mount namespaces.
|
||||
/// Returns the child PID so the caller can `waitpid` on it.
|
||||
pub fn run_in_net_and_mnt_namespace(ns: Namespaces, func: Box<dyn Fn()>) -> nix::Result<Pid> {
|
||||
//Note: The child process is created with a single thread—the one that called fork().
|
||||
match unsafe { fork()? } {
|
||||
ForkResult::Parent { child } => {
|
||||
// Parent: return the PID of the child
|
||||
Ok(child)
|
||||
}
|
||||
ForkResult::Child => {
|
||||
// enter namespace
|
||||
let net = File::open(ns.nspath.clone() + "/net").expect("net not found");
|
||||
let mnt = File::open(ns.nspath.clone() + "/mnt").expect("mnt not found");
|
||||
setns(net.as_fd(), CloneFlags::CLONE_NEWNET).expect("couldn't enter net");
|
||||
setns(mnt.as_fd(), CloneFlags::CLONE_NEWNS).expect("couldn't enter mnt");
|
||||
// execute your function
|
||||
func();
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
20
vuinputd/systemd/vuinputd.service
Normal file
20
vuinputd/systemd/vuinputd.service
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
[Unit]
|
||||
Description=uinput proxy
|
||||
After=systemd-udevd.service
|
||||
Requires=systemd-udevd.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/vuinputd
|
||||
Restart=on-failure
|
||||
|
||||
# Needs CAP_SYS_ADMIN for CUSE + /dev/uinput (I am still missing a capability for the correct working mode)
|
||||
#CapabilityBoundingSet=CAP_SYS_ADMIN CAP_MKNOD CAP_DAC_OVERRIDE CAP_FOWNER
|
||||
|
||||
# Allow full privileges (dangerous, but okay for debugging)
|
||||
CapabilityBoundingSet=~ # remove all limits
|
||||
DeviceAllow=/dev/uinput rw
|
||||
DeviceAllow=/dev/cuse rw
|
||||
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
47
vuinputd/udev/90-vuinputd-protect.rules
Normal file
47
vuinputd/udev/90-vuinputd-protect.rules
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# ===========================================================
|
||||
# Default permissions for /dev/vuinput
|
||||
# -----------------------------------------------------------
|
||||
# Rule details:
|
||||
# For now, everyone can use it.
|
||||
|
||||
SUBSYSTEM=="cuse", KERNEL=="vuinput", MODE="0666"
|
||||
|
||||
# ===========================================================
|
||||
# Cleanup rule for our virtual keyboards
|
||||
# -----------------------------------------------------------
|
||||
# Purpose:
|
||||
# The builtin input_id sets ID_INPUT_KEYBOARD=1 by default.
|
||||
# We want our virtual keyboards to be treated differently,
|
||||
# so we clear the default keyboard flag.
|
||||
#
|
||||
# Rule details:
|
||||
# - SUBSYSTEM=="input" -> matches input devices
|
||||
# - KERNEL=="event*" -> matches event nodes
|
||||
# - ENV{ID_VUINPUT}=="1" -> only affects our virtual keyboards
|
||||
# - ENV{ID_INPUT_KEYBOARD}="" -> clears the default keyboard flag
|
||||
# - ENV{ID_SEAT}="seat_vuinput" -> assign to virtual seat for vuinput
|
||||
#
|
||||
# Rule ordering:
|
||||
# - This runs after the hwdb entry and input_id builtin rules.
|
||||
# - Ensures other keyboards are unaffected.
|
||||
#
|
||||
# Update procedure after editing:
|
||||
# 1. sudo udevadm control --reload
|
||||
# 2. sudo udevadm trigger -s input
|
||||
# To check seat status:
|
||||
# loginctl seat-status
|
||||
#
|
||||
# Note:
|
||||
# - Quote from logind: Seats are identified by seat names, which are
|
||||
# strings (<= 255 characters), that start with the four characters "seat"
|
||||
# followed by at least one character from the range [a-zA-Z0-9], "_" and "-".
|
||||
# - Even though the device is listed under the seat, without a graphical device,
|
||||
# or a master-of-seat-tag, the seat won't be created and won't disturb.
|
||||
# - in libinput, ID_INPUT_KEY leads to EVDEV_UDEV_TAG_KEYBOARD, which means
|
||||
# that a device is tagged as keyboard. We don't want that for the host system.
|
||||
|
||||
SUBSYSTEMS=="input", ENV{ID_VUINPUT}=="1", ENV{ID_INPUT_KEYBOARD}=="1" \
|
||||
ENV{ID_VUINPUT_KEYBOARD}="1", ENV{ID_INPUT_KEYBOARD}="", ENV{ID_SEAT}="seat_vuinput"
|
||||
|
||||
SUBSYSTEMS=="input", ENV{ID_VUINPUT}=="1", ENV{ID_INPUT_MOUSE}=="1" \
|
||||
ENV{ID_VUINPUT_MOUSE}="1", ENV{ID_INPUT_MOUSE}="", ENV{ID_SEAT}="seat_vuinput"
|
||||
32
vuinputd/udev/90-vuinputd.hwdb
Normal file
32
vuinputd/udev/90-vuinputd.hwdb
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# ===========================================================
|
||||
# Custom HWDB entry for BEEF:DEAD virtual devices
|
||||
# -----------------------------------------------------------
|
||||
# Purpose:
|
||||
# This entry targets the virtual devices created via uinput
|
||||
# with vendor_id=0xBEEF and product_id=0xDEAD.
|
||||
#
|
||||
# Why HWDB is used:
|
||||
# - uinput devices are virtual, so ATTRS{idVendor} / ATTRS{idProduct}
|
||||
# may not always be reliable in udev rules due to timing/order.
|
||||
# - HWDB entries are applied after the builtin input_id, ensuring
|
||||
# that the modalias is available and can be matched reliably.
|
||||
# - Using the modalias avoids problems with virtual sysfs attributes.
|
||||
#
|
||||
# Properties set:
|
||||
# - ID_VUINPUT=1 -> marks the device as our virtual device
|
||||
#
|
||||
# Notes:
|
||||
# - The first rule adds to the input device, e.g.,
|
||||
# /devices/virtual/input/input92.
|
||||
# - The second rule adds to the event device, e.g.,
|
||||
# /devices/virtual/input/input92/event9
|
||||
#
|
||||
# Update procedure after editing:
|
||||
# 1. sudo systemd-hwdb update
|
||||
# ===========================================================
|
||||
|
||||
evdev:input:b0003vBEEFpDEADe0000-*
|
||||
ID_VUINPUT=1
|
||||
|
||||
input:b0003vBEEFpDEADe0000-*
|
||||
ID_VUINPUT=1
|
||||
Loading…
Add table
Add a link
Reference in a new issue