From ffa94986d55b7efdd2b385194bbe43d735818160 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 21 Apr 2026 04:10:08 +0800 Subject: [PATCH] [feat] add Windows secure desktop remote unlock support for locked sessions, refs #77 --- src/app/main.cpp | 150 ++ src/device_controller/device_controller.h | 38 + .../assets/localization/localization_data.h | 16 + src/gui/render.cpp | 200 ++ src/gui/render.h | 30 + src/gui/render_callback.cpp | 137 +- src/gui/toolbars/control_bar.cpp | 88 + src/gui/windows/stream_window.cpp | 58 + .../windows/screen_capturer_win.cpp | 513 ++++- .../windows/screen_capturer_win.h | 15 + src/service/windows/interactive_state.h | 35 + src/service/windows/main.cpp | 87 + src/service/windows/service_host.cpp | 2045 +++++++++++++++++ src/service/windows/service_host.h | 141 ++ src/service/windows/session_helper_main.cpp | 1123 +++++++++ src/service/windows/session_helper_shared.h | 51 + xmake/targets.lua | 25 +- 17 files changed, 4712 insertions(+), 40 deletions(-) create mode 100644 src/service/windows/interactive_state.h create mode 100644 src/service/windows/main.cpp create mode 100644 src/service/windows/service_host.cpp create mode 100644 src/service/windows/service_host.h create mode 100644 src/service/windows/session_helper_main.cpp create mode 100644 src/service/windows/session_helper_shared.h diff --git a/src/app/main.cpp b/src/app/main.cpp index dd8d5dd..d90a1f9 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -7,15 +7,165 @@ #endif #include +#include +#include #include #include +#ifdef _WIN32 +#include +#include "service_host.h" +#endif + #include "config_center.h" #include "daemon.h" #include "path_manager.h" #include "render.h" +#ifdef _WIN32 +namespace { + +void EnsureConsoleForCli() { + static bool console_ready = false; + if (console_ready) { + return; + } + + if (!AttachConsole(ATTACH_PARENT_PROCESS)) { + DWORD error = GetLastError(); + if (error != ERROR_ACCESS_DENIED) { + AllocConsole(); + } + } + + FILE* stream = nullptr; + freopen_s(&stream, "CONOUT$", "w", stdout); + freopen_s(&stream, "CONOUT$", "w", stderr); + freopen_s(&stream, "CONIN$", "r", stdin); + SetConsoleOutputCP(CP_UTF8); + console_ready = true; +} + +void PrintServiceCliUsage() { + std::cout + << "CrossDesk service management commands\n" + << " --service-install Install the sibling crossdesk_service.exe\n" + << " --service-uninstall Remove the installed Windows service\n" + << " --service-start Start the Windows service\n" + << " --service-stop Stop the Windows service\n" + << " --service-sas Ask the service to send Secure Attention Sequence\n" + << " --service-ping Ping the service over named pipe IPC\n" + << " --service-status Query service runtime status\n" + << " --service-help Show this help\n"; +} + +std::wstring GetCurrentExecutablePathW() { + wchar_t path[MAX_PATH] = {0}; + DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return L""; + } + return std::wstring(path, length); +} + +std::filesystem::path GetSiblingServiceExecutablePath() { + std::wstring current_executable = GetCurrentExecutablePathW(); + if (current_executable.empty()) { + return {}; + } + + return std::filesystem::path(current_executable).parent_path() / + L"crossdesk_service.exe"; +} + +bool IsServiceCliCommand(const char* arg) { + if (arg == nullptr) { + return false; + } + + return std::strcmp(arg, "--service-install") == 0 || + std::strcmp(arg, "--service-uninstall") == 0 || + std::strcmp(arg, "--service-start") == 0 || + std::strcmp(arg, "--service-stop") == 0 || + std::strcmp(arg, "--service-sas") == 0 || + std::strcmp(arg, "--service-ping") == 0 || + std::strcmp(arg, "--service-status") == 0 || + std::strcmp(arg, "--service-help") == 0; +} + +int HandleServiceCliCommand(const std::string& command) { + EnsureConsoleForCli(); + + if (command == "--service-help") { + PrintServiceCliUsage(); + return 0; + } + + if (command == "--service-install") { + std::filesystem::path service_path = GetSiblingServiceExecutablePath(); + if (service_path.empty()) { + std::cerr << "Failed to locate crossdesk_service.exe" << std::endl; + return 1; + } + if (!std::filesystem::exists(service_path)) { + std::cerr << "Service binary not found: " << service_path.string() + << std::endl; + return 1; + } + + bool success = crossdesk::InstallCrossDeskService(service_path.wstring()); + std::cout << (success ? "install ok" : "install failed") << std::endl; + return success ? 0 : 1; + } + + if (command == "--service-uninstall") { + bool success = crossdesk::UninstallCrossDeskService(); + std::cout << (success ? "uninstall ok" : "uninstall failed") + << std::endl; + return success ? 0 : 1; + } + + if (command == "--service-start") { + bool success = crossdesk::StartCrossDeskService(); + std::cout << (success ? "start ok" : "start failed") << std::endl; + return success ? 0 : 1; + } + + if (command == "--service-stop") { + bool success = crossdesk::StopCrossDeskService(); + std::cout << (success ? "stop ok" : "stop failed") << std::endl; + return success ? 0 : 1; + } + + if (command == "--service-sas") { + std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl; + return 0; + } + + if (command == "--service-ping") { + std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl; + return 0; + } + + if (command == "--service-status") { + std::cout << crossdesk::QueryCrossDeskService("status") << std::endl; + return 0; + } + + PrintServiceCliUsage(); + return 1; +} + +} // namespace +#endif + int main(int argc, char* argv[]) { +#ifdef _WIN32 + if (argc > 1 && IsServiceCliCommand(argv[1])) { + return HandleServiceCliCommand(argv[1]); + } +#endif + // check if running as child process bool is_child = false; for (int i = 1; i < argc; i++) { diff --git a/src/device_controller/device_controller.h b/src/device_controller/device_controller.h index a02a83b..fcfcbe6 100644 --- a/src/device_controller/device_controller.h +++ b/src/device_controller/device_controller.h @@ -7,6 +7,7 @@ #ifndef _DEVICE_CONTROLLER_H_ #define _DEVICE_CONTROLLER_H_ +#include #include #include @@ -23,6 +24,8 @@ typedef enum { audio_capture, host_infomation, display_id, + service_status, + service_command, } ControlType; typedef enum { move = 0, @@ -36,6 +39,7 @@ typedef enum { wheel_horizontal } MouseFlag; typedef enum { key_down = 0, key_up } KeyFlag; +typedef enum { send_sas = 0 } ServiceCommandFlag; typedef struct { float x; float y; @@ -59,6 +63,15 @@ typedef struct { int* bottom; } HostInfo; +typedef struct { + bool available; + char interactive_stage[32]; +} ServiceStatus; + +typedef struct { + ServiceCommandFlag flag; +} ServiceCommand; + struct RemoteAction { ControlType type; union { @@ -67,6 +80,8 @@ struct RemoteAction { HostInfo i; bool a; int d; + ServiceStatus ss; + ServiceCommand c; }; // parse @@ -96,6 +111,14 @@ struct RemoteAction { case ControlType::display_id: j["display_id"] = a.d; break; + case ControlType::service_status: + j["service_status"] = {{"available", a.ss.available}, + {"interactive_stage", + a.ss.interactive_stage}}; + break; + case ControlType::service_command: + j["service_command"] = {{"flag", a.c.flag}}; + break; case ControlType::host_infomation: { json displays = json::array(); for (size_t idx = 0; idx < a.i.display_num; idx++) { @@ -137,6 +160,21 @@ struct RemoteAction { case ControlType::display_id: out.d = j.at("display_id").get(); break; + case ControlType::service_status: { + const auto& service_status_json = j.at("service_status"); + out.ss.available = service_status_json.value("available", false); + std::string interactive_stage = + service_status_json.value("interactive_stage", std::string()); + std::strncpy(out.ss.interactive_stage, interactive_stage.c_str(), + sizeof(out.ss.interactive_stage) - 1); + out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] = + '\0'; + break; + } + case ControlType::service_command: + out.c.flag = static_cast( + j.at("service_command").at("flag").get()); + break; case ControlType::host_infomation: { std::string host_name = j.at("host_info").at("host_name").get(); diff --git a/src/gui/assets/localization/localization_data.h b/src/gui/assets/localization/localization_data.h index 17ddb40..76ea5f8 100644 --- a/src/gui/assets/localization/localization_data.h +++ b/src/gui/assets/localization/localization_data.h @@ -51,6 +51,22 @@ struct TranslationRow { X(release_mouse, u8"释放", "Release", u8"Освободить") \ X(audio_capture, u8"声音", "Audio", u8"Звук") \ X(mute, u8" 静音", " Mute", u8"Без звука") \ + X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \ + X(remote_password_box_visible, u8"远端密码框已出现", \ + "Remote password box visible", u8"Окно ввода пароля видно") \ + X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \ + "Remote lock screen visible, send SAS", \ + u8"Видна блокировка, отправьте SAS") \ + X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ + "Remote secure desktop active", \ + u8"Активен защищенный рабочий стол") \ + X(remote_service_unavailable, u8"远端Windows服务不可用", \ + "Remote Windows service unavailable", \ + u8"Служба Windows на удаленной стороне недоступна") \ + X(remote_unlock_requires_secure_desktop, \ + u8"当前仍需要安全桌面专用采集/输入", \ + "Secure desktop capture/input is still required", \ + u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего стола") \ X(settings, u8"设置", "Settings", u8"Настройки") \ X(language, u8"语言:", "Language:", u8"Язык:") \ X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 85f070d..8192b56 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -28,6 +28,10 @@ #include "screen_capturer_factory.h" #include "version_checker.h" +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif #if defined(__APPLE__) #include "window_util_mac.h" @@ -75,6 +79,68 @@ HICON LoadTrayIcon() { return LoadIconW(nullptr, IDI_APPLICATION); } + +struct WindowsServiceInteractiveStatus { + bool available = false; + unsigned int error_code = 0; + std::string interactive_stage; + std::string error; +}; + +constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000; +constexpr DWORD kWindowsServiceQueryTimeoutMs = 100; +constexpr DWORD kWindowsServiceSasTimeoutMs = 500; + +RemoteAction BuildWindowsServiceStatusAction( + const WindowsServiceInteractiveStatus& status) { + RemoteAction action{}; + action.type = ControlType::service_status; + action.ss.available = status.available; + std::strncpy(action.ss.interactive_stage, status.interactive_stage.c_str(), + sizeof(action.ss.interactive_stage) - 1); + action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = + '\0'; + return action; +} + +bool QueryWindowsServiceInteractiveStatus( + WindowsServiceInteractiveStatus* status) { + if (status == nullptr) { + return false; + } + + *status = WindowsServiceInteractiveStatus{}; + const std::string response = + QueryCrossDeskService("status", kWindowsServiceQueryTimeoutMs); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.is_object()) { + status->error = "invalid_service_status_json"; + return false; + } + + status->available = json.value("ok", false); + if (!status->available) { + status->error = json.value("error", std::string("service_unavailable")); + status->error_code = json.value("code", 0u); + return true; + } + + status->interactive_stage = json.value("interactive_stage", std::string()); + + if (ShouldNormalizeUnlockToUserDesktop( + json.value("interactive_lock_screen_visible", false), + status->interactive_stage, json.value("session_locked", false), + json.value("interactive_logon_ui_visible", false), + json.value("interactive_secure_desktop_active", + json.value("secure_desktop_active", false)), + json.value("credential_ui_visible", false), + json.value("password_box_visible", false), + json.value("unlock_ui_visible", false), + json.value("last_session_event", std::string()))) { + status->interactive_stage = "user-desktop"; + } + return true; +} #endif #if defined(__linux__) && !defined(__APPLE__) @@ -1746,6 +1812,7 @@ void Render::MainLoop() { HandlePendingPresenceProbe(); HandleStreamWindow(); HandleServerWindow(); + HandleWindowsServiceIntegration(); DrawMainWindow(); if (stream_window_inited_) { @@ -1772,6 +1839,139 @@ void Render::UpdateLabels() { } } +void Render::ResetRemoteServiceStatus(SubStreamWindowProperties& props) { + props.remote_service_status_received_ = false; + props.remote_service_available_ = false; + props.remote_interactive_stage_.clear(); +} + +void Render::ApplyRemoteServiceStatus(SubStreamWindowProperties& props, + const ServiceStatus& status) { + props.remote_service_status_received_ = true; + props.remote_service_available_ = status.available; + props.remote_interactive_stage_ = status.interactive_stage; +} + +Render::RemoteUnlockState Render::GetRemoteUnlockState( + const SubStreamWindowProperties& props) const { + if (!props.remote_service_status_received_) { + return RemoteUnlockState::none; + } + if (!props.remote_service_available_) { + return RemoteUnlockState::service_unavailable; + } + if (props.remote_interactive_stage_ == "credential-ui") { + return RemoteUnlockState::credential_ui; + } + if (props.remote_interactive_stage_ == "lock-screen") { + return RemoteUnlockState::lock_screen; + } + if (props.remote_interactive_stage_ == "secure-desktop") { + return RemoteUnlockState::secure_desktop; + } + return RemoteUnlockState::none; +} + +void Render::HandleWindowsServiceIntegration() { +#if _WIN32 + static bool last_logged_service_available = true; + static unsigned int last_logged_service_error_code = 0; + static std::string last_logged_service_error; + + if (!is_server_mode_ || peer_ == nullptr) { + ResetLocalWindowsServiceState(true); + return; + } + + const bool has_connected_remote = std::any_of( + connection_status_.begin(), connection_status_.end(), + [](const auto& entry) { + return entry.second == ConnectionStatus::Connected; + }); + if (!has_connected_remote) { + ResetLocalWindowsServiceState(false); + return; + } + + bool force_broadcast = false; + if (pending_windows_service_sas_.exchange(false, + std::memory_order_relaxed)) { + const std::string response = + QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.value("ok", false)) { + LOG_WARN("Remote SAS request failed: {}", response); + } else { + LOG_INFO("Remote SAS request forwarded to local Windows service"); + } + last_windows_service_status_tick_ = 0; + force_broadcast = true; + } + + const uint32_t now = static_cast(SDL_GetTicks()); + if (!force_broadcast && last_windows_service_status_tick_ != 0 && + now - last_windows_service_status_tick_ < + kWindowsServiceStatusIntervalMs) { + return; + } + last_windows_service_status_tick_ = now; + + WindowsServiceInteractiveStatus status; + const bool status_ok = QueryWindowsServiceInteractiveStatus(&status); + local_service_status_received_ = status_ok; + local_service_available_ = status.available; + local_interactive_stage_ = status.available ? status.interactive_stage : ""; + + if (status_ok) { + const bool availability_changed = + status.available != last_logged_service_available; + const bool error_changed = + !status.available && + (status.error != last_logged_service_error || + status.error_code != last_logged_service_error_code); + if (availability_changed || error_changed) { + if (status.available) { + LOG_INFO("Local Windows service available for secure desktop integration"); + } else { + LOG_WARN( + "Local Windows service unavailable, secure desktop integration disabled: error={}, code={}", + status.error, status.error_code); + } + last_logged_service_available = status.available; + last_logged_service_error = status.error; + last_logged_service_error_code = status.error_code; + } + } else if (last_logged_service_available || + last_logged_service_error != "invalid_service_status_json") { + LOG_WARN( + "Local Windows service status query failed, secure desktop integration disabled"); + last_logged_service_available = false; + last_logged_service_error = "invalid_service_status_json"; + last_logged_service_error_code = 0; + } + + RemoteAction remote_action = BuildWindowsServiceStatusAction(status); + std::string msg = remote_action.to_json(); + int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(), + control_data_label_.c_str()); + if (ret != 0) { + LOG_WARN("Broadcast Windows service status failed, ret={}", ret); + } +#endif +} + +#if _WIN32 +void Render::ResetLocalWindowsServiceState(bool clear_pending_sas) { + last_windows_service_status_tick_ = 0; + if (clear_pending_sas) { + pending_windows_service_sas_.store(false, std::memory_order_relaxed); + } + local_service_status_received_ = false; + local_service_available_ = false; + local_interactive_stage_.clear(); +} +#endif + void Render::HandleRecentConnections() { if (reload_recent_connections_ && main_renderer_) { uint32_t now_time = SDL_GetTicks(); diff --git a/src/gui/render.h b/src/gui/render.h index fee335b..35e1ee5 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -44,6 +44,14 @@ namespace crossdesk { class Render { public: + enum class RemoteUnlockState { + none, + service_unavailable, + lock_screen, + credential_ui, + secure_desktop, + }; + struct FileTransferState { std::atomic file_sending_ = false; std::atomic file_sent_bytes_ = 0; @@ -159,6 +167,9 @@ class Render { std::string mouse_control_button_label_ = "Mouse Control"; std::string audio_capture_button_label_ = "Audio Capture"; std::string remote_host_name_ = ""; + bool remote_service_status_received_ = false; + bool remote_service_available_ = false; + std::string remote_interactive_stage_ = ""; std::vector display_info_list_; SDL_Texture* stream_texture_ = nullptr; uint8_t* argb_buffer_ = nullptr; @@ -271,6 +282,13 @@ class Render { std::shared_ptr& props); void DrawReceivingScreenText( std::shared_ptr& props); + void DrawRemoteUnlockStateText( + std::shared_ptr& props); + void ResetRemoteServiceStatus(SubStreamWindowProperties& props); + void ApplyRemoteServiceStatus(SubStreamWindowProperties& props, + const ServiceStatus& status); + RemoteUnlockState GetRemoteUnlockState( + const SubStreamWindowProperties& props) const; #ifdef __APPLE__ int RequestPermissionWindow(); bool CheckScreenRecordingPermission(); @@ -359,6 +377,10 @@ class Render { int AudioDeviceInit(); int AudioDeviceDestroy(); + void HandleWindowsServiceIntegration(); +#if _WIN32 + void ResetLocalWindowsServiceState(bool clear_pending_sas); +#endif private: struct CDCache { @@ -515,6 +537,14 @@ class Render { SDL_Event last_mouse_event; SDL_AudioStream* output_stream_; uint32_t STREAM_REFRESH_EVENT = 0; +#if _WIN32 + std::atomic pending_windows_service_sas_{false}; + bool local_service_status_received_ = false; + bool local_service_available_ = false; + std::string local_interactive_stage_; + uint32_t last_local_secure_input_block_log_tick_ = 0; + uint32_t last_windows_service_status_tick_ = 0; +#endif // stream window render SDL_Window* stream_window_ = nullptr; diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index 9e1364a..35cc2ec 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -17,11 +17,63 @@ #include "platform.h" #include "rd_log.h" #include "render.h" +#if _WIN32 +#include "interactive_state.h" +#include "service_host.h" +#endif #define NV12_BUFFER_SIZE 1280 * 720 * 3 / 2 namespace crossdesk { +namespace { + +#if _WIN32 +constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; + +bool BuildAbsoluteMousePosition(const std::vector& displays, + int display_index, float normalized_x, + float normalized_y, int* absolute_x_out, + int* absolute_y_out) { + if (absolute_x_out == nullptr || absolute_y_out == nullptr || + display_index < 0 || + display_index >= static_cast(displays.size())) { + return false; + } + + const DisplayInfo& display = displays[display_index]; + if (display.width <= 0 || display.height <= 0) { + return false; + } + + const float clamped_x = std::clamp(normalized_x, 0.0f, 1.0f); + const float clamped_y = std::clamp(normalized_y, 0.0f, 1.0f); + *absolute_x_out = static_cast(clamped_x * display.width) + display.left; + *absolute_y_out = static_cast(clamped_y * display.height) + display.top; + return true; +} + +void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* side, + const char* stage) { + if (last_tick == nullptr) { + return; + } + + const uint32_t now = static_cast(SDL_GetTicks()); + if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) { + return; + } + + *last_tick = now; + LOG_WARN( + "{} secure-desktop input blocked, stage={}, normal SendInput path " + "cannot drive the Windows password UI", + side != nullptr ? side : "unknown", stage != nullptr ? stage : ""); +} +#endif + +} // namespace + void Render::OnSignalMessageCb(const char* message, size_t size, void* user_data) { Render* render = (Render*)user_data; @@ -709,16 +761,31 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size, } std::string json_str(data, size); - RemoteAction remote_action; - - try { - remote_action.from_json(json_str); - } catch (const std::exception& e) { - LOG_ERROR("Failed to parse RemoteAction JSON: {}", e.what()); + RemoteAction remote_action{}; + if (!remote_action.from_json(json_str)) { + LOG_ERROR("Failed to parse RemoteAction JSON payload"); return; } std::string remote_id(user_id, user_id_size); + if (remote_action.type == ControlType::service_status) { + auto props_it = render->client_properties_.find(remote_id); + if (props_it != render->client_properties_.end()) { + render->ApplyRemoteServiceStatus(*props_it->second, remote_action.ss); + } + return; + } + + if (remote_action.type == ControlType::service_command) { +#if _WIN32 + if (remote_action.c.flag == ServiceCommandFlag::send_sas) { + render->pending_windows_service_sas_.store(true, + std::memory_order_relaxed); + } +#endif + return; + } + // std::shared_lock lock(render->client_properties_mutex_); if (remote_action.type == ControlType::host_infomation) { if (render->client_properties_.find(remote_id) != @@ -748,6 +815,59 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size, } } else { // remote +#if _WIN32 + if (render->local_service_status_received_ && + render->local_service_available_ && + IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) { + if (remote_action.type == ControlType::mouse) { + int absolute_x = 0; + int absolute_y = 0; + if (!BuildAbsoluteMousePosition(render->display_info_list_, + render->selected_display_, + remote_action.m.x, remote_action.m.y, + &absolute_x, &absolute_y)) { + LOG_WARN( + "Secure desktop mouse injection skipped, invalid display mapping: display_index={}, x={}, y={}", + render->selected_display_, remote_action.m.x, + remote_action.m.y); + return; + } + + const std::string response = SendCrossDeskSecureDesktopMouseInput( + absolute_x, absolute_y, remote_action.m.s, + static_cast(remote_action.m.flag), 1000); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.value("ok", false)) { + LogSecureDesktopInputBlocked( + &render->last_local_secure_input_block_log_tick_, "local", + render->local_interactive_stage_.c_str()); + LOG_WARN( + "Secure desktop mouse injection failed, x={}, y={}, wheel={}, flag={}, response={}", + absolute_x, absolute_y, remote_action.m.s, + static_cast(remote_action.m.flag), response); + } + return; + } + + if (remote_action.type == ControlType::keyboard) { + const int key_code = static_cast(remote_action.k.key_value); + const bool is_down = remote_action.k.flag == KeyFlag::key_down; + const std::string response = + SendCrossDeskSecureDesktopKeyInput(key_code, is_down, 1000); + auto json = nlohmann::json::parse(response, nullptr, false); + if (json.is_discarded() || !json.value("ok", false)) { + LogSecureDesktopInputBlocked( + &render->last_local_secure_input_block_log_tick_, "local", + render->local_interactive_stage_.c_str()); + LOG_WARN( + "Secure desktop keyboard injection failed, key_code={}, " + "is_down={}, response={}", + key_code, is_down, response); + } + return; + } + } +#endif if (remote_action.type == ControlType::mouse && render->mouse_controller_) { render->mouse_controller_->SendMouseCommand(remote_action, render->selected_display_); @@ -841,6 +961,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, switch (status) { case ConnectionStatus::Connected: { + render->ResetRemoteServiceStatus(*props); { RemoteAction remote_action; remote_action.i.display_num = render->display_info_list_.size(); @@ -904,6 +1025,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, case ConnectionStatus::Closed: { props->connection_established_ = false; props->enable_mouse_control_ = false; + render->ResetRemoteServiceStatus(*props); { std::lock_guard lock(props->video_frame_mutex_); @@ -954,6 +1076,9 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, switch (status) { case ConnectionStatus::Connected: { +#if _WIN32 + render->last_windows_service_status_tick_ = 0; +#endif { RemoteAction remote_action; remote_action.i.display_num = render->display_info_list_.size(); diff --git a/src/gui/toolbars/control_bar.cpp b/src/gui/toolbars/control_bar.cpp index 9f0cf30..c820058 100644 --- a/src/gui/toolbars/control_bar.cpp +++ b/src/gui/toolbars/control_bar.cpp @@ -193,6 +193,94 @@ int Render::ControlBar(std::shared_ptr& props) { text_pos, IM_COL32(0, 0, 0, 255), std::to_string(props->selected_display_ + 1).c_str()); + if (props->remote_service_status_received_) { + ImGui::SameLine(); + const RemoteUnlockState unlock_state = GetRemoteUnlockState(*props); + bool sas_button_style_pushed = false; + switch (unlock_state) { + case RemoteUnlockState::service_unavailable: + ImGui::PushStyleColor(ImGuiCol_Button, + ImVec4(185 / 255.0f, 28 / 255.0f, + 28 / 255.0f, 1.0f)); + sas_button_style_pushed = true; + break; + case RemoteUnlockState::credential_ui: + ImGui::PushStyleColor(ImGuiCol_Button, + ImVec4(22 / 255.0f, 163 / 255.0f, + 74 / 255.0f, 1.0f)); + sas_button_style_pushed = true; + break; + case RemoteUnlockState::lock_screen: + ImGui::PushStyleColor(ImGuiCol_Button, + ImVec4(202 / 255.0f, 138 / 255.0f, + 4 / 255.0f, 1.0f)); + sas_button_style_pushed = true; + break; + default: + break; + } + + const bool can_send_sas = + props->connection_status_ == ConnectionStatus::Connected && + props->peer_ != nullptr && props->remote_service_available_; + if (!can_send_sas) { + ImGui::BeginDisabled(); + } + + std::string sas_button = ICON_FA_UNLOCK_KEYHOLE; + ImGui::SetWindowFontScale(0.5f); + if (ImGui::Button(sas_button.c_str(), + ImVec2(button_width, button_height))) { + RemoteAction remote_action{}; + remote_action.type = ControlType::service_command; + remote_action.c.flag = ServiceCommandFlag::send_sas; + std::string msg = remote_action.to_json(); + SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(), + props->control_data_label_.c_str()); + } + + if (!can_send_sas) { + ImGui::EndDisabled(); + } + + if (ImGui::IsItemHovered()) { + std::string tooltip = localization::send_sas[localization_language_index_]; + switch (unlock_state) { + case RemoteUnlockState::service_unavailable: + tooltip = localization::remote_service_unavailable + [localization_language_index_]; + break; + case RemoteUnlockState::credential_ui: + tooltip = localization::remote_password_box_visible + [localization_language_index_] + + "\n" + + localization::remote_unlock_requires_secure_desktop + [localization_language_index_]; + break; + case RemoteUnlockState::lock_screen: + tooltip = localization::remote_lock_screen_hint + [localization_language_index_]; + break; + case RemoteUnlockState::secure_desktop: + tooltip = localization::remote_secure_desktop_active + [localization_language_index_]; + break; + default: + break; + } + + ImGui::BeginTooltip(); + ImGui::PushTextWrapPos(button_width * 8.0f); + ImGui::TextWrapped("%s", tooltip.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + if (sas_button_style_pushed) { + ImGui::PopStyleColor(); + } + } + ImGui::SameLine(); float mouse_x = ImGui::GetCursorScreenPos().x; float mouse_y = ImGui::GetCursorScreenPos().y; diff --git a/src/gui/windows/stream_window.cpp b/src/gui/windows/stream_window.cpp index 06e13d7..44addbd 100644 --- a/src/gui/windows/stream_window.cpp +++ b/src/gui/windows/stream_window.cpp @@ -59,6 +59,62 @@ void Render::DrawReceivingScreenText( ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.92f), "%s", text.c_str()); } +void Render::DrawRemoteUnlockStateText( + std::shared_ptr& props) { + if (!props->remote_service_status_received_ || + !props->connection_established_ || + props->connection_status_ != ConnectionStatus::Connected) { + return; + } + + const RemoteUnlockState unlock_state = GetRemoteUnlockState(*props); + std::string text; + ImU32 background_color = IM_COL32(37, 99, 235, 220); + + switch (unlock_state) { + case RemoteUnlockState::service_unavailable: + text = localization::remote_service_unavailable + [localization_language_index_]; + background_color = IM_COL32(185, 28, 28, 220); + break; + case RemoteUnlockState::credential_ui: + text = localization::remote_password_box_visible + [localization_language_index_]; + background_color = IM_COL32(22, 163, 74, 220); + break; + case RemoteUnlockState::lock_screen: + text = localization::remote_lock_screen_hint + [localization_language_index_]; + background_color = IM_COL32(202, 138, 4, 220); + break; + case RemoteUnlockState::secure_desktop: + text = localization::remote_secure_desktop_active + [localization_language_index_]; + background_color = IM_COL32(147, 51, 234, 220); + break; + default: + return; + } + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + ImVec2 window_pos = ImGui::GetWindowPos(); + ImVec2 window_size = ImGui::GetWindowSize(); + ImVec2 text_size = ImGui::CalcTextSize(text.c_str()); + float padding_x = title_bar_height_ * 0.45f; + float padding_y = title_bar_height_ * 0.18f; + float top_margin = fullscreen_button_pressed_ ? title_bar_height_ * 0.35f + : title_bar_height_ * 0.18f; + ImVec2 text_pos(window_pos.x + (window_size.x - text_size.x) * 0.5f, + window_pos.y + top_margin + padding_y); + ImVec2 rect_min(text_pos.x - padding_x, text_pos.y - padding_y); + ImVec2 rect_max(text_pos.x + text_size.x + padding_x, + text_pos.y + text_size.y + padding_y); + + draw_list->AddRectFilled(rect_min, rect_max, background_color, + window_rounding_ * 0.9f); + draw_list->AddText(text_pos, IM_COL32(255, 255, 255, 255), text.c_str()); +} + void Render::CloseTab(decltype(client_properties_)::iterator& it) { // std::unique_lock lock(client_properties_mutex_); if (it != client_properties_.end()) { @@ -173,6 +229,7 @@ int Render::StreamWindow() { FileTransferWindow(props); DrawReceivingScreenText(props); + DrawRemoteUnlockStateText(props); focused_remote_id_ = props->remote_id_; @@ -275,6 +332,7 @@ int Render::StreamWindow() { FileTransferWindow(props); DrawReceivingScreenText(props); + DrawRemoteUnlockStateText(props); ImGui::End(); diff --git a/src/screen_capturer/windows/screen_capturer_win.cpp b/src/screen_capturer/windows/screen_capturer_win.cpp index 9235bb2..d3477ee 100644 --- a/src/screen_capturer/windows/screen_capturer_win.cpp +++ b/src/screen_capturer/windows/screen_capturer_win.cpp @@ -2,22 +2,50 @@ #include +#include + #include +#include +#include #include #include +#include #include +#include #include #include #include "rd_log.h" #include "screen_capturer_dxgi.h" #include "screen_capturer_gdi.h" +#include "interactive_state.h" +#include "service_host.h" +#include "session_helper_shared.h" #include "wgc_plugin_api.h" namespace crossdesk { namespace { +using Json = nlohmann::json; + +constexpr DWORD kSecureDesktopStatusIntervalMs = 250; +constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 150; +constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120; +constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500; +constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000; +constexpr int kSecureDesktopCaptureMinIntervalMs = 100; + +struct SecureDesktopServiceStatus { + bool service_available = false; + bool capture_active = false; + bool helper_running = false; + DWORD active_session_id = 0xFFFFFFFF; + DWORD error_code = 0; + std::string interactive_stage; + std::string error; +}; + class WgcPluginCapturer final : public ScreenCapturer { public: using CreateFn = ScreenCapturer* (*)(); @@ -101,6 +129,213 @@ class WgcPluginCapturer final : public ScreenCapturer { DestroyFn destroy_fn_ = nullptr; }; +std::string BuildSecureCaptureCommand(int left, int top, int width, int height, + bool show_cursor) { + std::ostringstream stream; + stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top + << ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0); + return stream.str(); +} + +std::string ExtractPipeTextResponse(const std::vector& response) { + if (response.empty() || response.front() != '{') { + return ""; + } + return std::string(response.begin(), response.end()); +} + +bool IsTransientSecureDesktopFrameError(const std::string& error_message) { + return error_message.rfind("pipe_unavailable:", 0) == 0 || + error_message.find("\"error\":\"bitblt_failed\"") != + std::string::npos; +} + +bool ReadPipeMessage(HANDLE pipe, std::vector* response_out, + DWORD* error_code_out = nullptr) { + if (response_out == nullptr) { + return false; + } + + response_out->clear(); + if (error_code_out != nullptr) { + *error_code_out = 0; + } + + std::vector chunk(64 * 1024); + while (true) { + DWORD bytes_read = 0; + if (ReadFile(pipe, chunk.data(), static_cast(chunk.size()), + &bytes_read, nullptr)) { + response_out->insert(response_out->end(), chunk.begin(), + chunk.begin() + bytes_read); + return true; + } + + const DWORD error = GetLastError(); + response_out->insert(response_out->end(), chunk.begin(), + chunk.begin() + bytes_read); + if (error == ERROR_MORE_DATA) { + continue; + } + + if (error_code_out != nullptr) { + *error_code_out = error; + } + return false; + } +} + +bool ParseSecureDesktopFrameResponse(const std::vector& response, + std::vector* nv12_frame_out, + int* width_out, int* height_out, + std::string* error_out) { + if (nv12_frame_out == nullptr || width_out == nullptr || + height_out == nullptr) { + return false; + } + + if (response.size() < sizeof(CrossDeskSecureDesktopFrameHeader)) { + if (error_out != nullptr) { + *error_out = ExtractPipeTextResponse(response); + } + return false; + } + + CrossDeskSecureDesktopFrameHeader header{}; + std::memcpy(&header, response.data(), sizeof(header)); + if (header.magic != kCrossDeskSecureDesktopFrameMagic || + header.version != kCrossDeskSecureDesktopFrameVersion) { + if (error_out != nullptr) { + *error_out = ExtractPipeTextResponse(response); + } + return false; + } + + const size_t expected_size = sizeof(header) + header.payload_size; + if (expected_size != response.size()) { + if (error_out != nullptr) { + *error_out = ""; + } + return false; + } + + *width_out = static_cast(header.width); + *height_out = static_cast(header.height); + nv12_frame_out->assign(response.begin() + sizeof(header), response.end()); + return true; +} + +bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) { + if (status == nullptr) { + return false; + } + + *status = {}; + const std::string response = + QueryCrossDeskService("status", kSecureDesktopStatusPipeTimeoutMs); + Json json = Json::parse(response, nullptr, false); + if (json.is_discarded() || !json.is_object()) { + status->error = "invalid_service_status_json"; + return false; + } + + status->service_available = json.value("ok", false); + if (!status->service_available) { + status->error = json.value("error", std::string("service_unavailable")); + status->error_code = json.value("code", 0u); + return true; + } + + if (ShouldNormalizeUnlockToUserDesktop( + json.value("interactive_lock_screen_visible", false), + json.value("interactive_stage", std::string()), + json.value("session_locked", false), + json.value("interactive_logon_ui_visible", false), + json.value("interactive_secure_desktop_active", + json.value("secure_desktop_active", false)), + json.value("credential_ui_visible", false), + json.value("password_box_visible", false), + json.value("unlock_ui_visible", false), + json.value("last_session_event", std::string()))) { + status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu); + status->interactive_stage = "user-desktop"; + status->capture_active = false; + return true; + } + + status->active_session_id = json.value("active_session_id", 0xFFFFFFFFu); + status->helper_running = json.value("secure_input_helper_running", false); + status->interactive_stage = json.value("interactive_stage", std::string()); + const bool secure_desktop_active = json.value( + "interactive_secure_desktop_active", + json.value("secure_desktop_active", false)); + status->capture_active = + status->active_session_id != 0xFFFFFFFF && + (secure_desktop_active || + IsSecureDesktopInteractionRequired(status->interactive_stage)); + return true; +} + +bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top, + int width, int height, bool show_cursor, + std::vector* nv12_frame_out, + int* captured_width_out, + int* captured_height_out, + std::string* error_out) { + if (nv12_frame_out == nullptr || captured_width_out == nullptr || + captured_height_out == nullptr) { + return false; + } + + const std::wstring pipe_name = GetCrossDeskSecureInputHelperPipeName(session_id); + if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) { + if (error_out != nullptr) { + *error_out = "pipe_unavailable:" + std::to_string(GetLastError()); + } + return false; + } + + HANDLE pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + if (pipe == INVALID_HANDLE_VALUE) { + if (error_out != nullptr) { + *error_out = "pipe_connect_failed:" + std::to_string(GetLastError()); + } + return false; + } + + DWORD pipe_mode = PIPE_READMODE_MESSAGE; + SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr); + + const std::string command = + BuildSecureCaptureCommand(left, top, width, height, show_cursor); + DWORD bytes_written = 0; + if (!WriteFile(pipe, command.data(), static_cast(command.size()), + &bytes_written, nullptr)) { + const DWORD error = GetLastError(); + CloseHandle(pipe); + if (error_out != nullptr) { + *error_out = "pipe_write_failed:" + std::to_string(error); + } + return false; + } + + std::vector response; + DWORD read_error = 0; + const bool read_ok = ReadPipeMessage(pipe, &response, &read_error); + CloseHandle(pipe); + if (!read_ok) { + if (error_out != nullptr) { + *error_out = "pipe_read_failed:" + std::to_string(read_error); + } + return false; + } + + return ParseSecureDesktopFrameResponse(response, nv12_frame_out, + captured_width_out, + captured_height_out, error_out); +} + } // namespace ScreenCapturerWin::ScreenCapturerWin() {} @@ -111,6 +346,10 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { cb_orig_ = cb; cb_ = [this](unsigned char* data, int size, int w, int h, const char* display_name) { + if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) { + return; + } + std::string mapped_name; { std::lock_guard lock(alias_mutex_); @@ -137,6 +376,8 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { if (ret == 0) { LOG_INFO("Windows capturer: using WGC plugin"); BuildCanonicalFromImpl(); + monitor_index_.store(0, std::memory_order_relaxed); + initial_monitor_index_ = 0; return 0; } @@ -150,6 +391,8 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { if (ret == 0) { LOG_INFO("Windows capturer: using DXGI Desktop Duplication"); BuildCanonicalFromImpl(); + monitor_index_.store(0, std::memory_order_relaxed); + initial_monitor_index_ = 0; return 0; } @@ -162,6 +405,8 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { if (ret == 0) { LOG_INFO("Windows capturer: using GDI BitBlt"); BuildCanonicalFromImpl(); + monitor_index_.store(0, std::memory_order_relaxed); + initial_monitor_index_ = 0; return 0; } @@ -171,6 +416,10 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { } int ScreenCapturerWin::Destroy() { + StopSecureCaptureThread(); + running_.store(false, std::memory_order_relaxed); + paused_.store(false, std::memory_order_relaxed); + secure_desktop_capture_active_.store(false, std::memory_order_relaxed); if (impl_) { impl_->Destroy(); impl_.reset(); @@ -187,67 +436,100 @@ int ScreenCapturerWin::Destroy() { int ScreenCapturerWin::Start(bool show_cursor) { if (!impl_) return -1; + if (running_.load(std::memory_order_relaxed)) { + return 0; + } + + show_cursor_.store(show_cursor, std::memory_order_relaxed); + paused_.store(false, std::memory_order_relaxed); + int ret = impl_->Start(show_cursor); - if (ret == 0) return 0; + if (ret != 0) { + LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret); - LOG_WARN("Windows capturer: Start failed (ret={}), trying fallback", ret); + auto try_init_start = [&](std::unique_ptr cand) -> bool { + int r = cand->Init(fps_, cb_); + if (r != 0) return false; + int s = cand->Start(show_cursor); + if (s == 0) { + impl_ = std::move(cand); + impl_is_wgc_plugin_ = false; + RebuildAliasesFromImpl(); + return true; + } + return false; + }; - auto try_init_start = [&](std::unique_ptr cand) -> bool { - int r = cand->Init(fps_, cb_); - if (r != 0) return false; - int s = cand->Start(show_cursor); - if (s == 0) { - impl_ = std::move(cand); - impl_is_wgc_plugin_ = false; - RebuildAliasesFromImpl(); - return true; + bool fallback_started = false; + if (impl_is_wgc_plugin_) { + if (try_init_start(std::make_unique())) { + LOG_INFO("Windows capturer: fallback to DXGI"); + fallback_started = true; + } else if (try_init_start(std::make_unique())) { + LOG_INFO("Windows capturer: fallback to GDI"); + fallback_started = true; + } + } else if (dynamic_cast(impl_.get())) { + if (try_init_start(std::make_unique())) { + LOG_INFO("Windows capturer: fallback to GDI"); + fallback_started = true; + } } - return false; - }; - if (impl_is_wgc_plugin_) { - if (try_init_start(std::make_unique())) { - LOG_INFO("Windows capturer: fallback to DXGI"); - return 0; - } - if (try_init_start(std::make_unique())) { - LOG_INFO("Windows capturer: fallback to GDI"); - return 0; - } - } else if (dynamic_cast(impl_.get())) { - if (try_init_start(std::make_unique())) { - LOG_INFO("Windows capturer: fallback to GDI"); - return 0; + if (!fallback_started) { + LOG_ERROR("Windows capturer: all fallbacks failed to start"); + return ret; } } - LOG_ERROR("Windows capturer: all fallbacks failed to start"); - return ret; + running_.store(true, std::memory_order_relaxed); + secure_desktop_capture_active_.store(false, std::memory_order_relaxed); + if (!secure_capture_thread_.joinable()) { + secure_capture_thread_ = + std::thread([this]() { SecureDesktopCaptureLoop(); }); + } + return 0; } int ScreenCapturerWin::Stop() { - if (!impl_) return 0; - return impl_->Stop(); + running_.store(false, std::memory_order_relaxed); + secure_desktop_capture_active_.store(false, std::memory_order_relaxed); + int ret = 0; + if (impl_) { + ret = impl_->Stop(); + } + StopSecureCaptureThread(); + return ret; } int ScreenCapturerWin::Pause(int monitor_index) { + paused_.store(true, std::memory_order_relaxed); if (!impl_) return -1; return impl_->Pause(monitor_index); } int ScreenCapturerWin::Resume(int monitor_index) { + paused_.store(false, std::memory_order_relaxed); if (!impl_) return -1; return impl_->Resume(monitor_index); } int ScreenCapturerWin::SwitchTo(int monitor_index) { if (!impl_) return -1; - return impl_->SwitchTo(monitor_index); + const int ret = impl_->SwitchTo(monitor_index); + if (ret == 0) { + monitor_index_.store(monitor_index, std::memory_order_relaxed); + } + return ret; } int ScreenCapturerWin::ResetToInitialMonitor() { if (!impl_) return -1; - return impl_->ResetToInitialMonitor(); + const int ret = impl_->ResetToInitialMonitor(); + if (ret == 0) { + monitor_index_.store(initial_monitor_index_, std::memory_order_relaxed); + } + return ret; } std::vector ScreenCapturerWin::GetDisplayInfoList() { @@ -297,4 +579,171 @@ void ScreenCapturerWin::RebuildAliasesFromImpl() { } } +void ScreenCapturerWin::StopSecureCaptureThread() { + if (secure_capture_thread_.joinable()) { + secure_capture_thread_.join(); + } +} + +bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, + int* width, int* height, + std::string* display_name) { + if (left == nullptr || top == nullptr || width == nullptr || + height == nullptr || display_name == nullptr) { + return false; + } + + std::lock_guard lock(alias_mutex_); + if (canonical_displays_.empty()) { + return false; + } + + int current_monitor = monitor_index_.load(std::memory_order_relaxed); + if (current_monitor < 0 || + current_monitor >= static_cast(canonical_displays_.size())) { + current_monitor = 0; + } + + const auto& display = canonical_displays_[current_monitor]; + const int capture_width = display.width & ~1; + const int capture_height = display.height & ~1; + if (capture_width <= 0 || capture_height <= 0) { + return false; + } + + *left = display.left; + *top = display.top; + *width = capture_width; + *height = capture_height; + *display_name = display.name; + return true; +} + +void ScreenCapturerWin::SecureDesktopCaptureLoop() { + const int frame_interval_ms = + fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinIntervalMs, 1000 / fps_) + : kSecureDesktopCaptureMinIntervalMs; + ULONGLONG last_status_tick = 0; + ULONGLONG last_error_tick = 0; + bool last_capture_active = false; + bool last_service_available = true; + std::string last_stage; + std::string last_service_error; + ULONGLONG capture_stage_started_tick = 0; + SecureDesktopServiceStatus status; + std::vector secure_frame; + + while (running_.load(std::memory_order_relaxed)) { + if (paused_.load(std::memory_order_relaxed)) { + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + continue; + } + + const ULONGLONG now = GetTickCount64(); + if (last_status_tick == 0 || + now - last_status_tick >= kSecureDesktopStatusIntervalMs) { + SecureDesktopServiceStatus latest_status; + const bool status_ok = QuerySecureDesktopServiceStatus(&latest_status); + status = latest_status; + if (status_ok) { + const bool service_changed = + status.service_available != last_service_available; + const bool service_error_changed = + !status.service_available && status.error != last_service_error; + if (service_changed || service_error_changed) { + if (status.service_available) { + LOG_INFO( + "Windows capturer secure desktop service available, polling session_id={}", + status.active_session_id); + } else { + LOG_WARN( + "Windows capturer secure desktop service unavailable: error={}, code={}", + status.error, status.error_code); + } + last_service_available = status.service_available; + last_service_error = status.error; + } + } else if (last_service_available || + last_service_error != "invalid_service_status_json") { + LOG_WARN( + "Windows capturer secure desktop service status query failed"); + last_service_available = false; + last_service_error = "invalid_service_status_json"; + } + + secure_desktop_capture_active_.store(status.capture_active, + std::memory_order_relaxed); + if (status.capture_active != last_capture_active || + status.interactive_stage != last_stage) { + capture_stage_started_tick = now; + LOG_INFO( + "Windows capturer secure desktop state: active={}, stage='{}', session_id={}", + status.capture_active, status.interactive_stage, + status.active_session_id); + last_capture_active = status.capture_active; + last_stage = status.interactive_stage; + } + last_status_tick = now; + } + + if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { + std::this_thread::sleep_for(std::chrono::milliseconds( + status.service_available ? 50 : 200)); + continue; + } + + if (!status.helper_running) { + std::this_thread::sleep_for(std::chrono::milliseconds(30)); + continue; + } + + int left = 0; + int top = 0; + int width = 0; + int height = 0; + std::string display_name; + if (!GetCurrentCaptureRegion(&left, &top, &width, &height, &display_name)) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + continue; + } + + int captured_width = 0; + int captured_height = 0; + std::string error_message; + if (QuerySecureDesktopHelperFrame( + status.active_session_id, left, top, width, height, + show_cursor_.load(std::memory_order_relaxed), &secure_frame, + &captured_width, &captured_height, &error_message)) { + if (cb_orig_ && !secure_frame.empty()) { + cb_orig_(secure_frame.data(), static_cast(secure_frame.size()), + captured_width, captured_height, display_name.c_str()); + } + } else { + const bool transient_error = + IsTransientSecureDesktopFrameError(error_message); + const bool in_grace_period = + capture_stage_started_tick != 0 && + now - capture_stage_started_tick < kSecureDesktopTransientErrorGraceMs; + const DWORD log_interval = transient_error + ? kSecureDesktopTransientErrorLogIntervalMs + : 1000; + if (transient_error && in_grace_period) { + std::this_thread::sleep_for( + std::chrono::milliseconds(frame_interval_ms)); + continue; + } + if (now - last_error_tick >= log_interval) { + LOG_WARN( + "Windows capturer secure desktop frame query failed, stage='{}', session_id={}, error={}", + status.interactive_stage, status.active_session_id, error_message); + last_error_tick = now; + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms)); + } + + secure_desktop_capture_active_.store(false, std::memory_order_relaxed); +} + } // namespace crossdesk \ No newline at end of file diff --git a/src/screen_capturer/windows/screen_capturer_win.h b/src/screen_capturer/windows/screen_capturer_win.h index 4605e93..b6cb5d8 100644 --- a/src/screen_capturer/windows/screen_capturer_win.h +++ b/src/screen_capturer/windows/screen_capturer_win.h @@ -7,8 +7,12 @@ #ifndef _SCREEN_CAPTURER_WIN_H_ #define _SCREEN_CAPTURER_WIN_H_ +#include + +#include #include #include +#include #include #include #include @@ -48,9 +52,20 @@ class ScreenCapturerWin : public ScreenCapturer { std::mutex alias_mutex_; std::vector canonical_displays_; std::unordered_set canonical_labels_; + std::atomic running_{false}; + std::atomic paused_{false}; + std::atomic show_cursor_{true}; + std::atomic monitor_index_{0}; + int initial_monitor_index_ = 0; + std::atomic secure_desktop_capture_active_{false}; + std::thread secure_capture_thread_; void BuildCanonicalFromImpl(); void RebuildAliasesFromImpl(); + void StopSecureCaptureThread(); + void SecureDesktopCaptureLoop(); + bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height, + std::string* display_name); }; } // namespace crossdesk #endif \ No newline at end of file diff --git a/src/service/windows/interactive_state.h b/src/service/windows/interactive_state.h new file mode 100644 index 0000000..1c36708 --- /dev/null +++ b/src/service/windows/interactive_state.h @@ -0,0 +1,35 @@ +#ifndef _CROSSDESK_INTERACTIVE_STATE_H_ +#define _CROSSDESK_INTERACTIVE_STATE_H_ + +#include + +namespace crossdesk { + +inline bool IsSecureDesktopInteractionRequired( + const std::string& interactive_stage) { + return interactive_stage == "credential-ui" || + interactive_stage == "secure-desktop"; +} + +inline bool ShouldNormalizeUnlockToUserDesktop( + bool interactive_lock_screen_visible, + const std::string& interactive_stage, bool session_locked, + bool interactive_logon_ui_visible, bool interactive_secure_desktop_active, + bool credential_ui_visible, bool password_box_visible, + bool unlock_ui_visible, const std::string& last_session_event) { + if (!interactive_lock_screen_visible && interactive_stage != "lock-screen") { + return false; + } + + if (session_locked || interactive_logon_ui_visible || + interactive_secure_desktop_active || credential_ui_visible || + password_box_visible || unlock_ui_visible) { + return false; + } + + return last_session_event != "session-lock"; +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/src/service/windows/main.cpp b/src/service/windows/main.cpp new file mode 100644 index 0000000..8d88458 --- /dev/null +++ b/src/service/windows/main.cpp @@ -0,0 +1,87 @@ +#include + +#include +#include + +#include "service_host.h" + +namespace { + +std::wstring GetExecutablePath() { + wchar_t path[MAX_PATH] = {0}; + DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return L""; + } + return std::wstring(path, length); +} + +void PrintUsage() { + std::cout << "CrossDesk Windows service skeleton\n" + << " --service Run under the Windows Service Control Manager\n" + << " --console Run the service loop in console mode\n" + << " --install Install the service for the current executable\n" + << " --uninstall Remove the installed service\n" + << " --start Start the installed service\n" + << " --stop Stop the installed service\n" + << " --sas Ask the service to send Secure Attention Sequence\n" + << " --ping Ping the running service over named pipe IPC\n" + << " --status Query runtime status over named pipe IPC\n"; +} + +} // namespace + +int main(int argc, char* argv[]) { + crossdesk::CrossDeskServiceHost host; + + if (argc <= 1) { + PrintUsage(); + return 0; + } + + std::string command = argv[1]; + if (command == "--service") { + return host.RunAsService(); + } + if (command == "--console") { + return host.RunInConsole(); + } + if (command == "--install") { + std::wstring executable_path = GetExecutablePath(); + bool success = !executable_path.empty() && + crossdesk::InstallCrossDeskService(executable_path); + std::cout << (success ? "install ok" : "install failed") << std::endl; + return success ? 0 : 1; + } + if (command == "--uninstall") { + bool success = crossdesk::UninstallCrossDeskService(); + std::cout << (success ? "uninstall ok" : "uninstall failed") + << std::endl; + return success ? 0 : 1; + } + if (command == "--start") { + bool success = crossdesk::StartCrossDeskService(); + std::cout << (success ? "start ok" : "start failed") << std::endl; + return success ? 0 : 1; + } + if (command == "--stop") { + bool success = crossdesk::StopCrossDeskService(); + std::cout << (success ? "stop ok" : "stop failed") << std::endl; + return success ? 0 : 1; + } + if (command == "--sas") { + std::cout << crossdesk::QueryCrossDeskService("sas") << std::endl; + return 0; + } + if (command == "--ping") { + std::cout << crossdesk::QueryCrossDeskService("ping") << std::endl; + return 0; + } + if (command == "--status") { + std::cout << crossdesk::QueryCrossDeskService("status") << std::endl; + return 0; + } + + PrintUsage(); + return 1; +} \ No newline at end of file diff --git a/src/service/windows/service_host.cpp b/src/service/windows/service_host.cpp new file mode 100644 index 0000000..9b5f4dc --- /dev/null +++ b/src/service/windows/service_host.cpp @@ -0,0 +1,2045 @@ +#include "service_host.h" + +#include + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "path_manager.h" +#include "rd_log.h" +#include "session_helper_shared.h" + +namespace crossdesk { + +namespace { + +using Json = nlohmann::json; + +constexpr char kSecureDesktopKeyboardIpcCommandPrefix[] = + "secure-input-key:"; +constexpr char kSecureDesktopMouseIpcCommandPrefix[] = + "secure-input-mouse:"; + +using SendSasFunction = VOID(WINAPI*)(BOOL); + +struct SasResult { + bool success = false; + DWORD error_code = 0; + std::string error; +}; + +struct InputDesktopInfo { + bool available = false; + DWORD error_code = 0; + std::string name; +}; + +struct ScopedEnvironmentBlock { + ~ScopedEnvironmentBlock() { + if (environment != nullptr) { + DestroyEnvironmentBlock(environment); + } + } + + LPVOID environment = nullptr; +}; + +std::string WideToUtf8(const std::wstring& value); + +std::wstring GetCurrentExecutablePathW() { + wchar_t path[MAX_PATH] = {0}; + DWORD length = GetModuleFileNameW(nullptr, path, MAX_PATH); + if (length == 0 || length >= MAX_PATH) { + return L""; + } + return std::wstring(path, length); +} + +std::wstring QuoteWindowsArgument(const std::wstring& value) { + std::wstring escaped = L"\""; + for (wchar_t character : value) { + if (character == L'\"') { + escaped += L"\\\""; + continue; + } + escaped += character; + } + escaped += L"\""; + return escaped; +} + +void InitializeServiceLogger() { + static std::once_flag once_flag; + std::call_once(once_flag, []() { + PathManager path_manager("CrossDesk"); + std::filesystem::path log_path = path_manager.GetLogPath() / "service"; + if (!log_path.empty() && path_manager.CreateDirectories(log_path)) { + InitLogger(log_path.string()); + return; + } + InitLogger("logs/service"); + }); +} + +std::string EscapeJsonString(const std::string& value) { + std::string escaped; + escaped.reserve(value.size()); + for (char character : value) { + switch (character) { + case '\\': + escaped += "\\\\"; + break; + case '"': + escaped += "\\\""; + break; + case '\b': + escaped += "\\b"; + break; + case '\f': + escaped += "\\f"; + break; + case '\n': + escaped += "\\n"; + break; + case '\r': + escaped += "\\r"; + break; + case '\t': + escaped += "\\t"; + break; + default: + escaped += character; + break; + } + } + return escaped; +} + +std::string Trim(const std::string& value) { + size_t begin = 0; + while (begin < value.size() && + std::isspace(static_cast(value[begin])) != 0) { + ++begin; + } + + size_t end = value.size(); + while (end > begin && + std::isspace(static_cast(value[end - 1])) != 0) { + --end; + } + + return value.substr(begin, end - begin); +} + +std::string ToLower(std::string value) { + std::transform(value.begin(), value.end(), value.begin(), + [](unsigned char character) { + return static_cast(std::tolower(character)); + }); + return value; +} + +std::string BuildErrorJson(const char* error, DWORD error_code = 0) { + std::ostringstream stream; + stream << "{\"ok\":false,\"error\":\"" << error << "\""; + if (error_code != 0) { + stream << ",\"code\":" << error_code; + } + stream << "}"; + return stream.str(); +} + +std::string QueryNamedPipeMessage(const std::wstring& pipe_name, + const std::string& command, + DWORD timeout_ms) { + constexpr int kPipeConnectRetryCount = 3; + constexpr DWORD kPipeConnectRetryDelayMs = 15; + + auto is_transient_pipe_error = [](DWORD error) { + return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY || + error == ERROR_SEM_TIMEOUT; + }; + + HANDLE pipe = INVALID_HANDLE_VALUE; + for (int attempt = 0; attempt < kPipeConnectRetryCount; ++attempt) { + if (!WaitNamedPipeW(pipe_name.c_str(), timeout_ms)) { + const DWORD error = GetLastError(); + if (attempt + 1 < kPipeConnectRetryCount && + is_transient_pipe_error(error)) { + Sleep(kPipeConnectRetryDelayMs); + continue; + } + return BuildErrorJson("pipe_unavailable", error); + } + + pipe = CreateFileW(pipe_name.c_str(), GENERIC_READ | GENERIC_WRITE, 0, + nullptr, OPEN_EXISTING, 0, nullptr); + if (pipe != INVALID_HANDLE_VALUE) { + break; + } + + const DWORD error = GetLastError(); + if (attempt + 1 < kPipeConnectRetryCount && + is_transient_pipe_error(error)) { + Sleep(kPipeConnectRetryDelayMs); + continue; + } + return BuildErrorJson("pipe_connect_failed", error); + } + + DWORD pipe_mode = PIPE_READMODE_MESSAGE; + SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr); + + DWORD bytes_written = 0; + if (!WriteFile(pipe, command.data(), static_cast(command.size()), + &bytes_written, nullptr)) { + std::string error = BuildErrorJson("pipe_write_failed", GetLastError()); + CloseHandle(pipe); + return error; + } + + char buffer[4096] = {0}; + DWORD bytes_read = 0; + if (!ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr)) { + std::string error = BuildErrorJson("pipe_read_failed", GetLastError()); + CloseHandle(pipe); + return error; + } + + CloseHandle(pipe); + return std::string(buffer, buffer + bytes_read); +} + +std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down) { + std::ostringstream stream; + stream << kSecureDesktopKeyboardIpcCommandPrefix << key_code << ":" + << (is_down ? 1 : 0); + return stream.str(); +} + +std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel, + int flag) { + std::ostringstream stream; + stream << kSecureDesktopMouseIpcCommandPrefix << x << ":" << y << ":" + << wheel << ":" << flag; + return stream.str(); +} + +std::string BuildSecureInputHelperKeyboardCommand(int key_code, + bool is_down) { + std::ostringstream stream; + stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":" + << (is_down ? 1 : 0); + return stream.str(); +} + +bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command, + int* key_code_out, + bool* is_down_out) { + if (key_code_out == nullptr || is_down_out == nullptr) { + return false; + } + + if (command.rfind(kSecureDesktopKeyboardIpcCommandPrefix, 0) != 0) { + return false; + } + + const size_t key_begin = sizeof(kSecureDesktopKeyboardIpcCommandPrefix) - 1; + const size_t separator = command.find(':', key_begin); + if (separator == std::string::npos) { + return false; + } + + try { + *key_code_out = std::stoi(command.substr(key_begin, separator - key_begin)); + } catch (...) { + return false; + } + + const std::string state = command.substr(separator + 1); + if (state == "1" || state == "down") { + *is_down_out = true; + return true; + } + if (state == "0" || state == "up") { + *is_down_out = false; + return true; + } + return false; +} + +bool CreateSessionSystemToken(DWORD session_id, HANDLE* token_out, + DWORD* error_code_out = nullptr) { + if (token_out == nullptr) { + return false; + } + + *token_out = nullptr; + if (error_code_out != nullptr) { + *error_code_out = 0; + } + + HANDLE process_token = nullptr; + if (!OpenProcessToken(GetCurrentProcess(), + TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | + TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID, + &process_token)) { + if (error_code_out != nullptr) { + *error_code_out = GetLastError(); + } + return false; + } + + HANDLE primary_token = nullptr; + BOOL duplicated = DuplicateTokenEx( + process_token, + TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | + TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID, + nullptr, SecurityImpersonation, TokenPrimary, &primary_token); + CloseHandle(process_token); + if (!duplicated) { + if (error_code_out != nullptr) { + *error_code_out = GetLastError(); + } + return false; + } + + if (!SetTokenInformation(primary_token, TokenSessionId, &session_id, + sizeof(session_id))) { + if (error_code_out != nullptr) { + *error_code_out = GetLastError(); + } + CloseHandle(primary_token); + return false; + } + + *token_out = primary_token; + return true; +} + +const char* SessionEventToString(DWORD event_type) { + switch (event_type) { + case WTS_CONSOLE_CONNECT: + return "console-connect"; + case WTS_CONSOLE_DISCONNECT: + return "console-disconnect"; + case WTS_REMOTE_CONNECT: + return "remote-connect"; + case WTS_REMOTE_DISCONNECT: + return "remote-disconnect"; + case WTS_SESSION_LOGON: + return "session-logon"; + case WTS_SESSION_LOGOFF: + return "session-logoff"; + case WTS_SESSION_LOCK: + return "session-lock"; + case WTS_SESSION_UNLOCK: + return "session-unlock"; + default: + return "unknown"; + } +} + +const char* DetermineInteractiveStage(bool lock_app_visible, + bool credential_ui_visible, + bool secure_desktop_active) { + if (credential_ui_visible) { + return "credential-ui"; + } + if (lock_app_visible) { + return "lock-screen"; + } + if (secure_desktop_active) { + return "secure-desktop"; + } + return "user-desktop"; +} + +bool GetSessionUserName(DWORD session_id, std::wstring* username_out) { + if (username_out == nullptr) { + return false; + } + + username_out->clear(); + LPWSTR username = nullptr; + DWORD bytes = 0; + if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session_id, + WTSUserName, &username, &bytes)) { + return false; + } + + if (username != nullptr) { + *username_out = username; + WTSFreeMemory(username); + return true; + } + + return false; +} + +bool QuerySessionLockState(DWORD session_id, bool* session_locked_out) { + if (session_locked_out == nullptr) { + return false; + } + + *session_locked_out = false; + PWTSINFOEXW session_info = nullptr; + DWORD bytes = 0; + if (!WTSQuerySessionInformationW( + WTS_CURRENT_SERVER_HANDLE, session_id, WTSSessionInfoEx, + reinterpret_cast(&session_info), &bytes)) { + return false; + } + + bool success = false; + if (session_info != nullptr && bytes >= sizeof(WTSINFOEXW) && + session_info->Level == 1) { + const LONG session_flags = session_info->Data.WTSInfoExLevel1.SessionFlags; + if (session_flags == WTS_SESSIONSTATE_LOCK) { + *session_locked_out = true; + success = true; + } else if (session_flags == WTS_SESSIONSTATE_UNLOCK) { + *session_locked_out = false; + success = true; + } + } + + if (session_info != nullptr) { + WTSFreeMemory(session_info); + } + return success; +} + +bool IsLogonUiRunningInSession(DWORD session_id) { + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + return false; + } + + PROCESSENTRY32W entry{}; + entry.dwSize = sizeof(entry); + bool found = false; + if (Process32FirstW(snapshot, &entry)) { + do { + if (_wcsicmp(entry.szExeFile, L"LogonUI.exe") != 0) { + continue; + } + + DWORD process_session_id = 0xFFFFFFFF; + if (ProcessIdToSessionId(entry.th32ProcessID, &process_session_id) && + process_session_id == session_id) { + found = true; + break; + } + } while (Process32NextW(snapshot, &entry)); + } + + CloseHandle(snapshot); + return found; +} + +InputDesktopInfo GetInputDesktopInfo() { + InputDesktopInfo info; + HDESK desktop = OpenInputDesktop(0, FALSE, GENERIC_READ); + if (desktop == nullptr) { + info.error_code = GetLastError(); + return info; + } + + DWORD bytes_needed = 0; + GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed); + if (bytes_needed == 0) { + info.error_code = GetLastError(); + CloseDesktop(desktop); + return info; + } + + std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0'); + if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(), + bytes_needed, &bytes_needed)) { + info.error_code = GetLastError(); + CloseDesktop(desktop); + return info; + } + + CloseDesktop(desktop); + while (!desktop_name.empty() && desktop_name.back() == L'\0') { + desktop_name.pop_back(); + } + info.available = true; + info.name = WideToUtf8(desktop_name); + return info; +} + +std::string WideToUtf8(const std::wstring& value) { + if (value.empty()) { + return {}; + } + + int size_needed = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, + nullptr, 0, nullptr, nullptr); + if (size_needed <= 1) { + return {}; + } + + std::string result(static_cast(size_needed), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, result.data(), + size_needed, nullptr, nullptr); + result.pop_back(); + return result; +} + +bool QuerySoftwareSasGeneration(DWORD* value_out, bool* existed_out) { + if (value_out == nullptr || existed_out == nullptr) { + return false; + } + + *value_out = 0; + *existed_out = false; + HKEY key = nullptr; + constexpr wchar_t kPolicyKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, kPolicyKey, 0, + KEY_QUERY_VALUE | KEY_SET_VALUE, &key) != ERROR_SUCCESS) { + return false; + } + + DWORD value = 0; + DWORD value_size = sizeof(value); + DWORD type = REG_DWORD; + LONG result = RegQueryValueExW(key, L"SoftwareSASGeneration", nullptr, &type, + reinterpret_cast(&value), + &value_size); + if (result == ERROR_SUCCESS && type == REG_DWORD) { + *value_out = value; + *existed_out = true; + } + + RegCloseKey(key); + return true; +} + +bool SetSoftwareSasGeneration(DWORD value) { + HKEY key = nullptr; + constexpr wchar_t kPolicyKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + DWORD disposition = 0; + LONG result = RegCreateKeyExW(HKEY_LOCAL_MACHINE, kPolicyKey, 0, nullptr, 0, + KEY_SET_VALUE, nullptr, &key, &disposition); + if (result != ERROR_SUCCESS) { + return false; + } + + result = RegSetValueExW(key, L"SoftwareSASGeneration", 0, REG_DWORD, + reinterpret_cast(&value), + sizeof(value)); + RegCloseKey(key); + return result == ERROR_SUCCESS; +} + +bool RestoreSoftwareSasGeneration(DWORD original_value, bool existed_before) { + HKEY key = nullptr; + constexpr wchar_t kPolicyKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System"; + LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, kPolicyKey, 0, KEY_SET_VALUE, + &key); + if (result != ERROR_SUCCESS) { + return false; + } + + if (!existed_before) { + result = RegDeleteValueW(key, L"SoftwareSASGeneration"); + RegCloseKey(key); + return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND; + } + + result = RegSetValueExW(key, L"SoftwareSASGeneration", 0, REG_DWORD, + reinterpret_cast(&original_value), + sizeof(original_value)); + RegCloseKey(key); + return result == ERROR_SUCCESS; +} + +SasResult SendSasNow() { + SasResult result; + HMODULE sas_module = LoadLibraryW(L"sas.dll"); + if (sas_module == nullptr) { + result.error = "sas_dll_unavailable"; + result.error_code = GetLastError(); + return result; + } + + auto* send_sas = reinterpret_cast( + GetProcAddress(sas_module, "SendSAS")); + if (send_sas == nullptr) { + result.error = "send_sas_proc_missing"; + result.error_code = GetLastError(); + FreeLibrary(sas_module); + return result; + } + + DWORD original_value = 0; + bool existed_before = false; + bool queried = QuerySoftwareSasGeneration(&original_value, &existed_before); + bool mutated_policy = false; + if (queried) { + if (!existed_before || (original_value != 1 && original_value != 3)) { + if (!SetSoftwareSasGeneration(1)) { + result.error = "set_software_sas_generation_failed"; + result.error_code = GetLastError(); + FreeLibrary(sas_module); + return result; + } + mutated_policy = true; + } + } + + send_sas(FALSE); + + if (mutated_policy) { + RestoreSoftwareSasGeneration(original_value, existed_before); + } + + FreeLibrary(sas_module); + result.success = true; + return result; +} + +struct PipeSecurityAttributes { + PipeSecurityAttributes() = default; + ~PipeSecurityAttributes() { + if (security_descriptor_ != nullptr) { + LocalFree(security_descriptor_); + } + } + + bool Initialize() { + constexpr wchar_t kPipeSddl[] = + L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)"; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + kPipeSddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) { + return false; + } + + attributes_.nLength = sizeof(attributes_); + attributes_.lpSecurityDescriptor = security_descriptor_; + attributes_.bInheritHandle = FALSE; + return true; + } + + SECURITY_ATTRIBUTES* get() { return &attributes_; } + + private: + SECURITY_ATTRIBUTES attributes_{}; + PSECURITY_DESCRIPTOR security_descriptor_ = nullptr; +}; + +struct KernelObjectSecurityAttributes { + KernelObjectSecurityAttributes() = default; + ~KernelObjectSecurityAttributes() { + if (security_descriptor_ != nullptr) { + LocalFree(security_descriptor_); + } + } + + bool Initialize() { + constexpr wchar_t kObjectSddl[] = + L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;AU)"; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + kObjectSddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) { + return false; + } + + attributes_.nLength = sizeof(attributes_); + attributes_.lpSecurityDescriptor = security_descriptor_; + attributes_.bInheritHandle = FALSE; + return true; + } + + SECURITY_ATTRIBUTES* get() { return &attributes_; } + + private: + SECURITY_ATTRIBUTES attributes_{}; + PSECURITY_DESCRIPTOR security_descriptor_ = nullptr; +}; + +} // namespace + +CrossDeskServiceHost* CrossDeskServiceHost::instance_ = nullptr; + +CrossDeskServiceHost::CrossDeskServiceHost() { InitializeServiceLogger(); } + +CrossDeskServiceHost::~CrossDeskServiceHost() { ShutdownRuntime(); } + +void WINAPI CrossDeskServiceHost::ServiceMain([[maybe_unused]] DWORD argc, + [[maybe_unused]] LPWSTR* argv) { + if (instance_ != nullptr) { + instance_->RunServiceLoop(true); + } +} + +BOOL WINAPI CrossDeskServiceHost::ConsoleControlHandler(DWORD control_type) { + if (instance_ == nullptr) { + return FALSE; + } + + switch (control_type) { + case CTRL_C_EVENT: + case CTRL_BREAK_EVENT: + case CTRL_CLOSE_EVENT: + case CTRL_SHUTDOWN_EVENT: + instance_->RequestStop(); + return TRUE; + default: + return FALSE; + } +} + +DWORD WINAPI CrossDeskServiceHost::ServiceControlHandler( + DWORD control, DWORD event_type, LPVOID event_data, + [[maybe_unused]] LPVOID context) { + if (instance_ == nullptr) { + return ERROR_CALL_NOT_IMPLEMENTED; + } + + switch (control) { + case SERVICE_CONTROL_INTERROGATE: + instance_->ReportServiceStatus(instance_->service_status_.dwCurrentState, + NO_ERROR, 0); + return NO_ERROR; + case SERVICE_CONTROL_STOP: + case SERVICE_CONTROL_SHUTDOWN: + instance_->ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 3000); + instance_->RequestStop(); + return NO_ERROR; + case SERVICE_CONTROL_SESSIONCHANGE: { + DWORD session_id = 0xFFFFFFFF; + if (event_data != nullptr) { + auto* session = + reinterpret_cast(event_data); + session_id = session->dwSessionId; + } + instance_->RecordSessionEvent(event_type, session_id); + return NO_ERROR; + } + default: + return ERROR_CALL_NOT_IMPLEMENTED; + } +} + +int CrossDeskServiceHost::RunAsService() { + instance_ = this; + + SERVICE_TABLE_ENTRYW service_table[] = { + {const_cast(kCrossDeskServiceName), &CrossDeskServiceHost::ServiceMain}, + {nullptr, nullptr}}; + + if (!StartServiceCtrlDispatcherW(service_table)) { + DWORD error = GetLastError(); + LOG_ERROR("StartServiceCtrlDispatcherW failed, error={}", error); + return static_cast(error); + } + + return 0; +} + +int CrossDeskServiceHost::RunInConsole() { + instance_ = this; + return RunServiceLoop(false); +} + +int CrossDeskServiceHost::InitializeRuntime() { + stop_event_ = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (stop_event_ == nullptr) { + DWORD error = GetLastError(); + LOG_ERROR("CreateEventW failed, error={}", error); + return static_cast(error); + } + + started_at_tick_ = GetTickCount64(); + last_sas_tick_ = 0; + active_session_id_ = WTSGetActiveConsoleSessionId(); + process_session_id_ = 0xFFFFFFFF; + input_desktop_error_code_ = 0; + session_helper_process_id_ = 0; + session_helper_session_id_ = 0xFFFFFFFF; + session_helper_exit_code_ = 0; + session_helper_last_error_code_ = 0; + session_helper_status_error_code_ = 0; + session_helper_report_session_id_ = 0xFFFFFFFF; + session_helper_report_process_id_ = 0; + session_helper_report_input_desktop_error_code_ = 0; + secure_input_helper_process_id_ = 0; + secure_input_helper_session_id_ = 0xFFFFFFFF; + secure_input_helper_exit_code_ = 0; + secure_input_helper_last_error_code_ = 0; + session_locked_ = false; + logon_ui_visible_ = false; + prelogin_ = false; + secure_desktop_active_ = false; + input_desktop_available_ = false; + session_helper_running_ = false; + session_helper_status_ok_ = false; + session_helper_report_input_desktop_available_ = false; + session_helper_report_lock_app_visible_ = false; + session_helper_report_logon_ui_visible_ = false; + session_helper_report_secure_desktop_active_ = false; + session_helper_report_credential_ui_visible_ = false; + session_helper_report_unlock_ui_visible_ = false; + secure_input_helper_running_ = false; + last_sas_error_code_ = 0; + last_sas_success_ = false; + session_helper_started_at_tick_ = 0; + session_helper_report_state_age_ms_ = 0; + session_helper_report_uptime_ms_ = 0; + secure_input_helper_started_at_tick_ = 0; + session_helper_process_handle_ = nullptr; + session_helper_stop_event_ = nullptr; + secure_input_helper_process_handle_ = nullptr; + secure_input_helper_stop_event_ = nullptr; + input_desktop_name_.clear(); + last_sas_error_.clear(); + session_helper_last_error_.clear(); + session_helper_status_error_.clear(); + session_helper_report_input_desktop_.clear(); + session_helper_report_interactive_stage_.clear(); + secure_input_helper_last_error_.clear(); + last_session_event_type_ = 0; + last_session_event_session_id_ = active_session_id_; + RefreshSessionState(); + EnsureSessionHelper(); + ipc_thread_ = std::thread(&CrossDeskServiceHost::IpcServerLoop, this); + LOG_INFO("CrossDesk service runtime initialized, session_id={}", + active_session_id_); + return 0; +} + +void CrossDeskServiceHost::ShutdownRuntime() { + StopSecureInputHelper(); + StopSessionHelper(); + + if (stop_event_ != nullptr) { + SetEvent(stop_event_); + } + + if (ipc_thread_.joinable()) { + ipc_thread_.join(); + } + + if (stop_event_ != nullptr) { + CloseHandle(stop_event_); + stop_event_ = nullptr; + } +} + +void CrossDeskServiceHost::RequestStop() { + if (stop_event_ != nullptr) { + SetEvent(stop_event_); + } +} + +void CrossDeskServiceHost::ReportServiceStatus(DWORD current_state, + DWORD win32_exit_code, + DWORD wait_hint) { + if (status_handle_ == nullptr) { + return; + } + + service_status_.dwServiceType = SERVICE_WIN32_OWN_PROCESS; + service_status_.dwCurrentState = current_state; + service_status_.dwWin32ExitCode = win32_exit_code; + service_status_.dwWaitHint = wait_hint; + service_status_.dwControlsAccepted = + current_state == SERVICE_RUNNING + ? (SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_SHUTDOWN | + SERVICE_ACCEPT_SESSIONCHANGE) + : 0; + SetServiceStatus(status_handle_, &service_status_); +} + +int CrossDeskServiceHost::RunServiceLoop(bool as_service) { + console_mode_ = !as_service; + + if (as_service) { + status_handle_ = RegisterServiceCtrlHandlerExW( + kCrossDeskServiceName, &CrossDeskServiceHost::ServiceControlHandler, + this); + if (status_handle_ == nullptr) { + DWORD error = GetLastError(); + LOG_ERROR("RegisterServiceCtrlHandlerExW failed, error={}", error); + return static_cast(error); + } + ReportServiceStatus(SERVICE_START_PENDING, NO_ERROR, 3000); + } + + int init_error = InitializeRuntime(); + if (init_error != 0) { + if (as_service) { + ReportServiceStatus(SERVICE_STOPPED, static_cast(init_error), 0); + } + return init_error; + } + + if (as_service) { + ReportServiceStatus(SERVICE_RUNNING, NO_ERROR, 0); + } + + if (console_mode_) { + SetConsoleCtrlHandler(&CrossDeskServiceHost::ConsoleControlHandler, TRUE); + std::cout << "CrossDesk service skeleton running in console mode. Press Ctrl+C to stop." + << std::endl; + } + + WaitForSingleObject(stop_event_, INFINITE); + + if (as_service) { + ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 3000); + } + + ShutdownRuntime(); + + if (console_mode_) { + SetConsoleCtrlHandler(&CrossDeskServiceHost::ConsoleControlHandler, FALSE); + } + + if (as_service) { + ReportServiceStatus(SERVICE_STOPPED, NO_ERROR, 0); + } + + return 0; +} + +void CrossDeskServiceHost::IpcServerLoop() { + PipeSecurityAttributes security_attributes; + SECURITY_ATTRIBUTES* pipe_attributes = nullptr; + if (security_attributes.Initialize()) { + pipe_attributes = security_attributes.get(); + } else { + LOG_WARN("Pipe security initialization failed, error={}", GetLastError()); + } + + while (stop_event_ != nullptr && + WaitForSingleObject(stop_event_, 0) != WAIT_OBJECT_0) { + HANDLE pipe = CreateNamedPipeW( + kCrossDeskServicePipeName, + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + 1, 4096, 4096, 0, pipe_attributes); + if (pipe == INVALID_HANDLE_VALUE) { + DWORD error = GetLastError(); + LOG_ERROR("CreateNamedPipeW failed, error={}", error); + WaitForSingleObject(stop_event_, 1000); + continue; + } + + OVERLAPPED overlapped{}; + overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (overlapped.hEvent == nullptr) { + LOG_ERROR("CreateEventW for pipe failed, error={}", GetLastError()); + CloseHandle(pipe); + break; + } + + BOOL connected = ConnectNamedPipe(pipe, &overlapped); + DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError(); + if (connected) { + SetEvent(overlapped.hEvent); + } + if (!connected && connect_error == ERROR_PIPE_CONNECTED) { + SetEvent(overlapped.hEvent); + connected = TRUE; + } + + if (!connected && connect_error != ERROR_IO_PENDING) { + LOG_WARN("ConnectNamedPipe failed, error={}", connect_error); + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + continue; + } + + if (!connected) { + HANDLE wait_handles[] = {stop_event_, overlapped.hEvent}; + DWORD wait_result = + WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); + if (wait_result == WAIT_OBJECT_0) { + CancelIoEx(pipe, &overlapped); + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + break; + } + } + + char buffer[1024] = {0}; + DWORD bytes_read = 0; + if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) && + bytes_read > 0) { + std::string response = + HandleIpcCommand(std::string(buffer, buffer + bytes_read)); + DWORD bytes_written = 0; + WriteFile(pipe, response.data(), static_cast(response.size()), + &bytes_written, nullptr); + FlushFileBuffers(pipe); + } + + DisconnectNamedPipe(pipe); + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + } +} + +void CrossDeskServiceHost::RefreshSessionState() { + std::lock_guard lock(state_mutex_); + active_session_id_ = WTSGetActiveConsoleSessionId(); + DWORD process_session_id = 0xFFFFFFFF; + if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) { + process_session_id_ = process_session_id; + } + logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_); + InputDesktopInfo desktop_info = GetInputDesktopInfo(); + input_desktop_available_ = desktop_info.available; + input_desktop_error_code_ = desktop_info.error_code; + input_desktop_name_ = desktop_info.name; + secure_desktop_active_ = + _stricmp(input_desktop_name_.c_str(), "Winlogon") == 0; + + std::wstring username; + bool username_available = GetSessionUserName(active_session_id_, &username); + prelogin_ = !username_available || username.empty(); + if (!QuerySessionLockState(active_session_id_, &session_locked_)) { + session_locked_ = + (logon_ui_visible_ || secure_desktop_active_) && !prelogin_; + } +} + +void CrossDeskServiceHost::ResetSessionHelperReportedStateLocked( + const char* error, DWORD error_code) { + session_helper_status_ok_ = false; + session_helper_status_error_ = error != nullptr ? error : ""; + session_helper_status_error_code_ = error_code; + session_helper_report_session_id_ = 0xFFFFFFFF; + session_helper_report_process_id_ = 0; + session_helper_report_session_locked_ = false; + session_helper_report_input_desktop_available_ = false; + session_helper_report_input_desktop_error_code_ = 0; + session_helper_report_input_desktop_.clear(); + session_helper_report_lock_app_visible_ = false; + session_helper_report_logon_ui_visible_ = false; + session_helper_report_secure_desktop_active_ = false; + session_helper_report_credential_ui_visible_ = false; + session_helper_report_unlock_ui_visible_ = false; + session_helper_report_interactive_stage_.clear(); + session_helper_report_state_age_ms_ = 0; + session_helper_report_uptime_ms_ = 0; +} + +bool CrossDeskServiceHost::GetEffectiveSessionLockedLocked() const { + return session_helper_status_ok_ ? session_helper_report_session_locked_ + : session_locked_; +} + +bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const { + return session_helper_report_lock_app_visible_ || + session_helper_report_interactive_stage_ == "lock-screen"; +} + +bool CrossDeskServiceHost::HasSecureInputUiLocked() const { + return prelogin_ || secure_desktop_active_ || logon_ui_visible_ || + session_helper_report_credential_ui_visible_ || + session_helper_report_secure_desktop_active_ || + session_helper_report_unlock_ui_visible_ || + session_helper_report_interactive_stage_ == "credential-ui" || + session_helper_report_interactive_stage_ == "secure-desktop"; +} + +bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked( + DWORD target_session_id) const { + if (target_session_id == 0xFFFFFFFF) { + return false; + } + + return HasSecureInputUiLocked() || + (GetEffectiveSessionLockedLocked() && + IsHelperReportingLockScreenLocked()); +} + +std::wstring CrossDeskServiceHost::GetSessionHelperPath() const { + std::wstring current_executable = GetCurrentExecutablePathW(); + if (current_executable.empty()) { + return L""; + } + + return (std::filesystem::path(current_executable).parent_path() / + L"crossdesk_session_helper.exe") + .wstring(); +} + +std::wstring CrossDeskServiceHost::GetSessionHelperStopEventName( + DWORD session_id) const { + return L"Global\\CrossDeskSessionHelperStop-" + std::to_wstring(session_id); +} + +std::wstring CrossDeskServiceHost::GetSecureInputHelperPath() const { + return GetSessionHelperPath(); +} + +std::wstring CrossDeskServiceHost::GetSecureInputHelperStopEventName( + DWORD session_id) const { + return L"Global\\CrossDeskSecureInputHelperStop-" + + std::to_wstring(session_id); +} + +void CrossDeskServiceHost::ReapSessionHelper() { + HANDLE process_handle = nullptr; + HANDLE stop_event_handle = nullptr; + DWORD exit_code = 0; + + { + std::lock_guard lock(state_mutex_); + if (session_helper_process_handle_ == nullptr) { + return; + } + + DWORD wait_result = WaitForSingleObject(session_helper_process_handle_, 0); + if (wait_result != WAIT_OBJECT_0) { + session_helper_running_ = true; + return; + } + + GetExitCodeProcess(session_helper_process_handle_, &exit_code); + process_handle = session_helper_process_handle_; + stop_event_handle = session_helper_stop_event_; + session_helper_process_handle_ = nullptr; + session_helper_stop_event_ = nullptr; + session_helper_running_ = false; + session_helper_process_id_ = 0; + session_helper_exit_code_ = exit_code; + session_helper_started_at_tick_ = 0; + } + + if (process_handle != nullptr) { + CloseHandle(process_handle); + } + if (stop_event_handle != nullptr) { + CloseHandle(stop_event_handle); + } +} + +void CrossDeskServiceHost::ReapSecureInputHelper() { + HANDLE process_handle = nullptr; + HANDLE stop_event_handle = nullptr; + DWORD exit_code = 0; + + { + std::lock_guard lock(state_mutex_); + if (secure_input_helper_process_handle_ == nullptr) { + return; + } + + DWORD wait_result = + WaitForSingleObject(secure_input_helper_process_handle_, 0); + if (wait_result != WAIT_OBJECT_0) { + secure_input_helper_running_ = true; + return; + } + + GetExitCodeProcess(secure_input_helper_process_handle_, &exit_code); + process_handle = secure_input_helper_process_handle_; + stop_event_handle = secure_input_helper_stop_event_; + secure_input_helper_process_handle_ = nullptr; + secure_input_helper_stop_event_ = nullptr; + secure_input_helper_running_ = false; + secure_input_helper_process_id_ = 0; + secure_input_helper_exit_code_ = exit_code; + secure_input_helper_started_at_tick_ = 0; + } + + if (process_handle != nullptr) { + CloseHandle(process_handle); + } + if (stop_event_handle != nullptr) { + CloseHandle(stop_event_handle); + } +} + +void CrossDeskServiceHost::StopSessionHelper() { + HANDLE process_handle = nullptr; + HANDLE stop_event_handle = nullptr; + + { + std::lock_guard lock(state_mutex_); + process_handle = session_helper_process_handle_; + stop_event_handle = session_helper_stop_event_; + session_helper_process_handle_ = nullptr; + session_helper_stop_event_ = nullptr; + session_helper_running_ = false; + session_helper_process_id_ = 0; + session_helper_started_at_tick_ = 0; + ResetSessionHelperReportedStateLocked(nullptr, 0); + } + + if (stop_event_handle != nullptr) { + SetEvent(stop_event_handle); + } + + if (process_handle != nullptr) { + if (WaitForSingleObject(process_handle, 3000) == WAIT_TIMEOUT) { + TerminateProcess(process_handle, ERROR_PROCESS_ABORTED); + WaitForSingleObject(process_handle, 1000); + } + } + + if (process_handle != nullptr) { + CloseHandle(process_handle); + } + if (stop_event_handle != nullptr) { + CloseHandle(stop_event_handle); + } +} + +void CrossDeskServiceHost::StopSecureInputHelper() { + HANDLE process_handle = nullptr; + HANDLE stop_event_handle = nullptr; + + { + std::lock_guard lock(state_mutex_); + process_handle = secure_input_helper_process_handle_; + stop_event_handle = secure_input_helper_stop_event_; + secure_input_helper_process_handle_ = nullptr; + secure_input_helper_stop_event_ = nullptr; + secure_input_helper_running_ = false; + secure_input_helper_process_id_ = 0; + secure_input_helper_started_at_tick_ = 0; + } + + if (stop_event_handle != nullptr) { + SetEvent(stop_event_handle); + } + + if (process_handle != nullptr) { + if (WaitForSingleObject(process_handle, 3000) == WAIT_TIMEOUT) { + TerminateProcess(process_handle, ERROR_PROCESS_ABORTED); + WaitForSingleObject(process_handle, 1000); + } + } + + if (process_handle != nullptr) { + CloseHandle(process_handle); + } + if (stop_event_handle != nullptr) { + CloseHandle(stop_event_handle); + } +} + +bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) { + std::wstring helper_path = GetSessionHelperPath(); + if (helper_path.empty() || !std::filesystem::exists(helper_path)) { + std::lock_guard lock(state_mutex_); + session_helper_last_error_ = "helper_binary_missing"; + session_helper_last_error_code_ = ERROR_FILE_NOT_FOUND; + return false; + } + + std::wstring stop_event_name = GetSessionHelperStopEventName(session_id); + KernelObjectSecurityAttributes event_security; + SECURITY_ATTRIBUTES* event_attributes = nullptr; + if (event_security.Initialize()) { + event_attributes = event_security.get(); + } + + HANDLE stop_event_handle = CreateEventW(event_attributes, TRUE, FALSE, + stop_event_name.c_str()); + if (stop_event_handle == nullptr) { + std::lock_guard lock(state_mutex_); + session_helper_last_error_ = "create_helper_stop_event_failed"; + session_helper_last_error_code_ = GetLastError(); + return false; + } + + std::wstring command_line = QuoteWindowsArgument(helper_path) + + L" --session-helper --session-id " + + std::to_wstring(session_id) + L" --stop-event " + + QuoteWindowsArgument(stop_event_name); + std::wstring mutable_command_line = command_line; + + STARTUPINFOW startup_info{}; + startup_info.cb = sizeof(startup_info); + startup_info.lpDesktop = const_cast(L"winsta0\\default"); + PROCESS_INFORMATION process_info{}; + BOOL created = FALSE; + + if (console_mode_ && process_session_id_ == session_id) { + created = CreateProcessW(helper_path.c_str(), mutable_command_line.data(), + nullptr, nullptr, FALSE, CREATE_NO_WINDOW, + nullptr, nullptr, &startup_info, &process_info); + } else { + HANDLE user_token = nullptr; + HANDLE primary_token = nullptr; + ScopedEnvironmentBlock environment_block; + + if (!WTSQueryUserToken(session_id, &user_token)) { + DWORD error = GetLastError(); + CloseHandle(stop_event_handle); + std::lock_guard lock(state_mutex_); + session_helper_last_error_ = "wts_query_user_token_failed"; + session_helper_last_error_code_ = error; + return false; + } + + BOOL duplicated = DuplicateTokenEx( + user_token, + TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY | + TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID, + nullptr, SecurityImpersonation, TokenPrimary, &primary_token); + CloseHandle(user_token); + if (!duplicated) { + DWORD error = GetLastError(); + CloseHandle(stop_event_handle); + std::lock_guard lock(state_mutex_); + session_helper_last_error_ = "duplicate_token_failed"; + session_helper_last_error_code_ = error; + return false; + } + + CreateEnvironmentBlock(&environment_block.environment, primary_token, + FALSE); + created = CreateProcessAsUserW( + primary_token, helper_path.c_str(), mutable_command_line.data(), + nullptr, nullptr, FALSE, + CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, + environment_block.environment, nullptr, &startup_info, &process_info); + DWORD error = created ? ERROR_SUCCESS : GetLastError(); + CloseHandle(primary_token); + if (!created) { + CloseHandle(stop_event_handle); + std::lock_guard lock(state_mutex_); + session_helper_last_error_ = "create_process_as_user_failed"; + session_helper_last_error_code_ = error; + return false; + } + } + + CloseHandle(process_info.hThread); + { + std::lock_guard lock(state_mutex_); + session_helper_process_handle_ = process_info.hProcess; + session_helper_stop_event_ = stop_event_handle; + session_helper_process_id_ = process_info.dwProcessId; + session_helper_session_id_ = session_id; + session_helper_exit_code_ = STILL_ACTIVE; + session_helper_last_error_code_ = 0; + session_helper_last_error_.clear(); + session_helper_running_ = true; + session_helper_started_at_tick_ = GetTickCount64(); + } + + LOG_INFO("Session helper started: session_id={}, pid={}", session_id, + process_info.dwProcessId); + return true; +} + +bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) { + std::wstring helper_path = GetSecureInputHelperPath(); + if (helper_path.empty() || !std::filesystem::exists(helper_path)) { + std::lock_guard lock(state_mutex_); + secure_input_helper_last_error_ = "secure_input_helper_binary_missing"; + secure_input_helper_last_error_code_ = ERROR_FILE_NOT_FOUND; + return false; + } + + std::wstring stop_event_name = GetSecureInputHelperStopEventName(session_id); + KernelObjectSecurityAttributes event_security; + SECURITY_ATTRIBUTES* event_attributes = nullptr; + if (event_security.Initialize()) { + event_attributes = event_security.get(); + } + + HANDLE stop_event_handle = CreateEventW(event_attributes, TRUE, FALSE, + stop_event_name.c_str()); + if (stop_event_handle == nullptr) { + std::lock_guard lock(state_mutex_); + secure_input_helper_last_error_ = + "create_secure_input_helper_stop_event_failed"; + secure_input_helper_last_error_code_ = GetLastError(); + return false; + } + + std::wstring command_line = QuoteWindowsArgument(helper_path) + + L" --secure-input-helper --session-id " + + std::to_wstring(session_id) + L" --stop-event " + + QuoteWindowsArgument(stop_event_name); + std::wstring mutable_command_line = command_line; + + STARTUPINFOW startup_info{}; + startup_info.cb = sizeof(startup_info); + startup_info.lpDesktop = const_cast(L"winsta0\\Winlogon"); + PROCESS_INFORMATION process_info{}; + BOOL created = FALSE; + + if (console_mode_ && process_session_id_ == session_id) { + created = CreateProcessW(helper_path.c_str(), mutable_command_line.data(), + nullptr, nullptr, FALSE, CREATE_NO_WINDOW, + nullptr, nullptr, &startup_info, &process_info); + } else { + HANDLE primary_token = nullptr; + ScopedEnvironmentBlock environment_block; + DWORD error = 0; + if (!CreateSessionSystemToken(session_id, &primary_token, &error)) { + CloseHandle(stop_event_handle); + std::lock_guard lock(state_mutex_); + secure_input_helper_last_error_ = + "create_session_system_token_failed"; + secure_input_helper_last_error_code_ = error; + return false; + } + + CreateEnvironmentBlock(&environment_block.environment, primary_token, + FALSE); + created = CreateProcessAsUserW( + primary_token, helper_path.c_str(), mutable_command_line.data(), + nullptr, nullptr, FALSE, + CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, + environment_block.environment, nullptr, &startup_info, &process_info); + error = created ? ERROR_SUCCESS : GetLastError(); + CloseHandle(primary_token); + if (!created) { + CloseHandle(stop_event_handle); + std::lock_guard lock(state_mutex_); + secure_input_helper_last_error_ = + "create_secure_input_helper_failed"; + secure_input_helper_last_error_code_ = error; + return false; + } + } + + CloseHandle(process_info.hThread); + { + std::lock_guard lock(state_mutex_); + secure_input_helper_process_handle_ = process_info.hProcess; + secure_input_helper_stop_event_ = stop_event_handle; + secure_input_helper_process_id_ = process_info.dwProcessId; + secure_input_helper_session_id_ = session_id; + secure_input_helper_exit_code_ = STILL_ACTIVE; + secure_input_helper_last_error_code_ = 0; + secure_input_helper_last_error_.clear(); + secure_input_helper_running_ = true; + secure_input_helper_started_at_tick_ = GetTickCount64(); + } + + LOG_INFO("Secure input helper started: session_id={}, pid={}", session_id, + process_info.dwProcessId); + return true; +} + +void CrossDeskServiceHost::EnsureSessionHelper() { + ReapSessionHelper(); + + DWORD target_session_id = 0xFFFFFFFF; + bool has_active_user = false; + bool already_running = false; + + { + std::lock_guard lock(state_mutex_); + target_session_id = active_session_id_; + has_active_user = !prelogin_; + already_running = session_helper_running_ && + session_helper_session_id_ == target_session_id; + } + + if (already_running) { + return; + } + + if (target_session_id == 0xFFFFFFFF || !has_active_user) { + StopSessionHelper(); + std::lock_guard lock(state_mutex_); + session_helper_last_error_ = target_session_id == 0xFFFFFFFF + ? "no_active_console_session" + : "no_active_user_session"; + session_helper_last_error_code_ = 0; + return; + } + + StopSessionHelper(); + LaunchSessionHelper(target_session_id); +} + +void CrossDeskServiceHost::RefreshSessionHelperReportedState() { + DWORD target_session_id = 0xFFFFFFFF; + bool helper_running = false; + { + std::lock_guard lock(state_mutex_); + target_session_id = session_helper_session_id_; + helper_running = session_helper_running_; + } + + if (!helper_running || target_session_id == 0xFFFFFFFF) { + std::lock_guard lock(state_mutex_); + ResetSessionHelperReportedStateLocked("helper_not_running", 0); + return; + } + + std::string response = QueryNamedPipeMessage( + GetCrossDeskSessionHelperPipeName(target_session_id), + kCrossDeskSessionHelperStatusCommand, 300); + Json json = Json::parse(response, nullptr, false); + if (json.is_discarded()) { + std::lock_guard lock(state_mutex_); + ResetSessionHelperReportedStateLocked("invalid_helper_status_json", 0); + return; + } + + if (!json.value("ok", false)) { + std::lock_guard lock(state_mutex_); + const std::string error = + json.value("error", std::string("helper_status_failed")); + ResetSessionHelperReportedStateLocked( + error.c_str(), json.value("code", static_cast(0))); + return; + } + + std::lock_guard lock(state_mutex_); + session_helper_status_ok_ = true; + session_helper_status_error_.clear(); + session_helper_status_error_code_ = 0; + session_helper_report_session_id_ = + json.value("session_id", static_cast(0xFFFFFFFF)); + session_helper_report_process_id_ = json.value("process_id", 0u); + session_helper_report_session_locked_ = + json.value("session_locked", false); + session_helper_report_input_desktop_available_ = + json.value("input_desktop_available", false); + session_helper_report_input_desktop_error_code_ = + json.value("input_desktop_error_code", 0u); + session_helper_report_input_desktop_ = + json.value("input_desktop", std::string()); + session_helper_report_lock_app_visible_ = + json.value("lock_app_visible", false); + session_helper_report_logon_ui_visible_ = + json.value("logon_ui_visible", false); + session_helper_report_secure_desktop_active_ = + json.value("secure_desktop_active", false); + session_helper_report_credential_ui_visible_ = + json.value("credential_ui_visible", false); + session_helper_report_unlock_ui_visible_ = + json.value("unlock_ui_visible", false); + session_helper_report_interactive_stage_ = + json.value("interactive_stage", std::string()); + session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull); + session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull); +} + +void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, + DWORD session_id) { + { + std::lock_guard lock(state_mutex_); + last_session_event_type_ = event_type; + last_session_event_session_id_ = session_id; + active_session_id_ = WTSGetActiveConsoleSessionId(); + DWORD process_session_id = 0xFFFFFFFF; + if (ProcessIdToSessionId(GetCurrentProcessId(), &process_session_id)) { + process_session_id_ = process_session_id; + } + logon_ui_visible_ = IsLogonUiRunningInSession(active_session_id_); + InputDesktopInfo desktop_info = GetInputDesktopInfo(); + input_desktop_available_ = desktop_info.available; + input_desktop_error_code_ = desktop_info.error_code; + input_desktop_name_ = desktop_info.name; + secure_desktop_active_ = + _stricmp(input_desktop_name_.c_str(), "Winlogon") == 0; + + std::wstring username; + bool username_available = GetSessionUserName(active_session_id_, &username); + prelogin_ = !username_available || username.empty(); + + if (!QuerySessionLockState(active_session_id_, &session_locked_)) { + if (event_type == WTS_SESSION_LOCK) { + session_locked_ = true; + } else if (event_type == WTS_SESSION_UNLOCK || + event_type == WTS_SESSION_LOGON) { + session_locked_ = false; + } else if (logon_ui_visible_ || secure_desktop_active_) { + session_locked_ = !prelogin_; + } + } + } + + LOG_INFO("Session event: type={}, session_id={}, active_session_id={}", + SessionEventToString(event_type), session_id, active_session_id_); + EnsureSessionHelper(); + if (!secure_desktop_active_ && !logon_ui_visible_) { + StopSecureInputHelper(); + } +} + +std::string CrossDeskServiceHost::HandleIpcCommand( + const std::string& command) { + std::string normalized = ToLower(Trim(command)); + if (normalized == "ping") { + return "{\"ok\":true,\"reply\":\"pong\"}"; + } + if (normalized == "status") { + return BuildStatusResponse(); + } + if (normalized == "sas") { + return SendSecureAttentionSequence(); + } + int key_code = 0; + bool is_down = false; + if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, + &is_down)) { + return SendSecureDesktopKeyboardInput(key_code, is_down); + } + return BuildErrorJson("unknown_command"); +} + +std::string CrossDeskServiceHost::BuildStatusResponse() { + ReapSecureInputHelper(); + ReapSessionHelper(); + RefreshSessionState(); + EnsureSessionHelper(); + RefreshSessionHelperReportedState(); + bool keep_secure_input_helper = false; + bool launch_secure_input_helper = false; + DWORD secure_input_target_session_id = 0xFFFFFFFF; + { + std::lock_guard lock(state_mutex_); + secure_input_target_session_id = active_session_id_; + keep_secure_input_helper = + ShouldKeepSecureInputHelperLocked(secure_input_target_session_id); + launch_secure_input_helper = + keep_secure_input_helper && + (!secure_input_helper_running_ || + secure_input_helper_session_id_ != secure_input_target_session_id); + } + + if (keep_secure_input_helper) { + if (launch_secure_input_helper) { + StopSecureInputHelper(); + LaunchSecureInputHelper(secure_input_target_session_id); + } + } else { + StopSecureInputHelper(); + } + + std::lock_guard lock(state_mutex_); + std::wstring username; + GetSessionUserName(active_session_id_, &username); + std::string username_utf8 = EscapeJsonString(WideToUtf8(username)); + std::string input_desktop = EscapeJsonString(input_desktop_name_); + std::string last_sas_error = EscapeJsonString(last_sas_error_); + std::string session_helper_last_error = + EscapeJsonString(session_helper_last_error_); + std::string session_helper_status_error = + EscapeJsonString(session_helper_status_error_); + std::string session_helper_path = + EscapeJsonString(WideToUtf8(GetSessionHelperPath())); + std::string secure_input_helper_path = + EscapeJsonString(WideToUtf8(GetSecureInputHelperPath())); + std::string helper_input_desktop = + EscapeJsonString(session_helper_report_input_desktop_); + std::string secure_input_helper_last_error = + EscapeJsonString(secure_input_helper_last_error_); + bool interactive_state_ready = session_helper_status_ok_; + const char* interactive_state_source = + interactive_state_ready ? "session-helper" : "service-host"; + const bool effective_session_locked = GetEffectiveSessionLockedLocked(); + const bool interactive_lock_screen_visible = + interactive_state_ready + ? (effective_session_locked && IsHelperReportingLockScreenLocked()) + : false; + bool credential_ui_visible = interactive_state_ready + ? session_helper_report_credential_ui_visible_ + : logon_ui_visible_; + bool unlock_ui_visible = interactive_state_ready + ? session_helper_report_unlock_ui_visible_ + : (logon_ui_visible_ || secure_desktop_active_); + bool interactive_secure_desktop_active = + interactive_state_ready ? session_helper_report_secure_desktop_active_ + : secure_desktop_active_; + bool interactive_logon_ui_visible = + interactive_state_ready ? session_helper_report_logon_ui_visible_ + : logon_ui_visible_; + bool interactive_session_locked = + effective_session_locked || interactive_lock_screen_visible || + unlock_ui_visible; + std::string interactive_input_desktop = EscapeJsonString( + interactive_state_ready ? session_helper_report_input_desktop_ + : input_desktop_name_); + std::string interactive_stage = EscapeJsonString( + DetermineInteractiveStage(interactive_lock_screen_visible, + credential_ui_visible, + interactive_secure_desktop_active)); + std::ostringstream stream; + stream << "{\"ok\":true,\"service\":\"CrossDeskService\"" + << ",\"active_session_id\":" << active_session_id_ + << ",\"process_session_id\":" << process_session_id_ + << ",\"session_locked\":" << (session_locked_ ? "true" : "false") + << ",\"interactive_state_ready\":" + << (interactive_state_ready ? "true" : "false") + << ",\"interactive_state_source\":\"" + << interactive_state_source << "\"" + << ",\"interactive_session_locked\":" + << (interactive_session_locked ? "true" : "false") + << ",\"interactive_stage\":\"" << interactive_stage << "\"" + << ",\"interactive_input_desktop\":\"" + << interactive_input_desktop << "\"" + << ",\"interactive_lock_screen_visible\":" + << (interactive_lock_screen_visible ? "true" : "false") + << ",\"interactive_logon_ui_visible\":" + << (interactive_logon_ui_visible ? "true" : "false") + << ",\"interactive_secure_desktop_active\":" + << (interactive_secure_desktop_active ? "true" : "false") + << ",\"unlock_ui_visible\":" + << (unlock_ui_visible ? "true" : "false") + << ",\"credential_ui_visible\":" + << (credential_ui_visible ? "true" : "false") + << ",\"password_box_visible\":" + << (credential_ui_visible ? "true" : "false") + << ",\"logon_ui_visible\":" + << (logon_ui_visible_ ? "true" : "false") + << ",\"secure_desktop_active\":" + << (secure_desktop_active_ ? "true" : "false") + << ",\"input_desktop_available\":" + << (input_desktop_available_ ? "true" : "false") + << ",\"input_desktop_error_code\":" << input_desktop_error_code_ + << ",\"input_desktop\":\"" << input_desktop << "\"" + << ",\"prelogin\":" << (prelogin_ ? "true" : "false") + << ",\"session_user\":\"" << username_utf8 << "\"" + << ",\"session_helper_path\":\"" << session_helper_path << "\"" + << ",\"session_helper_running\":" + << (session_helper_running_ ? "true" : "false") + << ",\"session_helper_pid\":" << session_helper_process_id_ + << ",\"session_helper_session_id\":" + << session_helper_session_id_ + << ",\"session_helper_exit_code\":" << session_helper_exit_code_ + << ",\"session_helper_last_error\":\"" + << session_helper_last_error << "\"" + << ",\"session_helper_last_error_code\":" + << session_helper_last_error_code_ + << ",\"session_helper_status_ok\":" + << (session_helper_status_ok_ ? "true" : "false") + << ",\"session_helper_status_error\":\"" + << session_helper_status_error << "\"" + << ",\"session_helper_status_error_code\":" + << session_helper_status_error_code_ + << ",\"session_helper_report_session_id\":" + << session_helper_report_session_id_ + << ",\"session_helper_report_process_id\":" + << session_helper_report_process_id_ + << ",\"session_helper_report_session_locked\":" + << (session_helper_report_session_locked_ ? "true" : "false") + << ",\"session_helper_report_input_desktop_available\":" + << (session_helper_report_input_desktop_available_ ? "true" : "false") + << ",\"session_helper_report_input_desktop_error_code\":" + << session_helper_report_input_desktop_error_code_ + << ",\"session_helper_report_input_desktop\":\"" + << helper_input_desktop << "\"" + << ",\"session_helper_report_lock_app_visible\":" + << (session_helper_report_lock_app_visible_ ? "true" : "false") + << ",\"session_helper_report_logon_ui_visible\":" + << (session_helper_report_logon_ui_visible_ ? "true" : "false") + << ",\"session_helper_report_secure_desktop_active\":" + << (session_helper_report_secure_desktop_active_ ? "true" : "false") + << ",\"session_helper_report_credential_ui_visible\":" + << (session_helper_report_credential_ui_visible_ ? "true" : "false") + << ",\"session_helper_report_unlock_ui_visible\":" + << (session_helper_report_unlock_ui_visible_ ? "true" : "false") + << ",\"session_helper_report_interactive_stage\":\"" + << EscapeJsonString(session_helper_report_interactive_stage_) << "\"" + << ",\"session_helper_report_state_age_ms\":" + << session_helper_report_state_age_ms_ + << ",\"session_helper_report_uptime_ms\":" + << session_helper_report_uptime_ms_ + << ",\"session_helper_uptime_ms\":" + << (session_helper_started_at_tick_ >= started_at_tick_ + ? (GetTickCount64() - session_helper_started_at_tick_) + : 0) + << ",\"secure_input_helper_path\":\"" + << secure_input_helper_path << "\"" + << ",\"secure_input_helper_running\":" + << (secure_input_helper_running_ ? "true" : "false") + << ",\"secure_input_helper_pid\":" + << secure_input_helper_process_id_ + << ",\"secure_input_helper_session_id\":" + << secure_input_helper_session_id_ + << ",\"secure_input_helper_exit_code\":" + << secure_input_helper_exit_code_ + << ",\"secure_input_helper_last_error\":\"" + << secure_input_helper_last_error << "\"" + << ",\"secure_input_helper_last_error_code\":" + << secure_input_helper_last_error_code_ + << ",\"secure_input_helper_uptime_ms\":" + << (secure_input_helper_started_at_tick_ >= started_at_tick_ + ? (GetTickCount64() - secure_input_helper_started_at_tick_) + : 0) + << ",\"last_sas_success\":" + << (last_sas_success_ ? "true" : "false") + << ",\"last_sas_error\":\"" << last_sas_error << "\"" + << ",\"last_sas_error_code\":" << last_sas_error_code_ + << ",\"last_sas_uptime_ms\":" + << (last_sas_tick_ >= started_at_tick_ + ? (last_sas_tick_ - started_at_tick_) + : 0) + << ",\"last_session_event\":\"" + << SessionEventToString(last_session_event_type_) << "\"" + << ",\"last_session_event_id\":" << last_session_event_type_ + << ",\"last_session_id\":" << last_session_event_session_id_ + << ",\"uptime_ms\":" + << (GetTickCount64() >= started_at_tick_ + ? (GetTickCount64() - started_at_tick_) + : 0) + << "}"; + return stream.str(); +} + +std::string CrossDeskServiceHost::SendSecureAttentionSequence() { + RefreshSessionState(); + LOG_INFO("Received SAS request for session_id={}", active_session_id_); + SasResult result = SendSasNow(); + { + std::lock_guard lock(state_mutex_); + last_sas_tick_ = GetTickCount64(); + last_sas_success_ = result.success; + last_sas_error_code_ = result.error_code; + last_sas_error_ = result.error; + } + + if (!result.success) { + return BuildErrorJson(result.error.c_str(), result.error_code); + } + return "{\"ok\":true,\"sent\":\"sas\"}"; +} + +std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(int key_code, + bool is_down) { + RefreshSessionState(); + ReapSecureInputHelper(); + EnsureSessionHelper(); + + DWORD target_session_id = 0xFFFFFFFF; + bool helper_running = false; + bool can_inject = false; + { + std::lock_guard lock(state_mutex_); + target_session_id = active_session_id_; + helper_running = secure_input_helper_running_ && + secure_input_helper_session_id_ == target_session_id; + can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked(); + } + + if (target_session_id == 0xFFFFFFFF) { + return BuildErrorJson("no_active_console_session"); + } + if (!can_inject) { + return BuildErrorJson("secure_input_not_active"); + } + + if (!helper_running) { + StopSecureInputHelper(); + if (!LaunchSecureInputHelper(target_session_id)) { + std::lock_guard lock(state_mutex_); + return BuildErrorJson(secure_input_helper_last_error_.c_str(), + secure_input_helper_last_error_code_); + } + } + + return QueryNamedPipeMessage( + GetCrossDeskSecureInputHelperPipeName(target_session_id), + BuildSecureInputHelperKeyboardCommand(key_code, is_down), 1000); +} + +bool InstallCrossDeskService(const std::wstring& binary_path) { + SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (manager == nullptr) { + LOG_ERROR("OpenSCManagerW failed, error={}", GetLastError()); + return false; + } + + std::wstring service_command = L"\"" + binary_path + L"\" --service"; + SC_HANDLE service = CreateServiceW( + manager, kCrossDeskServiceName, kCrossDeskServiceDisplayName, + SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, + SERVICE_ERROR_NORMAL, service_command.c_str(), nullptr, nullptr, nullptr, + nullptr, nullptr); + + if (service == nullptr) { + DWORD error = GetLastError(); + if (error != ERROR_SERVICE_EXISTS) { + LOG_ERROR("CreateServiceW failed, error={}", error); + CloseServiceHandle(manager); + return false; + } + + service = OpenServiceW(manager, kCrossDeskServiceName, + SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS); + if (service == nullptr) { + LOG_ERROR("OpenServiceW failed, error={}", GetLastError()); + CloseServiceHandle(manager); + return false; + } + + if (!ChangeServiceConfigW( + service, SERVICE_NO_CHANGE, SERVICE_AUTO_START, + SERVICE_NO_CHANGE, service_command.c_str(), nullptr, nullptr, + nullptr, nullptr, nullptr, kCrossDeskServiceDisplayName)) { + LOG_ERROR("ChangeServiceConfigW failed, error={}", GetLastError()); + CloseServiceHandle(service); + CloseServiceHandle(manager); + return false; + } + } + + CloseServiceHandle(service); + CloseServiceHandle(manager); + return true; +} + +bool StartCrossDeskService() { + SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + if (manager == nullptr) { + LOG_ERROR("OpenSCManagerW failed, error={}", GetLastError()); + return false; + } + + SC_HANDLE service = + OpenServiceW(manager, kCrossDeskServiceName, SERVICE_START); + if (service == nullptr) { + LOG_ERROR("OpenServiceW failed, error={}", GetLastError()); + CloseServiceHandle(manager); + return false; + } + + bool success = StartServiceW(service, 0, nullptr) != FALSE; + DWORD error = success ? ERROR_SUCCESS : GetLastError(); + if (!success && error != ERROR_SERVICE_ALREADY_RUNNING) { + LOG_ERROR("StartServiceW failed, error={}", error); + } + + CloseServiceHandle(service); + CloseServiceHandle(manager); + return success || error == ERROR_SERVICE_ALREADY_RUNNING; +} + +bool StopCrossDeskService(DWORD timeout_ms) { + SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT); + if (manager == nullptr) { + LOG_ERROR("OpenSCManagerW failed, error={}", GetLastError()); + return false; + } + + SC_HANDLE service = OpenServiceW(manager, kCrossDeskServiceName, + SERVICE_STOP | SERVICE_QUERY_STATUS); + if (service == nullptr) { + DWORD error = GetLastError(); + CloseServiceHandle(manager); + if (error == ERROR_SERVICE_DOES_NOT_EXIST) { + return true; + } + LOG_ERROR("OpenServiceW failed, error={}", error); + return false; + } + + SERVICE_STATUS_PROCESS status{}; + DWORD bytes_needed = 0; + QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, + reinterpret_cast(&status), sizeof(status), + &bytes_needed); + + if (status.dwCurrentState != SERVICE_STOPPED && + status.dwCurrentState != SERVICE_STOP_PENDING) { + SERVICE_STATUS service_status{}; + if (!ControlService(service, SERVICE_CONTROL_STOP, &service_status)) { + DWORD error = GetLastError(); + if (error != ERROR_SERVICE_NOT_ACTIVE) { + LOG_ERROR("ControlService failed, error={}", error); + CloseServiceHandle(service); + CloseServiceHandle(manager); + return false; + } + } + } + + DWORD deadline = GetTickCount() + timeout_ms; + while (GetTickCount() < deadline) { + if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, + reinterpret_cast(&status), + sizeof(status), &bytes_needed)) { + LOG_ERROR("QueryServiceStatusEx failed, error={}", GetLastError()); + CloseServiceHandle(service); + CloseServiceHandle(manager); + return false; + } + + if (status.dwCurrentState == SERVICE_STOPPED) { + CloseServiceHandle(service); + CloseServiceHandle(manager); + return true; + } + Sleep(200); + } + + CloseServiceHandle(service); + CloseServiceHandle(manager); + return false; +} + +bool UninstallCrossDeskService() { + SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (manager == nullptr) { + LOG_ERROR("OpenSCManagerW failed, error={}", GetLastError()); + return false; + } + + SC_HANDLE service = OpenServiceW(manager, kCrossDeskServiceName, + DELETE | SERVICE_STOP | SERVICE_QUERY_STATUS); + if (service == nullptr) { + DWORD error = GetLastError(); + CloseServiceHandle(manager); + if (error == ERROR_SERVICE_DOES_NOT_EXIST) { + return true; + } + LOG_ERROR("OpenServiceW failed, error={}", error); + return false; + } + + StopCrossDeskService(); + + bool success = DeleteService(service) != FALSE; + if (!success) { + LOG_ERROR("DeleteService failed, error={}", GetLastError()); + } + + CloseServiceHandle(service); + CloseServiceHandle(manager); + return success; +} + +std::string QueryCrossDeskService(const std::string& command, + DWORD timeout_ms) { + return QueryNamedPipeMessage(kCrossDeskServicePipeName, command, timeout_ms); +} + +std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down, + DWORD timeout_ms) { + return QueryCrossDeskService( + BuildSecureDesktopKeyboardIpcCommand(key_code, is_down), timeout_ms); +} + +std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel, + int flag, DWORD timeout_ms) { + return QueryCrossDeskService( + BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms); +} + +} // namespace crossdesk \ No newline at end of file diff --git a/src/service/windows/service_host.h b/src/service/windows/service_host.h new file mode 100644 index 0000000..0ded115 --- /dev/null +++ b/src/service/windows/service_host.h @@ -0,0 +1,141 @@ +#ifndef _CROSSDESK_SERVICE_HOST_H_ +#define _CROSSDESK_SERVICE_HOST_H_ + +#include + +#include +#include +#include + +namespace crossdesk { + +inline constexpr wchar_t kCrossDeskServiceName[] = L"CrossDeskService"; +inline constexpr wchar_t kCrossDeskServiceDisplayName[] = + L"CrossDesk Service"; +inline constexpr wchar_t kCrossDeskServicePipeName[] = + L"\\\\.\\pipe\\CrossDeskService"; + +class CrossDeskServiceHost { + public: + CrossDeskServiceHost(); + ~CrossDeskServiceHost(); + + int RunAsService(); + int RunInConsole(); + + private: + int RunServiceLoop(bool as_service); + int InitializeRuntime(); + void ShutdownRuntime(); + void RequestStop(); + void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code, + DWORD wait_hint); + void IpcServerLoop(); + void RefreshSessionState(); + void EnsureSessionHelper(); + void ReapSessionHelper(); + void StopSessionHelper(); + bool LaunchSessionHelper(DWORD session_id); + void ReapSecureInputHelper(); + void StopSecureInputHelper(); + bool LaunchSecureInputHelper(DWORD session_id); + std::wstring GetSessionHelperPath() const; + std::wstring GetSessionHelperStopEventName(DWORD session_id) const; + std::wstring GetSecureInputHelperPath() const; + std::wstring GetSecureInputHelperStopEventName(DWORD session_id) const; + void ResetSessionHelperReportedStateLocked(const char* error, + DWORD error_code); + bool GetEffectiveSessionLockedLocked() const; + bool IsHelperReportingLockScreenLocked() const; + bool HasSecureInputUiLocked() const; + bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const; + void RefreshSessionHelperReportedState(); + void RecordSessionEvent(DWORD event_type, DWORD session_id); + std::string HandleIpcCommand(const std::string& command); + std::string BuildStatusResponse(); + std::string SendSecureAttentionSequence(); + std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down); + + static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv); + static BOOL WINAPI ConsoleControlHandler(DWORD control_type); + static DWORD WINAPI ServiceControlHandler(DWORD control, DWORD event_type, + LPVOID event_data, + LPVOID context); + + private: + SERVICE_STATUS_HANDLE status_handle_ = nullptr; + SERVICE_STATUS service_status_{}; + HANDLE stop_event_ = nullptr; + std::thread ipc_thread_; + std::mutex state_mutex_; + DWORD active_session_id_ = 0xFFFFFFFF; + DWORD process_session_id_ = 0xFFFFFFFF; + DWORD input_desktop_error_code_ = 0; + DWORD session_helper_process_id_ = 0; + DWORD session_helper_session_id_ = 0xFFFFFFFF; + DWORD session_helper_exit_code_ = 0; + DWORD session_helper_last_error_code_ = 0; + DWORD session_helper_status_error_code_ = 0; + DWORD session_helper_report_session_id_ = 0xFFFFFFFF; + DWORD session_helper_report_process_id_ = 0; + DWORD session_helper_report_input_desktop_error_code_ = 0; + DWORD secure_input_helper_process_id_ = 0; + DWORD secure_input_helper_session_id_ = 0xFFFFFFFF; + DWORD secure_input_helper_exit_code_ = 0; + DWORD secure_input_helper_last_error_code_ = 0; + DWORD last_session_event_type_ = 0; + DWORD last_session_event_session_id_ = 0xFFFFFFFF; + ULONGLONG started_at_tick_ = 0; + ULONGLONG last_sas_tick_ = 0; + ULONGLONG session_helper_started_at_tick_ = 0; + ULONGLONG session_helper_report_state_age_ms_ = 0; + ULONGLONG session_helper_report_uptime_ms_ = 0; + ULONGLONG secure_input_helper_started_at_tick_ = 0; + bool session_locked_ = false; + bool logon_ui_visible_ = false; + bool prelogin_ = false; + bool secure_desktop_active_ = false; + bool input_desktop_available_ = false; + bool session_helper_running_ = false; + bool session_helper_status_ok_ = false; + bool session_helper_report_session_locked_ = false; + bool session_helper_report_input_desktop_available_ = false; + bool session_helper_report_lock_app_visible_ = false; + bool session_helper_report_logon_ui_visible_ = false; + bool session_helper_report_secure_desktop_active_ = false; + bool session_helper_report_credential_ui_visible_ = false; + bool session_helper_report_unlock_ui_visible_ = false; + bool secure_input_helper_running_ = false; + bool console_mode_ = false; + DWORD last_sas_error_code_ = 0; + bool last_sas_success_ = false; + HANDLE session_helper_process_handle_ = nullptr; + HANDLE session_helper_stop_event_ = nullptr; + HANDLE secure_input_helper_process_handle_ = nullptr; + HANDLE secure_input_helper_stop_event_ = nullptr; + std::string input_desktop_name_; + std::string last_sas_error_; + std::string session_helper_last_error_; + std::string session_helper_status_error_; + std::string session_helper_report_input_desktop_; + std::string session_helper_report_interactive_stage_; + std::string secure_input_helper_last_error_; + + static CrossDeskServiceHost* instance_; +}; + +bool InstallCrossDeskService(const std::wstring& binary_path); +bool UninstallCrossDeskService(); +bool StartCrossDeskService(); +bool StopCrossDeskService(DWORD timeout_ms = 5000); +std::string QueryCrossDeskService(const std::string& command, + DWORD timeout_ms = 1000); +std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down, + DWORD timeout_ms = 1000); +std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel, + int flag, + DWORD timeout_ms = 1000); + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/src/service/windows/session_helper_main.cpp b/src/service/windows/session_helper_main.cpp new file mode 100644 index 0000000..e986688 --- /dev/null +++ b/src/service/windows/session_helper_main.cpp @@ -0,0 +1,1123 @@ +#include + +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "path_manager.h" +#include "rd_log.h" +#include "session_helper_shared.h" + +namespace { + +using crossdesk::InitLogger; +using crossdesk::get_logger; +using Json = nlohmann::json; + +struct InputDesktopInfo { + bool available = false; + DWORD error_code = 0; + std::string name; +}; + +struct HelperState { + std::mutex mutex; + DWORD session_id = 0xFFFFFFFF; + DWORD process_id = 0; + bool session_locked = false; + bool input_desktop_available = false; + DWORD input_desktop_error_code = 0; + std::string input_desktop_name; + bool lock_app_visible = false; + bool logon_ui_visible = false; + bool secure_desktop_active = false; + ULONGLONG started_at_tick = 0; + ULONGLONG last_update_tick = 0; +}; + +struct SecureCaptureRequest { + int left = 0; + int top = 0; + int width = 0; + int height = 0; + bool show_cursor = true; +}; + +struct SecureMouseRequest { + int x = 0; + int y = 0; + int wheel = 0; + int flag = 0; +}; + +struct SecureCaptureBuffers { + std::vector nv12_frame; +}; + +struct PipeSecurityAttributes { + PipeSecurityAttributes() = default; + ~PipeSecurityAttributes() { + if (security_descriptor_ != nullptr) { + LocalFree(security_descriptor_); + } + } + + bool Initialize() { + constexpr wchar_t kPipeSddl[] = + L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)"; + if (!ConvertStringSecurityDescriptorToSecurityDescriptorW( + kPipeSddl, SDDL_REVISION_1, &security_descriptor_, nullptr)) { + return false; + } + + attributes_.nLength = sizeof(attributes_); + attributes_.lpSecurityDescriptor = security_descriptor_; + attributes_.bInheritHandle = FALSE; + return true; + } + + SECURITY_ATTRIBUTES* get() { return &attributes_; } + + private: + SECURITY_ATTRIBUTES attributes_{}; + PSECURITY_DESCRIPTOR security_descriptor_ = nullptr; +}; + +void InitializeHelperLogger() { + static std::once_flag once_flag; + std::call_once(once_flag, []() { + crossdesk::PathManager path_manager("CrossDesk"); + std::filesystem::path log_path = path_manager.GetLogPath() / "session_helper"; + if (!log_path.empty() && path_manager.CreateDirectories(log_path)) { + InitLogger(log_path.string()); + return; + } + InitLogger("logs/session_helper"); + }); +} + +std::wstring Utf8ToWide(const std::string& value) { + if (value.empty()) { + return {}; + } + + int size_needed = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, + nullptr, 0); + if (size_needed <= 1) { + return {}; + } + + std::wstring result(static_cast(size_needed), L'\0'); + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, result.data(), + size_needed); + result.pop_back(); + return result; +} + +std::string WideToUtf8(const std::wstring& value) { + if (value.empty()) { + return {}; + } + + int size_needed = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, + nullptr, 0, nullptr, nullptr); + if (size_needed <= 1) { + return {}; + } + + std::string result(static_cast(size_needed), '\0'); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, result.data(), + size_needed, nullptr, nullptr); + result.pop_back(); + return result; +} + +InputDesktopInfo GetInputDesktopInfo() { + InputDesktopInfo info; + HDESK desktop = OpenInputDesktop(0, FALSE, GENERIC_READ); + if (desktop == nullptr) { + info.error_code = GetLastError(); + return info; + } + + DWORD bytes_needed = 0; + GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed); + if (bytes_needed == 0) { + info.error_code = GetLastError(); + CloseDesktop(desktop); + return info; + } + + std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0'); + if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(), + bytes_needed, &bytes_needed)) { + info.error_code = GetLastError(); + CloseDesktop(desktop); + return info; + } + + CloseDesktop(desktop); + while (!desktop_name.empty() && desktop_name.back() == L'\0') { + desktop_name.pop_back(); + } + info.available = true; + info.name = WideToUtf8(desktop_name); + return info; +} + +bool IsProcessRunningInCurrentSession(const wchar_t* executable_name, + DWORD session_id) { + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + return false; + } + + PROCESSENTRY32W entry{}; + entry.dwSize = sizeof(entry); + bool found = false; + if (Process32FirstW(snapshot, &entry)) { + do { + if (_wcsicmp(entry.szExeFile, executable_name) != 0) { + continue; + } + + DWORD process_session_id = 0xFFFFFFFF; + if (ProcessIdToSessionId(entry.th32ProcessID, &process_session_id) && + process_session_id == session_id) { + found = true; + break; + } + } while (Process32NextW(snapshot, &entry)); + } + + CloseHandle(snapshot); + return found; +} + +bool IsLogonUiRunningInCurrentSession(DWORD session_id) { + return IsProcessRunningInCurrentSession(L"LogonUI.exe", session_id); +} + +bool IsLockAppRunningInCurrentSession(DWORD session_id) { + return IsProcessRunningInCurrentSession(L"LockApp.exe", session_id); +} + +bool QuerySessionLockState(DWORD session_id, bool* session_locked_out) { + if (session_locked_out == nullptr) { + return false; + } + + *session_locked_out = false; + PWTSINFOEXW session_info = nullptr; + DWORD bytes = 0; + if (!WTSQuerySessionInformationW( + WTS_CURRENT_SERVER_HANDLE, session_id, WTSSessionInfoEx, + reinterpret_cast(&session_info), &bytes)) { + return false; + } + + bool success = false; + if (session_info != nullptr && bytes >= sizeof(WTSINFOEXW) && + session_info->Level == 1) { + const LONG session_flags = session_info->Data.WTSInfoExLevel1.SessionFlags; + if (session_flags == WTS_SESSIONSTATE_LOCK) { + *session_locked_out = true; + success = true; + } else if (session_flags == WTS_SESSIONSTATE_UNLOCK) { + *session_locked_out = false; + success = true; + } + } + + if (session_info != nullptr) { + WTSFreeMemory(session_info); + } + return success; +} + +const char* DetermineInteractiveStage(bool lock_app_visible, + bool credential_ui_visible, + bool secure_desktop_active) { + if (credential_ui_visible) { + return "credential-ui"; + } + if (lock_app_visible) { + return "lock-screen"; + } + if (secure_desktop_active) { + return "secure-desktop"; + } + return "user-desktop"; +} + +std::string BuildErrorJson(const char* error, DWORD error_code = 0) { + Json json; + json["ok"] = false; + json["error"] = error; + if (error_code != 0) { + json["code"] = error_code; + } + return json.dump(); +} + +void UpdateHelperState(HelperState* helper_state) { + if (helper_state == nullptr) { + return; + } + + InputDesktopInfo desktop_info = GetInputDesktopInfo(); + bool lock_app_process_running = + IsLockAppRunningInCurrentSession(helper_state->session_id); + bool logon_ui_visible = + IsLogonUiRunningInCurrentSession(helper_state->session_id); + const bool secure_desktop_active = + _stricmp(desktop_info.name.c_str(), "Winlogon") == 0; + bool session_locked = false; + if (!QuerySessionLockState(helper_state->session_id, &session_locked)) { + session_locked = + lock_app_process_running || logon_ui_visible || secure_desktop_active; + } + const bool lock_app_visible = session_locked && lock_app_process_running; + + std::lock_guard lock(helper_state->mutex); + helper_state->session_locked = session_locked; + helper_state->input_desktop_available = desktop_info.available; + helper_state->input_desktop_error_code = desktop_info.error_code; + helper_state->input_desktop_name = desktop_info.name; + helper_state->lock_app_visible = lock_app_visible; + helper_state->logon_ui_visible = logon_ui_visible; + helper_state->secure_desktop_active = secure_desktop_active; + helper_state->last_update_tick = GetTickCount64(); +} + +std::string BuildHelperStatusResponse(HelperState* helper_state) { + if (helper_state == nullptr) { + return BuildErrorJson("invalid_state"); + } + + Json json; + std::lock_guard lock(helper_state->mutex); + const bool credential_ui_visible = + helper_state->logon_ui_visible || + (helper_state->session_locked && !helper_state->input_desktop_available); + const bool unlock_ui_visible = + credential_ui_visible || helper_state->secure_desktop_active; + json["ok"] = true; + json["session_id"] = helper_state->session_id; + json["process_id"] = helper_state->process_id; + json["session_locked"] = helper_state->session_locked; + json["input_desktop_available"] = helper_state->input_desktop_available; + json["input_desktop_error_code"] = helper_state->input_desktop_error_code; + json["input_desktop"] = helper_state->input_desktop_name; + json["lock_app_visible"] = helper_state->lock_app_visible; + json["logon_ui_visible"] = helper_state->logon_ui_visible; + json["secure_desktop_active"] = helper_state->secure_desktop_active; + json["credential_ui_visible"] = credential_ui_visible; + json["unlock_ui_visible"] = unlock_ui_visible; + json["interactive_stage"] = DetermineInteractiveStage( + helper_state->lock_app_visible, credential_ui_visible, + helper_state->secure_desktop_active); + json["uptime_ms"] = + GetTickCount64() >= helper_state->started_at_tick + ? (GetTickCount64() - helper_state->started_at_tick) + : 0; + json["state_age_ms"] = + GetTickCount64() >= helper_state->last_update_tick + ? (GetTickCount64() - helper_state->last_update_tick) + : 0; + return json.dump(); +} + +void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id, + HelperState* helper_state) { + PipeSecurityAttributes security_attributes; + SECURITY_ATTRIBUTES* pipe_attributes = nullptr; + if (security_attributes.Initialize()) { + pipe_attributes = security_attributes.get(); + } + + std::wstring pipe_name = crossdesk::GetCrossDeskSessionHelperPipeName(session_id); + while (stop_event == nullptr || WaitForSingleObject(stop_event, 0) != WAIT_OBJECT_0) { + HANDLE pipe = CreateNamedPipeW( + pipe_name.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, 1, 4096, 4096, + 0, pipe_attributes); + if (pipe == INVALID_HANDLE_VALUE) { + LOG_ERROR("CreateNamedPipeW failed in helper, error={}", GetLastError()); + if (stop_event != nullptr) { + WaitForSingleObject(stop_event, 1000); + } + continue; + } + + OVERLAPPED overlapped{}; + overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (overlapped.hEvent == nullptr) { + LOG_ERROR("CreateEventW failed in helper IPC loop, error={}", + GetLastError()); + CloseHandle(pipe); + break; + } + + BOOL connected = ConnectNamedPipe(pipe, &overlapped); + DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError(); + if (connected) { + SetEvent(overlapped.hEvent); + } + if (!connected && connect_error == ERROR_PIPE_CONNECTED) { + SetEvent(overlapped.hEvent); + connected = TRUE; + } + if (!connected && connect_error != ERROR_IO_PENDING) { + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + continue; + } + + if (!connected) { + if (stop_event != nullptr) { + HANDLE wait_handles[] = {stop_event, overlapped.hEvent}; + DWORD wait_result = + WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); + if (wait_result == WAIT_OBJECT_0) { + CancelIoEx(pipe, &overlapped); + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + break; + } + } else { + WaitForSingleObject(overlapped.hEvent, INFINITE); + } + } + + char buffer[1024] = {0}; + DWORD bytes_read = 0; + if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) && + bytes_read > 0) { + std::string command(buffer, buffer + bytes_read); + std::string response; + if (command == crossdesk::kCrossDeskSessionHelperStatusCommand) { + response = BuildHelperStatusResponse(helper_state); + } else if (command == "ping") { + response = "{\"ok\":true,\"reply\":\"pong\"}"; + } else { + response = BuildErrorJson("unknown_command"); + } + + DWORD bytes_written = 0; + WriteFile(pipe, response.data(), static_cast(response.size()), + &bytes_written, nullptr); + FlushFileBuffers(pipe); + } + + DisconnectNamedPipe(pipe); + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + } +} + +std::wstring GetCurrentThreadDesktopNameW() { + HDESK desktop = GetThreadDesktop(GetCurrentThreadId()); + if (desktop == nullptr) { + return L""; + } + + DWORD bytes_needed = 0; + GetUserObjectInformationW(desktop, UOI_NAME, nullptr, 0, &bytes_needed); + if (bytes_needed == 0) { + return L""; + } + + std::wstring desktop_name(bytes_needed / sizeof(wchar_t), L'\0'); + if (!GetUserObjectInformationW(desktop, UOI_NAME, desktop_name.data(), + bytes_needed, &bytes_needed)) { + return L""; + } + + while (!desktop_name.empty() && desktop_name.back() == L'\0') { + desktop_name.pop_back(); + } + return desktop_name; +} + +bool EnsureThreadDesktop(const wchar_t* desktop_name, + HDESK* opened_desktop_out = nullptr) { + if (desktop_name == nullptr) { + return false; + } + + std::wstring current_desktop = GetCurrentThreadDesktopNameW(); + if (!current_desktop.empty() && + _wcsicmp(current_desktop.c_str(), desktop_name) == 0) { + return true; + } + + HDESK desktop = OpenDesktopW( + desktop_name, 0, FALSE, + DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | + DESKTOP_SWITCHDESKTOP); + if (desktop == nullptr) { + return false; + } + + if (!SetThreadDesktop(desktop)) { + CloseDesktop(desktop); + return false; + } + + if (opened_desktop_out != nullptr) { + *opened_desktop_out = desktop; + } else { + CloseDesktop(desktop); + } + return true; +} + +int InjectKeyboardInput(int key_code, bool is_down) { + INPUT input = {0}; + input.type = INPUT_KEYBOARD; + input.ki.wVk = static_cast(key_code); + + const UINT scan_code = + MapVirtualKeyW(static_cast(key_code), MAPVK_VK_TO_VSC_EX); + if (scan_code != 0) { + input.ki.wVk = 0; + input.ki.wScan = static_cast(scan_code & 0xFF); + input.ki.dwFlags |= KEYEVENTF_SCANCODE; + if ((scan_code & 0xFF00) != 0) { + input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY; + } + } + + if (!is_down) { + input.ki.dwFlags |= KEYEVENTF_KEYUP; + } + + UINT sent = SendInput(1, &input, sizeof(INPUT)); + if (sent != 1) { + return static_cast(GetLastError()); + } + + return 0; +} + +bool ParseSecureInputKeyboardCommand(const std::string& command, + int* key_code_out, + bool* is_down_out) { + if (key_code_out == nullptr || is_down_out == nullptr) { + return false; + } + + if (command.rfind(crossdesk::kCrossDeskSecureInputKeyboardCommandPrefix, + 0) != 0) { + return false; + } + + const size_t key_begin = + std::strlen(crossdesk::kCrossDeskSecureInputKeyboardCommandPrefix); + const size_t separator = command.find(':', key_begin); + if (separator == std::string::npos) { + return false; + } + + try { + *key_code_out = std::stoi(command.substr(key_begin, separator - key_begin)); + } catch (...) { + return false; + } + + const std::string state = command.substr(separator + 1); + if (state == "1" || state == "down") { + *is_down_out = true; + return true; + } + if (state == "0" || state == "up") { + *is_down_out = false; + return true; + } + return false; +} + +bool ParseSecureInputMouseCommand(const std::string& command, + SecureMouseRequest* request_out) { + if (request_out == nullptr) { + return false; + } + + if (command.rfind(crossdesk::kCrossDeskSecureInputMouseCommandPrefix, 0) != + 0) { + return false; + } + + const size_t x_begin = + std::strlen(crossdesk::kCrossDeskSecureInputMouseCommandPrefix); + size_t separator = command.find(':', x_begin); + if (separator == std::string::npos) { + return false; + } + + try { + request_out->x = std::stoi(command.substr(x_begin, separator - x_begin)); + } catch (...) { + return false; + } + + const size_t y_begin = separator + 1; + separator = command.find(':', y_begin); + if (separator == std::string::npos) { + return false; + } + + try { + request_out->y = std::stoi(command.substr(y_begin, separator - y_begin)); + } catch (...) { + return false; + } + + const size_t wheel_begin = separator + 1; + separator = command.find(':', wheel_begin); + if (separator == std::string::npos) { + return false; + } + + try { + request_out->wheel = + std::stoi(command.substr(wheel_begin, separator - wheel_begin)); + request_out->flag = std::stoi(command.substr(separator + 1)); + } catch (...) { + return false; + } + + return true; +} + +bool ParseSecureInputCaptureCommand(const std::string& command, + SecureCaptureRequest* request_out) { + if (request_out == nullptr) { + return false; + } + + if (command.rfind(crossdesk::kCrossDeskSecureInputCaptureCommandPrefix, 0) != + 0) { + return false; + } + + const size_t values_begin = + std::strlen(crossdesk::kCrossDeskSecureInputCaptureCommandPrefix); + int parsed_values[5] = {0}; + size_t token_begin = values_begin; + for (int index = 0; index < 5; ++index) { + const size_t separator = command.find(':', token_begin); + const bool is_last = index == 4; + const size_t token_end = + is_last ? command.size() : separator; + if (token_end == std::string::npos || token_end <= token_begin) { + return false; + } + + try { + parsed_values[index] = + std::stoi(command.substr(token_begin, token_end - token_begin)); + } catch (...) { + return false; + } + + token_begin = token_end + 1; + } + + request_out->left = parsed_values[0]; + request_out->top = parsed_values[1]; + request_out->width = parsed_values[2] & ~1; + request_out->height = parsed_values[3] & ~1; + request_out->show_cursor = parsed_values[4] != 0; + return request_out->width > 0 && request_out->height > 0; +} + +int InjectMouseInput(const SecureMouseRequest& request) { + SetCursorPos(request.x, request.y); + + INPUT input = {0}; + input.type = INPUT_MOUSE; + switch (request.flag) { + case 1: + input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; + break; + case 2: + input.mi.dwFlags = MOUSEEVENTF_LEFTUP; + break; + case 3: + input.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN; + break; + case 4: + input.mi.dwFlags = MOUSEEVENTF_RIGHTUP; + break; + case 5: + input.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN; + break; + case 6: + input.mi.dwFlags = MOUSEEVENTF_MIDDLEUP; + break; + case 7: + input.mi.dwFlags = MOUSEEVENTF_WHEEL; + input.mi.mouseData = request.wheel * 120; + break; + case 8: + input.mi.dwFlags = MOUSEEVENTF_HWHEEL; + input.mi.mouseData = request.wheel * 120; + break; + default: + input.mi.dwFlags = 0; + break; + } + + if (input.mi.dwFlags == 0) { + return 0; + } + + UINT sent = SendInput(1, &input, sizeof(INPUT)); + if (sent != 1) { + return static_cast(GetLastError()); + } + + return 0; +} + +std::vector BuildTextResponseBytes(const std::string& response) { + return std::vector(response.begin(), response.end()); +} + +std::vector CaptureSecureDesktopFrame( + const SecureCaptureRequest& request, + SecureCaptureBuffers* capture_buffers) { + if (capture_buffers == nullptr) { + return BuildTextResponseBytes(BuildErrorJson("invalid_capture_buffers")); + } + + HDC screen_dc = GetDC(nullptr); + if (screen_dc == nullptr) { + return BuildTextResponseBytes( + BuildErrorJson("get_dc_failed", GetLastError())); + } + + HDC mem_dc = CreateCompatibleDC(screen_dc); + if (mem_dc == nullptr) { + const DWORD error = GetLastError(); + ReleaseDC(nullptr, screen_dc); + return BuildTextResponseBytes(BuildErrorJson("create_dc_failed", error)); + } + + BITMAPINFO bmi{}; + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = request.width; + bmi.bmiHeader.biHeight = -request.height; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + void* bits = nullptr; + HBITMAP dib = + CreateDIBSection(mem_dc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0); + if (dib == nullptr || bits == nullptr) { + const DWORD error = GetLastError(); + DeleteDC(mem_dc); + ReleaseDC(nullptr, screen_dc); + return BuildTextResponseBytes( + BuildErrorJson("create_dib_failed", error)); + } + + HGDIOBJ old_bitmap = SelectObject(mem_dc, dib); + if (!BitBlt(mem_dc, 0, 0, request.width, request.height, screen_dc, + request.left, request.top, SRCCOPY | CAPTUREBLT)) { + const DWORD error = GetLastError(); + SelectObject(mem_dc, old_bitmap); + DeleteObject(dib); + DeleteDC(mem_dc); + ReleaseDC(nullptr, screen_dc); + return BuildTextResponseBytes( + BuildErrorJson("bitblt_failed", error)); + } + + if (request.show_cursor) { + CURSORINFO cursor_info{}; + cursor_info.cbSize = sizeof(CURSORINFO); + if (GetCursorInfo(&cursor_info) && cursor_info.flags == CURSOR_SHOWING && + cursor_info.hCursor != nullptr) { + const int cursor_x = cursor_info.ptScreenPos.x - request.left; + const int cursor_y = cursor_info.ptScreenPos.y - request.top; + if (cursor_x >= -64 && cursor_y >= -64 && + cursor_x < request.width + 64 && cursor_y < request.height + 64) { + DrawIconEx(mem_dc, cursor_x, cursor_y, cursor_info.hCursor, 0, 0, 0, + nullptr, DI_NORMAL); + } + } + } + + const size_t nv12_size = + static_cast(request.width) * request.height * 3 / 2; + capture_buffers->nv12_frame.resize(nv12_size); + const int convert_result = libyuv::ARGBToNV12( + static_cast(bits), request.width * 4, + capture_buffers->nv12_frame.data(), request.width, + capture_buffers->nv12_frame.data() + request.width * request.height, + request.width, request.width, request.height); + + SelectObject(mem_dc, old_bitmap); + DeleteObject(dib); + DeleteDC(mem_dc); + ReleaseDC(nullptr, screen_dc); + + if (convert_result != 0) { + return BuildTextResponseBytes(BuildErrorJson("argb_to_nv12_failed")); + } + + crossdesk::CrossDeskSecureDesktopFrameHeader header{}; + header.magic = crossdesk::kCrossDeskSecureDesktopFrameMagic; + header.version = crossdesk::kCrossDeskSecureDesktopFrameVersion; + header.left = request.left; + header.top = request.top; + header.width = static_cast(request.width); + header.height = static_cast(request.height); + header.payload_size = static_cast(capture_buffers->nv12_frame.size()); + + std::vector response(sizeof(header) + + capture_buffers->nv12_frame.size()); + std::memcpy(response.data(), &header, sizeof(header)); + if (!capture_buffers->nv12_frame.empty()) { + std::memcpy(response.data() + sizeof(header), + capture_buffers->nv12_frame.data(), + capture_buffers->nv12_frame.size()); + } + return response; +} + +std::vector HandleSecureInputHelperCommand( + const std::string& command, + SecureCaptureBuffers* capture_buffers) { + if (command == "ping") { + return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}"); + } + + int key_code = 0; + bool is_down = false; + if (ParseSecureInputKeyboardCommand(command, &key_code, &is_down)) { + const int inject_result = InjectKeyboardInput(key_code, is_down); + if (inject_result != 0) { + LOG_WARN( + "Secure input helper SendInput failed for key_code={}, is_down={}, err={}", + key_code, is_down, inject_result); + return BuildTextResponseBytes(BuildErrorJson( + "send_input_failed", static_cast(inject_result))); + } + + Json json; + json["ok"] = true; + json["injected"] = "keyboard"; + json["key_code"] = key_code; + json["is_down"] = is_down; + json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW()); + return BuildTextResponseBytes(json.dump()); + } + + SecureMouseRequest mouse_request; + if (ParseSecureInputMouseCommand(command, &mouse_request)) { + const int inject_result = InjectMouseInput(mouse_request); + if (inject_result != 0) { + LOG_WARN( + "Secure input helper SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}", + mouse_request.x, mouse_request.y, mouse_request.wheel, + mouse_request.flag, inject_result); + return BuildTextResponseBytes(BuildErrorJson( + "send_input_failed", static_cast(inject_result))); + } + + Json json; + json["ok"] = true; + json["injected"] = "mouse"; + json["x"] = mouse_request.x; + json["y"] = mouse_request.y; + json["wheel"] = mouse_request.wheel; + json["flag"] = mouse_request.flag; + json["desktop"] = WideToUtf8(GetCurrentThreadDesktopNameW()); + return BuildTextResponseBytes(json.dump()); + } + + SecureCaptureRequest capture_request; + if (ParseSecureInputCaptureCommand(command, &capture_request)) { + return CaptureSecureDesktopFrame(capture_request, capture_buffers); + } + + return BuildTextResponseBytes(BuildErrorJson("unknown_command")); +} + +void HandleSecureInputHelperPipeClient(HANDLE pipe, HANDLE event_handle) { + SecureCaptureBuffers capture_buffers; + char buffer[1024] = {0}; + DWORD bytes_read = 0; + if (ReadFile(pipe, buffer, sizeof(buffer) - 1, &bytes_read, nullptr) && + bytes_read > 0) { + std::vector response = HandleSecureInputHelperCommand( + std::string(buffer, buffer + bytes_read), &capture_buffers); + DWORD bytes_written = 0; + if (!response.empty()) { + WriteFile(pipe, response.data(), static_cast(response.size()), + &bytes_written, nullptr); + FlushFileBuffers(pipe); + } + } + + DisconnectNamedPipe(pipe); + if (event_handle != nullptr) { + CloseHandle(event_handle); + } + CloseHandle(pipe); +} + +void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) { + PipeSecurityAttributes security_attributes; + SECURITY_ATTRIBUTES* pipe_attributes = nullptr; + if (security_attributes.Initialize()) { + pipe_attributes = security_attributes.get(); + } + + std::wstring pipe_name = + crossdesk::GetCrossDeskSecureInputHelperPipeName(session_id); + while (stop_event == nullptr || + WaitForSingleObject(stop_event, 0) != WAIT_OBJECT_0) { + HANDLE pipe = CreateNamedPipeW( + pipe_name.c_str(), PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, + PIPE_UNLIMITED_INSTANCES, + crossdesk::kCrossDeskSecureInputPipeBufferBytes, 4096, 0, + pipe_attributes); + if (pipe == INVALID_HANDLE_VALUE) { + LOG_ERROR("CreateNamedPipeW failed in secure input helper, error={}", + GetLastError()); + if (stop_event != nullptr) { + WaitForSingleObject(stop_event, 1000); + } + continue; + } + + OVERLAPPED overlapped{}; + overlapped.hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr); + if (overlapped.hEvent == nullptr) { + LOG_ERROR("CreateEventW failed in secure input IPC loop, error={}", + GetLastError()); + CloseHandle(pipe); + break; + } + + BOOL connected = ConnectNamedPipe(pipe, &overlapped); + DWORD connect_error = connected ? ERROR_SUCCESS : GetLastError(); + if (connected) { + SetEvent(overlapped.hEvent); + } + if (!connected && connect_error == ERROR_PIPE_CONNECTED) { + SetEvent(overlapped.hEvent); + connected = TRUE; + } + if (!connected && connect_error != ERROR_IO_PENDING) { + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + continue; + } + + if (!connected) { + if (stop_event != nullptr) { + HANDLE wait_handles[] = {stop_event, overlapped.hEvent}; + DWORD wait_result = + WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); + if (wait_result == WAIT_OBJECT_0) { + CancelIoEx(pipe, &overlapped); + CloseHandle(overlapped.hEvent); + CloseHandle(pipe); + break; + } + } else { + WaitForSingleObject(overlapped.hEvent, INFINITE); + } + } + + std::thread(HandleSecureInputHelperPipeClient, pipe, overlapped.hEvent) + .detach(); + } +} + +void PrintUsage() { + std::cout << "CrossDesk session helper\n" + << " --session-helper Run helper loop\n" + << " --secure-input-helper Run Winlogon input helper\n" + << " --session-id Expected target session id\n" + << " --stop-event Global stop event name\n"; +} + +} // namespace + +int main(int argc, char* argv[]) { + InitializeHelperLogger(); + + bool run_helper = false; + bool run_secure_input_helper = false; + DWORD expected_session_id = 0xFFFFFFFF; + std::wstring stop_event_name; + for (int index = 1; index < argc; ++index) { + std::string argument = argv[index]; + if (argument == "--session-helper") { + run_helper = true; + continue; + } + if (argument == "--secure-input-helper") { + run_secure_input_helper = true; + continue; + } + if (argument == "--session-id" && index + 1 < argc) { + expected_session_id = static_cast(std::stoul(argv[++index])); + continue; + } + if (argument == "--stop-event" && index + 1 < argc) { + stop_event_name = Utf8ToWide(argv[++index]); + continue; + } + } + + if (!run_helper && !run_secure_input_helper) { + PrintUsage(); + return 0; + } + + DWORD current_session_id = 0xFFFFFFFF; + ProcessIdToSessionId(GetCurrentProcessId(), ¤t_session_id); + HANDLE stop_event = nullptr; + if (!stop_event_name.empty()) { + stop_event = OpenEventW(SYNCHRONIZE, FALSE, stop_event_name.c_str()); + if (stop_event == nullptr) { + LOG_ERROR("OpenEventW failed for stop event, error={}", GetLastError()); + return 1; + } + } + + if (run_secure_input_helper) { + LOG_INFO("Secure input helper starting: pid={}, current_session_id={}, expected_session_id={}", + GetCurrentProcessId(), current_session_id, expected_session_id); + if (expected_session_id != 0xFFFFFFFF && + expected_session_id != current_session_id) { + LOG_WARN("Secure input helper session mismatch: expected={}, current={}", + expected_session_id, current_session_id); + } + + HDESK secure_desktop = nullptr; + if (!EnsureThreadDesktop(L"Winlogon", &secure_desktop)) { + LOG_ERROR("Failed to switch secure input helper to Winlogon desktop, error={}", + GetLastError()); + if (stop_event != nullptr) { + CloseHandle(stop_event); + } + return 1; + } + + LOG_INFO("Secure input helper desktop: '{}'", + WideToUtf8(GetCurrentThreadDesktopNameW())); + SecureInputHelperIpcServerLoop(stop_event, current_session_id); + + if (secure_desktop != nullptr) { + CloseDesktop(secure_desktop); + } + if (stop_event != nullptr) { + CloseHandle(stop_event); + } + LOG_INFO("Secure input helper exiting: session_id={}", current_session_id); + return 0; + } + + LOG_INFO("Session helper starting: pid={}, current_session_id={}, expected_session_id={}", + GetCurrentProcessId(), current_session_id, expected_session_id); + + HelperState helper_state; + helper_state.session_id = current_session_id; + helper_state.process_id = GetCurrentProcessId(); + helper_state.started_at_tick = GetTickCount64(); + UpdateHelperState(&helper_state); + + std::thread ipc_thread(HelperIpcServerLoop, stop_event, current_session_id, + &helper_state); + + std::string last_desktop_name; + bool last_lock_app = false; + bool last_logon_ui = false; + bool last_secure_desktop = false; + bool last_session_locked = false; + std::string last_stage; + while (true) { + UpdateHelperState(&helper_state); + + std::string desktop_name; + bool session_locked = false; + bool input_desktop_available = false; + bool lock_app_visible = false; + bool logon_ui_running = false; + bool secure_desktop_active = false; + { + std::lock_guard lock(helper_state.mutex); + desktop_name = helper_state.input_desktop_name; + session_locked = helper_state.session_locked; + input_desktop_available = helper_state.input_desktop_available; + lock_app_visible = helper_state.lock_app_visible; + logon_ui_running = helper_state.logon_ui_visible; + secure_desktop_active = helper_state.secure_desktop_active; + } + const bool credential_ui_visible = + logon_ui_running || (session_locked && !input_desktop_available); + std::string stage = DetermineInteractiveStage( + lock_app_visible, credential_ui_visible, secure_desktop_active); + + if (desktop_name != last_desktop_name || + session_locked != last_session_locked || + lock_app_visible != last_lock_app || + logon_ui_running != last_logon_ui || + secure_desktop_active != last_secure_desktop || + stage != last_stage) { + LOG_INFO("Session helper state: session_id={}, input_desktop='{}', session_locked={}, lock_app_visible={}, logon_ui_running={}, secure_desktop_active={}, stage={}", + current_session_id, desktop_name, session_locked, lock_app_visible, + logon_ui_running, secure_desktop_active, stage); + last_desktop_name = desktop_name; + last_session_locked = session_locked; + last_lock_app = lock_app_visible; + last_logon_ui = logon_ui_running; + last_secure_desktop = secure_desktop_active; + last_stage = stage; + } + + DWORD wait_result = + stop_event != nullptr ? WaitForSingleObject(stop_event, 1000) : WAIT_TIMEOUT; + if (wait_result == WAIT_OBJECT_0) { + break; + } + if (wait_result != WAIT_TIMEOUT && wait_result != WAIT_FAILED) { + break; + } + } + + if (ipc_thread.joinable()) { + ipc_thread.join(); + } + + if (stop_event != nullptr) { + CloseHandle(stop_event); + } + + LOG_INFO("Session helper exiting: session_id={}", current_session_id); + return 0; +} \ No newline at end of file diff --git a/src/service/windows/session_helper_shared.h b/src/service/windows/session_helper_shared.h new file mode 100644 index 0000000..13a4c35 --- /dev/null +++ b/src/service/windows/session_helper_shared.h @@ -0,0 +1,51 @@ +#ifndef _CROSSDESK_SESSION_HELPER_SHARED_H_ +#define _CROSSDESK_SESSION_HELPER_SHARED_H_ + +#include + +#include +#include + +namespace crossdesk { + +inline constexpr wchar_t kCrossDeskSessionHelperPipePrefix[] = + L"\\\\.\\pipe\\CrossDeskSessionHelper-"; +inline constexpr wchar_t kCrossDeskSecureInputHelperPipePrefix[] = + L"\\\\.\\pipe\\CrossDeskSecureInputHelper-"; +inline constexpr char kCrossDeskSessionHelperStatusCommand[] = "status"; +inline constexpr char kCrossDeskSecureInputKeyboardCommandPrefix[] = + "keyboard:"; +inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = + "mouse:"; +inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = + "capture:"; +inline constexpr DWORD kCrossDeskSecureInputPipeBufferBytes = + 16 * 1024 * 1024; +inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358; +inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1; + +#pragma pack(push, 1) +struct CrossDeskSecureDesktopFrameHeader { + uint32_t magic; + uint32_t version; + int32_t left; + int32_t top; + uint32_t width; + uint32_t height; + uint32_t payload_size; +}; +#pragma pack(pop) + +inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) { + return std::wstring(kCrossDeskSessionHelperPipePrefix) + + std::to_wstring(session_id); +} + +inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) { + return std::wstring(kCrossDeskSecureInputHelperPipePrefix) + + std::to_wstring(session_id); +} + +} // namespace crossdesk + +#endif \ No newline at end of file diff --git a/xmake/targets.lua b/xmake/targets.lua index 78e10dd..484af70 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -34,7 +34,8 @@ function setup_targets() add_files("src/screen_capturer/windows/screen_capturer_dxgi.cpp", "src/screen_capturer/windows/screen_capturer_gdi.cpp", "src/screen_capturer/windows/screen_capturer_win.cpp") - add_includedirs("src/screen_capturer/windows", {public = true}) + add_includedirs("src/screen_capturer/windows", "src/service/windows", + {public = true}) elseif is_os("macosx") then add_files("src/screen_capturer/macosx/*.cpp", "src/screen_capturer/macosx/*.mm") @@ -146,7 +147,8 @@ function setup_targets() "src/gui/windows", {public = true}) if is_os("windows") then add_files("src/gui/tray/*.cpp") - add_includedirs("src/gui/tray", {public = true}) + add_includedirs("src/gui/tray", "src/service/windows", + {public = true}) elseif is_os("macosx") then add_files("src/gui/windows/*.mm") end @@ -163,6 +165,22 @@ function setup_targets() "src/screen_capturer/windows/wgc_plugin_entry.cpp") add_includedirs("src/common", "src/screen_capturer", "src/screen_capturer/windows") + + target("crossdesk_service") + set_kind("binary") + add_deps("rd_log", "path_manager") + add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv") + add_files("src/service/windows/main.cpp", + "src/service/windows/service_host.cpp") + add_includedirs("src/service/windows", {public = true}) + + target("crossdesk_session_helper") + set_kind("binary") + add_packages("libyuv") + add_deps("rd_log", "path_manager") + add_links("Advapi32", "User32", "Wtsapi32", "Gdi32") + add_files("src/service/windows/session_helper_main.cpp") + add_includedirs("src/service/windows", {public = true}) end target("crossdesk") @@ -171,6 +189,9 @@ function setup_targets() add_files("src/app/*.cpp") add_includedirs("src/app", {public = true}) if is_os("windows") then + add_files("src/service/windows/service_host.cpp") + add_includedirs("src/service/windows", {public = true}) + add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv") add_deps("wgc_plugin") add_files("scripts/windows/crossdesk.rc") end