mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-04 16:50:31 +08:00
[fix] capture Windows keyboard input with Raw Input, refs #94
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
#ifndef CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
#define CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// SendInput copies dwExtraInfo into KBDLLHOOKSTRUCT. Tag CrossDesk-generated
|
||||
// keyboard input so the controller hook can ignore only its own injections
|
||||
// while still accepting input from accessibility tools or remote sessions.
|
||||
inline constexpr std::uintptr_t kInjectedKeyboardInputMarker = 0x4353444B;
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
@@ -1,36 +1,17 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <hidusage.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
constexpr wchar_t kRawInputWindowClassName[] =
|
||||
L"CrossDeskKeyboardRawInputWindow";
|
||||
|
||||
static int NormalizeModifierVkCode(const KBDLLHOOKSTRUCT* kb_data) {
|
||||
if (kb_data == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (kb_data->vkCode != VK_SHIFT && kb_data->vkCode != VK_CONTROL &&
|
||||
kb_data->vkCode != VK_MENU) {
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
UINT scan_code = static_cast<UINT>(kb_data->scanCode & 0xFF);
|
||||
if ((kb_data->flags & LLKHF_EXTENDED) != 0) {
|
||||
scan_code |= 0xE000;
|
||||
}
|
||||
|
||||
const UINT normalized_vk = MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized_vk != 0) {
|
||||
return static_cast<int>(normalized_vk);
|
||||
}
|
||||
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
@@ -46,58 +27,247 @@ static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
if ((kbData->flags & LLKHF_INJECTED) != 0) {
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
g_on_key_action(key_code, true, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
|
||||
g_on_key_action(key_code, false, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() {}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {}
|
||||
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
if (capture_thread_.joinable()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
keyboard_hook_ = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
|
||||
if (!keyboard_hook_) {
|
||||
LOG_ERROR("Failed to install keyboard hook");
|
||||
on_key_action_ = on_key_action;
|
||||
user_ptr_ = user_ptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
capture_start_complete_ = false;
|
||||
capture_start_succeeded_ = false;
|
||||
}
|
||||
|
||||
capture_thread_ = std::thread(&KeyboardCapturer::RawInputThreadMain, this);
|
||||
|
||||
std::unique_lock<std::mutex> lock(capture_state_mutex_);
|
||||
capture_start_condition_.wait(
|
||||
lock, [this] { return capture_start_complete_; });
|
||||
const bool capture_started = capture_start_succeeded_;
|
||||
lock.unlock();
|
||||
|
||||
if (!capture_started) {
|
||||
if (capture_thread_.joinable()) {
|
||||
capture_thread_.join();
|
||||
}
|
||||
on_key_action_ = nullptr;
|
||||
user_ptr_ = nullptr;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
if (keyboard_hook_) {
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
UnhookWindowsHookEx(keyboard_hook_);
|
||||
keyboard_hook_ = nullptr;
|
||||
DWORD capture_thread_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id = capture_thread_id_;
|
||||
}
|
||||
if (capture_thread_id != 0 &&
|
||||
!PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) {
|
||||
LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}",
|
||||
capture_thread_id, GetLastError());
|
||||
}
|
||||
if (capture_thread_.joinable()) {
|
||||
capture_thread_.join();
|
||||
}
|
||||
|
||||
on_key_action_ = nullptr;
|
||||
user_ptr_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// apply remote keyboard commands to the local machine
|
||||
void KeyboardCapturer::RawInputThreadMain() {
|
||||
const DWORD thread_id = GetCurrentThreadId();
|
||||
|
||||
MSG message{};
|
||||
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
|
||||
const bool capture_started = CreateRawInputWindow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = thread_id;
|
||||
capture_start_succeeded_ = capture_started;
|
||||
capture_start_complete_ = true;
|
||||
}
|
||||
capture_start_condition_.notify_one();
|
||||
|
||||
if (!capture_started) {
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id);
|
||||
while (true) {
|
||||
const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
|
||||
if (get_message_result <= 0) {
|
||||
if (get_message_result < 0) {
|
||||
LOG_WARN("Keyboard raw input message loop failed, thread_id={}, "
|
||||
"error={}",
|
||||
thread_id, GetLastError());
|
||||
}
|
||||
break;
|
||||
}
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
|
||||
DestroyRawInputWindow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
}
|
||||
LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id);
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::CreateRawInputWindow() {
|
||||
const HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
WNDCLASSEXW window_class{};
|
||||
window_class.cbSize = sizeof(window_class);
|
||||
window_class.lpfnWndProc = &KeyboardCapturer::RawInputWindowProc;
|
||||
window_class.hInstance = instance;
|
||||
window_class.lpszClassName = kRawInputWindowClassName;
|
||||
|
||||
if (RegisterClassExW(&window_class) == 0) {
|
||||
const DWORD error = GetLastError();
|
||||
if (error != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
LOG_WARN("Failed to register keyboard raw input window class, error={}",
|
||||
error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
raw_input_window_ = CreateWindowExW(
|
||||
0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
|
||||
instance, this);
|
||||
if (!raw_input_window_) {
|
||||
LOG_WARN("Failed to create keyboard raw input window, error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
RAWINPUTDEVICE keyboard_device{};
|
||||
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
||||
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
|
||||
keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
||||
keyboard_device.hwndTarget = raw_input_window_;
|
||||
if (!RegisterRawInputDevices(&keyboard_device, 1,
|
||||
sizeof(keyboard_device))) {
|
||||
LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError());
|
||||
DestroyWindow(raw_input_window_);
|
||||
raw_input_window_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
raw_input_registered_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void KeyboardCapturer::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 KeyboardCapturer::RawInputWindowProc(
|
||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
KeyboardCapturer* capturer = reinterpret_cast<KeyboardCapturer*>(
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||
if (message == WM_NCCREATE) {
|
||||
auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
|
||||
capturer = static_cast<KeyboardCapturer*>(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 KeyboardCapturer::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;
|
||||
if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) {
|
||||
is_down = true;
|
||||
} else if (keyboard.Message != WM_KEYUP &&
|
||||
keyboard.Message != WM_SYSKEYUP) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0;
|
||||
UINT mapped_scan_code = keyboard.MakeCode;
|
||||
if ((keyboard.Flags & RI_KEY_E0) != 0) {
|
||||
mapped_scan_code |= 0xE000;
|
||||
} else if ((keyboard.Flags & RI_KEY_E1) != 0) {
|
||||
mapped_scan_code |= 0xE100;
|
||||
}
|
||||
if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) {
|
||||
return;
|
||||
}
|
||||
|
||||
int key_code = static_cast<int>(keyboard.VKey);
|
||||
if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
|
||||
const UINT normalized =
|
||||
MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized != 0) {
|
||||
key_code = static_cast<int>(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
if (on_key_action_) {
|
||||
on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remote keyboard commands to the local machine.
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo =
|
||||
static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
@@ -113,7 +283,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else {
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
input.ki.wVk = static_cast<WORD>(key_code);
|
||||
|
||||
if (prefer_vk && resolved_scan_code != 0) {
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
@@ -134,7 +304,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
const UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
|
||||
is_down, GetLastError());
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -26,8 +30,25 @@ class KeyboardCapturer : public DeviceController {
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
HHOOK keyboard_hook_ = nullptr;
|
||||
static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message,
|
||||
WPARAM w_param, LPARAM l_param);
|
||||
|
||||
void RawInputThreadMain();
|
||||
bool CreateRawInputWindow();
|
||||
void DestroyRawInputWindow();
|
||||
void HandleRawInput(HRAWINPUT raw_input_handle);
|
||||
|
||||
OnKeyAction on_key_action_ = nullptr;
|
||||
void* user_ptr_ = nullptr;
|
||||
HWND raw_input_window_ = nullptr;
|
||||
bool raw_input_registered_ = false;
|
||||
std::thread capture_thread_;
|
||||
DWORD capture_thread_id_ = 0;
|
||||
bool capture_start_complete_ = false;
|
||||
bool capture_start_succeeded_ = false;
|
||||
std::mutex capture_state_mutex_;
|
||||
std::condition_variable capture_start_condition_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -3042,9 +3042,8 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
|
||||
controlled_remote_id_ = props->remote_id_;
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
|
||||
// Native hooks see the same physical key before Slint does. When a native
|
||||
// hook is active, forwarding the FocusScope callback as well would duplicate
|
||||
// the event on platforms whose hook does not consume the local key.
|
||||
// Native capture forwards the event independently. Slint keyboard events
|
||||
// are used only by platforms whose native capture backend is unavailable.
|
||||
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -256,18 +257,18 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
void *user_ptr) {
|
||||
if (user_ptr) {
|
||||
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
|
||||
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
|
||||
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
},
|
||||
&owner_);
|
||||
this);
|
||||
if (hook_ret != 0) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"Start keyboard capturer failed, falling back to Slint keyboard events");
|
||||
} else {
|
||||
LOG_INFO("Start keyboard capturer with native hook");
|
||||
LOG_INFO("Start keyboard capturer with native input");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -281,6 +282,7 @@ int SessionDeviceManager::StopKeyboardCapturer() {
|
||||
|
||||
if (keyboard_capturer_) {
|
||||
keyboard_capturer_->Unhook();
|
||||
ClearCapturedKeyboardInput();
|
||||
LOG_INFO("Stop keyboard capturer");
|
||||
}
|
||||
return 0;
|
||||
@@ -410,6 +412,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.keyboard_capturer_is_started_ = true;
|
||||
}
|
||||
if (owner_.keyboard_capturer_is_started_) {
|
||||
DrainCapturedKeyboardInput();
|
||||
owner_.keyboard_.SendHeartbeat(false);
|
||||
}
|
||||
} else if (owner_.keyboard_capturer_is_started_) {
|
||||
@@ -421,6 +424,36 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
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() {
|
||||
std::deque<CapturedKeyboardInput> inputs;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
inputs.swap(captured_keyboard_inputs_);
|
||||
}
|
||||
for (const CapturedKeyboardInput &input : inputs) {
|
||||
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||
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,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -53,6 +55,18 @@ public:
|
||||
const std::vector<DisplayInfo> &display_info_list() const;
|
||||
|
||||
private:
|
||||
struct CapturedKeyboardInput {
|
||||
int key_code = 0;
|
||||
bool is_down = false;
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
};
|
||||
|
||||
void QueueCapturedKeyboardInput(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended);
|
||||
void DrainCapturedKeyboardInput();
|
||||
void ClearCapturedKeyboardInput();
|
||||
|
||||
GuiRuntime &owner_;
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||
@@ -63,6 +77,8 @@ private:
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "features/input/keyboard_controller.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -45,8 +45,8 @@ int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
||||
#endif
|
||||
}
|
||||
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
bool *extended_out) {
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (!scan_code_out || !extended_out) {
|
||||
return;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||
return;
|
||||
@@ -77,8 +77,8 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
stage ? stage : "");
|
||||
}
|
||||
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
const RemoteAction &action) {
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||
const RemoteAction& action) {
|
||||
return response.is_object() &&
|
||||
response.value("error", std::string()) == "send_input_failed" &&
|
||||
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
||||
@@ -87,9 +87,9 @@ bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
|
||||
KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {}
|
||||
|
||||
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
@@ -106,13 +106,13 @@ void KeyboardController::ForceReleasePressedKeys() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
pressed_keys.reserve(pressed_keys_.size());
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
for (const auto& [_, key] : pressed_keys_) {
|
||||
pressed_keys.push_back(key);
|
||||
}
|
||||
pressed_keys_.clear();
|
||||
}
|
||||
|
||||
for (const PressedKey &key : pressed_keys) {
|
||||
for (const PressedKey& key : pressed_keys) {
|
||||
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
SendHeartbeat(true);
|
||||
@@ -130,7 +130,7 @@ void KeyboardController::SendHeartbeat(bool force) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
size_t index = 0;
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
for (const auto& [_, key] : pressed_keys_) {
|
||||
if (index >= kMaxKeyboardStateKeys) {
|
||||
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
||||
pressed_keys_.size());
|
||||
@@ -223,9 +223,10 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, action)) {
|
||||
LOG_INFO("Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -245,15 +246,15 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
extended);
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
const int key_code = static_cast<int>(action.k.key_value);
|
||||
const bool is_down = action.k.flag == KeyFlag::key_down;
|
||||
const bool injected =
|
||||
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
||||
|
||||
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());
|
||||
if (is_down && injected) {
|
||||
state.pressed_keys[key_code] =
|
||||
@@ -263,13 +264,13 @@ void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
std::vector<PressedKey> keys_to_press;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
auto& state = remote_states_[remote_id];
|
||||
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
||||
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
||||
return;
|
||||
@@ -283,24 +284,24 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
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 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) {
|
||||
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) {
|
||||
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) {
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
const auto state_it = remote_states_.find(remote_id);
|
||||
@@ -309,7 +310,7 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const PressedKey &key : keys_to_press) {
|
||||
for (const PressedKey& key : keys_to_press) {
|
||||
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
remote_states_[remote_id].pressed_keys[key.key_code] = key;
|
||||
@@ -317,8 +318,8 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason) {
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
const char* reason) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
@@ -326,7 +327,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
if (state_it == remote_states_.end()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[_, key] : state_it->second.pressed_keys) {
|
||||
for (const auto& [_, key] : state_it->second.pressed_keys) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
remote_states_.erase(state_it);
|
||||
@@ -336,7 +337,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const PressedKey &key : keys_to_release) {
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
}
|
||||
@@ -346,7 +347,7 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
for (const auto &[remote_id, state] : remote_states_) {
|
||||
for (const auto& [remote_id, state] : remote_states_) {
|
||||
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
||||
state.last_seen_tick != 0 &&
|
||||
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
|
||||
@@ -354,9 +355,9 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const std::string &remote_id : timed_out_remotes) {
|
||||
for (const std::string& remote_id : timed_out_remotes) {
|
||||
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -828,6 +829,8 @@ InputInjectionResult InjectKeyboardInput(
|
||||
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(
|
||||
crossdesk::kInjectedKeyboardInputMarker);
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
|
||||
+1
-1
@@ -315,7 +315,7 @@ function setup_targets()
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("src/service/windows/session_helper_main.cpp")
|
||||
add_files(crossdesk_windows_resource)
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_includedirs("src/common", "src/service/windows", {public = true})
|
||||
end
|
||||
|
||||
target("crossdesk")
|
||||
|
||||
Reference in New Issue
Block a user