feat(runtime): implement builtins, add help

This commit is contained in:
buffet 2022-10-20 13:06:53 +00:00
parent 6c759e57cc
commit 092360bce6
7 changed files with 274 additions and 78 deletions

View file

@ -6,6 +6,7 @@ use oyster_runtime::{Shell, Status};
fn main() { fn main() {
let mut shell = Shell::new().unwrap(); let mut shell = Shell::new().unwrap();
shell.builtins_mut().add_defaults();
let mut exit_code = Status::SUCCESS; let mut exit_code = Status::SUCCESS;
loop { loop {

View file

@ -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<str>]),
}
pub static HELP: Builtin = {
pub fn help(shell: &mut Shell, _args: &[Cow<str>]) {
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<Item = &Builtin> {
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
}
}

View file

@ -1,13 +1,18 @@
//! The runtime for executing oyster programs. //! The runtime for executing oyster programs.
//! Panics when an invalid ast gets passed. //! Panics when an invalid ast gets passed.
pub mod builtins;
mod pipeline; mod pipeline;
use std::io; use std::io;
use nix::sys::{ use builtins::BuiltinMap;
signal::{self, SaFlags, SigAction, SigHandler, Signal}, use nix::{
signalfd::SigSet, errno::Errno,
sys::{
signal::{self, SaFlags, SigAction, SigHandler, Signal},
signalfd::SigSet,
},
}; };
use oyster_parser::ast; use oyster_parser::ast;
use thiserror::Error; use thiserror::Error;
@ -38,11 +43,16 @@ pub enum RuntimeError {
#[error("failed to spawn process: {0}")] #[error("failed to spawn process: {0}")]
SpawnFailed(#[source] io::Error), SpawnFailed(#[source] io::Error),
#[error("failed to fork: {0}")]
ForkFailed(#[source] Errno),
#[error("waitpid error: {0}")] #[error("waitpid error: {0}")]
WaidPid(nix::Error), WaidPid(nix::Error),
} }
pub struct Shell; pub struct Shell {
builtins: BuiltinMap,
}
impl Shell { impl Shell {
pub fn new() -> Result<Shell, nix::Error> { pub fn new() -> Result<Shell, nix::Error> {
@ -53,7 +63,9 @@ impl Shell {
signal::sigaction(Signal::SIGTTOU, &ignore)?; signal::sigaction(Signal::SIGTTOU, &ignore)?;
} }
Ok(Shell) Ok(Shell {
builtins: Default::default(),
})
} }
pub fn run<'a>(&mut self, code: &'a ast::Code) -> Result<Status, RuntimeError> { pub fn run<'a>(&mut self, code: &'a ast::Code) -> Result<Status, RuntimeError> {

View file

@ -3,9 +3,11 @@ use std::{
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
fs::File, fs::File,
io, io,
os::unix::{io::FromRawFd, process::CommandExt}, os::unix::{
process::{Child, Command, Stdio}, io::{AsRawFd, FromRawFd, RawFd},
slice::Iter, process::CommandExt,
},
process::{self, Command, Stdio},
}; };
use nix::{ use nix::{
@ -17,16 +19,23 @@ use nix::{
signalfd::SigSet, signalfd::SigSet,
wait::{self, WaitPidFlag, WaitStatus}, wait::{self, WaitPidFlag, WaitStatus},
}, },
unistd::{self, Pid}, unistd::{self, ForkResult, Pid},
}; };
use oyster_parser::ast::{self, Redirect}; 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. /// A command that's ready to be run.
struct PreparedCommand<'a> { struct PreparedCommand<'a> {
kind: CommandKind,
cmd: Cow<'a, str>, cmd: Cow<'a, str>,
args: WordBuilder<'a>, args: Vec<Cow<'a, str>>,
redirect: Redirect, redirect: Redirect,
stdin: Option<File>, stdin: Option<File>,
stdout: Option<File>, stdout: Option<File>,
@ -35,13 +44,33 @@ struct PreparedCommand<'a> {
impl<'a> PreparedCommand<'a> { impl<'a> PreparedCommand<'a> {
/// Create a new PreparedCommand. /// Create a new PreparedCommand.
fn new(command: &'a ast::Command) -> Self { fn new(command: &'a ast::Command, shell: &Shell) -> Self {
let mut words = WordBuilder(command.0.iter()); 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 cmd = words.next().expect("words need to have >1 parts");
let args = words; let args = words.collect();
let redirect = command.1; let redirect = command.1;
let kind = match shell.builtins().get(&cmd) {
Some(builtin) => CommandKind::Builtin(builtin),
None => CommandKind::External,
};
PreparedCommand { PreparedCommand {
kind,
cmd, cmd,
args, args,
redirect, redirect,
@ -52,53 +81,87 @@ impl<'a> PreparedCommand<'a> {
} }
/// Run this command with the given context. /// Run this command with the given context.
fn spawn(self, pgid: &mut Pid) -> Result<Child, RuntimeError> { fn spawn(self, shell: &mut Shell, pgid: &mut Pid) -> Result<(), RuntimeError> {
let args = self.args.map(|w| match w { 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::Borrowed(s) => Cow::Borrowed(OsStr::new(s)),
Cow::Owned(s) => Cow::Owned(OsString::from(s)), Cow::Owned(s) => Cow::Owned(OsString::from(s)),
}); });
let mut cmd = Command::new(self.cmd.as_ref()); let child = match self.kind {
cmd.args(args); CommandKind::External => {
cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from)); let mut cmd = Command::new(self.cmd.as_ref());
cmd.stdout(self.stdout.map_or(Stdio::inherit(), Stdio::from)); cmd.args(args);
cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from)); cmd.stdin(self.stdin.map_or(Stdio::inherit(), Stdio::from));
cmd.process_group(pgid.as_raw()); cmd.stdout(self.stdout.map_or(Stdio::inherit(), Stdio::from));
cmd.stderr(self.stderr.map_or(Stdio::inherit(), Stdio::from));
unsafe { unsafe {
cmd.pre_exec(move || { cmd.pre_exec(pre_exec);
let _ = unistd::tcsetpgrp(libc::STDIN_FILENO, unistd::getpid()); }
let default = SigAction::new(SigHandler::SigDfl, SaFlags::empty(), SigSet::empty()); let child = match cmd.spawn() {
let _ = signal::sigaction(signal::Signal::SIGTSTP, &default); Ok(child) => child,
let _ = signal::sigaction(signal::Signal::SIGTTOU, &default); Err(err) => {
return Err(match err.kind() {
Ok(()) io::ErrorKind::NotFound => {
}); RuntimeError::CommandNotFound(self.cmd.into_owned())
} }
io::ErrorKind::PermissionDenied => {
let child = match cmd.spawn() { RuntimeError::PermissionDenied(self.cmd.into_owned())
Ok(child) => child, }
Err(err) => { _ => RuntimeError::SpawnFailed(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), };
})
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<File>, 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) { if *pgid == Pid::from_raw(0) {
*pgid = child_id; *pgid = child;
}; }
// prevent race conditions // prevent race conditions
let _ = unistd::setpgid(child_id, *pgid); let _ = unistd::setpgid(child, *pgid);
Ok(child) Ok(())
} }
} }
@ -109,10 +172,14 @@ impl Shell {
) -> Result<Status, RuntimeError> { ) -> Result<Status, RuntimeError> {
let mut pgid = Pid::from_raw(0); let mut pgid = Pid::from_raw(0);
let status = (|| { 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"); let mut last_cmd = cmds
for mut cmd in 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()?; let (output, input) = create_pipe()?;
cmd.stdin = Some(output); cmd.stdin = Some(output);
@ -121,12 +188,12 @@ impl Shell {
Redirect::Stdout => last_cmd.stdout = Some(input), Redirect::Stdout => last_cmd.stdout = Some(input),
} }
last_cmd.spawn(&mut pgid)?; last_cmd.spawn(self, &mut pgid)?;
last_cmd = cmd; last_cmd = cmd;
} }
last_cmd.spawn(&mut pgid)?; last_cmd.spawn(self, &mut pgid)?;
wait_pgid(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)) }) 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::Item> {
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
})
}
}

View file

@ -13,7 +13,7 @@ use nix::{
sys, sys,
unistd::{self, ForkResult}, unistd::{self, ForkResult},
}; };
use oyster_runtime::Shell; use oyster_runtime::{builtins::Builtin, Shell};
// TODO: test signal return codes // TODO: test signal return codes
@ -170,3 +170,66 @@ fn multipart_word() {
assert_snapshot!(actual); 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);
}

View file

@ -0,0 +1,5 @@
---
source: crates/oyster_runtime/tests/it/pipeline.rs
expression: actual
---
"15\r\n"

View file

@ -0,0 +1,5 @@
---
source: crates/oyster_runtime/tests/it/pipeline.rs
expression: actual
---
"this is a test\r\n"