mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-21 03:40:40 +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;
|
key_code, is_down, scan_code, extended) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
bool SessionDeviceManager::SendMouseCommand(const RemoteAction &action,
|
||||||
int selected_display) {
|
int selected_display) {
|
||||||
if (mouse_controller_) {
|
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) {
|
int SessionDeviceManager::SwitchDisplay(int display_id) {
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ public:
|
|||||||
|
|
||||||
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
bool SendKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||||
bool extended);
|
bool extended);
|
||||||
void SendMouseCommand(const RemoteAction &action, int selected_display);
|
bool SendMouseCommand(const RemoteAction &action, int selected_display);
|
||||||
int SwitchDisplay(int display_id);
|
int SwitchDisplay(int display_id);
|
||||||
void ResetToInitialDisplay();
|
void ResetToInitialDisplay();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
#include "windows_key_metadata.h"
|
#include "windows_key_metadata.h"
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
#include "interactive_state.h"
|
#include "desktop_transition_policy.h"
|
||||||
|
#include "interactive_desktop.h"
|
||||||
#include "service_host.h"
|
#include "service_host.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -82,12 +83,11 @@ void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||||
const RemoteAction& action) {
|
bool is_down) {
|
||||||
return response.is_object() &&
|
return response.is_object() &&
|
||||||
response.value("error", std::string()) == "send_input_failed" &&
|
response.value("error", std::string()) == "send_input_failed" &&
|
||||||
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
||||||
action.type == ControlType::keyboard &&
|
!is_down;
|
||||||
action.k.flag == KeyFlag::key_up;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -218,40 +218,51 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
|||||||
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||||
owner_.privacy_.SuspendForDesktop();
|
owner_.privacy_.SuspendForDesktop();
|
||||||
}
|
}
|
||||||
if (owner_.local_service_status_received_ &&
|
if (owner_.is_server_mode_ || !owner_.WindowsInputStage().empty()) {
|
||||||
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
|
return DispatchDesktopInput(
|
||||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
[&] {
|
||||||
key_code, is_down, scan_code, extended, 1000);
|
return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(),
|
||||||
const auto json = nlohmann::json::parse(response, nullptr, false);
|
owner_.windows_consent_ui_.load());
|
||||||
if (json.is_discarded() || !json.value("ok", false)) {
|
},
|
||||||
RemoteAction action{};
|
[&] {
|
||||||
action.type = ControlType::keyboard;
|
SetLastError(ERROR_SUCCESS);
|
||||||
action.k.key_value = static_cast<size_t>(key_code);
|
const bool sent = owner_.devices_.SendKeyboardCommand(
|
||||||
action.k.scan_code = scan_code;
|
key_code, is_down, scan_code, extended);
|
||||||
action.k.extended = extended;
|
return DesktopInputResult{
|
||||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
sent, IsDesktopTransitionInputError(GetLastError())};
|
||||||
if (!json.is_discarded() &&
|
},
|
||||||
IsTransientSecureDesktopInputFailure(json, action)) {
|
[&]() -> DesktopInputResult {
|
||||||
LOG_INFO(
|
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||||
"Secure desktop keyboard injection transient failure, "
|
key_code, is_down, scan_code, extended, 1000);
|
||||||
"key_code={}, is_down={}, response={}",
|
const auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
key_code, is_down, response);
|
if (!json.is_object() || !json.value("ok", false)) {
|
||||||
return true;
|
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(
|
LogSecureDesktopInputBlocked(
|
||||||
&owner_.last_local_secure_input_block_log_tick_,
|
&owner_.last_local_secure_input_block_log_tick_,
|
||||||
owner_.local_interactive_stage_.c_str());
|
owner_.WindowsInputStage().c_str());
|
||||||
LOG_WARN(
|
LOG_WARN(
|
||||||
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
|
"Secure desktop keyboard injection failed, key_code={}, "
|
||||||
"response={}",
|
"is_down={}, "
|
||||||
key_code, is_down, response);
|
"response={}",
|
||||||
return false;
|
key_code, is_down, response);
|
||||||
}
|
return {};
|
||||||
return true;
|
}
|
||||||
|
return {true, false};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// Recheck after the desktop query; key-up cleanup remains allowed while paused.
|
|
||||||
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
|
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
|
||||||
extended);
|
extended);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ class GuiRuntime : protected gui_detail::GuiState {
|
|||||||
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
|
RemoteUnlockState GetRemoteUnlockState(const RemoteSession& props) const;
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
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
|
#endif
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
#include "runtime/gui_runtime.h"
|
#include "runtime/gui_runtime.h"
|
||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
#include "interactive_state.h"
|
#include "desktop_transition_policy.h"
|
||||||
|
#include "interactive_desktop.h"
|
||||||
#include "service_host.h"
|
#include "service_host.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -392,45 +393,60 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
|||||||
std::chrono::steady_clock::now();
|
std::chrono::steady_clock::now();
|
||||||
}
|
}
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
if (runtime->local_service_status_received_ &&
|
if (runtime->is_server_mode_ && remote_action.type == ControlType::mouse) {
|
||||||
IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_) &&
|
std::string response;
|
||||||
remote_action.type != ControlType::keyboard &&
|
const bool delivered = DispatchDesktopInput(
|
||||||
remote_action.type != ControlType::keyboard_state) {
|
[&] {
|
||||||
if (remote_action.type == ControlType::mouse) {
|
return PreferUserDesktopInput(IsCurrentSessionUserDesktopActive(),
|
||||||
int absolute_x = 0;
|
runtime->windows_consent_ui_.load());
|
||||||
int absolute_y = 0;
|
},
|
||||||
if (!BuildAbsoluteMousePosition(runtime->devices_.display_info_list(),
|
[&] {
|
||||||
runtime->selected_display_,
|
SetLastError(ERROR_SUCCESS);
|
||||||
remote_action.m.x, remote_action.m.y,
|
const bool sent = runtime->devices_.SendMouseCommand(
|
||||||
&absolute_x, &absolute_y)) {
|
remote_action, runtime->selected_display_);
|
||||||
LOG_WARN("Secure desktop mouse injection skipped, invalid display "
|
return DesktopInputResult{
|
||||||
"mapping: display_index={}, x={}, y={}",
|
sent, IsDesktopTransitionInputError(GetLastError())};
|
||||||
runtime->selected_display_, remote_action.m.x,
|
},
|
||||||
remote_action.m.y);
|
[&]() -> DesktopInputResult {
|
||||||
return;
|
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(
|
response = SendCrossDeskSecureDesktopMouseInput(
|
||||||
absolute_x, absolute_y, remote_action.m.s,
|
absolute_x, absolute_y, remote_action.m.s,
|
||||||
static_cast<int>(remote_action.m.flag), 1000);
|
static_cast<int>(remote_action.m.flag), 1000);
|
||||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
if (json.is_discarded() || !json.value("ok", false)) {
|
const bool sent = json.is_object() && json.value("ok", false);
|
||||||
LogSecureDesktopInputBlocked(
|
const std::string error =
|
||||||
&runtime->last_local_secure_input_block_log_tick_, "local",
|
json.is_object() ? json.value("error", "") : "";
|
||||||
runtime->local_interactive_stage_.c_str());
|
return {sent, IsDesktopInputSetupPending(error)};
|
||||||
LOG_WARN(
|
});
|
||||||
"Secure desktop mouse injection failed, x={}, y={}, wheel={}, "
|
if (!delivered && !response.empty()) {
|
||||||
"flag={}, response={}",
|
LogSecureDesktopInputBlocked(
|
||||||
absolute_x, absolute_y, remote_action.m.s,
|
&runtime->last_local_secure_input_block_log_tick_, "local",
|
||||||
static_cast<int>(remote_action.m.flag), response);
|
runtime->WindowsInputStage().c_str());
|
||||||
}
|
LOG_WARN(
|
||||||
return;
|
"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
|
#endif
|
||||||
if (remote_action.type == ControlType::mouse) {
|
if (remote_action.type == ControlType::mouse) {
|
||||||
runtime->devices_.SendMouseCommand(remote_action,
|
runtime->devices_.SendMouseCommand(remote_action,
|
||||||
runtime->selected_display_);
|
runtime->selected_display_);
|
||||||
} else if (remote_action.type == ControlType::audio_capture) {
|
} else if (remote_action.type == ControlType::audio_capture) {
|
||||||
if (remote_action.a)
|
if (remote_action.a)
|
||||||
runtime->devices_.StartSpeakerCapturer();
|
runtime->devices_.StartSpeakerCapturer();
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
#include "runtime/session_lifecycle.h"
|
#include "runtime/session_lifecycle.h"
|
||||||
#include "thumbnail.h"
|
#include "thumbnail.h"
|
||||||
|
|
||||||
|
#if _WIN32
|
||||||
|
#include "platform/windows/service/service_query_worker.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace crossdesk::gui_detail {
|
namespace crossdesk::gui_detail {
|
||||||
|
|
||||||
struct InfrastructureState {
|
struct InfrastructureState {
|
||||||
@@ -83,6 +87,11 @@ struct PeerState {
|
|||||||
// state so platform code does not appear to be part of the wire contract.
|
// state so platform code does not appear to be part of the wire contract.
|
||||||
struct PlatformIntegrationState {
|
struct PlatformIntegrationState {
|
||||||
#if _WIN32
|
#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};
|
std::atomic<bool> pending_windows_service_sas_{false};
|
||||||
bool local_service_status_received_ = false;
|
bool local_service_status_received_ = false;
|
||||||
bool local_service_available_ = false;
|
bool local_service_available_ = false;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
#include "interactive_desktop.h"
|
#include "interactive_desktop.h"
|
||||||
|
#include "desktop_transition_policy.h"
|
||||||
#include "interactive_state.h"
|
#include "interactive_state.h"
|
||||||
#include "service_host.h"
|
#include "service_host.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -25,12 +26,13 @@ struct WindowsServiceInteractiveStatus {
|
|||||||
bool available = false;
|
bool available = false;
|
||||||
bool session_mismatch = false;
|
bool session_mismatch = false;
|
||||||
bool sas_secure_desktop_grace_active = false;
|
bool sas_secure_desktop_grace_active = false;
|
||||||
|
bool consent_ui_visible = false;
|
||||||
unsigned int error_code = 0;
|
unsigned int error_code = 0;
|
||||||
std::string interactive_stage;
|
std::string interactive_stage;
|
||||||
std::string error;
|
std::string error;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
|
constexpr uint32_t kWindowsServiceStatusIntervalMs = 250;
|
||||||
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
|
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
|
||||||
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
|
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
|
||||||
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
|
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
|
||||||
@@ -51,15 +53,13 @@ BuildWindowsServiceStatusAction(const WindowsServiceInteractiveStatus &status) {
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QueryWindowsServiceInteractiveStatus(
|
bool ParseWindowsServiceInteractiveStatus(const std::string& response,
|
||||||
WindowsServiceInteractiveStatus *status) {
|
WindowsServiceInteractiveStatus *status) {
|
||||||
if (status == nullptr) {
|
if (status == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
*status = WindowsServiceInteractiveStatus{};
|
*status = WindowsServiceInteractiveStatus{};
|
||||||
const std::string response =
|
|
||||||
QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs);
|
|
||||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
if (json.is_discarded() || !json.is_object()) {
|
if (json.is_discarded() || !json.is_object()) {
|
||||||
status->error = "invalid_service_status_json";
|
status->error = "invalid_service_status_json";
|
||||||
@@ -84,6 +84,7 @@ bool QueryWindowsServiceInteractiveStatus(
|
|||||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||||
status->sas_secure_desktop_grace_active =
|
status->sas_secure_desktop_grace_active =
|
||||||
json.value("sas_secure_desktop_grace_active", false);
|
json.value("sas_secure_desktop_grace_active", false);
|
||||||
|
status->consent_ui_visible = json.value("consent_ui_visible", false);
|
||||||
|
|
||||||
if (ShouldNormalizeUnlockToUserDesktop(
|
if (ShouldNormalizeUnlockToUserDesktop(
|
||||||
json.value("interactive_lock_screen_visible", false),
|
json.value("interactive_lock_screen_visible", false),
|
||||||
@@ -125,13 +126,31 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
return;
|
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 (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
|
||||||
if (privacy_.Engaged()) {
|
if (privacy_.Engaged()) {
|
||||||
privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
||||||
}
|
}
|
||||||
const std::string response =
|
windows_service_worker_->RequestSas();
|
||||||
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
|
}
|
||||||
|
|
||||||
|
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);
|
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
if (json.is_discarded() || !json.value("ok", false)) {
|
if (json.is_discarded() || !json.value("ok", false)) {
|
||||||
LOG_WARN("Remote SAS request failed: {}", response);
|
LOG_WARN("Remote SAS request failed: {}", response);
|
||||||
@@ -145,21 +164,33 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
local_interactive_stage_ = "secure-desktop";
|
local_interactive_stage_ = "secure-desktop";
|
||||||
}
|
}
|
||||||
last_windows_service_status_tick_ = 0;
|
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 (last_windows_service_status_tick_ == 0 ||
|
||||||
if (!force_broadcast && last_windows_service_status_tick_ != 0 &&
|
now - last_windows_service_status_tick_ >=
|
||||||
now - last_windows_service_status_tick_ <
|
|
||||||
kWindowsServiceStatusIntervalMs) {
|
kWindowsServiceStatusIntervalMs) {
|
||||||
return;
|
windows_service_worker_->RequestStatus();
|
||||||
|
last_windows_service_status_tick_ = now;
|
||||||
}
|
}
|
||||||
last_windows_service_status_tick_ = now;
|
if (!result) return;
|
||||||
|
|
||||||
WindowsServiceInteractiveStatus status;
|
WindowsServiceInteractiveStatus status;
|
||||||
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
|
const bool status_ok = ParseWindowsServiceInteractiveStatus(result->response, &status);
|
||||||
const bool user_desktop_recovered =
|
const bool user_desktop_recovered = PreferUserDesktopInput(
|
||||||
!status.available && IsCurrentSessionUserDesktopActive();
|
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";
|
if (user_desktop_recovered) status.interactive_stage = "user-desktop";
|
||||||
WindowsServiceInteractiveStatus broadcast_status = status;
|
WindowsServiceInteractiveStatus broadcast_status = status;
|
||||||
const bool previous_secure_desktop_interaction =
|
const bool previous_secure_desktop_interaction =
|
||||||
@@ -169,7 +200,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
|
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
|
||||||
now) > 0;
|
now) > 0;
|
||||||
const bool keep_optimistic_secure_desktop =
|
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.sas_secure_desktop_grace_active &&
|
||||||
status.interactive_stage == "user-desktop";
|
status.interactive_stage == "user-desktop";
|
||||||
local_service_status_received_ =
|
local_service_status_received_ =
|
||||||
@@ -226,6 +257,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
last_logged_service_error_code = 0;
|
last_logged_service_error_code = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PublishWindowsInputStage();
|
||||||
RemoteAction remote_action =
|
RemoteAction remote_action =
|
||||||
BuildWindowsServiceStatusAction(broadcast_status);
|
BuildWindowsServiceStatusAction(broadcast_status);
|
||||||
std::string msg = remote_action.to_json();
|
std::string msg = remote_action.to_json();
|
||||||
@@ -239,14 +271,17 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
|||||||
|
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
|
void GuiRuntime::ResetLocalWindowsServiceState(bool clear_pending_sas) {
|
||||||
|
if (windows_service_worker_) windows_service_worker_->Reset();
|
||||||
last_windows_service_status_tick_ = 0;
|
last_windows_service_status_tick_ = 0;
|
||||||
if (clear_pending_sas) {
|
if (clear_pending_sas) {
|
||||||
pending_windows_service_sas_.store(false, std::memory_order_relaxed);
|
pending_windows_service_sas_.store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
local_service_status_received_ = false;
|
local_service_status_received_ = false;
|
||||||
local_service_available_ = false;
|
local_service_available_ = false;
|
||||||
|
windows_consent_ui_.store(false);
|
||||||
local_interactive_stage_.clear();
|
local_interactive_stage_.clear();
|
||||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||||
|
PublishWindowsInputStage();
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -313,8 +313,10 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
|||||||
|
|
||||||
const UINT sent = SendInputOnUserDesktop(input);
|
const UINT sent = SendInputOnUserDesktop(input);
|
||||||
if (sent != 1) {
|
if (sent != 1) {
|
||||||
|
const DWORD error = GetLastError();
|
||||||
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
|
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
|
||||||
is_down, GetLastError());
|
is_down, error);
|
||||||
|
SetLastError(error);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,9 +144,11 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
|
|||||||
ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
|
ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
|
||||||
const UINT sent = SendInputOnUserDesktop(ip);
|
const UINT sent = SendInputOnUserDesktop(ip);
|
||||||
if (sent != 1) {
|
if (sent != 1) {
|
||||||
|
const DWORD error = GetLastError();
|
||||||
LOG_WARN("SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
LOG_WARN("SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
||||||
ip.mi.dx, ip.mi.dy, remote_action.m.s,
|
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;
|
return -1;
|
||||||
}
|
}
|
||||||
if (ip.mi.dwFlags & MOUSEEVENTF_LEFTDOWN) pressed_buttons_.fetch_or(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.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_running = json.value("secure_input_helper_running", false);
|
||||||
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
|
status.helper_process_id = json.value("secure_input_helper_pid", 0u);
|
||||||
status.interactive_stage = json.value("interactive_stage", std::string());
|
status.interactive_stage = json.value("interactive_stage", std::string());
|
||||||
@@ -425,11 +426,7 @@ SecureDesktopServiceStatus ReadSecureDesktopServiceStatus() {
|
|||||||
|
|
||||||
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
|
SecureDesktopServiceStatus QuerySecureDesktopServiceStatus() {
|
||||||
auto status = ReadSecureDesktopServiceStatus();
|
auto status = ReadSecureDesktopServiceStatus();
|
||||||
if (!status.service_available && IsCurrentSessionUserDesktopActive()) {
|
if (IsCurrentSessionUserDesktopActive()) status.OnUserDesktopObserved();
|
||||||
status.desktop_state_known = true;
|
|
||||||
status.capture_active = false;
|
|
||||||
status.interactive_stage = "user-desktop";
|
|
||||||
}
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,10 +442,11 @@ bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
|||||||
GetCrossDeskSecureInputHelperPipeName(session_id);
|
GetCrossDeskSecureInputHelperPipeName(session_id);
|
||||||
std::string error;
|
std::string error;
|
||||||
DWORD code = 0;
|
DWORD code = 0;
|
||||||
// The legacy single-frame path includes a full GDI capture and a large
|
// Starting may have to join/release the old GPU producer first. Its reply
|
||||||
// response. Give it a separate deadline from the small control messages.
|
// cannot use the short deadline intended for lightweight control messages.
|
||||||
const DWORD timeout_ms =
|
const DWORD timeout_ms =
|
||||||
command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0
|
(command.rfind(kCrossDeskSecureInputCaptureCommandPrefix, 0) == 0 ||
|
||||||
|
command.rfind(kCrossDeskSecureInputCaptureStartCommandPrefix, 0) == 0)
|
||||||
? 1000
|
? 1000
|
||||||
: kSecureDesktopHelperPipeTimeoutMs;
|
: kSecureDesktopHelperPipeTimeoutMs;
|
||||||
const bool ok = QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
|
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);
|
0, std::memory_order_relaxed);
|
||||||
const ULONGLONG elapsed_ms =
|
const ULONGLONG elapsed_ms =
|
||||||
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
|
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
|
||||||
post_secure_desktop_drop_logged_.store(false,
|
|
||||||
std::memory_order_relaxed);
|
|
||||||
LOG_INFO(
|
LOG_INFO(
|
||||||
"Windows capturer first normal frame after secure desktop: "
|
"Windows capturer first normal frame after secure desktop: "
|
||||||
"reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
|
"reported_stream='{}', mapped_stream='{}', size={}x{}, bytes={}, "
|
||||||
@@ -804,7 +800,6 @@ void ScreenCapturerWin::ResetPostSecureDesktopState() {
|
|||||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||||
post_secure_desktop_waiting_for_frame_.store(false,
|
post_secure_desktop_waiting_for_frame_.store(false,
|
||||||
std::memory_order_relaxed);
|
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);
|
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||||
SharedSecureDesktopCursorState().SetActive(false);
|
SharedSecureDesktopCursorState().SetActive(false);
|
||||||
}
|
}
|
||||||
@@ -1237,6 +1232,7 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
|||||||
|
|
||||||
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
||||||
impl_->Stop();
|
impl_->Stop();
|
||||||
|
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
|
||||||
int ret = impl_->Start(show_cursor);
|
int ret = impl_->Start(show_cursor);
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
applied_show_cursor_ = show_cursor;
|
applied_show_cursor_ = show_cursor;
|
||||||
@@ -1403,7 +1399,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
|||||||
target_session_id = secure_shared_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) {
|
target_session_id != 0xFFFFFFFF) {
|
||||||
std::vector<uint8_t> response;
|
std::vector<uint8_t> response;
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
@@ -1414,15 +1410,9 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
|||||||
|
|
||||||
CloseSecureDesktopSharedFrame();
|
CloseSecureDesktopSharedFrame();
|
||||||
secure_shared_capture_started_ = false;
|
secure_shared_capture_started_ = false;
|
||||||
|
secure_shared_start_pending_ = false;
|
||||||
secure_shared_session_id_ = 0xFFFFFFFF;
|
secure_shared_session_id_ = 0xFFFFFFFF;
|
||||||
secure_shared_left_ = 0;
|
secure_shared_configuration_ = {};
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
||||||
@@ -1569,27 +1559,37 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SharedCaptureConfiguration configuration{left, top, width, height,
|
||||||
|
show_cursor, fps};
|
||||||
if (secure_shared_capture_started_ &&
|
if (secure_shared_capture_started_ &&
|
||||||
secure_shared_session_id_ == session_id &&
|
secure_shared_session_id_ == session_id &&
|
||||||
secure_shared_left_ == left && secure_shared_top_ == top &&
|
secure_shared_configuration_ == configuration &&
|
||||||
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 &&
|
|
||||||
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||||
return true;
|
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 =
|
const std::string command = BuildSecureCaptureStartCommand(
|
||||||
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
|
left, top, width, height, show_cursor, fps, stage, desktop);
|
||||||
stage, desktop);
|
|
||||||
std::vector<uint8_t> response;
|
std::vector<uint8_t> response;
|
||||||
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||||
error_out)) {
|
error_out)) {
|
||||||
|
secure_shared_start_pending_ =
|
||||||
|
error_out && IsPendingSecureCaptureStart(*error_out);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
secure_shared_start_pending_ = false;
|
||||||
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||||
if (json.is_discarded() || !json.value("ok", false)) {
|
if (json.is_discarded() || !json.value("ok", false)) {
|
||||||
if (error_out != nullptr) {
|
if (error_out != nullptr) {
|
||||||
@@ -1600,14 +1600,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
|||||||
|
|
||||||
secure_shared_capture_started_ = true;
|
secure_shared_capture_started_ = true;
|
||||||
secure_shared_session_id_ = session_id;
|
secure_shared_session_id_ = session_id;
|
||||||
secure_shared_left_ = left;
|
secure_shared_configuration_ = configuration;
|
||||||
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;
|
|
||||||
|
|
||||||
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||||
StopSecureDesktopSharedCapture(session_id);
|
StopSecureDesktopSharedCapture(session_id);
|
||||||
@@ -1628,6 +1621,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
ULONGLONG capture_stage_started_tick = 0;
|
ULONGLONG capture_stage_started_tick = 0;
|
||||||
bool post_secure_restart_pending = false;
|
bool post_secure_restart_pending = false;
|
||||||
bool desktop_was_unavailable = false;
|
bool desktop_was_unavailable = false;
|
||||||
|
bool normal_backend_suspended = false;
|
||||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||||
ULONGLONG last_post_secure_restart_tick = 0;
|
ULONGLONG last_post_secure_restart_tick = 0;
|
||||||
ULONGLONG next_cursor_update_tick = 0;
|
ULONGLONG next_cursor_update_tick = 0;
|
||||||
@@ -1685,9 +1679,14 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
const ULONGLONG now = GetTickCount64();
|
const ULONGLONG now = GetTickCount64();
|
||||||
const auto frame_started = std::chrono::steady_clock::now();
|
const auto frame_started = std::chrono::steady_clock::now();
|
||||||
report_stats(false);
|
report_stats(false);
|
||||||
if (auto sample = status_poller.Take()) {
|
auto sample = status_poller.Take();
|
||||||
const auto previous = std::exchange(status, std::move(sample->status));
|
if (sample || (desktop_available && status.capture_active)) {
|
||||||
max_status_ms = (std::max)(max_status_ms, sample->query_ms);
|
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 ||
|
if (status.service_available != previous.service_available ||
|
||||||
status.error != previous.error) {
|
status.error != previous.error) {
|
||||||
if (status.service_available) {
|
if (status.service_available) {
|
||||||
@@ -1718,8 +1717,12 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
report_stats(true);
|
report_stats(true);
|
||||||
// A restarted helper can leave our mapping/event handles alive, but
|
// A restarted helper can leave our mapping/event handles alive, but
|
||||||
// their old producer is gone. Explicitly establish a fresh stream.
|
// their old producer is gone. Explicitly establish a fresh stream.
|
||||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
if (status.capture_active != previous.capture_active ||
|
||||||
frame_schedule.Reset(now);
|
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 =
|
const bool secure_capture_started =
|
||||||
!previous.capture_active && status.capture_active;
|
!previous.capture_active && status.capture_active;
|
||||||
const bool secure_capture_ended =
|
const bool secure_capture_ended =
|
||||||
@@ -1734,8 +1737,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
post_secure_restart_pending = false;
|
post_secure_restart_pending = false;
|
||||||
post_secure_desktop_waiting_for_frame_.store(
|
post_secure_desktop_waiting_for_frame_.store(
|
||||||
false, std::memory_order_relaxed);
|
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);
|
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||||
} else if (secure_capture_ended) {
|
} else if (secure_capture_ended) {
|
||||||
post_secure_restart_pending = true;
|
post_secure_restart_pending = true;
|
||||||
@@ -1744,8 +1745,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
last_post_secure_restart_tick = 0;
|
last_post_secure_restart_tick = 0;
|
||||||
post_secure_desktop_waiting_for_frame_.store(
|
post_secure_desktop_waiting_for_frame_.store(
|
||||||
true, std::memory_order_relaxed);
|
true, std::memory_order_relaxed);
|
||||||
post_secure_desktop_drop_logged_.store(false,
|
|
||||||
std::memory_order_relaxed);
|
|
||||||
post_secure_desktop_started_tick_.store(now,
|
post_secure_desktop_started_tick_.store(now,
|
||||||
std::memory_order_relaxed);
|
std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
@@ -1763,28 +1762,52 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
last_post_secure_restart_tick = 0;
|
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) {
|
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
CheckDisplayPresence(now);
|
// Normal capture was intentionally unavailable on the secure desktop.
|
||||||
MaybeAdoptVirtualDisplay();
|
// Do not schedule a second topology/stall rebuild during its recovery.
|
||||||
CheckCaptureProgress(now);
|
if (!post_secure_restart_pending && desktop_available &&
|
||||||
|
!normal_backend_suspended) {
|
||||||
|
CheckDisplayPresence(now);
|
||||||
|
MaybeAdoptVirtualDisplay();
|
||||||
|
CheckCaptureProgress(now);
|
||||||
|
}
|
||||||
if (post_secure_restart_pending && desktop_available) {
|
if (post_secure_restart_pending && desktop_available) {
|
||||||
if (now >= post_secure_restart_deadline_tick) {
|
if (now >= post_secure_restart_deadline_tick) {
|
||||||
LOG_WARN(
|
LOG_WARN(
|
||||||
"Windows capturer: capture backend restart after secure desktop "
|
"Windows capturer: capture backend restart after secure desktop "
|
||||||
"timed out");
|
"still unavailable; retrying");
|
||||||
post_secure_restart_pending = false;
|
// 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 ||
|
} else if (last_post_secure_restart_tick == 0 ||
|
||||||
now - last_post_secure_restart_tick >=
|
now - last_post_secure_restart_tick >=
|
||||||
kPostSecureDesktopRestartRetryMs) {
|
kPostSecureDesktopRestartRetryMs) {
|
||||||
last_post_secure_restart_tick = now;
|
last_post_secure_restart_tick = now;
|
||||||
post_secure_restart_pending =
|
post_secure_restart_pending =
|
||||||
!RestartCaptureBackendAfterSecureDesktop();
|
!RestartCaptureBackendAfterSecureDesktop();
|
||||||
|
if (!post_secure_restart_pending) normal_backend_suspended = false;
|
||||||
if (!post_secure_restart_pending && privacy_)
|
if (!post_secure_restart_pending && privacy_)
|
||||||
privacy_->ResumeAfterDesktopRecovery();
|
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;
|
next_cursor_update_tick = now + 500;
|
||||||
ApplyCursorCaptureSetting();
|
ApplyCursorCaptureSetting();
|
||||||
}
|
}
|
||||||
@@ -1835,7 +1858,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
status.interactive_stage, status.interactive_desktop, show_cursor,
|
status.interactive_stage, status.interactive_desktop, show_cursor,
|
||||||
shared_fps, &error_message);
|
shared_fps, &error_message);
|
||||||
if (!shared_ready) {
|
if (!shared_ready) {
|
||||||
report_shared_error("start", error_message);
|
if (!secure_shared_start_pending_) report_shared_error("start", error_message);
|
||||||
frame_schedule.OnStartFailure(GetTickCount64());
|
frame_schedule.OnStartFailure(GetTickCount64());
|
||||||
} else if (!was_started) {
|
} else if (!was_started) {
|
||||||
frame_schedule.Reset(GetTickCount64());
|
frame_schedule.Reset(GetTickCount64());
|
||||||
@@ -1851,8 +1874,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
frame_schedule.OnSharedFrame(GetTickCount64());
|
frame_schedule.OnSharedFrame(GetTickCount64());
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool frame_pending = shared_ready && !frame_delivered &&
|
const bool frame_pending = secure_shared_start_pending_ ||
|
||||||
IsPendingSecureDesktopFrame(error_message);
|
(shared_ready && !frame_delivered && IsPendingSecureDesktopFrame(error_message));
|
||||||
if (frame_pending) ++shared_waits;
|
if (frame_pending) ++shared_waits;
|
||||||
if (shared_ready && !frame_delivered && !frame_pending) {
|
if (shared_ready && !frame_delivered && !frame_pending) {
|
||||||
report_shared_error("read", error_message);
|
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 =
|
const auto elapsed_ms =
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
std::chrono::steady_clock::now() - frame_started)
|
std::chrono::steady_clock::now() - frame_started)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include "privacy_controller.h"
|
#include "privacy_controller.h"
|
||||||
// Relative: platform include directories stay private to the capture target.
|
// Relative: platform include directories stay private to the capture target.
|
||||||
#include "../virtual_display/virtual_display_provisioner.h"
|
#include "../virtual_display/virtual_display_provisioner.h"
|
||||||
|
#include "../service/shared_capture_configuration.h"
|
||||||
#include "headless_recovery.h"
|
#include "headless_recovery.h"
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
@@ -117,7 +118,6 @@ class ScreenCapturerWin : public ScreenCapturer {
|
|||||||
std::atomic<int> monitor_index_{0};
|
std::atomic<int> monitor_index_{0};
|
||||||
std::atomic<bool> secure_desktop_capture_active_{false};
|
std::atomic<bool> secure_desktop_capture_active_{false};
|
||||||
std::atomic<bool> post_secure_desktop_waiting_for_frame_{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::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
|
||||||
std::thread secure_capture_thread_;
|
std::thread secure_capture_thread_;
|
||||||
HANDLE secure_frame_mapping_ = nullptr;
|
HANDLE secure_frame_mapping_ = nullptr;
|
||||||
@@ -125,15 +125,9 @@ class ScreenCapturerWin : public ScreenCapturer {
|
|||||||
uint8_t* secure_frame_view_ = nullptr;
|
uint8_t* secure_frame_view_ = nullptr;
|
||||||
size_t secure_frame_view_size_ = 0;
|
size_t secure_frame_view_size_ = 0;
|
||||||
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
|
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
|
||||||
int secure_shared_left_ = 0;
|
SharedCaptureConfiguration secure_shared_configuration_;
|
||||||
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_;
|
|
||||||
bool secure_shared_capture_started_ = false;
|
bool secure_shared_capture_started_ = false;
|
||||||
|
bool secure_shared_start_pending_ = false;
|
||||||
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
|
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
|
||||||
|
|
||||||
void BuildCanonicalFromImpl();
|
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 {
|
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) {
|
inline bool IsPendingSecureDesktopFrame(const std::string& error) {
|
||||||
return error == "frame_wait_timeout" ||
|
return error == "frame_wait_timeout" ||
|
||||||
error == "shared_frame_write_in_progress" ||
|
error == "shared_frame_write_in_progress" ||
|
||||||
@@ -47,4 +53,4 @@ class SecureDesktopFrameSchedule {
|
|||||||
|
|
||||||
} // namespace crossdesk
|
} // 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 service_available = false;
|
||||||
bool desktop_state_known = false;
|
bool desktop_state_known = false;
|
||||||
bool capture_active = false;
|
bool capture_active = false;
|
||||||
|
bool consent_ui_visible = false;
|
||||||
bool helper_running = false;
|
bool helper_running = false;
|
||||||
uint32_t helper_process_id = 0;
|
uint32_t helper_process_id = 0;
|
||||||
uint32_t active_session_id = 0xFFFFFFFF;
|
uint32_t active_session_id = 0xFFFFFFFF;
|
||||||
@@ -31,6 +32,16 @@ struct SecureDesktopServiceStatus {
|
|||||||
std::string interactive_desktop;
|
std::string interactive_desktop;
|
||||||
std::string error;
|
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) {
|
void UpdateFrom(SecureDesktopServiceStatus latest) {
|
||||||
if (latest.service_available || latest.desktop_state_known) {
|
if (latest.service_available || latest.desktop_state_known) {
|
||||||
*this = std::move(latest);
|
*this = std::move(latest);
|
||||||
@@ -108,4 +119,4 @@ class SecureDesktopStatusPoller {
|
|||||||
|
|
||||||
} // namespace crossdesk
|
} // 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 {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
inline bool IsDesktopTransitionInputError(DWORD error) {
|
||||||
|
return error == ERROR_ACCESS_DENIED ||
|
||||||
|
error == ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION;
|
||||||
|
}
|
||||||
|
|
||||||
inline std::wstring GetDesktopNameW(HDESK desktop) {
|
inline std::wstring GetDesktopNameW(HDESK desktop) {
|
||||||
DWORD bytes_needed = 0;
|
DWORD bytes_needed = 0;
|
||||||
if (desktop == nullptr) {
|
if (desktop == nullptr) {
|
||||||
@@ -149,4 +154,4 @@ class ScopedInteractiveDesktop {
|
|||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "interactive_state.h"
|
#include "interactive_state.h"
|
||||||
|
#include "interactive_desktop.h"
|
||||||
|
#include "desktop_transition_policy.h"
|
||||||
|
#include "named_pipe_deadline.h"
|
||||||
#include "path_manager.h"
|
#include "path_manager.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "session_helper_shared.h"
|
#include "session_helper_shared.h"
|
||||||
@@ -267,80 +270,14 @@ bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) {
|
|||||||
std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
||||||
const std::string& command,
|
const std::string& command,
|
||||||
DWORD timeout_ms) {
|
DWORD timeout_ms) {
|
||||||
constexpr DWORD kPipeConnectRetryDelayMs = 15;
|
std::vector<uint8_t> response;
|
||||||
const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms;
|
std::string error;
|
||||||
|
DWORD code = ERROR_SUCCESS;
|
||||||
auto is_transient_pipe_error = [](DWORD error) {
|
if (!QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms,
|
||||||
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY ||
|
&response, &error, &code)) {
|
||||||
error == ERROR_SEM_TIMEOUT;
|
return BuildErrorJson(error.c_str(), code);
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
return std::string(response.begin(), response.end());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down,
|
std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down,
|
||||||
@@ -1079,6 +1016,8 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
|||||||
return ERROR_INVALID_HANDLE;
|
return ERROR_INVALID_HANDLE;
|
||||||
|
|
||||||
started_at_tick_ = GetTickCount64();
|
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;
|
last_sas_tick_ = 0;
|
||||||
active_session_id_ = WTSGetActiveConsoleSessionId();
|
active_session_id_ = WTSGetActiveConsoleSessionId();
|
||||||
process_session_id_ = 0xFFFFFFFF;
|
process_session_id_ = 0xFFFFFFFF;
|
||||||
@@ -1136,6 +1075,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
|||||||
last_session_event_session_id_ = active_session_id_;
|
last_session_event_session_id_ = active_session_id_;
|
||||||
RefreshSessionState();
|
RefreshSessionState();
|
||||||
EnsureSessionHelper();
|
EnsureSessionHelper();
|
||||||
|
session_state_thread_ = std::thread(&CrossDeskServiceHost::SessionStateLoop, this);
|
||||||
ipc_thread_ = std::thread(&CrossDeskServiceHost::IpcServerLoop, this);
|
ipc_thread_ = std::thread(&CrossDeskServiceHost::IpcServerLoop, this);
|
||||||
if (!console_mode_) {
|
if (!console_mode_) {
|
||||||
client_process_monitor_thread_ =
|
client_process_monitor_thread_ =
|
||||||
@@ -1159,8 +1099,13 @@ void CrossDeskServiceHost::ShutdownRuntime() {
|
|||||||
if (ipc_thread_.joinable()) {
|
if (ipc_thread_.joinable()) {
|
||||||
ipc_thread_.join();
|
ipc_thread_.join();
|
||||||
}
|
}
|
||||||
|
if (session_state_thread_.joinable()) session_state_thread_.join();
|
||||||
StopSecureInputHelper();
|
StopSecureInputHelper();
|
||||||
StopSessionHelper();
|
StopSessionHelper();
|
||||||
|
if (session_state_event_) {
|
||||||
|
CloseHandle(session_state_event_);
|
||||||
|
session_state_event_ = nullptr;
|
||||||
|
}
|
||||||
if (lifetime_mutex_) {
|
if (lifetime_mutex_) {
|
||||||
ReleaseMutex(lifetime_mutex_);
|
ReleaseMutex(lifetime_mutex_);
|
||||||
CloseHandle(lifetime_mutex_);
|
CloseHandle(lifetime_mutex_);
|
||||||
@@ -1399,12 +1344,22 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
|||||||
LOG_WARN("Pipe security initialization failed, error={}", GetLastError());
|
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 &&
|
while (stop_event_ != nullptr &&
|
||||||
WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) {
|
WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) {
|
||||||
HANDLE pipe = CreateNamedPipeW(
|
// Keep the name registered between requests. Recreating the only instance
|
||||||
kCrossDeskServicePipeName, PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED,
|
// made high-rate mouse clients observe FILE_NOT_FOUND and sleep on retries.
|
||||||
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 4096, 4096, 0,
|
if (pipe == INVALID_HANDLE_VALUE) {
|
||||||
pipe_attributes);
|
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) {
|
if (pipe == INVALID_HANDLE_VALUE) {
|
||||||
DWORD error = GetLastError();
|
DWORD error = GetLastError();
|
||||||
LOG_ERROR("CreateNamedPipeW failed, error={}", error);
|
LOG_ERROR("CreateNamedPipeW failed, error={}", error);
|
||||||
@@ -1413,12 +1368,8 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OVERLAPPED overlapped{};
|
OVERLAPPED overlapped{};
|
||||||
overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
overlapped.hEvent = event.value;
|
||||||
if (overlapped.hEvent == nullptr) {
|
ResetEvent(event.value);
|
||||||
LOG_ERROR("CreateEventW for pipe failed, error={}", GetLastError());
|
|
||||||
CloseHandle(pipe);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOL connected = ConnectNamedPipe(pipe, &overlapped);
|
BOOL connected = ConnectNamedPipe(pipe, &overlapped);
|
||||||
DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError();
|
DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError();
|
||||||
@@ -1432,8 +1383,8 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
|||||||
|
|
||||||
if (!connected && connect_error != ERROR_IO_PENDING) {
|
if (!connected && connect_error != ERROR_IO_PENDING) {
|
||||||
LOG_WARN("ConnectNamedPipe failed, error={}", connect_error);
|
LOG_WARN("ConnectNamedPipe failed, error={}", connect_error);
|
||||||
CloseHandle(overlapped.hEvent);
|
|
||||||
CloseHandle(pipe);
|
CloseHandle(pipe);
|
||||||
|
pipe = INVALID_HANDLE_VALUE;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1441,17 +1392,20 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
|||||||
HANDLE wait_handles[] = {stop_event_, overlapped.hEvent};
|
HANDLE wait_handles[] = {stop_event_, overlapped.hEvent};
|
||||||
DWORD wait_result =
|
DWORD wait_result =
|
||||||
WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
|
WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
|
||||||
if (wait_result == WAIT_OBJECT_0) {
|
if (wait_result != WAIT_OBJECT_0 + 1) {
|
||||||
CancelIoEx(pipe, &overlapped);
|
CancelIoEx(pipe, &overlapped);
|
||||||
CloseHandle(overlapped.hEvent);
|
DWORD ignored = 0;
|
||||||
CloseHandle(pipe);
|
GetOverlappedResult(pipe, &overlapped, &ignored, TRUE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
char buffer[1024] = {0};
|
char buffer[1024] = {0};
|
||||||
DWORD bytes_read = 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) {
|
bytes_read > 0) {
|
||||||
ULONG client_session_id = 0xFFFFFFFF;
|
ULONG client_session_id = 0xFFFFFFFF;
|
||||||
// Obtain identity from Windows, never from caller-supplied JSON. A GUI
|
// Obtain identity from Windows, never from caller-supplied JSON. A GUI
|
||||||
@@ -1462,43 +1416,80 @@ void CrossDeskServiceHost::IpcServerLoop() {
|
|||||||
client_session_id)
|
client_session_id)
|
||||||
: BuildErrorJson("client_session_query_failed", GetLastError());
|
: BuildErrorJson("client_session_query_failed", GetLastError());
|
||||||
DWORD bytes_written = 0;
|
DWORD bytes_written = 0;
|
||||||
WriteFile(pipe, response.data(), static_cast<DWORD>(response.size()),
|
if (pipe_deadline_detail::Transfer(
|
||||||
&bytes_written, nullptr);
|
pipe, overlapped.hEvent, true, const_cast<char*>(response.data()),
|
||||||
FlushFileBuffers(pipe);
|
static_cast<DWORD>(response.size()), GetTickCount64() + 1000,
|
||||||
|
&bytes_written, &transfer_error)) {
|
||||||
|
FlushFileBuffers(pipe);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisconnectNamedPipe(pipe);
|
DisconnectNamedPipe(pipe);
|
||||||
CloseHandle(overlapped.hEvent);
|
|
||||||
CloseHandle(pipe);
|
|
||||||
}
|
}
|
||||||
|
if (pipe != INVALID_HANDLE_VALUE) CloseHandle(pipe);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossDeskServiceHost::RefreshSessionState() {
|
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_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
|
if (generation != session_state_generation_ ||
|
||||||
|
session != WTSGetActiveConsoleSessionId()) return;
|
||||||
const DWORD previous_session_id = active_session_id_;
|
const DWORD previous_session_id = active_session_id_;
|
||||||
active_session_id_ = WTSGetActiveConsoleSessionId();
|
active_session_id_ = session;
|
||||||
if (active_session_id_ != previous_session_id) {
|
if (active_session_id_ != previous_session_id) {
|
||||||
|
last_session_transition_tick_ = probe_started;
|
||||||
|
session_lock_state_known_ = false;
|
||||||
ResetSessionHelperReportedStateLocked("session_changed", 0);
|
ResetSessionHelperReportedStateLocked("session_changed", 0);
|
||||||
sas_secure_desktop_until_tick_ = 0;
|
sas_secure_desktop_until_tick_ = 0;
|
||||||
sas_secure_desktop_seen_ = false;
|
sas_secure_desktop_seen_ = false;
|
||||||
}
|
}
|
||||||
DWORD process_session_id = 0xFFFFFFFF;
|
process_session_id_ = process_session;
|
||||||
if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) {
|
logon_ui_visible_ = logon_ui;
|
||||||
process_session_id_ = process_session_id;
|
consent_ui_visible_ = consent_ui;
|
||||||
}
|
input_desktop_available_ = desktop.available;
|
||||||
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
|
input_desktop_error_code_ = desktop.error_code;
|
||||||
consent_ui_visible_ = IsConsentUiRunningInSession(active_session_id_);
|
input_desktop_name_ = desktop.name;
|
||||||
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_ =
|
secure_desktop_active_ =
|
||||||
_stricmp(input_desktop_name_.c_str(), "Winlogon") == 0;
|
_stricmp(input_desktop_name_.c_str(), "Winlogon") == 0;
|
||||||
|
|
||||||
std::wstring username;
|
session_username_ = WideToUtf8(username);
|
||||||
bool username_available = GetSessionUserName(active_session_id_, &username);
|
prelogin_ = prelogin;
|
||||||
prelogin_ = !username_available || username.empty();
|
if (lock_known) {
|
||||||
if (!QuerySessionLockState(active_session_id_, &session_locked_)) {
|
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_ =
|
session_locked_ =
|
||||||
(logon_ui_visible_ || secure_desktop_active_) && !prelogin_;
|
(logon_ui_visible_ || secure_desktop_active_) && !prelogin_;
|
||||||
}
|
}
|
||||||
@@ -1527,8 +1518,8 @@ void CrossDeskServiceHost::ResetSessionHelperReportedStateLocked(
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CrossDeskServiceHost::GetEffectiveSessionLockedLocked() const {
|
bool CrossDeskServiceHost::GetEffectiveSessionLockedLocked() const {
|
||||||
return session_helper_status_ok_ ? session_helper_report_session_locked_
|
return session_lock_state_known_ || !session_helper_status_ok_
|
||||||
: session_locked_;
|
? session_locked_ : session_helper_report_session_locked_;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
|
bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
|
||||||
@@ -1583,8 +1574,7 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return HasSecureInputUiLocked() || (GetEffectiveSessionLockedLocked() &&
|
return HasSecureInputUiLocked() || GetEffectiveSessionLockedLocked();
|
||||||
IsHelperReportingLockScreenLocked());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||||
@@ -1601,6 +1591,10 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
|||||||
return session_helper_report_interactive_stage_;
|
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 =
|
const bool service_host_credential_ui_visible =
|
||||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||||
consent_ui_visible_,
|
consent_ui_visible_,
|
||||||
@@ -2071,14 +2065,17 @@ void CrossDeskServiceHost::EnsureSessionHelper() {
|
|||||||
void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
||||||
DWORD target_session_id = 0xFFFFFFFF;
|
DWORD target_session_id = 0xFFFFFFFF;
|
||||||
bool helper_running = false;
|
bool helper_running = false;
|
||||||
|
uint64_t generation = 0;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
target_session_id = session_helper_session_id_;
|
target_session_id = session_helper_session_id_;
|
||||||
helper_running = session_helper_running_;
|
helper_running = session_helper_running_;
|
||||||
|
generation = session_state_generation_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!helper_running || target_session_id == 0xFFFFFFFF) {
|
if (!helper_running || target_session_id == 0xFFFFFFFF) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
|
if (generation != session_state_generation_) return;
|
||||||
ResetSessionHelperReportedStateLocked("helper_not_running", 0);
|
ResetSessionHelperReportedStateLocked("helper_not_running", 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2089,12 +2086,14 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
|||||||
Json json = Json::parse(response, nullptr, false);
|
Json json = Json::parse(response, nullptr, false);
|
||||||
if (json.is_discarded()) {
|
if (json.is_discarded()) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
|
if (generation != session_state_generation_) return;
|
||||||
ResetSessionHelperReportedStateLocked("invalid_helper_status_json", 0);
|
ResetSessionHelperReportedStateLocked("invalid_helper_status_json", 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!json.value("ok", false)) {
|
if (!json.value("ok", false)) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
|
if (generation != session_state_generation_) return;
|
||||||
const std::string error =
|
const std::string error =
|
||||||
json.value("error", std::string("helper_status_failed"));
|
json.value("error", std::string("helper_status_failed"));
|
||||||
ResetSessionHelperReportedStateLocked(
|
ResetSessionHelperReportedStateLocked(
|
||||||
@@ -2103,6 +2102,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
|
if (generation != session_state_generation_) return;
|
||||||
if (target_session_id != active_session_id_ ||
|
if (target_session_id != active_session_id_ ||
|
||||||
target_session_id != session_helper_session_id_ ||
|
target_session_id != session_helper_session_id_ ||
|
||||||
json.value("session_id", static_cast<DWORD>(0xFFFFFFFF)) !=
|
json.value("session_id", static_cast<DWORD>(0xFFFFFFFF)) !=
|
||||||
@@ -2111,6 +2111,20 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
|||||||
ResetSessionHelperReportedStateLocked("helper_session_mismatch", 0);
|
ResetSessionHelperReportedStateLocked("helper_session_mismatch", 0);
|
||||||
return;
|
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_ok_ = true;
|
||||||
session_helper_status_error_.clear();
|
session_helper_status_error_.clear();
|
||||||
session_helper_status_error_code_ = 0;
|
session_helper_status_error_code_ = 0;
|
||||||
@@ -2152,6 +2166,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
last_session_event_type_ = event_type;
|
last_session_event_type_ = event_type;
|
||||||
|
++session_state_generation_;
|
||||||
last_session_event_session_id_ = session_id;
|
last_session_event_session_id_ = session_id;
|
||||||
const DWORD previous_session_id = active_session_id_;
|
const DWORD previous_session_id = active_session_id_;
|
||||||
active_session_id_ = WTSGetActiveConsoleSessionId();
|
active_session_id_ = WTSGetActiveConsoleSessionId();
|
||||||
@@ -2160,42 +2175,26 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
|||||||
sas_secure_desktop_until_tick_ = 0;
|
sas_secure_desktop_until_tick_ = 0;
|
||||||
sas_secure_desktop_seen_ = false;
|
sas_secure_desktop_seen_ = false;
|
||||||
}
|
}
|
||||||
DWORD process_session_id = 0xFFFFFFFF;
|
if (session_id == active_session_id_) {
|
||||||
if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) {
|
// Do not let an older helper sample override a session notification.
|
||||||
process_session_id_ = process_session_id;
|
ResetSessionHelperReportedStateLocked("session_event", 0);
|
||||||
}
|
last_session_transition_tick_ = GetTickCount64();
|
||||||
logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_);
|
if (event_type == WTS_SESSION_LOCK) {
|
||||||
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) {
|
|
||||||
session_locked_ = true;
|
session_locked_ = true;
|
||||||
} else if (session_id == active_session_id_ &&
|
session_lock_state_known_ = true;
|
||||||
(event_type == WTS_SESSION_UNLOCK ||
|
}
|
||||||
event_type == WTS_SESSION_LOGON)) {
|
if (event_type == WTS_SESSION_UNLOCK || event_type == WTS_SESSION_LOGON) {
|
||||||
session_locked_ = false;
|
session_locked_ = false;
|
||||||
} else if (logon_ui_visible_ || secure_desktop_active_) {
|
session_lock_state_known_ = true;
|
||||||
session_locked_ = !prelogin_;
|
sas_secure_desktop_until_tick_ = 0;
|
||||||
|
sas_secure_desktop_seen_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Session event: type={}, session_id={}, active_session_id={}",
|
LOG_INFO("Session event: type={}, session_id={}, active_session_id={}",
|
||||||
SessionEventToString(event_type), session_id, active_session_id_);
|
SessionEventToString(event_type), session_id, WTSGetActiveConsoleSessionId());
|
||||||
EnsureSessionHelper();
|
WakeSessionState();
|
||||||
if (!secure_desktop_active_ && !logon_ui_visible_ && !consent_ui_visible_) {
|
|
||||||
StopSecureInputHelper();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
|
std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
|
||||||
@@ -2228,17 +2227,22 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command,
|
|||||||
return BuildErrorJson("unknown_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();
|
ReapSecureInputHelper();
|
||||||
ReapSessionHelper();
|
ReapSessionHelper();
|
||||||
RefreshSessionState();
|
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();
|
EnsureSessionHelper();
|
||||||
RefreshSessionHelperReportedState();
|
RefreshSessionHelperReportedState();
|
||||||
bool keep_secure_input_helper = false;
|
bool keep_secure_input_helper = false;
|
||||||
@@ -2257,11 +2261,7 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
|
|||||||
launch_secure_input_helper =
|
launch_secure_input_helper =
|
||||||
keep_secure_input_helper &&
|
keep_secure_input_helper &&
|
||||||
(!secure_input_helper_running_ ||
|
(!secure_input_helper_running_ ||
|
||||||
secure_input_helper_session_id_ != secure_input_target_session_id ||
|
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keep_secure_input_helper) {
|
if (keep_secure_input_helper) {
|
||||||
@@ -2274,14 +2274,16 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
|
|||||||
} else {
|
} else {
|
||||||
StopSecureInputHelper();
|
StopSecureInputHelper();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
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");
|
return BuildErrorJson("service_session_mismatch");
|
||||||
}
|
}
|
||||||
std::wstring username;
|
std::string username_utf8 = EscapeJsonString(session_username_);
|
||||||
GetSessionUserName(active_session_id_, &username);
|
|
||||||
std::string username_utf8 = EscapeJsonString(WideToUtf8(username));
|
|
||||||
std::string input_desktop = EscapeJsonString(input_desktop_name_);
|
std::string input_desktop = EscapeJsonString(input_desktop_name_);
|
||||||
std::string last_sas_error = EscapeJsonString(last_sas_error_);
|
std::string last_sas_error = EscapeJsonString(last_sas_error_);
|
||||||
std::string session_helper_last_error =
|
std::string session_helper_last_error =
|
||||||
@@ -2467,12 +2469,11 @@ std::string CrossDeskServiceHost::BuildStatusResponse(DWORD client_session_id) {
|
|||||||
|
|
||||||
std::string CrossDeskServiceHost::SendSecureAttentionSequence(
|
std::string CrossDeskServiceHost::SendSecureAttentionSequence(
|
||||||
DWORD client_session_id) {
|
DWORD client_session_id) {
|
||||||
RefreshSessionState();
|
|
||||||
if (client_session_id == 0xFFFFFFFF ||
|
if (client_session_id == 0xFFFFFFFF ||
|
||||||
client_session_id != WTSGetActiveConsoleSessionId()) {
|
client_session_id != WTSGetActiveConsoleSessionId()) {
|
||||||
return BuildErrorJson("service_session_mismatch");
|
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();
|
SasResult result = SendSasNow();
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
@@ -2489,114 +2490,68 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence(
|
|||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return BuildErrorJson(result.error.c_str(), result.error_code);
|
return BuildErrorJson(result.error.c_str(), result.error_code);
|
||||||
}
|
}
|
||||||
|
WakeSessionState();
|
||||||
return "{\"ok\":true,\"sent\":\"sas\"}";
|
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(
|
std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
||||||
DWORD client_session_id, int key_code, bool is_down,
|
DWORD client_session_id, int key_code, bool is_down,
|
||||||
uint32_t scan_code, bool extended) {
|
uint32_t scan_code, bool extended) {
|
||||||
RefreshSessionState();
|
SecureInputTarget target;
|
||||||
ReapSecureInputHelper();
|
const auto error = ResolveSecureInputTarget(client_session_id, target);
|
||||||
EnsureSessionHelper();
|
if (!error.empty()) return error;
|
||||||
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_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return QueryNamedPipeMessage(
|
return QueryNamedPipeMessage(
|
||||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
GetCrossDeskSecureInputHelperPipeName(target.session_id),
|
||||||
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
|
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
|
||||||
extended, interactive_stage,
|
extended, target.stage,
|
||||||
interactive_desktop),
|
target.desktop),
|
||||||
1000);
|
1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(
|
std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(
|
||||||
DWORD client_session_id, int x, int y, int wheel, int flag) {
|
DWORD client_session_id, int x, int y, int wheel, int flag) {
|
||||||
RefreshSessionState();
|
SecureInputTarget target;
|
||||||
ReapSecureInputHelper();
|
const auto error = ResolveSecureInputTarget(client_session_id, target);
|
||||||
EnsureSessionHelper();
|
if (!error.empty()) return error;
|
||||||
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_);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return QueryNamedPipeMessage(
|
return QueryNamedPipeMessage(
|
||||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
GetCrossDeskSecureInputHelperPipeName(target.session_id),
|
||||||
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage,
|
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, target.stage,
|
||||||
interactive_desktop),
|
target.desktop),
|
||||||
1000);
|
1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2831,18 +2786,40 @@ std::string QueryCrossDeskService(const std::string& command,
|
|||||||
return QueryNamedPipeMessage(kCrossDeskServicePipeName, command, timeout_ms);
|
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,
|
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
|
||||||
uint32_t scan_code,
|
uint32_t scan_code,
|
||||||
bool extended,
|
bool extended,
|
||||||
DWORD timeout_ms) {
|
DWORD timeout_ms) {
|
||||||
return QueryCrossDeskService(BuildSecureDesktopKeyboardIpcCommand(
|
return QueryDesktopInput(BuildSecureDesktopKeyboardIpcCommand(
|
||||||
key_code, is_down, scan_code, extended),
|
key_code, is_down, scan_code, extended),
|
||||||
timeout_ms);
|
timeout_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||||
int flag, DWORD timeout_ms) {
|
int flag, DWORD timeout_ms) {
|
||||||
return QueryCrossDeskService(
|
return QueryDesktopInput(
|
||||||
BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms);
|
BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class CrossDeskServiceHost {
|
|||||||
void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code,
|
void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code,
|
||||||
DWORD wait_hint);
|
DWORD wait_hint);
|
||||||
void IpcServerLoop();
|
void IpcServerLoop();
|
||||||
|
void SessionStateLoop();
|
||||||
|
void RefreshInteractiveState();
|
||||||
|
void WakeSessionState();
|
||||||
void RefreshSessionState();
|
void RefreshSessionState();
|
||||||
void EnsureSessionHelper();
|
void EnsureSessionHelper();
|
||||||
void ReapSessionHelper();
|
void ReapSessionHelper();
|
||||||
@@ -72,6 +75,14 @@ class CrossDeskServiceHost {
|
|||||||
DWORD client_session_id);
|
DWORD client_session_id);
|
||||||
std::string BuildStatusResponse(DWORD client_session_id);
|
std::string BuildStatusResponse(DWORD client_session_id);
|
||||||
std::string SendSecureAttentionSequence(DWORD client_session_id);
|
std::string SendSecureAttentionSequence(DWORD client_session_id);
|
||||||
|
struct SecureInputTarget {
|
||||||
|
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,
|
std::string SendSecureDesktopKeyboardInput(DWORD client_session_id,
|
||||||
int key_code, bool is_down,
|
int key_code, bool is_down,
|
||||||
uint32_t scan_code = 0,
|
uint32_t scan_code = 0,
|
||||||
@@ -89,6 +100,8 @@ class CrossDeskServiceHost {
|
|||||||
SERVICE_STATUS service_status_{};
|
SERVICE_STATUS service_status_{};
|
||||||
HANDLE stop_event_ = nullptr;
|
HANDLE stop_event_ = nullptr;
|
||||||
std::thread ipc_thread_;
|
std::thread ipc_thread_;
|
||||||
|
std::thread session_state_thread_;
|
||||||
|
HANDLE session_state_event_ = nullptr;
|
||||||
std::thread client_process_monitor_thread_;
|
std::thread client_process_monitor_thread_;
|
||||||
// Owned exclusively by the monitor, then drained after it joins.
|
// Owned exclusively by the monitor, then drained after it joins.
|
||||||
HANDLE unattended_process_ = nullptr;
|
HANDLE unattended_process_ = nullptr;
|
||||||
@@ -96,6 +109,9 @@ class CrossDeskServiceHost {
|
|||||||
DWORD unattended_session_ = 0xFFFFFFFF;
|
DWORD unattended_session_ = 0xFFFFFFFF;
|
||||||
std::atomic<uint64_t> unattended_logoff_generation_{0};
|
std::atomic<uint64_t> unattended_logoff_generation_{0};
|
||||||
std::mutex state_mutex_;
|
std::mutex state_mutex_;
|
||||||
|
uint64_t session_state_generation_ = 0;
|
||||||
|
ULONGLONG last_session_transition_tick_ = 0;
|
||||||
|
bool session_lock_state_known_ = false;
|
||||||
HANDLE lifetime_mutex_ = nullptr;
|
HANDLE lifetime_mutex_ = nullptr;
|
||||||
std::wstring lifetime_mutex_name_;
|
std::wstring lifetime_mutex_name_;
|
||||||
DWORD active_session_id_ = 0xFFFFFFFF;
|
DWORD active_session_id_ = 0xFFFFFFFF;
|
||||||
@@ -148,6 +164,7 @@ class CrossDeskServiceHost {
|
|||||||
HANDLE secure_input_helper_process_handle_ = nullptr;
|
HANDLE secure_input_helper_process_handle_ = nullptr;
|
||||||
HANDLE secure_input_helper_stop_event_ = nullptr;
|
HANDLE secure_input_helper_stop_event_ = nullptr;
|
||||||
std::string input_desktop_name_;
|
std::string input_desktop_name_;
|
||||||
|
std::string session_username_;
|
||||||
std::string last_sas_error_;
|
std::string last_sas_error_;
|
||||||
std::string session_helper_last_error_;
|
std::string session_helper_last_error_;
|
||||||
std::string session_helper_status_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 <vector>
|
||||||
|
|
||||||
#include "interactive_desktop.h"
|
#include "interactive_desktop.h"
|
||||||
|
#include "named_pipe_deadline.h"
|
||||||
#include "path_manager.h"
|
#include "path_manager.h"
|
||||||
|
#include "platform/path_backend.h"
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
#include "session_helper_shared.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 "service_lifetime.h"
|
||||||
|
#include "shared_capture_lifecycle.h"
|
||||||
|
#include "shared_capture_configuration.h"
|
||||||
#include "usbmmidd_policy.h"
|
#include "usbmmidd_policy.h"
|
||||||
#include "usbmmidd_virtual_display.h"
|
#include "usbmmidd_virtual_display.h"
|
||||||
#include "windows_cursor_state.h"
|
#include "windows_cursor_state.h"
|
||||||
#include "cursor_draw.h"
|
#include "cursor_draw.h"
|
||||||
#include "windows_input_marker.h"
|
#include "windows_input_marker.h"
|
||||||
#include "windows_key_metadata.h"
|
#include "windows_key_metadata.h"
|
||||||
|
#include "../windows_thread_dpi.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -67,13 +75,7 @@ struct HelperState {
|
|||||||
ULONGLONG last_update_tick = 0;
|
ULONGLONG last_update_tick = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SecureCaptureRequest {
|
struct SecureCaptureRequest : crossdesk::SharedCaptureConfiguration {
|
||||||
int left = 0;
|
|
||||||
int top = 0;
|
|
||||||
int width = 0;
|
|
||||||
int height = 0;
|
|
||||||
bool show_cursor = true;
|
|
||||||
int fps = 30;
|
|
||||||
std::string interactive_stage;
|
std::string interactive_stage;
|
||||||
std::string interactive_desktop;
|
std::string interactive_desktop;
|
||||||
};
|
};
|
||||||
@@ -87,21 +89,15 @@ struct SecureMouseRequest {
|
|||||||
std::string interactive_desktop;
|
std::string interactive_desktop;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SecureCaptureBuffers {
|
|
||||||
std::vector<uint8_t> nv12_frame;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SecureSharedCaptureState {
|
struct SecureSharedCaptureState {
|
||||||
std::mutex mutex;
|
|
||||||
std::thread capture_thread;
|
|
||||||
std::atomic<bool> stop_requested{false};
|
|
||||||
DWORD session_id = 0xFFFFFFFF;
|
DWORD session_id = 0xFFFFFFFF;
|
||||||
SecureCaptureRequest request;
|
SecureCaptureRequest request;
|
||||||
HANDLE frame_mapping = nullptr;
|
HANDLE frame_mapping = nullptr;
|
||||||
HANDLE frame_ready_event = nullptr;
|
HANDLE frame_ready_event = nullptr;
|
||||||
uint8_t* frame_view = nullptr;
|
uint8_t* frame_view = nullptr;
|
||||||
size_t frame_view_size = 0;
|
|
||||||
uint32_t sequence = 0;
|
uint32_t sequence = 0;
|
||||||
|
// Last member: joins/cleans the producer before any captured state is destroyed.
|
||||||
|
crossdesk::SharedCaptureLifecycle lifecycle;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct IpcSecurityAttributes {
|
struct IpcSecurityAttributes {
|
||||||
@@ -136,12 +132,23 @@ void InitializeHelperLogger() {
|
|||||||
static std::once_flag once_flag;
|
static std::once_flag once_flag;
|
||||||
std::call_once(once_flag, []() {
|
std::call_once(once_flag, []() {
|
||||||
crossdesk::PathManager path_manager("CrossDesk");
|
crossdesk::PathManager path_manager("CrossDesk");
|
||||||
std::filesystem::path log_path =
|
const auto initialize = [&](const std::filesystem::path& log_path) {
|
||||||
path_manager.GetLogPath() / "session_helper";
|
if (log_path.empty() || !path_manager.CreateDirectories(log_path)) return false;
|
||||||
if (!log_path.empty() && path_manager.CreateDirectories(log_path)) {
|
try {
|
||||||
InitLogger(log_path.string());
|
InitLogger(log_path.string());
|
||||||
return;
|
// 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");
|
InitLogger("logs/session_helper");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -381,15 +388,16 @@ void UpdateHelperState(HelperState* helper_state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
InputDesktopInfo desktop_info = GetInputDesktopInfo();
|
const ULONGLONG sample_started = GetTickCount64();
|
||||||
bool lock_app_process_running =
|
bool lock_app_process_running =
|
||||||
IsLockAppRunningInCurrentSession(helper_state->session_id);
|
IsLockAppRunningInCurrentSession(helper_state->session_id);
|
||||||
bool logon_ui_visible =
|
bool logon_ui_visible =
|
||||||
IsLogonUiRunningInCurrentSession(helper_state->session_id);
|
IsLogonUiRunningInCurrentSession(helper_state->session_id);
|
||||||
bool consent_ui_visible =
|
bool consent_ui_visible =
|
||||||
IsConsentUiRunningInCurrentSession(helper_state->session_id);
|
IsConsentUiRunningInCurrentSession(helper_state->session_id);
|
||||||
const bool consent_on_input_desktop =
|
// Process enumeration can straddle an unlock. Observe the input desktop
|
||||||
desktop_info.available && consent_ui_visible;
|
// afterwards, and expose the sample's start time for service-side rejection.
|
||||||
|
InputDesktopInfo desktop_info = GetInputDesktopInfo();
|
||||||
const bool input_desktop_is_winlogon =
|
const bool input_desktop_is_winlogon =
|
||||||
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
|
_stricmp(desktop_info.name.c_str(), "Winlogon") == 0;
|
||||||
const bool inaccessible_secure_input_desktop =
|
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->input_desktop_name = desktop_info.name;
|
||||||
helper_state->lock_app_visible = lock_app_visible;
|
helper_state->lock_app_visible = lock_app_visible;
|
||||||
helper_state->logon_ui_visible = logon_ui_visible;
|
helper_state->logon_ui_visible = logon_ui_visible;
|
||||||
helper_state->consent_ui_visible =
|
helper_state->consent_ui_visible = consent_ui_visible;
|
||||||
consent_on_input_desktop || consent_ui_visible;
|
|
||||||
helper_state->secure_desktop_active = secure_desktop_active;
|
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) {
|
std::string BuildHelperStatusResponse(HelperState* helper_state) {
|
||||||
@@ -435,6 +442,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
|
|||||||
json["session_id"] = helper_state->session_id;
|
json["session_id"] = helper_state->session_id;
|
||||||
json["process_id"] = helper_state->process_id;
|
json["process_id"] = helper_state->process_id;
|
||||||
json["session_locked"] = helper_state->session_locked;
|
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_available"] = helper_state->input_desktop_available;
|
||||||
json["input_desktop_error_code"] = helper_state->input_desktop_error_code;
|
json["input_desktop_error_code"] = helper_state->input_desktop_error_code;
|
||||||
json["input_desktop"] = helper_state->input_desktop_name;
|
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());
|
return std::vector<uint8_t>(response.begin(), response.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Own the desktop before its GDI objects, and release those objects before
|
// Own the desktop before its capture objects, and release those objects before
|
||||||
// restoring the original desktop. Both capture paths use the same lifecycle.
|
// restoring the original desktop. The legacy pipe path remains GDI-only.
|
||||||
struct SecureDesktopGdiResources {
|
struct SecureDesktopCaptureResources {
|
||||||
ScopedInteractiveDesktop desktop;
|
ScopedInteractiveDesktop desktop;
|
||||||
HDC screen_dc = nullptr;
|
HDC screen_dc = nullptr;
|
||||||
HDC mem_dc = nullptr;
|
HDC mem_dc = nullptr;
|
||||||
@@ -1341,8 +1349,16 @@ struct SecureDesktopGdiResources {
|
|||||||
const char* error = nullptr;
|
const char* error = nullptr;
|
||||||
DWORD error_code = ERROR_SUCCESS;
|
DWORD error_code = ERROR_SUCCESS;
|
||||||
crossdesk::SecureDesktopCursorSnapshot cursor_snapshot{};
|
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) {
|
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
|
||||||
SelectObject(mem_dc, old_bitmap);
|
SelectObject(mem_dc, old_bitmap);
|
||||||
}
|
}
|
||||||
@@ -1364,6 +1380,15 @@ struct SecureDesktopGdiResources {
|
|||||||
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
|
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
|
||||||
return Fail("switch_interactive_desktop_failed");
|
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);
|
screen_dc = GetDC(nullptr);
|
||||||
if (screen_dc == nullptr) return Fail("get_dc_failed");
|
if (screen_dc == nullptr) return Fail("get_dc_failed");
|
||||||
mem_dc = CreateCompatibleDC(screen_dc);
|
mem_dc = CreateCompatibleDC(screen_dc);
|
||||||
@@ -1384,11 +1409,50 @@ struct SecureDesktopGdiResources {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Capture(const SecureCaptureRequest& request) {
|
bool Capture(const SecureCaptureRequest& request, bool allow_dxgi = false) {
|
||||||
if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc,
|
frame_pending = false;
|
||||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
const ULONGLONG now = GetTickCount64();
|
||||||
return Fail("bitblt_failed");
|
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
|
// Sample on the thread bound to the captured input desktop, including when
|
||||||
// native controllers render the cursor themselves (show_cursor == false).
|
// native controllers render the cursor themselves (show_cursor == false).
|
||||||
cursor_snapshot = {};
|
cursor_snapshot = {};
|
||||||
@@ -1397,7 +1461,8 @@ struct SecureDesktopGdiResources {
|
|||||||
if (GetCursorInfo(&cursor)) {
|
if (GetCursorInfo(&cursor)) {
|
||||||
static thread_local crossdesk::WindowsCursorState cursor_state;
|
static thread_local crossdesk::WindowsCursorState cursor_state;
|
||||||
crossdesk::CursorState 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.valid = 1;
|
||||||
cursor_snapshot.visible = state.visible;
|
cursor_snapshot.visible = state.visible;
|
||||||
cursor_snapshot.shape = static_cast<uint32_t>(state.shape);
|
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.hidden_reason = static_cast<uint32_t>(state.hidden_reason);
|
||||||
cursor_snapshot.x = cursor.ptScreenPos.x;
|
cursor_snapshot.x = cursor.ptScreenPos.x;
|
||||||
cursor_snapshot.y = cursor.ptScreenPos.y;
|
cursor_snapshot.y = cursor.ptScreenPos.y;
|
||||||
if (request.show_cursor && crossdesk::DrawCursorInCapture(
|
// Never draw into DXGI's cached desktop: each cursor position starts
|
||||||
mem_dc, cursor, request.left, request.top,
|
// with clean pixels, including on a static screen or after a toggle.
|
||||||
request.width, request.height)) {
|
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.visible = 0;
|
||||||
cursor_snapshot.render_mode =
|
cursor_snapshot.render_mode =
|
||||||
static_cast<uint32_t>(crossdesk::CursorRenderMode::embedded);
|
static_cast<uint32_t>(crossdesk::CursorRenderMode::embedded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Complete GDI writes before libyuv reads the DIB's pixel memory.
|
// Complete GDI writes before libyuv reads the DIB's pixel memory.
|
||||||
GdiFlush();
|
if (!using_dxgi || frame_bits == bits) GdiFlush();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::vector<uint8_t> CaptureSecureDesktopFrame(
|
std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||||
const SecureCaptureRequest& request,
|
const SecureCaptureRequest& request) {
|
||||||
SecureCaptureBuffers* capture_buffers) {
|
SecureDesktopCaptureResources capture;
|
||||||
if (capture_buffers == nullptr) {
|
|
||||||
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers"));
|
|
||||||
}
|
|
||||||
|
|
||||||
SecureDesktopGdiResources capture;
|
|
||||||
if (!capture.Initialize(request) || !capture.Capture(request)) {
|
if (!capture.Initialize(request) || !capture.Capture(request)) {
|
||||||
return BuildTextResponseBytes(
|
return BuildTextResponseBytes(
|
||||||
BuildErrorJson(capture.error, capture.error_code));
|
BuildErrorJson(capture.error, capture.error_code));
|
||||||
@@ -1434,40 +1509,34 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
|
|||||||
|
|
||||||
const size_t nv12_size =
|
const size_t nv12_size =
|
||||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
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(
|
const int convert_result = libyuv::ARGBToNV12(
|
||||||
static_cast<const uint8_t*>(capture.bits), request.width * 4,
|
capture.frame_bits, capture.frame_stride, payload, request.width,
|
||||||
capture_buffers->nv12_frame.data(), request.width,
|
payload + static_cast<size_t>(request.width) * request.height,
|
||||||
capture_buffers->nv12_frame.data() + request.width * request.height,
|
|
||||||
request.width, request.width, request.height);
|
request.width, request.width, request.height);
|
||||||
|
|
||||||
if (convert_result != 0) {
|
if (convert_result != 0) {
|
||||||
return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed"));
|
return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed"));
|
||||||
}
|
}
|
||||||
|
|
||||||
crossdesk::CrossDeskSecureDesktopFrameHeader header{};
|
|
||||||
header.magic = crossdesk::kCrossDeskSecureDesktopFrameMagic;
|
header.magic = crossdesk::kCrossDeskSecureDesktopFrameMagic;
|
||||||
header.version = crossdesk::kCrossDeskSecureDesktopFrameVersion;
|
header.version = crossdesk::kCrossDeskSecureDesktopFrameVersion;
|
||||||
header.left = request.left;
|
header.left = request.left;
|
||||||
header.top = request.top;
|
header.top = request.top;
|
||||||
header.width = static_cast<uint32_t>(request.width);
|
header.width = static_cast<uint32_t>(request.width);
|
||||||
header.height = static_cast<uint32_t>(request.height);
|
header.height = static_cast<uint32_t>(request.height);
|
||||||
header.payload_size =
|
header.payload_size = static_cast<uint32_t>(nv12_size);
|
||||||
static_cast<uint32_t>(capture_buffers->nv12_frame.size());
|
|
||||||
header.cursor = capture.cursor_snapshot;
|
header.cursor = capture.cursor_snapshot;
|
||||||
|
|
||||||
std::vector<uint8_t> response(sizeof(header) +
|
|
||||||
capture_buffers->nv12_frame.size());
|
|
||||||
std::memcpy(response.data(), &header, sizeof(header));
|
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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
void CloseSecureDesktopSharedCaptureResourcesLocked(
|
void CloseSecureDesktopSharedCaptureResources(
|
||||||
SecureSharedCaptureState* capture_state) {
|
SecureSharedCaptureState* capture_state) {
|
||||||
if (capture_state == nullptr) {
|
if (capture_state == nullptr) {
|
||||||
return;
|
return;
|
||||||
@@ -1485,7 +1554,7 @@ void CloseSecureDesktopSharedCaptureResourcesLocked(
|
|||||||
CloseHandle(capture_state->frame_mapping);
|
CloseHandle(capture_state->frame_mapping);
|
||||||
capture_state->frame_mapping = nullptr;
|
capture_state->frame_mapping = nullptr;
|
||||||
}
|
}
|
||||||
capture_state->frame_view_size = 0;
|
capture_state->sequence = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||||
@@ -1493,28 +1562,22 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
SecureCaptureRequest request;
|
// Start publishes these fields before launching us. Lifecycle control keeps
|
||||||
uint8_t* frame_view = nullptr;
|
// the request, mapping and event alive and unchanged until this thread joins.
|
||||||
{
|
const SecureCaptureRequest request = capture_state->request;
|
||||||
std::lock_guard<std::mutex> lock(capture_state->mutex);
|
uint8_t* const frame_view = capture_state->frame_view;
|
||||||
request = capture_state->request;
|
|
||||||
frame_view = capture_state->frame_view;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (frame_view == nullptr || request.width <= 0 || request.height <= 0) {
|
if (frame_view == nullptr || request.width <= 0 || request.height <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int interval_ms =
|
crossdesk::ScopedWindowsPhysicalCoordinates physical_coordinates;
|
||||||
request.fps > 0 ? (std::max)(1, 1000 / request.fps) : 33;
|
crossdesk::SecureDesktopFramePacer pacer(request.fps);
|
||||||
const size_t nv12_size =
|
std::unique_ptr<SecureDesktopCaptureResources> capture;
|
||||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
|
||||||
std::vector<uint8_t> nv12_frame(nv12_size);
|
|
||||||
|
|
||||||
std::unique_ptr<SecureDesktopGdiResources> capture;
|
|
||||||
ULONGLONG last_error_tick = 0;
|
ULONGLONG last_error_tick = 0;
|
||||||
auto stats_started = std::chrono::steady_clock::now();
|
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;
|
double capture_ms = 0, convert_ms = 0, publish_ms = 0;
|
||||||
auto report_stats = [&](bool force) {
|
auto report_stats = [&](bool force) {
|
||||||
const auto elapsed = std::chrono::duration<double>(
|
const auto elapsed = std::chrono::duration<double>(
|
||||||
@@ -1524,16 +1587,20 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
|||||||
if (frame_count > 0) {
|
if (frame_count > 0) {
|
||||||
LOG_INFO(
|
LOG_INFO(
|
||||||
"Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, "
|
"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,
|
frame_count / elapsed, capture_ms / frame_count,
|
||||||
convert_ms / frame_count, publish_ms / frame_count, request.width,
|
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();
|
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;
|
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();
|
const auto frame_started = std::chrono::steady_clock::now();
|
||||||
report_stats(false);
|
report_stats(false);
|
||||||
// A lock/credential/UAC transition can change the desktop before the
|
// A lock/credential/UAC transition can change the desktop before the
|
||||||
@@ -1542,7 +1609,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
|||||||
capture.reset();
|
capture.reset();
|
||||||
}
|
}
|
||||||
if (!capture) {
|
if (!capture) {
|
||||||
capture = std::make_unique<SecureDesktopGdiResources>();
|
capture = std::make_unique<SecureDesktopCaptureResources>();
|
||||||
if (!capture->Initialize(request)) {
|
if (!capture->Initialize(request)) {
|
||||||
const DWORD error = capture->error_code;
|
const DWORD error = capture->error_code;
|
||||||
capture.reset();
|
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 auto captured_at = std::chrono::steady_clock::now();
|
||||||
const int convert_result = libyuv::ARGBToNV12(
|
// Composited/animated cursors and GDI frames always need conversion.
|
||||||
static_cast<const uint8_t*>(capture->bits), request.width * 4,
|
// Native controllers use independent cursor metadata, so a DXGI timeout
|
||||||
nv12_frame.data(), request.width,
|
// or pointer-only update can reuse the NV12 image safely.
|
||||||
nv12_frame.data() + request.width * request.height, request.width,
|
const bool pixels_changed = !capture->using_dxgi ||
|
||||||
request.width, request.height);
|
capture->dxgi->updated() || request.show_cursor;
|
||||||
if (convert_result == 0) {
|
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();
|
const auto converted_at = std::chrono::steady_clock::now();
|
||||||
auto* header = reinterpret_cast<
|
auto* header = reinterpret_cast<
|
||||||
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
|
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
|
||||||
@@ -1581,12 +1651,19 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
|||||||
header->height = static_cast<uint32_t>(request.height);
|
header->height = static_cast<uint32_t>(request.height);
|
||||||
header->payload_size = static_cast<uint32_t>(nv12_frame.size());
|
header->payload_size = static_cast<uint32_t>(nv12_frame.size());
|
||||||
header->cursor = capture->cursor_snapshot;
|
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;
|
header->sequence = ++capture_state->sequence;
|
||||||
MemoryBarrier();
|
MemoryBarrier();
|
||||||
header->writing = 0;
|
header->writing = 0;
|
||||||
SetEvent(capture_state->frame_ready_event);
|
SetEvent(capture_state->frame_ready_event);
|
||||||
++frame_count;
|
++frame_count;
|
||||||
|
if (capture->using_dxgi) {
|
||||||
|
++dxgi_frames;
|
||||||
|
if (capture->dxgi->updated()) ++dxgi_updates;
|
||||||
|
}
|
||||||
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
|
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
|
||||||
frame_started)
|
frame_started)
|
||||||
.count();
|
.count();
|
||||||
@@ -1597,7 +1674,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
|||||||
std::chrono::steady_clock::now() - converted_at)
|
std::chrono::steady_clock::now() - converted_at)
|
||||||
.count();
|
.count();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!capture->frame_pending) {
|
||||||
const DWORD error = capture->error_code;
|
const DWORD error = capture->error_code;
|
||||||
capture.reset();
|
capture.reset();
|
||||||
const ULONGLONG now = GetTickCount64();
|
const ULONGLONG now = GetTickCount64();
|
||||||
@@ -1610,14 +1687,10 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto elapsed_ms =
|
if (capture) {
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
pacer.Wait(frame_started);
|
||||||
std::chrono::steady_clock::now() - frame_started)
|
} else {
|
||||||
.count();
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
report_stats(true);
|
report_stats(true);
|
||||||
@@ -1629,41 +1702,18 @@ std::vector<uint8_t> StopSecureDesktopSharedCapture(
|
|||||||
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
|
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::thread thread_to_join;
|
capture_state->lifecycle.Stop();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BuildTextResponseBytes("{\"ok\":true,\"shared_capture\":\"stopped\"}");
|
return BuildTextResponseBytes("{\"ok\":true,\"shared_capture\":\"stopped\"}");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
std::vector<uint8_t> PrepareSecureDesktopSharedCapture(
|
||||||
const SecureCaptureRequest& request,
|
const SecureCaptureRequest& request,
|
||||||
SecureSharedCaptureState* capture_state) {
|
SecureSharedCaptureState* capture_state) {
|
||||||
if (capture_state == nullptr) {
|
if (capture_state == nullptr) {
|
||||||
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
|
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_state"));
|
||||||
}
|
}
|
||||||
|
|
||||||
StopSecureDesktopSharedCapture(capture_state);
|
|
||||||
|
|
||||||
const size_t payload_size =
|
const size_t payload_size =
|
||||||
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
static_cast<size_t>(request.width) * request.height * 3 / 2;
|
||||||
const size_t mapping_size =
|
const size_t mapping_size =
|
||||||
@@ -1735,17 +1785,44 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
|||||||
header->height = static_cast<uint32_t>(request.height);
|
header->height = static_cast<uint32_t>(request.height);
|
||||||
header->buffer_size = static_cast<uint32_t>(payload_size);
|
header->buffer_size = static_cast<uint32_t>(payload_size);
|
||||||
|
|
||||||
{
|
// Transfer handles before any potentially throwing request/string copy.
|
||||||
std::lock_guard<std::mutex> lock(capture_state->mutex);
|
// The lifecycle owner cleans partially prepared resources on failure too.
|
||||||
capture_state->request = request;
|
capture_state->frame_mapping = frame_mapping;
|
||||||
capture_state->frame_mapping = frame_mapping;
|
capture_state->frame_ready_event = frame_ready_event;
|
||||||
capture_state->frame_ready_event = frame_ready_event;
|
capture_state->frame_view = frame_view;
|
||||||
capture_state->frame_view = frame_view;
|
capture_state->sequence = 0;
|
||||||
capture_state->frame_view_size = mapping_size;
|
capture_state->request = request;
|
||||||
capture_state->sequence = 0;
|
|
||||||
capture_state->stop_requested.store(false, std::memory_order_relaxed);
|
return {};
|
||||||
capture_state->capture_thread =
|
}
|
||||||
std::thread(SecureDesktopSharedCaptureThread, capture_state);
|
|
||||||
|
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;
|
Json json;
|
||||||
@@ -1758,7 +1835,7 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> HandleSecureInputHelperCommand(
|
std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||||
const std::string& command, SecureCaptureBuffers* capture_buffers,
|
const std::string& command,
|
||||||
SecureSharedCaptureState* capture_state) {
|
SecureSharedCaptureState* capture_state) {
|
||||||
if (command == "ping") {
|
if (command == "ping") {
|
||||||
return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}");
|
return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}");
|
||||||
@@ -1840,7 +1917,7 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
|||||||
|
|
||||||
SecureCaptureRequest capture_request;
|
SecureCaptureRequest capture_request;
|
||||||
if (ParseSecureInputCaptureCommand(command, &capture_request)) {
|
if (ParseSecureInputCaptureCommand(command, &capture_request)) {
|
||||||
return CaptureSecureDesktopFrame(capture_request, capture_buffers);
|
return CaptureSecureDesktopFrame(capture_request);
|
||||||
}
|
}
|
||||||
|
|
||||||
return BuildTextResponseBytes(BuildErrorJson("unknown_command"));
|
return BuildTextResponseBytes(BuildErrorJson("unknown_command"));
|
||||||
@@ -1849,19 +1926,23 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
|||||||
void HandleSecureInputHelperPipeClient(
|
void HandleSecureInputHelperPipeClient(
|
||||||
HANDLE pipe, HANDLE event_handle,
|
HANDLE pipe, HANDLE event_handle,
|
||||||
std::shared_ptr<SecureSharedCaptureState> capture_state) {
|
std::shared_ptr<SecureSharedCaptureState> capture_state) {
|
||||||
SecureCaptureBuffers capture_buffers;
|
|
||||||
char buffer[1024] = {0};
|
char buffer[1024] = {0};
|
||||||
DWORD bytes_read = 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) {
|
bytes_read > 0) {
|
||||||
std::vector<uint8_t> response = HandleSecureInputHelperCommand(
|
std::vector<uint8_t> response = HandleSecureInputHelperCommand(
|
||||||
std::string(buffer, buffer + bytes_read), &capture_buffers,
|
std::string(buffer, buffer + bytes_read), capture_state.get());
|
||||||
capture_state.get());
|
|
||||||
DWORD bytes_written = 0;
|
DWORD bytes_written = 0;
|
||||||
if (!response.empty()) {
|
if (!response.empty()) {
|
||||||
WriteFile(pipe, response.data(), static_cast<DWORD>(response.size()),
|
if (crossdesk::pipe_deadline_detail::Transfer(
|
||||||
&bytes_written, nullptr);
|
pipe, event_handle, true, response.data(),
|
||||||
FlushFileBuffers(pipe);
|
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};
|
HANDLE wait_handles[] = {stop_event, overlapped.hEvent};
|
||||||
DWORD wait_result =
|
DWORD wait_result =
|
||||||
WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
|
WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE);
|
||||||
if (wait_result == WAIT_OBJECT_0) {
|
if (wait_result != WAIT_OBJECT_0 + 1) {
|
||||||
CancelIoEx(pipe, &overlapped);
|
CancelIoEx(pipe, &overlapped);
|
||||||
|
DWORD ignored = 0;
|
||||||
|
GetOverlappedResult(pipe, &overlapped, &ignored, TRUE);
|
||||||
CloseHandle(overlapped.hEvent);
|
CloseHandle(overlapped.hEvent);
|
||||||
CloseHandle(pipe);
|
CloseHandle(pipe);
|
||||||
break;
|
break;
|
||||||
@@ -1946,7 +2029,7 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) {
|
|||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
StopSecureDesktopSharedCapture(capture_state.get());
|
capture_state->lifecycle.Shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintUsage() {
|
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");
|
ReadFile(repo_root / "apps/desktop/src/gui/features/input/keyboard_controller.cpp");
|
||||||
const std::string windows_service_runtime =
|
const std::string windows_service_runtime =
|
||||||
ReadFile(repo_root / "apps/desktop/src/platform/windows/gui/runtime/windows_service_runtime.cpp");
|
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(
|
const std::string screen_capturer_cpp = ReadFile(
|
||||||
repo_root / "apps/desktop/src/platform/windows/screen_capturer/screen_capturer_win.cpp");
|
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,
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
"EnablePerMonitorDpiAwareness();\n\n"
|
"EnablePerMonitorDpiAwareness();\n\n"
|
||||||
" InitializeHelperLogger();");
|
" InitializeHelperLogger();");
|
||||||
ok &= ExpectContains(
|
|
||||||
"service_host.cpp", service_host,
|
|
||||||
"const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms");
|
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"while (GetTickCount64() <= deadline_tick)");
|
"QueryNamedPipeWithDeadline(pipe_name, command, timeout_ms");
|
||||||
ok &= ExpectNotContains("service_host.cpp", service_host,
|
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||||
"constexpr int kPipeConnectRetryCount = 3");
|
"constexpr int kPipeConnectRetryCount = 3");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
@@ -128,8 +123,6 @@ int main() {
|
|||||||
"IsConsentUiRunningInCurrentSession");
|
"IsConsentUiRunningInCurrentSession");
|
||||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
"L\"Consent.exe\"");
|
"L\"Consent.exe\"");
|
||||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
|
||||||
"desktop_info.available && consent_ui_visible");
|
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"session_helper_report_input_desktop_available_ &&");
|
"session_helper_report_input_desktop_available_ &&");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
@@ -145,18 +138,13 @@ int main() {
|
|||||||
"return L\"winsta0\\\\Winlogon\"");
|
"return L\"winsta0\\\\Winlogon\"");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"return L\"winsta0\\\\default\"");
|
"return L\"winsta0\\\\default\"");
|
||||||
ok &= ExpectContains(
|
|
||||||
"service_host.cpp", service_host,
|
|
||||||
"secure_input_helper_interactive_stage_ == interactive_stage");
|
|
||||||
ok &= ExpectContains(
|
ok &= ExpectContains(
|
||||||
"service_host.cpp", service_host,
|
"service_host.cpp", service_host,
|
||||||
"secure_input_helper_interactive_stage_ = interactive_stage");
|
"secure_input_helper_interactive_stage_ = interactive_stage");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"secure_input_helper_interactive_stage_.clear()");
|
"secure_input_helper_interactive_stage_.clear()");
|
||||||
ok &= ExpectContains(
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"service_host.cpp", service_host,
|
"LaunchSecureInputHelper(secure_input_target_session_id,");
|
||||||
"LaunchSecureInputHelper(target_session_id, interactive_stage,\n"
|
|
||||||
" interactive_desktop)");
|
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"\\\"secure_input_helper_stage\\\":\\\"");
|
"\\\"secure_input_helper_stage\\\":\\\"");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
@@ -256,15 +244,7 @@ int main() {
|
|||||||
"runtime->local_service_available_ &&\n"
|
"runtime->local_service_available_ &&\n"
|
||||||
" "
|
" "
|
||||||
"IsSecureDesktopInteractionRequired(runtime->local_interactive_stage_)");
|
"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,
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
"const std::string& stage");
|
"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;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -593,7 +593,8 @@ function setup_targets()
|
|||||||
set_kind("binary")
|
set_kind("binary")
|
||||||
add_packages("libyuv", "nlohmann_json")
|
add_packages("libyuv", "nlohmann_json")
|
||||||
add_deps("rd_log", "path_manager")
|
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/service/session_helper_main.cpp")
|
||||||
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
|
add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp")
|
||||||
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
||||||
|
|||||||
Reference in New Issue
Block a user