[virt-tools-list] [PATCH 22/22] spice: hook into QMP port

marcandre.lureau at redhat.com marcandre.lureau at redhat.com
Tue Jul 31 13:41:25 UTC 2018


From: Marc-André Lureau <marcandre.lureau at redhat.com>

If the "org.qemu.monitor.qmp.0" port is available:
- enable the VM UI
- get and follow the VM state
- send the requested VM actions

This adds a json-glib dependency on spice-gtk display. If necessary,
we could make it optional.

Signed-off-by: Marc-André Lureau <marcandre.lureau at redhat.com>
---
 configure.ac                    |   8 +-
 src/Makefile.am                 |   2 +
 src/virt-viewer-session-spice.c | 166 ++++++++++++++++++++++++++++++++
 3 files changed, 175 insertions(+), 1 deletion(-)

diff --git a/configure.ac b/configure.ac
index 3c7522d..c5aa0dc 100644
--- a/configure.ac
+++ b/configure.ac
@@ -27,6 +27,7 @@ GTK_VNC_REQUIRED="0.4.0"
 SPICE_GTK_REQUIRED="0.35"
 SPICE_PROTOCOL_REQUIRED="0.12.7"
 GOVIRT_REQUIRED="0.3.2"
+JSON_GLIB_REQUIRED="1.0"
 
 AC_SUBST([GLIB2_REQUIRED])
 AC_SUBST([LIBXML2_REQUIRED])
@@ -37,6 +38,7 @@ AC_SUBST([GTK_VNC_REQUIRED])
 AC_SUBST([SPICE_GTK_REQUIRED])
 AC_SUBST([SPICE_PROTOCOL_REQUIRED])
 AC_SUBST([GOVIRT_REQUIRED])
+AC_SUBST([JSON_GLIB_REQUIRED])
 
 AC_MSG_CHECKING([for native Win32])
 case "$host_os" in
@@ -159,13 +161,15 @@ AC_ARG_WITH([spice-gtk],
 AS_IF([test "x$with_spice_gtk" != "xno" && test "x$with_spice_gtk" != "xyes"],
       [PKG_CHECK_EXISTS([spice-client-gtk-3.0 >= $SPICE_GTK_REQUIRED
                          spice-client-glib-2.0 >= $SPICE_GTK_REQUIRED
-                         spice-protocol >= $SPICE_PROTOCOL_REQUIRED],
+                         spice-protocol >= $SPICE_PROTOCOL_REQUIRED
+                         json-glib-1.0 >= $JSON_GLIB_REQUIRED],
                         [with_spice_gtk=yes], [with_spice_gtk=no])])
 
 AS_IF([test "x$with_spice_gtk" = "xyes"],
       [PKG_CHECK_MODULES(SPICE_GTK, [spice-client-gtk-3.0 >= $SPICE_GTK_REQUIRED
                                      spice-client-glib-2.0 >= $SPICE_GTK_REQUIRED])]
       [PKG_CHECK_MODULES(SPICE_PROTOCOL, [spice-protocol >= $SPICE_PROTOCOL_REQUIRED])]
+      [PKG_CHECK_MODULES(JSON_GLIB, [json-glib-1.0 >= JSON_GLIB_REQUIRED])]
       [AC_DEFINE([HAVE_SPICE_GTK], 1, [Have spice-gtk?])]
 )
 AM_CONDITIONAL([HAVE_SPICE_GTK], [test "x$with_spice_gtk" = "xyes"])
@@ -294,3 +298,5 @@ AC_MSG_NOTICE([     LIBVIRT: $LIBVIRT_CFLAGS $LIBVIRT_LIBS])
 AC_MSG_NOTICE([])
 AC_MSG_NOTICE([       OVIRT: $OVIRT_CFLAGS $OVIRT_LIBS])
 AC_MSG_NOTICE([])
