Implement new process spawn options & cwd constant

This commit is contained in:
Filip Tibell 2023-01-27 23:46:07 -05:00
parent 16911e904e
commit 08360a861f
No known key found for this signature in database
7 changed files with 291 additions and 28 deletions

View file

@ -322,7 +322,7 @@ declare process: {
options: { options: {
cwd: string?, cwd: string?,
env: { [string]: string }?, env: { [string]: string }?,
shell: boolean | string, shell: (boolean | string)?,
stdio: ("inherit" | "default")?, stdio: ("inherit" | "default")?,
}? }?
) -> { ) -> {

View file

@ -1,12 +1,26 @@
use std::{env, process::Stdio, sync::Weak}; use std::{
collections::HashMap,
env,
path::PathBuf,
process::{Command, Stdio},
sync::Weak,
};
use mlua::prelude::*; use mlua::prelude::*;
use os_str_bytes::RawOsString; use os_str_bytes::RawOsString;
use smol::{channel::Sender, process::Command}; use smol::channel::Sender;
use crate::{utils::table::TableBuilder, LuneMessage}; use crate::{
utils::{process::pipe_and_inherit_child_process_stdio, table::TableBuilder},
LuneMessage,
};
pub fn create(lua: &Lua, args_vec: Vec<String>) -> LuaResult<()> { pub fn create(lua: &Lua, args_vec: Vec<String>) -> LuaResult<()> {
let cwd = env::current_dir()?.canonicalize()?;
let mut cwd_str = cwd.to_string_lossy().to_string();
if !cwd_str.ends_with('/') {
cwd_str = format!("{}/", cwd_str);
}
// Create readonly args array // Create readonly args array
let args_tab = TableBuilder::new(lua)? let args_tab = TableBuilder::new(lua)?
.with_sequential_values(args_vec)? .with_sequential_values(args_vec)?
@ -26,9 +40,10 @@ pub fn create(lua: &Lua, args_vec: Vec<String>) -> LuaResult<()> {
"process", "process",
TableBuilder::new(lua)? TableBuilder::new(lua)?
.with_value("args", args_tab)? .with_value("args", args_tab)?
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)? .with_value("env", env_tab)?
.with_async_function("exit", process_exit)? .with_async_function("exit", process_exit)?
.with_async_function("spawn", process_spawn)? .with_function("spawn", process_spawn)?
.build_readonly()?, .build_readonly()?,
) )
} }
@ -112,37 +127,141 @@ async fn process_exit(lua: &Lua, exit_code: Option<u8>) -> LuaResult<()> {
Ok(()) Ok(())
} }
async fn process_spawn( fn process_spawn<'a>(
lua: &Lua, lua: &'a Lua,
(program, args): (String, Option<Vec<String>>), (mut program, args, options): (String, Option<Vec<String>>, Option<LuaTable<'a>>),
) -> LuaResult<LuaTable> { ) -> LuaResult<LuaTable<'a>> {
// Create and spawn our child process // Parse any given options or create defaults
let pwd = env::current_dir()?; let (child_cwd, child_envs, child_shell, child_stdio_inherit) = match options {
let mut cmd = Command::new(program); Some(options) => {
if let Some(args) = args { let mut cwd = env::current_dir()?;
cmd.args(args); let mut envs = HashMap::new();
} let mut shell = None;
let output = cmd let mut inherit = false;
.current_dir(pwd) match options.raw_get("cwd")? {
LuaValue::Nil => {}
LuaValue::String(s) => {
cwd = PathBuf::from(s.to_string_lossy().to_string());
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
}
};
// FUTURE: Implement and test for tilde (~) to home dir substitution in child_cwd
cmd.current_dir(child_cwd);
cmd.envs(child_envs);
// Spawn the child process
let child = cmd
.stdin(Stdio::null()) .stdin(Stdio::null())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.output() .spawn()?;
.await?; // Inherit the output and stderr if wanted
let result = if child_stdio_inherit {
pipe_and_inherit_child_process_stdio(child)
} else {
let output = child.wait_with_output()?;
Ok((output.status, output.stdout, output.stderr))
};
// Extract result
let (status, stdout, stderr) = result?;
// NOTE: If an exit code was not given by the child process, // NOTE: If an exit code was not given by the child process,
// we default to 1 if it yielded any error output, otherwise 0 // we default to 1 if it yielded any error output, otherwise 0
let code = output let code = status.code().unwrap_or(match stderr.is_empty() {
.status true => 0,
.code() false => 1,
.unwrap_or(match output.stderr.is_empty() { });
true => 0,
false => 1,
});
// Construct and return a readonly lua table with results // Construct and return a readonly lua table with results
TableBuilder::new(lua)? TableBuilder::new(lua)?
.with_value("ok", code == 0)? .with_value("ok", code == 0)?
.with_value("code", code)? .with_value("code", code)?
.with_value("stdout", lua.create_string(&output.stdout)?)? .with_value("stdout", lua.create_string(&stdout)?)?
.with_value("stderr", lua.create_string(&output.stderr)?)? .with_value("stderr", lua.create_string(&stderr)?)?
.build_readonly() .build_readonly()
} }

View file

@ -219,6 +219,7 @@ mod tests {
net_json_decode: "net/json/decode", net_json_decode: "net/json/decode",
net_json_encode: "net/json/encode", net_json_encode: "net/json/encode",
process_args: "process/args", process_args: "process/args",
process_cwd: "process/cwd",
process_env: "process/env", process_env: "process/env",
process_exit: "process/exit", process_exit: "process/exit",
process_spawn: "process/spawn", process_spawn: "process/spawn",

View file

@ -1,4 +1,5 @@
pub mod formatting; pub mod formatting;
pub mod net; pub mod net;
pub mod process;
pub mod table; pub mod table;
pub mod task; pub mod task;

74
src/lib/utils/process.rs Normal file
View file

@ -0,0 +1,74 @@
// https://stackoverflow.com/questions/71141122/-
use std::{
io,
io::Write,
process::{Child, ExitStatus},
};
use mlua::prelude::*;
pub struct TeeWriter<'a, W0: Write, W1: Write> {
w0: &'a mut W0,
w1: &'a mut W1,
}
impl<'a, W0: Write, W1: Write> TeeWriter<'a, W0, W1> {
pub fn new(w0: &'a mut W0, w1: &'a mut W1) -> Self {
Self { w0, w1 }
}
}
impl<'a, W0: Write, W1: Write> Write for TeeWriter<'a, W0, W1> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
// We have to use write_all() otherwise what
// happens if different amounts are written?
self.w0.write_all(buf)?;
self.w1.write_all(buf)?;
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.w0.flush()?;
self.w1.flush()?;
Ok(())
}
}
pub fn pipe_and_inherit_child_process_stdio(
mut child: Child,
) -> LuaResult<(ExitStatus, Vec<u8>, Vec<u8>)> {
// https://stackoverflow.com/questions/71141122/-
let mut child_stdout = child.stdout.take().unwrap();
let mut child_stderr = child.stderr.take().unwrap();
std::thread::scope(|s| {
let stdout_thread = s.spawn(|| {
let stdout = io::stdout();
let mut log = Vec::new();
let mut stdout = stdout.lock();
let mut tee = TeeWriter::new(&mut stdout, &mut log);
io::copy(&mut child_stdout, &mut tee).map_err(LuaError::external)?;
Ok(log)
});
let stderr_thread = s.spawn(|| {
let stderr = io::stderr();
let mut log = Vec::new();
let mut stderr = stderr.lock();
let mut tee = TeeWriter::new(&mut stderr, &mut log);
io::copy(&mut child_stderr, &mut tee).map_err(LuaError::external)?;
Ok(log)
});
let status = child.wait().expect("child wasn't running");
let stdout_log: Result<_, LuaError> = stdout_thread.join().expect("stdout thread panicked");
let stderr_log: Result<_, LuaError> = stderr_thread.join().expect("stderr thread panicked");
Ok::<_, LuaError>((status, stdout_log?, stderr_log?))
})
}

