From d2894a231fce5c92eaf611f8cf453e7355b80c91 Mon Sep 17 00:00:00 2001 From: Arvid Norlander Date: Mon, 22 Jul 2024 13:21:09 +0200 Subject: [PATCH] modules: Improve process module This greatly increases the power of the process module: * Capturing output * Creating pipelines --- crates/rune-modules/src/process.rs | 281 +++++++++++++++++++++++++++-- 1 file changed, 270 insertions(+), 11 deletions(-) diff --git a/crates/rune-modules/src/process.rs b/crates/rune-modules/src/process.rs index 8a398c38a..4b7940643 100644 --- a/crates/rune-modules/src/process.rs +++ b/crates/rune-modules/src/process.rs @@ -32,37 +32,83 @@ use rune::alloc::clone::TryClone; use rune::alloc::fmt::TryWrite; use rune::alloc::Vec; -use rune::runtime::{Bytes, Formatter, Value, VmResult}; -use rune::{vm_try, Any, ContextError, Module}; +use rune::runtime::{Bytes, Formatter, Mut, Value, VmResult}; +use rune::{vm_try, vm_write, Any, ContextError, Module}; use std::io; use tokio::process; -/// Construct the `process` module. +/// A module for working with processes. +/// +/// This allows spawning child processes, capturing their output, and creating pipelines. +#[rune::module(::process)] pub fn module(_stdio: bool) -> Result { - let mut module = Module::with_crate("process")?; + let mut module = Module::from_meta(self::module_meta)?; module.ty::()?; module.ty::()?; module.ty::()?; module.ty::()?; + module.ty::()?; + module.ty::()?; + module.ty::()?; + module.ty::()?; + module.function_meta(Command::string_debug)?; module.function_meta(Command::new)?; module.function_meta(Command::spawn)?; module.function_meta(Command::arg)?; module.function_meta(Command::args)?; + #[cfg(unix)] + module.function_meta(Command::arg0)?; + module.function_meta(Command::stdin)?; + module.function_meta(Command::stdout)?; + module.function_meta(Command::stderr)?; + + module.function_meta(Child::string_debug)?; + module.function_meta(Child::stdin)?; + module.function_meta(Child::stdout)?; + module.function_meta(Child::stderr)?; + module.function_meta(Child::id)?; + module.function_meta(Child::start_kill)?; + module.function_meta(Child::kill)?; + module.function_meta(Child::wait)?; module.function_meta(Child::wait_with_output)?; + + module.function_meta(ExitStatus::string_debug)?; module.function_meta(ExitStatus::string_display)?; module.function_meta(ExitStatus::code)?; + module.function_meta(ExitStatus::success)?; + + module.function_meta(Output::string_debug)?; + module.function_meta(Stdio::null)?; + module.function_meta(Stdio::inherit)?; + module.function_meta(Stdio::piped)?; + + module.function_meta(ChildStdin::string_debug)?; + module.function_meta(ChildStdin::try_into_stdio)?; + + module.function_meta(ChildStdout::string_debug)?; + module.function_meta(ChildStdout::try_into_stdio)?; + + module.function_meta(ChildStderr::string_debug)?; + module.function_meta(ChildStderr::try_into_stdio)?; + Ok(module) } -#[derive(Any)] +/// A builder for a child command to execute +#[derive(Debug, Any)] #[rune(item = ::process)] struct Command { inner: process::Command, } impl Command { + #[rune::function(vm_result, protocol = STRING_DEBUG)] + fn string_debug(&self, f: &mut Formatter) { + vm_write!(f, "{:?}", self); + } + /// Construct a new command. #[rune::function(path = Self::new)] fn new(command: &str) -> Self { @@ -87,6 +133,31 @@ impl Command { self.inner.arg(arg); } + #[cfg(unix)] + #[rune::function(instance)] + /// Set the first process argument, argv[0], to something other than the default executable path. (Unix only) + fn arg0(&mut self, arg: &str) { + self.inner.arg0(arg); + } + + /// Sets configuration for the child process’s standard input (stdin) handle. + #[rune::function(instance)] + fn stdin(&mut self, stdio: Stdio) { + self.inner.stdin(stdio.inner); + } + + /// Sets configuration for the child process’s standard output (stdout) handle. + #[rune::function(instance)] + fn stdout(&mut self, stdio: Stdio) { + self.inner.stdout(stdio.inner); + } + + /// Sets configuration for the child process’s standard error (stderr) handle. + #[rune::function(instance)] + fn stderr(&mut self, stdio: Stdio) { + self.inner.stderr(stdio.inner); + } + /// Spawn the command. #[rune::function(instance)] fn spawn(mut self) -> io::Result { @@ -96,7 +167,8 @@ impl Command { } } -#[derive(Any)] +/// A running child process +#[derive(Debug, Any)] #[rune(item = ::process)] struct Child { // we use an option to avoid a panic if we try to complete the child process @@ -107,6 +179,103 @@ struct Child { } impl Child { + #[rune::function(vm_result, protocol = STRING_DEBUG)] + fn string_debug(&self, f: &mut Formatter) { + vm_write!(f, "{:?}", self); + } + + /// Attempt to take the stdin of the child process. + /// + /// Once taken this can not be taken again. + #[rune::function(instance)] + fn stdin(&mut self) -> Option { + let inner = match &mut self.inner { + Some(inner) => inner, + None => return None, + }; + let stdin = inner.stdin.take()?; + Some(ChildStdin { inner: stdin }) + } + + /// Attempt to take the stdout of the child process. + /// + /// Once taken this can not be taken again. + #[rune::function(instance)] + fn stdout(&mut self) -> Option { + let inner = match &mut self.inner { + Some(inner) => inner, + None => return None, + }; + let stdout = inner.stdout.take()?; + Some(ChildStdout { inner: stdout }) + } + + /// Attempt to take the stderr of the child process. + /// + /// Once taken this can not be taken again. + #[rune::function(instance)] + fn stderr(&mut self) -> Option { + let inner = match &mut self.inner { + Some(inner) => inner, + None => return None, + }; + let stderr = inner.stderr.take()?; + Some(ChildStderr { inner: stderr }) + } + + /// Attempt to get the OS process id of the child process. + /// + /// This will return None after the child process has completed. + #[rune::function(instance)] + fn id(&self) -> Option { + match &self.inner { + Some(inner) => inner.id(), + None => None, + } + } + + #[rune::function(vm_result, instance)] + fn start_kill(&mut self) -> io::Result<()> { + let inner = match &mut self.inner { + Some(inner) => inner, + None => { + rune::vm_panic!("already completed"); + } + }; + + inner.start_kill() + } + + /// Sends a signal to the child process. + #[rune::function(vm_result, instance, path = Self::kill)] + async fn kill(mut this: Mut) -> io::Result<()> { + let inner = match &mut this.inner { + Some(inner) => inner, + None => { + rune::vm_panic!("already completed"); + } + }; + + inner.kill().await + } + + /// Attempt to wait for the child process to exit. + /// + /// This will not capture output, use [`wait_with_output`] for that. + #[rune::function(vm_result, instance)] + async fn wait(self) -> io::Result { + let mut inner = match self.inner { + Some(inner) => inner, + None => { + rune::vm_panic!("already completed"); + } + }; + + let status = inner.wait().await?; + + Ok(ExitStatus { status }) + } + // Returns a future that will resolve to an Output, containing the exit // status, stdout, and stderr of the child process. #[rune::function(vm_result, instance)] @@ -130,7 +299,8 @@ impl Child { } } -#[derive(Any)] +/// The output and exit status, returned by [`Child::wait_with_output`]. +#[derive(Debug, Any)] #[rune(item = ::process)] struct Output { #[rune(get)] @@ -141,16 +311,34 @@ struct Output { stderr: Bytes, } -#[derive(TryClone, Clone, Copy, Any)] +impl Output { + #[rune::function(vm_result, protocol = STRING_DEBUG)] + fn string_debug(&self, f: &mut Formatter) { + vm_write!(f, "{:?}", self); + } +} + +/// The exit status from a completed child process +#[derive(Debug, TryClone, Clone, Copy, Any)] #[rune(item = ::process)] struct ExitStatus { status: std::process::ExitStatus, } impl ExitStatus { - #[rune::function(protocol = STRING_DISPLAY)] - fn string_display(&self, f: &mut Formatter) -> VmResult<()> { - rune::vm_write!(f, "{}", self.status) + #[rune::function(vm_result, protocol = STRING_DISPLAY)] + fn string_display(&self, f: &mut Formatter) { + vm_write!(f, "{}", self.status); + } + + #[rune::function(vm_result, protocol = STRING_DEBUG)] + fn string_debug(&self, f: &mut Formatter) { + vm_write!(f, "{:?}", self); + } + + #[rune::function] + fn success(&self) -> bool { + self.status.success() } #[rune::function] @@ -158,3 +346,74 @@ impl ExitStatus { self.status.code() } } + +/// Describes what to do with a standard I/O stream for a child process when passed to the stdin, stdout, and stderr methods of Command. +#[derive(Debug, Any)] +#[rune(item = ::process)] +struct Stdio { + inner: std::process::Stdio, +} + +impl Stdio { + #[rune::function(vm_result, protocol = STRING_DEBUG)] + fn string_debug(&self, f: &mut Formatter) { + vm_write!(f, "{:?}", self); + } + + /// This stream will be ignored. This is the equivalent of attaching the stream to /dev/null. + #[rune::function(path = Self::null)] + fn null() -> Self { + Self { + inner: std::process::Stdio::null(), + } + } + + /// The child inherits from the corresponding parent descriptor. This is the default. + #[rune::function(path = Self::inherit)] + fn inherit() -> Self { + Self { + inner: std::process::Stdio::inherit(), + } + } + + /// A new pipe should be arranged to connect the parent and child processes. + #[rune::function(path = Self::piped)] + fn piped() -> Self { + Self { + inner: std::process::Stdio::piped(), + } + } +} + +macro_rules! stdio_stream { + ($name:ident, $stream:tt) => { + #[derive(Debug, Any)] + #[rune(item = ::process)] + #[doc = concat!("The ", $stream, " stream for spawned children.")] + struct $name { + inner: process::$name, + } + + impl $name { + #[rune::function(vm_result, protocol = STRING_DEBUG)] + fn string_debug(&self, f: &mut Formatter) { + vm_write!(f, "{:?}", self); + } + + /// Try to convert into a `Stdio`, which allows creating a pipeline between processes. + /// + /// This consumes the stream, as it can only be used once. + /// + /// Returns a Result + #[rune::function(instance)] + fn try_into_stdio(self) -> Result { + Ok(Stdio { + inner: self.inner.try_into()?, + }) + } + } + }; +} +stdio_stream!(ChildStdin, "stdin"); +stdio_stream!(ChildStdout, "stdout"); +stdio_stream!(ChildStderr, "stderr");