From 286f540ce828b97e8fdf6684c019aba6c59bdc33 Mon Sep 17 00:00:00 2001 From: eldritch horrors Date: Sat, 6 Dec 2025 19:44:28 +0100 Subject: [PATCH] nix/daemon: socket-activate single connections the cgroups experimental feature does not work properly without this because we do not stop subdaemons when the main daemon is shut down. systemd needs the assigned cgroups to be empty to restart the daemon and thus cannot cleanly restart the daemon if any connections exist. starting a fresh unit for each connection creates a new cgroup every time instead of sharing any delegations and thus solves the problem. fixes #1030 Change-Id: Id6c458aad30eaa08c3609ac8280a7dde8e8f3cf9 --- doc/manual/rl-next/socket-activation.md | 15 +++++++ lix/nix/daemon.cc | 55 +++++++++++++++++++++---- misc/systemd/meson.build | 2 +- misc/systemd/nix-daemon.service.in | 4 +- misc/systemd/nix-daemon.socket.in | 2 + misc/systemd/nix-daemon@.service.in | 15 +++++++ tests/nixos/default.nix | 4 +- 7 files changed, 84 insertions(+), 13 deletions(-) create mode 100644 doc/manual/rl-next/socket-activation.md create mode 100644 misc/systemd/nix-daemon@.service.in diff --git a/doc/manual/rl-next/socket-activation.md b/doc/manual/rl-next/socket-activation.md new file mode 100644 index 000000000..b448b5b2c --- /dev/null +++ b/doc/manual/rl-next/socket-activation.md @@ -0,0 +1,15 @@ +--- +synopsis: "Lix daemons are now fully socket-activated on systemd setups" +cls: [] +issues: [1030] +category: "Miscellany" +credits: [horrors] +--- + +When launched by systemd, Lix no longer uses a persistent daemon process and uses systemd socket +activation instead. This is necessary to support the `cgroups` and `auto-allocate-uids` features +and may improve observability of daemon behavior with common systemd-based monitoring solutions. + +The old behavior with a single persistent daemon is still available, but disabled by default. It +is not possible to enable both a persistent daemon and socket activation, starting one stops the +other automatically. Existing installations should not require any changes when they're updated. diff --git a/lix/nix/daemon.cc b/lix/nix/daemon.cc index 326cc90f9..c2d9db376 100644 --- a/lix/nix/daemon.cc +++ b/lix/nix/daemon.cc @@ -380,12 +380,37 @@ try { co_return result::current_exception(); } -static void daemonInstance(AsyncIoRoot & aio, std::optional forceTrustClientOpt) +static void +daemonInstance(AsyncIoRoot & aio, std::optional forceTrustClientOpt, char * peerPidArg) { - PeerInfo peer = getPeerInfo(SUBDAEMON_CONNECTION_FD); + // Handle socket-based activation by systemd. + const auto [launchedByManager, connectionFd] = []() -> std::pair { + auto listenFds = getEnv("LISTEN_FDS"); + if (listenFds) { + if (getEnv("LISTEN_PID") != std::to_string(getpid()) || listenFds != "1") { + throw Error("unexpected systemd environment variables"); + } + closeOnExec(SD_LISTEN_FDS_START); + return {true, SD_LISTEN_FDS_START}; + } else { + return {false, SUBDAEMON_CONNECTION_FD}; + } + }(); + + PeerInfo peer = getPeerInfo(connectionFd); TrustedFlag trusted; std::string user; + // replace peerPidArg contents with the peer pid if possible. the forking daemon does + // this as a debugging aid and it is easy enough to do it here also, so we just do it + if (peerPidArg && peer.pidKnown) { + auto pidForArgv = std::to_string(peer.pid); + if (pidForArgv.size() <= strlen(peerPidArg)) { + memset(peerPidArg, ' ', strlen(peerPidArg)); + strcpy(peerPidArg, pidForArgv.c_str()); + } + } + if (forceTrustClientOpt) { trusted = *forceTrustClientOpt; } else { @@ -401,7 +426,7 @@ static void daemonInstance(AsyncIoRoot & aio, std::optional forceTr ); // Background the daemon. - if (setsid() == -1) { + if (!launchedByManager && setsid() == -1) { throw SysError("creating a new session"); } @@ -414,8 +439,8 @@ static void daemonInstance(AsyncIoRoot & aio, std::optional forceTr } // Handle the connection. - FdSource from(SUBDAEMON_CONNECTION_FD); - FdSink to(SUBDAEMON_CONNECTION_FD); + FdSource from(connectionFd); + FdSink to(connectionFd); processConnection(aio, store, from, to, trusted); } @@ -493,12 +518,14 @@ runDaemon(AsyncIoRoot & aio, bool stdio, std::optional forceTrustCl } } -static int main_nix_daemon(AsyncIoRoot & aio, std::string programName, Strings argv) +static int +main_nix_daemon(AsyncIoRoot & aio, std::string programName, Strings argv, std::span rawArgv) { { auto stdio = false; std::optional isTrustedOpt = std::nullopt; bool isInstance = false; + char * peerPidArg = nullptr; Verbosity subdaemonLogLevel = lvlInfo; LegacyArgs(aio, programName, [&](Strings::iterator & arg, const Strings::iterator & end) { @@ -522,6 +549,18 @@ static int main_nix_daemon(AsyncIoRoot & aio, std::string programName, Strings a } else if (*arg == "--for") { isInstance = true; getArg(*arg, arg, end); + } else if (*arg == "--for-socket-activation") { + isInstance = true; + // HACK: too many copies and rewrites happen by the time we get here to + // be able to calculate a rawArgv offset. instead we will search for an + // exact match and blindly assume that it's the one we want to rewrite. + for (auto [i, rawArg] : enumerate(rawArgv)) { + if (rawArg == *arg) { + peerPidArg = rawArg + strlen("--for-"); + peerPidArg[-1] = ' '; + break; + } + } } else if (*arg == "--log-level") { if (auto level = string2Int(getArg(*arg, arg, end)); level) { subdaemonLogLevel = static_cast(std::min(lvlVomit, *level)); @@ -536,7 +575,7 @@ static int main_nix_daemon(AsyncIoRoot & aio, std::string programName, Strings a if (isInstance) { verbosity = Verbosity(std::min(subdaemonLogLevel, lvlVomit)); - daemonInstance(aio, isTrustedOpt); + daemonInstance(aio, isTrustedOpt, peerPidArg); } else { runDaemon(aio, stdio, isTrustedOpt); } @@ -546,7 +585,7 @@ static int main_nix_daemon(AsyncIoRoot & aio, std::string programName, Strings a } void registerLegacyNixDaemon() { - LegacyCommandRegistry::add("nix-daemon", main_nix_daemon); + LegacyCommandRegistry::addWithRaw("nix-daemon", main_nix_daemon); } struct CmdDaemon : StoreCommand diff --git a/misc/systemd/meson.build b/misc/systemd/meson.build index 26e20af95..1ab3034ac 100644 --- a/misc/systemd/meson.build +++ b/misc/systemd/meson.build @@ -1,4 +1,4 @@ -foreach config : [ 'nix-daemon.socket', 'nix-daemon.service' ] +foreach config : [ 'nix-daemon.socket', 'nix-daemon.service', 'nix-daemon@.service' ] configure_file( input : config + '.in', output : config, diff --git a/misc/systemd/nix-daemon.service.in b/misc/systemd/nix-daemon.service.in index cf0cd7292..3a77918be 100644 --- a/misc/systemd/nix-daemon.service.in +++ b/misc/systemd/nix-daemon.service.in @@ -1,6 +1,7 @@ [Unit] Description=Nix Daemon Documentation=man:nix-daemon https://docs.lix.systems/manual/lix/stable +Conflicts=nix-daemon.socket RequiresMountsFor=@storedir@ RequiresMountsFor=@localstatedir@ RequiresMountsFor=@localstatedir@/nix/db @@ -14,6 +15,3 @@ LimitNOFILE=1048576 TasksMax=1048576 Delegate=yes DelegateSubgroup=supervisor - -[Install] -WantedBy=multi-user.target diff --git a/misc/systemd/nix-daemon.socket.in b/misc/systemd/nix-daemon.socket.in index 9ed39ffe6..d9b06fbd2 100644 --- a/misc/systemd/nix-daemon.socket.in +++ b/misc/systemd/nix-daemon.socket.in @@ -1,11 +1,13 @@ [Unit] Description=Nix Daemon Socket Before=multi-user.target +Conflicts=nix-daemon.service RequiresMountsFor=@storedir@ ConditionPathIsReadWrite=@localstatedir@/nix/daemon-socket [Socket] ListenStream=@localstatedir@/nix/daemon-socket/socket +Accept=yes [Install] WantedBy=sockets.target diff --git a/misc/systemd/nix-daemon@.service.in b/misc/systemd/nix-daemon@.service.in new file mode 100644 index 000000000..ae0924f74 --- /dev/null +++ b/misc/systemd/nix-daemon@.service.in @@ -0,0 +1,15 @@ +[Unit] +Description=Nix Daemon instance +Documentation=man:nix-daemon https://docs.lix.systems/manual/lix/stable +CollectMode=inactive-or-failed +RequiresMountsFor=@storedir@ +RequiresMountsFor=@localstatedir@ +RequiresMountsFor=@localstatedir@/nix/db + +[Service] +ExecStart=@@bindir@/nix-daemon nix-daemon --for-socket-activation +CacheDirectory=nix +LimitNOFILE=1048576 +TasksMax=1048576 +Delegate=yes +DelegateSubgroup=supervisor diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 30783e081..c2d6c2acd 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -9,11 +9,13 @@ let (nixos-lib.runTest { imports = [ test ]; hostPkgs = nixpkgsFor.${system}.native; - defaults = { + defaults = { config, ... }: { nixpkgs.pkgs = nixpkgsFor.${system}.native; nix.checkAllErrors = false; # nixos-option fails to build with lix and no tests use any of the tools system.disableInstallerTools = true; + # FIXME: remove this once the nixos module sets these overrides + systemd.services."nix-daemon@".path = config.systemd.services.nix-daemon.path; }; _module.args.nixpkgs = nixpkgs; _module.args.system = system;