mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-11-11 02:06:38 +08:00
[feat] unify Windows unattended hosting and device identity
This commit is contained in:
@@ -0,0 +1,416 @@
|
||||
#include "application/gui_application.h"
|
||||
|
||||
#include <SDL3/SDL_timer.h>
|
||||
#include <web_clipboard_protocol.h>
|
||||
#include <algorithm>
|
||||
#include <tuple>
|
||||
|
||||
#include "interactive_desktop.h"
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "platform/windows/screen_capturer/dxgi_cursor_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
using Json = nlohmann::json;
|
||||
Json DeviceInfo(const TrustedDevices::Device& device) {
|
||||
return {{"key", device.public_key}, {"name", device.name}, {"device", device.device_id}};
|
||||
}
|
||||
TrustedDevices::Device ReadDevice(const Json& message) {
|
||||
return {message.at("key").get<std::string>(), message.at("name").get<std::string>(),
|
||||
message.at("device").get<std::string>()};
|
||||
}
|
||||
auto ServerSettings(const ConfigCenter& config) {
|
||||
return std::tuple(config.IsSelfHosted(), config.GetSignalServerHost(),
|
||||
config.GetSignalServerPort());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool GuiRuntime::SendLocalHostCommand(Json message) {
|
||||
return local_host_ && local_host_->Send(message);
|
||||
}
|
||||
|
||||
bool GuiRuntime::ForwardUserSessionData(const std::string& id,
|
||||
const std::string& stream, const char* data, size_t size) {
|
||||
const bool user_stream = stream == file_label_ || stream == file_feedback_label_ ||
|
||||
stream == clipboard_label_ || stream == web_clipboard::kStream;
|
||||
Json action;
|
||||
if (stream == control_data_label_ && size <= 65536)
|
||||
action = Json::parse(data, data + size, nullptr, false);
|
||||
const auto type = action.is_object() ? action.value("type", Json{}) : Json{};
|
||||
const bool host_info = type == static_cast<int>(ControlType::host_infomation);
|
||||
const bool preview = type == "deskhub_preview";
|
||||
const bool user_control = type == "file_browser" ||
|
||||
type == static_cast<int>(ControlType::privacy_command);
|
||||
const bool audio = type == static_cast<int>(ControlType::audio_capture);
|
||||
uint64_t epoch = 0;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
const auto found = local_controller_info_.find(id);
|
||||
if (found == local_controller_info_.end()) return user_stream || user_control;
|
||||
epoch = found->second.value("epoch", uint64_t{});
|
||||
if (host_info) found->second["host_info"] = action;
|
||||
if (preview) found->second["preview"] = action;
|
||||
}
|
||||
if (local_gui_ready_ && local_host_ &&
|
||||
(user_stream || user_control || host_info || preview || audio)) {
|
||||
if (!local_host_->Send({{"op", "data"}, {"id", id}, {"epoch", epoch}, {"stream", stream},
|
||||
{"bytes", Json::binary(std::vector<uint8_t>(data, data + size))}})) {
|
||||
LOG_WARN("Cannot forward user-session data for [{}] on {}; reconnecting local frontend", id, stream);
|
||||
local_gui_ready_ = false;
|
||||
// Reset the mirrored sessions so an incomplete reliable stream fails
|
||||
// explicitly. Never fall through to file/clipboard handling as SYSTEM.
|
||||
local_host_->Reconnect();
|
||||
}
|
||||
}
|
||||
return user_stream || user_control;
|
||||
}
|
||||
|
||||
void GuiRuntime::PublishLocalHostStatus() {
|
||||
if (!local_host_ || !local_gui_ready_) return;
|
||||
Json controllers = Json::object();
|
||||
for (const auto& id : ConnectedControllers()) {
|
||||
if (!IsAuthorizedController(id)) continue;
|
||||
Json info;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
const auto found = local_controller_info_.find(id);
|
||||
if (found == local_controller_info_.end()) continue;
|
||||
info = found->second;
|
||||
}
|
||||
if (auto device = VerifiedController(id)) info["verified"] = DeviceInfo(*device);
|
||||
controllers[id] = std::move(info);
|
||||
}
|
||||
bool password_pending = false;
|
||||
{
|
||||
std::lock_guard lock(password_change_mutex_);
|
||||
password_pending = password_change_pending_ || credential_recovery_in_progress_;
|
||||
}
|
||||
Json status{{"op", "status"}, {"online", signal_connected_.load()},
|
||||
{"identity", settings_.ActiveIdentity()}, {"self_hosted", config_center_->IsSelfHosted()},
|
||||
{"controllers", std::move(controllers)},
|
||||
{"capture", screen_capturer_is_started_ && host_capture_tick_ != 0 &&
|
||||
SDL_GetTicks() - host_capture_tick_.load() < 2000},
|
||||
{"cursor_owned", devices_.OwnsNativeCursor()},
|
||||
{"password_pending", password_pending}};
|
||||
// Settings/trust mutations invalidate this profile; reattachment sends a
|
||||
// full snapshot. Frequent heartbeats carry only changed large fields.
|
||||
const bool refresh_profile = local_host_profile_dirty_.exchange(false);
|
||||
if (refresh_profile) {
|
||||
status["config"] = config_center_->ExportSharedHostConfig();
|
||||
auto& trusted = status["trusted"] = Json::array();
|
||||
if (trusted_devices_)
|
||||
for (const auto& device : trusted_devices_->List()) trusted.push_back(DeviceInfo(device));
|
||||
}
|
||||
for (const char* field : {"config", "trusted", "controllers"}) {
|
||||
if (status.contains(field) && local_host_status_.values.contains(field) &&
|
||||
status.at(field) == local_host_status_.values.at(field)) status.erase(field);
|
||||
}
|
||||
if (local_host_->Send(status)) local_host_status_.values.update(status);
|
||||
else if (refresh_profile) local_host_profile_dirty_ = true;
|
||||
}
|
||||
|
||||
void GuiApplication::TickLocalHost() {
|
||||
if (!local_host_) return;
|
||||
const auto error = [this](const std::string& message) {
|
||||
if (unattended_host_) local_host_->Send({{"op", "error"}, {"message", message}});
|
||||
else {
|
||||
offline_warning_text_ = message;
|
||||
show_offline_warning_window_ = true;
|
||||
}
|
||||
};
|
||||
const auto deliver = [this](const std::string& id, const std::string& stream,
|
||||
const char* bytes, size_t size) {
|
||||
PeerEventHandler::OnReceiveDataBuffer(bytes, size, id.data(), id.size(),
|
||||
stream.data(), stream.size(), peer_events_.get());
|
||||
};
|
||||
const auto remove_mirror = [this](const std::string& id) {
|
||||
if (peer_events_) peer_events_->ApplyConnectionStatus(MiniRtcConnectionClosed, id);
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
mirrored_controllers_.erase(id);
|
||||
local_controller_info_.erase(id);
|
||||
};
|
||||
const auto ensure_mirror = [this, &remove_mirror](const std::string& id, uint64_t epoch) {
|
||||
if (!peer_ || !peer_events_ || !epoch || id.empty() || id.size() > 128) return false;
|
||||
uint64_t existing = 0;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
const auto found = mirrored_controllers_.find(id);
|
||||
if (found != mirrored_controllers_.end()) existing = found->second;
|
||||
}
|
||||
if (existing == epoch) return true;
|
||||
local_host_status_.controllers_dirty = true;
|
||||
if (existing) remove_mirror(id);
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
mirrored_controllers_[id] = epoch;
|
||||
}
|
||||
peer_events_->ApplyConnectionStatus(MiniRtcConnectionConnected, id);
|
||||
return true;
|
||||
};
|
||||
for (auto& message : local_host_->Receive()) {
|
||||
try {
|
||||
const auto op = message.at("op").get<std::string>();
|
||||
if (op == "attached" || op == "detached" || (unattended_host_ && op == "hello")) {
|
||||
local_host_status_.Reset();
|
||||
local_host_profile_dirty_ = true;
|
||||
}
|
||||
if (unattended_host_) {
|
||||
if (op == "attached" || op == "hello" || op == "detached") {
|
||||
local_gui_ready_ = op != "detached";
|
||||
if (op == "detached") {
|
||||
SharedDxgiCursorState().SetExternalCursorHidden(false);
|
||||
devices_.SetNativeCursorPolicy(true, true);
|
||||
}
|
||||
if (op == "hello") {
|
||||
devices_.SetNativeCursorPolicy(message.value("privacy_on_connect", true),
|
||||
message.value("privacy_requested", true));
|
||||
}
|
||||
PublishLocalHostStatus();
|
||||
for (const auto& id : ConnectedControllers()) AnnounceHostCapabilities(id);
|
||||
LOG_INFO("User-session frontend {}; RTC host remains running",
|
||||
local_gui_ready_ ? "attached" : "detached");
|
||||
continue;
|
||||
}
|
||||
if (!local_gui_ready_) continue;
|
||||
if (op == "privacy_cursor") {
|
||||
if (message.contains("requested"))
|
||||
devices_.SetNativeCursorPolicy(host_cursor_default_, message.at("requested").get<bool>());
|
||||
const bool hidden = message.at("hidden").get<bool>();
|
||||
if (hidden != SharedDxgiCursorState().ExternalCursorHidden()) {
|
||||
SharedDxgiCursorState().SetExternalCursorHidden(hidden);
|
||||
LOG_INFO("Host cursor: user-session privacy cursor hidden={}", hidden);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (op == "data" || op == "audio" || op == "disconnect") {
|
||||
const auto id = message.at("id").get<std::string>();
|
||||
if (!IsAuthorizedController(id)) continue;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
const auto found = local_controller_info_.find(id);
|
||||
if (found == local_controller_info_.end() ||
|
||||
message.at("epoch") != found->second.at("epoch")) continue;
|
||||
}
|
||||
if (op == "disconnect") { CloseServerController(id); continue; }
|
||||
const auto& bytes = message.at("bytes").get_binary();
|
||||
const auto stream = message.at("stream").get<std::string>();
|
||||
if (bytes.empty()) continue;
|
||||
if (op == "audio") {
|
||||
if (stream != audio_label_ || bytes.size() > 65536) continue;
|
||||
WithController(id, [&](MiniRtcPeerPtr* peer, const std::string& wire) {
|
||||
MiniRtcAudioFrame frame{};
|
||||
frame.data = reinterpret_cast<const char*>(bytes.data());
|
||||
frame.size = bytes.size();
|
||||
frame.captured_timestamp = MiniRtcGetSystemTimeMicros(peer);
|
||||
return MiniRtcSendAudioFrameToPeer(peer, &frame, audio_label_.c_str(),
|
||||
wire.data(), wire.size());
|
||||
});
|
||||
} else if (stream == file_label_ || stream == file_feedback_label_ ||
|
||||
stream == clipboard_label_ || stream == web_clipboard::kStream ||
|
||||
stream == control_data_label_) {
|
||||
SendControllerData(id, reinterpret_cast<const char*>(bytes.data()), bytes.size(), stream.c_str());
|
||||
}
|
||||
} else if (op == "signal") {
|
||||
const auto text = message.at("message").get<std::string>();
|
||||
const auto request = Json::parse(text, nullptr, false);
|
||||
const auto type = request.is_object() ? request.value("type", Json{}) : Json{};
|
||||
// Authentication, credential changes and connection ownership stay
|
||||
// exclusively in the persistent host's own signaling exchange.
|
||||
if (peer_ && (type == "recent_connections_presence" || type == "announcements_list"))
|
||||
MiniRtcSendSignalMessage(peer_, text.data(), text.size());
|
||||
} else if (op == "password") {
|
||||
if (!RequestPasswordChange(message.at("password").get<std::string>()))
|
||||
error("Invalid host password");
|
||||
} else if (op == "config") {
|
||||
const auto previous = config_center_->ExportSharedHostConfig();
|
||||
const auto server = ServerSettings(*config_center_);
|
||||
bool ok = config_center_->ApplySharedHostConfig(message.at("config")) == 0;
|
||||
const bool reconnect = server != ServerSettings(*config_center_);
|
||||
if (ok && reconnect && HasActiveSession()) {
|
||||
config_center_->ApplySharedHostConfig(previous);
|
||||
ok = false;
|
||||
}
|
||||
if (ok) {
|
||||
devices_.SetNativeCursorPolicy(message.value("privacy_on_connect", true),
|
||||
host_cursor_preference_);
|
||||
ok = PublishSharedIdentity(true);
|
||||
ApplySettingsFromConfig(reconnect);
|
||||
TickControllerTrust();
|
||||
}
|
||||
if (!local_host_->Send({{"op", "config_result"}, {"ok", ok},
|
||||
{"request", message.at("request")}})) {
|
||||
LOG_WARN("Cannot acknowledge host settings; reconnecting local frontend");
|
||||
local_host_->Reconnect();
|
||||
}
|
||||
if (!ok) error("Could not apply host settings while a connection is active");
|
||||
} else if (op == "connection_settings") {
|
||||
const auto turn = message.at("turn").get<int>();
|
||||
if (peer_ && turn >= 0 && turn <= 3)
|
||||
MiniRtcUpdateConnectionSettings(peer_, true, message.at("av1"),
|
||||
static_cast<MiniRtcTurnMode>(turn));
|
||||
} else if (op == "reconnect") {
|
||||
if (peer_ && !HasActiveSession()) MiniRtcReconnectSignal(peer_);
|
||||
} else if (op == "trust_add") {
|
||||
if (!AddTrustedDevice(ReadDevice(message))) error("Could not add trusted device");
|
||||
} else if (op == "trust_remove") {
|
||||
if (!RemoveTrustedDevice(message.at("key"))) error("Could not remove trusted device");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (op == "attached" || op == "detached") {
|
||||
SharedDxgiCursorState().SetExternalControllerAvailable(op == "attached");
|
||||
local_config_sent_ = false;
|
||||
local_cursor_hidden_sent_.reset();
|
||||
local_cursor_request_sent_.reset();
|
||||
local_cursor_policy_ready_ = false;
|
||||
}
|
||||
if (op == "attached") {
|
||||
local_host_->Send({{"op", "hello"},
|
||||
{"privacy_on_connect", config_center_->IsEnablePrivacyScreen()},
|
||||
{"privacy_requested", privacy_.Requested() || config_center_->IsEnablePrivacyScreen()}});
|
||||
} else if (op == "detached") {
|
||||
SharedDxgiCursorState().SetExternalCursorHidden(false);
|
||||
signal_connected_ = false;
|
||||
device_presence_cache_.SetSignalConnected(false);
|
||||
signal_status_ = MiniRtcSignalReconnecting;
|
||||
std::vector<std::string> ids;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
for (const auto& [id, _] : mirrored_controllers_) ids.push_back(id);
|
||||
}
|
||||
for (const auto& id : ids) remove_mirror(id);
|
||||
privacy_.Disconnected();
|
||||
} else if (op == "status" && peer_) {
|
||||
// Retain omitted fields across heartbeats, including while a local
|
||||
// settings request is pending. Detach/attach clears this snapshot.
|
||||
local_host_status_.Merge(message);
|
||||
const auto& status = local_host_status_.values;
|
||||
// Apply before mirroring Connected/resuming privacy. The GUI then
|
||||
// reuses this owner instead of launching another cursor hider.
|
||||
SharedDxgiCursorState().SetExternalCursorHidden(status.value("cursor_owned", false));
|
||||
const bool online = status.at("online");
|
||||
if (online && !signal_connected_) need_to_send_recent_connections_ = true;
|
||||
signal_connected_ = online;
|
||||
device_presence_cache_.SetSignalConnected(online);
|
||||
signal_status_ = online ? MiniRtcSignalConnected : MiniRtcSignalReconnecting;
|
||||
if (pending_local_config_.is_null()) {
|
||||
if (local_host_status_.config_dirty) {
|
||||
const auto& config = status.at("config").get_ref<const std::string&>();
|
||||
const bool changed = config != config_center_->ExportSharedHostConfig();
|
||||
if (!changed || config_center_->ApplySharedHostConfig(config) == 0) {
|
||||
local_host_status_.config_dirty = false;
|
||||
if (changed) ApplySettingsFromConfig(false);
|
||||
}
|
||||
}
|
||||
if (!local_host_status_.config_dirty && local_host_status_.identity_dirty) {
|
||||
// ApplyHostIdentity may activate the identity before a disk write
|
||||
// fails. Retry persistence even if ActiveIdentity already matches.
|
||||
local_host_status_.identity_dirty = !settings_.ApplyHostIdentity(
|
||||
status.at("identity"), status.at("self_hosted"));
|
||||
}
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(password_change_mutex_);
|
||||
password_change_pending_ = status.at("password_pending");
|
||||
credential_recovery_in_progress_ = false;
|
||||
credential_recovery_attempt_pending_ = false;
|
||||
}
|
||||
if (local_host_status_.controllers_dirty) {
|
||||
bool applied = true;
|
||||
const auto& controllers = status.at("controllers");
|
||||
if (!controllers.empty() && std::all_of(controllers.begin(), controllers.end(),
|
||||
[](const auto& info) { return info.contains("host_info"); }))
|
||||
local_cursor_policy_ready_ = true;
|
||||
std::vector<std::string> removed;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
for (const auto& [id, _] : mirrored_controllers_)
|
||||
if (!controllers.contains(id)) removed.push_back(id);
|
||||
}
|
||||
for (const auto& id : removed) remove_mirror(id);
|
||||
for (const auto& [id, info] : controllers.items()) {
|
||||
if (!ensure_mirror(id, info.at("epoch"))) { applied = false; continue; }
|
||||
if (info.contains("verified")) {
|
||||
std::lock_guard lock(controller_trust_mutex_);
|
||||
auto& trust = controller_trust_[id];
|
||||
trust.events = peer_events_.get();
|
||||
trust.verified = ReadDevice(info.at("verified"));
|
||||
}
|
||||
for (const char* field : {"host_info", "preview"}) {
|
||||
if (!info.contains(field)) continue;
|
||||
bool changed = false;
|
||||
{
|
||||
std::lock_guard lock(local_host_state_mutex_);
|
||||
auto& previous = local_controller_info_[id][field];
|
||||
changed = previous != info.at(field);
|
||||
previous = info.at(field);
|
||||
}
|
||||
if (changed) {
|
||||
const auto data = info.at(field).dump();
|
||||
deliver(id, control_data_label_, data.data(), data.size());
|
||||
}
|
||||
}
|
||||
}
|
||||
local_host_status_.controllers_dirty = !applied;
|
||||
}
|
||||
if (trusted_devices_ && local_host_status_.trusted_dirty) {
|
||||
std::vector<TrustedDevices::Device> current;
|
||||
for (const auto& item : status.at("trusted")) current.push_back(ReadDevice(item));
|
||||
trusted_devices_->Replace(current);
|
||||
local_host_status_.trusted_dirty = false;
|
||||
}
|
||||
if (IsCurrentSessionUserDesktopActive()) {
|
||||
privacy_.CaptureChanged(status.at("capture"));
|
||||
privacy_.ResumeAfterDesktopRecovery();
|
||||
} else privacy_.SuspendForDesktop();
|
||||
} else if (op == "data") {
|
||||
const auto id = message.at("id").get<std::string>();
|
||||
if (!ensure_mirror(id, message.at("epoch"))) continue;
|
||||
const auto stream = message.at("stream").get<std::string>();
|
||||
const auto& data = message.at("bytes").get_binary();
|
||||
deliver(id, stream, reinterpret_cast<const char*>(data.data()), data.size());
|
||||
} else if (op == "signal" && peer_events_) {
|
||||
const auto data = message.at("message").get<std::string>();
|
||||
PeerEventHandler::OnSignalMessage(data.data(), data.size(), peer_events_.get());
|
||||
} else if (op == "config_result") {
|
||||
if (!pending_local_config_.is_null() && message.at("request") == pending_local_config_.at("request")) {
|
||||
pending_local_config_ = Json{};
|
||||
local_host_status_.config_dirty = local_host_status_.identity_dirty = true;
|
||||
}
|
||||
} else if (op == "error") {
|
||||
error(message.at("message"));
|
||||
}
|
||||
} catch (const std::exception&) {
|
||||
LOG_WARN("Ignored malformed local host message");
|
||||
}
|
||||
}
|
||||
if (managed_host_frontend_ && !pending_local_config_.is_null() && !local_config_sent_)
|
||||
local_config_sent_ = local_host_->Send(pending_local_config_);
|
||||
if (managed_host_frontend_) {
|
||||
// Report only the GUI's own helper and privacy intent. Echoing a borrowed
|
||||
// host owner back as GUI-owned would create a feedback loop on release.
|
||||
const bool hidden = SharedDxgiCursorState().LocalPrivacyCursorHidden();
|
||||
const bool requested = privacy_.Requested();
|
||||
if (requested || !config_center_->IsEnablePrivacyScreen()) local_cursor_policy_ready_ = true;
|
||||
if (local_cursor_hidden_sent_ != hidden ||
|
||||
(local_cursor_policy_ready_ && local_cursor_request_sent_ != requested)) {
|
||||
Json command{{"op", "privacy_cursor"}, {"hidden", hidden}};
|
||||
if (local_cursor_policy_ready_) command["requested"] = requested;
|
||||
if (local_host_->Send(command)) {
|
||||
local_cursor_hidden_sent_ = hidden;
|
||||
if (local_cursor_policy_ready_) local_cursor_request_sent_ = requested;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (unattended_host_ && GetTickCount64() >= next_local_host_status_) {
|
||||
next_local_host_status_ = GetTickCount64() + 250;
|
||||
PublishLocalHostStatus();
|
||||
if (show_offline_warning_window_) {
|
||||
error(offline_warning_text_);
|
||||
show_offline_warning_window_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "unattended_config.h"
|
||||
#include "shared_identity.h"
|
||||
#include "platform/windows/winrt_apartment.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int GuiApplication::RunUnattended(void* stop_event) {
|
||||
if (!stop_event || !IsLocalSystemProcess() || !IsUnattendedEnabled() ||
|
||||
if (!stop_event || !IsLocalSystemProcess() ||
|
||||
(!IsUnattendedEnabled() && !SharedHostIdentity::GuiPresent()) ||
|
||||
!PrepareUnattendedDirectory(false))
|
||||
return 1;
|
||||
unattended_host_ = true;
|
||||
@@ -19,12 +22,23 @@ int GuiApplication::RunUnattended(void* stop_event) {
|
||||
|
||||
path_manager_ = std::make_unique<PathManager>("CrossDesk");
|
||||
const auto root = UnattendedDataPath();
|
||||
cache_path_ = root.string();
|
||||
std::string identity_error;
|
||||
shared_identity_ = SharedHostIdentity::AcquireHost(identity_error);
|
||||
if (!shared_identity_) {
|
||||
LOG_ERROR("Cannot acquire the unattended host identity: {}", identity_error);
|
||||
return 1;
|
||||
}
|
||||
cache_path_ = shared_identity_->CachePath().string();
|
||||
exec_log_path_ = (root / L"logs").string();
|
||||
dll_log_path_ = exec_log_path_;
|
||||
InitializeLogger();
|
||||
LOG_INFO("Unattended host initialized in session {}", session);
|
||||
// Reconnect starts WGC on this main thread, whereas the first unlock can
|
||||
// start it on the capture recovery thread. Both need their own WinRT scope.
|
||||
ScopedWinRtApartment apartment;
|
||||
if (!apartment.ready()) return 1;
|
||||
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
||||
InitializeTrustedDevices(root / "trusted-devices");
|
||||
InitializeTrustedDevices(shared_identity_->TrustedDevicesPath());
|
||||
if (config_center_->SetPrivacyScreen(false) != 0 ||
|
||||
config_center_->SetSaveRemotePreviews(false) != 0)
|
||||
return 1;
|
||||
@@ -34,19 +48,22 @@ int GuiApplication::RunUnattended(void* stop_event) {
|
||||
// video driver, a tray icon, or a logged-in user's audio/clipboard session.
|
||||
if (!SDL_Init(SDL_INIT_EVENTS)) return 1;
|
||||
devices_.Initialize();
|
||||
local_host_ = std::make_shared<LocalHostChannel>(true);
|
||||
ULONGLONG last_status = 0;
|
||||
ULONGLONG next_peer_attempt = 0;
|
||||
while (WaitForSingleObject(stop_event, 16) == WAIT_TIMEOUT) {
|
||||
HandleSessionCleanup();
|
||||
HandlePasswordChangeResult();
|
||||
HandleCredentialRecovery();
|
||||
TickLocalHost();
|
||||
const auto now = GetTickCount64();
|
||||
if (!peer_ && now >= next_peer_attempt) {
|
||||
next_peer_attempt = now + 3000;
|
||||
if (CreateConnectionPeer() != 0) CloseConnectionPeer();
|
||||
}
|
||||
TickControllerTrust();
|
||||
HandleConnectionStatusChange();
|
||||
// The GUI owns presence/announcement subscriptions on this transport.
|
||||
// Publishing an empty headless subscription would replace its device list.
|
||||
HandleScreenLock();
|
||||
HandleWindowsServiceIntegration();
|
||||
devices_.UpdateInteractions();
|
||||
@@ -70,6 +87,7 @@ int GuiApplication::RunUnattended(void* stop_event) {
|
||||
remote_input_.Stop();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForSessionCleanup();
|
||||
local_host_.reset();
|
||||
keyboard_.ReleaseAllRemotePressedKeys("unattended_host_exit");
|
||||
devices_.DestroyDevices();
|
||||
devices_.DestroyFactories();
|
||||
|
||||
@@ -295,6 +295,7 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
||||
BuildWindowsServiceStatusAction(broadcast_status);
|
||||
std::string msg = remote_action.to_json();
|
||||
for (const auto& id : ConnectedControllers()) {
|
||||
if (managed_host_frontend_ && id.rfind("lan/", 0) != 0) continue;
|
||||
const int ret = SendControllerData(id, msg.data(), msg.size(),
|
||||
control_data_label_.c_str());
|
||||
if (ret != 0)
|
||||
|
||||
@@ -30,15 +30,18 @@ DWORD FailureCode() {
|
||||
} // namespace
|
||||
|
||||
int wmain(int argc, wchar_t** argv) {
|
||||
if (argc != 5) return ERROR_INVALID_PARAMETER;
|
||||
if (argc != 5 && argc != 6) return ERROR_INVALID_PARAMETER;
|
||||
HANDLE parent = ParseHandle(argv[1]);
|
||||
HANDLE stop = ParseHandle(argv[2]);
|
||||
HANDLE ready = ParseHandle(argv[3]);
|
||||
HANDLE mapping = ParseHandle(argv[4]);
|
||||
HANDLE capture_owner = argc == 6 ? ParseHandle(argv[5]) : nullptr;
|
||||
// Observe the inherited process OBJECT, never a reused PID or a process name.
|
||||
if (!parent || !stop || !ready || !mapping ||
|
||||
WaitForSingleObject(parent, 0) != WAIT_TIMEOUT ||
|
||||
WaitForSingleObject(stop, 0) != WAIT_TIMEOUT)
|
||||
WaitForSingleObject(stop, 0) != WAIT_TIMEOUT ||
|
||||
(argc == 6 && (!capture_owner ||
|
||||
WaitForSingleObject(capture_owner, 0) != WAIT_TIMEOUT)))
|
||||
return ERROR_INVALID_HANDLE;
|
||||
auto shared = static_cast<crossdesk::PrivacyCursorReady*>(MapViewOfFile(
|
||||
mapping, FILE_MAP_WRITE, 0, 0, sizeof(crossdesk::PrivacyCursorReady)));
|
||||
@@ -50,12 +53,13 @@ int wmain(int argc, wchar_t** argv) {
|
||||
InterlockedExchange(&shared->error, static_cast<LONG>(error));
|
||||
if (!SetEvent(ready)) error = FailureCode();
|
||||
if (!error) {
|
||||
HANDLE watched[] = {parent, stop};
|
||||
HANDLE watched[] = {parent, stop, capture_owner};
|
||||
const DWORD count = capture_owner ? 3 : 2;
|
||||
for (;;) {
|
||||
const DWORD result =
|
||||
MsgWaitForMultipleObjects(2, watched, FALSE, 250, QS_ALLINPUT);
|
||||
if (result == WAIT_OBJECT_0 || result == WAIT_OBJECT_0 + 1) break;
|
||||
if (result == WAIT_OBJECT_0 + 2) {
|
||||
MsgWaitForMultipleObjects(count, watched, FALSE, 250, QS_ALLINPUT);
|
||||
if (result < WAIT_OBJECT_0 + count) break;
|
||||
if (result == WAIT_OBJECT_0 + count) {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
@@ -79,5 +83,6 @@ int wmain(int argc, wchar_t** argv) {
|
||||
CloseHandle(ready);
|
||||
CloseHandle(stop);
|
||||
CloseHandle(parent);
|
||||
if (capture_owner) CloseHandle(capture_owner);
|
||||
return static_cast<int>(error);
|
||||
}
|
||||
|
||||
@@ -39,9 +39,34 @@ PrivacyCursorGuard::~PrivacyCursorGuard() {
|
||||
if (stop_event_) CloseHandle(stop_event_);
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::Start(std::string& error) {
|
||||
if (process_ || restore_pending_)
|
||||
bool PrivacyCursorGuard::Start(std::string& error, const std::wstring& desktop,
|
||||
HANDLE capture_owner) {
|
||||
if (process_ || restore_pending_ || delegated_)
|
||||
return Failed("Cursor guard already active", error, ERROR_BUSY);
|
||||
desktop_ = desktop;
|
||||
if (!capture_owner && SharedDxgiCursorState().ExternalControllerAvailable()) {
|
||||
// The managed GUI's privacy request reaches the host on its next tick.
|
||||
// Give that owner time to become ready before falling back to a local
|
||||
// helper (e.g. a GUI-only LAN session). Keep the privacy window responsive.
|
||||
const auto deadline = GetTickCount64() + 1000;
|
||||
while (!SharedDxgiCursorState().ExternalCursorHidden() &&
|
||||
SharedDxgiCursorState().ExternalControllerAvailable() &&
|
||||
GetTickCount64() < deadline) {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
Sleep(10);
|
||||
}
|
||||
}
|
||||
// A managed GUI can reuse the persistent host's confirmed cursor guard.
|
||||
// It must not start a second helper that restores the shared cursor later.
|
||||
// Secure capture with its own client lifetime always owns its watchdog.
|
||||
if (!capture_owner && SharedDxgiCursorState().ExternalCursorHidden()) {
|
||||
delegated_ = true;
|
||||
return true;
|
||||
}
|
||||
helper_failure_reported_ = false;
|
||||
std::vector<wchar_t> module(32768);
|
||||
const DWORD count = GetModuleFileNameW(nullptr, module.data(),
|
||||
@@ -52,10 +77,14 @@ bool PrivacyCursorGuard::Start(std::string& error) {
|
||||
L"crossdesk_privacy_cursor_helper.exe";
|
||||
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
|
||||
Handle parent;
|
||||
Handle owner;
|
||||
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
|
||||
GetCurrentProcess(), &parent.value, SYNCHRONIZE, TRUE,
|
||||
0))
|
||||
return Failed("Duplicate parent process handle", error);
|
||||
if (capture_owner && !DuplicateHandle(GetCurrentProcess(), capture_owner,
|
||||
GetCurrentProcess(), &owner.value, SYNCHRONIZE, TRUE, 0))
|
||||
return Failed("Duplicate capture owner process handle", error);
|
||||
Handle stop{CreateEventW(&security, TRUE, FALSE, nullptr)};
|
||||
Handle ready{CreateEventW(&security, TRUE, FALSE, nullptr)};
|
||||
Handle mapping{CreateFileMappingW(INVALID_HANDLE_VALUE, &security,
|
||||
@@ -73,6 +102,8 @@ bool PrivacyCursorGuard::Start(std::string& error) {
|
||||
std::vector<unsigned char> attributes(bytes);
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.StartupInfo.lpDesktop = desktop.empty()
|
||||
? nullptr : const_cast<wchar_t*>(desktop.c_str());
|
||||
startup.lpAttributeList =
|
||||
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
|
||||
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0,
|
||||
@@ -81,10 +112,11 @@ bool PrivacyCursorGuard::Start(std::string& error) {
|
||||
UnmapViewOfFile(shared);
|
||||
return Failed("Initialize cursor guard handle list", error, code);
|
||||
}
|
||||
HANDLE inherited[] = {parent.value, stop.value, ready.value, mapping.value};
|
||||
std::vector<HANDLE> inherited{parent.value, stop.value, ready.value, mapping.value};
|
||||
if (owner.value) inherited.push_back(owner.value);
|
||||
bool ok = UpdateProcThreadAttribute(
|
||||
startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
inherited, sizeof(inherited), nullptr, nullptr) != FALSE;
|
||||
inherited.data(), inherited.size() * sizeof(HANDLE), nullptr, nullptr) != FALSE;
|
||||
PROCESS_INFORMATION child{};
|
||||
bool used_job_fallback = false;
|
||||
if (ok) {
|
||||
@@ -140,6 +172,11 @@ bool PrivacyCursorGuard::RestoreAfterFailure(std::string& error) {
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::Healthy(std::string& error) {
|
||||
if (delegated_) {
|
||||
if (SharedDxgiCursorState().ExternalCursorHidden()) return true;
|
||||
delegated_ = false;
|
||||
return Start(error, desktop_);
|
||||
}
|
||||
if (process_ && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT) return true;
|
||||
if (restore_pending_ || (process_ && !helper_failure_reported_))
|
||||
RestoreAfterFailure(error);
|
||||
@@ -149,6 +186,10 @@ bool PrivacyCursorGuard::Healthy(std::string& error) {
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::Stop(std::string& error) {
|
||||
if (delegated_) {
|
||||
delegated_ = false;
|
||||
return true;
|
||||
}
|
||||
if (!process_) return !restore_pending_ || RestoreAfterFailure(error);
|
||||
if (!SetEvent(stop_event_)) return Failed("Stop cursor helper", error);
|
||||
if (WaitForSingleObject(process_, 2000) != WAIT_OBJECT_0) {
|
||||
|
||||
@@ -13,18 +13,22 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Called only on the privacy message thread. The child owns cursor suppression
|
||||
// and restores it when either this guard stops it or its parent process exits.
|
||||
// Owned by one privacy or capture thread. The child owns cursor suppression
|
||||
// and restores it on Stop(), parent exit, or optional capture-owner exit.
|
||||
class PrivacyCursorGuard {
|
||||
public:
|
||||
PrivacyCursorGuard() = default;
|
||||
PrivacyCursorGuard(const PrivacyCursorGuard&) = delete;
|
||||
PrivacyCursorGuard& operator=(const PrivacyCursorGuard&) = delete;
|
||||
~PrivacyCursorGuard();
|
||||
bool Start(std::string& error);
|
||||
// Capture on Winlogon can specify its bound desktop and an additional
|
||||
// process whose exit must restore the cursor, even if capture is blocked.
|
||||
bool Start(std::string& error, const std::wstring& desktop = {},
|
||||
HANDLE capture_owner = nullptr);
|
||||
bool Healthy(std::string& error);
|
||||
bool Stop(std::string& error);
|
||||
bool stopped() const { return !process_ && !restore_pending_; }
|
||||
bool stopped() const { return !delegated_ && !process_ && !restore_pending_; }
|
||||
bool delegated() const { return delegated_; }
|
||||
|
||||
private:
|
||||
bool RestoreAfterFailure(std::string& error);
|
||||
@@ -32,6 +36,8 @@ class PrivacyCursorGuard {
|
||||
HANDLE stop_event_ = nullptr;
|
||||
bool restore_pending_ = false;
|
||||
bool helper_failure_reported_ = false;
|
||||
bool delegated_ = false;
|
||||
std::wstring desktop_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,25 +7,18 @@
|
||||
#ifndef _CAPTURE_BACKEND_POLICY_H_
|
||||
#define _CAPTURE_BACKEND_POLICY_H_
|
||||
|
||||
#include <vector>
|
||||
#include <array>
|
||||
|
||||
#include "screen_capture_method.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline std::vector<ScreenCaptureMethod> WindowsCaptureBackendOrder(
|
||||
ScreenCaptureMethod requested, bool headless, bool usbmmidd) {
|
||||
if (requested != ScreenCaptureMethod::Auto) return {requested};
|
||||
if (headless) return {ScreenCaptureMethod::Gdi};
|
||||
// usbmmidd may put its software cursor directly into a DXGI/GDI image.
|
||||
// WGC can exclude it at capture time. Keep WGC for both cursor modes so a
|
||||
// native client joining/leaving never needs to inherit an embedded cursor.
|
||||
// Do not silently fall back to a backend that cannot exclude that cursor.
|
||||
if (usbmmidd) return {ScreenCaptureMethod::Wgc};
|
||||
inline constexpr std::array<ScreenCaptureMethod, 3> WindowsCaptureBackendOrder() {
|
||||
// One policy for physical/virtual outputs, headless and secure desktops.
|
||||
return {ScreenCaptureMethod::Dxgi, ScreenCaptureMethod::Wgc,
|
||||
ScreenCaptureMethod::Gdi};
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -41,12 +41,13 @@ inline DisplayPresence ClassifyDisplayPresence(
|
||||
const DisplayPresenceProbe& probe, bool capture_stalled = false) {
|
||||
if (probe.query_succeeded) {
|
||||
if (probe.available_targets > 0) {
|
||||
// NVIDIA can retain both availability AND EDID/interface identity after
|
||||
// the last unplug. Those describe a target, not a connected monitor.
|
||||
// Require stalled capture plus a desktop adapter with no active monitor,
|
||||
// and exempt built-in/wireless/IDD targets and failed target queries.
|
||||
// A remaining physical monitor (even without EDID) prevents takeover.
|
||||
if (capture_stalled && probe.desktop_devices > 0 &&
|
||||
// KVMs can leave an available phantom target with no monitor identity.
|
||||
// NVIDIA can also retain an old EDID after unplug; only that ambiguous
|
||||
// case needs stalled capture as additional evidence. An active monitor,
|
||||
// built-in/IDD target, or failed target query still prevents takeover.
|
||||
const bool unidentified_only = probe.identified_targets == 0 &&
|
||||
probe.unidentified_targets == probe.available_targets;
|
||||
if ((unidentified_only || capture_stalled) && probe.desktop_devices > 0 &&
|
||||
probe.attached_monitors == 0 &&
|
||||
probe.embedded_or_virtual_targets == 0 &&
|
||||
probe.target_query_failures == 0) {
|
||||
|
||||
@@ -19,7 +19,7 @@ class DxgiCursorState {
|
||||
// a separate pointer update. Preserve this output-specific default across
|
||||
// privacy restoration; an explicit DXGI update can still override it.
|
||||
software_cursor_monitor_ = software_cursor ? monitor : nullptr;
|
||||
embedded_monitor_ = privacy_cursor_hidden_ ? nullptr : software_cursor_monitor_;
|
||||
embedded_monitor_ = HiddenLocked() ? nullptr : software_cursor_monitor_;
|
||||
}
|
||||
|
||||
void Update(int64_t last_mouse_update_time, bool separate_pointer_visible,
|
||||
@@ -28,7 +28,7 @@ class DxgiCursorState {
|
||||
// pointer state until DXGI actually reports another pointer update.
|
||||
if (last_mouse_update_time == 0) return;
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (privacy_cursor_hidden_) return;
|
||||
if (HiddenLocked()) return;
|
||||
embedded_monitor_ = separate_pointer_visible ? nullptr : monitor;
|
||||
}
|
||||
|
||||
@@ -41,10 +41,57 @@ class DxgiCursorState {
|
||||
void SetPrivacyCursorHidden(bool hidden) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (privacy_cursor_hidden_ == hidden) return;
|
||||
const bool previous = HiddenLocked();
|
||||
privacy_cursor_hidden_ = hidden;
|
||||
RefreshHiddenState(previous);
|
||||
}
|
||||
|
||||
void SetExternalCursorHidden(bool hidden) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (external_cursor_hidden_ == hidden) return;
|
||||
const bool previous = HiddenLocked();
|
||||
external_cursor_hidden_ = hidden;
|
||||
RefreshHiddenState(previous);
|
||||
}
|
||||
|
||||
bool LocalPrivacyCursorHidden() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return privacy_cursor_hidden_;
|
||||
}
|
||||
|
||||
bool ExternalCursorHidden() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return external_cursor_hidden_;
|
||||
}
|
||||
|
||||
void SetExternalControllerAvailable(bool available) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
external_controller_available_ = available;
|
||||
}
|
||||
bool ExternalControllerAvailable() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return external_controller_available_;
|
||||
}
|
||||
|
||||
uint64_t CursorVisibilityRevision() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return visibility_revision_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool HiddenLocked() const { return privacy_cursor_hidden_ || external_cursor_hidden_; }
|
||||
void RefreshHiddenState(bool previous) {
|
||||
if (previous == HiddenLocked()) return;
|
||||
++visibility_revision_;
|
||||
// Discard stale pointer-plane updates, then restore the output's default.
|
||||
// A software-cursor IDD may never send a fresh pointer-plane update.
|
||||
embedded_monitor_ = hidden ? nullptr : software_cursor_monitor_;
|
||||
embedded_monitor_ = HiddenLocked() ? nullptr : software_cursor_monitor_;
|
||||
}
|
||||
|
||||
public:
|
||||
bool PrivacyCursorHidden() const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
return HiddenLocked();
|
||||
}
|
||||
|
||||
bool ShouldDrawCursor(bool system_cursor_visible, void* cursor_monitor) const {
|
||||
@@ -53,7 +100,7 @@ class DxgiCursorState {
|
||||
// CURSOR_SHOWING. DXGI Visible=false then means hidden, not embedded in the
|
||||
// captured frame. Keep sending the application's cursor to the controller.
|
||||
// CURSOR_SHOWING still takes precedence when the application hides it.
|
||||
if (privacy_cursor_hidden_) return system_cursor_visible;
|
||||
if (HiddenLocked()) return system_cursor_visible;
|
||||
// A hidden cursor or a pointer on another monitor also makes DXGI report
|
||||
// Visible=false. Only suppress a visible cursor on the captured monitor.
|
||||
return system_cursor_visible &&
|
||||
@@ -65,6 +112,9 @@ class DxgiCursorState {
|
||||
void* embedded_monitor_ = nullptr;
|
||||
void* software_cursor_monitor_ = nullptr;
|
||||
bool privacy_cursor_hidden_ = false;
|
||||
bool external_cursor_hidden_ = false;
|
||||
bool external_controller_available_ = false;
|
||||
uint64_t visibility_revision_ = 0;
|
||||
};
|
||||
|
||||
inline DxgiCursorState& SharedDxgiCursorState() {
|
||||
|
||||
@@ -300,15 +300,25 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
SharedDxgiCursorState().Reset();
|
||||
// A stopped candidate may be destroyed after its replacement has started.
|
||||
// Only the live duplication owns the shared pointer-plane state.
|
||||
if (duplication_) SharedDxgiCursorState().Reset();
|
||||
++duplication_generation_;
|
||||
staging_.Reset();
|
||||
if (duplication_) {
|
||||
duplication_->ReleaseFrame();
|
||||
}
|
||||
ReleaseAcquiredFrame();
|
||||
duplication_.Reset();
|
||||
}
|
||||
|
||||
HRESULT ScreenCapturerDxgi::ReleaseAcquiredFrame() {
|
||||
if (!frame_acquired_) return S_OK;
|
||||
frame_acquired_ = false;
|
||||
const HRESULT result = duplication_->ReleaseFrame();
|
||||
if (FAILED(result))
|
||||
LOG_WARN("DXGI: ReleaseFrame failed, hr=0x{:08X}",
|
||||
static_cast<uint32_t>(result));
|
||||
return result;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::ConvertFrame(int frame_monitor, bool* cursor_embedded,
|
||||
CursorFrameCompositor& compositor) {
|
||||
*cursor_embedded = false;
|
||||
@@ -417,6 +427,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
uint64_t cached_generation = 0;
|
||||
auto next_topology_check = std::chrono::steady_clock::now();
|
||||
unsigned acquire_failures = 0;
|
||||
auto cursor_revision = SharedDxgiCursorState().CursorVisibilityRevision();
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
cached_frame_valid = false;
|
||||
@@ -429,7 +440,13 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
std::unique_lock capture_lock(switch_mutex_);
|
||||
if (paused_ || !running_) continue;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const bool refresh = topology_refresh_requested_.exchange(false);
|
||||
const auto revision = SharedDxgiCursorState().CursorVisibilityRevision();
|
||||
// An IDD can bake the pointer into the last desktop texture. Retire that
|
||||
// cached texture when the user helper hides/restores the system cursor;
|
||||
// pointer-only updates must not keep replaying its old image.
|
||||
const bool refresh = topology_refresh_requested_.exchange(false) ||
|
||||
revision != cursor_revision;
|
||||
cursor_revision = revision;
|
||||
if (refresh || now >= next_topology_check) {
|
||||
next_topology_check = now + std::chrono::milliseconds(500);
|
||||
const bool changed = EnumerateDisplays();
|
||||
@@ -452,6 +469,23 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
|
||||
const int frame_monitor = monitor_index_.load();
|
||||
const auto deliver_cached = [&] {
|
||||
if (!cached_frame_valid || cached_generation != duplication_generation_ ||
|
||||
cached_monitor != monitor_index_.load() || !callback_ || !nv12_frame_)
|
||||
return;
|
||||
// Recompose from the clean staging texture while showing a cursor, or
|
||||
// once after disabling it. Never replay a baked-in stale cursor.
|
||||
if ((show_cursor_.load(std::memory_order_relaxed) || cached_cursor_embedded) &&
|
||||
!ConvertFrame(cached_monitor, &cached_cursor_embedded, cursor_compositor)) {
|
||||
cached_frame_valid = false;
|
||||
return;
|
||||
}
|
||||
const auto stream_id = MakeDisplayStreamId(cached_monitor);
|
||||
capture_lock.unlock();
|
||||
CapturedCursorFrameScope cursor_scope(cached_cursor_embedded);
|
||||
callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_,
|
||||
nv12_height_, stream_id.c_str(), nullptr);
|
||||
};
|
||||
DXGI_OUTDUPL_FRAME_INFO frame_info{};
|
||||
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
|
||||
HRESULT hr = duplication_->AcquireNextFrame(
|
||||
@@ -459,30 +493,13 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
|
||||
// DXGI explicitly reports an unchanged desktop. Re-emit only a frame
|
||||
// successfully acquired for this monitor to keep delivering static frames.
|
||||
if (cached_frame_valid && cached_generation == duplication_generation_ &&
|
||||
cached_monitor == monitor_index_.load() && callback_ && nv12_frame_) {
|
||||
// Recompose from the clean staging texture while showing a cursor, or
|
||||
// once after disabling it. Never replay a baked-in stale cursor.
|
||||
if (show_cursor_.load(std::memory_order_relaxed) ||
|
||||
cached_cursor_embedded) {
|
||||
if (!ConvertFrame(cached_monitor, &cached_cursor_embedded,
|
||||
cursor_compositor)) {
|
||||
cached_frame_valid = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const auto stream_id = MakeDisplayStreamId(cached_monitor);
|
||||
capture_lock.unlock();
|
||||
CapturedCursorFrameScope cursor_scope(cached_cursor_embedded);
|
||||
callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_,
|
||||
nv12_height_, stream_id.c_str(), nullptr);
|
||||
}
|
||||
deliver_cached();
|
||||
continue;
|
||||
}
|
||||
// Never replay a cached image after an acquisition/conversion failure or
|
||||
// a backend rebuild. Display recovery retains the session privacy state.
|
||||
cached_frame_valid = false;
|
||||
if (FAILED(hr)) {
|
||||
cached_frame_valid = false;
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
ReleaseDuplication();
|
||||
// A single ACCESS_LOST (mode switch, fullscreen toggle, UAC return) is
|
||||
@@ -494,6 +511,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
frame_acquired_ = true;
|
||||
acquire_failures = 0;
|
||||
|
||||
if (frame_monitor >= 0 &&
|
||||
@@ -504,16 +522,31 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
display_info_list_[frame_monitor].handle);
|
||||
}
|
||||
|
||||
// A successful acquire can contain only a cursor update. It is not a new
|
||||
// desktop image; in particular, do not seed a newly rebuilt capture with
|
||||
// its surface. Keep the last confirmed desktop until a present arrives.
|
||||
if (frame_info.LastPresentTime.QuadPart == 0) {
|
||||
if (FAILED(ReleaseAcquiredFrame())) {
|
||||
cached_frame_valid = false;
|
||||
ReleaseDuplication();
|
||||
next_topology_check = std::chrono::steady_clock::now();
|
||||
} else {
|
||||
deliver_cached();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
cached_frame_valid = false;
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
|
||||
if (desktop_resource) {
|
||||
hr = desktop_resource->QueryInterface(
|
||||
IID_PPV_ARGS(acquired_tex.GetAddressOf()));
|
||||
if (FAILED(hr)) {
|
||||
duplication_->ReleaseFrame();
|
||||
ReleaseAcquiredFrame();
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
duplication_->ReleaseFrame();
|
||||
ReleaseAcquiredFrame();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -530,7 +563,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
staging_.GetAddressOf());
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: CreateTexture2D staging failed, hr={}", (int)hr);
|
||||
duplication_->ReleaseFrame();
|
||||
ReleaseAcquiredFrame();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -558,7 +591,13 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
last_logged_cursor_embedding_ = static_cast<int>(cursor_embedded);
|
||||
}
|
||||
}
|
||||
duplication_->ReleaseFrame();
|
||||
// A mode/desktop transition can invalidate the acquire during readback.
|
||||
// Do not publish that frame if releasing it reports the lost capture.
|
||||
if (FAILED(ReleaseAcquiredFrame())) {
|
||||
ReleaseDuplication();
|
||||
next_topology_check = std::chrono::steady_clock::now();
|
||||
continue;
|
||||
}
|
||||
capture_lock.unlock();
|
||||
|
||||
if (converted && frame_monitor == monitor_index_.load() && callback_) {
|
||||
|
||||
@@ -66,6 +66,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
bool ConvertFrame(int frame_monitor, bool* cursor_embedded,
|
||||
CursorFrameCompositor& compositor);
|
||||
void ReleaseDuplication();
|
||||
HRESULT ReleaseAcquiredFrame();
|
||||
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
@@ -76,6 +77,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
Microsoft::WRL::ComPtr<ID3D11Device> d3d_device_;
|
||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
|
||||
bool frame_acquired_ = false; // Guarded by switch_mutex_ while capturing.
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
|
||||
DXGI_MODE_ROTATION rotation_ = DXGI_MODE_ROTATION_IDENTITY;
|
||||
uint64_t duplication_generation_ = 0;
|
||||
|
||||
@@ -123,6 +123,7 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
int interval_ms = fps_ > 0 ? (1000 / fps_) : 16;
|
||||
HDC screen_dc = GetDC(nullptr);
|
||||
bool dc_error_logged = false;
|
||||
bool capture_error_logged = false;
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
@@ -184,8 +185,21 @@ void ScreenCapturerGdi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
|
||||
SRCCOPY | CAPTUREBLT);
|
||||
if (!BitBlt(mem_dc, 0, 0, width, height, screen_dc, left, top,
|
||||
SRCCOPY | CAPTUREBLT)) {
|
||||
if (!capture_error_logged) {
|
||||
LOG_WARN("GDI: BitBlt failed, error={}; reacquiring desktop DC", GetLastError());
|
||||
capture_error_logged = true;
|
||||
}
|
||||
SelectObject(mem_dc, old);
|
||||
DeleteObject(dib);
|
||||
DeleteDC(mem_dc);
|
||||
ReleaseDC(nullptr, screen_dc);
|
||||
screen_dc = nullptr;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
|
||||
continue;
|
||||
}
|
||||
capture_error_logged = false;
|
||||
|
||||
bool cursor_embedded = false;
|
||||
if (show_cursor_) {
|
||||
|
||||
@@ -36,7 +36,9 @@
|
||||
#include "service_host.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "../virtual_display/usbmmidd_virtual_display.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
#include "wgc_plugin_capturer.h"
|
||||
#include "../winrt_apartment.h"
|
||||
#include "../privacy/privacy_cursor_guard.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -58,141 +60,21 @@ constexpr int kSecureDesktopCaptureMinFps = 30;
|
||||
constexpr int kSecureDesktopCaptureMaxIntervalMs =
|
||||
1000 / kSecureDesktopCaptureMinFps;
|
||||
|
||||
class CaptureThreadApartment {
|
||||
public:
|
||||
CaptureThreadApartment() : result_(RoInitialize(RO_INIT_MULTITHREADED)) {
|
||||
if (FAILED(result_) && result_ != RPC_E_CHANGED_MODE)
|
||||
LOG_WARN("Windows capturer: WinRT initialization failed, hr={}",
|
||||
static_cast<int>(result_));
|
||||
}
|
||||
~CaptureThreadApartment() {
|
||||
if (SUCCEEDED(result_)) RoUninitialize();
|
||||
std::unique_ptr<ScreenCapturer> CreateCaptureBackend(ScreenCaptureMethod method) {
|
||||
switch (method) {
|
||||
case ScreenCaptureMethod::Dxgi: return std::make_unique<ScreenCapturerDxgi>();
|
||||
case ScreenCaptureMethod::Wgc: return WgcPluginCapturer::Create();
|
||||
case ScreenCaptureMethod::Gdi: return std::make_unique<ScreenCapturerGdi>();
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
HRESULT result_;
|
||||
};
|
||||
|
||||
class WgcPluginCapturer final : public ScreenCapturer {
|
||||
public:
|
||||
using CreateFn = ScreenCapturer* (*)();
|
||||
using DestroyFn = void (*)(ScreenCapturer*);
|
||||
using SetCursorFn = decltype(&CrossDeskSetWgcCursorCapture);
|
||||
using FrameCursorFn = decltype(&CrossDeskWgcFrameCapturesCursor);
|
||||
|
||||
static std::unique_ptr<ScreenCapturer> Create() {
|
||||
std::filesystem::path plugin_path;
|
||||
wchar_t module_path[MAX_PATH] = {0};
|
||||
const DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
|
||||
if (len == 0 || len >= MAX_PATH) {
|
||||
return nullptr;
|
||||
}
|
||||
plugin_path =
|
||||
std::filesystem::path(module_path).parent_path() / L"wgc_plugin.dll";
|
||||
|
||||
HMODULE module = LoadLibraryW(plugin_path.c_str());
|
||||
if (!module) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto create_fn = reinterpret_cast<CreateFn>(
|
||||
GetProcAddress(module, "CrossDeskCreateWgcCapturer"));
|
||||
auto destroy_fn = reinterpret_cast<DestroyFn>(
|
||||
GetProcAddress(module, "CrossDeskDestroyWgcCapturer"));
|
||||
if (!create_fn || !destroy_fn) {
|
||||
FreeLibrary(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScreenCapturer* impl = create_fn();
|
||||
if (!impl) {
|
||||
FreeLibrary(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::unique_ptr<ScreenCapturer>(
|
||||
new WgcPluginCapturer(module, impl, destroy_fn));
|
||||
}
|
||||
|
||||
~WgcPluginCapturer() override {
|
||||
if (impl_) {
|
||||
destroy_fn_(impl_);
|
||||
impl_ = nullptr;
|
||||
}
|
||||
if (module_) {
|
||||
FreeLibrary(module_);
|
||||
module_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
int Init(const int fps, cb_desktop_data cb) override {
|
||||
if (!impl_) return -1;
|
||||
return impl_->Init(fps, [this, cb = std::move(cb)](
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
|
||||
bool embedded = false;
|
||||
if (frame_cursor_fn_) {
|
||||
embedded = frame_cursor_fn_();
|
||||
} else if (legacy_capture_cursor_.load(std::memory_order_relaxed)) {
|
||||
CURSORINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
embedded = GetCursorInfo(&info) && (info.flags & CURSOR_SHOWING) != 0;
|
||||
}
|
||||
// The plugin has its own globals. Bridge only this callback's metadata
|
||||
// into the executable before the wrapper accepts/publishes the frame.
|
||||
CapturedCursorFrameScope cursor_scope(embedded);
|
||||
cb(data, size, width, height, stream_id, native_frame);
|
||||
});
|
||||
}
|
||||
int Destroy() override { return impl_ ? impl_->Destroy() : 0; }
|
||||
int Start(bool show_cursor) override {
|
||||
if (!frame_cursor_fn_) {
|
||||
legacy_capture_cursor_.store(show_cursor, std::memory_order_relaxed);
|
||||
}
|
||||
return impl_ ? impl_->Start(show_cursor) : -1;
|
||||
}
|
||||
int SetCursorCapture(bool enabled) {
|
||||
if (!impl_) return -1;
|
||||
if (set_cursor_fn_) return set_cursor_fn_(impl_, enabled);
|
||||
// Older plugins only apply the cursor setting at startup.
|
||||
const int ret = impl_->Stop();
|
||||
return ret == 0 ? Start(enabled) : ret;
|
||||
}
|
||||
int Stop() override { return impl_ ? impl_->Stop() : 0; }
|
||||
int Pause(int monitor_index) override {
|
||||
return impl_ ? impl_->Pause(monitor_index) : -1;
|
||||
}
|
||||
int Resume(int monitor_index) override {
|
||||
return impl_ ? impl_->Resume(monitor_index) : -1;
|
||||
}
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override {
|
||||
return impl_ ? impl_->GetDisplayInfoList() : std::vector<DisplayInfo>{};
|
||||
}
|
||||
int SwitchTo(int monitor_index) override {
|
||||
return impl_ ? impl_->SwitchTo(monitor_index) : -1;
|
||||
}
|
||||
int ResetToInitialMonitor() override {
|
||||
return impl_ ? impl_->ResetToInitialMonitor() : -1;
|
||||
}
|
||||
|
||||
private:
|
||||
WgcPluginCapturer(HMODULE module, ScreenCapturer* impl, DestroyFn destroy_fn)
|
||||
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {
|
||||
set_cursor_fn_ = reinterpret_cast<SetCursorFn>(
|
||||
GetProcAddress(module, "CrossDeskSetWgcCursorCapture"));
|
||||
frame_cursor_fn_ = reinterpret_cast<FrameCursorFn>(
|
||||
GetProcAddress(module, "CrossDeskWgcFrameCapturesCursor"));
|
||||
// Use the legacy restart path unless both parts of the extension exist.
|
||||
if (!frame_cursor_fn_) set_cursor_fn_ = nullptr;
|
||||
}
|
||||
|
||||
HMODULE module_ = nullptr;
|
||||
ScreenCapturer* impl_ = nullptr;
|
||||
DestroyFn destroy_fn_ = nullptr;
|
||||
SetCursorFn set_cursor_fn_ = nullptr;
|
||||
FrameCursorFn frame_cursor_fn_ = nullptr;
|
||||
std::atomic<bool> legacy_capture_cursor_{false};
|
||||
};
|
||||
ScreenCaptureMethod CaptureBackendMethod(const ScreenCapturer* capturer) {
|
||||
if (dynamic_cast<const ScreenCapturerDxgi*>(capturer)) return ScreenCaptureMethod::Dxgi;
|
||||
if (dynamic_cast<const WgcPluginCapturer*>(capturer)) return ScreenCaptureMethod::Wgc;
|
||||
if (dynamic_cast<const ScreenCapturerGdi*>(capturer)) return ScreenCaptureMethod::Gdi;
|
||||
return ScreenCaptureMethod::Auto;
|
||||
}
|
||||
|
||||
const char* CaptureBackendName(const ScreenCapturer* capturer) {
|
||||
if (dynamic_cast<const ScreenCapturerDxgi*>(capturer)) return "DXGI";
|
||||
@@ -213,6 +95,15 @@ bool CapturesUsbmmiddOutput(ScreenCapturer* capturer) {
|
||||
IsUsbmmiddDisplayDevice(info.szDevice);
|
||||
}
|
||||
|
||||
bool BackendCursorCapture(ScreenCapturer* capturer, bool requested) {
|
||||
// GDI cannot exclude usbmmidd's software cursor. Compose and advertise it
|
||||
// as part of the frame so native controllers suppress their extra cursor.
|
||||
// DXGI already detects cursor embedding from the selected output's metadata.
|
||||
return requested || (dynamic_cast<ScreenCapturerGdi*>(capturer) &&
|
||||
!SharedDxgiCursorState().PrivacyCursorHidden() &&
|
||||
CapturesUsbmmiddOutput(capturer));
|
||||
}
|
||||
|
||||
// Debug aid: CROSSDESK_FORCE_HEADLESS=1 makes a host that has monitors behave
|
||||
// as if it had none until a usbmmidd monitor is plugged, so the virtual
|
||||
// display path can be exercised without unplugging cables. Once the virtual
|
||||
@@ -227,15 +118,10 @@ bool ForcedHeadlessForTesting() {
|
||||
return forced && CountUsbmmiddMonitors() == 0;
|
||||
}
|
||||
|
||||
// Headless compatibility mode: no monitor is attached, so the desktop lives on
|
||||
// a phantom target at an OS-chosen resolution. DXGI cannot duplicate it and
|
||||
// WGC has no real monitor item; GDI reads the composed desktop. This is a
|
||||
// best-effort baseline that needs no driver. Only an explicit Auto selection
|
||||
// takes this shortcut; a pinned method keeps its own failure semantics.
|
||||
bool DetectHeadlessDesktop(ScreenCaptureMethod method,
|
||||
bool capture_stalled = false,
|
||||
// Detect phantom/headless topology for virtual-display provisioning.
|
||||
// This does not change the DXGI/WGC/GDI backend order.
|
||||
bool DetectHeadlessDesktop(bool capture_stalled = false,
|
||||
bool log_probe = false) {
|
||||
if (method != ScreenCaptureMethod::Auto) return false;
|
||||
if (ForcedHeadlessForTesting()) {
|
||||
static std::atomic<bool> logged{false};
|
||||
if (!logged.exchange(true)) {
|
||||
@@ -486,12 +372,77 @@ bool QuerySecureDesktopHelperFrame(
|
||||
ScreenCapturerWin::ScreenCapturerWin() {}
|
||||
ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
|
||||
|
||||
void ScreenCapturerWin::ReleaseNativeCursor() {
|
||||
if (!native_cursor_guard_) return;
|
||||
native_cursor_transition_.store(true, std::memory_order_release);
|
||||
std::string error;
|
||||
if (!native_cursor_guard_->Stop(error))
|
||||
LOG_WARN("Desktop cursor restoration pending: {}", error);
|
||||
native_cursor_guard_.reset();
|
||||
native_cursor_owned_.store(false, std::memory_order_release);
|
||||
native_cursor_transition_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::UpdateNativeCursor(ScreenCapturer* backend) {
|
||||
const bool required = unattended_host_ && native_cursor_enabled_.load() && !show_cursor_.load() &&
|
||||
IsCurrentSessionUserDesktopActive() &&
|
||||
(virtual_display_.active() || CapturesUsbmmiddOutput(backend));
|
||||
if (!required) {
|
||||
ReleaseNativeCursor();
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
if (native_cursor_guard_) {
|
||||
if (native_cursor_guard_->delegated() &&
|
||||
!SharedDxgiCursorState().ExternalCursorHidden())
|
||||
native_cursor_transition_.store(true, std::memory_order_release);
|
||||
if (native_cursor_guard_->Healthy(error)) {
|
||||
native_cursor_owned_.store(!native_cursor_guard_->delegated(), std::memory_order_release);
|
||||
native_cursor_transition_.store(false, std::memory_order_release);
|
||||
return;
|
||||
}
|
||||
ReleaseNativeCursor();
|
||||
}
|
||||
const auto now = GetTickCount64();
|
||||
if (now < next_native_cursor_retry_) return;
|
||||
next_native_cursor_retry_ = now + 1000;
|
||||
native_cursor_transition_.store(true, std::memory_order_release);
|
||||
native_cursor_guard_ = std::make_unique<PrivacyCursorGuard>();
|
||||
if (native_cursor_guard_->Start(error, L"winsta0\\Default")) {
|
||||
native_cursor_owned_.store(!native_cursor_guard_->delegated(), std::memory_order_release);
|
||||
LOG_INFO("Desktop cursor ready before capture: owner={}",
|
||||
native_cursor_guard_->delegated() ? "GUI" : "persistent host");
|
||||
} else {
|
||||
LOG_WARN("Desktop cursor exclusion unavailable: {}; retaining captured cursor", error);
|
||||
native_cursor_guard_.reset();
|
||||
native_cursor_owned_.store(false, std::memory_order_release);
|
||||
}
|
||||
native_cursor_transition_.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::PrepareUnattendedDisplay() {
|
||||
if (!unattended_host_ ||
|
||||
(!virtual_display_.active() && !DetectHeadlessDesktop())) return false;
|
||||
virtual_display_.PrepareAcquire();
|
||||
std::string error;
|
||||
if (!virtual_display_.Acquire(ResolveVirtualDisplayMode(), &error)) {
|
||||
LOG_WARN("Cannot prepare unattended capture display: {}", error);
|
||||
return false;
|
||||
}
|
||||
const auto name = GetDisplayLabel(FindUsbmmiddDisplayDeviceName());
|
||||
std::lock_guard lock(alias_mutex_);
|
||||
virtual_display_name_ = name;
|
||||
virtual_display_slot_ = name.empty() ? -1 : 0;
|
||||
return !name.empty();
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::NotifyPrivacyCapture(bool running) {
|
||||
if (privacy_)
|
||||
privacy_->CaptureChanged(running);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
ResolveVirtualDisplayMode(); // Remember the attached mode before a later unplug/reboot.
|
||||
NotifyPrivacyCapture(false);
|
||||
fps_ = fps;
|
||||
cb_orig_ = cb;
|
||||
@@ -515,10 +466,14 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
if (size == ScreenCapturer::kBackendReset) {
|
||||
SharedCapturedCursorState().Reset();
|
||||
backend_reset_pending_.store(true, std::memory_order_relaxed);
|
||||
// Backend recovery is not a user request to disable local privacy.
|
||||
return;
|
||||
}
|
||||
if (data && size > 0 && w > 0 && h > 0) {
|
||||
if (native_cursor_transition_.load(std::memory_order_acquire)) return;
|
||||
if ((data || native_frame) && size > 0 && w > 0 && h > 0) {
|
||||
backend_frame_received_.store(true, std::memory_order_relaxed);
|
||||
backend_reset_pending_.store(false, std::memory_order_relaxed);
|
||||
last_capture_progress_tick_.store(GetTickCount64(),
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
@@ -572,6 +527,9 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
SharedCapturedCursorState().Update(current_frame_has_cursor, frame_monitor);
|
||||
if (post_secure_desktop_waiting_for_frame_.exchange(
|
||||
false, std::memory_order_relaxed)) {
|
||||
// Keep the independent secure-desktop cursor until a valid normal
|
||||
// frame commits the new source, after its cursor guard is ready.
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
const ULONGLONG start_tick =
|
||||
post_secure_desktop_started_tick_.exchange(
|
||||
0, std::memory_order_relaxed);
|
||||
@@ -587,12 +545,13 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
native_frame);
|
||||
};
|
||||
|
||||
JoinVirtualDisplayThread();
|
||||
const bool prepared_display = PrepareUnattendedDisplay();
|
||||
impl_.reset();
|
||||
const bool headless = DetectHeadlessDesktop(capture_method_);
|
||||
headless_compat_.store(headless, std::memory_order_relaxed);
|
||||
const bool headless = DetectHeadlessDesktop();
|
||||
int ret = -1;
|
||||
for (const auto& create : PreferredBackends(headless)) {
|
||||
std::unique_ptr<ScreenCapturer> candidate = create();
|
||||
for (const auto backend : WindowsCaptureBackendOrder()) {
|
||||
auto candidate = CreateCaptureBackend(backend);
|
||||
ret = candidate ? candidate->Init(fps_, cb_) : -1;
|
||||
if (ret == 0) {
|
||||
impl_ = std::move(candidate);
|
||||
@@ -603,21 +562,38 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
if (impl_) {
|
||||
LOG_INFO("Windows capturer: using {}{}", CaptureBackendName(impl_.get()),
|
||||
headless ? " (headless compatibility mode)" : "");
|
||||
headless ? " (headless desktop)" : "");
|
||||
BuildCanonicalFromImpl();
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
if (prepared_display) {
|
||||
const auto name = virtual_display_name_;
|
||||
const auto displays = impl_->GetDisplayInfoList();
|
||||
if (std::none_of(displays.begin(), displays.end(), [&name](const DisplayInfo& display) {
|
||||
return display.name == name && display.width > 0 && display.height > 0;
|
||||
})) {
|
||||
LOG_ERROR("Prepared startup display '{}' is not available to capture", name);
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(alias_mutex_);
|
||||
canonical_displays_.erase(canonical_displays_.begin() + 1, canonical_displays_.end());
|
||||
}
|
||||
RestoreMonitor(0);
|
||||
const auto selected = GetDisplayInfoList().front();
|
||||
LOG_INFO("Startup display '{}' bound to Display1: {}x{}",
|
||||
name, selected.width, selected.height);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Windows capturer: selected capture method {} failed, ret={}",
|
||||
static_cast<int>(capture_method_), ret);
|
||||
LOG_ERROR("Windows capturer: DXGI/WGC/GDI initialization failed, ret={}", ret);
|
||||
return -1;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Destroy() {
|
||||
Stop();
|
||||
// Stop() releases the virtual display on a worker; finish it here so the
|
||||
// provisioner is idle before its owner goes away.
|
||||
if (unattended_host_) BeginVirtualDisplayRelease();
|
||||
// Finish display release before the provisioner and its owner go away.
|
||||
JoinVirtualDisplayThread();
|
||||
paused_.store(false, std::memory_order_relaxed);
|
||||
if (impl_) {
|
||||
@@ -739,8 +715,11 @@ bool ScreenCapturerWin::TryStartBackend(
|
||||
candidate->SwitchTo(static_cast<int>(found - displays.begin()));
|
||||
}
|
||||
if (ret == 0) {
|
||||
UpdateNativeCursor(candidate.get());
|
||||
backend_frame_received_.store(false, std::memory_order_relaxed);
|
||||
backend_reset_pending_.store(false, std::memory_order_relaxed);
|
||||
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
ret = candidate->Start(show_cursor);
|
||||
ret = candidate->Start(BackendCursorCapture(candidate.get(), show_cursor));
|
||||
}
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name,
|
||||
@@ -754,76 +733,47 @@ bool ScreenCapturerWin::TryStartBackend(
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
impl_ = std::move(candidate);
|
||||
}
|
||||
applied_show_cursor_ = show_cursor;
|
||||
applied_show_cursor_ = BackendCursorCapture(impl_.get(), show_cursor);
|
||||
RestoreMonitor(monitor_index);
|
||||
committed->store(true, std::memory_order_release);
|
||||
LOG_INFO("Windows capturer: started {}", name);
|
||||
if (!required_display.empty())
|
||||
LOG_INFO("Windows capturer: virtual display '{}' captured via {}",
|
||||
required_display, name);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::vector<ScreenCapturerWin::BackendFactory>
|
||||
ScreenCapturerWin::PreferredBackends(bool headless, bool usbmmidd) const {
|
||||
std::vector<BackendFactory> factories;
|
||||
for (const auto backend :
|
||||
WindowsCaptureBackendOrder(capture_method_, headless, usbmmidd)) {
|
||||
switch (backend) {
|
||||
case ScreenCaptureMethod::Dxgi:
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerDxgi>(); });
|
||||
break;
|
||||
case ScreenCaptureMethod::Wgc:
|
||||
factories.push_back([] { return WgcPluginCapturer::Create(); });
|
||||
break;
|
||||
case ScreenCaptureMethod::Gdi:
|
||||
factories.push_back([] { return std::make_unique<ScreenCapturerGdi>(); });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return factories;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::StartPreferredBackend(bool headless,
|
||||
int monitor_index,
|
||||
bool usbmmidd) {
|
||||
if (usbmmidd && capture_method_ == ScreenCaptureMethod::Auto) {
|
||||
LOG_INFO("Windows capturer: usbmmidd uses WGC with cursor capture={}",
|
||||
show_cursor_.load(std::memory_order_relaxed));
|
||||
}
|
||||
for (const auto& create : PreferredBackends(headless, usbmmidd)) {
|
||||
if (TryStartBackend(create(), monitor_index)) return true;
|
||||
bool ScreenCapturerWin::StartPreferredBackend(int monitor_index, ScreenCaptureMethod after) {
|
||||
bool include = after == ScreenCaptureMethod::Auto;
|
||||
for (const auto backend : WindowsCaptureBackendOrder()) {
|
||||
if (!include) { include = backend == after; continue; }
|
||||
if (TryStartBackend(CreateCaptureBackend(backend), monitor_index)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::ResetPostSecureDesktopState() {
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
void ScreenCapturerWin::ResetPostSecureDesktopState(bool secure) {
|
||||
secure_desktop_capture_active_.store(secure, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::MarkStarted() {
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(true);
|
||||
ResetPostSecureDesktopState();
|
||||
if (!secure_capture_thread_.joinable()) {
|
||||
secure_capture_thread_ =
|
||||
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
||||
}
|
||||
return 0;
|
||||
SharedSecureDesktopCursorState().SetActive(secure);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (!impl_) return -1;
|
||||
if (!impl_) {
|
||||
ReleaseNativeCursor();
|
||||
return -1;
|
||||
}
|
||||
if (running_.load(std::memory_order_relaxed)) {
|
||||
return 0;
|
||||
}
|
||||
native_cursor_enabled_.store(native_cursor_default_.load());
|
||||
NotifyPrivacyCapture(false);
|
||||
|
||||
show_cursor_.store(show_cursor, std::memory_order_relaxed);
|
||||
applied_show_cursor_ = show_cursor;
|
||||
ResetPostSecureDesktopState(!IsCurrentSessionUserDesktopActive());
|
||||
SharedCapturedCursorState().Reset();
|
||||
paused_.store(false, std::memory_order_relaxed);
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
@@ -837,94 +787,34 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
// A monitor may have been attached or removed since the last session. Pick
|
||||
// the backend for the desktop as it is now, not as it was at Init.
|
||||
JoinVirtualDisplayThread();
|
||||
{
|
||||
// Reconcile the live target, not the display name cached at process startup.
|
||||
if (!PrepareUnattendedDisplay()) {
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
virtual_display_name_.clear();
|
||||
virtual_display_slot_ = -1;
|
||||
}
|
||||
const bool headless = DetectHeadlessDesktop(capture_method_, false, true);
|
||||
const bool usbmmidd = !headless && CapturesUsbmmiddOutput(impl_.get());
|
||||
// The virtual display is provisioned off this thread (Start() runs on the
|
||||
// UI thread and a driver install can take minutes). Begin in compatibility
|
||||
// mode; MaybeAdoptVirtualDisplay() switches to a GPU backend once ready.
|
||||
DetectHeadlessDesktop(false, true);
|
||||
virtual_display_ready_.store(false, std::memory_order_relaxed);
|
||||
display_probe_requested_.store(true, std::memory_order_relaxed);
|
||||
last_display_probe_tick_ = 0;
|
||||
last_capture_retry_tick_ = 0;
|
||||
last_virtual_adoption_tick_ = 0;
|
||||
headless_recovery_.Reset();
|
||||
const bool was_headless =
|
||||
headless_compat_.exchange(headless, std::memory_order_relaxed);
|
||||
const bool backend_is_gdi = dynamic_cast<ScreenCapturerGdi*>(impl_.get()) != nullptr;
|
||||
int ret = 0;
|
||||
const bool needs_wgc = usbmmidd &&
|
||||
capture_method_ == ScreenCaptureMethod::Auto &&
|
||||
dynamic_cast<WgcPluginCapturer*>(impl_.get()) == nullptr;
|
||||
// A prior session may have fallen back while RDP was changing its output.
|
||||
// Retry the preferred GPU backend on a new connection instead of retaining
|
||||
// WGC/GDI indefinitely. Keep the headless and usbmmidd cursor policies.
|
||||
const bool needs_dxgi = capture_method_ == ScreenCaptureMethod::Auto &&
|
||||
!headless && !usbmmidd &&
|
||||
dynamic_cast<ScreenCapturerDxgi*>(impl_.get()) == nullptr;
|
||||
if (headless != was_headless || (headless && !backend_is_gdi) || needs_wgc ||
|
||||
needs_dxgi) {
|
||||
LOG_INFO("Windows capturer: reselecting capture backend "
|
||||
"(headless={}, usbmmidd={}, retry_dxgi={})", headless, usbmmidd,
|
||||
needs_dxgi);
|
||||
impl_->Destroy();
|
||||
// Keep an instance around on failure so the next Start can retry.
|
||||
std::unique_ptr<ScreenCapturer> previous;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
previous = std::move(impl_);
|
||||
}
|
||||
if (!StartPreferredBackend(headless, requested_monitor, usbmmidd)) {
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
impl_ = std::move(previous);
|
||||
LOG_ERROR("Windows capturer: no capture backend could start");
|
||||
return -1;
|
||||
}
|
||||
return MarkStarted();
|
||||
}
|
||||
impl_->Destroy();
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
RebuildAliasesFromImpl();
|
||||
// Canonical wire/input slots may differ from this backend's refreshed
|
||||
// enumeration. Select through the mapping before its first callback.
|
||||
SwitchTo(requested_monitor);
|
||||
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
ret = impl_->Start(show_cursor);
|
||||
if (!StartPreferredBackend(requested_monitor)) {
|
||||
LOG_ERROR("Windows capturer: DXGI/WGC/GDI could not start");
|
||||
ReleaseNativeCursor();
|
||||
return -1;
|
||||
}
|
||||
if (ret != 0) {
|
||||
if (capture_method_ != ScreenCaptureMethod::Auto) {
|
||||
LOG_ERROR("Windows capturer: selected backend {} failed to start (ret={})",
|
||||
CaptureBackendName(impl_.get()), ret);
|
||||
return ret;
|
||||
}
|
||||
LOG_WARN("Windows capturer: refresh/start failed (ret={}), trying fallback",
|
||||
ret);
|
||||
|
||||
bool fallback_started = false;
|
||||
if (usbmmidd) {
|
||||
fallback_started = StartPreferredBackend(false, requested_monitor, true);
|
||||
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get()) &&
|
||||
TryStartBackend(WgcPluginCapturer::Create(), requested_monitor)) {
|
||||
fallback_started = true;
|
||||
}
|
||||
if (!usbmmidd && !fallback_started && (dynamic_cast<WgcPluginCapturer*>(impl_.get()) ||
|
||||
dynamic_cast<ScreenCapturerDxgi*>(impl_.get()))) {
|
||||
fallback_started = TryStartBackend(std::make_unique<ScreenCapturerGdi>(),
|
||||
requested_monitor);
|
||||
}
|
||||
|
||||
if (!fallback_started) {
|
||||
LOG_ERROR("Windows capturer: all fallbacks failed to start");
|
||||
return ret;
|
||||
}
|
||||
applied_show_cursor_ = BackendCursorCapture(
|
||||
impl_.get(), show_cursor_.load(std::memory_order_relaxed));
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(true);
|
||||
if (!secure_capture_thread_.joinable()) {
|
||||
secure_capture_thread_ =
|
||||
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
||||
}
|
||||
|
||||
return MarkStarted();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Stop() {
|
||||
@@ -943,11 +833,14 @@ int ScreenCapturerWin::Stop() {
|
||||
if (impl_) {
|
||||
ret = impl_->Stop();
|
||||
}
|
||||
ReleaseNativeCursor();
|
||||
SharedCapturedCursorState().Reset();
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
// After the backend released its duplication of the virtual output. The
|
||||
// unplug itself can take seconds; do not hold the UI thread for it.
|
||||
BeginVirtualDisplayRelease();
|
||||
// Keep the machine host's display alive between controller connections.
|
||||
// Destroy() releases it when the host hands off or exits.
|
||||
if (!unattended_host_) BeginVirtualDisplayRelease();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -965,7 +858,7 @@ void ScreenCapturerWin::BeginVirtualDisplayProvisioning() {
|
||||
// A usbmmidd virtual monitor turns the phantom desktop into a real
|
||||
// display target, so the GPU backends and a chosen resolution apply.
|
||||
std::string error;
|
||||
if (virtual_display_.Acquire(kDefaultVirtualDisplayMode, &error)) {
|
||||
if (virtual_display_.Acquire(ResolveVirtualDisplayMode(), &error)) {
|
||||
virtual_display_ready_.store(true, std::memory_order_release);
|
||||
} else if (error != "cancelled") {
|
||||
LOG_WARN("Windows capturer: no usbmmidd virtual display ({}); "
|
||||
@@ -1013,14 +906,13 @@ void ScreenCapturerWin::MaybeAdoptVirtualDisplay() {
|
||||
virtual_display_name_ = virtual_name;
|
||||
virtual_display_slot_ = current_monitor;
|
||||
}
|
||||
if (StartPreferredBackend(false, current_monitor, true)) {
|
||||
if (StartPreferredBackend(current_monitor)) {
|
||||
if (!IsCurrentSessionUserDesktopActive()) impl_->Stop();
|
||||
virtual_display_ready_.store(false, std::memory_order_release);
|
||||
headless_compat_.store(false, std::memory_order_relaxed);
|
||||
// The owner refreshes its display list and input mapping every tick from
|
||||
// GetDisplayInfoList(), so the new geometry is picked up without a hint.
|
||||
} else {
|
||||
LOG_ERROR("Windows capturer: WGC could not start on usbmmidd; "
|
||||
"will retry cursor-excluding capture");
|
||||
LOG_ERROR("Windows capturer: no backend could start on usbmmidd; will retry");
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
virtual_display_name_.clear();
|
||||
@@ -1030,8 +922,7 @@ void ScreenCapturerWin::MaybeAdoptVirtualDisplay() {
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
impl_ = std::move(previous);
|
||||
}
|
||||
// Retain the stopped instance for Stop()/the next retry, but do not start
|
||||
// DXGI/GDI on the now-attached IDD: it can bake the cursor into the video.
|
||||
// Retain the stopped instance for Stop() and the next ordered retry.
|
||||
}
|
||||
NotifyPrivacyCapture(true);
|
||||
}
|
||||
@@ -1074,6 +965,7 @@ int ScreenCapturerWin::SwitchTo(int monitor_index) {
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::ResetToInitialMonitor() {
|
||||
if (unattended_host_ && virtual_display_.active()) return SwitchTo(0);
|
||||
std::lock_guard<std::mutex> lock(impl_mutex_);
|
||||
if (!impl_) return -1;
|
||||
const int ret = impl_->ResetToInitialMonitor();
|
||||
@@ -1230,69 +1122,20 @@ void ScreenCapturerWin::StopSecureCaptureThread() {
|
||||
|
||||
bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||
NotifyPrivacyCapture(false);
|
||||
if (!impl_ || !running_.load(std::memory_order_relaxed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||
const bool usbmmidd = virtual_display_.active() || CapturesUsbmmiddOutput(impl_.get());
|
||||
const bool headless = DetectHeadlessDesktop(capture_method_, false, true);
|
||||
headless_compat_.store(headless, std::memory_order_relaxed);
|
||||
|
||||
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
||||
if (!impl_ || !running_.load(std::memory_order_relaxed)) return false;
|
||||
const int monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||
LOG_INFO("Windows capturer: restarting DXGI/WGC/GDI after secure desktop");
|
||||
impl_->Stop();
|
||||
if (capture_method_ == ScreenCaptureMethod::Auto && !headless && !usbmmidd &&
|
||||
dynamic_cast<ScreenCapturerDxgi*>(impl_.get()) == nullptr) {
|
||||
LOG_INFO("Windows capturer: desktop recovered; retrying DXGI before {}",
|
||||
CaptureBackendName(impl_.get()));
|
||||
if (StartPreferredBackend(headless, current_monitor, usbmmidd)) {
|
||||
NotifyPrivacyCapture(true);
|
||||
return true;
|
||||
}
|
||||
// Failed candidates retain the previous instance, which may still recover
|
||||
// even if constructing a replacement failed during the display transition.
|
||||
}
|
||||
last_capture_progress_tick_.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
int ret = impl_->Start(show_cursor);
|
||||
if (ret == 0) {
|
||||
applied_show_cursor_ = show_cursor;
|
||||
RestoreMonitor(current_monitor);
|
||||
if (StartPreferredBackend(monitor)) {
|
||||
NotifyPrivacyCapture(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_WARN(
|
||||
"Windows capturer: capture backend restart after secure desktop failed "
|
||||
"(ret={}), rebuilding backend",
|
||||
ret);
|
||||
impl_->Destroy();
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
ret = impl_->Start(show_cursor);
|
||||
}
|
||||
if (ret == 0) {
|
||||
applied_show_cursor_ = show_cursor;
|
||||
RestoreMonitor(current_monitor);
|
||||
NotifyPrivacyCapture(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (StartPreferredBackend(headless_compat_.load(std::memory_order_relaxed),
|
||||
current_monitor, usbmmidd)) {
|
||||
NotifyPrivacyCapture(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_WARN(
|
||||
"Windows capturer: all backend restart attempts after secure desktop "
|
||||
"failed (last_ret={})",
|
||||
ret);
|
||||
LOG_WARN("Windows capturer: all backend restart attempts failed");
|
||||
ReleaseNativeCursor();
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
if (capture_method_ != ScreenCaptureMethod::Auto) return;
|
||||
// Topology events accelerate the next probe, but driver rebuild callbacks
|
||||
// must not flood QueryDisplayConfig. Polling also covers WGC/GDI and unplug
|
||||
// events that NVIDIA does not expose as a changed DXGI output.
|
||||
@@ -1303,7 +1146,8 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
last_display_probe_tick_ = now;
|
||||
const bool topology_changed =
|
||||
display_probe_requested_.exchange(false, std::memory_order_relaxed);
|
||||
if (!IsCurrentSessionUserDesktopActive()) {
|
||||
const bool desktop_available = IsCurrentSessionUserDesktopActive();
|
||||
if (!desktop_available && !unattended_host_) {
|
||||
headless_recovery_.ShouldProvision(now, false, false, false);
|
||||
return;
|
||||
}
|
||||
@@ -1311,54 +1155,77 @@ void ScreenCapturerWin::CheckDisplayPresence(ULONGLONG now) {
|
||||
last_capture_progress_tick_.load(std::memory_order_relaxed);
|
||||
const bool stalled = last_progress != 0 && now >= last_progress &&
|
||||
now - last_progress >= kCaptureStallTimeoutMs;
|
||||
const bool headless = DetectHeadlessDesktop(capture_method_, stalled,
|
||||
const bool headless = DetectHeadlessDesktop(stalled,
|
||||
topology_changed);
|
||||
const bool owns_display = virtual_display_.active();
|
||||
bool target_ready = false;
|
||||
if (owns_display) {
|
||||
const auto current = GetDisplayLabel(FindUsbmmiddDisplayDeviceName());
|
||||
std::lock_guard lock(alias_mutex_);
|
||||
target_ready = !current.empty() && current == virtual_display_name_;
|
||||
}
|
||||
if (!headless_recovery_.ShouldProvision(
|
||||
now, headless, virtual_display_busy_.load(std::memory_order_acquire),
|
||||
virtual_display_.active())) return;
|
||||
now, headless || (owns_display && !target_ready),
|
||||
virtual_display_busy_.load(std::memory_order_acquire), target_ready)) return;
|
||||
if (!running_.load(std::memory_order_relaxed)) return;
|
||||
headless_recovery_.Attempted(now);
|
||||
headless_compat_.store(true, std::memory_order_relaxed);
|
||||
// The current backend has lost its usable target. Continue the same order
|
||||
// while the virtual-display worker prepares a replacement target.
|
||||
if (headless && desktop_available && impl_ &&
|
||||
dynamic_cast<ScreenCapturerGdi*>(impl_.get()) == nullptr) {
|
||||
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(false);
|
||||
impl_->Stop();
|
||||
const bool started = StartPreferredBackend(current_monitor, CaptureBackendMethod(impl_.get()));
|
||||
NotifyPrivacyCapture(started);
|
||||
}
|
||||
LOG_WARN("Windows capturer: confirmed headless desktop during capture "
|
||||
"(stalled={}); provisioning usbmmidd", stalled);
|
||||
BeginVirtualDisplayProvisioning();
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::CheckCaptureProgress(ULONGLONG now) {
|
||||
if (capture_method_ != ScreenCaptureMethod::Auto || !running_.load()) return;
|
||||
auto* dxgi = dynamic_cast<ScreenCapturerDxgi*>(impl_.get());
|
||||
if (!dxgi) return;
|
||||
|
||||
const auto last_progress =
|
||||
last_capture_progress_tick_.load(std::memory_order_relaxed);
|
||||
// A capture callback may have advanced the tick after this loop sampled now.
|
||||
if (last_progress == 0 || now < last_progress ||
|
||||
now - last_progress < kCaptureStallTimeoutMs) return;
|
||||
if (last_capture_retry_tick_ != 0 &&
|
||||
now - last_capture_retry_tick_ < kCaptureStallTimeoutMs) return;
|
||||
// Probe/recovery is handled separately. When another real monitor remains,
|
||||
// retry only the pinned output instead of silently selecting that monitor.
|
||||
LOG_WARN("Windows capturer: DXGI has delivered no frame for {} ms; "
|
||||
"refreshing the selected output", now - last_progress);
|
||||
dxgi->RequestTopologyRefresh();
|
||||
if (!running_.load() || !impl_) return;
|
||||
if (dynamic_cast<WgcPluginCapturer*>(impl_.get()) && backend_frame_received_.load() &&
|
||||
!backend_reset_pending_.load()) return; // Static WGC images have no new events.
|
||||
const auto last_progress = last_capture_progress_tick_.load(std::memory_order_relaxed);
|
||||
if (!last_progress || now < last_progress || now - last_progress < kCaptureStallTimeoutMs) return;
|
||||
if (last_capture_retry_tick_ && now - last_capture_retry_tick_ < kCaptureStallTimeoutMs) return;
|
||||
if (auto* dxgi = dynamic_cast<ScreenCapturerDxgi*>(impl_.get());
|
||||
dxgi && (!last_capture_retry_tick_ || last_progress > last_capture_retry_tick_)) {
|
||||
LOG_WARN("Windows capturer: DXGI has no frame; refreshing the selected output");
|
||||
dxgi->RequestTopologyRefresh();
|
||||
} else {
|
||||
const auto failed = CaptureBackendMethod(impl_.get());
|
||||
const int monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||
LOG_WARN("Windows capturer: {} has no frame; advancing DXGI/WGC/GDI fallback", CaptureBackendName(impl_.get()));
|
||||
NotifyPrivacyCapture(false);
|
||||
impl_->Stop();
|
||||
// Exhausting the sequence starts a fresh ordered recovery cycle.
|
||||
const bool started = StartPreferredBackend(monitor, failed) || StartPreferredBackend(monitor);
|
||||
NotifyPrivacyCapture(started);
|
||||
if (!started) ReleaseNativeCursor();
|
||||
}
|
||||
last_capture_retry_tick_ = now;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::ApplyCursorCaptureSetting() {
|
||||
const bool requested = show_cursor_.load(std::memory_order_relaxed);
|
||||
if (!impl_ || requested == applied_show_cursor_) return;
|
||||
if (!impl_) return;
|
||||
const bool effective = BackendCursorCapture(impl_.get(), requested);
|
||||
if (effective == applied_show_cursor_) return;
|
||||
int ret = 0;
|
||||
if (auto* gdi = dynamic_cast<ScreenCapturerGdi*>(impl_.get())) {
|
||||
gdi->SetCursorCapture(requested);
|
||||
gdi->SetCursorCapture(effective);
|
||||
} else if (auto* dxgi = dynamic_cast<ScreenCapturerDxgi*>(impl_.get())) {
|
||||
dxgi->SetCursorCapture(requested);
|
||||
dxgi->SetCursorCapture(effective);
|
||||
} else if (auto* wgc = dynamic_cast<WgcPluginCapturer*>(impl_.get())) {
|
||||
ret = wgc->SetCursorCapture(requested);
|
||||
ret = wgc->SetCursorCapture(effective);
|
||||
}
|
||||
if (ret == 0) {
|
||||
applied_show_cursor_ = requested;
|
||||
LOG_INFO("Windows capturer cursor capture updated: backend={}, enabled={}",
|
||||
CaptureBackendName(impl_.get()), requested);
|
||||
applied_show_cursor_ = effective;
|
||||
LOG_INFO("Windows capturer cursor capture updated: backend={}, requested={}, effective={}",
|
||||
CaptureBackendName(impl_.get()), requested, effective);
|
||||
} else {
|
||||
LOG_WARN("Windows capturer cursor update failed, ret={}; will retry", ret);
|
||||
}
|
||||
@@ -1434,6 +1301,7 @@ void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
||||
secure_shared_start_pending_ = false;
|
||||
secure_shared_session_id_ = 0xFFFFFFFF;
|
||||
secure_shared_configuration_ = {};
|
||||
secure_shared_last_keepalive_ = 0;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
||||
@@ -1586,6 +1454,20 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
secure_shared_session_id_ == session_id &&
|
||||
secure_shared_configuration_ == configuration &&
|
||||
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
const auto now = GetTickCount64();
|
||||
if (now - secure_shared_last_keepalive_ >= 1000) {
|
||||
// Reuse the idempotent start operation to renew the producer's lease.
|
||||
// If a stop packet is lost, the helper still restores the local cursor.
|
||||
secure_shared_last_keepalive_ = now;
|
||||
std::vector<uint8_t> response;
|
||||
std::string error;
|
||||
const auto command = BuildSecureCaptureStartCommand(
|
||||
left, top, width, height, show_cursor, fps, stage, desktop);
|
||||
const bool sent = QuerySecureDesktopHelperCommand(session_id, command, &response, &error);
|
||||
const auto reply = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||
if (!sent || !reply.is_object() || !reply.value("ok", false))
|
||||
LOG_WARN("Secure capture keepalive failed; retaining available frames: {}", error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1622,6 +1504,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
secure_shared_capture_started_ = true;
|
||||
secure_shared_session_id_ = session_id;
|
||||
secure_shared_configuration_ = configuration;
|
||||
secure_shared_last_keepalive_ = GetTickCount64();
|
||||
|
||||
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
StopSecureDesktopSharedCapture(session_id);
|
||||
@@ -1634,7 +1517,7 @@ bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
// Virtual-display adoption constructs WGC on this std::thread, which does
|
||||
// not inherit the UI thread's WinRT apartment.
|
||||
CaptureThreadApartment apartment;
|
||||
ScopedWinRtApartment apartment;
|
||||
const int frame_interval_ms =
|
||||
fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
|
||||
: kSecureDesktopCaptureMaxIntervalMs;
|
||||
@@ -1647,7 +1530,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
bool normal_backend_suspended = false;
|
||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||
ULONGLONG last_post_secure_restart_tick = 0;
|
||||
ULONGLONG next_cursor_update_tick = 0;
|
||||
SecureDesktopServiceStatus status;
|
||||
SecureDesktopStatusPoller status_poller(
|
||||
QuerySecureDesktopServiceStatus,
|
||||
@@ -1743,7 +1625,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
|
||||
secure_desktop_capture_active_.store(status.capture_active,
|
||||
std::memory_order_relaxed);
|
||||
SharedSecureDesktopCursorState().SetActive(status.capture_active);
|
||||
if (status.capture_active)
|
||||
SharedSecureDesktopCursorState().SetActive(true);
|
||||
if (status.capture_active != previous.capture_active ||
|
||||
status.interactive_stage != previous.interactive_stage ||
|
||||
status.active_session_id != previous.active_session_id ||
|
||||
@@ -1816,12 +1699,18 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
if (unattended_host_ && !desktop_available) {
|
||||
ReleaseNativeCursor();
|
||||
CheckDisplayPresence(now);
|
||||
MaybeAdoptVirtualDisplay();
|
||||
}
|
||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
// Normal capture was intentionally unavailable on the secure desktop.
|
||||
// Do not schedule a second topology/stall rebuild during its recovery.
|
||||
if (!post_secure_restart_pending && desktop_available &&
|
||||
!normal_backend_suspended) {
|
||||
UpdateNativeCursor(impl_.get());
|
||||
CheckDisplayPresence(now);
|
||||
MaybeAdoptVirtualDisplay();
|
||||
CheckCaptureProgress(now);
|
||||
@@ -1853,9 +1742,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
privacy_recovery_pending = false;
|
||||
if (privacy_) privacy_->ResumeAfterDesktopRecovery();
|
||||
}
|
||||
if (!post_secure_restart_pending && !normal_backend_suspended &&
|
||||
now >= next_cursor_update_tick) {
|
||||
next_cursor_update_tick = now + 500;
|
||||
// Apply once after any backend/desktop recovery in this iteration.
|
||||
if (desktop_available && !post_secure_restart_pending && !normal_backend_suspended) {
|
||||
ApplyCursorCaptureSetting();
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -31,6 +30,7 @@
|
||||
namespace crossdesk {
|
||||
|
||||
class CapturedNv12FramePool;
|
||||
class PrivacyCursorGuard;
|
||||
|
||||
class ScreenCapturerWin : public ScreenCapturer {
|
||||
public:
|
||||
@@ -55,27 +55,33 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
int GetCurrentMonitorIndex() const override;
|
||||
// True while this session captures a usbmmidd monitor it plugged itself.
|
||||
bool IsVirtualDisplayActive() const { return virtual_display_.active(); }
|
||||
bool OwnsNativeCursor() const { return native_cursor_owned_.load(); }
|
||||
void SetNativeCursorPolicy(bool default_enabled, bool current_enabled) {
|
||||
native_cursor_default_.store(default_enabled);
|
||||
native_cursor_enabled_.store(current_enabled);
|
||||
}
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
// Set before Init(), while capture is stopped.
|
||||
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
|
||||
void SetUnattendedHost(bool enabled) { unattended_host_ = enabled; }
|
||||
// Query synchronously inside the frame callback, on the delivering thread.
|
||||
// This describes the actual frame, independently of service status polling.
|
||||
static bool CurrentFrameIsFromSecureDesktop();
|
||||
|
||||
private:
|
||||
using BackendFactory = std::function<std::unique_ptr<ScreenCapturer>()>;
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
// Serializes backend replacement (secure-desktop recovery runs on the
|
||||
// management thread) against the public entry points that dereference it.
|
||||
// Lock order: impl_mutex_ before alias_mutex_; never the reverse.
|
||||
std::mutex impl_mutex_;
|
||||
ScreenCaptureMethod capture_method_ = ScreenCaptureMethod::Auto;
|
||||
bool IsBackendEnabled(ScreenCaptureMethod method) const {
|
||||
return capture_method_ == ScreenCaptureMethod::Auto ||
|
||||
capture_method_ == method;
|
||||
}
|
||||
bool unattended_host_ = false;
|
||||
bool PrepareUnattendedDisplay();
|
||||
void UpdateNativeCursor(ScreenCapturer* backend);
|
||||
void ReleaseNativeCursor();
|
||||
std::unique_ptr<PrivacyCursorGuard> native_cursor_guard_;
|
||||
std::atomic<bool> native_cursor_owned_{false};
|
||||
std::atomic<bool> native_cursor_transition_{false};
|
||||
std::atomic<bool> native_cursor_default_{true}, native_cursor_enabled_{true};
|
||||
ULONGLONG next_native_cursor_retry_ = 0;
|
||||
PrivacyController* privacy_ = nullptr;
|
||||
void NotifyPrivacyCapture(bool running);
|
||||
int fps_ = 60;
|
||||
@@ -92,6 +98,7 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::atomic<bool> native_output_error_logged_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<ULONGLONG> last_capture_progress_tick_{0};
|
||||
std::atomic<bool> backend_frame_received_{false}, backend_reset_pending_{false};
|
||||
std::atomic<bool> display_probe_requested_{false};
|
||||
ULONGLONG last_display_probe_tick_ = 0;
|
||||
ULONGLONG last_capture_retry_tick_ = 0;
|
||||
@@ -99,13 +106,11 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::atomic<bool> paused_{false};
|
||||
std::atomic<bool> show_cursor_{true};
|
||||
bool applied_show_cursor_ = true;
|
||||
std::atomic<bool> headless_compat_{false};
|
||||
// Session-scoped: plugged on a headless desktop after Start() returns and
|
||||
// released after Stop(), so an attached physical monitor is never shadowed
|
||||
// between sessions. Both run on virtual_display_thread_ because the helper
|
||||
// may need up to two minutes to install the driver; Start() itself begins
|
||||
// in GDI compatibility mode and the management thread adopts the virtual
|
||||
// display (switching to a GPU backend) once virtual_display_ready_ is set.
|
||||
// GUI sessions provision on a worker and release after Stop(). Machine
|
||||
// hosts prepare before signaling, revalidate before capture, and retain
|
||||
// the monitor until Destroy(). Recovery also handles lost login targets.
|
||||
// The management thread adopts the output once virtual_display_ready_ is
|
||||
// set, trying the same ordered backends on the replacement target.
|
||||
VirtualDisplayProvisioner virtual_display_;
|
||||
std::thread virtual_display_thread_;
|
||||
std::atomic<bool> virtual_display_ready_{false};
|
||||
@@ -128,23 +133,17 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
SharedCaptureConfiguration secure_shared_configuration_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
bool secure_shared_start_pending_ = false;
|
||||
ULONGLONG secure_shared_last_keepalive_ = 0;
|
||||
std::shared_ptr<CapturedNv12FramePool> native_frame_pool_;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl(bool preserve_backend_slots = false);
|
||||
void RestoreMonitor(int monitor_index);
|
||||
// Backends in preference order for the current desktop; each factory is
|
||||
// invoked lazily so an unused WGC plugin is never loaded.
|
||||
std::vector<BackendFactory> PreferredBackends(bool headless,
|
||||
bool usbmmidd = false) const;
|
||||
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
|
||||
int monitor_index);
|
||||
bool StartPreferredBackend(bool headless, int monitor_index,
|
||||
bool usbmmidd = false);
|
||||
void ResetPostSecureDesktopState();
|
||||
// Common tail of every successful Start(): publish running state and
|
||||
// launch the management thread.
|
||||
int MarkStarted();
|
||||
bool StartPreferredBackend(int monitor_index,
|
||||
ScreenCaptureMethod after = ScreenCaptureMethod::Auto);
|
||||
void ResetPostSecureDesktopState(bool secure = false);
|
||||
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame = nullptr,
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SECURE_DESKTOP_WGC_CAPTURE_H_
|
||||
#define _SECURE_DESKTOP_WGC_CAPTURE_H_
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include <dxgi.h>
|
||||
#include <cstring>
|
||||
#include <future>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "wgc_plugin_capturer.h"
|
||||
#include "../winrt_apartment.h"
|
||||
#include "../windows_thread_dpi.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// The helper's producer consumes the latest cropped NV12 image. The plugin
|
||||
// remains responsible for WGC/WinRT and cursor exclusion, as in normal capture.
|
||||
class SecureDesktopWgcCapture {
|
||||
public:
|
||||
~SecureDesktopWgcCapture() {
|
||||
if (stop_) SetEvent(stop_);
|
||||
if (worker_.joinable()) worker_.join();
|
||||
// Close from the caller after the worker's windows have been destroyed.
|
||||
if (desktop_) CloseDesktop(desktop_);
|
||||
if (stop_) CloseHandle(stop_);
|
||||
}
|
||||
|
||||
int Initialize(int left, int top, int width, int height, int fps, bool cursor) {
|
||||
if (width <= 0 || height <= 0 || width > 16384 || height > 16384 ||
|
||||
((width | height) & 1) || worker_.joinable()) return -1;
|
||||
DWORD bytes = 0;
|
||||
const auto current = GetThreadDesktop(GetCurrentThreadId());
|
||||
GetUserObjectInformationW(current, UOI_NAME, nullptr, 0, &bytes);
|
||||
if (!bytes) return -1;
|
||||
std::vector<wchar_t> name(bytes / sizeof(wchar_t) + 1);
|
||||
if (!GetUserObjectInformationW(current, UOI_NAME, name.data(), bytes, &bytes)) return -1;
|
||||
desktop_ = OpenDesktopW(name.data(), 0, FALSE,
|
||||
DESKTOP_READOBJECTS | DESKTOP_WRITEOBJECTS | DESKTOP_CREATEWINDOW);
|
||||
stop_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!desktop_ || !stop_) return -1;
|
||||
std::promise<int> ready;
|
||||
auto initialized = ready.get_future();
|
||||
// WinRT may create thread-owned windows. Keep those off the producer,
|
||||
// which must remain able to bind another input desktop after lock/unlock.
|
||||
worker_ = std::thread([this, left, top, width, height, fps, cursor,
|
||||
ready = std::move(ready)]() mutable {
|
||||
if (!SetThreadDesktop(desktop_)) { ready.set_value(-1); return; }
|
||||
ScopedWindowsPhysicalCoordinates coordinates;
|
||||
ScopedWinRtApartment apartment;
|
||||
int result = -1;
|
||||
try {
|
||||
if (apartment.ready()) result = InitializeWorker(left, top, width, height, fps, cursor);
|
||||
} catch (...) { result = -1; }
|
||||
ready.set_value(result);
|
||||
if (result == 0) {
|
||||
while (MsgWaitForMultipleObjects(1, &stop_, FALSE, INFINITE, QS_ALLINPUT) == WAIT_OBJECT_0 + 1) {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drain callbacks before either the apartment or frame storage goes away.
|
||||
if (capture_) { capture_->Stop(); capture_->Destroy(); capture_.reset(); }
|
||||
});
|
||||
return initialized.get();
|
||||
}
|
||||
|
||||
private:
|
||||
int InitializeWorker(int left, int top, int width, int height, int fps, bool cursor) {
|
||||
capture_ = WgcPluginCapturer::Create();
|
||||
if (!capture_) return -1;
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
const int initialized = capture_->Init(fps, [this](unsigned char* data,
|
||||
int size, int width, int height, const char* stream, const MiniRtcNativeVideoFrame*) {
|
||||
if (size == ScreenCapturer::kBackendReset || size == ScreenCapturer::kDisplayTopologyChanged) {
|
||||
std::lock_guard lock(mutex_);
|
||||
invalidated_ = true;
|
||||
return;
|
||||
}
|
||||
if (!data || size <= 0 || !stream || stream_ != stream) return;
|
||||
std::lock_guard lock(mutex_);
|
||||
if (x_ < 0 || y_ < 0 || width <= 0 || height <= 0 ||
|
||||
x_ + width_ > width || y_ + height_ > height ||
|
||||
static_cast<size_t>(size) < static_cast<size_t>(width) * height * 3 / 2) {
|
||||
invalidated_ = true;
|
||||
return;
|
||||
}
|
||||
const size_t luma = static_cast<size_t>(width_) * height_;
|
||||
pending_.resize(luma * 3 / 2);
|
||||
for (int row = 0; row < height_; ++row)
|
||||
std::memcpy(pending_.data() + static_cast<size_t>(row) * width_,
|
||||
data + static_cast<size_t>(y_ + row) * width + x_, width_);
|
||||
const auto* uv = data + static_cast<size_t>(width) * height;
|
||||
for (int row = 0; row < height_ / 2; ++row)
|
||||
std::memcpy(pending_.data() + luma + static_cast<size_t>(row) * width_,
|
||||
uv + static_cast<size_t>(y_ / 2 + row) * width + x_, width_);
|
||||
pending_cursor_ = current_frame_has_cursor;
|
||||
invalidated_ = false;
|
||||
++sequence_;
|
||||
});
|
||||
if (initialized != 0) return initialized;
|
||||
const auto displays = capture_->GetDisplayInfoList();
|
||||
for (size_t index = 0; index < displays.size(); ++index) {
|
||||
const auto& display = displays[index];
|
||||
if (left < display.left || top < display.top ||
|
||||
static_cast<int64_t>(left) + width > display.right ||
|
||||
static_cast<int64_t>(top) + height > display.bottom) continue;
|
||||
x_ = left - display.left;
|
||||
y_ = top - display.top;
|
||||
// The IPC regions are normally whole monitors. A region with an odd
|
||||
// chroma offset, or spanning outputs, falls through to GDI.
|
||||
if ((x_ | y_ | width | height) & 1) return -1;
|
||||
stream_ = MakeDisplayStreamId(index);
|
||||
const int selected = capture_->SwitchTo(static_cast<int>(index));
|
||||
return selected == 0 ? capture_->Start(cursor) : selected;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public:
|
||||
HRESULT Capture() {
|
||||
std::lock_guard lock(mutex_);
|
||||
updated_ = false;
|
||||
if (invalidated_) return DXGI_ERROR_ACCESS_LOST;
|
||||
if (!sequence_) return S_FALSE;
|
||||
// A static desktop need not produce another WGC event. Retain the valid
|
||||
// image until a new frame or an explicit reset, just like DXGI timeouts.
|
||||
if (consumed_ != sequence_) {
|
||||
pixels_.swap(pending_);
|
||||
cursor_ = pending_cursor_;
|
||||
consumed_ = sequence_;
|
||||
updated_ = true;
|
||||
}
|
||||
return S_OK;
|
||||
}
|
||||
const std::vector<uint8_t>& pixels() const { return pixels_; }
|
||||
bool updated() const { return updated_; }
|
||||
bool CursorEmbedded() const { return cursor_; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> capture_;
|
||||
HDESK desktop_ = nullptr;
|
||||
HANDLE stop_ = nullptr;
|
||||
std::thread worker_;
|
||||
std::mutex mutex_;
|
||||
int width_ = 0, height_ = 0, x_ = 0, y_ = 0;
|
||||
std::string stream_;
|
||||
std::vector<uint8_t> pending_, pixels_;
|
||||
uint64_t sequence_ = 0, consumed_ = 0;
|
||||
bool invalidated_ = false;
|
||||
bool pending_cursor_ = false, cursor_ = false, updated_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WGC_PLUGIN_CAPTURER_H_
|
||||
#define _WGC_PLUGIN_CAPTURER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
#include "captured_cursor_state.h"
|
||||
#include "wgc_plugin_api.h"
|
||||
|
||||
namespace crossdesk {
|
||||
class WgcPluginCapturer final : public ScreenCapturer {
|
||||
public:
|
||||
using CreateFn = ScreenCapturer* (*)();
|
||||
using DestroyFn = void (*)(ScreenCapturer*);
|
||||
using SetCursorFn = decltype(&CrossDeskSetWgcCursorCapture);
|
||||
using FrameCursorFn = decltype(&CrossDeskWgcFrameCapturesCursor);
|
||||
|
||||
static std::unique_ptr<ScreenCapturer> Create() {
|
||||
wchar_t module_path[MAX_PATH] = {0};
|
||||
const DWORD len = GetModuleFileNameW(nullptr, module_path, MAX_PATH);
|
||||
if (len == 0 || len >= MAX_PATH) {
|
||||
return nullptr;
|
||||
}
|
||||
const auto plugin_path =
|
||||
std::filesystem::path(module_path).parent_path() / L"wgc_plugin.dll";
|
||||
|
||||
HMODULE module = LoadLibraryW(plugin_path.c_str());
|
||||
if (!module) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto create_fn = reinterpret_cast<CreateFn>(
|
||||
GetProcAddress(module, "CrossDeskCreateWgcCapturer"));
|
||||
auto destroy_fn = reinterpret_cast<DestroyFn>(
|
||||
GetProcAddress(module, "CrossDeskDestroyWgcCapturer"));
|
||||
if (!create_fn || !destroy_fn) {
|
||||
FreeLibrary(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ScreenCapturer* impl = create_fn();
|
||||
if (!impl) {
|
||||
FreeLibrary(module);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::unique_ptr<ScreenCapturer>(
|
||||
new WgcPluginCapturer(module, impl, destroy_fn));
|
||||
}
|
||||
|
||||
~WgcPluginCapturer() override {
|
||||
destroy_fn_(impl_);
|
||||
FreeLibrary(module_);
|
||||
}
|
||||
|
||||
int Init(const int fps, cb_desktop_data cb) override {
|
||||
return impl_->Init(fps, [this, cb = std::move(cb)](
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
|
||||
bool embedded = false;
|
||||
if (frame_cursor_fn_) {
|
||||
embedded = frame_cursor_fn_();
|
||||
} else if (legacy_capture_cursor_.load(std::memory_order_relaxed)) {
|
||||
CURSORINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
embedded = GetCursorInfo(&info) && (info.flags & CURSOR_SHOWING) != 0;
|
||||
}
|
||||
// The plugin has its own globals. Bridge only this callback's metadata
|
||||
// into the executable before the wrapper accepts/publishes the frame.
|
||||
CapturedCursorFrameScope cursor_scope(embedded);
|
||||
cb(data, size, width, height, stream_id, native_frame);
|
||||
});
|
||||
}
|
||||
int Destroy() override { return impl_->Destroy(); }
|
||||
int Start(bool show_cursor) override {
|
||||
if (!frame_cursor_fn_) {
|
||||
legacy_capture_cursor_.store(show_cursor, std::memory_order_relaxed);
|
||||
}
|
||||
return impl_->Start(show_cursor);
|
||||
}
|
||||
int SetCursorCapture(bool enabled) {
|
||||
if (set_cursor_fn_) return set_cursor_fn_(impl_, enabled);
|
||||
// Older plugins only apply the cursor setting at startup.
|
||||
const int ret = impl_->Stop();
|
||||
return ret == 0 ? Start(enabled) : ret;
|
||||
}
|
||||
int Stop() override { return impl_->Stop(); }
|
||||
int Pause(int monitor_index) override {
|
||||
return impl_->Pause(monitor_index);
|
||||
}
|
||||
int Resume(int monitor_index) override {
|
||||
return impl_->Resume(monitor_index);
|
||||
}
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override {
|
||||
return impl_->GetDisplayInfoList();
|
||||
}
|
||||
int SwitchTo(int monitor_index) override {
|
||||
return impl_->SwitchTo(monitor_index);
|
||||
}
|
||||
int ResetToInitialMonitor() override {
|
||||
return impl_->ResetToInitialMonitor();
|
||||
}
|
||||
|
||||
private:
|
||||
WgcPluginCapturer(HMODULE module, ScreenCapturer* impl, DestroyFn destroy_fn)
|
||||
: module_(module), impl_(impl), destroy_fn_(destroy_fn) {
|
||||
set_cursor_fn_ = reinterpret_cast<SetCursorFn>(
|
||||
GetProcAddress(module, "CrossDeskSetWgcCursorCapture"));
|
||||
frame_cursor_fn_ = reinterpret_cast<FrameCursorFn>(
|
||||
GetProcAddress(module, "CrossDeskWgcFrameCapturesCursor"));
|
||||
// Use the legacy restart path unless both parts of the extension exist.
|
||||
if (!frame_cursor_fn_) set_cursor_fn_ = nullptr;
|
||||
}
|
||||
|
||||
// Create() validates these before constructing the wrapper.
|
||||
HMODULE module_;
|
||||
ScreenCapturer* impl_;
|
||||
DestroyFn destroy_fn_;
|
||||
SetCursorFn set_cursor_fn_ = nullptr;
|
||||
FrameCursorFn frame_cursor_fn_ = nullptr;
|
||||
std::atomic<bool> legacy_capture_cursor_{false};
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -193,8 +193,9 @@ int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
|
||||
capture_session_.StartCapture();
|
||||
is_running_ = true;
|
||||
return 0;
|
||||
} catch (const winrt::hresult_error&) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
} catch (const winrt::hresult_error& error) {
|
||||
LOG_ERROR("WGC: start capture failed, hr=0x{:08X}, thread={}",
|
||||
static_cast<uint32_t>(error.code().value), GetCurrentThreadId());
|
||||
return 86;
|
||||
} catch (...) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
@@ -296,9 +297,7 @@ void WgcSessionImpl::OnFrame(
|
||||
if (!d3d11_texture_mapped_ || is_new_size) {
|
||||
HRESULT tex_hr = CreateMappedTexture(frame_captured);
|
||||
if (FAILED(tex_hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
|
||||
.c_str());
|
||||
ReportFrameFailure(tex_hr);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -314,33 +313,38 @@ void WgcSessionImpl::OnFrame(
|
||||
,
|
||||
&map_result);
|
||||
if (FAILED(hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
||||
ReportFrameFailure(hr);
|
||||
return;
|
||||
}
|
||||
|
||||
// copy data from map_result.pData
|
||||
if (map_result.pData && observer_) {
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
const bool cursor_captured =
|
||||
last_show_cursor_ && GetCursorInfo(&cursor) && cursor.hCursor &&
|
||||
(cursor.flags & CURSOR_SHOWING) != 0 &&
|
||||
(target_.is_window ||
|
||||
MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) ==
|
||||
target_.hmonitor);
|
||||
observer_->OnFrame(
|
||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||
static_cast<unsigned int>(frame_size.Height),
|
||||
map_result.RowPitch,
|
||||
const_cast<const unsigned char*>(
|
||||
(unsigned char*)map_result.pData),
|
||||
cursor_captured},
|
||||
id_);
|
||||
{
|
||||
struct UnmapOnExit {
|
||||
ID3D11DeviceContext* context;
|
||||
ID3D11Texture2D* texture;
|
||||
~UnmapOnExit() { context->Unmap(texture, 0); }
|
||||
} unmap{d3d11_device_context_.get(), d3d11_texture_mapped_.get()};
|
||||
// copy data from map_result.pData
|
||||
if (map_result.pData && observer_) {
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
const bool cursor_captured =
|
||||
last_show_cursor_ && GetCursorInfo(&cursor) && cursor.hCursor &&
|
||||
(cursor.flags & CURSOR_SHOWING) != 0 &&
|
||||
(target_.is_window ||
|
||||
MonitorFromPoint(cursor.ptScreenPos, MONITOR_DEFAULTTONULL) ==
|
||||
target_.hmonitor);
|
||||
observer_->OnFrame(
|
||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||
static_cast<unsigned int>(frame_size.Height),
|
||||
map_result.RowPitch,
|
||||
const_cast<const unsigned char*>(
|
||||
(unsigned char*)map_result.pData),
|
||||
cursor_captured},
|
||||
id_);
|
||||
frame_failure_reported_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
||||
|
||||
if (is_new_size) {
|
||||
capture_framepool_.Recreate(
|
||||
d3d11_direct_device_,
|
||||
@@ -348,13 +352,22 @@ void WgcSessionImpl::OnFrame(
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, capture_frame_size_);
|
||||
}
|
||||
} catch (const winrt::hresult_error&) {
|
||||
LOG_WARN("WGC: frame processing failed");
|
||||
} catch (const winrt::hresult_error& error) {
|
||||
ReportFrameFailure(error.code());
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: frame processing failed");
|
||||
ReportFrameFailure(E_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
void WgcSessionImpl::ReportFrameFailure(HRESULT error) {
|
||||
if (frame_failure_reported_) return;
|
||||
frame_failure_reported_ = true;
|
||||
LOG_WARN("WGC: frame processing failed, hr=0x{:08X}", static_cast<uint32_t>(error));
|
||||
// Let both normal and secure capture advance to GDI rather than replay a
|
||||
// stale image indefinitely after a readback/device failure.
|
||||
if (observer_) observer_->OnFrame({}, id_);
|
||||
}
|
||||
|
||||
void WgcSessionImpl::OnClosed(
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||
winrt::Windows::Foundation::IInspectable const&) {
|
||||
@@ -409,8 +422,9 @@ int WgcSessionImpl::InitializeLocked() {
|
||||
GetDXGIInterfaceFromObject<ID3D11Device>(d3d11_direct_device_);
|
||||
d3d11_device->GetImmediateContext(d3d11_device_context_.put());
|
||||
|
||||
} catch (winrt::hresult_error) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
} catch (const winrt::hresult_error& error) {
|
||||
LOG_ERROR("WGC: create capture item failed, hr=0x{:08X}, thread={}",
|
||||
static_cast<uint32_t>(error.code().value), GetCurrentThreadId());
|
||||
return 86;
|
||||
} catch (...) {
|
||||
return 86;
|
||||
|
||||
@@ -69,6 +69,7 @@ class WgcSessionImpl : public WgcSession {
|
||||
winrt::Windows::Foundation::IInspectable const&);
|
||||
|
||||
int InitializeLocked();
|
||||
void ReportFrameFailure(HRESULT error);
|
||||
int StartLocked(bool show_cursor);
|
||||
int StartCaptureLocked(bool show_cursor);
|
||||
void StopLocked();
|
||||
@@ -84,6 +85,7 @@ class WgcSessionImpl : public WgcSession {
|
||||
bool is_running_ = false;
|
||||
bool is_paused_ = false;
|
||||
bool last_show_cursor_ = false;
|
||||
bool frame_failure_reported_ = false;
|
||||
|
||||
wgc_session_observer* observer_ = nullptr;
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "platform/windows/service/autostart_control.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
#include "config_center.h"
|
||||
#include "path_manager.h"
|
||||
#include "platform/single_instance.h"
|
||||
#include "platform/windows/autostart_task.h"
|
||||
#include "platform/windows/virtual_display/usbmmidd_virtual_display.h"
|
||||
#include "platform/autostart_backend.h"
|
||||
#include "service_host.h"
|
||||
#include "shared_identity.h"
|
||||
#include "rd_log.h"
|
||||
#include "unattended_config.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
struct ServiceHandle {
|
||||
SC_HANDLE value;
|
||||
~ServiceHandle() { if (value) CloseServiceHandle(value); }
|
||||
};
|
||||
|
||||
bool ReadServiceStartType(DWORD& start_type) {
|
||||
start_type = SERVICE_DEMAND_START;
|
||||
ServiceHandle manager{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
|
||||
if (!manager.value) return false;
|
||||
ServiceHandle service{OpenServiceW(manager.value, L"CrossDeskService", SERVICE_QUERY_CONFIG)};
|
||||
const DWORD open_error = service.value ? ERROR_SUCCESS : GetLastError();
|
||||
bool read = open_error == ERROR_SERVICE_DOES_NOT_EXIST;
|
||||
if (service.value) {
|
||||
alignas(QUERY_SERVICE_CONFIGW) BYTE buffer[8192]{};
|
||||
auto* config = reinterpret_cast<QUERY_SERVICE_CONFIGW*>(buffer);
|
||||
DWORD size = 0;
|
||||
read = QueryServiceConfigW(service.value, config, sizeof(buffer), &size) != FALSE;
|
||||
if (read) start_type = config->dwStartType;
|
||||
}
|
||||
return read;
|
||||
}
|
||||
|
||||
bool ConfigureServiceAutostart(bool enabled) {
|
||||
ServiceHandle manager{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
|
||||
if (!manager.value) return false;
|
||||
ServiceHandle service{OpenServiceW(manager.value, L"CrossDeskService", SERVICE_CHANGE_CONFIG)};
|
||||
if (!service.value) return !enabled && GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST;
|
||||
return ChangeServiceConfigW(service.value, SERVICE_NO_CHANGE,
|
||||
enabled ? SERVICE_AUTO_START : SERVICE_DEMAND_START, SERVICE_NO_CHANGE,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr) != FALSE;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool SupportsServiceAutostart() {
|
||||
return platform::kAutostartRequiresElevation;
|
||||
}
|
||||
|
||||
bool IsServiceAutostartEnabled() {
|
||||
if (!SupportsServiceAutostart() || !IsUnattendedEnabled() ||
|
||||
!IsSharedIdentityServiceConfigured() || !SharedHostIdentity::Configured()) return false;
|
||||
DWORD start_type = 0;
|
||||
return ReadServiceStartType(start_type) && start_type == SERVICE_AUTO_START;
|
||||
}
|
||||
|
||||
bool CleanupLegacyServiceAutostart() {
|
||||
return !SupportsServiceAutostart() || !IsServiceAutostartEnabled() ||
|
||||
platform::MigrateLegacyAutostart("CrossDesk");
|
||||
}
|
||||
|
||||
bool SetServiceAutostart(bool enabled, std::string& error) {
|
||||
error.clear();
|
||||
if (enabled && !SupportsServiceAutostart()) {
|
||||
error = "Unattended startup requires the installed Release build";
|
||||
return false;
|
||||
}
|
||||
if (!IsAdministratorProcess()) {
|
||||
error = "Administrator privileges are required to configure boot startup";
|
||||
return false;
|
||||
}
|
||||
// GUI and CLI use the same operation and command lease. No second GUI
|
||||
// executable is launched, and saving settings never stops a current service.
|
||||
platform::SingleInstanceGuard command;
|
||||
if (command.TryAcquire(platform::InstanceRole::kServiceCli, error) !=
|
||||
platform::InstanceResult::kAcquired) {
|
||||
if (error.empty()) error = "Another service configuration is in progress";
|
||||
return false;
|
||||
}
|
||||
DWORD start_type = 0;
|
||||
if (!ReadServiceStartType(start_type)) {
|
||||
error = "Could not query Windows service startup configuration";
|
||||
return false;
|
||||
}
|
||||
if (!enabled) {
|
||||
// Disarm the host without stopping the lock-screen service used by GUI
|
||||
// sessions. A missing service/binary must not prevent disabling startup.
|
||||
if (SetUnattendedEnabled(false) && ConfigureServiceAutostart(false) &&
|
||||
platform::RemoveInstalledAutostart("CrossDesk")) return true;
|
||||
error = "Could not disable unattended startup";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto executable = platform::GetAutostartExecutablePath();
|
||||
const auto service = std::filesystem::u8path(executable).parent_path() / L"crossdesk_service.exe";
|
||||
std::error_code file_error;
|
||||
if (executable.empty() || !std::filesystem::is_regular_file(service, file_error) ||
|
||||
!std::filesystem::is_regular_file(service.parent_path() / L"crossdesk_session_helper.exe", file_error)) {
|
||||
error = "The Windows service or session helper is missing. Repair the installation";
|
||||
return false;
|
||||
}
|
||||
platform::SingleInstanceGuard bootstrap_client;
|
||||
if (!SharedHostIdentity::GuiPresent() &&
|
||||
bootstrap_client.TryAcquire(platform::InstanceRole::kClient, error) !=
|
||||
platform::InstanceResult::kAcquired) {
|
||||
error = "Enable boot startup from the running GUI settings, or close the GUI first";
|
||||
return false;
|
||||
}
|
||||
const bool current = IsSharedIdentityServiceConfigured() &&
|
||||
QueryCrossDeskServiceRegistration(service.wstring()) == ServiceRegistrationState::current;
|
||||
if (!current && IsCrossDeskServiceInstalled() && !StopCrossDeskService(20000)) {
|
||||
error = "Could not stop the old service to configure shared identity";
|
||||
return false;
|
||||
}
|
||||
PathManager paths("CrossDesk");
|
||||
ResolveVirtualDisplayMode(); // Persist the console mode while enabling boot startup.
|
||||
ConfigCenter source((paths.GetCachePath() / "config.ini").string());
|
||||
if (!SharedHostIdentity::Prepare(paths.GetCachePath(), paths.GetConfigPath() / "trusted-devices",
|
||||
source.ExportSharedHostConfig(), error)) return false;
|
||||
const bool was_enabled = IsUnattendedEnabled();
|
||||
if (SetUnattendedEnabled(true) &&
|
||||
((current && start_type == SERVICE_AUTO_START) || InstallCrossDeskService(service.wstring())) &&
|
||||
StartCrossDeskService() && platform::EnableAutostart("CrossDesk", executable)) return true;
|
||||
|
||||
const bool restored_flag = SetUnattendedEnabled(was_enabled);
|
||||
const bool restored_start = ConfigureServiceAutostart(start_type == SERVICE_AUTO_START);
|
||||
if (!restored_flag || !restored_start)
|
||||
LOG_ERROR("Could not restore the previous unattended startup configuration");
|
||||
error = "Could not enable persistent host startup and the user-session GUI";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-08
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _AUTOSTART_CONTROL_H_
|
||||
#define _AUTOSTART_CONTROL_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Windows startup preferences control the machine host and its GUI handoff.
|
||||
// Portable and Debug builds keep the existing unattended-mode restriction.
|
||||
bool SupportsServiceAutostart();
|
||||
bool IsServiceAutostartEnabled();
|
||||
// Shared GUI/CLI entry point; serializes service changes and preserves a
|
||||
// running host when the installed service already matches this executable.
|
||||
bool SetServiceAutostart(bool enabled, std::string& error);
|
||||
|
||||
// Keep a legacy Run entry from bypassing the elevated GUI handoff task.
|
||||
bool CleanupLegacyServiceAutostart();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,274 @@
|
||||
#include "local_host_channel.h"
|
||||
|
||||
#include <sddl.h>
|
||||
#include <wtsapi32.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
constexpr size_t kMaxPacket = 1024 * 1024;
|
||||
constexpr size_t kMaxQueue = 8 * kMaxPacket;
|
||||
struct Handle {
|
||||
HANDLE value = INVALID_HANDLE_VALUE;
|
||||
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
|
||||
};
|
||||
std::wstring ImagePath(HANDLE process) {
|
||||
std::wstring path(32768, L'\0');
|
||||
DWORD size = static_cast<DWORD>(path.size());
|
||||
if (!QueryFullProcessImageNameW(process, 0, path.data(), &size)) return {};
|
||||
path.resize(size);
|
||||
return path;
|
||||
}
|
||||
std::vector<uint8_t> User(HANDLE token) {
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
|
||||
std::vector<uint8_t> result(size);
|
||||
if (!size || !GetTokenInformation(token, TokenUser, result.data(), size, &size))
|
||||
result.clear();
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
LocalHostChannel::LocalHostChannel(bool server) : server_(server) {
|
||||
ProcessIdToSessionId(GetCurrentProcessId(), &session_);
|
||||
stop_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (stop_ && session_) worker_ = std::thread([this] { Run(); });
|
||||
}
|
||||
LocalHostChannel::~LocalHostChannel() {
|
||||
if (stop_) SetEvent(stop_);
|
||||
if (worker_.joinable()) worker_.join();
|
||||
if (stop_) CloseHandle(stop_);
|
||||
}
|
||||
|
||||
bool LocalHostChannel::Authenticate(HANDLE pipe, std::string& reason) const {
|
||||
reason = "pipe peer PID unavailable";
|
||||
ULONG pid = 0;
|
||||
if (!(server_ ? GetNamedPipeClientProcessId(pipe, &pid)
|
||||
: GetNamedPipeServerProcessId(pipe, &pid))) return false;
|
||||
DWORD session = 0;
|
||||
reason = "pipe peer belongs to another session";
|
||||
if (!ProcessIdToSessionId(pid, &session) || session != session_) return false;
|
||||
Handle process{OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, pid)};
|
||||
const auto expected = ImagePath(GetCurrentProcess());
|
||||
reason = "pipe peer executable does not match this installation";
|
||||
if (!process.value || expected.empty() ||
|
||||
_wcsicmp(expected.c_str(), ImagePath(process.value).c_str())) return false;
|
||||
Handle token;
|
||||
reason = "pipe peer token cannot be queried";
|
||||
if (!OpenProcessToken(process.value, TOKEN_QUERY, &token.value)) return false;
|
||||
const auto user = User(token.value);
|
||||
if (user.empty()) return false;
|
||||
const auto sid = reinterpret_cast<const TOKEN_USER*>(user.data())->User.Sid;
|
||||
if (!server_) {
|
||||
reason = "pipe server is not SYSTEM";
|
||||
return IsWellKnownSid(sid, WinLocalSystemSid);
|
||||
}
|
||||
// A different elevated user must not attach to this console user's files,
|
||||
// clipboard, or host controls. The pipe DACL also excludes non-admin callers.
|
||||
Handle console_token;
|
||||
reason = "console user token unavailable";
|
||||
if (!WTSQueryUserToken(session_, &console_token.value)) return false;
|
||||
const auto console_user = User(console_token.value);
|
||||
reason = "pipe client is not the console user";
|
||||
return !console_user.empty() && EqualSid(sid,
|
||||
reinterpret_cast<const TOKEN_USER*>(console_user.data())->User.Sid);
|
||||
}
|
||||
|
||||
bool LocalHostChannel::Read(HANDLE pipe, HANDLE event, void* data, DWORD size) {
|
||||
if (!ResetEvent(event)) return false;
|
||||
OVERLAPPED operation{};
|
||||
operation.hEvent = event;
|
||||
DWORD completed = 0;
|
||||
const BOOL ok = ReadFile(pipe, data, size, &completed, &operation);
|
||||
if (!ok) {
|
||||
if (GetLastError() != ERROR_IO_PENDING) return false;
|
||||
HANDLE waits[] = {stop_, event};
|
||||
if (WaitForMultipleObjects(2, waits, FALSE, 1000) != WAIT_OBJECT_0 + 1) {
|
||||
CancelIoEx(pipe, &operation);
|
||||
GetOverlappedResult(pipe, &operation, &completed, TRUE);
|
||||
return false;
|
||||
}
|
||||
if (!GetOverlappedResult(pipe, &operation, &completed, FALSE)) return false;
|
||||
}
|
||||
return completed == size;
|
||||
}
|
||||
|
||||
bool LocalHostChannel::Send(const nlohmann::json& message) {
|
||||
if (!connected_) return false;
|
||||
auto bytes = nlohmann::json::to_cbor(message);
|
||||
if (bytes.empty() || bytes.size() > kMaxPacket) return false;
|
||||
const uint32_t size = static_cast<uint32_t>(bytes.size());
|
||||
bytes.insert(bytes.begin(), sizeof(size), 0);
|
||||
std::memcpy(bytes.data(), &size, sizeof(size));
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!connected_ || outgoing_bytes_ + bytes.size() > kMaxQueue) return false;
|
||||
outgoing_bytes_ += bytes.size();
|
||||
outgoing_.push_back(std::move(bytes));
|
||||
return true;
|
||||
}
|
||||
void LocalHostChannel::Reconnect() {
|
||||
std::lock_guard lock(mutex_);
|
||||
connected_ = false;
|
||||
}
|
||||
std::vector<nlohmann::json> LocalHostChannel::Receive() {
|
||||
std::lock_guard lock(mutex_);
|
||||
std::vector<nlohmann::json> result;
|
||||
result.swap(incoming_);
|
||||
incoming_bytes_ = 0;
|
||||
return result;
|
||||
}
|
||||
bool LocalHostChannel::QueueIncoming(nlohmann::json message, size_t size) {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (incoming_bytes_ + size > kMaxQueue) return false;
|
||||
incoming_bytes_ += size;
|
||||
incoming_.push_back(std::move(message));
|
||||
return true;
|
||||
}
|
||||
void LocalHostChannel::SetConnected(bool connected) {
|
||||
std::lock_guard lock(mutex_);
|
||||
connected_ = connected;
|
||||
outgoing_.clear();
|
||||
outgoing_bytes_ = 0;
|
||||
if (!connected) {
|
||||
incoming_.clear();
|
||||
incoming_bytes_ = 0;
|
||||
}
|
||||
incoming_.push_back({{"op", connected ? "attached" : "detached"}});
|
||||
}
|
||||
|
||||
void LocalHostChannel::Run() {
|
||||
const auto name = L"\\\\.\\pipe\\CrossDesk.Host." + std::to_wstring(session_);
|
||||
std::string last_auth_error;
|
||||
while (WaitForSingleObject(stop_, 0) == WAIT_TIMEOUT) {
|
||||
Handle pipe;
|
||||
if (server_) {
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
L"D:P(A;;GA;;;SY)(A;;GA;;;BA)", SDDL_REVISION_1,
|
||||
&descriptor, nullptr)) return;
|
||||
SECURITY_ATTRIBUTES security{sizeof(security), descriptor, FALSE};
|
||||
pipe.value = CreateNamedPipeW(name.c_str(), PIPE_ACCESS_DUPLEX |
|
||||
FILE_FLAG_OVERLAPPED | FILE_FLAG_FIRST_PIPE_INSTANCE,
|
||||
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT | PIPE_REJECT_REMOTE_CLIENTS,
|
||||
1, 65536, 65536, 0, &security);
|
||||
LocalFree(descriptor);
|
||||
if (pipe.value != INVALID_HANDLE_VALUE) {
|
||||
Handle event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
||||
OVERLAPPED connection{};
|
||||
connection.hEvent = event.value;
|
||||
const BOOL accepted = ConnectNamedPipe(pipe.value, &connection);
|
||||
const auto error = accepted ? ERROR_SUCCESS : GetLastError();
|
||||
if (error == ERROR_IO_PENDING) {
|
||||
HANDLE waits[] = {stop_, event.value};
|
||||
if (WaitForMultipleObjects(2, waits, FALSE, INFINITE) != WAIT_OBJECT_0 + 1) {
|
||||
CancelIoEx(pipe.value, &connection);
|
||||
DWORD unused = 0;
|
||||
GetOverlappedResult(pipe.value, &connection, &unused, TRUE);
|
||||
return;
|
||||
}
|
||||
} else if (error != ERROR_SUCCESS && error != ERROR_PIPE_CONNECTED) {
|
||||
WaitForSingleObject(stop_, 250);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pipe.value = CreateFileW(name.c_str(), GENERIC_READ | GENERIC_WRITE,
|
||||
0, nullptr, OPEN_EXISTING, FILE_FLAG_OVERLAPPED |
|
||||
SECURITY_SQOS_PRESENT | SECURITY_IDENTIFICATION, nullptr);
|
||||
}
|
||||
if (pipe.value == INVALID_HANDLE_VALUE) {
|
||||
WaitForSingleObject(stop_, 250);
|
||||
continue;
|
||||
}
|
||||
std::string auth_error;
|
||||
if (!Authenticate(pipe.value, auth_error)) {
|
||||
if (auth_error != last_auth_error)
|
||||
LOG_WARN("Local host channel authentication rejected: {}", auth_error);
|
||||
last_auth_error = auth_error;
|
||||
WaitForSingleObject(stop_, 250);
|
||||
continue;
|
||||
}
|
||||
last_auth_error.clear();
|
||||
Handle read_event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
||||
Handle write_event{CreateEventW(nullptr, TRUE, FALSE, nullptr)};
|
||||
if (!read_event.value || !write_event.value) return;
|
||||
SetConnected(true);
|
||||
std::vector<uint8_t> input;
|
||||
std::vector<uint8_t> output;
|
||||
OVERLAPPED writing{};
|
||||
writing.hEvent = write_event.value;
|
||||
bool write_pending = false;
|
||||
ULONGLONG write_started = 0;
|
||||
while (connected_ && WaitForSingleObject(stop_, 2) == WAIT_TIMEOUT) {
|
||||
if (write_pending) {
|
||||
DWORD completed = 0;
|
||||
if (GetOverlappedResult(pipe.value, &writing, &completed, FALSE)) {
|
||||
if (completed != output.size()) break;
|
||||
write_pending = false;
|
||||
output.clear();
|
||||
} else if (GetLastError() != ERROR_IO_INCOMPLETE ||
|
||||
GetTickCount64() - write_started > 5000) break;
|
||||
}
|
||||
if (!write_pending) {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (!outgoing_.empty()) {
|
||||
output = std::move(outgoing_.front());
|
||||
outgoing_.pop_front();
|
||||
outgoing_bytes_ -= output.size();
|
||||
}
|
||||
}
|
||||
if (!write_pending && !output.empty()) {
|
||||
ResetEvent(write_event.value);
|
||||
DWORD completed = 0;
|
||||
if (WriteFile(pipe.value, output.data(), static_cast<DWORD>(output.size()),
|
||||
&completed, &writing)) {
|
||||
if (completed != output.size()) break;
|
||||
output.clear();
|
||||
} else {
|
||||
if (GetLastError() != ERROR_IO_PENDING) break;
|
||||
write_pending = true;
|
||||
write_started = GetTickCount64();
|
||||
}
|
||||
}
|
||||
// Drain the other direction even while a large write is waiting. Both
|
||||
// processes may be sending files at once; blocking writes would deadlock.
|
||||
DWORD available = 0;
|
||||
if (!PeekNamedPipe(pipe.value, nullptr, 0, nullptr, &available, nullptr)) break;
|
||||
if (!available) continue;
|
||||
const DWORD count = std::min<DWORD>(available, 65536);
|
||||
const auto offset = input.size();
|
||||
input.resize(offset + count);
|
||||
if (!Read(pipe.value, read_event.value, input.data() + offset, count)) break;
|
||||
bool valid = true;
|
||||
size_t consumed = 0;
|
||||
while (input.size() - consumed >= sizeof(uint32_t)) {
|
||||
uint32_t size = 0;
|
||||
std::memcpy(&size, input.data() + consumed, sizeof(size));
|
||||
if (!size || size > kMaxPacket) { valid = false; break; }
|
||||
if (input.size() - consumed < sizeof(size) + size) break;
|
||||
auto message = nlohmann::json::from_cbor(
|
||||
input.begin() + consumed + sizeof(size),
|
||||
input.begin() + consumed + sizeof(size) + size, true, false);
|
||||
if (!message.is_object() || !QueueIncoming(std::move(message), size)) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
consumed += sizeof(size) + size;
|
||||
}
|
||||
if (!valid) break;
|
||||
input.erase(input.begin(), input.begin() + consumed);
|
||||
}
|
||||
if (write_pending) {
|
||||
CancelIoEx(pipe.value, &writing);
|
||||
DWORD completed = 0;
|
||||
GetOverlappedResult(pipe.value, &writing, &completed, TRUE);
|
||||
}
|
||||
SetConnected(false);
|
||||
if (server_) DisconnectNamedPipe(pipe.value);
|
||||
}
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _LOCAL_HOST_CHANNEL_H_
|
||||
#define _LOCAL_HOST_CHANNEL_H_
|
||||
|
||||
#include <windows.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Authenticated, local-only link between the persistent SYSTEM host and the
|
||||
// console user's GUI. Losing this link never tears down a remote RTC session.
|
||||
// Application messages are consumed on the application's main thread.
|
||||
class LocalHostChannel {
|
||||
public:
|
||||
explicit LocalHostChannel(bool server);
|
||||
~LocalHostChannel();
|
||||
bool Send(const nlohmann::json& message);
|
||||
// Fail pending user-session operations and reconnect without closing RTC.
|
||||
void Reconnect();
|
||||
std::vector<nlohmann::json> Receive();
|
||||
|
||||
private:
|
||||
void Run();
|
||||
bool Authenticate(HANDLE pipe, std::string& reason) const;
|
||||
bool Read(HANDLE pipe, HANDLE event, void* bytes, DWORD size);
|
||||
bool QueueIncoming(nlohmann::json message, size_t size);
|
||||
void SetConnected(bool connected);
|
||||
const bool server_;
|
||||
DWORD session_ = 0;
|
||||
HANDLE stop_ = nullptr;
|
||||
std::thread worker_;
|
||||
std::atomic<bool> connected_{false};
|
||||
std::mutex mutex_;
|
||||
std::deque<std::vector<uint8_t>> outgoing_;
|
||||
std::vector<nlohmann::json> incoming_;
|
||||
size_t outgoing_bytes_ = 0, incoming_bytes_ = 0;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -27,6 +27,7 @@
|
||||
#include "secure_text_input.h"
|
||||
#include "unattended_config.h"
|
||||
#include "unattended_policy.h"
|
||||
#include "shared_identity.h"
|
||||
#include "rdp_session_handoff.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -991,6 +992,8 @@ int CrossDeskServiceHost::RunInConsole() {
|
||||
}
|
||||
|
||||
int CrossDeskServiceHost::InitializeRuntime() {
|
||||
if (IsUnattendedEnabled() && !PrepareUnattendedDirectoryForService())
|
||||
LOG_ERROR("Cannot prepare the protected machine identity directory");
|
||||
stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (stop_event_ == nullptr) {
|
||||
DWORD error = GetLastError();
|
||||
@@ -1177,7 +1180,8 @@ void CrossDeskServiceHost::ClientProcessMonitorLoop() {
|
||||
}
|
||||
const DWORD console_session = WTSGetActiveConsoleSessionId();
|
||||
const auto action =
|
||||
UnattendedAction(enabled, console_session,
|
||||
UnattendedAction((enabled || SharedHostIdentity::GuiPresent()) &&
|
||||
SharedHostIdentity::Configured(), console_session,
|
||||
unattended_process_ != nullptr, unattended_session_);
|
||||
if (action == UnattendedProcessAction::stop) StopUnattendedHost();
|
||||
if (action == UnattendedProcessAction::start) {
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
#include "session_helper_shared.h"
|
||||
#include "secure_text_input.h"
|
||||
#include "secure_desktop_dxgi_capture.h"
|
||||
#include "secure_desktop_wgc_capture.h"
|
||||
#include "capture_backend_policy.h"
|
||||
#include "secure_desktop_frame_pacer.h"
|
||||
#include "secure_desktop_nv12_cache.h"
|
||||
#include "service_lifetime.h"
|
||||
@@ -40,10 +42,12 @@
|
||||
#include "usbmmidd_virtual_display.h"
|
||||
#include "windows_cursor_state.h"
|
||||
#include "cursor_draw.h"
|
||||
#include "dxgi_cursor_state.h"
|
||||
#include "windows_input_marker.h"
|
||||
#include "windows_text_input.h"
|
||||
#include "windows_key_metadata.h"
|
||||
#include "../windows_thread_dpi.h"
|
||||
#include "../privacy/privacy_cursor_guard.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -53,6 +57,7 @@ using crossdesk::ScopedInteractiveDesktop;
|
||||
using Json = nlohmann::json;
|
||||
|
||||
constexpr DWORD kSessionHelperStatePollMs = 1000;
|
||||
constexpr ULONGLONG kCaptureOwnerLeaseMs = 4000;
|
||||
|
||||
std::atomic<HANDLE> g_session_helper_desktop_switch_event{nullptr};
|
||||
|
||||
@@ -81,6 +86,8 @@ struct HelperState {
|
||||
struct SecureCaptureRequest : crossdesk::SharedCaptureConfiguration {
|
||||
std::string interactive_stage;
|
||||
std::string interactive_desktop;
|
||||
std::shared_ptr<void> owner_process;
|
||||
DWORD owner_pid = 0;
|
||||
};
|
||||
|
||||
struct SecureMouseRequest {
|
||||
@@ -99,6 +106,7 @@ struct SecureSharedCaptureState {
|
||||
HANDLE frame_ready_event = nullptr;
|
||||
uint8_t* frame_view = nullptr;
|
||||
uint32_t sequence = 0;
|
||||
std::atomic<ULONGLONG> last_owner_refresh{0};
|
||||
// Last member: joins/cleans the producer before any captured state is destroyed.
|
||||
crossdesk::SharedCaptureLifecycle lifecycle;
|
||||
};
|
||||
@@ -1363,9 +1371,12 @@ std::vector<uint8_t> BuildTextResponseBytes(const std::string& response) {
|
||||
}
|
||||
|
||||
// Own the desktop before its capture objects, and release those objects before
|
||||
// restoring the original desktop. The legacy pipe path remains GDI-only.
|
||||
// restoring the original desktop. Shared and pipe capture use the same order.
|
||||
struct SecureDesktopCaptureResources {
|
||||
ScopedInteractiveDesktop desktop;
|
||||
// Destroy before releasing the bound desktop, after capture objects.
|
||||
std::unique_ptr<crossdesk::PrivacyCursorGuard> cursor_guard;
|
||||
bool cursor_hidden = false;
|
||||
HDC screen_dc = nullptr;
|
||||
HDC mem_dc = nullptr;
|
||||
HBITMAP dib = nullptr;
|
||||
@@ -1375,12 +1386,15 @@ struct SecureDesktopCaptureResources {
|
||||
DWORD error_code = ERROR_SUCCESS;
|
||||
crossdesk::SecureDesktopCursorSnapshot cursor_snapshot{};
|
||||
std::unique_ptr<crossdesk::SecureDesktopDxgiCapture> dxgi;
|
||||
ULONGLONG next_dxgi_attempt = 0;
|
||||
ULONGLONG dxgi_started = 0;
|
||||
std::unique_ptr<crossdesk::SecureDesktopWgcCapture> wgc;
|
||||
ULONGLONG next_gpu_attempt = 0, dxgi_started = 0, wgc_started = 0;
|
||||
bool wgc_attempted = false;
|
||||
bool using_dxgi = false;
|
||||
bool using_wgc = false;
|
||||
bool frame_pending = false;
|
||||
const uint8_t* frame_bits = nullptr;
|
||||
int frame_stride = 0;
|
||||
const std::vector<uint8_t>* frame_nv12 = nullptr;
|
||||
crossdesk::SecureDesktopNv12Cache nv12;
|
||||
|
||||
~SecureDesktopCaptureResources() {
|
||||
@@ -1399,7 +1413,7 @@ struct SecureDesktopCaptureResources {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Initialize(const SecureCaptureRequest& request) {
|
||||
bool Initialize(const SecureCaptureRequest& request, bool shared = false) {
|
||||
const auto fallback = DesktopNameForInteractiveStage(
|
||||
request.interactive_stage, request.interactive_desktop);
|
||||
if (!desktop.Bind(fallback.empty() ? L"Winlogon" : fallback)) {
|
||||
@@ -1409,6 +1423,19 @@ struct SecureDesktopCaptureResources {
|
||||
SetLastError(ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION);
|
||||
return Fail("inactive_capture_desktop");
|
||||
}
|
||||
if (shared && !request.show_cursor && request.owner_process) {
|
||||
cursor_guard = std::make_unique<crossdesk::PrivacyCursorGuard>();
|
||||
std::string cursor_error;
|
||||
cursor_hidden = cursor_guard->Start(cursor_error,
|
||||
L"winsta0\\" + desktop.name(), request.owner_process.get());
|
||||
if (cursor_hidden) {
|
||||
LOG_INFO("Secure capture: system cursor hidden on '{}'; native controller draws it",
|
||||
WideToUtf8(desktop.name()));
|
||||
} else {
|
||||
LOG_WARN("Secure capture: cursor exclusion unavailable: {}", cursor_error);
|
||||
cursor_guard.reset();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1434,48 +1461,101 @@ struct SecureDesktopCaptureResources {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Capture(const SecureCaptureRequest& request, bool allow_dxgi = false) {
|
||||
bool Capture(const SecureCaptureRequest& request, bool wait_for_first_frame = false) {
|
||||
frame_pending = false;
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (allow_dxgi && !dxgi && now >= next_dxgi_attempt) {
|
||||
next_dxgi_attempt = now + 5000;
|
||||
auto candidate = std::make_unique<crossdesk::SecureDesktopDxgiCapture>();
|
||||
const HRESULT hr = candidate->Initialize(
|
||||
request.left, request.top, request.width, request.height);
|
||||
if (SUCCEEDED(hr)) {
|
||||
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));
|
||||
if (cursor_hidden) {
|
||||
std::string cursor_error;
|
||||
if (!cursor_guard->Healthy(cursor_error)) {
|
||||
cursor_hidden = false;
|
||||
cursor_guard.reset();
|
||||
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;
|
||||
wgc.reset();
|
||||
next_gpu_attempt = 0;
|
||||
wgc_attempted = false;
|
||||
LOG_WARN("Secure capture: cursor helper stopped; restoring captured cursor: {}",
|
||||
cursor_error);
|
||||
}
|
||||
}
|
||||
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");
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
const bool retry_gpu = now >= next_gpu_attempt;
|
||||
if (retry_gpu) {
|
||||
next_gpu_attempt = now + 5000;
|
||||
wgc_attempted = false;
|
||||
}
|
||||
using_dxgi = using_wgc = false;
|
||||
frame_nv12 = nullptr;
|
||||
frame_bits = nullptr;
|
||||
for (const auto backend : crossdesk::WindowsCaptureBackendOrder()) {
|
||||
if (backend == crossdesk::ScreenCaptureMethod::Dxgi) {
|
||||
if (!dxgi && retry_gpu) {
|
||||
auto candidate = std::make_unique<crossdesk::SecureDesktopDxgiCapture>();
|
||||
const HRESULT result = candidate->Initialize(request.left, request.top,
|
||||
request.width, request.height);
|
||||
if (SUCCEEDED(result)) {
|
||||
dxgi = std::move(candidate);
|
||||
dxgi_started = GetTickCount64();
|
||||
} else LOG_INFO("Secure capture: DXGI unavailable, trying WGC, hr=0x{:08X}",
|
||||
static_cast<uint32_t>(result));
|
||||
}
|
||||
if (!dxgi) continue;
|
||||
const HRESULT result = dxgi->Capture();
|
||||
if (result == S_OK) {
|
||||
using_dxgi = true;
|
||||
wgc.reset();
|
||||
wgc_attempted = false;
|
||||
frame_bits = dxgi->pixels();
|
||||
frame_stride = dxgi->stride();
|
||||
break;
|
||||
}
|
||||
if (result == S_FALSE && GetTickCount64() - dxgi_started < 250) {
|
||||
// Keep an already usable fallback while the preferred GPU warms up.
|
||||
if (wait_for_first_frame && !wgc && !bits) {
|
||||
frame_pending = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Secure capture: DXGI lost, trying WGC, hr=0x{:08X}", static_cast<uint32_t>(result));
|
||||
dxgi.reset();
|
||||
next_gpu_attempt = now + 5000;
|
||||
}
|
||||
} else if (backend == crossdesk::ScreenCaptureMethod::Wgc) {
|
||||
if (!wgc && !wgc_attempted) {
|
||||
wgc_attempted = true;
|
||||
auto candidate = std::make_unique<crossdesk::SecureDesktopWgcCapture>();
|
||||
const int result = candidate->Initialize(request.left, request.top, request.width,
|
||||
request.height, request.fps, request.show_cursor);
|
||||
if (result == 0) {
|
||||
wgc = std::move(candidate);
|
||||
wgc_started = GetTickCount64();
|
||||
LOG_INFO("Secure capture: WGC started");
|
||||
} else LOG_INFO("Secure capture: WGC unavailable, trying GDI, ret={}", result);
|
||||
}
|
||||
if (!wgc) continue;
|
||||
const HRESULT result = wgc->Capture();
|
||||
if (result == S_OK) {
|
||||
using_wgc = true;
|
||||
frame_nv12 = &wgc->pixels();
|
||||
break;
|
||||
}
|
||||
if (result == S_FALSE && GetTickCount64() - wgc_started < 250) {
|
||||
if (wait_for_first_frame && !bits) {
|
||||
frame_pending = true;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
LOG_INFO("Secure capture: WGC lost, trying GDI, hr=0x{:08X}", static_cast<uint32_t>(result));
|
||||
wgc.reset();
|
||||
next_gpu_attempt = now + 5000;
|
||||
}
|
||||
} else {
|
||||
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 = static_cast<const uint8_t*>(bits);
|
||||
frame_stride = request.width * 4;
|
||||
}
|
||||
}
|
||||
frame_bits = using_dxgi ? dxgi->pixels() : static_cast<const uint8_t*>(bits);
|
||||
frame_stride = using_dxgi ? dxgi->stride() : request.width * 4;
|
||||
// Sample on the thread bound to the captured input desktop, including when
|
||||
// native controllers render the cursor themselves (show_cursor == false).
|
||||
cursor_snapshot = {};
|
||||
@@ -1484,7 +1564,12 @@ struct SecureDesktopCaptureResources {
|
||||
if (GetCursorInfo(&cursor)) {
|
||||
static thread_local crossdesk::WindowsCursorState cursor_state;
|
||||
crossdesk::CursorState state{};
|
||||
const bool embedded = using_dxgi && dxgi->CursorEmbedded(cursor);
|
||||
// MagShowSystemCursor hides the pointer without clearing CURSOR_SHOWING.
|
||||
// A successfully hidden pointer is separate, regardless of the driver's
|
||||
// usual software-cursor default on Winlogon.
|
||||
const bool embedded = using_wgc ? wgc->CursorEmbedded()
|
||||
: !(cursor_hidden || crossdesk::SharedDxgiCursorState().PrivacyCursorHidden()) &&
|
||||
using_dxgi && dxgi->CursorEmbedded(cursor);
|
||||
cursor_state.Sample(cursor, embedded, &state);
|
||||
cursor_snapshot.valid = 1;
|
||||
cursor_snapshot.visible = state.visible;
|
||||
@@ -1495,14 +1580,22 @@ struct SecureDesktopCaptureResources {
|
||||
cursor_snapshot.y = cursor.ptScreenPos.y;
|
||||
// Never draw into DXGI's cached desktop: each cursor position starts
|
||||
// with clean pixels, including on a static screen or after a toggle.
|
||||
if (request.show_cursor && !embedded && using_dxgi &&
|
||||
if (request.show_cursor && !embedded && (using_dxgi || using_wgc) &&
|
||||
(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);
|
||||
if (frame_nv12) {
|
||||
const auto* pixels = frame_nv12->data();
|
||||
if (libyuv::NV12ToARGB(pixels, request.width,
|
||||
pixels + static_cast<size_t>(request.width) * request.height,
|
||||
request.width, static_cast<uint8_t*>(bits), request.width * 4,
|
||||
request.width, request.height) != 0)
|
||||
return Fail("nv12_to_argb_failed");
|
||||
frame_nv12 = nullptr;
|
||||
} else {
|
||||
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;
|
||||
@@ -1517,7 +1610,7 @@ struct SecureDesktopCaptureResources {
|
||||
}
|
||||
}
|
||||
// Complete GDI writes before libyuv reads the DIB's pixel memory.
|
||||
if (!using_dxgi || frame_bits == bits) GdiFlush();
|
||||
if ((!using_dxgi && !using_wgc) || (bits && frame_bits == bits)) GdiFlush();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -1537,7 +1630,9 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
// 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 = capture.frame_nv12
|
||||
? (std::memcpy(payload, capture.frame_nv12->data(), nv12_size), 0)
|
||||
: libyuv::ARGBToNV12(
|
||||
capture.frame_bits, capture.frame_stride, payload, request.width,
|
||||
payload + static_cast<size_t>(request.width) * request.height,
|
||||
request.width, request.width, request.height);
|
||||
@@ -1578,6 +1673,8 @@ void CloseSecureDesktopSharedCaptureResources(
|
||||
capture_state->frame_mapping = nullptr;
|
||||
}
|
||||
capture_state->sequence = 0;
|
||||
capture_state->request = {};
|
||||
capture_state->last_owner_refresh.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
@@ -1599,7 +1696,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
std::unique_ptr<SecureDesktopCaptureResources> capture;
|
||||
ULONGLONG last_error_tick = 0;
|
||||
auto stats_started = std::chrono::steady_clock::now();
|
||||
unsigned frame_count = 0, dxgi_frames = 0, dxgi_updates = 0;
|
||||
unsigned frame_count = 0, dxgi_frames = 0, dxgi_updates = 0, wgc_frames = 0;
|
||||
unsigned converted_frames = 0;
|
||||
double capture_ms = 0, convert_ms = 0, publish_ms = 0;
|
||||
auto report_stats = [&](bool force) {
|
||||
@@ -1611,19 +1708,29 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
LOG_INFO(
|
||||
"Secure capture producer: fps={:.1f}, capture_avg_ms={:.1f}, "
|
||||
"convert_avg_ms={:.1f}, publish_avg_ms={:.1f}, size={}x{}, "
|
||||
"target_fps={}, dxgi_frames={}, dxgi_updates={}, gdi_frames={}, "
|
||||
"target_fps={}, dxgi_frames={}, dxgi_updates={}, wgc_frames={}, gdi_frames={}, "
|
||||
"converted_frames={}, reused_frames={}",
|
||||
frame_count / elapsed, capture_ms / frame_count,
|
||||
convert_ms / frame_count, publish_ms / frame_count, request.width,
|
||||
request.height, request.fps, dxgi_frames, dxgi_updates,
|
||||
frame_count - dxgi_frames, converted_frames,
|
||||
request.height, request.fps, dxgi_frames, dxgi_updates, wgc_frames,
|
||||
frame_count - dxgi_frames - wgc_frames, converted_frames,
|
||||
frame_count - converted_frames);
|
||||
}
|
||||
stats_started = std::chrono::steady_clock::now();
|
||||
frame_count = dxgi_frames = dxgi_updates = converted_frames = 0;
|
||||
frame_count = dxgi_frames = dxgi_updates = wgc_frames = converted_frames = 0;
|
||||
capture_ms = convert_ms = publish_ms = 0;
|
||||
};
|
||||
while (!capture_state->lifecycle.StopRequested()) {
|
||||
if (request.owner_process &&
|
||||
WaitForSingleObject(request.owner_process.get(), 0) != WAIT_TIMEOUT) {
|
||||
LOG_INFO("Secure capture owner exited; stopping capture and restoring cursor");
|
||||
break;
|
||||
}
|
||||
if (GetTickCount64() - capture_state->last_owner_refresh.load(
|
||||
std::memory_order_relaxed) >= kCaptureOwnerLeaseMs) {
|
||||
LOG_INFO("Secure capture lease expired; restoring cursor");
|
||||
break;
|
||||
}
|
||||
const auto frame_started = std::chrono::steady_clock::now();
|
||||
report_stats(false);
|
||||
// A lock/credential/UAC transition can change the desktop before the
|
||||
@@ -1633,7 +1740,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
}
|
||||
if (!capture) {
|
||||
capture = std::make_unique<SecureDesktopCaptureResources>();
|
||||
if (!capture->Initialize(request)) {
|
||||
if (!capture->Initialize(request, true)) {
|
||||
const DWORD error = capture->error_code;
|
||||
capture.reset();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
@@ -1657,9 +1764,10 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
// or pointer-only update can reuse the NV12 image safely.
|
||||
const bool pixels_changed = !capture->using_dxgi ||
|
||||
capture->dxgi->updated() || request.show_cursor;
|
||||
if (capture->nv12.Convert(capture->frame_bits, capture->frame_stride,
|
||||
if (capture->frame_nv12 || capture->nv12.Convert(capture->frame_bits, capture->frame_stride,
|
||||
request.width, request.height, pixels_changed)) {
|
||||
const auto& nv12_frame = capture->nv12.pixels();
|
||||
const auto& nv12_frame = capture->frame_nv12 ? *capture->frame_nv12 : capture->nv12.pixels();
|
||||
const bool pixels_updated = capture->frame_nv12 ? capture->wgc->updated() : capture->nv12.updated();
|
||||
const auto converted_at = std::chrono::steady_clock::now();
|
||||
auto* header = reinterpret_cast<
|
||||
crossdesk::CrossDeskSecureDesktopSharedFrameHeader*>(frame_view);
|
||||
@@ -1674,7 +1782,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
header->height = static_cast<uint32_t>(request.height);
|
||||
header->payload_size = static_cast<uint32_t>(nv12_frame.size());
|
||||
header->cursor = capture->cursor_snapshot;
|
||||
if (capture->nv12.updated()) {
|
||||
if (pixels_updated) {
|
||||
std::memcpy(payload, nv12_frame.data(), nv12_frame.size());
|
||||
++converted_frames;
|
||||
}
|
||||
@@ -1686,7 +1794,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
if (capture->using_dxgi) {
|
||||
++dxgi_frames;
|
||||
if (capture->dxgi->updated()) ++dxgi_updates;
|
||||
}
|
||||
} else if (capture->using_wgc) ++wgc_frames;
|
||||
capture_ms += std::chrono::duration<double, std::milli>(captured_at -
|
||||
frame_started)
|
||||
.count();
|
||||
@@ -1815,6 +1923,7 @@ std::vector<uint8_t> PrepareSecureDesktopSharedCapture(
|
||||
capture_state->frame_view = frame_view;
|
||||
capture_state->sequence = 0;
|
||||
capture_state->request = request;
|
||||
capture_state->last_owner_refresh.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
|
||||
return {};
|
||||
}
|
||||
@@ -1837,7 +1946,13 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
||||
},
|
||||
[&] {
|
||||
// A timed-out caller may retry a start that already succeeded.
|
||||
return capture_state->request == request;
|
||||
const bool reuse = capture_state->request == request &&
|
||||
capture_state->request.owner_pid == request.owner_pid &&
|
||||
capture_state->request.owner_process &&
|
||||
WaitForSingleObject(capture_state->request.owner_process.get(), 0) == WAIT_TIMEOUT;
|
||||
if (reuse)
|
||||
capture_state->last_owner_refresh.store(GetTickCount64(), std::memory_order_relaxed);
|
||||
return reuse;
|
||||
});
|
||||
if (result == crossdesk::SharedCaptureLifecycle::StartResult::ShuttingDown) {
|
||||
return BuildTextResponseBytes(BuildErrorJson("capture_shutting_down"));
|
||||
@@ -1859,7 +1974,8 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
||||
|
||||
std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
const std::string& command,
|
||||
SecureSharedCaptureState* capture_state) {
|
||||
SecureSharedCaptureState* capture_state,
|
||||
const std::shared_ptr<void>& owner_process, DWORD owner_pid) {
|
||||
if (command.rfind(crossdesk::kSecureTextInputCommandPrefix, 0) == 0) {
|
||||
crossdesk::SecureTextInputRequest request;
|
||||
if (!crossdesk::ParseSecureTextInputCommand(command, request))
|
||||
@@ -1885,6 +2001,10 @@ std::vector<uint8_t> HandleSecureInputHelperCommand(
|
||||
|
||||
SecureCaptureRequest capture_start_request;
|
||||
if (ParseSecureInputCaptureStartCommand(command, &capture_start_request)) {
|
||||
if (!owner_process || WaitForSingleObject(owner_process.get(), 0) != WAIT_TIMEOUT)
|
||||
return BuildTextResponseBytes(BuildErrorJson("capture_owner_unavailable"));
|
||||
capture_start_request.owner_process = owner_process;
|
||||
capture_start_request.owner_pid = owner_pid;
|
||||
return StartSecureDesktopSharedCapture(capture_start_request,
|
||||
capture_state);
|
||||
}
|
||||
@@ -1971,8 +2091,19 @@ void HandleSecureInputHelperPipeClient(
|
||||
pipe, event_handle, false, buffer, sizeof(buffer) - 1,
|
||||
GetTickCount64() + 1000, &bytes_read, &error) &&
|
||||
bytes_read > 0) {
|
||||
// Bind the producer/cursor watchdog to the Windows-identified caller,
|
||||
// never a PID supplied in a capture command. The process object remains
|
||||
// valid even after its PID is reused.
|
||||
ULONG owner_pid = 0;
|
||||
std::shared_ptr<void> owner_process;
|
||||
if (GetNamedPipeClientProcessId(pipe, &owner_pid)) {
|
||||
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, owner_pid);
|
||||
if (process) owner_process = std::shared_ptr<void>(process,
|
||||
[](void* handle) { CloseHandle(handle); });
|
||||
}
|
||||
std::vector<uint8_t> response = HandleSecureInputHelperCommand(
|
||||
std::string(buffer, buffer + bytes_read), capture_state.get());
|
||||
std::string(buffer, buffer + bytes_read), capture_state.get(),
|
||||
owner_process, owner_pid);
|
||||
DWORD bytes_written = 0;
|
||||
if (!response.empty()) {
|
||||
if (crossdesk::pipe_deadline_detail::Transfer(
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
#include "shared_identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <openssl/rand.h>
|
||||
#include <stdexcept>
|
||||
#include <set>
|
||||
|
||||
#include "common/credential_cache.h"
|
||||
#include "private_file.h"
|
||||
#include "unattended_config.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
using Json = nlohmann::json;
|
||||
constexpr wchar_t kSnapshot[] = L"shared-identity.json";
|
||||
constexpr wchar_t kGuiOwner[] = L"gui-owner.json";
|
||||
|
||||
struct Handle {
|
||||
HANDLE value = INVALID_HANDLE_VALUE;
|
||||
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
|
||||
};
|
||||
void Require(bool ok, const char* message) {
|
||||
if (!ok) throw std::runtime_error(message);
|
||||
}
|
||||
uint64_t Created(HANDLE process) {
|
||||
FILETIME created{}, exited{}, kernel{}, user{};
|
||||
if (!GetProcessTimes(process, &created, &exited, &kernel, &user)) return 0;
|
||||
return (uint64_t(created.dwHighDateTime) << 32) | created.dwLowDateTime;
|
||||
}
|
||||
std::wstring ProcessPath(HANDLE process) {
|
||||
wchar_t path[32768]{};
|
||||
DWORD count = 32768;
|
||||
if (!QueryFullProcessImageNameW(process, 0, path, &count)) return {};
|
||||
return {path, count};
|
||||
}
|
||||
std::string Read(const std::filesystem::path& path, DWORD limit = 256 * 1024) {
|
||||
const DWORD sharing = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
|
||||
Handle file{path.parent_path() == UnattendedDataPath()
|
||||
? unattended_detail::OpenPrivateMachineFile(UnattendedDataPath(), path,
|
||||
GENERIC_READ, sharing, OPEN_EXISTING)
|
||||
: CreateFileW(path.c_str(), GENERIC_READ, sharing, nullptr, OPEN_EXISTING,
|
||||
FILE_FLAG_OPEN_REPARSE_POINT, nullptr)};
|
||||
Require(file.value != INVALID_HANDLE_VALUE, "Cannot open shared identity data");
|
||||
BY_HANDLE_FILE_INFORMATION info{};
|
||||
Require(GetFileInformationByHandle(file.value, &info) &&
|
||||
!(info.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) &&
|
||||
info.nNumberOfLinks == 1 && !info.nFileSizeHigh && info.nFileSizeLow <= limit,
|
||||
"Invalid shared identity file");
|
||||
std::string value(info.nFileSizeLow, '\0');
|
||||
DWORD read = 0;
|
||||
Require(ReadFile(file.value, value.data(), static_cast<DWORD>(value.size()), &read,
|
||||
nullptr) && read == value.size(), "Cannot read shared identity data");
|
||||
return value;
|
||||
}
|
||||
Json Document(const std::filesystem::path& path) {
|
||||
auto document = Json::parse(Read(path), nullptr, false);
|
||||
Require(document.is_object(), "Invalid shared identity document");
|
||||
return document;
|
||||
}
|
||||
Json PrivateDocument(const std::filesystem::path& path) {
|
||||
auto document = Json::parse(private_file::Read(path), nullptr, false);
|
||||
Require(document.is_object(), "Invalid private identity document");
|
||||
return document;
|
||||
}
|
||||
void WriteMachine(const wchar_t* name, const std::string& contents) {
|
||||
const auto root = UnattendedDataPath();
|
||||
Require(unattended_detail::WritePrivateMachineFile(root, root / name, contents),
|
||||
"Cannot persist machine identity");
|
||||
}
|
||||
HANDLE Lock(const wchar_t* name) {
|
||||
const auto root = UnattendedDataPath();
|
||||
return unattended_detail::OpenPrivateMachineFile(root,
|
||||
root / name, GENERIC_READ | GENERIC_WRITE, 0, OPEN_ALWAYS);
|
||||
}
|
||||
std::string Hex(const std::string& bytes) {
|
||||
constexpr char digits[] = "0123456789abcdef";
|
||||
std::string result;
|
||||
for (unsigned char byte : bytes) { result += digits[byte >> 4]; result += digits[byte & 15]; }
|
||||
return result;
|
||||
}
|
||||
std::string Unhex(const std::string& text) {
|
||||
Require(text.size() % 2 == 0, "Invalid identity cache encoding");
|
||||
const auto nibble = [](char c) -> unsigned {
|
||||
if (c >= '0' && c <= '9') return c - '0';
|
||||
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
|
||||
throw std::runtime_error("Invalid identity cache encoding");
|
||||
};
|
||||
std::string bytes;
|
||||
for (size_t i = 0; i < text.size(); i += 2)
|
||||
bytes += static_cast<char>((nibble(text[i]) << 4) | nibble(text[i + 1]));
|
||||
return bytes;
|
||||
}
|
||||
credential_cache::CacheV3 ValidateCache(const std::string& bytes) {
|
||||
Require(bytes.size() == sizeof(credential_cache::CacheV3), "Missing valid v3 identity cache");
|
||||
credential_cache::CacheV3 cache{};
|
||||
std::memcpy(&cache, bytes.data(), sizeof(cache));
|
||||
Require(credential_cache::Valid(cache), "Invalid identity cache checksum");
|
||||
const auto valid = [](const char* identity) {
|
||||
if (!identity[0]) return true;
|
||||
if (identity[16] || identity[9] != '@') return false;
|
||||
for (int i = 0; i < 9; ++i) if (identity[i] < '0' || identity[i] > '9') return false;
|
||||
for (int i = 10; i < 16; ++i) if (!identity[i]) return false;
|
||||
return true;
|
||||
};
|
||||
Require(valid(cache.base.client_id_with_password) && valid(cache.base.self_hosted_id) &&
|
||||
valid(cache.pending_identity) &&
|
||||
(cache.base.client_id_with_password[0] || cache.base.self_hosted_id[0]),
|
||||
"Connect once to obtain a device ID before enabling unattended startup");
|
||||
return cache;
|
||||
}
|
||||
void PreserveLocalCipher(const std::filesystem::path& directory,
|
||||
credential_cache::CacheV3& destination, bool gui) {
|
||||
bool existing = false;
|
||||
for (const char* name : {"secure_cache_v3.enc", "secure_cache_v2.enc", "secure_cache.enc"}) {
|
||||
const auto path = directory / name;
|
||||
if (!std::filesystem::exists(path)) continue;
|
||||
existing = true;
|
||||
const auto bytes = Read(path);
|
||||
if (std::strcmp(name, "secure_cache_v3.enc") == 0 &&
|
||||
bytes.size() == sizeof(credential_cache::CacheV3)) {
|
||||
credential_cache::CacheV3 local{};
|
||||
std::memcpy(&local, bytes.data(), sizeof(local));
|
||||
if (!credential_cache::Valid(local)) continue;
|
||||
std::memcpy(destination.base.key, local.base.key, sizeof(local.base.key));
|
||||
std::memcpy(destination.base.iv, local.base.iv, sizeof(local.base.iv));
|
||||
return;
|
||||
}
|
||||
if (std::strcmp(name, "secure_cache_v2.enc") == 0 &&
|
||||
bytes.size() == sizeof(credential_cache::CacheV2)) {
|
||||
credential_cache::CacheV2 local{};
|
||||
std::memcpy(&local, bytes.data(), sizeof(local));
|
||||
std::memcpy(destination.base.key, local.key, sizeof(local.key));
|
||||
std::memcpy(destination.base.iv, local.iv, sizeof(local.iv));
|
||||
return;
|
||||
}
|
||||
if (std::strcmp(name, "secure_cache.enc") == 0 &&
|
||||
bytes.size() == sizeof(credential_cache::CacheV1)) {
|
||||
credential_cache::CacheV1 local{};
|
||||
std::memcpy(&local, bytes.data(), sizeof(local));
|
||||
std::memcpy(destination.base.key, local.key, sizeof(local.key));
|
||||
std::memcpy(destination.base.iv, local.iv, sizeof(local.iv));
|
||||
return;
|
||||
}
|
||||
}
|
||||
Require(!gui || !existing, "Cannot preserve this user's saved-connection encryption keys");
|
||||
Require(RAND_bytes(destination.base.key, sizeof(destination.base.key)) == 1 &&
|
||||
RAND_bytes(destination.base.iv, sizeof(destination.base.iv)) == 1,
|
||||
"Cannot create local cache encryption keys");
|
||||
}
|
||||
void ValidateTrustedDevices(const Json& devices) {
|
||||
Require(devices.at("version") == 2 && !devices.contains("private_key") &&
|
||||
devices.at("devices").is_array() && devices.at("devices").size() <= 128,
|
||||
"Invalid shared trusted device store");
|
||||
std::set<std::string> keys;
|
||||
for (const auto& device : devices.at("devices")) {
|
||||
const auto key = device.at("public_key").get<std::string>();
|
||||
Require(Unhex(key).size() == 32 && keys.insert(key).second,
|
||||
"Invalid shared trusted device key");
|
||||
for (const char* field : {"name", "device_id"}) {
|
||||
const auto label = device.at(field).get<std::string>();
|
||||
Require(label.size() <= 128 && std::none_of(label.begin(), label.end(),
|
||||
[](unsigned char c) { return c < 32 || c == 127; }),
|
||||
"Invalid shared trusted device label");
|
||||
}
|
||||
}
|
||||
}
|
||||
Json Snapshot() {
|
||||
Require(PrepareUnattendedDirectory(false), "Machine identity directory is not private");
|
||||
auto data = Document(UnattendedDataPath() / kSnapshot);
|
||||
Require(data.at("version") == 1 && data.at("host_config").is_string(),
|
||||
"Unsupported shared identity version");
|
||||
ValidateCache(Unhex(data.at("cache_v3").get<std::string>()));
|
||||
Require(Unhex(data.at("trust_private_key").get<std::string>()).size() == 32,
|
||||
"Invalid shared device identity key");
|
||||
ValidateTrustedDevices(data.at("trusted_devices"));
|
||||
return data;
|
||||
}
|
||||
bool SameOwner(const Json& owner) {
|
||||
return owner.value("pid", 0u) == GetCurrentProcessId() &&
|
||||
owner.value("created", uint64_t{0}) == Created(GetCurrentProcess());
|
||||
}
|
||||
void RemoveGuiOwner() {
|
||||
try {
|
||||
if (SameOwner(Document(UnattendedDataPath() / kGuiOwner)))
|
||||
DeleteFileW((UnattendedDataPath() / kGuiOwner).c_str());
|
||||
} catch (...) {}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SharedHostIdentity::SharedHostIdentity(std::filesystem::path cache,
|
||||
std::filesystem::path trusted, HANDLE lease, bool gui)
|
||||
: cache_(std::move(cache)), trusted_(std::move(trusted)), lease_(lease), gui_(gui) {}
|
||||
|
||||
SharedHostIdentity::~SharedHostIdentity() {
|
||||
if (gui_) RemoveGuiOwner();
|
||||
if (lease_ != INVALID_HANDLE_VALUE) CloseHandle(lease_);
|
||||
}
|
||||
|
||||
bool SharedHostIdentity::Configured() {
|
||||
const auto path = UnattendedDataPath() / kSnapshot;
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
return attributes != INVALID_FILE_ATTRIBUTES;
|
||||
}
|
||||
|
||||
bool SharedHostIdentity::GuiPresent() {
|
||||
try {
|
||||
if (!PrepareUnattendedDirectory(false)) return false;
|
||||
const auto owner = Document(UnattendedDataPath() / kGuiOwner);
|
||||
Handle process{OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION,
|
||||
FALSE, owner.at("pid").get<DWORD>())};
|
||||
if (!process.value || WaitForSingleObject(process.value, 0) != WAIT_TIMEOUT ||
|
||||
!Created(process.value) || Created(process.value) != owner.at("created")) return false;
|
||||
DWORD session = 0;
|
||||
return ProcessIdToSessionId(owner.at("pid").get<DWORD>(), &session) && session != 0 &&
|
||||
session == owner.at("session").get<DWORD>() &&
|
||||
std::filesystem::path(ProcessPath(process.value)).u8string() == owner.at("executable");
|
||||
} catch (...) { return false; }
|
||||
}
|
||||
|
||||
std::unique_ptr<SharedHostIdentity> SharedHostIdentity::AttachGui(
|
||||
const std::filesystem::path& cache, const std::filesystem::path& trusted,
|
||||
bool import_existing, std::string& error) {
|
||||
error.clear();
|
||||
bool reserved = false;
|
||||
try {
|
||||
Require(IsAdministratorProcess() && !IsLocalSystemProcess(), "Shared GUI identity requires an administrator");
|
||||
Require(PrepareUnattendedDirectory(true), "Cannot prepare private machine identity directory");
|
||||
Handle claim{Lock(L"gui-claim.lock")};
|
||||
Require(claim.value != INVALID_HANDLE_VALUE, "Another GUI attachment is in progress");
|
||||
Require(!GuiPresent(), "Another GUI is already using this machine identity");
|
||||
DWORD session = 0;
|
||||
Require(ProcessIdToSessionId(GetCurrentProcessId(), &session) && session != 0,
|
||||
"A GUI identity requires an interactive session");
|
||||
WriteMachine(kGuiOwner, Json{{"pid", GetCurrentProcessId()}, {"created", Created(GetCurrentProcess())},
|
||||
{"session", session}, {"executable", std::filesystem::path(ProcessPath(GetCurrentProcess())).u8string()}}.dump());
|
||||
reserved = true;
|
||||
// The GUI imports a profile, but never acquires the host's network lease.
|
||||
// Keeping the lease in SYSTEM is what preserves RTC across console login.
|
||||
auto result = std::unique_ptr<SharedHostIdentity>(
|
||||
new SharedHostIdentity(cache, trusted, INVALID_HANDLE_VALUE, true));
|
||||
if (import_existing) result->Import();
|
||||
else result->host_config_ = Snapshot().at("host_config").get<std::string>();
|
||||
return result;
|
||||
} catch (const std::exception& failure) {
|
||||
if (reserved) RemoveGuiOwner();
|
||||
error = failure.what();
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<SharedHostIdentity> SharedHostIdentity::AcquireHost(std::string& error) {
|
||||
error.clear();
|
||||
try {
|
||||
Require(IsLocalSystemProcess() && PrepareUnattendedDirectory(false), "Machine identity requires SYSTEM");
|
||||
HANDLE lease = Lock(L"identity-owner.lock");
|
||||
Require(lease != INVALID_HANDLE_VALUE, "Machine identity is already in use");
|
||||
const auto cache = UnattendedDataPath() / L"host";
|
||||
auto result = std::unique_ptr<SharedHostIdentity>(
|
||||
new SharedHostIdentity(cache, cache / L"trusted-devices", lease, false));
|
||||
result->Import();
|
||||
private_file::Write(cache / "config.ini", result->host_config_);
|
||||
return result;
|
||||
} catch (const std::exception& failure) { error = failure.what(); return nullptr; }
|
||||
}
|
||||
|
||||
void SharedHostIdentity::Import() {
|
||||
const auto data = Snapshot();
|
||||
private_file::EnsureDirectory(cache_, true);
|
||||
private_file::EnsureDirectory(trusted_);
|
||||
Json identity{{"version", 1}};
|
||||
if (std::filesystem::exists(cache_ / "identity.json"))
|
||||
identity = PrivateDocument(cache_ / "identity.json");
|
||||
Require(identity.is_object() && identity.value("version", 0) == 1,
|
||||
"Invalid existing device identity store");
|
||||
identity["trust_private_key"] = data.at("trust_private_key");
|
||||
const auto bytes = Unhex(data.at("cache_v3").get<std::string>());
|
||||
credential_cache::CacheV3 cache{};
|
||||
std::memcpy(&cache, bytes.data(), sizeof(cache));
|
||||
// These keys encrypt local thumbnails/recent credentials, not the device
|
||||
// identity. Sharing them would make another user's saved history unreadable.
|
||||
PreserveLocalCipher(cache_, cache, gui_);
|
||||
cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum));
|
||||
// Validate everything before replacing any user identity data. Both stores
|
||||
// stay private to this process's user; their DACLs are never relaxed.
|
||||
private_file::Write(cache_ / "identity.json", identity.dump());
|
||||
private_file::Write(trusted_ / "devices.json", data.at("trusted_devices").dump());
|
||||
private_file::Write(cache_ / "secure_cache_v3.enc",
|
||||
std::string(reinterpret_cast<const char*>(&cache), sizeof(cache)));
|
||||
private_file::Write(cache_ / "secure_cache_v2.enc",
|
||||
std::string(reinterpret_cast<const char*>(&cache.base), sizeof(cache.base)));
|
||||
host_config_ = data.at("host_config").get<std::string>();
|
||||
}
|
||||
|
||||
bool SharedHostIdentity::Publish(const std::string& host_config, std::string& error) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
error.clear();
|
||||
try {
|
||||
Require(!gui_ && lease_ != INVALID_HANDLE_VALUE, "Machine identity lease is unavailable");
|
||||
auto cache = ValidateCache(Read(cache_ / "secure_cache_v3.enc"));
|
||||
std::memset(cache.base.key, 0, sizeof(cache.base.key));
|
||||
std::memset(cache.base.iv, 0, sizeof(cache.base.iv));
|
||||
cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum));
|
||||
const std::string identity_cache(reinterpret_cast<const char*>(&cache), sizeof(cache));
|
||||
const auto identity = PrivateDocument(cache_ / "identity.json");
|
||||
const auto key = identity.at("trust_private_key").get<std::string>();
|
||||
Require(Unhex(key).size() == 32, "Invalid device identity key");
|
||||
const auto devices = PrivateDocument(trusted_ / "devices.json");
|
||||
ValidateTrustedDevices(devices);
|
||||
const auto& configuration = host_config.empty() ? host_config_ : host_config;
|
||||
Require(!configuration.empty(), "Shared host configuration is missing");
|
||||
Json snapshot{{"version", 1}, {"cache_v3", Hex(identity_cache)}, {"trust_private_key", key},
|
||||
{"trusted_devices", devices}, {"host_config", configuration}};
|
||||
const auto contents = snapshot.dump();
|
||||
Require(contents.size() <= 256 * 1024, "Shared identity is too large");
|
||||
WriteMachine(kSnapshot, contents);
|
||||
host_config_ = configuration;
|
||||
return true;
|
||||
} catch (const std::exception& failure) { error = failure.what(); return false; }
|
||||
}
|
||||
|
||||
bool SharedHostIdentity::Prepare(const std::filesystem::path& cache,
|
||||
const std::filesystem::path& trusted, const std::string& host_config, std::string& error) {
|
||||
error.clear();
|
||||
try {
|
||||
Require(PrepareUnattendedDirectory(true), "Cannot prepare private machine identity directory");
|
||||
if (Configured()) { Snapshot(); return true; }
|
||||
Require(!GuiPresent(), "Enable shared startup from the running GUI settings");
|
||||
HANDLE lease = Lock(L"identity-owner.lock");
|
||||
Require(lease != INVALID_HANDLE_VALUE, "Machine identity is already in use");
|
||||
SharedHostIdentity snapshot(cache, trusted, lease, false);
|
||||
return snapshot.Publish(host_config, error);
|
||||
} catch (const std::exception& failure) { error = failure.what(); return false; }
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-08
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SHARED_IDENTITY_H_
|
||||
#define _SHARED_IDENTITY_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Credential snapshots live in the administrator/SYSTEM-only machine root.
|
||||
// Each process imports them into its own user-only identity store. Only SYSTEM
|
||||
// takes the exclusive network lease. The GUI marker records frontend presence.
|
||||
class SharedHostIdentity {
|
||||
public:
|
||||
~SharedHostIdentity();
|
||||
static bool Configured();
|
||||
static bool GuiPresent();
|
||||
static bool Prepare(const std::filesystem::path& cache,
|
||||
const std::filesystem::path& trusted_devices,
|
||||
const std::string& host_config, std::string& error);
|
||||
static std::unique_ptr<SharedHostIdentity> AttachGui(
|
||||
const std::filesystem::path& cache,
|
||||
const std::filesystem::path& trusted_devices, bool import_existing,
|
||||
std::string& error);
|
||||
static std::unique_ptr<SharedHostIdentity> AcquireHost(std::string& error);
|
||||
|
||||
bool Publish(const std::string& host_config, std::string& error);
|
||||
const std::string& HostConfig() const { return host_config_; }
|
||||
const std::filesystem::path& CachePath() const { return cache_; }
|
||||
const std::filesystem::path& TrustedDevicesPath() const { return trusted_; }
|
||||
|
||||
private:
|
||||
SharedHostIdentity(std::filesystem::path cache, std::filesystem::path trusted,
|
||||
HANDLE lease, bool gui);
|
||||
void Import();
|
||||
std::filesystem::path cache_, trusted_;
|
||||
HANDLE lease_ = INVALID_HANDLE_VALUE;
|
||||
bool gui_ = false;
|
||||
std::string host_config_;
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <sddl.h>
|
||||
|
||||
#include <ctime>
|
||||
#include <atomic>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <vector>
|
||||
@@ -20,6 +21,20 @@ bool TrustedSid(PSID sid) {
|
||||
IsWellKnownSid(sid, WinBuiltinAdministratorsSid));
|
||||
}
|
||||
|
||||
bool PrivateAccess(PSID owner, PACL acl, BYTE required_flags = 0) {
|
||||
if (!TrustedSid(owner) || !acl || !acl->AceCount) return false;
|
||||
for (DWORD i = 0; i < acl->AceCount; ++i) {
|
||||
void* raw = nullptr;
|
||||
if (!GetAce(acl, i, &raw)) return false;
|
||||
auto* ace = static_cast<ACCESS_ALLOWED_ACE*>(raw);
|
||||
// Credentials and SYSTEM configuration are restricted to SYSTEM/admins.
|
||||
if (ace->Header.AceType != ACCESS_ALLOWED_ACE_TYPE ||
|
||||
!TrustedSid(&ace->SidStart) ||
|
||||
(ace->Header.AceFlags & required_flags) != required_flags) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivateDirectory(const std::filesystem::path& path) {
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES ||
|
||||
@@ -34,22 +49,7 @@ bool PrivateDirectory(const std::filesystem::path& path) {
|
||||
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner,
|
||||
nullptr, &dacl, nullptr, &descriptor) != ERROR_SUCCESS)
|
||||
return false;
|
||||
bool safe = TrustedSid(owner) && dacl && dacl->AceCount > 0;
|
||||
for (DWORD index = 0; safe && index < dacl->AceCount; ++index) {
|
||||
void* raw = nullptr;
|
||||
if (!GetAce(dacl, index, &raw)) {
|
||||
safe = false;
|
||||
break;
|
||||
}
|
||||
auto* ace = static_cast<ACCESS_ALLOWED_ACE*>(raw);
|
||||
// Only SYSTEM and elevated administrators can read credentials or write
|
||||
// configuration that the service will consume as SYSTEM.
|
||||
safe =
|
||||
ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE &&
|
||||
TrustedSid(&ace->SidStart) &&
|
||||
(ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
|
||||
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE);
|
||||
}
|
||||
const bool safe = PrivateAccess(owner, dacl, OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE);
|
||||
LocalFree(descriptor);
|
||||
return safe;
|
||||
}
|
||||
@@ -82,13 +82,25 @@ bool SetUnattendedEnabled(bool enabled) {
|
||||
nullptr) != ERROR_SUCCESS)
|
||||
return false;
|
||||
const DWORD value = enabled ? 1 : 0;
|
||||
const auto result =
|
||||
auto result =
|
||||
RegSetValueExW(key, L"Enabled", 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
if (result == ERROR_SUCCESS && enabled) {
|
||||
const DWORD version = 1;
|
||||
result = RegSetValueExW(key, L"SharedIdentityVersion", 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&version), sizeof(version));
|
||||
}
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
bool IsSharedIdentityServiceConfigured() {
|
||||
DWORD version = 0;
|
||||
DWORD size = sizeof(version);
|
||||
return RegGetValueW(HKEY_LOCAL_MACHINE, kRegistryKey, L"SharedIdentityVersion",
|
||||
RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, &version, &size) == ERROR_SUCCESS && version == 1;
|
||||
}
|
||||
|
||||
bool IsAdministratorProcess() {
|
||||
BYTE sid[SECURITY_MAX_SID_SIZE];
|
||||
DWORD size = sizeof(sid);
|
||||
@@ -116,6 +128,25 @@ bool PrepareUnattendedDirectory(bool create) {
|
||||
create);
|
||||
}
|
||||
|
||||
bool PrepareUnattendedDirectoryForService() {
|
||||
if (!IsLocalSystemProcess()) return false;
|
||||
const auto root = UnattendedDataPath();
|
||||
// Only adopt an already trusted SYSTEM/Administrators directory. Never
|
||||
// repair a permissive, linked, or ordinary-user-owned machine profile.
|
||||
if (!unattended_detail::PreparePrivateDirectory(root, true)) return false;
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
kDirectorySecurity, SDDL_REVISION_1, &descriptor, nullptr)) return false;
|
||||
PACL acl = nullptr;
|
||||
BOOL present = FALSE, defaulted = FALSE;
|
||||
const bool updated = GetSecurityDescriptorDacl(descriptor, &present, &acl, &defaulted) &&
|
||||
present && SetNamedSecurityInfoW(const_cast<wchar_t*>(root.c_str()), SE_FILE_OBJECT,
|
||||
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
|
||||
nullptr, nullptr, acl, nullptr) == ERROR_SUCCESS;
|
||||
LocalFree(descriptor);
|
||||
return updated && PrivateDirectory(root);
|
||||
}
|
||||
|
||||
bool unattended_detail::PreparePrivateDirectory(
|
||||
const std::filesystem::path& root, bool create) {
|
||||
if (root.empty()) return false;
|
||||
@@ -134,6 +165,62 @@ bool unattended_detail::PreparePrivateDirectory(
|
||||
return PrivateDirectory(root);
|
||||
}
|
||||
|
||||
HANDLE unattended_detail::OpenPrivateMachineFile(const std::filesystem::path& root,
|
||||
const std::filesystem::path& path, DWORD access, DWORD sharing,
|
||||
DWORD creation, DWORD flags) {
|
||||
if (path.parent_path() != root || !PreparePrivateDirectory(root, false)) {
|
||||
SetLastError(ERROR_ACCESS_DENIED);
|
||||
return INVALID_HANDLE_VALUE;
|
||||
}
|
||||
PSECURITY_DESCRIPTOR security = nullptr;
|
||||
const auto sddl = IsLocalSystemProcess()
|
||||
? L"O:SYG:SYD:P(A;;FA;;;SY)(A;;FA;;;BA)"
|
||||
: L"O:BAG:BAD:P(A;;FA;;;SY)(A;;FA;;;BA)";
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
sddl, SDDL_REVISION_1, &security, nullptr)) return INVALID_HANDLE_VALUE;
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(attributes), security, FALSE};
|
||||
HANDLE file = CreateFileW(path.c_str(), access | READ_CONTROL, sharing,
|
||||
&attributes, creation, flags | FILE_FLAG_OPEN_REPARSE_POINT, nullptr);
|
||||
const DWORD error = GetLastError();
|
||||
LocalFree(security);
|
||||
if (file == INVALID_HANDLE_VALUE) { SetLastError(error); return file; }
|
||||
BY_HANDLE_FILE_INFORMATION info{};
|
||||
PSID owner = nullptr;
|
||||
PACL acl = nullptr;
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
const bool safe = GetFileInformationByHandle(file, &info) && info.nNumberOfLinks == 1 &&
|
||||
!(info.dwFileAttributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY)) &&
|
||||
GetSecurityInfo(file, SE_FILE_OBJECT, OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
|
||||
&owner, nullptr, &acl, nullptr, &descriptor) == ERROR_SUCCESS &&
|
||||
PrivateAccess(owner, acl);
|
||||
if (descriptor) LocalFree(descriptor);
|
||||
if (safe) return file;
|
||||
CloseHandle(file);
|
||||
SetLastError(ERROR_ACCESS_DENIED);
|
||||
return INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
bool unattended_detail::WritePrivateMachineFile(const std::filesystem::path& root,
|
||||
const std::filesystem::path& path, const std::string& contents) {
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
if (attributes != INVALID_FILE_ATTRIBUTES &&
|
||||
(attributes & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY))) return false;
|
||||
static std::atomic<unsigned long> sequence{0};
|
||||
const auto temporary = path.wstring() + L".tmp-" + std::to_wstring(GetCurrentProcessId()) +
|
||||
L"-" + std::to_wstring(GetTickCount64()) + L"-" + std::to_wstring(sequence++);
|
||||
HANDLE output = OpenPrivateMachineFile(
|
||||
root, temporary, GENERIC_WRITE, 0, CREATE_NEW);
|
||||
if (output == INVALID_HANDLE_VALUE) return false;
|
||||
DWORD written = 0;
|
||||
const bool saved = WriteFile(output, contents.data(), static_cast<DWORD>(contents.size()),
|
||||
&written, nullptr) && written == contents.size() && FlushFileBuffers(output);
|
||||
CloseHandle(output);
|
||||
if (saved && MoveFileExW(temporary.c_str(), path.c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) return true;
|
||||
DeleteFileW(temporary.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WriteUnattendedStatus(const std::string& identity, bool online,
|
||||
DWORD session_id) {
|
||||
const auto root = UnattendedDataPath();
|
||||
@@ -141,6 +228,7 @@ bool WriteUnattendedStatus(const std::string& identity, bool online,
|
||||
const auto separator = identity.find('@');
|
||||
nlohmann::json status = {
|
||||
{"online", online},
|
||||
{"mode", "service"},
|
||||
{"process_id", GetCurrentProcessId()},
|
||||
{"session_id", session_id},
|
||||
{"updated_at", std::time(nullptr)},
|
||||
@@ -148,13 +236,7 @@ bool WriteUnattendedStatus(const std::string& identity, bool online,
|
||||
separator == std::string::npos ? "" : identity.substr(0, separator)},
|
||||
{"password",
|
||||
separator == std::string::npos ? "" : identity.substr(separator + 1)}};
|
||||
const auto temporary = root / L"status.tmp";
|
||||
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
|
||||
output << status.dump(2);
|
||||
output.close();
|
||||
return !output.fail() &&
|
||||
MoveFileExW(temporary.c_str(), (root / L"status.json").c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
|
||||
return unattended_detail::WritePrivateMachineFile(root, root / L"status.json", status.dump());
|
||||
}
|
||||
|
||||
std::string ReadUnattendedStatus() {
|
||||
|
||||
@@ -10,15 +10,26 @@ namespace crossdesk {
|
||||
// This directory is deliberately separate from every user's GUI profile.
|
||||
std::filesystem::path UnattendedDataPath();
|
||||
bool IsUnattendedEnabled();
|
||||
bool IsSharedIdentityServiceConfigured();
|
||||
bool SetUnattendedEnabled(bool enabled);
|
||||
bool IsLocalSystemProcess();
|
||||
bool IsAdministratorProcess();
|
||||
// Reject reparse points, untrusted owners and permissive existing DACLs.
|
||||
bool PrepareUnattendedDirectory(bool create);
|
||||
// SYSTEM repairs the old host's user-only root ACL before GUI migration.
|
||||
bool PrepareUnattendedDirectoryForService();
|
||||
namespace unattended_detail {
|
||||
// Parameterized only for isolated native ACL tests; production always uses
|
||||
// the fixed ProgramData path above.
|
||||
bool PreparePrivateDirectory(const std::filesystem::path& root, bool create);
|
||||
// Both owner and DACL are restricted to SYSTEM/Administrators, including
|
||||
// WRITE_DAC access from a filtered token belonging to the file's creator.
|
||||
HANDLE OpenPrivateMachineFile(const std::filesystem::path& root,
|
||||
const std::filesystem::path& path, DWORD access, DWORD sharing,
|
||||
DWORD creation, DWORD flags = FILE_ATTRIBUTE_NORMAL);
|
||||
// Write through a protected temporary file, then atomically replace the target.
|
||||
bool WritePrivateMachineFile(const std::filesystem::path& root,
|
||||
const std::filesystem::path& path, const std::string& contents);
|
||||
} // namespace unattended_detail
|
||||
bool WriteUnattendedStatus(const std::string& identity, bool online,
|
||||
DWORD session_id);
|
||||
|
||||
@@ -191,6 +191,58 @@ std::vector<VirtualDisplayMode> EnumerateModes(const std::wstring& device) {
|
||||
|
||||
} // namespace
|
||||
|
||||
VirtualDisplayMode ResolveVirtualDisplayMode() {
|
||||
constexpr wchar_t value_name[] = L"LastAttachedDisplayMode";
|
||||
DWORD saved = 0, size = sizeof(saved);
|
||||
if (RegGetValueW(HKEY_LOCAL_MACHINE, kUsbmmiddInstallMarkerKey, value_name,
|
||||
RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, &saved, &size) != ERROR_SUCCESS)
|
||||
saved = 0;
|
||||
const auto valid = [](VirtualDisplayMode mode) {
|
||||
return mode.width > 0 && mode.height > 0 && mode.width <= 16384 && mode.height <= 16384;
|
||||
};
|
||||
VirtualDisplayMode attached;
|
||||
// RDP creates a display mode for that client, not the console's next boot.
|
||||
if (!GetSystemMetrics(SM_REMOTESESSION)) {
|
||||
for (DWORD index = 0;; ++index) {
|
||||
DISPLAY_DEVICEW adapter{sizeof(adapter)};
|
||||
if (!EnumDisplayDevicesW(nullptr, index, &adapter, 0)) break;
|
||||
if (!(adapter.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) ||
|
||||
IsUsbmmiddAdapter(adapter.DeviceString, adapter.DeviceID)) continue;
|
||||
bool present = false;
|
||||
for (DWORD child = 0; !present; ++child) {
|
||||
DISPLAY_DEVICEW monitor{sizeof(monitor)};
|
||||
if (!EnumDisplayDevicesW(adapter.DeviceName, child, &monitor, 0)) break;
|
||||
present = (monitor.StateFlags & DISPLAY_DEVICE_ACTIVE) != 0 &&
|
||||
!ContainsCaseInsensitive(monitor.DeviceID, L"Default_Monitor");
|
||||
}
|
||||
DEVMODEW mode{};
|
||||
mode.dmSize = sizeof(mode);
|
||||
if (!present || !EnumDisplaySettingsExW(adapter.DeviceName, ENUM_CURRENT_SETTINGS, &mode, 0))
|
||||
continue;
|
||||
const VirtualDisplayMode current{static_cast<int>(mode.dmPelsWidth),
|
||||
static_cast<int>(mode.dmPelsHeight)};
|
||||
if (!valid(current)) continue;
|
||||
if (!valid(attached) || (adapter.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE)) attached = current;
|
||||
if (adapter.StateFlags & DISPLAY_DEVICE_PRIMARY_DEVICE) break;
|
||||
}
|
||||
}
|
||||
if (valid(attached)) {
|
||||
const DWORD packed = (static_cast<DWORD>(attached.width) << 16) |
|
||||
static_cast<DWORD>(attached.height);
|
||||
HKEY key = nullptr;
|
||||
if (packed != saved && RegCreateKeyExW(HKEY_LOCAL_MACHINE, kUsbmmiddInstallMarkerKey,
|
||||
0, nullptr, 0, KEY_SET_VALUE | KEY_WOW64_64KEY, nullptr, &key, nullptr) == ERROR_SUCCESS) {
|
||||
if (RegSetValueExW(key, value_name, 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&packed), sizeof(packed)) == ERROR_SUCCESS)
|
||||
LOG_INFO("Remembered attached display mode: {}x{}", attached.width, attached.height);
|
||||
RegCloseKey(key);
|
||||
}
|
||||
return attached;
|
||||
}
|
||||
const VirtualDisplayMode previous{static_cast<int>(saved >> 16), static_cast<int>(saved & 0xffff)};
|
||||
return valid(previous) ? previous : kDefaultVirtualDisplayMode;
|
||||
}
|
||||
|
||||
UsbmmiddPackage LocateUsbmmiddPackage() {
|
||||
UsbmmiddPackage package;
|
||||
const std::filesystem::path base = CurrentExecutableDirectory();
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// Plug/unplug talks to the driver through Amyuni's installer and needs
|
||||
// administrator rights inside the interactive session. The session helper
|
||||
// (SYSTEM, console session) is the intended caller; the GUI only calls these
|
||||
// directly when it is elevated and no helper is reachable.
|
||||
// administrator rights inside the interactive session. An elevated GUI or
|
||||
// SYSTEM host provisions locally with its own token.
|
||||
|
||||
struct UsbmmiddPackage {
|
||||
std::filesystem::path directory;
|
||||
@@ -40,6 +39,10 @@ struct UsbmmiddStatus {
|
||||
};
|
||||
UsbmmiddStatus QueryUsbmmiddStatus();
|
||||
|
||||
// Remember an attached display's current pixel mode for GUI/SYSTEM startup.
|
||||
// Phantom targets and our own fallback monitor must not overwrite that mode.
|
||||
VirtualDisplayMode ResolveVirtualDisplayMode();
|
||||
|
||||
// Adapters attached to the desktop, i.e. monitors currently plugged in.
|
||||
int CountUsbmmiddMonitors();
|
||||
// GDI device name of an attached usbmmidd output, or empty while settling.
|
||||
|
||||
@@ -32,6 +32,14 @@ constexpr DWORD kInstallAndPlugTimeoutMs = 120 * 1000;
|
||||
constexpr DWORD kUnplugTimeoutMs = 10 * 1000;
|
||||
constexpr DWORD kPresenceSettleTimeoutMs = 5 * 1000;
|
||||
|
||||
bool HasProvisioningPrivileges() {
|
||||
BYTE admin_sid[SECURITY_MAX_SID_SIZE];
|
||||
DWORD size = sizeof(admin_sid);
|
||||
BOOL member = FALSE;
|
||||
return CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admin_sid, &size) &&
|
||||
CheckTokenMembership(nullptr, admin_sid, &member) && member;
|
||||
}
|
||||
|
||||
struct HelperVirtualDisplayStatus {
|
||||
bool ok = false;
|
||||
bool package_available = false;
|
||||
@@ -189,23 +197,36 @@ bool VirtualDisplayProvisioner::Acquire(VirtualDisplayMode mode,
|
||||
std::string* error) {
|
||||
std::string local_error;
|
||||
if (error == nullptr) error = &local_error;
|
||||
if (active_.load(std::memory_order_relaxed)) return true;
|
||||
if (Cancelled()) {
|
||||
*error = "cancelled";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (active_.load(std::memory_order_relaxed)) {
|
||||
if (CountUsbmmiddMonitors() > 0) return ApplyUsbmmiddDisplayMode(mode, error);
|
||||
// Windows can remove a boot-time target while restoring display topology.
|
||||
// The old ownership count no longer describes any attached monitor.
|
||||
LOG_WARN("Virtual display target disappeared; recreating it");
|
||||
active_.store(false, std::memory_order_relaxed);
|
||||
via_helper_ = pending_helper_release_ = false;
|
||||
plugged_locally_ = 0;
|
||||
}
|
||||
bool helper_reachable = false;
|
||||
const ULONGLONG started = GetTickCount64();
|
||||
bool ok = AcquireViaHelper(mode, error, &helper_reachable);
|
||||
if (ok) {
|
||||
via_helper_ = true;
|
||||
} else if (!helper_reachable && *error != "cancelled") {
|
||||
LOG_INFO("Virtual display: session helper unreachable ({}), trying "
|
||||
"in-process provisioning",
|
||||
*error);
|
||||
bool ok = false;
|
||||
if (HasProvisioningPrivileges()) {
|
||||
// A user-token helper cannot inspect/authorize a SYSTEM caller. SYSTEM
|
||||
// and elevated GUI hosts already have the rights required by the driver.
|
||||
LOG_INFO("Virtual display: provisioning with this host's administrator/SYSTEM token");
|
||||
ok = AcquireLocally(mode, error);
|
||||
via_helper_ = false;
|
||||
} else {
|
||||
ok = AcquireViaHelper(mode, error, &helper_reachable);
|
||||
via_helper_ = ok;
|
||||
if (!ok && !helper_reachable && *error != "cancelled") {
|
||||
LOG_INFO("Virtual display: session helper unreachable ({}), trying in-process provisioning", *error);
|
||||
error->clear();
|
||||
ok = AcquireLocally(mode, error);
|
||||
}
|
||||
}
|
||||
if (!ok) {
|
||||
LOG_WARN("Virtual display: provisioning failed after {} ms, error={}",
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
namespace crossdesk {
|
||||
|
||||
// Session-scoped usbmmidd virtual display for a host without any monitor.
|
||||
// Acquire() asks the session helper (SYSTEM in the console session) to
|
||||
// install/plug the driver; without a reachable helper an elevated GUI does it
|
||||
// itself. Release() unplugs only what this object plugged.
|
||||
// SYSTEM/elevated hosts provision directly with their existing token. Other
|
||||
// hosts use the session helper, with a local fallback if it is unreachable.
|
||||
// Release() unplugs only what this object plugged.
|
||||
class VirtualDisplayProvisioner {
|
||||
public:
|
||||
VirtualDisplayProvisioner() = default;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-11-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINRT_APARTMENT_H_
|
||||
#define _WINRT_APARTMENT_H_
|
||||
|
||||
#include <roapi.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Keep WinRT initialized for the lifetime of the thread's capture objects.
|
||||
// An existing STA is valid too; balance only our successful initialization.
|
||||
class ScopedWinRtApartment {
|
||||
public:
|
||||
ScopedWinRtApartment() : result_(RoInitialize(RO_INIT_MULTITHREADED)) {
|
||||
if (!ready())
|
||||
LOG_ERROR("WinRT initialization failed, hr=0x{:08X}, thread={}",
|
||||
static_cast<uint32_t>(result_), GetCurrentThreadId());
|
||||
}
|
||||
~ScopedWinRtApartment() {
|
||||
if (SUCCEEDED(result_)) RoUninitialize();
|
||||
}
|
||||
ScopedWinRtApartment(const ScopedWinRtApartment&) = delete;
|
||||
ScopedWinRtApartment& operator=(const ScopedWinRtApartment&) = delete;
|
||||
bool ready() const { return SUCCEEDED(result_) || result_ == RPC_E_CHANGED_MODE; }
|
||||
|
||||
private:
|
||||
HRESULT result_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user