From 06e23007edbe01d6ed46ffad37117ed33808d074 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Fri, 9 Oct 2026 03:43:14 +0800 Subject: [PATCH] [fix] restore Windows remote keyboard shortcuts and input recovery --- .../devices/session_device_manager.cpp | 67 ++-- .../features/devices/session_device_manager.h | 19 +- .../features/input/captured_keyboard_queue.h | 75 +++++ .../features/input/keyboard_controller.cpp | 220 ++++++------ .../gui/features/input/keyboard_controller.h | 24 +- apps/desktop/src/gui/ui/stream_window.slint | 14 +- .../platform/common/input/keyboard_state.h | 71 ++++ .../input/keyboard/keyboard_capturer.cpp | 314 +++++++++++------- .../input/keyboard/keyboard_capturer.h | 47 ++- .../input/windows_raw_keyboard_guard.h | 87 +++++ 10 files changed, 626 insertions(+), 312 deletions(-) create mode 100644 apps/desktop/src/gui/features/input/captured_keyboard_queue.h create mode 100644 apps/desktop/src/platform/common/input/keyboard_state.h create mode 100644 apps/desktop/src/platform/windows/input/windows_raw_keyboard_guard.h diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.cpp b/apps/desktop/src/gui/features/devices/session_device_manager.cpp index 47914cc3..2195611f 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -27,8 +27,10 @@ namespace crossdesk { namespace { constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500; -constexpr size_t kMaxCapturedKeyboardInputs = 512; constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1); +#ifdef _WIN32 +constexpr auto kKeyboardCaptureRetryDelay = std::chrono::seconds(1); +#endif } // namespace @@ -419,8 +421,12 @@ int SessionDeviceManager::StopMouseController() { } int SessionDeviceManager::StartKeyboardCapturer() { - ClearCapturedKeyboardInput(); + captured_keyboard_inputs_.Clear(); owner_.keyboard_capturer_uses_window_events_ = false; +#ifdef _WIN32 + next_keyboard_capture_retry_ = + std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay; +#endif #ifdef __APPLE__ if (!owner_.EnsureMacAccessibilityPermission()) { @@ -449,8 +455,10 @@ int SessionDeviceManager::StartKeyboardCapturer() { void *user_ptr) { if (user_ptr) { auto *devices = static_cast(user_ptr); - devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code, - extended); + // Native hooks only enqueue; forwarding and logging stay off the + // hook thread so they cannot trigger LowLevelHooksTimeout. + devices->captured_keyboard_inputs_.Push( + {key_code, is_down, scan_code, extended}); } }, this); @@ -468,7 +476,7 @@ int SessionDeviceManager::StartKeyboardCapturer() { int SessionDeviceManager::StopKeyboardCapturer() { owner_.keyboard_capturer_is_started_ = false; if (keyboard_capturer_) keyboard_capturer_->Unhook(); - ClearCapturedKeyboardInput(); + captured_keyboard_inputs_.Clear(); if (owner_.keyboard_capturer_uses_window_events_) { owner_.keyboard_capturer_uses_window_events_ = false; LOG_INFO("Stop keyboard capturer with Slint keyboard backend"); @@ -602,7 +610,18 @@ void SessionDeviceManager::UpdateInteractions() { #endif } - if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_) { + if (owner_.start_keyboard_capturer_ && owner_.focus_on_stream_window_ && + !owner_.privacy_.Snapshot().input_blocked) { +#ifdef _WIN32 + if (owner_.keyboard_capturer_is_started_ && + owner_.keyboard_capturer_uses_window_events_ && keyboard_capturer_ && + std::chrono::steady_clock::now() >= next_keyboard_capture_retry_) { + // A transient installation failure or stalled hook must not leave + // system shortcuts in the window-event fallback until focus changes. + StopKeyboardCapturer(); + owner_.keyboard_.ForceReleasePressedKeys(); + } +#endif if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) { owner_.keyboard_capturer_is_started_ = true; } @@ -613,6 +632,10 @@ void SessionDeviceManager::UpdateInteractions() { owner_.keyboard_.ForceReleasePressedKeys(); owner_.keyboard_capturer_uses_window_events_ = true; owner_.keyboard_capturer_is_started_ = true; +#ifdef _WIN32 + next_keyboard_capture_retry_ = + std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay; +#endif LOG_WARN("Native keyboard capture stopped; using Slint keyboard events"); } DrainCapturedKeyboardInput(); @@ -621,43 +644,25 @@ void SessionDeviceManager::UpdateInteractions() { } else if (owner_.keyboard_capturer_is_started_) { owner_.keyboard_.ForceReleasePressedKeys(); StopKeyboardCapturer(); - owner_.keyboard_capturer_is_started_ = false; } owner_.keyboard_.CheckRemoteTimeouts(); } -void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code, - bool is_down, - uint32_t scan_code, - bool extended) { - std::lock_guard lock(captured_keyboard_inputs_mutex_); - if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) { - captured_keyboard_inputs_.pop_front(); - LOG_WARN("Captured keyboard input queue overflow, dropping oldest event"); - } - captured_keyboard_inputs_.push_back( - CapturedKeyboardInput{key_code, is_down, scan_code, extended}); -} - void SessionDeviceManager::DrainCapturedKeyboardInput() { - std::deque inputs; - { - std::lock_guard lock(captured_keyboard_inputs_mutex_); - inputs.swap(captured_keyboard_inputs_); - } + const auto inputs = captured_keyboard_inputs_.Drain(); if (owner_.privacy_.Snapshot().input_blocked) return; - for (const CapturedKeyboardInput &input : inputs) { + if (inputs.resync) { + LOG_WARN("Captured keyboard queue overflow; reconciling pressed keys"); + owner_.keyboard_.RecoverCapturedState(inputs.pressed); + return; + } + for (const KeyboardInput &input : inputs.events) { owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down, input.scan_code, input.extended); } } -void SessionDeviceManager::ClearCapturedKeyboardInput() { - std::lock_guard lock(captured_keyboard_inputs_mutex_); - captured_keyboard_inputs_.clear(); -} - bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code, bool extended) { diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.h b/apps/desktop/src/gui/features/devices/session_device_manager.h index 4984375c..def0dd11 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.h +++ b/apps/desktop/src/gui/features/devices/session_device_manager.h @@ -7,12 +7,10 @@ #ifndef _SESSION_DEVICE_MANAGER_H_ #define _SESSION_DEVICE_MANAGER_H_ - #include #include #include #include -#include #include #include #include @@ -21,6 +19,7 @@ #include "device_controller.h" #include "display_info.h" +#include "features/input/captured_keyboard_queue.h" #include "speaker_capture_controller.h" struct SDL_AudioStream; @@ -83,17 +82,7 @@ public: std::vector host_display_list() const; private: - struct CapturedKeyboardInput { - int key_code = 0; - bool is_down = false; - uint32_t scan_code = 0; - bool extended = false; - }; - - void QueueCapturedKeyboardInput(int key_code, bool is_down, - uint32_t scan_code, bool extended); void DrainCapturedKeyboardInput(); - void ClearCapturedKeyboardInput(); bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now, int fps); bool ShouldCaptureCursor() const; @@ -115,8 +104,10 @@ private: mutable std::mutex display_info_mutex_; std::vector display_info_list_; size_t registered_display_stream_count_ = 0; - std::deque captured_keyboard_inputs_; - std::mutex captured_keyboard_inputs_mutex_; + CapturedKeyboardQueue captured_keyboard_inputs_; +#ifdef _WIN32 + std::chrono::steady_clock::time_point next_keyboard_capture_retry_{}; +#endif std::chrono::steady_clock::time_point last_frame_time_{}; std::chrono::steady_clock::time_point next_frame_deadline_{}; std::mutex capture_metrics_mutex_; diff --git a/apps/desktop/src/gui/features/input/captured_keyboard_queue.h b/apps/desktop/src/gui/features/input/captured_keyboard_queue.h new file mode 100644 index 00000000..d361c97a --- /dev/null +++ b/apps/desktop/src/gui/features/input/captured_keyboard_queue.h @@ -0,0 +1,75 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-09 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _CAPTURED_KEYBOARD_QUEUE_H_ +#define _CAPTURED_KEYBOARD_QUEUE_H_ + +#include +#include +#include +#include + +#include "keyboard_state.h" + +namespace crossdesk { + +class CapturedKeyboardQueue { + public: + struct Batch { + std::vector events; + bool resync = false; + PressedKeys pressed; + }; + + void Push(const KeyboardInput& input) { + std::lock_guard lock(mutex_); + if (input.is_down) + pressed_[input.key_code] = + {input.key_code, input.scan_code, input.extended}; + else + pressed_.erase(input.key_code); + + if (resync_) return; + if (count_ == kCapacity) { + // The event history is incomplete. Retain the authoritative state, + // including releases, rather than dropping arbitrary key edges. + count_ = 0; + resync_ = true; + return; + } + events_[count_++] = input; + } + + Batch Drain() { + std::lock_guard lock(mutex_); + Batch batch; + batch.resync = resync_; + if (resync_) batch.pressed = pressed_; + else batch.events.assign(events_.begin(), events_.begin() + count_); + count_ = 0; + resync_ = false; + return batch; + } + + void Clear() { + std::lock_guard lock(mutex_); + count_ = 0; + resync_ = false; + pressed_.clear(); + } + + private: + static constexpr size_t kCapacity = 512; + std::mutex mutex_; + std::array events_{}; + size_t count_ = 0; + bool resync_ = false; + PressedKeys pressed_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/features/input/keyboard_controller.cpp b/apps/desktop/src/gui/features/input/keyboard_controller.cpp index 2604dd78..435ed306 100644 --- a/apps/desktop/src/gui/features/input/keyboard_controller.cpp +++ b/apps/desktop/src/gui/features/input/keyboard_controller.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include #include "minirtc.h" @@ -26,6 +26,7 @@ namespace { constexpr uint32_t kHeartbeatIntervalMs = 500; constexpr uint32_t kRemoteReleaseTimeoutMs = 2500; +constexpr uint32_t kReleaseRetryIntervalMs = 250; int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code, bool extended) { @@ -99,29 +100,38 @@ void KeyboardController::TrackPressedKey(int key_code, bool is_down, uint32_t scan_code, bool extended) { std::lock_guard lock(pressed_keys_mutex_); if (is_down) { - pressed_keys_[key_code] = PressedKey{key_code, scan_code, extended}; + pressed_keys_[key_code] = KeyboardKey{key_code, scan_code, extended}; } else { pressed_keys_.erase(key_code); } } void KeyboardController::ForceReleasePressedKeys() { - std::vector pressed_keys; + PressedKeys pressed_keys; { std::lock_guard lock(pressed_keys_mutex_); - pressed_keys.reserve(pressed_keys_.size()); - for (const auto& [_, key] : pressed_keys_) { - pressed_keys.push_back(key); - } - pressed_keys_.clear(); + pressed_keys.swap(pressed_keys_); } - for (const PressedKey& key : pressed_keys) { + for (const auto& [_, key] : pressed_keys) { SendKeyCommand(key.key_code, false, key.scan_code, key.extended); } SendHeartbeat(true); } +void KeyboardController::RecoverCapturedState(const PressedKeys& pressed) { + std::vector changes; + { + std::lock_guard lock(pressed_keys_mutex_); + changes = ReconcileKeyboardState(pressed_keys_, pressed); + } + for (const auto& event : changes) { + SendKeyCommand(event.key_code, event.is_down, event.scan_code, + event.extended); + } + SendHeartbeat(true); +} + void KeyboardController::SendHeartbeat(bool force) { const uint32_t now = static_cast(SDL_GetTicks()); if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) { @@ -246,7 +256,9 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down, "Secure desktop keyboard injection transient failure, " "key_code={}, is_down={}, response={}", key_code, is_down, response); - return {true, false}; + // The release was not injected. Keep it tracked so cleanup can + // retry on the ordinary desktop after the transition. + return {false, false}; } LogSecureDesktopInputBlocked( @@ -269,17 +281,18 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down, void KeyboardController::ApplyRemoteEvent(const std::string& remote_id, const RemoteAction& action) { + std::lock_guard input_lock(remote_input_mutex_); const int key_code = static_cast(action.k.key_value); const bool is_down = action.k.flag == KeyFlag::key_down; const bool injected = InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended); + if (injected) pending_key_releases_.erase(key_code); - std::lock_guard lock(remote_states_mutex_); auto& state = remote_states_[remote_id]; state.last_seen_tick = static_cast(SDL_GetTicks()); if (is_down && injected) { state.pressed_keys[key_code] = - PressedKey{key_code, action.k.scan_code, action.k.extended}; + KeyboardKey{key_code, action.k.scan_code, action.k.extended}; } else if (!is_down && injected) { state.pressed_keys.erase(key_code); } @@ -287,136 +300,117 @@ void KeyboardController::ApplyRemoteEvent(const std::string& remote_id, void KeyboardController::ApplyRemoteState(const std::string& remote_id, const RemoteAction& action) { - std::vector keys_to_release; - std::vector keys_to_press; - { - std::lock_guard lock(remote_states_mutex_); - auto& state = remote_states_[remote_id]; - if (action.ks.seq != 0 && state.last_seq != 0 && - static_cast(action.ks.seq - state.last_seq) <= 0) { - return; - } - - state.last_seq = action.ks.seq; - state.last_seen_tick = static_cast(SDL_GetTicks()); - state.keyboard_state_seen = true; - - std::unordered_map desired_keys; - const size_t count = - (std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys); - for (size_t index = 0; index < count; ++index) { - const auto& key = action.ks.pressed_keys[index]; - const int key_code = static_cast(key.key_value); - desired_keys[key_code] = - PressedKey{key_code, key.scan_code, key.extended}; - } - for (const auto& [key_code, key] : state.pressed_keys) { - if (desired_keys.find(key_code) == desired_keys.end()) { - keys_to_release.push_back(key); - } - } - for (const auto& [key_code, key] : desired_keys) { - if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) { - keys_to_press.push_back(key); - } - } + std::lock_guard input_lock(remote_input_mutex_); + auto& state = remote_states_[remote_id]; + if (action.ks.seq != 0 && state.last_seq != 0 && + static_cast(action.ks.seq - state.last_seq) <= 0) { + return; } - for (const PressedKey& key : keys_to_release) { - if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) { - std::lock_guard lock(remote_states_mutex_); - const auto state_it = remote_states_.find(remote_id); - if (state_it != remote_states_.end()) { - state_it->second.pressed_keys.erase(key.key_code); - } - } + state.last_seq = action.ks.seq; + state.last_seen_tick = static_cast(SDL_GetTicks()); + state.keyboard_state_seen = true; + + PressedKeys desired_keys; + const size_t count = + (std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys); + for (size_t index = 0; index < count; ++index) { + const auto& key = action.ks.pressed_keys[index]; + const int key_code = static_cast(key.key_value); + desired_keys[key_code] = {key_code, key.scan_code, key.extended}; } - for (const PressedKey& key : keys_to_press) { - if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) { - std::lock_guard lock(remote_states_mutex_); - remote_states_[remote_id].pressed_keys[key.key_code] = key; + const auto changes = ReconcileKeyboardState(state.pressed_keys, desired_keys); + bool can_press = true; + for (const auto& key : changes) { + if (key.is_down && !can_press) continue; + if (!InjectRemoteKey(key.key_code, key.is_down, key.scan_code, key.extended)) { + // Do not replay a shortcut with a stale/missing modifier. The next + // heartbeat will retry from the state that was actually injected. + can_press = false; + continue; } + pending_key_releases_.erase(key.key_code); + if (key.is_down) + state.pressed_keys[key.key_code] = + {key.key_code, key.scan_code, key.extended}; + else + state.pressed_keys.erase(key.key_code); } } void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id, const char* reason) { - std::vector keys_to_release; - { - std::lock_guard lock(remote_states_mutex_); - const auto state_it = remote_states_.find(remote_id); - if (state_it == remote_states_.end()) { - return; - } - for (const auto& [_, key] : state_it->second.pressed_keys) { - keys_to_release.push_back(key); - } - remote_states_.erase(state_it); - } + std::lock_guard input_lock(remote_input_mutex_); + ReleaseRemotePressedKeysLocked(remote_id, reason); +} + +void KeyboardController::ReleaseRemotePressedKeysLocked( + std::string remote_id, const char* reason) { + const auto state_it = remote_states_.find(remote_id); + if (state_it == remote_states_.end()) return; + auto keys_to_release = std::move(state_it->second.pressed_keys); + remote_states_.erase(state_it); if (!keys_to_release.empty()) { LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}", keys_to_release.size(), remote_id, reason ? reason : "unknown"); } - for (const PressedKey& key : keys_to_release) { + for (const auto& [code, key] : keys_to_release) { if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) { -#ifdef _WIN32 - if (owner_.privacy_.Engaged()) { - std::lock_guard lock(remote_states_mutex_); - pending_privacy_releases_[key.key_code] = key; - } -#endif + pending_key_releases_[code] = key; + } else { + pending_key_releases_.erase(code); } } } void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) { - std::vector remotes; { - std::lock_guard lock(remote_states_mutex_); - for (const auto& [id, state] : remote_states_) remotes.push_back(id); + std::lock_guard input_lock(remote_input_mutex_); + while (!remote_states_.empty()) + ReleaseRemotePressedKeysLocked(remote_states_.begin()->first, reason); } - for (const auto& id : remotes) ReleaseRemotePressedKeys(id, reason); CheckRemoteTimeouts(); } void KeyboardController::CheckRemoteTimeouts() { -#ifdef _WIN32 - bool pending = false; - { - std::lock_guard lock(remote_states_mutex_); - pending = !pending_privacy_releases_.empty(); - } - // A key-up rejected on the secure desktop is retained until the ordinary - // desktop returns. Never route cleanup through the security UI helper. - if (pending && IsWindowsPrivacyDesktopAvailable()) { - std::unordered_map releases; - { - std::lock_guard lock(remote_states_mutex_); - releases.swap(pending_privacy_releases_); - } - for (const auto& [code, key] : releases) { - if (!owner_.devices_.SendKeyboardCommand(code, false, key.scan_code, key.extended)) { - std::lock_guard lock(remote_states_mutex_); - pending_privacy_releases_[code] = key; - } - } - } -#endif + // Do not stall the UI while a transport thread waits on desktop IPC. + std::unique_lock input_lock(remote_input_mutex_, std::try_to_lock); + if (!input_lock.owns_lock()) return; const uint32_t now = static_cast(SDL_GetTicks()); - std::vector timed_out_remotes; - { - std::lock_guard lock(remote_states_mutex_); - for (const auto& [remote_id, state] : remote_states_) { - if (state.keyboard_state_seen && !state.pressed_keys.empty() && - state.last_seen_tick != 0 && - now - state.last_seen_tick > kRemoteReleaseTimeoutMs) { - timed_out_remotes.push_back(remote_id); - } + // Retain failed releases in ordinary mode as well as privacy mode. Retry + // only on the ordinary desktop, never through the security UI helper. + if (!pending_key_releases_.empty() && + (last_release_retry_tick_ == 0 || + now - last_release_retry_tick_ >= kReleaseRetryIntervalMs) +#ifdef _WIN32 + && IsCurrentSessionUserDesktopActive() +#endif + ) { + last_release_retry_tick_ = now; + for (auto it = pending_key_releases_.begin(); + it != pending_key_releases_.end();) { + const auto& key = it->second; + if (owner_.devices_.SendKeyboardCommand( + key.key_code, false, key.scan_code, key.extended)) + it = pending_key_releases_.erase(it); + else + ++it; } } - for (const std::string& remote_id : timed_out_remotes) { - ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout"); + + // Keep the timeout decision and release under the same lock, so a fresh + // heartbeat cannot arrive between selecting a stale peer and clearing it. + for (auto it = remote_states_.begin(); it != remote_states_.end();) { + const auto& state = it->second; + if (state.keyboard_state_seen && !state.pressed_keys.empty() && + state.last_seen_tick != 0 && + now - state.last_seen_tick > kRemoteReleaseTimeoutMs) { + const auto expired = it++; + ReleaseRemotePressedKeysLocked(expired->first, "keyboard_heartbeat_timeout"); + } else { + ++it; + } } } diff --git a/apps/desktop/src/gui/features/input/keyboard_controller.h b/apps/desktop/src/gui/features/input/keyboard_controller.h index 0dd3c4c3..09b0108e 100644 --- a/apps/desktop/src/gui/features/input/keyboard_controller.h +++ b/apps/desktop/src/gui/features/input/keyboard_controller.h @@ -8,7 +8,7 @@ #include -#include "device_controller.h" +#include "keyboard_state.h" namespace crossdesk { @@ -23,6 +23,7 @@ public: int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0, bool extended = false); void ForceReleasePressedKeys(); + void RecoverCapturedState(const PressedKeys& pressed); void SendHeartbeat(bool force); void ApplyRemoteEvent(const std::string &remote_id, const RemoteAction &remote_action); @@ -34,14 +35,8 @@ public: void CheckRemoteTimeouts(); private: - struct PressedKey { - int key_code = 0; - uint32_t scan_code = 0; - bool extended = false; - }; - struct RemoteState { - std::unordered_map pressed_keys; + PressedKeys pressed_keys; uint32_t last_seq = 0; uint32_t last_seen_tick = 0; bool keyboard_state_seen = false; @@ -51,17 +46,20 @@ private: bool extended); bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code, bool extended); + // Caller holds remote_input_mutex_. Copy the id because this erases state. + void ReleaseRemotePressedKeysLocked(std::string remote_id, const char* reason); GuiRuntime &owner_; - std::unordered_map pressed_keys_; + PressedKeys pressed_keys_; std::mutex pressed_keys_mutex_; uint32_t state_sequence_ = 0; uint32_t last_heartbeat_tick_ = 0; std::unordered_map remote_states_; - std::mutex remote_states_mutex_; -#ifdef _WIN32 - std::unordered_map pending_privacy_releases_; -#endif + // Guards all remote key state and serializes injection, cleanup and retries. + std::mutex remote_input_mutex_; + // Successful new input supersedes cleanup of an older press of that key. + PressedKeys pending_key_releases_; + uint32_t last_release_retry_tick_ = 0; }; } // namespace crossdesk diff --git a/apps/desktop/src/gui/ui/stream_window.slint b/apps/desktop/src/gui/ui/stream_window.slint index 0aa9e331..bc6137a8 100644 --- a/apps/desktop/src/gui/ui/stream_window.slint +++ b/apps/desktop/src/gui/ui/stream_window.slint @@ -282,9 +282,21 @@ export component StreamWindow inherits Window { root.display-menu-open = false; root.shortcut-menu-open = false; } - input-focus.focus(); + // Changed handlers may run while Winit/AccessKit is building the + // accessibility tree. Changing focus there re-enters its borrowed + // adapter and aborts the process; defer focus to the event loop. + input-focus-timer.running = true; + input-focus-timer.restart(); root.keyboard-focus-changed(!root.remote-update-visible); } + input-focus-timer := Timer { + interval: 1ms; + running: false; + triggered => { + self.running = false; + input-focus.focus(); + } + } private property control-tooltip-visible: false; private property hovered-control: { if root.window-active && !root.control-dragging diff --git a/apps/desktop/src/platform/common/input/keyboard_state.h b/apps/desktop/src/platform/common/input/keyboard_state.h new file mode 100644 index 00000000..131c2a21 --- /dev/null +++ b/apps/desktop/src/platform/common/input/keyboard_state.h @@ -0,0 +1,71 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-09 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _KEYBOARD_STATE_H_ +#define _KEYBOARD_STATE_H_ + +#include +#include +#include +#include + +namespace crossdesk { + +struct KeyboardKey { + int key_code = 0; + uint32_t scan_code = 0; + bool extended = false; +}; + +using PressedKeys = std::map; + +struct KeyboardInput { + int key_code = 0; + bool is_down = false; + uint32_t scan_code = 0; + bool extended = false; +}; + +inline bool IsKeyboardModifier(int key_code) { + return (key_code >= 0x10 && key_code <= 0x12) || + (key_code >= 0xA0 && key_code <= 0xA5) || + key_code == 0x5B || key_code == 0x5C; +} + +// Reconcile state after capture overflow without replaying discarded shortcuts. +// Keep keys that are still held, release stale keys, then restore modifiers +// before ordinary keys. Metadata matters for Enter/numpad Enter, etc. +inline std::vector ReconcileKeyboardState( + const PressedKeys& current, const PressedKeys& desired) { + const auto same_key = [](const KeyboardKey& a, const KeyboardKey& b) { + return a.scan_code == b.scan_code && a.extended == b.extended; + }; + std::vector result; + for (const auto& [code, key] : current) { + const auto it = desired.find(code); + if (it == desired.end() || !same_key(key, it->second)) + result.push_back({code, false, key.scan_code, key.extended}); + } + // Release ordinary keys before modifiers as well. + std::stable_partition(result.begin(), result.end(), [](const auto& key) { + return !IsKeyboardModifier(key.key_code); + }); + const auto releases = result.size(); + for (const auto& [code, key] : desired) { + const auto it = current.find(code); + if (it == current.end() || !same_key(key, it->second)) + result.push_back({code, true, key.scan_code, key.extended}); + } + std::stable_partition(result.begin() + releases, result.end(), + [](const auto& key) { + return IsKeyboardModifier(key.key_code); + }); + return result; +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp b/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp index 26237a1b..2824b6b9 100644 --- a/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp +++ b/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp @@ -1,7 +1,5 @@ #include "keyboard_capturer.h" -#include - #include "rd_log.h" #include "windows_input_injector.h" #include "windows_input_marker.h" @@ -10,8 +8,8 @@ namespace crossdesk { namespace { -constexpr wchar_t kRawInputWindowClassName[] = - L"CrossDeskKeyboardRawInputWindow"; +constexpr UINT kHookRenewIntervalMs = 500; +constexpr ULONGLONG kHookStallTimeoutMs = 2000; bool PreferSideSpecificVkInjection(int key_code) { switch (key_code) { @@ -31,15 +29,32 @@ bool PreferSideSpecificVkInjection(int key_code) { } // namespace -PlatformKeyboardCapturer::PlatformKeyboardCapturer() {} +thread_local PlatformKeyboardCapturer* + PlatformKeyboardCapturer::active_capturer_ = nullptr; PlatformKeyboardCapturer::~PlatformKeyboardCapturer() { Unhook(); } +bool PlatformKeyboardCapturer::IsHookActive() const { + if (!hook_active_.load()) return false; + const auto last_pump_tick = last_hook_pump_tick_.load(); + return GetTickCount64() - last_pump_tick < kHookStallTimeoutMs; +} + int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) { if (capture_thread_.joinable()) { - return 0; + return IsHookActive() ? 0 : -1; } + // Hook() is called only while the stream has focus. Pin capture to that + // window so a focus change cannot steal input before the UI stops capture. + const HWND foreground_window = GetForegroundWindow(); + DWORD process_id = 0; + GetWindowThreadProcessId(foreground_window, &process_id); + if (!on_key_action || !foreground_window || + process_id != GetCurrentProcessId()) { + return -1; + } + capture_window_ = foreground_window; on_key_action_ = on_key_action; user_ptr_ = user_ptr; { @@ -49,7 +64,7 @@ int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) { capture_start_succeeded_ = false; } - capture_thread_ = std::thread(&PlatformKeyboardCapturer::RawInputThreadMain, this); + capture_thread_ = std::thread(&PlatformKeyboardCapturer::CaptureThreadMain, this); std::unique_lock lock(capture_state_mutex_); capture_start_condition_.wait( @@ -69,6 +84,7 @@ int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) { } int PlatformKeyboardCapturer::Unhook() { + hook_active_.store(false); DWORD capture_thread_id = 0; { std::lock_guard lock(capture_state_mutex_); @@ -76,7 +92,7 @@ int PlatformKeyboardCapturer::Unhook() { } if (capture_thread_id != 0 && !PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) { - LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}", + LOG_WARN("Failed to stop keyboard hook thread, thread_id={}, error={}", capture_thread_id, GetLastError()); } if (capture_thread_.joinable()) { @@ -85,15 +101,130 @@ int PlatformKeyboardCapturer::Unhook() { on_key_action_ = nullptr; user_ptr_ = nullptr; + capture_window_ = nullptr; + local_keys_down_.fill(false); + captured_keys_.fill({}); return 0; } -void PlatformKeyboardCapturer::RawInputThreadMain() { +void PlatformKeyboardCapturer::SnapshotLocalKeys() { + local_keys_down_.fill(false); + for (int code = VK_BACK; code < 0xFF; ++code) + local_keys_down_[code] = (GetAsyncKeyState(code) & 0x8000) != 0; + ForwardLocalModifiers(); +} + +void PlatformKeyboardCapturer::ForwardKey(int code, bool down, + uint32_t scan_code, bool extended) { + captured_keys_[code] = down ? KeyboardKey{code, scan_code, extended} + : KeyboardKey{}; + on_key_action_(code, down, scan_code, extended, user_ptr_); +} + +void PlatformKeyboardCapturer::ForwardLocalModifiers() { + // Windows already saw these presses before capture began. Forward held + // modifiers now, but let their eventual physical releases reach Windows + // too. Swallowing those releases would leave the controller's keys stuck. + for (const int code : {VK_LSHIFT, VK_RSHIFT, VK_LCONTROL, VK_RCONTROL, + VK_LMENU, VK_RMENU, VK_LWIN, VK_RWIN}) { + if (!local_keys_down_[code] || captured_keys_[code].key_code != 0) continue; + const UINT scan = MapVirtualKeyW(code, MAPVK_VK_TO_VSC_EX); + ForwardKey(code, true, scan & 0xFF, (scan & 0xFF00) != 0); + } +} + +void PlatformKeyboardCapturer::ReleaseCapturedKeys() { + for (const bool modifiers : {false, true}) { + for (const auto key : captured_keys_) { + if (key.key_code != 0 && IsKeyboardModifier(key.key_code) == modifiers) + ForwardKey(key.key_code, false, key.scan_code, key.extended); + } + } +} + +bool PlatformKeyboardCapturer::RenewKeyboardHook() { + bool raw_keyboard_suspended = false; + if (!raw_keyboard_guard_.Suspend(raw_keyboard_suspended)) { + hook_active_.store(false); + LOG_WARN("Failed to suspend raw keyboard registration, error={}", + GetLastError()); + return false; + } + // Windows can silently remove a timed-out hook. Renew periodically instead + // of relying on a bool set at startup. Install first to avoid an input gap. + const HHOOK replacement = SetWindowsHookExW( + WH_KEYBOARD_LL, &PlatformKeyboardCapturer::KeyboardHookProc, + GetModuleHandleW(nullptr), 0); + if (!replacement) { + hook_active_.store(false); + LOG_WARN("Failed to install keyboard hook, error={}", GetLastError()); + return false; + } + const HHOOK previous = keyboard_hook_; + keyboard_hook_ = replacement; + if (raw_keyboard_suspended && !previous) + LOG_INFO("Suspended raw keyboard input for shortcut capture"); + bool reset_keys = previous && raw_keyboard_suspended; + if (previous && !UnhookWindowsHookEx(previous)) { + const DWORD error = GetLastError(); + if (error != ERROR_INVALID_HOOK_HANDLE) { + // Do not keep capturing through two live hooks: a passed-through + // inherited release could otherwise be handled twice and swallowed. + // Renewal stops immediately, so only this one old hook needs cleanup. + retired_hook_ = previous; + hook_active_.store(false); + LOG_WARN("Failed to retire keyboard hook, error={}", error); + return false; + } + reset_keys = true; + LOG_WARN("Recovered keyboard hook, error={}", error); + } + if (reset_keys) { + // Events may have escaped while the hook was unavailable or a backend + // re-enabled raw input. Cancel uncertain presses and adopt local state. + ReleaseCapturedKeys(); + if (GetForegroundWindow() == capture_window_) + SnapshotLocalKeys(); + else + local_keys_down_.fill(false); + LOG_WARN("Reset captured keys after keyboard capture recovery"); + } + last_hook_pump_tick_.store(GetTickCount64()); + hook_active_.store(true); + return true; +} + +void PlatformKeyboardCapturer::RemoveKeyboardHook() { + if (keyboard_hook_) UnhookWindowsHookEx(keyboard_hook_); + keyboard_hook_ = nullptr; + if (retired_hook_) UnhookWindowsHookEx(retired_hook_); + retired_hook_ = nullptr; + if (!raw_keyboard_guard_.Restore()) + LOG_WARN("Failed to restore raw keyboard registration, error={}", + GetLastError()); +} + +void PlatformKeyboardCapturer::CaptureThreadMain() { const DWORD thread_id = GetCurrentThreadId(); MSG message{}; PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE); - const bool capture_started = CreateRawInputWindow(); + active_capturer_ = this; + // Raw Input observes shortcuts but cannot prevent Alt+Tab, Win+D, etc. + // from acting on the controller. Keep the hook off the UI/network thread: + // its callback only queues events for SessionDeviceManager to forward. + const bool installed = RenewKeyboardHook(); + const UINT_PTR timer = installed + ? SetTimer(nullptr, 0, kHookRenewIntervalMs, nullptr) + : 0; + const bool capture_started = installed && timer != 0; + if (installed && !timer) { + LOG_WARN("Failed to create keyboard hook renewal timer, error={}", + GetLastError()); + } + hook_active_.store(capture_started); + if (capture_started && GetForegroundWindow() == capture_window_) + SnapshotLocalKeys(); { std::lock_guard lock(capture_state_mutex_); capture_thread_id_ = thread_id; @@ -103,153 +234,78 @@ void PlatformKeyboardCapturer::RawInputThreadMain() { capture_start_condition_.notify_one(); if (!capture_started) { + RemoveKeyboardHook(); + active_capturer_ = nullptr; std::lock_guard lock(capture_state_mutex_); capture_thread_id_ = 0; return; } - LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id); + LOG_INFO("Keyboard hook capture started, thread_id={}", thread_id); while (true) { const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0); if (get_message_result <= 0) { if (get_message_result < 0) { - LOG_WARN("Keyboard raw input message loop failed, thread_id={}, " + LOG_WARN("Keyboard hook message loop failed, thread_id={}, " "error={}", thread_id, GetLastError()); } break; } + last_hook_pump_tick_.store(GetTickCount64()); + if (message.message == WM_TIMER && message.wParam == timer) { + if (!RenewKeyboardHook()) break; + continue; + } TranslateMessage(&message); DispatchMessageW(&message); } - DestroyRawInputWindow(); + hook_active_.store(false); + KillTimer(nullptr, timer); + RemoveKeyboardHook(); + active_capturer_ = nullptr; { std::lock_guard lock(capture_state_mutex_); capture_thread_id_ = 0; } - LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id); + LOG_INFO("Keyboard hook capture stopped, thread_id={}", thread_id); } -bool PlatformKeyboardCapturer::CreateRawInputWindow() { - const HINSTANCE instance = GetModuleHandleW(nullptr); - WNDCLASSEXW window_class{}; - window_class.cbSize = sizeof(window_class); - window_class.lpfnWndProc = &PlatformKeyboardCapturer::RawInputWindowProc; - window_class.hInstance = instance; - window_class.lpszClassName = kRawInputWindowClassName; - - if (RegisterClassExW(&window_class) == 0) { - const DWORD error = GetLastError(); - if (error != ERROR_CLASS_ALREADY_EXISTS) { - LOG_WARN("Failed to register keyboard raw input window class, error={}", - error); - return false; - } +LRESULT CALLBACK PlatformKeyboardCapturer::KeyboardHookProc( + int code, WPARAM message, LPARAM data) { + if (code == HC_ACTION && active_capturer_ && data != 0 && + active_capturer_->HandleKeyboardInput( + message, *reinterpret_cast(data), + GetForegroundWindow())) { + return 1; } + return CallNextHookEx(nullptr, code, message, data); +} - raw_input_window_ = CreateWindowExW( - 0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, - instance, this); - if (!raw_input_window_) { - LOG_WARN("Failed to create keyboard raw input window, error={}", - GetLastError()); +bool PlatformKeyboardCapturer::HandleKeyboardInput( + WPARAM message, const KBDLLHOOKSTRUCT& keyboard, HWND foreground_window) { + if (!hook_active_.load() || !on_key_action_ || !capture_window_ || + keyboard.vkCode < VK_BACK || keyboard.vkCode >= 0xFF || + keyboard.dwExtraInfo == kInjectedKeyboardInputMarker) { return false; } - - RAWINPUTDEVICE keyboard_device{}; - keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC; - keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD; - keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK; - keyboard_device.hwndTarget = raw_input_window_; - if (!RegisterRawInputDevices(&keyboard_device, 1, - sizeof(keyboard_device))) { - LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError()); - DestroyWindow(raw_input_window_); - raw_input_window_ = nullptr; - return false; - } - - raw_input_registered_ = true; - return true; -} - -void PlatformKeyboardCapturer::DestroyRawInputWindow() { - if (raw_input_registered_) { - RAWINPUTDEVICE keyboard_device{}; - keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC; - keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD; - keyboard_device.dwFlags = RIDEV_REMOVE; - keyboard_device.hwndTarget = nullptr; - if (!RegisterRawInputDevices(&keyboard_device, 1, - sizeof(keyboard_device))) { - LOG_WARN("Failed to unregister keyboard raw input, error={}", - GetLastError()); - } - raw_input_registered_ = false; - } - if (raw_input_window_) { - DestroyWindow(raw_input_window_); - raw_input_window_ = nullptr; - } -} - -LRESULT CALLBACK PlatformKeyboardCapturer::RawInputWindowProc( - HWND window, UINT message, WPARAM w_param, LPARAM l_param) { - PlatformKeyboardCapturer* capturer = reinterpret_cast( - GetWindowLongPtrW(window, GWLP_USERDATA)); - if (message == WM_NCCREATE) { - auto* create = reinterpret_cast(l_param); - capturer = static_cast(create->lpCreateParams); - SetWindowLongPtrW(window, GWLP_USERDATA, - reinterpret_cast(capturer)); - } else if (message == WM_INPUT && capturer) { - capturer->HandleRawInput(reinterpret_cast(l_param)); - } else if (message == WM_NCDESTROY) { - SetWindowLongPtrW(window, GWLP_USERDATA, 0); - } - return DefWindowProcW(window, message, w_param, l_param); -} - -void PlatformKeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) { - RAWINPUT input{}; - UINT input_size = sizeof(input); - const UINT bytes_read = - GetRawInputData(raw_input_handle, RID_INPUT, &input, &input_size, - sizeof(RAWINPUTHEADER)); - if (bytes_read == static_cast(-1) || - bytes_read < sizeof(RAWINPUTHEADER) || - input.header.dwType != RIM_TYPEKEYBOARD) { - return; - } - - const RAWKEYBOARD& keyboard = input.data.keyboard; - if (keyboard.VKey == 0xFF || - keyboard.ExtraInformation == - static_cast(kInjectedKeyboardInputMarker)) { - return; - } + // Do not reject LLKHF_INJECTED wholesale: an upstream remote session or + // accessibility tool can be the controller's keyboard source. bool is_down = false; - if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) { + if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) { is_down = true; - } else if (keyboard.Message != WM_KEYUP && - keyboard.Message != WM_SYSKEYUP) { - return; + } else if (message != WM_KEYUP && message != WM_SYSKEYUP) { + return false; } - const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0; - UINT mapped_scan_code = keyboard.MakeCode; - if ((keyboard.Flags & RI_KEY_E0) != 0) { + const bool extended = (keyboard.flags & LLKHF_EXTENDED) != 0; + UINT mapped_scan_code = keyboard.scanCode & 0xFF; + if (extended) { mapped_scan_code |= 0xE000; - } else if ((keyboard.Flags & RI_KEY_E1) != 0) { - mapped_scan_code |= 0xE100; } - if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) { - return; - } - - int key_code = static_cast(keyboard.VKey); + int key_code = static_cast(keyboard.vkCode); if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) { const UINT normalized = MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX); @@ -258,9 +314,19 @@ void PlatformKeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) { } } - if (on_key_action_) { - on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_); + if (foreground_window != capture_window_) { + // This event goes to Windows, even if focus changed before the UI thread + // has noticed. Remember ownership in case focus returns before Unhook(). + local_keys_down_[key_code] = is_down; + if (!is_down && captured_keys_[key_code].key_code != 0) + ForwardKey(key_code, false, keyboard.scanCode, extended); + return false; } + ForwardLocalModifiers(); + const bool release_local = !is_down && local_keys_down_[key_code]; + if (!is_down) local_keys_down_[key_code] = false; + ForwardKey(key_code, is_down, keyboard.scanCode, extended); + return !release_local; } // Apply remote keyboard commands to the local machine. diff --git a/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.h b/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.h index eca54cbf..efb35efd 100644 --- a/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.h +++ b/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.h @@ -9,39 +9,54 @@ #include +#include +#include #include #include #include #include "device_controller.h" +#include "keyboard_state.h" +#include "windows_raw_keyboard_guard.h" namespace crossdesk { class PlatformKeyboardCapturer final : public KeyboardCapturer { public: - PlatformKeyboardCapturer(); - virtual ~PlatformKeyboardCapturer(); + PlatformKeyboardCapturer() = default; + ~PlatformKeyboardCapturer() override; - public: - virtual int Hook(OnKeyAction on_key_action, void* user_ptr); - virtual int Unhook(); - virtual int SendKeyboardCommand(int key_code, bool is_down, - uint32_t scan_code = 0, - bool extended = false); + int Hook(OnKeyAction on_key_action, void* user_ptr) override; + int Unhook() override; + bool IsHookActive() const override; + int SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code = 0, + bool extended = false) override; private: - static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message, - WPARAM w_param, LPARAM l_param); + static LRESULT CALLBACK KeyboardHookProc(int code, WPARAM message, + LPARAM data); + static thread_local PlatformKeyboardCapturer* active_capturer_; - void RawInputThreadMain(); - bool CreateRawInputWindow(); - void DestroyRawInputWindow(); - void HandleRawInput(HRAWINPUT raw_input_handle); + void CaptureThreadMain(); + bool RenewKeyboardHook(); + void RemoveKeyboardHook(); + void SnapshotLocalKeys(); + void ForwardLocalModifiers(); + void ReleaseCapturedKeys(); + void ForwardKey(int code, bool down, uint32_t scan_code, bool extended); + bool HandleKeyboardInput(WPARAM message, const KBDLLHOOKSTRUCT& keyboard, + HWND foreground_window); OnKeyAction on_key_action_ = nullptr; void* user_ptr_ = nullptr; - HWND raw_input_window_ = nullptr; - bool raw_input_registered_ = false; + HWND capture_window_ = nullptr; + std::atomic hook_active_{false}; + std::atomic last_hook_pump_tick_{0}; + HHOOK keyboard_hook_ = nullptr; + WindowsRawKeyboardGuard raw_keyboard_guard_; + HHOOK retired_hook_ = nullptr; + std::array local_keys_down_{}; + std::array captured_keys_{}; std::thread capture_thread_; DWORD capture_thread_id_ = 0; bool capture_start_complete_ = false; diff --git a/apps/desktop/src/platform/windows/input/windows_raw_keyboard_guard.h b/apps/desktop/src/platform/windows/input/windows_raw_keyboard_guard.h new file mode 100644 index 00000000..2c6a4033 --- /dev/null +++ b/apps/desktop/src/platform/windows/input/windows_raw_keyboard_guard.h @@ -0,0 +1,87 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-09 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _WINDOWS_RAW_KEYBOARD_GUARD_H_ +#define _WINDOWS_RAW_KEYBOARD_GUARD_H_ + +#include + +#include +#include + +namespace crossdesk { + +// Winit registers raw keyboard input for the process. Windows bypasses this +// process's WH_KEYBOARD_LL hook while that registration is active. Temporarily +// suspend only the keyboard registration while forwarding remote input, then +// restore the backend's latest registration on exit. Raw mouse input is kept. +class WindowsRawKeyboardGuard { + public: + bool Suspend(bool& changed) { + changed = false; + std::optional current; + if (!FindKeyboard(current)) return false; + if (!current) return true; + const RAWINPUTDEVICE removal{1, 6, RIDEV_REMOVE, nullptr}; + if (!RegisterRawInputDevices(&removal, 1, sizeof(removal))) return false; + saved_ = current; + // GetRegisteredRawInputDevices can omit DEVNOTIFY from its result. + // Winit requests it for keyboard hotplug notifications, so retain it. + saved_->dwFlags |= RIDEV_DEVNOTIFY; + changed = true; + return true; + } + + bool Restore() { + if (!saved_) return true; + std::optional current; + if (!FindKeyboard(current)) return false; + // A backend may have changed its registration or destroyed its target + // since capture stopped. Never overwrite a newer registration. + if (current || (saved_->hwndTarget && !IsWindow(saved_->hwndTarget))) { + saved_.reset(); + return true; + } + if (!RegisterRawInputDevices(&*saved_, 1, sizeof(RAWINPUTDEVICE))) return false; + saved_.reset(); + return true; + } + + private: + static bool FindKeyboard(std::optional& result) { + result.reset(); + for (int attempt = 0; attempt < 3; ++attempt) { + UINT count = 0; + const UINT status = GetRegisteredRawInputDevices( + nullptr, &count, sizeof(RAWINPUTDEVICE)); + if (status == static_cast(-1) && + GetLastError() != ERROR_INSUFFICIENT_BUFFER) return false; + if (count == 0) return true; + std::vector devices(count); + const UINT read = GetRegisteredRawInputDevices( + devices.data(), &count, sizeof(RAWINPUTDEVICE)); + if (read == static_cast(-1)) { + if (GetLastError() == ERROR_INSUFFICIENT_BUFFER) continue; + return false; + } + for (UINT i = 0; i < read; ++i) { + if (devices[i].usUsagePage == 1 && devices[i].usUsage == 6) { + result = devices[i]; + break; + } + } + return true; + } + SetLastError(ERROR_RETRY); + return false; + } + + std::optional saved_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file