Remove old lune lib

This commit is contained in:
Filip Tibell 2023-08-20 19:18:16 -05:00
parent 90fce384d0
commit ebdc8a6261
53 changed files with 0 additions and 5919 deletions

View file

@ -1,123 +0,0 @@
use std::io::ErrorKind as IoErrorKind;
use std::path::{PathBuf, MAIN_SEPARATOR};
use mlua::prelude::*;
use tokio::fs;
use crate::lune_temp::lua::{
fs::{copy, FsMetadata, FsWriteOptions},
table::TableBuilder,
};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_async_function("readFile", fs_read_file)?
.with_async_function("readDir", fs_read_dir)?
.with_async_function("writeFile", fs_write_file)?
.with_async_function("writeDir", fs_write_dir)?
.with_async_function("removeFile", fs_remove_file)?
.with_async_function("removeDir", fs_remove_dir)?
.with_async_function("metadata", fs_metadata)?
.with_async_function("isFile", fs_is_file)?
.with_async_function("isDir", fs_is_dir)?
.with_async_function("move", fs_move)?
.with_async_function("copy", fs_copy)?
.build_readonly()
}
async fn fs_read_file(lua: &Lua, path: String) -> LuaResult<LuaString> {
let bytes = fs::read(&path).await.into_lua_err()?;
lua.create_string(bytes)
}
async fn fs_read_dir(_: &Lua, path: String) -> LuaResult<Vec<String>> {
let mut dir_strings = Vec::new();
let mut dir = fs::read_dir(&path).await.into_lua_err()?;
while let Some(dir_entry) = dir.next_entry().await.into_lua_err()? {
if let Some(dir_path_str) = dir_entry.path().to_str() {
dir_strings.push(dir_path_str.to_owned());
} else {
return Err(LuaError::RuntimeError(format!(
"File path could not be converted into a string: '{}'",
dir_entry.path().display()
)));
}
}
let mut dir_string_prefix = path;
if !dir_string_prefix.ends_with(MAIN_SEPARATOR) {
dir_string_prefix.push(MAIN_SEPARATOR);
}
let dir_strings_no_prefix = dir_strings
.iter()
.map(|inner_path| {
inner_path
.trim()
.trim_start_matches(&dir_string_prefix)
.to_owned()
})
.collect::<Vec<_>>();
Ok(dir_strings_no_prefix)
}
async fn fs_write_file(_: &Lua, (path, contents): (String, LuaString<'_>)) -> LuaResult<()> {
fs::write(&path, &contents.as_bytes()).await.into_lua_err()
}
async fn fs_write_dir(_: &Lua, path: String) -> LuaResult<()> {
fs::create_dir_all(&path).await.into_lua_err()
}
async fn fs_remove_file(_: &Lua, path: String) -> LuaResult<()> {
fs::remove_file(&path).await.into_lua_err()
}
async fn fs_remove_dir(_: &Lua, path: String) -> LuaResult<()> {
fs::remove_dir_all(&path).await.into_lua_err()
}
async fn fs_metadata(_: &Lua, path: String) -> LuaResult<FsMetadata> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(FsMetadata::not_found()),
Ok(meta) => Ok(FsMetadata::from(meta)),
Err(e) => Err(e.into()),
}
}
async fn fs_is_file(_: &Lua, path: String) -> LuaResult<bool> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
Ok(meta) => Ok(meta.is_file()),
Err(e) => Err(e.into()),
}
}
async fn fs_is_dir(_: &Lua, path: String) -> LuaResult<bool> {
match fs::metadata(path).await {
Err(e) if e.kind() == IoErrorKind::NotFound => Ok(false),
Ok(meta) => Ok(meta.is_dir()),
Err(e) => Err(e.into()),
}
}
async fn fs_move(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
let path_from = PathBuf::from(from);
if !path_from.exists() {
return Err(LuaError::RuntimeError(format!(
"No file or directory exists at the path '{}'",
path_from.display()
)));
}
let path_to = PathBuf::from(to);
if !options.overwrite && path_to.exists() {
return Err(LuaError::RuntimeError(format!(
"A file or directory already exists at the path '{}'",
path_to.display()
)));
}
fs::rename(path_from, path_to).await.into_lua_err()?;
Ok(())
}
async fn fs_copy(_: &Lua, (from, to, options): (String, String, FsWriteOptions)) -> LuaResult<()> {
copy(from, to, options).await
}

View file

@ -1,39 +0,0 @@
use mlua::prelude::*;
use crate::lune_temp::lua::{
luau::{LuauCompileOptions, LuauLoadOptions},
table::TableBuilder,
};
const BYTECODE_ERROR_BYTE: u8 = 0;
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("compile", compile_source)?
.with_function("load", load_source)?
.build_readonly()
}
fn compile_source<'lua>(
lua: &'lua Lua,
(source, options): (LuaString<'lua>, LuauCompileOptions),
) -> LuaResult<LuaString<'lua>> {
let bytecode = options.into_compiler().compile(source);
match bytecode.first() {
Some(&BYTECODE_ERROR_BYTE) => Err(LuaError::RuntimeError(
String::from_utf8_lossy(&bytecode).into_owned(),
)),
Some(_) => lua.create_string(bytecode),
None => panic!("Compiling resulted in empty bytecode"),
}
}
fn load_source<'lua>(
lua: &'lua Lua,
(source, options): (LuaString<'lua>, LuauLoadOptions),
) -> LuaResult<LuaFunction<'lua>> {
lua.load(source.as_bytes())
.set_name(options.debug_name)
.into_function()
}

View file

@ -1,11 +0,0 @@
pub mod fs;
pub mod luau;
pub mod net;
pub mod process;
pub mod serde;
pub mod stdio;
pub mod task;
pub mod top_level;
#[cfg(feature = "roblox")]
pub mod roblox;

View file

@ -1,216 +0,0 @@
use std::collections::HashMap;
use mlua::prelude::*;
use console::style;
use hyper::{
header::{CONTENT_ENCODING, CONTENT_LENGTH},
Server,
};
use tokio::{sync::mpsc, task};
use crate::lune_temp::lua::{
net::{
NetClient, NetClientBuilder, NetLocalExec, NetService, NetWebSocket, RequestConfig,
ServeConfig,
},
serde::{decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat},
table::TableBuilder,
task::{TaskScheduler, TaskSchedulerAsyncExt},
};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
// Create a reusable client for performing our
// web requests and store it in the lua registry,
// allowing us to reuse headers and internal structs
let client = NetClientBuilder::new()
.headers(&[("User-Agent", create_user_agent_header())])?
.build()?;
lua.set_named_registry_value("net.client", client)?;
// Create the global table for net
TableBuilder::new(lua)?
.with_function("jsonEncode", net_json_encode)?
.with_function("jsonDecode", net_json_decode)?
.with_async_function("request", net_request)?
.with_async_function("socket", net_socket)?
.with_async_function("serve", net_serve)?
.with_function("urlEncode", net_url_encode)?
.with_function("urlDecode", net_url_decode)?
.build_readonly()
}
fn create_user_agent_header() -> String {
let (github_owner, github_repo) = env!("CARGO_PKG_REPOSITORY")
.trim_start_matches("https://github.com/")
.split_once('/')
.unwrap();
format!("{github_owner}-{github_repo}-cli")
}
fn net_json_encode<'lua>(
lua: &'lua Lua,
(val, pretty): (LuaValue<'lua>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
EncodeDecodeConfig::from((EncodeDecodeFormat::Json, pretty.unwrap_or_default()))
.serialize_to_string(lua, val)
}
fn net_json_decode<'lua>(lua: &'lua Lua, json: LuaString<'lua>) -> LuaResult<LuaValue<'lua>> {
EncodeDecodeConfig::from(EncodeDecodeFormat::Json).deserialize_from_string(lua, json)
}
async fn net_request<'lua>(
lua: &'static Lua,
config: RequestConfig<'lua>,
) -> LuaResult<LuaTable<'lua>> {
// Create and send the request
let client: LuaUserDataRef<NetClient> = lua.named_registry_value("net.client")?;
let mut request = client.request(config.method, &config.url);
for (query, value) in config.query {
request = request.query(&[(query.to_str()?, value.to_str()?)]);
}
for (header, value) in config.headers {
request = request.header(header.to_str()?, value.to_str()?);
}
let res = request
.body(config.body.unwrap_or_default())
.send()
.await
.into_lua_err()?;
// Extract status, headers
let res_status = res.status().as_u16();
let res_status_text = res.status().canonical_reason();
let mut res_headers = res
.headers()
.iter()
.map(|(name, value)| {
(
name.as_str().to_string(),
value.to_str().unwrap().to_owned(),
)
})
.collect::<HashMap<String, String>>();
// Read response bytes
let mut res_bytes = res.bytes().await.into_lua_err()?.to_vec();
// Check for extra options, decompression
if config.options.decompress {
// NOTE: Header names are guaranteed to be lowercase because of the above
// transformations of them into the hashmap, so we can compare directly
let format = res_headers.iter().find_map(|(name, val)| {
if name == CONTENT_ENCODING.as_str() {
CompressDecompressFormat::detect_from_header_str(val)
} else {
None
}
});
if let Some(format) = format {
res_bytes = decompress(format, res_bytes).await?;
let content_encoding_header_str = CONTENT_ENCODING.as_str();
let content_length_header_str = CONTENT_LENGTH.as_str();
res_headers.retain(|name, _| {
name != content_encoding_header_str && name != content_length_header_str
});
}
}
// Construct and return a readonly lua table with results
TableBuilder::new(lua)?
.with_value("ok", (200..300).contains(&res_status))?
.with_value("statusCode", res_status)?
.with_value("statusMessage", res_status_text)?
.with_value("headers", res_headers)?
.with_value("body", lua.create_string(&res_bytes)?)?
.build_readonly()
}
async fn net_socket<'lua>(lua: &'static Lua, url: String) -> LuaResult<LuaTable> {
let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?;
NetWebSocket::new(ws).into_lua_table(lua)
}
async fn net_serve<'lua>(
lua: &'static Lua,
(port, config): (u16, ServeConfig<'lua>),
) -> LuaResult<LuaTable<'lua>> {
// Note that we need to use a mpsc here and not
// a oneshot channel since we move the sender
// into our table with the stop function
let (shutdown_tx, mut shutdown_rx) = mpsc::channel::<()>(1);
let server_request_callback = lua.create_registry_value(config.handle_request)?;
let server_websocket_callback = config.handle_web_socket.map(|handler| {
lua.create_registry_value(handler)
.expect("Failed to store websocket handler")
});
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption");
// Bind first to make sure that we can bind to this address
let bound = match Server::try_bind(&([127, 0, 0, 1], port).into()) {
Err(e) => {
return Err(LuaError::external(format!(
"Failed to bind to localhost on port {port}\n{}",
format!("{e}").replace(
"error creating server listener: ",
&format!("{}", style("> ").dim())
)
)));
}
Ok(bound) => bound,
};
// Register a background task to prevent the task scheduler from
// exiting early and start up our web server on the bound address
let task = sched.register_background_task();
let server = bound
.http1_only(true) // Web sockets can only use http1
.http1_keepalive(true) // Web sockets must be kept alive
.executor(NetLocalExec)
.serve(NetService::new(
lua,
server_request_callback,
server_websocket_callback,
))
.with_graceful_shutdown(async move {
task.unregister(Ok(()));
shutdown_rx
.recv()
.await
.expect("Server was stopped instantly");
shutdown_rx.close();
});
// Spawn a new tokio task so we don't block
task::spawn_local(server);
// Create a new read-only table that contains methods
// for manipulating server behavior and shutting it down
let handle_stop = move |_, _: ()| match shutdown_tx.try_send(()) {
Ok(_) => Ok(()),
Err(_) => Err(LuaError::RuntimeError(
"Server has already been stopped".to_string(),
)),
};
TableBuilder::new(lua)?
.with_function("stop", handle_stop)?
.build_readonly()
}
fn net_url_encode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
if matches!(as_binary, Some(true)) {
urlencoding::encode_binary(lua_string.as_bytes()).into_lua(lua)
} else {
urlencoding::encode(lua_string.to_str()?).into_lua(lua)
}
}
fn net_url_decode<'lua>(
lua: &'lua Lua,
(lua_string, as_binary): (LuaString<'lua>, Option<bool>),
) -> LuaResult<LuaValue<'lua>> {
if matches!(as_binary, Some(true)) {
urlencoding::decode_binary(lua_string.as_bytes()).into_lua(lua)
} else {
urlencoding::decode(lua_string.to_str()?)
.map_err(|e| LuaError::RuntimeError(format!("Encountered invalid encoding - {e}")))?
.into_lua(lua)
}
}

View file

@ -1,296 +0,0 @@
use std::{
collections::HashMap,
env::{self, consts},
path::{self, PathBuf},
process::{ExitCode, Stdio},
};
use directories::UserDirs;
use dunce::canonicalize;
use mlua::prelude::*;
use os_str_bytes::RawOsString;
use tokio::process::Command;
use crate::lune_temp::lua::{
process::pipe_and_inherit_child_process_stdio, table::TableBuilder, task::TaskScheduler,
};
const PROCESS_EXIT_IMPL_LUA: &str = r#"
exit(...)
yield()
"#;
pub fn create(lua: &'static Lua, args_vec: Vec<String>) -> LuaResult<LuaTable> {
let cwd_str = {
let cwd = canonicalize(env::current_dir()?)?;
let cwd_str = cwd.to_string_lossy().to_string();
if !cwd_str.ends_with(path::MAIN_SEPARATOR) {
format!("{cwd_str}{}", path::MAIN_SEPARATOR)
} else {
cwd_str
}
};
// Create constants for OS & processor architecture
let os = lua.create_string(&consts::OS.to_lowercase())?;
let arch = lua.create_string(&consts::ARCH.to_lowercase())?;
// Create readonly args array
let args_tab = TableBuilder::new(lua)?
.with_sequential_values(args_vec)?
.build_readonly()?;
// Create proxied table for env that gets & sets real env vars
let env_tab = TableBuilder::new(lua)?
.with_metatable(
TableBuilder::new(lua)?
.with_function(LuaMetaMethod::Index.name(), process_env_get)?
.with_function(LuaMetaMethod::NewIndex.name(), process_env_set)?
.with_function(LuaMetaMethod::Iter.name(), process_env_iter)?
.build_readonly()?,
)?
.build_readonly()?;
// Create our process exit function, this is a bit involved since
// we have no way to yield from c / rust, we need to load a lua
// chunk that will set the exit code and yield for us instead
let process_exit_env_yield: LuaFunction = lua.named_registry_value("co.yield")?;
let process_exit_env_exit: LuaFunction = lua.create_function(|lua, code: Option<u8>| {
let exit_code = code.map_or(ExitCode::SUCCESS, ExitCode::from);
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler - make sure it is added as a lua app data before the first scheduler resumption");
sched.set_exit_code(exit_code);
Ok(())
})?;
let process_exit = lua
.load(PROCESS_EXIT_IMPL_LUA)
.set_name("=process.exit")
.set_environment(
TableBuilder::new(lua)?
.with_value("yield", process_exit_env_yield)?
.with_value("exit", process_exit_env_exit)?
.build_readonly()?,
)
.into_function()?;
// Create the full process table
TableBuilder::new(lua)?
.with_value("os", os)?
.with_value("arch", arch)?
.with_value("args", args_tab)?
.with_value("cwd", cwd_str)?
.with_value("env", env_tab)?
.with_value("exit", process_exit)?
.with_async_function("spawn", process_spawn)?
.build_readonly()
}
fn process_env_get<'lua>(
lua: &'lua Lua,
(_, key): (LuaValue<'lua>, String),
) -> LuaResult<LuaValue<'lua>> {
match env::var_os(key) {
Some(value) => {
let raw_value = RawOsString::new(value);
Ok(LuaValue::String(
lua.create_string(raw_value.as_raw_bytes())?,
))
}
None => Ok(LuaValue::Nil),
}
}
fn process_env_set<'lua>(
_: &'lua Lua,
(_, key, value): (LuaValue<'lua>, String, Option<String>),
) -> LuaResult<()> {
// Make sure key is valid, otherwise set_var will panic
if key.is_empty() {
Err(LuaError::RuntimeError("Key must not be empty".to_string()))
} else if key.contains('=') {
Err(LuaError::RuntimeError(
"Key must not contain the equals character '='".to_string(),
))
} else if key.contains('\0') {
Err(LuaError::RuntimeError(
"Key must not contain the NUL character".to_string(),
))
} else {
match value {
Some(value) => {
// Make sure value is valid, otherwise set_var will panic
if value.contains('\0') {
Err(LuaError::RuntimeError(
"Value must not contain the NUL character".to_string(),
))
} else {
env::set_var(&key, &value);
Ok(())
}
}
None => {
env::remove_var(&key);
Ok(())
}
}
}
}
fn process_env_iter<'lua>(
lua: &'lua Lua,
(_, _): (LuaValue<'lua>, ()),
) -> LuaResult<LuaFunction<'lua>> {
let mut vars = env::vars_os().collect::<Vec<_>>().into_iter();
lua.create_function_mut(move |lua, _: ()| match vars.next() {
Some((key, value)) => {
let raw_key = RawOsString::new(key);
let raw_value = RawOsString::new(value);
Ok((
LuaValue::String(lua.create_string(raw_key.as_raw_bytes())?),
LuaValue::String(lua.create_string(raw_value.as_raw_bytes())?),
))
}
None => Ok((LuaValue::Nil, LuaValue::Nil)),
})
}
async fn process_spawn<'lua>(
lua: &'static Lua,
(mut program, args, options): (String, Option<Vec<String>>, Option<LuaTable<'lua>>),
) -> LuaResult<LuaTable<'lua>> {
// Parse any given options or create defaults
let (child_cwd, child_envs, child_shell, child_stdio_inherit) = match options {
Some(options) => {
let mut cwd = env::current_dir()?;
let mut envs = HashMap::new();
let mut shell = None;
let mut inherit = false;
match options.raw_get("cwd")? {
LuaValue::Nil => {}
LuaValue::String(s) => {
cwd = PathBuf::from(s.to_string_lossy().to_string());
// Substitute leading tilde (~) for the actual home dir
if cwd.starts_with("~") {
if let Some(user_dirs) = UserDirs::new() {
cwd = user_dirs.home_dir().join(cwd.strip_prefix("~").unwrap())
}
};
if !cwd.exists() {
return Err(LuaError::RuntimeError(
"Invalid value for option 'cwd' - path does not exist".to_string(),
));
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'cwd' - expected 'string', got '{}'",
value.type_name()
)))
}
}
match options.raw_get("env")? {
LuaValue::Nil => {}
LuaValue::Table(t) => {
for pair in t.pairs::<String, String>() {
let (k, v) = pair?;
envs.insert(k, v);
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'env' - expected 'table', got '{}'",
value.type_name()
)))
}
}
match options.raw_get("shell")? {
LuaValue::Nil => {}
LuaValue::String(s) => shell = Some(s.to_string_lossy().to_string()),
LuaValue::Boolean(true) => {
shell = match env::consts::FAMILY {
"unix" => Some("/bin/sh".to_string()),
"windows" => Some("/bin/sh".to_string()),
_ => None,
};
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'shell' - expected 'true' or 'string', got '{}'",
value.type_name()
)))
}
}
match options.raw_get("stdio")? {
LuaValue::Nil => {}
LuaValue::String(s) => {
match s.to_str()? {
"inherit" => {
inherit = true;
},
"default" => {
inherit = false;
}
_ => return Err(LuaError::RuntimeError(
format!("Invalid value for option 'stdio' - expected 'inherit' or 'default', got '{}'", s.to_string_lossy()),
))
}
}
value => {
return Err(LuaError::RuntimeError(format!(
"Invalid type for option 'stdio' - expected 'string', got '{}'",
value.type_name()
)))
}
}
Ok::<_, LuaError>((cwd, envs, shell, inherit))
}
None => Ok((env::current_dir()?, HashMap::new(), None, false)),
}?;
// Run a shell using the command param if wanted
let child_args = if let Some(shell) = child_shell {
let shell_args = match args {
Some(args) => vec!["-c".to_string(), format!("{} {}", program, args.join(" "))],
None => vec!["-c".to_string(), program],
};
program = shell;
Some(shell_args)
} else {
args
};
// Create command with the wanted options
let mut cmd = match child_args {
None => Command::new(program),
Some(args) => {
let mut cmd = Command::new(program);
cmd.args(args);
cmd
}
};
// Set dir to run in and env variables
cmd.current_dir(child_cwd);
cmd.envs(child_envs);
// Spawn the child process
let child = cmd
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
// Inherit the output and stderr if wanted
let result = if child_stdio_inherit {
pipe_and_inherit_child_process_stdio(child).await
} else {
let output = child.wait_with_output().await?;
Ok((output.status, output.stdout, output.stderr))
};
// Extract result
let (status, stdout, stderr) = result?;
// NOTE: If an exit code was not given by the child process,
// we default to 1 if it yielded any error output, otherwise 0
let code = status.code().unwrap_or(match stderr.is_empty() {
true => 0,
false => 1,
});
// Construct and return a readonly lua table with results
TableBuilder::new(lua)?
.with_value("ok", code == 0)?
.with_value("code", code)?
.with_value("stdout", lua.create_string(&stdout)?)?
.with_value("stderr", lua.create_string(&stderr)?)?
.build_readonly()
}

