Files
stack/tools/agent-watch/jarvis-email-watch-liveness.sh
T

69 lines
3.0 KiB
Bash
Executable File

#!/usr/bin/env bash
# jarvis-email-watch-liveness.sh — T40 timer-owner liveness check (code-be-01).
#
# Owner: code-be-01 (Jason ruling, board T40; jarvis remains author of the
# watcher code). Detection is SYSTEMD-STATE-ONLY — no pane scraping, no fixed
# interval assumptions beyond the unit's own 30-min cadence.
#
# DEAD conditions (any one pages the owner):
# 1. timer unit not found or not active
# 2. NextElapse is infinity (timer will never fire again)
# 3. service last result failed
# 4. last service run older than 90 min (3 missed 30-min slots)
#
# Exit: 0 = alive (prints state line) · 1 = DEAD (prints reason) · 2 = cannot
# assert (systemd unreachable — surface, don't guess).
set -uo pipefail
UNIT_TIMER="jarvis-email-watch.timer"
UNIT_SERVICE="jarvis-email-watch.service"
CADENCE_MIN=30
MISSED_SLOTS=3
STALE_SEC=$(( CADENCE_MIN * 60 * MISSED_SLOTS ))
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
active="$(systemctl --user show "$UNIT_TIMER" --property=ActiveState --value 2>/dev/null)"
rc=$?
if [ $rc -ne 0 ] || [ -z "$active" ]; then
echo "DEAD(reason=systemd-unreachable-or-unit-missing unit=$UNIT_TIMER rc=$rc)"
exit 2
fi
[ "$active" = "active" ] || { echo "DEAD(reason=timer-not-active state=$active)"; exit 1; }
# This timer is MONOTONIC-ONLY (OnBootSec/OnUnitActiveSec/OnUnitInactiveSec — no
# realtime calendar), so NextElapseUSecRealtime is legitimately empty. The
# unit file's own comment records the failure signature that killed brain-sync
# for five days: monotonic timer + reboot => NextElapse becomes 'infinity'
# while is-active/is-enabled stay green. Probe the monotonic property; then
# take the AUTHORITATIVE next-fire from list-timers (it computes the min over
# all timer bases; `show`'s aggregate includes the long-expired OnBootSec base
# and reads as ~never on a healthy timer).
mono="$(systemctl --user show "$UNIT_TIMER" --property=NextElapseUSecMonotonic --value 2>/dev/null)"
[ "$mono" != "infinity" ] || { echo "DEAD(reason=next-elapse-infinity monotonic — reboot-killed-timer signature, see unit-file note)"; exit 1; }
row="$(systemctl --user list-timers "$UNIT_TIMER" --no-pager 2>/dev/null | grep "$UNIT_TIMER")"
[ -n "$row" ] || { echo "DEAD(reason=no-list-timers-row unit=$UNIT_TIMER)"; exit 1; }
case "$row" in
*"n/a"*) echo "DEAD(reason=list-timers-next-na)"; exit 1 ;;
esac
next="$row"
result="$(systemctl --user show "$UNIT_SERVICE" --property=Result --value 2>/dev/null)"
[ "$result" = "success" ] || [ "$result" = "" ] || { echo "DEAD(reason=service-result-failed result=$result)"; exit 1; }
execstamp="$(systemctl --user show "$UNIT_SERVICE" --property=ExecMainExitTimestamp --value 2>/dev/null)"
if [ -n "$execstamp" ] && [ "$execstamp" != "0" ]; then
last_ep="$(date -d "$execstamp" +%s 2>/dev/null || echo 0)"
now_ep="$(date +%s)"
age=$(( now_ep - last_ep ))
if [ "$age" -gt "$STALE_SEC" ]; then
echo "DEAD(reason=stale-last-run age_min=$((age/60)) threshold_min=$((STALE_SEC/60)))"
exit 1
fi
fi
echo "ALIVE(active=$active next=$next result=$result last_exit=$execstamp)"
exit 0