364 lines
11 KiB
Rust
364 lines
11 KiB
Rust
use std::{
|
|
backtrace::{Backtrace, BacktraceStatus},
|
|
fmt::{Display, Write as _},
|
|
fs::{File, create_dir_all},
|
|
panic::{Location as PanicLocation, PanicHookInfo},
|
|
path::{Path, PathBuf},
|
|
thread::{self, Thread},
|
|
};
|
|
|
|
use pumpkin_util::text::{
|
|
TextComponent,
|
|
color::{Color, NamedColor},
|
|
};
|
|
use pumpkin_world::CURRENT_MC_VERSION;
|
|
use rustc_hash::FxHashMap;
|
|
use sysinfo::{Cpu, System};
|
|
use time::OffsetDateTime;
|
|
use tracing::error;
|
|
|
|
pub const BYTES_PER_MEBIBYTE: u64 = 1024 * 1024;
|
|
|
|
/// Writes to a string which cannot fail.
|
|
macro_rules! writeln_output {
|
|
($dst:expr $(,)?) => {
|
|
let _ = writeln!($dst);
|
|
};
|
|
($dst:expr, $($arg:tt)*) => {
|
|
let _ = writeln!($dst, $($arg)*);
|
|
};
|
|
}
|
|
|
|
/// A backtrace that either references
|
|
/// a full backtrace already generated,
|
|
/// or a new one.
|
|
pub enum FullBacktrace {
|
|
Captured,
|
|
ForceCaptured(Backtrace),
|
|
}
|
|
|
|
/// Represents the location of a character
|
|
/// in a file.
|
|
pub struct Location {
|
|
pub file_name: String,
|
|
pub line: u32,
|
|
pub column: u32,
|
|
}
|
|
|
|
impl From<&PanicLocation<'_>> for Location {
|
|
fn from(value: &PanicLocation) -> Self {
|
|
Self {
|
|
file_name: value.file().to_string(),
|
|
line: value.line(),
|
|
column: value.column(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for Location {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}:{}:{}", self.file_name, self.line, self.column)
|
|
}
|
|
}
|
|
|
|
/// Represents a crash report, containing
|
|
/// the required information.
|
|
///
|
|
/// This is stored without a lifetime related to a [`PanicHookInfo`]
|
|
/// so that information from a reference to a [`PanicHookInfo`]
|
|
/// can be stored globally without lifetime issues.
|
|
pub struct CrashReport {
|
|
utc_time: OffsetDateTime,
|
|
payload: Option<String>,
|
|
thread: Thread,
|
|
panic_location: Option<Location>,
|
|
full_backtrace: FullBacktrace,
|
|
captured_backtrace: Backtrace,
|
|
}
|
|
|
|
impl CrashReport {
|
|
/// Creates a new crash report detailing the state of the
|
|
/// server at the panic handler call site.
|
|
pub fn new(
|
|
panic_info: &PanicHookInfo<'_>,
|
|
captured_backtrace: Backtrace,
|
|
full_backtrace: FullBacktrace,
|
|
) -> Self {
|
|
Self {
|
|
utc_time: OffsetDateTime::now_utc(),
|
|
payload: {
|
|
let payload = panic_info.payload();
|
|
payload.downcast_ref::<&str>().map_or_else(
|
|
|| payload.downcast_ref::<String>().cloned(),
|
|
|s| Some(s.to_string()),
|
|
)
|
|
},
|
|
thread: thread::current(),
|
|
full_backtrace,
|
|
captured_backtrace,
|
|
panic_location: panic_info.location().map(Into::into),
|
|
}
|
|
}
|
|
|
|
/// Prints information about the crash to the console,
|
|
/// but not the full report.
|
|
#[allow(clippy::print_stderr)]
|
|
pub fn print_to_console(&self) {
|
|
const RED: Color = Color::Named(NamedColor::Red);
|
|
|
|
error!(
|
|
"{}",
|
|
TextComponent::text("Pumpkin has encountered a panic!")
|
|
.color(RED)
|
|
.bold()
|
|
.to_pretty_console()
|
|
);
|
|
|
|
error!("");
|
|
|
|
// Printing panic info.
|
|
let thread_name = self.thread.name().unwrap_or("<unnamed>");
|
|
let thread_id = self.thread.id();
|
|
|
|
let message = self.panic_location.as_ref().map_or_else(
|
|
|| format!("Thread '{thread_name}' {thread_id:?} panicked"),
|
|
|location| format!("Thread '{thread_name}' with {thread_id:?} panicked at {location}"),
|
|
);
|
|
|
|
if let Some(payload) = &self.payload {
|
|
error!("{}:", RED.console_color(&message));
|
|
error!("{payload}");
|
|
} else {
|
|
error!("{}", RED.console_color(&message));
|
|
}
|
|
|
|
error!("");
|
|
|
|
let backtrace_status = self.full_backtrace().status();
|
|
|
|
match backtrace_status {
|
|
BacktraceStatus::Unsupported => {
|
|
error!(
|
|
"{}",
|
|
RED.console_color("Backtracing is not supported for this platform.")
|
|
);
|
|
}
|
|
// It cannot possibly be BacktraceStatus::Disabled
|
|
// as it is a forced backtrace.
|
|
BacktraceStatus::Captured => {
|
|
error!(
|
|
"{}",
|
|
RED.console_color("The full backtrace will be printed to the crash report.")
|
|
);
|
|
|
|
if self.captured_backtrace.status() == BacktraceStatus::Captured {
|
|
eprintln!(
|
|
"{}\n{}",
|
|
RED.console_color("Backtrace:"),
|
|
self.captured_backtrace
|
|
);
|
|
}
|
|
}
|
|
_ => {
|
|
error!("{}", RED.console_color("Backtrace status is unknown, so no backtrace will be generated for the crash report."));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generates the file content of the crash report file
|
|
/// that would be generated from this report.
|
|
pub fn generate_file_content(&self) -> String {
|
|
let mut output = String::new();
|
|
|
|
writeln_output!(&mut output, "====== Pumpkin Crash Report ======");
|
|
writeln_output!(&mut output);
|
|
writeln_output!(&mut output, "Time: {}", self.utc_time);
|
|
writeln_output!(
|
|
&mut output,
|
|
"Message: {}",
|
|
self.payload.as_deref().unwrap_or("<unknown>")
|
|
);
|
|
|
|
if let Some(panic_location) = &self.panic_location {
|
|
writeln_output!(&mut output, "Panic Location: {}", panic_location);
|
|
}
|
|
|
|
writeln_output!(&mut output);
|
|
writeln_output!(&mut output, "--- Panicking Thread ---");
|
|
writeln_output!(&mut output, "ID: {:?}", self.thread.id());
|
|
if let Some(thread_name) = self.thread.name() {
|
|
writeln_output!(&mut output, "Name: {}", thread_name);
|
|
}
|
|
writeln_output!(&mut output, "Backtrace:");
|
|
writeln_output!(&mut output, "{}", self.full_backtrace());
|
|
|
|
writeln_output!(&mut output, "--- Server Details ---");
|
|
|
|
writeln_output!(
|
|
&mut output,
|
|
"Pumpkin Version: {}",
|
|
Self::get_pumpkin_version()
|
|
);
|
|
writeln_output!(&mut output, "Minecraft Version: {}", CURRENT_MC_VERSION);
|
|
writeln_output!(
|
|
&mut output,
|
|
"Server compiled with Rust {}",
|
|
rustc_version_runtime::version()
|
|
);
|
|
|
|
if sysinfo::IS_SUPPORTED_SYSTEM {
|
|
writeln_output!(&mut output, "\n--- System Details ---");
|
|
|
|
let mut sys = System::new_all();
|
|
sys.refresh_all();
|
|
|
|
writeln_output!(
|
|
&mut output,
|
|
"Operating System: {}",
|
|
System::long_os_version().unwrap_or("Unknown".to_string())
|
|
);
|
|
writeln_output!(
|
|
&mut output,
|
|
"Kernel: {}",
|
|
System::kernel_version().unwrap_or_else(|| "Unknown".to_string())
|
|
);
|
|
writeln_output!(
|
|
&mut output,
|
|
"Physical Memory: {} MiB/{} MiB used, {} MiB free",
|
|
sys.used_memory() / BYTES_PER_MEBIBYTE,
|
|
sys.total_memory() / BYTES_PER_MEBIBYTE,
|
|
sys.free_memory() / BYTES_PER_MEBIBYTE
|
|
);
|
|
writeln_output!(
|
|
&mut output,
|
|
"Swap Memory: {} MiB/{} MiB used, {} MiB free",
|
|
sys.used_swap() / BYTES_PER_MEBIBYTE,
|
|
sys.total_swap() / BYTES_PER_MEBIBYTE,
|
|
sys.free_swap() / BYTES_PER_MEBIBYTE
|
|
);
|
|
|
|
Self::write_cpus(&mut output, &sys);
|
|
}
|
|
|
|
output
|
|
}
|
|
|
|
fn write_cpus(output: &mut String, sys: &System) {
|
|
writeln_output!(output);
|
|
let cpus = sys.cpus();
|
|
|
|
writeln_output!(output, "Total cores: {}", cpus.len());
|
|
writeln_output!(output);
|
|
|
|
let mut different_brands: FxHashMap<(&str, &str), Vec<&Cpu>> = FxHashMap::default();
|
|
|
|
// `sysinfo` provides us a CPU for each core, so we try to group them.
|
|
for cpu in cpus {
|
|
different_brands
|
|
.entry((cpu.brand(), cpu.vendor_id()))
|
|
.or_default()
|
|
.push(cpu);
|
|
}
|
|
|
|
for (i, ((brand, vendor_id), cpus)) in different_brands.iter().enumerate() {
|
|
let prefix = format!(" CPU #{:<5}", i + 1);
|
|
let padded = " ".repeat(prefix.len());
|
|
|
|
let names = cpus
|
|
.iter()
|
|
.map(|cpu| cpu.name())
|
|
.collect::<Vec<&str>>()
|
|
.join(", ");
|
|
|
|
let avg_freq = cpus.iter().map(|cpu| cpu.frequency()).sum::<u64>() / cpus.len() as u64;
|
|
|
|
writeln_output!(output, "|{prefix} Cores: {}", cpus.len());
|
|
writeln_output!(output, "|{padded} Names: {}", names);
|
|
writeln_output!(output, "|{padded} Brand: {}", brand);
|
|
writeln_output!(output, "|{padded} Average Frequency: {} MHz", avg_freq);
|
|
writeln_output!(output, "|{padded} Vendor ID: {}", vendor_id);
|
|
writeln_output!(output);
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn get_pumpkin_version() -> String {
|
|
let profile = if cfg!(debug_assertions) {
|
|
"debug"
|
|
} else {
|
|
"release"
|
|
};
|
|
format!(
|
|
"{} (Commit: {}/{})",
|
|
env!("CARGO_PKG_VERSION"),
|
|
env!("GIT_HASH"),
|
|
profile
|
|
)
|
|
}
|
|
|
|
/// Saves this report to the `crash-reports` directory.
|
|
///
|
|
/// Returns a `Result` containing a path if successful.
|
|
pub fn save(&self) -> std::io::Result<PathBuf> {
|
|
const CRASH_REPORTS_DIR: &str = "./crash-reports";
|
|
|
|
let file_name = format!(
|
|
"crash-{}-{:02}-{:02}_{:02}.{:02}.{:02}.txt",
|
|
self.utc_time.year(),
|
|
self.utc_time.month() as u8,
|
|
self.utc_time.day(),
|
|
self.utc_time.hour(),
|
|
self.utc_time.minute(),
|
|
self.utc_time.second()
|
|
);
|
|
|
|
let path = Path::new(CRASH_REPORTS_DIR).join(file_name);
|
|
Self::write_text_to_path(&path, &self.generate_file_content()).map(|()| path)
|
|
}
|
|
|
|
/// Saves the crash report to a file and
|
|
/// prints about whether and where it saved.
|
|
///
|
|
/// Returns `true` if the file successfully saved.
|
|
pub fn save_and_log(&self) -> bool {
|
|
match self.save() {
|
|
Ok(path) => {
|
|
tracing::info!(
|
|
"{} {}",
|
|
Color::Named(NamedColor::Green)
|
|
.console_color("Successfully saved the crash report to file:"),
|
|
path.display()
|
|
);
|
|
true
|
|
}
|
|
Err(error) => {
|
|
tracing::error!(
|
|
"{} {}",
|
|
Color::Named(NamedColor::Red).console_color("Could not save the crash report:"),
|
|
error
|
|
);
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
fn write_text_to_path(path: &Path, text: &str) -> std::io::Result<()> {
|
|
if let Some(parent) = path.parent() {
|
|
create_dir_all(parent)?;
|
|
}
|
|
|
|
let mut file = File::create_new(path)?;
|
|
<File as std::io::Write>::write_all(&mut file, text.as_bytes())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Convenient method to return a reference to the report's full backtrace.
|
|
const fn full_backtrace(&self) -> &Backtrace {
|
|
match &self.full_backtrace {
|
|
FullBacktrace::Captured => &self.captured_backtrace,
|
|
FullBacktrace::ForceCaptured(backtrace) => backtrace,
|
|
}
|
|
}
|
|
}
|