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)]
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!("{}", crate::output_blocks::create_command_line(command));
712 println!();
713
714 let needs_network_setup = docker_networks(options).len() > 1;
715
716 if options.detached {
717 let effective_command = if options.keep_alive {
718 format!("{}; exec {}", command, shell_to_use)
719 } else {
720 command.to_string()
721 };
722
723 let mut args = vec![if needs_network_setup { "create" } else { "run" }];
724 if !needs_network_setup {
725 args.push("-d");
726 }
727 args.extend(["--name", &container_name]);
728
729 if let Some(ref user) = options.user {
730 args.push("--user");
731 args.push(user);
732 }
733
734 args.extend(build_docker_runtime_args(options));
735
736 args.push(&image);
737 args.push(&shell_to_use);
738 if let Some(flag) = shell_interactive_flag {
739 args.push(flag);
740 }
741 args.extend(&["-c", &effective_command]);
742
743 if is_debug() {
744 eprintln!("[DEBUG] Running: docker {:?}", args);
745 eprintln!("[DEBUG] shell: {}", shell_to_use);
746 }
747
748 match Command::new("docker").args(&args).output() {
749 Ok(output) if output.status.success() => {
750 let container_id = String::from_utf8_lossy(&output.stdout).trim().to_string();
751
752 if needs_network_setup {
753 let setup_result = connect_and_start(&container_name, options);
754 if let Err(error) = setup_result {
755 if !container_existed_before_launch {
756 remove_docker_container(&container_name, options.log_path.as_ref());
757 }
758 return IsolationResult {
759 success: false,
760 session_name: Some(container_name),
761 message: format!("Failed to start docker container: {}", error),
762 ..Default::default()
763 };
764 }
765 }
766
767 if let Some(log_path) = options.log_path.as_ref() {
768 start_detached_docker_completion_watcher(
769 &container_name,
770 cleanup_policy,
771 Some(log_path),
772 );
773 } else {
774 start_detached_docker_completion_watcher(&container_name, cleanup_policy, None);
775 }
776
777 let mut message = format!(
778 "Command started in detached docker container: {}",
779 container_name
780 );
781 message.push_str(&format!(
782 "\nContainer ID: {}",
783 &container_id[..12.min(container_id.len())]
784 ));
785 if options.keep_alive {
786 message.push_str("\nContainer will stay alive after command completes.");
787 } else {
788 message
789 .push_str("\nContainer will exit automatically after command completes.");
790 }
791 append_docker_container_cleanup_policy_message(
792 &mut message,
793 &container_name,
794 cleanup_policy,
795 );
796 message.push_str(&format!("\nAttach with: docker attach {}", container_name));
797 message.push_str(&format!("\nView logs: docker logs {}", container_name));
798 if let Some(log_path) = options.log_path.as_ref() {
799 message.push_str(&format!("\nLive log: {}", log_path.display()));
800 }
801
802 IsolationResult {
803 success: true,
804 session_name: Some(container_name),
805 container_id: Some(container_id),
806 message,
807 ..Default::default()
808 }
809 }
810 Ok(output) => {
811 let stderr = String::from_utf8_lossy(&output.stderr);
812 if !container_existed_before_launch
813 && crate::docker_cleanup::read_docker_container_status(&container_name)
814 .as_deref()
815 == Some("created")
816 {
817 remove_docker_container(&container_name, options.log_path.as_ref());
818 }
819 IsolationResult {
820 success: false,
821 session_name: Some(container_name),
822 message: format!("Failed to start docker container: {}", stderr),
823 ..Default::default()
824 }
825 }
826 Err(e) => IsolationResult {
827 success: false,
828 session_name: Some(container_name),
829 message: format!("Failed to run docker: {}", e),
830 ..Default::default()
831 },
832 }
833 } else {
834 let mut args = vec![if needs_network_setup { "create" } else { "run" }];
836 args.push(if has_tty() { "-it" } else { "-i" });
837 args.extend(["--name", &container_name]);
838
839 if let Some(ref user) = options.user {
840 args.push("--user");
841 args.push(user);
842 }
843
844 args.extend(build_docker_runtime_args(options));
845
846 if is_debug() {
847 eprintln!("[DEBUG] shell: {}", shell_to_use);
848 }
849
850 args.push(&image);
851 args.push(&shell_to_use);
852 if let Some(flag) = shell_interactive_flag {
853 args.push(flag);
854 }
855 args.extend(&["-c", command]);
856
857 if needs_network_setup {
858 let setup_result = create_and_connect(&args, &container_name, options);
859 if let Err(error) = setup_result {
860 if !container_existed_before_launch {
861 remove_docker_container(&container_name, options.log_path.as_ref());
862 }
863 return IsolationResult {
864 success: false,
865 session_name: Some(container_name),
866 message: format!("Failed to start docker container: {}", error),
867 ..Default::default()
868 };
869 }
870 }
871
872 let start_args = ["start", "-a", "-i", &container_name];
873 let child = if needs_network_setup {
874 spawn_attached_docker(&start_args, options.log_path.as_ref())
875 } else {
876 spawn_attached_docker(&args, options.log_path.as_ref())
877 };
878
879 match child {
880 Ok(child) => match child.wait() {
881 Ok(s) => {
882 let exit_code = s.code().unwrap_or(1);
883 let mut message = format!(
884 "Docker container \"{}\" exited with code {}",
885 container_name, exit_code
886 );
887 append_attached_docker_cleanup_message(
888 &mut message,
889 &container_name,
890 cleanup_policy,
891 exit_code,
892 options.log_path.as_ref(),
893 container_existed_before_launch,
894 );
895
896 IsolationResult {
897 success: s.success(),
898 session_name: Some(container_name.clone()),
899 message,
900 exit_code: Some(exit_code),
901 ..Default::default()
902 }
903 }
904 Err(e) => IsolationResult {
905 success: false,
906 session_name: Some(container_name),
907 message: format!("Failed to wait for docker: {}", e),
908 ..Default::default()
909 },
910 },
911 Err(e) => IsolationResult {
912 success: false,
913 session_name: Some(container_name),
914 message: format!("Failed to start docker: {}", e),
915 ..Default::default()
916 },
917 }
918 }
919}
920
921pub fn run_isolated(backend: &str, command: &str, options: &IsolationOptions) -> IsolationResult {
923 match backend {
924 "screen" => run_in_screen(command, options),
925 "tmux" => run_in_tmux(command, options),
926 "docker" => run_in_docker(command, options),
927 "ssh" => run_in_ssh(command, options),
928 _ => IsolationResult {
929 success: false,
930 message: format!("Unknown isolation backend: {}", backend),
931 ..Default::default()
932 },
933 }
934}
935
936pub fn run_as_isolated_user(command: &str, username: &str) -> IsolationResult {
938 let status = Command::new("sudo")
939 .args(["-n", "-u", username, "sh", "-c", command])
940 .status();
941
942 match status {
943 Ok(s) => IsolationResult {
944 success: s.success(),
945 message: format!(
946 "Command completed as user \"{}\" with exit code {}",
947 username,
948 s.code().unwrap_or(-1)
949 ),
950 exit_code: s.code(),
951 ..Default::default()
952 },
953 Err(e) => IsolationResult {
954 success: false,
955 message: format!("Failed to run as user \"{}\": {}", username, e),
956 exit_code: Some(1),
957 ..Default::default()
958 },
959 }
960}
961
962#[path = "isolation_log.rs"]
963pub mod isolation_log;
964#[path = "isolation_shell.rs"]
965mod isolation_shell;
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};
971pub use isolation_shell::{
972 build_shell_with_args_cmd_args, is_interactive_shell_command, is_shell_invocation_with_args,
973};
974
975fn is_debug() -> bool {
976 env::var("START_DEBUG").is_ok_and(|v| v == "1" || v == "true")
977}
978
979fn shell_escape(command: &str) -> String {
980 format!("'{}'", command.replace('\'', "'\\''"))
981}
982
983#[path = "atty.rs"]
984mod atty;
985
986#[cfg(test)]
987#[path = "isolation_cases.rs"]
988mod tests;