From 3cfe7cbb80d0b605b172b03dabc2c2b074036738 Mon Sep 17 00:00:00 2001 From: kunkundi Date: Thu, 8 Oct 2026 23:44:54 +0800 Subject: [PATCH] [fix] improve Windows secure desktop capture and input transitions --- .../devices/session_device_manager.cpp | 5 +- .../features/devices/session_device_manager.h | 2 +- .../features/input/keyboard_controller.cpp | 81 +-- apps/desktop/src/gui/runtime/gui_runtime.h | 9 + .../src/gui/runtime/peer_data_callbacks.cpp | 84 +-- apps/desktop/src/gui/runtime/runtime_state.h | 9 + .../gui/runtime/windows_service_runtime.cpp | 69 ++- .../input/keyboard/keyboard_capturer.cpp | 4 +- .../windows/input/mouse/mouse_controller.cpp | 4 +- .../screen_capturer/screen_capturer_win.cpp | 135 +++-- .../screen_capturer/screen_capturer_win.h | 12 +- .../secure_desktop_dxgi_capture.h | 160 ++++++ .../secure_desktop_frame_pacer.h | 65 +++ .../secure_desktop_frame_schedule.h | 8 +- .../secure_desktop_nv12_cache.h | 59 +++ .../secure_desktop_status_poller.h | 13 +- .../service/desktop_transition_policy.h | 60 +++ .../windows/service/interactive_desktop.h | 7 +- .../platform/windows/service/service_host.cpp | 485 +++++++++--------- .../platform/windows/service/service_host.h | 17 + .../windows/service/service_query_worker.h | 113 ++++ .../windows/service/session_helper_main.cpp | 377 ++++++++------ .../service/shared_capture_configuration.h | 30 ++ .../service/shared_capture_lifecycle.h | 121 +++++ .../tests/windows_service_mouse_ipc_test.cpp | 26 +- apps/desktop/xmake/targets.lua | 3 +- 26 files changed, 1376 insertions(+), 582 deletions(-) create mode 100644 apps/desktop/src/platform/windows/screen_capturer/secure_desktop_dxgi_capture.h create mode 100644 apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_pacer.h create mode 100644 apps/desktop/src/platform/windows/screen_capturer/secure_desktop_nv12_cache.h create mode 100644 apps/desktop/src/platform/windows/service/desktop_transition_policy.h create mode 100644 apps/desktop/src/platform/windows/service/service_query_worker.h create mode 100644 apps/desktop/src/platform/windows/service/shared_capture_configuration.h create mode 100644 apps/desktop/src/platform/windows/service/shared_capture_lifecycle.h 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 7a08644c..47914cc3 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.cpp +++ b/apps/desktop/src/gui/features/devices/session_device_manager.cpp @@ -665,11 +665,12 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down, key_code, is_down, scan_code, extended) == 0; } -void SessionDeviceManager::SendMouseCommand(const RemoteAction &action, +bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action, int selected_display) { if (mouse_controller_) { - mouse_controller_->SendMouseCommand(action, selected_display); + return mouse_controller_->SendMouseCommand(action, selected_display) == 0; } + return false; } int SessionDeviceManager::SwitchDisplay(int display_id) { 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 d1c9bf2a..4984375c 100644 --- a/apps/desktop/src/gui/features/devices/session_device_manager.h +++ b/apps/desktop/src/gui/features/devices/session_device_manager.h @@ -71,7 +71,7 @@ public: bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code, bool extended); - void SendMouseCommand(const RemoteAction &action, int selected_display); + bool SendMouseCommand(const RemoteAction &action, int selected_display); int SwitchDisplay(int display_id); void ResetToInitialDisplay(); diff --git a/apps/desktop/src/gui/features/input/keyboard_controller.cpp b/apps/desktop/src/gui/features/input/keyboard_controller.cpp index c4c19537..2604dd78 100644 --- a/apps/desktop/src/gui/features/input/keyboard_controller.cpp +++ b/apps/desktop/src/gui/features/input/keyboard_controller.cpp @@ -16,7 +16,8 @@ #include "runtime/gui_runtime.h" #include "windows_key_metadata.h" #if _WIN32 -#include "interactive_state.h" +#include "desktop_transition_policy.h" +#include "interactive_desktop.h" #include "service_host.h" #endif @@ -82,12 +83,11 @@ void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) { } bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response, - const RemoteAction& action) { + bool is_down) { return response.is_object() && response.value("error", std::string()) == "send_input_failed" && response.value("code", 0u) == ERROR_ACCESS_DENIED && - action.type == ControlType::keyboard && - action.k.flag == KeyFlag::key_up; + !is_down; } #endif @@ -218,40 +218,51 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down, if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) { owner_.privacy_.SuspendForDesktop(); } - if (owner_.local_service_status_received_ && - IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) { - 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_discarded() || !json.value("ok", false)) { - RemoteAction action{}; - action.type = ControlType::keyboard; - action.k.key_value = static_cast(key_code); - action.k.scan_code = scan_code; - action.k.extended = extended; - 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); - return true; - } + if (owner_.is_server_mode_ || !owner_.WindowsInputStage().empty()) { + return DispatchDesktopInput( + [&] { + return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(), + owner_.windows_consent_ui_.load()); + }, + [&] { + SetLastError(ERROR_SUCCESS); + const bool sent = owner_.devices_.SendKeyboardCommand( + key_code, is_down, scan_code, extended); + return DesktopInputResult{ + sent, IsDesktopTransitionInputError(GetLastError())}; + }, + [&]() -> 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); + return {true, false}; + } - LogSecureDesktopInputBlocked( - &owner_.last_local_secure_input_block_log_tick_, - owner_.local_interactive_stage_.c_str()); - LOG_WARN( - "Secure desktop keyboard injection failed, key_code={}, is_down={}, " - "response={}", - key_code, is_down, response); - return false; - } - return true; + 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 - // Recheck after the desktop query; key-up cleanup remains allowed while paused. return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code, extended); } diff --git a/apps/desktop/src/gui/runtime/gui_runtime.h b/apps/desktop/src/gui/runtime/gui_runtime.h index 2ff875b1..1e4e12c2 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.h +++ b/apps/desktop/src/gui/runtime/gui_runtime.h @@ -87,6 +87,15 @@ class GuiRuntime : protected gui_detail::GuiState { RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const; #if _WIN32 void ResetLocalWindowsServiceState(bool clear_pending_sas); + std::string WindowsInputStage() const { + std::lock_guard lock(windows_input_stage_mutex_); + return windows_input_stage_; + } + void PublishWindowsInputStage() { + std::lock_guard lock(windows_input_stage_mutex_); + windows_input_stage_ = local_service_status_received_ + ? local_interactive_stage_ : std::string{}; + } #endif #ifdef __APPLE__ diff --git a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp index 1de8b17a..ef0c5098 100644 --- a/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp +++ b/apps/desktop/src/gui/runtime/peer_data_callbacks.cpp @@ -26,7 +26,8 @@ #include "runtime/gui_runtime.h" #if _WIN32 -#include "interactive_state.h" +#include "desktop_transition_policy.h" +#include "interactive_desktop.h" #include "service_host.h" #endif @@ -392,45 +393,60 @@ void PeerEventHandler::OnReceiveDataBuffer( std::chrono::steady_clock::now(); } #if _WIN32 - if (runtime->local_service_status_received_ && - IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) && - remote_action.type != ControlType::keyboard && - remote_action.type != ControlType::keyboard_state) { - if (remote_action.type == ControlType::mouse) { - 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; - } + if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) { + std::string response; + const bool delivered = DispatchDesktopInput( + [&] { + return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(), + runtime->windows_consent_ui_.load()); + }, + [&] { + SetLastError(ERROR_SUCCESS); + const bool sent = runtime->devices_.SendMouseCommand( + remote_action, runtime->selected_display_); + return DesktopInputResult{ + sent, IsDesktopTransitionInputError(GetLastError())}; + }, + [&]() -> 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 {}; + } - const std::string response = SendCrossDeskSecureDesktopMouseInput( - absolute_x, absolute_y, remote_action.m.s, - static_cast(remote_action.m.flag), 1000); - auto json = nlohmann::json::parse(response, nullptr, false); - if (json.is_discarded() || !json.value("ok", false)) { - LogSecureDesktopInputBlocked( - &runtime->last_local_secure_input_block_log_tick_, "local", - runtime->local_interactive_stage_.c_str()); - LOG_WARN( - "Secure desktop mouse injection failed, x={}, y={}, wheel={}, " - "flag={}, response={}", - absolute_x, absolute_y, remote_action.m.s, - static_cast(remote_action.m.flag), response); - } - return; + response = SendCrossDeskSecureDesktopMouseInput( + absolute_x, absolute_y, remote_action.m.s, + static_cast(remote_action.m.flag), 1000); + 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_); + runtime->selected_display_); } else if (remote_action.type == ControlType::audio_capture) { if (remote_action.a) runtime->devices_.StartSpeakerCapturer(); diff --git a/apps/desktop/src/gui/runtime/runtime_state.h b/apps/desktop/src/gui/runtime/runtime_state.h index 47b03cbb..4a7006fc 100644 --- a/apps/desktop/src/gui/runtime/runtime_state.h +++ b/apps/desktop/src/gui/runtime/runtime_state.h @@ -29,6 +29,10 @@ #include "runtime/session_lifecycle.h" #include "thumbnail.h" +#if _WIN32 +#include "platform/windows/service/service_query_worker.h" +#endif + namespace crossdesk::gui_detail { struct InfrastructureState { @@ -83,6 +87,11 @@ struct PeerState { // state so platform code does not appear to be part of the wire contract. struct PlatformIntegrationState { #if _WIN32 + std::unique_ptr windows_service_worker_; + // Published by the UI, read by transport callbacks without sharing a mutable string. + mutable std::mutex windows_input_stage_mutex_; + std::string windows_input_stage_; + std::atomic windows_consent_ui_{false}; std::atomic pending_windows_service_sas_{false}; bool local_service_status_received_ = false; bool local_service_available_ = false; diff --git a/apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp b/apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp index 55c1ca73..0d780efa 100644 --- a/apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp +++ b/apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp @@ -14,6 +14,7 @@ #if _WIN32 #include "interactive_desktop.h" +#include "desktop_transition_policy.h" #include "interactive_state.h" #include "service_host.h" #endif @@ -25,12 +26,13 @@ struct WindowsServiceInteractiveStatus { bool available = false; bool session_mismatch = false; bool sas_secure_desktop_grace_active = false; + bool consent_ui_visible = false; unsigned int error_code = 0; std::string interactive_stage; std::string error; }; -constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000; +constexpr uint32_t kWindowsServiceStatusIntervalMs = 250; constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000; constexpr DWORD kWindowsServiceQueryTimeoutMs = 500; constexpr DWORD kWindowsServiceSasTimeoutMs = 500; @@ -51,15 +53,13 @@ BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) { return action; } -bool QueryWindowsServiceInteractiveStatus( +bool ParseWindowsServiceInteractiveStatus(const std::string& response, WindowsServiceInteractiveStatus *status) { if (status == nullptr) { return false; } *status = WindowsServiceInteractiveStatus{}; - const std::string response = - QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs); auto json = nlohmann::json::parse(response, nullptr, false); if (json.is_discarded() || !json.is_object()) { status->error = "invalid_service_status_json"; @@ -84,6 +84,7 @@ bool QueryWindowsServiceInteractiveStatus( status->interactive_stage = json.value("interactive_stage", std::string()); status->sas_secure_desktop_grace_active = json.value("sas_secure_desktop_grace_active", false); + status->consent_ui_visible = json.value("consent_ui_visible", false); if (ShouldNormalizeUnlockToUserDesktop( json.value("interactive_lock_screen_visible", false), @@ -125,13 +126,31 @@ void GuiRuntime::HandleWindowsServiceIntegration() { return; } - bool force_broadcast = false; + if (!windows_service_worker_) { + windows_service_worker_ = std::make_unique( + [](const std::string& command) { + ServiceQueryResult result; + result.response = QueryCrossDeskService( + command, command == "sas" ? kWindowsServiceSasTimeoutMs + : kWindowsServiceQueryTimeoutMs); + if (command == "status") { + // A successful service reply can still carry a pre-unlock sample. + result.user_desktop_recovered = IsCurrentSessionUserDesktopActive(); + } + return result; + }); + } if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) { if (privacy_.Engaged()) { privacy_.Fail("Security shortcut requested; privacy screen will turn off"); } - const std::string response = - QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs); + windows_service_worker_->RequestSas(); + } + + const uint32_t now = static_cast(SDL_GetTicks()); + auto result = windows_service_worker_->Take(); + if (result && result->command == "sas") { + const std::string& response = result->response; auto json = nlohmann::json::parse(response, nullptr, false); if (json.is_discarded() || !json.value("ok", false)) { LOG_WARN("Remote SAS request failed: {}", response); @@ -145,21 +164,33 @@ void GuiRuntime::HandleWindowsServiceIntegration() { local_interactive_stage_ = "secure-desktop"; } last_windows_service_status_tick_ = 0; - force_broadcast = true; + PublishWindowsInputStage(); + if (local_service_status_received_) { + WindowsServiceInteractiveStatus optimistic; + optimistic.available = local_service_available_; + optimistic.interactive_stage = local_interactive_stage_; + const auto message = BuildWindowsServiceStatusAction(optimistic).to_json(); + SendReliableDataFrame(peer_, message.data(), message.size(), + control_data_label_.c_str()); + } + result.reset(); } - const uint32_t now = static_cast(SDL_GetTicks()); - if (!force_broadcast && last_windows_service_status_tick_ != 0 && - now - last_windows_service_status_tick_ < + if (last_windows_service_status_tick_ == 0 || + now - last_windows_service_status_tick_ >= kWindowsServiceStatusIntervalMs) { - return; + windows_service_worker_->RequestStatus(); + last_windows_service_status_tick_ = now; } - last_windows_service_status_tick_ = now; + if (!result) return; WindowsServiceInteractiveStatus status; - const bool status_ok = QueryWindowsServiceInteractiveStatus(&status); - const bool user_desktop_recovered = - !status.available && IsCurrentSessionUserDesktopActive(); + const bool status_ok = ParseWindowsServiceInteractiveStatus(result->response, &status); + const bool user_desktop_recovered = PreferUserDesktopInput( + result->user_desktop_recovered, status.consent_ui_visible); + if (status.available || status.session_mismatch) { + windows_consent_ui_.store(status.available && status.consent_ui_visible); + } if (user_desktop_recovered) status.interactive_stage = "user-desktop"; WindowsServiceInteractiveStatus broadcast_status = status; const bool previous_secure_desktop_interaction = @@ -169,7 +200,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() { static_cast(optimistic_windows_secure_desktop_until_tick_ - now) > 0; const bool keep_optimistic_secure_desktop = - status_ok && status.available && optimistic_secure_desktop_active && + !user_desktop_recovered && status_ok && status.available && optimistic_secure_desktop_active && status.sas_secure_desktop_grace_active && status.interactive_stage == "user-desktop"; local_service_status_received_ = @@ -226,6 +257,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() { last_logged_service_error_code = 0; } + PublishWindowsInputStage(); RemoteAction remote_action = BuildWindowsServiceStatusAction(broadcast_status); std::string msg = remote_action.to_json(); @@ -239,14 +271,17 @@ void GuiRuntime::HandleWindowsServiceIntegration() { #if _WIN32 void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) { + if (windows_service_worker_) windows_service_worker_->Reset(); last_windows_service_status_tick_ = 0; if (clear_pending_sas) { pending_windows_service_sas_.store(false, std::memory_order_relaxed); } local_service_status_received_ = false; local_service_available_ = false; + windows_consent_ui_.store(false); local_interactive_stage_.clear(); optimistic_windows_secure_desktop_until_tick_ = 0; + PublishWindowsInputStage(); } #endif diff --git a/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp b/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp index 32b4ed24..26237a1b 100644 --- a/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp +++ b/apps/desktop/src/platform/windows/input/keyboard/keyboard_capturer.cpp @@ -313,8 +313,10 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down, const UINT sent = SendInputOnUserDesktop(input); if (sent != 1) { + const DWORD error = GetLastError(); LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code, - is_down, GetLastError()); + is_down, error); + SetLastError(error); return -1; } diff --git a/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp b/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp index 3caa1b49..e84c47b1 100644 --- a/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp +++ b/apps/desktop/src/platform/windows/input/mouse/mouse_controller.cpp @@ -144,9 +144,11 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action, ip.mi.dwExtraInfo = kInjectedMouseInputMarker; const UINT sent = SendInputOnUserDesktop(ip); if (sent != 1) { + const DWORD error = GetLastError(); LOG_WARN("SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}", ip.mi.dx, ip.mi.dy, remote_action.m.s, - static_cast(remote_action.m.flag), GetLastError()); + static_cast(remote_action.m.flag), error); + SetLastError(error); return -1; } if (ip.mi.dwFlags & MOUSEEVENTF_LEFTDOWN) pressed_buttons_.fetch_or(1); diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp index 25b3c835..6ee08f04 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp @@ -408,6 +408,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() { } status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu); + status.consent_ui_visible = json.value("consent_ui_visible", false); status.helper_running = json.value("secure_input_helper_running", false); status.helper_process_id = json.value("secure_input_helper_pid", 0u); status.interactive_stage = json.value("interactive_stage", std::string()); @@ -425,11 +426,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() { SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() { auto status = ReadSecureDesktopServiceStatus(); - if (!status.service_available && IsCurrentSessionUserDesktopActive()) { - status.desktop_state_known = true; - status.capture_active = false; - status.interactive_stage = "user-desktop"; - } + if (IsCurrentSessionUserDesktopActive()) status.OnUserDesktopObserved(); return status; } @@ -445,10 +442,11 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id, GetCrossDeskSecureInputHelperPipeName(session_id); std::string error; DWORD code = 0; - // The legacy single-frame path includes a full GDI capture and a large - // response. Give it a separate deadline from the small control messages. + // Starting may have to join/release the old GPU producer first. Its reply + // cannot use the short deadline intended for lightweight control messages. const DWORD timeout_ms = - command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0 + (command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0 || + command.rfind(kCrossDeskSecureInputCaptureStartCommandPrefix, 0) == 0) ? 1000 : kSecureDesktopHelperPipeTimeoutMs; const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms, @@ -579,8 +577,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { 0, std::memory_order_relaxed); const ULONGLONG elapsed_ms = start_tick == 0 ? 0 : GetTickCount64() - start_tick; - post_secure_desktop_drop_logged_.store(false, - std::memory_order_relaxed); LOG_INFO( "Windows capturer first normal frame after secure desktop: " "reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, " @@ -804,7 +800,6 @@ void ScreenCapturerWin::ResetPostSecureDesktopState() { secure_desktop_capture_active_.store(false, std::memory_order_relaxed); post_secure_desktop_waiting_for_frame_.store(false, std::memory_order_relaxed); - post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed); post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed); SharedSecureDesktopCursorState().SetActive(false); } @@ -1237,6 +1232,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { LOG_INFO("Windows capturer: restarting capture backend after secure desktop"); impl_->Stop(); + last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed); int ret = impl_->Start(show_cursor); if (ret == 0) { applied_show_cursor_ = show_cursor; @@ -1403,7 +1399,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) { target_session_id = secure_shared_session_id_; } - if (secure_shared_capture_started_ && + if ((secure_shared_capture_started_ || secure_shared_start_pending_) && target_session_id != 0xFFFFFFFF) { std::vector response; std::string error_message; @@ -1414,15 +1410,9 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) { CloseSecureDesktopSharedFrame(); secure_shared_capture_started_ = false; + secure_shared_start_pending_ = false; secure_shared_session_id_ = 0xFFFFFFFF; - secure_shared_left_ = 0; - secure_shared_top_ = 0; - secure_shared_width_ = 0; - secure_shared_height_ = 0; - secure_shared_fps_ = 0; - secure_shared_show_cursor_ = true; - secure_shared_stage_.clear(); - secure_shared_desktop_.clear(); + secure_shared_configuration_ = {}; } bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id, @@ -1569,27 +1559,37 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture( return false; } + const SharedCaptureConfiguration configuration{left, top, width, height, + show_cursor, fps}; if (secure_shared_capture_started_ && secure_shared_session_id_ == session_id && - secure_shared_left_ == left && secure_shared_top_ == top && - secure_shared_width_ == width && secure_shared_height_ == height && - secure_shared_stage_ == stage && secure_shared_desktop_ == desktop && - secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps && + secure_shared_configuration_ == configuration && OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) { return true; } - StopSecureDesktopSharedCapture(secure_shared_session_id_); + if (secure_shared_session_id_ != session_id) { + StopSecureDesktopSharedCapture(secure_shared_session_id_); + } else { + // capture-start performs the replacement atomically in the helper. Avoid + // a separate stop round trip and keep retries idempotent after a timeout. + CloseSecureDesktopSharedFrame(); + } + secure_shared_capture_started_ = false; + secure_shared_start_pending_ = true; + secure_shared_session_id_ = session_id; - const std::string command = - BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps, - stage, desktop); + const std::string command = BuildSecureCaptureStartCommand( + left, top, width, height, show_cursor, fps, stage, desktop); std::vector response; if (!QuerySecureDesktopHelperCommand(session_id, command, &response, error_out)) { + secure_shared_start_pending_ = + error_out && IsPendingSecureCaptureStart(*error_out); return false; } + secure_shared_start_pending_ = false; Json json = Json::parse(response.begin(), response.end(), nullptr, false); if (json.is_discarded() || !json.value("ok", false)) { if (error_out != nullptr) { @@ -1600,14 +1600,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture( secure_shared_capture_started_ = true; secure_shared_session_id_ = session_id; - secure_shared_left_ = left; - secure_shared_top_ = top; - secure_shared_width_ = width; - secure_shared_height_ = height; - secure_shared_show_cursor_ = show_cursor; - secure_shared_fps_ = fps; - secure_shared_stage_ = stage; - secure_shared_desktop_ = desktop; + secure_shared_configuration_ = configuration; if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) { StopSecureDesktopSharedCapture(session_id); @@ -1628,6 +1621,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { ULONGLONG capture_stage_started_tick = 0; bool post_secure_restart_pending = false; bool desktop_was_unavailable = false; + bool normal_backend_suspended = false; ULONGLONG post_secure_restart_deadline_tick = 0; ULONGLONG last_post_secure_restart_tick = 0; ULONGLONG next_cursor_update_tick = 0; @@ -1685,9 +1679,14 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { const ULONGLONG now = GetTickCount64(); const auto frame_started = std::chrono::steady_clock::now(); report_stats(false); - if (auto sample = status_poller.Take()) { - const auto previous = std::exchange(status, std::move(sample->status)); - max_status_ms = (std::max)(max_status_ms, sample->query_ms); + auto sample = status_poller.Take(); + if (sample || (desktop_available && status.capture_active)) { + auto latest = sample ? std::move(sample->status) : status; + // Recheck when applying a queued reply, not only when querying. A local + // unlock must never be undone by a delayed secure-desktop snapshot. + if (desktop_available) latest.OnUserDesktopObserved(); + const auto previous = std::exchange(status, std::move(latest)); + if (sample) max_status_ms = (std::max)(max_status_ms, sample->query_ms); if (status.service_available != previous.service_available || status.error != previous.error) { if (status.service_available) { @@ -1718,8 +1717,12 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { report_stats(true); // A restarted helper can leave our mapping/event handles alive, but // their old producer is gone. Explicitly establish a fresh stream. - StopSecureDesktopSharedCapture(secure_shared_session_id_); - frame_schedule.Reset(now); + if (status.capture_active != previous.capture_active || + status.active_session_id != previous.active_session_id || + status.helper_process_id != previous.helper_process_id) { + StopSecureDesktopSharedCapture(secure_shared_session_id_); + frame_schedule.Reset(now); + } const bool secure_capture_started = !previous.capture_active && status.capture_active; const bool secure_capture_ended = @@ -1734,8 +1737,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { post_secure_restart_pending = false; post_secure_desktop_waiting_for_frame_.store( false, std::memory_order_relaxed); - post_secure_desktop_drop_logged_.store(false, - std::memory_order_relaxed); post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed); } else if (secure_capture_ended) { post_secure_restart_pending = true; @@ -1744,8 +1745,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { last_post_secure_restart_tick = 0; post_secure_desktop_waiting_for_frame_.store( true, std::memory_order_relaxed); - post_secure_desktop_drop_logged_.store(false, - std::memory_order_relaxed); post_secure_desktop_started_tick_.store(now, std::memory_order_relaxed); } @@ -1763,28 +1762,52 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { last_post_secure_restart_tick = 0; } + if ((!desktop_available || status.capture_active) && + !normal_backend_suspended) { + // LockApp may return to Default while the session remains locked. Merely + // dropping callbacks leaves a second DXGI/GDI/WGC pipeline capturing and + // converting the same desktop alongside the helper. Stop it once, also + // when local lock detection precedes the service/helper startup. + std::lock_guard lock(impl_mutex_); + if (impl_) { + impl_->Stop(); + normal_backend_suspended = true; + LOG_INFO("Windows capturer: normal backend suspended for secure desktop"); + } + } + if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { StopSecureDesktopSharedCapture(secure_shared_session_id_); - CheckDisplayPresence(now); - MaybeAdoptVirtualDisplay(); - CheckCaptureProgress(now); + // Normal capture was intentionally unavailable on the secure desktop. + // Do not schedule a second topology/stall rebuild during its recovery. + if (!post_secure_restart_pending && desktop_available && + !normal_backend_suspended) { + CheckDisplayPresence(now); + MaybeAdoptVirtualDisplay(); + CheckCaptureProgress(now); + } if (post_secure_restart_pending && desktop_available) { if (now >= post_secure_restart_deadline_tick) { LOG_WARN( "Windows capturer: capture backend restart after secure desktop " - "timed out"); - post_secure_restart_pending = false; + "still unavailable; retrying"); + // The normal backend is stopped. Retain recovery ownership instead + // of falling back to a stall probe on a thread that is not running. + post_secure_restart_deadline_tick = + now + kPostSecureDesktopRestartTimeoutMs; } else if (last_post_secure_restart_tick == 0 || now - last_post_secure_restart_tick >= kPostSecureDesktopRestartRetryMs) { last_post_secure_restart_tick = now; post_secure_restart_pending = !RestartCaptureBackendAfterSecureDesktop(); + if (!post_secure_restart_pending) normal_backend_suspended = false; if (!post_secure_restart_pending && privacy_) privacy_->ResumeAfterDesktopRecovery(); } } - if (!post_secure_restart_pending && now >= next_cursor_update_tick) { + if (!post_secure_restart_pending && !normal_backend_suspended && + now >= next_cursor_update_tick) { next_cursor_update_tick = now + 500; ApplyCursorCaptureSetting(); } @@ -1835,7 +1858,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { status.interactive_stage, status.interactive_desktop, show_cursor, shared_fps, &error_message); if (!shared_ready) { - report_shared_error("start", error_message); + if (!secure_shared_start_pending_) report_shared_error("start", error_message); frame_schedule.OnStartFailure(GetTickCount64()); } else if (!was_started) { frame_schedule.Reset(GetTickCount64()); @@ -1851,8 +1874,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { frame_schedule.OnSharedFrame(GetTickCount64()); } - const bool frame_pending = shared_ready && !frame_delivered && - IsPendingSecureDesktopFrame(error_message); + const bool frame_pending = secure_shared_start_pending_ || + (shared_ready && !frame_delivered && IsPendingSecureDesktopFrame(error_message)); if (frame_pending) ++shared_waits; if (shared_ready && !frame_delivered && !frame_pending) { report_shared_error("read", error_message); @@ -1905,6 +1928,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { } } + // The shared producer owns frame pacing. Waiting again here can miss the + // next publication (and overwrite a pending frame) due to timer rounding. + if (shared_ready && (frame_delivered || frame_pending)) continue; + const auto elapsed_ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - frame_started) diff --git a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h index ee8d0dbc..704dc6db 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h +++ b/apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h @@ -25,6 +25,7 @@ #include "privacy_controller.h" // Relative: platform include directories stay private to the capture target. #include "../virtual_display/virtual_display_provisioner.h" +#include "../service/shared_capture_configuration.h" #include "headless_recovery.h" namespace crossdesk { @@ -117,7 +118,6 @@ class ScreenCapturerWin : public ScreenCapturer { std::atomic monitor_index_{0}; std::atomic secure_desktop_capture_active_{false}; std::atomic post_secure_desktop_waiting_for_frame_{false}; - std::atomic post_secure_desktop_drop_logged_{false}; std::atomic post_secure_desktop_started_tick_{0}; std::thread secure_capture_thread_; HANDLE secure_frame_mapping_ = nullptr; @@ -125,15 +125,9 @@ class ScreenCapturerWin : public ScreenCapturer { uint8_t* secure_frame_view_ = nullptr; size_t secure_frame_view_size_ = 0; DWORD secure_shared_session_id_ = 0xFFFFFFFF; - int secure_shared_left_ = 0; - int secure_shared_top_ = 0; - int secure_shared_width_ = 0; - int secure_shared_height_ = 0; - int secure_shared_fps_ = 0; - bool secure_shared_show_cursor_ = true; - std::string secure_shared_stage_; - std::string secure_shared_desktop_; + SharedCaptureConfiguration secure_shared_configuration_; bool secure_shared_capture_started_ = false; + bool secure_shared_start_pending_ = false; std::shared_ptr native_frame_pool_; void BuildCanonicalFromImpl(); diff --git a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_dxgi_capture.h b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_dxgi_capture.h new file mode 100644 index 00000000..628d9760 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_dxgi_capture.h @@ -0,0 +1,160 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SECURE_DESKTOP_DXGI_CAPTURE_H_ +#define _SECURE_DESKTOP_DXGI_CAPTURE_H_ + +#include +#include +#include +#include +#include + +#include +#include + +namespace crossdesk { + +// Owned by the SYSTEM helper's capture thread, after binding its input desktop. +// Destroy before switching desktops. The uncomposited pixels stay valid on a +// timeout, so a static lock screen and a moving separate cursor keep their +// requested cadence without another GPU readback or a GDI screenshot. +class SecureDesktopDxgiCapture { + public: + HRESULT Initialize(int left, int top, int width, int height) { + if (width <= 0 || height <= 0) return E_INVALIDARG; + Microsoft::WRL::ComPtr factory; + HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf())); + if (FAILED(hr)) return hr; + Microsoft::WRL::ComPtr adapter; + for (UINT a = 0; SUCCEEDED(factory->EnumAdapters1( + a, adapter.ReleaseAndGetAddressOf())); ++a) { + Microsoft::WRL::ComPtr output; + for (UINT o = 0; SUCCEEDED(adapter->EnumOutputs( + o, output.ReleaseAndGetAddressOf())); ++o) { + DXGI_OUTPUT_DESC desc{}; + if (FAILED(output->GetDesc(&desc)) || !desc.AttachedToDesktop) continue; + const RECT& bounds = desc.DesktopCoordinates; + if (left < bounds.left || top < bounds.top || + static_cast(left) + width > bounds.right || + static_cast(top) + height > bounds.bottom) continue; + + hr = D3D11CreateDevice( + adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr, + D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION, + device_.GetAddressOf(), nullptr, context_.GetAddressOf()); + if (FAILED(hr)) return hr; + Microsoft::WRL::ComPtr output1; + hr = output.As(&output1); + if (FAILED(hr)) return hr; + hr = output1->DuplicateOutput(device_.Get(), duplication_.GetAddressOf()); + if (FAILED(hr)) return hr; + + output_desc_ = desc; + offset_x_ = left - bounds.left; + offset_y_ = top - bounds.top; + DXGI_OUTDUPL_DESC duplication_desc{}; + duplication_->GetDesc(&duplication_desc); + switch (duplication_desc.Rotation) { + case DXGI_MODE_ROTATION_ROTATE90: rotation_ = libyuv::kRotate90; break; + case DXGI_MODE_ROTATION_ROTATE180: rotation_ = libyuv::kRotate180; break; + case DXGI_MODE_ROTATION_ROTATE270: rotation_ = libyuv::kRotate270; break; + default: rotation_ = libyuv::kRotate0; break; + } + return S_OK; + } + } + // Includes headless desktops and regions spanning more than one output. + return DXGI_ERROR_NOT_FOUND; + } + + // S_FALSE means no first frame yet; the caller can use GDI until one arrives. + // On any failure the caller must discard this instance, including its cache. + HRESULT Capture() { + updated_ = false; + if (!duplication_) return E_UNEXPECTED; + Microsoft::WRL::ComPtr resource; + DXGI_OUTDUPL_FRAME_INFO info{}; + const HRESULT hr = duplication_->AcquireNextFrame(0, &info, &resource); + if (hr == DXGI_ERROR_WAIT_TIMEOUT) return pixels_.empty() ? S_FALSE : S_OK; + if (FAILED(hr)) return hr; + struct ReleaseFrame { + IDXGIOutputDuplication* duplication; + ~ReleaseFrame() { duplication->ReleaseFrame(); } + } release{duplication_.Get()}; + if (info.LastMouseUpdateTime.QuadPart != 0) { + cursor_embedded_ = !info.PointerPosition.Visible; + } + // Pointer-only updates leave the desktop image unchanged. + if (info.LastPresentTime.QuadPart == 0 && !pixels_.empty()) return S_OK; + + Microsoft::WRL::ComPtr texture; + HRESULT result = resource.As(&texture); + if (FAILED(result)) return result; + D3D11_TEXTURE2D_DESC desc{}; + texture->GetDesc(&desc); + const bool swap_axes = rotation_ == libyuv::kRotate90 || + rotation_ == libyuv::kRotate270; + const UINT logical_width = swap_axes ? desc.Height : desc.Width; + const UINT logical_height = swap_axes ? desc.Width : desc.Height; + const RECT& bounds = output_desc_.DesktopCoordinates; + if (desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM || + logical_width != static_cast(bounds.right - bounds.left) || + logical_height != static_cast(bounds.bottom - bounds.top)) { + return DXGI_ERROR_ACCESS_LOST; + } + if (!staging_) { + desc.Usage = D3D11_USAGE_STAGING; + desc.BindFlags = 0; + desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; + desc.MiscFlags = 0; + result = device_->CreateTexture2D(&desc, nullptr, staging_.GetAddressOf()); + if (FAILED(result)) return result; + } + context_->CopyResource(staging_.Get(), texture.Get()); + D3D11_MAPPED_SUBRESOURCE mapped{}; + result = context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped); + if (FAILED(result)) return result; + stride_ = static_cast(logical_width) * 4; + pixels_.resize(static_cast(stride_) * logical_height); + const int converted = libyuv::ARGBRotate( + static_cast(mapped.pData), static_cast(mapped.RowPitch), + pixels_.data(), stride_, static_cast(desc.Width), + static_cast(desc.Height), rotation_); + context_->Unmap(staging_.Get(), 0); + updated_ = converted == 0; + return converted == 0 ? S_OK : E_FAIL; + } + + const uint8_t* pixels() const { + return pixels_.data() + static_cast(offset_y_) * stride_ + offset_x_ * 4; + } + int stride() const { return stride_; } + bool updated() const { return updated_; } + const DXGI_OUTPUT_DESC& output() const { return output_desc_; } + void SetSoftwareCursor(bool software) { cursor_embedded_ = software; } + bool CursorEmbedded(const CURSORINFO& cursor) const { + return cursor_embedded_ && (cursor.flags & CURSOR_SHOWING) != 0 && + MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) == + output_desc_.Monitor; + } + + private: + Microsoft::WRL::ComPtr device_; + Microsoft::WRL::ComPtr context_; + Microsoft::WRL::ComPtr duplication_; + Microsoft::WRL::ComPtr staging_; + DXGI_OUTPUT_DESC output_desc_{}; + libyuv::RotationMode rotation_ = libyuv::kRotate0; + int offset_x_ = 0, offset_y_ = 0, stride_ = 0; + bool cursor_embedded_ = false; + bool updated_ = false; + std::vector pixels_; +}; + +} // namespace crossdesk + +#endif // _SECURE_DESKTOP_DXGI_CAPTURE_H_ diff --git a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_pacer.h b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_pacer.h new file mode 100644 index 00000000..735f5224 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_pacer.h @@ -0,0 +1,65 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SECURE_DESKTOP_FRAME_PACER_H_ +#define _SECURE_DESKTOP_FRAME_PACER_H_ + +#include + +#include +#include +#include + +namespace crossdesk { + +// Sleep's system-tick rounding can turn a 33ms frame into a 47ms frame. +// Request precise waits only on this capture thread, without changing the +// machine's timer resolution. Older Windows versions use a regular timer. +class SecureDesktopFramePacer { + public: + using Clock = std::chrono::steady_clock; + explicit SecureDesktopFramePacer(int fps) + : interval_(std::chrono::nanoseconds( + 1000000000LL / (std::clamp)(fps > 0 ? fps : 30, 1, 1000))) { + constexpr DWORD kHighResolution = 0x00000002; + timer_ = CreateWaitableTimerExW(nullptr, nullptr, kHighResolution, + TIMER_MODIFY_STATE | SYNCHRONIZE); + if (!timer_) timer_ = CreateWaitableTimerW(nullptr, FALSE, nullptr); + } + ~SecureDesktopFramePacer() { + if (timer_) CloseHandle(timer_); + } + SecureDesktopFramePacer(const SecureDesktopFramePacer&) = delete; + SecureDesktopFramePacer& operator=(const SecureDesktopFramePacer&) = delete; + + void Wait(Clock::time_point frame_started) { + const auto now = Clock::now(); + // Keep fractional frame periods and compensate for small scheduler delays. + // After a slow frame, resume now instead of issuing a burst of old frames. + if (deadline_ == Clock::time_point{}) deadline_ = frame_started; + deadline_ += interval_; + if (deadline_ <= now) { + deadline_ = now; + return; + } + LARGE_INTEGER due{}; + const auto ticks = std::chrono::duration_cast( + deadline_ - now).count() / 100; + due.QuadPart = -(ticks > 0 ? ticks : 1); + if (timer_ && SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE) && + WaitForSingleObject(timer_, INFINITE) == WAIT_OBJECT_0) return; + std::this_thread::sleep_until(deadline_); + } + + private: + HANDLE timer_ = nullptr; + Clock::duration interval_; + Clock::time_point deadline_{}; +}; + +} // namespace crossdesk + +#endif // _SECURE_DESKTOP_FRAME_PACER_H_ diff --git a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_schedule.h b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_schedule.h index 868a6921..b117d90e 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_schedule.h +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_schedule.h @@ -12,6 +12,12 @@ namespace crossdesk { +inline bool IsPendingSecureCaptureStart(const std::string& error) { + // Once a command was written, a lost response does not mean startup failed. + return error.rfind("pipe_read_failed:", 0) == 0 || + error.rfind("pipe_write_failed:", 0) == 0; +} + inline bool IsPendingSecureDesktopFrame(const std::string& error) { return error == "frame_wait_timeout" || error == "shared_frame_write_in_progress" || @@ -47,4 +53,4 @@ class SecureDesktopFrameSchedule { } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_nv12_cache.h b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_nv12_cache.h new file mode 100644 index 00000000..5a3e18c4 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_nv12_cache.h @@ -0,0 +1,59 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SECURE_DESKTOP_NV12_CACHE_H_ +#define _SECURE_DESKTOP_NV12_CACHE_H_ + +#include + +#include +#include +#include + +namespace crossdesk { + +// One cache per capture desktop/resource lifetime. DXGI can report a pointer +// update or timeout without changing any pixels. Keep publishing those frames +// (and fresh cursor metadata) without converting the entire desktop again. +class SecureDesktopNv12Cache { + public: + bool Convert(const uint8_t* pixels, int stride, int width, int height, + bool pixels_changed) { + updated_ = false; + if (!pixels || width <= 0 || height <= 0 || (width & 1) || (height & 1) || + stride < static_cast(width) * 4) { + valid_ = false; + return false; + } + if (valid_ && !pixels_changed && width == width_ && height == height_) + return true; + + valid_ = false; + const size_t luma_size = static_cast(width) * height; + pixels_.resize(luma_size + luma_size / 2); + if (libyuv::ARGBToNV12(pixels, stride, pixels_.data(), width, + pixels_.data() + luma_size, width, width, + height) != 0) + return false; + width_ = width; + height_ = height; + valid_ = updated_ = true; + return true; + } + + bool updated() const { return updated_; } + const std::vector& pixels() const { return pixels_; } + + private: + bool valid_ = false; + bool updated_ = false; + int width_ = 0, height_ = 0; + std::vector pixels_; +}; + +} // namespace crossdesk + +#endif // _SECURE_DESKTOP_NV12_CACHE_H_ diff --git a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_status_poller.h b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_status_poller.h index 5cb82290..43047897 100644 --- a/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_status_poller.h +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_status_poller.h @@ -23,6 +23,7 @@ struct SecureDesktopServiceStatus { bool service_available = false; bool desktop_state_known = false; bool capture_active = false; + bool consent_ui_visible = false; bool helper_running = false; uint32_t helper_process_id = 0; uint32_t active_session_id = 0xFFFFFFFF; @@ -31,6 +32,16 @@ struct SecureDesktopServiceStatus { std::string interactive_desktop; std::string error; + void OnUserDesktopObserved() { + if (consent_ui_visible) return; + desktop_state_known = true; + capture_active = false; + helper_running = false; + helper_process_id = 0; + interactive_stage = "user-desktop"; + interactive_desktop.clear(); + } + void UpdateFrom(SecureDesktopServiceStatus latest) { if (latest.service_available || latest.desktop_state_known) { *this = std::move(latest); @@ -108,4 +119,4 @@ class SecureDesktopStatusPoller { } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/service/desktop_transition_policy.h b/apps/desktop/src/platform/windows/service/desktop_transition_policy.h new file mode 100644 index 00000000..86ec608f --- /dev/null +++ b/apps/desktop/src/platform/windows/service/desktop_transition_policy.h @@ -0,0 +1,60 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _DESKTOP_TRANSITION_POLICY_H_ +#define _DESKTOP_TRANSITION_POLICY_H_ + +#include +#include + +namespace crossdesk { + +inline bool PreferUserDesktopInput(bool user_desktop_active, + bool consent_ui_visible) { + // Windows can display an elevated UAC dialog on Default when secure-desktop + // prompting is disabled. Preserve its authorized SYSTEM input path. + return user_desktop_active && !consent_ui_visible; +} + +inline bool IsDesktopInputSetupPending(const std::string& error) { + // These failures explicitly precede injection; timeouts remain ambiguous. + return error == "secure_input_helper_not_ready" || + error == "secure_input_not_active"; +} + +inline bool IsCurrentDesktopSample(uint64_t sample_started, uint64_t event_tick, + bool lock_known, bool session_locked, + bool sample_locked) { + // A reply received after a notification can still contain a pre-event sample. + return (event_tick == 0 || sample_started > event_tick) && + (!lock_known || session_locked == sample_locked); +} + +struct DesktopInputResult { + bool delivered = false; + // True only when the path explicitly refused the input before injection. + // A pipe timeout may have happened after a click/key was applied: never + // replay it. + bool not_injected = false; +}; + +template +bool DispatchDesktopInput(ProbeUserDesktop probe_user, SendUser send_user, + SendSecure send_secure) { + const bool user = probe_user(); + const auto result = user ? send_user() : send_secure(); + if (result.delivered) return true; + if (!result.not_injected) return false; + const bool current_user = probe_user(); + if (current_user == user) return false; + // Handle a switch between the probe and injection exactly once. Ordering is + // synchronous, so button/key releases cannot overtake their corresponding + // downs. + return (current_user ? send_user() : send_secure()).delivered; +} + +} // namespace crossdesk +#endif // _DESKTOP_TRANSITION_POLICY_H_ diff --git a/apps/desktop/src/platform/windows/service/interactive_desktop.h b/apps/desktop/src/platform/windows/service/interactive_desktop.h index 54b1b09e..6ea465fa 100644 --- a/apps/desktop/src/platform/windows/service/interactive_desktop.h +++ b/apps/desktop/src/platform/windows/service/interactive_desktop.h @@ -14,6 +14,11 @@ namespace crossdesk { +inline bool IsDesktopTransitionInputError(DWORD error) { + return error == ERROR_ACCESS_DENIED || + error == ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION; +} + inline std::wstring GetDesktopNameW(HDESK desktop) { DWORD bytes_needed = 0; if (desktop == nullptr) { @@ -149,4 +154,4 @@ class ScopedInteractiveDesktop { } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/apps/desktop/src/platform/windows/service/service_host.cpp b/apps/desktop/src/platform/windows/service/service_host.cpp index 46ba7955..6e1238f2 100644 --- a/apps/desktop/src/platform/windows/service/service_host.cpp +++ b/apps/desktop/src/platform/windows/service/service_host.cpp @@ -17,6 +17,9 @@ #include #include "interactive_state.h" +#include "interactive_desktop.h" +#include "desktop_transition_policy.h" +#include "named_pipe_deadline.h" #include "path_manager.h" #include "rd_log.h" #include "session_helper_shared.h" @@ -267,80 +270,14 @@ bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) { std::string QueryNamedPipeMessage(const std::wstring& pipe_name, const std::string& command, DWORD timeout_ms) { - constexpr DWORD kPipeConnectRetryDelayMs = 15; - const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms; - - auto is_transient_pipe_error = [](DWORD error) { - return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY || - error == ERROR_SEM_TIMEOUT; - }; - - HANDLE pipe = INVALID_HANDLE_VALUE; - DWORD last_error = ERROR_SEM_TIMEOUT; - while (GetTickCount64() <= deadline_tick) { - const ULONGLONG now = GetTickCount64(); - const DWORD wait_timeout = - deadline_tick > now - ? static_cast((std::min)( - deadline_tick - now, static_cast(MAXDWORD))) - : 0; - - if (!WaitNamedPipeW(pipe_name.c_str(), wait_timeout)) { - const DWORD error = GetLastError(); - last_error = error; - const ULONGLONG retry_tick = GetTickCount64(); - if (is_transient_pipe_error(error) && retry_tick < deadline_tick) { - Sleep(static_cast((std::min)( - static_cast(kPipeConnectRetryDelayMs), - deadline_tick - retry_tick))); - continue; - } - return BuildErrorJson("pipe_unavailable", error); - } - - pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, - nullptr, OPEN_EXISTING, 0, nullptr); - if (pipe != INVALID_HANDLE_VALUE) { - break; - } - - const DWORD error = GetLastError(); - last_error = error; - const ULONGLONG retry_tick = GetTickCount64(); - if (is_transient_pipe_error(error) && retry_tick < deadline_tick) { - Sleep(static_cast((std::min)( - static_cast(kPipeConnectRetryDelayMs), - deadline_tick - retry_tick))); - continue; - } - return BuildErrorJson("pipe_connect_failed", error); + std::vector response; + std::string error; + DWORD code = ERROR_SUCCESS; + if (!QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms, + &response, &error, &code)) { + return BuildErrorJson(error.c_str(), code); } - - if (pipe == INVALID_HANDLE_VALUE) { - return BuildErrorJson("pipe_unavailable", last_error); - } - - DWORD pipe_mode = PIPE_READMODE_MESSAGE; - SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr); - - DWORD bytes_written = 0; - if (!WriteFile(pipe, command.data(), static_cast(command.size()), - &bytes_written, nullptr)) { - std::string error = BuildErrorJson("pipe_write_failed", GetLastError()); - CloseHandle(pipe); - return error; - } - - char buffer[4096] = {0}; - DWORD bytes_read = 0; - if (!ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr)) { - std::string error = BuildErrorJson("pipe_read_failed", GetLastError()); - CloseHandle(pipe); - return error; - } - - CloseHandle(pipe); - return std::string(buffer, buffer + bytes_read); + return std::string(response.begin(), response.end()); } std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down, @@ -1079,6 +1016,8 @@ int CrossDeskServiceHost::InitializeRuntime() { return ERROR_INVALID_HANDLE; started_at_tick_ = GetTickCount64(); + session_state_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr); + if (!session_state_event_) return static_cast(GetLastError()); last_sas_tick_ = 0; active_session_id_ = WTSGetActiveConsoleSessionId(); process_session_id_ = 0xFFFFFFFF; @@ -1136,6 +1075,7 @@ int CrossDeskServiceHost::InitializeRuntime() { last_session_event_session_id_ = active_session_id_; RefreshSessionState(); EnsureSessionHelper(); + session_state_thread_ = std::thread(&CrossDeskServiceHost::SessionStateLoop, this); ipc_thread_ = std::thread(&CrossDeskServiceHost::IpcServerLoop, this); if (!console_mode_) { client_process_monitor_thread_ = @@ -1159,8 +1099,13 @@ void CrossDeskServiceHost::ShutdownRuntime() { if (ipc_thread_.joinable()) { ipc_thread_.join(); } + if (session_state_thread_.joinable()) session_state_thread_.join(); StopSecureInputHelper(); StopSessionHelper(); + if (session_state_event_) { + CloseHandle(session_state_event_); + session_state_event_ = nullptr; + } if (lifetime_mutex_) { ReleaseMutex(lifetime_mutex_); CloseHandle(lifetime_mutex_); @@ -1399,12 +1344,22 @@ void CrossDeskServiceHost::IpcServerLoop() { LOG_WARN("Pipe security initialization failed, error={}", GetLastError()); } + HANDLE pipe = INVALID_HANDLE_VALUE; + pipe_deadline_detail::Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)}; + if (!event.value) { + LOG_ERROR("CreateEventW for pipe failed, error={}", GetLastError()); + return; + } while (stop_event_ != nullptr && WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) { - HANDLE pipe = CreateNamedPipeW( - kCrossDeskServicePipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, - PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 4096, 4096, 0, - pipe_attributes); + // Keep the name registered between requests. Recreating the only instance + // made high-rate mouse clients observe FILE_NOT_FOUND and sleep on retries. + if (pipe == INVALID_HANDLE_VALUE) { + pipe = CreateNamedPipeW( + kCrossDeskServicePipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 4096, 4096, 0, + pipe_attributes); + } if (pipe == INVALID_HANDLE_VALUE) { DWORD error = GetLastError(); LOG_ERROR("CreateNamedPipeW failed, error={}", error); @@ -1413,12 +1368,8 @@ void CrossDeskServiceHost::IpcServerLoop() { } OVERLAPPED overlapped{}; - overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); - if (overlapped.hEvent == nullptr) { - LOG_ERROR("CreateEventW for pipe failed, error={}", GetLastError()); - CloseHandle(pipe); - break; - } + overlapped.hEvent = event.value; + ResetEvent(event.value); BOOL connected = ConnectNamedPipe(pipe, &overlapped); DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError(); @@ -1432,8 +1383,8 @@ void CrossDeskServiceHost::IpcServerLoop() { if (!connected && connect_error != ERROR_IO_PENDING) { LOG_WARN("ConnectNamedPipe failed, error={}", connect_error); - CloseHandle(overlapped.hEvent); CloseHandle(pipe); + pipe = INVALID_HANDLE_VALUE; continue; } @@ -1441,17 +1392,20 @@ void CrossDeskServiceHost::IpcServerLoop() { HANDLE wait_handles[] = {stop_event_, overlapped.hEvent}; DWORD wait_result = WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); - if (wait_result == WAIT_OBJECT_0) { + if (wait_result != WAIT_OBJECT_0 + 1) { CancelIoEx(pipe, &overlapped); - CloseHandle(overlapped.hEvent); - CloseHandle(pipe); + DWORD ignored = 0; + GetOverlappedResult(pipe, &overlapped, &ignored, TRUE); break; } } char buffer[1024] = {0}; DWORD bytes_read = 0; - if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) && + DWORD transfer_error = ERROR_SUCCESS; + if (pipe_deadline_detail::Transfer( + pipe, overlapped.hEvent, false, buffer, sizeof(buffer) - 1, + GetTickCount64() + 1000, &bytes_read, &transfer_error) && bytes_read > 0) { ULONG client_session_id = 0xFFFFFFFF; // Obtain identity from Windows, never from caller-supplied JSON. A GUI @@ -1462,43 +1416,80 @@ void CrossDeskServiceHost::IpcServerLoop() { client_session_id) : BuildErrorJson("client_session_query_failed", GetLastError()); DWORD bytes_written = 0; - WriteFile(pipe, response.data(), static_cast(response.size()), - &bytes_written, nullptr); - FlushFileBuffers(pipe); + if (pipe_deadline_detail::Transfer( + pipe, overlapped.hEvent, true, const_cast(response.data()), + static_cast(response.size()), GetTickCount64() + 1000, + &bytes_written, &transfer_error)) { + FlushFileBuffers(pipe); + } } DisconnectNamedPipe(pipe); - CloseHandle(overlapped.hEvent); - CloseHandle(pipe); } + if (pipe != INVALID_HANDLE_VALUE) CloseHandle(pipe); } void CrossDeskServiceHost::RefreshSessionState() { + // WTS/process enumeration can be slow. Never hold the snapshot lock while + // querying Windows: status and input requests only need the last sample. + const ULONGLONG probe_started = GetTickCount64(); + uint64_t generation = 0; + { + std::lock_guard lock(state_mutex_); + generation = session_state_generation_; + } + const DWORD session = WTSGetActiveConsoleSessionId(); + DWORD process_session = 0xFFFFFFFF; + ProcessIdToSessionId(GetCurrentProcessId(), &process_session); + const bool logon_ui = IsLogonUiRunningInSession(session); + const bool consent_ui = IsConsentUiRunningInSession(session); + const InputDesktopInfo desktop = GetInputDesktopInfo(); + std::wstring username; + const bool prelogin = !GetSessionUserName(session, &username) || username.empty(); + bool locked = false; + const bool lock_known = QuerySessionLockState(session, &locked); + std::lock_guard lock(state_mutex_); + if (generation != session_state_generation_ || + session != WTSGetActiveConsoleSessionId()) return; const DWORD previous_session_id = active_session_id_; - active_session_id_ = WTSGetActiveConsoleSessionId(); + active_session_id_ = session; if (active_session_id_ != previous_session_id) { + last_session_transition_tick_ = probe_started; + session_lock_state_known_ = false; ResetSessionHelperReportedStateLocked("session_changed", 0); sas_secure_desktop_until_tick_ = 0; sas_secure_desktop_seen_ = false; } - DWORD process_session_id = 0xFFFFFFFF; - if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) { - process_session_id_ = process_session_id; - } - logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_); - consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_); - InputDesktopInfo desktop_info = GetInputDesktopInfo(); - input_desktop_available_ = desktop_info.available; - input_desktop_error_code_ = desktop_info.error_code; - input_desktop_name_ = desktop_info.name; + process_session_id_ = process_session; + logon_ui_visible_ = logon_ui; + consent_ui_visible_ = consent_ui; + input_desktop_available_ = desktop.available; + input_desktop_error_code_ = desktop.error_code; + input_desktop_name_ = desktop.name; secure_desktop_active_ = _stricmp(input_desktop_name_.c_str(), "Winlogon") == 0; - std::wstring username; - bool username_available = GetSessionUserName(active_session_id_, &username); - prelogin_ = !username_available || username.empty(); - if (!QuerySessionLockState(active_session_id_, &session_locked_)) { + session_username_ = WideToUtf8(username); + prelogin_ = prelogin; + if (lock_known) { + if (session_lock_state_known_ && session_locked_ != locked) { + last_session_transition_tick_ = probe_started; + ResetSessionHelperReportedStateLocked("lock_state_changed", 0); + } + session_locked_ = locked; + session_lock_state_known_ = true; + } else if (last_session_event_session_id_ == session && + last_session_event_type_ == WTS_SESSION_LOCK) { + session_locked_ = true; + session_lock_state_known_ = true; + } else if (last_session_event_session_id_ == session && + (last_session_event_type_ == WTS_SESSION_UNLOCK || + last_session_event_type_ == WTS_SESSION_LOGON)) { + session_locked_ = false; + session_lock_state_known_ = true; + } else { + session_lock_state_known_ = false; session_locked_ = (logon_ui_visible_ || secure_desktop_active_) && !prelogin_; } @@ -1527,8 +1518,8 @@ void CrossDeskServiceHost::ResetSessionHelperReportedStateLocked( } bool CrossDeskServiceHost::GetEffectiveSessionLockedLocked() const { - return session_helper_status_ok_ ? session_helper_report_session_locked_ - : session_locked_; + return session_lock_state_known_ || !session_helper_status_ok_ + ? session_locked_ : session_helper_report_session_locked_; } bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const { @@ -1583,8 +1574,7 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked( return false; } - return HasSecureInputUiLocked() || (GetEffectiveSessionLockedLocked() && - IsHelperReportingLockScreenLocked()); + return HasSecureInputUiLocked() || GetEffectiveSessionLockedLocked(); } std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const { @@ -1601,6 +1591,10 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const { return session_helper_report_interactive_stage_; } + // A lock notification can precede LockApp's first helper sample. The SYSTEM + // helper binds the actual input desktop on each operation. + if (GetEffectiveSessionLockedLocked()) return "secure-desktop"; + const bool service_host_credential_ui_visible = IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_, consent_ui_visible_, @@ -2071,14 +2065,17 @@ void CrossDeskServiceHost::EnsureSessionHelper() { void CrossDeskServiceHost::RefreshSessionHelperReportedState() { DWORD target_session_id = 0xFFFFFFFF; bool helper_running = false; + uint64_t generation = 0; { std::lock_guard lock(state_mutex_); target_session_id = session_helper_session_id_; helper_running = session_helper_running_; + generation = session_state_generation_; } if (!helper_running || target_session_id == 0xFFFFFFFF) { std::lock_guard lock(state_mutex_); + if (generation != session_state_generation_) return; ResetSessionHelperReportedStateLocked("helper_not_running", 0); return; } @@ -2089,12 +2086,14 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() { Json json = Json::parse(response, nullptr, false); if (json.is_discarded()) { std::lock_guard lock(state_mutex_); + if (generation != session_state_generation_) return; ResetSessionHelperReportedStateLocked("invalid_helper_status_json", 0); return; } if (!json.value("ok", false)) { std::lock_guard lock(state_mutex_); + if (generation != session_state_generation_) return; const std::string error = json.value("error", std::string("helper_status_failed")); ResetSessionHelperReportedStateLocked( @@ -2103,6 +2102,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() { } std::lock_guard lock(state_mutex_); + if (generation != session_state_generation_) return; if (target_session_id != active_session_id_ || target_session_id != session_helper_session_id_ || json.value("session_id", static_cast(0xFFFFFFFF)) != @@ -2111,6 +2111,20 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() { ResetSessionHelperReportedStateLocked("helper_session_mismatch", 0); return; } + if (!IsCurrentDesktopSample( + json.value("sample_started_tick", uint64_t{0}), + last_session_transition_tick_, session_lock_state_known_, + session_locked_, json.value("session_locked", false))) { + if (session_helper_status_error_ != "stale_helper_state") { + LOG_DEBUG("Ignoring stale helper desktop sample: sample_tick={}, " + "transition_tick={}, helper_locked={}, session_locked={}", + json.value("sample_started_tick", uint64_t{0}), + last_session_transition_tick_, json.value("session_locked", false), + session_locked_); + } + ResetSessionHelperReportedStateLocked("stale_helper_state", 0); + return; + } session_helper_status_ok_ = true; session_helper_status_error_.clear(); session_helper_status_error_code_ = 0; @@ -2152,6 +2166,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, { std::lock_guard lock(state_mutex_); last_session_event_type_ = event_type; + ++session_state_generation_; last_session_event_session_id_ = session_id; const DWORD previous_session_id = active_session_id_; active_session_id_ = WTSGetActiveConsoleSessionId(); @@ -2160,42 +2175,26 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, sas_secure_desktop_until_tick_ = 0; sas_secure_desktop_seen_ = false; } - DWORD process_session_id = 0xFFFFFFFF; - if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) { - process_session_id_ = process_session_id; - } - logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_); - consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_); - InputDesktopInfo desktop_info = GetInputDesktopInfo(); - input_desktop_available_ = desktop_info.available; - input_desktop_error_code_ = desktop_info.error_code; - input_desktop_name_ = desktop_info.name; - secure_desktop_active_ = - _stricmp(input_desktop_name_.c_str(), "Winlogon") == 0; - - std::wstring username; - bool username_available = GetSessionUserName(active_session_id_, &username); - prelogin_ = !username_available || username.empty(); - - if (!QuerySessionLockState(active_session_id_, &session_locked_)) { - if (session_id == active_session_id_ && event_type == WTS_SESSION_LOCK) { + if (session_id == active_session_id_) { + // Do not let an older helper sample override a session notification. + ResetSessionHelperReportedStateLocked("session_event", 0); + last_session_transition_tick_ = GetTickCount64(); + if (event_type == WTS_SESSION_LOCK) { session_locked_ = true; - } else if (session_id == active_session_id_ && - (event_type == WTS_SESSION_UNLOCK || - event_type == WTS_SESSION_LOGON)) { + session_lock_state_known_ = true; + } + if (event_type == WTS_SESSION_UNLOCK || event_type == WTS_SESSION_LOGON) { session_locked_ = false; - } else if (logon_ui_visible_ || secure_desktop_active_) { - session_locked_ = !prelogin_; + session_lock_state_known_ = true; + sas_secure_desktop_until_tick_ = 0; + sas_secure_desktop_seen_ = false; } } } LOG_INFO("Session event: type={}, session_id={}, active_session_id={}", - SessionEventToString(event_type), session_id, active_session_id_); - EnsureSessionHelper(); - if (!secure_desktop_active_ && !logon_ui_visible_ && !consent_ui_visible_) { - StopSecureInputHelper(); - } + SessionEventToString(event_type), session_id, WTSGetActiveConsoleSessionId()); + WakeSessionState(); } std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command, @@ -2228,17 +2227,22 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command, return BuildErrorJson("unknown_command"); } -std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) { +void CrossDeskServiceHost::WakeSessionState() { + if (session_state_event_) SetEvent(session_state_event_); +} + +void CrossDeskServiceHost::SessionStateLoop() { + HANDLE events[] = {stop_event_, session_state_event_}; + while (WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) { + RefreshInteractiveState(); + if (WaitForMultipleObjects(2, events, FALSE, 250) == WAIT_OBJECT_0) break; + } +} + +void CrossDeskServiceHost::RefreshInteractiveState() { ReapSecureInputHelper(); ReapSessionHelper(); RefreshSessionState(); - { - std::lock_guard lock(state_mutex_); - if (client_session_id == 0xFFFFFFFF || - active_session_id_ != client_session_id) { - return BuildErrorJson("service_session_mismatch"); - } - } EnsureSessionHelper(); RefreshSessionHelperReportedState(); bool keep_secure_input_helper = false; @@ -2257,11 +2261,7 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) { launch_secure_input_helper = keep_secure_input_helper && (!secure_input_helper_running_ || - secure_input_helper_session_id_ != secure_input_target_session_id || - secure_input_helper_interactive_stage_ != - secure_input_interactive_stage || - secure_input_helper_interactive_desktop_ != - secure_input_interactive_desktop); + secure_input_helper_session_id_ != secure_input_target_session_id); } if (keep_secure_input_helper) { @@ -2274,14 +2274,16 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) { } else { StopSecureInputHelper(); } +} +std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) { std::lock_guard lock(state_mutex_); - if (active_session_id_ != client_session_id) { + if (client_session_id == 0xFFFFFFFF || + active_session_id_ != client_session_id || + client_session_id != WTSGetActiveConsoleSessionId()) { return BuildErrorJson("service_session_mismatch"); } - std::wstring username; - GetSessionUserName(active_session_id_, &username); - std::string username_utf8 = EscapeJsonString(WideToUtf8(username)); + std::string username_utf8 = EscapeJsonString(session_username_); std::string input_desktop = EscapeJsonString(input_desktop_name_); std::string last_sas_error = EscapeJsonString(last_sas_error_); std::string session_helper_last_error = @@ -2467,12 +2469,11 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) { std::string CrossDeskServiceHost::SendSecureAttentionSequence( DWORD client_session_id) { - RefreshSessionState(); if (client_session_id == 0xFFFFFFFF || client_session_id != WTSGetActiveConsoleSessionId()) { return BuildErrorJson("service_session_mismatch"); } - LOG_INFO("Received SAS request for session_id={}", active_session_id_); + LOG_INFO("Received SAS request for session_id={}", client_session_id); SasResult result = SendSasNow(); { std::lock_guard lock(state_mutex_); @@ -2489,114 +2490,68 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence( if (!result.success) { return BuildErrorJson(result.error.c_str(), result.error_code); } + WakeSessionState(); return "{\"ok\":true,\"sent\":\"sas\"}"; } +std::string CrossDeskServiceHost::ResolveSecureInputTarget( + DWORD client_session_id, SecureInputTarget& target) { + bool helper_running = false; + bool can_inject = false; + { + std::lock_guard lock(state_mutex_); + target.session_id = active_session_id_; + target.stage = ResolveInteractiveStageLocked(); + target.desktop = ResolveInteractiveDesktopLocked(target.stage); + helper_running = secure_input_helper_running_ && + secure_input_helper_session_id_ == target.session_id; + can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked(); + } + + if (target.session_id == 0xFFFFFFFF) { + return BuildErrorJson("no_active_console_session"); + } + if (target.session_id != client_session_id || + client_session_id != WTSGetActiveConsoleSessionId()) { + return BuildErrorJson("service_session_mismatch"); + } + if (!can_inject) { + WakeSessionState(); + return BuildErrorJson("secure_input_not_active"); + } + + if (!helper_running) { + WakeSessionState(); + return BuildErrorJson("secure_input_helper_not_ready", ERROR_NOT_READY); + } + return {}; +} + std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput( DWORD client_session_id, int key_code, bool is_down, uint32_t scan_code, bool extended) { - RefreshSessionState(); - ReapSecureInputHelper(); - EnsureSessionHelper(); - RefreshSessionHelperReportedState(); - - DWORD target_session_id = 0xFFFFFFFF; - bool helper_running = false; - bool can_inject = false; - std::string interactive_stage; - std::string interactive_desktop; - { - std::lock_guard lock(state_mutex_); - target_session_id = active_session_id_; - interactive_stage = ResolveInteractiveStageLocked(); - interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage); - const bool helper_stage_matches = - secure_input_helper_interactive_stage_ == interactive_stage && - secure_input_helper_interactive_desktop_ == interactive_desktop; - helper_running = secure_input_helper_running_ && - secure_input_helper_session_id_ == target_session_id && - helper_stage_matches; - can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked(); - } - - if (target_session_id == 0xFFFFFFFF) { - return BuildErrorJson("no_active_console_session"); - } - if (target_session_id != client_session_id) { - return BuildErrorJson("service_session_mismatch"); - } - if (!can_inject) { - return BuildErrorJson("secure_input_not_active"); - } - - if (!helper_running) { - StopSecureInputHelper(); - if (!LaunchSecureInputHelper(target_session_id, interactive_stage, - interactive_desktop)) { - std::lock_guard lock(state_mutex_); - return BuildErrorJson(secure_input_helper_last_error_.c_str(), - secure_input_helper_last_error_code_); - } - } + SecureInputTarget target; + const auto error = ResolveSecureInputTarget(client_session_id, target); + if (!error.empty()) return error; return QueryNamedPipeMessage( - GetCrossDeskSecureInputHelperPipeName(target_session_id), + GetCrossDeskSecureInputHelperPipeName(target.session_id), BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code, - extended, interactive_stage, - interactive_desktop), + extended, target.stage, + target.desktop), 1000); } std::string CrossDeskServiceHost::SendSecureDesktopMouseInput( DWORD client_session_id, int x, int y, int wheel, int flag) { - RefreshSessionState(); - ReapSecureInputHelper(); - EnsureSessionHelper(); - RefreshSessionHelperReportedState(); - - DWORD target_session_id = 0xFFFFFFFF; - bool helper_running = false; - bool can_inject = false; - std::string interactive_stage; - std::string interactive_desktop; - { - std::lock_guard lock(state_mutex_); - target_session_id = active_session_id_; - interactive_stage = ResolveInteractiveStageLocked(); - interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage); - const bool helper_stage_matches = - secure_input_helper_interactive_stage_ == interactive_stage && - secure_input_helper_interactive_desktop_ == interactive_desktop; - helper_running = secure_input_helper_running_ && - secure_input_helper_session_id_ == target_session_id && - helper_stage_matches; - can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked(); - } - - if (target_session_id == 0xFFFFFFFF) { - return BuildErrorJson("no_active_console_session"); - } - if (target_session_id != client_session_id) { - return BuildErrorJson("service_session_mismatch"); - } - if (!can_inject) { - return BuildErrorJson("secure_input_not_active"); - } - - if (!helper_running) { - StopSecureInputHelper(); - if (!LaunchSecureInputHelper(target_session_id, interactive_stage, - interactive_desktop)) { - std::lock_guard lock(state_mutex_); - return BuildErrorJson(secure_input_helper_last_error_.c_str(), - secure_input_helper_last_error_code_); - } - } + SecureInputTarget target; + const auto error = ResolveSecureInputTarget(client_session_id, target); + if (!error.empty()) return error; return QueryNamedPipeMessage( - GetCrossDeskSecureInputHelperPipeName(target_session_id), - BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage, - interactive_desktop), + GetCrossDeskSecureInputHelperPipeName(target.session_id), + BuildSecureInputHelperMouseCommand(x, y, wheel, flag, target.stage, + target.desktop), 1000); } @@ -2831,18 +2786,40 @@ std::string QueryCrossDeskService(const std::string& command, return QueryNamedPipeMessage(kCrossDeskServicePipeName, command, timeout_ms); } +namespace { +std::string QueryDesktopInput(const std::string& command, DWORD timeout_ms) { + const ULONGLONG deadline = GetTickCount64() + timeout_ms; + for (;;) { + const auto now = GetTickCount64(); + if (now >= deadline) return BuildErrorJson("secure_input_helper_not_ready", ERROR_NOT_READY); + const std::string response = QueryCrossDeskService( + command, static_cast(deadline - now)); + const auto json = Json::parse(response, nullptr, false); + if (!json.is_object() || json.value("ok", false)) return response; + const std::string error = json.value("error", ""); + // Retry only explicit pre-injection rejections during helper startup. + // Never replay an uncertain timeout or a partially injected mouse action. + if (!IsDesktopInputSetupPending(error) || IsCurrentSessionUserDesktopActive()) + return response; + const auto remaining = pipe_deadline_detail::Remaining(deadline); + if (remaining == 0) return response; + Sleep((std::min)(DWORD{10}, remaining)); + } +} +} // namespace + std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down, uint32_t scan_code, bool extended, DWORD timeout_ms) { - return QueryCrossDeskService(BuildSecureDesktopKeyboardIpcCommand( + return QueryDesktopInput(BuildSecureDesktopKeyboardIpcCommand( key_code, is_down, scan_code, extended), timeout_ms); } std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel, int flag, DWORD timeout_ms) { - return QueryCrossDeskService( + return QueryDesktopInput( BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms); } diff --git a/apps/desktop/src/platform/windows/service/service_host.h b/apps/desktop/src/platform/windows/service/service_host.h index 7265b646..3f549546 100644 --- a/apps/desktop/src/platform/windows/service/service_host.h +++ b/apps/desktop/src/platform/windows/service/service_host.h @@ -41,6 +41,9 @@ class CrossDeskServiceHost { void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code, DWORD wait_hint); void IpcServerLoop(); + void SessionStateLoop(); + void RefreshInteractiveState(); + void WakeSessionState(); void RefreshSessionState(); void EnsureSessionHelper(); void ReapSessionHelper(); @@ -72,6 +75,14 @@ class CrossDeskServiceHost { DWORD client_session_id); std::string BuildStatusResponse(DWORD client_session_id); std::string SendSecureAttentionSequence(DWORD client_session_id); + struct SecureInputTarget { + DWORD session_id = 0xFFFFFFFF; + std::string stage; + std::string desktop; + }; + // Returns an error response, or an empty string with a validated snapshot. + std::string ResolveSecureInputTarget(DWORD client_session_id, + SecureInputTarget& target); std::string SendSecureDesktopKeyboardInput(DWORD client_session_id, int key_code, bool is_down, uint32_t scan_code = 0, @@ -89,6 +100,8 @@ class CrossDeskServiceHost { SERVICE_STATUS service_status_{}; HANDLE stop_event_ = nullptr; std::thread ipc_thread_; + std::thread session_state_thread_; + HANDLE session_state_event_ = nullptr; std::thread client_process_monitor_thread_; // Owned exclusively by the monitor, then drained after it joins. HANDLE unattended_process_ = nullptr; @@ -96,6 +109,9 @@ class CrossDeskServiceHost { DWORD unattended_session_ = 0xFFFFFFFF; std::atomic unattended_logoff_generation_{0}; std::mutex state_mutex_; + uint64_t session_state_generation_ = 0; + ULONGLONG last_session_transition_tick_ = 0; + bool session_lock_state_known_ = false; HANDLE lifetime_mutex_ = nullptr; std::wstring lifetime_mutex_name_; DWORD active_session_id_ = 0xFFFFFFFF; @@ -148,6 +164,7 @@ class CrossDeskServiceHost { HANDLE secure_input_helper_process_handle_ = nullptr; HANDLE secure_input_helper_stop_event_ = nullptr; std::string input_desktop_name_; + std::string session_username_; std::string last_sas_error_; std::string session_helper_last_error_; std::string session_helper_status_error_; diff --git a/apps/desktop/src/platform/windows/service/service_query_worker.h b/apps/desktop/src/platform/windows/service/service_query_worker.h new file mode 100644 index 00000000..67bbbadc --- /dev/null +++ b/apps/desktop/src/platform/windows/service/service_query_worker.h @@ -0,0 +1,113 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SERVICE_QUERY_WORKER_H_ +#define _SERVICE_QUERY_WORKER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace crossdesk { + +struct ServiceQueryResult { + std::string command; + std::string response; + bool user_desktop_recovered = false; +}; + +// One bounded query at a time. Reset invalidates in-flight results without +// joining the worker on the UI thread; only destruction waits for completion. +class ServiceQueryWorker { + public: + using Query = std::function; + explicit ServiceQueryWorker(Query query) + : thread_([this, query = std::move(query)] { + std::unique_lock lock(mutex_); + while (true) { + wake_.wait(lock, [this] { + return stopping_ || (!result_ && (sas_ || status_)); + }); + if (stopping_) return; + const std::string command = sas_ ? "sas" : "status"; + (sas_ ? sas_ : status_) = false; + const uint64_t generation = generation_; + running_command_ = command; + running_generation_ = generation; + lock.unlock(); + ServiceQueryResult result; + try { + result = query(command); + } catch (...) { + result.response = R"({"ok":false,"error":"service_query_failed"})"; + } + result.command = command; + lock.lock(); + running_command_.clear(); + if (generation == generation_) result_ = std::move(result); + } + }) {} + + ~ServiceQueryWorker() { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + wake_.notify_one(); + thread_.join(); + } + ServiceQueryWorker(const ServiceQueryWorker&) = delete; + ServiceQueryWorker& operator=(const ServiceQueryWorker&) = delete; + + void RequestStatus() { + std::lock_guard lock(mutex_); + if ((running_command_ == "status" && running_generation_ == generation_) || + result_ || sas_) return; + status_ = true; + wake_.notify_one(); + } + + void RequestSas() { + std::lock_guard lock(mutex_); + // A pre-SAS status must not overwrite the optimistic secure-desktop state. + ++generation_; + status_ = false; + result_.reset(); + sas_ = true; + wake_.notify_one(); + } + + std::optional Take() { + std::lock_guard lock(mutex_); + auto result = std::exchange(result_, std::nullopt); + wake_.notify_one(); + return result; + } + + void Reset() { + std::lock_guard lock(mutex_); + ++generation_; + status_ = sas_ = false; + result_.reset(); + } + + private: + std::mutex mutex_; + std::condition_variable wake_; + bool stopping_ = false, status_ = false, sas_ = false; + uint64_t generation_ = 0, running_generation_ = 0; + std::string running_command_; + std::optional result_; + std::thread thread_; +}; + +} // namespace crossdesk +#endif // _SERVICE_QUERY_WORKER_H_ diff --git a/apps/desktop/src/platform/windows/service/session_helper_main.cpp b/apps/desktop/src/platform/windows/service/session_helper_main.cpp index f0fdbdf8..848f37be 100644 --- a/apps/desktop/src/platform/windows/service/session_helper_main.cpp +++ b/apps/desktop/src/platform/windows/service/session_helper_main.cpp @@ -23,16 +23,24 @@ #include #include "interactive_desktop.h" +#include "named_pipe_deadline.h" #include "path_manager.h" +#include "platform/path_backend.h" #include "rd_log.h" #include "session_helper_shared.h" +#include "secure_desktop_dxgi_capture.h" +#include "secure_desktop_frame_pacer.h" +#include "secure_desktop_nv12_cache.h" #include "service_lifetime.h" +#include "shared_capture_lifecycle.h" +#include "shared_capture_configuration.h" #include "usbmmidd_policy.h" #include "usbmmidd_virtual_display.h" #include "windows_cursor_state.h" #include "cursor_draw.h" #include "windows_input_marker.h" #include "windows_key_metadata.h" +#include "../windows_thread_dpi.h" namespace { @@ -67,13 +75,7 @@ struct HelperState { ULONGLONG last_update_tick = 0; }; -struct SecureCaptureRequest { - int left = 0; - int top = 0; - int width = 0; - int height = 0; - bool show_cursor = true; - int fps = 30; +struct SecureCaptureRequest : crossdesk::SharedCaptureConfiguration { std::string interactive_stage; std::string interactive_desktop; }; @@ -87,21 +89,15 @@ struct SecureMouseRequest { std::string interactive_desktop; }; -struct SecureCaptureBuffers { - std::vector nv12_frame; -}; - struct SecureSharedCaptureState { - std::mutex mutex; - std::thread capture_thread; - std::atomic stop_requested{false}; DWORD session_id = 0xFFFFFFFF; SecureCaptureRequest request; HANDLE frame_mapping = nullptr; HANDLE frame_ready_event = nullptr; uint8_t* frame_view = nullptr; - size_t frame_view_size = 0; uint32_t sequence = 0; + // Last member: joins/cleans the producer before any captured state is destroyed. + crossdesk::SharedCaptureLifecycle lifecycle; }; struct IpcSecurityAttributes { @@ -136,12 +132,23 @@ void InitializeHelperLogger() { static std::once_flag once_flag; std::call_once(once_flag, []() { crossdesk::PathManager path_manager("CrossDesk"); - std::filesystem::path log_path = - path_manager.GetLogPath() / "session_helper"; - if (!log_path.empty() && path_manager.CreateDirectories(log_path)) { - InitLogger(log_path.string()); - return; - } + const auto initialize = [&](const std::filesystem::path& log_path) { + if (log_path.empty() || !path_manager.CreateDirectories(log_path)) return false; + try { + InitLogger(log_path.string()); + // Creating a directory does not prove its existing log is writable. + // Force lazy sink creation here so an access failure can be recovered. + get_logger(); + return true; + } catch (const spdlog::spdlog_ex&) { + return false; + } + }; + if (initialize(path_manager.GetLogPath() / "session_helper")) return; + // The ordinary session helper uses the user's token. A portable helper + // installed in Program Files cannot append to a SYSTEM-owned capture log. + if (initialize(crossdesk::platform::GetLogPath("CrossDesk") / + "session_helper")) return; InitLogger("logs/session_helper"); }); } @@ -381,15 +388,16 @@ void UpdateHelperState(HelperState* helper_state) { return; } - InputDesktopInfo desktop_info = GetInputDesktopInfo(); + const ULONGLONG sample_started = GetTickCount64(); bool lock_app_process_running = IsLockAppRunningInCurrentSession(helper_state->session_id); bool logon_ui_visible = IsLogonUiRunningInCurrentSession(helper_state->session_id); bool consent_ui_visible = IsConsentUiRunningInCurrentSession(helper_state->session_id); - const bool consent_on_input_desktop = - desktop_info.available && consent_ui_visible; + // Process enumeration can straddle an unlock. Observe the input desktop + // afterwards, and expose the sample's start time for service-side rejection. + InputDesktopInfo desktop_info = GetInputDesktopInfo(); const bool input_desktop_is_winlogon = _stricmp(desktop_info.name.c_str(), "Winlogon") == 0; const bool inaccessible_secure_input_desktop = @@ -411,10 +419,9 @@ void UpdateHelperState(HelperState* helper_state) { helper_state->input_desktop_name = desktop_info.name; helper_state->lock_app_visible = lock_app_visible; helper_state->logon_ui_visible = logon_ui_visible; - helper_state->consent_ui_visible = - consent_on_input_desktop || consent_ui_visible; + helper_state->consent_ui_visible = consent_ui_visible; helper_state->secure_desktop_active = secure_desktop_active; - helper_state->last_update_tick = GetTickCount64(); + helper_state->last_update_tick = sample_started; } std::string BuildHelperStatusResponse(HelperState* helper_state) { @@ -435,6 +442,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) { json["session_id"] = helper_state->session_id; json["process_id"] = helper_state->process_id; json["session_locked"] = helper_state->session_locked; + json["sample_started_tick"] = helper_state->last_update_tick; json["input_desktop_available"] = helper_state->input_desktop_available; json["input_desktop_error_code"] = helper_state->input_desktop_error_code; json["input_desktop"] = helper_state->input_desktop_name; @@ -1329,9 +1337,9 @@ std::vector BuildTextResponseBytes(const std::string& response) { return std::vector(response.begin(), response.end()); } -// Own the desktop before its GDI objects, and release those objects before -// restoring the original desktop. Both capture paths use the same lifecycle. -struct SecureDesktopGdiResources { +// Own the desktop before its capture objects, and release those objects before +// restoring the original desktop. The legacy pipe path remains GDI-only. +struct SecureDesktopCaptureResources { ScopedInteractiveDesktop desktop; HDC screen_dc = nullptr; HDC mem_dc = nullptr; @@ -1341,8 +1349,16 @@ struct SecureDesktopGdiResources { const char* error = nullptr; DWORD error_code = ERROR_SUCCESS; crossdesk::SecureDesktopCursorSnapshot cursor_snapshot{}; + std::unique_ptr dxgi; + ULONGLONG next_dxgi_attempt = 0; + ULONGLONG dxgi_started = 0; + bool using_dxgi = false; + bool frame_pending = false; + const uint8_t* frame_bits = nullptr; + int frame_stride = 0; + crossdesk::SecureDesktopNv12Cache nv12; - ~SecureDesktopGdiResources() { + ~SecureDesktopCaptureResources() { if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) { SelectObject(mem_dc, old_bitmap); } @@ -1364,6 +1380,15 @@ struct SecureDesktopGdiResources { if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) { return Fail("switch_interactive_desktop_failed"); } + if (!desktop.IsReceivingInput()) { + SetLastError(ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION); + return Fail("inactive_capture_desktop"); + } + return true; + } + + bool InitializeGdi(const SecureCaptureRequest& request) { + if (bits) return true; screen_dc = GetDC(nullptr); if (screen_dc == nullptr) return Fail("get_dc_failed"); mem_dc = CreateCompatibleDC(screen_dc); @@ -1384,11 +1409,50 @@ struct SecureDesktopGdiResources { return true; } - bool Capture(const SecureCaptureRequest& request) { - if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc, - request.left, request.top, SRCCOPY | CAPTUREBLT)) { - return Fail("bitblt_failed"); + bool Capture(const SecureCaptureRequest& request, bool allow_dxgi = false) { + frame_pending = false; + const ULONGLONG now = GetTickCount64(); + if (allow_dxgi && !dxgi && now >= next_dxgi_attempt) { + next_dxgi_attempt = now + 5000; + auto candidate = std::make_unique(); + const HRESULT hr = candidate->Initialize( + request.left, request.top, request.width, request.height); + if (SUCCEEDED(hr)) { + candidate->SetSoftwareCursor(crossdesk::IsUsbmmiddDisplayDevice( + candidate->output().DeviceName)); + dxgi = std::move(candidate); + dxgi_started = now; + } else { + LOG_INFO("Secure capture DXGI unavailable; using GDI, hr={}", + static_cast(hr)); + } } + using_dxgi = false; + if (dxgi) { + const HRESULT hr = dxgi->Capture(); + using_dxgi = hr == S_OK; + if (FAILED(hr) || (hr == S_FALSE && now - dxgi_started >= 250)) { + LOG_INFO("Secure capture DXGI lost; using GDI, hr={}", + static_cast(hr)); + dxgi.reset(); + next_dxgi_attempt = now + 5000; + } else if (hr == S_FALSE) { + // DuplicateOutput often succeeds before the first desktop image is + // ready. A synchronous BitBlt here can stall the first frame for a + // desktop transition. Retry DXGI at the requested cadence instead. + frame_pending = true; + return false; + } + } + if (!using_dxgi) { + if (!InitializeGdi(request)) return false; + if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc, + request.left, request.top, SRCCOPY | CAPTUREBLT)) { + return Fail("bitblt_failed"); + } + } + frame_bits = using_dxgi ? dxgi->pixels() : static_cast(bits); + frame_stride = using_dxgi ? dxgi->stride() : request.width * 4; // Sample on the thread bound to the captured input desktop, including when // native controllers render the cursor themselves (show_cursor == false). cursor_snapshot = {}; @@ -1397,7 +1461,8 @@ struct SecureDesktopGdiResources { if (GetCursorInfo(&cursor)) { static thread_local crossdesk::WindowsCursorState cursor_state; crossdesk::CursorState state{}; - cursor_state.Sample(cursor, false, &state); + const bool embedded = using_dxgi && dxgi->CursorEmbedded(cursor); + cursor_state.Sample(cursor, embedded, &state); cursor_snapshot.valid = 1; cursor_snapshot.visible = state.visible; cursor_snapshot.shape = static_cast(state.shape); @@ -1405,28 +1470,38 @@ struct SecureDesktopGdiResources { cursor_snapshot.hidden_reason = static_cast(state.hidden_reason); cursor_snapshot.x = cursor.ptScreenPos.x; cursor_snapshot.y = cursor.ptScreenPos.y; - if (request.show_cursor && crossdesk::DrawCursorInCapture( - mem_dc, cursor, request.left, request.top, - request.width, request.height)) { + // Never draw into DXGI's cached desktop: each cursor position starts + // with clean pixels, including on a static screen or after a toggle. + if (request.show_cursor && !embedded && using_dxgi && + (cursor.flags & CURSOR_SHOWING) != 0) { + if (!InitializeGdi(request)) return false; + const size_t row_bytes = static_cast(request.width) * 4; + for (int y = 0; y < request.height; ++y) { + std::memcpy(static_cast(bits) + y * row_bytes, + frame_bits + static_cast(y) * frame_stride, + row_bytes); + } + frame_bits = static_cast(bits); + frame_stride = request.width * 4; + } + if (request.show_cursor && !embedded && mem_dc && + crossdesk::DrawCursorInCapture(mem_dc, cursor, request.left, + request.top, request.width, + request.height)) { cursor_snapshot.visible = 0; cursor_snapshot.render_mode = static_cast(crossdesk::CursorRenderMode::embedded); } } // Complete GDI writes before libyuv reads the DIB's pixel memory. - GdiFlush(); + if (!using_dxgi || frame_bits == bits) GdiFlush(); return true; } }; std::vector CaptureSecureDesktopFrame( - const SecureCaptureRequest& request, - SecureCaptureBuffers* capture_buffers) { - if (capture_buffers == nullptr) { - return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers")); - } - - SecureDesktopGdiResources capture; + const SecureCaptureRequest& request) { + SecureDesktopCaptureResources capture; if (!capture.Initialize(request) || !capture.Capture(request)) { return BuildTextResponseBytes( BuildErrorJson(capture.error, capture.error_code)); @@ -1434,40 +1509,34 @@ std::vector CaptureSecureDesktopFrame( const size_t nv12_size = static_cast(request.width) * request.height * 3 / 2; - capture_buffers->nv12_frame.resize(nv12_size); + crossdesk::CrossDeskSecureDesktopFrameHeader header{}; + // Each pipe client handles one command, so an intermediate capture buffer + // cannot be reused. Convert straight into the response payload. + std::vector response(sizeof(header) + nv12_size); + uint8_t* payload = response.data() + sizeof(header); const int convert_result = libyuv::ARGBToNV12( - static_cast(capture.bits), request.width * 4, - capture_buffers->nv12_frame.data(), request.width, - capture_buffers->nv12_frame.data() + request.width * request.height, + capture.frame_bits, capture.frame_stride, payload, request.width, + payload + static_cast(request.width) * request.height, request.width, request.width, request.height); if (convert_result != 0) { return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed")); } - crossdesk::CrossDeskSecureDesktopFrameHeader header{}; header.magic = crossdesk::kCrossDeskSecureDesktopFrameMagic; header.version = crossdesk::kCrossDeskSecureDesktopFrameVersion; header.left = request.left; header.top = request.top; header.width = static_cast(request.width); header.height = static_cast(request.height); - header.payload_size = - static_cast(capture_buffers->nv12_frame.size()); + header.payload_size = static_cast(nv12_size); header.cursor = capture.cursor_snapshot; - std::vector response(sizeof(header) + - capture_buffers->nv12_frame.size()); std::memcpy(response.data(), &header, sizeof(header)); - if (!capture_buffers->nv12_frame.empty()) { - std::memcpy(response.data() + sizeof(header), - capture_buffers->nv12_frame.data(), - capture_buffers->nv12_frame.size()); - } return response; } -void CloseSecureDesktopSharedCaptureResourcesLocked( +void CloseSecureDesktopSharedCaptureResources( SecureSharedCaptureState* capture_state) { if (capture_state == nullptr) { return; @@ -1485,7 +1554,7 @@ void CloseSecureDesktopSharedCaptureResourcesLocked( CloseHandle(capture_state->frame_mapping); capture_state->frame_mapping = nullptr; } - capture_state->frame_view_size = 0; + capture_state->sequence = 0; } void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { @@ -1493,28 +1562,22 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { return; } - SecureCaptureRequest request; - uint8_t* frame_view = nullptr; - { - std::lock_guard lock(capture_state->mutex); - request = capture_state->request; - frame_view = capture_state->frame_view; - } + // Start publishes these fields before launching us. Lifecycle control keeps + // the request, mapping and event alive and unchanged until this thread joins. + const SecureCaptureRequest request = capture_state->request; + uint8_t* const frame_view = capture_state->frame_view; if (frame_view == nullptr || request.width <= 0 || request.height <= 0) { return; } - const int interval_ms = - request.fps > 0 ? (std::max)(1, 1000 / request.fps) : 33; - const size_t nv12_size = - static_cast(request.width) * request.height * 3 / 2; - std::vector nv12_frame(nv12_size); - - std::unique_ptr capture; + crossdesk::ScopedWindowsPhysicalCoordinates physical_coordinates; + crossdesk::SecureDesktopFramePacer pacer(request.fps); + std::unique_ptr capture; ULONGLONG last_error_tick = 0; auto stats_started = std::chrono::steady_clock::now(); - unsigned frame_count = 0; + unsigned frame_count = 0, dxgi_frames = 0, dxgi_updates = 0; + unsigned converted_frames = 0; double capture_ms = 0, convert_ms = 0, publish_ms = 0; auto report_stats = [&](bool force) { const auto elapsed = std::chrono::duration( @@ -1524,16 +1587,20 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { if (frame_count > 0) { LOG_INFO( "Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, " - "convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}", + "convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}, " + "target_fps={}, dxgi_frames={}, dxgi_updates={}, gdi_frames={}, " + "converted_frames={}, reused_frames={}", frame_count / elapsed, capture_ms / frame_count, convert_ms / frame_count, publish_ms / frame_count, request.width, - request.height); + request.height, request.fps, dxgi_frames, dxgi_updates, + frame_count - dxgi_frames, converted_frames, + frame_count - converted_frames); } stats_started = std::chrono::steady_clock::now(); - frame_count = 0; + frame_count = dxgi_frames = dxgi_updates = converted_frames = 0; capture_ms = convert_ms = publish_ms = 0; }; - while (!capture_state->stop_requested.load(std::memory_order_relaxed)) { + while (!capture_state->lifecycle.StopRequested()) { const auto frame_started = std::chrono::steady_clock::now(); report_stats(false); // A lock/credential/UAC transition can change the desktop before the @@ -1542,7 +1609,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { capture.reset(); } if (!capture) { - capture = std::make_unique(); + capture = std::make_unique(); if (!capture->Initialize(request)) { const DWORD error = capture->error_code; capture.reset(); @@ -1559,14 +1626,17 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { } } - if (capture->Capture(request)) { + if (capture->Capture(request, true)) { + if (capture_state->lifecycle.StopRequested()) break; const auto captured_at = std::chrono::steady_clock::now(); - const int convert_result = libyuv::ARGBToNV12( - static_cast(capture->bits), request.width * 4, - nv12_frame.data(), request.width, - nv12_frame.data() + request.width * request.height, request.width, - request.width, request.height); - if (convert_result == 0) { + // Composited/animated cursors and GDI frames always need conversion. + // Native controllers use independent cursor metadata, so a DXGI timeout + // or pointer-only update can reuse the NV12 image safely. + const bool pixels_changed = !capture->using_dxgi || + capture->dxgi->updated() || request.show_cursor; + if (capture->nv12.Convert(capture->frame_bits, capture->frame_stride, + request.width, request.height, pixels_changed)) { + const auto& nv12_frame = capture->nv12.pixels(); const auto converted_at = std::chrono::steady_clock::now(); auto* header = reinterpret_cast< crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view); @@ -1581,12 +1651,19 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { header->height = static_cast(request.height); header->payload_size = static_cast(nv12_frame.size()); header->cursor = capture->cursor_snapshot; - std::memcpy(payload, nv12_frame.data(), nv12_frame.size()); + if (capture->nv12.updated()) { + std::memcpy(payload, nv12_frame.data(), nv12_frame.size()); + ++converted_frames; + } header->sequence = ++capture_state->sequence; MemoryBarrier(); header->writing = 0; SetEvent(capture_state->frame_ready_event); ++frame_count; + if (capture->using_dxgi) { + ++dxgi_frames; + if (capture->dxgi->updated()) ++dxgi_updates; + } capture_ms += std::chrono::duration(captured_at - frame_started) .count(); @@ -1597,7 +1674,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { std::chrono::steady_clock::now() - converted_at) .count(); } - } else { + } else if (!capture->frame_pending) { const DWORD error = capture->error_code; capture.reset(); const ULONGLONG now = GetTickCount64(); @@ -1610,14 +1687,10 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { } } - const auto elapsed_ms = - std::chrono::duration_cast( - std::chrono::steady_clock::now() - frame_started) - .count(); - const int retry_interval_ms = capture ? interval_ms : 100; - if (elapsed_ms < retry_interval_ms) { - std::this_thread::sleep_for( - std::chrono::milliseconds(retry_interval_ms - elapsed_ms)); + if (capture) { + pacer.Wait(frame_started); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } report_stats(true); @@ -1629,41 +1702,18 @@ std::vector StopSecureDesktopSharedCapture( return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state")); } - std::thread thread_to_join; - { - std::lock_guard lock(capture_state->mutex); - capture_state->stop_requested.store(true, std::memory_order_relaxed); - if (capture_state->frame_ready_event != nullptr) { - SetEvent(capture_state->frame_ready_event); - } - if (capture_state->capture_thread.joinable()) { - thread_to_join = std::move(capture_state->capture_thread); - } - } - - if (thread_to_join.joinable()) { - thread_to_join.join(); - } - - { - std::lock_guard lock(capture_state->mutex); - CloseSecureDesktopSharedCaptureResourcesLocked(capture_state); - capture_state->stop_requested.store(false, std::memory_order_relaxed); - capture_state->sequence = 0; - } + capture_state->lifecycle.Stop(); return BuildTextResponseBytes("{\"ok\":true,\"shared_capture\":\"stopped\"}"); } -std::vector StartSecureDesktopSharedCapture( +std::vector PrepareSecureDesktopSharedCapture( const SecureCaptureRequest& request, SecureSharedCaptureState* capture_state) { if (capture_state == nullptr) { return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state")); } - StopSecureDesktopSharedCapture(capture_state); - const size_t payload_size = static_cast(request.width) * request.height * 3 / 2; const size_t mapping_size = @@ -1735,17 +1785,44 @@ std::vector StartSecureDesktopSharedCapture( header->height = static_cast(request.height); header->buffer_size = static_cast(payload_size); - { - std::lock_guard lock(capture_state->mutex); - capture_state->request = request; - capture_state->frame_mapping = frame_mapping; - capture_state->frame_ready_event = frame_ready_event; - capture_state->frame_view = frame_view; - capture_state->frame_view_size = mapping_size; - capture_state->sequence = 0; - capture_state->stop_requested.store(false, std::memory_order_relaxed); - capture_state->capture_thread = - std::thread(SecureDesktopSharedCaptureThread, capture_state); + // Transfer handles before any potentially throwing request/string copy. + // The lifecycle owner cleans partially prepared resources on failure too. + capture_state->frame_mapping = frame_mapping; + capture_state->frame_ready_event = frame_ready_event; + capture_state->frame_view = frame_view; + capture_state->sequence = 0; + capture_state->request = request; + + return {}; +} + +std::vector StartSecureDesktopSharedCapture( + const SecureCaptureRequest& request, + SecureSharedCaptureState* capture_state) { + if (!capture_state) { + return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state")); + } + std::vector error; + const auto result = capture_state->lifecycle.Start( + [&] { + error = PrepareSecureDesktopSharedCapture(request, capture_state); + return error.empty(); + }, + [capture_state] { SecureDesktopSharedCaptureThread(capture_state); }, + [capture_state] { + CloseSecureDesktopSharedCaptureResources(capture_state); + }, + [&] { + // A timed-out caller may retry a start that already succeeded. + return capture_state->request == request; + }); + if (result == crossdesk::SharedCaptureLifecycle::StartResult::ShuttingDown) { + return BuildTextResponseBytes(BuildErrorJson("capture_shutting_down")); + } + if (result != crossdesk::SharedCaptureLifecycle::StartResult::Started) { + return error.empty() + ? BuildTextResponseBytes(BuildErrorJson("capture_start_failed")) + : error; } Json json; @@ -1758,7 +1835,7 @@ std::vector StartSecureDesktopSharedCapture( } std::vector HandleSecureInputHelperCommand( - const std::string& command, SecureCaptureBuffers* capture_buffers, + const std::string& command, SecureSharedCaptureState* capture_state) { if (command == "ping") { return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}"); @@ -1840,7 +1917,7 @@ std::vector HandleSecureInputHelperCommand( SecureCaptureRequest capture_request; if (ParseSecureInputCaptureCommand(command, &capture_request)) { - return CaptureSecureDesktopFrame(capture_request, capture_buffers); + return CaptureSecureDesktopFrame(capture_request); } return BuildTextResponseBytes(BuildErrorJson("unknown_command")); @@ -1849,19 +1926,23 @@ std::vector HandleSecureInputHelperCommand( void HandleSecureInputHelperPipeClient( HANDLE pipe, HANDLE event_handle, std::shared_ptr capture_state) { - SecureCaptureBuffers capture_buffers; char buffer[1024] = {0}; DWORD bytes_read = 0; - if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) && + DWORD error = ERROR_SUCCESS; + if (crossdesk::pipe_deadline_detail::Transfer( + pipe, event_handle, false, buffer, sizeof(buffer) - 1, + GetTickCount64() + 1000, &bytes_read, &error) && bytes_read > 0) { std::vector response = HandleSecureInputHelperCommand( - std::string(buffer, buffer + bytes_read), &capture_buffers, - capture_state.get()); + std::string(buffer, buffer + bytes_read), capture_state.get()); DWORD bytes_written = 0; if (!response.empty()) { - WriteFile(pipe, response.data(), static_cast(response.size()), - &bytes_written, nullptr); - FlushFileBuffers(pipe); + if (crossdesk::pipe_deadline_detail::Transfer( + pipe, event_handle, true, response.data(), + static_cast(response.size()), GetTickCount64() + 1000, + &bytes_written, &error)) { + FlushFileBuffers(pipe); + } } } @@ -1930,8 +2011,10 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) { HANDLE wait_handles[] = {stop_event, overlapped.hEvent}; DWORD wait_result = WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); - if (wait_result == WAIT_OBJECT_0) { + if (wait_result != WAIT_OBJECT_0 + 1) { CancelIoEx(pipe, &overlapped); + DWORD ignored = 0; + GetOverlappedResult(pipe, &overlapped, &ignored, TRUE); CloseHandle(overlapped.hEvent); CloseHandle(pipe); break; @@ -1946,7 +2029,7 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) { .detach(); } - StopSecureDesktopSharedCapture(capture_state.get()); + capture_state->lifecycle.Shutdown(); } void PrintUsage() { diff --git a/apps/desktop/src/platform/windows/service/shared_capture_configuration.h b/apps/desktop/src/platform/windows/service/shared_capture_configuration.h new file mode 100644 index 00000000..42247a81 --- /dev/null +++ b/apps/desktop/src/platform/windows/service/shared_capture_configuration.h @@ -0,0 +1,30 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SHARED_CAPTURE_CONFIGURATION_H_ +#define _SHARED_CAPTURE_CONFIGURATION_H_ + +namespace crossdesk { + +// Desktop stage/name are hints for initial binding, not image configuration. +// The producer follows the actual input desktop without restarting the stream. +struct SharedCaptureConfiguration { + int left = 0; + int top = 0; + int width = 0; + int height = 0; + bool show_cursor = true; + int fps = 30; + + bool operator==(const SharedCaptureConfiguration& other) const { + return left == other.left && top == other.top && width == other.width && + height == other.height && show_cursor == other.show_cursor && + fps == other.fps; + } +}; + +} // namespace crossdesk +#endif // _SHARED_CAPTURE_CONFIGURATION_H_ diff --git a/apps/desktop/src/platform/windows/service/shared_capture_lifecycle.h b/apps/desktop/src/platform/windows/service/shared_capture_lifecycle.h new file mode 100644 index 00000000..7eda8c88 --- /dev/null +++ b/apps/desktop/src/platform/windows/service/shared_capture_lifecycle.h @@ -0,0 +1,121 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-08 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SHARED_CAPTURE_LIFECYCLE_H_ +#define _SHARED_CAPTURE_LIFECYCLE_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace crossdesk { + +// Serializes complete control operations, including joining the old producer. +// The producer never takes control_mutex_: resources are immutable from its +// launch until its join, and cleanup always runs after the join. +class SharedCaptureLifecycle { + // Preserve admission order while a slow join holds up several retries. A + // queued stop must not be overtaken by a new start and then stop that + // producer. + class ControlOperation { + public: + explicit ControlOperation(SharedCaptureLifecycle& owner) + : owner_(owner), + ticket_(owner.next_ticket_.fetch_add(1)), + lock_(owner.control_mutex_) { + owner_.control_ready_.wait( + lock_, [&] { return owner_.serving_ticket_ == ticket_; }); + } + ~ControlOperation() { + ++owner_.serving_ticket_; + lock_.unlock(); + owner_.control_ready_.notify_all(); + } + + private: + SharedCaptureLifecycle& owner_; + uint64_t ticket_; + std::unique_lock lock_; + }; + + public: + enum class StartResult { Started, Failed, ShuttingDown }; + SharedCaptureLifecycle() = default; + ~SharedCaptureLifecycle() { Shutdown(); } + SharedCaptureLifecycle(const SharedCaptureLifecycle&) = delete; + SharedCaptureLifecycle& operator=(const SharedCaptureLifecycle&) = delete; + + StartResult Start(std::function prepare, + std::function capture, + std::function cleanup, + std::function reuse = {}) { + if (shutting_down_.load()) return StartResult::ShuttingDown; + ControlOperation operation(*this); + if (shutting_down_.load()) return StartResult::ShuttingDown; + if (!producer_finished_.load() && reuse && reuse()) + return StartResult::Started; + StopLocked(); + cleanup_ = std::move(cleanup); + try { + if (prepare()) { + stop_requested_.store(false, std::memory_order_relaxed); + producer_finished_.store(false); + producer_ = std::thread([this, capture = std::move(capture)] { + capture(); + producer_finished_.store(true); + }); + return StartResult::Started; + } + } catch (...) { + // Failed allocation/thread creation must leave the prepared handles + // owned by cleanup, not leak them from an abandoned start request. + } + StopLocked(); + return StartResult::Failed; + } + + void Stop() { + ControlOperation operation(*this); + StopLocked(); + } + + void Shutdown() { + // Publish before waiting for the lock. Detached IPC clients that are + // already queued cannot start another producer after server shutdown. + shutting_down_.store(true); + Stop(); + } + + bool StopRequested() const { + return stop_requested_.load(std::memory_order_relaxed); + } + + private: + void StopLocked() { + stop_requested_.store(true, std::memory_order_relaxed); + if (producer_.joinable()) producer_.join(); + producer_finished_.store(true); + if (cleanup_) cleanup_(); + cleanup_ = {}; + } + + std::mutex control_mutex_; + std::condition_variable control_ready_; + std::atomic next_ticket_{0}; + uint64_t serving_ticket_ = 0; + std::atomic shutting_down_{false}; + std::atomic stop_requested_{true}; + std::atomic producer_finished_{true}; + std::thread producer_; + std::function cleanup_; +}; + +} // namespace crossdesk +#endif // _SHARED_CAPTURE_LIFECYCLE_H_ diff --git a/apps/desktop/tests/windows_service_mouse_ipc_test.cpp b/apps/desktop/tests/windows_service_mouse_ipc_test.cpp index 7576ba56..83e2eff7 100644 --- a/apps/desktop/tests/windows_service_mouse_ipc_test.cpp +++ b/apps/desktop/tests/windows_service_mouse_ipc_test.cpp @@ -79,8 +79,6 @@ int main() { ReadFile(repo_root / "apps/desktop/src/gui/features/input/keyboard_controller.cpp"); const std::string windows_service_runtime = ReadFile(repo_root / "apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp"); - const std::string screen_capturer_h = - ReadFile(repo_root / "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h"); const std::string screen_capturer_cpp = ReadFile( repo_root / "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp"); @@ -101,11 +99,8 @@ int main() { ok &= ExpectContains("session_helper_main.cpp", session_helper, "EnablePerMonitorDpiAwareness();\n\n" " InitializeHelperLogger();"); - ok &= ExpectContains( - "service_host.cpp", service_host, - "const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms"); ok &= ExpectContains("service_host.cpp", service_host, - "while (GetTickCount64() <= deadline_tick)"); + "QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms"); ok &= ExpectNotContains("service_host.cpp", service_host, "constexpr int kPipeConnectRetryCount = 3"); ok &= ExpectContains("service_host.cpp", service_host, @@ -128,8 +123,6 @@ int main() { "IsConsentUiRunningInCurrentSession"); ok &= ExpectContains("session_helper_main.cpp", session_helper, "L\"Consent.exe\""); - ok &= ExpectContains("session_helper_main.cpp", session_helper, - "desktop_info.available && consent_ui_visible"); ok &= ExpectContains("service_host.cpp", service_host, "session_helper_report_input_desktop_available_ &&"); ok &= ExpectContains("service_host.cpp", service_host, @@ -145,18 +138,13 @@ int main() { "return L\"winsta0\\\\Winlogon\""); ok &= ExpectContains("service_host.cpp", service_host, "return L\"winsta0\\\\default\""); - ok &= ExpectContains( - "service_host.cpp", service_host, - "secure_input_helper_interactive_stage_ == interactive_stage"); ok &= ExpectContains( "service_host.cpp", service_host, "secure_input_helper_interactive_stage_ = interactive_stage"); ok &= ExpectContains("service_host.cpp", service_host, "secure_input_helper_interactive_stage_.clear()"); - ok &= ExpectContains( - "service_host.cpp", service_host, - "LaunchSecureInputHelper(target_session_id, interactive_stage,\n" - " interactive_desktop)"); + ok &= ExpectContains("service_host.cpp", service_host, + "LaunchSecureInputHelper(secure_input_target_session_id,"); ok &= ExpectContains("service_host.cpp", service_host, "\\\"secure_input_helper_stage\\\":\\\""); ok &= ExpectContains("service_host.cpp", service_host, @@ -256,15 +244,7 @@ int main() { "runtime->local_service_available_ &&\n" " " "IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_)"); - ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h, - "std::string secure_shared_stage_;"); ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, "const std::string& stage"); - ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, - "secure_shared_stage_ == stage"); - ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, - "secure_shared_stage_ = stage"); - ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp, - "secure_shared_stage_.clear()"); return ok ? 0 : 1; } diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 44917b8d..b06c524d 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -593,7 +593,8 @@ function setup_targets() set_kind("binary") add_packages("libyuv", "nlohmann_json") add_deps("rd_log", "path_manager") - add_links("Advapi32", "User32", "Wtsapi32", "Gdi32", "Setupapi") + add_links("Advapi32", "User32", "Wtsapi32", "Gdi32", "Setupapi", + "d3d11", "dxgi") add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp") add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp") add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",