use chrono::{DateTime, Utc};
use crate::exit_reason::{describe_exit_code, SIGNAL_NAMES, UNKNOWN_EXIT_CODE};
use crate::isolation::isolation_log::shell_quote;
pub const DOCKER_ZERO_TIME: &str = "0001-01-01T00:00:00Z";
pub const UNKNOWN: &str = UNKNOWN_EXIT_CODE;
pub const NO_ERROR: &str = "(none)";
pub const POST_MORTEM_HEADER: &str = "=== Container post-mortem ===";
pub const DOCKER_STATE_INSPECT_FORMAT: &str =
"{{.State.ExitCode}} {{.State.OOMKilled}} {{.State.StartedAt}} {{.State.FinishedAt}}";
pub mod shell_vars {
pub const EXIT: &str = "__start_command_exit";
pub const OOM: &str = "__start_command_oom";
pub const STARTED: &str = "__start_command_started";
pub const FINISHED: &str = "__start_command_finished";
pub const ERROR: &str = "__start_command_error";
pub const ERROR_TEXT: &str = "__start_command_error_text";
pub const SIGNAL: &str = "__start_command_signal";
pub const EXIT_TEXT: &str = "__start_command_exit_text";
pub const LIFETIME: &str = "__start_command_lifetime";
pub const RUNNING: &str = "__start_command_running";
pub const REMOVED: &str = "__start_command_removed";
}
pub const DOCKER_RUNNING_INSPECT_FORMAT: &str = "{{.State.Running}}";
pub const LOG_CAPTURE_STOPPED_NOTE: &str = concat!(
"Log capture stopped early: docker logs -f returned while the container ",
"was still running (a failed write to this log such as ENOSPC, or a dockerd ",
"restart). Waiting for the container to exit."
);
pub const STILL_RUNNING_NOTE: &str = concat!(
"The watcher stopped observing it before it exited, so it was not removed ",
"and the execution record stays executing."
);
pub fn build_docker_running_condition(quoted_name: &str) -> String {
format!(
"[ \"$(docker inspect -f '{}' {} 2>/dev/null)\" = true ]",
DOCKER_RUNNING_INSPECT_FORMAT, quoted_name
)
}
pub fn build_docker_wait_for_exit_snippet(
container_name: &str,
quoted_log_path: Option<&str>,
) -> String {
let quoted_name = shell_quote(container_name);
let running = build_docker_running_condition("ed_name);
let mut parts = Vec::new();
if let Some(log) = quoted_log_path {
parts.push(format!(
"if {}; then printf '\\n%s\\n' {} >> {} 2>/dev/null; fi",
running,
shell_quote(LOG_CAPTURE_STOPPED_NOTE),
log
));
}
parts.push(format!(
"while {}; do docker wait {} >/dev/null 2>&1 || sleep 1; done",
running, quoted_name
));
parts.join("; ")
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ContainerPostMortem {
pub container_name: String,
pub exit_code: Option<i32>,
pub oom_killed: Option<bool>,
pub started_at: Option<String>,
pub finished_at: Option<String>,
pub error: Option<String>,
}
pub fn normalize_docker_timestamp(value: Option<&str>) -> Option<String> {
let text = value.unwrap_or("").trim();
if text.is_empty()
|| text == DOCKER_ZERO_TIME
|| text == UNKNOWN
|| text == "<no value>"
|| text == NO_ERROR
{
return None;
}
parse_timestamp(text)?;
Some(text.to_string())
}
fn parse_timestamp(text: &str) -> Option<DateTime<Utc>> {
DateTime::parse_from_rfc3339(text)
.map(|value| value.with_timezone(&Utc))
.ok()
}
pub fn format_lifetime(started_at: Option<&str>, finished_at: Option<&str>) -> Option<String> {
let start = parse_timestamp(&normalize_docker_timestamp(started_at)?)?;
let end = parse_timestamp(&normalize_docker_timestamp(finished_at)?)?;
let ms = end.signed_duration_since(start).num_milliseconds();
if ms < 0 {
return None;
}
Some(format!("{}.{:03}s", ms / 1000, ms % 1000))
}
fn optional_bool_text(value: Option<bool>) -> String {
match value {
Some(value) => value.to_string(),
None => UNKNOWN.to_string(),
}
}
pub fn format_container_post_mortem(facts: &ContainerPostMortem) -> String {
let started = normalize_docker_timestamp(facts.started_at.as_deref());
let finished = normalize_docker_timestamp(facts.finished_at.as_deref());
let lifetime = format_lifetime(facts.started_at.as_deref(), facts.finished_at.as_deref());
let error = facts.error.as_deref().unwrap_or("").trim().to_string();
let container = if facts.container_name.is_empty() {
UNKNOWN
} else {
&facts.container_name
};
format!(
"{}\nContainer: {}\nExit Code: {}\nOOMKilled: {}\nStartedAt: {}\nFinishedAt: {}\nLifetime: {}\nError: {}\n",
POST_MORTEM_HEADER,
container,
describe_exit_code(facts.exit_code).text,
optional_bool_text(facts.oom_killed),
started.as_deref().unwrap_or(UNKNOWN),
finished.as_deref().unwrap_or(UNKNOWN),
lifetime.as_deref().unwrap_or(UNKNOWN),
if error.is_empty() { NO_ERROR } else { &error },
)
}
pub fn format_container_removal_note(facts: &ContainerPostMortem) -> String {
let described = describe_exit_code(facts.exit_code);
let signal = described
.signal
.as_deref()
.map(|name| format!(", {}", name))
.unwrap_or_default();
let lifetime = format_lifetime(facts.started_at.as_deref(), facts.finished_at.as_deref())
.unwrap_or_else(|| UNKNOWN.to_string());
let container = if facts.container_name.is_empty() {
UNKNOWN
} else {
&facts.container_name
};
format!(
"Container removed: {} (exit {}{}, lifetime {}, oomKilled={})\n",
container,
described
.code
.map(|code| code.to_string())
.unwrap_or_else(|| UNKNOWN.to_string()),
signal,
lifetime,
optional_bool_text(facts.oom_killed),
)
}
pub fn build_docker_state_snippet(container_name: &str) -> String {
let quoted_name = shell_quote(container_name);
[
format!(
"__start_command_state=$(docker inspect -f '{}' {} 2>/dev/null || printf '%s' '-1 false {} {}')",
DOCKER_STATE_INSPECT_FORMAT, quoted_name, UNKNOWN, UNKNOWN
),
"set -- $__start_command_state".to_string(),
format!("{}=${{1:--1}}", shell_vars::EXIT),
format!("{}=${{2:-false}}", shell_vars::OOM),
format!("{}=${{3:-{}}}", shell_vars::STARTED, UNKNOWN),
format!("{}=${{4:-{}}}", shell_vars::FINISHED, UNKNOWN),
format!(
"{}=$(docker inspect -f '{{{{.State.Error}}}}' {} 2>/dev/null)",
shell_vars::ERROR,
quoted_name
),
format!(
"case \"${}\" in {}|'') {}={};; esac",
shell_vars::STARTED,
DOCKER_ZERO_TIME,
shell_vars::STARTED,
UNKNOWN
),
format!(
"case \"${}\" in {}|'') {}={};; esac",
shell_vars::FINISHED,
DOCKER_ZERO_TIME,
shell_vars::FINISHED,
UNKNOWN
),
format!(
"if [ \"${}\" = {} ] && [ \"${}\" = 0 ]; then {}=-1; fi",
shell_vars::FINISHED,
UNKNOWN,
shell_vars::EXIT,
shell_vars::EXIT
),
format!(
"{}=$(docker inspect -f '{}' {} 2>/dev/null)",
shell_vars::RUNNING,
DOCKER_RUNNING_INSPECT_FORMAT,
quoted_name
),
build_signal_decode_snippet(),
build_lifetime_snippet(),
]
.join("; ")
}
pub fn build_signal_decode_snippet() -> String {
let arms: Vec<String> = SIGNAL_NAMES
.iter()
.map(|(signal, name)| format!("{}) {}={};;", 128 + signal, shell_vars::SIGNAL, name))
.collect();
[
format!("{}=''", shell_vars::SIGNAL),
format!("case \"${}\" in {} esac", shell_vars::EXIT, arms.join(" ")),
format!("{}=\"${}\"", shell_vars::EXIT_TEXT, shell_vars::EXIT),
format!(
"if [ -n \"${}\" ]; then {}=\"${} (${} - 128+$(({} - 128)))\"; fi",
shell_vars::SIGNAL,
shell_vars::EXIT_TEXT,
shell_vars::EXIT,
shell_vars::SIGNAL,
shell_vars::EXIT
),
]
.join("; ")
}
pub fn build_lifetime_snippet() -> String {
[
concat!(
"__start_command_millis() { ",
"case \"$1\" in *.*) __start_command_frac=${1#*.};; ",
"*) printf '000'; return;; esac; ",
"__start_command_frac=${__start_command_frac%%[!0-9]*}; ",
"printf '%.3s' \"${__start_command_frac}000\"; }"
)
.to_string(),
concat!(
"__start_command_epoch_ms() { ",
"__start_command_ts=$(date -u -d \"$1\" +%s%3N 2>/dev/null); ",
"case \"$__start_command_ts\" in ''|*[!0-9]*) ",
"__start_command_ts=$(date -u -j -f '%Y-%m-%dT%H:%M:%S' \"${1%.*}\" +%s 2>/dev/null) && ",
"__start_command_ts=\"${__start_command_ts}$(__start_command_millis \"$1\")\";; esac; ",
"case \"$__start_command_ts\" in ''|*[!0-9]*) __start_command_ts='';; esac; ",
"printf '%s' \"$__start_command_ts\"; }"
)
.to_string(),
format!("{}={}", shell_vars::LIFETIME, UNKNOWN),
format!(
"__start_command_t0=$(__start_command_epoch_ms \"${}\")",
shell_vars::STARTED
),
format!(
"__start_command_t1=$(__start_command_epoch_ms \"${}\")",
shell_vars::FINISHED
),
format!(
concat!(
"if [ -n \"$__start_command_t0\" ] && [ -n \"$__start_command_t1\" ] && ",
"[ \"$__start_command_t1\" -ge \"$__start_command_t0\" ] 2>/dev/null; then ",
"__start_command_ms=$((__start_command_t1 - __start_command_t0)); ",
"{}=\"$((__start_command_ms / 1000)).$(printf '%03d' \"$((__start_command_ms % 1000))\")s\"; fi"
),
shell_vars::LIFETIME
),
]
.join("; ")
}
pub fn build_docker_post_mortem_snippet(container_name: &str, quoted_log_path: &str) -> String {
format!(
concat!(
"{error_text}=\"${error}\"; ",
"if [ -z \"${error_text}\" ]; then {error_text}='{none}'; fi; ",
"printf '\\n{header}\\nContainer: %s\\nExit Code: %s\\n",
"OOMKilled: %s\\nStartedAt: %s\\nFinishedAt: %s\\nLifetime: %s\\nError: %s\\n' ",
"{name} \"${exit_text}\" \"${oom}\" \"${started}\" \"${finished}\" ",
"\"${lifetime}\" \"${error_text}\" >> {log}"
),
error_text = shell_vars::ERROR_TEXT,
error = shell_vars::ERROR,
none = NO_ERROR,
header = POST_MORTEM_HEADER,
name = shell_quote(container_name),
exit_text = shell_vars::EXIT_TEXT,
oom = shell_vars::OOM,
started = shell_vars::STARTED,
finished = shell_vars::FINISHED,
lifetime = shell_vars::LIFETIME,
log = quoted_log_path,
)
}
pub fn build_docker_still_running_note_snippet(
container_name: &str,
quoted_log_path: &str,
) -> String {
let quoted_name = shell_quote(container_name);
format!(
"printf '\\nContainer still running: %s\\n%s\\nCheck it with: docker inspect %s\\n' {} {} {} >> {}",
quoted_name,
shell_quote(STILL_RUNNING_NOTE),
quoted_name,
quoted_log_path
)
}
pub fn build_docker_removal_note_snippet(container_name: &str, quoted_log_path: &str) -> String {
format!(
concat!(
"__start_command_signal_note=''; ",
"if [ -n \"${signal}\" ]; then __start_command_signal_note=\", ${signal}\"; fi; ",
"printf '\\nContainer removed: %s (exit %s%s, lifetime %s, oomKilled=%s)\\n' ",
"{name} \"${exit}\" \"$__start_command_signal_note\" \"${lifetime}\" \"${oom}\" >> {log}"
),
signal = shell_vars::SIGNAL,
name = shell_quote(container_name),
exit = shell_vars::EXIT,
lifetime = shell_vars::LIFETIME,
oom = shell_vars::OOM,
log = quoted_log_path,
)
}