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