src/spbot/nexus.c
author Tero Marttila <terom@fixme.fi>
Thu, 28 May 2009 00:35:02 +0300
branchnew-lib-errors
changeset 218 5229a5d098b2
parent 217 7728d6ec3abf
child 219 cefec18b8268
permissions -rw-r--r--
some of spbot and lib compiles
#include "nexus.h"
#include "lua_config.h"
#include "sock.h"
#include <lib/log.h>

#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <getopt.h>
#include <signal.h>
#include <string.h>

/**
 * Command-line option codes
 */
enum option_code {
    OPT_HELP            = 'h',
    OPT_DEFAULTS        = 'D',
    OPT_NETWORK         = 'n',
    OPT_MODULE          = 'm',
    OPT_CONF            = 'C',
    OPT_DEBUG           = 'd',
    OPT_CONFIG          = 'c',
    
    /** Options without short names */
    _OPT_EXT_BEGIN      = 0x00ff,
    
    OPT_CONSOLE,
    OPT_CHANNEL,
};

/**
 * Command-line option definitions
 */
static struct option options[] = {
    {"help",            0,  NULL,   OPT_HELP        },
    {"defaults",        1,  NULL,   OPT_DEFAULTS    },
    {"network",         1,  NULL,   OPT_NETWORK     },
    {"channel",         1,  NULL,   OPT_CHANNEL     },
    {"module",          1,  NULL,   OPT_MODULE      },
    {"conf",            1,  NULL,   OPT_CONF        },
    {"debug",           0,  NULL,   OPT_DEBUG       },
    {"console",         0,  NULL,   OPT_CONSOLE     },
    {"config",          1,  NULL,   OPT_CONFIG      },
    {0,                 0,  0,      0               },
};

/**
 * Short command-line option defintion
 */
const char *short_options = "hn:c:m:C:d";

/**
 * Display --help output on stdout.
 *
 * If nexus is given, --config options for loaded modules are also listed
 */
static void usage (struct nexus *nexus, const char *exe)
{
    printf("Usage: %s [OPTIONS]\n", exe);
    printf("\n");
    printf(" --help / -h            display this message\n");
    printf(" --defaults / -D        set the IRC client default info using '<nickname>:<username>:<realname>'\n");
    printf(" --network / -n         add an IRC network using '<name>:<hostname>[:<port>[:ssl][:ssl_cafile=<path>][:ssl_verify][:ssl_cert=<path>][:ssl_pkey=<path>]]' format\n");
    printf(" --channel              add an IRC channel using '<network>:<channel>' format\n");
    printf(" --module / -m          add a module using '<name>:<path>' format\n");
    printf(" --config / -C          add a module configuration option using '<mod_name>:<name>[:<value>]' format\n");
    printf(" --debug / -d           set logging level to DEBUG\n");
    printf(" --config / -c          load a Lua config file from '<path>'\n");
    printf(" --console              use the interactive console\n");
    
    // dump loaded module configs
    if (nexus && !TAILQ_EMPTY(&nexus->modules->list)) {
        struct module *module;

        printf("\n");
        printf("Module configuration options\n");

        TAILQ_FOREACH(module, &nexus->modules->list, modules_list) {
            printf("\n");
            printf("%s:\n", module->info.name);

            // XXX: shouldn't be accessing directly
            if (module->desc->config_options) {
                const struct config_option *opt;

                for (opt = module->desc->config_options; opt->name; opt++) {
                    printf(" --config %s:%s:%s\t\t%s\n", module->info.name, opt->name, "xxx", opt->help);
                }

            } else {
                printf("\t???\n");
            }
        }
    }
}

/**
 * Parse and apply a --defaults option, setting the resulting defaults to our irc_client
 */
