zenyx-engine/engine/src/metadata.rs

809 lines
23 KiB
Rust

use std::collections::HashSet;
use std::fmt;
use std::str::FromStr;
use std::{env, error::Error, path::PathBuf, thread};
use native_dialog::{MessageDialog, MessageType};
use parking_lot::Once;
use raw_cpuid::CpuId;
use sysinfo::{CpuRefreshKind, RefreshKind, System};
use tracing::error;
use wgpu::DeviceType;
mod build_info {
include!(concat!(env!("OUT_DIR"), "/built.rs"));
}
static INIT: Once = Once::new();
pub fn set_panic_hook() {
INIT.call_once(|| {
let default_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
if let Err(e) = process_panic(info) {
eprintln!("Error in panic hook: {}", e);
default_hook(info);
}
std::process::exit(1);
}));
});
}
fn process_panic(info: &std::panic::PanicHookInfo<'_>) -> Result<(), Box<dyn Error>> {
use std::io::Write;
use colored::Colorize;
let log_dir = PathBuf::from_str("./").expect("wtf, The current directory no longer exists?");
if !log_dir.exists() {
std::fs::create_dir_all(&log_dir)?;
}
let log_path = log_dir.join("panic.log");
let mut file = std::fs::File::create(&log_path)?;
let payload = info.payload();
let payload_str = if let Some(s) = payload.downcast_ref::<&str>() {
*s
} else if let Some(s) = payload.downcast_ref::<String>() {
s
} else {
"<non-string panic payload>"
};
writeln!(file, "Panic Occurred: {}", payload_str)?;
if let Some(location) = info.location() {
writeln!(file, "Panic Location: {}", location)?;
}
writeln!(file, "{}", capture_backtrace().sanitize_path())?;
// Add more contextual information
writeln!(file, "\n--- Additional Information ---")?;
// Rust Version
if let Ok(rust_version) = rust_version() {
writeln!(file, "Rust Version: {}", rust_version)?;
}
// Command-line Arguments
writeln!(file, "Command-line Arguments:")?;
for arg in env::args() {
writeln!(file, " {}", arg)?;
}
// Environment Variables (consider filtering sensitive ones)
writeln!(file, "\nEnvironment Variables (selected):")?;
let interesting_env_vars = ["PATH", "RUST_VERSION", "CARGO_TARGET_DIR", "HOME", "USER"];
for (key, value) in env::vars() {
if interesting_env_vars.contains(&key.as_str()) {
writeln!(file, " {}: {}", key, value)?;
}
}
// Current Working Directory
if let Ok(cwd) = env::current_dir() {
writeln!(file, "\nCurrent Working Directory: {}", cwd.display())?;
}
// Thread Information
if let Some(thread) = thread::current().name() {
writeln!(file, "\nThread Name: {}", thread)?;
} else {
writeln!(file, "\nThread ID: {:?}", thread::current().id())?;
}
let panic_msg = format!(
r#"Zenyx had a problem and crashed. To help us diagnose the problem you can send us a crash report.
We have generated a detailed report file at '{}'. Submit an issue or email with the subject of 'Zenyx Crash Report' and include the report as an attachment.
To submit the crash report:
https://codeberg.org/Caznix/Zenyx/issues
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!"#,
log_path.display()
);
let final_msg = format!(
r#"{}
For future reference, the error summary is as follows:
{}
More details can be found in the crash report file."#,
panic_msg, payload_str
);
println!("{}", final_msg.red().bold());
if let Err(e) = MessageDialog::new()
.set_type(MessageType::Error)
.set_title("A fatal error in Zenyx has occurred")
.set_text(&final_msg)
.show_confirm()
{
error!("Failed to show message dialog: {e}")
}
Ok(())
}
fn rust_version() -> Result<String, Box<dyn Error>> {
let version = env!("CARGO_PKG_RUST_VERSION");
Ok(version.to_string())
}
fn capture_backtrace() -> String {
let mut backtrace = String::new();
let sysinfo = SystemMetadata::current();
backtrace.push_str(&format!(
"--- System Information ---\n{}\n",
sysinfo.verbose_summary()
));
let trace = std::backtrace::Backtrace::force_capture();
let message = "\n--- Backtrace ---\n\n".to_string();
backtrace.push_str(&message);
backtrace.push_str(&format!("{trace:#}"));
backtrace
}
trait Sanitize {
fn sanitize_path(&self) -> String;
}
impl Sanitize for str {
fn sanitize_path(&self) -> String {
let prefixes = ["/home/", "/Users/", "\\Users\\", "/opt/home/"];
let mut result = String::from(self);
for prefix in prefixes {
if let Some(start_index) = result.find(prefix) {
let start_of_user = start_index + prefix.len();
let mut end_of_user = result[start_of_user..]
.find(|c| c == '/' || c == '\\')
.map(|i| start_of_user + i)
.unwrap_or(result.len());
if end_of_user == start_of_user && start_of_user < result.len() {
end_of_user = result.len();
}
result.replace_range(start_of_user..end_of_user, "<USER>");
break;
}
}
result
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Memory {
bytes: u64,
}
impl Memory {
pub const fn from_bytes(bytes: u64) -> Self {
Self { bytes }
}
pub const fn from_kb(kb: u64) -> Self {
Self { bytes: kb * 1024 }
}
pub const fn from_mb(mb: u64) -> Self {
Self {
bytes: mb * 1024 * 1024,
}
}
pub const fn from_gb(gb: u64) -> Self {
Self {
bytes: gb * 1024 * 1024 * 1024,
}
}
pub const fn as_bytes(&self) -> u64 {
self.bytes
}
pub const fn as_kb(&self) -> u64 {
self.bytes / 1024
}
pub const fn as_mb(&self) -> u64 {
self.bytes / (1024 * 1024)
}
pub const fn as_gb(&self) -> u64 {
self.bytes / (1024 * 1024 * 1024)
}
pub fn format_human(&self) -> String {
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
let mut size = self.bytes as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
format!("{:.2} {}", size, UNITS[unit_index])
}
pub fn verbose_info(&self) -> String {
format!(
"{} ({} bytes, {} KB, {} MB, {} GB)",
self.format_human(),
self.as_bytes(),
self.as_kb(),
self.as_mb(),
self.as_gb()
)
}
}
impl fmt::Display for Memory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.format_human())
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CPUBrand {
Intel,
AMD,
SnapDragon,
Apple,
Other(String),
}
impl From<&str> for CPUBrand {
fn from(s: &str) -> Self {
let sl = s.to_lowercase();
if sl.contains("intel") {
Self::Intel
} else if sl.contains("amd") {
Self::AMD
} else if sl.contains("snapdragon") {
Self::SnapDragon
} else if sl.contains("apple") {
Self::Apple
} else {
Self::Other(s.to_string())
}
}
}
impl fmt::Display for CPUBrand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Intel => write!(f, "Intel"),
Self::AMD => write!(f, "AMD"),
Self::SnapDragon => write!(f, "SnapDragon"),
Self::Apple => write!(f, "Apple"),
Self::Other(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum CPUArch {
X86,
X86_64,
AArch64,
Other(String),
}
impl FromStr for CPUArch {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(match s {
"x86" => Self::X86,
"x86_64" => Self::X86_64,
"arm" => Self::AArch64,
"aarch64" => Self::AArch64,
_ => Self::Other(s.to_string()),
})
}
}
impl fmt::Display for CPUArch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::X86 => write!(f, "x86"),
Self::X86_64 => write!(f, "x86_64"),
Self::AArch64 => write!(f, "AArch64"),
Self::Other(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ClockSpeed(pub u32);
impl ClockSpeed {
pub fn as_mhz(&self) -> u32 {
self.0
}
pub fn as_ghz(&self) -> f32 {
self.0 as f32 / 1000.0
}
}
impl fmt::Display for ClockSpeed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:.2} GHz ({} MHz)", self.as_ghz(), self.as_mhz())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CPU {
pub brand: CPUBrand,
pub arch: CPUArch,
pub name: String,
pub vendor_id: String,
pub physical_cores: Option<u8>,
pub logical_cores: Option<u8>,
pub max_clock_speed: ClockSpeed,
pub current_clock_speed: ClockSpeed,
pub l1_cache: Option<Memory>,
pub l2_cache: Option<Memory>,
pub l3_cache: Option<Memory>,
}
impl CPU {
pub fn current() -> Self {
let mut sys = System::new_with_specifics(
RefreshKind::default().with_cpu(CpuRefreshKind::everything()),
);
sys.refresh_cpu_all();
let cpu_opt = sys.cpus().first();
let brand = cpu_opt
.map(|cpu| cpu.brand().into())
.unwrap_or(CPUBrand::Other("unknown".into()));
let name = cpu_opt
.map(|cpu| cpu.name().to_string())
.unwrap_or_else(|| "unknown".into());
let vendor_id = cpu_opt
.map(|cpu| cpu.vendor_id().to_string())
.unwrap_or_else(|| "unknown".into());
let max_clock_speed = cpu_opt
.map(|cpu| ClockSpeed(cpu.frequency() as u32))
.unwrap_or(ClockSpeed(0));
let current_clock_speed = max_clock_speed;
let logical_cores = cpu_opt.map(|_| sys.cpus().len() as u8);
let physical_cores = System::physical_core_count().map(|pc| pc as u8);
let cpuid = CpuId::new();
let mut l1_cache = None;
let mut l2_cache = None;
let mut l3_cache = None;
if let Some(iter) = cpuid.get_cache_parameters() {
for cache in iter {
match cache.level() {
1 => {
let size = cache.physical_line_partitions()
* cache.coherency_line_size()
* cache.associativity();
if size > 0 {
l1_cache = Some(Memory::from_bytes(size.try_into().unwrap()));
}
}
2 => {
let size = cache.physical_line_partitions()
* cache.coherency_line_size()
* cache.associativity();
if size > 0 {
l2_cache = Some(Memory::from_bytes(size.try_into().unwrap()));
}
}
3 => {
let size = (cache.physical_line_partitions() as u64)
* (cache.coherency_line_size() as u64)
* (cache.associativity() as u64);
if size > 0 {
l3_cache = Some(Memory::from_bytes(size));
}
}
_ => {}
}
}
}
Self {
brand,
arch: env::consts::ARCH
.parse()
.unwrap_or(CPUArch::Other("unknown".into())),
name,
vendor_id,
physical_cores,
logical_cores,
max_clock_speed,
current_clock_speed,
l1_cache,
l2_cache,
l3_cache,
}
}
pub fn is_intel(&self) -> bool {
matches!(self.brand, CPUBrand::Intel)
}
pub fn is_amd(&self) -> bool {
matches!(self.brand, CPUBrand::AMD)
}
pub fn is_arm(&self) -> bool {
matches!(self.brand, CPUBrand::Intel)
}
pub fn is_high_clock(&self) -> bool {
self.max_clock_speed.0 > 3000
}
pub fn verbose_info(&self) -> String {
format!(
"CPU Information:\n\
- Brand: {}\n\
- Architecture: {}\n\
- Name: {}\n\
- Vendor ID: {}\n\
- Cores: {} physical, {} logical\n\
- Clock Speed: {} (max), {} (current)\n\
- Cache: L1 {}, L2 {}, L3 {}",
self.brand,
self.arch,
self.name,
self.vendor_id,
self.physical_cores
.map(|c| c.to_string())
.unwrap_or_else(|| "unknown".into()),
self.logical_cores
.map(|c| c.to_string())
.unwrap_or_else(|| "unknown".into()),
self.max_clock_speed,
self.current_clock_speed,
self.l1_cache
.map(|c| c.format_human())
.unwrap_or_else(|| "unknown".into()),
self.l2_cache
.map(|c| c.format_human())
.unwrap_or_else(|| "unknown".into()),
self.l3_cache
.map(|c| c.format_human())
.unwrap_or_else(|| "unknown".into()),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum GPUBrand {
Nvidia,
AMD,
Intel,
Other(String),
}
impl From<&str> for GPUBrand {
fn from(s: &str) -> Self {
let sl = s.to_lowercase();
if sl.contains("nvidia") || sl.contains("geforce") {
Self::Nvidia
} else if sl.contains("amd") || sl.contains("radeon") {
Self::AMD
} else if sl.contains("intel") {
Self::Intel
} else {
Self::Other(s.to_string())
}
}
}
impl fmt::Display for GPUBrand {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nvidia => write!(f, "NVIDIA"),
Self::AMD => write!(f, "AMD"),
Self::Intel => write!(f, "Intel"),
Self::Other(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GPU {
pub brand: GPUBrand,
pub name: String,
pub device_type: DeviceType,
pub vram: Memory,
pub driver_version: Option<String>,
}
impl GPU {
pub fn current() -> Vec<Self> {
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
instance
.enumerate_adapters(wgpu::Backends::all())
.iter()
.map(|adapter| {
let info = adapter.get_info();
GPU {
brand: info.name.as_str().into(),
name: info.name.to_string(),
device_type: info.device_type,
vram: Memory::from_bytes(0),
driver_version: Some(info.driver.to_string()),
}
})
.collect()
}
pub fn is_integrated(&self) -> bool {
matches!(self.brand, GPUBrand::Intel)
}
pub fn is_dedicated(&self) -> bool {
!self.is_integrated()
}
pub fn is_mobile(&self) -> bool {
let lower_name = self.name.to_lowercase();
lower_name.contains("adreno")
|| lower_name.contains("mali")
|| lower_name.contains("apple")
|| lower_name.contains("mobile")
|| lower_name.contains("snapdragon")
}
pub fn verbose_info(&self) -> String {
format!(
"GPU Information:\n\
- Brand: {}\n\
- Name: {}\n\
- VRAM: {}\n\
- Driver: {}",
self.brand,
self.name,
self.vram.format_human(),
self.driver_version.as_deref().unwrap_or("Unknown")
)
}
fn unique_id(&self) -> String {
format!("{}-{:?}", self.name, self.device_type)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemMemory {
pub total: Memory,
pub used: Memory,
pub free: Memory,
pub available: Memory,
pub swap_total: Memory,
pub swap_used: Memory,
pub swap_free: Memory,
}
impl SystemMemory {
pub fn current() -> Self {
let mut system = System::new();
system.refresh_memory();
Self {
total: Memory::from_bytes(system.total_memory()),
used: Memory::from_bytes(system.used_memory()),
free: Memory::from_bytes(system.free_memory()),
available: Memory::from_bytes(system.available_memory()),
swap_total: Memory::from_bytes(system.total_swap()),
swap_used: Memory::from_bytes(system.used_swap()),
swap_free: Memory::from_bytes(system.free_swap()),
}
}
pub fn verbose_info(&self) -> String {
format!(
"Memory Information:\n\
- RAM: {} total, {} used, {} free, {} available\n\
- Swap: {} total, {} used, {} free",
self.total.format_human(),
self.used.format_human(),
self.free.format_human(),
self.available.format_human(),
self.swap_total.format_human(),
self.swap_used.format_human(),
self.swap_free.format_human()
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EngineInfo {
pub timestamp: String,
pub pkg_version: String,
pub pkg_name: String,
pub target_arch: String,
pub target_os: String,
pub target_env: String,
pub rustc_version: String,
pub wgpu_version: String,
pub winit_version: String,
pub commit_hash: String,
}
impl EngineInfo {
pub fn current() -> Self {
Self {
timestamp: build_info::BUILT_TIME_UTC.to_string(),
pkg_version: build_info::PKG_VERSION.to_string(),
pkg_name: build_info::PKG_NAME.to_string(),
target_arch: build_info::CFG_TARGET_ARCH.to_string(),
target_os: build_info::CFG_OS.to_string(),
target_env: build_info::CFG_ENV.to_string(),
rustc_version: build_info::RUSTC_VERSION.to_string(),
wgpu_version: build_info::WGPU_VERSION.to_string(),
winit_version: build_info::WGPU_VERSION.to_string(),
commit_hash: build_info::GIT_COMMIT_HASH
.unwrap_or(&format!("UNKNOWN-{:?}", std::time::SystemTime::now()))
.to_string(),
}
}
pub fn verbose_info(&self) -> String {
format!(
"Build Information:\n\
- Timestamp: {}\n\
- Package: {} v{}\n\
- Target: {} {} {}\n\
- Rustc version: {}\n\
- Wgpu version: {}\n\
- Winit version: {}\n\
- commit_hash: {}\n\
",
self.timestamp,
self.pkg_name,
self.pkg_version,
self.target_arch,
self.target_os,
self.target_env,
self.rustc_version,
self.wgpu_version,
self.winit_version,
self.commit_hash
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemMetadata {
pub cpu: CPU,
pub memory: SystemMemory,
pub gpus: Vec<GPU>,
pub compile_info: EngineInfo,
pub os_info: OSInfo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OSInfo {
pub name: String,
pub version: Option<String>,
pub kernel_version: Option<String>,
}
impl OSInfo {
pub fn current() -> Self {
let mut system = System::new();
system.refresh_all();
Self {
name: System::name().unwrap_or_else(|| build_info::TARGET.to_string()),
version: System::os_version(),
kernel_version: System::kernel_version(),
}
}
pub fn verbose_info(&self) -> String {
format!(
"Operating System Information:\n\
- Name: {}\n\
- Version: {}\n\
- Kernel Version: {}",
self.name,
self.version.as_deref().unwrap_or("Unknown"),
self.kernel_version.as_deref().unwrap_or("Unknown")
)
}
}
impl SystemMetadata {
pub fn current() -> Self {
Self {
cpu: CPU::current(),
memory: SystemMemory::current(),
gpus: GPU::current(),
compile_info: EngineInfo::current(),
os_info: OSInfo::current(),
}
}
pub fn main_gpu(&self) -> Option<&GPU> {
self.gpus
.iter()
.find(|g| g.is_dedicated())
.or_else(|| self.gpus.iter().find(|g| g.is_integrated()))
.or_else(|| self.gpus.first())
}
pub fn verbose_summary(&self) -> String {
let main_gpu = self.main_gpu();
let main_gpu_info = main_gpu
.map(|gpu| format!("Main GPU:\n{}", gpu.verbose_info()))
.unwrap_or_else(|| "No main GPU detected".to_string());
let mut seen_gpu_ids = HashSet::new();
let mut other_gpus_info = Vec::new();
if let Some(gpu) = main_gpu {
seen_gpu_ids.insert(gpu.unique_id());
}
for gpu in &self.gpus {
let gpu_id = gpu.unique_id();
if !seen_gpu_ids.contains(&gpu_id) {
other_gpus_info.push(format!("[{:?}] {}", gpu.device_type, gpu.verbose_info()));
seen_gpu_ids.insert(gpu_id);
}
}
let other_gpu_list = if other_gpus_info.is_empty() {
String::new()
} else {
format!("Other GPUs:\n{}", other_gpus_info.join("\n\n"))
};
format!(
"System Information:\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}\n\n{}",
self.os_info.verbose_info(),
self.cpu.verbose_info(),
main_gpu_info,
other_gpu_list,
self.memory.verbose_info(),
self.compile_info.verbose_info()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memory_conversions() {
let mem = Memory::from_gb(8);
assert_eq!(mem.as_bytes(), 8 * 1024 * 1024 * 1024);
assert_eq!(mem.as_mb(), 8 * 1024);
assert_eq!(mem.as_kb(), 8 * 1024 * 1024);
}
#[test]
fn test_system_metadata() {
let metadata = SystemMetadata::current();
assert!(!metadata.cpu.name.is_empty());
assert!(metadata.memory.total.as_bytes() > 0);
assert!(!metadata.compile_info.pkg_version.is_empty());
assert!(!metadata.os_info.name.is_empty());
}
}