Merge branch 'main' into feature/jit-toggle

This commit is contained in:
Erica Marigold 2024-10-17 06:54:31 +01:00 committed by GitHub
commit eedcc1e16d
Signed by: DevComp
GPG key ID: B5690EEEBB952194
43 changed files with 546 additions and 223 deletions

View file

@ -23,11 +23,8 @@ jobs:
with: with:
components: rustfmt components: rustfmt
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling - name: Install Tooling
uses: ok-nick/setup-aftman@v0.4.2 uses: CompeyDev/setup-rokit@v0.1.0
- name: Check Formatting - name: Check Formatting
run: just fmt-check run: just fmt-check
@ -40,11 +37,8 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install Just
uses: extractions/setup-just@v2
- name: Install Tooling - name: Install Tooling
uses: ok-nick/setup-aftman@v0.4.2 uses: CompeyDev/setup-rokit@v0.1.0
- name: Analyze - name: Analyze
run: just analyze run: just analyze

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

@ -27,9 +27,8 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M)
} }
fn get_or_create_material_colors(instance: &Instance) -> MaterialColors { fn get_or_create_material_colors(instance: &Instance) -> MaterialColors {
if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors") if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") {
{ inner
material_colors
} else { } else {
MaterialColors::default() MaterialColors::default()
} }

View file

@ -65,9 +65,9 @@ async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult<LuaTable> {
res.await?.into_lua_table(lua) res.await?.into_lua_table(lua)
} }
async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaTable> { async fn net_socket(lua: &Lua, url: String) -> LuaResult<LuaValue> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?; let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua_table(lua) NetWebSocket::new(ws).into_lua(lua)
} }
async fn net_serve<'lua>( async fn net_serve<'lua>(

View file

@ -40,13 +40,13 @@ impl Service<Request<Incoming>> for Svc {
lua.spawn_local(async move { lua.spawn_local(async move {
let sock = sock.await.unwrap(); let sock = sock.await.unwrap();
let lua_sock = NetWebSocket::new(sock); let lua_sock = NetWebSocket::new(sock);
let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap(); let lua_val = lua_sock.into_lua(&lua_inner).unwrap();
let handler_websocket: LuaFunction = let handler_websocket: LuaFunction =
keys.websocket_handler(&lua_inner).unwrap().unwrap(); keys.websocket_handler(&lua_inner).unwrap().unwrap();
lua_inner lua_inner
.push_thread_back(handler_websocket, lua_tab) .push_thread_back(handler_websocket, lua_val)
.unwrap(); .unwrap();
}); });

View file

@ -23,29 +23,6 @@ use hyper_tungstenite::{
WebSocketStream, WebSocketStream,
}; };
use lune_utils::TableBuilder;
// Wrapper implementation for compatibility and changing colon syntax to dot syntax
const WEB_SOCKET_IMPL_LUA: &str = r#"
return freeze(setmetatable({
close = function(...)
return websocket:close(...)
end,
send = function(...)
return websocket:send(...)
end,
next = function(...)
return websocket:next(...)
end,
}, {
__index = function(self, key)
if key == "closeCode" then
return websocket.closeCode
end
end,
}))
"#;
#[derive(Debug)] #[derive(Debug)]
pub struct NetWebSocket<T> { pub struct NetWebSocket<T> {
close_code_exists: Arc<AtomicBool>, close_code_exists: Arc<AtomicBool>,
@ -125,25 +102,6 @@ where
let mut ws = self.write_stream.lock().await; let mut ws = self.write_stream.lock().await;
ws.close().await.into_lua_err() ws.close().await.into_lua_err()
} }
pub fn into_lua_table(self, lua: &Lua) -> LuaResult<LuaTable> {
let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?;
let table_freeze = lua
.globals()
.get::<_, LuaTable>("table")?
.get::<_, LuaFunction>("freeze")?;
let env = TableBuilder::new(lua)?
.with_value("websocket", self.clone())?
.with_value("setmetatable", setmetatable)?
.with_value("freeze", table_freeze)?
.build_readonly()?;
lua.load(WEB_SOCKET_IMPL_LUA)
.set_name("websocket")
.set_environment(env)
.eval()
}
} }
impl<T> LuaUserData for NetWebSocket<T> impl<T> LuaUserData for NetWebSocket<T>

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

