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

@ -232,3 +232,23 @@ shared_library(
install_dir : wireplumber_module_dir,
dependencies : [wp_dep, pipewire_dep],
)
m_lua_scripting_resources = gnome.compile_resources(
'm-lua-scripting-resources',
'module-lua-scripting/gresource.xml',
source_dir: 'module-lua-scripting',
c_name: '_m_lua_scripting_resources')
shared_library(
'wireplumber-module-lua-scripting',
[
'module-lua-scripting.c',
'module-lua-scripting/engine.c',
'module-lua-scripting/api.c',
m_lua_scripting_resources,
],
c_args : [common_c_args, '-DG_LOG_DOMAIN="m-lua-scripting"'],
install : true,
install_dir : wireplumber_module_dir,
dependencies : [wp_dep, pipewire_dep, wplua_dep],
)

View file

@ -0,0 +1,182 @@
/* WirePlumber
*
* Copyright © 2020 Collabora Ltd.
* @author George Kiagiadakis <george.kiagiadakis@collabora.com>
*
* SPDX-License-Identifier: MIT
*/
#include <wp/wp.h>
#include <wplua/wplua.h>
#include <pipewire/keys.h>
#define WP_TYPE_LUA_SCRIPTING_ENGINE \
(wp_lua_scripting_engine_get_type ())
GType wp_lua_scripting_engine_get_type ();
void wp_lua_scripting_api_init (lua_State *L);
struct _WpLuaScriptingPlugin
{
WpPlugin parent;
/* properties */
gchar *profile;
/* data */
WpCore *export_core;
gchar *config_ext;
};
enum {
PROP_0,
PROP_PROFILE,
};
G_DECLARE_FINAL_TYPE (WpLuaScriptingPlugin, wp_lua_scripting_plugin,
WP, LUA_SCRIPTING_PLUGIN, WpPlugin)
G_DEFINE_TYPE (WpLuaScriptingPlugin, wp_lua_scripting_plugin, WP_TYPE_PLUGIN)
static void
wp_lua_scripting_plugin_init (WpLuaScriptingPlugin * self)
{
}
static void
wp_lua_scripting_plugin_finalize (GObject * object)
{
WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (object);
g_clear_pointer (&self->profile, g_free);
G_OBJECT_CLASS (wp_lua_scripting_plugin_parent_class)->finalize (object);
}
static void
wp_lua_scripting_plugin_set_property (GObject * object, guint property_id,
const GValue * value, GParamSpec * pspec)
{
WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (object);
switch (property_id) {
case PROP_PROFILE:
g_clear_pointer (&self->profile, g_free);
self->profile = g_value_dup_string (value);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_lua_scripting_plugin_get_property (GObject * object, guint property_id,
GValue * value, GParamSpec * pspec)
{
WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (object);
switch (property_id) {
case PROP_PROFILE:
g_value_set_string (value, self->profile);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
break;
}
}
static void
wp_lua_scripting_plugin_init_lua_ctx (WpConfigParser * engine, lua_State * L,
WpLuaScriptingPlugin * self)
{
g_autoptr (WpCore) core = wp_plugin_get_core (WP_PLUGIN (self));
lua_pushliteral (L, "wireplumber_core");
lua_pushlightuserdata (L, core);
lua_settable (L, LUA_REGISTRYINDEX);
lua_pushliteral (L, "wireplumber_export_core");
lua_pushlightuserdata (L, self->export_core);
lua_settable (L, LUA_REGISTRYINDEX);
wp_lua_scripting_api_init (L);
}
static void
wp_lua_scripting_plugin_activate (WpPlugin * plugin)
{
WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin);
g_autoptr (WpCore) core = wp_plugin_get_core (plugin);
g_autoptr (WpConfiguration) config = wp_configuration_get_instance (core);
g_autoptr (WpConfigParser) engine = NULL;
/* initialize secondary connection to pipewire */
self->export_core = wp_core_clone (core);
wp_core_update_properties (self->export_core, wp_properties_new (
PW_KEY_APP_NAME, "WirePlumber (export)",
NULL));
if (!wp_core_connect (self->export_core)) {
wp_warning_object (self, "failed to connect export core");
return;
}
/* load the lua scripts & execute them via the engine */
self->config_ext = g_strdup_printf ("%s/lua", self->profile);
wp_configuration_add_extension (config, self->config_ext,
WP_TYPE_LUA_SCRIPTING_ENGINE);
engine = wp_configuration_get_parser (config, self->config_ext);
g_signal_connect (engine, "init-lua-context",
G_CALLBACK (wp_lua_scripting_plugin_init_lua_ctx), plugin);
wp_configuration_reload (config, self->config_ext);
}
static void
wp_lua_scripting_plugin_deactivate (WpPlugin * plugin)
{
WpLuaScriptingPlugin * self = WP_LUA_SCRIPTING_PLUGIN (plugin);
g_autoptr (WpCore) core = wp_plugin_get_core (plugin);
if (core) {
g_autoptr (WpConfiguration) config = wp_configuration_get_instance (core);
wp_configuration_remove_extension (config, self->config_ext);
}
g_clear_pointer (&self->config_ext, g_free);
g_clear_object (&self->export_core);
}
static void
wp_lua_scripting_plugin_class_init (WpLuaScriptingPluginClass * klass)
{
GObjectClass *object_class = (GObjectClass *) klass;
WpPluginClass *plugin_class = (WpPluginClass *) klass;
object_class->finalize = wp_lua_scripting_plugin_finalize;
object_class->set_property = wp_lua_scripting_plugin_set_property;
object_class->get_property = wp_lua_scripting_plugin_get_property;
plugin_class->activate = wp_lua_scripting_plugin_activate;
plugin_class->deactivate = wp_lua_scripting_plugin_deactivate;
g_object_class_install_property(object_class, PROP_PROFILE,
g_param_spec_string ("profile", "profile",
"The configuration profile", NULL,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
}
WP_PLUGIN_EXPORT void
wireplumber__module_init (WpModule * module, WpCore * core, GVariant * args)
{
const gchar *profile;
if (!g_variant_lookup (args, "profile", "&s", &profile)) {
wp_warning_object (module, "module-lua-scripting requires a 'profile'");
return;
}
wp_plugin_register (g_object_new (wp_lua_scripting_plugin_get_type (),
"name", "lua-scripting",
"module", module,
"profile", profile,
NULL));
}

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>