mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[fix] restore Windows remote keyboard shortcuts and input recovery
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user