Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement jobs builtin command #3

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions RESOURCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@
- https://www.howtogeek.com/440848/how-to-run-and-control-background-processes-on-linux/
- https://wiki.bash-hackers.org/howto/redirection_tutorial
- https://stackoverflow.com/questions/8319484/regarding-background-processes-using-fork-and-child-processes-in-my-dummy-shel
- Job Control Signals: https://www.gnu.org/software/libc/manual/html_node/Job-Control-Signals.html
- Implementing a Job Control Shell: https://www.andrew.cmu.edu/course/15-310/applications/homework/homework4/lab4.pdf
6 changes: 5 additions & 1 deletion TASKS_AND_BUGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@
- [X] add support for redirect <& operator
- [X] add support for fd in redirect operations
- [ ] add support for $() in shell
- [ ] add support for & parsing for job control
- [ ] add support for background processes
- [X] add support for `jobs` command
- [ ] add support to bring commands in foreground again
- [ ] add support for proper process chains i.e. jobs
- [ ] add support for another execution mode (job control) in engine
- [ ] seperate types from engine, and into a new file

## Bonus Tasks:

Expand Down
8 changes: 8 additions & 0 deletions src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ impl Command {
})
.collect()
}

pub fn as_string(&self) -> String {
self.tokens.iter().fold(String::new(), |mut acc, token| {
acc += " ";
acc += &token.lexeme;
return acc;
})
}
}

// Old Lexing + Parsing
Expand Down
224 changes: 194 additions & 30 deletions src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ use nix::{
stat::Mode,
wait::{waitpid, WaitStatus},
},
unistd::{chdir, close, dup2, execve, fork, pipe, setpgid, ForkResult, Pid, getpid},
unistd::{
chdir, close, dup2, execve, fork, getpid, isatty, pipe, setpgid, tcsetpgrp, ForkResult, Pid,
},
};
use signal_hook::consts;

use std::{
collections::HashMap,
convert::Infallible,
ffi::{CStr, CString},
fmt::{Display, Formatter},
io,
os::unix::prelude::OsStrExt,
path::{Path, PathBuf},
Expand All @@ -35,15 +38,68 @@ use crate::{
frontend::{write_error_to_shell, write_to_stderr, write_to_stdout, Prompt},
};

const BUILTIN_COMMANDS: [&str; 2] = ["cd", "exec"];
const BUILTIN_COMMANDS: [&str; 4] = ["cd", "exec", "jobs", "fg"];

#[derive(Clone, Debug)]
pub enum ProcessStatus {
Running,
// Suspended
// Done,
}

impl Display for ProcessStatus {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ProcessStatus::Running => write!(f, "running"),
// ProcessStatus::Suspended => write!(f, "suspended"),
}
}
}

#[derive(Clone, Debug)]
pub struct Process {
pid: Pid,
cmd: String,
status: ProcessStatus,
}

impl Process {
fn new(pid: Pid, cmd: String, status: ProcessStatus) -> Self {
Process { pid, cmd, status }
}
}

impl Display for Process {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", self.pid, self.status, self.cmd)
}
}

#[derive(Clone, Debug)]
pub struct Job {
processes: Vec<Process>,
pgrp: Pid,
}

impl Job {
fn new() -> Self {
Self {
processes: Vec::new(),
pgrp: Pid::from_raw(0),
}
}
}

#[derive(Clone, Debug)]
pub struct Engine {
pub execution_successful: bool,
pub env_paths: Vec<String>,
execution_mode: ExecutionMode,
pub is_interactive: bool,
// Operations to be done on different `fd`s
fds_ops: HashMap<i32, FdOperation>,
job: Job,
tty_fd: i32,
}

#[derive(Copy, Clone, Debug)]
Expand All @@ -62,13 +118,18 @@ enum ExecutionMode {
}

