[Spice-devel] [PATCH spice-gtk] glib: add SpiceQmpPort helper

marcandre.lureau at redhat.com marcandre.lureau at redhat.com
Fri Aug 10 13:44:19 UTC 2018


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

Add a few helper functions to deal with a QMP port channel, in order
to ease json handling, and wrapping a few commands.

(by convention, the port should have the name
"org.qemu.monitor.qmp.0", but it's not strictly required)

This helper is put into use in the virt-viewer "Add QEMU-like UI: VT
console & basic VM status" series.

Note: this adds a strong dependency on json-glib for
spice-client-glib, a widely available and fairly small
library.

Signed-off-by: Marc-André Lureau <marcandre.lureau at redhat.com>
---
 configure.ac                         |   2 +
 doc/reference/spice-gtk-docs.xml     |   1 +
 doc/reference/spice-gtk-sections.txt |  27 ++
 src/Makefile.am                      |   6 +
 src/map-file                         |   9 +
 src/qmp-port.c                       | 571 +++++++++++++++++++++++++++
 src/qmp-port.h                       | 122 ++++++
 src/spice-client.h                   |   1 +
 src/spice-glib-sym-file              |   9 +
 9 files changed, 748 insertions(+)
 create mode 100644 src/qmp-port.c
 create mode 100644 src/qmp-port.h

diff --git a/configure.ac b/configure.ac
index 7b32e29..e686d76 100644
--- a/configure.ac
+++ b/configure.ac
@@ -173,6 +173,8 @@ PKG_CHECK_MODULES(CAIRO, cairo >= 1.2.0)
 
 PKG_CHECK_MODULES(GTHREAD, gthread-2.0)
 
+PKG_CHECK_MODULES(JSON, json-glib-1.0)
+
 AC_ARG_ENABLE([webdav],
   AS_HELP_STRING([--enable-webdav=@<:@auto/yes/no@:>@],
                  [Enable webdav support @<:@default=auto@:>@]),
diff --git a/doc/reference/spice-gtk-docs.xml b/doc/reference/spice-gtk-docs.xml
index db5dd3d..696c375 100644
--- a/doc/reference/spice-gtk-docs.xml
+++ b/doc/reference/spice-gtk-docs.xml
@@ -52,6 +52,7 @@
       <xi:include href="xml/spice-audio.xml"/>
       <xi:include href="xml/smartcard-manager.xml"/>
       <xi:include href="xml/usb-device-manager.xml"/>
+      <xi:include href="xml/qmp-port.xml"/>
       <xi:include href="xml/spice-util.xml"/>
       <xi:include href="xml/spice-version.xml"/>
       <xi:include href="xml/spice-uri.xml"/>
diff --git a/doc/reference/spice-gtk-sections.txt b/doc/reference/spice-gtk-sections.txt
index a85df7a..a0336aa 100644
--- a/doc/reference/spice-gtk-sections.txt
+++ b/doc/reference/spice-gtk-sections.txt
@@ -494,6 +494,33 @@ SPICE_PORT_CHANNEL_GET_CLASS
 SpicePortChannelPrivate
 </SECTION>
 
+<SECTION>
+<FILE>qmp-port</FILE>
+<TITLE>SpiceQmpPort</TITLE>
+
+<SUBSECTION>
+SpiceQmpPort
+SpiceQmpPortVmAction
+SpiceQmpStatus
+<SUBSECTION>
+spice_qmp_port_get
+spice_qmp_port_vm_action_async
+spice_qmp_port_vm_action_finish
+spice_qmp_port_query_status_async
+spice_qmp_port_query_status_finish
+spice_qmp_status_ref
+spice_qmp_status_unref
+<SUBSECTION Standard>
+SPICE_IS_QMP_PORT
+SPICE_IS_QMP_PORT_CLASS
+SPICE_QMP_PORT
+SPICE_QMP_PORT_CLASS
+SPICE_QMP_PORT_GET_CLASS
+SPICE_TYPE_QMP_PORT
+spice_qmp_port_get_type
+spice_qmp_status_get_type
+</SECTION>
+
 <SECTION>
 <FILE>spice-uri</FILE>
 spice_uri_get_scheme
diff --git a/src/Makefile.am b/src/Makefile.am
index afad922..8f8ccdf 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -77,6 +77,7 @@ SPICE_COMMON_CPPFLAGS =						\
 	$(GLIB2_CFLAGS)						\
 	$(GIO_CFLAGS)						\
 	$(GOBJECT2_CFLAGS)					\
+	$(JSON_CFLAGS)						\
 	$(SSL_CFLAGS)						\
 	$(SASL_CFLAGS)						\
 	$(GSTAUDIO_CFLAGS)					\
@@ -181,6 +182,7 @@ libspice_client_glib_2_0_la_LIBADD =					\
 	$(GIO_LIBS)							\
 	$(GOBJECT2_LIBS)						\
 	$(JPEG_LIBS)							\
+	$(JSON_LIBS)							\
 	$(Z_LIBS)							\
 	$(LZ4_LIBS)							\
 	$(PIXMAN_LIBS)							\
@@ -242,6 +244,8 @@ libspice_client_glib_2_0_la_SOURCES =			\
 	channel-smartcard.c				\
 	channel-usbredir.c				\
 	channel-usbredir-priv.h				\
+	qmp-port.c					\
+	qmp-port.h					\
 	smartcard-manager.c				\
 	smartcard-manager-priv.h			\
 	spice-uri.c					\
@@ -292,6 +296,7 @@ libspice_client_glibinclude_HEADERS =	\
 	channel-smartcard.h		\
 	channel-usbredir.h		\
 	channel-webdav.h		\
+	qmp-port.h			\
 	usb-device-manager.h		\
 	smartcard-manager.h		\
 	spice-file-transfer-task.h	\
@@ -525,6 +530,7 @@ glib_introspection_files =				\
 	channel-record.c				\
 	channel-smartcard.c				\
 	channel-usbredir.c				\
+	qmp-port.c					\
 	smartcard-manager.c				\
 	usb-device-manager.c				\
 	$(NULL)
diff --git a/src/map-file b/src/map-file
index cdb81c3..500683c 100644
--- a/src/map-file
+++ b/src/map-file
@@ -117,6 +117,15 @@ spice_port_channel_write_finish;
 spice_port_event;
 spice_port_write_async;
 spice_port_write_finish;
+spice_qmp_port_get;
+spice_qmp_port_get_type;
+spice_qmp_port_query_status_async;
+spice_qmp_port_query_status_finish;
+spice_qmp_port_vm_action_async;
+spice_qmp_port_vm_action_finish;
+spice_qmp_status_get_type;
+spice_qmp_status_ref;
+spice_qmp_status_unref;
 spice_record_channel_get_type;
 spice_record_channel_send_data;
 spice_record_send_data;
diff --git a/src/qmp-port.c b/src/qmp-port.c
new file mode 100644
index 0000000..04ebc98
--- /dev/null
+++ b/src/qmp-port.c
@@ -0,0 +1,571 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+   Copyright (C) 2018 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, see <http://www.gnu.org/licenses/>.
+*/
+#include "config.h"
+
+#define _GNU_SOURCE
+#include <string.h>
+#include <json-glib/json-glib.h>
+#include "spice-client.h"
+
+/**
+ * SECTION:qmp-port
+ * @short_description: QMP port helper
+ * @title: QMP port channel helper
+ * @section_id:
+ * @see_also: #SpicePortChannel
+ * @stability: Stable
+ * @include: spice-client.h
+ *
+ * A helper to handle QMP messages over a %SpicePortChannel.
+ *
+ * Since: 0.36
+ */
+
+typedef struct _SpiceQmpPortPrivate SpiceQmpPortPrivate;
+
+struct _SpiceQmpPortPrivate
+{
+    SpicePortChannel *channel;
+    gboolean ready;
+
+    gint id_counter;
+    GString *qmp_data;
+    JsonParser *qmp_parser;
+    GHashTable *qmp_tasks;
+};
+
+struct _SpiceQmpPort
+{
+    GObject parent;
+
+    SpiceQmpPortPrivate *priv;
+};
+
+struct _SpiceQmpPortClass
+{
+    GObjectClass parent_class;
+};
+
+enum {
+    PROP_CHANNEL = 1,
+    PROP_READY,
+
+    PROP_LAST,
+};
+
+enum {
+    SIGNAL_EVENT,
+
+    SIGNAL_LAST,
+};
+
+static guint signals[SIGNAL_LAST];
+static GParamSpec *props[PROP_LAST] = { NULL, };
+
+G_DEFINE_TYPE_WITH_PRIVATE(SpiceQmpPort, spice_qmp_port, G_TYPE_OBJECT)
+
+G_DEFINE_BOXED_TYPE(SpiceQmpStatus, spice_qmp_status, spice_qmp_status_ref, spice_qmp_status_unref)
+
+typedef void (QMPCb)(GTask *task, JsonNode *node);
+
+static void
+qmp_error_return(GTask *task, const gchar *desc)
+{
+    g_task_return_new_error(task, SPICE_CLIENT_ERROR,
+                            SPICE_CLIENT_ERROR_FAILED, "%s", desc);
+    g_object_unref(task);
+}
+
+static void
+spice_qmp_dispatch_message(SpiceQmpPort *self)
+{
+    JsonObject *obj = json_node_get_object(json_parser_get_root(self->priv->qmp_parser));
+    JsonNode *node;
+    GTask *task;
+    const gchar *event;
+
+    if (!self->priv->ready && json_object_get_member(obj, "QMP")) {
+        g_debug("QMP greeting received");
+    } else if ((node = json_object_get_member(obj, "error"))) {
+        gint id = json_object_get_int_member(obj, "id");
+        const gchar *desc = json_object_get_string_member(obj, "desc");
+
+        g_debug("QMP return error: %s, id:%d", desc, id);
+        if ((task = g_hash_table_lookup(self->priv->qmp_tasks, GINT_TO_POINTER(id)))) {
+            g_hash_table_steal(self->priv->qmp_tasks, GINT_TO_POINTER(id));
+            qmp_error_return(task, desc);
+        }
+    } else if ((node = json_object_get_member(obj, "return"))) {
+        gint id = json_object_get_int_member(obj, "id");
+
+        g_debug("QMP return id:%d", id);
+        if (!self->priv->ready && id == 0) {
+            self->priv->ready = TRUE;
+            g_object_notify(G_OBJECT(self), "ready");
+        }
+
+        if (!self->priv->ready) {
+            g_warning("Invalid QMP return, not ready");
+        } else if ((task = g_hash_table_lookup(self->priv->qmp_tasks, GINT_TO_POINTER(id)))) {
+            QMPCb *cb = g_task_get_task_data(task);
+            g_hash_table_steal(self->priv->qmp_tasks, GINT_TO_POINTER(id));
+            cb(task, node);
+        } else {
+            g_debug("No task associated with return");
+        }
+    } else if ((event = json_object_get_string_member(obj, "event"))) {
+        g_debug("QMP event %s", event);
+        g_signal_emit(G_OBJECT(self), signals[SIGNAL_EVENT], 0, event,
+                      json_object_get_member(obj, "data"));
+    } else {
+        g_debug("unhandled JSON %s", self->priv->qmp_data->str);
+    }
+}
+
+#define QMP_MAX_RESPONSE (10 * 1024 * 1024)
+
+static void
+spice_qmp_data(SpiceQmpPort *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);
+    if (qmp->len > QMP_MAX_RESPONSE) {
+        g_warning("QMP response is too large, over %d bytes, truncating",
+                  QMP_MAX_RESPONSE);
+        g_string_set_size(qmp, 0);
+        return;
+    }
+
+    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 {
+            spice_qmp_dispatch_message(self);
+        }
+        str = crlf + 2;
+    }
+
+    g_string_erase(qmp, 0, str - qmp->str);
+}
+
+static void qmp_task_cancelled_cb(gpointer data)
+{
+    qmp_error_return(G_TASK(data), "Task got cancelled");
+}
+
+static void spice_qmp_port_init(SpiceQmpPort *self)
+{
+     self->priv = spice_qmp_port_get_instance_private(self);
+     self->priv->qmp_data = g_string_sized_new(256);
+     self->priv->qmp_parser = json_parser_new();
+     self->priv->qmp_tasks = g_hash_table_new_full(g_direct_hash, g_direct_equal,
+                                                   NULL, qmp_task_cancelled_cb);
+}
+
+static void spice_qmp_port_dispose(GObject *gobject)
+{
+    SpiceQmpPort *self = SPICE_QMP_PORT(gobject);
+
+    g_string_free(self->priv->qmp_data, TRUE);
+    g_object_unref(self->priv->qmp_parser);
+    g_hash_table_unref(self->priv->qmp_tasks);
+
+    if (G_OBJECT_CLASS(spice_qmp_port_parent_class)->dispose)
+        G_OBJECT_CLASS(spice_qmp_port_parent_class)->dispose(gobject);
+}
+
+static void spice_qmp_port_constructed(GObject *gobject)
+{
+    SpiceQmpPort *self = SPICE_QMP_PORT(gobject);
+
+    g_object_set_data_full(G_OBJECT(self->priv->channel),
+                           "spice-qmp-port", self, g_object_unref);
+
+    spice_g_signal_connect_object(self->priv->channel,
+                                  "port-data", G_CALLBACK(spice_qmp_data),
+                                  self, G_CONNECT_SWAPPED);
+
+    if (G_OBJECT_CLASS(spice_qmp_port_parent_class)->constructed)
+        G_OBJECT_CLASS(spice_qmp_port_parent_class)->constructed(gobject);
+}
+
+static void
+spice_qmp_port_set_property(GObject *object,
+                            guint property_id,
+                            const GValue *value,
+                            GParamSpec *pspec)
+{
+    SpiceQmpPort *self = SPICE_QMP_PORT(object);
+
+    switch (property_id) {
+    case PROP_CHANNEL:
+        g_clear_object(&self->priv->channel);
+        self->priv->channel = g_value_dup_object(value);
+        break;
+
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+        break;
+    }
+}
+
+static void
+spice_qmp_port_get_property(GObject *object,
+                            guint property_id,
+                            GValue *value,
+                            GParamSpec *pspec)
+{
+    SpiceQmpPort *self = SPICE_QMP_PORT(object);
+
+    switch (property_id) {
+    case PROP_CHANNEL:
+        g_value_set_object(value, self->priv->channel);
+        break;
+
+    case PROP_READY:
+        g_value_set_boolean(value, self->priv->ready);
+        break;
+
+    default:
+        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+        break;
+    }
+}
+
+static void spice_qmp_port_class_init(SpiceQmpPortClass *klass)
+{
+    GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
+
+    gobject_class->dispose = spice_qmp_port_dispose;
+    gobject_class->get_property = spice_qmp_port_get_property;
+    gobject_class->set_property = spice_qmp_port_set_property;
+    gobject_class->constructed = spice_qmp_port_constructed;
+
+    /**
+     * SpiceQmpPort::event:
+     * @self: the #SpiceQmpPort that emitted the signal
+     * @name: the QMP event name
+     * @node: the event data json-node, or NULL
+     *
+     * Event emitted whenever a QMP event is received.
+     *
+     * Since: 0.36
+     */
+    signals[SIGNAL_EVENT] =
+        g_signal_new("event",
+                     G_OBJECT_CLASS_TYPE(gobject_class),
+                     G_SIGNAL_RUN_FIRST,
+                     0,
+                     NULL, NULL, NULL,
+                     G_TYPE_NONE,
+                     2, G_TYPE_STRING, G_TYPE_POINTER);
+
+    props[PROP_CHANNEL] =
+        g_param_spec_object("channel",
+                            "Channel",
+                            "Associated port channel",
+                            SPICE_TYPE_PORT_CHANNEL,
+                            G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+    props[PROP_READY] =
+        g_param_spec_boolean("ready",
+                             "Ready",
+                             "Whether the QMP port is ready",
+                             FALSE,
+                             G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+    g_object_class_install_properties(gobject_class, PROP_LAST, props);
+ }
+
+static void
+qmp_empty_return_cb(GTask *task, JsonNode *node)
+{
+    g_task_return_boolean(task, TRUE);
+    g_object_unref(task);
+}
+
+static void
+spice_qmp_port_write_finished(GObject *source_object,
+                              GAsyncResult *res,
+                              gpointer t)
+{
+    SpicePortChannel *port = SPICE_PORT_CHANNEL(source_object);
+    GTask *task = G_TASK(t);
+    SpiceQmpPort *self = g_task_get_source_object(task);
+    gint id = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(task), "qmp-id"));
+    GError *err = NULL;
+
+    spice_port_channel_write_finish(port, res, &err);
+    if (err) {
+        g_hash_table_steal(self->priv->qmp_tasks, GINT_TO_POINTER(id));
+        qmp_error_return(task, err->message);
+        g_error_free(err);
+    }
+}
+
+static void
+qmp(SpiceQmpPort *self, GTask *task,
+    const char *cmd, const gchar *args)
+{
+    GString *str = g_string_sized_new(256);
+    gsize len;
+    gchar *data;
+    gint id = self->priv->id_counter;
+
+    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, " }");
+
+    g_hash_table_insert(self->priv->qmp_tasks, GINT_TO_POINTER(id), task);
+
+    len = str->len;
+    data = g_string_free(str, FALSE);
+    spice_port_channel_write_async(self->priv->channel, data, len,
+                                   g_task_get_cancellable(task),
+                                   spice_qmp_port_write_finished, task);
+    g_object_set_data_full(G_OBJECT(task), "qmp-data", data, g_free);
+    g_object_set_data(G_OBJECT(task), "qmp-id", GINT_TO_POINTER(id));
+
+    self->priv->id_counter++;
+}
+
+/**
+ * spice_qmp_port_vm_action_finish:
+ * @self: a qmp port helper
+ * @result: The async #GAsyncResult result
+ * @error: a #GError pointer, or %NULL
+ *
+ * Finishes asynchronous VM action and returns the result.
+ *
+ * Since: 0.36
+ **/
+gboolean spice_qmp_port_vm_action_finish(SpiceQmpPort *self,
+                                         GAsyncResult *result,
+                                         GError **error)
+{
+    g_return_val_if_fail(SPICE_IS_QMP_PORT(self), FALSE);
+    g_return_val_if_fail(g_task_is_valid(result, self), FALSE);
+
+    return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+/**
+ * spice_qmp_port_vm_action_async:
+ * @self: a qmp port helper
+ * @action: a VM action
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: callback to call when the action is complete
+ * @user_data: the data to pass to the callback function
+ *
+ * Request the VM to perform an action.
+ *
+ * Since: 0.36
+ **/
+void spice_qmp_port_vm_action_async(SpiceQmpPort *self,
+                                    SpiceQmpPortVmAction action,
+                                    GCancellable *cancellable,
+                                    GAsyncReadyCallback callback,
+                                    gpointer user_data)
+{
+    GTask *task;
+    const gchar *cmd;
+
+    g_return_if_fail(SPICE_IS_QMP_PORT(self));
+    g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable));
+    g_return_if_fail(self->priv->ready);
+    g_return_if_fail(action >= 0 && action < SPICE_QMP_PORT_VM_ACTION_LAST);
+
+    task = g_task_new(self, cancellable, callback, user_data);
+    g_task_set_task_data(task, qmp_empty_return_cb, NULL);
+
+    switch (action) {
+    case SPICE_QMP_PORT_VM_ACTION_QUIT:
+        cmd = "quit";
+        break;
+    case SPICE_QMP_PORT_VM_ACTION_RESET:
+        cmd = "system_reset";
+        break;
+    case SPICE_QMP_PORT_VM_ACTION_POWER_DOWN:
+        cmd = "system_powerdown";
+        break;
+    case SPICE_QMP_PORT_VM_ACTION_PAUSE:
+        cmd = "stop";
+        break;
+    case SPICE_QMP_PORT_VM_ACTION_CONTINUE:
+        cmd = "cont";
+        break;
+    default:
+        g_return_if_reached();
+    }
+
+    qmp(self, task, cmd, NULL);
+}
+
+static void
+qmp_capabilities_cb(GTask *task, JsonNode *node)
+{
+    g_task_return_boolean(task, TRUE);
+    g_object_unref(task);
+}
+
+/**
+ * spice_qmp_port_get:
+ * @channel: the QMP port channel
+ *
+ * Associate a QMP port helper to the given port channel.  If there is
+ * already a helper associated with the channel, it is simply returned.
+ *
+ * Returns: (transfer none): a weak reference to the associated SpiceQmpPort
+ *
+ * Since: 0.36
+ **/
+SpiceQmpPort *spice_qmp_port_get(SpicePortChannel *channel)
+{
+    GObject *self;
+
+    g_return_val_if_fail(SPICE_IS_PORT_CHANNEL(channel), NULL);
+
+    self = g_object_get_data(G_OBJECT(channel), "spice-qmp-port");
+
+    if (self == NULL) {
+        GTask *task;
+
+        self = g_object_new(SPICE_TYPE_QMP_PORT, "channel", channel, NULL);
+        task = g_task_new(self, NULL, NULL, NULL);
+        g_task_set_task_data(task, qmp_capabilities_cb, NULL);
+        qmp(SPICE_QMP_PORT(self), task, "qmp_capabilities", NULL);
+    }
+
+    return SPICE_QMP_PORT(self);
+}
+
+/**
+ * spice_qmp_status_ref:
+ * @status: a #SpiceQmpStatus
+ *
+ * References a @status.
+ *
+ * Returns: The same @status
+ *
+ * Since: 0.36
+ **/
+SpiceQmpStatus *
+spice_qmp_status_ref(SpiceQmpStatus *status)
+{
+    g_return_val_if_fail(status != NULL, NULL);
+
+    status->ref++;
+
+    return status;
+}
+
+/**
+ * spice_qmp_status_unref:
+ * @status: a #SpiceQmpStatus
+ *
+ * Removes a reference from the given @status.
+ *
+ * Since: 0.36
+ **/
+void spice_qmp_status_unref(SpiceQmpStatus *status)
+{
+    if (status && status->ref-- == 0) {
+        g_free(status->status);
+        g_free(status);
+    }
+}
+
+static void
+qmp_query_status_return_cb(GTask *task, JsonNode *node)
+{
+    SpiceQmpStatus *status = g_new0(SpiceQmpStatus, 1);
+    JsonObject *obj = json_node_get_object(node);
+
+    status->version = 1;
+    status->ref = 1;
+    status->running = json_object_get_boolean_member(obj, "running");
+    status->singlestep = json_object_get_boolean_member(obj, "singlestep");
+    status->status = g_strdup(json_object_get_string_member(obj, "status"));
+
+    g_task_return_pointer(task, status, (GDestroyNotify)spice_qmp_status_unref);
+    g_object_unref(task);
+}
+
+/**
+ * spice_qmp_port_query_status_async:
+ * @self: A #SpiceQmpPort
+ * @cancellable: A #GCancellable
+ * @callback: The async callback.
+ * @user_data: The async callback user data.
+ *
+ * Query the run status of all VCPUs.
+ *
+ * Since: 0.36
+ **/
+void spice_qmp_port_query_status_async(SpiceQmpPort *self,
+                                       GCancellable *cancellable,
+                                       GAsyncReadyCallback callback,
+                                       gpointer user_data)
+{
+    GTask *task;
+
+    g_return_if_fail(SPICE_IS_QMP_PORT(self));
+    g_return_if_fail(cancellable == NULL || G_IS_CANCELLABLE(cancellable));
+    g_return_if_fail(self->priv->ready);
+
+    task = g_task_new(self, cancellable, callback, user_data);
+    g_task_set_task_data(task, qmp_query_status_return_cb, NULL);
+
+    qmp(self, task, "query-status", NULL);
+}
+
+/**
+ * spice_qmp_port_query_status_finish:
+ * @self: A #SpiceQmpPort
+ * @result: The async #GAsyncResult result
+ * @error: a #GError pointer, or %NULL
+ *
+ * Finish the asynchronous status query.
+ *
+ * Returns: The #SpiceQmpStatus result or %NULL, in which case @error
+ * will be set.
+ *
+ * Since: 0.36
+ **/
+SpiceQmpStatus *
+spice_qmp_port_query_status_finish(SpiceQmpPort *self,
+                                   GAsyncResult *result,
+                                   GError **error)
+{
+    g_return_val_if_fail(SPICE_IS_QMP_PORT(self), NULL);
+    g_return_val_if_fail(g_task_is_valid(result, self), NULL);
+
+    return g_task_propagate_pointer(G_TASK(result), error);
+}
diff --git a/src/qmp-port.h b/src/qmp-port.h
new file mode 100644
index 0000000..a8a4e30
--- /dev/null
+++ b/src/qmp-port.h
@@ -0,0 +1,122 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+  Copyright (C) 2018 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, see <http://www.gnu.org/licenses/>.
+*/
+#ifndef QMP_PORT_H_
+#define QMP_PORT_H_
+
+#if !defined(__SPICE_CLIENT_H_INSIDE__) && !defined(SPICE_COMPILATION)
+#warning "Only <spice-client.h> can be included directly"
+#endif
+
+#include <glib-object.h>
+#include "channel-port.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_QMP_PORT            (spice_qmp_port_get_type ())
+#define SPICE_QMP_PORT(obj)            (G_TYPE_CHECK_INSTANCE_CAST ((obj), SPICE_TYPE_QMP_PORT, SpiceQmpPort))
+#define SPICE_QMP_PORT_CLASS(klass)    (G_TYPE_CHECK_CLASS_CAST ((klass), SPICE_TYPE_QMP_PORT, SpiceQmpPortClass))
+#define SPICE_IS_QMP_PORT(obj)         (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SPICE_TYPE_QMP_PORT))
+#define SPICE_IS_QMP_PORT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SPICE_TYPE_QMP_PORT))
+#define SPICE_QMP_PORT_GET_CLASS(obj)  (G_TYPE_INSTANCE_GET_CLASS ((obj), SPICE_TYPE_QMP_PORT, SpiceQmpPortClass))
+
+/**
+ * SpiceQmpPort:
+ *
+ * Opaque data structure.
+ * Since: 0.36
+ */
+typedef struct _SpiceQmpPort SpiceQmpPort;
+typedef struct _SpiceQmpPortClass SpiceQmpPortClass;
+
+/**
+ * SpiceQmpPortVmAction:
+ * @SPICE_QMP_PORT_VM_ACTION_QUIT: This command will cause the VM process to exit gracefully.
+ * @SPICE_QMP_PORT_VM_ACTION_RESET: Performs a hard reset of the VM.
+ * @SPICE_QMP_PORT_VM_ACTION_POWER_DOWN: Performs a power down operation.
+ * @SPICE_QMP_PORT_VM_ACTION_PAUSE: Stop all VCPU execution.
+ * @SPICE_QMP_PORT_VM_ACTION_CONTINUE: Resume all VCPU execution.
+ * @SPICE_QMP_PORT_VM_ACTION_LAST: the last enum value.
+ *
+ * An action to perform on the VM.
+ *
+ * Since: 0.36
+ **/
+typedef enum SpiceQmpPortVmAction {
+    SPICE_QMP_PORT_VM_ACTION_QUIT,
+    SPICE_QMP_PORT_VM_ACTION_RESET,
+    SPICE_QMP_PORT_VM_ACTION_POWER_DOWN,
+    SPICE_QMP_PORT_VM_ACTION_PAUSE,
+    SPICE_QMP_PORT_VM_ACTION_CONTINUE,
+
+    SPICE_QMP_PORT_VM_ACTION_LAST,
+} SpiceQmpPortVmAction;
+
+/**
+ * SpiceQmpStatus:
+ * @version: the structure version
+ * @running: true if all VCPUs are runnable, false if not runnable
+ * @singlestep: true if VCPUs are in single-step mode
+ * @status: the virtual machine run state
+ *
+ * Information about VCPU run state.
+ *
+ * Since: 0.36
+ **/
+typedef struct _SpiceQmpStatus {
+    /*< private >*/
+    gint ref;
+
+    /*< public >*/
+    gint version;
+
+    gboolean running;
+    gboolean singlestep;
+    gchar *status;
+} SpiceQmpStatus;
+
+GType spice_qmp_port_get_type(void);
+
+SpiceQmpPort *spice_qmp_port_get(SpicePortChannel *channel);
+
+void spice_qmp_port_vm_action_async(SpiceQmpPort *self,
+                                    SpiceQmpPortVmAction action,
+                                    GCancellable *cancellable,
+                                    GAsyncReadyCallback callback,
+                                    gpointer user_data);
+
+gboolean spice_qmp_port_vm_action_finish(SpiceQmpPort *self,
+                                         GAsyncResult *result,
+                                         GError **error);
+
+GType spice_qmp_status_get_type(void);
+
+SpiceQmpStatus *spice_qmp_status_ref(SpiceQmpStatus *status);
+void spice_qmp_status_unref(SpiceQmpStatus *status);
+
+void spice_qmp_port_query_status_async(SpiceQmpPort *self,
+                                       GCancellable *cancellable,
+                                       GAsyncReadyCallback callback,
+                                       gpointer user_data);
+
+SpiceQmpStatus *spice_qmp_port_query_status_finish(SpiceQmpPort *self,
+                                                   GAsyncResult *result,
+                                                   GError **error);
+
+G_END_DECLS
+
+#endif /* QMP_PORT_H_ */
diff --git a/src/spice-client.h b/src/spice-client.h
index 32b79ea..77362b9 100644
--- a/src/spice-client.h
+++ b/src/spice-client.h
@@ -51,6 +51,7 @@
 #include "usb-device-manager.h"
 #include "spice-audio.h"
 #include "spice-file-transfer-task.h"
+#include "qmp-port.h"
 
 G_BEGIN_DECLS
 
diff --git a/src/spice-glib-sym-file b/src/spice-glib-sym-file
index b19844c..2df1cc0 100644
--- a/src/spice-glib-sym-file
+++ b/src/spice-glib-sym-file
@@ -96,6 +96,15 @@ spice_port_channel_write_finish
 spice_port_event
 spice_port_write_async
 spice_port_write_finish
+spice_qmp_port_get
+spice_qmp_port_get_type
+spice_qmp_port_query_status_async
+spice_qmp_port_query_status_finish
+spice_qmp_port_vm_action_async
+spice_qmp_port_vm_action_finish
+spice_qmp_status_get_type
+spice_qmp_status_ref
+spice_qmp_status_unref
 spice_record_channel_get_type
 spice_record_channel_send_data
 spice_record_send_data
-- 
2.18.0.547.g1d89318c48



More information about the Spice-devel mailing list