fix(deploy): /tmp 占满把部署自己卡死了 —— 收构建暂存 + 构建带 -trimpath + 判据 ⑤
起因是用户让「清理一下」那批带仓库路径的残留。照着清理策略走时撞上更大的事实:
本机 /tmp 是 9.8G 的 tmpfs,**已 100% 满、可用 0 字节**,我自己的 `go build` 当场
ENOSPC 失败 —— 而部署的第一步就是构建。
## 1 谁把 /tmp 占满的(agentmail 自己的那份)
`redeploy-gateway.sh` 把网关构建到 /tmp 再 install 过去(为了原子替换),**用完没人删**:
每次部署留一个 24MB,实测 7 份 / 162MB。加上电子打包的中间物(squashfs-root 283MB、
pkgcheck/deb 291MB)、go-build-agentmail 缓存 172MB、4 个孤儿 go-build 工作目录 50MB
—— agentmail 名下约 960MB。另有别的产品的 /tmp/gocache 4.6G(TrueAgent 的
rebuild-plugins.sh 里 `export GOCACHE=/tmp/gocache`),不是本项目的,没动。
- prune-deploy-artifacts.sh 新增一类「构建暂存」,窗口 KEEP_BUILD_STAGES=1
(正常路径下部署脚本自己会收,留下的只可能是失败那次,正好留现场)。
自检 +1 项、变异验证过(把删除改成永不删 → 恰好那一项红)。
- 本次实际收:删除 8 项 / 释放 164MB(另加手动清 623MB 不可再生的中间物)。
- redeploy-gateway.sh 成功分支上收掉 $STAGE;失败/回滚分支**不删**(要留现场)。
## 2 残留里还藏着两处「旧真相」
- /etc/systemd/system/zcode.service.bak-20260912-145744(+ 同一次改动的
zcode.service.d/10-dbus.conf.bak-…)里躺着 /home/program/agentmail/deploy/
service-failure-notify.mjs —— 就是我上一封报「/etc/systemd 引用仓库 = 0 个文件」时
**判据自己划掉了的那一类**(walk 里 `!name.includes('.bak')`)。已删(在线单元与
deploy/systemd/ 逐字节一致,sha256 核对过),另外 4 个是别的产品的,没动。
- 判据 ① 因此放宽到含 .bak,并补了坏样本(.bak 里引用仓库路径必须判红)。
上一封那句「0 个文件」的边界现在写进判据里了 —— 边界不说出口,就等于报了个假的 0。
## 3 -trimpath:标准目录部署只做了一半
Go 默认把源文件绝对路径编进二进制。对照实验(同一份源码、同一个 go,只差标志):
带 -trimpath 0 处,不带 57 处 —— 而 19:05 那次部署产出的
/opt/agentmail/agentmail-gateway 里就有 57 处 /home/program/agentmail/…。
依赖确实没了,但**源仓库位置还印在产物上**。两个构建点都加上 -trimpath,
并新增判据 ⑤(已安装二进制不得含源码路径,两侧样本都验)。
判据 ⑤ 现在**是红的**,这是存量产物的实情:磁盘上那份要等下一次
redeploy-gateway.sh 才会被换掉。我没替它单独重启网关 —— 会掐断正在跑的会话。
(工作区是多会话共用的,本次只 add 了上面这 4 个 deploy/ 文件。)
This commit is contained in:
@ -510,6 +510,13 @@ export function checkLayout(inject = {}) {
|
||||
};
|
||||
|
||||
// ① 任何 unit/drop-in 都不得引用源码目录
|
||||
//
|
||||
// ★ `.bak` 也算在内。原先这里把它排除了,理由是"systemd 不加载 .bak"——
|
||||
// 运行时确实不加载,但后果是:2026-09-14 我向用户报"`/etc/systemd` 引用仓库 = 0 个文件",
|
||||
// 而 `/etc/systemd/system/zcode.service.bak-20260912-145744` 里就躺着两行
|
||||
// `/home/program/agentmail/deploy/service-failure-notify.mjs`。
|
||||
// 那句话只对我自己划的那个圈成立 —— **判据的边界没说出口,就等于报了个假的 0**。
|
||||
// 留着 .bak 的代价也不是零:它们是"过期的旧真相",`grep` 到它的人会以为改动没生效。
|
||||
const offenders = [];
|
||||
const walk = dir => {
|
||||
let entries = [];
|
||||
@ -517,7 +524,7 @@ export function checkLayout(inject = {}) {
|
||||
for (const e of entries) {
|
||||
const full = `${dir}/${e.name}`;
|
||||
if (e.isDirectory()) walk(full);
|
||||
else if (/\.(conf|service|timer)$/.test(e.name) && !e.name.includes('.bak')) {
|
||||
else if (/\.(conf|service|timer)(\.bak.*)?$/.test(e.name)) {
|
||||
let text = '';
|
||||
try { text = String(readFile(full, 'utf8')); } catch { continue; }
|
||||
if (text.includes(REPO)) offenders.push(full);
|
||||
@ -525,7 +532,7 @@ export function checkLayout(inject = {}) {
|
||||
}
|
||||
};
|
||||
walk(SYS);
|
||||
push('没有任何 unit/drop-in 引用源码目录', offenders.length === 0, offenders.join(' '));
|
||||
push('没有任何 unit/drop-in/.bak 引用源码目录', offenders.length === 0, offenders.join(' '));
|
||||
|
||||
// ② 已安装单元与仓库副本一致(仓库是唯一真相)
|
||||
const drift = [];
|
||||
@ -579,6 +586,25 @@ export function checkLayout(inject = {}) {
|
||||
}
|
||||
push('各服务的工作目录/可执行文件不在源码目录', badHosts.length === 0, badHosts.join(' '));
|
||||
|
||||
// ⑤ 生产二进制里不得嵌源码路径(-trimpath)。
|
||||
//
|
||||
// Go 默认把源文件的**绝对路径**编进二进制。2026-09-14 实测:换到标准目录部署之后,
|
||||
// `/opt/agentmail/agentmail-gateway` 里仍有 57 处 `/home/program/agentmail/…` ——
|
||||
// 构建脚本漏了 `-trimpath`(对照实验:同一份源码、同一个 go,带标志 0 处、不带 57 处)。
|
||||
// 这条是"运行时不再依赖源码目录"的**后半句**:依赖确实没了,但源仓库位置还印在产物上,
|
||||
// 而且它会把"这个二进制是从哪份源码建的"变成只能靠推断的事。
|
||||
const BIN = '/opt/agentmail/agentmail-gateway';
|
||||
let binHits = -1;
|
||||
let binNote = `读不到 ${BIN}(标准位置没有网关二进制)`;
|
||||
try {
|
||||
const text = String(readFile(BIN));
|
||||
binHits = text.split(REPO).length - 1;
|
||||
binNote = binHits === 0
|
||||
? `${BIN} 里一处都没有`
|
||||
: `${BIN} 里有 ${binHits} 处 ${REPO}/…(重新构建即可清零:跑一次 redeploy-gateway.sh)`;
|
||||
} catch { /* binHits 保持 -1 = 读不到 */ }
|
||||
push('已安装的网关二进制不含源码路径(构建带 -trimpath)', binHits === 0, binNote);
|
||||
|
||||
// ⑥ 工作区干净度 —— **WARN,不参与退出码**。
|
||||
//
|
||||
// 判据 ① 比的是「仓库工作区 → 快照」这一跳。它覆盖不到「HEAD → 工作区」
|
||||
@ -630,33 +656,58 @@ export function layoutSelfCheck() {
|
||||
'/etc/systemd/system': [{ name: 'x.service', isDirectory: () => false }],
|
||||
'/etc/systemd/system/x.service': 'ExecStart=/home/program/agentmail/bin/x',
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x'
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'fake-elf'
|
||||
}));
|
||||
// ① 的 .bak 分支:旧备份单元里躺着仓库路径,也必须报出来(原先它被过滤掉了)。
|
||||
const badBak = checkLayout(fake({
|
||||
'/etc/systemd/system': [{ name: 'z.service.bak-20260101-000000', isDirectory: () => false }],
|
||||
'/etc/systemd/system/z.service.bak-20260101-000000': 'ExecStopPost=-/usr/bin/node /home/program/agentmail/deploy/x.mjs',
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'fake-elf'
|
||||
}));
|
||||
// ⑤ 的坏样本:二进制里嵌着源码路径。
|
||||
const badBin = checkLayout(fake({
|
||||
'/etc/systemd/system': [],
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'ELF…/home/program/agentmail/server/cmd/server/main.go…'
|
||||
}));
|
||||
const good = checkLayout(fake({
|
||||
'/etc/systemd/system': [{ name: 'y.service', isDirectory: () => false }],
|
||||
'/etc/systemd/system/y.service': 'ExecStart=/opt/agentmail/agentmail-gateway',
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x'
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'ELF…github.com/agentmail/gateway/cmd/server…'
|
||||
}));
|
||||
const dirty = checkLayout({ ...fake({
|
||||
'/etc/systemd/system': [],
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x'
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'fake-elf'
|
||||
}), ...fakeGit(' M src/pool.mjs\n') });
|
||||
const clean = checkLayout({ ...fake({
|
||||
'/etc/systemd/system': [],
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x'
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'fake-elf'
|
||||
}), ...fakeGit('') });
|
||||
const unreadable = checkLayout({ ...fake({
|
||||
'/etc/systemd/system': [],
|
||||
'/repo/systemd': [],
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x'
|
||||
'/opt/agentmail/bin/service-failure-notify.mjs': 'x',
|
||||
'/opt/agentmail/agentmail-gateway': 'fake-elf'
|
||||
}), git: () => { throw new Error('not a git repo'); } });
|
||||
const fifth = o => o.find(c => c.name.startsWith('已安装的网关二进制'));
|
||||
const sixth = o => o.find(c => c.name.startsWith('工作区干净'));
|
||||
return [
|
||||
{ name: '标准目录:引用源码目录的样本必须判红', ok: bad[0].ok === false },
|
||||
{ name: '标准目录:.bak 里引用源码目录也必须判红', ok: badBak[0].ok === false },
|
||||
{ name: '标准目录:干净样本必须判绿', ok: good[0].ok === true },
|
||||
// 二进制那条两侧都要真:嵌了源码路径必须红,trimpath 的必须绿。
|
||||
{ name: '网关二进制:嵌了源码路径必须判红', ok: fifth(badBin)?.ok === false },
|
||||
{ name: '网关二进制:trimpath 过的必须判绿', ok: fifth(good)?.ok === true },
|
||||
// 编号:id 从 1 连续排到 N,不许跳号(原先只有 ⑥ 带编号,读者会去找不存在的 ⑤)。
|
||||
{ name: '标准目录:id 连续编号,不跳号', ok: good.every((c, i) => c.id === String(i + 1)) },
|
||||
// 「工作区干净」是"说出来但不改结论"的提示:脏 / 干净 / 读不到三种都被报出来,
|
||||
|
||||
@ -183,7 +183,7 @@ if [[ $CHECK_ONLY -eq 1 ]]; then
|
||||
|
||||
下面这些步骤干跑**没有执行**(它们要写工作区外的目录,正式安装需要 root 或先放行):
|
||||
· 前端产物 → server/internal/static/static/
|
||||
· 构建 Gateway(go vet + go test + go build -o server/agentmail-gateway)
|
||||
· 构建 Gateway(go vet + go test + go build -trimpath -o server/agentmail-gateway)
|
||||
· install -m 0755 server/agentmail-gateway → /opt/agentmail/agentmail-gateway
|
||||
· 生成 /etc/agentmail/*.env(已存在的不覆盖)+ chmod 0600
|
||||
· 装 systemd unit → /etc/systemd/system/,daemon-reload + enable --now
|
||||
@ -204,8 +204,10 @@ cp -r "$REPO/client/electron/dist/." "$REPO/server/internal/static/static/"
|
||||
|
||||
echo "==> 构建 Gateway(单二进制,内含前端 + SQLite)"
|
||||
# 先删再建:go build -o 到已存在的路径时可能拿到 stale 二进制(此坑中过多次)
|
||||
# `-trimpath` 与 redeploy-gateway.sh 同源:不带它时 Go 会把源文件绝对路径编进
|
||||
# 生产二进制(2026-09-14 实测 57 处 /home/program/agentmail/…)。
|
||||
rm -f "$REPO/server/agentmail-gateway"
|
||||
( cd "$REPO/server" && go vet ./... && go test ./... && go build -o "$REPO/server/agentmail-gateway" ./cmd/server )
|
||||
( cd "$REPO/server" && go vet ./... && go test ./... && go build -trimpath -o "$REPO/server/agentmail-gateway" ./cmd/server )
|
||||
|
||||
echo "==> 安装到 $PREFIX"
|
||||
install -d "$PREFIX" "$PREFIX/data" "$ETC"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# 清理部署残留:旧网关二进制、旧插件快照、旧数据库/附件备份、/tmp 里的部署前备份。
|
||||
# 清理部署残留:旧网关二进制、旧插件快照、旧数据库/附件备份、/tmp 里的构建暂存与部署前备份。
|
||||
#
|
||||
# # 为什么要脚本而不是手敲 rm
|
||||
#
|
||||
@ -25,7 +25,7 @@
|
||||
# bash deploy/prune-deploy-artifacts.sh # 干跑,只报告
|
||||
# bash deploy/prune-deploy-artifacts.sh --apply # 真删
|
||||
# bash deploy/prune-deploy-artifacts.sh --self-check # 自检:坏样本必须红、干净样本必须绿
|
||||
# KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 bash ... --apply # 调保留窗口
|
||||
# KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 KEEP_BUILD_STAGES=0 bash ... --apply # 调保留窗口
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="${AGENTMAIL_ROOT:-/opt/agentmail}"
|
||||
@ -33,6 +33,7 @@ KEEP_GATEWAY="${KEEP_GATEWAY:-3}"
|
||||
KEEP_SNAPSHOTS="${KEEP_SNAPSHOTS:-3}"
|
||||
KEEP_DB_BACKUPS="${KEEP_DB_BACKUPS:-1}"
|
||||
KEEP_TMP_DB="${KEEP_TMP_DB:-2}"
|
||||
KEEP_BUILD_STAGES="${KEEP_BUILD_STAGES:-1}"
|
||||
TMPD="${PRUNE_TMP_DIR:-/tmp}"
|
||||
APPLY=0
|
||||
[ "${1:-}" = "--apply" ] && APPLY=1
|
||||
@ -117,6 +118,8 @@ if [ "${1:-}" = "--self-check" ]; then
|
||||
for i in 1 2 3; do
|
||||
: > "$t/tmp/agentmail-pre-deploy-2026010$i-000000.db"
|
||||
touch -d "2026-01-0$i 00:00:00" "$t/tmp/agentmail-pre-deploy-2026010$i-000000.db"
|
||||
: > "$t/tmp/agentmail-gateway-build-2026010$i-000000"
|
||||
touch -d "2026-01-0$i 00:00:00" "$t/tmp/agentmail-gateway-build-2026010$i-000000"
|
||||
done
|
||||
echo "$t"
|
||||
}
|
||||
@ -131,7 +134,7 @@ if [ "${1:-}" = "--self-check" ]; then
|
||||
"$TMPD"/am-prune-selftest-*) ;;
|
||||
*) echo " [FATAL] 自检根目录不在临时区:$t —— 拒跑" >&2; exit 1;;
|
||||
esac
|
||||
AGENTMAIL_ROOT="$t" PRUNE_TMP_DIR="$t/tmp" KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 KEEP_DB_BACKUPS=1 KEEP_TMP_DB=1 \
|
||||
AGENTMAIL_ROOT="$t" PRUNE_TMP_DIR="$t/tmp" KEEP_GATEWAY=2 KEEP_SNAPSHOTS=2 KEEP_DB_BACKUPS=1 KEEP_TMP_DB=1 KEEP_BUILD_STAGES=1 \
|
||||
bash "$0" $mode
|
||||
}
|
||||
|
||||
@ -168,6 +171,8 @@ if [ "${1:-}" = "--self-check" ]; then
|
||||
"$([ ! -e "$T/backups/agentmail-20251201-000000.db" ] && [ ! -e "$T/backups/attachments-20251201-000000.tar.gz" ] && [ -e "$T/data/agentmail.db.bak-20260105-000000" ] && echo 1 || echo 0)"
|
||||
ck "干净样本:/tmp 备份只留窗口内那 1 份" \
|
||||
"$([ -e "$T/tmp/agentmail-pre-deploy-20260103-000000.db" ] && [ ! -e "$T/tmp/agentmail-pre-deploy-20260101-000000.db" ] && echo 1 || echo 0)"
|
||||
ck "干净样本:/tmp 构建暂存只留窗口内那 1 份" \
|
||||
"$([ -e "$T/tmp/agentmail-gateway-build-20260103-000000" ] && [ ! -e "$T/tmp/agentmail-gateway-build-20260101-000000" ] && echo 1 || echo 0)"
|
||||
|
||||
# 坏样本:超窗口的那个备份是指向在线库的符号链接 —— 必须**拒跑**,而不是"删了才发现"
|
||||
B="$(mktree)"; rm -f "$B/backups/agentmail-20251201-000000.db"
|
||||
@ -261,6 +266,22 @@ else
|
||||
done
|
||||
fi
|
||||
|
||||
# 构建暂存:`redeploy-gateway.sh` 先把网关构建到 `$TMPD` 再 `install` 过去(为了原子
|
||||
# 替换),**用完没有任何人删过它** —— 每次部署留下一个 24MB。2026-09-14 实测 7 份
|
||||
# / 162MB。这不是"几百 MB 而已":本机 `$TMPD` 是 9.8G 的 tmpfs,当时已被占满
|
||||
# (可用 0 字节),连 `go build` 都进不去 —— 而部署的第一步就是构建。
|
||||
#
|
||||
# 窗口留 1 份而不是 0:正常路径下部署脚本自己会收(见 redeploy-gateway.sh 末尾),
|
||||
# 留下的只可能是**失败**的那次;失败时那一份正好用来事后查,删了就没现场了。
|
||||
echo "==> $TMPD 的构建暂存(保留最新 $KEEP_BUILD_STAGES 份)"
|
||||
mapfile -t stages < <(ls -1t "$TMPD"/agentmail-gateway-build-* 2>/dev/null)
|
||||
for i in "${!stages[@]}"; do
|
||||
[ "$i" -lt "$KEEP_BUILD_STAGES" ] && { report " 保留" "${stages[$i]}"; continue; }
|
||||
# 二进制可能正被某个部署进程 install 到一半 —— 与快照同理,用过的路径一律先问一句。
|
||||
if in_use "${stages[$i]}"; then report " ★跳过(正在被使用)" "${stages[$i]}"; skipped=$((skipped+1)); continue; fi
|
||||
del "${stages[$i]}"
|
||||
done
|
||||
|
||||
echo "==> $TMPD 的部署前数据库备份(保留最新 $KEEP_TMP_DB 份)"
|
||||
mapfile -t dbs < <(ls -1t "$TMPD"/agentmail-pre-deploy-*.db 2>/dev/null)
|
||||
for i in "${!dbs[@]}"; do
|
||||
|
||||
@ -127,11 +127,16 @@ fi
|
||||
say "3. 构建二进制"
|
||||
STAGE="/tmp/agentmail-gateway-build-$TS"
|
||||
# 先删再建:go build -o 到已存在的路径时可能拿到 stale 二进制(此坑中过多次)
|
||||
# ★ `-trimpath`:Go 默认把源文件的**绝对路径**编进二进制。2026-09-14 实测:本机
|
||||
# 历史二进制都是 trimpath 的(里面一处源码路径都没有),19:05 那次不是 ——
|
||||
# 于是生产件 /opt/agentmail/agentmail-gateway 里躺着 57 处 /home/program/agentmail/…。
|
||||
# 改标准目录部署是为了"运行时不再依赖源码目录";没有 -trimpath 时这条只做到一半:
|
||||
# 依赖确实没了,但**源仓库位置还印在产物上**。判据在 check-deploy-drift(标准目录 ⑤)。
|
||||
run "rm -f '$STAGE'"
|
||||
if [ "$DRY_RUN" = 1 ]; then
|
||||
printf ' [dry-run] (cd server && go build -o %s ./cmd/server)\n' "$STAGE"
|
||||
printf ' [dry-run] (cd server && go build -trimpath -o %s ./cmd/server)\n' "$STAGE"
|
||||
else
|
||||
( cd "$REPO/server" && go build -o "$STAGE" ./cmd/server ) \
|
||||
( cd "$REPO/server" && go build -trimpath -o "$STAGE" ./cmd/server ) \
|
||||
|| { bad "构建失败"; exit 1; }
|
||||
ok "构建完成 $(du -h "$STAGE" | cut -f1)"
|
||||
fi
|
||||
@ -248,6 +253,13 @@ if [ "$CHECK_FAIL" -gt 0 ]; then
|
||||
fi
|
||||
|
||||
say "结论: 自动项全绿"
|
||||
# 构建暂存收尾。这份 24MB 副本只是为了「构建 → install」那一步的原子性,装完就没
|
||||
# 用了;原先没人删 ⇒ 每次部署在 `$TMPD` 留一份。2026-09-14 实测:7 份 / 162MB,
|
||||
# 而 `$TMPD` 是 9.8G 的 tmpfs —— 占满后**连部署自己的第一步都跑不动**
|
||||
# (go build 报 ENOSPC)。存量由 `deploy/prune-deploy-artifacts.sh` 按窗口收。
|
||||
# 只在这个**成功分支**上删:失败/回滚时那份二进制正是要留的现场,见上面的回滚分支。
|
||||
rm -f "$STAGE"
|
||||
ok "构建暂存已收($STAGE)"
|
||||
echo " 回滚命令(留档 24 小时内有效):"
|
||||
[ -n "$BINBAK" ] && echo " install -m 0755 '$BINBAK' '$TARGET' && systemctl restart '$SERVICE'"
|
||||
[ -n "$DBBAK" ] && echo " install -m 0644 '$DBBAK' '$DB' # 先 systemctl stop"
|
||||
|
||||
Reference in New Issue
Block a user