mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-15 00:09:39 +08:00
[fix] improve Windows secure desktop capture and input transitions
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user