From 216fc3ca2d8332957353588e6b126f2c1cb64e0c Mon Sep 17 00:00:00 2001 From: roboomp Date: Fri, 7 Aug 2026 16:12:29 +0000 Subject: [PATCH] fix(computer): waited for all granted libei devices - Tracked pointer and keyboard grants from the RemoteDesktop response. - Drained asynchronous EIS announcements after the first resumed device. - Covered GNOME's keyboard-before-pointer ordering with a regression test. Fixes #7926 --- Cargo.lock | 1 + Cargo.toml | 1 + crates/pi-natives/Cargo.toml | 1 + .../src/desktop/linux/wayland/libei.rs | 199 +++++++++++------- packages/natives/CHANGELOG.md | 1 + 5 files changed, 131 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b578763b..4759a00f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4995,6 +4995,7 @@ dependencies = [ "flume", "fontdue", "foreign-types", + "futures", "globset", "grep-matcher", "grep-pcre2", diff --git a/Cargo.toml b/Cargo.toml index 18349a7a8..7aca46a41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -204,6 +204,7 @@ brush-builtins = { path = "crates/vendor/brush-builtins" } # Async Runtime & Concurrency # ────────────────────────────────────────────────────────────────────────────── async-trait = "0.1" +futures = "0.3" dashmap = "6.1" parking_lot = "0.12.5" rayon = "1.12" diff --git a/crates/pi-natives/Cargo.toml b/crates/pi-natives/Cargo.toml index a0b56e945..f51aab587 100644 --- a/crates/pi-natives/Cargo.toml +++ b/crates/pi-natives/Cargo.toml @@ -30,6 +30,7 @@ clap.workspace = true globset.workspace = true fontdue.workspace = true grep-matcher.workspace = true +futures.workspace = true grep-pcre2.workspace = true grep-regex.workspace = true grep-searcher.workspace = true diff --git a/crates/pi-natives/src/desktop/linux/wayland/libei.rs b/crates/pi-natives/src/desktop/linux/wayland/libei.rs index e9fb81ca9..c48bedb64 100644 --- a/crates/pi-natives/src/desktop/linux/wayland/libei.rs +++ b/crates/pi-natives/src/desktop/linux/wayland/libei.rs @@ -1,15 +1,17 @@ use std::{ os::unix::net::UnixStream, - time::{SystemTime, UNIX_EPOCH}, + time::{Duration, SystemTime, UNIX_EPOCH}, }; use ashpd::desktop::{ PersistMode, Session, remote_desktop::{DeviceType, RemoteDesktop}, }; +use futures::StreamExt; use reis::{ ei, - event::{Device, DeviceCapability, EiConvertEventIterator, EiEvent}, + event::{Device, DeviceCapability, EiEvent}, + tokio::EiConvertEventStream, }; use crate::desktop::{ @@ -18,6 +20,22 @@ use crate::desktop::{ keys::KeyName, }; +const DEVICE_DISCOVERY_DRAIN_TIMEOUT: Duration = Duration::from_millis(500); + +#[derive(Clone, Copy)] +struct DiscoveryTargets { + pointer: bool, + keyboard: bool, +} + +impl DiscoveryTargets { + const ALL: Self = Self { pointer: true, keyboard: true }; + + const fn is_complete(self, pointer: bool, keyboard: bool) -> bool { + (!self.pointer || pointer) && (!self.keyboard || keyboard) + } +} + struct EiDevice { device: Device, serial: u32, @@ -47,7 +65,7 @@ impl Drop for Libei { } } -/// Closes a RemoteDesktop portal session, bounded by `CLOSE_TIMEOUT` so an +/// Closes a `RemoteDesktop` portal session, bounded by `CLOSE_TIMEOUT` so an /// unresponsive `xdg-desktop-portal` cannot hang teardown indefinitely. fn close_session(runtime: &tokio::runtime::Runtime, session: &RemoteDesktopSession) { let _ = runtime.block_on(async { @@ -57,21 +75,25 @@ fn close_session(runtime: &tokio::runtime::Runtime, session: &RemoteDesktopSessi impl Libei { pub(super) fn new() -> CoreResult { - let (context, portal_session) = match ei::Context::connect_to_env() { - Ok(Some(context)) => (context, None), + let runtime = super::portal::portal_runtime()?; + let (context, portal_session, targets) = match ei::Context::connect_to_env() { + Ok(Some(context)) => (context, None, DiscoveryTargets::ALL), Ok(None) => { - let (context, session) = Self::portal_context()?; - (context, Some(session)) + let (context, session, targets) = Self::portal_context(runtime)?; + (context, Some(session), targets) }, Err(err) => return Err(DesktopError::permission_denied(format!("LIBEI_SOCKET: {err}"))), }; let mut backend = Self { context, pointer: None, keyboard: None, sequence: 1, portal_session }; - let (_connection, mut events) = backend - .context - .handshake_blocking("omp-computer", ei::handshake::ContextType::Sender) + let (_connection, mut events) = runtime + .block_on( + backend + .context + .handshake_tokio("omp-computer", ei::handshake::ContextType::Sender), + ) .map_err(|err| DesktopError::input_failed(format!("libei handshake: {err}")))?; - backend.discover_devices(&mut events)?; + backend.discover_devices(runtime, &mut events, targets)?; if backend.pointer.is_none() && backend.keyboard.is_none() { return Err(DesktopError::permission_denied( "RemoteDesktop portal granted no libei keyboard or pointer devices", @@ -80,9 +102,10 @@ impl Libei { Ok(backend) } - fn portal_context() -> CoreResult<(ei::Context, PortalSession)> { - let runtime = super::portal::portal_runtime()?; - let (fd, session) = runtime + fn portal_context( + runtime: &'static tokio::runtime::Runtime, + ) -> CoreResult<(ei::Context, PortalSession, DiscoveryTargets)> { + let (fd, session, targets) = runtime .block_on(async { let portal = RemoteDesktop::new() .await @@ -101,20 +124,26 @@ impl Libei { ) .await .map_err(|err| format!("RemoteDesktop SelectDevices: {err}"))?; - portal + let response = portal .start(&session, None) .await .map_err(|err| format!("RemoteDesktop Start: {err}"))? .response() .map_err(|err| format!("RemoteDesktop permission: {err}"))?; + let devices = response.devices(); + let targets = DiscoveryTargets { + pointer: devices.contains(DeviceType::Pointer), + keyboard: devices.contains(DeviceType::Keyboard), + }; portal .connect_to_eis(&session) .await + .map(|fd| (fd, targets)) .map_err(|err| format!("RemoteDesktop ConnectToEIS: {err}")) } .await; match fd { - Ok(fd) => Ok((fd, session)), + Ok((fd, targets)) => Ok((fd, session, targets)), Err(err) => { // Already inside `runtime.block_on`, so the `close_session` // helper (itself a `block_on`) would abort with a nested-runtime @@ -133,69 +162,82 @@ impl Libei { return Err(DesktopError::input_failed(format!("libei portal socket: {err}"))); }, }; - Ok((context, PortalSession { runtime, session })) + Ok((context, PortalSession { runtime, session }, targets)) } - fn discover_devices(&mut self, events: &mut EiConvertEventIterator) -> CoreResult<()> { - let mut pending_pointer = None; - let mut pending_keyboard = None; - for _ in 0..128 { - let event = events - .next() + fn discover_devices( + &mut self, + runtime: &tokio::runtime::Runtime, + events: &mut EiConvertEventStream, + targets: DiscoveryTargets, + ) -> CoreResult<()> { + runtime.block_on(async { + let mut pending_pointer = None; + let mut pending_keyboard = None; + let mut drain_deadline = None; + for _ in 0..128 { + let event = if let Some(deadline) = drain_deadline { + match tokio::time::timeout_at(deadline, events.next()).await { + Ok(event) => event, + Err(_) => break, + } + } else { + events.next().await + } .ok_or_else(|| { DesktopError::input_failed("libei disconnected during device discovery") })? .map_err(|err| DesktopError::input_failed(format!("libei device discovery: {err}")))?; - match event { - EiEvent::SeatAdded(event) => { - event.seat.bind_capabilities(&[ - DeviceCapability::PointerAbsolute, - DeviceCapability::Pointer, - DeviceCapability::Button, - DeviceCapability::Scroll, - DeviceCapability::Keyboard, - ]); - self - .context - .flush() - .map_err(|err| DesktopError::input_failed(format!("libei bind seat: {err}")))?; - }, - EiEvent::DeviceAdded(event) => { - if event - .device - .has_capability(DeviceCapability::PointerAbsolute) - { - pending_pointer = Some(event.device.clone()); - } - if event.device.has_capability(DeviceCapability::Keyboard) { - pending_keyboard = Some(event.device); - } - }, - EiEvent::DeviceResumed(event) => { - if pending_pointer.as_ref() == Some(&event.device) { - self.pointer = - Some(EiDevice { device: event.device.clone(), serial: event.serial }); - } - if pending_keyboard.as_ref() == Some(&event.device) { - self.keyboard = Some(EiDevice { device: event.device, serial: event.serial }); - } - }, - EiEvent::Disconnected(event) => { - return Err(DesktopError::input_failed(format!( - "libei disconnected: {}", - event.explanation - ))); - }, - _ => {}, + match event { + EiEvent::SeatAdded(event) => { + event.seat.bind_capabilities(&[ + DeviceCapability::PointerAbsolute, + DeviceCapability::Pointer, + DeviceCapability::Button, + DeviceCapability::Scroll, + DeviceCapability::Keyboard, + ]); + self.context.flush().map_err(|err| { + DesktopError::input_failed(format!("libei bind seat: {err}")) + })?; + }, + EiEvent::DeviceAdded(event) => { + if event + .device + .has_capability(DeviceCapability::PointerAbsolute) + { + pending_pointer = Some(event.device.clone()); + } + if event.device.has_capability(DeviceCapability::Keyboard) { + pending_keyboard = Some(event.device); + } + }, + EiEvent::DeviceResumed(event) => { + if pending_pointer.as_ref() == Some(&event.device) { + self.pointer = + Some(EiDevice { device: event.device.clone(), serial: event.serial }); + } + if pending_keyboard.as_ref() == Some(&event.device) { + self.keyboard = Some(EiDevice { device: event.device, serial: event.serial }); + } + }, + EiEvent::Disconnected(event) => { + return Err(DesktopError::input_failed(format!( + "libei disconnected: {}", + event.explanation + ))); + }, + _ => {}, + } + if targets.is_complete(self.pointer.is_some(), self.keyboard.is_some()) { + break; + } + if drain_deadline.is_none() && (self.pointer.is_some() || self.keyboard.is_some()) { + drain_deadline = Some(tokio::time::Instant::now() + DEVICE_DISCOVERY_DRAIN_TIMEOUT); + } } - if (pending_pointer.is_none() || self.pointer.is_some()) - && (pending_keyboard.is_none() || self.keyboard.is_some()) - && (self.pointer.is_some() || self.keyboard.is_some()) - { - break; - } - } - Ok(()) + Ok(()) + }) } fn timestamp() -> u64 { @@ -530,3 +572,16 @@ fn evdev_char(character: char) -> Option<(u32, bool)> { ); Some((code, shift)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn discovery_waits_for_every_granted_device() { + let targets = DiscoveryTargets { pointer: true, keyboard: true }; + + assert!(!targets.is_complete(false, true)); + assert!(targets.is_complete(true, true)); + } +} diff --git a/packages/natives/CHANGELOG.md b/packages/natives/CHANGELOG.md index 653a1afc6..5f7710fd5 100644 --- a/packages/natives/CHANGELOG.md +++ b/packages/natives/CHANGELOG.md @@ -14,6 +14,7 @@ - Fixed a rustc ICE building `maudio` for `x86_64-pc-windows-msvc` under the pinned rustup nightly by capping that package at `opt-level = 1`; MIR const-folding turned `MaybeUninit` into an `Uninit` operand that codegen rejects for a ScalarPair argument. - Fixed synthesized macOS keyboard and pointer events suppressing the user's physical input. - Fixed read-only Wayland `computer` calls acquiring persistent keyboard and pointer control; RemoteDesktop input permission is now requested only on first input, is not persisted, and closes with the desktop session ([#7884](https://github.com/can1357/oh-my-pi/issues/7884)). +- Fixed GNOME Wayland pointer input failing after successful portal authorization when mutter announced the absolute-pointer device after the keyboard ([#7926](https://github.com/can1357/oh-my-pi/issues/7926)). - Fixed Wayland `libei` input initialization poisoning PipeWire screen capture: both paths now share one long-lived Tokio runtime so `ashpd`'s process-global D-Bus connection is never orphaned by a dropped runtime ([#7886](https://github.com/can1357/oh-my-pi/issues/7886)). - Fixed the `wayland-pipewire` Cargo feature failing to compile: the PipeWire capture path still used the removed 0.8 `MainLoop::new` / `Context::new` / `connect_fd` constructors instead of the 0.9 `MainLoopRc` / `ContextRc` / `connect_fd_rc` handle API ([#7885](https://github.com/can1357/oh-my-pi/issues/7885)). - Removed the orphaned world-readable RemoteDesktop restore token that pre-fix builds wrote under `$XDG_STATE_HOME/omp/remote-desktop-token` during read-only calls; the Wayland backend now unlinks it on startup ([#7884](https://github.com/can1357/oh-my-pi/issues/7884)).