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