diff --git a/apps/desktop/src/gui/features/input/input_outcome.h b/apps/desktop/src/gui/features/input/input_outcome.h new file mode 100644 index 00000000..bfecceab --- /dev/null +++ b/apps/desktop/src/gui/features/input/input_outcome.h @@ -0,0 +1,29 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-22 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _INPUT_OUTCOME_H_ +#define _INPUT_OUTCOME_H_ + +namespace crossdesk { + +// Applied means the native/service API accepted the operation. It does not +// assert that the foreground application has consumed the injected event. +enum class InputOutcome { applied, ignored, failed }; + +inline const char* InputOutcomeName(InputOutcome outcome) { + switch (outcome) { + case InputOutcome::applied: + return "applied"; + case InputOutcome::ignored: + return "ignored"; + case InputOutcome::failed: + return "failed"; + } + return "failed"; +} + +} // namespace crossdesk +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/features/input/keyboard_controller.cpp b/apps/desktop/src/gui/features/input/keyboard_controller.cpp index 917f73b4..102f3c4a 100644 --- a/apps/desktop/src/gui/features/input/keyboard_controller.cpp +++ b/apps/desktop/src/gui/features/input/keyboard_controller.cpp @@ -4,6 +4,7 @@ #include +#include #include #include #include @@ -249,14 +250,14 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down, extended); } -void KeyboardController::ApplyRemoteEvent(const std::string& remote_id, +InputOutcome 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); + if (injected) pending_key_releases_.Supersede(key_code); auto& state = remote_states_[remote_id]; state.last_seen_tick = static_cast(SDL_GetTicks()); @@ -266,24 +267,28 @@ void KeyboardController::ApplyRemoteEvent(const std::string& remote_id, } else if (!is_down && injected) { state.pressed_keys.erase(key_code); } + return injected ? InputOutcome::applied : InputOutcome::failed; } -void KeyboardController::ApplyRemoteText(const std::string& remote_id, +InputOutcome KeyboardController::ApplyRemoteText(const std::string& remote_id, const std::string& text) { std::lock_guard input_lock(remote_input_mutex_); - if (!owner_.IsAuthorizedController(remote_id) || - !owner_.devices_.SupportsTextInput()) return; - if (!owner_.devices_.SendTextCommand(text)) + if (!owner_.IsAuthorizedController(remote_id)) return InputOutcome::ignored; + if (!owner_.devices_.SupportsTextInput()) return InputOutcome::failed; + if (!owner_.devices_.SendTextCommand(text)) { LOG_WARN("Failed to inject committed remote text"); + return InputOutcome::failed; + } + return InputOutcome::applied; } -void KeyboardController::ApplyRemoteState(const std::string& remote_id, +InputOutcome 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; + return InputOutcome::ignored; } state.last_seq = action.ks.seq; @@ -300,56 +305,60 @@ void KeyboardController::ApplyRemoteState(const std::string& remote_id, } const auto changes = ReconcileKeyboardState(state.pressed_keys, desired_keys); bool can_press = true; + bool applied = 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; + applied = false; continue; } - pending_key_releases_.erase(key.key_code); + pending_key_releases_.Supersede(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); } + return applied ? InputOutcome::applied : InputOutcome::failed; } -void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id, +bool KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id, const char* reason) { std::lock_guard input_lock(remote_input_mutex_); - ReleaseRemotePressedKeysLocked(remote_id, reason); + return ReleaseRemotePressedKeysLocked(remote_id, reason); } -void KeyboardController::ReleaseRemotePressedKeysLocked( +bool 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); + if (state_it != remote_states_.end()) { + const auto& keys = state_it->second.pressed_keys; + if (!keys.empty()) { + LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}", + keys.size(), remote_id, reason ? reason : "unknown"); } + // Stage before erasing: subsequent dispatcher retries must still see the + // keys if injection fails, even though active/heartbeat state is gone. + pending_key_releases_.Queue(remote_id, keys); + remote_states_.erase(state_it); } + return pending_key_releases_.ReleaseController( + remote_id, [this](const KeyboardKey& key) { + return InjectRemoteKey(key.key_code, false, key.scan_code, key.extended); + }); } 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(); + std::lock_guard input_lock(remote_input_mutex_); + auto ids = pending_key_releases_.ControllerIds(); + for (const auto& [id, _] : remote_states_) ids.push_back(id); + std::sort(ids.begin(), ids.end()); + ids.erase(std::unique(ids.begin(), ids.end()), ids.end()); + // A failure remains pending; shutdown must never spin until it succeeds. + for (const auto& id : ids) ReleaseRemotePressedKeysLocked(id, reason); } void KeyboardController::CheckRemoteTimeouts() { @@ -359,7 +368,7 @@ void KeyboardController::CheckRemoteTimeouts() { 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() && + if (!pending_key_releases_.Empty() && (last_release_retry_tick_ == 0 || now - last_release_retry_tick_ >= kReleaseRetryIntervalMs) #ifdef _WIN32 @@ -367,15 +376,10 @@ void KeyboardController::CheckRemoteTimeouts() { #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; - } + pending_key_releases_.RetryAll([this](const KeyboardKey& key) { + return owner_.devices_.SendKeyboardCommand( + key.key_code, false, key.scan_code, key.extended); + }); } // Keep the timeout decision and release under the same lock, so a fresh diff --git a/apps/desktop/src/gui/features/input/keyboard_controller.h b/apps/desktop/src/gui/features/input/keyboard_controller.h index 20ba2a7d..148970b4 100644 --- a/apps/desktop/src/gui/features/input/keyboard_controller.h +++ b/apps/desktop/src/gui/features/input/keyboard_controller.h @@ -9,6 +9,8 @@ #include #include "keyboard_state.h" +#include "input_outcome.h" +#include "pending_key_releases.h" namespace crossdesk { @@ -25,12 +27,12 @@ public: void ForceReleasePressedKeys(); void RecoverCapturedState(const PressedKeys& pressed); void SendHeartbeat(bool force); - void ApplyRemoteEvent(const std::string &remote_id, + InputOutcome ApplyRemoteEvent(const std::string &remote_id, const RemoteAction &remote_action); - void ApplyRemoteText(const std::string& remote_id, const std::string& text); - void ApplyRemoteState(const std::string &remote_id, + InputOutcome ApplyRemoteText(const std::string& remote_id, const std::string& text); + InputOutcome ApplyRemoteState(const std::string &remote_id, const RemoteAction &remote_action); - void ReleaseRemotePressedKeys(const std::string &remote_id, + bool ReleaseRemotePressedKeys(const std::string &remote_id, const char *reason); void ReleaseAllRemotePressedKeys(const char* reason); void CheckRemoteTimeouts(); @@ -48,7 +50,7 @@ private: bool InjectRemoteKey(int key_code, bool is_down, uint32_t scan_code, bool extended); // Caller holds remote_input_mutex_. Copy the id because this erases state. - void ReleaseRemotePressedKeysLocked(std::string remote_id, const char* reason); + bool ReleaseRemotePressedKeysLocked(std::string remote_id, const char* reason); GuiRuntime &owner_; PressedKeys pressed_keys_; @@ -59,7 +61,7 @@ private: // Guards all remote key state and serializes injection, cleanup and retries. std::mutex remote_input_mutex_; // Successful new input supersedes cleanup of an older press of that key. - PressedKeys pending_key_releases_; + PendingKeyReleases pending_key_releases_; uint32_t last_release_retry_tick_ = 0; }; diff --git a/apps/desktop/src/gui/features/input/pending_key_releases.h b/apps/desktop/src/gui/features/input/pending_key_releases.h new file mode 100644 index 00000000..c34f15c9 --- /dev/null +++ b/apps/desktop/src/gui/features/input/pending_key_releases.h @@ -0,0 +1,79 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-22 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _PENDING_KEY_RELEASES_H_ +#define _PENDING_KEY_RELEASES_H_ + +#include +#include +#include + +#include "keyboard_state.h" + +namespace crossdesk { + +// Caller serializes access with KeyboardController's remote-input mutex. +// Pending releases outlive the peer's active key/heartbeat state, but retain +// their controller identity so cleanup cannot report success prematurely. +class PendingKeyReleases { + public: + void Queue(const std::string& id, const PressedKeys& keys) { + if (keys.empty()) return; + auto& pending = peers_[id]; + for (const auto& [code, key] : keys) pending[code] = key; + } + + bool Empty() const { return peers_.empty(); } + bool Contains(const std::string& id) const { return peers_.count(id) != 0; } + + std::vector ControllerIds() const { + std::vector ids; + ids.reserve(peers_.size()); + for (const auto& [id, _] : peers_) ids.push_back(id); + return ids; + } + + // A confirmed new event supersedes an older pending release of that key, + // including one belonging to a disconnected controller. + void Supersede(int key_code) { + for (auto it = peers_.begin(); it != peers_.end();) { + it->second.erase(key_code); + if (it->second.empty()) + it = peers_.erase(it); + else + ++it; + } + } + + template + bool ReleaseController(const std::string& id, SendUp send_up) { + auto peer = peers_.find(id); + if (peer == peers_.end()) return true; + auto& keys = peer->second; + for (auto it = keys.begin(); it != keys.end();) { + if (send_up(it->second)) + it = keys.erase(it); + else + ++it; + } + if (!keys.empty()) return false; + peers_.erase(peer); + return true; + } + + template + void RetryAll(SendUp send_up) { + // One bounded pass, even when every native release fails. + for (const auto& id : ControllerIds()) ReleaseController(id, send_up); + } + + private: + std::unordered_map peers_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file