users: add a systemd backend for managing services

This commit is contained in:
2024-09-25 15:39:47 +00:00
parent 3bbec161bf
commit edb665abd0
6 changed files with 172 additions and 11 deletions

View File

@@ -43,7 +43,7 @@ in
# then we can create our own. not sure if there's a dependency ordering issue here: lots
# of things depend on dbus but i don't do anything special to guarantee this is initialized
# before them.
services.dbus = {
services.dbus-user = {
description = "dbus user session";
partOf = lib.mkIf cfg.config.autostart [ "default" ];
command = pkgs.writeShellScript "dbus-start" ''

View File

@@ -43,7 +43,8 @@
services.ols = {
description = "ols: Offline Location Service";
command = "ols 2>&1"; # XXX: it logs to stderr, and my s6 infrastructure apparently doesn't handle that
command = "ols";
# command = "ols 2>&1"; # XXX: it logs to stderr, and my s6 infrastructure apparently doesn't handle that
partOf = [ "graphical-session" ];
};
};

View File

@@ -255,7 +255,7 @@ in
# N.B.: gtk apps support absolute paths for this; webkit apps (e.g. geary) support only relative paths (relative to $XDG_RUNTIME_DIR)
env.WAYLAND_DISPLAY = "wl/wayland-1";
services.private-storage.dependencyOf = [ "sway" ]; #< HACK: prevent unl0kr and sway from fighting over the tty
# services.private-storage.dependencyOf = [ "sway" ]; #< HACK: prevent unl0kr and sway from fighting over the tty
services.sway = {
description = "sway: tiling wayland desktop environment";
partOf = [
@@ -263,6 +263,7 @@ in
] ++ lib.optionals enableXWayland [
"x11"
];
depends = [ "private-storage" ]; #< HACK: prevent unl0kr and sway from fighting over the tty. TODO: introduce some earlier `graphical-session-ready` target?
# partOf = lib.mkMerge [
# "wayland"
# (lib.mkIf enableXWayland "x11")

View File

@@ -250,8 +250,8 @@ let
depends = svcCfg.depends
++ lib.optionals (((config.persist.byStore or {}).private or []) != []) [
"private-storage"
] ++ lib.optionals (svcName != "dbus" && builtins.elem "user" config.sandbox.whitelistDbus && cfg.dbus.enabled) [
"dbus"
] ++ lib.optionals (svcName != "dbus-user" && builtins.elem "user" config.sandbox.whitelistDbus && cfg.dbus.enabled) [
"dbus-user"
] ++ lib.optionals ((!builtins.elem "wayland" svcCfg.partOf) && config.sandbox.whitelistWayland) [
"wayland"
] ++ lib.optionals ((!builtins.elem "x11" svcCfg.partOf) && config.sandbox.whitelistX) [

View File

@@ -7,7 +7,24 @@ let
serviceType = with lib; types.submodule ({ config, ... }: {
options = {
description = mkOption {
type = types.str;
# XXX: has to be defaulted so consumers can set specific attributes of a service which was defined from a different nix module.
# but swallow the default, because we want to still enforce that it's set *somewhere*.
# type = types.str // {
# merge = loc: defs: types.str.merge
# loc
# (builtins.filter (v: v != "") defs)
# ;
# };
type = lib.mkOptionType {
merge = loc: defs: types.str.merge
loc
(if builtins.length defs == 1 then defs
else builtins.filter (def: def.value != "") defs
)
;
name = "defaultable str";
};
default = "";
};
documentation = mkOption {
type = types.listOf types.str;
@@ -154,7 +171,7 @@ let
'';
};
serviceManager = mkOption {
type = types.nullOr (types.enum [ "s6" ]);
type = types.nullOr (types.enum [ "s6" "systemd" ]);
default = "s6";
description = ''
which service manager to plumb `services` into.
@@ -217,10 +234,15 @@ let
# some of my program-specific environment variables depend on some of these being set,
# hence do that early:
# TODO: consider moving XDG_RUNTIME_DIR to $HOME/.run
fs.".config/environment.d/10-sane-baseline.conf".symlink.text = ''
HOME=/home/${name}
XDG_RUNTIME_DIR=/run/user/${name}
'';
environment.HOME = "/home/${name}";
environment.XDG_RUNTIME_DIR = "/run/user/${name}";
# XDG_DATA_DIRS gets set by shell init somewhere, but needs to be explicitly set here so it's available to services too.
environment.XDG_DATA_DIRS = "/etc/profiles/per-user/${name}/share:/run/current-system/sw/share";
# fs.".config/environment.d/10-sane-baseline.conf".symlink.text = ''
# HOME=/home/${name}
# XDG_RUNTIME_DIR=/run/user/${name}
# '';
fs.".config/environment.d/20-sane-nixos-users.conf".symlink.text =
let
env = lib.mapAttrsToList
@@ -369,6 +391,7 @@ in
{
imports = [
./s6-rc.nix
./systemd.nix
];
options = with lib; {

136
modules/users/systemd.nix Normal file
View File

@@ -0,0 +1,136 @@
{ 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
restartCondition,
...
}: lib.mkMerge [
{
# serviceConfig.BoundBy = toUnitNames partOf;
# serviceConfig.Restart = restartCondition; #< not allowed for `oneshot` types
serviceConfig.User = userName;
serviceConfig.Group = "users";
serviceConfig.WorkingDirectory = "~";
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;
serviceConfig.ExecStart = command;
})
(lib.mkIf (command != null && readiness.waitCommand != null) {
serviceConfig.Type = "notify";
serviceConfig.Restart = restartCondition;
# 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
}
readinessPollLoop &
${command}
exit $?
'';
})
(lib.mkIf (startCommand != null) {
serviceConfig.Type = "oneshot";
serviceConfig.ExecStart = startCommand;
})
(lib.mkIf (cleanupCommand != null) {
serviceConfig.ExecStopPost = cleanupCommand;
})
];
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);
}