返回博客

tauri-plugin-single-instance 在 dev 模式下把自己踢出局

给 Tauri 2 桌面 App 接入 tauri-plugin-single-instance 之后,开发体验变得玄学:改一行 Rust 代码,窗口消失,终端输出一段错误然后退出;有时重新 pnpm tauri dev 刚起来又立刻死掉,像被人踢了一脚。症状反复,但日志里没有任何 Rust panic——这让排查方向一开始就走偏了。

现象

终端输出如下,随即 dev 进程整体退出:

ELIFECYCLE  Command failed with exit code 143.
      Error The "beforeDevCommand" terminated with a non-zero status code.
ELIFECYCLE  Command failed with exit code 1.

有时是修改 src-tauri/ 下任意文件(.rstauri.conf.json)触发;有时是上一次 dev session 崩溃留下僵尸进程,导致每一次新启动都即死。

根因

pnpm tauri dev 是一个进程监管器:它同时运行 vite(beforeDevCommand)和 Rust App,并把两者的生命周期绑在一起。只要 App 进程退出,tauri dev 就会 SIGTERM vite(exit 143),整个 dev session 随之结束。

当你编辑 src-tauri/ 下的文件时,tauri dev 会执行 kill 旧二进制 → 重建 → 启动新二进制 的流程。此时 tauri-plugin-single-instance 在新进程启动时检查锁(socket 或 lockfile,key 为 bundle identifier)。若旧进程还没完全释放锁——这是一个时序竞争,快速重建时尤为明显——新进程触发 single-instance 回调后立即自杀,tauri dev 随即认定 App 已退出,级联 SIGTERM vite,整个 dev session 崩掉。

两种失败形态,同一个根因:

  1. 重建竞争:改 Rust 代码 → tauri dev 重建 → 新实例锁竞争失败 → 自杀 → dev session 退出。时好时坏,因为是竞态。
  2. 僵尸实例:上次 dev 崩溃留下一个存活进程 → 新启动的实例一上来就触发 single-instance 自杀 → 永远启动不了。

single-instance 插件在 dev 阶段本就没有任何意义——dev 本来就只有一个窗口。

坑:single-instance 插件无条件注册,与 tauri dev 的重建机制互相撕杀

tauri-plugin-single-instance 的设计假设是:同一台机器上只允许一个实例运行,多出来的那个自杀。但 tauri dev 的热重建恰好制造了「新旧实例短暂共存」的窗口期,导致新实例把自己当成「多余的那个」而自杀。exit code 143 是 SIGTERM(128+15),不是 Rust panic,所以日志里没有 backtrace,极具迷惑性。

正解

#[cfg(not(debug_assertions))] 把插件注册限定在 release 构建,debug 构建跳过:

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    #[allow(unused_mut)]
    let mut builder = tauri::Builder::default();
 
    // single-instance 只在 release 注册。
    // debug 构建中,tauri dev 的 kill-old→start-new 重建会与锁释放竞争,
    // 导致新实例自杀 → tauri dev 认为 App 退出 → SIGTERM vite (exit 143) → dev session 崩溃。
    #[cfg(not(debug_assertions))]
    {
        builder = builder
            .plugin(tauri_plugin_single_instance::init(|_app, _argv, _cwd| {}));
    }
 
    builder
        .plugin(/* 其他插件 */)
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

注意:官方要求 single-instance 必须是第一个注册的插件,上面的写法保留了这个顺序。

如果当前已有僵尸进程,先清理再重启:

pkill -f target/debug/<你的-app-name>
pnpm tauri dev
正解:#[cfg(not(debug_assertions))] 把插件限定在 release

一行条件编译,debug 构建完全跳过 single-instance 检查,tauri dev 重建不再竞争锁,热重启恢复正常。release 构建行为不变,单实例保护依然生效。

附:tauri dev 的热重载边界

顺带澄清一个常见误区:

  • 修改 src/**(React / Vue / CSS / HTML)→ vite HMR,窗口不闪,只刷新局部。
  • 修改 src-tauri/**.rstauri.conf.jsonCargo.toml)→ 完整 cargo 重建 + 窗口销毁重建。这是设计行为,不是 bug,Rust 没有热重载。

区分正常重建与单实例崩溃的方法:正常重建日志会打印 Info File src-tauri/... changed. Rebuilding application... 然后新窗口弹出;单实例崩溃则直接输出 exit 143 / exit 1 后不再恢复。

一句话外卖

只在 release 构建注册 tauri-plugin-single-instance——dev 模式下它会与 tauri dev 自己的重建机制竞争锁,把整个 dev session 拖下水。