Backport clipboard-race patches for #1755038

This commit is contained in:
Adam Williamson 2019-10-04 08:05:56 -07:00
parent 9b912f4c8f
commit 56f7953ad2
7 changed files with 523 additions and 1 deletions

View File

@ -0,0 +1,77 @@
From 16b41429618985e4ab8c597fbd0c5cca5cf7b3e2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <marcandre.lureau@redhat.com>
Date: Thu, 21 Mar 2019 13:21:33 +0100
Subject: [PATCH 1/6] clipboard: do not release between client grabs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
On the client side, whenever the grab owner changes (and the clipboard
was previously grabbed), spice-gtk sends a clipboard release followed
immediately by a new grab. But some clipboard managers on the remote
side react to clipboard release events by taking a clipboard grab,
presumably to avoid empty clipboards.
The two grabs, coming from the client and from the remote sides, will
race in both directions, which may confuse the client & remote side,
as both believe the other side is the current grab owner, and thus
further clipboard data requests are likely to fail.
Let's avoid sending a release event when re-grabing.
The race described above may still happen in other rare circunstances,
and will require a protocol change. To avoid the conflict, a discussed
solution could use a clipboard serial number.
Tested with current linux & windows vdagent. Looking at earlier
version of the code, it doesn't seem like subsequent grabs will be
treated as an error.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
src/spice-gtk-session.c | 21 ++++++++-------------
1 file changed, 8 insertions(+), 13 deletions(-)
diff --git a/src/spice-gtk-session.c b/src/spice-gtk-session.c
index 60399d0..cb00fa5 100644
--- a/src/spice-gtk-session.c
+++ b/src/spice-gtk-session.c
@@ -577,8 +577,7 @@ static void clipboard_get_targets(GtkClipboard *clipboard,
g_return_if_fail(selection != -1);
if (s->clip_grabbed[selection]) {
- SPICE_DEBUG("Clipboard is already grabbed, ignoring %d atoms", n_atoms);
- return;
+ SPICE_DEBUG("Clipboard is already grabbed, re-grab: %d atoms", n_atoms);
}
/* Set all Atoms that matches our current protocol implementation */
@@ -660,18 +659,14 @@ static void clipboard_owner_change(GtkClipboard *clipboard,
return;
}
- /* In case we sent a grab to the agent, we need to release it now as
- * previous clipboard data should not be reachable anymore */
- if (s->clip_grabbed[selection]) {
- s->clip_grabbed[selection] = FALSE;
- if (spice_main_channel_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)) {
- spice_main_channel_clipboard_selection_release(s->main, selection);
- }
- }
-
- /* We are mostly interested when owner has changed in which case
- * we would like to let agent know about new clipboard data. */
if (event->reason != GDK_OWNER_CHANGE_NEW_OWNER) {
+ if (s->clip_grabbed[selection]) {
+ /* grab was sent to the agent, so release it */
+ s->clip_grabbed[selection] = FALSE;
+ if (spice_main_channel_agent_test_capability(s->main, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND)) {
+ spice_main_channel_clipboard_selection_release(s->main, selection);
+ }
+ }
s->clip_hasdata[selection] = FALSE;
return;
}
--
2.23.0

View File

