From patchwork Wed Apr 1 12:09:13 2015 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Max Reitz X-Patchwork-Id: 457240 Return-Path: X-Original-To: incoming@patchwork.ozlabs.org Delivered-To: patchwork-incoming@bilbo.ozlabs.org Received: from lists.gnu.org (lists.gnu.org [IPv6:2001:4830:134:3::11]) (using TLSv1 with cipher AES256-SHA (256/256 bits)) (No client certificate requested) by ozlabs.org (Postfix) with ESMTPS id 440241400DE for ; Wed, 1 Apr 2015 23:10:03 +1100 (AEDT) Received: from localhost ([::1]:52066 helo=lists.gnu.org) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1YdHT6-0004UV-Ql for incoming@patchwork.ozlabs.org; Wed, 01 Apr 2015 08:10:00 -0400 Received: from eggs.gnu.org ([2001:4830:134:3::10]:35311) by lists.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1YdHSY-0003Yn-BR for qemu-devel@nongnu.org; Wed, 01 Apr 2015 08:09:28 -0400 Received: from Debian-exim by eggs.gnu.org with spam-scanned (Exim 4.71) (envelope-from ) id 1YdHSW-0000ae-3F for qemu-devel@nongnu.org; Wed, 01 Apr 2015 08:09:26 -0400 Received: from mx1.redhat.com ([209.132.183.28]:41101) by eggs.gnu.org with esmtp (Exim 4.71) (envelope-from ) id 1YdHSV-0000aP-QU for qemu-devel@nongnu.org; Wed, 01 Apr 2015 08:09:24 -0400 Received: from int-mx09.intmail.prod.int.phx2.redhat.com (int-mx09.intmail.prod.int.phx2.redhat.com [10.5.11.22]) by mx1.redhat.com (Postfix) with ESMTPS id 902AD8F312 for ; Wed, 1 Apr 2015 12:09:23 +0000 (UTC) Received: from localhost (ovpn-116-114.ams2.redhat.com [10.36.116.114]) by int-mx09.intmail.prod.int.phx2.redhat.com (8.14.4/8.14.4) with ESMTP id t31C9KlV004570 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-GCM-SHA384 bits=256 verify=NO); Wed, 1 Apr 2015 08:09:22 -0400 From: Max Reitz To: qemu-devel@nongnu.org Date: Wed, 1 Apr 2015 14:09:13 +0200 Message-Id: <1427890157-18639-2-git-send-email-mreitz@redhat.com> In-Reply-To: <1427890157-18639-1-git-send-email-mreitz@redhat.com> References: <1427890157-18639-1-git-send-email-mreitz@redhat.com> X-Scanned-By: MIMEDefang 2.68 on 10.5.11.22 X-detected-operating-system: by eggs.gnu.org: GNU/Linux 3.x X-Received-From: 209.132.183.28 Cc: Kevin Wolf , Paolo Bonzini , Markus Armbruster , Max Reitz Subject: [Qemu-devel] [PATCH for-2.4 1/5] chardev: Add IRC char driver X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.14 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org Sender: qemu-devel-bounces+incoming=patchwork.ozlabs.org@nongnu.org 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 --- qapi-schema.json | 16 ++ qemu-char.c | 492 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 508 insertions(+) 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