use std::{
collections::HashMap,
ffi::{CStr, CString},
io::{stdin, Stdin},
os::fd::{AsRawFd, RawFd},
process::exit,
};
use nix::{
libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, TCSADRAIN, WNOHANG, WUNTRACED},
sys::{
signal::{
kill, signal, sigprocmask, SigHandler, SigmaskHow,
Signal::{self, SIGCONT, SIGTTIN},
},
signalfd::SigSet,
termios::{tcgetattr, tcsetattr, SetArg, Termios},
wait::{waitpid, WaitPidFlag, WaitStatus},
},
unistd::{
close, dup2, execvp, fork, getpgrp, getpid, isatty, setpgid, tcgetpgrp, tcsetpgrp,
ForkResult, Pid,
},
};
pub struct Process {
pub pid: Pid,
pub argv: Vec<String>,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct JobId(pub usize);
pub struct Job {
pub jobid: JobId,
pub pgid: Pid,
pub processes: Vec<Pid>,
}
pub struct Context {
pub stdin: RawFd,
pub stdout: RawFd,
pub stderr: RawFd,
pub is_foreground: bool,
pub is_interactive: bool,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ProcessState {
Running,
Exited(i32),
}
#[derive(Debug, PartialEq, Eq)]
pub enum ExitStatus {
Exited(i32),
Running(Pid),
}
pub enum Pgid {
Current,
Pgid(Pid),
}
pub fn run_process(
argv: &[String],
pgid: Pgid,
ctx: &Context,
) -> Result<ExitStatus, std::io::Error> {
match unsafe { fork() } {
Ok(ForkResult::Parent { child }) => Ok(ExitStatus::Running(child)),
Ok(ForkResult::Child) => {
setup_process(argv, pgid, ctx)?;
unreachable!()
},
Err(_) => todo!(),
}
}
fn setup_process(argv: &[String], pgid: Pgid, ctx: &Context) -> Result<(), std::io::Error> {
let shell_term = STDIN_FILENO;
if ctx.is_interactive {
let pid = getpid();
let new_pgid = match pgid {
Pgid::Current => pid,
Pgid::Pgid(pgid) => pgid,
};
setpgid(pid, new_pgid)?;
if ctx.is_foreground {
tcsetpgrp(shell_term, new_pgid)?;
}
unsafe {
signal(Signal::SIGINT, SigHandler::SigIgn);
signal(Signal::SIGQUIT, SigHandler::SigIgn);
signal(Signal::SIGTSTP, SigHandler::SigIgn);
signal(Signal::SIGTTIN, SigHandler::SigIgn);
signal(Signal::SIGTTOU, SigHandler::SigIgn);
signal(Signal::SIGCHLD, SigHandler::SigIgn);
};
}
if ctx.stdin != STDIN_FILENO {
dup2(ctx.stdin, STDIN_FILENO)?;
close(ctx.stdin)?;
}
if ctx.stdout != STDOUT_FILENO {
dup2(ctx.stdout, STDOUT_FILENO)?;
close(ctx.stdout)?;
}
if ctx.stderr != STDERR_FILENO {
dup2(ctx.stderr, STDERR_FILENO)?;
close(ctx.stderr)?;
}
let filename = argv.get(0).unwrap();
let args = argv
.iter()
.map(|s| CString::new(s.clone()).unwrap())
.collect::<Vec<_>>();
execvp(&CString::new(filename.clone()).unwrap(), &args)?;
exit(1);
}
impl Job {
pub fn exited(&self, os: &Os) -> bool {
self.processes.iter().all(|pid| {
let state = os.get_process_state(pid).expect("missing process");
matches!(state, ProcessState::Exited(_))
})
}
pub fn last_process_state(&self, os: &Os) -> Option<ProcessState> {
self.processes
.iter()
.last()
.map(|pid| os.get_process_state(pid).expect("missing process").clone())
}
}
pub struct Os {
pgid: Pid,
tmods: Termios,
jobs: HashMap<JobId, Job>,
proc_state: HashMap<Pid, ProcessState>,
}
impl Os {
pub fn init_shell() -> Result<Self, std::io::Error> {
let shell_term = STDIN_FILENO;
if !isatty(shell_term)? {
panic!("Not interactive")
}
while tcgetpgrp(shell_term)? != getpgrp() {
kill(getpgrp(), SIGTTIN)?;
}
unsafe {
signal(Signal::SIGINT, SigHandler::SigIgn);
signal(Signal::SIGQUIT, SigHandler::SigIgn);
signal(Signal::SIGTSTP, SigHandler::SigIgn);
signal(Signal::SIGTTIN, SigHandler::SigIgn);
signal(Signal::SIGTTOU, SigHandler::SigIgn);
signal(Signal::SIGCHLD, SigHandler::SigIgn);
};
let pgid = getpid();
setpgid(pgid, pgid)?;
tcsetpgrp(shell_term, pgid)?;
let tmods = tcgetattr(shell_term)?;
let os = Os {
pgid,
tmods,
jobs: HashMap::new(),
proc_state: HashMap::new(),
};
Ok(os)
}
pub fn shell_pgid(&self) -> Pid {
self.pgid
}
pub fn create_job(&mut self, pgid: Pid, processes: Vec<Pid>) -> Result<JobId, std::io::Error> {
let jobid = self.find_free_job_id();
let new_job = Job {
jobid: jobid.clone(),
pgid,
processes,
};
self.jobs.insert(jobid.clone(), new_job);
Ok(jobid)
}
fn find_free_job_id(&self) -> JobId {
let mut id = 1usize;
while self.jobs.contains_key(&JobId(id)) {
id += 1;
}
JobId(id)
}
pub fn wait_for_job(&mut self, jobid: JobId) -> Result<ProcessState, std::io::Error> {
loop {
let job = self.jobs.get(&jobid).expect("non existent jobid");
if job.exited(self) {
break;
}
self.wait_for_any_process()?;
}
let job = self.jobs.get(&jobid).expect("non existent jobid");
let process_state = job.last_process_state(self).unwrap();
match process_state {
ProcessState::Exited(status) => {
self.remove_job(&jobid);
Ok(process_state)
},
_ => unreachable!(),
}
}
fn wait_for_any_process(&mut self) -> Result<Option<Pid>, std::io::Error> {
let wait_status = waitpid(None, WaitPidFlag::from_bits(WUNTRACED | WNOHANG))?;
match wait_status {
WaitStatus::Exited(pid, status) => {
self.set_process_state(pid, ProcessState::Exited(status));
Ok(Some(pid))
},
WaitStatus::StillAlive => Ok(None),
_ => todo!(),
}
}
fn set_process_state(&mut self, pid: Pid, state: ProcessState) {
self.proc_state.insert(pid, state);
}
pub fn get_process_state(&self, pid: &Pid) -> Option<&ProcessState> {
self.proc_state.get(pid)
}
fn remove_job(&mut self, jobid: &JobId) {
self.jobs.remove(jobid);
}
pub fn run_in_foreground(
&mut self,
jobid: JobId,
cont: bool,
) -> Result<ProcessState, std::io::Error> {
let shell_term = STDIN_FILENO;
let job = self.jobs.get(&jobid).unwrap();
tcsetpgrp(shell_term, job.pgid)?;
if cont {
kill(job.pgid, SIGCONT)?;
}
let proc_state = self.wait_for_job(jobid)?;
tcsetpgrp(shell_term, self.shell_pgid())?;
tcsetattr(shell_term, SetArg::TCSADRAIN, &self.tmods)?;
Ok(proc_state)
}
pub fn run_in_background(&self, jobid: JobId, cont: bool) -> Result<(), std::io::Error> {
if cont {
let job = self.jobs.get(&jobid).unwrap();
kill(job.pgid, SIGCONT)?;
}
Ok(())
}
}