[fix] improve Windows secure desktop capture and input transitions

This commit is contained in:
kunkundi
2026-10-09 00:05:37 +08:00
parent 5004c91a6c
commit 3cfe7cbb80
26 changed files with 1376 additions and 582 deletions
@@ -665,11 +665,12 @@ bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
key_code, is_down, scan_code, extended) == 0;
}
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
int selected_display) {
if (mouse_controller_) {
mouse_controller_->SendMouseCommand(action, selected_display);
return mouse_controller_->SendMouseCommand(action, selected_display) == 0;
}
return false;
}
int SessionDeviceManager::SwitchDisplay(int display_id) {
@@ -71,7 +71,7 @@ public:
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void SendMouseCommand(const RemoteAction &action, int selected_display);
bool SendMouseCommand(const RemoteAction &action, int selected_display);
int SwitchDisplay(int display_id);
void ResetToInitialDisplay();
@@ -16,7 +16,8 @@
#include "runtime/gui_runtime.h"
#include "windows_key_metadata.h"
#if _WIN32
#include "interactive_state.h"
#include "desktop_transition_policy.h"
#include "interactive_desktop.h"
#include "service_host.h"
#endif
@@ -82,12 +83,11 @@ void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
}
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
const RemoteAction& action) {
bool is_down) {
return response.is_object() &&
response.value("error", std::string()) == "send_input_failed" &&
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
action.type == ControlType::keyboard &&
action.k.flag == KeyFlag::key_up;
!is_down;
}
#endif
@@ -218,40 +218,51 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
owner_.privacy_.SuspendForDesktop();
}
if (owner_.local_service_status_received_ &&
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, scan_code, extended, 1000);
const auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.key_value = static_cast<size_t>(key_code);
action.k.scan_code = scan_code;
action.k.extended = extended;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, action)) {
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return true;
}
if (owner_.is_server_mode_ || !owner_.WindowsInputStage().empty()) {
return DispatchDesktopInput(
[&] {
return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(),
owner_.windows_consent_ui_.load());
},
[&] {
SetLastError(ERROR_SUCCESS);
const bool sent = owner_.devices_.SendKeyboardCommand(
key_code, is_down, scan_code, extended);
return DesktopInputResult{
sent, IsDesktopTransitionInputError(GetLastError())};
},
[&]() -> DesktopInputResult {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, scan_code, extended, 1000);
const auto json = nlohmann::json::parse(response, nullptr, false);
if (!json.is_object() || !json.value("ok", false)) {
const std::string error =
json.is_object() ? json.value("error", "") : "";
if (IsDesktopInputSetupPending(error)) {
return {false, true};
}
if (IsTransientSecureDesktopInputFailure(json, is_down)) {
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return {true, false};
}
LogSecureDesktopInputBlocked(
&owner_.last_local_secure_input_block_log_tick_,
owner_.local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
"response={}",
key_code, is_down, response);
return false;
}
return true;
LogSecureDesktopInputBlocked(
&owner_.last_local_secure_input_block_log_tick_,
owner_.WindowsInputStage().c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, "
"is_down={}, "
"response={}",
key_code, is_down, response);
return {};
}
return {true, false};
});
}
#endif
// Recheck after the desktop query; key-up cleanup remains allowed while paused.
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
extended);
}
@@ -87,6 +87,15 @@ class GuiRuntime : protected gui_detail::GuiState {
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
#if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas);
std::string WindowsInputStage() const {
std::lock_guard lock(windows_input_stage_mutex_);
return windows_input_stage_;
}
void PublishWindowsInputStage() {
std::lock_guard lock(windows_input_stage_mutex_);
windows_input_stage_ = local_service_status_received_
? local_interactive_stage_ : std::string{};
}
#endif
#ifdef __APPLE__
@@ -26,7 +26,8 @@
#include "runtime/gui_runtime.h"
#if _WIN32
#include "interactive_state.h"
#include "desktop_transition_policy.h"
#include "interactive_desktop.h"
#include "service_host.h"
#endif
@@ -392,45 +393,60 @@ void PeerEventHandler::OnReceiveDataBuffer(
std::chrono::steady_clock::now();
}
#if _WIN32
if (runtime->local_service_status_received_ &&
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
if (remote_action.type == ControlType::mouse) {
int absolute_x = 0;
int absolute_y = 0;
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
runtime->selected_display_,
remote_action.m.x, remote_action.m.y,
&absolute_x, &absolute_y)) {
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
"mapping: display_index={}, x={}, y={}",
runtime->selected_display_, remote_action.m.x,
remote_action.m.y);
return;
}
if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) {
std::string response;
const bool delivered = DispatchDesktopInput(
[&] {
return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(),
runtime->windows_consent_ui_.load());
},
[&] {
SetLastError(ERROR_SUCCESS);
const bool sent = runtime->devices_.SendMouseCommand(
remote_action, runtime->selected_display_);
return DesktopInputResult{
sent, IsDesktopTransitionInputError(GetLastError())};
},
[&]() -> DesktopInputResult {
int absolute_x = 0;
int absolute_y = 0;
if (!BuildAbsoluteMousePosition(
runtime->devices_.display_info_list(),
runtime->selected_display_, remote_action.m.x,
remote_action.m.y, &absolute_x, &absolute_y)) {
LOG_WARN(
"Secure desktop mouse injection skipped, invalid display "
"mapping: display_index={}, x={}, y={}",
runtime->selected_display_, remote_action.m.x,
remote_action.m.y);
return {};
}
const std::string response = SendCrossDeskSecureDesktopMouseInput(
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LogSecureDesktopInputBlocked(
&runtime->last_local_secure_input_block_log_tick_, "local",
runtime->local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
"flag={}, response={}",
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), response);
}
return;
response = SendCrossDeskSecureDesktopMouseInput(
absolute_x, absolute_y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
const bool sent = json.is_object() && json.value("ok", false);
const std::string error =
json.is_object() ? json.value("error", "") : "";
return {sent, IsDesktopInputSetupPending(error)};
});
if (!delivered && !response.empty()) {
LogSecureDesktopInputBlocked(
&runtime->last_local_secure_input_block_log_tick_, "local",
runtime->WindowsInputStage().c_str());
LOG_WARN(
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
"flag={}, response={}",
remote_action.m.x, remote_action.m.y, remote_action.m.s,
static_cast<int>(remote_action.m.flag), response);
}
return;
}
#endif
if (remote_action.type == ControlType::mouse) {
runtime->devices_.SendMouseCommand(remote_action,
runtime->selected_display_);
runtime->selected_display_);
} else if (remote_action.type == ControlType::audio_capture) {
if (remote_action.a)
runtime->devices_.StartSpeakerCapturer();
@@ -29,6 +29,10 @@
#include "runtime/session_lifecycle.h"
#include "thumbnail.h"
#if _WIN32
#include "platform/windows/service/service_query_worker.h"
#endif
namespace crossdesk::gui_detail {
struct InfrastructureState {
@@ -83,6 +87,11 @@ struct PeerState {
// state so platform code does not appear to be part of the wire contract.
struct PlatformIntegrationState {
#if _WIN32
std::unique_ptr<ServiceQueryWorker> windows_service_worker_;
// 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_;
std::atomic<bool> windows_consent_ui_{false};
std::atomic<bool> pending_windows_service_sas_{false};
bool local_service_status_received_ = false;
bool local_service_available_ = false;
@@ -14,6 +14,7 @@
#if _WIN32
#include "interactive_desktop.h"
#include "desktop_transition_policy.h"
#include "interactive_state.h"
#include "service_host.h"
#endif
@@ -25,12 +26,13 @@ struct WindowsServiceInteractiveStatus {
bool available = false;
bool session_mismatch = false;
bool sas_secure_desktop_grace_active = false;
bool consent_ui_visible = false;
unsigned int error_code = 0;
std::string interactive_stage;
std::string error;
};
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
constexpr uint32_t kWindowsServiceStatusIntervalMs = 250;
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
@@ -51,15 +53,13 @@ BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) {
return action;
}
bool QueryWindowsServiceInteractiveStatus(
bool ParseWindowsServiceInteractiveStatus(const std::string& response,
WindowsServiceInteractiveStatus *status) {
if (status == nullptr) {
return false;
}
*status = WindowsServiceInteractiveStatus{};
const std::string response =
QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.is_object()) {
status->error = "invalid_service_status_json";
@@ -84,6 +84,7 @@ 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);
status->consent_ui_visible = json.value("consent_ui_visible", false);
if (ShouldNormalizeUnlockToUserDesktop(
json.value("interactive_lock_screen_visible", false),
@@ -125,13 +126,31 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
return;
}
bool force_broadcast = false;
if (!windows_service_worker_) {
windows_service_worker_ = std::make_unique<ServiceQueryWorker>(
[](const std::string& command) {
ServiceQueryResult result;
result.response = QueryCrossDeskService(
command, command == "sas" ? kWindowsServiceSasTimeoutMs
: kWindowsServiceQueryTimeoutMs);
if (command == "status") {
// A successful service reply can still carry a pre-unlock sample.
result.user_desktop_recovered = IsCurrentSessionUserDesktopActive();
}
return result;
});
}
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
if (privacy_.Engaged()) {
privacy_.Fail("Security shortcut requested; privacy screen will turn off");
}
const std::string response =
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
windows_service_worker_->RequestSas();
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
auto result = windows_service_worker_->Take();
if (result && result->command == "sas") {
const std::string& response = result->response;
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
LOG_WARN("Remote SAS request failed: {}", response);
@@ -145,21 +164,33 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
local_interactive_stage_ = "secure-desktop";
}
last_windows_service_status_tick_ = 0;
force_broadcast = true;
PublishWindowsInputStage();
if (local_service_status_received_) {
WindowsServiceInteractiveStatus optimistic;
optimistic.available = local_service_available_;
optimistic.interactive_stage = local_interactive_stage_;
const auto message = BuildWindowsServiceStatusAction(optimistic).to_json();
SendReliableDataFrame(peer_, message.data(), message.size(),
control_data_label_.c_str());
}
result.reset();
}
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force_broadcast && last_windows_service_status_tick_ != 0 &&
now - last_windows_service_status_tick_ <
if (last_windows_service_status_tick_ == 0 ||
now - last_windows_service_status_tick_ >=
kWindowsServiceStatusIntervalMs) {
return;
windows_service_worker_->RequestStatus();
last_windows_service_status_tick_ = now;
}
last_windows_service_status_tick_ = now;
if (!result) return;
WindowsServiceInteractiveStatus status;
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
const bool user_desktop_recovered =
!status.available && IsCurrentSessionUserDesktopActive();
const bool status_ok = ParseWindowsServiceInteractiveStatus(result->response, &status);
const bool user_desktop_recovered = PreferUserDesktopInput(
result->user_desktop_recovered, status.consent_ui_visible);
if (status.available || status.session_mismatch) {
windows_consent_ui_.store(status.available && status.consent_ui_visible);
}
if (user_desktop_recovered) status.interactive_stage = "user-desktop";
WindowsServiceInteractiveStatus broadcast_status = status;
const bool previous_secure_desktop_interaction =
@@ -169,7 +200,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
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 &&
!user_desktop_recovered && status_ok && status.available && optimistic_secure_desktop_active &&
status.sas_secure_desktop_grace_active &&
status.interactive_stage == "user-desktop";
local_service_status_received_ =
@@ -226,6 +257,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
last_logged_service_error_code = 0;
}
PublishWindowsInputStage();
RemoteAction remote_action =
BuildWindowsServiceStatusAction(broadcast_status);
std::string msg = remote_action.to_json();
@@ -239,14 +271,17 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
#if _WIN32
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
if (windows_service_worker_) windows_service_worker_->Reset();
last_windows_service_status_tick_ = 0;
if (clear_pending_sas) {
pending_windows_service_sas_.store(false, std::memory_order_relaxed);
}
local_service_status_received_ = false;
local_service_available_ = false;
windows_consent_ui_.store(false);
local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
PublishWindowsInputStage();
}
#endif
@@ -313,8 +313,10 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
const UINT sent = SendInputOnUserDesktop(input);
if (sent != 1) {
const DWORD error = GetLastError();
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
is_down, GetLastError());
is_down, error);
SetLastError(error);
return -1;
}
@@ -144,9 +144,11 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
const UINT sent = SendInputOnUserDesktop(ip);
if (sent != 1) {
const DWORD error = GetLastError();
LOG_WARN("SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
ip.mi.dx, ip.mi.dy, remote_action.m.s,
static_cast<int>(remote_action.m.flag), GetLastError());
static_cast<int>(remote_action.m.flag), error);
SetLastError(error);
return -1;
}
if (ip.mi.dwFlags & MOUSEEVENTF_LEFTDOWN) pressed_buttons_.fetch_or(1);
@@ -408,6 +408,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() {
}
status.active_session_id = json.value("active_session_id", 0xFFFFFFFFu);
status.consent_ui_visible = json.value("consent_ui_visible", false);
status.helper_running = json.value("secure_input_helper_running", false);
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
status.interactive_stage = json.value("interactive_stage", std::string());
@@ -425,11 +426,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() {
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
auto status = ReadSecureDesktopServiceStatus();
if (!status.service_available && IsCurrentSessionUserDesktopActive()) {
status.desktop_state_known = true;
status.capture_active = false;
status.interactive_stage = "user-desktop";
}
if (IsCurrentSessionUserDesktopActive()) status.OnUserDesktopObserved();
return status;
}
@@ -445,10 +442,11 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
GetCrossDeskSecureInputHelperPipeName(session_id);
std::string error;
DWORD code = 0;
// The legacy single-frame path includes a full GDI capture and a large
// response. Give it a separate deadline from the small control messages.
// Starting may have to join/release the old GPU producer first. Its reply
// cannot use the short deadline intended for lightweight control messages.
const DWORD timeout_ms =
command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0
(command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0 ||
command.rfind(kCrossDeskSecureInputCaptureStartCommandPrefix, 0) == 0)
? 1000
: kSecureDesktopHelperPipeTimeoutMs;
const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
@@ -579,8 +577,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
0, std::memory_order_relaxed);
const ULONGLONG elapsed_ms =
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
post_secure_desktop_drop_logged_.store(false,
std::memory_order_relaxed);
LOG_INFO(
"Windows capturer first normal frame after secure desktop: "
"reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
@@ -804,7 +800,6 @@ void ScreenCapturerWin::ResetPostSecureDesktopState() {
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
post_secure_desktop_waiting_for_frame_.store(false,
std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
SharedSecureDesktopCursorState().SetActive(false);
}
@@ -1237,6 +1232,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
impl_->Stop();
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
int ret = impl_->Start(show_cursor);
if (ret == 0) {
applied_show_cursor_ = show_cursor;
@@ -1403,7 +1399,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
target_session_id = secure_shared_session_id_;
}
if (secure_shared_capture_started_ &&
if ((secure_shared_capture_started_ || secure_shared_start_pending_) &&
target_session_id != 0xFFFFFFFF) {
std::vector<uint8_t> response;
std::string error_message;
@@ -1414,15 +1410,9 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
CloseSecureDesktopSharedFrame();
secure_shared_capture_started_ = false;
secure_shared_start_pending_ = false;
secure_shared_session_id_ = 0xFFFFFFFF;
secure_shared_left_ = 0;
secure_shared_top_ = 0;
secure_shared_width_ = 0;
secure_shared_height_ = 0;
secure_shared_fps_ = 0;
secure_shared_show_cursor_ = true;
secure_shared_stage_.clear();
secure_shared_desktop_.clear();
secure_shared_configuration_ = {};
}
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
@@ -1569,27 +1559,37 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
return false;
}
const SharedCaptureConfiguration configuration{left, top, width, height,
show_cursor, fps};
if (secure_shared_capture_started_ &&
secure_shared_session_id_ == session_id &&
secure_shared_left_ == left && secure_shared_top_ == top &&
secure_shared_width_ == width && secure_shared_height_ == height &&
secure_shared_stage_ == stage && secure_shared_desktop_ == desktop &&
secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps &&
secure_shared_configuration_ == configuration &&
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
return true;
}
StopSecureDesktopSharedCapture(secure_shared_session_id_);
if (secure_shared_session_id_ != session_id) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
} else {
// capture-start performs the replacement atomically in the helper. Avoid
// a separate stop round trip and keep retries idempotent after a timeout.
CloseSecureDesktopSharedFrame();
}
secure_shared_capture_started_ = false;
secure_shared_start_pending_ = true;
secure_shared_session_id_ = session_id;
const std::string command =
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
stage, desktop);
const std::string command = BuildSecureCaptureStartCommand(
left, top, width, height, show_cursor, fps, stage, desktop);
std::vector<uint8_t> response;
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
error_out)) {
secure_shared_start_pending_ =
error_out && IsPendingSecureCaptureStart(*error_out);
return false;
}
secure_shared_start_pending_ = false;
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
if (error_out != nullptr) {
@@ -1600,14 +1600,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
secure_shared_capture_started_ = true;
secure_shared_session_id_ = session_id;
secure_shared_left_ = left;
secure_shared_top_ = top;
secure_shared_width_ = width;
secure_shared_height_ = height;
secure_shared_show_cursor_ = show_cursor;
secure_shared_fps_ = fps;
secure_shared_stage_ = stage;
secure_shared_desktop_ = desktop;
secure_shared_configuration_ = configuration;
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
StopSecureDesktopSharedCapture(session_id);
@@ -1628,6 +1621,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
ULONGLONG capture_stage_started_tick = 0;
bool post_secure_restart_pending = false;
bool desktop_was_unavailable = false;
bool normal_backend_suspended = false;
ULONGLONG post_secure_restart_deadline_tick = 0;
ULONGLONG last_post_secure_restart_tick = 0;
ULONGLONG next_cursor_update_tick = 0;
@@ -1685,9 +1679,14 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
const ULONGLONG now = GetTickCount64();
const auto frame_started = std::chrono::steady_clock::now();
report_stats(false);
if (auto sample = status_poller.Take()) {
const auto previous = std::exchange(status, std::move(sample->status));
max_status_ms = (std::max)(max_status_ms, sample->query_ms);
auto sample = status_poller.Take();
if (sample || (desktop_available && status.capture_active)) {
auto latest = sample ? std::move(sample->status) : status;
// Recheck when applying a queued reply, not only when querying. A local
// unlock must never be undone by a delayed secure-desktop snapshot.
if (desktop_available) latest.OnUserDesktopObserved();
const auto previous = std::exchange(status, std::move(latest));
if (sample) max_status_ms = (std::max)(max_status_ms, sample->query_ms);
if (status.service_available != previous.service_available ||
status.error != previous.error) {
if (status.service_available) {
@@ -1718,8 +1717,12 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
report_stats(true);
// A restarted helper can leave our mapping/event handles alive, but
// their old producer is gone. Explicitly establish a fresh stream.
StopSecureDesktopSharedCapture(secure_shared_session_id_);
frame_schedule.Reset(now);
if (status.capture_active != previous.capture_active ||
status.active_session_id != previous.active_session_id ||
status.helper_process_id != previous.helper_process_id) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
frame_schedule.Reset(now);
}
const bool secure_capture_started =
!previous.capture_active && status.capture_active;
const bool secure_capture_ended =
@@ -1734,8 +1737,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
post_secure_restart_pending = false;
post_secure_desktop_waiting_for_frame_.store(
false, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false,
std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
} else if (secure_capture_ended) {
post_secure_restart_pending = true;
@@ -1744,8 +1745,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
last_post_secure_restart_tick = 0;
post_secure_desktop_waiting_for_frame_.store(
true, std::memory_order_relaxed);
post_secure_desktop_drop_logged_.store(false,
std::memory_order_relaxed);
post_secure_desktop_started_tick_.store(now,
std::memory_order_relaxed);
}
@@ -1763,28 +1762,52 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
last_post_secure_restart_tick = 0;
}
if ((!desktop_available || status.capture_active) &&
!normal_backend_suspended) {
// LockApp may return to Default while the session remains locked. Merely
// dropping callbacks leaves a second DXGI/GDI/WGC pipeline capturing and
// converting the same desktop alongside the helper. Stop it once, also
// when local lock detection precedes the service/helper startup.
std::lock_guard<std::mutex> lock(impl_mutex_);
if (impl_) {
impl_->Stop();
normal_backend_suspended = true;
LOG_INFO("Windows capturer: normal backend suspended for secure desktop");
}
}
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
CheckDisplayPresence(now);
MaybeAdoptVirtualDisplay();
CheckCaptureProgress(now);
// Normal capture was intentionally unavailable on the secure desktop.
// Do not schedule a second topology/stall rebuild during its recovery.
if (!post_secure_restart_pending && desktop_available &&
!normal_backend_suspended) {
CheckDisplayPresence(now);
MaybeAdoptVirtualDisplay();
CheckCaptureProgress(now);
}
if (post_secure_restart_pending && desktop_available) {
if (now >= post_secure_restart_deadline_tick) {
LOG_WARN(
"Windows capturer: capture backend restart after secure desktop "
"timed out");
post_secure_restart_pending = false;
"still unavailable; retrying");
// The normal backend is stopped. Retain recovery ownership instead
// of falling back to a stall probe on a thread that is not running.
post_secure_restart_deadline_tick =
now + kPostSecureDesktopRestartTimeoutMs;
} else if (last_post_secure_restart_tick == 0 ||
now - last_post_secure_restart_tick >=
kPostSecureDesktopRestartRetryMs) {
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 && now >= next_cursor_update_tick) {
if (!post_secure_restart_pending && !normal_backend_suspended &&
now >= next_cursor_update_tick) {
next_cursor_update_tick = now + 500;
ApplyCursorCaptureSetting();
}
@@ -1835,7 +1858,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
status.interactive_stage, status.interactive_desktop, show_cursor,
shared_fps, &error_message);
if (!shared_ready) {
report_shared_error("start", error_message);
if (!secure_shared_start_pending_) report_shared_error("start", error_message);
frame_schedule.OnStartFailure(GetTickCount64());
} else if (!was_started) {
frame_schedule.Reset(GetTickCount64());
@@ -1851,8 +1874,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
frame_schedule.OnSharedFrame(GetTickCount64());
}
const bool frame_pending = shared_ready && !frame_delivered &&
IsPendingSecureDesktopFrame(error_message);
const bool frame_pending = secure_shared_start_pending_ ||
(shared_ready && !frame_delivered && IsPendingSecureDesktopFrame(error_message));
if (frame_pending) ++shared_waits;
if (shared_ready && !frame_delivered && !frame_pending) {
report_shared_error("read", error_message);
@@ -1905,6 +1928,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
}
}
// The shared producer owns frame pacing. Waiting again here can miss the
// next publication (and overwrite a pending frame) due to timer rounding.
if (shared_ready && (frame_delivered || frame_pending)) continue;
const auto elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - frame_started)
@@ -25,6 +25,7 @@
#include "privacy_controller.h"
// Relative: platform include directories stay private to the capture target.
#include "../virtual_display/virtual_display_provisioner.h"
#include "../service/shared_capture_configuration.h"
#include "headless_recovery.h"
namespace crossdesk {
@@ -117,7 +118,6 @@ class ScreenCapturerWin : public ScreenCapturer {
std::atomic<int> monitor_index_{0};
std::atomic<bool> secure_desktop_capture_active_{false};
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
std::atomic<bool> post_secure_desktop_drop_logged_{false};
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
std::thread secure_capture_thread_;
HANDLE secure_frame_mapping_ = nullptr;
@@ -125,15 +125,9 @@ class ScreenCapturerWin : public ScreenCapturer {
uint8_t* secure_frame_view_ = nullptr;
size_t secure_frame_view_size_ = 0;
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
int secure_shared_left_ = 0;
int secure_shared_top_ = 0;
int secure_shared_width_ = 0;
int secure_shared_height_ = 0;
int secure_shared_fps_ = 0;
bool secure_shared_show_cursor_ = true;
std::string secure_shared_stage_;
std::string secure_shared_desktop_;
SharedCaptureConfiguration secure_shared_configuration_;
bool secure_shared_capture_started_ = false;
bool secure_shared_start_pending_ = false;
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
void BuildCanonicalFromImpl();
@@ -0,0 +1,160 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_DXGI_CAPTURE_H_
#define _SECURE_DESKTOP_DXGI_CAPTURE_H_
#include <Windows.h>
#include <d3d11.h>
#include <dxgi1_2.h>
#include <libyuv.h>
#include <wrl/client.h>
#include <cstdint>
#include <vector>
namespace crossdesk {
// Owned by the SYSTEM helper's capture thread, after binding its input desktop.
// Destroy before switching desktops. The uncomposited pixels stay valid on a
// timeout, so a static lock screen and a moving separate cursor keep their
// requested cadence without another GPU readback or a GDI screenshot.
class SecureDesktopDxgiCapture {
public:
HRESULT Initialize(int left, int top, int width, int height) {
if (width <= 0 || height <= 0) return E_INVALIDARG;
Microsoft::WRL::ComPtr<IDXGIFactory1> factory;
HRESULT hr = CreateDXGIFactory1(IID_PPV_ARGS(factory.GetAddressOf()));
if (FAILED(hr)) return hr;
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
for (UINT a = 0; SUCCEEDED(factory->EnumAdapters1(
a, adapter.ReleaseAndGetAddressOf())); ++a) {
Microsoft::WRL::ComPtr<IDXGIOutput> output;
for (UINT o = 0; SUCCEEDED(adapter->EnumOutputs(
o, output.ReleaseAndGetAddressOf())); ++o) {
DXGI_OUTPUT_DESC desc{};
if (FAILED(output->GetDesc(&desc)) || !desc.AttachedToDesktop) continue;
const RECT& bounds = desc.DesktopCoordinates;
if (left < bounds.left || top < bounds.top ||
static_cast<int64_t>(left) + width > bounds.right ||
static_cast<int64_t>(top) + height > bounds.bottom) continue;
hr = D3D11CreateDevice(
adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN, nullptr,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, nullptr, 0, D3D11_SDK_VERSION,
device_.GetAddressOf(), nullptr, context_.GetAddressOf());
if (FAILED(hr)) return hr;
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
hr = output.As(&output1);
if (FAILED(hr)) return hr;
hr = output1->DuplicateOutput(device_.Get(), duplication_.GetAddressOf());
if (FAILED(hr)) return hr;
output_desc_ = desc;
offset_x_ = left - bounds.left;
offset_y_ = top - bounds.top;
DXGI_OUTDUPL_DESC duplication_desc{};
duplication_->GetDesc(&duplication_desc);
switch (duplication_desc.Rotation) {
case DXGI_MODE_ROTATION_ROTATE90: rotation_ = libyuv::kRotate90; break;
case DXGI_MODE_ROTATION_ROTATE180: rotation_ = libyuv::kRotate180; break;
case DXGI_MODE_ROTATION_ROTATE270: rotation_ = libyuv::kRotate270; break;
default: rotation_ = libyuv::kRotate0; break;
}
return S_OK;
}
}
// Includes headless desktops and regions spanning more than one output.
return DXGI_ERROR_NOT_FOUND;
}
// S_FALSE means no first frame yet; the caller can use GDI until one arrives.
// On any failure the caller must discard this instance, including its cache.
HRESULT Capture() {
updated_ = false;
if (!duplication_) return E_UNEXPECTED;
Microsoft::WRL::ComPtr<IDXGIResource> resource;
DXGI_OUTDUPL_FRAME_INFO info{};
const HRESULT hr = duplication_->AcquireNextFrame(0, &info, &resource);
if (hr == DXGI_ERROR_WAIT_TIMEOUT) return pixels_.empty() ? S_FALSE : S_OK;
if (FAILED(hr)) return hr;
struct ReleaseFrame {
IDXGIOutputDuplication* duplication;
~ReleaseFrame() { duplication->ReleaseFrame(); }
} release{duplication_.Get()};
if (info.LastMouseUpdateTime.QuadPart != 0) {
cursor_embedded_ = !info.PointerPosition.Visible;
}
// Pointer-only updates leave the desktop image unchanged.
if (info.LastPresentTime.QuadPart == 0 && !pixels_.empty()) return S_OK;
Microsoft::WRL::ComPtr<ID3D11Texture2D> texture;
HRESULT result = resource.As(&texture);
if (FAILED(result)) return result;
D3D11_TEXTURE2D_DESC desc{};
texture->GetDesc(&desc);
const bool swap_axes = rotation_ == libyuv::kRotate90 ||
rotation_ == libyuv::kRotate270;
const UINT logical_width = swap_axes ? desc.Height : desc.Width;
const UINT logical_height = swap_axes ? desc.Width : desc.Height;
const RECT& bounds = output_desc_.DesktopCoordinates;
if (desc.Format != DXGI_FORMAT_B8G8R8A8_UNORM ||
logical_width != static_cast<UINT>(bounds.right - bounds.left) ||
logical_height != static_cast<UINT>(bounds.bottom - bounds.top)) {
return DXGI_ERROR_ACCESS_LOST;
}
if (!staging_) {
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.MiscFlags = 0;
result = device_->CreateTexture2D(&desc, nullptr, staging_.GetAddressOf());
if (FAILED(result)) return result;
}
context_->CopyResource(staging_.Get(), texture.Get());
D3D11_MAPPED_SUBRESOURCE mapped{};
result = context_->Map(staging_.Get(), 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(result)) return result;
stride_ = static_cast<int>(logical_width) * 4;
pixels_.resize(static_cast<size_t>(stride_) * logical_height);
const int converted = libyuv::ARGBRotate(
static_cast<const uint8_t*>(mapped.pData), static_cast<int>(mapped.RowPitch),
pixels_.data(), stride_, static_cast<int>(desc.Width),
static_cast<int>(desc.Height), rotation_);
context_->Unmap(staging_.Get(), 0);
updated_ = converted == 0;
return converted == 0 ? S_OK : E_FAIL;
}
const uint8_t* pixels() const {
return pixels_.data() + static_cast<size_t>(offset_y_) * stride_ + offset_x_ * 4;
}
int stride() const { return stride_; }
bool updated() const { return updated_; }
const DXGI_OUTPUT_DESC& output() const { return output_desc_; }
void SetSoftwareCursor(bool software) { cursor_embedded_ = software; }
bool CursorEmbedded(const CURSORINFO& cursor) const {
return cursor_embedded_ && (cursor.flags & CURSOR_SHOWING) != 0 &&
MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) ==
output_desc_.Monitor;
}
private:
Microsoft::WRL::ComPtr<ID3D11Device> device_;
Microsoft::WRL::ComPtr<ID3D11DeviceContext> context_;
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
DXGI_OUTPUT_DESC output_desc_{};
libyuv::RotationMode rotation_ = libyuv::kRotate0;
int offset_x_ = 0, offset_y_ = 0, stride_ = 0;
bool cursor_embedded_ = false;
bool updated_ = false;
std::vector<uint8_t> pixels_;
};
} // namespace crossdesk
#endif // _SECURE_DESKTOP_DXGI_CAPTURE_H_
@@ -0,0 +1,65 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_FRAME_PACER_H_
#define _SECURE_DESKTOP_FRAME_PACER_H_
#include <Windows.h>
#include <algorithm>
#include <chrono>
#include <thread>
namespace crossdesk {
// Sleep's system-tick rounding can turn a 33ms frame into a 47ms frame.
// Request precise waits only on this capture thread, without changing the
// machine's timer resolution. Older Windows versions use a regular timer.
class SecureDesktopFramePacer {
public:
using Clock = std::chrono::steady_clock;
explicit SecureDesktopFramePacer(int fps)
: interval_(std::chrono::nanoseconds(
1000000000LL / (std::clamp)(fps > 0 ? fps : 30, 1, 1000))) {
constexpr DWORD kHighResolution = 0x00000002;
timer_ = CreateWaitableTimerExW(nullptr, nullptr, kHighResolution,
TIMER_MODIFY_STATE | SYNCHRONIZE);
if (!timer_) timer_ = CreateWaitableTimerW(nullptr, FALSE, nullptr);
}
~SecureDesktopFramePacer() {
if (timer_) CloseHandle(timer_);
}
SecureDesktopFramePacer(const SecureDesktopFramePacer&) = delete;
SecureDesktopFramePacer& operator=(const SecureDesktopFramePacer&) = delete;
void Wait(Clock::time_point frame_started) {
const auto now = Clock::now();
// Keep fractional frame periods and compensate for small scheduler delays.
// After a slow frame, resume now instead of issuing a burst of old frames.
if (deadline_ == Clock::time_point{}) deadline_ = frame_started;
deadline_ += interval_;
if (deadline_ <= now) {
deadline_ = now;
return;
}
LARGE_INTEGER due{};
const auto ticks = std::chrono::duration_cast<std::chrono::nanoseconds>(
deadline_ - now).count() / 100;
due.QuadPart = -(ticks > 0 ? ticks : 1);
if (timer_ && SetWaitableTimer(timer_, &due, 0, nullptr, nullptr, FALSE) &&
WaitForSingleObject(timer_, INFINITE) == WAIT_OBJECT_0) return;
std::this_thread::sleep_until(deadline_);
}
private:
HANDLE timer_ = nullptr;
Clock::duration interval_;
Clock::time_point deadline_{};
};
} // namespace crossdesk
#endif // _SECURE_DESKTOP_FRAME_PACER_H_
@@ -12,6 +12,12 @@
namespace crossdesk {
inline bool IsPendingSecureCaptureStart(const std::string& error) {
// Once a command was written, a lost response does not mean startup failed.
return error.rfind("pipe_read_failed:", 0) == 0 ||
error.rfind("pipe_write_failed:", 0) == 0;
}
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
return error == "frame_wait_timeout" ||
error == "shared_frame_write_in_progress" ||
@@ -47,4 +53,4 @@ class SecureDesktopFrameSchedule {
} // namespace crossdesk
#endif
#endif
@@ -0,0 +1,59 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SECURE_DESKTOP_NV12_CACHE_H_
#define _SECURE_DESKTOP_NV12_CACHE_H_
#include <libyuv.h>
#include <cstddef>
#include <cstdint>
#include <vector>
namespace crossdesk {
// One cache per capture desktop/resource lifetime. DXGI can report a pointer
// update or timeout without changing any pixels. Keep publishing those frames
// (and fresh cursor metadata) without converting the entire desktop again.
class SecureDesktopNv12Cache {
public:
bool Convert(const uint8_t* pixels, int stride, int width, int height,
bool pixels_changed) {
updated_ = false;
if (!pixels || width <= 0 || height <= 0 || (width & 1) || (height & 1) ||
stride < static_cast<int64_t>(width) * 4) {
valid_ = false;
return false;
}
if (valid_ && !pixels_changed && width == width_ && height == height_)
return true;
valid_ = false;
const size_t luma_size = static_cast<size_t>(width) * height;
pixels_.resize(luma_size + luma_size / 2);
if (libyuv::ARGBToNV12(pixels, stride, pixels_.data(), width,
pixels_.data() + luma_size, width, width,
height) != 0)
return false;
width_ = width;
height_ = height;
valid_ = updated_ = true;
return true;
}
bool updated() const { return updated_; }
const std::vector<uint8_t>& pixels() const { return pixels_; }
private:
bool valid_ = false;
bool updated_ = false;
int width_ = 0, height_ = 0;
std::vector<uint8_t> pixels_;
};
} // namespace crossdesk
#endif // _SECURE_DESKTOP_NV12_CACHE_H_
@@ -23,6 +23,7 @@ struct SecureDesktopServiceStatus {
bool service_available = false;
bool desktop_state_known = false;
bool capture_active = false;
bool consent_ui_visible = false;
bool helper_running = false;
uint32_t helper_process_id = 0;
uint32_t active_session_id = 0xFFFFFFFF;
@@ -31,6 +32,16 @@ struct SecureDesktopServiceStatus {
std::string interactive_desktop;
std::string error;
void OnUserDesktopObserved() {
if (consent_ui_visible) return;
desktop_state_known = true;
capture_active = false;
helper_running = false;
helper_process_id = 0;
interactive_stage = "user-desktop";
interactive_desktop.clear();
}
void UpdateFrom(SecureDesktopServiceStatus latest) {
if (latest.service_available || latest.desktop_state_known) {
*this = std::move(latest);
@@ -108,4 +119,4 @@ class SecureDesktopStatusPoller {
} // namespace crossdesk
#endif
#endif
@@ -0,0 +1,60 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DESKTOP_TRANSITION_POLICY_H_
#define _DESKTOP_TRANSITION_POLICY_H_
#include <cstdint>
#include <string>
namespace crossdesk {
inline bool PreferUserDesktopInput(bool user_desktop_active,
bool consent_ui_visible) {
// Windows can display an elevated UAC dialog on Default when secure-desktop
// prompting is disabled. Preserve its authorized SYSTEM input path.
return user_desktop_active && !consent_ui_visible;
}
inline bool IsDesktopInputSetupPending(const std::string& error) {
// These failures explicitly precede injection; timeouts remain ambiguous.
return error == "secure_input_helper_not_ready" ||
error == "secure_input_not_active";
}
inline bool IsCurrentDesktopSample(uint64_t sample_started, uint64_t event_tick,
bool lock_known, bool session_locked,
bool sample_locked) {
// A reply received after a notification can still contain a pre-event sample.
return (event_tick == 0 || sample_started > event_tick) &&
(!lock_known || session_locked == sample_locked);
}
struct DesktopInputResult {
bool delivered = false;
// True only when the path explicitly refused the input before injection.
// A pipe timeout may have happened after a click/key was applied: never
// replay it.
bool not_injected = false;
};
template <typename ProbeUserDesktop, typename SendUser, typename SendSecure>
bool DispatchDesktopInput(ProbeUserDesktop probe_user, SendUser send_user,
SendSecure send_secure) {
const bool user = probe_user();
const auto result = user ? send_user() : send_secure();
if (result.delivered) return true;
if (!result.not_injected) return false;
const bool current_user = probe_user();
if (current_user == user) return false;
// Handle a switch between the probe and injection exactly once. Ordering is
// synchronous, so button/key releases cannot overtake their corresponding
// downs.
return (current_user ? send_user() : send_secure()).delivered;
}
} // namespace crossdesk
#endif // _DESKTOP_TRANSITION_POLICY_H_
@@ -14,6 +14,11 @@
namespace crossdesk {
inline bool IsDesktopTransitionInputError(DWORD error) {
return error == ERROR_ACCESS_DENIED ||
error == ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
}
inline std::wstring GetDesktopNameW(HDESK desktop) {
DWORD bytes_needed = 0;
if (desktop == nullptr) {
@@ -149,4 +154,4 @@ class ScopedInteractiveDesktop {
} // namespace crossdesk
#endif
#endif
@@ -17,6 +17,9 @@
#include <thread>
#include "interactive_state.h"
#include "interactive_desktop.h"
#include "desktop_transition_policy.h"
#include "named_pipe_deadline.h"
#include "path_manager.h"
#include "rd_log.h"
#include "session_helper_shared.h"
@@ -267,80 +270,14 @@ bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) {
std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
const std::string& command,
DWORD timeout_ms) {
constexpr DWORD kPipeConnectRetryDelayMs = 15;
const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms;
auto is_transient_pipe_error = [](DWORD error) {
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY ||
error == ERROR_SEM_TIMEOUT;
};
HANDLE pipe = INVALID_HANDLE_VALUE;
DWORD last_error = ERROR_SEM_TIMEOUT;
while (GetTickCount64() <= deadline_tick) {
const ULONGLONG now = GetTickCount64();
const DWORD wait_timeout =
deadline_tick > now
? static_cast<DWORD>((std::min)(
deadline_tick - now, static_cast<ULONGLONG>(MAXDWORD)))
: 0;
if (!WaitNamedPipeW(pipe_name.c_str(), wait_timeout)) {
const DWORD error = GetLastError();
last_error = error;
const ULONGLONG retry_tick = GetTickCount64();
if (is_transient_pipe_error(error) && retry_tick < deadline_tick) {
Sleep(static_cast<DWORD>((std::min)(
static_cast<ULONGLONG>(kPipeConnectRetryDelayMs),
deadline_tick - retry_tick)));
continue;
}
return BuildErrorJson("pipe_unavailable", error);
}
pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0,
nullptr, OPEN_EXISTING, 0, nullptr);
if (pipe != INVALID_HANDLE_VALUE) {
break;
}
const DWORD error = GetLastError();
last_error = error;
const ULONGLONG retry_tick = GetTickCount64();
if (is_transient_pipe_error(error) && retry_tick < deadline_tick) {
Sleep(static_cast<DWORD>((std::min)(
static_cast<ULONGLONG>(kPipeConnectRetryDelayMs),
deadline_tick - retry_tick)));
continue;
}
return BuildErrorJson("pipe_connect_failed", error);
std::vector<uint8_t> response;
std::string error;
DWORD code = ERROR_SUCCESS;
if (!QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
&response, &error, &code)) {
return BuildErrorJson(error.c_str(), code);
}
if (pipe == INVALID_HANDLE_VALUE) {
return BuildErrorJson("pipe_unavailable", last_error);
}
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
DWORD bytes_written = 0;
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
&bytes_written, nullptr)) {
std::string error = BuildErrorJson("pipe_write_failed", GetLastError());
CloseHandle(pipe);
return error;
}
char buffer[4096] = {0};
DWORD bytes_read = 0;
if (!ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr)) {
std::string error = BuildErrorJson("pipe_read_failed", GetLastError());
CloseHandle(pipe);
return error;
}
CloseHandle(pipe);
return std::string(buffer, buffer + bytes_read);
return std::string(response.begin(), response.end());
}
std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down,
@@ -1079,6 +1016,8 @@ int CrossDeskServiceHost::InitializeRuntime() {
return ERROR_INVALID_HANDLE;
started_at_tick_ = GetTickCount64();
session_state_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
if (!session_state_event_) return static_cast<int>(GetLastError());
last_sas_tick_ = 0;
active_session_id_ = WTSGetActiveConsoleSessionId();
process_session_id_ = 0xFFFFFFFF;
@@ -1136,6 +1075,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
last_session_event_session_id_ = active_session_id_;
RefreshSessionState();
EnsureSessionHelper();
session_state_thread_ = std::thread(&CrossDeskServiceHost::SessionStateLoop, this);
ipc_thread_ = std::thread(&CrossDeskServiceHost::IpcServerLoop, this);
if (!console_mode_) {
client_process_monitor_thread_ =
@@ -1159,8 +1099,13 @@ void CrossDeskServiceHost::ShutdownRuntime() {
if (ipc_thread_.joinable()) {
ipc_thread_.join();
}
if (session_state_thread_.joinable()) session_state_thread_.join();
StopSecureInputHelper();
StopSessionHelper();
if (session_state_event_) {
CloseHandle(session_state_event_);
session_state_event_ = nullptr;
}
if (lifetime_mutex_) {
ReleaseMutex(lifetime_mutex_);
CloseHandle(lifetime_mutex_);
@@ -1399,12 +1344,22 @@ void CrossDeskServiceHost::IpcServerLoop() {
LOG_WARN("Pipe security initialization failed, error={}", GetLastError());
}
HANDLE pipe = INVALID_HANDLE_VALUE;
pipe_deadline_detail::Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
if (!event.value) {
LOG_ERROR("CreateEventW for pipe failed, error={}", GetLastError());
return;
}
while (stop_event_ != nullptr &&
WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) {
HANDLE pipe = CreateNamedPipeW(
kCrossDeskServicePipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 4096, 4096, 0,
pipe_attributes);
// Keep the name registered between requests. Recreating the only instance
// made high-rate mouse clients observe FILE_NOT_FOUND and sleep on retries.
if (pipe == INVALID_HANDLE_VALUE) {
pipe = CreateNamedPipeW(
kCrossDeskServicePipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 4096, 4096, 0,
pipe_attributes);
}
if (pipe == INVALID_HANDLE_VALUE) {
DWORD error = GetLastError();
LOG_ERROR("CreateNamedPipeW failed, error={}", error);
@@ -1413,12 +1368,8 @@ void CrossDeskServiceHost::IpcServerLoop() {
}
OVERLAPPED overlapped{};
overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
if (overlapped.hEvent == nullptr) {
LOG_ERROR("CreateEventW for pipe failed, error={}", GetLastError());
CloseHandle(pipe);
break;
}
overlapped.hEvent = event.value;
ResetEvent(event.value);
BOOL connected = ConnectNamedPipe(pipe, &overlapped);
DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError();
@@ -1432,8 +1383,8 @@ void CrossDeskServiceHost::IpcServerLoop() {
if (!connected && connect_error != ERROR_IO_PENDING) {
LOG_WARN("ConnectNamedPipe failed, error={}", connect_error);
CloseHandle(overlapped.hEvent);
CloseHandle(pipe);
pipe = INVALID_HANDLE_VALUE;
continue;
}
@@ -1441,17 +1392,20 @@ void CrossDeskServiceHost::IpcServerLoop() {
HANDLE wait_handles[] = {stop_event_, overlapped.hEvent};
DWORD wait_result =
WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
if (wait_result == WAIT_OBJECT_0) {
if (wait_result != WAIT_OBJECT_0 + 1) {
CancelIoEx(pipe, &overlapped);
CloseHandle(overlapped.hEvent);
CloseHandle(pipe);
DWORD ignored = 0;
GetOverlappedResult(pipe, &overlapped, &ignored, TRUE);
break;
}
}
char buffer[1024] = {0};
DWORD bytes_read = 0;
if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) &&
DWORD transfer_error = ERROR_SUCCESS;
if (pipe_deadline_detail::Transfer(
pipe, overlapped.hEvent, false, buffer, sizeof(buffer) - 1,
GetTickCount64() + 1000, &bytes_read, &transfer_error) &&
bytes_read > 0) {
ULONG client_session_id = 0xFFFFFFFF;
// Obtain identity from Windows, never from caller-supplied JSON. A GUI
@@ -1462,43 +1416,80 @@ void CrossDeskServiceHost::IpcServerLoop() {
client_session_id)
: BuildErrorJson("client_session_query_failed", GetLastError());
DWORD bytes_written = 0;
WriteFile(pipe, response.data(), static_cast<DWORD>(response.size()),
&bytes_written, nullptr);
FlushFileBuffers(pipe);
if (pipe_deadline_detail::Transfer(
pipe, overlapped.hEvent, true, const_cast<char*>(response.data()),
static_cast<DWORD>(response.size()), GetTickCount64() + 1000,
&bytes_written, &transfer_error)) {
FlushFileBuffers(pipe);
}
}
DisconnectNamedPipe(pipe);
CloseHandle(overlapped.hEvent);
CloseHandle(pipe);
}
if (pipe != INVALID_HANDLE_VALUE) CloseHandle(pipe);
}
void CrossDeskServiceHost::RefreshSessionState() {
// WTS/process enumeration can be slow. Never hold the snapshot lock while
// querying Windows: status and input requests only need the last sample.
const ULONGLONG probe_started = GetTickCount64();
uint64_t generation = 0;
{
std::lock_guard lock(state_mutex_);
generation = session_state_generation_;
}
const DWORD session = WTSGetActiveConsoleSessionId();
DWORD process_session = 0xFFFFFFFF;
ProcessIdToSessionId(GetCurrentProcessId(), &process_session);
const bool logon_ui = IsLogonUiRunningInSession(session);
const bool consent_ui = IsConsentUiRunningInSession(session);
const InputDesktopInfo desktop = GetInputDesktopInfo();
std::wstring username;
const bool prelogin = !GetSessionUserName(session, &username) || username.empty();
bool locked = false;
const bool lock_known = QuerySessionLockState(session, &locked);
std::lock_guard<std::mutex> lock(state_mutex_);
if (generation != session_state_generation_ ||
session != WTSGetActiveConsoleSessionId()) return;
const DWORD previous_session_id = active_session_id_;
active_session_id_ = WTSGetActiveConsoleSessionId();
active_session_id_ = session;
if (active_session_id_ != previous_session_id) {
last_session_transition_tick_ = probe_started;
session_lock_state_known_ = false;
ResetSessionHelperReportedStateLocked("session_changed", 0);
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
}
DWORD process_session_id = 0xFFFFFFFF;
if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) {
process_session_id_ = process_session_id;
}
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
InputDesktopInfo desktop_info = GetInputDesktopInfo();
input_desktop_available_ = desktop_info.available;
input_desktop_error_code_ = desktop_info.error_code;
input_desktop_name_ = desktop_info.name;
process_session_id_ = process_session;
logon_ui_visible_ = logon_ui;
consent_ui_visible_ = consent_ui;
input_desktop_available_ = desktop.available;
input_desktop_error_code_ = desktop.error_code;
input_desktop_name_ = desktop.name;
secure_desktop_active_ =
_stricmp(input_desktop_name_.c_str(), "Winlogon") == 0;
std::wstring username;
bool username_available = GetSessionUserName(active_session_id_, &username);
prelogin_ = !username_available || username.empty();
if (!QuerySessionLockState(active_session_id_, &session_locked_)) {
session_username_ = WideToUtf8(username);
prelogin_ = prelogin;
if (lock_known) {
if (session_lock_state_known_ && session_locked_ != locked) {
last_session_transition_tick_ = probe_started;
ResetSessionHelperReportedStateLocked("lock_state_changed", 0);
}
session_locked_ = locked;
session_lock_state_known_ = true;
} else if (last_session_event_session_id_ == session &&
last_session_event_type_ == WTS_SESSION_LOCK) {
session_locked_ = true;
session_lock_state_known_ = true;
} else if (last_session_event_session_id_ == session &&
(last_session_event_type_ == WTS_SESSION_UNLOCK ||
last_session_event_type_ == WTS_SESSION_LOGON)) {
session_locked_ = false;
session_lock_state_known_ = true;
} else {
session_lock_state_known_ = false;
session_locked_ =
(logon_ui_visible_ || secure_desktop_active_) && !prelogin_;
}
@@ -1527,8 +1518,8 @@ void CrossDeskServiceHost::ResetSessionHelperReportedStateLocked(
}
bool CrossDeskServiceHost::GetEffectiveSessionLockedLocked() const {
return session_helper_status_ok_ ? session_helper_report_session_locked_
: session_locked_;
return session_lock_state_known_ || !session_helper_status_ok_
? session_locked_ : session_helper_report_session_locked_;
}
bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
@@ -1583,8 +1574,7 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
return false;
}
return HasSecureInputUiLocked() || (GetEffectiveSessionLockedLocked() &&
IsHelperReportingLockScreenLocked());
return HasSecureInputUiLocked() || GetEffectiveSessionLockedLocked();
}
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
@@ -1601,6 +1591,10 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
return session_helper_report_interactive_stage_;
}
// A lock notification can precede LockApp's first helper sample. The SYSTEM
// helper binds the actual input desktop on each operation.
if (GetEffectiveSessionLockedLocked()) return "secure-desktop";
const bool service_host_credential_ui_visible =
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
consent_ui_visible_,
@@ -2071,14 +2065,17 @@ void CrossDeskServiceHost::EnsureSessionHelper() {
void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
DWORD target_session_id = 0xFFFFFFFF;
bool helper_running = false;
uint64_t generation = 0;
{
std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = session_helper_session_id_;
helper_running = session_helper_running_;
generation = session_state_generation_;
}
if (!helper_running || target_session_id == 0xFFFFFFFF) {
std::lock_guard<std::mutex> lock(state_mutex_);
if (generation != session_state_generation_) return;
ResetSessionHelperReportedStateLocked("helper_not_running", 0);
return;
}
@@ -2089,12 +2086,14 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
Json json = Json::parse(response, nullptr, false);
if (json.is_discarded()) {
std::lock_guard<std::mutex> lock(state_mutex_);
if (generation != session_state_generation_) return;
ResetSessionHelperReportedStateLocked("invalid_helper_status_json", 0);
return;
}
if (!json.value("ok", false)) {
std::lock_guard<std::mutex> lock(state_mutex_);
if (generation != session_state_generation_) return;
const std::string error =
json.value("error", std::string("helper_status_failed"));
ResetSessionHelperReportedStateLocked(
@@ -2103,6 +2102,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
}
std::lock_guard<std::mutex> lock(state_mutex_);
if (generation != session_state_generation_) return;
if (target_session_id != active_session_id_ ||
target_session_id != session_helper_session_id_ ||
json.value("session_id", static_cast<DWORD>(0xFFFFFFFF)) !=
@@ -2111,6 +2111,20 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
ResetSessionHelperReportedStateLocked("helper_session_mismatch", 0);
return;
}
if (!IsCurrentDesktopSample(
json.value("sample_started_tick", uint64_t{0}),
last_session_transition_tick_, session_lock_state_known_,
session_locked_, json.value("session_locked", false))) {
if (session_helper_status_error_ != "stale_helper_state") {
LOG_DEBUG("Ignoring stale helper desktop sample: sample_tick={}, "
"transition_tick={}, helper_locked={}, session_locked={}",
json.value("sample_started_tick", uint64_t{0}),
last_session_transition_tick_, json.value("session_locked", false),
session_locked_);
}
ResetSessionHelperReportedStateLocked("stale_helper_state", 0);
return;
}
session_helper_status_ok_ = true;
session_helper_status_error_.clear();
session_helper_status_error_code_ = 0;
@@ -2152,6 +2166,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
{
std::lock_guard<std::mutex> lock(state_mutex_);
last_session_event_type_ = event_type;
++session_state_generation_;
last_session_event_session_id_ = session_id;
const DWORD previous_session_id = active_session_id_;
active_session_id_ = WTSGetActiveConsoleSessionId();
@@ -2160,42 +2175,26 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
}
DWORD process_session_id = 0xFFFFFFFF;
if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) {
process_session_id_ = process_session_id;
}
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
InputDesktopInfo desktop_info = GetInputDesktopInfo();
input_desktop_available_ = desktop_info.available;
input_desktop_error_code_ = desktop_info.error_code;
input_desktop_name_ = desktop_info.name;
secure_desktop_active_ =
_stricmp(input_desktop_name_.c_str(), "Winlogon") == 0;
std::wstring username;
bool username_available = GetSessionUserName(active_session_id_, &username);
prelogin_ = !username_available || username.empty();
if (!QuerySessionLockState(active_session_id_, &session_locked_)) {
if (session_id == active_session_id_ && event_type == WTS_SESSION_LOCK) {
if (session_id == active_session_id_) {
// Do not let an older helper sample override a session notification.
ResetSessionHelperReportedStateLocked("session_event", 0);
last_session_transition_tick_ = GetTickCount64();
if (event_type == WTS_SESSION_LOCK) {
session_locked_ = true;
} else if (session_id == active_session_id_ &&
(event_type == WTS_SESSION_UNLOCK ||
event_type == WTS_SESSION_LOGON)) {
session_lock_state_known_ = true;
}
if (event_type == WTS_SESSION_UNLOCK || event_type == WTS_SESSION_LOGON) {
session_locked_ = false;
} else if (logon_ui_visible_ || secure_desktop_active_) {
session_locked_ = !prelogin_;
session_lock_state_known_ = true;
sas_secure_desktop_until_tick_ = 0;
sas_secure_desktop_seen_ = false;
}
}
}
LOG_INFO("Session event: type={}, session_id={}, active_session_id={}",
SessionEventToString(event_type), session_id, active_session_id_);
EnsureSessionHelper();
if (!secure_desktop_active_ && !logon_ui_visible_ && !consent_ui_visible_) {
StopSecureInputHelper();
}
SessionEventToString(event_type), session_id, WTSGetActiveConsoleSessionId());
WakeSessionState();
}
std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
@@ -2228,17 +2227,22 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
return BuildErrorJson("unknown_command");
}
std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
void CrossDeskServiceHost::WakeSessionState() {
if (session_state_event_) SetEvent(session_state_event_);
}
void CrossDeskServiceHost::SessionStateLoop() {
HANDLE events[] = {stop_event_, session_state_event_};
while (WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) {
RefreshInteractiveState();
if (WaitForMultipleObjects(2, events, FALSE, 250) == WAIT_OBJECT_0) break;
}
}
void CrossDeskServiceHost::RefreshInteractiveState() {
ReapSecureInputHelper();
ReapSessionHelper();
RefreshSessionState();
{
std::lock_guard<std::mutex> lock(state_mutex_);
if (client_session_id == 0xFFFFFFFF ||
active_session_id_ != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
}
EnsureSessionHelper();
RefreshSessionHelperReportedState();
bool keep_secure_input_helper = false;
@@ -2257,11 +2261,7 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
launch_secure_input_helper =
keep_secure_input_helper &&
(!secure_input_helper_running_ ||
secure_input_helper_session_id_ != secure_input_target_session_id ||
secure_input_helper_interactive_stage_ !=
secure_input_interactive_stage ||
secure_input_helper_interactive_desktop_ !=
secure_input_interactive_desktop);
secure_input_helper_session_id_ != secure_input_target_session_id);
}
if (keep_secure_input_helper) {
@@ -2274,14 +2274,16 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
} else {
StopSecureInputHelper();
}
}
std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
std::lock_guard<std::mutex> lock(state_mutex_);
if (active_session_id_ != client_session_id) {
if (client_session_id == 0xFFFFFFFF ||
active_session_id_ != client_session_id ||
client_session_id != WTSGetActiveConsoleSessionId()) {
return BuildErrorJson("service_session_mismatch");
}
std::wstring username;
GetSessionUserName(active_session_id_, &username);
std::string username_utf8 = EscapeJsonString(WideToUtf8(username));
std::string username_utf8 = EscapeJsonString(session_username_);
std::string input_desktop = EscapeJsonString(input_desktop_name_);
std::string last_sas_error = EscapeJsonString(last_sas_error_);
std::string session_helper_last_error =
@@ -2467,12 +2469,11 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
std::string CrossDeskServiceHost::SendSecureAttentionSequence(
DWORD client_session_id) {
RefreshSessionState();
if (client_session_id == 0xFFFFFFFF ||
client_session_id != WTSGetActiveConsoleSessionId()) {
return BuildErrorJson("service_session_mismatch");
}
LOG_INFO("Received SAS request for session_id={}", active_session_id_);
LOG_INFO("Received SAS request for session_id={}", client_session_id);
SasResult result = SendSasNow();
{
std::lock_guard<std::mutex> lock(state_mutex_);
@@ -2489,114 +2490,68 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence(
if (!result.success) {
return BuildErrorJson(result.error.c_str(), result.error_code);
}
WakeSessionState();
return "{\"ok\":true,\"sent\":\"sas\"}";
}
std::string CrossDeskServiceHost::ResolveSecureInputTarget(
DWORD client_session_id, SecureInputTarget& target) {
bool helper_running = false;
bool can_inject = false;
{
std::lock_guard<std::mutex> lock(state_mutex_);
target.session_id = active_session_id_;
target.stage = ResolveInteractiveStageLocked();
target.desktop = ResolveInteractiveDesktopLocked(target.stage);
helper_running = secure_input_helper_running_ &&
secure_input_helper_session_id_ == target.session_id;
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
}
if (target.session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target.session_id != client_session_id ||
client_session_id != WTSGetActiveConsoleSessionId()) {
return BuildErrorJson("service_session_mismatch");
}
if (!can_inject) {
WakeSessionState();
return BuildErrorJson("secure_input_not_active");
}
if (!helper_running) {
WakeSessionState();
return BuildErrorJson("secure_input_helper_not_ready", ERROR_NOT_READY);
}
return {};
}
std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
DWORD client_session_id, int key_code, bool is_down,
uint32_t scan_code, bool extended) {
RefreshSessionState();
ReapSecureInputHelper();
EnsureSessionHelper();
RefreshSessionHelperReportedState();
DWORD target_session_id = 0xFFFFFFFF;
bool helper_running = false;
bool can_inject = false;
std::string interactive_stage;
std::string interactive_desktop;
{
std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = active_session_id_;
interactive_stage = ResolveInteractiveStageLocked();
interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage);
const bool helper_stage_matches =
secure_input_helper_interactive_stage_ == interactive_stage &&
secure_input_helper_interactive_desktop_ == interactive_desktop;
helper_running = secure_input_helper_running_ &&
secure_input_helper_session_id_ == target_session_id &&
helper_stage_matches;
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
}
if (target_session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target_session_id != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
if (!can_inject) {
return BuildErrorJson("secure_input_not_active");
}
if (!helper_running) {
StopSecureInputHelper();
if (!LaunchSecureInputHelper(target_session_id, interactive_stage,
interactive_desktop)) {
std::lock_guard<std::mutex> lock(state_mutex_);
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
secure_input_helper_last_error_code_);
}
}
SecureInputTarget target;
const auto error = ResolveSecureInputTarget(client_session_id, target);
if (!error.empty()) return error;
return QueryNamedPipeMessage(
GetCrossDeskSecureInputHelperPipeName(target_session_id),
GetCrossDeskSecureInputHelperPipeName(target.session_id),
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
extended, interactive_stage,
interactive_desktop),
extended, target.stage,
target.desktop),
1000);
}
std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(
DWORD client_session_id, int x, int y, int wheel, int flag) {
RefreshSessionState();
ReapSecureInputHelper();
EnsureSessionHelper();
RefreshSessionHelperReportedState();
DWORD target_session_id = 0xFFFFFFFF;
bool helper_running = false;
bool can_inject = false;
std::string interactive_stage;
std::string interactive_desktop;
{
std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = active_session_id_;
interactive_stage = ResolveInteractiveStageLocked();
interactive_desktop = ResolveInteractiveDesktopLocked(interactive_stage);
const bool helper_stage_matches =
secure_input_helper_interactive_stage_ == interactive_stage &&
secure_input_helper_interactive_desktop_ == interactive_desktop;
helper_running = secure_input_helper_running_ &&
secure_input_helper_session_id_ == target_session_id &&
helper_stage_matches;
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
}
if (target_session_id == 0xFFFFFFFF) {
return BuildErrorJson("no_active_console_session");
}
if (target_session_id != client_session_id) {
return BuildErrorJson("service_session_mismatch");
}
if (!can_inject) {
return BuildErrorJson("secure_input_not_active");
}
if (!helper_running) {
StopSecureInputHelper();
if (!LaunchSecureInputHelper(target_session_id, interactive_stage,
interactive_desktop)) {
std::lock_guard<std::mutex> lock(state_mutex_);
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
secure_input_helper_last_error_code_);
}
}
SecureInputTarget target;
const auto error = ResolveSecureInputTarget(client_session_id, target);
if (!error.empty()) return error;
return QueryNamedPipeMessage(
GetCrossDeskSecureInputHelperPipeName(target_session_id),
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage,
interactive_desktop),
GetCrossDeskSecureInputHelperPipeName(target.session_id),
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, target.stage,
target.desktop),
1000);
}
@@ -2831,18 +2786,40 @@ std::string QueryCrossDeskService(const std::string& command,
return QueryNamedPipeMessage(kCrossDeskServicePipeName, command, timeout_ms);
}
namespace {
std::string QueryDesktopInput(const std::string& command, DWORD timeout_ms) {
const ULONGLONG deadline = GetTickCount64() + timeout_ms;
for (;;) {
const auto now = GetTickCount64();
if (now >= deadline) return BuildErrorJson("secure_input_helper_not_ready", ERROR_NOT_READY);
const std::string response = QueryCrossDeskService(
command, static_cast<DWORD>(deadline - now));
const auto json = Json::parse(response, nullptr, false);
if (!json.is_object() || json.value("ok", false)) return response;
const std::string error = json.value("error", "");
// Retry only explicit pre-injection rejections during helper startup.
// Never replay an uncertain timeout or a partially injected mouse action.
if (!IsDesktopInputSetupPending(error) || IsCurrentSessionUserDesktopActive())
return response;
const auto remaining = pipe_deadline_detail::Remaining(deadline);
if (remaining == 0) return response;
Sleep((std::min)(DWORD{10}, remaining));
}
}
} // namespace
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
uint32_t scan_code,
bool extended,
DWORD timeout_ms) {
return QueryCrossDeskService(BuildSecureDesktopKeyboardIpcCommand(
return QueryDesktopInput(BuildSecureDesktopKeyboardIpcCommand(
key_code, is_down, scan_code, extended),
timeout_ms);
}
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
int flag, DWORD timeout_ms) {
return QueryCrossDeskService(
return QueryDesktopInput(
BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms);
}
@@ -41,6 +41,9 @@ class CrossDeskServiceHost {
void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code,
DWORD wait_hint);
void IpcServerLoop();
void SessionStateLoop();
void RefreshInteractiveState();
void WakeSessionState();
void RefreshSessionState();
void EnsureSessionHelper();
void ReapSessionHelper();
@@ -72,6 +75,14 @@ class CrossDeskServiceHost {
DWORD client_session_id);
std::string BuildStatusResponse(DWORD client_session_id);
std::string SendSecureAttentionSequence(DWORD client_session_id);
struct SecureInputTarget {
DWORD session_id = 0xFFFFFFFF;
std::string stage;
std::string desktop;
};
// Returns an error response, or an empty string with a validated snapshot.
std::string ResolveSecureInputTarget(DWORD client_session_id,
SecureInputTarget& target);
std::string SendSecureDesktopKeyboardInput(DWORD client_session_id,
int key_code, bool is_down,
uint32_t scan_code = 0,
@@ -89,6 +100,8 @@ class CrossDeskServiceHost {
SERVICE_STATUS service_status_{};
HANDLE stop_event_ = nullptr;
std::thread ipc_thread_;
std::thread session_state_thread_;
HANDLE session_state_event_ = nullptr;
std::thread client_process_monitor_thread_;
// Owned exclusively by the monitor, then drained after it joins.
HANDLE unattended_process_ = nullptr;
@@ -96,6 +109,9 @@ class CrossDeskServiceHost {
DWORD unattended_session_ = 0xFFFFFFFF;
std::atomic<uint64_t> unattended_logoff_generation_{0};
std::mutex state_mutex_;
uint64_t session_state_generation_ = 0;
ULONGLONG last_session_transition_tick_ = 0;
bool session_lock_state_known_ = false;
HANDLE lifetime_mutex_ = nullptr;
std::wstring lifetime_mutex_name_;
DWORD active_session_id_ = 0xFFFFFFFF;
@@ -148,6 +164,7 @@ class CrossDeskServiceHost {
HANDLE secure_input_helper_process_handle_ = nullptr;
HANDLE secure_input_helper_stop_event_ = nullptr;
std::string input_desktop_name_;
std::string session_username_;
std::string last_sas_error_;
std::string session_helper_last_error_;
std::string session_helper_status_error_;
@@ -0,0 +1,113 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SERVICE_QUERY_WORKER_H_
#define _SERVICE_QUERY_WORKER_H_
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <utility>
namespace crossdesk {
struct ServiceQueryResult {
std::string command;
std::string response;
bool user_desktop_recovered = false;
};
// One bounded query at a time. Reset invalidates in-flight results without
// joining the worker on the UI thread; only destruction waits for completion.
class ServiceQueryWorker {
public:
using Query = std::function<ServiceQueryResult(const std::string&)>;
explicit ServiceQueryWorker(Query query)
: thread_([this, query = std::move(query)] {
std::unique_lock lock(mutex_);
while (true) {
wake_.wait(lock, [this] {
return stopping_ || (!result_ && (sas_ || status_));
});
if (stopping_) return;
const std::string command = sas_ ? "sas" : "status";
(sas_ ? sas_ : status_) = false;
const uint64_t generation = generation_;
running_command_ = command;
running_generation_ = generation;
lock.unlock();
ServiceQueryResult result;
try {
result = query(command);
} catch (...) {
result.response = R"({"ok":false,"error":"service_query_failed"})";
}
result.command = command;
lock.lock();
running_command_.clear();
if (generation == generation_) result_ = std::move(result);
}
}) {}
~ServiceQueryWorker() {
{
std::lock_guard lock(mutex_);
stopping_ = true;
}
wake_.notify_one();
thread_.join();
}
ServiceQueryWorker(const ServiceQueryWorker&) = delete;
ServiceQueryWorker& operator=(const ServiceQueryWorker&) = delete;
void RequestStatus() {
std::lock_guard lock(mutex_);
if ((running_command_ == "status" && running_generation_ == generation_) ||
result_ || sas_) return;
status_ = true;
wake_.notify_one();
}
void RequestSas() {
std::lock_guard lock(mutex_);
// A pre-SAS status must not overwrite the optimistic secure-desktop state.
++generation_;
status_ = false;
result_.reset();
sas_ = true;
wake_.notify_one();
}
std::optional<ServiceQueryResult> Take() {
std::lock_guard lock(mutex_);
auto result = std::exchange(result_, std::nullopt);
wake_.notify_one();
return result;
}
void Reset() {
std::lock_guard lock(mutex_);
++generation_;
status_ = sas_ = false;
result_.reset();
}
private:
std::mutex mutex_;
std::condition_variable wake_;
bool stopping_ = false, status_ = false, sas_ = false;
uint64_t generation_ = 0, running_generation_ = 0;
std::string running_command_;
std::optional<ServiceQueryResult> result_;
std::thread thread_;
};
} // namespace crossdesk
#endif // _SERVICE_QUERY_WORKER_H_
@@ -23,16 +23,24 @@
#include <vector>
#include "interactive_desktop.h"
#include "named_pipe_deadline.h"
#include "path_manager.h"
#include "platform/path_backend.h"
#include "rd_log.h"
#include "session_helper_shared.h"
#include "secure_desktop_dxgi_capture.h"
#include "secure_desktop_frame_pacer.h"
#include "secure_desktop_nv12_cache.h"
#include "service_lifetime.h"
#include "shared_capture_lifecycle.h"
#include "shared_capture_configuration.h"
#include "usbmmidd_policy.h"
#include "usbmmidd_virtual_display.h"
#include "windows_cursor_state.h"
#include "cursor_draw.h"
#include "windows_input_marker.h"
#include "windows_key_metadata.h"
#include "../windows_thread_dpi.h"
namespace {
@@ -67,13 +75,7 @@ struct HelperState {
ULONGLONG last_update_tick = 0;
};
struct SecureCaptureRequest {
int left = 0;
int top = 0;
int width = 0;
int height = 0;
bool show_cursor = true;
int fps = 30;
struct SecureCaptureRequest : crossdesk::SharedCaptureConfiguration {
std::string interactive_stage;
std::string interactive_desktop;
};
@@ -87,21 +89,15 @@ struct SecureMouseRequest {
std::string interactive_desktop;
};
struct SecureCaptureBuffers {
std::vector<uint8_t> nv12_frame;
};
struct SecureSharedCaptureState {
std::mutex mutex;
std::thread capture_thread;
std::atomic<bool> stop_requested{false};
DWORD session_id = 0xFFFFFFFF;
SecureCaptureRequest request;
HANDLE frame_mapping = nullptr;
HANDLE frame_ready_event = nullptr;
uint8_t* frame_view = nullptr;
size_t frame_view_size = 0;
uint32_t sequence = 0;
// Last member: joins/cleans the producer before any captured state is destroyed.
crossdesk::SharedCaptureLifecycle lifecycle;
};
struct IpcSecurityAttributes {
@@ -136,12 +132,23 @@ void InitializeHelperLogger() {
static std::once_flag once_flag;
std::call_once(once_flag, []() {
crossdesk::PathManager path_manager("CrossDesk");
std::filesystem::path log_path =
path_manager.GetLogPath() / "session_helper";
if (!log_path.empty() && path_manager.CreateDirectories(log_path)) {
InitLogger(log_path.string());
return;
}
const auto initialize = [&](const std::filesystem::path& log_path) {
if (log_path.empty() || !path_manager.CreateDirectories(log_path)) return false;
try {
InitLogger(log_path.string());
// Creating a directory does not prove its existing log is writable.
// Force lazy sink creation here so an access failure can be recovered.
get_logger();
return true;
} catch (const spdlog::spdlog_ex&) {
return false;
}
};
if (initialize(path_manager.GetLogPath() / "session_helper")) return;
// The ordinary session helper uses the user's token. A portable helper
// installed in Program Files cannot append to a SYSTEM-owned capture log.
if (initialize(crossdesk::platform::GetLogPath("CrossDesk") /
"session_helper")) return;
InitLogger("logs/session_helper");
});
}
@@ -381,15 +388,16 @@ void UpdateHelperState(HelperState* helper_state) {
return;
}
InputDesktopInfo desktop_info = GetInputDesktopInfo();
const ULONGLONG sample_started = GetTickCount64();
bool lock_app_process_running =
IsLockAppRunningInCurrentSession(helper_state->session_id);
bool logon_ui_visible =
IsLogonUiRunningInCurrentSession(helper_state->session_id);
bool consent_ui_visible =
IsConsentUiRunningInCurrentSession(helper_state->session_id);
const bool consent_on_input_desktop =
desktop_info.available && consent_ui_visible;
// Process enumeration can straddle an unlock. Observe the input desktop
// afterwards, and expose the sample's start time for service-side rejection.
InputDesktopInfo desktop_info = GetInputDesktopInfo();
const bool input_desktop_is_winlogon =
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
const bool inaccessible_secure_input_desktop =
@@ -411,10 +419,9 @@ void UpdateHelperState(HelperState* helper_state) {
helper_state->input_desktop_name = desktop_info.name;
helper_state->lock_app_visible = lock_app_visible;
helper_state->logon_ui_visible = logon_ui_visible;
helper_state->consent_ui_visible =
consent_on_input_desktop || consent_ui_visible;
helper_state->consent_ui_visible = consent_ui_visible;
helper_state->secure_desktop_active = secure_desktop_active;
helper_state->last_update_tick = GetTickCount64();
helper_state->last_update_tick = sample_started;
}
std::string BuildHelperStatusResponse(HelperState* helper_state) {
@@ -435,6 +442,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
json["session_id"] = helper_state->session_id;
json["process_id"] = helper_state->process_id;
json["session_locked"] = helper_state->session_locked;
json["sample_started_tick"] = helper_state->last_update_tick;
json["input_desktop_available"] = helper_state->input_desktop_available;
json["input_desktop_error_code"] = helper_state->input_desktop_error_code;
json["input_desktop"] = helper_state->input_desktop_name;
@@ -1329,9 +1337,9 @@ std::vector<uint8_t> BuildTextResponseBytes(const std::string& response) {
return std::vector<uint8_t>(response.begin(), response.end());
}
// Own the desktop before its GDI objects, and release those objects before
// restoring the original desktop. Both capture paths use the same lifecycle.
struct SecureDesktopGdiResources {
// Own the desktop before its capture objects, and release those objects before
// restoring the original desktop. The legacy pipe path remains GDI-only.
struct SecureDesktopCaptureResources {
ScopedInteractiveDesktop desktop;
HDC screen_dc = nullptr;
HDC mem_dc = nullptr;
@@ -1341,8 +1349,16 @@ struct SecureDesktopGdiResources {
const char* error = nullptr;
DWORD error_code = ERROR_SUCCESS;
crossdesk::SecureDesktopCursorSnapshot cursor_snapshot{};
std::unique_ptr<crossdesk::SecureDesktopDxgiCapture> dxgi;
ULONGLONG next_dxgi_attempt = 0;
ULONGLONG dxgi_started = 0;
bool using_dxgi = false;
bool frame_pending = false;
const uint8_t* frame_bits = nullptr;
int frame_stride = 0;
crossdesk::SecureDesktopNv12Cache nv12;
~SecureDesktopGdiResources() {
~SecureDesktopCaptureResources() {
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
SelectObject(mem_dc, old_bitmap);
}
@@ -1364,6 +1380,15 @@ struct SecureDesktopGdiResources {
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
return Fail("switch_interactive_desktop_failed");
}
if (!desktop.IsReceivingInput()) {
SetLastError(ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION);
return Fail("inactive_capture_desktop");
}
return true;
}
bool InitializeGdi(const SecureCaptureRequest& request) {
if (bits) return true;
screen_dc = GetDC(nullptr);
if (screen_dc == nullptr) return Fail("get_dc_failed");
mem_dc = CreateCompatibleDC(screen_dc);
@@ -1384,11 +1409,50 @@ struct SecureDesktopGdiResources {
return true;
}
bool Capture(const SecureCaptureRequest& request) {
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
return Fail("bitblt_failed");
bool Capture(const SecureCaptureRequest& request, bool allow_dxgi = false) {
frame_pending = false;
const ULONGLONG now = GetTickCount64();
if (allow_dxgi && !dxgi && now >= next_dxgi_attempt) {
next_dxgi_attempt = now + 5000;
auto candidate = std::make_unique<crossdesk::SecureDesktopDxgiCapture>();
const HRESULT hr = candidate->Initialize(
request.left, request.top, request.width, request.height);
if (SUCCEEDED(hr)) {
candidate->SetSoftwareCursor(crossdesk::IsUsbmmiddDisplayDevice(
candidate->output().DeviceName));
dxgi = std::move(candidate);
dxgi_started = now;
} else {
LOG_INFO("Secure capture DXGI unavailable; using GDI, hr={}",
static_cast<int>(hr));
}
}
using_dxgi = false;
if (dxgi) {
const HRESULT hr = dxgi->Capture();
using_dxgi = hr == S_OK;
if (FAILED(hr) || (hr == S_FALSE && now - dxgi_started >= 250)) {
LOG_INFO("Secure capture DXGI lost; using GDI, hr={}",
static_cast<int>(hr));
dxgi.reset();
next_dxgi_attempt = now + 5000;
} else if (hr == S_FALSE) {
// DuplicateOutput often succeeds before the first desktop image is
// ready. A synchronous BitBlt here can stall the first frame for a
// desktop transition. Retry DXGI at the requested cadence instead.
frame_pending = true;
return false;
}
}
if (!using_dxgi) {
if (!InitializeGdi(request)) return false;
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
return Fail("bitblt_failed");
}
}
frame_bits = using_dxgi ? dxgi->pixels() : static_cast<const uint8_t*>(bits);
frame_stride = using_dxgi ? dxgi->stride() : request.width * 4;
// Sample on the thread bound to the captured input desktop, including when
// native controllers render the cursor themselves (show_cursor == false).
cursor_snapshot = {};
@@ -1397,7 +1461,8 @@ struct SecureDesktopGdiResources {
if (GetCursorInfo(&cursor)) {
static thread_local crossdesk::WindowsCursorState cursor_state;
crossdesk::CursorState state{};
cursor_state.Sample(cursor, false, &state);
const bool embedded = using_dxgi && dxgi->CursorEmbedded(cursor);
cursor_state.Sample(cursor, embedded, &state);
cursor_snapshot.valid = 1;
cursor_snapshot.visible = state.visible;
cursor_snapshot.shape = static_cast<uint32_t>(state.shape);
@@ -1405,28 +1470,38 @@ struct SecureDesktopGdiResources {
cursor_snapshot.hidden_reason = static_cast<uint32_t>(state.hidden_reason);
cursor_snapshot.x = cursor.ptScreenPos.x;
cursor_snapshot.y = cursor.ptScreenPos.y;
if (request.show_cursor && crossdesk::DrawCursorInCapture(
mem_dc, cursor, request.left, request.top,
request.width, request.height)) {
// Never draw into DXGI's cached desktop: each cursor position starts
// with clean pixels, including on a static screen or after a toggle.
if (request.show_cursor && !embedded && using_dxgi &&
(cursor.flags & CURSOR_SHOWING) != 0) {
if (!InitializeGdi(request)) return false;
const size_t row_bytes = static_cast<size_t>(request.width) * 4;
for (int y = 0; y < request.height; ++y) {
std::memcpy(static_cast<uint8_t*>(bits) + y * row_bytes,
frame_bits + static_cast<size_t>(y) * frame_stride,
row_bytes);
}
frame_bits = static_cast<const uint8_t*>(bits);
frame_stride = request.width * 4;
}
if (request.show_cursor && !embedded && mem_dc &&
crossdesk::DrawCursorInCapture(mem_dc, cursor, request.left,
request.top, request.width,
request.height)) {
cursor_snapshot.visible = 0;
cursor_snapshot.render_mode =
static_cast<uint32_t>(crossdesk::CursorRenderMode::embedded);
}
}
// Complete GDI writes before libyuv reads the DIB's pixel memory.
GdiFlush();
if (!using_dxgi || frame_bits == bits) GdiFlush();
return true;
}
};
std::vector<uint8_t> CaptureSecureDesktopFrame(
const SecureCaptureRequest& request,
SecureCaptureBuffers* capture_buffers) {
if (capture_buffers == nullptr) {
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers"));
}
SecureDesktopGdiResources capture;
const SecureCaptureRequest& request) {
SecureDesktopCaptureResources capture;
if (!capture.Initialize(request) || !capture.Capture(request)) {
return BuildTextResponseBytes(
BuildErrorJson(capture.error, capture.error_code));
@@ -1434,40 +1509,34 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
const size_t nv12_size =
static_cast<size_t>(request.width) * request.height * 3 / 2;
capture_buffers->nv12_frame.resize(nv12_size);
crossdesk::CrossDeskSecureDesktopFrameHeader header{};
// Each pipe client handles one command, so an intermediate capture buffer
// cannot be reused. Convert straight into the response payload.
std::vector<uint8_t> response(sizeof(header) + nv12_size);
uint8_t* payload = response.data() + sizeof(header);
const int convert_result = libyuv::ARGBToNV12(
static_cast<const uint8_t*>(capture.bits), request.width * 4,
capture_buffers->nv12_frame.data(), request.width,
capture_buffers->nv12_frame.data() + request.width * request.height,
capture.frame_bits, capture.frame_stride, payload, request.width,
payload + static_cast<size_t>(request.width) * request.height,
request.width, request.width, request.height);
if (convert_result != 0) {
return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed"));
}
crossdesk::CrossDeskSecureDesktopFrameHeader header{};
header.magic = crossdesk::kCrossDeskSecureDesktopFrameMagic;
header.version = crossdesk::kCrossDeskSecureDesktopFrameVersion;
header.left = request.left;
header.top = request.top;
header.width = static_cast<uint32_t>(request.width);
header.height = static_cast<uint32_t>(request.height);
header.payload_size =
static_cast<uint32_t>(capture_buffers->nv12_frame.size());
header.payload_size = static_cast<uint32_t>(nv12_size);
header.cursor = capture.cursor_snapshot;
std::vector<uint8_t> response(sizeof(header) +
capture_buffers->nv12_frame.size());
std::memcpy(response.data(), &header, sizeof(header));
if (!capture_buffers->nv12_frame.empty()) {
std::memcpy(response.data() + sizeof(header),
capture_buffers->nv12_frame.data(),
capture_buffers->nv12_frame.size());
}
return response;
}
void CloseSecureDesktopSharedCaptureResourcesLocked(
void CloseSecureDesktopSharedCaptureResources(
SecureSharedCaptureState* capture_state) {
if (capture_state == nullptr) {
return;
@@ -1485,7 +1554,7 @@ void CloseSecureDesktopSharedCaptureResourcesLocked(
CloseHandle(capture_state->frame_mapping);
capture_state->frame_mapping = nullptr;
}
capture_state->frame_view_size = 0;
capture_state->sequence = 0;
}
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
@@ -1493,28 +1562,22 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
return;
}
SecureCaptureRequest request;
uint8_t* frame_view = nullptr;
{
std::lock_guard<std::mutex> lock(capture_state->mutex);
request = capture_state->request;
frame_view = capture_state->frame_view;
}
// Start publishes these fields before launching us. Lifecycle control keeps
// the request, mapping and event alive and unchanged until this thread joins.
const SecureCaptureRequest request = capture_state->request;
uint8_t* const frame_view = capture_state->frame_view;
if (frame_view == nullptr || request.width <= 0 || request.height <= 0) {
return;
}
const int interval_ms =
request.fps > 0 ? (std::max)(1, 1000 / request.fps) : 33;
const size_t nv12_size =
static_cast<size_t>(request.width) * request.height * 3 / 2;
std::vector<uint8_t> nv12_frame(nv12_size);
std::unique_ptr<SecureDesktopGdiResources> capture;
crossdesk::ScopedWindowsPhysicalCoordinates physical_coordinates;
crossdesk::SecureDesktopFramePacer pacer(request.fps);
std::unique_ptr<SecureDesktopCaptureResources> capture;
ULONGLONG last_error_tick = 0;
auto stats_started = std::chrono::steady_clock::now();
unsigned frame_count = 0;
unsigned frame_count = 0, dxgi_frames = 0, dxgi_updates = 0;
unsigned converted_frames = 0;
double capture_ms = 0, convert_ms = 0, publish_ms = 0;
auto report_stats = [&](bool force) {
const auto elapsed = std::chrono::duration<double>(
@@ -1524,16 +1587,20 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
if (frame_count > 0) {
LOG_INFO(
"Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, "
"convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}",
"convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}, "
"target_fps={}, dxgi_frames={}, dxgi_updates={}, gdi_frames={}, "
"converted_frames={}, reused_frames={}",
frame_count / elapsed, capture_ms / frame_count,
convert_ms / frame_count, publish_ms / frame_count, request.width,
request.height);
request.height, request.fps, dxgi_frames, dxgi_updates,
frame_count - dxgi_frames, converted_frames,
frame_count - converted_frames);
}
stats_started = std::chrono::steady_clock::now();
frame_count = 0;
frame_count = dxgi_frames = dxgi_updates = converted_frames = 0;
capture_ms = convert_ms = publish_ms = 0;
};
while (!capture_state->stop_requested.load(std::memory_order_relaxed)) {
while (!capture_state->lifecycle.StopRequested()) {
const auto frame_started = std::chrono::steady_clock::now();
report_stats(false);
// A lock/credential/UAC transition can change the desktop before the
@@ -1542,7 +1609,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
capture.reset();
}
if (!capture) {
capture = std::make_unique<SecureDesktopGdiResources>();
capture = std::make_unique<SecureDesktopCaptureResources>();
if (!capture->Initialize(request)) {
const DWORD error = capture->error_code;
capture.reset();
@@ -1559,14 +1626,17 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
}
}
if (capture->Capture(request)) {
if (capture->Capture(request, true)) {
if (capture_state->lifecycle.StopRequested()) break;
const auto captured_at = std::chrono::steady_clock::now();
const int convert_result = libyuv::ARGBToNV12(
static_cast<const uint8_t*>(capture->bits), request.width * 4,
nv12_frame.data(), request.width,
nv12_frame.data() + request.width * request.height, request.width,
request.width, request.height);
if (convert_result == 0) {
// Composited/animated cursors and GDI frames always need conversion.
// Native controllers use independent cursor metadata, so a DXGI timeout
// or pointer-only update can reuse the NV12 image safely.
const bool pixels_changed = !capture->using_dxgi ||
capture->dxgi->updated() || request.show_cursor;
if (capture->nv12.Convert(capture->frame_bits, capture->frame_stride,
request.width, request.height, pixels_changed)) {
const auto& nv12_frame = capture->nv12.pixels();
const auto converted_at = std::chrono::steady_clock::now();
auto* header = reinterpret_cast<
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
@@ -1581,12 +1651,19 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
header->height = static_cast<uint32_t>(request.height);
header->payload_size = static_cast<uint32_t>(nv12_frame.size());
header->cursor = capture->cursor_snapshot;
std::memcpy(payload, nv12_frame.data(), nv12_frame.size());
if (capture->nv12.updated()) {
std::memcpy(payload, nv12_frame.data(), nv12_frame.size());
++converted_frames;
}
header->sequence = ++capture_state->sequence;
MemoryBarrier();
header->writing = 0;
SetEvent(capture_state->frame_ready_event);
++frame_count;
if (capture->using_dxgi) {
++dxgi_frames;
if (capture->dxgi->updated()) ++dxgi_updates;
}
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
frame_started)
.count();
@@ -1597,7 +1674,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
std::chrono::steady_clock::now() - converted_at)
.count();
}
} else {
} else if (!capture->frame_pending) {
const DWORD error = capture->error_code;
capture.reset();
const ULONGLONG now = GetTickCount64();
@@ -1610,14 +1687,10 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
}
}
const auto elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - frame_started)
.count();
const int retry_interval_ms = capture ? interval_ms : 100;
if (elapsed_ms < retry_interval_ms) {
std::this_thread::sleep_for(
std::chrono::milliseconds(retry_interval_ms - elapsed_ms));
if (capture) {
pacer.Wait(frame_started);
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
report_stats(true);
@@ -1629,41 +1702,18 @@ std::vector<uint8_t> StopSecureDesktopSharedCapture(
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
}
std::thread thread_to_join;
{
std::lock_guard<std::mutex> lock(capture_state->mutex);
capture_state->stop_requested.store(true, std::memory_order_relaxed);
if (capture_state->frame_ready_event != nullptr) {
SetEvent(capture_state->frame_ready_event);
}
if (capture_state->capture_thread.joinable()) {
thread_to_join = std::move(capture_state->capture_thread);
}
}
if (thread_to_join.joinable()) {
thread_to_join.join();
}
{
std::lock_guard<std::mutex> lock(capture_state->mutex);
CloseSecureDesktopSharedCaptureResourcesLocked(capture_state);
capture_state->stop_requested.store(false, std::memory_order_relaxed);
capture_state->sequence = 0;
}
capture_state->lifecycle.Stop();
return BuildTextResponseBytes("{\"ok\":true,\"shared_capture\":\"stopped\"}");
}
std::vector<uint8_t> StartSecureDesktopSharedCapture(
std::vector<uint8_t> PrepareSecureDesktopSharedCapture(
const SecureCaptureRequest& request,
SecureSharedCaptureState* capture_state) {
if (capture_state == nullptr) {
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
}
StopSecureDesktopSharedCapture(capture_state);
const size_t payload_size =
static_cast<size_t>(request.width) * request.height * 3 / 2;
const size_t mapping_size =
@@ -1735,17 +1785,44 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
header->height = static_cast<uint32_t>(request.height);
header->buffer_size = static_cast<uint32_t>(payload_size);
{
std::lock_guard<std::mutex> lock(capture_state->mutex);
capture_state->request = request;
capture_state->frame_mapping = frame_mapping;
capture_state->frame_ready_event = frame_ready_event;
capture_state->frame_view = frame_view;
capture_state->frame_view_size = mapping_size;
capture_state->sequence = 0;
capture_state->stop_requested.store(false, std::memory_order_relaxed);
capture_state->capture_thread =
std::thread(SecureDesktopSharedCaptureThread, capture_state);
// Transfer handles before any potentially throwing request/string copy.
// The lifecycle owner cleans partially prepared resources on failure too.
capture_state->frame_mapping = frame_mapping;
capture_state->frame_ready_event = frame_ready_event;
capture_state->frame_view = frame_view;
capture_state->sequence = 0;
capture_state->request = request;
return {};
}
std::vector<uint8_t> StartSecureDesktopSharedCapture(
const SecureCaptureRequest& request,
SecureSharedCaptureState* capture_state) {
if (!capture_state) {
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
}
std::vector<uint8_t> error;
const auto result = capture_state->lifecycle.Start(
[&] {
error = PrepareSecureDesktopSharedCapture(request, capture_state);
return error.empty();
},
[capture_state] { SecureDesktopSharedCaptureThread(capture_state); },
[capture_state] {
CloseSecureDesktopSharedCaptureResources(capture_state);
},
[&] {
// A timed-out caller may retry a start that already succeeded.
return capture_state->request == request;
});
if (result == crossdesk::SharedCaptureLifecycle::StartResult::ShuttingDown) {
return BuildTextResponseBytes(BuildErrorJson("capture_shutting_down"));
}
if (result != crossdesk::SharedCaptureLifecycle::StartResult::Started) {
return error.empty()
? BuildTextResponseBytes(BuildErrorJson("capture_start_failed"))
: error;
}
Json json;
@@ -1758,7 +1835,7 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
}
std::vector<uint8_t> HandleSecureInputHelperCommand(
const std::string& command, SecureCaptureBuffers* capture_buffers,
const std::string& command,
SecureSharedCaptureState* capture_state) {
if (command == "ping") {
return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}");
@@ -1840,7 +1917,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
SecureCaptureRequest capture_request;
if (ParseSecureInputCaptureCommand(command, &capture_request)) {
return CaptureSecureDesktopFrame(capture_request, capture_buffers);
return CaptureSecureDesktopFrame(capture_request);
}
return BuildTextResponseBytes(BuildErrorJson("unknown_command"));
@@ -1849,19 +1926,23 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
void HandleSecureInputHelperPipeClient(
HANDLE pipe, HANDLE event_handle,
std::shared_ptr<SecureSharedCaptureState> capture_state) {
SecureCaptureBuffers capture_buffers;
char buffer[1024] = {0};
DWORD bytes_read = 0;
if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) &&
DWORD error = ERROR_SUCCESS;
if (crossdesk::pipe_deadline_detail::Transfer(
pipe, event_handle, false, buffer, sizeof(buffer) - 1,
GetTickCount64() + 1000, &bytes_read, &error) &&
bytes_read > 0) {
std::vector<uint8_t> response = HandleSecureInputHelperCommand(
std::string(buffer, buffer + bytes_read), &capture_buffers,
capture_state.get());
std::string(buffer, buffer + bytes_read), capture_state.get());
DWORD bytes_written = 0;
if (!response.empty()) {
WriteFile(pipe, response.data(), static_cast<DWORD>(response.size()),
&bytes_written, nullptr);
FlushFileBuffers(pipe);
if (crossdesk::pipe_deadline_detail::Transfer(
pipe, event_handle, true, response.data(),
static_cast<DWORD>(response.size()), GetTickCount64() + 1000,
&bytes_written, &error)) {
FlushFileBuffers(pipe);
}
}
}
@@ -1930,8 +2011,10 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) {
HANDLE wait_handles[] = {stop_event, overlapped.hEvent};
DWORD wait_result =
WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
if (wait_result == WAIT_OBJECT_0) {
if (wait_result != WAIT_OBJECT_0 + 1) {
CancelIoEx(pipe, &overlapped);
DWORD ignored = 0;
GetOverlappedResult(pipe, &overlapped, &ignored, TRUE);
CloseHandle(overlapped.hEvent);
CloseHandle(pipe);
break;
@@ -1946,7 +2029,7 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) {
.detach();
}
StopSecureDesktopSharedCapture(capture_state.get());
capture_state->lifecycle.Shutdown();
}
void PrintUsage() {
@@ -0,0 +1,30 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SHARED_CAPTURE_CONFIGURATION_H_
#define _SHARED_CAPTURE_CONFIGURATION_H_
namespace crossdesk {
// Desktop stage/name are hints for initial binding, not image configuration.
// The producer follows the actual input desktop without restarting the stream.
struct SharedCaptureConfiguration {
int left = 0;
int top = 0;
int width = 0;
int height = 0;
bool show_cursor = true;
int fps = 30;
bool operator==(const SharedCaptureConfiguration& other) const {
return left == other.left && top == other.top && width == other.width &&
height == other.height && show_cursor == other.show_cursor &&
fps == other.fps;
}
};
} // namespace crossdesk
#endif // _SHARED_CAPTURE_CONFIGURATION_H_
@@ -0,0 +1,121 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-08
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _SHARED_CAPTURE_LIFECYCLE_H_
#define _SHARED_CAPTURE_LIFECYCLE_H_
#include <atomic>
#include <condition_variable>
#include <cstdint>
#include <functional>
#include <mutex>
#include <thread>
#include <utility>
namespace crossdesk {
// Serializes complete control operations, including joining the old producer.
// The producer never takes control_mutex_: resources are immutable from its
// launch until its join, and cleanup always runs after the join.
class SharedCaptureLifecycle {
// Preserve admission order while a slow join holds up several retries. A
// queued stop must not be overtaken by a new start and then stop that
// producer.
class ControlOperation {
public:
explicit ControlOperation(SharedCaptureLifecycle& owner)
: owner_(owner),
ticket_(owner.next_ticket_.fetch_add(1)),
lock_(owner.control_mutex_) {
owner_.control_ready_.wait(
lock_, [&] { return owner_.serving_ticket_ == ticket_; });
}
~ControlOperation() {
++owner_.serving_ticket_;
lock_.unlock();
owner_.control_ready_.notify_all();
}
private:
SharedCaptureLifecycle& owner_;
uint64_t ticket_;
std::unique_lock<std::mutex> lock_;
};
public:
enum class StartResult { Started, Failed, ShuttingDown };
SharedCaptureLifecycle() = default;
~SharedCaptureLifecycle() { Shutdown(); }
SharedCaptureLifecycle(const SharedCaptureLifecycle&) = delete;
SharedCaptureLifecycle& operator=(const SharedCaptureLifecycle&) = delete;
StartResult Start(std::function<bool()> prepare,
std::function<void()> capture,
std::function<void()> cleanup,
std::function<bool()> reuse = {}) {
if (shutting_down_.load()) return StartResult::ShuttingDown;
ControlOperation operation(*this);
if (shutting_down_.load()) return StartResult::ShuttingDown;
if (!producer_finished_.load() && reuse && reuse())
return StartResult::Started;
StopLocked();
cleanup_ = std::move(cleanup);
try {
if (prepare()) {
stop_requested_.store(false, std::memory_order_relaxed);
producer_finished_.store(false);
producer_ = std::thread([this, capture = std::move(capture)] {
capture();
producer_finished_.store(true);
});
return StartResult::Started;
}
} catch (...) {
// Failed allocation/thread creation must leave the prepared handles
// owned by cleanup, not leak them from an abandoned start request.
}
StopLocked();
return StartResult::Failed;
}
void Stop() {
ControlOperation operation(*this);
StopLocked();
}
void Shutdown() {
// Publish before waiting for the lock. Detached IPC clients that are
// already queued cannot start another producer after server shutdown.
shutting_down_.store(true);
Stop();
}
bool StopRequested() const {
return stop_requested_.load(std::memory_order_relaxed);
}
private:
void StopLocked() {
stop_requested_.store(true, std::memory_order_relaxed);
if (producer_.joinable()) producer_.join();
producer_finished_.store(true);
if (cleanup_) cleanup_();
cleanup_ = {};
}
std::mutex control_mutex_;
std::condition_variable control_ready_;
std::atomic<uint64_t> next_ticket_{0};
uint64_t serving_ticket_ = 0;
std::atomic<bool> shutting_down_{false};
std::atomic<bool> stop_requested_{true};
std::atomic<bool> producer_finished_{true};
std::thread producer_;
std::function<void()> cleanup_;
};
} // namespace crossdesk
#endif // _SHARED_CAPTURE_LIFECYCLE_H_
@@ -79,8 +79,6 @@ int main() {
ReadFile(repo_root / "apps/desktop/src/gui/features/input/keyboard_controller.cpp");
const std::string windows_service_runtime =
ReadFile(repo_root / "apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp");
const std::string screen_capturer_h =
ReadFile(repo_root / "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.h");
const std::string screen_capturer_cpp = ReadFile(
repo_root / "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp");
@@ -101,11 +99,8 @@ int main() {
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"EnablePerMonitorDpiAwareness();\n\n"
" InitializeHelperLogger();");
ok &= ExpectContains(
"service_host.cpp", service_host,
"const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms");
ok &= ExpectContains("service_host.cpp", service_host,
"while (GetTickCount64() <= deadline_tick)");
"QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms");
ok &= ExpectNotContains("service_host.cpp", service_host,
"constexpr int kPipeConnectRetryCount = 3");
ok &= ExpectContains("service_host.cpp", service_host,
@@ -128,8 +123,6 @@ int main() {
"IsConsentUiRunningInCurrentSession");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"L\"Consent.exe\"");
ok &= ExpectContains("session_helper_main.cpp", session_helper,
"desktop_info.available && consent_ui_visible");
ok &= ExpectContains("service_host.cpp", service_host,
"session_helper_report_input_desktop_available_ &&");
ok &= ExpectContains("service_host.cpp", service_host,
@@ -145,18 +138,13 @@ int main() {
"return L\"winsta0\\\\Winlogon\"");
ok &= ExpectContains("service_host.cpp", service_host,
"return L\"winsta0\\\\default\"");
ok &= ExpectContains(
"service_host.cpp", service_host,
"secure_input_helper_interactive_stage_ == interactive_stage");
ok &= ExpectContains(
"service_host.cpp", service_host,
"secure_input_helper_interactive_stage_ = interactive_stage");
ok &= ExpectContains("service_host.cpp", service_host,
"secure_input_helper_interactive_stage_.clear()");
ok &= ExpectContains(
"service_host.cpp", service_host,
"LaunchSecureInputHelper(target_session_id, interactive_stage,\n"
" interactive_desktop)");
ok &= ExpectContains("service_host.cpp", service_host,
"LaunchSecureInputHelper(secure_input_target_session_id,");
ok &= ExpectContains("service_host.cpp", service_host,
"\\\"secure_input_helper_stage\\\":\\\"");
ok &= ExpectContains("service_host.cpp", service_host,
@@ -256,15 +244,7 @@ int main() {
"runtime->local_service_available_ &&\n"
" "
"IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_)");
ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h,
"std::string secure_shared_stage_;");
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
"const std::string& stage");
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
"secure_shared_stage_ == stage");
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
"secure_shared_stage_ = stage");
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
"secure_shared_stage_.clear()");
return ok ? 0 : 1;
}
+2 -1
View File
@@ -593,7 +593,8 @@ function setup_targets()
set_kind("binary")
add_packages("libyuv", "nlohmann_json")
add_deps("rd_log", "path_manager")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32", "Setupapi")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32", "Setupapi",
"d3d11", "dxgi")
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",