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:
Generated
+1
@@ -4995,6 +4995,7 @@ dependencies = [
|
||||
"flume",
|
||||
"fontdue",
|
||||
"foreign-types",
|
||||
"futures",
|
||||
"globset",
|
||||
"grep-matcher",
|
||||
"grep-pcre2",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)).
|
||||
|
||||
Reference in New Issue
Block a user