[fix] apply Windows daemon settings immediately

Add a runtime watchdog with Unicode process launch and session-end handling. Report autostart save failures and document the updated Windows behavior.

Validated with a Windows Release build, daemon and autostart integration checks, GUI crash recovery, tray exit, real logoff/login, and full reboot/login.
This commit is contained in:
dijunkun
2026-11-08 17:38:30 +08:00
parent c75394e498
commit fd6410e044
8 changed files with 388 additions and 16 deletions
+18 -2
View File
@@ -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) {
@@ -12,6 +12,10 @@
#include "rd_log.h"
#include <minirtc.h>
#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;
@@ -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();
@@ -4,6 +4,7 @@
#include <atomic>
#include <chrono>
#include <filesystem>
#include <thread>
#include <vector>
@@ -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<bool()>& keep_running) {
DaemonChildResult result;
STARTUPINFOA startup = {sizeof(startup)};
STARTUPINFOW startup = {sizeof(startup)};
PROCESS_INFORMATION process = {};
std::string command = "\"" + executable_path + "\" --child";
std::vector<char> 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;
}
@@ -0,0 +1,290 @@
#include "platform/windows/runtime_daemon.h"
#include <shellapi.h>
#include <cerrno>
#include <cstdint>
#include <cwchar>
#include <limits>
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<wchar_t> path(32768);
const DWORD count = GetModuleFileNameW(nullptr, path.data(),
static_cast<DWORD>(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<uintptr_t>::max)()) return nullptr;
return reinterpret_cast<HANDLE>(static_cast<uintptr_t>(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<CREATESTRUCTW*>(lparam);
SetWindowLongPtrW(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
}
auto* state = reinterpret_cast<SessionWindow*>(
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<std::mutex> 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<std::mutex> 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<unsigned char> attributes(bytes);
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.lpAttributeList =
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(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<uintptr_t>(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<std::mutex> 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<DWORD>(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
@@ -0,0 +1,42 @@
#ifndef CROSSDESK_PLATFORM_WINDOWS_RUNTIME_DAEMON_H_
#define CROSSDESK_PLATFORM_WINDOWS_RUNTIME_DAEMON_H_
#include <windows.h>
#include <mutex>
#include <string>
#include <vector>
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<std::wstring> 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
+8
View File
@@ -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",
+1 -1
View File
@@ -158,7 +158,7 @@ sudo apt install "./crossdesk-virtual-desktop-xfce-linux-amd64-<version>.deb"
| 被连接时开启隐私屏 | 设置本机被远程连接时是否自动启用隐私屏;会话中可从控制端工具栏切换 |
| 自托管配置 | 点击“编辑”填写服务器地址和信令服务端口,使用右侧开关启用 |
| 开机自启 | 保存后设置登录桌面时自动启动 |
| 启用守护进程 | Linux 桌面模式保存后立即启用或停用异常退出后的自动重启,无需重启程序;正常退出不会重新拉起。Windows / macOS 下次启动生效,无头控制台模式不启用内置守护 |
| 启用守护进程 | Windows / Linux 桌面模式保存后立即启用或停用异常退出后的自动重启,无需重启程序;正常退出或注销不会重新拉起。macOS 下次启动生效,无头控制台模式不启用内置守护 |
| 文件保存路径 | 选择接收文件的目录 |
网络连接分组中的 IP 直连开关独立于 ID 连接使用的中继设置: