mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 13:30:38 +00:00
Implement new process spawn options & cwd constant
This commit is contained in:
parent
16911e904e
commit
08360a861f
7 changed files with 291 additions and 28 deletions
|
@ -322,7 +322,7 @@ declare process: {
|
|||
options: {
|
||||
cwd: string?,
|
||||
env: { [string]: string }?,
|
||||
shell: boolean | string,
|
||||
shell: (boolean | string)?,
|
||||
stdio: ("inherit" | "default")?,
|
||||
}?
|
||||
) -> {
|
||||
|
|
|
@ -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 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<()> {
|
||||
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
|
||||
let args_tab = TableBuilder::new(lua)?
|
||||
.with_sequential_values(args_vec)?
|
||||
|
@ -26,9 +40,10 @@ pub fn create(lua: &Lua, args_vec: Vec<String>) -> LuaResult<()> {
|
|||
"process",
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("args", args_tab)?
|
||||
.with_value("cwd", cwd_str)?
|
||||
.with_value("env", env_tab)?
|
||||
.with_async_function("exit", process_exit)?
|
||||
.with_async_function("spawn", process_spawn)?
|
||||
.with_function("spawn", process_spawn)?
|
||||
.build_readonly()?,
|
||||
)
|
||||
}
|
||||
|
@ -112,37 +127,141 @@ async fn process_exit(lua: &Lua, exit_code: Option<u8>) -> LuaResult<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn process_spawn(
|
||||
lua: &Lua,
|
||||
(program, args): (String, Option<Vec<String>>),
|
||||
) -> LuaResult<LuaTable> {
|
||||
// Create and spawn our child process
|
||||
let pwd = env::current_dir()?;
|
||||
let mut cmd = Command::new(program);
|
||||
if let Some(args) = args {
|
||||
cmd.args(args);
|
||||
}
|
||||
let output = cmd
|
||||
.current_dir(pwd)
|
||||
fn process_spawn<'a>(
|
||||
lua: &'a Lua,
|
||||
(mut program, args, options): (String, Option<Vec<String>>, Option<LuaTable<'a>>),
|
||||
) -> LuaResult<LuaTable<'a>> {
|
||||
// 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());
|
||||
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())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
.spawn()?;
|
||||
// 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,
|
||||
// we default to 1 if it yielded any error output, otherwise 0
|
||||
let code = output
|
||||
.status
|
||||
.code()
|
||||
.unwrap_or(match output.stderr.is_empty() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
});
|
||||
let code = status.code().unwrap_or(match stderr.is_empty() {
|
||||
true => 0,
|
||||
false => 1,
|
||||
});
|
||||
// Construct and return a readonly lua table with results
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("ok", code == 0)?
|
||||
.with_value("code", code)?
|
||||
.with_value("stdout", lua.create_string(&output.stdout)?)?
|
||||
.with_value("stderr", lua.create_string(&output.stderr)?)?
|
||||
.with_value("stdout", lua.create_string(&stdout)?)?
|
||||
.with_value("stderr", lua.create_string(&stderr)?)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
|
|
@ -219,6 +219,7 @@ mod tests {
|
|||
net_json_decode: "net/json/decode",
|
||||
net_json_encode: "net/json/encode",
|
||||
process_args: "process/args",
|
||||
process_cwd: "process/cwd",
|
||||
process_env: "process/env",
|
||||
process_exit: "process/exit",
|
||||
process_spawn: "process/spawn",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod formatting;
|
||||
pub mod net;
|
||||
pub mod process;
|
||||
pub mod table;
|
||||
pub mod task;
|
||||
|
|
74
src/lib/utils/process.rs
Normal file
74
src/lib/utils/process.rs
Normal 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?))
|
||||
})
|
||||
}
|
7
src/tests/process/cwd.luau
Normal file
7
src/tests/process/cwd.luau
Normal 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 '/'")
|
|
@ -1,3 +1,5 @@
|
|||
-- Spawning a child process should work with options
|
||||
|
||||
local result = process.spawn("ls", {
|
||||
"-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, ".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)
|
||||
|
|
Loading…
Reference in a new issue