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