diff --git a/Cargo.lock b/Cargo.lock index 47057557..58789e3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1896,6 +1896,8 @@ dependencies = [ name = "inlyne" version = "0.4.1" dependencies = [ + "anstream", + "anstyle", "anyhow", "base64 0.22.0", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 72dfa564..a8628725 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,8 @@ glyphon = "0.3" string_cache = { version = "0.8.7", default-features = false } raw-window-handle = "0.5.2" edit = "0.1.5" +anstream = "0.6.13" +anstyle = "1.0.6" [profile.release] strip = true diff --git a/src/main.rs b/src/main.rs index 31ccf5df..87e77111 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,7 @@ pub mod image; pub mod interpreter; mod keybindings; pub mod opts; +mod panic_hook; pub mod positioner; pub mod renderer; pub mod table; @@ -745,7 +746,7 @@ impl Inlyne { } fn main() -> anyhow::Result<()> { - human_panic::setup_panic!(); + setup_panic!(); let env_filter = tracing_subscriber::EnvFilter::builder() .with_default_directive("inlyne=info".parse()?) diff --git a/src/panic_hook.rs b/src/panic_hook.rs new file mode 100644 index 00000000..c01e5def --- /dev/null +++ b/src/panic_hook.rs @@ -0,0 +1,203 @@ +//! `human_panic` tailored more to our needs +//! +//! We provide the report data in markdown so that it can be pasted into a github issue and provide +//! actionable information on how to find and submit issues + +// We need to display info to `stderr`, and the macro makes it harder to use a more local `allow` +#![allow(clippy::print_stderr)] + +use std::{ + fmt::Write, + hash::Hasher, + io, + panic::PanicInfo, + path::{Path, PathBuf}, +}; + +use human_panic::{report::Method, Metadata}; +use serde::Deserialize; + +#[macro_export] +macro_rules! setup_panic { + () => { + match ::human_panic::PanicStyle::default() { + ::human_panic::PanicStyle::Debug => {} + ::human_panic::PanicStyle::Human => { + let meta = ::human_panic::metadata!(); + + ::std::panic::set_hook(::std::boxed::Box::new( + move |info: &::std::panic::PanicInfo| { + eprintln!("{info}"); + let file_path = $crate::panic_hook::handle_dump(&meta, info); + $crate::panic_hook::print_msg(file_path.as_deref(), &meta).unwrap(); + }, + )); + } + } + }; +} + +#[derive(Deserialize)] +struct Report { + name: String, + operating_system: String, + crate_version: String, + explanation: String, + cause: String, + backtrace: String, +} + +impl Report { + fn new(name: &str, version: &str, method: Method, explanation: String, cause: String) -> Self { + human_panic::report::Report::new(name, version, method, explanation, cause).into() + } + + fn serialize(&self) -> Option { + let Self { + name, + operating_system, + crate_version, + explanation, + cause, + backtrace, + } = self; + + let explanation = explanation.trim(); + + let mut buf = String::new(); + write!( + buf, + "\ +# Crash Report + +| Name | `{name}` | +| ---: | :--- | +| Version | `{crate_version}` | +| Operating System | {operating_system} | + +`````text +Cause: {cause} + +Explanation: +{explanation} +````` + +
+(backtrace) + +`````text{backtrace} +````` + +
+ +--- + +" + ) + .ok()?; + Some(buf) + } + + fn persist(&self) -> Option { + let contents = self.serialize()?; + let tmp_dir = std::env::temp_dir(); + let report_uid = { + let mut hasher = twox_hash::XxHash64::default(); + hasher.write(contents.as_bytes()); + hasher.finish() + }; + let report_filename = format!("inlyne-report-{report_uid:x}.md"); + let report_path = tmp_dir.join(report_filename); + std::fs::write(&report_path, &contents).ok()?; + + Some(report_path) + } +} + +impl From for Report { + fn from(report: human_panic::report::Report) -> Self { + let toml_text = toml::to_string(&report).unwrap(); + toml::from_str(&toml_text).unwrap() + } +} + +pub fn handle_dump(meta: &Metadata, panic_info: &PanicInfo) -> Option { + let mut expl = String::new(); + + let message = match ( + panic_info.payload().downcast_ref::<&str>(), + panic_info.payload().downcast_ref::(), + ) { + (Some(s), _) => Some(s.to_string()), + (_, Some(s)) => Some(s.to_string()), + (None, None) => None, + }; + + let cause = match message { + Some(m) => m, + None => "Unknown".into(), + }; + + match panic_info.location() { + Some(location) => { + let file = location.file(); + let line = location.line(); + expl.push_str(&format!("Panic occurred in file '{file}' at line {line}\n",)) + } + None => expl.push_str("Panic location unknown.\n"), + } + + let report = Report::new(&meta.name, &meta.version, Method::Panic, expl, cause); + let maybe = report.persist(); + if maybe.is_none() { + eprintln!("{}", report.serialize().unwrap()); + } + + maybe +} + +pub fn print_msg(file_path: Option<&Path>, meta: &Metadata) -> Option<()> { + use io::Write as _; + + let stderr = anstream::stderr(); + let mut stderr = stderr.lock(); + + write!(stderr, "{}", anstyle::AnsiColor::Red.render_fg()).ok()?; + write_msg(&mut stderr, file_path, meta)?; + write!(stderr, "{}", anstyle::Reset.render()).ok()?; + + Some(()) +} + +fn write_msg(buffer: &mut impl io::Write, file_path: Option<&Path>, meta: &Metadata) -> Option<()> { + let name = &meta.name; + let report_path = match file_path { + Some(fp) => format!("{}", fp.display()), + None => "".to_string(), + }; + + write!( + buffer, + "\ +Well, this is embarrassing. + +{name} 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 \"{report_path}\". You can search +for issues with similar explanations at the following url: + +- https://github.com/Inlyne-Project/inlyne/issues?q=label%3AC-crash-report + +and you can submit a new crash report using the report file as a template if there are no existing +issues matching your own (the following link has the crash report label) + +- https://github.com/Inlyne-Project/inlyne/issues/new?labels=C-crash-report + +We take privacy seriously, and do not preform any auotmated error collection. In order to improve +the software we, rely on people to submit reports. + +Thank you kindly!" + ) + .ok()?; + + Some(()) +}