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
42#[derive(Debug, Clone, Default)]
44pub struct DetachedFinalizeFacts {
45 pub exit_code: String,
46 pub oom_killed: String,
47 pub started_at: String,
48 pub finished_at: String,
49 pub container_error: String,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct FinalizeOutcome {
56 pub updated: bool,
57 pub reason: String,
58}
59
60fn normalize_bool(value: &str) -> Option<bool> {
61 match value.trim() {
62 "true" => Some(true),
63 "false" => Some(false),
64 _ => None,
65 }
66}
67
68fn normalize_container_error(value: &str) -> Option<String> {
69 let trimmed = value.trim();
70 if trimmed.is_empty() || trimmed == "<no value>" || trimmed == "(none)" {
71 None
72 } else {
73 Some(trimmed.to_string())
74 }
75}
76
77pub fn finalize_detached_execution(
83 store: &ExecutionStore,
84 execution_id: &str,
85 facts: &DetachedFinalizeFacts,
86) -> FinalizeOutcome {
87 if execution_id.is_empty() {
88 return FinalizeOutcome {
89 updated: false,
90 reason: "missing-arguments".to_string(),
91 };
92 }
93 let mut record = match store.get(execution_id) {
94 Some(record) => record,
95 None => {
96 return FinalizeOutcome {
97 updated: false,
98 reason: "record-not-found".to_string(),
99 }
100 }
101 };
102 if record.status == ExecutionStatus::Executed && record.end_time.is_some() {
103 return FinalizeOutcome {
106 updated: false,
107 reason: "already-final".to_string(),
108 };
109 }
110
111 let described = describe_exit_code_str(&facts.exit_code);
112 record.status = ExecutionStatus::Executed;
113 match described.code {
114 Some(code) => record.exit_code = Some(code),
115 None => {
116 if record.exit_code.is_none() {
117 record.exit_code = Some(-1);
118 }
119 }
120 }
121
122 match normalize_docker_timestamp(Some(&facts.finished_at)) {
123 Some(finished_at) => {
124 record.end_time = Some(finished_at);
125 record.end_time_source = Some(END_TIME_SOURCE_DOCKER_FINISHED_AT.to_string());
126 }
127 None => {
128 let now = Utc::now().to_rfc3339();
133 record.end_time = Some(now.clone());
134 record.end_time_source = Some(END_TIME_SOURCE_OBSERVED_AT.to_string());
135 record.observed_at = Some(now);
136 }
137 }
138
139 if let Some(started_at) = normalize_docker_timestamp(Some(&facts.started_at)) {
140 record.container_started_at = Some(started_at);
141 }
142 if let Some(oom_killed) = normalize_bool(&facts.oom_killed) {
143 record.oom_killed = Some(oom_killed);
144 }
145 if let Some(reason) = resolve_reason(&record) {
146 record.exit_reason = Some(reason);
147 }
148 if let Some(error) = normalize_container_error(&facts.container_error) {
149 record
150 .options
151 .insert("containerError".to_string(), Value::String(error));
152 }
153
154 match store.save(&record) {
155 Ok(()) => FinalizeOutcome {
156 updated: true,
157 reason: "finalized".to_string(),
158 },
159 Err(error) => FinalizeOutcome {
160 updated: false,
161 reason: format!("save-failed: {}", error),
162 },
163 }
164}
165
166fn resolve_reason(record: &ExecutionRecord) -> Option<String> {
168 let tail = if record.log_path.is_empty() {
169 None
170 } else {
171 read_log_tail(&record.log_path, FATAL_MARKER_TAIL_BYTES)
172 };
173 resolve_exit_reason(record.exit_code, tail.as_deref(), record.oom_killed)
174}
175
176pub fn reconcile_finalized_record(
185 store: &ExecutionStore,
186 record: &ExecutionRecord,
187) -> ExecutionRecord {
188 let stored = match store.get(&record.uuid) {
189 Some(stored) => stored,
190 None => return record.clone(),
191 };
192 if stored.status != ExecutionStatus::Executed || stored.end_time.is_none() {
193 return record.clone();
194 }
195 let mut merged = stored;
196 for (key, value) in &record.options {
197 merged.options.insert(key.clone(), value.clone());
198 }
199 merged
200}
201
202pub fn build_detached_finalize_snippet(execution_id: &str) -> String {
208 let executable = std::env::current_exe()
209 .map(|path| path.to_string_lossy().to_string())
210 .unwrap_or_else(|_| "start".to_string());
211 format!(
212 "{} {} {} \"${}\" \"${}\" \"${}\" \"${}\" \"${}\" >/dev/null 2>&1 || true",
213 shell_quote(&executable),
214 INTERNAL_FINALIZE_FLAG,
215 shell_quote(execution_id),
216 shell_vars::EXIT,
217 shell_vars::OOM,
218 shell_vars::STARTED,
219 shell_vars::FINISHED,
220 shell_vars::ERROR,
221 )
222}
223
224pub fn run_internal_finalize(args: &[String]) {
227 let execution_id = match args.first() {
228 Some(value) if !value.is_empty() => value.clone(),
229 _ => return,
230 };
231 if std::env::var("START_DISABLE_TRACKING").as_deref() == Ok("true") {
232 return;
233 }
234 let field = |index: usize| args.get(index).cloned().unwrap_or_default();
235 let store = ExecutionStore::with_options(ExecutionStoreOptions {
236 app_folder: std::env::var("START_APP_FOLDER").ok().map(PathBuf::from),
237 ..ExecutionStoreOptions::default()
238 });
239 finalize_detached_execution(
240 &store,
241 &execution_id,
242 &DetachedFinalizeFacts {
243 exit_code: field(1),
244 oom_killed: field(2),
245 started_at: field(3),
246 finished_at: field(4),
247 container_error: field(5),
248 },
249 );
250}