Implement a non-blocking child process interface (#211)

This commit is contained in:
Erica Marigold 2024-10-16 20:48:12 +01:00 committed by GitHub
parent 93fa14d832
commit 309c461e11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 414 additions and 64 deletions

View file

@ -129,7 +129,7 @@ end
]] ]]
print("Sending 4 pings to google 🌏") print("Sending 4 pings to google 🌏")
local result = process.spawn("ping", { local result = process.exec("ping", {
"google.com", "google.com",
"-c 4", "-c 4",
}) })

2
Cargo.lock generated
View file

@ -1629,6 +1629,8 @@ dependencies = [
name = "lune-std-process" name = "lune-std-process"
version = "0.1.3" version = "0.1.3"
dependencies = [ dependencies = [
"bstr",
"bytes",
"directories", "directories",
"lune-utils", "lune-utils",
"mlua", "mlua",

View file

@ -20,6 +20,9 @@ directories = "5.0"
pin-project = "1.0" pin-project = "1.0"
os_str_bytes = { version = "7.0", features = ["conversions"] } os_str_bytes = { version = "7.0", features = ["conversions"] }
bstr = "1.9"
bytes = "1.6.0"
tokio = { version = "1", default-features = false, features = [ tokio = { version = "1", default-features = false, features = [
"io-std", "io-std",
"io-util", "io-util",

View file

@ -1,27 +1,33 @@
#![allow(clippy::cargo_common_metadata)] #![allow(clippy::cargo_common_metadata)]
use std::{ use std::{
cell::RefCell,
env::{ env::{
self, self,
consts::{ARCH, OS}, consts::{ARCH, OS},
}, },
path::MAIN_SEPARATOR, path::MAIN_SEPARATOR,
process::Stdio, process::Stdio,
rc::Rc,
sync::Arc,
}; };
use mlua::prelude::*; use mlua::prelude::*;
use lune_utils::TableBuilder; use lune_utils::TableBuilder;
use mlua_luau_scheduler::{Functions, LuaSpawnExt}; use mlua_luau_scheduler::{Functions, LuaSpawnExt};
use options::ProcessSpawnOptionsStdio;
use os_str_bytes::RawOsString; use os_str_bytes::RawOsString;
use tokio::io::AsyncWriteExt; use stream::{ChildProcessReader, ChildProcessWriter};
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
mod options; mod options;
mod stream;
mod tee_writer; mod tee_writer;
mod wait_for_child; mod wait_for_child;
use self::options::ProcessSpawnOptions; use self::options::ProcessSpawnOptions;
use self::wait_for_child::{wait_for_child, WaitForChildResult}; use self::wait_for_child::wait_for_child;
use lune_utils::path::get_current_dir; use lune_utils::path::get_current_dir;
@ -73,7 +79,8 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
.with_value("cwd", cwd_str)? .with_value("cwd", cwd_str)?
.with_value("env", env_tab)? .with_value("env", env_tab)?
.with_value("exit", process_exit)? .with_value("exit", process_exit)?
.with_async_function("spawn", process_spawn)? .with_async_function("exec", process_exec)?
.with_function("create", process_create)?
.build_readonly() .build_readonly()
} }
@ -141,11 +148,16 @@ fn process_env_iter<'lua>(
}) })
} }
async fn process_spawn( async fn process_exec(
lua: &Lua, lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions), (program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> { ) -> LuaResult<LuaTable> {
let res = lua.spawn(spawn_command(program, args, options)).await?; let res = lua
.spawn(async move {
let cmd = spawn_command_with_stdin(program, args, options.clone()).await?;
wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await
})
.await?;
/* /*
NOTE: If an exit code was not given by the child process, NOTE: If an exit code was not given by the child process,
@ -168,30 +180,104 @@ async fn process_spawn(
.build_readonly() .build_readonly()
} }
async fn spawn_command( #[allow(clippy::await_holding_refcell_ref)]
fn process_create(
lua: &Lua,
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
) -> LuaResult<LuaTable> {
// We do not want the user to provide stdio options for process.create,
// so we reset the options, regardless of what the user provides us
let mut spawn_options = options.clone();
spawn_options.stdio = ProcessSpawnOptionsStdio::default();
let (code_tx, code_rx) = tokio::sync::broadcast::channel(4);
let code_rx_rc = Rc::new(RefCell::new(code_rx));
let child = spawn_command(program, args, spawn_options)?;
let child_arc = Arc::new(RwLock::new(child));
let child_arc_clone = Arc::clone(&child_arc);
let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write());
let stdin = child_lock.stdin.take().unwrap();
let stdout = child_lock.stdout.take().unwrap();
let stderr = child_lock.stderr.take().unwrap();
let child_arc_inner = Arc::clone(&child_arc);
// Spawn a background task to wait for the child to exit and send the exit code
let status_handle = tokio::spawn(async move {
let res = child_arc_inner.write().await.wait().await;
if let Ok(output) = res {
let code = output.code().unwrap_or_default();
code_tx
.send(code)
.expect("ExitCode receiver was unexpectedly dropped");
}
});
TableBuilder::new(lua)?
.with_value("stdout", ChildProcessReader(stdout))?
.with_value("stderr", ChildProcessReader(stderr))?
.with_value("stdin", ChildProcessWriter(stdin))?
.with_async_function("kill", move |_, ()| {
// First, stop the status task so the RwLock is dropped
status_handle.abort();
let child_arc_clone = Arc::clone(&child_arc);
// Then get another RwLock to write to the child process and kill it
async move { Ok(child_arc_clone.write().await.kill().await?) }
})?
.with_async_function("status", move |lua, ()| {
let code_rx_rc_clone = Rc::clone(&code_rx_rc);
async move {
// Exit code of 9 corresponds to SIGKILL, which should be the only case where
// the receiver gets suddenly dropped
let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9);
TableBuilder::new(lua)?
.with_value("code", code)?
.with_value("ok", code == 0)?
.build_readonly()
}
})?
.build_readonly()
}
async fn spawn_command_with_stdin(
program: String, program: String,
args: Option<Vec<String>>, args: Option<Vec<String>>,
mut options: ProcessSpawnOptions, mut options: ProcessSpawnOptions,
) -> LuaResult<WaitForChildResult> { ) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let stdin = options.stdio.stdin.take(); let stdin = options.stdio.stdin.take();
let mut child = options let mut child = spawn_command(program, args, options)?;
.into_command(program, args)
.stdin(if stdin.is_some() {
Stdio::piped()
} else {
Stdio::null()
})
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
if let Some(stdin) = stdin { if let Some(stdin) = stdin {
let mut child_stdin = child.stdin.take().unwrap(); let mut child_stdin = child.stdin.take().unwrap();
child_stdin.write_all(&stdin).await.into_lua_err()?; child_stdin.write_all(&stdin).await.into_lua_err()?;
} }
wait_for_child(child, stdout, stderr).await Ok(child)
}
fn spawn_command(
program: String,
args: Option<Vec<String>>,
options: ProcessSpawnOptions,
) -> LuaResult<Child> {
let stdout = options.stdio.stdout;
let stderr = options.stdio.stderr;
let child = options
.into_command(program, args)
.stdin(Stdio::piped())
.stdout(stdout.as_stdio())
.stderr(stderr.as_stdio())
.spawn()?;
Ok(child)
} }

View file

@ -0,0 +1,58 @@
use bstr::BString;
use bytes::BytesMut;
use mlua::prelude::*;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
const CHUNK_SIZE: usize = 8;
#[derive(Debug, Clone)]
pub struct ChildProcessReader<R: AsyncRead>(pub R);
#[derive(Debug, Clone)]
pub struct ChildProcessWriter<W: AsyncWrite>(pub W);
impl<R: AsyncRead + Unpin> ChildProcessReader<R> {
pub async fn read(&mut self, chunk_size: Option<usize>) -> LuaResult<Vec<u8>> {
let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE));
self.0.read_buf(&mut buf).await?;
Ok(buf.to_vec())
}
pub async fn read_to_end(&mut self) -> LuaResult<Vec<u8>> {
let mut buf = vec![];
self.0.read_to_end(&mut buf).await?;
Ok(buf)
}
}
impl<R: AsyncRead + Unpin + 'static> LuaUserData for ChildProcessReader<R> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("read", |lua, this, chunk_size: Option<usize>| async move {
let buf = this.read(chunk_size).await?;
if buf.is_empty() {
return Ok(LuaValue::Nil);
}
Ok(LuaValue::String(lua.create_string(buf)?))
});
methods.add_async_method_mut("readToEnd", |lua, this, ()| async {
Ok(lua.create_string(this.read_to_end().await?))
});
}
}
impl<W: AsyncWrite + Unpin> ChildProcessWriter<W> {
pub async fn write(&mut self, data: BString) -> LuaResult<()> {
self.0.write_all(data.as_ref()).await?;
Ok(())
}
}
impl<W: AsyncWrite + Unpin + 'static> LuaUserData for ChildProcessWriter<W> {
fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await });
}
}

View file

@ -138,13 +138,16 @@ create_tests! {
process_cwd: "process/cwd", process_cwd: "process/cwd",
process_env: "process/env", process_env: "process/env",
process_exit: "process/exit", process_exit: "process/exit",
process_spawn_async: "process/spawn/async", process_exec_async: "process/exec/async",
process_spawn_basic: "process/spawn/basic", process_exec_basic: "process/exec/basic",
process_spawn_cwd: "process/spawn/cwd", process_exec_cwd: "process/exec/cwd",
process_spawn_no_panic: "process/spawn/no_panic", process_exec_no_panic: "process/exec/no_panic",
process_spawn_shell: "process/spawn/shell", process_exec_shell: "process/exec/shell",
process_spawn_stdin: "process/spawn/stdin", process_exec_stdin: "process/exec/stdin",
process_spawn_stdio: "process/spawn/stdio", process_exec_stdio: "process/exec/stdio",
process_spawn_non_blocking: "process/create/non_blocking",
process_spawn_status: "process/create/status",
process_spawn_stream: "process/create/stream",
} }
#[cfg(feature = "std-regex")] #[cfg(feature = "std-regex")]

View file

@ -108,7 +108,7 @@ local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pi
local function checkInstalled(program: string, args: { string }?) local function checkInstalled(program: string, args: { string }?)
print("Checking if", program, "is installed") print("Checking if", program, "is installed")
local result = process.spawn(program, args) local result = process.exec(program, args)
if not result.ok then if not result.ok then
stdio.ewrite(string.format("Program '%s' is not installed\n", program)) stdio.ewrite(string.format("Program '%s' is not installed\n", program))
process.exit(1) process.exit(1)
@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" })
-- Run them to generate files -- Run them to generate files
local function run(program: string, args: { string }): string local function run(program: string, args: { string }): string
local result = process.spawn(program, args) local result = process.exec(program, args)
if not result.ok then if not result.ok then
stdio.ewrite(string.format("Command '%s' failed\n", program)) stdio.ewrite(string.format("Command '%s' failed\n", program))
if #result.stdout > 0 then if #result.stdout > 0 then

View file

@ -31,7 +31,7 @@ if not runLocaleTests then
return return
end end
local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, { local dateCmd = process.exec("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, {
env = { env = {
LC_ALL = "fr_FR.UTF-8 ", LC_ALL = "fr_FR.UTF-8 ",
}, },

View file

@ -0,0 +1,21 @@
local process = require("@lune/process")
-- Killing a child process should work as expected
local message = "Hello, world!"
local child = process.create("cat")
child.stdin:write(message)
child.kill()
assert(child.status().code == 9, "Child process should have an exit code of 9 (SIGKILL)")
assert(
child.stdout:readToEnd() == message,
"Reading from stdout of child process should work even after kill"
)
local stdinWriteOk = pcall(function()
child.stdin:write(message)
end)
assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill")

View file

@ -0,0 +1,13 @@
local process = require("@lune/process")
-- Spawning a child process should not block the thread
local childThread = coroutine.create(process.create)
local ok, err = coroutine.resume(childThread, "echo", { "hello, world" })
assert(ok, err)
assert(
coroutine.status(childThread) == "dead",
"Child process should not block the thread it is running on"
)

View file

@ -0,0 +1,15 @@
local process = require("@lune/process")
-- The exit code of an child process should be correct
local randomExitCode = math.random(0, 255)
local isOk = randomExitCode == 0
local child = process.create("exit", { tostring(randomExitCode) }, { shell = true })
local status = child.status()
assert(
status.code == randomExitCode,
`Child process exited with wrong exit code, expected {randomExitCode}`
)
assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`)

View file

@ -0,0 +1,18 @@
local process = require("@lune/process")
-- Should be able to write and read from child process streams
local msg = "hello, world"
local catChild = process.create("cat")
catChild.stdin:write(msg)
assert(
msg == catChild.stdout:read(#msg),
"Failed to write to stdin or read from stdout of child process"
)
local echoChild = if process.os == "windows"
then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" })
else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true })
assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process")

View file

@ -4,7 +4,7 @@ local task = require("@lune/task")
local IS_WINDOWS = process.os == "windows" local IS_WINDOWS = process.os == "windows"
-- Spawning a process should not block any lua thread(s) -- Executing a command should not block any lua thread(s)
local SLEEP_DURATION = 1 / 4 local SLEEP_DURATION = 1 / 4
local SLEEP_SAMPLES = 2 local SLEEP_SAMPLES = 2
@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do
table.insert(args, 1, "-Milliseconds") table.insert(args, 1, "-Milliseconds")
end end
-- Windows does not have `sleep` as a process, so we use powershell instead. -- Windows does not have `sleep` as a process, so we use powershell instead.
process.spawn("sleep", args, if IS_WINDOWS then { shell = true } else nil) process.exec("sleep", args, if IS_WINDOWS then { shell = true } else nil)
sleepCounter += 1 sleepCounter += 1
end) end)
end end

View file

@ -2,7 +2,7 @@ local process = require("@lune/process")
local stdio = require("@lune/stdio") local stdio = require("@lune/stdio")
local task = require("@lune/task") local task = require("@lune/task")
-- Spawning a child process should work, with options -- Executing a command should work, with options
local thread = task.delay(1, function() local thread = task.delay(1, function()
stdio.ewrite("Spawning a process should take a reasonable amount of time\n") stdio.ewrite("Spawning a process should take a reasonable amount of time\n")
@ -12,7 +12,7 @@ end)
local IS_WINDOWS = process.os == "windows" local IS_WINDOWS = process.os == "windows"
local result = process.spawn( local result = process.exec(
if IS_WINDOWS then "cmd" else "ls", if IS_WINDOWS then "cmd" else "ls",
if IS_WINDOWS then { "/c", "dir" } else { "-a" } if IS_WINDOWS then { "/c", "dir" } else { "-a" }
) )

View file

@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd"
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {} local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
-- Make sure the cwd option actually uses the directory we want -- Make sure the cwd option actually uses the directory we want
local rootPwd = process.spawn(pwdCommand, pwdArgs, { local rootPwd = process.exec(pwdCommand, pwdArgs, {
cwd = "/", cwd = "/",
}).stdout }).stdout
rootPwd = string.gsub(rootPwd, "^%s+", "") rootPwd = string.gsub(rootPwd, "^%s+", "")
@ -27,24 +27,24 @@ end
-- Setting cwd should not change the cwd of this process -- Setting cwd should not change the cwd of this process
local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout
process.spawn("ls", {}, { process.exec("ls", {}, {
cwd = "/", cwd = "/",
shell = true, shell = true,
}) })
local pwdAfter = process.spawn(pwdCommand, pwdArgs).stdout local pwdAfter = process.exec(pwdCommand, pwdArgs).stdout
assert(pwdBefore == pwdAfter, "Current working directory changed after running child process") assert(pwdBefore == pwdAfter, "Current working directory changed after running child process")
-- Setting the cwd on a child process should properly -- Setting the cwd on a child process should properly
-- replace any leading ~ with the users real home dir -- replace any leading ~ with the users real home dir
local homeDir1 = process.spawn("echo $HOME", nil, { local homeDir1 = process.exec("echo $HOME", nil, {
shell = true, shell = true,
}).stdout }).stdout
-- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return -- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return
-- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object -- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object
local homeDir2 = process.spawn(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, { local homeDir2 = process.exec(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, {
shell = true, shell = true,
cwd = "~", cwd = "~",
}).stdout }).stdout

View file

@ -0,0 +1,7 @@
local process = require("@lune/process")
-- Executing a non existent command as a child process
-- should not panic, but should error
local success = pcall(process.exec, "someProgramThatDoesNotExist")
assert(not success, "Spawned a non-existent program")

View file

@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows"
-- Default shell should be /bin/sh on unix and powershell on Windows, -- Default shell should be /bin/sh on unix and powershell on Windows,
-- note that powershell needs slightly different command flags for ls -- note that powershell needs slightly different command flags for ls
local shellResult = process.spawn("ls", { local shellResult = process.exec("ls", {
if IS_WINDOWS then "-Force" else "-a", if IS_WINDOWS then "-Force" else "-a",
}, { }, {
shell = true, shell = true,

View file

@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!"
-- When passing stdin to powershell on windows we must "accept" using the double newline -- When passing stdin to powershell on windows we must "accept" using the double newline
local result = if IS_WINDOWS local result = if IS_WINDOWS
then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" }) then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
else process.spawn("xargs", { "echo" }, { stdin = echoMessage }) else process.exec("xargs", { "echo" }, { stdin = echoMessage })
local resultStdout = if IS_WINDOWS local resultStdout = if IS_WINDOWS
then string.sub(result.stdout, #result.stdout - #echoMessage - 1) then string.sub(result.stdout, #result.stdout - #echoMessage - 1)

View file

@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows"
-- Inheriting stdio & environment variables should work -- Inheriting stdio & environment variables should work
local echoMessage = "Hello from child process!" local echoMessage = "Hello from child process!"
local echoResult = process.spawn("echo", { local echoResult = process.exec("echo", {
if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"', if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"',
}, { }, {
env = { TEST_VAR = echoMessage }, env = { TEST_VAR = echoMessage },
shell = if IS_WINDOWS then "powershell" else "bash", shell = if IS_WINDOWS then "powershell" else "bash",
stdio = "inherit", stdio = "inherit" :: process.SpawnOptionsStdioKind, -- FIXME: This should just work without a cast?
}) })
-- Windows uses \r\n (CRLF) and unix uses \n (LF) -- Windows uses \r\n (CRLF) and unix uses \n (LF)

View file

@ -1,7 +0,0 @@
local process = require("@lune/process")
-- Spawning a child process for a non-existent
-- program should not panic, but should error
local success = pcall(process.spawn, "someProgramThatDoesNotExist")
assert(not success, "Spawned a non-existent program")

View file

@ -109,7 +109,7 @@ assertContains(
local _, errorMessage = pcall(function() local _, errorMessage = pcall(function()
local function innerInnerFn() local function innerInnerFn()
process.spawn("PROGRAM_THAT_DOES_NOT_EXIST") process.exec("PROGRAM_THAT_DOES_NOT_EXIST")
end end
local function innerFn() local function innerFn()
innerInnerFn() innerInnerFn()

View file

@ -5,6 +5,9 @@ export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
export type SpawnOptionsStdio = { export type SpawnOptionsStdio = {
stdout: SpawnOptionsStdioKind?, stdout: SpawnOptionsStdioKind?,
stderr: SpawnOptionsStdioKind?, stderr: SpawnOptionsStdioKind?,
}
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
stdin: string?, stdin: string?,
} }
@ -12,27 +15,117 @@ export type SpawnOptionsStdio = {
@interface SpawnOptions @interface SpawnOptions
@within Process @within Process
A dictionary of options for `process.spawn`, with the following available values: A dictionary of options for `process.create`, with the following available values:
* `cwd` - The current working directory for the process * `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process * `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell * `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info * `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info
* `stdin` - Optional standard input to pass to spawned child process
]=] ]=]
export type SpawnOptions = { export type SpawnOptions = {
cwd: string?, cwd: string?,
env: { [string]: string }?, env: { [string]: string }?,
shell: (boolean | string)?, shell: (boolean | string)?,
}
--[=[
@interface ExecuteOptions
@within Process
A dictionary of options for `process.exec`, with the following available values:
* `cwd` - The current working directory for the process
* `env` - Extra environment variables to give to the process
* `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell
* `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `ExecuteOptionsStdio` for more info
* `stdin` - Optional standard input to pass to executed child process
]=]
export type ExecuteOptions = SpawnOptions & {
stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?, stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?,
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
} }
--[=[ --[=[
@interface SpawnResult @class ChildProcessReader
@within Process @within Process
Result type for child processes in `process.spawn`. A reader class to read data from a child process' streams in realtime.
]=]
local ChildProcessReader = {}
--[=[
@within ChildProcessReader
Reads a chunk of data (specified length or a default of 8 bytes at a time) from
the reader as a string. Returns nil if there is no more data to read.
This function may yield until there is new data to read from reader, if all data
till present has already been read, and the process has not exited.
@return The string containing the data read from the reader
]=]
function ChildProcessReader:read(chunkSize: number?): string?
return nil :: any
end
--[=[
@within ChildProcessReader
Reads all the data currently present in the reader as a string.
This function will yield until the process exits.
@return The string containing the data read from the reader
]=]
function ChildProcessReader:readToEnd(): string
return nil :: any
end
--[=[
@class ChildProcessWriter
@within Process
A writer class to write data to a child process' streams in realtime.
]=]
local ChildProcessWriter = {}
--[=[
@within ChildProcessWriter
Writes a buffer or string of data to the writer.
@param data The data to write to the writer
]=]
function ChildProcessWriter:write(data: buffer | string): ()
return nil :: any
end
--[=[
@interface ChildProcess
@within Process
Result type for child processes in `process.create`.
This is a dictionary containing the following values:
* `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info
* `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info
* `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info
* `kill` - A function that kills the child process
* `status` - A function that yields and returns the exit status of the child process
]=]
export type ChildProcess = {
stdin: typeof(ChildProcessWriter),
stdout: typeof(ChildProcessReader),
stderr: typeof(ChildProcessReader),
kill: () -> ();
status: () -> { ok: boolean, code: number }
}
--[=[
@interface ExecuteResult
@within Process
Result type for child processes in `process.exec`.
This is a dictionary containing the following values: This is a dictionary containing the following values:
@ -41,7 +134,7 @@ export type SpawnOptions = {
* `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written * `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written
* `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written * `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written
]=] ]=]
export type SpawnResult = { export type ExecuteResult = {
ok: boolean, ok: boolean,
code: number, code: number,
stdout: string, stdout: string,
@ -73,8 +166,8 @@ export type SpawnResult = {
-- Getting the current os and processor architecture -- Getting the current os and processor architecture
print("Running " .. process.os .. " on " .. process.arch .. "!") print("Running " .. process.os .. " on " .. process.arch .. "!")
-- Spawning a child process -- Executing a command
local result = process.spawn("program", { local result = process.exec("program", {
"cli argument", "cli argument",
"other cli argument" "other cli argument"
}) })
@ -83,6 +176,19 @@ export type SpawnResult = {
else else
print(result.stderr) print(result.stderr)
end end
-- Spawning a child process
local child = process.create("program", {
"cli argument",
"other cli argument"
})
-- Writing to the child process' stdin
child.stdin:write("Hello from Lune!")
-- Reading from the child process' stdout
local data = child.stdout:read()
print(buffer.tostring(data))
``` ```
]=] ]=]
local process = {} local process = {}
@ -163,19 +269,44 @@ end
--[=[ --[=[
@within Process @within Process
Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process. Spawns a child process in the background that runs the program `program`, and immediately returns
readers and writers to communicate with it.
In order to execute a command and wait for its output, see `process.exec`.
The second argument, `params`, can be passed as a list of string parameters to give to the program. The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process. The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `SpawnOptions` for specific option keys and their values. Refer to the documentation for `SpawnOptions` for specific option keys and their values.
@param program The program to spawn as a child process @param program The program to Execute as a child process
@param params Additional parameters to pass to the program
@param options A dictionary of options for the child process
@return A dictionary with the readers and writers to communicate with the child process
]=]
function process.create(program: string, params: { string }?, options: SpawnOptions?): ChildProcess
return nil :: any
end
--[=[
@within Process
Executes a child process that will execute the command `program`, waiting for it to exit.
Upon exit, it returns a dictionary that describes the final status and ouput of the child process.
In order to spawn a child process in the background, see `process.create`.
The second argument, `params`, can be passed as a list of string parameters to give to the program.
The third argument, `options`, can be passed as a dictionary of options to give to the child process.
Refer to the documentation for `ExecuteOptions` for specific option keys and their values.
@param program The program to Execute as a child process
@param params Additional parameters to pass to the program @param params Additional parameters to pass to the program
@param options A dictionary of options for the child process @param options A dictionary of options for the child process
@return A dictionary representing the result of the child process @return A dictionary representing the result of the child process
]=] ]=]
function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult function process.exec(program: string, params: { string }?, options: ExecuteOptions?): ExecuteResult
return nil :: any return nil :: any
end end