start_command/
isolation_log.rs1use std::env;
4use std::fs::{self, File, OpenOptions};
5use std::io::{Read, Seek, SeekFrom, Write};
6use std::path::PathBuf;
7
8pub fn get_timestamp() -> String {
10 chrono::Utc::now()
11 .format("%Y-%m-%d %H:%M:%S%.3f")
12 .to_string()
13}
14
15pub fn generate_log_filename(environment: &str) -> String {
17 let timestamp = std::time::SystemTime::now()
18 .duration_since(std::time::UNIX_EPOCH)
19 .unwrap()
20 .as_millis();
21 let random: String = (0..6)
22 .map(|_| {
23 let idx = (std::time::SystemTime::now()
24 .duration_since(std::time::UNIX_EPOCH)
25 .unwrap()
26 .as_nanos()
27 % 36) as u8;
28 if idx < 10 {
29 (b'0' + idx) as char
30 } else {
31 (b'a' + idx - 10) as char
32 }
33 })
34 .collect();
35 format!("start-command-{}-{}-{}.log", environment, timestamp, random)
36}
37
38pub fn get_temp_root() -> PathBuf {
40 env::var("START_TEMP_ROOT")
41 .map(PathBuf::from)
42 .unwrap_or_else(|_| env::temp_dir().join("start-command"))
43}
44
45#[derive(Debug)]
47pub struct LogHeaderParams {
48 pub command: String,
49 pub environment: String,
50 pub mode: String,
51 pub session_name: String,
52 pub image: Option<String>,
53 pub user: Option<String>,
54 pub start_time: String,
55}
56
57pub fn create_log_header(params: &LogHeaderParams) -> String {
59 let mut content = String::new();
60 content.push_str("=== Start Command Log ===\n");
61 content.push_str(&format!("Timestamp: {}\n", params.start_time));
62 content.push_str(&format!("Command: {}\n", params.command));
63 content.push_str(&format!("Environment: {}\n", params.environment));
64 content.push_str(&format!("Mode: {}\n", params.mode));
65 content.push_str(&format!("Session: {}\n", params.session_name));
66 if let Some(ref image) = params.image {
67 content.push_str(&format!("Image: {}\n", image));
68 }
69 if let Some(ref user) = params.user {
70 content.push_str(&format!("User: {}\n", user));
71 }
72 content.push_str(&format!("Platform: {}\n", std::env::consts::OS));
73 content.push_str(&format!(
74 "Working Directory: {}\n",
75 env::current_dir().unwrap_or_default().display()
76 ));
77 content.push_str(&format!("{}\n\n", "=".repeat(50)));
78 content
79}
80
81pub fn create_log_footer(end_time: &str, exit_code: i32) -> String {
83 let mut content = String::new();
84 content.push_str(&format!("\n{}\n", "=".repeat(50)));
85 content.push_str(&format!("Finished: {}\n", end_time));
86 content.push_str(&format!("Exit Code: {}\n", exit_code));
87 content
88}
89
90pub fn write_log_file(log_path: &PathBuf, content: &str) -> bool {
92 if let Some(parent) = log_path.parent() {
93 if let Err(e) = fs::create_dir_all(parent) {
94 eprintln!("\nWarning: Could not create log directory: {}", e);
95 return false;
96 }
97 }
98 match File::create(log_path) {
99 Ok(mut file) => file.write_all(content.as_bytes()).is_ok(),
100 Err(e) => {
101 eprintln!("\nWarning: Could not save log file: {}", e);
102 false
103 }
104 }
105}
106
107pub fn append_log_file(log_path: &PathBuf, content: &str) -> bool {
109 if let Some(parent) = log_path.parent() {
110 if let Err(e) = fs::create_dir_all(parent) {
111 eprintln!("\nWarning: Could not create log directory: {}", e);
112 return false;
113 }
114 }
115 match OpenOptions::new().create(true).append(true).open(log_path) {
116 Ok(mut file) => file.write_all(content.as_bytes()).is_ok(),
117 Err(e) => {
118 eprintln!("\nWarning: Could not append log file: {}", e);
119 false
120 }
121 }
122}
123
124pub fn get_log_dir() -> PathBuf {
126 env::var("START_LOG_DIR")
127 .map(PathBuf::from)
128 .unwrap_or_else(|_| get_temp_root().join("logs"))
129}
130
131pub fn get_temp_dir(segments: &[&str]) -> PathBuf {
133 let mut dir = get_temp_root().join("tmp");
134 for segment in segments {
135 dir.push(segment);
136 }
137 let _ = fs::create_dir_all(&dir);
138 dir
139}
140
141pub fn create_log_path(environment: &str) -> PathBuf {
143 let log_dir = get_log_dir();
144 let log_filename = generate_log_filename(environment);
145 if environment == "direct" {
146 log_dir.join("direct").join(log_filename)
147 } else {
148 log_dir
149 .join("isolation")
150 .join(environment)
151 .join(log_filename)
152 }
153}
154
155pub fn create_log_path_for_execution(environment: &str, execution_id: &str) -> PathBuf {
157 if environment == "direct" {
158 get_log_dir()
159 .join("direct")
160 .join(format!("{}.log", execution_id))
161 } else {
162 get_log_dir()
163 .join("isolation")
164 .join(environment)
165 .join(format!("{}.log", execution_id))
166 }
167}
168
169pub fn shell_quote(value: &str) -> String {
170 format!("'{}'", value.replace('\'', "'\\''"))
171}
172
173pub fn create_shell_log_footer_snippet() -> String {
174 let date_command =
178 "date -u '+%Y-%m-%d %H:%M:%S.%3N' 2>/dev/null || date -u '+%Y-%m-%d %H:%M:%S'";
179 format!(
180 "printf '\\n==================================================\\nFinished: %s\\nExit Code: %s\\n' \"$({})\" \"$__start_command_exit\"",
181 date_command
182 )
183}
184
185pub fn wrap_command_with_log_footer(command: &str, shell: &str, keep_alive: bool) -> String {
186 let after_footer = if keep_alive {
187 format!("exec {}", shell_quote(shell))
188 } else {
189 "exit \"$__start_command_exit\"".to_string()
190 };
191 format!(
192 "({}); __start_command_exit=$?; {}; {}",
193 command,
194 create_shell_log_footer_snippet(),
195 after_footer
196 )
197}
198
199pub fn get_default_docker_image() -> String {
207 #[cfg(target_os = "macos")]
208 {
209 return "alpine:latest".to_string();
211 }
212
213 #[cfg(target_os = "windows")]
214 {
215 return "alpine:latest".to_string();
217 }
218
219 #[cfg(target_os = "linux")]
220 {
221 use std::fs;
222 if let Ok(os_release) = fs::read_to_string("/etc/os-release") {
224 if os_release.contains("ID=ubuntu")
226 || os_release.contains("ID_LIKE=ubuntu")
227 || os_release.contains("ID_LIKE=debian ubuntu")
228 {
229 return "ubuntu:latest".to_string();
230 }
231
232 if os_release.contains("ID=debian") || os_release.contains("ID_LIKE=debian") {
234 return "debian:latest".to_string();
235 }
236
237 if os_release.contains("ID=arch") || os_release.contains("ID_LIKE=arch") {
239 return "archlinux:latest".to_string();
240 }
241
242 if os_release.contains("ID=fedora") {
244 return "fedora:latest".to_string();
245 }
246
247 if os_release.contains("ID=centos")
249 || os_release.contains("ID=rhel")
250 || os_release.contains("ID_LIKE=rhel")
251 {
252 return "centos:latest".to_string();
253 }
254
255 if os_release.contains("ID=alpine") {
257 return "alpine:latest".to_string();
258 }
259 }
260
261 "alpine:latest".to_string()
263 }
264
265 #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
267 {
268 "alpine:latest".to_string()
269 }
270}
271
272pub const LOG_TAIL_BYTES: u64 = 16 * 1024;
276
277pub const FATAL_MARKER_TAIL_BYTES: u64 = 64 * 1024;
283
284pub fn read_log_tail(log_path: &str, bytes: u64) -> Option<String> {
290 read_log_tail_from(log_path, bytes, 0)
291}
292
293pub fn read_log_tail_from(log_path: &str, bytes: u64, offset: u64) -> Option<String> {
295 read_log_tail_range(log_path, bytes, offset, u64::MAX)
296}
297
298pub fn read_log_tail_range(log_path: &str, bytes: u64, offset: u64, end: u64) -> Option<String> {
300 let mut file = fs::File::open(log_path).ok()?;
301 let size = file.metadata().ok()?.len().min(end);
302 let length = size.saturating_sub(offset).min(bytes);
303 file.seek(SeekFrom::Start(size - length)).ok()?;
304 let mut buffer = Vec::with_capacity(length as usize);
305 file.take(length).read_to_end(&mut buffer).ok()?;
306 let tail = String::from_utf8_lossy(&buffer).into_owned();
307 if size - length == offset || length == 0 {
308 return Some(tail);
309 }
310 Some(match tail.find('\n') {
311 Some(index) => tail[index + 1..].to_string(),
312 None => String::new(),
313 })
314}