start_command/
container_removal.rs1use std::process::Command;
14
15use chrono::Utc;
16
17use crate::docker_cleanup::docker_command;
18use crate::execution_store::{ExecutionRecord, ExecutionStatus, ExecutionStore};
19
20pub const CONTAINER_STATE_REMOVED: &str = "removed";
22
23pub const CONTAINER_REMOVED_ENV: &str = "START_INTERNAL_CONTAINER_REMOVED";
26
27pub fn is_docker_not_found(success: bool, stderr: &str) -> bool {
33 if success {
34 return false;
35 }
36 let stderr = stderr.to_ascii_lowercase();
37 stderr.contains("no such object") || stderr.contains("no such container")
38}
39
40fn docker_object_missing(target: &str) -> bool {
42 if let Some(answer) = crate::docker_inspect_batch::prefetched_inspect("{{.Id}}", target) {
43 return is_docker_not_found(answer.success, &answer.stderr);
44 }
45 Command::new(docker_command())
46 .args(["inspect", "-f", "{{.Id}}", target])
47 .output()
48 .is_ok_and(|output| {
49 is_docker_not_found(
50 output.status.success(),
51 &String::from_utf8_lossy(&output.stderr),
52 )
53 })
54}
55
56pub fn mark_container_removed(record: &mut ExecutionRecord, observed_at: Option<&str>) {
59 record.container_state = Some(CONTAINER_STATE_REMOVED.to_string());
60 if record.container_removed_observed_at.is_none() {
61 record.container_removed_observed_at = Some(
62 observed_at
63 .map(str::to_string)
64 .unwrap_or_else(|| Utc::now().to_rfc3339()),
65 );
66 }
67}
68
69fn option_str<'a>(record: &'a ExecutionRecord, key: &str) -> Option<&'a str> {
70 record
71 .options
72 .get(key)
73 .and_then(|value| value.as_str())
74 .filter(|value| !value.is_empty())
75}
76
77fn should_probe_removal(record: &ExecutionRecord) -> bool {
81 record.status == ExecutionStatus::Executed
82 && option_str(record, "isolated") == Some("docker")
83 && option_str(record, "isolationMode") == Some("detached")
84 && (option_str(record, "sessionName").is_some()
85 || option_str(record, "containerId").is_some())
86 && record.container_state.as_deref() != Some(CONTAINER_STATE_REMOVED)
87}
88
89pub fn attach_container_removal(mut record: ExecutionRecord) -> ExecutionRecord {
93 if !should_probe_removal(&record) {
94 return record;
95 }
96 let target = option_str(&record, "containerId")
97 .or_else(|| option_str(&record, "sessionName"))
98 .map(str::to_string);
99 if target.is_some_and(|target| docker_object_missing(&target)) {
100 mark_container_removed(&mut record, None);
101 }
102 record
103}
104
105pub fn persist_container_removal(
108 store: &ExecutionStore,
109 stored: &ExecutionRecord,
110 enriched: &ExecutionRecord,
111) {
112 if enriched.container_state.as_deref() != Some(CONTAINER_STATE_REMOVED)
113 || stored.container_state.as_deref() == Some(CONTAINER_STATE_REMOVED)
114 {
115 return;
116 }
117 if let Some(observed_at) = enriched.container_removed_observed_at.as_deref() {
118 let _ = store.mark_container_removed(&stored.uuid, observed_at);
120 }
121}