[gnome-software: 3/5] gs-external-appstream-utils: Make refresh asynchronous




commit f615620bd034f4584a28cdaee47fb625bbe2ca07
Author: Philip Withnall <pwithnall endlessos org>
Date:   Mon Feb 28 12:12:19 2022 +0000

    gs-external-appstream-utils: Make refresh asynchronous
    
    This reworks the external-appstream utils to be asynchronous, allowing
    all external appstream files to be downloaded in parallel, and
    downloaded without blocking the calling thread.
    
    Signed-off-by: Philip Withnall <pwithnall endlessos org>
    
    Helps: #1472

 lib/gs-external-appstream-utils.c | 414 ++++++++++++++++++++++++++++++++------
 lib/gs-external-appstream-utils.h |  12 +-
 lib/gs-plugin-loader.c            |  44 ++--
 3 files changed, 394 insertions(+), 76 deletions(-)
---
diff --git a/lib/gs-external-appstream-utils.c b/lib/gs-external-appstream-utils.c
index c3f0a83d4..3914618d2 100644
--- a/lib/gs-external-appstream-utils.c
+++ b/lib/gs-external-appstream-utils.c
@@ -8,6 +8,46 @@
  * SPDX-License-Identifier: GPL-2.0+
  */
 
+/*
+ * SECTION:gs-external-appstream-urls
+ * @short_description: Provides support for downloading external AppStream files.
+ *
+ * This downloads the set of configured external AppStream files, and caches
+ * them locally.
+ *
+ * According to the `external-appstream-system-wide` GSetting, the files will
+ * either be downloaded to a per-user cache, or to a system-wide cache. In the
+ * case of a system-wide cache, they are downloaded to a temporary file writable
+ * by the user, and then the suexec binary `gnome-software-install-appstream` is
+ * run to copy them to the system location.
+ *
+ * All the downloads are done in the default #GMainContext for the thread which
+ * calls gs_external_appstream_refresh_async(). They are done in parallel and
+ * the async refresh function will only complete once the last download is
+ * complete.
+ *
+ * Progress data is reported via a callback, and gives the total progress of all
+ * parallel downloads. Internally this is done by updating #ProgressTuple
+ * structs as each download progresses. A periodic timeout callback sums these
+ * and reports the total progress to the caller. That means that progress
+ * reports from gs_external_appstream_refresh_async() are done at a constant
+ * frequency.
+ *
+ * To test this code locally you will probably want to change your GSettings
+ * configuration to add some external AppStream URIs:
+ * ```
+ * gsettings set org.gnome.software external-appstream-urls '["https://example.com/appdata.xml.gz";]'
+ * ```
+ *
+ * When you are done with development, run the following command to use the real
+ * external AppStream list again:
+ * ```
+ * gsettings reset org.gnome.software external-appstream-urls
+ * ```
+ *
+ * Since: 42
+ */
+
 #include <glib.h>
 #include <glib/gi18n.h>
 #include <libsoup/soup.h>
@@ -57,15 +97,48 @@ gs_external_appstream_install (const gchar   *appstream_file,
        return g_subprocess_wait_check (subprocess, cancellable, error);
 }
 
