re-imported job/signal code from old implementation

This commit is contained in:
2025-03-15 17:14:52 -04:00
parent 2acf70ef96
commit d4f8f023af
15 changed files with 494 additions and 35 deletions

View File

@@ -1,4 +1,4 @@
use crate::{libsh::error::{ShErr, ShErrKind}, parse::lex::{is_hard_sep, LexFlags, LexStream, Tk, Span, TkErr, TkFlags, TkState, TkRule}, prelude::*, state::read_vars};
use crate::{parse::lex::{is_hard_sep, LexFlags, LexStream, Tk, Span, TkErr, TkFlags, TkRule}, state::read_vars};
/// Variable substitution marker
pub const VAR_SUB: char = '\u{fdd0}';

View File

@@ -7,12 +7,43 @@ pub mod expand;
pub mod state;
pub mod builtin;
pub mod jobs;
pub mod signal;
#[cfg(test)]
pub mod tests;
use parse::{execute::Dispatcher, lex::{LexFlags, LexStream}, ParseStream};
use termios::{LocalFlags, Termios};
use crate::prelude::*;
pub static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
pub fn save_termios() {
unsafe {
SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
Some(termios)
} else {
None
});
}
}
pub fn get_saved_termios() -> Option<Termios> {
unsafe {
// This is only used when the shell exits so it's fine
// SAVED_TERMIOS is only mutated once at the start as well
SAVED_TERMIOS.clone().flatten()
}
}
fn set_termios() {
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL;
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap();
}
}
fn main() {
'main: loop {
let input = prompt::read_line().unwrap();

View File

@@ -1,4 +1,6 @@
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*, procio::borrow_fd};
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*, procio::borrow_fd, state::{set_status, write_jobs}};
pub const SIG_EXIT_OFFSET: i32 = 128;
bitflags! {
#[derive(Debug, Copy, Clone)]
@@ -354,6 +356,67 @@ pub fn term_ctlr() -> Pid {
tcgetpgrp(borrow_fd(0)).unwrap_or(getpgrp())
}
/// Calls attach_tty() on the shell's process group to retake control of the terminal
pub fn take_term() -> ShResult<()> {
attach_tty(getpgrp())?;
Ok(())
}
pub fn disable_reaping() -> ShResult<()> {
flog!(TRACE, "Disabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::ignore_sigchld)) }?;
Ok(())
}
pub fn enable_reaping() -> ShResult<()> {
flog!(TRACE, "Enabling reaping");
unsafe { signal(Signal::SIGCHLD, SigHandler::Handler(crate::signal::handle_sigchld)) }.unwrap();
Ok(())
}
/// Waits on the current foreground job and updates the shell's last status code
pub fn wait_fg(job: Job) -> ShResult<()> {
flog!(TRACE, "Waiting on foreground job");
let mut code = 0;
attach_tty(job.pgid())?;
disable_reaping()?;
let statuses = write_jobs(|j| j.new_fg(job))?;
for status in statuses {
match status {
WtStat::Exited(_, exit_code) => {
code = exit_code;
}
WtStat::Stopped(_, sig) => {
write_jobs(|j| j.fg_to_bg(status))?;
code = SIG_EXIT_OFFSET + sig as i32;
},
WtStat::Signaled(_, sig, _) => {
if sig == Signal::SIGTSTP {
write_jobs(|j| j.fg_to_bg(status))?;
}
code = SIG_EXIT_OFFSET + sig as i32;
},
_ => { /* Do nothing */ }
}
}
take_term()?;
set_status(code);
flog!(TRACE, "exit code: {}", code);
enable_reaping()?;
Ok(())
}
pub fn dispatch_job(job: Job, is_bg: bool) -> ShResult<()> {
if is_bg {
write_jobs(|j| {
j.insert_job(job, false)
})?;
} else {
wait_fg(job)?;
}
Ok(())
}
pub fn attach_tty(pgid: Pid) -> ShResult<()> {
// If we aren't attached to a terminal, the pgid already controls it, or the process group does not exist
// Then return ok

View File

@@ -1,4 +1,4 @@
use std::{fmt::Display, ops::Range, str::FromStr};
use std::{fmt::Display, ops::Range};
use crate::{parse::lex::Span, prelude::*};

View File

@@ -1,3 +1,4 @@
pub mod error;
pub mod term;
pub mod flog;
pub mod sys;

18
src/libsh/sys.rs Normal file
View File

@@ -0,0 +1,18 @@
use crate::{prelude::*, state::write_jobs};
pub fn sh_quit(code: i32) -> ! {
write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok();
}
});
if let Some(termios) = crate::get_saved_termios() {
termios::tcsetattr(std::io::stdin(), termios::SetArg::TCSANOW, &termios).unwrap();
}
if code == 0 {
eprintln!("exit");
} else {
eprintln!("exit {code}");
}
exit(code);
}

