From 7cce76021fc60c8ccf537705a7cbe12ea33acaad Mon Sep 17 00:00:00 2001 From: Elian Thorne <289383015+ElianThorne@users.noreply.github.com> Date: Sat, 18 Jul 2026 06:52:11 +0000 Subject: [PATCH] feat: add job control and signal handling --- Cargo.lock | 21 ++++++++++ Cargo.toml | 4 ++ src/job_control.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 32 +++++---------- 4 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 src/job_control.rs diff --git a/Cargo.lock b/Cargo.lock index 0c77882..1d7ec34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,6 +82,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "nix" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + [[package]] name = "parking_lot" version = "0.12.5" @@ -162,6 +179,10 @@ name = "trushell" version = "0.1.0" dependencies = [ "crossterm", + "libc", + "nix", + "once_cell", + "signal-hook", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 68e8a1a..2a85f63 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,7 @@ authors = ["TruFoundation "] [dependencies] crossterm = "0.27" +signal-hook = "0.3" +nix = "0.27" +once_cell = "1.20" +libc = "0.2" diff --git a/src/job_control.rs b/src/job_control.rs new file mode 100644 index 0000000..1ee9327 --- /dev/null +++ b/src/job_control.rs @@ -0,0 +1,98 @@ +use once_cell::sync::Lazy; +use std::process::Command; +use std::sync::atomic::{AtomicI32, Ordering}; +use std::thread; +use std::os::unix::process::CommandExt; +use libc; + +static FG_PGID: Lazy = Lazy::new(|| AtomicI32::new(0)); + +pub fn init_signal_handlers() { + static START: std::sync::Once = std::sync::Once::new(); + START.call_once(|| { + // Spawn a thread to handle signals using signal-hook's iterator + let _ = thread::spawn(|| { + let mut signals = match signal_hook::iterator::Signals::new(&[ + libc::SIGCHLD, + libc::SIGINT, + libc::SIGTSTP, + ]) { + Ok(s) => s, + Err(e) => { + eprintln!("Failed to register signal handlers: {}", e); + return; + } + }; + + for signal in signals.forever() { + match signal { + libc::SIGCHLD => { + // Reap any finished children (non-blocking) + loop { + let mut status: libc::c_int = 0; + let pid = unsafe { libc::waitpid(-1, &mut status as *mut i32, libc::WNOHANG) }; + if pid <= 0 { + break; + } + } + } + libc::SIGINT => { + let pgid = FG_PGID.load(Ordering::SeqCst); + if pgid > 0 { + unsafe { libc::kill(-(pgid as libc::pid_t), libc::SIGINT) }; + } + } + libc::SIGTSTP => { + let pgid = FG_PGID.load(Ordering::SeqCst); + if pgid > 0 { + unsafe { libc::kill(-(pgid as libc::pid_t), libc::SIGTSTP) }; + } + } + _ => {} + } + } + }); + }); +} + +pub fn spawn_and_wait(command_name: &str, args: &[String]) { + // Build the command and ensure the child is placed into its own process group + let mut cmd = Command::new(command_name); + cmd.args(args) + .stdin(std::process::Stdio::inherit()) + .stdout(std::process::Stdio::inherit()) + .stderr(std::process::Stdio::inherit()) + .before_exec(|| { + // create new process group for the child + let r = unsafe { libc::setpgid(0, 0) }; + if r != 0 { + return Err(std::io::Error::last_os_error()); + } + Ok(()) + }); + + match cmd.spawn() { + Ok(mut child) => { + let pid = child.id() as i32; + FG_PGID.store(pid, Ordering::SeqCst); + + // Give terminal control to child process group + unsafe { libc::tcsetpgrp(libc::STDIN_FILENO, pid) }; + + // Wait for the child to exit (blocking) + let mut status: libc::c_int = 0; + let _ = unsafe { libc::waitpid(pid, &mut status as *mut i32, 0) }; + + // Restore terminal control to the shell + let shell_pgid = unsafe { libc::getpgrp() }; + unsafe { libc::tcsetpgrp(libc::STDIN_FILENO, shell_pgid) }; + FG_PGID.store(0, Ordering::SeqCst); + + // Attempt to reap the std::process::Child to avoid zombies + let _ = child.try_wait(); + } + Err(e) => { + eprintln!("trushell: command not found '{}': {}", command_name, e); + } + } +} diff --git a/src/main.rs b/src/main.rs index 61730bc..eb246a2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,14 @@ mod parser; +mod job_control; use std::io::{self, Write}; -use std::process::{Command, Stdio}; fn main() { println!("Welcome to TruShell Native Engine"); + // Initialize job control and signal handlers + job_control::init_signal_handlers(); + loop { print!("trushell ❯ "); if let Err(e) = io::stdout().flush() { @@ -51,15 +54,15 @@ fn main() { // If the parsed AST looks like a CLI invocation that was // accidentally parsed as subtraction (e.g. `ls -la` -> `ls - la`), // fall back to executing the system command. - if let Some((cmd, args)) = probable_cli_from_ast(&ast) { - execute_system_command(&cmd, &args); + if let Some((cmd, args)) = probable_cli_from_ast(&ast) { + job_control::spawn_and_wait(&cmd, &args); } else { println!("Parsed AST: {:#?}", ast); } } Err(err) => { eprintln!("Parse error: {}", err); - execute_system_command_from_input(trimmed_input); + job_control::spawn_and_wait(&parts[0], &parts[1..].to_vec()); } } } @@ -173,23 +176,8 @@ fn execute_system_command(cmd: &str, args: &[String]) { cmd }; - let child = Command::new(command_name) - .args(args) - .stdin(Stdio::inherit()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .spawn(); - - match child { - Ok(mut child_proc) => { - if let Err(e) = child_proc.wait() { - eprintln!("Execution error: {}", e); - } - } - Err(e) => { - eprintln!("trushell: command not found '{}': {}", command_name, e); - } - } + // Deprecated: routed to job_control::spawn_and_wait in main + job_control::spawn_and_wait(command_name, args); } fn execute_system_command_from_input(input: &str) { @@ -200,5 +188,5 @@ fn execute_system_command_from_input(input: &str) { let cmd = parts[0].clone(); let args = parts.into_iter().skip(1).collect::>(); - execute_system_command(&cmd, &args); + job_control::spawn_and_wait(&cmd, &args); }