From 04f87b75876d9ddd62d75981127bf624e0dac894 Mon Sep 17 00:00:00 2001
From: Přemysl Janouch
Date: Mon, 28 Dec 2015 02:03:26 +0100
Subject: degesch: enable configuration in Lua plugins
---
degesch.c | 325 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
liberty | 2 +-
2 files changed, 321 insertions(+), 6 deletions(-)
diff --git a/degesch.c b/degesch.c
index 12c1345..a6b5e8d 100644
--- a/degesch.c
+++ b/degesch.c
@@ -1939,6 +1939,7 @@ register_config_modules (struct app_context *ctx)
// The servers are loaded later when we can create buffers for them
config_register_module (config, "servers", NULL, NULL);
config_register_module (config, "aliases", NULL, NULL);
+ config_register_module (config, "plugins", NULL, NULL);
config_register_module (config, "behaviour", load_config_behaviour, ctx);
config_register_module (config, "attributes", load_config_attributes, ctx);
}
@@ -2004,6 +2005,12 @@ get_aliases_config (struct app_context *ctx)
return &config_item_get (ctx->config.root, "aliases", NULL)->value.object;
}
+static struct str_map *
+get_plugins_config (struct app_context *ctx)
+{
+ return &config_item_get (ctx->config.root, "plugins", NULL)->value.object;
+}
+
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
static void
@@ -7290,6 +7297,22 @@ server_rename (struct app_context *ctx, struct server *s, const char *new_name)
}
}
+// --- Plugins -----------------------------------------------------------------
+
+/// Returns the basename of the plugin's name without any extensions,
+/// or NULL if the name isn't suitable (starts with a dot)
+static char *
+plugin_config_name (struct plugin *self)
+{
+ const char *begin = self->name;
+ for (const char *p = begin; *p; )
+ if (*p++ == '/')
+ begin = p;
+
+ size_t len = strcspn (begin, ".");
+ return len ? xstrndup (begin, len) : NULL;
+}
+
// --- Lua ---------------------------------------------------------------------
// Each plugin has its own Lua state object, so that a/ they don't disturb each
@@ -7304,6 +7327,8 @@ struct lua_plugin
struct plugin super; ///< The structure we're deriving
struct app_context *ctx; ///< Application context
lua_State *L; ///< Lua state
+
+ struct lua_schema_item *schemas; ///< Registered schema items
};
static void
@@ -7819,12 +7844,300 @@ lua_plugin_hook_timer (lua_State *L)
return 1;
}
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+#define XLUA_SCHEMA_METATABLE "schema" ///< Identifier for the Lua metatable
+
+struct lua_schema_item
+{
+ LIST_HEADER (struct lua_schema_item)
+
+ struct lua_plugin *plugin; ///< The plugin we belong to
+ struct config_item *item; ///< The item managed by the schema
+ struct config_schema schema; ///< Schema itself
+
+ int ref_validate; ///< Reference to "validate" callback
+ int ref_on_change; ///< Reference to "on_change" callback
+};
+
+static void
+lua_schema_item_discard (struct lua_schema_item *self)
+{
+ if (self->item)
+ {
+ self->item->schema = NULL;
+ self->item->user_data = NULL;
+ self->item = NULL;
+ LIST_UNLINK (self->plugin->schemas, self);
+ }
+
+ // Now that we've disconnected from the item, allow garbage collection
+ lua_cache_invalidate (self->plugin->L, self);
+}
+
+static int
+lua_schema_item_gc (lua_State *L)
+{
+ struct lua_schema_item *self =
+ luaL_checkudata (L, 1, XLUA_SCHEMA_METATABLE);
+ lua_schema_item_discard (self);
+
+ free ((char *) self->schema.name);
+ free ((char *) self->schema.comment);
+ free ((char *) self->schema.default_);
+
+ luaL_unref (L, LUA_REGISTRYINDEX, self->ref_validate);
+ luaL_unref (L, LUA_REGISTRYINDEX, self->ref_on_change);
+ return 0;
+}
+
+static luaL_Reg lua_schema_item_table[] =
+{
+ { "__gc", lua_schema_item_gc },
+ { NULL, NULL }
+};
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+/// Unfortunately this has the same problem as JSON libraries in that Lua
+/// cannot store null values in containers (it has no distinct "undefined" type)
+static void
+lua_plugin_push_config_item (lua_State *L, const struct config_item *item)
+{
+ switch (item->type)
+ {
+ case CONFIG_ITEM_NULL:
+ lua_pushnil (L);
+ break;
+ case CONFIG_ITEM_OBJECT:
+ {
+ lua_createtable (L, 0, item->value.object.len);
+
+ struct str_map_iter iter;
+ str_map_iter_init (&iter, &item->value.object);
+ struct config_item *child;
+ while ((child = str_map_iter_next (&iter)))
+ {
+ lua_plugin_push_config_item (L, child);
+ lua_setfield (L, -2, iter.link->key);
+ }
+ break;
+ }
+ case CONFIG_ITEM_BOOLEAN:
+ lua_pushboolean (L, item->value.boolean);
+ break;
+ case CONFIG_ITEM_INTEGER:
+ lua_pushinteger (L, item->value.integer);
+ break;
+ case CONFIG_ITEM_STRING:
+ case CONFIG_ITEM_STRING_ARRAY:
+ lua_pushlstring (L, item->value.string.str, item->value.string.len);
+ break;
+ }
+}
+
+static bool
+lua_schema_item_validate (const struct config_item *item, struct error **e)
+{
+ struct lua_schema_item *self = item->user_data;
+ if (self->ref_validate == LUA_REFNIL)
+ return true;
+
+ struct lua_plugin *plugin = self->plugin;
+ lua_State *L = plugin->L;
+
+ lua_pushcfunction (L, lua_plugin_error_handler);
+ lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_validate);
+ lua_plugin_push_config_item (L, item);
+
+ // The callback can make use of error("...", 0) to produce nice messages
+ if (lua_pcall (L, 1, 0, -3))
+ {
+ (void) lua_plugin_process_error (plugin, lua_tostring (L, -1), e);
+ lua_pop (L, 1);
+ return false;
+ }
+ return true;
+}
+
+static void
+lua_schema_item_on_change (struct config_item *item)
+{
+ struct lua_schema_item *self = item->user_data;
+ if (self->ref_on_change == LUA_REFNIL)
+ return;
+
+ struct lua_plugin *plugin = self->plugin;
+ lua_State *L = plugin->L;
+
+ lua_pushcfunction (L, lua_plugin_error_handler);
+ lua_rawgeti (L, LUA_REGISTRYINDEX, self->ref_on_change);
+ lua_plugin_push_config_item (L, item);
+
+ if (lua_pcall (L, 1, 0, -3))
+ {
+ struct error *e = NULL;
+ (void) lua_plugin_process_error (plugin, lua_tostring (L, -1), &e);
+ lua_pop (L, 1);
+
+ log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s: #s",
+ plugin->super.name, "schema on_change", e->message);
+ error_free (e);
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+static int
+lua_plugin_decode_config_item_type (const char *type)
+{
+ if (!strcmp (type, "null")) return CONFIG_ITEM_NULL;
+ if (!strcmp (type, "object")) return CONFIG_ITEM_OBJECT;
+ if (!strcmp (type, "boolean")) return CONFIG_ITEM_BOOLEAN;
+ if (!strcmp (type, "integer")) return CONFIG_ITEM_INTEGER;
+ if (!strcmp (type, "string")) return CONFIG_ITEM_STRING;
+ if (!strcmp (type, "string_array")) return CONFIG_ITEM_STRING_ARRAY;
+ return -1;
+}
+
+static bool
+lua_plugin_check_field (lua_State *L, int idx, const char *name,
+ int expected, bool optional)
+{
+ int found = lua_getfield (L, idx, name);
+ if (found == expected)
+ return true;
+ if (optional && found == LUA_TNIL)
+ return false;
+
+ const char *message = optional
+ ? "invalid field \"%s\" (found: %s, expected: %s or nil)"
+ : "invalid or missing field \"%s\" (found: %s, expected: %s)";
+ return luaL_error (L, message, name,
+ lua_typename (L, found), lua_typename (L, expected));
+}
+
+static int
+lua_plugin_add_config_schema (struct lua_plugin *plugin,
+ struct config_item *subtree, const char *name)
+{
+ struct config_item *item = str_map_find (&subtree->value.object, name);
+ lua_State *L = plugin->L;
+
+ // This should only ever happen because of a conflict with another plugin;
+ // this is the price we pay for simplicity
+ if (item && item->schema)
+ {
+ struct lua_schema_item *owner = item->user_data;
+ return luaL_error (L, "conflicting schema item: %s (owned by: %s)",
+ name, owner->plugin->super.name);
+ }
+
+ // Create and initialize a full userdata wrapper for the schema item
+ struct lua_schema_item *self = lua_newuserdata (L, sizeof *self);
+ luaL_setmetatable (L, XLUA_SCHEMA_METATABLE);
+ memset (self, 0, sizeof *self);
+
+ self->plugin = plugin;
+ self->ref_on_change = LUA_REFNIL;
+ self->ref_validate = LUA_REFNIL;
+
+ struct config_schema *schema = &self->schema;
+ schema->name = xstrdup (name);
+ schema->comment = NULL;
+ schema->default_ = NULL;
+ schema->type = CONFIG_ITEM_NULL;
+
+ // Try to update the defaults with values provided by the plugin
+ int values = lua_absindex (L, -2);
+ (void) lua_plugin_check_field (L, values, "type", LUA_TSTRING, false);
+ int item_type = schema->type =
+ lua_plugin_decode_config_item_type (lua_tostring (L, -1));
+ if (item_type == -1)
+ return luaL_error (L, "invalid type of schema item");
+
+ if (lua_plugin_check_field (L, values, "comment", LUA_TSTRING, true))
+ schema->comment = xstrdup (lua_tostring (L, -1));
+ if (lua_plugin_check_field (L, values, "default", LUA_TSTRING, true))
+ schema->default_ = xstrdup (lua_tostring (L, -1));
+ if (lua_plugin_check_field (L, values, "on_change", LUA_TFUNCTION, true))
+ self->ref_on_change = luaL_ref (L, -1);
+ if (lua_plugin_check_field (L, values, "validate", LUA_TFUNCTION, true))
+ self->ref_validate = luaL_ref (L, -1);
+
+ lua_pop (L, 5);
+
+ // Try to install the created schema item into our configuration
+ struct error *warning = NULL, *e = NULL;
+ item = config_schema_initialize_item
+ (&self->schema, subtree, self, &warning, &e);
+
+ if (warning)
+ {
+ log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s",
+ plugin->super.name, warning->message);
+ error_free (warning);
+ }
+ if (e)
+ {
+ const char *error = lua_pushstring (L, e->message);
+ error_free (e);
+ return luaL_error (L, "%s", error);
+ }
+
+ self->item = item;
+ LIST_PREPEND (plugin->schemas, self);
+
+ // On the stack there should be the schema table and the resulting object;
+ // we need to make sure Lua doesn't GC the second and get rid of them both
+ lua_cache_store (L, self, -1);
+ lua_pop (L, 2);
+ return 0;
+}
+
+static int
+lua_plugin_setup_config (lua_State *L)
+{
+ struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1));
+ luaL_checktype (L, 1, LUA_TTABLE);
+
+ struct app_context *ctx = plugin->ctx;
+ char *config_name = plugin_config_name (&plugin->super);
+ if (!config_name)
+ return luaL_error (L, "unsuitable plugin name");
+
+ struct str_map *plugins = get_plugins_config (ctx);
+ struct config_item *subtree = str_map_find (plugins, config_name);
+ if (!subtree || subtree->type != CONFIG_ITEM_OBJECT)
+ str_map_set (plugins, config_name, (subtree = config_item_object ()));
+ free (config_name);
+
+ LIST_FOR_EACH (struct lua_schema_item, iter, plugin->schemas)
+ lua_schema_item_discard (iter);
+
+ // Load all schema items and apply them to the plugin's subtree
+ lua_pushnil (L);
+ while (lua_next (L, 1))
+ {
+ if (lua_type (L, -2) != LUA_TSTRING
+ || lua_type (L, -1) != LUA_TTABLE)
+ return luaL_error (L, "%s: %s -> %s", "invalid types",
+ lua_typename (L, -2), lua_typename (L, -1));
+ lua_plugin_add_config_schema (plugin, subtree, lua_tostring (L, -2));
+ }
+
+ // Let the plugin read out configuration via on_change callbacks
+ config_schema_call_changed (subtree);
+ return 0;
+}
+
static luaL_Reg lua_plugin_library[] =
{
- { "hook_input", lua_plugin_hook_input },
- { "hook_irc", lua_plugin_hook_irc },
- { "hook_timer", lua_plugin_hook_timer },
- { NULL, NULL },
+ { "hook_input", lua_plugin_hook_input },
+ { "hook_irc", lua_plugin_hook_irc },
+ { "hook_timer", lua_plugin_hook_timer },
+ { "setup_config", lua_plugin_setup_config },
+ { NULL, NULL },
};
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -7921,6 +8234,7 @@ lua_plugin_load (struct app_context *ctx, const char *filename,
lua_plugin_create_meta (L, XLUA_HOOK_METATABLE, lua_hook_table);
lua_plugin_create_meta (L, XLUA_BUFFER_METATABLE, lua_buffer_table);
lua_plugin_create_meta (L, XLUA_SERVER_METATABLE, lua_server_table);
+ lua_plugin_create_meta (L, XLUA_SCHEMA_METATABLE, lua_schema_item_table);
int ret;
if (!(ret = luaL_loadfile (L, filename))
@@ -9351,7 +9665,8 @@ try_handle_command_help_option (struct app_context *ctx, const char *name)
log_global_indent (ctx, "");
log_global_indent (ctx, "Option \"#s\":", name);
- log_global_indent (ctx, " Description: #s", schema->comment);
+ log_global_indent (ctx, " Description: #s",
+ schema->comment ? schema->comment : "(none)");
log_global_indent (ctx, " Type: #s", config_item_type_name (schema->type));
log_global_indent (ctx, " Default: #s",
schema->default_ ? schema->default_ : "null");
diff --git a/liberty b/liberty
index 8b2e41e..f6d7454 160000
--- a/liberty
+++ b/liberty
@@ -1 +1 @@
-Subproject commit 8b2e41ed8ffac0494763495896c6a80a9e9db543
+Subproject commit f6d74544f82ce8186e73a6ba268c2bc56b3ce5c7
--
cgit v1.2.3-70-g09d2