start_command/
status_probe.rs1use crate::detached_finalize::{
10 END_TIME_SOURCE_DOCKER_FINISHED_AT, END_TIME_SOURCE_LOG_FOOTER, END_TIME_SOURCE_OBSERVED_AT,
11};
12use crate::docker_cleanup::docker_command;
13use crate::docker_post_mortem::normalize_docker_timestamp;
14use crate::execution_store::ExecutionRecord;
15use crate::status_footer::LogFooter;
16use chrono::Utc;
17use std::process::Command;
18
19#[derive(Clone, Default)]
21pub struct DockerState {
22 pub running: bool,
23 exit_code: Option<i32>,
24 oom_killed: Option<bool>,
25 started_at: Option<String>,
27 finished_at: Option<String>,
31}
32
33pub(crate) fn inspect_docker_state(session_name: &str) -> Option<DockerState> {
45 let output = Command::new(docker_command())
46 .args([
47 "inspect",
48 "-f",
49 concat!(
50 "{{.State.Running}} {{.State.ExitCode}} {{.State.OOMKilled}} ",
51 "{{.State.StartedAt}} {{.State.FinishedAt}}"
52 ),
53 session_name,
54 ])
55 .output()
56 .ok()?;
57 if !output.status.success() {
58 return None;
59 }
60 let stdout = String::from_utf8_lossy(&output.stdout);
61 let trimmed = stdout.trim();
62 if trimmed.is_empty() {
63 return None;
64 }
65 let mut parts = trimmed.split_whitespace();
66 let running = parts.next() == Some("true");
67 let exit_code = parts.next().and_then(|value| value.parse::<i32>().ok());
68 let oom_killed = parts.next().and_then(|value| match value {
69 "true" => Some(true),
70 "false" => Some(false),
71 _ => None,
72 });
73 let started_at = normalize_docker_timestamp(parts.next());
74 let finished_at = normalize_docker_timestamp(parts.next());
75 Some(DockerState {
76 running,
77 exit_code,
78 oom_killed,
79 started_at,
80 finished_at,
81 })
82}
83
84pub(crate) fn is_detached_docker_record(record: &ExecutionRecord) -> bool {
85 record.options.get("isolated").and_then(|v| v.as_str()) == Some("docker")
86 && record.options.get("isolationMode").and_then(|v| v.as_str()) == Some("detached")
87 && record
88 .options
89 .get("sessionName")
90 .and_then(|v| v.as_str())
91 .is_some()
92}
93
94pub(crate) fn read_docker_state(record: &ExecutionRecord) -> Option<DockerState> {
95 if record.options.get("isolated")?.as_str()? != "docker" {
96 return None;
97 }
98 let session_name = record.options.get("sessionName")?.as_str()?;
99 let state = inspect_docker_state(session_name)?;
100 if let Some(attempt) = &record.attempt {
101 let older = state
102 .started_at
103 .as_deref()
104 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
105 .zip(chrono::DateTime::parse_from_rfc3339(&attempt.started_at).ok())
106 .is_some_and(|(started, since)| started < since);
107 if older {
108 return None;
109 }
110 }
111 Some(state)
112}
113
114pub(crate) fn backend_exit_code(docker_state: Option<&DockerState>) -> Option<i32> {
121 let state = docker_state?;
122 if state.running {
123 None
124 } else {
125 state.exit_code
126 }
127}
128
129pub(crate) fn resolve_oom_observation(
138 record: &ExecutionRecord,
139 docker_state: Option<&DockerState>,
140) -> Option<bool> {
141 let inspected = docker_state.and_then(|state| state.oom_killed);
142 if record.oom_killed == Some(true) || inspected == Some(true) {
143 return Some(true);
144 }
145 if record.oom_killed == Some(false) || inspected == Some(false) {
146 return Some(false);
147 }
148 None
149}
150
151pub fn is_detached_session_alive(record: &ExecutionRecord) -> Option<bool> {
154 let session_name = record.options.get("sessionName")?.as_str()?;
155 let isolation_mode = record.options.get("isolationMode")?.as_str()?;
156 let isolated = record.options.get("isolated")?.as_str()?;
157
158 if isolation_mode != "detached" {
159 return None;
160 }
161
162 match isolated {
163 "screen" => {
164 let output = Command::new("screen").args(["-ls"]).output().ok()?;
165 let stdout = String::from_utf8_lossy(&output.stdout);
166 Some(stdout.contains(session_name))
167 }
168 "tmux" => {
169 let status = Command::new("tmux")
170 .args(["has-session", "-t", session_name])
171 .output()
172 .ok()?;
173 Some(status.status.success())
174 }
175 "docker" => {
176 inspect_docker_state(session_name).map(|state| state.running)
181 }
182 "ssh" => {
183 #[cfg(unix)]
185 {
186 if let Some(pid) = record.pid {
187 let result = unsafe { libc::kill(pid as i32, 0) };
188 Some(result == 0)
189 } else {
190 None
191 }
192 }
193 #[cfg(not(unix))]
194 {
195 let _ = record.pid;
196 None
197 }
198 }
199 _ => None,
200 }
201}
202
203pub(crate) fn apply_end_time(
211 record: &mut ExecutionRecord,
212 docker_state: Option<&DockerState>,
213 footer: &LogFooter,
214) {
215 if let Some(started_at) = docker_state.and_then(|state| state.started_at.clone()) {
216 record.container_started_at = Some(started_at);
217 }
218 if record.end_time.is_some() {
219 return;
220 }
221 if let Some(finished_at) = docker_state.and_then(|state| state.finished_at.clone()) {
222 record.end_time = Some(finished_at);
223 record.end_time_source = Some(END_TIME_SOURCE_DOCKER_FINISHED_AT.to_string());
224 return;
225 }
226 if let Some(finished_at) = footer.finished_at.clone() {
227 record.end_time = Some(finished_at);
228 record.end_time_source = Some(END_TIME_SOURCE_LOG_FOOTER.to_string());
229 return;
230 }
231 let now = Utc::now().to_rfc3339();
232 record.end_time = Some(now.clone());
233 record.end_time_source = Some(END_TIME_SOURCE_OBSERVED_AT.to_string());
234 record.observed_at = Some(now);
235}