#!/system/bin/sh
# On-device supervisor: keeps tb-agent running forever. Started at boot by the
# persistence hooks, or manually by provision.sh.
#
# Design rule: THIS SCRIPT MUST NEVER EXIT on its own. Nothing restarts it until
# the next boot (the boot hooks fire once), so every "impossible" exit path here
# is a device that stays offline until someone drives to it. Anything that used
# to be fatal now waits and retries instead.
DIR=/data/local/tmp
LOG="$DIR/agent.log"
HEARTBEAT="$DIR/tb-agent.alive"
LOCK="$DIR/tb-supervisor.lock"

# Keep the low-memory killer away from us and from the agent. Under memory
# pressure Android kills by oom_score_adj, and a killed supervisor is not
# restarted by anything — which looks exactly like the boot-persistence bug but
# happens hours after boot. oom_adj is the pre-3.14 kernel spelling (TB2).
protect_oom() {
  [ -n "$1" ] || return 0
  echo -1000 > /proc/"$1"/oom_score_adj 2>/dev/null \
    || echo -17 > /proc/"$1"/oom_adj 2>/dev/null || true
}

# Single-instance lock. mkdir is atomic (safe if two boot hooks fire at once).
# The lock lives in /data and SURVIVES reboots, so a stored pid may be reused by
# an unrelated process after boot — we must verify the holder is a LIVE
# tb-supervisor before honoring it, else a reused pid makes us skip autostart.
# (That was the flaky-autorun bug: worked one boot, not the next.)
supervisor_alive() {  # $1=pid — true only if it's a live tb-supervisor process
  [ -n "$1" ] || return 1
  kill -0 "$1" 2>/dev/null || return 1
  cl="$(cat /proc/"$1"/cmdline 2>/dev/null | busybox tr -d '\0')"
  case "$cl" in *tb-supervisor*) return 0 ;; esac
  [ -n "$cl" ] && return 1   # readable and it's somebody else -> stale pid, reclaim
  # Unreadable cmdline: Android 11 restricts /proc for processes we don't own,
  # so "can't tell" is a normal answer there rather than an error. Ask ps before
  # deciding. When even that is unusable we return false and reclaim the lock:
  # a duplicate supervisor is noisy, but honoring a stale lock leaves the device
  # offline until someone drives out to it — which is the exact failure this
  # lock was introduced to prevent.
  busybox ps 2>/dev/null | busybox grep -v grep | busybox grep -q "^ *$1 .*tb-supervisor" && return 0
  return 1
}
if ! mkdir "$LOCK" 2>/dev/null; then
  if supervisor_alive "$(cat "$LOCK/pid" 2>/dev/null)"; then
    echo "supervisor already running"
    exit 0
  fi
  rm -rf "$LOCK" 2>/dev/null; mkdir "$LOCK" 2>/dev/null || exit 0  # reclaim stale lock
fi
echo $$ > "$LOCK/pid"
rm -f "$DIR/tb-supervisor.pid" 2>/dev/null  # drop the legacy pidfile
APID=""
# ONLY clear the lock on exit — do NOT kill the agent here. This trap fires on
# SIGTERM too, and on TB2 (Android 5.1) the system sends SIGTERM to background
# processes under memory pressure. Killing the agent from the trap took the
# whole thing down — and nothing restarts the supervisor on TB2, where the boot
# hooks fire once and there is no init.rc loop (that only exists on TB40). An
# orphaned agent, by contrast, keeps running and keeps the device online; that
# was the pre-audit behaviour, reliable for months. device-update.sh already
# pkills the agent itself when it relaunches, so we never rely on this trap to
# reap it.
trap 'rm -rf "$LOCK" 2>/dev/null' EXIT
protect_oom $$

# Config (server url / id / token). WAIT for it — never exit. This used to be
# `: "${TB_SERVER:?...}"`, which killed the supervisor outright; a config file
# that was briefly unreadable (device-update.sh rewriting it, /data not mounted
# yet on a cold boot) therefore took the device offline until the next reboot.
# Waiting inside the lock means only one instance ever sits here.
waited=0
while :; do
  [ -f "$DIR/tb-agent.env" ] && . "$DIR/tb-agent.env"
  [ -n "$TB_SERVER" ] && [ -n "$TB_ID" ] && [ -n "$TB_TOKEN" ] && break
  [ "$waited" = 0 ] && echo "waiting for $DIR/tb-agent.env (server/id/token)…"
  waited=$((waited + 1))
  sleep 10
done
[ "$waited" = 0 ] || echo "config appeared after ${waited}0s"

# self-heal: (re)install boot hooks an OTA may have wiped. Once, before the loop
# (never inside it — a crash-looping agent must not hammer /system remounts).
[ -f "$DIR/tb-install-persistence.sh" ] && sh "$DIR/tb-install-persistence.sh" 2>/dev/null

# Cold boot only: give DHCP / Wi-Fi association a moment before the first dial.
# Gated on uptime on purpose — device-update.sh relaunches this script and then
# checks 3s later whether the agent came up, so an unconditional sleep would
# make every manual update report a false failure.
up="$(busybox cut -d. -f1 /proc/uptime 2>/dev/null)"
case "$up" in ''|*[!0-9]*) up=99999 ;; esac
if [ "$up" -lt 120 ]; then
  echo "cold boot (up ${up}s) — settling 5s before first dial"
  sleep 5
fi

# A HUNG agent never exits, so respawn-on-exit alone would leave the device
# offline forever behind a healthy-looking process. The agent stamps $HEARTBEAT
# from its dial loop and from each ping, so a stale stamp means no loop in it is
# turning any more. Missing file = an agent too old to stamp: never kill on that.
HB_STALE_MIN=5
hb_stale() {
  [ -f "$HEARTBEAT" ] || return 1
  [ -n "$(busybox find "$HEARTBEAT" -mmin "+$HB_STALE_MIN" 2>/dev/null)" ]
}

while true; do
  # keep the log bounded so months of respawns can't fill /data
  [ -f "$LOG" ] && [ "$(wc -c < "$LOG" 2>/dev/null || echo 0)" -gt 1000000 ] && : > "$LOG"
  echo "=== agent start $(date 2>/dev/null) ===" >> "$LOG" 2>/dev/null
  # Drop the previous run's stamp: a stale one would make the watchdog shoot the
  # fresh agent before it has had a chance to write its first beat.
  rm -f "$HEARTBEAT" 2>/dev/null

  "$DIR/tb-agent" -server "$TB_SERVER" -id "$TB_ID" -token "$TB_TOKEN" -dir "$DIR" >> "$LOG" 2>&1 &
  APID=$!
  protect_oom "$APID"

  # Poll fast: this loop is also how we notice the agent EXITING, and the old
  # code restarted within 3s by blocking on it. A slow poll here would quietly
  # turn every crash into a half-minute outage. The heartbeat check is a single
  # stat, so running it at the same rate costs nothing.
  while kill -0 "$APID" 2>/dev/null; do
    sleep 5
    if hb_stale; then
      echo "=== agent wedged (no heartbeat for ${HB_STALE_MIN}m) — killing $APID ===" >> "$LOG" 2>&1
      kill -9 "$APID" 2>/dev/null
      break
    fi
  done
  wait "$APID" 2>/dev/null
  sleep 3
done
