use std::fs::File;
use std::io::{self, Write};
#[cfg(unix)]
use std::os::unix::fs::OpenOptionsExt;
use std::sync::Mutex;
use std::{
fs::{self, OpenOptions},
path::PathBuf,
process,
time::SystemTime,
};
use once_cell::sync::OnceCell;
use crate::{
common::{filesystem, strings::format_timestamp},
constants,
};
static LOGGER: OnceCell<Mutex<Logger>> = OnceCell::new();
static LOG_LEVEL: OnceCell<Mutex<Level>> = OnceCell::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Level {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3,
}
impl Level {
fn as_str(&self) -> &'static str {
match self {
Level::Debug => "DEBUG",
Level::Info => "INFO",
Level::Warn => "WARN",
Level::Error => "ERROR",
}
}
}
#[derive(Debug)]
pub struct Logger {
file: Mutex<File>,
}
impl Logger {
fn new(log_file: PathBuf) -> io::Result<Self> {
if let Some(dir) = log_file.parent() {
fs::create_dir_all(dir)?;
}
let file = {
#[cfg(unix)]
{
OpenOptions::new()
.create(true)
.append(true)
.mode(0o600)
.open(&log_file)?
}
#[cfg(windows)]
{
OpenOptions::new()
.create(true)
.append(true)
.open(&log_file)?
}
};
Ok(Self {
file: Mutex::new(file),
})
}
fn write(&self, level: Level, file: Option<&str>, line: Option<u32>, msg: &str) {
if let Some(level_lock) = LOG_LEVEL.get() {
if let Ok(min_level) = level_lock.lock() {
if level < *min_level {
return;
}
}
}
let now = SystemTime::now();
let timestamp = format_timestamp(now);
let file_info = if let (Some(file), Some(line)) = (file, line) {
format!("[{}:{}] ", file, line)
} else {
"".to_string()
};
let result = (|| -> io::Result<()> {
let mut file_handle = self
.file
.lock()
.map_err(|_| io::Error::other("Mutex poisoned"))?;
writeln!(
file_handle,
"[{}] {} {}{}",
timestamp,
level.as_str(),
file_info,
msg
)
})();
if let Err(e) = result {
eprintln!("Failed to write to log file: {e}");
}
}
fn write_startup_banner(&self) {
let version = env!("CARGO_PKG_VERSION");
let os = std::env::consts::OS;
let arch = std::env::consts::ARCH;
let pid = process::id();
let now = SystemTime::now();
let timestamp = format_timestamp(now);
self.write(Level::Info, None, None, "=== TermiType Startup ===");
self.write(Level::Info, None, None, &format!("Version: {}", version));
self.write(Level::Info, None, None, &format!("OS: {} ({})", os, arch));
self.write(Level::Info, None, None, &format!("PID: {}", pid));
self.write(
Level::Info,
None,
None,
&format!("Started at: {}", timestamp),
);
self.write(Level::Info, None, None, "==========================");
}
}
pub fn init() -> io::Result<()> {
let initial_level = if cfg!(debug_assertions) {
Level::Debug
} else {
Level::Info
};
let config_dir = filesystem::config_dir().map_err(std::io::Error::other)?;
let log_file = config_dir.join(constants::logger_file());
LOG_LEVEL.get_or_init(|| Mutex::new(initial_level));
LOGGER.get_or_try_init(|| -> io::Result<Mutex<Logger>> {
let logger = Logger::new(log_file)?;
logger.write_startup_banner();
Ok(Mutex::new(logger))
})?;
Ok(())
}
pub fn set_level(level: Level) {
if let Some(level_lock) = LOG_LEVEL.get() {
if let Ok(mut current_level) = level_lock.lock() {
*current_level = level;
}
}
}
pub fn write(level: Level, file: Option<&str>, line: Option<u32>, msg: &str) {
if let Some(logger) = LOGGER.get() {
if let Ok(logger) = logger.lock() {
logger.write(level, file, line, msg);
}
}
}
#[cfg(debug_assertions)]
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {{
$crate::logger::write(
$crate::logger::Level::Debug,
Some(file!()),
Some(line!()),
&format!($($arg)*)
)
}};
}
#[cfg(not(debug_assertions))]
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => {{}};
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => {{
$crate::logger::write(
$crate::logger::Level::Info,
Some(file!()),
Some(line!()),
&format!($($arg)*)
)
}};
}
#[macro_export]
macro_rules! log_warn {
($($arg:tt)*) => {{
$crate::logger::write(
$crate::logger::Level::Warn,
Some(file!()),
Some(line!()),
&format!($($arg)*)
)
}};
}
#[macro_export]
macro_rules! log_error {
($($arg:tt)*) => {{
$crate::logger::write(
$crate::logger::Level::Error,
Some(file!()),
Some(line!()),
&format!($($arg)*)
)
}};
}
#[cfg(test)]
mod test {
use super::*;
use std::fs;
use std::path::PathBuf;
use tempfile::NamedTempFile;
fn setup(log_path: PathBuf) {
let dummy_path = PathBuf::from("target/.init_dummy.log");
if let Some(p) = dummy_path.parent() {
fs::create_dir_all(p).ok();
}
let _ = LOGGER.get_or_try_init(|| -> io::Result<Mutex<Logger>> {
LOG_LEVEL.get_or_init(|| Mutex::new(Level::Debug));
let logger = Logger::new(dummy_path)?;
Ok(Mutex::new(logger))
});
if let Some(logger_mutex) = LOGGER.get() {
let mut logger_guard = logger_mutex.lock().expect("Failed to lock logger mutex");
let test_specific_logger = Logger::new(log_path).expect("Failed to create test logger");
*logger_guard = test_specific_logger;
} else {
panic!("Logger failed to initialize");
}
}
#[test]
fn test_log_levels() {
let temp_file = NamedTempFile::new().expect("Failed to create temp log file");
let log_path = temp_file.path().to_path_buf();
setup(log_path.clone());
set_level(Level::Info);
fs::write(&log_path, "").expect("Failed to clear log file");
write(Level::Debug, None, None, "Debug message");
write(Level::Info, None, None, "Info message");
write(Level::Warn, None, None, "Warn message");
write(Level::Error, None, None, "Error message");
let content = fs::read_to_string(&log_path).expect("Failed to read temp log file");
assert!(
!content.contains("Debug message"),
"Content should not contain Debug: {content}",
);
assert!(
content.contains("Info message"),
"Content should contain Info: {content}",
);
assert!(
content.contains("Warn message"),
"Content should contain Warn: {content}",
);
assert!(
content.contains("Error message"),
"Content should contain Error: {content}",
);
}
}