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 5f188077..0f503e86 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 @@ -17,9 +17,12 @@ #include #include "captured_nv12_frame.h" #include "interactive_state.h" +#include "named_pipe_deadline.h" #include "rd_log.h" #include "screen_capturer_dxgi.h" #include "screen_capturer_gdi.h" +#include "secure_desktop_frame_schedule.h" +#include "secure_desktop_status_poller.h" #include "service_host.h" #include "session_helper_shared.h" #include "wgc_plugin_api.h" @@ -41,17 +44,6 @@ constexpr int kSecureDesktopCaptureMinFps = 30; constexpr int kSecureDesktopCaptureMaxIntervalMs = 1000 / kSecureDesktopCaptureMinFps; -struct SecureDesktopServiceStatus { - bool service_available = false; - bool capture_active = false; - bool helper_running = false; - DWORD active_session_id = 0xFFFFFFFF; - DWORD error_code = 0; - std::string interactive_stage; - std::string interactive_desktop; - std::string error; -}; - class WgcPluginCapturer final : public ScreenCapturer { public: using CreateFn = ScreenCapturer* (*)(); @@ -186,41 +178,6 @@ bool IsTransientWindowsServiceStatusError(const std::string& error) { error == "pipe_read_failed"; } -bool ReadPipeMessage(HANDLE pipe, std::vector* response_out, - DWORD* error_code_out = nullptr) { - if (response_out == nullptr) { - return false; - } - - response_out->clear(); - if (error_code_out != nullptr) { - *error_code_out = 0; - } - - std::vector chunk(64 * 1024); - while (true) { - DWORD bytes_read = 0; - if (ReadFile(pipe, chunk.data(), static_cast(chunk.size()), - &bytes_read, nullptr)) { - response_out->insert(response_out->end(), chunk.begin(), - chunk.begin() + bytes_read); - return true; - } - - const DWORD error = GetLastError(); - response_out->insert(response_out->end(), chunk.begin(), - chunk.begin() + bytes_read); - if (error == ERROR_MORE_DATA) { - continue; - } - - if (error_code_out != nullptr) { - *error_code_out = error; - } - return false; - } -} - bool ParseSecureDesktopFrameResponse(const std::vector& response, std::vector* nv12_frame_out, int* width_out, int* height_out, @@ -261,25 +218,27 @@ bool ParseSecureDesktopFrameResponse(const std::vector& response, return true; } -bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) { - if (status == nullptr) { - return false; +SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() { + SecureDesktopServiceStatus status; + std::vector response; + DWORD error_code = 0; + if (!QueryNamedPipeWithDeadline(kCrossDeskServicePipeName, "status", + kSecureDesktopStatusPipeTimeoutMs, &response, + &status.error, &error_code)) { + status.error_code = error_code; + return status; } - - *status = {}; - const std::string response = - QueryCrossDeskService("status", kSecureDesktopStatusPipeTimeoutMs); - Json json = Json::parse(response, nullptr, false); + Json json = Json::parse(response.begin(), response.end(), nullptr, false); if (json.is_discarded() || !json.is_object()) { - status->error = "invalid_service_status_json"; - return false; + status.error = "invalid_service_status_json"; + return status; } - status->service_available = json.value("ok", false); - if (!status->service_available) { - status->error = json.value("error", std::string("service_unavailable")); - status->error_code = json.value("code", 0u); - return true; + status.service_available = json.value("ok", false); + if (!status.service_available) { + status.error = json.value("error", std::string("service_unavailable")); + status.error_code = json.value("code", 0u); + return status; } if (ShouldNormalizeUnlockToUserDesktop( @@ -293,26 +252,27 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) { json.value("password_box_visible", false), json.value("unlock_ui_visible", false), json.value("last_session_event", std::string()))) { - status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu); - status->interactive_stage = "user-desktop"; - status->interactive_desktop.clear(); - status->capture_active = false; - return true; + status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu); + status.interactive_stage = "user-desktop"; + status.interactive_desktop.clear(); + status.capture_active = false; + return status; } - status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu); - status->helper_running = json.value("secure_input_helper_running", false); - status->interactive_stage = json.value("interactive_stage", std::string()); - status->interactive_desktop = + status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu); + 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()); + status.interactive_desktop = json.value("interactive_input_desktop", std::string()); const bool secure_desktop_active = json.value("interactive_secure_desktop_active", json.value("secure_desktop_active", false)); - status->capture_active = - status->active_session_id != 0xFFFFFFFF && + status.capture_active = + status.active_session_id != 0xFFFFFFFF && (secure_desktop_active || - IsSecureDesktopInteractionRequired(status->interactive_stage)); - return true; + IsSecureDesktopInteractionRequired(status.interactive_stage)); + return status; } bool QuerySecureDesktopHelperCommand(DWORD session_id, @@ -323,60 +283,29 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id, return false; } - response_out->clear(); const std::wstring pipe_name = GetCrossDeskSecureInputHelperPipeName(session_id); - if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) { - if (error_out != nullptr) { - *error_out = "pipe_unavailable:" + std::to_string(GetLastError()); - } - return false; + 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. + const DWORD timeout_ms = + command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0 + ? 1000 + : kSecureDesktopHelperPipeTimeoutMs; + const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms, + response_out, &error, &code); + if (!ok && error_out != nullptr) { + *error_out = error + ":" + std::to_string(code); } - - HANDLE pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, - nullptr, OPEN_EXISTING, 0, nullptr); - if (pipe == INVALID_HANDLE_VALUE) { - if (error_out != nullptr) { - *error_out = "pipe_connect_failed:" + std::to_string(GetLastError()); - } - return false; - } - - 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)) { - const DWORD error = GetLastError(); - CloseHandle(pipe); - if (error_out != nullptr) { - *error_out = "pipe_write_failed:" + std::to_string(error); - } - return false; - } - - DWORD read_error = 0; - const bool read_ok = ReadPipeMessage(pipe, response_out, &read_error); - CloseHandle(pipe); - if (!read_ok) { - if (error_out != nullptr) { - *error_out = "pipe_read_failed:" + std::to_string(read_error); - } - return false; - } - - return true; + return ok; } -bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top, - int width, int height, bool show_cursor, - const std::string& stage, - const std::string& desktop, - std::vector* nv12_frame_out, - int* captured_width_out, - int* captured_height_out, - std::string* error_out) { +bool QuerySecureDesktopHelperFrame( + DWORD session_id, int left, int top, int width, int height, + bool show_cursor, const std::string& stage, const std::string& desktop, + std::vector* nv12_frame_out, int* captured_width_out, + int* captured_height_out, std::string* error_out) { if (nv12_frame_out == nullptr || captured_width_out == nullptr || captured_height_out == nullptr) { return false; @@ -1162,17 +1091,35 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { const int frame_interval_ms = fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_) : kSecureDesktopCaptureMaxIntervalMs; - ULONGLONG last_status_tick = 0; ULONGLONG last_error_tick = 0; - bool last_capture_active = false; - bool last_service_available = true; - std::string last_stage; - std::string last_service_error; ULONGLONG capture_stage_started_tick = 0; bool post_secure_restart_pending = false; ULONGLONG post_secure_restart_deadline_tick = 0; ULONGLONG last_post_secure_restart_tick = 0; SecureDesktopServiceStatus status; + SecureDesktopStatusPoller status_poller( + QuerySecureDesktopServiceStatus, + std::chrono::milliseconds(kSecureDesktopStatusIntervalMs)); + SecureDesktopFrameSchedule frame_schedule; + frame_schedule.Reset(GetTickCount64()); + ULONGLONG stats_started = GetTickCount64(); + unsigned shared_frames = 0, fallback_frames = 0, shared_waits = 0; + unsigned shared_restarts = 0; + int64_t max_status_ms = 0; + auto report_stats = [&](bool force) { + const ULONGLONG elapsed = GetTickCount64() - stats_started; + if (elapsed == 0 || (!force && elapsed < 5000)) return; + if (shared_frames || fallback_frames || shared_waits || shared_restarts) { + LOG_INFO( + "Secure capture delivery: shared_fps={:.1f}, fallback_fps={:.1f}, " + "pending_waits={}, shared_restarts={}, status_max_ms={}", + shared_frames * 1000.0 / elapsed, fallback_frames * 1000.0 / elapsed, + shared_waits, shared_restarts, max_status_ms); + } + stats_started = GetTickCount64(); + shared_frames = fallback_frames = shared_waits = shared_restarts = 0; + max_status_ms = 0; + }; std::vector secure_frame; while (running_.load(std::memory_order_relaxed)) { @@ -1182,67 +1129,60 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { } const ULONGLONG now = GetTickCount64(); - if (last_status_tick == 0 || - now - last_status_tick >= kSecureDesktopStatusIntervalMs) { - SecureDesktopServiceStatus latest_status; - const bool status_ok = QuerySecureDesktopServiceStatus(&latest_status); - status = latest_status; - if (status_ok) { - const bool service_changed = - status.service_available != last_service_available; - const bool service_error_changed = - !status.service_available && status.error != last_service_error; - if (service_changed || service_error_changed) { - if (status.service_available) { - LOG_INFO( - "Windows capturer secure desktop service available, polling " - "session_id={}", - status.active_session_id); - } else if (IsTransientWindowsServiceStatusError(status.error)) { - LOG_INFO( - "Windows capturer secure desktop service temporarily unavailable: " - "error={}, code={}", - status.error, status.error_code); - } else { - LOG_WARN( - "Windows capturer secure desktop service unavailable: " - "error={}, code={}", - status.error, status.error_code); - } - last_service_available = status.service_available; - last_service_error = status.error; + 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); + if (status.service_available != previous.service_available || + status.error != previous.error) { + if (status.service_available) { + LOG_INFO( + "Windows capturer secure desktop service available, " + "polling session_id={}", + status.active_session_id); + } else if (IsTransientWindowsServiceStatusError(status.error)) { + LOG_INFO( + "Windows capturer secure desktop service temporarily " + "unavailable; " + "keeping last capture state: error={}, code={}", + status.error, status.error_code); + } else { + LOG_WARN( + "Windows capturer secure desktop service unavailable: " + "error={}, code={}", + status.error, status.error_code); } - } else if (last_service_available || - last_service_error != "invalid_service_status_json") { - LOG_WARN("Windows capturer secure desktop service status query failed"); - last_service_available = false; - last_service_error = "invalid_service_status_json"; } secure_desktop_capture_active_.store(status.capture_active, std::memory_order_relaxed); - if (status.capture_active != last_capture_active || - status.interactive_stage != last_stage) { + if (status.capture_active != previous.capture_active || + status.interactive_stage != previous.interactive_stage || + status.active_session_id != previous.active_session_id || + status.helper_process_id != previous.helper_process_id) { + 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); const bool secure_capture_started = - !last_capture_active && status.capture_active; + !previous.capture_active && status.capture_active; const bool secure_capture_ended = - last_capture_active && !status.capture_active; + previous.capture_active && !status.capture_active; capture_stage_started_tick = now; LOG_INFO( "Windows capturer secure desktop state: active={}, stage='{}', " "session_id={}", status.capture_active, status.interactive_stage, status.active_session_id); - last_capture_active = status.capture_active; - last_stage = status.interactive_stage; if (secure_capture_started) { 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); + 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; post_secure_restart_deadline_tick = @@ -1250,13 +1190,12 @@ 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); + post_secure_desktop_drop_logged_.store(false, + std::memory_order_relaxed); + post_secure_desktop_started_tick_.store(now, + std::memory_order_relaxed); } } - last_status_tick = now; } if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { @@ -1302,45 +1241,64 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { std::string error_message; bool frame_delivered = false; const bool show_cursor = show_cursor_.load(std::memory_order_relaxed); - const int shared_fps = - fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinFps, fps_) - : kSecureDesktopCaptureMinFps; + const int shared_fps = fps_ > 0 + ? (std::max)(kSecureDesktopCaptureMinFps, fps_) + : kSecureDesktopCaptureMinFps; - if (StartSecureDesktopSharedCapture(status.active_session_id, left, top, - width, height, - status.interactive_stage, - status.interactive_desktop, - show_cursor, shared_fps, - &error_message) && + if (secure_shared_capture_started_ && + frame_schedule.SharedCaptureStalled(now)) { + LOG_WARN("Secure shared capture stalled; restarting producer"); + StopSecureDesktopSharedCapture(secure_shared_session_id_); + frame_schedule.Reset(GetTickCount64()); + ++shared_restarts; + } + + bool shared_ready = false; + const bool was_started = secure_shared_capture_started_; + if (was_started || frame_schedule.CanStart(now)) { + shared_ready = StartSecureDesktopSharedCapture( + status.active_session_id, left, top, width, height, + status.interactive_stage, status.interactive_desktop, show_cursor, + shared_fps, &error_message); + if (!shared_ready) { + frame_schedule.OnStartFailure(GetTickCount64()); + } else if (!was_started) { + frame_schedule.Reset(GetTickCount64()); + } + } + + if (shared_ready && ReadSecureDesktopSharedFrame( - static_cast(frame_interval_ms + 20), &secure_frame, + static_cast((std::max)(50, frame_interval_ms + 20)), + &secure_frame, &captured_width, &captured_height, &error_message)) { + frame_delivered = true; + ++shared_frames; + frame_schedule.OnSharedFrame(GetTickCount64()); + } + + const bool frame_pending = shared_ready && !frame_delivered && + IsPendingSecureDesktopFrame(error_message); + if (frame_pending) ++shared_waits; + if (shared_ready && !frame_delivered && !frame_pending) { + StopSecureDesktopSharedCapture(secure_shared_session_id_); + frame_schedule.OnStartFailure(GetTickCount64()); + } + if (!frame_delivered && !frame_pending && + QuerySecureDesktopHelperFrame( + status.active_session_id, left, top, width, height, show_cursor, + status.interactive_stage, status.interactive_desktop, &secure_frame, &captured_width, &captured_height, &error_message)) { - if (!secure_frame.empty()) { - EmitCapturedFrame(secure_frame.data(), - static_cast(secure_frame.size()), - captured_width, captured_height, - display_name.c_str()); - } frame_delivered = true; + ++fallback_frames; } - if (!frame_delivered && - QuerySecureDesktopHelperFrame(status.active_session_id, left, top, - width, height, show_cursor, - status.interactive_stage, - status.interactive_desktop, - &secure_frame, &captured_width, - &captured_height, &error_message)) { - if (!secure_frame.empty()) { - EmitCapturedFrame(secure_frame.data(), - static_cast(secure_frame.size()), - captured_width, captured_height, - display_name.c_str()); - } - frame_delivered = true; + if (frame_delivered && !secure_frame.empty()) { + EmitCapturedFrame(secure_frame.data(), + static_cast(secure_frame.size()), captured_width, + captured_height, display_name.c_str()); } - if (!frame_delivered) { + if (!frame_delivered && !frame_pending) { const bool transient_error = IsTransientSecureDesktopFrameError(error_message); const bool in_grace_period = capture_stage_started_tick != 0 && @@ -1371,9 +1329,18 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { } } - std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms)); + const auto elapsed_ms = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - frame_started) + .count(); + const int sleep_ms = SecureDesktopFrameSchedule::RemainingFrameDelay( + frame_interval_ms, static_cast(elapsed_ms)); + if (sleep_ms > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); + } } + report_stats(true); StopSecureDesktopSharedCapture(secure_shared_session_id_); secure_desktop_capture_active_.store(false, std::memory_order_relaxed); } 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 new file mode 100644 index 00000000..868a6921 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_frame_schedule.h @@ -0,0 +1,50 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-07 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SECURE_DESKTOP_FRAME_SCHEDULE_H_ +#define _SECURE_DESKTOP_FRAME_SCHEDULE_H_ + +#include +#include + +namespace crossdesk { + +inline bool IsPendingSecureDesktopFrame(const std::string& error) { + return error == "frame_wait_timeout" || + error == "shared_frame_write_in_progress" || + error == "shared_frame_changed_during_read"; +} + +// A 4K GDI frame can take longer than a single consumer wait. Do not issue +// another expensive screenshot while the shared producer is still healthy. +class SecureDesktopFrameSchedule { + public: + void Reset(uint64_t now) { + last_shared_frame_ = now; + next_start_attempt_ = 0; + } + + void OnSharedFrame(uint64_t now) { last_shared_frame_ = now; } + bool SharedCaptureStalled(uint64_t now) const { + return now - last_shared_frame_ >= 1000; + } + bool CanStart(uint64_t now) const { return now >= next_start_attempt_; } + void OnStartFailure(uint64_t now) { next_start_attempt_ = now + 1000; } + + static int RemainingFrameDelay(int interval_ms, uint64_t elapsed_ms) { + return elapsed_ms >= static_cast(interval_ms) + ? 0 + : interval_ms - static_cast(elapsed_ms); + } + + private: + uint64_t last_shared_frame_ = 0; + uint64_t next_start_attempt_ = 0; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file 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 new file mode 100644 index 00000000..a792b7b9 --- /dev/null +++ b/apps/desktop/src/platform/windows/screen_capturer/secure_desktop_status_poller.h @@ -0,0 +1,110 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-07 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SECURE_DESKTOP_STATUS_POLLER_H_ +#define _SECURE_DESKTOP_STATUS_POLLER_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace crossdesk { + +struct SecureDesktopServiceStatus { + bool service_available = false; + bool capture_active = false; + bool helper_running = false; + uint32_t helper_process_id = 0; + uint32_t active_session_id = 0xFFFFFFFF; + uint32_t error_code = 0; + std::string interactive_stage; + std::string interactive_desktop; + std::string error; + + void UpdateFrom(SecureDesktopServiceStatus latest) { + if (latest.service_available) { + *this = std::move(latest); + } else { + // Failed IPC says nothing about the desktop. Retain the last confirmed + // capture state until a valid response reports a change or an unlock. + service_available = false; + error = std::move(latest.error); + error_code = latest.error_code; + } + } +}; + +// The query runs without holding the snapshot mutex. Slow service/helper IPC +// must not stop delivery of frames that are already available in shared memory. +class SecureDesktopStatusPoller { + public: + using Query = std::function; + + struct Sample { + SecureDesktopServiceStatus status; + int64_t query_ms = 0; + }; + + explicit SecureDesktopStatusPoller(Query query, + std::chrono::milliseconds interval) + : thread_([this, query = std::move(query), interval]() { + std::unique_lock lock(mutex_); + while (!stopping_) { + lock.unlock(); + const auto started = std::chrono::steady_clock::now(); + auto latest = query(); + const auto elapsed = + std::chrono::duration_cast( + std::chrono::steady_clock::now() - started); + lock.lock(); + sample_.status.UpdateFrom(std::move(latest)); + sample_.query_ms = elapsed.count(); + pending_ = true; + // Even a failed/slow query gets a pause, avoiding a retry storm. + wake_.wait_for(lock, interval, [this] { return stopping_; }); + } + }) {} + + ~SecureDesktopStatusPoller() { + { + std::lock_guard lock(mutex_); + stopping_ = true; + } + wake_.notify_one(); + thread_.join(); + } + + SecureDesktopStatusPoller(const SecureDesktopStatusPoller&) = delete; + SecureDesktopStatusPoller& operator=(const SecureDesktopStatusPoller&) = + delete; + + std::optional Take() { + std::lock_guard lock(mutex_); + if (!pending_) { + return std::nullopt; + } + pending_ = false; + return sample_; + } + + private: + std::mutex mutex_; + std::condition_variable wake_; + bool stopping_ = false; + bool pending_ = false; + Sample sample_; + std::thread thread_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/service/interactive_desktop.h b/apps/desktop/src/platform/windows/service/interactive_desktop.h new file mode 100644 index 00000000..518b165b --- /dev/null +++ b/apps/desktop/src/platform/windows/service/interactive_desktop.h @@ -0,0 +1,101 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-07 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _INTERACTIVE_DESKTOP_H_ +#define _INTERACTIVE_DESKTOP_H_ + +#include + +#include + +namespace crossdesk { + +inline std::wstring GetDesktopNameW(HDESK desktop) { + DWORD bytes_needed = 0; + if (desktop == nullptr) { + return {}; + } + GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed); + if (bytes_needed == 0) { + return {}; + } + std::wstring name(bytes_needed / sizeof(wchar_t), L'\0'); + if (!GetUserObjectInformationW(desktop, UOI_NAME, name.data(), bytes_needed, + &bytes_needed)) { + return {}; + } + while (!name.empty() && name.back() == L'\0') { + name.pop_back(); + } + return name; +} + +// Use only on worker threads without windows or hooks. Restore the original +// desktop before closing the handle: Windows cannot close a desktop in use. +class ScopedInteractiveDesktop { + public: + ScopedInteractiveDesktop() + : original_(GetThreadDesktop(GetCurrentThreadId())) {} + ScopedInteractiveDesktop(const ScopedInteractiveDesktop&) = delete; + ScopedInteractiveDesktop& operator=(const ScopedInteractiveDesktop&) = delete; + + ~ScopedInteractiveDesktop() { + if (desktop_ != nullptr) { + SetThreadDesktop(original_); + CloseDesktop(desktop_); + } + } + + bool Bind(const std::wstring& fallback_name) { + constexpr ACCESS_MASK access = + DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP | DESKTOP_JOURNALPLAYBACK; + // SendInput requires playback access on the attached desktop; otherwise + // it returns ERROR_ACCESS_DENIED even for a correctly attached SYSTEM helper. + // The service's stage/name can lag a lock, unlock, or UAC transition. + // Opening a cached desktop can succeed even when it is no longer visible. + HDESK desktop = OpenInputDesktop(0, FALSE, access); + if (desktop == nullptr && !fallback_name.empty()) { + desktop = OpenDesktopW(fallback_name.c_str(), 0, FALSE, access); + } + if (desktop == nullptr) { + return false; + } + if (!SetThreadDesktop(desktop)) { + const DWORD error = GetLastError(); + CloseDesktop(desktop); + SetLastError(error); + return false; + } + if (desktop_ != nullptr) { + CloseDesktop(desktop_); + } + desktop_ = desktop; + name_ = GetDesktopNameW(desktop_); + return true; + } + + bool IsInputDesktop() const { + HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS); + if (input == nullptr) { + return false; + } + const std::wstring input_name = GetDesktopNameW(input); + CloseDesktop(input); + return !name_.empty() && _wcsicmp(name_.c_str(), input_name.c_str()) == 0; + } + + const std::wstring& name() const { return name_; } + + private: + HDESK original_ = nullptr; + HDESK desktop_ = nullptr; + std::wstring name_; +}; + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/service/named_pipe_deadline.h b/apps/desktop/src/platform/windows/service/named_pipe_deadline.h new file mode 100644 index 00000000..3a3216fa --- /dev/null +++ b/apps/desktop/src/platform/windows/service/named_pipe_deadline.h @@ -0,0 +1,143 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-09-07 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _NAMED_PIPE_DEADLINE_H_ +#define _NAMED_PIPE_DEADLINE_H_ + +#include + +#include +#include +#include +#include + +namespace crossdesk { + +namespace pipe_deadline_detail { +struct Handle { + explicit Handle(HANDLE handle = INVALID_HANDLE_VALUE) : value(handle) {} + Handle(const Handle&) = delete; + Handle& operator=(const Handle&) = delete; + HANDLE value = INVALID_HANDLE_VALUE; + ~Handle() { + if (value != INVALID_HANDLE_VALUE && value != nullptr) CloseHandle(value); + } +}; + +inline DWORD Remaining(ULONGLONG deadline) { + const ULONGLONG now = GetTickCount64(); + return now >= deadline ? 0 : static_cast(deadline - now); +} + +inline bool Transfer(HANDLE pipe, HANDLE event, bool write, void* data, + DWORD size, ULONGLONG deadline, DWORD* transferred, + DWORD* error) { + *transferred = 0; + if (Remaining(deadline) == 0) { + *error = ERROR_SEM_TIMEOUT; + return false; + } + ResetEvent(event); + OVERLAPPED operation{}; + operation.hEvent = event; + const BOOL complete = + write ? WriteFile(pipe, data, size, transferred, &operation) + : ReadFile(pipe, data, size, transferred, &operation); + *error = complete ? ERROR_SUCCESS : GetLastError(); + if (*error == ERROR_IO_PENDING) { + const DWORD wait = WaitForSingleObject(event, Remaining(deadline)); + if (wait != WAIT_OBJECT_0) { + *error = wait == WAIT_TIMEOUT ? ERROR_SEM_TIMEOUT : GetLastError(); + // Keep the OVERLAPPED and buffer alive until cancellation completes. + CancelIoEx(pipe, &operation); + DWORD ignored = 0; + GetOverlappedResult(pipe, &operation, &ignored, TRUE); + return false; + } + } else if (*error != ERROR_SUCCESS && *error != ERROR_MORE_DATA) { + return false; + } + // Immediate MORE_DATA also needs GetOverlappedResult: ReadFile may leave + // its byte-count output at zero even though part of the message was read. + const BOOL result = GetOverlappedResult(pipe, &operation, transferred, FALSE); + *error = result ? ERROR_SUCCESS : GetLastError(); + return result != FALSE; +} +} // namespace pipe_deadline_detail + +// One deadline covers connection, write, and every response chunk. A connect +// timeout alone does not bound a synchronous ReadFile on an unresponsive peer. +inline bool QueryNamedPipeWithDeadline(const std::wstring& name, + const std::string& command, + DWORD timeout_ms, + std::vector* response, + std::string* error, DWORD* error_code) { + using namespace pipe_deadline_detail; + response->clear(); + error->clear(); + *error_code = ERROR_SUCCESS; + const ULONGLONG deadline = GetTickCount64() + timeout_ms; + Handle pipe; + while (true) { + const DWORD remaining = Remaining(deadline); + if (remaining == 0) break; + if (WaitNamedPipeW(name.c_str(), remaining)) { + pipe.value = + CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, nullptr, + OPEN_EXISTING, FILE_FLAG_OVERLAPPED, nullptr); + if (pipe.value != INVALID_HANDLE_VALUE) break; + } + *error_code = GetLastError(); + if (*error_code != ERROR_FILE_NOT_FOUND && *error_code != ERROR_PIPE_BUSY && + *error_code != ERROR_SEM_TIMEOUT) + break; + Sleep((std::min)(DWORD{10}, Remaining(deadline))); + } + if (pipe.value == INVALID_HANDLE_VALUE) { + *error = "pipe_unavailable"; + if (*error_code == ERROR_SUCCESS) *error_code = ERROR_SEM_TIMEOUT; + return false; + } + DWORD mode = PIPE_READMODE_MESSAGE; + if (!SetNamedPipeHandleState(pipe.value, &mode, nullptr, nullptr)) { + *error = "pipe_connect_failed"; + *error_code = GetLastError(); + return false; + } + Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)}; + if (event.value == nullptr) { + *error = "pipe_connect_failed"; + *error_code = GetLastError(); + return false; + } + DWORD transferred = 0; + if (!Transfer(pipe.value, event.value, true, + const_cast(command.data()), + static_cast(command.size()), deadline, &transferred, + error_code) || + transferred != command.size()) { + *error = "pipe_write_failed"; + return false; + } + std::vector chunk(64 * 1024); + while (true) { + const bool complete = Transfer(pipe.value, event.value, false, chunk.data(), + static_cast(chunk.size()), deadline, + &transferred, error_code); + if (!complete && *error_code != ERROR_MORE_DATA) { + response->clear(); + *error = "pipe_read_failed"; + return false; + } + response->insert(response->end(), chunk.begin(), + chunk.begin() + transferred); + if (complete) return true; + } +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/apps/desktop/src/platform/windows/service/service_host.cpp b/apps/desktop/src/platform/windows/service/service_host.cpp index 340037d2..b774cd88 100644 --- a/apps/desktop/src/platform/windows/service/service_host.cpp +++ b/apps/desktop/src/platform/windows/service/service_host.cpp @@ -16,6 +16,7 @@ #include #include +#include "interactive_state.h" #include "path_manager.h" #include "rd_log.h" #include "session_helper_shared.h" @@ -1459,18 +1460,18 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const { std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked( const std::string& interactive_stage) const { - if (interactive_stage == "lock-screen") { - return "Default"; - } - if (session_helper_status_ok_ && session_helper_report_input_desktop_available_ && !session_helper_report_input_desktop_.empty() && - (interactive_stage == "credential-ui" || + (IsSecureDesktopInteractionRequired(interactive_stage) || session_helper_report_consent_ui_visible_)) { return session_helper_report_input_desktop_; } + if (interactive_stage == "lock-screen") { + return "Default"; + } + if (input_desktop_available_ && !input_desktop_name_.empty() && (interactive_stage == "credential-ui" || consent_ui_visible_)) { return input_desktop_name_; 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 8b44a8f0..d3fdc34d 100644 --- a/apps/desktop/src/platform/windows/service/session_helper_main.cpp +++ b/apps/desktop/src/platform/windows/service/session_helper_main.cpp @@ -21,6 +21,7 @@ #include #include +#include "interactive_desktop.h" #include "path_manager.h" #include "rd_log.h" #include "session_helper_shared.h" @@ -30,6 +31,7 @@ namespace { using crossdesk::get_logger; using crossdesk::InitLogger; +using crossdesk::ScopedInteractiveDesktop; using Json = nlohmann::json; constexpr DWORD kSessionHelperStatePollMs = 1000; @@ -538,123 +540,8 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id, } } -std::wstring GetDesktopNameW(HDESK desktop) { - if (desktop == nullptr) { - return L""; - } - - DWORD bytes_needed = 0; - GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed); - if (bytes_needed == 0) { - return L""; - } - - std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0'); - if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(), - bytes_needed, &bytes_needed)) { - return L""; - } - - while (!desktop_name.empty() && desktop_name.back() == L'\0') { - desktop_name.pop_back(); - } - return desktop_name; -} - std::wstring GetCurrentThreadDesktopNameW() { - return GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId())); -} - -constexpr ACCESS_MASK kCrossDeskInteractiveDesktopAccess = - DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | - DESKTOP_SWITCHDESKTOP; - -bool EnsureThreadDesktop(const wchar_t* desktop_name, - HDESK* opened_desktop_out = nullptr) { - if (desktop_name == nullptr) { - return false; - } - - std::wstring current_desktop = GetCurrentThreadDesktopNameW(); - if (!current_desktop.empty() && - _wcsicmp(current_desktop.c_str(), desktop_name) == 0) { - return true; - } - - HDESK desktop = - OpenDesktopW(desktop_name, 0, FALSE, kCrossDeskInteractiveDesktopAccess); - if (desktop == nullptr) { - return false; - } - - if (!SetThreadDesktop(desktop)) { - CloseDesktop(desktop); - return false; - } - - if (opened_desktop_out != nullptr) { - *opened_desktop_out = desktop; - } else { - CloseDesktop(desktop); - } - return true; -} - -bool EnsureThreadInputDesktop(HDESK* opened_desktop_out = nullptr) { - HDESK desktop = - OpenInputDesktop(0, FALSE, kCrossDeskInteractiveDesktopAccess); - if (desktop == nullptr) { - return false; - } - - const std::wstring input_desktop = GetDesktopNameW(desktop); - const std::wstring current_desktop = GetCurrentThreadDesktopNameW(); - if (!input_desktop.empty() && !current_desktop.empty() && - _wcsicmp(input_desktop.c_str(), current_desktop.c_str()) == 0) { - if (opened_desktop_out != nullptr) { - *opened_desktop_out = desktop; - } else { - CloseDesktop(desktop); - } - return true; - } - - if (!SetThreadDesktop(desktop)) { - CloseDesktop(desktop); - return false; - } - - if (opened_desktop_out != nullptr) { - *opened_desktop_out = desktop; - } else { - CloseDesktop(desktop); - } - return true; -} - -bool EnsureThreadInteractiveDesktop(HDESK* opened_desktop_out = nullptr) { - if (opened_desktop_out != nullptr) { - *opened_desktop_out = nullptr; - } - - if (EnsureThreadInputDesktop(opened_desktop_out)) { - return true; - } - const DWORD input_desktop_error = GetLastError(); - - if (EnsureThreadDesktop(L"Winlogon", opened_desktop_out)) { - return true; - } - const DWORD winlogon_error = GetLastError(); - - LOG_WARN( - "Failed to switch secure input helper desktop, input_error={}, " - "winlogon_error={}, current='{}'", - input_desktop_error, winlogon_error, - WideToUtf8(GetCurrentThreadDesktopNameW())); - SetLastError(winlogon_error != ERROR_SUCCESS ? winlogon_error - : input_desktop_error); - return false; + return crossdesk::GetDesktopNameW(GetThreadDesktop(GetCurrentThreadId())); } std::wstring DesktopNameForInteractiveStage( @@ -700,8 +587,10 @@ DesktopSwitchDetails BuildDesktopSwitchDetails( return details; } -InputInjectionResult BuildInputSuccess() { - return {}; +InputInjectionResult BuildInputSuccess(DesktopSwitchDetails desktop) { + InputInjectionResult result; + result.desktop = std::move(desktop); + return result; } InputInjectionResult BuildInputFailure(const char* error, DWORD error_code, @@ -732,48 +621,23 @@ Json BuildInputFailureJson(const InputInjectionResult& result) { bool EnsureThreadInteractiveDesktopForStage( const std::string& interactive_stage, const std::string& interactive_desktop, - HDESK* opened_desktop_out = nullptr, + ScopedInteractiveDesktop* desktop, DesktopSwitchDetails* switch_details = nullptr) { - if (opened_desktop_out != nullptr) { - *opened_desktop_out = nullptr; - } - DesktopSwitchDetails local_details = BuildDesktopSwitchDetails(interactive_stage, interactive_desktop); if (switch_details != nullptr) { *switch_details = local_details; } - const std::wstring desktop_name = + std::wstring desktop_name = DesktopNameForInteractiveStage(interactive_stage, interactive_desktop); - if (!desktop_name.empty()) { - if (EnsureThreadDesktop(desktop_name.c_str(), opened_desktop_out)) { - if (switch_details != nullptr) { - switch_details->current_desktop = - WideToUtf8(GetCurrentThreadDesktopNameW()); - } - return true; - } - - const DWORD error = GetLastError(); - if (switch_details != nullptr) { - switch_details->error_code = error; - switch_details->current_desktop = - WideToUtf8(GetCurrentThreadDesktopNameW()); - } - LOG_WARN( - "Failed to switch secure input helper to stage desktop, stage='{}', " - "desktop='{}', error={}, current='{}'", - interactive_stage, WideToUtf8(desktop_name), - error, - WideToUtf8(GetCurrentThreadDesktopNameW())); - SetLastError(error); - return false; + if (desktop_name.empty()) { + desktop_name = L"Winlogon"; } - - if (EnsureThreadInteractiveDesktop(opened_desktop_out)) { + if (desktop->Bind(desktop_name)) { if (switch_details != nullptr) { - switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW()); + switch_details->target_desktop = WideToUtf8(desktop->name()); + switch_details->current_desktop = switch_details->target_desktop; } return true; } @@ -783,18 +647,15 @@ bool EnsureThreadInteractiveDesktopForStage( switch_details->error_code = error; switch_details->current_desktop = WideToUtf8(GetCurrentThreadDesktopNameW()); } + LOG_WARN( + "Failed to switch secure input helper desktop, stage='{}', " + "fallback='{}', error={}, current='{}'", + interactive_stage, WideToUtf8(desktop_name), error, + WideToUtf8(GetCurrentThreadDesktopNameW())); + SetLastError(error); return false; } -struct ScopedDesktopHandle { - HDESK handle = nullptr; - ~ScopedDesktopHandle() { - if (handle != nullptr) { - CloseDesktop(handle); - } - } -}; - bool PreferSideSpecificVkInjection(int key_code) { switch (key_code) { case VK_LSHIFT: @@ -815,11 +676,11 @@ InputInjectionResult InjectKeyboardInput( int key_code, bool is_down, uint32_t scan_code, bool extended, const std::string& interactive_stage, const std::string& interactive_desktop) { - ScopedDesktopHandle desktop; + ScopedInteractiveDesktop desktop; DesktopSwitchDetails desktop_switch; if (!EnsureThreadInteractiveDesktopForStage(interactive_stage, interactive_desktop, - &desktop.handle, + &desktop, &desktop_switch)) { const DWORD error = GetLastError(); return BuildInputFailure("switch_interactive_desktop_failed", @@ -875,7 +736,7 @@ InputInjectionResult InjectKeyboardInput( desktop_switch); } - return BuildInputSuccess(); + return BuildInputSuccess(std::move(desktop_switch)); } void ParseInteractionTail(const std::string& tail, @@ -1192,11 +1053,11 @@ INPUT BuildAbsoluteMouseMoveInput(int x, int y) { } InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) { - ScopedDesktopHandle desktop; + ScopedInteractiveDesktop desktop; DesktopSwitchDetails desktop_switch; if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage, request.interactive_desktop, - &desktop.handle, + &desktop, &desktop_switch)) { const DWORD error = GetLastError(); return BuildInputFailure("switch_interactive_desktop_failed", @@ -1256,13 +1117,91 @@ InputInjectionResult InjectMouseInput(const SecureMouseRequest& request) { desktop_switch); } - return BuildInputSuccess(); + return BuildInputSuccess(std::move(desktop_switch)); } 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 { + ScopedInteractiveDesktop desktop; + HDC screen_dc = nullptr; + HDC mem_dc = nullptr; + HBITMAP dib = nullptr; + HGDIOBJ old_bitmap = nullptr; + void* bits = nullptr; + const char* error = nullptr; + DWORD error_code = ERROR_SUCCESS; + + ~SecureDesktopGdiResources() { + if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) { + SelectObject(mem_dc, old_bitmap); + } + if (dib != nullptr) DeleteObject(dib); + if (mem_dc != nullptr) DeleteDC(mem_dc); + if (screen_dc != nullptr) ReleaseDC(nullptr, screen_dc); + } + + bool Fail(const char* reason) { + error = reason; + error_code = GetLastError(); + if (error_code == ERROR_SUCCESS) error_code = ERROR_GEN_FAILURE; + return false; + } + + bool Initialize(const SecureCaptureRequest& request) { + const auto fallback = DesktopNameForInteractiveStage( + request.interactive_stage, request.interactive_desktop); + if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) { + return Fail("switch_interactive_desktop_failed"); + } + screen_dc = GetDC(nullptr); + if (screen_dc == nullptr) return Fail("get_dc_failed"); + mem_dc = CreateCompatibleDC(screen_dc); + if (mem_dc == nullptr) return Fail("create_dc_failed"); + BITMAPINFO bmi{}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = request.width; + bmi.bmiHeader.biHeight = -request.height; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + dib = CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); + if (dib == nullptr || bits == nullptr) return Fail("create_dib_failed"); + old_bitmap = SelectObject(mem_dc, dib); + if (old_bitmap == nullptr || old_bitmap == HGDI_ERROR) { + return Fail("select_bitmap_failed"); + } + 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"); + } + if (request.show_cursor) { + CURSORINFO cursor{}; + cursor.cbSize = sizeof(cursor); + if (GetCursorInfo(&cursor) && cursor.flags == CURSOR_SHOWING && + cursor.hCursor != nullptr) { + const int x = cursor.ptScreenPos.x - request.left; + const int y = cursor.ptScreenPos.y - request.top; + if (x >= -64 && y >= -64 && x < request.width + 64 && + y < request.height + 64) { + DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL); + } + } + } + // Complete GDI writes before libyuv reads the DIB's pixel memory. + GdiFlush(); + return true; + } +}; + std::vector CaptureSecureDesktopFrame( const SecureCaptureRequest& request, SecureCaptureBuffers* capture_buffers) { @@ -1270,87 +1209,21 @@ std::vector CaptureSecureDesktopFrame( return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers")); } - ScopedDesktopHandle desktop; - if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage, - request.interactive_desktop, - &desktop.handle)) { - const DWORD error = GetLastError(); - return BuildTextResponseBytes(BuildErrorJson( - "switch_interactive_desktop_failed", - error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE)); - } - - HDC screen_dc = GetDC(nullptr); - if (screen_dc == nullptr) { + SecureDesktopGdiResources capture; + if (!capture.Initialize(request) || !capture.Capture(request)) { return BuildTextResponseBytes( - BuildErrorJson("get_dc_failed", GetLastError())); - } - - HDC mem_dc = CreateCompatibleDC(screen_dc); - if (mem_dc == nullptr) { - const DWORD error = GetLastError(); - ReleaseDC(nullptr, screen_dc); - return BuildTextResponseBytes(BuildErrorJson("create_dc_failed", error)); - } - - BITMAPINFO bmi{}; - bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bmi.bmiHeader.biWidth = request.width; - bmi.bmiHeader.biHeight = -request.height; - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; - - void* bits = nullptr; - HBITMAP dib = - CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); - if (dib == nullptr || bits == nullptr) { - const DWORD error = GetLastError(); - DeleteDC(mem_dc); - ReleaseDC(nullptr, screen_dc); - return BuildTextResponseBytes(BuildErrorJson("create_dib_failed", error)); - } - - HGDIOBJ old_bitmap = SelectObject(mem_dc, dib); - if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc, - request.left, request.top, SRCCOPY | CAPTUREBLT)) { - const DWORD error = GetLastError(); - SelectObject(mem_dc, old_bitmap); - DeleteObject(dib); - DeleteDC(mem_dc); - ReleaseDC(nullptr, screen_dc); - return BuildTextResponseBytes(BuildErrorJson("bitblt_failed", error)); - } - - if (request.show_cursor) { - CURSORINFO cursor_info{}; - cursor_info.cbSize = sizeof(CURSORINFO); - if (GetCursorInfo(&cursor_info) && cursor_info.flags == CURSOR_SHOWING && - cursor_info.hCursor != nullptr) { - const int cursor_x = cursor_info.ptScreenPos.x - request.left; - const int cursor_y = cursor_info.ptScreenPos.y - request.top; - if (cursor_x >= -64 && cursor_y >= -64 && cursor_x < request.width + 64 && - cursor_y < request.height + 64) { - DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0, 0, - nullptr, DI_NORMAL); - } - } + BuildErrorJson(capture.error, capture.error_code)); } const size_t nv12_size = static_cast(request.width) * request.height * 3 / 2; capture_buffers->nv12_frame.resize(nv12_size); const int convert_result = libyuv::ARGBToNV12( - static_cast(bits), request.width * 4, + static_cast(capture.bits), request.width * 4, capture_buffers->nv12_frame.data(), request.width, capture_buffers->nv12_frame.data() + request.width * request.height, request.width, request.width, request.height); - SelectObject(mem_dc, old_bitmap); - DeleteObject(dib); - DeleteDC(mem_dc); - ReleaseDC(nullptr, screen_dc); - if (convert_result != 0) { return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed")); } @@ -1397,8 +1270,7 @@ void CloseSecureDesktopSharedCaptureResourcesLocked( capture_state->frame_view_size = 0; } -void SecureDesktopSharedCaptureThread( - SecureSharedCaptureState* capture_state) { +void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) { if (capture_state == nullptr) { return; } @@ -1421,79 +1293,65 @@ void SecureDesktopSharedCaptureThread( static_cast(request.width) * request.height * 3 / 2; std::vector nv12_frame(nv12_size); - ScopedDesktopHandle desktop; - if (!EnsureThreadInteractiveDesktopForStage(request.interactive_stage, - request.interactive_desktop, - &desktop.handle)) { - LOG_ERROR("Secure shared capture desktop switch failed, error={}", - GetLastError()); - return; - } - - HDC screen_dc = GetDC(nullptr); - if (screen_dc == nullptr) { - LOG_ERROR("Secure shared capture GetDC failed, error={}", GetLastError()); - return; - } - - HDC mem_dc = CreateCompatibleDC(screen_dc); - if (mem_dc == nullptr) { - LOG_ERROR("Secure shared capture CreateCompatibleDC failed, error={}", - GetLastError()); - ReleaseDC(nullptr, screen_dc); - return; - } - - BITMAPINFO bmi{}; - bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - bmi.bmiHeader.biWidth = request.width; - bmi.bmiHeader.biHeight = -request.height; - bmi.bmiHeader.biPlanes = 1; - bmi.bmiHeader.biBitCount = 32; - bmi.bmiHeader.biCompression = BI_RGB; - - void* bits = nullptr; - HBITMAP dib = - CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); - if (dib == nullptr || bits == nullptr) { - LOG_ERROR("Secure shared capture CreateDIBSection failed, error={}", - GetLastError()); - DeleteDC(mem_dc); - ReleaseDC(nullptr, screen_dc); - return; - } - - HGDIOBJ old_bitmap = SelectObject(mem_dc, dib); + std::unique_ptr capture; + ULONGLONG last_error_tick = 0; + auto stats_started = std::chrono::steady_clock::now(); + unsigned frame_count = 0; + double capture_ms = 0, convert_ms = 0, publish_ms = 0; + auto report_stats = [&](bool force) { + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - stats_started) + .count(); + if (elapsed <= 0 || (!force && elapsed < 5.0)) return; + 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{}", + frame_count / elapsed, capture_ms / frame_count, + convert_ms / frame_count, publish_ms / frame_count, request.width, + request.height); + } + stats_started = std::chrono::steady_clock::now(); + frame_count = 0; + capture_ms = convert_ms = publish_ms = 0; + }; while (!capture_state->stop_requested.load(std::memory_order_relaxed)) { const auto frame_started = std::chrono::steady_clock::now(); - if (BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc, - request.left, request.top, SRCCOPY | CAPTUREBLT)) { - if (request.show_cursor) { - CURSORINFO cursor_info{}; - cursor_info.cbSize = sizeof(CURSORINFO); - if (GetCursorInfo(&cursor_info) && - cursor_info.flags == CURSOR_SHOWING && - cursor_info.hCursor != nullptr) { - const int cursor_x = cursor_info.ptScreenPos.x - request.left; - const int cursor_y = cursor_info.ptScreenPos.y - request.top; - if (cursor_x >= -64 && cursor_y >= -64 && - cursor_x < request.width + 64 && - cursor_y < request.height + 64) { - DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0, - 0, nullptr, DI_NORMAL); - } + report_stats(false); + // A lock/credential/UAC transition can change the desktop before the + // service's next status poll. Release its DCs before binding the new one. + if (capture && !capture->desktop.IsInputDesktop()) { + capture.reset(); + } + if (!capture) { + capture = std::make_unique(); + if (!capture->Initialize(request)) { + const DWORD error = capture->error_code; + capture.reset(); + const ULONGLONG now = GetTickCount64(); + if (last_error_tick == 0 || now - last_error_tick >= 5000) { + LOG_WARN( + "Secure shared capture initialization failed; retrying, " + "error={}", + error); + last_error_tick = now; } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; } + } + if (capture->Capture(request)) { + const auto captured_at = std::chrono::steady_clock::now(); const int convert_result = libyuv::ARGBToNV12( - static_cast(bits), request.width * 4, + 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) { - auto* header = - reinterpret_cast( - frame_view); + const auto converted_at = std::chrono::steady_clock::now(); + auto* header = reinterpret_cast< + crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view); uint8_t* payload = frame_view + sizeof(*header); header->writing = 1; MemoryBarrier(); @@ -1509,25 +1367,41 @@ void SecureDesktopSharedCaptureThread( MemoryBarrier(); header->writing = 0; SetEvent(capture_state->frame_ready_event); + ++frame_count; + capture_ms += std::chrono::duration(captured_at - + frame_started) + .count(); + convert_ms += std::chrono::duration(converted_at - + captured_at) + .count(); + publish_ms += std::chrono::duration( + std::chrono::steady_clock::now() - converted_at) + .count(); } } else { - LOG_WARN("Secure shared capture BitBlt failed, error={}", GetLastError()); + const DWORD error = capture->error_code; + capture.reset(); + const ULONGLONG now = GetTickCount64(); + if (last_error_tick == 0 || now - last_error_tick >= 5000) { + LOG_WARN( + "Secure shared capture BitBlt failed; recreating capture, " + "error={}", + error); + last_error_tick = now; + } } const auto elapsed_ms = std::chrono::duration_cast( std::chrono::steady_clock::now() - frame_started) .count(); - if (elapsed_ms < interval_ms) { + const int retry_interval_ms = capture ? interval_ms : 100; + if (elapsed_ms < retry_interval_ms) { std::this_thread::sleep_for( - std::chrono::milliseconds(interval_ms - elapsed_ms)); + std::chrono::milliseconds(retry_interval_ms - elapsed_ms)); } } - - SelectObject(mem_dc, old_bitmap); - DeleteObject(dib); - DeleteDC(mem_dc); - ReleaseDC(nullptr, screen_dc); + report_stats(true); } std::vector StopSecureDesktopSharedCapture( @@ -1703,7 +1577,7 @@ std::vector HandleSecureInputHelperCommand( json["extended"] = extended; json["stage"] = interactive_stage; json["interactive_desktop"] = interactive_desktop; - json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW()); + json["desktop"] = inject_result.desktop.current_desktop; return BuildTextResponseBytes(json.dump()); } @@ -1731,7 +1605,7 @@ std::vector HandleSecureInputHelperCommand( json["flag"] = mouse_request.flag; json["stage"] = mouse_request.interactive_stage; json["interactive_desktop"] = mouse_request.interactive_desktop; - json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW()); + json["desktop"] = inject_result.desktop.current_desktop; return BuildTextResponseBytes(json.dump()); }