diff options
Diffstat (limited to 'zyklonb.c')
-rw-r--r-- | zyklonb.c | 1769 |
1 files changed, 1769 insertions, 0 deletions
diff --git a/zyklonb.c b/zyklonb.c new file mode 100644 index 0000000..c99b141 --- /dev/null +++ b/zyklonb.c @@ -0,0 +1,1769 @@ +/* + * zyklonb.c: the experimental IRC bot + * + * Copyright (c) 2014, Přemysl Janouch <p.janouch@gmail.com> + * All rights reserved. + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY + * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION + * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN + * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + */ + +#define PROGRAM_NAME "ZyklonB" +#define PROGRAM_VERSION "alpha" + +#include "common.c" + +// --- Configuration (application-specific) ------------------------------------ + +static struct config_item g_config_table[] = +{ + { "nickname", "ZyklonB", "IRC nickname" }, + { "username", "bot", "IRC user name" }, + { "realname", "ZyklonB IRC bot", "IRC real name/e-mail" }, + + { "irc_host", NULL, "Address of the IRC server" }, + { "irc_port", "6667", "Port of the IRC server" }, + { "ssl_use", "off", "Whether to use SSL" }, + { "ssl_cert", NULL, "Client SSL certificate (PEM)" }, + { "autojoin", NULL, "Channels to join on start" }, + { "reconnect", "on", "Whether to reconnect on error" }, + { "reconnect_delay", "5", "Time between reconnecting" }, + + { "prefix", ":", "The prefix for bot commands" }, + { "admin", NULL, "Host mask for administrators" }, + { "plugins", NULL, "The plugins to load on startup" }, + { "plugin_dir", NULL, "Where to search for plugins" }, + { "recover", "on", "Whether to re-launch on crash" }, + + { NULL, NULL, NULL } +}; + +// --- Application data -------------------------------------------------------- + +struct plugin_data +{ + LIST_HEADER (plugin_data) + struct bot_context *ctx; ///< Parent context + + char *name; ///< Plugin identifier + pid_t pid; ///< PID of the plugin process + + bool is_zombie; ///< Whether the child is a zombie + bool initialized; ///< Ready to exchange IRC messages + struct str queued_output; ///< Output queued up until initialized + + // Since we're doing non-blocking I/O, we need to queue up data so that + // we don't stall on plugins unnecessarily. + + int read_fd; ///< The read end of the comm. pipe + int write_fd; ///< The write end of the comm. pipe + + struct str read_buffer; ///< Unprocessed input + struct str write_buffer; ///< Output yet to be sent out +}; + +static void +plugin_data_init (struct plugin_data *self) +{ + memset (self, 0, sizeof *self); + + self->pid = -1; + str_init (&self->queued_output); + + self->read_fd = -1; + str_init (&self->read_buffer); + self->write_fd = -1; + str_init (&self->write_buffer); +} + +static void +plugin_data_free (struct plugin_data *self) +{ + soft_assert (self->pid == -1); + free (self->name); + + str_free (&self->read_buffer); + if (!soft_assert (self->read_fd == -1)) + xclose (self->read_fd); + + str_free (&self->write_buffer); + if (!soft_assert (self->write_fd == -1)) + xclose (self->write_fd); + + if (!self->initialized) + str_free (&self->queued_output); +} + +struct bot_context +{ + struct str_map config; ///< User configuration + regex_t *admin_re; ///< Regex to match our administrator + + int irc_fd; ///< Socket FD of the server + struct str read_buffer; ///< Input yet to be processed + bool irc_ready; ///< Whether we may send messages now + + SSL_CTX *ssl_ctx; ///< SSL context + SSL *ssl; ///< SSL connection + + struct plugin_data *plugins; ///< Linked list of plugins + struct str_map plugins_by_name; ///< Indexes @em plugins by their name + + struct poller poller; ///< Manages polled descriptors + bool quitting; ///< User requested quitting + bool polling; ///< The event loop is running +}; + +static void +bot_context_init (struct bot_context *self) +{ + str_map_init (&self->config); + self->config.free = free; + load_config_defaults (&self->config, g_config_table); + self->admin_re = NULL; + + self->irc_fd = -1; + str_init (&self->read_buffer); + self->irc_ready = false; + + self->ssl = NULL; + self->ssl_ctx = NULL; + + self->plugins = NULL; + str_map_init (&self->plugins_by_name); + + poller_init (&self->poller); + self->quitting = false; + self->polling = false; +} + +static void +bot_context_free (struct bot_context *self) +{ + str_map_free (&self->config); + if (self->admin_re) + regex_free (self->admin_re); + str_free (&self->read_buffer); + + // TODO: terminate the plugins properly before this is called + struct plugin_data *link, *tmp; + for (link = self->plugins; link; link = tmp) + { + tmp = link->next; + plugin_data_free (link); + free (link); + } + + if (self->irc_fd != -1) + xclose (self->irc_fd); + if (self->ssl) + SSL_free (self->ssl); + if (self->ssl_ctx) + SSL_CTX_free (self->ssl_ctx); + + str_map_free (&self->plugins_by_name); + poller_free (&self->poller); +} + +static void +irc_shutdown (struct bot_context *ctx) +{ + // Generally non-critical + if (ctx->ssl) + soft_assert (SSL_shutdown (ctx->ssl) != -1); + else + soft_assert (shutdown (ctx->irc_fd, SHUT_WR) == 0); +} + +static void +initiate_quit (struct bot_context *ctx) +{ + irc_shutdown (ctx); + ctx->quitting = true; +} + +static void +try_finish_quit (struct bot_context *ctx) +{ + if (!ctx->quitting) + return; + if (ctx->irc_fd == -1 && !ctx->plugins) + ctx->polling = false; +} + +static bool irc_send (struct bot_context *ctx, + const char *format, ...) ATTRIBUTE_PRINTF (2, 3); + +// XXX: is it okay to just ignore the return value and wait until we receive +// it in on_irc_readable()? +static bool +irc_send (struct bot_context *ctx, const char *format, ...) +{ + va_list ap; + + if (g_debug_mode) + { + fputs ("[IRC] <== \"", stderr); + va_start (ap, format); + vfprintf (stderr, format, ap); + va_end (ap); + fputs ("\"\n", stderr); + } + + if (!soft_assert (ctx->irc_fd != -1)) + return false; + + va_start (ap, format); + struct str str; + str_init (&str); + str_append_vprintf (&str, format, ap); + str_append (&str, "\r\n"); + va_end (ap); + + bool result = true; + if (ctx->ssl) + { + // TODO: call SSL_get_error() to detect if a clean shutdown has occured + if (SSL_write (ctx->ssl, str.str, str.len) != (int) str.len) + { + print_debug ("%s: %s: %s", __func__, "SSL_write", + ERR_error_string (ERR_get_error (), NULL)); + result = false; + } + } + else if (write (ctx->irc_fd, str.str, str.len) != (ssize_t) str.len) + { + print_debug ("%s: %s: %s", __func__, "write", strerror (errno)); + result = false; + } + + str_free (&str); + return result; +} + +static bool +irc_initialize_ssl (struct bot_context *ctx, struct error **e) +{ + ctx->ssl_ctx = SSL_CTX_new (SSLv23_client_method ()); + if (!ctx->ssl_ctx) + goto error_ssl_1; + // We don't care; some encryption is always better than no encryption + SSL_CTX_set_verify (ctx->ssl_ctx, SSL_VERIFY_NONE, NULL); + // XXX: maybe we should call SSL_CTX_set_options() for some workarounds + + ctx->ssl = SSL_new (ctx->ssl_ctx); + if (!ctx->ssl) + goto error_ssl_2; + + const char *ssl_cert = str_map_find (&ctx->config, "ssl_cert"); + if (ssl_cert) + { + char *path = resolve_config_filename (ssl_cert); + if (!path) + print_error ("%s: %s", "cannot open file", ssl_cert); + // XXX: perhaps we should read the file ourselves for better messages + else if (!SSL_use_certificate_file (ctx->ssl, path, SSL_FILETYPE_PEM)) + print_error ("%s: %s", "setting the SSL client certificate failed", + ERR_error_string (ERR_get_error (), NULL)); + free (path); + } + + SSL_set_connect_state (ctx->ssl); + if (!SSL_set_fd (ctx->ssl, ctx->irc_fd)) + goto error_ssl_3; + // Avoid SSL_write() returning SSL_ERROR_WANT_READ + SSL_set_mode (ctx->ssl, SSL_MODE_AUTO_RETRY); + if (SSL_connect (ctx->ssl) > 0) + return true; + +error_ssl_3: + SSL_free (ctx->ssl); + ctx->ssl = NULL; +error_ssl_2: + SSL_CTX_free (ctx->ssl_ctx); + ctx->ssl_ctx = NULL; +error_ssl_1: + // XXX: these error strings are really nasty; also there could be + // multiple errors on the OpenSSL stack. + error_set (e, "%s: %s", "could not initialize SSL", + ERR_error_string (ERR_get_error (), NULL)); + return false; +} + +static bool +irc_establish_connection (struct bot_context *ctx, + const char *host, const char *port, bool use_ssl, struct error **e) +{ + struct addrinfo gai_hints, *gai_result, *gai_iter; + memset (&gai_hints, 0, sizeof gai_hints); + + // We definitely want TCP. + gai_hints.ai_socktype = SOCK_STREAM; + + int err = getaddrinfo (host, port, &gai_hints, &gai_result); + if (err) + { + error_set (e, "%s: %s: %s", + "connection failed", "getaddrinfo", gai_strerror (err)); + return false; + } + + int sockfd; + for (gai_iter = gai_result; gai_iter; gai_iter = gai_iter->ai_next) + { + sockfd = socket (gai_iter->ai_family, + gai_iter->ai_socktype, gai_iter->ai_protocol); + if (sockfd == -1) + continue; + set_cloexec (sockfd); + + int yes = 1; + soft_assert (setsockopt (sockfd, SOL_SOCKET, SO_KEEPALIVE, + &yes, sizeof yes) != -1); + + const char *real_host = host; + + // Let's try to resolve the address back into a real hostname; + // we don't really need this, so we can let it quietly fail + char buf[NI_MAXHOST]; + err = getnameinfo (gai_iter->ai_addr, gai_iter->ai_addrlen, + buf, sizeof buf, NULL, 0, 0); + if (err) + print_debug ("%s: %s", "getnameinfo", gai_strerror (err)); + else + real_host = buf; + + // XXX: we shouldn't mix these statuses with `struct error'; choose 1! + print_status ("connecting to `%s:%s'...", real_host, port); + if (!connect (sockfd, gai_iter->ai_addr, gai_iter->ai_addrlen)) + break; + + xclose (sockfd); + } + + freeaddrinfo (gai_result); + + if (!gai_iter) + { + error_set (e, "connection failed"); + return false; + } + + ctx->irc_fd = sockfd; + if (use_ssl && !irc_initialize_ssl (ctx, e)) + { + xclose (ctx->irc_fd); + ctx->irc_fd = -1; + return false; + } + + print_status ("connection established"); + return true; +} + +// --- Signals ----------------------------------------------------------------- + +static int g_signal_pipe[2]; ///< A pipe used to signal... signals + +static struct str_vector + g_original_argv, ///< Original program arguments + g_recovery_env; ///< Environment for re-exec recovery + +/// Program termination has been requested by a signal +static volatile sig_atomic_t g_termination_requested; + +/// Points to startup reason location within `g_recovery_environment' +static char **g_startup_reason_location; +/// The environment variable used to pass the startup reason when re-executing +static const char g_startup_reason_str[] = "STARTUP_REASON"; + +static void +sigchld_handler (int signum) +{ + (void) signum; + + int original_errno = errno; + // Just so that the read end of the pipe wakes up the poller. + // NOTE: Linux has signalfd() and eventfd(), and the BSD's have kqueue. + // All of them are better than this approach, although platform-specific. + if (write (g_signal_pipe[1], "c", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +sigterm_handler (int signum) +{ + (void) signum; + + g_termination_requested = true; + + int original_errno = errno; + if (write (g_signal_pipe[1], "t", 1) == -1) + soft_assert (errno == EAGAIN); + errno = original_errno; +} + +static void +setup_signal_handlers (void) +{ + if (pipe (g_signal_pipe) == -1) + exit_fatal ("%s: %s", "pipe", strerror (errno)); + + set_cloexec (g_signal_pipe[0]); + set_cloexec (g_signal_pipe[1]); + + // So that the pipe cannot overflow; it would make write() block within + // the signal handler, which is something we really don't want to happen. + // The same holds true for read(). + set_blocking (g_signal_pipe[0], false); + set_blocking (g_signal_pipe[1], false); + + struct sigaction sa; + sa.sa_flags = SA_RESTART; + sa.sa_handler = sigchld_handler; + sigemptyset (&sa.sa_mask); + + if (sigaction (SIGCHLD, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); + + signal (SIGPIPE, SIG_IGN); + + sa.sa_handler = sigterm_handler; + if (sigaction (SIGINT, &sa, NULL) == -1 + || sigaction (SIGTERM, &sa, NULL) == -1) + exit_fatal ("sigaction: %s", strerror (errno)); +} + +static void +translate_signal_info (int no, const char **name, int code, const char **reason) +{ + if (code == SI_USER) *reason = "signal sent by kill()"; + if (code == SI_QUEUE) *reason = "signal sent by sigqueue()"; + + switch (no) + { + case SIGILL: + *name = "SIGILL"; + if (code == ILL_ILLOPC) *reason = "illegal opcode"; + if (code == ILL_ILLOPN) *reason = "illegal operand"; + if (code == ILL_ILLADR) *reason = "illegal addressing mode"; + if (code == ILL_ILLTRP) *reason = "illegal trap"; + if (code == ILL_PRVOPC) *reason = "privileged opcode"; + if (code == ILL_PRVREG) *reason = "privileged register"; + if (code == ILL_COPROC) *reason = "coprocessor error"; + if (code == ILL_BADSTK) *reason = "internal stack error"; + break; + case SIGFPE: + *name = "SIGFPE"; + if (code == FPE_INTDIV) *reason = "integer divide by zero"; + if (code == FPE_INTOVF) *reason = "integer overflow"; + if (code == FPE_FLTDIV) *reason = "floating-point divide by zero"; + if (code == FPE_FLTOVF) *reason = "floating-point overflow"; + if (code == FPE_FLTUND) *reason = "floating-point underflow"; + if (code == FPE_FLTRES) *reason = "floating-point inexact result"; + if (code == FPE_FLTINV) *reason = "invalid floating-point operation"; + if (code == FPE_FLTSUB) *reason = "subscript out of range"; + break; + case SIGSEGV: + *name = "SIGSEGV"; + if (code == SEGV_MAPERR) + *reason = "address not mapped to object"; + if (code == SEGV_ACCERR) + *reason = "invalid permissions for mapped object"; + break; + case SIGBUS: + *name = "SIGBUS"; + if (code == BUS_ADRALN) *reason = "invalid address alignment"; + if (code == BUS_ADRERR) *reason = "nonexistent physical address"; + if (code == BUS_OBJERR) *reason = "object-specific hardware error"; + break; + default: + *name = NULL; + } +} + +static void +recovery_handler (int signum, siginfo_t *info, void *context) +{ + (void) context; + + // TODO: maybe try to force a core dump like this: if (fork() == 0) return; + // TODO: maybe we could even send "\r\nQUIT :reason\r\n" to the server. >_> + // As long as we're not connected via TLS, that is. + + const char *signal_name = NULL, *reason = NULL; + translate_signal_info (signum, &signal_name, info->si_code, &reason); + + char buf[128], numbuf[8]; + if (!signal_name) + { + snprintf (numbuf, sizeof numbuf, "%d", signum); + signal_name = numbuf; + } + + if (reason) + snprintf (buf, sizeof buf, "%s=%s: %s: %s", g_startup_reason_str, + "signal received", signal_name, reason); + else + snprintf (buf, sizeof buf, "%s=%s: %s", g_startup_reason_str, + "signal received", signal_name); + *g_startup_reason_location = buf; + + // TODO: maybe pregenerate the path, see the following for some other ways + // that would be illegal to do from within a signal handler: + // http://stackoverflow.com/a/1024937 + // http://stackoverflow.com/q/799679 + // Especially if we change the current working directory in the program. + // + // Note that I can just overwrite g_orig_argv[0]. + + // NOTE: our children will read EOF on the read ends of their pipes as a + // a result of O_CLOEXEC. That should be enough to make them terminate. + + char **argv = g_original_argv.vector, **argp = g_recovery_env.vector; + execve ("/proc/self/exe", argv, argp); // Linux + execve ("/proc/curproc/file", argv, argp); // BSD + execve ("/proc/curproc/exe", argv, argp); // BSD + execve ("/proc/self/path/a.out", argv, argp); // Solaris + execve (argv[0], argv, argp); // unreliable fallback + + // Let's just crash + perror ("execve"); + signal (signum, SIG_DFL); + raise (signum); +} + +static void +prepare_recovery_environment (void) +{ + str_vector_init (&g_recovery_env); + str_vector_add_vector (&g_recovery_env, environ); + + // Prepare a location within the environment where we will put the startup + // (or maybe rather restart) reason in case of an irrecoverable error. + char **iter; + for (iter = g_recovery_env.vector; *iter; iter++) + { + const size_t len = sizeof g_startup_reason_str - 1; + if (!strncmp (*iter, g_startup_reason_str, len) && (*iter)[len] == '=') + break; + } + + if (iter) + g_startup_reason_location = iter; + else + { + str_vector_add (&g_recovery_env, ""); + g_startup_reason_location = + g_recovery_env.vector + g_recovery_env.len - 1; + } +} + +static void +setup_recovery_handler (struct bot_context *ctx) +{ + const char *recover_str = str_map_find (&ctx->config, "recover"); + hard_assert (recover_str != NULL); // We have a default value for this + + bool recover; + if (!set_boolean_if_valid (&recover, recover_str)) + { + print_error ("invalid configuration value for `%s'", "recover"); + exit (EXIT_FAILURE); + } + if (!recover) + return; + + // Make sure these signals aren't blocked, otherwise we would be unable + // to handle them, making the critical conditions fatal. + sigset_t mask; + sigemptyset (&mask); + sigaddset (&mask, SIGSEGV); + sigaddset (&mask, SIGBUS); + sigaddset (&mask, SIGFPE); + sigaddset (&mask, SIGILL); + sigprocmask (SIG_UNBLOCK, &mask, NULL); + + struct sigaction sa; + sa.sa_flags = SA_SIGINFO; + sa.sa_sigaction = recovery_handler; + sigemptyset (&sa.sa_mask); + + prepare_recovery_environment (); + + // TODO: also handle SIGABRT... or avoid doing abort() in the first place? + if (sigaction (SIGSEGV, &sa, NULL) == -1 + || sigaction (SIGBUS, &sa, NULL) == -1 + || sigaction (SIGFPE, &sa, NULL) == -1 + || sigaction (SIGILL, &sa, NULL) == -1) + print_error ("sigaction: %s", strerror (errno)); +} + +// --- Plugins ----------------------------------------------------------------- + +/// The name of the special IRC command for interprocess communication +static const char *plugin_ipc_command = "ZYKLONB"; + +static struct plugin_data * +plugin_find_by_pid (struct bot_context *ctx, pid_t pid) +{ + struct plugin_data *iter; + for (iter = ctx->plugins; iter; iter = iter->next) + if (iter->pid == pid) + return iter; + return NULL; +} + +static bool +plugin_zombify (struct plugin_data *plugin) +{ + if (plugin->is_zombie) + return false; + + // FIXME: make sure that we don't remove entries from the poller while we + // still may have stuff to read; maybe just check that the read pipe is + // empty before closing it... and then on EOF check if `pid == -1' and + // only then dispose of it (it'd be best to simulate that both of these + // cases may happen). + ssize_t poller_idx = + poller_find_by_fd (&plugin->ctx->poller, plugin->write_fd); + if (poller_idx != -1) + poller_remove_at_index (&plugin->ctx->poller, poller_idx); + + // TODO: try to flush the write buffer (non-blocking)? + + // The plugin should terminate itself after it receives EOF. + xclose (plugin->write_fd); + plugin->write_fd = -1; + + // Make it a pseudo-anonymous zombie. In this state we process any + // remaining commands it attempts to send to us before it finally dies. + str_map_set (&plugin->ctx->plugins_by_name, plugin->name, NULL); + plugin->is_zombie = true; + return true; +} + +static void +on_plugin_writable (const struct pollfd *fd, struct plugin_data *plugin) +{ + struct bot_context *ctx = plugin->ctx; + struct str *buf = &plugin->write_buffer; + size_t written_total = 0; + + if (fd->revents & ~(POLLOUT | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + while (written_total != buf->len) + { + ssize_t n_written = write (fd->fd, buf->str + written_total, + buf->len - written_total); + + if (n_written < 0) + { + if (errno == EAGAIN) + break; + if (errno == EINTR) + continue; + + soft_assert (errno == EPIPE); + // Zombies shouldn't get dispatched for writability + hard_assert (!plugin->is_zombie); + + print_debug ("%s: %s", "write", strerror (errno)); + print_error ("failure on writing to plugin `%s'," + " therefore I'm unloading it", plugin->name); + plugin_zombify (plugin); + break; + } + + // This may be equivalent to EAGAIN on some implementations + if (n_written == 0) + break; + + written_total += n_written; + } + + if (written_total != 0) + str_remove_slice (buf, 0, written_total); + + if (buf->len == 0) + { + // Everything has been written, there's no need to end up in here again + ssize_t index = poller_find_by_fd (&ctx->poller, fd->fd); + if (index != -1) + poller_remove_at_index (&ctx->poller, index); + } +} + +static void +plugin_queue_write (struct plugin_data *plugin) +{ + if (plugin->is_zombie) + return; + + // Don't let the write buffer grow infinitely. If there's a ton of data + // waiting to be processed by the plugin, it usually means there's something + // wrong with it (such as someone stopping the process). + if (plugin->write_buffer.len >= (1 << 20)) + { + print_warning ("plugin `%s' does not seem to process messages fast" + " enough, I'm unloading it", plugin->name); + plugin_zombify (plugin); + return; + } + + poller_set (&plugin->ctx->poller, plugin->write_fd, POLLOUT, + (poller_dispatcher_func) on_plugin_writable, plugin); +} + +static void +plugin_send (struct plugin_data *plugin, const char *format, ...) + ATTRIBUTE_PRINTF (2, 3); + +static void +plugin_send (struct plugin_data *plugin, const char *format, ...) +{ + va_list ap; + + if (g_debug_mode) + { + fprintf (stderr, "[%s] <-- \"", plugin->name); + va_start (ap, format); + vfprintf (stderr, format, ap); + va_end (ap); + fputs ("\"\n", stderr); + } + + va_start (ap, format); + str_append_vprintf (&plugin->write_buffer, format, ap); + va_end (ap); + str_append (&plugin->write_buffer, "\r\n"); + + plugin_queue_write (plugin); +} + +static void +plugin_process_message (const struct irc_message *msg, + const char *raw, void *user_data) +{ + struct plugin_data *plugin = user_data; + struct bot_context *ctx = plugin->ctx; + + if (g_debug_mode) + fprintf (stderr, "[%s] --> \"%s\"\n", plugin->name, raw); + + if (!strcasecmp (msg->command, plugin_ipc_command)) + { + // Replies are sent in the order in which they came in, so there's + // no need to attach a special identifier to them. It might be + // desirable in some cases, though. + + if (msg->params.len < 1) + return; + + const char *command = msg->params.vector[0]; + if (!plugin->initialized && !strcasecmp (command, "register")) + { + // Register for relaying of IRC traffic + plugin->initialized = true; + + // Flush any queued up traffic here. The point of queuing it in + // the first place is so that we don't have to wait for plugin + // initialization during startup. + // + // Note that if we start filtering data coming to the plugins e.g. + // based on what it tells us upon registration, we might need to + // filter `queued_output' as well. + str_append_str (&plugin->write_buffer, &plugin->queued_output); + str_free (&plugin->queued_output); + + // NOTE: this may trigger the buffer length check + plugin_queue_write (plugin); + } + else if (!strcasecmp (command, "get_config")) + { + if (msg->params.len < 2) + return; + + const char *value = + str_map_find (&ctx->config, msg->params.vector[1]); + // TODO: escape the value (although there's no need to ATM) + plugin_send (plugin, "%s :%s", + plugin_ipc_command, value ? value : ""); + } + else if (!strcasecmp (command, "print")) + { + if (msg->params.len < 2) + return; + + printf ("%s\n", msg->params.vector[1]); + } + } + else if (plugin->initialized && ctx->irc_ready) + { + // Pass everything else through to the IRC server + // XXX: when the server isn't ready yet, these messages get silently + // discarded, which shouldn't pose a problem most of the time. + // Perhaps we could send a "connected" notification on `register' + // if `irc_ready' is true, or after it becomes true later, so that + // plugins know when to start sending unprovoked IRC messages. + // XXX: another case is when the connection gets interrupted and the + // plugin tries to send something back while we're reconnecting. + // For that we might set up a global buffer that gets flushed out + // after `irc_ready' becomes true. Note that there is always some + // chance of messages getting lost without us even noticing it. + irc_send (ctx, "%s", raw); + } +} + +static void +on_plugin_readable (const struct pollfd *fd, struct plugin_data *plugin) +{ + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + // TODO: see if I can reuse irc_fill_read_buffer() + struct str *buf = &plugin->read_buffer; + while (true) + { + str_ensure_space (buf, 512 + 1); + ssize_t n_read = read (fd->fd, buf->str + buf->len, + buf->alloc - buf->len - 1); + + if (n_read < 0) + { + if (errno == EAGAIN) + break; + if (soft_assert (errno == EINTR)) + continue; + + if (!plugin->is_zombie) + { + print_error ("failure on reading from plugin `%s'," + " therefore I'm unloading it", plugin->name); + plugin_zombify (plugin); + } + return; + } + + // EOF; hopefully it will die soon (maybe it already has) + if (n_read == 0) + break; + + buf->str[buf->len += n_read] = '\0'; + if (buf->len >= (1 << 20)) + { + // XXX: this isn't really the best flood prevention mechanism, + // but it wasn't even supposed to be one. + if (plugin->is_zombie) + { + print_error ("a zombie of plugin `%s' is trying to flood us," + " therefore I'm killing it", plugin->name); + kill (plugin->pid, SIGKILL); + } + else + { + print_error ("plugin `%s' seems to spew out data frantically," + " therefore I'm unloading it", plugin->name); + plugin_zombify (plugin); + } + return; + } + } + + irc_process_buffer (buf, plugin_process_message, plugin); +} + +static bool +is_valid_plugin_name (const char *name) +{ + if (!*name) + return false; + for (const char *p = name; *p; p++) + if (!isgraph (*p) || *p == '/') + return false; + return true; +} + +static bool +plugin_load (struct bot_context *ctx, const char *name, struct error **e) +{ + const char *plugin_dir = str_map_find (&ctx->config, "plugin_dir"); + if (!plugin_dir) + { + error_set (e, "plugin directory not set"); + return false; + } + + if (!is_valid_plugin_name (name)) + { + error_set (e, "invalid plugin name"); + return false; + } + + if (str_map_find (&ctx->plugins_by_name, name)) + { + error_set (e, "the plugin has already been loaded"); + return false; + } + + int stdin_pipe[2]; + if (pipe (stdin_pipe) == -1) + { + error_set (e, "%s: %s: %s", + "failed to load the plugin", "pipe", strerror (errno)); + goto fail_1; + } + + int stdout_pipe[2]; + if (pipe (stdout_pipe) == -1) + { + error_set (e, "%s: %s: %s", + "failed to load the plugin", "pipe", strerror (errno)); + goto fail_2; + } + + set_cloexec (stdin_pipe[1]); + set_cloexec (stdout_pipe[0]); + + pid_t pid = fork (); + if (pid == -1) + { + error_set (e, "%s: %s: %s", + "failed to load the plugin", "fork", strerror (errno)); + goto fail_3; + } + + if (pid == 0) + { + // Redirect the child's stdin and stdout to the pipes + hard_assert (dup2 (stdin_pipe[0], STDIN_FILENO) != -1); + hard_assert (dup2 (stdout_pipe[1], STDOUT_FILENO) != -1); + + xclose (stdin_pipe[0]); + xclose (stdout_pipe[1]); + + struct str pathname; + str_init (&pathname); + str_append (&pathname, plugin_dir); + str_append_c (&pathname, '/'); + str_append (&pathname, name); + + // Restore some of the signal handling + signal (SIGPIPE, SIG_DFL); + + char *const argv[] = { pathname.str, NULL }; + execve (argv[0], argv, environ); + + // We will collect the failure later via SIGCHLD + print_error ("%s: %s: %s", + "failed to load the plugin", "exec", strerror (errno)); + _exit (EXIT_FAILURE); + } + + xclose (stdin_pipe[0]); + xclose (stdout_pipe[1]); + + set_blocking (stdout_pipe[0], false); + set_blocking (stdin_pipe[1], false); + + struct plugin_data *plugin = xmalloc (sizeof *plugin); + plugin_data_init (plugin); + plugin->ctx = ctx; + plugin->pid = pid; + plugin->name = xstrdup (name); + plugin->read_fd = stdout_pipe[0]; + plugin->write_fd = stdin_pipe[1]; + + LIST_PREPEND (ctx->plugins, plugin); + str_map_set (&ctx->plugins_by_name, name, plugin); + + poller_set (&ctx->poller, stdout_pipe[0], POLLIN, + (poller_dispatcher_func) on_plugin_readable, plugin); + return true; + +fail_3: + xclose (stdout_pipe[0]); + xclose (stdout_pipe[1]); +fail_2: + xclose (stdin_pipe[0]); + xclose (stdin_pipe[1]); +fail_1: + return false; +} + +static bool +plugin_unload (struct bot_context *ctx, const char *name, struct error **e) +{ + struct plugin_data *plugin = str_map_find (&ctx->plugins_by_name, name); + + if (!plugin) + { + error_set (e, "no such plugin is loaded"); + return false; + } + + plugin_zombify (plugin); + + // TODO: add a `kill zombies' command to forcefully get rid of processes + // that do not understand the request. + // TODO: set a timeout before we go for a kill automatically (and if this + // was a reload request, try to bring the plugin back up) + return true; +} + +static void +plugin_load_all_from_config (struct bot_context *ctx) +{ + const char *plugin_list = str_map_find (&ctx->config, "plugins"); + if (!plugin_list) + return; + + struct str_vector plugins; + str_vector_init (&plugins); + + split_str_ignore_empty (plugin_list, ',', &plugins); + for (size_t i = 0; i < plugins.len; i++) + { + char *name = strip_str_in_place (plugins.vector[i], " "); + + struct error *e = NULL; + if (!plugin_load (ctx, name, &e)) + { + print_error ("plugin `%s' failed to load: %s", name, e->message); + error_free (e); + } + } + + str_vector_free (&plugins); +} + +// --- Main program ------------------------------------------------------------ + +static bool +parse_bot_command (const char *s, const char *command, const char **following) +{ + size_t command_len = strlen (command); + if (strncasecmp (s, command, command_len)) + return false; + s += command_len; + + // Expect a word boundary, so that we don't respond to invalid things + if (isalnum (*s)) + return false; + + // Ignore any initial spaces; the rest is the command's argument + while (isblank (*s)) + s++; + *following = s; + return true; +} + +static void +split_bot_command_argument_list (const char *arguments, struct str_vector *out) +{ + split_str_ignore_empty (arguments, ',', out); + for (size_t i = 0; i < out->len; ) + { + if (!*strip_str_in_place (out->vector[i], " \t")) + str_vector_remove (out, i); + else + i++; + } +} + +static bool +is_private_message (const struct irc_message *msg) +{ + hard_assert (msg->params.len); + return !strchr ("#&+!", *msg->params.vector[0]); +} + +static bool +is_sent_by_admin (struct bot_context *ctx, const struct irc_message *msg) +{ + // No administrator set -> everyone is an administrator + if (!ctx->admin_re) + return true; + return regexec (ctx->admin_re, msg->prefix, 0, NULL, 0) != REG_NOMATCH; +} + +static void respond_to_user (struct bot_context *ctx, const struct + irc_message *msg, const char *format, ...) ATTRIBUTE_PRINTF (3, 4); + +static void +respond_to_user (struct bot_context *ctx, const struct irc_message *msg, + const char *format, ...) +{ + if (!soft_assert (msg->prefix && msg->params.len)) + return; + + char nick[strcspn (msg->prefix, "!") + 1]; + strncpy (nick, msg->prefix, sizeof nick - 1); + nick[sizeof nick - 1] = '\0'; + + struct str text; + va_list ap; + + str_init (&text); + va_start (ap, format); + str_append_vprintf (&text, format, ap); + va_end (ap); + + if (is_private_message (msg)) + irc_send (ctx, "PRIVMSG %s :%s", nick, text.str); + else + irc_send (ctx, "PRIVMSG %s :%s: %s", + msg->params.vector[0], nick, text.str); + + str_free (&text); +} + +static void +process_plugin_load (struct bot_context *ctx, + const struct irc_message *msg, const char *name) +{ + struct error *e = NULL; + if (plugin_load (ctx, name, &e)) + respond_to_user (ctx, msg, "plugin `%s' queued for loading", name); + else + { + respond_to_user (ctx, msg, "plugin `%s' could not be loaded: %s", + name, e->message); + error_free (e); + } +} + +static void +process_plugin_unload (struct bot_context *ctx, + const struct irc_message *msg, const char *name) +{ + struct error *e = NULL; + if (plugin_unload (ctx, name, &e)) + respond_to_user (ctx, msg, "plugin `%s' unloaded", name); + else + { + respond_to_user (ctx, msg, "plugin `%s' could not be unloaded: %s", + name, e->message); + error_free (e); + } +} + +static void +process_plugin_reload (struct bot_context *ctx, + const struct irc_message *msg, const char *name) +{ + // So far the only error that can occur is that the plugin hasn't been + // loaded, which in this case doesn't really matter. + plugin_unload (ctx, name, NULL); + + process_plugin_load (ctx, msg, name); +} + +static void +process_privmsg (struct bot_context *ctx, const struct irc_message *msg) +{ + if (!is_sent_by_admin (ctx, msg)) + return; + if (msg->params.len < 2) + return; + + const char *prefix = str_map_find (&ctx->config, "prefix"); + hard_assert (prefix != NULL); // We have a default value for this + + // For us to recognize the command, it has to start with the prefix, + // with the exception of PM's sent directly to us. + const char *text = msg->params.vector[1]; + if (!strncmp (text, prefix, strlen (prefix))) + text += strlen (prefix); + else if (!is_private_message (msg)) + return; + + const char *following; + struct str_vector list; + str_vector_init (&list); + + if (parse_bot_command (text, "quote", &following)) + // This seems to replace tons of random stupid commands + irc_send (ctx, "%s", following); + else if (parse_bot_command (text, "quit", &following)) + { + // We actually need this command (instead of just `quote') because we + // could try to reconnect to the server automatically otherwise. + if (*following) + irc_send (ctx, "QUIT :%s", following); + else + irc_send (ctx, "QUIT"); + initiate_quit (ctx); + } + else if (parse_bot_command (text, "status", &following)) + { + struct str report; + str_init (&report); + + const char *reason = getenv (g_startup_reason_str); + if (!reason) + reason = "launched normally"; + str_append_printf (&report, "\x02startup reason:\x0f %s, ", reason); + + str_append (&report, "\x02plugins:\x0f "); + size_t zombies = 0; + for (struct plugin_data *plugin = ctx->plugins; + plugin; plugin = plugin->next) + { + if (plugin->is_zombie) + zombies++; + else + str_append_printf (&report, "%s, ", plugin->name); + } + if (!ctx->plugins) + str_append (&report, "\x02none\x0f, "); + str_append_printf (&report, "\x02zombies:\x0f %zu", zombies); + + respond_to_user (ctx, msg, "%s", report.str); + str_free (&report); + } + else if (parse_bot_command (text, "load", &following)) + { + split_bot_command_argument_list (following, &list); + for (size_t i = 0; i < list.len; i++) + process_plugin_load (ctx, msg, list.vector[i]); + } + else if (parse_bot_command (text, "reload", &following)) + { + split_bot_command_argument_list (following, &list); + for (size_t i = 0; i < list.len; i++) + process_plugin_reload (ctx, msg, list.vector[i]); + } + else if (parse_bot_command (text, "unload", &following)) + { + split_bot_command_argument_list (following, &list); + for (size_t i = 0; i < list.len; i++) + process_plugin_unload (ctx, msg, list.vector[i]); + } + + str_vector_free (&list); +} + +static void +irc_forward_message_to_plugins (struct bot_context *ctx, const char *raw) +{ + // For consistency with plugin_process_message() + if (!ctx->irc_ready) + return; + + for (struct plugin_data *plugin = ctx->plugins; + plugin; plugin = plugin->next) + { + if (plugin->is_zombie) + continue; + + if (plugin->initialized) + plugin_send (plugin, "%s", raw); + else + // TODO: make sure that this buffer doesn't get too large either + str_append_printf (&plugin->queued_output, "%s\r\n", raw); + } +} + +static void +irc_process_message (const struct irc_message *msg, + const char *raw, void *user_data) +{ + struct bot_context *ctx = user_data; + if (g_debug_mode) + fprintf (stderr, "[%s] ==> \"%s\"\n", "IRC", raw); + + // This should be as minimal as possible, I don't want to have the whole bot + // written in C, especially when I have this overengineered plugin system. + // Therefore the very basic functionality only. + // + // I should probably even rip out the autojoin... + + irc_forward_message_to_plugins (ctx, raw); + + if (!strcasecmp (msg->command, "PING")) + { + if (msg->params.len) + irc_send (ctx, "PONG :%s", msg->params.vector[0]); + else + irc_send (ctx, "PONG"); + } + else if (!ctx->irc_ready && (!strcasecmp (msg->command, "MODE") + || !strcasecmp (msg->command, "376") // RPL_ENDOFMOTD + || !strcasecmp (msg->command, "422"))) // ERR_NOMOTD + { + print_status ("successfully connected"); + ctx->irc_ready = true; + + const char *autojoin = str_map_find (&ctx->config, "autojoin"); + if (autojoin) + irc_send (ctx, "JOIN :%s", autojoin); + } + else if (!strcasecmp (msg->command, "PRIVMSG")) + process_privmsg (ctx, msg); +} + +enum irc_read_result +{ + IRC_READ_OK, ///< Some data were read successfully + IRC_READ_EOF, ///< The server has closed connection + IRC_READ_AGAIN, ///< No more data at the moment + IRC_READ_ERROR ///< General connection failure +}; + +static enum irc_read_result +irc_fill_read_buffer_ssl (struct bot_context *ctx, struct str *buf) +{ + int n_read; +start: + n_read = SSL_read (ctx->ssl, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */); + + const char *error_info = NULL; + switch (xssl_get_error (ctx->ssl, n_read, &error_info)) + { + case SSL_ERROR_NONE: + buf->str[buf->len += n_read] = '\0'; + return IRC_READ_OK; + case SSL_ERROR_ZERO_RETURN: + return IRC_READ_EOF; + case SSL_ERROR_WANT_READ: + return IRC_READ_AGAIN; + case SSL_ERROR_WANT_WRITE: + { + // Let it finish the handshake as we don't poll for writability; + // any errors are to be collected by SSL_read() in the next iteration + struct pollfd pfd = { .fd = ctx->irc_fd, .events = POLLOUT }; + soft_assert (poll (&pfd, 1, 0) > 0); + goto start; + } + case XSSL_ERROR_TRY_AGAIN: + goto start; + default: + print_debug ("%s: %s: %s", __func__, "SSL_read", error_info); + return IRC_READ_ERROR; + } +} + +static enum irc_read_result +irc_fill_read_buffer (struct bot_context *ctx, struct str *buf) +{ + ssize_t n_read; +start: + n_read = recv (ctx->irc_fd, buf->str + buf->len, + buf->alloc - buf->len - 1 /* null byte */, 0); + + if (n_read > 0) + { + buf->str[buf->len += n_read] = '\0'; + return IRC_READ_OK; + } + if (n_read == 0) + return IRC_READ_EOF; + + if (errno == EAGAIN) + return IRC_READ_AGAIN; + if (errno == EINTR) + goto start; + + print_debug ("%s: %s: %s", __func__, "recv", strerror (errno)); + return IRC_READ_ERROR; +} + +static bool irc_connect (struct bot_context *ctx, struct error **e); + +static void +irc_try_reconnect (struct bot_context *ctx) +{ + if (!soft_assert (ctx->irc_fd == -1)) + return; + + const char *reconnect_str = str_map_find (&ctx->config, "reconnect"); + hard_assert (reconnect_str != NULL); // We have a default value for this + + bool reconnect; + if (!set_boolean_if_valid (&reconnect, reconnect_str)) + { + print_fatal ("invalid configuration value for `%s'", "recover"); + try_finish_quit (ctx); + return; + } + if (!reconnect) + return; + + const char *delay_str = str_map_find (&ctx->config, "reconnect_delay"); + hard_assert (delay_str != NULL); // We have a default value for this + + unsigned long delay; + if (!xstrtoul (&delay, delay_str, 10)) + { + print_error ("invalid configuration value for `%s'", + "reconnect_delay"); + delay = 0; + } + + while (true) + { + // TODO: this would be better suited by a timeout event; + // remember to update try_finish_quit() etc. to reflect this + print_status ("trying to reconnect in %ld seconds...", delay); + sleep (delay); + + struct error *e = NULL; + if (irc_connect (ctx, &e)) + break; + + print_error ("%s", e->message); + error_free (e); + } + + // TODO: inform plugins about the new connection +} + +static void +on_irc_disconnected (struct bot_context *ctx) +{ + // Get rid of the dead socket etc. + if (ctx->ssl) + { + SSL_free (ctx->ssl); + ctx->ssl = NULL; + SSL_CTX_free (ctx->ssl_ctx); + ctx->ssl_ctx = NULL; + } + + ssize_t i = poller_find_by_fd (&ctx->poller, ctx->irc_fd); + if (i != -1) + poller_remove_at_index (&ctx->poller, i); + + xclose (ctx->irc_fd); + ctx->irc_fd = -1; + ctx->irc_ready = false; + + // TODO: inform plugins about the disconnect event + + if (ctx->quitting) + { + // Unload all plugins + // TODO: wait for a few seconds and then send SIGKILL to all plugins + for (struct plugin_data *plugin = ctx->plugins; + plugin; plugin = plugin->next) + plugin_zombify (plugin); + + try_finish_quit (ctx); + return; + } + + irc_try_reconnect (ctx); +} + +static void +on_irc_readable (const struct pollfd *fd, struct bot_context *ctx) +{ + if (fd->revents & ~(POLLIN | POLLHUP | POLLERR)) + print_debug ("fd %d: unexpected revents: %d", fd->fd, fd->revents); + + (void) set_blocking (ctx->irc_fd, false); + + struct str *buf = &ctx->read_buffer; + enum irc_read_result (*fill_buffer)(struct bot_context *, struct str *) + = ctx->ssl + ? irc_fill_read_buffer_ssl + : irc_fill_read_buffer; + bool disconnected = false; + while (true) + { + str_ensure_space (buf, 512); + switch (fill_buffer (ctx, buf)) + { + case IRC_READ_AGAIN: + goto end; + case IRC_READ_ERROR: + print_error ("reading from the IRC server failed"); + disconnected = true; + goto end; + case IRC_READ_EOF: + print_status ("the IRC server closed the connection"); + disconnected = true; + goto end; + case IRC_READ_OK: + break; + } + + if (buf->len >= (1 << 20)) + { + print_error ("the IRC server seems to spew out data frantically"); + irc_shutdown (ctx); + goto end; + } + } +end: + (void) set_blocking (ctx->irc_fd, true); + irc_process_buffer (buf, irc_process_message, ctx); + + if (disconnected) + on_irc_disconnected (ctx); +} + +static bool +irc_connect (struct bot_context *ctx, struct error **e) +{ + const char *irc_host = str_map_find (&ctx->config, "irc_host"); + const char *irc_port = str_map_find (&ctx->config, "irc_port"); + const char *ssl_use_str = str_map_find (&ctx->config, "ssl_use"); + + const char *nickname = str_map_find (&ctx->config, "nickname"); + const char *username = str_map_find (&ctx->config, "username"); + const char *realname = str_map_find (&ctx->config, "realname"); + + // We have a default value for these + hard_assert (irc_port && ssl_use_str); + hard_assert (nickname && username && realname); + + // TODO: again, get rid of `struct error' in here. The question is: how + // do we tell our caller that he should not try to reconnect? + if (!irc_host) + { + error_set (e, "no hostname specified in configuration"); + return false; + } + + bool use_ssl; + if (!set_boolean_if_valid (&use_ssl, ssl_use_str)) + { + error_set (e, "invalid configuration value for `%s'", "use_ssl"); + return false; + } + + if (!irc_establish_connection (ctx, irc_host, irc_port, use_ssl, e)) + return false; + + // TODO: set a timeout on the socket, something like 30 minutes, then we + // should ideally send a PING... or just forcefully reconnect. + // + // TODO: in exec try: 1/ set blocking, 2/ setsockopt() SO_LINGER, + // (struct linger) { .l_onoff = true; .l_linger = 1 /* 1s should do */; } + // 3/ /* O_CLOEXEC */ But only if the QUIT message proves unreliable. + poller_set (&ctx->poller, ctx->irc_fd, POLLIN, + (poller_dispatcher_func) on_irc_readable, ctx); + + // TODO: probably check for errors from these calls as well + irc_send (ctx, "NICK %s", nickname); + irc_send (ctx, "USER %s 8 * :%s", username, realname); + return true; +} + +static bool +load_admin_regex (struct bot_context *ctx) +{ + hard_assert (!ctx->admin_re); + const char *admin = str_map_find (&ctx->config, "admin"); + + if (!admin) + return true; + + struct error *e = NULL; + ctx->admin_re = regex_compile (admin, REG_EXTENDED | REG_NOSUB, &e); + if (!e) + return true; + + print_error ("invalid configuration value for `%s': %s", + "admin", e->message); + error_free (e); + return false; +} + +static void +on_signal_pipe_readable (const struct pollfd *fd, struct bot_context *ctx) +{ + char *dummy; + (void) read (fd->fd, &dummy, 1); + + // XXX: do we need to check if we have a connection? + if (g_termination_requested && !ctx->quitting) + { + irc_send (ctx, "QUIT :Terminated by signal"); + initiate_quit (ctx); + } + + // Reap all dead children (since the pipe may overflow, we ask waitpid() + // to return all the zombies it knows about). + while (true) + { + int status; + pid_t zombie = waitpid (-1, &status, WNOHANG); + + if (zombie == -1) + { + // No children to wait on + if (errno == ECHILD) + break; + + hard_assert (errno == EINTR); + continue; + } + + if (zombie == 0) + break; + + struct plugin_data *plugin = plugin_find_by_pid (ctx, zombie); + // Something has died but we don't recognize it (re-exec?) + if (!soft_assert (plugin != NULL)) + continue; + + // TODO: callbacks on children death, so that we may tell the user + // "plugin `name' died like a dirty jewish pig"; use `status' + if (!plugin->is_zombie && WIFSIGNALED (status)) + { + const char *notes = ""; +#ifdef WCOREDUMP + if (WCOREDUMP (status)) + notes = " (core dumped)"; +#endif + print_warning ("Plugin `%s' died from signal %d%s", + plugin->name, WTERMSIG (status), notes); + } + + // Let's go through the zombie state to simplify things a bit + // TODO: might not be a completely bad idea to restart the plugin + plugin_zombify (plugin); + + plugin->pid = -1; + + ssize_t poller_idx = poller_find_by_fd (&ctx->poller, plugin->read_fd); + if (poller_idx != -1) + poller_remove_at_index (&ctx->poller, poller_idx); + + xclose (plugin->read_fd); + plugin->read_fd = -1; + + LIST_UNLINK (ctx->plugins, plugin); + plugin_data_free (plugin); + free (plugin); + + // Living child processes block us from quitting + try_finish_quit (ctx); + } +} + +static void +print_usage (const char *program_name) +{ + fprintf (stderr, + "Usage: %s [OPTION]...\n" + "Experimental IRC bot.\n" + "\n" + " -d, --debug run in debug mode\n" + " -h, --help display this help and exit\n" + " -V, --version output version information and exit\n" + " --write-default-cfg [filename]\n" + " write a default configuration file and exit\n", + program_name); +} + +int +main (int argc, char *argv[]) +{ + const char *invocation_name = argv[0]; + str_vector_init (&g_original_argv); + str_vector_add_vector (&g_original_argv, argv); + + static struct option opts[] = + { + { "debug", no_argument, NULL, 'd' }, + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, 'V' }, + { "write-default-cfg", optional_argument, NULL, 'w' }, + { NULL, 0, NULL, 0 } + }; + + while (1) + { + int c, opt_index; + + c = getopt_long (argc, argv, "dhV", opts, &opt_index); + if (c == -1) + break; + + switch (c) + { + case 'd': + g_debug_mode = true; + break; + case 'h': + print_usage (invocation_name); + exit (EXIT_SUCCESS); + case 'V': + printf (PROGRAM_NAME " " PROGRAM_VERSION "\n"); + exit (EXIT_SUCCESS); + case 'w': + call_write_default_config (optarg, g_config_table); + exit (EXIT_SUCCESS); + default: + print_error ("wrong options"); + exit (EXIT_FAILURE); + } + } + + print_status (PROGRAM_NAME " " PROGRAM_VERSION " starting"); + setup_signal_handlers (); + + SSL_library_init (); + atexit (EVP_cleanup); + SSL_load_error_strings (); + // XXX: ERR_load_BIO_strings()? Anything else? + atexit (ERR_free_strings); + + struct bot_context ctx; + bot_context_init (&ctx); + + struct error *e = NULL; + if (!read_config_file (&ctx.config, &e)) + { + print_error ("error loading configuration: %s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + setup_recovery_handler (&ctx); + poller_set (&ctx.poller, g_signal_pipe[0], POLLIN, + (poller_dispatcher_func) on_signal_pipe_readable, &ctx); + + plugin_load_all_from_config (&ctx); + if (!load_admin_regex (&ctx)) + exit (EXIT_FAILURE); + if (!irc_connect (&ctx, &e)) + { + print_error ("%s", e->message); + error_free (e); + exit (EXIT_FAILURE); + } + + // TODO: clean re-exec support; to save the state I can either use argv, + // argp, or I can create a temporary file, unlink it and use the FD + // (mkstemp() on a `struct str' constructed from XDG_RUNTIME_DIR, TMPDIR + // or /tmp as a last resort + PROGRAM_NAME + ".XXXXXX" -> unlink(); + // remember to use O_CREAT | O_EXCL). The state needs to be versioned. + // Unfortunately I cannot de/serialize SSL state. + + ctx.polling = true; + while (ctx.polling) + poller_run (&ctx.poller); + + bot_context_free (&ctx); + str_vector_free (&g_original_argv); + return EXIT_SUCCESS; +} + |