[fix] repair Linux autostart and apply daemon settings immediately, refs #147

This commit is contained in:
dijunkun
2026-11-08 15:57:58 +08:00
parent 32b4e442a1
commit c75394e498
9 changed files with 529 additions and 16 deletions
+28 -1
View File
@@ -37,9 +37,11 @@
#ifdef __linux__
#include "SimpleIni.h"
#include "platform/linux/autostart.h"
#include "platform/linux/headless/headless_console.h"
#include "platform/linux/headless/headless_session.h"
#include "platform/linux/privacy/privacy_guard.h"
#include "platform/linux/runtime_daemon.h"
#endif
#ifdef _WIN32
@@ -365,6 +367,13 @@ static int RunApplication(
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini");
enable_daemon = config_center.IsEnableDaemon();
#ifdef __linux__
if (config_center.IsEnableAutostart() &&
!crossdesk::platform::MigrateLegacyAutostart("CrossDesk")) {
crossdesk::get_logger()->warn(
"Could not migrate the legacy Linux autostart entry");
}
#endif
}
#ifdef __linux__
@@ -373,7 +382,20 @@ static int RunApplication(
// session instead.
if (std::getenv("CROSSDESK_HEADLESS_ACTIVE") ||
crossdesk::HeadlessConsole::Instance().active()) enable_daemon = false;
#endif
auto& runtime_daemon = crossdesk::platform::LinuxRuntimeDaemon::Instance();
runtime_daemon.Initialize(
argc, argv, !std::getenv("CROSSDESK_HEADLESS_ACTIVE") &&
!crossdesk::HeadlessConsole::Instance().active());
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) {
// The launcher lease stays in main for the supervisor's entire lifetime.
@@ -406,6 +428,7 @@ static int RunApplication(
// run without daemon: direct execution
crossdesk::Render render;
return render.Run();
#endif
}
int main(int argc, char* argv[]) {
@@ -421,6 +444,10 @@ int main(int argc, char* argv[]) {
#endif
#ifdef __linux__
// Run before GUI, configuration, logging or display initialization.
if (argc >= 2 &&
std::strcmp(argv[1], crossdesk::platform::kLinuxDaemonArgument) == 0) {
return crossdesk::platform::RunLinuxRuntimeDaemon(argc, argv);
}
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kLinuxPrivacyGuardArgument) == 0) {
return crossdesk::RunLinuxPrivacyGuard();
@@ -12,6 +12,11 @@
#include "rd_log.h"
#include <minirtc.h>
#if defined(__linux__) && !defined(__OHOS__)
#include "platform/linux/autostart.h"
#include "platform/linux/runtime_daemon.h"
#endif
namespace crossdesk {
struct ConfigCenter::IniStorage : CSimpleIniA {};
@@ -386,6 +391,14 @@ int ConfigCenter::SetSelfHosted(bool enabled) {
int ConfigCenter::SetAutostart(bool enabled) {
const bool previous = IsAutostartEnabled("CrossDesk");
#if defined(__linux__) && !defined(__OHOS__)
// Repair old deb entries even when the saved boolean is unchanged, keeping
// desktop-managed disabled flags and custom commands intact.
if (enabled && previous && !platform::MigrateLegacyAutostart("CrossDesk")) {
LOG_ERROR("Failed to migrate Linux autostart");
return -1;
}
#endif
// Refresh the executable path and remove legacy registrations even when the
// boolean state is unchanged. In particular, a Run entry no longer counts
// as enabled for an elevated Windows build, but still needs deleting.
@@ -411,8 +424,23 @@ int ConfigCenter::SetAutostart(bool enabled) {
}
int ConfigCenter::SetDaemon(bool enabled) {
#if 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__)
std::string error;
if (!platform::LinuxRuntimeDaemon::Instance().SetEnabled(enabled, error)) {
LOG_ERROR("Cannot apply daemon setting: {}", error);
if (StoreValues({{"enable_daemon", previous ? "true" : "false"}}) == 0) {
enable_daemon_ = previous;
} else {
LOG_ERROR("Cannot restore daemon preference after startup failure");
}
return -1;
}
#endif
return 0;
}
@@ -4985,7 +4985,13 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
#if !defined(__OHOS__)
config_center_->SetAutostart(enable_autostart_);
#endif
config_center_->SetDaemon(enable_daemon_);
if (config_center_->SetDaemon(enable_daemon_) != 0) {
enable_daemon_ = config_center_->IsEnableDaemon();
main->set_daemon_enabled(enable_daemon_);
main->set_settings_save_error(UiText(
localization::Literal("保存失败,请重试", localization_language_index_)));
return false;
}
if (config_center_->SetSaveRemotePreviews(main->get_save_remote_previews()) != 0) {
LOG_ERROR("Failed to save remote preview preference");
}
+79 -13
View File
@@ -1,15 +1,21 @@
#include "platform/autostart_backend.h"
#include "platform/linux/autostart.h"
#include <limits.h>
#include <unistd.h>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
namespace crossdesk::platform {
namespace {
constexpr const char* kInstalledExecutable = "/usr/lib/crossdesk/crossdesk-bin";
constexpr const char* kInstalledLauncher = "/usr/bin/crossdesk";
std::filesystem::path AutostartPath(const std::string& app_name) {
const char* config = std::getenv("XDG_CONFIG_HOME");
if (config && *config) return std::filesystem::path(config) / "autostart" / (app_name + ".desktop");
@@ -19,13 +25,39 @@ std::filesystem::path AutostartPath(const std::string& app_name) {
(app_name + ".desktop");
}
bool WriteDesktopEntry(const std::filesystem::path& path,
const std::string& contents) {
// In particular, a failed migration must leave the previous entry intact.
std::string temporary = path.string() + ".XXXXXX";
const int fd = mkstemp(temporary.data());
if (fd < 0) return false;
std::FILE* file = fdopen(fd, "w");
if (!file) {
close(fd);
std::remove(temporary.c_str());
return false;
}
const bool written =
std::fwrite(contents.data(), 1, contents.size(), file) == contents.size();
const bool closed = std::fclose(file) == 0;
if (written && closed && std::rename(temporary.c_str(), path.c_str()) == 0)
return true;
std::remove(temporary.c_str());
return false;
}
} // namespace
std::string GetAutostartExecutablePath() {
char path[PATH_MAX] = {};
const ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
return count > 0 ? std::string(path, static_cast<size_t>(count))
: std::string();
if (count <= 0 || count >= static_cast<ssize_t>(sizeof(path) - 1)) return {};
const std::string executable(path, static_cast<size_t>(count));
// The deb launcher sets LD_LIBRARY_PATH for the private Slint runtime.
// /proc/self/exe resolves to its exec() target and loses that setup.
if (executable == kInstalledExecutable && access(kInstalledLauncher, X_OK) == 0)
return kInstalledLauncher;
return executable;
}
bool EnableAutostart(const std::string& app_name,
@@ -37,19 +69,53 @@ bool EnableAutostart(const std::string& app_name,
std::filesystem::create_directories(path.parent_path(), error);
if (error) return false;
std::ofstream file(path);
return WriteDesktopEntry(path,
"[Desktop Entry]\n"
"Type=Application\n"
"Exec=" + executable_path + "\n"
"Hidden=false\n"
"NoDisplay=false\n"
"X-GNOME-Autostart-enabled=true\n"
"Terminal=false\n"
"StartupNotify=false\n"
"Name=" + app_name + "\n");
}
bool MigrateLegacyAutostart(const std::string& app_name) {
// Development/standalone copies must not take over an installed entry.
if (GetAutostartExecutablePath() != kInstalledLauncher) return true;
const std::filesystem::path path = AutostartPath(app_name);
if (path.empty()) return false;
std::error_code error;
const bool exists = std::filesystem::exists(path, error);
if (error) return false;
if (!exists) return true; // Do not recreate entries removed by the user.
if (!std::filesystem::is_regular_file(path, error) || error) return false;
std::ifstream file(path);
if (!file.is_open()) return false;
file << "[Desktop Entry]\n"
<< "Type=Application\n"
<< "Exec=" << executable_path << "\n"
<< "Hidden=false\n"
<< "NoDisplay=false\n"
<< "X-GNOME-Autostart-enabled=true\n"
<< "Terminal=false\n"
<< "StartupNotify=false\n"
<< "Name=" << app_name << "\n";
std::string contents((std::istreambuf_iterator<char>(file)), {});
if (file.bad()) return false;
file.close();
return file.good();
bool desktop_entry = false;
for (size_t start = 0; start < contents.size();) {
const size_t newline = contents.find('\n', start);
const size_t end = newline == std::string::npos ? contents.size() : newline;
size_t line_end = end;
if (line_end > start && contents[line_end - 1] == '\r') --line_end;
const std::string line = contents.substr(start, line_end - start);
if (!line.empty() && line.front() == '[')
desktop_entry = line == "[Desktop Entry]";
if (desktop_entry && line == std::string("Exec=") + kInstalledExecutable) {
// Preserve Hidden, desktop restrictions and all other user settings.
contents.replace(start, line_end - start,
std::string("Exec=") + kInstalledLauncher);
return WriteDesktopEntry(path, contents);
}
start = end == contents.size() ? end : end + 1;
}
return true;
}
bool DisableAutostart(const std::string& app_name) {
@@ -0,0 +1,14 @@
#ifndef CROSSDESK_LINUX_AUTOSTART_H_
#define CROSSDESK_LINUX_AUTOSTART_H_
#include <string>
namespace crossdesk::platform {
// Repair the old deb entry at normal startup without enabling missing entries
// or overwriting desktop-managed settings and custom commands.
bool MigrateLegacyAutostart(const std::string& app_name);
} // namespace crossdesk::platform
#endif
@@ -0,0 +1,320 @@
#include "platform/linux/runtime_daemon.h"
#include <dirent.h>
#include <dlfcn.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <signal.h>
#include <spawn.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstdlib>
#include <cstring>
#include <thread>
extern char** environ;
namespace crossdesk::platform {
namespace {
constexpr int kControlSocket = 3;
constexpr int kStartupTimeoutMs = 3000;
// A lost GUI socket can mean either a crash or desktop logout. Remember the
// owner's login session before it disappears, and never carry a restart into
// another login that happens to reuse the same DISPLAY (notably under XRDP).
class LoginSession {
public:
explicit LoginSession(pid_t owner) {
library_ = dlopen("libsystemd.so.0", RTLD_LAZY | RTLD_LOCAL);
if (!library_) return; // Non-logind desktops retain crash monitoring.
const auto pid_session = reinterpret_cast<int (*)(pid_t, char**)>(
dlsym(library_, "sd_pid_get_session"));
const auto uid_display = reinterpret_cast<int (*)(uid_t, char**)>(
dlsym(library_, "sd_uid_get_display"));
state_ = reinterpret_cast<int (*)(const char*, char**)>(
dlsym(library_, "sd_session_get_state"));
if (!state_) return;
char* id = nullptr;
if (pid_session && pid_session(owner, &id) >= 0 && id) {
session_ = id;
}
std::free(id);
// GNOME can start applications in its user manager rather than directly
// in the session scope. Use the supplied session or the user's graphical
// session in that case; a process's actual scope always takes precedence.
if (session_.empty()) {
if (const char* inherited = std::getenv("XDG_SESSION_ID"))
session_ = inherited;
}
if (session_.empty() && uid_display) {
id = nullptr;
if (uid_display(geteuid(), &id) >= 0 && id) session_ = id;
std::free(id);
}
}
~LoginSession() {
if (library_) dlclose(library_);
}
bool Alive() const {
if (!state_ || session_.empty()) return true;
char* state = nullptr;
const int result = state_(session_.c_str(), &state);
// "online" includes a locked/background session and must keep monitoring.
// A removed or closing session must never be restarted.
const bool alive = result >= 0 && state && std::strcmp(state, "closing") != 0;
std::free(state);
return alive;
}
private:
void* library_ = nullptr;
int (*state_)(const char*, char**) = nullptr;
std::string session_;
};
bool Send(int socket, char command) {
ssize_t count;
do {
count = send(socket, &command, 1, MSG_NOSIGNAL);
} while (count < 0 && errno == EINTR);
return count == 1;
}
bool Receive(int socket, char expected) {
pollfd fd{socket, POLLIN, 0};
int result;
do {
result = poll(&fd, 1, kStartupTimeoutMs);
} while (result < 0 && errno == EINTR);
if (result <= 0) return false;
char reply = 0;
return recv(socket, &reply, 1, 0) == 1 && reply == expected;
}
void Reap(int child) {
if (child < 0) return;
const auto deadline = std::chrono::steady_clock::now() +
std::chrono::seconds(2);
while (std::chrono::steady_clock::now() < deadline) {
const auto result = waitpid(child, nullptr, WNOHANG);
if (result == child || (result < 0 && errno != EINTR)) return;
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
// This is an unreaped direct child, so its PID cannot have been reused.
kill(child, SIGKILL);
while (waitpid(child, nullptr, 0) < 0 && errno == EINTR) {}
}
bool CloseInheritedDescriptors() {
// Libraries may own descriptors without CLOEXEC. Keeping those alive in the
// watcher could retain network ports or locks after the GUI crashes.
DIR* directory = opendir("/proc/self/fd");
if (!directory) return false;
while (const auto* entry = readdir(directory)) {
char* end = nullptr;
const long fd = std::strtol(entry->d_name, &end, 10);
if (*end == '\0' && fd > kControlSocket && fd != dirfd(directory))
close(static_cast<int>(fd));
}
closedir(directory);
return true;
}
} // namespace
LinuxRuntimeDaemon& LinuxRuntimeDaemon::Instance() {
static LinuxRuntimeDaemon daemon;
return daemon;
}
LinuxRuntimeDaemon::~LinuxRuntimeDaemon() { Shutdown(); }
void LinuxRuntimeDaemon::Initialize(int argc, char* argv[], bool allowed) {
std::lock_guard<std::mutex> lock(mutex_);
Stop();
allowed_ = allowed;
arguments_.clear();
char path[PATH_MAX]{};
const ssize_t count = readlink("/proc/self/exe", path, sizeof(path));
if (count <= 0 || count >= static_cast<ssize_t>(sizeof(path))) return;
arguments_.emplace_back(path, static_cast<size_t>(count));
arguments_.emplace_back(kLinuxDaemonArgument);
bool no_headless = false;
for (int i = 1; i < argc; ++i) {
arguments_.emplace_back(argv[i]);
no_headless |= arguments_.back() == "--no-headless";
}
// A desktop watchdog must never create a headless display after logout.
if (!no_headless) arguments_.emplace_back("--no-headless");
}
bool LinuxRuntimeDaemon::SetEnabled(bool enabled, std::string& error) {
std::lock_guard<std::mutex> lock(mutex_);
error.clear();
// Configuration-only callers and the headless supervisor do not own a GUI
// watchdog. Their preference is still saved for the next desktop launch.
if (!allowed_ || !enabled) {
Stop();
return true;
}
if (child_ >= 0) {
pollfd fd{socket_, POLLIN, 0};
if (poll(&fd, 1, 0) == 0) return true;
Stop();
}
if (arguments_.empty()) {
error = "Cannot resolve the executable for the daemon";
return false;
}
int sockets[2];
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockets) != 0) {
error = std::strerror(errno);
return false;
}
posix_spawn_file_actions_t actions;
posix_spawnattr_t attributes;
int result = posix_spawn_file_actions_init(&actions);
const bool actions_ready = result == 0;
bool attributes_ready = false;
if (result == 0) {
result = posix_spawnattr_init(&attributes);
attributes_ready = result == 0;
}
if (result == 0)
result = posix_spawn_file_actions_adddup2(&actions, sockets[1], kControlSocket);
if (result == 0 && sockets[0] != kControlSocket)
result = posix_spawn_file_actions_addclose(&actions, sockets[0]);
if (result == 0 && sockets[1] != kControlSocket)
result = posix_spawn_file_actions_addclose(&actions, sockets[1]);
sigset_t empty, defaults;
sigemptyset(&empty);
sigfillset(&defaults);
if (result == 0) result = posix_spawnattr_setsigmask(&attributes, &empty);
if (result == 0) result = posix_spawnattr_setsigdefault(&attributes, &defaults);
if (result == 0)
result = posix_spawnattr_setflags(
&attributes, POSIX_SPAWN_SETSID | POSIX_SPAWN_SETSIGMASK |
POSIX_SPAWN_SETSIGDEF);
std::vector<char*> args;
for (auto& argument : arguments_) args.push_back(argument.data());
args.push_back(nullptr);
pid_t child = -1;
if (result == 0)
result = posix_spawn(&child, "/proc/self/exe", &actions, &attributes,
args.data(), environ);
if (attributes_ready) posix_spawnattr_destroy(&attributes);
if (actions_ready) posix_spawn_file_actions_destroy(&actions);
close(sockets[1]);
if (result != 0) {
close(sockets[0]);
error = "Cannot start daemon: " + std::string(std::strerror(result));
return false;
}
socket_ = sockets[0];
child_ = child;
// Do not report an enabled setting until the helper is ready and armed.
if (!Receive(socket_, 'R') || !Send(socket_, 'A') || !Receive(socket_, 'A')) {
error = "Daemon did not acknowledge startup";
Stop();
return false;
}
return true;
}
void LinuxRuntimeDaemon::Stop() {
if (socket_ >= 0) {
Send(socket_, 'S');
close(socket_);
socket_ = -1;
}
Reap(child_);
child_ = -1;
}
void LinuxRuntimeDaemon::Shutdown(bool restart) {
std::lock_guard<std::mutex> lock(mutex_);
if (!restart) {
Stop();
} else {
if (socket_ >= 0) close(socket_);
socket_ = -1;
child_ = -1;
}
}
int RunLinuxRuntimeDaemon(int argc, char* argv[]) {
int type = 0;
socklen_t size = sizeof(type);
ucred peer{};
socklen_t peer_size = sizeof(peer);
if (argc < 2 || argv[0][0] != '/' ||
getsockopt(kControlSocket, SOL_SOCKET, SO_TYPE, &type, &size) != 0 ||
type != SOCK_SEQPACKET ||
getsockopt(kControlSocket, SOL_SOCKET, SO_PEERCRED, &peer, &peer_size) != 0 ||
peer.uid != geteuid() || !CloseInheritedDescriptors()) return 1;
LoginSession session(peer.pid);
if (!session.Alive()) return 1;
if (fcntl(kControlSocket, F_SETFD, FD_CLOEXEC) != 0 ||
!Send(kControlSocket, 'R')) return 1;
// Track the original process, not a potentially reused PID. A failed render
// can close the socket before its destructors have released instance locks.
int owner = -1;
#ifdef SYS_pidfd_open
owner = static_cast<int>(syscall(SYS_pidfd_open, peer.pid, 0));
#endif
bool armed = false;
for (;;) {
if (!session.Alive()) return 0;
pollfd control{kControlSocket, POLLIN, 0};
const int ready = poll(&control, 1, 1000);
if (ready < 0 && errno == EINTR) continue;
if (ready < 0) return 1;
if (ready == 0) continue;
char command = 0;
const auto count = recv(kControlSocket, &command, 1, 0);
if (count < 0 && errno == EINTR) continue;
if (count == 0 || (count < 0 && errno == ECONNRESET)) break;
if (count < 0) return 1;
if (command == 'S') return 0;
if (command != 'A' || armed) return 1;
armed = true;
// Even if the GUI disappears before receiving this acknowledgement, keep
// reading: a queued stop disarms us; otherwise the lost owner is restarted.
Send(kControlSocket, 'A');
}
close(kControlSocket);
if (!armed) return 0;
if (owner >= 0) {
pollfd fd{owner, POLLIN, 0};
int result;
do {
if (!session.Alive()) return 0;
result = poll(&fd, 1, 1000);
} while (result == 0 || (result < 0 && errno == EINTR));
close(owner);
if (result < 0) return 1;
}
// Let process teardown release its single-instance leases, and rate-limit
// repeated startup failures. The new GUI acquires the normal leases itself.
std::this_thread::sleep_for(std::chrono::seconds(1));
if (!session.Alive()) return 0;
std::vector<char*> arguments{argv[0]};
for (int i = 2; i < argc; ++i) arguments.push_back(argv[i]);
arguments.push_back(nullptr);
execv(argv[0], arguments.data());
return 1;
}
} // namespace crossdesk::platform
@@ -0,0 +1,40 @@
#ifndef CROSSDESK_PLATFORM_LINUX_RUNTIME_DAEMON_H_
#define CROSSDESK_PLATFORM_LINUX_RUNTIME_DAEMON_H_
#include <mutex>
#include <string>
#include <vector>
namespace crossdesk::platform {
inline constexpr char kLinuxDaemonArgument[] = "--linux-daemon";
// A separate process watches this GUI over a private socket. It can be attached
// after the GUI has started, without forking its threads or restarting sessions.
class LinuxRuntimeDaemon {
public:
static LinuxRuntimeDaemon& Instance();
~LinuxRuntimeDaemon();
void Initialize(int argc, char* argv[], bool allowed);
bool SetEnabled(bool enabled, std::string& error);
// Normal exit disarms the watcher. An unsuccessful application exit lets it
// restart, just like an unexpected loss of the control socket.
void Shutdown(bool restart = false);
private:
LinuxRuntimeDaemon() = default;
void Stop();
std::mutex mutex_;
bool allowed_ = false;
std::vector<std::string> arguments_;
int socket_ = -1;
int child_ = -1;
};
// Internal entry point, dispatched before logging, display or instance locks.
int RunLinuxRuntimeDaemon(int argc, char* argv[]);
} // namespace crossdesk::platform
#endif
+11
View File
@@ -509,11 +509,22 @@ function setup_targets()
target("config_center")
set_kind("object")
add_deps("rd_log", "autostart")
if is_os("linux") then
add_deps("linux_runtime_daemon")
end
add_files("apps/desktop/src/config_center/*.cpp")
add_includedirs("apps/desktop/src", "apps/desktop/src/config_center",
"deps/submodules/minirtc/src/api",
{public = true})
if is_os("linux") then
target("linux_runtime_daemon")
set_kind("static")
add_includedirs("apps/desktop/src", {public = true})
add_files("apps/desktop/src/platform/linux/runtime_daemon.cpp")
add_syslinks("dl", {public = true})
end
target("assets")
set_kind("headeronly")
add_includedirs("apps/desktop/src/gui/assets/localization",