Rust in Everything
基于 tauri 2.12.5

failed to initiate panic, error 5, aborting

主线位置:调用主线的「同步命令就地执行」一步。同步命令是 wry 的 start_task(WebKit 回调 WKURLSchemeHandler 的入口)一路同步调下来的,它的 panic 要往上穿过这些栈帧。

flowchart TB
  wk["WebKit 栈帧"] --> st["wry start_task"]
  st --> proto["ipc 协议处理"]
  proto --> onmsg["on_message"]
  onmsg --> cmd["同步命令 panic"]
  cmd -->|"向上找 catch"| fail["error 5 找不到"]
  fail --> abort["abort 退出码 134"]

本页的崩溃输出都是之前几页的实验留下的(app-life,tauri 2.12.5,debug 构建),没有为本页重新跑会崩溃的程序:在 macOS 上,进程 abort 会弹出系统的崩溃报告对话框。修法部分是新跑的,用的是实验 crate pit2-app(MODE=panic,debug 构建),不会崩溃。平台 M2 Max、macOS 27、rustc 1.99.0。

现象

app-life 的 report 命令是同步命令,设 RT=late 时在里面调 async_runtime::set。这时运行时早已初始化,set panic(这件事本身见异步运行时):

#[tauri::command]
fn report(app: tauri::AppHandle, msg: String) {
  println!("REPORT\n{msg}");
  if std::env::var("RT").as_deref() == Ok("late") {
    log("calling async_runtime::set after the page loaded");
    let rt = tokio::runtime::Runtime::new().unwrap();
    tauri::async_runtime::set(rt.handle().clone());
  }
  // ...
}
  853ms [main] calling async_runtime::set after the page loaded

thread 'main' (2368571) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/async_runtime.rs:282:25:
runtime already initialized
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5, aborting
exit=134

panic 消息照常打印,接着是一行 fatal runtime error,进程退出码 134(128 + 6,SIGABRT),不是普通 panic 的 101。report 里后面的 app.exit(0) 没执行,RunEvent::ExitRequested / Exit 都没有发出,所有 Drop 都没机会跑:缓冲区里没刷到磁盘的数据丢了,临时文件、锁文件原样留下。

同一份程序换一种 panic 也一样。RT=tokiomain 模拟 #[tokio::main]:主线程在 Runtime::block_on 里,同步命令 block_sync 再 block_on,tokio panic「Cannot start a runtime from within a runtime」:

  564ms [main] block_sync: async_runtime::block_on on this thread

thread 'main' (2470140) panicked at src/main.rs:87:3:
Cannot start a runtime from within a runtime. This happens because a function (like `block_on`) attempted to block the current thread while the thread is being used to drive asynchronous tasks. If you are in an async function, use `.await` on the future instead of blocking on it.
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
fatal runtime error: failed to initiate panic, error 5, aborting
exit=134

两次 panic 的来源完全不同,结局一样。下一节会看到,决定结局的是 panic 发生的位置,不是它的内容;unwrap() 一个 None、数组越界在同一个位置上也是这样(这两种没有为本页单独跑)。

出处

加 RUST_BACKTRACE=1 跑第一个例子,回溯是这样的(app-life/bt-late.txt;省略的第 8、12、14 帧是很长的 Box<dyn Fn ...> 调用):

