modules: implement module-lua-scripting

a module that allows loading and executing Lua
scripts from a configured directory
This commit is contained in:
George Kiagiadakis
2020-12-20 18:10:22 +02:00
parent d0d2f2a59e
commit 879f771d7f
7 changed files with 748 additions and 0 deletions

View File

@@ -0,0 +1,350 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include "wp/global-proxy.h"
#include <wp/wp.h>
#include <wplua/wplua.h>
#define URI_API "resource:///org/freedesktop/pipewire/wireplumber/m-lua-scripting/api.lua"
/* WpDebug */
static int
log_log (lua_State *L, GLogLevelFlags lvl)
{
lua_Debug ar;
const gchar *message;
gchar line_str[11];
gconstpointer instance = NULL;
GType type = G_TYPE_INVALID;
int index = 1;
if (!wp_log_level_is_enabled (lvl))
return 0;
lua_getstack (L, 1, &ar);
lua_getinfo (L, "nSl", &ar);
if (wplua_isobject (L, 1, G_TYPE_OBJECT)) {
instance = wplua_toobject (L, 1);
type = G_TYPE_FROM_INSTANCE (instance);
index++;
}
message = luaL_checkstring (L, index);
sprintf (line_str, "%d", ar.currentline);
wp_log_structured_standard (G_LOG_DOMAIN, lvl,
ar.source, line_str, ar.name, type, instance, "%s", message);
return 0;
}
static int
log_warning (lua_State *L) { return log_log (L, G_LOG_LEVEL_WARNING); }
static int
log_message (lua_State *L) { return log_log (L, G_LOG_LEVEL_MESSAGE); }
static int
log_info (lua_State *L) { return log_log (L, G_LOG_LEVEL_INFO); }
static int
log_debug (lua_State *L) { return log_log (L, G_LOG_LEVEL_DEBUG); }
static int
log_trace (lua_State *L) { return log_log (L, WP_LOG_LEVEL_TRACE); }
static const luaL_Reg log_funcs[] = {
{ "warning", log_warning },
{ "message", log_message },
{ "info", log_info },
{ "debug", log_debug },
{ "trace", log_trace },
{ NULL, NULL }
};
/* WpGlobalProxy */
static int
global_proxy_request_destroy (lua_State *L)
{
WpGlobalProxy * p = wplua_checkobject (L, 1, WP_TYPE_GLOBAL_PROXY);
wp_global_proxy_request_destroy (p);
return 0;
}
static const luaL_Reg global_proxy_methods[] = {
{ "request_destroy", global_proxy_request_destroy },
{ NULL, NULL }
};
/* WpIterator */
static int
iterator_reset (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
wp_iterator_reset (it);
return 0;
}
static int
iterator_next (lua_State *L)
{
WpIterator *it = wplua_checkboxed (L, 1, WP_TYPE_ITERATOR);
GValue v = G_VALUE_INIT;
if (wp_iterator_next (it, &v)) {
return wplua_gvalue_to_lua (L, &v);
} else {
lua_pushnil (L);
return 1;
}
}
static const luaL_Reg iterator_methods[] = {
{ "reset", iterator_reset },
{ "next", iterator_next },
{ NULL, NULL }
};
/* WpObjectInterest */
static GVariant *
constraint_value_to_variant (lua_State *L, int idx)
{
switch (lua_type (L, idx)) {
case LUA_TBOOLEAN:
return g_variant_new_boolean (lua_toboolean (L, idx));
case LUA_TSTRING:
return g_variant_new_string (lua_tostring (L, idx));
case LUA_TNUMBER:
if (lua_isinteger (L, idx))
return g_variant_new_int64 (lua_tointeger (L, idx));
else
return g_variant_new_double (lua_tonumber (L, idx));
default:
return NULL;
}
}
static void
object_interest_new_add_constraint (lua_State *L, GType type,
WpObjectInterest *interest)
{
int constraint_idx;
WpConstraintType ctype;
const gchar *subject;
WpConstraintVerb verb;
GVariant *value = NULL;
constraint_idx = lua_absindex (L, -1);
/* verify this is a Constraint{} */
if (lua_type (L, constraint_idx) != LUA_TTABLE) {
luaL_error (L, "Interest: expected Constraint at index %d",
lua_tointeger (L, -2));
}
if (luaL_getmetafield (L, constraint_idx, "__name") == LUA_TNIL ||
g_strcmp0 (lua_tostring (L, -1), "Constraint") != 0) {
luaL_error (L, "Interest: expected Constraint at index %d",
lua_tointeger (L, -2));
}
lua_pop (L, 1);
/* get the constraint type */
lua_pushliteral (L, "type");
if (lua_gettable (L, constraint_idx) == LUA_TNUMBER)
ctype = lua_tointeger (L, -1);
else
ctype = g_type_is_a (type, WP_TYPE_GLOBAL_PROXY) ?
WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY : WP_CONSTRAINT_TYPE_G_PROPERTY;
lua_pop (L, 1);
/* get t[1] (the subject) and t[2] (the verb) */
lua_geti (L, constraint_idx, 1);
subject = lua_tostring (L, -1);
lua_geti (L, constraint_idx, 2);
verb = lua_tostring (L, -1)[0];
switch (verb) {
case WP_CONSTRAINT_VERB_EQUALS:
case WP_CONSTRAINT_VERB_MATCHES: {
lua_geti (L, constraint_idx, 3);
value = constraint_value_to_variant (L, -1);
if (G_UNLIKELY (!value))
luaL_error (L, "Constraint: bad value type");
break;
}
case WP_CONSTRAINT_VERB_IN_RANGE: {
GVariant *values[2];
lua_geti (L, constraint_idx, 3);
lua_geti (L, constraint_idx, 4);
values[0] = constraint_value_to_variant (L, -2);
values[1] = constraint_value_to_variant (L, -1);
if (G_UNLIKELY (!values[0] || !values[1])) {
g_clear_pointer (&values[0], g_variant_unref);
g_clear_pointer (&values[1], g_variant_unref);
luaL_error (L, "Constraint: bad value type");
}
value = g_variant_new_tuple (values, 2);
break;
}
case WP_CONSTRAINT_VERB_IN_LIST: {
GPtrArray *values =
g_ptr_array_new_with_free_func ((GDestroyNotify) g_variant_unref);
int i = 3;
while (lua_geti (L, constraint_idx, i++) != LUA_TNIL) {
GVariant *tmp = constraint_value_to_variant (L, -1);
if (G_UNLIKELY (!tmp)) {
g_ptr_array_unref (values);
luaL_error (L, "Constraint: bad value type");
}
g_ptr_array_add (values, g_variant_ref_sink (tmp));
lua_pop (L, 1);
}
value = g_variant_new_tuple ((GVariant **) values->pdata, values->len);
g_ptr_array_unref (values);
break;
}
default:
break;
}
wp_object_interest_add_constraint (interest, ctype, subject, verb, value);
lua_settop (L, constraint_idx);
}
static int
object_interest_new (lua_State *L)
{
WpObjectInterest *interest = NULL;
GType type = 0;
gchar *typestr;
luaL_checktype (L, 1, LUA_TTABLE);
/* type = "string" -> required */
lua_pushliteral (L, "type");
if (lua_gettable (L, -2) != LUA_TSTRING)
luaL_error (L, "Interest: expected 'type' as string");
/* "device" -> "WpDevice" */
typestr = g_strdup_printf ("Wp%s", lua_tostring (L, -1));
if (typestr[2] != 0) {
typestr[2] = g_ascii_toupper (typestr[2]);
type = g_type_from_name (typestr);
}
g_free (typestr);
lua_pop (L, 1);
if (!type)
luaL_error (L, "Interest: unknown type '%s'", lua_tostring (L, -1));
interest = wp_object_interest_new_type (type);
wplua_pushboxed (L, WP_TYPE_OBJECT_INTEREST, interest);
/* add constraints */
lua_pushnil (L);
while (lua_next (L, 1)) {
/* if the key isn't "type" */
if (!(lua_type (L, -2) == LUA_TSTRING &&
!g_strcmp0 ("type", lua_tostring (L, -2))))
object_interest_new_add_constraint (L, type, interest);
lua_pop (L, 1);
}
return 1;
}
/* WpObjectManager */
static int
object_manager_new (lua_State *L)
{
WpObjectManager *om;
/* validate arguments */
luaL_checktype (L, 1, LUA_TTABLE);
/* push to Lua asap to have a way to unref in case of error */
om = wp_object_manager_new ();
wplua_pushobject (L, om);
lua_pushnil (L);
while (lua_next (L, 1)) {
if (!wplua_isboxed (L, -1, WP_TYPE_OBJECT_INTEREST))
luaL_error (L, "ObjectManager: expected Interest");
/* steal the interest out of the GValue to avoid doing mem copy */
GValue *v = lua_touserdata (L, -1);
wp_object_manager_add_interest_full (om, g_value_get_boxed (v));
memset (v, 0, sizeof (GValue));
g_value_init (v, WP_TYPE_OBJECT_INTEREST);
lua_pop (L, 1);
}
/* request all the features for Lua scripts to make their job easier */
wp_object_manager_request_object_features (om,
WP_TYPE_OBJECT, WP_OBJECT_FEATURES_ALL);
return 1;
}
static int
object_manager_activate (lua_State *L)
{
WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
WpCore *core;
lua_pushliteral (L, "wireplumber_core");
lua_gettable (L, LUA_REGISTRYINDEX);
core = lua_touserdata (L, -1);
wp_core_install_object_manager (core, om);
return 0;
}
static int
object_manager_iterate (lua_State *L)
{
WpObjectManager *om = wplua_checkobject (L, 1, WP_TYPE_OBJECT_MANAGER);
WpIterator *it = wp_object_manager_iterate (om);
wplua_pushboxed (L, WP_TYPE_ITERATOR, it);
return 1;
}
static const luaL_Reg object_manager_methods[] = {
{ "activate", object_manager_activate },
{ "iterate", object_manager_iterate },
{ NULL, NULL }
};
void
wp_lua_scripting_api_init (lua_State *L)
{
g_autoptr (GError) error = NULL;
luaL_newlib (L, log_funcs);
lua_setglobal (L, "WpDebug");
wplua_register_type_methods (L, WP_TYPE_GLOBAL_PROXY,
NULL, global_proxy_methods);
wplua_register_type_methods (L, WP_TYPE_ITERATOR,
NULL, iterator_methods);
wplua_register_type_methods (L, WP_TYPE_OBJECT_INTEREST,
object_interest_new, NULL);
wplua_register_type_methods (L, WP_TYPE_OBJECT_MANAGER,
object_manager_new, object_manager_methods);
wplua_load_uri (L, URI_API, &error);
if (G_UNLIKELY (error))
wp_critical ("Failed to load api: %s", error->message);
}

