use std::fs::{File, OpenOptions}; use std::io::Write; use std::path::PathBuf; use std::time::SystemTime; const LOG_NAME: &str = "meatshell-debug.log"; pub fn init() -> Option { let (file, path) = match open_log_file() { Some(pair) => pair, None => { install_panic_hook(None); init_stderr_tracing(); tracing::warn!("failed to create {LOG_NAME}; logging to stderr only"); return None; } }; install_panic_hook(Some(path.clone())); let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,meatshell=debug,winit=error")); let log_file = file; if tracing_subscriber::fmt() .with_env_filter(filter) .with_ansi(false) .with_thread_ids(true) .with_writer(move || log_file.try_clone().expect("clone debug log file")) .try_init() .is_err() { return Some(path); } tracing::info!( version = env!("CARGO_PKG_VERSION"), os = std::env::consts::OS, arch = std::env::consts::ARCH, "debug logging initialized" ); if log_paths_enabled() { tracing::debug!(log = %path.display(), "debug log path"); if let Ok(cwd) = std::env::current_dir() { tracing::debug!(cwd = %cwd.display(), "current working directory"); } if let Ok(exe) = std::env::current_exe() { tracing::debug!(exe = %exe.display(), "current executable"); } } Some(path) } fn init_stderr_tracing() { let filter = tracing_subscriber::EnvFilter::try_from_default_env() .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info,meatshell=debug,winit=error")); let _ = tracing_subscriber::fmt() .with_env_filter(filter) .with_ansi(false) .with_thread_ids(true) .try_init(); } fn open_log_file() -> Option<(File, PathBuf)> { let mut dirs = Vec::new(); if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { dirs.push(dir.to_path_buf()); } } if let Ok(cwd) = std::env::current_dir() { if !dirs.iter().any(|dir| dir == &cwd) { dirs.push(cwd); } } for dir in dirs { let path = dir.join(LOG_NAME); if let Ok(file) = OpenOptions::new().create(true).append(true).open(&path) { return Some((file, path)); } } None } fn install_panic_hook(path: Option) { let previous = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { let payload = if let Some(message) = info.payload().downcast_ref::<&str>() { *message } else if let Some(message) = info.payload().downcast_ref::() { message.as_str() } else { "" }; let location = info .location() .map(|loc| format!("{}:{}:{}", short_file(loc.file()), loc.line(), loc.column())) .unwrap_or_else(|| "".to_string()); let backtrace = if env_flag("MEATSHELL_DEBUG_BACKTRACE") { Some(std::backtrace::Backtrace::force_capture().to_string()) } else { None }; if let Some(path) = &path { if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) { let _ = writeln!( file, "\n===== panic {:?} =====\nlocation: {}\nmessage: {}", SystemTime::now(), location, payload ); if let Some(backtrace) = &backtrace { let _ = writeln!(file, "backtrace:\n{}", backtrace); } let _ = writeln!(file); } } if let Some(backtrace) = &backtrace { tracing::error!( location = %location, message = %payload, backtrace = %backtrace, "panic captured" ); } else { tracing::error!( location = %location, message = %payload, "panic captured" ); } previous(info); })); } pub(crate) fn log_paths_enabled() -> bool { env_flag("MEATSHELL_DEBUG_PATHS") } fn env_flag(name: &str) -> bool { matches!( std::env::var(name).as_deref(), Ok("1") | Ok("true") | Ok("TRUE") | Ok("yes") | Ok("YES") | Ok("on") | Ok("ON") ) } fn short_file(path: &str) -> &str { path.rsplit(['/', '\\']).next().unwrap_or(path) }