Skip to content

第 10 章:RunState——把运行事实压成一个一致快照

服务健康、核心模式、管理员权限、待执行操作和 Sidecar 降级曾经很容易散成多个布尔值。RunState 的价值,是把这些事实集中建模,并把 `serviceUsable`、`tunCapable` 等结论在后端一次派生后推给所有界面。

一、状态不是枚举,而是多个正交维度

内部 RunState 包含:

text
mode: RunningMode
health: ServiceHealth
pending: Option<PendingAction>
sidecar_allowed: bool
is_admin: bool
op_in_flight: bool

其中:

rust
enum ServiceHealth {
    Unknown,
    Ready,
    NotInstalled,
    VersionMismatch,
    Unavailable(String),
}

如果强行做成一个大枚举,会出现组合爆炸:Ready+Service、Ready+NotRunning、NotInstalled+SidecarAllowed、VersionMismatch+OperationInFlight……正交字段更适合表达现实。

二、派生答案由后端统一计算

对前端暴露的 RunStateView 同时携带原始字段和派生结果:

  • serviceUsable
  • tunCapable
  • serviceNeedsAttention
  • serviceUnavailableReason

2.1 service_readyservice_usable

  • ready:最后一次可靠探测确认协议兼容;
  • usable:ready 且当前没有特权操作进行中。

安装/卸载过程中,服务的已知健康仍可保留为 Ready,但不能被新任务使用。

2.2 tun_capable

满足任一条件:

  • 应用本身已提升权限;
  • Service 当前 usable。

这比前端自行判断 isAdmin || service === 'ready' 更可靠,因为还考虑 op_in_flight

2.3 service_needs_attention

服务仅“未安装”不一定需要弹窗:普通 Sidecar 场景完全可用。只有:

  • 服务存在但版本不兼容/不可用;
  • 或有显式 install/reinstall 等 pending 请求;
  • 且用户尚未允许本次会话 Sidecar;

才需要用户决策。

三、StoredService:观察、请求和会话选择分开

Store 不把所有变化写入一个 status 槽,而是分别处理:

  • observe(health):机器探测得到的事实;
  • request(action):用户/流程提出的特权动作;
  • allow_sidecar():本次会话的降级选择。

观察到新 health 会清除已完成/过期的 pending;请求特权动作会取消 sidecar allowance;允许 Sidecar 会撤回未执行请求。互斥关系在状态模型内部维护,不依赖每个调用方记住。

四、RunStateStore 的环境抽象

RunStateEnv trait 提供:

  • 探测服务版本;
  • 检查平台安装证据;
  • 判断是否 elevated;
  • 控制 PAC availability;
  • publish 新状态;
  • 执行特权 action。

生产使用 RealEnv,测试使用 FakeEnv。状态机因此可以在不安装真实系统服务、不修改代理的情况下覆盖大量组合。

这个抽象的重点不是 mock 框架,而是把“状态变换规则”和“如何观察/改变机器”分开。

五、每次有意义变化都递增 generation

Store 保存 generation counter 与 Notify。变更函数会:

  1. 计算修改前后状态;
  2. 若完全相同,不发布;
  3. 若不同,递增 generation;
  4. 通过 env.publish 推送完整快照;
  5. 唤醒等待 settled 的任务。

完整快照比 delta 更稳健:前端不需要按严格顺序重放事件,丢一次事件后重新读取也能恢复。

六、settled():等待特权操作结束

ServiceManager 的 current() 不直接读瞬时 state,而是等待 op_in_flight=false 后返回一致快照。

实现必须在同一次 snapshot 中检查 operation slot。若先检查 bool、再单独读取状态,中间可能开始新操作,返回值却声称已 settled。

多线程测试专门反复制造这个窗口,固定“不返回描述 in-flight 的 settled snapshot”这一不变量。

七、特权 operation guard

begin_operation() 用单槽保证一次只有一个安装/卸载/修复。Guard 创建和 drop 都发布状态:

text
begin → op_in_flight=true → 前端禁用相关操作
drop  → op_in_flight=false → 前端收到最终可用性

如果只在开始时 publish,界面会永久停在 loading,直到另一个无关事件触发刷新。

八、服务探测分两步:证据与协议

8.1 trusted install evidence

先检查平台层是否有可信安装证据:注册的 Windows Service、systemd unit、launch daemon 等。

  • 没有证据:NotInstalled,不必尝试 IPC;
  • 证据无法检查:Unavailable,不可谎报未安装;
  • 有证据:继续 probe。

8.2 protocol probe

get_version() 回复经过分类:

  • epoch 不同;
  • revision 过低;
  • response code 非零;
  • 缺协议信息;
  • 兼容。

只有兼容才是 Ready。可读但不兼容无需重试 20 次;静默不可达才按次数与 delay 重试。

九、为何保留最后一次已确认 health

一个 Ready 服务在重启期间可能短暂 IPC 失败。单次 transport error 不会立刻把 health 改成 Unavailable,因为这不是新事实,只是“这次没读到”。

只有平台证据和持续失败共同支持,或 owner monitor 确认 transport 丢失,才降级状态。这种设计区分:

  • negative evidence:确认服务不存在/不兼容;
  • absence of evidence:暂时没有回复。

十、Legacy ServiceStatus 是视图,不是主模型

旧调用链仍使用单槽 ServiceStatus:Checking、Ready、NotInstalled、NeedsReinstall、InstallRequired 等。现在它由 RunState flatten 得到,反向写入时再拆成 observe/request/allowance。

迁移策略的价值在于:

  • 新状态机一次落地;
  • 旧 API 不必同时全部重写;
  • flatten/unflatten 函数集中测试;
  • 最终可逐步消除 legacy 枚举。

十一、RunState 如何进入前端

RealEnv.publish 调用 Handle::notify_run_state(),发 verge://run-state-changed 完整 view。前端 AppDataProvider:

  1. 启动时 command 读取一次;
  2. 注册 event listener;
  3. listener 完成后再读一次关闭竞态;
  4. 保存完整 RunState;
  5. ServiceMigrationDialog、TUN 开关和首页状态共同消费。

因此不同 UI 表面不会各自轮询和解释状态。

十二、TUN reconciliation 的谨慎条件

tun_should_be_disabled(tun_enabled) 只在以下条件成立时返回 true:

  • 用户确实打开 TUN;
  • 核心正在运行;
  • 服务健康已知,不是 Unknown;
  • 没有 operation in flight;
  • 没有 pending 用户决策;
  • 当前既不 elevated,也无 usable service;
  • 或用户明确 settle on Sidecar。

这种保守性防止安装过程中的短暂状态把用户设置永久关闭。

十三、状态机设计的三条经验

  1. 事实、请求、选择分开存,不要压成一个含糊 status。
  2. 派生能力在权威侧计算,消费者拿结论而不是重复业务规则。
  3. 暂时读不到不等于负面事实,监控系统要区分 unavailable 与 unknown。

本章源码索引

  • src-tauri/src/core/runstate/health.rs:RunState、ServiceHealth 与派生能力
  • src-tauri/src/core/runstate/mod.rs::RunStateStore:变更、generation、settled、operation
  • src-tauri/src/core/runstate/env.rs::RunStateEnv:真实环境与 FakeEnv 边界
  • src-tauri/src/core/runstate/probe.rs:版本回复与 health 分类
  • src-tauri/src/core/runstate/owner.rs:服务 owner 监控状态
  • src-tauri/src/core/service.rs::ServiceStatus:legacy flatten view
  • src-tauri/src/core/handle.rs::notify_run_state:完整快照推送
  • src/providers/app-data-provider.tsx:前端 RunState 消费

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