#!/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