Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ authors = ["TruFoundation <noreply@trufoundation.org>"]

[dependencies]
crossterm = "0.27"
signal-hook = "0.3"
nix = "0.27"
once_cell = "1.20"
libc = "0.2"
98 changes: 98 additions & 0 deletions src/job_control.rs
Original file line number Diff line number Diff line change
@@ -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<AtomicI32> = 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;
}
};
Comment on lines +10 to +25

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== job_control.rs outline ==\n'
ast-grep outline src/job_control.rs --view expanded || true

printf '\n== relevant lines in job_control.rs ==\n'
nl -ba src/job_control.rs | sed -n '1,220p'

printf '\n== dependency versions ==\n'
rg -n 'signal-hook|libc' Cargo.toml Cargo.lock --context 2 || true

printf '\n== search for init_signal_handlers usage ==\n'
rg -n 'init_signal_handlers|Signals::new|waitpid|tcsetpgrp|SIGTTOU|WUNTRACED' src --context 3 || true

Repository: TruFoundation/TruShell

Length of output: 422


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/job_control.rs ==\n'
sed -n '1,220p' src/job_control.rs

printf '\n== Cargo.toml entries ==\n'
rg -n 'signal-hook|libc' Cargo.toml --context 2 || true

printf '\n== Cargo.lock entries ==\n'
rg -n 'name = "signal-hook"|name = "libc"|version = "' Cargo.lock --context 2 || true

Repository: TruFoundation/TruShell

Length of output: 11436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== call sites for init_signal_handlers ==\n'
rg -n 'init_signal_handlers\(' src --context 3 || true

printf '\n== broader job control uses ==\n'
rg -n 'SIGCHLD|SIGINT|SIGTSTP|tcsetpgrp|waitpid|setpgid|FG_PGID' src --context 2 || true

Repository: TruFoundation/TruShell

Length of output: 4578


🌐 Web query:

signal-hook 0.3 iterator::Signals::new signature accepts slice or iterator

💡 Result:

Yes, the signal-hook 0.3 library's Signals::new method accepts a slice iterator. Specifically, the method is defined with a generic signature that accepts any type I that implements IntoIterator where the items S can be borrowed as a c_int [1][2][3]. The signature is: pub fn new<I, S>(signals: I) -> Result<Self, Error> where I: IntoIterator<Item = S>, S: Borrow<c_int>, Because a reference to a slice, such as &[c_int], implements IntoIterator [1][3], you can pass a slice directly to Signals::new as shown in the following example: let mut signals = Signals::new(&[SIGUSR1, SIGUSR2])?; This flexibility allows you to pass arrays, vectors, or slices, as they all satisfy the IntoIterator trait requirement [2][3].

Citations:


Register the handlers before starting the worker thread. Signals::new runs inside thread::spawn, so init_signal_handlers() can return before SIGCHLD/SIGINT/SIGTSTP are registered. Create Signals first, then move it into the thread.

🧰 Tools
🪛 Clippy (1.96.0)

[warning] 15-15: the borrowed expression implements the required traits

(warning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/job_control.rs` around lines 10 - 25, Update init_signal_handlers to
construct signal_hook::iterator::Signals for SIGCHLD, SIGINT, and SIGTSTP before
calling thread::spawn, handling registration errors there; then move the
successfully created Signals instance into the worker thread instead of creating
it inside the thread.


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;
}
}
Comment on lines +29 to +37

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

sed -n '1,140p' src/job_control.rs | nl -ba

Repository: TruFoundation/TruShell

Length of output: 200


🏁 Script executed:

sed -n '1,160p' src/job_control.rs | cat -n

Repository: TruFoundation/TruShell

Length of output: 4405


🏁 Script executed:

rg -n "waitpid|WUNTRACED|SIGCHLD|SIGTSTP|try_wait|tcsetpgrp|setpgid" src

Repository: TruFoundation/TruShell

Length of output: 1046


Use a single foreground child-status owner src/job_control.rs:29-37, 82-89

The SIGCHLD reaper can consume the foreground child’s status before the blocking waitpid(pid, 0), and that wait never reports SIGTSTP. Ctrl-Z can leave the shell hung with the terminal still assigned to the stopped process group. Let the foreground path own status handling, wait with WUNTRACED, and handle EINTR/ECHILD before restoring tcsetpgrp.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/job_control.rs` around lines 29 - 37, Ensure the SIGCHLD handler does not
reap the foreground child, leaving its status for the foreground wait path.
Update the foreground wait logic around the blocking waitpid call to use
WUNTRACED, retry on EINTR, handle ECHILD, and process stopped-child status
before restoring terminal ownership with tcsetpgrp.

}
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);
Comment on lines +79 to +89

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== job_control.rs outline ==\n'
ast-grep outline src/job_control.rs --view expanded || true

