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