mirror of
https://github.com/lune-org/lune.git
synced 2024-12-13 13:30:38 +00:00
Implement task library & test suite, mostly
This commit is contained in:
parent
8ab5855ccc
commit
f8a2eb79d4
13 changed files with 188 additions and 85 deletions
|
@ -5,7 +5,7 @@ use crate::utils::{
|
|||
table_builder::ReadonlyTableBuilder,
|
||||
};
|
||||
|
||||
pub fn new(lua: &Lua) -> Result<Table> {
|
||||
pub async fn new(lua: &Lua) -> Result<Table> {
|
||||
let print = |args: &MultiValue, throw: bool| -> Result<()> {
|
||||
let s = pretty_format_multi_value(args)?;
|
||||
if throw {
|
||||
|
|
|
@ -5,7 +5,7 @@ use tokio::fs;
|
|||
|
||||
use crate::utils::table_builder::ReadonlyTableBuilder;
|
||||
|
||||
pub fn new(lua: &Lua) -> Result<Table> {
|
||||
pub async fn new(lua: &Lua) -> Result<Table> {
|
||||
ReadonlyTableBuilder::new(lua)?
|
||||
.with_async_function("readFile", fs_read_file)?
|
||||
.with_async_function("readDir", fs_read_dir)?
|
||||
|
|
|
@ -9,5 +9,3 @@ pub use fs::new as new_fs;
|
|||
pub use net::new as new_net;
|
||||
pub use process::new as new_process;
|
||||
pub use task::new as new_task;
|
||||
|
||||
pub use task::WaitingThread as WaitingTaskThread;
|
||||
|
|
|
@ -8,7 +8,7 @@ use reqwest::{
|
|||
|
||||
use crate::utils::{net::get_request_user_agent_header, table_builder::ReadonlyTableBuilder};
|
||||
|
||||
pub fn new(lua: &Lua) -> Result<Table> {
|
||||
pub async fn new(lua: &Lua) -> Result<Table> {
|
||||
ReadonlyTableBuilder::new(lua)?
|
||||
.with_function("jsonEncode", net_json_encode)?
|
||||
.with_function("jsonDecode", net_json_decode)?
|
||||
|
|
|
@ -9,7 +9,7 @@ use tokio::process::Command;
|
|||
|
||||
use crate::utils::table_builder::ReadonlyTableBuilder;
|
||||
|
||||
pub fn new(lua: &Lua, args_vec: Vec<String>) -> Result<Table> {
|
||||
pub async fn new(lua: &Lua, args_vec: Vec<String>) -> Result<Table> {
|
||||
// Create readonly args array
|
||||
let inner_args = lua.create_table()?;
|
||||
for arg in &args_vec {
|
||||
|
|
|
@ -1,60 +1,40 @@
|
|||
use std::{
|
||||
sync::{Arc, Mutex},
|
||||
time::Duration,
|
||||
};
|
||||
use std::{thread::sleep, time::Duration};
|
||||
|
||||
use mlua::{Function, Lua, Result, Table, Thread, Value, Variadic};
|
||||
use tokio::time;
|
||||
use mlua::{Function, Lua, Result, Table, Value};
|
||||
|
||||
use crate::utils::table_builder::ReadonlyTableBuilder;
|
||||
|
||||
const DEFAULT_SLEEP_DURATION: f32 = 1.0 / 60.0;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct WaitingThread<'a> {
|
||||
is_delayed_for: Option<f32>,
|
||||
is_deferred: Option<bool>,
|
||||
thread: Thread<'a>,
|
||||
args: Variadic<Value<'a>>,
|
||||
}
|
||||
const TASK_LIB_LUAU: &str = include_str!("../luau/task.luau");
|
||||
|
||||
pub fn new<'a>(lua: &'a Lua, _threads: &Arc<Mutex<Vec<WaitingThread<'a>>>>) -> Result<Table<'a>> {
|
||||
// TODO: Figure out how to insert into threads vec
|
||||
pub async fn new(lua: &Lua) -> Result<Table> {
|
||||
let task_lib: Table = lua
|
||||
.load(TASK_LIB_LUAU)
|
||||
.set_name("task")?
|
||||
.eval_async()
|
||||
.await?;
|
||||
// FUTURE: Properly implementing the task library in async rust is
|
||||
// very complicated but should be done at some point, for now we will
|
||||
// fall back to implementing only task.wait and doing the rest in lua
|
||||
let task_cancel: Function = task_lib.raw_get("cancel")?;
|
||||
let task_defer: Function = task_lib.raw_get("defer")?;
|
||||
let task_delay: Function = task_lib.raw_get("delay")?;
|
||||
let task_spawn: Function = task_lib.raw_get("spawn")?;
|
||||
ReadonlyTableBuilder::new(lua)?
|
||||
.with_function("cancel", |lua, thread: Thread| {
|
||||
thread.reset(lua.create_function(|_, _: ()| Ok(()))?)?;
|
||||
Ok(())
|
||||
})?
|
||||
.with_async_function(
|
||||
"defer",
|
||||
|lua, (func, args): (Function, Variadic<Value>)| async move {
|
||||
let thread = lua.create_thread(func)?;
|
||||
thread.into_async(args).await?;
|
||||
Ok(())
|
||||
},
|
||||
)?
|
||||
.with_async_function(
|
||||
"delay",
|
||||
|lua, (duration, func, args): (Option<f32>, Function, Variadic<Value>)| async move {
|
||||
let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION);
|
||||
time::sleep(Duration::from_secs_f32(secs)).await;
|
||||
let thread = lua.create_thread(func)?;
|
||||
thread.into_async(args).await?;
|
||||
Ok(())
|
||||
},
|
||||
)?
|
||||
.with_async_function(
|
||||
"spawn",
|
||||
|lua, (func, args): (Function, Variadic<Value>)| async move {
|
||||
let thread = lua.create_thread(func)?;
|
||||
thread.into_async(args).await?;
|
||||
Ok(())
|
||||
},
|
||||
)?
|
||||
.with_async_function("wait", |_, duration: Option<f32>| async move {
|
||||
let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION);
|
||||
time::sleep(Duration::from_secs_f32(secs)).await;
|
||||
Ok(secs)
|
||||
})?
|
||||
.with_value("cancel", Value::Function(task_cancel))?
|
||||
.with_value("defer", Value::Function(task_defer))?
|
||||
.with_value("delay", Value::Function(task_delay))?
|
||||
.with_value("spawn", Value::Function(task_spawn))?
|
||||
.with_function("wait", wait)?
|
||||
.build()
|
||||
}
|
||||
|
||||
// FIXME: It does seem possible to properly make an async wait
|
||||
// function with mlua right now, something breaks when using
|
||||
// async wait functions inside of coroutines
|
||||
fn wait(_: &Lua, duration: Option<f32>) -> Result<f32> {
|
||||
let secs = duration.unwrap_or(DEFAULT_SLEEP_DURATION);
|
||||
sleep(Duration::from_secs_f32(secs));
|
||||
Ok(secs)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use mlua::Lua;
|
||||
|
||||
|
@ -13,16 +11,15 @@ use crate::{
|
|||
|
||||
pub async fn run_lune(name: &str, chunk: &str, args: Vec<String>) -> Result<()> {
|
||||
let lua = Lua::new();
|
||||
let threads = Arc::new(Mutex::new(Vec::new()));
|
||||
lua.sandbox(true)?;
|
||||
// Add in all globals
|
||||
{
|
||||
let globals = lua.globals();
|
||||
globals.raw_set("console", new_console(&lua)?)?;
|
||||
globals.raw_set("fs", new_fs(&lua)?)?;
|
||||
globals.raw_set("net", new_net(&lua)?)?;
|
||||
globals.raw_set("process", new_process(&lua, args.clone())?)?;
|
||||
globals.raw_set("task", new_task(&lua, &threads)?)?;
|
||||
globals.raw_set("console", new_console(&lua).await?)?;
|
||||
globals.raw_set("fs", new_fs(&lua).await?)?;
|
||||
globals.raw_set("net", new_net(&lua).await?)?;
|
||||
globals.raw_set("process", new_process(&lua, args.clone()).await?)?;
|
||||
globals.raw_set("task", new_task(&lua).await?)?;
|
||||
globals.set_readonly(true);
|
||||
}
|
||||
// Run the requested chunk asynchronously
|
||||
|
|
112
src/lib/luau/task.luau
Normal file
112
src/lib/luau/task.luau
Normal file
|
@ -0,0 +1,112 @@
|
|||
local MINIMUM_DELAY_TIME = 1 / 100
|
||||
|
||||
type ThreadOrFunction<A..., R...> = thread | (A...) -> R...
|
||||
type AnyThreadOrFunction = ThreadOrFunction<...any, ...any>
|
||||
|
||||
type WaitingThreadKind = "Normal" | "Deferred" | "Delayed"
|
||||
type WaitingThread = {
|
||||
idx: number,
|
||||
kind: WaitingThreadKind,
|
||||
thread: thread,
|
||||
args: { [number]: any, n: number },
|
||||
}
|
||||
|
||||
local waitingThreadCounter = 0
|
||||
local waitingThreads: { WaitingThread } = {}
|
||||
|
||||
local function scheduleWaitingThreads()
|
||||
-- Grab currently waiting threads and clear the queue but keep capacity
|
||||
local threadsToResume: { WaitingThread } = table.clone(waitingThreads)
|
||||
table.clear(waitingThreads)
|
||||
table.sort(threadsToResume, function(t0, t1)
|
||||
local k0: WaitingThreadKind = t0.kind
|
||||
local k1: WaitingThreadKind = t1.kind
|
||||
if k0 == k1 then
|
||||
return t0.idx < t1.idx
|
||||
end
|
||||
if k0 == "Normal" then
|
||||
return true
|
||||
elseif k1 == "Normal" then
|
||||
return false
|
||||
elseif k0 == "Deferred" then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end)
|
||||
-- Resume threads in order, giving args & waiting if necessary
|
||||
for _, waitingThread in threadsToResume do
|
||||
coroutine.resume(
|
||||
waitingThread.thread,
|
||||
table.unpack(waitingThread.args, 1, waitingThread.args.n)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
local function insertWaitingThread(kind: WaitingThreadKind, tof: AnyThreadOrFunction, ...: any)
|
||||
if typeof(tof) ~= "thread" and typeof(tof) ~= "function" then
|
||||
if tof == nil then
|
||||
error("Expected thread or function, got nil", 3)
|
||||
end
|
||||
error(
|
||||
string.format("Expected thread or function, got %s %s", typeof(tof), tostring(tof)),
|
||||
3
|
||||
)
|
||||
end
|
||||
local thread = if type(tof) == "function" then coroutine.create(tof) else tof
|
||||
waitingThreadCounter += 1
|
||||
local waitingThread: WaitingThread = {
|
||||
idx = waitingThreadCounter,
|
||||
kind = kind,
|
||||
thread = thread,
|
||||
args = table.pack(...),
|
||||
}
|
||||
table.insert(waitingThreads, waitingThread)
|
||||
return waitingThread
|
||||
end
|
||||
|
||||
local function cancel(thread: unknown)
|
||||
if typeof(thread) ~= "thread" then
|
||||
if thread == nil then
|
||||
error("Expected thread, got nil", 2)
|
||||
end
|
||||
error(string.format("Expected thread, got %s %s", typeof(thread), tostring(thread)), 2)
|
||||
else
|
||||
coroutine.close(thread)
|
||||
end
|
||||
end
|
||||
|
||||
local function defer(tof: AnyThreadOrFunction, ...: any): thread
|
||||
local waiting = insertWaitingThread("Deferred", tof, ...)
|
||||
local original = waiting.thread
|
||||
waiting.thread = coroutine.create(function(...)
|
||||
task.wait(1 / 1_000_000)
|
||||
coroutine.resume(original, ...)
|
||||
end)
|
||||
scheduleWaitingThreads()
|
||||
return waiting.thread
|
||||
end
|
||||
|
||||
local function delay(delay: number?, tof: AnyThreadOrFunction, ...: any): thread
|
||||
local waiting = insertWaitingThread("Delayed", tof, ...)
|
||||
local original = waiting.thread
|
||||
waiting.thread = coroutine.create(function(...)
|
||||
task.wait(math.max(MINIMUM_DELAY_TIME, delay or 0))
|
||||
coroutine.resume(original, ...)
|
||||
end)
|
||||
scheduleWaitingThreads()
|
||||
return waiting.thread
|
||||
end
|
||||
|
||||
local function spawn(tof: AnyThreadOrFunction, ...: any): thread
|
||||
local waiting = insertWaitingThread("Normal", tof, ...)
|
||||
scheduleWaitingThreads()
|
||||
return waiting.thread
|
||||
end
|
||||
|
||||
return {
|
||||
cancel = cancel,
|
||||
defer = defer,
|
||||
delay = delay,
|
||||
spawn = spawn,
|
||||
}
|
|
@ -18,9 +18,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> {
|
|||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_table(self, key: &'static str, value: Table) -> Result<Self> {
|
||||
self.tab.raw_set(key, value)?;
|
||||
Ok(self)
|
||||
pub fn with_table(self, key: &'static str, table: Table) -> Result<Self> {
|
||||
self.with_value(key, Value::Table(table))
|
||||
}
|
||||
|
||||
pub fn with_function<A, R, F>(self, key: &'static str, func: F) -> Result<Self>
|
||||
|
@ -29,9 +28,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> {
|
|||
R: ToLuaMulti<'lua>,
|
||||
F: 'static + Fn(&'lua Lua, A) -> Result<R>,
|
||||
{
|
||||
let value = self.lua.create_function(func)?;
|
||||
self.tab.raw_set(key, value)?;
|
||||
Ok(self)
|
||||
let f = self.lua.create_function(func)?;
|
||||
self.with_value(key, Value::Function(f))
|
||||
}
|
||||
|
||||
pub fn with_async_function<A, R, F, FR>(self, key: &'static str, func: F) -> Result<Self>
|
||||
|
@ -41,9 +39,8 @@ impl<'lua> ReadonlyTableBuilder<'lua> {
|
|||
F: 'static + Fn(&'lua Lua, A) -> FR,
|
||||
FR: 'lua + Future<Output = Result<R>>,
|
||||
{
|
||||
let value = self.lua.create_async_function(func)?;
|
||||
self.tab.raw_set(key, value)?;
|
||||
Ok(self)
|
||||
let f = self.lua.create_async_function(func)?;
|
||||
self.with_value(key, Value::Function(f))
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Table<'lua>> {
|
||||
|
|
|
@ -4,16 +4,20 @@ local flag: boolean = false
|
|||
task.defer(function()
|
||||
flag = true
|
||||
end)
|
||||
assert(not flag, "Defer should run after other threads, including the main thread")
|
||||
assert(not flag, "Defer should not run instantly or block")
|
||||
task.wait(1 / 60)
|
||||
assert(flag, "Defer should run")
|
||||
|
||||
-- Deferred functions should work with yielding
|
||||
|
||||
local flag2: boolean = false
|
||||
task.defer(function()
|
||||
task.wait()
|
||||
task.wait(1 / 60)
|
||||
flag2 = true
|
||||
end)
|
||||
assert(not flag2, "Defer should work with yielding")
|
||||
assert(not flag2, "Defer should work with yielding (1)")
|
||||
task.wait(1 / 30)
|
||||
assert(flag2, "Defer should work with yielding (2)")
|
||||
|
||||
-- Deferred functions should run after other spawned threads
|
||||
local flag3: boolean = false
|
||||
|
|
|
@ -4,20 +4,22 @@ local flag: boolean = false
|
|||
task.delay(0, function()
|
||||
flag = true
|
||||
end)
|
||||
assert(not flag, "Delay should never run instantly")
|
||||
assert(not flag, "Delay should not run instantly or block")
|
||||
task.wait(1 / 60)
|
||||
assert(flag, "Delay should run after the wanted duration")
|
||||
|
||||
-- Delayed functions should work with yielding
|
||||
|
||||
local flag2: boolean = false
|
||||
task.delay(0.2, function()
|
||||
flag2 = true
|
||||
task.wait(0.2)
|
||||
task.wait(0.4)
|
||||
flag2 = false
|
||||
end)
|
||||
task.wait(0.25)
|
||||
assert(flag2, "Delay should work with yielding")
|
||||
task.wait(0.25)
|
||||
assert(not flag2, "Delay should work with yielding")
|
||||
task.wait(0.4)
|
||||
assert(flag, "Delay should work with yielding (1)")
|
||||
task.wait(0.4)
|
||||
assert(not flag2, "Delay should work with yielding (2)")
|
||||
|
||||
-- Varargs should get passed correctly
|
||||
|
||||
|
|
|
@ -4,16 +4,18 @@ local flag: boolean = false
|
|||
task.spawn(function()
|
||||
flag = true
|
||||
end)
|
||||
assert(flag, "Spawn should run instantly until yielded")
|
||||
assert(flag, "Spawn should run instantly")
|
||||
|
||||
-- Spawned functions should work with yielding
|
||||
|
||||
local flag2: boolean = false
|
||||
task.spawn(function()
|
||||
task.wait()
|
||||
task.wait(0.1)
|
||||
flag2 = true
|
||||
end)
|
||||
assert(not flag2, "Spawn should work with yielding")
|
||||
assert(not flag2, "Spawn should work with yielding (1)")
|
||||
task.wait(0.2)
|
||||
assert(flag2, "Spawn should work with yielding (2)")
|
||||
|
||||
-- Varargs should get passed correctly
|
||||
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
-- Wait should work everywhere
|
||||
|
||||
local flag: boolean = false
|
||||
coroutine.wrap(function()
|
||||
task.wait(0.1)
|
||||
flag = true
|
||||
end)()
|
||||
assert(flag, "Wait failed while in a coroutine")
|
||||
|
||||
-- Wait should be accurate
|
||||
|
||||
local DEFAULT = 1 / 60
|
||||
local EPSILON = 1 / 100
|
||||
|
||||
|
|
Loading…
Reference in a new issue