fix operation of module_unload/module_destroy so that unloaded modules are now destroyed once they have been unloaded
authorTero Marttila <terom@fixme.fi>
Thu, 02 Apr 2009 02:25:35 +0300
changeset 110 43e9a7984955
parent 109 bfe9b9a8fe5b
child 111 5a1ebffca81a
fix operation of module_unload/module_destroy so that unloaded modules are now destroyed once they have been unloaded
src/irc_log.c
src/module.c
src/module.h
src/nexus.c
test/valgrind-suppressions
--- a/src/irc_log.c	Wed Apr 01 19:31:11 2009 +0300
+++ b/src/irc_log.c	Thu Apr 02 02:25:35 2009 +0300
@@ -27,6 +27,9 @@
     
     /** Are we currently unloading ourself (via irc_log_unload) ? */
     bool unloading;
+
+    /** The `struct module` passed to us once we unload() */
+    struct module *module;
 };
 
 /**
@@ -53,22 +56,13 @@
 
 /**
  * The irc_log_ctx has completed stopping all the channels, and should now destroy itself
- *
  */
 static void irc_log_stopped (struct irc_log_ctx *ctx)
 {
-    log_debug("destroying the irc_log_ctx");
-
-    // schedule an evsql_destroy
-    if (evsql_destroy_next(ctx->db))
-        log_fatal("evsql_destroy_next failed");
+    log_debug("module unload() completed");
     
-    // hands-off on the db
-    ctx->db = NULL;
-
-    // free ourself
-    // XXX: wrong, we need to implement a irc_log_destroy that's called by module?
-    free(ctx);
+    // notify
+    module_unloaded(ctx->module);
 }
 
 /**
@@ -478,13 +472,14 @@
 /**
  * Deinitialize, logging CLOSE events for all channels, and removing any hooks we've added
  */
-static err_t irc_log_unload (void *_ctx)
+static err_t irc_log_unload (void *_ctx, struct module *module)
 {
     struct irc_log_ctx *ctx = _ctx;
     struct irc_log_chan *chan_ctx;
 
     // update our state to mark ourselves as unloading
     ctx->unloading = true;
+    ctx->module = module;
 
     // stop logging each channel
     TAILQ_FOREACH(chan_ctx, &ctx->channels, ctx_channels) {
@@ -496,11 +491,28 @@
 }
 
 /**
+ * We can safely destroy the evsql instance from here, since we're outside of any callbacks from this module
+ */
+static void irc_log_destroy (void *_ctx)
+{
+    struct irc_log_ctx *ctx = _ctx;
+
+    log_debug("destroying the irc_log_ctx");
+
+    // destroy the evsql instance
+    evsql_destroy(ctx->db);
+
+    // ...no more
+    free(ctx);
+}
+
+/**
  * The module function table
  */
 struct module_desc irc_log_module = {
     .init               = &irc_log_init,
     .config_options     = irc_log_config_options,
     .unload             = &irc_log_unload,
+    .destroy            = &irc_log_destroy
 };
 
--- a/src/module.c	Wed Apr 01 19:31:11 2009 +0300
+++ b/src/module.c	Thu Apr 02 02:25:35 2009 +0300
@@ -38,6 +38,68 @@
     return old_path;
 }
 
+/**
+ * module_get without the refcount stuff
+ */
+static struct module* module_lookup (struct modules *modules, const char *name)
+{
+    struct module *module = NULL;
+
+    // look for it...
+    TAILQ_FOREACH(module, &modules->list, modules_list) {
+        if (strcasecmp(module->info.name, name) == 0) {
+            // found it
+            return module;
+        }
+    }
+
+    // no such module
+    return NULL;
+}
+
+struct module* module_get (struct modules *modules, const char *name)
+{
+    struct module *module;
+    
+    // get it
+    if (!(module = module_lookup(modules, name)))
+        return NULL;
+
+    // up the refcount
+    module->refcount++;
+    
+    // ok
+    return module;
+}
+
+err_t modules_unload (struct modules *modules)
+{
+    struct module *module;
+    err_t err;
+
+    // unload each module in turn
+    while ((module = TAILQ_FIRST(&modules->list))) {
+        if ((err = module_unload(module)))
+            log_warn("module_unload(%s) failed: %s", module->info.name, error_name(err));
+    }
+
+    // ok
+    return SUCCESS;
+}
+
+void modules_destroy (struct modules *modules)
+{
+    struct module *module;
+
+    // destroy each module
+    while ((module = TAILQ_FIRST(&modules->list)))
+        module_destroy(module);
+
+    // ourselves
+    free(modules);
+}
+
+
 const char* module_name (struct module *module)
 {
     return module->info.name;
@@ -131,16 +193,17 @@
         return SET_ERROR(err, ERR_MODULE_NAME);
     
     // already open with the same name?
-    if (module_get(modules, _info->name))
+    if (module_lookup(modules, _info->name))
         return SET_ERROR(err, ERR_MODULE_DUP);
 
     // alloc
     if ((module = calloc(1, sizeof(*module))) == NULL)
         return SET_ERROR(err, ERR_CALLOC);
     
-    // store
+    // initialize
     module->info = *_info;
     module->modules = modules;
+    module->refcount = 1;
  
     // add to modules list
     TAILQ_INSERT_TAIL(&modules->list, module, modules_list);
@@ -182,28 +245,27 @@
 
 error:
     // cleanup
+    module->refcount = 0;
     module_destroy(module);
 
     return ERROR_CODE(err);    
 }
 
