diff --git a/apps/desktop/src/app/main.cpp b/apps/desktop/src/app/main.cpp index d3ad9878..de5c650a 100644 --- a/apps/desktop/src/app/main.cpp +++ b/apps/desktop/src/app/main.cpp @@ -18,6 +18,7 @@ #include "platform/windows/autostart_task.h" #include "platform/windows/gui/slint_backend.h" +#include "platform/windows/runtime_daemon.h" #include "service_host.h" #include "service_lifetime.h" #include "shutdown_order.h" @@ -395,6 +396,17 @@ static int RunApplication( const int result = render.Run(); runtime_daemon.Shutdown(result != 0); return result; +#elif defined(_WIN32) + auto& runtime_daemon = crossdesk::platform::WindowsRuntimeDaemon::Instance(); + runtime_daemon.Initialize(); + std::string daemon_error; + if (!runtime_daemon.SetEnabled(enable_daemon, daemon_error)) { + crossdesk::get_logger()->warn("Could not start daemon: {}", daemon_error); + } + crossdesk::Render render; + const int result = render.Run(); + runtime_daemon.Shutdown(result != 0); + return result; #else if (enable_daemon) { @@ -433,8 +445,12 @@ static int RunApplication( int main(int argc, char* argv[]) { #ifdef _WIN32 - // Apply in every process, including the daemon and --child. Windows does - // not inherit shutdown priority from a parent process. + if (argc >= 2 && + std::strcmp(argv[1], crossdesk::platform::kWindowsDaemonArgument) == 0) { + return crossdesk::platform::RunWindowsRuntimeDaemon(); + } + // Apply in every GUI process, including legacy --child launches. The watcher + // keeps the default shutdown priority so it ends before this remote host. const DWORD shutdown_error = crossdesk::ConfigureWindowsShutdownOrder( crossdesk::WindowsShutdownRole::host); if (shutdown_error != ERROR_SUCCESS) { diff --git a/apps/desktop/src/config_center/config_center.cpp b/apps/desktop/src/config_center/config_center.cpp index 5b87bcad..6385c54d 100644 --- a/apps/desktop/src/config_center/config_center.cpp +++ b/apps/desktop/src/config_center/config_center.cpp @@ -12,6 +12,10 @@ #include "rd_log.h" #include +#ifdef _WIN32 +#include "platform/windows/runtime_daemon.h" +#endif + #if defined(__linux__) && !defined(__OHOS__) #include "platform/linux/autostart.h" #include "platform/linux/runtime_daemon.h" @@ -424,14 +428,19 @@ int ConfigCenter::SetAutostart(bool enabled) { } int ConfigCenter::SetDaemon(bool enabled) { -#if defined(__linux__) && !defined(__OHOS__) +#if defined(_WIN32) || (defined(__linux__) && !defined(__OHOS__)) const bool previous = enable_daemon_; #endif if (StoreValues({{"enable_daemon", enabled ? "true" : "false"}}) != 0) return -1; enable_daemon_ = enabled; -#if defined(__linux__) && !defined(__OHOS__) +#if defined(_WIN32) || (defined(__linux__) && !defined(__OHOS__)) std::string error; - if (!platform::LinuxRuntimeDaemon::Instance().SetEnabled(enabled, error)) { +#ifdef _WIN32 + auto& daemon = platform::WindowsRuntimeDaemon::Instance(); +#else + auto& daemon = platform::LinuxRuntimeDaemon::Instance(); +#endif + if (!daemon.SetEnabled(enabled, error)) { LOG_ERROR("Cannot apply daemon setting: {}", error); if (StoreValues({{"enable_daemon", previous ? "true" : "false"}}) == 0) { enable_daemon_ = previous; diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index aa02fd49..f27c56e0 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -4983,7 +4983,13 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) { config_center_->SetForceRelay(main->get_force_relay_enabled()); main->set_force_relay_enabled(config_center_->IsForceRelay()); #if !defined(__OHOS__) - config_center_->SetAutostart(enable_autostart_); + if (config_center_->SetAutostart(enable_autostart_) != 0) { + enable_autostart_ = config_center_->IsEnableAutostart(); + main->set_autostart_enabled(enable_autostart_); + main->set_settings_save_error(UiText( + localization::Literal("保存失败,请重试", localization_language_index_))); + return false; + } #endif if (config_center_->SetDaemon(enable_daemon_) != 0) { enable_daemon_ = config_center_->IsEnableDaemon(); diff --git a/apps/desktop/src/platform/windows/daemon_backend.cpp b/apps/desktop/src/platform/windows/daemon_backend.cpp index 8d05cb81..dccf4706 100644 --- a/apps/desktop/src/platform/windows/daemon_backend.cpp +++ b/apps/desktop/src/platform/windows/daemon_backend.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -23,21 +24,21 @@ bool DaemonStopRequested() { return g_stop_requested.load(); } bool PrepareDaemon() { return true; } std::string GetDaemonExecutablePath() { - char path[32768] = {}; - const DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path)); - return length > 0 && length < sizeof(path) ? std::string(path, length) - : std::string(); + wchar_t path[32768] = {}; + const DWORD length = GetModuleFileNameW(nullptr, path, 32768); + return length > 0 && length < 32768 + ? std::filesystem::path(path, path + length).u8string() + : std::string(); } DaemonChildResult RunDaemonChild(const std::string& executable_path, const std::function& keep_running) { DaemonChildResult result; - STARTUPINFOA startup = {sizeof(startup)}; + STARTUPINFOW startup = {sizeof(startup)}; PROCESS_INFORMATION process = {}; - std::string command = "\"" + executable_path + "\" --child"; - std::vector command_buffer(command.begin(), command.end()); - command_buffer.push_back('\0'); - if (!CreateProcessA(nullptr, command_buffer.data(), nullptr, nullptr, FALSE, + const auto path = std::filesystem::u8path(executable_path).wstring(); + std::wstring command = L"\"" + path + L"\" --child"; + if (!CreateProcessW(path.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup, &process)) { return result; } diff --git a/apps/desktop/src/platform/windows/runtime_daemon.cpp b/apps/desktop/src/platform/windows/runtime_daemon.cpp new file mode 100644 index 00000000..da198394 --- /dev/null +++ b/apps/desktop/src/platform/windows/runtime_daemon.cpp @@ -0,0 +1,290 @@ +#include "platform/windows/runtime_daemon.h" + +#include + +#include +#include +#include +#include + +namespace crossdesk::platform { +namespace { + +constexpr DWORD kStartupTimeoutMs = 3000; +constexpr DWORD kRestartDelayMs = 1000; +constexpr wchar_t kWindowClass[] = L"CrossDesk.RuntimeDaemon"; + +struct Handle { + HANDLE value = nullptr; + ~Handle() { if (value) CloseHandle(value); } +}; + +struct Arguments { + int count = 0; + wchar_t** values = CommandLineToArgvW(GetCommandLineW(), &count); + ~Arguments() { if (values) LocalFree(values); } +}; + +std::wstring ExecutablePath() { + std::vector path(32768); + const DWORD count = GetModuleFileNameW(nullptr, path.data(), + static_cast(path.size())); + if (!count || count >= path.size()) return {}; + return {path.data(), count}; +} + +std::wstring QuoteArgument(const std::wstring& value) { + std::wstring result = L"\""; + size_t slashes = 0; + for (const wchar_t c : value) { + if (c == L'\\') { + ++slashes; + } else { + result.append(c == L'"' ? slashes * 2 + 1 : slashes, L'\\'); + result += c; + slashes = 0; + } + } + result.append(slashes * 2, L'\\'); + return result + L'"'; +} + +bool Failed(const char* operation, std::string& error, + DWORD code = GetLastError()) { + error = std::string(operation) + " failed (Windows error " + + std::to_string(code ? code : ERROR_GEN_FAILURE) + ")"; + return false; +} + +HANDLE ParseHandle(const wchar_t* text) { + wchar_t* end = nullptr; + errno = 0; + const auto value = std::wcstoull(text, &end, 10); + if (errno || !text[0] || *end || text[0] == L'-' || !value || + value > (std::numeric_limits::max)()) return nullptr; + return reinterpret_cast(static_cast(value)); +} + +struct SessionWindow { + HWND window = nullptr; + bool query_pending = false; + bool ending = false; + + ~SessionWindow() { if (window) DestroyWindow(window); } + + static LRESULT CALLBACK WindowProc(HWND window, UINT message, + WPARAM wparam, LPARAM lparam) { + if (message == WM_NCCREATE) { + const auto* create = reinterpret_cast(lparam); + SetWindowLongPtrW(window, GWLP_USERDATA, + reinterpret_cast(create->lpCreateParams)); + } + auto* state = reinterpret_cast( + GetWindowLongPtrW(window, GWLP_USERDATA)); + if (state && message == WM_QUERYENDSESSION) { + state->query_pending = true; + return TRUE; + } + if (state && message == WM_ENDSESSION) { + state->query_pending = false; + state->ending = wparam != FALSE; + return 0; + } + return DefWindowProcW(window, message, wparam, lparam); + } + + bool Create(DWORD owner) { + WNDCLASSW klass{}; + klass.lpfnWndProc = WindowProc; + klass.hInstance = GetModuleHandleW(nullptr); + klass.lpszClassName = kWindowClass; + if (!RegisterClassW(&klass) && GetLastError() != ERROR_CLASS_ALREADY_EXISTS) + return false; + const auto title = std::to_wstring(owner); + // Message-only windows do not receive session-end broadcasts. + window = CreateWindowExW(WS_EX_TOOLWINDOW, kWindowClass, title.c_str(), + WS_OVERLAPPED, 0, 0, 0, 0, nullptr, nullptr, + klass.hInstance, this); + return window != nullptr; + } + + void Pump() { + MSG message{}; + while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) { + if (message.message == WM_QUIT) ending = true; + TranslateMessage(&message); + DispatchMessageW(&message); + } + } +}; + +} // namespace + +WindowsRuntimeDaemon& WindowsRuntimeDaemon::Instance() { + static WindowsRuntimeDaemon daemon; + return daemon; +} + +WindowsRuntimeDaemon::~WindowsRuntimeDaemon() { Shutdown(); } + +void WindowsRuntimeDaemon::Initialize() { + std::lock_guard lock(mutex_); + initialized_ = true; + executable_ = ExecutablePath(); + arguments_.clear(); + Arguments arguments; + if (!arguments.values) { + executable_.clear(); + return; + } + for (int i = 1; i < arguments.count; ++i) + arguments_.emplace_back(arguments.values[i]); +} + +bool WindowsRuntimeDaemon::SetEnabled(bool enabled, std::string& error) { + std::lock_guard lock(mutex_); + error.clear(); + // Config-only callers and the unattended service do not own a GUI watcher. + if (!initialized_) return true; + if (!enabled) return Stop(error); + if (process_ && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT) return true; + if (!Stop(error)) return false; + if (executable_.empty()) + return Failed("Resolve daemon executable", error, ERROR_FILE_NOT_FOUND); + + SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE}; + Handle owner; + if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), + GetCurrentProcess(), &owner.value, + SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE, 0)) + return Failed("Duplicate daemon owner", error); + Handle stop{CreateEventW(&security, TRUE, FALSE, nullptr)}; + Handle ready{CreateEventW(&security, TRUE, FALSE, nullptr)}; + if (!stop.value || !ready.value) return Failed("Create daemon events", error); + + SIZE_T bytes = 0; + InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes); + std::vector attributes(bytes); + STARTUPINFOEXW startup{}; + startup.StartupInfo.cb = sizeof(startup); + startup.lpAttributeList = + reinterpret_cast(attributes.data()); + if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0, &bytes)) + return Failed("Initialize daemon handle list", error); + // Inherit only these private handles, never GUI locks, sockets or files. + HANDLE inherited[] = {owner.value, stop.value, ready.value}; + bool started = UpdateProcThreadAttribute( + startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST, + inherited, sizeof(inherited), nullptr, nullptr) != FALSE; + PROCESS_INFORMATION child{}; + if (started) { + std::wstring command = QuoteArgument(executable_) + L" --windows-daemon"; + for (HANDLE handle : inherited) + command += L" " + std::to_wstring(reinterpret_cast(handle)); + for (const auto& argument : arguments_) + command += L" " + QuoteArgument(argument); + started = CreateProcessW(executable_.c_str(), command.data(), nullptr, + nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW, + nullptr, nullptr, &startup.StartupInfo, &child) != FALSE; + } + const DWORD code = GetLastError(); + DeleteProcThreadAttributeList(startup.lpAttributeList); + if (!started) return Failed("Start daemon", error, code); + CloseHandle(child.hThread); + process_ = child.hProcess; + stop_ = stop.value; + stop.value = nullptr; + HANDLE startup_events[] = {process_, ready.value}; + if (WaitForMultipleObjects(2, startup_events, FALSE, kStartupTimeoutMs) != + WAIT_OBJECT_0 + 1) { + std::string cleanup; + Stop(cleanup); + return Failed("Wait for daemon readiness", error, ERROR_PROCESS_ABORTED); + } + return true; +} + +bool WindowsRuntimeDaemon::Stop(std::string& error) { + if (!process_) return true; + SetEvent(stop_); + if (WaitForSingleObject(process_, kStartupTimeoutMs) != WAIT_OBJECT_0) { + // The handle still identifies our helper even if its PID has been reused. + if (!TerminateProcess(process_, ERROR_PROCESS_ABORTED) || + WaitForSingleObject(process_, kStartupTimeoutMs) != WAIT_OBJECT_0) + return Failed("Stop daemon", error); + } + CloseHandle(process_); + CloseHandle(stop_); + process_ = stop_ = nullptr; + return true; +} + +void WindowsRuntimeDaemon::Shutdown(bool restart) { + std::lock_guard lock(mutex_); + if (!restart) { + std::string error; + Stop(error); + } else { + // Leave the helper armed until this process exits and releases its leases. + if (process_) CloseHandle(process_); + if (stop_) CloseHandle(stop_); + process_ = stop_ = nullptr; + } +} + +int RunWindowsRuntimeDaemon() { + Arguments arguments; + if (!arguments.values || arguments.count < 5 || + std::wcscmp(arguments.values[1], L"--windows-daemon") != 0) return 1; + Handle owner{ParseHandle(arguments.values[2])}; + Handle stop{ParseHandle(arguments.values[3])}; + Handle ready{ParseHandle(arguments.values[4])}; + const DWORD owner_id = GetProcessId(owner.value); + if (!owner_id || owner_id == GetCurrentProcessId() || !stop.value || + !ready.value) return 1; + for (HANDLE handle : {owner.value, stop.value, ready.value}) { + DWORD flags = 0; + if (!GetHandleInformation(handle, &flags) || !(flags & HANDLE_FLAG_INHERIT) || + !SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0)) return 1; + } + SessionWindow session; + if (!session.Create(owner_id) || !SetEvent(ready.value)) return 1; + + bool exited = false; + ULONGLONG restart_at = 0; + HANDLE watched[] = {stop.value, owner.value}; + for (;;) { + session.Pump(); + if (session.ending || WaitForSingleObject(stop.value, 0) == WAIT_OBJECT_0) + return 0; + const auto now = GetTickCount64(); + if (exited && now >= restart_at && !session.query_pending && + !GetSystemMetrics(SM_SHUTTINGDOWN)) break; + const DWORD timeout = exited && now < restart_at + ? static_cast(restart_at - now) : INFINITE; + const DWORD count = exited ? 1 : 2; + const DWORD result = MsgWaitForMultipleObjectsEx( + count, watched, timeout, QS_ALLINPUT, MWMO_INPUTAVAILABLE); + if (result == WAIT_OBJECT_0) return 0; + if (!exited && result == WAIT_OBJECT_0 + 1) { + exited = true; + restart_at = GetTickCount64() + kRestartDelayMs; + } else if (result != WAIT_OBJECT_0 + count && result != WAIT_TIMEOUT) { + return 1; + } + } + const auto executable = ExecutablePath(); + if (executable.empty()) return 1; + std::wstring command = QuoteArgument(executable); + for (int i = 5; i < arguments.count; ++i) + command += L" " + QuoteArgument(arguments.values[i]); + STARTUPINFOW startup{sizeof(startup)}; + PROCESS_INFORMATION child{}; + if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, + FALSE, 0, nullptr, nullptr, &startup, &child)) return 1; + CloseHandle(child.hThread); + CloseHandle(child.hProcess); + return 0; +} + +} // namespace crossdesk::platform diff --git a/apps/desktop/src/platform/windows/runtime_daemon.h b/apps/desktop/src/platform/windows/runtime_daemon.h new file mode 100644 index 00000000..f3ea60ac --- /dev/null +++ b/apps/desktop/src/platform/windows/runtime_daemon.h @@ -0,0 +1,42 @@ +#ifndef CROSSDESK_PLATFORM_WINDOWS_RUNTIME_DAEMON_H_ +#define CROSSDESK_PLATFORM_WINDOWS_RUNTIME_DAEMON_H_ + +#include + +#include +#include +#include + +namespace crossdesk::platform { + +inline constexpr char kWindowsDaemonArgument[] = "--windows-daemon"; + +// Watches the GUI from a separate process, so settings can start/stop crash +// recovery without replacing the GUI or interrupting its sessions. +class WindowsRuntimeDaemon { + public: + static WindowsRuntimeDaemon& Instance(); + ~WindowsRuntimeDaemon(); + + void Initialize(); + bool SetEnabled(bool enabled, std::string& error); + void Shutdown(bool restart = false); + + private: + WindowsRuntimeDaemon() = default; + bool Stop(std::string& error); + std::mutex mutex_; + bool initialized_ = false; + std::wstring executable_; + std::vector arguments_; + HANDLE process_ = nullptr; + HANDLE stop_ = nullptr; +}; + +// Dispatch before logging, configuration, service startup and instance locks. +// Reads the Unicode command line itself, including the original GUI arguments. +int RunWindowsRuntimeDaemon(); + +} // namespace crossdesk::platform + +#endif diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index 8aed5d09..fffb6d87 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -504,6 +504,12 @@ function setup_targets() add_defines("CROSSDESK_PORTABLE=1") end add_files("apps/desktop/tests/windows_autostart_test.cpp") + + target("windows_runtime_daemon") + set_kind("static") + add_includedirs("apps/desktop/src", {public = true}) + add_files("apps/desktop/src/platform/windows/runtime_daemon.cpp") + add_syslinks("User32", "Shell32", {public = true}) end target("config_center") @@ -511,6 +517,8 @@ function setup_targets() add_deps("rd_log", "autostart") if is_os("linux") then add_deps("linux_runtime_daemon") + elseif is_os("windows") then + add_deps("windows_runtime_daemon") end add_files("apps/desktop/src/config_center/*.cpp") add_includedirs("apps/desktop/src", "apps/desktop/src/config_center", diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index 636810f2..936d9273 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -158,7 +158,7 @@ sudo apt install "./crossdesk-virtual-desktop-xfce-linux-amd64-.deb" | 被连接时开启隐私屏 | 设置本机被远程连接时是否自动启用隐私屏;会话中可从控制端工具栏切换 | | 自托管配置 | 点击“编辑”填写服务器地址和信令服务端口,使用右侧开关启用 | | 开机自启 | 保存后设置登录桌面时自动启动 | -| 启用守护进程 | Linux 桌面模式保存后立即启用或停用异常退出后的自动重启,无需重启程序;正常退出不会重新拉起。Windows / macOS 下次启动生效,无头控制台模式不启用内置守护 | +| 启用守护进程 | Windows / Linux 桌面模式保存后立即启用或停用异常退出后的自动重启,无需重启程序;正常退出或注销不会重新拉起。macOS 下次启动生效,无头控制台模式不启用内置守护 | | 文件保存路径 | 选择接收文件的目录 | 网络连接分组中的 IP 直连开关独立于 ID 连接使用的中继设置: