[fix] restore Windows remote control after RDP disconnects

This commit is contained in:
dijunkun
2026-10-16 18:20:45 +08:00
parent 40670de954
commit 0901d61533
14 changed files with 380 additions and 32 deletions
+6
View File
@@ -314,6 +314,12 @@ static int RunApplication(
auto path_manager = std::make_unique<crossdesk::PathManager>("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;
@@ -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
@@ -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;
@@ -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();
}
@@ -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();
}
@@ -92,6 +92,13 @@ struct PeerState {
struct PlatformIntegrationState {
#if _WIN32
std::unique_ptr<ServiceQueryWorker> windows_service_worker_;
// Lifecycle gate for interactive hosts; revoked before shutdown.
std::shared_ptr<std::atomic<bool>> windows_rdp_handoff_allowed_ =
std::make_shared<std::atomic<bool>>(false);
// Transport callbacks revoke this immediately; a stale UI snapshot cannot
// authorize a request after the last authenticated controller disconnects.
std::shared_ptr<std::atomic<bool>> windows_rdp_handoff_connected_ =
std::make_shared<std::atomic<bool>>(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_;
@@ -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<ServiceQueryWorker>(
[](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) {
@@ -860,9 +860,17 @@ int ScreenCapturerWin::Start(bool show_cursor) {
const bool needs_wgc = usbmmidd &&
capture_method_ == ScreenCaptureMethod::Auto &&
dynamic_cast<WgcPluginCapturer*>(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<ScreenCapturerDxgi*>(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<ScreenCapturer> 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<ScreenCapturerDxgi*>(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;
@@ -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<LPWSTR*>(&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
@@ -0,0 +1,148 @@
#include "rdp_session_handoff.h"
#include <WtsApi32.h>
#include <filesystem>
#include <vector>
namespace crossdesk {
namespace {
struct Handle {
HANDLE value = nullptr;
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
};
bool ReadTokenUser(HANDLE token, std::vector<unsigned char>& 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<unsigned char> user;
return OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token.value) &&
ReadTokenUser(token.value, user) &&
IsWellKnownSid(reinterpret_cast<TOKEN_USER*>(user.data())->User.Sid,
WinLocalSystemSid);
}
template <typename Value>
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<const Value*>(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<unsigned char> 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<TOKEN_USER*>(caller_user.data())->User.Sid,
reinterpret_cast<TOKEN_USER*>(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
@@ -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 <Windows.h>
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
@@ -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<int>(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<USHORT*>(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<std::mutex> 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<std::mutex> 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\"}";
}
@@ -14,6 +14,7 @@
#include <mutex>
#include <string>
#include <thread>
#include <unordered_set>
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<uint64_t> unattended_logoff_generation_{0};
std::mutex state_mutex_;
// Preserve RDP ownership when Windows clears the protocol after disconnect.
std::unordered_set<DWORD> rdp_sessions_;
uint64_t session_state_generation_ = 0;
ULONGLONG last_session_transition_tick_ = 0;
bool session_lock_state_known_ = false;
+4 -2
View File
@@ -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")