static err_t apply_defaults (struct nexus *nexus, char *opt, error_t *err)
{
    struct irc_client_defaults defaults = {
        .register_info      = { NULL, NULL, NULL },
        .service            = IRC_PORT,
        .service_ssl        = IRC_SSL_PORT,
    };

    // parse the required fields
    if ((defaults.register_info.nickname = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <nickname> field for --defaults");
    
    if ((defaults.register_info.username = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <username> field for --defaults");
    
    if ((defaults.register_info.realname = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <realname> field for --defaults");

    // trailing garbage?
    if (opt)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "trailing values for --channel");
    
    // apply them
    log_info("using default nick/user/real-name: %s/%s/%s", 
            defaults.register_info.nickname, defaults.register_info.username, defaults.register_info.realname);

    irc_client_set_defaults(nexus->client, &defaults);

    // ok
    return SUCCESS;
}

/**
 * Parse and apply a --network option, adding the given network to our irc_client.
 */
static err_t apply_network (struct nexus *nexus, char *opt, error_t *err)
{
    struct irc_net_info info;
    const char *ssl_cafile = NULL, *ssl_cert = NULL, *ssl_pkey = NULL;
    bool use_ssl = false, ssl_verify = false;

    // init to zero
    memset(&info, 0, sizeof(info));

    // parse the required fields
    if ((info.network = strsep(&opt, ":")) == NULL)
        JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <name> field for --network");
    
    if ((info.hostname = strsep(&opt, ":")) == NULL)
        JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <hostname> field for --network");
    
    // parse the optional fields
    if (opt)
        info.service = strsep(&opt, ":");
    
    // parse any remaining flags
    while (opt) {
        char *flag_token = strsep(&opt, ":");
        char *flag = strsep(&flag_token, "=");
        char *value = flag_token;

        if (strcmp(flag, "ssl") == 0) {
            use_ssl = true;

        } else if (strcmp(flag, "ssl_cafile") == 0) {
            if (!value)
                JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing value for :ssl_cafile");

            ssl_cafile = value;
        
        } else if (strcmp(flag, "ssl_verify") == 0) {
            ssl_verify = true;

        } else if (strcmp(flag, "ssl_cert") == 0) {
            if (!value)
                JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing value for :ssl_cert");
            
            ssl_cert = value;

        } else if (strcmp(flag, "ssl_pkey") == 0) {
            if (!value)
                JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing value for :ssl_pkey");

            ssl_pkey = value;

        } else
            JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "unrecognized flag for --network");
    }

    // SSL?
    if (use_ssl || ssl_cafile || ssl_verify || ssl_cert || ssl_pkey) {
        // verify
        if ((ssl_cert || ssl_pkey) && !(ssl_cert && ssl_pkey))
            JUMP_SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "must give both :ssl_cert/:ssl_pkey");
        
        // create
        if (ssl_client_cred_create(&info.ssl_cred, ssl_cafile, ssl_verify, ssl_cert, ssl_pkey, err))
            goto error;
    }

    // create the net
    log_info("add network '%s' at '%s:%s'", info.network, info.hostname, info.service);

    if (irc_client_add_net(nexus->client, NULL, &info, err))
        goto error;

    // ok
    return SUCCESS;

error:
    // cleanup
    if (info.ssl_cred)
        ssl_client_cred_put(info.ssl_cred);

    return ERROR_CODE(err);
}

/**
 * Parse and apply a --channel option, adding the given channel to the given irc_net.
 */
static err_t apply_channel (struct nexus *nexus, char *opt, error_t *err)
{
    const char *network = NULL;
    struct irc_net *net;
    struct irc_chan_info info = {
        .channel            = NULL,
    };

    // parse the required fields
    if ((network = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <network> field for --channel");
    
    if ((info.channel = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <channel> field for --channel");

    // trailing garbage?
    if (opt)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "trailing values for --channel");

    // look up the net
    if ((net = irc_client_get_net(nexus->client, network)) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "unknown network for --channel");
    
    // add the channel
    log_info("add channel '%s' on network '%s'", info.channel, net->info.network);

    if (irc_net_add_chan(net, NULL, &info, err))
        return ERROR_CODE(err);

    // ok
    return SUCCESS;
}

/**
 * Parse and apply a --module option, loading the given module.
 */
static err_t apply_module (struct nexus *nexus, char *opt, error_t *err)
{
    struct module_info info = {
        .name       = NULL,
        .path       = NULL,
    };

    // parse the required fields
    if ((info.name = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <name> field for --module");

    if ((info.path = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <path> field for --module");

    // trailing garbage?
    if (opt)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "trailing values for --channel");
    
    // load it
    log_info("loading module '%s' from path '%s'", info.name, info.path);

    if (module_load(nexus->modules, NULL, &info, err))
        return ERROR_CODE(err);

    // ok
    return SUCCESS;
}

/**
 * Parse and apply a --conf option, calling the module's conf func.
 */
static err_t apply_conf (struct nexus *nexus, char *opt, error_t *err)
{
    struct module *module;
    const char *module_name, *conf_name;
    char *conf_value;

    // parse the required fields
    if ((module_name = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <module> field for --config");

    if ((conf_name = strsep(&opt, ":")) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "missing <name> field for --config");

    // value is the rest of the data, might be NULL
    conf_value = opt;

    // lookup the module
    if ((module = modules_get(nexus->modules, module_name)) == NULL)
        return SET_ERROR_STR(err, &general_errors, ERR_CMD_OPT, "unknown module for --config");
    
    // do the config
    log_info("applying module '%s' config name '%s' with value: %s", module->info.name, conf_name, conf_value);

    if (module_conf_raw(module, conf_name, conf_value, err))
        return ERROR_CODE(err);

    // ok
    return SUCCESS;
}

/**
 * Open the console
 */
static err_t apply_console (struct nexus *nexus, error_t *err)
{
    struct console_config config = {
        .prompt     = "> ",
    };
    struct console *console;

    log_info("initializing the console");
    
    // init the console
    if (console_init(&console, nexus->ev_base, &config, NULL, NULL, err))
        return ERROR_CODE(err);
    
    // set it as the log output handler
    console_set_log_output(console);

    // create the lua console on top of that
    if (lua_console_create(&nexus->lua_console, console, nexus->lua, err))
        goto error;

    // ok
    return SUCCESS;

error:
    console_destroy(console);

    return ERROR_CODE(err);
}

err_t nexus_load_config (struct nexus *nexus, const char *path, error_t *err)
{
    return lua_config_load(nexus->lua, path, err);
}

/**
 * Load the lua config file from \a path
 */
static err_t apply_config (struct nexus *nexus, char *path, error_t *err)
{
    log_info("loading lua config from %s", path);
    
    return nexus_load_config(nexus, path, err);
}

/**
 * Parse arguments and apply them to the given nexus
 */
static err_t parse_args (struct nexus *nexus, int argc, char **argv, error_t *err)
{
    int opt, option_index;
    
    // parse options
    while ((opt = getopt_long(argc, argv, short_options, options, &option_index)) != -1) {
        switch (opt) {
            case OPT_HELP:
                usage(nexus, argv[0]);

                // XXX: return instead
                exit(EXIT_SUCCESS);
            
            // XXX: come up with something nicer for parsing these command-line options            
            case OPT_NETWORK:
                if (apply_network(nexus, optarg, err))
                    goto error;

                break;
            
            case OPT_DEFAULTS:
                if (apply_defaults(nexus, optarg, err))
                    goto error;

                break;

            case OPT_CHANNEL:
                if (apply_channel(nexus, optarg, err))
                    goto error;

                break;
            
            case OPT_MODULE:
                if (apply_module(nexus, optarg, err))
                    goto error;

                break;

            case OPT_CONF:
                if (apply_conf(nexus, optarg, err))
                    goto error;

                break;
            
            case OPT_DEBUG:
                set_log_level(LOG_DEBUG);
                break;
            
            case OPT_CONSOLE:
                if (apply_console(nexus, err))
                    goto error;
                
                break;
            
            case OPT_CONFIG:
                if (apply_config(nexus, optarg, err))
                    goto error;

                break;

            case '?':
                usage(nexus, argv[0]);
                return SET_ERROR(err, &general_errors, ERR_CMD_OPT);
        }
    }
    
    // ok
    return SUCCESS;

error:
    return ERROR_CODE(err);    
}

void nexus_shutdown (struct nexus *nexus)
{
    // destroy the console
    if (nexus->lua_console)
        lua_console_destroy(nexus->lua_console);
    
    nexus->lua_console = NULL;

    // unload the modules
    if (nexus->modules)
       modules_unload(nexus->modules);

    // quit the irc client
    if (nexus->client)
       irc_client_quit(nexus->client, "Goodbye, cruel world ;(");

    // remove the signal handlers
    if (nexus->signals)
        signals_free(nexus->signals);

    nexus->signals = NULL;

    // now event_base_dispatch should return once everythings' shut down...
    nexus->shutdown = true;
}

void nexus_crash (struct nexus *nexus)
{
    // force-quit the event loop
    event_base_loopbreak(nexus->ev_base);
}

void nexus_destroy (struct nexus *nexus)
{
    // destroy the console
    if (nexus->lua_console)
        lua_console_destroy(nexus->lua_console);

    // destroy the lua state
    if (nexus->lua)
        nexus_lua_destroy(nexus->lua);

    // destroy the modules
    if (nexus->modules)
        modules_destroy(nexus->modules);

    // destroy the irc client
    if (nexus->client)
        irc_client_destroy(nexus->client);

    // 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, so drop all pointers to make valgrind show things as leaked :)
    memset(nexus, 0, sizeof(*nexus));
}

static void on_sig_shutdown (evutil_socket_t sig, short what, void *arg)
{
    struct nexus *nexus = arg;

    (void) sig;
    (void) what;
    
    if (!nexus->shutdown) {
        // shutdown
        log_info("Terminating...");

        nexus_shutdown(nexus);

    } else {
        // already tried to shutdown
        log_info("Crashing!");

        nexus_crash(nexus);
    }
}

int main (int argc, char **argv) 
{
    struct nexus _nexus, *nexus = &_nexus;
    error_t err;

    // zero nexus
    memset(nexus, 0, sizeof(*nexus));

    // initialize libevent
    if ((nexus->ev_base = event_base_new()) == NULL)
        FATAL("event_base_new");
    

    // initialize signal handlers
    if (signals_create(&nexus->signals, nexus->ev_base, &err))
        FATAL("signals_create");
 
    // add our signal handlers
    if (signal_ignore(SIGPIPE, &err))
        FATAL_ERROR(&err, "signals_ignore");
    
    // add our SIGTERM handler before console_init()?
    if (
            (ERROR_CODE(&err) = signals_add(nexus->signals, SIGTERM, on_sig_shutdown, nexus))
        ||  (ERROR_CODE(&err) = signals_add(nexus->signals, SIGINT, on_sig_shutdown, nexus))
    )
        FATAL_ERROR(&err, "signals_add");

    // initialize sock module
    if (sock_init(nexus->ev_base, &err))
        FATAL_ERROR(&err, "sock_init");

    // modules 
    if (modules_create(&nexus->modules, nexus, &err))
        FATAL_ERROR(&err, "modules_create");
    
    // the IRC client
    if (irc_client_create(&nexus->client, &err))
        FATAL_ERROR(&err, "irc_client_create");
    
    // lua state
    if (nexus_lua_create(&nexus->lua, nexus, &err))
        FATAL_ERROR(&err, "nexus_lua_create");

    
    // parse args
    if (parse_args(nexus, argc, argv, &err))
        FATAL_ERROR(&err, "parse_args");

  
    // run event loop
    log_info("entering event loop");

    if (event_base_dispatch(nexus->ev_base)) {
        if (nexus->shutdown) {
            log_info("clean shutdown completed");

        } else {
            FATAL("event_base_dispatch returned without shutdown");

        }
    } else {
        log_warn("zero return from event_base_dispatch");
    }

    // cleanup
    nexus_destroy(nexus);
    nexus = NULL;

    // ok
    return EXIT_SUCCESS;
}