use std::{
backtrace::Backtrace,
borrow::Cow,
fs::File,
io::{BufWriter, Write},
panic::PanicHookInfo,
path::PathBuf,
};
use chrono::{DateTime, Utc};
#[doc(hidden)]
pub fn try_generate_report(
metadata: &ProgramMetadata,
info: &PanicHookInfo,
timestamp: &DateTime<Utc>,
backtrace: &Backtrace,
) -> Option<PathBuf> {
let mut path = std::env::temp_dir();
path.push(format!("{:08x}.txt", fastrand::u64(0..=u64::MAX)));
let file = File::create(&path).ok()?;
let mut w = BufWriter::new(file);
let os = os_info::get();
writeln!(w, "Package: {}", metadata.package).ok()?;
writeln!(w, "Binary: {}", metadata.binary).ok()?;
writeln!(w, "Version: {}", metadata.version).ok()?;
writeln!(w).ok()?;
writeln!(w, "Architecture: {}", os.architecture().unwrap_or("(unknown)")).ok()?;
writeln!(w, "Operating system: {os}").ok()?;
writeln!(w, "Timestamp: {timestamp}").ok()?;
writeln!(w).ok()?;
let payload_str =
match (info.payload().downcast_ref::<&str>(), info.payload().downcast_ref::<String>()) {
(None, None) => "Unknown",
(Some(str), None) => *str,
(None, Some(string)) => string.as_str(),
(Some(_), Some(_)) => unreachable!(),
};
writeln!(w, "Message: {payload_str}").ok()?;
if let Some(loc) = info.location() {
writeln!(w, "Source location: {}:{}", loc.file(), loc.line()).ok()?;
} else {
writeln!(w, "Source location: (unknown)").ok()?;
}
writeln!(w).ok()?;
write!(w, "{backtrace}").ok()?;
w.flush().ok()?;
Some(path)
}
#[doc(hidden)]
pub fn get_timestamp() -> DateTime<Utc> {
chrono::Utc::now()
}
#[macro_export]
macro_rules! setup {
($metadata:expr, $replace:expr) => {
$crate::setup!(
$metadata,
$replace,
"\
Uh oh! {package} crashed.
A crash log was saved at the following path:
{log_path}
To help us figure out why this happened, please report this crash.
Either open a new issue on GitHub [1] or send an email to the author(s) [2].
Attach the file listed above or copy and paste its contents into the report.
[1]: {repository}/issues/new
[2]: {authors}
For your privacy, we don't automatically collect any information, so we rely on
users to submit crash reports to help us find issues. Thank you!"
)
};
($metadata:expr, $replace:expr, $template:literal) => {{
let metadata = $metadata;
let replace = $replace;
let old_hook = std::panic::take_hook();
let new_hook = ::std::boxed::Box::new(move |info: &::std::panic::PanicHookInfo| {
let timestamp = $crate::get_timestamp();
if !replace {
old_hook(info);
}
if let Some(log_path) =
$crate::try_generate_report(&metadata, info, ×tamp, &::std::backtrace::Backtrace::force_capture())
{
if <::std::io::Stderr as ::std::io::IsTerminal>::is_terminal(&::std::io::stderr()) {
eprint!("\x1b[31m");
}
if !replace {
eprintln!("\n---\n");
}
eprintln!(
concat!("{package:.0}{binary:.0}{version:.0}{repository:.0}{authors:.0}{log_path:.0}", $template),
package = metadata.package,
binary = metadata.binary,
version = metadata.version,
repository = metadata.repository,
authors = metadata.authors,
log_path = log_path.display(),
);
if <::std::io::Stderr as ::std::io::IsTerminal>::is_terminal(&::std::io::stderr()) {
eprint!("\x1b[m");
}
} else if !replace {
old_hook(info);
}
});
::std::panic::set_hook(new_hook);
}};
}
pub const DEFAULT_USER_MESSAGE_TEMPLATE: &str = "\
Uh oh! {package} crashed.
A crash log was saved at the following path:
{log_path}
To help us figure out why this happened, please report this crash.
Either open a new issue on GitHub [1] or send an email to the author(s) [2].
Attach the file listed above or copy and paste its contents into the report.
[1]: {repository}/issues/new
[2]: {authors}
For your privacy, we don't automatically collect any information, so we rely on
users to submit crash reports to help us find issues. Thank you!";
#[derive(Debug, Clone)]
pub struct ProgramMetadata {
pub package: Cow<'static, str>,
pub binary: Cow<'static, str>,
pub version: Cow<'static, str>,
pub repository: Cow<'static, str>,
pub authors: Cow<'static, str>,
}
impl ProgramMetadata {
pub fn capitalized(self) -> Self {
let mut new = self;
let mut chars = new.package.chars();
new.package = chars
.next()
.iter()
.flat_map(|first_letter| first_letter.to_uppercase())
.chain(chars)
.collect::<String>()
.into();
new
}
}
#[macro_export]
macro_rules! cargo_metadata {
() => {
$crate::ProgramMetadata {
package: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_NAME")),
binary: ::std::borrow::Cow::Borrowed(env!("CARGO_BIN_NAME")),
version: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_VERSION")),
repository: ::std::borrow::Cow::Borrowed(env!("CARGO_PKG_REPOSITORY")),
authors: $crate::cow_replace(env!("CARGO_PKG_AUTHORS"), ":", ", "),
}
};
(default = $placeholder:literal) => {
$crate::ProgramMetadata {
package: ::std::borrow::Cow::Borrowed(
option_env!("CARGO_PKG_NAME").unwrap_or($placeholder),
),
binary: ::std::borrow::Cow::Borrowed(
option_env!("CARGO_BIN_NAME").unwrap_or($placeholder),
),
version: ::std::borrow::Cow::Borrowed(
option_env!("CARGO_PKG_VERSION").unwrap_or($placeholder),
),
repository: ::std::borrow::Cow::Borrowed(
option_env!("CARGO_PKG_REPOSITORY").unwrap_or($placeholder),
),
authors: option_env!("CARGO_PKG_AUTHORS")
.map_or(::std::borrow::Cow::Borrowed($placeholder), |s| {
$crate::cow_replace(s, ":", ", ")
}),
}
};
}
#[doc(hidden)]
pub fn cow_replace<'a>(s: &'a str, from: &str, to: &str) -> Cow<'a, str> {
if s.contains(from) {
Cow::Owned(s.replace(from, to))
} else {
Cow::Borrowed(s)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn capitalize_package_name() {
let metadata = ProgramMetadata {
package: "crashlog".into(),
binary: "".into(),
version: "".into(),
repository: "".into(),
authors: "".into(),
};
let new = metadata.capitalized();
assert_eq!("Crashlog", new.package);
let mut empty = new;
empty.package = "".into();
let new = empty.capitalized();
assert_eq!("", new.package);
}
#[test]
fn metadata_placeholder() {
let metadata = cargo_metadata!(default = "place");
assert_eq!(metadata.binary, "place");
}
#[test]
fn test_cow_replace() {
let s = "abc:def";
assert!(matches!(cow_replace(s, ":", ","), Cow::Owned(val) if val == "abc,def"));
let s = "abc:def";
assert!(matches!(cow_replace(s, "+", " "), Cow::Borrowed(val) if val == s));
}
}