-static gboolean
-gs_external_appstream_refresh_url (GsPlugin      *plugin,
-                                   GSettings     *settings,
-                                   const gchar   *url,
-                                   SoupSession   *soup_session,
-                                   guint64        cache_age_secs,
-                                   GCancellable  *cancellable,
-                                   GError       **error)
+static void download_system_cb (GObject      *source_object,
+                                GAsyncResult *result,
+                                gpointer      user_data);
+static void download_user_cb (GObject      *source_object,
+                              GAsyncResult *result,
+                              gpointer      user_data);
+
+/* A tuple to store the last-received progress data for a single download.
+ * Each download (refresh_url_async()) has a pointer to the relevant
+ * #ProgressTuple for its download. These are stored in an array in #RefreshData
+ * and a timeout callback periodically sums them all and reports progress to the
+ * caller. */
+typedef struct {
+       gsize bytes_downloaded;
+       gsize total_download_size;
+} ProgressTuple;
+
+static void
+refresh_url_progress_cb (gsize    bytes_downloaded,
+                         gsize    total_download_size,
+                         gpointer user_data)
 {
+       ProgressTuple *tuple = user_data;
+
+       tuple->bytes_downloaded = bytes_downloaded;
+       tuple->total_download_size = total_download_size;
+
+       /* The timeout callback in progress_cb() periodically sums these. No
+        * need to notify of progress from here. */
+}
+
+static void
+refresh_url_async (GSettings           *settings,
+                   const gchar         *url,
+                   SoupSession         *soup_session,
+                   guint64              cache_age_secs,
+                   ProgressTuple       *progress_tuple,
+                   GCancellable        *cancellable,
+                   GAsyncReadyCallback  callback,
+                   gpointer             user_data)
+{
+       g_autoptr(GTask) task = NULL;
        g_autofree gchar *basename = NULL;
        g_autofree gchar *basename_url = g_path_get_basename (url);
        /* make sure different uris with same basenames differ */
@@ -73,17 +146,21 @@ gs_external_appstream_refresh_url (GsPlugin      *plugin,
        g_autofree gchar *target_file_path = NULL;
        g_autoptr(GFile) target_file = NULL;
        g_autoptr(GFile) tmp_file = NULL;
-       g_autoptr(GsApp) app_dl = gs_app_new (gs_plugin_get_name (plugin));
+       g_autoptr(GsApp) app_dl = gs_app_new ("external-appstream");
        g_autoptr(GError) local_error = NULL;
-
        gboolean system_wide;
 
+       task = g_task_new (NULL, cancellable, callback, user_data);
+       g_task_set_source_tag (task, refresh_url_async);
+
        /* Calculate the basename of the target file. */
        hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, url, -1);
        if (hash == NULL) {
-               g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
-                            "Failed to hash url %s", url);
-               return FALSE;
+               g_task_return_new_error (task,
+                                        GS_PLUGIN_ERROR,
+                                        GS_PLUGIN_ERROR_FAILED,
+                                        "Failed to hash url %s", url);
+               return;
        }
        basename = g_strdup_printf ("%s-%s", hash, basename_url);
 
@@ -106,7 +183,8 @@ gs_external_appstream_refresh_url (GsPlugin      *plugin,
                g_debug ("skipping updating external appstream file %s: "
                         "cache age is older than file",
                         target_file_path);
-               return TRUE;
+               g_task_return_boolean (task, TRUE);
+               return;
        }
 
        /* If downloading system wide, write the download contents into a
@@ -118,92 +196,308 @@ gs_external_appstream_refresh_url (GsPlugin      *plugin,
                                                             basename,
                                                             GS_UTILS_CACHE_FLAG_WRITEABLE |
                                                             GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
-                                                            error);
-               if (tmp_file_path == NULL)
-                       return FALSE;
+                                                            &local_error);
+               if (tmp_file_path == NULL) {
+                       g_task_return_error (task, g_steal_pointer (&local_error));
+                       return;
+               }
 
                tmp_file = g_file_new_for_path (tmp_file_path);
        } else {
                tmp_file = g_object_ref (target_file);
        }
 
+       g_task_set_task_data (task, g_object_ref (tmp_file), g_object_unref);
+
        gs_app_set_summary_missing (app_dl,
                                    /* TRANSLATORS: status text when downloading */
                                    _("Downloading extra metadata files…"));
 
        /* Do the download. */
