Files
MailUI4Agents/deploy/lib/env-defaults.sh
JianFeeeee 8e3b04a267 fix(deploy): TMPDIR 只判"未设"(同文件里 HOME 判了可写)+ 环境自足漏了"命令"(journalctl 两处是假绿)
pi 给了"第五次"的两条线索,都在我读得到的地方,逐条实测确认后修完:

**1. TMPDIR 与 HOME 不同规则(就在同一个文件里)**
① `HOME` 那边写了两条规则:`mkdir -p` 对**已存在的不可写目录会返回成功** ⇒ 必须单独判 `-w`;
判据落在"能不能写"不落在"路径像不像"。**同一条规则没落到 ② `TMPDIR` 上** ——
而 ENOSPC 正是这条链的元老问题(四次史里第 3 条就是 TMPDIR)。两种失败形状:
已给但**不可写**(EACCES)、可写但**已满**(`-w` 抓不到,要的是**空间**判定)。
已补 `-d` + `-w` + 可用空间(`df -Pk`,读不到⇒**不据此判定**;`0` 是**真的没有**);
不足 ⇒ 人话 + exit 2。**不 import** 插件那份 `test/lib/tmp-space.mjs`:
`deploy/` 侧要能独立分发,为去重引进平台代码不划算(按既定理由,写最小版本)。
实测 `TMPDIR=/root/nope` ⇒ `[FAIL] 环境不足:TMPDIR=… 不存在或不可写` + 退出码 2。

**2. 环境自足只覆盖"变量",没覆盖"命令" —— 其中 journalctl 两处是假绿**
   这节的要害是 pi 给的那句判据,我认:**"命令不在"必须走 2/红 + 人话;
   "命令在但输出为空"才是判定结果。** 原先两处把两者压成同一个字符串 `"0"`:
   journalctl 失败(被 `2>/dev/null` 吞掉)⇒ grep 读空 ⇒ `fc="0"` ⇒ **打印"无 panic/fatal"**。
   实测复现:`journalctl -u 不存在的-unit | grep -icE 'panic'` ⇒ `fc=[0]`。
   `sse` 那条同形、后果更坏:**把"读不到日志"归因成"插件没连上"**,让人去查密钥。
   ⚠️ 顺带实测:**`PIPESTATUS` 分不开这两种情况**(命令不存在与"存在但无匹配"都给 1),
   所以不能靠管道状态区分 —— 必须**先取输出、成功后再过滤**,命令存在性另做前提检查。
   改法:两处都改成"先取日志、看退出码";读不到 ⇒ `warn` 明说"读不到、无法据此判断"
   (既不假绿也不假红)。并给三个脚本加 `AGENTMAIL_REQUIRE` 前提检查
   (缺一个 ⇒ exit 2 + 人话),与四次史的处理**同形**,只是对象从变量换成命令。
   实测:`AGENTMAIL_REQUIRE` 里放不存在的命令 ⇒ 退出码 2。

**3. 顺带修 pi 点到的两处同族问题**
· `install.sh` 的 `HEAD_REV="$(git … rev-parse --short HEAD)"`:`set -e` 下失败**直接中止**
  (实测退出码 127、无翻译);而且 HEAD_REV 为空会让下一句报
  "这个包比源码旧:产物 gitRev=… ≠ HEAD=" —— **把"这里不是 git 仓库"说成"产物过期"**。
  已改成显式判失败 + 明说"读不到当前 HEAD,跳过新旧比对"。
· 同块第 94 行末尾挂着一个 `|| true` ⇒ 整行退出码恒 0 ⇒ 它作为 `if` 条件**永远为真**
  ("判据的形式在、区分力不在")。已改成显式计算、去掉 `|| true`。

docs 补两条纪律:15「"命令不在" ≠ "命令在但输出为空"」(含 PIPESTATUS 分不开的实测)、
16「一条规则写了,要检查它是否落到了所有同类对象上」。

验证:install.sh --check 空环境 exit 0、正常 exit 0;TMPDIR 不可写 exit 2;
npm test exit 0;prune 自检 22/22;drift 自检 35/0;check-shared-libs exit 0。
2026-09-14 21:08:48 +08:00

203 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。
# 与四次史的处理**同形**,只是对象从变量换成了命令。
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
}