diff --git a/crates/oyster/src/main.rs b/crates/oyster/src/main.rs index 73c4f9f..84b2f61 100644 --- a/crates/oyster/src/main.rs +++ b/crates/oyster/src/main.rs @@ -6,6 +6,7 @@ use oyster_runtime::{Shell, Status}; fn main() { let mut shell = Shell::new().unwrap(); + shell.builtins_mut().add_defaults(); let mut exit_code = Status::SUCCESS; loop { diff --git a/crates/oyster_runtime/src/builtins/mod.rs b/crates/oyster_runtime/src/builtins/mod.rs new file mode 100644 index 0000000..a0ab658 --- /dev/null +++ b/crates/oyster_runtime/src/builtins/mod.rs @@ -0,0 +1,68 @@ +use std::{borrow::Cow, collections::HashMap}; + +use crate::Shell; + +#[derive(Clone, Copy)] +pub struct Builtin { + pub name: &'static str, + pub description: &'static str, + pub fun: fn(shell: &mut Shell, args: &[Cow]), +} + +pub static HELP: Builtin = { + pub fn help(shell: &mut Shell, _args: &[Cow]) { + println!( + r"oyster help: + +These are the loaded builtins:" + ); + + for builtin in shell.builtins().iter() { + println!(" {: <8} {}", builtin.name, builtin.description); + } + } + + Builtin { + name: "help", + description: "prints help for different builtins", + fun: help, + } +}; + +/// Used to register and retrieve builtins. +#[derive(Default)] +pub struct BuiltinMap(HashMap<&'static str, Builtin>); + +impl BuiltinMap { + /// Register a new builtin. + pub fn add(&mut self, builtin: Builtin) { + self.0.insert(builtin.name, builtin); + } + + /// Get a builtin with a given name, if it exists. + pub fn get(&self, name: &str) -> Option<&Builtin> { + self.0.get(name) + } + + /// Returns an iterator over all currently registered builtins. + pub fn iter(&self) -> impl Iterator { + self.0.iter().map(|(_, v)| v) + } + + /// Add default builtins. + pub fn add_defaults(&mut self) { + self.add(HELP); + } +} + +impl Shell { + /// Retrieve builtins. + pub fn builtins(&self) -> &BuiltinMap { + &self.builtins + } + + /// Retrieve builtins, mutably. + pub fn builtins_mut(&mut self) -> &mut BuiltinMap { + &mut self.builtins + } +} diff --git a/crates/oyster_runtime/src/lib.rs b/crates/oyster_runtime/src/lib.rs index 779376c..eebe0cb 100644 --- a/crates/oyster_runtime/src/lib.rs +++ b/crates/oyster_runtime/src/lib.rs @@ -1,13 +1,18 @@ //! The runtime for executing oyster programs. //! Panics when an invalid ast gets passed. +pub mod builtins; mod pipeline; use std::io; -use nix::sys::{ - signal::{self, SaFlags, SigAction, SigHandler, Signal}, - signalfd::SigSet, +use builtins::BuiltinMap; +use nix::{ + errno::Errno, + sys::{ + signal::{self, SaFlags, SigAction, SigHandler, Signal}, + signalfd::SigSet, + }, }; use oyster_parser::ast; use thiserror::Error; @@ -38,11 +43,16 @@ pub enum RuntimeError { #[error("failed to spawn process: {0}")] SpawnFailed(#[source] io::Error), + #[error("failed to fork: {0}")] + ForkFailed(#[source] Errno), + #[error("waitpid error: {0}")] WaidPid(nix::Error), } -pub struct Shell; +pub struct Shell { + builtins: BuiltinMap, +} impl Shell { pub fn new() -> Result { @@ -53,7 +63,9 @@ impl Shell { signal::sigaction(Signal::SIGTTOU, &ignore)?; } - Ok(Shell) + Ok(Shell { + builtins: Default::default(), + }) } pub fn run<'a>(&mut self, code: &'a ast::Code) -> Result { diff --git a/crates/oyster_runtime/src/pipeline.rs b/crates/oyster_runtime/src/pipeline.rs index 5ee036b..6708110 100644 --- a/crates/oyster_runtime/src/pipeline.rs +++ b/crates/oyster_runtime/src/pipeline.rs @@ -3,9 +3,11 @@ use std::{ ffi::{OsStr, OsString}, fs::File, io, - os::unix::{io::FromRawFd, process::CommandExt}, - process::{Child, Command, Stdio}, - slice::Iter, + os::unix::{ + io::{AsRawFd, FromRawFd, RawFd}, + process::CommandExt, + }, + process::{self, Command, Stdio}, }; use nix::{ @@ -17,16 +19,23 @@ use nix::{ signalfd::SigSet, wait::{self, WaitPidFlag, WaitStatus}, }, - unistd::{self, Pid}, + unistd::{self, ForkResult, Pid}, }; use oyster_parser::ast::{self, Redirect}; -use crate::{RuntimeError, Shell, Status}; +use crate::{builtins::Builtin, RuntimeError, Shell, Status}; + +/// The specific kind of the command. +enum CommandKind { + External, + Builtin(Builtin), +} /// A command that's ready to be run. struct PreparedCommand<'a> { + kind: CommandKind, cmd: Cow<'a, str>, - args: WordBuilder<'a>, + args: Vec>, redirect: Redirect, stdin: Option, stdout: Option, @@ -35,13 +44,33 @@ struct PreparedCommand<'a> { impl<'a> PreparedCommand<'a> { /// Create a new PreparedCommand. - fn new(command: &'a ast::Command) -> Self { - let mut words = WordBuilder(command.0.iter()); + fn new(command: &'a ast::Command, shell: &Shell) -> Self { + let mut words = command.0.iter().map(|word| { + let mut words = word.0.iter(); + + let mut s = match words.next().expect("words need to have >1 parts") { + ast::WordPart::Text(text) => Cow::from(*text), + }; + + for part in words { + match part { + ast::WordPart::Text(text) => s.to_mut().push_str(text), + } + } + + s + }); let cmd = words.next().expect("words need to have >1 parts"); - let args = words; + let args = words.collect(); let redirect = command.1; + let kind = match shell.builtins().get(&cmd) { + Some(builtin) => CommandKind::Builtin(builtin), + None => CommandKind::External, + }; + PreparedCommand { + kind, cmd, args, redirect, @@ -52,53 +81,87 @@ impl<'a> PreparedCommand<'a> { } /// Run this command with the given context. - fn spawn(self, pgid: &mut Pid) -> Result { - let args = self.args.map(|w| match w { + fn spawn(self, shell: &mut Shell, pgid: &mut Pid) -> Result<(), RuntimeError> { + let pre_exec = { + let pgid = *pgid; + move || { + let _ = unistd::setpgid(Pid::from_raw(0), pgid); + let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpid()); + + let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); + unsafe { + let _ = signal::sigaction(signal::Signal::SIGTSTP, &default); + let _ = signal::sigaction(signal::Signal::SIGTTOU, &default); + } + + Ok(()) + } + }; + + let args = self.args.iter().map(|w| match w { Cow::Borrowed(s) => Cow::Borrowed(OsStr::new(s)), Cow::Owned(s) => Cow::Owned(OsString::from(s)), }); - let mut cmd = Command::new(self.cmd.as_ref()); - cmd.args(args); - cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from)); - cmd.stdout(self.stdout.map_or(Stdio::inherit(), Stdio::from)); - cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from)); - cmd.process_group(pgid.as_raw()); + let child = match self.kind { + CommandKind::External => { + let mut cmd = Command::new(self.cmd.as_ref()); + cmd.args(args); + cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from)); + cmd.stdout(self.stdout.map_or(Stdio::inherit(), Stdio::from)); + cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from)); - unsafe { - cmd.pre_exec(move || { - let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpid()); + unsafe { + cmd.pre_exec(pre_exec); + } - let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); - let _ = signal::sigaction(signal::Signal::SIGTSTP, &default); - let _ = signal::sigaction(signal::Signal::SIGTTOU, &default); - - Ok(()) - }); - } - - let child = match cmd.spawn() { - Ok(child) => child, - Err(err) => { - return Err(match err.kind() { - io::ErrorKind::NotFound => RuntimeError::CommandNotFound(self.cmd.into_owned()), - io::ErrorKind::PermissionDenied => { - RuntimeError::PermissionDenied(self.cmd.into_owned()) + let child = match cmd.spawn() { + Ok(child) => child, + Err(err) => { + return Err(match err.kind() { + io::ErrorKind::NotFound => { + RuntimeError::CommandNotFound(self.cmd.into_owned()) + } + io::ErrorKind::PermissionDenied => { + RuntimeError::PermissionDenied(self.cmd.into_owned()) + } + _ => RuntimeError::SpawnFailed(err), + }) } - _ => RuntimeError::SpawnFailed(err), - }) + }; + + Pid::from_raw(child.id() as i32) + } + CommandKind::Builtin(builtin) => { + match unsafe { unistd::fork() }.map_err(RuntimeError::ForkFailed)? { + ForkResult::Parent { child } => child, + ForkResult::Child => { + fn redirect(old: Option, new: RawFd) { + if let Some(old) = old { + let _ = unistd::dup2(old.as_raw_fd(), new); + } + } + + redirect(self.stdin, libc::STDIN_FILENO); + redirect(self.stdout, libc::STDOUT_FILENO); + redirect(self.stderr, libc::STDERR_FILENO); + + let _ = pre_exec(); + (builtin.fun)(shell, &self.args); + process::exit(0); + } + } } }; - let child_id = Pid::from_raw(child.id() as i32); if *pgid == Pid::from_raw(0) { - *pgid = child_id; - }; + *pgid = child; + } // prevent race conditions - let _ = unistd::setpgid(child_id, *pgid); + let _ = unistd::setpgid(child, *pgid); - Ok(child) + Ok(()) } } @@ -109,10 +172,14 @@ impl Shell { ) -> Result { let mut pgid = Pid::from_raw(0); let status = (|| { - let mut cmds = pipeline.0.iter().map(PreparedCommand::new); + let mut cmds = pipeline.0.iter(); - let mut last_cmd = cmds.next().expect("pipelines need to have >1 commands"); - for mut cmd in cmds { + let mut last_cmd = cmds + .next() + .map(|cmd| PreparedCommand::new(cmd, self)) + .expect("pipelines need to have >1 commands"); + for cmd in cmds { + let mut cmd = PreparedCommand::new(cmd, self); let (output, input) = create_pipe()?; cmd.stdin = Some(output); @@ -121,12 +188,12 @@ impl Shell { Redirect::Stdout => last_cmd.stdout = Some(input), } - last_cmd.spawn(&mut pgid)?; + last_cmd.spawn(self, &mut pgid)?; last_cmd = cmd; } - last_cmd.spawn(&mut pgid)?; + last_cmd.spawn(self, &mut pgid)?; wait_pgid(pgid) })(); @@ -170,28 +237,3 @@ fn create_pipe() -> Result<(File, File), RuntimeError> { Ok(unsafe { (File::from_raw_fd(output), File::from_raw_fd(input)) }) } - -/// Build words from WordParts. -struct WordBuilder<'a>(Iter<'a, ast::Word<'a>>); - -impl<'a> Iterator for WordBuilder<'a> { - type Item = Cow<'a, str>; - - fn next(&mut self) -> Option { - self.0.next().map(|word| { - let mut words = word.0.iter(); - - let mut s = match words.next().expect("words need to have >1 parts") { - ast::WordPart::Text(text) => Cow::from(*text), - }; - - for part in words { - match part { - ast::WordPart::Text(text) => s.to_mut().push_str(text), - } - } - - s - }) - } -} diff --git a/crates/oyster_runtime/tests/it/pipeline.rs b/crates/oyster_runtime/tests/it/pipeline.rs index 12755cd..c2669e4 100644 --- a/crates/oyster_runtime/tests/it/pipeline.rs +++ b/crates/oyster_runtime/tests/it/pipeline.rs @@ -13,7 +13,7 @@ use nix::{ sys, unistd::{self, ForkResult}, }; -use oyster_runtime::Shell; +use oyster_runtime::{builtins::Builtin, Shell}; // TODO: test signal return codes @@ -170,3 +170,66 @@ fn multipart_word() { assert_snapshot!(actual); } + +#[test] +fn simple_builtin() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![Command( + vec![Word(vec![WordPart::Text("test_builtin")])], + Redirect::None, + )]))]) + }; + + let actual = collect_output(|| { + let mut shell = Shell::new().unwrap(); + shell.builtins_mut().add(Builtin { + name: "test_builtin", + description: "test", + fun: |_, _| { + // XXX: this is a workaround around libtest's use of io::set_output_capture + let _ = write!(std::io::stdout(), "this is a test\n"); + }, + }); + shell.run(&ast).unwrap(); + }); + + assert_snapshot!(actual); +} + +#[test] +fn builtin_redirection() { + let ast = { + use oyster_parser::ast::*; + + Code(vec![Statement::Pipeline(Pipeline(vec![ + Command( + vec![Word(vec![WordPart::Text("test_builtin")])], + Redirect::Stdout, + ), + Command( + vec![ + Word(vec![WordPart::Text("wc")]), + Word(vec![WordPart::Text("-c")]), + ], + Redirect::None, + ), + ]))]) + }; + + let actual = collect_output(|| { + let mut shell = Shell::new().unwrap(); + shell.builtins_mut().add(Builtin { + name: "test_builtin", + description: "test", + fun: |_, _| { + // XXX: this is a workaround around libtest's use of io::set_output_capture + let _ = write!(std::io::stdout(), "this is a test\n"); + }, + }); + shell.run(&ast).unwrap(); + }); + + assert_snapshot!(actual); +} diff --git a/crates/oyster_runtime/tests/it/snapshots/it__pipeline__builtin_redirection.snap b/crates/oyster_runtime/tests/it/snapshots/it__pipeline__builtin_redirection.snap new file mode 100644 index 0000000..351b518 --- /dev/null +++ b/crates/oyster_runtime/tests/it/snapshots/it__pipeline__builtin_redirection.snap @@ -0,0 +1,5 @@ +--- +source: crates/oyster_runtime/tests/it/pipeline.rs +expression: actual +--- +"15\r\n" diff --git a/crates/oyster_runtime/tests/it/snapshots/it__pipeline__simple_builtin.snap b/crates/oyster_runtime/tests/it/snapshots/it__pipeline__simple_builtin.snap new file mode 100644 index 0000000..7ad9b91 --- /dev/null +++ b/crates/oyster_runtime/tests/it/snapshots/it__pipeline__simple_builtin.snap @@ -0,0 +1,5 @@ +--- +source: crates/oyster_runtime/tests/it/pipeline.rs +expression: actual +--- +"this is a test\r\n"