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