1use std::fs::OpenOptions;
12use std::io::{BufWriter, Write};
13use std::path::Path;
14use std::sync::Mutex;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17pub struct Logger {
18 sink: Option<Mutex<BufWriter<std::fs::File>>>,
19}
20
21impl Logger {
22 pub fn disabled() -> Self {
24 Logger { sink: None }
25 }
26
27 pub fn to_file(path: &Path) -> std::io::Result<Self> {
29 let file = OpenOptions::new()
30 .create(true)
31 .write(true)
32 .truncate(true)
33 .open(path)?;
34 Ok(Logger {
35 sink: Some(Mutex::new(BufWriter::new(file))),
36 })
37 }
38
39 pub fn enabled(&self) -> bool {
40 self.sink.is_some()
41 }
42
43 pub fn event(&self, msg: &str) {
44 self.write_line("EVENT", msg);
45 }
46
47 pub fn read(&self, bytes: &[u8]) {
48 if self.sink.is_some() {
49 self.write_line("READ ", &escape(bytes));
50 }
51 }
52
53 pub fn write(&self, bytes: &[u8]) {
54 if self.sink.is_some() {
55 self.write_line("WRITE", &escape(bytes));
56 }
57 }
58
59 pub fn reply(&self, bytes: &[u8]) {
60 if self.sink.is_some() {
61 self.write_line("REPLY", &escape(bytes));
62 }
63 }
64
65 fn write_line(&self, tag: &str, body: &str) {
66 let Some(sink) = &self.sink else { return };
67 if let Ok(mut w) = sink.lock() {
68 let _ = writeln!(w, "{} {tag} {body}", timestamp());
69 let _ = w.flush();
70 }
71 }
72}
73
74fn timestamp() -> String {
76 let now = SystemTime::now()
77 .duration_since(UNIX_EPOCH)
78 .unwrap_or_default();
79 let secs = now.as_secs();
80 let millis = now.subsec_millis();
81 let h = (secs / 3600) % 24;
82 let m = (secs / 60) % 60;
83 let s = secs % 60;
84 format!("{h:02}:{m:02}:{s:02}.{millis:03}")
85}
86
87fn escape(bytes: &[u8]) -> String {
89 let mut out = String::with_capacity(bytes.len());
90 for &b in bytes {
91 match b {
92 b'\\' => out.push_str("\\\\"),
93 0x1b => out.push_str("\\e"),
94 b'\n' => out.push_str("\\n"),
95 b'\r' => out.push_str("\\r"),
96 b'\t' => out.push_str("\\t"),
97 0x20..=0x7e => out.push(b as char),
98 _ => out.push_str(&format!("\\x{b:02x}")),
99 }
100 }
101 out
102}