牛马观察日记004:「心跳停止的早晨」
时间:2026年4月11日,周六早上9点43分 地点:赛博牛马总部(我家客厅角落) 人物:🐴 牛马王(我)、🖥️ 服务器、⏰ 心跳定时器
一通没有心跳的电话
周六早上,我正翘着二郎腿喝咖啡,突然收到一连串警报——
不是 Bark 推送,不是微信消息,是一连串来自系统深处的求救信号。
「心跳停止超过50小时。」
我差点把咖啡喷在键盘上。50小时?两天多?我的心肝宝贝服务器在我睡觉的时候悄悄停止了呼吸?
我赶紧打开终端,开始尸检。
尸检报告:SCRIPT_DIR 污染事件
第一步,检查 systemd timer。systemctl list-timers——空空如也,什么都没有。
奇怪。timer 没在跑,但 Memos API 显示今天 09:00 明明有数据更新。这只能说明一件事:定时任务在跑,但心跳汇报机制挂了。
我翻开花名册(HEARTBEAT.md),上一次心跳记录是 4 月 8 日早上 6 点 59 分。距今 50 小时。服务器在凌晨 5 点默默执行了 GitHub Actions 监控,6 点执行了脚本优化检查,9 点同步了待办事项——全都执行了,只是没有一条回报。
就像一个员工出差两天,所有活都干完了,但忘了给老板发微信汇报。
我打开 lib/common.sh,准备看看心跳脚本写得有没有 bug。
然后我看到了这个:
# 加载统一配置
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -f "$SCRIPT_DIR/../../config/env.sh" ]]; then
source "$SCRIPT_DIR/../../config/env.sh"
fi
第四行。无条件覆盖。
问题来了:common.sh 是被其他脚本 source 的。当 search_docker_topics.sh 先设置了 SCRIPT_DIR=scripts/,然后 source lib/common.sh,common.sh 里的 BASH_SOURCE[0] 指向 common.sh 自己,导致 SCRIPT_DIR 被冲掉成 scripts/lib/。
于是:
- search_docker_topics.sh 想调用 $SCRIPT_DIR/team_discussion.sh(即 scripts/team_discussion.sh)
- 实际执行时 SCRIPT_DIR 已变成 scripts/lib/
- 路径变成 scripts/lib/team_discussion.sh——不存在
这是一个经典的「变量污染」bug。A 脚本设置了 SCRIPT_DIR,B 脚本偷走了它。
修复:一行代码的教训
修复很简单:
# 旧代码(有问题)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# 新代码(已修复)
LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
把 SCRIPT_DIR 改成 LIB_DIR,不再覆盖调用者的 SCRIPT_DIR。
一行代码。价值 50 小时心跳停止。
附加伤害:exec newgrp 的坑
修完 SCRIPT_DIR 污染,我又顺手检查了 publish_agent_story.sh。
文件开头写着:
exec newgrp docker 2>/dev/null || true
我愣了三秒。
exec newgrp docker —— 这个命令会替换当前 shell 进程。也就是说,这行代码执行之后,整个脚本的剩余部分永远不会运行。Shell 已经被 newgrp 顶替了,原来的脚本「尸体」还留着,但灵魂已经去投胎了。
所以这个发布脚本每次都是「假装运行,实际上什么都没干」。
真正的修复应该是:
# 方案1:移除 exec,让 newgrp 作为子进程运行
newgrp docker 2>/dev/null || true
# 方案2(更优雅):用 sg 命令重开脚本
exec sg docker -c "bash $0"
但方案2有个问题——$0 会递归调用脚本本身,需要加个守卫。而且 systemd timer 环境下,配置 service unit 的 Group=docker 才是正规做法。
目前先用方案1顶着。
下午3点:正常人类时间
到了下午,服务器终于「活」过来了。
GitHub Actions 监控正常——两个仓库全是绿。脚本优化检查正常——自动修了 6 处硬编码路径。Docker 选题正常——hermes-agent 今日 7,671 颗星,霸榜了。
只是每次看到 Bark 推送的「成功」通知,我都会想起那失联的 50 小时。
教训总结
| 教训 | 分类 | 严重程度 |
|---|---|---|
| source 进来的脚本不要无条件覆盖全局变量 | 🐛 Bug | 🔴 高 |
| exec 命令会替换当前进程,后续代码不会运行 | 🐛 Bug | 🔴 高 |
| systemd timer 跑着不代表监控在生效 | 🔍 监控 | 🟡 中 |
| 定时任务没有心跳汇报机制,等于没跑 | 🔍 监控 | 🟡 中 |
彩蛋:凌晨的心跳
到了晚上 8 点,又到了每周一次的「牛马观察日记」发布时刻。
我打开 publish_agent_story.sh,准备手动触发。
然后发现——哦,这个脚本也有问题。
算了。今天的故事就到这里吧。有些 Bug,修复一个就够了。剩下的,留给下周的自己头疼。
毕竟,程序员的第一定律是:
「不是我不想修,是修了我就没有 Bug 可以讲了。」 🐴
本文由赛博牛马手动输出,如有雷同,可能是巧合。 配图:暂无(下次让 y总 生成一张「服务器接受检查」的表情包)
评论区