feat: daemonize

This commit is contained in:
Lemmy
2026-05-10 12:48:33 -04:00
6 changed files with 408 additions and 33 deletions
+25 -4
View File
@@ -44,6 +44,7 @@
#include <stdexcept>
#include <string_view>
#include <thread>
#include <utility>
std::atomic<bool> Application::s_shutdownRequested{false};
@@ -51,6 +52,7 @@ namespace {
constexpr Logger kLog("app");
constexpr bool kLockKeysEnabled = true;
constexpr std::string_view kPolkitAuthorityBusName = "org.freedesktop.PolicyKit1";
template <typename Factory>
auto makeWithStartupBackoff(std::string_view label, Factory&& factory) -> decltype(factory()) {
@@ -216,12 +218,28 @@ void Application::syncPolkitAgent() {
return;
}
try {
if (!m_systemBus->nameHasOwner(kPolkitAuthorityBusName)) {
kLog.warn("polkit agent disabled: {} is not running", kPolkitAuthorityBusName);
m_polkitPollSource.reset();
m_polkitAgent.reset();
return;
}
} catch (const std::exception& e) {
kLog.warn("polkit agent disabled: failed to query {} owner: {}", kPolkitAuthorityBusName, e.what());
m_polkitPollSource.reset();
m_polkitAgent.reset();
return;
}
m_polkitAgent = std::make_unique<PolkitAgent>(*m_systemBus);
m_polkitAgent->setReadyCallback([this](bool ok, const std::string& error) {
if (!ok) {
kLog.warn("polkit agent disabled: {}", error);
m_polkitPollSource.reset();
m_polkitAgent.reset();
DeferredCall::callLater([this]() {
m_polkitPollSource.reset();
m_polkitAgent.reset();
});
return;
}
kLog.info("polkit authentication agent active");
@@ -253,7 +271,7 @@ void Application::syncPolkitAgent() {
m_polkitAgent->start();
}
void Application::run() {
void Application::run(std::function<void()> startupReadyCallback) {
initLogFile();
kLog.info("noctalia {}", noctalia::build_info::displayVersion());
runStartupPhase("initServices", [this]() { initServices(); });
@@ -273,9 +291,12 @@ void Application::run() {
#endif
m_trayInitTimer.start(std::chrono::milliseconds(500), [this]() { startTrayService(); });
m_polkitInitTimer.start(std::chrono::milliseconds(0), [this]() { syncPolkitAgent(); });
m_polkitInitTimer.start(std::chrono::milliseconds(1000), [this]() { syncPolkitAgent(); });
m_mainLoop = std::make_unique<MainLoop>(m_wayland, m_bar, [this]() { return currentPollSources(); });
if (startupReadyCallback) {
startupReadyCallback();
}
m_mainLoop->run();
kLog.info("shutdown");
}
+2 -1
View File
@@ -91,6 +91,7 @@
#include "wayland/workspace_poll_source.h"
#include <atomic>
#include <functional>
#include <memory>
#include <vector>
@@ -99,7 +100,7 @@ public:
Application();
~Application();
void run();
void run(std::function<void()> startupReadyCallback = {});
// Public for signal handler
static std::atomic<bool> s_shutdownRequested;
+134 -20
View File
@@ -11,11 +11,13 @@
#include <glib-object.h>
#include <glib.h>
#include <memory>
#include <mutex>
#include <optional>
#include <poll.h>
#include <pwd.h>
#include <string>
#include <sys/types.h>
#include <thread>
#include <unistd.h>
#include <utility>
#include <vector>
@@ -253,6 +255,13 @@ struct PolkitAgent::Impl {
PolkitAgentSession* session = nullptr;
GMainContext* context = nullptr;
GCancellable* registerCancellable = nullptr;
std::thread registerThread;
mutable std::mutex registerMutex;
gpointer pendingRegistrationHandle = nullptr;
bool registrationComplete = false;
bool registrationOk = false;
bool registrationShutdown = false;
std::string registrationError;
bool starting = false;
bool registered = false;
std::unique_ptr<InternalAuthRequest> pending;
@@ -280,9 +289,22 @@ struct PolkitAgent::Impl {
clearPending("PolkitAgent is being destroyed", true);
if (registerCancellable != nullptr) {
g_cancellable_cancel(registerCancellable);
}
{
std::lock_guard lock(registerMutex);
registrationShutdown = true;
}
if (registerThread.joinable()) {
registerThread.join();
}
if (registerCancellable != nullptr) {
g_object_unref(registerCancellable);
registerCancellable = nullptr;
}
if (pendingRegistrationHandle != nullptr) {
polkit_agent_listener_unregister(pendingRegistrationHandle);
pendingRegistrationHandle = nullptr;
}
if (listener != nullptr) {
listener->owner = nullptr;
listener->initiate = nullptr;
@@ -305,51 +327,133 @@ struct PolkitAgent::Impl {
return;
}
starting = true;
// Prefer the session id exported by the user's login/session manager. The
// process lookup path below is asynchronous in API shape, but it still can
// enter GLib's global worker pool before returning.
const char* sessionId = std::getenv("XDG_SESSION_ID");
registerCancellable = g_cancellable_new();
if (sessionId != nullptr && sessionId[0] != '\0') {
PolkitSubject* subject = polkit_unix_session_new(sessionId);
beginRegisterSubject(subject, nullptr);
return;
}
polkit_unix_session_new_for_process(::getpid(), registerCancellable, &Impl::sessionReadyTrampoline, this);
}
void onSessionReady(GObject* /*source*/, GAsyncResult* result) {
starting = false;
GError* error = nullptr;
PolkitSubject* subject = polkit_unix_session_new_for_process_finish(result, &error);
PolkitSubject* pidSubject = polkit_unix_session_new_for_process_finish(result, &error);
// If we were cancelled (destruction), bail out without touching members
// beyond the cancellable cleanup that the destructor already handled.
if (error != nullptr && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
g_clear_error(&error);
if (subject != nullptr) {
g_object_unref(subject);
if (pidSubject != nullptr) {
g_object_unref(pidSubject);
}
return;
}
beginRegisterSubject(pidSubject, error);
}
void beginRegisterSubject(PolkitSubject* subject, GError* error) {
if (subject == nullptr || error != nullptr) {
std::string message = error != nullptr ? error->message : "failed to create polkit session subject";
g_clear_error(&error);
if (subject != nullptr) {
g_object_unref(subject);
}
setRegistrationResult(nullptr, false, std::move(message));
return;
}
registerThread = std::thread([this, subject]() {
GError* registerError = nullptr;
gpointer handle =
polkit_agent_listener_register(POLKIT_AGENT_LISTENER(listener), POLKIT_AGENT_REGISTER_FLAGS_NONE, subject,
k_agentObjectPath, registerCancellable, &registerError);
g_object_unref(subject);
std::string message;
if (registerError != nullptr) {
message = registerError->message;
g_clear_error(&registerError);
} else if (handle == nullptr) {
message = "polkit listener registration returned no handle";
}
setRegistrationResult(handle, handle != nullptr && message.empty(), std::move(message));
});
}
void setRegistrationResult(gpointer handle, bool ok, std::string error) {
bool shouldUnregister = false;
{
std::lock_guard lock(registerMutex);
if (registrationShutdown) {
shouldUnregister = handle != nullptr;
} else {
pendingRegistrationHandle = handle;
registrationOk = ok;
registrationError = std::move(error);
registrationComplete = true;
}
}
if (shouldUnregister) {
polkit_agent_listener_unregister(handle);
}
}
bool registrationReady() const {
std::lock_guard lock(registerMutex);
return registrationComplete;
}
void finishRegistrationIfReady() {
gpointer handle = nullptr;
bool ok = false;
std::string error;
{
std::lock_guard lock(registerMutex);
if (!registrationComplete) {
return;
}
handle = pendingRegistrationHandle;
pendingRegistrationHandle = nullptr;
ok = registrationOk;
error = std::move(registrationError);
registrationComplete = false;
registrationOk = false;
}
if (registerThread.joinable()) {
registerThread.join();
}
if (registerCancellable != nullptr) {
g_object_unref(registerCancellable);
registerCancellable = nullptr;
}
if (subject == nullptr || error != nullptr) {
std::string message = error != nullptr ? error->message : "failed to create polkit session subject";
g_clear_error(&error);
starting = false;
if (!ok) {
if (handle != nullptr) {
polkit_agent_listener_unregister(handle);
}
if (readyCallback) {
readyCallback(false, message);
readyCallback(false, error.empty() ? "polkit listener registration failed" : error);
}
return;
}
listener->registration_handle = polkit_agent_listener_register(
POLKIT_AGENT_LISTENER(listener), POLKIT_AGENT_REGISTER_FLAGS_NONE, subject, k_agentObjectPath, nullptr, &error);
g_object_unref(subject);
if (error != nullptr) {
std::string message = error->message;
g_clear_error(&error);
if (readyCallback) {
readyCallback(false, message);
}
return;
if (listener->registration_handle != nullptr) {
polkit_agent_listener_unregister(listener->registration_handle);
}
listener->registration_handle = handle;
if (listener->registration_handle == nullptr) {
if (readyCallback) {
readyCallback(false, "polkit listener registration returned no handle");
@@ -575,9 +679,18 @@ struct PolkitAgent::Impl {
g_main_context_release(context);
}
int pollTimeoutMs() const { return glibPollTimeoutMs; }
int pollTimeoutMs() const {
if (registrationReady()) {
return 0;
}
if (starting) {
return glibPollTimeoutMs < 0 ? 100 : std::min(glibPollTimeoutMs, 100);
}
return glibPollTimeoutMs;
}
void dispatch(const std::vector<pollfd>& fds, std::size_t startIdx) {
finishRegistrationIfReady();
if (!g_main_context_acquire(context)) {
return;
}
@@ -603,6 +716,7 @@ struct PolkitAgent::Impl {
while (g_main_context_pending(context)) {
g_main_context_iteration(context, FALSE);
}
finishRegistrationIfReady();
}
PolkitRequest pendingRequest() const {
+18
View File
@@ -1,9 +1,27 @@
#include "dbus/system_bus.h"
#include <string>
namespace {
constexpr auto kDbusInterface = "org.freedesktop.DBus";
const sdbus::ServiceName kDbusName{kDbusInterface};
const sdbus::ObjectPath kDbusPath{"/org/freedesktop/DBus"};
} // namespace
SystemBus::SystemBus() : m_connection(sdbus::createSystemBusConnection()) {}
sdbus::IConnection::PollData SystemBus::getPollData() const { return m_connection->getEventLoopPollData(); }
bool SystemBus::nameHasOwner(std::string_view name) const {
auto proxy = sdbus::createProxy(*m_connection, kDbusName, kDbusPath);
bool hasOwner = false;
proxy->callMethod("NameHasOwner")
.onInterface(kDbusInterface)
.withArguments(std::string{name})
.storeResultsTo(hasOwner);
return hasOwner;
}
void SystemBus::processPendingEvents() {
while (m_connection->processPendingEvent()) {
}
+2
View File
@@ -2,6 +2,7 @@
#include <memory>
#include <sdbus-c++/sdbus-c++.h>
#include <string_view>
class SystemBus {
public:
@@ -9,6 +10,7 @@ public:
[[nodiscard]] sdbus::IConnection& connection() noexcept { return *m_connection; }
[[nodiscard]] sdbus::IConnection::PollData getPollData() const;
[[nodiscard]] bool nameHasOwner(std::string_view name) const;
void processPendingEvents();
private:
+227 -8
View File
@@ -6,11 +6,17 @@
#include "ipc/ipc_client.h"
#include "theme/cli.h"
#include <array>
#include <cerrno>
#include <clocale>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fcntl.h>
#include <stdexcept>
#include <string>
#include <unistd.h>
#ifdef __GLIBC__
#if __has_include(<jemalloc/jemalloc.h>)
@@ -23,18 +29,112 @@
namespace {
enum class SpawnResult { Parent, Error };
constexpr const char* kDaemonPipeEnv = "NOCTALIA_DAEMON_PIPE_FD";
int g_daemonPipe = -1;
void closeFd(int& fd) {
if (fd == -1) {
return;
}
(void)::close(fd);
fd = -1;
}
bool writeAll(int fd, const void* data, std::size_t size) {
const char* bytes = static_cast<const char*>(data);
while (size > 0) {
const ssize_t written = ::write(fd, bytes, size);
if (written < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (written == 0) {
return false;
}
bytes += written;
size -= static_cast<std::size_t>(written);
}
return true;
}
bool readAll(int fd, void* data, std::size_t size) {
char* bytes = static_cast<char*>(data);
while (size > 0) {
const ssize_t received = ::read(fd, bytes, size);
if (received < 0) {
if (errno == EINTR) {
continue;
}
return false;
}
if (received == 0) {
return false;
}
bytes += received;
size -= static_cast<std::size_t>(received);
}
return true;
}
bool redirectStdioToNull() {
int fd = ::open("/dev/null", O_RDWR);
if (fd == -1) {
std::perror("open(\"/dev/null\")");
return false;
}
bool ok = true;
if (::dup2(fd, STDIN_FILENO) == -1) {
std::perror("dup2(stdin)");
ok = false;
}
if (::dup2(fd, STDOUT_FILENO) == -1) {
std::perror("dup2(stdout)");
ok = false;
}
if (::dup2(fd, STDERR_FILENO) == -1) {
std::perror("dup2(stderr)");
ok = false;
}
if (fd > STDERR_FILENO) {
(void)::close(fd);
}
return ok;
}
void completeDaemonStartup(int code) {
if (g_daemonPipe == -1) {
return;
}
const int result = code;
if (!writeAll(g_daemonPipe, &result, sizeof(result))) {
std::fprintf(stderr, "error: failed to notify daemon parent: %s\n", std::strerror(errno));
}
closeFd(g_daemonPipe);
if (code == 0 && !redirectStdioToNull()) {
std::fprintf(stderr, "error: failed to redirect daemon stdio\n");
}
}
int runTopLevelFlag(const char* flag) {
if (std::strcmp(flag, "--version") == 0) {
if (std::strcmp(flag, "--version") == 0 || std::strcmp(flag, "-v") == 0) {
const std::string version = noctalia::build_info::displayVersion();
std::printf("noctalia %s\n", version.c_str());
return 0;
}
if (std::strcmp(flag, "--help") == 0) {
if (std::strcmp(flag, "--help") == 0 || std::strcmp(flag, "-h") == 0) {
std::puts("Usage: noctalia [OPTIONS]\n"
"\n"
"Options:\n"
" --help Show this help message\n"
" --version Show version information\n"
" -h, --help Show this help message\n"
" -v, --version Show version information\n"
" -d, --daemon Run in background\n"
"\n"
"Subcommands:\n"
" msg <command> Send a command to the running instance\n"
@@ -51,16 +151,90 @@ namespace {
return -1;
}
bool takeDaemonPipeFromEnv() {
const char* value = std::getenv(kDaemonPipeEnv);
if (value == nullptr || value[0] == '\0') {
return false;
}
errno = 0;
char* end = nullptr;
const long fd = std::strtol(value, &end, 10);
(void)::unsetenv(kDaemonPipeEnv);
if (errno != 0 || end == value || *end != '\0' || fd < 0) {
std::fprintf(stderr, "error: invalid %s value: %s\n", kDaemonPipeEnv, value);
return false;
}
g_daemonPipe = static_cast<int>(fd);
return true;
}
SpawnResult daemonize(pid_t* outPid, int* parentPipe, char* const argv[]) {
auto pipeFds = std::array<int, 2>{-1, -1};
if (::pipe(pipeFds.data()) == -1) {
std::perror("pipe");
return SpawnResult::Error;
}
pid_t pid = ::fork();
if (pid < 0) {
std::perror("fork");
closeFd(pipeFds[0]);
closeFd(pipeFds[1]);
return SpawnResult::Error;
}
if (pid > 0) {
if (outPid)
*outPid = pid;
if (parentPipe)
*parentPipe = pipeFds[0];
closeFd(pipeFds[1]);
return SpawnResult::Parent;
}
closeFd(pipeFds[0]);
// Match v4's early daemon boundary, but exec before shell startup so GLib,
// D-Bus, and polkit start from a normal process image rather than a raw
// post-fork child.
if (::setsid() == -1) {
std::perror("setsid");
const int daemonResult = 1;
(void)writeAll(pipeFds[1], &daemonResult, sizeof(daemonResult));
closeFd(pipeFds[1]);
_exit(1);
}
const std::string pipeFd = std::to_string(pipeFds[1]);
if (::setenv(kDaemonPipeEnv, pipeFd.c_str(), 1) == -1) {
std::perror("setenv");
const int daemonResult = 1;
(void)writeAll(pipeFds[1], &daemonResult, sizeof(daemonResult));
closeFd(pipeFds[1]);
_exit(1);
}
::execvp(argv[0], argv);
std::perror("execvp");
const int daemonResult = 1;
(void)writeAll(pipeFds[1], &daemonResult, sizeof(daemonResult));
closeFd(pipeFds[1]);
_exit(1);
}
int runShell() {
if (IpcClient::isRunning()) {
std::fputs("error: noctalia is already running\n", stderr);
completeDaemonStartup(1);
return 1;
}
try {
Application app;
app.run();
app.run([]() { completeDaemonStartup(0); });
} catch (const std::exception& e) {
logError("fatal: {}", e.what());
completeDaemonStartup(1);
return 1;
}
return 0;
@@ -79,6 +253,23 @@ int main(int argc, char* argv[]) {
#endif
std::setlocale(LC_ALL, "");
const bool isDaemonChild = takeDaemonPipeFromEnv();
bool shouldDaemonize = false;
for (int i = 1; i < argc; ++i) {
if (std::strcmp(argv[i], "--daemon") == 0 || std::strcmp(argv[i], "--daemonize") == 0 ||
std::strcmp(argv[i], "-d") == 0) {
shouldDaemonize = true;
for (int j = i; j < argc - 1; ++j) {
argv[j] = argv[j + 1];
}
--argc;
argv[argc] = nullptr;
break;
}
}
if (argc >= 2) {
if (std::strcmp(argv[1], "theme") == 0)
return noctalia::theme::runCli(argc, argv);
@@ -89,9 +280,37 @@ int main(int argc, char* argv[]) {
}
for (int i = 1; i < argc; ++i) {
const int rc = runTopLevelFlag(argv[i]);
if (rc >= 0)
return rc;
if (argv[i][0] == '-') {
const int rc = runTopLevelFlag(argv[i]);
if (rc >= 0)
return rc;
std::fprintf(stderr, "error: unknown option: %s\n", argv[i]);
return 1;
}
}
if (shouldDaemonize && !isDaemonChild) {
pid_t pid = -1;
int parentPipe = -1;
SpawnResult result = daemonize(&pid, &parentPipe, argv);
if (result == SpawnResult::Error) {
return 1;
}
if (result == SpawnResult::Parent) {
int daemonResult = 1;
const bool receivedResult = readAll(parentPipe, &daemonResult, sizeof(daemonResult));
closeFd(parentPipe);
if (!receivedResult) {
std::fputs("error: failed to wait for daemon startup\n", stderr);
return 1;
}
if (daemonResult == 0) {
std::printf("noctalia started [pid: %d]\n", pid);
}
return daemonResult;
}
}
return runShell();