Compare commits

..

3 Commits

Author SHA1 Message Date
mrcha033
6b1eddef7a fix(libei): only restart the capture session on device change under GNOME
`EiEvent::DeviceRemoved` currently tears down and recreates the whole
input-capture session. That is a workaround for mutter, which stops
delivering events after a device change, but it is applied on every
compositor.

Elsewhere the restart is pure overhead: every device change costs a
CreateSession + ConnectToEIS round trip. On compositors that keep
per-session state around it is worse than overhead - Hyprland keeps a
keymap fd per EIS session and never reaps sessions whose client went
away, so the churn drives it out of file descriptors and eventually
crashes it. On this machine the churn ran at ~35 ConnectToEIS per 38
minutes; gating it to GNOME brought that to 0 with no change in capture
behaviour.

`SeatRemoved` still releases unconditionally - a seat going away really
does invalidate the session.

The default is derived from XDG_CURRENT_DESKTOP and can be overridden
with LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 for compositors that need
the mutter behaviour but do not advertise themselves as GNOME.
2026-08-31 14:11:10 +02:00
Ian Douglas Scott
09889cd839 chore: Minor code style improvements to Wayland code 2026-08-31 12:59:27 +02:00
Ian Douglas Scott
c194df791d fix(input-capture/layer_shell): Call read() on guard only once
With `wayland-backend/client_system` enabled, this was causing an
infinite loop since `read` never returned `WouldBlock`.

This seems to be an inconsistency in wayland-rs:
https://github.com/Smithay/wayland-rs/pull/959

In any case, it isn't necessary to call `read` more than once.
2026-08-31 12:59:27 +02:00
2 changed files with 46 additions and 29 deletions

View File

@@ -123,7 +123,6 @@ struct State {
focused: Option<Arc<Window>>,
global_list: GlobalList,
globals: Globals,
wayland_fd: RawFd,
read_guard: Option<ReadEventsGuard>,
qh: QueueHandle<Self>,
pending_events: VecDeque<(Position, CaptureEvent)>,
@@ -138,7 +137,7 @@ struct Inner {
impl AsRawFd for Inner {
fn as_raw_fd(&self) -> RawFd {
self.state.wayland_fd
self.queue.as_fd().as_raw_fd()
}
}
@@ -332,7 +331,6 @@ impl LayerShellInputCapture {
active_windows: Vec::new(),
focused: None,
qh,
wayland_fd: queue.as_fd().as_raw_fd(),
read_guard: None,
pending_events: VecDeque::new(),
outputs: vec![],
@@ -548,13 +546,12 @@ impl State {
}
impl Inner {
fn read(&mut self) -> bool {
fn read(&mut self) {
match self.state.read_guard.take().unwrap().read() {
Ok(_) => true,
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => false,
Ok(_) => {}
Err(WaylandError::Io(e)) if e.kind() == ErrorKind::WouldBlock => {}
Err(WaylandError::Io(e)) => {
log::error!("error reading from wayland socket: {e}");
false
}
Err(WaylandError::Protocol(e)) => {
panic!("wayland protocol violation: {e}")
@@ -564,16 +561,11 @@ impl Inner {
fn prepare_read(&mut self) -> io::Result<()> {
loop {
match self.queue.prepare_read() {
None => match self.queue.dispatch_pending(&mut self.state) {
Ok(_) => continue,
Err(DispatchError::Backend(WaylandError::Io(e))) => return Err(e),
Err(e) => panic!("failed to dispatch wayland events: {e}"),
},
Some(r) => {
self.state.read_guard = Some(r);
break Ok(());
}
if let Some(guard) = self.queue.prepare_read() {
self.state.read_guard = Some(guard);
break Ok(());
} else {
self.dispatch_events();
}
}
}
@@ -655,13 +647,7 @@ impl Stream for LayerShellInputCapture {
let inner = guard.get_inner_mut();
// read events
while inner.read() {
// prepare next read
match inner.prepare_read() {
Ok(_) => {}
Err(e) => return Poll::Ready(Some(Err(e.into()))),
}
}
inner.read();
// dispatch the events
inner.dispatch_events();

View File

@@ -18,12 +18,12 @@ use reis::{
use std::{
cell::Cell,
collections::HashMap,
io,
env, io,
num::NonZeroU32,
os::unix::net::UnixStream,
pin::Pin,
rc::Rc,
sync::Arc,
sync::{Arc, LazyLock},
task::{Context, Poll},
};
use tokio::{
@@ -50,6 +50,25 @@ use super::{
* prevents receiving further events after a session has been disabled once.
* Therefore the session needs to be recreated when the barriers are updated */
/* mutter also kills the session whenever ei devices come and go, so there the
* whole session has to be torn down and recreated on every device change.
* Elsewhere that is pure overhead: each restart costs a CreateSession +
* ConnectToEIS round trip, and compositors that keep per-session state around
* (hyprland leaks a keymap fd per eis session, see hyprwm/Hyprland) can be
* driven out of file descriptors by the churn.
* Set LM_RESTART_SESSION_ON_DEVICE_CHANGE=1/0 to override the default. */
static RESTART_SESSION_ON_DEVICE_CHANGE: LazyLock<bool> =
LazyLock::new(restart_session_on_device_change);
fn restart_session_on_device_change() -> bool {
match env::var("LM_RESTART_SESSION_ON_DEVICE_CHANGE").as_deref() {
Ok("1") => true,
Ok("0") => false,
_ => env::var("XDG_CURRENT_DESKTOP")
.is_ok_and(|desktops| desktops.to_uppercase().split(':').any(|d| d == "GNOME")),
}
}
/// events that necessitate restarting the capture session
#[derive(Clone, Copy, Debug)]
enum LibeiNotifyEvent {
@@ -555,20 +574,32 @@ async fn handle_ei_event(
s.seat.bind_capabilities(all_capabilities);
context.flush().map_err(|e| io::Error::new(e.kind(), e))?;
}
EiEvent::SeatRemoved(_) | /* EiEvent::DeviceAdded(_) | */ EiEvent::DeviceRemoved(_) => {
EiEvent::SeatRemoved(_) => {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
}
/* EiEvent::DeviceAdded(_) | */
EiEvent::DeviceRemoved(_) => {
if *RESTART_SESSION_ON_DEVICE_CHANGE {
log::debug!("releasing session: {ei_event:?}");
release_session.notify_waiters();
} else {
log::debug!("ignoring device change: {ei_event:?}");
}
}
EiEvent::DevicePaused(_) | EiEvent::DeviceResumed(_) => {}
EiEvent::DeviceStartEmulating(_) => log::debug!("START EMULATING"),
EiEvent::DeviceStopEmulating(_) => log::debug!("STOP EMULATING"),
EiEvent::Disconnected(d) => {
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)))
return Err(CaptureError::Disconnected(format!("{:?}", d.reason)));
}
_ => {
if let Some(pos) = current_client {
for event in Event::from_ei_event(ei_event) {
event_tx.send((pos, CaptureEvent::Input(event))).await.expect("no channel");
event_tx
.send((pos, CaptureEvent::Input(event)))
.await
.expect("no channel");
}
}
}