[Spice-devel] [PATCH spice-gtk 2/3] Add a port channel
Marc-André Lureau
marcandre.lureau at gmail.com
Fri Nov 30 04:43:45 PST 2012
A Spice port channel carry arbitrary data between the Spice client and
the Spice server. It may be used to provide additional services on top
of a Spice connection. For example, a channel can be associated with
the qemu monitor for the client to interact with it, just like any
qemu chardev. Or it may be used with various protocols, such as the
Spice Controller.
A port kind is identified simply by a fqdn, such as org.qemu.monitor,
org.spice.spicy.test or org.ovirt.controller...
---
doc/reference/spice-gtk-docs.xml | 1 +
doc/reference/spice-gtk-sections.txt | 21 ++
doc/reference/spice-gtk.types | 1 +
gtk/Makefile.am | 3 +
gtk/channel-port.c | 425 +++++++++++++++++++++++++++++++++++
gtk/channel-port.h | 76 +++++++
gtk/map-file | 4 +
gtk/spice-channel.c | 4 +
gtk/spice-client.h | 1 +
9 files changed, 536 insertions(+)
create mode 100644 gtk/channel-port.c
create mode 100644 gtk/channel-port.h
diff --git a/doc/reference/spice-gtk-docs.xml b/doc/reference/spice-gtk-docs.xml
index 82cdce8..4a9a3cf 100644
--- a/doc/reference/spice-gtk-docs.xml
+++ b/doc/reference/spice-gtk-docs.xml
@@ -36,6 +36,7 @@
<xi:include href="xml/channel-record.xml"/>
<xi:include href="xml/channel-smartcard.xml"/>
<xi:include href="xml/channel-usbredir.xml"/>
+ <xi:include href="xml/channel-port.xml"/>
</chapter>
<chapter>
diff --git a/doc/reference/spice-gtk-sections.txt b/doc/reference/spice-gtk-sections.txt
index 87d4225..d82a886 100644
--- a/doc/reference/spice-gtk-sections.txt
+++ b/doc/reference/spice-gtk-sections.txt
@@ -403,3 +403,24 @@ SPICE_DEPRECATED_FOR
spice_g_signal_connect_object
</SECTION>
+<SECTION>
+<FILE>channel-port</FILE>
+<TITLE>SpicePortChannel</TITLE>
+SpicePortChannel
+SpicePortChannelClass
+<SUBSECTION>
+spice_port_event
+spice_port_write_async
+spice_port_write_finish
+<SUBSECTION Standard>
+SPICE_PORT_CHANNEL
+SPICE_IS_PORT_CHANNEL
+SPICE_TYPE_PORT_CHANNEL
+spice_port_channel_get_type
+SPICE_PORT_CHANNEL_CLASS
+SPICE_IS_PORT_CHANNEL_CLASS
+SPICE_PORT_CHANNEL_GET_CLASS
+<SUBSECTION Private>
+SpicePortChannelPrivate
+</SECTION>
+
diff --git a/doc/reference/spice-gtk.types b/doc/reference/spice-gtk.types
index ff80277..2f52845 100644
--- a/doc/reference/spice-gtk.types
+++ b/doc/reference/spice-gtk.types
@@ -42,3 +42,4 @@ spice_usbredir_channel_get_type
spice_usb_device_get_type
spice_usb_device_manager_get_type
spice_usb_device_widget_get_type
+spice_port_channel_get_type
\ No newline at end of file
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index 06cc6e1..d38ba7a 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -236,6 +236,7 @@ libspice_client_glib_2_0_la_SOURCES = \
channel-inputs.c \
channel-main.c \
channel-playback.c \
+ channel-port.c \
channel-record.c \
channel-smartcard.c \
channel-usbredir.c \
@@ -277,6 +278,7 @@ libspice_client_glibinclude_HEADERS = \
channel-inputs.h \
channel-main.h \
channel-playback.h \
+ channel-port.h \
channel-record.h \
channel-smartcard.h \
channel-usbredir.h \
@@ -589,6 +591,7 @@ glib_introspection_files = \
channel-inputs.c \
channel-main.c \
channel-playback.c \
+ channel-port.c \
channel-record.c \
channel-smartcard.c \
channel-usbredir.c \
diff --git a/gtk/channel-port.c b/gtk/channel-port.c
new file mode 100644
index 0000000..e1f61d2
--- /dev/null
+++ b/gtk/channel-port.c
@@ -0,0 +1,425 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 "spice-client.h"
+#include "spice-common.h"
+#include "spice-channel-priv.h"
+#include "spice-marshal.h"
+
+/**
+ * SECTION:channel-port
+ * @short_description: private communication channel
+ * @title: Port Channel
+ * @section_id:
+ * @see_also: #SpiceChannel
+ * @stability: Stable
+ * @include: channel-port.h
+ *
+ * A Spice port channel carry arbitrary data between the Spice client
+ * and the Spice server. It may be used to provide additional
+ * services on top of a Spice connection. For example, a channel can
+ * be associated with the qemu monitor for the client to interact
+ * with it, just like any qemu chardev. Or it may be used with
+ * various protocols, such as the Spice Controller.
+ *
+ * A port kind is identified simply by a fqdn, such as
+ * org.qemu.monitor, org.spice.spicy.test or org.ovirt.controller...
+ *
+ * Once connected and initialized, the client may read the name of the
+ * port via SpicePortChannel:port-name.
+
+ * When the other end of the port is ready,
+ * SpicePortChannel:port-opened is set to %TRUE and you can start
+ * receiving data via the signal SpicePortChannel::port-data, or
+ * sending data via spice_port_write_async().
+ *
+ * Since: 0.15
+ */
+
+#define SPICE_PORT_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_PORT_CHANNEL, SpicePortChannelPrivate))
+
+struct _SpicePortChannelPrivate {
+ gchar *name;
+ gboolean opened;
+};
+
+G_DEFINE_TYPE(SpicePortChannel, spice_port_channel, SPICE_TYPE_CHANNEL)
+
+/* Properties */
+enum {
+ PROP_0,
+ PROP_PORT_NAME,
+ PROP_PORT_OPENED,
+};
+
+/* Signals */
+enum {
+ SPICE_PORT_DATA,
+ SPICE_PORT_EVENT,
+ LAST_SIGNAL,
+};
+
+static guint signals[LAST_SIGNAL];
+
+static void spice_port_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
+
+static void spice_port_channel_init(SpicePortChannel *channel)
+{
+ channel->priv = SPICE_PORT_CHANNEL_GET_PRIVATE(channel);
+}
+
+static void spice_port_get_property(GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ SpicePortChannelPrivate *c = SPICE_PORT_CHANNEL(object)->priv;
+
+ switch (prop_id) {
+ case PROP_PORT_NAME:
+ g_value_set_string(value, c->name);
+ break;
+ case PROP_PORT_OPENED:
+ g_value_set_boolean(value, c->opened);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
+ break;
+ }
+}
+
+static void spice_port_channel_finalize(GObject *object)
+{
+ SpicePortChannelPrivate *c = SPICE_PORT_CHANNEL(object)->priv;
+
+ g_free(c->name);
+
+ if (G_OBJECT_CLASS(spice_port_channel_parent_class)->finalize)
+ G_OBJECT_CLASS(spice_port_channel_parent_class)->finalize(object);
+}
+
+static void spice_port_channel_reset(SpiceChannel *channel, gboolean migrating)
+{
+ SpicePortChannelPrivate *c = SPICE_PORT_CHANNEL(channel)->priv;
+
+ g_clear_pointer(&c->name, g_free);
+ c->opened = FALSE;
+
+ SPICE_CHANNEL_CLASS(spice_port_channel_parent_class)->channel_reset(channel, migrating);
+}
+
+static void spice_port_channel_class_init(SpicePortChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->finalize = spice_port_channel_finalize;
+ gobject_class->get_property = spice_port_get_property;
+ channel_class->handle_msg = spice_port_handle_msg;
+ channel_class->channel_reset = spice_port_channel_reset;
+
+ g_object_class_install_property
+ (gobject_class, PROP_PORT_NAME,
+ g_param_spec_string("port-name",
+ "Port name",
+ "Port name",
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ g_object_class_install_property
+ (gobject_class, PROP_PORT_OPENED,
+ g_param_spec_boolean("port-opened",
+ "Port opened",
+ "Port opened",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+ /**
+ * SpicePort::port-data:
+ * @channel: the channel that emitted the signal
+ * @data: the data received
+ * @size: number of bytes read
+ *
+ * The #SpicePortChannel::port-data signal is emitted when new
+ * port data is received.
+ * Since: 0.15
+ **/
+ signals[SPICE_PORT_DATA] =
+ g_signal_new("port-data",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_user_marshal_VOID__POINTER_INT,
+ G_TYPE_NONE,
+ 2,
+ G_TYPE_POINTER, G_TYPE_INT);
+
+
+ /**
+ * SpicePort::port-event:
+ * @channel: the channel that emitted the signal
+ * @event: the event received
+ * @size: number of bytes read
+ *
+ * The #SpicePortChannel::port-event signal is emitted when new
+ * port event is received.
+ * Since: 0.15
+ **/
+ signals[SPICE_PORT_EVENT] =
+ g_signal_new("port-event",
+ G_OBJECT_CLASS_TYPE(gobject_class),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL,
+ g_cclosure_marshal_VOID__INT,
+ G_TYPE_NONE,
+ 1,
+ G_TYPE_INT);
+
+ g_type_class_add_private(klass, sizeof(SpicePortChannelPrivate));
+}
+
+/* signal trampoline---------------------------------------------------------- */
+
+struct SPICE_PORT_DATA {
+ uint8_t *data;
+ gsize data_size;
+};
+
+struct SPICE_PORT_EVENT {
+ int event;
+};
+
+/* main context */
+static void do_emit_main_context(GObject *object, int signum, gpointer params)
+{
+ switch (signum) {
+ case SPICE_PORT_DATA: {
+ struct SPICE_PORT_DATA *p = params;
+ g_signal_emit(object, signals[signum], 0, p->data, p->data_size);
+ break;
+ }
+ case SPICE_PORT_EVENT: {
+ struct SPICE_PORT_EVENT *p = params;
+ g_signal_emit(object, signals[signum], 0, p->event);
+ break;
+ }
+ default:
+ g_warn_if_reached();
+ }
+}
+
+/* coroutine context */
+static void port_set_opened(SpicePortChannel *self, gboolean opened)
+{
+ SpicePortChannelPrivate *c = self->priv;
+
+ if (c->opened == opened)
+ return;
+
+ c->opened = opened;
+ g_object_notify_main_context(G_OBJECT(self), "port-opened");
+}
+
+/* coroutine context */
+static void port_handle_init(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePortChannel *self = SPICE_PORT_CHANNEL(channel);
+ SpicePortChannelPrivate *c = self->priv;
+ SpiceMsgPortInit *init = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "init: %s %d", init->name, init->opened);
+ g_return_if_fail(init->name != NULL && *init->name != '\0');
+ g_return_if_fail(c->name == NULL);
+
+ c->name = g_strdup((gchar*)init->name);
+ g_object_notify(G_OBJECT(channel), "port-name");
+
+ port_set_opened(self, init->opened);
+}
+
+/* coroutine context */
+static void port_handle_event(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePortChannel *self = SPICE_PORT_CHANNEL(channel);
+ SpiceMsgPortEvent *event = spice_msg_in_parsed(in);
+
+ CHANNEL_DEBUG(channel, "port event: %d", event->event);
+ switch (event->event) {
+ case SPICE_PORT_EVENT_OPENED:
+ port_set_opened(self, true);
+ break;
+ case SPICE_PORT_EVENT_CLOSED:
+ port_set_opened(self, false);
+ break;
+ }
+
+ emit_main_context(channel, SPICE_PORT_EVENT, event->event);
+}
+
+/* coroutine context */
+static void port_handle_msg(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpicePortChannel *self = SPICE_PORT_CHANNEL(channel);
+ int size;
+ uint8_t *buf;
+
+ buf = spice_msg_in_raw(in, &size);
+ CHANNEL_DEBUG(channel, "port %p got %d %p", channel, size, buf);
+ port_set_opened(self, true);
+ emit_main_context(channel, SPICE_PORT_DATA, buf, size);
+}
+
+static void port_write_free_cb(uint8_t *data, void *user_data)
+{
+ GSimpleAsyncResult *result = user_data;
+
+ g_simple_async_result_complete(result);
+ g_object_unref(result);
+}
+
+/**
+ * spice_port_write_async:
+ * @port: A #SpicePortChannel
+ * @buffer: (array length=count) (element-type guint8): the buffer
+ * containing the data to write
+ * @count: the number of bytes to write
+ * @cancellable: (allow-none): optional GCancellable object, NULL to ignore
+ * @callback: (scope async): callback to call when the request is satisfied
+ * @user_data: (closure): the data to pass to callback function
+ *
+ * Request an asynchronous write of count bytes from @buffer into the
+ * @port. When the operation is finished @callback will be called. You
+ * can then call spice_port_write_finish() to get the result of
+ * the operation.
+ *
+ * Since: 0.15
+ **/
+void spice_port_write_async(SpicePortChannel *self,
+ const void *buffer, gsize count,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GSimpleAsyncResult *simple;
+ SpicePortChannelPrivate *c;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(SPICE_IS_PORT_CHANNEL(self));
+ g_return_if_fail(buffer != NULL);
+ c = self->priv;
+
+ if (!c->opened) {
+ g_simple_async_report_error_in_idle(G_OBJECT(self), callback, user_data,
+ SPICE_CLIENT_ERROR, SPICE_CLIENT_ERROR_FAILED,
+ "The port is not opened");
+ return;
+ }
+
+ simple = g_simple_async_result_new(G_OBJECT(self), callback, user_data,
+ spice_port_write_async);
+ g_simple_async_result_set_op_res_gssize(simple, count);
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(self), SPICE_MSGC_SPICEVMC_DATA);
+ spice_marshaller_add_ref_full(msg->marshaller, (uint8_t*)buffer, count,
+ port_write_free_cb, simple);
+ spice_msg_out_send(msg);
+}
+
+/**
+ * spice_port_write_finish:
+ * @port: a #SpicePortChannel
+ * @result: a #GAsyncResult
+ * @error: a #GError location to store the error occurring, or %NULL
+ * to ignore
+ *
+ * Finishes a port write operation.
+ *
+ * Returns: a #gssize containing the number of bytes written to the stream.
+ * Since: 0.15
+ **/
+gssize spice_port_write_finish(SpicePortChannel *self,
+ GAsyncResult *result, GError **error)
+{
+ GSimpleAsyncResult *simple;
+
+ g_return_val_if_fail(SPICE_IS_PORT_CHANNEL(self), -1);
+ g_return_val_if_fail(result != NULL, -1);
+
+ simple = (GSimpleAsyncResult *)result;
+
+ if (g_simple_async_result_propagate_error(simple, error))
+ return -1;
+
+ g_return_val_if_fail(g_simple_async_result_is_valid(result, G_OBJECT(self),
+ spice_port_write_async), -1);
+
+ return g_simple_async_result_get_op_res_gssize(simple);
+}
+
+/**
+ * spice_port_event:
+ * @port: a #SpicePortChannel
+ * @event: a SPICE_PORT_EVENT value
+ *
+ * Send an event to the port.
+ *
+ * Note: The values SPICE_PORT_EVENT_CLOSED and
+ * SPICE_PORT_EVENT_OPENED are managed by the channel connection
+ * state.
+ *
+ * Since: 0.15
+ **/
+void spice_port_event(SpicePortChannel *self, guint8 event)
+{
+ SpiceMsgcPortEvent e;
+ SpiceMsgOut *msg;
+
+ g_return_if_fail(SPICE_IS_PORT_CHANNEL(self));
+ g_return_if_fail(event > SPICE_PORT_EVENT_CLOSED);
+
+ msg = spice_msg_out_new(SPICE_CHANNEL(self), SPICE_MSGC_PORT_EVENT);
+ e.event = event;
+ msg->marshallers->msgc_port_event(msg->marshaller, &e);
+ spice_msg_out_send(msg);
+}
+
+static const spice_msg_handler port_handlers[] = {
+ [ SPICE_MSG_PORT_INIT ] = port_handle_init,
+ [ SPICE_MSG_PORT_EVENT ] = port_handle_event,
+ [ SPICE_MSG_SPICEVMC_DATA ] = port_handle_msg,
+};
+
+/* coroutine context */
+static void spice_port_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
+{
+ int type = spice_msg_in_type(msg);
+ SpiceChannelClass *parent_class;
+
+ g_return_if_fail(type < SPICE_N_ELEMENTS(port_handlers));
+
+ parent_class = SPICE_CHANNEL_CLASS(spice_port_channel_parent_class);
+
+ if (port_handlers[type] != NULL)
+ port_handlers[type](channel, msg);
+ else if (parent_class->handle_msg)
+ parent_class->handle_msg(channel, msg);
+ else
+ g_return_if_reached();
+}
diff --git a/gtk/channel-port.h b/gtk/channel-port.h
new file mode 100644
index 0000000..84d512d
--- /dev/null
+++ b/gtk/channel-port.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2012 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 __SPICE_CLIENT_PORT_CHANNEL_H__
+#define __SPICE_CLIENT_PORT_CHANNEL_H__
+
+#include <gio/gio.h>
+#include "spice-client.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_PORT_CHANNEL (spice_port_channel_get_type())
+#define SPICE_PORT_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_PORT_CHANNEL, SpicePortChannel))
+#define SPICE_PORT_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_PORT_CHANNEL, SpicePortChannelClass))
+#define SPICE_IS_PORT_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_PORT_CHANNEL))
+#define SPICE_IS_PORT_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_PORT_CHANNEL))
+#define SPICE_PORT_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_PORT_CHANNEL, SpicePortChannelClass))
+
+typedef struct _SpicePortChannel SpicePortChannel;
+typedef struct _SpicePortChannelClass SpicePortChannelClass;
+typedef struct _SpicePortChannelPrivate SpicePortChannelPrivate;
+
+/**
+ * SpicePortChannel:
+ *
+ * The #SpicePortChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpicePortChannel {
+ SpiceChannel parent;
+
+ /*< private >*/
+ SpicePortChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpicePortChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpicePortChannel.
+ */
+struct _SpicePortChannelClass {
+ SpiceChannelClass parent_class;
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_port_channel_get_type(void);
+
+void spice_port_write_async(SpicePortChannel *port,
+ const void *buffer, gsize count,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+gssize spice_port_write_finish(SpicePortChannel *port,
+ GAsyncResult *result, GError **error);
+void spice_port_event(SpicePortChannel *port, guint8 event);
+
+G_END_DECLS
+
+#endif /* __SPICE_CLIENT_PORT_CHANNEL_H__ */
diff --git a/gtk/map-file b/gtk/map-file
index 79ad9d2..516764c 100644
--- a/gtk/map-file
+++ b/gtk/map-file
@@ -69,6 +69,10 @@ spice_main_set_display;
spice_main_set_display_enabled;
spice_playback_channel_get_type;
spice_playback_channel_set_delay;
+spice_port_channel_get_type;
+spice_port_event;
+spice_port_write_async;
+spice_port_write_finish;
spice_record_channel_get_type;
spice_record_send_data;
spice_session_connect;
diff --git a/gtk/spice-channel.c b/gtk/spice-channel.c
index c9c4639..d374204 100644
--- a/gtk/spice-channel.c
+++ b/gtk/spice-channel.c
@@ -1872,6 +1872,7 @@ const gchar* spice_channel_type_to_string(gint type)
[ SPICE_CHANNEL_TUNNEL ] = "tunnel",
[ SPICE_CHANNEL_SMARTCARD ] = "smartcard",
[ SPICE_CHANNEL_USBREDIR ] = "usbredir",
+ [ SPICE_CHANNEL_PORT ] = "port",
};
const char *str = NULL;
@@ -1942,6 +1943,9 @@ SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id)
break;
}
#endif
+ case SPICE_CHANNEL_PORT:
+ gtype = SPICE_TYPE_PORT_CHANNEL;
+ break;
default:
g_debug("unsupported channel kind: %s: %d",
spice_channel_type_to_string(type), type);
diff --git a/gtk/spice-client.h b/gtk/spice-client.h
index 5c05ebb..730d11a 100644
--- a/gtk/spice-client.h
+++ b/gtk/spice-client.h
@@ -40,6 +40,7 @@
#include "channel-record.h"
#include "channel-smartcard.h"
#include "channel-usbredir.h"
+#include "channel-port.h"
#include "smartcard-manager.h"
#include "usb-device-manager.h"
--
1.7.11.7
More information about the Spice-devel
mailing list