View File

@@ -1,6 +1,5 @@
use std::collections::VecDeque;
use nix::sys::wait::WaitPidFlag;
use crate::{builtin::echo::echo, libsh::error::ShResult, prelude::*, procio::{IoFrame, IoPipe, IoStack}, state::{self, write_vars}};

View File

@@ -2,7 +2,7 @@ use std::{fmt::Display, ops::{Bound, Deref, Range, RangeBounds}};
use bitflags::bitflags;
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind}, prelude::*};
use crate::{builtin::BUILTINS, prelude::*};
pub const KEYWORDS: [&'static str;14] = [
"if",

View File

@@ -1,7 +1,7 @@
use std::str::FromStr;
use bitflags::bitflags;
use lex::{is_hard_sep, Span, Tk, TkFlags, TkRule};
use lex::{Span, Tk, TkFlags, TkRule};
use crate::{prelude::*, libsh::error::{ShErr, ShErrKind, ShResult}, procio::{IoFd, IoFile, IoInfo}};

View File

@@ -1,15 +1,15 @@
// Standard Library Common IO and FS Abstractions
pub use std::io::{
self,
BufRead,
BufReader,
BufWriter,
Error,
ErrorKind,
Read,
Seek,
SeekFrom,
Write,
self,
BufRead,
BufReader,
BufWriter,
Error,
ErrorKind,
Read,
Seek,
SeekFrom,
Write,
};
pub use std::fs::{ self, File, OpenOptions };
pub use std::path::{ Path, PathBuf };
@@ -25,18 +25,19 @@ pub use std::os::unix::io::{ AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
// Nix crate for POSIX APIs
pub use nix::{
errno::Errno,
fcntl::{ open, OFlag },
sys::{
signal::{ self, kill, killpg, pthread_sigmask, SigSet, SigmaskHow, SigHandler, Signal },
stat::Mode,
wait::{ waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat },
},
libc::{ STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO },
unistd::{
dup, read, isatty, write, close, setpgid, dup2, getpgrp,
execvpe, tcgetpgrp, tcsetpgrp, fork, pipe, Pid, ForkResult
},
errno::Errno,
fcntl::{ open, OFlag },
sys::{
termios::{ self },
signal::{ self, signal, kill, killpg, pthread_sigmask, SigSet, SigmaskHow, SigHandler, Signal },
stat::Mode,
wait::{ waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat },
},
libc::{ self, STDIN_FILENO, STDERR_FILENO, STDOUT_FILENO },
unistd::{
dup, read, isatty, write, close, setpgid, dup2, getpgrp, getpgid,
execvpe, tcgetpgrp, tcsetpgrp, fork, pipe, Pid, ForkResult
},
};
pub use bitflags::bitflags;

View File

@@ -1,4 +1,4 @@
use std::{fs::{File, OpenOptions}, ops::{Deref, DerefMut}, path::PathBuf};
use std::{fs::File, ops::{Deref, DerefMut}, path::PathBuf};
use bitflags::bitflags;
use rustyline::history::{History, SearchResult};

View File

@@ -3,9 +3,8 @@ pub mod readline;
use std::path::Path;
use history::FernHist;
use readline::FernReadline;
use rustyline::{error::ReadlineError, history::{FileHistory, History}, Config, Editor};
use rustyline::{error::ReadlineError, history::FileHistory, Editor};
use crate::{libsh::{error::ShResult, term::{Style, Styled}}, prelude::*};

View File

@@ -2,7 +2,7 @@ use std::borrow::Cow;
use rustyline::{completion::Completer, highlight::Highlighter, hint::{Hint, Hinter}, validate::{ValidationResult, Validator}, Helper};
use crate::{libsh::term::{Style, Styled}, prelude::*};
use crate::libsh::term::{Style, Styled};
pub struct FernReadline {
}

159
src/signal.rs Normal file
View File

@@ -0,0 +1,159 @@
use crate::{jobs::{take_term, JobCmdFlags, JobID}, libsh::{error::ShResult, sys::sh_quit}, prelude::*, state::{read_jobs, write_jobs}};
pub fn sig_setup() {
unsafe {
signal(Signal::SIGCHLD, SigHandler::Handler(handle_sigchld)).unwrap();
signal(Signal::SIGQUIT, SigHandler::Handler(handle_sigquit)).unwrap();
signal(Signal::SIGTSTP, SigHandler::Handler(handle_sigtstp)).unwrap();
signal(Signal::SIGHUP, SigHandler::Handler(handle_sighup)).unwrap();
signal(Signal::SIGINT, SigHandler::Handler(handle_sigint)).unwrap();
signal(Signal::SIGTTIN, SigHandler::SigIgn).unwrap();
signal(Signal::SIGTTOU, SigHandler::SigIgn).unwrap();
}
}
extern "C" fn handle_sighup(_: libc::c_int) {
write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok();
}
});
std::process::exit(0);
}
extern "C" fn handle_sigtstp(_: libc::c_int) {
write_jobs(|j| {
if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGTSTP).ok();
}
});
}
extern "C" fn handle_sigint(_: libc::c_int) {
write_jobs(|j| {
if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGINT).ok();
}
});
}
pub extern "C" fn ignore_sigchld(_: libc::c_int) {
/*
Do nothing
This function exists because using SIGIGN to ignore SIGCHLD
will cause the kernel to automatically reap the child process, which is not what we want.
This handler will leave the signaling process as a zombie, allowing us
to handle it somewhere else.
This handler is used when we want to handle SIGCHLD explicitly,
like in the case of handling foreground jobs
*/
}
extern "C" fn handle_sigquit(_: libc::c_int) {
sh_quit(0)
}
pub extern "C" fn handle_sigchld(_: libc::c_int) {
let flags = WtFlag::WNOHANG | WtFlag::WSTOPPED;
while let Ok(status) = waitpid(None, Some(flags)) {
if let Err(e) = match status {
WtStat::Exited(pid, _) => child_exited(pid, status),
WtStat::Signaled(pid, signal, _) => child_signaled(pid, signal),
WtStat::Stopped(pid, signal) => child_stopped(pid, signal),
WtStat::Continued(pid) => child_continued(pid),
WtStat::StillAlive => break,
_ => unimplemented!()
} {
eprintln!("{}",e)
}
}
}
pub fn child_signaled(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap();
let stat = WtStat::Signaled(pid, sig, false);
child.set_stat(stat);
}
});
if sig == Signal::SIGINT {
take_term().unwrap()
}
Ok(())
}
pub fn child_stopped(pid: Pid, sig: Signal) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
let child = job.children_mut().iter_mut().find(|chld| pid == chld.pid()).unwrap();
let status = WtStat::Stopped(pid, sig);
child.set_stat(status);
} else if j.get_fg_mut().is_some_and(|fg| fg.pgid() == pgid) {
j.fg_to_bg(WtStat::Stopped(pid, sig)).unwrap();
}
});
take_term()?;
Ok(())
}
pub fn child_continued(pid: Pid) -> ShResult<()> {
let pgid = getpgid(Some(pid)).unwrap_or(pid);
write_jobs(|j| {
if let Some(job) = j.query_mut(JobID::Pgid(pgid)) {
job.killpg(Signal::SIGCONT).ok();
}
});
Ok(())
}
pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
/*
* Here we are going to get metadata on the exited process by querying the job table with the pid.
* Then if the discovered job is the fg task, return terminal control to rsh
* If it is not the fg task, print the display info for the job in the job table
* We can reasonably assume that if it is not a foreground job, then it exists in the job table
* If this assumption is incorrect, the code has gone wrong somewhere.
*/
if let Some((
pgid,
is_fg,
is_finished
)) = write_jobs(|j| {
let fg_pgid = j.get_fg().map(|job| job.pgid());
if let Some(job) = j.query_mut(JobID::Pid(pid)) {
let pgid = job.pgid();
let is_fg = fg_pgid.is_some_and(|fg| fg == pgid);
job.update_by_id(JobID::Pid(pid), status).unwrap();
let is_finished = !job.running();
if let Some(child) = job.children_mut().iter_mut().find(|chld| pid == chld.pid()) {
child.set_stat(status);
}
Some((pgid, is_fg, is_finished))
} else {
None
}
}) {
if is_finished {
if is_fg {
take_term()?;
} else {
println!();
let job_order = read_jobs(|j| j.order().to_vec());
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
if let Some(job) = result {
println!("{}",job.display(&job_order,JobCmdFlags::PIDS))
}
}
}
}
Ok(())
}

