diff --git a/degesch.c b/degesch.c index ff48263..6b32f7b 100644 --- a/degesch.c +++ b/degesch.c @@ -7280,6 +7280,11 @@ irc_hook_insert (struct app_context *ctx, struct irc_hook *hook) // --- Lua --------------------------------------------------------------------- +// Each plugin has its own Lua state object, so that a/ they don't disturb each +// other and b/ unloading a plugin releases all resources. +// +// References to internal objects (buffers, servers) are all weak. + #ifdef HAVE_LUA struct lua_plugin @@ -7303,6 +7308,427 @@ struct plugin_vtable lua_plugin_vtable = // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// The registry can be used as a cache for weakly referenced objects + +static bool +lua_cache_get (lua_State *L, void *object) +{ + lua_rawgetp (L, LUA_REGISTRYINDEX, object); + if (lua_isnil (L, -1)) + { + lua_pop (L, 1); + return false; + } + return true; +} + +static void +lua_cache_store (lua_State *L, void *object, int index) +{ + lua_pushvalue (L, index); + lua_rawsetp (L, LUA_REGISTRYINDEX, object); +} + +static void +lua_cache_invalidate (lua_State *L, void *object) +{ + lua_pushnil (L); + lua_rawsetp (L, LUA_REGISTRYINDEX, object); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#define XLUA_BUFFER_METATABLE "buffer" ///< Identifier for the Lua metatable + +struct lua_buffer +{ + struct lua_plugin *plugin; ///< The plugin we belong to + struct buffer *buffer; ///< The buffer + struct weak_ref_link *weak_ref; ///< A weak reference link +}; + +static int +lua_buffer_gc (lua_State *L) +{ + struct lua_buffer *wrapper = luaL_checkudata (L, 1, XLUA_BUFFER_METATABLE); + if (wrapper->buffer) + { + lua_cache_invalidate (L, wrapper->buffer); + buffer_weak_unref (wrapper->buffer, &wrapper->weak_ref); + wrapper->buffer = NULL; + } + return 0; +} + +static int +lua_buffer_log (lua_State *L) +{ + struct lua_buffer *wrapper = luaL_checkudata (L, 1, XLUA_BUFFER_METATABLE); + luaL_argcheck (L, wrapper->buffer, 1, "dead reference used"); + const char *message = luaL_checkstring (L, 2); + + struct buffer *buffer = wrapper->buffer; + log_full (wrapper->plugin->ctx, buffer->server, buffer, + BUFFER_LINE_STATUS, "#s", message); + return 0; +} + +static luaL_Reg lua_buffer_table[] = +{ + // TODO: some useful methods or values + { "__gc", lua_buffer_gc }, + { "log", lua_buffer_log }, + { NULL, NULL } +}; + +static void +lua_buffer_invalidate (void *object, void *user_data) +{ + struct lua_buffer *wrapper = user_data; + wrapper->buffer = NULL; + wrapper->weak_ref = NULL; + // This can in theory call the GC, order isn't arbitrary here + lua_cache_invalidate (wrapper->plugin->L, object); +} + +static void +lua_plugin_push_buffer (struct lua_plugin *plugin, struct buffer *buffer) +{ + struct lua_State *L = plugin->L; + if (lua_cache_get (L, buffer)) + return; + + struct lua_buffer *wrapper = lua_newuserdata (L, sizeof *wrapper); + luaL_setmetatable (L, XLUA_BUFFER_METATABLE); + wrapper->plugin = plugin; + wrapper->buffer = buffer; + wrapper->weak_ref = buffer_weak_ref + (buffer, lua_buffer_invalidate, wrapper); + lua_cache_store (L, buffer, -1); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#define XLUA_SERVER_METATABLE "server" ///< Identifier for the Lua metatable + +struct lua_server +{ + struct lua_plugin *plugin; ///< The plugin we belong to + struct server *server; ///< The server + struct weak_ref_link *weak_ref; ///< A weak reference link +}; + +static int +lua_server_gc (lua_State *L) +{ + struct lua_server *wrapper = luaL_checkudata (L, 1, XLUA_SERVER_METATABLE); + if (wrapper->server) + { + lua_cache_invalidate (L, wrapper->server); + server_weak_unref (wrapper->server, &wrapper->weak_ref); + wrapper->server = NULL; + } + return 0; +} + +static int +lua_server_send (lua_State *L) +{ + struct lua_server *wrapper = luaL_checkudata (L, 1, XLUA_SERVER_METATABLE); + luaL_argcheck (L, wrapper->server, 1, "dead reference used"); + const char *line = luaL_checkstring (L, 2); + + irc_send (wrapper->server, "%s", line); + return 0; +} + +static luaL_Reg lua_server_table[] = +{ + // TODO: some useful methods or values + { "__gc", lua_server_gc }, + { "send", lua_server_send }, + { NULL, NULL } +}; + +static void +lua_server_invalidate (void *object, void *user_data) +{ + struct lua_server *wrapper = user_data; + wrapper->server = NULL; + wrapper->weak_ref = NULL; + // This can in theory call the GC, order isn't arbitrary here + lua_cache_invalidate (wrapper->plugin->L, object); +} + +static void +lua_plugin_push_server (struct lua_plugin *plugin, struct server *server) +{ + struct lua_State *L = plugin->L; + if (lua_cache_get (L, server)) + return; + + struct lua_server *wrapper = lua_newuserdata (L, sizeof *wrapper); + luaL_setmetatable (L, XLUA_SERVER_METATABLE); + wrapper->plugin = plugin; + wrapper->server = server; + wrapper->weak_ref = server_weak_ref + (server, lua_server_invalidate, wrapper); + lua_cache_store (L, server, -1); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +#define XLUA_HOOK_METATABLE "hook" ///< Identifier for the Lua metatable + +enum lua_hook_type +{ + XLUA_HOOK_DEFUNCT, ///< No longer functional + XLUA_HOOK_INPUT, ///< Input hook + XLUA_HOOK_IRC, ///< IRC hook +}; + +struct lua_hook +{ + struct lua_plugin *plugin; ///< The plugin we belong to + enum lua_hook_type type; ///< Type of the hook + union + { + struct input_hook input_hook; ///< Input hook + struct irc_hook irc_hook; ///< IRC hook + } + data; ///< Hook data +}; + +static int +lua_hook_unhook (lua_State *L) +{ + struct lua_hook *hook = luaL_checkudata (L, 1, XLUA_HOOK_METATABLE); + switch (hook->type) + { + case XLUA_HOOK_INPUT: + LIST_UNLINK (hook->plugin->ctx->input_hooks, &hook->data.input_hook); + break; + case XLUA_HOOK_IRC: + LIST_UNLINK (hook->plugin->ctx->irc_hooks, &hook->data.irc_hook); + break; + default: + hard_assert (!"invalid hook type"); + case XLUA_HOOK_DEFUNCT: + break; + } + + // The hook no longer has to stay alive + lua_pushnil (L); + lua_rawsetp (L, LUA_REGISTRYINDEX, hook); + hook->type = XLUA_HOOK_DEFUNCT; + return 0; +} + +// The hook dies either when the plugin requests it or at plugin unload +static luaL_Reg lua_hook_table[] = +{ + { "unhook", lua_hook_unhook }, + { "__gc", lua_hook_unhook }, + { NULL, NULL } +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// Append a traceback to all errors so that we can later extract it +static int +lua_plugin_error_handler (lua_State *L) +{ + luaL_traceback (L, L, luaL_checkstring (L, 1), 1); + return 1; +} + +static bool +lua_plugin_process_error (struct lua_plugin *self, const char *message, + struct error **e) +{ + struct str_vector v; + str_vector_init (&v); + cstr_split_ignore_empty (message, '\n', &v); + + if (v.len < 2) + error_set (e, "%s", message); + else + { + error_set (e, "%s", v.vector[0]); + log_global_debug (self->ctx, "Lua: plugin \"#s\": #s", + self->super.name, v.vector[1]); + for (size_t i = 2; i < v.len; i++) + log_global_debug (self->ctx, " #s", v.vector[i]); + } + + str_vector_free (&v); + return false; +} + +// Convenience function; replaces the "original" string or produces an error +static bool +lua_plugin_handle_string_filter_result (struct lua_plugin *self, + int result, char **original, bool utf8, struct error **e) +{ + lua_State *L = self->L; + if (result) + return lua_plugin_process_error (self, lua_tostring (L, -1), e); + if (lua_isnil (L, -1)) + return NULL; + if (!lua_isstring (L, -1)) + FAIL ("must return either a string or nil"); + + size_t len; + const char *processed = lua_tolstring (L, -1, &len); + if (utf8 && !utf8_validate (processed, len)) + FAIL ("must return valid UTF-8"); + + // Only replace the string if it's different + if (strcmp (processed, *original)) + { + free (*original); + *original = xstrdup (processed); + } + return true; +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static char * +lua_input_hook_filter (struct input_hook *self, struct buffer *buffer, + char *input) +{ + struct lua_hook *hook = (struct lua_hook *) + ((char *) self - offsetof (struct lua_hook, data.input_hook)); + struct lua_plugin *plugin = hook->plugin; + lua_State *L = plugin->L; + + lua_pushcfunction (L, lua_plugin_error_handler); + + lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook + lua_getuservalue (L, -1); // Retrieve function + lua_insert (L, -2); // Swap with thet hook + + lua_plugin_push_buffer (plugin, buffer); // 2: buffer + lua_pushstring (L, input); // 3: input + + struct error *e = NULL; + bool failed = !lua_plugin_handle_string_filter_result + (plugin, lua_pcall (L, 3, 1, -5), &input, true, &e); + lua_pop (L, 1); + + if (failed) + { + log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s: #s", + plugin->super.name, "input hook", e->message); + error_free (e); + } + return input; +} + +struct input_hook_vtable lua_input_hook_vtable = +{ + .filter = lua_input_hook_filter, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static char * +lua_irc_hook_filter (struct irc_hook *self, struct server *s, char *message) +{ + struct lua_hook *hook = (struct lua_hook *) + ((char *) self - offsetof (struct lua_hook, data.irc_hook)); + struct lua_plugin *plugin = hook->plugin; + lua_State *L = plugin->L; + + lua_pushcfunction (L, lua_plugin_error_handler); + + lua_rawgetp (L, LUA_REGISTRYINDEX, hook); // 1: hook + lua_getuservalue (L, -1); // Retrieve function + lua_insert (L, -2); // Swap with thet hook + + lua_plugin_push_server (plugin, s); // 2: server + lua_pushstring (L, message); // 3: message + + struct error *e = NULL; + bool failed = !lua_plugin_handle_string_filter_result + (plugin, lua_pcall (L, 3, 1, -5), &message, false, &e); + lua_pop (L, 1); + + if (failed) + { + log_global_error (plugin->ctx, "Lua: plugin \"#s\": #s: #s", + plugin->super.name, "IRC hook", e->message); + error_free (e); + } + return message; +} + +struct irc_hook_vtable lua_irc_hook_vtable = +{ + .filter = lua_irc_hook_filter, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +static struct lua_hook * +lua_plugin_push_hook + (struct lua_plugin *plugin, int callback_index, enum lua_hook_type type) +{ + struct lua_State *L = plugin->L; + struct lua_hook *hook = lua_newuserdata (L, sizeof *hook); + luaL_setmetatable (L, XLUA_HOOK_METATABLE); + memset (hook, 0, sizeof *hook); + hook->type = type; + hook->plugin = plugin; + + // Associate the callback with the hook + lua_pushvalue (L, callback_index); + lua_setuservalue (L, -2); + + // Make sure the hook doesn't get garbage collected and return it + lua_pushvalue (L, -1); + lua_rawsetp (L, LUA_REGISTRYINDEX, hook); + return hook; +} + +static int +lua_plugin_hook_input (lua_State *L) +{ + struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); + luaL_checktype (L, 1, LUA_TFUNCTION); + lua_Integer priority = luaL_optinteger (L, 2, 0); + + struct lua_hook *hook = lua_plugin_push_hook (plugin, 1, XLUA_HOOK_INPUT); + hook->data.input_hook.vtable = &lua_input_hook_vtable; + hook->data.input_hook.priority = priority; + input_hook_insert (plugin->ctx, &hook->data.input_hook); + return 1; +} + +static int +lua_plugin_hook_irc (lua_State *L) +{ + struct lua_plugin *plugin = lua_touserdata (L, lua_upvalueindex (1)); + luaL_checktype (L, 1, LUA_TFUNCTION); + lua_Integer priority = luaL_optinteger (L, 2, 0); + + struct lua_hook *hook = lua_plugin_push_hook (plugin, 1, XLUA_HOOK_IRC); + hook->data.irc_hook.vtable = &lua_irc_hook_vtable; + hook->data.irc_hook.priority = priority; + irc_hook_insert (plugin->ctx, &hook->data.irc_hook); + return 1; +} + +static luaL_Reg lua_plugin_library[] = +{ + { "hook_input", lua_plugin_hook_input }, + { "hook_irc", lua_plugin_hook_irc }, + { NULL, NULL }, +}; + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + static void * lua_plugin_alloc (void *ud, void *ptr, size_t o_size, size_t n_size) { @@ -7326,10 +7752,22 @@ lua_plugin_panic (lua_State *L) return 0; } -static luaL_Reg lua_plugin_library[] = +static void +lua_plugin_create_meta (lua_State *L, const char *name, luaL_Reg *fns) { - { NULL, NULL }, -}; + luaL_newmetatable (L, name); + luaL_setfuncs (L, fns, 0); + + // Otherwise any non-meta functions would be inaccessible + bool has_own_index = (lua_getfield (L, -1, "__index") != LUA_TNIL); + lua_pop (L, 1); + if (!has_own_index) + { + lua_pushvalue (L, -1); + lua_setfield (L, -2, "__index"); + } + lua_pop (L, 1); +} static struct plugin * lua_plugin_load (struct app_context *ctx, const char *filename, @@ -7345,24 +7783,33 @@ lua_plugin_load (struct app_context *ctx, const char *filename, lua_atpanic (L, lua_plugin_panic); luaL_openlibs (L); - luaL_newlib (L, lua_plugin_library); - lua_setglobal (L, PROGRAM_NAME); - - int ret; - if ((ret = luaL_loadfile (L, filename)) - || (ret = lua_pcall (L, 0, 0, 0))) - { - error_set (e, "%s: %s", "Lua", lua_tostring (L, -1)); - lua_close (L); - return NULL; - } - struct lua_plugin *plugin = xcalloc (1, sizeof *plugin); plugin->super.name = NULL; plugin->super.vtable = &lua_plugin_vtable; plugin->ctx = ctx; plugin->L = L; - return &plugin->super; + + // Register the degesch library with "plugin" as an upvalue + luaL_checkversion (L); + luaL_newlibtable (L, lua_plugin_library); + lua_pushlightuserdata (L, plugin); + luaL_setfuncs (L, lua_plugin_library, 1); + lua_setglobal (L, PROGRAM_NAME); + + // Create metatables for our objects + 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); + + int ret; + if (!(ret = luaL_loadfile (L, filename)) + && !(ret = lua_pcall (L, 0, 0, 0))) + return &plugin->super; + + error_set (e, "%s: %s", "Lua", lua_tostring (L, -1)); + lua_close (L); + free (plugin); + return NULL; } #endif // HAVE_LUA