use std::{ fs::{self, OpenOptions}, io::Write, sync::{OnceLock, atomic::Ordering, mpsc}, thread, time::{SystemTime, UNIX_EPOCH}, }; use mtp::codec::{CommunicationValue, DataTypeId, DataValue, Version}; use ratatui::style::Color; use iota_state::{UNIQUE, UiLogEntry}; use tokio::sync::broadcast; pub mod language_creator; pub mod language_manager; static LOGGER: OnceLock> = OnceLock::new(); static LOG_BROADCASTER: OnceLock> = OnceLock::new(); #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[allow(unused)] pub enum PrintType { Call, Client, Iota, Omikron, Omega, General, Command, } impl PrintType { pub fn prefix_color(self) -> Color { match self { PrintType::Call => Color::Magenta, PrintType::Client => Color::Green, PrintType::Iota => Color::Yellow, PrintType::Omikron => Color::Blue, PrintType::Omega => Color::Cyan, PrintType::General => Color::LightCyan, PrintType::Command => Color::LightGreen, } } } struct LogMessage { timestamp_ms: u128, prefix: String, kind: PrintType, is_error: bool, translation_key: Option, format_args: Vec, message: Option, } /* The logger owns file persistence while consumers receive rendered entries * through a process-local broadcast subscription. */ pub fn startup() { startup_with_log_dir(Some( iota_paths::IotaPaths::resolve(iota_paths::Scope::User) .expect("resolve Iota user paths") .log_dir, )); } /// `None` keeps logging on stderr only (the systemd default). pub fn startup_with_log_dir(log_dir: Option) { let (tx, rx) = mpsc::channel::(); if LOGGER.set(tx).is_err() { return; } let (broadcast_tx, _) = broadcast::channel(512); let _ = LOG_BROADCASTER.set(broadcast_tx.clone()); thread::spawn(move || { let mut file = log_dir.and_then(|log_dir| { fs::create_dir_all(&log_dir).ok()?; let start_ts = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); OpenOptions::new() .create(true) .append(true) .open(log_dir.join(format!("log_{start_ts}.txt"))) .ok() }); for msg in rx { let resolved_message = if let Some(key) = msg.translation_key { let args: Vec<&str> = msg.format_args.iter().map(|s| s.as_str()).collect(); language_manager::format(&key, &args) } else { msg.message.unwrap_or_default() }; let timestamp = format_timestamp_inline(msg.timestamp_ms); let prefix = if msg.prefix.is_empty() { String::new() } else { format!("{} ", msg.prefix) }; let line = format!( "{} {} {}{}", fixed_box(&msg.timestamp_ms.to_string(), 13), timestamp, prefix, resolved_message ); if let Some(file) = file.as_mut() { let _ = writeln!(file, "{}", line); } let _ = writeln!(std::io::stderr(), "{}", line); let entry = UiLogEntry { timestamp_ms: msg.timestamp_ms, sender: format!("{:?}", msg.kind), message: resolved_message, is_error: msg.is_error, }; let _ = broadcast_tx.send(entry); } }); } pub fn subscribe() -> Option> { LOG_BROADCASTER.get().map(broadcast::Sender::subscribe) } fn format_timestamp_inline(timestamp_ms: u128) -> String { let secs = (timestamp_ms / 1000) as i64; let hours = (secs / 3600) % 24; let minutes = (secs / 60) % 60; let seconds = secs % 60; format!("[{:02}:{:02}:{:02}]", hours, minutes, seconds) } fn fixed_box(content: &str, width: usize) -> String { let s: String = content.chars().take(width).collect(); let len = s.chars().count(); if len < width { format!("[{}{}]", " ".repeat(width - len), s) } else { s } } pub fn log_internal_translated( kind: PrintType, prefix: String, is_error: bool, key: &str, args: Vec, ) { if let Some(tx) = LOGGER.get() { UNIQUE.store(true, Ordering::Relaxed); let _ = tx.send(LogMessage { timestamp_ms: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), prefix, kind, is_error, translation_key: Some(key.to_string()), format_args: args, message: None, }); } } pub fn log_internal(kind: PrintType, prefix: String, is_error: bool, message: String) { if let Some(tx) = LOGGER.get() { UNIQUE.store(true, Ordering::Relaxed); let _ = tx.send(LogMessage { timestamp_ms: SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_millis(), prefix, kind, is_error, translation_key: None, format_args: Vec::new(), message: Some(message), }); } } #[macro_export] macro_rules! log_t { ($key:expr) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), false, $key, vec![] ) }; ($key:expr, $($arg:expr),+) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), false, $key, vec![$($arg),+] ) }; } #[macro_export] macro_rules! log_t_err { ($key:expr) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), true, $key, vec![] ) }; ($key:expr, $($arg:expr),+) => { $crate::log_internal_translated( $crate::PrintType::General, "".to_string(), true, $key, vec![$($arg.to_string()),+] ) }; } /// Log a command message. #[macro_export] macro_rules! log_command { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::Command, "".to_string(), false, format!($($arg)*) ) }; } /// Log a general informational message. #[macro_export] macro_rules! log { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, "".to_string(), false, format!($($arg)*) ) }; } /// Log an inbound message (`>`). #[macro_export] macro_rules! log_in { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, ">".to_string(), false, format!($($arg)*) ) }; } /// Log an outbound message (`<`). #[macro_export] macro_rules! log_out { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, "<".to_string(), false, format!($($arg)*) ) }; } /// Log an error message (`>>`). #[macro_export] macro_rules! log_err { ($($arg:tt)*) => { $crate::log_internal( $crate::PrintType::General, ">>".to_string(), true, format!($($arg)*) ) }; } // ******** COMMUNICATION VALUES ******** pub fn log_cv_internal( prefix: &'static str, cv: &CommunicationValue, print_type: Option, ) { let formatted = format_cv(cv); log_internal( print_type.unwrap_or(PrintType::General), prefix.to_string(), false, formatted, ); } pub fn format_cv(cv: &CommunicationValue) -> String { let mut parts = Vec::new(); match (cv.sender(), cv.receiver()) { (Some(sender), Some(receiver)) => parts.push(format!("{} > {}", sender, receiver)), (Some(sender), None) => parts.push(sender.to_string()), (None, Some(receiver)) => parts.push(format!("> {}", receiver)), (None, None) => {} } let comm_type = cv .get_comm_type_enum() .map(|kind| kind.to_string()) .unwrap_or_else(|| cv.get_type().to_string()); let id = cv .id() .map_or_else(|| "none".to_string(), |value| value.to_string()); parts.push(format!("{} (id={})", comm_type, id)); let version = cv .type_map() .map(|type_map| type_map.version.clone()) .unwrap_or_else(|| Version(3, 0)); let formated_data = cv.data().map_or_else( || "".to_string(), |data| format_data_container(data.to_vec(), version), ); parts.push(format!("{}", formated_data)); parts.join(": ") } fn format_data_container(data: Vec<(DataTypeId, DataValue)>, version: Version) -> String { let parts: Vec = data .into_iter() .map(|(key, value)| { let key_str = key.to_string(); match value { DataValue::Str(s) => format!("{}=\"{}\"", key_str, abbreviate_string(&s)), DataValue::Container(inner) => { let inner_formatted = format_data_container(inner, version.clone()); format!("{}={{ {} }}", key_str, inner_formatted) } DataValue::Array(arr) => { let arr_formatted = format_array(arr, version.clone()); format!("{}=[{}]", key_str, arr_formatted) } DataValue::Bool(b) => format!("{}={}", key_str, b), DataValue::BoolTrue => format!("{}=true", key_str), DataValue::BoolFalse => format!("{}=false", key_str), DataValue::SignedNumber(num) => format!("{}={}", key_str, num), _ => "".to_string(), } }) .collect(); parts.join(", ") } fn format_array(arr: Vec, version: Version) -> String { let parts: Vec = arr .into_iter() .map(|value| match value { DataValue::Str(s) => format!("\"{}\"", abbreviate_string(&s)), DataValue::Container(inner) => { let inner_formatted = format_data_container(inner, version.clone()); format!("{{ {} }}", inner_formatted) } DataValue::Array(inner_arr) => { let formatted = format_array(inner_arr, version.clone()); format!("[{}]", formatted) } DataValue::Bool(b) => b.to_string(), DataValue::BoolTrue => "true".to_string(), DataValue::BoolFalse => "false".to_string(), DataValue::SignedNumber(num) => num.to_string(), _ => String::new(), }) .collect(); parts.join(", ") } fn abbreviate_string(value: &str) -> String { const EDGE_LENGTH: usize = 4; let chars: Vec = value.chars().collect(); if chars.len() <= EDGE_LENGTH * 2 { return value.to_string(); } let prefix: String = chars.iter().take(EDGE_LENGTH).collect(); let suffix: String = chars.iter().rev().take(EDGE_LENGTH).rev().collect(); format!("{prefix}...{suffix}") } #[cfg(test)] mod tests { use super::abbreviate_string; #[test] fn abbreviates_only_strings_longer_than_eight_characters() { assert_eq!(abbreviate_string("12345678"), "12345678"); assert_eq!(abbreviate_string("123456789"), "1234...6789"); assert_eq!(abbreviate_string("YWJjZGVmZ2hpag=="), "YWJj...ag=="); } } #[macro_export] macro_rules! log_cv { ($kind:expr, $cv:expr) => { $crate::log_cv_internal("", &$cv, Some($kind)) }; ($cv:expr) => { $crate::log_cv_internal("", &$cv, None) }; } #[macro_export] macro_rules! log_cv_in { ($kind:expr, $cv:expr) => { $crate::log_cv_internal("> ", &$cv, Some($kind)) }; ($cv:expr) => { $crate::log_cv_internal("> ", &$cv, None) }; } #[macro_export] macro_rules! log_cv_out { ($kind:expr, $cv:expr) => { $crate::log_cv_internal("< ", &$cv, Some($kind)) }; ($cv:expr) => { $crate::log_cv_internal("< ", &$cv, None) }; }