fix operation of module_unload/module_destroy so that unloaded modules are now destroyed once they have been unloaded
#include "module.h"
#include "irc_chan.h"
#include "config.h"
#include "error.h"
#include "log.h"
#include <stdlib.h>
#include <string.h>
#include <event2/event.h>
#include <evsql.h>
/**
* The irc_log module state.
*/
struct irc_log_ctx {
/** The nexus this module is loaded for */
struct nexus *nexus;
/** The database connection */
struct evsql *db;
/**
* Our logged channels
*/
TAILQ_HEAD(irc_log_ctx_channels, irc_log_chan) channels;
/** Are we currently unloading ourself (via irc_log_unload) ? */
bool unloading;
/** The `struct module` passed to us once we unload() */
struct module *module;
};
/**
* State required to use irc_cmd_handler with irc_chan.handlers.
*/
struct irc_log_chan {
/** The irc_log context */
struct irc_log_ctx *ctx;
/** The target channel */
struct irc_chan *chan;
/** Are we stopping (irc_log_chan_stop called)? */
bool stopping;
/** We are part of the irc_log_ctx.channels list */
TAILQ_ENTRY(irc_log_chan) ctx_channels;
};
/*
* Forward-declare
*/
static void irc_log_chan_destroy (struct irc_log_chan *chan_ctx);
/**
* 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("module unload() completed");
// notify
module_unloaded(ctx->module);
}
/**
* The irc_log_chan has completed shutdown
*/
static void irc_log_chan_stopped (struct irc_log_chan *chan_ctx)
{
struct irc_log_ctx *ctx = chan_ctx->ctx;
log_debug("destroying the irc_log_chan (chan=%s)", irc_chan_name(chan_ctx->chan));
// destroy the channel
irc_log_chan_destroy(chan_ctx);
// was it the final channel for unloading?
if (ctx->unloading && TAILQ_EMPTY(&ctx->channels))
irc_log_stopped(ctx);
}
/**
* Our evsql result handler for irc_log_event INSERTs.
*/
static void irc_log_on_sql_result (struct evsql_result *res, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
err_t err;
// check errors
if ((err = evsql_result_check(res)))
log_error("irc_log_event: %s", evsql_result_error(res));
// ok, don't need the result anymore
evsql_result_free(res);
// if stopping, then handle that
if (chan_ctx->stopping)
irc_log_chan_stopped(chan_ctx);
}
/**
* Log the event into our database.
*
* Any of chan_ctx, source, target, message can be NULL to insert NULLs
*/
static err_t irc_log_event (struct irc_log_ctx *ctx, struct irc_log_chan *chan_ctx, const struct irc_nm *source,
const char *type, const char *target, const char *message)
{
struct evsql_query *query;
// unpack the nick/user/hostname args, as source can be NULL
const char *network = chan_ctx ? chan_ctx->chan->net->info.network : NULL;
const char *channel = chan_ctx ? irc_chan_name(chan_ctx->chan) : NULL;
const char *nickname = source ? source->nickname : NULL;
const char *username = source ? source->username : NULL;
const char *hostname = source ? source->hostname : NULL;
// our SQL query
static struct evsql_query_info sql = {
"INSERT INTO logs ("
" network, channel, nickname, username, hostname, type, target, message"
" ) VALUES ("
" $1::varchar, $2::varchar, $3::varchar, $4::varchar, $5::varchar, $6::varchar, $7::varchar, $8::varchar"
" )", {
EVSQL_TYPE(STRING), // network
EVSQL_TYPE(STRING), // channel
EVSQL_TYPE(STRING), // nickname
EVSQL_TYPE(STRING), // username
EVSQL_TYPE(STRING), // hostname
EVSQL_TYPE(STRING), // type
EVSQL_TYPE(STRING), // target
EVSQL_TYPE(STRING), // message
EVSQL_TYPE_END
}
};
// drop lines if not connected
if (ctx->db == NULL) {
log_warn("no database connected");
return ERR_MODULE_CONF;
}
// run the INSERT
if ((query = evsql_query_exec(ctx->db, NULL, &sql, &irc_log_on_sql_result, chan_ctx,
network, channel, nickname, username, hostname, type, target, message
)) == NULL) {
// XXX: get evsql_query error info somehow...
log_error("evsql_query_exec failed for %s:%s - %s!%s@%s - %s -> %s - %s",
network, channel, nickname, username, hostname, type, target, message
);
return ERR_EVSQL_QUERY_EXEC;
}
// ok
log_debug("%s:%s - %s!%s@%s - %s -> %s - %s",
network, channel, nickname, username, hostname, type, target, message
);
return SUCCESS;
}
/**
* Log a simple channel event of the form:
*
* :nm <type> <channel> [<msg>]
*/
static void irc_log_on_chan_generic (const struct irc_line *line, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
const char *msg = line->args[1];
irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, NULL, msg);
}
/**
* Log a NICK event on a channel
*
* :nm NICK <nickname>
*
* This logs the new nickname as the target
*/
static void irc_log_on_chan_NICK (const struct irc_line *line, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
const char *nickname = line->args[0];
// log it
irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, nickname, NULL);
}
/**
* Log a QUIT event on a channel
*
* :nm QUIT [<message>]
*/
static void irc_log_on_chan_QUIT (const struct irc_line *line, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
const char *message = line->args[0];
// log it
irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, NULL, message);
}
/**
* Log a MODE event on a channel
*
* :nm MODE <channel> [<modearg> [...]]
*
* This conacts all the modeargs together, and logs that as the message
*/
static void irc_log_on_chan_MODE (const struct irc_line *line, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
char message[512], *ptr = message;
const char *cmdarg;
// iterate over each arg
FOREACH_IRC_LINE_ARGS(line, cmdarg) {
// separate with spaces
if (cmdarg > line->args[0])
*ptr++ = ' ';
// append
ptr += snprintf(ptr, (message + 512 - ptr), "%s", cmdarg);
// buffer full?
if (ptr > message + 512)
break;
}
// log
irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, NULL, message);
}
/**
* Log a KICK event on a channel
*
* :nm KICK <channel> <target> [<comment>]
*/
static void irc_log_on_chan_KICK (const struct irc_line *line, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
const char *target = line->args[1];
const char *msg = line->args[2];
irc_log_event(chan_ctx->ctx, chan_ctx, line->source, line->command, target, msg);
}
/**
* Log a CTCP ACTION message on a channel
*
* :nm "CTCP ACTION" <channel> <action>
*/
static void irc_log_on_chan_CTCP_ACTION (const struct irc_line *line, void *arg)
{
struct irc_log_chan *chan_ctx = arg;
const char *action = line->args[1];
irc_log_event(chan_ctx->ctx, chan_ctx, line->source, "ACTION", NULL, action);
}
/**
* Our low-level channel-specific message handlers for logged channels.
*
* Note that these get a `struct irc_log_chan*` as an argument.
*/
static struct irc_cmd_handler _chan_cmd_handlers[] = {
{ "NICK", &irc_log_on_chan_NICK },
{ "QUIT", &irc_log_on_chan_QUIT },
{ "JOIN", &irc_log_on_chan_generic },
{ "PART", &irc_log_on_chan_generic },
{ "MODE", &irc_log_on_chan_MODE },
{ "TOPIC", &irc_log_on_chan_generic },
{ "KICK", &irc_log_on_chan_KICK },
{ "PRIVMSG", &irc_log_on_chan_generic },
{ "NOTICE", &irc_log_on_chan_generic },
{ "CTCP ACTION", &irc_log_on_chan_CTCP_ACTION },
{ NULL, NULL }
};
/**
* Our high-level channel-specific callbacks for logged channels.
*
* Note that these get a `struct irc_log_chan*` as an argument.
*/
static struct irc_chan_callbacks _chan_callbacks = {
.on_self_join = NULL,
.on_msg = NULL,
};
/**
* Release resources associated with the given irc_log_chan without doing any clean shutdown stuff
*/
static void irc_log_chan_destroy (struct irc_log_chan *chan_ctx)
{
// remove any handlers/callbacks
irc_cmd_remove(&chan_ctx->chan->handlers, _chan_cmd_handlers, chan_ctx);
irc_chan_remove_callbacks(chan_ctx->chan, &_chan_callbacks, chan_ctx);
// remove from channels list
TAILQ_REMOVE(&chan_ctx->ctx->channels, chan_ctx, ctx_channels);
// free ourselves
free(chan_ctx);
}
/**
* Begin logging the given channel
*/
static err_t irc_log_chan (struct irc_log_ctx *ctx, struct irc_chan *chan, struct error_info *err)
{
struct irc_log_chan *chan_ctx;
// alloc
if ((chan_ctx = calloc(1, sizeof(*chan_ctx))) == NULL)
return SET_ERROR(err, ERR_CALLOC);
// store
chan_ctx->ctx = ctx;
chan_ctx->chan = chan;
// add to channels list
TAILQ_INSERT_TAIL(&ctx->channels, chan_ctx, ctx_channels);
// add low-level handlers
if ((ERROR_CODE(err) = irc_cmd_add(&chan_ctx->chan->handlers, _chan_cmd_handlers, chan_ctx)))
goto error;
// add channel callbacks
if ((ERROR_CODE(err) = irc_chan_add_callbacks(chan_ctx->chan, &_chan_callbacks, chan_ctx)))
goto error;
// log an OPEN message
// XXX: move this to when we first JOIN the channel
if ((ERROR_CODE(err) = irc_log_event(ctx, chan_ctx, NULL, "OPEN", NULL, NULL)))
goto error;
// ok
log_info("logging channel %s:%s", chan_ctx->chan->net->info.network, irc_chan_name(chan_ctx->chan));
return SUCCESS;
error:
// cleanup
irc_log_chan_destroy(chan_ctx);
return ERROR_CODE(err);
}
/**
* Stop logging the given channel, shutting it down nicely and then releasing its resources.
*
* If shutting it down nicely fails, this just destroys it.
*/
static err_t irc_log_chan_stop (struct irc_log_chan *chan_ctx)
{
err_t err;
// mark it as stopping, so the result callback knows to destroy it
chan_ctx->stopping = true;
// log the CLOSE event
if ((err = irc_log_event(chan_ctx->ctx, chan_ctx, NULL, "CLOSE", NULL, NULL)))
goto error;
// ok
return SUCCESS;
error:
// destroy it uncleanly
irc_log_chan_destroy(chan_ctx);
return err;
}
/**
* Allocate and initialize an irc_log_ctx. This doesn't do very much, the real magic happens in irc_log_conf.
*/
static err_t irc_log_init (struct nexus *nexus, void **ctx_ptr, struct error_info *err)
{
struct irc_log_ctx *ctx;
// allocate
if ((ctx = calloc(1, sizeof(*ctx))) == NULL)
return SET_ERROR(err, ERR_CALLOC);
// store
ctx->nexus = nexus;
// initialize
TAILQ_INIT(&ctx->channels);
log_info("module initialized");
// ok
*ctx_ptr = ctx;
return SUCCESS;
}
/**
* Process the irc_log.db_info config option.
*
* Creates a new evsql handle.
*
* Fails if ctx->db is already set.
*/
static err_t irc_log_conf_db_info (void *_ctx, char *value, struct error_info *err)
{
struct irc_log_ctx *ctx = _ctx;
log_info("connect to database: %s", value);
// already connected?
if (ctx->db)
RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "irc_log.db_info already set");
// create a new evsql handle
if ((ctx->db = evsql_new_pq(ctx->nexus->ev_base, value, NULL, NULL)) == NULL)
return SET_ERROR(err, ERR_EVSQL_NEW_PQ);
// ok
return SUCCESS;
}
/**
* Process the irc_log.channel config option.
*
* Creates a new irc_log_chan context, looks up the channel, adds our command handlers, and logs an OPEN event.
*
* Fails if the value is invalid, we don't have a database connected, the channel doesn't exist, adding our command
* handlers/callbacks fails, or sending the initial INSERT-OPEN query fails.
*/
static err_t irc_log_conf_channel (void *_ctx, struct irc_chan *chan, struct error_info *err)
{
struct irc_log_ctx *ctx = _ctx;
// have a db configured?
if (!ctx->db)
RETURN_SET_ERROR_STR(err, ERR_MODULE_CONF, "irc_log.channel used without any irc_log.db_info");
// begin logging it
if (irc_log_chan(ctx, chan, err))
return ERROR_CODE(err);
// ok
return SUCCESS;
}
/**
* Our configuration options
*/
struct config_option irc_log_config_options[] = {
CONFIG_OPT_STRING( "db_info", &irc_log_conf_db_info, "[<key>=<value> [...]]", "set database connection info, see libpq docs" ),
CONFIG_OPT_IRC_CHAN( "channel", &irc_log_conf_channel, "<channel>", "log the given channel" ),
CONFIG_OPT_END
};
/**
* Deinitialize, logging CLOSE events for all channels, and removing any hooks we've added
*/
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) {
irc_log_chan_stop(chan_ctx);
}
// wait for all the channels to be stopped, which will call irc_log_stopped
return SUCCESS;
}
/**
* 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
};