Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: Add color-eyre and human-panic panic handler section 📚 #73

Merged
merged 1 commit into from
Sep 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.0); // 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,12 +183,12 @@ 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 {
if counter > 100 {
log::error!("Failed to abort task 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
Loading