diff mbox

[for-2.4,1/5] chardev: Add IRC char driver

Message ID 1427890157-18639-2-git-send-email-mreitz@redhat.com
State New
Headers show

Commit Message

Max Reitz April 1, 2015, 12:09 p.m. UTC
In order for qemu to become more human-friendly, we require an interface
which is actually used by humans in their normal day-to-day
communication. Many people in the qemu community will agree that IRC is
indeed such a communication platform. By adding that support to qemu,
users no longer have to switch from IRC to command line and back to
debug QMP issues and the like while discussing it in the #qemu channel.

Furthermore, debugging has never been this social: In the past, people
needed to ask the debugging person to try specific commands and that
person had to paste the reply back to IRC. Now, you can simply let qemu
connect to a discussion channel and everyone can participate in
debugging.

And finally, there are technical merits as well: IRC is a well-tested,
fault-tolerant network with redundant nodes. This gives us much greater
reliability than any of the existing character devices, which is
especially important for remote debugging.

Signed-off-by: Max Reitz <mreitz@redhat.com>
---
 qapi-schema.json |  16 ++
 qemu-char.c      | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 508 insertions(+)
diff mbox

Patch

diff --git a/qapi-schema.json b/qapi-schema.json
index ac9594d..f427e04 100644
--- a/qapi-schema.json
+++ b/qapi-schema.json
@@ -2876,6 +2876,21 @@ 
 { 'type': 'ChardevRingbuf', 'data': { '*size'  : 'int' } }
 
 ##
+# @ChardevIrc
+#
+# Configuration info for IRC chardevs.
+#
+# @addr:    IRC server
+# @nick:    Nick for qemu to use
+# @channel: Channel to join, or nick to query
+#
+# Since: 2.4
+##
+{ 'type': 'ChardevIrc', 'data': { 'addr'        : 'SocketAddress',
+                                  'nick'        : 'str',
+                                  'channel'     : 'str' } }
+
+##
 # @ChardevBackend:
 #
 # Configuration info for the new chardev backend.
@@ -2902,6 +2917,7 @@ 
                                        'spiceport' : 'ChardevSpicePort',
                                        'vc'     : 'ChardevVC',
                                        'ringbuf': 'ChardevRingbuf',
+                                       'irc'    : 'ChardevIrc',
                                        # next one is just for compatibility
                                        'memory' : 'ChardevRingbuf' } }
 
diff --git a/qemu-char.c b/qemu-char.c
index a405d76..a68fef3 100644
--- a/qemu-char.c
+++ b/qemu-char.c
@@ -3296,6 +3296,296 @@  char *qmp_ringbuf_read(const char *device, int64_t size,
     return data;
 }
 
+/*********************************************************/
+/* IRC chardev */
+
+typedef struct {
+    int fd;
+    bool query;
+    char *nick, *channel;
+
+    GIOChannel *chan;
+
+    uint8_t *sendbuf;
+    int sendbuf_idx, sendbuf_prefixlen;
+    bool send_line_skip;
+
+    uint8_t *recvbuf;
+    int recvbuf_idx;
+    bool recv_line_skip;
+} IrcCharDriverState;
+
+static ssize_t irc_send(IrcCharDriverState *irc, const void *buf, size_t len)
+{
+    return send_all(irc->fd, buf, len);
+}
+
+static ssize_t irc_recv(IrcCharDriverState *irc, void *buf, size_t len)
+{
+    return recv_all(irc->fd, buf, len, true);
+}
+
+static int irc_read_line(IrcCharDriverState *irc, char **buf)
+{
+    ssize_t ret;
+    int eol_idx = -1, i;
+
+    for (i = 0; i < irc->recvbuf_idx; i++) {
+        if (irc->recvbuf[i] == '\n' || irc->recvbuf[i] == '\r') {
+            break;
+        }
+    }
+
+    if (i < irc->recvbuf_idx) {
+        eol_idx = i;
+    }
+
+    while (eol_idx < 0) {
+        if (irc->recvbuf_idx >= 512) {
+            irc->recvbuf_idx = 0;
+            irc->recv_line_skip = true;
+        }
+
+        ret = irc_recv(irc, irc->recvbuf + irc->recvbuf_idx,
+                       512 - irc->recvbuf_idx);
+        if (ret < 0) {
+            irc->recvbuf_idx = 0;
+            return ret;
+        }
+
+        for (i = 0; i < ret; i++) {
+            if (irc->recvbuf[irc->recvbuf_idx + i] == '\n' ||
+                irc->recvbuf[irc->recvbuf_idx + i] == '\r')
+            {
+                break;
+            }
+        }
+        irc->recvbuf_idx += ret;
+
+        if (i < ret) {
+            eol_idx = irc->recvbuf_idx - ret + i;
+            break;
+        }
+    }
+
+    if (!irc->recv_line_skip) {
+        *buf = g_new(char, 512);
+        memcpy(*buf, irc->recvbuf, eol_idx);
+        (*buf)[eol_idx] = 0;
+    }
+
+    /* CRLF is the standard, but supporting LF only will not hurt */
+    if (irc->recvbuf[++eol_idx] == '\n') {
+        ++eol_idx;
+    }
+
+    memmove(irc->recvbuf, irc->recvbuf + eol_idx, 512 - eol_idx);
+    irc->recvbuf_idx -= eol_idx;
+    irc->recvbuf[irc->recvbuf_idx] = 0;
+
+    if (irc->recv_line_skip) {
+        irc->recv_line_skip = false;
+        return -EPROTO;
+    }
+    return 0;
+}
+
+static int irc_chr_write(CharDriverState *s, const uint8_t *buf, int len)
+{
+    IrcCharDriverState *irc = s->opaque;
+    static const int max_line_length = 384; /* 512 minus the prefix */
+    int i;
+
+    if (!len) {
+        return 0;
+    }
+
+    for (i = 0; i < len; i++) {
+        if (buf[i] == '\r') {
+            continue;
+        } else if (buf[i] == '\n') {
+            if (irc->sendbuf) {
+                if (!irc->send_line_skip) {
+                    irc->sendbuf[irc->sendbuf_idx++] = '\r';
+                    irc->sendbuf[irc->sendbuf_idx++] = '\n';
+
+                    /* Ignore errors */
+                    irc_send(irc, irc->sendbuf, irc->sendbuf_idx);
+                }
+
+                irc->sendbuf_idx = irc->sendbuf_prefixlen;
+                irc->send_line_skip = false;
+            }
+        } else if (iscntrl(buf[i])) {
+            /* IRC is no terminal, control characters do not make a whole lot
+             * of sense; just skip those lines */
+            irc->send_line_skip = true;
+        } else {
+            assert(irc->sendbuf_idx <= max_line_length);
+
+            if (irc->sendbuf_idx == max_line_length) {
+                int break_index;
+
+                for (break_index = max_line_length - 1; break_index > 0;
+                     break_index--)
+                {
+                    if (isspace(irc->sendbuf[break_index])) {
+                        break;
+                    }
+                }
+                if (break_index > 0) {
+                    irc->sendbuf[break_index] = 0;
+                } else {
+                    break_index = max_line_length;
+                }
+
+                if (!irc->send_line_skip) {
+                    char replaced = irc->sendbuf[break_index + 1];
+
+                    irc->sendbuf[break_index] = '\r';
+                    irc->sendbuf[break_index + 1] = '\n';
+
+                    irc_send(irc, irc->sendbuf, break_index + 2);
+
+                    irc->sendbuf[break_index + 1] = replaced;
+                }
+
+                memmove(&irc->sendbuf[irc->sendbuf_prefixlen],
+                        &irc->sendbuf[break_index + 1],
+                        max_line_length - break_index - 1);
+
+                irc->sendbuf_idx = irc->sendbuf_prefixlen +
+                                   max_line_length - break_index - 1;
+            }
+
+            irc->sendbuf[irc->sendbuf_idx++] = buf[i];
+        }
+    }
+
+    return len;
+}
+
+static int irc_chr_read_poll(void *opaque)
+{
+    CharDriverState *s = opaque;
+    return qemu_chr_be_can_write(s);
+}
+
+static gboolean irc_chr_read(GIOChannel *chan, GIOCondition cond, void *opaque)
+{
+    CharDriverState *s = opaque;
+    IrcCharDriverState *irc = s->opaque;
+    char *cmd, *src = NULL, *dest = NULL, *msg = NULL;
+    char *cmd_save = NULL, *line, real_src[128], real_dest[128], *buffer;
+    bool in_query;
+    int i, ret;
+
+    ret = irc_read_line(irc, &buffer);
+    if (ret < 0) {
+        return FALSE;
+    }
+    line = buffer;
+
+    cmd = strtok_r(line, " ", &cmd_save);
+    if (cmd && cmd[0] == ':') {
+        src = cmd + 1;
+        cmd = strtok_r(NULL, " ", &cmd_save);
+    }
+
+    if (!cmd) {
+        g_free(buffer);
+        return FALSE;
+    }
+
+    if (!strcmp(cmd, "PING")) {
+        char *pong = g_strdup_printf("PONG %s\r\n", cmd + strlen(cmd) + 1);
+        /* Ignore errors */
+        irc_send(irc, pong, strlen(pong));
+        g_free(pong);
+        return TRUE;
+    }
+
+    dest = strtok_r(NULL, " ", &cmd_save);
+    if (!dest || dest[0] == ':') {
+        g_free(buffer);
+        return FALSE;
+    }
+
+    msg = dest + strlen(dest) + 1;
+    if (msg[0] == ':') {
+        msg++;
+    } else {
+        msg = strtok_r(NULL, " ", &cmd_save);
+    }
+
+    for (i = 0; src[i] && src[i] != '!' && i < 127; i++) {
+        real_src[i] = src[i];
+    }
+    real_src[i] = 0;
+
+    for (i = 0; dest[i] && dest[i] != '!' && i < 127; i++) {
+        real_dest[i] = dest[i];
+    }
+    real_dest[i] = 0;
+
+    (void)real_src;
+
+    in_query = !strcmp(real_dest, irc->nick);
+
+    /* ignore CTCP */
+    if (!strcmp(cmd, "PRIVMSG") && msg[0] != 1 &&
+        (in_query || (!strncmp(msg, irc->nick, strlen(irc->nick)) &&
+                      !isalnum(msg[strlen(irc->nick)]))))
+    {
+        const char *start = msg;
+
+        if (!in_query) {
+            start = msg + strlen(irc->nick) + 1;
+            while (*start && isspace(*start)) {
+                start++;
+            }
+        }
+
+        qemu_chr_be_write(s, (uint8_t *)start, strlen(start));
+        qemu_chr_be_write(s, (uint8_t *)"\n", 1);
+    }
+
+    g_free(buffer);
+    return TRUE;
+}
+
+static GSource *irc_chr_add_watch(CharDriverState *s, GIOCondition cond)
+{
+    IrcCharDriverState *irc = s->opaque;
+    return g_io_create_watch(irc->chan, cond);
+}
+
+static void irc_chr_update_read_handler(CharDriverState *s)
+{
+    IrcCharDriverState *irc = s->opaque;
+    io_add_watch_poll(irc->chan, irc_chr_read_poll, irc_chr_read, s);
+}
+
+static void irc_destroy(IrcCharDriverState *irc)
+{
+    irc_send(irc, "QUIT\r\n", strlen("QUIT\r\n"));
+    g_io_channel_unref(irc->chan);
+    close(irc->fd);
+
+    g_free(irc->nick);
+    g_free(irc->channel);
+    g_free(irc->sendbuf);
+    g_free(irc->recvbuf);
+
+    g_free(irc);
+}
+
+static void irc_chr_close(CharDriverState *s)
+{
+    irc_destroy(s->opaque);
+    s->opaque = NULL;
+}
+
 QemuOpts *qemu_chr_parse_compat(const char *label, const char *filename)
 {
     char host[65], port[33], width[8], height[8];
@@ -3433,6 +3723,16 @@  QemuOpts *qemu_chr_parse_compat(const char *label, const char *filename)
         return opts;
     }
 
