[fix] handle SAS secure desktop transitions and restore desktop capture promptly, refs #77

This commit is contained in:
dijunkun
2026-05-26 04:38:07 +08:00
parent 665f4e684c
commit 06c53fdc9c
7 changed files with 336 additions and 14 deletions
+29 -2
View File
@@ -82,12 +82,14 @@ HICON LoadTrayIcon() {
struct WindowsServiceInteractiveStatus {
bool available = false;
bool sas_secure_desktop_grace_active = false;
unsigned int error_code = 0;
std::string interactive_stage;
std::string error;
};
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
@@ -130,6 +132,8 @@ bool QueryWindowsServiceInteractiveStatus(
}
status->interactive_stage = json.value("interactive_stage", std::string());
status->sas_secure_desktop_grace_active =
json.value("sas_secure_desktop_grace_active", false);
if (ShouldNormalizeUnlockToUserDesktop(
json.value("interactive_lock_screen_visible", false),
@@ -1928,6 +1932,12 @@ void Render::HandleWindowsServiceIntegration() {
LOG_WARN("Remote SAS request failed: {}", response);
} else {
LOG_INFO("Remote SAS request forwarded to local Windows service");
optimistic_windows_secure_desktop_until_tick_ =
static_cast<uint32_t>(SDL_GetTicks()) +
kWindowsServiceSasSecureDesktopGraceMs;
local_service_status_received_ = true;
local_service_available_ = true;
local_interactive_stage_ = "secure-desktop";
}
last_windows_service_status_tick_ = 0;
force_broadcast = true;
@@ -1943,15 +1953,31 @@ void Render::HandleWindowsServiceIntegration() {
WindowsServiceInteractiveStatus status;
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
WindowsServiceInteractiveStatus broadcast_status = status;
const bool previous_secure_desktop_interaction =
IsSecureDesktopInteractionRequired(local_interactive_stage_);
const bool optimistic_secure_desktop_active =
optimistic_windows_secure_desktop_until_tick_ != 0 &&
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
now) > 0;
const bool keep_optimistic_secure_desktop =
status_ok && status.available && optimistic_secure_desktop_active &&
status.sas_secure_desktop_grace_active &&
status.interactive_stage == "user-desktop";
local_service_status_received_ =
status_ok || previous_secure_desktop_interaction;
local_service_available_ = status.available;
if (status.available) {
local_interactive_stage_ = status.interactive_stage;
if (keep_optimistic_secure_desktop) {
local_interactive_stage_ = "secure-desktop";
broadcast_status.interactive_stage = local_interactive_stage_;
} else {
local_interactive_stage_ = status.interactive_stage;
optimistic_windows_secure_desktop_until_tick_ = 0;
}
} else if (!previous_secure_desktop_interaction) {
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
}
if (status_ok) {
@@ -1990,7 +2016,7 @@ void Render::HandleWindowsServiceIntegration() {
last_logged_service_error_code = 0;
}
RemoteAction remote_action = BuildWindowsServiceStatusAction(status);
RemoteAction remote_action = BuildWindowsServiceStatusAction(broadcast_status);
std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
control_data_label_.c_str());
@@ -2009,6 +2035,7 @@ void Render::ResetLocalWindowsServiceState(bool clear_pending_sas) {
local_service_status_received_ = false;
local_service_available_ = false;
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
}
#endif
+1
View File
@@ -547,6 +547,7 @@ class Render {
std::string local_interactive_stage_;
uint32_t last_local_secure_input_block_log_tick_ = 0;
uint32_t last_windows_service_status_tick_ = 0;
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
#endif
// stream window render
+51 -6
View File
@@ -31,6 +31,7 @@ constexpr char kSecureDesktopMouseIpcCommandPrefix[] = "secure-input-mouse:";
constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe";
constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000;
constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000;
constexpr ULONGLONG kSasSecureDesktopGraceMs = 15000;
using SendSasFunction = VOID(WINAPI*)(BOOL);
@@ -1027,12 +1028,14 @@ int CrossDeskServiceHost::InitializeRuntime() {
session_helper_report_credential_ui_visible_ = false;
session_helper_report_unlock_ui_visible_ = false;
secure_input_helper_running_ = false;
sas_secure_desktop_seen_ = false;
last_sas_error_code_ = 0;
last_sas_success_ = false;
session_helper_started_at_tick_ = 0;
session_helper_report_state_age_ms_ = 0;
session_helper_report_uptime_ms_ = 0;
secure_input_helper_started_at_tick_ = 0;
sas_secure_desktop_until_tick_ = 0;
session_helper_process_handle_ = nullptr;
session_helper_stop_event_ = nullptr;
secure_input_helper_process_handle_ = nullptr;
@@ -1320,7 +1323,8 @@ bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
}
bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
return prelogin_ || secure_desktop_active_ || logon_ui_visible_ ||
return IsSasSecureDesktopGraceActiveLocked() || prelogin_ ||
secure_desktop_active_ || logon_ui_visible_ ||
session_helper_report_credential_ui_visible_ ||
session_helper_report_secure_desktop_active_ ||
session_helper_report_unlock_ui_visible_ ||
@@ -1328,6 +1332,30 @@ bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
session_helper_report_interactive_stage_ == "secure-desktop";
}
void CrossDeskServiceHost::UpdateSasSecureDesktopGraceLocked(
const std::string& observed_stage) {
if (sas_secure_desktop_until_tick_ == 0) {
sas_secure_desktop_seen_ = false;
return;
}
if (observed_stage == "credential-ui" || observed_stage == "secure-desktop" ||
observed_stage == "lock-screen") {
sas_secure_desktop_seen_ = true;
return;
}
if (sas_secure_desktop_seen_ && observed_stage == "user-desktop") {
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
}
}
bool CrossDeskServiceHost::IsSasSecureDesktopGraceActiveLocked() const {
return last_sas_success_ && sas_secure_desktop_until_tick_ != 0 &&
GetTickCount64() < sas_secure_desktop_until_tick_;
}
bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
DWORD target_session_id) const {
if (target_session_id == 0xFFFFFFFF) {
@@ -1339,6 +1367,12 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
}
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
if (IsSasSecureDesktopGraceActiveLocked() &&
(session_helper_report_interactive_stage_.empty() ||
session_helper_report_interactive_stage_ == "user-desktop")) {
return "secure-desktop";
}
if (!session_helper_report_interactive_stage_.empty()) {
return session_helper_report_interactive_stage_;
}
@@ -1818,6 +1852,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
json.value("interactive_stage", std::string());
session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull);
session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull);
UpdateSasSecureDesktopGraceLocked(session_helper_report_interactive_stage_);
}
void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
@@ -1947,6 +1982,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
std::string secure_input_helper_interactive_stage =
EscapeJsonString(secure_input_helper_interactive_stage_);
bool interactive_state_ready = session_helper_status_ok_;
const bool sas_secure_desktop_grace_active =
IsSasSecureDesktopGraceActiveLocked();
const char* interactive_state_source =
interactive_state_ready ? "session-helper" : "service-host";
const bool effective_session_locked = GetEffectiveSessionLockedLocked();
@@ -1960,21 +1997,23 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
bool unlock_ui_visible = interactive_state_ready
? session_helper_report_unlock_ui_visible_
: (logon_ui_visible_ || secure_desktop_active_);
unlock_ui_visible = unlock_ui_visible || sas_secure_desktop_grace_active;
bool interactive_secure_desktop_active =
interactive_state_ready ? session_helper_report_secure_desktop_active_
: secure_desktop_active_;
interactive_secure_desktop_active =
interactive_secure_desktop_active || sas_secure_desktop_grace_active;
bool interactive_logon_ui_visible =
interactive_state_ready ? session_helper_report_logon_ui_visible_
: logon_ui_visible_;
bool interactive_session_locked = effective_session_locked ||
interactive_lock_screen_visible ||
unlock_ui_visible;
unlock_ui_visible ||
sas_secure_desktop_grace_active;
std::string interactive_input_desktop = EscapeJsonString(
interactive_state_ready ? session_helper_report_input_desktop_
: input_desktop_name_);
std::string raw_interactive_stage = DetermineInteractiveStage(
interactive_lock_screen_visible, credential_ui_visible,
interactive_secure_desktop_active);
std::string raw_interactive_stage = ResolveInteractiveStageLocked();
std::string interactive_stage = EscapeJsonString(raw_interactive_stage);
std::ostringstream stream;
stream << "{\"ok\":true,\"service\":\"CrossDeskService\""
@@ -1996,6 +2035,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
<< (interactive_logon_ui_visible ? "true" : "false")
<< ",\"interactive_secure_desktop_active\":"
<< (interactive_secure_desktop_active ? "true" : "false")
<< ",\"sas_secure_desktop_grace_active\":"
<< (sas_secure_desktop_grace_active ? "true" : "false")
<< ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
<< ",\"credential_ui_visible\":"
<< (credential_ui_visible ? "true" : "false")
@@ -2100,10 +2141,14 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
SasResult result = SendSasNow();
{
std::lock_guard<std::mutex> lock(state_mutex_);
last_sas_tick_ = GetTickCount64();
const ULONGLONG now = GetTickCount64();
last_sas_tick_ = now;
last_sas_success_ = result.success;
last_sas_error_code_ = result.error_code;
last_sas_error_ = result.error;
sas_secure_desktop_until_tick_ =
result.success ? now + kSasSecureDesktopGraceMs : 0;
sas_secure_desktop_seen_ = false;
}
if (!result.success) {
+4
View File
@@ -56,6 +56,8 @@ class CrossDeskServiceHost {
bool GetEffectiveSessionLockedLocked() const;
bool IsHelperReportingLockScreenLocked() const;
bool HasSecureInputUiLocked() const;
void UpdateSasSecureDesktopGraceLocked(const std::string& observed_stage);
bool IsSasSecureDesktopGraceActiveLocked() const;
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
std::string ResolveInteractiveStageLocked() const;
void RefreshSessionHelperReportedState();
@@ -103,6 +105,7 @@ class CrossDeskServiceHost {
ULONGLONG session_helper_report_state_age_ms_ = 0;
ULONGLONG session_helper_report_uptime_ms_ = 0;
ULONGLONG secure_input_helper_started_at_tick_ = 0;
ULONGLONG sas_secure_desktop_until_tick_ = 0;
bool session_locked_ = false;
bool logon_ui_visible_ = false;
bool prelogin_ = false;
@@ -119,6 +122,7 @@ class CrossDeskServiceHost {
bool session_helper_report_unlock_ui_visible_ = false;
bool secure_input_helper_running_ = false;
bool console_mode_ = false;
bool sas_secure_desktop_seen_ = false;
DWORD last_sas_error_code_ = 0;
bool last_sas_success_ = false;
HANDLE session_helper_process_handle_ = nullptr;
+96 -6
View File
@@ -31,6 +31,10 @@ using crossdesk::get_logger;
using crossdesk::InitLogger;
using Json = nlohmann::json;
constexpr DWORD kSessionHelperStatePollMs = 1000;
std::atomic<HANDLE> g_session_helper_desktop_switch_event{nullptr};
struct InputDesktopInfo {
bool available = false;
DWORD error_code = 0;
@@ -134,6 +138,54 @@ void EnablePerMonitorDpiAwareness() {
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
}
void CALLBACK SessionHelperDesktopSwitchWinEventProc(
HWINEVENTHOOK, DWORD event, HWND, LONG, LONG, DWORD, DWORD) {
if (event != EVENT_SYSTEM_DESKTOPSWITCH) {
return;
}
HANDLE event_handle =
g_session_helper_desktop_switch_event.load(std::memory_order_relaxed);
if (event_handle != nullptr) {
SetEvent(event_handle);
}
}
void EnsureSessionHelperMessageQueue() {
MSG message{};
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
}
void PumpSessionHelperMessages() {
MSG message{};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
DWORD WaitForSessionHelperStateChange(HANDLE stop_event,
HANDLE desktop_switch_event) {
std::vector<HANDLE> wait_handles;
if (stop_event != nullptr) {
wait_handles.push_back(stop_event);
}
if (desktop_switch_event != nullptr) {
wait_handles.push_back(desktop_switch_event);
}
const DWORD handle_count = static_cast<DWORD>(wait_handles.size());
const DWORD wait_result = MsgWaitForMultipleObjects(
handle_count, wait_handles.empty() ? nullptr : wait_handles.data(), FALSE,
kSessionHelperStatePollMs, QS_ALLINPUT);
if (wait_result == WAIT_OBJECT_0 + handle_count) {
PumpSessionHelperMessages();
return WAIT_TIMEOUT;
}
return wait_result;
}
std::wstring Utf8ToWide(const std::string& value) {
if (value.empty()) {
return {};
@@ -308,8 +360,13 @@ void UpdateHelperState(HelperState* helper_state) {
IsLockAppRunningInCurrentSession(helper_state->session_id);
bool logon_ui_visible =
IsLogonUiRunningInCurrentSession(helper_state->session_id);
const bool secure_desktop_active =
const bool input_desktop_is_winlogon =
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
const bool inaccessible_secure_input_desktop =
!desktop_info.available &&
desktop_info.error_code == ERROR_ACCESS_DENIED && !logon_ui_visible;
const bool secure_desktop_active = input_desktop_is_winlogon ||
inaccessible_secure_input_desktop;
bool session_locked = false;
if (!QuerySessionLockState(helper_state->session_id, &session_locked)) {
session_locked =
@@ -1796,6 +1853,26 @@ int main(int argc, char* argv[]) {
helper_state.started_at_tick = GetTickCount64();
UpdateHelperState(&helper_state);
EnsureSessionHelperMessageQueue();
HANDLE desktop_switch_event = CreateEventW(nullptr, FALSE, FALSE, nullptr);
HWINEVENTHOOK desktop_switch_hook = nullptr;
if (desktop_switch_event != nullptr) {
g_session_helper_desktop_switch_event.store(
desktop_switch_event, std::memory_order_relaxed);
desktop_switch_hook = SetWinEventHook(
EVENT_SYSTEM_DESKTOPSWITCH, EVENT_SYSTEM_DESKTOPSWITCH, nullptr,
SessionHelperDesktopSwitchWinEventProc, 0, 0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS);
if (desktop_switch_hook == nullptr) {
LOG_WARN(
"SetWinEventHook(EVENT_SYSTEM_DESKTOPSWITCH) failed, error={}",
GetLastError());
}
} else {
LOG_WARN("CreateEventW failed for desktop switch watcher, error={}",
GetLastError());
}
std::thread ipc_thread(HelperIpcServerLoop, stop_event, current_session_id,
&helper_state);
@@ -1847,13 +1924,17 @@ int main(int argc, char* argv[]) {
last_stage = stage;
}
DWORD wait_result = stop_event != nullptr
? WaitForSingleObject(stop_event, 1000)
: WAIT_TIMEOUT;
if (wait_result == WAIT_OBJECT_0) {
DWORD wait_result =
WaitForSessionHelperStateChange(stop_event, desktop_switch_event);
if (stop_event != nullptr && wait_result == WAIT_OBJECT_0) {
break;
}
if (wait_result != WAIT_TIMEOUT && wait_result != WAIT_FAILED) {
if (wait_result == WAIT_FAILED) {
LOG_WARN("Session helper wait failed, error={}", GetLastError());
break;
}
if (wait_result != WAIT_TIMEOUT && wait_result != WAIT_OBJECT_0 &&
wait_result != WAIT_OBJECT_0 + 1) {
break;
}
}
@@ -1862,6 +1943,15 @@ int main(int argc, char* argv[]) {
ipc_thread.join();
}
if (desktop_switch_hook != nullptr) {
UnhookWinEvent(desktop_switch_hook);
}
g_session_helper_desktop_switch_event.store(nullptr,
std::memory_order_relaxed);
if (desktop_switch_event != nullptr) {
CloseHandle(desktop_switch_event);
}
if (stop_event != nullptr) {
CloseHandle(stop_event);
}
+149
View File
@@ -0,0 +1,149 @@
#include <filesystem>
#include <fstream>
#include <iostream>
#include <sstream>
#include <string>
#include "interactive_state.h"
namespace {
std::filesystem::path FindRepoRoot() {
std::filesystem::path current = std::filesystem::current_path();
while (!current.empty()) {
if (std::filesystem::exists(current / "xmake.lua") &&
std::filesystem::exists(
current / "src/service/windows/service_host.cpp")) {
return current;
}
current = current.parent_path();
}
return {};
}
std::string ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {};
}
std::ostringstream stream;
stream << file.rdbuf();
return stream.str();
}
bool ExpectContains(const char* name, const std::string& value,
const std::string& expected) {
if (value.find(expected) != std::string::npos) {
return true;
}
std::cerr << name << " missing expected text: " << expected << "\n";
return false;
}
bool ExpectNotContains(const char* name, const std::string& value,
const std::string& unexpected) {
if (value.find(unexpected) == std::string::npos) {
return true;
}
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
return false;
}
bool ExpectTrue(const char* name, bool value) {
if (value) {
return true;
}
std::cerr << name << " expected true\n";
return false;
}
} // namespace
int main() {
const std::filesystem::path repo_root = FindRepoRoot();
if (repo_root.empty()) {
std::cerr << "failed to locate repository root\n";
return 1;
}
const std::string control_bar =
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
const std::string render_h = ReadFile(repo_root / "src/gui/render.h");
const std::string service_host =
ReadFile(repo_root / "src/service/windows/service_host.cpp");
const std::string service_host_h =
ReadFile(repo_root / "src/service/windows/service_host.h");
const std::string session_helper =
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
bool ok = true;
ok &= ExpectTrue("secure desktop input routing",
crossdesk::IsSecureDesktopInteractionRequired(
"secure-desktop"));
ok &= ExpectNotContains("control_bar.cpp", control_bar,
"CanSendSecureAttentionSequence("
"props->remote_interactive_stage_)");
ok &= ExpectNotContains("control_bar.cpp", control_bar,
"ImGui::BeginDisabled();\n"
" }\n"
" if (ImGui::Selectable(sas_label.c_str()))");
ok &= ExpectNotContains("render.cpp", render, "sas_requires_lock_screen");
ok &= ExpectContains("render.h", render_h,
"optimistic_windows_secure_desktop_until_tick_");
ok &= ExpectContains("render.cpp", render,
"kWindowsServiceSasSecureDesktopGraceMs");
ok &= ExpectContains("render.cpp", render,
"status->sas_secure_desktop_grace_active");
ok &= ExpectContains("render.cpp", render,
"json.value(\"sas_secure_desktop_grace_active\", false)");
ok &= ExpectContains("render.cpp", render,
"status.sas_secure_desktop_grace_active");
ok &= ExpectContains("render.cpp", render,
"local_interactive_stage_ = \"secure-desktop\"");
ok &= ExpectContains("service_host.h", service_host_h,
"sas_secure_desktop_until_tick_");
ok &= ExpectContains("service_host.h", service_host_h,
"sas_secure_desktop_seen_");
ok &= ExpectContains("service_host.cpp", service_host,
"kSasSecureDesktopGraceMs");
ok &= ExpectContains("service_host.cpp", service_host,
"IsSasSecureDesktopGraceActiveLocked()");
ok &= ExpectContains("service_host.cpp", service_host,
"UpdateSasSecureDesktopGraceLocked("
"session_helper_report_interactive_stage_)");
ok &= ExpectContains("service_host.cpp", service_host,
"sas_secure_desktop_seen_ = true");
ok &= ExpectContains("service_host.cpp", service_host,
"sas_secure_desktop_until_tick_ = 0");
ok &= ExpectContains("service_host.cpp", service_host,
"sas_secure_desktop_until_tick_ =");
ok &= ExpectContains("service_host.cpp", service_host,
"now + kSasSecureDesktopGraceMs");
ok &= ExpectContains("service_host.cpp", service_host,
"\\\"sas_secure_desktop_grace_active\\\"");
ok &= ExpectContains("service_host.cpp", service_host,
"raw_interactive_stage = ResolveInteractiveStageLocked()");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"kSessionHelperStatePollMs = 1000");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"EVENT_SYSTEM_DESKTOPSWITCH");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"SetWinEventHook(");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"MsgWaitForMultipleObjects");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"WaitForSessionHelperStateChange(stop_event, "
"desktop_switch_event)");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"inaccessible_secure_input_desktop");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"desktop_info.error_code == ERROR_ACCESS_DENIED");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"secure_desktop_active = input_desktop_is_winlogon ||");
return ok ? 0 : 1;
}
+6
View File
@@ -54,6 +54,12 @@ function setup_targets()
set_default(false)
add_files("tests/windows_mouse_controller_safety_test.cpp")
target("windows_sas_guard_test")
set_kind("binary")
set_default(false)
add_includedirs("src/service/windows")
add_files("tests/windows_sas_guard_test.cpp")
target("display_popup_hover_state_test")
set_kind("binary")
set_default(false)