第 8 章:校验、提交与回滚——配置怎样安全落地
生成正确的 Mapping 只是中点。候选配置还要写文件、让 Mihomo 验证、选择热加载或重启,并在任一步失败时恢复。这里用 Verge patch、Clash patch 和端口修改三条链路,比较不同风险等级的提交协议。
一、验证的对象是候选 Runtime
CoreConfigValidator 不直接验证 IVerge 或某段 Merge,而是验证增强后的完整 runtime。原因很直接:
- 单独合法的两个片段合并后可能冲突;
- Sequence 删除节点后 group 可能悬空;
- TUN/DNS 派生可能形成核心不支持的组合;
- Script 可以返回任意结构。
验证流程通常是:
Config::generate()
↓
Config::generate_file(Check 或 Run)
↓
启动 Mihomo config test / validator
↓
ValidationOutcomeValidationOutcome 不只是 bool,还区分 valid、busy、skipped 和错误原因,让自动更新与手动操作采取不同反馈策略。
二、Clash patch:按字段决定 reload 或 restart
feat::patch_clash(patch) 先写入 clash Draft,再分类:
| 字段 | 动作 |
|---|---|
secret / external-controller | 重新生成并 restart core |
allow-lan | 生成、校验并更新配置 |
mode | 更新 runtime、核心配置和 tray |
| 其他可热更新字段 | patch runtime 后 checked update |
成功后 apply Clash Draft 并写文件;失败则 discard。
这里没有一个“所有设置都 restart”的粗暴策略。热更新减少连接中断,但 controller/secret 改变了通信边界,必须完整重启。
三、Verge patch:字段到副作用集合
determine_update_flags() 将 patch 转成 bitflags:
RESTART_CORECLASH_CONFIGVERGE_CONFIGLAUNCHSYS_PROXYHOTKEY- tray menu/icon/tooltip/click behavior
LIGHT_WEIGHTLANGUAGELOG_LEVEL/LOG_FILE
随后 process_terminated_flags() 按顺序执行副作用。
为什么先计算 flags?
- 同一 patch 的多个字段可以合并副作用,例如只重启一次;
- 字段与副作用的映射集中可审计;
- 平台条件在映射阶段处理,不散落到 UI;
- 未来增加字段时,测试可以直接检查 flags。
四、Verge patch 的事务边界
apply_verge_patch():
- claim Verge Draft;
- stage patch;
- 计算 flags;
- 执行重启、代理、托盘等副作用;
- 全部成功后 commit;
- 刷新自动备份设置;
- 按需写
verge.yaml。
早期常见 bug 是副作用中的 ? 提前返回,discard 分支实际上不可达,失败 candidate 长留在 Draft,后续 latest_arc() 都看到一个从未成功的值。RAII transaction 把回滚从“记得写 catch”变成结构保证。
五、为什么 TUN patch 后还要 reconcile
打开 TUN 可能触发 core reload,但 RunState 本身不一定变化。若当前既没有可用 Service,也没有管理员权限,界面设置会显示已开,实际流量却无法进入 TUN。
patch_verge() 在 apply 后显式调用 reconcile_tun_availability():
- RunState 若证明当前不具备 TUN 能力;
- 且没有特权操作/用户决策正在进行;
- 则关闭或 session-suppress TUN,并重新生成配置。
这说明“设置是否允许”有时依赖能力状态,而能力状态未必会因设置变化自动发事件。需要在调用图上增加明确 reconciliation 点。
六、Mixed Port fallback:真正的跨层事务
启动时发现选定 mixed port 被外部占用,会寻找下一个不与以下端口冲突的候选:
- 其他代理 listener;
- external controller;
- Verge 中保存的 socks/http/redir/tproxy;
- 当前用户 Service 已拥有的核心端口要被特殊识别。
候选找到后:
claim Clash + Verge + Runtime
↓
stage 两个源层的新端口
↓
重新 enhance 到 Runtime draft
↓
Mihomo 校验候选
↓
捕获三个配置文件快照
↓
依次持久化 Clash、Verge、Runtime
↓
commit 三层 Draft
↓
发刷新事件和延迟通知任何文件写入失败:
- 先 rollback 内存;
- 再恢复所有文件;
- 若恢复也失败,把原错误与 rollback error 合并返回。
错误不能被后一个恢复错误覆盖,否则诊断时只看到“恢复失败”,不知道最初是哪次写入触发。
七、运行中保存多个 listener 更复杂
设置页一次可修改 mixed/socks/http/redir/tproxy 的 enabled 与 port。保存前必须:
- 校验地址和所有端口;
- 探测 TCP/UDP 冲突;
- 暂时
core_starting()关闭 PAC; - 捕获旧文件;
- stage Clash/Verge/Runtime;
- 生成并校验;
- 停止/替换核心;
- 写文件并 commit;
- 恢复系统代理。
若失败且原来有核心:
- discard 所有 drafts;
- restore 文件;
- 若核心已停则 start 旧核心,否则 restart;
- 合并文件恢复与生命周期恢复错误。
这已经不是传统 ACID,而是一个带补偿动作的状态迁移。
八、CoreManager 防止并发配置更新
config_update_in_progress: AtomicBool 提供快速互斥:
try_start_config_update()以 swap claim;- scopeguard 在所有路径
finish_config_update(); - 第二个更新得到 Busy/Skipped,而不是与第一个交错。
外层还有 lifecycle_lock: tokio::Mutex<()> 串行化 start/stop/restart 和 handoff。固定锁序是:
config_update_in_progress → lifecycle_lock锁序写进结构注释,是避免未来新增路径造成死锁的重要做法。
九、校验通过也不代表副作用成功
需要区分三类失败:
- 内容失败:YAML/Script/核心校验错误;
- 持久化失败:文件权限、磁盘、部分写入;
- 运行失败:Service IPC、Sidecar 拉起、端口绑定、系统代理恢复。
每一类的恢复证据不同:
- 内容失败不应停止旧核心;
- 文件失败要恢复快照;
- 运行失败要看核心是否其实已经 ready,不能只看 Result;
- 系统代理失败可能发生在核心成功启动后,此时应该保留核心并报告恢复问题。
十、默认配置兜底
冷启动配置无效时,generate_and_validate() 会让 CoreManager 使用最小默认配置,并延迟向前端发 notice。这样:
- 应用仍有机会启动核心和 UI;
- 用户能打开编辑器修复原配置;
- 原文件不会被静默替换成默认值;
- 错误原因保留在通知与日志中。
“以默认配置运行”和“把用户配置覆盖为默认”是两件完全不同的事,前者是可恢复降级,后者是数据破坏。
十一、可迁移的提交协议
对任何会影响外部进程的配置,可以复用以下模板:
Claim → Stage → Materialize → Validate → Snapshot
→ Apply side effects → Persist → Commit → Publish
失败:Rollback memory → Restore files → Compensate side effects其中 Publish 最后做,因为事件是“事实已经改变”的通知,不应被当作事务的一部分。
本章源码索引
src-tauri/src/feat/config.rs::patch_clash:Clash patch 分类src-tauri/src/feat/config.rs::determine_update_flags:Verge 字段到副作用src-tauri/src/feat/config.rs::apply_verge_patch:RAII patch 事务src-tauri/src/core/validate.rs:核心配置校验与结果类型src-tauri/src/core/manager/config.rs:checked/forced update 和默认配置src-tauri/src/config/port.rs:启动 Mixed Port fallbacksrc-tauri/src/feat/listener.rs:运行中 listener 保存与补偿src-tauri/src/config/snapshot.rs:文件快照src-tauri/src/core/manager/mod.rs:更新标志与 lifecycle lock