#!/usr/bin/env bash # 清理部署残留:旧网关二进制、旧插件快照、/tmp 里的部署前数据库备份。 # # # 为什么要脚本而不是手敲 rm # # 这些东西**天天在长**:每次 redeploy 留一个 24MB 的网关二进制、每个插件留一份快照 # (opencode/dsh 的快照含 node_modules,一份几十 MB)。2026-09-14 实测: # /opt/agentmail 1.3G,其中旧二进制 ~790MB、插件快照 ~540MB。 # 手敲的清理不可复现、下次还得再想一遍,而且很容易漏掉"这一份还在被用"的判断。 # # # 两条硬规矩 # # ① **保留回滚窗口**:网关二进制留最新 N 份、每个插件留最新 N 份快照 # (`current` 永远保留)。发布纪律要求"有回滚目标",所以不是全清。 # ② ★ **绝不删正在使用的快照**:任何进程的 cmdline 里出现过的快照路径一律跳过。 # 删掉它 = 进程一重启就找不到自己的代码(而这类事故此前真发生过)。 # # 用法: # bash deploy/prune-deploy-artifacts.sh # 干跑,只报告 # bash deploy/prune-deploy-artifacts.sh --apply # 真删 # KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 bash ... --apply # 调保留窗口 set -uo pipefail ROOT="${AGENTMAIL_ROOT:-/opt/agentmail}" KEEP_GATEWAY="${KEEP_GATEWAY:-3}" KEEP_SNAPSHOTS="${KEEP_SNAPSHOTS:-3}" KEEP_TMP_DB="${KEEP_TMP_DB:-2}" APPLY=0 [ "${1:-}" = "--apply" ] && APPLY=1 freed_kb=0 removed=0 skipped=0 report() { printf ' %s %s\n' "$1" "$2"; } # 收集所有"正在被进程使用"的路径(cmdline + cwd),用来做安全闸。 # 自己这条命令的 cmdline 里就可能含这个路径(例如调用方把路径写在命令行里), # 于是"检查在用"会永远为真 —— 与 `pkill -f` 杀掉自己那条命令是同一个坑。 # 所以先把本进程及其祖先链排除掉。 _SELF_CHAIN=" " _p=$$ while [ -n "$_p" ] && [ "$_p" != "1" ] && [ "$_p" != "0" ]; do _SELF_CHAIN="$_SELF_CHAIN$_p " _p=$(awk '{print $4}' "/proc/$_p/stat" 2>/dev/null || echo "") done in_use() { local needle="$1" p pid for p in /proc/[0-9]*; do pid=${p#/proc/} [ -r "$p/cmdline" ] || continue case "$_SELF_CHAIN" in *" $pid "*) continue;; esac if tr '\0' ' ' <"$p/cmdline" 2>/dev/null | grep -qF "$needle"; then return 0; fi local cwd; cwd=$(readlink "$p/cwd" 2>/dev/null || true) case "$cwd" in *"$needle"*) return 0;; esac done return 1 } del() { local path="$1" local kb; kb=$(du -sk "$path" 2>/dev/null | cut -f1); kb=${kb:-0} if [ "$APPLY" = "1" ]; then rm -rf "$path"; fi freed_kb=$((freed_kb + kb)); removed=$((removed + 1)) report " $([ "$APPLY" = 1 ] && echo 删除 || echo 待删)" "$path($((kb/1024))MB)" } echo "==> 网关旧二进制(保留最新 $KEEP_GATEWAY 份)" mapfile -t bins < <(ls -1t "$ROOT"/agentmail-gateway.bak-* 2>/dev/null) for i in "${!bins[@]}"; do [ "$i" -lt "$KEEP_GATEWAY" ] && { report " 保留" "${bins[$i]}"; continue; } if in_use "${bins[$i]}"; then report " ★跳过(正在被使用)" "${bins[$i]}"; skipped=$((skipped+1)); continue; fi del "${bins[$i]}" done echo "==> 插件快照(每个插件保留最新 $KEEP_SNAPSHOTS 份,current 永远保留)" for plug in "$ROOT"/plugins/*/; do [ -d "$plug" ] || continue name=$(basename "$plug") mapfile -t snaps < <(ls -1dt "$plug"*/ 2>/dev/null | sed 's:/$::') for i in "${!snaps[@]}"; do s="${snaps[$i]}" [ "$(basename "$s")" = "current" ] && continue # current 指向的那一份也必须保留 cur=$(readlink -f "$plug/current" 2>/dev/null || true) [ "$cur" = "$(readlink -f "$s")" ] && { report " 保留" "$s(current 指向)"; continue; } if [ "$i" -lt "$KEEP_SNAPSHOTS" ]; then report " 保留" "$s"; continue; fi if in_use "$s"; then report " ★跳过(正在被使用)" "$s"; skipped=$((skipped+1)); continue; fi del "$s" done done echo "==> /tmp 的部署前数据库备份(保留最新 $KEEP_TMP_DB 份)" mapfile -t dbs < <(ls -1t /tmp/agentmail-pre-deploy-*.db 2>/dev/null) for i in "${!dbs[@]}"; do [ "$i" -lt "$KEEP_TMP_DB" ] && { report " 保留" "${dbs[$i]}"; continue; } del "${dbs[$i]}" done echo if [ "$APPLY" = "1" ]; then echo " 已删除 $removed 项,释放约 $((freed_kb/1024))MB;跳过(在用)$skipped 项" else echo " 干跑:将删除 $removed 项,可释放约 $((freed_kb/1024))MB;跳过(在用)$skipped 项" echo " 加 --apply 执行。" fi