Rust · 5677 bytes Raw Blame History
1 // Terminal-aware command execution with proper signal handling
2
3 #[cfg(unix)]
4 pub mod unix {
5 use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal};
6 use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus};
7 use nix::unistd::{setpgid, tcsetpgrp, Pid};
8 use std::io;
9 use std::os::unix::process::{CommandExt, ExitStatusExt};
10 use std::process::Command;
11
12 use super::super::{ExecutionError, ExecutionResult, JobControlInfo};
13
14 /// Initialize signal handling for the shell
15 /// The shell should ignore SIGINT and SIGTSTP so that Ctrl+C and Ctrl+Z
16 /// only affect the foreground process, not the shell itself
17 pub fn setup_shell_signals() -> Result<(), ExecutionError> {
18 unsafe {
19 // Ignore SIGINT (Ctrl+C)
20 let handler = SigHandler::SigIgn;
21 let sig_action = SigAction::new(handler, SaFlags::empty(), SigSet::empty());
22 signal::sigaction(Signal::SIGINT, &sig_action)
23 .map_err(|e| ExecutionError::IoError(io::Error::new(io::ErrorKind::Other, e)))?;
24
25 // Ignore SIGTSTP (Ctrl+Z)
26 signal::sigaction(Signal::SIGTSTP, &sig_action)
27 .map_err(|e| ExecutionError::IoError(io::Error::new(io::ErrorKind::Other, e)))?;
28
29 // Ignore SIGTTOU (background writes to terminal)
30 signal::sigaction(Signal::SIGTTOU, &sig_action)
31 .map_err(|e| ExecutionError::IoError(io::Error::new(io::ErrorKind::Other, e)))?;
32 }
33 Ok(())
34 }
35
36 /// Execute a command with proper terminal and signal handling
37 pub fn execute_with_terminal_control(
38 mut command: Command,
39 interactive: bool,
40 ) -> Result<ExecutionResult, ExecutionError> {
41 if !interactive {
42 // Non-interactive mode: just run the command normally
43 let status = command.status()?;
44 return Ok(ExecutionResult {
45 exit_status: status,
46 job_control: None,
47 });
48 }
49
50 // Interactive mode: set up process groups and terminal control
51 unsafe {
52 command.pre_exec(|| {
53 // Child process: restore default signal handlers
54 let handler = SigHandler::SigDfl;
55 let sig_action = SigAction::new(handler, SaFlags::empty(), SigSet::empty());
56
57 signal::sigaction(Signal::SIGINT, &sig_action)?;
58 signal::sigaction(Signal::SIGTSTP, &sig_action)?;
59 signal::sigaction(Signal::SIGTTOU, &sig_action)?;
60
61 // Put the child in its own process group
62 setpgid(Pid::from_raw(0), Pid::from_raw(0))?;
63
64 Ok(())
65 });
66 }
67
68 // Spawn the child process
69 let child = command.spawn()?;
70 let child_pid = Pid::from_raw(child.id() as i32);
71
72 // Set the child's process group (belt and suspenders)
73 let _ = setpgid(child_pid, child_pid);
74
75 // Give the child's process group control of the terminal
76 let stdin = std::io::stdin();
77 let _ = tcsetpgrp(&stdin, child_pid);
78
79 // Wait for the child to complete
80 let (status, job_control) = loop {
81 match waitpid(child_pid, Some(WaitPidFlag::WUNTRACED)) {
82 Ok(WaitStatus::Exited(_, code)) => {
83 break (std::process::ExitStatus::from_raw(code << 8), None);
84 }
85 Ok(WaitStatus::Signaled(_, signal, _)) => {
86 // Child was killed by a signal
87 break (std::process::ExitStatus::from_raw(signal as i32 + 128), None);
88 }
89 Ok(WaitStatus::Stopped(_, _)) => {
90 // Child was stopped (Ctrl+Z)
91 // Return job control info so the shell can add it to the job list
92 let job_control = Some(JobControlInfo {
93 pid: child_pid,
94 pgid: child_pid, // Child is its own process group leader
95 stopped: true,
96 });
97 break (std::process::ExitStatus::from_raw(148), job_control); // SIGTSTP + 128
98 }
99 Ok(WaitStatus::Continued(_)) => {
100 // Child continued, keep waiting
101 continue;
102 }
103 Ok(_) => {
104 // Other status, keep waiting
105 continue;
106 }
107 Err(nix::errno::Errno::EINTR) => {
108 // Interrupted by signal, try again
109 continue;
110 }
111 Err(e) => {
112 return Err(ExecutionError::IoError(io::Error::new(
113 io::ErrorKind::Other,
114 e,
115 )));
116 }
117 }
118 };
119
120 // Take back terminal control
121 let stdin = std::io::stdin();
122 let shell_pgid = Pid::from_raw(std::process::id() as i32);
123 let _ = tcsetpgrp(&stdin, shell_pgid);
124
125 Ok(ExecutionResult {
126 exit_status: status,
127 job_control,
128 })
129 }
130 }
131
132 #[cfg(not(unix))]
133 pub mod non_unix {
134 use std::process::Command;
135
136 use super::super::{ExecutionError, ExecutionResult};
137
138 pub fn setup_shell_signals() -> Result<(), ExecutionError> {
139 // No-op on non-Unix systems
140 Ok(())
141 }
142
143 pub fn execute_with_terminal_control(
144 mut command: Command,
145 _interactive: bool,
146 ) -> Result<ExecutionResult, ExecutionError> {
147 let status = command.status()?;
148 Ok(ExecutionResult {
149 exit_status: status,
150 })
151 }
152 }
153