use crate::{
app::{APP_NAME, data_dir, set_rust_backtrace},
term_output, utils,
};
use anyhow::{Context, Result};
use std::{
backtrace::Backtrace,
io::Write,
panic::PanicHookInfo,
path::{Path, PathBuf},
};
use termcolor::{Color, ColorSpec, WriteColor};
pub fn initialize_panic_hook(no_color: bool) -> Result<()> {
set_rust_backtrace();
std::panic::set_hook(Box::new(move |panic_info| {
let crash_report_file = crash_report_file();
let backtrace = std::backtrace::Backtrace::capture();
let panic_report = PanicReport::new(panic_info, backtrace, no_color);
if let Err(err) = panic_report.write_report_and_print_msg(&crash_report_file) {
log::error!("{err}");
eprintln!("{err}")
}
std::process::exit(1);
}));
Ok(())
}
#[derive(Debug)]
pub struct CargoMetadata {
pub crate_name: String,
pub crate_version: String,
pub crate_authors: String,
pub crate_homepage: String,
pub crate_repository: String,
pub operating_system: String,
}
impl Default for CargoMetadata {
fn default() -> Self {
let crate_name = {
let name = env!("CARGO_PKG_NAME").trim().to_string();
if !name.is_empty() {
name
} else {
"Unknown".to_string()
}
};
let crate_version = {
let version = env!("CARGO_PKG_VERSION").trim().to_string();
if !version.is_empty() {
version
} else {
"Unknown".to_string()
}
};
let crate_authors = {
let authors = env!("CARGO_PKG_AUTHORS").trim().to_string();
if !authors.is_empty() {
authors.replace(':', ", ")
} else {
"Unknown".to_string()
}
};
let crate_homepage = {
let homepage = env!("CARGO_PKG_HOMEPAGE").trim().to_string();
if !homepage.is_empty() {
homepage
} else {
"Unknown".to_string()
}
};
let crate_repository = {
let repository = env!("CARGO_PKG_REPOSITORY").trim().to_string();
if !repository.is_empty() {
repository
} else {
"Unknown".to_string()
}
};
let operating_system: String = os_info::get().to_string();
Self {
crate_name,
crate_version,
crate_authors,
crate_homepage,
crate_repository,
operating_system,
}
}
}
impl std::fmt::Display for CargoMetadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let pretty_print = format!(
"crate_name : {}\ncrate_version : {}\ncrate_authors : {}\ncrate_homepage : {}\ncrate_repository: {}\noperating_system: {}\n",
self.crate_name,
self.crate_version,
self.crate_authors,
self.crate_homepage,
self.crate_repository,
self.operating_system,
);
write!(f, "{pretty_print}")
}
}
#[derive(Debug)]
pub struct PanicReport<'a> {
panic_info: &'a PanicHookInfo<'a>,
backtrace: Backtrace,
no_color: bool,
}
#[derive(Debug, Default)]
struct HumanReadableReport {
cargo_metadata: CargoMetadata,
explanation: String,
cause: String,
backtrace: String,
thread_name: String,
}
impl HumanReadableReport {
fn explanation(mut self, explanation: String) -> Self {
self.explanation = explanation;
self
}
fn cause(mut self, cause: String) -> Self {
self.cause = cause;
self
}
fn backtrace(mut self, backtrace: String) -> Self {
self.backtrace = backtrace;
self
}
fn thread_name(mut self, thread_name: &str) -> Self {
self.thread_name = thread_name.to_string();
self
}
fn serialize(&self) -> String {
format!(
"{}\nexplanation: {}\ncause : {}\nthread : {}\n\n{}",
self.cargo_metadata, self.explanation, self.cause, self.thread_name, self.backtrace
)
}
}
impl<'a> PanicReport<'a> {
pub fn new(panic_info: &'a PanicHookInfo, backtrace: Backtrace, no_color: bool) -> Self {
Self {
panic_info,
backtrace,
no_color,
}
}
pub fn write_report_and_print_msg(&self, p: &Path) -> Result<()> {
let report = self.build_human_readable_report();
let mut crash_report = std::fs::File::create(p).with_context(|| {
format!(
"Failed to create Crash-Report file: {}",
utils::absolute_path_as_string(p)
)
})?;
crash_report.write_all(report.as_bytes()).with_context(|| {
format!(
"Failed to write crash report to file: {}",
utils::absolute_path_as_string(p),
)
})?;
let path_to_crash_report = utils::absolute_path_as_string(p);
let mut stdout = term_output::get_stdout(self.no_color);
stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)).set_bold(true))?;
writeln!(
stdout,
"\nThe application panicked (crashed). Please see the Crash-Report file for more information"
)?;
stdout.reset()?;
println!(
"\n- A crash report file was generated: '{}' \
\n- Submit an issue to: '{}/issues' with the subject of '{} Crash Report' \
and include the report as an attachment. \
\n- Thank you for your help!",
path_to_crash_report,
env!("CARGO_PKG_REPOSITORY"),
APP_NAME
);
Ok(())
}
fn build_human_readable_report(&self) -> String {
let thread = std::thread::current();
let thread_name = thread.name().unwrap_or("<unnamed>");
let message = match (
self.panic_info.payload().downcast_ref::<&str>(),
self.panic_info.payload().downcast_ref::<String>(),
) {
(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(),
};
let panic_location = match self.panic_info.location() {
Some(location) => format!(
"Panic occurred in file '{}' at line '{}'",
location.file(),
location.line()
),
None => "Panic location unknown".to_string(),
};
let backtrace = format!("{:#?}", self.backtrace);
HumanReadableReport::default()
.explanation(panic_location)
.cause(cause)
.backtrace(backtrace)
.thread_name(thread_name)
.serialize()
}
}
fn crash_report_file() -> PathBuf {
let crash_report_file_name = format!(
"{}-Crash-Report_{}.log",
APP_NAME,
chrono::Local::now().format("%Y-%m-%dT%H_%M_%S")
);
data_dir().join(crash_report_file_name)
}