Compare commits

...
Author SHA1 Message Date
Raito Bezarius 24df5d98f9 lix/libstore/linux: rename cgroup with drvHash
Change-Id: I238d0568a3e4b1ff3057781c0639528d666b4d37
Signed-off-by: Raito Bezarius <raito@lix.systems>
2025-12-12 01:30:44 +01:00
eldritch horrors 286f540ce8 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
2025-12-06 19:44:28 +01:00
eldritch horrors acb85fb910 libcmd: add raw arg access to legacy commands
we'll need this to modify argv for socket-activated daemons. this is our
replacement for the old savedArgv mechanism that was unscoped and fucky.

Change-Id: Ie048eb8ea99f1c9cd627a051292c836c83197068
2025-12-06 19:44:28 +01:00
eldritch horrors 775832d7f0 libcmd: remove unused savedArgv
this was only used in the pre-exec daemon days.

Change-Id: I3bbb113f9940e6980f01af60e6614a9656b0fd03
2025-12-06 19:44:28 +01:00
eldritch horrors 6cec6929b2 nix/daemon: remove settings copy from parent
the parent daemon does not change any settings before starting a child,
so there's nothing we may want to change that is not already set by the
config file. this also doesn't prevent changed of the config file being
applied to daemons where we do not expect it since it'll only restore a
setting to the parents' value if the child also has an override for it.