View File

@@ -1,18 +1,206 @@
use std::{collections::HashMap, sync::{LazyLock, RwLock, RwLockReadGuard, RwLockWriteGuard}};
use crate::prelude::*;
use crate::{jobs::{wait_fg, attach_tty, take_term, Job, JobCmdFlags, JobID}, libsh::error::ShResult, prelude::*, procio::borrow_fd};
pub static JOB_TABLE: LazyLock<RwLock<JobTab>> = LazyLock::new(|| RwLock::new(JobTab::new()));
pub static VAR_TABLE: LazyLock<RwLock<VarTab>> = LazyLock::new(|| RwLock::new(VarTab::new()));
pub struct JobTab {
fg: Option<Job>,
order: Vec<usize>,
new_updates: Vec<usize>,
jobs: Vec<Option<Job>>
}
impl JobTab {
pub fn new() -> Self {
Self {}
Self { fg: None, order: vec![], new_updates: vec![], jobs: vec![] }
}
pub fn take_fg(&mut self) -> Option<Job> {
self.fg.take()
}
fn next_open_pos(&self) -> usize {
if let Some(position) = self.jobs.iter().position(|slot| slot.is_none()) {
position
} else {
self.jobs.len()
}
}
pub fn jobs(&self) -> &Vec<Option<Job>> {
&self.jobs
}
pub fn jobs_mut(&mut self) -> &mut Vec<Option<Job>> {
&mut self.jobs
}
pub fn curr_job(&self) -> Option<usize> {
self.order.last().copied()
}
pub fn prev_job(&self) -> Option<usize> {
self.order.last().copied()
}
fn prune_jobs(&mut self) {
while let Some(job) = self.jobs.last() {
if job.is_none() {
self.jobs.pop();
} else {
break
}
}
}
pub fn insert_job(&mut self, mut job: Job, silent: bool) -> ShResult<usize> {
self.prune_jobs();
let tab_pos = if let Some(id) = job.tabid() { id } else { self.next_open_pos() };
job.set_tabid(tab_pos);
self.order.push(tab_pos);
if !silent {
write(borrow_fd(1),format!("{}", job.display(&self.order, JobCmdFlags::INIT)).as_bytes())?;
}
if tab_pos == self.jobs.len() {
self.jobs.push(Some(job))
} else {
self.jobs[tab_pos] = Some(job);
}
Ok(tab_pos)
}
pub fn order(&self) -> &[usize] {
&self.order
}
pub fn query(&self, identifier: JobID) -> Option<&Job> {
match identifier {
// Match by process group ID
JobID::Pgid(pgid) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| j.pgid() == pgid)
})
}
// Match by process ID
JobID::Pid(pid) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| j.children().iter().any(|child| child.pid() == pid))
})
}
// Match by table ID (index in the job table)
JobID::TableID(id) => {
self.jobs.get(id).and_then(|job| job.as_ref())
}
// Match by command name (partial match)
JobID::Command(cmd) => {
self.jobs.iter().find_map(|job| {
job.as_ref().filter(|j| {
j.children().iter().any(|child| {
child.cmd().as_ref().is_some_and(|c| c.contains(&cmd))
})
})
})
}
}
}
pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> {
match identifier {
// Match by process group ID
JobID::Pgid(pgid) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| j.pgid() == pgid)
})
}
// Match by process ID
JobID::Pid(pid) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| j.children().iter().any(|child| child.pid() == pid))
})
}
// Match by table ID (index in the job table)
JobID::TableID(id) => {
self.jobs.get_mut(id).and_then(|job| job.as_mut())
}
// Match by command name (partial match)
JobID::Command(cmd) => {
self.jobs.iter_mut().find_map(|job| {
job.as_mut().filter(|j| {
j.children().iter().any(|child| {
child.cmd().as_ref().is_some_and(|c| c.contains(&cmd))
})
})
})
}
}
}
pub fn get_fg(&self) -> Option<&Job> {
self.fg.as_ref()
}
pub fn get_fg_mut(&mut self) -> Option<&mut Job> {
self.fg.as_mut()
}
pub fn new_fg<'a>(&mut self, job: Job) -> ShResult<Vec<WtStat>> {
let pgid = job.pgid();
self.fg = Some(job);
attach_tty(pgid)?;
let statuses = self.fg.as_mut().unwrap().wait_pgrp()?;
attach_tty(getpgrp())?;
Ok(statuses)
}
pub fn fg_to_bg(&mut self, stat: WtStat) -> ShResult<()> {
if self.fg.is_none() {
return Ok(())
}
take_term()?;
let fg = std::mem::take(&mut self.fg);
if let Some(mut job) = fg {
job.set_stats(stat);
self.insert_job(job, false)?;
}
Ok(())
}
pub fn bg_to_fg(&mut self, id: JobID) -> ShResult<()> {
let job = self.remove_job(id);
if let Some(job) = job {
wait_fg(job)?;
}
Ok(())
}
pub fn remove_job(&mut self, id: JobID) -> Option<Job> {
let tabid = self.query(id).map(|job| job.tabid().unwrap());
if let Some(tabid) = tabid {
self.jobs.get_mut(tabid).and_then(Option::take)
} else {
None
}
}
pub fn print_jobs(&mut self, flags: JobCmdFlags) -> ShResult<()> {
let jobs = if flags.contains(JobCmdFlags::NEW_ONLY) {
&self.jobs
.iter()
.filter(|job| job.as_ref().is_some_and(|job| self.new_updates.contains(&job.tabid().unwrap())))
.map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>()
} else {
&self.jobs
.iter()
.map(|job| job.as_ref())
.collect::<Vec<Option<&Job>>>()
};
let mut jobs_to_remove = vec![];
for job in jobs.iter().flatten() {
// Skip foreground job
let id = job.tabid().unwrap();
// Filter jobs based on flags
if flags.contains(JobCmdFlags::RUNNING) && !matches!(job.get_stats().get(id).unwrap(), WtStat::StillAlive | WtStat::Continued(_)) {
continue;
}
if flags.contains(JobCmdFlags::STOPPED) && !matches!(job.get_stats().get(id).unwrap(), WtStat::Stopped(_,_)) {
continue;
}
// Print the job in the selected format
write(borrow_fd(1), format!("{}\n",job.display(&self.order,flags)).as_bytes())?;
if job.get_stats().iter().all(|stat| matches!(stat,WtStat::Exited(_, _))) {
jobs_to_remove.push(JobID::TableID(id));
}
}
for id in jobs_to_remove {
self.remove_job(id);
}
Ok(())
}
}