Complete migration of lune-std-process to use async-process instead of tokio

This commit is contained in:
Filip Tibell 2025-04-24 16:28:34 +02:00
parent 8059026251
commit c4374a0e18
No known key found for this signature in database
16 changed files with 466 additions and 203 deletions

4
Cargo.lock generated
View file

@ -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",

View file

@ -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" }

View 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;
}

View 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())),
}
}
}

View 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())),
}
}
}

View 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;

View 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()
}

View file

@ -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 {

View file

@ -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)
}

View file

@ -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
});
}
}

View file

@ -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")

View file

@ -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")

View file

@ -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"}`)

View file

@ -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}"`
)

View file

@ -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,14 +309,14 @@ 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
--[=[
@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.
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 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