Change-Id: Ic5a9ef13458c103ec9979cb187ba8d3ce5e1e719
2025-12-06 19:44:28 +01:00
12 changed files with 108 additions and 58 deletions
+15
View File
@@ -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.
-2
View File
@@ -15,8 +15,6 @@ namespace nix {
extern std::string programPath;
extern char * * savedArgv;
class EvalState;
struct Pos;
class Store;
+16 -3
View File
@@ -9,14 +9,27 @@
namespace nix {
typedef std::function<int(AsyncIoRoot &, std::string, std::list<std::string>)> MainFunction;
struct LegacyCommandRegistry
{
using LegacyCommandMap = std::map<std::string, MainFunction>;
typedef std::function<int(AsyncIoRoot &, std::string, std::list<std::string>)> MainFunction;
typedef std::function<
int(AsyncIoRoot &, std::string, std::list<std::string>, std::span<char *>)>
RawMainFunction;
using LegacyCommandMap = std::map<std::string, RawMainFunction>;
static LegacyCommandMap * commands;
static void add(const std::string & name, MainFunction fun)
{
addWithRaw(
name,
[fun](AsyncIoRoot & aio, std::string name, std::list<std::string> args, std::span<char *>) {
return fun(aio, name, args);
}
);
}
static void addWithRaw(const std::string & name, RawMainFunction fun)
{
if (!commands) commands = new LegacyCommandMap;
(*commands)[name] = fun;
-2
View File
@@ -33,8 +33,6 @@
namespace nix {
char * * savedArgv;
static bool gcWarning = true;
void printGCWarning()
+1 -1
View File
@@ -858,7 +858,7 @@ void LinuxLocalDerivationGoal::prepareSandbox()
if (buildUser && (buildUser->getUIDCount() != 1 || settings.useCgroups)) {
context.cgroup.emplace(
settings.nixStateDir + "/cgroups",
fmt("nix-build-uid-%d", buildUser->getUID()),
fmt("nix-build-drvHash-%s", drvPath.hashPart()),
buildUser->getUID(),
buildUser->getGID()
);
+48 -41
View File
@@ -55,7 +55,6 @@
#endif
static constexpr int SUBDAEMON_CONNECTION_FD = 0;
static constexpr int SUBDAEMON_SETTINGS_FD = 3;
namespace nix {
@@ -346,9 +345,6 @@ try {
peer.pidKnown ? fmt("pid %1%", peer.pid) : "unknown peer"
);
Pipe settings;
settings.create();
// Fork a child to handle the connection. make sure it's called with
// argv0 `nix-daemon` so we don't try to run `nix --for` when called
// from more modern scripts that assume nix-command being available.
@@ -363,11 +359,7 @@ try {
fmt("%1%", int(verbosity)),
},
.dieWithParent = false,
.redirections =
{
{.dup = SUBDAEMON_CONNECTION_FD, .from = remote.get()},
{.dup = SUBDAEMON_SETTINGS_FD, .from = settings.readSide.get()},
}
.redirections = {{.dup = SUBDAEMON_CONNECTION_FD, .from = remote.get()}}
};
if (forceTrustClientOpt) {
options.args.push_back(
@@ -375,15 +367,6 @@ try {
);
}
runProgram2(options).release();
FdSink sink(settings.writeSide.get());
std::map<std::string, Config::SettingInfo> overriddenSettings;
globalConfig.getSettings(overriddenSettings, true);
for (auto & setting : overriddenSettings) {
sink << 1 << setting.first << setting.second.value;
}
sink << 0;
sink.flush();
} catch (Error & error) {
auto ei = error.info();
// FIXME: add to trace?
@@ -397,12 +380,37 @@ try {
co_return result::current_exception();
}
static void daemonInstance(AsyncIoRoot & aio, std::optional<TrustedFlag> forceTrustClientOpt)
static void
daemonInstance(AsyncIoRoot & aio, std::optional<TrustedFlag> forceTrustClientOpt, char * peerPidArg)
{
PeerInfo peer = getPeerInfo(SUBDAEMON_CONNECTION_FD);
// Handle socket-based activation by systemd.
const auto [launchedByManager, connectionFd] = []() -> std::pair<bool, int> {
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 {
@@ -417,23 +425,8 @@ static void daemonInstance(AsyncIoRoot & aio, std::optional<TrustedFlag> forceTr
forceTrustClientOpt ? " by override" : ""
);
{
FdSource source(SUBDAEMON_SETTINGS_FD);
/* Read the parent's settings. */
while (readNum<unsigned>(source)) {
auto name = readString(source);
auto value = readString(source);
settings.set(name, value);
}
if (close(SUBDAEMON_SETTINGS_FD) < 0) {
throw SysError("preparing subdaemon connection");
}
}
// Background the daemon.
if (setsid() == -1) {
if (!launchedByManager && setsid() == -1) {
throw SysError("creating a new session");
}
@@ -446,8 +439,8 @@ static void daemonInstance(AsyncIoRoot & aio, std::optional<TrustedFlag> 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);
}
@@ -525,12 +518,14 @@ runDaemon(AsyncIoRoot & aio, bool stdio, std::optional<TrustedFlag> 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<char *> rawArgv)
{
{
auto stdio = false;
std::optional<TrustedFlag> isTrustedOpt = std::nullopt;
bool isInstance = false;
char * peerPidArg = nullptr;
Verbosity subdaemonLogLevel = lvlInfo;
LegacyArgs(aio, programName, [&](Strings::iterator & arg, const Strings::iterator & end) {
@@ -554,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<int>(getArg(*arg, arg, end)); level) {
subdaemonLogLevel = static_cast<Verbosity>(std::min<int>(lvlVomit, *level));
@@ -568,7 +575,7 @@ static int main_nix_daemon(AsyncIoRoot & aio, std::string programName, Strings a
if (isInstance) {
verbosity = Verbosity(std::min<uint64_t>(subdaemonLogLevel, lvlVomit));
daemonInstance(aio, isTrustedOpt);
daemonInstance(aio, isTrustedOpt, peerPidArg);
} else {
runDaemon(aio, stdio, isTrustedOpt);
}
@@ -578,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
+6 -4
View File
@@ -455,8 +455,6 @@ void registerNixHelp()
int mainWrapped(AsyncIoRoot & aio, int argc, char ** argv)
{
savedArgv = argv;
/* The chroot helper needs to be run before any threads have been
started. */
if (argc > 0 && argv[0] == chrootHelperName) {
@@ -493,7 +491,12 @@ int mainWrapped(AsyncIoRoot & aio, int argc, char ** argv)
registerLegacyCommands();
auto legacy = (*LegacyCommandRegistry::commands)[programName];
if (legacy) {
return legacy(aio, std::string(baseNameOf(argv[0])), Strings(argv + 1, argv + argc));
return legacy(
aio,
std::string(baseNameOf(argv[0])),
Strings(argv + 1, argv + argc),
{argv + 1, argv + argc}
);
}
}
@@ -645,7 +648,6 @@ int mainWrapped(AsyncIoRoot & aio, int argc, char ** argv)
return 0;
}
}
int main(int argc, char * * argv)
+1 -1
View File
@@ -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,
+1 -3
View File
@@ -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
+2
View File
@@ -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
+15
View File
@@ -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
+3 -1
View File
@@ -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;