| 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 |