| 1 | use nix::sys::signal::{signal, SigHandler, Signal}; |
| 2 | use nix::sys::wait::{waitpid, WaitPidFlag, WaitStatus}; |
| 3 | use nix::unistd::Pid; |
| 4 | use std::sync::atomic::{AtomicBool, Ordering}; |
| 5 | |
| 6 | /// Flag indicating SIGWINCH was received (terminal resized) |
| 7 | pub static SIGWINCH_RECEIVED: AtomicBool = AtomicBool::new(false); |
| 8 | |
| 9 | /// Flag indicating SIGHUP was received (terminal closed) |
| 10 | pub static SIGHUP_RECEIVED: AtomicBool = AtomicBool::new(false); |
| 11 | |
| 12 | /// SIGWINCH handler - sets flag for main loop to check |
| 13 | extern "C" fn handle_sigwinch(_: i32) { |
| 14 | SIGWINCH_RECEIVED.store(true, Ordering::SeqCst); |
| 15 | } |
| 16 | |
| 17 | /// SIGHUP handler - sets flag for main loop to check |
| 18 | extern "C" fn handle_sighup(_: i32) { |
| 19 | SIGHUP_RECEIVED.store(true, Ordering::SeqCst); |
| 20 | } |
| 21 | |
| 22 | /// Setup job control signal handlers |
| 23 | /// |
| 24 | /// This configures the shell to handle: |
| 25 | /// - SIGCHLD: Reap completed/stopped child processes |
| 26 | /// - SIGINT: Interrupt (Ctrl-C) - handled by foreground job |
| 27 | /// - SIGTSTP: Suspend (Ctrl-Z) - handled by foreground job |
| 28 | /// - SIGWINCH: Terminal resize - flagged for notification |
| 29 | /// - SIGHUP: Terminal hangup - flagged for cleanup |
| 30 | /// - SIGQUIT: Quit (Ctrl-\) - ignored by shell |
| 31 | /// |
| 32 | /// Note: The actual signal handling is done via polling in the main loop, |
| 33 | /// not via signal handlers (to avoid async-signal-safety issues). |
| 34 | pub fn setup_job_control_signals() -> Result<(), nix::Error> { |
| 35 | unsafe { |
| 36 | // Set SIGCHLD to default (we'll poll for child status changes) |
| 37 | signal(Signal::SIGCHLD, SigHandler::SigDfl)?; |
| 38 | |
| 39 | // SIGWINCH: Handle terminal resize |
| 40 | signal(Signal::SIGWINCH, SigHandler::Handler(handle_sigwinch))?; |
| 41 | |
| 42 | // SIGHUP: Handle terminal hangup (e.g., closing terminal window) |
| 43 | signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup))?; |
| 44 | |
| 45 | // SIGQUIT (Ctrl-\): Ignore in shell, let foreground job handle it |
| 46 | signal(Signal::SIGQUIT, SigHandler::SigIgn)?; |
| 47 | } |
| 48 | |
| 49 | // SIGINT and SIGTSTP are handled by the foreground job |
| 50 | // The shell ignores them (they're set up in terminal::setup_shell_terminal) |
| 51 | |
| 52 | Ok(()) |
| 53 | } |
| 54 | |
| 55 | /// Check if terminal was resized |
| 56 | pub fn check_sigwinch() -> bool { |
| 57 | SIGWINCH_RECEIVED.swap(false, Ordering::SeqCst) |
| 58 | } |
| 59 | |
| 60 | /// Check if SIGHUP was received |
| 61 | pub fn check_sighup() -> bool { |
| 62 | SIGHUP_RECEIVED.swap(false, Ordering::SeqCst) |
| 63 | } |
| 64 | |
| 65 | /// Check for completed or stopped child processes |
| 66 | /// |
| 67 | /// This should be called periodically (e.g., before displaying a prompt) |
| 68 | /// to reap zombie processes and update job states. |
| 69 | /// |
| 70 | /// Returns a list of (pid, status) pairs for processes that changed state. |
| 71 | pub fn check_children() -> Vec<(Pid, WaitStatus)> { |
| 72 | let mut statuses = Vec::new(); |
| 73 | |
| 74 | loop { |
| 75 | // Use WNOHANG to avoid blocking, and WUNTRACED to catch stopped processes |
| 76 | let flags = WaitPidFlag::WNOHANG | WaitPidFlag::WUNTRACED | WaitPidFlag::WCONTINUED; |
| 77 | |
| 78 | match waitpid(Pid::from_raw(-1), Some(flags)) { |
| 79 | Ok(WaitStatus::StillAlive) => break, // No more children have changed state |
| 80 | Ok(status) => { |
| 81 | // Extract PID from status |
| 82 | if let Some(pid) = status.pid() { |
| 83 | statuses.push((pid, status)); |
| 84 | } |
| 85 | } |
| 86 | Err(_) => break, // No children or error |
| 87 | } |
| 88 | } |
| 89 | |
| 90 | statuses |
| 91 | } |
| 92 | |
| 93 | /// Wait for a specific process to change state |
| 94 | /// |
| 95 | /// This blocks until the process exits, is stopped, or is continued. |
| 96 | /// Used when waiting for a foreground job to complete. |
| 97 | pub fn wait_for_process(pid: Pid) -> Result<WaitStatus, nix::Error> { |
| 98 | let flags = WaitPidFlag::WUNTRACED | WaitPidFlag::WCONTINUED; |
| 99 | waitpid(pid, Some(flags)) |
| 100 | } |
| 101 | |
| 102 | /// Wait for any process in a process group |
| 103 | /// |
| 104 | /// This blocks until any process in the group changes state. |
| 105 | pub fn wait_for_process_group(pgid: Pid) -> Result<WaitStatus, nix::Error> { |
| 106 | let flags = WaitPidFlag::WUNTRACED | WaitPidFlag::WCONTINUED; |
| 107 | // Negative PID means wait for process group |
| 108 | waitpid(Pid::from_raw(-pgid.as_raw()), Some(flags)) |
| 109 | } |
| 110 | |
| 111 | #[cfg(test)] |
| 112 | mod tests { |
| 113 | use super::*; |
| 114 | |
| 115 | #[test] |
| 116 | fn test_setup_signals() { |
| 117 | // Just verify it doesn't error |
| 118 | let result = setup_job_control_signals(); |
| 119 | assert!(result.is_ok()); |
| 120 | } |
| 121 | |
| 122 | #[test] |
| 123 | fn test_check_children_no_block() { |
| 124 | // This should return immediately even if there are no children |
| 125 | let statuses = check_children(); |
| 126 | // We can't assert much about the result since it depends on system state |
| 127 | // But we can verify it doesn't panic |
| 128 | let _ = statuses; |
| 129 | } |
| 130 | } |
| 131 |