返回博客

Tauri 2 OTA 完全沉默——updater plugin 注册了≠会自动 check

某桌面 App 从 Tauri 1 迁到 Tauri 2 之后,OTA 升级彻底哑了。update.json 放好了,.sig 签名文件也在,capability 里 updater:default 也配了,版本号也 bump 了——重新发布后在装了旧版的机器上等了半天,弹窗一个没有。更诡异的是:服务端 nginx access log 里连一条拉 update.json 的请求都找不到,客户端根本没有"尝试"过去拉。

现象

  • 服务端 update.json curl 200 OK,签名文件齐全
  • tauri.conf.jsonplugins.updater.endpoints + pubkey 都填了
  • lib.rs 里有 .plugin(tauri_plugin_updater::Builder::new().build())
  • capability 文件有 updater:defaultprocess:allow-restart
  • bump version 重 build 再发,旧版客户端毫无反应
  • nginx access log 里只有开发者自己 curl 的记录,零客户端访问

根因

Tauri 1 的 updater 是"注册即生效"——只要 tauri.conf.jsonupdater.active: true,框架启动时就会自动拉 update.json + 弹窗。

Tauri 2 完全换了设计。updater plugin 在 Tauri 2 里是一个纯库(library-only).plugin(tauri_plugin_updater::Builder::new().build()) 这行代码只是把 app.updater() API 注册进去,让后续代码可以调用它——框架本身永远不会自己触发 check

update.json 没有被拉到,不是网络问题、不是签名问题、不是权限问题,而是从来就没有人去拉

坑:Tauri 2 没有「自动 check」开关

Tauri 1 的 updater.active: true 在 Tauri 2 里已废弃且无效tauri.conf.json 里的 endpoints + pubkey 只是配置,不是触发器。capability 里的 updater:default 只是给 check() API 鉴权,不会自动调它。

唯一让 OTA 真正工作的办法:在 Rust setup hook 或 JS 启动代码里主动调 check().await

正解

Rust setup hook(推荐)

适合 webview 加载远程 URL 的部署场景,前端无需依赖 Tauri JS plugin:

use tauri_plugin_updater::UpdaterExt;
 
pub fn run() {
  tauri::Builder::default()
    .plugin(tauri_plugin_updater::Builder::new().build())
    .setup(|app| {
      let handle = app.handle().clone();
      // spawn 异步任务:不阻塞启动,check 失败也不挡 app 跑
      tauri::async_runtime::spawn(async move {
        if let Ok(updater) = handle.updater() {
          if let Ok(Some(update)) = updater.check().await {
            if update.download_and_install(|_, _| {}, || {}).await.is_ok() {
              handle.restart();
            }
          }
        }
      });
      Ok(())
    })
    .run(tauri::generate_context!())
    .expect("error while running tauri application");
}

JS 端调用(前端是本地 bundle 时)

npm install @tauri-apps/plugin-updater @tauri-apps/plugin-process
import { check } from '@tauri-apps/plugin-updater'
import { relaunch } from '@tauri-apps/plugin-process'
 
const update = await check()
if (update?.available) {
  await update.downloadAndInstall()
  await relaunch()
}
正解:在 setup hook 里 spawn 一个异步 check

tauri::async_runtime::spawn 已经是非阻塞的,无需自己加 sleep。check 失败(网络断 / 签名错)只影响后台 task,不会 panic 或挡住 App 启动。download_and_install 之后必须显式调 app.restart(),老进程不会自动替换为新二进制。

Bootstrap 困境

加好 check 代码后还有一个无法绕过的陷阱:旧版本里没有 check 代码,它永远不会自动 OTA。服务端再怎么折腾都没用——客户端不去拉,什么都白搭。

实操路径:

  1. 把 check 代码加进去(按上面 Rust 范式)
  2. bump 版本号,重新 build 所有目标(macOS / Windows / Linux),推到服务端
  3. 让用户手动装一次新版本(这是无法绕过的 bootstrap——把"会 check 的代码"植入用户机器)
  4. 从下一次发版起,才会真正自动 OTA
可观测性:0 事件 = 0 check

updater.check / updater.found / updater.installed / updater.check_failed 写到中心化日志服务。下次再遇到"OTA 没动静",curl 一下日志 sink:0 事件 = 压根没调 check,这个坑;有事件 = 签名 / 网络问题,另一个坑。

排查三板斧

# 1. 服务端 update.json 是否 200
curl -sI http://your-host/update.json
# HTTP/1.1 200 OK ← 不是服务端问题
 
# 2. nginx access log 有无客户端拉过
tail -n 200 /var/log/nginx/access.log | grep update.json
# 如果只有你自己测的 ← 客户端根本没请求
 
# 3. 代码里有没有 check() 调用
grep -rn "updater().*check\|@tauri-apps/plugin-updater" src-tauri/src/
# 0 命中 ← 就是这个坑

一句话外卖

Tauri 2 updater plugin 是工具箱,不是定时器——箱子放在那里不会自己跑,你不伸手调 check(),OTA 永远是零。