From 9cd43d8d8c414595defc44b80c3f72a66d9d2aa4 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Fri, 24 Jul 2026 12:11:10 +0900 Subject: [PATCH] inputd: exclusive input grab (EVIOCGRAB equivalent) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete the Linux-aligned seat-takeover primitive set: a raw consumer can take an EXCLUSIVE grab of the input stream by writing a single control byte to its own consumer_raw handle (1=grab, 0=ungrab) — self-identifying, unlike the shared control fd. While a grab is held: * events route ONLY to the grabbing handle; * the text console AND every other raw tap are suspended; and * the grab is released automatically when the handle closes, so a compositor that crashes cannot wedge input away from the console (Linux __input_release_device). A second grab attempt by a different handle returns EBUSY. Additive and behaviour-preserving: grab defaults to None, so nothing changes until a client grabs. new_raw() now opens read+write and ConsumerHandle::set_grab() drives it. Together with set_vt_mode (KDSKBMODE) this gives seatd/a compositor the full grab + VT-mode takeover surface. Compile-checked for x86_64-unknown-redox; boot-validation pending build-green. --- drivers/inputd/src/lib.rs | 15 ++++++ drivers/inputd/src/main.rs | 98 ++++++++++++++++++++++++++++---------- 2 files changed, 88 insertions(+), 25 deletions(-) diff --git a/drivers/inputd/src/lib.rs b/drivers/inputd/src/lib.rs index 797521e9a9..322bbbeb24 100644 --- a/drivers/inputd/src/lib.rs +++ b/drivers/inputd/src/lib.rs @@ -82,8 +82,11 @@ impl ConsumerHandle { /// `ConsumerHandle` do not apply to a raw tap (it owns no VT); only /// [`Self::read_events`] / [`Self::event_handle`] are meaningful. pub fn new_raw() -> io::Result { + // Opened read+write: reads drain events, and a single control byte is + // written to this same handle to grab/ungrab (see `set_grab`). let file = OpenOptions::new() .read(true) + .write(true) .custom_flags(O_NONBLOCK as i32) .open(format!("/scheme/input/consumer_raw"))?; Ok(Self(file)) @@ -172,6 +175,18 @@ impl ConsumerHandle { Ok(display_file) } + /// Request (`grab = true`) or release (`grab = false`) an EXCLUSIVE grab of + /// the input stream on a raw tap opened with [`Self::new_raw`] — the RedBear + /// `EVIOCGRAB`. While grabbed, this handle is the ONLY consumer fed: the text + /// console and every other raw tap are suspended. The grab is dropped + /// automatically when the handle closes. Returns `EBUSY` if another handle + /// already holds the grab. Meaningless on a console (`new_vt`) handle. + pub fn set_grab(&mut self, grab: bool) -> io::Result<()> { + let byte = [u8::from(grab)]; + self.0.write(&byte)?; + Ok(()) + } + pub fn read_events<'a>(&self, events: &'a mut [Event]) -> io::Result> { match read_to_slice(self.0.as_fd(), events) { Ok(count) => Ok(ConsumerHandleEvent::Events(&events[..count])), diff --git a/drivers/inputd/src/main.rs b/drivers/inputd/src/main.rs index facf057cb9..ad1a2860ac 100644 --- a/drivers/inputd/src/main.rs +++ b/drivers/inputd/src/main.rs @@ -27,7 +27,7 @@ use redox_scheme::{CallerCtx, OpenResult, Response, SignalBehavior, Socket}; use orbclient::{Event, EventOption}; use scheme_utils::{Blocking, FpathWriter, HandleMap}; use syscall::schemev2::NewFdFlags; -use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EEXIST, EINVAL}; +use syscall::{Error as SysError, EventFlags, EACCES, EBADF, EBUSY, EEXIST, EINVAL}; pub mod keymap; @@ -84,6 +84,13 @@ struct InputScheme { /// The compositor reads the raw device tap (`consumer_raw`) instead. Empty by /// default, so console behaviour is unchanged until a compositor opts in. graphics_vts: BTreeSet, + /// Handle id of a raw consumer holding an EXCLUSIVE grab of the input stream + /// (the Linux `input_grab_device` / `EVIOCGRAB` role). While set, events are + /// routed ONLY to this handle — the console consumer and every other raw tap + /// are suspended — and it is cleared automatically when that handle closes + /// (like `__input_release_device`). `None` by default, so nothing changes + /// until a client explicitly grabs. + grab: Option, super_key: bool, active_vt: Option, active_keymap: KeymapData, @@ -103,6 +110,7 @@ impl InputScheme { display: None, vts: BTreeSet::new(), graphics_vts: BTreeSet::new(), + grab: None, super_key: false, active_vt: None, // TODO: configurable init? @@ -414,6 +422,29 @@ impl SchemeSync for InputScheme { ) -> syscall::Result { self.has_new_events = true; + // A raw consumer manages its exclusive grab by writing a single control + // byte to its OWN handle (self-identifying, unlike the shared control + // fd): `1` = grab, `0` = ungrab. This is the RedBear `EVIOCGRAB`. Handled + // up front so it does not fall into the event-processing path below. + // `matches!` ends the immutable borrow before we touch `self.grab`. + if matches!(self.handles.get(id), Ok(Handle::ConsumerRaw { .. })) { + let want_grab = buf.first().copied().unwrap_or(0) != 0; + if want_grab { + match self.grab { + None => { + self.grab = Some(id); + log::debug!("raw consumer #{id} grabbed the input stream"); + } + Some(g) if g == id => {} // idempotent re-grab by the owner + Some(_) => return Err(SysError::new(EBUSY)), + } + } else if self.grab == Some(id) { + self.grab = None; + log::debug!("raw consumer #{id} released the input stream"); + } + return Ok(buf.len()); + } + let handle = self.handles.get_mut(id)?; match handle { @@ -557,41 +588,52 @@ impl SchemeSync for InputScheme { ) }; - if let Some(active_vt) = self.active_vt { - // Suppress the text console entirely when the active VT is owned by a - // graphical client — the compositor drives it via the raw tap, and - // cooked bytes must not also reach a (now hidden) text console. - let console_suppressed = self.graphics_vts.contains(&active_vt); - if !console_suppressed { - for handle in self.handles.values_mut() { - match handle { - Handle::Consumer { - pending, - notified, - vt, - .. - } => { - if *vt != active_vt { - continue; - } + let grab = self.grab; - pending.extend_from_slice(buf); - *notified = false; + // The text console is fed only when NObody holds an exclusive grab and + // the active VT is not owned by a graphical client. A grab (a fullscreen + // compositor) or graphics-mode VT both mean cooked bytes must not reach a + // hidden text console. + if grab.is_none() { + if let Some(active_vt) = self.active_vt { + let console_suppressed = self.graphics_vts.contains(&active_vt); + if !console_suppressed { + for handle in self.handles.values_mut() { + match handle { + Handle::Consumer { + pending, + notified, + vt, + .. + } => { + if *vt != active_vt { + continue; + } + + pending.extend_from_slice(buf); + *notified = false; + } + _ => continue, } - _ => continue, } } } } - // Raw taps get the pre-keymap stream unconditionally — no VT gate, and - // even when no VT is active — so a Wayland compositor keeps receiving - // input regardless of which text VT (if any) is foregrounded. - for handle in self.handles.values_mut() { + // Raw taps get the pre-keymap stream with no VT gate — so a compositor + // keeps receiving input regardless of the active text VT. When a grab is + // held, ONLY the grabbing handle is fed (Linux `EVIOCGRAB` exclusivity); + // otherwise every raw tap is fed. + for (hid, handle) in self.handles.iter_mut() { if let Handle::ConsumerRaw { pending, notified, .. } = handle { + if let Some(gid) = grab { + if *hid != gid { + continue; + } + } pending.extend_from_slice(&raw_buf); *notified = false; } @@ -648,6 +690,12 @@ impl SchemeSync for InputScheme { let Some(handle) = self.handles.remove(id) else { return; }; + // Release an exclusive grab held by the closing handle, so a compositor + // that crashes or exits does not wedge input away from the console. + if self.grab == Some(id) { + self.grab = None; + log::debug!("raw consumer #{id} closed; input grab released"); + } if let Handle::Consumer { vt, .. } = handle { self.vts.remove(&vt); if self.active_vt == Some(vt) {