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