degesch: enable configuration in Lua plugins

This commit is contained in:
Přemysl Eric Janouch 2015-12-28 02:03:26 +01:00
parent b7dd384048
commit 04f87b7587
2 changed files with 321 additions and 6 deletions

325
degesch.c
View File

@ -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");

@ -1 +1 @@
Subproject commit 8b2e41ed8ffac0494763495896c6a80a9e9db543
Subproject commit f6d74544f82ce8186e73a6ba268c2bc56b3ce5c7