mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Implement a non-blocking child process interface (#211)
This commit is contained in:
parent
93fa14d832
commit
309c461e11
22 changed files with 414 additions and 64 deletions
|
@ -129,7 +129,7 @@ end
|
||||||
]]
|
]]
|
||||||
|
|
||||||
print("Sending 4 pings to google 🌏")
|
print("Sending 4 pings to google 🌏")
|
||||||
local result = process.spawn("ping", {
|
local result = process.exec("ping", {
|
||||||
"google.com",
|
"google.com",
|
||||||
"-c 4",
|
"-c 4",
|
||||||
})
|
})
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1629,6 +1629,8 @@ dependencies = [
|
||||||
name = "lune-std-process"
|
name = "lune-std-process"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"bytes",
|
||||||
"directories",
|
"directories",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
|
|
|
@ -20,6 +20,9 @@ directories = "5.0"
|
||||||
pin-project = "1.0"
|
pin-project = "1.0"
|
||||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||||
|
|
||||||
|
bstr = "1.9"
|
||||||
|
bytes = "1.6.0"
|
||||||
|
|
||||||
tokio = { version = "1", default-features = false, features = [
|
tokio = { version = "1", default-features = false, features = [
|
||||||
"io-std",
|
"io-std",
|
||||||
"io-util",
|
"io-util",
|
||||||
|
|
|
@ -1,27 +1,33 @@
|
||||||
#![allow(clippy::cargo_common_metadata)]
|
#![allow(clippy::cargo_common_metadata)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
cell::RefCell,
|
||||||
env::{
|
env::{
|
||||||
self,
|
self,
|
||||||
consts::{ARCH, OS},
|
consts::{ARCH, OS},
|
||||||
},
|
},
|
||||||
path::MAIN_SEPARATOR,
|
path::MAIN_SEPARATOR,
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
|
rc::Rc,
|
||||||
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
|
|
||||||
use lune_utils::TableBuilder;
|
use lune_utils::TableBuilder;
|
||||||
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
||||||
|
use options::ProcessSpawnOptionsStdio;
|
||||||
use os_str_bytes::RawOsString;
|
use os_str_bytes::RawOsString;
|
||||||
use tokio::io::AsyncWriteExt;
|
use stream::{ChildProcessReader, ChildProcessWriter};
|
||||||
|
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
|
||||||
|
|
||||||
mod options;
|
mod options;
|
||||||
|
mod stream;
|
||||||
mod tee_writer;
|
mod tee_writer;
|
||||||
mod wait_for_child;
|
mod wait_for_child;
|
||||||
|
|
||||||
use self::options::ProcessSpawnOptions;
|
use self::options::ProcessSpawnOptions;
|
||||||
use self::wait_for_child::{wait_for_child, WaitForChildResult};
|
use self::wait_for_child::wait_for_child;
|
||||||
|
|
||||||
use lune_utils::path::get_current_dir;
|
use lune_utils::path::get_current_dir;
|
||||||
|
|
||||||
|
@ -73,7 +79,8 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
||||||
.with_value("cwd", cwd_str)?
|
.with_value("cwd", cwd_str)?
|
||||||
.with_value("env", env_tab)?
|
.with_value("env", env_tab)?
|
||||||
.with_value("exit", process_exit)?
|
.with_value("exit", process_exit)?
|
||||||
.with_async_function("spawn", process_spawn)?
|
.with_async_function("exec", process_exec)?
|
||||||
|
.with_function("create", process_create)?
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,11 +148,16 @@ fn process_env_iter<'lua>(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn process_spawn(
|
async fn process_exec(
|
||||||
lua: &Lua,
|
lua: &Lua,
|
||||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||||
) -> LuaResult<LuaTable> {
|
) -> LuaResult<LuaTable> {
|
||||||
let res = lua.spawn(spawn_command(program, args, options)).await?;
|
let res = lua
|
||||||
|
.spawn(async move {
|
||||||
|
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
|
||||||
|
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
NOTE: If an exit code was not given by the child process,
|
NOTE: If an exit code was not given by the child process,
|
||||||
|
@ -168,30 +180,104 @@ async fn process_spawn(
|
||||||
.build_readonly()
|
.build_readonly()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn spawn_command(
|
#[allow(clippy::await_holding_refcell_ref)]
|
||||||
|
fn process_create(
|
||||||
|
lua: &Lua,
|
||||||
|
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||||
|
) -> LuaResult<LuaTable> {
|
||||||
|
// We do not want the user to provide stdio options for process.create,
|
||||||
|
// so we reset the options, regardless of what the user provides us
|
||||||
|
let mut spawn_options = options.clone();
|
||||||
|
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
|
||||||
|
|
||||||
|
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
|
||||||
|
let code_rx_rc = Rc::new(RefCell::new(code_rx));
|
||||||
|
|
||||||
|
let child = spawn_command(program, args, spawn_options)?;
|
||||||
|
|
||||||
|
let child_arc = Arc::new(RwLock::new(child));
|
||||||
|
|
||||||
|
let child_arc_clone = Arc::clone(&child_arc);
|
||||||
|
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
|
||||||
|
|
||||||
|
let stdin = child_lock.stdin.take().unwrap();
|
||||||
|
let stdout = child_lock.stdout.take().unwrap();
|
||||||
|
let stderr = child_lock.stderr.take().unwrap();
|
||||||
|
|
||||||
|
let child_arc_inner = Arc::clone(&child_arc);
|
||||||
|
|
||||||
|
// Spawn a background task to wait for the child to exit and send the exit code
|
||||||
|
let status_handle = tokio::spawn(async move {
|
||||||
|
let res = child_arc_inner.write().await.wait().await;
|
||||||
|
|
||||||
|
if let Ok(output) = res {
|
||||||
|
let code = output.code().unwrap_or_default();
|
||||||
|
|
||||||
|
code_tx
|
||||||
|
.send(code)
|
||||||
|
.expect("ExitCode receiver was unexpectedly dropped");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
TableBuilder::new(lua)?
|
||||||
|
.with_value("stdout", ChildProcessReader(stdout))?
|
||||||
|
.with_value("stderr", ChildProcessReader(stderr))?
|
||||||
|
.with_value("stdin", ChildProcessWriter(stdin))?
|
||||||
|
.with_async_function("kill", move |_, ()| {
|
||||||
|
// First, stop the status task so the RwLock is dropped
|
||||||
|
status_handle.abort();
|
||||||
|
let child_arc_clone = Arc::clone(&child_arc);
|
||||||
|
|
||||||
|
// Then get another RwLock to write to the child process and kill it
|
||||||
|
async move { Ok(child_arc_clone.write().await.kill().await?) }
|
||||||
|
})?
|
||||||
|
.with_async_function("status", move |lua, ()| {
|
||||||
|
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
|
||||||
|
async move {
|
||||||
|
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
|
||||||
|
// the receiver gets suddenly dropped
|
||||||
|
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
|
||||||
|
|
||||||
|
TableBuilder::new(lua)?
|
||||||
|
.with_value("code", code)?
|
||||||
|
.with_value("ok", code == 0)?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
||||||
|
})?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn spawn_command_with_stdin(
|
||||||
program: String,
|
program: String,
|
||||||
args: Option<Vec<String>>,
|
args: Option<Vec<String>>,
|
||||||
mut options: ProcessSpawnOptions,
|
mut options: ProcessSpawnOptions,
|
||||||
) -> LuaResult<WaitForChildResult> {
|
) -> LuaResult<Child> {
|
||||||
let stdout = options.stdio.stdout;
|
|
||||||
let stderr = options.stdio.stderr;
|
|
||||||
let stdin = options.stdio.stdin.take();
|
let stdin = options.stdio.stdin.take();
|
||||||
|
|
||||||
let mut child = options
|
let mut child = spawn_command(program, args, options)?;
|
||||||
.into_command(program, args)
|
|
||||||
.stdin(if stdin.is_some() {
|
|
||||||
Stdio::piped()
|
|
||||||
} else {
|
|
||||||
Stdio::null()
|
|
||||||
})
|
|
||||||
.stdout(stdout.as_stdio())
|
|
||||||
.stderr(stderr.as_stdio())
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
if let Some(stdin) = stdin {
|
if let Some(stdin) = stdin {
|
||||||
let mut child_stdin = child.stdin.take().unwrap();
|
let mut child_stdin = child.stdin.take().unwrap();
|
||||||
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_child(child, stdout, stderr).await
|
Ok(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_command(
|
||||||
|
program: String,
|
||||||
|
args: Option<Vec<String>>,
|
||||||
|
options: ProcessSpawnOptions,
|
||||||
|
) -> LuaResult<Child> {
|
||||||
|
let stdout = options.stdio.stdout;
|
||||||
|
let stderr = options.stdio.stderr;
|
||||||
|
|
||||||
|
let child = options
|
||||||
|
.into_command(program, args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(stdout.as_stdio())
|
||||||
|
.stderr(stderr.as_stdio())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
Ok(child)
|
||||||
}
|
}
|
||||||
|
|
58
crates/lune-std-process/src/stream.rs
Normal file
58
crates/lune-std-process/src/stream.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
use bstr::BString;
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use mlua::prelude::*;
|
||||||
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
|
|
||||||
|
const CHUNK_SIZE: usize = 8;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChildProcessReader<R: AsyncRead>(pub R);
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
|
||||||
|
|
||||||
|
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
|
||||||
|
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
|
||||||
|
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
|
||||||
|
self.0.read_buf(&mut buf).await?;
|
||||||
|
|
||||||
|
Ok(buf.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
|
||||||
|
let mut buf = vec![];
|
||||||
|
self.0.read_to_end(&mut buf).await?;
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
|
||||||
|
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
|
||||||
|
let buf = this.read(chunk_size).await?;
|
||||||
|
|
||||||
|
if buf.is_empty() {
|
||||||
|
return Ok(LuaValue::Nil);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(LuaValue::String(lua.create_string(buf)?))
|
||||||
|
});
|
||||||
|
|
||||||
|
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
|
||||||
|
Ok(lua.create_string(this.read_to_end().await?))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
|
||||||
|
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
|
||||||
|
self.0.write_all(data.as_ref()).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
|
||||||
|
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||||
|
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
|
||||||
|
}
|
||||||
|
}
|
|
@ -138,13 +138,16 @@ create_tests! {
|
||||||
process_cwd: "process/cwd",
|
process_cwd: "process/cwd",
|
||||||
process_env: "process/env",
|
process_env: "process/env",
|
||||||
process_exit: "process/exit",
|
process_exit: "process/exit",
|
||||||
process_spawn_async: "process/spawn/async",
|
process_exec_async: "process/exec/async",
|
||||||
process_spawn_basic: "process/spawn/basic",
|
process_exec_basic: "process/exec/basic",
|
||||||
process_spawn_cwd: "process/spawn/cwd",
|
process_exec_cwd: "process/exec/cwd",
|
||||||
process_spawn_no_panic: "process/spawn/no_panic",
|
process_exec_no_panic: "process/exec/no_panic",
|
||||||
process_spawn_shell: "process/spawn/shell",
|
process_exec_shell: "process/exec/shell",
|
||||||
process_spawn_stdin: "process/spawn/stdin",
|
process_exec_stdin: "process/exec/stdin",
|
||||||
process_spawn_stdio: "process/spawn/stdio",
|
process_exec_stdio: "process/exec/stdio",
|
||||||
|
process_spawn_non_blocking: "process/create/non_blocking",
|
||||||
|
process_spawn_status: "process/create/status",
|
||||||
|
process_spawn_stream: "process/create/stream",
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "std-regex")]
|
#[cfg(feature = "std-regex")]
|
||||||
|
|
|
@ -108,7 +108,7 @@ local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pi
|
||||||
|
|
||||||
local function checkInstalled(program: string, args: { string }?)
|
local function checkInstalled(program: string, args: { string }?)
|
||||||
print("Checking if", program, "is installed")
|
print("Checking if", program, "is installed")
|
||||||
local result = process.spawn(program, args)
|
local result = process.exec(program, args)
|
||||||
if not result.ok then
|
if not result.ok then
|
||||||
stdio.ewrite(string.format("Program '%s' is not installed\n", program))
|
stdio.ewrite(string.format("Program '%s' is not installed\n", program))
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
|
@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" })
|
||||||
-- Run them to generate files
|
-- Run them to generate files
|
||||||
|
|
||||||
local function run(program: string, args: { string }): string
|
local function run(program: string, args: { string }): string
|
||||||
local result = process.spawn(program, args)
|
local result = process.exec(program, args)
|
||||||
if not result.ok then
|
if not result.ok then
|
||||||
stdio.ewrite(string.format("Command '%s' failed\n", program))
|
stdio.ewrite(string.format("Command '%s' failed\n", program))
|
||||||
if #result.stdout > 0 then
|
if #result.stdout > 0 then
|
||||||
|
|
|
@ -31,7 +31,7 @@ if not runLocaleTests then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
|
local dateCmd = process.exec("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
|
||||||
env = {
|
env = {
|
||||||
LC_ALL = "fr_FR.UTF-8 ",
|
LC_ALL = "fr_FR.UTF-8 ",
|
||||||
},
|
},
|
||||||
|
|
21
tests/process/create/kill.luau
Normal file
21
tests/process/create/kill.luau
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Killing a child process should work as expected
|
||||||
|
|
||||||
|
local message = "Hello, world!"
|
||||||
|
local child = process.create("cat")
|
||||||
|
|
||||||
|
child.stdin:write(message)
|
||||||
|
child.kill()
|
||||||
|
|
||||||
|
assert(child.status().code == 9, "Child process should have an exit code of 9 (SIGKILL)")
|
||||||
|
|
||||||
|
assert(
|
||||||
|
child.stdout:readToEnd() == message,
|
||||||
|
"Reading from stdout of child process should work even after kill"
|
||||||
|
)
|
||||||
|
|
||||||
|
local stdinWriteOk = pcall(function()
|
||||||
|
child.stdin:write(message)
|
||||||
|
end)
|
||||||
|
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")
|
13
tests/process/create/non_blocking.luau
Normal file
13
tests/process/create/non_blocking.luau
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Spawning a child process should not block the thread
|
||||||
|
|
||||||
|
local childThread = coroutine.create(process.create)
|
||||||
|
|
||||||
|
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
|
||||||
|
assert(ok, err)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
coroutine.status(childThread) == "dead",
|
||||||
|
"Child process should not block the thread it is running on"
|
||||||
|
)
|
15
tests/process/create/status.luau
Normal file
15
tests/process/create/status.luau
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- The exit code of an child process should be correct
|
||||||
|
|
||||||
|
local randomExitCode = math.random(0, 255)
|
||||||
|
local isOk = randomExitCode == 0
|
||||||
|
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
|
||||||
|
local status = child.status()
|
||||||
|
|
||||||
|
assert(
|
||||||
|
status.code == randomExitCode,
|
||||||
|
`Child process exited with wrong exit code, expected {randomExitCode}`
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`)
|
18
tests/process/create/stream.luau
Normal file
18
tests/process/create/stream.luau
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Should be able to write and read from child process streams
|
||||||
|
|
||||||
|
local msg = "hello, world"
|
||||||
|
|
||||||
|
local catChild = process.create("cat")
|
||||||
|
catChild.stdin:write(msg)
|
||||||
|
assert(
|
||||||
|
msg == catChild.stdout:read(#msg),
|
||||||
|
"Failed to write to stdin or read from stdout of child process"
|
||||||
|
)
|
||||||
|
|
||||||
|
local echoChild = if process.os == "windows"
|
||||||
|
then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" })
|
||||||
|
else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true })
|
||||||
|
|
||||||
|
assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process")
|
|
@ -4,7 +4,7 @@ local task = require("@lune/task")
|
||||||
|
|
||||||
local IS_WINDOWS = process.os == "windows"
|
local IS_WINDOWS = process.os == "windows"
|
||||||
|
|
||||||
-- Spawning a process should not block any lua thread(s)
|
-- Executing a command should not block any lua thread(s)
|
||||||
|
|
||||||
local SLEEP_DURATION = 1 / 4
|
local SLEEP_DURATION = 1 / 4
|
||||||
local SLEEP_SAMPLES = 2
|
local SLEEP_SAMPLES = 2
|
||||||
|
@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do
|
||||||
table.insert(args, 1, "-Milliseconds")
|
table.insert(args, 1, "-Milliseconds")
|
||||||
end
|
end
|
||||||
-- Windows does not have `sleep` as a process, so we use powershell instead.
|
-- Windows does not have `sleep` as a process, so we use powershell instead.
|
||||||
process.spawn("sleep", args, if IS_WINDOWS then { shell = true } else nil)
|
process.exec("sleep", args, if IS_WINDOWS then { shell = true } else nil)
|
||||||
sleepCounter += 1
|
sleepCounter += 1
|
||||||
end)
|
end)
|
||||||
end
|
end
|
|
@ -2,7 +2,7 @@ local process = require("@lune/process")
|
||||||
local stdio = require("@lune/stdio")
|
local stdio = require("@lune/stdio")
|
||||||
local task = require("@lune/task")
|
local task = require("@lune/task")
|
||||||
|
|
||||||
-- Spawning a child process should work, with options
|
-- Executing a command should work, with options
|
||||||
|
|
||||||
local thread = task.delay(1, function()
|
local thread = task.delay(1, function()
|
||||||
stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
|
stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
|
||||||
|
@ -12,7 +12,7 @@ end)
|
||||||
|
|
||||||
local IS_WINDOWS = process.os == "windows"
|
local IS_WINDOWS = process.os == "windows"
|
||||||
|
|
||||||
local result = process.spawn(
|
local result = process.exec(
|
||||||
if IS_WINDOWS then "cmd" else "ls",
|
if IS_WINDOWS then "cmd" else "ls",
|
||||||
if IS_WINDOWS then { "/c", "dir" } else { "-a" }
|
if IS_WINDOWS then { "/c", "dir" } else { "-a" }
|
||||||
)
|
)
|
|
@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd"
|
||||||
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
|
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
|
||||||
|
|
||||||
-- Make sure the cwd option actually uses the directory we want
|
-- Make sure the cwd option actually uses the directory we want
|
||||||
local rootPwd = process.spawn(pwdCommand, pwdArgs, {
|
local rootPwd = process.exec(pwdCommand, pwdArgs, {
|
||||||
cwd = "/",
|
cwd = "/",
|
||||||
}).stdout
|
}).stdout
|
||||||
rootPwd = string.gsub(rootPwd, "^%s+", "")
|
rootPwd = string.gsub(rootPwd, "^%s+", "")
|
||||||
|
@ -27,24 +27,24 @@ end
|
||||||
|
|
||||||
-- Setting cwd should not change the cwd of this process
|
-- Setting cwd should not change the cwd of this process
|
||||||
|
|
||||||
local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout
|
local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout
|
||||||
process.spawn("ls", {}, {
|
process.exec("ls", {}, {
|
||||||
cwd = "/",
|
cwd = "/",
|
||||||
shell = true,
|
shell = true,
|
||||||
})
|
})
|
||||||
local pwdAfter = process.spawn(pwdCommand, pwdArgs).stdout
|
local pwdAfter = process.exec(pwdCommand, pwdArgs).stdout
|
||||||
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
|
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
|
||||||
|
|
||||||
-- Setting the cwd on a child process should properly
|
-- Setting the cwd on a child process should properly
|
||||||
-- replace any leading ~ with the users real home dir
|
-- replace any leading ~ with the users real home dir
|
||||||
|
|
||||||
local homeDir1 = process.spawn("echo $HOME", nil, {
|
local homeDir1 = process.exec("echo $HOME", nil, {
|
||||||
shell = true,
|
shell = true,
|
||||||
}).stdout
|
}).stdout
|
||||||
|
|
||||||
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
|
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
|
||||||
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
|
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
|
||||||
local homeDir2 = process.spawn(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
|
local homeDir2 = process.exec(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
|
||||||
shell = true,
|
shell = true,
|
||||||
cwd = "~",
|
cwd = "~",
|
||||||
}).stdout
|
}).stdout
|
7
tests/process/exec/no_panic.luau
Normal file
7
tests/process/exec/no_panic.luau
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
local process = require("@lune/process")
|
||||||
|
|
||||||
|
-- Executing a non existent command as a child process
|
||||||
|
-- should not panic, but should error
|
||||||
|
|
||||||
|
local success = pcall(process.exec, "someProgramThatDoesNotExist")
|
||||||
|
assert(not success, "Spawned a non-existent program")
|
|
@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows"
|
||||||
-- Default shell should be /bin/sh on unix and powershell on Windows,
|
-- Default shell should be /bin/sh on unix and powershell on Windows,
|
||||||
-- note that powershell needs slightly different command flags for ls
|
-- note that powershell needs slightly different command flags for ls
|
||||||
|
|
||||||
local shellResult = process.spawn("ls", {
|
local shellResult = process.exec("ls", {
|
||||||
if IS_WINDOWS then "-Force" else "-a",
|
if IS_WINDOWS then "-Force" else "-a",
|
||||||
}, {
|
}, {
|
||||||
shell = true,
|
shell = true,
|
|
@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!"
|
||||||
-- When passing stdin to powershell on windows we must "accept" using the double newline
|
-- When passing stdin to powershell on windows we must "accept" using the double newline
|
||||||
|
|
||||||
local result = if IS_WINDOWS
|
local result = if IS_WINDOWS
|
||||||
then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
|
then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
|
||||||
else process.spawn("xargs", { "echo" }, { stdin = echoMessage })
|
else process.exec("xargs", { "echo" }, { stdin = echoMessage })
|
||||||
|
|
||||||
local resultStdout = if IS_WINDOWS
|
local resultStdout = if IS_WINDOWS
|
||||||
then string.sub(result.stdout, #result.stdout - #echoMessage - 1)
|
then string.sub(result.stdout, #result.stdout - #echoMessage - 1)
|
|
@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows"
|
||||||
-- Inheriting stdio & environment variables should work
|
-- Inheriting stdio & environment variables should work
|
||||||
|
|
||||||
local echoMessage = "Hello from child process!"
|
local echoMessage = "Hello from child process!"
|
||||||
local echoResult = process.spawn("echo", {
|
local echoResult = process.exec("echo", {
|
||||||
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
|
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
|
||||||
}, {
|
}, {
|
||||||
env = { TEST_VAR = echoMessage },
|
env = { TEST_VAR = echoMessage },
|
||||||
shell = if IS_WINDOWS then "powershell" else "bash",
|
shell = if IS_WINDOWS then "powershell" else "bash",
|
||||||
stdio = "inherit",
|
stdio = "inherit" :: process.SpawnOptionsStdioKind, -- FIXME: This should just work without a cast?
|
||||||
})
|
})
|
||||||
|
|
||||||
-- Windows uses \r\n (CRLF) and unix uses \n (LF)
|
-- Windows uses \r\n (CRLF) and unix uses \n (LF)
|
|
@ -1,7 +0,0 @@
|
||||||
local process = require("@lune/process")
|
|
||||||
|
|
||||||
-- Spawning a child process for a non-existent
|
|
||||||
-- program should not panic, but should error
|
|
||||||
|
|
||||||
local success = pcall(process.spawn, "someProgramThatDoesNotExist")
|
|
||||||
assert(not success, "Spawned a non-existent program")
|
|
|
@ -109,7 +109,7 @@ assertContains(
|
||||||
|
|
||||||
local _, errorMessage = pcall(function()
|
local _, errorMessage = pcall(function()
|
||||||
local function innerInnerFn()
|
local function innerInnerFn()
|
||||||
process.spawn("PROGRAM_THAT_DOES_NOT_EXIST")
|
process.exec("PROGRAM_THAT_DOES_NOT_EXIST")
|
||||||
end
|
end
|
||||||
local function innerFn()
|
local function innerFn()
|
||||||
innerInnerFn()
|
innerInnerFn()
|
||||||
|
|
|
@ -5,6 +5,9 @@ export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
|
||||||
export type SpawnOptionsStdio = {
|
export type SpawnOptionsStdio = {
|
||||||
stdout: SpawnOptionsStdioKind?,
|
stdout: SpawnOptionsStdioKind?,
|
||||||
stderr: SpawnOptionsStdioKind?,
|
stderr: SpawnOptionsStdioKind?,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
|
||||||
stdin: string?,
|
stdin: string?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,27 +15,117 @@ export type SpawnOptionsStdio = {
|
||||||
@interface SpawnOptions
|
@interface SpawnOptions
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
A dictionary of options for `process.spawn`, with the following available values:
|
A dictionary of options for `process.create`, with the following available values:
|
||||||
|
|
||||||
* `cwd` - The current working directory for the process
|
* `cwd` - The current working directory for the process
|
||||||
* `env` - Extra environment variables to give to the process
|
* `env` - Extra environment variables to give to the process
|
||||||
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
||||||
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
|
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
|
||||||
* `stdin` - Optional standard input to pass to spawned child process
|
|
||||||
]=]
|
]=]
|
||||||
export type SpawnOptions = {
|
export type SpawnOptions = {
|
||||||
cwd: string?,
|
cwd: string?,
|
||||||
env: { [string]: string }?,
|
env: { [string]: string }?,
|
||||||
shell: (boolean | string)?,
|
shell: (boolean | string)?,
|
||||||
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@interface ExecuteOptions
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
A dictionary of options for `process.exec`, with the following available values:
|
||||||
|
|
||||||
|
* `cwd` - The current working directory for the process
|
||||||
|
* `env` - Extra environment variables to give to the process
|
||||||
|
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
|
||||||
|
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `ExecuteOptionsStdio` for more info
|
||||||
|
* `stdin` - Optional standard input to pass to executed child process
|
||||||
|
]=]
|
||||||
|
export type ExecuteOptions = SpawnOptions & {
|
||||||
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
|
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
|
||||||
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface SpawnResult
|
@class ChildProcessReader
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Result type for child processes in `process.spawn`.
|
A reader class to read data from a child process' streams in realtime.
|
||||||
|
]=]
|
||||||
|
local ChildProcessReader = {}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessReader
|
||||||
|
|
||||||
|
Reads a chunk of data (specified length or a default of 8 bytes at a time) from
|
||||||
|
the reader as a string. Returns nil if there is no more data to read.
|
||||||
|
|
||||||
|
This function may yield until there is new data to read from reader, if all data
|
||||||
|
till present has already been read, and the process has not exited.
|
||||||
|
|
||||||
|
@return The string containing the data read from the reader
|
||||||
|
]=]
|
||||||
|
function ChildProcessReader:read(chunkSize: number?): string?
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessReader
|
||||||
|
|
||||||
|
Reads all the data currently present in the reader as a string.
|
||||||
|
This function will yield until the process exits.
|
||||||
|
|
||||||
|
@return The string containing the data read from the reader
|
||||||
|
]=]
|
||||||
|
function ChildProcessReader:readToEnd(): string
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@class ChildProcessWriter
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
A writer class to write data to a child process' streams in realtime.
|
||||||
|
]=]
|
||||||
|
local ChildProcessWriter = {}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessWriter
|
||||||
|
|
||||||
|
Writes a buffer or string of data to the writer.
|
||||||
|
|
||||||
|
@param data The data to write to the writer
|
||||||
|
]=]
|
||||||
|
function ChildProcessWriter:write(data: buffer | string): ()
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@interface ChildProcess
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
Result type for child processes in `process.create`.
|
||||||
|
|
||||||
|
This is a dictionary containing the following values:
|
||||||
|
|
||||||
|
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
|
||||||
|
* `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info
|
||||||
|
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
|
||||||
|
* `kill` - A function that kills the child process
|
||||||
|
* `status` - A function that yields and returns the exit status of the child process
|
||||||
|
]=]
|
||||||
|
export type ChildProcess = {
|
||||||
|
stdin: typeof(ChildProcessWriter),
|
||||||
|
stdout: typeof(ChildProcessReader),
|
||||||
|
stderr: typeof(ChildProcessReader),
|
||||||
|
kill: () -> ();
|
||||||
|
status: () -> { ok: boolean, code: number }
|
||||||
|
}
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@interface ExecuteResult
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
Result type for child processes in `process.exec`.
|
||||||
|
|
||||||
This is a dictionary containing the following values:
|
This is a dictionary containing the following values:
|
||||||
|
|
||||||
|
@ -41,7 +134,7 @@ export type SpawnOptions = {
|
||||||
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
|
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
|
||||||
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
|
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
|
||||||
]=]
|
]=]
|
||||||
export type SpawnResult = {
|
export type ExecuteResult = {
|
||||||
ok: boolean,
|
ok: boolean,
|
||||||
code: number,
|
code: number,
|
||||||
stdout: string,
|
stdout: string,
|
||||||
|
@ -73,8 +166,8 @@ export type SpawnResult = {
|
||||||
-- Getting the current os and processor architecture
|
-- Getting the current os and processor architecture
|
||||||
print("Running " .. process.os .. " on " .. process.arch .. "!")
|
print("Running " .. process.os .. " on " .. process.arch .. "!")
|
||||||
|
|
||||||
-- Spawning a child process
|
-- Executing a command
|
||||||
local result = process.spawn("program", {
|
local result = process.exec("program", {
|
||||||
"cli argument",
|
"cli argument",
|
||||||
"other cli argument"
|
"other cli argument"
|
||||||
})
|
})
|
||||||
|
@ -83,6 +176,19 @@ export type SpawnResult = {
|
||||||
else
|
else
|
||||||
print(result.stderr)
|
print(result.stderr)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Spawning a child process
|
||||||
|
local child = process.create("program", {
|
||||||
|
"cli argument",
|
||||||
|
"other cli argument"
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Writing to the child process' stdin
|
||||||
|
child.stdin:write("Hello from Lune!")
|
||||||
|
|
||||||
|
-- Reading from the child process' stdout
|
||||||
|
local data = child.stdout:read()
|
||||||
|
print(buffer.tostring(data))
|
||||||
```
|
```
|
||||||
]=]
|
]=]
|
||||||
local process = {}
|
local process = {}
|
||||||
|
@ -163,19 +269,44 @@ end
|
||||||
--[=[
|
--[=[
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process.
|
Spawns a child process in the background that runs the program `program`, and immediately returns
|
||||||
|
readers and writers to communicate with it.
|
||||||
|
|
||||||
|
In order to execute a command and wait for its output, see `process.exec`.
|
||||||
|
|
||||||
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
||||||
|
|
||||||
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
||||||
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
|
Refer to the documentation for `SpawnOptions` for specific option keys and their values.
|
||||||
|
|
||||||
@param program The program to spawn as a child process
|
@param program The program to Execute as a child process
|
||||||
|
@param params Additional parameters to pass to the program
|
||||||
|
@param options A dictionary of options for the child process
|
||||||
|
@return A dictionary with the readers and writers to communicate with the child process
|
||||||
|
]=]
|
||||||
|
function process.create(program: string, params: { string }?, options: SpawnOptions?): ChildProcess
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within Process
|
||||||
|
|
||||||
|
Executes a child process that will execute the command `program`, waiting for it to exit.
|
||||||
|
Upon exit, it returns a dictionary that describes the final status and ouput of the child process.
|
||||||
|
|
||||||
|
In order to spawn a child process in the background, see `process.create`.
|
||||||
|
|
||||||
|
The second argument, `params`, can be passed as a list of string parameters to give to the program.
|
||||||
|
|
||||||
|
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
|
||||||
|
Refer to the documentation for `ExecuteOptions` for specific option keys and their values.
|
||||||
|
|
||||||
|
@param program The program to Execute as a child process
|
||||||
@param params Additional parameters to pass to the program
|
@param params Additional parameters to pass to the program
|
||||||
@param options A dictionary of options for the child process
|
@param options A dictionary of options for the child process
|
||||||
@return A dictionary representing the result of the child process
|
@return A dictionary representing the result of the child process
|
||||||
]=]
|
]=]
|
||||||
function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult
|
function process.exec(program: string, params: { string }?, options: ExecuteOptions?): ExecuteResult
|
||||||
return nil :: any
|
return nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue