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;