diff options
author | Gerd Hoffmann <kraxel@redhat.com> | 2012-08-15 16:49:39 +0200 |
---|---|---|
committer | Gerd Hoffmann <kraxel@redhat.com> | 2012-08-15 16:49:39 +0200 |
commit | daf23c350f0fe8fffbbde331d43460c517d3e445 (patch) | |
tree | 8afe6a21c38ffb961b7cbba89107d3f996e6d02e | |
download | vconsole-daf23c350f0fe8fffbbde331d43460c517d3e445.tar.gz |
initial commit
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | GNUmakefile | 68 | ||||
-rw-r--r-- | README | 45 | ||||
-rw-r--r-- | connect.c | 74 | ||||
-rw-r--r-- | domain.c | 331 | ||||
-rw-r--r-- | libvirt-glib-event.c | 481 | ||||
-rw-r--r-- | libvirt-glib-event.h | 35 | ||||
-rw-r--r-- | mk/Autoconf.mk | 168 | ||||
-rw-r--r-- | mk/Compile.mk | 102 | ||||
-rw-r--r-- | mk/Maintainer.mk | 30 | ||||
-rw-r--r-- | mk/Variables.mk | 55 | ||||
-rw-r--r-- | vconsole.c | 590 | ||||
-rw-r--r-- | vconsole.h | 82 |
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) @@ -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); |