-struct module* module_get (struct modules *modules, const char *name)
+void module_put (struct module *module)
 {
-    struct module *module = NULL;
-
-    // look for it...
-    TAILQ_FOREACH(module, &modules->list, modules_list) {
-        if (strcasecmp(module->info.name, name) == 0)
-            // found it
-            return module;
-    }
-
-    // no such module
-    return NULL;
+    assert(module->refcount > 0);
+    
+    // decrement, just return if still alive
+    if (--module->refcount > 0)
+        return;
+    
+    // refcount zero, destroy
+    module_destroy(module);
 }
 
 err_t module_conf_raw (struct module *module, const char *name, char *value, struct error_info *err)
 {
+    // sanity-check
     if (!module->desc->config_options)
         RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "module does not have any config options");
 
@@ -217,6 +279,7 @@
 
 const struct config_option* module_conf_lookup (struct module *module, const char *name, struct error_info *err)
 {
+    // sanity-check
     if (!module->desc->config_options)
         JUMP_SET_ERROR_STR(err, ERR_MODULE_CONF, "module does not have any config options");
     
@@ -240,23 +303,69 @@
 err_t module_unload (struct module *module)
 {
     err_t err;
+    
+    // sanity-check
+    assert(!module->unloading);
 
-    // invoke the unload func
-    if ((err = module->desc->unload(module->ctx)))
+    // remove from modules list
+    TAILQ_REMOVE(&module->modules->list, module, modules_list);
+    
+    // update status
+    module->unloading = true;
+
+    // invoke the unload func, passing it our reference
+    // note that the module might not exist anymore after calling this
+    if ((err = module->desc->unload(module->ctx, module)))
         return err;
-        
-    // update status
-    // XXX: before or after?
-    module->unloading = true;
 
     // ok
     return SUCCESS;
 }
 
+/**
+ * A wrapper for module_destroy suitable for use as a libevent callback
+ */
+static void _module_destroy (int fd, short what, void *arg)
+{
+    struct module *module = arg;
+
+    (void) fd;
+    (void) what;
+    
+    // execute
+    module_destroy(module);
+}
+
+void module_unloaded (struct module *module)
+{
+    struct timeval tv = { 0, 0 };
+    assert(module->refcount > 0);
+    
+    // decrement, just return if still alive
+    if (--module->refcount > 0)
+        return;
+    
+    // schedule a deferred module_destroy
+    if (event_base_once(module->modules->nexus->ev_base, -1, EV_TIMEOUT, &_module_destroy, module, &tv))
+        // XXX: how to reach?
+        log_fatal("event_base_once failed, unable to schedule deferred module_destroy");
+}
+
 void module_destroy (struct module *module)
 {
-    // remove from modules list
-    TAILQ_REMOVE(&module->modules->list, module, modules_list);
+    // XXX: warn about destroy with significant refcount...
+    if (module->refcount)
+        log_warn("destroying module %s with refcount>0: %zu", module_name(module), module->refcount);
+    else
+        log_debug("destroying module %s", module_name(module));
+
+    // still need to remove from modules_list?
+    if (!module->unloading)
+        TAILQ_REMOVE(&module->modules->list, module, modules_list);
+
+    // run the destroy hook
+    if (module->desc->destroy)
+        module->desc->destroy(module->ctx);
 
     // unload the dl handle
     if (module->handle && dlclose(module->handle))
@@ -269,30 +378,4 @@
     free(module);
 }
 
-err_t modules_unload (struct modules *modules)
-{
-    struct module *module;
-    err_t err;
-
-    // unload each module in turn
-    TAILQ_FOREACH(module, &modules->list, modules_list) {
-        if ((err = module_unload(module)))
-            log_warn("module_unload(%s) failed: %s", module->info.name, error_name(err));
-    }
 
-    // ok
-    return SUCCESS;
-}
-
-void modules_destroy (struct modules *modules)
-{
-    struct module *module;
-
-    // destroy each module
-    while ((module = TAILQ_FIRST(&modules->list)))
-        module_destroy(module);
-
-    // ourselves
-    free(modules);
-}
-
--- a/src/module.h	Wed Apr 01 19:31:11 2009 +0300
+++ b/src/module.h	Thu Apr 02 02:25:35 2009 +0300
@@ -9,7 +9,27 @@
  * The modules are loaded using dlopen(), and hence should be standard dynamic libraries. Modules are then "loaded" by
  * resolving a `struct module_funcs` symbol called "<modname>_funcs", and then using the init func to construct a
  * "context", which is then further manipulated by various other module functions.
+ *
+ * Modules are also reference counted, mainly for implementing module_unload(). When a module is first loaded, it has
+ * a reference count of one - the entry in struct modules. Later, modules can be referenced using module_get(), and
+ * returned with module_put() once done. The module should never be destroyed during its lifetime, until a
+ * module_unload() occurs. At this point, the module is removed from the modules_list, and the one reference is
+ * 'passed on' to the module itself. Once it has finished unloading, it can call module_unloaded() on the reference it was
+ * given by module_desc::unload, which may then result in a deferred module_destroy().
+ *
+ * Module destroying itself is an interesting issue, since modules effectively need to be able to destroy themselves
+ * (as they must be able to perform cleanup, and then notify completion from inside an event loop callback). This means
+ * that they cannot directly execute a module_destroy() on themselves - if we call dlclose() with dlopen-mapped code
+ * pages on the stack, a segfault ensues. Hence, they must call module_unloaded() on themselves, which then executes a
+ * deferred module_destroy() if there are no references left. Otherwise, the module should be safe from external code,
+ * as module_put() should never cause a module to be destroyed before module_unloaded() is executed, due to the primary
+ * reference.
+ *
+ * Ugh, it's compliated, I need to write a clearer explenation once it's implemented :)
  */
