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