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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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