#!/bin/bash
# alpha-memguard — keeps stray processes from quietly eating the box's memory.
#
# The one real leak class on this server: HEADLESS CHROME. It is only ever a
# build tool here (mail thumbnails, screenshots, store artwork) and a healthy
# run lives for seconds. When a parent dies mid-run (timeout, killed SSH, a
# crashed script) the browser tree survives — 100-250 MB each, forever. Six of
# those is over a gigabyte doing nothing.
#
#   reap    kill host-namespace headless-Chrome older than MAX_AGE (default
#           15 min) + sweep stale Chromium temp profiles. Run from cron.
#   status  what would be reaped, and the current headless footprint.
#
# ⚠️ Container workloads are NEVER touched: anything whose cgroup says
#   lxc.payload belongs to a guest (VS Code studios, builders) and gets to
#   manage its own memory.
# ⚠️ Matching is on the CMDLINE (chrome + --headless), never a bare name —
#   a user's real browser session has no --headless and is never a match.
set -u

MAX_AGE_S="${MEMGUARD_MAX_AGE:-900}"
LOG=/var/log/alpha-memguard.log

up=$(cut -d. -f1 /proc/uptime)
hz=$(getconf CLK_TCK)

# emit "pid age_s rss_kb" for every host-namespace headless-chrome
candidates() {
  local p pid cmd st age rss
  for p in /proc/[0-9]*; do
    pid=${p#/proc/}
    [ "$pid" = "$$" ] && continue
    # braces so a vanished pid's failed open is silenced too, not just tr
    cmd=$( { tr '\0' ' ' < "$p/cmdline"; } 2>/dev/null ) || continue
    [ -n "$cmd" ] || continue
    case "$cmd" in *chrom*) ;; *) continue;; esac
    case "$cmd" in *--headless*) ;; *) continue;; esac
    grep -q 'lxc.payload' "$p/cgroup" 2>/dev/null && continue
    # starttime is field 20 AFTER stripping "pid (comm) " — comm may hold spaces
    st=$(sed 's/^[0-9]* ([^)]*) //' "$p/stat" 2>/dev/null | awk '{print $20}') || continue
    [ -n "$st" ] || continue
    age=$(( up - st / hz ))
    rss=$(awk '/VmRSS/ {print $2}' "$p/status" 2>/dev/null)
    echo "$pid $age ${rss:-0}"
  done
}

case "${1:-status}" in
  reap)
    reaped=0; freed_kb=0
    while read -r pid age rss; do
      [ "$age" -ge "$MAX_AGE_S" ] || continue
      kill -TERM "$pid" 2>/dev/null && { reaped=$((reaped+1)); freed_kb=$((freed_kb+rss)); }
    done < <(candidates)
    if [ "$reaped" -gt 0 ]; then
      sleep 3
      # hard-kill anything that ignored the TERM
      while read -r pid age rss; do
        [ "$age" -ge "$MAX_AGE_S" ] && kill -KILL "$pid" 2>/dev/null
      done < <(candidates)
      echo "$(date '+%F %T') reaped $reaped stale headless-chrome, ~$((freed_kb / 1024)) MB freed" >> "$LOG"
    fi
    # stale one-shot Chromium profiles under /tmp (a run's scratch, never data)
    find /tmp -maxdepth 1 \( -name '.org.chromium.*' -o -name '.com.google.Chrome*' \) -mmin +120 -exec rm -rf {} + 2>/dev/null
    # keep the log from becoming its own leak
    [ -f "$LOG" ] && [ "$(stat -c%s "$LOG" 2>/dev/null || echo 0)" -gt 524288 ] && tail -n 200 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
    echo "OK reaped=$reaped freed_mb=$((freed_kb / 1024))"
    ;;

  status)
    total=0; total_kb=0; old=0; old_kb=0
    while read -r pid age rss; do
      total=$((total+1)); total_kb=$((total_kb+rss))
      if [ "$age" -ge "$MAX_AGE_S" ]; then old=$((old+1)); old_kb=$((old_kb+rss)); fi
    done < <(candidates)
    echo "headless-chrome: $total procs, $((total_kb / 1024)) MB — stale (>${MAX_AGE_S}s): $old procs, $((old_kb / 1024)) MB"
    ;;

  *)
    echo "usage: alpha-memguard {reap|status}"; exit 2;;
esac