View file

@ -1,104 +0,0 @@
use mlua::prelude::*;
use once_cell::sync::OnceCell;
use crate::roblox::{
self,
document::{Document, DocumentError, DocumentFormat, DocumentKind},
instance::Instance,
reflection::Database as ReflectionDatabase,
};
use tokio::task;
use crate::lune_temp::lua::table::TableBuilder;
static REFLECTION_DATABASE: OnceCell<ReflectionDatabase> = OnceCell::new();
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
let mut roblox_constants = Vec::new();
let roblox_module = roblox::module(lua)?;
for pair in roblox_module.pairs::<LuaValue, LuaValue>() {
roblox_constants.push(pair?);
}
TableBuilder::new(lua)?
.with_values(roblox_constants)?
.with_async_function("deserializePlace", deserialize_place)?
.with_async_function("deserializeModel", deserialize_model)?
.with_async_function("serializePlace", serialize_place)?
.with_async_function("serializeModel", serialize_model)?
.with_function("getAuthCookie", get_auth_cookie)?
.with_function("getReflectionDatabase", get_reflection_database)?
.build_readonly()
}
async fn deserialize_place<'lua>(
lua: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = contents.as_bytes().to_vec();
let fut = task::spawn_blocking(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Place)?;
let data_model = doc.into_data_model_instance()?;
Ok::<_, DocumentError>(data_model)
});
fut.await.into_lua_err()??.into_lua(lua)
}
async fn deserialize_model<'lua>(
lua: &'lua Lua,
contents: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = contents.as_bytes().to_vec();
let fut = task::spawn_blocking(move || {
let doc = Document::from_bytes(bytes, DocumentKind::Model)?;
let instance_array = doc.into_instance_array()?;
Ok::<_, DocumentError>(instance_array)
});
fut.await.into_lua_err()??.into_lua(lua)
}
async fn serialize_place<'lua>(
lua: &'lua Lua,
(data_model, as_xml): (LuaUserDataRef<'lua, Instance>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let data_model = (*data_model).clone();
let fut = task::spawn_blocking(move || {
let doc = Document::from_data_model_instance(data_model)?;
let bytes = doc.to_bytes_with_format(match as_xml {
Some(true) => DocumentFormat::Xml,
_ => DocumentFormat::Binary,
})?;
Ok::<_, DocumentError>(bytes)
});
let bytes = fut.await.into_lua_err()??;
lua.create_string(bytes)
}
async fn serialize_model<'lua>(
lua: &'lua Lua,
(instances, as_xml): (Vec<LuaUserDataRef<'lua, Instance>>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let instances = instances.iter().map(|i| (*i).clone()).collect();
let fut = task::spawn_blocking(move || {
let doc = Document::from_instance_array(instances)?;
let bytes = doc.to_bytes_with_format(match as_xml {
Some(true) => DocumentFormat::Xml,
_ => DocumentFormat::Binary,
})?;
Ok::<_, DocumentError>(bytes)
});
let bytes = fut.await.into_lua_err()??;
lua.create_string(bytes)
}
fn get_auth_cookie(_: &Lua, raw: Option<bool>) -> LuaResult<Option<String>> {
if matches!(raw, Some(true)) {
Ok(rbx_cookie::get_value())
} else {
Ok(rbx_cookie::get())
}
}
fn get_reflection_database(_: &Lua, _: ()) -> LuaResult<ReflectionDatabase> {
Ok(*REFLECTION_DATABASE.get_or_init(ReflectionDatabase::new))
}

View file

@ -1,49 +0,0 @@
use mlua::prelude::*;
use crate::lune_temp::lua::{
serde::{
compress, decompress, CompressDecompressFormat, EncodeDecodeConfig, EncodeDecodeFormat,
},
table::TableBuilder,
};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("encode", serde_encode)?
.with_function("decode", serde_decode)?
.with_async_function("compress", serde_compress)?
.with_async_function("decompress", serde_decompress)?
.build_readonly()
}
fn serde_encode<'lua>(
lua: &'lua Lua,
(format, val, pretty): (EncodeDecodeFormat, LuaValue<'lua>, Option<bool>),
) -> LuaResult<LuaString<'lua>> {
let config = EncodeDecodeConfig::from((format, pretty.unwrap_or_default()));
config.serialize_to_string(lua, val)
}
fn serde_decode<'lua>(
lua: &'lua Lua,
(format, str): (EncodeDecodeFormat, LuaString<'lua>),
) -> LuaResult<LuaValue<'lua>> {
let config = EncodeDecodeConfig::from(format);
config.deserialize_from_string(lua, str)
}
async fn serde_compress<'lua>(
lua: &'lua Lua,
(format, str): (CompressDecompressFormat, LuaString<'lua>),
) -> LuaResult<LuaString<'lua>> {
let bytes = compress(format, str).await?;
lua.create_string(bytes)
}
async fn serde_decompress<'lua>(
lua: &'lua Lua,
(format, str): (CompressDecompressFormat, LuaString<'lua>),
) -> LuaResult<LuaString<'lua>> {
let bytes = decompress(format, str).await?;
lua.create_string(bytes)
}

View file

@ -1,99 +0,0 @@
use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
use mlua::prelude::*;
use tokio::{
io::{self, AsyncWriteExt},
task,
};
use crate::lune_temp::lua::{
stdio::{
formatting::{
format_style, pretty_format_multi_value, style_from_color_str, style_from_style_str,
},
prompt::{PromptKind, PromptOptions, PromptResult},
},
table::TableBuilder,
};
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable> {
TableBuilder::new(lua)?
.with_function("color", |_, color: String| {
let ansi_string = format_style(style_from_color_str(&color)?);
Ok(ansi_string)
})?
.with_function("style", |_, style: String| {
let ansi_string = format_style(style_from_style_str(&style)?);
Ok(ansi_string)
})?
.with_function("format", |_, args: LuaMultiValue| {
pretty_format_multi_value(&args)
})?
.with_async_function("write", |_, s: LuaString| async move {
let mut stdout = io::stdout();
stdout.write_all(s.as_bytes()).await?;
stdout.flush().await?;
Ok(())
})?
.with_async_function("ewrite", |_, s: LuaString| async move {
let mut stderr = io::stderr();
stderr.write_all(s.as_bytes()).await?;
stderr.flush().await?;
Ok(())
})?
.with_async_function("prompt", |_, options: PromptOptions| async move {
task::spawn_blocking(move || prompt(options))
.await
.into_lua_err()?
})?
.build_readonly()
}
fn prompt_theme() -> ColorfulTheme {
ColorfulTheme::default()
}
fn prompt(options: PromptOptions) -> LuaResult<PromptResult> {
let theme = prompt_theme();
match options.kind {
PromptKind::Text => {
let input: String = Input::with_theme(&theme)
.allow_empty(true)
.with_prompt(options.text.unwrap_or_default())
.with_initial_text(options.default_string.unwrap_or_default())
.interact_text()?;
Ok(PromptResult::String(input))
}
PromptKind::Confirm => {
let mut prompt = Confirm::with_theme(&theme);
if let Some(b) = options.default_bool {
prompt.default(b);
};
let result = prompt
.with_prompt(&options.text.expect("Missing text in prompt options"))
.interact()?;
Ok(PromptResult::Boolean(result))
}
PromptKind::Select => {
let chosen = Select::with_theme(&prompt_theme())
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
Ok(match chosen {
Some(idx) => PromptResult::Index(idx + 1),
None => PromptResult::None,
})
}
PromptKind::MultiSelect => {
let chosen = MultiSelect::with_theme(&prompt_theme())
.with_prompt(&options.text.unwrap_or_default())
.items(&options.options.expect("Missing options in prompt options"))
.interact_opt()?;
Ok(match chosen {
None => PromptResult::None,
Some(indices) => {
PromptResult::Indices(indices.iter().map(|idx| *idx + 1).collect())
}
})
}
}
}

View file

@ -1,168 +0,0 @@
use mlua::prelude::*;
use crate::lune_temp::lua::{
async_ext::LuaAsyncExt,
table::TableBuilder,
task::{
LuaThreadOrFunction, LuaThreadOrTaskReference, TaskKind, TaskReference, TaskScheduler,
TaskSchedulerScheduleExt,
},
};
const SPAWN_IMPL_LUA: &str = r#"
scheduleNext(thread())
local task = scheduleNext(...)
yield()
return task
"#;
pub fn create(lua: &'static Lua) -> LuaResult<LuaTable<'static>> {
lua.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler in app data");
/*
1. Schedule the current thread at the front
2. Schedule the wanted task arg at the front,
the previous schedule now comes right after
3. Give control over to the scheduler, which will
resume the above tasks in order when its ready
The spawn function needs special treatment,
we need to yield right away to allow the
spawned task to run until first yield
*/
let task_spawn_env_yield: LuaFunction = lua.named_registry_value("co.yield")?;
let task_spawn = lua
.load(SPAWN_IMPL_LUA)
.set_name("task.spawn")
.set_environment(
TableBuilder::new(lua)?
.with_function("thread", |lua, _: ()| Ok(lua.current_thread()))?
.with_value("yield", task_spawn_env_yield)?
.with_function(
"scheduleNext",
|lua, (tof, args): (LuaThreadOrFunction, LuaMultiValue)| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_blocking(tof.into_thread(lua)?, args)
},
)?
.build_readonly()?,
)
.into_function()?;
// Functions in the built-in coroutine library also need to be
// replaced, these are a bit different than the ones above because
// calling resume or the function that wrap returns must return
// whatever lua value(s) that the thread or task yielded back
let globals = lua.globals();
let coroutine = globals.get::<_, LuaTable>("coroutine")?;
coroutine.set("status", lua.create_function(coroutine_status)?)?;
coroutine.set("resume", lua.create_function(coroutine_resume)?)?;
coroutine.set("wrap", lua.create_function(coroutine_wrap)?)?;
// All good, return the task scheduler lib
TableBuilder::new(lua)?
.with_value("wait", lua.create_waiter_function()?)?
.with_value("spawn", task_spawn)?
.with_function("cancel", task_cancel)?
.with_function("defer", task_defer)?
.with_function("delay", task_delay)?
.build_readonly()
}
/*
Basic task functions
*/
fn task_cancel(lua: &Lua, task: LuaUserDataRef<TaskReference>) -> LuaResult<()> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.remove_task(*task)?;
Ok(())
}
fn task_defer(
lua: &Lua,
(tof, args): (LuaThreadOrFunction, LuaMultiValue),
) -> LuaResult<TaskReference> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_blocking_deferred(tof.into_thread(lua)?, args)
}
fn task_delay(
lua: &Lua,
(secs, tof, args): (f64, LuaThreadOrFunction, LuaMultiValue),
) -> LuaResult<TaskReference> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched.schedule_blocking_after_seconds(secs, tof.into_thread(lua)?, args)
}
/*
Coroutine library overrides for compat with task scheduler
*/
fn coroutine_status<'a>(
lua: &'a Lua,
value: LuaThreadOrTaskReference<'a>,
) -> LuaResult<LuaString<'a>> {
Ok(match value {
LuaThreadOrTaskReference::Thread(thread) => {
let get_status: LuaFunction = lua.named_registry_value("co.status")?;
get_status.call(thread)?
}
LuaThreadOrTaskReference::TaskReference(task) => {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
sched
.get_task_status(task)
.unwrap_or_else(|| lua.create_string("dead").unwrap())
}
})
}
fn coroutine_resume<'lua>(
lua: &'lua Lua,
(value, arguments): (LuaThreadOrTaskReference, LuaMultiValue<'lua>),
) -> LuaResult<(bool, LuaMultiValue<'lua>)> {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
if sched.current_task().is_none() {
return Err(LuaError::RuntimeError(
"No current task to inherit".to_string(),
));
}
let current = sched.current_task().unwrap();
let result = match value {
LuaThreadOrTaskReference::Thread(t) => {
let task = sched.create_task(TaskKind::Instant, t, Some(arguments), true)?;
sched.resume_task(task, None)
}
LuaThreadOrTaskReference::TaskReference(t) => sched.resume_task(t, Some(Ok(arguments))),
};
sched.force_set_current_task(Some(current));
match result {
Ok(rets) => Ok((true, rets.1)),
Err(e) => Ok((false, e.into_lua_multi(lua)?)),
}
}
fn coroutine_wrap<'lua>(lua: &'lua Lua, func: LuaFunction) -> LuaResult<LuaFunction<'lua>> {
let task = lua.app_data_ref::<&TaskScheduler>().unwrap().create_task(
TaskKind::Instant,
lua.create_thread(func)?,
None,
false,
)?;
lua.create_function(move |lua, args: LuaMultiValue| {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
if sched.current_task().is_none() {
return Err(LuaError::RuntimeError(
"No current task to inherit".to_string(),
));
}
let current = sched.current_task().unwrap();
let result = lua
.app_data_ref::<&TaskScheduler>()
.unwrap()
.resume_task(task, Some(Ok(args)));
sched.force_set_current_task(Some(current));
match result {
Ok(rets) => Ok(rets.1),
Err(e) => Err(e),
}
})
}

View file

@ -1,80 +0,0 @@
use mlua::prelude::*;
use std::io::{self, Write as _};
#[cfg(feature = "roblox")]
use crate::roblox::datatypes::extension::RobloxUserdataTypenameExt;
use crate::lune_temp::lua::{
stdio::formatting::{format_label, pretty_format_multi_value},
task::TaskReference,
};
pub fn print(_: &Lua, args: LuaMultiValue) -> LuaResult<()> {
let formatted = format!("{}\n", pretty_format_multi_value(&args)?);
let mut stdout = io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
}
pub fn warn(_: &Lua, args: LuaMultiValue) -> LuaResult<()> {
let formatted = format!(
"{}\n{}",
format_label("warn"),
pretty_format_multi_value(&args)?
);
let mut stdout = io::stdout();
stdout.write_all(formatted.as_bytes())?;
stdout.flush()?;
Ok(())
}
// HACK: We need to preserve the default behavior of
// the lua error function, for pcall and such, which
// is really tricky to do from scratch so we will
// just proxy the default function here instead
pub fn error(lua: &Lua, (arg, level): (LuaValue, Option<u32>)) -> LuaResult<()> {
let error: LuaFunction = lua.named_registry_value("error")?;
let trace: LuaFunction = lua.named_registry_value("dbg.trace")?;
error.call((
LuaError::CallbackError {
traceback: format!("override traceback:{}", trace.call::<_, String>(())?),
cause: LuaError::external(format!(
"{}\n{}",
format_label("error"),
pretty_format_multi_value(&arg.into_lua_multi(lua)?)?
))
.into(),
},
level,
))?;
Ok(())
}
pub fn proxy_type<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult<LuaString<'lua>> {
if let LuaValue::UserData(u) = &value {
if u.is::<TaskReference>() {
return lua.create_string("thread");
}
}
lua.named_registry_value::<LuaFunction>("type")?.call(value)
}
pub fn proxy_typeof<'lua>(lua: &'lua Lua, value: LuaValue<'lua>) -> LuaResult<LuaString<'lua>> {
if let LuaValue::UserData(u) = &value {
if u.is::<TaskReference>() {
return lua.create_string("thread");
}
#[cfg(feature = "roblox")]
{
if let Some(type_name) = u.roblox_type_name() {
return lua.create_string(type_name);
}
}
}
lua.named_registry_value::<LuaFunction>("typeof")?
.call(value)
}
// TODO: Add an override for tostring that formats errors in a nicer way

View file

@ -1,83 +0,0 @@
use std::{
error::Error,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
use mlua::prelude::*;
use crate::lune_temp::lua::stdio::formatting::pretty_format_luau_error;
/**
An opaque error type for formatted lua errors.
*/
#[derive(Debug, Clone)]
pub struct LuneError {
error: LuaError,
disable_colors: bool,
}
impl LuneError {
/**
Enables colorization of the error message when formatted using the [`Display`] trait.
Colorization is enabled by default.
*/
#[doc(hidden)]
pub fn enable_colors(mut self) -> Self {
self.disable_colors = false;
self
}
/**
Disables colorization of the error message when formatted using the [`Display`] trait.
Colorization is enabled by default.
*/
#[doc(hidden)]
pub fn disable_colors(mut self) -> Self {
self.disable_colors = true;
self
}
/**
Returns `true` if the error can likely be fixed by appending more input to the source code.
See [`mlua::Error::SyntaxError`] for more information.
*/
pub fn is_incomplete_input(&self) -> bool {
matches!(
self.error,
LuaError::SyntaxError {
incomplete_input: true,
..
}
)
}
}
impl From<LuaError> for LuneError {
fn from(value: LuaError) -> Self {
Self {
error: value,
disable_colors: false,
}
}
}
impl Display for LuneError {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"{}",
pretty_format_luau_error(&self.error, !self.disable_colors)
)
}
}
impl Error for LuneError {
// TODO: Comment this out when we are ready to also re-export
// `mlua` as part of our public library interface in Lune
// fn cause(&self) -> Option<&dyn Error> {
// Some(&self.error)
// }
}

View file

@ -1,42 +0,0 @@
use mlua::prelude::*;
mod require;
mod require_waker;
use crate::lune_temp::builtins::{self, top_level};
pub fn create(lua: &'static Lua, args: Vec<String>) -> LuaResult<()> {
// Create all builtins
let builtins = vec![
("fs", builtins::fs::create(lua)?),
("net", builtins::net::create(lua)?),
("process", builtins::process::create(lua, args)?),
("serde", builtins::serde::create(lua)?),
("stdio", builtins::stdio::create(lua)?),
("task", builtins::task::create(lua)?),
("luau", builtins::luau::create(lua)?),
#[cfg(feature = "roblox")]
("roblox", builtins::roblox::create(lua)?),
];
// Create our importer (require) with builtins
let require_fn = require::create(lua, builtins)?;
// Create all top-level globals
let globals = vec![
("require", require_fn),
("print", lua.create_function(top_level::print)?),
("warn", lua.create_function(top_level::warn)?),
("error", lua.create_function(top_level::error)?),
("type", lua.create_function(top_level::proxy_type)?),
("typeof", lua.create_function(top_level::proxy_typeof)?),
];
// Set top-level globals
let lua_globals = lua.globals();
for (name, global) in globals {
lua_globals.set(name, global)?;
}
Ok(())
}

View file

