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.jsoncurl 200 OK,签名文件齐全 tauri.conf.json里plugins.updater.endpoints+pubkey都填了lib.rs里有.plugin(tauri_plugin_updater::Builder::new().build())- capability 文件有
updater:default和process:allow-restart - bump version 重 build 再发,旧版客户端毫无反应
- nginx access log 里只有开发者自己 curl 的记录,零客户端访问
根因
Tauri 1 的 updater 是"注册即生效"——只要 tauri.conf.json 里 updater.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 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-processimport { 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()
}tauri::async_runtime::spawn 已经是非阻塞的,无需自己加 sleep。check 失败(网络断 / 签名错)只影响后台 task,不会 panic 或挡住 App 启动。download_and_install 之后必须显式调 app.restart(),老进程不会自动替换为新二进制。
Bootstrap 困境
加好 check 代码后还有一个无法绕过的陷阱:旧版本里没有 check 代码,它永远不会自动 OTA。服务端再怎么折腾都没用——客户端不去拉,什么都白搭。
实操路径:
- 把 check 代码加进去(按上面 Rust 范式)
- bump 版本号,重新 build 所有目标(macOS / Windows / Linux),推到服务端
- 让用户手动装一次新版本(这是无法绕过的 bootstrap——把"会 check 的代码"植入用户机器)
- 从下一次发版起,才会真正自动 OTA
把 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 永远是零。