From f121aa47f76fe3a8620478190062693cf2264b36 Mon Sep 17 00:00:00 2001 From: kunkundi Date: Wed, 27 May 2026 19:32:58 +0800 Subject: [PATCH] [feat] add portable Windows Service install prompt with one-click setup --- .../assets/localization/localization_data.h | 15 + src/gui/render.cpp | 12 + src/gui/render.h | 20 ++ .../portable_service_install_window.cpp | 279 ++++++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 src/gui/windows/portable_service_install_window.cpp diff --git a/src/gui/assets/localization/localization_data.h b/src/gui/assets/localization/localization_data.h index 785df53..160f032 100644 --- a/src/gui/assets/localization/localization_data.h +++ b/src/gui/assets/localization/localization_data.h @@ -69,6 +69,21 @@ struct TranslationRow { X(remote_service_unavailable, u8"远端Windows服务不可用", \ "Remote Windows service unavailable", \ u8"Служба Windows на удаленной стороне недоступна") \ + X(windows_service_setup_title, u8"安装 CrossDesk Service", \ + "Install CrossDesk Service", u8"Установить CrossDesk Service") \ + X(windows_service_setup_message, \ + u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \ + "The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \ + u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \ + X(install_windows_service, u8"安装", "Install", \ + u8"Установить службу") \ + X(installing_windows_service, u8"正在安装服务...", "Installing service...", \ + u8"Установка службы...") \ + X(windows_service_install_success, u8"服务已安装并启动", \ + "Service installed and started", u8"Служба установлена и запущена") \ + X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \ + "Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \ + u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \ X(remote_unlock_requires_secure_desktop, \ u8"当前仍需要安全桌面专用采集/输入", \ "Secure desktop capture/input is still required", \ diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 149eb75..0bbd940 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -1570,6 +1570,10 @@ int Render::DrawMainWindow() { UpdateNotificationWindow(); +#if _WIN32 && CROSSDESK_PORTABLE + PortableServiceInstallWindow(); +#endif + #ifdef __APPLE__ if (show_request_permission_window_) { RequestPermissionWindow(); @@ -1732,6 +1736,10 @@ int Render::Run() { InitializeModules(); InitializeMainWindow(); +#if _WIN32 && CROSSDESK_PORTABLE + CheckPortableWindowsService(); +#endif + const int scaled_video_width_ = 160; const int scaled_video_height_ = 90; @@ -2247,6 +2255,10 @@ void Render::Cleanup() { CleanupFactories(); CleanupPeers(); +#if _WIN32 && CROSSDESK_PORTABLE + JoinPortableWindowsServiceInstallThread(); +#endif + WaitForThumbnailSaveTasks(); AudioDeviceDestroy(); diff --git a/src/gui/render.h b/src/gui/render.h index c7e7fd3..42bad0d 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -382,6 +382,19 @@ class Render { void HandleWindowsServiceIntegration(); #if _WIN32 void ResetLocalWindowsServiceState(bool clear_pending_sas); +#if CROSSDESK_PORTABLE + enum class PortableServiceInstallState { + idle, + installing, + succeeded, + failed, + }; + + void CheckPortableWindowsService(); + int PortableServiceInstallWindow(); + void StartPortableWindowsServiceInstall(); + void JoinPortableWindowsServiceInstallThread(); +#endif #endif private: @@ -548,6 +561,13 @@ class Render { uint32_t last_local_secure_input_block_log_tick_ = 0; uint32_t last_windows_service_status_tick_ = 0; uint32_t optimistic_windows_secure_desktop_until_tick_ = 0; +#if CROSSDESK_PORTABLE + bool portable_service_prompt_checked_ = false; + bool show_portable_service_install_window_ = false; + std::atomic portable_service_install_state_{ + PortableServiceInstallState::idle}; + std::thread portable_service_install_thread_; +#endif #endif // stream window render diff --git a/src/gui/windows/portable_service_install_window.cpp b/src/gui/windows/portable_service_install_window.cpp new file mode 100644 index 0000000..bfc59d6 --- /dev/null +++ b/src/gui/windows/portable_service_install_window.cpp @@ -0,0 +1,279 @@ +#include "render.h" + +#if _WIN32 && CROSSDESK_PORTABLE + +#include + +#include + +#include "localization.h" +#include "rd_log.h" +#include "service_host.h" + +namespace crossdesk { +namespace { + +std::filesystem::path GetCurrentExecutablePath() { + std::vector buffer(MAX_PATH); + while (true) { + DWORD length = + GetModuleFileNameW(nullptr, buffer.data(), + static_cast(buffer.size())); + if (length == 0) { + return {}; + } + if (length < buffer.size()) { + return std::filesystem::path(buffer.data(), buffer.data() + length); + } + if (buffer.size() >= 32768) { + return {}; + } + buffer.resize(buffer.size() * 2); + } +} + +bool InstallServiceWithElevation() { + const std::filesystem::path executable_path = GetCurrentExecutablePath(); + if (executable_path.empty()) { + LOG_ERROR("Portable service install failed: current executable not found"); + return false; + } + + const std::filesystem::path service_path = + executable_path.parent_path() / L"crossdesk_service.exe"; + const std::filesystem::path helper_path = + executable_path.parent_path() / L"crossdesk_session_helper.exe"; + if (!std::filesystem::exists(service_path) || + !std::filesystem::exists(helper_path)) { + LOG_ERROR( + "Portable service install failed: service binaries missing, service={}, " + "helper={}", + service_path.string(), helper_path.string()); + return false; + } + + std::wstring executable = executable_path.wstring(); + std::wstring working_dir = executable_path.parent_path().wstring(); + std::wstring parameters = L"--service-install"; + + SHELLEXECUTEINFOW execute_info{}; + execute_info.cbSize = sizeof(execute_info); + execute_info.fMask = SEE_MASK_NOCLOSEPROCESS; + execute_info.hwnd = nullptr; + execute_info.lpVerb = L"runas"; + execute_info.lpFile = executable.c_str(); + execute_info.lpParameters = parameters.c_str(); + execute_info.lpDirectory = working_dir.c_str(); + execute_info.nShow = SW_HIDE; + + if (!ShellExecuteExW(&execute_info)) { + LOG_ERROR("Portable service install failed: ShellExecuteExW error={}", + GetLastError()); + return false; + } + + DWORD wait_result = WaitForSingleObject(execute_info.hProcess, INFINITE); + DWORD exit_code = 1; + if (wait_result == WAIT_OBJECT_0) { + GetExitCodeProcess(execute_info.hProcess, &exit_code); + } else { + LOG_ERROR("Portable service install wait failed, result={}", wait_result); + } + CloseHandle(execute_info.hProcess); + + if (exit_code != 0) { + LOG_ERROR("Portable service install command failed, exit_code={}", + exit_code); + return false; + } + + const bool started = StartCrossDeskService(); + if (!started) { + LOG_WARN("Portable service installed but start failed"); + } + return IsCrossDeskServiceInstalled() && started; +} + +} // namespace + +void Render::CheckPortableWindowsService() { + if (portable_service_prompt_checked_) { + return; + } + portable_service_prompt_checked_ = true; + + if (IsCrossDeskServiceInstalled()) { + return; + } + + portable_service_install_state_.store(PortableServiceInstallState::idle, + std::memory_order_relaxed); + show_portable_service_install_window_ = true; +} + +void Render::StartPortableWindowsServiceInstall() { + PortableServiceInstallState expected = PortableServiceInstallState::idle; + if (!portable_service_install_state_.compare_exchange_strong( + expected, PortableServiceInstallState::installing, + std::memory_order_acq_rel)) { + if (expected != PortableServiceInstallState::failed) { + return; + } + portable_service_install_state_.store( + PortableServiceInstallState::installing, std::memory_order_release); + } + + JoinPortableWindowsServiceInstallThread(); + portable_service_install_thread_ = std::thread([this]() { + const bool installed = InstallServiceWithElevation(); + portable_service_install_state_.store( + installed ? PortableServiceInstallState::succeeded + : PortableServiceInstallState::failed, + std::memory_order_release); + }); +} + +void Render::JoinPortableWindowsServiceInstallThread() { + if (portable_service_install_thread_.joinable()) { + portable_service_install_thread_.join(); + } +} + +int Render::PortableServiceInstallWindow() { + if (!show_portable_service_install_window_) { + return 0; + } + + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const float window_width = title_bar_button_width_ * 12.0f; + const float window_height = title_bar_button_width_ * 4.0f; + ImGui::SetNextWindowPos( + ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) / + 2.0f, + (viewport->WorkSize.y - viewport->WorkPos.y - window_height) / + 2.0f), + ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(window_width, window_height), + ImGuiCond_Always); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f); + + ImGui::Begin( + localization::windows_service_setup_title[localization_language_index_] + .c_str(), + nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoTitleBar); + + ImGui::Spacing(); + ImGui::SetWindowFontScale(0.55f); + ImGui::SetCursorPosX(window_width * 0.08f); + ImGui::Text( + "%s", + localization::windows_service_setup_title[localization_language_index_] + .c_str()); + + const PortableServiceInstallState state = + portable_service_install_state_.load(std::memory_order_acquire); + const char* status_text = nullptr; + if (state == PortableServiceInstallState::installing || + state == PortableServiceInstallState::succeeded || + state == PortableServiceInstallState::failed) { + status_text = + localization::installing_windows_service[localization_language_index_] + .c_str(); + if (state == PortableServiceInstallState::succeeded) { + status_text = + localization::windows_service_install_success + [localization_language_index_] + .c_str(); + } else if (state == PortableServiceInstallState::failed) { + status_text = + localization::windows_service_install_failed + [localization_language_index_] + .c_str(); + } + } + + ImGui::SetWindowFontScale(0.45f); + ImGui::SetCursorPosX(window_width * 0.04f); + ImGui::SetCursorPosY(window_height * 0.22f); + ImGui::BeginChild("PortableServiceInstallContent", + ImVec2(window_width * 0.92f, window_height * 0.5f), + ImGuiChildFlags_Borders, ImGuiWindowFlags_None); + ImGui::SetWindowFontScale(0.5f); + const float wrap_pos = ImGui::GetContentRegionAvail().x; + ImGui::PushTextWrapPos(wrap_pos); + ImGui::TextWrapped( + "%s", + localization::windows_service_setup_message[localization_language_index_] + .c_str()); + if (status_text != nullptr) { + ImGui::Spacing(); + ImGui::TextWrapped("%s", status_text); + } + ImGui::PopTextWrapPos(); + ImGui::EndChild(); + + ImGui::SetWindowFontScale(0.5f); + const float button_y = window_height * 0.76f; + const ImGuiStyle& style = ImGui::GetStyle(); + const auto default_button_width = [&style](const std::string& label) { + return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f; + }; + const std::string install_label = + localization::install_windows_service[localization_language_index_]; + const std::string cancel_label = + localization::cancel[localization_language_index_]; + const std::string ok_label = localization::ok[localization_language_index_]; + const float buttons_width = state == PortableServiceInstallState::succeeded + ? default_button_width(ok_label) + : default_button_width(install_label) + + style.ItemSpacing.x + + default_button_width(cancel_label); + ImGui::SetCursorPosX((window_width - buttons_width) * 0.5f); + ImGui::SetCursorPosY(button_y); + + if (state == PortableServiceInstallState::succeeded) { + if (ImGui::Button(ok_label.c_str())) { + show_portable_service_install_window_ = false; + JoinPortableWindowsServiceInstallThread(); + } + } else { + if (state == PortableServiceInstallState::installing) { + ImGui::BeginDisabled(); + } + if (ImGui::Button(install_label.c_str())) { + StartPortableWindowsServiceInstall(); + } + if (state == PortableServiceInstallState::installing) { + ImGui::EndDisabled(); + } + + ImGui::SameLine(); + if (state == PortableServiceInstallState::installing) { + ImGui::BeginDisabled(); + } + if (ImGui::Button(cancel_label.c_str())) { + show_portable_service_install_window_ = false; + } + if (state == PortableServiceInstallState::installing) { + ImGui::EndDisabled(); + } + } + + ImGui::SetWindowFontScale(1.0f); + ImGui::End(); + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); + + return 0; +} + +} // namespace crossdesk + +#endif