@ -1,319 +0,0 @@
use std::{
cell::RefCell,
collections::{HashMap, HashSet},
env::current_dir,
path::{self, PathBuf},
sync::Arc,
};
use mlua::prelude::*;
use tokio::fs;
use tokio::sync::Mutex as AsyncMutex;
use crate::lune_temp::lua::{
table::TableBuilder,
task::{TaskScheduler, TaskSchedulerScheduleExt},
};
use super::require_waker::{RequireWakerFuture, RequireWakerState};
const REQUIRE_IMPL_LUA: &str = r#"
local source = info(1, "s")
if source == '[string "require"]' then
source = info(2, "s")
end
load(context, source, ...)
return yield()
"#;
type RequireWakersVec<'lua> = Vec<Arc<AsyncMutex<RequireWakerState<'lua>>>>;
fn append_extension_and_canonicalize(
path: impl Into<PathBuf>,
ext: &'static str,
) -> Result<PathBuf, std::io::Error> {
let mut new = path.into();
match new.extension() {
// FUTURE: There's probably a better way to do this than converting to a lossy string
Some(e) => new.set_extension(format!("{}.{ext}", e.to_string_lossy())),
None => new.set_extension(ext),
};
dunce::canonicalize(new)
}
#[derive(Debug, Clone, Default)]
struct RequireContext<'lua> {
// NOTE: We need to use arc here so that mlua clones
// the reference and not the entire inner value(s)
builtins: Arc<HashMap<String, LuaMultiValue<'lua>>>,
cached: Arc<RefCell<HashMap<String, LuaResult<LuaMultiValue<'lua>>>>>,
wakers: Arc<RefCell<HashMap<String, RequireWakersVec<'lua>>>>,
locks: Arc<RefCell<HashSet<String>>>,
pwd: String,
}
impl<'lua> RequireContext<'lua> {
pub fn new<K, V>(lua: &'lua Lua, builtins_vec: Vec<(K, V)>) -> LuaResult<Self>
where
K: Into<String>,
V: IntoLua<'lua>,
{
let mut pwd = current_dir()
.expect("Failed to access current working directory")
.to_string_lossy()
.to_string();
if !pwd.ends_with(path::MAIN_SEPARATOR) {
pwd = format!("{pwd}{}", path::MAIN_SEPARATOR)
}
let mut builtins = HashMap::new();
for (key, value) in builtins_vec {
builtins.insert(key.into(), value.into_lua_multi(lua)?);
}
Ok(Self {
pwd,
builtins: Arc::new(builtins),
..Default::default()
})
}
pub fn is_locked(&self, absolute_path: &str) -> bool {
self.locks.borrow().contains(absolute_path)
}
pub fn set_locked(&self, absolute_path: &str) -> bool {
self.locks.borrow_mut().insert(absolute_path.to_string())
}
pub fn set_unlocked(&self, absolute_path: &str) -> bool {
self.locks.borrow_mut().remove(absolute_path)
}
pub fn try_acquire_lock_sync(&self, absolute_path: &str) -> bool {
if self.is_locked(absolute_path) {
false
} else {
self.set_locked(absolute_path);
true
}
}
pub fn set_cached(&self, absolute_path: &str, result: &LuaResult<LuaMultiValue<'lua>>) {
self.cached
.borrow_mut()
.insert(absolute_path.to_string(), result.clone());
if let Some(wakers) = self.wakers.borrow_mut().remove(absolute_path) {
for waker in wakers {
waker
.try_lock()
.expect("Failed to lock waker")
.finalize(result.clone());
}
}
}
pub fn wait_for_cache(self, absolute_path: &str) -> RequireWakerFuture<'lua> {
let state = RequireWakerState::new();
let fut = RequireWakerFuture::new(&state);
self.wakers
.borrow_mut()
.entry(absolute_path.to_string())
.or_insert_with(Vec::new)
.push(Arc::clone(&state));
fut
}
pub fn get_paths(
&self,
require_source: String,
require_path: String,
) -> LuaResult<(String, String)> {
if require_path.starts_with('@') {
return Ok((require_path.clone(), require_path));
}
let path_relative_to_pwd = PathBuf::from(
&require_source
.trim_start_matches("[string \"")
.trim_end_matches("\"]"),
)
.parent()
.unwrap()
.join(&require_path);
// Try to normalize and resolve relative path segments such as './' and '../'
let file_path = match (
append_extension_and_canonicalize(&path_relative_to_pwd, "luau"),
append_extension_and_canonicalize(&path_relative_to_pwd, "lua"),
) {
(Ok(luau), _) => luau,
(_, Ok(lua)) => lua,
// If we did not find a luau/lua file at the wanted path,
// we should also look for "init" files in directories
_ => {
let init_dir_path = path_relative_to_pwd.join("init");
match (
append_extension_and_canonicalize(&init_dir_path, "luau"),
append_extension_and_canonicalize(&init_dir_path, "lua"),
) {
(Ok(luau), _) => luau,
(_, Ok(lua)) => lua,
_ => {
return Err(LuaError::RuntimeError(format!(
"File does not exist at path '{require_path}'"
)));
}
}
}
};
let absolute = file_path.to_string_lossy().to_string();
let relative = absolute.trim_start_matches(&self.pwd).to_string();
Ok((absolute, relative))
}
}
impl<'lua> LuaUserData for RequireContext<'lua> {}
fn load_builtin<'lua>(
_lua: &'lua Lua,
context: RequireContext<'lua>,
module_name: String,
_has_acquired_lock: bool,
) -> LuaResult<LuaMultiValue<'lua>> {
match context.builtins.get(&module_name) {
Some(module) => Ok(module.clone()),
None => Err(LuaError::RuntimeError(format!(
"No builtin module exists with the name '{}'",
module_name
))),
}
}
async fn load_file<'lua>(
lua: &'lua Lua,
context: RequireContext<'lua>,
absolute_path: String,
relative_path: String,
has_acquired_lock: bool,
) -> LuaResult<LuaMultiValue<'lua>> {
let cached = { context.cached.borrow().get(&absolute_path).cloned() };
match cached {
Some(cached) => cached,
None => {
if !has_acquired_lock {
return context.wait_for_cache(&absolute_path).await;
}
// Try to read the wanted file, note that we use bytes instead of reading
// to a string since lua scripts are not necessarily valid utf-8 strings
let contents = fs::read(&absolute_path).await.into_lua_err()?;
// Use a name without extensions for loading the chunk, some
// other code assumes the require path is without extensions
let path_relative_no_extension = relative_path
.trim_end_matches(".lua")
.trim_end_matches(".luau");
// Load the file into a thread
let loaded_func = lua
.load(contents)
.set_name(path_relative_no_extension)
.into_function()?;
let loaded_thread = lua.create_thread(loaded_func)?;
// Run the thread and wait for completion using the native task scheduler waker
let task_fut = {
let sched = lua.app_data_ref::<&TaskScheduler>().unwrap();
let task = sched.schedule_blocking(loaded_thread, LuaMultiValue::new())?;
sched.wait_for_task_completion(task)
};
// Wait for the thread to finish running, cache + return our result,
// notify any other threads that are also waiting on this to finish
let rets = task_fut.await;
context.set_cached(&absolute_path, &rets);
rets
}
}
}
async fn load<'lua>(
lua: &'lua Lua,
context: LuaUserDataRef<'lua, RequireContext<'lua>>,
absolute_path: String,
relative_path: String,
has_acquired_lock: bool,
) -> LuaResult<LuaMultiValue<'lua>> {
let result = if absolute_path == relative_path && absolute_path.starts_with('@') {
if let Some(module_name) = absolute_path.strip_prefix("@lune/") {
load_builtin(
lua,
context.clone(),
module_name.to_string(),
has_acquired_lock,
)
} else {
// FUTURE: '@' can be used a special prefix for users to set their own
// paths relative to a project file, similar to typescript paths config
// https://www.typescriptlang.org/tsconfig#paths
Err(LuaError::RuntimeError(
"Require paths prefixed by '@' are not yet supported".to_string(),
))
}
} else {
load_file(
lua,
context.clone(),
absolute_path.to_string(),
relative_path,
has_acquired_lock,
)
.await
};
if has_acquired_lock {
context.set_unlocked(&absolute_path);
}
result
}
pub fn create<K, V>(lua: &'static Lua, builtins: Vec<(K, V)>) -> LuaResult<LuaFunction>
where
K: Clone + Into<String>,
V: Clone + IntoLua<'static>,
{
let require_context = RequireContext::new(lua, builtins)?;
let require_yield: LuaFunction = lua.named_registry_value("co.yield")?;
let require_info: LuaFunction = lua.named_registry_value("dbg.info")?;
let require_print: LuaFunction = lua.named_registry_value("print")?;
let require_env = TableBuilder::new(lua)?
.with_value("context", require_context)?
.with_value("yield", require_yield)?
.with_value("info", require_info)?
.with_value("print", require_print)?
.with_function(
"load",
|lua,
(context, require_source, require_path): (
LuaUserDataRef<RequireContext>,
String,
String,
)| {
let (absolute_path, relative_path) =
context.get_paths(require_source, require_path)?;
// NOTE: We can not acquire the lock in the async part of the require
// load process since several requires may have happened for the
// same path before the async load task even gets a chance to run
let has_lock = context.try_acquire_lock_sync(&absolute_path);
let fut = load(lua, context, absolute_path, relative_path, has_lock);
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler as a lua app data");
sched.queue_async_task_inherited(lua.current_thread(), None, async {
let rets = fut.await?;
let mult = rets.into_lua_multi(lua)?;
Ok(Some(mult))
})
},
)?
.build_readonly()?;
let require_fn_lua = lua
.load(REQUIRE_IMPL_LUA)
.set_name("require")
.set_environment(require_env)
.into_function()?;
Ok(require_fn_lua)
}

View file

@ -1,66 +0,0 @@
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll, Waker},
};
use tokio::sync::Mutex as AsyncMutex;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub(super) struct RequireWakerState<'lua> {
rets: Option<LuaResult<LuaMultiValue<'lua>>>,
waker: Option<Waker>,
}
impl<'lua> RequireWakerState<'lua> {
pub fn new() -> Arc<AsyncMutex<Self>> {
Arc::new(AsyncMutex::new(RequireWakerState {
rets: None,
waker: None,
}))
}
pub fn finalize(&mut self, rets: LuaResult<LuaMultiValue<'lua>>) {
self.rets = Some(rets);
if let Some(waker) = self.waker.take() {
waker.wake();
}
}
}
#[derive(Debug)]
pub(super) struct RequireWakerFuture<'lua> {
state: Arc<AsyncMutex<RequireWakerState<'lua>>>,
}
impl<'lua> RequireWakerFuture<'lua> {
pub fn new(state: &Arc<AsyncMutex<RequireWakerState<'lua>>>) -> Self {
Self {
state: Arc::clone(state),
}
}
}
impl<'lua> Clone for RequireWakerFuture<'lua> {
fn clone(&self) -> Self {
Self {
state: Arc::clone(&self.state),
}
}
}
impl<'lua> Future for RequireWakerFuture<'lua> {
type Output = LuaResult<LuaMultiValue<'lua>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut shared_state = self.state.try_lock().unwrap();
if let Some(rets) = shared_state.rets.clone() {
Poll::Ready(rets)
} else {
shared_state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}

View file

@ -1,93 +0,0 @@
use async_trait::async_trait;
use futures_util::Future;
use mlua::prelude::*;
use crate::lune_temp::{lua::table::TableBuilder, lua::task::TaskScheduler};
use super::task::TaskSchedulerAsyncExt;
const ASYNC_IMPL_LUA: &str = r#"
resumeAsync(...)
return yield()
"#;
const WAIT_IMPL_LUA: &str = r#"
resumeAfter(...)
return yield()
"#;
#[async_trait(?Send)]
pub trait LuaAsyncExt {
fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult<LuaFunction<'lua>>
where
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'lua Lua, A) -> FR,
FR: 'static + Future<Output = LuaResult<R>>;
fn create_waiter_function<'lua>(self) -> LuaResult<LuaFunction<'lua>>;
}
impl LuaAsyncExt for &'static Lua {
/**
Creates a function callable from Lua that runs an async
closure and returns the results of it to the call site.
*/
fn create_async_function<'lua, A, R, F, FR>(self, func: F) -> LuaResult<LuaFunction<'lua>>
where
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'lua Lua, A) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?;
let async_env = TableBuilder::new(self)?
.with_value("yield", async_env_yield)?
.with_function("resumeAsync", move |lua: &Lua, args: A| {
let thread = lua.current_thread();
let fut = func(lua, args);
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler as a lua app data");
sched.queue_async_task(thread, None, async {
let rets = fut.await?;
let mult = rets.into_lua_multi(lua)?;
Ok(Some(mult))
})
})?
.build_readonly()?;
let async_func = self
.load(ASYNC_IMPL_LUA)
.set_name("async")
.set_environment(async_env)
.into_function()?;
Ok(async_func)
}
/**
Creates a special async function that waits the
desired amount of time, inheriting the guid of the
current thread / task for proper cancellation.
This will yield the lua thread calling the function until the
desired time has passed and the scheduler resumes the thread.
*/
fn create_waiter_function<'lua>(self) -> LuaResult<LuaFunction<'lua>> {
let async_env_yield: LuaFunction = self.named_registry_value("co.yield")?;
let async_env = TableBuilder::new(self)?
.with_value("yield", async_env_yield)?
.with_function("resumeAfter", move |lua: &Lua, duration: Option<f64>| {
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler as a lua app data");
sched.schedule_wait(lua.current_thread(), duration)
})?
.build_readonly()?;
let async_func = self
.load(WAIT_IMPL_LUA)
.set_name("wait")
.set_environment(async_env)
.into_function()?;
Ok(async_func)
}
}

View file

@ -1,184 +0,0 @@
use mlua::{prelude::*, Compiler as LuaCompiler};
/*
- Level 0 is the call to info
- Level 1 is the load call in create() below where we load this into a function
- Level 2 is the call to the trace, which we also want to skip, so start at 3
Also note that we must match the mlua traceback format here so that we
can pattern match and beautify it properly later on when outputting it
*/
const TRACE_IMPL_LUA: &str = r#"
local lines = {}
for level = 3, 16 do
local parts = {}
local source, line, name = info(level, "sln")
if source then
push(parts, source)
else
break
end
if line == -1 then
line = nil
end
if name and #name <= 0 then
name = nil
end
if line then
push(parts, format("%d", line))
end
if name and #parts > 1 then
push(parts, format(" in function '%s'", name))
elseif name then
push(parts, format("in function '%s'", name))
end
if #parts > 0 then
push(lines, concat(parts, ":"))
end
end
if #lines > 0 then
return concat(lines, "\n")
else
return nil
end
"#;
/**
Stores the following globals in the Lua registry:
| Registry Name | Global |
|-----------------|-------------------|
| `"print"` | `print` |
| `"error"` | `error` |
| `"type"` | `type` |
| `"typeof"` | `typeof` |
| `"pcall"` | `pcall` |
| `"xpcall"` | `xpcall` |
| `"tostring"` | `tostring` |
| `"tonumber"` | `tonumber` |
| `"co.yield"` | `coroutine.yield` |
| `"co.close"` | `coroutine.close` |
| `"tab.pack"` | `table.pack` |
| `"tab.unpack"` | `table.unpack` |
| `"tab.freeze"` | `table.freeze` |
| `"tab.getmeta"` | `getmetatable` |
| `"tab.setmeta"` | `setmetatable` |
| `"dbg.info"` | `debug.info` |
| `"dbg.trace"` | `debug.traceback` |
These globals can then be modified safely from other runtime code.
*/
fn store_globals_in_registry(lua: &Lua) -> LuaResult<()> {
// Extract some global tables that we will extract
// built-in functions from and store in the registry
let globals = lua.globals();
let debug: LuaTable = globals.get("debug")?;
let string: LuaTable = globals.get("string")?;
let table: LuaTable = globals.get("table")?;
let coroutine: LuaTable = globals.get("coroutine")?;
// Store original lua global functions in the registry so we can use
// them later without passing them around and dealing with lifetimes
lua.set_named_registry_value("print", globals.get::<_, LuaFunction>("print")?)?;
lua.set_named_registry_value("error", globals.get::<_, LuaFunction>("error")?)?;
lua.set_named_registry_value("type", globals.get::<_, LuaFunction>("type")?)?;
lua.set_named_registry_value("typeof", globals.get::<_, LuaFunction>("typeof")?)?;
lua.set_named_registry_value("xpcall", globals.get::<_, LuaFunction>("xpcall")?)?;
lua.set_named_registry_value("pcall", globals.get::<_, LuaFunction>("pcall")?)?;
lua.set_named_registry_value("tostring", globals.get::<_, LuaFunction>("tostring")?)?;
lua.set_named_registry_value("tonumber", globals.get::<_, LuaFunction>("tonumber")?)?;
lua.set_named_registry_value("co.status", coroutine.get::<_, LuaFunction>("status")?)?;
lua.set_named_registry_value("co.yield", coroutine.get::<_, LuaFunction>("yield")?)?;
lua.set_named_registry_value("co.close", coroutine.get::<_, LuaFunction>("close")?)?;
lua.set_named_registry_value("dbg.info", debug.get::<_, LuaFunction>("info")?)?;
lua.set_named_registry_value("tab.pack", table.get::<_, LuaFunction>("pack")?)?;
lua.set_named_registry_value("tab.unpack", table.get::<_, LuaFunction>("unpack")?)?;
lua.set_named_registry_value("tab.freeze", table.get::<_, LuaFunction>("freeze")?)?;
lua.set_named_registry_value(
"tab.getmeta",
globals.get::<_, LuaFunction>("getmetatable")?,
)?;
lua.set_named_registry_value(
"tab.setmeta",
globals.get::<_, LuaFunction>("setmetatable")?,
)?;
// Create a trace function that can be called to obtain a full stack trace from
// lua, this is not possible to do from rust when using our manual scheduler
let dbg_trace_env = lua.create_table_with_capacity(0, 1)?;
dbg_trace_env.set("info", debug.get::<_, LuaFunction>("info")?)?;
dbg_trace_env.set("push", table.get::<_, LuaFunction>("insert")?)?;
dbg_trace_env.set("concat", table.get::<_, LuaFunction>("concat")?)?;
dbg_trace_env.set("format", string.get::<_, LuaFunction>("format")?)?;
let dbg_trace_fn = lua
.load(TRACE_IMPL_LUA)
.set_name("=dbg.trace")
.set_environment(dbg_trace_env)
.into_function()?;
lua.set_named_registry_value("dbg.trace", dbg_trace_fn)?;
Ok(())
}
/**
Sets the `_VERSION` global to a value matching the string `Lune x.y.z+w` where
`x.y.z` is the current Lune runtime version and `w` is the current Luau version
*/
fn set_global_version(lua: &Lua) -> LuaResult<()> {
let luau_version_full = lua
.globals()
.get::<_, LuaString>("_VERSION")
.expect("Missing _VERSION global");
let luau_version = luau_version_full
.to_str()?
.strip_prefix("Luau 0.")
.expect("_VERSION global is formatted incorrectly")
.trim();
if luau_version.is_empty() {
panic!("_VERSION global is missing version number")
}
lua.globals().set(
"_VERSION",
lua.create_string(&format!(
"Lune {lune}+{luau}",
lune = env!("CARGO_PKG_VERSION"),
luau = luau_version,
))?,
)
}
/**
Creates a _G table that is separate from our built-in globals
*/
fn set_global_table(lua: &Lua) -> LuaResult<()> {
lua.globals().set("_G", lua.create_table()?)
}
/**
Enables JIT and sets default compiler settings for the Lua struct.
*/
fn init_compiler_settings(lua: &Lua) {
lua.enable_jit(true);
lua.set_compiler(
LuaCompiler::default()
.set_coverage_level(0)
.set_debug_level(1)
.set_optimization_level(1),
);
}
/**
Creates a new [`mlua::Lua`] struct with compiler,
registry, and globals customized for the Lune runtime.
Refer to the source code for additional details and specifics.
*/
pub fn create() -> LuaResult<Lua> {
let lua = Lua::new();
init_compiler_settings(&lua);
store_globals_in_registry(&lua)?;
set_global_version(&lua)?;
set_global_table(&lua)?;
Ok(lua)
}

View file

@ -1,155 +0,0 @@
use std::collections::VecDeque;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use mlua::prelude::*;
use tokio::fs;
use super::FsWriteOptions;
pub struct CopyContents {
// Vec<(relative depth, path)>
pub dirs: Vec<(usize, PathBuf)>,
pub files: Vec<(usize, PathBuf)>,
}
async fn get_contents_at(root: PathBuf, _options: FsWriteOptions) -> LuaResult<CopyContents> {
let mut dirs = Vec::new();
let mut files = Vec::new();
let mut queue = VecDeque::new();
let normalized_root = fs::canonicalize(&root).await.map_err(|e| {
LuaError::RuntimeError(format!("Failed to canonicalize root directory path\n{e}"))
})?;
// Push initial children of the root path into the queue
let mut entries = fs::read_dir(&normalized_root).await?;
while let Some(entry) = entries.next_entry().await? {
queue.push_back((1, entry.path()));
}
// Go through the current queue, pushing to it
// when we find any new descendant directories
// FUTURE: Try to do async reading here concurrently to speed it up a bit
while let Some((current_depth, current_path)) = queue.pop_front() {
let meta = fs::metadata(&current_path).await?;
if meta.is_symlink() {
return Err(LuaError::RuntimeError(format!(
"Symlinks are not yet supported, encountered at path '{}'",
current_path.display()
)));
} else if meta.is_dir() {
// FUTURE: Add an option in FsWriteOptions for max depth and limit it here
let mut entries = fs::read_dir(&current_path).await?;
while let Some(entry) = entries.next_entry().await? {
queue.push_back((current_depth + 1, entry.path()));
}
dirs.push((current_depth, current_path));
} else {
files.push((current_depth, current_path));
}
}
// Ensure that all directory and file paths are relative to the root path
// SAFETY: Since we only ever push dirs and files relative to the root, unwrap is safe
for (_, dir) in dirs.iter_mut() {
*dir = dir.strip_prefix(&normalized_root).unwrap().to_path_buf()
}
for (_, file) in files.iter_mut() {
*file = file.strip_prefix(&normalized_root).unwrap().to_path_buf()
}
// FUTURE: Deduplicate paths such that these directories:
// - foo/
// - foo/bar/
// - foo/bar/baz/
// turn into a single foo/bar/baz/ and let create_dir_all do the heavy lifting
Ok(CopyContents { dirs, files })
}
async fn ensure_no_dir_exists(path: impl AsRef<Path>) -> LuaResult<()> {
let path = path.as_ref();
match fs::metadata(&path).await {
Ok(meta) if meta.is_dir() => Err(LuaError::RuntimeError(format!(
"A directory already exists at the path '{}'",
path.display()
))),
_ => Ok(()),
}
}
async fn ensure_no_file_exists(path: impl AsRef<Path>) -> LuaResult<()> {
let path = path.as_ref();
match fs::metadata(&path).await {
Ok(meta) if meta.is_file() => Err(LuaError::RuntimeError(format!(
"A file already exists at the path '{}'",
path.display()
))),
_ => Ok(()),
}
}
pub async fn copy(
source: impl AsRef<Path>,
target: impl AsRef<Path>,
options: FsWriteOptions,
) -> LuaResult<()> {
let source = source.as_ref();
let target = target.as_ref();
// Check if we got a file or directory - we will handle them differently below
let (is_dir, is_file) = match fs::metadata(&source).await {
Ok(meta) => (meta.is_dir(), meta.is_file()),
Err(e) if e.kind() == ErrorKind::NotFound => {
return Err(LuaError::RuntimeError(format!(
"No file or directory exists at the path '{}'",
source.display()
)))
}
Err(e) => return Err(e.into()),
};
if !is_file && !is_dir {
return Err(LuaError::RuntimeError(format!(
"The given path '{}' is not a file or a directory",
source.display()
)));
}
// Perform copying:
//
// 1. If we are not allowed to overwrite, make sure nothing exists at the target path
// 2. If we are allowed to overwrite, remove any previous entry at the path
// 3. Write all directories first
// 4. Write all files
if !options.overwrite {
if is_file {
ensure_no_file_exists(target).await?;
} else if is_dir {
ensure_no_dir_exists(target).await?;
}
}
if is_file {
fs::copy(source, target).await?;
} else if is_dir {
let contents = get_contents_at(source.to_path_buf(), options).await?;
if options.overwrite {
fs::remove_dir_all(target).await?;
}
// FUTURE: Write dirs / files concurrently
// to potentially speed these operations up
for (_, dir) in &contents.dirs {
fs::create_dir_all(target.join(dir)).await?;
}
for (_, file) in &contents.files {
fs::copy(source.join(file), target.join(file)).await?;
}
}
Ok(())
}

