users: add a systemd
backend for managing services
This commit is contained in:
@@ -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" ''
|
||||
|
@@ -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" ];
|
||||
};
|
||||
};
|
||||
|
@@ -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")
|
||||
|
@@ -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) [
|
||||
|
@@ -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
136
modules/users/systemd.nix
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user