1use std::fs;
4use std::path::Path;
5use std::process::{Command, Stdio};
6use std::thread;
7use std::time::Duration;
8
9use super::{get_shell, is_debug, wrap_command_with_user, IsolationResult};
10use crate::isolation::isolation_log::{get_temp_dir, wrap_command_with_log_footer};
11
12pub fn get_screen_version() -> Option<(u32, u32, u32)> {
14 let output = Command::new("screen").arg("--version").output().ok()?;
15
16 let output_str = String::from_utf8_lossy(&output.stdout);
17 let stderr_str = String::from_utf8_lossy(&output.stderr);
18 let combined = format!("{}{}", output_str, stderr_str);
19
20 let re = regex::Regex::new(r"(\d+)\.(\d+)\.(\d+)").ok()?;
22 let caps = re.captures(&combined)?;
23
24 Some((
25 caps.get(1)?.as_str().parse().ok()?,
26 caps.get(2)?.as_str().parse().ok()?,
27 caps.get(3)?.as_str().parse().ok()?,
28 ))
29}
30
31pub fn supports_logfile_option() -> bool {
33 match get_screen_version() {
34 Some((major, minor, patch)) => {
35 if major > 4 {
36 return true;
37 }
38 if major < 4 {
39 return false;
40 }
41 if minor > 5 {
43 return true;
44 }
45 if minor < 5 {
46 return false;
47 }
48 patch >= 1
50 }
51 None => false,
52 }
53}
54
55pub fn run_screen_with_log_capture(
71 command: &str,
72 session_name: &str,
73 user: Option<&str>,
74 log_path: Option<&Path>,
75 env: &crate::execution_env::ExecutionEnv,
76) -> IsolationResult {
77 let (shell, shell_arg) = get_shell();
78 let screen_temp_dir = get_temp_dir(&["isolation", "screen"]);
79 let log_file = log_path
80 .map(|p| p.to_path_buf())
81 .unwrap_or_else(|| screen_temp_dir.join(format!("screen-output-{}.log", session_name)));
82 let should_cleanup_log_file = log_path.is_none();
83 let exit_code_file = screen_temp_dir.join(format!("screen-exit-{}.code", session_name));
84 if let Some(parent) = log_file.parent() {
85 let _ = fs::create_dir_all(parent);
86 }
87 let log_start_offset = if log_path.is_some() {
88 fs::metadata(&log_file)
89 .map(|m| m.len() as usize)
90 .unwrap_or(0)
91 } else {
92 0
93 };
94 let effective_command = wrap_command_with_user(command, user);
95
96 let is_bare_shell = command.trim() == "bash"
98 || command.trim() == "zsh"
99 || command.trim() == "sh"
100 || command.trim() == "/bin/bash"
101 || command.trim() == "/bin/zsh"
102 || command.trim() == "/bin/sh";
103
104 let final_command = if is_bare_shell {
106 effective_command.clone()
107 } else {
108 format!(
109 "{}; echo $? > \"{}\"",
110 effective_command,
111 exit_code_file.display()
112 )
113 };
114
115 let screenrc_path = screen_temp_dir.join(format!("screenrc-{}", session_name));
122 let screenrc_content = format!(
123 "logfile {}\nlogfile flush 0\ndeflog on\n",
124 log_file.display()
125 );
126 if let Err(e) = fs::write(&screenrc_path, &screenrc_content) {
127 if is_debug() {
128 eprintln!("[screen-isolation] Failed to create screenrc: {}", e);
129 }
130 return IsolationResult {
131 success: false,
132 session_name: Some(session_name.to_string()),
133 message: format!("Failed to create screenrc for logging: {}", e),
134 ..Default::default()
135 };
136 }
137
138 let screen_args: Vec<String> = if is_bare_shell {
151 let mut args = vec![
152 "-dmS".to_string(),
153 session_name.to_string(),
154 "-L".to_string(),
155 "-c".to_string(),
156 screenrc_path.to_string_lossy().to_string(),
157 ];
158 args.extend(super::to_shell_words(command));
159 args
160 } else {
161 vec![
162 "-dmS".to_string(),
163 session_name.to_string(),
164 "-L".to_string(),
165 "-c".to_string(),
166 screenrc_path.to_string_lossy().to_string(),
167 shell.clone(),
168 shell_arg.clone(),
169 final_command.clone(),
170 ]
171 };
172
173 if is_debug() {
174 eprintln!("[screen-isolation] Running: screen {:?}", screen_args);
175 eprintln!("[screen-isolation] screenrc: {}", screenrc_content.trim());
176 eprintln!("[screen-isolation] Log file: {}", log_file.display());
177 eprintln!(
178 "[screen-isolation] Exit code file: {}",
179 exit_code_file.display()
180 );
181 }
182
183 let status = Command::new("screen")
185 .args(&screen_args)
186 .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
187 .stdout(Stdio::null())
188 .stderr(Stdio::null())
189 .status();
190
191 if status.is_err() {
192 let _ = fs::remove_file(&screenrc_path);
194 return IsolationResult {
195 success: false,
196 session_name: Some(session_name.to_string()),
197 message: "Failed to start screen session".to_string(),
198 ..Default::default()
199 };
200 }
201
202 let read_log_with_retry = || -> Option<String> {
205 let retry_delays = [50u64, 100, 200];
206
207 let content = fs::read_to_string(&log_file)
208 .ok()
209 .map(|s| s.chars().skip(log_start_offset).collect::<String>());
210 if let Some(ref s) = content {
211 if !s.trim().is_empty() {
212 return content;
213 }
214 }
215
216 for (i, delay) in retry_delays.iter().enumerate() {
218 if is_debug() {
219 eprintln!(
220 "[screen-isolation] Log file empty, retry {}/{} after {}ms",
221 i + 1,
222 retry_delays.len(),
223 delay
224 );
225 }
226 thread::sleep(Duration::from_millis(*delay));
227 let retry_content = fs::read_to_string(&log_file)
228 .ok()
229 .map(|s| s.chars().skip(log_start_offset).collect::<String>());
230 if let Some(ref s) = retry_content {
231 if !s.trim().is_empty() {
232 return retry_content;
233 }
234 }
235 }
236
237 if is_debug() {
238 eprintln!(
239 "[screen-isolation] Log file still empty after {} retries",
240 retry_delays.len()
241 );
242 match fs::metadata(&log_file) {
243 Ok(meta) => eprintln!(
244 "[screen-isolation] Log file exists, size: {} bytes",
245 meta.len()
246 ),
247 Err(_) => eprintln!("[screen-isolation] Log file does not exist"),
248 }
249 }
250
251 content
252 };
253
254 let read_exit_code = || -> i32 {
256 if is_bare_shell {
257 return 0;
258 }
259 match fs::read_to_string(&exit_code_file) {
260 Ok(content) => {
261 let code = content.trim().parse::<i32>().unwrap_or(0);
262 if is_debug() {
263 eprintln!("[screen-isolation] Captured exit code: {}", code);
264 }
265 code
266 }
267 Err(_) => {
268 if is_debug() {
269 eprintln!("[screen-isolation] Could not read exit code file, defaulting to 0");
270 }
271 0
272 }
273 }
274 };
275
276 let cleanup = || {
278 if should_cleanup_log_file {
279 let _ = fs::remove_file(&log_file);
280 }
281 let _ = fs::remove_file(&screenrc_path);
282 let _ = fs::remove_file(&exit_code_file);
283 };
284
285 let max_wait = Duration::from_secs(300);
287 let check_interval = Duration::from_millis(100);
288 let mut waited = Duration::ZERO;
289
290 loop {
291 let sessions = Command::new("screen")
293 .arg("-ls")
294 .output()
295 .map(|o| String::from_utf8_lossy(&o.stdout).to_string())
296 .unwrap_or_default();
297
298 if !sessions.contains(session_name) {
299 let output = read_log_with_retry();
301 let exit_code = read_exit_code();
302
303 if let Some(ref out) = output {
305 if !out.trim().is_empty() {
306 print!("{}", out);
307 }
308 }
309
310 cleanup();
312
313 return IsolationResult {
314 diagnostics: None,
315 success: exit_code == 0,
316 session_name: Some(session_name.to_string()),
317 container_id: None,
318 message: format!(
319 "Screen session \"{}\" exited with code {}",
320 session_name, exit_code
321 ),
322 exit_code: Some(exit_code),
323 output,
324 container_removed: false,
325 };
326 }
327
328 thread::sleep(check_interval);
329 waited += check_interval;
330
331 if waited >= max_wait {
332 cleanup();
333 return IsolationResult {
334 diagnostics: None,
335 success: false,
336 session_name: Some(session_name.to_string()),
337 message: format!(
338 "Screen session \"{}\" timed out after {} seconds",
339 session_name,
340 max_wait.as_secs()
341 ),
342 exit_code: Some(1),
343 ..Default::default()
344 };
345 }
346 }
347}
348
349pub fn start_detached_screen_with_log_capture(
351 command: &str,
352 session_name: &str,
353 user: Option<&str>,
354 keep_alive: bool,
355 log_path: Option<&Path>,
356 env: &crate::execution_env::ExecutionEnv,
357) -> IsolationResult {
358 let (shell, shell_arg) = get_shell();
359 let screen_temp_dir = get_temp_dir(&["isolation", "screen"]);
360 let log_file = log_path
361 .map(|p| p.to_path_buf())
362 .unwrap_or_else(|| screen_temp_dir.join(format!("screen-output-{}.log", session_name)));
363 if let Some(parent) = log_file.parent() {
364 let _ = fs::create_dir_all(parent);
365 }
366
367 let screenrc_path = screen_temp_dir.join(format!("screenrc-{}", session_name));
368 let screenrc_content = format!(
369 "logfile {}\nlogfile flush 0\ndeflog on\n",
370 log_file.display()
371 );
372 if let Err(e) = fs::write(&screenrc_path, &screenrc_content) {
373 if is_debug() {
374 eprintln!("[screen-isolation] Failed to create screenrc: {}", e);
375 }
376 return IsolationResult {
377 success: false,
378 session_name: Some(session_name.to_string()),
379 message: format!("Failed to create screenrc for logging: {}", e),
380 ..Default::default()
381 };
382 }
383
384 let effective_command = wrap_command_with_user(command, user);
385 let final_command = wrap_command_with_log_footer(&effective_command, &shell, keep_alive);
386 let screen_args = vec![
387 "-dmS".to_string(),
388 session_name.to_string(),
389 "-L".to_string(),
390 "-c".to_string(),
391 screenrc_path.to_string_lossy().to_string(),
392 shell.clone(),
393 shell_arg,
394 final_command,
395 ];
396
397 if is_debug() {
398 eprintln!("[screen-isolation] Running: screen {:?}", screen_args);
399 eprintln!("[screen-isolation] screenrc: {}", screenrc_content.trim());
400 eprintln!("[screen-isolation] Log file: {}", log_file.display());
401 }
402
403 match Command::new("screen")
404 .args(&screen_args)
405 .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
406 .status()
407 {
408 Ok(status) if status.success() => {
409 let mut message = format!(
410 "Command started in detached screen session: {}",
411 session_name
412 );
413 if keep_alive {
414 message.push_str("\nSession will stay alive after command completes.");
415 } else {
416 message.push_str("\nSession will exit automatically after command completes.");
417 }
418 message.push_str(&format!("\nReattach with: screen -r {}", session_name));
419 message.push_str(&format!("\nLive log: {}", log_file.display()));
420 IsolationResult {
421 success: true,
422 session_name: Some(session_name.to_string()),
423 message,
424 ..Default::default()
425 }
426 }
427 Ok(status) => IsolationResult {
428 success: false,
429 session_name: Some(session_name.to_string()),
430 message: format!(
431 "Failed to start screen session (exit code {})",
432 status.code().unwrap_or(-1)
433 ),
434 ..Default::default()
435 },
436 Err(e) => IsolationResult {
437 success: false,
438 session_name: Some(session_name.to_string()),
439 message: format!("Failed to start screen session: {}", e),
440 ..Default::default()
441 },
442 }
443}