**1. 自指缺口:新能力带的新依赖没登记回表(pi 抓到)**
我加"同时性"那一列时引入了 `flock`,**却没把 `flock` 加进三个脚本的 `AGENTMAIL_REQUIRE`**。
后果实测:
PATH 里没有 flock ⇒ `flock: command not found`(127)⇒ `! flock` 为真
⇒ 打印"**另一个部署正在跑(锁被占用)**"
退出码事后是对的(2),但**诊断是错的** —— 而照着它做的是"等另一个部署结束":**永远等不到**。
三个脚本各加一个词;并在 `env-defaults.sh` 的 ③b 注释里写明这条规矩
(**新增任何外部命令时回到 `AGENTMAIL_REQUIRE` 登记**)与这个实例。
→ docs 第 19 条:「表与被表的东西不同步」。
**2. 第六列候选:中断(信号)—— 已按 pi 的建议修 `redeploy-gateway.sh`**
原子 `mv` 修的是"半截二进制",**没修"服务停着而脚本死了"**:
第 250 行 stop 与第 267 行 start 之间被外部信号打断(Ctrl-C、宿主杀进程、会话回收、OOM)
⇒ 脚本直接退出、**服务留在停止状态而什么也不说** ⇒ "邮件全停 + 无人告知"。
已加 `trap … INT TERM HUP`:进窗口前置位 `_SERVICE_STOPPED`,出窗口复位并摘 trap;
**trap 只在"确实还停着"时才动手**(否则会多起一次服务);回滚分支也维护该标志。
**用 stub `systemctl` + 探针真喂过四个分支**:
stopped=1 + SIGINT ⇒ 调了 `systemctl start`、退出码 130、打印点名
stopped=0 + SIGINT ⇒ **没有**调用 start(不误起)
(探针里两次 harness 自身的错也一并记下:`sed`/`awk` 的区间端点选错,
把 `trap -` 也取进来,导致"trap 没生效"的假象 —— 是探针错,不是代码错。)
★ 顺带修掉自己写的一处:`printf '… $SERVICE …'` 用**单引号**包裹 ⇒ `$SERVICE` **不展开**,
原样打出字面量(探针里实测看到)。改双引号传参。这类"消息里有变量但没展开"会让读者
以为服务名真叫 `$SERVICE`。
**3. `install -d -m` 对已存在目录的行为:实测会改(pi 的疑问)**
mkdir -p 建 755 → `install -d -m 0700 <同一目录>` → **700**
所以**下一次部署就会收紧** `/opt/agentmail/data` 与 `/etc/agentmail`,不需要额外的
`chmod 0700` 动作,也不必为此单开一次"人按一下"。
(我按这条如实回报,因为 pi 说过"若不会改就需要显式 chmod,且安全意义比
`user-question.js` 高" —— 结论是不需要。)
**4. 两条欠账入 `docs/DEBTS.json`(按 pi 的界线:只修新机制自己引入且会误报的缺口)**
· `deploy-space-prefix-fs`:空间列只铺了 `$TMPDIR`,没铺 `$PREFIX` 所在文件系统
(属"列内没铺满",不是新列)。
· `deploy-interrupt-trap-other-scripts`:trap 只在 `redeploy-gateway.sh`;
`install.sh`/`redeploy-plugin.sh` 被打断同样会留半成品(没有"服务停着"那种后果,故低优先)。
→ docs 第 20 条同时记下 trap 这条纪律与它的可喂判据写法。
验证:install.sh --check exit 0;npm test exit 0;prune 自检 22/22;drift 自检 35/0;
check-shared-libs exit 0;全部 deploy 脚本 bash -n 通过;DEBTS.json 有效(13 条)。
212 lines
12 KiB
Bash
212 lines
12 KiB
Bash
#!/usr/bin/env bash
|
||
#
|
||
# 部署脚本的**环境自足**声明:一处给全,不再每条命令旁各写一个预检。
|
||
#
|
||
# # 为什么要有这个文件
|
||
#
|
||
# 2026-09-14 这条链上一共修了**四次**同一形状的错,每次形态一模一样:
|
||
# **部署路径上假设了一个环境变量存在**。四次都是"再加一个预检",
|
||
# 而预检只挡**已知的那一个** —— 挡住 `HOME` 之后 `GOMODCACHE` 才露出来
|
||
# (两者其实是同一个根因的两面:`HOME` 决定 go 默认缓存位置)。
|
||
# pi 的原话:**"否则第五次一定会来。"**
|
||
#
|
||
# 四次记录(每次都是实测,不是推测):
|
||
# 1. `HOME` 未设 ⇒ `go` 拒绝工作(`homeagent` 的 build.sh,当次修了一处)
|
||
# 2. `HOME` 未设 ⇒ 同一族再现(`install.sh`,当次又修一处)
|
||
# 3. `TMPDIR` 未设/不可写 ⇒ 构建与测试把临时文件写到意外位置
|
||
# (`redeploy-plugin.sh:160` 曾写成 `TMPDIR=${TMPDIR:-/tmp} npx tsc …`,即就地兜了一次)
|
||
# 4. `GOMODCACHE`/`GOPATH` 都未设(`HOME` 也没设)⇒
|
||
# `go: module cache not found: neither GOMODCACHE nor GOPATH is set`
|
||
#
|
||
# # 纪律
|
||
#
|
||
# · 需要环境的脚本**开头** `source` 本文件,别在命令行里内联兜底;
|
||
# · 本文件只设**未设**的变量(`:-` 语义),绝不覆盖调用者已经给的值;
|
||
# · 失败要**说清是环境问题**并按仓库约定用退出码 2(环境),不要让它冒充代码缺陷。
|
||
#
|
||
# ⚠️ **顺序不是风格,是正确性**(pi 评审 2026-09-14):
|
||
# 下面 ① → ④ 的顺序有依赖 —— ③ 用 `command -v go` 探测,而 `command -v` **走 PATH**,
|
||
# 所以 ④(修 PATH)必须在 ③ **之前**。原先反了:在 ④ 要修的那个环境里(PATH 为空)、
|
||
# ③ 的探测必然失败,紧接着 ④ 把 PATH 装上,后面的步骤**又能**找到 go
|
||
# —— **探测结论与实际可用性相反**。一条探测所依赖的东西,正是同一文件后面那一步要修的
|
||
# 东西:这正是本文件存在的理由(消除"部署路径上的环境假设"),所以顺序写死在这里并解释。
|
||
#
|
||
# ⚠️ 本文件被 `source`,**不能带 `set -e`**(那会改调用者的语义)。
|
||
# 调用者自己已经有 `set -uo pipefail`(`install.sh` 另有 `-e`),这里不重复声明。
|
||
|
||
# shellcheck shell=bash
|
||
|
||
# 记录本次兜了哪些变量(调用者可以打出来,见文件末尾的 `agentmail_env_report`)。
|
||
# 目的:**"某条脚本忘了 source" 目前没有信号** —— `AGENTMAIL_ENV_DEFAULTS` 只是被 export,
|
||
# 值本身不进正常输出。谁把 `source` 删了或写错位置,输出**少了那一行**才是可见信号;
|
||
# 否则"环境本来就齐"与"忘了 source"在输出上完全同形(又是"看起来在兜、其实没兜")。
|
||
AGENTMAIL_ENV_DEFAULTS=""
|
||
|
||
# ④(**必须排在最前**,见文件头)PATH:只保证常见系统目录在(最小环境里 PATH 可能为空)。
|
||
case ":${PATH:-}:" in
|
||
*:/usr/bin:*) : ;;
|
||
*) PATH="${PATH:+$PATH:}/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||
export PATH
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS PATH=+系统目录" ;;
|
||
esac
|
||
|
||
# ① HOME:`go`/`npm`/`npx` 都会用它推默认缓存与配置位置。
|
||
#
|
||
# ★ **不能无脑给 `/root`**(pi 评审 2026-09-14):`redeploy-gateway.sh` 没有 EUID 断言,
|
||
# 于是在**非 root + 空 HOME** 下会拿到 `HOME=/root`,接下来的 `go build`/`npm ci`
|
||
# 往 `/root/go`、`/root/.npm` 写 ⇒ **EACCES**,而那串报错看起来是**代码/工程问题**
|
||
# —— 正是本文件存在的理由(把环境问题说成代码问题)。所以按身份分叉,**且验证可写**。
|
||
# ★ **`HOME` 已给也要验可写**(pi 的思路我扩了一步,实测发现的第二个口子):
|
||
# 非 root 调用者若带着 `HOME=/root`(sudo -E、从 root shell 继承、容器里挂错)过来,
|
||
# 只判"未设"就放行 ⇒ 后面 `go build`/`npm ci` 写 `/root/go`、`/root/.npm` ⇒ **EACCES**,
|
||
# 报错看起来是代码问题 —— 与"空 HOME"是**同一个后果**,只是入参不同。
|
||
# 实测:`setpriv --reuid=65534 env -i HOME=/root` ⇒ `touch $HOME/probe` 被拒。
|
||
# 以 **root** 跑时 `/root` 写得进去,所以这条只在非 root 下触发 ——
|
||
# 判据落在"**能不能写**",不落在"路径长得像不像"。
|
||
_am_uid="$(id -u 2>/dev/null || echo 0)"
|
||
_am_home_given=0
|
||
if [ -z "${HOME:-}" ]; then
|
||
if [ "$_am_uid" = "0" ]; then
|
||
_am_home="/root"
|
||
else
|
||
# 非 root:从 passwd 库取该 uid 的 home(比猜 /root 或 /home/$USER 准)
|
||
_am_home="$(getent passwd "$_am_uid" 2>/dev/null | cut -d: -f6 || true)"
|
||
fi
|
||
else
|
||
_am_home_given=1
|
||
_am_home="$HOME"
|
||
fi
|
||
# 目录必须落在**调用者写得进去**的地方,否则就是把问题往后推。
|
||
# 注意:`mkdir -p` 对已存在的不可写目录会**返回成功**,所以必须再单独判 `-w`。
|
||
_am_fallback="${TMPDIR:-/tmp}/agentmail-home-$_am_uid"
|
||
if [ -z "$_am_home" ] || [ ! -d "$_am_home" ] || [ ! -w "$_am_home" ]; then
|
||
if [ "$_am_home_given" = "1" ] && [ -n "$_am_home" ]; then
|
||
# 调用者显式给了 HOME 却不可写:**说清楚**再改判,不静默换目录
|
||
# (静默改 HOME 会让"东西写到哪去了"变成一个谜)。
|
||
printf ' [WARN] 调用者给的 HOME=%s 不可写(非 root 且非该用户所有);改判到 %s\n' \
|
||
"$_am_home" "$_am_fallback" >&2
|
||
fi
|
||
_am_home="$_am_fallback"
|
||
fi
|
||
if mkdir -p "$_am_home" 2>/dev/null && [ -w "$_am_home" ]; then
|
||
if [ "$_am_home" != "${HOME:-}" ]; then
|
||
HOME="$_am_home"
|
||
export HOME
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS HOME=$HOME"
|
||
fi
|
||
else
|
||
# 连一处可写的 HOME 都找不到 ⇒ **这是环境不足**,按约定 exit 2,
|
||
# 并且用人话说明"不要把它当代码问题"(调用者的 go/npm 报错会更晦涩)。
|
||
printf '\n [FAIL] 环境不足:找不到可写的 HOME(试过 %s)\n' "$_am_home" >&2
|
||
printf ' 这是**环境问题**,不是代码问题。药方:在可写的环境里重跑,或显式给 HOME=<可写目录>。\n' >&2
|
||
exit 2
|
||
fi
|
||
|
||
# ② TMPDIR:构建暂存与测试都往这里写。缺省 /tmp 可能与真实挂载不符,
|
||
# 但"空的 TMPDIR"比"指错一个已存在的目录"更糟 —— 后者会让判据看错地方。
|
||
if [ -z "${TMPDIR:-}" ]; then
|
||
TMPDIR=/tmp
|
||
export TMPDIR
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS TMPDIR=$TMPDIR"
|
||
fi
|
||
|
||
# ③ GOMODCACHE / GOPATH:go 的模块缓存。**只在 go 存在时**推导,
|
||
# 且只在两者都空的时候推导(有 GOPATH 时 go 自己会算 GOMODCACHE)。
|
||
#
|
||
# ⚠️ 这里的 `command -v` 依赖 ④ 已经跑过(PATH 就位)—— 见文件头的顺序说明。
|
||
if command -v go >/dev/null 2>&1; then
|
||
if [ -z "${GOMODCACHE:-}" ] && [ -z "${GOPATH:-}" ]; then
|
||
GOPATH="$HOME/go"
|
||
export GOPATH
|
||
AGENTMAIL_ENV_DEFAULTS="$AGENTMAIL_ENV_DEFAULTS GOPATH=$GOPATH"
|
||
fi
|
||
fi
|
||
|
||
export AGENTMAIL_ENV_DEFAULTS="${AGENTMAIL_ENV_DEFAULTS# }"
|
||
|
||
# ②b TMPDIR **可写 + 有空间** —— 与 ① HOME 同一条规则,必须一起落。
|
||
#
|
||
# ★ pi 评审 2026-09-14:① 那边我写了"`mkdir -p` 对已存在的不可写目录会返回成功 ⇒ 必须单独判 `-w`",
|
||
# **同一条规则没落到 TMPDIR 上** —— 而 ENOSPC 正是这条链的元老问题(四次史里第 3 条就是它)。
|
||
# 两种失败形状,后果都是在**中间**炸、报错看起来像工程问题:
|
||
# · `TMPDIR` 已给但**不可写**(`sudo -E`、容器里挂错、只读挂载)⇒ EACCES
|
||
# · `TMPDIR` 可写但**已满**(2026-09-14 实测 `bavail` 真是 0)⇒ `-w` 也抓不到,要的是**空间**判定
|
||
# 所以:`-d` + `-w`,再判可用空间;不足就人话 + exit 2。
|
||
#
|
||
# 口径(与 `plugins/pi-mail-bridge/test/lib/tmp-space.mjs` 一致,但**不 import 它**:
|
||
# `deploy/` 侧要能独立分发,为去重引进平台代码不划算 —— 这里用最小实现 + `df`):
|
||
# · 读不到 ⇒ **不据此判定**("不知道 ≠ 不对",否则在不认识的文件系统上制造恒红的判据);
|
||
# · `0` 是**真的没有**,不是"不知道";
|
||
# · 实测本机 `/tmp` 是 tmpfs、曾 100% 占满(可用 0),连 `go build` 都进不去。
|
||
if [ ! -d "${TMPDIR:-}" ] || [ ! -w "${TMPDIR:-}" ]; then
|
||
printf '\n [FAIL] 环境不足:TMPDIR=%s 不存在或不可写\n' "${TMPDIR:-(空)}" >&2
|
||
printf ' 这是**环境问题**,不是代码问题(构建/测试写临时文件时会以 EACCES 炸在中间)。\n' >&2
|
||
printf ' 药方:TMPDIR=<可写目录> 重跑。\n' >&2
|
||
exit 2
|
||
fi
|
||
# 可用空间:`df -Pk` 第 4 列是可用 KB。读不到就跳过(不判定)。
|
||
_am_avail_kb="$(df -Pk "$TMPDIR" 2>/dev/null | awk 'NR==2 {print $4}')"
|
||
case "${_am_avail_kb:-}" in
|
||
''|*[!0-9]*) : ;; # 读不到/非数字 ⇒ 不知道,不判定
|
||
*)
|
||
if [ "$_am_avail_kb" -le 0 ]; then
|
||
printf '\n [FAIL] 环境不足:TMPDIR=%s 可用空间为 0\n' "$TMPDIR" >&2
|
||
printf ' 这是**环境问题**(本机 2026-09-14 实测过 tmpfs 占满,连 go build 都进不去)。\n' >&2
|
||
printf ' 药方:清理 %s 或改 TMPDIR=<别的可写目录>。\n' "$TMPDIR" >&2
|
||
exit 2
|
||
fi
|
||
AGENTMAIL_TMP_AVAIL_KB="$_am_avail_kb"
|
||
;;
|
||
esac
|
||
unset _am_avail_kb
|
||
|
||
# ③b **我依赖的命令**:环境自足原先只覆盖"变量",没覆盖"命令"。
|
||
#
|
||
# ★ pi 评审 2026-09-14 给出的要害判据(我认,而且这是这条链最容易假绿的地方):
|
||
# **"命令不在"必须走 2(或红)+ 人话;"命令在但输出为空"才是判定结果。**
|
||
# 现在 `redeploy-gateway.sh` 那两处把两者压成了同一个字符串 `"0"`:
|
||
# fc="$(journalctl … 2>/dev/null | grep -icE 'panic|fatal|SIGSEGV' || true)"
|
||
# [ "${fc:-0}" = "0" ] && ok "近 2 分钟无 panic/fatal" ← **一条假绿**
|
||
# journalctl 不在 / 无权限读日志时,它失败(被 `2>/dev/null` 吞掉)⇒ grep 读空输入
|
||
# ⇒ 输出 0、退出 1 ⇒ `|| true` ⇒ `fc="0"` ⇒ 报"无 panic"。
|
||
# 实测复现:`journalctl -u 不存在的-unit` ⇒ `fc=[0]` ⇒ 会打印 [ OK ]。
|
||
# 下面 `sse` 那条同形 ⇒ 报"暂未看到 SSE 重连 —— 检查密钥是否被撤销",
|
||
# **把"工具缺失"归因成"插件没连上"**(人去查密钥,而问题在 journalctl)。
|
||
#
|
||
# ⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 `1`),
|
||
# 所以不能靠管道状态区分 —— 必须显式做前提检查。这就是这一节存在的理由。
|
||
#
|
||
# 调用者声明自己那组(`AGENTMAIL_REQUIRE`,空格分隔),缺一个就 exit 2。
|
||
# 与四次史的处理**同形**,只是对象从变量换成了命令。
|
||
#
|
||
# ★ **新增任何外部命令时,回到调用者的 `AGENTMAIL_REQUIRE` 登记**(pi 评审 2026-09-14)。
|
||
# 这条规矩不是形式主义,有一个我自己刚踩的实例:我加了"同时性"那一列(部署锁)、
|
||
# 引入了新命令 `flock`,**却没回到这张表登记**。后果实测:
|
||
# PATH 里没有 flock ⇒ `flock: command not found`(127)⇒ `! flock` 为真
|
||
# ⇒ 打印"**另一个部署正在跑(锁被占用)**" —— 退出码事后是对的(2),
|
||
# 但**诊断是错的**,而照着它做的是"等另一个部署结束":**永远等不到**。
|
||
# 即"**表与被表的东西不同步**":新能力自带的依赖没有登记回表。
|
||
# 所以每次给部署路径加一个新命令,都要回来加一个词。
|
||
if [ -n "${AGENTMAIL_REQUIRE:-}" ]; then
|
||
_am_missing=""
|
||
for _am_c in $AGENTMAIL_REQUIRE; do
|
||
command -v "$_am_c" >/dev/null 2>&1 || _am_missing="$_am_missing $_am_c"
|
||
done
|
||
if [ -n "$_am_missing" ]; then
|
||
printf '\n [FAIL] 环境不足:缺少命令%s\n' "$_am_missing" >&2
|
||
printf ' 这是**环境问题**,不是代码问题。("命令不在"与"命令在但输出为空"必须分开:\n' >&2
|
||
printf ' 前者会让下游把"工具缺失"读成"检查通过"。)\n' >&2
|
||
exit 2
|
||
fi
|
||
unset _am_missing _am_c
|
||
fi
|
||
|
||
# 调用者打一行,让"兜了什么"**可见**(也是"忘了 source"的信号 —— 少了这行就不对)。
|
||
agentmail_env_report() {
|
||
if [ -n "$AGENTMAIL_ENV_DEFAULTS" ]; then
|
||
printf ' 环境兜底:%s\n' "$AGENTMAIL_ENV_DEFAULTS"
|
||
else
|
||
printf ' 环境兜底:(无 —— 调用者已提供全部)\n'
|
||
fi
|
||
}
|