[Spice-devel] [PATCH spice-gtk 1/2] Add controller foreign menu support
Christophe Fergeau
cfergeau at redhat.com
Wed Feb 29 08:43:59 PST 2012
I'm not fluent in vala, but this looks sane after a quick glance.
Christophe
On Wed, Feb 29, 2012 at 03:04:17PM +0100, Marc-André Lureau wrote:
> ---
> data/spice-protocol.vapi | 91 +++++++++++
> gtk/controller/Makefile.am | 10 +-
> gtk/controller/custom.vapi | 9 +
> gtk/controller/foreign-menu.vala | 207 ++++++++++++++++++++++++++
> gtk/controller/menu.vala | 6 +
> gtk/controller/spice-foreign-menu-listener.c | 159 ++++++++++++++++++++
> gtk/controller/spice-foreign-menu-listener.h | 47 ++++++
> spice-protocol | 2 +-
> 8 files changed, 529 insertions(+), 2 deletions(-)
> create mode 100644 gtk/controller/foreign-menu.vala
> create mode 100644 gtk/controller/spice-foreign-menu-listener.c
> create mode 100644 gtk/controller/spice-foreign-menu-listener.h
>
> diff --git a/data/spice-protocol.vapi b/data/spice-protocol.vapi
> index 4cb1a2f..01b1a81 100644
> --- a/data/spice-protocol.vapi
> +++ b/data/spice-protocol.vapi
> @@ -107,4 +107,95 @@ namespace SpiceProtocol {
> GRAYED,
> }
> }
> +
> + [CCode (cprefix = "FrgMenu", cheader_filename = "spice/foreign_menu_prot.h")]
> + namespace ForeignMenu {
> + [CCode (cname = "FOREIGN_MENU_MAGIC")]
> + public const uint32 MAGIC;
> + [CCode (cname = "FOREIGN_MENU_VERSION")]
> + public const int VERSION;
> +
> + [Compact]
> + public struct InitHeader {
> + uint32 magic;
> + uint32 version;
> + uint32 size;
> + }
> +
> + [Compact]
> + [CCode (has_destroy_function = false)]
> + public struct Init {
> + InitHeader base;
> + uint64 credentials;
> + string title; // utf8
> + }
> +
> + [Compact]
> + public struct Msg {
> + uint32 id;
> + uint32 size;
> + }
> +
> + [CCode (cprefix = "FOREIGN_MENU_", cname = "int")]
> + public enum MsgId {
> + //extrenal app -> spice client
> + SET_TITLE,
> + ADD_ITEM,
> + MODIFY_ITEM,
> + REMOVE_ITEM,
> + CLEAR,
> +
> + //spice client -> external app
> + ITEM_EVENT,
> + APP_ACTIVATED,
> + APP_DEACTIVATED,
> + }
> +
> + [Compact]
> + [CCode (cname = "FrgMenuSetTitle")]
> + public struct SetTitle {
> + Msg base;
> + string string; // utf8
> + }
> +
> + [CCode (cprefix = "FOREIGN_MENU_ITEM_TYPE_", cname = "unsigned int", has_type_id = false)]
> + [Flags]
> + public enum MenuFlags {
> + CHECKED,
> + DIM,
> + SEPARATOR
> + }
> +
> + [Compact]
> + [CCode (cname = "FrgMenuAddItem")]
> + public struct AddItem {
> + Msg base;
> + uint32 id;
> + uint32 type;
> + uint32 position;
> + string string; // utf8
> + }
> +
> + [Compact]
> + [CCode (cname = "FrgMenuRmItem")]
> + public struct RmItem {
> + Msg base;
> + uint32 id;
> + }
> +
> + [CCode (cprefix = "FOREIGN_MENU_EVENT_", cname = "int")]
> + public enum EventType {
> + CLICK,
> + CHECKED,
> + UNCHECKED,
> + }
> +
> + [Compact]
> + [CCode (cname = "FrgMenuEvent")]
> + public struct Event {
> + Msg base;
> + uint32 id;
> + uint32 action;
> + }
> + }
> }
> diff --git a/gtk/controller/Makefile.am b/gtk/controller/Makefile.am
> index 522e014..20f1a48 100644
> --- a/gtk/controller/Makefile.am
> +++ b/gtk/controller/Makefile.am
> @@ -1,6 +1,11 @@
> NULL =
>
> -AM_CPPFLAGS = $(GIO_CFLAGS) $(PROTOCOL_CFLAGS)
> +AM_CPPFLAGS = \
> + -DG_LOG_DOMAIN=\"GSpiceController\" \
> + $(GIO_CFLAGS) \
> + $(PROTOCOL_CFLAGS) \
> + $(NULL)
> +
> # http://www.gnu.org/software/libtool/manual/html_node/Updating-version-info.html
> AM_LDFLAGS = \
> -no-undefined \
> @@ -22,11 +27,14 @@ BUILT_SOURCES = controller.vala.stamp
> libspice_controller_la_VALASOURCES = \
> menu.vala \
> controller.vala \
> + foreign-menu.vala \
> $(NULL)
> libspice_controller_la_SOURCES = \
> custom.h \
> spice-controller-listener.c \
> spice-controller-listener.h \
> + spice-foreign-menu-listener.c \
> + spice-foreign-menu-listener.h \
> $(libspice_controller_la_VALASOURCES:.vala=.c) \
> $(NULL)
>
> diff --git a/gtk/controller/custom.vapi b/gtk/controller/custom.vapi
> index 7a94b82..a12fdec 100644
> --- a/gtk/controller/custom.vapi
> +++ b/gtk/controller/custom.vapi
> @@ -16,4 +16,13 @@ namespace Spice {
> [CCode (cname = "spice_controller_listener_accept_async", cheader_filename = "spice-controller-listener.h")]
> public async unowned GLib.IOStream accept_async (GLib.Cancellable? cancellable = null, out GLib.Object? source_object = null) throws GLib.Error;
> }
> +
> + [CCode (cname = "GObject", ref_function = "g_object_ref", unref_function = "g_object_unref", free_function = "")]
> + class ForeignMenuListener {
> + [CCode (cname = "spice_foreign_menu_listener_new", cheader_filename = "spice-foreign-menu-listener.h")]
> + public static ForeignMenuListener new_listener (string addr) throws GLib.Error;
> +
> + [CCode (cname = "spice_foreign_menu_listener_accept_async", cheader_filename = "spice-foreign-menu-listener.h")]
> + public async unowned GLib.IOStream accept_async (GLib.Cancellable? cancellable = null, out GLib.Object? source_object = null) throws GLib.Error;
> + }
> }
> diff --git a/gtk/controller/foreign-menu.vala b/gtk/controller/foreign-menu.vala
> new file mode 100644
> index 0000000..677e2ad
> --- /dev/null
> +++ b/gtk/controller/foreign-menu.vala
> @@ -0,0 +1,207 @@
> +// 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/>.
> +
> +using Custom;
> +
> +namespace SpiceCtrl {
> +
> +public class ForeignMenu: Object {
> +
> + public Menu menu { get; private set; }
> + public string title { get; private set; }
> +
> + private int nclients;
> + private List<IOStream> clients;
> +
> + public ForeignMenu() {
> + menu = new Menu ();
> + }
> +
> + public void menu_item_click_msg (int32 item_id) {
> + debug ("clicked id: %d".printf (item_id));
> +
> + var msg = SpiceProtocol.ForeignMenu.Event ();
> + msg.base.size = (uint32)sizeof (SpiceProtocol.ForeignMenu.Event);
> + msg.base.id = SpiceProtocol.ForeignMenu.MsgId.ITEM_EVENT;
> + msg.id = item_id;
> + msg.action = SpiceProtocol.ForeignMenu.EventType.CLICK;
> +
> + unowned uint8[] p = ((uint8[])(&msg))[0:msg.base.size];
> + send_msg (p);
> + }
> +
> + public void menu_item_checked_msg (int32 item_id, bool checked = true) {
> + debug ("%schecked id: %d".printf (checked ? "" : "un", item_id));
> +
> + var msg = SpiceProtocol.ForeignMenu.Event ();
> + msg.base.size = (uint32)sizeof (SpiceProtocol.ForeignMenu.Event);
> + msg.base.id = SpiceProtocol.ForeignMenu.MsgId.ITEM_EVENT;
> + msg.id = item_id;
> + msg.action = checked ?
> + SpiceProtocol.ForeignMenu.EventType.CHECKED :
> + SpiceProtocol.ForeignMenu.EventType.UNCHECKED;
> +
> + unowned uint8[] p = ((uint8[])(&msg))[0:msg.base.size];
> + send_msg (p);
> + }
> +
> + public void app_activated_msg (bool activated = true) {
> + var msg = SpiceProtocol.ForeignMenu.Msg ();
> + msg.size = (uint32)sizeof (SpiceProtocol.ForeignMenu.Event);
> + msg.id = activated ?
> + SpiceProtocol.ForeignMenu.MsgId.APP_ACTIVATED :
> + SpiceProtocol.ForeignMenu.MsgId.APP_DEACTIVATED;
> +
> + unowned uint8[] p = ((uint8[])(&msg))[0:msg.size];
> + send_msg (p);
> + }
> +
> + public async bool send_msg (uint8[] p) throws GLib.Error {
> + // vala FIXME: pass Controller.Msg instead
> + // vala doesn't keep reference on the struct in async methods
> + // it copies only base, which is not enough to transmit the whole
> + // message.
> + try {
> + foreach (var c in clients)
> + yield c.output_stream.write_async (p);
> + } catch (GLib.Error e) {
> + warning (e.message);
> + }
> +
> + return true;
> + }
> +
> + SpiceProtocol.Controller.MenuFlags get_menu_flags (uint32 type) {
> + SpiceProtocol.Controller.MenuFlags flags = 0;
> +
> + if ((SpiceProtocol.ForeignMenu.MenuFlags.CHECKED & type) != 0)
> + flags |= SpiceProtocol.Controller.MenuFlags.CHECKED;
> + if ((SpiceProtocol.ForeignMenu.MenuFlags.DIM & type) != 0)
> + flags |= SpiceProtocol.Controller.MenuFlags.GRAYED;
> +
> + return flags;
> + }
> +
> + private bool handle_message (SpiceProtocol.ForeignMenu.Msg* msg) {
> + switch (msg.id) {
> + case SpiceProtocol.ForeignMenu.MsgId.SET_TITLE:
> + var t = (SpiceProtocol.ForeignMenu.SetTitle*)(msg);
> + title = t.string;
> + break;
> + case SpiceProtocol.ForeignMenu.MsgId.ADD_ITEM:
> + var i = (SpiceProtocol.ForeignMenu.AddItem*)(msg);
> + debug ("add id:%u type:%u position:%u title:%s", i.id, i.type, i.position, i.string);
> + menu.items.append (new MenuItem ((int)i.id, i.string, get_menu_flags (i.type)));
> + notify_property ("menu");
> + break;
> + case SpiceProtocol.ForeignMenu.MsgId.MODIFY_ITEM:
> + debug ("deprecated: modify item");
> + break;
> + case SpiceProtocol.ForeignMenu.MsgId.REMOVE_ITEM:
> + var i = (SpiceProtocol.ForeignMenu.RmItem*)(msg);
> + debug ("not implemented: remove id:%u".printf (i.id));
> + break;
> + case SpiceProtocol.ForeignMenu.MsgId.CLEAR:
> + menu = new Menu ();
> + break;
> + default:
> + warn_if_reached ();
> + return false;
> + }
> + return true;
> + }
> +
> + private async void handle_client (IOStream c) throws GLib.Error {
> + var header = SpiceProtocol.ForeignMenu.InitHeader ();
> + unowned uint8[] p = null;
> +
> + debug ("new socket client, reading init header");
> +
> + p = ((uint8[])(&header))[0:sizeof(SpiceProtocol.ForeignMenu.InitHeader)]; // FIXME vala
> + var read = yield c.input_stream.read_async (p);
> + if (warn_if (read != sizeof (SpiceProtocol.ForeignMenu.InitHeader)))
> + return;
> + if (warn_if (header.magic != SpiceProtocol.ForeignMenu.MAGIC))
> + return;
> + if (warn_if (header.version != SpiceProtocol.ForeignMenu.VERSION))
> + return;
> + if (warn_if (header.size < sizeof (SpiceProtocol.ForeignMenu.Init)))
> + return;
> +
> + uint64 credentials = 0;
> + p = ((uint8[])(&credentials))[0:sizeof(uint64)];
> + read = yield c.input_stream.read_async (p);
> + if (warn_if (read != sizeof(uint64)))
> + return;
> + if (warn_if (credentials != 0))
> + return;
> +
> + var title_size = header.size - sizeof(SpiceProtocol.ForeignMenu.Init);
> + var title = new uint8[title_size + 1];
> + read = yield c.input_stream.read_async (title[0:title_size]);
> + this.title = (string)title;
> +
> + var t = new uint8[sizeof(SpiceProtocol.ForeignMenu.Msg)];
> + for (;;) {
> + read = yield c.input_stream.read_async (t[0:sizeof(SpiceProtocol.ForeignMenu.Msg)]);
> + if (read == 0)
> + break;
> +
> + if (warn_if (read != sizeof (SpiceProtocol.ForeignMenu.Msg))) {
> + warning ("read only: " + read.to_string ());
> + break;
> + }
> +
> + var msg = (SpiceProtocol.ForeignMenu.Msg*)t;
> + if (warn_if (msg.size < sizeof (SpiceProtocol.ForeignMenu.Msg)))
> + break;
> +
> + if (msg.size > sizeof (SpiceProtocol.ForeignMenu.Msg)) {
> + t.resize ((int)msg.size);
> + msg = (SpiceProtocol.ForeignMenu.Msg*)t;
> + read = yield c.input_stream.read_async (t[sizeof(SpiceProtocol.ForeignMenu.Msg):msg.size]);
> + if (read == 0)
> + break;
> + if (warn_if (read != msg.size - sizeof(SpiceProtocol.ForeignMenu.Msg)))
> + break;
> + }
> +
> + handle_message (msg);
> + }
> +
> + }
> +
> + public async void listen (string? addr = null) throws GLib.Error, SpiceCtrl.Error
> + {
> + var listener = Spice.ForeignMenuListener.new_listener (addr);
> +
> + for (;;) {
> + var c = yield listener.accept_async ();
> + nclients += 1;
> + clients.append (c);
> + try {
> + yield handle_client (c);
> + } catch (GLib.Error e) {
> + warning (e.message);
> + }
> + c.close ();
> + clients.remove (c);
> + nclients -= 1;
> + }
> + }
> +
> +}
> +
> +} // SpiceCtrl
> diff --git a/gtk/controller/menu.vala b/gtk/controller/menu.vala
> index 7f2f42a..7e8fc16 100644
> --- a/gtk/controller/menu.vala
> +++ b/gtk/controller/menu.vala
> @@ -28,6 +28,12 @@ public class MenuItem: Object {
> public string accel;
> public SpiceProtocol.Controller.MenuFlags flags;
>
> + public MenuItem (int id, string text, SpiceProtocol.Controller.MenuFlags flags) {
> + this.id = id;
> + this.text = text;
> + this.flags = flags;
> + }
> +
> public MenuItem.from_string (string str) throws SpiceCtrl.Error {
> var params = str.split (SpiceProtocol.Controller.MENU_PARAM_DELIMITER);
> if (warn_if (params.length != 5))
> diff --git a/gtk/controller/spice-foreign-menu-listener.c b/gtk/controller/spice-foreign-menu-listener.c
> new file mode 100644
> index 0000000..8322a13
> --- /dev/null
> +++ b/gtk/controller/spice-foreign-menu-listener.c
> @@ -0,0 +1,159 @@
> +/* -*- 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 <glib.h>
> +#include <glib/gstdio.h>
> +
> +#include "spice-foreign-menu-listener.h"
> +
> +#ifdef G_OS_WIN32
> +#include <windows.h>
> +#include "namedpipe.h"
> +#include "namedpipelistener.h"
> +#endif
> +
> +#ifdef G_OS_UNIX
> +#include <gio/gunixsocketaddress.h>
> +#endif
> +
> +/**
> + * SpiceForeignMenuListenerError:
> + * @SPICE_FOREIGN_MENU_LISTENER_ERROR_VALUE: invalid value.
> + *
> + * Possible errors of foreign menu listener related functions.
> + **/
> +
> +/**
> + * SPICE_FOREIGN_MENU_LISTENER_ERROR:
> + *
> + * The error domain of the foreign menu listener subsystem.
> + **/
> +GQuark
> +spice_foreign_menu_listener_error_quark (void)
> +{
> + return g_quark_from_static_string ("spice-foreign-menu-listener-error");
> +}
> +
> +GObject*
> +spice_foreign_menu_listener_new (const gchar *address, GError **error)
> +{
> + GObject *listener = NULL;
> + gchar *addr = NULL;
> +
> + g_return_val_if_fail (error == NULL || *error == NULL, NULL);
> +
> + addr = g_strdup (address);
> +
> +#ifdef G_OS_WIN32
> + if (addr == NULL)
> + addr = g_strdup (g_getenv ("SPICE_FOREIGN_MENU_NAMEDPIPE"));
> + if (addr == NULL)
> + addr = g_strdup_printf ("\\\\.\\pipe\\SpiceForeignMenu-%" G_GUINT64_FORMAT, (guint64)GetCurrentProcessId ());
> +#else
> + if (addr == NULL)
> + addr = g_strdup (g_getenv ("SPICE_FOREIGN_MENU_SOCKET"));
> + if (addr == NULL)
> + addr = g_strdup_printf ("/tmp/SpiceForeignMenu-%" G_GUINT64_FORMAT ".uds", (guint64)getpid ());
> +#endif
> + if (addr == NULL) {
> + g_set_error (error,
> + SPICE_FOREIGN_MENU_LISTENER_ERROR,
> + SPICE_FOREIGN_MENU_LISTENER_ERROR_VALUE,
> +#ifdef G_OS_WIN32
> + "Missing namedpipe address"
> +#else
> + "Missing socket address"
> +#endif
> + );
> + goto end;
> + }
> +
> + g_unlink (addr);
> +
> +#ifdef G_OS_WIN32
> + {
> + SpiceNamedPipe *np;
> +
> + listener = G_OBJECT (spice_named_pipe_listener_new ());
> +
> + np = spice_named_pipe_new (addr, error);
> + if (!np) {
> + g_object_unref (listener);
> + listener = NULL;
> + goto end;
> + }
> +
> + spice_named_pipe_listener_add_named_pipe (SPICE_NAMED_PIPE_LISTENER (listener), np);
> + }
> +#else
> + {
> + listener = G_OBJECT (g_socket_listener_new ());
> +
> + if (!g_socket_listener_add_address (G_SOCKET_LISTENER (listener),
> + G_SOCKET_ADDRESS (g_unix_socket_address_new (addr)),
> + G_SOCKET_TYPE_STREAM,
> + G_SOCKET_PROTOCOL_DEFAULT,
> + NULL,
> + NULL,
> + error))
> + g_warning ("failed to add address");
> + }
> +#endif
> +
> +end:
> + g_free (addr);
> + return listener;
> +}
> +
> +void
> +spice_foreign_menu_listener_accept_async (GObject *listener,
> + GCancellable *cancellable,
> + GAsyncReadyCallback callback,
> + gpointer user_data)
> +{
> + g_return_if_fail(G_IS_OBJECT(listener));
> +
> +#ifdef G_OS_WIN32
> + spice_named_pipe_listener_accept_async (SPICE_NAMED_PIPE_LISTENER (listener), cancellable, callback, user_data);
> +#else
> + g_socket_listener_accept_async (G_SOCKET_LISTENER (listener), cancellable, callback, user_data);
> +#endif
> +}
> +
> +GIOStream*
> +spice_foreign_menu_listener_accept_finish (GObject *listener,
> + GAsyncResult *result,
> + GObject **source_object,
> + GError **error)
> +{
> + g_return_val_if_fail(G_IS_OBJECT(listener), NULL);
> +
> +#ifdef G_OS_WIN32
> + SpiceNamedPipeConnection *np;
> + np = spice_named_pipe_listener_accept_finish (SPICE_NAMED_PIPE_LISTENER (listener), result, source_object, error);
> + if (np)
> + return G_IO_STREAM (np);
> +#else
> + GSocketConnection *socket;
> + socket = g_socket_listener_accept_finish (G_SOCKET_LISTENER (listener), result, source_object, error);
> + if (socket)
> + return G_IO_STREAM (socket);
> +#endif
> +
> + return NULL;
> +}
> diff --git a/gtk/controller/spice-foreign-menu-listener.h b/gtk/controller/spice-foreign-menu-listener.h
> new file mode 100644
> index 0000000..1071528
> --- /dev/null
> +++ b/gtk/controller/spice-foreign-menu-listener.h
> @@ -0,0 +1,47 @@
> +/* -*- 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_FOREIGN_MENU_LISTENER_H__
> +#define __SPICE_FOREIGN_MENU_LISTENER_H__
> +
> +#include <gio/gio.h>
> +
> +G_BEGIN_DECLS
> +
> +#define SPICE_FOREIGN_MENU_LISTENER_ERROR spice_foreign_menu_listener_error_quark ()
> +GQuark spice_foreign_menu_listener_error_quark (void);
> +
> +typedef enum
> +{
> + SPICE_FOREIGN_MENU_LISTENER_ERROR_VALUE /* incorrect value */
> +} SpiceForeignMenuListenerError;
> +
> +
> +GObject* spice_foreign_menu_listener_new (const gchar *address, GError **error);
> +
> +void spice_foreign_menu_listener_accept_async (GObject *listener,
> + GCancellable *cancellable,
> + GAsyncReadyCallback callback,
> + gpointer user_data);
> +
> +GIOStream* spice_foreign_menu_listener_accept_finish (GObject *listener,
> + GAsyncResult *result,
> + GObject **source_object,
> + GError **error);
> +G_END_DECLS
> +
> +#endif /* __SPICE_FOREIGN_MENU_LISTENER_H__ */
> diff --git a/spice-protocol b/spice-protocol
> index cda8862..d5edafd 160000
> --- a/spice-protocol
> +++ b/spice-protocol
> @@ -1 +1 @@
> -Subproject commit cda88623d0754aeeda005ddc048dd113d279845b
> +Subproject commit d5edafd28ab762b1b5f663aec449d3e3743f1184
> --
> 1.7.7.6
>
> _______________________________________________
> Spice-devel mailing list
> Spice-devel at lists.freedesktop.org
> http://lists.freedesktop.org/mailman/listinfo/spice-devel
-------------- next part --------------
A non-text attachment was scrubbed...
Name: not available
Type: application/pgp-signature
Size: 198 bytes
Desc: not available
URL: <http://lists.freedesktop.org/archives/spice-devel/attachments/20120229/4a9aa8ab/attachment-0001.pgp>
More information about the Spice-devel
mailing list