@ -39,9 +39,9 @@ impl RunCommand {
let file_display_name = file_path.with_extension("").display().to_string(); let file_display_name = file_path.with_extension("").display().to_string();
(file_display_name, file_contents) (file_display_name, file_contents)
}; };
// Create a new lune object with all globals & run the script // Create a new lune runtime with all globals & run the script
let result = Runtime::new() let mut rt = Runtime::new()
.with_args(self.script_args) .with_args(self.script_args)
// Enable JIT compilation unless it was requested to be disabled // Enable JIT compilation unless it was requested to be disabled
.with_jit( .with_jit(
@ -49,15 +49,18 @@ impl RunCommand {
env::var("LUNE_LUAU_JIT").ok(), env::var("LUNE_LUAU_JIT").ok(),
Some(jit_enabled) if jit_enabled == "0" || jit_enabled == "false" || jit_enabled == "off" Some(jit_enabled) if jit_enabled == "0" || jit_enabled == "false" || jit_enabled == "off"
) )
) );
let result = rt
.run(&script_display_name, strip_shebang(script_contents)) .run(&script_display_name, strip_shebang(script_contents))
.await; .await;
Ok(match result { Ok(match result {
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
ExitCode::FAILURE ExitCode::FAILURE
} }
Ok(code) => code, Ok((code, _)) => ExitCode::from(code),
}) })
} }
} }

View file

@ -1,7 +1,6 @@
#![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_panics_doc)]
use std::{ use std::{
process::ExitCode,
rc::Rc, rc::Rc,
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
@ -153,7 +152,7 @@ impl Runtime {
&mut self, &mut self,
script_name: impl AsRef<str>, script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>, script_contents: impl AsRef<[u8]>,
) -> RuntimeResult<ExitCode> { ) -> RuntimeResult<(u8, Vec<LuaValue>)> {
let lua = self.inner.lua(); let lua = self.inner.lua();
let sched = self.inner.scheduler(); let sched = self.inner.scheduler();
@ -171,18 +170,19 @@ impl Runtime {
.set_name(script_name.as_ref()); .set_name(script_name.as_ref());
// Run it on our scheduler until it and any other spawned threads complete // Run it on our scheduler until it and any other spawned threads complete
sched.push_thread_back(main, ())?; let main_thread_id = sched.push_thread_back(main, ())?;
sched.run().await; sched.run().await;
// Return the exit code - default to FAILURE if we got any errors let main_thread_res = match sched.get_thread_result(main_thread_id) {
let exit_code = sched.get_exit_code().unwrap_or({ Some(res) => res,
if got_any_error.load(Ordering::SeqCst) { None => LuaValue::Nil.into_lua_multi(lua),
ExitCode::FAILURE }?;
} else {
ExitCode::SUCCESS
}
});
Ok(exit_code) Ok((
sched
.get_exit_code()
.unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))),
main_thread_res.into_vec(),
))
} }
} }

View file

@ -29,16 +29,15 @@ pub async fn run(patched_bin: impl AsRef<[u8]>) -> Result<ExitCode> {
let args = env::args().skip(1).collect::<Vec<_>>(); let args = env::args().skip(1).collect::<Vec<_>>();
let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary"); let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary");
let result = Runtime::new() let mut rt = Runtime::new().with_args(args);
.with_args(args)
.run("STANDALONE", meta.bytecode) let result = rt.run("STANDALONE", meta.bytecode).await;
.await;
Ok(match result { Ok(match result {
Err(err) => { Err(err) => {
eprintln!("{err}"); eprintln!("{err}");
ExitCode::FAILURE ExitCode::FAILURE
} }
Ok(code) => code, Ok((code, _)) => ExitCode::from(code),
}) })
} }

View file

@ -42,8 +42,8 @@ macro_rules! create_tests {
.trim_end_matches(".luau") .trim_end_matches(".luau")
.trim_end_matches(".lua") .trim_end_matches(".lua")
.to_string(); .to_string();
let exit_code = lune.run(&script_name, &script).await?; let (exit_code, _) = lune.run(&script_name, &script).await?;
Ok(exit_code) Ok(ExitCode::from(exit_code))
} }
)* } )* }
} }
@ -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

@ -32,7 +32,7 @@ pub fn main() -> LuaResult<()> {
// Verify that we got a correct exit code // Verify that we got a correct exit code
let code = sched.get_exit_code().unwrap_or_default(); let code = sched.get_exit_code().unwrap_or_default();
assert!(format!("{code:?}").contains("(1)")); assert_eq!(code, 1);
Ok(()) Ok(())
} }

View file

@ -1,10 +1,10 @@
use std::{cell::Cell, process::ExitCode, rc::Rc}; use std::{cell::Cell, rc::Rc};
use event_listener::Event; use event_listener::Event;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Exit { pub(crate) struct Exit {
code: Rc<Cell<Option<ExitCode>>>, code: Rc<Cell<Option<u8>>>,
event: Rc<Event>, event: Rc<Event>,
} }
@ -16,12 +16,12 @@ impl Exit {
} }
} }
pub fn set(&self, code: ExitCode) { pub fn set(&self, code: u8) {
self.code.set(Some(code)); self.code.set(Some(code));
self.event.notify(usize::MAX); self.event.notify(usize::MAX);
} }
pub fn get(&self) -> Option<ExitCode> { pub fn get(&self) -> Option<u8> {
self.code.get() self.code.get()
} }

View file

@ -1,15 +1,11 @@
#![allow(unused_imports)]
#![allow(clippy::too_many_lines)] #![allow(clippy::too_many_lines)]
use std::process::ExitCode;
use mlua::prelude::*; use mlua::prelude::*;
use crate::{ use crate::{
error_callback::ThreadErrorCallback, error_callback::ThreadErrorCallback,
queue::{DeferredThreadQueue, SpawnedThreadQueue}, queue::{DeferredThreadQueue, SpawnedThreadQueue},
result_map::ThreadResultMap, result_map::ThreadResultMap,
scheduler::Scheduler,
thread_id::ThreadId, thread_id::ThreadId,
traits::LuaSchedulerExt, traits::LuaSchedulerExt,
util::{is_poll_pending, LuaThreadOrFunction, ThreadResult}, util::{is_poll_pending, LuaThreadOrFunction, ThreadResult},
@ -232,7 +228,7 @@ impl<'lua> Functions<'lua> {
"exit", "exit",
lua.create_function(|lua, code: Option<u8>| { lua.create_function(|lua, code: Option<u8>| {
let _span = tracing::trace_span!("Scheduler::fn_exit").entered(); let _span = tracing::trace_span!("Scheduler::fn_exit").entered();
let code = code.map(ExitCode::from).unwrap_or_default(); let code = code.unwrap_or_default();
lua.set_exit_code(code); lua.set_exit_code(code);
Ok(()) Ok(())
})?, })?,

View file

@ -2,7 +2,6 @@
use std::{ use std::{
cell::Cell, cell::Cell,
process::ExitCode,
rc::{Rc, Weak as WeakRc}, rc::{Rc, Weak as WeakRc},
sync::{Arc, Weak as WeakArc}, sync::{Arc, Weak as WeakArc},
thread::panicking, thread::panicking,
@ -168,7 +167,7 @@ impl<'lua> Scheduler<'lua> {
Gets the exit code for this scheduler, if one has been set. Gets the exit code for this scheduler, if one has been set.
*/ */
#[must_use] #[must_use]
pub fn get_exit_code(&self) -> Option<ExitCode> { pub fn get_exit_code(&self) -> Option<u8> {
self.exit.get() self.exit.get()
} }
@ -177,7 +176,7 @@ impl<'lua> Scheduler<'lua> {
This will cause [`Scheduler::run`] to exit immediately. This will cause [`Scheduler::run`] to exit immediately.
*/ */
pub fn set_exit_code(&self, code: ExitCode) { pub fn set_exit_code(&self, code: u8) {
self.exit.set(code); self.exit.set(code);
} }

View file

@ -82,7 +82,7 @@ pub trait LuaSchedulerExt<'lua> {
Panics if called outside of a running [`Scheduler`]. Panics if called outside of a running [`Scheduler`].
*/ */
fn set_exit_code(&self, code: ExitCode); fn set_exit_code(&self, code: u8);
/** /**
Pushes (spawns) a lua thread to the **front** of the current scheduler. Pushes (spawns) a lua thread to the **front** of the current scheduler.
@ -283,7 +283,7 @@ pub trait LuaSpawnExt<'lua> {
} }
impl<'lua> LuaSchedulerExt<'lua> for Lua { impl<'lua> LuaSchedulerExt<'lua> for Lua {
fn set_exit_code(&self, code: ExitCode) { fn set_exit_code(&self, code: u8) {
let exit = self let exit = self
.app_data_ref::<Exit>() .app_data_ref::<Exit>()
.expect("exit code can only be set from within an active scheduler"); .expect("exit code can only be set from within an active scheduler");

View file

@ -1,3 +1,4 @@
[tools] [tools]
luau-lsp = "JohnnyMorganz/luau-lsp@1.32.1" luau-lsp = "JohnnyMorganz/luau-lsp@1.32.1"
stylua = "JohnnyMorganz/StyLua@0.20.0" stylua = "JohnnyMorganz/StyLua@0.20.0"
just = "casey/just@1.34.0"

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

@ -24,10 +24,10 @@ local handle = net.serve(PORT, {
return "unreachable" return "unreachable"
end, end,
handleWebSocket = function(socket) handleWebSocket = function(socket)
local socketMessage = socket.next() local socketMessage = socket:next()
assert(socketMessage == REQUEST, "Invalid web socket request from client") assert(socketMessage == REQUEST, "Invalid web socket request from client")
socket.send(RESPONSE) socket:send(RESPONSE)
socket.close() socket:close()
end, end,
}) })
@ -43,19 +43,19 @@ end)
local socket = net.socket(WS_URL) local socket = net.socket(WS_URL)
socket.send(REQUEST) socket:send(REQUEST)
local socketMessage = socket.next() local socketMessage = socket:next()
assert(socketMessage ~= nil, "Got no web socket response from server") assert(socketMessage ~= nil, "Got no web socket response from server")
assert(socketMessage == RESPONSE, "Invalid web socket response from server") assert(socketMessage == RESPONSE, "Invalid web socket response from server")
socket.close() socket:close()
task.cancel(thread2) task.cancel(thread2)
-- Wait for the socket to close and make sure we can't send messages afterwards -- Wait for the socket to close and make sure we can't send messages afterwards
task.wait() task.wait()
local success3, err2 = (pcall :: any)(socket.send, "") local success3, err2 = (pcall :: any)(socket.send, socket, "")
assert(not success3, "Sending messages after the socket has been closed should error") assert(not success3, "Sending messages after the socket has been closed should error")
local message2 = tostring(err2) local message2 = tostring(err2)
assert( assert(

View file

@ -8,17 +8,17 @@ assert(type(socket.send) == "function", "send must be a function")
assert(type(socket.close) == "function", "close must be a function") assert(type(socket.close) == "function", "close must be a function")
-- Request to close the socket -- Request to close the socket
socket.close() socket:close()
-- Drain remaining messages, until we got our close message -- Drain remaining messages, until we got our close message
while socket.next() do while socket:next() do
end end
assert(type(socket.closeCode) == "number", "closeCode should exist after closing") assert(type(socket.closeCode) == "number", "closeCode should exist after closing")
assert(socket.closeCode == 1000, "closeCode should be 1000 after closing") assert(socket.closeCode == 1000, "closeCode should be 1000 after closing")
local success, message = pcall(function() local success, message = pcall(function()
socket.send("Hello, world!") socket:send("Hello, world!")
end) end)
assert(not success, "send should fail after closing") assert(not success, "send should fail after closing")

View file

@ -8,7 +8,7 @@ local task = require("@lune/task")
local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json") local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
while not socket.closeCode do while not socket.closeCode do
local response = socket.next() local response = socket:next()
if response then if response then
local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response) local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response)
@ -23,6 +23,6 @@ while not socket.closeCode do
-- Close the connection after a second with the success close code -- Close the connection after a second with the success close code
task.wait(1) task.wait(1)
socket.close(1000) socket:close(1000)
end end
end end

View file

@ -10,7 +10,7 @@ local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json")
local spawnedThread = task.spawn(function() local spawnedThread = task.spawn(function()
while not socket.closeCode do while not socket.closeCode do
socket.next() socket:next()
end end
end) end)
@ -23,9 +23,9 @@ end)
task.wait(1) task.wait(1)
local payload = '{"op":1,"d":null}' local payload = '{"op":1,"d":null}'
socket.send(payload) socket:send(payload)
socket.send(buffer.fromstring(payload)) socket:send(buffer.fromstring(payload))
socket.close(1000) socket:close(1000)
task.cancel(delayedThread) task.cancel(delayedThread)
task.cancel(spawnedThread) task.cancel(spawnedThread)

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

@ -87,10 +87,19 @@ export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond
]=] ]=]
export type DateTimeValueReturns = DateTimeValues & Millisecond export type DateTimeValueReturns = DateTimeValues & Millisecond
--[=[
@prop unixTimestamp number
@within DateTime
Number of seconds passed since the UNIX epoch.
]=]
--[=[
@prop unixTimestampMillis number
@within DateTime
Number of milliseconds passed since the UNIX epoch.
]=]
local DateTime = { local DateTime = {
--- Number of seconds passed since the UNIX epoch.
unixTimestamp = (nil :: any) :: number, unixTimestamp = (nil :: any) :: number,
--- Number of milliseconds passed since the UNIX epoch.
unixTimestampMillis = (nil :: any) :: number, unixTimestampMillis = (nil :: any) :: number,
} }

View file

@ -173,9 +173,9 @@ export type ServeHandle = {
]=] ]=]
export type WebSocket = { export type WebSocket = {
closeCode: number?, closeCode: number?,
close: (code: number?) -> (), close: (self: WebSocket, code: number?) -> (),
send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (), send: (self: WebSocket, message: (string | buffer)?, asBinaryMessage: boolean?) -> (),
next: () -> string?, next: (self: WebSocket) -> string?,
} }
--[=[ --[=[

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

View file

@ -19,67 +19,82 @@ local RegexMatch = {
type RegexMatch = typeof(RegexMatch) type RegexMatch = typeof(RegexMatch)
local RegexCaptures = {}
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
return nil :: any
end
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch?
return nil :: any
end
function RegexCaptures.format(self: RegexCaptures, format: string): string
return nil :: any
end
--[=[ --[=[
@class RegexCaptures @class RegexCaptures
Captures from a regular expression. Captures from a regular expression.
]=] ]=]
local RegexCaptures = {} export type RegexCaptures = typeof(setmetatable(
{} :: {
--[=[
@within RegexCaptures
@tag Method
@method get
--[=[ Returns the match at the given index, if one exists.
@within RegexCaptures
@tag Method
Returns the match at the given index, if one exists. @param index -- The index of the match to get
@return RegexMatch -- The match, if one exists
]=]
@param index -- The index of the match to get get: (self: RegexCaptures, index: number) -> RegexMatch?,
@return RegexMatch -- The match, if one exists
]=]
function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch?
return nil :: any
end
--[=[ --[=[
@within RegexCaptures @within RegexCaptures
@tag Method @tag Method
@method group
Returns the match for the given named match group, if one exists. Returns the match for the given named match group, if one exists.
@param group -- The name of the group to get @param group -- The name of the group to get
@return RegexMatch -- The match, if one exists @return RegexMatch -- The match, if one exists
]=] ]=]
function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch? group: (self: RegexCaptures, group: string) -> RegexMatch?,
return nil :: any
end
--[=[ --[=[
@within RegexCaptures @within RegexCaptures
@tag Method @tag Method
@method format
Formats the captures using the given format string. Formats the captures using the given format string.
### Example usage ### Example usage
```lua ```lua
local regex = require("@lune/regex") local regex = require("@lune/regex")
local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})") local re = regex.new("(?<day>[0-9]{2})-(?<month>[0-9]{2})-(?<year>[0-9]{4})")
local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb."); local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb.");
assert(caps ~= nil, "Example pattern should match example text") assert(caps ~= nil, "Example pattern should match example text")
local formatted = caps:format("year=$year, month=$month, day=$day") local formatted = caps:format("year=$year, month=$month, day=$day")
print(formatted) -- "year=2010, month=03, day=14" print(formatted) -- "year=2010, month=03, day=14"
``` ```
@param format -- The format string to use @param format -- The format string to use
@return string -- The formatted string @return string -- The formatted string
]=] ]=]
function RegexCaptures.format(self: RegexCaptures, format: string): string format: (self: RegexCaptures, format: string) -> string,
return nil :: any },
end {} :: {
__len: (self: RegexCaptures) -> number,
export type RegexCaptures = typeof(RegexCaptures) }
))
local Regex = {} local Regex = {}