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
This commit is contained in:
roboomp
2026-08-07 16:12:29 +00:00
committed by can1357
parent 4981eba1c2
commit 216fc3ca2d
5 changed files with 131 additions and 72 deletions
Generated
+1
View File
@@ -4995,6 +4995,7 @@ dependencies = [
"flume",
"fontdue",
"foreign-types",
"futures",
"globset",
"grep-matcher",
"grep-pcre2",
+1
View File
@@ -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"
+1
View File
@@ -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
@@ -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<Self> {
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));
}
}
+1
View File
@@ -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<ma_fence>` 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)).