Skip to content

Commit

Permalink
docs: Add color-eyre human-panic panic handler section 📚
Browse files Browse the repository at this point in the history
  • Loading branch information
kdheepak committed Sep 16, 2023
1 parent 8cba860 commit b2c9558
Show file tree
Hide file tree
Showing 9 changed files with 271 additions and 52 deletions.
1 change: 1 addition & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
- [Arrange your App Code](./how-to/develop-apps/arrange-your-app-code.md)
- [Setup Panic Hooks](./how-to/develop-apps/setup-panic-hooks.md)
- [Use `better-panic`](./how-to/develop-apps/better-panic.md)
- [Use color-eyre and human-panic](./how-to/develop-apps/setup-panic-hooks-color-eyre.md)
- [Migrate from tui-rs](./how-to/develop-apps/migrate-from-tui-rs.md)

- [FAQ](./faq/README.md)
Expand Down
29 changes: 0 additions & 29 deletions src/how-to/develop-apps/better-panic.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,32 +100,3 @@ In the screenshot below, I added a `None.unwrap()` into a function that is calle
that you can see what a prettier stacktrace looks like:

![](https://user-images.githubusercontent.com/1813121/252723080-18c15640-c75f-42b3-8aeb-d4e6ce323430.png)

So far we used `crossterm` for the `Tui` and panic handling. Similarly, if you are using `termion`
you can do something like the following:

```rust
use std::panic;
use std::error::Error;

let panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic| {
let panic_cleanup = || -> Result<(), Box<dyn Error>> {
let mut output = io::stderr();
write!(
output,
"{}{}{}",
termion::clear::All,
termion::screen::ToMainScreen,
termion::cursor::Show
)?;
output.into_raw_mode()?.suspend_raw_mode()?;
io::stderr().flush()?;
Ok(())
};
panic_cleanup().expect("failed to clean up for panic");
panic_hook(panic);
}));
```

This will take the original panic hook and execute it after cleaning up the terminal.
201 changes: 201 additions & 0 deletions src/how-to/develop-apps/setup-panic-hooks-color-eyre.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
# Use color-eyre and human-panic

### color-eyre panic hook

One of the things we can do is use [`color-eyre`](https://github.com/eyre-rs/color-eyre):

```console
cargo add color-eyre
```

You will also want to add a `repository` key to your `Cargo.toml` file:

```toml
repository = "https://github.com/ratatui-org/ratatui-async-template" # used by env!("CARGO_PKG_REPOSITORY")
```

Now, let's say I added a `panic!` to my application as an example:

```diff
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);
}
```

Then when this function is called, I can have the application cleanly restore the terminal and print
out a nice error message like so:

```
The application panicked (crashed).
Message: At the disco
Location: src/components/app.rs:80
This is a bug. Consider reporting it at https://github.com/ratatui-org/ratatui-async-template
Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
```

Users can opt to give you a more detailed stacktrace if they can reproduce the error with
`export RUST_BACKTRACE=1`:

```
The application panicked (crashed).
Message: At the disco
Location: src/components/app.rs:80
This is a bug. Consider reporting it at https://github.com/ratatui-org/ratatui-async-template
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⋮ 13 frames hidden ⋮
14: ratatui_async_template::components::app::App::increment::h4e8b6e0d83d3d575
at /Users/kd/gitrepos/ratatui-async-template/src/components/app.rs:80
15: <ratatui_async_template::components::app::App as ratatui_async_template::components::Component>::update::hc78145b4a91e06b6
at /Users/kd/gitrepos/ratatui-async-template/src/components/app.rs:132
16: ratatui_async_template::runner::Runner::run::{{closure}}::h802b0d3c3413762b
at /Users/kd/gitrepos/ratatui-async-template/src/runner.rs:80
17: ratatui_async_template::main::{{closure}}::hd78d335f19634c3f
at /Users/kd/gitrepos/ratatui-async-template/src/main.rs:44
18: tokio::runtime::park::CachedParkThread::block_on::{{closure}}::hd7949515524de9f8
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/park.rs:283
19: tokio::runtime::coop::with_budget::h39648e20808374d3
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/coop.rs:107
20: tokio::runtime::coop::budget::h653c1593abdd982d
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/coop.rs:73
21: tokio::runtime::park::CachedParkThread::block_on::hb0a0dd4a7c3cf33b
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/park.rs:283
22: tokio::runtime::context::BlockingRegionGuard::block_on::h4d02ab23bd93d0fd
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/context.rs:315
23: tokio::runtime::scheduler::multi_thread::MultiThread::block_on::h8aaba9030519c80d
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/scheduler/multi_thread/mod.rs:66
24: tokio::runtime::runtime::Runtime::block_on::h73a6fbfba201fac9
at /Users/kd/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tokio-1.28.2/src/runtime/runtime.rs:304
25: ratatui_async_template::main::h6da543b193746523
at /Users/kd/gitrepos/ratatui-async-template/src/main.rs:46
26: core::ops::function::FnOnce::call_once::h6cac3edc975fcef2
at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/ops/function.rs:250
⋮ 13 frames hidden ⋮
```

## human-panic

Personally, I like to use `human-panic` to print out a user friendly message like so:

```
The application panicked (crashed).
Message: At the disco
Location: src/components/app.rs:80
This is a bug. Consider reporting it at https://github.com/ratatui-org/ratatui-async-template
Backtrace omitted. Run with RUST_BACKTRACE=1 environment variable to display it.
Run with RUST_BACKTRACE=full to include source snippets.
Well, this is embarrassing.
ratatui-async-template 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 "ratatui-async-template 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!
```

Here's the content of the temporary report file that `human-panic` creates:

```
name = "ratatui-async-template"
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"""
```

You'll need [human-panic](https://github.com/rust-cli/human-panic) installed as a dependency for
this:

```console
cargo add human-panic
```

You'll want to manually configure `human-panic` to run after you've restored the terminal state.

## `initialize_panic_handler`

You can mix and match different panic handlers, for example using `better-panic` for debug builds
and `color-eyre` and `human-panic` for release builds.

```console
cargo add color-eyre human-panic libc better-panic
```

Here's code you can copy paste into your project (if you use the [`Tui`](./arrange-your-app-code.md)
struct to handle terminal exits):

```rust
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 meta = Metadata {
version: env!("CARGO_PKG_VERSION").into(),
name: env!("CARGO_PKG_NAME").into(),
authors: env!("CARGO_PKG_AUTHORS").replace(':', ", ").into(),
homepage: env!("CARGO_PKG_HOMEPAGE").into(),
};

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));

// Better Panic. Only enabled *when* debugging.
#[cfg(debug_assertions)]
{
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(())
}
```
70 changes: 54 additions & 16 deletions src/how-to/develop-apps/setup-panic-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,66 @@ library functionality and no external dependencies.

```rust
pub fn initialize_panic_handler() {
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
crossterm::terminal::disable_raw_mode().unwrap();
original_hook(panic_info);
}));
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen).unwrap();
crossterm::terminal::disable_raw_mode().unwrap();
original_hook(panic_info);
}));
}
```

With this function, all your need to do is call `initialize_panic_handler()` in `main()` before
running any terminal initialization code:

```rust
fn main() -> Result<()> {
initialize_panic_handler();
initialize_panic_handler();

// Startup
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?;
// Startup
crossterm::terminal::enable_raw_mode()?;
crossterm::execute!(std::io::stderr(), crossterm::terminal::EnterAlternateScreen)?;

let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stderr()))?;

// ...
// ...

// Shutdown
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?;
crossterm::terminal::disable_raw_mode()?;
Ok(())
// Shutdown
crossterm::execute!(std::io::stderr(), crossterm::terminal::LeaveAlternateScreen)?;
crossterm::terminal::disable_raw_mode()?;
Ok(())
}
```

We used `crossterm` for panic handling. If you are using `termion` you can do something like the
following:

```rust
use std::panic;
use std::error::Error;

pub fn initialize_panic_handler() {
let panic_hook = panic::take_hook();
panic::set_hook(Box::new(move |panic| {
let panic_cleanup = || -> Result<(), Box<dyn Error>> {
let mut output = io::stderr();
write!(
output,
"{}{}{}",
termion::clear::All,
termion::screen::ToMainScreen,
termion::cursor::Show
)?;
output.into_raw_mode()?.suspend_raw_mode()?;
io::stderr().flush()?;
Ok(())
};
panic_cleanup().expect("failed to clean up for panic");
panic_hook(panic);
}));
}
```

As a general rule, you want to take the original panic hook and execute it after cleaning up the
terminal. In the next sections we will discuss some third party packages that can help give better
stacktraces.
1 change: 0 additions & 1 deletion src/how-to/handle-panics/README.md

This file was deleted.

1 change: 0 additions & 1 deletion src/how-to/handle-panics/with-better-panic.md

This file was deleted.

1 change: 0 additions & 1 deletion src/how-to/handle-panics/with-panic-hooks.md

This file was deleted.

12 changes: 11 additions & 1 deletion src/tutorial/counter-app/multiple-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ Instead of `anyhow` you can also use [`eyre`](https://github.com/eyre-rs/eyre) o
+ use color_eyre::eyre::Result;
```
You'll need to add `color-eyre` and remove `anyhow`:
```console
cargo remove anyhow
cargo add color-eyre
```
If you are using `color_eyre`, you'll also want to add `color_eyre::install()?` to the beginning of
your `main()` function:
Expand All @@ -52,7 +59,10 @@ fn main() -> Result<()> {
}
```
`color_eyre` is an error report handler for colorful, consistent, and well formatted error reports for all kinds of errors.
`color_eyre` is an error report handler for colorful, consistent, and well formatted error
reports for all kinds of errors.
Check out the [section](../../how-to/develop-apps/setup-panic-hooks-color-eyre.md) for
setting up panic hooks with color-eyre.
````

Now we are ready to start refactoring our app.
7 changes: 4 additions & 3 deletions src/tutorial/counter-app/multiple-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ pub type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<std::io::Stderr>>;
```

````admonish tip
If you use the popular [`anyhow`](https://docs.rs/anyhow/latest/anyhow/) crate,
If you use the popular [`anyhow`](https://docs.rs/anyhow/latest/anyhow/)
then instead of these two lines:
```rust
type Err = Box<dyn std::error::Error>;
type Result<T> = std::result::Result<T, Err>;
```
you can simply import `anyhow::Result`:
you can simply import `Result` from `anyhow`:
```rust
use anyhow::Result;
Expand Down Expand Up @@ -334,5 +334,6 @@ What do you think happens if you modify the example above to change the polling
What would happen if you change the example to poll every 10 seconds?
Experiment with different "tick rates" and see how that affects the user experience.
Also, monitor your CPU usage when you do this experiment.
Monitor your CPU usage when you do this experiment.
What happens to your CPU usage as you change the poll frequency?
```

0 comments on commit b2c9558

Please sign in to comment.