#!/bin/bash
# dshd-supervisor — dsh 保命外壳(L0)
# 职责:探活主 dsh(3080) · 自动拉起 · 卡死/崩溃检测 · 机械回滚重启(不依赖 LLM)·
# 唤醒 standby(备用大脑/人工窗口)。
# standby 平时是磁盘副本(零进程/零端口),只在进化切流与故障修复时被临时唤醒。
#
# 用法:
# dshd-supervisor start 拉起 dsh web(若未在跑;复用 dshd-start 常驻逻辑)
# dshd-supervisor stop 停止主 dsh
# dshd-supervisor status 三合一探活主 + standby 状态
# dshd-supervisor restart 重启主并等健康
# dshd-supervisor monitor 前台轮询循环(检测故障 → 机械恢复;nohup/launchd 常驻)
# dshd-supervisor recover 立即执行一次机械恢复(回滚 last-known-good 再拉起)
# dshd-supervisor wake-standby 唤醒 standby 为临时 dsh 实例(@3081)
# dshd-supervisor stop-standby 停止已唤醒的 standby
#
# launchd 常驻示例(~/Library/LaunchAgents/com.iroam.dshd-supervisor.plist):
# ProgramArguments/Users/iroam/.local/bin/dshd-supervisormonitor
# RunAtLoad (KeepAlive 可选:supervisor 自己挂了 launchd 拉起)
set -u
HOME_DIR="$HOME"
DSH_HOME="${DSH_HOME:-$HOME/.dsh}"
PORT="${DSH_MAIN_PORT:-3080}"
STANDBY_PORT="${DSH_STANDBY_PORT:-3082}"
DOUBLE_DIR="$DSH_HOME/double"
LOG="$DOUBLE_DIR/supervisor.log"
DDOUBLE="$HOME/.local/bin/dsh-double.py"
mkdir -p "$DOUBLE_DIR"
# 主 = `dsh web` 行尾(无 --port);试验田/试启动带 --port N,绝不能算主。
PAT='dsh web$'
PROBE_FAILS=2 # 连续探活失败次数达到即判不可用
RESTART_COOLDOWN=60 # 恢复后冷却秒数,防止重启风暴
RECOVER_MARK="$DOUBLE_DIR/.last-recover"
now() { date '+%Y-%m-%d %H:%M:%S'; }
evlog() { echo "[$(now)] $*" | tee -a "$LOG"; }
# 三合一探活:TCP + HTTP + pgrep,任一不过即不健康
probe() {
local port="${1:-$PORT}"
nc -z 127.0.0.1 "$port" >/dev/null 2>&1 || return 1
local code
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 3 "http://127.0.0.1:$port/" 2>/dev/null)
[ "$code" = "200" ] || return 1
pgrep -f "$PAT" >/dev/null 2>&1 || return 1
return 0
}
# standby 是否唤醒中
probe_standby() { probe "$STANDBY_PORT"; }
main_pids() { pgrep -f "$PAT" | tr '\n' ' '; }
start_main() {
if probe; then
evlog "start: 主 dsh 已在运行 pid=$(main_pids)"
return 0
fi
"$HOME/.local/bin/dshd-start" start
sleep 4
if probe; then
evlog "start: 主 dsh 已拉起 pid=$(main_pids)"
else
evlog "start: 拉起后仍不可用(tail -20 $DSH_HOME/web-daemon.log)"
return 1
fi
}
stop_main() { "$HOME/.local/bin/dshd-start" stop; }
# 机械恢复:回滚 last-known-good → 重启主 → 探活确认
recover() {
local why="$1"
local ts
ts=$(date +%s)
evlog "recover: 触发($why)"
# 冷却:60s 内已恢复过且又挂 → 判 bad config,回滚
if [ -f "$RECOVER_MARK" ]; then
local last=$(cat "$RECOVER_MARK" 2>/dev/null || echo 0)
local age=$((ts - last))
if [ "$age" -lt "$RESTART_COOLDOWN" ]; then
evlog "recover: ${age}s 内已恢复过又挂 → 判定 bad config,回滚 last-known-good"
"$DDOUBLE" rollback && evlog "recover: 已回滚 last-known-good"
fi
fi
echo "$ts" > "$RECOVER_MARK"
evlog "recover: 重启主…"
stop_main; sleep 2
"$HOME/.local/bin/dshd-start" start >/dev/null 2>&1
sleep 6
if probe; then
evlog "recover: ✅ 主恢复健康 pid=$(main_pids)"
return 0
fi
evlog "recover: 机械恢复后仍不健康 → 再次回滚 + 重启"
"$DDOUBLE" rollback
stop_main; sleep 2
"$HOME/.local/bin/dshd-start" start >/dev/null 2>&1
sleep 8
if probe; then
evlog "recover: ✅ 回滚后主恢复健康 pid=$(main_pids)"
return 0
fi
evlog "recover: ❌ 机械恢复失败 —— 唤醒 standby 备用大脑 @$STANDBY_PORT(dshd-supervisor wake-standby 或 dsh-double.py wake)"
"$DDOUBLE" wake --port "$STANDBY_PORT" >/dev/null 2>&1 && \
evlog "recover: standby 已唤醒 @127.0.0.1:$STANDBY_PORT(备用大脑/人工窗口)"
return 1
}
case "${1:-status}" in
start) start_main ;;
stop) stop_main; echo "已停止" ;;
status)
if probe; then
echo "主 dsh @127.0.0.1:$PORT: ✅ 健康 pid=$(main_pids)"
else
echo "主 dsh @127.0.0.1:$PORT: ❌ 不可用"
fi
if probe_standby; then
echo "standby @127.0.0.1:$STANDBY_PORT: 🟢 唤醒中"
else
echo "standby @127.0.0.1:$STANDBY_PORT: 💤 休眠(磁盘副本,零进程)"
fi
;;
restart)
evlog "restart: 手动重启主"
stop_main; sleep 2
"$HOME/.local/bin/dshd-start" start >/dev/null 2>&1
sleep 6
if probe; then
evlog "restart: ✅ 主健康 pid=$(main_pids)"
else
evlog "restart: ❌ 重启后不可用"
exit 1
fi
;;
recover)
recover "manual"
;;
monitor)
evlog "monitor: 启动轮询(每 5s 探活主 @$PORT)"
fails=0
while true; do
if probe; then
fails=0
else
fails=$((fails+1))
if [ "$fails" -ge "$PROBE_FAILS" ]; then
evlog "monitor: 连续 ${fails} 次探活失败(进程 $(main_pids) 端口探测异常)"
recover "probe-fail"
fails=0
fi
fi
sleep 5
done
;;
wake-standby)
"$DDOUBLE" wake --port "$STANDBY_PORT"
;;
stop-standby)
"$DDOUBLE" stop --port "$STANDBY_PORT"
;;
*) echo "用法: $0 [start|stop|status|restart|recover|monitor|wake-standby|stop-standby]";;
esac