1use std::env;
10use std::path::PathBuf;
11use std::process::{Command, Stdio};
12
13use crate::args_parser::generate_session_name;
14use crate::docker_cleanup::{
15 append_docker_container_cleanup_policy_message, build_docker_runtime_args,
16 docker_container_cleanup_instructions, get_docker_container_cleanup_policy,
17 remove_docker_container, should_cleanup_docker_container, spawn_attached_docker,
18 start_detached_docker_completion_watcher, DockerContainerCleanupPolicy,
19};
20
21#[derive(Debug, Default)]
23pub struct IsolationResult {
24 pub success: bool,
26 pub session_name: Option<String>,
28 pub container_id: Option<String>,
30 pub message: String,
32 pub exit_code: Option<i32>,
34 pub output: Option<String>,
36}
37
38#[derive(Debug, Clone)]
40pub struct IsolationOptions {
41 pub session: Option<String>,
43 pub image: Option<String>,
45 pub volumes: Vec<String>,
47 pub mounts: Vec<String>,
49 pub env: Vec<String>,
51 pub privileged: bool,
53 pub endpoint: Option<String>,
55 pub detached: bool,
57 pub user: Option<String>,
59 pub keep_alive: bool,
61 pub auto_remove_docker_container: bool,
63 pub always_cleanup_container: bool,
65 pub keep_container: bool,
67 pub keep_container_on_fail: bool,
69 pub shell: String,
71 pub log_path: Option<PathBuf>,
73}
74
75impl Default for IsolationOptions {
76 fn default() -> Self {
77 IsolationOptions {
78 session: None,
79 image: None,
80 volumes: Vec::new(),
81 mounts: Vec::new(),
82 env: Vec::new(),
83 privileged: false,
84 endpoint: None,
85 detached: false,
86 user: None,
87 keep_alive: false,
88 auto_remove_docker_container: false,
89 always_cleanup_container: false,
90 keep_container: false,
91 keep_container_on_fail: false,
92 shell: "auto".to_string(),
93 log_path: None,
94 }
95 }
96}
97
98pub fn is_command_available(command: &str) -> bool {
100 let check_cmd = if cfg!(windows) { "where" } else { "which" };
101 Command::new(check_cmd)
102 .arg(command)
103 .stdout(Stdio::null())
104 .stderr(Stdio::null())
105 .status()
106 .map(|s| s.success())
107 .unwrap_or(false)
108}
109
110pub fn get_shell() -> (String, String) {
112 if cfg!(windows) {
113 ("cmd.exe".to_string(), "/c".to_string())
114 } else {
115 let shell = env::var("SHELL").unwrap_or_else(|_| "/bin/sh".to_string());
116 (shell, "-c".to_string())
117 }
118}
119
120pub fn has_tty() -> bool {
122 atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout)
123}
124
125pub fn wrap_command_with_user(command: &str, user: Option<&str>) -> String {
127 match user {
128 Some(u) => {
129 let escaped = command.replace('\'', "'\\''");
131 format!("sudo -n -u {} sh -c '{}'", u, escaped)
132 }
133 None => command.to_string(),
134 }
135}
136
137const SHELL_NAMES: [&str; 8] = ["bash", "zsh", "sh", "fish", "ksh", "csh", "tcsh", "dash"];
140
141pub fn is_interactive_shell_command(command: &str) -> bool {
147 let parts: Vec<&str> = command.split_whitespace().collect();
148 if parts.is_empty() {
149 return false;
150 }
151 let basename = parts[0].rsplit('/').next().unwrap_or(parts[0]);
152 SHELL_NAMES.contains(&basename) && !parts.contains(&"-c")
153}
154
155pub fn is_shell_invocation_with_args(command: &str) -> bool {
158 let parts: Vec<&str> = command.split_whitespace().collect();
159 if parts.is_empty() {
160 return false;
161 }
162 let basename = parts[0].rsplit('/').next().unwrap_or(parts[0]);
163 SHELL_NAMES.contains(&basename) && parts.contains(&"-c")
164}
165
166pub fn build_shell_with_args_cmd_args(command: &str) -> Vec<String> {
170 let parts: Vec<&str> = command.split_whitespace().collect();
171 let c_idx = parts.iter().position(|&p| p == "-c");
172 match c_idx {
173 None => parts.iter().map(|s| s.to_string()).collect(),
174 Some(idx) => {
175 let script_arg = parts[idx + 1..].join(" ");
176 let mut result: Vec<String> = parts[..idx + 1].iter().map(|s| s.to_string()).collect();
177 if !script_arg.is_empty() {
178 result.push(script_arg);
179 }
180 result
181 }
182 }
183}
184
185fn get_shell_interactive_flag(shell_path: &str) -> Option<&'static str> {
187 let shell_name = shell_path.rsplit('/').next().unwrap_or(shell_path);
188 match shell_name {
189 "bash" => Some("-i"),
190 "zsh" => Some("-i"),
191 _ => None,
192 }
193}
194
195pub fn detect_shell_in_environment(environment: &str, options: &IsolationOptions) -> String {
199 let shell_preference = &options.shell;
200
201 if !shell_preference.is_empty() && shell_preference != "auto" {
203 if is_debug() {
204 eprintln!("[DEBUG] Using forced shell: {}", shell_preference);
205 }
206 return shell_preference.clone();
207 }
208
209 let shells_to_try = ["bash", "zsh", "sh"];
211
212 if environment == "docker" {
213 let image = match &options.image {
214 Some(i) => i.clone(),
215 None => return "sh".to_string(),
216 };
217
218 for shell in &shells_to_try {
219 let result = Command::new("docker")
220 .args([
221 "run",
222 "--rm",
223 &image,
224 "sh",
225 "-c",
226 &format!("command -v {}", shell),
227 ])
228 .stdout(Stdio::piped())
229 .stderr(Stdio::null())
230 .output();
231
232 if let Ok(output) = result {
233 if output.status.success() {
234 let detected = String::from_utf8_lossy(&output.stdout).trim().to_string();
235 if !detected.is_empty() {
236 if is_debug() {
237 eprintln!(
238 "[DEBUG] Detected shell in docker image {}: {}",
239 image, detected
240 );
241 }
242 return detected;
243 }
244 }
245 }
246 }
247
248 if is_debug() {
249 eprintln!(
250 "[DEBUG] Could not detect shell in docker image {}, falling back to sh",
251 image
252 );
253 }
254 return "sh".to_string();
255 }
256
257 if environment == "ssh" {
258 let endpoint = match &options.endpoint {
259 Some(e) => e.clone(),
260 None => return "sh".to_string(),
261 };
262
263 let check_cmd: Vec<String> = shells_to_try
265 .iter()
266 .map(|s| format!("command -v {}", s))
267 .collect();
268 let check_cmd_str = check_cmd.join(" || ");
269
270 let result = Command::new("ssh")
271 .args([&endpoint, &check_cmd_str])
272 .stdout(Stdio::piped())
273 .stderr(Stdio::null())
274 .output();
275
276 if let Ok(output) = result {
277 if output.status.success() {
278 let detected = String::from_utf8_lossy(&output.stdout).trim().to_string();
279 if !detected.is_empty() {
280 if is_debug() {
281 eprintln!(
282 "[DEBUG] Detected shell on SSH host {}: {}",
283 endpoint, detected
284 );
285 }
286 return detected;
287 }
288 }
289 }
290
291 if is_debug() {
292 eprintln!(
293 "[DEBUG] Could not detect shell on SSH host {}, falling back to sh",
294 endpoint
295 );
296 }
297 return "sh".to_string();
298 }
299
300 "sh".to_string()
301}
302
303#[path = "isolation_screen.rs"]
304pub mod isolation_screen;
305pub use self::isolation_screen::{get_screen_version, supports_logfile_option};
306
307pub fn run_in_screen(command: &str, options: &IsolationOptions) -> IsolationResult {
309 if !is_command_available("screen") {
310 return IsolationResult {
311 success: false,
312 message: "screen is not installed. Install it with: sudo apt-get install screen (Debian/Ubuntu) or brew install screen (macOS)".to_string(),
313 ..Default::default()
314 };
315 }
316
317 let session_name = options
318 .session
319 .clone()
320 .unwrap_or_else(|| generate_session_name(Some("screen")));
321
322 if options.detached {
323 isolation_screen::start_detached_screen_with_log_capture(
324 command,
325 &session_name,
326 options.user.as_deref(),
327 options.keep_alive,
328 options.log_path.as_deref(),
329 )
330 } else {
331 isolation_screen::run_screen_with_log_capture(
333 command,
334 &session_name,
335 options.user.as_deref(),
336 options.log_path.as_deref(),
337 )
338 }
339}
340
341pub fn run_in_tmux(command: &str, options: &IsolationOptions) -> IsolationResult {
343 if !is_command_available("tmux") {
344 return IsolationResult {
345 success: false,
346 message: "tmux is not installed. Install it with: sudo apt-get install tmux (Debian/Ubuntu) or brew install tmux (macOS)".to_string(),
347 ..Default::default()
348 };
349 }
350
351 let session_name = options
352 .session
353 .clone()
354 .unwrap_or_else(|| generate_session_name(Some("tmux")));
355
356 let (shell, _) = get_shell();
357 let effective_command = wrap_command_with_user(command, options.user.as_deref());
358
359 if options.detached {
360 let final_command = if options.log_path.is_some() {
361 crate::isolation::isolation_log::wrap_command_with_log_footer(
362 &effective_command,
363 &shell,
364 options.keep_alive,
365 )
366 } else if options.keep_alive {
367 format!("{}; exec {}", effective_command, shell)
368 } else {
369 effective_command.clone()
370 };
371
372 let status = if let Some(log_path) = options.log_path.as_ref() {
373 let start_status = Command::new("tmux")
374 .args(["new-session", "-d", "-s", &session_name, &shell])
375 .status();
376 if start_status.as_ref().is_ok_and(|s| s.success()) {
377 let pipe_command = format!(
378 "cat >> {}",
379 crate::isolation::isolation_log::shell_quote(&log_path.to_string_lossy())
380 );
381 let _ = Command::new("tmux")
382 .args(["pipe-pane", "-t", &session_name, "-o", &pipe_command])
383 .status();
384 Command::new("tmux")
385 .args(["send-keys", "-t", &session_name, &final_command, "C-m"])
386 .status()
387 } else {
388 start_status
389 }
390 } else {
391 Command::new("tmux")
392 .args(["new-session", "-d", "-s", &session_name, &final_command])
393 .status()
394 };
395
396 match status {
397 Ok(s) if s.success() => {
398 let mut message =
399 format!("Command started in detached tmux session: {}", session_name);
400 if options.keep_alive {
401 message.push_str("\nSession will stay alive after command completes.");
402 } else {
403 message.push_str("\nSession will exit automatically after command completes.");
404 }
405 message.push_str(&format!("\nReattach with: tmux attach -t {}", session_name));
406 if let Some(log_path) = options.log_path.as_ref() {
407 message.push_str(&format!("\nLive log: {}", log_path.display()));
408 }
409
410 IsolationResult {
411 success: true,
412 session_name: Some(session_name),
413 message,
414 ..Default::default()
415 }
416 }
417 _ => IsolationResult {
418 success: false,
419 session_name: Some(session_name),
420 message: "Failed to start tmux session".to_string(),
421 ..Default::default()
422 },
423 }
424 } else {
425 let output = Command::new("tmux")
427 .args(["new-session", "-s", &session_name, &effective_command])
428 .status();
429
430 match output {
431 Ok(status) => IsolationResult {
432 success: status.success(),
433 session_name: Some(session_name.clone()),
434 message: format!(
435 "Tmux session \"{}\" exited with code {}",
436 session_name,
437 status.code().unwrap_or(-1)
438 ),
439 exit_code: status.code(),
440 ..Default::default()
441 },
442 Err(e) => IsolationResult {
443 success: false,
444 session_name: Some(session_name),
445 message: format!("Failed to start tmux: {}", e),
446 ..Default::default()
447 },
448 }
449 }
450}
451
452pub fn run_in_ssh(command: &str, options: &IsolationOptions) -> IsolationResult {
454 if !is_command_available("ssh") {
455 return IsolationResult {
456 success: false,
457 message: "ssh is not installed".to_string(),
458 ..Default::default()
459 };
460 }
461
462 let endpoint = match &options.endpoint {
463 Some(e) => e.clone(),
464 None => {
465 return IsolationResult {
466 success: false,
467 message: "SSH isolation requires --endpoint option".to_string(),
468 ..Default::default()
469 };
470 }
471 };
472
473 let session_name = options
474 .session
475 .clone()
476 .unwrap_or_else(|| generate_session_name(Some("ssh")));
477
478 let shell_to_use = detect_shell_in_environment("ssh", options);
480 let shell_interactive_flag = get_shell_interactive_flag(&shell_to_use);
483
484 if options.detached {
485 let shell_invocation = if let Some(flag) = shell_interactive_flag {
488 format!("{} {}", shell_to_use, flag)
489 } else {
490 shell_to_use.clone()
491 };
492 let remote_command = format!(
493 "mkdir -p /tmp/start-command/logs/isolation/ssh && nohup {} -c {} > /tmp/start-command/logs/isolation/ssh/{}.log 2>&1 &",
494 shell_invocation,
495 shell_escape(command),
496 session_name
497 );
498 let ssh_args = vec![endpoint.as_str(), remote_command.as_str()];
499
500 if is_debug() {
501 eprintln!("[DEBUG] Running: ssh {:?}", ssh_args);
502 eprintln!("[DEBUG] shell: {}", shell_invocation);
503 }
504
505 let status = Command::new("ssh").args(&ssh_args).status();
506
507 match status {
508 Ok(s) if s.success() => IsolationResult {
509 success: true,
510 session_name: Some(session_name.clone()),
511 message: format!(
512 "Command started in detached SSH session on {}\nSession: {}\nView logs: ssh {} \"tail -f /tmp/start-command/logs/isolation/ssh/{}.log\"",
513 endpoint, session_name, endpoint, session_name
514 ),
515 ..Default::default()
516 },
517 _ => IsolationResult {
518 success: false,
519 session_name: Some(session_name),
520 message: "Failed to start SSH session".to_string(),
521 ..Default::default()
522 },
523 }
524 } else {
525 let mut ssh_cmd_args = vec![endpoint.clone(), shell_to_use.clone()];
528 if let Some(flag) = shell_interactive_flag {
529 ssh_cmd_args.push(flag.to_string());
530 }
531 ssh_cmd_args.push("-c".to_string());
532 ssh_cmd_args.push(command.to_string());
533
534 if is_debug() {
535 eprintln!("[DEBUG] Running: ssh {:?}", ssh_cmd_args);
536 eprintln!("[DEBUG] shell: {}", shell_to_use);
537 }
538
539 let status = Command::new("ssh").args(&ssh_cmd_args).status();
540
541 match status {
542 Ok(s) => IsolationResult {
543 success: s.success(),
544 session_name: Some(session_name.clone()),
545 message: format!(
546 "SSH session \"{}\" on {} exited with code {}",
547 session_name,
548 endpoint,
549 s.code().unwrap_or(-1)
550 ),
551 exit_code: s.code(),
552 ..Default::default()
553 },
554 Err(e) => IsolationResult {
555 success: false,
556 session_name: Some(session_name),
557 message: format!("Failed to start SSH: {}", e),
558 ..Default::default()
559 },
560 }
561 }
562}
563
564pub fn docker_image_exists(image: &str) -> bool {
566 Command::new("docker")
567 .args(["image", "inspect", image])
568 .stdout(Stdio::null())
569 .stderr(Stdio::null())
570 .status()
571 .map(|s| s.success())
572 .unwrap_or(false)
573}
574
575pub fn docker_pull_image(image: &str, log_path: Option<&PathBuf>) -> (bool, String) {
586 use crate::isolation::isolation_log::{append_log_file, get_timestamp};
587 use std::io::{BufRead, BufReader};
588 use std::time::Instant;
589
590 println!(
592 "{}",
593 crate::output_blocks::create_virtual_command_block(&format!("docker pull {}", image))
594 );
595 println!();
596
597 let prep_start = Instant::now();
600 if let Some(path) = log_path {
601 append_log_file(
602 path,
603 &format!(
604 "$ docker pull {}\nPreparing image {}… ({})\n",
605 image,
606 image,
607 get_timestamp()
608 ),
609 );
610 }
611
612 let mut child = match Command::new("docker")
613 .args(["pull", image])
614 .stdout(Stdio::piped())
615 .stderr(Stdio::piped())
616 .spawn()
617 {
618 Ok(c) => c,
619 Err(e) => {
620 let error_msg = format!("Failed to run docker pull: {}", e);
621 eprintln!("{}", error_msg);
622 if let Some(path) = log_path {
623 append_log_file(
624 path,
625 &format!(
626 "{}\nImage preparation failed ({:.1}s)\n",
627 error_msg,
628 prep_start.elapsed().as_secs_f64()
629 ),
630 );
631 }
632 println!();
633 println!(
634 "{}",
635 crate::output_blocks::create_virtual_command_result(false)
636 );
637 return (false, error_msg);
638 }
639 };
640
641 let mut output = String::new();
642
643 if let Some(stdout) = child.stdout.take() {
645 let reader = BufReader::new(stdout);
646 for line in reader.lines().map_while(Result::ok) {
647 println!("{}", line);
648 if let Some(path) = log_path {
649 append_log_file(path, &format!("{}\n", line));
650 }
651 output.push_str(&line);
652 output.push('\n');
653 }
654 }
655
656 if let Some(stderr) = child.stderr.take() {
658 let reader = BufReader::new(stderr);
659 for line in reader.lines().map_while(Result::ok) {
660 eprintln!("{}", line);
661 if let Some(path) = log_path {
662 append_log_file(path, &format!("{}\n", line));
663 }
664 output.push_str(&line);
665 output.push('\n');
666 }
667 }
668
669 let success = child.wait().map(|s| s.success()).unwrap_or(false);
670
671 if let Some(path) = log_path {
674 let duration = prep_start.elapsed().as_secs_f64();
675 append_log_file(
676 path,
677 &if success {
678 format!("Image ready ({:.1}s)\n", duration)
679 } else {
680 format!("Image preparation failed ({:.1}s)\n", duration)
681 },
682 );
683 }
684
685 println!();
688 println!(
689 "{}",
690 crate::output_blocks::create_virtual_command_result(success)
691 );
692 println!("{}", crate::output_blocks::create_timeline_separator());
693
694 (success, output)
695}
696
697pub fn run_in_docker(command: &str, options: &IsolationOptions) -> IsolationResult {
699 if !is_command_available("docker") {
700 return IsolationResult {
701 success: false,
702 message:
703 "docker is not installed. Install Docker from https://docs.docker.com/get-docker/"
704 .to_string(),
705 ..Default::default()
706 };
707 }
708
709 let image = match &options.image {
710 Some(i) => i.clone(),
711 None => {
712 return IsolationResult {
713 success: false,
714 message: "Docker isolation requires --image option".to_string(),
715 ..Default::default()
716 };
717 }
718 };
719
720 if !docker_image_exists(&image) {
724 let (pull_success, _pull_output) = docker_pull_image(&image, options.log_path.as_ref());
725 if !pull_success {
726 return IsolationResult {
727 success: false,
728 message: format!("Failed to pull Docker image: {}", image),
729 exit_code: Some(1),
730 ..Default::default()
731 };
732 }
733 }
734
735 let container_name = options
736 .session
737 .clone()
738 .unwrap_or_else(|| generate_session_name(Some("docker")));
739 let cleanup_policy = get_docker_container_cleanup_policy(options);
740
741 let shell_to_use = detect_shell_in_environment("docker", options);
743 let shell_interactive_flag = get_shell_interactive_flag(&shell_to_use);
746
747 println!("{}", crate::output_blocks::create_command_line(command));
749 println!();
750
751 if options.detached {
752 let effective_command = if options.keep_alive {
753 format!("{}; exec {}", command, shell_to_use)
754 } else {
755 command.to_string()
756 };
757
758 let mut args = vec!["run", "-d", "--name", &container_name];
759
760 if let Some(ref user) = options.user {
761 args.push("--user");
762 args.push(user);
763 }
764
765 args.extend(build_docker_runtime_args(options));
766
767 args.push(&image);
768 args.push(&shell_to_use);
769 if let Some(flag) = shell_interactive_flag {
770 args.push(flag);
771 }
772 args.extend(&["-c", &effective_command]);
773
774 if is_debug() {
775 eprintln!("[DEBUG] Running: docker {:?}", args);
776 eprintln!("[DEBUG] shell: {}", shell_to_use);
777 }
778
779 match Command::new("docker").args(&args).output() {
780 Ok(output) if output.status.success() => {
781 let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
782
783 if let Some(log_path) = options.log_path.as_ref() {
784 start_detached_docker_completion_watcher(
785 &container_name,
786 cleanup_policy,
787 Some(log_path),
788 );
789 } else {
790 start_detached_docker_completion_watcher(&container_name, cleanup_policy, None);
791 }
792
793 let mut message = format!(
794 "Command started in detached docker container: {}",
795 container_name
796 );
797 message.push_str(&format!(
798 "\nContainer ID: {}",
799 &container_id[..12.min(container_id.len())]
800 ));
801 if options.keep_alive {
802 message.push_str("\nContainer will stay alive after command completes.");
803 } else {
804 message
805 .push_str("\nContainer will exit automatically after command completes.");
806 }
807 append_docker_container_cleanup_policy_message(
808 &mut message,
809 &container_name,
810 cleanup_policy,
811 );
812 message.push_str(&format!("\nAttach with: docker attach {}", container_name));
813 message.push_str(&format!("\nView logs: docker logs {}", container_name));
814 if let Some(log_path) = options.log_path.as_ref() {
815 message.push_str(&format!("\nLive log: {}", log_path.display()));
816 }
817
818 IsolationResult {
819 success: true,
820 session_name: Some(container_name),
821 container_id: Some(container_id),
822 message,
823 ..Default::default()
824 }
825 }
826 Ok(output) => {
827 let stderr = String::from_utf8_lossy(&output.stderr);
828 IsolationResult {
829 success: false,
830 session_name: Some(container_name),
831 message: format!("Failed to start docker container: {}", stderr),
832 ..Default::default()
833 }
834 }
835 Err(e) => IsolationResult {
836 success: false,
837 session_name: Some(container_name),
838 message: format!("Failed to run docker: {}", e),
839 ..Default::default()
840 },
841 }
842 } else {
843 let mut args = vec!["run"];
845 args.push(if has_tty() { "-it" } else { "-i" });
846 args.extend(["--name", &container_name]);
847
848 if let Some(ref user) = options.user {
849 args.push("--user");
850 args.push(user);
851 }
852
853 args.extend(build_docker_runtime_args(options));
854
855 if is_debug() {
856 eprintln!("[DEBUG] shell: {}", shell_to_use);
857 }
858
859 args.push(&image);
860 args.push(&shell_to_use);
861 if let Some(flag) = shell_interactive_flag {
862 args.push(flag);
863 }
864 args.extend(&["-c", command]);
865
866 let child = spawn_attached_docker(&args, options.log_path.as_ref());
867
868 match child {
869 Ok(child) => match child.wait() {
870 Ok(s) => {
871 let exit_code = s.code().unwrap_or(1);
872 let mut message = format!(
873 "Docker container \"{}\" exited with code {}",
874 container_name, exit_code
875 );
876 if should_cleanup_docker_container(cleanup_policy, exit_code) {
877 if remove_docker_container(&container_name, options.log_path.as_ref()) {
878 message.push_str("\nContainer removed after completion.");
879 } else {
880 message
881 .push_str("\nWarning: failed to remove container automatically.");
882 message.push_str(&format!(
883 "\nRemove when done: docker rm -f {}",
884 container_name
885 ));
886 }
887 } else if cleanup_policy == DockerContainerCleanupPolicy::Keep {
888 message.push('\n');
889 message.push_str(&docker_container_cleanup_instructions(&container_name));
890 } else if cleanup_policy == DockerContainerCleanupPolicy::KeepOnFail {
891 message.push_str("\nContainer kept because the command failed.");
892 message.push_str(&format!(
893 "\nRemove when done: docker rm -f {}",
894 container_name
895 ));
896 }
897
898 IsolationResult {
899 success: s.success(),
900 session_name: Some(container_name.clone()),
901 message,
902 exit_code: Some(exit_code),
903 ..Default::default()
904 }
905 }
906 Err(e) => IsolationResult {
907 success: false,
908 session_name: Some(container_name),
909 message: format!("Failed to wait for docker: {}", e),
910 ..Default::default()
911 },
912 },
913 Err(e) => IsolationResult {
914 success: false,
915 session_name: Some(container_name),
916 message: format!("Failed to start docker: {}", e),
917 ..Default::default()
918 },
919 }
920 }
921}
922
923pub fn run_isolated(backend: &str, command: &str, options: &IsolationOptions) -> IsolationResult {
925 match backend {
926 "screen" => run_in_screen(command, options),
927 "tmux" => run_in_tmux(command, options),
928 "docker" => run_in_docker(command, options),
929 "ssh" => run_in_ssh(command, options),
930 _ => IsolationResult {
931 success: false,
932 message: format!("Unknown isolation backend: {}", backend),
933 ..Default::default()
934 },
935 }
936}
937
938pub fn run_as_isolated_user(command: &str, username: &str) -> IsolationResult {
940 let status = Command::new("sudo")
941 .args(["-n", "-u", username, "sh", "-c", command])
942 .status();
943
944 match status {
945 Ok(s) => IsolationResult {
946 success: s.success(),
947 message: format!(
948 "Command completed as user \"{}\" with exit code {}",
949 username,
950 s.code().unwrap_or(-1)
951 ),
952 exit_code: s.code(),
953 ..Default::default()
954 },
955 Err(e) => IsolationResult {
956 success: false,
957 message: format!("Failed to run as user \"{}\": {}", username, e),
958 exit_code: Some(1),
959 ..Default::default()
960 },
961 }
962}
963
964#[path = "isolation_log.rs"]
965pub mod isolation_log;
966pub use self::isolation_log::{
967 append_log_file, create_log_footer, create_log_header, create_log_path,
968 create_log_path_for_execution, generate_log_filename, get_default_docker_image, get_log_dir,
969 get_temp_dir, get_temp_root, get_timestamp, write_log_file, LogHeaderParams,
970};
971
972fn is_debug() -> bool {
973 env::var("START_DEBUG").is_ok_and(|v| v == "1" || v == "true")
974}
975
976fn shell_escape(command: &str) -> String {
977 format!("'{}'", command.replace('\'', "'\\''"))
978}
979
980#[path = "atty.rs"]
981mod atty;
982
983#[cfg(test)]
984#[path = "isolation_cases.rs"]
985mod tests;