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