1use std::fmt;23use std::sync::Arc;45use std::sync::Mutex;678use anyhow::Context;9use anyhow::{Result, bail};10use sigil_protocol::{Capability, InputEvent};1112use crate::config::{HostConfig, InputMode};1314mod acl;15mod keymap;1617mod linux;1819const ACK_ONLY_CAPABILITIES: &[Capability] = &[Capability::InputAck];20const UINPUT_CAPABILITIES: &[Capability] = &[21 Capability::RelativePointer,22 Capability::Keyboard,23 Capability::Gamepad,24 Capability::InputAck,25];262728// This Linux input identity tuple is an external ABI consumed by Gamescope,29// udev/libinput rules, and hardware evidence. Preserve it exactly; see30// docs/compatibility-identifiers.md.31const UINPUT_BUS_TYPE: u16 = 0x06; // Linux BUS_VIRTUAL3233const UINPUT_VENDOR_ID: u16 = 0x5347;3435const UINPUT_DEVICE_VERSION: u16 = 1;3637const POINTER_PRODUCT_ID: u16 = 1;3839const GAMEPAD_PRODUCT_ID: u16 = 2;4041const KEYBOARD_PRODUCT_ID: u16 = 3;4243const POINTER_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Pointer";4445const KEYBOARD_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Keyboard";4647const GAMEPAD_DEVICE_NAME: &[u8] = b"Sigil Spark Virtual Gamepad";484950pub enum InputDisposition {51 Probed,52 Disabled,53 Observed,54 55 Injected,56 TextIgnored,57}585960pub struct InputBackend {61 mode: InputMode,62 63 device: Option<Arc<std::sync::Mutex<linux::UinputDevice>>>,64}6566impl fmt::Debug for InputBackend {67 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {68 formatter69 .debug_struct("InputBackend")70 .field("mode", &self.mode)71 .finish_non_exhaustive()72 }73}7475impl InputBackend {76 /// Construct the backend before the network endpoint is exposed. For77 /// uinput this opens, validates, and registers the virtual device.78 pub fn initialize(config: &HostConfig) -> Result<Self> {79 match config.input_mode {80 InputMode::Disabled | InputMode::Log => Ok(Self {81 mode: config.input_mode.clone(),82 83 device: None,84 }),85 InputMode::Uinput => {86 87 {88 let settings = config89 .uinput90 .as_ref()91 .context("missing validated uinput configuration")?;92 let device = linux::UinputDevice::open(settings)?;93 Ok(Self {94 mode: InputMode::Uinput,95 device: Some(Arc::new(std::sync::Mutex::new(device))),96 })97 }98 99 {100 let _ = config;101 bail!("input_mode uinput is supported only on Linux")102 }103 }104 }105 }106107 pub fn capabilities(&self) -> &'static [Capability] {108 capabilities_for_mode(&self.mode)109 }110111 pub fn apply(&self, event: &InputEvent, negotiated: &[Capability]) -> Result<InputDisposition> {112 ensure_event_was_negotiated(&self.mode, event, negotiated)?;113 if matches!(event, InputEvent::Probe) {114 return Ok(InputDisposition::Probed);115 }116 match self.mode {117 InputMode::Disabled => Ok(InputDisposition::Disabled),118 InputMode::Log => Ok(InputDisposition::Observed),119 InputMode::Uinput => {120 if matches!(event, InputEvent::Text { .. }) {121 return Ok(InputDisposition::TextIgnored);122 }123 124 {125 let device = self126 .device127 .as_ref()128 .context("uinput backend was not initialized")?;129 device130 .lock()131 .map_err(|_| anyhow::anyhow!("uinput backend lock poisoned"))?132 .apply(event)?;133 Ok(InputDisposition::Injected)134 }135 136 {137 let _ = event;138 bail!("uinput backend is unavailable on this platform")139 }140 }141 }142 }143144 /// Release any transitions held by the ending input session so a dropped145 /// connection cannot leave a key or mouse button pressed.146 pub fn reset_session(&self) -> Result<()> {147 148 if let Some(device) = &self.device {149 reset_through_poisoned_lock(device, linux::UinputDevice::release_all)?;150 }151 Ok(())152 }153}154155156fn reset_through_poisoned_lock<T>(157 device: &Mutex<T>,158 reset: impl FnOnce(&mut T) -> Result<()>,159) -> Result<()> {160 let mut device = match device.lock() {161 Ok(device) => device,162 Err(poisoned) => {163 // A panic while applying input poisons this mutex. Teardown must164 // still recover the device solely to release held transitions;165 // ordinary input continues to reject the poisoned lock.166 tracing::warn!("recovering poisoned uinput lock for session reset");167 poisoned.into_inner()168 }169 };170 reset(&mut device)171}172173fn ensure_event_was_negotiated(174 mode: &InputMode,175 event: &InputEvent,176 negotiated: &[Capability],177) -> Result<()> {178 if !matches!(mode, InputMode::Uinput) {179 return Ok(());180 }181 let required = match event {182 InputEvent::Probe => Some(Capability::InputAck),183 InputEvent::MouseMove { .. } => Some(Capability::AbsolutePointer),184 InputEvent::MouseMoveRelative { .. }185 | InputEvent::MousePositionSync { .. }186 | InputEvent::MouseClick { .. }187 | InputEvent::MouseDown { .. }188 | InputEvent::MouseUp { .. }189 | InputEvent::MouseScroll { .. } => Some(Capability::RelativePointer),190 InputEvent::KeyDown { .. } | InputEvent::KeyUp { .. } | InputEvent::KeyClick { .. } => {191 Some(Capability::Keyboard)192 }193 InputEvent::Gamepad { .. } => Some(Capability::Gamepad),194 // Text remains an explicit content-free no-op for compatibility with195 // clients that emit it alongside negotiated physical key transitions.196 InputEvent::Text { .. } => None,197 };198 if required.is_some_and(|capability| !negotiated.contains(&capability)) {199 bail!("input event class was not negotiated for this session");200 }201 Ok(())202}203204fn capabilities_for_mode(mode: &InputMode) -> &'static [Capability] {205 match mode {206 InputMode::Disabled | InputMode::Log => ACK_ONLY_CAPABILITIES,207 InputMode::Uinput => UINPUT_CAPABILITIES,208 }209}210211212mod tests {213 use std::panic::{AssertUnwindSafe, catch_unwind};214 use std::sync::Arc;215216 use sigil_protocol::GamepadState;217218 use super::*;219220 221 fn virtual_input_topology_has_stable_distinct_identities() {222 assert_eq!(UINPUT_BUS_TYPE, 0x06);223 224 assert_eq!(UINPUT_BUS_TYPE, input_linux::sys::BUS_VIRTUAL);225 assert_eq!(UINPUT_VENDOR_ID, 0x5347);226 assert_eq!(UINPUT_DEVICE_VERSION, 1);227 assert_eq!(POINTER_DEVICE_NAME, b"Sigil Spark Virtual Pointer");228 assert_eq!(KEYBOARD_DEVICE_NAME, b"Sigil Spark Virtual Keyboard");229 assert_eq!(GAMEPAD_DEVICE_NAME, b"Sigil Spark Virtual Gamepad");230 assert_eq!(POINTER_PRODUCT_ID, 1);231 assert_eq!(GAMEPAD_PRODUCT_ID, 2);232 assert_eq!(KEYBOARD_PRODUCT_ID, 3);233 assert_ne!(POINTER_PRODUCT_ID, KEYBOARD_PRODUCT_ID);234 assert_ne!(POINTER_PRODUCT_ID, GAMEPAD_PRODUCT_ID);235 assert_ne!(KEYBOARD_PRODUCT_ID, GAMEPAD_PRODUCT_ID);236 }237238 239 fn session_reset_recovers_a_lock_poisoned_while_applying_input() {240 241 struct DeviceState {242 released: bool,243 }244245 let device = Arc::new(Mutex::new(DeviceState::default()));246 let panic_device = Arc::clone(&device);247 let panic_result = catch_unwind(AssertUnwindSafe(move || {248 let _device = panic_device.lock().unwrap();249 panic!("simulate panic while applying uinput");250 }));251 assert!(panic_result.is_err());252 assert!(device.lock().is_err());253254 reset_through_poisoned_lock(device.as_ref(), |device| {255 device.released = true;256 Ok(())257 })258 .unwrap();259260 let device = device.lock().unwrap_err().into_inner();261 assert!(device.released);262 }263264 265 fn content_free_probe_requires_ack_and_never_reaches_uinput() {266 assert!(ensure_event_was_negotiated(&InputMode::Uinput, &InputEvent::Probe, &[]).is_err());267 assert!(268 ensure_event_was_negotiated(269 &InputMode::Uinput,270 &InputEvent::Probe,271 &[Capability::InputAck]272 )273 .is_ok()274 );275 }276277 278 fn relative_pointer_events_require_relative_pointer_negotiation() {279 let movement = InputEvent::MouseMoveRelative { dx: 4, dy: -3 };280 let synchronization = InputEvent::MousePositionSync { x: 123, y: 456 };281 let click = InputEvent::MouseClick { b: 1 };282 let legacy = InputEvent::MouseMove { x: 10, y: 20 };283 for event in [&movement, &synchronization, &click] {284 assert!(ensure_event_was_negotiated(&InputMode::Uinput, event, &[]).is_err());285 assert!(286 ensure_event_was_negotiated(287 &InputMode::Uinput,288 event,289 &[Capability::AbsolutePointer]290 )291 .is_err()292 );293 assert!(294 ensure_event_was_negotiated(295 &InputMode::Uinput,296 event,297 &[Capability::RelativePointer]298 )299 .is_ok()300 );301 }302 assert!(303 ensure_event_was_negotiated(304 &InputMode::Uinput,305 &legacy,306 &[Capability::RelativePointer]307 )308 .is_err()309 );310 }311312 313 fn uinput_rejects_unnegotiated_gamepad_snapshots() {314 let event = InputEvent::Gamepad {315 state: GamepadState::default(),316 };317 assert!(ensure_event_was_negotiated(&InputMode::Uinput, &event, &[]).is_err());318 assert!(319 ensure_event_was_negotiated(&InputMode::Uinput, &event, &[Capability::Gamepad]).is_ok()320 );321 assert!(ensure_event_was_negotiated(&InputMode::Log, &event, &[]).is_ok());322 }323324 325 fn advertised_capabilities_follow_operational_mode() {326 let disabled = InputBackend {327 mode: InputMode::Disabled,328 329 device: None,330 };331 assert_eq!(disabled.capabilities(), &[Capability::InputAck]);332333 let log = InputBackend {334 mode: InputMode::Log,335 336 device: None,337 };338 assert_eq!(log.capabilities(), &[Capability::InputAck]);339 assert!(!log.capabilities().contains(&Capability::Keyboard));340 assert_eq!(341 capabilities_for_mode(&InputMode::Uinput),342 &[343 Capability::RelativePointer,344 Capability::Keyboard,345 Capability::Gamepad,346 Capability::InputAck,347 ]348 );349 assert!(!capabilities_for_mode(&InputMode::Uinput).contains(&Capability::AbsolutePointer));350 }351}