#!/bin/sh
# svctl state <unit> [--json]  -- E2 (1.2.0)
#
# ONE line that answers the question "how is this service doing?".
#
# Why this exists: until 1.1.3 this question took FIVE calls and three tools
# (svctl isup, svctl isenabled, svctl iswanted, s6-svstat, s6-svdt), and the
# only machine-readable merge lived in aep-zabbix-checks -- in a MONITORING
# package, not in the control tool. Whoever sits at the machine should not have
# to type five commands and assemble the answer in their head.
#
# EVERY FIELD IS -1 WHEN NOT MEASURABLE, and never 0: for updownfor,
# deaths_window and the booleans 0 is a valid statement. A 0 for "I did not
# look" is exactly the green light that misled us several times this week.
#
# Permissions: the fields from s6-svstat/s6-svdt need read access to
# servicedirs/<unit>/supervise (drwx------ root root), i.e. root. Without root
# they are -1 -- the call does not fail, it just says less.
set -u

PROG="svctl-state"
unit=""
json=false
for a in "$@" ; do
  case "$a" in
    --json) json=true ;;
    -*) echo "$PROG: unknown option: $a" 1>&2; exit 100 ;;
    *) if [ -z "$unit" ]; then unit="$a"; else echo "$PROG: too many arguments" 1>&2; exit 100; fi ;;
  esac
done
if [ -z "$unit" ]; then
  echo "$PROG: usage: svctl state <unit> [--json]" 1>&2
  exit 100
fi
case "$unit" in
  *[!A-Za-z0-9_.-]*) echo "$PROG: invalid unit name: $unit" 1>&2; exit 100 ;;
esac
if ! test -L /run/s6-rc/compiled ; then
  echo "$PROG: fatal: s6-rc is not running" 1>&2
  exit 2
fi
if ! s6-rc-db list services 2>/dev/null | grep -qFx "$unit" ; then
  if s6-rc-db list bundles 2>/dev/null | grep -qFx "$unit" ; then
    echo "$PROG: $unit is a bundle, not a service -- svctl isup shows bundles" 1>&2
    exit 100
  fi
  echo "$PROG: unknown atomic service: $unit" 1>&2
  exit 4
fi

DIR="/run/s6-rc/servicedirs/$unit"
TAI64_EPOCH=4611686018427387904
TAI_UTC_OFFSET=37
WINDOW=${SVCTL_DEATH_WINDOW:-600}

up=-1 ready=-1 has_notification=0 wanted=-1 enabled=-1 normallyup=-1
updownfor=-1 deaths_window=-1 tally_full=-1 restart_pending=-1 permafail_suspect=-1

# --- wanted state and enabled set: no root needed --------------------------
if s6-rc -a list 2>/dev/null | grep -qFx "$unit" ; then wanted=1; else wanted=0; fi
if /usr/lib/s6-policy-common/bin/svctl-isenabled "$unit" >/dev/null 2>&1 ; then
  enabled=1
else
  case $? in
    3|1) enabled=0 ;;
    *) enabled=-1 ;;
  esac
fi

