Hi On Fri, Aug 10, 2018 at 11:26 PM, Victor Toso <victortoso@xxxxxxxxxx> wrote: > Hi, > > On Fri, Aug 10, 2018 at 03:44:19PM +0200, marcandre.lureau@xxxxxxxxxx wrote: >> From: Marc-André Lureau <marcandre.lureau@xxxxxxxxxx> >> >> 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. > > I'm a bit confused by the thread [0], the discussion with Daniel > ending at [1]. Why would it be so interesting to have this for > client == host use case only? My main motivation here is to have a light alternative to the QEMU UI (GTK/SDL etc), usually started from command line by developpers. The VT part is probably worth for a remote/general case. But for VM control, QEMU exits when the client exits. This is probably not what you'd want remotely. > > [0] https://www.redhat.com/archives/virt-tools-list/2018-July/msg00047.html > [1] https://www.redhat.com/archives/virt-tools-list/2018-August/msg00020.html > > I'm not knowledgeable about QMP but I take these API are stable? Yes > > | @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. > > I think I recall discussions around having a way to > power-on/off/reboot in spice and that it was rejected to keep > spice as a viewer, etc. Not sure what might have changed? It's obvious to me that libvirt is the preferred API if you need to "manage" the VM. Now, when you don't have libvirt, but qemu exposes a QMP port channel, it's an easy and flexible alternative. This doesn't have much to do with Spice, since QMP is defined by QEMU. But I'd rather have a QEMU-specific solution here, since my aim is simply to have a "QEMU UI" alternative. For anything else, I'd eventually try to generalize that and make it part of Spice. But so far, I think we have libvirt and libgovirt covering our needs. > I like the idea of rebooting a *remote* vm when it is stuck/etc > by triggering a client command that qemu would pick it up but I'm > a bit at lost with local case. > >> Note: this adds a strong dependency on json-glib for >> spice-client-glib, a widely available and fairly small >> library. > > I doubt that would be a problem. > > Cheers, > >> Signed-off-by: Marc-André Lureau <marcandre.lureau@xxxxxxxxxx> >> --- >> 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 >> >> _______________________________________________ >> Spice-devel mailing list >> Spice-devel@xxxxxxxxxxxxxxxxxxxxx >> https://lists.freedesktop.org/mailman/listinfo/spice-devel > > _______________________________________________ > Spice-devel mailing list > Spice-devel@xxxxxxxxxxxxxxxxxxxxx > https://lists.freedesktop.org/mailman/listinfo/spice-devel > -- Marc-André Lureau _______________________________________________ Spice-devel mailing list Spice-devel@xxxxxxxxxxxxxxxxxxxxx https://lists.freedesktop.org/mailman/listinfo/spice-devel