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