fix(deploy)!: install 不是 rename(头部那句"原子"论断不成立,实测半截二进制)+ 加并发锁 + journalctl 抽成可喂函数 + data/ 权限
pi 的四条,逐条实测: **1. `install(1)` 不是 rename —— 而那句话是整节设计的理由** 头部原话:"install(1) 本质是 rename,是原子的 —— 要么完整换掉,要么原样不动"。 按他给的命令实测 `strace … install -m 0755 /bin/true /tmp/t`: 目标不存在:`openat(t, O_WRONLY|O_CREAT|O_EXCL)` 目标已存在:`unlinkat(t, 0)` → `openat(… O_CREAT|O_EXCL)` → 写入 **全程没有 rename/renameat**。即复制路径,**旧文件在新文件写完整之前就没了**。 中途失败实证:`ulimit -f 1` ⇒ 退出码 **153**(SIGXFSZ),目标变成 **1024 字节截断 ELF**, 原 14 字节内容**已被销毁** —— 正是本段前半句写的风险,`install` 并不免疫。 (第一次测时我把退出码经管道取到了 `head` 的 0 —— 正是 docs 第 6 条那个坑,重测才拿到 153。) ★ 他补的第二个坑也确认:`/tmp` 与 `/opt/agentmail` **不同文件系统** (实测设备号 40 vs 2049)⇒ 就算换成 `mv` 也不原子(跨 fs 退化成 copy+unlink)。 已改成真原子三步:**目标同目录**暂存 → `install`(动的是"还没人用的名字")→ 一次 `mv -f`。 对照 `redeploy-plugin.sh` 的 `mv "$STAGING" "$SNAP"` 是**真原子**(同 fs)—— 同一仓库原先两套"原子切换",一套真、一套名义上的。 **2. 缺并发锁(环境前提表的第五列:同时性)** 原表(变量/命令/空间/身份)漏了这一类,而它不是假设:工作区是多 agent 共用的。 两个部署同时跑 ⇒ 各自 stop(一次失败、状态没人看)→ 两次写同一目标(配合上面那条 ⇒ 真能留半截)→ 两次后置验证互相把对方的"验证不过"当自己结论 → 谁回滚不确定。 三个脚本都加 `flock`(**不是**"检查锁文件存在",那本身有竞态)。 实测:同一把锁上第二个进程 `flock -n` 失败;脚本形态下 `install.sh` 的 `--check` 不建锁 (干跑只读、且刻意允许无写权限运行)。 **3. journalctl 抽成可喂函数 —— 并且他对我那次"复现"的更正成立** 他说我复现的是**"空输出"支**,不是"读不到"支。实测确认: `journalctl -u 不存在的-unit` 退出码 **0** ⇒ 我测到的是 else 分支。 已抽成 `am_scan_logs <unit> <since> <pattern> [命令]`,输出三态 `clean`/`hit`/`unreadable:<码>`(与既有 `describeEnvError`、`judgeRestart` 同一做法: 把能被样本喂的部分抽出来)。**用 `/bin/false`、`/bin/true`、假"输出含 panic"的脚本 三个样本喂过**(不碰生产):`unreadable:1` / `clean` / `hit` —— 三条支路现在都有覆盖。 判据也随之分开:**"命令不在"由 `AGENTMAIL_REQUIRE` 兜、"命令在但读不到"由这个函数兜**, 两列在代码里分开,而不只是注释里分开。 **4. 低优先项里 umask 那条是真问题(我原来以为可忽略)** 实测 `install -d` 权限位受 umask 影响;而本机生产 `/opt/agentmail/data` = **755**、 `agentmail.db` = **644**(全局可读),同一脚本里 `agent-config`/`pi-config` 却是显式 `-m 0700` —— 同一脚本两套口径,而那个库里是全部往来邮件。 已改:`install -d -m 0700 "$PREFIX/data"`、`-m 0700 "$ETC"`(密码与密钥)。 (现有生产权限不在本次改动范围,属部署后生效。) **顺带修一处我自己的口径不一致**:`install.sh` 的 root 检查用 `exit 1`(判据失败), 而另外两个脚本与 `env-defaults.sh` 的"环境不足"都用 **2** —— 调用者无法据此区分 "该重跑"还是"该修代码"。已统一为 2。 ★ 加锁过程中我自己连踩三次"复制粘贴的上下文假设"(都已修,并记进 docs 第 18 条): `install.sh` 没有 `bad()` ⇒ 127;`redeploy-plugin.sh` 没有 `$PREFIX`(用 `$DEST_ROOT`)⇒ `unbound variable`;`install.sh --check` 无写权限 ⇒ 建锁 `Permission denied` 又变 127。 **同一份代码搬到另一个脚本里,能引用的变量和函数是不一样的。** 验证:install.sh --check exit 0;npm test exit 0;prune 自检 22/22;drift 自检 35/0; check-shared-libs exit 0;全部 deploy 脚本 bash -n 通过。
This commit is contained in:
@ -19,10 +19,12 @@ REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# 下游把前者读成后者时就会产出假绿(journalctl 那两处就是:工具缺失被读成"无 panic")。
|
||||
AGENTMAIL_REQUIRE="git go npm npx node curl systemctl"
|
||||
agentmail_env_report
|
||||
|
||||
# 与 `redeploy-gateway.sh` / `reset-demo.sh` 同源(pi 评审 2026-09-14 对出来的):
|
||||
# 那两处都写 `${AGENTMAIL_PREFIX:-/opt/agentmail}`,只有这里写死 ——
|
||||
# 谁设了那个变量,install 装到 A、redeploy 和 drift-check 看 B,三个工具各说各话。
|
||||
PREFIX="${AGENTMAIL_PREFIX:-/opt/agentmail}"
|
||||
|
||||
ETC=/etc/agentmail
|
||||
|
||||
#
|
||||
@ -45,8 +47,34 @@ for a in "$@"; do
|
||||
esac
|
||||
done
|
||||
|
||||
# ★ **部署锁**(pi 评审 2026-09-14):环境前提里原先缺的第五列 —— **同时性**。
|
||||
# 这台机器的工作区是多 agent 共用的(docs/DEV-TOOLING.md 自己记过),并发部署会互相踩:
|
||||
# 两次写同一个目录/文件、两次后置验证互相把对方的"验证不过"当自己的结论、谁回滚不确定。
|
||||
# 用 flock("检查文件存在"本身有竞态);锁随进程退出自动释放(fd 9 关闭)。
|
||||
#
|
||||
# ⚠️ 两个只有在这个脚本里才成立的前提(都是我自己实测撞出来的):
|
||||
# · **本脚本没有 `bad` 这类输出函数**(它用裸 `echo >&2`)——照抄别的脚本的写法会得到
|
||||
# `bad: command not found`(退出码 127),把"锁没拿到"报成"脚本坏了"。
|
||||
# · **`--check` 阶段不建锁**:干跑是只读的、不需要互斥,而它又刻意允许在**没有写权限**时
|
||||
# 运行(`/opt/agentmail/.deploy.lock: Permission denied` ⇒ 又变成 127)。
|
||||
# 干跑的价值恰恰是"还没权限的人也能把门跑一遍",所以锁必须在写分支里。
|
||||
if [[ $CHECK_ONLY -eq 0 ]]; then
|
||||
[[ $EUID -eq 0 ]] || { echo "需要 root:sudo $0(只验证不安装用 ./deploy/install.sh --check)" >&2; exit 1; }
|
||||
_LOCK="$PREFIX/.deploy.lock"
|
||||
if ! exec 9>"$_LOCK"; then
|
||||
echo " [FAIL] 环境不足:无法创建部署锁 $_LOCK" >&2
|
||||
exit 2
|
||||
fi
|
||||
if ! flock -n 9; then
|
||||
echo " [FAIL] 环境不足:另一个部署正在跑($_LOCK 被占用)—— 等它结束再跑" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $CHECK_ONLY -eq 0 ]]; then
|
||||
# ★ 退出码 **2**(环境/权限),不是 1(判据失败)—— 与另外两个部署脚本、
|
||||
# 以及 env-defaults.sh 里"环境不足"的口径统一。原来这里是 1:
|
||||
# "没有权限"与"门禁没过"在退出码上不可区分,调用者没法据此决定"该重跑还是该修代码"。
|
||||
[[ $EUID -eq 0 ]] || { echo " [FAIL] 环境不足:需要 root:sudo $0(只验证不安装用 ./deploy/install.sh --check)" >&2; exit 2; }
|
||||
else
|
||||
echo "==> 干跑模式(--check):不会写 $PREFIX 或 $ETC"
|
||||
# 写权限是这一步唯一的硬门槛,先说清楚 —— 下一个拿到权限的人要一眼看到需要什么
|
||||
@ -257,7 +285,16 @@ rm -f "$REPO/server/agentmail-gateway"
|
||||
( 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"
|
||||
# ★ 权限位**显式给**,不靠 umask(pi 评审 2026-09-14 提的低优先项,实测是真问题):
|
||||
# `install -d` 的权限受进程 umask 影响,而下面这些目录里装着**邮件数据库**。
|
||||
# 实测本机生产:`/opt/agentmail/data` = **755**、`agentmail.db` = **644**(全局可读),
|
||||
# 而同一段代码里 `agent-config`/`pi-config` 是显式 `-m 0700` —— 同一个脚本里两套口径。
|
||||
# 数据库里是全部往来邮件(含正文与地址),不该全局可读。
|
||||
# 目录 0700 是真正的保护(库文件权限由创建它的进程决定);
|
||||
# `/etc/agentmail` 里是密码与密钥,同样收到 0700。
|
||||
install -d -m 0755 "$PREFIX"
|
||||
install -d -m 0700 "$PREFIX/data"
|
||||
install -d -m 0700 "$ETC"
|
||||
install -m 0755 "$REPO/server/agentmail-gateway" "$PREFIX/agentmail-gateway"
|
||||
|
||||
# ---- env 文件:仅在缺失时生成,密码随机 ----
|
||||
|
||||
Reference in New Issue
Block a user