fix(capture): release peer keys on release-bind

When the user pressed the release-bind chord (typically all four
modifiers) the down events for the chord were forwarded to the peer
while capture was active, but the matching up events arrived after
the local tap flipped to passthrough and were never forwarded. The
peer was left with phantom held modifiers until either its watchdog
ran (1+ s) or the Leave message was processed — and Leave is sent
over UDP/DTLS and can be lost.

Drain the capture's pressed_keys set in release_capture and emit a
KeyboardEvent::Key{state: 0} for every still-held key, plus a
zeroed KeyboardEvent::Modifiers, before sending Leave. The receiver
already maintains pressed_keys per handle and processes these
key-up events through its normal path, so no protocol change is
required and an unmodified peer picks up the fix automatically.

The receiver-side release_keys safety net stays in place for the
genuine packet-loss / disconnect-without-Leave cases.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jon Kinney
2026-04-27 12:59:33 -05:00
committed by Ferdinand Schober
parent 10fd728804
commit 373e382152
2 changed files with 49 additions and 1 deletions

View File

@@ -171,6 +171,19 @@ impl InputCapture {
self.capture.release().await
}
/// Drain and return every key the capture has forwarded as
/// down-but-not-up. The caller is expected to synthesize key-up
/// events to the remote peer for each — otherwise the peer
/// retains phantom-held keys after capture is released. The
/// canonical case is the release-bind chord
/// (Ctrl+Shift+Alt+Meta): the down events were sent while
/// capture was active, but the matching up events arrive after
/// the local tap has flipped to passthrough and never reach
/// the peer.
pub fn take_pressed_keys(&mut self) -> HashSet<scancode::Linux> {
std::mem::take(&mut self.pressed_keys)
}
/// destroy the input capture
pub async fn terminate(&mut self) -> Result<(), CaptureError> {
self.capture.terminate().await

View File

@@ -8,7 +8,7 @@ use futures::StreamExt;
use input_capture::{
CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position,
};
use input_event::scancode;
use input_event::{Event, KeyboardEvent, scancode};
use lan_mouse_proto::ProtoEvent;
use local_channel::mpsc::{Receiver, Sender, channel};
use tokio::task::{JoinHandle, spawn_local};
@@ -376,6 +376,41 @@ impl CaptureTask {
async fn release_capture(&mut self, capture: &mut InputCapture) -> Result<(), CaptureError> {
// If we have an active client, notify them we're leaving
if let Some(handle) = self.active_client.take() {
// Synthesize key-up events for every key still held in the
// capture's pressed_keys set BEFORE sending Leave. Without
// this, pressing the release-bind chord (typically all four
// modifiers) leaves the peer with phantom held modifiers:
// the down events were forwarded while capture was active,
// but the matching up events arrive after the local tap
// flips to passthrough and never reach the peer. The peer
// then runs every subsequent keystroke through those held
// mods until its watchdog times out (1+ s) or our Leave
// arrives — and Leave can be lost over UDP/DTLS.
for key in capture.take_pressed_keys() {
let key_up = ProtoEvent::Input(Event::Keyboard(KeyboardEvent::Key {
time: 0,
key: key as u32,
state: 0,
}));
if let Err(e) = self.conn.send(key_up, handle).await {
log::warn!("failed to send key-up to client {handle}: {e}");
}
}
// Reset the modifier mask too. The peer's input-emulation
// layer keeps a separate XKB-style modifier state that's
// updated by KeyboardEvent::Modifiers, distinct from the
// pressed_keys set drained above. Without this, an
// already-locked CapsLock would survive the release.
let mods_zero = ProtoEvent::Input(Event::Keyboard(KeyboardEvent::Modifiers {
depressed: 0,
latched: 0,
locked: 0,
group: 0,
}));
if let Err(e) = self.conn.send(mods_zero, handle).await {
log::warn!("failed to reset modifiers on client {handle}: {e}");
}
log::info!("sending Leave event to client {handle}");
if let Err(e) = self.conn.send(ProtoEvent::Leave(0), handle).await {
log::warn!("failed to send Leave to client {handle}: {e}");