xianyun_executor/
editmodelogger.rs1use std::io::Write;
14use std::path::PathBuf;
15
16pub const DIR_NAME: &str = "logs";
18
19pub struct Logger {
21 name: String,
22 dir: String,
23 out: Box<dyn Write + Send>,
24 err: Box<dyn Write + Send>,
25}
26
27impl Logger {
28 pub fn info(&mut self, msg: &str) {
35 let _ = writeln!(self.out, "[{}] {msg}", self.name);
36 }
37
38 pub fn error(&mut self, msg: &str) {
40 let _ = writeln!(self.err, "[{}] {msg}", self.name);
41 }
42
43 fn filename(&self) -> PathBuf {
44 PathBuf::from(&self.dir).join(format!("{}.log", escape_filename(&self.name)))
45 }
46}
47
48pub fn new(proc_name: &str, logs_dir: &str) -> Result<Logger, std::io::Error> {
60 let logger_shape = Logger {
61 name: proc_name.to_string(),
62 dir: logs_dir.to_string(),
63 out: Box::new(std::io::sink()),
64 err: Box::new(std::io::sink()),
65 };
66
67 if logs_dir.is_empty() {
68 return Ok(logger_shape);
69 }
70
71 let path = logger_shape.filename();
72 let file = std::fs::OpenOptions::new()
73 .create(true)
74 .append(true)
75 .open(&path)?;
76 let file2 = std::fs::OpenOptions::new()
77 .create(true)
78 .append(true)
79 .open(&path)?;
80
81 Ok(Logger {
82 name: logger_shape.name,
83 dir: logger_shape.dir,
84 out: Box::new(file),
85 err: Box::new(file2),
86 })
87}
88
89fn escape_filename(name: &str) -> String {
91 let base = std::path::Path::new(name)
92 .file_name()
93 .map(|s| s.to_string_lossy().to_string())
94 .unwrap_or_else(|| name.to_string());
95
96 let mut out = base;
97 for r in ['/', '\\', ':', '*', '?', '"', '<', '>', '|'] {
98 out = out.replace(r, "_");
99 }
100 out
101}
102
103pub fn tail(
105 proc_name: &str,
106 logs_dir: &str,
107 max_lines: i64,
108) -> Result<Vec<String>, std::io::Error> {
109 let filename = PathBuf::from(logs_dir).join(format!("{}.log", escape_filename(proc_name)));
110 let content = std::fs::read_to_string(&filename)?;
111
112 let max_lines = if max_lines < 0 {
113 usize::MAX
114 } else {
115 usize::try_from(max_lines).unwrap_or(usize::MAX)
116 };
117
118 let mut lines: Vec<String> = Vec::new();
119 for line in content.lines() {
120 if lines.len() >= max_lines {
121 lines.remove(0);
122 }
123 lines.push(line.to_string());
124 }
125
126 Ok(lines)
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn logger_info() {
135 let mut log = Logger {
142 name: "puma".into(),
143 dir: String::new(),
144 out: Box::new(std::io::sink()),
145 err: Box::new(std::io::sink()),
146 };
147
148 log.info("starting...");
149 }
150
151 #[test]
152 fn logger_error() {
153 let mut log = Logger {
154 name: "vite".into(),
155 dir: String::new(),
156 out: Box::new(std::io::sink()),
157 err: Box::new(std::io::sink()),
158 };
159
160 log.error("something went wrong");
161 }
162
163 #[test]
164 fn new_creates_log_file() {
165 let tmp_dir = tempfile::tempdir().unwrap();
166
167 let _log = new("puma", tmp_dir.path().to_str().unwrap()).unwrap();
168
169 let log_file = tmp_dir.path().join("puma.log");
170 assert!(log_file.exists());
171 }
172
173 #[test]
174 fn new_no_logs_dir() {
175 let _log = new("puma", "").unwrap();
176 }
181
182 #[test]
183 fn tail_reads_last_n_lines() {
184 let tmp_dir = tempfile::tempdir().unwrap();
185 let mut log = new("dog", tmp_dir.path().to_str().unwrap()).unwrap();
186
187 log.info("woof: tail wag");
188
189 let lines = tail("dog", tmp_dir.path().to_str().unwrap(), 1).unwrap();
190 assert_eq!(lines, vec!["[dog] woof: tail wag".to_string()]);
191 }
192}