ec-node: add relay CAS archiver and nix auto-archive service

This commit is contained in:
every.channel 2026-02-24 02:50:14 -08:00
parent f70d4a02fd
commit 656ec11c73
No known key found for this signature in database
4 changed files with 823 additions and 6 deletions

View file

@ -191,6 +191,61 @@ in
};
};
archive = {
enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Run relay archival workers that subscribe and persist streams into CAS storage.";
};
directoryUrl = lib.mkOption {
type = lib.types.str;
default = "https://every.channel";
description = "Directory base URL used to discover public streams (`/api/public-streams`).";
};
outputDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/every-channel/archive";
description = "CAS object root passed to `ec-node wt-archive --output-dir`.";
};
manifestDir = lib.mkOption {
type = lib.types.str;
default = "/var/lib/every-channel/manifests";
description = "Manifest/index root passed to `ec-node wt-archive --manifest-dir`.";
};
pollIntervalMs = lib.mkOption {
type = lib.types.ints.positive;
default = 15000;
description = "Discovery poll interval for public streams.";
};
streamPrefix = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Optional broadcast-name prefix filter for archival workers.";
};
tracks = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [
"catalog.json"
"init.mp4"
"video0.m4s"
"audio0.m4s"
];
description = "Tracks passed to each `wt-archive` worker.";
};
tlsDisableVerify = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Danger: disable TLS verification for relay archive subscribers.";
};
};
broadcasts = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
@ -218,8 +273,8 @@ in
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = cfg.broadcasts != [ ];
message = "services.every-channel.ec-node.broadcasts must be non-empty when enabled";
assertion = (cfg.broadcasts != [ ]) || cfg.archive.enable;
message = "services.every-channel.ec-node.broadcasts must be non-empty unless archive.enable=true";
}
{
assertion =
@ -239,9 +294,14 @@ in
}
];
systemd.tmpfiles.rules = [
"d /run/every-channel 1777 root root - -"
];
systemd.tmpfiles.rules =
[
"d /run/every-channel 1777 root root - -"
]
++ lib.optionals cfg.archive.enable [
"d ${cfg.archive.outputDir} 0750 root root - -"
"d ${cfg.archive.manifestDir} 0750 root root - -"
];
systemd.services =
lib.listToAttrs (map
@ -571,6 +631,146 @@ in
SystemCallArchitectures = "native";
};
environment = cfg.environment;
};
})
// lib.optionalAttrs cfg.archive.enable
(let
archiveUnit = "every-channel-wt-archive-auto";
archivePrefix = if cfg.archive.streamPrefix == null then "" else cfg.archive.streamPrefix;
archiveTrackLines = lib.concatMapStrings (track: " cmd+=(--track ${lib.escapeShellArg track})\n") cfg.archive.tracks;
archiveRunner = pkgs.writeShellApplication {
name = archiveUnit;
runtimeInputs = [
pkgs.coreutils
pkgs.curl
pkgs.gawk
pkgs.jq
cfg.package
];
text = ''
set -euo pipefail
directory_url=${lib.escapeShellArg (cfg.archive.directoryUrl + "/api/public-streams")}
output_dir=${lib.escapeShellArg cfg.archive.outputDir}
manifest_dir=${lib.escapeShellArg cfg.archive.manifestDir}
relay_fallback=${lib.escapeShellArg cfg.relayUrl}
stream_prefix=${lib.escapeShellArg archivePrefix}
state_dir="/run/every-channel/archive"
pids_dir="$state_dir/pids"
logs_dir="$state_dir/logs"
mkdir -p "$pids_dir" "$logs_dir"
poll_secs="$(awk 'BEGIN { printf "%.3f", ${toString cfg.archive.pollIntervalMs} / 1000.0 }')"
cleanup_children() {
for pid_file in "$pids_dir"/*.pid; do
[[ -e "$pid_file" ]] || continue
pid="$(cat "$pid_file" 2>/dev/null || true)"
if [[ -n "$pid" ]]; then
kill "$pid" 2>/dev/null || true
fi
rm -f "$pid_file"
done
}
trap cleanup_children INT TERM EXIT
while true; do
entries_json="$(curl -fsS "$directory_url" || true)"
if [[ -z "$entries_json" ]]; then
sleep "$poll_secs"
continue
fi
while IFS= read -r entry; do
name="$(printf '%s\n' "$entry" | jq -r '.broadcast_name // empty')"
relay="$(printf '%s\n' "$entry" | jq -r '.relay_url // empty')"
if [[ -z "$name" ]]; then
continue
fi
if [[ -n "$stream_prefix" && "$name" != "$stream_prefix"* ]]; then
continue
fi
if [[ -z "$relay" ]]; then
relay="$relay_fallback"
fi
key="$(printf '%s' "$name" | tr -c 'A-Za-z0-9_.-' '_')"
pid_file="$pids_dir/$key.pid"
if [[ -s "$pid_file" ]]; then
pid="$(cat "$pid_file" 2>/dev/null || true)"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
continue
fi
fi
cmd=(
${lib.escapeShellArg "${cfg.package}/bin/ec-node"}
wt-archive
--url "$relay"
--name "$name"
--output-dir "$output_dir"
--manifest-dir "$manifest_dir"
)
${lib.optionalString cfg.archive.tlsDisableVerify "cmd+=(--tls-disable-verify)"}
${archiveTrackLines}
log_file="$logs_dir/$key.log"
(
exec "''${cmd[@]}"
) >>"$log_file" 2>&1 &
echo "$!" > "$pid_file"
done < <(printf '%s\n' "$entries_json" | jq -rc '.entries[]?')
for pid_file in "$pids_dir"/*.pid; do
[[ -e "$pid_file" ]] || continue
pid="$(cat "$pid_file" 2>/dev/null || true)"
if [[ -z "$pid" ]] || ! kill -0 "$pid" 2>/dev/null; then
rm -f "$pid_file"
fi
done
sleep "$poll_secs"
done
'';
};
in
{
"${archiveUnit}" = {
description = "every.channel relay archival auto-worker";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
unitConfig = {
StartLimitIntervalSec = 0;
};
serviceConfig = {
Type = "simple";
ExecStart = "${archiveRunner}/bin/${archiveUnit}";
Restart = "always";
RestartSec = 2;
NoNewPrivileges = true;
PrivateTmp = true;
ProtectSystem = "strict";
ProtectHome = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
RestrictSUIDSGID = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
ReadWritePaths = [
"/run/every-channel"
cfg.archive.outputDir
cfg.archive.manifestDir
];
};
environment = cfg.environment;
};
});