From ffa94986d55b7efdd2b385194bbe43d735818160 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 21 Apr 2026 04:10:08 +0800 Subject: [PATCH 1/9] [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 From 97e48bfe71b578de0cab2a2e7b86e43c7a15a55d Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 21 Apr 2026 14:47:10 +0800 Subject: [PATCH 2/9] [fix] fix Wayland keyboard capture by using SDL key events --- src/device_controller/keyboard_converter.h | 6 + src/gui/render.cpp | 50 +++-- src/gui/render.h | 11 +- src/gui/render_callback.cpp | 233 +++++++++++++++++++-- 4 files changed, 261 insertions(+), 39 deletions(-) diff --git a/src/device_controller/keyboard_converter.h b/src/device_controller/keyboard_converter.h index 54616c5..4ef7f9f 100644 --- a/src/device_controller/keyboard_converter.h +++ b/src/device_controller/keyboard_converter.h @@ -98,6 +98,7 @@ std::map vkCodeToCGKeyCode = { {0x67, 0x59}, // Numpad 7 {0x68, 0x5B}, // Numpad 8 {0x69, 0x5C}, // Numpad 9 + {0x90, 0x47}, // Num Lock / Keypad Clear {0x6E, 0x41}, // Numpad . {0x6F, 0x4B}, // Numpad / {0x6A, 0x43}, // Numpad * @@ -216,6 +217,7 @@ std::map CGKeyCodeToVkCode = { {0x59, 0x67}, // Numpad 7 {0x5B, 0x68}, // Numpad 8 {0x5C, 0x69}, // Numpad 9 + {0x47, 0x90}, // Num Lock / Keypad Clear {0x41, 0x6E}, // Numpad . {0x4B, 0x6F}, // Numpad / {0x43, 0x6A}, // Numpad * @@ -336,6 +338,7 @@ std::map vkCodeToX11KeySym = { {0x67, 0xFFB7}, // Numpad 7 {0x68, 0xFFB8}, // Numpad 8 {0x69, 0xFFB9}, // Numpad 9 + {0x90, 0xFF7F}, // Num Lock {0x6E, 0xFFAE}, // Numpad . {0x6F, 0xFFAF}, // Numpad / {0x6A, 0xFFAA}, // Numpad * @@ -464,6 +467,7 @@ std::map x11KeySymToVkCode = { {0xFFB7, 0x67}, // Numpad 7 {0xFFB8, 0x68}, // Numpad 8 {0xFFB9, 0x69}, // Numpad 9 + {0xFF7F, 0x90}, // Num Lock {0xFFAE, 0x6E}, // Numpad . {0xFFAF, 0x6F}, // Numpad / {0xFFAA, 0x6A}, // Numpad * @@ -582,6 +586,7 @@ std::map cgKeyCodeToX11KeySym = { {0x59, 0xFFB7}, // Numpad 7 {0x5B, 0xFFB8}, // Numpad 8 {0x5C, 0xFFB9}, // Numpad 9 + {0x47, 0xFF7F}, // Num Lock / Keypad Clear {0x41, 0xFFAE}, // Numpad . {0x4B, 0xFFAF}, // Numpad / {0x43, 0xFFAA}, // Numpad * @@ -708,6 +713,7 @@ std::map x11KeySymToCgKeyCode = { {0xFFB7, 0x59}, // Numpad 7 {0xFFB8, 0x5B}, // Numpad 8 {0xFFB9, 0x5C}, // Numpad 9 + {0xFF7F, 0x47}, // Num Lock / Keypad Clear {0xFFAE, 0x41}, // Numpad . {0xFFAF, 0x4B}, // Numpad / {0xFFAA, 0x43}, // Numpad * diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 8192b56..8a843b2 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -98,8 +98,7 @@ RemoteAction BuildWindowsServiceStatusAction( 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'; + action.ss.interactive_stage[sizeof(action.ss.interactive_stage) - 1] = '\0'; return action; } @@ -795,6 +794,13 @@ int Render::StopMouseController() { } int Render::StartKeyboardCapturer() { +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + LOG_INFO("Start keyboard capturer with SDL Wayland backend"); + return 0; + } +#endif + if (!keyboard_capturer_) { LOG_INFO("keyboard capturer is nullptr"); return -1; @@ -818,6 +824,13 @@ int Render::StartKeyboardCapturer() { } int Render::StopKeyboardCapturer() { +#if defined(__linux__) && !defined(__APPLE__) + if (IsWaylandSession()) { + LOG_INFO("Stop keyboard capturer with SDL Wayland backend"); + return 0; + } +#endif + if (keyboard_capturer_) { keyboard_capturer_->Unhook(); LOG_INFO("Stop keyboard capturer"); @@ -1883,19 +1896,18 @@ void Render::HandleWindowsServiceIntegration() { return; } - const bool has_connected_remote = std::any_of( - connection_status_.begin(), connection_status_.end(), - [](const auto& entry) { - return entry.second == ConnectionStatus::Connected; - }); + 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)) { + 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); @@ -1931,10 +1943,12 @@ void Render::HandleWindowsServiceIntegration() { 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"); + LOG_INFO( + "Local Windows service available for secure desktop integration"); } else { LOG_WARN( - "Local Windows service unavailable, secure desktop integration disabled: error={}, code={}", + "Local Windows service unavailable, secure desktop integration " + "disabled: error={}, code={}", status.error, status.error_code); } last_logged_service_available = status.available; @@ -1944,7 +1958,8 @@ void Render::HandleWindowsServiceIntegration() { } 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"); + "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; @@ -2670,7 +2685,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) { case SDL_EVENT_WINDOW_FOCUS_LOST: if (stream_window_ && SDL_GetWindowID(stream_window_) == event.window.windowID) { - ForceReleasePressedModifiers(); + ForceReleasePressedKeys(); focus_on_stream_window_ = false; } else if (main_window_ && SDL_GetWindowID(main_window_) == event.window.windowID) { @@ -2702,6 +2717,15 @@ void Render::ProcessSdlEvent(const SDL_Event& event) { break; } + case SDL_EVENT_KEY_DOWN: + case SDL_EVENT_KEY_UP: + if (keyboard_capturer_is_started_ && focus_on_stream_window_ && + stream_window_ && + SDL_GetWindowID(stream_window_) == event.key.windowID) { + ProcessKeyboardEvent(event); + } + break; + default: if (event.type == STREAM_REFRESH_EVENT) { auto* props = static_cast(event.user.data1); diff --git a/src/gui/render.h b/src/gui/render.h index 35e1ee5..950f4d6 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -287,7 +287,7 @@ class Render { void ResetRemoteServiceStatus(SubStreamWindowProperties& props); void ApplyRemoteServiceStatus(SubStreamWindowProperties& props, const ServiceStatus& status); - RemoteUnlockState GetRemoteUnlockState( + RemoteUnlockState GetRemoteUnlockState( const SubStreamWindowProperties& props) const; #ifdef __APPLE__ int RequestPermissionWindow(); @@ -342,8 +342,9 @@ class Render { private: int SendKeyCommand(int key_code, bool is_down); static bool IsModifierVkKey(int key_code); - void UpdatePressedModifierState(int key_code, bool is_down); - void ForceReleasePressedModifiers(); + void TrackPressedKeyState(int key_code, bool is_down); + void ForceReleasePressedKeys(); + int ProcessKeyboardEvent(const SDL_Event& event); int ProcessMouseEvent(const SDL_Event& event); static void SdlCaptureAudioIn(void* userdata, Uint8* stream, int len); @@ -532,8 +533,8 @@ class Render { std::string controlled_remote_id_ = ""; std::string focused_remote_id_ = ""; std::string remote_client_id_ = ""; - std::unordered_set pressed_modifier_keys_; - std::mutex pressed_modifier_keys_mutex_; + std::unordered_set pressed_keyboard_keys_; + std::mutex pressed_keyboard_keys_mutex_; SDL_Event last_mouse_event; SDL_AudioStream* output_stream_; uint32_t STREAM_REFRESH_EVENT = 0; diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index 35cc2ec..19fc679 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -28,6 +28,178 @@ namespace crossdesk { namespace { +int TranslateSdlKeypadScancodeToVk(SDL_Scancode scancode) { + switch (scancode) { + case SDL_SCANCODE_NUMLOCKCLEAR: + return 0x90; + case SDL_SCANCODE_KP_ENTER: + return 0x0D; + case SDL_SCANCODE_KP_0: + return 0x60; + case SDL_SCANCODE_KP_1: + return 0x61; + case SDL_SCANCODE_KP_2: + return 0x62; + case SDL_SCANCODE_KP_3: + return 0x63; + case SDL_SCANCODE_KP_4: + return 0x64; + case SDL_SCANCODE_KP_5: + return 0x65; + case SDL_SCANCODE_KP_6: + return 0x66; + case SDL_SCANCODE_KP_7: + return 0x67; + case SDL_SCANCODE_KP_8: + return 0x68; + case SDL_SCANCODE_KP_9: + return 0x69; + case SDL_SCANCODE_KP_PERIOD: + case SDL_SCANCODE_KP_COMMA: + return 0x6E; + case SDL_SCANCODE_KP_DIVIDE: + return 0x6F; + case SDL_SCANCODE_KP_MULTIPLY: + return 0x6A; + case SDL_SCANCODE_KP_MINUS: + return 0x6D; + case SDL_SCANCODE_KP_PLUS: + return 0x6B; + case SDL_SCANCODE_KP_EQUALS: + return 0xBB; + default: + return -1; + } +} + +int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent& event) { + const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event.scancode); + if (keypad_key_code >= 0) { + return keypad_key_code; + } + + const int key = static_cast(event.key); + if (key >= 'a' && key <= 'z') { + return key - 'a' + 0x41; + } + if (key >= 'A' && key <= 'Z') { + return key; + } + if (key >= '0' && key <= '9') { + return key; + } + + switch (key) { + case ';': + return 0xBA; + case '\'': + return 0xDE; + case '`': + return 0xC0; + case ',': + return 0xBC; + case '.': + return 0xBE; + case '/': + return 0xBF; + case '\\': + return 0xDC; + case '[': + return 0xDB; + case ']': + return 0xDD; + case '-': + return 0xBD; + case '=': + return 0xBB; + default: + break; + } + + switch (event.scancode) { + case SDL_SCANCODE_ESCAPE: + return 0x1B; + case SDL_SCANCODE_RETURN: + return 0x0D; + case SDL_SCANCODE_SPACE: + return 0x20; + case SDL_SCANCODE_BACKSPACE: + return 0x08; + case SDL_SCANCODE_TAB: + return 0x09; + case SDL_SCANCODE_PRINTSCREEN: + return 0x2C; + case SDL_SCANCODE_SCROLLLOCK: + return 0x91; + case SDL_SCANCODE_PAUSE: + return 0x13; + case SDL_SCANCODE_INSERT: + return 0x2D; + case SDL_SCANCODE_DELETE: + return 0x2E; + case SDL_SCANCODE_HOME: + return 0x24; + case SDL_SCANCODE_END: + return 0x23; + case SDL_SCANCODE_PAGEUP: + return 0x21; + case SDL_SCANCODE_PAGEDOWN: + return 0x22; + case SDL_SCANCODE_LEFT: + return 0x25; + case SDL_SCANCODE_RIGHT: + return 0x27; + case SDL_SCANCODE_UP: + return 0x26; + case SDL_SCANCODE_DOWN: + return 0x28; + case SDL_SCANCODE_F1: + return 0x70; + case SDL_SCANCODE_F2: + return 0x71; + case SDL_SCANCODE_F3: + return 0x72; + case SDL_SCANCODE_F4: + return 0x73; + case SDL_SCANCODE_F5: + return 0x74; + case SDL_SCANCODE_F6: + return 0x75; + case SDL_SCANCODE_F7: + return 0x76; + case SDL_SCANCODE_F8: + return 0x77; + case SDL_SCANCODE_F9: + return 0x78; + case SDL_SCANCODE_F10: + return 0x79; + case SDL_SCANCODE_F11: + return 0x7A; + case SDL_SCANCODE_F12: + return 0x7B; + case SDL_SCANCODE_CAPSLOCK: + return 0x14; + case SDL_SCANCODE_LSHIFT: + return 0xA0; + case SDL_SCANCODE_RSHIFT: + return 0xA1; + case SDL_SCANCODE_LCTRL: + return 0xA2; + case SDL_SCANCODE_RCTRL: + return 0xA3; + case SDL_SCANCODE_LALT: + return 0xA4; + case SDL_SCANCODE_RALT: + return 0xA5; + case SDL_SCANCODE_LGUI: + return 0x5B; + case SDL_SCANCODE_RGUI: + return 0x5C; + default: + return -1; + } +} + #if _WIN32 constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000; @@ -36,8 +208,7 @@ bool BuildAbsoluteMousePosition(const std::vector& displays, 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())) { + display_index < 0 || display_index >= static_cast(displays.size())) { return false; } @@ -152,29 +323,29 @@ bool Render::IsModifierVkKey(int key_code) { } } -void Render::UpdatePressedModifierState(int key_code, bool is_down) { - if (!IsModifierVkKey(key_code)) { +void Render::TrackPressedKeyState(int key_code, bool is_down) { + if (!IsWaylandSession() && !IsModifierVkKey(key_code)) { return; } - std::lock_guard lock(pressed_modifier_keys_mutex_); + std::lock_guard lock(pressed_keyboard_keys_mutex_); if (is_down) { - pressed_modifier_keys_.insert(key_code); + pressed_keyboard_keys_.insert(key_code); } else { - pressed_modifier_keys_.erase(key_code); + pressed_keyboard_keys_.erase(key_code); } } -void Render::ForceReleasePressedModifiers() { +void Render::ForceReleasePressedKeys() { std::vector pressed_keys; { - std::lock_guard lock(pressed_modifier_keys_mutex_); - if (pressed_modifier_keys_.empty()) { + std::lock_guard lock(pressed_keyboard_keys_mutex_); + if (pressed_keyboard_keys_.empty()) { return; } - pressed_keys.assign(pressed_modifier_keys_.begin(), - pressed_modifier_keys_.end()); - pressed_modifier_keys_.clear(); + pressed_keys.assign(pressed_keyboard_keys_.begin(), + pressed_keyboard_keys_.end()); + pressed_keyboard_keys_.clear(); } for (int key_code : pressed_keys) { @@ -210,11 +381,28 @@ int Render::SendKeyCommand(int key_code, bool is_down) { } } - UpdatePressedModifierState(key_code, is_down); + TrackPressedKeyState(key_code, is_down); return 0; } +int Render::ProcessKeyboardEvent(const SDL_Event& event) { + if (event.type != SDL_EVENT_KEY_DOWN && event.type != SDL_EVENT_KEY_UP) { + return -1; + } + + if (event.type == SDL_EVENT_KEY_DOWN && event.key.repeat) { + return 0; + } + + const int key_code = TranslateSdlKeyboardEventToVk(event.key); + if (key_code < 0) { + return 0; + } + + return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN); +} + int Render::ProcessMouseEvent(const SDL_Event& event) { controlled_remote_id_ = ""; RemoteAction remote_action; @@ -292,7 +480,8 @@ int Render::ProcessMouseEvent(const SDL_Event& event) { } if (is_pointer_position_event && cursor_x >= render_rect.x && - cursor_x <= render_rect.x + render_rect.w && cursor_y >= render_rect.y && + cursor_x <= render_rect.x + render_rect.w && + cursor_y >= render_rect.y && cursor_y <= render_rect.y + render_rect.h) { controlled_remote_id_ = it.first; last_mouse_event.motion.x = cursor_x; @@ -827,9 +1016,9 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size, 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); + "Secure desktop mouse injection skipped, invalid display " + "mapping: display_index={}, x={}, y={}", + render->selected_display_, remote_action.m.x, remote_action.m.y); return; } @@ -842,7 +1031,8 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size, &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={}", + "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); } @@ -1153,8 +1343,9 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id, // Keep Wayland capture session warm to avoid black screen on // subsequent reconnects. render->start_screen_capturer_ = true; - LOG_INFO("Keeping Wayland screen capturer running after " - "disconnect to preserve reconnect stability"); + LOG_INFO( + "Keeping Wayland screen capturer running after " + "disconnect to preserve reconnect stability"); } else { render->start_screen_capturer_ = false; } From d3b886c3f647c6b1d139b8ac8b7b4f7ce8438f19 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 21 Apr 2026 16:52:59 +0800 Subject: [PATCH 3/9] [fix] fix blocking issue on controlled-side during shutdown --- .../windows/screen_capturer_win.cpp | 63 ++-- src/service/windows/interactive_state.h | 10 +- src/service/windows/main.cpp | 24 +- src/service/windows/service_host.cpp | 343 ++++++++---------- src/service/windows/service_host.h | 8 +- src/service/windows/session_helper_main.cpp | 137 +++---- src/service/windows/session_helper_shared.h | 15 +- 7 files changed, 287 insertions(+), 313 deletions(-) diff --git a/src/screen_capturer/windows/screen_capturer_win.cpp b/src/screen_capturer/windows/screen_capturer_win.cpp index d3477ee..3a38256 100644 --- a/src/screen_capturer/windows/screen_capturer_win.cpp +++ b/src/screen_capturer/windows/screen_capturer_win.cpp @@ -2,23 +2,22 @@ #include -#include - -#include #include +#include #include #include #include +#include #include #include #include #include #include +#include "interactive_state.h" #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" @@ -146,8 +145,7 @@ std::string ExtractPipeTextResponse(const std::vector& response) { bool IsTransientSecureDesktopFrameError(const std::string& error_message) { return error_message.rfind("pipe_unavailable:", 0) == 0 || - error_message.find("\"error\":\"bitblt_failed\"") != - std::string::npos; + error_message.find("\"error\":\"bitblt_failed\"") != std::string::npos; } bool ReadPipeMessage(HANDLE pipe, std::vector* response_out, @@ -266,9 +264,9 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) { 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)); + 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 || @@ -287,7 +285,8 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top, return false; } - const std::wstring pipe_name = GetCrossDeskSecureInputHelperPipeName(session_id); + 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()); @@ -416,10 +415,8 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) { } int ScreenCapturerWin::Destroy() { - StopSecureCaptureThread(); - running_.store(false, std::memory_order_relaxed); + Stop(); paused_.store(false, std::memory_order_relaxed); - secure_desktop_capture_active_.store(false, std::memory_order_relaxed); if (impl_) { impl_->Destroy(); impl_.reset(); @@ -585,8 +582,8 @@ void ScreenCapturerWin::StopSecureCaptureThread() { } } -bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, - int* width, int* height, +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) { @@ -653,11 +650,13 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { if (service_changed || service_error_changed) { if (status.service_available) { LOG_INFO( - "Windows capturer secure desktop service available, polling session_id={}", + "Windows capturer secure desktop service available, polling " + "session_id={}", status.active_session_id); } else { LOG_WARN( - "Windows capturer secure desktop service unavailable: error={}, code={}", + "Windows capturer secure desktop service unavailable: " + "error={}, code={}", status.error, status.error_code); } last_service_available = status.service_available; @@ -665,8 +664,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { } } else if (last_service_available || last_service_error != "invalid_service_status_json") { - LOG_WARN( - "Windows capturer secure desktop service status query failed"); + LOG_WARN("Windows capturer secure desktop service status query failed"); last_service_available = false; last_service_error = "invalid_service_status_json"; } @@ -677,7 +675,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { status.interactive_stage != last_stage) { capture_stage_started_tick = now; LOG_INFO( - "Windows capturer secure desktop state: active={}, stage='{}', session_id={}", + "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; @@ -687,8 +686,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { } if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { - std::this_thread::sleep_for(std::chrono::milliseconds( - status.service_available ? 50 : 200)); + std::this_thread::sleep_for( + std::chrono::milliseconds(status.service_available ? 50 : 200)); continue; } @@ -721,22 +720,22 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() { } 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; + 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; + 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; } } diff --git a/src/service/windows/interactive_state.h b/src/service/windows/interactive_state.h index 1c36708..8476983 100644 --- a/src/service/windows/interactive_state.h +++ b/src/service/windows/interactive_state.h @@ -12,11 +12,11 @@ inline bool IsSecureDesktopInteractionRequired( } 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) { + 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; } diff --git a/src/service/windows/main.cpp b/src/service/windows/main.cpp index 8d88458..a8c1a5c 100644 --- a/src/service/windows/main.cpp +++ b/src/service/windows/main.cpp @@ -17,16 +17,17 @@ std::wstring GetExecutablePath() { } 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"; + 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 @@ -55,8 +56,7 @@ int main(int argc, char* argv[]) { } if (command == "--uninstall") { bool success = crossdesk::UninstallCrossDeskService(); - std::cout << (success ? "uninstall ok" : "uninstall failed") - << std::endl; + std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl; return success ? 0 : 1; } if (command == "--start") { diff --git a/src/service/windows/service_host.cpp b/src/service/windows/service_host.cpp index 9b5f4dc..c135f6b 100644 --- a/src/service/windows/service_host.cpp +++ b/src/service/windows/service_host.cpp @@ -1,17 +1,16 @@ #include "service_host.h" -#include - +#include #include #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -26,10 +25,8 @@ namespace { using Json = nlohmann::json; -constexpr char kSecureDesktopKeyboardIpcCommandPrefix[] = - "secure-input-key:"; -constexpr char kSecureDesktopMouseIpcCommandPrefix[] = - "secure-input-mouse:"; +constexpr char kSecureDesktopKeyboardIpcCommandPrefix[] = "secure-input-key:"; +constexpr char kSecureDesktopMouseIpcCommandPrefix[] = "secure-input-mouse:"; using SendSasFunction = VOID(WINAPI*)(BOOL); @@ -231,13 +228,12 @@ std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down) { std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel, int flag) { std::ostringstream stream; - stream << kSecureDesktopMouseIpcCommandPrefix << x << ":" << y << ":" - << wheel << ":" << flag; + stream << kSecureDesktopMouseIpcCommandPrefix << x << ":" << y << ":" << wheel + << ":" << flag; return stream.str(); } -std::string BuildSecureInputHelperKeyboardCommand(int key_code, - bool is_down) { +std::string BuildSecureInputHelperKeyboardCommand(int key_code, bool is_down) { std::ostringstream stream; stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":" << (is_down ? 1 : 0); @@ -487,15 +483,15 @@ std::string WideToUtf8(const std::wstring& value) { return {}; } - int size_needed = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, - nullptr, 0, nullptr, nullptr); + 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); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, result.data(), size_needed, + nullptr, nullptr); result.pop_back(); return result; } @@ -519,8 +515,7 @@ bool QuerySoftwareSasGeneration(DWORD* value_out, bool* existed_out) { DWORD value_size = sizeof(value); DWORD type = REG_DWORD; LONG result = RegQueryValueExW(key, L"SoftwareSASGeneration", nullptr, &type, - reinterpret_cast(&value), - &value_size); + reinterpret_cast(&value), &value_size); if (result == ERROR_SUCCESS && type == REG_DWORD) { *value_out = value; *existed_out = true; @@ -542,8 +537,7 @@ bool SetSoftwareSasGeneration(DWORD value) { } result = RegSetValueExW(key, L"SoftwareSASGeneration", 0, REG_DWORD, - reinterpret_cast(&value), - sizeof(value)); + reinterpret_cast(&value), sizeof(value)); RegCloseKey(key); return result == ERROR_SUCCESS; } @@ -552,8 +546,8 @@ 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); + LONG result = + RegOpenKeyExW(HKEY_LOCAL_MACHINE, kPolicyKey, 0, KEY_SET_VALUE, &key); if (result != ERROR_SUCCESS) { return false; } @@ -580,8 +574,8 @@ SasResult SendSasNow() { return result; } - auto* send_sas = reinterpret_cast( - GetProcAddress(sas_module, "SendSAS")); + auto* send_sas = + reinterpret_cast(GetProcAddress(sas_module, "SendSAS")); if (send_sas == nullptr) { result.error = "send_sas_proc_missing"; result.error_code = GetLastError(); @@ -625,8 +619,7 @@ struct PipeSecurityAttributes { } bool Initialize() { - constexpr wchar_t kPipeSddl[] = - L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)"; + 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; @@ -654,8 +647,7 @@ struct KernelObjectSecurityAttributes { } bool Initialize() { - constexpr wchar_t kObjectSddl[] = - L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GA;;;AU)"; + 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; @@ -726,8 +718,7 @@ DWORD WINAPI CrossDeskServiceHost::ServiceControlHandler( case SERVICE_CONTROL_SESSIONCHANGE: { DWORD session_id = 0xFFFFFFFF; if (event_data != nullptr) { - auto* session = - reinterpret_cast(event_data); + auto* session = reinterpret_cast(event_data); session_id = session->dwSessionId; } instance_->RecordSessionEvent(event_type, session_id); @@ -742,7 +733,8 @@ int CrossDeskServiceHost::RunAsService() { instance_ = this; SERVICE_TABLE_ENTRYW service_table[] = { - {const_cast(kCrossDeskServiceName), &CrossDeskServiceHost::ServiceMain}, + {const_cast(kCrossDeskServiceName), + &CrossDeskServiceHost::ServiceMain}, {nullptr, nullptr}}; if (!StartServiceCtrlDispatcherW(service_table)) { @@ -897,7 +889,8 @@ int CrossDeskServiceHost::RunServiceLoop(bool as_service) { if (console_mode_) { SetConsoleCtrlHandler(&CrossDeskServiceHost::ConsoleControlHandler, TRUE); - std::cout << "CrossDesk service skeleton running in console mode. Press Ctrl+C to stop." + std::cout << "CrossDesk service skeleton running in console mode. Press " + "Ctrl+C to stop." << std::endl; } @@ -932,10 +925,9 @@ void CrossDeskServiceHost::IpcServerLoop() { 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); + 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); @@ -1068,9 +1060,8 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked( return false; } - return HasSecureInputUiLocked() || - (GetEffectiveSessionLockedLocked() && - IsHelperReportingLockScreenLocked()); + return HasSecureInputUiLocked() || (GetEffectiveSessionLockedLocked() && + IsHelperReportingLockScreenLocked()); } std::wstring CrossDeskServiceHost::GetSessionHelperPath() const { @@ -1257,8 +1248,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) { event_attributes = event_security.get(); } - HANDLE stop_event_handle = CreateEventW(event_attributes, TRUE, FALSE, - stop_event_name.c_str()); + 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"; @@ -1280,8 +1271,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) { 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); + nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, + nullptr, &startup_info, &process_info); } else { HANDLE user_token = nullptr; HANDLE primary_token = nullptr; @@ -1315,8 +1306,7 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) { FALSE); created = CreateProcessAsUserW( primary_token, helper_path.c_str(), mutable_command_line.data(), - nullptr, nullptr, FALSE, - CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, + 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); @@ -1364,8 +1354,8 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) { event_attributes = event_security.get(); } - HANDLE stop_event_handle = CreateEventW(event_attributes, TRUE, FALSE, - stop_event_name.c_str()); + 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_ = @@ -1388,8 +1378,8 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) { 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); + nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, + nullptr, &startup_info, &process_info); } else { HANDLE primary_token = nullptr; ScopedEnvironmentBlock environment_block; @@ -1397,8 +1387,7 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) { 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_ = "create_session_system_token_failed"; secure_input_helper_last_error_code_ = error; return false; } @@ -1407,16 +1396,14 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) { FALSE); created = CreateProcessAsUserW( primary_token, helper_path.c_str(), mutable_command_line.data(), - nullptr, nullptr, FALSE, - CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW, + 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_ = "create_secure_input_helper_failed"; secure_input_helper_last_error_code_ = error; return false; } @@ -1515,25 +1502,24 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() { 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_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_ = + 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_ = + session_helper_report_credential_ui_visible_ = json.value("credential_ui_visible", false); - session_helper_report_unlock_ui_visible_ = + session_helper_report_unlock_ui_visible_ = json.value("unlock_ui_visible", false); - session_helper_report_interactive_stage_ = + 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); @@ -1582,8 +1568,7 @@ void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, } } -std::string CrossDeskServiceHost::HandleIpcCommand( - const std::string& command) { +std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command) { std::string normalized = ToLower(Trim(command)); if (normalized == "ping") { return "{\"ok\":true,\"reply\":\"pong\"}"; @@ -1596,8 +1581,7 @@ std::string CrossDeskServiceHost::HandleIpcCommand( } int key_code = 0; bool is_down = false; - if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, - &is_down)) { + if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, &is_down)) { return SendSecureDesktopKeyboardInput(key_code, is_down); } return BuildErrorJson("unknown_command"); @@ -1639,16 +1623,16 @@ std::string CrossDeskServiceHost::BuildStatusResponse() { 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_); + 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 = + 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 = + 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 = @@ -1658,9 +1642,9 @@ std::string CrossDeskServiceHost::BuildStatusResponse() { 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 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_); @@ -1670,121 +1654,113 @@ std::string CrossDeskServiceHost::BuildStatusResponse() { 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; + 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::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_ + << ",\"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") + << ",\"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_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") + << ",\"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\":" @@ -1889,10 +1865,10 @@ bool InstallCrossDeskService(const std::wstring& binary_path) { return false; } - if (!ChangeServiceConfigW( - service, SERVICE_NO_CHANGE, SERVICE_AUTO_START, - SERVICE_NO_CHANGE, service_command.c_str(), nullptr, nullptr, - nullptr, nullptr, nullptr, kCrossDeskServiceDisplayName)) { + 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); @@ -1973,8 +1949,8 @@ bool StopCrossDeskService(DWORD timeout_ms) { DWORD deadline = GetTickCount() + timeout_ms; while (GetTickCount() < deadline) { if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, - reinterpret_cast(&status), - sizeof(status), &bytes_needed)) { + reinterpret_cast(&status), sizeof(status), + &bytes_needed)) { LOG_ERROR("QueryServiceStatusEx failed, error={}", GetLastError()); CloseServiceHandle(service); CloseServiceHandle(manager); @@ -2001,8 +1977,9 @@ bool UninstallCrossDeskService() { return false; } - SC_HANDLE service = OpenServiceW(manager, kCrossDeskServiceName, - DELETE | SERVICE_STOP | SERVICE_QUERY_STATUS); + SC_HANDLE service = + OpenServiceW(manager, kCrossDeskServiceName, + DELETE | SERVICE_STOP | SERVICE_QUERY_STATUS); if (service == nullptr) { DWORD error = GetLastError(); CloseServiceHandle(manager); @@ -2037,9 +2014,9 @@ std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down, } std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel, - int flag, DWORD timeout_ms) { + int flag, DWORD timeout_ms) { return QueryCrossDeskService( - BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms); + 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 index 0ded115..726029d 100644 --- a/src/service/windows/service_host.h +++ b/src/service/windows/service_host.h @@ -10,8 +10,7 @@ namespace crossdesk { inline constexpr wchar_t kCrossDeskServiceName[] = L"CrossDeskService"; -inline constexpr wchar_t kCrossDeskServiceDisplayName[] = - L"CrossDesk Service"; +inline constexpr wchar_t kCrossDeskServiceDisplayName[] = L"CrossDesk Service"; inline constexpr wchar_t kCrossDeskServicePipeName[] = L"\\\\.\\pipe\\CrossDeskService"; @@ -59,8 +58,7 @@ class CrossDeskServiceHost { 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); + LPVOID event_data, LPVOID context); private: SERVICE_STATUS_HANDLE status_handle_ = nullptr; @@ -98,7 +96,7 @@ class CrossDeskServiceHost { 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_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; diff --git a/src/service/windows/session_helper_main.cpp b/src/service/windows/session_helper_main.cpp index e986688..126f0ad 100644 --- a/src/service/windows/session_helper_main.cpp +++ b/src/service/windows/session_helper_main.cpp @@ -1,17 +1,14 @@ -#include - -#include - -#include - #include +#include #include +#include #include #include #include #include #include +#include #include #include #include @@ -23,8 +20,8 @@ namespace { -using crossdesk::InitLogger; using crossdesk::get_logger; +using crossdesk::InitLogger; using Json = nlohmann::json; struct InputDesktopInfo { @@ -76,8 +73,7 @@ struct PipeSecurityAttributes { } bool Initialize() { - constexpr wchar_t kPipeSddl[] = - L"D:(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGW;;;AU)"; + 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; @@ -100,7 +96,8 @@ 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"; + std::filesystem::path log_path = + path_manager.GetLogPath() / "session_helper"; if (!log_path.empty() && path_manager.CreateDirectories(log_path)) { InitLogger(log_path.string()); return; @@ -114,8 +111,8 @@ std::wstring Utf8ToWide(const std::string& value) { return {}; } - int size_needed = MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, - nullptr, 0); + int size_needed = + MultiByteToWideChar(CP_UTF8, 0, value.c_str(), -1, nullptr, 0); if (size_needed <= 1) { return {}; } @@ -132,15 +129,15 @@ std::string WideToUtf8(const std::wstring& value) { return {}; } - int size_needed = WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, - nullptr, 0, nullptr, nullptr); + 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); + WideCharToMultiByte(CP_UTF8, 0, value.c_str(), -1, result.data(), size_needed, + nullptr, nullptr); result.pop_back(); return result; } @@ -179,7 +176,7 @@ InputDesktopInfo GetInputDesktopInfo() { } bool IsProcessRunningInCurrentSession(const wchar_t* executable_name, - DWORD session_id) { + DWORD session_id) { HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshot == INVALID_HANDLE_VALUE) { return false; @@ -325,15 +322,14 @@ std::string BuildHelperStatusResponse(HelperState* helper_state) { 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["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["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) @@ -349,12 +345,14 @@ void HelperIpcServerLoop(HANDLE stop_event, DWORD session_id, pipe_attributes = security_attributes.get(); } - std::wstring pipe_name = crossdesk::GetCrossDeskSessionHelperPipeName(session_id); - while (stop_event == nullptr || WaitForSingleObject(stop_event, 0) != WAIT_OBJECT_0) { + 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); + 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) { @@ -465,10 +463,9 @@ bool EnsureThreadDesktop(const wchar_t* desktop_name, return true; } - HDESK desktop = OpenDesktopW( - desktop_name, 0, FALSE, - DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | DESKTOP_READOBJECTS | - DESKTOP_SWITCHDESKTOP); + HDESK desktop = OpenDesktopW(desktop_name, 0, FALSE, + DESKTOP_CREATEWINDOW | DESKTOP_WRITEOBJECTS | + DESKTOP_READOBJECTS | DESKTOP_SWITCHDESKTOP); if (desktop == nullptr) { return false; } @@ -515,14 +512,13 @@ int InjectKeyboardInput(int key_code, bool is_down) { } bool ParseSecureInputKeyboardCommand(const std::string& command, - int* key_code_out, - bool* is_down_out) { + 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) { + if (command.rfind(crossdesk::kCrossDeskSecureInputKeyboardCommandPrefix, 0) != + 0) { return false; } @@ -552,7 +548,7 @@ bool ParseSecureInputKeyboardCommand(const std::string& command, } bool ParseSecureInputMouseCommand(const std::string& command, - SecureMouseRequest* request_out) { + SecureMouseRequest* request_out) { if (request_out == nullptr) { return false; } @@ -622,8 +618,7 @@ bool ParseSecureInputCaptureCommand(const std::string& command, 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; + const size_t token_end = is_last ? command.size() : separator; if (token_end == std::string::npos || token_end <= token_begin) { return false; } @@ -734,8 +729,7 @@ std::vector CaptureSecureDesktopFrame( const DWORD error = GetLastError(); DeleteDC(mem_dc); ReleaseDC(nullptr, screen_dc); - return BuildTextResponseBytes( - BuildErrorJson("create_dib_failed", error)); + return BuildTextResponseBytes(BuildErrorJson("create_dib_failed", error)); } HGDIOBJ old_bitmap = SelectObject(mem_dc, dib); @@ -746,8 +740,7 @@ std::vector CaptureSecureDesktopFrame( DeleteObject(dib); DeleteDC(mem_dc); ReleaseDC(nullptr, screen_dc); - return BuildTextResponseBytes( - BuildErrorJson("bitblt_failed", error)); + return BuildTextResponseBytes(BuildErrorJson("bitblt_failed", error)); } if (request.show_cursor) { @@ -757,8 +750,8 @@ std::vector CaptureSecureDesktopFrame( 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) { + 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); } @@ -790,7 +783,8 @@ std::vector CaptureSecureDesktopFrame( 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()); + header.payload_size = + static_cast(capture_buffers->nv12_frame.size()); std::vector response(sizeof(header) + capture_buffers->nv12_frame.size()); @@ -804,8 +798,7 @@ std::vector CaptureSecureDesktopFrame( } std::vector HandleSecureInputHelperCommand( - const std::string& command, - SecureCaptureBuffers* capture_buffers) { + const std::string& command, SecureCaptureBuffers* capture_buffers) { if (command == "ping") { return BuildTextResponseBytes("{\"ok\":true,\"reply\":\"pong\"}"); } @@ -816,7 +809,8 @@ std::vector HandleSecureInputHelperCommand( 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={}", + "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))); @@ -836,7 +830,8 @@ std::vector HandleSecureInputHelperCommand( 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={}", + "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( @@ -898,10 +893,10 @@ void SecureInputHelperIpcServerLoop(HANDLE stop_event, DWORD session_id) { 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); + 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()); @@ -1010,8 +1005,10 @@ int main(int argc, char* argv[]) { } 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); + 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={}", @@ -1020,15 +1017,16 @@ int main(int argc, char* argv[]) { HDESK secure_desktop = nullptr; if (!EnsureThreadDesktop(L"Winlogon", &secure_desktop)) { - LOG_ERROR("Failed to switch secure input helper to Winlogon desktop, error={}", - GetLastError()); + 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: '{}'", + LOG_INFO("Secure input helper desktop: '{}'", WideToUtf8(GetCurrentThreadDesktopNameW())); SecureInputHelperIpcServerLoop(stop_event, current_session_id); @@ -1042,8 +1040,10 @@ int main(int argc, char* argv[]) { return 0; } - LOG_INFO("Session helper starting: pid={}, current_session_id={}, expected_session_id={}", - GetCurrentProcessId(), current_session_id, expected_session_id); + 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; @@ -1087,11 +1087,13 @@ int main(int argc, char* argv[]) { 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); + 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; @@ -1100,8 +1102,9 @@ int main(int argc, char* argv[]) { last_stage = stage; } - DWORD wait_result = - stop_event != nullptr ? WaitForSingleObject(stop_event, 1000) : WAIT_TIMEOUT; + DWORD wait_result = stop_event != nullptr + ? WaitForSingleObject(stop_event, 1000) + : WAIT_TIMEOUT; if (wait_result == WAIT_OBJECT_0) { break; } diff --git a/src/service/windows/session_helper_shared.h b/src/service/windows/session_helper_shared.h index 13a4c35..b68e8f5 100644 --- a/src/service/windows/session_helper_shared.h +++ b/src/service/windows/session_helper_shared.h @@ -11,16 +11,13 @@ namespace crossdesk { inline constexpr wchar_t kCrossDeskSessionHelperPipePrefix[] = L"\\\\.\\pipe\\CrossDeskSessionHelper-"; inline constexpr wchar_t kCrossDeskSecureInputHelperPipePrefix[] = - L"\\\\.\\pipe\\CrossDeskSecureInputHelper-"; + 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; + "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; @@ -43,7 +40,7 @@ inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) { inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) { return std::wstring(kCrossDeskSecureInputHelperPipePrefix) + - std::to_wstring(session_id); + std::to_wstring(session_id); } } // namespace crossdesk From 2be6e727ce548d9c3bb989bc13dc95e2882704cc Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 21 Apr 2026 17:37:19 +0800 Subject: [PATCH 4/9] [fix] use SDL keyboard capture on Wayland only --- src/gui/render.cpp | 29 ++++++---- src/gui/render.h | 3 +- src/gui/toolbars/control_bar.cpp | 88 ------------------------------ src/gui/windows/control_window.cpp | 1 - src/gui/windows/stream_window.cpp | 58 -------------------- 5 files changed, 19 insertions(+), 160 deletions(-) diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 8a843b2..0a2d459 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -794,16 +794,21 @@ int Render::StopMouseController() { } int Render::StartKeyboardCapturer() { + keyboard_capturer_uses_sdl_events_ = false; + #if defined(__linux__) && !defined(__APPLE__) if (IsWaylandSession()) { + keyboard_capturer_uses_sdl_events_ = true; LOG_INFO("Start keyboard capturer with SDL Wayland backend"); return 0; } #endif if (!keyboard_capturer_) { - LOG_INFO("keyboard capturer is nullptr"); - return -1; + keyboard_capturer_uses_sdl_events_ = true; + LOG_WARN( + "keyboard capturer is nullptr, falling back to SDL keyboard events"); + return 0; } int keyboard_capturer_init_ret = keyboard_capturer_->Hook( @@ -815,21 +820,23 @@ int Render::StartKeyboardCapturer() { }, this); if (0 != keyboard_capturer_init_ret) { - LOG_ERROR("Start keyboard capturer failed"); + keyboard_capturer_uses_sdl_events_ = true; + LOG_WARN( + "Start keyboard capturer failed, falling back to SDL keyboard " + "events"); } else { - LOG_INFO("Start keyboard capturer"); + LOG_INFO("Start keyboard capturer with native hook"); } - return keyboard_capturer_init_ret; + return 0; } int Render::StopKeyboardCapturer() { -#if defined(__linux__) && !defined(__APPLE__) - if (IsWaylandSession()) { - LOG_INFO("Stop keyboard capturer with SDL Wayland backend"); + if (keyboard_capturer_uses_sdl_events_) { + keyboard_capturer_uses_sdl_events_ = false; + LOG_INFO("Stop keyboard capturer with SDL keyboard backend"); return 0; } -#endif if (keyboard_capturer_) { keyboard_capturer_->Unhook(); @@ -2719,8 +2726,8 @@ void Render::ProcessSdlEvent(const SDL_Event& event) { case SDL_EVENT_KEY_DOWN: case SDL_EVENT_KEY_UP: - if (keyboard_capturer_is_started_ && focus_on_stream_window_ && - stream_window_ && + if (keyboard_capturer_is_started_ && keyboard_capturer_uses_sdl_events_ && + focus_on_stream_window_ && stream_window_ && SDL_GetWindowID(stream_window_) == event.key.windowID) { ProcessKeyboardEvent(event); } diff --git a/src/gui/render.h b/src/gui/render.h index 950f4d6..c6b8a34 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -282,8 +282,6 @@ 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); @@ -478,6 +476,7 @@ class Render { bool start_keyboard_capturer_ = false; bool show_cursor_ = false; bool keyboard_capturer_is_started_ = false; + bool keyboard_capturer_uses_sdl_events_ = false; bool foucs_on_main_window_ = false; bool focus_on_stream_window_ = false; bool main_window_minimized_ = false; diff --git a/src/gui/toolbars/control_bar.cpp b/src/gui/toolbars/control_bar.cpp index c820058..9f0cf30 100644 --- a/src/gui/toolbars/control_bar.cpp +++ b/src/gui/toolbars/control_bar.cpp @@ -193,94 +193,6 @@ 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/control_window.cpp b/src/gui/windows/control_window.cpp index 0183e38..bd55e4a 100644 --- a/src/gui/windows/control_window.cpp +++ b/src/gui/windows/control_window.cpp @@ -257,4 +257,3 @@ int Render::ControlWindow(std::shared_ptr& props) { return 0; } } // namespace crossdesk - diff --git a/src/gui/windows/stream_window.cpp b/src/gui/windows/stream_window.cpp index 44addbd..06e13d7 100644 --- a/src/gui/windows/stream_window.cpp +++ b/src/gui/windows/stream_window.cpp @@ -59,62 +59,6 @@ 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()) { @@ -229,7 +173,6 @@ int Render::StreamWindow() { FileTransferWindow(props); DrawReceivingScreenText(props); - DrawRemoteUnlockStateText(props); focused_remote_id_ = props->remote_id_; @@ -332,7 +275,6 @@ int Render::StreamWindow() { FileTransferWindow(props); DrawReceivingScreenText(props); - DrawRemoteUnlockStateText(props); ImGui::End(); From 4089e80fe8fa0465cb98622fdae8f7001506f6bd Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 21 Apr 2026 23:23:11 +0800 Subject: [PATCH 5/9] [feat] register and remove CrossDeskService in the Windows installer --- scripts/windows/nsis_script.nsi | 94 +++++++++++++++++++++ src/service/windows/interactive_state.h | 10 ++- src/service/windows/service_host.h | 10 ++- src/service/windows/session_helper_main.cpp | 4 +- src/service/windows/session_helper_shared.h | 10 ++- xmake/targets.lua | 2 +- 6 files changed, 122 insertions(+), 8 deletions(-) diff --git a/scripts/windows/nsis_script.nsi b/scripts/windows/nsis_script.nsi index 111b2f2..8c456ce 100644 --- a/scripts/windows/nsis_script.nsi +++ b/scripts/windows/nsis_script.nsi @@ -8,6 +8,7 @@ !define PRODUCT_WEB_SITE "https://www.crossdesk.cn/" !define APP_NAME "CrossDesk" !define UNINSTALL_REG_KEY "CrossDesk" +!define PRODUCT_SERVICE_NAME "CrossDeskService" ; Installer icon path !define MUI_ICON "${__FILEDIR__}\..\..\icons\windows\crossdesk.ico" @@ -68,14 +69,21 @@ cancelInstall: Abort installApp: + Call StopInstalledService + SetOutPath "$INSTDIR" SetOverwrite ifnewer ; Main application executable path File /oname=CrossDesk.exe "..\..\build\windows\x64\release\crossdesk.exe" + ; Bundle service-side binaries required by the Windows service flow + File "..\..\build\windows\x64\release\crossdesk_service.exe" + File "..\..\build\windows\x64\release\crossdesk_session_helper.exe" ; Bundle runtime DLLs from the release output directory File "..\..\build\windows\x64\release\*.dll" + Call RegisterInstalledService + ; Write uninstall information WriteUninstaller "$INSTDIR\uninstall.exe" @@ -122,8 +130,12 @@ cancelUninstall: Abort uninstallApp: + Call UnregisterInstalledService + ; Delete main executable and uninstaller Delete "$INSTDIR\CrossDesk.exe" + Delete "$INSTDIR\crossdesk_service.exe" + Delete "$INSTDIR\crossdesk_session_helper.exe" Delete "$INSTDIR\uninstall.exe" ; Recursively delete installation directory @@ -148,3 +160,85 @@ SectionEnd Function LaunchApp Exec "$INSTDIR\CrossDesk.exe" FunctionEnd + +Function StopInstalledService + IfFileExists "$INSTDIR\CrossDesk.exe" 0 stop_with_sc + IfFileExists "$INSTDIR\crossdesk_service.exe" 0 stop_with_sc + + DetailPrint "Stopping existing CrossDesk service" + ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0 + ${If} $0 = 0 + Return + ${EndIf} + +stop_with_sc: + DetailPrint "Stopping existing CrossDesk service via Service Control Manager" + ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0 + ${If} $0 != 0 + ${AndIf} $0 != 1060 + ${AndIf} $0 != 1062 + MessageBox MB_ICONSTOP|MB_OK "Failed to stop the existing CrossDesk service. The installation will be aborted." + Abort + ${EndIf} + Sleep 1500 +FunctionEnd + +Function RegisterInstalledService + IfFileExists "$INSTDIR\CrossDesk.exe" 0 missing_service_binary + IfFileExists "$INSTDIR\crossdesk_service.exe" 0 missing_service_binary + IfFileExists "$INSTDIR\crossdesk_session_helper.exe" 0 missing_service_binary + + DetailPrint "Registering CrossDesk service" + ExecWait '"$INSTDIR\CrossDesk.exe" --service-install' $0 + ${If} $0 != 0 + MessageBox MB_ICONSTOP|MB_OK "Failed to register the CrossDesk service. The installation will be aborted." + Abort + ${EndIf} + + DetailPrint "Starting CrossDesk service" + ExecWait '"$INSTDIR\CrossDesk.exe" --service-start' $0 + ${If} $0 != 0 + ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $1 + ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $1 + MessageBox MB_ICONSTOP|MB_OK "The CrossDesk service was registered but could not be started. The installation will be aborted." + Abort + ${EndIf} + + Return + +missing_service_binary: + MessageBox MB_ICONSTOP|MB_OK "CrossDesk service files are missing from the installer package. The installation will be aborted." + Abort +FunctionEnd + +Function UnregisterInstalledService + IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc + + DetailPrint "Stopping CrossDesk service" + ExecWait '"$INSTDIR\CrossDesk.exe" --service-stop' $0 + ${If} $0 = 0 + DetailPrint "Removing CrossDesk service" + ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $0 + ${If} $0 = 0 + Return + ${EndIf} + ${EndIf} + +unregister_with_sc: + DetailPrint "Removing CrossDesk service via Service Control Manager" + ExecWait '"$SYSDIR\sc.exe" stop ${PRODUCT_SERVICE_NAME}' $0 + ${If} $0 != 0 + ${AndIf} $0 != 1060 + ${AndIf} $0 != 1062 + MessageBox MB_ICONSTOP|MB_OK "Failed to stop the CrossDesk service. Uninstall will be aborted." + Abort + ${EndIf} + Sleep 1500 + + ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $0 + ${If} $0 != 0 + ${AndIf} $0 != 1060 + MessageBox MB_ICONSTOP|MB_OK "Failed to remove the CrossDesk service. Uninstall will be aborted." + Abort + ${EndIf} +FunctionEnd diff --git a/src/service/windows/interactive_state.h b/src/service/windows/interactive_state.h index 8476983..0a2a9cd 100644 --- a/src/service/windows/interactive_state.h +++ b/src/service/windows/interactive_state.h @@ -1,5 +1,11 @@ -#ifndef _CROSSDESK_INTERACTIVE_STATE_H_ -#define _CROSSDESK_INTERACTIVE_STATE_H_ +/* + * @Author: DI JUNKUN + * @Date: 2026-04-21 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _INTERACTIVE_STATE_H_ +#define _INTERACTIVE_STATE_H_ #include diff --git a/src/service/windows/service_host.h b/src/service/windows/service_host.h index 726029d..f851ffb 100644 --- a/src/service/windows/service_host.h +++ b/src/service/windows/service_host.h @@ -1,5 +1,11 @@ -#ifndef _CROSSDESK_SERVICE_HOST_H_ -#define _CROSSDESK_SERVICE_HOST_H_ +/* + * @Author: DI JUNKUN + * @Date: 2026-04-21 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SERVICE_HOST_H_ +#define _SERVICE_HOST_H_ #include diff --git a/src/service/windows/session_helper_main.cpp b/src/service/windows/session_helper_main.cpp index 126f0ad..7d55859 100644 --- a/src/service/windows/session_helper_main.cpp +++ b/src/service/windows/session_helper_main.cpp @@ -1,5 +1,7 @@ -#include +// clang-format off #include +#include +// clang-format on #include #include #include diff --git a/src/service/windows/session_helper_shared.h b/src/service/windows/session_helper_shared.h index b68e8f5..1dabd24 100644 --- a/src/service/windows/session_helper_shared.h +++ b/src/service/windows/session_helper_shared.h @@ -1,5 +1,11 @@ -#ifndef _CROSSDESK_SESSION_HELPER_SHARED_H_ -#define _CROSSDESK_SESSION_HELPER_SHARED_H_ +/* + * @Author: DI JUNKUN + * @Date: 2026-04-21 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SESSION_HELPER_SHARED_H_ +#define _SESSION_HELPER_SHARED_H_ #include diff --git a/xmake/targets.lua b/xmake/targets.lua index 484af70..206054b 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -192,7 +192,7 @@ function setup_targets() 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_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper") add_files("scripts/windows/crossdesk.rc") end end \ No newline at end of file From 37b9badb2a32e45753e518300100a6f570d16805 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 22 Apr 2026 00:16:58 +0800 Subject: [PATCH 6/9] [ci] fix NSIS uninstall function naming for service cleanup --- scripts/windows/nsis_script.nsi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/windows/nsis_script.nsi b/scripts/windows/nsis_script.nsi index 8c456ce..c3ef88d 100644 --- a/scripts/windows/nsis_script.nsi +++ b/scripts/windows/nsis_script.nsi @@ -130,7 +130,7 @@ cancelUninstall: Abort uninstallApp: - Call UnregisterInstalledService + Call un.UnregisterInstalledService ; Delete main executable and uninstaller Delete "$INSTDIR\CrossDesk.exe" @@ -211,7 +211,7 @@ missing_service_binary: Abort FunctionEnd -Function UnregisterInstalledService +Function un.UnregisterInstalledService IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc DetailPrint "Stopping CrossDesk service" From 71bce08549ad2ff4017a81d65cdb6989993bb88d Mon Sep 17 00:00:00 2001 From: dijunkun Date: Mon, 27 Apr 2026 17:57:11 +0800 Subject: [PATCH 7/9] [fix] select the correct X11 pixel format conversion to prevent green-tinted screen capture on ubuntu --- .../linux/screen_capturer_x11.cpp | 58 +++++++++++-------- .../linux/screen_capturer_x11.h | 1 + 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/screen_capturer/linux/screen_capturer_x11.cpp b/src/screen_capturer/linux/screen_capturer_x11.cpp index 607267f..e3adc03 100644 --- a/src/screen_capturer/linux/screen_capturer_x11.cpp +++ b/src/screen_capturer/linux/screen_capturer_x11.cpp @@ -122,9 +122,8 @@ int ScreenCapturerX11::Init(const int fps, cb_desktop_data cb) { height_ = attr.height; if ((width_ & 1) != 0 || (height_ & 1) != 0) { - LOG_WARN( - "X11 root size {}x{} is not even, aligning down to {}x{} for NV12", - width_, height_, width_ & ~1, height_ & ~1); + LOG_WARN("X11 root size {}x{} is not even, aligning down to {}x{} for NV12", + width_, height_, width_ & ~1, height_ & ~1); width_ &= ~1; height_ &= ~1; } @@ -183,8 +182,9 @@ int ScreenCapturerX11::Start(bool show_cursor) { OnFrame(); } - const auto elapsed = std::chrono::duration_cast( - clock::now() - frame_start); + const auto elapsed = + std::chrono::duration_cast(clock::now() - + frame_start); if (elapsed < frame_interval) { std::this_thread::sleep_for(frame_interval - elapsed); } @@ -282,21 +282,17 @@ void ScreenCapturerX11::OnFrame() { } } - bool needs_copy = image->bytes_per_line != width_ * 4; - std::vector argb_buf; - uint8_t* src_argb = nullptr; - - if (needs_copy) { - argb_buf.resize(width_ * height_ * 4); - for (int y = 0; y < height_; ++y) { - memcpy(&argb_buf[y * width_ * 4], image->data + y * image->bytes_per_line, - width_ * 4); - } - src_argb = argb_buf.data(); - } else { - src_argb = reinterpret_cast(image->data); + if (image->bits_per_pixel != 32 || image->bytes_per_line <= 0) { + LOG_WARN( + "Unsupported X11 image layout: bits_per_pixel={}, bytes_per_line={}", + image->bits_per_pixel, image->bytes_per_line); + XDestroyImage(image); + return; } + const uint8_t* src_argb = reinterpret_cast(image->data); + const int src_stride_argb = image->bytes_per_line; + const size_t y_size = static_cast(width_) * static_cast(height_); const size_t uv_size = y_size / 2; @@ -307,8 +303,20 @@ void ScreenCapturerX11::OnFrame() { uv_plane_.resize(uv_size); } - libyuv::ARGBToNV12(src_argb, width_ * 4, y_plane_.data(), width_, - uv_plane_.data(), width_, width_, height_); + const int convert_ret = + use_abgr_to_nv12_ + ? libyuv::ABGRToNV12(src_argb, src_stride_argb, y_plane_.data(), + width_, uv_plane_.data(), width_, width_, + height_) + : libyuv::ARGBToNV12(src_argb, src_stride_argb, y_plane_.data(), + width_, uv_plane_.data(), width_, width_, + height_); + if (convert_ret != 0) { + LOG_WARN("X11 {} failed: {}", + use_abgr_to_nv12_ ? "ABGRToNV12" : "ARGBToNV12", convert_ret); + XDestroyImage(image); + return; + } std::vector nv12; nv12.reserve(y_plane_.size() + uv_plane_.size()); @@ -416,16 +424,18 @@ bool ScreenCapturerX11::ProbeCapture() { x11_error = trap.SyncAndGetError(); } - if (probe_image) { - XDestroyImage(probe_image); - } - if (x11_error != 0 || !probe_image) { LOG_WARN("X11 probe XGetImage failed: x11_error={}, image={}", x11_error, probe_image ? "valid" : "null"); return false; } + const bool red_in_low_byte = (probe_image->red_mask & 0x000000FFu) != 0; + const bool blue_in_low_byte = (probe_image->blue_mask & 0x000000FFu) != 0; + use_abgr_to_nv12_ = red_in_low_byte && !blue_in_low_byte; + + XDestroyImage(probe_image); + return true; } } // namespace crossdesk diff --git a/src/screen_capturer/linux/screen_capturer_x11.h b/src/screen_capturer/linux/screen_capturer_x11.h index 024b3bc..9d1219f 100644 --- a/src/screen_capturer/linux/screen_capturer_x11.h +++ b/src/screen_capturer/linux/screen_capturer_x11.h @@ -71,6 +71,7 @@ class ScreenCapturerX11 : public ScreenCapturer { cb_desktop_data callback_; std::vector display_info_list_; int capture_error_count_ = 0; + bool use_abgr_to_nv12_ = false; std::vector y_plane_; std::vector uv_plane_; From 5f541f5c8b5c8919adc73e8ef01eaaddd99d8755 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 28 Apr 2026 10:25:16 +0800 Subject: [PATCH 8/9] [feat] make CrossDesk service start and stop with the app --- scripts/windows/nsis_script.nsi | 9 +- src/app/main.cpp | 26 +++- src/service/windows/service_host.cpp | 176 ++++++++++++++++++++++++++- src/service/windows/service_host.h | 3 + 4 files changed, 198 insertions(+), 16 deletions(-) diff --git a/scripts/windows/nsis_script.nsi b/scripts/windows/nsis_script.nsi index c3ef88d..335e42c 100644 --- a/scripts/windows/nsis_script.nsi +++ b/scripts/windows/nsis_script.nsi @@ -195,14 +195,7 @@ Function RegisterInstalledService Abort ${EndIf} - DetailPrint "Starting CrossDesk service" - ExecWait '"$INSTDIR\CrossDesk.exe" --service-start' $0 - ${If} $0 != 0 - ExecWait '"$INSTDIR\CrossDesk.exe" --service-uninstall' $1 - ExecWait '"$SYSDIR\sc.exe" delete ${PRODUCT_SERVICE_NAME}' $1 - MessageBox MB_ICONSTOP|MB_OK "The CrossDesk service was registered but could not be started. The installation will be aborted." - Abort - ${EndIf} + DetailPrint "CrossDesk service registered for on-demand start" Return diff --git a/src/app/main.cpp b/src/app/main.cpp index d90a1f9..f38f92a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -14,6 +14,7 @@ #ifdef _WIN32 #include + #include "service_host.h" #endif @@ -53,7 +54,8 @@ void PrintServiceCliUsage() { << " --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-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"; @@ -87,12 +89,25 @@ bool IsServiceCliCommand(const char* arg) { 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-sas") == 0 || std::strcmp(arg, "--service-ping") == 0 || std::strcmp(arg, "--service-status") == 0 || std::strcmp(arg, "--service-help") == 0; } +void TryStartManagedWindowsService() { + std::filesystem::path service_path = GetSiblingServiceExecutablePath(); + if (service_path.empty() || !std::filesystem::exists(service_path)) { + return; + } + + if (!crossdesk::IsCrossDeskServiceInstalled()) { + return; + } + + crossdesk::StartCrossDeskService(); +} + int HandleServiceCliCommand(const std::string& command) { EnsureConsoleForCli(); @@ -120,8 +135,7 @@ int HandleServiceCliCommand(const std::string& command) { if (command == "--service-uninstall") { bool success = crossdesk::UninstallCrossDeskService(); - std::cout << (success ? "uninstall ok" : "uninstall failed") - << std::endl; + std::cout << (success ? "uninstall ok" : "uninstall failed") << std::endl; return success ? 0 : 1; } @@ -182,6 +196,10 @@ int main(int argc, char* argv[]) { return 0; } +#ifdef _WIN32 + TryStartManagedWindowsService(); +#endif + bool enable_daemon = false; auto path_manager = std::make_unique("CrossDesk"); if (path_manager) { diff --git a/src/service/windows/service_host.cpp b/src/service/windows/service_host.cpp index c135f6b..1a1cfed 100644 --- a/src/service/windows/service_host.cpp +++ b/src/service/windows/service_host.cpp @@ -1,5 +1,6 @@ #include "service_host.h" +#include #include #include #include @@ -27,6 +28,9 @@ using Json = nlohmann::json; constexpr char kSecureDesktopKeyboardIpcCommandPrefix[] = "secure-input-key:"; constexpr char kSecureDesktopMouseIpcCommandPrefix[] = "secure-input-mouse:"; +constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe"; +constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000; +constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000; using SendSasFunction = VOID(WINAPI*)(BOOL); @@ -157,6 +161,97 @@ std::string BuildErrorJson(const char* error, DWORD error_code = 0) { return stream.str(); } +bool HasRunningCrossDeskClientProcess() { + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) { + LOG_ERROR("CreateToolhelp32Snapshot failed, error={}", GetLastError()); + return true; + } + + PROCESSENTRY32W entry{}; + entry.dwSize = sizeof(entry); + if (!Process32FirstW(snapshot, &entry)) { + DWORD error = GetLastError(); + CloseHandle(snapshot); + if (error != ERROR_NO_MORE_FILES) { + LOG_ERROR("Process32FirstW failed, error={}", error); + return true; + } + return false; + } + + do { + if (_wcsicmp(entry.szExeFile, kCrossDeskClientProcessName) == 0) { + CloseHandle(snapshot); + return true; + } + } while (Process32NextW(snapshot, &entry)); + + DWORD error = GetLastError(); + CloseHandle(snapshot); + if (error != ERROR_NO_MORE_FILES) { + LOG_ERROR("Process32NextW failed, error={}", error); + return true; + } + + return false; +} + +bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) { + if (service == nullptr) { + return false; + } + + PACL existing_dacl = nullptr; + PACL updated_dacl = nullptr; + PSECURITY_DESCRIPTOR security_descriptor = nullptr; + DWORD error = + GetSecurityInfo(service, SE_SERVICE, DACL_SECURITY_INFORMATION, nullptr, + nullptr, &existing_dacl, nullptr, &security_descriptor); + if (error != ERROR_SUCCESS) { + LOG_ERROR("GetSecurityInfo failed, error={}", error); + return false; + } + + BYTE sid_buffer[SECURITY_MAX_SID_SIZE] = {0}; + DWORD sid_size = sizeof(sid_buffer); + if (!CreateWellKnownSid(WinAuthenticatedUserSid, nullptr, sid_buffer, + &sid_size)) { + error = GetLastError(); + LOG_ERROR("CreateWellKnownSid failed, error={}", error); + LocalFree(security_descriptor); + return false; + } + + EXPLICIT_ACCESSW access{}; + access.grfAccessPermissions = SERVICE_START | SERVICE_QUERY_STATUS; + access.grfAccessMode = SET_ACCESS; + access.grfInheritance = NO_INHERITANCE; + access.Trustee.TrusteeForm = TRUSTEE_IS_SID; + access.Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP; + access.Trustee.ptstrName = reinterpret_cast(sid_buffer); + + error = SetEntriesInAclW(1, &access, existing_dacl, &updated_dacl); + if (error != ERROR_SUCCESS) { + LOG_ERROR("SetEntriesInAclW failed, error={}", error); + LocalFree(security_descriptor); + return false; + } + + error = SetSecurityInfo(service, SE_SERVICE, DACL_SECURITY_INFORMATION, + nullptr, nullptr, updated_dacl, nullptr); + if (error != ERROR_SUCCESS) { + LOG_ERROR("SetSecurityInfo failed, error={}", error); + LocalFree(updated_dacl); + LocalFree(security_descriptor); + return false; + } + + LocalFree(updated_dacl); + LocalFree(security_descriptor); + return true; +} + std::string QueryNamedPipeMessage(const std::wstring& pipe_name, const std::string& command, DWORD timeout_ms) { @@ -812,6 +907,10 @@ int CrossDeskServiceHost::InitializeRuntime() { RefreshSessionState(); EnsureSessionHelper(); ipc_thread_ = std::thread(&CrossDeskServiceHost::IpcServerLoop, this); + if (!console_mode_) { + client_process_monitor_thread_ = + std::thread(&CrossDeskServiceHost::ClientProcessMonitorLoop, this); + } LOG_INFO("CrossDesk service runtime initialized, session_id={}", active_session_id_); return 0; @@ -825,6 +924,10 @@ void CrossDeskServiceHost::ShutdownRuntime() { SetEvent(stop_event_); } + if (client_process_monitor_thread_.joinable()) { + client_process_monitor_thread_.join(); + } + if (ipc_thread_.joinable()) { ipc_thread_.join(); } @@ -841,6 +944,34 @@ void CrossDeskServiceHost::RequestStop() { } } +void CrossDeskServiceHost::ClientProcessMonitorLoop() { + const ULONGLONG monitor_started_at = GetTickCount64(); + + while (stop_event_ != nullptr) { + DWORD wait_result = + WaitForSingleObject(stop_event_, kCrossDeskClientMonitorIntervalMs); + if (wait_result == WAIT_OBJECT_0) { + return; + } + if (wait_result != WAIT_TIMEOUT) { + continue; + } + + if (GetTickCount64() - monitor_started_at < + kCrossDeskClientMonitorStartupGraceMs) { + continue; + } + + if (HasRunningCrossDeskClientProcess()) { + continue; + } + + LOG_INFO("No crossdesk client process detected, stopping service"); + RequestStop(); + return; + } +} + void CrossDeskServiceHost::ReportServiceStatus(DWORD current_state, DWORD win32_exit_code, DWORD wait_hint) { @@ -1845,7 +1976,7 @@ bool InstallCrossDeskService(const std::wstring& binary_path) { 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_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, service_command.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr); @@ -1858,14 +1989,15 @@ bool InstallCrossDeskService(const std::wstring& binary_path) { } service = OpenServiceW(manager, kCrossDeskServiceName, - SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS); + SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS | + READ_CONTROL | WRITE_DAC); if (service == nullptr) { LOG_ERROR("OpenServiceW failed, error={}", GetLastError()); CloseServiceHandle(manager); return false; } - if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_AUTO_START, + if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_DEMAND_START, SERVICE_NO_CHANGE, service_command.c_str(), nullptr, nullptr, nullptr, nullptr, nullptr, kCrossDeskServiceDisplayName)) { @@ -1876,6 +2008,39 @@ bool InstallCrossDeskService(const std::wstring& binary_path) { } } + if (!GrantCrossDeskServiceStartAccessToAuthenticatedUsers(service)) { + CloseServiceHandle(service); + CloseServiceHandle(manager); + return false; + } + + CloseServiceHandle(service); + CloseServiceHandle(manager); + return true; +} + +bool IsCrossDeskServiceInstalled() { + 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_QUERY_STATUS); + if (service == nullptr) { + DWORD error = GetLastError(); + CloseServiceHandle(manager); + if (error == ERROR_SERVICE_DOES_NOT_EXIST) { + return false; + } + if (error == ERROR_ACCESS_DENIED) { + return true; + } + LOG_ERROR("OpenServiceW failed, error={}", error); + return false; + } + CloseServiceHandle(service); CloseServiceHandle(manager); return true; @@ -1891,7 +2056,10 @@ bool StartCrossDeskService() { SC_HANDLE service = OpenServiceW(manager, kCrossDeskServiceName, SERVICE_START); if (service == nullptr) { - LOG_ERROR("OpenServiceW failed, error={}", GetLastError()); + DWORD error = GetLastError(); + if (error != ERROR_SERVICE_DOES_NOT_EXIST) { + LOG_ERROR("OpenServiceW failed, error={}", error); + } CloseServiceHandle(manager); return false; } diff --git a/src/service/windows/service_host.h b/src/service/windows/service_host.h index f851ffb..9bf7b89 100644 --- a/src/service/windows/service_host.h +++ b/src/service/windows/service_host.h @@ -33,6 +33,7 @@ class CrossDeskServiceHost { int InitializeRuntime(); void ShutdownRuntime(); void RequestStop(); + void ClientProcessMonitorLoop(); void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code, DWORD wait_hint); void IpcServerLoop(); @@ -71,6 +72,7 @@ class CrossDeskServiceHost { SERVICE_STATUS service_status_{}; HANDLE stop_event_ = nullptr; std::thread ipc_thread_; + std::thread client_process_monitor_thread_; std::mutex state_mutex_; DWORD active_session_id_ = 0xFFFFFFFF; DWORD process_session_id_ = 0xFFFFFFFF; @@ -128,6 +130,7 @@ class CrossDeskServiceHost { static CrossDeskServiceHost* instance_; }; +bool IsCrossDeskServiceInstalled(); bool InstallCrossDeskService(const std::wstring& binary_path); bool UninstallCrossDeskService(); bool StartCrossDeskService(); From 1d5d6f51216ee780195d967aa5afd2f16b91ee2b Mon Sep 17 00:00:00 2001 From: dijunkun Date: Tue, 28 Apr 2026 11:14:25 +0800 Subject: [PATCH 9/9] [fix] fix Debian package dependencies for PipeWire and ALSA t64 transitions --- scripts/linux/pkg_amd64.sh | 6 ++++-- scripts/linux/pkg_arm64.sh | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/scripts/linux/pkg_amd64.sh b/scripts/linux/pkg_amd64.sh index 85618f4..e693115 100644 --- a/scripts/linux/pkg_amd64.sh +++ b/scripts/linux/pkg_amd64.sh @@ -8,6 +8,8 @@ APP_VERSION="$1" ARCHITECTURE="amd64" MAINTAINER="Junkun Di " DESCRIPTION="A simple cross-platform remote desktop client." +ALSA_RUNTIME_DEP="libasound2 | libasound2t64" +PIPEWIRE_RUNTIME_DEP="libpipewire-0.3-0 | libpipewire-0.3-0t64" # Remove 'v' prefix from version for Debian package (Debian version must start with digit) DEB_VERSION="${APP_VERSION#v}" @@ -41,9 +43,9 @@ Maintainer: $MAINTAINER Description: $DESCRIPTION Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1, libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, - libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2, + libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, - libpipewire-0.3-0, xdg-desktop-portal, + $PIPEWIRE_RUNTIME_DEP, xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr Recommends: nvidia-cuda-toolkit Priority: optional diff --git a/scripts/linux/pkg_arm64.sh b/scripts/linux/pkg_arm64.sh index c608b85..66da7cd 100644 --- a/scripts/linux/pkg_arm64.sh +++ b/scripts/linux/pkg_arm64.sh @@ -8,6 +8,8 @@ APP_VERSION="$1" ARCHITECTURE="arm64" MAINTAINER="Junkun Di " DESCRIPTION="A simple cross-platform remote desktop client." +ALSA_RUNTIME_DEP="libasound2 | libasound2t64" +PIPEWIRE_RUNTIME_DEP="libpipewire-0.3-0 | libpipewire-0.3-0t64" # Remove 'v' prefix from version for Debian package (Debian version must start with digit) DEB_VERSION="${APP_VERSION#v}" @@ -41,9 +43,9 @@ Maintainer: $MAINTAINER Description: $DESCRIPTION Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1, libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, - libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, libasound2, + libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, - libpipewire-0.3-0, xdg-desktop-portal, + $PIPEWIRE_RUNTIME_DEP, xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr Priority: optional Section: utils