#!/usr/bin/env bash
# watch-one-app.sh: restart a Docker app that's running but not answering.
# From Broke It First (brokeitfirst.com/articles/docker-said-healthy/). Free to use.
#
# Usage:   watch-one-app.sh URL CONTAINER [FAILURES]
# Example: watch-one-app.sh http://localhost:32400/identity plex 3
#
# Run it every 5 minutes from cron (crontab -e):
#   */5 * * * * /path/to/watch-one-app.sh http://localhost:32400/identity plex 3
#
# It checks the URL the way a browser would. Any reply below 500 (even a login page)
# counts as "answering". After FAILURES failed checks in a row, if the container is
# running, it restarts it once and starts counting again. It never starts a stopped
# container, because backups and people stop containers on purpose.
set -u

URL="${1:?usage: watch-one-app.sh URL CONTAINER [FAILURES]}"
CONTAINER="${2:?usage: watch-one-app.sh URL CONTAINER [FAILURES]}"
LIMIT="${3:-3}"
case "$LIMIT" in
  ''|*[!0-9]*|0) echo "FAILURES must be a whole number above 0 (got '$LIMIT')" >&2; exit 2 ;;
esac
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/watch-one-app"
COUNT_FILE="$STATE_DIR/$CONTAINER.failures"
mkdir -p "$STATE_DIR"

# -k: a home app with a self-signed certificate still counts as answering.
code="$(curl -s -k -o /dev/null -m 10 -w '%{http_code}' "$URL" 2>/dev/null)"
code="${code:-000}"

if [ "$code" != "000" ] && [ "$code" -lt 500 ] 2>/dev/null; then
  echo 0 > "$COUNT_FILE"
  exit 0
fi

count=$(( $(cat "$COUNT_FILE" 2>/dev/null || echo 0) + 1 ))
echo "$count" > "$COUNT_FILE"
[ "$count" -lt "$LIMIT" ] && exit 0

if [ "$(docker inspect -f '{{.State.Running}}' "$CONTAINER" 2>/dev/null)" = "true" ]; then
  if docker restart "$CONTAINER" >/dev/null 2>&1; then
    echo "$(date '+%F %T') $URL failed $count checks in a row (last: $code); restarted $CONTAINER"
  else
    echo "$(date '+%F %T') $URL failed $count checks in a row (last: $code); couldn't restart $CONTAINER"
  fi
else
  echo "$(date '+%F %T') $URL isn't answering, but $CONTAINER isn't running; leaving it alone"
fi
echo 0 > "$COUNT_FILE"
