Skip to content

第 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 只记录核心当前由谁托管

rust
enum RunningMode {
    Service,
    Sidecar,
    NotRunning,
}

它没有表达“Service 是否安装”“是否需要修复”“用户是否允许降级”。这些属于 RunState 的其他维度,下一章详讲。

CoreManager 自身只保存 Sidecar 子进程句柄、readiness generation 和生命周期互斥等监督数据;RunningMode 则委托给统一 RunState store,避免两个模块各有一份模式真相。

三、启动决策表

startup_decision(status, service_required) 将 ServiceStatus 映射为:

服务状态不强制 Service必须 Service(如非管理员 TUN)
ReadyServiceService
NotInstalledSidecarWait
SidecarAllowedSidecarSidecar(同时 suppress TUN)
NeedsReinstall / UnavailableWaitWait
Install/Reinstall 请求WaitWait

“Wait”不是死循环,而是保持 UI 可用,等待用户在 ServiceMigrationDialog 中选择安装、修复或本次会话继续 Sidecar。

四、一次 start 的外围协议

start_core() 先获取 lifecycle_lock,然后通过 run_core_start_transition 执行:

text
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。重要步骤包括:

  1. 选择 stable/alpha 二进制;
  2. 传入 runtime config 与工作目录;
  3. 接管 stdout/stderr 到 AsyncLogger;
  4. 在 Windows 将进程加入 Job Object;
  5. 轮询 Mihomo API 判断 readiness;
  6. ready 后记录 RunningMode::Sidecar;
  7. 启动进程终止监控。

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 可能仍在提供代理。安全交接的目标是:

text
保持旧 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 路由。模式差异留在后端,不渗透到日志页。

十二、生命周期的核心不变量

  1. 同一时间最多一个 start/stop/restart/handoff。
  2. RunningMode 只在核心真正 ready 后变为 Service/Sidecar。
  3. PAC 可用性永远从 RunningMode 派生,并在 start 临界区短暂关闭。
  4. 旧进程、旧 owner、旧 readiness 的异步结果不能修改新一代状态。
  5. start 返回错误不自动等于 NotRunning;必须读取事实状态。

本章源码索引

  • src-tauri/src/core/manager/mod.rs:CoreManager、RunningMode、readiness generation
  • src-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/restart
  • src-tauri/src/core/service.rs:Service 安装、bundle、owner session 与 monitor
  • src-tauri/src/core/runtime_bundle.rs:特权运行包
  • src-tauri/src/core/owner_identity.rs:当前用户 owner credentials
  • src-tauri/src/core/runstate/owner.rs:OwnerWatch 收敛状态机

原创文档 CC BY-SA 4.0 · 站点代码 MIT