+
+struct module;
+
 #include "nexus.h"
 #include "config.h"
 #include "error.h"
@@ -37,6 +57,8 @@
      * Initialize the module, returning an opaque context pointer that is stored in the module state, and supplied for
      * subsequent calls. The supplied nexus arg can be used to access the global state.
      *
+     * Implementing this is mandatory.
+     *
      * @param nexus a pointer to the nexus struct containing the global state
      * @param ctx_ptr the context pointer should be returned via this
      * @param err returned error info
@@ -48,6 +70,8 @@
      * array of config_option's.
      *
      * The handler functions will recieve the module's context pointer as their ctx argument.
+     *
+     * Implementing this is optional, but recommended.
      */
     const struct config_option *config_options;
 
@@ -57,9 +81,26 @@
      * cleanly. But the unloading should complete reasonably quickly, after which all event handlers added by the
      * module to the nexus' ev_base should have been removed, and resources released.
      *
+     * The module given as an argument is the module itself - which has been removed from the modules_list - the
+     * primary reference is passed on to the module. Once the module has finished unloading, it may call module_put(),
+     * which may then call module_destroy(), if no other references were left.
+     *
+     * Implementing this is mandatory.
+     *
      * @param ctx the module's context pointer as returned by init
      */
-    err_t (*unload) (void *ctx);
+    err_t (*unload) (void *ctx, struct module *module);
+
+    /**
+     * Destroy the module now. No later chances, the module's code will be unloaded directly after this, which means
+     * that attempts to execute the module's code (even on the stack...) after this will segfault.
+     *
+     * The module code /should/ garuntee that this is never called from *inside* the module code - calls to
+     * module_destroy() will be deferred via the event loop if needed.
+     *
+     * Implementing this is optional.
+     */
+    void (*destroy) (void *ctx);
 };
 
 /**
@@ -84,9 +125,10 @@
     /** Reference back to modules struct used to load this module */
     struct modules *modules;
 
