mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 04:50:36 +00:00
Implement a non-blocking child process interface (#211)
This commit is contained in:
parent
93fa14d832
commit
309c461e11
22 changed files with 414 additions and 64 deletions
|
@ -129,7 +129,7 @@ end
|
|||
]]
|
||||
|
||||
print("Sending 4 pings to google 🌏")
|
||||
local result = process.spawn("ping", {
|
||||
local result = process.exec("ping", {
|
||||
"google.com",
|
||||
"-c 4",
|
||||
})
|
||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1629,6 +1629,8 @@ dependencies = [
|
|||
name = "lune-std-process"
|
||||
version = "0.1.3"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"bytes",
|
||||
"directories",
|
||||
"lune-utils",
|
||||
"mlua",
|
||||
|
|
|
@ -20,6 +20,9 @@ directories = "5.0"
|
|||
pin-project = "1.0"
|
||||
os_str_bytes = { version = "7.0", features = ["conversions"] }
|
||||
|
||||
bstr = "1.9"
|
||||
bytes = "1.6.0"
|
||||
|
||||
tokio = { version = "1", default-features = false, features = [
|
||||
"io-std",
|
||||
"io-util",
|
||||
|
|
|
@ -1,27 +1,33 @@
|
|||
#![allow(clippy::cargo_common_metadata)]
|
||||
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
env::{
|
||||
self,
|
||||
consts::{ARCH, OS},
|
||||
},
|
||||
path::MAIN_SEPARATOR,
|
||||
process::Stdio,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use mlua::prelude::*;
|
||||
|
||||
use lune_utils::TableBuilder;
|
||||
use mlua_luau_scheduler::{Functions, LuaSpawnExt};
|
||||
use options::ProcessSpawnOptionsStdio;
|
||||
use os_str_bytes::RawOsString;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use stream::{ChildProcessReader, ChildProcessWriter};
|
||||
use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock};
|
||||
|
||||
mod options;
|
||||
mod stream;
|
||||
mod tee_writer;
|
||||
mod wait_for_child;
|
||||
|
||||
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;
|
||||
|
||||
|
@ -73,7 +79,8 @@ pub fn module(lua: &Lua) -> LuaResult<LuaTable> {
|
|||
.with_value("cwd", cwd_str)?
|
||||
.with_value("env", env_tab)?
|
||||
.with_value("exit", process_exit)?
|
||||
.with_async_function("spawn", process_spawn)?
|
||||
.with_async_function("exec", process_exec)?
|
||||
.with_function("create", process_create)?
|
||||
.build_readonly()
|
||||
}
|
||||
|
||||
|
@ -141,11 +148,16 @@ fn process_env_iter<'lua>(
|
|||
})
|
||||
}
|
||||
|
||||
async fn process_spawn(
|
||||
async fn process_exec(
|
||||
lua: &Lua,
|
||||
(program, args, options): (String, Option<Vec<String>>, ProcessSpawnOptions),
|
||||
) -> 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,
|
||||
|
@ -168,30 +180,104 @@ async fn process_spawn(
|
|||
.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,
|
||||
args: Option<Vec<String>>,
|
||||
mut options: ProcessSpawnOptions,
|
||||
) -> LuaResult<WaitForChildResult> {
|
||||
let stdout = options.stdio.stdout;
|
||||
let stderr = options.stdio.stderr;
|
||||
) -> LuaResult<Child> {
|
||||
let stdin = options.stdio.stdin.take();
|
||||
|
||||
let mut child = options
|
||||
.into_command(program, args)
|
||||
.stdin(if stdin.is_some() {
|
||||
Stdio::piped()
|
||||
} else {
|
||||
Stdio::null()
|
||||
})
|
||||
.stdout(stdout.as_stdio())
|
||||
.stderr(stderr.as_stdio())
|
||||
.spawn()?;
|
||||
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()?;
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
58
crates/lune-std-process/src/stream.rs
Normal file
58
crates/lune-std-process/src/stream.rs
Normal 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 });
|
||||
}
|
||||
}
|
|
@ -138,13 +138,16 @@ create_tests! {
|
|||
process_cwd: "process/cwd",
|
||||
process_env: "process/env",
|
||||
process_exit: "process/exit",
|
||||
process_spawn_async: "process/spawn/async",
|
||||
process_spawn_basic: "process/spawn/basic",
|
||||
process_spawn_cwd: "process/spawn/cwd",
|
||||
process_spawn_no_panic: "process/spawn/no_panic",
|
||||
process_spawn_shell: "process/spawn/shell",
|
||||
process_spawn_stdin: "process/spawn/stdin",
|
||||
process_spawn_stdio: "process/spawn/stdio",
|
||||
process_exec_async: "process/exec/async",
|
||||
process_exec_basic: "process/exec/basic",
|
||||
process_exec_cwd: "process/exec/cwd",
|
||||
process_exec_no_panic: "process/exec/no_panic",
|
||||
process_exec_shell: "process/exec/shell",
|
||||
process_exec_stdin: "process/exec/stdin",
|
||||
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")]
|
||||
|
|
|
@ -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 }?)
|
||||
print("Checking if", program, "is installed")
|
||||
local result = process.spawn(program, args)
|
||||
local result = process.exec(program, args)
|
||||
if not result.ok then
|
||||
stdio.ewrite(string.format("Program '%s' is not installed\n", program))
|
||||
process.exit(1)
|
||||
|
@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" })
|
|||
-- Run them to generate files
|
||||
|
||||
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
|
||||
stdio.ewrite(string.format("Command '%s' failed\n", program))
|
||||
if #result.stdout > 0 then
|
||||
|
|
|
@ -31,7 +31,7 @@ if not runLocaleTests then
|
|||
return
|
||||
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 = {
|
||||
LC_ALL = "fr_FR.UTF-8 ",
|
||||
},
|
||||
|
|
21
tests/process/create/kill.luau
Normal file
21
tests/process/create/kill.luau
Normal 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")
|
13
tests/process/create/non_blocking.luau
Normal file
13
tests/process/create/non_blocking.luau
Normal 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"
|
||||
)
|
15
tests/process/create/status.luau
Normal file
15
tests/process/create/status.luau
Normal 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"}`)
|
18
tests/process/create/stream.luau
Normal file
18
tests/process/create/stream.luau
Normal 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")
|
|
@ -4,7 +4,7 @@ local task = require("@lune/task")
|
|||
|
||||
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_SAMPLES = 2
|
||||
|
@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do
|
|||
table.insert(args, 1, "-Milliseconds")
|
||||
end
|
||||
-- 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
|
||||
end)
|
||||
end
|
|
@ -2,7 +2,7 @@ local process = require("@lune/process")
|
|||
local stdio = require("@lune/stdio")
|
||||
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()
|
||||
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 result = process.spawn(
|
||||
local result = process.exec(
|
||||
if IS_WINDOWS then "cmd" else "ls",
|
||||
if IS_WINDOWS then { "/c", "dir" } else { "-a" }
|
||||
)
|
|
@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd"
|
|||
local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {}
|
||||
|
||||
-- Make sure the cwd option actually uses the directory we want
|
||||
local rootPwd = process.spawn(pwdCommand, pwdArgs, {
|
||||
local rootPwd = process.exec(pwdCommand, pwdArgs, {
|
||||
cwd = "/",
|
||||
}).stdout
|
||||
rootPwd = string.gsub(rootPwd, "^%s+", "")
|
||||
|
@ -27,24 +27,24 @@ end
|
|||
|
||||
-- Setting cwd should not change the cwd of this process
|
||||
|
||||
local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout
|
||||
process.spawn("ls", {}, {
|
||||
local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout
|
||||
process.exec("ls", {}, {
|
||||
cwd = "/",
|
||||
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")
|
||||
|
||||
-- Setting the cwd on a child process should properly
|
||||
-- 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,
|
||||
}).stdout
|
||||
|
||||
-- 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
|
||||
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,
|
||||
cwd = "~",
|
||||
}).stdout
|
7
tests/process/exec/no_panic.luau
Normal file
7
tests/process/exec/no_panic.luau
Normal 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")
|
|
@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows"
|
|||
-- Default shell should be /bin/sh on unix and powershell on Windows,
|
||||
-- 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",
|
||||
}, {
|
||||
shell = true,
|
|
@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!"
|
|||
-- When passing stdin to powershell on windows we must "accept" using the double newline
|
||||
|
||||
local result = if IS_WINDOWS
|
||||
then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
|
||||
else process.spawn("xargs", { "echo" }, { stdin = echoMessage })
|
||||
then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" })
|
||||
else process.exec("xargs", { "echo" }, { stdin = echoMessage })
|
||||
|
||||
local resultStdout = if IS_WINDOWS
|
||||
then string.sub(result.stdout, #result.stdout - #echoMessage - 1)
|
|
@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows"
|
|||
-- Inheriting stdio & environment variables should work
|
||||
|
||||
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"',
|
||||
}, {
|
||||
env = { TEST_VAR = echoMessage },
|
||||
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)
|
|
@ -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")
|
|
@ -109,7 +109,7 @@ assertContains(
|
|||
|
||||
local _, errorMessage = pcall(function()
|
||||
local function innerInnerFn()
|
||||
process.spawn("PROGRAM_THAT_DOES_NOT_EXIST")
|
||||
process.exec("PROGRAM_THAT_DOES_NOT_EXIST")
|
||||
end
|
||||
local function innerFn()
|
||||
innerInnerFn()
|
||||
|
|
|
@ -5,6 +5,9 @@ export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none"
|
|||
export type SpawnOptionsStdio = {
|
||||
stdout: SpawnOptionsStdioKind?,
|
||||
stderr: SpawnOptionsStdioKind?,
|
||||
}
|
||||
|
||||
export type ExecuteOptionsStdio = SpawnOptionsStdio & {
|
||||
stdin: string?,
|
||||
}
|
||||
|
||||
|
@ -12,27 +15,117 @@ export type SpawnOptionsStdio = {
|
|||
@interface SpawnOptions
|
||||
@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
|
||||
* `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
|
||||
* `stdin` - Optional standard input to pass to spawned child process
|
||||
]=]
|
||||
export type SpawnOptions = {
|
||||
cwd: string?,
|
||||
env: { [string]: 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)?,
|
||||
stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change
|
||||
}
|
||||
|
||||
--[=[
|
||||
@interface SpawnResult
|
||||
@class ChildProcessReader
|
||||
@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:
|
||||
|
||||
|
@ -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
|
||||
* `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,
|
||||
code: number,
|
||||
stdout: string,
|
||||
|
@ -73,8 +166,8 @@ export type SpawnResult = {
|
|||
-- Getting the current os and processor architecture
|
||||
print("Running " .. process.os .. " on " .. process.arch .. "!")
|
||||
|
||||
-- Spawning a child process
|
||||
local result = process.spawn("program", {
|
||||
-- Executing a command
|
||||
local result = process.exec("program", {
|
||||
"cli argument",
|
||||
"other cli argument"
|
||||
})
|
||||
|
@ -83,6 +176,19 @@ export type SpawnResult = {
|
|||
else
|
||||
print(result.stderr)
|
||||
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 = {}
|
||||
|
@ -163,19 +269,44 @@ end
|
|||
--[=[
|
||||
@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 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.
|
||||
|
||||
@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 options A dictionary of options for 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
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in a new issue