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 360855c
Show file tree
Hide file tree
Showing 11 changed files with 311 additions and 66 deletions.
3 changes: 2 additions & 1 deletion src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,10 @@
- [CLI arguments](./how-to/develop-apps/cli-arguments.md)
- [Configuration Directories](./how-to/develop-apps/config-directories.md)
- [Tracing](./how-to/develop-apps/tracing.md)
- [Arrange your App Code](./how-to/develop-apps/arrange-your-app-code.md)
- [Arrange your App Code](./how-to/develop-apps/abstract-terminal-and-event-handler.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
3 changes: 2 additions & 1 deletion src/how-to/develop-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This section covers topics on how to develop applications:
- [CLI arguments](./cli-arguments.md)
- [Configuration Directories](./config-directories.md)
- [Tracing](./tracing.md)
- [Arrange your App Code](./arrange-your-app-code.md)
- [Arrange your App Code](./abstract-terminal-and-event-handler.md)
- [Setup Panic Hooks](./setup-panic-hooks.md)
- [Use `better-panic`](./better-panic.md)
- [Use `color-eyre` and `human-panic`](./setup-panic-hooks-color-eyre.md)
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ Then you'll be able write code like this:
```rust
impl App {
async fn run(&mut self) -> Result<()> {
let mut tui = tui::Tui::new(self.tick_rate)?;
let mut tui = tui::Tui::new()?;
tui.tick_rate(4.0); // 4 ticks per second
tui.frame_rate(30); // 30 frames per second
tui.enter()?; // Starts event handler
loop {
tui.draw(|f| { // Deref allows calling `tui.draw`
Expand Down Expand Up @@ -67,12 +69,16 @@ use tokio::{
};
use tokio_util::sync::CancellationToken;

pub type Frame<'a> = ratatui::Frame<'a, Backend<std::io::Stderr>>;

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum Event {
Init,
Quit,
Error,
Closed,
Tick,
Render,
FocusGained,
FocusLost,
Paste(String),
Expand All @@ -87,29 +93,44 @@ pub struct Tui {
pub cancellation_token: CancellationToken,
pub event_rx: UnboundedReceiver<Event>,
pub event_tx: UnboundedSender<Event>,
pub tick_rate: usize,
pub frame_rate: f64,
pub tick_rate: f64,
}

impl Tui {
pub fn new(tick_rate: usize) -> Result<Self> {
pub fn new() -> Result<Self> {
let tick_rate = 4.0; // 4 ticks per second
let frame_rate = 30.0; // 30 frames per seconds
let terminal = ratatui::Terminal::new(Backend::new(std::io::stderr()))?;
let (event_tx, event_rx) = mpsc::unbounded_channel();
let cancellation_token = CancellationToken::new();
let task = tokio::spawn(async {});
Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, tick_rate })
Ok(Self { terminal, task, cancellation_token, event_rx, event_tx, frame_rate, tick_rate })
}

pub fn tick_rate(&mut self, tick_rate: f64) {
self.tick_rate = tick_rate;
}

pub fn frame_rate(&mut self, frame_rate: f64) {
self.frame_rate = frame_rate;
}

pub fn start(&mut self) {
let tick_rate = std::time::Duration::from_millis(self.tick_rate as u64);
let tick_delay = std::time::Duration::from_secs_f64(1.0 / self.tick_rate);
let render_delay = std::time::Duration::from_secs_f64(1.0 / self.frame_rate);
self.cancel();
self.cancellation_token = CancellationToken::new();
let _cancellation_token = self.cancellation_token.clone();
let _event_tx = self.event_tx.clone();
self.task = tokio::spawn(async move {
let mut reader = crossterm::event::EventStream::new();
let mut interval = tokio::time::interval(tick_rate);
let mut tick_interval = tokio::time::interval(tick_delay);
let mut render_interval = tokio::time::interval(render_delay);
_event_tx.send(Event::Init).unwrap();
loop {
let delay = interval.tick();
let tick_delay = tick_interval.tick();
let render_delay = render_interval.tick();
let crossterm_event = reader.next().fuse();
tokio::select! {
_ = _cancellation_token.cancelled() => {
Expand Down Expand Up @@ -147,9 +168,12 @@ impl Tui {
None => {},
}
},
_ = delay => {
_ = tick_delay => {
_event_tx.send(Event::Tick).unwrap();
},
_ = render_delay => {
_event_tx.send(Event::Render).unwrap();
},
}
}
});
Expand All @@ -159,13 +183,13 @@ impl Tui {
self.cancel();
let mut counter = 0;
while !self.task.is_finished() {
std::thread::sleep(Duration::from_secs(1));
std::thread::sleep(Duration::from_millis(1));
counter += 1;
if counter > 5 {
if counter > 50 {
self.task.abort();
}
if counter > 10 {
log::error!("Failed to abort task for unknown reason");
if counter > 100 {
log::error!("Failed to abort task in 100 milliseconds for unknown reason");
return Err(color_eyre::eyre::eyre!("Unable to abort task"));
}
}
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.
202 changes: 202 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,202 @@
# Use color-eyre and human-panic

### color-eyre panic hook

One way to manage printing of stack-traces is by using
[`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
```

## Configuration

You can mix and match different panic handlers, using `better-panic` for debug builds and
`color-eyre` and `human-panic` for release builds. The code below also prints the `color-eyre`
stacktrace to `log::error!` for good measure (after striping ansi escape sequences).

```console
cargo add color-eyre human-panic libc better-panic strip-ansi-escapes
```

Here's code you can copy paste into your project (if you use the
[`Tui`](./abstract-terminal-and-event-handler.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); // prints color-eyre stack trace to stderr
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);
// prints human-panic message
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(())
}
```
Loading

0 comments on commit 360855c

Please sign in to comment.