mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-05 13:42:38 +08:00
[fix] restore Windows remote control after RDP disconnects
This commit is contained in:
@@ -314,6 +314,12 @@ static int RunApplication(
|
|||||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||||
crossdesk::InitLogger(path_manager->GetLogPath().string());
|
crossdesk::InitLogger(path_manager->GetLogPath().string());
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
crossdesk::get_logger()->info(
|
||||||
|
"CrossDesk executable: {}",
|
||||||
|
std::filesystem::path(GetCurrentExecutablePathW()).u8string());
|
||||||
|
#endif
|
||||||
|
|
||||||
if (IsDaemonChild(argc, argv)) {
|
if (IsDaemonChild(argc, argv)) {
|
||||||
// child process: run render directly
|
// child process: run render directly
|
||||||
crossdesk::Render render;
|
crossdesk::Render render;
|
||||||
|
|||||||
@@ -5821,6 +5821,11 @@ void GuiApplication::SyncGallery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GuiApplication::Cleanup() {
|
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__)
|
#if defined(__linux__) && !defined(__OHOS__)
|
||||||
HeadlessConsole::Instance().BindSettings(nullptr, {}, {});
|
HeadlessConsole::Instance().BindSettings(nullptr, {}, {});
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -216,6 +216,10 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
|||||||
remaining_controller_id = id;
|
remaining_controller_id = id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#if _WIN32
|
||||||
|
windows_rdp_handoff_connected_->store(has_connected_controller,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
if (has_connected_controller) {
|
if (has_connected_controller) {
|
||||||
remote_client_id_ = remaining_controller_id;
|
remote_client_id_ = remaining_controller_id;
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ GuiRuntime::GuiRuntime()
|
|||||||
keyboard_(*this) {}
|
keyboard_(*this) {}
|
||||||
|
|
||||||
GuiRuntime::~GuiRuntime() {
|
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();
|
transfers_.Shutdown();
|
||||||
WaitForSessionCleanup();
|
WaitForSessionCleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -436,6 +436,12 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
|||||||
runtime->ApplyPrivacyAdmission(automatic_enable);
|
runtime->ApplyPrivacyAdmission(automatic_enable);
|
||||||
}
|
}
|
||||||
runtime->connection_status_[remote_id] = status;
|
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();
|
runtime->devices_.OnControllerConnectionsChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,6 +92,13 @@ struct PeerState {
|
|||||||
struct PlatformIntegrationState {
|
struct PlatformIntegrationState {
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
std::unique_ptr<ServiceQueryWorker> windows_service_worker_;
|
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.
|
// Published by the UI, read by transport callbacks without sharing a mutable string.
|
||||||
mutable std::mutex windows_input_stage_mutex_;
|
mutable std::mutex windows_input_stage_mutex_;
|
||||||
std::string windows_input_stage_;
|
std::string windows_input_stage_;
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include "interactive_state.h"
|
#include "interactive_state.h"
|
||||||
#include "service_host.h"
|
#include "service_host.h"
|
||||||
#include "unattended_config.h"
|
#include "unattended_config.h"
|
||||||
|
#include "rdp_session_handoff.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -126,11 +127,36 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
ResetLocalWindowsServiceState(false);
|
ResetLocalWindowsServiceState(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
windows_rdp_handoff_allowed_->store(
|
||||||
|
!unattended_host_, std::memory_order_relaxed);
|
||||||
|
|
||||||
if (!windows_service_worker_) {
|
if (!windows_service_worker_) {
|
||||||
windows_service_worker_ = std::make_unique<ServiceQueryWorker>(
|
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;
|
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(
|
result.response = QueryCrossDeskService(
|
||||||
command, command == "sas" ? kWindowsServiceSasTimeoutMs
|
command, command == "sas" ? kWindowsServiceSasTimeoutMs
|
||||||
: kWindowsServiceQueryTimeoutMs);
|
: kWindowsServiceQueryTimeoutMs);
|
||||||
@@ -277,6 +303,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
|
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();
|
if (windows_service_worker_) windows_service_worker_->Reset();
|
||||||
last_windows_service_status_tick_ = 0;
|
last_windows_service_status_tick_ = 0;
|
||||||
if (clear_pending_sas) {
|
if (clear_pending_sas) {
|
||||||
|
|||||||
@@ -860,9 +860,17 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
|||||||
const bool needs_wgc = usbmmidd &&
|
const bool needs_wgc = usbmmidd &&
|
||||||
capture_method_ == ScreenCaptureMethod::Auto &&
|
capture_method_ == ScreenCaptureMethod::Auto &&
|
||||||
dynamic_cast<WgcPluginCapturer*>(impl_.get()) == nullptr;
|
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 "
|
LOG_INFO("Windows capturer: reselecting capture backend "
|
||||||
"(headless={}, usbmmidd={})", headless, usbmmidd);
|
"(headless={}, usbmmidd={}, retry_dxgi={})", headless, usbmmidd,
|
||||||
|
needs_dxgi);
|
||||||
impl_->Destroy();
|
impl_->Destroy();
|
||||||
// Keep an instance around on failure so the next Start can retry.
|
// Keep an instance around on failure so the next Start can retry.
|
||||||
std::unique_ptr<ScreenCapturer> previous;
|
std::unique_ptr<ScreenCapturer> previous;
|
||||||
@@ -1229,9 +1237,22 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
|||||||
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||||
const int current_monitor = monitor_index_.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 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");
|
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
||||||
impl_->Stop();
|
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);
|
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
|
||||||
int ret = impl_->Start(show_cursor);
|
int ret = impl_->Start(show_cursor);
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
@@ -1282,7 +1303,7 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
|||||||
last_display_probe_tick_ = now;
|
last_display_probe_tick_ = now;
|
||||||
const bool topology_changed =
|
const bool topology_changed =
|
||||||
display_probe_requested_.exchange(false, std::memory_order_relaxed);
|
display_probe_requested_.exchange(false, std::memory_order_relaxed);
|
||||||
if (!IsWindowsPrivacyDesktopAvailable()) {
|
if (!IsCurrentSessionUserDesktopActive()) {
|
||||||
headless_recovery_.ShouldProvision(now, false, false, false);
|
headless_recovery_.ShouldProvision(now, false, false, false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1621,6 +1642,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
ULONGLONG capture_stage_started_tick = 0;
|
ULONGLONG capture_stage_started_tick = 0;
|
||||||
bool post_secure_restart_pending = false;
|
bool post_secure_restart_pending = false;
|
||||||
bool desktop_was_unavailable = false;
|
bool desktop_was_unavailable = false;
|
||||||
|
std::string last_desktop_probe_reason;
|
||||||
|
bool privacy_recovery_pending = false;
|
||||||
bool normal_backend_suspended = false;
|
bool normal_backend_suspended = false;
|
||||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||||
ULONGLONG last_post_secure_restart_tick = 0;
|
ULONGLONG last_post_secure_restart_tick = 0;
|
||||||
@@ -1664,11 +1687,22 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
while (running_.load(std::memory_order_relaxed)) {
|
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
|
// Remember automatic privacy even if the connection started while locked
|
||||||
// and its first enable attempt has already failed.
|
// and its first enable attempt has already failed.
|
||||||
const bool desktop_available = IsWindowsPrivacyDesktopAvailable();
|
if (!privacy_available) {
|
||||||
if (!desktop_available) {
|
privacy_recovery_pending = true;
|
||||||
desktop_was_unavailable = true;
|
|
||||||
if (privacy_) privacy_->SuspendForDesktop();
|
if (privacy_) privacy_->SuspendForDesktop();
|
||||||
}
|
}
|
||||||
if (paused_.load(std::memory_order_relaxed)) {
|
if (paused_.load(std::memory_order_relaxed)) {
|
||||||
@@ -1772,7 +1806,13 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
if (impl_) {
|
if (impl_) {
|
||||||
impl_->Stop();
|
impl_->Stop();
|
||||||
normal_backend_suspended = true;
|
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;
|
last_post_secure_restart_tick = now;
|
||||||
post_secure_restart_pending =
|
post_secure_restart_pending =
|
||||||
!RestartCaptureBackendAfterSecureDesktop();
|
!RestartCaptureBackendAfterSecureDesktop();
|
||||||
if (!post_secure_restart_pending) normal_backend_suspended = false;
|
if (!post_secure_restart_pending) {
|
||||||
if (!post_secure_restart_pending && privacy_)
|
normal_backend_suspended = false;
|
||||||
privacy_->ResumeAfterDesktopRecovery();
|
// 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 &&
|
if (!post_secure_restart_pending && !normal_backend_suspended &&
|
||||||
now >= next_cursor_update_tick) {
|
now >= next_cursor_update_tick) {
|
||||||
next_cursor_update_tick = now + 500;
|
next_cursor_update_tick = now + 500;
|
||||||
|
|||||||
@@ -54,35 +54,53 @@ inline bool IsCurrentProcessSession(DWORD session_id) {
|
|||||||
session_id == process_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;
|
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;
|
PWTSINFOEXW info = nullptr;
|
||||||
DWORD bytes = 0;
|
DWORD bytes = 0;
|
||||||
if (!WTSQuerySessionInformationW(
|
if (!WTSQuerySessionInformationW(
|
||||||
WTS_CURRENT_SERVER_HANDLE, session_id, WTSSessionInfoEx,
|
WTS_CURRENT_SERVER_HANDLE, session_id, WTSSessionInfoEx,
|
||||||
reinterpret_cast<LPWSTR*>(&info), &bytes)) {
|
reinterpret_cast<LPWSTR*>(&info), &bytes)) {
|
||||||
return false;
|
return DesktopProbeFailed(reason, "session_info_query_failed", GetLastError());
|
||||||
}
|
}
|
||||||
const bool unlocked =
|
const bool valid = info && bytes >= sizeof(WTSINFOEXW) && info->Level == 1;
|
||||||
info != nullptr && bytes >= sizeof(WTSINFOEXW) && info->Level == 1 &&
|
const DWORD state = valid ? info->Data.WTSInfoExLevel1.SessionState : 0xFFFFFFFF;
|
||||||
info->Data.WTSInfoExLevel1.SessionState == WTSActive &&
|
const DWORD flags = valid ? info->Data.WTSInfoExLevel1.SessionFlags : 0xFFFFFFFF;
|
||||||
info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_UNLOCK;
|
|
||||||
if (info != nullptr) WTSFreeMemory(info);
|
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.
|
// 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);
|
HDESK desktop = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
|
||||||
if (desktop == nullptr) return false;
|
if (desktop == nullptr)
|
||||||
// OpenInputDesktop also succeeds in disconnected RDP sessions. The name
|
return DesktopProbeFailed(reason, "open_input_desktop_failed", GetLastError());
|
||||||
// alone does not tell us whether this desktop can currently receive input.
|
// A minimized RDP connection can suppress desktop output while WTS still
|
||||||
const bool active = IsDesktopReceivingInput(desktop) &&
|
// reports Active/Unlocked. Do not treat its black WGC callbacks as recovery.
|
||||||
_wcsicmp(GetDesktopNameW(desktop).c_str(), L"Default") == 0;
|
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);
|
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
|
// 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 "session_helper_shared.h"
|
||||||
#include "unattended_config.h"
|
#include "unattended_config.h"
|
||||||
#include "unattended_policy.h"
|
#include "unattended_policy.h"
|
||||||
|
#include "rdp_session_handoff.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -1021,6 +1022,24 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
|||||||
if (!session_state_event_) return static_cast<int>(GetLastError());
|
if (!session_state_event_) return static_cast<int>(GetLastError());
|
||||||
last_sas_tick_ = 0;
|
last_sas_tick_ = 0;
|
||||||
active_session_id_ = WTSGetActiveConsoleSessionId();
|
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;
|
process_session_id_ = 0xFFFFFFFF;
|
||||||
input_desktop_error_code_ = 0;
|
input_desktop_error_code_ = 0;
|
||||||
session_helper_process_id_ = 0;
|
session_helper_process_id_ = 0;
|
||||||
@@ -1415,7 +1434,7 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
|||||||
const std::string response =
|
const std::string response =
|
||||||
GetNamedPipeClientSessionId(pipe, &client_session_id)
|
GetNamedPipeClientSessionId(pipe, &client_session_id)
|
||||||
? HandleIpcCommand(std::string(buffer, buffer + bytes_read),
|
? HandleIpcCommand(std::string(buffer, buffer + bytes_read),
|
||||||
client_session_id)
|
client_session_id, pipe)
|
||||||
: BuildErrorJson("client_session_query_failed", GetLastError());
|
: BuildErrorJson("client_session_query_failed", GetLastError());
|
||||||
DWORD bytes_written = 0;
|
DWORD bytes_written = 0;
|
||||||
if (pipe_deadline_detail::Transfer(
|
if (pipe_deadline_detail::Transfer(
|
||||||
@@ -2174,6 +2193,10 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
last_session_event_type_ = event_type;
|
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_;
|
++session_state_generation_;
|
||||||
last_session_event_session_id_ = session_id;
|
last_session_event_session_id_ = session_id;
|
||||||
const DWORD previous_session_id = active_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,
|
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));
|
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") {
|
if (normalized == "ping") {
|
||||||
return "{\"ok\":true,\"reply\":\"pong\"}";
|
return "{\"ok\":true,\"reply\":\"pong\"}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <unordered_set>
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
@@ -72,7 +73,7 @@ class CrossDeskServiceHost {
|
|||||||
void RefreshSessionHelperReportedState();
|
void RefreshSessionHelperReportedState();
|
||||||
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
||||||
std::string HandleIpcCommand(const std::string& command,
|
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 BuildStatusResponse(DWORD client_session_id);
|
||||||
std::string SendSecureAttentionSequence(DWORD client_session_id);
|
std::string SendSecureAttentionSequence(DWORD client_session_id);
|
||||||
struct SecureInputTarget {
|
struct SecureInputTarget {
|
||||||
@@ -109,6 +110,8 @@ class CrossDeskServiceHost {
|
|||||||
DWORD unattended_session_ = 0xFFFFFFFF;
|
DWORD unattended_session_ = 0xFFFFFFFF;
|
||||||
std::atomic<uint64_t> unattended_logoff_generation_{0};
|
std::atomic<uint64_t> unattended_logoff_generation_{0};
|
||||||
std::mutex state_mutex_;
|
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;
|
uint64_t session_state_generation_ = 0;
|
||||||
ULONGLONG last_session_transition_tick_ = 0;
|
ULONGLONG last_session_transition_tick_ = 0;
|
||||||
bool session_lock_state_known_ = false;
|
bool session_lock_state_known_ = false;
|
||||||
|
|||||||
@@ -611,7 +611,8 @@ function setup_targets()
|
|||||||
add_deps("rd_log", "path_manager", "windows_unattended_config")
|
add_deps("rd_log", "path_manager", "windows_unattended_config")
|
||||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||||
add_files("apps/desktop/src/platform/windows/service/main.cpp",
|
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",
|
add_files("apps/desktop/resources/windows/crossdesk_service.rc",
|
||||||
{defines = windows_resource_defines})
|
{defines = windows_resource_defines})
|
||||||
add_includedirs("apps/desktop/src/platform/windows/service",
|
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})
|
add_includedirs("apps/desktop/src", "apps/desktop/src/app", {public = true})
|
||||||
if is_os("windows") then
|
if is_os("windows") then
|
||||||
add_files("apps/desktop/src/platform/windows/daemon_backend.cpp")
|
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",
|
add_includedirs("apps/desktop/src/platform/windows/service",
|
||||||
"apps/desktop/src/platform/windows/screen_capturer")
|
"apps/desktop/src/platform/windows/screen_capturer")
|
||||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||||
|
|||||||
Reference in New Issue
Block a user