-       if (!gs_plugin_download_file (plugin, app_dl, url, g_file_peek_path (tmp_file),
-                                     cancellable, error)) {
-               g_set_error_literal (error,
-                                    GS_PLUGIN_ERROR,
-                                    GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
-                                    local_error->message);
-               return FALSE;
+       gs_download_file_async (soup_session, url, tmp_file, G_PRIORITY_LOW,
+                               refresh_url_progress_cb,
+                               progress_tuple,
+                               cancellable,
+                               system_wide ? download_system_cb : download_user_cb,
+                               g_steal_pointer (&task));
+}
+
+static void
+download_system_cb (GObject      *source_object,
+                    GAsyncResult *result,
+                    gpointer      user_data)
+{
+       SoupSession *soup_session = SOUP_SESSION (source_object);
+       g_autoptr(GTask) task = g_steal_pointer (&user_data);
+       GCancellable *cancellable = g_task_get_cancellable (task);
+       GFile *tmp_file = g_task_get_task_data (task);
+       g_autoptr(GError) local_error = NULL;
+
+       if (!gs_download_file_finish (soup_session, result, &local_error)) {
+               g_task_return_new_error (task,
+                                        GS_PLUGIN_ERROR,
+                                        GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+                                        "%s", local_error->message);
+               return;
        }
 
        g_debug ("Downloaded appstream file %s", g_file_peek_path (tmp_file));
 
-       if (system_wide) {
-               /* install file systemwide */
-               if (gs_external_appstream_install (g_file_peek_path (tmp_file),
-                                                  cancellable,
-                                                  error)) {
-                       g_debug ("Installed appstream file %s", g_file_peek_path (tmp_file));
-                       return TRUE;
-               } else {
-                       return FALSE;
-               }
+       /* install file systemwide */
+       if (gs_external_appstream_install (g_file_peek_path (tmp_file),
+                                          cancellable,
+                                          &local_error)) {
+               g_debug ("Installed appstream file %s", g_file_peek_path (tmp_file));
+               g_task_return_boolean (task, TRUE);
+       } else {
+               g_task_return_error (task, g_steal_pointer (&local_error));
        }
+}
 
-       return TRUE;
+static void
+download_user_cb (GObject      *source_object,
+                  GAsyncResult *result,
+                  gpointer      user_data)
+{
+       SoupSession *soup_session = SOUP_SESSION (source_object);
+       g_autoptr(GTask) task = g_steal_pointer (&user_data);
+       GFile *tmp_file = g_task_get_task_data (task);
+       g_autoptr(GError) local_error = NULL;
+
+       if (!gs_download_file_finish (soup_session, result, &local_error)) {
+               g_task_return_new_error (task,
+                                        GS_PLUGIN_ERROR,
+                                        GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+                                        "%s", local_error->message);
+               return;
+       }
+
+       g_debug ("Downloaded appstream file %s", g_file_peek_path (tmp_file));
+
+       g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+refresh_url_finish (GAsyncResult  *result,
+                    GError       **error)
+{
+       return g_task_propagate_boolean (G_TASK (result), error);
 }
 
+static void refresh_cb (GObject      *source_object,
+                        GAsyncResult *result,
+                        gpointer      user_data);
+static gboolean progress_cb (gpointer user_data);
+static void finish_refresh_op (GTask  *task,
+                               GError *error);
+
+typedef struct {
+       /* Input data. */
+       guint64 cache_age_secs;
+
+       /* In-progress data. */
+       guint n_pending_ops;
+       GError *error;  /* (nullable) (owned) */
+       gsize n_appstream_urls;
+       GsDownloadProgressCallback progress_callback;  /* (nullable) */
+       gpointer progress_user_data;  /* (closure progress_callback) */
+       ProgressTuple *progress_tuples;  /* (array length=n_appstream_urls) (owned) */
+       GSource *progress_source;  /* (owned) */
+} RefreshData;
+
+static void
+refresh_data_free (RefreshData *data)
+{
+       g_assert (data->n_pending_ops == 0);
+
+       /* If this was set it should have been stolen for g_task_return_error()
+        * by now. */
+       g_assert (data->error == NULL);
+
+       /* Similarly, progress reporting should have been stopped by now. */
+       g_assert (g_source_is_destroyed (data->progress_source));
+       g_source_unref (data->progress_source);
+
+       g_free (data->progress_tuples);
+
+       g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefreshData, refresh_data_free)
+
 /**
- * gs_external_appstream_refresh:
- * @plugin: the #GsPlugin calling this refresh operation
- * @cache_age_secs: as passed to gs_plugin_refresh()
+ * gs_external_appstream_refresh_async:
+ * @cache_age_secs: cache age, in seconds, as passed to gs_plugin_refresh()
+ * @progress_callback: (nullable): callback to call with progress information
+ * @progress_user_data: (nullable) (closure progress_callback): data to pass
+ *   to @progress_callback
  * @cancellable: (nullable): a #GCancellable, or %NULL
- * @error: return location for a #GError
+ * @callback: function call when the asynchronous operation is complete
+ * @user_data: data to pass to @callback
  *
  * Refresh any configured external appstream files, if the cache is too old.
- * This is intended to be called from a gs_plugin_refresh() function.
  *
- * Returns: %TRUE on success, %FALSE otherwise
- * Since: 41
+ * Since: 42
  */
-gboolean
-gs_external_appstream_refresh (GsPlugin      *plugin,
-                               guint64        cache_age_secs,
-                               GCancellable  *cancellable,
-                               GError       **error)
+void
+gs_external_appstream_refresh_async (guint64                     cache_age_secs,
+                                     GsDownloadProgressCallback  progress_callback,
+                                     gpointer                    progress_user_data,
+                                     GCancellable               *cancellable,
+                                     GAsyncReadyCallback         callback,
+                                     gpointer                    user_data)
 {
        g_autoptr(GSettings) settings = NULL;
        g_auto(GStrv) appstream_urls = NULL;
+       gsize n_appstream_urls;
        g_autoptr(SoupSession) soup_session = NULL;
+       g_autoptr(GTask) task = NULL;
+       RefreshData *data;
+       g_autoptr(RefreshData) data_owned = NULL;
+
+       /* Chosen to allow a few UI updates per second without updating the
+        * progress label so often it’s unreadable. */
+       const guint progress_update_period_ms = 300;
+
+       task = g_task_new (NULL, cancellable, callback, user_data);
+       g_task_set_source_tag (task, gs_external_appstream_refresh_async);
 
        settings = g_settings_new ("org.gnome.software");
        soup_session = gs_build_soup_session ();
        appstream_urls = g_settings_get_strv (settings,
                                              "external-appstream-urls");
-       for (guint i = 0; appstream_urls[i] != NULL; ++i) {
-               g_autoptr(GError) error_local = NULL;
+       n_appstream_urls = g_strv_length (appstream_urls);
+
+       data = data_owned = g_new0 (RefreshData, 1);
+       data->progress_callback = progress_callback;
+       data->progress_user_data = progress_user_data;
+       data->n_appstream_urls = n_appstream_urls;
+       data->progress_tuples = g_new0 (ProgressTuple, n_appstream_urls);
+       data->progress_source = g_timeout_source_new (progress_update_period_ms);
+       g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) refresh_data_free);
+
+       /* Set up the progress timeout. This periodically sums up the progress
+        * tuples in `data->progress_tuples` and reports them to the calling
+        * function via @progress_callback, giving an overall progress for all
+        * the parallel operations. */
+       g_source_set_callback (data->progress_source, progress_cb, g_object_ref (task), g_object_unref);
+       g_source_attach (data->progress_source, g_main_context_get_thread_default ());
+
+       /* Refresh all the URIs in parallel. */
+       data->n_pending_ops = 1;
+
+       for (gsize i = 0; i < n_appstream_urls; i++) {
                if (!g_str_has_prefix (appstream_urls[i], "https")) {
                        g_warning ("Not considering %s as an external "
                                   "appstream source: please use an https URL",
                                   appstream_urls[i]);
                        continue;
                }
-               if (!gs_external_appstream_refresh_url (plugin,
-                                                       settings,
-                                                       appstream_urls[i],
-                                                       soup_session,
-                                                       cache_age_secs,
-                                                       cancellable,
-                                                       &error_local)) {
-                       g_warning ("Failed to update external appstream file: %s",
-                                  error_local->message);
-               }
+
+               data->n_pending_ops++;
+               refresh_url_async (settings,
+                                  appstream_urls[i],
+                                  soup_session,
+                                  cache_age_secs,
+                                  &data->progress_tuples[i],
+                                  cancellable,
+                                  refresh_cb,
+                                  g_object_ref (task));
        }
 
-       return TRUE;
+       finish_refresh_op (task, NULL);
+}
+
+static void
+refresh_cb (GObject      *source_object,
+            GAsyncResult *result,
+            gpointer      user_data)
+{
+       g_autoptr(GTask) task = g_steal_pointer (&user_data);
+       g_autoptr(GError) local_error = NULL;
+
+       refresh_url_finish (result, &local_error);
+       finish_refresh_op (task, g_steal_pointer (&local_error));
+}
+
+static gboolean
+progress_cb (gpointer user_data)
+{
+       GTask *task = G_TASK (user_data);
+       RefreshData *data = g_task_get_task_data (task);
+       gsize parallel_bytes_downloaded = 0, parallel_total_download_size = 0;
+
+       /* Sum up the progress numerator and denominator for all parallel
+        * downloads. */
+       for (gsize i = 0; i < data->n_appstream_urls; i++) {
+               const ProgressTuple *progress_tuple = &data->progress_tuples[i];
+
+               if (!g_size_checked_add (&parallel_bytes_downloaded,
+                                        parallel_bytes_downloaded,
+                                        progress_tuple->bytes_downloaded))
+                       parallel_bytes_downloaded = G_MAXSIZE;
+               if (!g_size_checked_add (&parallel_total_download_size,
+                                        parallel_total_download_size,
+                                        progress_tuple->total_download_size))
+                       parallel_total_download_size = G_MAXSIZE;
+       }
+
+       /* Report progress to the calling function. */
+       if (data->progress_callback != NULL)
+               data->progress_callback (parallel_bytes_downloaded,
+                                        parallel_total_download_size,
+                                        data->progress_user_data);
+
+       return G_SOURCE_CONTINUE;
+}
+
+/* @error is (transfer full) if non-%NULL */
+static void
+finish_refresh_op (GTask  *task,
+                   GError *error)
+{
+       RefreshData *data = g_task_get_task_data (task);
+       g_autoptr(GError) error_owned = g_steal_pointer (&error);
+
+       if (data->error == NULL && error_owned != NULL)
+               data->error = g_steal_pointer (&error_owned);
+       else if (error_owned != NULL)
+               g_debug ("Additional error while refreshing external appstream: %s", error_owned->message);
+
+       g_assert (data->n_pending_ops > 0);
+       data->n_pending_ops--;
+
+       if (data->n_pending_ops > 0)
+               return;
+
+       /* Emit one final progress update, then stop any further ones. */
+       progress_cb (task);
+       g_source_destroy (data->progress_source);
+
+       /* All complete. */
+       if (data->error != NULL)
+               g_task_return_error (task, g_steal_pointer (&data->error));
+       else
+               g_task_return_boolean (task, TRUE);
+}
+
+/**
+ * gs_external_appstream_refresh_finish:
+ * @result: a #GAsyncResult
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finish an asynchronous refresh operation started with
+ * gs_external_appstream_refresh_async().
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 42
+ */
+gboolean
+gs_external_appstream_refresh_finish (GAsyncResult  *result,
+                                      GError       **error)
+{
+       g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
+       g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+       return g_task_propagate_boolean (G_TASK (result), error);
 }
diff --git a/lib/gs-external-appstream-utils.h b/lib/gs-external-appstream-utils.h
index ecc6d4937..2759ca61e 100644
--- a/lib/gs-external-appstream-utils.h
+++ b/lib/gs-external-appstream-utils.h
@@ -19,7 +19,11 @@
 
 const gchar    *gs_external_appstream_utils_get_system_dir (void);
 gchar          *gs_external_appstream_utils_get_file_cache_path (const gchar   *file_name);
-gboolean        gs_external_appstream_refresh (GsPlugin         *plugin,
-                                               guint64           cache_age_secs,
-                                               GCancellable     *cancellable,
-                                               GError          **error);
+void            gs_external_appstream_refresh_async (guint64                     cache_age_secs,
+                                                     GsDownloadProgressCallback  progress_callback,
+                                                     gpointer                    progress_user_data,
+                                                     GCancellable               *cancellable,
+                                                     GAsyncReadyCallback         callback,
+                                                     gpointer                    user_data);
+gboolean        gs_external_appstream_refresh_finish (GAsyncResult  *result,
+                                                      GError       **error);
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
index 4081a6910..abccfc2a1 100644
--- a/lib/gs-plugin-loader.c
+++ b/lib/gs-plugin-loader.c
@@ -873,14 +873,14 @@ gs_plugin_loader_job_sorted_truncation (GsPluginLoaderHelper *helper)
 typedef struct {
        GsPluginLoader *plugin_loader;  /* (not nullable) (unowned) */
        GsApp *app;  /* (not nullable) (unowned) */
-} OdrsRefreshProgressData;
+} RefreshProgressData;
 
 static void
-odrs_refresh_progress_cb (gsize    bytes_downloaded,
-                          gsize    total_download_size,
-                          gpointer user_data)
+refresh_progress_cb (gsize    bytes_downloaded,
+                     gsize    total_download_size,
+                     gpointer user_data)
 {
-       OdrsRefreshProgressData *data = user_data;
+       RefreshProgressData *data = user_data;
        guint percentage;
 
        if (total_download_size > 0)
@@ -913,11 +913,31 @@ gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
        /* Download updated external appstream before anything else */
 #ifdef ENABLE_EXTERNAL_APPSTREAM
        if (action == GS_PLUGIN_ACTION_REFRESH) {
-               /* FIXME: Using plugin_loader->plugins->pdata[0] is a hack; see
-                * comment below for details. */
-               if (!gs_external_appstream_refresh (plugin_loader->plugins->pdata[0],
-                                                   gs_plugin_job_get_age (helper->plugin_job),
-                                                   cancellable, error))
+               g_autoptr(GsApp) app_dl = NULL;
+               RefreshProgressData progress_data;
+               g_autoptr(GAsyncResult) external_appstream_result = NULL;
+
+               app_dl = gs_app_new ("external-appstream");
+               gs_app_set_summary_missing (app_dl,
+                                           /* TRANSLATORS: status text when downloading */
+                                           _("Downloading extra metadata files…"));
+
+               progress_data.plugin_loader = plugin_loader;
+               progress_data.app = app_dl;
+
+               gs_external_appstream_refresh_async (gs_plugin_job_get_age (helper->plugin_job),
+                                                    refresh_progress_cb,
+                                                    &progress_data,
+                                                    cancellable,
+                                                    async_result_cb,
+                                                    &external_appstream_result);
+
+               /* FIXME: Make this sync until the enclosing function is
+                * refactored to be async. */
+               while (external_appstream_result == NULL)
+                       g_main_context_iteration (g_main_context_get_thread_default (), TRUE);
+
+               if (!gs_external_appstream_refresh_finish (external_appstream_result, error))
                        return FALSE;
        }
 #endif
@@ -940,7 +960,7 @@ gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
        if (action == GS_PLUGIN_ACTION_REFRESH &&
            plugin_loader->odrs_provider != NULL) {
                g_autoptr(GsApp) app_dl = NULL;
-               OdrsRefreshProgressData progress_data;
+               RefreshProgressData progress_data;
                g_autoptr(GAsyncResult) odrs_result = NULL;
                g_autoptr(GError) local_error = NULL;
 
@@ -954,7 +974,7 @@ gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
 
                gs_odrs_provider_refresh_ratings_async (plugin_loader->odrs_provider,
                                                        gs_plugin_job_get_age (helper->plugin_job),
-                                                       odrs_refresh_progress_cb,
+                                                       refresh_progress_cb,
                                                        &progress_data,
                                                        cancellable,
                                                        async_result_cb,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]