Clean up process spawn options

This commit is contained in:
Filip Tibell 2023-08-20 20:45:24 -05:00
parent 1baf10fa87
commit 6db01d7b94
2 changed files with 186 additions and 118 deletions

View file

@ -1,15 +1,12 @@
use std::{ use std::{
collections::HashMap,
env::{self, consts}, env::{self, consts},
path::{self, PathBuf}, path,
process::Stdio, process::Stdio,
}; };
use directories::UserDirs;
use dunce::canonicalize; use dunce::canonicalize;
use mlua::prelude::*; use mlua::prelude::*;
use os_str_bytes::RawOsString; use os_str_bytes::RawOsString;
use tokio::process::Command;
use crate::lune::{scheduler::Scheduler, util::TableBuilder}; use crate::lune::{scheduler::Scheduler, util::TableBuilder};
@ -18,6 +15,9 @@ mod tee_writer;
mod pipe_inherit; mod pipe_inherit;
use pipe_inherit::pipe_and_inherit_child_process_stdio; use pipe_inherit::pipe_and_inherit_child_process_stdio;
mod options;
use options::ProcessSpawnOptions;
const PROCESS_EXIT_IMPL_LUA: &str = r#" const PROCESS_EXIT_IMPL_LUA: &str = r#"
exit(...) exit(...)
yield() yield()
@ -161,127 +161,18 @@ fn process_env_iter<'lua>(
async fn process_spawn<'lua>( async fn process_spawn<'lua>(
lua: &'static Lua, lua: &'static Lua,
(mut program, args, options): (String, Option<Vec<String>>, Option<LuaTable<'lua>>), (program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable<'lua>> { ) -> LuaResult<LuaTable<'lua>> {
// Parse any given options or create defaults let inherit_stdio = options.inherit_stdio;
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::<String, String>() {
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);
// Spawn the child process // Spawn the child process
let child = cmd let child = options
.into_command(program, args)
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn()?; .spawn()?;
// Inherit the output and stderr if wanted // 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 pipe_and_inherit_child_process_stdio(child).await
} else { } else {
let output = child.wait_with_output().await?; let output = child.wait_with_output().await?;

View file

@ -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<PathBuf>,
pub(crate) envs: HashMap<String, String>,
pub(crate) shell: Option<String>,
pub(crate) inherit_stdio: bool,
}
impl<'lua> FromLua<'lua> for ProcessSpawnOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
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::<String, String>() {
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<String>, args: Option<Vec<String>>) -> 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
}
}