impl Engine {
pub fn new() -> Self {
Self {
pub fn new(is_interactive: bool) -> anyhow::Result<Self> {
let tty_fd = get_tty_fd()?;

Ok(Self {
execution_successful: true,
env_paths: parse_paths(),
execution_mode: ExecutionMode::Normal,
fds_ops: HashMap::new(),
}
job: Job::new(),
tty_fd,
is_interactive,
})
}

pub fn fire_on(&mut self) -> anyhow::Result<()> {
Expand Down Expand Up @@ -341,6 +402,40 @@ impl Engine {
self.parse_and_execute(&command.tokens)?;
Ok(())
}
"jobs" => {
self.job
.processes
.iter()
.enumerate()
.for_each(|(idx, process)| {
// FIXME: use panic safe write_to_stdout here
println!("{} {}", idx, process);
});

Ok(())
}
"fg" => {
let pgrp = match self.job.processes.first() {
Some(first_process) => {
println!("first process pid: {}", first_process.pid);
first_process.pid
}
None => getpid(),
};

let curr_pid = getpid();
println!(
"pid received from getpid for shell in foreground: {}",
curr_pid
);

// let curr_tty_pid = tcgetattr(self.tty_fd)?;
// curr_tty_pid.

println!("TTY FD: {}", self.tty_fd);
tcsetpgrp(self.tty_fd, pgrp)?;
Ok(())
}
_ => Err(ShellError::CommandNotFound(cmd_str.to_string()).into()),
}
}
Expand All @@ -355,9 +450,24 @@ impl Engine {
Ok(ForkResult::Parent {
child: child_pid, ..
}) => {
if matches!(self.execution_mode, ExecutionMode::Background) {
setpgid(child_pid, child_pid)?;
if self.is_interactive {
if self.job.processes.len() == 0 {
self.job.pgrp = child_pid;
}

let command = command.expect(
"invalid state: should have had command in background process execution flow",
);
self.job.processes.push(Process::new(
child_pid,
command.as_string(),
ProcessStatus::Running,
));

setpgid(child_pid, self.job.pgrp)?;
}
// if matches!(self.execution_mode, ExecutionMode::Background) {
// }

for (fd, value) in &self.fds_ops {
match value {
Expand Down Expand Up @@ -386,6 +496,7 @@ impl Engine {
// condition and let it wait on each command execution
if !matches!(self.execution_mode, ExecutionMode::Pipeline)
&& !matches!(self.execution_mode, ExecutionMode::Background)
&& !self.is_interactive
{
let wait_status = waitpid(child_pid, None).expect(&format!(
"Expected to wait for child with pid: {:?}",
Expand All @@ -409,34 +520,49 @@ impl Engine {
}
}
}
Ok(ForkResult::Child) => match execute_mode {
ExecuteMode::Normal => {
if matches!(self.execution_mode, ExecutionMode::Background) {
let pgrp = getpid();
setpgid(Pid::from_raw(0), pgrp)?;
}
Ok(ForkResult::Child) => {
self.tty_fd = get_tty_fd()?;
match execute_mode {
ExecuteMode::Normal => {
if self.is_interactive {
let curr_pid = getpid();

if self.job.processes.len() == 0 {
// first process
self.job.pgrp = curr_pid;
}

let command =
command.expect("internal error: should have contained valid command");
println!("pgrp: {}", self.job.pgrp);
// passing frist as 0, makes it take it's own
// pid by default
setpgid(curr_pid, self.job.pgrp)?;
}
// tcsetpgrp(self.tty_fd, pgrp)?;

for (fd, op) in &self.fds_ops {
match op {
FdOperation::Set { to } => {
dup2(*to, *fd)?;
close(*to)?;
}
FdOperation::Close => {
close(*fd)?;
// if matches!(self.execution_mode, ExecutionMode::Background) {}

let command =
command.expect("internal error: should have contained valid command");

for (fd, op) in &self.fds_ops {
match op {
FdOperation::Set { to } => {
dup2(*to, *fd)?;
close(*to)?;
}
FdOperation::Close => {
close(*fd)?;
}
}
}
}

execute_external_cmd(command.clone(), self.env_paths.clone())?;
}
ExecuteMode::Subshell(tokens) => {
self.parse_and_execute(&tokens)?;
execute_external_cmd(command.clone(), self.env_paths.clone())?;
}
ExecuteMode::Subshell(tokens) => {
self.parse_and_execute(&tokens)?;
}
}
},
}
Err(err) => panic!("Fork failed: {err:?}"),
}

Expand Down Expand Up @@ -522,6 +648,42 @@ fn parse_paths() -> Vec<String> {
.collect();
}

fn get_tty_fd() -> anyhow::Result<i32> {
let tty_fd;

match open("/dev/tty", OFlag::O_RDWR, Mode::S_IRWXU) {
Ok(fd) => {
tty_fd = fd;
}
Err(_) => {
// Comment from busybox:
/* BTW, bash will try to open(ttyname(0)) if open("/dev/tty") fails.
* That sometimes helps to acquire controlling tty.
* Obviously, a workaround for bugs when someone
* failed to provide a controlling tty to bash! :) */

let mut fd = 2;

loop {
if isatty(fd)? {
tty_fd = fd;
break;
}

fd -= 1;
if fd < 0 {
return Err(ShellError::EngineError(
"can't access tty; job control turned off".into(),
)
.into());
}
}
}
}

Ok(tty_fd)
}

#[cfg(test)]
mod tests {
use crate::command::lexer::Lexer;
Expand All @@ -539,7 +701,9 @@ mod tests {
}

fn check(input_str: &str) -> Engine {
let mut engine = Engine::new();
let is_interactive = true;
let mut engine = Engine::new(is_interactive)
.expect("engine should have been able to be created successfully");

let ip_str = input_str.to_string() + "\n";
let lexer = get_tokens(&ip_str).expect("lexer failed, check lexer tests");
Expand Down
2 changes: 2 additions & 0 deletions src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub enum ShellError {
LexError(LexError),
#[error("dss: internal error [BUG]: {0}\n")]
InternalError(String),
#[error("dss: engine error: {0}\n")]
EngineError(String),
}

#[derive(Error, Debug)]
Expand Down
4 changes: 3 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use engine::Engine;
// FIXME: Refine APIs exposed by Engine and Command

fn main() -> anyhow::Result<()> {
let mut engine = Engine::new();
// FIXME: calculate this using process described here: https://www.gnu.org/software/libc/manual/html_node/Initializing-the-Shell.html
let is_interactive = true;
let mut engine = Engine::new(is_interactive)?;

engine.fire_on()?;

Expand Down