#!/bin/bash
# aaden-cli 私有通道看门狗（POS-46 第 2 期）。以 root 由 /etc/cron.d/aaden-cli-watchdog 每 10 分钟跑一次，
# 装在 /usr/local/sbin/aaden-cli-watchdog（self-update 每次切换版本时从新版本的包里拷过去）。
#
# 只管一种情况：/usr/local/bin/aaden-cli 指向 /opt/aaden-cli（私有分发）却坏了 ——
#   ① 连续 2 轮 `aaden-cli --version` 跑不出版本号（每轮失败后隔 30 秒再试一次才算这轮失败），或者
#   ② do-log 连续 12 轮（约 2 小时）没跑完过一次：do-log 每次跑完都会写 /var/tmp/aaden-cli-do-log.<uid>.ok，
#      不管云端、门店后端连没连上都写，所以断网、PHP 挂了都不算。
#      不比墙钟（RTC 没电、NTP 一下跳过去会误判），而是在 /run 下的状态文件里数「.ok 修改时间没变过」的轮数；
#      /run 开机清空，开机后天然有 2 小时宽限。
# 这时如果 npm 装的那份（/usr/local/lib/node_modules/aaden-cli）自己能跑出版本号，就把链接指回它，
# 再用 npm 那份跑一次 `self-update --back-to-npm` 收尾（恢复 crontab、写通道标记、报云端），和下发 CLI_BACK_TO_NPM 一样；
# npm 那份太老、没有这个命令时至少把通道标记写成 npm。npm 那份也跑不起来就什么都不动，只记日志。
# 指向 npm 那份的设备、或者正在 self-update / npm 重装的时候，什么都不做（也不计轮数）。
#
# 只用 shell，不依赖 CLI 本身能不能跑。日志 /var/log/aaden-cli-watchdog.log。
# 只给自测留的口子：AADEN_CLI_BIN / AADEN_CLI_OPT_DIR / AADEN_CLI_CONFIG_DIR / AADEN_CLI_WATCHDOG_*，设备上从不设置。
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
BIN="${AADEN_CLI_BIN:-/usr/local/bin/aaden-cli}"
OPT="${AADEN_CLI_OPT_DIR:-/opt/aaden-cli}"
CONFIG_DIR="${AADEN_CLI_CONFIG_DIR:-/etc/aaden/config}"
LOG="${AADEN_CLI_WATCHDOG_LOG:-/var/log/aaden-cli-watchdog.log}"
DO_LOG_OK_GLOB="${AADEN_CLI_WATCHDOG_DO_LOG_OK_GLOB:-/var/tmp/aaden-cli-do-log.*.ok}"
STALE_ROUNDS="${AADEN_CLI_WATCHDOG_STALE_ROUNDS:-12}"
VERSION_FAIL_ROUNDS="${AADEN_CLI_WATCHDOG_VERSION_FAIL_ROUNDS:-2}"
RETRY_SLEEP="${AADEN_CLI_WATCHDOG_RETRY_SLEEP:-30}"
BACK_TO_NPM_TIMEOUT="${AADEN_CLI_WATCHDOG_BACK_TO_NPM_TIMEOUT:-900}"
# 和 modules/self-update.js 的 LOCK_STALE_MS 一致
SELF_UPDATE_LOCK_STALE=1800
# npm 自己建的就是这个相对链接（相对 /usr/local/bin）
NPM_TARGET=../lib/node_modules/aaden-cli/dist/index.js

# 状态和锁放 /run（开机清空）；没有就退到 /var/tmp
RUN_DIR="${AADEN_CLI_WATCHDOG_RUN_DIR:-}"
if [ -z "$RUN_DIR" ]; then
  for d in /run /var/tmp; do
    if [ -d "$d" ] && [ -w "$d" ]; then RUN_DIR=$d; break; fi
  done
fi
LOCK_DIR="$RUN_DIR"
[ -z "${AADEN_CLI_WATCHDOG_RUN_DIR:-}" ] && [ -d /run/lock ] && [ -w /run/lock ] && LOCK_DIR=/run/lock
LOCK="$LOCK_DIR/aaden-cli-watchdog.lock"
STATE="$RUN_DIR/aaden-cli-watchdog.state"

log() {
  echo "$(date '+%F %T') $*" >>"$LOG"
  # 只留最近 1000 行
  if [ "$(wc -l <"$LOG" 2>/dev/null || echo 0)" -gt 2000 ]; then
    tail -n 1000 "$LOG" >"$LOG.tmp" && mv -f "$LOG.tmp" "$LOG"
  fi
}

