diff --git a/src/app/application.cpp b/src/app/application.cpp index a4dfccc01..24e0d2ce9 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -44,6 +44,7 @@ #include #include #include +#include std::atomic 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 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(*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 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(m_wayland, m_bar, [this]() { return currentPollSources(); }); + if (startupReadyCallback) { + startupReadyCallback(); + } m_mainLoop->run(); kLog.info("shutdown"); } diff --git a/src/app/application.h b/src/app/application.h index d36847b94..261b23a54 100644 --- a/src/app/application.h +++ b/src/app/application.h @@ -91,6 +91,7 @@ #include "wayland/workspace_poll_source.h" #include +#include #include #include @@ -99,7 +100,7 @@ public: Application(); ~Application(); - void run(); + void run(std::function startupReadyCallback = {}); // Public for signal handler static std::atomic s_shutdownRequested; diff --git a/src/dbus/polkit/polkit_agent.cpp b/src/dbus/polkit/polkit_agent.cpp index cfae9c7df..23709133c 100644 --- a/src/dbus/polkit/polkit_agent.cpp +++ b/src/dbus/polkit/polkit_agent.cpp @@ -11,11 +11,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -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 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, ®isterError); + g_object_unref(subject); + + std::string message; + if (registerError != nullptr) { + message = registerError->message; + g_clear_error(®isterError); + } 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& 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 { diff --git a/src/dbus/system_bus.cpp b/src/dbus/system_bus.cpp index 484ebf8d8..b7e1b4ead 100644 --- a/src/dbus/system_bus.cpp +++ b/src/dbus/system_bus.cpp @@ -1,9 +1,27 @@ #include "dbus/system_bus.h" +#include + +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()) { } diff --git a/src/dbus/system_bus.h b/src/dbus/system_bus.h index bb256f4cf..3579efcb7 100644 --- a/src/dbus/system_bus.h +++ b/src/dbus/system_bus.h @@ -2,6 +2,7 @@ #include #include +#include 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: diff --git a/src/main.cpp b/src/main.cpp index 9bfc5c3c6..47e7afa5e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,11 +6,17 @@ #include "ipc/ipc_client.h" #include "theme/cli.h" +#include +#include #include +#include #include +#include #include +#include #include #include +#include #ifdef __GLIBC__ #if __has_include() @@ -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(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(written); + } + return true; + } + + bool readAll(int fd, void* data, std::size_t size) { + char* bytes = static_cast(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(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 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(fd); + return true; + } + + SpawnResult daemonize(pid_t* outPid, int* parentPipe, char* const argv[]) { + auto pipeFds = std::array{-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();