# --- supervision state: needs root -----------------------------------------
if [ -d "$DIR" ]; then
  [ -e "$DIR/notification-fd" ] && has_notification=1
  if _st=$(s6-svstat -o up,ready,normallyup,updownfor "$DIR" 2>/dev/null) ; then
    # shellcheck disable=SC2086
    set -- $_st
    if [ $# -ge 4 ]; then
      [ "$1" = true ] && up=1 || up=0
      if [ "$has_notification" = 1 ]; then
        [ "$2" = true ] && ready=1 || ready=0
      else
        # Without notification-fd s6 reports ready == up. That is no statement
        # about readiness, so none is claimed here either.
        ready=-1
      fi
      [ "$3" = true ] && normallyup=1 || normallyup=0
      case "$4" in ''|*[!0-9]*) updownfor=-1 ;; *) updownfor=$4 ;; esac
    fi
  fi

  # --- death tally -----------------------------------------------------------
  # FAILURES are counted, not deaths: an orderly stop is not a failure.
  # Measured on 24.09.2026: "svctl restart"/"svctl stop" leave "signal SIGTERM",
  # a service that catches SIGTERM leaves "exitcode 0", and a service that dies
  # on its own leaves "exitcode <n>". Counting everything declares every second
  # restart a crash -- that mistake cost the provider two versions.
  _tmax=100
  if [ -r "$DIR/max-death-tally" ]; then
    read -r _m < "$DIR/max-death-tally" 2>/dev/null || _m=''
    case ${_m:-} in ''|*[!0-9]*) ;; *) _tmax=$_m ;; esac
  fi
  if _svdt=$(s6-svdt -n "$_tmax" "$DIR" 2>/dev/null) ; then
    _now=$(( $(date -u +%s) + TAI_UTC_OFFSET ))
    _n=0 _f=0 _pf=0
    while IFS= read -r _line ; do
      case "$_line" in @*) ;; *) continue ;; esac
      _n=$((_n + 1))
      _lbl=${_line#@}
      _sec=$(printf '%.16s' "$_lbl")
      _t=$(( 0x$_sec - TAI64_EPOCH ))
      [ $(( _now - _t )) -le "$WINDOW" ] || continue
      case "$_line" in
        *'signal SIGTERM'*) continue ;;
        *'exitcode 0'*) continue ;;
      esac
      _f=$((_f + 1))
      case "$_line" in *'exitcode 125'*) _pf=1 ;; esac
    done <<EOF2
$_svdt
EOF2
    deaths_window=$_f
    [ "$_n" -ge "$_tmax" ] && tally_full=1 || tally_full=0
    permafail_suspect=$_pf
  fi

  # --- restart_pending -----------------------------------------------------
  # s6-rc wraps the run script and keeps the original next to it as run.user;
  # so its mtime is compared, not that of run.
  #
  # LIMIT, named instead of hidden: s6-rc-update rewrites the WHOLE tree on
  # every change, so after installing any s6 package all run.user files carry
  # a fresh mtime. This field therefore says "the file is newer than the
  # process", not "the content has changed". Whoever needs the latter uses
  # restart_pending from aep-zabbix-checks (>= 0.4.5), which keeps a hash
  # state.
  if [ "$up" = 1 ] && [ "$updownfor" -ge 0 ] 2>/dev/null ; then
    _rf="$DIR/run.user"
    [ -e "$_rf" ] || _rf="$DIR/run"
    if [ -r "$_rf" ] && _mt=$(stat -c %Y "$_rf" 2>/dev/null) ; then
      if [ "$_mt" -gt $(( $(date -u +%s) - updownfor )) ] 2>/dev/null ; then
        restart_pending=1
      else
        restart_pending=0
      fi
    fi
  fi
fi

if $json ; then
  printf '{"unit":"%s","up":%s,"ready":%s,"has_notification":%s,"wanted":%s,"enabled":%s,"normallyup":%s,"updownfor":%s,"deaths_window":%s,"tally_full":%s,"restart_pending":%s,"permafail_suspect":%s}\n' \
    "$unit" "$up" "$ready" "$has_notification" "$wanted" "$enabled" "$normallyup" \
    "$updownfor" "$deaths_window" "$tally_full" "$restart_pending" "$permafail_suspect"
else
  printf '%s up=%s ready=%s has_notification=%s wanted=%s enabled=%s normallyup=%s updownfor=%s deaths_window=%s tally_full=%s restart_pending=%s permafail_suspect=%s\n' \
    "$unit" "$up" "$ready" "$has_notification" "$wanted" "$enabled" "$normallyup" \
    "$updownfor" "$deaths_window" "$tally_full" "$restart_pending" "$permafail_suspect"
fi
