1
0
Fork 0
mirror of https://git.rwth-aachen.de/acs/public/villas/node/ synced 2025-03-09 00:00:00 +01:00
VILLASnode/lib/hooks/lua.cpp

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

719 lines
17 KiB
C++
Raw Permalink Normal View History

2020-12-07 21:30:22 +01:00
/* Lua expressions hook.
*
2022-03-15 09:18:01 -04:00
* Author: Steffen Vogel <post@steffenvogel.de>
2022-03-15 09:28:57 -04:00
* SPDX-FileCopyrightText: 2014-2023 Institute for Automation of Complex Power Systems, RWTH Aachen University
2022-07-04 18:20:03 +02:00
* SPDX-License-Identifier: Apache-2.0
2020-12-07 21:30:22 +01:00
*/
2021-02-22 23:30:19 +01:00
#include <cstdio>
2021-02-19 06:50:25 +01:00
#include <map>
#include <vector>
2020-12-07 21:30:22 +01:00
2021-02-19 06:50:25 +01:00
extern "C" {
2021-03-09 01:49:18 +01:00
#include <lua.h>
#include <lualib.h>
#include <lauxlib.h>
2021-02-19 06:50:25 +01:00
};
#include <villas/exceptions.hpp>
#include <villas/hooks/lua.hpp>
#include <villas/node.hpp>
#include <villas/path.hpp>
#include <villas/sample.hpp>
#include <villas/signal.hpp>
#include <villas/signal_list.hpp>
2021-02-19 06:50:25 +01:00
#include <villas/utils.hpp>
2020-12-07 21:30:22 +01:00
2021-02-19 06:50:25 +01:00
using namespace villas;
using namespace villas::node;
2020-12-07 21:30:22 +01:00
2021-02-19 06:50:25 +01:00
class LuaError : public RuntimeError {
2020-12-07 21:30:22 +01:00
protected:
lua_State *L;
2021-02-19 06:50:25 +01:00
int err;
2020-12-07 21:30:22 +01:00
2021-02-19 06:50:25 +01:00
public:
LuaError(lua_State *l, int e) : RuntimeError(""), L(l), err(e) {}
2021-02-19 06:50:25 +01:00
virtual const char *what() const noexcept {
const char *msg;
2021-02-19 06:50:25 +01:00
switch (err) {
case LUA_ERRSYNTAX:
msg = "Syntax error";
break;
2021-02-19 06:50:25 +01:00
case LUA_ERRMEM:
msg = "Memory allocation error";
break;
2021-02-19 06:50:25 +01:00
case LUA_ERRFILE:
msg = "Failed to open Lua script";
break;
2021-02-19 06:50:25 +01:00
case LUA_ERRRUN:
msg = "Runtime error";
break;
2021-02-19 06:50:25 +01:00
case LUA_ERRERR:
msg = "Failed to call error handler";
break;
2021-02-19 06:50:25 +01:00
default:
msg = "Unknown error";
break;
2020-12-07 21:30:22 +01:00
}
2021-02-22 23:30:19 +01:00
char *buf;
if (asprintf(&buf, "Lua: %s: %s", msg, lua_tostring(L, -1)) < 0)
return "Lua: could not format error message";
2021-02-22 23:30:19 +01:00
return buf;
2020-12-07 21:30:22 +01:00
}
2021-02-19 06:50:25 +01:00
};
2020-12-07 21:30:22 +01:00
static void lua_pushtimespec(lua_State *L, struct timespec *ts) {
2021-02-19 06:50:25 +01:00
lua_createtable(L, 2, 0);
lua_pushnumber(L, ts->tv_sec);
lua_rawseti(L, -2, 0);
lua_pushnumber(L, ts->tv_nsec);
lua_rawseti(L, -2, 1);
}
static void lua_totimespec(lua_State *L, struct timespec *ts) {
2021-02-19 06:50:25 +01:00
lua_rawgeti(L, -1, 0);
ts->tv_sec = lua_tonumber(L, -1);
lua_rawgeti(L, -2, 1);
ts->tv_nsec = lua_tonumber(L, -1);
2021-02-19 06:50:25 +01:00
lua_pop(L, 2);
}
static bool lua_pushsignaldata(lua_State *L, const union SignalData *data,
const Signal::Ptr sig) {
switch (sig->type) {
case SignalType::FLOAT:
lua_pushnumber(L, data->f);
break;
case SignalType::INTEGER:
lua_pushinteger(L, data->i);
break;
case SignalType::BOOLEAN:
lua_pushboolean(L, data->b);
break;
case SignalType::COMPLEX:
case SignalType::INVALID:
default:
return false; // we skip unknown types. Lua will see a nil value in the table
}
return true;
}
static void lua_tosignaldata(lua_State *L, union SignalData *data,
enum SignalType targetType, int idx = -1) {
int luaType;
2021-02-19 06:50:25 +01:00
enum SignalType type;
luaType = lua_type(L, idx);
switch (luaType) {
2021-02-19 06:50:25 +01:00
case LUA_TBOOLEAN:
data->b = lua_toboolean(L, idx);
type = SignalType::BOOLEAN;
break;
2021-02-19 06:50:25 +01:00
case LUA_TNUMBER:
data->f = lua_tonumber(L, idx);
type = SignalType::FLOAT;
break;
2021-02-19 06:50:25 +01:00
default:
return;
}
*data = data->cast(type, targetType);
2021-02-19 06:50:25 +01:00
}
2020-12-07 21:30:22 +01:00
static void lua_tosample(lua_State *L, struct Sample *smp,
SignalList::Ptr signals, bool use_names = true,
int idx = -1) {
int ret;
2021-03-17 10:23:03 -04:00
smp->length = 0;
smp->flags = 0;
2021-03-17 10:23:03 -04:00
lua_getfield(L, idx, "sequence");
ret = lua_type(L, -1);
if (ret != LUA_TNIL) {
smp->sequence = lua_tonumber(L, -1);
smp->flags |= (int)SampleFlags::HAS_SEQUENCE;
}
2021-02-19 06:50:25 +01:00
lua_pop(L, 1);
2021-02-19 06:50:25 +01:00
lua_getfield(L, idx, "ts_origin");
2021-03-17 10:23:03 -04:00
ret = lua_type(L, -1);
if (ret != LUA_TNIL) {
lua_totimespec(L, &smp->ts.origin);
smp->flags |= (int)SampleFlags::HAS_TS_ORIGIN;
}
2021-02-19 06:50:25 +01:00
lua_pop(L, 1);
2021-02-19 06:50:25 +01:00
lua_getfield(L, idx, "ts_received");
2021-03-17 10:23:03 -04:00
ret = lua_type(L, -1);
if (ret != LUA_TNIL) {
lua_totimespec(L, &smp->ts.received);
smp->flags |= (int)SampleFlags::HAS_TS_RECEIVED;
}
2021-02-19 06:50:25 +01:00
lua_pop(L, 1);
2021-02-19 06:50:25 +01:00
lua_getfield(L, idx, "data");
2021-03-17 10:23:03 -04:00
ret = lua_type(L, -1);
if (ret != LUA_TNIL) {
int i = 0;
for (auto sig : *signals) {
2021-03-17 10:23:03 -04:00
if (use_names)
lua_getfield(L, -1, sig->name.c_str());
2021-03-17 10:23:03 -04:00
else
lua_rawgeti(L, -1, i);
2021-03-17 10:23:03 -04:00
ret = lua_type(L, -1);
if (ret != LUA_TNIL)
lua_tosignaldata(L, &smp->data[i], sig->type, -1);
else
smp->data[i] = sig->init;
2021-03-17 10:23:03 -04:00
lua_pop(L, 1);
i++;
smp->length++;
2021-03-17 10:23:03 -04:00
}
2021-03-17 10:23:03 -04:00
if (smp->length > 0)
smp->flags |= (int)SampleFlags::HAS_DATA;
2020-12-07 21:30:22 +01:00
}
2021-02-19 06:50:25 +01:00
lua_pop(L, 1);
}
static void lua_pushsample(lua_State *L, struct Sample *smp,
bool use_names = true) {
2021-02-19 06:50:25 +01:00
lua_createtable(L, 0, 5);
lua_pushnumber(L, smp->flags);
lua_setfield(L, -2, "flags");
if (smp->flags & (int)SampleFlags::HAS_SEQUENCE) {
lua_pushnumber(L, smp->sequence);
lua_setfield(L, -2, "sequence");
}
2021-02-19 06:50:25 +01:00
if (smp->flags & (int)SampleFlags::HAS_TS_ORIGIN) {
lua_pushtimespec(L, &smp->ts.origin);
lua_setfield(L, -2, "ts_origin");
}
2021-02-19 06:50:25 +01:00
if (smp->flags & (int)SampleFlags::HAS_TS_RECEIVED) {
lua_pushtimespec(L, &smp->ts.received);
lua_setfield(L, -2, "ts_received");
}
2021-02-19 06:50:25 +01:00
if (smp->flags & (int)SampleFlags::HAS_DATA) {
lua_createtable(L, smp->length, 0);
2021-02-19 06:50:25 +01:00
for (unsigned i = 0; i < smp->length; i++) {
const auto sig = smp->signals->getByIndex(i);
const auto *data = &smp->data[i];
auto pushed = lua_pushsignaldata(L, data, sig);
if (!pushed)
continue;
if (use_names)
lua_setfield(L, -2, sig->name.c_str());
else
lua_rawseti(L, -2, i);
2020-12-07 21:30:22 +01:00
}
lua_setfield(L, -2, "data");
}
}
static void lua_pushjson(lua_State *L, json_t *json) {
size_t i;
const char *key;
json_t *json_value;
switch (json_typeof(json)) {
case JSON_OBJECT:
lua_newtable(L);
json_object_foreach(json, key, json_value) {
lua_pushjson(L, json_value);
lua_setfield(L, -2, key);
}
break;
case JSON_ARRAY:
lua_newtable(L);
json_array_foreach(json, i, json_value) {
lua_pushjson(L, json_value);
lua_rawseti(L, -2, i);
}
break;
case JSON_STRING:
lua_pushstring(L, json_string_value(json));
break;
case JSON_REAL:
case JSON_INTEGER:
lua_pushnumber(L, json_integer_value(json));
break;
case JSON_TRUE:
case JSON_FALSE:
lua_pushboolean(L, json_boolean_value(json));
break;
case JSON_NULL:
lua_pushnil(L);
break;
}
}
static json_t *lua_tojson(lua_State *L, int index = -1) {
double n;
const char *s;
bool b;
switch (lua_type(L, index)) {
case LUA_TFUNCTION:
case LUA_TUSERDATA:
case LUA_TTHREAD:
case LUA_TLIGHTUSERDATA:
case LUA_TNIL:
return json_null();
case LUA_TNUMBER:
n = lua_tonumber(L, index);
return n == (int)n ? json_integer(n) : json_real(n);
case LUA_TBOOLEAN:
b = lua_toboolean(L, index);
return json_boolean(b);
case LUA_TSTRING:
s = lua_tostring(L, index);
return json_string(s);
case LUA_TTABLE: {
int keys_total = 0, keys_int = 0, key_highest = -1;
lua_pushnil(L);
while (lua_next(L, index) != 0) {
keys_total++;
if (lua_type(L, -2) == LUA_TNUMBER) {
int key = lua_tonumber(L, -1);
if (key == (int)key) {
keys_int++;
if (key > key_highest)
key_highest = key;
}
}
lua_pop(L, 1);
}
bool is_array = keys_total == keys_int && key_highest / keys_int > 0.5;
json_t *json = is_array ? json_array() : json_object();
lua_pushnil(L);
while (lua_next(L, index) != 0) {
json_t *val = lua_tojson(L, -1);
if (is_array) {
int key = lua_tonumber(L, -2);
json_array_set(json, key, val);
} else {
const char *key = lua_tostring(L, -2);
if (key) // Skip table entries whose keys are neither string or number!
json_object_set(json, key, val);
}
lua_pop(L, 1);
}
return json;
}
2020-12-07 21:30:22 +01:00
}
return nullptr;
2021-02-19 06:50:25 +01:00
}
namespace villas {
namespace node {
LuaSignalExpression::LuaSignalExpression(lua_State *l, json_t *json_sig)
2021-02-19 06:50:25 +01:00
: cookie(0), L(l) {
int ret;
json_error_t err;
2021-02-19 06:50:25 +01:00
const char *expr;
// Parse expression
2021-02-19 06:50:25 +01:00
ret = json_unpack_ex(json_sig, &err, 0, "{ s: s }", "expression", &expr);
if (ret)
throw ConfigError(json_sig, err, "node-config-hook-lua-signals");
cfg = json_sig;
expression = expr;
}
void LuaSignalExpression::prepare() { parseExpression(expression); }
2021-02-19 06:50:25 +01:00
void LuaSignalExpression::parseExpression(const std::string &expr) {
2021-02-19 06:50:25 +01:00
// Release previous expression
if (cookie)
luaL_unref(L, LUA_REGISTRYINDEX, cookie);
2021-02-19 06:50:25 +01:00
auto fexpr = fmt::format("return {}", expr);
int err = luaL_loadstring(L, fexpr.c_str());
2021-02-19 06:50:25 +01:00
if (err)
throw ConfigError(cfg, "node-config-hook-lua-signals",
"Failed to load Lua expression: {}", lua_tostring(L, -1));
2021-02-19 06:50:25 +01:00
cookie = luaL_ref(L, LUA_REGISTRYINDEX);
2021-02-19 06:50:25 +01:00
}
void LuaSignalExpression::evaluate(union SignalData *data,
enum SignalType type) {
2021-02-19 06:50:25 +01:00
int err;
lua_rawgeti(L, LUA_REGISTRYINDEX, cookie);
2021-02-19 06:50:25 +01:00
err = lua_pcall(L, 0, 1, 0);
2021-02-19 06:50:25 +01:00
if (err) {
throw RuntimeError("Lua: Evaluation failed: {}", lua_tostring(L, -1));
lua_pop(L, 1);
2020-12-07 21:30:22 +01:00
}
2021-03-17 10:23:03 -04:00
lua_tosignaldata(L, data, type, -1);
lua_pop(L, 1);
}
LuaHook::LuaHook(Path *p, Node *n, int fl, int prio, bool en)
: Hook(p, n, fl, prio, en),
signalsExpressions(std::make_shared<SignalList>()), L(luaL_newstate()),
useNames(true), hasExpressions(false), needsLocking(false),
functions({0}) {}
2020-12-07 21:30:22 +01:00
LuaHook::~LuaHook() { lua_close(L); }
2020-12-07 21:30:22 +01:00
void LuaHook::parseExpressions(json_t *json_sigs) {
int ret;
size_t i;
2021-02-19 06:50:25 +01:00
json_t *json_sig;
2020-12-07 21:30:22 +01:00
2022-03-28 16:26:41 +02:00
signalsExpressions->clear();
2021-02-19 06:50:25 +01:00
ret = signalsExpressions->parse(json_sigs);
if (ret)
throw ConfigError(json_sigs, "node-config-hook-lua-signals",
"Setting 'signals' must be a list of dicts");
2020-12-07 21:30:22 +01:00
2022-03-28 16:26:41 +02:00
// cppcheck-suppress unknownMacro
json_array_foreach(json_sigs, i, json_sig)
expressions.emplace_back(L, json_sig);
hasExpressions = true;
2021-02-19 06:50:25 +01:00
}
void LuaHook::parse(json_t *json) {
2021-02-19 06:50:25 +01:00
int ret;
const char *script_str = nullptr;
int names = 1;
json_error_t err;
json_t *json_signals = nullptr;
assert(state != State::STARTED);
2021-02-16 14:15:14 +01:00
Hook::parse(json);
2021-02-19 06:50:25 +01:00
2021-02-16 14:15:14 +01:00
ret = json_unpack_ex(json, &err, 0, "{ s?: s, s?: o, s?: b }", "script",
2021-02-19 06:50:25 +01:00
&script_str, "signals", &json_signals, "use_names",
&names);
if (ret)
2021-02-16 14:15:14 +01:00
throw ConfigError(json, err, "node-config-hook-lua");
2021-02-19 06:50:25 +01:00
useNames = names;
if (script_str)
2020-12-07 21:30:22 +01:00
script = script_str;
2021-02-19 06:50:25 +01:00
if (json_signals)
parseExpressions(json_signals);
2020-12-07 21:30:22 +01:00
2021-02-19 06:50:25 +01:00
state = State::PARSED;
}
2020-12-07 21:30:22 +01:00
void LuaHook::lookupFunctions() {
2021-02-19 06:50:25 +01:00
int ret;
2021-02-19 06:50:25 +01:00
std::map<const char *, int *> funcs = {
{"start", &functions.start}, {"stop", &functions.stop},
{"restart", &functions.restart}, {"prepare", &functions.prepare},
{"periodic", &functions.periodic}, {"process", &functions.process}};
2021-02-19 06:50:25 +01:00
for (auto it : funcs) {
lua_getglobal(L, it.first);
2021-02-19 06:50:25 +01:00
ret = lua_type(L, -1);
if (ret == LUA_TFUNCTION) {
2021-02-16 14:15:14 +01:00
logger->debug("Found Lua function: {}()", it.first);
2021-02-19 06:50:25 +01:00
*(it.second) = lua_gettop(L);
} else {
*(it.second) = 0;
lua_pop(L, 1);
}
}
}
void LuaHook::loadScript() {
int ret;
if (script.empty())
return; // No script given
ret = luaL_loadfile(L, script.c_str());
if (ret)
throw LuaError(L, ret);
ret = lua_pcall(L, 0, LUA_MULTRET, 0);
if (ret)
throw LuaError(L, ret);
}
int LuaHook::luaRegisterApiHandler(lua_State *L) {
// register_api_handler(path_regex)
return 0;
}
int LuaHook::luaInfo(lua_State *L) {
logger->info(luaL_checkstring(L, 1));
return 0;
}
int LuaHook::luaWarn(lua_State *L) {
logger->warn(luaL_checkstring(L, 1));
return 0;
}
int LuaHook::luaError(lua_State *L) {
logger->error(luaL_checkstring(L, 1));
return 0;
}
int LuaHook::luaDebug(lua_State *L) {
logger->debug(luaL_checkstring(L, 1));
return 0;
}
void LuaHook::setupEnvironment() {
lua_pushlightuserdata(L, this);
lua_rawseti(L, LUA_REGISTRYINDEX, SELF_REFERENCE);
lua_register(L, "info", &dispatch<&LuaHook::luaInfo>);
lua_register(L, "warn", &dispatch<&LuaHook::luaWarn>);
lua_register(L, "error", &dispatch<&LuaHook::luaError>);
lua_register(L, "debug", &dispatch<&LuaHook::luaDebug>);
lua_register(L, "register_api_handler",
&dispatch<&LuaHook::luaRegisterApiHandler>);
}
void LuaHook::prepare() {
// Load Lua standard libraries
luaL_openlibs(L);
// Load our Lua script
2021-02-16 14:15:14 +01:00
logger->debug("Loading Lua script: {}", script);
setupEnvironment();
loadScript();
lookupFunctions();
/* Check if we need to protect the Lua state with a mutex
* This is the case if we have a periodic callback defined
* As periodic() gets called from the main thread
*/
needsLocking = functions.periodic > 0;
// Prepare Lua process()
if (functions.process) {
/* We currently do not support the alteration of
* signal metadata in process() */
signalsProcessed = signals;
}
// Prepare Lua expressions
if (hasExpressions) {
for (auto &expr : expressions)
expr.prepare();
signals = signalsExpressions;
}
if (!functions.process && !hasExpressions)
logger->warn(
"The hook has neither a script or expressions defined. It is a no-op!");
2021-02-19 06:50:25 +01:00
if (functions.prepare) {
auto lockScope = needsLocking ? std::unique_lock<std::mutex>(mutex)
: std::unique_lock<std::mutex>();
2021-02-19 06:50:25 +01:00
logger->debug("Executing Lua function: prepare()");
lua_pushvalue(L, functions.prepare);
2021-02-16 14:15:14 +01:00
lua_pushjson(L, config);
int ret = lua_pcall(L, 1, 0, 0);
if (ret)
throw LuaError(L, ret);
2021-02-19 06:50:25 +01:00
}
}
void LuaHook::start() {
2021-02-19 06:50:25 +01:00
assert(state == State::PREPARED);
auto lockScope = needsLocking ? std::unique_lock<std::mutex>(mutex)
: std::unique_lock<std::mutex>();
2021-02-19 06:50:25 +01:00
if (functions.start) {
logger->debug("Executing Lua function: start()");
lua_pushvalue(L, functions.start);
int ret = lua_pcall(L, 0, 0, 0);
if (ret)
throw LuaError(L, ret);
2020-12-07 21:30:22 +01:00
}
2021-02-19 06:50:25 +01:00
state = State::STARTED;
}
2020-12-07 21:30:22 +01:00
void LuaHook::stop() {
2021-02-19 06:50:25 +01:00
assert(state == State::STARTED);
auto lockScope = needsLocking ? std::unique_lock<std::mutex>(mutex)
: std::unique_lock<std::mutex>();
2021-02-19 06:50:25 +01:00
if (functions.stop) {
logger->debug("Executing Lua function: stop()");
lua_pushvalue(L, functions.stop);
int ret = lua_pcall(L, 0, 0, 0);
if (ret)
throw LuaError(L, ret);
2020-12-07 21:30:22 +01:00
}
2021-02-19 06:50:25 +01:00
state = State::STOPPED;
}
2020-12-07 21:30:22 +01:00
void LuaHook::restart() {
auto lockScope = needsLocking ? std::unique_lock<std::mutex>(mutex)
: std::unique_lock<std::mutex>();
2021-02-19 06:50:25 +01:00
assert(state == State::STARTED);
2021-02-19 06:50:25 +01:00
if (functions.restart) {
logger->debug("Executing Lua function: restart()");
lua_pushvalue(L, functions.restart);
int ret = lua_pcall(L, 0, 0, 0);
if (ret)
throw LuaError(L, ret);
2021-02-19 06:50:25 +01:00
} else
Hook::restart();
}
void LuaHook::periodic() {
assert(state == State::STARTED);
if (functions.periodic) {
auto lockScope = needsLocking ? std::unique_lock<std::mutex>(mutex)
: std::unique_lock<std::mutex>();
logger->debug("Executing Lua function: restart()");
lua_pushvalue(L, functions.periodic);
int ret = lua_pcall(L, 0, 0, 0);
if (ret)
throw LuaError(L, ret);
2021-02-19 06:50:25 +01:00
}
}
2020-12-07 21:30:22 +01:00
Hook::Reason LuaHook::process(struct Sample *smp) {
if (!functions.process && !hasExpressions)
return Reason::OK;
int rtype;
enum Reason reason;
auto lockScope = needsLocking ? std::unique_lock<std::mutex>(mutex)
: std::unique_lock<std::mutex>();
// First, run the process() function of the script
2021-02-19 06:50:25 +01:00
if (functions.process) {
logger->debug("Executing Lua function: process(smp)");
lua_pushsample(L, smp, useNames);
2021-02-19 06:50:25 +01:00
lua_pushvalue(L, functions.process);
lua_pushvalue(L, -2); // Push a copy since lua_pcall() will pop it
int ret = lua_pcall(L, 1, 1, 0);
if (ret)
throw LuaError(L, ret);
rtype = lua_type(L, -1);
if (rtype == LUA_TNUMBER) {
reason = (Reason)lua_tonumber(L, -1);
} else {
logger->warn(
"Lua process() did not return a valid number. Assuming Reason::OK");
reason = Reason::OK;
}
lua_pop(L, 1);
lua_tosample(L, smp, signalsProcessed, useNames);
} else
reason = Reason::OK;
// After that evaluate expressions
if (hasExpressions) {
2021-02-19 06:50:25 +01:00
lua_pushsample(L, smp, useNames);
lua_setglobal(L, "smp");
for (unsigned i = 0; i < expressions.size(); i++) {
auto sig = signalsExpressions->getByIndex(i);
if (!sig)
continue;
expressions[i].evaluate(&smp->data[i], sig->type);
}
smp->length = expressions.size();
2021-02-19 06:50:25 +01:00
}
return reason;
2021-02-19 06:50:25 +01:00
}
2020-12-07 21:30:22 +01:00
// Register hook
static char n[] = "lua";
static char d[] = "Implement hook functions or expressions in Lua";
2020-12-07 21:30:22 +01:00
static HookPlugin<LuaHook, n, d,
(int)Hook::Flags::NODE_READ | (int)Hook::Flags::NODE_WRITE |
(int)Hook::Flags::PATH,
1>
p;
} // namespace node
} // namespace villas