diff --git a/crates/oyster/src/main.rs b/crates/oyster/src/main.rs index cf7b2c6..73c4f9f 100644 --- a/crates/oyster/src/main.rs +++ b/crates/oyster/src/main.rs @@ -5,7 +5,7 @@ use oyster_parser::ast::Code; use oyster_runtime::{Shell, Status}; fn main() { - let mut shell = Shell::default(); + let mut shell = Shell::new().unwrap(); let mut exit_code = Status::SUCCESS; loop { diff --git a/crates/oyster_runtime/Cargo.toml b/crates/oyster_runtime/Cargo.toml index 482542f..cf41047 100644 --- a/crates/oyster_runtime/Cargo.toml +++ b/crates/oyster_runtime/Cargo.toml @@ -13,7 +13,7 @@ thiserror = "1.0.35" [dependencies.nix] version = "~0.25.0" default-features = false -features = [ "fs", "ioctl", "process", "term" ] +features = [ "fs", "ioctl", "process", "signal", "term" ] [dev-dependencies] insta = "^1.15.0" diff --git a/crates/oyster_runtime/src/lib.rs b/crates/oyster_runtime/src/lib.rs index dada854..779376c 100644 --- a/crates/oyster_runtime/src/lib.rs +++ b/crates/oyster_runtime/src/lib.rs @@ -5,6 +5,10 @@ mod pipeline; use std::io; +use nix::sys::{ + signal::{self, SaFlags, SigAction, SigHandler, Signal}, + signalfd::SigSet, +}; use oyster_parser::ast; use thiserror::Error; @@ -33,12 +37,25 @@ pub enum RuntimeError { #[error("failed to spawn process: {0}")] SpawnFailed(#[source] io::Error), + + #[error("waitpid error: {0}")] + WaidPid(nix::Error), } -#[derive(Default)] pub struct Shell; impl Shell { + pub fn new() -> Result { + let ignore = SigAction::new(SigHandler::SigIgn, SaFlags::empty(), SigSet::empty()); + + unsafe { + signal::sigaction(Signal::SIGTSTP, &ignore)?; + signal::sigaction(Signal::SIGTTOU, &ignore)?; + } + + Ok(Shell) + } + pub fn run<'a>(&mut self, code: &'a ast::Code) -> Result { let mut last_status = Status::SUCCESS; diff --git a/crates/oyster_runtime/src/pipeline.rs b/crates/oyster_runtime/src/pipeline.rs index 07bad7f..f677b94 100644 --- a/crates/oyster_runtime/src/pipeline.rs +++ b/crates/oyster_runtime/src/pipeline.rs @@ -3,16 +3,16 @@ use std::{ ffi::{OsStr, OsString}, fs::File, io, - os::unix::{ - io::FromRawFd, - process::{CommandExt, ExitStatusExt}, - }, + os::unix::{io::FromRawFd, process::CommandExt}, process::{Child, Command, Stdio}, slice::Iter, }; use nix::{ + errno::Errno, fcntl::OFlag, + libc, + sys::wait::{self, WaitPidFlag, WaitStatus}, unistd::{self, Pid}, }; use oyster_parser::ast::{self, Redirect}; @@ -44,6 +44,15 @@ impl<'a> PreparedCommand<'a> { cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from)); cmd.process_group(pgid.as_raw()); + if *pgid == Pid::from_raw(0) { + unsafe { + cmd.pre_exec(move || { + let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpid()); + Ok(()) + }); + } + } + let child = match cmd.spawn() { Ok(child) => child, Err(err) => { @@ -92,7 +101,6 @@ impl Shell { &mut self, pipeline: &ast::Pipeline, ) -> Result { - let mut children = Vec::with_capacity(pipeline.0.len()); let mut cmds = pipeline.0.iter().map(PreparedCommand::from); let mut pgid = Pid::from_raw(0); @@ -106,28 +114,40 @@ impl Shell { Redirect::Stdout => last_cmd.stdout = Some(input), } - children.push(last_cmd.spawn(&mut pgid)?); + last_cmd.spawn(&mut pgid)?; last_cmd = cmd; } - children.push(last_cmd.spawn(&mut pgid)?); + last_cmd.spawn(&mut pgid)?; // TODO: kill children if error occured - // TODO: set foreground group then wait for foreground group - let mut last_status = Status::SUCCESS; - for mut c in children { - // TODO: handle error - let status = c.wait().unwrap(); - last_status = Status( - status - .code() - .unwrap_or_else(|| Status::SIG_BASE.0 + status.signal().unwrap()), - ); + let status = wait_pgid(pgid)?; + let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpgid(None).unwrap()); + + Ok(status) + } +} + +/// Wait for all processes of the specified job to terminate. +fn wait_pgid(pgid: Pid) -> Result { + let pgid = Pid::from_raw(-pgid.as_raw()); + let mut last_status = Status::SUCCESS; + + loop { + match wait::waitpid(pgid, Some(WaitPidFlag::WUNTRACED)) { + Ok(WaitStatus::Exited(_, code)) => last_status = Status(code), + Ok(WaitStatus::Signaled(_, signal, _)) => { + last_status = Status(Status::SIG_BASE.0 + signal as i32) + } + Ok(WaitStatus::Stopped(_, _)) => todo!("put job into background job list"), + Ok(_) => (), + Err(err) => match err { + Errno::ECHILD => break Ok(last_status), // no more children + _ => break Err(RuntimeError::WaidPid(err)), + }, } - - Ok(last_status) } } diff --git a/crates/oyster_runtime/tests/it/pipeline.rs b/crates/oyster_runtime/tests/it/pipeline.rs index 9627b20..12755cd 100644 --- a/crates/oyster_runtime/tests/it/pipeline.rs +++ b/crates/oyster_runtime/tests/it/pipeline.rs @@ -76,7 +76,7 @@ fn simple_command() { }; let actual = collect_output(|| { - let mut shell = Shell::default(); + let mut shell = Shell::new().unwrap(); shell.run(&ast).unwrap(); }); @@ -107,7 +107,7 @@ fn pipeline() { }; let actual = collect_output(|| { - let mut shell = Shell::default(); + let mut shell = Shell::new().unwrap(); shell.run(&ast).unwrap(); }); @@ -126,7 +126,7 @@ fn command_not_found() { }; // XXX: this relies on the command actually not existing, as unsetting PATH is rather complex - let mut shell = Shell::default(); + let mut shell = Shell::new().unwrap(); let actual = shell.run(&ast); assert_snapshot!(actual); @@ -143,7 +143,7 @@ fn permission_denied() { )]))]) }; - let mut shell = Shell::default(); + let mut shell = Shell::new().unwrap(); let actual = shell.run(&ast); assert_snapshot!(actual); @@ -164,7 +164,7 @@ fn multipart_word() { }; let actual = collect_output(|| { - let mut shell = Shell::default(); + let mut shell = Shell::new().unwrap(); shell.run(&ast).unwrap(); });