Rust · 4511 bytes Raw Blame History
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