summaryrefslogtreecommitdiffstats
path: root/src/runner
diff options
context:
space:
mode:
Diffstat (limited to 'src/runner')
-rw-r--r--src/runner/builtins/command.rs373
-rw-r--r--src/runner/builtins/mod.rs242
-rw-r--r--src/runner/command.rs203
-rw-r--r--src/runner/mod.rs499
-rw-r--r--src/runner/prelude.rs1
-rw-r--r--src/runner/sys.rs79
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())
+}