+    if (strstart(filename, "irc,", &p)) {
+        qemu_opts_do_parse(opts, p, NULL, &local_err);
+        if (local_err) {
+            error_report_err(local_err);
+            goto fail;
+        }
+        qemu_opt_set(opts, "backend", "irc", &error_abort);
+        return opts;
+    }
+
 fail:
     qemu_opts_del(opts);
     return NULL;
@@ -3634,6 +3934,40 @@  static void qemu_chr_parse_udp(QemuOpts *opts, ChardevBackend *backend,
     }
 }
 
+static void qemu_chr_parse_irc(QemuOpts *opts, ChardevBackend *backend,
+                               Error **errp)
+{
+    const char *host, *port, *nick, *channel;
+    SocketAddress *addr;
+
+    host    = qemu_opt_get(opts, "host");
+    port    = qemu_opt_get(opts, "port") ?: "6667";
+    nick    = qemu_opt_get(opts, "nick");
+    channel = qemu_opt_get(opts, "channel");
+
+    if (!host || !nick || !channel) {
+        error_setg(errp, "chardev: irc: Missing options");
+        return;
+    }
+
+    if (strlen(nick) > 64 || strlen(channel) > 64) {
+        error_setg(errp, "chardev: irc: Nick or channel too long");
+        return;
+    }
+
+    backend->irc = g_new0(ChardevIrc, 1);
+
+    addr = g_new0(SocketAddress, 1);
+    addr->kind = SOCKET_ADDRESS_KIND_INET;
+    addr->inet = g_new0(InetSocketAddress, 1);
+    addr->inet->host = g_strdup(host);
+    addr->inet->port = g_strdup(port);
+    backend->irc->addr = addr;
+
+    backend->irc->nick = g_strdup(nick);
+    backend->irc->channel = g_strdup(channel);
+}
+
 typedef struct CharDriver {
     const char *name;
     ChardevBackendKind kind;
@@ -3992,6 +4326,12 @@  QemuOptsList qemu_chardev_opts = {
         },{
             .name = "chardev",
             .type = QEMU_OPT_STRING,
+        },{
+            .name = "nick",
+            .type = QEMU_OPT_STRING,
+        },{
+            .name = "channel",
+            .type = QEMU_OPT_STRING,
         },
         { /* end of list */ }
     },
