start_command/
detached_finalize.rs1use std::path::PathBuf;
18
19use chrono::Utc;
20use serde_json::Value;
21
22use crate::docker_post_mortem::{normalize_docker_timestamp, shell_vars};
23use crate::execution_store::{
24 ExecutionRecord, ExecutionStatus, ExecutionStore, ExecutionStoreOptions,
25};
26use crate::exit_reason::{describe_exit_code_str, resolve_exit_reason};
27use crate::isolation::isolation_log::{read_log_tail, shell_quote, FATAL_MARKER_TAIL_BYTES};
28
29pub const INTERNAL_FINALIZE_FLAG: &str = "--internal-finalize-detached-docker";
33
34pub const END_TIME_SOURCE_DOCKER_FINISHED_AT: &str = "docker-finished-at";
37pub const END_TIME_SOURCE_LOG_FOOTER: &str = "log-footer";
39pub const END_TIME_SOURCE_OBSERVED_AT: &str = "observed-at";
41
42pub const WATCHER_LOST_CONTAINER: &str = "watcher-lost-container";
46
47#[derive(Debug, Clone, Default)]
49pub struct DetachedFinalizeFacts {
50 pub exit_code: String,
51 pub oom_killed: String,
52 pub started_at: String,
53 pub finished_at: String,
54 pub container_error: String,
55 pub running: String,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct FinalizeOutcome {
63 pub updated: bool,
64 pub reason: String,
65}
66
67fn normalize_bool(value: &str) -> Option<bool> {
68 match value.trim() {
69 "true" => Some(true),
70 "false" => Some(false),
71 _ => None,
72 }
73}
74
75fn normalize_container_error(value: &str) -> Option<String> {
76 let trimmed = value.trim();
77 if trimmed.is_empty() || trimmed == "<no value>" || trimmed == "(none)" {
78 None
79 } else {
80 Some(trimmed.to_string())
81 }
82}
83
84pub fn finalize_detached_execution(
90 store: &ExecutionStore,
91 execution_id: &str,
92 facts: &DetachedFinalizeFacts,
93) -> FinalizeOutcome {
94 if execution_id.is_empty() {
95 return FinalizeOutcome {
96 updated: false,
97 reason: "missing-arguments".to_string(),
98 };
99 }
100 if normalize_bool(&facts.running) == Some(true) {
101 return FinalizeOutcome {
104 updated: false,
105 reason: "still-running".to_string(),
106 };
107 }
108 let mut record = match store.get(execution_id) {
109 Some(record) => record,
110 None => {
111 return FinalizeOutcome {
112 updated: false,
113 reason: "record-not-found".to_string(),
114 }
115 }
116 };
117 if record.status == ExecutionStatus::Executed && record.end_time.is_some() {
118 return FinalizeOutcome {
121 updated: false,
122 reason: "already-final".to_string(),
123 };
124 }
125
126 let finished_at = normalize_docker_timestamp(Some(&facts.finished_at));
127 let described = describe_exit_code_str(&facts.exit_code);
128 let exit_code = match described.code {
131 Some(0) if finished_at.is_none() => None,
132 code => code,
133 };
134 record.status = ExecutionStatus::Executed;
135 match exit_code {
136 Some(code) => record.exit_code = Some(code),
137 None => {
138 if record.exit_code.is_none() || finished_at.is_none() {
139 record.exit_code = Some(-1);
140 }
141 }
142 }
143 let lost = finished_at.is_none();
144
145 match finished_at {
146 Some(finished_at) => {
147 record.end_time = Some(finished_at);
148 record.end_time_source = Some(END_TIME_SOURCE_DOCKER_FINISHED_AT.to_string());
149 }
150 None => {
151 let now = Utc::now().to_rfc3339();
156 record.end_time = Some(now.clone());
157 record.end_time_source = Some(END_TIME_SOURCE_OBSERVED_AT.to_string());
158 record.observed_at = Some(now);
159 }
160 }
161
162 if let Some(started_at) = normalize_docker_timestamp(Some(&facts.started_at)) {
163 record.container_started_at = Some(started_at);
164 }
165 if let Some(oom_killed) = normalize_bool(&facts.oom_killed) {
166 record.oom_killed = Some(oom_killed);
167 }
168 let reason =
169 resolve_reason(&record).or_else(|| lost.then(|| WATCHER_LOST_CONTAINER.to_string()));
170 if let Some(reason) = reason {
171 record.exit_reason = Some(reason);
172 }
173 if let Some(error) = normalize_container_error(&facts.container_error) {
174 record
175 .options
176 .insert("containerError".to_string(), Value::String(error));
177 }
178
179 match store.save(&record) {
180 Ok(()) => FinalizeOutcome {
181 updated: true,
182 reason: "finalized".to_string(),
183 },
184 Err(error) => FinalizeOutcome {
185 updated: false,
186 reason: format!("save-failed: {}", error),
187 },
188 }
189}
190
191fn resolve_reason(record: &ExecutionRecord) -> Option<String> {
193 let tail = if record.log_path.is_empty() {
194 None
195 } else {
196 read_log_tail(&record.log_path, FATAL_MARKER_TAIL_BYTES)
197 };
198 resolve_exit_reason(record.exit_code, tail.as_deref(), record.oom_killed)
199}
200
201pub fn reconcile_finalized_record(
210 store: &ExecutionStore,
211 record: &ExecutionRecord,
212) -> ExecutionRecord {
213 let stored = match store.get(&record.uuid) {
214 Some(stored) => stored,
215 None => return record.clone(),
216 };
217 if stored.status != ExecutionStatus::Executed || stored.end_time.is_none() {
218 return record.clone();
219 }
220 let mut merged = stored;
221 for (key, value) in &record.options {
222 merged.options.insert(key.clone(), value.clone());
223 }
224 merged
225}
226
227pub fn build_detached_finalize_snippet(execution_id: &str) -> String {
233 let executable = std::env::current_exe()
234 .map(|path| path.to_string_lossy().to_string())
235 .unwrap_or_else(|_| "start".to_string());
236 format!(
237 "{} {} {} \"${}\" \"${}\" \"${}\" \"${}\" \"${}\" \"${}\" >/dev/null 2>&1 || true",
238 shell_quote(&executable),
239 INTERNAL_FINALIZE_FLAG,
240 shell_quote(execution_id),
241 shell_vars::EXIT,
242 shell_vars::OOM,
243 shell_vars::STARTED,
244 shell_vars::FINISHED,
245 shell_vars::ERROR,
246 shell_vars::RUNNING,
247 )
248}
249
250pub fn run_internal_finalize(args: &[String]) {
253 let execution_id = match args.first() {
254 Some(value) if !value.is_empty() => value.clone(),
255 _ => return,
256 };
257 if std::env::var("START_DISABLE_TRACKING").as_deref() == Ok("true") {
258 return;
259 }
260 let field = |index: usize| args.get(index).cloned().unwrap_or_default();
261 let store = ExecutionStore::with_options(ExecutionStoreOptions {
262 app_folder: std::env::var("START_APP_FOLDER").ok().map(PathBuf::from),
263 ..ExecutionStoreOptions::default()
264 });
265 finalize_detached_execution(
266 &store,
267 &execution_id,
268 &DetachedFinalizeFacts {
269 exit_code: field(1),
270 oom_killed: field(2),
271 started_at: field(3),
272 finished_at: field(4),
273 container_error: field(5),
274 running: field(6),
275 },
276 );
277}