@ -0,0 +1,167 @@
From 271656ae32e756eacfcc522d1aaab9d7102b0ce7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <marcandre.lureau@redhat.com>
Date: Thu, 21 Mar 2019 13:21:34 +0100
Subject: [PATCH 2/6] clipboard: do not release between remote grabs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Delay the release events for 0.5 sec. If no further grab comes in,
then release the grab. Otherwise, let's skip the release. This avoids
some races with clipboard managers.
Related to:
https://gitlab.freedesktop.org/spice/spice-gtk/issues/82
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
src/spice-gtk-session.c | 80 ++++++++++++++++++++++++++++++++++++-----
1 file changed, 72 insertions(+), 8 deletions(-)
diff --git a/src/spice-gtk-session.c b/src/spice-gtk-session.c
index cb00fa5..79385a4 100644
--- a/src/spice-gtk-session.c
+++ b/src/spice-gtk-session.c
@@ -59,6 +59,7 @@ struct _SpiceGtkSessionPrivate {
gboolean clip_hasdata[CLIPBOARD_LAST];
gboolean clip_grabbed[CLIPBOARD_LAST];
gboolean clipboard_by_guest[CLIPBOARD_LAST];
+ guint clipboard_release_delay[CLIPBOARD_LAST];
/* auto-usbredir related */
gboolean auto_usbredir_enable;
int auto_usbredir_reqs;
@@ -95,6 +96,7 @@ struct _SpiceGtkSessionPrivate {
/* ------------------------------------------------------------------ */
/* Prototypes for private functions */
+static void clipboard_release(SpiceGtkSession *self, guint selection);
static void clipboard_owner_change(GtkClipboard *clipboard,
GdkEventOwnerChange *event,
gpointer user_data);
@@ -255,6 +257,23 @@ static void spice_gtk_session_dispose(GObject *gobject)
G_OBJECT_CLASS(spice_gtk_session_parent_class)->dispose(gobject);
}
+static void clipboard_release_delay_remove(SpiceGtkSession *self, guint selection,
+ gboolean release_if_delayed)
+{
+ SpiceGtkSessionPrivate *s = self->priv;
+
+ if (!s->clipboard_release_delay[selection])
+ return;
+
+ if (release_if_delayed) {
+ SPICE_DEBUG("delayed clipboard release, sel:%u", selection);
+ clipboard_release(self, selection);
+ }
+
+ g_source_remove(s->clipboard_release_delay[selection]);
+ s->clipboard_release_delay[selection] = 0;
+}
+
static void spice_gtk_session_finalize(GObject *gobject)
{
SpiceGtkSession *self = SPICE_GTK_SESSION(gobject);
@@ -264,6 +283,7 @@ static void spice_gtk_session_finalize(GObject *gobject)
/* release stuff */
for (i = 0; i < CLIPBOARD_LAST; ++i) {
g_clear_pointer(&s->clip_targets[i], g_free);
+ clipboard_release_delay_remove(self, i, true);
}
/* Chain up to the parent class */
@@ -823,6 +843,8 @@ static gboolean clipboard_grab(SpiceMainChannel *main, guint selection,
int m, n;
int num_targets = 0;
+ clipboard_release_delay_remove(self, selection, false);
+
cb = get_clipboard_from_selection(s, selection);
g_return_val_if_fail(cb != NULL, FALSE);
@@ -1052,17 +1074,12 @@ static gboolean clipboard_request(SpiceMainChannel *main, guint selection,
return TRUE;
}
-static void clipboard_release(SpiceMainChannel *main, guint selection,
- gpointer user_data)
+static void clipboard_release(SpiceGtkSession *self, guint selection)
{
- g_return_if_fail(SPICE_IS_GTK_SESSION(user_data));
-
- SpiceGtkSession *self = user_data;
SpiceGtkSessionPrivate *s = self->priv;
GtkClipboard* clipboard = get_clipboard_from_selection(s, selection);
- if (!clipboard)
- return;
+ g_return_if_fail(clipboard != NULL);
s->nclip_targets[selection] = 0;
@@ -1072,6 +1089,53 @@ static void clipboard_release(SpiceMainChannel *main, guint selection,
s->clipboard_by_guest[selection] = FALSE;
}
+typedef struct SpiceGtkClipboardRelease {
+ SpiceGtkSession *self;
+ guint selection;
+} SpiceGtkClipboardRelease;
+
+static gboolean clipboard_release_timeout(gpointer user_data)
+{
+ SpiceGtkClipboardRelease *rel = user_data;
+
+ clipboard_release_delay_remove(rel->self, rel->selection, true);
+
+ return G_SOURCE_REMOVE;
+}
+
+/*
+ * The agents send release between two grabs. This may trigger
+ * clipboard managers trying to grab the clipboard. We end up with two
+ * sides, client and remote, racing for the clipboard grab, and
+ * believing each other is the owner.
+ *
+ * Workaround this problem by delaying the release event by 0.5 sec.
+ * FIXME: protocol change to solve the conflict and set client priority.
+ */
+#define CLIPBOARD_RELEASE_DELAY 500 /* ms */
+
+static void clipboard_release_delay(SpiceMainChannel *main, guint selection,
+ gpointer user_data)
+{
+ SpiceGtkSession *self = SPICE_GTK_SESSION(user_data);
+ SpiceGtkSessionPrivate *s = self->priv;
+ GtkClipboard* clipboard = get_clipboard_from_selection(s, selection);
+ SpiceGtkClipboardRelease *rel;
+
+ if (!clipboard)
+ return;
+
+ clipboard_release_delay_remove(self, selection, true);
+
+ rel = g_new0(SpiceGtkClipboardRelease, 1);
+ rel->self = self;
+ rel->selection = selection;
+ s->clipboard_release_delay[selection] =
+ g_timeout_add_full(G_PRIORITY_DEFAULT, CLIPBOARD_RELEASE_DELAY,
+ clipboard_release_timeout, rel, g_free);
+
+}
+
static void channel_new(SpiceSession *session, SpiceChannel *channel,
gpointer user_data)
{
@@ -1088,7 +1152,7 @@ static void channel_new(SpiceSession *session, SpiceChannel *channel,
g_signal_connect(channel, "main-clipboard-selection-request",
G_CALLBACK(clipboard_request), self);
g_signal_connect(channel, "main-clipboard-selection-release",
- G_CALLBACK(clipboard_release), self);
+ G_CALLBACK(clipboard_release_delay), self);
}
if (SPICE_IS_INPUTS_CHANNEL(channel)) {
spice_g_signal_connect_object(channel, "inputs-modifiers",
--
2.23.0

View File

@ -0,0 +1,32 @@
From c771229a978c69323869d4063228306dc41e15b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakub=20Jank=C5=AF?= <jjanku@redhat.com>
Date: Mon, 30 Sep 2019 18:44:05 +0200
Subject: [PATCH 3/6] fixup! clipboard: do not release between remote grabs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Jakub Janků <jjanku@redhat.com>
---
src/spice-gtk-session.c | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/spice-gtk-session.c b/src/spice-gtk-session.c
index 79385a4..cbfe3bf 100644
--- a/src/spice-gtk-session.c
+++ b/src/spice-gtk-session.c
@@ -782,6 +782,11 @@ static void clipboard_get(GtkClipboard *clipboard,
g_return_if_fail(info < SPICE_N_ELEMENTS(atom2agent));
g_return_if_fail(s->main != NULL);
+ if (s->clipboard_release_delay[selection]) {
+ SPICE_DEBUG("not requesting data from guest during delayed release");
+ return;
+ }
+
ri.selection_data = selection_data;
ri.info = info;
ri.loop = g_main_loop_new(NULL, FALSE);
--
2.23.0

View File

@ -0,0 +1,79 @@
From 31a44bfd0acc3ae18682ae19ff1c8a8101bc96f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <marcandre.lureau@redhat.com>
Date: Fri, 22 Mar 2019 15:20:12 +0100
Subject: [PATCH 4/6] clipboard: do not delay release if agent has "no release
on regrab"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
meson.build | 2 +-
src/channel-main.c | 2 ++
src/spice-gtk-session.c | 9 ++++++++-
3 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/meson.build b/meson.build
index 171a3f6..b89a15b 100644
--- a/meson.build
+++ b/meson.build
@@ -81,7 +81,7 @@ endforeach
#
# check for mandatory dependencies
#
-spice_protocol_version='>= 0.12.15'
+spice_protocol_version='>= 0.14.1'
glib_version = '2.46'
glib_version_info = '>= @0@'.format(glib_version)
diff --git a/src/channel-main.c b/src/channel-main.c
index 4305dcd..adf6bab 100644
--- a/src/channel-main.c
+++ b/src/channel-main.c
@@ -222,6 +222,7 @@ static const char *agent_caps[] = {
[ VD_AGENT_CAP_AUDIO_VOLUME_SYNC ] = "volume-sync",
[ VD_AGENT_CAP_MONITORS_CONFIG_POSITION ] = "monitors config position",
[ VD_AGENT_CAP_FILE_XFER_DISABLED ] = "file transfer disabled",
+ [ VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB ] = "no release on re-grab",
};
#define NAME(_a, _i) ((_i) < SPICE_N_ELEMENTS(_a) ? (_a[(_i)] ?: "?") : "?")
@@ -1333,6 +1334,7 @@ static void agent_announce_caps(SpiceMainChannel *channel)
VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_SELECTION);
VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_MONITORS_CONFIG_POSITION);
VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB);
agent_msg_queue(channel, VD_AGENT_ANNOUNCE_CAPABILITIES, size, caps);
g_free(caps);
diff --git a/src/spice-gtk-session.c b/src/spice-gtk-session.c
index cbfe3bf..34ae4a1 100644
--- a/src/spice-gtk-session.c
+++ b/src/spice-gtk-session.c
@@ -1114,7 +1114,8 @@ static gboolean clipboard_release_timeout(gpointer user_data)
* sides, client and remote, racing for the clipboard grab, and
* believing each other is the owner.
*
- * Workaround this problem by delaying the release event by 0.5 sec.
+ * Workaround this problem by delaying the release event by 0.5 sec,
+ * unless the no-release-on-regrab capability is present.
* FIXME: protocol change to solve the conflict and set client priority.
*/
#define CLIPBOARD_RELEASE_DELAY 500 /* ms */
@@ -1132,6 +1133,12 @@ static void clipboard_release_delay(SpiceMainChannel *main, guint selection,
clipboard_release_delay_remove(self, selection, true);
+ if (spice_main_channel_agent_test_capability(s->main,
+ VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB)) {
+ clipboard_release(self, selection);
+ return;
+ }
+
rel = g_new0(SpiceGtkClipboardRelease, 1);
rel->self = self;
rel->selection = selection;
--
2.23.0

View File

@ -0,0 +1,33 @@
From 7bbf04e5484129037d29018352ab6d3333fddf05 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <marcandre.lureau@redhat.com>
Date: Fri, 22 Mar 2019 15:20:13 +0100
Subject: [PATCH 5/6] clipboard: pre-condition on selection value < 256
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The protocol uses a u8 for the selection value. Make sure the given
argument value fits there, or throw a critical.
The other places seem to use u8 variables already.
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
src/channel-main.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/channel-main.c b/src/channel-main.c
index adf6bab..8574ae6 100644
--- a/src/channel-main.c
+++ b/src/channel-main.c
@@ -1354,6 +1354,7 @@ static void agent_clipboard_grab(SpiceMainChannel *channel, guint selection,
if (!c->agent_connected)
return;
+ g_return_if_fail(selection < 256);
g_return_if_fail(test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_BY_DEMAND));
size = sizeof(VDAgentClipboardGrab) + sizeof(uint32_t) * ntypes;
--
2.23.0

View File

@ -0,0 +1,121 @@
From 48e516347587f6f9e21f3ba9616079521296888d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Marc-Andr=C3=A9=20Lureau?= <marcandre.lureau@redhat.com>
Date: Fri, 22 Mar 2019 15:20:14 +0100
Subject: [PATCH 6/6] clipboard: implement CAP_CLIPBOARD_GRAB_SERIAL
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
---
src/channel-main.c | 32 +++++++++++++++++++++++++++++++-
src/spice-gtk-session.c | 1 -
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/src/channel-main.c b/src/channel-main.c
index 8574ae6..0062593 100644
--- a/src/channel-main.c
+++ b/src/channel-main.c
@@ -111,6 +111,7 @@ struct _SpiceMainChannelPrivate {
guint migrate_delayed_id;
spice_migrate *migrate_data;
int max_clipboard;
+ uint32_t clipboard_serial[256];
gboolean agent_volume_playback_sync;
gboolean agent_volume_record_sync;
@@ -223,6 +224,7 @@ static const char *agent_caps[] = {
[ VD_AGENT_CAP_MONITORS_CONFIG_POSITION ] = "monitors config position",
[ VD_AGENT_CAP_FILE_XFER_DISABLED ] = "file transfer disabled",
[ VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB ] = "no release on re-grab",
+ [ VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL ] = "clipboard grab serial",
};
#define NAME(_a, _i) ((_i) < SPICE_N_ELEMENTS(_a) ? (_a[(_i)] ?: "?") : "?")
@@ -412,6 +414,7 @@ static void spice_main_channel_reset_agent(SpiceMainChannel *channel)
spice_main_channel_reset_all_xfer_operations(channel);
file_xfer_flushed(channel, FALSE);
+ memset(c->clipboard_serial, 0, sizeof(c->clipboard_serial));
}
/* main or coroutine context */
@@ -1335,6 +1338,7 @@ static void agent_announce_caps(SpiceMainChannel *channel)
VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_MONITORS_CONFIG_POSITION);
VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_FILE_XFER_DETAILED_ERRORS);
VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_NO_RELEASE_ON_REGRAB);
+ VD_AGENT_SET_CAPABILITY(caps->caps, VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL);
agent_msg_queue(channel, VD_AGENT_ANNOUNCE_CAPABILITIES, size, caps);
g_free(caps);
@@ -1365,6 +1369,10 @@ static void agent_clipboard_grab(SpiceMainChannel *channel, guint selection,
return;
}
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) {
+ size += sizeof(uint32_t);
+ }
+
msg = g_alloca(size);
memset(msg, 0, size);
@@ -1372,7 +1380,13 @@ static void agent_clipboard_grab(SpiceMainChannel *channel, guint selection,
if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_SELECTION)) {
msg[0] = selection;
- grab = (VDAgentClipboardGrab *)(msg + 4);
+ grab = (void *)grab + 4;
+ }
+
+ if (test_agent_cap(channel, VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) {
+ guint32 *serial = (guint32 *)grab;
+ *serial = GUINT32_TO_LE(c->clipboard_serial[selection]++);
+ grab = (void *)grab + sizeof(uint32_t);
}
for (i = 0; i < ntypes; i++) {
@@ -1974,6 +1988,7 @@ static void main_agent_handle_msg(SpiceChannel *channel,
SpiceMainChannel *self = SPICE_MAIN_CHANNEL(channel);
SpiceMainChannelPrivate *c = self->priv;
guint8 selection = VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD;
+ guint32 serial;
g_return_if_fail(msg->protocol == VD_AGENT_PROTOCOL);
@@ -2045,6 +2060,21 @@ static void main_agent_handle_msg(SpiceChannel *channel,
case VD_AGENT_CLIPBOARD_GRAB:
{
gboolean ret;
+
+ if (test_agent_cap(self, VD_AGENT_CAP_CLIPBOARD_GRAB_SERIAL)) {
+ serial = GUINT32_FROM_LE(*((guint32 *)payload));
+ payload = ((guint8*)payload) + sizeof(uint32_t);
+ msg->size -= sizeof(uint32_t);
+
+ if (serial == c->clipboard_serial[selection]) {
+ c->clipboard_serial[selection]++;
+ } else {
+ CHANNEL_DEBUG(channel, "grab discard, serial:%u != c->serial:%u",
+ serial, c->clipboard_serial[selection]);
+ break;
+ }
+ }
+
g_coroutine_signal_emit(self, signals[SPICE_MAIN_CLIPBOARD_SELECTION_GRAB], 0, selection,
(guint8*)payload, msg->size / sizeof(uint32_t), &ret);
if (selection == VD_AGENT_CLIPBOARD_SELECTION_CLIPBOARD)
diff --git a/src/spice-gtk-session.c b/src/spice-gtk-session.c
index 34ae4a1..439199b 100644
--- a/src/spice-gtk-session.c
+++ b/src/spice-gtk-session.c
@@ -1116,7 +1116,6 @@ static gboolean clipboard_release_timeout(gpointer user_data)
*
* Workaround this problem by delaying the release event by 0.5 sec,
* unless the no-release-on-regrab capability is present.
- * FIXME: protocol change to solve the conflict and set client priority.
*/
#define CLIPBOARD_RELEASE_DELAY 500 /* ms */
--
2.23.0

View File

@ -2,7 +2,7 @@
Name: spice-gtk
Version: 0.37
Release: 3%{?dist}
Release: 4%{?dist}
Summary: A GTK+ widget for SPICE clients
License: LGPLv2+
@ -13,6 +13,16 @@ Source1: https://www.spice-space.org/download/gtk/%{name}-%{version}%{?_v
Source2: victortoso-E37A484F.keyring
Patch0001: 0001-vmcstream-Fix-buffer-overflow-sending-data-to-task.patch
# clipboard-race patches: together with patches for spice-protocol
# and spice-gtk these fix problems interacting with mutter's new
# clipboard manager
# https://bugzilla.redhat.com/show_bug.cgi?id=1755038
Patch0002: 0001-clipboard-do-not-release-between-client-grabs.patch
Patch0003: 0002-clipboard-do-not-release-between-remote-grabs.patch
Patch0004: 0003-fixup-clipboard-do-not-release-between-remote-grabs.patch
Patch0005: 0004-clipboard-do-not-delay-release-if-agent-has-no-relea.patch
Patch0006: 0005-clipboard-pre-condition-on-selection-value-256.patch
Patch0007: 0006-clipboard-implement-CAP_CLIPBOARD_GRAB_SERIAL.patch
BuildRequires: git-core
BuildRequires: meson
@ -194,6 +204,9 @@ gpgv2 --quiet --keyring %{SOURCE2} %{SOURCE1} %{SOURCE0}
%{_bindir}/spicy-stats
%changelog
* Fri Oct 04 2019 Adam Williamson <awilliam@redhat.com> - 0.37-4
- Backport clipboard-race patches for #1755038
* Fri Jul 26 2019 Fedora Release Engineering <releng@fedoraproject.org> - 0.37-3
- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild