Release 1.0
This commit is contained in:
parent
009ea6da0e
commit
730a8b61d7
32 changed files with 6629 additions and 0 deletions
340
breadd/src/lua/mod.rs
Normal file
340
breadd/src/lua/mod.rs
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use bread_shared::{AdapterSource, BreadEvent};
|
||||
use mlua::{Function, Lua, LuaSerdeExt, RegistryKey, Value};
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::core::config::Config;
|
||||
use crate::core::state_engine::StateHandle;
|
||||
use crate::core::subscriptions::SubscriptionId;
|
||||
use crate::core::types::ModuleLoadState;
|
||||
|
||||
pub enum LuaMessage {
|
||||
Event {
|
||||
subscription_id: SubscriptionId,
|
||||
event: BreadEvent,
|
||||
},
|
||||
SubscriptionCancelled {
|
||||
id: SubscriptionId,
|
||||
},
|
||||
Reload {
|
||||
reply: oneshot::Sender<std::result::Result<(), String>>,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RuntimeHandle {
|
||||
tx: mpsc::UnboundedSender<LuaMessage>,
|
||||
}
|
||||
|
||||
impl RuntimeHandle {
|
||||
pub fn sender(&self) -> mpsc::UnboundedSender<LuaMessage> {
|
||||
self.tx.clone()
|
||||
}
|
||||
|
||||
pub async fn reload(&self) -> Result<()> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.tx
|
||||
.send(LuaMessage::Reload { reply: tx })
|
||||
.map_err(|_| anyhow!("lua runtime channel closed"))?;
|
||||
match rx.await {
|
||||
Ok(Ok(())) => Ok(()),
|
||||
Ok(Err(err)) => Err(anyhow!(err)),
|
||||
Err(_) => Err(anyhow!("lua runtime dropped reload response")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shutdown(&self) {
|
||||
let _ = self.tx.send(LuaMessage::Shutdown);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spawn_runtime(
|
||||
config: Config,
|
||||
state_handle: StateHandle,
|
||||
emit_tx: mpsc::UnboundedSender<BreadEvent>,
|
||||
) -> Result<RuntimeHandle> {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
let handle = RuntimeHandle { tx };
|
||||
let thread_tx = handle.tx.clone();
|
||||
|
||||
std::thread::Builder::new()
|
||||
.name("breadd-lua".to_string())
|
||||
.spawn(move || {
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.expect("failed to create lua runtime thread");
|
||||
|
||||
rt.block_on(async move {
|
||||
let mut engine = match LuaEngine::new(config, state_handle, emit_tx) {
|
||||
Ok(engine) => engine,
|
||||
Err(err) => {
|
||||
error!(error = %err, "failed to initialize lua engine");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = engine.reload_internal() {
|
||||
error!(error = %err, "initial lua load failed");
|
||||
}
|
||||
|
||||
while let Some(msg) = rx.recv().await {
|
||||
match msg {
|
||||
LuaMessage::Event {
|
||||
subscription_id,
|
||||
event,
|
||||
} => {
|
||||
if let Err(err) = engine.handle_event(subscription_id, event) {
|
||||
error!(error = %err, "lua event handler failed");
|
||||
}
|
||||
}
|
||||
LuaMessage::SubscriptionCancelled { id } => {
|
||||
engine.remove_handler(id);
|
||||
}
|
||||
LuaMessage::Reload { reply } => {
|
||||
let result = engine.reload_internal().map_err(|e| e.to_string());
|
||||
let _ = reply.send(result);
|
||||
}
|
||||
LuaMessage::Shutdown => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("lua runtime thread exiting");
|
||||
});
|
||||
})?;
|
||||
|
||||
let _ = thread_tx;
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
struct LuaEngine {
|
||||
lua: Lua,
|
||||
handlers: Arc<Mutex<HashMap<SubscriptionId, RegistryKey>>>,
|
||||
next_sub_id: Arc<AtomicU64>,
|
||||
state_handle: StateHandle,
|
||||
emit_tx: mpsc::UnboundedSender<BreadEvent>,
|
||||
entry_point: PathBuf,
|
||||
module_path: PathBuf,
|
||||
}
|
||||
|
||||
impl LuaEngine {
|
||||
fn new(config: Config, state_handle: StateHandle, emit_tx: mpsc::UnboundedSender<BreadEvent>) -> Result<Self> {
|
||||
Ok(Self {
|
||||
lua: Lua::new(),
|
||||
handlers: Arc::new(Mutex::new(HashMap::new())),
|
||||
next_sub_id: Arc::new(AtomicU64::new(1)),
|
||||
state_handle,
|
||||
emit_tx,
|
||||
entry_point: config.lua_entry_point(),
|
||||
module_path: config.lua_module_path(),
|
||||
})
|
||||
}
|
||||
|
||||
fn reload_internal(&mut self) -> Result<()> {
|
||||
self.state_handle.clear_subscriptions();
|
||||
self.lua = Lua::new();
|
||||
self.handlers
|
||||
.lock()
|
||||
.expect("lua handlers mutex poisoned")
|
||||
.clear();
|
||||
|
||||
self.install_api()?;
|
||||
self.load_init_and_modules()?;
|
||||
info!("lua runtime reloaded");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_api(&self) -> Result<()> {
|
||||
let globals = self.lua.globals();
|
||||
let bread = self.lua.create_table()?;
|
||||
|
||||
let handlers = self.handlers.clone();
|
||||
let next_sub_id = self.next_sub_id.clone();
|
||||
let state_handle = self.state_handle.clone();
|
||||
let on_fn = self.lua.create_function(move |lua, (pattern, callback): (String, Function)| {
|
||||
let id = SubscriptionId(next_sub_id.fetch_add(1, Ordering::Relaxed));
|
||||
let key = lua.create_registry_value(callback)?;
|
||||
handlers
|
||||
.lock()
|
||||
.map_err(|_| mlua::Error::external("handler lock poisoned"))?
|
||||
.insert(id, key);
|
||||
state_handle
|
||||
.register_subscription(id, pattern, false)
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(id.0)
|
||||
})?;
|
||||
bread.set("on", on_fn)?;
|
||||
|
||||
let handlers = self.handlers.clone();
|
||||
let next_sub_id = self.next_sub_id.clone();
|
||||
let state_handle = self.state_handle.clone();
|
||||
let once_fn = self.lua.create_function(move |lua, (pattern, callback): (String, Function)| {
|
||||
let id = SubscriptionId(next_sub_id.fetch_add(1, Ordering::Relaxed));
|
||||
let key = lua.create_registry_value(callback)?;
|
||||
handlers
|
||||
.lock()
|
||||
.map_err(|_| mlua::Error::external("handler lock poisoned"))?
|
||||
.insert(id, key);
|
||||
state_handle
|
||||
.register_subscription(id, pattern, true)
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(id.0)
|
||||
})?;
|
||||
bread.set("once", once_fn)?;
|
||||
|
||||
let emit_tx = self.emit_tx.clone();
|
||||
let emit_fn = self.lua.create_function(move |lua, (event_name, payload): (String, Value)| {
|
||||
let data = match payload {
|
||||
Value::Nil => serde_json::json!({}),
|
||||
other => lua
|
||||
.from_value::<serde_json::Value>(other)
|
||||
.unwrap_or_else(|_| serde_json::json!({})),
|
||||
};
|
||||
emit_tx
|
||||
.send(BreadEvent::new(event_name, AdapterSource::System, data))
|
||||
.map_err(|_| mlua::Error::external("event channel closed"))?;
|
||||
Ok(())
|
||||
})?;
|
||||
bread.set("emit", emit_fn)?;
|
||||
|
||||
let state_arc = self.state_handle.state_arc();
|
||||
let state_tbl = self.lua.create_table()?;
|
||||
let get_fn = self.lua.create_function(move |lua, path: String| {
|
||||
let snapshot = state_arc.blocking_read();
|
||||
let mut value = serde_json::to_value(&*snapshot)
|
||||
.map_err(|e| mlua::Error::external(e.to_string()))?;
|
||||
if path.is_empty() {
|
||||
return lua
|
||||
.to_value(&value)
|
||||
.map_err(|e| mlua::Error::external(e.to_string()));
|
||||
}
|
||||
for part in path.split('.') {
|
||||
value = value
|
||||
.get(part)
|
||||
.cloned()
|
||||
.ok_or_else(|| mlua::Error::external("state path not found"))?;
|
||||
}
|
||||
lua.to_value(&value)
|
||||
.map_err(|e| mlua::Error::external(e.to_string()))
|
||||
})?;
|
||||
state_tbl.set("get", get_fn)?;
|
||||
bread.set("state", state_tbl)?;
|
||||
|
||||
let profile_tbl = self.lua.create_table()?;
|
||||
let state_handle = self.state_handle.clone();
|
||||
let activate_fn = self.lua.create_function(move |_lua, name: String| {
|
||||
state_handle.set_profile(name.clone());
|
||||
Ok(())
|
||||
})?;
|
||||
profile_tbl.set("activate", activate_fn)?;
|
||||
bread.set("profile", profile_tbl)?;
|
||||
|
||||
let exec_fn = self.lua.create_function(move |_lua, cmd: String| {
|
||||
let status = std::process::Command::new("sh")
|
||||
.arg("-lc")
|
||||
.arg(&cmd)
|
||||
.status()
|
||||
.map_err(mlua::Error::external)?;
|
||||
Ok(status.code().unwrap_or_default())
|
||||
})?;
|
||||
bread.set("exec", exec_fn)?;
|
||||
|
||||
globals.set("bread", bread)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_init_and_modules(&self) -> Result<()> {
|
||||
self.load_lua_file(&self.entry_point, "init")?;
|
||||
|
||||
let mut files = list_lua_files(&self.module_path)?;
|
||||
files.sort();
|
||||
for path in files {
|
||||
let module_name = path
|
||||
.file_stem()
|
||||
.and_then(|v| v.to_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
match self.load_lua_file(&path, &module_name) {
|
||||
Ok(()) => {
|
||||
self.state_handle
|
||||
.set_module_status(module_name, ModuleLoadState::Loaded, None);
|
||||
}
|
||||
Err(err) => {
|
||||
self.state_handle.set_module_status(
|
||||
module_name,
|
||||
ModuleLoadState::LoadError,
|
||||
Some(err.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_lua_file(&self, path: &Path, module_name: &str) -> Result<()> {
|
||||
if !path.exists() {
|
||||
warn!(path = %path.display(), "lua file does not exist; skipping");
|
||||
self.state_handle.set_module_status(
|
||||
module_name.to_string(),
|
||||
ModuleLoadState::NotFound,
|
||||
None,
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let src = fs::read_to_string(path)?;
|
||||
self.lua.load(&src).set_name(path.to_string_lossy().as_ref()).exec()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_event(&self, id: SubscriptionId, event: BreadEvent) -> Result<()> {
|
||||
let handlers = self.handlers.lock().expect("lua handlers mutex poisoned");
|
||||
let Some(reg) = handlers.get(&id) else {
|
||||
return Ok(());
|
||||
};
|
||||
let callback: Function = self.lua.registry_value(reg)?;
|
||||
let event_value = self.lua.to_value(&event)?;
|
||||
if let Err(err) = callback.call::<_, ()>(event_value) {
|
||||
error!(subscription = id.0, error = %err, "lua callback failed");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_handler(&self, id: SubscriptionId) {
|
||||
if let Ok(mut map) = self.handlers.lock() {
|
||||
map.remove(&id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn list_lua_files(root: &Path) -> Result<Vec<PathBuf>> {
|
||||
let mut out = Vec::new();
|
||||
if !root.exists() {
|
||||
return Ok(out);
|
||||
}
|
||||
|
||||
let mut stack = vec![root.to_path_buf()];
|
||||
while let Some(dir) = stack.pop() {
|
||||
for entry in fs::read_dir(&dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
stack.push(path);
|
||||
} else if path.extension().and_then(|e| e.to_str()) == Some("lua") {
|
||||
out.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue