hermes update 不再打断你的任务:gateway 优雅排空、Docker 镜像保护、Windows 精准击杀

你有没有过这种经历:一个长任务正跑到一半,你随手敲了 hermes update,下一秒 gateway 被整个杀掉,对话当场中断,辛辛苦苦跑出来的结果也跟着没了?在 Windows 上更糟——更新器一度会杀掉机器上所有 hermes.exe,连别的安装、别的项目都一起陪葬。过去几周,官方一直在重做升级这件事(campaign #91277),最近一批改动已经全部合并:升级开始变得“温柔”了,正在跑的任务会先排空再退出,远程连接不会断,Docker 容器里的安装则直接被保护起来。
升级曾经是一场“豪赌”
旧版的更新流程本质上是一条“强杀”路线:hermes update 需要解锁 venv 文件,于是它把运行中的 gateway 直接树杀(tree-kill),不管 gateway 当时是不是正卡在一个漫长的回合里。识别哪些进程属于自己,靠的是 argv 模式扫描——既不准,还容易误伤。
在 Windows 上这个问题更尖锐:gateway 要“应用关闭后仍然存活”,更新才能进行,两者在旧的暂停机制下只能二选一。更新落到一个进行中的回合头上,那一回合就没了。
改进一:gateway 优雅排空而不是被强杀(#95695)
现在 hermes update 会先通过控制 socket 给运行中的 gateway 发一个 pause-for-update 动词,让它完成当前回合、交付最终回复、释放所有 venv 文件句柄,然后自行退出——这就是 gateway 处理 SIGUSR1 和服务重启时用的同一条排空路径(拒绝新回合 → 跑完当前回合 → 交付响应 → 停止)。
如果连不上控制 socket(比如 gateway 太旧、没有这个动词),客户端会得到 None,自动回退到老的强杀路径,行为保持完全不变。正常的 ACK 会带上 pausing / already_stopping / pid / drain_timeout,更新器按 gateway 自己声明的排空预算等待(再多给 10 秒收尾),不再用太短的本地超时把还在跑回合的 gateway 强行掐死。
对普通用户来说,这意味着:升级时正在跑的回合会跑完,而不是被一刀切断。
改进二:远程 serve 后端更新后继续存活(#95576)
如果你用 hermes serve --host <ip> 给远程 Desktop 提供后端,旧版更新流程根本不知道这个进程存在:它不在运行时清单里,--status 也看不到它,一旦被什么东西杀掉就再也不会重启,远程客户端只能对着一个死掉的端点发呆。
修复后,serve/dashboard 后端通过 spawn ledger 注册自己实际绑定的 host/port/profile(这刻意不用 argv 模式扫描,而是进程自注册身份),更新后它们继续在记录端点上运行,远程连接不中断。顺带还修了一个不对称问题:serve 后端以前能被 hermes dashboard --stop 杀掉,却藏在 --status 的视野之外——现在 --status 会列出它们了。
改进三:Windows 更新器只杀“这一个” hermes.exe(#95086)
这是最吓人的一个修复。旧版 Windows 更新器用 taskkill /IM hermes.exe 按镜像名杀进程——机器上凡是叫 hermes.exe 的进程全灭,包括其他安装目录、其他项目正在跑的任务(issue #91964 就是更新器 shim 误杀无关安装的真实案例)。
现在 force_kill_other_hermes() 只终止完整路径匹配本安装 venv Scripts 目录的进程(Toolhelp32 快照 + QueryFullProcessImageNameW,大小写不敏感)。同一台机器上多个 Hermes 安装互不干扰,升级时只有“自己”会被处理。
改进四:SSH 后端和外来 HOME 不再被误伤(#95641)
清理陈旧后端的逻辑也收敛了:更新时的 stale-backend 清扫不再杀掉 SSH 拥有的后端(SSH 所有权只在更新期间保留),也不会从“别人的” HERMES_HOME 里重生后端(#94030)。以前更新一次,远端 SSH 会话和你其他目录的进程都可能被误杀,现在这些边界都被守住了。
改进五:Docker 镜像管理的安装,直接拒绝就地更新(#95722)
Docker、Nix、apt 这类镜像/包管理的安装,更新方式应该是重新拉取镜像、重建容器,而不是在容器里跑 hermes update 去改一个只读镜像。以前三条更新入口(hermes update、hermes update --check、桌面端 Update 按钮)各自维护一套 Docker/Nix/apt 启发式判断,而且容器里 bind-mount 的代码仓库会伪装成 git 安装,骗过检查——拒绝之后也不留任何记录。
现在镜像构建时会把 /etc/hermes/image-provenance.json 烘烤进镜像(放在 bind-mount 目录和 HERMES_HOME 卷之外),三条更新入口统一走一个 admission gate:
- 标记存在(哪怕损坏)→ 判定为镜像管理安装,就地更新被拒绝,CLI 以退出码 2(refused-by-contract)结束,并给出提示:
not updatable in place (<code>); use: <command>; - 标记缺失 → 按原有启发式逻辑继续判断;
hermes update --plan也会如实显示updatable_in_place=False,哪怕 bind-mount 的 checkout 骗过了旧的 git 启发式。
你的升级流程应该是什么样
普通安装(curl 脚本装的那种)照旧:
hermes update --check # 先看更新计划,确认没有意外
hermes update # 执行升级:gateway 会排空,serve 后端存活
Docker 用户请记住:不要在容器里就地 update。正确姿势是拉新镜像、重建容器:
docker pull <your-hermes-image>:latest
docker compose up -d --build # 或按你的编排方式重建
Windows 多安装用户:现在可以放心了,更新只碰自己那份安装。升级前想看看都有哪些后端进程在跑,hermes status 会列出 serve/dashboard 后端,心里有数再动手。
小结
这一轮改动没有加新功能,但把“升级”这件事从“随时可能翻车”变成了“可以放心在日常工作中执行”:回合会跑完、远程连接不会断、其他安装不会被误杀、容器安装不会把自己搞坏。再配合我们之前介绍过的 gateway 循环看门狗 和 Docker 共享容器玩法,日常运维的可靠性又上了一个台阶。这些改动目前都在 main 分支上,尚未进入正式 release 标签——想立刻用上,跑一次 hermes update 到最新开发版即可;等不及的话,v0.20.5 的发布说明 里的功能已经是上一波运维改进的汇总,可以先去翻翻。