View file

@ -1,153 +0,0 @@
use std::{
fmt,
fs::{FileType as StdFileType, Metadata as StdMetadata, Permissions as StdPermissions},
io::Result as IoResult,
str::FromStr,
time::SystemTime,
};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FsMetadataKind {
None,
File,
Dir,
Symlink,
}
impl fmt::Display for FsMetadataKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::None => "none",
Self::File => "file",
Self::Dir => "dir",
Self::Symlink => "symlink",
}
)
}
}
impl FromStr for FsMetadataKind {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_ref() {
"none" => Ok(Self::None),
"file" => Ok(Self::File),
"dir" => Ok(Self::Dir),
"symlink" => Ok(Self::Symlink),
_ => Err("Invalid metadata kind"),
}
}
}
impl From<StdFileType> for FsMetadataKind {
fn from(value: StdFileType) -> Self {
if value.is_file() {
Self::File
} else if value.is_dir() {
Self::Dir
} else if value.is_symlink() {
Self::Symlink
} else {
panic!("Encountered unknown filesystem filetype")
}
}
}
impl<'lua> IntoLua<'lua> for FsMetadataKind {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
if self == Self::None {
Ok(LuaValue::Nil)
} else {
self.to_string().into_lua(lua)
}
}
}
#[derive(Debug, Clone)]
pub struct FsPermissions {
pub(crate) read_only: bool,
}
impl From<StdPermissions> for FsPermissions {
fn from(value: StdPermissions) -> Self {
Self {
read_only: value.readonly(),
}
}
}
impl<'lua> IntoLua<'lua> for FsPermissions {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
let tab = lua.create_table_with_capacity(0, 1)?;
tab.set("readOnly", self.read_only)?;
tab.set_readonly(true);
Ok(LuaValue::Table(tab))
}
}
#[derive(Debug, Clone)]
pub struct FsMetadata {
pub(crate) kind: FsMetadataKind,
pub(crate) exists: bool,
pub(crate) created_at: Option<f64>,
pub(crate) modified_at: Option<f64>,
pub(crate) accessed_at: Option<f64>,
pub(crate) permissions: Option<FsPermissions>,
}
impl FsMetadata {
pub fn not_found() -> Self {
Self {
kind: FsMetadataKind::None,
exists: false,
created_at: None,
modified_at: None,
accessed_at: None,
permissions: None,
}
}
}
impl<'lua> IntoLua<'lua> for FsMetadata {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
let tab = lua.create_table_with_capacity(0, 5)?;
tab.set("kind", self.kind)?;
tab.set("exists", self.exists)?;
tab.set("createdAt", self.created_at)?;
tab.set("modifiedAt", self.modified_at)?;
tab.set("accessedAt", self.accessed_at)?;
tab.set("permissions", self.permissions)?;
tab.set_readonly(true);
Ok(LuaValue::Table(tab))
}
}
impl From<StdMetadata> for FsMetadata {
fn from(value: StdMetadata) -> Self {
Self {
kind: value.file_type().into(),
exists: true,
// FUTURE: Turn these into DateTime structs instead when that's implemented
created_at: system_time_to_timestamp(value.created()),
modified_at: system_time_to_timestamp(value.modified()),
accessed_at: system_time_to_timestamp(value.accessed()),
permissions: Some(FsPermissions::from(value.permissions())),
}
}
}
fn system_time_to_timestamp(res: IoResult<SystemTime>) -> Option<f64> {
match res {
Ok(t) => match t.duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => Some(d.as_secs_f64()),
Err(_) => None,
},
Err(_) => None,
}
}

View file

@ -1,7 +0,0 @@
mod copy;
mod metadata;
mod options;
pub use copy::copy;
pub use metadata::FsMetadata;
pub use options::FsWriteOptions;

View file

@ -1,31 +0,0 @@
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub struct FsWriteOptions {
pub(crate) overwrite: bool,
}
impl<'lua> FromLua<'lua> for FsWriteOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self { overwrite: false },
LuaValue::Boolean(b) => Self { overwrite: b },
LuaValue::Table(t) => {
let overwrite: Option<bool> = t.get("overwrite")?;
Self {
overwrite: overwrite.unwrap_or(false),
}
}
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "FsWriteOptions",
message: Some(format!(
"Invalid write options - expected boolean or table, got {}",
value.type_name()
)),
})
}
})
}
}

View file

@ -1,3 +0,0 @@
mod options;
pub use options::{LuauCompileOptions, LuauLoadOptions};

View file

@ -1,116 +0,0 @@
use mlua::prelude::*;
use mlua::Compiler as LuaCompiler;
const DEFAULT_DEBUG_NAME: &str = "luau.load(...)";
pub struct LuauCompileOptions {
pub(crate) optimization_level: u8,
pub(crate) coverage_level: u8,
pub(crate) debug_level: u8,
}
impl LuauCompileOptions {
pub fn into_compiler(self) -> LuaCompiler {
LuaCompiler::default()
.set_optimization_level(self.optimization_level)
.set_coverage_level(self.coverage_level)
.set_debug_level(self.debug_level)
}
}
impl Default for LuauCompileOptions {
fn default() -> Self {
// NOTE: This is the same as LuaCompiler::default() values, but they are
// not accessible from outside of mlua so we need to recreate them here.
Self {
optimization_level: 1,
coverage_level: 0,
debug_level: 1,
}
}
}
impl<'lua> FromLua<'lua> for LuauCompileOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self::default(),
LuaValue::Table(t) => {
let mut options = Self::default();
let get_and_check = |name: &'static str| -> LuaResult<Option<u8>> {
match t.get(name)? {
Some(n @ (0 | 1 | 2)) => Ok(Some(n)),
Some(n) => Err(LuaError::runtime(format!(
"'{name}' must be one of: 0, 1, or 2 - got {n}"
))),
None => Ok(None),
}
};
if let Some(optimization_level) = get_and_check("optimizationLevel")? {
options.optimization_level = optimization_level;
}
if let Some(coverage_level) = get_and_check("coverageLevel")? {
options.coverage_level = coverage_level;
}
if let Some(debug_level) = get_and_check("debugLevel")? {
options.debug_level = debug_level;
}
options
}
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "CompileOptions",
message: Some(format!(
"Invalid compile options - expected table, got {}",
value.type_name()
)),
})
}
})
}
}
pub struct LuauLoadOptions {
pub(crate) debug_name: String,
}
impl Default for LuauLoadOptions {
fn default() -> Self {
Self {
debug_name: DEFAULT_DEBUG_NAME.to_string(),
}
}
}
impl<'lua> FromLua<'lua> for LuauLoadOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
Ok(match value {
LuaValue::Nil => Self::default(),
LuaValue::Table(t) => {
let mut options = Self::default();
if let Some(debug_name) = t.get("debugName")? {
options.debug_name = debug_name;
}
options
}
LuaValue::String(s) => Self {
debug_name: s.to_string_lossy().to_string(),
},
_ => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "LoadOptions",
message: Some(format!(
"Invalid load options - expected string or table, got {}",
value.type_name()
)),
})
}
})
}
}

View file

@ -1,13 +0,0 @@
mod create;
pub mod async_ext;
pub mod fs;
pub mod luau;
pub mod net;
pub mod process;
pub mod serde;
pub mod stdio;
pub mod table;
pub mod task;
pub use create::create as create_lune_lua;

View file

@ -1,49 +0,0 @@
use std::str::FromStr;
use mlua::prelude::*;
use hyper::{header::HeaderName, http::HeaderValue, HeaderMap};
use reqwest::{IntoUrl, Method, RequestBuilder};
pub struct NetClientBuilder {
builder: reqwest::ClientBuilder,
}
impl NetClientBuilder {
pub fn new() -> NetClientBuilder {
Self {
builder: reqwest::ClientBuilder::new(),
}
}
pub fn headers<K, V>(mut self, headers: &[(K, V)]) -> LuaResult<Self>
where
K: AsRef<str>,
V: AsRef<[u8]>,
{
let mut map = HeaderMap::new();
for (key, val) in headers {
let hkey = HeaderName::from_str(key.as_ref()).into_lua_err()?;
let hval = HeaderValue::from_bytes(val.as_ref()).into_lua_err()?;
map.insert(hkey, hval);
}
self.builder = self.builder.default_headers(map);
Ok(self)
}
pub fn build(self) -> LuaResult<NetClient> {
let client = self.builder.build().into_lua_err()?;
Ok(NetClient(client))
}
}
#[derive(Debug, Clone)]
pub struct NetClient(reqwest::Client);
impl NetClient {
pub fn request<U: IntoUrl>(&self, method: Method, url: U) -> RequestBuilder {
self.0.request(method, url)
}
}
impl LuaUserData for NetClient {}

View file

@ -1,204 +0,0 @@
use std::collections::HashMap;
use mlua::prelude::*;
use reqwest::Method;
// Net request config
#[derive(Debug, Clone)]
pub struct RequestConfigOptions {
pub decompress: bool,
}
impl Default for RequestConfigOptions {
fn default() -> Self {
Self { decompress: true }
}
}
impl<'lua> FromLua<'lua> for RequestConfigOptions {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
// Nil means default options, table means custom options
if let LuaValue::Nil = value {
return Ok(Self::default());
} else if let LuaValue::Table(tab) = value {
// Extract flags
let decompress = match tab.raw_get::<_, Option<bool>>("decompress") {
Ok(decomp) => Ok(decomp.unwrap_or(true)),
Err(_) => Err(LuaError::RuntimeError(
"Invalid option value for 'decompress' in request config options".to_string(),
)),
}?;
return Ok(Self { decompress });
}
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfigOptions",
message: Some(format!(
"Invalid request config options - expected table or nil, got {}",
value.type_name()
)),
})
}
}
#[derive(Debug, Clone)]
pub struct RequestConfig<'a> {
pub url: String,
pub method: Method,
pub query: HashMap<LuaString<'a>, LuaString<'a>>,
pub headers: HashMap<LuaString<'a>, LuaString<'a>>,
pub body: Option<Vec<u8>>,
pub options: RequestConfigOptions,
}
impl<'lua> FromLua<'lua> for RequestConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
// If we just got a string we assume its a GET request to a given url
if let LuaValue::String(s) = value {
return Ok(Self {
url: s.to_string_lossy().to_string(),
method: Method::GET,
query: HashMap::new(),
headers: HashMap::new(),
body: None,
options: Default::default(),
});
}
// If we got a table we are able to configure the entire request
if let LuaValue::Table(tab) = value {
// Extract url
let url = match tab.raw_get::<_, LuaString>("url") {
Ok(config_url) => Ok(config_url.to_string_lossy().to_string()),
Err(_) => Err(LuaError::RuntimeError(
"Missing 'url' in request config".to_string(),
)),
}?;
// Extract method
let method = match tab.raw_get::<_, LuaString>("method") {
Ok(config_method) => config_method.to_string_lossy().trim().to_ascii_uppercase(),
Err(_) => "GET".to_string(),
};
// Extract query
let query = match tab.raw_get::<_, LuaTable>("query") {
Ok(config_headers) => {
let mut lua_headers = HashMap::new();
for pair in config_headers.pairs::<LuaString, LuaString>() {
let (key, value) = pair?.to_owned();
lua_headers.insert(key, value);
}
lua_headers
}
Err(_) => HashMap::new(),
};
// Extract headers
let headers = match tab.raw_get::<_, LuaTable>("headers") {
Ok(config_headers) => {
let mut lua_headers = HashMap::new();
for pair in config_headers.pairs::<LuaString, LuaString>() {
let (key, value) = pair?.to_owned();
lua_headers.insert(key, value);
}
lua_headers
}
Err(_) => HashMap::new(),
};
// Extract body
let body = match tab.raw_get::<_, LuaString>("body") {
Ok(config_body) => Some(config_body.as_bytes().to_owned()),
Err(_) => None,
};
// Convert method string into proper enum
let method = method.trim().to_ascii_uppercase();
let method = match method.as_ref() {
"GET" => Ok(Method::GET),
"POST" => Ok(Method::POST),
"PUT" => Ok(Method::PUT),
"DELETE" => Ok(Method::DELETE),
"HEAD" => Ok(Method::HEAD),
"OPTIONS" => Ok(Method::OPTIONS),
"PATCH" => Ok(Method::PATCH),
_ => Err(LuaError::RuntimeError(format!(
"Invalid request config method '{}'",
&method
))),
}?;
// Parse any extra options given
let options = match tab.raw_get::<_, LuaValue>("options") {
Ok(opts) => RequestConfigOptions::from_lua(opts, lua)?,
Err(_) => RequestConfigOptions::default(),
};
// All good, validated and we got what we need
return Ok(Self {
url,
method,
query,
headers,
body,
options,
});
};
// Anything else is invalid
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "RequestConfig",
message: Some(format!(
"Invalid request config - expected string or table, got {}",
value.type_name()
)),
})
}
}
// Net serve config
pub struct ServeConfig<'a> {
pub handle_request: LuaFunction<'a>,
pub handle_web_socket: Option<LuaFunction<'a>>,
}
impl<'lua> FromLua<'lua> for ServeConfig<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let message = match &value {
LuaValue::Function(f) => {
return Ok(ServeConfig {
handle_request: f.clone(),
handle_web_socket: None,
})
}
LuaValue::Table(t) => {
let handle_request: Option<LuaFunction> = t.raw_get("handleRequest")?;
let handle_web_socket: Option<LuaFunction> = t.raw_get("handleWebSocket")?;
if handle_request.is_some() || handle_web_socket.is_some() {
return Ok(ServeConfig {
handle_request: handle_request.unwrap_or_else(|| {
let chunk = r#"
return {
status = 426,
body = "Upgrade Required",
headers = {
Upgrade = "websocket",
},
}
"#;
lua.load(chunk)
.into_function()
.expect("Failed to create default http responder function")
}),
handle_web_socket,
});
} else {
Some("Missing handleRequest and / or handleWebSocket".to_string())
}
}
_ => None,
};
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "ServeConfig",
message,
})
}
}

View file

@ -1,11 +0,0 @@
mod client;
mod config;
mod response;
mod server;
mod websocket;
pub use client::{NetClient, NetClientBuilder};
pub use config::{RequestConfig, ServeConfig};
pub use response::{NetServeResponse, NetServeResponseKind};
pub use server::{NetLocalExec, NetService};
pub use websocket::NetWebSocket;

View file

@ -1,106 +0,0 @@
use std::collections::HashMap;
use hyper::{Body, Response};
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub enum NetServeResponseKind {
PlainText,
Table,
}
#[derive(Debug, Clone)]
pub struct NetServeResponse {
kind: NetServeResponseKind,
status: u16,
headers: HashMap<String, Vec<u8>>,
body: Option<Vec<u8>>,
}
impl NetServeResponse {
pub fn into_response(self) -> LuaResult<Response<Body>> {
Ok(match self.kind {
NetServeResponseKind::PlainText => Response::builder()
.status(200)
.header("Content-Type", "text/plain")
.body(Body::from(self.body.unwrap()))
.into_lua_err()?,
NetServeResponseKind::Table => {
let mut response = Response::builder();
for (key, value) in self.headers {
response = response.header(&key, value);
}
response
.status(self.status)
.body(Body::from(self.body.unwrap_or_default()))
.into_lua_err()?
}
})
}
}
impl<'lua> FromLua<'lua> for NetServeResponse {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
// Plain strings from the handler are plaintext responses
LuaValue::String(s) => Ok(Self {
kind: NetServeResponseKind::PlainText,
status: 200,
headers: HashMap::new(),
body: Some(s.as_bytes().to_vec()),
}),
// Tables are more detailed responses with potential status, headers, body
LuaValue::Table(t) => {
let status: Option<u16> = t.get("status")?;
let headers: Option<LuaTable> = t.get("headers")?;
let body: Option<LuaString> = t.get("body")?;
let mut headers_map = HashMap::new();
if let Some(headers) = headers {
for pair in headers.pairs::<String, LuaString>() {
let (h, v) = pair?;
headers_map.insert(h, v.as_bytes().to_vec());
}
}
let body_bytes = body.map(|s| s.as_bytes().to_vec());
Ok(Self {
kind: NetServeResponseKind::Table,
status: status.unwrap_or(200),
headers: headers_map,
body: body_bytes,
})
}
// Anything else is an error
value => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "NetServeResponse",
message: None,
}),
}
}
}
impl<'lua> IntoLua<'lua> for NetServeResponse {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
if self.headers.len() > i32::MAX as usize {
return Err(LuaError::ToLuaConversionError {
from: "NetServeResponse",
to: "table",
message: Some("Too many header values".to_string()),
});
}
let body = self.body.map(|b| lua.create_string(b)).transpose()?;
let headers = lua.create_table_with_capacity(0, self.headers.len())?;
for (key, value) in self.headers {
headers.set(key, lua.create_string(&value)?)?;
}
let table = lua.create_table_with_capacity(0, 3)?;
table.set("status", self.status)?;
table.set("headers", headers)?;
table.set("body", body)?;
table.set_readonly(true);
Ok(LuaValue::Table(table))
}
}

View file

