mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-30 01:05:47 +00:00
fix(keyboard): shortcuts, keep fired keys per session
FIRED_KEYS was process-global while dispatch is per session. Closing or entering any session cleared the fired keys of all of them, so a key still held in another session leaked its release to that remote, and a key fired in one session made the same key in another session look like a repeat. Key the fired set by session: try_dispatch resolves the session first and only looks at that session's keys, and session_close clears only the closing session. Entering a session view no longer clears anything: it is a pointer enter, and a fired key may still be physically held across it. A key whose release is missed otherwise heals itself: its next press is consumed as a repeat and that release removes it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ra4my61t8q1FN5n59wB16D
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
63fc0a3b20
commit
c69b2a60e6
+1
-2
@@ -263,7 +263,7 @@ pub fn will_session_close_close_session(session_id: SessionID) -> SyncReturn<boo
|
||||
|
||||
pub fn session_close(session_id: SessionID) {
|
||||
if let Some(session) = sessions::remove_session_by_session_id(&session_id) {
|
||||
crate::keyboard::shortcuts::clear_fired_keys();
|
||||
crate::keyboard::shortcuts::clear_fired_keys(&session_id);
|
||||
// `release_remote_keys` is not required for mobile platforms in common cases.
|
||||
// But we still call it to make the code more stable.
|
||||
#[cfg(any(target_os = "android", target_os = "ios"))]
|
||||
@@ -623,7 +623,6 @@ pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncRetur
|
||||
// so that two windows viewing the same peer get distinct grab owners.
|
||||
let window_id = _session_id.as_u128();
|
||||
if _enter {
|
||||
crate::keyboard::shortcuts::clear_fired_keys();
|
||||
set_cur_session_id_(_session_id, &keyboard_mode);
|
||||
crate::keyboard::client::change_grab_status(
|
||||
crate::common::GrabState::Run,
|
||||
|
||||
+81
-27
@@ -312,19 +312,22 @@ pub fn match_event(event: &rdev::Event) -> Option<String> {
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
lazy_static::lazy_static! {
|
||||
/// Physical keys whose press fired a shortcut and whose release has not
|
||||
/// arrived yet. Every event of such a key belongs to RustDesk until its
|
||||
/// release, whatever the modifiers do meanwhile: auto repeats and the
|
||||
/// release are consumed and never reach the remote.
|
||||
static ref FIRED_KEYS: std::sync::Mutex<std::collections::HashSet<rdev::Key>> =
|
||||
Default::default();
|
||||
/// Per session, the physical keys whose press fired a shortcut and whose
|
||||
/// release has not arrived yet. Every event of such a key belongs to
|
||||
/// RustDesk until its release, whatever the modifiers do meanwhile: auto
|
||||
/// repeats and the release are consumed and never reach the remote.
|
||||
static ref FIRED_KEYS: std::sync::Mutex<
|
||||
std::collections::HashMap<hbb_common::SessionID, std::collections::HashSet<rdev::Key>>,
|
||||
> = Default::default();
|
||||
}
|
||||
|
||||
/// Forget the fired keys whose release will never arrive, e.g. because focus
|
||||
/// left the session while they were held.
|
||||
/// Forget the fired keys of a session that is going away.
|
||||
///
|
||||
/// A key whose release is missed in other ways stays recorded: its next press
|
||||
/// is consumed as a repeat and its release then removes it.
|
||||
#[cfg(feature = "flutter")]
|
||||
pub fn clear_fired_keys() {
|
||||
FIRED_KEYS.lock().unwrap().clear();
|
||||
pub fn clear_fired_keys(session_id: &hbb_common::SessionID) {
|
||||
FIRED_KEYS.lock().unwrap().remove(session_id);
|
||||
}
|
||||
|
||||
/// Match `event` against the cached bindings; if it matched, push a
|
||||
@@ -350,17 +353,6 @@ pub fn try_dispatch(
|
||||
send: impl Fn(&base::message_proto::KeyEvent),
|
||||
) -> bool {
|
||||
use rdev::EventType;
|
||||
{
|
||||
let mut fired = FIRED_KEYS.lock().unwrap();
|
||||
match event.event_type {
|
||||
EventType::KeyPress(k) if fired.contains(&k) => return true,
|
||||
EventType::KeyRelease(k) if fired.remove(&k) => return true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let Some(action_id) = match_event(event) else {
|
||||
return false;
|
||||
};
|
||||
let resolved;
|
||||
let sid = match session_id {
|
||||
Some(id) => id,
|
||||
@@ -369,9 +361,27 @@ pub fn try_dispatch(
|
||||
&resolved
|
||||
}
|
||||
};
|
||||
{
|
||||
let mut all = FIRED_KEYS.lock().unwrap();
|
||||
if let Some(fired) = all.get_mut(sid) {
|
||||
match event.event_type {
|
||||
EventType::KeyPress(k) if fired.contains(&k) => return true,
|
||||
EventType::KeyRelease(k) if fired.remove(&k) => {
|
||||
if fired.is_empty() {
|
||||
all.remove(sid);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
let Some(action_id) = match_event(event) else {
|
||||
return false;
|
||||
};
|
||||
release_remote_keys(keyboard_mode, &peer(), &send);
|
||||
if let EventType::KeyPress(k) = event.event_type {
|
||||
FIRED_KEYS.lock().unwrap().insert(k);
|
||||
FIRED_KEYS.lock().unwrap().entry(*sid).or_default().insert(k);
|
||||
}
|
||||
crate::flutter::push_session_event(sid, "shortcut_triggered", vec![("action", &action_id)]);
|
||||
true
|
||||
@@ -816,9 +826,9 @@ mod tests {
|
||||
// was not already fired before the call.
|
||||
let fired = std::cell::RefCell::new(Vec::new());
|
||||
let dispatch = |e: &rdev::Event| {
|
||||
let before = FIRED_KEYS.lock().unwrap().clone();
|
||||
let before = fired_keys_of(&SID_A);
|
||||
let action = match_event(e);
|
||||
let hit = try_dispatch(None, e, "map", || "windows".into(), |_| {});
|
||||
let hit = try_dispatch(Some(&SID_A), e, "map", || "windows".into(), |_| {});
|
||||
if let rdev::EventType::KeyPress(k) = e.event_type {
|
||||
if hit && !before.contains(&k) {
|
||||
fired.borrow_mut().push(action.unwrap_or_default());
|
||||
@@ -839,7 +849,7 @@ mod tests {
|
||||
assert!(dispatch(&make_press(Key::KeyP)), "P repeat is consumed");
|
||||
assert!(dispatch(&make_release(Key::KeyP)), "P release is consumed");
|
||||
assert!(dispatch(&make_release(Key::KeyC)), "C release is consumed");
|
||||
assert!(FIRED_KEYS.lock().unwrap().is_empty());
|
||||
assert!(fired_keys_of(&SID_A).is_empty());
|
||||
assert_eq!(
|
||||
*fired.borrow(),
|
||||
vec![action_id::SCREENSHOT.to_owned(), action_id::TOGGLE_CHAT.to_owned()]
|
||||
@@ -856,7 +866,8 @@ mod tests {
|
||||
|
||||
let _guard = CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let chord = enable_defaults_and_hold_chord();
|
||||
let dispatch = |e: &rdev::Event| try_dispatch(None, e, "map", || "windows".into(), |_| {});
|
||||
let dispatch =
|
||||
|e: &rdev::Event| try_dispatch(Some(&SID_A), e, "map", || "windows".into(), |_| {});
|
||||
|
||||
assert!(dispatch(&make_press(Key::KeyP)));
|
||||
release_chord(chord);
|
||||
@@ -866,6 +877,48 @@ mod tests {
|
||||
assert!(!dispatch(&make_release(Key::KeyP)));
|
||||
}
|
||||
|
||||
/// Fired keys are owned per session: closing another session keeps them,
|
||||
/// and the same physical key in another session is not taken as a repeat.
|
||||
#[cfg(feature = "flutter")]
|
||||
#[test]
|
||||
fn dispatch_keeps_fired_keys_per_session() {
|
||||
use rdev::Key;
|
||||
|
||||
let _guard = CACHE_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let chord = enable_defaults_and_hold_chord();
|
||||
let dispatch = |sid: &hbb_common::SessionID, e: &rdev::Event| {
|
||||
try_dispatch(Some(sid), e, "map", || "windows".into(), |_| {})
|
||||
};
|
||||
|
||||
assert!(dispatch(&SID_A, &make_press(Key::KeyP)));
|
||||
release_chord(chord);
|
||||
assert!(
|
||||
!dispatch(&SID_B, &make_press(Key::KeyP)),
|
||||
"P in session B is not a repeat of session A's P"
|
||||
);
|
||||
assert!(!dispatch(&SID_B, &make_release(Key::KeyP)));
|
||||
clear_fired_keys(&SID_B);
|
||||
assert!(dispatch(&SID_A, &make_press(Key::KeyP)), "A's repeat survives B's close");
|
||||
assert!(dispatch(&SID_A, &make_release(Key::KeyP)), "A's release survives B's close");
|
||||
assert!(fired_keys_of(&SID_A).is_empty());
|
||||
|
||||
let chord = enable_defaults_and_hold_chord();
|
||||
assert!(dispatch(&SID_A, &make_press(Key::KeyP)));
|
||||
release_chord(chord);
|
||||
clear_fired_keys(&SID_A);
|
||||
assert!(!dispatch(&SID_A, &make_release(Key::KeyP)), "closing A drops its keys");
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
const SID_A: hbb_common::SessionID = hbb_common::SessionID::from_u128(0xA);
|
||||
#[cfg(feature = "flutter")]
|
||||
const SID_B: hbb_common::SessionID = hbb_common::SessionID::from_u128(0xB);
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
fn fired_keys_of(sid: &hbb_common::SessionID) -> std::collections::HashSet<rdev::Key> {
|
||||
FIRED_KEYS.lock().unwrap().get(sid).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(feature = "flutter")]
|
||||
fn enable_defaults_and_hold_chord() -> [rdev::Key; 3] {
|
||||
use base::message_proto::KeyboardMode;
|
||||
@@ -876,7 +929,8 @@ mod tests {
|
||||
pass_through: false,
|
||||
bindings: default_bindings(),
|
||||
});
|
||||
clear_fired_keys();
|
||||
clear_fired_keys(&SID_A);
|
||||
clear_fired_keys(&SID_B);
|
||||
let primary = if cfg!(any(target_os = "macos", target_os = "ios")) {
|
||||
Key::MetaLeft
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user