printf '\n== tcsetpgrp / SIGTTOU references ==\n'
rg -n "tcsetpgrp|SIGTTOU|sigaction|signal|setpgid|getpgrp|FG_PGID" src Cargo.toml . -g '!target' || true

printf '\n== src/job_control.rs relevant lines ==\n'
nl -ba src/job_control.rs | sed -n '1,220p'

Repository: TruFoundation/TruShell

Length of output: 3521


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== src/job_control.rs (first 220 lines) ==\n'
sed -n '1,220p' src/job_control.rs

printf '\n== src/main.rs ==\n'
sed -n '1,120p' src/main.rs

Repository: TruFoundation/TruShell

Length of output: 7516


Ignore SIGTTOU around tcsetpgrp here. src/job_control.rs:79-89 restores the terminal while TruShell is backgrounded, so the final tcsetpgrp can stop the shell. Check both terminal handoff calls and restore ownership from a cleanup path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/job_control.rs` around lines 79 - 89, Update the foreground terminal
handoff flow around the existing tcsetpgrp calls to temporarily ignore SIGTTOU
before both terminal ownership changes, then restore the prior signal
disposition afterward. Move terminal restoration and FG_PGID reset into a
cleanup path that runs even if waiting or handoff handling exits early, ensuring
the shell regains control without being stopped.


// Attempt to reap the std::process::Child to avoid zombies
let _ = child.try_wait();
}
Err(e) => {
eprintln!("trushell: command not found '{}': {}", command_name, e);
}
}
}
32 changes: 10 additions & 22 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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());
Comment on lines 63 to +65

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## src/main.rs (around lines 1-140)\n'
cat -n src/main.rs | sed -n '1,140p'

printf '\n## Search for split_posix_words and empty-token guards\n'
rg -n "split_posix_words|split_first\\(|parts\\[0\\]|spawn_and_wait" src -S

Repository: TruFoundation/TruShell

Length of output: 6371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## src/main.rs (split_posix_words and related fallback)\n'
cat -n src/main.rs | sed -n '120,240p'

printf '\n## parser.rs outline\n'
ast-grep outline src/parser.rs --view expanded || true

printf '\n## Search for empty-token handling around command execution\n'
rg -n "is_empty\\(|split_first\\(|spawn_and_wait\\(|parts\\[0\\]" src/main.rs src/job_control.rs src/parser.rs -S

Repository: TruFoundation/TruShell

Length of output: 7671


Preserve the empty-token guard in the parse-error fallback. split_posix_words can return [] for malformed input like "" or a trailing \, so parts[0] can panic here. Use split_first() and return early instead of indexing.

🧰 Tools
🪛 Clippy (1.96.0)

[warning] 65-65: unnecessary use of to_vec

(warning)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.rs` around lines 63 - 65, Update the parse-error fallback in the Err
branch around split_posix_words to use split_first() and return early when no
tokens are present, preserving the existing spawn_and_wait behavior for
non-empty parts without indexing parts[0] unsafely.

}
}
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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::<Vec<_>>();
execute_system_command(&cmd, &args);
job_control::spawn_and_wait(&cmd, &args);
}
Loading