mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 09:53:05 +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");
|
||||
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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user