#!/usr/bin/env bash
# Watch a single GPU for foreign processes (anyone other than the current
# user) appearing during a long-running test. Intended companion to
# `/tir-test`: leave this running in a side terminal while pytest runs, and
# it will alert if someone else lands on the same GPU.
#
# Usage:
#   monitor_gpu.sh                       # uses $CUDA_VISIBLE_DEVICES, defaults to 0
#   monitor_gpu.sh --gpu 3               # watch GPU 3
#   monitor_gpu.sh --gpu 3 --interval 2  # poll every 2 seconds
#   monitor_gpu.sh --log /tmp/gpu.log    # also tee to a log file

# Note: deliberately not `set -u` — bash <5.2 errors on `${#assoc[@]}` when
# the associative array is empty.

GPU=""
INTERVAL=5
LOG=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --gpu) GPU="$2"; shift 2 ;;
    --interval) INTERVAL="$2"; shift 2 ;;
    --log) LOG="$2"; shift 2 ;;
    -h|--help)
      sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//'
      exit 0 ;;
    *) echo "unknown arg: $1" >&2; exit 2 ;;
  esac
done

if [[ -z "$GPU" ]]; then
  GPU="${CUDA_VISIBLE_DEVICES:-0}"
fi
# Only the first index if CUDA_VISIBLE_DEVICES is a list.
GPU="${GPU%%,*}"
if ! [[ "$GPU" =~ ^[0-9]+$ ]]; then
  echo "monitor_gpu: GPU must be an integer index (got '$GPU'); pass --gpu <n>" >&2
  exit 2
fi

ME="$(id -un)"

emit() {
  local line="[$(date +'%H:%M:%S')] $*"
  if [[ -n "$LOG" ]]; then
    printf '%s\n' "$line" | tee -a "$LOG" >&2
  else
    printf '%s\n' "$line" >&2
  fi
}

# Returns "pid|user|mem_mib|process_name" lines for compute apps on $GPU.
snapshot() {
  nvidia-smi --id="$GPU" \
    --query-compute-apps=pid,process_name,used_memory \
    --format=csv,noheader,nounits 2>/dev/null \
  | while IFS=, read -r pid pname mem; do
      pid="${pid// /}"
      [[ -z "$pid" ]] && continue
      local user
      user="$(ps -o user= -p "$pid" 2>/dev/null | tr -d ' ')"
      [[ -z "$user" ]] && user="?"
      pname="${pname# }"
      mem="${mem# }"
      printf '%s|%s|%s|%s\n' "$pid" "$user" "$mem" "$pname"
    done
}

emit "monitor_gpu started: GPU=$GPU interval=${INTERVAL}s user=$ME"

declare -A KNOWN  # pid -> "user|mem|pname"

# Initial snapshot — record everyone we already see as the baseline.
while IFS='|' read -r pid user mem pname; do
  [[ -z "${pid:-}" ]] && continue
  KNOWN[$pid]="$user|$mem|$pname"
  flag=""
  [[ "$user" != "$ME" ]] && flag=" [FOREIGN]"
  emit "baseline pid=$pid user=$user mem=${mem}MiB cmd=$pname$flag"
done < <(snapshot)

if [[ ${#KNOWN[@]} -eq 0 ]]; then
  emit "baseline: GPU $GPU is idle"
fi

trap 'emit "monitor_gpu stopped"; exit 0' INT TERM

heartbeat_due=$(( $(date +%s) + 60 ))

while :; do
  sleep "$INTERVAL"

  declare -A SEEN=()
  while IFS='|' read -r pid user mem pname; do
    [[ -z "${pid:-}" ]] && continue
    SEEN[$pid]=1
    if [[ -z "${KNOWN[$pid]:-}" ]]; then
      flag=""
      [[ "$user" != "$ME" ]] && flag=" *** FOREIGN USER ***"
      emit "NEW pid=$pid user=$user mem=${mem}MiB cmd=$pname$flag"
      KNOWN[$pid]="$user|$mem|$pname"
    fi
  done < <(snapshot)

  for pid in "${!KNOWN[@]}"; do
    if [[ -z "${SEEN[$pid]:-}" ]]; then
      emit "GONE pid=$pid (was: ${KNOWN[$pid]})"
      unset 'KNOWN[$pid]'
    fi
  done
  unset SEEN

  now=$(date +%s)
  if (( now >= heartbeat_due )); then
    foreign=0
    for v in "${KNOWN[@]}"; do
      u="${v%%|*}"
      [[ "$u" != "$ME" ]] && foreign=$((foreign+1))
    done
    emit "heartbeat: ${#KNOWN[@]} process(es) on GPU $GPU (${foreign} foreign)"
    heartbeat_due=$(( now + 60 ))
  fi
done
