Skip to content

Better Panic Hooks

アプリケーションは、さまざまな理由でパニックになることがあります (たとえば、None.unwrap() を呼び出す場合)。そして、このような状況が発生した場合、良き市民として次のことを行う必要があります。

  1. ユーザーがエラーを報告できるように、役立つスタック トレースを提供する。
  2. ユーザーのターミナル状態を不完全な状態のままにせず、元の状態に戻す。

better-panic

main は、パニックのためのかなりのバックトレースを提供します。

Terminal window
cargo add better-panic

以下は、better-panic を使用してデフォルトでよりきれいなバックトレースを提供する initialize_panic_handler() の例です。

use better_panic::Settings;
pub fn initialize_panic_handler() {
std::panic::set_hook(Box::new(|panic_info| {
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
crossterm::terminal::disable_raw_mode().unwrap();
Settings::auto().most_recent_first(false).lineno_suffix(true).create_panic_handler()(panic_info);
}));
}

私は個人的に、パニック ハンドラで Tui 構造体を再利用することを好みます。 こうすることで、将来 crossterm から termion に移行することに決めた場合、プロジェクト内でリファクタリングについて心配しなければならない場所が 1 つ減ります。

以下は、デフォルトでよりきれいなバックトレースを提供するために better_paniclibc を使用する initialize_panic_handler() の例です。

use better_panic::Settings;
pub fn initialize_panic_handler() {
std::panic::set_hook(Box::new(|panic_info| {
match crate::tui::Tui::new() {
Ok(t) => {
if let Err(r) = t.exit() {
error!("Unable to exit Terminal: {r:?}");
}
},
Err(r) => error!("Unable to exit Terminal: {r:?}"),
}
better_panic::Settings::auto()
.most_recent_first(false)
.lineno_suffix(true)
.verbosity(better_panic::Verbosity::Full)
.create_panic_handler()(panic_info);
std::process::exit(libc::EXIT_FAILURE);
}));
}

さて、例として panic! をアプリケーションに追加したとしましょう。

diff --git a/src/components/app.rs b/src/components/app.rs
index 289e40b..de48392 100644
--- a/src/components/app.rs
+++ b/src/components/app.rs
@@ -77,6 +77,7 @@ impl App {
}
pub fn increment(&mut self, i: usize) {
+ panic!("At the disco");
self.counter = self.counter.saturating_add(i);
}

これは、 better-panic で、きれいなStacktraceがどのように見えるかです。

