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"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"async-io",
|
||||
"async-channel",
|
||||
"async-lock",
|
||||
"async-process",
|
||||
"blocking",
|
||||
"bstr",
|
||||
"bytes",
|
||||
"directories",
|
||||
"futures-lite",
|
||||
"futures-util",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
"mlua-luau-scheduler",
|
||||
|
|
|
@ -23,9 +23,11 @@ os_str_bytes = { version = "7.0", features = ["conversions"] }
|
|||
bstr = "1.9"
|
||||
bytes = "1.6.0"
|
||||
|
||||
async-io = "2.4"
|
||||
async-channel = "2.3"
|
||||
async-lock = "3.4"
|
||||
async-process = "2.3"
|
||||
blocking = "1.6"
|
||||
futures-lite = "2.6"
|
||||
futures-util = "0.3" # Needed for select! macro...
|
||||
|
||||
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 futures_lite::{io, prelude::*};
|
||||
|
||||
use super::{options::ProcessSpawnOptionsStdioKind, tee_writer::AsyncTeeWriter};
|
||||
use super::tee_writer::AsyncTeeWriter;
|
||||
use crate::options::ProcessSpawnOptionsStdioKind;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(super) struct WaitForChildResult {
|
|
@ -10,21 +10,17 @@ use std::{
|
|||
};
|
||||
|
||||
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 lune_utils::{path::get_current_dir, TableBuilder};
|
||||
|
||||
mod create;
|
||||
mod exec;
|
||||
mod options;
|
||||
mod stream;
|
||||
mod tee_writer;
|
||||
mod wait_for_child;
|
||||
|
||||
use self::options::ProcessSpawnOptions;
|
||||
use self::wait_for_child::wait_for_child;
|
||||
|
||||
/**
|
||||
Creates the `process` standard library module.
|
||||
|
@ -42,6 +38,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
|||
if !cwd_str.ends_with(MAIN_SEPARATOR) {
|
||||
cwd_str.push(MAIN_SEPARATOR);
|
||||
}
|
||||
|
||||
// Create constants for OS & processor architecture
|
||||
let os = lua.create_string(OS.to_lowercase())?;
|
||||
let arch = lua.create_string(ARCH.to_lowercase())?;
|
||||
|
@ -50,6 +47,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
|||
} else {
|
||||
"little"
|
||||
})?;
|
||||
|
||||
// Create readonly args array
|
||||
let args_vec = lua
|
||||
.app_data_ref::<Vec<String>>()
|
||||
|
@ -58,6 +56,7 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
|||
let args_tab = TableBuilder::new(lua.clone())?
|
||||
.with_sequential_values(args_vec)?
|
||||
.build_readonly()?;
|
||||
|
||||
// Create proxied table for env that gets & sets real env vars
|
||||
let env_tab = TableBuilder::new(lua.clone())?
|
||||
.with_metatable(
|
||||
|
@ -68,9 +67,11 @@ pub fn module(lua: Lua) -> LuaResult<LuaTable> {
|
|||
.build_readonly()?,
|
||||
)?
|
||||
.build_readonly()?;
|
||||
|
||||
// Create our process exit function, the scheduler crate provides this
|
||||
let fns = Functions::new(lua.clone())?;
|
||||
let process_exit = fns.exit;
|
||||
|
||||
// Create the full process table
|
||||
TableBuilder::new(lua)?
|
||||
.with_value("os", os)?
|
||||
|
@ -142,66 +143,9 @@ fn process_env_iter(lua: &Lua, (_, ()): (LuaValue, ())) -> LuaResult<LuaFunction
|
|||
|
||||
async fn process_exec(
|
||||
lua: Lua,
|
||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
(program, args, mut options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
) -> 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 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 stderr = options.stdio.stderr;
|
||||
|
||||
|
@ -212,5 +156,19 @@ fn spawn_command(
|
|||
.stderr(stderr.as_stdio())
|
||||
.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")
|
||||
|
||||
-- Killing a child process should work as expected
|
||||
local expected = "Hello, world!"
|
||||
|
||||
local message = "Hello, world!"
|
||||
local child = process.create("cat")
|
||||
local catChild = process.create("cat")
|
||||
catChild.stdin:write(expected)
|
||||
catChild:kill()
|
||||
local catStatus = catChild:status()
|
||||
local catStdout = catChild.stdout:readToEnd()
|
||||
|
||||
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"
|
||||
)
|
||||
assert(catStatus.code == 9, "Child process should have an exit code of 9 (SIGKILL)")
|
||||
assert(catStdout == expected, "Reading from stdout of child process should work even after kill")
|
||||
|
||||
local stdinWriteOk = pcall(function()
|
||||
child.stdin:write(message)
|
||||
catChild.stdin:write(expected)
|
||||
end)
|
||||
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
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"
|
||||
)
|
||||
assert(coroutine.status(childThread) == "dead", "Child process should not yield the thread it is created on")
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
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 isOk = randomExitCode == 0
|
||||
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
|
||||
local status = child.status()
|
||||
local exitChild = process.create("exit", { tostring(testCode) }, { shell = true })
|
||||
local exitStatus = exitChild: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(
|
||||
status.code == randomExitCode,
|
||||
`Child process exited with wrong exit code, expected {randomExitCode}`
|
||||
exitStatus.ok == testOk,
|
||||
"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")
|
||||
|
||||
-- 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")
|
||||
catChild.stdin:write(msg)
|
||||
catChild.stdin:write(expected)
|
||||
catChild.stdin:close()
|
||||
local catOutput = catChild.stdout:read(#expected)
|
||||
|
||||
assert(
|
||||
msg == catChild.stdout:read(#msg),
|
||||
"Failed to write to stdin or read from stdout of child process"
|
||||
expected == catOutput,
|
||||
"Failed to write to stdin or read from stdout of child process!"
|
||||
.. `\nExpected: "{expected}"`
|
||||
.. `\nReceived: "{catOutput}"`
|
||||
)
|
||||
|
||||
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 })
|
||||
-- Stderr test, needs to run in shell because there is no
|
||||
-- other good cross-platform way to simply write to stdout
|
||||
|
||||
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 Endianness = "big" | "little"
|
||||
|
||||
export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
|
||||
export type SpawnOptionsStdio = {
|
||||
stdout: SpawnOptionsStdioKind?,
|
||||
stderr: SpawnOptionsStdioKind?,
|
||||
}
|
||||
|
||||
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
|
||||
stdin: string?,
|
||||
export type StdioKind = "default" | "inherit" | "forward" | "none"
|
||||
export type StdioOptions = {
|
||||
stdin: StdioKind?,
|
||||
stdout: StdioKind?,
|
||||
stderr: StdioKind?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface SpawnOptions
|
||||
@interface CreateOptions
|
||||
@within Process
|
||||
|
||||
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
|
||||
* `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 `SpawnOptionsStdio` for more info
|
||||
]=]
|
||||
export type SpawnOptions = {
|
||||
export type CreateOptions = {
|
||||
cwd: string?,
|
||||
env: { [string]: string }?,
|
||||
shell: (boolean | string)?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface ExecuteOptions
|
||||
@interface ExecOptions
|
||||
@within Process
|
||||
|
||||
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
|
||||
* `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
|
||||
* `stdio` - How to treat output and error streams from the child process - see `StdioKind` and `StdioOptions` for more info
|
||||
]=]
|
||||
export type ExecuteOptions = SpawnOptions & {
|
||||
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
|
||||
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
||||
export type ExecOptions = {
|
||||
cwd: string?,
|
||||
env: { [string]: string }?,
|
||||
shell: (boolean | string)?,
|
||||
stdio: (StdioKind | StdioOptions)?,
|
||||
}
|
||||
|
||||
--[=[
|
||||
|
@ -57,8 +54,9 @@ 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.
|
||||
Reads a chunk of data up to the specified length, or a default of 1KB at a time.
|
||||
|
||||
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.
|
||||
|
@ -100,6 +98,15 @@ function ChildProcessWriter:write(data: buffer | string): ()
|
|||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@within ChildProcessWriter
|
||||
|
||||
Closes the underlying I/O stream for the writer.
|
||||
]=]
|
||||
function ChildProcessWriter:close(): ()
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
--[=[
|
||||
@interface ChildProcess
|
||||
@within Process
|
||||
|
@ -111,19 +118,22 @@ end
|
|||
* `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
|
||||
* `kill` - A method that kills the child process
|
||||
* `status` - A method 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 },
|
||||
kill: (self: ChildProcess) -> (),
|
||||
status: (self: ChildProcess) -> {
|
||||
ok: boolean,
|
||||
code: number,
|
||||
},
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface ExecuteResult
|
||||
@interface ExecResult
|
||||
@within Process
|
||||
|
||||
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
|
||||
* `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,
|
||||
code: number,
|
||||
stdout: string,
|
||||
|
@ -189,7 +199,7 @@ export type ExecuteResult = {
|
|||
|
||||
-- Reading from the child process' stdout
|
||||
local data = child.stdout:read()
|
||||
print(buffer.tostring(data))
|
||||
print(data)
|
||||
```
|
||||
]=]
|
||||
local process = {}
|
||||
|
@ -284,8 +294,8 @@ end
|
|||
--[=[
|
||||
@within Process
|
||||
|
||||
Spawns a child process in the background that runs the program `program`, and immediately returns
|
||||
readers and writers to communicate with it.
|
||||
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`.
|
||||
|
||||
|
@ -299,7 +309,7 @@ end
|
|||
@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
|
||||
function process.create(program: string, params: { string }?, options: CreateOptions?): ChildProcess
|
||||
return nil :: any
|
||||
end
|
||||
|
||||
|
@ -314,14 +324,14 @@ end
|
|||
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.
|
||||
Refer to the documentation for `ExecOptions` 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 options A dictionary of options for 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
|
||||
end
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue