[Spice-commits] 3 commits - .gitmodules NEWS autogen.sh configure.ac doc/reference gtk/Makefile.am gtk/channel-webdav.c gtk/channel-webdav.h gtk/map-file gtk/phodav gtk/spice-channel.c gtk/spice-client.h gtk/spice-glib-sym-file gtk/spice-option.c gtk/spice-session-priv.h gtk/spice-session.c po/POTFILES.skip spice-common
Marc-André Lureau
elmarco at kemper.freedesktop.org
Wed Mar 19 07:24:43 PDT 2014
.gitmodules | 3
NEWS | 15
autogen.sh | 2
configure.ac | 4
doc/reference/spice-gtk-docs.xml | 1
doc/reference/spice-gtk-sections.txt | 17
doc/reference/spice-gtk.types | 4
gtk/Makefile.am | 9
gtk/channel-webdav.c | 737 +++++++++++++++++++++++++++++++++++
gtk/channel-webdav.h | 68 +++
gtk/map-file | 1
gtk/phodav | 1
gtk/spice-channel.c | 6
gtk/spice-client.h | 1
gtk/spice-glib-sym-file | 1
gtk/spice-option.c | 5
gtk/spice-session-priv.h | 6
gtk/spice-session.c | 50 ++
po/POTFILES.skip | 1
spice-common | 2
20 files changed, 928 insertions(+), 6 deletions(-)
New commits:
commit a696dcca167286a44cebaf705e86808e820dbb64
Author: Marc-André Lureau <marcandre.lureau at redhat.com>
Date: Wed Mar 19 15:10:28 2014 +0100
Start NEWS file
diff --git a/NEWS b/NEWS
index cb01c1e..6931c14 100644
--- a/NEWS
+++ b/NEWS
@@ -1,3 +1,18 @@
+v0.24 (draft)
+=====
+
+- support folder sharing, via WebDAV channel
+- add HTTPS proxy support (requires glib 2.28), and Basic auth
+- add SPICE_GTK_CHECK_VERSION macro
+- fix crash when releasing primary surface
+- fix a few memory leaks with SASL
+- fix spice_display_get_pixbuf() with offset area
+- build-sys improvements
+
+- note: until now, providing an invalid plain-port didn't error, and
+ was falling back silently on tls-port. With this release, an error
+ will be reported if the port can't be opened.
+
v0.23
=====
commit eeeb6e27be376e74c1ad02c092d8e27011ee738e
Author: Marc-André Lureau <marcandre.lureau at redhat.com>
Date: Thu Feb 27 11:10:25 2014 +0100
session: add shared-dir property and option
Allow to specify the shared directory from the command line, or at
runtime via properties. (still default to xdg public share, if none
specified)
diff --git a/gtk/channel-webdav.c b/gtk/channel-webdav.c
index 28760f5..ffd617e 100644
--- a/gtk/channel-webdav.c
+++ b/gtk/channel-webdav.c
@@ -36,7 +36,11 @@ static PhodavServer* phodav_server_get(SpiceSession *session, gint *port);
*
* The "webdav" channel exports a directory to the guest for file
* manipulation (read/write/copy etc). The underlying protocol is
- * implemented using WebDAV (RFC 4918)
+ * implemented using WebDAV (RFC 4918).
+ *
+ * By default, the shared directory is the one associated with GLib
+ * %G_USER_DIRECTORY_PUBLIC_SHARE. You can specify a different
+ * directory with #SpiceSession #SpiceSession:shared-dir property.
*
* Since: 0.24
*/
@@ -697,7 +701,7 @@ static PhodavServer* webdav_server_new(SpiceSession *session)
g_warn_if_fail(!session->priv->webdav);
- dav = phodav_server_new(0, g_get_user_special_dir(G_USER_DIRECTORY_PUBLIC_SHARE));
+ dav = phodav_server_new(0, spice_session_get_shared_dir(session));
session->priv->webdav = dav;
for (i = 0; i < sizeof(session->priv->webdav_magic); i++)
session->priv->webdav_magic[i] = g_random_int_range(0, 255);
diff --git a/gtk/spice-option.c b/gtk/spice-option.c
index 5f7c803..1c861e2 100644
--- a/gtk/spice-option.c
+++ b/gtk/spice-option.c
@@ -41,6 +41,7 @@ static gboolean disable_usbredir = FALSE;
static gint cache_size = 0;
static gint glz_window_size = 0;
static gchar *secure_channels = NULL;
+static gchar *shared_dir = NULL;
G_GNUC_NORETURN
static void option_version(void)
@@ -192,6 +193,8 @@ GOptionGroup* spice_get_option_group(void)
N_("Image cache size"), N_("<bytes>") },
{ "spice-glz-window-size", '\0', 0, G_OPTION_ARG_INT, &glz_window_size,
N_("Glz compression history size"), N_("<bytes>") },
+ { "spice-shared-dir", '\0', 0, G_OPTION_ARG_FILENAME, &shared_dir,
+ N_("Shared directory"), N_("<dir>") },
{ "spice-debug", '\0', G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, option_debug,
N_("Enable Spice-GTK debugging"), NULL },
@@ -275,4 +278,6 @@ void spice_set_session_option(SpiceSession *session)
g_object_set(session, "cache-size", cache_size, NULL);
if (glz_window_size)
g_object_set(session, "glz-window-size", glz_window_size, NULL);
+ if (shared_dir)
+ g_object_set(session, "shared-dir", shared_dir, NULL);
}
diff --git a/gtk/spice-session-priv.h b/gtk/spice-session-priv.h
index cf9f9d1..94535a8 100644
--- a/gtk/spice-session-priv.h
+++ b/gtk/spice-session-priv.h
@@ -46,6 +46,7 @@ struct _SpiceSessionPrivate {
guint verify;
gboolean read_only;
SpiceURI *proxy;
+ gchar *shared_dir;
/* whether to enable audio */
gboolean audio;
@@ -160,6 +161,8 @@ void spice_session_set_name(SpiceSession *session, const gchar *name);
gboolean spice_session_is_playback_active(SpiceSession *session);
guint32 spice_session_get_playback_latency(SpiceSession *session);
void spice_session_sync_playback_latency(SpiceSession *session);
+const gchar* spice_session_get_shared_dir(SpiceSession *session);
+void spice_session_set_shared_dir(SpiceSession *session, const gchar *dir);
G_END_DECLS
diff --git a/gtk/spice-session.c b/gtk/spice-session.c
index 1c4c34e..c168bc3 100644
--- a/gtk/spice-session.c
+++ b/gtk/spice-session.c
@@ -110,7 +110,8 @@ enum {
PROP_NAME,
PROP_CA,
PROP_PROXY,
- PROP_SECURE_CHANNELS
+ PROP_SECURE_CHANNELS,
+ PROP_SHARED_DIR
};
/* signals */
@@ -239,6 +240,7 @@ spice_session_finalize(GObject *gobject)
g_free(s->smartcard_db);
g_strfreev(s->disable_effects);
g_strfreev(s->secure_channels);
+ g_free(s->shared_dir);
g_clear_pointer(&s->images, cache_unref);
glz_decoder_window_destroy(s->glz_window);
@@ -504,6 +506,9 @@ static void spice_session_get_property(GObject *gobject,
case PROP_PROXY:
g_value_take_string(value, spice_uri_to_string(s->proxy));
break;
+ case PROP_SHARED_DIR:
+ g_value_set_string(value, spice_session_get_shared_dir(session));
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
break;
@@ -626,6 +631,9 @@ static void spice_session_set_property(GObject *gobject,
case PROP_PROXY:
update_proxy(session, g_value_get_string(value));
break;
+ case PROP_SHARED_DIR:
+ spice_session_set_shared_dir(session, g_value_get_string(value));
+ break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID(gobject, prop_id, pspec);
break;
@@ -1173,6 +1181,23 @@ static void spice_session_class_init(SpiceSessionClass *klass)
G_PARAM_READWRITE |
G_PARAM_STATIC_STRINGS));
+ /**
+ * SpiceSession:shared-dir:
+ *
+ * Location of the shared directory
+ *
+ * Since: 0.24
+ **/
+ g_object_class_install_property
+ (gobject_class, PROP_SHARED_DIR,
+ g_param_spec_string("shared-dir",
+ "Shared directory",
+ "Shared directory",
+ g_get_user_special_dir(G_USER_DIRECTORY_PUBLIC_SHARE),
+ G_PARAM_READWRITE |
+ G_PARAM_CONSTRUCT |
+ G_PARAM_STATIC_STRINGS));
+
g_type_class_add_private(klass, sizeof(SpiceSessionPrivate));
}
@@ -2193,6 +2218,28 @@ guint32 spice_session_get_playback_latency(SpiceSession *session)
}
}
+G_GNUC_INTERNAL
+const gchar* spice_session_get_shared_dir(SpiceSession *session)
+{
+ SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
+
+ g_return_val_if_fail(s != NULL, NULL);
+
+ return s->shared_dir;
+}
+
+G_GNUC_INTERNAL
+void spice_session_set_shared_dir(SpiceSession *session, const gchar *dir)
+{
+ SpiceSessionPrivate *s = SPICE_SESSION_GET_PRIVATE(session);
+
+ g_return_if_fail(dir != NULL);
+ g_return_if_fail(s != NULL);
+
+ g_free(s->shared_dir);
+ s->shared_dir = g_strdup(dir);
+}
+
/**
* spice_session_get_proxy_uri:
* @session: a #SpiceSession
commit 382ecfa16f30c2db80a8996f93dacf5a62d7231c
Author: Marc-André Lureau <marcandre.lureau at redhat.com>
Date: Fri Nov 29 09:56:44 2013 +0100
Add webdav channel
See spice-common for protocol details. phodav, a webdav server library,
is imported thanks to a submodule, until this project has a stable API
and releases.
The webdav channel is reponsible for handling port events and
multiplexing the request streams. Extra care has been made to avoid
blocking and to enable some fairness between concurrent streams, however
this has been particularly tricky and is likely to have some issues
left.
The webdav server is run in a seperate thread, using libsoup. The client
communication is done via a local tcp socket, but protected to only
accept local connection and with a pretty strong password.
The home directory is exported for the remote to browse, which seems to
be a sensible default atm.
diff --git a/.gitmodules b/.gitmodules
index 0c618ee..cfce54a 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -1,3 +1,6 @@
[submodule "spice-common"]
path = spice-common
url = ../spice-common
+[submodule "gtk/phodav"]
+ path = gtk/phodav
+ url = git://git.gnome.org/phodav
diff --git a/autogen.sh b/autogen.sh
index 0c18272..d71be70 100755
--- a/autogen.sh
+++ b/autogen.sh
@@ -6,6 +6,7 @@ srcdir=`dirname $0`
test -z "$srcdir" && srcdir=.
git submodule update --init --recursive
+(cd "$srcdir/gtk/phodav/" && intltoolize -f)
gtkdocize
autoreconf -v --force --install
@@ -15,4 +16,3 @@ if [ -z "$NOCONFIGURE" ]; then
echo "Running configure with --enable-maintainer-mode --enable-gtk-doc --with-gtk=3.0 --enable-vala ${1+"$@"}"
"$srcdir"/configure --enable-maintainer-mode --enable-gtk-doc --with-gtk=3.0 --enable-vala ${1+"$@"}
fi
-
diff --git a/configure.ac b/configure.ac
index afdbdbd..1fcf149 100644
--- a/configure.ac
+++ b/configure.ac
@@ -75,6 +75,8 @@ AC_CONFIG_SUBDIRS([spice-common])
COMMON_CFLAGS='-I ${top_srcdir}/spice-common/ -I ${top_srcdir}/spice-common/spice-protocol/'
AC_SUBST(COMMON_CFLAGS)
+AC_CONFIG_SUBDIRS([gtk/phodav])
+
SPICE_GTK_MAJOR_VERSION=`echo $PACKAGE_VERSION | cut -d. -f1`
SPICE_GTK_MINOR_VERSION=`echo $PACKAGE_VERSION | cut -d. -f2`
SPICE_GTK_MICRO_VERSION=`echo $PACKAGE_VERSION | cut -d. -f3 | cut -d- -f1`
@@ -267,6 +269,8 @@ PKG_CHECK_MODULES(GTHREAD, gthread-2.0 > 2.0.0)
AC_SUBST(GTHREAD_CFLAGS)
AC_SUBST(GTHREAD_LIBS)
+PKG_CHECK_MODULES(SOUP, libsoup-2.4)
+
AC_ARG_WITH([audio],
AS_HELP_STRING([--with-audio=@<:@gstreamer/pulse/auto/no@:>@], [Select audio backend @<:@default=auto@:>@]),
[],
diff --git a/doc/reference/spice-gtk-docs.xml b/doc/reference/spice-gtk-docs.xml
index d2c1a2b..5faea74 100644
--- a/doc/reference/spice-gtk-docs.xml
+++ b/doc/reference/spice-gtk-docs.xml
@@ -37,6 +37,7 @@
<xi:include href="xml/channel-smartcard.xml"/>
<xi:include href="xml/channel-usbredir.xml"/>
<xi:include href="xml/channel-port.xml"/>
+ <xi:include href="xml/channel-webdav.xml"/>
</chapter>
<chapter>
diff --git a/doc/reference/spice-gtk-sections.txt b/doc/reference/spice-gtk-sections.txt
index 9232a23..caaa92c 100644
--- a/doc/reference/spice-gtk-sections.txt
+++ b/doc/reference/spice-gtk-sections.txt
@@ -461,3 +461,20 @@ spice_uri_get_type
<SUBSECTION Private>
SpiceURIPrivate
</SECTION>
+
+<SECTION>
+<FILE>channel-webdav</FILE>
+<TITLE>SpiceWebdavChannel</TITLE>
+SpiceWebdavChannel
+SpiceWebdavChannelClass
+<SUBSECTION Standard>
+SPICE_IS_WEBDAV_CHANNEL
+SPICE_IS_WEBDAV_CHANNEL_CLASS
+SPICE_TYPE_WEBDAV_CHANNEL
+SPICE_WEBDAV_CHANNEL
+SPICE_WEBDAV_CHANNEL_CLASS
+SPICE_WEBDAV_CHANNEL_GET_CLASS
+spice_webdav_channel_get_type
+<SUBSECTION Private>
+SpiceWebdavChannelPrivate
+</SECTION>
diff --git a/doc/reference/spice-gtk.types b/doc/reference/spice-gtk.types
index 2f52845..db0374a 100644
--- a/doc/reference/spice-gtk.types
+++ b/doc/reference/spice-gtk.types
@@ -13,6 +13,7 @@
#include "channel-record.h"
#include "channel-smartcard.h"
#include "channel-usbredir.h"
+#include "channel-webdav.h"
#include "spice-gtk-session.h"
#include "spice-widget.h"
#include "spice-grabsequence.h"
@@ -42,4 +43,5 @@ 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
+spice_port_channel_get_type
+spice_webdav_channel_get_type
\ No newline at end of file
diff --git a/gtk/Makefile.am b/gtk/Makefile.am
index 75962cf..2e38cce 100644
--- a/gtk/Makefile.am
+++ b/gtk/Makefile.am
@@ -1,6 +1,6 @@
NULL =
-SUBDIRS =
+SUBDIRS = phodav
if WITH_CONTROLLER
SUBDIRS += controller
@@ -96,6 +96,7 @@ SPICE_COMMON_CPPFLAGS = \
$(SMARTCARD_CFLAGS) \
$(USBREDIR_CFLAGS) \
$(GUDEV_CFLAGS) \
+ $(SOUP_CFLAGS) \
$(NULL)
AM_CPPFLAGS = \
@@ -185,8 +186,9 @@ libspice_client_glib_2_0_la_LDFLAGS = \
libspice_client_glib_2_0_la_LIBADD = \
$(top_builddir)/spice-common/common/libspice-common.la \
$(top_builddir)/spice-common/common/libspice-common-client.la \
+ phodav/libphodav.la \
$(GLIB2_LIBS) \
- $(GIO_LIBS) \
+ $(SOUP_LIBS) \
$(GOBJECT2_LIBS) \
$(CELT051_LIBS) \
$(OPUS_LIBS) \
@@ -236,6 +238,7 @@ libspice_client_glib_2_0_la_SOURCES = \
gio-coroutine.h \
\
channel-base.c \
+ channel-webdav.c \
channel-cursor.c \
channel-display.c \
channel-display-priv.h \
@@ -303,6 +306,7 @@ libspice_client_glibinclude_HEADERS = \
channel-record.h \
channel-smartcard.h \
channel-usbredir.h \
+ channel-webdav.h \
usb-device-manager.h \
smartcard-manager.h \
$(NULL)
@@ -601,6 +605,7 @@ glib_introspection_files = \
spice-glib-enums.c \
spice-option.c \
spice-util.c \
+ channel-webdav.c \
channel-cursor.c \
channel-display.c \
channel-inputs.c \
diff --git a/gtk/channel-webdav.c b/gtk/channel-webdav.c
new file mode 100644
index 0000000..28760f5
--- /dev/null
+++ b/gtk/channel-webdav.c
@@ -0,0 +1,733 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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-session-priv.h"
+#include "spice-marshal.h"
+#include "glib-compat.h"
+#include "vmcstream.h"
+
+static PhodavServer* phodav_server_get(SpiceSession *session, gint *port);
+
+/**
+ * SECTION:channel-webdav
+ * @short_description: exports a directory
+ * @title: WebDAV Channel
+ * @section_id:
+ * @see_also: #SpiceChannel
+ * @stability: Stable
+ * @include: channel-webdav.h
+ *
+ * The "webdav" channel exports a directory to the guest for file
+ * manipulation (read/write/copy etc). The underlying protocol is
+ * implemented using WebDAV (RFC 4918)
+ *
+ * Since: 0.24
+ */
+
+#define SPICE_WEBDAV_CHANNEL_GET_PRIVATE(obj) \
+ (G_TYPE_INSTANCE_GET_PRIVATE((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelPrivate))
+
+typedef struct _OutputQueue OutputQueue;
+
+struct _SpiceWebdavChannelPrivate {
+ SpiceVmcStream *stream;
+ GCancellable *cancellable;
+ GHashTable *clients;
+ OutputQueue *queue;
+
+ gboolean demuxing;
+ struct _demux {
+ gint64 client;
+ guint16 size;
+ guint8 *buf;
+ } demux;
+};
+
+G_DEFINE_TYPE(SpiceWebdavChannel, spice_webdav_channel, SPICE_TYPE_PORT_CHANNEL)
+
+static void spice_webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg);
+
+struct _OutputQueue {
+ GOutputStream *output;
+ gboolean flushing;
+ guint idle_id;
+ GQueue *queue;
+};
+
+typedef struct _OutputQueueElem {
+ OutputQueue *queue;
+ const guint8 *buf;
+ gsize size;
+ GFunc cb;
+ gpointer user_data;
+} OutputQueueElem;
+
+static OutputQueue* output_queue_new(GOutputStream *output)
+{
+ OutputQueue *queue = g_new0(OutputQueue, 1);
+
+ queue->output = g_object_ref(output);
+ queue->queue = g_queue_new();
+
+ return queue;
+}
+
+static void output_queue_free(OutputQueue *queue)
+{
+ g_warn_if_fail(g_queue_get_length(queue->queue) == 0);
+ g_warn_if_fail(!queue->flushing);
+ g_warn_if_fail(!queue->idle_id);
+
+ g_queue_free_full(queue->queue, g_free);
+ g_clear_object(&queue->output);
+ if (queue->idle_id)
+ g_source_remove(queue->idle_id);
+ g_free(queue);
+}
+
+static gboolean output_queue_idle(gpointer user_data);
+
+static void output_queue_flush_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GError *error = NULL;
+ OutputQueueElem *e = user_data;
+ OutputQueue *q = e->queue;
+
+ q->flushing = FALSE;
+ g_output_stream_flush_finish(G_OUTPUT_STREAM(source_object),
+ res, &error);
+ if (error)
+ g_warning("error: %s", error->message);
+
+ g_clear_error(&error);
+
+ if (!q->idle_id)
+ q->idle_id = g_idle_add(output_queue_idle, q);
+
+ g_free(e);
+}
+
+static gboolean output_queue_idle(gpointer user_data)
+{
+ OutputQueue *q = user_data;
+ OutputQueueElem *e;
+ GError *error = NULL;
+
+ if (q->flushing) {
+ q->idle_id = 0;
+ return FALSE;
+ }
+
+ e = g_queue_pop_head(q->queue);
+ if (!e) {
+ q->idle_id = 0;
+ return FALSE;
+ }
+
+ g_output_stream_write_all(q->output, e->buf, e->size, NULL, NULL, &error);
+ if (error)
+ goto err;
+ else if (e->cb)
+ e->cb(q, e->user_data);
+
+ q->flushing = TRUE;
+ g_output_stream_flush_async(q->output, G_PRIORITY_DEFAULT, NULL, output_queue_flush_cb, e);
+
+ return TRUE;
+
+err:
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+
+ q->idle_id = 0;
+ return FALSE;
+}
+
+static void output_queue_push(OutputQueue *q, const guint8 *buf, gsize size,
+ GFunc pushed_cb, gpointer user_data)
+{
+ OutputQueueElem *e = g_new(OutputQueueElem, 1);
+
+ e->buf = buf;
+ e->size = size;
+ e->cb = pushed_cb;
+ e->user_data = user_data;
+ e->queue = q;
+ g_queue_push_tail(q->queue, e);
+
+ if (!q->idle_id && !q->flushing)
+ q->idle_id = g_idle_add(output_queue_idle, q);
+}
+
+typedef struct Client
+{
+ guint refs;
+ SpiceWebdavChannel *self;
+ GSocketConnection *conn;
+ OutputQueue *output;
+ gint64 id;
+ GCancellable *cancellable;
+
+ struct _mux {
+ gint64 id;
+ guint16 size;
+ guint8 *buf;
+ } mux;
+} Client;
+
+static void
+client_unref(Client *client)
+{
+ if (--client->refs > 0)
+ return;
+
+ g_free(client->mux.buf);
+ output_queue_free(client->output);
+
+ g_object_unref(client->conn);
+ g_object_unref(client->cancellable);
+
+ g_free(client);
+}
+
+static Client *
+client_ref(Client *client)
+{
+ client->refs++;
+ return client;
+}
+
+static void client_start_read(SpiceWebdavChannel *self, Client *client);
+
+static void remove_client(SpiceWebdavChannel *self, Client *client)
+{
+ SpiceWebdavChannelPrivate *c;
+
+ if (g_cancellable_is_cancelled(client->cancellable))
+ return;
+
+ g_cancellable_cancel(client->cancellable);
+
+ c = self->priv;
+ g_hash_table_remove(c->clients, &client->id);
+}
+
+static void mux_pushed_cb(OutputQueue *q, gpointer user_data)
+{
+ Client *client = user_data;
+
+ if (client->mux.size == 0) {
+ remove_client(client->self, client);
+ } else {
+ client_start_read(client->self, client);
+ }
+
+ client_unref(client);
+}
+
+static void server_reply_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ Client *client = user_data;
+ SpiceWebdavChannel *self = client->self;
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GError *err = NULL;
+ gssize size;
+
+ size = g_input_stream_read_finish(G_INPUT_STREAM(source_object), res, &err);
+ if (err || g_cancellable_is_cancelled(client->cancellable))
+ goto end;
+
+ g_return_if_fail(size <= G_MAXUINT16);
+ g_return_if_fail(size >= 0);
+ client->mux.size = size;
+
+ output_queue_push(c->queue, (guint8 *)&client->mux.id, sizeof(gint64), NULL, NULL);
+ client->mux.size = GUINT16_TO_LE(client->mux.size);
+ output_queue_push(c->queue, (guint8 *)&client->mux.size, sizeof(guint16), NULL, NULL);
+ output_queue_push(c->queue, (guint8 *)client->mux.buf, size, (GFunc)mux_pushed_cb, client);
+
+ return;
+
+end:
+ if (err) {
+ if (!g_cancellable_is_cancelled(client->cancellable))
+ g_warning("read error: %s", err->message);
+ remove_client(self, client);
+ g_clear_error(&err);
+ }
+
+ client_unref(client);
+}
+
+static void client_start_read(SpiceWebdavChannel *self, Client *client)
+{
+ GInputStream *input;
+
+ input = g_io_stream_get_input_stream(G_IO_STREAM(client->conn));
+ g_input_stream_read_async(input, client->mux.buf, G_MAXUINT16,
+ G_PRIORITY_DEFAULT, client->cancellable, server_reply_cb,
+ client_ref(client));
+}
+
+static void start_demux(SpiceWebdavChannel *self);
+
+static void pushed_client_cb(OutputQueue *q, gpointer user_data)
+{
+ Client *client = user_data;
+ SpiceWebdavChannel *self = client->self;
+ SpiceWebdavChannelPrivate *c = self->priv;
+
+ c->demuxing = FALSE;
+ start_demux(self);
+}
+
+static void demux_to_client(SpiceWebdavChannel *self,
+ Client *client)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+ gssize size = c->demux.size;
+
+ CHANNEL_DEBUG(self, "pushing %ld to client %p", size, client);
+
+ if (size != 0) {
+ output_queue_push(client->output, (guint8 *)c->demux.buf, size,
+ (GFunc)pushed_client_cb, client);
+ } else {
+ remove_client(self, client);
+ c->demuxing = FALSE;
+ start_demux(self);
+ }
+}
+
+static void magic_written(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ Client *client = user_data;
+ SpiceWebdavChannel *self = client->self;
+ SpiceWebdavChannelPrivate *c = self->priv;
+ gssize bytes_written;
+ GError *err = NULL;
+ SpiceSession *session;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(self));
+ bytes_written = g_output_stream_write_finish(G_OUTPUT_STREAM(source_object),
+ res, &err);
+
+ if (err || bytes_written != sizeof(session->priv->webdav_magic))
+ goto error;
+
+ client_start_read(self, client);
+ g_hash_table_insert(c->clients, &client->id, client);
+
+ demux_to_client(self, client);
+
+ return;
+
+error:
+ if (err) {
+ g_critical("socket creation failed %s", err->message);
+ g_clear_error(&err);
+ }
+ if (client) {
+ client_unref(client);
+ }
+}
+
+static void client_connected(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GSocketClient *sclient = G_SOCKET_CLIENT(source_object);
+ GError *err = NULL;
+ GSocketConnection *conn;
+ SpiceSession *session;
+ Client *client = NULL;
+ GOutputStream *output;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(self));
+
+ conn = g_socket_client_connect_to_host_finish(sclient, res, &err);
+ g_object_unref(sclient);
+ if (err)
+ goto error;
+
+ client = g_new0(Client, 1);
+ client->refs = 1;
+ client->id = c->demux.client;
+ client->self = self;
+ client->conn = conn;
+ client->mux.id = GINT64_TO_LE(client->id);
+ client->mux.buf = g_malloc(G_MAXUINT16);
+ client->cancellable = g_cancellable_new();
+
+ output = g_buffered_output_stream_new(g_io_stream_get_output_stream(G_IO_STREAM(conn)));
+ client->output = output_queue_new(output);
+ g_object_unref(output);
+
+ g_output_stream_write_async(g_io_stream_get_output_stream(G_IO_STREAM(conn)),
+ session->priv->webdav_magic, sizeof(session->priv->webdav_magic),
+ G_PRIORITY_DEFAULT, c->cancellable,
+ magic_written, client);
+ return;
+
+error:
+ if (err) {
+ g_critical("socket creation failed %s", err->message);
+ g_clear_error(&err);
+ }
+ if (client) {
+ client_unref(client);
+ }
+}
+
+static void start_client(SpiceWebdavChannel *self)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GSocketClient *sclient;
+ gint davport = -1;
+ SpiceSession *session;
+
+ session = spice_channel_get_session(SPICE_CHANNEL(self));
+ phodav_server_get(session, &davport);
+ CHANNEL_DEBUG(self, "starting client %" G_GINT64_FORMAT, c->demux.client);
+
+ sclient = g_socket_client_new();
+ g_socket_client_connect_to_host_async(sclient, "localhost", davport,
+ c->cancellable, client_connected, self);
+}
+
+static void data_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c;
+ Client *client;
+ GError *error = NULL;
+ gssize size;
+
+ size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+ if (error) {
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+ return;
+ }
+
+ c = self->priv;
+ g_return_if_fail(size == c->demux.size);
+
+ client = g_hash_table_lookup(c->clients, &c->demux.client);
+
+ if (client)
+ demux_to_client(self, client);
+ else
+ start_client(self);
+}
+
+
+static void size_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c;
+ GInputStream *istream = G_INPUT_STREAM(source_object);
+ GError *error = NULL;
+ gssize size;
+
+ size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+ if (error || size != sizeof(guint16))
+ goto end;
+
+ c = self->priv;
+ c->demux.size = GUINT16_FROM_LE(c->demux.size);
+ spice_vmc_input_stream_read_all_async(istream,
+ c->demux.buf, c->demux.size,
+ G_PRIORITY_DEFAULT, c->cancellable, data_read_cb, self);
+ return;
+
+end:
+ if (error) {
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+ }
+}
+
+static void client_read_cb(GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ SpiceWebdavChannel *self = user_data;
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GInputStream *istream = G_INPUT_STREAM(source_object);
+ GError *error = NULL;
+ gssize size;
+
+ size = spice_vmc_input_stream_read_all_finish(G_INPUT_STREAM(source_object), res, &error);
+ if (error || size != sizeof(gint64))
+ goto end;
+
+ c->demux.client = GINT64_FROM_LE(c->demux.client);
+ spice_vmc_input_stream_read_all_async(istream,
+ &c->demux.size, sizeof(guint16),
+ G_PRIORITY_DEFAULT, c->cancellable, size_read_cb, self);
+ return;
+
+end:
+ if (error) {
+ g_warning("error: %s", error->message);
+ g_clear_error(&error);
+ }
+}
+
+static void start_demux(SpiceWebdavChannel *self)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+ GInputStream *istream = g_io_stream_get_input_stream(G_IO_STREAM(c->stream));
+
+ if (c->demuxing)
+ return;
+
+ c->demuxing = TRUE;
+
+ CHANNEL_DEBUG(self, "start demux");
+ spice_vmc_input_stream_read_all_async(istream, &c->demux.client, sizeof(gint64),
+ G_PRIORITY_DEFAULT, c->cancellable, client_read_cb, self);
+
+}
+
+static void port_event(SpiceWebdavChannel *self, gint event)
+{
+ SpiceWebdavChannelPrivate *c = self->priv;
+
+ CHANNEL_DEBUG(self, "port event:%d", event);
+ if (event == SPICE_PORT_EVENT_OPENED) {
+ g_cancellable_reset(c->cancellable);
+ start_demux(self);
+ } else {
+ g_cancellable_cancel(c->cancellable);
+ c->demuxing = FALSE;
+ g_hash_table_remove_all(c->clients);
+ }
+}
+
+static void client_remove_unref(gpointer data)
+{
+ Client *client = data;
+
+ g_cancellable_cancel(client->cancellable);
+ client_unref(client);
+}
+
+static void spice_webdav_channel_init(SpiceWebdavChannel *channel)
+{
+ SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL_GET_PRIVATE(channel);
+
+ channel->priv = c;
+ c->stream = spice_vmc_stream_new(SPICE_CHANNEL(channel));
+ c->cancellable = g_cancellable_new();
+ c->clients = g_hash_table_new_full(g_int64_hash, g_int64_equal,
+ NULL, client_remove_unref);
+ c->demux.buf = g_malloc(G_MAXUINT16);
+
+ GOutputStream *ostream = g_io_stream_get_output_stream(G_IO_STREAM(c->stream));
+ c->queue = output_queue_new(ostream);
+}
+
+static void spice_webdav_channel_finalize(GObject *object)
+{
+ SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL(object)->priv;
+
+ g_free(c->demux.buf);
+
+ G_OBJECT_CLASS(spice_webdav_channel_parent_class)->finalize(object);
+}
+
+static void spice_webdav_channel_dispose(GObject *object)
+{
+ SpiceWebdavChannelPrivate *c = SPICE_WEBDAV_CHANNEL(object)->priv;
+
+ g_cancellable_cancel(c->cancellable);
+ g_clear_object(&c->cancellable);
+ g_clear_pointer(&c->queue, output_queue_free);
+ g_clear_object(&c->stream);
+ g_hash_table_unref(c->clients);
+
+ G_OBJECT_CLASS(spice_webdav_channel_parent_class)->dispose(object);
+}
+
+static void spice_webdav_channel_up(SpiceChannel *channel)
+{
+ CHANNEL_DEBUG(channel, "up");
+}
+
+static void spice_webdav_channel_class_init(SpiceWebdavChannelClass *klass)
+{
+ GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+ SpiceChannelClass *channel_class = SPICE_CHANNEL_CLASS(klass);
+
+ gobject_class->dispose = spice_webdav_channel_dispose;
+ gobject_class->finalize = spice_webdav_channel_finalize;
+ channel_class->handle_msg = spice_webdav_handle_msg;
+ channel_class->channel_up = spice_webdav_channel_up;
+
+ g_signal_override_class_handler("port-event",
+ SPICE_TYPE_WEBDAV_CHANNEL,
+ G_CALLBACK(port_event));
+
+ g_type_class_add_private(klass, sizeof(SpiceWebdavChannelPrivate));
+}
+
+/* coroutine context */
+static void webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *in)
+{
+ SpiceWebdavChannel *self = SPICE_WEBDAV_CHANNEL(channel);
+ SpiceWebdavChannelPrivate *c = self->priv;
+ int size;
+ uint8_t *buf;
+
+ buf = spice_msg_in_raw(in, &size);
+ CHANNEL_DEBUG(channel, "len:%d buf:%p", size, buf);
+
+ spice_vmc_input_stream_co_data(
+ SPICE_VMC_INPUT_STREAM(g_io_stream_get_input_stream(G_IO_STREAM(c->stream))),
+ buf, size);
+}
+
+
+/* coroutine context */
+static void spice_webdav_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
+{
+ int type = spice_msg_in_type(msg);
+ SpiceChannelClass *parent_class;
+
+ parent_class = SPICE_CHANNEL_CLASS(spice_webdav_channel_parent_class);
+
+ if (type == SPICE_MSG_SPICEVMC_DATA)
+ webdav_handle_msg(channel, msg);
+ else if (parent_class->handle_msg)
+ parent_class->handle_msg(channel, msg);
+ else
+ g_return_if_reached();
+}
+
+
+
+static void new_connection(SoupSocket *sock,
+ SoupSocket *new,
+ gpointer user_data)
+{
+ SpiceSession *session = user_data;
+ SoupAddress *addr;
+ GSocketAddress *gaddr;
+ GInetAddress *iaddr;
+ guint port;
+ guint8 magic[16];
+ gsize nread;
+ gboolean success = FALSE;
+ SoupSocketIOStatus status;
+
+ /* note: this is sync calls, since webdav server is in a seperate thread */
+ addr = soup_socket_get_remote_address(new);
+ gaddr = soup_address_get_gsockaddr(addr);
+ iaddr = g_inet_socket_address_get_address(G_INET_SOCKET_ADDRESS(gaddr));
+ port = g_inet_socket_address_get_port(G_INET_SOCKET_ADDRESS(gaddr));
+
+ SPICE_DEBUG("port %d %p", port, iaddr);
+ if (!g_inet_address_get_is_loopback(iaddr)) {
+ g_warn_if_reached();
+ goto end;
+ }
+
+ g_object_set(new, "non-blocking", FALSE, NULL);
+ status = soup_socket_read(new, magic, sizeof(magic), &nread, NULL, NULL);
+ if (status != SOUP_SOCKET_OK) {
+ g_warning("bad initial socket read: %d", status);
+ goto end;
+ }
+ g_object_set(new, "non-blocking", TRUE, NULL);
+
+ /* check we got the right magic */
+ if (memcmp(session->priv->webdav_magic, magic, sizeof(magic))) {
+ g_warn_if_reached();
+ goto end;
+ }
+
+ success = TRUE;
+
+end:
+ if (!success) {
+ g_warn_if_reached();
+ soup_socket_disconnect(new);
+ g_signal_stop_emission_by_name(sock, "new_connection");
+ }
+ g_object_unref(gaddr);
+}
+
+static PhodavServer* webdav_server_new(SpiceSession *session)
+{
+ PhodavServer *dav;
+ SoupServer *server;
+ SoupSocket *listener;
+ int i;
+
+ g_warn_if_fail(!session->priv->webdav);
+
+ dav = phodav_server_new(0, g_get_user_special_dir(G_USER_DIRECTORY_PUBLIC_SHARE));
+ session->priv->webdav = dav;
+ for (i = 0; i < sizeof(session->priv->webdav_magic); i++)
+ session->priv->webdav_magic[i] = g_random_int_range(0, 255);
+
+ server = phodav_server_get_soup_server(dav);
+ listener = soup_server_get_listener(server);
+ spice_g_signal_connect_object(listener, "new_connection",
+ G_CALLBACK(new_connection), session,
+ 0);
+
+ return dav;
+}
+
+static PhodavServer* phodav_server_get(SpiceSession *session, gint *port)
+{
+ g_return_val_if_fail(SPICE_IS_SESSION(session), NULL);
+
+ PhodavServer *self;
+ static GStaticMutex mutex = G_STATIC_MUTEX_INIT;
+
+ g_static_mutex_lock(&mutex);
+ self = session->priv->webdav;
+ if (self == NULL) {
+ self = webdav_server_new(session);
+ phodav_server_run(self);
+ }
+ g_static_mutex_unlock(&mutex);
+
+ if (port)
+ *port = phodav_server_get_port(self);
+
+ return self;
+}
diff --git a/gtk/channel-webdav.h b/gtk/channel-webdav.h
new file mode 100644
index 0000000..7940706
--- /dev/null
+++ b/gtk/channel-webdav.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2013 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_WEBDAV_CHANNEL_H__
+#define __SPICE_WEBDAV_CHANNEL_H__
+
+#include <gio/gio.h>
+#include "spice-client.h"
+#include "channel-port.h"
+
+G_BEGIN_DECLS
+
+#define SPICE_TYPE_WEBDAV_CHANNEL (spice_webdav_channel_get_type())
+#define SPICE_WEBDAV_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannel))
+#define SPICE_WEBDAV_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelClass))
+#define SPICE_IS_WEBDAV_CHANNEL(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), SPICE_TYPE_WEBDAV_CHANNEL))
+#define SPICE_IS_WEBDAV_CHANNEL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), SPICE_TYPE_WEBDAV_CHANNEL))
+#define SPICE_WEBDAV_CHANNEL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), SPICE_TYPE_WEBDAV_CHANNEL, SpiceWebdavChannelClass))
+
+typedef struct _SpiceWebdavChannel SpiceWebdavChannel;
+typedef struct _SpiceWebdavChannelClass SpiceWebdavChannelClass;
+typedef struct _SpiceWebdavChannelPrivate SpiceWebdavChannelPrivate;
+
+/**
+ * SpiceWebdavChannel:
+ *
+ * The #SpiceWebdavChannel struct is opaque and should not be accessed directly.
+ */
+struct _SpiceWebdavChannel {
+ SpicePortChannel parent;
+
+ /*< private >*/
+ SpiceWebdavChannelPrivate *priv;
+ /* Do not add fields to this struct */
+};
+
+/**
+ * SpiceWebdavChannelClass:
+ * @parent_class: Parent class.
+ *
+ * Class structure for #SpiceWebdavChannel.
+ */
+struct _SpiceWebdavChannelClass {
+ SpicePortChannelClass parent_class;
+
+ /*< private >*/
+ /* Do not add fields to this struct */
+};
+
+GType spice_webdav_channel_get_type(void);
+
+G_END_DECLS
+
+#endif /* __SPICE_WEBDAV_CHANNEL_H__ */
diff --git a/gtk/map-file b/gtk/map-file
index f98680c..90f14f1 100644
--- a/gtk/map-file
+++ b/gtk/map-file
@@ -132,6 +132,7 @@ spice_uri_set_port;
spice_uri_set_scheme;
spice_uri_set_user;
spice_uri_to_string;
+spice_webdav_channel_get_type;
local:
*;
};
diff --git a/gtk/phodav b/gtk/phodav
new file mode 160000
index 0000000..5d857bb
--- /dev/null
+++ b/gtk/phodav
@@ -0,0 +1 @@
+Subproject commit 5d857bbddeb5ddeef216cfebbd38b55771275fd3
diff --git a/gtk/spice-channel.c b/gtk/spice-channel.c
index 83c7006..46c51b0 100644
--- a/gtk/spice-channel.c
+++ b/gtk/spice-channel.c
@@ -1881,6 +1881,7 @@ static const char *to_string[] = {
[ SPICE_CHANNEL_SMARTCARD ] = "smartcard",
[ SPICE_CHANNEL_USBREDIR ] = "usbredir",
[ SPICE_CHANNEL_PORT ] = "port",
+ [ SPICE_CHANNEL_WEBDAV ] = "webdav",
};
/**
@@ -1941,6 +1942,7 @@ gchar *spice_channel_supported_string(void)
#ifdef USE_USBREDIR
spice_channel_type_to_string(SPICE_CHANNEL_USBREDIR),
#endif
+ spice_channel_type_to_string(SPICE_CHANNEL_WEBDAV),
NULL);
}
@@ -2005,6 +2007,10 @@ SpiceChannel *spice_channel_new(SpiceSession *s, int type, int id)
break;
}
#endif
+ case SPICE_CHANNEL_WEBDAV: {
+ gtype = SPICE_TYPE_WEBDAV_CHANNEL;
+ break;
+ }
case SPICE_CHANNEL_PORT:
gtype = SPICE_TYPE_PORT_CHANNEL;
break;
diff --git a/gtk/spice-client.h b/gtk/spice-client.h
index 98aaffe..0e1e49d 100644
--- a/gtk/spice-client.h
+++ b/gtk/spice-client.h
@@ -43,6 +43,7 @@
#include "channel-smartcard.h"
#include "channel-usbredir.h"
#include "channel-port.h"
+#include "channel-webdav.h"
#include "smartcard-manager.h"
#include "usb-device-manager.h"
diff --git a/gtk/spice-glib-sym-file b/gtk/spice-glib-sym-file
index 2aa17cb..878dd12 100644
--- a/gtk/spice-glib-sym-file
+++ b/gtk/spice-glib-sym-file
@@ -106,3 +106,4 @@ spice_uri_set_scheme
spice_uri_set_user
spice_uri_to_string
spice_session_get_proxy_uri
+spice_webdav_channel_get_type
diff --git a/gtk/spice-session-priv.h b/gtk/spice-session-priv.h
index 1aae342..cf9f9d1 100644
--- a/gtk/spice-session-priv.h
+++ b/gtk/spice-session-priv.h
@@ -23,6 +23,7 @@
#include "desktop-integration.h"
#include "spice-session.h"
#include "spice-gtk-session.h"
+#include "phodav/libphodav/phodav.h"
#include "spice-channel-cache.h"
#include "decode.h"
@@ -107,6 +108,8 @@ struct _SpiceSessionPrivate {
SpiceGtkSession *gtk_session;
SpiceUsbDeviceManager *usb_manager;
SpicePlaybackChannel *playback_channel;
+ PhodavServer *webdav;
+ guint8 webdav_magic[16];
};
SpiceSession *spice_session_new_from_session(SpiceSession *session);
diff --git a/gtk/spice-session.c b/gtk/spice-session.c
index 7dea542..1c4c34e 100644
--- a/gtk/spice-session.c
+++ b/gtk/spice-session.c
@@ -214,6 +214,7 @@ spice_session_dispose(GObject *gobject)
g_clear_object(&s->gtk_session);
g_clear_object(&s->usb_manager);
g_clear_object(&s->proxy);
+ g_clear_object(&s->webdav);
/* Chain up to the parent class */
if (G_OBJECT_CLASS(spice_session_parent_class)->dispose)
diff --git a/po/POTFILES.skip b/po/POTFILES.skip
index 5c78729..971470e 100644
--- a/po/POTFILES.skip
+++ b/po/POTFILES.skip
@@ -1 +1,2 @@
data/spicy.desktop.in
+gtk/phodav/libphodav/chezdav.c
diff --git a/spice-common b/spice-common
index 96ca358..01955e7 160000
--- a/spice-common
+++ b/spice-common
@@ -1 +1 @@
-Subproject commit 96ca358669cd32d17ce51f30de3cdbf0a1c0518c
+Subproject commit 01955e70079876de62bb8c86ee6793c1405fb47d
More information about the Spice-commits
mailing list