daemon: fast grab fallback (300ms) to non-grabbed evdev when grab produces no events

In VM environments, EVIOCGRAB on the AT keyboard device succeeds but
produces no events — the kernel/VM routing prevents event delivery
to the grabber. Previously the daemon waited 30 seconds then exited.

Now: after 3 consecutive 100ms poll timeouts (~300ms) with no events
received, the grab is released and the daemon continues in non-grabbed
evdev mode. In this mode events reach both X (characters appear on
screen) and the daemon simultaneously; the daemon applies backspace
corrections via uinput.

Also removes the 30-second-exit behavior (which locked the keyboard
for 30 seconds unnecessarily) and replaces it with the fast fallback.
This commit is contained in:
Khoa Vo 2026-07-02 13:41:01 +07:00
parent 24f9bc8c7e
commit 8d68edb321

View file

@ -1126,7 +1126,7 @@ fn run_with_evdev(
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let injector = create_injector(display)?; let injector = create_injector(display)?;
let grabbed = if daemon.grab_enabled { let mut grabbed = if daemon.grab_enabled {
match device.grab() { match device.grab() {
Ok(()) => { Ok(()) => {
log_info("[vietc] Keyboard grabbed — race condition eliminated"); log_info("[vietc] Keyboard grabbed — race condition eliminated");
@ -1157,10 +1157,14 @@ fn run_with_evdev(
// (catches in-terminal sudo prompts where window stays the same) // (catches in-terminal sudo prompts where window stays the same)
let mut password_check_counter: u32 = 0; let mut password_check_counter: u32 = 0;
// Safety: if grab is active and no events arrive for 30 seconds, // Safety: if grab is active and no events arrive for 3 seconds,
// release the grab so the user isn't locked out. // release the grab so the user isn't locked out, and continue in
// non-grabbed mode (events reach both X and the daemon; daemon
// applies backspace corrections via uinput).
let mut last_event_time = std::time::Instant::now(); let mut last_event_time = std::time::Instant::now();
let mut last_key_time = std::time::Instant::now(); let mut last_key_time = std::time::Instant::now();
// Track consecutive idle polls for fast grab fallback
let mut idle_polls: u32 = 0;
log_info("[vietc] Event loop started"); log_info("[vietc] Event loop started");
loop { loop {
@ -1174,13 +1178,17 @@ fn run_with_evdev(
return Ok(()); return Ok(());
} }
// Check for event timeout (grab safety) // Grab safety timeout: if the grabbed device produces no events
if grabbed && last_event_time.elapsed() > std::time::Duration::from_secs(30) { // (common in VMs where EVIOCGRAB breaks event delivery), release
// the grab after ~300ms idle and continue in non-grabbed mode
// where events reach both X and the daemon.
if grabbed && idle_polls >= 3 && last_event_time.elapsed() > std::time::Duration::from_millis(200) {
log_info( log_info(
"[vietc] No events for 30s — releasing grab timeout, releasing grab for safety", "[vietc] No events received via grab — releasing grab, continuing in non-grabbed evdev mode",
); );
let _ = device.ungrab(); let _ = device.ungrab();
return Ok(()); grabbed = false;
continue;
} }
// Poll evdev fd with 100ms timeout so the loop stays responsive // Poll evdev fd with 100ms timeout so the loop stays responsive
@ -1204,6 +1212,7 @@ fn run_with_evdev(
return Err(err.into()); return Err(err.into());
} }
if poll_ret == 0 { if poll_ret == 0 {
idle_polls += 1;
// No events available — check for background window changes even // No events available — check for background window changes even
// without a keypress (the background thread polls every 250ms). // without a keypress (the background thread polls every 250ms).
if daemon.config.app_state.enabled { if daemon.config.app_state.enabled {
@ -1215,6 +1224,7 @@ fn run_with_evdev(
} }
continue; continue;
} }
idle_polls = 0;
let caps = is_caps_lock_on(&device); let caps = is_caps_lock_on(&device);
let mut key_state = device let mut key_state = device