-    /**
-     * Is the module currently being unloaded?
-     */
+    /** Reference count for destroy() */
+    size_t refcount;
+    
+    /** Is the module currently being unloaded? */
     bool unloading;
 
     /** Our entry in the list of modules */
@@ -157,6 +199,30 @@
 const char* modules_path (struct modules *modules, const char *path);
 
 /**
+ * Get a reference to the module, which must be returned using module_put
+ *
+ * @param modules the modules state
+ * @param name the module name to get
+ * @return the module struct, or NULL if not found
+ */
+struct module* module_get (struct modules *modules, const char *name);
+
+/**
+ * Unload all modules, this just calls module_unload for each module, logging errors as warnings.
+ */
+err_t modules_unload (struct modules *modules);
+
+/**
+ * Destroy all modules immediately.
+ */
+void modules_destroy (struct modules *modules);
+
+
+
+/*******************************************/
+
+
+/**
  * Return a module's name
  */
 const char* module_name (struct module *module);
@@ -174,13 +240,9 @@
 err_t module_load (struct modules *modules, struct module **module_ptr, const struct module_info *info, struct error_info *err);
 
 /**
- * Lookup a module by name
- *
- * @param modules the modules state
- * @param name the module name to get
- * @return the module struct, or NULL if not found
+ * Return a module retrieved using module_get
  */
-struct module* module_get (struct modules *modules, const char *name);
+void module_put (struct module *module);
 
 /**
  * Look up a module configuration option by name
@@ -198,30 +260,26 @@
 err_t module_conf_raw (struct module *module, const char *name, char *value, struct error_info *err);
 
 /**
- * Unload a module. This means calling its 'unload' func, which will then go about shutting down the module. This might
- * not happen right away, though, so the module is not destroyed yet.
- *
- * XXX: currently the module is never destroyed, there needs to be a "module unload done" callback...
+ * Unload a module. This removes the module from the modules_list, marks it as unloading, and then calls the module's
+ * \p unload function, passing it the primary reference. The module's unload code will then go about shutting down the
+ * module, and once that is done, it may module_put() the primary reference, which may then lead to module_destroy().
  */
 err_t module_unload (struct module *module);
 
 /**
- * Destroy a module, releasing as many resources as possible
+ * Used by a module itself to indicate that an module_desc::unload() operation has completed. This will execute a
+ * deferred module_destroy() if there are no more references left on the module.
+ *
+ * Note: this is not intended to be called from outside the given module itself...
+ */
+void module_unloaded (struct module *module);
+
+/**
+ * Destroy a module, releasing as many resources as possible, but not stopping for errors.
+ *
+ * This does not enforce the correct refcount - 'tis the caller's responsibility. Prints out a warning if
+ * refcount > 0.
  */
 void module_destroy (struct module *module);
 
-/**
- * Unload all modules, this just calls module_unload for each module, logging errors as warnings.
- *
- * XXX: currently, this does not destroy any modules, or the modules state itself.
- */
-err_t modules_unload (struct modules *modules);
-
-/**
- * Destroy all modules immediately.
- *
- * XXX: this doesn't leaves hanging module resources. The program will probably crash in soon
- */
-void modules_destroy (struct modules *modules);
-
 #endif
--- a/src/nexus.c	Wed Apr 01 19:31:11 2009 +0300
+++ b/src/nexus.c	Thu Apr 02 02:25:35 2009 +0300
@@ -441,8 +441,13 @@
     // remove the signal handlers
     if (nexus->signals)
         signals_free(nexus->signals);
+    
+    // finally, the libevent base
+    if (nexus->ev_base)
+        event_base_free(nexus->ev_base);
 
-    // ok, nexus is now dead
+    // ok, nexus is now dead, so drop all pointers to make valgrind show things as leaked :)
+    memset(nexus, 0, sizeof(*nexus));
 }
 
 static void on_sigint (evutil_socket_t sig, short what, void *arg)
--- a/test/valgrind-suppressions	Wed Apr 01 19:31:11 2009 +0300
+++ b/test/valgrind-suppressions	Thu Apr 02 02:25:35 2009 +0300
@@ -125,3 +125,16 @@
     fun:module_load
 }
 
+# this is an allocate-global-state-on-the-heap "bug", see:
+#   http://sourceware.org/bugzilla/show_bug.cgi?id=2314
+{
+    getpwuid_r-1
+    Memcheck:Leak
+    fun:malloc
+    obj:/lib/libc-2.7.so
+    fun:__nss_database_lookup
+    obj:*
+    obj:*
+    fun:getpwuid_r
+}
+