@ -1,178 +0,0 @@
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use mlua::prelude::*;
use hyper::{body::to_bytes, server::conn::AddrStream, service::Service};
use hyper::{Body, Request, Response};
use hyper_tungstenite::{is_upgrade_request as is_ws_upgrade_request, upgrade as ws_upgrade};
use tokio::task;
use crate::lune_temp::{
lua::table::TableBuilder,
lua::task::{TaskScheduler, TaskSchedulerAsyncExt, TaskSchedulerScheduleExt},
};
use super::{NetServeResponse, NetWebSocket};
// Hyper service implementation for net, lots of boilerplate here
// but make_svc and make_svc_function do not work for what we need
pub struct NetServiceInner(
&'static Lua,
Arc<LuaRegistryKey>,
Arc<Option<LuaRegistryKey>>,
);
impl Service<Request<Body>> for NetServiceInner {
type Response = Response<Body>;
type Error = LuaError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, mut req: Request<Body>) -> Self::Future {
let lua = self.0;
if self.2.is_some() && is_ws_upgrade_request(&req) {
// Websocket upgrade request + websocket handler exists,
// we should now upgrade this connection to a websocket
// and then call our handler with a new socket object
let kopt = self.2.clone();
let key = kopt.as_ref().as_ref().unwrap();
let handler: LuaFunction = lua.registry_value(key).expect("Missing websocket handler");
let (response, ws) = ws_upgrade(&mut req, None).expect("Failed to upgrade websocket");
// This should be spawned as a registered task, otherwise
// the scheduler may exit early and cancel this even though what
// we want here is a long-running task that keeps the program alive
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler");
let task = sched.register_background_task();
task::spawn_local(async move {
// Create our new full websocket object, then
// schedule our handler to get called asap
let ws = ws.await.into_lua_err()?;
let sock = NetWebSocket::new(ws).into_lua_table(lua)?;
let sched = lua
.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler");
let result = sched.schedule_blocking(
lua.create_thread(handler)?,
LuaMultiValue::from_vec(vec![LuaValue::Table(sock)]),
);
task.unregister(Ok(()));
result
});
Box::pin(async move { Ok(response) })
} else {
// Got a normal http request or no websocket handler
// exists, just call the http request handler
let key = self.1.clone();
let (parts, body) = req.into_parts();
Box::pin(async move {
// Convert request body into bytes, extract handler
let bytes = to_bytes(body).await.into_lua_err()?;
let handler: LuaFunction = lua.registry_value(&key)?;
// Create a readonly table for the request query params
let query_params = TableBuilder::new(lua)?
.with_values(
parts
.uri
.query()
.unwrap_or_default()
.split('&')
.filter_map(|q| q.split_once('='))
.collect(),
)?
.build_readonly()?;
// Do the same for headers
let header_map = TableBuilder::new(lua)?
.with_values(
parts
.headers
.iter()
.map(|(name, value)| {
(name.to_string(), value.to_str().unwrap().to_string())
})
.collect(),
)?
.build_readonly()?;
// Create a readonly table with request info to pass to the handler
let request = TableBuilder::new(lua)?
.with_value("path", parts.uri.path())?
.with_value("query", query_params)?
.with_value("method", parts.method.as_str())?
.with_value("headers", header_map)?
.with_value("body", lua.create_string(&bytes)?)?
.build_readonly()?;
let response: LuaResult<NetServeResponse> = handler.call(request);
// Send below errors to task scheduler so that they can emit properly
let lua_error = match response {
Ok(r) => match r.into_response() {
Ok(res) => return Ok(res),
Err(err) => err,
},
Err(err) => err,
};
lua.app_data_ref::<&TaskScheduler>()
.expect("Missing task scheduler")
.forward_lua_error(lua_error);
Ok(Response::builder()
.status(500)
.body(Body::from("Internal Server Error"))
.unwrap())
})
}
}
}
pub struct NetService(
&'static Lua,
Arc<LuaRegistryKey>,
Arc<Option<LuaRegistryKey>>,
);
impl NetService {
pub fn new(
lua: &'static Lua,
callback_http: LuaRegistryKey,
callback_websocket: Option<LuaRegistryKey>,
) -> Self {
Self(lua, Arc::new(callback_http), Arc::new(callback_websocket))
}
}
impl Service<&AddrStream> for NetService {
type Response = NetServiceInner;
type Error = hyper::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, _: &mut Context) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, _: &AddrStream) -> Self::Future {
let lua = self.0;
let key1 = self.1.clone();
let key2 = self.2.clone();
Box::pin(async move { Ok(NetServiceInner(lua, key1, key2)) })
}
}
#[derive(Clone, Copy, Debug)]
pub struct NetLocalExec;
impl<F> hyper::rt::Executor<F> for NetLocalExec
where
F: std::future::Future + 'static, // not requiring `Send`
{
fn execute(&self, fut: F) {
task::spawn_local(fut);
}
}

View file

@ -1,229 +0,0 @@
use std::{cell::Cell, sync::Arc};
use hyper::upgrade::Upgraded;
use mlua::prelude::*;
use futures_util::{
stream::{SplitSink, SplitStream},
SinkExt, StreamExt,
};
use tokio::{
io::{AsyncRead, AsyncWrite},
net::TcpStream,
sync::Mutex as AsyncMutex,
};
use hyper_tungstenite::{
tungstenite::{
protocol::{frame::coding::CloseCode as WsCloseCode, CloseFrame as WsCloseFrame},
Message as WsMessage,
},
WebSocketStream,
};
use tokio_tungstenite::MaybeTlsStream;
use crate::lune_temp::lua::table::TableBuilder;
const WEB_SOCKET_IMPL_LUA: &str = r#"
return freeze(setmetatable({
close = function(...)
return close(websocket, ...)
end,
send = function(...)
return send(websocket, ...)
end,
next = function(...)
return next(websocket, ...)
end,
}, {
__index = function(self, key)
if key == "closeCode" then
return close_code(websocket)
end
end,
}))
"#;
#[derive(Debug)]
pub struct NetWebSocket<T> {
close_code: Arc<Cell<Option<u16>>>,
read_stream: Arc<AsyncMutex<SplitStream<WebSocketStream<T>>>>,
write_stream: Arc<AsyncMutex<SplitSink<WebSocketStream<T>, WsMessage>>>,
}
impl<T> Clone for NetWebSocket<T> {
fn clone(&self) -> Self {
Self {
close_code: Arc::clone(&self.close_code),
read_stream: Arc::clone(&self.read_stream),
write_stream: Arc::clone(&self.write_stream),
}
}
}
impl<T> NetWebSocket<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
pub fn new(value: WebSocketStream<T>) -> Self {
let (write, read) = value.split();
Self {
close_code: Arc::new(Cell::new(None)),
read_stream: Arc::new(AsyncMutex::new(read)),
write_stream: Arc::new(AsyncMutex::new(write)),
}
}
fn into_lua_table_with_env<'lua>(
lua: &'lua Lua,
env: LuaTable<'lua>,
) -> LuaResult<LuaTable<'lua>> {
lua.load(WEB_SOCKET_IMPL_LUA)
.set_name("websocket")
.set_environment(env)
.eval()
}
}
type NetWebSocketStreamClient = MaybeTlsStream<TcpStream>;
impl NetWebSocket<NetWebSocketStreamClient> {
pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult<LuaTable> {
let socket_env = TableBuilder::new(lua)?
.with_value("websocket", self)?
.with_function("close_code", close_code::<NetWebSocketStreamClient>)?
.with_async_function("close", close::<NetWebSocketStreamClient>)?
.with_async_function("send", send::<NetWebSocketStreamClient>)?
.with_async_function("next", next::<NetWebSocketStreamClient>)?
.with_value(
"setmetatable",
lua.named_registry_value::<LuaFunction>("tab.setmeta")?,
)?
.with_value(
"freeze",
lua.named_registry_value::<LuaFunction>("tab.freeze")?,
)?
.build_readonly()?;
Self::into_lua_table_with_env(lua, socket_env)
}
}
type NetWebSocketStreamServer = Upgraded;
impl NetWebSocket<NetWebSocketStreamServer> {
pub fn into_lua_table(self, lua: &'static Lua) -> LuaResult<LuaTable> {
let socket_env = TableBuilder::new(lua)?
.with_value("websocket", self)?
.with_function("close_code", close_code::<NetWebSocketStreamServer>)?
.with_async_function("close", close::<NetWebSocketStreamServer>)?
.with_async_function("send", send::<NetWebSocketStreamServer>)?
.with_async_function("next", next::<NetWebSocketStreamServer>)?
.with_value(
"setmetatable",
lua.named_registry_value::<LuaFunction>("tab.setmeta")?,
)?
.with_value(
"freeze",
lua.named_registry_value::<LuaFunction>("tab.freeze")?,
)?
.build_readonly()?;
Self::into_lua_table_with_env(lua, socket_env)
}
}
impl<T> LuaUserData for NetWebSocket<T> {}
fn close_code<'lua, T>(
_lua: &'lua Lua,
socket: LuaUserDataRef<'lua, NetWebSocket<T>>,
) -> LuaResult<LuaValue<'lua>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
Ok(match socket.close_code.get() {
Some(code) => LuaValue::Number(code as f64),
None => LuaValue::Nil,
})
}
async fn close<'lua, T>(
_lua: &'lua Lua,
(socket, code): (LuaUserDataRef<'lua, NetWebSocket<T>>, Option<u16>),
) -> LuaResult<()>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let mut ws = socket.write_stream.lock().await;
ws.send(WsMessage::Close(Some(WsCloseFrame {
code: match code {
Some(code) if (1000..=4999).contains(&code) => WsCloseCode::from(code),
Some(code) => {
return Err(LuaError::RuntimeError(format!(
"Close code must be between 1000 and 4999, got {code}"
)))
}
None => WsCloseCode::Normal,
},
reason: "".into(),
})))
.await
.into_lua_err()?;
let res = ws.close();
res.await.into_lua_err()
}
async fn send<'lua, T>(
_lua: &'lua Lua,
(socket, string, as_binary): (
LuaUserDataRef<'lua, NetWebSocket<T>>,
LuaString<'lua>,
Option<bool>,
),
) -> LuaResult<()>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let msg = if matches!(as_binary, Some(true)) {
WsMessage::Binary(string.as_bytes().to_vec())
} else {
let s = string.to_str().into_lua_err()?;
WsMessage::Text(s.to_string())
};
let mut ws = socket.write_stream.lock().await;
ws.send(msg).await.into_lua_err()
}
async fn next<'lua, T>(
lua: &'lua Lua,
socket: LuaUserDataRef<'lua, NetWebSocket<T>>,
) -> LuaResult<LuaValue<'lua>>
where
T: AsyncRead + AsyncWrite + Unpin,
{
let mut ws = socket.read_stream.lock().await;
let item = ws.next().await.transpose().into_lua_err();
let msg = match item {
Ok(Some(WsMessage::Close(msg))) => {
if let Some(msg) = &msg {
socket.close_code.replace(Some(msg.code.into()));
}
Ok(Some(WsMessage::Close(msg)))
}
val => val,
}?;
while let Some(msg) = &msg {
let msg_string_opt = match msg {
WsMessage::Binary(bin) => Some(lua.create_string(bin)?),
WsMessage::Text(txt) => Some(lua.create_string(txt)?),
// Stop waiting for next message if we get a close message
WsMessage::Close(_) => return Ok(LuaValue::Nil),
// Ignore ping/pong/frame messages, they are handled by tungstenite
_ => None,
};
if let Some(msg_string) = msg_string_opt {
return Ok(LuaValue::String(msg_string));
}
}
Ok(LuaValue::Nil)
}

View file

@ -1,47 +0,0 @@
use std::process::ExitStatus;
use mlua::prelude::*;
use tokio::{io, process::Child, task};
mod tee_writer;
use tee_writer::AsyncTeeWriter;
pub async fn pipe_and_inherit_child_process_stdio(
mut child: Child,
) -> LuaResult<(ExitStatus, Vec<u8>, Vec<u8>)> {
let mut child_stdout = child.stdout.take().unwrap();
let mut child_stderr = child.stderr.take().unwrap();
/*
NOTE: We do not need to register these
independent tasks spawning in the scheduler
This function is only used by `process.spawn` which in
turn registers a task with the scheduler that awaits this
*/
let stdout_thread = task::spawn(async move {
let mut stdout = io::stdout();
let mut tee = AsyncTeeWriter::new(&mut stdout);
io::copy(&mut child_stdout, &mut tee).await.into_lua_err()?;
Ok::<_, LuaError>(tee.into_vec())
});
let stderr_thread = task::spawn(async move {
let mut stderr = io::stderr();
let mut tee = AsyncTeeWriter::new(&mut stderr);
io::copy(&mut child_stderr, &mut tee).await.into_lua_err()?;
Ok::<_, LuaError>(tee.into_vec())
});
let status = child.wait().await.expect("Child process failed to start");
let stdout_buffer = stdout_thread.await.expect("Tee writer for stdout errored");
let stderr_buffer = stderr_thread.await.expect("Tee writer for stderr errored");
Ok::<_, LuaError>((status, stdout_buffer?, stderr_buffer?))
}

View file

@ -1,64 +0,0 @@
use std::{
io::Write,
pin::Pin,
task::{Context, Poll},
};
use pin_project::pin_project;
use tokio::io::{self, AsyncWrite};
#[pin_project]
pub struct AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{
#[pin]
writer: &'a mut W,
buffer: Vec<u8>,
}
impl<'a, W> AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{
pub fn new(writer: &'a mut W) -> Self {
Self {
writer,
buffer: Vec::new(),
}
}
pub fn into_vec(self) -> Vec<u8> {
self.buffer
}
}
impl<'a, W> AsyncWrite for AsyncTeeWriter<'a, W>
where
W: AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let mut this = self.project();
match this.writer.as_mut().poll_write(cx, buf) {
Poll::Ready(res) => {
this.buffer
.write_all(buf)
.expect("Failed to write to internal tee buffer");
Poll::Ready(res)
}
Poll::Pending => Poll::Pending,
}
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project().writer.as_mut().poll_flush(cx)
}
fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.project().writer.as_mut().poll_shutdown(cx)
}
}

View file

@ -1,162 +0,0 @@
use lz4_flex::{compress_prepend_size, decompress_size_prepended};
use mlua::prelude::*;
use tokio::{
io::{copy, BufReader},
task,
};
use async_compression::{
tokio::bufread::{
BrotliDecoder, BrotliEncoder, GzipDecoder, GzipEncoder, ZlibDecoder, ZlibEncoder,
},
Level::Best as CompressionQuality,
};
#[derive(Debug, Clone, Copy)]
pub enum CompressDecompressFormat {
Brotli,
GZip,
LZ4,
ZLib,
}
#[allow(dead_code)]
impl CompressDecompressFormat {
pub fn detect_from_bytes(bytes: impl AsRef<[u8]>) -> Option<Self> {
match bytes.as_ref() {
// https://github.com/PSeitz/lz4_flex/blob/main/src/frame/header.rs#L28
b if b.len() >= 4
&& matches!(
u32::from_le_bytes(b[0..4].try_into().unwrap()),
0x184D2204 | 0x184C2102
) =>
{
Some(Self::LZ4)
}
// https://github.com/dropbox/rust-brotli/blob/master/src/enc/brotli_bit_stream.rs#L2805
b if b.len() >= 4
&& matches!(
b[0..3],
[0xE1, 0x97, 0x81] | [0xE1, 0x97, 0x82] | [0xE1, 0x97, 0x80]
) =>
{
Some(Self::Brotli)
}
// https://github.com/rust-lang/flate2-rs/blob/main/src/gz/mod.rs#L135
b if b.len() >= 3 && matches!(b[0..3], [0x1F, 0x8B, 0x08]) => Some(Self::GZip),
// https://stackoverflow.com/a/43170354
b if b.len() >= 2
&& matches!(
b[0..2],
[0x78, 0x01] | [0x78, 0x5E] | [0x78, 0x9C] | [0x78, 0xDA]
) =>
{
Some(Self::ZLib)
}
_ => None,
}
}
pub fn detect_from_header_str(header: impl AsRef<str>) -> Option<Self> {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding#directives
match header.as_ref().to_ascii_lowercase().trim() {
"br" | "brotli" => Some(Self::Brotli),
"deflate" => Some(Self::ZLib),
"gz" | "gzip" => Some(Self::GZip),
_ => None,
}
}
}
impl<'lua> FromLua<'lua> for CompressDecompressFormat {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = &value {
match s.to_string_lossy().to_ascii_lowercase().trim() {
"brotli" => Ok(Self::Brotli),
"gzip" => Ok(Self::GZip),
"lz4" => Ok(Self::LZ4),
"zlib" => Ok(Self::ZLib),
kind => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "CompressDecompressFormat",
message: Some(format!(
"Invalid format '{kind}', valid formats are: brotli, gzip, lz4, zlib"
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "CompressDecompressFormat",
message: None,
})
}
}
}
pub async fn compress<'lua>(
format: CompressDecompressFormat,
source: impl AsRef<[u8]>,
) -> LuaResult<Vec<u8>> {
if let CompressDecompressFormat::LZ4 = format {
let source = source.as_ref().to_vec();
return task::spawn_blocking(move || compress_prepend_size(&source))
.await
.into_lua_err();
}
let mut bytes = Vec::new();
let reader = BufReader::new(source.as_ref());
match format {
CompressDecompressFormat::Brotli => {
let mut encoder = BrotliEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::GZip => {
let mut encoder = GzipEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::ZLib => {
let mut encoder = ZlibEncoder::with_quality(reader, CompressionQuality);
copy(&mut encoder, &mut bytes).await?;
}
CompressDecompressFormat::LZ4 => unreachable!(),
}
Ok(bytes)
}
pub async fn decompress<'lua>(
format: CompressDecompressFormat,
source: impl AsRef<[u8]>,
) -> LuaResult<Vec<u8>> {
if let CompressDecompressFormat::LZ4 = format {
let source = source.as_ref().to_vec();
return task::spawn_blocking(move || decompress_size_prepended(&source))
.await
.into_lua_err()?
.into_lua_err();
}
let mut bytes = Vec::new();
let reader = BufReader::new(source.as_ref());
match format {
CompressDecompressFormat::Brotli => {
let mut decoder = BrotliDecoder::new(reader);
copy(&mut decoder, &mut bytes).await?;
}
CompressDecompressFormat::GZip => {
let mut decoder = GzipDecoder::new(reader);
copy(&mut decoder, &mut bytes).await?;
}
CompressDecompressFormat::ZLib => {
let mut decoder = ZlibDecoder::new(reader);
copy(&mut decoder, &mut bytes).await?;
}
CompressDecompressFormat::LZ4 => unreachable!(),
}
Ok(bytes)
}

View file

@ -1,133 +0,0 @@
use mlua::prelude::*;
use serde_json::Value as JsonValue;
use serde_yaml::Value as YamlValue;
use toml::Value as TomlValue;
const LUA_SERIALIZE_OPTIONS: LuaSerializeOptions = LuaSerializeOptions::new()
.set_array_metatable(false)
.serialize_none_to_null(false)
.serialize_unit_to_null(false);
const LUA_DESERIALIZE_OPTIONS: LuaDeserializeOptions = LuaDeserializeOptions::new()
.deny_recursive_tables(false)
.deny_unsupported_types(true);
#[derive(Debug, Clone, Copy)]
pub enum EncodeDecodeFormat {
Json,
Yaml,
Toml,
}
impl<'lua> FromLua<'lua> for EncodeDecodeFormat {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::String(s) = &value {
match s.to_string_lossy().to_ascii_lowercase().trim() {
"json" => Ok(Self::Json),
"yaml" => Ok(Self::Yaml),
"toml" => Ok(Self::Toml),
kind => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "EncodeDecodeFormat",
message: Some(format!(
"Invalid format '{kind}', valid formats are: json, yaml, toml"
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "EncodeDecodeFormat",
message: None,
})
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EncodeDecodeConfig {
pub format: EncodeDecodeFormat,
pub pretty: bool,
}
impl EncodeDecodeConfig {
pub fn serialize_to_string<'lua>(
self,
lua: &'lua Lua,
value: LuaValue<'lua>,
) -> LuaResult<LuaString<'lua>> {
let bytes = match self.format {
EncodeDecodeFormat::Json => {
let serialized: JsonValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
if self.pretty {
serde_json::to_vec_pretty(&serialized).into_lua_err()?
} else {
serde_json::to_vec(&serialized).into_lua_err()?
}
}
EncodeDecodeFormat::Yaml => {
let serialized: YamlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
let mut writer = Vec::with_capacity(128);
serde_yaml::to_writer(&mut writer, &serialized).into_lua_err()?;
writer
}
EncodeDecodeFormat::Toml => {
let serialized: TomlValue = lua.from_value_with(value, LUA_DESERIALIZE_OPTIONS)?;
let s = if self.pretty {
toml::to_string_pretty(&serialized).into_lua_err()?
} else {
toml::to_string(&serialized).into_lua_err()?
};
s.as_bytes().to_vec()
}
};
lua.create_string(bytes)
}
pub fn deserialize_from_string<'lua>(
self,
lua: &'lua Lua,
string: LuaString<'lua>,
) -> LuaResult<LuaValue<'lua>> {
let bytes = string.as_bytes();
match self.format {
EncodeDecodeFormat::Json => {
let value: JsonValue = serde_json::from_slice(bytes).into_lua_err()?;
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
}
EncodeDecodeFormat::Yaml => {
let value: YamlValue = serde_yaml::from_slice(bytes).into_lua_err()?;
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
}
EncodeDecodeFormat::Toml => {
if let Ok(s) = string.to_str() {
let value: TomlValue = toml::from_str(s).into_lua_err()?;
lua.to_value_with(&value, LUA_SERIALIZE_OPTIONS)
} else {
Err(LuaError::RuntimeError(
"TOML must be valid utf-8".to_string(),
))
}
}
}
}
}
impl From<EncodeDecodeFormat> for EncodeDecodeConfig {
fn from(format: EncodeDecodeFormat) -> Self {
Self {
format,
pretty: false,
}
}
}
impl From<(EncodeDecodeFormat, bool)> for EncodeDecodeConfig {
fn from(value: (EncodeDecodeFormat, bool)) -> Self {
Self {
format: value.0,
pretty: value.1,
}
}
}

View file

@ -1,5 +0,0 @@
mod compress_decompress;
mod encode_decode;
pub use compress_decompress::{compress, decompress, CompressDecompressFormat};
pub use encode_decode::{EncodeDecodeConfig, EncodeDecodeFormat};

View file

