diff --git a/src/lune/builtins/process/mod.rs b/src/lune/builtins/process/mod.rs index 895db4e..8c36235 100644 --- a/src/lune/builtins/process/mod.rs +++ b/src/lune/builtins/process/mod.rs @@ -1,15 +1,12 @@ use std::{ - collections::HashMap, env::{self, consts}, - path::{self, PathBuf}, + path, process::Stdio, }; -use directories::UserDirs; use dunce::canonicalize; use mlua::prelude::*; use os_str_bytes::RawOsString; -use tokio::process::Command; use crate::lune::{scheduler::Scheduler, util::TableBuilder}; @@ -18,6 +15,9 @@ mod tee_writer; mod pipe_inherit; use pipe_inherit::pipe_and_inherit_child_process_stdio; +mod options; +use options::ProcessSpawnOptions; + const PROCESS_EXIT_IMPL_LUA: &str = r#" exit(...) yield() @@ -161,127 +161,18 @@ fn process_env_iter<'lua>( async fn process_spawn<'lua>( lua: &'static Lua, - (mut program, args, options): (String, Option>, Option>), + (program, args, options): (String, Option>, ProcessSpawnOptions), ) -> LuaResult> { - // Parse any given options or create defaults - let (child_cwd, child_envs, child_shell, child_stdio_inherit) = match options { - Some(options) => { - let mut cwd = env::current_dir()?; - let mut envs = HashMap::new(); - let mut shell = None; - let mut inherit = false; - match options.raw_get("cwd")? { - LuaValue::Nil => {} - LuaValue::String(s) => { - cwd = PathBuf::from(s.to_string_lossy().to_string()); - // Substitute leading tilde (~) for the actual home dir - if cwd.starts_with("~") { - if let Some(user_dirs) = UserDirs::new() { - cwd = user_dirs.home_dir().join(cwd.strip_prefix("~").unwrap()) - } - }; - if !cwd.exists() { - return Err(LuaError::RuntimeError( - "Invalid value for option 'cwd' - path does not exist".to_string(), - )); - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'cwd' - expected 'string', got '{}'", - value.type_name() - ))) - } - } - match options.raw_get("env")? { - LuaValue::Nil => {} - LuaValue::Table(t) => { - for pair in t.pairs::() { - let (k, v) = pair?; - envs.insert(k, v); - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'env' - expected 'table', got '{}'", - value.type_name() - ))) - } - } - match options.raw_get("shell")? { - LuaValue::Nil => {} - LuaValue::String(s) => shell = Some(s.to_string_lossy().to_string()), - LuaValue::Boolean(true) => { - shell = match env::consts::FAMILY { - "unix" => Some("/bin/sh".to_string()), - "windows" => Some("/bin/sh".to_string()), - _ => None, - }; - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'", - value.type_name() - ))) - } - } - match options.raw_get("stdio")? { - LuaValue::Nil => {} - LuaValue::String(s) => { - match s.to_str()? { - "inherit" => { - inherit = true; - }, - "default" => { - inherit = false; - } - _ => return Err(LuaError::RuntimeError( - format!("Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", s.to_string_lossy()), - )) - } - } - value => { - return Err(LuaError::RuntimeError(format!( - "Invalid type for option 'stdio' - expected 'string', got '{}'", - value.type_name() - ))) - } - } - Ok::<_, LuaError>((cwd, envs, shell, inherit)) - } - None => Ok((env::current_dir()?, HashMap::new(), None, false)), - }?; - // Run a shell using the command param if wanted - let child_args = if let Some(shell) = child_shell { - let shell_args = match args { - Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))], - None => vec!["-c".to_string(), program], - }; - program = shell; - Some(shell_args) - } else { - args - }; - // Create command with the wanted options - let mut cmd = match child_args { - None => Command::new(program), - Some(args) => { - let mut cmd = Command::new(program); - cmd.args(args); - cmd - } - }; - // Set dir to run in and env variables - cmd.current_dir(child_cwd); - cmd.envs(child_envs); + let inherit_stdio = options.inherit_stdio; // Spawn the child process - let child = cmd + let child = options + .into_command(program, args) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn()?; // Inherit the output and stderr if wanted - let result = if child_stdio_inherit { + let result = if inherit_stdio { pipe_and_inherit_child_process_stdio(child).await } else { let output = child.wait_with_output().await?; diff --git a/src/lune/builtins/process/options.rs b/src/lune/builtins/process/options.rs new file mode 100644 index 0000000..688caa3 --- /dev/null +++ b/src/lune/builtins/process/options.rs @@ -0,0 +1,177 @@ +use std::{ + collections::HashMap, + env::{self}, + path::PathBuf, +}; + +use directories::UserDirs; +use mlua::prelude::*; +use tokio::process::Command; + +#[derive(Debug, Clone, Default)] +pub struct ProcessSpawnOptions { + pub(crate) cwd: Option, + pub(crate) envs: HashMap, + pub(crate) shell: Option, + pub(crate) inherit_stdio: bool, +} + +impl<'lua> FromLua<'lua> for ProcessSpawnOptions { + fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult { + let mut this = Self::default(); + let value = match value { + LuaValue::Nil => return Ok(this), + LuaValue::Table(t) => t, + _ => { + return Err(LuaError::FromLuaConversionError { + from: value.type_name(), + to: "ProcessSpawnOptions", + message: Some(format!( + "Invalid spawn options - expected table, got {}", + value.type_name() + )), + }) + } + }; + + /* + If we got a working directory to use: + + 1. Substitute leading tilde (~) for the users home dir + 2. Make sure it exists + */ + match value.get("cwd")? { + LuaValue::Nil => {} + LuaValue::String(s) => { + let mut cwd = PathBuf::from(s.to_str()?); + if let Ok(stripped) = cwd.strip_prefix("~") { + let user_dirs = UserDirs::new().ok_or_else(|| { + LuaError::runtime( + "Invalid value for option 'cwd' - failed to get home directory", + ) + })?; + cwd = user_dirs.home_dir().join(stripped) + } + if !cwd.exists() { + return Err(LuaError::runtime( + "Invalid value for option 'cwd' - path does not exist", + )); + }; + this.cwd = Some(cwd); + } + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'cwd' - expected string, got '{}'", + value.type_name() + ))) + } + } + + /* + If we got environment variables, make sure they are strings + */ + match value.get("env")? { + LuaValue::Nil => {} + LuaValue::Table(e) => { + for pair in e.pairs::() { + let (k, v) = pair.context("Environment variables must be strings")?; + this.envs.insert(k, v); + } + } + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'env' - expected table, got '{}'", + value.type_name() + ))) + } + } + + /* + If we got a shell to use: + + 1. When given as a string, use that literally + 2. When set to true, use a default shell for the platform + */ + match value.get("shell")? { + LuaValue::Nil => {} + LuaValue::String(s) => this.shell = Some(s.to_string_lossy().to_string()), + LuaValue::Boolean(true) => { + this.shell = match env::consts::FAMILY { + "unix" => Some("/bin/sh".to_string()), + "windows" => Some("/bin/sh".to_string()), + _ => None, + }; + } + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'shell' - expected 'true' or 'string', got '{}'", + value.type_name() + ))) + } + } + + /* + If we got options for stdio handling, make sure its one of the constant values + */ + match value.get("stdio")? { + LuaValue::Nil => {} + LuaValue::String(s) => match s.to_str()? { + "inherit" => this.inherit_stdio = true, + "default" => this.inherit_stdio = false, + _ => { + return Err(LuaError::RuntimeError(format!( + "Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", + s.to_string_lossy() + ))) + } + }, + value => { + return Err(LuaError::RuntimeError(format!( + "Invalid type for option 'stdio' - expected 'string', got '{}'", + value.type_name() + ))) + } + } + + Ok(this) + } +} + +impl ProcessSpawnOptions { + pub fn into_command(self, program: impl Into, args: Option>) -> Command { + let mut program = program.into(); + + // Run a shell using the command param if wanted + let pargs = match self.shell { + None => args, + Some(shell) => { + let shell_args = match args { + Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))], + None => vec!["-c".to_string(), program.to_string()], + }; + program = shell.to_string(); + Some(shell_args) + } + }; + + // Create command with the wanted options + let mut cmd = match pargs { + None => Command::new(program), + Some(args) => { + let mut cmd = Command::new(program); + cmd.args(args); + cmd + } + }; + + // Set dir to run in and env variables + if let Some(cwd) = self.cwd { + cmd.current_dir(cwd); + } + if !self.envs.is_empty() { + cmd.envs(self.envs); + } + + cmd + } +}