#!/usr/bin/env bash
# GPU 驱动错配重启哨兵 v2 (2026-10-11 修复提案, 勿直接覆盖生产脚本)
# 根因: unattended-upgrades 06:53 升级 nvidia 580.173.02→580.178.04, 内核模块未重载
#       → nvidia-smi NVML mismatch (exit 18) → 用量页 GPU 全 0 + 新 CUDA 进程不可启动
# 修复: 等 GPU 空闲窗口 → 两段式重启(arm→fire, 5min取消窗) → 开机后对账 → 自清理 cron
# 模式A: 空闲探测(每tick) → KAP租约空×连续2次 + ComfyUI三实例队列空 + 宿主无杂散GPU进程
#        第1次空闲=ARMED+预告; 下一tick仍空闲且无ABORT=执行 reboot
# 模式B: 开机后检测到「reboot 已发生」(boot_id 变化) 且存在 MARKER → 对账(轮询≤4min)
#        → 全绿=终报+自清理; 任何终态(全绿/迟到失败/超时)都收敛 MARKER 生命周期
# 纪律: 看门狗静默模式(无事不响); 探测失败≠空闲(空租约表≠unknown教训); cron脚本禁放/tmp
#
# ==== 2026-10-11 事故修订 (kais-engine 黑屏+B0 8h22m RCA) ====
# [FIX-A] Bug A: 旧版 ARM 时写 `uptime` 人文输出到 pre-reboot-uptime.txt, 对账端
#         `awk '{print int($1)}' 取到的是 "01:00:25" 的前导数字 1(秒), 不是 /proc/uptime
#         秒数 → `UP > PRE_UP-60` 永真 → 永远判「同boot」→ 模式B对账永不执行
#         (实证: 状态目录残留文件首字段 " 09:45:06 up 6 min...")
#         修复: 快照一律存 `awk '{print $1}' /proc/uptime` 的纯秒数; 并新增
#         pre-reboot-bootid.txt (内核 boot_id) 作为「reboot 已发生」的主判定锚点
#         (单调、免解析、免时钟干扰)。
# [FIX-B] Bug B: MARKER 一旦存在且 24h 内, 模式A空闲双tick即再次 reboot, 没有
#         「本次 reboot 是否已成功发生」判定 → 09:45 误重ARM, 10:00 仅因 GPU busy
#         侥幸未 FIRE。修复: ①「reboot 已发生」三重判定(boot_id 主判 + MARKER mtime
#         早于本boot起点兜底 + 秒数快照校验); ②FIRING 时自写 MARKER+快照(自发重启
#         也走对账); ③对账任何终态必收敛(删)MARKER——失败只报告, 绝不触发再次
#         reboot; ④同boot悬空标记仍按 2026-10-10 22:5x 语义落入模式A自主补发,
#         但 ARM/FIRING 会刷新快照, 下一轮对账必然可判定。
set -u
JOB_ID="0b4b077d307f"
STATE_DIR="/home/kai/.hermes/state/gpu-reboot-sentinel"
HERMES_CLI="/home/kai/workspace/hermes-agent/.venv/bin/hermes"
MARKER="$STATE_DIR/reboot-initiated"      # arm/fire 时写, 对账任意终态后删
ARMED="$STATE_DIR/armed"                   # 预告已发, 下一tick执行
ABORT="$STATE_DIR/ABORT"                   # 存在=取消本次重启(由Hermes会话或Kai触碰)
PARKED="$STATE_DIR/parked"                 # 连续unknown≥6次后停车, mtime>6h自动过期
UNKNOWN_N="$STATE_DIR/unknown-count"
LOG="$STATE_DIR/sentinel.log"
# [FIX-A] 新增快照: 重启前 boot_id + uptime 秒数(纯数字)
BOOTID_SNAP="$STATE_DIR/pre-reboot-bootid.txt"
UPTIME_SNAP="$STATE_DIR/pre-reboot-uptime.txt"
mkdir -p "$STATE_DIR"
log(){ echo "$(date '+%F %T') $*" >> "$LOG"; }

# [FIX-A] 快照写入: 秒数(非uptime人文输出) + boot_id。ARM 与 FIRING 各写一次。
write_snapshots(){
  awk '{print int($1)}' /proc/uptime > "$UPTIME_SNAP"
  cat /proc/sys/kernel/random/boot_id   > "$BOOTID_SNAP" 2>/dev/null || true
}

