mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-20 11:06:46 +08:00
Compare commits
5
Commits
f3e67d7d6e
...
ad62e60b9a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad62e60b9a | ||
|
|
c4aca303c8 | ||
|
|
57bb91dba3 | ||
|
|
57cab3979e | ||
|
|
d8ccdbaa38 |
@@ -22,6 +22,7 @@
|
||||
#include "config_center.h"
|
||||
#include "daemon.h"
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
|
||||
#ifdef __linux__
|
||||
@@ -192,6 +193,14 @@ int main(int argc, char* argv[]) {
|
||||
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
|
||||
return crossdesk::RunSlintRendererProbe();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Service operations and config migration can log before the GUI starts.
|
||||
// Configure the directory first, including CLI and daemon child paths.
|
||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||
crossdesk::InitLogger(path_manager->GetLogPath().string());
|
||||
|
||||
#ifdef _WIN32
|
||||
if (argc > 1 && IsServiceCliCommand(argv[1])) {
|
||||
return HandleServiceCliCommand(argv[1]);
|
||||
}
|
||||
@@ -218,7 +227,6 @@ int main(int argc, char* argv[]) {
|
||||
#endif
|
||||
|
||||
bool enable_daemon = false;
|
||||
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
|
||||
if (path_manager) {
|
||||
std::string cache_path = path_manager->GetCachePath().string();
|
||||
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
|
||||
|
||||
@@ -950,10 +950,10 @@ int GuiApplication::Run() {
|
||||
}
|
||||
exec_log_path_ = path_manager_->GetLogPath().string();
|
||||
dll_log_path_ = exec_log_path_;
|
||||
InitializeLogger();
|
||||
cache_path_ = path_manager_->GetCachePath().string();
|
||||
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
|
||||
|
||||
InitializeLogger();
|
||||
LOG_INFO("CrossDesk version: {} (Slint UI)", CROSSDESK_VERSION);
|
||||
|
||||
strncpy(signal_server_ip_self_, config_center_->GetSignalServerHost().c_str(),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
#include "features/devices/session_device_manager.h"
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -59,6 +60,53 @@ bool SessionDeviceManager::ShouldSendCapturedFrame(
|
||||
return true;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::RecordCaptureCadence(
|
||||
std::chrono::steady_clock::time_point now, int fps,
|
||||
bool from_secure_desktop) {
|
||||
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
|
||||
if (capture_metrics_started_ == std::chrono::steady_clock::time_point{})
|
||||
capture_metrics_started_ = now;
|
||||
const auto elapsed_us = std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
now - capture_metrics_started_)
|
||||
.count();
|
||||
if (elapsed_us >= 2000000) {
|
||||
if (capture_forwarded_) {
|
||||
const char* source = capture_secure_callbacks_ == 0 ? "normal_desktop"
|
||||
: capture_secure_callbacks_ == capture_callbacks_
|
||||
? "secure_desktop"
|
||||
: "mixed";
|
||||
LOG_INFO(
|
||||
"Capture delivery: source={} normal_callbacks={} secure_callbacks={} "
|
||||
"target_fps={} input_fps={:.2f} "
|
||||
"forwarded_fps={:.2f} callbacks={} rate_dropped={} "
|
||||
"forwarded={} send_errors={} max_callback_gap_us={} "
|
||||
"send_avg_us={} send_max_us={} window_ms={}",
|
||||
source, capture_callbacks_ - capture_secure_callbacks_,
|
||||
capture_secure_callbacks_, fps,
|
||||
capture_callbacks_ * 1000000.0 / elapsed_us,
|
||||
capture_forwarded_ * 1000000.0 / elapsed_us, capture_callbacks_,
|
||||
capture_rate_drops_, capture_forwarded_, capture_send_errors_,
|
||||
capture_max_gap_us_, capture_send_us_ / capture_forwarded_,
|
||||
capture_max_send_us_, elapsed_us / 1000);
|
||||
}
|
||||
capture_metrics_started_ = now;
|
||||
capture_callbacks_ = capture_rate_drops_ = capture_forwarded_ = 0;
|
||||
capture_secure_callbacks_ = 0;
|
||||
capture_send_errors_ = 0;
|
||||
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
|
||||
}
|
||||
if (last_capture_callback_ != std::chrono::steady_clock::time_point{}) {
|
||||
capture_max_gap_us_ =
|
||||
std::max<int64_t>(capture_max_gap_us_,
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
now - last_capture_callback_)
|
||||
.count());
|
||||
}
|
||||
last_capture_callback_ = std::max(last_capture_callback_, now);
|
||||
++capture_callbacks_;
|
||||
if (from_secure_desktop) ++capture_secure_callbacks_;
|
||||
}
|
||||
|
||||
void SessionDeviceManager::Initialize() {
|
||||
InitializeAudioOutput();
|
||||
screen_capturer_factory_ = new ScreenCapturerFactory();
|
||||
@@ -88,6 +136,11 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
|
||||
last_frame_time_ = {};
|
||||
next_frame_deadline_ = {};
|
||||
capture_metrics_started_ = last_capture_callback_ = {};
|
||||
capture_callbacks_ = capture_rate_drops_ = capture_forwarded_ = 0;
|
||||
capture_secure_callbacks_ = 0;
|
||||
capture_send_errors_ = 0;
|
||||
capture_max_gap_us_ = capture_send_us_ = capture_max_send_us_ = 0;
|
||||
const int fps = owner_.config_center_->GetVideoFrameRate() ==
|
||||
ConfigCenter::VIDEO_FRAME_RATE::FPS_30
|
||||
? 30
|
||||
@@ -103,7 +156,15 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
const char *display_name,
|
||||
const MiniRtcNativeVideoFrame *native_frame) {
|
||||
const auto now_time = std::chrono::steady_clock::now();
|
||||
bool from_secure_desktop = false;
|
||||
#ifdef _WIN32
|
||||
from_secure_desktop =
|
||||
ScreenCapturerWin::CurrentFrameIsFromSecureDesktop();
|
||||
#endif
|
||||
RecordCaptureCadence(now_time, fps, from_secure_desktop);
|
||||
if (!ShouldSendCapturedFrame(now_time, fps)) {
|
||||
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
|
||||
++capture_rate_drops_;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,9 +231,24 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(owner_.peer_);
|
||||
frame.native_frame = native_frame;
|
||||
const auto send_started = std::chrono::steady_clock::now();
|
||||
uint64_t send_errors = 0;
|
||||
for (const std::string &remote_id : connected_remote_ids) {
|
||||
SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size());
|
||||
if (SendVideoFrameToPeer(owner_.peer_, &frame, stream_id.c_str(),
|
||||
remote_id.data(), remote_id.size()) != 0)
|
||||
++send_errors;
|
||||
}
|
||||
const auto send_us =
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now() - send_started)
|
||||
.count();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_metrics_mutex_);
|
||||
++capture_forwarded_;
|
||||
capture_send_errors_ += send_errors;
|
||||
capture_send_us_ += send_us;
|
||||
capture_max_send_us_ =
|
||||
std::max<int64_t>(capture_max_send_us_, send_us);
|
||||
}
|
||||
last_video_frame_stream_id_ = stream_id;
|
||||
last_frame_time_ = now_time;
|
||||
|
||||
@@ -84,6 +84,8 @@ private:
|
||||
void ClearCapturedKeyboardInput();
|
||||
bool ShouldSendCapturedFrame(std::chrono::steady_clock::time_point now,
|
||||
int fps);
|
||||
void RecordCaptureCadence(std::chrono::steady_clock::time_point now, int fps,
|
||||
bool from_secure_desktop);
|
||||
|
||||
GuiRuntime &owner_;
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
@@ -99,6 +101,14 @@ private:
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
std::chrono::steady_clock::time_point last_frame_time_{};
|
||||
std::chrono::steady_clock::time_point next_frame_deadline_{};
|
||||
std::mutex capture_metrics_mutex_;
|
||||
std::chrono::steady_clock::time_point capture_metrics_started_{};
|
||||
std::chrono::steady_clock::time_point last_capture_callback_{};
|
||||
uint64_t capture_callbacks_ = 0, capture_rate_drops_ = 0;
|
||||
uint64_t capture_secure_callbacks_ = 0;
|
||||
uint64_t capture_forwarded_ = 0, capture_send_errors_ = 0;
|
||||
int64_t capture_max_gap_us_ = 0;
|
||||
int64_t capture_send_us_ = 0, capture_max_send_us_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool invalid_video_stream_id_logged_ = false;
|
||||
};
|
||||
|
||||
@@ -6,37 +6,12 @@
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include "platform/windows/input/windows_cursor_shape.h"
|
||||
#include "platform/windows/screen_capturer/dxgi_cursor_state.h"
|
||||
#include "platform/windows/screen_capturer/secure_desktop_cursor_state.h"
|
||||
#include "runtime/cursor_position.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
bool IsSystemCursor(HCURSOR cursor, LPCWSTR resource) {
|
||||
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
|
||||
}
|
||||
|
||||
RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
|
||||
if (IsSystemCursor(cursor, IDC_HELP)) return RemoteCursorShape::help;
|
||||
if (IsSystemCursor(cursor, IDC_HAND)) return RemoteCursorShape::pointer;
|
||||
if (IsSystemCursor(cursor, IDC_APPSTARTING))
|
||||
return RemoteCursorShape::progress;
|
||||
if (IsSystemCursor(cursor, IDC_WAIT)) return RemoteCursorShape::wait;
|
||||
if (IsSystemCursor(cursor, IDC_CROSS)) return RemoteCursorShape::crosshair;
|
||||
if (IsSystemCursor(cursor, IDC_IBEAM)) return RemoteCursorShape::text;
|
||||
if (IsSystemCursor(cursor, IDC_NO))
|
||||
return RemoteCursorShape::not_allowed;
|
||||
if (IsSystemCursor(cursor, IDC_SIZEALL)) return RemoteCursorShape::move;
|
||||
if (IsSystemCursor(cursor, IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
|
||||
if (IsSystemCursor(cursor, IDC_SIZENS)) return RemoteCursorShape::ns_resize;
|
||||
if (IsSystemCursor(cursor, IDC_SIZENESW))
|
||||
return RemoteCursorShape::nesw_resize;
|
||||
if (IsSystemCursor(cursor, IDC_SIZENWSE))
|
||||
return RemoteCursorShape::nwse_resize;
|
||||
if (IsSystemCursor(cursor, IDC_UPARROW)) return RemoteCursorShape::n_resize;
|
||||
return RemoteCursorShape::default_cursor;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct CursorStateProvider::Impl {};
|
||||
|
||||
@@ -47,16 +22,33 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||
int preferred_display, CursorState* state) {
|
||||
if (!state) return false;
|
||||
|
||||
SecureDesktopCursorSnapshot secure_cursor{};
|
||||
if (SharedSecureDesktopCursorState().Get(&secure_cursor)) {
|
||||
if (!secure_cursor.valid) return false;
|
||||
state->seq = 0;
|
||||
NormalizeCursorPosition(secure_cursor.x, secure_cursor.y, displays,
|
||||
preferred_display, state);
|
||||
state->visible = secure_cursor.visible != 0;
|
||||
state->shape = state->visible
|
||||
? static_cast<RemoteCursorShape>(secure_cursor.shape)
|
||||
: RemoteCursorShape::none;
|
||||
return true;
|
||||
}
|
||||
|
||||
CURSORINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
if (!GetCursorInfo(&info)) return false;
|
||||
|
||||
state->seq = 0;
|
||||
state->visible = (info.flags & CURSOR_SHOWING) != 0;
|
||||
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
||||
: RemoteCursorShape::none;
|
||||
NormalizeCursorPosition(info.ptScreenPos.x, info.ptScreenPos.y, displays,
|
||||
preferred_display, state);
|
||||
void* cursor_monitor = state->position_valid
|
||||
? displays[state->display_id].handle
|
||||
: nullptr;
|
||||
state->visible = SharedDxgiCursorState().ShouldDrawCursor(
|
||||
(info.flags & CURSOR_SHOWING) != 0, cursor_monitor);
|
||||
state->shape = state->visible ? ShapeFromWindowsCursor(info.hCursor)
|
||||
: RemoteCursorShape::none;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "rd_log.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -9,26 +8,38 @@ namespace {
|
||||
|
||||
std::string g_log_dir = "logs";
|
||||
std::once_flag g_logger_once_flag;
|
||||
std::mutex g_logger_mutex;
|
||||
std::shared_ptr<spdlog::logger> g_logger;
|
||||
std::atomic<bool> g_logger_created{false};
|
||||
|
||||
std::string NormalizeLogDirectory(const std::string& log_dir) {
|
||||
const std::filesystem::path path = log_dir.empty() ? "logs" : log_dir;
|
||||
std::error_code ec;
|
||||
const auto absolute = std::filesystem::absolute(path, ec);
|
||||
return (ec ? path : absolute).lexically_normal().string();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitLogger(const std::string& log_dir) {
|
||||
if (g_logger_created.load()) {
|
||||
LOG_WARN(
|
||||
"InitLogger called after logger initialized. Ignoring log_dir: {}, "
|
||||
"using previous log_dir: {}",
|
||||
log_dir, g_log_dir);
|
||||
const auto directory = NormalizeLogDirectory(log_dir);
|
||||
std::lock_guard<std::mutex> lock(g_logger_mutex);
|
||||
if (g_logger) {
|
||||
if (directory != g_log_dir) {
|
||||
g_logger->warn(
|
||||
"InitLogger called after logger initialized. Ignoring log_dir: {}, "
|
||||
"using previous log_dir: {}",
|
||||
directory, g_log_dir);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
g_log_dir = log_dir;
|
||||
g_log_dir = directory;
|
||||
}
|
||||
|
||||
std::shared_ptr<spdlog::logger> get_logger() {
|
||||
std::call_once(g_logger_once_flag, []() {
|
||||
g_logger_created.store(true);
|
||||
std::lock_guard<std::mutex> lock(g_logger_mutex);
|
||||
g_log_dir = NormalizeLogDirectory(g_log_dir);
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(g_log_dir, ec);
|
||||
@@ -54,10 +65,12 @@ std::shared_ptr<spdlog::logger> get_logger() {
|
||||
sinks.push_back(std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
|
||||
filename, 5 * 1024 * 1024, 3));
|
||||
|
||||
g_logger = std::make_shared<spdlog::logger>(LOGGER_NAME, sinks.begin(),
|
||||
sinks.end());
|
||||
g_logger->flush_on(spdlog::level::info);
|
||||
spdlog::register_logger(g_logger);
|
||||
auto logger = std::make_shared<spdlog::logger>(LOGGER_NAME, sinks.begin(),
|
||||
sinks.end());
|
||||
logger->flush_on(spdlog::level::info);
|
||||
spdlog::register_logger(logger);
|
||||
logger->info("Logger initialized: path={}", filename);
|
||||
g_logger = std::move(logger);
|
||||
});
|
||||
|
||||
return g_logger;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef CROSSDESK_WINDOWS_CURSOR_SHAPE_H_
|
||||
#define CROSSDESK_WINDOWS_CURSOR_SHAPE_H_
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <remote_cursor_shape.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline RemoteCursorShape ShapeFromWindowsCursor(HCURSOR cursor) {
|
||||
const auto is_system_cursor = [cursor](LPCWSTR resource) {
|
||||
return cursor != nullptr && cursor == LoadCursorW(nullptr, resource);
|
||||
};
|
||||
if (is_system_cursor(IDC_HELP)) return RemoteCursorShape::help;
|
||||
if (is_system_cursor(IDC_HAND)) return RemoteCursorShape::pointer;
|
||||
if (is_system_cursor(IDC_APPSTARTING)) return RemoteCursorShape::progress;
|
||||
if (is_system_cursor(IDC_WAIT)) return RemoteCursorShape::wait;
|
||||
if (is_system_cursor(IDC_CROSS)) return RemoteCursorShape::crosshair;
|
||||
if (is_system_cursor(IDC_IBEAM)) return RemoteCursorShape::text;
|
||||
if (is_system_cursor(IDC_NO)) return RemoteCursorShape::not_allowed;
|
||||
if (is_system_cursor(IDC_SIZEALL)) return RemoteCursorShape::move;
|
||||
if (is_system_cursor(IDC_SIZEWE)) return RemoteCursorShape::ew_resize;
|
||||
if (is_system_cursor(IDC_SIZENS)) return RemoteCursorShape::ns_resize;
|
||||
if (is_system_cursor(IDC_SIZENESW)) return RemoteCursorShape::nesw_resize;
|
||||
if (is_system_cursor(IDC_SIZENWSE)) return RemoteCursorShape::nwse_resize;
|
||||
if (is_system_cursor(IDC_UPARROW)) return RemoteCursorShape::n_resize;
|
||||
return RemoteCursorShape::default_cursor;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_WINDOWS_CURSOR_SHAPE_H_
|
||||
@@ -0,0 +1,48 @@
|
||||
#ifndef CROSSDESK_DXGI_CURSOR_STATE_H_
|
||||
#define CROSSDESK_DXGI_CURSOR_STATE_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// DXGI's Visible flag describes the separate hardware pointer, not the
|
||||
// system cursor. When it is false, a visible system cursor can already be
|
||||
// included in the desktop texture (for example, during window dragging).
|
||||
// Share that distinction with the GUI thread without changing cursor input
|
||||
// coordinates or the wire protocol.
|
||||
class DxgiCursorState {
|
||||
public:
|
||||
void Update(int64_t last_mouse_update_time, bool separate_pointer_visible,
|
||||
void* monitor) {
|
||||
// PointerPosition is undefined on desktop-only updates. Retain the last
|
||||
// pointer state until DXGI actually reports another pointer update.
|
||||
if (last_mouse_update_time == 0) return;
|
||||
embedded_monitor_.store(separate_pointer_visible ? nullptr : monitor,
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
embedded_monitor_.store(nullptr, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
bool ShouldDrawCursor(bool system_cursor_visible, void* cursor_monitor) const {
|
||||
// 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 &&
|
||||
(!cursor_monitor ||
|
||||
embedded_monitor_.load(std::memory_order_relaxed) != cursor_monitor);
|
||||
}
|
||||
|
||||
private:
|
||||
std::atomic<void*> embedded_monitor_{nullptr};
|
||||
};
|
||||
|
||||
inline DxgiCursorState& SharedDxgiCursorState() {
|
||||
static DxgiCursorState state;
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_DXGI_CURSOR_STATE_H_
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dxgi_cursor_state.h"
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -232,6 +233,7 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
SharedDxgiCursorState().Reset();
|
||||
if (monitor_index < 0 || monitor_index >= (int)outputs_.size()) return false;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutput1> output1;
|
||||
HRESULT hr = outputs_[monitor_index]->QueryInterface(
|
||||
@@ -284,6 +286,7 @@ bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
SharedDxgiCursorState().Reset();
|
||||
++duplication_generation_;
|
||||
staging_.Reset();
|
||||
if (duplication_) {
|
||||
@@ -344,6 +347,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
// a backend rebuild. Normal error/reset handling releases optional privacy.
|
||||
cached_frame_valid = false;
|
||||
if (FAILED(hr)) {
|
||||
SharedDxgiCursorState().Reset();
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
capture_lock.unlock();
|
||||
if (callback_)
|
||||
@@ -352,6 +356,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (frame_monitor >= 0 &&
|
||||
frame_monitor < static_cast<int>(display_info_list_.size())) {
|
||||
SharedDxgiCursorState().Update(
|
||||
frame_info.LastMouseUpdateTime.QuadPart,
|
||||
frame_info.PointerPosition.Visible != FALSE,
|
||||
display_info_list_[frame_monitor].handle);
|
||||
}
|
||||
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> acquired_tex;
|
||||
if (desktop_resource) {
|
||||
hr = desktop_resource->QueryInterface(
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "captured_nv12_frame.h"
|
||||
#include "dxgi_cursor_state.h"
|
||||
#include "interactive_state.h"
|
||||
#include "named_pipe_deadline.h"
|
||||
#include "rd_log.h"
|
||||
#include "screen_capturer_dxgi.h"
|
||||
#include "screen_capturer_gdi.h"
|
||||
#include "secure_desktop_cursor_state.h"
|
||||
#include "secure_desktop_frame_schedule.h"
|
||||
#include "secure_desktop_status_poller.h"
|
||||
#include "service_host.h"
|
||||
@@ -33,6 +35,8 @@ namespace {
|
||||
|
||||
using Json = nlohmann::json;
|
||||
|
||||
thread_local bool current_frame_from_secure_desktop = false;
|
||||
|
||||
constexpr DWORD kSecureDesktopStatusIntervalMs = 250;
|
||||
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500;
|
||||
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
|
||||
@@ -222,6 +226,7 @@ bool ParseSecureDesktopFrameResponse(const std::vector<uint8_t>& response,
|
||||
*width_out = static_cast<int>(header.width);
|
||||
*height_out = static_cast<int>(header.height);
|
||||
nv12_frame_out->assign(response.begin() + sizeof(header), response.end());
|
||||
SharedSecureDesktopCursorState().Update(header.cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -362,6 +367,7 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
return;
|
||||
}
|
||||
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
|
||||
SharedDxgiCursorState().Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -460,10 +466,17 @@ int ScreenCapturerWin::Destroy() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::CurrentFrameIsFromSecureDesktop() {
|
||||
return current_frame_from_secure_desktop;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::EmitCapturedFrame(
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame,
|
||||
bool from_secure_desktop) {
|
||||
// Helper frames do not use DXGI's pointer plane. Do not carry normal-desktop
|
||||
// cursor suppression into the lock screen or UAC desktop.
|
||||
if (from_secure_desktop) SharedDxgiCursorState().Reset();
|
||||
// Secure-desktop capture takes priority; remove privacy without dropping
|
||||
// the helper frame or delaying the remote session.
|
||||
if (from_secure_desktop && privacy_ && privacy_->Engaged()) {
|
||||
@@ -473,6 +486,14 @@ void ScreenCapturerWin::EmitCapturedFrame(
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal and helper callbacks can originate on different threads during
|
||||
// desktop transitions. Keep diagnostics tied to this callback's source.
|
||||
struct RestoreFrameSource {
|
||||
bool previous;
|
||||
~RestoreFrameSource() { current_frame_from_secure_desktop = previous; }
|
||||
} restore_source{
|
||||
std::exchange(current_frame_from_secure_desktop, from_secure_desktop)};
|
||||
|
||||
if (native_frame) {
|
||||
if (!native_output_logged_.exchange(true, std::memory_order_relaxed)) {
|
||||
LOG_INFO("Windows capturer native frame output enabled (type={})",
|
||||
@@ -593,6 +614,7 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(true);
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
@@ -618,6 +640,7 @@ int ScreenCapturerWin::Stop() {
|
||||
ret = impl_->Stop();
|
||||
}
|
||||
StopSecureCaptureThread();
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
return ret;
|
||||
}
|
||||
@@ -999,6 +1022,9 @@ bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
|
||||
}
|
||||
|
||||
nv12_frame_out->resize(payload_size);
|
||||
const SecureDesktopCursorSnapshot cursor = header->cursor;
|
||||
const int captured_width = static_cast<int>(header->width);
|
||||
const int captured_height = static_cast<int>(header->height);
|
||||
std::memcpy(nv12_frame_out->data(), secure_frame_view_ + sizeof(*header),
|
||||
payload_size);
|
||||
MemoryBarrier();
|
||||
@@ -1009,8 +1035,9 @@ bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
|
||||
return false;
|
||||
}
|
||||
|
||||
*width_out = static_cast<int>(header->width);
|
||||
*height_out = static_cast<int>(header->height);
|
||||
*width_out = captured_width;
|
||||
*height_out = captured_height;
|
||||
SharedSecureDesktopCursorState().Update(cursor);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1111,6 +1138,18 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
max_status_ms = 0;
|
||||
};
|
||||
std::vector<uint8_t> secure_frame;
|
||||
ULONGLONG last_shared_error_tick = 0;
|
||||
auto report_shared_error = [&](const char* operation,
|
||||
const std::string& error) {
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
if (last_shared_error_tick == 0 || now - last_shared_error_tick >= 5000) {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure shared capture {} failed; using pipe "
|
||||
"fallback, stage='{}', session_id={}, error={}",
|
||||
operation, status.interactive_stage, status.active_session_id, error);
|
||||
last_shared_error_tick = now;
|
||||
}
|
||||
};
|
||||
|
||||
while (running_.load(std::memory_order_relaxed)) {
|
||||
// Privacy covers belong to the ordinary desktop. Drop privacy on a
|
||||
@@ -1153,6 +1192,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
|
||||
secure_desktop_capture_active_.store(status.capture_active,
|
||||
std::memory_order_relaxed);
|
||||
SharedSecureDesktopCursorState().SetActive(status.capture_active);
|
||||
if (status.capture_active != previous.capture_active ||
|
||||
status.interactive_stage != previous.interactive_stage ||
|
||||
status.active_session_id != previous.active_session_id ||
|
||||
@@ -1257,6 +1297,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
status.interactive_stage, status.interactive_desktop, show_cursor,
|
||||
shared_fps, &error_message);
|
||||
if (!shared_ready) {
|
||||
report_shared_error("start", error_message);
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
} else if (!was_started) {
|
||||
frame_schedule.Reset(GetTickCount64());
|
||||
@@ -1276,6 +1317,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
IsPendingSecureDesktopFrame(error_message);
|
||||
if (frame_pending) ++shared_waits;
|
||||
if (shared_ready && !frame_delivered && !frame_pending) {
|
||||
report_shared_error("read", error_message);
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
frame_schedule.OnStartFailure(GetTickCount64());
|
||||
}
|
||||
@@ -1339,6 +1381,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
report_stats(true);
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
SharedSecureDesktopCursorState().SetActive(false);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -48,6 +48,9 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
// Set before Init(), while capture is stopped.
|
||||
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
|
||||
// Query synchronously inside the frame callback, on the delivering thread.
|
||||
// This describes the actual frame, independently of service status polling.
|
||||
static bool CurrentFrameIsFromSecureDesktop();
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#ifndef CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||
#define CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Fixed-width IPC metadata. Cursor handles belong to the helper's desktop and
|
||||
// must be converted to a semantic shape before crossing the process boundary.
|
||||
struct SecureDesktopCursorSnapshot {
|
||||
uint32_t valid = 0;
|
||||
uint32_t visible = 0;
|
||||
uint32_t shape = 0;
|
||||
int32_t x = 0;
|
||||
int32_t y = 0;
|
||||
};
|
||||
static_assert(sizeof(SecureDesktopCursorSnapshot) == 20);
|
||||
|
||||
// The capture thread publishes helper samples for the GUI thread. While secure
|
||||
// capture is active, even an unavailable sample must not fall back to sampling
|
||||
// the ordinary desktop, which can report a hidden or stale cursor.
|
||||
class SecureDesktopCursorState {
|
||||
public:
|
||||
void SetActive(bool active) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (active_ != active) snapshot_ = {};
|
||||
active_ = active;
|
||||
}
|
||||
|
||||
void Update(const SecureDesktopCursorSnapshot& snapshot) {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (active_ && snapshot.valid) snapshot_ = snapshot;
|
||||
}
|
||||
|
||||
bool Get(SecureDesktopCursorSnapshot* snapshot) const {
|
||||
std::lock_guard<std::mutex> lock(mutex_);
|
||||
if (!active_ || !snapshot) return false;
|
||||
*snapshot = snapshot_;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::mutex mutex_;
|
||||
bool active_ = false;
|
||||
SecureDesktopCursorSnapshot snapshot_{};
|
||||
};
|
||||
|
||||
inline SecureDesktopCursorState& SharedSecureDesktopCursorState() {
|
||||
static SecureDesktopCursorState state;
|
||||
return state;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_SECURE_DESKTOP_CURSOR_STATE_H_
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "windows_cursor_shape.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace {
|
||||
@@ -97,18 +98,18 @@ struct SecureSharedCaptureState {
|
||||
uint32_t sequence = 0;
|
||||
};
|
||||
|
||||
struct PipeSecurityAttributes {
|
||||
PipeSecurityAttributes() = default;
|
||||
~PipeSecurityAttributes() {
|
||||
struct IpcSecurityAttributes {
|
||||
IpcSecurityAttributes() = default;
|
||||
~IpcSecurityAttributes() {
|
||||
if (security_descriptor_ != nullptr) {
|
||||
LocalFree(security_descriptor_);
|
||||
}
|
||||
}
|
||||
|
||||
bool Initialize() {
|
||||
constexpr wchar_t kPipeSddl[] = L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)";
|
||||
bool Initialize(const wchar_t* sddl =
|
||||
L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)") {
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
kPipeSddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) {
|
||||
sddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -452,7 +453,7 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) {
|
||||
|
||||
void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id,
|
||||
HelperState* helper_state) {
|
||||
PipeSecurityAttributes security_attributes;
|
||||
IpcSecurityAttributes security_attributes;
|
||||
SECURITY_ATTRIBUTES* pipe_attributes = nullptr;
|
||||
if (security_attributes.Initialize()) {
|
||||
pipe_attributes = security_attributes.get();
|
||||
@@ -1135,6 +1136,7 @@ struct SecureDesktopGdiResources {
|
||||
void* bits = nullptr;
|
||||
const char* error = nullptr;
|
||||
DWORD error_code = ERROR_SUCCESS;
|
||||
crossdesk::SecureDesktopCursorSnapshot cursor_snapshot{};
|
||||
|
||||
~SecureDesktopGdiResources() {
|
||||
if (old_bitmap != nullptr && old_bitmap != HGDI_ERROR) {
|
||||
@@ -1183,16 +1185,30 @@ struct SecureDesktopGdiResources {
|
||||
request.left, request.top, SRCCOPY | CAPTUREBLT)) {
|
||||
return Fail("bitblt_failed");
|
||||
}
|
||||
if (request.show_cursor) {
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
if (GetCursorInfo(&cursor) && cursor.flags == CURSOR_SHOWING &&
|
||||
// Sample on the thread bound to the captured input desktop, including when
|
||||
// native controllers render the cursor themselves (show_cursor == false).
|
||||
cursor_snapshot = {};
|
||||
CURSORINFO cursor{};
|
||||
cursor.cbSize = sizeof(cursor);
|
||||
if (GetCursorInfo(&cursor)) {
|
||||
cursor_snapshot.valid = 1;
|
||||
cursor_snapshot.visible = (cursor.flags & CURSOR_SHOWING) != 0;
|
||||
cursor_snapshot.shape = static_cast<uint32_t>(
|
||||
crossdesk::ShapeFromWindowsCursor(cursor.hCursor));
|
||||
cursor_snapshot.x = cursor.ptScreenPos.x;
|
||||
cursor_snapshot.y = cursor.ptScreenPos.y;
|
||||
if (request.show_cursor && cursor_snapshot.visible &&
|
||||
cursor.hCursor != nullptr) {
|
||||
const int x = cursor.ptScreenPos.x - request.left;
|
||||
const int y = cursor.ptScreenPos.y - request.top;
|
||||
if (x >= -64 && y >= -64 && x < request.width + 64 &&
|
||||
y < request.height + 64) {
|
||||
DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr, DI_NORMAL);
|
||||
if (DrawIconEx(mem_dc, x, y, cursor.hCursor, 0, 0, 0, nullptr,
|
||||
DI_NORMAL)) {
|
||||
// A frame with an embedded cursor must not get a second cursor
|
||||
// overlay from a native controller sharing the web capture path.
|
||||
cursor_snapshot.visible = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1237,6 +1253,7 @@ std::vector<uint8_t> CaptureSecureDesktopFrame(
|
||||
header.height = static_cast<uint32_t>(request.height);
|
||||
header.payload_size =
|
||||
static_cast<uint32_t>(capture_buffers->nv12_frame.size());
|
||||
header.cursor = capture.cursor_snapshot;
|
||||
|
||||
std::vector<uint8_t> response(sizeof(header) +
|
||||
capture_buffers->nv12_frame.size());
|
||||
@@ -1362,6 +1379,7 @@ void SecureDesktopSharedCaptureThread(SecureSharedCaptureState* capture_state) {
|
||||
header->width = static_cast<uint32_t>(request.width);
|
||||
header->height = static_cast<uint32_t>(request.height);
|
||||
header->payload_size = static_cast<uint32_t>(nv12_frame.size());
|
||||
header->cursor = capture->cursor_snapshot;
|
||||
std::memcpy(payload, nv12_frame.data(), nv12_frame.size());
|
||||
header->sequence = ++capture_state->sequence;
|
||||
MemoryBarrier();
|
||||
@@ -1453,12 +1471,21 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
||||
return BuildTextResponseBytes(BuildErrorJson("invalid_capture_size"));
|
||||
}
|
||||
|
||||
PipeSecurityAttributes security_attributes;
|
||||
IpcSecurityAttributes security_attributes;
|
||||
SECURITY_ATTRIBUTES* attributes = nullptr;
|
||||
if (security_attributes.Initialize()) {
|
||||
attributes = security_attributes.get();
|
||||
}
|
||||
|
||||
// Event GENERIC_READ/GENERIC_WRITE omit SYNCHRONIZE. The GUI only needs
|
||||
// to wait for frames; the SYSTEM helper retains permission to signal them.
|
||||
IpcSecurityAttributes event_security_attributes;
|
||||
if (!event_security_attributes.Initialize(
|
||||
L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x00100000;;;AU)")) {
|
||||
return BuildTextResponseBytes(
|
||||
BuildErrorJson("create_frame_event_security_failed", GetLastError()));
|
||||
}
|
||||
|
||||
const std::wstring mapping_name =
|
||||
crossdesk::GetCrossDeskSecureDesktopFrameMappingName(
|
||||
capture_state->session_id);
|
||||
@@ -1485,7 +1512,8 @@ std::vector<uint8_t> StartSecureDesktopSharedCapture(
|
||||
}
|
||||
|
||||
HANDLE frame_ready_event =
|
||||
CreateEventW(attributes, FALSE, FALSE, event_name.c_str());
|
||||
CreateEventW(event_security_attributes.get(), FALSE, FALSE,
|
||||
event_name.c_str());
|
||||
if (frame_ready_event == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
UnmapViewOfFile(frame_view);
|
||||
@@ -1647,7 +1675,7 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) {
|
||||
auto capture_state = std::make_shared<SecureSharedCaptureState>();
|
||||
capture_state->session_id = session_id;
|
||||
|
||||
PipeSecurityAttributes security_attributes;
|
||||
IpcSecurityAttributes security_attributes;
|
||||
SECURITY_ATTRIBUTES* pipe_attributes = nullptr;
|
||||
if (security_attributes.Initialize()) {
|
||||
pipe_attributes = security_attributes.get();
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "secure_desktop_cursor_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr wchar_t kCrossDeskSessionHelperPipePrefix[] =
|
||||
@@ -33,7 +35,9 @@ inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
|
||||
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
|
||||
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
|
||||
// Version 2 adds cursor metadata to both headers. The GUI and local session
|
||||
// helper must be deployed together; the remote-control wire format is unchanged.
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 2;
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct CrossDeskSecureDesktopFrameHeader {
|
||||
@@ -44,6 +48,7 @@ struct CrossDeskSecureDesktopFrameHeader {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t payload_size;
|
||||
SecureDesktopCursorSnapshot cursor;
|
||||
};
|
||||
|
||||
struct CrossDeskSecureDesktopSharedFrameHeader {
|
||||
@@ -57,6 +62,7 @@ struct CrossDeskSecureDesktopSharedFrameHeader {
|
||||
uint32_t height;
|
||||
uint32_t payload_size;
|
||||
uint32_t buffer_size;
|
||||
SecureDesktopCursorSnapshot cursor;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
|
||||
@@ -459,7 +459,8 @@ function setup_targets()
|
||||
"apps/desktop/src/platform/windows/service/service_host.cpp")
|
||||
add_files("apps/desktop/resources/windows/crossdesk_service.rc",
|
||||
{defines = windows_resource_defines})
|
||||
add_includedirs("apps/desktop/src/platform/windows/service")
|
||||
add_includedirs("apps/desktop/src/platform/windows/service",
|
||||
"apps/desktop/src/platform/windows/screen_capturer")
|
||||
|
||||
target("crossdesk_session_helper")
|
||||
set_kind("binary")
|
||||
@@ -470,8 +471,10 @@ function setup_targets()
|
||||
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
||||
{defines = windows_helper_resource_defines})
|
||||
add_includedirs("apps/desktop/src/common",
|
||||
"apps/desktop/src/platform/windows/service")
|
||||
"apps/desktop/src/platform/windows/service",
|
||||
"apps/desktop/src/platform/windows/screen_capturer")
|
||||
add_includedirs("apps/desktop/src/platform/windows/input")
|
||||
add_includedirs("libs/wire/include")
|
||||
end
|
||||
|
||||
target("crossdesk")
|
||||
@@ -482,7 +485,8 @@ function setup_targets()
|
||||
if is_os("windows") then
|
||||
add_files("apps/desktop/src/platform/windows/daemon_backend.cpp")
|
||||
add_files("apps/desktop/src/platform/windows/service/service_host.cpp")
|
||||
add_includedirs("apps/desktop/src/platform/windows/service")
|
||||
add_includedirs("apps/desktop/src/platform/windows/service",
|
||||
"apps/desktop/src/platform/windows/screen_capturer")
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||
add_files(crossdesk_windows_resource, {defines = windows_resource_defines})
|
||||
|
||||
Vendored
+1
-1
Submodule deps/submodules/minirtc updated: 6931e63554...5b0ce34383
Reference in New Issue
Block a user