@@ -4206,6 +4546,154 @@  static CharDriverState *qmp_chardev_open_udp(ChardevUdp *udp,
     return qemu_chr_open_udp_fd(fd);
 }
 
+#ifdef CONFIG_GNUTLS
+static ssize_t push_all(gnutls_transport_ptr_t tptr,
+                        const void *buf, size_t len)
+{
+    return send_all((uintptr_t)tptr, buf, len);
+}
+
+static ssize_t pull_all(gnutls_transport_ptr_t tptr, void *buf, size_t len)
+{
+    return recv_all((uintptr_t)tptr, buf, len, true);
+}
+
+static int gnutls_cert_allow_all(gnutls_session_t session)
+{
+    return 0;
+}
+#endif
+
+static CharDriverState *qemu_chr_open_irc(ChardevIrc *irc, Error **errp)
+{
+    IrcCharDriverState *irc_cds;
+    CharDriverState *s;
+    Error *local_err = NULL;
+    GIOChannel *chan;
+    char *sendstr, *buffer = NULL;
+    bool joined = false, query;
+    int fd, ret;
+
+    fd = socket_connect(irc->addr, &local_err, NULL, NULL);
+    if (local_err) {
+        error_propagate(errp, local_err);
+        return NULL;
+    }
+
+#ifdef _WIN32
+    chan = g_io_channel_win32_new_socket(fd);
+#else
+    chan = g_io_channel_unix_new(fd);
+#endif
+
+    irc_cds = g_new0(IrcCharDriverState, 1);
+    irc_cds->fd      = fd;
+    irc_cds->nick    = g_strdup(irc->nick);
+    irc_cds->channel = g_strdup(irc->channel);
+
+    irc_cds->chan = chan;
+
+    irc_cds->sendbuf = g_malloc(512);
+    irc_cds->sendbuf_prefixlen = sprintf((char *)irc_cds->sendbuf,
+                                         "PRIVMSG %s :", irc_cds->channel);
+    irc_cds->sendbuf_idx = irc_cds->sendbuf_prefixlen;
+
+    irc_cds->recvbuf = g_malloc(1024);
+    irc_cds->recvbuf_idx = 0;
+
+    sendstr = g_strdup_printf("USER qemu qemu qemu qemu\r\nNICK %s\r\n",
+                              irc->nick);
+    ret = irc_send(irc_cds, sendstr, strlen(sendstr));
+    g_free(sendstr);
+    if (ret < 0) {
+        error_setg(errp, "Failed to send IRC USER+NICK");
+        goto fail;
+    }
+
+    query = irc->channel[0] != '#' && irc->channel[0] != '&' &&
+            irc->channel[0] != '+' && irc->channel[0] != '!';
+
+    while (!joined) {
+        const char *pars, *cmd, *src = NULL;
+        char *cmd_save = NULL;
+
+        ret = irc_read_line(irc_cds, &buffer);
+        if (ret < 0) {
+            continue;
+        }
+
+        cmd = strtok_r(buffer, " ", &cmd_save);
+        if (cmd && cmd[0] == ':') {
+            src = cmd + 1;
+            cmd = strtok_r(NULL, " ", &cmd_save);
+        }
+
+        if (!cmd) {
+            continue;
+        }
+
+        pars = cmd + strlen(cmd) + 1;
+
+        if (!strcmp(cmd, "PING")) {
+            sendstr = g_strdup_printf("PONG %s\r\n", pars);
+            ret = irc_send(irc_cds, sendstr, strlen(sendstr));
+            g_free(sendstr);
+            if (ret < 0) {
+                error_setg_errno(errp, errno, "Failed to send IRC PONG");
+                goto fail;
+            }
+        } else if (!strcmp(cmd, "376")) {
+            /* Ignore erros */
+            irc_send(irc_cds, "MODE qemu42 +B qemu42\r\n",
+                     strlen("MODE qemu42 +B qemu42\r\n"));
+
+            if (query) {
+                joined = true;
+            } else {
+                sendstr = g_strdup_printf("JOIN %s\r\n", irc->channel);
+                ret = irc_send(irc_cds, sendstr, strlen(sendstr));
+                g_free(sendstr);
+                if (ret < 0) {
+                    error_setg_errno(errp, errno, "Failed to send IRC JOIN");
+                    goto fail;
+                }
+            }
+        } else if (!strcmp(cmd, "JOIN")) {
+            char real_src[128];
+            int i;
+
+            for (i = 0; src[i] && src[i] != '!' && i < 127; i++) {
+                real_src[i] = src[i];
+            }
+            real_src[i] = 0;
+
+            if (!strcmp(real_src, irc->nick)) {
+                joined = true;
+            }
+        }
+
+        g_free(buffer);
+        buffer = NULL;
+    }
+
+    irc_cds->query = query;
+
+    s = qemu_chr_alloc();
+    s->chr_write = irc_chr_write;
+    s->chr_close = irc_chr_close;
+    s->chr_add_watch = irc_chr_add_watch;
+    s->chr_update_read_handler = irc_chr_update_read_handler;
+
+    s->opaque = irc_cds;
+
+    return s;
+
+
+fail:
+    irc_destroy(irc_cds);
+    return NULL;
+}
+
 ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
                                Error **errp)
 {
@@ -4289,6 +4777,9 @@  ChardevReturn *qmp_chardev_add(const char *id, ChardevBackend *backend,
     case CHARDEV_BACKEND_KIND_MEMORY:
         chr = qemu_chr_open_ringbuf(backend->ringbuf, errp);
         break;
+    case CHARDEV_BACKEND_KIND_IRC:
+        chr = qemu_chr_open_irc(backend->irc, errp);
+        break;
     default:
         error_setg(errp, "unknown chardev backend (%d)", backend->kind);
         break;
@@ -4366,6 +4857,7 @@  static void register_types(void)
     /* Bug-compatibility: */
     register_char_driver("memory", CHARDEV_BACKEND_KIND_MEMORY,
                          qemu_chr_parse_ringbuf);
+    register_char_driver("irc", CHARDEV_BACKEND_KIND_IRC, qemu_chr_parse_irc);
     /* this must be done after machine init, since we register FEs with muxes
      * as part of realize functions like serial_isa_realizefn when -nographic
      * is specified