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 b0359a6d3..d98b1fb7a 100644 --- a/lix/nix/daemon.cc +++ b/lix/nix/daemon.cc @@ -290,22 +290,8 @@ try { if (chdir("/") == -1) throw SysError("cannot change current directory"); - AutoCloseFD fdSocket; - - // Handle socket-based activation by systemd. - auto listenFds = getEnv("LISTEN_FDS"); - if (listenFds) { - if (getEnv("LISTEN_PID") != std::to_string(getpid()) || listenFds != "1") - throw Error("unexpected systemd environment variables"); - fdSocket = AutoCloseFD{SD_LISTEN_FDS_START}; - closeOnExec(fdSocket.get()); - } - - // Otherwise, create and bind to a Unix domain socket. - else { - createDirs(dirOf(settings.nixDaemonSocketFile)); - fdSocket = createUnixDomainSocket(settings.nixDaemonSocketFile, 0666); - } + createDirs(dirOf(settings.nixDaemonSocketFile)); + auto fdSocket = createUnixDomainSocket(settings.nixDaemonSocketFile, 0666); // Get rid of children automatically; don't let them become zombies. setSigChldAction(true); @@ -391,12 +377,42 @@ 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); + // these unsets are not critical, we never did this for accept=no sockets either + (void) sys::unsetenv("LISTEN_FDS"); + (void) sys::unsetenv("LISTEN_PID"); + (void) sys::unsetenv("LISTEN_FDNAMES"); + 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)); + peerPidArg[0] = '\0'; + memcpy(peerPidArg + 1, pidForArgv.c_str(), pidForArgv.size()); + } + } + if (forceTrustClientOpt) { trusted = *forceTrustClientOpt; } else { @@ -412,21 +428,18 @@ static void daemonInstance(AsyncIoRoot & aio, std::optional forceTr ); // Background the daemon. - if (setsid() == -1) { + if (!launchedByManager && setsid() == -1) { throw SysError("creating a new session"); } - // Restore normal handling of SIGCHLD. - setSigChldAction(false); - auto store = aio.blockOn(openUncachedStore(AllowDaemon::Disallow)); if (auto local = dynamic_cast(&*store); local && peer.uidKnown && peer.gidKnown) { local->associateWithCredentials(peer.uid, peer.gid); } // 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); } @@ -504,12 +517,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) { @@ -533,6 +548,17 @@ 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"); + break; + } + } } else if (*arg == "--log-level") { if (auto level = string2Int(getArg(*arg, arg, end)); level) { subdaemonLogLevel = static_cast(std::min(lvlVomit, *level)); @@ -547,7 +573,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); } @@ -557,7 +583,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/cgroups/default.nix b/tests/nixos/cgroups/default.nix index ae94f9ac6..0816bc4f3 100644 --- a/tests/nixos/cgroups/default.nix +++ b/tests/nixos/cgroups/default.nix @@ -29,7 +29,7 @@ # Start build in background host.execute("nix build --use-cgroups --auto-allocate-uids --file ${./hang.nix} >&2 &") pid = int(host.succeed("pgrep nix")) - service = "/sys/fs/cgroup/system.slice/nix-daemon.service" + service = "/sys/fs/cgroup/system.slice/system-nix\\\\x2ddaemon.slice/nix-daemon@*.service" # Wait for cgroups to be created host.succeed(f"until [ -e {service}/supervisor ]; do sleep 1; done", timeout=30) diff --git a/tests/nixos/default.nix b/tests/nixos/default.nix index 01acd260b..34ebf7030 100644 --- a/tests/nixos/default.nix +++ b/tests/nixos/default.nix @@ -15,11 +15,21 @@ 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@" = + let prev = config.systemd.services.nix-daemon; + in + { + path = prev.path; + environment = lib.filterAttrs (n: v: n != "PATH") prev.environment; + serviceConfig = prev.serviceConfig; + unitConfig = prev.unitConfig; + }; }; _module.args.nixpkgs = nixpkgs; _module.args.system = system;