1use chrono::{DateTime, Utc};
24
25use crate::exit_reason::{describe_exit_code, SIGNAL_NAMES, UNKNOWN_EXIT_CODE};
26use crate::isolation::isolation_log::shell_quote;
27
28pub const DOCKER_ZERO_TIME: &str = "0001-01-01T00:00:00Z";
33
34pub const UNKNOWN: &str = UNKNOWN_EXIT_CODE;
36
37pub const NO_ERROR: &str = "(none)";
39
40pub const POST_MORTEM_HEADER: &str = "=== Container post-mortem ===";
42
43pub const DOCKER_STATE_INSPECT_FORMAT: &str =
45 "{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.StartedAt}} {{.State.FinishedAt}}";
46
47pub mod shell_vars {
49 pub const EXIT: &str = "__start_command_exit";
50 pub const OOM: &str = "__start_command_oom";
51 pub const STARTED: &str = "__start_command_started";
52 pub const FINISHED: &str = "__start_command_finished";
53 pub const ERROR: &str = "__start_command_error";
54 pub const ERROR_TEXT: &str = "__start_command_error_text";
55 pub const SIGNAL: &str = "__start_command_signal";
56 pub const EXIT_TEXT: &str = "__start_command_exit_text";
57 pub const LIFETIME: &str = "__start_command_lifetime";
58 pub const RUNNING: &str = "__start_command_running";
59 pub const REMOVED: &str = "__start_command_removed";
61}
62
63pub const DOCKER_RUNNING_INSPECT_FORMAT: &str = "{{.State.Running}}";
65
66pub const LOG_CAPTURE_STOPPED_NOTE: &str = concat!(
69 "Log capture stopped early: docker logs -f returned while the container ",
70 "was still running (a failed write to this log such as ENOSPC, or a dockerd ",
71 "restart). Waiting for the container to exit."
72);
73
74pub const STILL_RUNNING_NOTE: &str = concat!(
77 "The watcher stopped observing it before it exited, so it was not removed ",
78 "and the execution record stays executing."
79);
80
81pub fn build_docker_running_condition(quoted_name: &str) -> String {
83 format!(
84 "[ \"$(docker inspect -f '{}' {} 2>/dev/null)\" = true ]",
85 DOCKER_RUNNING_INSPECT_FORMAT, quoted_name
86 )
87}
88
89pub fn build_docker_wait_for_exit_snippet(
105 container_name: &str,
106 quoted_log_path: Option<&str>,
107) -> String {
108 let quoted_name = shell_quote(container_name);
109 let running = build_docker_running_condition("ed_name);
110 let mut parts = Vec::new();
111 if let Some(log) = quoted_log_path {
112 parts.push(format!(
113 "if {}; then printf '\\n%s\\n' {} >> {} 2>/dev/null; fi",
114 running,
115 shell_quote(LOG_CAPTURE_STOPPED_NOTE),
116 log
117 ));
118 }
119 parts.push(format!(
120 "while {}; do docker wait {} >/dev/null 2>&1 || sleep 1; done",
121 running, quoted_name
122 ));
123 parts.join("; ")
124}
125
126#[derive(Debug, Clone, Default, PartialEq, Eq)]
128pub struct ContainerPostMortem {
129 pub container_name: String,
130 pub exit_code: Option<i32>,
131 pub oom_killed: Option<bool>,
132 pub started_at: Option<String>,
133 pub finished_at: Option<String>,
134 pub error: Option<String>,
135}
136
137pub fn normalize_docker_timestamp(value: Option<&str>) -> Option<String> {
143 let text = value.unwrap_or("").trim();
144 if text.is_empty()
145 || text == DOCKER_ZERO_TIME
146 || text == UNKNOWN
147 || text == "<no value>"
148 || text == NO_ERROR
149 {
150 return None;
151 }
152 parse_timestamp(text)?;
153 Some(text.to_string())
154}
155
156fn parse_timestamp(text: &str) -> Option<DateTime<Utc>> {
157 DateTime::parse_from_rfc3339(text)
158 .map(|value| value.with_timezone(&Utc))
159 .ok()
160}
161
162pub fn format_lifetime(started_at: Option<&str>, finished_at: Option<&str>) -> Option<String> {
164 let start = parse_timestamp(&normalize_docker_timestamp(started_at)?)?;
165 let end = parse_timestamp(&normalize_docker_timestamp(finished_at)?)?;
166 let ms = end.signed_duration_since(start).num_milliseconds();
167 if ms < 0 {
168 return None;
169 }
170 Some(format!("{}.{:03}s", ms / 1000, ms % 1000))
171}
172
173fn optional_bool_text(value: Option<bool>) -> String {
174 match value {
175 Some(value) => value.to_string(),
176 None => UNKNOWN.to_string(),
177 }
178}
179
180pub fn format_container_post_mortem(facts: &ContainerPostMortem) -> String {
182 let started = normalize_docker_timestamp(facts.started_at.as_deref());
183 let finished = normalize_docker_timestamp(facts.finished_at.as_deref());
184 let lifetime = format_lifetime(facts.started_at.as_deref(), facts.finished_at.as_deref());
185 let error = facts.error.as_deref().unwrap_or("").trim().to_string();
186 let container = if facts.container_name.is_empty() {
187 UNKNOWN
188 } else {
189 &facts.container_name
190 };
191 format!(
192 "{}\nContainer: {}\nExit Code: {}\nOOMKilled: {}\nStartedAt: {}\nFinishedAt: {}\nLifetime: {}\nError: {}\n",
193 POST_MORTEM_HEADER,
194 container,
195 describe_exit_code(facts.exit_code).text,
196 optional_bool_text(facts.oom_killed),
197 started.as_deref().unwrap_or(UNKNOWN),
198 finished.as_deref().unwrap_or(UNKNOWN),
199 lifetime.as_deref().unwrap_or(UNKNOWN),
200 if error.is_empty() { NO_ERROR } else { &error },
201 )
202}
203
204pub fn format_container_removal_note(facts: &ContainerPostMortem) -> String {
207 let described = describe_exit_code(facts.exit_code);
208 let signal = described
209 .signal
210 .as_deref()
211 .map(|name| format!(", {}", name))
212 .unwrap_or_default();
213 let lifetime = format_lifetime(facts.started_at.as_deref(), facts.finished_at.as_deref())
214 .unwrap_or_else(|| UNKNOWN.to_string());
215 let container = if facts.container_name.is_empty() {
216 UNKNOWN
217 } else {
218 &facts.container_name
219 };
220 format!(
221 "Container removed: {} (exit {}{}, lifetime {}, oomKilled={})\n",
222 container,
223 described
224 .code
225 .map(|code| code.to_string())
226 .unwrap_or_else(|| UNKNOWN.to_string()),
227 signal,
228 lifetime,
229 optional_bool_text(facts.oom_killed),
230 )
231}
232
233pub fn build_docker_state_snippet(container_name: &str) -> String {
244 let quoted_name = shell_quote(container_name);
245 [
246 format!(
247 "__start_command_state=$(docker inspect -f '{}' {} 2>/dev/null || printf '%s' '-1 false {} {}')",
248 DOCKER_STATE_INSPECT_FORMAT, quoted_name, UNKNOWN, UNKNOWN
249 ),
250 "set -- $__start_command_state".to_string(),
251 format!("{}=${{1:--1}}", shell_vars::EXIT),
252 format!("{}=${{2:-false}}", shell_vars::OOM),
253 format!("{}=${{3:-{}}}", shell_vars::STARTED, UNKNOWN),
254 format!("{}=${{4:-{}}}", shell_vars::FINISHED, UNKNOWN),
255 format!(
256 "{}=$(docker inspect -f '{{{{.State.Error}}}}' {} 2>/dev/null)",
257 shell_vars::ERROR,
258 quoted_name
259 ),
260 format!(
261 "case \"${}\" in {}|'') {}={};; esac",
262 shell_vars::STARTED,
263 DOCKER_ZERO_TIME,
264 shell_vars::STARTED,
265 UNKNOWN
266 ),
267 format!(
268 "case \"${}\" in {}|'') {}={};; esac",
269 shell_vars::FINISHED,
270 DOCKER_ZERO_TIME,
271 shell_vars::FINISHED,
272 UNKNOWN
273 ),
274 format!(
277 "if [ \"${}\" = {} ] && [ \"${}\" = 0 ]; then {}=-1; fi",
278 shell_vars::FINISHED,
279 UNKNOWN,
280 shell_vars::EXIT,
281 shell_vars::EXIT
282 ),
283 format!(
284 "{}=$(docker inspect -f '{}' {} 2>/dev/null)",
285 shell_vars::RUNNING,
286 DOCKER_RUNNING_INSPECT_FORMAT,
287 quoted_name
288 ),
289 build_signal_decode_snippet(),
290 build_lifetime_snippet(),
291 ]
292 .join("; ")
293}
294
295pub fn build_signal_decode_snippet() -> String {
301 let arms: Vec<String> = SIGNAL_NAMES
302 .iter()
303 .map(|(signal, name)| format!("{}) {}={};;", 128 + signal, shell_vars::SIGNAL, name))
304 .collect();
305 [
306 format!("{}=''", shell_vars::SIGNAL),
307 format!("case \"${}\" in {} esac", shell_vars::EXIT, arms.join(" ")),
308 format!("{}=\"${}\"", shell_vars::EXIT_TEXT, shell_vars::EXIT),
309 format!(
310 "if [ -n \"${}\" ]; then {}=\"${} (${} - 128+$(({} - 128)))\"; fi",
311 shell_vars::SIGNAL,
312 shell_vars::EXIT_TEXT,
313 shell_vars::EXIT,
314 shell_vars::SIGNAL,
315 shell_vars::EXIT
316 ),
317 ]
318 .join("; ")
319}
320
321pub fn build_lifetime_snippet() -> String {
333 [
334 concat!(
335 "__start_command_millis() { ",
336 "case \"$1\" in *.*) __start_command_frac=${1#*.};; ",
337 "*) printf '000'; return;; esac; ",
338 "__start_command_frac=${__start_command_frac%%[!0-9]*}; ",
339 "printf '%.3s' \"${__start_command_frac}000\"; }"
340 )
341 .to_string(),
342 concat!(
343 "__start_command_epoch_ms() { ",
344 "__start_command_ts=$(date -u -d \"$1\" +%s%3N 2>/dev/null); ",
345 "case \"$__start_command_ts\" in ''|*[!0-9]*) ",
346 "__start_command_ts=$(date -u -j -f '%Y-%m-%dT%H:%M:%S' \"${1%.*}\" +%s 2>/dev/null) && ",
347 "__start_command_ts=\"${__start_command_ts}$(__start_command_millis \"$1\")\";; esac; ",
348 "case \"$__start_command_ts\" in ''|*[!0-9]*) __start_command_ts='';; esac; ",
349 "printf '%s' \"$__start_command_ts\"; }"
350 )
351 .to_string(),
352 format!("{}={}", shell_vars::LIFETIME, UNKNOWN),
353 format!(
354 "__start_command_t0=$(__start_command_epoch_ms \"${}\")",
355 shell_vars::STARTED
356 ),
357 format!(
358 "__start_command_t1=$(__start_command_epoch_ms \"${}\")",
359 shell_vars::FINISHED
360 ),
361 format!(
362 concat!(
363 "if [ -n \"$__start_command_t0\" ] && [ -n \"$__start_command_t1\" ] && ",
364 "[ \"$__start_command_t1\" -ge \"$__start_command_t0\" ] 2>/dev/null; then ",
365 "__start_command_ms=$((__start_command_t1 - __start_command_t0)); ",
366 "{}=\"$((__start_command_ms / 1000)).$(printf '%03d' \"$((__start_command_ms % 1000))\")s\"; fi"
367 ),
368 shell_vars::LIFETIME
369 ),
370 ]
371 .join("; ")
372}
373
374pub fn build_docker_post_mortem_snippet(container_name: &str, quoted_log_path: &str) -> String {
376 format!(
377 concat!(
378 "{error_text}=\"${error}\"; ",
379 "if [ -z \"${error_text}\" ]; then {error_text}='{none}'; fi; ",
380 "printf '\\n{header}\\nContainer: %s\\nExit Code: %s\\n",
381 "OOMKilled: %s\\nStartedAt: %s\\nFinishedAt: %s\\nLifetime: %s\\nError: %s\\n' ",
382 "{name} \"${exit_text}\" \"${oom}\" \"${started}\" \"${finished}\" ",
383 "\"${lifetime}\" \"${error_text}\" >> {log}"
384 ),
385 error_text = shell_vars::ERROR_TEXT,
386 error = shell_vars::ERROR,
387 none = NO_ERROR,
388 header = POST_MORTEM_HEADER,
389 name = shell_quote(container_name),
390 exit_text = shell_vars::EXIT_TEXT,
391 oom = shell_vars::OOM,
392 started = shell_vars::STARTED,
393 finished = shell_vars::FINISHED,
394 lifetime = shell_vars::LIFETIME,
395 log = quoted_log_path,
396 )
397}
398
399pub fn build_docker_still_running_note_snippet(
402 container_name: &str,
403 quoted_log_path: &str,
404) -> String {
405 let quoted_name = shell_quote(container_name);
406 format!(
407 "printf '\\nContainer still running: %s\\n%s\\nCheck it with: docker inspect %s\\n' {} {} {} >> {}",
408 quoted_name,
409 shell_quote(STILL_RUNNING_NOTE),
410 quoted_name,
411 quoted_log_path
412 )
413}
414
415pub fn build_docker_removal_note_snippet(container_name: &str, quoted_log_path: &str) -> String {
417 format!(
418 concat!(
419 "__start_command_signal_note=''; ",
420 "if [ -n \"${signal}\" ]; then __start_command_signal_note=\", ${signal}\"; fi; ",
421 "printf '\\nContainer removed: %s (exit %s%s, lifetime %s, oomKilled=%s)\\n' ",
422 "{name} \"${exit}\" \"$__start_command_signal_note\" \"${lifetime}\" \"${oom}\" >> {log}"
423 ),
424 signal = shell_vars::SIGNAL,
425 name = shell_quote(container_name),
426 exit = shell_vars::EXIT,
427 lifetime = shell_vars::LIFETIME,
428 oom = shell_vars::OOM,
429 log = quoted_log_path,
430 )
431}