use std::{collections::HashSet, process::ExitCode, sync::Arc}; use anyhow::{bail, Result}; use mlua::prelude::*; use tokio::{sync::mpsc, task}; pub mod globals; pub mod utils; use crate::{ globals::{create_console, create_fs, create_net, create_process, create_require, create_task}, utils::formatting::pretty_format_luau_error, }; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum LuneGlobal { Console, Fs, Net, Process, Require, Task, } impl LuneGlobal { pub fn get_all() -> Vec { vec![ Self::Console, Self::Fs, Self::Net, Self::Process, Self::Require, Self::Task, ] } } #[derive(Debug)] pub(crate) enum LuneMessage { Exit(u8), Spawned, Finished, LuaError(mlua::Error), } #[derive(Clone, Debug, Default)] pub struct Lune { globals: HashSet, args: Vec, } impl Lune { pub fn new() -> Self { Self::default() } pub fn with_args(mut self, args: Vec) -> Self { self.args = args; self } pub fn with_global(mut self, global: LuneGlobal) -> Self { self.globals.insert(global); self } pub fn with_all_globals(mut self) -> Self { for global in LuneGlobal::get_all() { self.globals.insert(global); } self } pub async fn run(&self, name: &str, chunk: &str) -> Result { let task_set = task::LocalSet::new(); let (sender, mut receiver) = mpsc::channel::(64); let lua = Arc::new(mlua::Lua::new()); let snd = Arc::new(sender); lua.set_app_data(Arc::downgrade(&lua)); lua.set_app_data(Arc::downgrade(&snd)); // Add in wanted lune globals for global in &self.globals { match &global { LuneGlobal::Console => create_console(&lua)?, LuneGlobal::Fs => create_fs(&lua)?, LuneGlobal::Net => create_net(&lua)?, LuneGlobal::Process => create_process(&lua, self.args.clone())?, LuneGlobal::Require => create_require(&lua)?, LuneGlobal::Task => create_task(&lua)?, } } // Spawn the main thread from our entrypoint script let script_lua = lua.clone(); let script_name = name.to_string(); let script_chunk = chunk.to_string(); let script_sender = snd.clone(); script_sender.send(LuneMessage::Spawned).await?; task_set.spawn_local(async move { let result = script_lua .load(&script_chunk) .set_name(&format!("={}", script_name)) .unwrap() .eval_async::() .await; match result { Err(e) => script_sender.send(LuneMessage::LuaError(e)).await, Ok(_) => script_sender.send(LuneMessage::Finished).await, } }); // Run the executor until there are no tasks left, // taking care to not exit right away for errors let (got_code, got_error, exit_code) = task_set .run_until(async { let mut task_count = 0; let mut got_error = false; let mut got_code = false; let mut exit_code = 0; while let Some(message) = receiver.recv().await { // Make sure our task-count-modifying messages are sent correctly, one // task spawned must always correspond to one task finished / errored match &message { LuneMessage::Exit(_) => {} LuneMessage::Spawned => {} message => { if task_count == 0 { bail!( "Got message while task count was 0!\nMessage: {:#?}", message ) } } } // Handle whatever message we got match message { LuneMessage::Exit(code) => { exit_code = code; got_code = true; break; } LuneMessage::Spawned => task_count += 1, LuneMessage::Finished => task_count -= 1, LuneMessage::LuaError(e) => { eprintln!("{}", pretty_format_luau_error(&e)); got_error = true; task_count += 1; } }; // If there are no tasks left running, it is now // safe to close the receiver and end execution if task_count == 0 { receiver.close(); } } Ok((got_code, got_error, exit_code)) }) .await?; // If we got an error, we will default to exiting // with code 1, unless a code was manually given if got_code { Ok(ExitCode::from(exit_code)) } else if got_error { Ok(ExitCode::FAILURE) } else { Ok(ExitCode::SUCCESS) } } } #[cfg(test)] mod tests { use std::process::ExitCode; use anyhow::Result; use tokio::fs::read_to_string; use crate::Lune; const ARGS: &[&str] = &["Foo", "Bar"]; macro_rules! run_tests { ($($name:ident: $value:expr,)*) => { $( #[tokio::test] async fn $name() -> Result { let full_name = format!("src/tests/{}.luau", $value); let script = read_to_string(&full_name) .await .unwrap(); let lune = Lune::new() .with_args( ARGS .clone() .iter() .map(ToString::to_string) .collect() ) .with_all_globals(); let script_name = full_name.strip_suffix(".luau").unwrap(); lune.run(&script_name, &script).await } )* } } run_tests! { console_format: "console/format", console_set_color: "console/set_color", console_set_style: "console/set_style", fs_files: "fs/files", fs_dirs: "fs/dirs", net_request_codes: "net/request/codes", net_request_methods: "net/request/methods", net_request_redirect: "net/request/redirect", net_json_decode: "net/json/decode", net_json_encode: "net/json/encode", net_serve: "net/serve", process_args: "process/args", process_cwd: "process/cwd", process_env: "process/env", process_exit: "process/exit", process_spawn: "process/spawn", require_children: "require/tests/children", require_invalid: "require/tests/invalid", require_nested: "require/tests/nested", require_parents: "require/tests/parents", require_siblings: "require/tests/siblings", task_cancel: "task/cancel", task_defer: "task/defer", task_delay: "task/delay", task_spawn: "task/spawn", task_wait: "task/wait", } }