+AC_MSG_NOTICE([   JSON_GLIB: $JSON_GLIB_CFLAGS $JSON_GLIB_LIBS])
+AC_MSG_NOTICE([])
diff --git a/src/Makefile.am b/src/Makefile.am
index 3a5d90d..b2dc786 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -114,6 +114,7 @@ COMMON_LIBS = \
 	$(GTK_VNC_LIBS)				\
 	$(VTE_LIBS)				\
 	$(SPICE_GTK_LIBS)			\
+	$(JSON_GLIB_LIBS)			\
 	$(LIBXML2_LIBS)				\
 	$(OVIRT_LIBS) \
 	$(NULL)
@@ -125,6 +126,7 @@ COMMON_CFLAGS = \
 	$(GTK_CFLAGS) \
 	$(GTK_VNC_CFLAGS) \
 	$(VTE_CFLAGS) \
+	$(JSON_GLIB_CFLAGS) \
 	$(SPICE_GTK_CFLAGS) \
 	$(LIBXML2_CFLAGS) \
 	$(OVIRT_CFLAGS) \
diff --git a/src/virt-viewer-session-spice.c b/src/virt-viewer-session-spice.c
index 1c622e6..39aae1d 100644
--- a/src/virt-viewer-session-spice.c
+++ b/src/virt-viewer-session-spice.c
@@ -27,6 +27,7 @@
 #include <glib/gi18n.h>
 
 #include <spice-client-gtk.h>
+#include <json-glib/json-glib.h>
 
 #include <usb-device-widget.h>
 #include "virt-viewer-file.h"
@@ -39,6 +40,8 @@
 
 G_DEFINE_TYPE (VirtViewerSessionSpice, virt_viewer_session_spice, VIRT_VIEWER_TYPE_SESSION)
 
