diff --git a/src/device_controller/keyboard/linux/keyboard_capturer.cpp b/src/device_controller/keyboard/linux/keyboard_capturer.cpp index 95df0c7..ba32663 100644 --- a/src/device_controller/keyboard/linux/keyboard_capturer.cpp +++ b/src/device_controller/keyboard/linux/keyboard_capturer.cpp @@ -8,6 +8,7 @@ #include #include "keyboard_converter.h" +#include "linux_evdev_keycode.h" #include "platform.h" #include "rd_log.h" #include "windows_key_metadata.h" @@ -17,6 +18,40 @@ namespace crossdesk { static OnKeyAction g_on_key_action = nullptr; static void* g_user_ptr = nullptr; +static KeyCode ResolveX11Keycode(Display* display, int key_code, + uint32_t scan_code, bool extended) { + if (!display) { + return 0; + } + + const auto key_it = vkCodeToX11KeySym.find(key_code); + if (key_it != vkCodeToX11KeySym.end()) { + const KeyCode x11_keycode = + XKeysymToKeycode(display, static_cast(key_it->second)); + if (x11_keycode != 0) { + return x11_keycode; + } + } + + // Some controllers can preserve the physical Windows scan code even when + // they cannot resolve a Windows virtual-key value. Xorg's evdev keycodes use + // the Linux input code plus the protocol-reserved offset of 8. + const int evdev_keycode = + ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended); + if (evdev_keycode < 0) { + return 0; + } + + int min_keycode = 0; + int max_keycode = 0; + XDisplayKeycodes(display, &min_keycode, &max_keycode); + const int x11_keycode = evdev_keycode + 8; + if (x11_keycode < min_keycode || x11_keycode > max_keycode) { + return 0; + } + return static_cast(x11_keycode); +} + static KeySym NormalizeKeySym(KeySym key_sym) { if (key_sym >= XK_a && key_sym <= XK_z) { return key_sym - XK_a + XK_A; @@ -62,7 +97,16 @@ KeyboardCapturer::KeyboardCapturer() display_ = XOpenDisplay(nullptr); if (!display_) { LOG_ERROR("Failed to open X display."); + return; } + + int event_base = 0; + int error_base = 0; + int major_version = 0; + int minor_version = 0; + x11_xtest_available_ = + XTestQueryExtension(display_, &event_base, &error_base, &major_version, + &minor_version) != 0; } KeyboardCapturer::~KeyboardCapturer() { @@ -70,8 +114,10 @@ KeyboardCapturer::~KeyboardCapturer() { CleanupWaylandPortal(); if (display_) { + std::lock_guard lock(x11_injection_mutex_); XCloseDisplay(display_); display_ = nullptr; + x11_xtest_available_ = false; } } @@ -156,8 +202,6 @@ int KeyboardCapturer::Unhook() { int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code, bool extended) { - (void)scan_code; - (void)extended; if (IsWaylandSession()) { if (!use_wayland_portal_ && !wayland_init_attempted_) { wayland_init_attempted_ = true; @@ -181,12 +225,33 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down, return -1; } - if (vkCodeToX11KeySym.find(key_code) != vkCodeToX11KeySym.end()) { - int x11_key_code = vkCodeToX11KeySym[key_code]; - KeyCode keycode = XKeysymToKeycode(display_, x11_key_code); - XTestFakeKeyEvent(display_, keycode, is_down, CurrentTime); - XFlush(display_); + std::lock_guard lock(x11_injection_mutex_); + if (!x11_xtest_available_) { + LOG_ERROR("XTest extension not available for keyboard injection"); + return -2; } + + const KeyCode x11_keycode = + ResolveX11Keycode(display_, key_code, scan_code, extended); + if (x11_keycode == 0) { + LOG_WARN( + "Cannot map remote keyboard event to X11 keycode, vk_code={}, " + "scan_code={}, extended={}", + key_code, scan_code, extended); + return -3; + } + + if (!XTestFakeKeyEvent(display_, x11_keycode, is_down, CurrentTime)) { + LOG_ERROR( + "XTest keyboard injection failed, vk_code={}, scan_code={}, " + "extended={}, x11_keycode={}, is_down={}", + key_code, scan_code, extended, static_cast(x11_keycode), is_down); + return -4; + } + + // Complete the request before reporting success to the keyboard-state + // reconciler. This also makes X11 protocol errors observable immediately. + XSync(display_, False); return 0; } } // namespace crossdesk diff --git a/src/device_controller/keyboard/linux/keyboard_capturer.h b/src/device_controller/keyboard/linux/keyboard_capturer.h index 80f44ca..f17ac31 100644 --- a/src/device_controller/keyboard/linux/keyboard_capturer.h +++ b/src/device_controller/keyboard/linux/keyboard_capturer.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -49,6 +50,8 @@ class KeyboardCapturer : public DeviceController { unsigned long root_; std::atomic running_; std::thread event_thread_; + std::mutex x11_injection_mutex_; + bool x11_xtest_available_ = false; bool use_wayland_portal_ = false; bool wayland_init_attempted_ = false; DBusConnection* dbus_connection_ = nullptr; diff --git a/src/device_controller/keyboard/linux/keyboard_capturer_wayland.cpp b/src/device_controller/keyboard/linux/keyboard_capturer_wayland.cpp index ee7db16..30129c6 100644 --- a/src/device_controller/keyboard/linux/keyboard_capturer_wayland.cpp +++ b/src/device_controller/keyboard/linux/keyboard_capturer_wayland.cpp @@ -611,6 +611,7 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down, // Prefer keycode injection to preserve physical-key semantics and avoid // implicit Shift interpretation for uppercase keysyms. if (display_) { + std::lock_guard lock(x11_injection_mutex_); const int keysym = key_it->second; const KeyCode x11_keycode = XKeysymToKeycode(display_, static_cast(keysym)); diff --git a/tests/linux_keyboard_x11_integration_test.cpp b/tests/linux_keyboard_x11_integration_test.cpp new file mode 100644 index 0000000..121a391 --- /dev/null +++ b/tests/linux_keyboard_x11_integration_test.cpp @@ -0,0 +1,52 @@ +#include + +#include +#include +#include + +#include "keyboard_capturer.h" + +int main() { + Display* receiver = XOpenDisplay(nullptr); + if (!receiver) { + std::fprintf(stderr, "open display failed\n"); + return 1; + } + + const int screen = DefaultScreen(receiver); + Window window = XCreateSimpleWindow(receiver, RootWindow(receiver, screen), + 0, 0, 100, 100, 0, 0, 0); + XSelectInput(receiver, window, KeyPressMask | KeyReleaseMask); + XMapWindow(receiver, window); + XSync(receiver, False); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + XSetInputFocus(receiver, window, RevertToParent, CurrentTime); + XSync(receiver, False); + + crossdesk::KeyboardCapturer keyboard; + // Exercise the scan-code fallback: the controller supplied no usable VK, + // but did preserve the set-1 scan code for the A key. + const int down = keyboard.SendKeyboardCommand(0, true, 0x1E, false); + const int up = keyboard.SendKeyboardCommand(0, false, 0x1E, false); + + bool saw_down = false; + bool saw_up = false; + const auto deadline = std::chrono::steady_clock::now() + + std::chrono::seconds(2); + while (std::chrono::steady_clock::now() < deadline && + (!saw_down || !saw_up)) { + while (XPending(receiver) > 0) { + XEvent event{}; + XNextEvent(receiver, &event); + saw_down = saw_down || event.type == KeyPress; + saw_up = saw_up || event.type == KeyRelease; + } + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + XDestroyWindow(receiver, window); + XCloseDisplay(receiver); + std::printf("down_ret=%d up_ret=%d saw_down=%d saw_up=%d\n", down, up, + saw_down, saw_up); + return down == 0 && up == 0 && saw_down && saw_up ? 0 : 2; +} diff --git a/xmake/targets.lua b/xmake/targets.lua index 250614d..a95f494 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -197,6 +197,14 @@ function setup_targets() "src/device_controller/keyboard/linux", {public = true}) end + if is_os("linux") then + target("linux_keyboard_x11_integration_test") + set_kind("binary") + set_default(false) + add_deps("device_controller") + add_files("tests/linux_keyboard_x11_integration_test.cpp") + end + target("thumbnail") set_kind("object") add_packages("libyuv", "openssl3")