From 97ab9bfca55d186687c356d937a6ec99544a7576 Mon Sep 17 00:00:00 2001 From: dijunkun Date: Wed, 19 Nov 2025 22:09:51 +0800 Subject: [PATCH] [feat] add daemon support with automatic restart on crash --- src/app/daemon.cpp | 396 +++++++++++++++++++++ src/app/daemon.h | 44 +++ src/app/main.cpp | 55 ++- src/config_center/config_center.cpp | 16 + src/config_center/config_center.h | 3 + src/gui/assets/layouts/layout.h | 8 +- src/gui/assets/localization/localization.h | 2 + src/gui/render.cpp | 1 + src/gui/render.h | 2 + src/gui/windows/main_settings_window.cpp | 26 ++ xmake.lua | 3 +- 11 files changed, 549 insertions(+), 7 deletions(-) create mode 100644 src/app/daemon.cpp create mode 100644 src/app/daemon.h diff --git a/src/app/daemon.cpp b/src/app/daemon.cpp new file mode 100644 index 0000000..e9a47ab --- /dev/null +++ b/src/app/daemon.cpp @@ -0,0 +1,396 @@ +#include "daemon.h" + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#elif __APPLE__ +#include +#include +#include +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#include +#include +#include + +#include +#endif + +#ifndef _WIN32 +Daemon* Daemon::instance_ = nullptr; +#endif + +// get executable file path +static std::string GetExecutablePath() { +#ifdef _WIN32 + char path[32768]; + DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path)); + if (length > 0 && length < sizeof(path)) { + return std::string(path); + } +#elif __APPLE__ + char path[PATH_MAX]; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + char resolved_path[PATH_MAX]; + if (realpath(path, resolved_path) != nullptr) { + return std::string(resolved_path); + } + return std::string(path); + } +#else + char path[PATH_MAX]; + ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1); + if (count != -1) { + path[count] = '\0'; + return std::string(path); + } +#endif + return ""; +} + +Daemon::Daemon(const std::string& name) + : name_(name) +#ifdef _WIN32 + , + running_(false) +#else + , + running_(true) +#endif +{ +} + +void Daemon::stop() { +#ifdef _WIN32 + running_ = false; +#else + running_ = false; +#endif +} + +bool Daemon::isRunning() const { +#ifdef _WIN32 + return running_; +#else + return running_; +#endif +} + +bool Daemon::start(MainLoopFunc loop) { +#ifdef _WIN32 + running_ = true; + return runWithRestart(loop); +#elif __APPLE__ + // macOS: Use child process monitoring (like Windows) to preserve GUI + running_ = true; + return runWithRestart(loop); +#else + // Linux: Use traditional daemonization + instance_ = this; + runUnix(loop); + return true; +#endif +} + +#ifdef _WIN32 +// windows: execute loop and catch C++ exceptions +static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) { + try { + loop(); + return 0; // normal exit + } catch (const std::exception& e) { + std::cerr << "Exception caught: " << e.what() << std::endl; + return 1; // c++ exception + } catch (...) { + std::cerr << "Unknown exception caught" << std::endl; + return 1; // other exception + } +} + +// windows: Use SEH wrapper function to catch system-level crashes +static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) { + __try { + return RunLoopCatchCpp(loop); + } __except (EXCEPTION_EXECUTE_HANDLER) { + // Catch system-level crashes (access violation, divide by zero, etc.) + DWORD code = GetExceptionCode(); + std::cerr << "System crash detected (SEH exception code: 0x" << std::hex + << code << std::dec << ")" << std::endl; + return 2; // System crash + } +} +#endif + +// run function with restart logic (infinite restart) +// use child process monitoring: parent process creates child process to run +// main program, monitors child process status, restarts on crash +bool Daemon::runWithRestart(MainLoopFunc loop) { + int restart_count = 0; + std::string exe_path = GetExecutablePath(); + if (exe_path.empty()) { + std::cerr + << "Failed to get executable path, falling back to direct execution" + << std::endl; + // fallback to direct execution + while (isRunning()) { + try { + loop(); + break; + } catch (...) { + restart_count++; + std::cerr << "Exception caught, restarting... (attempt " + << restart_count << ")" << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + } + } + return true; + } + + while (isRunning()) { +#ifdef _WIN32 + // windows: use CreateProcess to create child process + STARTUPINFOA si = {sizeof(si)}; + PROCESS_INFORMATION pi = {0}; + + // build command line arguments (add --child flag) + std::string cmd_line = "\"" + exe_path + "\" --child"; + std::vector cmd_line_buf(cmd_line.begin(), cmd_line.end()); + cmd_line_buf.push_back('\0'); + + BOOL success = CreateProcessA( + nullptr, // executable file path (specified in command line) + cmd_line_buf.data(), // command line arguments + nullptr, // process security attributes + nullptr, // thread security attributes + FALSE, // don't inherit handles + 0, // creation flags + nullptr, // environment variables (inherit from parent) + nullptr, // current directory + &si, // startup info + &pi // process information + ); + + if (!success) { + std::cerr << "Failed to create child process, error: " << GetLastError() + << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + restart_count++; + continue; + } + + // wait for child process to exit + DWORD exit_code = 0; + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exit_code); + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + + if (exit_code == 0) { + // normal exit + break; + } else { + // abnormal exit, restart + restart_count++; + std::cerr << "Child process exited with code " << exit_code + << ", restarting... (attempt " << restart_count << ")" + << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + } +#else + // linux: use fork + exec to create child process + pid_t pid = fork(); + if (pid == 0) { + // child process: execute main program, pass --child argument + execl(exe_path.c_str(), exe_path.c_str(), "--child", nullptr); + // if exec fails, exit + _exit(1); + } else if (pid > 0) { + // parent process: wait for child process to exit + int status = 0; + waitpid(pid, &status, 0); + + if (WIFEXITED(status)) { + int exit_code = WEXITSTATUS(status); + if (exit_code == 0) { + // normal exit + break; + } else { + // abnormal exit, restart + restart_count++; + std::cerr << "Child process exited with code " << exit_code + << ", restarting... (attempt " << restart_count << ")" + << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + } + } else if (WIFSIGNALED(status)) { + // child process terminated by signal (crash) + int sig = WTERMSIG(status); + restart_count++; + std::cerr << "Child process crashed with signal " << sig + << ", restarting... (attempt " << restart_count << ")" + << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + } + } else { + // fork failed + std::cerr << "Failed to fork child process" << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + restart_count++; + } +#endif + } + + return true; +} + +#ifndef _WIN32 +void Daemon::runUnix(MainLoopFunc loop) { + struct sigaction sa; + sa.sa_handler = [](int) {}; + sigemptyset(&sa.sa_mask); + sa.sa_flags = 0; + sigaction(SIGPIPE, &sa, nullptr); + sigaction(SIGCHLD, &sa, nullptr); + + // daemon mode: fork twice, redirect output + pid_t pid = fork(); + if (pid > 0) _exit(0); + setsid(); + pid = fork(); + if (pid > 0) _exit(0); + umask(0); + chdir("/"); + int fd = open("/dev/null", O_RDWR); + if (fd >= 0) { + dup2(fd, STDIN_FILENO); + dup2(fd, STDOUT_FILENO); + dup2(fd, STDERR_FILENO); + if (fd > 2) close(fd); + } + + // catch signals for exit + signal(SIGTERM, [](int) { instance_->stop(); }); + signal(SIGINT, [](int) { instance_->stop(); }); + + // catch crash signals, use atomic flags to mark crash + static std::atomic crash_detected{false}; + static std::atomic should_restart{false}; + + // use safer signal handling: only set flags, don't call longjmp + struct sigaction sa_crash; + sa_crash.sa_handler = [](int sig) { + const char* sig_name = "Unknown"; + switch (sig) { + case SIGSEGV: + sig_name = "SIGSEGV"; + break; + case SIGABRT: + sig_name = "SIGABRT"; + break; + case SIGFPE: + sig_name = "SIGFPE"; + break; + case SIGILL: + sig_name = "SIGILL"; + break; + } + std::cerr << "Crash signal detected: " << sig_name + << ", will restart after process exits" << std::endl; + crash_detected = true; + should_restart = true; + // don't call longjmp, let program exit normally, restart by monitoring + // thread + }; + sigemptyset(&sa_crash.sa_mask); + sa_crash.sa_flags = SA_RESETHAND; // handle only once, avoid recursion + + sigaction(SIGSEGV, &sa_crash, nullptr); + sigaction(SIGABRT, &sa_crash, nullptr); + sigaction(SIGFPE, &sa_crash, nullptr); + sigaction(SIGILL, &sa_crash, nullptr); + + running_ = true; + + // run with restart logic (infinite restart) + // run main loop in separate thread, main thread monitors thread status + int restart_count = 0; + while (running_) { + crash_detected = false; + should_restart = false; + std::atomic loop_completed{false}; + std::exception_ptr loop_exception = nullptr; + + // run main loop in separate thread + std::thread loop_thread([&loop, &loop_completed, &loop_exception]() { + try { + loop(); + loop_completed = true; + } catch (const std::exception& e) { + loop_exception = std::current_exception(); + loop_completed = true; + } catch (...) { + loop_exception = std::current_exception(); + loop_completed = true; + } + }); + + // wait for thread to complete + loop_thread.join(); + + // check exit reason + if (loop_exception) { + restart_count++; + try { + std::rethrow_exception(loop_exception); + } catch (const std::exception& e) { + std::cerr << "Exception caught: " << e.what() << std::endl; + } catch (...) { + std::cerr << "Unknown exception caught" << std::endl; + } + std::cerr << "Restarting... (attempt " << restart_count << ")" + << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + continue; + } + + // check if crash signal detected + if (crash_detected || should_restart) { + restart_count++; + std::cerr << "Crash detected, restarting... (attempt " << restart_count + << ")" << std::endl; + std::this_thread::sleep_for( + std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS)); + continue; + } + + // normal exit + if (loop_completed) { + break; + } + } +} +#endif diff --git a/src/app/daemon.h b/src/app/daemon.h new file mode 100644 index 0000000..86a670b --- /dev/null +++ b/src/app/daemon.h @@ -0,0 +1,44 @@ +/* + * @Author: DI JUNKUN + * @Date: 2025-11-19 + * Copyright (c) 2025 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _DAEMON_H_ +#define _DAEMON_H_ + +#include +#include + +// default restart delay (milliseconds) +#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000 + +class Daemon { + public: + using MainLoopFunc = std::function; + + Daemon(const std::string& name); + + // start daemon (restart after 1 second by default) + bool start(MainLoopFunc loop); + + // request exit + void stop(); + + // check if running + bool isRunning() const; + + private: + std::string name_; + bool runWithRestart(MainLoopFunc loop); + +#ifdef _WIN32 + bool running_; +#else + static Daemon* instance_; + void runUnix(MainLoopFunc loop); + volatile bool running_; +#endif +}; + +#endif \ No newline at end of file diff --git a/src/app/main.cpp b/src/app/main.cpp index b583526..4e3a35a 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -6,12 +6,61 @@ #endif #endif -#include "rd_log.h" +#include +#include +#include + +#include "config_center.h" +#include "daemon.h" +#include "path_manager.h" #include "render.h" -int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { +int main(int argc, char* argv[]) { + // check if running as child process + bool is_child = false; + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--child") == 0) { + is_child = true; + break; + } + } + + if (is_child) { + // child process: run render directly + crossdesk::Render render; + render.Run(); + return 0; + } + + bool enable_daemon = false; + auto path_manager = std::make_unique("CrossDesk"); + if (path_manager) { + std::string cert_path = + (path_manager->GetCertPath() / "crossdesk.cn_root.crt").string(); + std::string cache_path = path_manager->GetCachePath().string(); + crossdesk::ConfigCenter config_center(cache_path + "/config.ini", + cert_path); + enable_daemon = config_center.IsEnableDaemon(); + } + + if (enable_daemon) { + // start daemon with restart monitoring + Daemon daemon("CrossDesk"); + + // define main loop function: run render and stop daemon on normal exit + Daemon::MainLoopFunc main_loop = [&daemon]() { + crossdesk::Render render; + render.Run(); + daemon.stop(); + }; + + // start daemon and return result + bool success = daemon.start(main_loop); + return success ? 0 : 1; + } + + // run without daemon: direct execution crossdesk::Render render; render.Run(); - return 0; } \ No newline at end of file diff --git a/src/config_center/config_center.cpp b/src/config_center/config_center.cpp index 15b7f3c..3908d70 100644 --- a/src/config_center/config_center.cpp +++ b/src/config_center/config_center.cpp @@ -53,6 +53,7 @@ int ConfigCenter::Load() { ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_); enable_autostart_ = ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_); + enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_); enable_minimize_to_tray_ = ini_.GetBoolValue( section_, "enable_minimize_to_tray", enable_minimize_to_tray_); @@ -76,6 +77,7 @@ int ConfigCenter::Save() { ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str()); ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_); ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_); + ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_); ini_.SetBoolValue(section_, "enable_minimize_to_tray", enable_minimize_to_tray_); @@ -249,6 +251,18 @@ int ConfigCenter::SetAutostart(bool enable_autostart) { return 0; } +int ConfigCenter::SetDaemon(bool enable_daemon) { + enable_daemon_ = enable_daemon; + + ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_); + SI_Error rc = ini_.SaveFile(config_path_.c_str()); + if (rc < 0) { + return -1; + } + + return 0; +} + // getters ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } @@ -304,4 +318,6 @@ bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; } bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; } bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; } + +bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; } } // namespace crossdesk \ No newline at end of file diff --git a/src/config_center/config_center.h b/src/config_center/config_center.h index 1726244..b930cd5 100644 --- a/src/config_center/config_center.h +++ b/src/config_center/config_center.h @@ -41,6 +41,7 @@ class ConfigCenter { int SetSelfHosted(bool enable_self_hosted); int SetMinimizeToTray(bool enable_minimize_to_tray); int SetAutostart(bool enable_autostart); + int SetDaemon(bool enable_daemon); // read config @@ -62,6 +63,7 @@ class ConfigCenter { bool IsSelfHosted() const; bool IsMinimizeToTray() const; bool IsEnableAutostart() const; + bool IsEnableDaemon() const; int Load(); int Save(); @@ -89,6 +91,7 @@ class ConfigCenter { bool enable_self_hosted_ = false; bool enable_minimize_to_tray_ = false; bool enable_autostart_ = false; + bool enable_daemon_ = false; }; } // namespace crossdesk #endif \ No newline at end of file diff --git a/src/gui/assets/layouts/layout.h b/src/gui/assets/layouts/layout.h index 1695b63..edfe402 100644 --- a/src/gui/assets/layouts/layout.h +++ b/src/gui/assets/layouts/layout.h @@ -21,11 +21,11 @@ #define SETTINGS_WINDOW_WIDTH_CN 202 #define SETTINGS_WINDOW_WIDTH_EN 248 #if _WIN32 +#define SETTINGS_WINDOW_HEIGHT_CN 405 +#define SETTINGS_WINDOW_HEIGHT_EN 405 +#else #define SETTINGS_WINDOW_HEIGHT_CN 375 #define SETTINGS_WINDOW_HEIGHT_EN 375 -#else -#define SETTINGS_WINDOW_HEIGHT_CN 345 -#define SETTINGS_WINDOW_HEIGHT_EN 345 #endif #define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228 #define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275 @@ -49,6 +49,8 @@ #define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 #define ENABLE_AUTOSTART_PADDING_CN 171 #define ENABLE_AUTOSTART_PADDING_EN 218 +#define ENABLE_DAEMON_PADDING_CN 171 +#define ENABLE_DAEMON_PADDING_EN 218 #define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171 #define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218 #define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90 diff --git a/src/gui/assets/localization/localization.h b/src/gui/assets/localization/localization.h index 0f2e4d9..1f3e376 100644 --- a/src/gui/assets/localization/localization.h +++ b/src/gui/assets/localization/localization.h @@ -171,6 +171,8 @@ static std::vector confirm_delete_connection = { static std::vector enable_autostart = { reinterpret_cast(u8"开机自启:"), "Auto Start:"}; +static std::vector enable_daemon = { + reinterpret_cast(u8"启用守护进程:"), "Enable Daemon:"}; #if _WIN32 static std::vector minimize_to_tray = { reinterpret_cast(u8"退出时最小化到系统托盘:"), diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 8efb218..aab12eb 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -261,6 +261,7 @@ int Render::LoadSettingsFromCacheFile() { enable_srtp_ = config_center_->IsEnableSrtp(); enable_self_hosted_ = config_center_->IsSelfHosted(); enable_autostart_ = config_center_->IsEnableAutostart(); + enable_daemon_ = config_center_->IsEnableDaemon(); enable_minimize_to_tray_ = config_center_->IsMinimizeToTray(); language_button_value_last_ = language_button_value_; diff --git a/src/gui/render.h b/src/gui/render.h index eb3faa1..0eca1af 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -465,6 +465,8 @@ class Render { bool enable_self_hosted_last_ = false; bool enable_autostart_ = false; bool enable_autostart_last_ = false; + bool enable_daemon_ = false; + bool enable_daemon_last_ = false; bool enable_minimize_to_tray_ = false; bool enable_minimize_to_tray_last_ = false; char signal_server_ip_self_[256] = ""; diff --git a/src/gui/windows/main_settings_window.cpp b/src/gui/windows/main_settings_window.cpp index ef3c03e..a5fcafb 100644 --- a/src/gui/windows/main_settings_window.cpp +++ b/src/gui/windows/main_settings_window.cpp @@ -252,6 +252,25 @@ int Render::SettingWindow() { ImGui::SetCursorPosY(settings_items_offset); ImGui::Checkbox("##enable_autostart_", &enable_autostart_); } + + ImGui::Separator(); + + { + settings_items_offset += settings_items_padding; + ImGui::SetCursorPosY(settings_items_offset + 4); + + ImGui::Text( + "%s", + localization::enable_daemon[localization_language_index_].c_str()); + + if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { + ImGui::SetCursorPosX(ENABLE_DAEMON_PADDING_CN); + } else { + ImGui::SetCursorPosX(ENABLE_DAEMON_PADDING_EN); + } + ImGui::SetCursorPosY(settings_items_offset); + ImGui::Checkbox("##enable_daemon_", &enable_daemon_); + } #if _WIN32 ImGui::Separator(); @@ -373,6 +392,13 @@ int Render::SettingWindow() { } enable_autostart_last_ = enable_autostart_; + if (enable_daemon_) { + config_center_->SetDaemon(true); + } else { + config_center_->SetDaemon(false); + } + enable_daemon_last_ = enable_daemon_; + #if _WIN32 if (enable_minimize_to_tray_) { config_center_->SetMinimizeToTray(true); diff --git a/xmake.lua b/xmake.lua index d7ed16b..57ddcf9 100644 --- a/xmake.lua +++ b/xmake.lua @@ -179,4 +179,5 @@ target("gui") target("crossdesk") set_kind("binary") add_deps("rd_log", "common", "gui") - add_files("src/app/main.cpp") \ No newline at end of file + add_files("src/app/*.cpp") + add_includedirs("src/app", {public = true}) \ No newline at end of file