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(command.split_whitespace().map(String::from));
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 success: exit_code == 0,
312 session_name: Some(session_name.to_string()),
313 container_id: None,
314 message: format!(
315 "Screen session \"{}\" exited with code {}",
316 session_name, exit_code
317 ),
318 exit_code: Some(exit_code),
319 output,
320 };
321 }
322
323 thread::sleep(check_interval);
324 waited += check_interval;
325
326 if waited >= max_wait {
327 cleanup();
328 return IsolationResult {
329 success: false,
330 session_name: Some(session_name.to_string()),
331 message: format!(
332 "Screen session \"{}\" timed out after {} seconds",
333 session_name,
334 max_wait.as_secs()
335 ),
336 exit_code: Some(1),
337 ..Default::default()
338 };
339 }
340 }
341}
342
343pub fn start_detached_screen_with_log_capture(
345 command: &str,
346 session_name: &str,
347 user: Option<&str>,
348 keep_alive: bool,
349 log_path: Option<&Path>,
350) -> IsolationResult {
351 let (shell, shell_arg) = get_shell();
352 let screen_temp_dir = get_temp_dir(&["isolation", "screen"]);
353 let log_file = log_path
354 .map(|p| p.to_path_buf())
355 .unwrap_or_else(|| screen_temp_dir.join(format!("screen-output-{}.log", session_name)));
356 if let Some(parent) = log_file.parent() {
357 let _ = fs::create_dir_all(parent);
358 }
359
360 let screenrc_path = screen_temp_dir.join(format!("screenrc-{}", session_name));
361 let screenrc_content = format!(
362 "logfile {}\nlogfile flush 0\ndeflog on\n",
363 log_file.display()
364 );
365 if let Err(e) = fs::write(&screenrc_path, &screenrc_content) {
366 if is_debug() {
367 eprintln!("[screen-isolation] Failed to create screenrc: {}", e);
368 }
369 return IsolationResult {
370 success: false,
371 session_name: Some(session_name.to_string()),
372 message: format!("Failed to create screenrc for logging: {}", e),
373 ..Default::default()
374 };
375 }
376
377 let effective_command = wrap_command_with_user(command, user);
378 let final_command = wrap_command_with_log_footer(&effective_command, &shell, keep_alive);
379 let screen_args = vec![
380 "-dmS".to_string(),
381 session_name.to_string(),
382 "-L".to_string(),
383 "-c".to_string(),
384 screenrc_path.to_string_lossy().to_string(),
385 shell.clone(),
386 shell_arg,
387 final_command,
388 ];
389
390 if is_debug() {
391 eprintln!("[screen-isolation] Running: screen {:?}", screen_args);
392 eprintln!("[screen-isolation] screenrc: {}", screenrc_content.trim());
393 eprintln!("[screen-isolation] Log file: {}", log_file.display());
394 }
395
396 match Command::new("screen").args(&screen_args).status() {
397 Ok(status) if status.success() => {
398 let mut message = format!(
399 "Command started in detached screen session: {}",
400 session_name
401 );
402 if keep_alive {
403 message.push_str("\nSession will stay alive after command completes.");
404 } else {
405 message.push_str("\nSession will exit automatically after command completes.");
406 }
407 message.push_str(&format!("\nReattach with: screen -r {}", session_name));
408 message.push_str(&format!("\nLive log: {}", log_file.display()));
409 IsolationResult {
410 success: true,
411 session_name: Some(session_name.to_string()),
412 message,
413 ..Default::default()
414 }
415 }
416 Ok(status) => IsolationResult {
417 success: false,
418 session_name: Some(session_name.to_string()),
419 message: format!(
420 "Failed to start screen session (exit code {})",
421 status.code().unwrap_or(-1)
422 ),
423 ..Default::default()
424 },
425 Err(e) => IsolationResult {
426 success: false,
427 session_name: Some(session_name.to_string()),
428 message: format!("Failed to start screen session: {}", e),
429 ..Default::default()
430 },
431 }
432}