@ -1,473 +0,0 @@
use std::fmt::Write;
use console::{colors_enabled, set_colors_enabled, style, Style};
use mlua::prelude::*;
use once_cell::sync::Lazy;
use crate::lune_temp::lua::task::TaskReference;
const MAX_FORMAT_DEPTH: usize = 4;
const INDENT: &str = " ";
pub const STYLE_RESET_STR: &str = "\x1b[0m";
// Colors
pub static COLOR_BLACK: Lazy<Style> = Lazy::new(|| Style::new().black());
pub static COLOR_RED: Lazy<Style> = Lazy::new(|| Style::new().red());
pub static COLOR_GREEN: Lazy<Style> = Lazy::new(|| Style::new().green());
pub static COLOR_YELLOW: Lazy<Style> = Lazy::new(|| Style::new().yellow());
pub static COLOR_BLUE: Lazy<Style> = Lazy::new(|| Style::new().blue());
pub static COLOR_PURPLE: Lazy<Style> = Lazy::new(|| Style::new().magenta());
pub static COLOR_CYAN: Lazy<Style> = Lazy::new(|| Style::new().cyan());
pub static COLOR_WHITE: Lazy<Style> = Lazy::new(|| Style::new().white());
// Styles
pub static STYLE_BOLD: Lazy<Style> = Lazy::new(|| Style::new().bold());
pub static STYLE_DIM: Lazy<Style> = Lazy::new(|| Style::new().dim());
fn can_be_plain_lua_table_key(s: &LuaString) -> bool {
let str = s.to_string_lossy().to_string();
let first_char = str.chars().next().unwrap();
if first_char.is_alphabetic() {
str.chars().all(|c| c == '_' || c.is_alphanumeric())
} else {
false
}
}
pub fn format_label<S: AsRef<str>>(s: S) -> String {
format!(
"{}{}{} ",
style("[").dim(),
match s.as_ref().to_ascii_lowercase().as_str() {
"info" => style("INFO").blue(),
"warn" => style("WARN").yellow(),
"error" => style("ERROR").red(),
_ => style(""),
},
style("]").dim()
)
}
pub fn format_style(style: Option<&'static Style>) -> String {
if cfg!(test) {
"".to_string()
} else if let Some(style) = style {
// HACK: We have no direct way of referencing the ansi color code
// of the style that console::Style provides, and we also know for
// sure that styles always include the reset sequence at the end,
// unless we are in a CI environment on non-interactive terminal
style
.apply_to("")
.to_string()
.trim_end_matches(STYLE_RESET_STR)
.to_string()
} else {
STYLE_RESET_STR.to_string()
}
}
pub fn style_from_color_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
Ok(match s.as_ref() {
"reset" => None,
"black" => Some(&COLOR_BLACK),
"red" => Some(&COLOR_RED),
"green" => Some(&COLOR_GREEN),
"yellow" => Some(&COLOR_YELLOW),
"blue" => Some(&COLOR_BLUE),
"purple" => Some(&COLOR_PURPLE),
"cyan" => Some(&COLOR_CYAN),
"white" => Some(&COLOR_WHITE),
_ => {
return Err(LuaError::RuntimeError(format!(
"The color '{}' is not a valid color name",
s.as_ref()
)));
}
})
}
pub fn style_from_style_str<S: AsRef<str>>(s: S) -> LuaResult<Option<&'static Style>> {
Ok(match s.as_ref() {
"reset" => None,
"bold" => Some(&STYLE_BOLD),
"dim" => Some(&STYLE_DIM),
_ => {
return Err(LuaError::RuntimeError(format!(
"The style '{}' is not a valid style name",
s.as_ref()
)));
}
})
}
pub fn pretty_format_value(
buffer: &mut String,
value: &LuaValue,
depth: usize,
) -> std::fmt::Result {
// TODO: Handle tables with cyclic references
match &value {
LuaValue::Nil => write!(buffer, "nil")?,
LuaValue::Boolean(true) => write!(buffer, "{}", COLOR_YELLOW.apply_to("true"))?,
LuaValue::Boolean(false) => write!(buffer, "{}", COLOR_YELLOW.apply_to("false"))?,
LuaValue::Number(n) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{n}")))?,
LuaValue::Integer(i) => write!(buffer, "{}", COLOR_CYAN.apply_to(format!("{i}")))?,
LuaValue::String(s) => write!(
buffer,
"\"{}\"",
COLOR_GREEN.apply_to(
s.to_string_lossy()
.replace('"', r#"\""#)
.replace('\r', r#"\r"#)
.replace('\n', r#"\n"#)
)
)?,
LuaValue::Table(ref tab) => {
if depth >= MAX_FORMAT_DEPTH {
write!(buffer, "{}", STYLE_DIM.apply_to("{ ... }"))?;
} else if let Some(s) = call_table_tostring_metamethod(tab) {
write!(buffer, "{s}")?;
} else {
let mut is_empty = false;
let depth_indent = INDENT.repeat(depth);
write!(buffer, "{}", STYLE_DIM.apply_to("{"))?;
for pair in tab.clone().pairs::<LuaValue, LuaValue>() {
let (key, value) = pair.unwrap();
match &key {
LuaValue::String(s) if can_be_plain_lua_table_key(s) => write!(
buffer,
"\n{}{}{} {} ",
depth_indent,
INDENT,
s.to_string_lossy(),
STYLE_DIM.apply_to("=")
)?,
_ => {
write!(buffer, "\n{depth_indent}{INDENT}[")?;
pretty_format_value(buffer, &key, depth)?;
write!(buffer, "] {} ", STYLE_DIM.apply_to("="))?;
}
}
pretty_format_value(buffer, &value, depth + 1)?;
write!(buffer, "{}", STYLE_DIM.apply_to(","))?;
is_empty = false;
}
if is_empty {
write!(buffer, "{}", STYLE_DIM.apply_to(" }"))?;
} else {
write!(buffer, "\n{depth_indent}{}", STYLE_DIM.apply_to("}"))?;
}
}
}
LuaValue::Vector(v) => write!(
buffer,
"{}",
COLOR_PURPLE.apply_to(format!(
"<vector({x}, {y}, {z})>",
x = v.x(),
y = v.y(),
z = v.z()
))
)?,
LuaValue::Thread(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<thread>"))?,
LuaValue::Function(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<function>"))?,
LuaValue::UserData(u) => {
if u.is::<TaskReference>() {
// Task references must be transparent
// to lua and pretend to be normal lua
// threads for compatibility purposes
write!(buffer, "{}", COLOR_PURPLE.apply_to("<thread>"))?
} else if let Some(s) = call_userdata_tostring_metamethod(u) {
write!(buffer, "{s}")?
} else {
write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?
}
}
LuaValue::LightUserData(_) => write!(buffer, "{}", COLOR_PURPLE.apply_to("<userdata>"))?,
LuaValue::Error(e) => write!(buffer, "{}", pretty_format_luau_error(e, false),)?,
}
Ok(())
}
pub fn pretty_format_multi_value(multi: &LuaMultiValue) -> LuaResult<String> {
let mut buffer = String::new();
let mut counter = 0;
for value in multi {
counter += 1;
if let LuaValue::String(s) = value {
write!(buffer, "{}", s.to_string_lossy()).into_lua_err()?;
} else {
pretty_format_value(&mut buffer, value, 0).into_lua_err()?;
}
if counter < multi.len() {
write!(&mut buffer, " ").into_lua_err()?;
}
}
Ok(buffer)
}
pub fn pretty_format_luau_error(e: &LuaError, colorized: bool) -> String {
let previous_colors_enabled = if !colorized {
set_colors_enabled(false);
Some(colors_enabled())
} else {
None
};
let stack_begin = format!("[{}]", COLOR_BLUE.apply_to("Stack Begin"));
let stack_end = format!("[{}]", COLOR_BLUE.apply_to("Stack End"));
let err_string = match e {
LuaError::RuntimeError(e) => {
// Remove unnecessary prefix
let mut err_string = e.to_string();
if let Some(no_prefix) = err_string.strip_prefix("runtime error: ") {
err_string = no_prefix.to_string();
}
// Add "Stack Begin" instead of default stack traceback string
let mut err_lines = err_string
.lines()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let mut found_stack_begin = false;
for (index, line) in err_lines.clone().iter().enumerate().rev() {
if *line == "stack traceback:" {
err_lines[index] = stack_begin.clone();
found_stack_begin = true;
break;
}
}
// Add "Stack End" to the very end of the stack trace for symmetry
if found_stack_begin {
err_lines.push(stack_end.clone());
}
err_lines.join("\n")
}
LuaError::CallbackError { traceback, cause } => {
// Find the best traceback (most lines) and the root error message
// The traceback may also start with "override traceback:" which
// means it was passed from somewhere that wants a custom trace,
// so we should then respect that and get the best override instead
let mut full_trace = traceback.to_string();
let mut root_cause = cause.as_ref();
let mut trace_override = false;
while let LuaError::CallbackError { cause, traceback } = root_cause {
let is_override = traceback.starts_with("override traceback:");
if is_override {
if !trace_override || traceback.lines().count() > full_trace.len() {
full_trace = traceback
.trim_start_matches("override traceback:")
.to_string();
trace_override = true;
}
} else if !trace_override {
full_trace = format!("{traceback}\n{full_trace}");
}
root_cause = cause;
}
// If we got a runtime error with an embedded traceback, we should
// use that instead since it generally contains more information
if matches!(root_cause, LuaError::RuntimeError(e) if e.contains("stack traceback:")) {
pretty_format_luau_error(root_cause, colorized)
} else {
// Otherwise we format whatever root error we got using
// the same error formatting as for above runtime errors
format!(
"{}\n{}\n{}\n{}",
pretty_format_luau_error(root_cause, colorized),
stack_begin,
full_trace.trim_start_matches("stack traceback:\n"),
stack_end
)
}
}
LuaError::BadArgument { pos, cause, .. } => match cause.as_ref() {
// TODO: Add more detail to this error message
LuaError::FromLuaConversionError { from, to, .. } => {
format!("Argument #{pos} must be of type '{to}', got '{from}'")
}
c => format!(
"Bad argument #{pos}\n{}",
pretty_format_luau_error(c, colorized)
),
},
e => format!("{e}"),
};
// Re-enable colors if they were previously enabled
if let Some(true) = previous_colors_enabled {
set_colors_enabled(true)
}
// Remove the script path from the error message
// itself, it can be found in the stack trace
let mut err_lines = err_string.lines().collect::<Vec<_>>();
if let Some(first_line) = err_lines.first() {
if first_line.starts_with("[string \"") {
if let Some(closing_bracket) = first_line.find("]:") {
let after_closing_bracket = &first_line[closing_bracket + 2..first_line.len()];
if let Some(last_colon) = after_closing_bracket.find(": ") {
err_lines[0] = &after_closing_bracket
[last_colon + 2..first_line.len() - closing_bracket - 2];
} else {
err_lines[0] = after_closing_bracket
}
}
}
}
// Find where the stack trace stars and ends
let stack_begin_idx =
err_lines.iter().enumerate().find_map(
|(i, line)| {
if *line == stack_begin {
Some(i)
} else {
None
}
},
);
let stack_end_idx =
err_lines.iter().enumerate().find_map(
|(i, line)| {
if *line == stack_end {
Some(i)
} else {
None
}
},
);
// If we have a stack trace, we should transform the formatting from the
// default mlua formatting into something more friendly, similar to Roblox
if let (Some(idx_start), Some(idx_end)) = (stack_begin_idx, stack_end_idx) {
let stack_lines = err_lines
.iter()
.enumerate()
// Filter out stack lines
.filter_map(|(idx, line)| {
if idx > idx_start && idx < idx_end {
Some(*line)
} else {
None
}
})
// Transform from mlua format into friendly format, while also
// ensuring that leading whitespace / indentation is consistent
.map(transform_stack_line)
.collect::<Vec<_>>();
fix_error_nitpicks(format!(
"{}\n{}\n{}\n{}",
err_lines
.iter()
.take(idx_start)
.copied()
.collect::<Vec<_>>()
.join("\n"),
stack_begin,
stack_lines.join("\n"),
stack_end,
))
} else {
fix_error_nitpicks(err_string)
}
}
fn transform_stack_line(line: &str) -> String {
match (line.find('['), line.find(']')) {
(Some(idx_start), Some(idx_end)) => {
let name = line[idx_start..idx_end + 1]
.trim_start_matches('[')
.trim_start_matches("string ")
.trim_start_matches('"')
.trim_end_matches(']')
.trim_end_matches('"');
let after_name = &line[idx_end + 1..];
let line_num = match after_name.find(':') {
Some(lineno_start) => match after_name[lineno_start + 1..].find(':') {
Some(lineno_end) => &after_name[lineno_start + 1..lineno_end + 1],
None => match after_name.contains("in function") || after_name.contains("in ?")
{
false => &after_name[lineno_start + 1..],
true => "",
},
},
None => "",
};
let func_name = match after_name.find("in function ") {
Some(func_start) => after_name[func_start + 12..]
.trim()
.trim_end_matches('\'')
.trim_start_matches('\'')
.trim_start_matches("_G."),
None => "",
};
let mut result = String::new();
write!(
result,
" Script '{}'",
match name {
"C" => "[C]",
name => name,
},
)
.unwrap();
if !line_num.is_empty() {
write!(result, ", Line {line_num}").unwrap();
}
if !func_name.is_empty() {
write!(result, " - function {func_name}").unwrap();
}
result
}
(_, _) => line.to_string(),
}
}
fn fix_error_nitpicks(full_message: String) -> String {
full_message
// Hacky fix for our custom require appearing as a normal script
// TODO: It's probably better to pull in the regex crate here ..
.replace("'require', Line 5", "'[C]' - function require")
.replace("'require', Line 7", "'[C]' - function require")
.replace("'require', Line 8", "'[C]' - function require")
// Same thing here for our async script
.replace("'async', Line 2", "'[C]'")
.replace("'async', Line 3", "'[C]'")
// Fix error calls in custom script chunks coming through
.replace(
"'[C]' - function error\n Script '[C]' - function require",
"'[C]' - function require",
)
// Fix strange double require
.replace(
"'[C]' - function require - function require",
"'[C]' - function require",
)
// Fix strange double C
.replace("'[C]'\n Script '[C]'", "'[C]'")
}
fn call_table_tostring_metamethod<'a>(tab: &'a LuaTable<'a>) -> Option<String> {
let f = match tab.get_metatable() {
None => None,
Some(meta) => match meta.get::<_, LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}
fn call_userdata_tostring_metamethod<'a>(tab: &'a LuaAnyUserData<'a>) -> Option<String> {
let f = match tab.get_metatable() {
Err(_) => None,
Ok(meta) => match meta.get::<LuaFunction>(LuaMetaMethod::ToString.name()) {
Ok(method) => Some(method),
Err(_) => None,
},
}?;
match f.call::<_, String>(()) {
Ok(res) => Some(res),
Err(_) => None,
}
}

View file

@ -1,2 +0,0 @@
pub mod formatting;
pub mod prompt;

View file

@ -1,192 +0,0 @@
use std::fmt;
use mlua::prelude::*;
#[derive(Debug, Clone, Copy)]
pub enum PromptKind {
Text,
Confirm,
Select,
MultiSelect,
}
impl PromptKind {
fn get_all() -> Vec<Self> {
vec![Self::Text, Self::Confirm, Self::Select, Self::MultiSelect]
}
}
impl Default for PromptKind {
fn default() -> Self {
Self::Text
}
}
impl fmt::Display for PromptKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
match self {
Self::Text => "Text",
Self::Confirm => "Confirm",
Self::Select => "Select",
Self::MultiSelect => "MultiSelect",
}
)
}
}
impl<'lua> FromLua<'lua> for PromptKind {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
if let LuaValue::Nil = value {
Ok(Self::default())
} else if let LuaValue::String(s) = value {
let s = s.to_str()?;
/*
If the user only typed the prompt kind slightly wrong, meaning
it has some kind of space in it, a weird character, or an uppercase
character, we should try to be permissive as possible and still work
Not everyone is using an IDE with proper Luau type definitions
installed, and Luau is still a permissive scripting language
even though it has a strict (but optional) type system
*/
let s = s
.chars()
.filter_map(|c| {
if c.is_ascii_alphabetic() {
Some(c.to_ascii_lowercase())
} else {
None
}
})
.collect::<String>();
// If the prompt kind is still invalid we will
// show the user a descriptive error message
match s.as_ref() {
"text" => Ok(Self::Text),
"confirm" => Ok(Self::Confirm),
"select" => Ok(Self::Select),
"multiselect" => Ok(Self::MultiSelect),
s => Err(LuaError::FromLuaConversionError {
from: "string",
to: "PromptKind",
message: Some(format!(
"Invalid prompt kind '{s}', valid kinds are:\n{}",
PromptKind::get_all()
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(", ")
)),
}),
}
} else {
Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptKind",
message: None,
})
}
}
}
pub struct PromptOptions {
pub kind: PromptKind,
pub text: Option<String>,
pub default_string: Option<String>,
pub default_bool: Option<bool>,
pub options: Option<Vec<String>>,
}
impl<'lua> FromLuaMulti<'lua> for PromptOptions {
fn from_lua_multi(mut values: LuaMultiValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
// Argument #1 - prompt kind (optional)
let kind = values
.pop_front()
.map(|value| PromptKind::from_lua(value, lua))
.transpose()?
.unwrap_or_default();
// Argument #2 - prompt text (optional)
let text = values
.pop_front()
.map(|text| String::from_lua(text, lua))
.transpose()?;
// Argument #3 - default value / options,
// this is different per each prompt kind
let (default_bool, default_string, options) = match values.pop_front() {
None => (None, None, None),
Some(options) => match options {
LuaValue::Nil => (None, None, None),
LuaValue::Boolean(b) => (Some(b), None, None),
LuaValue::String(s) => (
None,
Some(String::from_lua(LuaValue::String(s), lua)?),
None,
),
LuaValue::Table(t) => (
None,
None,
Some(Vec::<String>::from_lua(LuaValue::Table(t), lua)?),
),
value => {
return Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "PromptOptions",
message: Some("Argument #3 must be a boolean, table, or nil".to_string()),
})
}
},
};
/*
Make sure we got the required values for the specific prompt kind:
- "Confirm" requires a message to be present so the user knows what they are confirming
- "Select" and "MultiSelect" both require a table of options to choose from
*/
if matches!(kind, PromptKind::Confirm) && text.is_none() {
return Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptOptions",
message: Some("Argument #2 missing or nil".to_string()),
});
}
if matches!(kind, PromptKind::Select | PromptKind::MultiSelect) && options.is_none() {
return Err(LuaError::FromLuaConversionError {
from: "nil",
to: "PromptOptions",
message: Some("Argument #3 missing or nil".to_string()),
});
}
// All good, return the prompt options
Ok(Self {
kind,
text,
default_bool,
default_string,
options,
})
}
}
#[derive(Debug, Clone)]
pub enum PromptResult {
String(String),
Boolean(bool),
Index(usize),
Indices(Vec<usize>),
None,
}
impl<'lua> IntoLua<'lua> for PromptResult {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
Ok(match self {
Self::String(s) => LuaValue::String(lua.create_string(&s)?),
Self::Boolean(b) => LuaValue::Boolean(b),
Self::Index(i) => LuaValue::Number(i as f64),
Self::Indices(v) => v.into_lua(lua)?,
Self::None => LuaValue::Nil,
})
}
}

View file

@ -1,93 +0,0 @@
use std::future::Future;
use mlua::prelude::*;
use crate::lune_temp::lua::async_ext::LuaAsyncExt;
pub struct TableBuilder {
lua: &'static Lua,
tab: LuaTable<'static>,
}
#[allow(dead_code)]
impl TableBuilder {
pub fn new(lua: &'static Lua) -> LuaResult<Self> {
let tab = lua.create_table()?;
Ok(Self { lua, tab })
}
pub fn with_value<K, V>(self, key: K, value: V) -> LuaResult<Self>
where
K: IntoLua<'static>,
V: IntoLua<'static>,
{
self.tab.raw_set(key, value)?;
Ok(self)
}
pub fn with_values<K, V>(self, values: Vec<(K, V)>) -> LuaResult<Self>
where
K: IntoLua<'static>,
V: IntoLua<'static>,
{
for (key, value) in values {
self.tab.raw_set(key, value)?;
}
Ok(self)
}
pub fn with_sequential_value<V>(self, value: V) -> LuaResult<Self>
where
V: IntoLua<'static>,
{
self.tab.raw_push(value)?;
Ok(self)
}
pub fn with_sequential_values<V>(self, values: Vec<V>) -> LuaResult<Self>
where
V: IntoLua<'static>,
{
for value in values {
self.tab.raw_push(value)?;
}
Ok(self)
}
pub fn with_metatable(self, table: LuaTable) -> LuaResult<Self> {
self.tab.set_metatable(Some(table));
Ok(self)
}
pub fn with_function<K, A, R, F>(self, key: K, func: F) -> LuaResult<Self>
where
K: IntoLua<'static>,
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua, A) -> LuaResult<R>,
{
let f = self.lua.create_function(func)?;
self.with_value(key, LuaValue::Function(f))
}
pub fn with_async_function<K, A, R, F, FR>(self, key: K, func: F) -> LuaResult<Self>
where
K: IntoLua<'static>,
A: FromLuaMulti<'static>,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua, A) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
let f = self.lua.create_async_function(func)?;
self.with_value(key, LuaValue::Function(f))
}
pub fn build_readonly(self) -> LuaResult<LuaTable<'static>> {
self.tab.set_readonly(true);
Ok(self.tab)
}
pub fn build(self) -> LuaResult<LuaTable<'static>> {
Ok(self.tab)
}
}

