故障档案

「升级成功」,但它觉得自己没升级

portable 客户升级后版本判定用错了数据源,launcher 不断读旧版本号导致无限循环更新。

根因:读写位置错配

launcher 的 read_app_version 函数采用了三级 fallback:

  1. 优先读 portable_root/version.json(USB 上的文件)
  2. 其次读 WAKOU_LOCAL_CACHE_ROOT/version.json(缓存中的文件)
  3. 最后回落到 CARGO_PKG_VERSION

但 OTA 的 Phase 2 只更新 cache 里的 version.json,不更新 USB。这个顺序反了。

USB 上的版本号代表"上次制作镜像时是什么版本"。Cache 代表"当前系统已安装什么版本"。读取时应该读当前状态,而非历史记录。关键原则:读优先级必须与写位置一致

怎么形成的循环

0.14.3 用户接到 0.14.4 强制升级。OTA 链路完整跑完,但新版 launcher 启动后仍然弹升级弹窗。用户点更新,下载安装,重启。然后又弹窗。无限循环,直到 0.14.4 被撤回。

Windows 便携版 0.14.3,某个 F 盘 USB 上的流程是:

  1. 用户双击 Start-Wakou.cmd,0.14.3 launcher 正常启动
  2. Stage 5 完成 cache 准备(mirror 文件来自 USB,所有文件都是 0.14.3 era)
  3. Stage 6 OTA 检查调用远程 wrapper,返回 0.14.4 强制更新指令
  4. 写入 force-update-pending.json marker,Stage 7 启动 panel
  5. Panel 读取 marker,弹出「升级到 0.14.4」对话框
  6. 用户点击「立即更新」,OTA worker 开始下载、验证、准备安装
  7. Phase 2 trash_then_install 完成,杀掉 panel,启动新版 launcher
  8. 新 launcher 再次执行 OTA 检查,wrapper 仍然返回 0.14.4 强制更新
  9. 重新写入 marker,Panel 重启后再次弹窗
  10. 循环往复

用户无法正常进入新版本的面板。OTA 事件日志显示 "OTA applied to 0.14.4",但 USB 中的 version.json 仍然记录 0.14.3

观察点是这个矛盾:OTA 事件日志显示已成功安装到 0.14.4,但重启后又触发同样的升级流程。这意味着版本号的读取出了问题。

新版 launcher 启动后,调用 read_app_version。由于优先级错配,它读到的是 USB 上的旧版本号——0.14.3。向 wrapper 传 currentVersion=0.14.3,wrapper 返回 0.14.4 强制更新。marker 重新写入,panel 重启后再次弹窗。

用户点更新,OTA Phase 2 完成,cache 中的 version.json 写入 0.14.4。但 USB 同步有 120 秒延迟,甚至更久(guardian 的定时间隔)。下一次 launcher 启动,又读到 USB 的 0.14.3。

循环永不停止——除非 guardian 的同步周期刚好赶上 launcher 的下一次启动。时间窗口里,任何人为原因的重启都能触发循环。

关键是 OTA 的写入和读取位置不一致。OTA 的 Phase 2(trash_then_install)更新的是 WAKOU_LOCAL_CACHE_ROOT(本地缓存目录)下的文件。Cache 是操作系统启动后立刻可用的工作目录,USB 上的文件是长期存储。

但新 launcher 启动后,由于优先级设置,仍然首先读取 USB 上那份——而 USB 中的版本号还没有被同步到 0.14.4,因为 guardian 进程定时同步 USB 的间隔是 120 秒。

时间线还原:

  • T0:用户点"立即更新"
  • T0+20s:OTA Phase 2 完成,cache 中 version.json 已写入 0.14.4
  • T0+21s:新 launcher 启动
  • T0+22s:read_app_version 读 USB,得 0.14.3
  • T0+23s:launcher 向 wrapper 传 currentVersion=0.14.3
  • T0+24s:wrapper 返回 0.14.4 强制更新指令,marker 再次写入
  • T0+120s:guardian 才同步 USB 到 0.14.4

