rust_pty/unix/
signals.rs

1//! Signal handling for PTY operations.
2//!
3//! This module provides utilities for handling Unix signals relevant to
4//! PTY operations, particularly SIGWINCH (window size change) and SIGCHLD
5//! (child process state change).
6
7use std::io;
8use std::sync::Arc;
9use std::sync::atomic::{AtomicBool, Ordering};
10
11use signal_hook::consts::signal::{SIGCHLD, SIGWINCH};
12use signal_hook::iterator::Signals;
13use tokio::sync::mpsc;
14
15/// Signal types relevant to PTY operations.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PtySignalEvent {
18    /// Window size changed (SIGWINCH).
19    WindowChanged,
20    /// Child process state changed (SIGCHLD).
21    ChildStateChanged,
22}
23
24/// A handle to the signal handler that can be used to stop it.
25#[derive(Debug)]
26pub struct SignalHandle {
27    /// Flag to signal shutdown.
28    shutdown: Arc<AtomicBool>,
29}
30
31impl SignalHandle {
32    /// Signal the handler to stop.
33    pub fn shutdown(&self) {
34        self.shutdown.store(true, Ordering::SeqCst);
35    }
36}
37
38impl Drop for SignalHandle {
39    fn drop(&mut self) {
40        self.shutdown();
41    }
42}
43
44/// Start a background task that monitors signals and sends events.
45///
46/// Returns a receiver for signal events and a handle to control the handler.
47///
48/// # Errors
49///
50/// Returns an error if signal registration fails.
51pub fn start_signal_handler() -> io::Result<(mpsc::UnboundedReceiver<PtySignalEvent>, SignalHandle)>
52{
53    let mut signals = Signals::new([SIGWINCH, SIGCHLD])?;
54    let (tx, rx) = mpsc::unbounded_channel();
55    let shutdown = Arc::new(AtomicBool::new(false));
56    let shutdown_clone = Arc::clone(&shutdown);
57
58    std::thread::Builder::new()
59        .name("pty-signal-handler".into())
60        .spawn(move || {
61            for signal in signals.forever() {
62                if shutdown_clone.load(Ordering::SeqCst) {
63                    break;
64                }
65
66                let event = match signal {
67                    SIGWINCH => PtySignalEvent::WindowChanged,
68                    SIGCHLD => PtySignalEvent::ChildStateChanged,
69                    _ => continue,
70                };
71
72                if tx.send(event).is_err() {
73                    // Receiver dropped, exit
74                    break;
75                }
76            }
77        })?;
78
79    Ok((rx, SignalHandle { shutdown }))
80}
81
82/// Register a callback for SIGWINCH (window resize) signals.
83///
84/// The callback will be invoked each time the terminal window is resized.
85/// Returns a handle that must be kept alive for the handler to remain active.
86///
87/// # Errors
88///
89/// Returns an error if signal registration fails.
90pub fn on_window_change<F>(callback: F) -> io::Result<SignalHandle>
91where
92    F: Fn() + Send + 'static,
93{
94    let mut signals = Signals::new([SIGWINCH])?;
95    let shutdown = Arc::new(AtomicBool::new(false));
96    let shutdown_clone = Arc::clone(&shutdown);
97
98    std::thread::Builder::new()
99        .name("pty-sigwinch-handler".into())
100        .spawn(move || {
101            for _ in signals.forever() {
102                if shutdown_clone.load(Ordering::SeqCst) {
103                    break;
104                }
105                callback();
106            }
107        })?;
108
109    Ok(SignalHandle { shutdown })
110}
111
112/// Check if a signal number is SIGCHLD.
113#[must_use]
114pub const fn is_sigchld(signal: i32) -> bool {
115    signal == SIGCHLD
116}
117
118/// Check if a signal number is SIGWINCH.
119#[must_use]
120pub const fn is_sigwinch(signal: i32) -> bool {
121    signal == SIGWINCH
122}
123
124/// Get the signal number for SIGWINCH.
125#[must_use]
126pub const fn sigwinch() -> i32 {
127    SIGWINCH
128}
129
130/// Get the signal number for SIGCHLD.
131#[must_use]
132pub const fn sigchld() -> i32 {
133    SIGCHLD
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139
140    #[test]
141    fn signal_constants() {
142        assert!(is_sigchld(sigchld()));
143        assert!(is_sigwinch(sigwinch()));
144        assert!(!is_sigchld(sigwinch()));
145        assert!(!is_sigwinch(sigchld()));
146    }
147
148    #[test]
149    fn signal_handle_shutdown() {
150        let shutdown = Arc::new(AtomicBool::new(false));
151        let handle = SignalHandle {
152            shutdown: Arc::clone(&shutdown),
153        };
154
155        assert!(!shutdown.load(Ordering::SeqCst));
156        handle.shutdown();
157        assert!(shutdown.load(Ordering::SeqCst));
158    }
159}