start_command/
detached_finalize.rs1use std::path::PathBuf;
18
19use chrono::Utc;
20use serde_json::Value;
21
22use crate::cgroup_memory::{cgroup_shell_vars, parse_cgroup_memory_sample};
23use crate::docker_post_mortem::{normalize_docker_timestamp, shell_vars};
24use crate::execution_store::{
25 ExecutionRecord, ExecutionStatus, ExecutionStore, ExecutionStoreOptions,
26};
27use crate::exit_reason::{describe_exit_code_str, resolve_exit_reason};
28use crate::isolation::isolation_log::{read_log_tail, shell_quote, FATAL_MARKER_TAIL_BYTES};
29
30pub const INTERNAL_FINALIZE_FLAG: &str = "--internal-finalize-detached-docker";
34
35pub const END_TIME_SOURCE_DOCKER_FINISHED_AT: &str = "docker-finished-at";
38pub const END_TIME_SOURCE_LOG_FOOTER: &str = "log-footer";
40pub const END_TIME_SOURCE_OBSERVED_AT: &str = "observed-at";
42
43pub const WATCHER_LOST_CONTAINER: &str = "watcher-lost-container";
47
48#[derive(Debug, Clone, Default)]
50pub struct DetachedFinalizeFacts {
51 pub exit_code: String,
52 pub oom_killed: String,
53 pub started_at: String,
54 pub finished_at: String,
55 pub container_error: String,
56 pub running: String,
58 pub cgroup_memory: String,
61}
62
63#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct FinalizeOutcome {
67 pub updated: bool,
68 pub reason: String,
69}
70
71pub(crate) fn normalize_bool(value: &str) -> Option<bool> {
72 match value.trim() {
73 "true" => Some(true),
74 "false" => Some(false),
75 _ => None,
76 }
77}
78
79pub(crate) fn normalize_container_error(value: &str) -> Option<String> {
80 let trimmed = value.trim();
81 if trimmed.is_empty() || trimmed == "<no value>" || trimmed == "(none)" {
82 None
83 } else {
84 Some(trimmed.to_string())
85 }
86}
87
88pub fn finalize_detached_execution(
94 store: &ExecutionStore,
95 execution_id: &str,
96 facts: &DetachedFinalizeFacts,
97) -> FinalizeOutcome {
98 if execution_id.is_empty() {
99 return FinalizeOutcome {
100 updated: false,
101 reason: "missing-arguments".to_string(),
102 };
103 }
104 if normalize_bool(&facts.running) == Some(true) {
105 return FinalizeOutcome {
108 updated: false,
109 reason: "still-running".to_string(),
110 };
111 }
112 let mut record = match store.get(execution_id) {
113 Some(record) => record,
114 None => {
115 return FinalizeOutcome {
116 updated: false,
117 reason: "record-not-found".to_string(),
118 }
119 }
120 };
121 if record.status == ExecutionStatus::Executed && record.end_time.is_some() {
122 return FinalizeOutcome {
125 updated: false,
126 reason: "already-final".to_string(),
127 };
128 }
129
130 let finished_at = normalize_docker_timestamp(Some(&facts.finished_at));
131 let described = describe_exit_code_str(&facts.exit_code);
132 let exit_code = match described.code {
135 Some(0) if finished_at.is_none() => None,
136 code => code,
137 };
138 record.status = ExecutionStatus::Executed;
139 match exit_code {
140 Some(code) => record.exit_code = Some(code),
141 None => {
142 if record.exit_code.is_none() || finished_at.is_none() {
143 record.exit_code = Some(-1);
144 }
145 }
146 }
147 let lost = finished_at.is_none();
148
149 match finished_at {
150 Some(finished_at) => {
151 record.end_time = Some(finished_at);
152 record.end_time_source = Some(END_TIME_SOURCE_DOCKER_FINISHED_AT.to_string());
153 }
154 None => {
155 let now = Utc::now().to_rfc3339();
160 record.end_time = Some(now.clone());
161 record.end_time_source = Some(END_TIME_SOURCE_OBSERVED_AT.to_string());
162 record.observed_at = Some(now);
163 }
164 }
165
166 if let Some(started_at) = normalize_docker_timestamp(Some(&facts.started_at)) {
167 record.container_started_at = Some(started_at);
168 }
169 if let Some(oom_killed) = normalize_bool(&facts.oom_killed) {
170 record.oom_killed = Some(oom_killed);
171 }
172 if let Some(cgroup_memory) = parse_cgroup_memory_sample(&facts.cgroup_memory) {
174 record.cgroup_memory = Some(cgroup_memory);
175 }
176 let reason =
177 resolve_reason(&record).or_else(|| lost.then(|| WATCHER_LOST_CONTAINER.to_string()));
178 if let Some(reason) = reason {
179 record.exit_reason = Some(reason);
180 }
181 if let Some(error) = normalize_container_error(&facts.container_error) {
182 record
183 .options
184 .insert("containerError".to_string(), Value::String(error));
185 }
186
187 match store.save(&record) {
188 Ok(()) => FinalizeOutcome {
189 updated: true,
190 reason: "finalized".to_string(),
191 },
192 Err(error) => FinalizeOutcome {
193 updated: false,
194 reason: format!("save-failed: {}", error),
195 },
196 }
197}
198
199fn resolve_reason(record: &ExecutionRecord) -> Option<String> {
201 let tail = if record.log_path.is_empty() {
202 None
203 } else {
204 read_log_tail(&record.log_path, FATAL_MARKER_TAIL_BYTES)
205 };
206 resolve_exit_reason(
207 record.exit_code,
208 tail.as_deref(),
209 record.oom_killed,
210 record.cgroup_memory.and_then(|memory| memory.oom_kills),
211 )
212}
213
214pub fn reconcile_finalized_record(
223 store: &ExecutionStore,
224 record: &ExecutionRecord,
225) -> ExecutionRecord {
226 let stored = match store.get(&record.uuid) {
227 Some(stored) => stored,
228 None => return record.clone(),
229 };
230 if stored.status != ExecutionStatus::Executed || stored.end_time.is_none() {
231 return record.clone();
232 }
233 let mut merged = stored;
234 for (key, value) in &record.options {
235 merged.options.insert(key.clone(), value.clone());
236 }
237 merged
238}
239
240pub fn build_detached_finalize_snippet(execution_id: &str) -> String {
246 let executable = std::env::current_exe()
247 .map(|path| path.to_string_lossy().to_string())
248 .unwrap_or_else(|_| "start".to_string());
249 format!(
250 "{} {} {} \"${}\" \"${}\" \"${}\" \"${}\" \"${}\" \"${}\" \"${}\" >/dev/null 2>&1 || true",
251 shell_quote(&executable),
252 INTERNAL_FINALIZE_FLAG,
253 shell_quote(execution_id),
254 shell_vars::EXIT,
255 shell_vars::OOM,
256 shell_vars::STARTED,
257 shell_vars::FINISHED,
258 shell_vars::ERROR,
259 shell_vars::RUNNING,
260 cgroup_shell_vars::SAMPLE,
261 )
262}
263
264pub fn run_internal_finalize(args: &[String]) {
267 let execution_id = match args.first() {
268 Some(value) if !value.is_empty() => value.clone(),
269 _ => return,
270 };
271 if std::env::var("START_DISABLE_TRACKING").as_deref() == Ok("true") {
272 return;
273 }
274 let field = |index: usize| args.get(index).cloned().unwrap_or_default();
275 let store = ExecutionStore::with_options(ExecutionStoreOptions {
276 app_folder: std::env::var("START_APP_FOLDER").ok().map(PathBuf::from),
277 ..ExecutionStoreOptions::default()
278 });
279 finalize_detached_execution(
280 &store,
281 &execution_id,
282 &DetachedFinalizeFacts {
283 exit_code: field(1),
284 oom_killed: field(2),
285 started_at: field(3),
286 finished_at: field(4),
287 container_error: field(5),
288 running: field(6),
289 cgroup_memory: field(7),
290 },
291 );
292}