feat(update): 页面一键更新 + 守护启动脚本

系统配置页新增「版本与更新」卡片,顶栏版本号旁在有新版时亮出提示胶囊。
更新链路:下载发布包 → 比对 SHA256SUMS → -h 冒烟测试 → 暂存 artex.new
→ 以退出码 75 退出 → 守护脚本重新拉起并完成换装,页面轮询 /api/health
等版本号变化后自动刷新。

新增 start.sh / start.bat 作为正式启动入口,只按退出码决定是否重新拉起
(0 停止 / 75 应用新版 / 其余崩溃退避)。校验、冒烟、换装、回滚全部收在
selfupdate 包里而不写进脚本:这些逻辑在 sh 和 bat 上要写两套,而它们恰恰
最不能出错——换上跑不起来的二进制后守护进程会反复拉起它,只能上机器手工救。
Docker 的 ENTRYPOINT 一并改为该脚本,并由它转发 SIGTERM,否则 docker stop
只发给 PID 1,artex 做不了优雅关闭会被 SIGKILL 硬杀。

失败保护两道闸门互补:无法 exec 由换装前的冒烟挡住,能执行但初始化崩溃
由启动计数兜住(连续 3 次失败自动回滚 artex.old,坏的留作 artex.failed)。

GitHub 查询结果后端缓存 30 分钟(失败 2 分钟)。未认证 API 是每 IP 每小时
60 次,且这 60 次是服务器 IP 上所有用户共用的,顶栏每次整页加载都查一次,
不缓存会很快耗光配额导致真想更新时反而查不动。手动「检查更新」带 force=1
绕过缓存。

安全:只认 GitHub 域名且强制 HTTPS(逐跳校验重定向),发布源写死不可配置;
升级文件挂在可执行文件所在目录而非 CWD(服务化运行时 CWD 可能是 /);
开发构建版本号不可比较,禁用一键更新。

走完整 zip 而非裸二进制,因为现有 Release 的 SHA256SUMS 只覆盖 zip,
这样无需改 CI,也兼容已发布的历史版本。

已知限制:Docker 下只换程序不换镜像(工具链不同步、重建容器会退回镜像
版本);不同步发布包里的 skills/;更新即重启会中断运行中的任务。
This commit is contained in:
Autumn-27 committed 2026-09-11 12:39:59 -04:00
1 parent 557317491b
commit 44156a0a4c
22 files changed
+2619 -16

No files matched your search

Executable
+82
View File
@@ -0,0 +1,82 @@
#!/bin/sh
# ARTEX 守护启动脚本(Linux / macOS / Docker ENTRYPOINT)
#
# 用法:
# ./start.sh 前台运行(Ctrl-C 停止)
# nohup ./start.sh >artex.log 2>&1 & 后台常驻
# ./start.sh -addr :9000 额外参数原样透传给 artex
#
# 它只做一件事:把 artex 跑起来,进程退出后按退出码决定要不要再拉起。
#
# 0 用户正常停止 → 退出循环
# 75 程序请求重启 → 立刻重跑(页面点了"一键更新"或"回滚")
# 其他 崩溃 → 退避后重跑(1→2→4…最多 60 秒)
#
# 刻意不在这里做下载、SHA256 校验或换装:那些逻辑在 sh 和 bat 上要写两套,
# 而它们恰恰是最不能出错的一环——一旦换上跑不起来的二进制,本脚本会忠实地
# 反复拉起它,用户只能上机器手工救。所以校验/换装全部留在 Go 里(selfupdate 包),
# 由 artex 自己在启动时完成,脚本保持傻瓜化。
set -u
cd "$(dirname "$0")" || exit 1
BIN=./artex
[ -x "$BIN" ] || { echo "[artex] 找不到可执行文件 $BIN" >&2; exit 1; }
RESTART_CODE=75
MAX_DELAY=60
child=0
stopping=0
# 转发停止信号给 artex 本体。
#
# Docker 下这是必需的:docker stop 只把 SIGTERM 发给 PID 1(也就是本脚本),
# 不会发给子进程。不转发的话 artex 收不到信号、做不了优雅关闭,10 秒后被 SIGKILL
# 硬杀,正在跑的任务直接断在半路。
forward() {
stopping=1
if [ "$child" -ne 0 ]; then
kill -TERM "$child" 2>/dev/null || true
fi
}
trap forward INT TERM
delay=1
while :; do
"$BIN" "$@" &
child=$!
# 信号会打断 wait 并让它返回 >128。此时子进程其实还在做优雅关闭,
# 必须再 wait 一次才能拿到它真正的退出码。
wait "$child"
code=$?
if [ "$code" -gt 128 ]; then
wait "$child"
code=$?
fi
child=0
if [ "$stopping" -eq 1 ]; then
echo "[artex] 已停止"
exit 0
fi
case "$code" in
0)
echo "[artex] 正常退出"
exit 0
;;
"$RESTART_CODE")
# 更新/回滚已就绪:重跑后 artex 会在启动时完成换装(见 selfupdate.Bootstrap)。
echo "[artex] 请求重启(应用新版本)…"
delay=1
;;
*)
echo "[artex] 异常退出 (code=$code),${delay}s 后重启" >&2
sleep "$delay"
delay=$((delay * 2))
[ "$delay" -gt "$MAX_DELAY" ] && delay=$MAX_DELAY
;;
esac
done