Skip to content

Commit bc11eb3

Browse files
authored
Merge pull request #89 from TruFoundation/elian-patches
feat: add job control and signal handling
2 parents 502e663 + 7cce760 commit bc11eb3

4 files changed

Lines changed: 133 additions & 22 deletions

File tree

Cargo.lock

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ authors = ["TruFoundation <noreply@trufoundation.org>"]
66

77
[dependencies]
88
crossterm = "0.27"
9+
signal-hook = "0.3"
10+
nix = "0.27"
11+
once_cell = "1.20"
12+
libc = "0.2"

src/job_control.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use once_cell::sync::Lazy;
2+
use std::process::Command;
3+
use std::sync::atomic::{AtomicI32, Ordering};
4+
use std::thread;
5+
use std::os::unix::process::CommandExt;
6+
use libc;
7+
8+
static FG_PGID: Lazy<AtomicI32> = Lazy::new(|| AtomicI32::new(0));
9+
10+
pub fn init_signal_handlers() {
11+
static START: std::sync::Once = std::sync::Once::new();
12+
START.call_once(|| {
13+
// Spawn a thread to handle signals using signal-hook's iterator
14+
let _ = thread::spawn(|| {
15+
let mut signals = match signal_hook::iterator::Signals::new(&[
16+
libc::SIGCHLD,
17+
libc::SIGINT,
18+
libc::SIGTSTP,
19+
]) {
20+
Ok(s) => s,
21+
Err(e) => {
22+
eprintln!("Failed to register signal handlers: {}", e);
23+
return;
24+
}
25+
};
26+
27+
for signal in signals.forever() {
28+
match signal {
29+
libc::SIGCHLD => {
30+
// Reap any finished children (non-blocking)
31+
loop {
32+
let mut status: libc::c_int = 0;
33+
let pid = unsafe { libc::waitpid(-1, &mut status as *mut i32, libc::WNOHANG) };
34+
if pid <= 0 {
35+
break;
36+
}
37+
}
38+
}
39+
libc::SIGINT => {
40+
let pgid = FG_PGID.load(Ordering::SeqCst);
41+
if pgid > 0 {
42+
unsafe { libc::kill(-(pgid as libc::pid_t), libc::SIGINT) };
43+
}
44+
}
45+
libc::SIGTSTP => {
46+
let pgid = FG_PGID.load(Ordering::SeqCst);
47+
if pgid > 0 {
48+
unsafe { libc::kill(-(pgid as libc::pid_t), libc::SIGTSTP) };
49+
}
50+
}
51+
_ => {}
52+
}
53+
}
54+
});
55+
});
56+
}
57+
58+
pub fn spawn_and_wait(command_name: &str, args: &[String]) {
59+
// Build the command and ensure the child is placed into its own process group
60+
let mut cmd = Command::new(command_name);
61+
cmd.args(args)
62+
.stdin(std::process::Stdio::inherit())
63+
.stdout(std::process::Stdio::inherit())
64+
.stderr(std::process::Stdio::inherit())
65+
.before_exec(|| {
66+
// create new process group for the child
67+
let r = unsafe { libc::setpgid(0, 0) };
68+
if r != 0 {
69+
return Err(std::io::Error::last_os_error());
70+
}
71+
Ok(())
72+
});
73+
74+
match cmd.spawn() {
75+
Ok(mut child) => {
76+
let pid = child.id() as i32;
77+
FG_PGID.store(pid, Ordering::SeqCst);
78+
79+
// Give terminal control to child process group
80+
unsafe { libc::tcsetpgrp(libc::STDIN_FILENO, pid) };
81+
82+
// Wait for the child to exit (blocking)
83+
let mut status: libc::c_int = 0;
84+
let _ = unsafe { libc::waitpid(pid, &mut status as *mut i32, 0) };
85+
86+
// Restore terminal control to the shell
87+
let shell_pgid = unsafe { libc::getpgrp() };
88+
unsafe { libc::tcsetpgrp(libc::STDIN_FILENO, shell_pgid) };
89+
FG_PGID.store(0, Ordering::SeqCst);
90+
91+
// Attempt to reap the std::process::Child to avoid zombies
92+
let _ = child.try_wait();
93+
}
94+
Err(e) => {
95+
eprintln!("trushell: command not found '{}': {}", command_name, e);
96+
}
97+
}
98+
}

src/main.rs

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
mod parser;
2+
mod job_control;
23

34
use std::io::{self, Write};
4-
use std::process::{Command, Stdio};
55

66
fn main() {
77
println!("Welcome to TruShell Native Engine");
88

9+
// Initialize job control and signal handlers
10+
job_control::init_signal_handlers();
11+
912
loop {
1013
print!("trushell ❯ ");
1114
if let Err(e) = io::stdout().flush() {
@@ -51,15 +54,15 @@ fn main() {
5154
// If the parsed AST looks like a CLI invocation that was
5255
// accidentally parsed as subtraction (e.g. `ls -la` -> `ls - la`),
5356
// fall back to executing the system command.
54-
if let Some((cmd, args)) = probable_cli_from_ast(&ast) {
55-
execute_system_command(&cmd, &args);
57+
if let Some((cmd, args)) = probable_cli_from_ast(&ast) {
58+
job_control::spawn_and_wait(&cmd, &args);
5659
} else {
5760
println!("Parsed AST: {:#?}", ast);
5861
}
5962
}
6063
Err(err) => {
6164
eprintln!("Parse error: {}", err);
62-
execute_system_command_from_input(trimmed_input);
65+
job_control::spawn_and_wait(&parts[0], &parts[1..].to_vec());
6366
}
6467
}
6568
}
@@ -173,23 +176,8 @@ fn execute_system_command(cmd: &str, args: &[String]) {
173176
cmd
174177
};
175178

176-
let child = Command::new(command_name)
177-
.args(args)
178-
.stdin(Stdio::inherit())
179-
.stdout(Stdio::inherit())
180-
.stderr(Stdio::inherit())
181-
.spawn();
182-
183-
match child {
184-
Ok(mut child_proc) => {
185-
if let Err(e) = child_proc.wait() {
186-
eprintln!("Execution error: {}", e);
187-
}
188-
}
189-
Err(e) => {
190-
eprintln!("trushell: command not found '{}': {}", command_name, e);
191-
}
192-
}
179+
// Deprecated: routed to job_control::spawn_and_wait in main
180+
job_control::spawn_and_wait(command_name, args);
193181
}
194182

195183
fn execute_system_command_from_input(input: &str) {
@@ -200,5 +188,5 @@ fn execute_system_command_from_input(input: &str) {
200188

201189
let cmd = parts[0].clone();
202190
let args = parts.into_iter().skip(1).collect::<Vec<_>>();
203-
execute_system_command(&cmd, &args);
191+
job_control::spawn_and_wait(&cmd, &args);
204192
}

0 commit comments

Comments
 (0)