{ config, lib, pkgs, ... }: with lib; let writeExeclineScript = name: text: pkgs.writeScript name '' #!${pkgs.execline}/bin/execlineb -P ${text} ''; cfg = config.s6; # -------- configuration for s6-rc -------- # loggerNames = flatten (mapAttrsToList (_: srv: optional (srv.logger != null) srv.logger) cfg.longruns); loggingRelations = genAttrs loggerNames (loggerName: filter (serviceName: cfg.longruns.${serviceName}.logger == loggerName) (attrNames cfg.longruns)); loggerLongruns = mapAttrs' (loggerName: producers: nameValuePair "${loggerName}-log" { run = '' s6-setuidgid ${cfg.logUser} exec -c s6-log -d 3 -- ${cfg.loggingScript loggerName} ''; notificationFd = 3; consumerFor = producers; pipelineName = "${loggerName}-pipeline"; flagEssential = false; timeoutUp = null; timeoutDown = null; dependencies = null; finish = null; timeoutKill = null; timeoutFinish = null; maxDeathTally = null; downSignal = null; producerFor = null; logger = null; }) loggingRelations; allLongruns = cfg.longruns // loggerLongruns; makeCommonConfig = srv: '' mkdir -p $out '' + optionalString srv.flagEssential '' touch $out/flagEssential ''; makeBundleConfig = srv: makeCommonConfig srv + '' echo bundle > $out/type mkdir $out/contents.d '' + concatMapStringsSep "\n" (srvName: ''touch "$out/contents.d/${srvName}"'') srv.contents + "\n"; makeAtomicConfig = srv: makeCommonConfig srv + optionalString (srv.timeoutUp != null) '' echo "${toString srv.timeoutUp}" > $out/timeout-up '' + optionalString (srv.timeoutDown != null) '' echo "${toString srv.timeoutDown}" > $out/timeout-down '' + optionalString (srv.dependencies != null) ( '' mkdir $out/dependencies.d '' + concatMapStringsSep "\n" (srvName: ''touch "$out/dependencies.d/${srvName}"'') srv.dependencies + "\n" ); makeOneshotConfig = srv: let upScript = pkgs.writeText "up-script" srv.up; downScript = mapNullable (pkgs.writeText "down-script") srv.down; in makeAtomicConfig srv + '' echo oneshot > $out/type cp ${upScript} $out/up '' + optionalString (downScript != null) '' cp ${downScript} $out/down ''; makeLongrunConfig = srv: let runScript = writeExeclineScript "run-script" srv.run; finishScript = mapNullable (writeExeclineScript "finish-script") srv.finish; in makeAtomicConfig srv + '' echo longrun > $out/type cp ${runScript} $out/run '' + optionalString (finishScript != null) '' cp ${finishScript} $out/finish '' + optionalString (srv.notificationFd != null) '' echo "${toString srv.notificationFd}" > $out/notification-fd '' + optionalString (srv.timeoutKill != null) '' echo "${toString srv.timeoutKill}" > $out/timeout-kill '' + optionalString (srv.timeoutFinish != null) '' echo "${toString srv.timeoutFinish}" > $out/timeout-finish '' + optionalString (srv.maxDeathTally != null) '' echo "${toString srv.maxDeathTally}" > $out/max-death-tally '' + optionalString (srv.downSignal != null) '' echo "${srv.downSignal}" > $out/down-signal '' + optionalString (srv.producerFor != null) '' echo "${srv.producerFor}" > $out/producer-for '' + optionalString (srv.consumerFor != null) '' echo "${concatStringsSep "\n" srv.consumerFor}" > $out/consumer-for '' + optionalString (srv.pipelineName != null) '' echo "${srv.pipelineName}" > $out/pipeline-name '' + optionalString (srv.logger != null) '' echo "${srv.logger}-log" > $out/producer-for ''; makeBundleDefinitionDir = name: srv: pkgs.runCommand "s6-rc-bundle-definition-${name}" { } (makeBundleConfig srv); makeOneshotDefinitionDir = name: srv: pkgs.runCommand "s6-rc-oneshot-definition-${name}" { } (makeOneshotConfig srv); makeLongrunDefinitionDir = name: srv: pkgs.runCommand "s6-rc-longrun-definition-${name}" { } (makeLongrunConfig srv); bundleDefinitionDirs = mapAttrs makeBundleDefinitionDir cfg.bundles; oneshotDefinitionDirs = mapAttrs makeOneshotDefinitionDir cfg.oneshots; longrunDefinitionDirs = mapAttrs makeLongrunDefinitionDir allLongruns; allDefinitionDirs = bundleDefinitionDirs // oneshotDefinitionDirs // longrunDefinitionDirs; serviceSourceDir = pkgs.linkFarm "s6-rc-service-source" (mapAttrsToList (srvName: definitionDir: { name = srvName; path = definitionDir; }) allDefinitionDirs); compiledDatabase = pkgs.runCommand "s6-rc-compiled-database" { } '' ${pkgs.s6-rc}/bin/s6-rc-compile $out ${serviceSourceDir} ''; # -------- init script -------- # initialPath = lib.makeBinPath (with pkgs; [ coreutils execline s6 s6-rc ]); catchAllLoggerServiceDir = let runScript = writeExeclineScript "catch-all-logger-run-script" '' s6-setuidgid "${cfg.logUser}" redirfd -w 1 /dev/null redirfd -rnb 0 "${cfg.logFifo}" s6-log -bpd3 -- ${cfg.loggingScript "uncaught"} ''; in pkgs.runCommand "catch-all-logger-srvdir" { } '' mkdir -p $out cp ${runScript} $out/run echo 3 > $out/notification-fd ''; # this runs after NixOS' stage-2-init.sh initScript = writeExeclineScript "init-script" '' ${pkgs.execline}/bin/export PATH "${initialPath}" ${pkgs.execline}/bin/exec # stage-2-init.sh prints the line "starting systemd..." foreground { echo "...NOT! starting s6-svscan on ${cfg.scanDir}..." } if { mkdir -p "${cfg.scanDir}" } if { cp -r "${catchAllLoggerServiceDir}" "${cfg.scanDir}/catch-all-log" } if { mkdir -p "${cfg.logDir}" } if { chown "${cfg.logUser}:${cfg.logGroup}" "${cfg.logDir}" } if { chmod 700 "${cfg.logDir}" } if { mkfifo "${cfg.logFifo}" } if { chown "${cfg.logUser}:${cfg.logGroup}" "${cfg.logFifo}" } if { chmod 600 "${cfg.logFifo}" } redirfd -r 0 /dev/null # don't use /dev/console as stdin redirfd -wnb 1 "${cfg.logFifo}" # redirect stdout to fifo fdmove -c 2 1 # redirect stderr to fifo background { redirfd -w 3 "${cfg.logFifo}" # this blocks until the catch-all logger is running if { s6-rc-init -c "${compiledDatabase}" "${cfg.scanDir}" } s6-rc -v2 change "${cfg.allBundle}" } exec -a s6-svscan # name it 's6-svscan' instead of /nix/store/... s6-svscan "${cfg.scanDir}" ''; in { options = with types; let mkNullableOption = { type, ... } @ o: mkOption o // { type = nullOr type; default = null; }; # see: https://skarnet.org/software/s6-rc/s6-rc-compile.html#source commonOptions = { flagEssential = mkOption { type = bool; default = false; }; }; bundleOptions = { contents = mkOption { type = listOf str; }; } // commonOptions; atomicOptions = { timeoutUp = mkNullableOption { type = ints.unsigned; }; timeoutDown = mkNullableOption { type = ints.unsigned; }; dependencies = mkNullableOption { type = listOf str; }; } // commonOptions; oneshotOptions = { up = mkOption { type = str; }; down = mkNullableOption { type = str; }; } // atomicOptions; longrunOptions = { run = mkOption { type = str; }; finish = mkNullableOption { type = str; }; notificationFd = mkNullableOption { type = ints.unsigned; }; timeoutKill = mkNullableOption { type = ints.unsigned; }; timeoutFinish = mkNullableOption { type = ints.unsigned; }; maxDeathTally = mkNullableOption { type = ints.between 0 4096; }; downSignal = mkNullableOption { type = str; }; producerFor = mkNullableOption { type = str; }; consumerFor = mkNullableOption { type = listOf str; }; pipelineName = mkNullableOption { type = str; }; # this is not an actual option in s6-rc; it creates a logger service # with the name "-log" and adds the necessary "producer-for" # and "consumer-for" files. logger = mkNullableOption { type = str; }; } // atomicOptions; in { s6 = { enable = mkEnableOption "s6 init system"; allBundle = mkOption { type = str; }; logUser = mkOption { type = str; default = "log"; }; logGroup = mkOption { type = str; default = "log"; }; logDir = mkOption { type = path; default = "/log"; }; logFifo = mkOption { type = path; default = "/run/log-fifo"; }; loggingScript = mkOption { type = anything; default = name: "T s1000000 n20 ${cfg.logDir}/${name}"; }; scanDir = mkOption { type = path; default = "/run/service"; }; bundles = mkOption { type = with types; attrsOf (submodule { options = bundleOptions; }); }; oneshots = mkOption { type = with types; attrsOf (submodule { options = oneshotOptions; }); default = { }; }; longruns = mkOption { type = with types; attrsOf (submodule { options = longrunOptions; }); default = { }; }; }; }; config = lib.mkIf cfg.enable { assertions = [ { assertion = hasAttr cfg.allBundle cfg.bundles; message = "the bundle '${cfg.allBundle}' does not exist!"; } { assertion = !(elem "uncaught" loggerNames); message = "the logger name 'uncaught' is reserved"; } { assertion = length (attrNames allDefinitionDirs) == length (attrNames cfg.bundles) + length (attrNames cfg.oneshots) + length (attrNames cfg.longruns) + length (attrNames loggerLongruns); message = "no two services can have the same name, even if they are of different types"; } ] ++ mapAttrsToList (name: def: { assertion = def.producerFor == null || def.logger == null; message = "in s6.longruns.${name}: 'producerFor' and 'logger' are mutually exclusive"; }) cfg.longruns; users = { users.${cfg.logUser} = { isSystemUser = true; group = cfg.logGroup; home = cfg.logDir; }; groups.${cfg.logGroup} = { }; }; # checkmate! boot.systemdExecutable = "${initScript}"; }; }