aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGerd Hoffmann <kraxel@redhat.com>2012-08-15 16:49:39 +0200
committerGerd Hoffmann <kraxel@redhat.com>2012-08-15 16:49:39 +0200
commitdaf23c350f0fe8fffbbde331d43460c517d3e445 (patch)
tree8afe6a21c38ffb961b7cbba89107d3f996e6d02e
downloadvconsole-daf23c350f0fe8fffbbde331d43460c517d3e445.tar.gz
initial commit
-rw-r--r--.gitignore4
-rw-r--r--GNUmakefile68
-rw-r--r--README45
-rw-r--r--connect.c74
-rw-r--r--domain.c331
-rw-r--r--libvirt-glib-event.c481
-rw-r--r--libvirt-glib-event.h35
-rw-r--r--mk/Autoconf.mk168
-rw-r--r--mk/Compile.mk102
-rw-r--r--mk/Maintainer.mk30
-rw-r--r--mk/Variables.mk55
-rw-r--r--vconsole.c590
-rw-r--r--vconsole.h82
13 files changed, 2065 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ef694cb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+Make.config
+vconsole
+*.o
+*.dep
diff --git a/GNUmakefile b/GNUmakefile
new file mode 100644
index 0000000..c1c2411
--- /dev/null
+++ b/GNUmakefile
@@ -0,0 +1,68 @@
+# config
+-include Make.config
+include mk/Variables.mk
+
+# add our flags + libs
+CFLAGS += -DVERSION='"$(VERSION)"' -DLIB='"$(LIB)"'
+
+# build
+TARGETS := vconsole
+
+# default target
+all: build
+
+#################################################################
+# poor man's autoconf ;-)
+
+include mk/Autoconf.mk
+
+define make-config
+LIB := $(LIB)
+HAVE_GLIB := $(call ac_pkg_config,glib-2.0)
+HAVE_GTHREAD := $(call ac_pkg_config,gthread-2.0)
+HAVE_GTK := $(call ac_pkg_config,gtk+-2.0)
+HAVE_VTE := $(call ac_pkg_config,vte)
+HAVE_LIBVIRT := $(call ac_pkg_config,libvirt)
+endef
+
+pkglst := glib-2.0 gthread-2.0 gtk+-2.0 vte libvirt
+
+CFLAGS += -Wno-strict-prototypes
+CFLAGS += $(shell test "$(pkglst)" != "" && pkg-config --cflags $(pkglst))
+LDLIBS += $(shell test "$(pkglst)" != "" && pkg-config --libs $(pkglst))
+
+# desktop files
+DESKTOP := $(wildcard $(patsubst %,%.desktop,$(TARGETS)))
+
+
+########################################################################
+# rules
+
+ifeq ($(HAVE_GLIB)-$(HAVE_GTHREAD)-$(HAVE_GTK)-$(HAVE_VTE)-$(HAVE_LIBVIRT),yes-yes-yes-yes-yes)
+build: $(TARGETS)
+else
+build:
+ @echo "build dependencies are missing"
+ @false
+endif
+
+install: build
+ $(INSTALL_DIR) $(DESTDIR)/etc $(bindir) $(mandir)/man1
+ $(INSTALL_BINARY) $(TARGETS) $(bindir)
+ $(INSTALL_DATA) vconsole.man $(mandir)/man1/vconsole.1
+# $(INSTALL_DATA) $(DESKTOP) $(appdir)
+
+clean:
+ -rm -f *.o *~ $(depfiles)
+
+realclean distclean: clean
+ -rm -f Make.config
+ -rm -f $(TARGETS) *~ *.bak
+
+#############################################
+
+vconsole: vconsole.o connect.o domain.o libvirt-glib-event.o
+
+include mk/Compile.mk
+include mk/Maintainer.mk
+-include $(depfiles)
diff --git a/README b/README
new file mode 100644
index 0000000..f4de871
--- /dev/null
+++ b/README
@@ -0,0 +1,45 @@
+
+What is vconsole
+================
+
+This is a virtual machine console, with a strong focus on guests using
+a serial console. Features a tabbed GUI, one tab for the VM list,
+then one per guest console. It build on top of libvirt.
+
+
+Using vconsole
+==============
+
+Just start it. It comes up with a GUI which should be mostly
+self-explanatory. If you have VIRSH_DEFAULT_CONNECT_URI set it should
+pick it up automatically. If it comes up empty try passing the
+libvirt uri using the '-c' switch.
+
+Hit 'Enter' on a host line to expland/collapse the guest list.
+Hit 'Enter' on a guest line to open a guest (text) console tab for it.
+
+Guest console tabs stay open until you explicitly close them using the
+'close tab' file menu entry. When a guest starts the console will be
+(re-)connected automatically.
+
+Typing into a guest console for a guest not running will start it
+(this is probably temporary until we have more fancy gui controls for
+that).
+
+
+Known issues / TODO list
+========================
+
+Dialog asking for a libvirt uri isn't implemented yet, so you have to
+use the -c command line switch. vconsole will remember successful
+connects though and offer the hosts in a menu, so you have to do that
+only once per host.
+
+vconsole doesn't handle undefining domains yet.
+
+vconsole doesn't handle connection drops (caused by libvirtd restart
+for example) yet.
+
+vconsole lacks a .desktop file and manual page.
+
+Most likely there are bugs not listed here.
diff --git a/connect.c b/connect.c
new file mode 100644
index 0000000..d55eae5
--- /dev/null
+++ b/connect.c
@@ -0,0 +1,74 @@
+#include "vconsole.h"
+
+/* ------------------------------------------------------------------ */
+
+static int connect_domain_event(virConnectPtr c, virDomainPtr d,
+ int event, int detail, void *opaque)
+{
+ struct vconsole_connect *conn = opaque;
+
+ if (debug)
+ fprintf(stderr, "%s: %s, event %d\n", __func__,
+ virDomainGetName(d), event);
+ domain_update(conn, d, event);
+ return 0;
+}
+
+static void connect_list(struct vconsole_connect *conn)
+{
+ int i, n;
+ char **inactive;
+ int *active;
+
+ n = virConnectNumOfDomains(conn->ptr);
+ active = malloc(sizeof(int) * n);
+ n = virConnectListDomains(conn->ptr, active, n);
+ for (i = 0; i < n; i++) {
+ domain_update(conn, virDomainLookupByID(conn->ptr, active[i]), -1);
+ }
+ free(active);
+
+ n = virConnectNumOfDefinedDomains(conn->ptr);
+ inactive = malloc(sizeof(char *) * n);
+ n = virConnectListDefinedDomains(conn->ptr, inactive, n);
+ for (i = 0; i < n; i++) {
+ domain_update(conn, virDomainLookupByName(conn->ptr, inactive[i]), -1);
+ free(inactive[i]);
+ }
+ free(inactive);
+}
+
+struct vconsole_connect *connect_init(struct vconsole_window *win,
+ const char *uri)
+{
+ struct vconsole_connect *conn;
+ GtkTreeIter iter;
+ char *name;
+
+ conn = g_new0(struct vconsole_connect, 1);
+ conn->ptr = virConnectOpen(uri);
+ name = virConnectGetHostname(conn->ptr);
+ if (conn->ptr == NULL) {
+ fprintf(stderr, "Failed to open connection to %s\n", uri);
+ g_free(conn);
+ return NULL;
+ }
+ conn->win = win;
+ virConnectDomainEventRegister(conn->ptr, connect_domain_event,
+ conn, NULL);
+
+ gtk_tree_store_append(win->store, &iter, NULL);
+ gtk_tree_store_set(win->store, &iter,
+ CPTR_COL, conn,
+ NAME_COL, name,
+ URI_COL, uri,
+ -1);
+
+ if (debug)
+ fprintf(stderr, "%s: %s\n", __func__, uri);
+ g_key_file_set_string(config, "hosts", name, uri);
+ config_write();
+ connect_list(conn);
+
+ return conn;
+}
diff --git a/domain.c b/domain.c
new file mode 100644
index 0000000..465aded
--- /dev/null
+++ b/domain.c
@@ -0,0 +1,331 @@
+#include "vconsole.h"
+
+/* ------------------------------------------------------------------ */
+
+static const char *state_name[] = {
+ [ VIR_DOMAIN_NOSTATE ] = "-",
+ [ VIR_DOMAIN_RUNNING ] = "running",
+ [ VIR_DOMAIN_BLOCKED ] = "blocked",
+ [ VIR_DOMAIN_PAUSED ] = "paused",
+ [ VIR_DOMAIN_SHUTDOWN ] = "shutdown",
+ [ VIR_DOMAIN_SHUTOFF ] = "shutoff",
+ [ VIR_DOMAIN_CRASHED ] = "crashed",
+ [ VIR_DOMAIN_PMSUSPENDED ] = "suspended",
+};
+
+static const char *domain_state_name(struct vconsole_domain *dom)
+{
+ if (dom->info.state < sizeof(state_name)/sizeof(state_name[0]))
+ return state_name[dom->info.state];
+ return "-?-";
+}
+
+/* ------------------------------------------------------------------ */
+
+static void domain_configure_vte(struct vconsole_domain *dom)
+{
+ struct vconsole_window *win = dom->conn->win;
+ VteTerminal *vte = VTE_TERMINAL(dom->vte);
+ VteTerminalCursorBlinkMode bl =
+ win->tty_blink ? VTE_CURSOR_BLINK_ON : VTE_CURSOR_BLINK_OFF;
+ GdkColor fg = {0,0,0,0};
+ GdkColor bg = {0,0,0,0};
+
+ gdk_color_parse(win->tty_fg, &fg);
+ gdk_color_parse(win->tty_bg, &bg);
+
+ vte_terminal_set_font_from_string(vte, win->tty_font);
+ vte_terminal_set_cursor_blink_mode(vte, bl);
+ vte_terminal_set_color_foreground(vte, &fg);
+ vte_terminal_set_color_background(vte, &bg);
+}
+
+void domain_configure_all_vtes(struct vconsole_window *win)
+{
+ GtkTreeModel *model = GTK_TREE_MODEL(win->store);
+ GtkTreeIter host, guest;
+ struct vconsole_domain *dom;
+ int rc;
+
+ rc = gtk_tree_model_get_iter_first(model, &host);
+ while (rc) {
+ rc = gtk_tree_model_iter_nth_child(model, &guest, &host, 0);
+ while (rc) {
+ gtk_tree_model_get(model, &guest,
+ DPTR_COL, &dom,
+ -1);
+ if (dom->vte)
+ domain_configure_vte(dom);
+ rc = gtk_tree_model_iter_next(model, &guest);
+ }
+ rc = gtk_tree_model_iter_next(model, &host);
+ }
+}
+
+static void domain_update_status(struct vconsole_domain *dom)
+{
+ char line[128];
+
+ if (!dom->status)
+ return;
+ snprintf(line, sizeof(line), "%s%s",
+ domain_state_name(dom),
+ dom->stream ? ", connected" : "");
+ gtk_label_set_text(GTK_LABEL(dom->status), line);
+}
+
+static void domain_disconnect(struct vconsole_domain *dom, virDomainPtr d)
+{
+ if (!dom->stream)
+ return;
+
+ if (debug)
+ fprintf(stderr, "%s: %s\n", __func__, virDomainGetName(d));
+ virStreamEventRemoveCallback(dom->stream);
+ virStreamFree(dom->stream);
+ dom->stream = NULL;
+ domain_update_status(dom);
+}
+
+static void domain_console_event(virStreamPtr stream, int events, void *opaque)
+{
+ struct vconsole_domain *dom = opaque;
+ virDomainPtr d = virDomainLookupByUUIDString(dom->conn->ptr, dom->uuid);
+ const char *name = virDomainGetName(d);
+ char buf[128];
+ int rc, bytes = 0;
+
+ if (events & VIR_STREAM_EVENT_READABLE) {
+ for (;;) {
+ rc = virStreamRecv(stream, buf, sizeof(buf));
+ if (rc <= 0)
+ break;
+ bytes += rc;
+ if (dom->vte)
+ vte_terminal_feed(VTE_TERMINAL(dom->vte), buf, rc);
+ }
+ if (bytes == 0) {
+ if (debug)
+ fprintf(stderr, "%s: %s eof\n", __func__, name);
+ domain_disconnect(dom, d);
+ }
+ }
+ if (events & VIR_STREAM_EVENT_HANGUP) {
+ if (debug)
+ fprintf(stderr, "%s: %s hangup\n", __func__, name);
+ domain_disconnect(dom, d);
+ }
+}
+
+static void domain_user_input(VteTerminal *vte, gchar *buf, guint len,
+ gpointer opaque)
+{
+ struct vconsole_domain *dom = opaque;
+ virDomainPtr d;
+
+ if (dom->stream) {
+ virStreamSend(dom->stream, buf, len);
+ return;
+ }
+ if (dom->info.state == VIR_DOMAIN_SHUTOFF) {
+ d = virDomainLookupByUUIDString(dom->conn->ptr, dom->uuid);
+ virDomainCreate(d);
+ }
+}
+
+static void domain_connect(struct vconsole_domain *dom, virDomainPtr d)
+{
+ int rc;
+
+ if (dom->stream)
+ return;
+
+ dom->stream = virStreamNew(dom->conn->ptr,
+ VIR_STREAM_NONBLOCK);
+ rc = virDomainOpenConsole(d, NULL, dom->stream,
+ VIR_DOMAIN_CONSOLE_FORCE);
+ if (rc < 0) {
+ if (debug)
+ fprintf(stderr, "%s: %s failed\n", __func__, virDomainGetName(d));
+ virStreamFree(dom->stream);
+ dom->stream = NULL;
+ return;
+ }
+
+ virStreamEventAddCallback(dom->stream,
+ VIR_STREAM_EVENT_READABLE |
+ VIR_STREAM_EVENT_HANGUP,
+ domain_console_event, dom, NULL);
+ if (debug)
+ fprintf(stderr, "%s: %s ok\n", __func__, virDomainGetName(d));
+ domain_update_status(dom);
+}
+
+/* ------------------------------------------------------------------ */
+
+void domain_update(struct vconsole_connect *conn,
+ virDomainPtr d, virDomainEventType event)
+{
+ GtkTreeModel *model = GTK_TREE_MODEL(conn->win->store);
+ GtkTreeIter host, guest;
+ struct vconsole_domain *dom = NULL;
+ void *ptr;
+ gboolean rc;
+ const char *name;
+ char uuid[VIR_UUID_STRING_BUFLEN];
+ char idstr[16];
+ int id;
+
+ /* find host */
+ rc = gtk_tree_model_get_iter_first(model, &host);
+ while (rc) {
+ gtk_tree_model_get(model, &host,
+ CPTR_COL, &ptr,
+ -1);
+ if (ptr == conn)
+ break;
+ rc = gtk_tree_model_iter_next(model, &host);
+ }
+ assert(ptr == conn);
+
+ /* find guest */
+ virDomainGetUUIDString(d, uuid);
+ rc = gtk_tree_model_iter_nth_child(model, &guest, &host, 0);
+ while (rc) {
+ gtk_tree_model_get(model, &guest,
+ DPTR_COL, &dom,
+ -1);
+ if (strcmp(uuid, dom->uuid) == 0)
+ break;
+ dom = NULL;
+ rc = gtk_tree_model_iter_next(model, &guest);
+ }
+
+ /* no guest found -> create new */
+ if (dom == NULL) {
+ dom = g_new0(struct vconsole_domain, 1);
+ dom->conn = conn;
+ virDomainGetUUIDString(d, dom->uuid);
+ gtk_tree_store_append(conn->win->store, &guest, &host);
+ gtk_tree_store_set(conn->win->store, &guest,
+ DPTR_COL, dom, -1);
+ }
+
+ /* handle events */
+ switch (event) {
+ case VIR_DOMAIN_EVENT_UNDEFINED:
+ fprintf(stderr, "%s: undefined %s [ FIXME ]\n", __func__,
+ virDomainGetName(d));
+ break;
+ case VIR_DOMAIN_EVENT_STARTED:
+ domain_connect(dom, d);
+ break;
+ case VIR_DOMAIN_EVENT_STOPPED:
+ domain_disconnect(dom, d);
+ break;
+ default:
+ break;
+ }
+
+ /* update guest info */
+ name = virDomainGetName(d);
+ id = virDomainGetID(d);
+ if (id < 0)
+ strcpy(idstr, "-");
+ else
+ snprintf(idstr, sizeof(idstr), "%d", id);
+ virDomainGetInfo(d, &dom->info);
+ gtk_tree_store_set(conn->win->store, &guest,
+ NAME_COL, name,
+ ID_COL, idstr,
+ STATE_COL, domain_state_name(dom),
+ -1);
+ domain_update_status(dom);
+}
+
+void domain_activate(struct vconsole_domain *dom)
+{
+ virDomainPtr d = virDomainLookupByUUIDString(dom->conn->ptr, dom->uuid);
+ struct vconsole_window *win = dom->conn->win;
+ GtkWidget *label, *fstatus;
+ const char *name;
+
+ if (dom->vte) {
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), dom->page);
+ } else {
+ name = virDomainGetName(d);
+ if (debug)
+ fprintf(stderr, "new tab: %s\n", name);
+
+ dom->vte = vte_terminal_new();
+ g_signal_connect(dom->vte, "commit",
+ G_CALLBACK(domain_user_input), dom);
+
+ dom->status = gtk_label_new("-");
+ gtk_misc_set_alignment(GTK_MISC(dom->status), 0, 0.5);
+ gtk_misc_set_padding(GTK_MISC(dom->status), 3, 1);
+
+ dom->vbox = gtk_vbox_new(FALSE, 1);
+ gtk_container_set_border_width(GTK_CONTAINER(dom->vbox), 1);
+ gtk_box_pack_start(GTK_BOX(dom->vbox), dom->vte, TRUE, TRUE, 0);
+ fstatus = gtk_frame_new(NULL);
+ gtk_box_pack_end(GTK_BOX(dom->vbox), fstatus, FALSE, TRUE, 0);
+ gtk_container_add(GTK_CONTAINER(fstatus), dom->status);
+
+ label = gtk_label_new(name);
+ dom->page = gtk_notebook_insert_page(GTK_NOTEBOOK(win->notebook),
+ dom->vbox, label, -1);
+ gtk_widget_show_all(dom->vbox);
+ gtk_notebook_set_current_page(GTK_NOTEBOOK(win->notebook), dom->page);
+ domain_configure_vte(dom);
+ domain_update_status(dom);
+ }
+
+ domain_connect(dom, d);
+}
+
+static void domain_close_tab(struct vconsole_domain *dom)
+{
+ virDomainPtr d = virDomainLookupByUUIDString(dom->conn->ptr, dom->uuid);
+
+ domain_disconnect(dom, d);
+ gtk_notebook_remove_page(GTK_NOTEBOOK(dom->conn->win->notebook), dom->page);
+ dom->vbox = NULL;
+ dom->vte = NULL;
+ dom->status = NULL;
+}
+
+static struct vconsole_domain *domain_find_current_tab(struct vconsole_window *win)
+{
+ GtkTreeModel *model = GTK_TREE_MODEL(win->store);
+ GtkTreeIter host, guest;
+ struct vconsole_domain *dom;
+ int rc, page;
+
+ page = gtk_notebook_get_current_page(GTK_NOTEBOOK(win->notebook));
+ rc = gtk_tree_model_get_iter_first(model, &host);
+ while (rc) {
+ rc = gtk_tree_model_iter_nth_child(model, &guest, &host, 0);
+ while (rc) {
+ gtk_tree_model_get(model, &guest,
+ DPTR_COL, &dom,
+ -1);
+ if (dom->vbox && dom->page == page) {
+ return dom;
+ }
+ rc = gtk_tree_model_iter_next(model, &guest);
+ }
+ rc = gtk_tree_model_iter_next(model, &host);
+ }
+ return NULL;
+}
+
+void domain_close_current_tab(struct vconsole_window *win)
+{
+ struct vconsole_domain *dom;
+
+ dom = domain_find_current_tab(win);
+ if (dom)
+ domain_close_tab(dom);
+}
+
diff --git a/libvirt-glib-event.c b/libvirt-glib-event.c
new file mode 100644
index 0000000..7411c9b
--- /dev/null
+++ b/libvirt-glib-event.c
@@ -0,0 +1,481 @@
+/*
+ * libvirt-glib-event.c: libvirt glib integration
+ *
+ * Copyright (C) 2008 Daniel P. Berrange
+ * Copyright (C) 2010-2011 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: Daniel P. Berrange <berrange@redhat.com>
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include <libvirt/libvirt.h>
+
+#include "libvirt-glib-event.h"
+
+/**
+ * SECTION:libvirt-glib-event
+ * @short_description: Integrate libvirt with the GMain event framework
+ * @title: Event loop
+ * @stability: Stable
+ * @include: libvirt-glib/libvirt-glib.h
+ *
+ * The libvirt API has the ability to provide applications with asynchronous
+ * notifications of interesting events. To enable this functionality though,
+ * applications must provide libvirt with an event loop implementation. The
+ * libvirt-glib API provides such an implementation, which naturally integrates
+ * with the GMain event loop framework.
+ *
+ * To enable use of the GMain event loop glue, the <code>gvir_event_register()</code>
+ * should be invoked. Once this is done, it is mandatory to have the default
+ * GMain event loop run by a thread in the application, usually the primary
+ * thread, eg by using <code>gtk_main()</code> or <code>g_application_run()</code>
+ *
+ * <example>
+ * <title>Registering for events with a GTK application</title>
+ * <programlisting><![CDATA[
+ * int main(int argc, char **argv) {
+ * ...setup...
+ * gvir_event_register();
+ * ...more setup...
+ * gtk_main();
+ * return 0;
+ * }
+ * ]]></programlisting>
+ * </example>
+ *
+ * <example>
+ * <title>Registering for events using Appplication</title>
+ * <programlisting><![CDATA[
+ * int main(int argc, char **argv) {
+ * ...setup...
+ * GApplication *app = ...create some impl of GApplication...
+ * gvir_event_register();
+ * ...more setup...
+ * g_application_run(app);
+ * return 0;
+ * }
+ * ]]></programlisting>
+ * </example>
+ */
+
+
+#if GLIB_CHECK_VERSION(2, 31, 0)
+#define g_mutex_new() g_new0(GMutex, 1)
+#endif
+
+struct gvir_event_handle
+{
+ int watch;
+ int fd;
+ int events;
+ int removed;
+ GIOChannel *channel;
+ guint source;
+ virEventHandleCallback cb;
+ void *opaque;
+ virFreeCallback ff;
+};
+
+struct gvir_event_timeout
+{
+ int timer;
+ int interval;
+ int removed;
+ guint source;
+ virEventTimeoutCallback cb;
+ void *opaque;
+ virFreeCallback ff;
+};
+
+GMutex *eventlock = NULL;
+
+static int nextwatch = 1;
+static GPtrArray *handles;
+
+static int nexttimer = 1;
+static GPtrArray *timeouts;
+
+static gboolean
+gvir_event_handle_dispatch(GIOChannel *source G_GNUC_UNUSED,
+ GIOCondition condition,
+ gpointer opaque)
+{
+ struct gvir_event_handle *data = opaque;
+ int events = 0;
+
+ if (condition & G_IO_IN)
+ events |= VIR_EVENT_HANDLE_READABLE;
+ if (condition & G_IO_OUT)
+ events |= VIR_EVENT_HANDLE_WRITABLE;
+ if (condition & G_IO_HUP)
+ events |= VIR_EVENT_HANDLE_HANGUP;
+ if (condition & G_IO_ERR)
+ events |= VIR_EVENT_HANDLE_ERROR;
+
+ (data->cb)(data->watch, data->fd, events, data->opaque);
+
+ return TRUE;
+}
+
+
+static int
+gvir_event_handle_add(int fd,
+ int events,
+ virEventHandleCallback cb,
+ void *opaque,
+ virFreeCallback ff)
+{
+ struct gvir_event_handle *data;
+ GIOCondition cond = 0;
+ int ret;
+
+ g_mutex_lock(eventlock);
+
+ data = g_new0(struct gvir_event_handle, 1);
+
+ if (events & VIR_EVENT_HANDLE_READABLE)
+ cond |= G_IO_IN;
+ if (events & VIR_EVENT_HANDLE_WRITABLE)
+ cond |= G_IO_OUT;
+
+ data->watch = nextwatch++;
+ data->fd = fd;
+ data->events = events;
+ data->cb = cb;
+ data->opaque = opaque;
+ data->channel = g_io_channel_unix_new(fd);
+ data->ff = ff;
+
+ data->source = g_io_add_watch(data->channel,
+ cond,
+ gvir_event_handle_dispatch,
+ data);
+
+ g_ptr_array_add(handles, data);
+
+ ret = data->watch;
+
+ g_mutex_unlock(eventlock);
+
+ return ret;
+}
+
+static struct gvir_event_handle *
+gvir_event_handle_find(int watch)
+{
+ guint i;
+
+ for (i = 0 ; i < handles->len ; i++) {
+ struct gvir_event_handle *h = g_ptr_array_index(handles, i);
+
+ if (h == NULL) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ if ((h->watch == watch) && !h->removed) {
+ return h;
+ }
+ }
+
+ return NULL;
+}
+
+static void
+gvir_event_handle_update(int watch,
+ int events)
+{
+ struct gvir_event_handle *data;
+
+ g_mutex_lock(eventlock);
+
+ data = gvir_event_handle_find(watch);
+ if (!data) {
+ goto cleanup;
+ }
+
+ if (events) {
+ GIOCondition cond = 0;
+ if (events == data->events)
+ goto cleanup;
+
+ if (data->source)
+ g_source_remove(data->source);
+
+ cond |= G_IO_HUP;
+ if (events & VIR_EVENT_HANDLE_READABLE)
+ cond |= G_IO_IN;
+ if (events & VIR_EVENT_HANDLE_WRITABLE)
+ cond |= G_IO_OUT;
+ data->source = g_io_add_watch(data->channel,
+ cond,
+ gvir_event_handle_dispatch,
+ data);
+ data->events = events;
+ } else {
+ if (!data->source)
+ goto cleanup;
+
+ g_source_remove(data->source);
+ data->source = 0;
+ data->events = 0;
+ }
+
+cleanup:
+ g_mutex_unlock(eventlock);
+}
+
+static gboolean
+_event_handle_remove(gpointer data)
+{
+ struct gvir_event_handle *h = data;
+
+ g_mutex_lock(eventlock);
+
+ if (h->ff)
+ (h->ff)(h->opaque);
+
+ g_ptr_array_remove_fast(handles, h);
+
+ g_mutex_unlock(eventlock);
+
+ return FALSE;
+}
+
+static int
+gvir_event_handle_remove(int watch)
+{
+ struct gvir_event_handle *data;
+ int ret = -1;
+
+ g_mutex_lock(eventlock);
+
+ data = gvir_event_handle_find(watch);
+ if (!data) {
+ goto cleanup;
+ }
+
+ if (!data->source)
+ goto cleanup;
+
+ g_source_remove(data->source);
+ data->source = 0;
+ data->events = 0;
+ /* since the actual watch deletion is done asynchronously, a handle_update call may
+ * reschedule the watch before it's fully deleted, that's why we need to mark it as
+ * 'removed' to prevent reuse
+ */
+ data->removed = TRUE;
+ g_idle_add(_event_handle_remove, data);
+
+ ret = 0;
+
+cleanup:
+ g_mutex_unlock(eventlock);
+ return ret;
+}
+
+
+static gboolean
+gvir_event_timeout_dispatch(void *opaque)
+{
+ struct gvir_event_timeout *data = opaque;
+ (data->cb)(data->timer, data->opaque);
+
+ return TRUE;
+}
+
+static int
+gvir_event_timeout_add(int interval,
+ virEventTimeoutCallback cb,
+ void *opaque,
+ virFreeCallback ff)
+{
+ struct gvir_event_timeout *data;
+ int ret;
+
+ g_mutex_lock(eventlock);
+
+ data = g_new0(struct gvir_event_timeout, 1);
+ data->timer = nexttimer++;
+ data->interval = interval;
+ data->cb = cb;
+ data->opaque = opaque;
+ data->ff = ff;
+ if (interval >= 0)
+ data->source = g_timeout_add(interval,
+ gvir_event_timeout_dispatch,
+ data);
+
+ g_ptr_array_add(timeouts, data);
+
+ ret = data->timer;
+
+ g_mutex_unlock(eventlock);
+
+ return ret;
+}
+
+
+static struct gvir_event_timeout *
+gvir_event_timeout_find(int timer)
+{
+ guint i;
+
+ g_return_val_if_fail(timeouts != NULL, NULL);
+
+ for (i = 0 ; i < timeouts->len ; i++) {
+ struct gvir_event_timeout *t = g_ptr_array_index(timeouts, i);
+
+ if (t == NULL) {
+ g_warn_if_reached ();
+ continue;
+ }
+
+ if ((t->timer == timer) && !t->removed) {
+ return t;
+ }
+ }
+
+ return NULL;
+}
+
+
+static void
+gvir_event_timeout_update(int timer,
+ int interval)
+{
+ struct gvir_event_timeout *data;
+
+ g_mutex_lock(eventlock);
+
+ data = gvir_event_timeout_find(timer);
+ if (!data) {
+ goto cleanup;
+ }
+
+ if (interval >= 0) {
+ if (data->source)
+ g_source_remove(data->source);
+
+ data->interval = interval;
+ data->source = g_timeout_add(data->interval,
+ gvir_event_timeout_dispatch,
+ data);
+ } else {
+ if (!data->source)
+ goto cleanup;
+
+ g_source_remove(data->source);
+ data->source = 0;
+ }
+
+cleanup:
+ g_mutex_unlock(eventlock);
+}
+
+static gboolean
+_event_timeout_remove(gpointer data)
+{
+ struct gvir_event_timeout *t = data;
+
+ g_mutex_lock(eventlock);
+
+ if (t->ff)
+ (t->ff)(t->opaque);
+
+ g_ptr_array_remove_fast(timeouts, t);
+
+ g_mutex_unlock(eventlock);
+
+ return FALSE;
+}
+
+static int
+gvir_event_timeout_remove(int timer)
+{
+ struct gvir_event_timeout *data;
+ int ret = -1;
+
+ g_mutex_lock(eventlock);
+
+ data = gvir_event_timeout_find(timer);
+ if (!data) {
+ goto cleanup;
+ }
+
+ if (!data->source)
+ goto cleanup;
+
+ g_source_remove(data->source);
+ data->source = 0;
+ /* since the actual timeout deletion is done asynchronously, a timeout_update call may
+ * reschedule the timeout before it's fully deleted, that's why we need to mark it as
+ * 'removed' to prevent reuse
+ */
+ data->removed = TRUE;
+ g_idle_add(_event_timeout_remove, data);
+
+ ret = 0;
+
+cleanup:
+ g_mutex_unlock(eventlock);
+ return ret;
+}
+
+
+static gpointer event_register_once(gpointer data G_GNUC_UNUSED)
+{
+ eventlock = g_mutex_new();
+ timeouts = g_ptr_array_new_with_free_func(g_free);
+ handles = g_ptr_array_new_with_free_func(g_free);
+ virEventRegisterImpl(gvir_event_handle_add,
+ gvir_event_handle_update,
+ gvir_event_handle_remove,
+ gvir_event_timeout_add,
+ gvir_event_timeout_update,
+ gvir_event_timeout_remove);
+ return NULL;
+}
+
+
+/**
+ * gvir_event_register:
+ *
+ * Registers a libvirt event loop implementation that is backed
+ * by the default <code>GMain</code> context. If invoked more
+ * than once this method will be a no-op. Applications should,
+ * however, take care not to register any another non-GLib
+ * event loop with libvirt.
+ *
+ * After invoking this method, it is mandatory to run the
+ * default GMain event loop. Typically this can be satisfied
+ * by invoking <code>gtk_main</code> or <code>g_application_run</code>
+ * in the application's main thread. Failure to run the event
+ * loop will mean no libvirt events get dispatched, and the
+ * libvirt keepalive timer will kill off libvirt connections
+ * frequently.
+ */
+void gvir_event_register(void)
+{
+ static GOnce once = G_ONCE_INIT;
+
+ g_once(&once, event_register_once, NULL);
+}
diff --git a/libvirt-glib-event.h b/libvirt-glib-event.h
new file mode 100644
index 0000000..57cadab
--- /dev/null
+++ b/libvirt-glib-event.h
@@ -0,0 +1,35 @@
+/*
+ * libvirt-glib-event.h: libvirt glib integration
+ *
+ * Copyright (C) 2008 Daniel P. Berrange
+ * Copyright (C) 2010-2011 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ *
+ * Author: Daniel P. Berrange <berrange@redhat.com>
+ */
+
+#ifndef __LIBVIRT_GLIB_EVENT_H__
+#define __LIBVIRT_GLIB_EVENT_H__
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+void gvir_event_register(void);
+
+G_END_DECLS
+
+#endif /* __LIBVIRT_GLIB_EVENT_H__ */
diff --git a/mk/Autoconf.mk b/mk/Autoconf.mk
new file mode 100644
index 0000000..7608ea5
--- /dev/null
+++ b/mk/Autoconf.mk
@@ -0,0 +1,168 @@
+#
+# simple autoconf system for GNU make
+#
+# (c) 2002-2006 Gerd Hoffmann <kraxel@suse.de>
+#
+# credits for creating this one go to the autotools people because
+# they managed it to annoy lots of developers and users (including
+# me) with version incompatibilities.
+#
+# This file is public domain. No warranty. If it breaks you keep
+# both pieces.
+#
+########################################################################
+
+# verbose yes/no
+verbose ?= no
+
+# some stuff used by the tests
+ifneq ($(verbose),no)
+ # verbose (for debug)
+ ac_init = echo "checking $(1) ... " >&2; rc=no
+ ac_b_cmd = echo "run: $(1)" >&2; $(1) >/dev/null && rc=yes
+ ac_s_cmd = echo "run: $(1)" >&2; rc=`$(1)`
+ ac_fini = echo "... result is $${rc}" >&2; echo >&2; echo "$${rc}"
+else
+ # normal
+ ac_init = echo -n "checking $(1) ... " >&2; rc=no
+ ac_b_cmd = $(1) >/dev/null 2>&1 && rc=yes
+ ac_s_cmd = rc=`$(1) 2>/dev/null`
+ ac_fini = echo "$${rc}" >&2; echo "$${rc}"
+endif
+
+# some helpers to build cflags and related variables
+ac_def_cflags_1 = $(if $(filter yes,$($(1))),-D$(1))
+ac_lib_cflags = $(foreach lib,$(1),$(call ac_def_cflags_1,HAVE_LIB$(lib)))
+ac_inc_cflags = $(foreach inc,$(1),$(call ac_def_cflags_1,HAVE_$(inc)))
+ac_lib_mkvar_1 = $(if $(filter yes,$(HAVE_LIB$(1))),$($(1)_$(2)))
+ac_lib_mkvar = $(foreach lib,$(1),$(call ac_lib_mkvar_1,$(lib),$(2)))
+
+
+########################################################################
+# the tests ...
+
+# get uname
+ac_uname = $(shell \
+ $(call ac_init,for system);\
+ $(call ac_s_cmd,uname -s | tr 'A-Z' 'a-z');\
+ $(call ac_fini))
+
+ac_uname_arch = $(shell \
+ $(call ac_init,for arch);\
+ $(call ac_s_cmd,uname -m | tr 'A-Z' 'a-z');\
+ $(call ac_fini))
+
+# check for some header file
+# args: header file
+ac_header = $(shell \
+ $(call ac_init,for $(1));\
+ $(call ac_b_cmd,echo '\#include <$(1)>' |\
+ $(CC) $(CFLAGS) -E -);\
+ $(call ac_fini))
+
+# check for some function
+# args: function [, additional libs ]
+ac_func = $(shell \
+ $(call ac_init,for $(1));\
+ echo 'void $(1)(void); int main(void) {$(1)();return 0;}' \
+ > __actest.c;\
+ $(call ac_b_cmd,$(CC) $(CFLAGS) $(LDFLAGS) -o \
+ __actest __actest.c $(2));\
+ rm -f __actest __actest.c;\
+ $(call ac_fini))
+
+# check for some library
+# args: function, library [, additional libs ]
+ac_lib = $(shell \
+ $(call ac_init,for $(1) in $(2));\
+ echo 'void $(1)(void); int main(void) {$(1)();return 0;}' \
+ > __actest.c;\
+ $(call ac_b_cmd,$(CC) $(CFLAGS) $(LDFLAGS) -o \
+ __actest __actest.c -l$(2) $(3));\
+ rm -f __actest __actest.c;\
+ $(call ac_fini))
+
+# check if some compiler flag works
+# args: compiler flag
+ac_cflag = $(shell \
+ $(call ac_init,for $(CC) cflags);\
+ echo 'int main() {return 0;}' > __actest.c;\
+ $(call ac_b_cmd,$(CC) $(CFLAGS) $(1) $(LDFLAGS) -o \
+ __actest __actest.c);\
+ rm -f __actest __actest.c;\
+ if test "$${rc}" = "yes"; then rc="$(1)"; else rc="$(2)"; fi;\
+ $(call ac_fini))
+
+# check for some binary
+# args: binary name
+ac_binary = $(shell \
+ $(call ac_init,for $(1));\
+ $(call ac_s_cmd,which $(1));\
+ bin="$$rc";rc="no";\
+ $(call ac_b_cmd,test -x "$$$$bin");\
+ $(call ac_fini))
+
+# check if lib64 is used
+#ac_lib64 = $(shell \
+# $(call ac_init,for libdir name);\
+# $(call ac_s_cmd,$(CC) -print-search-dirs | grep -q lib64 &&\
+# echo "lib64" || echo "lib");\
+# $(call ac_fini))
+ac_lib64 = $(shell \
+ $(call ac_init,for libdir name);\
+ $(call ac_s_cmd,/sbin/ldconfig -p | grep -q lib64 &&\
+ echo "lib64" || echo "lib");\
+ $(call ac_fini))
+
+# check for x11 ressource dir prefix
+ac_resdir = $(shell \
+ $(call ac_init,for X11 app-defaults prefix);\
+ $(call ac_s_cmd, for dir in \
+ /etc/X11/app-defaults \
+ /usr/X11R6/lib/X11/app-defaults \
+ /usr/share/X11/app-defaults \
+ /usr/lib/X11/app-defaults \
+ ; do test -d "$$dir" || continue;\
+ dirname "$$dir"; break; done);\
+ $(call ac_fini))
+
+# check if package is installed, via pkg-config
+# args: pkg name
+ac_pkg_config = $(shell \
+ $(call ac_init,for $(1) (using pkg-config));\
+ $(call ac_b_cmd, pkg-config $(1));\
+ $(call ac_fini))
+
+# grep some file
+# args: regex, file
+ac_grep = $(shell \
+ $(call ac_init,for $(1) in $(2));\
+ $(call ac_b_cmd, grep -q $(1) $(2));\
+ $(call ac_fini))
+
+
+########################################################################
+# build Make.config
+
+define newline
+
+
+endef
+make-config-q = $(subst $(newline),\n,$(make-config))
+
+ifeq ($(filter config,$(MAKECMDGOALS)),config)
+.PHONY: Make.config
+ LIB := $(call ac_lib64)
+else
+ LIB ?= $(call ac_lib64)
+ LIB := $(LIB)
+endif
+.PHONY: config
+config: Make.config
+ @true
+
+Make.config: $(srcdir)/GNUmakefile
+ @echo -e "$(make-config-q)" > $@
+ @echo
+ @echo "Make.config written, edit if needed"
+ @echo
diff --git a/mk/Compile.mk b/mk/Compile.mk
new file mode 100644
index 0000000..ae88f14
--- /dev/null
+++ b/mk/Compile.mk
@@ -0,0 +1,102 @@
+#
+# some rules to compile stuff ...
+#
+# (c) 2002-2006 Gerd Hoffmann <kraxel@suse.de>
+#
+# main features:
+# * autodependencies via "cpp -MD"
+# * fancy, non-verbose output
+#
+# This file is public domain. No warranty. If it breaks you keep
+# both pieces.
+#
+########################################################################
+
+# verbose yes/no
+verbose ?= no
+
+# dependency files
+tmpdep = mk/$(subst /,_,$*).tmp
+depfile = mk/$(subst /,_,$*).dep
+depfiles = mk/*.dep
+
+compile_c = $(CC) $(CFLAGS) -Wp,-MD,$(tmpdep) -c -o $@ $<
+compile_c_pic = $(CC) $(CFLAGS) -fPIC -Wp,-MD,$(tmpdep) -c -o $@ $<
+compile_cc = $(CXX) $(CXXFLAGS) -Wp,-MD,$(tmpdep) -c -o $@ $<
+fixup_deps = sed -e "s|.*\.o:|$@:|" < $(tmpdep) > $(depfile) && rm -f $(tmpdep)
+cc_makedirs = mkdir -p $(dir $@) $(dir $(depfile))
+
+link_app = $(CC) $(LDFLAGS) -o $@ $^ $(LDLIBS)
+link_so = $(CC) $(LDFLAGS) -shared -Wl,-soname,$(@F) -o $@ $^ $(LDLIBS)
+ar_lib = rm -f $@ && ar -r $@ $^ && ranlib $@
+
+moc_h = $(MOC) $< -o $@
+msgfmt_po = msgfmt -o $@ $<
+
+# non-verbose output
+ifeq ($(verbose),no)
+ echo_compile_c = echo " CC " $@
+ echo_compile_c_pic = echo " CC " $@
+ echo_compile_cc = echo " CXX " $@
+ echo_link_app = echo " LD " $@
+ echo_link_so = echo " LD " $@
+ echo_ar_lib = echo " AR " $@
+ echo_moc_h = echo " MOC " $@
+ echo_msgfmt_po = echo " MSGFMT " $@
+else
+ echo_compile_c = echo $(compile_c)
+ echo_compile_c_pic = echo $(compile_c_pic)
+ echo_compile_cc = echo $(compile_cc)
+ echo_link_app = echo $(link_app)
+ echo_link_so = echo $(link_so)
+ echo_ar_lib = echo $(ar_lib)
+ echo_moc_h = echo $(moc_h)
+ echo_msgfmt_po = echo $(msgfmt_po)
+endif
+
+%.o: %.c
+ @$(cc_makedirs)
+ @$(echo_compile_c)
+ @$(compile_c)
+ @$(fixup_deps)
+
+%.opic: %.c
+ @$(cc_makedirs)
+ @$(echo_compile_c_pic)
+ @$(compile_c_pic)
+ @$(fixup_deps)
+
+%.o: %.cc
+ @$(cc_makedirs)
+ @$(echo_compile_cc)
+ @$(compile_cc)
+ @$(fixup_deps)
+
+%.o: %.cpp
+ @$(cc_makedirs)
+ @$(echo_compile_cc)
+ @$(compile_cc)
+ @$(fixup_deps)
+
+
+%: %.o
+ @$(echo_link_app)
+ @$(link_app)
+
+%.so: %.o
+ @$(echo_link_so)
+ @$(link_so)
+
+%.a: %.o
+ @$(echo_ar_lib)
+ @$(ar_lib)
+
+
+%.moc : %.h
+ @$(echo_moc_h)
+ @$(moc_h)
+
+%.mo : %.po
+ @$(echo_msgfmt_po)
+ @$(msgfmt_po)
+
diff --git a/mk/Maintainer.mk b/mk/Maintainer.mk
new file mode 100644
index 0000000..1e237d3
--- /dev/null
+++ b/mk/Maintainer.mk
@@ -0,0 +1,30 @@
+# just some maintainer stuff for me ...
+########################################################################
+
+make-sync-dir = $(HOME)/projects/gnu-makefiles
+
+.PHONY: sync
+sync:: distclean
+ test -d $(make-sync-dir)
+ rm -f $(srcdir)/INSTALL $(srcdir)/mk/*.mk
+ cp -v $(make-sync-dir)/INSTALL $(srcdir)/.
+ cp -v $(make-sync-dir)/*.mk $(srcdir)/mk
+ chmod 444 $(srcdir)/INSTALL $(srcdir)/mk/*.mk
+
+
+repository := $(shell basename $(PWD))
+release-dir ?= $(HOME)/projects/Releases
+release-pub ?= bigendian.kraxel.org:/public/vhosts/www.kraxel.org/releases/$(repository)
+tarball = $(release-dir)/$(repository)-$(VERSION).tar
+
+$(tarball).gz:
+ git tag -m "release $(VERSION)" "$(VERSION)"
+ git push --tags
+ git archive --format=tar --prefix=$(repository)-$(VERSION)/ \
+ -o $(tarball) $(VERSION)
+ gzip $(tarball)
+
+.PHONY: release
+release: $(tarball).gz
+ gpg --detach-sign --armor $(tarball).gz
+ scp $(tarball).gz* $(release-pub)
diff --git a/mk/Variables.mk b/mk/Variables.mk
new file mode 100644
index 0000000..99f787c
--- /dev/null
+++ b/mk/Variables.mk
@@ -0,0 +1,55 @@
+# common variables ...
+########################################################################
+
+# directories
+DESTDIR =
+srcdir ?= .
+prefix ?= /usr/local
+bindir = $(DESTDIR)$(prefix)/bin
+sbindir = $(DESTDIR)$(prefix)/sbin
+libdir = $(DESTDIR)$(prefix)/$(LIB)
+shrdir = $(DESTDIR)$(prefix)/share
+mandir = $(shrdir)/man
+locdir = $(shrdir)/locale
+appdir = $(shrdir)/applications
+
+# package + version
+empty :=
+space := $(empty) $(empty)
+ifneq ($(wildcard $(srcdir)/VERSION),)
+ VERSION := $(shell cat $(srcdir)/VERSION)
+else
+ VERSION := 42
+endif
+RELTAG := v$(subst .,_,$(VERSION))
+
+# programs
+CC ?= gcc
+CXX ?= g++
+MOC ?= $(if $(QTDIR),$(QTDIR)/bin/moc,moc)
+
+STRIP ?= -s
+INSTALL ?= install
+INSTALL_BINARY := $(INSTALL) $(STRIP)
+INSTALL_SCRIPT := $(INSTALL)
+INSTALL_DATA := $(INSTALL) -m 644
+INSTALL_DIR := $(INSTALL) -d
+
+# cflags
+CFLAGS ?= -g -O2
+CXXFLAGS ?= $(CFLAGS)
+CFLAGS += -Wall -Wmissing-prototypes -Wstrict-prototypes \
+ -Wpointer-arith -Wunused
+CXXFLAGS += -Wall -Wpointer-arith -Wunused
+
+# add /usr/local to the search path if something is in there ...
+ifneq ($(wildcard /usr/local/include/*.h),)
+ CFLAGS += -I/usr/local/include
+ LDFLAGS += -L/usr/local/$(LIB)
+endif
+
+# fixup include path for $(srcdir) != "."
+ifneq ($(srcdir),.)
+ CFLAGS += -I. -I$(srcdir)
+endif
+
diff --git a/vconsole.c b/vconsole.c
new file mode 100644
index 0000000..bbe7326
--- /dev/null
+++ b/vconsole.c
@@ -0,0 +1,590 @@
+#include "vconsole.h"
+#include "libvirt-glib-event.h"
+
+#define APPNAME "vconsole"
+
+/* ------------------------------------------------------------------ */
+
+int debug = 0;
+GKeyFile *config;
+
+/* ------------------------------------------------------------------ */
+
+static char *config_file;
+
+static void config_read(void)
+{
+ char *home = getenv("HOME");
+ GError *err = NULL;
+
+ if (!home)
+ return;
+ config_file = g_strdup_printf("%s/.vconsole", home);
+ config = g_key_file_new();
+ g_key_file_load_from_file(config, config_file,
+ G_KEY_FILE_KEEP_COMMENTS, &err);
+}
+
+void config_write(void)
+{
+ char *data;
+ gsize len;
+ GError *err = NULL;
+ int fd;
+
+ data = g_key_file_to_data(config, &len, &err);
+ fd = open(config_file, O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ if (-1 == fd)
+ return;
+ write(fd, data, len);
+ fsync(fd);
+ close(fd);
+}
+
+/* ------------------------------------------------------------------ */
+
+static void menu_cb_connect_ask(GtkAction *action, gpointer userdata)
+{
+// struct vconsole_window *win = userdata;
+ fprintf(stderr, "%s: [ FIXME ]\n", __func__);
+}
+
+static void menu_cb_connect_menu(GtkAction *action, gpointer userdata)
+{
+ struct vconsole_window *win = userdata;
+ GError *err = NULL;
+ char name[128];
+
+ if (1 != sscanf(gtk_action_get_name(action), "ConnectMenu_%127s", name))
+ return;
+ if (debug)
+ fprintf(stderr, "%s: %s\n", __func__, name);
+ connect_init(win, g_key_file_get_string(config, "hosts", name, &err));
+}
+
+static void menu_cb_close_tab(GtkAction *action, gpointer userdata)
+{
+ struct vconsole_window *win = userdata;
+ domain_close_current_tab(win);
+}
+
+static void menu_cb_close_app(GtkAction *action, gpointer userdata)
+{
+ struct vconsole_window *win = userdata;
+ gtk_widget_destroy(win->toplevel);
+}
+
+static void menu_cb_config_font(GtkAction *action, void *data)
+{
+ struct vconsole_window *win = data;
+ GtkWidget *dialog;
+
+ dialog = gtk_font_selection_dialog_new("Terminal font");
+ if (win->tty_font)
+ gtk_font_selection_dialog_set_font_name
+ (GTK_FONT_SELECTION_DIALOG(dialog), win->tty_font);
+
+ gtk_widget_show_all(dialog);
+ switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
+ case GTK_RESPONSE_OK:
+ win->tty_font = gtk_font_selection_dialog_get_font_name
+ (GTK_FONT_SELECTION_DIALOG(dialog));
+ g_key_file_set_string(config, "tty", "font", win->tty_font);
+ config_write();
+ domain_configure_all_vtes(win);
+ break;
+ }
+ gtk_widget_destroy(dialog);
+}
+
+static int pickcolor(char *title, char *group, char *key, char *current)
+{
+ GtkWidget *dialog;
+ GdkColor color = {0,0,0,0};
+ GtkColorSelection *csel;
+ char name[16];
+ int rc = -1;
+
+ gdk_color_parse(current, &color);
+ dialog = gtk_color_selection_dialog_new(title);
+ csel = GTK_COLOR_SELECTION(GTK_COLOR_SELECTION_DIALOG(dialog)->colorsel);
+ gtk_color_selection_set_has_opacity_control(csel, FALSE);
+ gtk_color_selection_set_current_color(csel, &color);
+
+ gtk_widget_show_all(dialog);
+ switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
+ case GTK_RESPONSE_OK:
+ gtk_color_selection_get_current_color(csel, &color);
+ snprintf(name, sizeof(name), "#%04x%04x%04x",
+ color.red, color.green, color.blue);
+ g_key_file_set_string(config, group, key, name);
+ config_write();
+ rc = 0;
+ }
+ gtk_widget_destroy(dialog);
+ return rc;
+}
+
+static void menu_cb_config_fg(GtkAction *action, void *data)
+{
+ struct vconsole_window *win = data;
+ GError *err = NULL;
+
+ if (0 != pickcolor("Terminal text color", "tty", "foreground", win->tty_fg))
+ return;
+ win->tty_fg = g_key_file_get_string(config, "tty", "foreground", &err);
+ domain_configure_all_vtes(win);
+}
+
+static void menu_cb_config_bg(GtkAction *action, void *data)
+{
+ struct vconsole_window *win = data;
+ GError *err = NULL;
+
+ if (0 != pickcolor("Terminal background", "tty", "background", win->tty_bg))
+ return;
+ win->tty_bg = g_key_file_get_string(config, "tty", "background", &err);
+ domain_configure_all_vtes(win);
+}
+
+static void menu_cb_about(GtkAction *action, gpointer userdata)
+{
+ static char *comments = "virtual machine console";
+ static char *copyright = "(c) 2012 Gerd Hoffmann";
+ static char *authors[] = { "Gerd Hoffmann <kraxel@redhat.com>", NULL };
+ struct vconsole_window *win = userdata;
+
+ gtk_show_about_dialog(GTK_WINDOW(win->toplevel),
+ "authors", authors,
+ "comments", comments,
+ "copyright", copyright,
+ "logo-icon-name", GTK_STOCK_ABOUT,
+ "version", VERSION,
+ NULL);
+}
+
+/* ------------------------------------------------------------------ */
+
+static void menu_cb_blink_cursor(GtkToggleAction *action, gpointer userdata)
+{
+ struct vconsole_window *win = userdata;
+
+ win->tty_blink = gtk_toggle_action_get_active(action);
+ domain_configure_all_vtes(win);
+ g_key_file_set_boolean(config, "tty", "blink", win->tty_blink);
+ config_write();
+}
+
+/* ------------------------------------------------------------------ */
+
+static const GtkActionEntry entries[] = {
+ {
+ /* --- menu bar --- */
+ .name = "FileMenu",
+ .label = "_File",
+ },{
+ .name = "ViewMenu",
+ .label = "_View",
+ },{
+ .name = "HelpMenu",
+ .label = "_Help",
+ },{
+
+ /* --- submenus --- */
+ .name = "ConnectMenu",
+ .label = "_Recent",
+ },{
+
+ /* --- file menu --- */
+ .name = "ConnectAsk",
+ .label = "_Connect ...",
+ .callback = G_CALLBACK(menu_cb_connect_ask),
+ },{
+ .name = "CloseTab",
+ .stock_id = GTK_STOCK_CLOSE,
+ .label = "Close _Tab",
+ .accelerator = "<control>T",
+ .callback = G_CALLBACK(menu_cb_close_tab),
+ },{
+ .name = "CloseApp",
+ .stock_id = GTK_STOCK_QUIT,
+ .label = "_Quit",
+ .accelerator = "<control>Q",
+ .callback = G_CALLBACK(menu_cb_close_app),
+ },{
+
+ /* --- view menu --- */
+ .name = "TerminalFont",
+ .stock_id = GTK_STOCK_SELECT_FONT,
+ .label = "Terminal _font ...",
+ .callback = G_CALLBACK(menu_cb_config_font),
+ },{
+ .name = "TerminalForeground",
+ .stock_id = GTK_STOCK_SELECT_COLOR,
+ .label = "Terminal _text color ...",
+ .callback = G_CALLBACK(menu_cb_config_fg),
+ },{
+ .name = "TerminalBackground",
+ .label = "Terminal _background ...",
+ .callback = G_CALLBACK(menu_cb_config_bg),
+ },{
+
+ /* --- help menu --- */
+ .name = "About",
+ .stock_id = GTK_STOCK_ABOUT,
+ .label = "_About ...",
+ .callback = G_CALLBACK(menu_cb_about),
+ },
+};
+
+static const GtkToggleActionEntry tentries[] = {
+ {
+ .name = "TerminalBlink",
+ .label = "Blinking cursor",
+ .callback = G_CALLBACK(menu_cb_blink_cursor),
+ }
+};
+
+static char ui_xml[] =
+"<ui>\n"
+" <menubar name='MainMenu'>\n"
+" <menu action='FileMenu'>\n"
+" <menuitem action='ConnectAsk'/>\n"
+" <menu action='ConnectMenu'>\n"
+" </menu>\n"
+" <separator/>\n"
+" <menuitem action='CloseTab'/>\n"
+" <menuitem action='CloseApp'/>\n"
+" </menu>\n"
+" <menu action='ViewMenu'>\n"
+" <menuitem action='TerminalFont'/>\n"
+" <menuitem action='TerminalForeground'/>\n"
+" <menuitem action='TerminalBackground'/>\n"
+" <menuitem action='TerminalBlink'/>\n"
+" </menu>\n"
+" <menu action='HelpMenu'>\n"
+" <menuitem action='About'/>\n"
+" </menu>\n"
+" </menubar>\n"
+"</ui>\n";
+
+static char recent_xml[] =
+"<ui>\n"
+" <menubar name='MainMenu'>\n"
+" <menu action='FileMenu'>\n"
+" <menu action='ConnectMenu'>\n"
+"%s"
+" </menu>\n"
+" </menu>\n"
+" </menubar>\n"
+"</ui>\n";
+
+static void destroy(GtkWidget *widget, gpointer data)
+{
+ gtk_main_quit();
+}
+
+static void vconsole_build_recent(struct vconsole_window *win)
+{
+ GError *err = NULL;
+ GtkActionEntry entry;
+ char *xml, *h, *entries = NULL;
+ gchar **keys, *action;
+ gsize i, nkeys = 0;
+
+ /* cleanup */
+ if (win->r_id) {
+ gtk_ui_manager_remove_ui(win->ui, win->r_id);
+ win->r_id = 0;
+ }
+ if (win->r_ag) {
+ gtk_ui_manager_remove_action_group(win->ui, win->r_ag);
+ g_object_unref(win->r_ag);
+ win->r_ag = NULL;
+ }
+
+ /* start */
+ win->r_ag = gtk_action_group_new("RecentActions");
+
+ /* add entries */
+ keys = g_key_file_get_keys(config, "hosts", &nkeys, &err);
+ for (i = 0; i < nkeys; i++) {
+ action = g_strdup_printf("ConnectMenu_%s", keys[i]);
+ memset(&entry, 0, sizeof(entry));
+ entry.callback = G_CALLBACK(menu_cb_connect_menu);
+ entry.name = action;
+ entry.label = keys[i];
+ gtk_action_group_add_actions(win->r_ag, &entry, 1, win);
+ h = entries;
+ entries = g_strdup_printf("%s <menuitem action='%s'/>\n",
+ h ? h : "", action);
+ g_free(h);
+ g_free(action);
+ }
+
+ /* finish */
+ xml = g_strdup_printf(recent_xml, entries ? entries : "");
+ if (debug)
+ fprintf(stderr, "---\n%s---\n", xml);
+ gtk_ui_manager_insert_action_group(win->ui, win->r_ag, 1);
+ win->r_id = gtk_ui_manager_add_ui_from_string(win->ui, xml, -1, &err);
+ if (!win->r_id) {
+ g_message("building menu failed: %s", err->message);
+ g_error_free(err);
+ }
+ g_free(xml);
+}
+
+static struct vconsole_window *vconsole_toplevel_create(void)
+{
+ struct vconsole_window *win;
+ GtkWidget *vbox, *menubar, *toolbar, *item;
+ GtkAccelGroup *accel;
+ GtkActionGroup *ag;
+ GError *err;
+
+ win = g_new0(struct vconsole_window, 1);
+ win->toplevel = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_title(GTK_WINDOW(win->toplevel), APPNAME);
+ gtk_window_set_default_size(GTK_WINDOW(win->toplevel), 320, 280);
+ g_signal_connect(G_OBJECT(win->toplevel), "destroy",
+ G_CALLBACK(destroy), win);
+
+ /* menu + toolbar */
+ win->ui = gtk_ui_manager_new();
+ ag = gtk_action_group_new("MenuActions");
+ gtk_action_group_add_actions(ag, entries, G_N_ELEMENTS(entries), win);
+ gtk_action_group_add_toggle_actions(ag, tentries,
+ G_N_ELEMENTS(tentries), win);
+ gtk_ui_manager_insert_action_group(win->ui, ag, 0);
+ accel = gtk_ui_manager_get_accel_group(win->ui);
+ gtk_window_add_accel_group(GTK_WINDOW(win->toplevel), accel);
+
+ err = NULL;
+ if (!gtk_ui_manager_add_ui_from_string(win->ui, ui_xml, -1, &err)) {
+ g_message("building menus failed: %s", err->message);
+ g_error_free(err);
+ exit(1);
+ }
+
+ /* main area */
+ win->notebook = gtk_notebook_new();
+
+ /* Make a vbox and put stuff in */
+ vbox = gtk_vbox_new(FALSE, 1);
+ gtk_container_set_border_width(GTK_CONTAINER(vbox), 1);
+ gtk_container_add(GTK_CONTAINER(win->toplevel), vbox);
+ menubar = gtk_ui_manager_get_widget(win->ui, "/MainMenu");
+ gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0);
+ toolbar = gtk_ui_manager_get_widget(win->ui, "/ToolBar");
+ if (toolbar)
+ gtk_box_pack_start(GTK_BOX(vbox), toolbar, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), win->notebook, TRUE, TRUE, 0);
+
+ /* read config */
+ err = NULL;
+ win->tty_font = g_key_file_get_string(config, "tty", "font", &err);
+ err = NULL;
+ win->tty_fg = g_key_file_get_string(config, "tty", "foreground", &err);
+ err = NULL;
+ win->tty_bg = g_key_file_get_string(config, "tty", "background", &err);
+ err = NULL;
+ win->tty_blink = g_key_file_get_boolean(config, "tty", "blink", &err);
+
+ /* config defaults */
+ if (!win->tty_font)
+ win->tty_font = "Monospace 12";
+ if (!win->tty_fg)
+ win->tty_fg = "white";
+ if (!win->tty_bg)
+ win->tty_bg = "black";
+
+ /* apply config */
+ item = gtk_ui_manager_get_widget(win->ui, "/MainMenu/ViewMenu/TerminalBlink");
+ gtk_check_menu_item_set_active(GTK_CHECK_MENU_ITEM(item), win->tty_blink);
+
+ return win;
+}
+
+static void vconsole_tab_list_activate(GtkTreeView *tree_view,
+ GtkTreePath *path,
+ GtkTreeViewColumn *column,
+ gpointer user_data)
+{
+ struct vconsole_window *win = user_data;
+ GtkTreeModel *model = GTK_TREE_MODEL(win->store);
+ GtkTreeIter parent, iter;
+ gboolean is_host;
+ char *name;
+ struct vconsole_domain *dom;
+
+ if (!gtk_tree_model_get_iter(model, &iter, path))
+ return;
+ gtk_tree_model_get(model, &iter, NAME_COL, &name, -1);
+ is_host = !gtk_tree_model_iter_parent(model, &parent, &iter);
+ if (is_host) {
+ if (debug)
+ fprintf(stderr, "%s: host %s\n", __func__, name);
+ if (gtk_tree_view_row_expanded(tree_view, path)) {
+ gtk_tree_view_collapse_row(tree_view, path);
+ } else {
+ gtk_tree_view_expand_row(tree_view, path, FALSE);
+ }
+ } else {
+ if (debug)
+ fprintf(stderr, "%s: guest %s\n", __func__, name);
+ gtk_tree_model_get(model, &iter, DPTR_COL, &dom, -1);
+ domain_activate(dom);
+ }
+ g_free(name);
+}
+
+static gint gtk_sort_iter_compare_str(GtkTreeModel *model,
+ GtkTreeIter *a,
+ GtkTreeIter *b,
+ gpointer userdata)
+{
+ gint sortcol = GPOINTER_TO_INT(userdata);
+ char *aa,*bb;
+
+ gtk_tree_model_get(model, a, sortcol, &aa, -1);
+ gtk_tree_model_get(model, b, sortcol, &bb, -1);
+ if (NULL == aa && NULL == bb)
+ return 0;
+ if (NULL == aa)
+ return 1;
+ if (NULL == bb)
+ return -1;
+ return strcmp(aa,bb);
+}
+
+static void vconsole_tab_list_create(struct vconsole_window *win)
+{
+ GtkCellRenderer *renderer;
+ GtkTreeViewColumn *column;
+ GtkWidget *label, *scroll;
+ GtkTreeSortable *sortable;
+
+ /* store & view */
+ win->store = gtk_tree_store_new(N_COLUMNS,
+ G_TYPE_STRING, // NAME_COL
+ G_TYPE_POINTER, // CPTR_COL
+ G_TYPE_STRING, // URI_COL
+ G_TYPE_POINTER, // DPTR_COL
+ G_TYPE_STRING, // ID_COL
+ G_TYPE_STRING); // STATE_COL
+ sortable = GTK_TREE_SORTABLE(win->store);
+ win->tree = gtk_tree_view_new_with_model(GTK_TREE_MODEL(win->store));
+
+ g_signal_connect(G_OBJECT(win->tree), "row-activated",
+ G_CALLBACK(vconsole_tab_list_activate),
+ win);
+
+ /* name */
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes("Name",
+ renderer,
+ "text", NAME_COL,
+ NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(win->tree), column);
+ gtk_tree_sortable_set_sort_func(sortable, NAME_COL,
+ gtk_sort_iter_compare_str,
+ GINT_TO_POINTER(NAME_COL), NULL);
+ gtk_tree_view_column_set_sort_column_id(column, NAME_COL);
+
+ /* id */
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes("ID",
+ renderer,
+ "text", ID_COL,
+ NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(win->tree), column);
+
+ /* state */
+ renderer = gtk_cell_renderer_text_new();
+ column = gtk_tree_view_column_new_with_attributes("State",
+ renderer,
+ "text", STATE_COL,
+ NULL);
+ gtk_tree_view_append_column(GTK_TREE_VIEW(win->tree), column);
+
+ /* sort store */
+ gtk_tree_sortable_set_sort_column_id(sortable, NAME_COL,
+ GTK_SORT_ASCENDING);
+
+ /* add tab */
+ label = gtk_label_new("Guests");
+ scroll = gtk_scrolled_window_new(NULL, NULL);
+ gtk_container_add(GTK_CONTAINER(scroll), win->tree);
+ gtk_notebook_insert_page(GTK_NOTEBOOK(win->notebook),
+ scroll, label, 0);
+}
+
+/* ------------------------------------------------------------------ */
+
+static void usage(FILE *fp)
+{
+ fprintf(fp,
+ "This is a virtual machine console\n"
+ "\n"
+ "usage: %s [ options ]\n"
+ "options:\n"
+ " -h Print this text.\n"
+ " -d Enable debugging.\n"
+ " -c <uri> Connect to libvirt.\n"
+ "\n"
+ "-- \n"
+ "(c) 2012 Gerd Hoffmann <kraxel@redhat.com>\n",
+ APPNAME);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct vconsole_window *win;
+ char *uri = NULL;
+ int c;
+
+ gtk_init(&argc, &argv);
+ for (;;) {
+ if (-1 == (c = getopt(argc, argv, "hdc:")))
+ break;
+ switch (c) {
+ case 'd':
+ debug++;
+ break;
+ case 'c':
+ uri = optarg;
+ break;
+ case 'h':
+ usage(stdout);
+ exit(0);
+ default:
+ usage(stderr);
+ exit(1);
+ }
+ }
+
+ if (uri == NULL)
+ uri = getenv("VIRSH_DEFAULT_CONNECT_URI");
+
+ /* init */
+ g_thread_init(NULL);
+ gvir_event_register();
+ config_read();
+
+ /* main window */
+ win = vconsole_toplevel_create();
+ vconsole_tab_list_create(win);
+ gtk_widget_show_all(win->toplevel);
+
+ if (uri)
+ connect_init(win, uri);
+ vconsole_build_recent(win);
+
+ /* main loop */
+ gtk_main();
+
+ /* cleanup */
+ exit(0);
+}
diff --git a/vconsole.h b/vconsole.h
new file mode 100644
index 0000000..b58f11f
--- /dev/null
+++ b/vconsole.h
@@ -0,0 +1,82 @@
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <assert.h>
+
+#include <gtk/gtk.h>
+#include <vte/vte.h>
+
+#include <libvirt/libvirt.h>
+
+/* ------------------------------------------------------------------ */
+
+enum vconsole_cols {
+ /* common */
+ NAME_COL,
+
+ /* hosts only */
+ CPTR_COL, // vconsole_connect
+ URI_COL,
+
+ /* guests only */
+ DPTR_COL, // vconsole_domain
+ ID_COL,
+ STATE_COL,
+ N_COLUMNS
+};
+
+struct vconsole_window {
+ /* toplevel window */
+ GtkWidget *toplevel;
+ GtkWidget *notebook;
+ GtkUIManager *ui;
+
+ /* recent hosts */
+ GtkActionGroup *r_ag;
+ guint r_id;
+
+ /* domain list tab */
+ GtkTreeStore *store;
+ GtkWidget *tree;
+
+ /* options */
+ gboolean tty_blink;
+ char *tty_font;
+ char *tty_fg;
+ char *tty_bg;
+};
+
+extern int debug;
+extern GKeyFile *config;
+
+void config_write(void);
+
+/* ------------------------------------------------------------------ */
+
+struct vconsole_connect {
+ struct vconsole_window *win;
+ virConnectPtr ptr;
+};
+
+struct vconsole_connect *connect_init(struct vconsole_window *win,
+ const char *uri);
+/* ------------------------------------------------------------------ */
+
+struct vconsole_domain {
+ struct vconsole_connect *conn;
+ char uuid[VIR_UUID_STRING_BUFLEN];
+
+ GtkWidget *vbox, *vte, *status;
+ int page;
+ virStreamPtr stream;
+ virDomainInfo info;
+};
+
+void domain_update(struct vconsole_connect *conn,
+ virDomainPtr d, virDomainEventType event);
+void domain_activate(struct vconsole_domain *dom);
+void domain_configure_all_vtes(struct vconsole_window *win);
+void domain_close_current_tab(struct vconsole_window *win);