View file

@ -0,0 +1,7 @@
assert(process.cwd ~= nil, "Process cwd is missing")
assert(type(process.cwd) == "string", "Process cwd is not a string")
assert(#process.cwd > 0, "Process cwd is an empty string")
assert(string.sub(process.cwd, -1) == "/", "Process cwd does not end with '/'")

View file

@ -1,3 +1,5 @@
-- Spawning a child process should work with options
local result = process.spawn("ls", { local result = process.spawn("ls", {
"-a", "-a",
}) })
@ -9,3 +11,62 @@ assert(result.stdout ~= "", "Stdout was empty")
assert(string.find(result.stdout, "Cargo.toml") ~= nil, "Missing Cargo.toml in output") assert(string.find(result.stdout, "Cargo.toml") ~= nil, "Missing Cargo.toml in output")
assert(string.find(result.stdout, ".gitignore") ~= nil, "Missing .gitignore in output") assert(string.find(result.stdout, ".gitignore") ~= nil, "Missing .gitignore in output")
-- It should also work the same when spawned using a shell
local shellResult = process.spawn("ls", {
"-a",
}, {
shell = true,
})
assert(shellResult.ok, "Failed to spawn child process (shell)")
assert(shellResult.stderr == "", "Stderr was not empty (shell)")
assert(shellResult.stdout ~= "", "Stdout was empty (shell)")
assert(string.find(shellResult.stdout, "Cargo.toml") ~= nil, "Missing Cargo.toml in output (shell)")
assert(string.find(shellResult.stdout, ".gitignore") ~= nil, "Missing .gitignore in output (shell)")
-- Make sure the cwd option actually uses the directory we want
local rootPwd = process.spawn("pwd", {}, {
cwd = "/",
}).stdout
rootPwd = string.gsub(rootPwd, "^%s+", "")
rootPwd = string.gsub(rootPwd, "%s+$", "")
if rootPwd ~= "/" then
error(
string.format(
"Current working directory for child process was not set correctly!"
.. "\nExpected '/', got '%s'",
rootPwd
)
)
end
-- Setting cwd should not change the cwd of this process
local before = process.spawn("pwd").stdout
process.spawn("ls", {}, {
cwd = "/",
shell = true,
})
local after = process.spawn("pwd").stdout
assert(before == after, "Current working directory changed after running child process")
-- Inheriting stdio & environment variables should work
task.delay(2, function()
local message = "Hello from child process!"
local result = process.spawn("echo", {
'"$TEST_VAR"',
}, {
env = { TEST_VAR = message },
shell = "bash",
stdio = "inherit",
})
assert(
result.stdout == (message .. "\n"), -- Note that echo adds a newline
"Inheriting stdio did not return proper output"
)
end)