Backtrace (most recent call last):
File "/Users/kd/gitrepos/myapp/src/main.rs:46", in ratatui_async_template::main
Ok(())
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/runtime.rs:304", in tokio::runtime::runtime::Runtime::block_on
Scheduler::MultiThread(exec) => exec.block_on(&self.handle.inner, future),
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/scheduler/multi_thread/mod.rs:66", in tokio::runtime::scheduler::multi_thread::MultiThread::block_on
enter
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/context.rs:315", in tokio::runtime::context::BlockingRegionGuard::block_on
park.block_on(f)
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/park.rs:283", in tokio::runtime::park::CachedParkThread::block_on
if let Ready(v) = crate::runtime::coop::budget(|| f.as_mut().poll(&mut cx)) {
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/coop.rs:73", in tokio::runtime::coop::budget
with_budget(Budget::initial(), f)
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/coop.rs:107", in tokio::runtime::coop::with_budget
f()
File "/Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/park.rs:283", in tokio::runtime::park::CachedParkThread::block_on::{{closure}}
if let Ready(v) = crate::runtime::coop::budget(|| f.as_mut().poll(&mut cx)) {
File "/Users/kd/gitrepos/myapp/src/main.rs:44", in ratatui_async_template::main::{{closure}}
runner.run().await?;
File "/Users/kd/gitrepos/myapp/src/runner.rs:80", in ratatui_async_template::runner::Runner::run::{{closure}}
if let Some(action) = component.update(action.clone())? {
File "/Users/kd/gitrepos/myapp/src/components/app.rs:132", in <ratatui_async_template::components::app::App as ratatui_async_template::components::Component>::update
Action::Increment(i) => self.increment(i),
File "/Users/kd/gitrepos/myapp/src/components/app.rs:80", in ratatui_async_template::components::app::App::increment
panic!("At the disco");
The application panicked (crashed).
At the disco
in src/components/app.rs:80
thread: main

.most_recent_first(false) を使用すると、スタックトレースの最後の行は通常、エラーが発生した場所です。これにより、ターミナル履歴を上にスクロールしなくてもエラーをすばやく簡単に見つけることができ、開発中にアプリケーションを迅速に反復処理できます。

このような詳細なスタックトレースは、デバッグビルドでのみ使用できます。リリースビルドでは、スタックトレースがインライン化または切り捨てられる場合があります。

たとえば、すべての最適化をオンにしてコンパイルすると、次のようになります。

Backtrace (most recent call last):
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
File "<unknown>:0", in __mh_execute_header
The application panicked (crashed).
At the disco
in src/components/app.rs:80
thread: main

これを一般ユーザーに表示しても特に役に立ちません。次のサブセクションでは、アプリケーションのユーザーに何を表示するかというより良い解決策について説明します。

人間 - パニック

human-panic を使用するには、依存関係としてインストールする必要があります。

Terminal window
cargo add human-panic

個人的には、human-panic は、ユーザーが予期せぬパニックに陥ったときに、すぐに使える最もユーザーフレンドリーなパニック処理機能を提供すると思います。

Well, this is embarrassing.
myapp had a problem and crashed. To help us diagnose the problem you can send us a crash report.
We have generated a report file at "/var/folders/l4/bnjjc6p15zd3jnty8c_qkrtr0000gn/T/report-ce1e29cb-c17c-4684-b9d4-92d9678242b7.toml". Submit an issue or email with the subject of "myapp Crash Report" and include the report as an attachment.
- Authors: Dheepak Krishnamurthy
We take privacy seriously, and do not perform any automated error collection. In order to improve the software, we rely on people to submit reports.
Thank you kindly!

クラッシュに関連する情報が記録されるレポートを生成します。 human-panic が作成する一時的なレポートファイルのコンテンツは次のとおりです (最適化をオンにします) :

name = "myapp"
operating_system = "Mac OS 13.5.2 [64-bit]"
crate_version = "0.1.0"
explanation = """
Panic occurred in file 'src/components/app.rs' at line 80
"""
cause = "At the disco"
method = "Panic"
backtrace = """
0: 0x10448f5f8 - __mh_execute_header
1: 0x1044a43c8 - __mh_execute_header
2: 0x1044a01ac - __mh_execute_header
3: 0x10446f8c0 - __mh_execute_header
4: 0x1044ac850 - __mh_execute_header"""

デバッグモードでは、Stacktraceは以前と同じように説明的です。

構成

デバッグ ビルドには better-panic、リリース ビルドには color-eyrehuman-panic を使用して、これらの異なるパニック ハンドラーを組み合わせて使用​​できます。以下のコードでは、念のため color-eyre スタック トレースを log::error! に出力します (ANSI エスケープ シーケンスを削除した後)。

Terminal window
cargo add color-eyre human-panic libc better-panic strip-ansi-escapes

プロジェクトにコピーして貼り付けることができるコードは次のとおりです (ターミナル終了を処理するために Tui 構造体を使用する場合)。

pub fn initialize_panic_handler() -> Result<()> {
let (panic_hook, eyre_hook) = color_eyre::config::HookBuilder::default()
.panic_section(format!("This is a bug. Consider reporting it at {}", env!("CARGO_PKG_REPOSITORY")))
.display_location_section(true)
.display_env_section(true)
.into_hooks();
eyre_hook.install()?;
std::panic::set_hook(Box::new(move |panic_info| {
if let Ok(t) = crate::tui::Tui::new() {
if let Err(r) = t.exit() {
error!("Unable to exit Terminal: {:?}", r);
}
}
let msg = format!("{}", panic_hook.panic_report(panic_info));
#[cfg(not(debug_assertions))]
{
eprintln!("{msg}");
use human_panic::{handle_dump, print_msg, Metadata};
let author = format!("authored by {}", env!("CARGO_PKG_AUTHORS"));
let support = format!(
"You can open a support request at {}",
env!("CARGO_PKG_REPOSITORY")
);
let meta = Metadata::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"))
.authors(author)
.support(support);
let file_path = handle_dump(&meta, panic_info);
print_msg(file_path, &meta).expect("human-panic: printing error message to console failed");
}
log::error!("Error: {}", strip_ansi_escapes::strip_str(msg));
#[cfg(debug_assertions)]
{
// Better Panic stacktrace that is only enabled when debugging.
better_panic::Settings::auto()
.most_recent_first(false)
.lineno_suffix(true)
.verbosity(better_panic::Verbosity::Full)
.create_panic_handler()(panic_info);
}
std::process::exit(libc::EXIT_FAILURE);
}));
Ok(())
}