View File

@@ -0,0 +1,77 @@
-- WirePlumber
--
-- This file contains the API that is made available to the Lua scripts
--
-- Copyright © 2020 Collabora Ltd.
-- @author George Kiagiadakis <george.kiagiadakis@collabora.com>
--
-- SPDX-License-Identifier: MIT
local function Constraint (spec)
assert (type(spec[1]) == "string", "Constraint: expected subject as string");
assert (type(spec[2]) == "string", "Constraint: expected verb as string");
local subject = spec[1]
local verb = spec[2]
local verbs = {
["="] = "equals",
["c"] = "in-list",
["~"] = "in-range",
["#"] = "matches",
["+"] = "is-present",
["-"] = "is-absent"
}
-- check and convert verb to its short version
local verb_is_valid = false
for k, v in pairs(verbs) do
if verb == k or verb == v then
verb = k
spec[2] = k
verb_is_valid = true
break
end
end
assert (verb_is_valid, "Constraint: invalid verb '" .. verb .. "'")
-- check and convert type to its integer value
local type = spec["type"]
if type then
local valid_types = { "pw-global", "pw", "gobject" }
local type_is_valid = false
for i, v in ipairs(valid_types) do
if type == v then
spec["type"] = i
type_is_valid = true
break
end
end
assert(type_is_valid, "Constraint: invalid subject type '" .. type .. "'")
end
-- check if we got the right amount of values
if verb == "=" or verb == "#" then
assert (spec[3] ~= nil,
"Constraint: " .. verbs[verb] .. ": expected constraint value")
elseif verb == "c" then
assert (spec[3] ~= nil,
"Constraint: " .. verbs[verb] .. ": expected at least one constraint value")
elseif verb == "~" then
assert (spec[3] ~= nil and spec[4] ~= nil,
"Constraint: " .. verbs[verb] .. ": expected two values")
else
assert (spec[3] == nil,
"Constraint: " .. verbs[verb] .. ": expected no value, but there is one")
end
return debug.setmetatable(spec, { __name = "Constraint" })
end
SANDBOX_EXPORT = {
Log = WpDebug,
ObjectManager = WpObjectManager_new,
Interest = WpObjectInterest_new,
Constraint = Constraint,
}