+typedef void (QMPCb)(VirtViewerSessionSpice *self, JsonNode *node);
+
 struct _VirtViewerSessionSpicePrivate {
     GtkWindow *main_window;
     SpiceSession *session;
@@ -51,7 +54,11 @@ struct _VirtViewerSessionSpicePrivate {
     guint pass_try;
     gboolean did_auto_conf;
     VirtViewerFileTransferDialog *file_transfer_dialog;
+    SpicePortChannel *qmp;
 
+    GString *qmp_data;
+    JsonParser *qmp_parser;
+    GHashTable *qmp_cb;
 };
 
 #define VIRT_VIEWER_SESSION_SPICE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE((o), VIRT_VIEWER_TYPE_SESSION_SPICE, VirtViewerSessionSpicePrivate))
@@ -80,6 +87,7 @@ static void virt_viewer_session_spice_smartcard_insert(VirtViewerSession *sessio
 static void virt_viewer_session_spice_smartcard_remove(VirtViewerSession *session);
 static gboolean virt_viewer_session_spice_fullscreen_auto_conf(VirtViewerSessionSpice *self);
 static void virt_viewer_session_spice_apply_monitor_geometry(VirtViewerSession *self, GHashTable *monitors);
+static void virt_viewer_session_spice_vm_action(VirtViewerSession *self, gint action);
 
 static void virt_viewer_session_spice_clear_displays(VirtViewerSessionSpice *self)
 {
@@ -236,6 +244,16 @@ virt_viewer_session_spice_constructed(GObject *obj)
     G_OBJECT_CLASS(virt_viewer_session_spice_parent_class)->constructed(obj);
 }
 
+static void
+virt_viewer_session_spice_finalize(GObject *obj)
+{
+    VirtViewerSessionSpice *self = VIRT_VIEWER_SESSION_SPICE(obj);
+
+    g_string_free(self->priv->qmp_data, TRUE);
+    g_object_unref(self->priv->qmp_parser);
+    g_hash_table_unref(self->priv->qmp_cb);
+}
+
 static void
 virt_viewer_session_spice_class_init(VirtViewerSessionSpiceClass *klass)
 {
@@ -246,6 +264,7 @@ virt_viewer_session_spice_class_init(VirtViewerSessionSpiceClass *klass)
     oclass->set_property = virt_viewer_session_spice_set_property;
     oclass->dispose = virt_viewer_session_spice_dispose;
     oclass->constructed = virt_viewer_session_spice_constructed;
+    oclass->finalize = virt_viewer_session_spice_finalize;
 
     dclass->close = virt_viewer_session_spice_close;
     dclass->open_fd = virt_viewer_session_spice_open_fd;
@@ -259,6 +278,7 @@ virt_viewer_session_spice_class_init(VirtViewerSessionSpiceClass *klass)
     dclass->apply_monitor_geometry = virt_viewer_session_spice_apply_monitor_geometry;
     dclass->can_share_folder = virt_viewer_session_spice_can_share_folder;
     dclass->can_retry_auth = virt_viewer_session_spice_can_retry_auth;
+    dclass->vm_action = virt_viewer_session_spice_vm_action;
 
     g_type_class_add_private(klass, sizeof(VirtViewerSessionSpicePrivate));
 
@@ -287,6 +307,9 @@ static void
 virt_viewer_session_spice_init(VirtViewerSessionSpice *self G_GNUC_UNUSED)
 {
     self->priv = VIRT_VIEWER_SESSION_SPICE_GET_PRIVATE(self);
+    self->priv->qmp_data = g_string_sized_new(256);
+    self->priv->qmp_parser = json_parser_new();
+    self->priv->qmp_cb = g_hash_table_new(g_direct_hash, g_direct_equal);
 }
 
 static void
@@ -998,6 +1021,134 @@ port_name_to_vte_name(const char *name)
     return NULL;
 }
 
+static void
+spice_qmp_data(VirtViewerSessionSpice *self, gpointer data,
+               int size G_GNUC_UNUSED,
+               SpicePortChannel *port G_GNUC_UNUSED)
+{
+    GString *qmp = self->priv->qmp_data;
+    gchar *str, *crlf;
+
+    g_string_append_len(qmp, data, size);
+
+    str = qmp->str;
+    while ((crlf = memmem(str, qmp->len - (str - qmp->str), "\r\n", 2))) {
+        GError *err = NULL;
+
+        *crlf = '\0';
+        json_parser_load_from_data(self->priv->qmp_parser, str, crlf - str, &err);
+        if (err) {
+            g_warning("JSON parsing error: %s", err->message);
+            g_error_free(err);
+        } else {
+            JsonObject *obj = json_node_get_object(json_parser_get_root(self->priv->qmp_parser));
+            JsonNode *node;
+            const gchar *event;
+
+            if (json_object_get_member(obj, "QMP")) {
+                g_debug("QMP greeting received");
+            } else if ((node = json_object_get_member(obj, "return"))) {
+                gint id = json_object_get_int_member(obj, "id");
+                QMPCb *cb;
+
+                g_debug("QMP return id:%d", id);
+                if ((cb = g_hash_table_lookup(self->priv->qmp_cb, GINT_TO_POINTER(id)))) {
+                    g_hash_table_remove(self->priv->qmp_cb, GINT_TO_POINTER(id));
+                    cb(self, node);
+                }
+            } else if ((event = json_object_get_string_member(obj, "event"))) {
+                g_debug("%s", event);
+                if (g_str_equal(event, "STOP")) {
+                    g_object_set(virt_viewer_session_get_app(VIRT_VIEWER_SESSION(self)),
+                                 "vm-running", FALSE, NULL);
+                } else if (g_str_equal(event, "RESUME")) {
+                    g_object_set(virt_viewer_session_get_app(VIRT_VIEWER_SESSION(self)),
+                                 "vm-running", TRUE, NULL);
+                } else {
+                    g_debug("unhandled QMP event: %s", event);
+                }
+            } else {
+                g_debug("unhandled JSON %s", str);
+            }
+        }
+        str = crlf + 2;
+    }
+
+    g_string_erase(qmp, 0, str - qmp->str);
+}
+
+static void
+qmp_query_status_cb(VirtViewerSessionSpice *self, JsonNode *node)
+{
+    const char *status = json_object_get_string_member(json_node_get_object(node), "status");
+    gboolean running = TRUE;
+
+    if (g_str_equal(status, "paused")) {
+        running = FALSE;
+    }
+
+    g_object_set(virt_viewer_session_get_app(VIRT_VIEWER_SESSION(self)),
+                 "vm-running", running, NULL);
+}
+
+static void
+qmp(VirtViewerSessionSpice *self,
+    const char *cmd, const gchar *args, QMPCb *cb)
+{
+    GString *str = g_string_sized_new(256);
+    gsize len;
+    gchar *data;
+    static gint id = 0;
+
+    if (!self->priv->qmp)
+        return;
+
+    g_string_append_printf(str, "{ 'execute': '%s'", cmd);
+    if (args)
+        g_string_append_printf(str, ", 'arguments': { %s }", args);
+    g_string_append_printf(str, ", 'id': %d", id);
+    g_string_append(str, " }");
+
+    if (cb)
+        g_hash_table_insert(self->priv->qmp_cb, GINT_TO_POINTER(id), cb);
+
+    id++;
+    len = str->len;
+    data = g_string_free(str, FALSE);
+    spice_port_channel_write_async(self->priv->qmp, data, len,
+                                   NULL, spice_port_write_finished,
+                                   data);
+}
+
+static void
+virt_viewer_session_spice_vm_action(VirtViewerSession *sess, gint action)
+{
+    VirtViewerSessionSpice *self = VIRT_VIEWER_SESSION_SPICE(sess);
+    const gchar *cmd;
+
+    switch (action) {
+    case VIRT_VIEWER_SESSION_VM_ACTION_QUIT:
+        cmd = "quit";
+        break;
+    case VIRT_VIEWER_SESSION_VM_ACTION_RESET:
+        cmd = "system_reset";
+        break;
+    case VIRT_VIEWER_SESSION_VM_ACTION_POWER_DOWN:
+        cmd = "system_powerdown";
+        break;
+    case VIRT_VIEWER_SESSION_VM_ACTION_PAUSE:
+        cmd = "stop";
+        break;
+    case VIRT_VIEWER_SESSION_VM_ACTION_CONTINUE:
+        cmd = "cont";
+        break;
+    default:
+        g_return_if_reached();
+    }
+
+    qmp(self, cmd, NULL, NULL);
+}
+
 static void
 spice_port_opened(SpiceChannel *channel, GParamSpec *pspec G_GNUC_UNUSED,
                   VirtViewerSessionSpice *self)
@@ -1018,6 +1169,21 @@ spice_port_opened(SpiceChannel *channel, GParamSpec *pspec G_GNUC_UNUSED,
     g_return_if_fail(name != NULL);
     g_debug("port#%d %s: %s", id, name, opened ? "opened" : "closed");
 
+    if (g_str_equal(name, "org.qemu.monitor.qmp.0") && opened) {
+        g_return_if_fail(!self->priv->qmp);
+
+        g_object_set(virt_viewer_session_get_app(VIRT_VIEWER_SESSION(self)),
+                     "vm-ui", TRUE, NULL);
+
+        self->priv->qmp = port;
+        qmp(self, "qmp_capabilities", NULL, NULL);
+        qmp(self, "query-status", NULL, qmp_query_status_cb);
+
+        virt_viewer_signal_connect_object(port, "port-data",
+                                          G_CALLBACK(spice_qmp_data), self, G_CONNECT_SWAPPED);
+        goto end;
+    }
+
     vte = g_object_get_data(G_OBJECT(port), "virt-viewer-vte");
     if (vte) {
         if (opened)
-- 
2.18.0.321.gffc6fa0e39




More information about the virt-tools-list mailing list