#include "features/input/keyboard_controller.h" #include #include #include #include #include #include #include #include #include "minirtc.h" #include "rd_log.h" #include "runtime/gui_runtime.h" #include "windows_key_metadata.h" #if _WIN32 #include "desktop_transition_policy.h" #include "interactive_desktop.h" #include "service_host.h" #include "unattended_config.h" #endif namespace crossdesk { namespace { constexpr uint32_t kHeartbeatIntervalMs = 500; constexpr uint32_t kRemoteReleaseTimeoutMs = 2500; constexpr uint32_t kReleaseRetryIntervalMs = 250; int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code, bool extended) { #if _WIN32 if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) { return key_code; } UINT scan_code_with_prefix = static_cast(scan_code & 0xFF); if (extended) { scan_code_with_prefix |= 0xE000; } const UINT normalized_vk = MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX); return normalized_vk != 0 ? static_cast(normalized_vk) : key_code; #else (void)scan_code; (void)extended; return key_code; #endif } void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out, bool* extended_out) { if (!scan_code_out || !extended_out) { return; } if (LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out)) { return; } #if _WIN32 const UINT scan_code = MapVirtualKeyW(static_cast(key_code), MAPVK_VK_TO_VSC_EX); if (scan_code != 0) { *scan_code_out = static_cast(scan_code & 0xFF); *extended_out = (scan_code & 0xFF00) != 0; return; } #endif } #if _WIN32 constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) { const uint32_t now = static_cast(SDL_GetTicks()); if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) { return; } *last_tick = now; LOG_WARN( "local secure-desktop input blocked, stage={}, normal SendInput path " "cannot drive the Windows password UI", stage ? stage : ""); } bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response, bool is_down) { return response.is_object() && response.value("error", std::string()) == "send_input_failed" && response.value("code", 0u) == ERROR_ACCESS_DENIED && !is_down; } #endif } // namespace KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {} void KeyboardController::TrackPressedKey(int key_code, bool is_down, uint32_t scan_code, bool extended) { std::lock_guard lock(pressed_keys_mutex_); if (is_down) { pressed_keys_[key_code] = KeyboardKey{key_code, scan_code, extended}; } else { pressed_keys_.erase(key_code); } } void KeyboardController::ForceReleasePressedKeys() { PressedKeys pressed_keys; { std::lock_guard lock(pressed_keys_mutex_); pressed_keys.swap(pressed_keys_); } for (const auto& [_, key] : pressed_keys) { SendKeyCommand(key.key_code, false, key.scan_code, key.extended); } SendHeartbeat(true); } void KeyboardController::RecoverCapturedState(const PressedKeys& pressed) { std::vector changes; { std::lock_guard lock(pressed_keys_mutex_); changes = ReconcileKeyboardState(pressed_keys_, pressed); } for (const auto& event : changes) { SendKeyCommand(event.key_code, event.is_down, event.scan_code, event.extended); } SendHeartbeat(true); } void KeyboardController::SendHeartbeat(bool force) { const uint32_t now = static_cast(SDL_GetTicks()); if (!force && now - last_heartbeat_tick_ < kHeartbeatIntervalMs) { return; } RemoteAction action{}; action.type = ControlType::keyboard_state; action.ks.seq = ++state_sequence_; { std::lock_guard lock(pressed_keys_mutex_); size_t index = 0; for (const auto& [_, key] : pressed_keys_) { if (index >= kMaxKeyboardStateKeys) { LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}", pressed_keys_.size()); break; } action.ks.pressed_keys[index].key_value = static_cast(key.key_code); action.ks.pressed_keys[index].scan_code = key.scan_code; action.ks.pressed_keys[index].extended = key.extended; ++index; } action.ks.pressed_count = index; } const std::string target_id = owner_.controlled_remote_id_.empty() ? owner_.focused_remote_id_ : owner_.controlled_remote_id_; std::shared_lock sessions_lock(owner_.remote_sessions_mutex_); const auto props_it = owner_.remote_sessions_.find(target_id); if (target_id.empty() || props_it == owner_.remote_sessions_.end() || props_it->second->connection_status_.load() != ConnectionStatus::Connected || !props_it->second->peer_) { last_heartbeat_tick_ = now; return; } const std::string message = action.to_json(); const int result = SendReliableDataFrame( props_it->second->peer_, message.c_str(), message.size(), props_it->second->keyboard_label_.c_str()); if (result != 0) { LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id, result); } last_heartbeat_tick_ = now; } int KeyboardController::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code, bool extended) { if (scan_code == 0) { PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended); } #if _WIN32 key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended); #endif RemoteAction action{}; action.type = ControlType::keyboard; action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up; action.k.key_value = key_code; action.k.scan_code = scan_code; action.k.extended = extended; const std::string target_id = owner_.controlled_remote_id_.empty() ? owner_.focused_remote_id_ : owner_.controlled_remote_id_; std::shared_lock sessions_lock(owner_.remote_sessions_mutex_); const auto props_it = owner_.remote_sessions_.find(target_id); if (!target_id.empty() && props_it != owner_.remote_sessions_.end() && props_it->second->connection_status_.load() == ConnectionStatus::Connected && props_it->second->peer_) { const std::string message = action.to_json(); const int result = SendReliableDataFrame( props_it->second->peer_, message.c_str(), message.size(), props_it->second->keyboard_label_.c_str()); if (result != 0) { LOG_WARN("Send keyboard command failed, remote_id={}, ret={}", target_id, result); } } TrackPressedKey(key_code, is_down, scan_code, extended); return 0; } bool KeyboardController::InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code, bool extended) { #if _WIN32 if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) { owner_.privacy_.SuspendForDesktop(); } if (owner_.is_server_mode_ || !owner_.WindowsInputStage().empty()) { static const bool process_elevated = IsAdministratorProcess(); return DispatchDesktopInput( [&] { return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(), owner_.windows_consent_ui_.load(), owner_.local_service_available_.load(), process_elevated); }, [&] { SetLastError(ERROR_SUCCESS); const bool sent = owner_.devices_.SendKeyboardCommand( key_code, is_down, scan_code, extended); const DWORD error = GetLastError(); const bool blocked = error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE; return DesktopInputResult{ sent, IsDesktopTransitionInputError(error) || blocked, blocked}; }, [&]() -> DesktopInputResult { const std::string response = SendCrossDeskSecureDesktopKeyInput( key_code, is_down, scan_code, extended, 1000); const auto json = nlohmann::json::parse(response, nullptr, false); if (!json.is_object() || !json.value("ok", false)) { const std::string error = json.is_object() ? json.value("error", "") : ""; if (IsDesktopInputSetupPending(error)) { return {false, true}; } if (IsTransientSecureDesktopInputFailure(json, is_down)) { LOG_INFO( "Secure desktop keyboard injection transient failure, " "key_code={}, is_down={}, response={}", key_code, is_down, response); // The release was not injected. Keep it tracked so cleanup can // retry on the ordinary desktop after the transition. return {false, false}; } LogSecureDesktopInputBlocked( &owner_.last_local_secure_input_block_log_tick_, owner_.WindowsInputStage().c_str()); LOG_WARN( "Secure desktop keyboard injection failed, key_code={}, " "is_down={}, " "response={}", key_code, is_down, response); return {}; } return {true, false}; }); } #endif return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code, extended); } void KeyboardController::ApplyRemoteEvent(const std::string& remote_id, const RemoteAction& action) { std::lock_guard input_lock(remote_input_mutex_); const int key_code = static_cast(action.k.key_value); const bool is_down = action.k.flag == KeyFlag::key_down; const bool injected = InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended); if (injected) pending_key_releases_.erase(key_code); auto& state = remote_states_[remote_id]; state.last_seen_tick = static_cast(SDL_GetTicks()); if (is_down && injected) { state.pressed_keys[key_code] = KeyboardKey{key_code, action.k.scan_code, action.k.extended}; } else if (!is_down && injected) { state.pressed_keys.erase(key_code); } } void KeyboardController::ApplyRemoteState(const std::string& remote_id, const RemoteAction& action) { std::lock_guard input_lock(remote_input_mutex_); auto& state = remote_states_[remote_id]; if (action.ks.seq != 0 && state.last_seq != 0 && static_cast(action.ks.seq - state.last_seq) <= 0) { return; } state.last_seq = action.ks.seq; state.last_seen_tick = static_cast(SDL_GetTicks()); state.keyboard_state_seen = true; PressedKeys desired_keys; const size_t count = (std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys); for (size_t index = 0; index < count; ++index) { const auto& key = action.ks.pressed_keys[index]; const int key_code = static_cast(key.key_value); desired_keys[key_code] = {key_code, key.scan_code, key.extended}; } const auto changes = ReconcileKeyboardState(state.pressed_keys, desired_keys); bool can_press = true; for (const auto& key : changes) { if (key.is_down && !can_press) continue; if (!InjectRemoteKey(key.key_code, key.is_down, key.scan_code, key.extended)) { // Do not replay a shortcut with a stale/missing modifier. The next // heartbeat will retry from the state that was actually injected. can_press = false; continue; } pending_key_releases_.erase(key.key_code); if (key.is_down) state.pressed_keys[key.key_code] = {key.key_code, key.scan_code, key.extended}; else state.pressed_keys.erase(key.key_code); } } void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id, const char* reason) { std::lock_guard input_lock(remote_input_mutex_); ReleaseRemotePressedKeysLocked(remote_id, reason); } void KeyboardController::ReleaseRemotePressedKeysLocked( std::string remote_id, const char* reason) { const auto state_it = remote_states_.find(remote_id); if (state_it == remote_states_.end()) return; auto keys_to_release = std::move(state_it->second.pressed_keys); remote_states_.erase(state_it); if (!keys_to_release.empty()) { LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}", keys_to_release.size(), remote_id, reason ? reason : "unknown"); } for (const auto& [code, key] : keys_to_release) { if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) { pending_key_releases_[code] = key; } else { pending_key_releases_.erase(code); } } } void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) { { std::lock_guard input_lock(remote_input_mutex_); while (!remote_states_.empty()) ReleaseRemotePressedKeysLocked(remote_states_.begin()->first, reason); } CheckRemoteTimeouts(); } void KeyboardController::CheckRemoteTimeouts() { // Do not stall the UI while a transport thread waits on desktop IPC. std::unique_lock input_lock(remote_input_mutex_, std::try_to_lock); if (!input_lock.owns_lock()) return; const uint32_t now = static_cast(SDL_GetTicks()); // Retain failed releases in ordinary mode as well as privacy mode. Retry // only on the ordinary desktop, never through the security UI helper. if (!pending_key_releases_.empty() && (last_release_retry_tick_ == 0 || now - last_release_retry_tick_ >= kReleaseRetryIntervalMs) #ifdef _WIN32 && IsCurrentSessionUserDesktopActive() #endif ) { last_release_retry_tick_ = now; for (auto it = pending_key_releases_.begin(); it != pending_key_releases_.end();) { const auto& key = it->second; if (owner_.devices_.SendKeyboardCommand( key.key_code, false, key.scan_code, key.extended)) it = pending_key_releases_.erase(it); else ++it; } } // 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; } } } } // namespace crossdesk