From 0901d61533450931f2ca93d624e936d6e0138cbe Mon Sep 17 00:00:00 2001 From: dijunkun Date: Fri, 16 Oct 2026 18:20:45 +0800 Subject: [PATCH] [fix] restore Windows remote control after RDP disconnects --- apps/desktop/src/app/main.cpp | 6 + .../src/gui/application/gui_application.cpp | 5 + .../src/gui/runtime/connection_runtime.cpp | 4 + apps/desktop/src/gui/runtime/gui_runtime.cpp | 5 + .../src/gui/runtime/peer_event_handler.cpp | 6 + apps/desktop/src/gui/runtime/runtime_state.h | 7 + .../gui/runtime/windows_service_runtime.cpp | 29 +++- .../screen_capturer/screen_capturer_win.cpp | 67 ++++++-- .../windows/service/interactive_desktop.h | 50 ++++-- .../windows/service/rdp_session_handoff.cpp | 148 ++++++++++++++++++ .../windows/service/rdp_session_handoff.h | 27 ++++ .../platform/windows/service/service_host.cpp | 47 +++++- .../platform/windows/service/service_host.h | 5 +- apps/desktop/xmake/targets.lua | 6 +- 14 files changed, 380 insertions(+), 32 deletions(-) create mode 100644 apps/desktop/src/platform/windows/service/rdp_session_handoff.cpp create mode 100644 apps/desktop/src/platform/windows/service/rdp_session_handoff.h diff --git a/apps/desktop/src/app/main.cpp b/apps/desktop/src/app/main.cpp index 3937c141..164c322b 100644 --- a/apps/desktop/src/app/main.cpp +++ b/apps/desktop/src/app/main.cpp @@ -314,6 +314,12 @@ static int RunApplication( auto path_manager = std::make_unique("CrossDesk"); crossdesk::InitLogger(path_manager->GetLogPath().string()); +#ifdef _WIN32 + crossdesk::get_logger()->info( + "CrossDesk executable: {}", + std::filesystem::path(GetCurrentExecutablePathW()).u8string()); +#endif + if (IsDaemonChild(argc, argv)) { // child process: run render directly crossdesk::Render render; diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index 4654a116..2022e8f6 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -5821,6 +5821,11 @@ void GuiApplication::SyncGallery() { } void GuiApplication::Cleanup() { +#if _WIN32 + windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed); + windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed); + if (windows_service_worker_) windows_service_worker_->Reset(); +#endif #if defined(__linux__) && !defined(__OHOS__) HeadlessConsole::Instance().BindSettings(nullptr, {}, {}); #endif diff --git a/apps/desktop/src/gui/runtime/connection_runtime.cpp b/apps/desktop/src/gui/runtime/connection_runtime.cpp index 8402876a..f611031e 100644 --- a/apps/desktop/src/gui/runtime/connection_runtime.cpp +++ b/apps/desktop/src/gui/runtime/connection_runtime.cpp @@ -216,6 +216,10 @@ void GuiRuntime::HandleServerControllerDisconnected( remaining_controller_id = id; } } +#if _WIN32 + windows_rdp_handoff_connected_->store(has_connected_controller, + std::memory_order_relaxed); +#endif } if (has_connected_controller) { remote_client_id_ = remaining_controller_id; diff --git a/apps/desktop/src/gui/runtime/gui_runtime.cpp b/apps/desktop/src/gui/runtime/gui_runtime.cpp index 322e9de2..f4e2fe52 100644 --- a/apps/desktop/src/gui/runtime/gui_runtime.cpp +++ b/apps/desktop/src/gui/runtime/gui_runtime.cpp @@ -27,6 +27,11 @@ GuiRuntime::GuiRuntime() keyboard_(*this) {} GuiRuntime::~GuiRuntime() { +#if _WIN32 + windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed); + windows_rdp_handoff_connected_->store(false, std::memory_order_relaxed); + if (windows_service_worker_) windows_service_worker_->Reset(); +#endif transfers_.Shutdown(); WaitForSessionCleanup(); } diff --git a/apps/desktop/src/gui/runtime/peer_event_handler.cpp b/apps/desktop/src/gui/runtime/peer_event_handler.cpp index ffd32ab0..0d2c9674 100644 --- a/apps/desktop/src/gui/runtime/peer_event_handler.cpp +++ b/apps/desktop/src/gui/runtime/peer_event_handler.cpp @@ -436,6 +436,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status, runtime->ApplyPrivacyAdmission(automatic_enable); } runtime->connection_status_[remote_id] = status; +#if _WIN32 + runtime->windows_rdp_handoff_connected_->store(std::any_of( + runtime->connection_status_.begin(), runtime->connection_status_.end(), + [](const auto& entry) { return entry.second == ConnectionStatus::Connected; }), + std::memory_order_relaxed); +#endif runtime->devices_.OnControllerConnectionsChanged(); } diff --git a/apps/desktop/src/gui/runtime/runtime_state.h b/apps/desktop/src/gui/runtime/runtime_state.h index 47252a1d..1d3bb3f7 100644 --- a/apps/desktop/src/gui/runtime/runtime_state.h +++ b/apps/desktop/src/gui/runtime/runtime_state.h @@ -92,6 +92,13 @@ struct PeerState { struct PlatformIntegrationState { #if _WIN32 std::unique_ptr windows_service_worker_; + // Lifecycle gate for interactive hosts; revoked before shutdown. + std::shared_ptr> windows_rdp_handoff_allowed_ = + std::make_shared>(false); + // Transport callbacks revoke this immediately; a stale UI snapshot cannot + // authorize a request after the last authenticated controller disconnects. + std::shared_ptr> windows_rdp_handoff_connected_ = + std::make_shared>(false); // 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_; 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 4bee9fab..41ed4c55 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 @@ -18,6 +18,7 @@ #include "interactive_state.h" #include "service_host.h" #include "unattended_config.h" +#include "rdp_session_handoff.h" #endif namespace crossdesk { @@ -126,11 +127,36 @@ void GuiRuntime::HandleWindowsServiceIntegration() { ResetLocalWindowsServiceState(false); return; } + windows_rdp_handoff_allowed_->store( + !unattended_host_, std::memory_order_relaxed); if (!windows_service_worker_) { windows_service_worker_ = std::make_unique( - [](const std::string& command) { + [allowed = windows_rdp_handoff_allowed_, + connected = windows_rdp_handoff_connected_, next_handoff_tick = ULONGLONG{0}, + last_handoff_response = std::string{}] + (const std::string& command) mutable { ServiceQueryResult result; + if (command == "status") { + const bool eligible = allowed->load(std::memory_order_relaxed) && + connected->load(std::memory_order_relaxed) && + IsCurrentProcessSessionDisconnected(); + const ULONGLONG now = GetTickCount64(); + if (!eligible) { + next_handoff_tick = 0; + last_handoff_response.clear(); + } else if (now >= next_handoff_tick && + allowed->load(std::memory_order_relaxed) && + connected->load(std::memory_order_relaxed)) { + next_handoff_tick = now + 5000; + const auto response = QueryCrossDeskService(kRdpHandoffCommand, 1000); + if (response != last_handoff_response) { + // A reply timeout does not imply that Windows rejected the switch. + LOG_INFO("RDP auto handoff service response: {}", response); + last_handoff_response = response; + } + } + } result.response = QueryCrossDeskService( command, command == "sas" ? kWindowsServiceSasTimeoutMs : kWindowsServiceQueryTimeoutMs); @@ -277,6 +303,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() { #if _WIN32 void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) { + windows_rdp_handoff_allowed_->store(false, std::memory_order_relaxed); if (windows_service_worker_) windows_service_worker_->Reset(); last_windows_service_status_tick_ = 0; if (clear_pending_sas) { 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 6ee08f04..84d86bd4 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 @@ -860,9 +860,17 @@ int ScreenCapturerWin::Start(bool show_cursor) { const bool needs_wgc = usbmmidd && capture_method_ == ScreenCaptureMethod::Auto && dynamic_cast(impl_.get()) == nullptr; - if (headless != was_headless || (headless && !backend_is_gdi) || needs_wgc) { + // A prior session may have fallen back while RDP was changing its output. + // Retry the preferred GPU backend on a new connection instead of retaining + // WGC/GDI indefinitely. Keep the headless and usbmmidd cursor policies. + const bool needs_dxgi = capture_method_ == ScreenCaptureMethod::Auto && + !headless && !usbmmidd && + dynamic_cast(impl_.get()) == nullptr; + if (headless != was_headless || (headless && !backend_is_gdi) || needs_wgc || + needs_dxgi) { LOG_INFO("Windows capturer: reselecting capture backend " - "(headless={}, usbmmidd={})", headless, usbmmidd); + "(headless={}, usbmmidd={}, retry_dxgi={})", headless, usbmmidd, + needs_dxgi); impl_->Destroy(); // Keep an instance around on failure so the next Start can retry. std::unique_ptr previous; @@ -1229,9 +1237,22 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() { const bool show_cursor = show_cursor_.load(std::memory_order_relaxed); const int current_monitor = monitor_index_.load(std::memory_order_relaxed); const bool usbmmidd = virtual_display_.active() || CapturesUsbmmiddOutput(impl_.get()); + const bool headless = DetectHeadlessDesktop(capture_method_, false, true); + headless_compat_.store(headless, std::memory_order_relaxed); LOG_INFO("Windows capturer: restarting capture backend after secure desktop"); impl_->Stop(); + if (capture_method_ == ScreenCaptureMethod::Auto && !headless && !usbmmidd && + dynamic_cast(impl_.get()) == nullptr) { + LOG_INFO("Windows capturer: desktop recovered; retrying DXGI before {}", + CaptureBackendName(impl_.get())); + if (StartPreferredBackend(headless, current_monitor, usbmmidd)) { + NotifyPrivacyCapture(true); + return true; + } + // Failed candidates retain the previous instance, which may still recover + // even if constructing a replacement failed during the display transition. + } last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed); int ret = impl_->Start(show_cursor); if (ret == 0) { @@ -1282,7 +1303,7 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) { last_display_probe_tick_ = now; const bool topology_changed = display_probe_requested_.exchange(false, std::memory_order_relaxed); - if (!IsWindowsPrivacyDesktopAvailable()) { + if (!IsCurrentSessionUserDesktopActive()) { headless_recovery_.ShouldProvision(now, false, false, false); return; } @@ -1621,6 +1642,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { ULONGLONG capture_stage_started_tick = 0; bool post_secure_restart_pending = false; bool desktop_was_unavailable = false; + std::string last_desktop_probe_reason; + bool privacy_recovery_pending = false; bool normal_backend_suspended = false; ULONGLONG post_secure_restart_deadline_tick = 0; ULONGLONG last_post_secure_restart_tick = 0; @@ -1664,11 +1687,22 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { }; while (running_.load(std::memory_order_relaxed)) { + // Capture follows this process's session, including an active RDP session. + // Privacy additionally requires the console desktop and must not gate video. + std::string desktop_probe_reason; + const bool desktop_available = IsCurrentSessionUserDesktopActive(&desktop_probe_reason); + if (desktop_probe_reason != last_desktop_probe_reason) { + LOG_INFO("Windows capturer: user desktop probe: {}", + desktop_available ? "available" : desktop_probe_reason); + last_desktop_probe_reason = desktop_probe_reason; + } + const bool privacy_available = + desktop_available && IsWindowsPrivacyDesktopAvailable(); + if (!desktop_available) desktop_was_unavailable = true; // Remember automatic privacy even if the connection started while locked // and its first enable attempt has already failed. - const bool desktop_available = IsWindowsPrivacyDesktopAvailable(); - if (!desktop_available) { - desktop_was_unavailable = true; + if (!privacy_available) { + privacy_recovery_pending = true; if (privacy_) privacy_->SuspendForDesktop(); } if (paused_.load(std::memory_order_relaxed)) { @@ -1772,7 +1806,13 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { if (impl_) { impl_->Stop(); normal_backend_suspended = true; - LOG_INFO("Windows capturer: normal backend suspended for secure desktop"); + DWORD process_session_id = 0xFFFFFFFF; + ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id); + LOG_INFO("Windows capturer: normal backend suspended, " + "user_desktop_available={}, secure_capture_active={}, " + "process_session_id={}, console_session_id={}", + desktop_available, status.capture_active, process_session_id, + WTSGetActiveConsoleSessionId()); } } @@ -1801,11 +1841,18 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { 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) { + normal_backend_suspended = false; + // Input callbacks may have suspended privacy between local probes. + privacy_recovery_pending = true; + } } } + if (privacy_recovery_pending && privacy_available && + !post_secure_restart_pending && !normal_backend_suspended) { + privacy_recovery_pending = false; + if (privacy_) privacy_->ResumeAfterDesktopRecovery(); + } if (!post_secure_restart_pending && !normal_backend_suspended && now >= next_cursor_update_tick) { next_cursor_update_tick = now + 500; diff --git a/apps/desktop/src/platform/windows/service/interactive_desktop.h b/apps/desktop/src/platform/windows/service/interactive_desktop.h index 6ea465fa..63339ccc 100644 --- a/apps/desktop/src/platform/windows/service/interactive_desktop.h +++ b/apps/desktop/src/platform/windows/service/interactive_desktop.h @@ -54,35 +54,53 @@ inline bool IsCurrentProcessSession(DWORD session_id) { session_id == process_session_id; } -inline bool IsCurrentSessionUnlocked() { +inline bool DesktopProbeFailed(std::string* reason, const char* stage, DWORD value) { + if (reason) *reason = std::string(stage) + ":" + std::to_string(value); + return false; +} + +inline bool IsCurrentSessionUnlocked(std::string* reason = nullptr) { + if (reason) reason->clear(); DWORD session_id = 0xFFFFFFFF; - if (!ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) return false; + if (!ProcessIdToSessionId(GetCurrentProcessId(), &session_id)) + return DesktopProbeFailed(reason, "session_id_query_failed", GetLastError()); PWTSINFOEXW info = nullptr; DWORD bytes = 0; if (!WTSQuerySessionInformationW( WTS_CURRENT_SERVER_HANDLE, session_id, WTSSessionInfoEx, reinterpret_cast(&info), &bytes)) { - return false; + return DesktopProbeFailed(reason, "session_info_query_failed", GetLastError()); } - const bool unlocked = - info != nullptr && bytes >= sizeof(WTSINFOEXW) && info->Level == 1 && - info->Data.WTSInfoExLevel1.SessionState == WTSActive && - info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_UNLOCK; + const bool valid = info && bytes >= sizeof(WTSINFOEXW) && info->Level == 1; + const DWORD state = valid ? info->Data.WTSInfoExLevel1.SessionState : 0xFFFFFFFF; + const DWORD flags = valid ? info->Data.WTSInfoExLevel1.SessionFlags : 0xFFFFFFFF; if (info != nullptr) WTSFreeMemory(info); - return unlocked; + if (!valid) return DesktopProbeFailed(reason, "session_info_invalid", bytes); + if (state != WTSActive) return DesktopProbeFailed(reason, "session_not_active", state); + if (flags != WTS_SESSIONSTATE_UNLOCK) + return DesktopProbeFailed(reason, "session_not_unlocked", flags); + return true; } -inline bool IsCurrentSessionUserDesktopActive() { +inline bool IsCurrentSessionUserDesktopActive(std::string* reason = nullptr) { // LockApp can be visible on Default while the session is still locked. - if (!IsCurrentSessionUnlocked()) return false; + if (!IsCurrentSessionUnlocked(reason)) return false; HDESK desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS); - if (desktop == nullptr) return false; - // OpenInputDesktop also succeeds in disconnected RDP sessions. The name - // alone does not tell us whether this desktop can currently receive input. - const bool active = IsDesktopReceivingInput(desktop) && - _wcsicmp(GetDesktopNameW(desktop).c_str(), L"Default") == 0; + if (desktop == nullptr) + return DesktopProbeFailed(reason, "open_input_desktop_failed", GetLastError()); + // A minimized RDP connection can suppress desktop output while WTS still + // reports Active/Unlocked. Do not treat its black WGC callbacks as recovery. + BOOL receiving_input = FALSE; + const bool queried = GetUserObjectInformationW( + desktop, UOI_IO, &receiving_input, sizeof(receiving_input), nullptr) != FALSE; + const DWORD error = queried ? ERROR_SUCCESS : GetLastError(); + const bool is_default = queried && receiving_input && + _wcsicmp(GetDesktopNameW(desktop).c_str(), L"Default") == 0; CloseDesktop(desktop); - return active; + if (!queried) return DesktopProbeFailed(reason, "desktop_input_query_failed", error); + if (!receiving_input) return DesktopProbeFailed(reason, "desktop_not_receiving_input", 0); + if (!is_default) return DesktopProbeFailed(reason, "desktop_not_default", 0); + return true; } // Use only on worker threads without windows or hooks. Restore the original diff --git a/apps/desktop/src/platform/windows/service/rdp_session_handoff.cpp b/apps/desktop/src/platform/windows/service/rdp_session_handoff.cpp new file mode 100644 index 00000000..8502e6b3 --- /dev/null +++ b/apps/desktop/src/platform/windows/service/rdp_session_handoff.cpp @@ -0,0 +1,148 @@ +#include "rdp_session_handoff.h" + +#include + +#include +#include + +namespace crossdesk { +namespace { +struct Handle { + HANDLE value = nullptr; + ~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); } +}; + +bool ReadTokenUser(HANDLE token, std::vector& data) { + DWORD bytes = 0; + GetTokenInformation(token, TokenUser, nullptr, 0, &bytes); + if (bytes == 0) return false; + data.resize(bytes); + return GetTokenInformation(token, TokenUser, data.data(), bytes, &bytes) != FALSE; +} + +bool RunningAsSystem() { + Handle token; + std::vector user; + return OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token.value) && + ReadTokenUser(token.value, user) && + IsWellKnownSid(reinterpret_cast(user.data())->User.Sid, + WinLocalSystemSid); +} + +template +bool QuerySessionValue(DWORD session, WTS_INFO_CLASS field, Value& value) { + LPWSTR data = nullptr; + DWORD bytes = 0; + if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session, field, + &data, &bytes)) return false; + const bool valid = data && bytes >= sizeof(Value); + if (valid) value = *reinterpret_cast(data); + WTSFreeMemory(data); + return valid; +} + +bool IsInstalledClient(HANDLE process) { + wchar_t service_path[32768]{}, client_path[32768]{}; + const DWORD length = GetModuleFileNameW(nullptr, service_path, ARRAYSIZE(service_path)); + DWORD capacity = ARRAYSIZE(client_path); + if (!length || length >= ARRAYSIZE(service_path) || + !QueryFullProcessImageNameW(process, 0, client_path, &capacity)) return false; + const auto expected = std::filesystem::path(service_path).parent_path() / L"crossdesk.exe"; + return _wcsicmp(expected.c_str(), client_path) == 0; +} + +bool VerifyOwner(HANDLE process, DWORD session) { + Handle caller, owner; + std::vector caller_user, owner_user; + DWORD token_session = 0xFFFFFFFF, bytes = 0; + if (!OpenProcessToken(process, TOKEN_QUERY, &caller.value) || + !GetTokenInformation(caller.value, TokenSessionId, &token_session, + sizeof(token_session), &bytes) || token_session != session || + !ReadTokenUser(caller.value, caller_user) || + !WTSQueryUserToken(session, &owner.value) || !ReadTokenUser(owner.value, owner_user)) + return false; + return EqualSid(reinterpret_cast(caller_user.data())->User.Sid, + reinterpret_cast(owner_user.data())->User.Sid) != FALSE; +} + +const char* ValidateSessions(DWORD session, bool observed_remote, + DWORD& console_session) { + console_session = WTSGetActiveConsoleSessionId(); + if (session == console_session) return nullptr; + if (console_session == 0 || console_session == 0xFFFFFFFF) + return "rdp_handoff_state_unavailable"; + + WTS_CONNECTSTATE_CLASS source_state{}, console_state{}; + USHORT protocol = 0; + wchar_t source_user = 0, console_user = 0; + // Only the first character is needed to distinguish an empty user name. + if (!QuerySessionValue(session, WTSConnectState, source_state) || + (!observed_remote && + !QuerySessionValue(session, WTSClientProtocolType, protocol)) || + !QuerySessionValue(session, WTSUserName, source_user) || !source_user || + !QuerySessionValue(console_session, WTSUserName, console_user) || + !QuerySessionValue(console_session, WTSConnectState, console_state)) + return "rdp_handoff_state_unavailable"; + if (source_state != WTSDisconnected || (!observed_remote && protocol != 2)) + return "rdp_handoff_session_not_disconnected"; + // Even a locked console with the same account must not be displaced. + if (console_user || (console_state != WTSConnected && console_state != WTSActive)) + return "rdp_handoff_console_busy"; + return nullptr; +} +} // namespace + +bool IsCurrentProcessSessionDisconnected() { + DWORD session = 0xFFFFFFFF; + WTS_CONNECTSTATE_CLASS state{}; + return ProcessIdToSessionId(GetCurrentProcessId(), &session) && + QuerySessionValue(session, WTSConnectState, state) && state == WTSDisconnected; +} + +RdpHandoffResult HandoffDisconnectedRdpSession(HANDLE pipe, DWORD client_session, + bool observed_rdp_session) { + if (!RunningAsSystem()) return {"rdp_handoff_requires_service"}; + if (client_session == 0 || client_session == 0xFFFFFFFF) + return {"rdp_handoff_client_denied", ERROR_ACCESS_DENIED}; + ULONG pid = 0, pipe_session = 0xFFFFFFFF; + if (!GetNamedPipeClientProcessId(pipe, &pid) || + !GetNamedPipeClientSessionId(pipe, &pipe_session)) + return {"rdp_handoff_client_denied", GetLastError()}; + if (pipe_session != client_session) + return {"rdp_handoff_client_denied", ERROR_ACCESS_DENIED}; + Handle process{OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | SYNCHRONIZE, FALSE, pid)}; + if (!process.value) return {"rdp_handoff_process_query_failed", GetLastError()}; + if (!IsInstalledClient(process.value)) + return {"rdp_handoff_client_path_mismatch", ERROR_ACCESS_DENIED}; + if (!VerifyOwner(process.value, client_session)) + return {"rdp_handoff_owner_unverified", ERROR_ACCESS_DENIED}; + + DWORD console_session = 0xFFFFFFFF; + if (const char* error = ValidateSessions(client_session, observed_rdp_session, + console_session)) + return {error}; + if (client_session == console_session) return {nullptr, 0, true}; + + // Recheck immediately before acting: WTS enumeration and token queries can + // span a logon, reconnect or console switch. Retain the process object by + // handle to prevent PID reuse and refuse a change of console destination. + if (WaitForSingleObject(process.value, 0) != WAIT_TIMEOUT || + !VerifyOwner(process.value, client_session)) + return {"rdp_handoff_client_denied", ERROR_ACCESS_DENIED}; + DWORD current_console = 0xFFFFFFFF; + if (const char* error = ValidateSessions(client_session, observed_rdp_session, + current_console)) + return {error}; + if (client_session == current_console) return {nullptr, 0, true}; + if (console_session != current_console) + return {"rdp_handoff_console_changed"}; + + // Ask Windows to perform the same session routing as tscon /dest:console. + // Asynchronous: the capture/status loops observe the resulting WTS change; + // the single service IPC thread must remain available for status and input. + wchar_t password[] = L""; + if (!WTSConnectSessionW(client_session, current_console, password, FALSE)) + return {"rdp_handoff_connect_failed", GetLastError()}; + return {}; +} +} // namespace crossdesk diff --git a/apps/desktop/src/platform/windows/service/rdp_session_handoff.h b/apps/desktop/src/platform/windows/service/rdp_session_handoff.h new file mode 100644 index 00000000..635bef47 --- /dev/null +++ b/apps/desktop/src/platform/windows/service/rdp_session_handoff.h @@ -0,0 +1,27 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-16 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _RDP_SESSION_HANDOFF_H_ +#define _RDP_SESSION_HANDOFF_H_ + +#include + +namespace crossdesk { +inline constexpr char kRdpHandoffCommand[] = "resume-rdp-session"; + +struct RdpHandoffResult { + const char* error = nullptr; + DWORD error_code = 0; + bool already_console = false; +}; + +// The session comes from GetNamedPipeClientSessionId, never request payloads. +RdpHandoffResult HandoffDisconnectedRdpSession(HANDLE pipe, DWORD client_session, + bool observed_rdp_session); +bool IsCurrentProcessSessionDisconnected(); +} // 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 276afcc5..663ab5fd 100644 --- a/apps/desktop/src/platform/windows/service/service_host.cpp +++ b/apps/desktop/src/platform/windows/service/service_host.cpp @@ -25,6 +25,7 @@ #include "session_helper_shared.h" #include "unattended_config.h" #include "unattended_policy.h" +#include "rdp_session_handoff.h" namespace crossdesk { @@ -1021,6 +1022,24 @@ int CrossDeskServiceHost::InitializeRuntime() { if (!session_state_event_) return static_cast(GetLastError()); last_sas_tick_ = 0; active_session_id_ = WTSGetActiveConsoleSessionId(); + // A service started inside an existing RDP login may not receive its earlier + // remote-connect notification. Seed currently identifiable RDP sessions. + PWTS_SESSION_INFOW sessions = nullptr; + DWORD session_count = 0; + if (WTSEnumerateSessionsW(WTS_CURRENT_SERVER_HANDLE, 0, 1, &sessions, &session_count)) { + for (DWORD index = 0; index < session_count; ++index) { + LPWSTR protocol = nullptr; + DWORD bytes = 0; + if (WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, sessions[index].SessionId, + WTSClientProtocolType, &protocol, &bytes)) { + if (protocol && bytes >= sizeof(USHORT) && + *reinterpret_cast(protocol) == 2) + rdp_sessions_.insert(sessions[index].SessionId); + WTSFreeMemory(protocol); + } + } + WTSFreeMemory(sessions); + } process_session_id_ = 0xFFFFFFFF; input_desktop_error_code_ = 0; session_helper_process_id_ = 0; @@ -1415,7 +1434,7 @@ void CrossDeskServiceHost::IpcServerLoop() { const std::string response = GetNamedPipeClientSessionId(pipe, &client_session_id) ? HandleIpcCommand(std::string(buffer, buffer + bytes_read), - client_session_id) + client_session_id, pipe) : BuildErrorJson("client_session_query_failed", GetLastError()); DWORD bytes_written = 0; if (pipe_deadline_detail::Transfer( @@ -2174,6 +2193,10 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, { std::lock_guard lock(state_mutex_); last_session_event_type_ = event_type; + if (event_type == WTS_REMOTE_CONNECT || event_type == WTS_REMOTE_DISCONNECT) + rdp_sessions_.insert(session_id); + else if (event_type == WTS_CONSOLE_CONNECT || event_type == WTS_SESSION_LOGOFF) + rdp_sessions_.erase(session_id); ++session_state_generation_; last_session_event_session_id_ = session_id; const DWORD previous_session_id = active_session_id_; @@ -2210,8 +2233,28 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, } std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command, - DWORD client_session_id) { + DWORD client_session_id, + HANDLE client_pipe) { std::string normalized = ToLower(Trim(command)); + if (normalized == kRdpHandoffCommand) { + bool observed_rdp = false; + { + std::lock_guard lock(state_mutex_); + observed_rdp = rdp_sessions_.count(client_session_id) != 0; + } + const auto result = HandoffDisconnectedRdpSession(client_pipe, client_session_id, + observed_rdp); + if (result.error) { + LOG_WARN("RDP handoff refused: session_id={}, error={}, code={}", + client_session_id, result.error, result.error_code); + return BuildErrorJson(result.error, result.error_code); + } + LOG_INFO("RDP handoff accepted: session_id={}, already_console={}", + client_session_id, result.already_console); + WakeSessionState(); + return Json{{"ok", true}, {"rdp_handoff", result.already_console ? + "already_console" : "requested"}, {"session_id", client_session_id}}.dump(); + } if (normalized == "ping") { return "{\"ok\":true,\"reply\":\"pong\"}"; } diff --git a/apps/desktop/src/platform/windows/service/service_host.h b/apps/desktop/src/platform/windows/service/service_host.h index 01c78a20..44deb50c 100644 --- a/apps/desktop/src/platform/windows/service/service_host.h +++ b/apps/desktop/src/platform/windows/service/service_host.h @@ -14,6 +14,7 @@ #include #include #include +#include namespace crossdesk { @@ -72,7 +73,7 @@ class CrossDeskServiceHost { void RefreshSessionHelperReportedState(); void RecordSessionEvent(DWORD event_type, DWORD session_id); std::string HandleIpcCommand(const std::string& command, - DWORD client_session_id); + DWORD client_session_id, HANDLE client_pipe); std::string BuildStatusResponse(DWORD client_session_id); std::string SendSecureAttentionSequence(DWORD client_session_id); struct SecureInputTarget { @@ -109,6 +110,8 @@ class CrossDeskServiceHost { DWORD unattended_session_ = 0xFFFFFFFF; std::atomic unattended_logoff_generation_{0}; std::mutex state_mutex_; + // Preserve RDP ownership when Windows clears the protocol after disconnect. + std::unordered_set rdp_sessions_; uint64_t session_state_generation_ = 0; ULONGLONG last_session_transition_tick_ = 0; bool session_lock_state_known_ = false; diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index e63d17d7..a0870619 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -611,7 +611,8 @@ function setup_targets() add_deps("rd_log", "path_manager", "windows_unattended_config") add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv") add_files("apps/desktop/src/platform/windows/service/main.cpp", - "apps/desktop/src/platform/windows/service/service_host.cpp") + "apps/desktop/src/platform/windows/service/service_host.cpp", + "apps/desktop/src/platform/windows/service/rdp_session_handoff.cpp") add_files("apps/desktop/resources/windows/crossdesk_service.rc", {defines = windows_resource_defines}) add_includedirs("apps/desktop/src/platform/windows/service", @@ -643,7 +644,8 @@ function setup_targets() add_includedirs("apps/desktop/src", "apps/desktop/src/app", {public = true}) if is_os("windows") then add_files("apps/desktop/src/platform/windows/daemon_backend.cpp") - add_files("apps/desktop/src/platform/windows/service/service_host.cpp") + add_files("apps/desktop/src/platform/windows/service/service_host.cpp", + "apps/desktop/src/platform/windows/service/rdp_session_handoff.cpp") add_includedirs("apps/desktop/src/platform/windows/service", "apps/desktop/src/platform/windows/screen_capturer") add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")