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