#!/bin/sh
# svctl forecast  -- E10 (1.2.0)
#
# Tells BEFOREHAND which services an "svctl update" would touch.
#
# Why this is needed: on STRUCTURAL changes (dependencies, bundles, type
# changes) s6-rc-update restarts the affected longruns -- including ones nobody
# changed, because the cascade runs through the dependants. Measured on
# 24.09.2026: a dependency change on keepalived took cron down with it (uptime
# 3305 s -> 3 s). On rt-1 a service in the live database carries 15
# dependencies of which the package source brings only one -- the rest comes
# from the policy. Raising the policy therefore touches almost everything, and
# that is the reason for the rule "an s6-policy upgrade with structural changes
# is a maintenance window".
#
# Until 1.1.3 this window could only be read AFTERWARDS from
# /var/log/apt/term.log -- and that lists only the FAILED services, i.e. the
# wrong set.
#
# Difference to "svctl listforecast": that lists which services would be UP
# after the next "apply" (target state). This is about the transitions, i.e.
# which services WOULD be touched, stopped or restarted.
#
# NOTHING is changed: compilation goes into a throwaway directory, and
# s6-rc-update runs with -n (dry run). The live database stays untouched.
set -u

PROG="svctl-forecast"

# umask 022 (1.2.7): everything svctl creates -- the compiled database, the
# live copy made by s6-rc-update, links, contents.d entries, masks -- has
# readers that are not root: s6-fdholderd runs as sysfdh and reads data/rules
# on every connection, aep-zabbix-checks reads as zabbix. The permissions must
# not depend on the session that calls svctl. Measured on 23.09.2026 on
# srv-mgmt-tick1-1: a Puppet run from a session with umask 077 produced a
# compiled database with 700/600, the fdholder then answered "Connection
# reset", every log pipe was missing, zabbix-agent and kubelet died without a
# log line.
umask 022
if ! test -L /run/s6-rc/compiled ; then
  echo "$PROG: fatal: s6-rc is not running" 1>&2
  exit 2
fi

policy=$(readlink /usr/lib/s6-rc/policy/current 2>/dev/null || true)
case "${policy:-}" in
  exclusive|shared) ;;
  *)
    echo "$PROG: fatal: /usr/lib/s6-rc/policy/current should be a relative symlink to either shared or exclusive" 1>&2
    exit 100
    ;;
esac

tmpdb=$(mktemp -d /tmp/svctl-forecast.XXXXXX) || {
  echo "$PROG: fatal: cannot create a throwaway directory" 1>&2
  exit 1
}
# s6-rc-compile wants a target that does NOT exist.
db="$tmpdb/db"
cleanup() { rm -rf "$tmpdb"; }
trap cleanup EXIT INT TERM

if ! s6-rc-compile -h sysfdh "$db" /usr/lib/s6-rc/policy/current /usr/lib/s6-rc/sources 2>"$tmpdb/err" ; then
  echo "$PROG: fatal: the database could not be compiled -- an 'svctl update' would" 1>&2
  echo "$PROG:        fail here as well. Message from s6-rc-compile:" 1>&2
  sed 's/^/'"$PROG"':        /' "$tmpdb/err" 1>&2
  exit 1
fi

# -v 2 is needed: with the default s6-rc-update prints two s6-rc lines with
# EMPTY service lists, and those do not show whether there is nothing to do or
# whether nothing is being said (measured on 24.09.2026 on three nodes).
out=$(s6-rc-update -n -v 2 "$db" 2>&1) || {
  echo "$PROG: fatal: the dry run failed:" 1>&2
  printf '%s\n' "$out" | sed 's/^/'"$PROG"':        /' 1>&2
  exit 1
}

# Extract the service names from the planned s6-rc calls. Everything after
# "change" is the list; options and paths are not.
names=$(printf '%s\n' "$out" | sed -n 's/.*-- *change *//p' | tr ' ' '\n' | sed '/^$/d' | sort -u)

printf '%s\n' "$out" | sed 's/^/  /'
echo
if [ -z "$names" ]; then
  echo "$PROG: no service would be affected -- an 'svctl update' is no window here."
  echo "$PROG: (structure and definitions match the running database.)"
  exit 0
fi

_n=$(printf '%s\n' "$names" | wc -l | tr -d ' ')
echo "$PROG: ${_n} service(s) would be affected -- 'svctl update' touches them:"
printf '%s\n' "$names" | while read -r _s ; do
  [ -n "$_s" ] || continue
  _d="/run/s6-rc/servicedirs/$_s"
  if [ -d "$_d" ]; then
    echo "  $_s  (now: $(s6-svstat -o up,updownfor "$_d" 2>/dev/null || echo 'not measurable'))"
  else
    echo "  $_s  (oneshot or not running)"
  fi
done
echo
echo "$PROG: A running service in this list will be restarted. On a hypervisor"
echo "$PROG: or database node that is a maintenance window; on the first boot of"
echo "$PROG: a fresh node it is free, because nothing is running yet."
exit 0
