128 lines
4.2 KiB
Bash
128 lines
4.2 KiB
Bash
#!/usr/bin/env bash
|
||
# state.db 回滚脚本(2026-09-03 主人 C 方案带的回滚保险;2026-09-06 模式15 加固)
|
||
# **必须从独立终端跑**(不能从 gateway 内部跑,会被杀)
|
||
# 用法:bash ~/.hermes/scripts/restore-state-db.sh [快照.db 或 含 state.db 的目录]
|
||
# (缺省用旧版 09-03 快照目录;传参可用 state-20260906-1800.db 这类 .backup 快照)
|
||
set -u
|
||
|
||
SNAP="/home/muc/.hermes/backups/state-db-snap-20260903_153655"
|
||
DB="/home/muc/.hermes/state.db"
|
||
WAL="/home/muc/.hermes/state.db-wal"
|
||
SHM="/home/muc/.hermes/state.db-shm"
|
||
JOBS="/home/muc/.hermes/cron/jobs.json"
|
||
|
||
# 参数化快照源:$1 可以是 *.db 文件,或含 state.db 的目录
|
||
SNAP_ARG="${1:-}"
|
||
|
||
LOG="/home/muc/.hermes/backups/state-db/restore-$(date +%Y%m%d_%H%M%S).log"
|
||
mkdir -p "$(dirname "$LOG")"
|
||
|
||
log() { echo "[$(date '+%F %T')] $*" | tee -a "$LOG"; }
|
||
|
||
# 🔒 2026-09-08 止血: lsof 零持有者 gate——任何进程持有 state.db 都拒绝继续
|
||
# (19:37 教训: 残留 hermes 进程挡修复; P1-1: gateway 在线第二写入者 = WAL 突破损坏)
|
||
HOLDERS=$(lsof "$DB" 2>/dev/null | awk 'NR>1 {print $1"("$2")"}' | sort -u | tr '\n' ' ')
|
||
if [ -n "$HOLDERS" ]; then
|
||
log "🔴 拒绝执行: state.db 仍被持有: $HOLDERS"
|
||
log " 先停止持有进程(含 hermes-gateway)再重试。lsof 确认零持有者才可恢复。"
|
||
exit 3
|
||
fi
|
||
log "✅ lsof gate 通过: 零持有者"
|
||
|
||
# 解析快照源(放 log() 定义后,避免未定义函数调用)
|
||
SRC_DB=""
|
||
if [ -n "$SNAP_ARG" ]; then
|
||
if [ -f "$SNAP_ARG" ]; then
|
||
SRC_DB="$SNAP_ARG"
|
||
elif [ -f "$SNAP_ARG/state.db" ]; then
|
||
SRC_DB="$SNAP_ARG/state.db"
|
||
else
|
||
log "❌ 快照参数无效: $SNAP_ARG"
|
||
exit 2
|
||
fi
|
||
fi
|
||
if [ -z "$SRC_DB" ]; then
|
||
if [ ! -f "$SNAP/state.db" ]; then
|
||
log "❌ 快照不存在: $SNAP/state.db"
|
||
exit 2
|
||
fi
|
||
SRC_DB="$SNAP/state.db"
|
||
fi
|
||
log "✅ 快照 OK: $SRC_DB"
|
||
|
||
# 0. 停 gateway
|
||
log "[0/4] 停 gateway..."
|
||
systemctl --user stop hermes-gateway 2>&1 | tee -a "$LOG"
|
||
sleep 2
|
||
|
||
# 1. 原子替换(只还原 state.db;wal/shm 为瞬时文件一律清掉,避免旧帧污染)
|
||
log "[1/4] 还原 state.db(来源: $SRC_DB)..."
|
||
cp -a "$SRC_DB" "$DB"
|
||
rm -f "$WAL" "$SHM"
|
||
log "✅ state.db 还原完成(已清 stale WAL/SHM)"
|
||
|
||
# 旧版目录快照附带 jobs.json 时还原(仅缺省模式;.db 文件快照不含 jobs)
|
||
if [ -z "${SNAP_ARG:-}" ] && [ -f "$SNAP/jobs.json" ]; then
|
||
cp -a "$SNAP/jobs.json" "$JOBS"
|
||
log "✅ jobs.json 还原完成"
|
||
fi
|
||
|
||
# 1.5 强制 WAL + integrity 断言(2026-09-06 模式15:快照/.backup 产物默认 delete 头,
|
||
# 若直接启 gateway → 多连接并发抢 delete→WAL 转换 → disk I/O error 风暴 → FTS5 撕裂。
|
||
# 失败即中止,绝不带病启动。)
|
||
log "[1.5/4] 强制 journal_mode=WAL + integrity 断言..."
|
||
JM=$(python3 - "$DB" <<'PY'
|
||
import sqlite3, sys
|
||
con = sqlite3.connect(sys.argv[1], timeout=30)
|
||
try:
|
||
print(con.execute("PRAGMA journal_mode=WAL").fetchone()[0])
|
||
finally:
|
||
con.close()
|
||
PY
|
||
)
|
||
log " journal_mode = $JM ← 应为 wal"
|
||
if [ "$JM" != "wal" ]; then
|
||
log "❌ 强制 WAL 失败($JM),中止启动,请人工检查 $DB"
|
||
exit 2
|
||
fi
|
||
IC=$(python3 - "$DB" <<'PY'
|
||
import sqlite3, sys
|
||
con = sqlite3.connect(sys.argv[1], timeout=30)
|
||
try:
|
||
print(con.execute("PRAGMA integrity_check").fetchone()[0])
|
||
finally:
|
||
con.close()
|
||
PY
|
||
)
|
||
log " integrity_check = $IC ← 应为 ok"
|
||
if [ "$IC" != "ok" ]; then
|
||
log "❌ 切 WAL 后 integrity 异常($IC),中止启动"
|
||
exit 2
|
||
fi
|
||
|
||
# 2. 启 gateway(3 次重试)
|
||
log "[2/4] 启 gateway(3 次重试)..."
|
||
for i in 1 2 3; do
|
||
systemctl --user start hermes-gateway 2>&1 | tee -a "$LOG"
|
||
sleep 3
|
||
if systemctl --user is-active hermes-gateway >/dev/null 2>&1; then
|
||
log "✅ gateway 第 $i 次启动成功"
|
||
break
|
||
fi
|
||
log "⚠️ 第 $i 次启动未就绪,等待后重试"
|
||
sleep 3
|
||
done
|
||
|
||
# 3. 健康检查
|
||
log "[3/4] 健康检查..."
|
||
sleep 5
|
||
if systemctl --user is-active hermes-gateway >/dev/null 2>&1; then
|
||
log "✅ gateway ACTIVE,回滚成功"
|
||
log "📋 主人请重新发起对话(小唯已失忆)"
|
||
exit 0
|
||
else
|
||
log "❌ gateway 还是没起来"
|
||
journalctl --user -u hermes-gateway -n 30 --no-pager 2>&1 | tee -a "$LOG"
|
||
exit 1
|
||
fi
|