diff options
Diffstat (limited to 'src/runner')
-rw-r--r-- | src/runner/builtins/command.rs | 373 | ||||
-rw-r--r-- | src/runner/builtins/mod.rs | 242 | ||||
-rw-r--r-- | src/runner/command.rs | 203 | ||||
-rw-r--r-- | src/runner/mod.rs | 499 | ||||
-rw-r--r-- | src/runner/prelude.rs | 1 | ||||
-rw-r--r-- | src/runner/sys.rs | 79 |
6 files changed, 1397 insertions, 0 deletions
diff --git a/src/runner/builtins/command.rs b/src/runner/builtins/command.rs new file mode 100644 index 0000000..16d8b40 --- /dev/null +++ b/src/runner/builtins/command.rs @@ -0,0 +1,373 @@ +use crate::runner::prelude::*; + +pub struct Command { + exe: crate::parse::Exe, + f: super::Builtin, + cfg: Cfg, +} + +impl Command { + pub fn new( + exe: crate::parse::Exe, + io: Io, + ) -> Result<Self, crate::parse::Exe> { + if let Some(s) = exe.exe().to_str() { + if let Some(f) = super::BUILTINS.get(s) { + Ok(Self { + exe, + f, + cfg: Cfg::new(io), + }) + } else { + Err(exe) + } + } else { + Err(exe) + } + } + + pub fn stdin(&mut self, fh: std::fs::File) { + self.cfg.io.set_stdin(fh); + } + + pub fn stdout(&mut self, fh: std::fs::File) { + self.cfg.io.set_stdout(fh); + } + + pub fn stderr(&mut self, fh: std::fs::File) { + self.cfg.io.set_stderr(fh); + } + + // Safety: see pre_exec in tokio::process::Command (this is just a + // wrapper) + pub unsafe fn pre_exec<F>(&mut self, f: F) + where + F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, + { + self.cfg.pre_exec(f); + } + + pub fn apply_redirects(&mut self, redirects: &[crate::parse::Redirect]) { + self.cfg.io.apply_redirects(redirects); + } + + pub fn spawn(self, env: &Env) -> Result<Child> { + let Self { f, exe, cfg } = self; + (f)(exe, env, cfg) + } +} + +pub struct Cfg { + io: Io, + pre_exec: Option< + Box<dyn 'static + FnMut() -> std::io::Result<()> + Send + Sync>, + >, +} + +impl Cfg { + fn new(io: Io) -> Self { + Self { io, pre_exec: None } + } + + pub fn io(&self) -> &Io { + &self.io + } + + // Safety: see pre_exec in tokio::process::Command (this is just a + // wrapper) + pub unsafe fn pre_exec<F>(&mut self, f: F) + where + F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, + { + self.pre_exec = Some(Box::new(f)); + } + + pub fn setup_command(mut self, cmd: &mut crate::runner::Command) { + self.io.setup_command(cmd); + if let Some(pre_exec) = self.pre_exec.take() { + // Safety: pre_exec can only have been set by calling the pre_exec + // method, which is itself unsafe, so the safety comments at the + // point where that is called are the relevant ones + unsafe { cmd.pre_exec(pre_exec) }; + } + } +} + +#[derive(Debug, Clone)] +pub struct Io { + fds: std::collections::HashMap< + std::os::unix::io::RawFd, + std::sync::Arc<File>, + >, +} + +impl Io { + pub fn new() -> Self { + Self { + fds: std::collections::HashMap::new(), + } + } + + fn stdin(&self) -> Option<std::sync::Arc<File>> { + self.fds.get(&0).map(std::sync::Arc::clone) + } + + pub fn set_stdin<T: std::os::unix::io::IntoRawFd>(&mut self, stdin: T) { + if let Some(file) = self.fds.remove(&0) { + File::maybe_drop(file); + } + self.fds.insert( + 0, + // Safety: we just acquired stdin via into_raw_fd, which acquires + // ownership of the fd, so we are now the sole owner + std::sync::Arc::new(unsafe { File::input(stdin.into_raw_fd()) }), + ); + } + + fn stdout(&self) -> Option<std::sync::Arc<File>> { + self.fds.get(&1).map(std::sync::Arc::clone) + } + + pub fn set_stdout<T: std::os::unix::io::IntoRawFd>(&mut self, stdout: T) { + if let Some(file) = self.fds.remove(&1) { + File::maybe_drop(file); + } + self.fds.insert( + 1, + // Safety: we just acquired stdout via into_raw_fd, which acquires + // ownership of the fd, so we are now the sole owner + std::sync::Arc::new(unsafe { + File::output(stdout.into_raw_fd()) + }), + ); + } + + fn stderr(&self) -> Option<std::sync::Arc<File>> { + self.fds.get(&2).map(std::sync::Arc::clone) + } + + pub fn set_stderr<T: std::os::unix::io::IntoRawFd>(&mut self, stderr: T) { + if let Some(file) = self.fds.remove(&2) { + File::maybe_drop(file); + } + self.fds.insert( + 2, + // Safety: we just acquired stderr via into_raw_fd, which acquires + // ownership of the fd, so we are now the sole owner + std::sync::Arc::new(unsafe { + File::output(stderr.into_raw_fd()) + }), + ); + } + + pub fn apply_redirects(&mut self, redirects: &[crate::parse::Redirect]) { + for redirect in redirects { + let to = match &redirect.to { + crate::parse::RedirectTarget::Fd(fd) => { + std::sync::Arc::clone(&self.fds[fd]) + } + crate::parse::RedirectTarget::File(path) => { + let fd = redirect.dir.open(path).unwrap(); + match redirect.dir { + crate::parse::Direction::In => { + // Safety: we just opened fd, and nothing else has + // or can use it + std::sync::Arc::new(unsafe { File::input(fd) }) + } + crate::parse::Direction::Out + | crate::parse::Direction::Append => { + // Safety: we just opened fd, and nothing else has + // or can use it + std::sync::Arc::new(unsafe { File::output(fd) }) + } + } + } + }; + self.fds.insert(redirect.from, to); + } + } + + pub fn read_line_stdin(&self) -> Result<(String, bool)> { + let mut line = vec![]; + if let Some(file) = self.stdin() { + if let File::In(fh) = &*file { + // we have to read only a single character at a time here + // because stdin needs to be shared across all commands in the + // command list, some of which may be builtins and others of + // which may be external commands - if we read past the end of + // a line, then the characters past the end of that line will + // no longer be available to the next command, since we have + // them buffered in memory rather than them being on the stdin + // pipe. + for byte in fh.bytes() { + let byte = byte?; + line.push(byte); + if byte == b'\n' { + break; + } + } + } + } + let done = line.is_empty(); + let mut line = String::from_utf8(line).unwrap(); + if line.ends_with('\n') { + line.truncate(line.len() - 1); + } + Ok((line, done)) + } + + pub fn write_stdout(&self, buf: &[u8]) -> Result<()> { + if let Some(file) = self.stdout() { + if let File::Out(fh) = &*file { + Ok((&*fh).write_all(buf)?) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + pub fn write_stderr(&self, buf: &[u8]) -> Result<()> { + if let Some(file) = self.stderr() { + if let File::Out(fh) = &*file { + Ok((&*fh).write_all(buf)?) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + pub fn setup_command(mut self, cmd: &mut crate::runner::Command) { + if let Some(stdin) = self.fds.remove(&0) { + if let Ok(stdin) = std::sync::Arc::try_unwrap(stdin) { + let stdin = stdin.into_raw_fd(); + if stdin != 0 { + // Safety: we just acquired stdin via into_raw_fd, which + // acquires ownership of the fd, so we are now the sole + // owner + cmd.stdin(unsafe { std::fs::File::from_raw_fd(stdin) }); + self.fds.remove(&0); + } + } + } + if let Some(stdout) = self.fds.remove(&1) { + if let Ok(stdout) = std::sync::Arc::try_unwrap(stdout) { + let stdout = stdout.into_raw_fd(); + if stdout != 1 { + // Safety: we just acquired stdout via into_raw_fd, which + // acquires ownership of the fd, so we are now the sole + // owner + cmd.stdout(unsafe { std::fs::File::from_raw_fd(stdout) }); + self.fds.remove(&1); + } + } + } + if let Some(stderr) = self.fds.remove(&2) { + if let Ok(stderr) = std::sync::Arc::try_unwrap(stderr) { + let stderr = stderr.into_raw_fd(); + if stderr != 2 { + // Safety: we just acquired stderr via into_raw_fd, which + // acquires ownership of the fd, so we are now the sole + // owner + cmd.stderr(unsafe { std::fs::File::from_raw_fd(stderr) }); + self.fds.remove(&2); + } + } + } + } +} + +impl Drop for Io { + fn drop(&mut self) { + for (_, file) in self.fds.drain() { + File::maybe_drop(file); + } + } +} + +#[derive(Debug)] +pub enum File { + In(std::fs::File), + Out(std::fs::File), +} + +impl File { + // Safety: fd must not be owned by any other File object + pub unsafe fn input(fd: std::os::unix::io::RawFd) -> Self { + Self::In(std::fs::File::from_raw_fd(fd)) + } + + // Safety: fd must not be owned by any other File object + pub unsafe fn output(fd: std::os::unix::io::RawFd) -> Self { + Self::Out(std::fs::File::from_raw_fd(fd)) + } + + fn maybe_drop(file: std::sync::Arc<Self>) { + if let Ok(file) = std::sync::Arc::try_unwrap(file) { + if file.as_raw_fd() <= 2 { + let _ = file.into_raw_fd(); + } + } + } +} + +impl std::os::unix::io::AsRawFd for File { + fn as_raw_fd(&self) -> std::os::unix::io::RawFd { + match self { + Self::In(fh) | Self::Out(fh) => fh.as_raw_fd(), + } + } +} + +impl std::os::unix::io::IntoRawFd for File { + fn into_raw_fd(self) -> std::os::unix::io::RawFd { + match self { + Self::In(fh) | Self::Out(fh) => fh.into_raw_fd(), + } + } +} + +pub enum Child { + Task(tokio::task::JoinHandle<std::process::ExitStatus>), + Wrapped(Box<crate::runner::Child>), +} + +impl Child { + pub fn new_task<F>(f: F) -> Self + where + F: FnOnce() -> std::process::ExitStatus + Send + 'static, + { + Self::Task(tokio::task::spawn_blocking(f)) + } + + pub fn new_wrapped(child: crate::runner::Child) -> Self { + Self::Wrapped(Box::new(child)) + } + + pub fn id(&self) -> Option<u32> { + match self { + Self::Task(_) => None, + Self::Wrapped(child) => child.id(), + } + } + + pub fn status( + self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future<Output = Result<std::process::ExitStatus>> + + Send + + Sync, + >, + > { + Box::pin(async move { + match self { + Self::Task(task) => task.await.map_err(|e| anyhow!(e)), + Self::Wrapped(child) => child.status().await, + } + }) + } +} diff --git a/src/runner/builtins/mod.rs b/src/runner/builtins/mod.rs new file mode 100644 index 0000000..b714c58 --- /dev/null +++ b/src/runner/builtins/mod.rs @@ -0,0 +1,242 @@ +use crate::runner::prelude::*; + +pub mod command; +pub use command::{Child, Command, File, Io}; + +type Builtin = &'static (dyn for<'a> Fn( + crate::parse::Exe, + &'a Env, + command::Cfg, +) -> Result<command::Child> + + Sync + + Send); + +#[allow(clippy::as_conversions)] +static BUILTINS: once_cell::sync::Lazy< + std::collections::HashMap<&'static str, Builtin>, +> = once_cell::sync::Lazy::new(|| { + let mut builtins = std::collections::HashMap::new(); + builtins.insert("cd", &cd as Builtin); + builtins.insert("set", &set); + builtins.insert("unset", &unset); + builtins.insert("echo", &echo); + builtins.insert("read", &read); + builtins.insert("and", &and); + builtins.insert("or", &or); + builtins.insert("command", &command); + builtins.insert("builtin", &builtin); + builtins +}); + +macro_rules! bail { + ($cfg:expr, $exe:expr, $msg:expr $(,)?) => { + $cfg.io().write_stderr( + format!("{}: {}\n", $exe.exe().display(), $msg).as_bytes() + ) + .unwrap(); + return std::process::ExitStatus::from_raw(1 << 8); + }; + ($cfg:expr, $exe:expr, $msg:expr, $($arg:tt)*) => { + $cfg.io().write_stderr( + format!("{}: ", $exe.exe().display()).as_bytes() + ) + .unwrap(); + $cfg.io().write_stderr(format!($msg, $($arg)*).as_bytes()) + .unwrap(); + $cfg.io().write_stderr(b"\n").unwrap(); + return std::process::ExitStatus::from_raw(1 << 8); + }; +} + +// clippy can't tell that the type is necessary +#[allow(clippy::unnecessary_wraps)] +fn cd( + exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + let prev_pwd = env.prev_pwd(); + let home = env.var("HOME"); + Ok(command::Child::new_task(move || { + let dir = if let Some(dir) = exe.args().get(0) { + if dir.is_empty() { + ".".to_string().into() + } else if dir == "-" { + prev_pwd + } else { + dir.into() + } + } else { + let dir = home; + if let Some(dir) = dir { + dir.into() + } else { + bail!(cfg, exe, "could not find home directory"); + } + }; + if let Err(e) = std::env::set_current_dir(&dir) { + bail!( + cfg, + exe, + "{}: {}", + crate::format::io_error(&e), + dir.display() + ); + } + std::process::ExitStatus::from_raw(0) + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn set( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + let k = if let Some(k) = exe.args().get(0).map(String::as_str) { + k + } else { + bail!(cfg, exe, "usage: set key value"); + }; + let v = if let Some(v) = exe.args().get(1).map(String::as_str) { + v + } else { + bail!(cfg, exe, "usage: set key value"); + }; + + std::env::set_var(k, v); + std::process::ExitStatus::from_raw(0) + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn unset( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + let k = if let Some(k) = exe.args().get(0).map(String::as_str) { + k + } else { + bail!(cfg, exe, "usage: unset key"); + }; + + std::env::remove_var(k); + std::process::ExitStatus::from_raw(0) + })) +} + +// clippy can't tell that the type is necessary +#[allow(clippy::unnecessary_wraps)] +// mostly just for testing and ensuring that builtins work, i'll likely remove +// this later, since the binary seems totally fine +fn echo( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + macro_rules! write_stdout { + ($bytes:expr) => { + if let Err(e) = cfg.io().write_stdout($bytes) { + cfg.io() + .write_stderr(format!("echo: {}", e).as_bytes()) + .unwrap(); + return std::process::ExitStatus::from_raw(1 << 8); + } + }; + } + let count = exe.args().len(); + for (i, arg) in exe.args().iter().enumerate() { + write_stdout!(arg.as_bytes()); + if i == count - 1 { + write_stdout!(b"\n"); + } else { + write_stdout!(b" "); + } + } + + std::process::ExitStatus::from_raw(0) + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn read( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + Ok(command::Child::new_task(move || { + let var = if let Some(var) = exe.args().get(0).map(String::as_str) { + var + } else { + bail!(cfg, exe, "usage: read var"); + }; + + let (val, done) = match cfg.io().read_line_stdin() { + Ok((line, done)) => (line, done), + Err(e) => { + bail!(cfg, exe, e); + } + }; + + std::env::set_var(var, val); + std::process::ExitStatus::from_raw(if done { 1 << 8 } else { 0 }) + })) +} + +fn and( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + if env.latest_status().success() { + let mut cmd = crate::runner::Command::new(exe, cfg.io().clone()); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) + } else { + let status = env.latest_status(); + Ok(command::Child::new_task(move || status)) + } +} + +fn or( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + if env.latest_status().success() { + let status = env.latest_status(); + Ok(command::Child::new_task(move || status)) + } else { + let mut cmd = crate::runner::Command::new(exe, cfg.io().clone()); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) + } +} + +fn command( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + let mut cmd = crate::runner::Command::new_binary(&exe); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) +} + +fn builtin( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> Result<command::Child> { + exe.shift(); + let mut cmd = crate::runner::Command::new_builtin(exe, cfg.io().clone()); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) +} diff --git a/src/runner/command.rs b/src/runner/command.rs new file mode 100644 index 0000000..cbc8dee --- /dev/null +++ b/src/runner/command.rs @@ -0,0 +1,203 @@ +use crate::runner::prelude::*; + +pub struct Command { + inner: Inner, + exe: std::path::PathBuf, + redirects: Vec<crate::parse::Redirect>, + pre_exec: Option< + Box<dyn FnMut() -> std::io::Result<()> + Send + Sync + 'static>, + >, +} + +impl Command { + pub fn new(exe: crate::parse::Exe, io: super::builtins::Io) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + Self { + inner: super::builtins::Command::new(exe, io).map_or_else( + |exe| Self::new_binary(&exe).inner, + Inner::Builtin, + ), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn new_binary(exe: &crate::parse::Exe) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + let mut cmd = tokio::process::Command::new(exe.exe()); + cmd.args(exe.args()); + Self { + inner: Inner::Binary(cmd), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn new_builtin( + exe: crate::parse::Exe, + io: super::builtins::Io, + ) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + Self { + inner: super::builtins::Command::new(exe, io) + .map_or_else(|_| todo!(), Inner::Builtin), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn stdin(&mut self, fh: std::fs::File) { + match &mut self.inner { + Inner::Binary(cmd) => { + cmd.stdin(fh); + } + Inner::Builtin(cmd) => { + cmd.stdin(fh); + } + } + } + + pub fn stdout(&mut self, fh: std::fs::File) { + match &mut self.inner { + Inner::Binary(cmd) => { + cmd.stdout(fh); + } + Inner::Builtin(cmd) => { + cmd.stdout(fh); + } + } + } + + pub fn stderr(&mut self, fh: std::fs::File) { + match &mut self.inner { + Inner::Binary(cmd) => { + cmd.stderr(fh); + } + Inner::Builtin(cmd) => { + cmd.stderr(fh); + } + } + } + + // Safety: see pre_exec in tokio::process::Command (this is just a + // wrapper) + pub unsafe fn pre_exec<F>(&mut self, f: F) + where + F: 'static + FnMut() -> std::io::Result<()> + Send + Sync, + { + self.pre_exec = Some(Box::new(f)); + } + + pub fn spawn(self, env: &Env) -> Result<Child> { + let Self { + inner, + exe, + redirects, + pre_exec, + } = self; + + #[allow(clippy::as_conversions)] + let pre_exec = pre_exec.map_or_else( + || { + let redirects = redirects.clone(); + Box::new(move || { + apply_redirects(&redirects)?; + Ok(()) + }) + as Box<dyn FnMut() -> std::io::Result<()> + Send + Sync> + }, + |mut pre_exec| { + let redirects = redirects.clone(); + Box::new(move || { + apply_redirects(&redirects)?; + pre_exec()?; + Ok(()) + }) + }, + ); + match inner { + Inner::Binary(mut cmd) => { + // Safety: open, dup2, and close are async-signal-safe + // functions + unsafe { cmd.pre_exec(pre_exec) }; + Ok(Child::Binary(cmd.spawn().map_err(|e| { + anyhow!( + "{}: {}", + crate::format::io_error(&e), + exe.display() + ) + })?)) + } + Inner::Builtin(mut cmd) => { + // Safety: open, dup2, and close are async-signal-safe + // functions + unsafe { cmd.pre_exec(pre_exec) }; + cmd.apply_redirects(&redirects); + Ok(Child::Builtin(cmd.spawn(env)?)) + } + } + } +} + +pub enum Inner { + Binary(tokio::process::Command), + Builtin(super::builtins::Command), +} + +pub enum Child { + Binary(tokio::process::Child), + Builtin(super::builtins::Child), +} + +impl Child { + pub fn id(&self) -> Option<u32> { + match self { + Self::Binary(child) => child.id(), + Self::Builtin(child) => child.id(), + } + } + + pub fn status( + self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future<Output = Result<std::process::ExitStatus>> + + Send + + Sync, + >, + > { + Box::pin(async move { + match self { + // this case is handled by waitpid + Self::Binary(_) => unreachable!(), + Self::Builtin(child) => Ok(child.status().await?), + } + }) + } +} + +fn apply_redirects( + redirects: &[crate::parse::Redirect], +) -> std::io::Result<()> { + for redirect in redirects { + match &redirect.to { + crate::parse::RedirectTarget::Fd(fd) => { + nix::unistd::dup2(*fd, redirect.from)?; + } + crate::parse::RedirectTarget::File(path) => { + let fd = redirect.dir.open(path)?; + if fd != redirect.from { + nix::unistd::dup2(fd, redirect.from)?; + nix::unistd::close(fd)?; + } + } + } + } + Ok(()) +} diff --git a/src/runner/mod.rs b/src/runner/mod.rs new file mode 100644 index 0000000..91e268a --- /dev/null +++ b/src/runner/mod.rs @@ -0,0 +1,499 @@ +use crate::runner::prelude::*; + +mod builtins; +mod command; +pub use command::{Child, Command}; +mod prelude; +mod sys; + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Event { + RunPipeline((usize, usize)), + Suspend, + Exit(Env), +} + +struct Stack { + frames: Vec<Frame>, +} + +impl Stack { + fn new() -> Self { + Self { frames: vec![] } + } + + fn push(&mut self, frame: Frame) { + self.frames.push(frame); + } + + fn pop(&mut self) -> Frame { + self.frames.pop().unwrap() + } + + fn top(&self) -> Option<&Frame> { + self.frames.last() + } + + fn top_mut(&mut self) -> Option<&mut Frame> { + self.frames.last_mut() + } + + fn current_pc(&self, pc: usize) -> bool { + match self.top() { + Some(Frame::If(..)) | None => false, + Some(Frame::While(_, start) | Frame::For(_, start, _)) => { + *start == pc + } + } + } + + fn should_execute(&self) -> bool { + for frame in &self.frames { + if matches!( + frame, + Frame::If(false, ..) + | Frame::While(false, ..) + | Frame::For(false, ..) + ) { + return false; + } + } + true + } +} + +enum Frame { + If(bool, bool), + While(bool, usize), + For(bool, usize, Vec<String>), +} + +pub async fn main( + commands: String, + shell_write: &mut Option<tokio::fs::File>, +) -> Result<i32> { + let mut env = Env::new_from_env()?; + let config = crate::config::Config::load()?; + run_commands(commands, &mut env, &config, shell_write).await?; + let status = env.latest_status(); + write_event(shell_write, Event::Exit(env)).await?; + + if let Some(signal) = status.signal() { + nix::sys::signal::raise(signal.try_into().unwrap())?; + } + Ok(status.code().unwrap()) +} + +async fn run_commands( + commands: String, + env: &mut Env, + config: &crate::config::Config, + shell_write: &mut Option<tokio::fs::File>, +) -> Result<()> { + let commands = crate::parse::ast::Commands::parse(&commands)?; + let commands = commands.commands(); + let mut pc = 0; + let mut stack = Stack::new(); + while pc < commands.len() { + match &commands[pc] { + crate::parse::ast::Command::Pipeline(pipeline) => { + if stack.should_execute() { + run_pipeline(pipeline.clone(), env, config, shell_write) + .await?; + } + pc += 1; + } + crate::parse::ast::Command::If(pipeline) => { + let should = stack.should_execute(); + if !stack.current_pc(pc) { + stack.push(Frame::If(false, false)); + } + if should { + let status = env.latest_status(); + run_pipeline(pipeline.clone(), env, config, shell_write) + .await?; + if let Some(Frame::If(should, found)) = stack.top_mut() { + *should = env.latest_status().success(); + if *should { + *found = true; + } + } else { + unreachable!(); + } + env.set_status(status); + } + pc += 1; + } + crate::parse::ast::Command::While(pipeline) => { + let should = stack.should_execute(); + if !stack.current_pc(pc) { + stack.push(Frame::While(false, pc)); + } + if should { + let status = env.latest_status(); + run_pipeline(pipeline.clone(), env, config, shell_write) + .await?; + if let Some(Frame::While(should, _)) = stack.top_mut() { + *should = env.latest_status().success(); + } else { + unreachable!(); + } + env.set_status(status); + } + pc += 1; + } + crate::parse::ast::Command::For(var, list) => { + let should = stack.should_execute(); + if !stack.current_pc(pc) { + stack.push(Frame::For( + false, + pc, + if stack.should_execute() { + list.clone() + .into_iter() + .map(|w| async { + w.eval(env) + .await + .map(IntoIterator::into_iter) + }) + .collect::<futures_util::stream::FuturesOrdered<_>>() + .try_collect::<Vec<_>>().await? + .into_iter() + .flatten() + .collect() + } else { + vec![] + }, + )); + } + if should { + if let Some(Frame::For(should, _, list)) = stack.top_mut() + { + *should = !list.is_empty(); + if *should { + let val = list.remove(0); + // XXX i really need to just pick one location and + // stick with it instead of trying to keep these + // in sync + env.set_var(var, &val); + std::env::set_var(var, &val); + } + } else { + unreachable!(); + } + } + pc += 1; + } + crate::parse::ast::Command::Else(pipeline) => { + let mut top = stack.pop(); + if stack.should_execute() { + if let Frame::If(ref mut should, ref mut found) = top { + if *found { + *should = false; + } else if let Some(pipeline) = pipeline { + let status = env.latest_status(); + run_pipeline( + pipeline.clone(), + env, + config, + shell_write, + ) + .await?; + *should = env.latest_status().success(); + if *should { + *found = true; + } + env.set_status(status); + } else { + *should = true; + *found = true; + } + } else { + todo!(); + } + } + stack.push(top); + pc += 1; + } + crate::parse::ast::Command::End => match stack.top() { + Some(Frame::If(..)) => { + stack.pop(); + pc += 1; + } + Some( + Frame::While(should, start) + | Frame::For(should, start, _), + ) => { + if *should { + pc = *start; + } else { + stack.pop(); + pc += 1; + } + } + None => todo!(), + }, + } + } + Ok(()) +} + +async fn run_pipeline( + pipeline: crate::parse::ast::Pipeline, + env: &mut Env, + config: &crate::config::Config, + shell_write: &mut Option<tokio::fs::File>, +) -> Result<()> { + write_event(shell_write, Event::RunPipeline(pipeline.span())).await?; + // Safety: pipelines are run serially, so only one copy of these will ever + // exist at once. note that reusing a single copy of these at the top + // level would not be safe, because in the case of a command line like + // "echo foo; ls", we would pass the stdout fd to the ls process while it + // is still open here, and may still have data buffered. + let stdin = unsafe { std::fs::File::from_raw_fd(0) }; + let stdout = unsafe { std::fs::File::from_raw_fd(1) }; + let stderr = unsafe { std::fs::File::from_raw_fd(2) }; + let mut io = builtins::Io::new(); + io.set_stdin(stdin); + io.set_stdout(stdout); + io.set_stderr(stderr); + + let pwd = env.pwd().to_path_buf(); + let interactive = shell_write.is_some(); + let pipeline = pipeline.eval(env).await?; + let mut exes: Vec<_> = pipeline.into_exes().collect(); + for exe in &mut exes { + let mut seen = std::collections::HashSet::new(); + while let Some(alias) = config.alias_for(exe.exe()) { + let mut new = alias.clone().eval(env).await?; + let override_self = exe.exe() == new.exe(); + if seen.contains(new.exe()) { + return Err(anyhow!( + "recursive alias found: {}", + new.exe().display() + )); + } + seen.insert(new.exe().to_path_buf()); + new.append(exe.clone()); + *exe = new; + if override_self { + break; + } + } + } + let cmds = exes + .into_iter() + .map(|exe| Command::new(exe, io.clone())) + .collect(); + let (children, pg) = spawn_children(cmds, env, interactive)?; + let status = wait_children(children, pg, shell_write).await; + if interactive { + sys::set_foreground_pg(nix::unistd::getpid())?; + } + env.update()?; + env.set_status(status); + if env.pwd() != pwd { + env.set_prev_pwd(pwd); + } + Ok(()) +} + +async fn write_event( + fh: &mut Option<tokio::fs::File>, + event: Event, +) -> Result<()> { + if let Some(fh) = fh { + fh.write_all(&bincode::serialize(&event)?).await?; + fh.flush().await?; + } + Ok(()) +} + +fn spawn_children( + mut cmds: Vec<Command>, + env: &Env, + interactive: bool, +) -> Result<(Vec<Child>, Option<nix::unistd::Pid>)> { + for i in 0..(cmds.len() - 1) { + let (r, w) = sys::pipe()?; + cmds[i].stdout(w); + cmds[i + 1].stdin(r); + } + + let mut children = vec![]; + let mut pg_pid = None; + for mut cmd in cmds { + // Safety: setpgid is an async-signal-safe function + unsafe { + cmd.pre_exec(move || { + sys::setpgid_child(pg_pid)?; + Ok(()) + }); + } + let child = cmd.spawn(env)?; + if let Some(id) = child.id() { + let child_pid = sys::id_to_pid(id); + sys::setpgid_parent(child_pid, pg_pid)?; + if pg_pid.is_none() { + pg_pid = Some(child_pid); + if interactive { + sys::set_foreground_pg(child_pid)?; + } + } + } + children.push(child); + } + Ok((children, pg_pid)) +} + +async fn wait_children( + children: Vec<Child>, + pg: Option<nix::unistd::Pid>, + shell_write: &mut Option<tokio::fs::File>, +) -> std::process::ExitStatus { + enum Res { + Child(nix::Result<nix::sys::wait::WaitStatus>), + Builtin((Result<std::process::ExitStatus>, bool)), + } + + macro_rules! bail { + ($e:expr) => { + eprintln!("nbsh: {}\n", $e); + return std::process::ExitStatus::from_raw(1 << 8); + }; + } + + let mut final_status = None; + + let count = children.len(); + let (children, builtins): (Vec<_>, Vec<_>) = children + .into_iter() + .enumerate() + .partition(|(_, child)| child.id().is_some()); + let mut children: std::collections::HashMap<_, _> = children + .into_iter() + .map(|(i, child)| { + (sys::id_to_pid(child.id().unwrap()), (child, i == count - 1)) + }) + .collect(); + let mut builtin_count = builtins.len(); + let builtins: futures_util::stream::FuturesUnordered<_> = + builtins + .into_iter() + .map(|(i, child)| async move { + (child.status().await, i == count - 1) + }) + .collect(); + + let (wait_w, wait_r) = tokio::sync::mpsc::unbounded_channel(); + if let Some(pg) = pg { + tokio::task::spawn_blocking(move || loop { + let res = nix::sys::wait::waitpid( + sys::neg_pid(pg), + Some(nix::sys::wait::WaitPidFlag::WUNTRACED), + ); + match wait_w.send(res) { + Ok(_) => {} + Err(tokio::sync::mpsc::error::SendError(res)) => { + // we should never drop wait_r while there are still valid + // things to read + assert!(res.is_err()); + break; + } + } + }); + } + + let mut stream: futures_util::stream::SelectAll<_> = [ + tokio_stream::wrappers::UnboundedReceiverStream::new(wait_r) + .map(Res::Child) + .boxed(), + builtins.map(Res::Builtin).boxed(), + ] + .into_iter() + .collect(); + while let Some(res) = stream.next().await { + match res { + Res::Child(Ok(status)) => { + match status { + // we can't call child.status() here to unify these + // branches because our waitpid call already collected the + // status + nix::sys::wait::WaitStatus::Exited(pid, code) => { + let (_, last) = children.remove(&pid).unwrap(); + if last { + final_status = Some( + std::process::ExitStatus::from_raw(code << 8), + ); + } + } + nix::sys::wait::WaitStatus::Signaled(pid, signal, _) => { + let (_, last) = children.remove(&pid).unwrap(); + if signal == nix::sys::signal::Signal::SIGINT { + if let Err(e) = nix::sys::signal::raise( + nix::sys::signal::Signal::SIGINT, + ) { + bail!(e); + } + } + // this conversion is safe because the Signal enum is + // repr(i32) + #[allow(clippy::as_conversions)] + if last { + final_status = + Some(std::process::ExitStatus::from_raw( + signal as i32, + )); + } + } + nix::sys::wait::WaitStatus::Stopped(pid, signal) => { + if signal == nix::sys::signal::Signal::SIGTSTP { + if let Err(e) = + write_event(shell_write, Event::Suspend).await + { + bail!(e); + } + if let Err(e) = nix::sys::signal::kill( + pid, + nix::sys::signal::Signal::SIGCONT, + ) { + bail!(e); + } + } + } + _ => {} + } + } + Res::Child(Err(e)) => { + bail!(e); + } + Res::Builtin((Ok(status), last)) => { + // this conversion is safe because the Signal enum is + // repr(i32) + #[allow(clippy::as_conversions)] + if status.signal() + == Some(nix::sys::signal::Signal::SIGINT as i32) + { + if let Err(e) = nix::sys::signal::raise( + nix::sys::signal::Signal::SIGINT, + ) { + bail!(e); + } + } + if last { + final_status = Some(status); + } + builtin_count -= 1; + } + Res::Builtin((Err(e), _)) => { + bail!(e); + } + } + + if children.is_empty() && builtin_count == 0 { + break; + } + } + + final_status.unwrap() +} diff --git a/src/runner/prelude.rs b/src/runner/prelude.rs new file mode 100644 index 0000000..53b67fc --- /dev/null +++ b/src/runner/prelude.rs @@ -0,0 +1 @@ +pub use crate::prelude::*; diff --git a/src/runner/sys.rs b/src/runner/sys.rs new file mode 100644 index 0000000..b6a9428 --- /dev/null +++ b/src/runner/sys.rs @@ -0,0 +1,79 @@ +use crate::runner::prelude::*; + +const PID0: nix::unistd::Pid = nix::unistd::Pid::from_raw(0); + +pub fn pipe() -> Result<(std::fs::File, std::fs::File)> { + let (r, w) = nix::unistd::pipe2(nix::fcntl::OFlag::O_CLOEXEC)?; + // Safety: these file descriptors were just returned by pipe2 above, and + // are only available in this function, so nothing else can be accessing + // them + Ok((unsafe { std::fs::File::from_raw_fd(r) }, unsafe { + std::fs::File::from_raw_fd(w) + })) +} + +pub fn set_foreground_pg(pg: nix::unistd::Pid) -> Result<()> { + let pty = nix::fcntl::open( + "/dev/tty", + nix::fcntl::OFlag::empty(), + nix::sys::stat::Mode::empty(), + )?; + + // if a background process calls tcsetpgrp, the kernel will send it + // SIGTTOU which suspends it. if that background process is the session + // leader and doesn't have SIGTTOU blocked, the kernel will instead just + // return ENOTTY from the tcsetpgrp call rather than sending a signal to + // avoid deadlocking the process. therefore, we need to ensure that + // SIGTTOU is blocked here. + + // Safety: setting a signal handler to SigIgn is always safe + unsafe { + nix::sys::signal::signal( + nix::sys::signal::Signal::SIGTTOU, + nix::sys::signal::SigHandler::SigIgn, + )?; + } + let res = nix::unistd::tcsetpgrp(pty, pg); + // Safety: setting a signal handler to SigDfl is always safe + unsafe { + nix::sys::signal::signal( + nix::sys::signal::Signal::SIGTTOU, + nix::sys::signal::SigHandler::SigDfl, + )?; + } + res?; + + nix::unistd::close(pty)?; + + nix::sys::signal::kill(neg_pid(pg), nix::sys::signal::Signal::SIGCONT) + // the process group has already exited + .allow(nix::errno::Errno::ESRCH)?; + + Ok(()) +} + +pub fn setpgid_child(pg: Option<nix::unistd::Pid>) -> std::io::Result<()> { + nix::unistd::setpgid(PID0, pg.unwrap_or(PID0))?; + Ok(()) +} + +pub fn setpgid_parent( + pid: nix::unistd::Pid, + pg: Option<nix::unistd::Pid>, +) -> Result<()> { + nix::unistd::setpgid(pid, pg.unwrap_or(PID0)) + // the child already called exec, so it must have already called + // setpgid itself + .allow(nix::errno::Errno::EACCES) + // the child already exited, so we don't care + .allow(nix::errno::Errno::ESRCH)?; + Ok(()) +} + +pub fn id_to_pid(id: u32) -> nix::unistd::Pid { + nix::unistd::Pid::from_raw(id.try_into().unwrap()) +} + +pub fn neg_pid(pid: nix::unistd::Pid) -> nix::unistd::Pid { + nix::unistd::Pid::from_raw(-pid.as_raw()) +} |