From cba75be9c673b846ad0c4f6f5f2e8461056582ad Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 9 Jan 2022 22:46:15 -0500 Subject: rename since this doesn't just run pipelines anymore --- src/main.rs | 4 +- src/pipeline/builtins/command.rs | 398 ----------------------------- src/pipeline/builtins/mod.rs | 314 ----------------------- src/pipeline/command.rs | 219 ---------------- src/pipeline/mod.rs | 534 --------------------------------------- src/pipeline/prelude.rs | 1 - src/runner/builtins/command.rs | 398 +++++++++++++++++++++++++++++ src/runner/builtins/mod.rs | 314 +++++++++++++++++++++++ src/runner/command.rs | 219 ++++++++++++++++ src/runner/mod.rs | 534 +++++++++++++++++++++++++++++++++++++++ src/runner/prelude.rs | 1 + src/shell/history/mod.rs | 6 +- 12 files changed, 1471 insertions(+), 1471 deletions(-) delete mode 100644 src/pipeline/builtins/command.rs delete mode 100644 src/pipeline/builtins/mod.rs delete mode 100644 src/pipeline/command.rs delete mode 100644 src/pipeline/mod.rs delete mode 100644 src/pipeline/prelude.rs create mode 100644 src/runner/builtins/command.rs create mode 100644 src/runner/builtins/mod.rs create mode 100644 src/runner/command.rs create mode 100644 src/runner/mod.rs create mode 100644 src/runner/prelude.rs diff --git a/src/main.rs b/src/main.rs index c240e11..2096b46 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,13 +17,13 @@ mod format; mod info; mod mutex; mod parse; -mod pipeline; mod prelude; +mod runner; mod shell; async fn async_main() -> anyhow::Result { if std::env::args().nth(1).as_deref() == Some("--internal-cmd-runner") { - return pipeline::run().await; + return runner::run().await; } shell::run().await diff --git a/src/pipeline/builtins/command.rs b/src/pipeline/builtins/command.rs deleted file mode 100644 index 40498fc..0000000 --- a/src/pipeline/builtins/command.rs +++ /dev/null @@ -1,398 +0,0 @@ -use crate::pipeline::prelude::*; - -use async_std::io::prelude::BufReadExt as _; - -pub struct Command { - exe: crate::parse::Exe, - f: super::Builtin, - cfg: Cfg, -} - -impl Command { - pub fn new(exe: crate::parse::Exe) -> Result { - if let Some(s) = exe.exe().to_str() { - if let Some(f) = super::BUILTINS.get(s) { - Ok(Self { - exe, - f, - cfg: Cfg::new(), - }) - } else { - Err(exe) - } - } else { - Err(exe) - } - } - - pub fn new_with_io( - exe: crate::parse::Exe, - io: Io, - ) -> Result { - if let Some(s) = exe.exe().to_str() { - if let Some(f) = super::BUILTINS.get(s) { - Ok(Self { - exe, - f, - cfg: Cfg::new_with_io(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 async_std::os::unix::process::CommandExt (this - // is just a wrapper) - pub unsafe fn pre_exec(&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) -> anyhow::Result { - let Self { f, exe, cfg } = self; - (f)(exe, env, cfg) - } -} - -pub struct Cfg { - io: Io, - pre_exec: Option< - Box std::io::Result<()> + Send + Sync>, - >, -} - -impl Cfg { - fn new() -> Self { - Self { - io: Io::new(), - pre_exec: None, - } - } - - fn new_with_io(io: Io) -> Self { - Self { io, pre_exec: None } - } - - pub fn io(&self) -> &Io { - &self.io - } - - // Safety: see pre_exec in async_std::os::unix::process::CommandExt (this - // is just a wrapper) - pub unsafe fn pre_exec(&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::pipeline::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, - crate::mutex::Mutex, - >, -} - -impl Io { - pub fn new() -> Self { - Self { - fds: std::collections::HashMap::new(), - } - } - - fn stdin(&self) -> Option> { - self.fds.get(&0).map(crate::mutex::clone) - } - - pub fn set_stdin(&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 - crate::mutex::new(unsafe { File::input(stdin.into_raw_fd()) }), - ); - } - - fn stdout(&self) -> Option> { - self.fds.get(&1).map(crate::mutex::clone) - } - - pub fn set_stdout(&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 - crate::mutex::new(unsafe { File::output(stdout.into_raw_fd()) }), - ); - } - - fn stderr(&self) -> Option> { - self.fds.get(&2).map(crate::mutex::clone) - } - - pub fn set_stderr(&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 - crate::mutex::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) => { - crate::mutex::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 - crate::mutex::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 - crate::mutex::new(unsafe { File::output(fd) }) - } - } - } - }; - self.fds.insert(redirect.from, to); - } - } - - pub async fn read_line_stdin(&self) -> anyhow::Result { - let mut buf = String::new(); - if let Some(fh) = self.stdin() { - if let File::In(fh) = &mut *fh.lock_arc().await { - fh.read_line(&mut buf).await?; - } - } - if buf.ends_with('\n') { - buf.truncate(buf.len() - 1); - } - Ok(buf) - } - - pub async fn write_stdout(&self, buf: &[u8]) -> anyhow::Result<()> { - if let Some(fh) = self.stdout() { - if let File::Out(fh) = &mut *fh.lock_arc().await { - Ok(fh.write_all(buf).await.map(|_| ())?) - } else { - Ok(()) - } - } else { - Ok(()) - } - } - - pub async fn write_stderr(&self, buf: &[u8]) -> anyhow::Result<()> { - if let Some(fh) = self.stderr() { - if let File::Out(fh) = &mut *fh.lock_arc().await { - Ok(fh.write_all(buf).await.map(|_| ())?) - } else { - Ok(()) - } - } else { - Ok(()) - } - } - - pub fn setup_command(mut self, cmd: &mut crate::pipeline::Command) { - if let Some(stdin) = self.fds.remove(&0) { - if let Some(stdin) = crate::mutex::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 Some(stdout) = crate::mutex::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 Some(stderr) = crate::mutex::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(async_std::io::BufReader), - Out(async_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(async_std::io::BufReader::new( - async_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(async_std::fs::File::from_raw_fd(fd)) - } - - fn maybe_drop(file: crate::mutex::Mutex) { - if let Some(file) = crate::mutex::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) => fh.get_ref().as_raw_fd(), - 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) => fh.into_inner().into_raw_fd(), - Self::Out(fh) => fh.into_raw_fd(), - } - } -} - -pub struct Child<'a> { - fut: std::pin::Pin< - Box< - dyn std::future::Future - + Sync - + Send - + 'a, - >, - >, - wrapped_child: Option>>, -} - -impl<'a> Child<'a> { - pub fn new_fut(fut: F) -> Self - where - F: std::future::Future - + Sync - + Send - + 'a, - { - Self { - fut: Box::pin(fut), - wrapped_child: None, - } - } - - pub fn new_wrapped(child: crate::pipeline::Child<'a>) -> Self { - Self { - fut: Box::pin(async move { unreachable!() }), - wrapped_child: Some(Box::new(child)), - } - } - - pub fn id(&self) -> Option { - self.wrapped_child.as_ref().and_then(|cmd| cmd.id()) - } - - pub fn status( - self, - ) -> std::pin::Pin< - Box< - dyn std::future::Future< - Output = anyhow::Result, - > + Send - + Sync - + 'a, - >, - > { - Box::pin(async move { - if let Some(child) = self.wrapped_child { - child.status().await - } else { - Ok(self.fut.await) - } - }) - } -} diff --git a/src/pipeline/builtins/mod.rs b/src/pipeline/builtins/mod.rs deleted file mode 100644 index 4e9cf2a..0000000 --- a/src/pipeline/builtins/mod.rs +++ /dev/null @@ -1,314 +0,0 @@ -use crate::pipeline::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, -) -> anyhow::Result> - + 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("setenv", &setenv); - builtins.insert("unsetenv", &unsetenv); - 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() - ) - .await - .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() - ) - .await - .unwrap(); - $cfg.io().write_stderr(format!($msg, $($arg)*).as_bytes()) - .await - .unwrap(); - $cfg.io().write_stderr(b"\n").await.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, -) -> anyhow::Result { - async fn async_cd( - exe: crate::parse::Exe, - _env: &Env, - cfg: command::Cfg, - ) -> std::process::ExitStatus { - let dir = exe.args().get(0).map_or("", String::as_str); - let dir = if dir.is_empty() { - if let Some(dir) = home(None) { - dir - } else { - bail!(cfg, exe, "couldn't find current user"); - } - } else if dir.starts_with('~') { - let path: std::path::PathBuf = dir.into(); - if let std::path::Component::Normal(prefix) = - path.components().next().unwrap() - { - let prefix_bytes = prefix.as_bytes(); - let name = if prefix_bytes == b"~" { - None - } else { - Some(std::ffi::OsStr::from_bytes(&prefix_bytes[1..])) - }; - if let Some(home) = home(name) { - home.join(path.strip_prefix(prefix).unwrap()) - } else { - bail!( - cfg, - exe, - "no such user: {}", - name.map(std::ffi::OsStr::to_string_lossy) - .as_ref() - .unwrap_or(&std::borrow::Cow::Borrowed( - "(deleted)" - )) - ); - } - } else { - unreachable!() - } - } else { - dir.into() - }; - if let Err(e) = std::env::set_current_dir(&dir) { - bail!( - cfg, - exe, - "{}: {}", - crate::format::io_error(&e), - dir.display() - ); - } - async_std::process::ExitStatus::from_raw(0) - } - - Ok(command::Child::new_fut(async move { - async_cd(exe, env, cfg).await - })) -} - -#[allow(clippy::unnecessary_wraps)] -fn setenv( - exe: crate::parse::Exe, - env: &Env, - cfg: command::Cfg, -) -> anyhow::Result { - async fn async_setenv( - exe: crate::parse::Exe, - _env: &Env, - cfg: command::Cfg, - ) -> std::process::ExitStatus { - let k = if let Some(k) = exe.args().get(0).map(String::as_str) { - k - } else { - bail!(cfg, exe, "usage: setenv key value"); - }; - let v = if let Some(v) = exe.args().get(1).map(String::as_str) { - v - } else { - bail!(cfg, exe, "usage: setenv key value"); - }; - - std::env::set_var(k, v); - async_std::process::ExitStatus::from_raw(0) - } - - Ok(command::Child::new_fut(async move { - async_setenv(exe, env, cfg).await - })) -} - -#[allow(clippy::unnecessary_wraps)] -fn unsetenv( - exe: crate::parse::Exe, - env: &Env, - cfg: command::Cfg, -) -> anyhow::Result { - async fn async_unsetenv( - exe: crate::parse::Exe, - _env: &Env, - cfg: command::Cfg, - ) -> std::process::ExitStatus { - let k = if let Some(k) = exe.args().get(0).map(String::as_str) { - k - } else { - bail!(cfg, exe, "usage: unsetenv key"); - }; - - std::env::remove_var(k); - async_std::process::ExitStatus::from_raw(0) - } - - Ok(command::Child::new_fut(async move { - async_unsetenv(exe, env, cfg).await - })) -} - -// 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, -) -> anyhow::Result { - async fn async_echo( - exe: crate::parse::Exe, - _env: &Env, - cfg: command::Cfg, - ) -> std::process::ExitStatus { - macro_rules! write_stdout { - ($bytes:expr) => { - if let Err(e) = cfg.io().write_stdout($bytes).await { - cfg.io() - .write_stderr(format!("echo: {}", e).as_bytes()) - .await - .unwrap(); - return async_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" "); - } - } - - async_std::process::ExitStatus::from_raw(0) - } - - Ok(command::Child::new_fut(async move { - async_echo(exe, env, cfg).await - })) -} - -#[allow(clippy::unnecessary_wraps)] -fn read( - exe: crate::parse::Exe, - env: &Env, - cfg: command::Cfg, -) -> anyhow::Result { - async fn async_read( - exe: crate::parse::Exe, - _env: &Env, - cfg: command::Cfg, - ) -> std::process::ExitStatus { - let var = if let Some(var) = exe.args().get(0).map(String::as_str) { - var - } else { - bail!(cfg, exe, "usage: read var"); - }; - - let val = match cfg.io().read_line_stdin().await { - Ok(line) => line, - Err(e) => { - bail!(cfg, exe, e); - } - }; - - std::env::set_var(var, val); - async_std::process::ExitStatus::from_raw(0) - } - - Ok(command::Child::new_fut(async move { - async_read(exe, env, cfg).await - })) -} - -fn and( - mut exe: crate::parse::Exe, - env: &Env, - cfg: command::Cfg, -) -> anyhow::Result { - exe.shift(); - if env.latest_status().success() { - let mut cmd = crate::pipeline::Command::new(exe); - cfg.setup_command(&mut cmd); - Ok(command::Child::new_wrapped(cmd.spawn(env)?)) - } else { - let status = *env.latest_status(); - Ok(command::Child::new_fut(async move { status })) - } -} - -fn or( - mut exe: crate::parse::Exe, - env: &Env, - cfg: command::Cfg, -) -> anyhow::Result { - exe.shift(); - if env.latest_status().success() { - let status = *env.latest_status(); - Ok(command::Child::new_fut(async move { status })) - } else { - let mut cmd = crate::pipeline::Command::new(exe); - 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, -) -> anyhow::Result { - exe.shift(); - let mut cmd = crate::pipeline::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, -) -> anyhow::Result { - exe.shift(); - let mut cmd = crate::pipeline::Command::new_builtin(exe); - cfg.setup_command(&mut cmd); - Ok(command::Child::new_wrapped(cmd.spawn(env)?)) -} - -fn home(user: Option<&std::ffi::OsStr>) -> Option { - let user = user.map_or_else( - || users::get_user_by_uid(users::get_current_uid()), - users::get_user_by_name, - ); - user.map(|user| user.home_dir().to_path_buf()) -} diff --git a/src/pipeline/command.rs b/src/pipeline/command.rs deleted file mode 100644 index 70fbe9f..0000000 --- a/src/pipeline/command.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::pipeline::prelude::*; - -pub struct Command { - inner: Inner, - exe: std::path::PathBuf, - redirects: Vec, - pre_exec: Option< - Box std::io::Result<()> + Send + Sync + 'static>, - >, -} -impl Command { - pub fn new(exe: crate::parse::Exe) -> Self { - let exe_path = exe.exe().to_path_buf(); - let redirects = exe.redirects().to_vec(); - Self { - inner: super::builtins::Command::new(exe).map_or_else( - |exe| Self::new_binary(exe).inner, - Inner::Builtin, - ), - exe: exe_path, - redirects, - pre_exec: None, - } - } - - pub fn new_with_io( - 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_with_io(exe, io) - .map_or_else( - |exe| Self::new_binary(exe).inner, - Inner::Builtin, - ), - exe: exe_path, - redirects, - pre_exec: None, - } - } - - #[allow(clippy::needless_pass_by_value)] - 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 = async_std::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) -> Self { - let exe_path = exe.exe().to_path_buf(); - let redirects = exe.redirects().to_vec(); - Self { - inner: super::builtins::Command::new(exe) - .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 async_std::os::unix::process::CommandExt (this - // is just a wrapper) - pub unsafe fn pre_exec(&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) -> anyhow::Result { - 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 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::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(async_std::process::Command), - Builtin(super::builtins::Command), -} - -pub enum Child<'a> { - Binary(async_std::process::Child), - Builtin(super::builtins::Child<'a>), -} - -impl<'a> Child<'a> { - pub fn id(&self) -> Option { - match self { - Self::Binary(child) => Some(child.id()), - Self::Builtin(child) => child.id(), - } - } - - pub fn status( - self, - ) -> std::pin::Pin< - Box< - dyn std::future::Future< - Output = anyhow::Result, - > + Send - + Sync - + 'a, - >, - > { - Box::pin(async move { - match self { - Self::Binary(child) => Ok(child.status_no_drop().await?), - 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/pipeline/mod.rs b/src/pipeline/mod.rs deleted file mode 100644 index ebaf8e6..0000000 --- a/src/pipeline/mod.rs +++ /dev/null @@ -1,534 +0,0 @@ -use crate::pipeline::prelude::*; - -mod builtins; -mod command; -pub use command::{Child, Command}; -mod prelude; - -const PID0: nix::unistd::Pid = nix::unistd::Pid::from_raw(0); - -#[derive(Debug, serde::Serialize, serde::Deserialize)] -pub enum Event { - Suspend(usize), - Exit(Env), -} - -struct Stack { - frames: Vec, -} - -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), - While(bool, usize), - For(bool, usize, Vec), -} - -pub async fn run() -> anyhow::Result { - // Safety: we don't create File instances for or read/write data on fds - // 3 or 4 anywhere else - let shell_read = unsafe { async_std::fs::File::from_raw_fd(3) }; - let shell_write = unsafe { async_std::fs::File::from_raw_fd(4) }; - cloexec(3)?; - cloexec(4)?; - - let (commands, mut env) = read_data(shell_read).await?; - run_commands(&commands, &mut env, &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: &str, - env: &mut Env, - shell_write: &async_std::fs::File, -) -> anyhow::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, 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)); - } - if should { - run_pipeline(pipeline.clone(), env, shell_write).await?; - if let Some(Frame::If(should)) = stack.top_mut() { - *should = env.latest_status().success(); - } else { - unreachable!(); - } - } - 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 { - run_pipeline(pipeline.clone(), env, shell_write).await?; - if let Some(Frame::While(should, _)) = stack.top_mut() { - *should = env.latest_status().success(); - } else { - unreachable!(); - } - } - 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| w.eval(env)) - .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); - env.set_var(var, &val); - } - } else { - unreachable!(); - } - } - 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, - shell_write: &async_std::fs::File, -) -> anyhow::Result<()> { - // 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 { async_std::fs::File::from_raw_fd(0) }; - let stdout = unsafe { async_std::fs::File::from_raw_fd(1) }; - let stderr = unsafe { async_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 (children, pg) = spawn_children(pipeline, env, &io)?; - let status = wait_children(children, pg, env, &io, shell_write).await; - set_foreground_pg(nix::unistd::getpid())?; - env.update(status)?; - Ok(()) -} - -async fn read_data( - mut fh: async_std::fs::File, -) -> anyhow::Result<(String, Env)> { - let mut data = vec![]; - fh.read_to_end(&mut data).await?; - let commands = bincode::deserialize(&data).unwrap(); - let len: usize = bincode::serialized_size(&commands) - .unwrap() - .try_into() - .unwrap(); - let env = Env::from_bytes(&data[len..]); - Ok((commands, env)) -} - -async fn write_event( - mut fh: &async_std::fs::File, - event: Event, -) -> anyhow::Result<()> { - fh.write_all(&bincode::serialize(&event)?).await?; - fh.flush().await?; - Ok(()) -} - -fn spawn_children<'a>( - pipeline: crate::parse::ast::Pipeline, - env: &'a Env, - io: &builtins::Io, -) -> anyhow::Result<(Vec>, Option)> { - let pipeline = pipeline.eval(env); - let mut cmds: Vec<_> = pipeline - .into_exes() - .map(|exe| Command::new_with_io(exe, io.clone())) - .collect(); - for i in 0..(cmds.len() - 1) { - let (r, w) = 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 || { - setpgid_child(pg_pid)?; - Ok(()) - }); - } - let child = cmd.spawn(env)?; - if let Some(id) = child.id() { - let child_pid = id_to_pid(id); - setpgid_parent(child_pid, pg_pid)?; - if pg_pid.is_none() { - pg_pid = Some(child_pid); - set_foreground_pg(child_pid)?; - } - } - children.push(child); - } - Ok((children, pg_pid)) -} - -async fn wait_children( - children: Vec>, - pg: Option, - env: &Env, - io: &builtins::Io, - shell_write: &async_std::fs::File, -) -> std::process::ExitStatus { - enum Res { - Child(nix::Result), - Builtin(Option<(anyhow::Result, bool)>), - } - - macro_rules! bail { - ($e:expr) => { - // if writing to stderr is not possible, we still want to exit - // normally with a failure exit code - #[allow(clippy::let_underscore_drop)] - let _ = - io.write_stderr(format!("nbsh: {}\n", $e).as_bytes()).await; - 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)| { - (id_to_pid(child.id().unwrap()), (child, i == count - 1)) - }) - .collect(); - let mut 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) = async_std::channel::unbounded(); - let new_wait = move || { - if let Some(pg) = pg { - let wait_w = wait_w.clone(); - async_std::task::spawn(async move { - let res = blocking::unblock(move || { - nix::sys::wait::waitpid( - neg_pid(pg), - Some(nix::sys::wait::WaitPidFlag::WUNTRACED), - ) - }) - .await; - if wait_w.is_closed() { - // we shouldn't be able to drop real process terminations - assert!(res.is_err()); - } else { - wait_w.send(res).await.unwrap(); - } - }); - } - }; - - new_wait(); - loop { - if children.is_empty() && builtins.is_empty() { - break; - } - - let child = async { Res::Child(wait_r.recv().await.unwrap()) }; - let builtin = async { - Res::Builtin(if builtins.is_empty() { - std::future::pending().await - } else { - builtins.next().await - }) - }; - match child.race(builtin).await { - 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(env.idx()), - ) - .await - { - bail!(e); - } - if let Err(e) = nix::sys::signal::kill( - pid, - nix::sys::signal::Signal::SIGCONT, - ) { - bail!(e); - } - } - } - _ => {} - } - new_wait(); - } - Res::Child(Err(e)) => { - bail!(e); - } - Res::Builtin(Some((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); - } - } - Res::Builtin(Some((Err(e), _))) => { - bail!(e); - } - Res::Builtin(None) => {} - } - } - - final_status.unwrap() -} - -fn pipe() -> anyhow::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) - })) -} - -fn set_foreground_pg(pg: nix::unistd::Pid) -> anyhow::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) - .or_else(|e| { - // the process group has already exited - if e == nix::errno::Errno::ESRCH { - Ok(()) - } else { - Err(e) - } - })?; - - Ok(()) -} - -fn setpgid_child(pg: Option) -> std::io::Result<()> { - nix::unistd::setpgid(PID0, pg.unwrap_or(PID0))?; - Ok(()) -} - -fn setpgid_parent( - pid: nix::unistd::Pid, - pg: Option, -) -> anyhow::Result<()> { - nix::unistd::setpgid(pid, pg.unwrap_or(PID0)).or_else(|e| { - // EACCES means that the child already called exec, but if it did, - // then it also must have already called setpgid itself, so we don't - // care. ESRCH means that the process already exited, which is similar - if e == nix::errno::Errno::EACCES || e == nix::errno::Errno::ESRCH { - Ok(()) - } else { - Err(e) - } - })?; - Ok(()) -} - -fn cloexec(fd: std::os::unix::io::RawFd) -> anyhow::Result<()> { - nix::fcntl::fcntl( - fd, - nix::fcntl::FcntlArg::F_SETFD(nix::fcntl::FdFlag::FD_CLOEXEC), - )?; - Ok(()) -} - -fn id_to_pid(id: u32) -> nix::unistd::Pid { - nix::unistd::Pid::from_raw(id.try_into().unwrap()) -} - -fn neg_pid(pid: nix::unistd::Pid) -> nix::unistd::Pid { - nix::unistd::Pid::from_raw(-pid.as_raw()) -} diff --git a/src/pipeline/prelude.rs b/src/pipeline/prelude.rs deleted file mode 100644 index 53b67fc..0000000 --- a/src/pipeline/prelude.rs +++ /dev/null @@ -1 +0,0 @@ -pub use crate::prelude::*; diff --git a/src/runner/builtins/command.rs b/src/runner/builtins/command.rs new file mode 100644 index 0000000..2c1f755 --- /dev/null +++ b/src/runner/builtins/command.rs @@ -0,0 +1,398 @@ +use crate::runner::prelude::*; + +use async_std::io::prelude::BufReadExt as _; + +pub struct Command { + exe: crate::parse::Exe, + f: super::Builtin, + cfg: Cfg, +} + +impl Command { + pub fn new(exe: crate::parse::Exe) -> Result { + if let Some(s) = exe.exe().to_str() { + if let Some(f) = super::BUILTINS.get(s) { + Ok(Self { + exe, + f, + cfg: Cfg::new(), + }) + } else { + Err(exe) + } + } else { + Err(exe) + } + } + + pub fn new_with_io( + exe: crate::parse::Exe, + io: Io, + ) -> Result { + if let Some(s) = exe.exe().to_str() { + if let Some(f) = super::BUILTINS.get(s) { + Ok(Self { + exe, + f, + cfg: Cfg::new_with_io(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 async_std::os::unix::process::CommandExt (this + // is just a wrapper) + pub unsafe fn pre_exec(&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) -> anyhow::Result { + let Self { f, exe, cfg } = self; + (f)(exe, env, cfg) + } +} + +pub struct Cfg { + io: Io, + pre_exec: Option< + Box std::io::Result<()> + Send + Sync>, + >, +} + +impl Cfg { + fn new() -> Self { + Self { + io: Io::new(), + pre_exec: None, + } + } + + fn new_with_io(io: Io) -> Self { + Self { io, pre_exec: None } + } + + pub fn io(&self) -> &Io { + &self.io + } + + // Safety: see pre_exec in async_std::os::unix::process::CommandExt (this + // is just a wrapper) + pub unsafe fn pre_exec(&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, + crate::mutex::Mutex, + >, +} + +impl Io { + pub fn new() -> Self { + Self { + fds: std::collections::HashMap::new(), + } + } + + fn stdin(&self) -> Option> { + self.fds.get(&0).map(crate::mutex::clone) + } + + pub fn set_stdin(&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 + crate::mutex::new(unsafe { File::input(stdin.into_raw_fd()) }), + ); + } + + fn stdout(&self) -> Option> { + self.fds.get(&1).map(crate::mutex::clone) + } + + pub fn set_stdout(&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 + crate::mutex::new(unsafe { File::output(stdout.into_raw_fd()) }), + ); + } + + fn stderr(&self) -> Option> { + self.fds.get(&2).map(crate::mutex::clone) + } + + pub fn set_stderr(&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 + crate::mutex::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) => { + crate::mutex::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 + crate::mutex::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 + crate::mutex::new(unsafe { File::output(fd) }) + } + } + } + }; + self.fds.insert(redirect.from, to); + } + } + + pub async fn read_line_stdin(&self) -> anyhow::Result { + let mut buf = String::new(); + if let Some(fh) = self.stdin() { + if let File::In(fh) = &mut *fh.lock_arc().await { + fh.read_line(&mut buf).await?; + } + } + if buf.ends_with('\n') { + buf.truncate(buf.len() - 1); + } + Ok(buf) + } + + pub async fn write_stdout(&self, buf: &[u8]) -> anyhow::Result<()> { + if let Some(fh) = self.stdout() { + if let File::Out(fh) = &mut *fh.lock_arc().await { + Ok(fh.write_all(buf).await.map(|_| ())?) + } else { + Ok(()) + } + } else { + Ok(()) + } + } + + pub async fn write_stderr(&self, buf: &[u8]) -> anyhow::Result<()> { + if let Some(fh) = self.stderr() { + if let File::Out(fh) = &mut *fh.lock_arc().await { + Ok(fh.write_all(buf).await.map(|_| ())?) + } 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 Some(stdin) = crate::mutex::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 Some(stdout) = crate::mutex::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 Some(stderr) = crate::mutex::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(async_std::io::BufReader), + Out(async_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(async_std::io::BufReader::new( + async_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(async_std::fs::File::from_raw_fd(fd)) + } + + fn maybe_drop(file: crate::mutex::Mutex) { + if let Some(file) = crate::mutex::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) => fh.get_ref().as_raw_fd(), + 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) => fh.into_inner().into_raw_fd(), + Self::Out(fh) => fh.into_raw_fd(), + } + } +} + +pub struct Child<'a> { + fut: std::pin::Pin< + Box< + dyn std::future::Future + + Sync + + Send + + 'a, + >, + >, + wrapped_child: Option>>, +} + +impl<'a> Child<'a> { + pub fn new_fut(fut: F) -> Self + where + F: std::future::Future + + Sync + + Send + + 'a, + { + Self { + fut: Box::pin(fut), + wrapped_child: None, + } + } + + pub fn new_wrapped(child: crate::runner::Child<'a>) -> Self { + Self { + fut: Box::pin(async move { unreachable!() }), + wrapped_child: Some(Box::new(child)), + } + } + + pub fn id(&self) -> Option { + self.wrapped_child.as_ref().and_then(|cmd| cmd.id()) + } + + pub fn status( + self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = anyhow::Result, + > + Send + + Sync + + 'a, + >, + > { + Box::pin(async move { + if let Some(child) = self.wrapped_child { + child.status().await + } else { + Ok(self.fut.await) + } + }) + } +} diff --git a/src/runner/builtins/mod.rs b/src/runner/builtins/mod.rs new file mode 100644 index 0000000..dec4dc2 --- /dev/null +++ b/src/runner/builtins/mod.rs @@ -0,0 +1,314 @@ +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, +) -> anyhow::Result> + + 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("setenv", &setenv); + builtins.insert("unsetenv", &unsetenv); + 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() + ) + .await + .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() + ) + .await + .unwrap(); + $cfg.io().write_stderr(format!($msg, $($arg)*).as_bytes()) + .await + .unwrap(); + $cfg.io().write_stderr(b"\n").await.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, +) -> anyhow::Result { + async fn async_cd( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, + ) -> std::process::ExitStatus { + let dir = exe.args().get(0).map_or("", String::as_str); + let dir = if dir.is_empty() { + if let Some(dir) = home(None) { + dir + } else { + bail!(cfg, exe, "couldn't find current user"); + } + } else if dir.starts_with('~') { + let path: std::path::PathBuf = dir.into(); + if let std::path::Component::Normal(prefix) = + path.components().next().unwrap() + { + let prefix_bytes = prefix.as_bytes(); + let name = if prefix_bytes == b"~" { + None + } else { + Some(std::ffi::OsStr::from_bytes(&prefix_bytes[1..])) + }; + if let Some(home) = home(name) { + home.join(path.strip_prefix(prefix).unwrap()) + } else { + bail!( + cfg, + exe, + "no such user: {}", + name.map(std::ffi::OsStr::to_string_lossy) + .as_ref() + .unwrap_or(&std::borrow::Cow::Borrowed( + "(deleted)" + )) + ); + } + } else { + unreachable!() + } + } else { + dir.into() + }; + if let Err(e) = std::env::set_current_dir(&dir) { + bail!( + cfg, + exe, + "{}: {}", + crate::format::io_error(&e), + dir.display() + ); + } + async_std::process::ExitStatus::from_raw(0) + } + + Ok(command::Child::new_fut(async move { + async_cd(exe, env, cfg).await + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn setenv( + exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> anyhow::Result { + async fn async_setenv( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, + ) -> std::process::ExitStatus { + let k = if let Some(k) = exe.args().get(0).map(String::as_str) { + k + } else { + bail!(cfg, exe, "usage: setenv key value"); + }; + let v = if let Some(v) = exe.args().get(1).map(String::as_str) { + v + } else { + bail!(cfg, exe, "usage: setenv key value"); + }; + + std::env::set_var(k, v); + async_std::process::ExitStatus::from_raw(0) + } + + Ok(command::Child::new_fut(async move { + async_setenv(exe, env, cfg).await + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn unsetenv( + exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> anyhow::Result { + async fn async_unsetenv( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, + ) -> std::process::ExitStatus { + let k = if let Some(k) = exe.args().get(0).map(String::as_str) { + k + } else { + bail!(cfg, exe, "usage: unsetenv key"); + }; + + std::env::remove_var(k); + async_std::process::ExitStatus::from_raw(0) + } + + Ok(command::Child::new_fut(async move { + async_unsetenv(exe, env, cfg).await + })) +} + +// 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, +) -> anyhow::Result { + async fn async_echo( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, + ) -> std::process::ExitStatus { + macro_rules! write_stdout { + ($bytes:expr) => { + if let Err(e) = cfg.io().write_stdout($bytes).await { + cfg.io() + .write_stderr(format!("echo: {}", e).as_bytes()) + .await + .unwrap(); + return async_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" "); + } + } + + async_std::process::ExitStatus::from_raw(0) + } + + Ok(command::Child::new_fut(async move { + async_echo(exe, env, cfg).await + })) +} + +#[allow(clippy::unnecessary_wraps)] +fn read( + exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> anyhow::Result { + async fn async_read( + exe: crate::parse::Exe, + _env: &Env, + cfg: command::Cfg, + ) -> std::process::ExitStatus { + let var = if let Some(var) = exe.args().get(0).map(String::as_str) { + var + } else { + bail!(cfg, exe, "usage: read var"); + }; + + let val = match cfg.io().read_line_stdin().await { + Ok(line) => line, + Err(e) => { + bail!(cfg, exe, e); + } + }; + + std::env::set_var(var, val); + async_std::process::ExitStatus::from_raw(0) + } + + Ok(command::Child::new_fut(async move { + async_read(exe, env, cfg).await + })) +} + +fn and( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> anyhow::Result { + exe.shift(); + if env.latest_status().success() { + let mut cmd = crate::runner::Command::new(exe); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) + } else { + let status = *env.latest_status(); + Ok(command::Child::new_fut(async move { status })) + } +} + +fn or( + mut exe: crate::parse::Exe, + env: &Env, + cfg: command::Cfg, +) -> anyhow::Result { + exe.shift(); + if env.latest_status().success() { + let status = *env.latest_status(); + Ok(command::Child::new_fut(async move { status })) + } else { + let mut cmd = crate::runner::Command::new(exe); + 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, +) -> anyhow::Result { + 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, +) -> anyhow::Result { + exe.shift(); + let mut cmd = crate::runner::Command::new_builtin(exe); + cfg.setup_command(&mut cmd); + Ok(command::Child::new_wrapped(cmd.spawn(env)?)) +} + +fn home(user: Option<&std::ffi::OsStr>) -> Option { + let user = user.map_or_else( + || users::get_user_by_uid(users::get_current_uid()), + users::get_user_by_name, + ); + user.map(|user| user.home_dir().to_path_buf()) +} diff --git a/src/runner/command.rs b/src/runner/command.rs new file mode 100644 index 0000000..34b770e --- /dev/null +++ b/src/runner/command.rs @@ -0,0 +1,219 @@ +use crate::runner::prelude::*; + +pub struct Command { + inner: Inner, + exe: std::path::PathBuf, + redirects: Vec, + pre_exec: Option< + Box std::io::Result<()> + Send + Sync + 'static>, + >, +} +impl Command { + pub fn new(exe: crate::parse::Exe) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + Self { + inner: super::builtins::Command::new(exe).map_or_else( + |exe| Self::new_binary(exe).inner, + Inner::Builtin, + ), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + pub fn new_with_io( + 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_with_io(exe, io) + .map_or_else( + |exe| Self::new_binary(exe).inner, + Inner::Builtin, + ), + exe: exe_path, + redirects, + pre_exec: None, + } + } + + #[allow(clippy::needless_pass_by_value)] + 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 = async_std::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) -> Self { + let exe_path = exe.exe().to_path_buf(); + let redirects = exe.redirects().to_vec(); + Self { + inner: super::builtins::Command::new(exe) + .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 async_std::os::unix::process::CommandExt (this + // is just a wrapper) + pub unsafe fn pre_exec(&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) -> anyhow::Result { + 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 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::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(async_std::process::Command), + Builtin(super::builtins::Command), +} + +pub enum Child<'a> { + Binary(async_std::process::Child), + Builtin(super::builtins::Child<'a>), +} + +impl<'a> Child<'a> { + pub fn id(&self) -> Option { + match self { + Self::Binary(child) => Some(child.id()), + Self::Builtin(child) => child.id(), + } + } + + pub fn status( + self, + ) -> std::pin::Pin< + Box< + dyn std::future::Future< + Output = anyhow::Result, + > + Send + + Sync + + 'a, + >, + > { + Box::pin(async move { + match self { + Self::Binary(child) => Ok(child.status_no_drop().await?), + 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..3d4d025 --- /dev/null +++ b/src/runner/mod.rs @@ -0,0 +1,534 @@ +use crate::runner::prelude::*; + +mod builtins; +mod command; +pub use command::{Child, Command}; +mod prelude; + +const PID0: nix::unistd::Pid = nix::unistd::Pid::from_raw(0); + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub enum Event { + Suspend(usize), + Exit(Env), +} + +struct Stack { + frames: Vec, +} + +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), + While(bool, usize), + For(bool, usize, Vec), +} + +pub async fn run() -> anyhow::Result { + // Safety: we don't create File instances for or read/write data on fds + // 3 or 4 anywhere else + let shell_read = unsafe { async_std::fs::File::from_raw_fd(3) }; + let shell_write = unsafe { async_std::fs::File::from_raw_fd(4) }; + cloexec(3)?; + cloexec(4)?; + + let (commands, mut env) = read_data(shell_read).await?; + run_commands(&commands, &mut env, &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: &str, + env: &mut Env, + shell_write: &async_std::fs::File, +) -> anyhow::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, 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)); + } + if should { + run_pipeline(pipeline.clone(), env, shell_write).await?; + if let Some(Frame::If(should)) = stack.top_mut() { + *should = env.latest_status().success(); + } else { + unreachable!(); + } + } + 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 { + run_pipeline(pipeline.clone(), env, shell_write).await?; + if let Some(Frame::While(should, _)) = stack.top_mut() { + *should = env.latest_status().success(); + } else { + unreachable!(); + } + } + 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| w.eval(env)) + .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); + env.set_var(var, &val); + } + } else { + unreachable!(); + } + } + 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, + shell_write: &async_std::fs::File, +) -> anyhow::Result<()> { + // 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 { async_std::fs::File::from_raw_fd(0) }; + let stdout = unsafe { async_std::fs::File::from_raw_fd(1) }; + let stderr = unsafe { async_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 (children, pg) = spawn_children(pipeline, env, &io)?; + let status = wait_children(children, pg, env, &io, shell_write).await; + set_foreground_pg(nix::unistd::getpid())?; + env.update(status)?; + Ok(()) +} + +async fn read_data( + mut fh: async_std::fs::File, +) -> anyhow::Result<(String, Env)> { + let mut data = vec![]; + fh.read_to_end(&mut data).await?; + let commands = bincode::deserialize(&data).unwrap(); + let len: usize = bincode::serialized_size(&commands) + .unwrap() + .try_into() + .unwrap(); + let env = Env::from_bytes(&data[len..]); + Ok((commands, env)) +} + +async fn write_event( + mut fh: &async_std::fs::File, + event: Event, +) -> anyhow::Result<()> { + fh.write_all(&bincode::serialize(&event)?).await?; + fh.flush().await?; + Ok(()) +} + +fn spawn_children<'a>( + pipeline: crate::parse::ast::Pipeline, + env: &'a Env, + io: &builtins::Io, +) -> anyhow::Result<(Vec>, Option)> { + let pipeline = pipeline.eval(env); + let mut cmds: Vec<_> = pipeline + .into_exes() + .map(|exe| Command::new_with_io(exe, io.clone())) + .collect(); + for i in 0..(cmds.len() - 1) { + let (r, w) = 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 || { + setpgid_child(pg_pid)?; + Ok(()) + }); + } + let child = cmd.spawn(env)?; + if let Some(id) = child.id() { + let child_pid = id_to_pid(id); + setpgid_parent(child_pid, pg_pid)?; + if pg_pid.is_none() { + pg_pid = Some(child_pid); + set_foreground_pg(child_pid)?; + } + } + children.push(child); + } + Ok((children, pg_pid)) +} + +async fn wait_children( + children: Vec>, + pg: Option, + env: &Env, + io: &builtins::Io, + shell_write: &async_std::fs::File, +) -> std::process::ExitStatus { + enum Res { + Child(nix::Result), + Builtin(Option<(anyhow::Result, bool)>), + } + + macro_rules! bail { + ($e:expr) => { + // if writing to stderr is not possible, we still want to exit + // normally with a failure exit code + #[allow(clippy::let_underscore_drop)] + let _ = + io.write_stderr(format!("nbsh: {}\n", $e).as_bytes()).await; + 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)| { + (id_to_pid(child.id().unwrap()), (child, i == count - 1)) + }) + .collect(); + let mut 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) = async_std::channel::unbounded(); + let new_wait = move || { + if let Some(pg) = pg { + let wait_w = wait_w.clone(); + async_std::task::spawn(async move { + let res = blocking::unblock(move || { + nix::sys::wait::waitpid( + neg_pid(pg), + Some(nix::sys::wait::WaitPidFlag::WUNTRACED), + ) + }) + .await; + if wait_w.is_closed() { + // we shouldn't be able to drop real process terminations + assert!(res.is_err()); + } else { + wait_w.send(res).await.unwrap(); + } + }); + } + }; + + new_wait(); + loop { + if children.is_empty() && builtins.is_empty() { + break; + } + + let child = async { Res::Child(wait_r.recv().await.unwrap()) }; + let builtin = async { + Res::Builtin(if builtins.is_empty() { + std::future::pending().await + } else { + builtins.next().await + }) + }; + match child.race(builtin).await { + 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(env.idx()), + ) + .await + { + bail!(e); + } + if let Err(e) = nix::sys::signal::kill( + pid, + nix::sys::signal::Signal::SIGCONT, + ) { + bail!(e); + } + } + } + _ => {} + } + new_wait(); + } + Res::Child(Err(e)) => { + bail!(e); + } + Res::Builtin(Some((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); + } + } + Res::Builtin(Some((Err(e), _))) => { + bail!(e); + } + Res::Builtin(None) => {} + } + } + + final_status.unwrap() +} + +fn pipe() -> anyhow::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) + })) +} + +fn set_foreground_pg(pg: nix::unistd::Pid) -> anyhow::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) + .or_else(|e| { + // the process group has already exited + if e == nix::errno::Errno::ESRCH { + Ok(()) + } else { + Err(e) + } + })?; + + Ok(()) +} + +fn setpgid_child(pg: Option) -> std::io::Result<()> { + nix::unistd::setpgid(PID0, pg.unwrap_or(PID0))?; + Ok(()) +} + +fn setpgid_parent( + pid: nix::unistd::Pid, + pg: Option, +) -> anyhow::Result<()> { + nix::unistd::setpgid(pid, pg.unwrap_or(PID0)).or_else(|e| { + // EACCES means that the child already called exec, but if it did, + // then it also must have already called setpgid itself, so we don't + // care. ESRCH means that the process already exited, which is similar + if e == nix::errno::Errno::EACCES || e == nix::errno::Errno::ESRCH { + Ok(()) + } else { + Err(e) + } + })?; + Ok(()) +} + +fn cloexec(fd: std::os::unix::io::RawFd) -> anyhow::Result<()> { + nix::fcntl::fcntl( + fd, + nix::fcntl::FcntlArg::F_SETFD(nix::fcntl::FdFlag::FD_CLOEXEC), + )?; + Ok(()) +} + +fn id_to_pid(id: u32) -> nix::unistd::Pid { + nix::unistd::Pid::from_raw(id.try_into().unwrap()) +} + +fn neg_pid(pid: nix::unistd::Pid) -> nix::unistd::Pid { + nix::unistd::Pid::from_raw(-pid.as_raw()) +} 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/shell/history/mod.rs b/src/shell/history/mod.rs index 429545b..b241ca5 100644 --- a/src/shell/history/mod.rs +++ b/src/shell/history/mod.rs @@ -374,7 +374,7 @@ async fn spawn_commands( let mut exit_done = None; loop { enum Res { - Read(bincode::Result), + Read(bincode::Result), Exit(std::io::Result), } @@ -389,11 +389,11 @@ async fn spawn_commands( }; match read.or(exit).await { Res::Read(Ok(event)) => match event { - crate::pipeline::Event::Suspend(idx) => { + crate::runner::Event::Suspend(idx) => { event_w.send(Event::ChildSuspend(idx)).await.unwrap(); new_read(); } - crate::pipeline::Event::Exit(new_env) => { + crate::runner::Event::Exit(new_env) => { *env = new_env; read_done = true; } -- cgit v1.2.3-54-g00ecf