Files
MailUI4Agents/deploy/lib/env-defaults.sh
JianFeeeee 4c2bf26c42 fix(deploy): flock 没登记进 AGENTMAIL_REQUIRE("缺命令"被报成"另一个部署在跑")+ 中断 trap + 两条欠账入册
**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 条)。
2026-09-14 21:26:51 +08:00

212 lines
12 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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
}