use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::Path;
use std::sync::Mutex;
use std::time::{SystemTime, UNIX_EPOCH};
pub struct Logger {
sink: Option<Mutex<BufWriter<std::fs::File>>>,
}
impl Logger {
pub fn disabled() -> Self {
Logger { sink: None }
}
pub fn to_file(path: &Path) -> std::io::Result<Self> {
let file = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(path)?;
Ok(Logger {
sink: Some(Mutex::new(BufWriter::new(file))),
})
}
pub fn enabled(&self) -> bool {
self.sink.is_some()
}
pub fn event(&self, msg: &str) {
self.write_line("EVENT", msg);
}
pub fn read(&self, bytes: &[u8]) {
if self.sink.is_some() {
self.write_line("READ ", &escape(bytes));
}
}
pub fn write(&self, bytes: &[u8]) {
if self.sink.is_some() {
self.write_line("WRITE", &escape(bytes));
}
}
pub fn reply(&self, bytes: &[u8]) {
if self.sink.is_some() {
self.write_line("REPLY", &escape(bytes));
}
}
fn write_line(&self, tag: &str, body: &str) {
let Some(sink) = &self.sink else { return };
if let Ok(mut w) = sink.lock() {
let _ = writeln!(w, "{} {tag} {body}", timestamp());
let _ = w.flush();
}
}
}
fn timestamp() -> String {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
let secs = now.as_secs();
let millis = now.subsec_millis();
let h = (secs / 3600) % 24;
let m = (secs / 60) % 60;
let s = secs % 60;
format!("{h:02}:{m:02}:{s:02}.{millis:03}")
}
fn escape(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len());
for &b in bytes {
match b {
b'\\' => out.push_str("\\\\"),
0x1b => out.push_str("\\e"),
b'\n' => out.push_str("\\n"),
b'\r' => out.push_str("\\r"),
b'\t' => out.push_str("\\t"),
0x20..=0x7e => out.push(b as char),
_ => out.push_str(&format!("\\x{b:02x}")),
}
}
out
}