Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
405 lines
13 KiB
Rust
405 lines
13 KiB
Rust
use std::fs;
|
|
use std::sync::Mutex;
|
|
|
|
use mlua::{Function, Lua, RegistryKey, Table, Value};
|
|
use tracing::warn;
|
|
use pipeview_core::frame::Framer;
|
|
use pipeview_core::protocol::plot::{PlotFormat, PlotFrame, SampleType};
|
|
use pipeview_core::protocol::{DecodedData, ProtocolDecoder};
|
|
|
|
use crate::error::{Error, Result};
|
|
|
|
pub struct LuaFramer {
|
|
lua: Lua,
|
|
feed: RegistryKey,
|
|
flush: RegistryKey,
|
|
reset: RegistryKey,
|
|
pending_len: RegistryKey,
|
|
script_path: String,
|
|
}
|
|
|
|
impl LuaFramer {
|
|
pub fn from_script_path(script_path: impl Into<String>) -> Result<Self> {
|
|
let script_path = script_path.into();
|
|
let lua = Lua::new();
|
|
let table = load_script_table(&lua, &script_path)?;
|
|
let feed = registry_function(&lua, &table, "feed", &script_path)?;
|
|
let flush = registry_function(&lua, &table, "flush", &script_path)?;
|
|
let reset = registry_function(&lua, &table, "reset", &script_path)?;
|
|
let pending_len = registry_function(&lua, &table, "pending_len", &script_path)?;
|
|
|
|
Ok(Self {
|
|
lua,
|
|
feed,
|
|
flush,
|
|
reset,
|
|
pending_len,
|
|
script_path,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Framer for LuaFramer {
|
|
fn feed(&mut self, data: &[u8]) -> Vec<Vec<u8>> {
|
|
let result = (|| -> mlua::Result<Vec<Vec<u8>>> {
|
|
let function: Function = self.lua.registry_value(&self.feed)?;
|
|
let bytes = self.lua.create_string(data)?;
|
|
frames_from_value(function.call::<Value>(bytes)?)
|
|
})();
|
|
|
|
match result {
|
|
Ok(frames) => frames,
|
|
Err(err) => {
|
|
warn!(script = %self.script_path, error = %err, "lua framer feed failed");
|
|
Vec::new()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn flush(&mut self) -> Option<Vec<u8>> {
|
|
let result = (|| -> mlua::Result<Option<Vec<u8>>> {
|
|
let function: Function = self.lua.registry_value(&self.flush)?;
|
|
optional_bytes_from_value(function.call::<Value>(())?)
|
|
})();
|
|
|
|
match result {
|
|
Ok(frame) => frame,
|
|
Err(err) => {
|
|
warn!(script = %self.script_path, error = %err, "lua framer flush failed");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reset(&mut self) {
|
|
let result = (|| -> mlua::Result<()> {
|
|
let function: Function = self.lua.registry_value(&self.reset)?;
|
|
function.call::<()>(())
|
|
})();
|
|
|
|
if let Err(err) = result {
|
|
warn!(script = %self.script_path, error = %err, "lua framer reset failed");
|
|
}
|
|
}
|
|
|
|
fn pending_len(&self) -> usize {
|
|
let result = (|| -> mlua::Result<usize> {
|
|
let function: Function = self.lua.registry_value(&self.pending_len)?;
|
|
function.call::<usize>(())
|
|
})();
|
|
|
|
match result {
|
|
Ok(len) => len,
|
|
Err(err) => {
|
|
warn!(script = %self.script_path, error = %err, "lua framer pending_len failed");
|
|
0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub struct LuaDecoder {
|
|
lua: Lua,
|
|
decode: RegistryKey,
|
|
script_path: String,
|
|
call_lock: Mutex<()>,
|
|
}
|
|
|
|
impl LuaDecoder {
|
|
pub fn from_script_path(script_path: impl Into<String>) -> Result<Self> {
|
|
let script_path = script_path.into();
|
|
let lua = Lua::new();
|
|
let table = load_script_table(&lua, &script_path)?;
|
|
let decode = registry_function(&lua, &table, "decode", &script_path)?;
|
|
|
|
Ok(Self {
|
|
lua,
|
|
decode,
|
|
script_path,
|
|
call_lock: Mutex::new(()),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ProtocolDecoder for LuaDecoder {
|
|
fn name(&self) -> &str {
|
|
"Lua"
|
|
}
|
|
|
|
fn decode(&self, frame: &[u8]) -> Option<DecodedData> {
|
|
let _guard = self
|
|
.call_lock
|
|
.lock()
|
|
.expect("lua decoder call mutex poisoned");
|
|
let result = (|| -> mlua::Result<Option<DecodedData>> {
|
|
let function: Function = self.lua.registry_value(&self.decode)?;
|
|
let bytes = self.lua.create_string(frame)?;
|
|
decoded_from_value(function.call::<Value>(bytes)?, frame)
|
|
})();
|
|
|
|
match result {
|
|
Ok(decoded) => decoded,
|
|
Err(err) => {
|
|
warn!(script = %self.script_path, error = %err, "lua decoder decode failed");
|
|
None
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn load_script_table(lua: &Lua, script_path: &str) -> Result<Table> {
|
|
let script = fs::read_to_string(script_path)?;
|
|
match lua.load(&script).eval::<Value>()? {
|
|
Value::Table(table) => Ok(table),
|
|
other => Err(Error::Other(format!(
|
|
"lua script '{script_path}' must return a table, got {}",
|
|
other.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn registry_function(
|
|
lua: &Lua,
|
|
table: &Table,
|
|
name: &str,
|
|
script_path: &str,
|
|
) -> Result<RegistryKey> {
|
|
let value: Value = table.get(name)?;
|
|
match value {
|
|
Value::Function(function) => Ok(lua.create_registry_value(function)?),
|
|
other => Err(Error::Other(format!(
|
|
"lua script '{script_path}' field '{name}' must be a function, got {}",
|
|
other.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn frames_from_value(value: Value) -> mlua::Result<Vec<Vec<u8>>> {
|
|
match value {
|
|
Value::Nil => Ok(Vec::new()),
|
|
Value::String(bytes) => Ok(vec![bytes.as_bytes().to_vec()]),
|
|
Value::Table(table) => {
|
|
let mut frames = Vec::new();
|
|
for value in table.sequence_values::<mlua::String>() {
|
|
frames.push(value?.as_bytes().to_vec());
|
|
}
|
|
Ok(frames)
|
|
}
|
|
other => Err(mlua::Error::RuntimeError(format!(
|
|
"framer feed must return nil, string, or table of strings, got {}",
|
|
other.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn optional_bytes_from_value(value: Value) -> mlua::Result<Option<Vec<u8>>> {
|
|
match value {
|
|
Value::Nil => Ok(None),
|
|
Value::String(bytes) => Ok(Some(bytes.as_bytes().to_vec())),
|
|
other => Err(mlua::Error::RuntimeError(format!(
|
|
"framer flush must return nil or string, got {}",
|
|
other.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn decoded_from_value(value: Value, raw: &[u8]) -> mlua::Result<Option<DecodedData>> {
|
|
match value {
|
|
Value::Nil => Ok(None),
|
|
Value::String(text) => Ok(Some(DecodedData::Text(text.to_str()?.to_string()))),
|
|
Value::Table(table) => decoded_from_table(table, raw).map(Some),
|
|
other => Err(mlua::Error::RuntimeError(format!(
|
|
"decoder decode must return nil, string, or table, got {}",
|
|
other.type_name()
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn decoded_from_table(table: Table, raw: &[u8]) -> mlua::Result<DecodedData> {
|
|
let kind: String = table.get("kind")?;
|
|
match kind.as_str() {
|
|
"text" => Ok(DecodedData::Text(table.get("data")?)),
|
|
"hex" => Ok(DecodedData::Hex(table.get("data")?)),
|
|
"binary" => {
|
|
let bytes: mlua::String = table.get("data")?;
|
|
Ok(DecodedData::Binary(bytes.as_bytes().to_vec()))
|
|
}
|
|
"plot" => Ok(DecodedData::Plot(plot_from_table(table, raw)?)),
|
|
other => Err(mlua::Error::RuntimeError(format!(
|
|
"unknown lua decoder kind '{other}'"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn plot_from_table(table: Table, raw: &[u8]) -> mlua::Result<PlotFrame> {
|
|
let channels_table: Table = table.get("channels")?;
|
|
let mut channels = Vec::new();
|
|
for channel in channels_table.sequence_values::<Table>() {
|
|
let channel = channel?;
|
|
let mut samples = Vec::new();
|
|
for sample in channel.sequence_values::<f64>() {
|
|
samples.push(sample?);
|
|
}
|
|
channels.push(samples);
|
|
}
|
|
|
|
Ok(PlotFrame {
|
|
channels,
|
|
raw: raw.to_vec(),
|
|
sample_type: match table.get::<Option<String>>("sample_type")?.as_deref() {
|
|
Some(value) => parse_sample_type(value)?,
|
|
None => SampleType::F64,
|
|
},
|
|
format: match table.get::<Option<String>>("format")?.as_deref() {
|
|
Some(value) => parse_plot_format(value)?,
|
|
None => PlotFormat::Interleaved,
|
|
},
|
|
})
|
|
}
|
|
|
|
fn parse_sample_type(value: &str) -> mlua::Result<SampleType> {
|
|
match value {
|
|
"i8" | "I8" => Ok(SampleType::I8),
|
|
"u8" | "U8" => Ok(SampleType::U8),
|
|
"i16" | "I16" => Ok(SampleType::I16),
|
|
"u16" | "U16" => Ok(SampleType::U16),
|
|
"i32" | "I32" => Ok(SampleType::I32),
|
|
"u32" | "U32" => Ok(SampleType::U32),
|
|
"i64" | "I64" => Ok(SampleType::I64),
|
|
"u64" | "U64" => Ok(SampleType::U64),
|
|
"f32" | "F32" => Ok(SampleType::F32),
|
|
"f64" | "F64" => Ok(SampleType::F64),
|
|
other => Err(mlua::Error::RuntimeError(format!(
|
|
"unknown plot sample_type '{other}'"
|
|
))),
|
|
}
|
|
}
|
|
|
|
fn parse_plot_format(value: &str) -> mlua::Result<PlotFormat> {
|
|
match value {
|
|
"Interleaved" | "interleaved" => Ok(PlotFormat::Interleaved),
|
|
"Block" | "block" => Ok(PlotFormat::Block),
|
|
"XY" | "xy" => Ok(PlotFormat::XY),
|
|
other => Err(mlua::Error::RuntimeError(format!(
|
|
"unknown plot format '{other}'"
|
|
))),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::path::PathBuf;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
fn write_script(name: &str, script: &str) -> PathBuf {
|
|
let nonce = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos();
|
|
let path = std::env::temp_dir().join(format!("pipeview_{name}_{nonce}.lua"));
|
|
fs::write(&path, script).unwrap();
|
|
path
|
|
}
|
|
|
|
#[test]
|
|
fn lua_framer_splits_lines_across_chunks() {
|
|
let path = write_script(
|
|
"framer",
|
|
r#"
|
|
local buffer = ""
|
|
return {
|
|
feed = function(bytes)
|
|
buffer = buffer .. bytes
|
|
local frames = {}
|
|
while true do
|
|
local i = buffer:find("\n", 1, true)
|
|
if not i then break end
|
|
frames[#frames + 1] = buffer:sub(1, i - 1)
|
|
buffer = buffer:sub(i + 1)
|
|
end
|
|
return frames
|
|
end,
|
|
flush = function()
|
|
if #buffer == 0 then return nil end
|
|
local frame = buffer
|
|
buffer = ""
|
|
return frame
|
|
end,
|
|
reset = function()
|
|
buffer = ""
|
|
end,
|
|
pending_len = function()
|
|
return #buffer
|
|
end,
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let mut framer = LuaFramer::from_script_path(path.to_string_lossy()).unwrap();
|
|
assert_eq!(framer.feed(b"one\nt"), vec![b"one".to_vec()]);
|
|
assert_eq!(framer.pending_len(), 1);
|
|
assert_eq!(framer.feed(b"wo\nthree"), vec![b"two".to_vec()]);
|
|
assert_eq!(framer.flush(), Some(b"three".to_vec()));
|
|
assert_eq!(framer.pending_len(), 0);
|
|
|
|
fs::remove_file(path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn lua_decoder_maps_text_binary_and_plot() {
|
|
let path = write_script(
|
|
"decoder",
|
|
r#"
|
|
return {
|
|
decode = function(frame)
|
|
if frame == "skip" then
|
|
return nil
|
|
elseif frame == "bin" then
|
|
return { kind = "binary", data = "\1\2" }
|
|
elseif frame == "plot" then
|
|
return {
|
|
kind = "plot",
|
|
channels = { { 1.0, 2.0 }, { 3.5, 4.5 } },
|
|
sample_type = "F64",
|
|
format = "Block",
|
|
}
|
|
end
|
|
return { kind = "text", data = "decoded:" .. frame }
|
|
end,
|
|
}
|
|
"#,
|
|
);
|
|
|
|
let decoder = LuaDecoder::from_script_path(path.to_string_lossy()).unwrap();
|
|
assert!(decoder.decode(b"skip").is_none());
|
|
assert!(matches!(
|
|
decoder.decode(b"abc"),
|
|
Some(DecodedData::Text(text)) if text == "decoded:abc"
|
|
));
|
|
assert!(matches!(
|
|
decoder.decode(b"bin"),
|
|
Some(DecodedData::Binary(bytes)) if bytes == vec![1, 2]
|
|
));
|
|
match decoder.decode(b"plot").unwrap() {
|
|
DecodedData::Plot(frame) => {
|
|
assert_eq!(frame.channels, vec![vec![1.0, 2.0], vec![3.5, 4.5]]);
|
|
assert_eq!(frame.sample_type, SampleType::F64);
|
|
assert_eq!(frame.format, PlotFormat::Block);
|
|
assert_eq!(frame.raw, b"plot");
|
|
}
|
|
other => panic!("expected plot, got {other:?}"),
|
|
}
|
|
|
|
fs::remove_file(path).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn lua_framer_requires_expected_functions() {
|
|
let path = write_script("bad_framer", "return { feed = function() return {} end }");
|
|
let result = LuaFramer::from_script_path(path.to_string_lossy());
|
|
assert!(result.is_err());
|
|
fs::remove_file(path).unwrap();
|
|
}
|
|
}
|