Blame SOURCES/as-store-locking.patch

0785d5
From e5f73b24c4950ec8e51f6970ad658d604baf6d24 Mon Sep 17 00:00:00 2001
0785d5
From: Kalev Lember <klember@redhat.com>
0785d5
Date: Fri, 14 Dec 2018 11:40:46 +0100
0785d5
Subject: [PATCH 1/3] store: Add thread safe dup() functions for multithreaded
0785d5
 clients
0785d5
0785d5
gnome-software spawns a new worker thread for each of its plugin
0785d5
operations and occasionally this leads to issues where one thread is
0785d5
reading from AsStore, and another one is changing it. There's also file
0785d5
monitor callbacks in AsStore that run in a yet another thread and can
0785d5
update internal data structures as well.
0785d5
0785d5
This leads to a situation where functions returning pointers to internal
0785d5
data structures can't guarantee the data structure lifetime because
0785d5
another thread might be changing it in the background.
0785d5
0785d5
To fix this, this commit adds new as_store_dup_apps() and
0785d5
as_store_dup_apps_by_id_merge() functions that return a deep copy of the
0785d5
internal structure, and updates existing "transfer container"
0785d5
as_store_get_apps_by_id() to return a deep copy, instead just returning
0785d5
a reffed container.
0785d5
---
0785d5
 libappstream-glib/as-self-test.c |  4 +-
0785d5
 libappstream-glib/as-store.c     | 65 +++++++++++++++++++++++++++++++-
0785d5
 libappstream-glib/as-store.h     |  3 ++
0785d5
 3 files changed, 69 insertions(+), 3 deletions(-)
0785d5
0785d5
diff --git a/libappstream-glib/as-self-test.c b/libappstream-glib/as-self-test.c
0785d5
index d90af48..7fd7d75 100644
0785d5
--- a/libappstream-glib/as-self-test.c
0785d5
+++ b/libappstream-glib/as-self-test.c
0785d5
@@ -3477,12 +3477,12 @@ as_test_store_flatpak_func (void)
0785d5
 	AsApp *app;
0785d5
 	AsFormat *format;
0785d5
 	GError *error = NULL;
0785d5
-	GPtrArray *apps;
0785d5
 	gboolean ret;
0785d5
 	g_autofree gchar *filename = NULL;
0785d5
 	g_autofree gchar *filename_root = NULL;
0785d5
 	g_autoptr(AsStore) store = NULL;
0785d5
 	g_autoptr(GFile) file = NULL;
0785d5
+	g_autoptr(GPtrArray) apps = NULL;
0785d5
 
0785d5
 	/* make throws us under a bus, yet again */
0785d5
 	g_setenv ("AS_SELF_TEST_PREFIX_DELIM", "_", TRUE);
0785d5
@@ -3503,7 +3503,7 @@ as_test_store_flatpak_func (void)
0785d5
 	/* test extraction of symlink data */
0785d5
 	g_assert_cmpstr (as_store_get_origin (store), ==, "flatpak");
0785d5
 	g_assert_cmpint (as_store_get_size (store), ==, 1);
0785d5
-	apps = as_store_get_apps (store);
0785d5
+	apps = as_store_dup_apps (store);
0785d5
 	g_assert_cmpint (apps->len, ==, 1);
0785d5
 	app = g_ptr_array_index (apps, 0);
0785d5
 	g_assert_cmpstr (as_app_get_id (app), ==, "flatpak:test.desktop");
0785d5
diff --git a/libappstream-glib/as-store.c b/libappstream-glib/as-store.c
0785d5
index d2075eb..59a7f1c 100644
0785d5
--- a/libappstream-glib/as-store.c
0785d5
+++ b/libappstream-glib/as-store.c
0785d5
@@ -1,6 +1,7 @@
0785d5
 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
0785d5
  *
0785d5
  * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
0785d5
+ * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com>
0785d5
  *
0785d5
  * Licensed under the GNU Lesser General Public License Version 2.1
0785d5
  *
0785d5
@@ -264,6 +265,22 @@ as_store_changed_uninhibit_cb (void *v)
0785d5
 
0785d5
 #define _cleanup_uninhibit_ __attribute__ ((cleanup(as_store_changed_uninhibit_cb)))
0785d5
 
0785d5
+static GPtrArray *
0785d5
+_dup_app_array (GPtrArray *array)
0785d5
+{
0785d5
+	GPtrArray *array_dup;
0785d5
+
0785d5
+	g_return_val_if_fail (array != NULL, NULL);
0785d5
+
0785d5
+	array_dup = g_ptr_array_new_full (array->len, (GDestroyNotify) g_object_unref);
0785d5
+	for (guint i = 0; i < array->len; i++) {
0785d5
+		AsApp *app = g_ptr_array_index (array, i);
0785d5
+		g_ptr_array_add (array_dup, g_object_ref (app));
0785d5
+	}
0785d5
+
0785d5
+	return array_dup;
0785d5
+}
0785d5
+
0785d5
 /**
0785d5
  * as_store_add_filter:
0785d5
  * @store: a #AsStore instance.
0785d5
@@ -344,6 +361,27 @@ as_store_get_apps (AsStore *store)
0785d5
 	return priv->array;
0785d5
 }
0785d5
 
0785d5
+/**
0785d5
+ * as_store_dup_apps:
0785d5
+ * @store: a #AsStore instance.
0785d5
+ *
0785d5
+ * Gets an array of all the valid applications in the store.
0785d5
+ *
0785d5
+ * Returns: (element-type AsApp) (transfer container): an array
0785d5
+ *
0785d5
+ * Since: 0.7.15
0785d5
+ **/
0785d5
+GPtrArray *
0785d5
+as_store_dup_apps (AsStore *store)
0785d5
+{
0785d5
+
0785d5
+	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+
0785d5
+	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
+
0785d5
+	return _dup_app_array (priv->array);
0785d5
+}
0785d5
+
0785d5
 /**
0785d5
  * as_store_remove_all:
0785d5
  * @store: a #AsStore instance.
0785d5
@@ -471,7 +509,7 @@ as_store_get_apps_by_id (AsStore *store, const gchar *id)
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 	apps = g_hash_table_lookup (priv->hash_id, id);
0785d5
 	if (apps != NULL)
0785d5
-		return g_ptr_array_ref (apps);
0785d5
+		return _dup_app_array (apps);
0785d5
 	return g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
 }
0785d5
 
0785d5
@@ -494,6 +532,31 @@ as_store_get_apps_by_id_merge (AsStore *store, const gchar *id)
0785d5
 	return g_hash_table_lookup (priv->hash_merge_id, id);
0785d5
 }
0785d5
 
0785d5
+/**
0785d5
+ * as_store_dup_apps_by_id_merge:
0785d5
+ * @store: a #AsStore instance.
0785d5
+ * @id: the application full ID.
0785d5
+ *
0785d5
+ * Gets an array of all the merge applications that match a specific ID.
0785d5
+ *
0785d5
+ * Returns: (element-type AsApp) (transfer container): an array
0785d5
+ *
0785d5
+ * Since: 0.7.15
0785d5
+ **/
0785d5
+GPtrArray *
0785d5
+as_store_dup_apps_by_id_merge (AsStore *store, const gchar *id)
0785d5
+{
0785d5
+	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	GPtrArray *apps;
0785d5
+
0785d5
+	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
+
0785d5
+	apps = g_hash_table_lookup (priv->hash_merge_id, id);
0785d5
+	if (apps != NULL)
0785d5
+		return _dup_app_array (apps);
0785d5
+	return g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
+}
0785d5
+
0785d5
 /**
0785d5
  * as_store_add_metadata_index:
0785d5
  * @store: a #AsStore instance.
0785d5
diff --git a/libappstream-glib/as-store.h b/libappstream-glib/as-store.h
0785d5
index 3d8c749..b15aca4 100644
0785d5
--- a/libappstream-glib/as-store.h
0785d5
+++ b/libappstream-glib/as-store.h
0785d5
@@ -209,10 +209,13 @@ void		 as_store_set_search_match	(AsStore	*store,
0785d5
 guint16		 as_store_get_search_match	(AsStore	*store);
0785d5
 void		 as_store_remove_all		(AsStore	*store);
0785d5
 GPtrArray	*as_store_get_apps		(AsStore	*store);
0785d5
+GPtrArray	*as_store_dup_apps		(AsStore	*store);
0785d5
 GPtrArray	*as_store_get_apps_by_id	(AsStore	*store,
0785d5
 						 const gchar	*id);
0785d5
 GPtrArray	*as_store_get_apps_by_id_merge	(AsStore	*store,
0785d5
 						 const gchar	*id);
0785d5
+GPtrArray	*as_store_dup_apps_by_id_merge	(AsStore	*store,
0785d5
+						 const gchar	*id);
0785d5
 GPtrArray	*as_store_get_apps_by_metadata	(AsStore	*store,
0785d5
 						 const gchar	*key,
0785d5
 						 const gchar	*value);
0785d5
-- 
0785d5
2.19.1
0785d5
0785d5
0785d5
From 0f79c943948abb62a54d0e54b1135ff89ebf5ab4 Mon Sep 17 00:00:00 2001
0785d5
From: Kalev Lember <klember@redhat.com>
0785d5
Date: Fri, 14 Dec 2018 11:52:19 +0100
0785d5
Subject: [PATCH 2/3] store: Return deep copy in as_store_get_apps_by_metadata
0785d5
0785d5
This is strictly not necessary for making gnome-software's AsStore use
0785d5
thread safe as gnome-software doesn't actually use this function, but
0785d5
let's fix it anyway while I'm at it.
0785d5
0785d5
This also updates tests that test the performance of the function as the
0785d5
deep copying makes it noticably slower (but that's fine because nothing
0785d5
actually uses it and it's still reasonably fast).
0785d5
---
0785d5
 libappstream-glib/as-self-test.c | 2 +-
0785d5
 libappstream-glib/as-store.c     | 2 +-
0785d5
 2 files changed, 2 insertions(+), 2 deletions(-)
0785d5
0785d5
diff --git a/libappstream-glib/as-self-test.c b/libappstream-glib/as-self-test.c
0785d5
index 7fd7d75..981b790 100644
0785d5
--- a/libappstream-glib/as-self-test.c
0785d5
+++ b/libappstream-glib/as-self-test.c
0785d5
@@ -4730,7 +4730,7 @@ static void
0785d5
 as_test_store_metadata_index_func (void)
0785d5
 {
0785d5
 	GPtrArray *apps;
0785d5
-	const guint repeats = 10000;
0785d5
+	const guint repeats = 500;
0785d5
 	guint i;
0785d5
 	g_autoptr(AsStore) store = NULL;
0785d5
 	g_autoptr(GTimer) timer = NULL;
0785d5
diff --git a/libappstream-glib/as-store.c b/libappstream-glib/as-store.c
0785d5
index 59a7f1c..018bdb5 100644
0785d5
--- a/libappstream-glib/as-store.c
0785d5
+++ b/libappstream-glib/as-store.c
0785d5
@@ -473,7 +473,7 @@ as_store_get_apps_by_metadata (AsStore *store,
0785d5
 		}
0785d5
 		apps = g_hash_table_lookup (index, value);
0785d5
 		if (apps != NULL)
0785d5
-			return g_ptr_array_ref (apps);
0785d5
+			return _dup_app_array (apps);
0785d5
 		return g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
 	}
0785d5
 
0785d5
-- 
0785d5
2.19.1
0785d5
0785d5
0785d5
From 24913fb9ac74f671fdba7547049bcaa0899704ee Mon Sep 17 00:00:00 2001
0785d5
From: Kalev Lember <klember@redhat.com>
0785d5
Date: Fri, 14 Dec 2018 12:03:37 +0100
0785d5
Subject: [PATCH 3/3] store: Add internal locking
0785d5
0785d5
This adds fine-grained locking around priv->array and various
0785d5
priv-stored hash table use. There are more thread safe issues in
0785d5
AsStore, but this should take care of those that cause frequent
0785d5
gnome-software crashes in F29.
0785d5
0785d5
This may regress the perfomance a bit because it changes a few places to
0785d5
keep a deep copy to simplify locking, but I haven't observed any visible
0785d5
performance regressions in gnome-software. Also, gnome-software is in
0785d5
the process of switching to libxmlb anyway so it shouldn't matter a
0785d5
whole lot in the long run.
0785d5
0785d5
This patch takes care to make sure that locking is fine grained enough
0785d5
so that we can be sure it doesn't lead into deadlocks, and also makes
0785d5
sure that we never invoke any callbacks (signals) while locked to
0785d5
prevent deadlocks when a client app calls back into AsStore code.
0785d5
---
0785d5
 libappstream-glib/as-store.c | 191 ++++++++++++++++++++++++++++-------
0785d5
 1 file changed, 155 insertions(+), 36 deletions(-)
0785d5
0785d5
diff --git a/libappstream-glib/as-store.c b/libappstream-glib/as-store.c
0785d5
index 018bdb5..0308585 100644
0785d5
--- a/libappstream-glib/as-store.c
0785d5
+++ b/libappstream-glib/as-store.c
0785d5
@@ -68,6 +68,7 @@ typedef struct
0785d5
 	GHashTable		*hash_merge_id;	/* of GPtrArray of AsApp{id} */
0785d5
 	GHashTable		*hash_unique_id;	/* of AsApp{unique_id} */
0785d5
 	GHashTable		*hash_pkgname;	/* of AsApp{pkgname} */
0785d5
+	GMutex			 mutex;
0785d5
 	AsMonitor		*monitor;
0785d5
 	GHashTable		*metadata_indexes;	/* GHashTable{key} */
0785d5
 	GHashTable		*appinfo_dirs;	/* GHashTable{path:AsStorePathData} */
0785d5
@@ -140,6 +141,7 @@ as_store_finalize (GObject *object)
0785d5
 	g_hash_table_unref (priv->metadata_indexes);
0785d5
 	g_hash_table_unref (priv->appinfo_dirs);
0785d5
 	g_hash_table_unref (priv->search_blacklist);
0785d5
+	g_mutex_clear (&priv->mutex);
0785d5
 
0785d5
 	G_OBJECT_CLASS (as_store_parent_class)->finalize (object);
0785d5
 }
0785d5
@@ -339,7 +341,11 @@ guint
0785d5
 as_store_get_size (AsStore *store)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), 0);
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	return priv->array->len;
0785d5
 }
0785d5
 
0785d5
@@ -357,7 +363,11 @@ GPtrArray *
0785d5
 as_store_get_apps (AsStore *store)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	return priv->array;
0785d5
 }
0785d5
 
0785d5
@@ -376,9 +386,11 @@ as_store_dup_apps (AsStore *store)
0785d5
 {
0785d5
 
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	return _dup_app_array (priv->array);
0785d5
 }
0785d5
 
