Files
nix-files/modules/users/systemd.nix

158 lines
5.6 KiB
Nix

{ config, lib, pkgs, sane-lib, ... }:
let
toUnitName = s:
# TODO: i could query `services."${s}".command` to figure out here if it's
# a target or a service, rather than hardcode it.
if lib.elem s [ "default" "graphical-session" "gps" "private-storage" "sound" "wayland" "x11" ] then
"${s}.target"
else
"${s}.service"
;
toUnitNames = lib.map toUnitName;
mkSystemdUnit = {
description,
documentation,
depends,
dependencyOf,
partOf,
...
}: {
inherit description documentation;
wants = toUnitNames depends;
after = toUnitNames depends;
wantedBy = (toUnitNames dependencyOf) ++ (toUnitNames partOf);
before = (toUnitNames dependencyOf) ++ (toUnitNames partOf);
};
mkSystemdService = userName: userConfig: _serviceName: {
# description,
# documentation,
# depends,
# dependencyOf,
# partOf,
command,
cleanupCommand,
startCommand,
readiness, # readiness.waitCommand, readiness.waitDbus, readiness.waitExists
reapChildren,
restartCondition,
...
}: lib.mkMerge [
{
# serviceConfig.BoundBy = toUnitNames partOf;
# serviceConfig.Restart = restartCondition; #< not allowed for `oneshot` types
serviceConfig.KillMode = if reapChildren then
"control-group" # (default)
else
"process" # kill only the main process; let it leave behind children if it likes
;
serviceConfig.User = userName;
serviceConfig.Group = "users";
serviceConfig.WorkingDirectory = "~";
serviceConfig.X-RestartIfChanged = lib.mkDefault false; #< NixOS attribute, so we don't restart the DE on activation
serviceConfig.ExecSearchPath = [
"/etc/profiles/per-user/${userName}/bin"
"/run/current-system/sw/bin"
];
path = [
"/etc/profiles/per-user/${userName}"
"/run/current-system/sw"
];
# systemd doesn't allow substitutions, so perform one layer of substitution over the environment.
# especially, this lets me base environment variables off of XDG dirs (like XDG_RUNTIME_DIR).
environment = lib.mapAttrs
(k: v: lib.replaceStrings
(lib.mapAttrsToList (var: value: "$" + var) userConfig.environment)
(lib.mapAttrsToList (var: value: value) userConfig.environment)
v
)
userConfig.environment;
}
(lib.mkIf (command != null && readiness.waitCommand == null) {
serviceConfig.Type = "simple";
serviceConfig.Restart = restartCondition;
startLimitIntervalSec = 0; #< disable restart limit
serviceConfig.ExecStart = command;
})
(lib.mkIf (command != null && readiness.waitCommand != null) {
serviceConfig.Type = "notify";
serviceConfig.Restart = restartCondition;
startLimitIntervalSec = 0; #< disable restart limit
# serviceConfig.NotifyAccess = "exec"; #< allow anything in Exec* to invoke systemd-notify
serviceConfig.NotifyAccess = "all"; #< allow anything in Exec* to invoke systemd-notify
script = ''
isReady() {
echo "checking readiness..."
${readiness.waitCommand}
}
readinessPollLoop() {
while ! isReady; do
echo "service is not ready: sleeping 1s"
${lib.getExe' pkgs.coreutils "sleep"} 1
done
echo "ready: notifying systemd"
${lib.getExe' pkgs.systemd "systemd-notify"} --ready
}
cleanup() {
# kill all direct children
# this is distinct from killing the whole process group,
# which i explicitly *don't* do, to allow spawned processes to outlive the service
# (if reapChildren = false; else systemd implicitly reaps all the children)
${lib.getExe' pkgs.procps "pkill"} -P $$
}
trap cleanup EXIT
readinessPollLoop &
${command}
exit $?
'';
})
(lib.mkIf (startCommand != null) {
serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true;
serviceConfig.ExecStart = startCommand;
})
(lib.mkIf (cleanupCommand != null) {
serviceConfig.ExecStopPost = cleanupCommand;
serviceConfig.TimeoutStopSec = "30s"; #< system-wide default is closer to 10s, but eg25-control-powered takes longer than that
})
];
mkSystemd = enable: userName: userConfig: unitName: unitConfig: let
unit = mkSystemdUnit unitConfig;
service = mkSystemdService userName userConfig unitName unitConfig;
isService = unitConfig.command != null || unitConfig.startCommand != null;
# isService = (service.serviceConfig.Type or null) != null;
in {
# XXX: can't use `systemd.units ... = unit` because it doesn't expose `after`
systemd.services = lib.optionalAttrs (enable && isService) {
"${unitName}" = lib.mkMerge [ unit service ];
};
# systemd complains if you manually define `default.target`, so omit that particular one
systemd.targets = lib.optionalAttrs (enable && !isService && unitName != "default") {
"${unitName}" = unit;
};
};
configsForUser = userName: userConfig: let
globalEn = userConfig.default && userConfig.serviceManager == "systemd";
in
lib.mapAttrsToList (mkSystemd globalEn userName userConfig) userConfig.services
;
in
{
config = let
configs = lib.flatten (lib.mapAttrsToList configsForUser config.sane.users);
take = f: {
systemd.services = f.systemd.services;
systemd.targets = f.systemd.targets;
};
in lib.mkMerge [
(take (sane-lib.mkTypedMerge take configs))
];
# systemd.services = lib.mkMerge (lib.mapAttrsToList servicesForUser config.sane.users);
}