#!/bin/sh -e

. /usr/lib/s6-policy-common/lib/svctl-common.sh

if test -z "$1" ; then
  echo "svctl-restart: usage: svctl-restart service..." 1>&2
  exit 100
fi

if ! test -L /run/s6-rc/compiled ; then
  echo "svctl-restart: fatal: s6-rc is not running" 1>&2
  exit 2
fi

list=$(s6-rc-db atomics "$@")

# Oneshots have to be taken down before they can run again: "s6-rc -u change" on
# an already-active oneshot is a no-op, so until 2026-09-22 "svctl restart" on a
# oneshot did nothing at all and still exited 0 -- every "notify => Service[...]"
# on a oneshot was silently lost (S6-SYSTEM.md, and the Puppet provider reported
# "Triggered 'refresh'" for it).
#
# Note what this implies: taking a oneshot down takes its dependants down too,
# and "s6-rc -u change" below brings the named service back up, not them. That is
# s6-rc's dependency semantics, and it is the reason this only happens for
# oneshots that are actually up -- restarting a stopped one would have no
# meaning either way.
downagain=""
for i in $list ; do
  t=$(s6-rc-db type "$i")
  if test x"$t" = x"longrun" ; then
    # E4: WAIT, as "svctl start" does.
    #
    # "s6-svc -r" sends the signal and returns at once; the following
    # "s6-rc -u change" is a no-op for a service s6-rc already lists as active.
    # Until 1.1.3 "svctl restart" was thus done before the service was back up.
    # The provider compensated for that, manual calls and foreign postinsts did
    # not.
    #
    # 1.2.3: -wR/-wr, NOT -wU/-wu. 1.2.0 to 1.2.2 used -wU/-wu, which waits for
    # a STATE: s6-svlisten1 reads the initial state before sending the signal,
    # and a service that is already up and ready satisfies "-wU" immediately.
    # Measured (container kat, 23.09.2026): restarting a unit that takes 5 s to
    # become ready returned after 9 ms with rc 0, the service was "up 0 s, not
    # ready" -- E4 never worked. -wR/-wr wait for the EVENT "restarted (and
    # ready)": the same unit took 5008 ms and was ready afterwards.
    #
    # -wR with notification-fd, else -wr: without notification-fd s6 never
    # reports "ready" (s6-svstat -o ready stays false), and -wR would run into
    # the limit.
    #
    # Only for a service that is up NOW: "s6-svc -r" on a stopped service does
    # nothing, and -wr then waits until the limit (measured: rc 99 after
    # 5010 ms). A stopped service is brought up by the "s6-rc -u change" below,
    # which waits by itself -- just like "svctl start".
    #
    # -T takes the same limit as every other transition, i.e. SVCTL_TIMEOUT_DPKG
    # inside a dpkg run (svctl_timeout decides that by DPKG_RUNNING_VERSION).
    _rdir="/run/s6-rc/servicedirs/$i"

    # E11 (1.2.6): for a permafail unit a deliberate restart is a RESET.
    # s6-permafailon counts the deaths within its window; without clearing the
    # tally the service goes permanently down again at its first failure after
    # the restart -- or, as measured in container cab, a clean restart (SIGTERM)
    # led to "permanently down" because the old deaths were still inside the
    # window. Detected by s6-permafailon in finish (dh-s6 .permafail generates
    # exactly that; s6-rc stores it as finish.user).
    if grep -qs 's6-permafailon' "$_rdir/finish.user" "$_rdir/finish" ; then
      if s6-svdt-clear "$_rdir" 2>/dev/null ; then
        echo "svctl-restart: $i: death tally cleared (permafail unit, a restart is a reset)" 1>&2
      fi
    fi

    if [ -e "$_rdir/notification-fd" ]; then
      _rwait="-wR"; _uwait="-wU"
      _rword="ready"
    else
      _rwait="-wr"; _uwait="-wu"
      _rword="up"
    fi
    _rlim="$(svctl_timeout)"
    _rrc=0
    if [ "$(s6-svstat -o up "$_rdir" 2>/dev/null)" != "true" ]; then
      # Down. If s6-rc does NOT list the service as active, the
      # "s6-rc -u change" below brings it up. If s6-rc lists it as active --
      # the case after permafail (exit 125: up=false wantedup=false) or after a
      # crash with a lowered wish --, that change is a no-op, and until 1.2.5
      # restart reported rc 0 and did NOTHING (measured in kat: after permafail
      # and a fixed cause the service stayed down, tally 3). So bring it up
      # directly here and wait, like svctl start does in its follow-up.
      if s6-rc -a list 2>/dev/null | grep -qFx "$i" ; then
        s6-svc -u "$_uwait" -T "$_rlim" "/run/s6-rc/scandir/$i" || _rrc=$?
      else
        continue
      fi
    else
      s6-svc -r "$_rwait" -T "$_rlim" "/run/s6-rc/scandir/$i" || _rrc=$?
    fi
    if [ "$_rrc" -ne 0 ]; then
      echo "svctl-restart: fatal: $i did not become ${_rword} again after the restart" 1>&2
      echo "svctl-restart:        (limit ${_rlim} ms, s6-svc rc=${_rrc}). State:" 1>&2
      s6-svstat "$_rdir" 2>&1 | sed 's/^/svctl-restart:        /' 1>&2
      exit "$_rrc"
    fi
  elif test x"$t" = x"oneshot" ; then
    if s6-rc -a list | grep -qFx "$i" ; then
      downagain="$downagain $i"
    fi
  fi
done

timeout="$(svctl_timeout)"

if test -n "$downagain" ; then
  # shellcheck disable=SC2086
  rc=0
  s6-rc -t "$timeout" -d change $downagain || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "svctl-restart: fatal: taking the oneshots down failed or exceeded" 1>&2
    echo "svctl-restart:        the limit of ${timeout} ms (s6-rc rc=${rc})." 1>&2
    # shellcheck disable=SC2086
    svctl_report_pending down $downagain
    exit "$rc"
  fi
fi

# With a time limit: a service that hangs on restart must not block a
# postinst or a Puppet run (see svctl-update.unlocked).
rc=0
s6-rc -t "$timeout" -u change "$@" || rc=$?
if [ "$rc" -ne 0 ]; then
  echo "svctl-restart: fatal: the state transition failed or exceeded the limit of" 1>&2
  echo "svctl-restart:        ${timeout} ms (s6-rc rc=${rc})." 1>&2
  if [ "$rc" -eq 111 ]; then
    echo "svctl-restart: rc=111 is the lock error: another s6-rc transition holds" 1>&2
    echo "svctl-restart: /run/s6-rc, the limit was not reached." 1>&2
  fi
  svctl_report_pending up "$@"
  exit "$rc"
fi
