[fix] restore Windows remote keyboard shortcuts and input recovery

This commit is contained in:
dijunkun
2026-10-09 03:43:14 +08:00
parent 3cfe7cbb80
commit 06e23007ed
10 changed files with 626 additions and 312 deletions
@@ -27,8 +27,10 @@ namespace crossdesk {
namespace { namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500; constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
constexpr size_t kMaxCapturedKeyboardInputs = 512;
constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1); constexpr auto kFrameDeadlineTolerance = std::chrono::milliseconds(1);
#ifdef _WIN32
constexpr auto kKeyboardCaptureRetryDelay = std::chrono::seconds(1);
#endif
} // namespace } // namespace
@@ -419,8 +421,12 @@ int SessionDeviceManager::StopMouseController() {
} }
int SessionDeviceManager::StartKeyboardCapturer() { int SessionDeviceManager::StartKeyboardCapturer() {
ClearCapturedKeyboardInput(); captured_keyboard_inputs_.Clear();
owner_.keyboard_capturer_uses_window_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
#ifdef _WIN32
next_keyboard_capture_retry_ =
std::chrono::steady_clock::now() + kKeyboardCaptureRetryDelay;
#endif
#ifdef __APPLE__ #ifdef __APPLE__
if (!owner_.EnsureMacAccessibilityPermission()) { if (!owner_.EnsureMacAccessibilityPermission()) {
@@ -449,8 +455,10 @@ int SessionDeviceManager::StartKeyboardCapturer() {
void *user_ptr) { void *user_ptr) {
if (user_ptr) { if (user_ptr) {
auto *devices = static_cast<SessionDeviceManager *>(user_ptr); auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code, // Native hooks only enqueue; forwarding and logging stay off the
extended); // hook thread so they cannot trigger LowLevelHooksTimeout.
devices->captured_keyboard_inputs_.Push(
{key_code, is_down, scan_code, extended});
} }
}, },
this); this);
@@ -468,7 +476,7 @@ int SessionDeviceManager::StartKeyboardCapturer() {
int SessionDeviceManager::StopKeyboardCapturer() { int SessionDeviceManager::StopKeyboardCapturer() {
owner_.keyboard_capturer_is_started_ = false; owner_.keyboard_capturer_is_started_ = false;
if (keyboard_capturer_) keyboard_capturer_->Unhook(); if (keyboard_capturer_) keyboard_capturer_->Unhook();
ClearCapturedKeyboardInput(); captured_keyboard_inputs_.Clear();
if (owner_.keyboard_capturer_uses_window_events_) { if (owner_.keyboard_capturer_uses_window_events_) {
owner_.keyboard_capturer_uses_window_events_ = false; owner_.keyboard_capturer_uses_window_events_ = false;
LOG_INFO("Stop keyboard capturer with Slint keyboard backend"); LOG_INFO("Stop keyboard capturer with Slint keyboard backend");
@@ -602,7 +610,18 @@ void SessionDeviceManager::UpdateInteractions() {
#endif #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) { if (!owner_.keyboard_capturer_is_started_ && StartKeyboardCapturer() == 0) {
owner_.keyboard_capturer_is_started_ = true; owner_.keyboard_capturer_is_started_ = true;
} }
@@ -613,6 +632,10 @@ void SessionDeviceManager::UpdateInteractions() {
owner_.keyboard_.ForceReleasePressedKeys(); owner_.keyboard_.ForceReleasePressedKeys();
owner_.keyboard_capturer_uses_window_events_ = true; owner_.keyboard_capturer_uses_window_events_ = true;
owner_.keyboard_capturer_is_started_ = 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"); LOG_WARN("Native keyboard capture stopped; using Slint keyboard events");
} }
DrainCapturedKeyboardInput(); DrainCapturedKeyboardInput();
@@ -621,43 +644,25 @@ void SessionDeviceManager::UpdateInteractions() {
} else if (owner_.keyboard_capturer_is_started_) { } else if (owner_.keyboard_capturer_is_started_) {
owner_.keyboard_.ForceReleasePressedKeys(); owner_.keyboard_.ForceReleasePressedKeys();
StopKeyboardCapturer(); StopKeyboardCapturer();
owner_.keyboard_capturer_is_started_ = false;
} }
owner_.keyboard_.CheckRemoteTimeouts(); owner_.keyboard_.CheckRemoteTimeouts();
} }
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
bool is_down,
uint32_t scan_code,
bool extended) {
std::lock_guard<std::mutex> 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() { void SessionDeviceManager::DrainCapturedKeyboardInput() {
std::deque<CapturedKeyboardInput> inputs; const auto inputs = captured_keyboard_inputs_.Drain();
{
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
inputs.swap(captured_keyboard_inputs_);
}
if (owner_.privacy_.Snapshot().input_blocked) return; 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, owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
input.scan_code, input.extended); input.scan_code, input.extended);
} }
} }
void SessionDeviceManager::ClearCapturedKeyboardInput() {
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
captured_keyboard_inputs_.clear();
}
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down, bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, uint32_t scan_code,
bool extended) { bool extended) {
@@ -7,12 +7,10 @@
#ifndef _SESSION_DEVICE_MANAGER_H_ #ifndef _SESSION_DEVICE_MANAGER_H_
#define _SESSION_DEVICE_MANAGER_H_ #define _SESSION_DEVICE_MANAGER_H_
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cstddef> #include <cstddef>
#include <cstdint> #include <cstdint>
#include <deque>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -21,6 +19,7 @@
#include "device_controller.h" #include "device_controller.h"
#include "display_info.h" #include "display_info.h"
#include "features/input/captured_keyboard_queue.h"
#include "speaker_capture_controller.h" #include "speaker_capture_controller.h"
struct SDL_AudioStream; struct SDL_AudioStream;
@@ -83,17 +82,7 @@ public:
std::vector<HostDisplay> host_display_list() const; std::vector<HostDisplay> host_display_list() const;
private: 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 DrainCapturedKeyboardInput();
void ClearCapturedKeyboardInput();
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now, bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
int fps); int fps);
bool ShouldCaptureCursor() const; bool ShouldCaptureCursor() const;
@@ -115,8 +104,10 @@ private:
mutable std::mutex display_info_mutex_; mutable std::mutex display_info_mutex_;
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
size_t registered_display_stream_count_ = 0; size_t registered_display_stream_count_ = 0;
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_; CapturedKeyboardQueue captured_keyboard_inputs_;
std::mutex captured_keyboard_inputs_mutex_; #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 last_frame_time_{};
std::chrono::steady_clock::time_point next_frame_deadline_{}; std::chrono::steady_clock::time_point next_frame_deadline_{};
std::mutex capture_metrics_mutex_; std::mutex capture_metrics_mutex_;
@@ -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 <array>
#include <cstddef>
#include <mutex>
#include <vector>
#include "keyboard_state.h"
namespace crossdesk {
class CapturedKeyboardQueue {
public:
struct Batch {
std::vector<KeyboardInput> 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<KeyboardInput, kCapacity> events_{};
size_t count_ = 0;
bool resync_ = false;
PressedKeys pressed_;
};
} // namespace crossdesk
#endif
@@ -8,7 +8,7 @@
#include <mutex> #include <mutex>
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include <string> #include <string>
#include <unordered_map> #include <utility>
#include <vector> #include <vector>
#include "minirtc.h" #include "minirtc.h"
@@ -26,6 +26,7 @@ namespace {
constexpr uint32_t kHeartbeatIntervalMs = 500; constexpr uint32_t kHeartbeatIntervalMs = 500;
constexpr uint32_t kRemoteReleaseTimeoutMs = 2500; constexpr uint32_t kRemoteReleaseTimeoutMs = 2500;
constexpr uint32_t kReleaseRetryIntervalMs = 250;
int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code, int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
bool extended) { bool extended) {
@@ -99,29 +100,38 @@ void KeyboardController::TrackPressedKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) { uint32_t scan_code, bool extended) {
std::lock_guard<std::mutex> lock(pressed_keys_mutex_); std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
if (is_down) { if (is_down) {
pressed_keys_[key_code] = PressedKey{key_code, scan_code, extended}; pressed_keys_[key_code] = KeyboardKey{key_code, scan_code, extended};
} else { } else {
pressed_keys_.erase(key_code); pressed_keys_.erase(key_code);
} }
} }
void KeyboardController::ForceReleasePressedKeys() { void KeyboardController::ForceReleasePressedKeys() {
std::vector<PressedKey> pressed_keys; PressedKeys pressed_keys;
{ {
std::lock_guard<std::mutex> lock(pressed_keys_mutex_); std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
pressed_keys.reserve(pressed_keys_.size()); pressed_keys.swap(pressed_keys_);
for (const auto& [_, key] : pressed_keys_) {
pressed_keys.push_back(key);
}
pressed_keys_.clear();
} }
for (const PressedKey& key : pressed_keys) { for (const auto& [_, key] : pressed_keys) {
SendKeyCommand(key.key_code, false, key.scan_code, key.extended); SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
} }
SendHeartbeat(true); SendHeartbeat(true);
} }
void KeyboardController::RecoverCapturedState(const PressedKeys& pressed) {
std::vector<KeyboardInput> 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) { void KeyboardController::SendHeartbeat(bool force) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks()); const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) { 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, " "Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}", "key_code={}, is_down={}, response={}",
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( LogSecureDesktopInputBlocked(
@@ -269,17 +281,18 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id, void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
const RemoteAction& action) { const RemoteAction& action) {
std::lock_guard input_lock(remote_input_mutex_);
const int key_code = static_cast<int>(action.k.key_value); const int key_code = static_cast<int>(action.k.key_value);
const bool is_down = action.k.flag == KeyFlag::key_down; const bool is_down = action.k.flag == KeyFlag::key_down;
const bool injected = const bool injected =
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended); InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
if (injected) pending_key_releases_.erase(key_code);
std::lock_guard<std::mutex> lock(remote_states_mutex_);
auto& state = remote_states_[remote_id]; auto& state = remote_states_[remote_id];
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks()); state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
if (is_down && injected) { if (is_down && injected) {
state.pressed_keys[key_code] = 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) { } else if (!is_down && injected) {
state.pressed_keys.erase(key_code); 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, void KeyboardController::ApplyRemoteState(const std::string& remote_id,
const RemoteAction& action) { const RemoteAction& action) {
std::vector<PressedKey> keys_to_release; std::lock_guard input_lock(remote_input_mutex_);
std::vector<PressedKey> keys_to_press; auto& state = remote_states_[remote_id];
{ if (action.ks.seq != 0 && state.last_seq != 0 &&
std::lock_guard<std::mutex> lock(remote_states_mutex_); static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
auto& state = remote_states_[remote_id]; return;
if (action.ks.seq != 0 && state.last_seq != 0 &&
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
return;
}
state.last_seq = action.ks.seq;
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
state.keyboard_state_seen = true;
std::unordered_map<int, PressedKey> 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<int>(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);
}
}
} }
for (const PressedKey& key : keys_to_release) { state.last_seq = action.ks.seq;
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) { state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
std::lock_guard<std::mutex> lock(remote_states_mutex_); state.keyboard_state_seen = true;
const auto state_it = remote_states_.find(remote_id);
if (state_it != remote_states_.end()) { PressedKeys desired_keys;
state_it->second.pressed_keys.erase(key.key_code); 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<int>(key.key_value);
desired_keys[key_code] = {key_code, key.scan_code, key.extended};
} }
for (const PressedKey& key : keys_to_press) { const auto changes = ReconcileKeyboardState(state.pressed_keys, desired_keys);
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) { bool can_press = true;
std::lock_guard<std::mutex> lock(remote_states_mutex_); for (const auto& key : changes) {
remote_states_[remote_id].pressed_keys[key.key_code] = key; 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, void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason) { const char* reason) {
std::vector<PressedKey> keys_to_release; std::lock_guard input_lock(remote_input_mutex_);
{ ReleaseRemotePressedKeysLocked(remote_id, reason);
std::lock_guard<std::mutex> lock(remote_states_mutex_); }
const auto state_it = remote_states_.find(remote_id);
if (state_it == remote_states_.end()) { void KeyboardController::ReleaseRemotePressedKeysLocked(
return; std::string remote_id, const char* reason) {
} const auto state_it = remote_states_.find(remote_id);
for (const auto& [_, key] : state_it->second.pressed_keys) { if (state_it == remote_states_.end()) return;
keys_to_release.push_back(key); auto keys_to_release = std::move(state_it->second.pressed_keys);
} remote_states_.erase(state_it);
remote_states_.erase(state_it);
}
if (!keys_to_release.empty()) { if (!keys_to_release.empty()) {
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}", LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
keys_to_release.size(), remote_id, reason ? reason : "unknown"); 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)) { if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
#ifdef _WIN32 pending_key_releases_[code] = key;
if (owner_.privacy_.Engaged()) { } else {
std::lock_guard lock(remote_states_mutex_); pending_key_releases_.erase(code);
pending_privacy_releases_[key.key_code] = key;
}
#endif
} }
} }
} }
void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) { void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) {
std::vector<std::string> remotes;
{ {
std::lock_guard lock(remote_states_mutex_); std::lock_guard input_lock(remote_input_mutex_);
for (const auto& [id, state] : remote_states_) remotes.push_back(id); while (!remote_states_.empty())
ReleaseRemotePressedKeysLocked(remote_states_.begin()->first, reason);
} }
for (const auto& id : remotes) ReleaseRemotePressedKeys(id, reason);
CheckRemoteTimeouts(); CheckRemoteTimeouts();
} }
void KeyboardController::CheckRemoteTimeouts() { void KeyboardController::CheckRemoteTimeouts() {
#ifdef _WIN32 // Do not stall the UI while a transport thread waits on desktop IPC.
bool pending = false; std::unique_lock input_lock(remote_input_mutex_, std::try_to_lock);
{ if (!input_lock.owns_lock()) return;
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<int, PressedKey> 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
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks()); const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
std::vector<std::string> timed_out_remotes; // Retain failed releases in ordinary mode as well as privacy mode. Retry
{ // only on the ordinary desktop, never through the security UI helper.
std::lock_guard<std::mutex> lock(remote_states_mutex_); if (!pending_key_releases_.empty() &&
for (const auto& [remote_id, state] : remote_states_) { (last_release_retry_tick_ == 0 ||
if (state.keyboard_state_seen && !state.pressed_keys.empty() && now - last_release_retry_tick_ >= kReleaseRetryIntervalMs)
state.last_seen_tick != 0 && #ifdef _WIN32
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) { && IsCurrentSessionUserDesktopActive()
timed_out_remotes.push_back(remote_id); #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;
}
} }
} }
@@ -8,7 +8,7 @@
#include <remote_action.h> #include <remote_action.h>
#include "device_controller.h" #include "keyboard_state.h"
namespace crossdesk { namespace crossdesk {
@@ -23,6 +23,7 @@ public:
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0, int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
bool extended = false); bool extended = false);
void ForceReleasePressedKeys(); void ForceReleasePressedKeys();
void RecoverCapturedState(const PressedKeys& pressed);
void SendHeartbeat(bool force); void SendHeartbeat(bool force);
void ApplyRemoteEvent(const std::string &remote_id, void ApplyRemoteEvent(const std::string &remote_id,
const RemoteAction &remote_action); const RemoteAction &remote_action);
@@ -34,14 +35,8 @@ public:
void CheckRemoteTimeouts(); void CheckRemoteTimeouts();
private: private:
struct PressedKey {
int key_code = 0;
uint32_t scan_code = 0;
bool extended = false;
};
struct RemoteState { struct RemoteState {
std::unordered_map<int, PressedKey> pressed_keys; PressedKeys pressed_keys;
uint32_t last_seq = 0; uint32_t last_seq = 0;
uint32_t last_seen_tick = 0; uint32_t last_seen_tick = 0;
bool keyboard_state_seen = false; bool keyboard_state_seen = false;
@@ -51,17 +46,20 @@ private:
bool extended); bool extended);
bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code, bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code,
bool extended); 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_; GuiRuntime &owner_;
std::unordered_map<int, PressedKey> pressed_keys_; PressedKeys pressed_keys_;
std::mutex pressed_keys_mutex_; std::mutex pressed_keys_mutex_;
uint32_t state_sequence_ = 0; uint32_t state_sequence_ = 0;
uint32_t last_heartbeat_tick_ = 0; uint32_t last_heartbeat_tick_ = 0;
std::unordered_map<std::string, RemoteState> remote_states_; std::unordered_map<std::string, RemoteState> remote_states_;
std::mutex remote_states_mutex_; // Guards all remote key state and serializes injection, cleanup and retries.
#ifdef _WIN32 std::mutex remote_input_mutex_;
std::unordered_map<int, PressedKey> pending_privacy_releases_; // Successful new input supersedes cleanup of an older press of that key.
#endif PressedKeys pending_key_releases_;
uint32_t last_release_retry_tick_ = 0;
}; };
} // namespace crossdesk } // namespace crossdesk
+13 -1
View File
@@ -282,9 +282,21 @@ export component StreamWindow inherits Window {
root.display-menu-open = false; root.display-menu-open = false;
root.shortcut-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); 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 <bool> control-tooltip-visible: false; private property <bool> control-tooltip-visible: false;
private property <ControlBarTooltip> hovered-control: { private property <ControlBarTooltip> hovered-control: {
if root.window-active && !root.control-dragging if root.window-active && !root.control-dragging
@@ -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 <algorithm>
#include <cstdint>
#include <map>
#include <vector>
namespace crossdesk {
struct KeyboardKey {
int key_code = 0;
uint32_t scan_code = 0;
bool extended = false;
};
using PressedKeys = std::map<int, KeyboardKey>;
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<KeyboardInput> 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<KeyboardInput> 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
@@ -1,7 +1,5 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <hidusage.h>
#include "rd_log.h" #include "rd_log.h"
#include "windows_input_injector.h" #include "windows_input_injector.h"
#include "windows_input_marker.h" #include "windows_input_marker.h"
@@ -10,8 +8,8 @@
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr wchar_t kRawInputWindowClassName[] = constexpr UINT kHookRenewIntervalMs = 500;
L"CrossDeskKeyboardRawInputWindow"; constexpr ULONGLONG kHookStallTimeoutMs = 2000;
bool PreferSideSpecificVkInjection(int key_code) { bool PreferSideSpecificVkInjection(int key_code) {
switch (key_code) { switch (key_code) {
@@ -31,15 +29,32 @@ bool PreferSideSpecificVkInjection(int key_code) {
} // namespace } // namespace
PlatformKeyboardCapturer::PlatformKeyboardCapturer() {} thread_local PlatformKeyboardCapturer*
PlatformKeyboardCapturer::active_capturer_ = nullptr;
PlatformKeyboardCapturer::~PlatformKeyboardCapturer() { Unhook(); } 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) { int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (capture_thread_.joinable()) { 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; on_key_action_ = on_key_action;
user_ptr_ = user_ptr; user_ptr_ = user_ptr;
{ {
@@ -49,7 +64,7 @@ int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
capture_start_succeeded_ = false; capture_start_succeeded_ = false;
} }
capture_thread_ = std::thread(&PlatformKeyboardCapturer::RawInputThreadMain, this); capture_thread_ = std::thread(&PlatformKeyboardCapturer::CaptureThreadMain, this);
std::unique_lock<std::mutex> lock(capture_state_mutex_); std::unique_lock<std::mutex> lock(capture_state_mutex_);
capture_start_condition_.wait( capture_start_condition_.wait(
@@ -69,6 +84,7 @@ int PlatformKeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
} }
int PlatformKeyboardCapturer::Unhook() { int PlatformKeyboardCapturer::Unhook() {
hook_active_.store(false);
DWORD capture_thread_id = 0; DWORD capture_thread_id = 0;
{ {
std::lock_guard<std::mutex> lock(capture_state_mutex_); std::lock_guard<std::mutex> lock(capture_state_mutex_);
@@ -76,7 +92,7 @@ int PlatformKeyboardCapturer::Unhook() {
} }
if (capture_thread_id != 0 && if (capture_thread_id != 0 &&
!PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 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()); capture_thread_id, GetLastError());
} }
if (capture_thread_.joinable()) { if (capture_thread_.joinable()) {
@@ -85,15 +101,130 @@ int PlatformKeyboardCapturer::Unhook() {
on_key_action_ = nullptr; on_key_action_ = nullptr;
user_ptr_ = nullptr; user_ptr_ = nullptr;
capture_window_ = nullptr;
local_keys_down_.fill(false);
captured_keys_.fill({});
return 0; 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(); const DWORD thread_id = GetCurrentThreadId();
MSG message{}; MSG message{};
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE); 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<std::mutex> lock(capture_state_mutex_); std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = thread_id; capture_thread_id_ = thread_id;
@@ -103,153 +234,78 @@ void PlatformKeyboardCapturer::RawInputThreadMain() {
capture_start_condition_.notify_one(); capture_start_condition_.notify_one();
if (!capture_started) { if (!capture_started) {
RemoveKeyboardHook();
active_capturer_ = nullptr;
std::lock_guard<std::mutex> lock(capture_state_mutex_); std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0; capture_thread_id_ = 0;
return; return;
} }
LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id); LOG_INFO("Keyboard hook capture started, thread_id={}", thread_id);
while (true) { while (true) {
const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0); const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
if (get_message_result <= 0) { if (get_message_result <= 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={}", "error={}",
thread_id, GetLastError()); thread_id, GetLastError());
} }
break; break;
} }
last_hook_pump_tick_.store(GetTickCount64());
if (message.message == WM_TIMER && message.wParam == timer) {
if (!RenewKeyboardHook()) break;
continue;
}
TranslateMessage(&message); TranslateMessage(&message);
DispatchMessageW(&message); DispatchMessageW(&message);
} }
DestroyRawInputWindow(); hook_active_.store(false);
KillTimer(nullptr, timer);
RemoveKeyboardHook();
active_capturer_ = nullptr;
{ {
std::lock_guard<std::mutex> lock(capture_state_mutex_); std::lock_guard<std::mutex> lock(capture_state_mutex_);
capture_thread_id_ = 0; 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() { LRESULT CALLBACK PlatformKeyboardCapturer::KeyboardHookProc(
const HINSTANCE instance = GetModuleHandleW(nullptr); int code, WPARAM message, LPARAM data) {
WNDCLASSEXW window_class{}; if (code == HC_ACTION && active_capturer_ && data != 0 &&
window_class.cbSize = sizeof(window_class); active_capturer_->HandleKeyboardInput(
window_class.lpfnWndProc = &PlatformKeyboardCapturer::RawInputWindowProc; message, *reinterpret_cast<const KBDLLHOOKSTRUCT*>(data),
window_class.hInstance = instance; GetForegroundWindow())) {
window_class.lpszClassName = kRawInputWindowClassName; return 1;
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;
}
} }
return CallNextHookEx(nullptr, code, message, data);
}
raw_input_window_ = CreateWindowExW( bool PlatformKeyboardCapturer::HandleKeyboardInput(
0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, WPARAM message, const KBDLLHOOKSTRUCT& keyboard, HWND foreground_window) {
instance, this); if (!hook_active_.load() || !on_key_action_ || !capture_window_ ||
if (!raw_input_window_) { keyboard.vkCode < VK_BACK || keyboard.vkCode >= 0xFF ||
LOG_WARN("Failed to create keyboard raw input window, error={}", keyboard.dwExtraInfo == kInjectedKeyboardInputMarker) {
GetLastError());
return false; return false;
} }
// Do not reject LLKHF_INJECTED wholesale: an upstream remote session or
RAWINPUTDEVICE keyboard_device{}; // accessibility tool can be the controller's keyboard source.
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<PlatformKeyboardCapturer*>(
GetWindowLongPtrW(window, GWLP_USERDATA));
if (message == WM_NCCREATE) {
auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
capturer = static_cast<PlatformKeyboardCapturer*>(create->lpCreateParams);
SetWindowLongPtrW(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(capturer));
} else if (message == WM_INPUT && capturer) {
capturer->HandleRawInput(reinterpret_cast<HRAWINPUT>(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<UINT>(-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<ULONG>(kInjectedKeyboardInputMarker)) {
return;
}
bool is_down = false; bool is_down = false;
if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) { if (message == WM_KEYDOWN || message == WM_SYSKEYDOWN) {
is_down = true; is_down = true;
} else if (keyboard.Message != WM_KEYUP && } else if (message != WM_KEYUP && message != WM_SYSKEYUP) {
keyboard.Message != WM_SYSKEYUP) { return false;
return;
} }
const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0; const bool extended = (keyboard.flags & LLKHF_EXTENDED) != 0;
UINT mapped_scan_code = keyboard.MakeCode; UINT mapped_scan_code = keyboard.scanCode & 0xFF;
if ((keyboard.Flags & RI_KEY_E0) != 0) { if (extended) {
mapped_scan_code |= 0xE000; 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) { int key_code = static_cast<int>(keyboard.vkCode);
return;
}
int key_code = static_cast<int>(keyboard.VKey);
if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) { if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
const UINT normalized = const UINT normalized =
MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX); MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
@@ -258,9 +314,19 @@ void PlatformKeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) {
} }
} }
if (on_key_action_) { if (foreground_window != capture_window_) {
on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_); // 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. // Apply remote keyboard commands to the local machine.
@@ -9,39 +9,54 @@
#include <Windows.h> #include <Windows.h>
#include <array>
#include <atomic>
#include <condition_variable> #include <condition_variable>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
#include "device_controller.h" #include "device_controller.h"
#include "keyboard_state.h"
#include "windows_raw_keyboard_guard.h"
namespace crossdesk { namespace crossdesk {
class PlatformKeyboardCapturer final : public KeyboardCapturer { class PlatformKeyboardCapturer final : public KeyboardCapturer {
public: public:
PlatformKeyboardCapturer(); PlatformKeyboardCapturer() = default;
virtual ~PlatformKeyboardCapturer(); ~PlatformKeyboardCapturer() override;
public: int Hook(OnKeyAction on_key_action, void* user_ptr) override;
virtual int Hook(OnKeyAction on_key_action, void* user_ptr); int Unhook() override;
virtual int Unhook(); bool IsHookActive() const override;
virtual int SendKeyboardCommand(int key_code, bool is_down, int SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code = 0,
uint32_t scan_code = 0, bool extended = false) override;
bool extended = false);
private: private:
static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message, static LRESULT CALLBACK KeyboardHookProc(int code, WPARAM message,
WPARAM w_param, LPARAM l_param); LPARAM data);
static thread_local PlatformKeyboardCapturer* active_capturer_;
void RawInputThreadMain(); void CaptureThreadMain();
bool CreateRawInputWindow(); bool RenewKeyboardHook();
void DestroyRawInputWindow(); void RemoveKeyboardHook();
void HandleRawInput(HRAWINPUT raw_input_handle); 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; OnKeyAction on_key_action_ = nullptr;
void* user_ptr_ = nullptr; void* user_ptr_ = nullptr;
HWND raw_input_window_ = nullptr; HWND capture_window_ = nullptr;
bool raw_input_registered_ = false; std::atomic<bool> hook_active_{false};
std::atomic<ULONGLONG> last_hook_pump_tick_{0};
HHOOK keyboard_hook_ = nullptr;
WindowsRawKeyboardGuard raw_keyboard_guard_;
HHOOK retired_hook_ = nullptr;
std::array<bool, 256> local_keys_down_{};
std::array<KeyboardKey, 256> captured_keys_{};
std::thread capture_thread_; std::thread capture_thread_;
DWORD capture_thread_id_ = 0; DWORD capture_thread_id_ = 0;
bool capture_start_complete_ = false; bool capture_start_complete_ = false;
@@ -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 <windows.h>
#include <optional>
#include <vector>
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<RAWINPUTDEVICE> 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<RAWINPUTDEVICE> 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<RAWINPUTDEVICE>& 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<UINT>(-1) &&
GetLastError() != ERROR_INSUFFICIENT_BUFFER) return false;
if (count == 0) return true;
std::vector<RAWINPUTDEVICE> devices(count);
const UINT read = GetRegisteredRawInputDevices(
devices.data(), &count, sizeof(RAWINPUTDEVICE));
if (read == static_cast<UINT>(-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<RAWINPUTDEVICE> saved_;
};
} // namespace crossdesk
#endif