| 1 | use std::{ |
| 2 | fs::{self, OpenOptions}, |
| 3 | io::{self, Write}, |
| 4 | path::{Path, PathBuf}, |
| 5 | sync::{Mutex, OnceLock}, |
| 6 | time::{SystemTime, UNIX_EPOCH}, |
| 7 | }; |
| 8 | |
| 9 | use crate::AppResult; |
| 10 | |
| 11 | static LOG_FILE: OnceLock<Mutex<fs::File>> = OnceLock::new(); |
| 12 | |
| 13 | pub fn init(path: &Path) -> AppResult<PathBuf> { |
| 14 | if let Some(parent) = path.parent() { |
| 15 | fs::create_dir_all(parent)?; |
| 16 | } |
| 17 | |
| 18 | let mut options = OpenOptions::new(); |
| 19 | options.create(true).append(true); |
| 20 | #[cfg(unix)] |
| 21 | { |
| 22 | use std::os::unix::fs::OpenOptionsExt; |
| 23 | options.mode(0o600); |
| 24 | } |
| 25 | let file = options.open(path)?; |
| 26 | let _ = LOG_FILE.set(Mutex::new(file)); |
| 27 | |
| 28 | let panic_hook = std::panic::take_hook(); |
| 29 | std::panic::set_hook(Box::new(move |panic| { |
| 30 | let message = panic |
| 31 | .payload() |
| 32 | .downcast_ref::<&str>() |
| 33 | .copied() |
| 34 | .or_else(|| panic.payload().downcast_ref::<String>().map(String::as_str)) |
| 35 | .unwrap_or("non-string panic payload"); |
| 36 | let location = panic |
| 37 | .location() |
| 38 | .map(|location| { |
| 39 | format!( |
| 40 | " at {}:{}:{}", |
| 41 | location.file(), |
| 42 | location.line(), |
| 43 | location.column() |
| 44 | ) |
| 45 | }) |
| 46 | .unwrap_or_default(); |
| 47 | error(&format!("panic: {message}{location}")); |
| 48 | panic_hook(panic); |
| 49 | })); |
| 50 | |
| 51 | info(&format!("logging initialized: {}", path.display())); |
| 52 | Ok(path.to_path_buf()) |
| 53 | } |
| 54 | |
| 55 | pub fn info(message: &str) { |
| 56 | write("INFO", message); |
| 57 | } |
| 58 | |
| 59 | pub fn error(message: &str) { |
| 60 | write("ERROR", message); |
| 61 | } |
| 62 | |
| 63 | fn write(level: &str, message: &str) { |
| 64 | let Some(file) = LOG_FILE.get() else { |
| 65 | return; |
| 66 | }; |
| 67 | |
| 68 | let timestamp = SystemTime::now() |
| 69 | .duration_since(UNIX_EPOCH) |
| 70 | .unwrap_or_default(); |
| 71 | let line = format!( |
| 72 | "[{}.{:03}] {level} {message}", |
| 73 | timestamp.as_secs(), |
| 74 | timestamp.subsec_millis() |
| 75 | ); |
| 76 | let mut file = file |
| 77 | .lock() |
| 78 | .unwrap_or_else(std::sync::PoisonError::into_inner); |
| 79 | if let Err(error) = writeln!(file, "{line}") { |
| 80 | report_write_error(error); |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | fn report_write_error(error: io::Error) { |
| 85 | eprintln!("failed to write rmptui log: {error}"); |
| 86 | } |