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 const SCREEN_UNLISTED_GRACE_POLLS: u32 = 20;
60
61pub fn is_screen_run_finished(listed: bool, exit_code_ready: bool, unlisted_polls: u32) -> bool {
65 !listed && (exit_code_ready || unlisted_polls >= SCREEN_UNLISTED_GRACE_POLLS)
66}
67
68pub fn run_screen_with_log_capture(
84 command: &str,
85 session_name: &str,
86 user: Option<&str>,
87 log_path: Option<&Path>,
88 env: &crate::execution_env::ExecutionEnv,
89) -> IsolationResult {
90 let (shell, shell_arg) = get_shell();
91 let screen_temp_dir = get_temp_dir(&["isolation", "screen"]);
92 let log_file = log_path
93 .map(|p| p.to_path_buf())
94 .unwrap_or_else(|| screen_temp_dir.join(format!("screen-output-{}.log", session_name)));
95 let should_cleanup_log_file = log_path.is_none();
96 let exit_code_file = screen_temp_dir.join(format!("screen-exit-{}.code", session_name));
97 if let Some(parent) = log_file.parent() {
98 let _ = fs::create_dir_all(parent);
99 }
100 let log_start_offset = if log_path.is_some() {
101 fs::metadata(&log_file)
102 .map(|m| m.len() as usize)
103 .unwrap_or(0)
104 } else {
105 0
106 };
107 let effective_command = wrap_command_with_user(command, user);
108
109 let is_bare_shell = command.trim() == "bash"
111 || command.trim() == "zsh"
112 || command.trim() == "sh"
113 || command.trim() == "/bin/bash"
114 || command.trim() == "/bin/zsh"
115 || command.trim() == "/bin/sh";
116
117 let final_command = if is_bare_shell {
119 effective_command.clone()
120 } else {
121 format!(
122 "{}; echo $? > \"{}\"",
123 effective_command,
124 exit_code_file.display()
125 )
126 };
127
128 let screenrc_path = screen_temp_dir.join(format!("screenrc-{}", session_name));
135 let screenrc_content = format!(
136 "logfile {}\nlogfile flush 0\ndeflog on\n",
137 log_file.display()
138 );
139 if let Err(e) = fs::write(&screenrc_path, &screenrc_content) {
140 if is_debug() {
141 eprintln!("[screen-isolation] Failed to create screenrc: {}", e);
142 }
143 return IsolationResult {
144 success: false,
145 session_name: Some(session_name.to_string()),
146 message: format!("Failed to create screenrc for logging: {}", e),
147 ..Default::default()
148 };
149 }
150
151 let screen_args: Vec<String> = if is_bare_shell {
164 let mut args = vec![
165 "-dmS".to_string(),
166 session_name.to_string(),
167 "-L".to_string(),
168 "-c".to_string(),
169 screenrc_path.to_string_lossy().to_string(),
170 ];
171 args.extend(super::to_shell_words(command));
172 args
173 } else {
174 vec![
175 "-dmS".to_string(),
176 session_name.to_string(),
177 "-L".to_string(),
178 "-c".to_string(),
179 screenrc_path.to_string_lossy().to_string(),
180 shell.clone(),
181 shell_arg.clone(),
182 final_command.clone(),
183 ]
184 };
185
186 if is_debug() {
187 eprintln!("[screen-isolation] Running: screen {:?}", screen_args);
188 eprintln!("[screen-isolation] screenrc: {}", screenrc_content.trim());
189 eprintln!("[screen-isolation] Log file: {}", log_file.display());
190 eprintln!(
191 "[screen-isolation] Exit code file: {}",
192 exit_code_file.display()
193 );
194 }
195
196 let status = Command::new("screen")
198 .args(&screen_args)
199 .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
200 .stdout(Stdio::null())
201 .stderr(Stdio::null())
202 .status();
203
204 if status.is_err() {
205 let _ = fs::remove_file(&screenrc_path);
207 return IsolationResult {
208 success: false,
209 session_name: Some(session_name.to_string()),
210 message: "Failed to start screen session".to_string(),
211 ..Default::default()
212 };
213 }
214
215 let read_log_with_retry = || -> Option<String> {
218 let retry_delays = [50u64, 100, 200];
219
220 let content = fs::read_to_string(&log_file)
221 .ok()
222 .map(|s| s.chars().skip(log_start_offset).collect::<String>());
223 if let Some(ref s) = content {
224 if !s.trim().is_empty() {
225 return content;
226 }
227 }
228
229 for (i, delay) in retry_delays.iter().enumerate() {
231 if is_debug() {
232 eprintln!(
233 "[screen-isolation] Log file empty, retry {}/{} after {}ms",
234 i + 1,
235 retry_delays.len(),
236 delay
237 );
238 }
239 thread::sleep(Duration::from_millis(*delay));
240 let retry_content = fs::read_to_string(&log_file)
241 .ok()
242 .map(|s| s.chars().skip(log_start_offset).collect::<String>());
243 if let Some(ref s) = retry_content {
244 if !s.trim().is_empty() {
245 return retry_content;
246 }
247 }
248 }
249
250 if is_debug() {
251 eprintln!(
252 "[screen-isolation] Log file still empty after {} retries",
253 retry_delays.len()
254 );
255 match fs::metadata(&log_file) {
256 Ok(meta) => eprintln!(
257 "[screen-isolation] Log file exists, size: {} bytes",
258 meta.len()
259 ),
260 Err(_) => eprintln!("[screen-isolation] Log file does not exist"),
261 }
262 }
263
264 content
265 };
266
267 let read_exit_code = || -> i32 {
269 if is_bare_shell {
270 return 0;
271 }
272 match fs::read_to_string(&exit_code_file) {
273 Ok(content) => {
274 let code = content.trim().parse::<i32>().unwrap_or(0);
275 if is_debug() {
276 eprintln!("[screen-isolation] Captured exit code: {}", code);
277 }
278 code
279 }
280 Err(_) => {
281 if is_debug() {
282 eprintln!("[screen-isolation] Could not read exit code file, defaulting to 0");
283 }
284 0
285 }
286 }
287 };
288
289 let cleanup = || {
291 if should_cleanup_log_file {
292 let _ = fs::remove_file(&log_file);
293 }
294 let _ = fs::remove_file(&screenrc_path);
295 let _ = fs::remove_file(&exit_code_file);
296 };
297
298 let max_wait = Duration::from_secs(300);
300 let check_interval = Duration::from_millis(100);
301 let mut waited = Duration::ZERO;
302
303 let exit_code_ready = || {
304 is_bare_shell
305 || fs::read_to_string(&exit_code_file).is_ok_and(|content| !content.trim().is_empty())
306 };
307 let mut unlisted_polls = 0;
308
309 loop {
310 let listed = Command::new("screen")
312 .arg("-ls")
313 .output()
314 .map(|o| String::from_utf8_lossy(&o.stdout).contains(session_name))
315 .unwrap_or(false);
316 unlisted_polls = if listed { 0 } else { unlisted_polls + 1 };
317
318 if is_screen_run_finished(listed, exit_code_ready(), unlisted_polls) {
319 let output = read_log_with_retry();
321 let exit_code = read_exit_code();
322
323 if let Some(ref out) = output {
325 if !out.trim().is_empty() {
326 print!("{}", out);
327 }
328 }
329
330 cleanup();
332
333 return IsolationResult {
334 diagnostics: None,
335 success: exit_code == 0,
336 session_name: Some(session_name.to_string()),
337 container_id: None,
338 message: format!(
339 "Screen session \"{}\" exited with code {}",
340 session_name, exit_code
341 ),
342 exit_code: Some(exit_code),
343 output,
344 container_removed: false,
345 };
346 }
347
348 thread::sleep(check_interval);
349 waited += check_interval;
350
351 if waited >= max_wait {
352 cleanup();
353 return IsolationResult {
354 diagnostics: None,
355 success: false,
356 session_name: Some(session_name.to_string()),
357 message: format!(
358 "Screen session \"{}\" timed out after {} seconds",
359 session_name,
360 max_wait.as_secs()
361 ),
362 exit_code: Some(1),
363 ..Default::default()
364 };
365 }
366 }
367}
368
369pub fn start_detached_screen_with_log_capture(
371 command: &str,
372 session_name: &str,
373 user: Option<&str>,
374 keep_alive: bool,
375 log_path: Option<&Path>,
376 env: &crate::execution_env::ExecutionEnv,
377) -> IsolationResult {
378 let (shell, shell_arg) = get_shell();
379 let screen_temp_dir = get_temp_dir(&["isolation", "screen"]);
380 let log_file = log_path
381 .map(|p| p.to_path_buf())
382 .unwrap_or_else(|| screen_temp_dir.join(format!("screen-output-{}.log", session_name)));
383 if let Some(parent) = log_file.parent() {
384 let _ = fs::create_dir_all(parent);
385 }
386
387 let screenrc_path = screen_temp_dir.join(format!("screenrc-{}", session_name));
388 let screenrc_content = format!(
389 "logfile {}\nlogfile flush 0\ndeflog on\n",
390 log_file.display()
391 );
392 if let Err(e) = fs::write(&screenrc_path, &screenrc_content) {
393 if is_debug() {
394 eprintln!("[screen-isolation] Failed to create screenrc: {}", e);
395 }
396 return IsolationResult {
397 success: false,
398 session_name: Some(session_name.to_string()),
399 message: format!("Failed to create screenrc for logging: {}", e),
400 ..Default::default()
401 };
402 }
403
404 let effective_command = wrap_command_with_user(command, user);
405 let final_command = wrap_command_with_log_footer(&effective_command, &shell, keep_alive);
406 let screen_args = vec![
407 "-dmS".to_string(),
408 session_name.to_string(),
409 "-L".to_string(),
410 "-c".to_string(),
411 screenrc_path.to_string_lossy().to_string(),
412 shell.clone(),
413 shell_arg,
414 final_command,
415 ];
416
417 if is_debug() {
418 eprintln!("[screen-isolation] Running: screen {:?}", screen_args);
419 eprintln!("[screen-isolation] screenrc: {}", screenrc_content.trim());
420 eprintln!("[screen-isolation] Log file: {}", log_file.display());
421 }
422
423 match Command::new("screen")
424 .args(&screen_args)
425 .envs(env.iter().map(|(k, v)| (k.as_str(), v.as_str())))
426 .status()
427 {
428 Ok(status) if status.success() => {
429 let mut message = format!(
430 "Command started in detached screen session: {}",
431 session_name
432 );
433 if keep_alive {
434 message.push_str("\nSession will stay alive after command completes.");
435 } else {
436 message.push_str("\nSession will exit automatically after command completes.");
437 }
438 message.push_str(&format!("\nReattach with: screen -r {}", session_name));
439 message.push_str(&format!("\nLive log: {}", log_file.display()));
440 IsolationResult {
441 success: true,
442 session_name: Some(session_name.to_string()),
443 message,
444 ..Default::default()
445 }
446 }
447 Ok(status) => IsolationResult {
448 success: false,
449 session_name: Some(session_name.to_string()),
450 message: format!(
451 "Failed to start screen session (exit code {})",
452 status.code().unwrap_or(-1)
453 ),
454 ..Default::default()
455 },
456 Err(e) => IsolationResult {
457 success: false,
458 session_name: Some(session_name.to_string()),
459 message: format!("Failed to start screen session: {}", e),
460 ..Default::default()
461 },
462 }
463}