cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/hero-release-note
goq / crates / sigil-host / src / input / linux.rs
linux.rs 23.1K
1use std::collections::BTreeSet;2use std::fs::{File, OpenOptions};3use std::os::fd::AsRawFd;4use std::os::unix::fs::{FileTypeExt, MetadataExt, OpenOptionsExt};56use anyhow::{Context, Result, ensure};7use input_linux::{8 AbsoluteAxis, AbsoluteEvent, AbsoluteInfo, AbsoluteInfoSetup, EventKind, EventTime, InputId,9 InputProperty, Key, KeyEvent, KeyState, RelativeAxis, RelativeEvent, SynchronizeEvent,10 UInputHandle,11};12use sigil_protocol::{13 GAMEPAD_AXIS_MAX, GAMEPAD_AXIS_MIN, GAMEPAD_TRIGGER_MAX, GamepadState,14 InputEvent as ProtocolInputEvent,15};1617use crate::config::UinputConfig;18use crate::input::acl::{POSIX_ACL_REQUIRED_BYTES, validate_single_user_access_acl};19use crate::input::keymap::{20 ALL_MAPPED_KEYS, GamepadAxis, GamepadButton, InputDeviceClass, PointerReportEvent,21 input_device_class, linux_key, map_gamepad_state, map_key, pointer_position_sync_report,22};23use crate::input::{24 GAMEPAD_DEVICE_NAME, GAMEPAD_PRODUCT_ID, KEYBOARD_DEVICE_NAME, KEYBOARD_PRODUCT_ID,25 POINTER_DEVICE_NAME, POINTER_PRODUCT_ID, UINPUT_BUS_TYPE, UINPUT_DEVICE_VERSION,26 UINPUT_VENDOR_ID,27};2829const UINPUT_MAJOR: u32 = 10;30const UINPUT_MINOR: u32 = 223;31const MAX_POINTER_EVENTS: usize = 6;32const MAX_KEYBOARD_EVENTS: usize = ALL_MAPPED_KEYS.len() + 1;33const GAMEPAD_REPORT_EVENTS: usize = 20;3435pub struct UinputDevice {36 pointer: PointerDevice,37 keyboard: KeyboardDevice,38 gamepad: GamepadDevice,39}4041impl UinputDevice {42 pub fn open(config: &UinputConfig) -> Result<Self> {43 // Each virtual device owns a distinct validated descriptor. This44 // keeps libinput/udev classification from merging keyboard keys45 // into the conventional relative pointer topology.46 let pointer = PointerDevice::open(config)?;47 let keyboard = KeyboardDevice::open(config)?;48 let gamepad = GamepadDevice::open(config)?;4950 Ok(Self {51 pointer,52 keyboard,53 gamepad,54 })55 }5657 pub fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {58 match input_device_class(event) {59 Some(InputDeviceClass::Pointer) => self.pointer.apply(event),60 Some(InputDeviceClass::Keyboard) => self.keyboard.apply(event),61 Some(InputDeviceClass::Gamepad) => match event {62 ProtocolInputEvent::Gamepad { state } => self.gamepad.apply(state),63 _ => unreachable!("input device classification is exhaustive"),64 },65 None => Ok(()),66 }67 }6869 pub fn release_all(&mut self) -> Result<()> {70 // Evaluate every release before combining results so one broken71 // descriptor cannot strand held state on either sibling device.72 let pointer_result = self.pointer.release_all();73 let keyboard_result = self.keyboard.release_all();74 let gamepad_result = self.gamepad.neutralize();75 pointer_result.and(keyboard_result).and(gamepad_result)76 }77}7879struct PointerDevice {80 handle: UInputHandle<File>,81 pressed: BTreeSet<Key>,82}8384impl PointerDevice {85 fn open(config: &UinputConfig) -> Result<Self> {86 let handle = open_validated_handle(config, "pointer")?;87 handle88 .set_propbit(InputProperty::Pointer)89 .context("enabling pointer property")?;90 handle91 .set_evbit(EventKind::Key)92 .context("enabling pointer button events")?;93 for key in [Key::ButtonLeft, Key::ButtonRight, Key::ButtonMiddle] {94 handle95 .set_keybit(key)96 .context("enabling a bounded pointer button capability")?;97 }98 handle99 .set_evbit(EventKind::Relative)100 .context("enabling relative pointer events")?;101 handle102 .set_relbit(RelativeAxis::X)103 .context("enabling relative X events")?;104 handle105 .set_relbit(RelativeAxis::Y)106 .context("enabling relative Y events")?;107 handle108 .set_relbit(RelativeAxis::Wheel)109 .context("enabling vertical wheel events")?;110 handle111 .set_relbit(RelativeAxis::HorizontalWheel)112 .context("enabling horizontal wheel events")?;113 handle114 .create(115 &InputId {116 bustype: UINPUT_BUS_TYPE,117 vendor: UINPUT_VENDOR_ID,118 product: POINTER_PRODUCT_ID,119 version: UINPUT_DEVICE_VERSION,120 },121 POINTER_DEVICE_NAME,122 0,123 &[],124 )125 .context("registering Sigil virtual pointer")?;126127 Ok(Self {128 handle,129 pressed: BTreeSet::new(),130 })131 }132133 fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {134 match event {135 ProtocolInputEvent::MouseMoveRelative { dx, dy } => {136 let mut events = [synchronize_event(); 3];137 let mut len = 0;138 if *dx != 0 {139 events[len] = relative_event(RelativeAxis::X, *dx);140 len += 1;141 }142 if *dy != 0 {143 events[len] = relative_event(RelativeAxis::Y, *dy);144 len += 1;145 }146 if len == 0 {147 return Ok(());148 }149 events[len] = synchronize_event();150 self.emit(&events[..=len])151 }152 ProtocolInputEvent::MousePositionSync { x, y } => {153 let events = pointer_position_sync_report(*x, *y).map(|event| match event {154 PointerReportEvent::RelativeX(value) => relative_event(RelativeAxis::X, value),155 PointerReportEvent::RelativeY(value) => relative_event(RelativeAxis::Y, value),156 PointerReportEvent::Synchronize => synchronize_event(),157 });158 self.emit(&events)159 }160 ProtocolInputEvent::MouseMove { .. } => anyhow::bail!(161 "absolute pointer movement is unavailable on the relative-only uinput device"162 ),163 ProtocolInputEvent::MouseClick { b } => {164 self.click(mouse_button(*b).context("unsupported mouse button")?)165 }166 ProtocolInputEvent::MouseDown { b } => {167 self.press(mouse_button(*b).context("unsupported mouse button")?)168 }169 ProtocolInputEvent::MouseUp { b } => {170 self.release(mouse_button(*b).context("unsupported mouse button")?)171 }172 ProtocolInputEvent::MouseScroll { dx, dy } => {173 let vertical = dy174 .checked_neg()175 .context("vertical wheel delta is out of range")?;176 let mut events = [synchronize_event(); 3];177 let mut len = 0;178 if *dx != 0 {179 events[len] = relative_event(RelativeAxis::HorizontalWheel, *dx);180 len += 1;181 }182 if vertical != 0 {183 events[len] = relative_event(RelativeAxis::Wheel, vertical);184 len += 1;185 }186 if len == 0 {187 return Ok(());188 }189 events[len] = synchronize_event();190 self.emit(&events[..=len])191 }192 _ => unreachable!("only pointer events are routed to the pointer device"),193 }194 }195196 fn press(&mut self, key: Key) -> Result<()> {197 self.emit(&[key_event(key, KeyState::PRESSED), synchronize_event()])?;198 self.pressed.insert(key);199 Ok(())200 }201202 fn release(&mut self, key: Key) -> Result<()> {203 self.emit(&[key_event(key, KeyState::RELEASED), synchronize_event()])?;204 self.pressed.remove(&key);205 Ok(())206 }207208 fn click(&mut self, key: Key) -> Result<()> {209 ensure!(210 !self.pressed.contains(&key),211 "click transition is invalid while the key or button is held"212 );213 self.emit(&[214 key_event(key, KeyState::PRESSED),215 synchronize_event(),216 key_event(key, KeyState::RELEASED),217 synchronize_event(),218 ])219 }220221 fn release_all(&mut self) -> Result<()> {222 if self.pressed.is_empty() {223 return Ok(());224 }225 let mut events = Vec::with_capacity(self.pressed.len() + 1);226 events.extend(227 self.pressed228 .iter()229 .copied()230 .map(|key| key_event(key, KeyState::RELEASED)),231 );232 events.push(synchronize_event());233 ensure!(234 events.len() <= MAX_POINTER_EVENTS,235 "pointer release set exceeded its static bound"236 );237 self.emit(&events)?;238 self.pressed.clear();239 Ok(())240 }241242 fn emit(&self, events: &[input_linux::sys::input_event]) -> Result<()> {243 ensure!(244 !events.is_empty() && events.len() <= MAX_POINTER_EVENTS,245 "pointer event batch is outside its static bound"246 );247 let written = self248 .handle249 .write(events)250 .context("writing nonblocking pointer event batch")?;251 ensure!(252 written == events.len(),253 "uinput accepted only {written} of {} pointer events",254 events.len()255 );256 Ok(())257 }258}259260impl Drop for PointerDevice {261 fn drop(&mut self) {262 let _ = self.release_all();263 let _ = self.handle.dev_destroy();264 }265}266267struct KeyboardDevice {268 handle: UInputHandle<File>,269 pressed: BTreeSet<Key>,270}271272impl KeyboardDevice {273 fn open(config: &UinputConfig) -> Result<Self> {274 let handle = open_validated_handle(config, "keyboard")?;275 handle276 .set_evbit(EventKind::Key)277 .context("enabling keyboard key events")?;278 for key in ALL_MAPPED_KEYS.iter().copied().map(linux_key) {279 handle280 .set_keybit(key)281 .context("enabling a bounded keyboard key capability")?;282 }283 handle284 .create(285 &InputId {286 bustype: UINPUT_BUS_TYPE,287 vendor: UINPUT_VENDOR_ID,288 product: KEYBOARD_PRODUCT_ID,289 version: UINPUT_DEVICE_VERSION,290 },291 KEYBOARD_DEVICE_NAME,292 0,293 &[],294 )295 .context("registering Sigil virtual keyboard")?;296297 Ok(Self {298 handle,299 pressed: BTreeSet::new(),300 })301 }302303 fn apply(&mut self, event: &ProtocolInputEvent) -> Result<()> {304 match event {305 ProtocolInputEvent::KeyDown { k } => {306 self.press(linux_key(map_key(k).context("unsupported keyboard key")?))307 }308 ProtocolInputEvent::KeyUp { k } => {309 self.release(linux_key(map_key(k).context("unsupported keyboard key")?))310 }311 ProtocolInputEvent::KeyClick { k } => {312 self.click(linux_key(map_key(k).context("unsupported keyboard key")?))313 }314 _ => unreachable!("only keyboard events are routed to the keyboard device"),315 }316 }317318 fn press(&mut self, key: Key) -> Result<()> {319 self.emit(&[key_event(key, KeyState::PRESSED), synchronize_event()])?;320 self.pressed.insert(key);321 Ok(())322 }323324 fn release(&mut self, key: Key) -> Result<()> {325 self.emit(&[key_event(key, KeyState::RELEASED), synchronize_event()])?;326 self.pressed.remove(&key);327 Ok(())328 }329330 fn click(&mut self, key: Key) -> Result<()> {331 ensure!(332 !self.pressed.contains(&key),333 "click transition is invalid while the key is held"334 );335 self.emit(&[336 key_event(key, KeyState::PRESSED),337 synchronize_event(),338 key_event(key, KeyState::RELEASED),339 synchronize_event(),340 ])341 }342343 fn release_all(&mut self) -> Result<()> {344 if self.pressed.is_empty() {345 return Ok(());346 }347 let mut events = Vec::with_capacity(self.pressed.len() + 1);348 events.extend(349 self.pressed350 .iter()351 .copied()352 .map(|key| key_event(key, KeyState::RELEASED)),353 );354 events.push(synchronize_event());355 ensure!(356 events.len() <= MAX_KEYBOARD_EVENTS,357 "keyboard release set exceeded its static bound"358 );359 self.emit(&events)?;360 self.pressed.clear();361 Ok(())362 }363364 fn emit(&self, events: &[input_linux::sys::input_event]) -> Result<()> {365 ensure!(366 !events.is_empty() && events.len() <= MAX_KEYBOARD_EVENTS,367 "keyboard event batch is outside its static bound"368 );369 let written = self370 .handle371 .write(events)372 .context("writing nonblocking keyboard event batch")?;373 ensure!(374 written == events.len(),375 "uinput accepted only {written} of {} keyboard events",376 events.len()377 );378 Ok(())379 }380}381382impl Drop for KeyboardDevice {383 fn drop(&mut self) {384 let _ = self.release_all();385 let _ = self.handle.dev_destroy();386 }387}388389struct GamepadDevice {390 handle: UInputHandle<File>,391}392393impl GamepadDevice {394 fn open(config: &UinputConfig) -> Result<Self> {395 let handle = open_validated_handle(config, "gamepad")?;396397 handle398 .set_evbit(EventKind::Key)399 .context("enabling gamepad key events")?;400 for (button, _) in map_gamepad_state(&GamepadState::default()).buttons {401 handle402 .set_keybit(linux_gamepad_button(button))403 .context("enabling a bounded gamepad button capability")?;404 }405 handle406 .set_evbit(EventKind::Absolute)407 .context("enabling gamepad absolute-axis events")?;408 for (axis, _) in map_gamepad_state(&GamepadState::default()).axes {409 handle410 .set_absbit(linux_gamepad_axis(axis))411 .context("enabling a bounded gamepad axis capability")?;412 }413414 let axes = [415 absolute_range(416 AbsoluteAxis::X,417 i32::from(GAMEPAD_AXIS_MIN),418 i32::from(GAMEPAD_AXIS_MAX),419 ),420 absolute_range(421 AbsoluteAxis::Y,422 i32::from(GAMEPAD_AXIS_MIN),423 i32::from(GAMEPAD_AXIS_MAX),424 ),425 absolute_range(426 AbsoluteAxis::RX,427 i32::from(GAMEPAD_AXIS_MIN),428 i32::from(GAMEPAD_AXIS_MAX),429 ),430 absolute_range(431 AbsoluteAxis::RY,432 i32::from(GAMEPAD_AXIS_MIN),433 i32::from(GAMEPAD_AXIS_MAX),434 ),435 absolute_range(AbsoluteAxis::Z, 0, i32::from(GAMEPAD_TRIGGER_MAX)),436 absolute_range(AbsoluteAxis::RZ, 0, i32::from(GAMEPAD_TRIGGER_MAX)),437 absolute_range(AbsoluteAxis::Hat0X, -1, 1),438 absolute_range(AbsoluteAxis::Hat0Y, -1, 1),439 ];440 handle441 .create(442 &InputId {443 bustype: UINPUT_BUS_TYPE,444 vendor: UINPUT_VENDOR_ID,445 product: GAMEPAD_PRODUCT_ID,446 version: UINPUT_DEVICE_VERSION,447 },448 GAMEPAD_DEVICE_NAME,449 0,450 &axes,451 )452 .context("registering Sigil virtual gamepad")?;453 let device = Self { handle };454 device.neutralize()?;455 Ok(device)456 }457458 fn apply(&self, state: &GamepadState) -> Result<()> {459 state.validate().context("validating gamepad snapshot")?;460 let report = map_gamepad_state(state);461 let mut events = [synchronize_event(); GAMEPAD_REPORT_EVENTS];462 let mut index = 0;463 for (button, pressed) in report.buttons {464 events[index] = key_event(linux_gamepad_button(button), KeyState::pressed(pressed));465 index += 1;466 }467 for (axis, value) in report.axes {468 events[index] = absolute_event(linux_gamepad_axis(axis), value);469 index += 1;470 }471 events[index] = synchronize_event();472 index += 1;473 ensure!(474 index == GAMEPAD_REPORT_EVENTS,475 "gamepad report violated its static event bound"476 );477 let written = self478 .handle479 .write(&events)480 .context("writing nonblocking gamepad snapshot")?;481 ensure!(482 written == events.len(),483 "uinput accepted only {written} of {} gamepad events",484 events.len()485 );486 Ok(())487 }488489 fn neutralize(&self) -> Result<()> {490 self.apply(&GamepadState::default())491 .context("neutralizing virtual gamepad")492 }493}494495impl Drop for GamepadDevice {496 fn drop(&mut self) {497 let _ = self.neutralize();498 let _ = self.handle.dev_destroy();499 }500}501502fn open_validated_handle(config: &UinputConfig, device_class: &str) -> Result<UInputHandle<File>> {503 let handle = UInputHandle::new(open_validated_device(config)?);504 let version = handle505 .version()506 .with_context(|| format!("querying uinput interface version for {device_class}"))?;507 ensure!(508 version >= 5,509 "uinput interface version {version} is too old for {device_class}"510 );511 Ok(handle)512}513514fn open_validated_device(config: &UinputConfig) -> Result<File> {515 let mut options = OpenOptions::new();516 options517 .read(true)518 .write(true)519 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK);520 let file = options.open(&config.device_path).with_context(|| {521 format!(522 "opening configured uinput device {} without following symlinks",523 config.device_path.display()524 )525 })?;526 validate_open_device(&file, config)?;527 Ok(file)528}529530fn validate_open_device(file: &File, config: &UinputConfig) -> Result<()> {531 let metadata = file.metadata().with_context(|| {532 format!(533 "inspecting opened uinput device {}",534 config.device_path.display()535 )536 })?;537 ensure!(538 metadata.file_type().is_char_device(),539 "configured uinput path is not a character device"540 );541 ensure!(542 metadata.uid() == config.expected_owner_uid,543 "configured uinput owner UID changed"544 );545 ensure!(546 metadata.gid() == config.expected_group_gid,547 "configured uinput group GID changed"548 );549 ensure!(550 metadata.mode() & 0o7777 == config.expected_mode,551 "configured uinput permission mode changed"552 );553 let device = metadata.rdev();554 ensure!(555 libc::major(device) == UINPUT_MAJOR && libc::minor(device) == UINPUT_MINOR,556 "configured character device is not the Linux uinput misc device"557 );558 validate_access_acl(file, config)?;559 Ok(())560}561562fn validate_access_acl(file: &File, config: &UinputConfig) -> Result<()> {563 if let Some(expected_uid) = config.expected_acl_user_uid {564 let effective_uid = unsafe { libc::geteuid() };565 ensure!(566 expected_uid == effective_uid,567 "configured uinput ACL user UID {expected_uid} does not match Sigil effective UID {effective_uid}"568 );569 }570571 let attribute = c"system.posix_acl_access";572 let mut bytes = [0_u8; POSIX_ACL_REQUIRED_BYTES];573 let result = unsafe {574 libc::fgetxattr(575 file.as_raw_fd(),576 attribute.as_ptr(),577 bytes.as_mut_ptr().cast(),578 bytes.len(),579 )580 };581 if result >= 0 {582 let Some(expected_uid) = config.expected_acl_user_uid else {583 anyhow::bail!(584 "configured uinput device must use the group-only scheme with no extended access ACL; set uinput.expected_acl_user_uid only for the explicit one-user ACL scheme"585 );586 };587 let length =588 usize::try_from(result).context("converting configured uinput access ACL length")?;589 return validate_single_user_access_acl(590 bytes591 .get(..length)592 .context("configured uinput access ACL exceeded its fixed bound")?,593 expected_uid,594 config.expected_mode,595 )596 .context("validating configured uinput one-user access ACL");597 }598 let error = std::io::Error::last_os_error();599 if error600 .raw_os_error()601 .is_some_and(|code| code == libc::ENODATA || code == libc::ENOTSUP)602 {603 ensure!(604 config.expected_acl_user_uid.is_none(),605 "configured uinput device is missing the explicitly required one-user access ACL"606 );607 return Ok(());608 }609 if error.raw_os_error() == Some(libc::ERANGE) {610 if config.expected_acl_user_uid.is_some() {611 anyhow::bail!("configured uinput access ACL exceeds the exact one-user ACL scheme");612 }613 anyhow::bail!(614 "configured uinput device must use the group-only scheme with no extended access ACL"615 );616 }617 Err(error).context("checking configured uinput device access ACL")618}619620fn absolute_range(axis: AbsoluteAxis, minimum: i32, maximum: i32) -> AbsoluteInfoSetup {621 AbsoluteInfoSetup {622 axis,623 info: AbsoluteInfo {624 minimum,625 maximum,626 ..AbsoluteInfo::default()627 },628 }629}630631fn mouse_button(button: u8) -> Option<Key> {632 match button {633 1 => Some(Key::ButtonLeft),634 2 => Some(Key::ButtonRight),635 3 => Some(Key::ButtonMiddle),636 _ => None,637 }638}639640fn linux_gamepad_button(button: GamepadButton) -> Key {641 match button {642 GamepadButton::A => Key::ButtonSouth,643 GamepadButton::B => Key::ButtonEast,644 GamepadButton::X => Key::ButtonNorth,645 GamepadButton::Y => Key::ButtonWest,646 GamepadButton::LeftShoulder => Key::ButtonTL,647 GamepadButton::RightShoulder => Key::ButtonTR,648 GamepadButton::Back => Key::ButtonSelect,649 GamepadButton::Start => Key::ButtonStart,650 GamepadButton::Guide => Key::ButtonMode,651 GamepadButton::LeftStick => Key::ButtonThumbl,652 GamepadButton::RightStick => Key::ButtonThumbr,653 }654}655656fn linux_gamepad_axis(axis: GamepadAxis) -> AbsoluteAxis {657 match axis {658 GamepadAxis::LeftX => AbsoluteAxis::X,659 GamepadAxis::LeftY => AbsoluteAxis::Y,660 GamepadAxis::RightX => AbsoluteAxis::RX,661 GamepadAxis::RightY => AbsoluteAxis::RY,662 GamepadAxis::LeftTrigger => AbsoluteAxis::Z,663 GamepadAxis::RightTrigger => AbsoluteAxis::RZ,664 GamepadAxis::DpadX => AbsoluteAxis::Hat0X,665 GamepadAxis::DpadY => AbsoluteAxis::Hat0Y,666 }667}668669fn key_event(key: Key, state: KeyState) -> input_linux::sys::input_event {670 KeyEvent::new(EventTime::default(), key, state)671 .into_event()672 .into_raw()673}674675fn absolute_event(axis: AbsoluteAxis, value: i32) -> input_linux::sys::input_event {676 AbsoluteEvent::new(EventTime::default(), axis, value)677 .into_event()678 .into_raw()679}680681fn relative_event(axis: RelativeAxis, value: i32) -> input_linux::sys::input_event {682 RelativeEvent::new(EventTime::default(), axis, value)683 .into_event()684 .into_raw()685}686687fn synchronize_event() -> input_linux::sys::input_event {688 SynchronizeEvent::report(EventTime::default())689 .into_event()690 .into_raw()691}