96 lines
4.6 KiB
Bash
96 lines
4.6 KiB
Bash
#!/usr/bin/env bash
|
||||
|
|
# op-lock.sh — 「平台高危操作锁」(服务器侧第二把锁)
|
|||
|
|
#
|
|||
|
|
# 为什么需要它:所有会话**共用一台服务器**,而下面这些操作跨会话没有任何互斥 ——
|
|||
|
|
# 重启 dshs / drain 实例 scope / 改实例 env·MemoryMax / 批量铺插件 /
|
|||
|
|
# 改 nginx vhost·证书·nft
|
|||
|
|
# 文档冲突靠 git status / mtime 能看出来,**服务器态变更看不出来**
|
|||
|
|
# (systemctl 不会告诉你 10 分钟前谁重启过)→ 只能靠显式锁。
|
|||
|
|
#
|
|||
|
|
# 与文档库锁的关系:`05-交接单/.doing-<单>` + `.exec-lock` 管「文档与单子」;本锁管「生产态」。
|
|||
|
|
# 顺序:**先抢全局执行锁 → 再占本锁**。
|
|||
|
|
#
|
|||
|
|
# 用法:
|
|||
|
|
# bash 07-scripts/op-lock.sh claim <操作名> "<影响面/时长>" [占用者]
|
|||
|
|
# bash 07-scripts/op-lock.sh release <操作名>
|
|||
|
|
# bash 07-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 07-scripts/op-lock.sh claim <操作名> \"<影响面>\""
|
|||
|
|
fi
|
|||
|
|
echo " 完工请:bash 07-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
|