468 lines
12 KiB
Rust
468 lines
12 KiB
Rust
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<mpsc::Sender<LogMessage>> = OnceLock::new();
|
|
static LOG_BROADCASTER: OnceLock<broadcast::Sender<UiLogEntry>> = 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<String>,
|
|
format_args: Vec<String>,
|
|
message: Option<String>,
|
|
}
|
|
|
|
/* 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<std::path::PathBuf>) {
|
|
let (tx, rx) = mpsc::channel::<LogMessage>();
|
|
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<broadcast::Receiver<UiLogEntry>> {
|
|
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<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: 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<PrintType>,
|
|
) {
|
|
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(
|
|
|| "<opaque payload>".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<String> = 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<DataValue>, version: Version) -> String {
|
|
let parts: Vec<String> = 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<char> = 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)
|
|
};
|
|
}
|