# 跑出来的最后一行是不是 x.y.z 版本号
versionOf() {
  timeout 60 node "$1" --version 2>/dev/null | tail -n 1 | tr -d '[:space:]'
}
isVersion() { [[ "$1" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; }

# self-update 正在切版本 / npm 正在装：链接可能正处在中间状态，这一轮不判断
busy() {
  local lock="$OPT/.self-update.lock" holder age
  if [ -f "$lock" ]; then
    age=$(($(date +%s) - $(stat -c %Y "$lock" 2>/dev/null || echo 0)))
    holder=$(cat "$lock" 2>/dev/null)
    # 和 JS 的 lockHolderAlive 一样：30 分钟内、进程命令行里有 self-update-root 才算有人拿着
    if [ "$age" -le "$SELF_UPDATE_LOCK_STALE" ] && [ -n "$holder" ] &&
      tr '\0' ' ' <"/proc/$holder/cmdline" 2>/dev/null | grep -q 'self-update-root'; then
      return 0
    fi
  fi
  pgrep -f 'npm (i|install) (-g )?(--force )?aaden-cli|aaden-cli-install' >/dev/null 2>&1
}

exec 9>"$LOCK" || exit 0
flock -n 9 || exit 0

target=$(readlink "$BIN" 2>/dev/null)
case "$target" in
  "$OPT"/*) ;;
  *) rm -f "$STATE"; exit 0 ;;
esac
busy && exit 0

okMtime=none
staleRounds=0
versionFailRounds=0
# 逐行解析，不 source（退到 /var/tmp 时别人也能写这个文件）；只认数字
if [ -f "$STATE" ]; then
  while IFS='=' read -r k v; do
    [[ "$v" =~ ^[0-9]+$ || ( "$k" = okMtime && "$v" = none ) ]] || continue
    case "$k" in
      okMtime) okMtime=$v ;;
      staleRounds) staleRounds=$v ;;
      versionFailRounds) versionFailRounds=$v ;;
    esac
  done <"$STATE"
fi

ver=$(versionOf "$BIN")
if ! isVersion "$ver"; then
  sleep "$RETRY_SLEEP"
  busy && exit 0
  ver=$(versionOf "$BIN")
fi
if isVersion "$ver"; then versionFailRounds=0; else versionFailRounds=$((versionFailRounds + 1)); fi

newest=none
for f in $DO_LOG_OK_GLOB; do
  [ -f "$f" ] || continue
  t=$(stat -c %Y "$f" 2>/dev/null) || continue
  if [ "$newest" = none ] || [ "$t" -gt "$newest" ]; then newest=$t; fi
done
if [ "$newest" = "$okMtime" ]; then staleRounds=$((staleRounds + 1)); else staleRounds=0; okMtime=$newest; fi

printf 'okMtime=%s\nstaleRounds=%s\nversionFailRounds=%s\n' "$okMtime" "$staleRounds" "$versionFailRounds" >"$STATE"

problem=
if [ "$versionFailRounds" -ge "$VERSION_FAIL_ROUNDS" ]; then
  problem="连续 $versionFailRounds 轮 aaden-cli --version 跑不出版本号（链接 $target，输出「$ver」）"
elif [ "$staleRounds" -ge "$STALE_ROUNDS" ]; then
  problem="do-log 连续 $staleRounds 轮没跑完过（版本 $ver）"
fi
[ -z "$problem" ] && exit 0

npmEntry="$(dirname "$BIN")/$NPM_TARGET"
npmVer=$(versionOf "$npmEntry")
if ! isVersion "$npmVer"; then
  log "$problem；npm 那份也跑不起来（输出「$npmVer」），不动，等人处理"
  exit 1
fi

# 动手前再确认一次：没人在切版本、链接还是刚才看到的那个
if busy || [ "$(readlink "$BIN" 2>/dev/null)" != "$target" ]; then
  log "$problem；但链接刚被别人改过或有人在升级，这一轮不动"
  exit 0
fi
tmp="$BIN.watchdog-$$"
rm -f "$tmp"
if ! { ln -s "$NPM_TARGET" "$tmp" && mv -Tf "$tmp" "$BIN"; }; then
  rm -f "$tmp"
  log "$problem；切回 npm 那份失败，链接没动"
  exit 1
fi
rm -f "$STATE"
mkdir -p "$CONFIG_DIR"
echo "$(date '+%F %T') $problem；已从 $target 切回 npm 那份 $npmVer" >"$CONFIG_DIR/cli-watchdog-fallback"
log "$problem；已切回 npm 那份 $npmVer（原链接 $target），接着跑 self-update --back-to-npm 收尾"

# 收尾和下发 CLI_BACK_TO_NPM 走同一段代码：恢复 crontab、写通道标记、报云端
out=$(timeout "$BACK_TO_NPM_TIMEOUT" node "$npmEntry" self-update --back-to-npm 2>&1)
rc=$?
log "self-update --back-to-npm rc=$rc：$(echo "$out" | tail -n 3 | tr '\n' ' ')"
if [ "$rc" != 0 ]; then
  # npm 那份太老没有这个命令、或者收尾失败：至少把通道标记写成 npm，self-update 不加 --force 不会再切回去
  echo npm >"$CONFIG_DIR/cli-channel"
  log "收尾没成功，通道标记已写 npm；crontab 没动，请人工检查 crontab -l"
  exit 1
fi
exit 0
