diff --git a/apps/desktop/src/gui/application/application_state.h b/apps/desktop/src/gui/application/application_state.h index f8a1116a..f3981bc8 100644 --- a/apps/desktop/src/gui/application/application_state.h +++ b/apps/desktop/src/gui/application/application_state.h @@ -32,7 +32,7 @@ struct InteractionState { bool audio_capture_ = false; int screen_width_ = 1280; int screen_height_ = 720; - int selected_display_ = 0; + std::atomic selected_display_{0}; char remote_id_display_[12] = ""; unsigned char audio_buffer_[720]{}; int audio_len_ = 0; @@ -77,7 +77,7 @@ struct UiState { bool show_connection_status_window_ = false; bool fullscreen_button_pressed_ = false; bool is_client_mode_ = false; - bool is_server_mode_ = false; + std::atomic is_server_mode_{false}; bool reload_recent_connections_ = true; bool show_offline_warning_window_ = false; std::string offline_warning_text_; diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index ab962077..63e55ec5 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -6201,6 +6201,7 @@ void GuiApplication::SyncGallery() { } void GuiApplication::Cleanup() { + remote_input_.Stop(); #if _WIN32 windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed); windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed); diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.cpp b/apps/desktop/src/gui/features/devices/session_device_manager.cpp index ab397060..8fcb3aba 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -21,7 +21,6 @@ #ifdef _WIN32 #include #include "platform/windows/screen_capturer/screen_capturer_win.h" -#include "platform/windows/input/mouse/mouse_controller.h" #include "service_host.h" #endif @@ -333,26 +332,44 @@ int SessionDeviceManager::StopScreenCapturer() { return 0; } +bool SessionDeviceManager::ReleaseRemoteMouseButtons() { + return remote_mouse_buttons_.ReleaseAll( + [this](unsigned mask) { return ReleaseMouseButtonMask(mask); }); +} + +bool SessionDeviceManager::ReleaseRemoteMouseButtons(const std::string& remote_id, + unsigned mask) { + return remote_mouse_buttons_.ReleaseController( + remote_id, [this](unsigned buttons) { return ReleaseMouseButtonMask(buttons); }, mask); +} + +unsigned SessionDeviceManager::ReleaseMouseButtonMask(unsigned mask) { + unsigned released = mask; + { + std::lock_guard lock(mouse_controller_mutex_); + if (mouse_controller_) released &= mouse_controller_->ReleasePressedButtons(mask); + } #ifdef _WIN32 -void SessionDeviceManager::ReleaseRemoteMouseButtons() { - if (auto mouse = dynamic_cast(mouse_controller_)) - mouse->ReleasePressedButtons(); std::lock_guard lock(service_mouse_mutex_); for (int button = 0; button < 3; ++button) { const unsigned bit = 1u << button; - if (!(service_mouse_buttons_ & bit)) continue; + if (!(service_mouse_buttons_ & mask & bit)) continue; const auto response = SendCrossDeskSecureDesktopMouseInput( service_mouse_x_, service_mouse_y_, 0, (button + 1) * 2, 1000); const auto json = nlohmann::json::parse(response, nullptr, false); if (json.is_object() && json.value("ok", false)) { service_mouse_buttons_ &= ~bit; } else { + released &= ~bit; LOG_WARN("Service mouse button release failed: button={}, response={}", button, response); } } +#endif + return released; } +#ifdef _WIN32 std::string SessionDeviceManager::SendServiceMouseCommand(int x, int y, int wheel, int flag) { std::lock_guard lock(service_mouse_mutex_); @@ -398,7 +415,10 @@ bool SessionDeviceManager::SyncDisplayInfo() { }); if (changed) display_info_list_ = displays; } - if (changed && mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays); + if (changed) { + std::lock_guard lock(mouse_controller_mutex_); + if (mouse_controller_) mouse_controller_->UpdateDisplayInfoList(displays); + } return true; } @@ -434,6 +454,7 @@ int SessionDeviceManager::StartMouseController() { #endif SyncDisplayInfo(); + std::lock_guard lock(mouse_controller_mutex_); mouse_controller_ = static_cast(device_controller_factory_->Create( DeviceControllerFactory::Device::Mouse)); @@ -452,10 +473,16 @@ int SessionDeviceManager::StartMouseController() { return init_ret; } -int SessionDeviceManager::StopMouseController() { -#ifdef _WIN32 +int SessionDeviceManager::StopMouseController(bool force) { + // Retain the native button state across transient cleanup failures. The UI + // can keep pumping while the input worker retries, including with no peers. + if (!force && owner_.remote_input_.HasPendingReleases()) return 1; + // A disconnect has revoked queued input. Finish its running injection and + // release callback before destroying the native controller. + owner_.remote_input_.WaitIdle(); + if (!force && owner_.remote_input_.HasPendingReleases()) return 1; ReleaseRemoteMouseButtons(); -#endif + std::lock_guard lock(mouse_controller_mutex_); if (mouse_controller_) { mouse_controller_->Destroy(); delete mouse_controller_; @@ -636,9 +663,9 @@ void SessionDeviceManager::UpdateInteractions() { owner_.screen_capturer_is_started_ && !owner_.start_mouse_controller_ && owner_.mouse_controller_is_started_; if (stop_wayland_mouse_before_screen) { - LOG_INFO("Stopping Wayland mouse controller before screen capturer to " + if (StopMouseController() != 0) return; + LOG_INFO("Stopped Wayland mouse controller before screen capturer to " "cleanly release the shared portal session"); - StopMouseController(); owner_.mouse_controller_is_started_ = false; } #endif @@ -659,8 +686,7 @@ void SessionDeviceManager::UpdateInteractions() { } } else if (!owner_.start_mouse_controller_ && owner_.mouse_controller_is_started_) { - StopMouseController(); - owner_.mouse_controller_is_started_ = false; + if (StopMouseController() == 0) owner_.mouse_controller_is_started_ = false; } if (owner_.screen_capturer_is_started_ && mouse_controller_ && SyncDisplayInfo()) { @@ -750,6 +776,7 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down, bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action, int selected_display) { + std::lock_guard lock(mouse_controller_mutex_); if (mouse_controller_) { return mouse_controller_->SendMouseCommand(action, selected_display) == 0; } @@ -763,19 +790,20 @@ int SessionDeviceManager::SwitchDisplay(int display_id) { return ret; } -void SessionDeviceManager::ResetToInitialDisplay() { +InputOutcome SessionDeviceManager::ResetToInitialDisplay() { if (!screen_capturer_) { - return; + return InputOutcome::ignored; } const int ret = screen_capturer_->ResetToInitialMonitor(); if (ret != 0) { LOG_WARN("Reset to initial display failed, ret={}", ret); - return; + return InputOutcome::failed; } // Capture backends start at display 0. Reset the input mapping as well so // the next connection cannot show that display while controlling another. owner_.selected_display_ = 0; SyncDisplayInfo(); + return InputOutcome::applied; } std::vector SessionDeviceManager::display_info_list() const { @@ -796,7 +824,7 @@ std::vector SessionDeviceManager::host_display_list() const { void SessionDeviceManager::DestroyDevices() { speaker_capture_.Shutdown(); - StopMouseController(); + StopMouseController(true); if (screen_capturer_) { screen_capturer_->Destroy(); delete screen_capturer_; diff --git a/apps/desktop/src/gui/features/devices/session_device_manager.h b/apps/desktop/src/gui/features/devices/session_device_manager.h index a05620da..becf58e2 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.h +++ b/apps/desktop/src/gui/features/devices/session_device_manager.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -21,6 +22,7 @@ #include "device_controller.h" #include "display_info.h" #include "features/input/captured_keyboard_queue.h" +#include "features/input/remote_mouse_buttons.h" #include "speaker_capture_controller.h" struct SDL_AudioStream; @@ -53,8 +55,15 @@ public: int InitializeScreenCapturer(); int StartScreenCapturer(); int StopScreenCapturer(); + bool ReleaseRemoteMouseButtons(); + bool ReleaseRemoteMouseButtons(const std::string& remote_id, + unsigned mask = 7); + template + InputOutcome ApplyRemoteMouse(const std::string& remote_id, MouseFlag flag, + Send send) { + return remote_mouse_buttons_.Apply(remote_id, flag, std::move(send)); + } #ifdef _WIN32 - void ReleaseRemoteMouseButtons(); std::string SendServiceMouseCommand(int x, int y, int wheel, int flag); #endif // Requests are asynchronous; Stop drains/revokes audio delivery before @@ -62,7 +71,9 @@ public: void StartSpeakerCapturer(); void StopSpeakerCapturer(); int StartMouseController(); - int StopMouseController(); + // Returns 1 while background releases are pending. force is only used + // during final destruction after the input dispatcher has been stopped. + int StopMouseController(bool force = false); int StartKeyboardCapturer(); int StopKeyboardCapturer(); @@ -79,7 +90,7 @@ public: bool extended); bool SendMouseCommand(const RemoteAction &action, int selected_display); int SwitchDisplay(int display_id); - void ResetToInitialDisplay(); + InputOutcome ResetToInitialDisplay(); // Snapshot of the capture displays. The UI thread refreshes the list every // tick while transport threads build host information and map mouse input @@ -89,6 +100,7 @@ public: std::vector host_display_list() const; private: + unsigned ReleaseMouseButtonMask(unsigned mask); void DrainCapturedKeyboardInput(); bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now, int fps); @@ -107,6 +119,9 @@ private: SpeakerCaptureController speaker_capture_; DeviceControllerFactory *device_controller_factory_ = nullptr; MouseController *mouse_controller_ = nullptr; + RemoteMouseButtons remote_mouse_buttons_; + // Input is executed on a worker while the UI starts/stops this controller. + std::mutex mouse_controller_mutex_; KeyboardCapturer *keyboard_capturer_ = nullptr; // Guards display_info_list_ (UI thread writes, transport threads read). mutable std::mutex display_info_mutex_; diff --git a/apps/desktop/src/gui/features/input/remote_input_dispatcher.cpp b/apps/desktop/src/gui/features/input/remote_input_dispatcher.cpp new file mode 100644 index 00000000..0cd3c6f2 --- /dev/null +++ b/apps/desktop/src/gui/features/input/remote_input_dispatcher.cpp @@ -0,0 +1,343 @@ +#include "remote_input_dispatcher.h" + +#include +#include + +namespace crossdesk { + +RemoteInputDispatcher::RemoteInputDispatcher(Apply apply, Release release, + Report report, + ResetDisplay reset_display, + ReleaseMouse release_mouse) + : apply_(std::move(apply)), + release_(std::move(release)), + report_(std::move(report)), + reset_display_(std::move(reset_display)), + release_mouse_(std::move(release_mouse)), + worker_([this] { Run(); }) {} + +RemoteInputDispatcher::~RemoteInputDispatcher() { Stop(); } + +bool RemoteInputDispatcher::Accepts(const RemoteAction& action) { + return action.type == ControlType::mouse || + action.type == ControlType::keyboard || + action.type == ControlType::keyboard_state || + action.type == ControlType::text_input || + action.type == ControlType::display_id || + action.type == ControlType::service_command; +} + +void RemoteInputDispatcher::Connect(const std::string& id) { + if (id.empty()) return; + std::lock_guard lock(mutex_); + if (stopping_ || !connected_.insert(id).second) return; + logs_.erase(id); // A new session must get its first diagnostic. + // In-flight resets finish before this session's input; queued old resets + // must not change the new session's display. Failed releases are retained. + RemoveResetsLocked(); + ready_.notify_one(); +} + +RemoteInputDispatcher::Result RemoteInputDispatcher::Push( + const std::string& id, const RemoteAction& action) { + // HostInfo owns raw arrays; it must never enter this value-copy queue. + if (!Accepts(action)) return Result::rejected; + std::lock_guard lock(mutex_); + if (stopping_ || !connected_.count(id)) return Result::rejected; + if (IsMove(action) && !pending_.empty()) { + auto& last = pending_.back(); + if (last.id == id && last.IsInput() && IsMove(*last.action)) { + last.action = action; + last.queued_at = Clock::now(); + return Result::coalesced; + } + } + if (pending_counts_[id] >= kMaxPendingPerController) { + QueueReleaseLocked(id); + return Result::overflow; + } + pending_.push_back({id, action, Clock::now(), ItemKind::input}); + ++pending_counts_[id]; + ready_.notify_one(); + return Result::queued; +} + +void RemoteInputDispatcher::Disconnect(const std::string& id) { + std::lock_guard lock(mutex_); + if (stopping_ || !connected_.erase(id)) return; + QueueReleaseLocked(id); + if (connected_.empty() && reset_display_) { + RemoveResetsLocked(); + pending_.push_back( + {{}, std::nullopt, Clock::now(), ItemKind::reset_display}); + } + ready_.notify_one(); +} + +void RemoteInputDispatcher::WaitIdle() { + std::unique_lock lock(mutex_); + idle_.wait(lock, [this] { return pending_.empty() && !executing_; }); +} + +bool RemoteInputDispatcher::HasPendingReleases() { + std::lock_guard lock(mutex_); + return !blocked_.empty(); +} + +void RemoteInputDispatcher::Stop() { + std::call_once(stop_once_, [this] { + { + std::lock_guard lock(mutex_); + stopping_ = true; + auto ids = std::move(connected_); + connected_.clear(); + ids.insert(blocked_.begin(), blocked_.end()); + for (const auto& item : pending_) + if (!item.IsReset()) ids.insert(item.id); + pending_.clear(); + pending_counts_.clear(); + retries_.clear(); + for (const auto& id : ids) { + pending_.push_back({id, std::nullopt, Clock::now()}); + pending_counts_[id] = 1; + } + } + ready_.notify_one(); + worker_.join(); + }); +} + +bool RemoteInputDispatcher::IsMove(const RemoteAction& action) { + return action.type == ControlType::mouse && action.m.flag == MouseFlag::move; +} + +unsigned RemoteInputDispatcher::MouseUpMask(const RemoteAction& action) { + if (action.type != ControlType::mouse) return 0; + switch (action.m.flag) { + case MouseFlag::left_up: + return 1; + case MouseFlag::right_up: + return 2; + case MouseFlag::middle_up: + return 4; + default: + return 0; + } +} + +void RemoteInputDispatcher::RemoveResetsLocked() { + pending_.erase( + std::remove_if(pending_.begin(), pending_.end(), + [](const Item& item) { return item.IsReset(); }), + pending_.end()); + if (pending_.empty() && !executing_) idle_.notify_all(); +} + +void RemoteInputDispatcher::QueueReleaseLocked(const std::string& id) { + pending_.erase( + std::remove_if(pending_.begin(), pending_.end(), + [&](const Item& item) { return item.id == id; }), + pending_.end()); + pending_counts_.erase(id); + blocked_.insert(id); + // Repeated overflow/disconnect must not defeat the retry backoff. + if (auto retry = retries_.find(id); retry != retries_.end()) { + retry->second.mouse_mask = 0; // Escalate a button repair on disconnect. + } else { + pending_.push_back({id, std::nullopt, Clock::now()}); + pending_counts_[id] = 1; + } + ready_.notify_one(); +} + +void RemoteInputDispatcher::QueueMouseReleaseLocked(const std::string& id, + unsigned mask) { + // A concurrent disconnect/overflow already requested the broader cleanup. + if (!blocked_.insert(id).second) return; + // Keep subsequent clicks/keys queued, but don't apply them until the old + // button is confirmed up. Never replay a timed-out down/click operation. + pending_.push_front( + {id, std::nullopt, Clock::now(), ItemKind::cleanup, mask}); + ++pending_counts_[id]; + ready_.notify_one(); +} + +std::optional RemoteInputDispatcher::NextLocked() { + const auto now = Clock::now(); + auto next = retries_.end(); + for (auto it = retries_.begin(); it != retries_.end(); ++it) + if (next == retries_.end() || it->second.due < next->second.due) next = it; + auto it = + std::find_if(pending_.begin(), pending_.end(), [&](const Item& item) { + return !item.IsInput() || !blocked_.count(item.id); + }); + // Alternate due retries with runnable queued work. Multiple slow cleanup + // calls must not monopolize this worker; continuous input must not starve + // recovery either. Input for the recovering controller remains blocked. + if (next != retries_.end() && next->second.due <= now && + (it == pending_.end() || !last_was_retry_)) { + last_was_retry_ = true; + return Item{next->first, std::nullopt, next->second.due, ItemKind::cleanup, + next->second.mouse_mask}; + } + if (it == pending_.end()) return std::nullopt; + last_was_retry_ = false; + Item item = std::move(*it); + pending_.erase(it); + if (!item.IsReset() && --pending_counts_.at(item.id) == 0) + pending_counts_.erase(item.id); + return item; +} + +bool RemoteInputDispatcher::ShouldReportLocked(const Item& item, + Clock::time_point now, + int64_t queued_ms, + int64_t execution_ms, + InputOutcome outcome) { + if (!report_) return false; + auto& state = logs_[item.id]; + auto& last = outcome == InputOutcome::failed + ? state.failure[item.IsInput() ? 0 : 1] + : state.slow; + if (outcome != InputOutcome::failed && queued_ms < 50 && execution_ms < 50) + return false; + if (last != Clock::time_point{} && now - last < std::chrono::seconds(2)) + return false; + last = now; + return true; +} + +void RemoteInputDispatcher::RemoveQueuedCleanupLocked(const std::string& id) { + // Cleanup queued during the running call covers the same old state. Merge + // duplicates so they cannot bypass backoff or release a later session twice. + const size_t before = pending_.size(); + pending_.erase(std::remove_if(pending_.begin(), pending_.end(), + [&](const Item& queued) { + return queued.id == id && queued.IsCleanup(); + }), + pending_.end()); + const size_t removed = before - pending_.size(); + if (removed && (pending_counts_.at(id) -= removed) == 0) + pending_counts_.erase(id); +} + +void RemoteInputDispatcher::ScheduleRetryLocked(const std::string& id, + unsigned mouse_mask, + Clock::time_point finished) { + auto [retry, inserted] = retries_.try_emplace(id); + retry->second.mouse_mask = mouse_mask; + if (!inserted) + retry->second.delay = + std::min(retry->second.delay * 2, std::chrono::milliseconds(1000)); + retry->second.due = finished + retry->second.delay; + blocked_.insert(id); +} + +void RemoteInputDispatcher::CompleteCleanupLocked(const Item& item, + InputOutcome outcome, + Clock::time_point finished) { + if (stopping_) { + retries_.erase(item.id); + blocked_.erase(item.id); + return; + } + // A successful button repair cannot discharge a concurrent request to + // release all keyboard/mouse state after disconnect or queue overflow. + const bool queued_full_cleanup = + item.mouse_mask && + std::any_of(pending_.begin(), pending_.end(), [&](const Item& queued) { + return queued.id == item.id && queued.IsCleanup() && !queued.mouse_mask; + }); + if (queued_full_cleanup) { + retries_.erase(item.id); // The queued full cleanup owns recovery now. + return; + } + auto retry = retries_.find(item.id); + if (item.mouse_mask && retry != retries_.end() && !retry->second.mouse_mask) { + if (outcome == InputOutcome::failed) + ScheduleRetryLocked(item.id, 0, finished); + else + retry->second.due = finished; + return; + } + RemoveQueuedCleanupLocked(item.id); + if (outcome == InputOutcome::failed) { + ScheduleRetryLocked(item.id, item.mouse_mask, finished); + } else { + retries_.erase(item.id); + blocked_.erase(item.id); + } +} + +InputOutcome RemoteInputDispatcher::Execute(const Item& item) { + if (item.IsReset()) return reset_display_(); + if (item.IsInput()) return apply_(item.id, *item.action); + if (item.mouse_mask && release_mouse_) + return release_mouse_(item.id, item.mouse_mask); + return release_(item.id); +} + +void RemoteInputDispatcher::Run() { + std::unique_lock lock(mutex_); + for (;;) { + auto next = NextLocked(); + if (!next) { + if (stopping_) return; + auto due = Clock::time_point::max(); + for (const auto& [_, retry] : retries_) due = std::min(due, retry.due); + if (due == Clock::time_point::max()) + ready_.wait(lock); + else + ready_.wait_until(lock, due); + continue; + } + Item item = std::move(*next); + executing_ = true; + lock.unlock(); + const auto started = Clock::now(); + InputOutcome outcome = InputOutcome::failed; + bool threw = false; + try { + outcome = Execute(item); + } catch (...) { + threw = true; + } + const auto finished = Clock::now(); + const auto queued_ms = + std::chrono::duration_cast(started - + item.queued_at) + .count(); + const auto execution_ms = + std::chrono::duration_cast(finished - + started) + .count(); + lock.lock(); + if (item.IsCleanup()) { + CompleteCleanupLocked(item, outcome, finished); + } else if (item.IsInput() && !stopping_) { + if (threw && connected_.count(item.id)) { + QueueReleaseLocked(item.id); + } else if (outcome == InputOutcome::failed) { + if (const unsigned mask = MouseUpMask(*item.action)) + QueueMouseReleaseLocked(item.id, mask); + } + } + const bool report = + ShouldReportLocked(item, finished, queued_ms, execution_ms, outcome); + if (!connected_.count(item.id) && !blocked_.count(item.id) && + !pending_counts_.count(item.id)) + logs_.erase(item.id); + lock.unlock(); + if (report) { + try { + report_(item.id, queued_ms, execution_ms, outcome); + } catch (...) { + } // Diagnostics must not terminate the input worker. + } + lock.lock(); + executing_ = false; + if (pending_.empty()) idle_.notify_all(); + } +} + +} // namespace crossdesk diff --git a/apps/desktop/src/gui/features/input/remote_input_dispatcher.h b/apps/desktop/src/gui/features/input/remote_input_dispatcher.h new file mode 100644 index 00000000..e7898651 --- /dev/null +++ b/apps/desktop/src/gui/features/input/remote_input_dispatcher.h @@ -0,0 +1,123 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-22 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _REMOTE_INPUT_DISPATCHER_H_ +#define _REMOTE_INPUT_DISPATCHER_H_ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "input_outcome.h" + +namespace crossdesk { + +// Serializes native input, recovery and display resets off the RTC thread. +class RemoteInputDispatcher { + public: + using Apply = + std::function; + using Release = std::function; + using Report = + std::function; + using ResetDisplay = std::function; + // Optional targeted adapter; legacy callers fall back to full Release. + using ReleaseMouse = + std::function; + enum class Result { queued, coalesced, rejected, overflow }; + static constexpr size_t kMaxPendingPerController = 128; + + RemoteInputDispatcher(Apply apply, Release release, Report report = {}, + ResetDisplay reset_display = {}, + ReleaseMouse release_mouse = {}); + ~RemoteInputDispatcher(); + static bool Accepts(const RemoteAction& action); + void Connect(const std::string& id); + Result Push(const std::string& id, const RemoteAction& action); + void Disconnect(const std::string& id); + + // Scheduled recovery is not idle work. Check HasPendingReleases() as well, + // or Stop() before destroying the devices used by callbacks. + void WaitIdle(); + bool HasPendingReleases(); + // Cancels pending input/timers and makes a final bounded cleanup attempt. + void Stop(); + + private: + using Clock = std::chrono::steady_clock; + enum class ItemKind { input, cleanup, reset_display }; + struct Item { + std::string id; + std::optional action; // Present only for ordinary input. + Clock::time_point queued_at; + ItemKind kind = ItemKind::cleanup; + // Zero requests full controller cleanup; otherwise only these mouse + // buttons. + unsigned mouse_mask = 0; + bool IsReset() const { return kind == ItemKind::reset_display; } + bool IsInput() const { return kind == ItemKind::input; } + bool IsCleanup() const { return kind == ItemKind::cleanup; } + }; + struct Retry { + Clock::time_point due; + std::chrono::milliseconds delay{100}; + unsigned mouse_mask = 0; + }; + struct LogState { + Clock::time_point slow{}; + std::array failure{}; + }; + + static bool IsMove(const RemoteAction& action); + static unsigned MouseUpMask(const RemoteAction& action); + void RemoveResetsLocked(); + void QueueReleaseLocked(const std::string& id); + void QueueMouseReleaseLocked(const std::string& id, unsigned mask); + void RemoveQueuedCleanupLocked(const std::string& id); + void ScheduleRetryLocked(const std::string& id, unsigned mouse_mask, + Clock::time_point finished); + void CompleteCleanupLocked(const Item& item, InputOutcome outcome, + Clock::time_point finished); + std::optional NextLocked(); + InputOutcome Execute(const Item& item); + bool ShouldReportLocked(const Item& item, Clock::time_point now, + int64_t queued_ms, int64_t execution_ms, + InputOutcome outcome); + void Run(); + + Apply apply_; + Release release_; + Report report_; + ResetDisplay reset_display_; + ReleaseMouse release_mouse_; + std::mutex mutex_; + std::condition_variable ready_, idle_; + std::deque pending_; + std::unordered_set connected_, blocked_; + std::unordered_map pending_counts_; + std::unordered_map retries_; + std::unordered_map logs_; + bool last_was_retry_ = false; + bool stopping_ = false; + bool executing_ = false; + std::once_flag stop_once_; + std::thread worker_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/features/input/remote_mouse_buttons.h b/apps/desktop/src/gui/features/input/remote_mouse_buttons.h new file mode 100644 index 00000000..115378d5 --- /dev/null +++ b/apps/desktop/src/gui/features/input/remote_mouse_buttons.h @@ -0,0 +1,78 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-22 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _REMOTE_MOUSE_BUTTONS_H_ +#define _REMOTE_MOUSE_BUTTONS_H_ + +#include + +#include +#include +#include + +#include "input_outcome.h" + +namespace crossdesk { + +// One physical button cannot belong to two controllers at once. A second +// controller's down/up must not steal or release an existing drag. +class RemoteMouseButtons { + public: + template + InputOutcome Apply(const std::string& id, MouseFlag flag, Send send) { + if (id.empty()) return InputOutcome::ignored; + std::lock_guard lock(mutex_); + const int value = static_cast(flag); + if (value < 1 || value > 6) return send(); + auto& owner = owners_[(value - 1) / 2]; + const bool down = value % 2 == 1; + if ((!owner.empty() && owner != id) || (!down && owner.empty())) + return InputOutcome::ignored; + // A service timeout may follow successful injection. Retain an attempted + // down until an up or explicit cleanup is confirmed, including exceptions. + if (down) owner = id; + const auto outcome = send(); + if (!down && outcome == InputOutcome::applied) owner.clear(); + return outcome; + } + + template + bool ReleaseController(const std::string& id, Release release, + unsigned requested_mask = 7) { + if (id.empty()) return true; + std::lock_guard lock(mutex_); + unsigned mask = 0; + for (size_t i = 0; i < owners_.size(); ++i) + if (owners_[i] == id && (requested_mask & (1u << i))) mask |= 1u << i; + return ReleaseLocked(mask, release); + } + + template + bool ReleaseAll(Release release) { + std::lock_guard lock(mutex_); + // Also clear any backend state retained after a desktop/route transition. + return ReleaseLocked(7, release); + } + + private: + template + bool ReleaseLocked(unsigned mask, Release release) { + if (!mask) return true; + // The sink returns the subset confirmed released, so one failed button + // does not retain ownership of other successfully released buttons. + const unsigned released = release(mask) & mask; + for (size_t i = 0; i < owners_.size(); ++i) + if (released & (1u << i)) owners_[i].clear(); + return released == mask; + } + + std::mutex mutex_; + std::array owners_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index f611031e..d70220fb 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -190,20 +190,13 @@ void GuiRuntime::HandleServerControllerDisconnected( const std::string& remote_id, const char* reason) { transfers_.Cancel(peer_events_.get(), remote_id); preview_subscriptions_.Remove(remote_id); - keyboard_.ReleaseRemotePressedKeys(remote_id, reason); -#ifdef _WIN32 - devices_.ReleaseRemoteMouseButtons(); -#endif - { - std::lock_guard lock(remote_pointer_input_mutex_); - last_remote_pointer_input_time_.erase(remote_id); - } bool has_connected_controller = false; std::string remaining_controller_id; { std::unique_lock lock(connection_status_mutex_); connection_status_.erase(remote_id); + remote_input_.Disconnect(remote_id); devices_.OnControllerConnectionsChanged(); connection_host_names_.erase(remote_id); privacy_sessions_.Disconnected(remote_id); @@ -252,7 +245,6 @@ void GuiRuntime::HandleServerControllerDisconnected( start_keyboard_capturer_ = false; remote_client_id_.clear(); audio_capture_ = false; - devices_.ResetToInitialDisplay(); } int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id, diff --git a/apps/desktop/src/gui/runtime/gui_runtime.cpp b/apps/desktop/src/gui/runtime/gui_runtime.cpp index eb4ad8fa..1bba9162 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.cpp +++ b/apps/desktop/src/gui/runtime/gui_runtime.cpp @@ -17,6 +17,9 @@ #include "localization.h" #include "platform/video_renderer.h" #include "rd_log.h" +#ifdef _WIN32 +#include "platform/windows/input/windows_input_injector.h" +#endif #if defined(__linux__) && !defined(__OHOS__) #include "platform/linux/headless/headless_console.h" #endif @@ -25,9 +28,43 @@ namespace crossdesk { GuiRuntime::GuiRuntime() : clipboard_(*this), devices_(*this), transfers_(*this), settings_(*this), - keyboard_(*this) {} + keyboard_(*this), + remote_input_( + [this](const std::string& id, const RemoteAction& action) { +#ifdef _WIN32 + ScopedUserDesktopInputThread input_thread; +#endif + return ApplyRemoteInput(id, action); + }, + [this](const std::string& id) { +#ifdef _WIN32 + ScopedUserDesktopInputThread input_thread; +#endif + const bool keys_released = + keyboard_.ReleaseRemotePressedKeys(id, "input_queue_reset"); + const bool mouse_released = devices_.ReleaseRemoteMouseButtons(id); + std::lock_guard lock(remote_pointer_input_mutex_); + last_remote_pointer_input_time_.erase(id); + return keys_released && mouse_released ? InputOutcome::applied + : InputOutcome::failed; + }, + [](const std::string& id, int64_t queued_ms, int64_t execution_ms, + InputOutcome outcome) { + LOG_WARN("Remote input result: peer={} queue_ms={} execution_ms={} " + "result={}", + id, queued_ms, execution_ms, InputOutcomeName(outcome)); + }, + [this] { return devices_.ResetToInitialDisplay(); }, + [this](const std::string& id, unsigned mask) { +#ifdef _WIN32 + ScopedUserDesktopInputThread input_thread; +#endif + return devices_.ReleaseRemoteMouseButtons(id, mask) + ? InputOutcome::applied : InputOutcome::failed; + }) {} GuiRuntime::~GuiRuntime() { + remote_input_.Stop(); #if _WIN32 windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed); windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed); diff --git a/apps/desktop/src/gui/runtime/gui_runtime.h b/apps/desktop/src/gui/runtime/gui_runtime.h index 854b5d71..6e2a213c 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.h +++ b/apps/desktop/src/gui/runtime/gui_runtime.h @@ -17,6 +17,7 @@ #include "features/devices/session_device_manager.h" #include "features/file_transfer/file_transfer_manager.h" #include "features/input/keyboard_controller.h" +#include "features/input/remote_input_dispatcher.h" #include "features/settings/settings_manager.h" #include "privacy_controller.h" #include "runtime/gui_state.h" @@ -90,6 +91,8 @@ class GuiRuntime : protected gui_detail::GuiState { // UI thread: send mapped mouse/keyboard input to the current control target, // or every connected session while that target leads DeskHub group control. void SendRemoteInput(const RemoteAction& action); + InputOutcome ApplyRemoteInput(const std::string& remote_id, const RemoteAction& action); + InputOutcome InjectRemoteMouse(const RemoteAction& action, int display); std::string gallery_group_control_id_; void ResetRemoteServiceStatus(RemoteSession& props); @@ -133,6 +136,7 @@ class GuiRuntime : protected gui_detail::GuiState { FileTransferManager transfers_; SettingsManager settings_; KeyboardController keyboard_; + RemoteInputDispatcher remote_input_; std::shared_ptr peer_events_; std::atomic video_frame_dirty_{false}; std::unique_ptr video_renderer_; diff --git a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp index 7db52ed3..6e58f07f 100644 --- a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp +++ b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp @@ -280,29 +280,22 @@ void PeerEventHandler::OnReceiveDataBuffer( runtime->QueuePrivacyCommand(remote_id, remote_action.pc); return; } - if (remote_action.type == ControlType::text_input) { - // Share the ordered keyboard stream with delete/return and shortcuts. - if (source_id == runtime->keyboard_label_ && - runtime->IsAuthorizedController(remote_id)) - runtime->keyboard_.ApplyRemoteText(remote_id, remote_action.text); + if (RemoteInputDispatcher::Accepts(remote_action)) { + if (!runtime->IsAuthorizedController(remote_id)) return; + if (remote_action.type == ControlType::text_input && + source_id != runtime->keyboard_label_) return; + if (runtime->remote_input_.Push(remote_id, remote_action) == + RemoteInputDispatcher::Result::overflow) + LOG_WARN("Remote input queue overflow: peer={}, clearing stale input and " + "releasing keys/buttons", + remote_id); return; } - const bool operates_host = remote_action.type == ControlType::mouse || - remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state || - remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id || - remote_action.type == ControlType::audio_capture; - if (operates_host) { + if (remote_action.type == ControlType::audio_capture) { if (!runtime->IsAuthorizedController(remote_id)) return; #ifdef _WIN32 - // KeyboardController checks the desktop immediately before each native - // key injection, including state reconciliation and cleanup. Avoid a - // second synchronous session query for every received keyboard packet. - const bool keyboard_input = remote_action.type == ControlType::keyboard || - remote_action.type == ControlType::keyboard_state; - if (!keyboard_input && runtime->privacy_.Engaged() && - !IsWindowsPrivacyDesktopAvailable()) { + if (runtime->privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) runtime->privacy_.SuspendForDesktop(); - } #endif } if (remote_action.type == ControlType::service_status) { @@ -312,28 +305,6 @@ void PeerEventHandler::OnReceiveDataBuffer( return; } - if (remote_action.type == ControlType::service_command) { - if (runtime->privacy_.Engaged()) { - runtime->privacy_.Fail("Security shortcut requested; privacy screen will turn off"); - } -#if _WIN32 - if (remote_action.c.flag == ServiceCommandFlag::send_sas) { - runtime->pending_windows_service_sas_.store(true, - std::memory_order_relaxed); - } else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) { - if (!LockWorkStation()) { - LOG_WARN("Remote lock workstation request failed, error={}", - GetLastError()); - } - } -#elif defined(__OHOS__) - if (!SendHarmonySystemShortcut(remote_action.c.flag)) - LOG_WARN("Harmony system shortcut unsupported or unauthorized, command={}", - static_cast(remote_action.c.flag)); -#endif - return; - } - if (remote_action.type == ControlType::cursor_state) { std::shared_ptr props; { @@ -464,75 +435,7 @@ void PeerEventHandler::OnReceiveDataBuffer( FreeRemoteAction(remote_action); } } else { - // remote - if (runtime->is_server_mode_ && - remote_action.type == ControlType::mouse) { - std::lock_guard lock(runtime->remote_pointer_input_mutex_); - runtime->last_remote_pointer_input_time_[remote_id] = - std::chrono::steady_clock::now(); - } -#if _WIN32 - if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) { - static const bool process_elevated = IsAdministratorProcess(); - std::string response; - const bool delivered = DispatchDesktopInput( - [&] { - return PreferUserDesktopInput( - IsCurrentSessionUserDesktopActive(), - runtime->windows_consent_ui_.load(), - runtime->local_service_available_.load(), process_elevated); - }, - [&] { - SetLastError(ERROR_SUCCESS); - const bool sent = runtime->devices_.SendMouseCommand( - remote_action, runtime->selected_display_); - const DWORD error = GetLastError(); - const bool blocked = - error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE; - return DesktopInputResult{ - sent, IsDesktopTransitionInputError(error) || blocked, blocked}; - }, - [&]() -> DesktopInputResult { - int absolute_x = 0; - int absolute_y = 0; - if (!BuildAbsoluteMousePosition( - runtime->devices_.display_info_list(), - runtime->selected_display_, remote_action.m.x, - remote_action.m.y, &absolute_x, &absolute_y)) { - LOG_WARN( - "Secure desktop mouse injection skipped, invalid display " - "mapping: display_index={}, x={}, y={}", - runtime->selected_display_, remote_action.m.x, - remote_action.m.y); - return {}; - } - - response = runtime->devices_.SendServiceMouseCommand( - absolute_x, absolute_y, remote_action.m.s, - static_cast(remote_action.m.flag)); - auto json = nlohmann::json::parse(response, nullptr, false); - const bool sent = json.is_object() && json.value("ok", false); - const std::string error = - json.is_object() ? json.value("error", "") : ""; - return {sent, IsDesktopInputSetupPending(error)}; - }); - if (!delivered && !response.empty()) { - LogSecureDesktopInputBlocked( - &runtime->last_local_secure_input_block_log_tick_, "local", - runtime->WindowsInputStage().c_str()); - LOG_WARN( - "Secure desktop mouse injection failed, x={}, y={}, wheel={}, " - "flag={}, response={}", - remote_action.m.x, remote_action.m.y, remote_action.m.s, - static_cast(remote_action.m.flag), response); - } - return; - } -#endif - if (remote_action.type == ControlType::mouse) { - runtime->devices_.SendMouseCommand(remote_action, - runtime->selected_display_); - } else if (remote_action.type == ControlType::audio_capture) { + if (remote_action.type == ControlType::audio_capture) { // A negotiated subscription remains authoritative if a legacy capture // command arrives after it (e.g. when unmuting an inactive window). const auto subscription = runtime->preview_subscriptions_.Get(remote_id); @@ -544,20 +447,139 @@ void PeerEventHandler::OnReceiveDataBuffer( else if (ret != 0) // Legacy/web transports without the per-peer API retain their fallback. runtime->devices_.StopSpeakerCapturer(); - } else if (remote_action.type == ControlType::keyboard) { - runtime->keyboard_.ApplyRemoteEvent(remote_id, remote_action); - } else if (remote_action.type == ControlType::keyboard_state) { - runtime->keyboard_.ApplyRemoteState(remote_id, remote_action); - } else if (remote_action.type == ControlType::display_id) { - const int ret = runtime->devices_.SwitchDisplay(remote_action.d); - if (ret == 0) { - runtime->selected_display_ = remote_action.d; - } else { - LOG_WARN("Display switch skipped, invalid display_id={}", - remote_action.d); - } } } } -} // namespace crossdesk +InputOutcome GuiRuntime::ApplyRemoteInput(const std::string& remote_id, + const RemoteAction& remote_action) { + if (!IsAuthorizedController(remote_id)) return InputOutcome::ignored; + if (remote_action.type == ControlType::text_input) { + return keyboard_.ApplyRemoteText(remote_id, remote_action.text); + } +#ifdef _WIN32 + const bool keyboard_input = remote_action.type == ControlType::keyboard || + remote_action.type == ControlType::keyboard_state; + if (!keyboard_input && privacy_.Engaged() && + !IsWindowsPrivacyDesktopAvailable()) + privacy_.SuspendForDesktop(); +#endif + if (remote_action.type == ControlType::service_command) { + if (privacy_.Engaged()) { + privacy_.Fail( + "Security shortcut requested; privacy screen will turn off"); + } +#if _WIN32 + if (remote_action.c.flag == ServiceCommandFlag::send_sas) { + pending_windows_service_sas_.store(true, std::memory_order_relaxed); + return InputOutcome::applied; + } else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) { + if (!LockWorkStation()) { + LOG_WARN("Remote lock workstation request failed, error={}", + GetLastError()); + return InputOutcome::failed; + } + return InputOutcome::applied; + } +#elif defined(__OHOS__) + if (!SendHarmonySystemShortcut(remote_action.c.flag)) { + LOG_WARN( + "Harmony system shortcut unsupported or unauthorized, command={}", + static_cast(remote_action.c.flag)); + return InputOutcome::failed; + } + return InputOutcome::applied; +#endif + return InputOutcome::ignored; + } + + const int display = selected_display_.load(); + if (is_server_mode_ && remote_action.type == ControlType::mouse) { + std::lock_guard lock(remote_pointer_input_mutex_); + last_remote_pointer_input_time_[remote_id] = + std::chrono::steady_clock::now(); + } + + if (remote_action.type == ControlType::mouse) { + return devices_.ApplyRemoteMouse(remote_id, remote_action.m.flag, + [this, &remote_action, display] { + return InjectRemoteMouse(remote_action, display); + }); + } else if (remote_action.type == ControlType::keyboard) { + return keyboard_.ApplyRemoteEvent(remote_id, remote_action); + } else if (remote_action.type == ControlType::keyboard_state) { + return keyboard_.ApplyRemoteState(remote_id, remote_action); + } else if (remote_action.type == ControlType::display_id) { + const int ret = devices_.SwitchDisplay(remote_action.d); + if (ret == 0) { + selected_display_ = remote_action.d; + return InputOutcome::applied; + } else { + LOG_WARN("Display switch skipped, invalid display_id={}", + remote_action.d); + return InputOutcome::failed; + } + } + return InputOutcome::ignored; +} + +InputOutcome GuiRuntime::InjectRemoteMouse(const RemoteAction& remote_action, + int display) { +#if _WIN32 + if (is_server_mode_) { + static const bool process_elevated = IsAdministratorProcess(); + std::string response; + const bool delivered = DispatchDesktopInput( + [&] { + return PreferUserDesktopInput( + IsCurrentSessionUserDesktopActive(), windows_consent_ui_.load(), + local_service_available_.load(), process_elevated); + }, + [&] { + SetLastError(ERROR_SUCCESS); + const bool sent = devices_.SendMouseCommand(remote_action, display); + const DWORD error = GetLastError(); + const bool blocked = + error == ERROR_ACCESS_DENIED || error == ERROR_GEN_FAILURE; + return DesktopInputResult{ + sent, IsDesktopTransitionInputError(error) || blocked, blocked}; + }, + [&]() -> DesktopInputResult { + int absolute_x = 0; + int absolute_y = 0; + if (!BuildAbsoluteMousePosition(devices_.display_info_list(), display, + remote_action.m.x, remote_action.m.y, + &absolute_x, &absolute_y)) { + LOG_WARN( + "Secure desktop mouse injection skipped, invalid display " + "mapping: display_index={}, x={}, y={}", + display, remote_action.m.x, remote_action.m.y); + return {}; + } + + response = devices_.SendServiceMouseCommand( + absolute_x, absolute_y, remote_action.m.s, + static_cast(remote_action.m.flag)); + auto json = nlohmann::json::parse(response, nullptr, false); + const bool sent = json.is_object() && json.value("ok", false); + const std::string error = + json.is_object() ? json.value("error", "") : ""; + return {sent, IsDesktopInputSetupPending(error)}; + }); + if (!delivered && !response.empty()) { + LogSecureDesktopInputBlocked(&last_local_secure_input_block_log_tick_, + "local", WindowsInputStage().c_str()); + LOG_WARN( + "Secure desktop mouse injection failed, x={}, y={}, wheel={}, " + "flag={}, response={}", + remote_action.m.x, remote_action.m.y, remote_action.m.s, + static_cast(remote_action.m.flag), response); + } + return delivered ? InputOutcome::applied : InputOutcome::failed; + } +#endif + return devices_.SendMouseCommand(remote_action, display) + ? InputOutcome::applied : InputOutcome::failed; +} + +} // namespace crossdesk diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index 02d33cc9..e1627e9b 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -437,6 +437,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, runtime->ApplyPrivacyAdmission(automatic_enable); } runtime->connection_status_[remote_id] = status; + if (status == ConnectionStatus::Connected) + runtime->remote_input_.Connect(remote_id); #if _WIN32 runtime->windows_rdp_handoff_connected_->store(std::any_of( runtime->connection_status_.begin(), runtime->connection_status_.end(), diff --git a/apps/desktop/src/platform/windows/gui/application/unattended_application.cpp b/apps/desktop/src/platform/windows/gui/application/unattended_application.cpp index fc669a24..b8ad2144 100644 --- a/apps/desktop/src/platform/windows/gui/application/unattended_application.cpp +++ b/apps/desktop/src/platform/windows/gui/application/unattended_application.cpp @@ -64,6 +64,7 @@ int GuiApplication::RunUnattended(void* stop_event) { } // Quiesce network callbacks before destroying the controllers they use. + remote_input_.Stop(); CloseAllRemoteSessions(); WaitForSessionCleanup(); keyboard_.ReleaseAllRemotePressedKeys("unattended_host_exit"); diff --git a/apps/desktop/src/platform/windows/input/windows_input_injector.cpp b/apps/desktop/src/platform/windows/input/windows_input_injector.cpp index b86cf40c..45f72947 100644 --- a/apps/desktop/src/platform/windows/input/windows_input_injector.cpp +++ b/apps/desktop/src/platform/windows/input/windows_input_injector.cpp @@ -16,6 +16,7 @@ namespace { struct InputResult { UINT sent; DWORD error; + bool requires_worker = false; }; InputResult InjectOnUserDesktop(INPUT* inputs, UINT count) { @@ -25,7 +26,10 @@ InputResult InjectOnUserDesktop(INPUT* inputs, UINT count) { ScopedInteractiveDesktop desktop; if (!desktop.Bind(L"")) { const DWORD error = GetLastError(); - return {0, error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE}; + // A library may have created a window/hook on an opted-in thread. This + // failure occurs before SendInput, so retrying on the worker is safe. + return {0, error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE, + error == ERROR_BUSY}; } if (!desktop.IsReceivingInput()) { return {0, ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION}; @@ -99,8 +103,13 @@ UINT SendInputOnUserDesktop(INPUT* inputs, UINT count) { SetLastError(ERROR_INVALID_PARAMETER); return 0; } - static UserDesktopInputWorker worker; - const InputResult result = worker.Send(inputs, count); + InputResult result{0, ERROR_SUCCESS, true}; + if (ScopedUserDesktopInputThread::IsCurrent()) + result = InjectOnUserDesktop(inputs, count); + if (result.requires_worker) { + static UserDesktopInputWorker worker; + result = worker.Send(inputs, count); + } const DWORD error = result.error; if (error != ERROR_SUCCESS) { // Include session identity even when Windows reports no useful SendInput diff --git a/apps/desktop/src/platform/windows/input/windows_input_injector.h b/apps/desktop/src/platform/windows/input/windows_input_injector.h index 735f2790..d2191d4b 100644 --- a/apps/desktop/src/platform/windows/input/windows_input_injector.h +++ b/apps/desktop/src/platform/windows/input/windows_input_injector.h @@ -11,9 +11,23 @@ namespace crossdesk { -// Executes on a worker without windows or hooks, attached to this process's -// current user input desktop. Returns a SendInput count and preserves the -// worker's failure code in the calling thread's last-error value. +// Opt in only on the remote input worker. UI/hook callers keep using the +// fallback worker. Desktop attachment is still checked for each injection. +class ScopedUserDesktopInputThread { + public: + ScopedUserDesktopInputThread() { ++depth_; } + ~ScopedUserDesktopInputThread() { --depth_; } + ScopedUserDesktopInputThread(const ScopedUserDesktopInputThread&) = delete; + ScopedUserDesktopInputThread& operator=(const ScopedUserDesktopInputThread&) = + delete; + static bool IsCurrent() { return depth_ != 0; } + private: + inline static thread_local unsigned depth_ = 0; +}; + +// Executes directly on an opted-in worker, or on the fallback input worker, +// attached to this process's current user input desktop. Returns a SendInput +// count and preserves the failure code in the calling thread's last-error value. UINT SendInputOnUserDesktop(const INPUT& input); // Synchronously injects one ordered batch. The caller owns the buffer until