diff --git a/src/expand.rs b/src/expand.rs index 9506257..0405640 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -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}'; diff --git a/src/fern.rs b/src/fern.rs index 4c9834a..bd34268 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -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> = 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 { + 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(); diff --git a/src/jobs.rs b/src/jobs.rs index af047d3..f792664 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -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 diff --git a/src/libsh/error.rs b/src/libsh/error.rs index 21f2acb..6ba4eeb 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display, ops::Range, str::FromStr}; +use std::{fmt::Display, ops::Range}; use crate::{parse::lex::Span, prelude::*}; diff --git a/src/libsh/mod.rs b/src/libsh/mod.rs index a789133..fd1f69b 100644 --- a/src/libsh/mod.rs +++ b/src/libsh/mod.rs @@ -1,3 +1,4 @@ pub mod error; pub mod term; pub mod flog; +pub mod sys; diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs new file mode 100644 index 0000000..1552bcd --- /dev/null +++ b/src/libsh/sys.rs @@ -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); +} diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 0c83eb9..50a97bb 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -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}}; diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 500b4b4..c834ff1 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -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", diff --git a/src/parse/mod.rs b/src/parse/mod.rs index ef2681d..d9cf4f1 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -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}}; diff --git a/src/prelude.rs b/src/prelude.rs index c427aa9..182d734 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -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; diff --git a/src/prompt/history.rs b/src/prompt/history.rs index e93f5e7..016bbfc 100644 --- a/src/prompt/history.rs +++ b/src/prompt/history.rs @@ -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}; diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 5a9a036..b8b82bb 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -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::*}; diff --git a/src/prompt/readline.rs b/src/prompt/readline.rs index c804f8a..4db8b36 100644 --- a/src/prompt/readline.rs +++ b/src/prompt/readline.rs @@ -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 { } diff --git a/src/signal.rs b/src/signal.rs new file mode 100644 index 0000000..86e22eb --- /dev/null +++ b/src/signal.rs @@ -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(()) +} diff --git a/src/state.rs b/src/state.rs index e2859dc..9a7eeca 100644 --- a/src/state.rs +++ b/src/state.rs @@ -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> = LazyLock::new(|| RwLock::new(JobTab::new())); pub static VAR_TABLE: LazyLock> = LazyLock::new(|| RwLock::new(VarTab::new())); pub struct JobTab { - + fg: Option, + order: Vec, + new_updates: Vec, + jobs: Vec> } impl JobTab { pub fn new() -> Self { - Self {} + Self { fg: None, order: vec![], new_updates: vec![], jobs: vec![] } + } + pub fn take_fg(&mut self) -> Option { + 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> { + &self.jobs + } + pub fn jobs_mut(&mut self) -> &mut Vec> { + &mut self.jobs + } + pub fn curr_job(&self) -> Option { + self.order.last().copied() + } + pub fn prev_job(&self) -> Option { + 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 { + 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> { + 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 { + 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::>>() + } else { + &self.jobs + .iter() + .map(|job| job.as_ref()) + .collect::>>() + }; + 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(()) } }