From 04f87b75876d9ddd62d75981127bf624e0dac894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Mon, 28 Dec 2015 02:03:26 +0100 Subject: [PATCH] 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