View File

@@ -0,0 +1,91 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <wplua/wplua.h>
struct _WpLuaScriptingEngine
{
GObject parent;
lua_State *L;
};
enum {
SIGNAL_INIT_LUA_CONTEXT,
N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0};
static void wp_lua_scripting_engine_parser_iface_init (WpConfigParserInterface * iface);
G_DECLARE_FINAL_TYPE (WpLuaScriptingEngine, wp_lua_scripting_engine,
WP, LUA_SCRIPTING_ENGINE, GObject)
G_DEFINE_TYPE_WITH_CODE (WpLuaScriptingEngine, wp_lua_scripting_engine,
G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (WP_TYPE_CONFIG_PARSER,
wp_lua_scripting_engine_parser_iface_init))
static void
wp_lua_scripting_engine_init (WpLuaScriptingEngine * self)
{
}
static void
wp_lua_scripting_engine_finalize (GObject * object)
{
WpLuaScriptingEngine * self = WP_LUA_SCRIPTING_ENGINE (object);
g_clear_pointer (&self->L, wplua_free);
G_OBJECT_CLASS (wp_lua_scripting_engine_parent_class)->finalize (object);
}
static void
wp_lua_scripting_engine_class_init (WpLuaScriptingEngineClass * klass)
{
GObjectClass * object_class = (GObjectClass *) klass;
object_class->finalize = wp_lua_scripting_engine_finalize;
signals[SIGNAL_INIT_LUA_CONTEXT] = g_signal_new ("init-lua-context",
G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL,
G_TYPE_NONE, 1, G_TYPE_POINTER);
}
static gboolean
wp_lua_scripting_engine_add_file (WpConfigParser * parser, const gchar * file)
{
WpLuaScriptingEngine * self = WP_LUA_SCRIPTING_ENGINE (parser);
g_autoptr (GError) error = NULL;
if (!wplua_load_path (self->L, file, &error)) {
wp_warning_object (self, "%s", error->message);
if (error->domain != WP_DOMAIN_LUA || error->code != WP_LUA_ERROR_RUNTIME)
return FALSE;
}
return TRUE;
}
static void
wp_lua_scripting_engine_reset (WpConfigParser * parser)
{
WpLuaScriptingEngine * self = WP_LUA_SCRIPTING_ENGINE (parser);
g_clear_pointer (&self->L, wplua_free);
self->L = wplua_new ();
g_signal_emit (self, signals[SIGNAL_INIT_LUA_CONTEXT], 0, self->L);
wplua_enable_sandbox (self->L);
}
static void
wp_lua_scripting_engine_parser_iface_init (WpConfigParserInterface * iface)
{
iface->add_file = wp_lua_scripting_engine_add_file;
iface->reset = wp_lua_scripting_engine_reset;
}

View File

@@ -0,0 +1,22 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#ifndef __WIREPLUMBER_LUA_SCRIPTING_ENGINE_H__
#define __WIREPLUMBER_LUA_SCRIPTING_ENGINE_H__
#include <wp/wp.h>
#include <wplua/wplua.h>
G_BEGIN_DECLS
#define WP_TYPE_LUA_SCRIPTING_ENGINE \
(wp_lua_scripting_engine_get_type ())
G_END_DECLS
#endif

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<gresources>
<gresource prefix="/org/freedesktop/pipewire/wireplumber/m-lua-scripting/">
<file compressed="true">api.lua</file>
</gresource>
</gresources>