1use std::{
2 fs::{remove_file, rename, File},
3 io::{stderr, BufRead, BufReader, Write},
4 os::{fd::FromRawFd, unix::fs::MetadataExt},
5 path::{Path, PathBuf},
6 str::FromStr,
7 sync::Arc,
8 time::SystemTime,
9};
10
11use anyhow::{anyhow, Context};
12use cj_path_util::path_util::AppendToPath;
13use nix::unistd::{close, dup2, pipe, setsid, Pid};
14
15use crate::{
16 daemon::DaemonError, eval_with_default::EvalWithDefault, file_util::open_append,
17 timestamp_formatter::TimestampFormatter, unix::easy_fork,
18};
19
20pub fn starts_with_timestamp(line: &str) -> bool {
23 let mut digits = 0;
24 let mut minus = 0;
25 let mut plus = 0;
26 let mut t = 0;
27 let mut slash = 0;
28 let mut colon = 0;
29 let mut space = 0;
30 let mut dot = 0;
31 let mut other = 0;
32 for (i, c) in line.chars().enumerate() {
33 if i > 40 {
34 return false;
35 }
36 if c == '\t' {
37 return other <= 5
38 && digits >= 16
39 && slash == 0
40 && colon >= 2
41 && dot <= 1
42 && space <= 2
43 && minus <= 3
44 && plus <= 1
45 && t <= 1;
46 }
47 if c.is_ascii_digit() {
48 digits += 1;
49 } else if c == '-' {
50 minus += 1;
51 } else if c == ':' {
52 colon += 1;
53 } else if c == ' ' {
54 space += 1;
55 } else if c == '+' {
56 plus += 1;
57 } else if c == 'T' {
58 t += 1;
59 } else if c == '/' {
60 slash += 1;
61 } else if c == '.' {
62 dot += 1;
63 } else {
64 other += 1;
65 }
66 }
67 false
69}
70
71#[test]
72fn t_starts_with_timestamp() {
73 let cases = [
74 ("2026-01-11 15:25:12.000445551 +01:00 src/run/working_directory_pool.rs:552:17 process_working_directory D40 (None for test-running versioned dataset search at_2026-01-11T15:25:11.936345998+01:00) succeeded.", true),
75 ("2026-01-11T15:25:12.000319897+01:00 src/run/working_directory_pool.rs:552:17 process_working_directory D40 (None for test-running versioned dataset search at_2026-01-11T15:25:11.936345998+01:00) succeeded.", true),
76 ("src/run/working_directory_pool.rs:552:17 process_working_directory D40 (None for test-running versioned dataset search at_2026-01-11T15:25:11.936345998+01:00) succeeded.", false),
77 ];
78 for (s, expected) in &cases {
79 assert!(
80 starts_with_timestamp(s) == *expected,
81 "{s:?} to yield {expected:?}"
82 );
83 }
84}
85
86#[derive(Debug, Clone)]
87pub enum TimestampMode {
88 Always,
90 Automatic {
92 mark_added_timestamps: bool,
98 },
99 Never,
102}
103
104#[derive(Debug, Clone)]
107pub struct TimestampOpts {
108 pub use_rfc3339: bool,
111
112 pub mode: TimestampMode,
113}
114
115#[derive(Debug, Clone, Default, clap::Args)]
116pub struct LocalTimeOpts {
117 #[clap(long)]
120 pub local_time: bool,
121
122 #[clap(long)]
125 pub utc_time: bool,
126}
127
128impl EvalWithDefault for LocalTimeOpts {
129 fn explicit_yes_and_no(&self) -> (bool, bool) {
130 let Self {
131 local_time,
132 utc_time,
133 } = self;
134 (*local_time, *utc_time)
135 }
136}
137
138#[derive(Debug, Clone, Default, clap::Args)]
139pub struct LoggingOpts {
140 #[clap(flatten)]
141 pub local_time_opts: LocalTimeOpts,
142
143 #[clap(long, default_value = "10000000")]
146 pub max_log_file_size: u64,
147
148 #[clap(long)]
154 pub max_log_files: Option<u32>,
155}
156
157impl LoggingOpts {
158 pub fn local_time(&self, default: bool) -> bool {
159 self.local_time_opts.eval_with_default(default)
160 }
161}
162
163pub struct Logger {
164 pub logging_opts: LoggingOpts,
165 pub local_time_default: bool,
168 pub timestamp_opts: TimestampOpts,
169 pub dir_path: Arc<Path>,
170}
171
172impl Logger {
173 pub fn current_log_path(&self) -> PathBuf {
174 self.dir_path.append("current.log")
175 }
176
177 pub fn rotate_logs(&self) -> anyhow::Result<()> {
181 let mut numbered_logfiles = Vec::new();
182 for entry in std::fs::read_dir(&self.dir_path)? {
183 let entry = entry?;
184 let file_name = entry.file_name();
185 if let Some(file_name) = file_name.to_str() {
186 if let Some((numstr, _)) = file_name.split_once('.') {
187 if let Ok(num) = usize::from_str(numstr) {
188 numbered_logfiles.push((num, entry.path()));
189 }
190 }
191 }
192 }
193 numbered_logfiles.sort_by_key(|(num, _)| *num);
194 let last_number = numbered_logfiles.last().map(|(num, _)| *num).unwrap_or(0);
195 let new_number = last_number + 1;
196 let new_log_path = (&self.dir_path).append(&format!("{new_number:06}.log"));
197 let current_log_path = self.current_log_path();
198 match rename(¤t_log_path, &new_log_path) {
199 Ok(_) => numbered_logfiles.push((new_number, new_log_path)),
200 Err(_) => (), };
202 let num_numbered_logfiles = numbered_logfiles.len();
203 if let Some(max_log_files) = self.logging_opts.max_log_files {
204 let max_log_files = usize::try_from(max_log_files).expect("u32 fits in usize");
205 if num_numbered_logfiles > max_log_files {
206 let delete_n = num_numbered_logfiles - max_log_files;
207 for (_, path) in &numbered_logfiles[0..delete_n] {
208 remove_file(path).with_context(|| anyhow!("deleting log file {path:?}"))?;
209 }
211 }
212 }
213 Ok(())
214 }
215
216 fn run_logger_proxy(&self, logging_r: i32, session_pid: Pid) -> anyhow::Result<()> {
217 let _logging_session_pid = setsid()?;
222
223 let timestamp_formatter = TimestampFormatter {
228 use_rfc3339: self.timestamp_opts.use_rfc3339,
229 local_time: self.logging_opts.local_time(self.local_time_default),
230 };
231
232 let mut messagesfh = BufReader::new(unsafe {
236 File::from_raw_fd(logging_r)
242 });
243
244 let mut input_line = String::new();
245 let mut output_line = Vec::new();
246
247 let mut logfh = open_append(self.current_log_path())?;
248 let mut total_written: u64 = logfh.metadata()?.size();
249 loop {
250 input_line.clear();
251 output_line.clear();
252 let nread = messagesfh.read_line(&mut input_line)?;
253 let daemon_ended = nread == 0;
254 let (starts_with_timestamp, mark_added_timestamps) = match &self.timestamp_opts.mode {
255 TimestampMode::Always => (false, false),
256 TimestampMode::Automatic {
257 mark_added_timestamps,
258 } => (starts_with_timestamp(&input_line), *mark_added_timestamps),
259 TimestampMode::Never => (true, false),
260 };
261 if !starts_with_timestamp {
262 let s = timestamp_formatter.format_systemtime(SystemTime::now());
263 _ = output_line.write_all(s.as_bytes());
264 if mark_added_timestamps {
265 output_line.push(b',');
274 }
275 output_line.push(b'\t');
276 }
277 {
278 let tmp;
279 let rest = if daemon_ended {
280 tmp = format!("daemon {session_pid} ended");
281 &tmp
282 } else {
283 input_line.trim_end()
284 };
285 _ = output_line.write_all(rest.as_bytes());
286 output_line.push(b'\n');
287 }
288
289 logfh.write_all(&output_line)?;
290 total_written += output_line.len() as u64;
291
292 if daemon_ended {
293 break;
294 }
295
296 if total_written >= self.logging_opts.max_log_file_size {
297 logfh.flush()?; drop(logfh);
299 self.rotate_logs()?;
300 logfh = open_append(self.current_log_path())?;
301 total_written = 0;
302 }
303 }
304 logfh.flush()?; Ok(())
306 }
307
308 pub fn redirect_to_logger(self, session_pid: Pid) -> Result<(), DaemonError> {
316 let (logging_r, logging_w) = pipe().map_err(|error| DaemonError::ErrnoError {
318 context: "pipe for logging",
319 error,
320 })?;
321
322 if let Some(_logging_pid) = easy_fork().map_err(|error| DaemonError::ErrnoError {
323 context: "forking the logger",
324 error,
325 })? {
326 close(logging_r).map_err(|error| DaemonError::ErrnoError {
332 context: "daemon: closing logging_r",
333 error,
334 })?;
335 dup2(logging_w, 1).map_err(|error| DaemonError::ErrnoError {
336 context: "daemon: dup to stdout",
337 error,
338 })?;
339 dup2(logging_w, 2).map_err(|error| DaemonError::ErrnoError {
340 context: "daemon: dup to stderr",
341 error,
342 })?;
343 close(logging_w).map_err(|error| DaemonError::ErrnoError {
344 context: "daemon: closing logging_w",
345 error,
346 })?;
347
348 Ok(())
349 } else {
350 close(logging_w).map_err(|error| DaemonError::ErrnoError {
355 context: "logger: closing logging_w",
356 error,
357 })?;
358
359 _ = close(1);
363 _ = close(2);
364
365 match self.run_logger_proxy(logging_r, session_pid) {
366 Ok(()) => (),
367 Err(e) => {
368 _ = writeln!(
371 &mut stderr(),
372 "logger process: ending because of error: {e:#}"
373 );
374 }
375 }
376 std::process::exit(0);
377 }
378 }
379}