mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-07 10:44:41 +08:00
[feat] add portable Windows Service install prompt with one-click setup
This commit is contained in:
@@ -69,6 +69,21 @@ struct TranslationRow {
|
|||||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||||
"Remote Windows service unavailable", \
|
"Remote Windows service unavailable", \
|
||||||
u8"Служба Windows на удаленной стороне недоступна") \
|
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, \
|
X(remote_unlock_requires_secure_desktop, \
|
||||||
u8"当前仍需要安全桌面专用采集/输入", \
|
u8"当前仍需要安全桌面专用采集/输入", \
|
||||||
"Secure desktop capture/input is still required", \
|
"Secure desktop capture/input is still required", \
|
||||||
|
|||||||
@@ -1570,6 +1570,10 @@ int Render::DrawMainWindow() {
|
|||||||
|
|
||||||
UpdateNotificationWindow();
|
UpdateNotificationWindow();
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
PortableServiceInstallWindow();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (show_request_permission_window_) {
|
if (show_request_permission_window_) {
|
||||||
RequestPermissionWindow();
|
RequestPermissionWindow();
|
||||||
@@ -1732,6 +1736,10 @@ int Render::Run() {
|
|||||||
InitializeModules();
|
InitializeModules();
|
||||||
InitializeMainWindow();
|
InitializeMainWindow();
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
CheckPortableWindowsService();
|
||||||
|
#endif
|
||||||
|
|
||||||
const int scaled_video_width_ = 160;
|
const int scaled_video_width_ = 160;
|
||||||
const int scaled_video_height_ = 90;
|
const int scaled_video_height_ = 90;
|
||||||
|
|
||||||
@@ -2247,6 +2255,10 @@ void Render::Cleanup() {
|
|||||||
CleanupFactories();
|
CleanupFactories();
|
||||||
CleanupPeers();
|
CleanupPeers();
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
JoinPortableWindowsServiceInstallThread();
|
||||||
|
#endif
|
||||||
|
|
||||||
WaitForThumbnailSaveTasks();
|
WaitForThumbnailSaveTasks();
|
||||||
|
|
||||||
AudioDeviceDestroy();
|
AudioDeviceDestroy();
|
||||||
|
|||||||
@@ -382,6 +382,19 @@ class Render {
|
|||||||
void HandleWindowsServiceIntegration();
|
void HandleWindowsServiceIntegration();
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
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
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -548,6 +561,13 @@ class Render {
|
|||||||
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
||||||
uint32_t last_windows_service_status_tick_ = 0;
|
uint32_t last_windows_service_status_tick_ = 0;
|
||||||
uint32_t optimistic_windows_secure_desktop_until_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<PortableServiceInstallState> portable_service_install_state_{
|
||||||
|
PortableServiceInstallState::idle};
|
||||||
|
std::thread portable_service_install_thread_;
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// stream window render
|
// stream window render
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
#include "render.h"
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
|
||||||
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "localization.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
#include "service_host.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::filesystem::path GetCurrentExecutablePath() {
|
||||||
|
std::vector<wchar_t> buffer(MAX_PATH);
|
||||||
|
while (true) {
|
||||||
|
DWORD length =
|
||||||
|
GetModuleFileNameW(nullptr, buffer.data(),
|
||||||
|
static_cast<DWORD>(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
|
||||||
Reference in New Issue
Block a user