# ---------- 探测 ----------
kap_idle(){ # 0=idle 3=busy 4=unknown
  local out; out=$(curl -s --noproxy '*' -m 8 http://127.0.0.1:10588/api/production/gpu-queue 2>/dev/null)
  [[ -z "$out" ]] && return 4
  python3 - "$out" <<'PY'
import json,sys
try:
    d=json.loads(sys.argv[1]); data=d.get('data',d)
    holders=data.get('holders') or {}
    waiting=data.get('waitingByEngine') or {}
    # 2026-10-10: 外部租约(ugq.leases, render_p11b acquire-batch 层)是独立于 holders 的
    # 占卡面 — 镜间空档 holders 可为空但租约仍持, 漏查会误判空闲炸掉在跑战役
    leases=(data.get('ugq') or {}).get('leases') or []
    busy=[k for k,v in holders.items() if v] or waiting or leases
    sys.exit(3) if busy else sys.exit(0)
except Exception:
    sys.exit(4)
PY
}
comfy_idle(){ # $1=port; 0=idle 3=busy 4=unknown
  local out; out=$(curl -s --noproxy '*' -m 8 "http://127.0.0.1:$1/queue" 2>/dev/null)
  [[ -z "$out" ]] && return 4
  python3 - "$out" <<'PY'
import json,sys
try:
    d=json.loads(sys.argv[1])
    sys.exit(3) if (d.get('queue_running') or d.get('queue_pending')) else sys.exit(0)
except Exception:
    sys.exit(4)
PY
}
host_guard(){ # 宿主杂散GPU进程(签名在py文件内, 防pgrep自匹配); 0=clean 3=busy
  python3 /home/kai/.hermes/scripts/gpu_reboot_hostguard.py >/dev/null 2>&1
  return $?
}

all_idle(){
  kap_idle || return $?
  local p
  for p in 8188 8189 8190; do comfy_idle "$p" || return $?; done
  host_guard || return $?
  return 0
}

# ---------- 模式B: 重启后对账 ----------
if [[ -f "$MARKER" ]]; then
  UP=$(awk '{print int($1)}' /proc/uptime)
  # [FIX-A] 判定「reboot 已发生」: ①boot_id 快照主判(权威, 单调)
  BOOT_NOW=$(cat /proc/sys/kernel/random/boot_id 2>/dev/null || true)
  BOOT_SAVED=$(cat "$BOOTID_SNAP" 2>/dev/null || true)
  # [FIX-A] 秒数快照解析: 旧版人文格式(" 09:45:06 up 6 min")一律判为不可信
  PRE_UP_RAW=$(tr -d '[:space:]' < "$UPTIME_SNAP" 2>/dev/null | head -c 16)
  case "$PRE_UP_RAW" in ''|*[!0-9]*) PRE_UP=999999999 ;; *) PRE_UP=$PRE_UP_RAW ;; esac
  NEW_BOOT=0; DETECT="bootid"
  if [[ -n "$BOOT_SAVED" && -n "$BOOT_NOW" ]]; then
    [[ "$BOOT_NOW" != "$BOOT_SAVED" ]] && NEW_BOOT=1
  else
    # [FIX-B] 兜底①: MARKER 创建时间早于当前 boot 墙钟起点 => 标记之后发生过重启
    DETECT="mtime"
    MK_MTIME=$(stat -c %Y "$MARKER" 2>/dev/null || echo 0)
    BOOT_START=$(( $(date +%s) - UP ))
    (( MK_MTIME > 0 && MK_MTIME < BOOT_START )) && NEW_BOOT=1
    # 兜底②: 可信秒数快照反证(UP 比重启前还小 60s 以上)
    if (( NEW_BOOT == 0 && PRE_UP != 999999999 && UP <= PRE_UP - 60 )); then
      NEW_BOOT=1; DETECT="uptime-sec"
    fi
  fi
  if (( NEW_BOOT == 0 )); then
    # 同 boot(重启已下令但尚未发生): 2026-10-10 22:5x 语义升级 — 不再静默等待。
    # 背景: 17:15 的 DIRECT-REBOOT 被会话执行器硬线闸拦截, journal 佐证 reboot 从未
    # 到达 systemd, 旧「静默等待」语义导致重启悬空 5h+。现落入模式A空闲探测自主补发:
    # busy=静默让路; 空闲=arm→fire 双tick(带取消窗); ABORT 文件=取消。
    # [FIX-B] 注意: 仅在「判定=同boot」时才允许补发; boot_id 一旦证明重启已发生,
    # 走下面的对账分支, 绝不回到模式A —— 杜绝 09:45 式的「重启后误重ARM」。
    if [[ "$DETECT" == "mtime" ]]; then
      log "MODE-B snapshot legacy/missing (no bootid), mtime-based same-boot verdict"
    fi
    age=$(( $(date +%s) - $(stat -c %Y "$MARKER") ))
    if (( age > 86400 )); then
      log "MARKER_STALE_24H drop (reboot never happened)"
      rm -f "$MARKER"
    fi
    # 故意不 exit — 继续向下走模式A
  else
  log "MODE-B reconcile start uptime=${UP}s detect=$DETECT"
  # 开机过久仍未全绿 → 收敛(只报不重启)
  if (( UP > 1800 )); then
    echo "❌ 驱动重启哨兵: 开机已超30分钟对账仍未全绿，需人工检查。日志: $LOG"
    log "RECONCILE_TIMEOUT uptime=${UP}s"
    rm -f "$MARKER"; exit 0
  fi
  # 轮询等待服务就绪 ≤240s
  local_wait=0
  while (( local_wait < 240 )); do
    if nvidia-smi -L >/dev/null 2>&1; then break; fi
    sleep 15; local_wait=$((local_wait+15))
  done
  FAIL=()
  # 1) NVML + 驱动版本
  if nvidia-smi -L >/dev/null 2>&1; then
    DV=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader 2>/dev/null | head -1)
    NG=$(nvidia-smi -L 2>/dev/null | wc -l)
    [[ "$DV" == "580.178.04" && "$NG" -ge 3 ]] || FAIL+=("驱动版本/GPU数异常: v=$DV n=$NG")
  else FAIL+=("nvidia-smi 仍报错(NVML mismatch未解除)"); fi
  # 2) KAP
  KQ=$(curl -s --noproxy '*' -m 8 http://127.0.0.1:10588/api/production/gpu-queue 2>/dev/null)
  echo "$KQ" | grep -q '"code":200' || FAIL+=("KAP :10588 gpu-queue 无响应")
  # 3) ComfyUI 三实例
  for p in 8188 8189 8190; do
    curl -s --noproxy '*' -m 8 "http://127.0.0.1:$p/queue" 2>/dev/null | grep -q queue_running || FAIL+=("ComfyUI :$p /queue 无响应")
  done
  # 4) 容器对账(与重启前快照比)
  EXP_CONTAINERS="$STATE_DIR/pre-reboot-docker.txt"
  if [[ -f "$EXP_CONTAINERS" ]]; then
    DOWN=""
    while IFS=$'\t' read -r name _st; do
      st=$(docker ps --filter "name=^${name}$" --format '{{.Status}}' 2>/dev/null)
      if [[ "$st" != Up* ]]; then
        docker start "$name" >/dev/null 2>&1; sleep 5
        st=$(docker ps --filter "name=^${name}$" --format '{{.Status}}' 2>/dev/null)
        [[ "$st" == Up* ]] || DOWN+="$name "
      fi
    done < "$EXP_CONTAINERS"
    [[ -n "$DOWN" ]] && FAIL+=("容器未恢复: $DOWN")
  else FAIL+=("缺重启前容器快照, 跳过容器对账"); fi
  # 5) systemd 关键服务
  for svc in usage-dashboard agent-status-dashboard kais-aigc-platform; do
    systemctl is-active --quiet "$svc" || FAIL+=("systemd $svc 非active")
  done
  # 6) 用量页 GPU 数据新鲜度(temp>0 = 真读到了)
  LIVE=$(curl -s --noproxy '*' -m 8 http://127.0.0.1:8095/api/metrics 2>/dev/null | python3 -c "
import json,sys
try:
    d=json.load(sys.stdin)
    ts=[g for g in d.get('gpus',[]) if (g.get('temp') or 0)>0]
    print(len(ts))
except Exception: print(-1)" 2>/dev/null)
  [[ "$LIVE" == "3" ]] || FAIL+=("用量页GPU数据未恢复(temp全0/解析失败): live=$LIVE/3")
  # ---- 判定 ---- [FIX-B] 任意终态都删 MARKER: 对账失败≠允许再次重启
  if [[ ${#FAIL[@]} -eq 0 ]]; then
    echo "✅ 驱动重启对账完成，全绿。"
    echo ""
    echo "| 项 | 结果 |"
    echo "|---|---|"
    echo "| nvidia-smi | 正常, 驱动 ${DV}, ${NG} 卡 |"
    echo "| KAP :10588 | code 200 |"
    echo "| ComfyUI 8188/8189/8190 | 全部响应 |"
    echo "| 容器 | 重启前 $(wc -l < "$EXP_CONTAINERS") 个全部 Up |"
    echo "| systemd | usage-dashboard / agent-status-dashboard / kais-aigc-platform active |"
    echo "| 用量页 :8095 | GPU 数据实时恢复(temp>0 3/3) |"
    echo ""
    echo "下一步: 无需操作。桌面会话(Chrome等)需手动重新打开。"
    log "RECONCILE_OK driver=$DV gpus=$NG"
    rm -f "$MARKER"
    # 自清理(venv绝对路径+jobs.json复验, 禁裸hermes/禁吞错)
    "$HERMES_CLI" cron remove "$JOB_ID" >> "$LOG" 2>&1
    python3 - "$JOB_ID" <<'PY' | grep -q REMOVED_CONFIRMED || echo "⚠ 自清理未确认, 请手动: hermes cron remove $JOB_ID"
import json,sys
try:
    d=json.load(open('/home/kai/.hermes/cron/jobs.json'))
    jobs=d if isinstance(d,list) else d.get('jobs',[])
    print('REMOVED_CONFIRMED' if not any(j.get('id')==sys.argv[1] for j in jobs) else 'STILL_PRESENT')
except Exception as e: print('STILL_PRESENT',e)
PY
  elif (( UP > 1500 )); then
    echo "❌ 驱动重启对账未全绿(开机${UP}s), 残留:"
    printf '  - %s\n' "${FAIL[@]}"
    echo "日志: $LOG"
    log "RECONCILE_LATE_FAIL ${FAIL[*]}"
    rm -f "$MARKER"
  else
    log "RECONCILE_PARTIAL(静默重试) ${FAIL[*]}"
    # 未到终态: 保留 MARKER 下一tick重试, 由上面 UP>1800 兜底强制收敛
  fi
  exit 0
  fi
fi

# ---------- 模式A: 空闲探测 ----------
if [[ -f "$ABORT" ]]; then
  rm -f "$ARMED" "$ABORT"
  log "ABORTED by operator"
  exit 0
fi
all_idle; rc=$?
if [[ $rc -eq 4 ]]; then
  N=$(cat "$UNKNOWN_N" 2>/dev/null || echo 0); N=$((N+1)); echo "$N" > "$UNKNOWN_N"
  log "PROBE_UNKNOWN x$N"
  if (( N >= 6 )) && [[ ! -f "$PARKED" ]]; then
    touch "$PARKED"
    echo "⚠ 驱动重启哨兵: 连续30分钟探测失败(KAP/ComfyUI不可达), 已停车不重启, 需人工判断。日志: $LOG"
  fi
  exit 0
fi
echo 0 > "$UNKNOWN_N"
if [[ -f "$PARKED" ]]; then
  # 过期判定: mtime>6h 自动复活
  age=$(( $(date +%s) - $(stat -c %Y "$PARKED") ))
  (( age > 21600 )) && rm -f "$PARKED" || exit 0
fi
if [[ $rc -eq 3 ]]; then
  # 又忙了: 若ARMED挂起则撤回(静默, busy即事实取消)
  [[ -f "$ARMED" ]] && { rm -f "$ARMED"; log "DISARMED (busy again before fire)"; }
  exit 0
fi
# ---- rc=0 空闲确认 ----
if [[ ! -f "$ARMED" ]]; then
  touch "$ARMED"
  docker ps --format '{{.Names}}\t{{.Status}}' > "$STATE_DIR/pre-reboot-docker.txt" 2>/dev/null
  write_snapshots   # [FIX-A] ARM 即写 秒数+boot_id 快照(即使 FIRE 前掉电也状态自洽)
  log "ARMED (idle confirmed)"
  echo "🔁 GPU空闲窗口确认(KAP租约空 + ComfyUI三实例队列空 + 宿主无杂散GPU进程)，下个探测点(≤15分钟)自动重启工作站对齐驱动 580.178.04。"
  echo ""
  echo "⚠ 会在重启前保留容器清单做开机对账。要取消请直接回复本条消息说一声。"
  echo "涉及: 桌面会话会断开, ComfyUI×3/KAP/用量页自动恢复(开机后自动对账回报)。"
  exit 0
fi
# ---- ARMED 且本tick仍空闲 → 执行重启 ----
log "FIRING reboot (double-check idle ok)"
rm -f "$ARMED"
touch "$MARKER"        # [FIX-B] 自发重启也必须留标记: 旧版只有外部(17:15式)写入,
write_snapshots        # [FIX-A] 刷新 秒数+boot_id, 保证下一 boot 对账可判定
sync
echo "🔁 取消窗口已过且仍空闲, 现在重启工作站(驱动 580.173.02→580.178.04 对齐)。开机后自动对账回报。"
sleep 2
sudo -n systemctl reboot
# reboot 发起后脚本大概率被杀, 到不了这里
exit 0