这 120 秒窗口内任何一次 launcher 启动都会重复。系统重启后 guardian 周期重新开始计时,问题仍然会重现。

launcher/src/main.rsread_app_version 的实现问题:

fn read_app_version(portable_root: &Path) -> String {
    // 1. portable_root/version.json (USB)   ← 错误优先级
    // 2. WAKOU_LOCAL_CACHE_ROOT/version.json (cache)
    // 3. CARGO_PKG_VERSION (last resort)
}

这个优先级顺序违反了核心原则:读优先级必须与写位置一致。USB 版本号代表"上次制作镜像时是什么版本"。Cache 代表"当前系统已安装什么版本"。读取时应该读当前状态,而非历史记录。

新 launcher 需要知道"我现在是什么版本",这个"我"指的是内存中正在运行的程序,其代码来自 cache(如果有最新版本),而不是来自 USB。读 USB 版本号等于问"USB 镜像上次更新是几天前",这不是正确的问题。

修复策略

反转 read_app_version 的优先级:

fn read_app_version(portable_root: &Path) -> String {
    // 1. WAKOU_LOCAL_CACHE_ROOT/version.json (cache — 当前已安装)
    // 2. portable_root/version.json (USB — 兜底)
    // 3. CARGO_PKG_VERSION (last resort)
}

修复后,cache 中的 version.json 一旦更新为 0.14.4,新 launcher 立刻读到。wrapper 返回 NoUpdate,循环断掉。USB 同步滞后已不关键——cache 就是当前事实。

读取的是"此时此刻运行的是什么版本",而不是"最后一次有人更新 USB 镜像是什么时候"。

验证边界场景:

场景旧优先级新优先级
全新 USB 首次启动(无 cache)读 USB ✓cache 不存在,fallback USB ✓
OTA 完成立刻重启USB 旧版 → 循环 ❌cache 已更新 ✓
dev 环境(无文件)fallback 常数 ✓同上 ✓

防回归

加入集成测试,验证当 cache 和 USB 版本号不一致时的行为:mock cache 为 0.14.5,USB 为 0.14.4,调用 read_app_version 必须返回 0.14.5(而非 0.14.4)。

read_app_version 是 launcher boot Stage 6 的唯一版本来源。任何后续改动涉及 portable_rootversion.json 的代码都应该 grep 一次这个函数,确保没有再把优先级搞反。

附注:同时期还有两个相关问题。一是 version.json 的 schema 分裂(schema-1 和 schema-2 并存),可能导致版本号回落到默认值 0.0.1,作为 OTA 循环的隐藏放大器——但这是独立的 schema 编码问题,不是优先级问题。二是 launcher 的两个不同代码路径(Stage 6 boot 的 read_app_version 与 OTA check 的 current_version_from_local_cache)的 fallback 链路设计不一致,这也是独立根因,表现为版本号又退化到 0.0.0,同样触发 OTA 循环。当前这篇解决的只是第一个(优先级错配),其他两个有各自的修复方案。

更宽的教训

分布式系统里同一份数据有多个副本时,读取顺序必须遵循写入顺序。OTA 系统中,cache 是主,USB 是从;应该读主再读从。任何"双位置存储"都有这个风险——没有绝对的"真理源",只有"最近写入的位置"。

同时期还有两个相关但独立的 bug。一是 version.json 的 schema 分裂(schema-1 和 schema-2 并存),可能导致版本号回落到 0.0.1,作为 OTA 循环的隐藏放大器。这是 schema 编码问题,不是优先级。二是 launcher 的两个代码路径(Stage 6 boot 的 read_app_version 与 OTA check 的 current_version_from_local_cache)的 fallback 链路不一致,版本号退化到 0.0.0,同样触发循环。这篇只解决了优先级错配,其他两个各有各的修复。

星野的头像

星野 XINGYE

一个人维护 AI 平台的工程师。这里记录 63 篇复盘:18 份故障档案、OTA、架构演进与工作流。