RemainAfterExit=true kept the oneshot 'active' forever, which broke the timer's OnUnitActiveSec re-arming: the timer fired once (NEXT emptied, Trigger n/a) and never polled again. Remove it so the service returns to dead and the 2-min cadence keeps firing.
99 lines
4.1 KiB
Nix
99 lines
4.1 KiB
Nix
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.radiantwaveUpdater;
|
|
in
|
|
{
|
|
options.services.radiantwaveUpdater = {
|
|
enable = lib.mkEnableOption "live RadiantWave app updates via a polled manifest";
|
|
|
|
manifestUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "HTTP(S) URL of the JSON manifest announcing the current app store path.";
|
|
};
|
|
|
|
atticUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
description = "Attic cache URL to fetch the app closure from (fetch-only, never builds).";
|
|
};
|
|
|
|
baselinePackage = lib.mkOption {
|
|
type = lib.types.package;
|
|
description = "Copy of RadiantWave baked into the system closure, used to seed the runtime pointer before the first update.";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# Runtime pointer the login shell launches RadiantWave from. Seeded on
|
|
# first boot to the baseline copy already baked into the system closure
|
|
# (so the ISO carries it with no duplication); the updater service later
|
|
# re-points it atomically to newer closures fetched from Attic.
|
|
systemd.tmpfiles.rules = [
|
|
"L /var/lib/radiantwave/current - - - - ${cfg.baselinePackage.outPath}"
|
|
];
|
|
|
|
systemd.services.radiantwave-updater = {
|
|
description = "Poll the RadiantWave manifest and swap the live app pointer";
|
|
after = [ "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
serviceConfig = {
|
|
Type = "oneshot";
|
|
# NOTE: no RemainAfterExit here. Keeping the oneshot "active" after
|
|
# it finishes breaks the timer's OnUnitActiveSec re-arming, so the
|
|
# 2-min cadence stops after the first run (NEXT goes empty, Trigger
|
|
# becomes n/a). Let the service return to dead so the timer refires.
|
|
};
|
|
script = ''
|
|
set -u
|
|
|
|
NEW_PATH=$(${pkgs.curl}/bin/curl -fsSL "${cfg.manifestUrl}" \
|
|
| ${pkgs.jq}/bin/jq -er .storePath) || {
|
|
echo "radiantwave-updater: failed to fetch/parse manifest from ${cfg.manifestUrl}"
|
|
exit 0
|
|
}
|
|
|
|
CURRENT=$(${pkgs.coreutils}/bin/readlink /var/lib/radiantwave/current 2>/dev/null || true)
|
|
|
|
if [ -z "$NEW_PATH" ] || [ "$NEW_PATH" = "$CURRENT" ]; then
|
|
# Already current; make sure the running closure is GC-rooted.
|
|
${pkgs.coreutils}/bin/ln -sfn "$CURRENT" /var/lib/radiantwave/.current-root 2>/dev/null || true
|
|
echo "radiantwave-updater: already current (''${CURRENT})"
|
|
exit 0
|
|
fi
|
|
|
|
# Realize the closure via the machine's substituters. The app itself
|
|
# only exists in the dev Attic cache, but nixpkgs deps (e.g. sqlite)
|
|
# are signed by cache.nixos.org and Attic intentionally skips them to
|
|
# avoid re-uploading the world. Fetch those from cache.nixos.org so
|
|
# the whole closure resolves without ever building from source.
|
|
# --add-root pins the fetched closure so GC can't collect it while
|
|
# the swap below makes it live.
|
|
echo "radiantwave-updater: acquiring ''${NEW_PATH}"
|
|
${pkgs.nix}/bin/nix-store -r "$NEW_PATH" \
|
|
--add-root /var/lib/radiantwave/.current-root \
|
|
--option extra-substituters "https://attic.radiantwavetech.com/radiantwave-dev https://cache.nixos.org" \
|
|
--option extra-trusted-public-keys "radiantwave-dev:B8FhLu3MQ9JfeEIHIKNrKxnP+3akzzkqMbTZR4hRfj0= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" || {
|
|
echo "radiantwave-updater: nix copy failed for ''${NEW_PATH}"
|
|
exit 1
|
|
}
|
|
|
|
# Atomic swap: the app never observes a half-updated pointer.
|
|
${pkgs.coreutils}/bin/ln -sfn "$NEW_PATH" /var/lib/radiantwave/current.tmp
|
|
${pkgs.coreutils}/bin/mv -T /var/lib/radiantwave/current.tmp /var/lib/radiantwave/current
|
|
echo "radiantwave-updater: swapped ''${CURRENT} -> ''${NEW_PATH}"
|
|
'';
|
|
};
|
|
|
|
systemd.timers.radiantwave-updater = {
|
|
description = "Check for RadiantWave app updates";
|
|
wantedBy = [ "timers.target" ];
|
|
timerConfig = {
|
|
OnBootSec = "2min";
|
|
OnUnitActiveSec = "2min";
|
|
Persistent = true;
|
|
Unit = "radiantwave-updater.service";
|
|
};
|
|
};
|
|
};
|
|
}
|