#!/usr/bin/env bash # op-lock.sh — 「平台高危操作锁」(服务器侧第二把锁) # # 为什么需要它:所有会话**共用一台服务器**,而下面这些操作跨会话没有任何互斥 —— # 重启 dshs / drain 实例 scope / 改实例 env·MemoryMax / 批量铺插件 / # 改 nginx vhost·证书·nft # 文档冲突靠 git status / mtime 能看出来,**服务器态变更看不出来** # (systemctl 不会告诉你 10 分钟前谁重启过)→ 只能靠显式锁。 # # 与文档库锁的关系:`交接单/.doing-<单>` + `.exec-lock` 管「文档与单子」;本锁管「生产态」。 # 顺序:**先抢全局执行锁 → 再占本锁**。 # # 用法: # bash scripts/op-lock.sh claim <操作名> "<影响面/时长>" [占用者] # bash scripts/op-lock.sh release <操作名> # bash scripts/op-lock.sh status # # 判据:`mkdir` 原子 —— 成功 = 你拿到;报 File exists = 有别的会话正在动线上 → **停手**。 # 退出码:0 = 成功/无人占用;1 = 已被占用(claim 失败)或环境错误。 set -uo pipefail REMOTE="${OP_LOCK_REMOTE:-bt-server}" LOCKROOT="${OP_LOCK_DIR:-/opt/dsh/state/.op-lock}" OP="${2:-}" WHO="${4:-${ME:-unknown-session}}" NOW="$(date '+%Y-%m-%d %H:%M')" usage() { sed -n '3,20p' "$0" | sed 's/^# \{0,1\}//' exit 2 } case "${1:-}" in claim) [ -n "$OP" ] || usage IMPACT="${3:-(未声明影响面 —— 补上:谁会被断、断多久)}" if ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" "mkdir '$LOCKROOT/$OP' 2>/dev/null"; then ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" \ "printf '占用者:%s\n开始时间:%s\n操作摘要:%s\n影响面:%s\n' '$WHO' '$NOW' '$OP' '$IMPACT' > '$LOCKROOT/$OP/OWNER'" || true echo "✅ 已占位:$LOCKROOT/$OP(占用者 $WHO,$NOW)" if [ "$WHO" = "unknown-session" ]; then echo " ⚠ 未提供会话名 → 已记作 unknown-session。后果有两个(2026-09-12 实测踩到):" echo " ① handoff-guard 的【1d】会把它当成**别人的锁**(连你自己都被挡住);" echo " ② release 的归属校验会拒绝你。请改用:" echo " ME=\"<会话名>\" bash scripts/op-lock.sh claim <操作名> \"<影响面>\"" fi echo " 完工请:bash scripts/op-lock.sh release $OP" exit 0 fi echo "❌ 占位失败 —— 该锁已存在(有会话正在动线上)→ 停手。" >&2 bash "$0" status >&2 || true exit 1 ;; release) [ -n "$OP" ] || usage # 归属校验(R9,2026-09-12 补):锁只能由**持有者自己**释放 —— # 否则 release 就成了绕过 R9 的后门(AI 读一下 status 拿到操作名,就能把别人的锁 rm 掉)。 RWHO="${4:-${ME:-unknown-session}}" ROWN="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" \ "sed -n '1p' '$LOCKROOT/$OP/OWNER' 2>/dev/null" 2>/dev/null | sed 's/^占用者://')" if [ -z "$ROWN" ]; then echo "❌ 拒绝释放:「$OP」没有 OWNER 记录(无法确认归属)。" >&2 echo " 按 **R9**:锁的处置权只属于用户本人 —— 请报告用户,由用户处置。" >&2 exit 1 fi if [ "$ROWN" != "$RWHO" ] && [ "${FORCE:-0}" != "1" ]; then echo "❌ 拒绝释放:「$OP」的占用者是「$ROWN」,而你是「$RWHO」。" >&2 echo " 按 **R9**(禁止接管 / 删锁):抢不到锁 = 停手 + 报告用户;**锁的处置权只属于用户本人**。" >&2 echo " (确需处置:由**用户本人**执行;或用户明确点头后用 FORCE=1,并在回报里写明理由。)" >&2 exit 1 fi ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" "rm -rf '$LOCKROOT/$OP'" || { echo "❌ 释放失败(ssh 不可达?)" >&2; exit 1; } echo "✅ 已释放:$OP(归属已核:$ROWN)" ;; status) echo "—— 平台操作锁($REMOTE:$LOCKROOT)——" OUT="$(ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" \ "ls -1 '$LOCKROOT' 2>/dev/null | grep -v '^README$'" 2>/dev/null)" if [ -z "$OUT" ]; then if ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" "test -d '$LOCKROOT'" 2>/dev/null; then echo " (无人占用 ✓)" else echo " ⚠ 锁根目录不存在:$LOCKROOT(需先建立)" >&2 exit 1 fi else for L in $OUT; do echo " 🔴 $L" ssh -o BatchMode=yes -o ConnectTimeout=10 "$REMOTE" \ "sed 's/^/ /' '$LOCKROOT/$L/OWNER' 2>/dev/null" || true done exit 1 fi ;; *) usage ;; esac