thread 'main' (2471366) panicked at ~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/src/async_runtime.rs:282:25:
runtime already initialized
stack backtrace:
   0: __rustc::rust_begin_unwind
   1: core::panicking::panic_fmt
   2: tauri::async_runtime::set::{closure#0}
   3: <core::result::Result<(), tauri::async_runtime::GlobalRuntime>>::unwrap_or_else::<tauri::async_runtime::set::{closure#0}>
   4: tauri::async_runtime::set
   5: app_life::report
   6: app_life::start::{closure#3}::{closure#0}
   7: app_life::start::{closure#3}
...
   9: <tauri::manager::AppManager>::run_invoke_handler
  10: <tauri::webview::Webview>::on_message
  11: tauri::ipc::protocol::get::<tauri_runtime_wry::Wry<tauri::EventLoopMessage>>::{closure#0}
...
  13: <tauri::manager::webview::WebviewManager<tauri_runtime_wry::Wry<tauri::EventLoopMessage>>>::prepare_pending_webview::<tauri::app::AppHandle>::{closure#2}
...
  15: tauri_runtime_wry::create_webview::<tauri::EventLoopMessage>::{closure#8}
  16: wry::wkwebview::class::url_scheme_handler::start_task
  17: <unknown>
  18: <unknown>
  19: <unknown>
  20: <unknown>
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
fatal runtime error: failed to initiate panic, error 5, aborting

从命令往上数:run_invoke_handler、on_message、ipc 协议的闭包、prepare_pending_webview 包的一层、tauri-runtime-wry 包的一层,然后是 wry 的 start_task(wry 0.57.1),再往上全是 <unknown>,即 WebKit 和系统框架的 Objective-C 栈帧。start_task 是注册给 Objective-C 运行时的方法实现,声明为 extern "C" fn:

// Task handler for custom protocol
extern "C" fn start_task(
  this: &AnyObject,
  _sel: objc2::runtime::Sel,
  webview: &WryWebView,
  task: &ProtocolObject<dyn WKURLSchemeTask>,
) {

Rust 的 panic 分两步:先打印消息(panic hook),再调平台的 _Unwind_RaiseException 开始展开。展开的第一阶段只「找」:从 panic 处往上逐帧看谁能接住。整条链上,从命令到 start_task 没有一处 catch_unwind(tauri 2.12.5 整个仓库只有 tauri-macros 的移动端入口宏用到它,wry 0.57.1 里没有),再往上是 WebKit 的帧。第一阶段没有找到接住它的帧,_Unwind_RaiseException 返回错误码 5,标准库的 unwind 里这个值叫 _URC_END_OF_STACK(「到栈底了」);std 的 rust_panic 拿到返回值后打印 failed to initiate panic, error {code} 并 abort。这就是为什么一个 Drop 也没执行:第二阶段(真正的展开)根本没开始。

对照两种不会 abort 的 panic:

  • 在 async 命令里:命令体是 respond_async_serialized 交给 async_runtime 的任务,tokio 在 poll 任务时自己包了 catch_unwind,panic 只结束这个任务。下文的修法里有实测。
  • 在 tao 事件循环的回调里:tao 0.37.2 在 macOS 上把 run loop observer 驱动的回调包在 stop_app_on_panic 里,接住 panic、停掉 NSApplication,等 app.run() 返回后再 resume_unwind。这条只读了源码,本卷没有为它跑会 panic 的程序。

ipc:// 请求从 WebKit 直接进 start_task,不经过 tao 的这一层。同样的结局也落在 tauri 自己的代码上:2.12.4 之前,ipc/protocol.rs 取命令名时对空路径做切片会越界,一个 ipc:foo 这样的请求就能让进程 abort(#16197),见 ipc 协议。

Windows 和 Linux 上协议回调的调用栈不同,结果是否一样本卷没有实测。

修法

pit2-app 里三个版本的「取第 i 个元素」,都用越界的下标调用:

#[tauri::command]
async fn nth_async(items: Vec<u32>, i: usize) -> u32 {
  items[i]
}

#[tauri::command]
fn nth_checked(items: Vec<u32>, i: usize) -> Result<u32, String> {
  items
    .get(i)
    .copied()
    .ok_or_else(|| format!("index {i} out of range for {} items", items.len()))
}

#[tauri::command]
fn nth_caught(items: Vec<u32>, i: usize) -> Result<u32, String> {
  std::panic::catch_unwind(|| items[i]).map_err(|e| {
    let msg = e
      .downcast_ref::<String>()
      .cloned()
      .or_else(|| e.downcast_ref::<&str>().map(|s| s.to_string()))
      .unwrap_or_default();
    format!("command panicked: {msg}")
  })
}

页面依次调用,nth_async 套一个 1 秒的超时,最后再调一次 greet 看应用还在不在:

      out.nthChecked = await attempt(() => inv('nth_checked', { items: [1, 2, 3], i: 7 }));
      out.nthCaught = await attempt(() => inv('nth_caught', { items: [1, 2, 3], i: 7 }));
      out.nthAsync = await attempt(() => within(inv('nth_async', { items: [1, 2, 3], i: 7 }), 1000));
      out.greetAfter = await attempt(() => inv('greet', { name: 'still alive' }));
    0ms [main] main
   83ms [main] probe plugin setup

thread 'main' (2735288) panicked at src/main.rs:105:36:
index out of bounds: the len is 3 but the index is 7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'tokio-rt-worker' (2735425) panicked at src/main.rs:92:8:
index out of bounds: the len is 3 but the index is 7
REPORT {
 "mode": "panic",
 "nthChecked": {
  "err": "index 7 out of range for 3 items"
 },
 "nthCaught": {
  "err": "command panicked: index out of bounds: the len is 3 but the index is 7"
 },
 "nthAsync": {
  "err": "no answer after 1000 ms"
 },
 "greetAfter": {
  "ok": "hello, still alive"
 }
}
 1537ms [main] ExitRequested { code: Some(0) }
 1537ms [main] Exit
exit=0
  • nth_checked:不 panic,把错误当返回值。JS 的 Promise 以这个字符串拒绝。这是首选,错误类型怎么序列化见参数、返回值与 InvokeResolver。
  • nth_caught:panic 发生在主线程上(thread 'main' ... src/main.rs:105,消息照样打印),但这次第一阶段在命令自己的 catch_unwind 里就找到了接住它的地方,展开正常进行,命令返回 Err,进程继续。适合包住你控制不了的代码(第三方库、复杂的解析)。
  • nth_async:panic 发生在 tokio-rt-worker 上,进程没死,但这次调用永远没有应答:tokio 接住 panic 后结束了任务,InvokeResolver 的应答闭包没被调用,JS 只能靠自己的超时。所以「改成 async」只是保住了进程,async 命令里同样该返回 Result。
  • 最后的 greet 正常返回,退出码 0。

Cargo.toml 里设 panic = "abort" 帮不上忙:panic 直接 abort,少的只是「找不到」这一步,而且上面的 catch_unwind 也随之失效。想让任何 panic 都不至于静默失败,可以用 std::panic::set_hook 在 abort 之前把消息写进日志,hook 在 abort 之前执行(上面的输出里 panic 消息都打印出来了)。

怎么确认没中招

  • 退出码 134、终端最后一行是 fatal runtime error: failed to initiate panic, error 5, aborting、macOS 弹出崩溃报告:基本就是主线程上的同步代码 panic 了。往上找 thread 'main' ... panicked at 那一行看位置。
  • 搜同步命令(没有 async 的 #[tauri::command])里的 unwrap()、expect(、[i] 下标、panic!、unreachable!,以及会 panic 的库调用(async_runtime::set、block_on)。
  • async 命令里的 panic 不会 abort,但会让 Promise 永远挂着;在前端给关键调用加超时,在 Rust 侧用 set_hook 记日志,才能发现它。

小结

  • macOS 上同步命令在 wry 的 start_task 之下执行,往上是 WebKit 的栈帧;panic 找不到接住它的地方,_Unwind_RaiseException 返回 5,进程 abort,退出码 134,不执行任何 Drop。
  • async 命令里的 panic 被 tokio 接住,进程活着,但这次调用永远不应答。
  • 修法:命令返回 Result,不用 panic 表达错误;包不住的代码用 catch_unwind 在命令内部接住;async 命令同样返回 Result。

💬 关联讨论

发起讨论
加载中...