mirror of
https://github.com/lune-org/lune.git
synced 2025-05-04 10:43:57 +01:00
Complete migration of lune-std-process to use async-process instead of tokio
This commit is contained in:
parent
8059026251
commit
c4374a0e18
16 changed files with 466 additions and 203 deletions
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -1709,13 +1709,15 @@ dependencies = [
|
||||||
name = "lune-std-process"
|
name = "lune-std-process"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io",
|
"async-channel",
|
||||||
|
"async-lock",
|
||||||
"async-process",
|
"async-process",
|
||||||
"blocking",
|
"blocking",
|
||||||
"bstr",
|
"bstr",
|
||||||
"bytes",
|
"bytes",
|
||||||
"directories",
|
"directories",
|
||||||
"futures-lite",
|
"futures-lite",
|
||||||
|
"futures-util",
|
||||||
"lune-utils",
|
"lune-utils",
|
||||||
"mlua",
|
"mlua",
|
||||||
"mlua-luau-scheduler",
|
"mlua-luau-scheduler",
|
||||||
|
|
|
@ -23,9 +23,11 @@ os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||||
bstr = "1.9"
|
bstr = "1.9"
|
||||||
bytes = "1.6.0"
|
bytes = "1.6.0"
|
||||||
|
|
||||||
async-io = "2.4"
|
async-channel = "2.3"
|
||||||
|
async-lock = "3.4"
|
||||||
async-process = "2.3"
|
async-process = "2.3"
|
||||||
blocking = "1.6"
|
blocking = "1.6"
|
||||||
futures-lite = "2.6"
|
futures-lite = "2.6"
|
||||||
|
futures-util = "0.3" # Needed for select! macro...
|
||||||
|
|
||||||
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
lune-utils = { version = "0.1.3", path = "../lune-utils" }
|
||||||
|
|
86
crates/lune-std-process/src/create/child.rs
Normal file
86
crates/lune-std-process/src/create/child.rs
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
use std::process::ExitStatus;
|
||||||
|
|
||||||
|
use async_channel::{unbounded, Receiver, Sender};
|
||||||
|
use async_process::Child as AsyncChild;
|
||||||
|
use futures_util::{select, FutureExt};
|
||||||
|
|
||||||
|
use mlua::prelude::*;
|
||||||
|
use mlua_luau_scheduler::LuaSpawnExt;
|
||||||
|
|
||||||
|
use lune_utils::TableBuilder;
|
||||||
|
|
||||||
|
use super::{ChildReader, ChildWriter};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Child {
|
||||||
|
stdin: ChildWriter,
|
||||||
|
stdout: ChildReader,
|
||||||
|
stderr: ChildReader,
|
||||||
|
kill_tx: Sender<()>,
|
||||||
|
status_rx: Receiver<Option<ExitStatus>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Child {
|
||||||
|
pub fn new(lua: &Lua, mut child: AsyncChild) -> Self {
|
||||||
|
let stdin = ChildWriter::from(child.stdin.take());
|
||||||
|
let stdout = ChildReader::from(child.stdout.take());
|
||||||
|
let stderr = ChildReader::from(child.stderr.take());
|
||||||
|
|
||||||
|
// NOTE: Kill channel is zero size, status is very small
|
||||||
|
// and implements Copy, unbounded will be just fine here
|
||||||
|
let (kill_tx, kill_rx) = unbounded();
|
||||||
|
let (status_tx, status_rx) = unbounded();
|
||||||
|
lua.spawn(handle_child(child, kill_rx, status_tx)).detach();
|
||||||
|
|
||||||
|
Self {
|
||||||
|
stdin,
|
||||||
|
stdout,
|
||||||
|
stderr,
|
||||||
|
kill_tx,
|
||||||
|
status_rx,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LuaUserData for Child {
|
||||||
|
fn add_fields<F: LuaUserDataFields<Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("stdin", |_, this| Ok(this.stdin.clone()));
|
||||||
|
fields.add_field_method_get("stdout", |_, this| Ok(this.stdout.clone()));
|
||||||
|
fields.add_field_method_get("stderr", |_, this| Ok(this.stderr.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
|
methods.add_method("kill", |_, this, (): ()| {
|
||||||
|
let _ = this.kill_tx.try_send(());
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
methods.add_async_method("status", |lua, this, (): ()| {
|
||||||
|
let rx = this.status_rx.clone();
|
||||||
|
async move {
|
||||||
|
let status = rx.recv().await.ok().flatten();
|
||||||
|
let code = status.and_then(|c| c.code()).unwrap_or(9);
|
||||||
|
TableBuilder::new(lua.clone())?
|
||||||
|
.with_value("ok", code == 0)?
|
||||||
|
.with_value("code", code)?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_child(
|
||||||
|
mut child: AsyncChild,
|
||||||
|
kill_rx: Receiver<()>,
|
||||||
|
status_tx: Sender<Option<ExitStatus>>,
|
||||||
|
) {
|
||||||
|
let status = select! {
|
||||||
|
s = child.status().fuse() => s.ok(), // FUTURE: Propagate this error somehow?
|
||||||
|
_ = kill_rx.recv().fuse() => {
|
||||||
|
let _ = child.kill(); // Will only error if already killed
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Will only error if there are no receivers waiting for the status
|
||||||
|
let _ = status_tx.send(status).await;
|
||||||
|
}
|
117
crates/lune-std-process/src/create/child_reader.rs
Normal file
117
crates/lune-std-process/src/create/child_reader.rs
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_lock::Mutex as AsyncMutex;
|
||||||
|
use async_process::{ChildStderr as AsyncChildStderr, ChildStdout as AsyncChildStdout};
|
||||||
|
use futures_lite::prelude::*;
|
||||||
|
|
||||||
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
const DEFAULT_BUFFER_SIZE: usize = 1024;
|
||||||
|
|
||||||
|
// Inner (plumbing) implementation
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ChildReaderInner {
|
||||||
|
None,
|
||||||
|
Stdout(AsyncChildStdout),
|
||||||
|
Stderr(AsyncChildStderr),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChildReaderInner {
|
||||||
|
async fn read(&mut self, size: usize) -> Result<Vec<u8>, std::io::Error> {
|
||||||
|
if matches!(self, ChildReaderInner::None) {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut buf = vec![0; size];
|
||||||
|
|
||||||
|
let read = match self {
|
||||||
|
ChildReaderInner::None => unreachable!(),
|
||||||
|
ChildReaderInner::Stdout(stdout) => stdout.read(&mut buf).await?,
|
||||||
|
ChildReaderInner::Stderr(stderr) => stderr.read(&mut buf).await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
buf.truncate(read);
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn read_to_end(&mut self) -> Result<Vec<u8>, std::io::Error> {
|
||||||
|
let mut buf = Vec::new();
|
||||||
|
|
||||||
|
let read = match self {
|
||||||
|
ChildReaderInner::None => 0,
|
||||||
|
ChildReaderInner::Stdout(stdout) => stdout.read_to_end(&mut buf).await?,
|
||||||
|
ChildReaderInner::Stderr(stderr) => stderr.read_to_end(&mut buf).await?,
|
||||||
|
};
|
||||||
|
|
||||||
|
buf.truncate(read);
|
||||||
|
|
||||||
|
Ok(buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AsyncChildStdout> for ChildReaderInner {
|
||||||
|
fn from(stdout: AsyncChildStdout) -> Self {
|
||||||
|
Self::Stdout(stdout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AsyncChildStderr> for ChildReaderInner {
|
||||||
|
fn from(stderr: AsyncChildStderr) -> Self {
|
||||||
|
Self::Stderr(stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<AsyncChildStdout>> for ChildReaderInner {
|
||||||
|
fn from(stdout: Option<AsyncChildStdout>) -> Self {
|
||||||
|
stdout.map_or(Self::None, Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<AsyncChildStderr>> for ChildReaderInner {
|
||||||
|
fn from(stderr: Option<AsyncChildStderr>) -> Self {
|
||||||
|
stderr.map_or(Self::None, Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer (lua-accessible, clonable) implementation
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChildReader {
|
||||||
|
inner: Arc<AsyncMutex<ChildReaderInner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LuaUserData for ChildReader {
|
||||||
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
|
methods.add_async_method("read", |lua, this, size: Option<usize>| {
|
||||||
|
let inner = this.inner.clone();
|
||||||
|
let size = size.unwrap_or(DEFAULT_BUFFER_SIZE);
|
||||||
|
async move {
|
||||||
|
let mut inner = inner.lock().await;
|
||||||
|
let bytes = inner.read(size).await.into_lua_err()?;
|
||||||
|
if bytes.is_empty() {
|
||||||
|
Ok(LuaValue::Nil)
|
||||||
|
} else {
|
||||||
|
Ok(LuaValue::String(lua.create_string(bytes)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
methods.add_async_method("readToEnd", |lua, this, (): ()| {
|
||||||
|
let inner = this.inner.clone();
|
||||||
|
async move {
|
||||||
|
let mut inner = inner.lock().await;
|
||||||
|
let bytes = inner.read_to_end().await.into_lua_err()?;
|
||||||
|
Ok(lua.create_string(bytes))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<ChildReaderInner>> From<T> for ChildReader {
|
||||||
|
fn from(inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(AsyncMutex::new(inner.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
79
crates/lune-std-process/src/create/child_writer.rs
Normal file
79
crates/lune-std-process/src/create/child_writer.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_lock::Mutex as AsyncMutex;
|
||||||
|
use async_process::ChildStdin as AsyncChildStdin;
|
||||||
|
use futures_lite::prelude::*;
|
||||||
|
|
||||||
|
use bstr::BString;
|
||||||
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
// Inner (plumbing) implementation
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum ChildWriterInner {
|
||||||
|
None,
|
||||||
|
Stdin(AsyncChildStdin),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChildWriterInner {
|
||||||
|
async fn write(&mut self, data: Vec<u8>) -> Result<(), std::io::Error> {
|
||||||
|
match self {
|
||||||
|
ChildWriterInner::None => Ok(()),
|
||||||
|
ChildWriterInner::Stdin(stdin) => stdin.write_all(&data).await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(&mut self) -> Result<(), std::io::Error> {
|
||||||
|
match self {
|
||||||
|
ChildWriterInner::None => Ok(()),
|
||||||
|
ChildWriterInner::Stdin(stdin) => stdin.close().await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<AsyncChildStdin> for ChildWriterInner {
|
||||||
|
fn from(stdin: AsyncChildStdin) -> Self {
|
||||||
|
ChildWriterInner::Stdin(stdin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<AsyncChildStdin>> for ChildWriterInner {
|
||||||
|
fn from(stdin: Option<AsyncChildStdin>) -> Self {
|
||||||
|
stdin.map_or(Self::None, Into::into)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer (lua-accessible, clonable) implementation
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ChildWriter {
|
||||||
|
inner: Arc<AsyncMutex<ChildWriterInner>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LuaUserData for ChildWriter {
|
||||||
|
fn add_methods<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
||||||
|
methods.add_async_method("write", |_, this, data: BString| {
|
||||||
|
let inner = this.inner.clone();
|
||||||
|
let data = data.to_vec();
|
||||||
|
async move {
|
||||||
|
let mut inner = inner.lock().await;
|
||||||
|
inner.write(data).await.into_lua_err()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
methods.add_async_method("close", |_, this, (): ()| {
|
||||||
|
let inner = this.inner.clone();
|
||||||
|
async move {
|
||||||
|
let mut inner = inner.lock().await;
|
||||||
|
inner.close().await.into_lua_err()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Into<ChildWriterInner>> From<T> for ChildWriter {
|
||||||
|
fn from(inner: T) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(AsyncMutex::new(inner.into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
crates/lune-std-process/src/create/mod.rs
Normal file
7
crates/lune-std-process/src/create/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
mod child;
|
||||||
|
mod child_reader;
|
||||||
|
mod child_writer;
|
||||||
|
|
||||||
|
pub use self::child::Child;
|
||||||
|
pub use self::child_reader::ChildReader;
|
||||||
|
pub use self::child_writer::ChildWriter;
|
51
crates/lune-std-process/src/exec/mod.rs
Normal file
51
crates/lune-std-process/src/exec/mod.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use async_process::Child;
|
||||||
|
use futures_lite::prelude::*;
|
||||||
|
|
||||||
|
use mlua::prelude::*;
|
||||||
|
|
||||||
|
use lune_utils::TableBuilder;
|
||||||
|
|
||||||
|
use super::options::ProcessSpawnOptionsStdioKind;
|
||||||
|
|
||||||
|
mod tee_writer;
|
||||||
|
mod wait_for_child;
|
||||||
|
|
||||||
|
use self::wait_for_child::wait_for_child;
|
||||||
|
|
||||||
|
pub async fn exec(
|
||||||
|
lua: Lua,
|
||||||
|
mut child: Child,
|
||||||
|
stdin: Option<Vec<u8>>,
|
||||||
|
stdout: ProcessSpawnOptionsStdioKind,
|
||||||
|
stderr: ProcessSpawnOptionsStdioKind,
|
||||||
|
) -> LuaResult<LuaTable> {
|
||||||
|
// Write to stdin before anything else - if we got it
|
||||||
|
if let Some(stdin) = stdin {
|
||||||
|
let mut child_stdin = child.stdin.take().unwrap();
|
||||||
|
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = wait_for_child(child, stdout, stderr).await?;
|
||||||
|
|
||||||
|
/*
|
||||||
|
NOTE: If an exit code was not given by the child process,
|
||||||
|
we default to 1 if it yielded any error output, otherwise 0
|
||||||
|
|
||||||
|
An exit code may be missing if the process was terminated by
|
||||||
|
some external signal, which is the only time we use this default
|
||||||
|
*/
|
||||||
|
let code = res
|
||||||
|
.status
|
||||||
|
.code()
|
||||||
|
.unwrap_or(i32::from(!res.stderr.is_empty()));
|
||||||
|
|
||||||
|
// Construct and return a readonly lua table with results
|
||||||
|
let stdout = lua.create_string(&res.stdout)?;
|
||||||
|
let stderr = lua.create_string(&res.stderr)?;
|
||||||
|
TableBuilder::new(lua)?
|
||||||
|
.with_value("ok", code == 0)?
|
||||||
|
.with_value("code", code)?
|
||||||
|
.with_value("stdout", stdout)?
|
||||||
|
.with_value("stderr", stderr)?
|
||||||
|
.build_readonly()
|
||||||
|
}
|
|
@ -6,7 +6,8 @@ use async_process::Child;
|
||||||
use blocking::Unblock;
|
use blocking::Unblock;
|
||||||
use futures_lite::{io, prelude::*};
|
use futures_lite::{io, prelude::*};
|
||||||
|
|
||||||
use super::{options::ProcessSpawnOptionsStdioKind, tee_writer::AsyncTeeWriter};
|
use super::tee_writer::AsyncTeeWriter;
|
||||||
|
use crate::options::ProcessSpawnOptionsStdioKind;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(super) struct WaitForChildResult {
|
pub(super) struct WaitForChildResult {
|
|
@ -10,21 +10,17 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use mlua::prelude::*;
|
use mlua::prelude::*;
|
||||||
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
use mlua_luau_scheduler::Functions;
|
||||||
|
|
||||||
use async_process::Child;
|
|
||||||
use futures_lite::prelude::*;
|
|
||||||
use os_str_bytes::RawOsString;
|
use os_str_bytes::RawOsString;
|
||||||
|
|
||||||
use lune_utils::{path::get_current_dir, TableBuilder};
|
use lune_utils::{path::get_current_dir, TableBuilder};
|
||||||
|
|
||||||
|
mod create;
|
||||||
|
mod exec;
|
||||||
mod options;
|
mod options;
|
||||||
mod stream;
|
|
||||||
mod tee_writer;
|
|
||||||
mod wait_for_child;
|
|
||||||
|
|
||||||
use self::options::ProcessSpawnOptions;
|
use self::options::ProcessSpawnOptions;
|
||||||
use self::wait_for_child::wait_for_child;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Creates the `process` standard library module.
|
Creates the `process` standard library module.
|
||||||
|
@ -42,6 +38,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||||
if !cwd_str.ends_with(MAIN_SEPARATOR) {
|
if !cwd_str.ends_with(MAIN_SEPARATOR) {
|
||||||
cwd_str.push(MAIN_SEPARATOR);
|
cwd_str.push(MAIN_SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create constants for OS & processor architecture
|
// Create constants for OS & processor architecture
|
||||||
let os = lua.create_string(OS.to_lowercase())?;
|
let os = lua.create_string(OS.to_lowercase())?;
|
||||||
let arch = lua.create_string(ARCH.to_lowercase())?;
|
let arch = lua.create_string(ARCH.to_lowercase())?;
|
||||||
|
@ -50,6 +47,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||||
} else {
|
} else {
|
||||||
"little"
|
"little"
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Create readonly args array
|
// Create readonly args array
|
||||||
let args_vec = lua
|
let args_vec = lua
|
||||||
.app_data_ref::<Vec<String>>()
|
.app_data_ref::<Vec<String>>()
|
||||||
|
@ -58,6 +56,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||||
let args_tab = TableBuilder::new(lua.clone())?
|
let args_tab = TableBuilder::new(lua.clone())?
|
||||||
.with_sequential_values(args_vec)?
|
.with_sequential_values(args_vec)?
|
||||||
.build_readonly()?;
|
.build_readonly()?;
|
||||||
|
|
||||||
// Create proxied table for env that gets & sets real env vars
|
// Create proxied table for env that gets & sets real env vars
|
||||||
let env_tab = TableBuilder::new(lua.clone())?
|
let env_tab = TableBuilder::new(lua.clone())?
|
||||||
.with_metatable(
|
.with_metatable(
|
||||||
|
@ -68,9 +67,11 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
||||||
.build_readonly()?,
|
.build_readonly()?,
|
||||||
)?
|
)?
|
||||||
.build_readonly()?;
|
.build_readonly()?;
|
||||||
|
|
||||||
// Create our process exit function, the scheduler crate provides this
|
// Create our process exit function, the scheduler crate provides this
|
||||||
let fns = Functions::new(lua.clone())?;
|
let fns = Functions::new(lua.clone())?;
|
||||||
let process_exit = fns.exit;
|
let process_exit = fns.exit;
|
||||||
|
|
||||||
// Create the full process table
|
// Create the full process table
|
||||||
TableBuilder::new(lua)?
|
TableBuilder::new(lua)?
|
||||||
.with_value("os", os)?
|
.with_value("os", os)?
|
||||||
|
@ -142,66 +143,9 @@ fn process_env_iter(lua: &Lua, (_, ()): (LuaValue, ())) -> LuaResult<LuaFunction
|
||||||
|
|
||||||
async fn process_exec(
|
async fn process_exec(
|
||||||
lua: Lua,
|
lua: Lua,
|
||||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
(program, args, mut options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||||
) -> LuaResult<LuaTable> {
|
) -> LuaResult<LuaTable> {
|
||||||
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,
|
|
||||||
we default to 1 if it yielded any error output, otherwise 0
|
|
||||||
|
|
||||||
An exit code may be missing if the process was terminated by
|
|
||||||
some external signal, which is the only time we use this default
|
|
||||||
*/
|
|
||||||
let code = res
|
|
||||||
.status
|
|
||||||
.code()
|
|
||||||
.unwrap_or(i32::from(!res.stderr.is_empty()));
|
|
||||||
|
|
||||||
// Construct and return a readonly lua table with results
|
|
||||||
TableBuilder::new(lua.clone())?
|
|
||||||
.with_value("ok", code == 0)?
|
|
||||||
.with_value("code", code)?
|
|
||||||
.with_value("stdout", lua.create_string(&res.stdout)?)?
|
|
||||||
.with_value("stderr", lua.create_string(&res.stderr)?)?
|
|
||||||
.build_readonly()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::await_holding_refcell_ref)]
|
|
||||||
fn process_create(
|
|
||||||
_lua: &Lua,
|
|
||||||
(_program, _args, _options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
|
||||||
) -> LuaResult<LuaTable> {
|
|
||||||
Err(LuaError::runtime("unimplemented"))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn spawn_command_with_stdin(
|
|
||||||
program: String,
|
|
||||||
args: Option<Vec<String>>,
|
|
||||||
mut options: ProcessSpawnOptions,
|
|
||||||
) -> LuaResult<Child> {
|
|
||||||
let stdin = options.stdio.stdin.take();
|
let stdin = options.stdio.stdin.take();
|
||||||
|
|
||||||
let mut child = spawn_command(program, args, options)?;
|
|
||||||
|
|
||||||
if let Some(stdin) = stdin {
|
|
||||||
let mut child_stdin = child.stdin.take().unwrap();
|
|
||||||
child_stdin.write_all(&stdin).await.into_lua_err()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(child)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn spawn_command(
|
|
||||||
program: String,
|
|
||||||
args: Option<Vec<String>>,
|
|
||||||
options: ProcessSpawnOptions,
|
|
||||||
) -> LuaResult<Child> {
|
|
||||||
let stdout = options.stdio.stdout;
|
let stdout = options.stdio.stdout;
|
||||||
let stderr = options.stdio.stderr;
|
let stderr = options.stdio.stderr;
|
||||||
|
|
||||||
|
@ -212,5 +156,19 @@ fn spawn_command(
|
||||||
.stderr(stderr.as_stdio())
|
.stderr(stderr.as_stdio())
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
Ok(child)
|
exec::exec(lua, child, stdin, stdout, stderr).await
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_create(
|
||||||
|
lua: &Lua,
|
||||||
|
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||||
|
) -> LuaResult<LuaValue> {
|
||||||
|
let child = options
|
||||||
|
.into_command(program, args)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
create::Child::new(lua, child).into_lua(lua)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
use bstr::BString;
|
|
||||||
use futures_lite::prelude::*;
|
|
||||||
use mlua::prelude::*;
|
|
||||||
|
|
||||||
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 = vec![0u8; chunk_size.unwrap_or(CHUNK_SIZE)];
|
|
||||||
|
|
||||||
let read = self.0.read(&mut buf).await?;
|
|
||||||
buf.truncate(read);
|
|
||||||
|
|
||||||
Ok(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
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<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
|
||||||
methods.add_async_method_mut(
|
|
||||||
"read",
|
|
||||||
|lua, mut 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, mut this, ()| async move {
|
|
||||||
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<M: LuaUserDataMethods<Self>>(methods: &mut M) {
|
|
||||||
methods.add_async_method_mut("write", |_, mut this, data| async move {
|
|
||||||
this.write(data).await
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,21 +1,17 @@
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
-- Killing a child process should work as expected
|
local expected = "Hello, world!"
|
||||||
|
|
||||||
local message = "Hello, world!"
|
local catChild = process.create("cat")
|
||||||
local child = process.create("cat")
|
catChild.stdin:write(expected)
|
||||||
|
catChild:kill()
|
||||||
|
local catStatus = catChild:status()
|
||||||
|
local catStdout = catChild.stdout:readToEnd()
|
||||||
|
|
||||||
child.stdin:write(message)
|
assert(catStatus.code == 9, "Child process should have an exit code of 9 (SIGKILL)")
|
||||||
child.kill()
|
assert(catStdout == expected, "Reading from stdout of child process should work even after 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()
|
local stdinWriteOk = pcall(function()
|
||||||
child.stdin:write(message)
|
catChild.stdin:write(expected)
|
||||||
end)
|
end)
|
||||||
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")
|
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
-- Spawning a child process should not block the thread
|
|
||||||
|
|
||||||
local childThread = coroutine.create(process.create)
|
local childThread = coroutine.create(process.create)
|
||||||
|
|
||||||
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
|
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
|
||||||
assert(ok, err)
|
assert(ok, err)
|
||||||
|
|
||||||
assert(
|
assert(coroutine.status(childThread) == "dead", "Child process should not yield the thread it is created on")
|
||||||
coroutine.status(childThread) == "dead",
|
|
||||||
"Child process should not block the thread it is running on"
|
|
||||||
)
|
|
||||||
|
|
|
@ -1,15 +1,25 @@
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
-- The exit code of an child process should be correct
|
local testCode = math.random(0, 255)
|
||||||
|
local testOk = testCode == 0
|
||||||
|
|
||||||
local randomExitCode = math.random(0, 255)
|
local exitChild = process.create("exit", { tostring(testCode) }, { shell = true })
|
||||||
local isOk = randomExitCode == 0
|
local exitStatus = exitChild:status()
|
||||||
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
|
|
||||||
local status = child.status()
|
assert(type(exitStatus) == "table", "Child status should be a table")
|
||||||
|
assert(type(exitStatus.ok) == "boolean", "Child status.ok should be a boolean")
|
||||||
|
assert(type(exitStatus.code) == "number", "Child status.code should be a number")
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
status.code == randomExitCode,
|
exitStatus.ok == testOk,
|
||||||
`Child process exited with wrong exit code, expected {randomExitCode}`
|
"Child status should be "
|
||||||
|
.. (if exitStatus.ok then "ok" else "not ok")
|
||||||
|
.. ", was "
|
||||||
|
.. (if exitStatus.ok then "not ok" else "ok")
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
exitStatus.code == testCode,
|
||||||
|
"Child process exited with an unexpected exit code!"
|
||||||
|
.. `\nExpected: ${testCode}`
|
||||||
|
.. `\nReceived: ${exitStatus.code}`
|
||||||
)
|
)
|
||||||
|
|
||||||
assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`)
|
|
||||||
|
|
|
@ -1,18 +1,32 @@
|
||||||
local process = require("@lune/process")
|
local process = require("@lune/process")
|
||||||
|
|
||||||
-- Should be able to write and read from child process streams
|
local expected = "hello, world"
|
||||||
|
|
||||||
local msg = "hello, world"
|
-- Stdout test
|
||||||
|
|
||||||
local catChild = process.create("cat")
|
local catChild = process.create("cat")
|
||||||
catChild.stdin:write(msg)
|
catChild.stdin:write(expected)
|
||||||
|
catChild.stdin:close()
|
||||||
|
local catOutput = catChild.stdout:read(#expected)
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
msg == catChild.stdout:read(#msg),
|
expected == catOutput,
|
||||||
"Failed to write to stdin or read from stdout of child process"
|
"Failed to write to stdin or read from stdout of child process!"
|
||||||
|
.. `\nExpected: "{expected}"`
|
||||||
|
.. `\nReceived: "{catOutput}"`
|
||||||
)
|
)
|
||||||
|
|
||||||
local echoChild = if process.os == "windows"
|
-- Stderr test, needs to run in shell because there is no
|
||||||
then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" })
|
-- other good cross-platform way to simply write to stdout
|
||||||
else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true })
|
|
||||||
|
|
||||||
assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process")
|
local echoChild = if process.os == "windows"
|
||||||
|
then process.create("/c", { "echo", expected, "1>&2" }, { shell = "cmd" })
|
||||||
|
else process.create("echo", { expected, ">>/dev/stderr" }, { shell = true })
|
||||||
|
local echoOutput = echoChild.stderr:read(#expected)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
expected == echoOutput,
|
||||||
|
"Failed to write to stdin or read from stderr of child process!"
|
||||||
|
.. `\nExpected: "{expected}"`
|
||||||
|
.. `\nReceived: "{echoOutput}"`
|
||||||
|
)
|
||||||
|
|
|
@ -2,18 +2,15 @@ export type OS = "linux" | "macos" | "windows"
|
||||||
export type Arch = "x86_64" | "aarch64"
|
export type Arch = "x86_64" | "aarch64"
|
||||||
export type Endianness = "big" | "little"
|
export type Endianness = "big" | "little"
|
||||||
|
|
||||||
export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
|
export type StdioKind = "default" | "inherit" | "forward" | "none"
|
||||||
export type SpawnOptionsStdio = {
|
export type StdioOptions = {
|
||||||
stdout: SpawnOptionsStdioKind?,
|
stdin: StdioKind?,
|
||||||
stderr: SpawnOptionsStdioKind?,
|
stdout: StdioKind?,
|
||||||
}
|
stderr: StdioKind?,
|
||||||
|
|
||||||
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
|
|
||||||
stdin: string?,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface SpawnOptions
|
@interface CreateOptions
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
A dictionary of options for `process.create`, with the following available values:
|
A dictionary of options for `process.create`, with the following available values:
|
||||||
|
@ -21,16 +18,15 @@ export type ExecuteOptionsStdio = SpawnOptionsStdio & {
|
||||||
* `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
|
|
||||||
]=]
|
]=]
|
||||||
export type SpawnOptions = {
|
export type CreateOptions = {
|
||||||
cwd: string?,
|
cwd: string?,
|
||||||
env: { [string]: string }?,
|
env: { [string]: string }?,
|
||||||
shell: (boolean | string)?,
|
shell: (boolean | string)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface ExecuteOptions
|
@interface ExecOptions
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
A dictionary of options for `process.exec`, with the following available values:
|
A dictionary of options for `process.exec`, with the following available values:
|
||||||
|
@ -38,12 +34,13 @@ export type SpawnOptions = {
|
||||||
* `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 `ExecuteOptionsStdio` for more info
|
* `stdio` - How to treat output and error streams from the child process - see `StdioKind` and `StdioOptions` for more info
|
||||||
* `stdin` - Optional standard input to pass to executed child process
|
|
||||||
]=]
|
]=]
|
||||||
export type ExecuteOptions = SpawnOptions & {
|
export type ExecOptions = {
|
||||||
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
|
cwd: string?,
|
||||||
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
env: { [string]: string }?,
|
||||||
|
shell: (boolean | string)?,
|
||||||
|
stdio: (StdioKind | StdioOptions)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
|
@ -57,8 +54,9 @@ local ChildProcessReader = {}
|
||||||
--[=[
|
--[=[
|
||||||
@within ChildProcessReader
|
@within ChildProcessReader
|
||||||
|
|
||||||
Reads a chunk of data (specified length or a default of 8 bytes at a time) from
|
Reads a chunk of data up to the specified length, or a default of 1KB at a time.
|
||||||
the reader as a string. Returns nil if there is no more data to read.
|
|
||||||
|
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
|
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.
|
till present has already been read, and the process has not exited.
|
||||||
|
@ -100,6 +98,15 @@ function ChildProcessWriter:write(data: buffer | string): ()
|
||||||
return nil :: any
|
return nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--[=[
|
||||||
|
@within ChildProcessWriter
|
||||||
|
|
||||||
|
Closes the underlying I/O stream for the writer.
|
||||||
|
]=]
|
||||||
|
function ChildProcessWriter:close(): ()
|
||||||
|
return nil :: any
|
||||||
|
end
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface ChildProcess
|
@interface ChildProcess
|
||||||
@within Process
|
@within Process
|
||||||
|
@ -111,19 +118,22 @@ end
|
||||||
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
|
* `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
|
* `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
|
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
|
||||||
* `kill` - A function that kills the child process
|
* `kill` - A method that kills the child process
|
||||||
* `status` - A function that yields and returns the exit status of the child process
|
* `status` - A method that yields and returns the exit status of the child process
|
||||||
]=]
|
]=]
|
||||||
export type ChildProcess = {
|
export type ChildProcess = {
|
||||||
stdin: typeof(ChildProcessWriter),
|
stdin: typeof(ChildProcessWriter),
|
||||||
stdout: typeof(ChildProcessReader),
|
stdout: typeof(ChildProcessReader),
|
||||||
stderr: typeof(ChildProcessReader),
|
stderr: typeof(ChildProcessReader),
|
||||||
kill: () -> (),
|
kill: (self: ChildProcess) -> (),
|
||||||
status: () -> { ok: boolean, code: number },
|
status: (self: ChildProcess) -> {
|
||||||
|
ok: boolean,
|
||||||
|
code: number,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@interface ExecuteResult
|
@interface ExecResult
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Result type for child processes in `process.exec`.
|
Result type for child processes in `process.exec`.
|
||||||
|
@ -135,7 +145,7 @@ export type ChildProcess = {
|
||||||
* `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 ExecuteResult = {
|
export type ExecResult = {
|
||||||
ok: boolean,
|
ok: boolean,
|
||||||
code: number,
|
code: number,
|
||||||
stdout: string,
|
stdout: string,
|
||||||
|
@ -189,7 +199,7 @@ export type ExecuteResult = {
|
||||||
|
|
||||||
-- Reading from the child process' stdout
|
-- Reading from the child process' stdout
|
||||||
local data = child.stdout:read()
|
local data = child.stdout:read()
|
||||||
print(buffer.tostring(data))
|
print(data)
|
||||||
```
|
```
|
||||||
]=]
|
]=]
|
||||||
local process = {}
|
local process = {}
|
||||||
|
@ -284,8 +294,8 @@ end
|
||||||
--[=[
|
--[=[
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Spawns a child process in the background that runs the program `program`, and immediately returns
|
Spawns a child process in the background that runs the program `program`,
|
||||||
readers and writers to communicate with it.
|
and immediately returns readers and writers to communicate with it.
|
||||||
|
|
||||||
In order to execute a command and wait for its output, see `process.exec`.
|
In order to execute a command and wait for its output, see `process.exec`.
|
||||||
|
|
||||||
|
@ -299,14 +309,14 @@ end
|
||||||
@param options A dictionary of options for the child process
|
@param options A dictionary of options for the child process
|
||||||
@return A dictionary with the readers and writers to communicate with 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
|
function process.create(program: string, params: { string }?, options: CreateOptions?): ChildProcess
|
||||||
return nil :: any
|
return nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
--[=[
|
--[=[
|
||||||
@within Process
|
@within Process
|
||||||
|
|
||||||
Executes a child process that will execute the command `program`, waiting for it to exit.
|
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.
|
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`.
|
In order to spawn a child process in the background, see `process.create`.
|
||||||
|
@ -314,14 +324,14 @@ end
|
||||||
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 `ExecuteOptions` for specific option keys and their values.
|
Refer to the documentation for `ExecOptions` for specific option keys and their values.
|
||||||
|
|
||||||
@param program The program to Execute as a child process
|
@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.exec(program: string, params: { string }?, options: ExecuteOptions?): ExecuteResult
|
function process.exec(program: string, params: { string }?, options: ExecOptions?): ExecResult
|
||||||
return nil :: any
|
return nil :: any
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue