Skip to main content

start_command/
status_footer.rs

1//! The anchored terminal footer `start` writes at the end of every log.
2//!
3//! ```text
4//! ==================================================
5//! Finished: 2026-07-30 23:36:20.295
6//! Exit Code: 0
7//! ```
8//!
9//! Matching the whole block at line starts means a bare `Exit Code: N`
10//! substring emitted by the wrapped command (inside a JSON payload, a quoted
11//! log excerpt, an `rg` dump, ...) can no longer be mistaken for the footer
12//! (issue #150).
13//!
14//! The `Finished:` line was previously parsed by nobody: `--status` read the
15//! exit code out of this block and then stamped `end_time` with the current
16//! time, even though the real finish time was sitting one line above the number
17//! it had just trusted (issue #170.2).
18
19use chrono::{NaiveDateTime, TimeZone, Utc};
20
21use crate::isolation::isolation_log::{read_log_tail, LOG_TAIL_BYTES};
22
23/// The two facts the terminal footer carries.
24#[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
34/// Parse the `Finished:` value both footer writers produce.
35///
36/// `create_log_footer()` writes an ISO timestamp and the shell snippet writes
37/// `date -u '+%Y-%m-%d %H:%M:%S.%3N'`; both are UTC. Anything else — a test
38/// fixture, a truncated line, a localized date — yields `None` so the caller
39/// falls back instead of inventing a finish time.
40pub 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
52/// Parse the last complete footer block out of a log tail.
53///
54/// Returns None fields rather than numbers parsed out of the command's own
55/// output: an absent footer is reported as absent.
56pub 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
83/// Read the terminal footer from the end of a log file.
84pub 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}