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_attempt::read_attempt_log_tail;
15use crate::execution_store::{ExecutionRecord, ExecutionStatus};
16use crate::isolation::isolation_log::FATAL_MARKER_TAIL_BYTES;
17use crate::status_footer::parse_footer_from_tail;
18use crate::status_footer::LogFooter;
19use chrono::Utc;
20use std::process::Command;
21
22#[derive(Clone, Default)]
24pub struct DockerState {
25 pub running: bool,
26 exit_code: Option<i32>,
27 oom_killed: Option<bool>,
28 started_at: Option<String>,
30 finished_at: Option<String>,
34}
35
36impl DockerState {
37 pub fn lost_watcher_evidence(&self, record: &ExecutionRecord) -> Option<serde_json::Value> {
40 if self.running || self.exit_code != Some(137) {
41 return None;
42 }
43 crate::exit_evidence::collect_lost_watcher_evidence(
44 record,
45 self.finished_at.as_deref()?,
46 self.oom_killed,
47 &crate::execution_control::SystemCommandRunner,
48 )
49 }
50}
51
52pub(crate) fn inspect_docker_state(session_name: &str) -> Option<DockerState> {
64 let format = concat!(
65 "{{.State.Running}} {{.State.ExitCode}} {{.State.OOMKilled}} ",
66 "{{.State.StartedAt}} {{.State.FinishedAt}}"
67 );
68 let stdout = match crate::docker_inspect_batch::prefetched_inspect(format, session_name) {
70 Some(answer) if answer.success => answer.stdout,
71 Some(_) => return None,
72 None => {
73 let output = Command::new(docker_command())
74 .args(["inspect", "-f", format, session_name])
75 .output()
76 .ok()?;
77 if !output.status.success() {
78 return None;
79 }
80 String::from_utf8_lossy(&output.stdout).to_string()
81 }
82 };
83 let trimmed = stdout.trim();
84 if trimmed.is_empty() {
85 return None;
86 }
87 let mut parts = trimmed.split_whitespace();
88 let running = parts.next() == Some("true");
89 let exit_code = parts.next().and_then(|value| value.parse::<i32>().ok());
90 let oom_killed = parts.next().and_then(|value| match value {
91 "true" => Some(true),
92 "false" => Some(false),
93 _ => None,
94 });
95 let started_at = normalize_docker_timestamp(parts.next());
96 let finished_at = normalize_docker_timestamp(parts.next());
97 Some(DockerState {
98 running,
99 exit_code,
100 oom_killed,
101 started_at,
102 finished_at,
103 })
104}
105
106pub(crate) fn is_detached_docker_record(record: &ExecutionRecord) -> bool {
107 record.options.get("isolated").and_then(|v| v.as_str()) == Some("docker")
108 && record.options.get("isolationMode").and_then(|v| v.as_str()) == Some("detached")
109 && record
110 .options
111 .get("sessionName")
112 .and_then(|v| v.as_str())
113 .is_some()
114}
115
116pub(crate) fn read_docker_state(record: &ExecutionRecord) -> Option<DockerState> {
117 if record.options.get("isolated")?.as_str()? != "docker" {
118 return None;
119 }
120 let session_name = record.options.get("sessionName")?.as_str()?;
121 if record.container_state.as_deref() == Some(crate::container_removal::CONTAINER_STATE_REMOVED)
122 {
123 return None;
126 }
127 let state = inspect_docker_state(session_name)?;
128 if let Some(attempt) = &record.attempt {
129 let older = state
130 .started_at
131 .as_deref()
132 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
133 .zip(chrono::DateTime::parse_from_rfc3339(&attempt.started_at).ok())
134 .is_some_and(|(started, since)| started < since);
135 if older {
136 return None;
137 }
138 }
139 Some(state)
140}
141
142pub(crate) fn backend_exit_code(docker_state: Option<&DockerState>) -> Option<i32> {
149 let state = docker_state?;
150 if state.running {
151 None
152 } else {
153 state.exit_code
154 }
155}
156
157pub(crate) fn resolve_oom_observation(
166 record: &ExecutionRecord,
167 docker_state: Option<&DockerState>,
168) -> Option<bool> {
169 let inspected = docker_state.and_then(|state| state.oom_killed);
170 if record.oom_killed == Some(true) || inspected == Some(true) {
171 return Some(true);
172 }
173 if record.oom_killed == Some(false) || inspected == Some(false) {
174 return Some(false);
175 }
176 None
177}
178
179pub fn is_detached_session_alive(record: &ExecutionRecord) -> Option<bool> {
182 let session_name = record.options.get("sessionName")?.as_str()?;
183 let isolation_mode = record.options.get("isolationMode")?.as_str()?;
184 let isolated = record.options.get("isolated")?.as_str()?;
185
186 if isolation_mode != "detached" {
187 return None;
188 }
189
190 match isolated {
191 "screen" => {
192 let output = Command::new("screen").args(["-ls"]).output().ok()?;
193 let stdout = String::from_utf8_lossy(&output.stdout);
194 Some(stdout.contains(session_name))
195 }
196 "tmux" => {
197 let status = Command::new("tmux")
198 .args(["has-session", "-t", session_name])
199 .output()
200 .ok()?;
201 Some(status.status.success())
202 }
203 "docker" => {
204 inspect_docker_state(session_name).map(|state| state.running)
209 }
210 "ssh" => {
211 #[cfg(unix)]
213 {
214 if let Some(pid) = record.pid {
215 let result = unsafe { libc::kill(pid as i32, 0) };
216 Some(result == 0)
217 } else {
218 None
219 }
220 }
221 #[cfg(not(unix))]
222 {
223 let _ = record.pid;
224 None
225 }
226 }
227 _ => None,
228 }
229}
230
231pub(crate) fn apply_end_time(
239 record: &mut ExecutionRecord,
240 docker_state: Option<&DockerState>,
241 footer: &LogFooter,
242) {
243 if let Some(started_at) = docker_state.and_then(|state| state.started_at.clone()) {
244 record.container_started_at = Some(started_at);
245 }
246 if record.end_time.is_some() {
247 return;
248 }
249 if let Some(finished_at) = docker_state.and_then(|state| state.finished_at.clone()) {
250 record.end_time = Some(finished_at);
251 record.end_time_source = Some(END_TIME_SOURCE_DOCKER_FINISHED_AT.to_string());
252 return;
253 }
254 if let Some(finished_at) = footer.finished_at.clone() {
255 record.end_time = Some(finished_at);
256 record.end_time_source = Some(END_TIME_SOURCE_LOG_FOOTER.to_string());
257 return;
258 }
259 let now = Utc::now().to_rfc3339();
260 record.end_time = Some(now.clone());
261 record.end_time_source = Some(END_TIME_SOURCE_OBSERVED_AT.to_string());
262 record.observed_at = Some(now);
263}
264
265pub(crate) fn resolve_detached_status(record: &ExecutionRecord) -> ExecutionRecord {
270 if crate::launch_owner::has_active_launch(record) {
271 return record.clone();
272 }
273 let tail = read_attempt_log_tail(record, FATAL_MARKER_TAIL_BYTES);
274 let footer = parse_footer_from_tail(tail.as_deref().unwrap_or(""));
275 let footer_exit = footer.exit_code;
276 let is_detached_docker = is_detached_docker_record(record);
277 let docker_state = if is_detached_docker {
278 read_docker_state(record)
279 } else {
280 None
281 };
282
283 let oom_killed = resolve_oom_observation(record, docker_state.as_ref());
285 let clone_record = || {
286 let mut enriched = record.clone();
287 if oom_killed.is_some() {
288 enriched.oom_killed = oom_killed;
289 }
290 enriched
291 };
292
293 let alive = if is_detached_docker {
294 docker_state.as_ref().map(|state| state.running)
295 } else {
296 is_detached_session_alive(record)
297 };
298
299 let alive = match alive {
300 Some(value) => value,
301 None => {
302 let is_detached =
310 record.options.get("isolationMode").and_then(|v| v.as_str()) == Some("detached");
311 if is_detached && record.status == ExecutionStatus::Executing {
312 if footer_exit.is_some() {
313 let mut enriched = clone_record();
314 enriched.status = ExecutionStatus::Executed;
315 enriched.exit_code = footer_exit;
316 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
317 return enriched;
318 }
319 if oom_killed == Some(true) {
320 let mut enriched = clone_record();
327 enriched.status = ExecutionStatus::Executed;
328 enriched.exit_code = Some(137);
329 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
330 return enriched;
331 }
332 }
333 return clone_record();
334 }
335 };
336
337 let mut enriched = clone_record();
338
339 if alive && enriched.status == ExecutionStatus::Executed {
340 if enriched.exit_code.is_none() && footer_exit.is_none() {
347 enriched.status = ExecutionStatus::Executing;
349 enriched.exit_code = None;
350 enriched.end_time = None;
351 enriched.end_time_source = None;
352 }
353 } else if !alive && enriched.status == ExecutionStatus::Executing {
355 enriched.status = ExecutionStatus::Executed;
363 if enriched.exit_code.is_none() {
364 enriched.exit_code = Some(
365 backend_exit_code(docker_state.as_ref())
366 .or(footer_exit)
367 .or(if oom_killed == Some(true) {
368 Some(137)
369 } else {
370 None
371 })
372 .unwrap_or(-1),
373 );
374 }
375 apply_end_time(&mut enriched, docker_state.as_ref(), &footer);
376 crate::exit_evidence::add_lost_watcher_evidence(
377 &mut enriched,
378 record,
379 docker_state.as_ref(),
380 tail.as_deref(),
381 );
382 }
383
384 enriched
385}