Skip to main content

kaish_kernel/
terminal.rs

1//! Terminal control and job control for interactive mode.
2//!
3//! Handles process group management, terminal ownership, and
4//! foreground wait with WUNTRACED support for Ctrl-Z (SIGTSTP).
5//!
6//! All functionality is `#[cfg(unix)]` — non-Unix platforms get stubs.
7//!
8//! Signal disposition (`sigaction`) requires unsafe per POSIX. This is the only
9//! module in kaish that uses unsafe code, and it's limited to well-understood
10//! signal-handling patterns that every shell must perform.
11
12#[cfg(unix)]
13#[allow(unsafe_code)]
14mod unix {
15    use std::os::unix::io::BorrowedFd;
16
17    use nix::sys::signal::{self, SigHandler, Signal};
18    use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
19    use nix::unistd::{self, Pid, tcsetpgrp};
20
21    /// Result of waiting for a foreground process.
22    #[derive(Debug)]
23    pub enum WaitResult {
24        /// Process exited with a status code.
25        Exited(i32),
26        /// Process was killed by a signal.
27        Signaled(i32),
28        /// Process was stopped (e.g., SIGTSTP from Ctrl-Z).
29        Stopped(Signal),
30    }
31
32    /// Terminal state for interactive job control.
33    ///
34    /// Created once at REPL startup. Manages signal disposition and
35    /// terminal foreground process group.
36    pub struct TerminalState {
37        /// The shell's own process group ID.
38        shell_pgid: Pid,
39    }
40
41    /// Get a borrowed fd for stdin.
42    fn stdin_fd() -> BorrowedFd<'static> {
43        // SAFETY: stdin (fd 0) is valid for the lifetime of the process
44        // in an interactive shell context where we've verified isatty.
45        unsafe { BorrowedFd::borrow_raw(0) }
46    }
47
48    /// Set a signal to SIG_IGN.
49    fn ignore_signal(sig: Signal) -> nix::Result<()> {
50        // SAFETY: SIG_IGN is a well-defined, safe signal disposition.
51        // No custom handler code is executed.
52        unsafe {
53            signal::sigaction(
54                sig,
55                &signal::SigAction::new(
56                    SigHandler::SigIgn,
57                    signal::SaFlags::empty(),
58                    signal::SigSet::empty(),
59                ),
60            )?;
61        }
62        Ok(())
63    }
64
65    impl TerminalState {
66        /// Initialize terminal state for interactive job control.
67        ///
68        /// - Puts the shell in its own process group
69        /// - Ignores SIGTSTP, SIGTTOU, SIGTTIN (so the shell isn't stopped)
70        /// - Takes the terminal foreground
71        pub fn init() -> nix::Result<Self> {
72            let shell_pid = unistd::getpid();
73
74            // Put the shell in its own process group.
75            // This may fail with EPERM if we're already a session leader
76            // (e.g., spawned via setsid), which is fine — we're already
77            // in our own process group in that case.
78            match unistd::setpgid(shell_pid, shell_pid) {
79                Ok(()) => {}
80                Err(nix::errno::Errno::EPERM) => {
81                    // Already session leader or in our own pgid — acceptable
82                }
83                Err(e) => return Err(e),
84            }
85
86            // Ignore SIGTTOU first so tcsetpgrp doesn't stop us
87            ignore_signal(Signal::SIGTTOU)?;
88
89            tcsetpgrp(stdin_fd(), shell_pid)?;
90
91            // Ignore the other job-control signals
92            ignore_signal(Signal::SIGTSTP)?;
93            ignore_signal(Signal::SIGTTIN)?;
94
95            Ok(Self {
96                shell_pgid: shell_pid,
97            })
98        }
99
100        /// Give the terminal foreground to a process group.
101        pub fn give_terminal_to(&self, pgid: Pid) -> nix::Result<()> {
102            tcsetpgrp(stdin_fd(), pgid)
103        }
104
105        /// Reclaim the terminal foreground for the shell.
106        pub fn reclaim_terminal(&self) -> nix::Result<()> {
107            tcsetpgrp(stdin_fd(), self.shell_pgid)
108        }
109
110        /// Wait for a foreground process, handling stop signals (WUNTRACED).
111        ///
112        /// This blocks the current thread. Call from `block_in_place`.
113        pub fn wait_for_foreground(&self, pid: Pid) -> WaitResult {
114            loop {
115                match waitpid(pid, Some(WaitPidFlag::WUNTRACED)) {
116                    Ok(WaitStatus::Exited(_, code)) => {
117                        return WaitResult::Exited(code);
118                    }
119                    Ok(WaitStatus::Signaled(_, sig, _)) => {
120                        return WaitResult::Signaled(sig as i32);
121                    }
122                    Ok(WaitStatus::Stopped(_, sig)) => {
123                        return WaitResult::Stopped(sig);
124                    }
125                    Ok(WaitStatus::Continued(_)) => continue,
126                    Ok(_) => continue,
127                    Err(nix::errno::Errno::EINTR) => continue,
128                    Err(nix::errno::Errno::ECHILD) => {
129                        return WaitResult::Exited(0);
130                    }
131                    Err(e) => {
132                        tracing::error!("waitpid failed: {}", e);
133                        return WaitResult::Exited(1);
134                    }
135                }
136            }
137        }
138    }
139}
140
141#[cfg(unix)]
142pub use unix::{TerminalState, WaitResult};