2024-03-17 05:40:31 +00:00
|
|
|
{ lib, pkgs, ... }:
|
|
|
|
let
|
2024-03-23 21:33:08 +00:00
|
|
|
logBase = "$HOME/.local/share/s6/logs";
|
2024-03-21 13:10:42 +00:00
|
|
|
maybe = cond: value: if cond then value else null;
|
2024-03-17 05:40:31 +00:00
|
|
|
|
2024-03-21 13:10:42 +00:00
|
|
|
# create a derivation whose output is the on-disk representation of some attrset.
|
|
|
|
# @path: /foo/bar/...
|
|
|
|
# @obj: one of:
|
|
|
|
# - { file = { text = "foo bar"; executable = true|false; } }
|
|
|
|
# - { dir = <obj>; }
|
|
|
|
fsItemToDerivation = path: obj: if obj ? dir then
|
|
|
|
pkgs.symlinkJoin {
|
|
|
|
name = "s6-${path}";
|
|
|
|
paths = lib.mapAttrsToList
|
|
|
|
(n: v: fsItemToDerivation "${path}/${n}" v)
|
|
|
|
obj.dir
|
|
|
|
;
|
|
|
|
}
|
|
|
|
else if obj ? text then
|
|
|
|
if obj.text != null then
|
|
|
|
pkgs.writeTextFile {
|
|
|
|
name = "s6-${path}";
|
|
|
|
destination = path;
|
|
|
|
text = obj.text;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
pkgs.emptyDirectory
|
|
|
|
else if obj ? executable then
|
|
|
|
if obj.executable != null then
|
|
|
|
pkgs.writeTextFile {
|
|
|
|
name = "s6-${path}";
|
|
|
|
destination = path;
|
|
|
|
executable = true;
|
|
|
|
text = obj.executable;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
pkgs.emptyDirectory
|
|
|
|
else throw "don't know how to convert fs item at path ${path} to derivation: ${obj}";
|
|
|
|
|
|
|
|
# call with an AttrSet of fs items:
|
|
|
|
# example:
|
|
|
|
# ```
|
|
|
|
# fsToDerivation {
|
|
|
|
# usr.dir = {
|
|
|
|
# normal.text = "i'm /usr/normal";
|
|
|
|
# exec.executable = ''
|
|
|
|
# #!/bin/sh
|
|
|
|
# echo "i'm executable"
|
|
|
|
# '';
|
|
|
|
# lib.dir = { ... };
|
|
|
|
# };
|
|
|
|
# bin.dir = { ... };
|
|
|
|
# }
|
|
|
|
fsToDerivation = fs: fsItemToDerivation "/" { dir = fs; };
|
|
|
|
|
|
|
|
# infers the service type from the arguments and creates an attrset usable by `fsToDerivation`.
|
2024-03-21 13:29:58 +00:00
|
|
|
# also configures the service for logging, if applicable.
|
2024-03-21 16:37:57 +00:00
|
|
|
serviceToFs = { name, check, contents, depends, finish, run }: let
|
2024-03-21 12:06:23 +00:00
|
|
|
type = if run != null then "longrun" else "bundle";
|
2024-03-21 13:29:58 +00:00
|
|
|
logger = serviceToFs' {
|
2024-03-21 16:02:06 +00:00
|
|
|
name = "logger:${name}";
|
|
|
|
consumerFor = name;
|
|
|
|
run = ''exec s6-log -- T p'${name}:' "${logBase}/${name}"'';
|
2024-03-21 13:29:58 +00:00
|
|
|
type = "longrun";
|
|
|
|
};
|
|
|
|
in (serviceToFs' {
|
2024-03-21 16:37:57 +00:00
|
|
|
inherit name check depends finish run type;
|
2024-03-21 16:02:06 +00:00
|
|
|
contents = maybe (type == "bundle") contents;
|
|
|
|
producerFor = maybe (type == "longrun") "logger:${name}";
|
2024-03-21 13:29:58 +00:00
|
|
|
}) // (lib.optionalAttrs (type == "longrun") logger);
|
|
|
|
|
|
|
|
serviceToFs'= {
|
|
|
|
name,
|
|
|
|
type,
|
2024-03-21 16:37:57 +00:00
|
|
|
check ? null, #< command to poll to check for service readiness
|
2024-03-21 16:18:31 +00:00
|
|
|
consumerFor ? null,
|
|
|
|
contents ? null, #< bundle contents
|
2024-03-21 13:29:58 +00:00
|
|
|
depends ? [],
|
|
|
|
finish ? null,
|
|
|
|
producerFor ? null,
|
2024-03-21 16:18:31 +00:00
|
|
|
run ? null,
|
2024-03-21 16:37:57 +00:00
|
|
|
}: let
|
|
|
|
maybe-notify = lib.optionalString (check != null) "s6-notifyoncheck -n 0 ";
|
|
|
|
in {
|
2024-03-21 13:10:42 +00:00
|
|
|
"${name}".dir = {
|
|
|
|
"type".text = type;
|
2024-03-21 13:29:58 +00:00
|
|
|
"contents".text = maybe (contents != null) (
|
2024-03-21 16:02:06 +00:00
|
|
|
lib.concatStringsSep "\n" contents
|
2024-03-21 13:10:42 +00:00
|
|
|
);
|
2024-03-21 16:18:31 +00:00
|
|
|
"consumer-for".text = maybe (consumerFor != null) consumerFor;
|
2024-03-21 16:37:57 +00:00
|
|
|
"data".dir = {
|
|
|
|
"check".executable = maybe (check != null) ''
|
|
|
|
#!/bin/sh
|
|
|
|
exec ${check}
|
|
|
|
'';
|
|
|
|
};
|
2024-03-23 13:04:12 +00:00
|
|
|
# N.B.: if this service is a bundle, then dependencies.d is ignored
|
2024-03-21 13:29:58 +00:00
|
|
|
"dependencies.d".dir = lib.genAttrs
|
2024-03-21 16:02:06 +00:00
|
|
|
depends
|
2024-03-21 13:29:58 +00:00
|
|
|
(dep: { text = ""; })
|
|
|
|
;
|
2024-03-21 16:18:31 +00:00
|
|
|
"finish".executable = maybe (finish != null) ''
|
|
|
|
#!/bin/sh
|
|
|
|
exec ${finish}
|
|
|
|
'';
|
2024-03-21 16:37:57 +00:00
|
|
|
"notification-fd".text = maybe (check != null) "3";
|
2024-03-21 13:29:58 +00:00
|
|
|
"producer-for".text = maybe (producerFor != null) producerFor;
|
2024-03-21 16:18:31 +00:00
|
|
|
"run".executable = maybe (run != null) ''
|
|
|
|
#!/bin/sh
|
|
|
|
echo "starting: s6-${name}"
|
2024-03-21 16:37:57 +00:00
|
|
|
exec ${maybe-notify}${run} 2>&1
|
2024-03-21 16:18:31 +00:00
|
|
|
'';
|
2024-03-21 13:10:42 +00:00
|
|
|
};
|
2024-03-17 05:40:31 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
# create a directory, containing N subdirectories:
|
|
|
|
# - svc-a/
|
|
|
|
# - type
|
|
|
|
# - run
|
|
|
|
# - svc-b/
|
|
|
|
# - type
|
|
|
|
# - run
|
|
|
|
# - ...
|
2024-03-21 13:10:42 +00:00
|
|
|
genServices = svcs: fsToDerivation (lib.foldl'
|
|
|
|
(acc: srv: acc // (serviceToFs srv))
|
|
|
|
{}
|
|
|
|
svcs
|
|
|
|
);
|
2024-03-17 05:40:31 +00:00
|
|
|
|
|
|
|
# output is a directory containing:
|
|
|
|
# - db
|
|
|
|
# - lock
|
|
|
|
# - n
|
|
|
|
# - resolve.cdb
|
|
|
|
# - servicedirs/
|
|
|
|
# - svc-a/
|
|
|
|
# - svc-b/
|
|
|
|
# - ...
|
|
|
|
#
|
|
|
|
# this can then be used by s6-rc-init, like:
|
|
|
|
# s6-svscan scandir &
|
|
|
|
# s6-rc-init -c $compiled -l $PWD/live -d $PWD/scandir
|
|
|
|
# s6-rc -l $PWD/live start svc-a
|
2024-03-17 06:08:19 +00:00
|
|
|
#
|
|
|
|
# N.B.: it seems the $compiled dir needs to be rw, for s6 to write lock files within it.
|
|
|
|
# so `cp` and `chmod -R 600` it, first.
|
2024-03-17 05:40:31 +00:00
|
|
|
compileServices = sources: with pkgs; stdenv.mkDerivation {
|
|
|
|
name = "s6-user-services";
|
|
|
|
src = sources;
|
|
|
|
nativeBuildInputs = [ s6-rc ];
|
|
|
|
buildPhase = ''
|
|
|
|
s6-rc-compile $out $src
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
|
|
|
# transform the `user.services` attrset into a s6 services list.
|
|
|
|
s6SvcsFromConfigServices = services: lib.mapAttrsToList
|
|
|
|
(name: service: {
|
|
|
|
inherit name;
|
2024-03-21 16:59:04 +00:00
|
|
|
check = service.readiness.waitCommand;
|
2024-03-21 16:02:06 +00:00
|
|
|
contents = builtins.attrNames (
|
|
|
|
lib.filterAttrs (_: cfg: lib.elem name cfg.partOf) services
|
2024-03-17 05:40:31 +00:00
|
|
|
);
|
2024-03-21 16:37:57 +00:00
|
|
|
depends = service.depends ++ builtins.attrNames (
|
|
|
|
lib.filterAttrs (_: cfg: lib.elem name cfg.dependencyOf) services
|
|
|
|
);
|
|
|
|
finish = service.cleanupCommand;
|
|
|
|
run = service.command;
|
2024-03-17 05:40:31 +00:00
|
|
|
})
|
|
|
|
services
|
|
|
|
;
|
2024-03-23 13:04:12 +00:00
|
|
|
# return a list of bundles (AttrSets) which contain this service
|
|
|
|
containedBy = services: name: lib.filter (svc: lib.elem name svc.contents) services;
|
|
|
|
# for each bundle to which this service belongs, add that bundle's dependencies as direct dependencies of this service.
|
|
|
|
# this is to overcome that s6 doesn't support bundles having dependencies.
|
|
|
|
pushDownDependencies = services: builtins.map
|
|
|
|
(svc: svc // {
|
|
|
|
depends = lib.unique (
|
|
|
|
svc.depends ++ lib.concatMap
|
|
|
|
(bundle: bundle.depends)
|
|
|
|
(containedBy services svc.name)
|
|
|
|
);
|
|
|
|
})
|
|
|
|
services
|
|
|
|
;
|
2024-03-17 05:40:31 +00:00
|
|
|
in
|
|
|
|
{
|
|
|
|
options.sane.users = with lib; mkOption {
|
|
|
|
type = types.attrsOf (types.submodule ({ config, ...}: let
|
2024-03-23 13:04:12 +00:00
|
|
|
sources = genServices (
|
|
|
|
lib.converge pushDownDependencies (
|
|
|
|
s6SvcsFromConfigServices config.services
|
|
|
|
)
|
|
|
|
);
|
2024-03-17 05:40:31 +00:00
|
|
|
in {
|
2024-03-23 21:33:08 +00:00
|
|
|
# N.B.: `compiled` needs to be writable (for locks -- maybe i can use symlinks to dodge this someday):
|
|
|
|
# i populate it to ~/.config as a well-known place, and then copy it to /run before actually using it live.
|
|
|
|
fs.".config/s6/compiled".symlink.target = compileServices sources;
|
|
|
|
# exposed only for convenience
|
2024-03-17 05:40:31 +00:00
|
|
|
fs.".config/s6/sources".symlink.target = sources;
|
2024-03-18 02:02:24 +00:00
|
|
|
|
|
|
|
fs.".profile".symlink.text = ''
|
|
|
|
function startS6() {
|
|
|
|
local S6_TARGET="''${1:-default}"
|
|
|
|
|
2024-03-23 21:33:08 +00:00
|
|
|
local S6_RUN_DIR="$XDG_RUNTIME_DIR/s6"
|
|
|
|
local COMPILED="$S6_RUN_DIR/compiled"
|
|
|
|
local LIVE="$S6_RUN_DIR/live"
|
|
|
|
local SCANDIR="$S6_RUN_DIR/scandir"
|
2024-03-18 02:02:24 +00:00
|
|
|
|
2024-03-19 06:50:03 +00:00
|
|
|
rm -rf "$SCANDIR"
|
2024-03-23 21:33:08 +00:00
|
|
|
mkdir -p "$SCANDIR"
|
2024-03-19 06:50:03 +00:00
|
|
|
s6-svscan "$SCANDIR" &
|
2024-03-18 02:02:24 +00:00
|
|
|
local SVSCAN=$!
|
|
|
|
|
|
|
|
# the scandir is just links back into the compiled dir,
|
|
|
|
# so the compiled dir therefore needs to be writable:
|
2024-03-19 06:50:03 +00:00
|
|
|
rm -rf "$COMPILED"
|
2024-03-23 21:33:08 +00:00
|
|
|
cp --dereference -R "$HOME/.config/s6/compiled" "$COMPILED"
|
2024-03-19 06:50:03 +00:00
|
|
|
chmod -R 0700 "$COMPILED"
|
2024-03-18 02:02:24 +00:00
|
|
|
|
2024-03-19 06:50:03 +00:00
|
|
|
s6-rc-init -c "$COMPILED" -l "$LIVE" -d "$SCANDIR"
|
2024-03-18 02:02:24 +00:00
|
|
|
|
|
|
|
if [ -n "$S6_TARGET" ]; then
|
2024-03-19 06:50:03 +00:00
|
|
|
s6-rc -l "$LIVE" start "$S6_TARGET"
|
2024-03-18 02:02:24 +00:00
|
|
|
fi
|
|
|
|
|
2024-03-19 06:50:03 +00:00
|
|
|
echo 's6 initialized: Ctrl+C to stop'
|
|
|
|
wait "$SVSCAN"
|
|
|
|
}
|
|
|
|
function startS6WithLogging() {
|
2024-03-23 21:33:08 +00:00
|
|
|
# the log dir should already exist by now (nixos persistence); create it just in case something went wrong.
|
2024-03-19 06:50:03 +00:00
|
|
|
mkdir -p "${logBase}"
|
2024-03-21 11:44:21 +00:00
|
|
|
startS6 2>&1 | tee /dev/stderr | s6-log -- T "${logBase}/catchall"
|
2024-03-18 02:02:24 +00:00
|
|
|
}
|
|
|
|
|
2024-03-19 06:50:03 +00:00
|
|
|
primarySessionCommands+=('startS6WithLogging &')
|
2024-03-18 02:02:24 +00:00
|
|
|
'';
|
2024-03-17 05:40:31 +00:00
|
|
|
}));
|
|
|
|
};
|
|
|
|
}
|