第 9 章:Mihomo 双运行模式与核心生命周期
同一份 runtime 配置可以交给特权 Service,也可以由应用直接拉起 Sidecar。难点不在两条 start 命令,而在选择、交接、ready 判定、日志接管、代理恢复和所有权丢失。本章把 CoreManager 的生命周期当成一个监督协议来读。
一、两种模式分别解决什么
1.1 Sidecar
桌面应用通过 Tauri shell 启动随包分发的 verge-mihomo:
- 不需要预安装系统服务;
- 适合普通系统代理场景;
- 生命周期由当前应用直接监督;
- 进程句柄和 PID 保存在 CoreManager;
- Windows 使用 Job Object,父进程退出可终止子进程。
1.2 Service
独立特权服务接收配置和核心路径,然后托管 Mihomo:
- 可以执行需要权限的 TUN/系统级动作;
- 桌面应用升级或窗口销毁不必等同于核心退出;
- 需要版本协议、owner credential 和 session proof;
- 通过
clash_verge_service_ipc通信; - 不同平台有不同安装器:Windows Service、Linux systemd、macOS launch daemon。
二、RunningMode 只记录核心当前由谁托管
enum RunningMode {
Service,
Sidecar,
NotRunning,
}它没有表达“Service 是否安装”“是否需要修复”“用户是否允许降级”。这些属于 RunState 的其他维度,下一章详讲。
CoreManager 自身只保存 Sidecar 子进程句柄、readiness generation 和生命周期互斥等监督数据;RunningMode 则委托给统一 RunState store,避免两个模块各有一份模式真相。
三、启动决策表
startup_decision(status, service_required) 将 ServiceStatus 映射为:
| 服务状态 | 不强制 Service | 必须 Service(如非管理员 TUN) |
|---|---|---|
| Ready | Service | Service |
| NotInstalled | Sidecar | Wait |
| SidecarAllowed | Sidecar | Sidecar(同时 suppress TUN) |
| NeedsReinstall / Unavailable | Wait | Wait |
| Install/Reinstall 请求 | Wait | Wait |
“Wait”不是死循环,而是保持 UI 可用,等待用户在 ServiceMigrationDialog 中选择安装、修复或本次会话继续 Sidecar。
四、一次 start 的外围协议
start_core() 先获取 lifecycle_lock,然后通过 run_core_start_transition 执行:
start_core_inner()
↓
确认 RunningMode != NotRunning
↓
apply_proxy_after_start()外围还要求 core_starting() 与 core_start_settled() 成对:
- starting 暂时关闭 PAC,避免核心交接时把客户端指向无人监听的端口;
- 成功 start 会按新 mode 打开 PAC;
- 任何 early return 的 settled 都重新让 PAC 跟随当前 mode。
因此即使候选配置在真正 stop 前就被拒绝,旧核心仍运行,settled 也会重新开放 PAC。
五、Service 启动路径
5.1 先探测协议兼容性
服务版本回复包含 protocol epoch/revision、服务版本和能力。只看到进程存在不代表可用:旧服务可能能连接,却不理解当前 RuntimeBundle 或 owner proof。
5.2 构造 RuntimeBundle
Service 不能只收到一个配置文件路径。路径中的 provider 相对引用、选择的 stable/alpha core、资源文件和工作目录都需要在特权侧得到一致解释。
collect_service_runtime_bundle() 收集并规范化:
- runtime config;
- 选定 core 二进制;
- provider 文件;
- 工作目录和日志 writer 信息。
服务支持 staging 时,可先接收候选 bundle,再原子替换运行核心,减少完全停止窗口。
5.3 OwnerSessionProof
服务成功启动后,桌面端保存包含 generation 和随机 token 的 session proof。后续 stop、stage、系统代理更新都需要当前 proof。
它解决的是“服务是全机共享能力,哪个桌面会话有权控制当前核心”。只验证用户 uid 不够,因为同一用户可能启动了新应用实例,旧异步任务不应继续操作新会话。
六、Sidecar 启动路径
Sidecar 由 Tauri shell command 启动,CoreManager 保存 CommandChild。重要步骤包括:
- 选择 stable/alpha 二进制;
- 传入 runtime config 与工作目录;
- 接管 stdout/stderr 到 AsyncLogger;
- 在 Windows 将进程加入 Job Object;
- 轮询 Mihomo API 判断 readiness;
- ready 后记录 RunningMode::Sidecar;
- 启动进程终止监控。
6.1 为什么 readiness 不能等同于 spawn 成功
spawn 只证明操作系统创建了进程。配置加载、端口绑定和 controller socket 可能随后失败。项目通过有界轮询实际 API 响应确认 ready,避免把“活了 20ms 的进程”报告为可用核心。
6.2 进程终止竞态
旧 Sidecar 的 wait task 可能在新 Sidecar 已启动后才收到退出。清理时要比较 terminated PID 是否仍是当前 child,只有当前进程终止才能清空状态。否则旧 watcher 会把新核心误标为 NotRunning。
七、readiness generation
CoreManager 用一个 AtomicU64 同时编码:
- generation;
- active bit。
核心 ready 时生成新的 active generation;停止时失效;异步恢复代理或 watcher 捕获 generation 后,执行前再次比较。
为什么不用 bool?bool 只能说“现在 ready”,不能证明“还是我当时看到的那个核心”。A 停止、B 启动后 bool 又回 true,A 的旧任务会误把 B 当成自己。generation 可以区分不同代。
八、重启不是 stop + start 的随意拼接
不同模式和平台需要不同停止顺序:
- macOS Service:先通过服务 stop guard;
- 其他 Service/Sidecar:先清系统代理;
- stop core;
- start 新核心并确认 ready;
- 恢复系统代理;
- 只有 generation/mode/owner 都未变才接受迟到的恢复结果。
项目把这些顺序提取为可注入闭包的 transition helpers,使单元测试可以验证顺序而不真的启动服务。
九、Sidecar → Service 交接
用户完成服务安装时,当前 Sidecar 可能仍在提供代理。安全交接的目标是:
保持旧 Sidecar 可用
↓
等待 Service IPC ready
↓
清理系统代理/guard,关闭 PAC
↓
停止 Sidecar
↓
由 Service 启动同一 runtime
↓
确认 Service core ready + owner proof
↓
恢复系统代理和 PAC交接失败要区分:
- Service 尚未 ready:稍后再试;
- 已经由 Service 托管:无需重复;
- 切换中失败:尝试回退 Sidecar。
Windows 还有单实例 handoff watcher 标志,防止多个安装完成事件并发执行交接。
十、Service owner monitor
进入 Service 模式后,后台任务周期读取 owner status。它不能“一次读失败就重启”:服务更新、系统忙和短暂 IPC 中断都会产生噪声。
OwnerWatch 区分:
- Healthy;
- Unreadable(连续达到阈值后才检查 transport);
- NotOwner / NotActive;
- CoreMissing(允许短暂 settling);
- Fatal / WantsStopped。
连续 3 个 unreadable sample 后,才通过 Mihomo endpoint 判断是“仅 status 通道坏了”还是“整个 transport 失效”。确认 owner loss 后,generation claim 保证只有一个恢复任务执行。
十一、日志在两种模式下的路径
- Sidecar:应用直接接管进程 stdout/stderr,写入
CLASH_LOGGER; - Service:通过服务 IPC 获取快照/流,并使用独立 writer 配置。
前端只调用 get_clash_logs,CoreManager 根据 RunningMode 路由。模式差异留在后端,不渗透到日志页。
十二、生命周期的核心不变量
- 同一时间最多一个 start/stop/restart/handoff。
- RunningMode 只在核心真正 ready 后变为 Service/Sidecar。
- PAC 可用性永远从 RunningMode 派生,并在 start 临界区短暂关闭。
- 旧进程、旧 owner、旧 readiness 的异步结果不能修改新一代状态。
- start 返回错误不自动等于 NotRunning;必须读取事实状态。
本章源码索引
src-tauri/src/core/manager/mod.rs:CoreManager、RunningMode、readiness generationsrc-tauri/src/core/manager/lifecycle.rs:启动决策、transition helpers、交接src-tauri/src/core/manager/state.rs:Sidecar spawn、ready、日志与终止监控src-tauri/src/core/manager/config.rs:配置替换与 reload/restartsrc-tauri/src/core/service.rs:Service 安装、bundle、owner session 与 monitorsrc-tauri/src/core/runtime_bundle.rs:特权运行包src-tauri/src/core/owner_identity.rs:当前用户 owner credentialssrc-tauri/src/core/runstate/owner.rs:OwnerWatch 收敛状态机