start_command/
status_footer.rs1use chrono::{NaiveDateTime, TimeZone, Utc};
20
21use crate::isolation::isolation_log::{read_log_tail, LOG_TAIL_BYTES};
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct LogFooter {
26 pub exit_code: Option<i32>,
27 pub finished_at: Option<String>,
28}
29
30fn is_footer_separator_line(line: &str) -> bool {
31 line.len() >= 10 && line.chars().all(|c| c == '=')
32}
33
34pub fn parse_footer_timestamp(value: &str) -> Option<String> {
41 let text = value.trim().trim_end_matches('Z');
42 if text.is_empty() {
43 return None;
44 }
45 let normalized = text.replacen('T', " ", 1);
46 let naive = NaiveDateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S%.f")
47 .or_else(|_| NaiveDateTime::parse_from_str(&normalized, "%Y-%m-%d %H:%M:%S"))
48 .ok()?;
49 Some(Utc.from_utc_datetime(&naive).to_rfc3339())
50}
51
52pub fn parse_footer_from_tail(tail: &str) -> LogFooter {
57 let lines: Vec<&str> = tail
58 .lines()
59 .map(|line| line.trim_end_matches('\r'))
60 .collect();
61 for index in (2..lines.len()).rev() {
62 let value = match lines[index].strip_prefix("Exit Code:") {
63 Some(value) => value,
64 None => continue,
65 };
66 let finished = match lines[index - 1].strip_prefix("Finished:") {
67 Some(finished) => finished,
68 None => continue,
69 };
70 if !is_footer_separator_line(lines[index - 2]) {
71 continue;
72 }
73 if let Ok(code) = value.trim().parse::<i32>() {
74 return LogFooter {
75 exit_code: Some(code),
76 finished_at: parse_footer_timestamp(finished),
77 };
78 }
79 }
80 LogFooter::default()
81}
82
83pub fn read_footer_from_log(log_path: &str) -> LogFooter {
85 match read_log_tail(log_path, LOG_TAIL_BYTES) {
86 Some(tail) => parse_footer_from_tail(&tail),
87 None => LogFooter::default(),
88 }
89}