0785d5
@@ -394,7 +406,11 @@ void
0785d5
 as_store_remove_all (AsStore *store)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
 	g_return_if_fail (AS_IS_STORE (store));
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	g_ptr_array_set_size (priv->array, 0);
0785d5
 	g_hash_table_remove_all (priv->hash_id);
0785d5
 	g_hash_table_remove_all (priv->hash_merge_id);
0785d5
@@ -461,9 +477,12 @@ as_store_get_apps_by_metadata (AsStore *store,
0785d5
 	GHashTable *index;
0785d5
 	GPtrArray *apps;
0785d5
 	guint i;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	/* do we have this indexed? */
0785d5
 	index = g_hash_table_lookup (priv->metadata_indexes, key);
0785d5
 	if (index != NULL) {
0785d5
@@ -506,7 +525,12 @@ as_store_get_apps_by_id (AsStore *store, const gchar *id)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	GPtrArray *apps;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	apps = g_hash_table_lookup (priv->hash_id, id);
0785d5
 	if (apps != NULL)
0785d5
 		return _dup_app_array (apps);
0785d5
@@ -528,7 +552,11 @@ GPtrArray *
0785d5
 as_store_get_apps_by_id_merge (AsStore *store, const gchar *id)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	return g_hash_table_lookup (priv->hash_merge_id, id);
0785d5
 }
0785d5
 
0785d5
@@ -548,9 +576,12 @@ as_store_dup_apps_by_id_merge (AsStore *store, const gchar *id)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	GPtrArray *apps;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	apps = g_hash_table_lookup (priv->hash_merge_id, id);
0785d5
 	if (apps != NULL)
0785d5
 		return _dup_app_array (apps);
0785d5
@@ -572,6 +603,12 @@ as_store_dup_apps_by_id_merge (AsStore *store, const gchar *id)
0785d5
 void
0785d5
 as_store_add_metadata_index (AsStore *store, const gchar *key)
0785d5
 {
0785d5
+	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
+	g_return_if_fail (AS_IS_STORE (store));
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	as_store_regen_metadata_index_key (store, key);
0785d5
 }
0785d5
 
0785d5
@@ -594,7 +631,11 @@ as_store_get_app_by_id (AsStore *store, const gchar *id)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	GPtrArray *apps;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
+
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
+
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
 	apps = g_hash_table_lookup (priv->hash_id, id);
0785d5
 	if (apps == NULL)
0785d5
 		return NULL;
0785d5
@@ -641,6 +682,7 @@ as_store_get_app_by_app (AsStore *store, AsApp *app)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	guint i;
0785d5
+	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
0785d5
 
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
 		AsApp *app_tmp = g_ptr_array_index (priv->array, i);
0785d5
@@ -675,8 +717,10 @@ as_store_get_app_by_unique_id (AsStore *store,
0785d5
 	g_return_val_if_fail (unique_id != NULL, NULL);
0785d5
 
0785d5
 	/* no globs */
0785d5
-	if ((search_flags & AS_STORE_SEARCH_FLAG_USE_WILDCARDS) == 0)
0785d5
+	if ((search_flags & AS_STORE_SEARCH_FLAG_USE_WILDCARDS) == 0) {
0785d5
+		g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
0785d5
 		return g_hash_table_lookup (priv->hash_unique_id, unique_id);
0785d5
+	}
0785d5
 
0785d5
 	/* create virtual app using scope/system/origin/kind/id/branch */
0785d5
 	app_tmp = _as_app_new_from_unique_id (unique_id);
0785d5
@@ -706,11 +750,14 @@ as_store_get_app_by_provide (AsStore *store, AsProvideKind kind, const gchar *va
0785d5
 	guint i;
0785d5
 	guint j;
0785d5
 	GPtrArray *provides;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 	g_return_val_if_fail (kind != AS_PROVIDE_KIND_UNKNOWN, NULL);
0785d5
 	g_return_val_if_fail (value != NULL, NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	/* find an application that provides something */
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
 		app = g_ptr_array_index (priv->array, i);
0785d5
@@ -744,11 +791,14 @@ AsApp *
0785d5
 as_store_get_app_by_launchable (AsStore *store, AsLaunchableKind kind, const gchar *value)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 	g_return_val_if_fail (kind != AS_LAUNCHABLE_KIND_UNKNOWN, NULL);
0785d5
 	g_return_val_if_fail (value != NULL, NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	for (guint i = 0; i < priv->array->len; i++) {
0785d5
 		AsApp *app = g_ptr_array_index (priv->array, i);
0785d5
 		GPtrArray *launchables = as_app_get_launchables (app);
0785d5
@@ -782,11 +832,14 @@ as_store_get_apps_by_provide (AsStore *store, AsProvideKind kind, const gchar *v
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	GPtrArray *apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 	g_return_val_if_fail (kind != AS_PROVIDE_KIND_UNKNOWN, NULL);
0785d5
 	g_return_val_if_fail (value != NULL, NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	/* find an application that provides something */
0785d5
 	for (guint i = 0; i < priv->array->len; i++) {
0785d5
 		AsApp *app = g_ptr_array_index (priv->array, i);
0785d5
@@ -820,10 +873,13 @@ as_store_get_app_by_id_ignore_prefix (AsStore *store, const gchar *id)
0785d5
 	AsApp *app;
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	guint i;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 	g_return_val_if_fail (id != NULL, NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	/* find an application that provides something */
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
 		app = g_ptr_array_index (priv->array, i);
0785d5
@@ -1017,9 +1073,12 @@ as_store_get_app_by_pkgname (AsStore *store, const gchar *pkgname)
0785d5
 	AsApp *app;
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	guint i;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	/* in most cases, we can use the cache */
0785d5
 	app = g_hash_table_lookup (priv->hash_pkgname, pkgname);
0785d5
 	if (app != NULL)
0785d5
@@ -1059,6 +1118,7 @@ as_store_get_app_by_pkgnames (AsStore *store, gchar **pkgnames)
0785d5
 	g_return_val_if_fail (pkgnames != NULL, NULL);
0785d5
 
0785d5
 	for (i = 0; pkgnames[i] != NULL; i++) {
0785d5
+		g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
0785d5
 		app = g_hash_table_lookup (priv->hash_pkgname, pkgnames[i]);
0785d5
 		if (app != NULL)
0785d5
 			return app;
0785d5
@@ -1087,6 +1147,7 @@ as_store_remove_app (AsStore *store, AsApp *app)
0785d5
 	g_signal_emit (store, signals[SIGNAL_APP_REMOVED], 0, app);
0785d5
 
0785d5
 	/* only remove this specific unique app */
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	apps = g_hash_table_lookup (priv->hash_id, as_app_get_id (app));
0785d5
 	if (apps != NULL) {
0785d5
 		g_ptr_array_remove (apps, app);
0785d5
@@ -1100,6 +1161,7 @@ as_store_remove_app (AsStore *store, AsApp *app)
0785d5
 	g_hash_table_remove (priv->hash_unique_id, as_app_get_unique_id (app));
0785d5
 	g_ptr_array_remove (priv->array, app);
0785d5
 	g_hash_table_remove_all (priv->metadata_indexes);
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 
0785d5
 	/* removed */
0785d5
 	as_store_perhaps_emit_changed (store, "remove-app");
0785d5
@@ -1117,27 +1179,37 @@ as_store_remove_app (AsStore *store, AsApp *app)
0785d5
 void
0785d5
 as_store_remove_app_by_id (AsStore *store, const gchar *id)
0785d5
 {
0785d5
-	AsApp *app;
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
-	guint i;
0785d5
+	g_autoptr(GPtrArray) apps = NULL;
0785d5
 
0785d5
 	g_return_if_fail (AS_IS_STORE (store));
0785d5
 
0785d5
-	if (!g_hash_table_remove (priv->hash_id, id))
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
+	if (!g_hash_table_remove (priv->hash_id, id)) {
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
 		return;
0785d5
-	for (i = 0; i < priv->array->len; i++) {
0785d5
-		app = g_ptr_array_index (priv->array, i);
0785d5
+	}
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
+
0785d5
+	apps = as_store_dup_apps (store);
0785d5
+	for (guint i = 0; i < apps->len; i++) {
0785d5
+		AsApp *app = g_ptr_array_index (apps, i);
0785d5
+
0785d5
 		if (g_strcmp0 (id, as_app_get_id (app)) != 0)
0785d5
 			continue;
0785d5
 
0785d5
 		/* emit before removal */
0785d5
 		g_signal_emit (store, signals[SIGNAL_APP_REMOVED], 0, app);
0785d5
 
0785d5
+		g_mutex_lock (&priv->mutex);
0785d5
 		g_ptr_array_remove (priv->array, app);
0785d5
 		g_hash_table_remove (priv->hash_unique_id,
0785d5
 				     as_app_get_unique_id (app));
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
 	}
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	g_hash_table_remove_all (priv->metadata_indexes);
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 
0785d5
 	/* removed */
0785d5
 	as_store_perhaps_emit_changed (store, "remove-app-by-id");
0785d5
@@ -1242,7 +1314,9 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 	if (as_app_has_quirk (app, AS_APP_QUIRK_MATCH_ANY_PREFIX)) {
0785d5
 		guint64 flags = AS_APP_SUBSUME_FLAG_MERGE;
0785d5
 		AsAppMergeKind merge_kind = as_app_get_merge_kind (app);
0785d5
+		g_autoptr(GPtrArray) apps_changed = NULL;
0785d5
 
0785d5
+		g_mutex_lock (&priv->mutex);
0785d5
 		apps = g_hash_table_lookup (priv->hash_merge_id, id);
0785d5
 		if (apps == NULL) {
0785d5
 			apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
@@ -1254,11 +1328,16 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 			 as_app_merge_kind_to_string (merge_kind),
0785d5
 			 as_app_get_unique_id (app));
0785d5
 		g_ptr_array_add (apps, g_object_ref (app));
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
 
0785d5
 		/* apply to existing components */
0785d5
 		flags |= AS_APP_SUBSUME_FLAG_NO_OVERWRITE;
0785d5
 		if (merge_kind == AS_APP_MERGE_KIND_REPLACE)
0785d5
 			flags |= AS_APP_SUBSUME_FLAG_REPLACE;
0785d5
+
0785d5
+		apps_changed = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
+
0785d5
+		g_mutex_lock (&priv->mutex);
0785d5
 		for (i = 0; i < priv->array->len; i++) {
0785d5
 			AsApp *app_tmp = g_ptr_array_index (priv->array, i);
0785d5
 			if (g_strcmp0 (as_app_get_id (app_tmp), id) != 0)
0785d5
@@ -1267,7 +1346,11 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 				 as_app_merge_kind_to_string (merge_kind),
0785d5
 				 id, as_app_get_unique_id (app_tmp));
0785d5
 			as_app_subsume_full (app_tmp, app, flags);
0785d5
-
0785d5
+			g_ptr_array_add (apps_changed, g_object_ref (app_tmp));
0785d5
+		}
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
+		for (i = 0; i < apps_changed->len; i++) {
0785d5
+			AsApp *app_tmp = g_ptr_array_index (apps_changed, i);
0785d5
 			/* emit after changes have been made */
0785d5
 			g_signal_emit (store, signals[SIGNAL_APP_CHANGED],
0785d5
 				       0, app_tmp);
0785d5
@@ -1276,6 +1359,7 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 	}
0785d5
 
0785d5
 	/* is there any merge components to add to this app */
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	apps = g_hash_table_lookup (priv->hash_merge_id, id);
0785d5
 	if (apps != NULL) {
0785d5
 		for (i = 0; i < apps->len; i++) {
0785d5
@@ -1292,14 +1376,17 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 			as_app_subsume_full (app, app_tmp, flags);
0785d5
 		}
0785d5
 	}
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 
0785d5
 	/* find the item */
0785d5
 	if (priv->add_flags & AS_STORE_ADD_FLAG_USE_UNIQUE_ID) {
0785d5
 		item = as_store_get_app_by_app (store, app);
0785d5
 	} else {
0785d5
+		g_mutex_lock (&priv->mutex);
0785d5
 		apps = g_hash_table_lookup (priv->hash_id, id);
0785d5
 		if (apps != NULL && apps->len > 0)
0785d5
 			item = g_ptr_array_index (apps, 0);
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
 	}
0785d5
 	if (item != NULL) {
0785d5
 		AsFormat *app_format = as_app_get_format_default (app);
0785d5
@@ -1427,6 +1514,7 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 	}
0785d5
 
0785d5
 	/* create hash of id:[apps] if required */
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	apps = g_hash_table_lookup (priv->hash_id, id);
0785d5
 	if (apps == NULL) {
0785d5
 		apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
@@ -1448,6 +1536,7 @@ as_store_add_app (AsStore *store, AsApp *app)
0785d5
 				     g_strdup (pkgname),
0785d5
 				     g_object_ref (app));
0785d5
 	}
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 
0785d5
 	/* add helper objects */
0785d5
 	as_app_set_stemmer (app, priv->stemmer);
0785d5
@@ -1495,15 +1584,16 @@ as_store_match_addons (AsStore *store)
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	guint i;
0785d5
 	g_autoptr(AsProfileTask) ptask = NULL;
0785d5
+	g_autoptr(GPtrArray) apps = NULL;
0785d5
 
0785d5
 	/* profile */
0785d5
 	ptask = as_profile_start_literal (priv->profile, "AsStore:match-addons");
0785d5
 	g_assert (ptask != NULL);
0785d5
-	for (i = 0; i < priv->array->len; i++) {
0785d5
-		AsApp *app = g_ptr_array_index (priv->array, i);
0785d5
-		if (as_app_get_kind (app) != AS_APP_KIND_ADDON)
0785d5
-			continue;
0785d5
-		as_store_match_addons_app (store, app);
0785d5
+	apps = as_store_dup_apps (store);
0785d5
+	for (i = 0; i < apps->len; i++) {
0785d5
+		AsApp *app = g_ptr_array_index (apps, i);
0785d5
+		if (as_app_get_kind (app) == AS_APP_KIND_ADDON)
0785d5
+			as_store_match_addons_app (store, app);
0785d5
 	}
0785d5
 }
0785d5
 
0785d5
@@ -1858,15 +1948,15 @@ static void
0785d5
 as_store_remove_by_source_file (AsStore *store, const gchar *filename)
0785d5
 {
0785d5
 	AsApp *app;
0785d5
-	GPtrArray *apps;
0785d5
 	guint i;
0785d5
 	const gchar *tmp;
0785d5
 	_cleanup_uninhibit_ guint32 *tok = NULL;
0785d5
+	g_autoptr(GPtrArray) apps = NULL;
0785d5
 	g_autoptr(GPtrArray) ids = NULL;
0785d5
 
0785d5
 	/* find any applications in the store with this source file */
0785d5
 	ids = g_ptr_array_new_with_free_func (g_free);
0785d5
-	apps = as_store_get_apps (store);
0785d5
+	apps = as_store_dup_apps (store);
0785d5
 	for (i = 0; i < apps->len; i++) {
0785d5
 		AsFormat *format;
0785d5
 		app = g_ptr_array_index (apps, i);
0785d5
@@ -1909,16 +1999,21 @@ as_store_watch_source_added (AsStore *store, const gchar *filename)
0785d5
 	if (!g_file_test (filename, G_FILE_TEST_IS_REGULAR))
0785d5
 		return;
0785d5
 
0785d5
-	/* we helpfully saved this */
0785d5
 	dirname = g_path_get_dirname (filename);
0785d5
 	g_debug ("parsing new file %s from %s", filename, dirname);
0785d5
+
0785d5
+	/* we helpfully saved this */
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	path_data = g_hash_table_lookup (priv->appinfo_dirs, filename);
0785d5
 	if (path_data == NULL)
0785d5
 		path_data = g_hash_table_lookup (priv->appinfo_dirs, dirname);
0785d5
 	if (path_data == NULL) {
0785d5
 		g_warning ("no path data for %s", dirname);
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
 		return;
0785d5
 	}
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
+
0785d5
 	file = g_file_new_for_path (filename);
0785d5
 	/* Do not watch the file for changes: we're already watching its
0785d5
 	 * parent directory */
0785d5
@@ -2010,7 +2105,9 @@ as_store_add_path_data (AsStore *store,
0785d5
 	}
0785d5
 
0785d5
 	/* check not already exists */
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	path_data = g_hash_table_lookup (priv->appinfo_dirs, path);
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 	if (path_data != NULL) {
0785d5
 		if (path_data->scope != scope ||
0785d5
 		    g_strcmp0 (path_data->arch, arch) != 0) {
0785d5
@@ -2033,7 +2130,9 @@ as_store_add_path_data (AsStore *store,
0785d5
 	path_data = g_slice_new0 (AsStorePathData);
0785d5
 	path_data->scope = scope;
0785d5
 	path_data->arch = g_strdup (arch);
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	g_hash_table_insert (priv->appinfo_dirs, g_strdup (path), path_data);
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 }
0785d5
 
0785d5
 static gboolean
0785d5
@@ -2287,6 +2386,7 @@ as_store_check_apps_for_veto (AsStore *store)
0785d5
 	guint i;
0785d5
 	AsApp *app;
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
0785d5
 
0785d5
 	/* add any vetos */
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
@@ -2306,27 +2406,28 @@ as_store_check_apps_for_veto (AsStore *store)
0785d5
 void
0785d5
 as_store_remove_apps_with_veto (AsStore *store)
0785d5
 {
0785d5
-	guint i;
0785d5
-	AsApp *app;
0785d5
-	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	_cleanup_uninhibit_ guint32 *tok = NULL;
0785d5
+	g_autoptr(GPtrArray) apps = NULL;
0785d5
+	g_autoptr(GPtrArray) apps_remove = NULL;
0785d5
 
0785d5
 	g_return_if_fail (AS_IS_STORE (store));
0785d5
 
0785d5
 	/* don't shortcut the list as we have to use as_store_remove_app()
0785d5
 	 * rather than just removing from the GPtrArray */
0785d5
 	tok = as_store_changed_inhibit (store);
0785d5
-	do {
0785d5
-		for (i = 0; i < priv->array->len; i++) {
0785d5
-			app = g_ptr_array_index (priv->array, i);
0785d5
-			if (as_app_get_vetos (app)->len > 0) {
0785d5
-				g_debug ("removing %s as vetoed",
0785d5
-					 as_app_get_id (app));
0785d5
-				as_store_remove_app (store, app);
0785d5
-				break;
0785d5
-			}
0785d5
-		}
0785d5
-	} while (i < priv->array->len);
0785d5
+	apps = as_store_dup_apps (store);
0785d5
+	apps_remove = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
0785d5
+	for (guint i = 0; i < apps->len; i++) {
0785d5
+		AsApp *app = g_ptr_array_index (apps, i);
0785d5
+		if (as_app_get_vetos (app)->len > 0)
0785d5
+			g_ptr_array_add (apps_remove, g_object_ref (app));
0785d5
+	}
0785d5
+	for (guint i = 0; i < apps_remove->len; i++) {
0785d5
+		AsApp *app = g_ptr_array_index (apps_remove, i);
0785d5
+		g_debug ("removing %s as vetoed",
0785d5
+		         as_app_get_id (app));
0785d5
+		as_store_remove_app (store, app);
0785d5
+	}
0785d5
 	as_store_changed_uninhibit (&tok;;
0785d5
 	as_store_perhaps_emit_changed (store, "remove-apps-with-veto");
0785d5
 }
0785d5
@@ -2379,22 +2480,28 @@ as_store_to_xml (AsStore *store, guint32 flags)
0785d5
 		as_node_add_attribute (node_apps, "version", version);
0785d5
 	}
0785d5
 
0785d5
-	/* sort by ID */
0785d5
-	g_ptr_array_sort (priv->array, as_store_apps_sort_cb);
0785d5
-
0785d5
 	/* output is trusted, so include update_contact */
0785d5
 	if (g_getenv ("APPSTREAM_GLIB_OUTPUT_TRUSTED") != NULL)
0785d5
 		output_trusted = TRUE;
0785d5
 
0785d5
-	/* add applications */
0785d5
 	ctx = as_node_context_new ();
0785d5
 	as_node_context_set_version (ctx, priv->api_version);
0785d5
 	as_node_context_set_output (ctx, AS_FORMAT_KIND_APPSTREAM);
0785d5
 	as_node_context_set_output_trusted (ctx, output_trusted);
0785d5
+
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
+
0785d5
+	/* sort by ID */
0785d5
+	g_ptr_array_sort (priv->array, as_store_apps_sort_cb);
0785d5
+
0785d5
+	/* add applications */
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
 		app = g_ptr_array_index (priv->array, i);
0785d5
 		as_app_node_insert (app, node_apps, ctx);
0785d5
 	}
0785d5
+
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
+
0785d5
 	xml = as_node_to_xml (node_root, flags);
0785d5
 	as_node_unref (node_root);
0785d5
 	return xml;
0785d5
@@ -2418,9 +2525,12 @@ as_store_convert_icons (AsStore *store, AsIconKind kind, GError **error)
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
 	AsApp *app;
0785d5
 	guint i;
0785d5
+	g_autoptr(GMutexLocker) locker = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), FALSE);
0785d5
 
0785d5
+	locker = g_mutex_locker_new (&priv->mutex);
0785d5
+
0785d5
 	/* convert application icons */
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
 		app = g_ptr_array_index (priv->array, i);
0785d5
@@ -2818,8 +2928,12 @@ as_store_load_app_info (AsStore *store,
0785d5
 	_cleanup_uninhibit_ guint32 *tok = NULL;
0785d5
 
0785d5
 	/* Don't add the same dir twice, we're monitoring it for changes anyway */
0785d5
-	if (g_hash_table_contains (priv->appinfo_dirs, path))
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
+	if (g_hash_table_contains (priv->appinfo_dirs, path)) {
0785d5
+		g_mutex_unlock (&priv->mutex);
0785d5
 		return TRUE;
0785d5
+	}
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 
0785d5
 	/* emit once when finished */
0785d5
 	tok = as_store_changed_inhibit (store);
0785d5
@@ -3329,10 +3443,12 @@ as_store_load_search_cache (AsStore *store)
0785d5
 	pool = g_thread_pool_new (as_store_load_search_cache_cb,
0785d5
 				  store, 4, TRUE, NULL);
0785d5
 	g_assert (pool != NULL);
0785d5
+	g_mutex_lock (&priv->mutex);
0785d5
 	for (i = 0; i < priv->array->len; i++) {
0785d5
 		AsApp *app = g_ptr_array_index (priv->array, i);
0785d5
 		g_thread_pool_push (pool, app, NULL);
0785d5
 	}
0785d5
+	g_mutex_unlock (&priv->mutex);
0785d5
 	g_thread_pool_free (pool, FALSE, TRUE);
0785d5
 }
0785d5
 
0785d5
@@ -3510,6 +3626,7 @@ as_store_validate (AsStore *store, guint32 flags, GError **error)
0785d5
 	GPtrArray *probs;
0785d5
 	guint i;
0785d5
 	g_autoptr(GHashTable) hash_names = NULL;
0785d5
+	g_autoptr(GPtrArray) apps = NULL;
0785d5
 
0785d5
 	g_return_val_if_fail (AS_IS_STORE (store), NULL);
0785d5
 
0785d5
@@ -3546,14 +3663,15 @@ as_store_validate (AsStore *store, guint32 flags, GError **error)
0785d5
 					    g_free, (GDestroyNotify) g_object_unref);
0785d5
 
0785d5
 	/* check each application */
0785d5
-	for (i = 0; i < priv->array->len; i++) {
0785d5
+	apps = as_store_dup_apps (store);
0785d5
+	for (i = 0; i < apps->len; i++) {
0785d5
 		AsApp *app_tmp;
0785d5
 		AsProblem *prob;
0785d5
 		guint j;
0785d5
 		g_autofree gchar *app_key = NULL;
0785d5
 		g_autoptr(GPtrArray) probs_app = NULL;
0785d5
 
0785d5
-		app = g_ptr_array_index (priv->array, i);
0785d5
+		app = g_ptr_array_index (apps, i);
0785d5
 		if (priv->api_version < 0.3) {
0785d5
 			if (as_app_get_source_pkgname (app) != NULL) {
0785d5
 				as_store_validate_add (probs,
0785d5
@@ -3804,6 +3922,7 @@ static void
0785d5
 as_store_init (AsStore *store)
0785d5
 {
0785d5
 	AsStorePrivate *priv = GET_PRIVATE (store);
0785d5
+	g_mutex_init (&priv->mutex);
0785d5
 	priv->profile = as_profile_new ();
0785d5
 	priv->stemmer = as_stemmer_new ();
0785d5
 	priv->api_version = AS_API_VERSION_NEWEST;
0785d5
-- 
0785d5
2.19.1
0785d5