跳转至

ADR-0009:放弃预提交自检,改为换后观测

日期:2026-07-23 状态:已接受

修订关系:本决定取代 ADR-0004、ADR-0007 里关于"预提交自检"的部分(它原本是本地三防线的第一道)。

这份记录在讲什么

热修会把主程序 exe 换成新的。换完之后,怎么保证"换上去的新 exe 真的能起来、没把用户的软件搞坏"?原方案设计了三道防线,其中第一道叫预提交自检:在正式替换之前,先由还在运行的旧 exe 把新 exe 以 --selftest 模式拉起来试一次,能正常起来才允许替换,起不来就放弃。

本轮评审我们从头问了一句:这道自检,到底有没有必要?结论是——没有,砍掉。这份记录说明为什么。

决定

去掉预提交自检。热修的完整链路简化为:下载 → 用 Ed25519 验签 → 替换 → 重启后生效,中间不再插入"换之前先自测一遍"这一步。

至于"新 exe 到底起不起得来",改成换上去之后再观测它的真实表现来判断(具体两层机制见 ADR-0004 修订版)。

为什么砍掉它

一、它想抓的问题,别人已经先抓了。

自检能发现的是"粗故障"——比如打包签名坏了、少了个核心 DLL、启动早期就崩。但这类故障,canary(灰度第一档的内部设备)在真实用户拿到热修之前就会撞上、就会拦下。而自检抓不到的那类"只在某台机器上起不来"(比如 CEF 初始化在特定机型上失败),又正好由后面的启动标记机制用真实启动去兜。上有 canary、下有启动标记,自检夹在中间,能独家覆盖的情况几乎没有。

二、就算想做,也只能做个"浅"的,抓不到要害。

StreamFab 有单例保护(QStreamSingleApplication,在 Stream/main.cpp:847 构造)。自检子进程和正在运行的旧 exe 同名,一启动就会发现"已经有一个实例在跑了",于是在 main.cpp:1148 直接给旧实例发个消息、然后 return 0 退出——它永远返回"成功",其实什么都没测。想让自检深入到 CEF 那一层去测真正容易出问题的地方,又会因为 CEF 的共享内存是按安装目录做键的(main.cpp:648),跟正在运行的实例撞车、甚至污染用户当前的会话。

三、让"嫌疑人自己证明自己没问题",本来就不可靠。

自检跑的是新 exe 自带的代码——如果这个新 exe 本身是坏的,凭什么相信它的自检会如实说"我坏了"?相比之下,换后的启动标记是由不参与热修的外部程序去观察新 exe 的真实启动结果,这种"外部旁观"比"自证清白"可信得多。

四、业界成熟做法里也没有这一步。

Chrome 的 safe-seed、Sparkle 的更新,走的都是"验签 → 替换 → 观察 → 崩了 N 次就回退",没有谁在替换前先自测一遍。我们方案参考的 safe-seed 范本(见业界调研《双份规则包》一节)里,同样没有这一环。

这么定之后会怎样

  • 客户端不用再新增 --selftest 模式,随之而来的单例冲突、CEF 隔离、"自检要测多深"等一堆麻烦,全部消失,方案更简单。
  • 本地安全网的全部重量,落到"换后观测"的两层机制上(首启监视 + 启动标记)——它们的细节在 ADR-0004 里抠死。
  • 唯一没被覆盖的极端情况是"新 exe 连 main() 都进不去就崩了"(比如缺 DLL、被杀软隔离)。这一条由外部的首启监视兜底(见 ADR-0007),不是自检能解决的。