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