inputd: exclusive input grab (EVIOCGRAB equivalent)

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.
This commit is contained in:
Red Bear OS
2026-07-24 12:11:10 +09:00
parent a916a6612c
commit 9cd43d8d8c
2 changed files with 88 additions and 25 deletions
+15
View File
@@ -82,8 +82,11 @@ impl ConsumerHandle {
/// `ConsumerHandle` do not apply to a raw tap (it owns no VT); only /// `ConsumerHandle` do not apply to a raw tap (it owns no VT); only
/// [`Self::read_events`] / [`Self::event_handle`] are meaningful. /// [`Self::read_events`] / [`Self::event_handle`] are meaningful.
pub fn new_raw() -> io::Result<Self> { pub fn new_raw() -> io::Result<Self> {
// 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() let file = OpenOptions::new()
.read(true) .read(true)
.write(true)
.custom_flags(O_NONBLOCK as i32) .custom_flags(O_NONBLOCK as i32)
.open(format!("/scheme/input/consumer_raw"))?; .open(format!("/scheme/input/consumer_raw"))?;
Ok(Self(file)) Ok(Self(file))
@@ -172,6 +175,18 @@ impl ConsumerHandle {
Ok(display_file) 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<ConsumerHandleEvent<'a>> { pub fn read_events<'a>(&self, events: &'a mut [Event]) -> io::Result<ConsumerHandleEvent<'a>> {
match read_to_slice(self.0.as_fd(), events) { match read_to_slice(self.0.as_fd(), events) {
Ok(count) => Ok(ConsumerHandleEvent::Events(&events[..count])), Ok(count) => Ok(ConsumerHandleEvent::Events(&events[..count])),
+56 -8
View File
@@ -27,7 +27,7 @@ use redox_scheme::{CallerCtx, OpenResult, Response, SignalBehavior, Socket};
use orbclient::{Event, EventOption}; use orbclient::{Event, EventOption};
use scheme_utils::{Blocking, FpathWriter, HandleMap}; use scheme_utils::{Blocking, FpathWriter, HandleMap};
use syscall::schemev2::NewFdFlags; 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; pub mod keymap;
@@ -84,6 +84,13 @@ struct InputScheme {
/// The compositor reads the raw device tap (`consumer_raw`) instead. Empty by /// The compositor reads the raw device tap (`consumer_raw`) instead. Empty by
/// default, so console behaviour is unchanged until a compositor opts in. /// default, so console behaviour is unchanged until a compositor opts in.
graphics_vts: BTreeSet<usize>, graphics_vts: BTreeSet<usize>,
/// 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<usize>,
super_key: bool, super_key: bool,
active_vt: Option<usize>, active_vt: Option<usize>,
active_keymap: KeymapData, active_keymap: KeymapData,
@@ -103,6 +110,7 @@ impl InputScheme {
display: None, display: None,
vts: BTreeSet::new(), vts: BTreeSet::new(),
graphics_vts: BTreeSet::new(), graphics_vts: BTreeSet::new(),
grab: None,
super_key: false, super_key: false,
active_vt: None, active_vt: None,
// TODO: configurable init? // TODO: configurable init?
@@ -414,6 +422,29 @@ impl SchemeSync for InputScheme {
) -> syscall::Result<usize> { ) -> syscall::Result<usize> {
self.has_new_events = true; 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)?; let handle = self.handles.get_mut(id)?;
match handle { match handle {
@@ -557,10 +588,14 @@ impl SchemeSync for InputScheme {
) )
}; };
let grab = self.grab;
// 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 { 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); let console_suppressed = self.graphics_vts.contains(&active_vt);
if !console_suppressed { if !console_suppressed {
for handle in self.handles.values_mut() { for handle in self.handles.values_mut() {
@@ -583,15 +618,22 @@ impl SchemeSync for InputScheme {
} }
} }
} }
}
// Raw taps get the pre-keymap stream unconditionally — no VT gate, and // Raw taps get the pre-keymap stream with no VT gate — so a compositor
// even when no VT is active — so a Wayland compositor keeps receiving // keeps receiving input regardless of the active text VT. When a grab is
// input regardless of which text VT (if any) is foregrounded. // held, ONLY the grabbing handle is fed (Linux `EVIOCGRAB` exclusivity);
for handle in self.handles.values_mut() { // otherwise every raw tap is fed.
for (hid, handle) in self.handles.iter_mut() {
if let Handle::ConsumerRaw { if let Handle::ConsumerRaw {
pending, notified, .. pending, notified, ..
} = handle } = handle
{ {
if let Some(gid) = grab {
if *hid != gid {
continue;
}
}
pending.extend_from_slice(&raw_buf); pending.extend_from_slice(&raw_buf);
*notified = false; *notified = false;
} }
@@ -648,6 +690,12 @@ impl SchemeSync for InputScheme {
let Some(handle) = self.handles.remove(id) else { let Some(handle) = self.handles.remove(id) else {
return; 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 { if let Handle::Consumer { vt, .. } = handle {
self.vts.remove(&vt); self.vts.remove(&vt);
if self.active_vt == Some(vt) { if self.active_vt == Some(vt) {