View file

@ -1,3 +0,0 @@
mod builder;
pub use builder::TableBuilder;

View file

@ -1,135 +0,0 @@
use std::time::Duration;
use async_trait::async_trait;
use futures_util::Future;
use mlua::prelude::*;
use tokio::time::{sleep, Instant};
use crate::lune_temp::lua::task::TaskKind;
use super::super::{
scheduler::TaskReference, scheduler::TaskScheduler, scheduler_handle::TaskSchedulerAsyncHandle,
scheduler_message::TaskSchedulerMessage,
};
/*
Trait definition - same as the implementation, ignore this
We use traits here to prevent misuse of certain scheduler
APIs, making importing of them as intentional as possible
*/
#[async_trait(?Send)]
pub trait TaskSchedulerAsyncExt<'fut> {
fn register_background_task(&self) -> TaskSchedulerAsyncHandle;
fn schedule_async<'sched, R, F, FR>(
&'sched self,
thread: LuaThread<'_>,
func: F,
) -> LuaResult<TaskReference>
where
'sched: 'fut,
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua) -> FR,
FR: 'static + Future<Output = LuaResult<R>>;
fn schedule_wait(
&'fut self,
reference: LuaThread<'_>,
duration: Option<f64>,
) -> LuaResult<TaskReference>;
}
/*
Trait implementation
*/
#[async_trait(?Send)]
impl<'fut> TaskSchedulerAsyncExt<'fut> for TaskScheduler<'fut> {
/**
Registers a new background task with the task scheduler.
The returned [`TaskSchedulerAsyncHandle`] must have its
[`TaskSchedulerAsyncHandle::unregister`] method called
upon completion of the background task to prevent
the task scheduler from running indefinitely.
*/
fn register_background_task(&self) -> TaskSchedulerAsyncHandle {
let sender = self.futures_tx.clone();
sender
.send(TaskSchedulerMessage::Spawned)
.unwrap_or_else(|e| {
panic!(
"\
\nFailed to unregister background task - this is an internal error! \
\nPlease report it at {} \
\nDetails: {e} \
",
env!("CARGO_PKG_REPOSITORY")
)
});
TaskSchedulerAsyncHandle::new(sender)
}
/**
Schedules a lua thread or function
to be resumed after running a future.
The given lua thread or function will be resumed
using the optional arguments returned by the future.
*/
fn schedule_async<'sched, R, F, FR>(
&'sched self,
thread: LuaThread<'_>,
func: F,
) -> LuaResult<TaskReference>
where
'sched: 'fut, // Scheduler must live at least as long as the future
R: IntoLuaMulti<'static>,
F: 'static + Fn(&'static Lua) -> FR,
FR: 'static + Future<Output = LuaResult<R>>,
{
self.queue_async_task(thread, None, async move {
match func(self.lua).await {
Ok(res) => match res.into_lua_multi(self.lua) {
Ok(multi) => Ok(Some(multi)),
Err(e) => Err(e),
},
Err(e) => Err(e),
}
})
}
/**
Schedules a task reference to be resumed after a certain amount of time.
The given task will be resumed with the elapsed time as its one and only argument.
*/
fn schedule_wait(
&'fut self,
thread: LuaThread<'_>,
duration: Option<f64>,
) -> LuaResult<TaskReference> {
let reference = self.create_task(TaskKind::Future, thread, None, true)?;
// Insert the future
let futs = self
.futures
.try_lock()
.expect("Tried to add future to queue during futures resumption");
futs.push(Box::pin(async move {
let before = Instant::now();
sleep(Duration::from_secs_f64(
duration.unwrap_or_default().max(0.0),
))
.await;
let elapsed_secs = before.elapsed().as_secs_f64();
let args = elapsed_secs.into_lua_multi(self.lua).unwrap();
(Some(reference), Ok(Some(args)))
}));
Ok(reference)
}
}

View file

@ -1,7 +0,0 @@
mod async_ext;
mod resume_ext;
mod schedule_ext;
pub use async_ext::TaskSchedulerAsyncExt;
pub use resume_ext::TaskSchedulerResumeExt;
pub use schedule_ext::TaskSchedulerScheduleExt;

View file

@ -1,180 +0,0 @@
use std::time::Duration;
use async_trait::async_trait;
use mlua::prelude::*;
use futures_util::StreamExt;
use tokio::time::sleep;
use super::super::{
scheduler_message::TaskSchedulerMessage, scheduler_state::TaskSchedulerState, TaskScheduler,
};
/*
Trait definition - same as the implementation, ignore this
We use traits here to prevent misuse of certain scheduler
APIs, making importing of them as intentional as possible
*/
#[async_trait(?Send)]
pub trait TaskSchedulerResumeExt {
async fn resume_queue(&self) -> TaskSchedulerState;
}
/*
Trait implementation
*/
#[async_trait(?Send)]
impl TaskSchedulerResumeExt for TaskScheduler<'_> {
/**
Resumes the task scheduler queue.
This will run any spawned or deferred Lua tasks in a blocking manner.
Once all spawned and / or deferred Lua tasks have finished running,
this will process delayed tasks, waiting tasks, and native Rust
futures concurrently, awaiting the first one to be ready for resumption.
*/
async fn resume_queue(&self) -> TaskSchedulerState {
let current = TaskSchedulerState::new(self);
if current.num_blocking > 0 {
// 1. Blocking tasks
resume_next_blocking_task(self, None)
} else if current.num_futures > 0 || current.num_background > 0 {
// 2. Async and/or background tasks
tokio::select! {
result = resume_next_async_task(self) => result,
result = receive_next_message(self) => result,
}
} else {
// 3. No tasks left, here we sleep one millisecond in case
// the caller of resume_queue accidentally calls this in
// a busy loop to prevent cpu usage from going to 100%
sleep(Duration::from_millis(1)).await;
TaskSchedulerState::new(self)
}
}
}
/*
Private functions for the trait that operate on the task scheduler
These could be implemented as normal methods but if we put them in the
trait they become public, and putting them in the task scheduler's
own implementation block will clutter that up unnecessarily
*/
/**
Resumes the next queued Lua task, if one exists, blocking
the current thread until it either yields or finishes.
*/
fn resume_next_blocking_task<'sched, 'args>(
scheduler: &TaskScheduler<'sched>,
override_args: Option<LuaResult<LuaMultiValue<'args>>>,
) -> TaskSchedulerState
where
'args: 'sched,
{
match {
let mut queue_guard = scheduler.tasks_queue_blocking.borrow_mut();
let task = queue_guard.pop_front();
drop(queue_guard);
task
} {
None => TaskSchedulerState::new(scheduler),
Some(task) => match scheduler.resume_task(task, override_args) {
Err(task_err) => {
scheduler.wake_completed_task(task, Err(task_err.clone()));
TaskSchedulerState::err(scheduler, task_err)
}
Ok(rets) if rets.0 == LuaThreadStatus::Unresumable => {
scheduler.wake_completed_task(task, Ok(rets.1));
TaskSchedulerState::new(scheduler)
}
Ok(_) => TaskSchedulerState::new(scheduler),
},
}
}
/**
Awaits the first available queued future, and resumes its associated
Lua task which will be ready for resumption when that future wakes.
Panics if there are no futures currently queued.
Use [`TaskScheduler::next_queue_future_exists`]
to check if there are any queued futures.
*/
async fn resume_next_async_task(scheduler: &TaskScheduler<'_>) -> TaskSchedulerState {
let (task, result) = {
let mut futs = scheduler
.futures
.try_lock()
.expect("Tried to resume next queued future while already resuming or modifying");
futs.next()
.await
.expect("Tried to resume next queued future but none are queued")
};
// The future might not return a reference that it wants to resume
if let Some(task) = task {
// Promote this future task to a blocking task and resume it
// right away, also taking care to not borrow mutably twice
// by dropping this guard before trying to resume it
let mut queue_guard = scheduler.tasks_queue_blocking.borrow_mut();
queue_guard.push_front(task);
drop(queue_guard);
}
resume_next_blocking_task(scheduler, result.transpose())
}
/**
Awaits the next background task registration
message, if any messages exist in the queue.
This is a no-op if there are no background tasks left running
and / or the background task messages channel was closed.
*/
async fn receive_next_message(scheduler: &TaskScheduler<'_>) -> TaskSchedulerState {
let message_opt = {
let mut rx = scheduler.futures_rx.lock().await;
rx.recv().await
};
if let Some(message) = message_opt {
match message {
TaskSchedulerMessage::NewBlockingTaskReady => TaskSchedulerState::new(scheduler),
TaskSchedulerMessage::NewLuaErrorReady(err) => TaskSchedulerState::err(scheduler, err),
TaskSchedulerMessage::Spawned => {
let prev = scheduler.futures_background_count.get();
scheduler.futures_background_count.set(prev + 1);
TaskSchedulerState::new(scheduler)
}
TaskSchedulerMessage::Terminated(result) => {
let prev = scheduler.futures_background_count.get();
scheduler.futures_background_count.set(prev - 1);
if prev == 0 {
panic!(
r#"
Terminated a background task without it running - this is an internal error!
Please report it at {}
"#,
env!("CARGO_PKG_REPOSITORY")
)
}
if let Err(e) = result {
TaskSchedulerState::err(scheduler, e)
} else {
TaskSchedulerState::new(scheduler)
}
}
}
} else {
TaskSchedulerState::new(scheduler)
}
}

View file

@ -1,91 +0,0 @@
use std::time::Duration;
use mlua::prelude::*;
use tokio::time::sleep;
use super::super::{scheduler::TaskKind, scheduler::TaskReference, scheduler::TaskScheduler};
/*
Trait definition - same as the implementation, ignore this
We use traits here to prevent misuse of certain scheduler
APIs, making importing of them as intentional as possible
*/
pub trait TaskSchedulerScheduleExt {
fn schedule_blocking(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference>;
fn schedule_blocking_deferred(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference>;
fn schedule_blocking_after_seconds(
&self,
after_secs: f64,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference>;
}
/*
Trait implementation
*/
impl TaskSchedulerScheduleExt for TaskScheduler<'_> {
/**
Schedules a lua thread or function to resume ***first*** during this
resumption point, ***skipping ahead*** of any other currently queued tasks.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
fn schedule_blocking(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_blocking_task(TaskKind::Instant, thread, Some(thread_args))
}
/**
Schedules a lua thread or function to resume ***after all***
currently resuming tasks, during this resumption point.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
fn schedule_blocking_deferred(
&self,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_blocking_task(TaskKind::Deferred, thread, Some(thread_args))
}
/**
Schedules a lua thread or function to
be resumed after waiting asynchronously.
The given lua thread or function will be resumed
using the given `thread_args` as its argument(s).
*/
fn schedule_blocking_after_seconds(
&self,
after_secs: f64,
thread: LuaThread<'_>,
thread_args: LuaMultiValue<'_>,
) -> LuaResult<TaskReference> {
self.queue_async_task(thread, Some(thread_args), async move {
sleep(Duration::from_secs_f64(after_secs)).await;
Ok(None)
})
}
}

View file

@ -1,15 +0,0 @@
mod ext;
mod proxy;
mod scheduler;
mod scheduler_handle;
mod scheduler_message;
mod scheduler_state;
mod task_kind;
mod task_reference;
mod task_waiter;
pub use ext::*;
pub use proxy::*;
pub use scheduler::*;
pub use scheduler_handle::*;
pub use scheduler_state::*;

View file

@ -1,118 +0,0 @@
use mlua::prelude::*;
use super::TaskReference;
/*
Proxy enum to deal with both threads & functions
*/
#[derive(Debug, Clone)]
pub enum LuaThreadOrFunction<'lua> {
Thread(LuaThread<'lua>),
Function(LuaFunction<'lua>),
}
impl<'lua> LuaThreadOrFunction<'lua> {
pub fn into_thread(self, lua: &'lua Lua) -> LuaResult<LuaThread<'lua>> {
match self {
Self::Thread(t) => Ok(t),
Self::Function(f) => lua.create_thread(f),
}
}
}
impl<'lua> From<LuaThread<'lua>> for LuaThreadOrFunction<'lua> {
fn from(value: LuaThread<'lua>) -> Self {
Self::Thread(value)
}
}
impl<'lua> From<LuaFunction<'lua>> for LuaThreadOrFunction<'lua> {
fn from(value: LuaFunction<'lua>) -> Self {
Self::Function(value)
}
}
impl<'lua> FromLua<'lua> for LuaThreadOrFunction<'lua> {
fn from_lua(value: LuaValue<'lua>, _: &'lua Lua) -> LuaResult<Self> {
match value {
LuaValue::Thread(t) => Ok(Self::Thread(t)),
LuaValue::Function(f) => Ok(Self::Function(f)),
value => Err(LuaError::FromLuaConversionError {
from: value.type_name(),
to: "LuaThreadOrFunction",
message: Some(format!(
"Expected thread or function, got '{}'",
value.type_name()
)),
}),
}
}
}
impl<'lua> IntoLua<'lua> for LuaThreadOrFunction<'lua> {
fn into_lua(self, _: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
match self {
Self::Thread(t) => Ok(LuaValue::Thread(t)),
Self::Function(f) => Ok(LuaValue::Function(f)),
}
}
}
/*
Proxy enum to deal with both threads & task scheduler task references
*/
#[derive(Debug, Clone)]
pub enum LuaThreadOrTaskReference<'lua> {
Thread(LuaThread<'lua>),
TaskReference(TaskReference),
}
impl<'lua> From<LuaThread<'lua>> for LuaThreadOrTaskReference<'lua> {
fn from(value: LuaThread<'lua>) -> Self {
Self::Thread(value)
}
}
impl<'lua> From<TaskReference> for LuaThreadOrTaskReference<'lua> {
fn from(value: TaskReference) -> Self {
Self::TaskReference(value)
}
}
impl<'lua> FromLua<'lua> for LuaThreadOrTaskReference<'lua> {
fn from_lua(value: LuaValue<'lua>, lua: &'lua Lua) -> LuaResult<Self> {
let tname = value.type_name();
match value {
LuaValue::Thread(t) => Ok(Self::Thread(t)),
LuaValue::UserData(u) => {
if let Ok(task) =
LuaUserDataRef::<TaskReference>::from_lua(LuaValue::UserData(u), lua)
{
Ok(Self::TaskReference(*task))
} else {
Err(LuaError::FromLuaConversionError {
from: tname,
to: "thread",
message: Some(format!("Expected thread, got '{tname}'")),
})
}
}
_ => Err(LuaError::FromLuaConversionError {
from: tname,
to: "thread",
message: Some(format!("Expected thread, got '{tname}'")),
}),
}
}
}
impl<'lua> IntoLua<'lua> for LuaThreadOrTaskReference<'lua> {
fn into_lua(self, lua: &'lua Lua) -> LuaResult<LuaValue<'lua>> {
match self {
Self::TaskReference(t) => t.into_lua(lua),
Self::Thread(t) => Ok(LuaValue::Thread(t)),
}
}
}

View file

