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