feat(runtime): add basic runtime
This commit is contained in:
parent
f2e4f43f35
commit
0bfa8b3c8f
13 changed files with 496 additions and 8 deletions
49
Cargo.lock
generated
49
Cargo.lock
generated
|
@ -8,6 +8,18 @@ version = "1.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.0"
|
||||
|
@ -29,9 +41,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
|
|||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.2"
|
||||
version = "0.12.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
|
@ -65,9 +77,9 @@ checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.126"
|
||||
version = "0.2.132"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836"
|
||||
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
|
@ -76,10 +88,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.13.0"
|
||||
name = "nix"
|
||||
version = "0.25.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
|
||||
checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
|
||||
|
||||
[[package]]
|
||||
name = "oyster"
|
||||
|
@ -87,6 +111,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"oyster_lineedit",
|
||||
"oyster_parser",
|
||||
"oyster_runtime",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -101,6 +126,16 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oyster_runtime"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"insta",
|
||||
"nix",
|
||||
"oyster_parser",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.40"
|
||||
|
|
|
@ -6,3 +6,4 @@ edition = "2021"
|
|||
[dependencies]
|
||||
oyster_lineedit = { path = "../oyster_lineedit" }
|
||||
oyster_parser = { path = "../oyster_parser" }
|
||||
oyster_runtime = { path = "../oyster_runtime" }
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use std::process;
|
||||
|
||||
use oyster_lineedit::readline;
|
||||
use oyster_parser::ast::Code;
|
||||
use oyster_runtime::{Shell, Status};
|
||||
|
||||
fn main() {
|
||||
let mut shell = Shell::default();
|
||||
let mut exit_code = Status::SUCCESS;
|
||||
|
||||
loop {
|
||||
let prog = readline("> ").unwrap();
|
||||
|
||||
|
@ -9,6 +15,9 @@ fn main() {
|
|||
break;
|
||||
}
|
||||
|
||||
Code::try_from(prog.as_ref()).unwrap();
|
||||
let ast = Code::try_from(prog.as_ref()).unwrap();
|
||||
exit_code = shell.run(&ast).unwrap();
|
||||
}
|
||||
|
||||
process::exit(exit_code.0);
|
||||
}
|
||||
|
|
19
crates/oyster_runtime/Cargo.toml
Normal file
19
crates/oyster_runtime/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "oyster_runtime"
|
||||
version = "0.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
oyster_parser = { path = "../oyster_parser" }
|
||||
thiserror = "1.0.35"
|
||||
|
||||
[dependencies.nix]
|
||||
version = "~0.25.0"
|
||||
default-features = false
|
||||
features = [ "fs", "ioctl", "process", "term" ]
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "^1.15.0"
|
53
crates/oyster_runtime/src/lib.rs
Normal file
53
crates/oyster_runtime/src/lib.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
//! The runtime for executing oyster programs.
|
||||
//! Panics when an invalid ast gets passed.
|
||||
|
||||
mod pipeline;
|
||||
|
||||
use std::io;
|
||||
|
||||
use oyster_parser::ast;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Exit status of a finished command.
|
||||
#[derive(Debug)]
|
||||
pub struct Status(pub i32);
|
||||
|
||||
impl Status {
|
||||
pub const SUCCESS: Status = Status(0);
|
||||
pub const COULD_NOT_EXEC: Status = Status(126);
|
||||
pub const COMMAND_NOT_FOUND: Status = Status(127);
|
||||
pub const SIG_BASE: Status = Status(128);
|
||||
}
|
||||
|
||||
/// Errors that occur during runtime.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum RuntimeError {
|
||||
#[error("failed to create pipe: {0}")]
|
||||
PipeCreationFailed(#[source] nix::Error),
|
||||
|
||||
#[error("command not found: {0}")]
|
||||
CommandNotFound(String),
|
||||
|
||||
#[error("permission denied: {0}")]
|
||||
PermissionDenied(String),
|
||||
|
||||
#[error("failed to spawn process: {0}")]
|
||||
SpawnFailed(#[source] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Shell;
|
||||
|
||||
impl Shell {
|
||||
pub fn run<'a>(&mut self, code: &'a ast::Code) -> Result<Status, RuntimeError> {
|
||||
let mut last_status = Status::SUCCESS;
|
||||
|
||||
for stmt in code.0.iter() {
|
||||
last_status = match stmt {
|
||||
ast::Statement::Pipeline(pipeline) => self.run_pipeline(pipeline)?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(last_status)
|
||||
}
|
||||
}
|
165
crates/oyster_runtime/src/pipeline.rs
Normal file
165
crates/oyster_runtime/src/pipeline.rs
Normal file
|
@ -0,0 +1,165 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
ffi::{OsStr, OsString},
|
||||
fs::File,
|
||||
io,
|
||||
os::unix::{
|
||||
io::FromRawFd,
|
||||
process::{CommandExt, ExitStatusExt},
|
||||
},
|
||||
process::{Child, Command, Stdio},
|
||||
slice::Iter,
|
||||
};
|
||||
|
||||
use nix::{
|
||||
fcntl::OFlag,
|
||||
unistd::{self, Pid},
|
||||
};
|
||||
use oyster_parser::ast::{self, Redirect};
|
||||
|
||||
use crate::{RuntimeError, Shell, Status};
|
||||
|
||||
/// A command that's ready to be run.
|
||||
struct PreparedCommand<'a> {
|
||||
cmd: Cow<'a, str>,
|
||||
args: WordBuilder<'a>,
|
||||
redirect: Redirect,
|
||||
stdin: Option<File>,
|
||||
stdout: Option<File>,
|
||||
stderr: Option<File>,
|
||||
}
|
||||
|
||||
impl<'a> PreparedCommand<'a> {
|
||||
/// Run this command with the given context.
|
||||
fn spawn(self, pgid: &mut Pid) -> Result<Child, RuntimeError> {
|
||||
let args = self.args.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 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),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let child_id = Pid::from_raw(child.id() as i32);
|
||||
if *pgid == Pid::from_raw(0) {
|
||||
*pgid = child_id;
|
||||
};
|
||||
|
||||
// prevent race conditions
|
||||
let _ = unistd::setpgid(child_id, *pgid);
|
||||
|
||||
Ok(child)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ast::Command<'a>> for PreparedCommand<'a> {
|
||||
fn from(command: &'a ast::Command<'a>) -> Self {
|
||||
let mut words = WordBuilder(command.0.iter());
|
||||
let cmd = words.next().expect("words need to have >1 parts");
|
||||
let args = words;
|
||||
let redirect = command.1;
|
||||
|
||||
PreparedCommand {
|
||||
cmd,
|
||||
args,
|
||||
redirect,
|
||||
stdin: None,
|
||||
stdout: None,
|
||||
stderr: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub(crate) fn run_pipeline(
|
||||
&mut self,
|
||||
pipeline: &ast::Pipeline,
|
||||
) -> Result<Status, RuntimeError> {
|
||||
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);
|
||||
|
||||
let mut last_cmd = cmds.next().expect("pipelines need to have >1 commands");
|
||||
for mut cmd in cmds {
|
||||
let (output, input) = create_pipe()?;
|
||||
cmd.stdin = Some(output);
|
||||
|
||||
match last_cmd.redirect {
|
||||
Redirect::None => (),
|
||||
Redirect::Stdout => last_cmd.stdout = Some(input),
|
||||
}
|
||||
|
||||
children.push(last_cmd.spawn(&mut pgid)?);
|
||||
|
||||
last_cmd = cmd;
|
||||
}
|
||||
|
||||
children.push(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()),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(last_status)
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new unix pipe.
|
||||
fn create_pipe() -> Result<(File, File), RuntimeError> {
|
||||
let (output, input) =
|
||||
unistd::pipe2(OFlag::O_CLOEXEC).map_err(RuntimeError::PipeCreationFailed)?;
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
1
crates/oyster_runtime/tests/it/main.rs
Normal file
1
crates/oyster_runtime/tests/it/main.rs
Normal file
|
@ -0,0 +1 @@
|
|||
mod pipeline;
|
172
crates/oyster_runtime/tests/it/pipeline.rs
Normal file
172
crates/oyster_runtime/tests/it/pipeline.rs
Normal file
|
@ -0,0 +1,172 @@
|
|||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
os::unix::io::FromRawFd,
|
||||
process,
|
||||
};
|
||||
|
||||
use insta::assert_debug_snapshot as assert_snapshot;
|
||||
use nix::{
|
||||
ioctl_write_int_bad, libc,
|
||||
pty::{self, OpenptyResult},
|
||||
sys,
|
||||
unistd::{self, ForkResult},
|
||||
};
|
||||
use oyster_runtime::Shell;
|
||||
|
||||
// TODO: test signal return codes
|
||||
|
||||
ioctl_write_int_bad!(tiocsctty, libc::TIOCSCTTY);
|
||||
|
||||
/// Forks to redirect stdin, stderr, stdout, then run the commands.
|
||||
/// Relies on inserting a NUL byte in the end, so there shouldn't be NUL in the output.
|
||||
fn collect_output<F>(mut f: F) -> String
|
||||
where
|
||||
F: FnMut(),
|
||||
{
|
||||
let OpenptyResult { master, slave } = pty::openpty(None, None).unwrap();
|
||||
|
||||
match unsafe { unistd::fork() }.unwrap() {
|
||||
ForkResult::Parent { child } => {
|
||||
sys::wait::waitpid(child, None).unwrap();
|
||||
}
|
||||
ForkResult::Child => {
|
||||
unistd::setsid().unwrap();
|
||||
unsafe { tiocsctty(slave, 0) }.unwrap();
|
||||
|
||||
unistd::dup2(slave, libc::STDIN_FILENO).unwrap();
|
||||
unistd::dup2(slave, libc::STDOUT_FILENO).unwrap();
|
||||
unistd::dup2(slave, libc::STDERR_FILENO).unwrap();
|
||||
|
||||
let _ = unistd::close(master);
|
||||
let _ = unistd::close(slave);
|
||||
|
||||
f();
|
||||
|
||||
process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
let master = unsafe { File::from_raw_fd(master) };
|
||||
let mut slave = unsafe { File::from_raw_fd(slave) };
|
||||
|
||||
slave.write(&[0]).unwrap();
|
||||
let mut r = BufReader::new(master);
|
||||
let mut buf = vec![];
|
||||
r.read_until(0, &mut buf).unwrap();
|
||||
|
||||
std::str::from_utf8(&buf[..buf.len() - 1])
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_command() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![Command(
|
||||
vec![
|
||||
Word(vec![WordPart::Text("echo")]),
|
||||
Word(vec![WordPart::Text("hi")]),
|
||||
],
|
||||
Redirect::None,
|
||||
)]))])
|
||||
};
|
||||
|
||||
let actual = collect_output(|| {
|
||||
let mut shell = Shell::default();
|
||||
shell.run(&ast).unwrap();
|
||||
});
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pipeline() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![
|
||||
Command(
|
||||
vec![
|
||||
Word(vec![WordPart::Text("echo")]),
|
||||
Word(vec![WordPart::Text("hi")]),
|
||||
],
|
||||
Redirect::Stdout,
|
||||
),
|
||||
Command(
|
||||
vec![
|
||||
Word(vec![WordPart::Text("wc")]),
|
||||
Word(vec![WordPart::Text("-c")]),
|
||||
],
|
||||
Redirect::None,
|
||||
),
|
||||
]))])
|
||||
};
|
||||
|
||||
let actual = collect_output(|| {
|
||||
let mut shell = Shell::default();
|
||||
shell.run(&ast).unwrap();
|
||||
});
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_not_found() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![Command(
|
||||
vec![Word(vec![WordPart::Text("this_command_doesnt_exist")])],
|
||||
Redirect::None,
|
||||
)]))])
|
||||
};
|
||||
|
||||
// XXX: this relies on the command actually not existing, as unsetting PATH is rather complex
|
||||
let mut shell = Shell::default();
|
||||
let actual = shell.run(&ast);
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_denied() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![Command(
|
||||
vec![Word(vec![WordPart::Text("/")])],
|
||||
Redirect::None,
|
||||
)]))])
|
||||
};
|
||||
|
||||
let mut shell = Shell::default();
|
||||
let actual = shell.run(&ast);
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multipart_word() {
|
||||
let ast = {
|
||||
use oyster_parser::ast::*;
|
||||
|
||||
Code(vec![Statement::Pipeline(Pipeline(vec![Command(
|
||||
vec![
|
||||
Word(vec![WordPart::Text("ec"), WordPart::Text("ho")]),
|
||||
Word(vec![WordPart::Text("hel"), WordPart::Text("lo")]),
|
||||
],
|
||||
Redirect::None,
|
||||
)]))])
|
||||
};
|
||||
|
||||
let actual = collect_output(|| {
|
||||
let mut shell = Shell::default();
|
||||
shell.run(&ast).unwrap();
|
||||
});
|
||||
|
||||
assert_snapshot!(actual);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
Err(
|
||||
CommandNotFound(
|
||||
"this_command_doesnt_exist",
|
||||
),
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
"hello\r\n"
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
Err(
|
||||
PermissionDenied(
|
||||
"/",
|
||||
),
|
||||
)
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
"3\r\n"
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
source: crates/oyster_runtime/tests/it/pipeline.rs
|
||||
expression: actual
|
||||
---
|
||||
"hi\r\n"
|
Loading…
Reference in a new issue