@ -1,480 +0,0 @@
use core::panic;
use std::{
cell::{Cell, RefCell},
collections::{HashMap, VecDeque},
process::ExitCode,
sync::Arc,
};
use futures_util::{future::LocalBoxFuture, stream::FuturesUnordered, Future};
use mlua::prelude::*;
use tokio::sync::{mpsc, Mutex as AsyncMutex};
use super::{
scheduler_message::TaskSchedulerMessage,
task_waiter::{TaskWaiterFuture, TaskWaiterState},
};
pub use super::{task_kind::TaskKind, task_reference::TaskReference};
type TaskFutureRets<'fut> = LuaResult<Option<LuaMultiValue<'fut>>>;
type TaskFuture<'fut> = LocalBoxFuture<'fut, (Option<TaskReference>, TaskFutureRets<'fut>)>;
/// A struct representing a task contained in the task scheduler
#[derive(Debug)]
pub struct Task {
kind: TaskKind,
thread: LuaRegistryKey,
args: LuaRegistryKey,
}
/// A task scheduler that implements task queues
/// with instant, deferred, and delayed tasks
#[derive(Debug)]
pub struct TaskScheduler<'fut> {
/*
Lots of cell and refcell here, however we need full interior mutability and never outer
since the scheduler struct may be accessed from lua more than once at the same time.
An example of this is the implementation of coroutine.resume, which instantly resumes the given
task, where the task getting resumed may also create new scheduler tasks during its resumption.
The same goes for values used during resumption of futures (`futures` and `futures_rx`)
which must use async-aware mutexes to be cancellation safe across await points.
*/
// Internal state & flags
pub(super) lua: &'static Lua,
pub(super) guid: Cell<usize>,
pub(super) exit_code: Cell<Option<ExitCode>>,
// Blocking tasks
pub(super) tasks: RefCell<HashMap<TaskReference, Task>>,
pub(super) tasks_count: Cell<usize>,
pub(super) tasks_current: Cell<Option<TaskReference>>,
pub(super) tasks_queue_blocking: RefCell<VecDeque<TaskReference>>,
pub(super) tasks_waiter_states:
RefCell<HashMap<TaskReference, Vec<Arc<AsyncMutex<TaskWaiterState<'fut>>>>>>,
pub(super) tasks_current_lua_error: Arc<AsyncMutex<Option<LuaError>>>,
// Future tasks & objects for waking
pub(super) futures: AsyncMutex<FuturesUnordered<TaskFuture<'fut>>>,
pub(super) futures_count: Cell<usize>,
pub(super) futures_background_count: Cell<usize>,
pub(super) futures_tx: mpsc::UnboundedSender<TaskSchedulerMessage>,
pub(super) futures_rx: AsyncMutex<mpsc::UnboundedReceiver<TaskSchedulerMessage>>,
}
impl<'fut> TaskScheduler<'fut> {
/**
Creates a new task scheduler.
*/
pub fn new(lua: &'static Lua) -> LuaResult<Self> {
let (tx, rx) = mpsc::unbounded_channel();
let tasks_current_lua_error = Arc::new(AsyncMutex::new(None));
let tasks_current_lua_error_inner = tasks_current_lua_error.clone();
lua.set_interrupt(move |_| {
match tasks_current_lua_error_inner.try_lock().unwrap().take() {
Some(err) => Err(err),
None => Ok(LuaVmState::Continue),
}
});
Ok(Self {
lua,
guid: Cell::new(0),
exit_code: Cell::new(None),
tasks: RefCell::new(HashMap::new()),
tasks_count: Cell::new(0),
tasks_current: Cell::new(None),
tasks_queue_blocking: RefCell::new(VecDeque::new()),
tasks_waiter_states: RefCell::new(HashMap::new()),
tasks_current_lua_error,
futures: AsyncMutex::new(FuturesUnordered::new()),
futures_tx: tx,
futures_rx: AsyncMutex::new(rx),
futures_count: Cell::new(0),
futures_background_count: Cell::new(0),
})
}
/**
Consumes and leaks the task scheduler,
returning a static reference `&'static TaskScheduler`.
This function is useful when the task scheduler object is
supposed to live for the remainder of the program's life.
Note that dropping the returned reference will cause a memory leak.
*/
pub fn into_static(self) -> &'static Self {
Box::leak(Box::new(self))
}
/**
Stores the exit code for the task scheduler.
This will be passed back to the Rust thread that is running the task scheduler,
in the [`TaskSchedulerState`] returned on resumption of the task scheduler queue.
Setting this exit code will signal to that thread that it
should stop resuming tasks, and gracefully terminate the program.
*/
pub fn set_exit_code(&self, code: ExitCode) {
self.exit_code.set(Some(code));
}
/**
Forwards a lua error to be emitted as soon as possible,
after any current blocking / queued tasks have been resumed.
Useful when an async function may call into Lua and get a
result back, without erroring out of the entire async block.
*/
pub fn forward_lua_error(&self, err: LuaError) {
let sender = self.futures_tx.clone();
sender
.send(TaskSchedulerMessage::NewLuaErrorReady(err))
.unwrap_or_else(|e| {
panic!(
"\
\nFailed to forward lua error - this is an internal error! \
\nPlease report it at {} \
\nDetails: {e} \
",
env!("CARGO_PKG_REPOSITORY")
)
});
}
/**
Forces the current task to be set to the given reference.
Useful if a task is to be resumed externally but full
compatibility with the task scheduler is still necessary.
*/
pub(crate) fn force_set_current_task(&self, reference: Option<TaskReference>) {
self.tasks_current.set(reference);
}
/**
Checks if a task still exists in the scheduler.
A task may no longer exist in the scheduler if it has been manually
cancelled and removed by calling [`TaskScheduler::cancel_task()`].
*/
#[allow(dead_code)]
pub fn contains_task(&self, reference: TaskReference) -> bool {
self.tasks.borrow().contains_key(&reference)
}
/**
Returns the currently running task, if any.
*/
pub fn current_task(&self) -> Option<TaskReference> {
self.tasks_current.get()
}
/**
Returns the status of a specific task, if it exists in the scheduler.
*/
pub fn get_task_status(&self, reference: TaskReference) -> Option<LuaString> {
self.tasks.borrow().get(&reference).map(|task| {
let status: LuaFunction = self
.lua
.named_registry_value("co.status")
.expect("Missing coroutine status function in registry");
let thread: LuaThread = self
.lua
.registry_value(&task.thread)
.expect("Task thread missing from registry");
status
.call(thread)
.expect("Task thread failed to call status")
})
}
/**
Creates a new task, storing a new Lua thread
for it, as well as the arguments to give the
thread on resumption, in the Lua registry.
Note that this task will ***not*** resume on its
own, it needs to be used together with either the
scheduling functions or [`TaskScheduler::resume_task`].
*/
pub fn create_task(
&self,
kind: TaskKind,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
inherit_current_guid: bool,
) -> LuaResult<TaskReference> {
// Store the thread and its arguments in the registry
// NOTE: We must convert to a vec since multis
// can't be stored in the registry directly
let task_args_vec: Option<Vec<LuaValue>> = thread_args.map(|opt| opt.into_vec());
let task_args_key: LuaRegistryKey = self.lua.create_registry_value(task_args_vec)?;
let task_thread_key: LuaRegistryKey = self.lua.create_registry_value(thread)?;
// Create the full task struct
let task = Task {
kind,
thread: task_thread_key,
args: task_args_key,
};
// Create the task ref to use
let guid = if inherit_current_guid {
self.current_task()
.ok_or_else(|| LuaError::RuntimeError("No current guid to inherit".to_string()))?
.id()
} else {
let guid = self.guid.get();
self.guid.set(guid + 1);
guid
};
let reference = TaskReference::new(kind, guid);
// Increment the corresponding task counter
match kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() + 1),
_ => self.tasks_count.set(self.tasks_count.get() + 1),
}
// Add the task to the scheduler
{
let mut tasks = self.tasks.borrow_mut();
tasks.insert(reference, task);
}
Ok(reference)
}
/**
Cancels a task, if the task still exists in the scheduler.
It is possible to hold one or more task references that point
to a task that no longer exists in the scheduler, and calling
this method with one of those references will return `false`.
*/
pub fn remove_task(&self, reference: TaskReference) -> LuaResult<bool> {
/*
Remove the task from the task list and the Lua registry
This is all we need to do since resume_task will always
ignore resumption of any task that no longer exists there
This does lead to having some amount of "junk" futures that will
build up in the queue but these will get cleaned up and not block
the program from exiting since the scheduler only runs until there
are no tasks left in the task list, the futures do not matter there
*/
let mut found = false;
let mut tasks = self.tasks.borrow_mut();
// Unfortunately we have to loop through to find which task
// references to remove instead of removing directly since
// tasks can switch kinds between instant, deferred, future
let tasks_to_remove: Vec<_> = tasks
.keys()
.filter(|task_ref| task_ref.id() == reference.id())
.copied()
.collect();
for task_ref in &tasks_to_remove {
if let Some(task) = tasks.remove(task_ref) {
// Decrement the corresponding task counter
match task.kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1),
_ => self.tasks_count.set(self.tasks_count.get() - 1),
}
// NOTE: We need to close the thread here to
// make 100% sure that nothing can resume it
let close: LuaFunction = self.lua.named_registry_value("co.close")?;
let thread: LuaThread = self.lua.registry_value(&task.thread)?;
close.call(thread)?;
self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?;
found = true;
}
}
Ok(found)
}
/**
Resumes a task, if the task still exists in the scheduler.
A task may no longer exist in the scheduler if it has been manually
cancelled and removed by calling [`TaskScheduler::cancel_task()`].
This will be a no-op if the task no longer exists.
*/
pub fn resume_task<'a, 'r>(
&self,
reference: TaskReference,
override_args: Option<LuaResult<LuaMultiValue<'a>>>,
) -> LuaResult<(LuaThreadStatus, LuaMultiValue<'r>)>
where
'a: 'r,
{
// Fetch and check if the task was removed, if it got
// removed it means it was intentionally cancelled
let task = {
let mut tasks = self.tasks.borrow_mut();
match tasks.remove(&reference) {
Some(task) => task,
None => return Ok((LuaThreadStatus::Unresumable, LuaMultiValue::new())),
}
};
// Decrement the corresponding task counter
match task.kind {
TaskKind::Future => self.futures_count.set(self.futures_count.get() - 1),
_ => self.tasks_count.set(self.tasks_count.get() - 1),
}
// Fetch and remove the thread to resume + its arguments
let thread: LuaThread = self.lua.registry_value(&task.thread)?;
let thread_args: Option<LuaMultiValue> = {
self.lua
.registry_value::<Option<Vec<LuaValue>>>(&task.args)
.expect("Failed to get stored args for task")
.map(LuaMultiValue::from_vec)
};
self.lua.remove_registry_value(task.thread)?;
self.lua.remove_registry_value(task.args)?;
// We got everything we need and our references
// were cleaned up properly, resume the thread
self.tasks_current.set(Some(reference));
let rets = match override_args {
Some(override_res) => match override_res {
Ok(args) => thread.resume(args),
Err(e) => {
// NOTE: Setting this error here means that when the thread
// is resumed it will error instantly, so we don't need
// to call it with proper args, empty args is fine
self.tasks_current_lua_error.try_lock().unwrap().replace(e);
thread.resume(())
}
},
None => match thread_args {
Some(args) => thread.resume(args),
None => thread.resume(()),
},
};
self.tasks_current.set(None);
match rets {
Ok(rets) => Ok((thread.status(), rets)),
Err(e) => Err(e),
}
}
/**
Queues a new blocking task to run on the task scheduler.
*/
pub(crate) fn queue_blocking_task(
&self,
kind: TaskKind,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
) -> LuaResult<TaskReference> {
if kind == TaskKind::Future {
panic!("Tried to schedule future using normal task schedule method")
}
let task_ref = self.create_task(kind, thread, thread_args, false)?;
// Add the task to the front of the queue, unless it
// should be deferred, in that case add it to the back
let mut queue = self.tasks_queue_blocking.borrow_mut();
let num_prev_blocking_tasks = queue.len();
if kind == TaskKind::Deferred {
queue.push_back(task_ref);
} else {
queue.push_front(task_ref);
}
/*
If we had any previous task and are currently async
waiting on tasks, we should send a signal to wake up
and run the new blocking task that was just queued
This can happen in cases such as an async http
server waking up from a connection and then wanting to
run a lua callback in response, to create the.. response
*/
if num_prev_blocking_tasks == 0 {
self.futures_tx
.send(TaskSchedulerMessage::NewBlockingTaskReady)
.expect("Futures waker channel was closed")
}
Ok(task_ref)
}
/**
Queues a new future to run on the task scheduler.
*/
pub(crate) fn queue_async_task(
&self,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
fut: impl Future<Output = TaskFutureRets<'fut>> + 'fut,
) -> LuaResult<TaskReference> {
let task_ref = self.create_task(TaskKind::Future, thread, thread_args, false)?;
let futs = self
.futures
.try_lock()
.expect("Tried to add future to queue during futures resumption");
futs.push(Box::pin(async move {
let result = fut.await;
(Some(task_ref), result)
}));
Ok(task_ref)
}
/**
Queues a new future to run on the task scheduler,
inheriting the task id of the currently running task.
*/
pub(crate) fn queue_async_task_inherited(
&self,
thread: LuaThread<'_>,
thread_args: Option<LuaMultiValue<'_>>,
fut: impl Future<Output = TaskFutureRets<'fut>> + 'fut,
) -> LuaResult<TaskReference> {
let task_ref = self.create_task(TaskKind::Future, thread, thread_args, true)?;
let futs = self
.futures
.try_lock()
.expect("Tried to add future to queue during futures resumption");
futs.push(Box::pin(async move {
let result = fut.await;
(Some(task_ref), result)
}));
Ok(task_ref)
}
/**
Waits for a task to complete.
Panics if the task is not currently in the scheduler.
*/
pub(crate) async fn wait_for_task_completion(
&self,
reference: TaskReference,
) -> LuaResult<LuaMultiValue> {
if !self.tasks.borrow().contains_key(&reference) {
panic!("Task does not exist in scheduler")
}
let state = TaskWaiterState::new();
{
let mut all_states = self.tasks_waiter_states.borrow_mut();
all_states
.entry(reference)
.or_insert_with(Vec::new)
.push(Arc::clone(&state));
}
TaskWaiterFuture::new(&state).await
}
/**
Wakes a task that has been completed and may have external code
waiting on it using [`TaskScheduler::wait_for_task_completion`].
*/
pub(super) fn wake_completed_task(
&self,
reference: TaskReference,
result: LuaResult<LuaMultiValue<'fut>>,
) {
if let Some(waiter_states) = self.tasks_waiter_states.borrow_mut().remove(&reference) {
for waiter_state in waiter_states {
waiter_state.try_lock().unwrap().finalize(result.clone());
}
}
}
}

View file

@ -1,46 +0,0 @@
use core::panic;
use mlua::prelude::*;
use tokio::sync::mpsc;
use super::scheduler_message::TaskSchedulerMessage;
/**
A handle to a registered asynchronous background task.
[`TaskSchedulerAsyncHandle::unregister`] must be
called upon completion of the background task to
prevent the task scheduler from running indefinitely.
*/
#[must_use = "Background tasks must be unregistered"]
#[derive(Debug)]
pub struct TaskSchedulerAsyncHandle {
unregistered: bool,
sender: mpsc::UnboundedSender<TaskSchedulerMessage>,
}
impl TaskSchedulerAsyncHandle {
pub fn new(sender: mpsc::UnboundedSender<TaskSchedulerMessage>) -> Self {
Self {
unregistered: false,
sender,
}
}
pub fn unregister(mut self, result: LuaResult<()>) {
self.unregistered = true;
self.sender
.send(TaskSchedulerMessage::Terminated(result))
.unwrap_or_else(|_| {
panic!(
"\
\nFailed to unregister background task - this is an internal error! \
\nPlease report it at {} \
\nDetails: Manual \
",
env!("CARGO_PKG_REPOSITORY")
)
});
}
}

View file

@ -1,11 +0,0 @@
use mlua::prelude::*;
/// Internal message enum for the task scheduler, used to notify
/// futures to wake up and schedule their respective blocking tasks
#[derive(Debug, Clone)]
pub enum TaskSchedulerMessage {
NewBlockingTaskReady,
NewLuaErrorReady(LuaError),
Spawned,
Terminated(LuaResult<()>),
}

View file

@ -1,172 +0,0 @@
use std::{fmt, process::ExitCode};
use mlua::prelude::*;
use super::scheduler::TaskScheduler;
/// Struct representing the current state of the task scheduler
#[derive(Debug, Clone)]
#[must_use = "Scheduler state must be checked after every resumption"]
pub struct TaskSchedulerState {
pub(super) lua_error: Option<LuaError>,
pub(super) exit_code: Option<ExitCode>,
pub(super) num_blocking: usize,
pub(super) num_futures: usize,
pub(super) num_background: usize,
}
impl TaskSchedulerState {
pub(super) fn new(sched: &TaskScheduler) -> Self {
Self {
lua_error: None,
exit_code: sched.exit_code.get(),
num_blocking: sched.tasks_count.get(),
num_futures: sched.futures_count.get(),
num_background: sched.futures_background_count.get(),
}
}
pub(super) fn err(sched: &TaskScheduler, err: LuaError) -> Self {
let mut this = Self::new(sched);
this.lua_error = Some(err);
this
}
/**
Returns a clone of the error from
this task scheduler result, if any.
*/
pub fn get_lua_error(&self) -> Option<LuaError> {
self.lua_error.clone()
}
/**
Returns a clone of the exit code from
this task scheduler result, if any.
*/
pub fn get_exit_code(&self) -> Option<ExitCode> {
self.exit_code
}
/**
Returns `true` if the task scheduler still
has blocking lua threads left to run.
*/
pub fn is_blocking(&self) -> bool {
self.num_blocking > 0
}
/**
Returns `true` if the task scheduler has finished all
blocking lua tasks, but still has yielding tasks running.
*/
pub fn is_yielding(&self) -> bool {
self.num_blocking == 0 && self.num_futures > 0
}
/**
Returns `true` if the task scheduler has finished all
lua threads, but still has background tasks running.
*/
pub fn is_background(&self) -> bool {
self.num_blocking == 0 && self.num_futures == 0 && self.num_background > 0
}
/**
Returns `true` if the task scheduler is done,
meaning it has no lua threads left to run, and
no spawned tasks are running in the background.
Also returns `true` if a task has requested to exit the process.
*/
pub fn is_done(&self) -> bool {
self.exit_code.is_some()
|| (self.num_blocking == 0 && self.num_futures == 0 && self.num_background == 0)
}
}
impl fmt::Display for TaskSchedulerState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let status = if self.is_blocking() {
"Busy"
} else if self.is_yielding() {
"Yielding"
} else if self.is_background() {
"Background"
} else {
"Done"
};
let code = match self.get_exit_code() {
Some(code) => format!("{code:?}"),
None => "-".to_string(),
};
let err = match self.get_lua_error() {
Some(e) => format!("{e:?}")
.as_bytes()
.chunks(42) // Kinda arbitrary but should fit in most terminals
.enumerate()
.map(|(idx, buf)| {
format!(
"{}{}{}{}{}",
if idx == 0 { "" } else { "\n" },
if idx == 0 {
"".to_string()
} else {
" ".repeat(16)
},
if idx == 0 { "" } else { "" },
String::from_utf8_lossy(buf),
if buf.len() == 42 { "" } else { "" },
)
})
.collect::<String>(),
None => "-".to_string(),
};
let parts = vec![
format!("Status │ {status}"),
format!("Tasks active │ {}", self.num_blocking),
format!("Tasks background │ {}", self.num_background),
format!("Status code │ {code}"),
format!("Lua error │ {err}"),
];
let lengths = parts
.iter()
.map(|part| {
part.lines()
.next()
.unwrap()
.trim_end_matches("")
.chars()
.count()
})
.collect::<Vec<_>>();
let longest = &parts
.iter()
.enumerate()
.fold(0, |acc, (index, _)| acc.max(lengths[index]));
let sep = "".repeat(longest + 2);
writeln!(f, "┌{}┐", &sep)?;
for (index, part) in parts.iter().enumerate() {
writeln!(
f,
"│ {}{} │",
part.trim_end_matches(""),
" ".repeat(
longest
- part
.lines()
.last()
.unwrap()
.trim_end_matches("")
.chars()
.count()
)
)?;
if index < parts.len() - 1 {
writeln!(f, "┝{}┥", &sep)?;
}
}
write!(f, "└{}┘", &sep)?;
Ok(())
}
}

View file

@ -1,39 +0,0 @@
use std::fmt;
/// Enum representing different kinds of tasks
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TaskKind {
Instant,
Deferred,
Future,
}
#[allow(dead_code)]
impl TaskKind {
pub fn is_instant(&self) -> bool {
*self == Self::Instant
}
pub fn is_deferred(&self) -> bool {
*self == Self::Deferred
}
pub fn is_blocking(&self) -> bool {
*self != Self::Future
}
pub fn is_future(&self) -> bool {
*self == Self::Future
}
}
impl fmt::Display for TaskKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let name: &'static str = match self {
TaskKind::Instant => "Instant",
TaskKind::Deferred => "Deferred",
TaskKind::Future => "Future",
};
write!(f, "{name}")
}
}

View file

@ -1,51 +0,0 @@
use std::{
fmt,
hash::{Hash, Hasher},
};
use mlua::prelude::*;
use super::task_kind::TaskKind;
/// A lightweight, copyable struct that represents a
/// task in the scheduler and is accessible from Lua
#[derive(Debug, Clone, Copy)]
pub struct TaskReference {
kind: TaskKind,
guid: usize,
}
impl TaskReference {
pub const fn new(kind: TaskKind, guid: usize) -> Self {
Self { kind, guid }
}
pub const fn id(&self) -> usize {
self.guid
}
}
impl fmt::Display for TaskReference {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.guid == 0 {
write!(f, "TaskReference(MAIN)")
} else {
write!(f, "TaskReference({} - {})", self.kind, self.guid)
}
}
}
impl Eq for TaskReference {}
impl PartialEq for TaskReference {
fn eq(&self, other: &Self) -> bool {
self.guid == other.guid
}
}
impl Hash for TaskReference {
fn hash<H: Hasher>(&self, state: &mut H) {
self.guid.hash(state);
}
}
impl LuaUserData for TaskReference {}

View file

@ -1,66 +0,0 @@
use std::{
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll, Waker},
};
use tokio::sync::Mutex as AsyncMutex;
use mlua::prelude::*;
#[derive(Debug, Clone)]
pub(super) struct TaskWaiterState<'fut> {
rets: Option<LuaResult<LuaMultiValue<'fut>>>,
waker: Option<Waker>,
}
impl<'fut> TaskWaiterState<'fut> {
pub fn new() -> Arc<AsyncMutex<Self>> {
Arc::new(AsyncMutex::new(TaskWaiterState {
rets: None,
waker: None,
}))
}
pub fn finalize(&mut self, rets: LuaResult<LuaMultiValue<'fut>>) {
self.rets = Some(rets);
if let Some(waker) = self.waker.take() {
waker.wake();
}
}
}
#[derive(Debug)]
pub(super) struct TaskWaiterFuture<'fut> {
state: Arc<AsyncMutex<TaskWaiterState<'fut>>>,
}
impl<'fut> TaskWaiterFuture<'fut> {
pub fn new(state: &Arc<AsyncMutex<TaskWaiterState<'fut>>>) -> Self {
Self {
state: Arc::clone(state),
}
}
}
impl<'fut> Clone for TaskWaiterFuture<'fut> {
fn clone(&self) -> Self {
Self {
state: Arc::clone(&self.state),
}
}
}
impl<'fut> Future for TaskWaiterFuture<'fut> {
type Output = LuaResult<LuaMultiValue<'fut>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut shared_state = self.state.try_lock().unwrap();
if let Some(rets) = shared_state.rets.clone() {
Poll::Ready(rets)
} else {
shared_state.waker = Some(cx.waker().clone());
Poll::Pending
}
}
}

View file

@ -1,107 +0,0 @@
use std::process::ExitCode;
use lua::task::{TaskScheduler, TaskSchedulerResumeExt, TaskSchedulerScheduleExt};
use mlua::prelude::*;
use tokio::task::LocalSet;
pub mod builtins;
pub mod importer;
pub mod lua;
mod error;
pub use error::LuneError;
#[derive(Clone, Debug, Default)]
pub struct Lune {
args: Vec<String>,
}
impl Lune {
/**
Creates a new Lune script runner.
*/
pub fn new() -> Self {
Self::default()
}
/**
Arguments to give in `process.args` for a Lune script.
*/
pub fn with_args<V>(mut self, args: V) -> Self
where
V: Into<Vec<String>>,
{
self.args = args.into();
self
}
/**
Runs a Lune script.
This will create a new sandboxed Luau environment with the configured
globals and arguments, running inside of a [`tokio::task::LocalSet`].
Some Lune globals may spawn separate tokio tasks on other threads, but the Luau
environment itself is guaranteed to run on a single thread in the local set.
Note that this will create a static Lua instance and task scheduler that will
both live for the remainer of the program, and that this leaks memory using
[`Box::leak`] that will then get deallocated when the program exits.
*/
pub async fn run(
&self,
script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>,
) -> Result<ExitCode, LuneError> {
self.run_inner(script_name, script_contents)
.await
.map_err(LuneError::from)
}
async fn run_inner(
&self,
script_name: impl AsRef<str>,
script_contents: impl AsRef<[u8]>,
) -> Result<ExitCode, LuaError> {
// Create our special lune-flavored Lua object with extra registry values
let lua = lua::create_lune_lua()?.into_static();
// Create our task scheduler and all globals
// NOTE: Some globals require the task scheduler to exist on startup
let sched = TaskScheduler::new(lua)?.into_static();
lua.set_app_data(sched);
importer::create(lua, self.args.clone())?;
// Create the main thread and schedule it
let main_chunk = lua
.load(script_contents.as_ref())
.set_name(script_name.as_ref())
.into_function()?;
let main_thread = lua.create_thread(main_chunk)?;
let main_thread_args = LuaValue::Nil.into_lua_multi(lua)?;
sched.schedule_blocking(main_thread, main_thread_args)?;
// Keep running the scheduler until there are either no tasks
// left to run, or until a task requests to exit the process
let exit_code = LocalSet::new()
.run_until(async move {
let mut got_error = false;
loop {
let result = sched.resume_queue().await;
if let Some(err) = result.get_lua_error() {
eprintln!("{}", LuneError::from(err));
got_error = true;
}
if result.is_done() {
if let Some(exit_code) = result.get_exit_code() {
break exit_code;
} else if got_error {
break ExitCode::FAILURE;
} else {
break ExitCode::SUCCESS;
}
}
}
})
.await;
Ok(exit_code)
}
}