summaryrefslogtreecommitdiffstats
path: root/lib/gs-plugin-loader.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 15:18:46 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 15:18:46 +0000
commit56294d30a82ec2da6f9ce399740c1ef65a9ddef4 (patch)
treebbe3823e41495d026ba8edc6eeaef166edb7e2a2 /lib/gs-plugin-loader.c
parentInitial commit. (diff)
downloadgnome-software-56294d30a82ec2da6f9ce399740c1ef65a9ddef4.tar.xz
gnome-software-56294d30a82ec2da6f9ce399740c1ef65a9ddef4.zip
Adding upstream version 3.38.1.upstream/3.38.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/gs-plugin-loader.c')
-rw-r--r--lib/gs-plugin-loader.c3968
1 files changed, 3968 insertions, 0 deletions
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
new file mode 100644
index 0000000..7783f77
--- /dev/null
+++ b/lib/gs-plugin-loader.c
@@ -0,0 +1,3968 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2007-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <appstream-glib.h>
+#include <math.h>
+
+#ifdef HAVE_SYSPROF
+#include <sysprof-capture.h>
+#endif
+
+#include "gs-app-collation.h"
+#include "gs-app-private.h"
+#include "gs-app-list-private.h"
+#include "gs-category-private.h"
+#include "gs-ioprio.h"
+#include "gs-plugin-loader.h"
+#include "gs-plugin.h"
+#include "gs-plugin-event.h"
+#include "gs-plugin-job-private.h"
+#include "gs-plugin-private.h"
+#include "gs-utils.h"
+
+#define GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY 3 /* s */
+#define GS_PLUGIN_LOADER_RELOAD_DELAY 5 /* s */
+
+typedef struct
+{
+ GPtrArray *plugins;
+ GPtrArray *locations;
+ gchar *locale;
+ gchar *language;
+ gboolean plugin_dir_dirty;
+ SoupSession *soup_session;
+ GPtrArray *file_monitors;
+ GsPluginStatus global_status_last;
+
+ GMutex pending_apps_mutex;
+ GPtrArray *pending_apps;
+
+ GThreadPool *queued_ops_pool;
+
+ GSettings *settings;
+
+ GMutex events_by_id_mutex;
+ GHashTable *events_by_id; /* unique-id : GsPluginEvent */
+
+ gchar **compatible_projects;
+ guint scale;
+
+ guint updates_changed_id;
+ guint updates_changed_cnt;
+ guint reload_id;
+ GHashTable *disallow_updates; /* GsPlugin : const char *name */
+
+ GNetworkMonitor *network_monitor;
+ gulong network_changed_handler;
+ gulong network_available_notify_handler;
+ gulong network_metered_notify_handler;
+
+#ifdef HAVE_SYSPROF
+ SysprofCaptureWriter *sysprof_writer; /* (owned) (nullable) */
+#endif
+} GsPluginLoaderPrivate;
+
+static void gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader);
+static void add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app);
+static void gs_plugin_loader_process_in_thread_pool_cb (gpointer data, gpointer user_data);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsPluginLoader, gs_plugin_loader, G_TYPE_OBJECT)
+
+enum {
+ SIGNAL_STATUS_CHANGED,
+ SIGNAL_PENDING_APPS_CHANGED,
+ SIGNAL_UPDATES_CHANGED,
+ SIGNAL_RELOAD,
+ SIGNAL_BASIC_AUTH_START,
+ SIGNAL_LAST
+};
+
+enum {
+ PROP_0,
+ PROP_EVENTS,
+ PROP_ALLOW_UPDATES,
+ PROP_NETWORK_AVAILABLE,
+ PROP_NETWORK_METERED,
+ PROP_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+typedef void (*GsPluginFunc) (GsPlugin *plugin);
+typedef gboolean (*GsPluginSetupFunc) (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginSearchFunc) (GsPlugin *plugin,
+ gchar **value,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginAlternatesFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginCategoryFunc) (GsPlugin *plugin,
+ GsCategory *category,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginGetRecentFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ guint64 age,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginResultsFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginCategoriesFunc) (GsPlugin *plugin,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginActionFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginReviewFunc) (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefineFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefineAppFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefineWildcardFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefreshFunc) (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginFileToAppFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginUrlToAppFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginUpdateFunc) (GsPlugin *plugin,
+ GsAppList *apps,
+ GCancellable *cancellable,
+ GError **error);
+typedef void (*GsPluginAdoptAppFunc) (GsPlugin *plugin,
+ GsApp *app);
+typedef gboolean (*GsPluginGetLangPacksFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+
+
+/* async helper */
+typedef struct {
+ GsPluginLoader *plugin_loader;
+ GCancellable *cancellable;
+ GCancellable *cancellable_caller;
+ gulong cancellable_id;
+ const gchar *function_name;
+ const gchar *function_name_parent;
+ GPtrArray *catlist;
+ GsPluginJob *plugin_job;
+ gboolean anything_ran;
+ guint timeout_id;
+ gboolean timeout_triggered;
+ gchar **tokens;
+} GsPluginLoaderHelper;
+
+static GsPluginLoaderHelper *
+gs_plugin_loader_helper_new (GsPluginLoader *plugin_loader, GsPluginJob *plugin_job)
+{
+ GsPluginLoaderHelper *helper = g_slice_new0 (GsPluginLoaderHelper);
+ GsPluginAction action = gs_plugin_job_get_action (plugin_job);
+ helper->plugin_loader = g_object_ref (plugin_loader);
+ helper->plugin_job = g_object_ref (plugin_job);
+ helper->function_name = gs_plugin_action_to_function_name (action);
+ return helper;
+}
+
+static void
+reset_app_progress (GsApp *app)
+{
+ GsAppList *addons = gs_app_get_addons (app);
+ GsAppList *related = gs_app_get_related (app);
+
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+
+ for (guint i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *app_addons = gs_app_list_index (addons, i);
+ gs_app_set_progress (app_addons, GS_APP_PROGRESS_UNKNOWN);
+ }
+ for (guint i = 0; i < gs_app_list_length (related); i++) {
+ GsApp *app_related = gs_app_list_index (related, i);
+ gs_app_set_progress (app_related, GS_APP_PROGRESS_UNKNOWN);
+ }
+}
+
+static void
+gs_plugin_loader_helper_free (GsPluginLoaderHelper *helper)
+{
+ /* reset progress */
+ switch (gs_plugin_job_get_action (helper->plugin_job)) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_UPDATE:
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ {
+ GsApp *app;
+ GsAppList *list;
+
+ app = gs_plugin_job_get_app (helper->plugin_job);
+ if (app != NULL)
+ reset_app_progress (app);
+
+ list = gs_plugin_job_get_list (helper->plugin_job);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ reset_app_progress (app_tmp);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (helper->cancellable_id > 0) {
+ g_debug ("Disconnecting cancellable %p", helper->cancellable_caller);
+ g_cancellable_disconnect (helper->cancellable_caller,
+ helper->cancellable_id);
+ }
+ g_object_unref (helper->plugin_loader);
+ if (helper->timeout_id != 0)
+ g_source_remove (helper->timeout_id);
+ if (helper->plugin_job != NULL)
+ g_object_unref (helper->plugin_job);
+ if (helper->cancellable != NULL)
+ g_object_unref (helper->cancellable);
+ if (helper->cancellable_caller != NULL)
+ g_object_unref (helper->cancellable_caller);
+ if (helper->catlist != NULL)
+ g_ptr_array_unref (helper->catlist);
+ g_strfreev (helper->tokens);
+ g_slice_free (GsPluginLoaderHelper, helper);
+}
+
+static void
+gs_plugin_loader_job_debug (GsPluginLoaderHelper *helper)
+{
+ g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job);
+ g_debug ("%s", str);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPluginLoaderHelper, gs_plugin_loader_helper_free)
+
+static gint
+gs_plugin_loader_app_sort_name_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ return gs_utils_sort_strcmp (gs_app_get_name (app1), gs_app_get_name (app2));
+}
+
+GsPlugin *
+gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (g_strcmp0 (gs_plugin_get_name (plugin), plugin_name) == 0)
+ return plugin;
+ }
+ return NULL;
+}
+
+static gboolean
+gs_plugin_loader_notify_idle_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ g_object_notify (G_OBJECT (plugin_loader), "events");
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_add_event (GsPluginLoader *plugin_loader, GsPluginEvent *event)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+
+ /* events should always have a unique ID, either constructed from the
+ * app they are processing or preferably from the GError message */
+ if (gs_plugin_event_get_unique_id (event) == NULL) {
+ g_warning ("failed to add event from action %s",
+ gs_plugin_action_to_string (gs_plugin_event_get_action (event)));
+ return;
+ }
+
+ g_hash_table_insert (priv->events_by_id,
+ g_strdup (gs_plugin_event_get_unique_id (event)),
+ g_object_ref (event));
+ g_idle_add (gs_plugin_loader_notify_idle_cb, plugin_loader);
+}
+
+static GsPluginEvent *
+gs_plugin_job_to_failed_event (GsPluginJob *plugin_job, const GError *error)
+{
+ GsPluginEvent *event;
+ g_autoptr(GError) error_copy = NULL;
+
+ g_return_val_if_fail (error != NULL, NULL);
+
+ /* invalid */
+ if (error->domain != GS_PLUGIN_ERROR) {
+ g_warning ("not GsPlugin error %s:%i: %s",
+ g_quark_to_string (error->domain),
+ error->code,
+ error->message);
+ g_set_error_literal (&error_copy,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ error->message);
+ } else {
+ error_copy = g_error_copy (error);
+ }
+
+ /* create plugin event */
+ event = gs_plugin_event_new ();
+ gs_plugin_event_set_error (event, error_copy);
+ gs_plugin_event_set_action (event, gs_plugin_job_get_action (plugin_job));
+ if (gs_plugin_job_get_app (plugin_job) != NULL)
+ gs_plugin_event_set_app (event, gs_plugin_job_get_app (plugin_job));
+ if (gs_plugin_job_get_interactive (plugin_job))
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ return event;
+}
+
+static gboolean
+gs_plugin_loader_is_error_fatal (const GError *err)
+{
+ if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT))
+ return TRUE;
+ if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED))
+ return TRUE;
+ if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID))
+ return TRUE;
+ return FALSE;
+}
+
+static gboolean
+gs_plugin_error_handle_failure (GsPluginLoaderHelper *helper,
+ GsPlugin *plugin,
+ const GError *error_local,
+ GError **error)
+{
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *origin_id = NULL;
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ /* badly behaved plugin */
+ if (error_local == NULL) {
+ g_critical ("%s did not set error for %s",
+ gs_plugin_get_name (plugin),
+ helper->function_name);
+ return TRUE;
+ }
+
+ /* this is only ever informational */
+ if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_debug ("ignoring error cancelled: %s", error_local->message);
+ return TRUE;
+ }
+
+ /* find and strip any unique IDs from the error message */
+ for (guint i = 0; i < 2; i++) {
+ if (app_id == NULL)
+ app_id = gs_utils_error_strip_app_id (error_local);
+ if (origin_id == NULL)
+ origin_id = gs_utils_error_strip_origin_id (error_local);
+ }
+
+ /* fatal error */
+ if (gs_plugin_job_get_action (helper->plugin_job) == GS_PLUGIN_ACTION_SETUP ||
+ gs_plugin_loader_is_error_fatal (error_local) ||
+ g_getenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD") != NULL) {
+ if (error != NULL)
+ *error = g_error_copy (error_local);
+ return FALSE;
+ }
+
+ /* create event which is handled by the GsShell */
+ event = gs_plugin_job_to_failed_event (helper->plugin_job, error_local);
+
+ /* set the app and origin IDs if we managed to scrape them from the error above */
+ if (as_utils_unique_id_valid (app_id)) {
+ g_autoptr(GsApp) app = gs_plugin_cache_lookup (plugin, app_id);
+ if (app != NULL) {
+ g_debug ("found app %s in error", origin_id);
+ gs_plugin_event_set_app (event, app);
+ } else {
+ g_debug ("no unique ID found for app %s", app_id);
+ }
+ }
+ if (as_utils_unique_id_valid (origin_id)) {
+ g_autoptr(GsApp) origin = gs_plugin_cache_lookup (plugin, origin_id);
+ if (origin != NULL) {
+ g_debug ("found origin %s in error", origin_id);
+ gs_plugin_event_set_origin (event, origin);
+ } else {
+ g_debug ("no unique ID found for origin %s", origin_id);
+ }
+ }
+
+ /* add event to queue */
+ gs_plugin_loader_add_event (helper->plugin_loader, event);
+ return TRUE;
+}
+
+static void
+gs_plugin_loader_run_adopt (GsPluginLoader *plugin_loader, GsAppList *list)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ guint i;
+ guint j;
+
+ /* go through each plugin in order */
+ for (i = 0; i < priv->plugins->len; i++) {
+ GsPluginAdoptAppFunc adopt_app_func = NULL;
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ adopt_app_func = gs_plugin_get_symbol (plugin, "gs_plugin_adopt_app");
+ if (adopt_app_func == NULL)
+ continue;
+ for (j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_management_plugin (app) != NULL)
+ continue;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ adopt_app_func (plugin, app);
+ if (gs_app_get_management_plugin (app) != NULL) {
+ g_debug ("%s adopted %s",
+ gs_plugin_get_name (plugin),
+ gs_app_get_unique_id (app));
+ }
+ }
+ }
+ for (j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_management_plugin (app) != NULL)
+ continue;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ g_debug ("nothing adopted %s", gs_app_get_unique_id (app));
+ }
+}
+
+static gint
+gs_plugin_loader_review_score_sort_cb (gconstpointer a, gconstpointer b)
+{
+ AsReview *ra = *((AsReview **) a);
+ AsReview *rb = *((AsReview **) b);
+ if (as_review_get_priority (ra) < as_review_get_priority (rb))
+ return 1;
+ if (as_review_get_priority (ra) > as_review_get_priority (rb))
+ return -1;
+ return 0;
+}
+
+static gboolean
+gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
+ GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifdef HAVE_SYSPROF
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+#endif
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ gboolean ret = TRUE;
+ gpointer func = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GTimer) timer = g_timer_new ();
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* load the possible symbol */
+ func = gs_plugin_get_symbol (plugin, helper->function_name);
+ if (func == NULL)
+ return TRUE;
+
+ /* fallback if unset */
+ if (app == NULL)
+ app = gs_plugin_job_get_app (helper->plugin_job);
+ if (list == NULL)
+ list = gs_plugin_job_get_list (helper->plugin_job);
+ if (refine_flags == GS_PLUGIN_REFINE_FLAGS_DEFAULT)
+ refine_flags = gs_plugin_job_get_refine_flags (helper->plugin_job);
+
+ /* set what plugin is running on the job */
+ gs_plugin_job_set_plugin (helper->plugin_job, plugin);
+
+ /* run the correct vfunc */
+ if (gs_plugin_job_get_interactive (helper->plugin_job))
+ gs_plugin_interactive_inc (plugin);
+ switch (action) {
+ case GS_PLUGIN_ACTION_INITIALIZE:
+ case GS_PLUGIN_ACTION_DESTROY:
+ {
+ GsPluginFunc plugin_func = func;
+ plugin_func (plugin);
+ }
+ break;
+ case GS_PLUGIN_ACTION_SETUP:
+ {
+ GsPluginSetupFunc plugin_func = func;
+ ret = plugin_func (plugin, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_REFINE:
+ if (g_strcmp0 (helper->function_name, "gs_plugin_refine_wildcard") == 0) {
+ GsPluginRefineWildcardFunc plugin_func = func;
+ ret = plugin_func (plugin, app, list, refine_flags, cancellable, &error_local);
+ } else if (g_strcmp0 (helper->function_name, "gs_plugin_refine") == 0) {
+ GsPluginRefineFunc plugin_func = func;
+ ret = plugin_func (plugin, list, refine_flags, cancellable, &error_local);
+ } else {
+ g_critical ("function_name %s invalid for %s",
+ helper->function_name,
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ case GS_PLUGIN_ACTION_UPDATE:
+ if (g_strcmp0 (helper->function_name, "gs_plugin_update_app") == 0) {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ } else if (g_strcmp0 (helper->function_name, "gs_plugin_update") == 0) {
+ GsPluginUpdateFunc plugin_func = func;
+ ret = plugin_func (plugin, list, cancellable, &error_local);
+ } else {
+ g_critical ("function_name %s invalid for %s",
+ helper->function_name,
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ if (g_strcmp0 (helper->function_name, "gs_plugin_download_app") == 0) {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ } else if (g_strcmp0 (helper->function_name, "gs_plugin_download") == 0) {
+ GsPluginUpdateFunc plugin_func = func;
+ ret = plugin_func (plugin, list, cancellable, &error_local);
+ } else {
+ g_critical ("function_name %s invalid for %s",
+ helper->function_name,
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_SET_RATING:
+ case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD:
+ case GS_PLUGIN_ACTION_UPGRADE_TRIGGER:
+ case GS_PLUGIN_ACTION_LAUNCH:
+ case GS_PLUGIN_ACTION_UPDATE_CANCEL:
+ case GS_PLUGIN_ACTION_ADD_SHORTCUT:
+ case GS_PLUGIN_ACTION_REMOVE_SHORTCUT:
+ {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
+ case GS_PLUGIN_ACTION_REVIEW_UPVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_REPORT:
+ case GS_PLUGIN_ACTION_REVIEW_REMOVE:
+ case GS_PLUGIN_ACTION_REVIEW_DISMISS:
+ {
+ GsPluginReviewFunc plugin_func = func;
+ ret = plugin_func (plugin, app,
+ gs_plugin_job_get_review (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ {
+ GsPluginGetRecentFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_age (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ case GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL:
+ case GS_PLUGIN_ACTION_GET_DISTRO_UPDATES:
+ case GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS:
+ case GS_PLUGIN_ACTION_GET_SOURCES:
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ case GS_PLUGIN_ACTION_GET_POPULAR:
+ case GS_PLUGIN_ACTION_GET_FEATURED:
+ {
+ GsPluginResultsFunc plugin_func = func;
+ ret = plugin_func (plugin, list, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_SEARCH:
+ {
+ GsPluginSearchFunc plugin_func = func;
+ ret = plugin_func (plugin, helper->tokens, list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ {
+ GsPluginSearchFunc plugin_func = func;
+ gchar *search[2] = { gs_plugin_job_get_search (helper->plugin_job), NULL };
+ ret = plugin_func (plugin, search, list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ {
+ GsPluginAlternatesFunc plugin_func = func;
+ ret = plugin_func (plugin, app, list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORIES:
+ {
+ GsPluginCategoriesFunc plugin_func = func;
+ ret = plugin_func (plugin, helper->catlist,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ {
+ GsPluginCategoryFunc plugin_func = func;
+ ret = plugin_func (plugin,
+ gs_plugin_job_get_category (helper->plugin_job),
+ list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_REFRESH:
+ {
+ GsPluginRefreshFunc plugin_func = func;
+ ret = plugin_func (plugin,
+ gs_plugin_job_get_age (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ {
+ GsPluginFileToAppFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_file (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ {
+ GsPluginUrlToAppFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_search (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_LANGPACKS:
+ {
+ GsPluginGetLangPacksFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_search (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ default:
+ g_critical ("no handler for %s", helper->function_name);
+ break;
+ }
+ if (gs_plugin_job_get_interactive (helper->plugin_job))
+ gs_plugin_interactive_dec (plugin);
+
+ /* plugin did not return error on cancellable abort */
+ if (ret && g_cancellable_set_error_if_cancelled (cancellable, &error_local)) {
+ g_debug ("plugin %s did not return error with cancellable set",
+ gs_plugin_get_name (plugin));
+ gs_utils_error_convert_gio (&error_local);
+ ret = FALSE;
+ }
+
+ /* failed */
+ if (!ret) {
+ /* we returned cancelled, but this was because of a timeout,
+ * so re-create error, throwing the plugin under the bus */
+ if (helper->timeout_triggered &&
+ g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+ g_debug ("converting cancelled to timeout");
+ g_clear_error (&error_local);
+ g_set_error (&error_local,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_TIMED_OUT,
+ "Timeout was reached as %s took "
+ "too long to return results",
+ gs_plugin_get_name (plugin));
+ }
+ return gs_plugin_error_handle_failure (helper,
+ plugin,
+ error_local,
+ error);
+ }
+
+ /* add app to the pending installation queue if necessary */
+ if (action == GS_PLUGIN_ACTION_INSTALL &&
+ app != NULL && gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL) {
+ add_app_to_install_queue (helper->plugin_loader, app);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = NULL;
+ g_autofree gchar *sysprof_message = NULL;
+
+ sysprof_name = g_strconcat ("vfunc:", gs_plugin_action_to_string (action), NULL);
+ sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* check the plugin didn't take too long */
+ if (g_timer_elapsed (timer, NULL) > 1.0f) {
+ GLogLevelFlags log_level;
+
+ switch (action) {
+ case GS_PLUGIN_ACTION_INITIALIZE:
+ case GS_PLUGIN_ACTION_DESTROY:
+ case GS_PLUGIN_ACTION_SETUP:
+ if (g_getenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD") == NULL)
+ log_level = G_LOG_LEVEL_WARNING;
+ else
+ log_level = G_LOG_LEVEL_DEBUG;
+ break;
+ default:
+ log_level = G_LOG_LEVEL_DEBUG;
+ break;
+ }
+
+ g_log_structured_standard (G_LOG_DOMAIN, log_level,
+ __FILE__, G_STRINGIFY (__LINE__),
+ G_STRFUNC,
+ "plugin %s took %.1f seconds to do %s",
+ gs_plugin_get_name (plugin),
+ g_timer_elapsed (timer, NULL),
+ gs_plugin_action_to_string (action));
+ }
+
+ /* success */
+ helper->anything_ran = TRUE;
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_non_wildcard (GsApp *app, gpointer user_data)
+{
+ return !gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+}
+
+static gboolean
+gs_plugin_loader_run_refine_filter (GsPluginLoaderHelper *helper,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+
+ /* run each plugin */
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ g_autoptr(GsAppList) app_list = NULL;
+
+ /* run the batched plugin symbol then refine wildcards per-app */
+ helper->function_name = "gs_plugin_refine";
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, list,
+ refine_flags, cancellable, error)) {
+ return FALSE;
+ }
+
+ if (gs_plugin_get_symbol (plugin, "gs_plugin_refine_wildcard") != NULL) {
+ /* use a copy of the list for the loop because a function called
+ * on the plugin may affect the list which can lead to problems
+ * (e.g. inserting an app in the list on every call results in
+ * an infinite loop) */
+ app_list = gs_app_list_copy (list);
+ helper->function_name = "gs_plugin_refine_wildcard";
+
+ for (guint j = 0; j < gs_app_list_length (app_list); j++) {
+ GsApp *app = gs_app_list_index (app_list, j);
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD) &&
+ !gs_plugin_loader_call_vfunc (helper, plugin, app, NULL,
+ refine_flags, cancellable, error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+
+ /* filter any wildcard apps left in the list */
+ gs_app_list_filter (list, gs_plugin_loader_app_is_non_wildcard, NULL);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_run_refine_internal (GsPluginLoaderHelper *helper,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* try to adopt each application with a plugin */
+ gs_plugin_loader_run_adopt (helper->plugin_loader, list);
+
+ /* run each plugin */
+ if (!gs_plugin_loader_run_refine_filter (helper, list,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, error))
+ return FALSE;
+
+ /* ensure these are sorted by score */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS)) {
+ GPtrArray *reviews;
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ reviews = gs_app_get_reviews (app);
+ g_ptr_array_sort (reviews,
+ gs_plugin_loader_review_score_sort_cb);
+ }
+ }
+
+ /* refine addons one layer deep */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS)) {
+ g_autoptr(GsAppList) addons_list = NULL;
+ gs_plugin_job_remove_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS);
+ addons_list = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GsAppList *addons = gs_app_get_addons (app);
+ for (guint j = 0; j < gs_app_list_length (addons); j++) {
+ GsApp *addon = gs_app_list_index (addons, j);
+ g_debug ("refining app %s addon %s",
+ gs_app_get_id (app),
+ gs_app_get_id (addon));
+ gs_app_list_add (addons_list, addon);
+ }
+ }
+ if (gs_app_list_length (addons_list) > 0) {
+ if (!gs_plugin_loader_run_refine_internal (helper,
+ addons_list,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* also do runtime */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME)) {
+ g_autoptr(GsAppList) list2 = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *runtime;
+ GsApp *app = gs_app_list_index (list, i);
+ runtime = gs_app_get_runtime (app);
+ if (runtime != NULL)
+ gs_app_list_add (list2, runtime);
+ }
+ if (gs_app_list_length (list2) > 0) {
+ if (!gs_plugin_loader_run_refine_internal (helper,
+ list2,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* also do related packages one layer deep */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED)) {
+ g_autoptr(GsAppList) related_list = NULL;
+ gs_plugin_job_remove_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED);
+ related_list = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GsAppList *related = gs_app_get_related (app);
+ for (guint j = 0; j < gs_app_list_length (related); j++) {
+ GsApp *app2 = gs_app_list_index (related, j);
+ g_debug ("refining related: %s[%s]",
+ gs_app_get_id (app2),
+ gs_app_get_source_default (app2));
+ gs_app_list_add (related_list, app2);
+ }
+ }
+ if (gs_app_list_length (related_list) > 0) {
+ if (!gs_plugin_loader_run_refine_internal (helper,
+ related_list,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+app_thaw_notify_idle (gpointer data)
+{
+ GsApp *app = GS_APP (data);
+ g_object_thaw_notify (G_OBJECT (app));
+ g_object_unref (app);
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+gs_plugin_loader_run_refine (GsPluginLoaderHelper *helper,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret;
+ g_autoptr(GsAppList) freeze_list = NULL;
+ g_autoptr(GsPluginLoaderHelper) helper2 = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* nothing to do */
+ if (gs_app_list_length (list) == 0)
+ return TRUE;
+
+ /* freeze all apps */
+ freeze_list = gs_app_list_copy (list);
+ for (guint i = 0; i < gs_app_list_length (freeze_list); i++) {
+ GsApp *app = gs_app_list_index (freeze_list, i);
+ g_object_freeze_notify (G_OBJECT (app));
+ }
+
+ /* first pass */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "list", list,
+ "refine-flags", gs_plugin_job_get_refine_flags (helper->plugin_job),
+ NULL);
+ helper2 = gs_plugin_loader_helper_new (helper->plugin_loader, plugin_job);
+ helper2->function_name_parent = helper->function_name;
+ ret = gs_plugin_loader_run_refine_internal (helper2, list, cancellable, error);
+ if (!ret)
+ goto out;
+
+ /* remove any addons that have the same source as the parent app */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ g_autoptr(GPtrArray) to_remove = g_ptr_array_new ();
+ GsApp *app = gs_app_list_index (list, i);
+ GsAppList *addons = gs_app_get_addons (app);
+
+ /* find any apps with the same source */
+ const gchar *pkgname_parent = gs_app_get_source_default (app);
+ if (pkgname_parent == NULL)
+ continue;
+ for (guint j = 0; j < gs_app_list_length (addons); j++) {
+ GsApp *addon = gs_app_list_index (addons, j);
+ if (g_strcmp0 (gs_app_get_source_default (addon),
+ pkgname_parent) == 0) {
+ g_debug ("%s has the same pkgname of %s as %s",
+ gs_app_get_unique_id (app),
+ pkgname_parent,
+ gs_app_get_unique_id (addon));
+ g_ptr_array_add (to_remove, addon);
+ }
+ }
+
+ /* remove any addons with the same source */
+ for (guint j = 0; j < to_remove->len; j++) {
+ GsApp *addon = g_ptr_array_index (to_remove, j);
+ gs_app_remove_addon (app, addon);
+ }
+ }
+
+out:
+ /* now emit all the changed signals */
+ for (guint i = 0; i < gs_app_list_length (freeze_list); i++) {
+ GsApp *app = gs_app_list_index (freeze_list, i);
+ g_idle_add (app_thaw_notify_idle, g_object_ref (app));
+ }
+ return ret;
+}
+
+static void
+gs_plugin_loader_job_sorted_truncation_again (GsPluginLoaderHelper *helper)
+{
+ GsAppListSortFunc sort_func;
+ gpointer sort_func_data;
+
+ /* not valid */
+ if (gs_plugin_job_get_list (helper->plugin_job) == NULL)
+ return;
+
+ /* unset */
+ sort_func = gs_plugin_job_get_sort_func (helper->plugin_job);
+ if (sort_func == NULL)
+ return;
+ sort_func_data = gs_plugin_job_get_sort_func_data (helper->plugin_job);
+ gs_app_list_sort (gs_plugin_job_get_list (helper->plugin_job), sort_func, sort_func_data);
+}
+
+static void
+gs_plugin_loader_job_sorted_truncation (GsPluginLoaderHelper *helper)
+{
+ GsAppListSortFunc sort_func;
+ guint max_results;
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+
+ /* not valid */
+ if (list == NULL)
+ return;
+
+ /* unset */
+ max_results = gs_plugin_job_get_max_results (helper->plugin_job);
+ if (max_results == 0)
+ return;
+
+ /* already small enough */
+ if (gs_app_list_length (list) <= max_results)
+ return;
+
+ /* nothing set */
+ g_debug ("truncating results to %u from %u",
+ max_results, gs_app_list_length (list));
+ sort_func = gs_plugin_job_get_sort_func (helper->plugin_job);
+ if (sort_func == NULL) {
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ g_debug ("no ->sort_func() set for %s, using random!",
+ gs_plugin_action_to_string (action));
+ gs_app_list_randomize (list);
+ } else {
+ gpointer sort_func_data;
+ sort_func_data = gs_plugin_job_get_sort_func_data (helper->plugin_job);
+ gs_app_list_sort (list, sort_func, sort_func_data);
+ }
+ gs_app_list_truncate (list, max_results);
+}
+
+static gboolean
+gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* run each plugin */
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_utils_error_convert_gio (error);
+ return FALSE;
+ }
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, error)) {
+ return FALSE;
+ }
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = NULL;
+ g_autofree gchar *sysprof_message = NULL;
+
+ sysprof_name = g_strconcat ("run-results:",
+ gs_plugin_action_to_string (gs_plugin_job_get_action (helper->plugin_job)),
+ NULL);
+ sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ return TRUE;
+}
+
+static const gchar *
+gs_plugin_loader_get_app_str (GsApp *app)
+{
+ const gchar *id;
+
+ /* first try the actual id */
+ id = gs_app_get_unique_id (app);
+ if (id != NULL)
+ return id;
+
+ /* then try the source */
+ id = gs_app_get_source_default (app);
+ if (id != NULL)
+ return id;
+
+ /* lastly try the source id */
+ id = gs_app_get_source_id_default (app);
+ if (id != NULL)
+ return id;
+
+ /* urmmm */
+ return "<invalid>";
+}
+
+static gboolean
+gs_plugin_loader_app_set_prio (GsApp *app, gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPlugin *plugin;
+ const gchar *tmp;
+
+ /* if set, copy the priority */
+ tmp = gs_app_get_management_plugin (app);
+ if (tmp == NULL)
+ return TRUE;
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, tmp);
+ if (plugin == NULL)
+ return TRUE;
+ gs_app_set_priority (app, gs_plugin_get_priority (plugin));
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid_installed (GsApp *app, gpointer user_data)
+{
+ /* even without AppData, show things in progress */
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_REMOVING:
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+
+ switch (gs_app_get_kind (app)) {
+ case AS_APP_KIND_OS_UPGRADE:
+ case AS_APP_KIND_CODEC:
+ case AS_APP_KIND_FONT:
+ g_debug ("app invalid as %s: %s",
+ as_app_kind_to_string (gs_app_get_kind (app)),
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ break;
+ default:
+ break;
+ }
+
+ /* sanity check */
+ if (!gs_app_is_installed (app)) {
+ g_autofree gchar *tmp = gs_app_to_string (app);
+ g_warning ("ignoring non-installed app %s", tmp);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid (GsApp *app, gpointer user_data)
+{
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+ /* never show addons */
+ if (gs_app_get_kind (app) == AS_APP_KIND_ADDON) {
+ g_debug ("app invalid as addon %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* never show CLI apps */
+ if (gs_app_get_kind (app) == AS_APP_KIND_CONSOLE) {
+ g_debug ("app invalid as console %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unknown state */
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) {
+ g_debug ("app invalid as state unknown %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unconverted unavailables */
+ if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN &&
+ gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) {
+ g_debug ("app invalid as unconverted unavailable %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show blocklisted apps */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE)) {
+ g_debug ("app invalid as blocklisted %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* Don’t show parentally filtered apps unless they’re already
+ * installed. See the comments in gs-details-page.c for details. */
+ if (!gs_app_is_installed (app) &&
+ gs_app_has_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER)) {
+ g_debug ("app invalid as parentally filtered %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show apps with hide-from-search quirk, unless they are already installed */
+ if (!gs_app_is_installed (app) &&
+ gs_app_has_quirk (app, GS_APP_QUIRK_HIDE_FROM_SEARCH)) {
+ g_debug ("app invalid as hide-from-search quirk set %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show sources */
+ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) {
+ g_debug ("app invalid as source %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unknown kind */
+ if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
+ g_debug ("app invalid as kind unknown %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unconverted packages in the application view */
+ if (!gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES) &&
+ (gs_app_get_kind (app) == AS_APP_KIND_GENERIC)) {
+ g_debug ("app invalid as only a %s: %s",
+ as_app_kind_to_string (gs_app_get_kind (app)),
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show apps that do not have the required details */
+ if (gs_app_get_name (app) == NULL) {
+ g_debug ("app invalid as no name %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+ if (gs_app_get_summary (app) == NULL) {
+ g_debug ("app invalid as no summary %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* ignore this crazy application */
+ if (g_strcmp0 (gs_app_get_id (app), "gnome-system-monitor-kde.desktop") == 0) {
+ g_debug ("Ignoring KDE version of %s", gs_app_get_id (app));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid_updatable (GsApp *app, gpointer user_data)
+{
+ return gs_plugin_loader_app_is_valid (app, user_data) &&
+ gs_app_is_updatable (app);
+}
+
+static gboolean
+gs_plugin_loader_filter_qt_for_gtk (GsApp *app, gpointer user_data)
+{
+ /* hide the QT versions in preference to the GTK ones */
+ if (g_strcmp0 (gs_app_get_id (app), "transmission-qt.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "nntpgrab_qt.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "gimagereader-qt4.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "gimagereader-qt5.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "nntpgrab_server_qt.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "hotot-qt.desktop") == 0) {
+ g_debug ("removing QT version of %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* hide the KDE version in preference to the GTK one */
+ if (g_strcmp0 (gs_app_get_id (app), "qalculate_kde.desktop") == 0) {
+ g_debug ("removing KDE version of %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* hide the KDE version in preference to the Qt one */
+ if (g_strcmp0 (gs_app_get_id (app), "kid3.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "kchmviewer.desktop") == 0) {
+ g_debug ("removing KDE version of %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_non_compulsory (GsApp *app, gpointer user_data)
+{
+ return !gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY);
+}
+
+static gboolean
+gs_plugin_loader_get_app_is_compatible (GsApp *app, gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ const gchar *tmp;
+ guint i;
+
+ /* search for any compatible projects */
+ tmp = gs_app_get_project_group (app);
+ if (tmp == NULL)
+ return TRUE;
+ for (i = 0; priv->compatible_projects[i] != NULL; i++) {
+ if (g_strcmp0 (tmp, priv->compatible_projects[i]) == 0)
+ return TRUE;
+ }
+ g_debug ("removing incompatible %s from project group %s",
+ gs_app_get_id (app), gs_app_get_project_group (app));
+ return FALSE;
+}
+
+/******************************************************************************/
+
+static gboolean
+gs_plugin_loader_featured_debug (GsApp *app, gpointer user_data)
+{
+ if (g_strcmp0 (gs_app_get_id (app),
+ g_getenv ("GNOME_SOFTWARE_FEATURED")) == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static gint
+gs_plugin_loader_app_sort_kind_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ if (gs_app_get_kind (app1) == AS_APP_KIND_DESKTOP)
+ return -1;
+ if (gs_app_get_kind (app2) == AS_APP_KIND_DESKTOP)
+ return 1;
+ return 0;
+}
+
+static gint
+gs_plugin_loader_app_sort_match_value_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ if (gs_app_get_match_value (app1) > gs_app_get_match_value (app2))
+ return -1;
+ if (gs_app_get_match_value (app1) < gs_app_get_match_value (app2))
+ return 1;
+ return 0;
+}
+
+static gint
+gs_plugin_loader_app_sort_prio_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ return gs_app_compare_priority (app1, app2);
+}
+
+static gint
+gs_plugin_loader_app_sort_version_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+#if AS_CHECK_VERSION(0,7,15)
+ return as_utils_vercmp_full (gs_app_get_version (app1),
+ gs_app_get_version (app2),
+ AS_VERSION_COMPARE_FLAG_NONE);
+#else
+ return as_utils_vercmp (gs_app_get_version (app1),
+ gs_app_get_version (app2));
+#endif
+}
+
+/******************************************************************************/
+
+/**
+ * gs_plugin_loader_job_process_finish:
+ * @plugin_loader: A #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Return value: (element-type GsApp) (transfer full): A list of applications
+ **/
+GsAppList *
+gs_plugin_loader_job_process_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ GTask *task;
+ GsAppList *list = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+ g_return_val_if_fail (G_IS_TASK (res), NULL);
+ g_return_val_if_fail (g_task_is_valid (res, plugin_loader), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ task = G_TASK (res);
+
+ /* Return cancelled if the task was cancelled and there is no other error set.
+ *
+ * This is needed because we set the task `check_cancellable` to FALSE,
+ * to be able to catch other errors such as timeout, but that means
+ * g_task_propagate_pointer() will ignore if the task was cancelled and only
+ * check if there was an error (i.e. g_task_return_*error*).
+ *
+ * We only do this if there is no error already set in the task (e.g.
+ * timeout) because in that case we want to return the existing error.
+ */
+ if (!g_task_had_error (task)) {
+ GCancellable *cancellable = g_task_get_cancellable (task);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ }
+ list = g_task_propagate_pointer (task, error);
+ gs_utils_error_convert_gio (error);
+ return list;
+}
+
+/**
+ * gs_plugin_loader_job_action_finish:
+ * @plugin_loader: A #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Return value: success
+ **/
+gboolean
+gs_plugin_loader_job_action_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ g_autoptr(GsAppList) list = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), FALSE);
+ g_return_val_if_fail (G_IS_TASK (res), FALSE);
+ g_return_val_if_fail (g_task_is_valid (res, plugin_loader), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ list = g_task_propagate_pointer (G_TASK (res), error);
+ return list != NULL;
+}
+
+/******************************************************************************/
+
+static gint
+gs_plugin_loader_category_sort_cb (gconstpointer a, gconstpointer b)
+{
+ GsCategory *cata = GS_CATEGORY (*(GsCategory **) a);
+ GsCategory *catb = GS_CATEGORY (*(GsCategory **) b);
+ if (gs_category_get_score (cata) < gs_category_get_score (catb))
+ return 1;
+ if (gs_category_get_score (cata) > gs_category_get_score (catb))
+ return -1;
+ return gs_utils_sort_strcmp (gs_category_get_name (cata),
+ gs_category_get_name (catb));
+}
+
+static void
+gs_plugin_loader_fix_category_all (GsCategory *category)
+{
+ GPtrArray *children;
+ GsCategory *cat_all;
+ guint i, j;
+
+ /* set correct size */
+ cat_all = gs_category_find_child (category, "all");
+ if (cat_all == NULL)
+ return;
+ gs_category_set_size (cat_all, gs_category_get_size (category));
+
+ /* add the desktop groups from all children */
+ children = gs_category_get_children (category);
+ for (i = 0; i < children->len; i++) {
+ GPtrArray *desktop_groups;
+ GsCategory *child;
+
+ /* ignore the all category */
+ child = g_ptr_array_index (children, i);
+ if (g_strcmp0 (gs_category_get_id (child), "all") == 0)
+ continue;
+
+ /* add all desktop groups */
+ desktop_groups = gs_category_get_desktop_groups (child);
+ for (j = 0; j < desktop_groups->len; j++) {
+ const gchar *tmp = g_ptr_array_index (desktop_groups, j);
+ gs_category_add_desktop_group (cat_all, tmp);
+ }
+ }
+}
+
+static void
+gs_plugin_loader_job_get_categories_thread_cb (GTask *task,
+ gpointer object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GError *error = NULL;
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) task_data;
+#ifdef HAVE_SYSPROF
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* run each plugin */
+ if (!gs_plugin_loader_run_results (helper, cancellable, &error)) {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ /* make sure 'All' has the right categories */
+ for (guint i = 0; i < helper->catlist->len; i++) {
+ GsCategory *cat = g_ptr_array_index (helper->catlist, i);
+ gs_plugin_loader_fix_category_all (cat);
+ }
+
+ /* sort by name */
+ g_ptr_array_sort (helper->catlist, gs_plugin_loader_category_sort_cb);
+ for (guint i = 0; i < helper->catlist->len; i++) {
+ GsCategory *cat = GS_CATEGORY (g_ptr_array_index (helper->catlist, i));
+ gs_category_sort_children (cat);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ "get-categories",
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* show elapsed time */
+ gs_plugin_loader_job_debug (helper);
+
+ /* success */
+ if (helper->catlist->len == 0)
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no categories to show");
+ else
+ g_task_return_pointer (task, g_ptr_array_ref (helper->catlist), (GDestroyNotify) g_ptr_array_unref);
+}
+
+/**
+ * gs_plugin_loader_job_get_categories_async:
+ * @plugin_loader: A #GsPluginLoader
+ * @plugin_job: job to process
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: function to call when complete
+ * @user_data: user data to pass to @callback
+ *
+ * This method calls all plugins that implement the gs_plugin_add_categories()
+ * function. The plugins return #GsCategory objects.
+ **/
+void
+gs_plugin_loader_job_get_categories_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginLoaderHelper *helper;
+ g_autoptr(GTask) task = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (GS_IS_PLUGIN_JOB (plugin_job));
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ /* save helper */
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ helper->catlist = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+
+ /* run in a thread */
+ task = g_task_new (plugin_loader, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_loader_job_get_categories_async);
+ g_task_set_task_data (task, helper, (GDestroyNotify) gs_plugin_loader_helper_free);
+ g_task_run_in_thread (task, gs_plugin_loader_job_get_categories_thread_cb);
+}
+
+/**
+ * gs_plugin_loader_job_get_categories_finish:
+ * @plugin_loader: A #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Return value: (element-type GsCategory) (transfer full): A list of applications
+ **/
+GPtrArray *
+gs_plugin_loader_job_get_categories_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ GPtrArray *array;
+
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+ g_return_val_if_fail (G_IS_TASK (res), NULL);
+ g_return_val_if_fail (g_task_is_valid (res, plugin_loader), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ array = g_task_propagate_pointer (G_TASK (res), error);
+ gs_utils_error_convert_gio (error);
+ return array;
+}
+
+/******************************************************************************/
+
+static gboolean
+emit_pending_apps_idle (gpointer loader)
+{
+ g_signal_emit (loader, signals[SIGNAL_PENDING_APPS_CHANGED], 0);
+ g_object_unref (loader);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_plugin_loader_pending_apps_add (GsPluginLoader *plugin_loader,
+ GsPluginLoaderHelper *helper)
+{
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->pending_apps_mutex);
+
+ g_assert (gs_app_list_length (list) > 0);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ g_ptr_array_add (priv->pending_apps, g_object_ref (app));
+ /* make sure the progress is properly initialized */
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+ }
+ g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+}
+
+static void
+gs_plugin_loader_pending_apps_remove (GsPluginLoader *plugin_loader,
+ GsPluginLoaderHelper *helper)
+{
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->pending_apps_mutex);
+
+ g_assert (gs_app_list_length (list) > 0);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ g_ptr_array_remove (priv->pending_apps, app);
+
+ /* check the app is not still in an action helper */
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_REMOVING:
+ g_warning ("application %s left in %s helper",
+ gs_app_get_unique_id (app),
+ as_app_state_to_string (gs_app_get_state (app)));
+ gs_app_set_state (app, AS_APP_STATE_UNKNOWN);
+ break;
+ default:
+ break;
+ }
+
+ }
+ g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+}
+
+static gboolean
+load_install_queue (GsPluginLoader *plugin_loader, GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autofree gchar *contents = NULL;
+ g_autofree gchar *file = NULL;
+ g_auto(GStrv) names = NULL;
+ g_autoptr(GsAppList) list = NULL;
+
+ /* load from file */
+ file = g_build_filename (g_get_user_data_dir (),
+ "gnome-software",
+ "install-queue",
+ NULL);
+ if (!g_file_test (file, G_FILE_TEST_EXISTS))
+ return TRUE;
+ g_debug ("loading install queue from %s", file);
+ if (!g_file_get_contents (file, &contents, NULL, error))
+ return FALSE;
+
+ /* add to GsAppList, deduplicating if required */
+ list = gs_app_list_new ();
+ names = g_strsplit (contents, "\n", 0);
+ for (guint i = 0; names[i] != NULL; i++) {
+ g_autoptr(GsApp) app = NULL;
+ if (strlen (names[i]) == 0)
+ continue;
+ app = gs_app_new (names[i]);
+ gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL);
+ gs_app_list_add (list, app);
+ }
+
+ /* add to pending list */
+ g_mutex_lock (&priv->pending_apps_mutex);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ g_debug ("adding pending app %s", gs_app_get_unique_id (app));
+ g_ptr_array_add (priv->pending_apps, g_object_ref (app));
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ /* refine */
+ if (gs_app_list_length (list) > 0) {
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_run_refine (helper, list, NULL, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+save_install_queue (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GPtrArray *pending_apps;
+ gboolean ret;
+ gint i;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GString) s = NULL;
+ g_autofree gchar *file = NULL;
+
+ s = g_string_new ("");
+ pending_apps = priv->pending_apps;
+ g_mutex_lock (&priv->pending_apps_mutex);
+ for (i = (gint) pending_apps->len - 1; i >= 0; i--) {
+ GsApp *app;
+ app = g_ptr_array_index (pending_apps, i);
+ if (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL) {
+ g_string_append (s, gs_app_get_id (app));
+ g_string_append_c (s, '\n');
+ }
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ /* save file */
+ file = g_build_filename (g_get_user_data_dir (),
+ "gnome-software",
+ "install-queue",
+ NULL);
+ if (!gs_mkdir_parent (file, &error)) {
+ g_warning ("failed to create dir for %s: %s",
+ file, error->message);
+ return;
+ }
+ g_debug ("saving install queue to %s", file);
+ ret = g_file_set_contents (file, s->str, (gssize) s->len, &error);
+ if (!ret)
+ g_warning ("failed to save install queue: %s", error->message);
+}
+
+static void
+add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsAppList *addons;
+ guint i;
+ guint id;
+
+ /* queue the app itself */
+ g_mutex_lock (&priv->pending_apps_mutex);
+ g_ptr_array_add (priv->pending_apps, g_object_ref (app));
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL);
+ id = g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+ g_source_set_name_by_id (id, "[gnome-software] emit_pending_apps_idle");
+ save_install_queue (plugin_loader);
+
+ /* recursively queue any addons */
+ addons = gs_app_get_addons (app);
+ for (i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon = gs_app_list_index (addons, i);
+ if (gs_app_get_to_be_installed (addon))
+ add_app_to_install_queue (plugin_loader, addon);
+ }
+}
+
+static gboolean
+remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsAppList *addons;
+ gboolean ret;
+ guint i;
+ guint id;
+
+ g_mutex_lock (&priv->pending_apps_mutex);
+ ret = g_ptr_array_remove (priv->pending_apps, app);
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ if (ret) {
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ id = g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+ g_source_set_name_by_id (id, "[gnome-software] emit_pending_apps_idle");
+ save_install_queue (plugin_loader);
+
+ /* recursively remove any queued addons */
+ addons = gs_app_get_addons (app);
+ for (i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon = gs_app_list_index (addons, i);
+ remove_app_from_install_queue (plugin_loader, addon);
+ }
+ }
+
+ return ret;
+}
+
+/******************************************************************************/
+
+gboolean
+gs_plugin_loader_get_allow_updates (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GHashTableIter iter;
+ gpointer value;
+
+ /* nothing */
+ if (g_hash_table_size (priv->disallow_updates) == 0)
+ return TRUE;
+
+ /* list */
+ g_hash_table_iter_init (&iter, priv->disallow_updates);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ const gchar *reason = value;
+ g_debug ("managed updates inhibited by %s", reason);
+ }
+ return FALSE;
+}
+
+GsAppList *
+gs_plugin_loader_get_pending (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsAppList *array;
+ guint i;
+
+ array = gs_app_list_new ();
+ g_mutex_lock (&priv->pending_apps_mutex);
+ for (i = 0; i < priv->pending_apps->len; i++) {
+ GsApp *app = g_ptr_array_index (priv->pending_apps, i);
+ gs_app_list_add (array, app);
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ return array;
+}
+
+gboolean
+gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name)
+{
+ GsPlugin *plugin;
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, plugin_name);
+ if (plugin == NULL)
+ return FALSE;
+ return gs_plugin_get_enabled (plugin);
+}
+
+/**
+ * gs_plugin_loader_get_events:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Gets all plugin events, even ones that are not active or visible anymore.
+ *
+ * Returns: (transfer container) (element-type GsPluginEvent): events
+ **/
+GPtrArray *
+gs_plugin_loader_get_events (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GPtrArray *events = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* just add everything */
+ g_hash_table_iter_init (&iter, priv->events_by_id);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *id = key;
+ GsPluginEvent *event = value;
+ if (event == NULL) {
+ g_warning ("failed to get event for '%s'", id);
+ continue;
+ }
+ g_ptr_array_add (events, g_object_ref (event));
+ }
+ return events;
+}
+
+/**
+ * gs_plugin_loader_get_event_default:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Gets an active plugin event where active means that it was not been
+ * already dismissed by the user.
+ *
+ * Returns: (transfer full): a #GsPluginEvent, or %NULL for none
+ **/
+GsPluginEvent *
+gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* just add everything */
+ g_hash_table_iter_init (&iter, priv->events_by_id);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *id = key;
+ GsPluginEvent *event = value;
+ if (event == NULL) {
+ g_warning ("failed to get event for '%s'", id);
+ continue;
+ }
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID))
+ return g_object_ref (event);
+ }
+ return NULL;
+}
+
+/**
+ * gs_plugin_loader_remove_events:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Removes all plugin events from the loader. This function should only be
+ * called from the self tests.
+ **/
+void
+gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+ g_hash_table_remove_all (priv->events_by_id);
+}
+
+static void
+gs_plugin_loader_report_event_cb (GsPlugin *plugin,
+ GsPluginEvent *event,
+ GsPluginLoader *plugin_loader)
+{
+ if (gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE))
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
+ gs_plugin_loader_add_event (plugin_loader, event);
+}
+
+static void
+gs_plugin_loader_allow_updates_cb (GsPlugin *plugin,
+ gboolean allow_updates,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ gboolean changed = FALSE;
+
+ /* plugin now allowing gnome-software to show updates panel */
+ if (allow_updates) {
+ if (g_hash_table_remove (priv->disallow_updates, plugin)) {
+ g_debug ("plugin %s no longer inhibited managed updates",
+ gs_plugin_get_name (plugin));
+ changed = TRUE;
+ }
+
+ /* plugin preventing the updates panel from being shown */
+ } else {
+ if (g_hash_table_replace (priv->disallow_updates,
+ (gpointer) plugin,
+ (gpointer) gs_plugin_get_name (plugin))) {
+ g_debug ("plugin %s inhibited managed updates",
+ gs_plugin_get_name (plugin));
+ changed = TRUE;
+ }
+ }
+
+ /* notify display layer */
+ if (changed)
+ g_object_notify (G_OBJECT (plugin_loader), "allow-updates");
+}
+
+static void
+gs_plugin_loader_status_changed_cb (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginStatus status,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* nothing specific */
+ if (app == NULL || gs_app_get_id (app) == NULL) {
+ if (priv->global_status_last != status) {
+ g_debug ("emitting global %s",
+ gs_plugin_status_to_string (status));
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_STATUS_CHANGED],
+ 0, app, status);
+ priv->global_status_last = status;
+ }
+ return;
+ }
+
+ /* a specific app */
+ g_debug ("emitting %s(%s)",
+ gs_plugin_status_to_string (status),
+ gs_app_get_id (app));
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_STATUS_CHANGED],
+ 0, app, status);
+}
+
+static void
+gs_plugin_loader_basic_auth_start_cb (GsPlugin *plugin,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data,
+ GsPluginLoader *plugin_loader)
+{
+ g_debug ("emitting basic-auth-start %s", realm);
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_BASIC_AUTH_START], 0,
+ remote,
+ realm,
+ callback,
+ user_data);
+}
+
+static gboolean
+gs_plugin_loader_job_actions_changed_delay_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* notify shells */
+ g_debug ("updates-changed");
+ g_signal_emit (plugin_loader, signals[SIGNAL_UPDATES_CHANGED], 0);
+ priv->updates_changed_id = 0;
+ priv->updates_changed_cnt = 0;
+
+ g_object_unref (plugin_loader);
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_job_actions_changed_cb (GsPlugin *plugin, GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ priv->updates_changed_cnt++;
+}
+
+static void
+gs_plugin_loader_updates_changed (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->updates_changed_id != 0)
+ return;
+ priv->updates_changed_id =
+ g_timeout_add_seconds (GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY,
+ gs_plugin_loader_job_actions_changed_delay_cb,
+ g_object_ref (plugin_loader));
+}
+
+static gboolean
+gs_plugin_loader_reload_delay_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* notify shells */
+ g_debug ("emitting ::reload");
+ g_signal_emit (plugin_loader, signals[SIGNAL_RELOAD], 0);
+ priv->reload_id = 0;
+
+ g_object_unref (plugin_loader);
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_reload_cb (GsPlugin *plugin,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->reload_id != 0)
+ return;
+ priv->reload_id =
+ g_timeout_add_seconds (GS_PLUGIN_LOADER_RELOAD_DELAY,
+ gs_plugin_loader_reload_delay_cb,
+ g_object_ref (plugin_loader));
+}
+
+static void
+gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
+ const gchar *filename)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsPlugin *plugin;
+ g_autoptr(GError) error = NULL;
+
+ /* create plugin from file */
+ plugin = gs_plugin_create (filename, &error);
+ if (plugin == NULL) {
+ g_warning ("Failed to load %s: %s", filename, error->message);
+ return;
+ }
+ g_signal_connect (plugin, "updates-changed",
+ G_CALLBACK (gs_plugin_loader_job_actions_changed_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "reload",
+ G_CALLBACK (gs_plugin_loader_reload_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "status-changed",
+ G_CALLBACK (gs_plugin_loader_status_changed_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "basic-auth-start",
+ G_CALLBACK (gs_plugin_loader_basic_auth_start_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "report-event",
+ G_CALLBACK (gs_plugin_loader_report_event_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "allow-updates",
+ G_CALLBACK (gs_plugin_loader_allow_updates_cb),
+ plugin_loader);
+ gs_plugin_set_soup_session (plugin, priv->soup_session);
+ gs_plugin_set_locale (plugin, priv->locale);
+ gs_plugin_set_language (plugin, priv->language);
+ gs_plugin_set_scale (plugin, gs_plugin_loader_get_scale (plugin_loader));
+ gs_plugin_set_network_monitor (plugin, priv->network_monitor);
+ g_debug ("opened plugin %s: %s", filename, gs_plugin_get_name (plugin));
+
+ /* add to array */
+ g_ptr_array_add (priv->plugins, plugin);
+}
+
+void
+gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader, guint scale)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* save globally, and update each plugin */
+ priv->scale = scale;
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ gs_plugin_set_scale (plugin, scale);
+ }
+}
+
+guint
+gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ return priv->scale;
+}
+
+void
+gs_plugin_loader_add_location (GsPluginLoader *plugin_loader, const gchar *location)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ for (guint i = 0; i < priv->locations->len; i++) {
+ const gchar *location_tmp = g_ptr_array_index (priv->locations, i);
+ if (g_strcmp0 (location_tmp, location) == 0)
+ return;
+ }
+ g_info ("adding plugin location %s", location);
+ g_ptr_array_add (priv->locations, g_strdup (location));
+}
+
+static gint
+gs_plugin_loader_plugin_sort_fn (gconstpointer a, gconstpointer b)
+{
+ GsPlugin *pa = *((GsPlugin **) a);
+ GsPlugin *pb = *((GsPlugin **) b);
+ if (gs_plugin_get_order (pa) < gs_plugin_get_order (pb))
+ return -1;
+ if (gs_plugin_get_order (pa) > gs_plugin_get_order (pb))
+ return 1;
+ return g_strcmp0 (gs_plugin_get_name (pa), gs_plugin_get_name (pb));
+}
+
+static void
+gs_plugin_loader_plugin_dir_changed_cb (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
+ g_autoptr(GError) error = NULL;
+
+ /* already triggered */
+ if (priv->plugin_dir_dirty)
+ return;
+
+ /* add app */
+ gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_SETUP);
+ app = gs_plugin_loader_app_create (plugin_loader,
+ "system/*/*/*/org.gnome.Software.desktop/*");
+ if (app != NULL)
+ gs_plugin_event_set_app (event, app);
+
+ /* add error */
+ g_set_error_literal (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_RESTART_REQUIRED,
+ "A restart is required");
+ gs_plugin_event_set_error (event, error);
+ gs_plugin_loader_add_event (plugin_loader, event);
+ priv->plugin_dir_dirty = TRUE;
+}
+
+void
+gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ gs_plugin_cache_invalidate (plugin);
+ }
+}
+
+/**
+ * gs_plugin_loader_setup_again:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Calls setup on each plugin. This should only be used from the self tests
+ * and in a controlled way.
+ */
+void
+gs_plugin_loader_setup_again (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsPluginAction actions[] = {
+ GS_PLUGIN_ACTION_DESTROY,
+ GS_PLUGIN_ACTION_INITIALIZE,
+ GS_PLUGIN_ACTION_SETUP,
+ GS_PLUGIN_ACTION_UNKNOWN };
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* clear global cache */
+ gs_plugin_loader_clear_caches (plugin_loader);
+
+ /* remove any events */
+ gs_plugin_loader_remove_events (plugin_loader);
+
+ /* call in order */
+ for (guint j = 0; actions[j] != GS_PLUGIN_ACTION_UNKNOWN; j++) {
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+
+ plugin_job = gs_plugin_job_newv (actions[j], NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ NULL, &error_local)) {
+ g_warning ("resetup of %s failed: %s",
+ gs_plugin_get_name (plugin),
+ error_local->message);
+ break;
+ }
+ if (actions[j] == GS_PLUGIN_ACTION_DESTROY)
+ gs_plugin_clear_data (plugin);
+ }
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ "setup-again",
+ NULL);
+ }
+#endif /* HAVE_SYSPROF */
+}
+
+static gint
+gs_plugin_loader_path_sort_fn (gconstpointer a, gconstpointer b)
+{
+ const gchar *sa = *((const gchar **) a);
+ const gchar *sb = *((const gchar **) b);
+ return g_strcmp0 (sa, sb);
+}
+
+static GPtrArray *
+gs_plugin_loader_find_plugins (const gchar *path, GError **error)
+{
+ const gchar *fn_tmp;
+ g_autoptr(GPtrArray) fns = g_ptr_array_new_with_free_func (g_free);
+ g_autoptr(GDir) dir = g_dir_open (path, 0, error);
+ if (dir == NULL)
+ return NULL;
+ while ((fn_tmp = g_dir_read_name (dir)) != NULL) {
+ if (!g_str_has_suffix (fn_tmp, ".so"))
+ continue;
+ g_ptr_array_add (fns, g_build_filename (path, fn_tmp, NULL));
+ }
+ g_ptr_array_sort (fns, gs_plugin_loader_path_sort_fn);
+ return g_steal_pointer (&fns);
+}
+
+/**
+ * gs_plugin_loader_setup:
+ * @plugin_loader: a #GsPluginLoader
+ * @allowlist: list of plugin names, or %NULL
+ * @blocklist: list of plugin names, or %NULL
+ * @cancellable: A #GCancellable, or %NULL
+ * @error: A #GError, or %NULL
+ *
+ * Sets up the plugin loader ready for use.
+ *
+ * Returns: %TRUE for success
+ */
+gboolean
+gs_plugin_loader_setup (GsPluginLoader *plugin_loader,
+ gchar **allowlist,
+ gchar **blocklist,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ const gchar *plugin_name;
+ gboolean changes;
+ GPtrArray *deps;
+ GsPlugin *dep;
+ GsPlugin *plugin;
+ guint dep_loop_check = 0;
+ guint i;
+ guint j;
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* use the default, but this requires a 'make install' */
+ if (priv->locations->len == 0) {
+ g_autofree gchar *filename = NULL;
+ filename = g_strdup_printf ("gs-plugins-%s", GS_PLUGIN_API_VERSION);
+ g_ptr_array_add (priv->locations, g_build_filename (LIBDIR, filename, NULL));
+ }
+
+ for (i = 0; i < priv->locations->len; i++) {
+ GFileMonitor *monitor;
+ const gchar *location = g_ptr_array_index (priv->locations, i);
+ g_autoptr(GFile) plugin_dir = g_file_new_for_path (location);
+ monitor = g_file_monitor_directory (plugin_dir,
+ G_FILE_MONITOR_NONE,
+ cancellable,
+ error);
+ if (monitor == NULL)
+ return FALSE;
+ g_signal_connect (monitor, "changed",
+ G_CALLBACK (gs_plugin_loader_plugin_dir_changed_cb), plugin_loader);
+ g_ptr_array_add (priv->file_monitors, monitor);
+ }
+
+ /* search for plugins */
+ for (i = 0; i < priv->locations->len; i++) {
+ const gchar *location = g_ptr_array_index (priv->locations, i);
+ g_autoptr(GPtrArray) fns = NULL;
+
+ /* search in the plugin directory for plugins */
+ g_debug ("searching for plugins in %s", location);
+ fns = gs_plugin_loader_find_plugins (location, error);
+ if (fns == NULL)
+ return FALSE;
+ for (j = 0; j < fns->len; j++) {
+ const gchar *fn = g_ptr_array_index (fns, j);
+ gs_plugin_loader_open_plugin (plugin_loader, fn);
+ }
+ }
+
+ /* optional allowlist */
+ if (allowlist != NULL) {
+ for (i = 0; i < priv->plugins->len; i++) {
+ gboolean ret;
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ ret = g_strv_contains ((const gchar * const *) allowlist,
+ gs_plugin_get_name (plugin));
+ if (!ret) {
+ g_debug ("%s not in allowlist, disabling",
+ gs_plugin_get_name (plugin));
+ }
+ gs_plugin_set_enabled (plugin, ret);
+ }
+ }
+
+ /* optional blocklist */
+ if (blocklist != NULL) {
+ for (i = 0; i < priv->plugins->len; i++) {
+ gboolean ret;
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ ret = g_strv_contains ((const gchar * const *) blocklist,
+ gs_plugin_get_name (plugin));
+ if (ret)
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+ }
+
+ /* run the plugins */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INITIALIZE, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_run_results (helper, cancellable, error))
+ return FALSE;
+
+ /* order by deps */
+ do {
+ changes = FALSE;
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_RUN_AFTER);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL) {
+ g_debug ("cannot find plugin '%s' "
+ "requested by '%s'",
+ plugin_name,
+ gs_plugin_get_name (plugin));
+ continue;
+ }
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ if (gs_plugin_get_order (plugin) <= gs_plugin_get_order (dep)) {
+ gs_plugin_set_order (plugin, gs_plugin_get_order (dep) + 1);
+ changes = TRUE;
+ }
+ }
+ }
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_RUN_BEFORE);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL) {
+ g_debug ("cannot find plugin '%s' "
+ "requested by '%s'",
+ plugin_name,
+ gs_plugin_get_name (plugin));
+ continue;
+ }
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ if (gs_plugin_get_order (plugin) >= gs_plugin_get_order (dep)) {
+ gs_plugin_set_order (dep, gs_plugin_get_order (plugin) + 1);
+ changes = TRUE;
+ }
+ }
+ }
+
+ /* check we're not stuck */
+ if (dep_loop_check++ > 100) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ "got stuck in dep loop");
+ return FALSE;
+ }
+ } while (changes);
+
+ /* check for conflicts */
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_CONFLICTS);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL)
+ continue;
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ g_debug ("disabling %s as conflicts with %s",
+ gs_plugin_get_name (dep),
+ gs_plugin_get_name (plugin));
+ gs_plugin_set_enabled (dep, FALSE);
+ }
+ }
+
+ /* sort by order */
+ g_ptr_array_sort (priv->plugins,
+ gs_plugin_loader_plugin_sort_fn);
+
+ /* assign priority values */
+ do {
+ changes = FALSE;
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_BETTER_THAN);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL) {
+ g_debug ("cannot find plugin '%s' "
+ "requested by '%s'",
+ plugin_name,
+ gs_plugin_get_name (plugin));
+ continue;
+ }
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ if (gs_plugin_get_priority (plugin) <= gs_plugin_get_priority (dep)) {
+ gs_plugin_set_priority (plugin, gs_plugin_get_priority (dep) + 1);
+ changes = TRUE;
+ }
+ }
+ }
+
+ /* check we're not stuck */
+ if (dep_loop_check++ > 100) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ "got stuck in priority loop");
+ return FALSE;
+ }
+ } while (changes);
+
+ /* run setup */
+ gs_plugin_job_set_action (helper->plugin_job, GS_PLUGIN_ACTION_SETUP);
+ helper->function_name = "gs_plugin_setup";
+ for (i = 0; i < priv->plugins->len; i++) {
+ g_autoptr(GError) error_local = NULL;
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, &error_local)) {
+ g_debug ("disabling %s as setup failed: %s",
+ gs_plugin_get_name (plugin),
+ error_local->message);
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+ }
+
+ /* now we can load the install-queue */
+ if (!load_install_queue (plugin_loader, error))
+ return FALSE;
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ "setup",
+ NULL);
+ }
+#endif /* HAVE_SYSPROF */
+
+ return TRUE;
+}
+
+void
+gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GString) str_enabled = g_string_new (NULL);
+ g_autoptr(GString) str_disabled = g_string_new (NULL);
+
+ /* print what the priorities are if verbose */
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ GString *str = gs_plugin_get_enabled (plugin) ? str_enabled : str_disabled;
+ g_string_append_printf (str, "%s, ", gs_plugin_get_name (plugin));
+ g_debug ("[%s]\t%u\t->\t%s",
+ gs_plugin_get_enabled (plugin) ? "enabled" : "disabld",
+ gs_plugin_get_order (plugin),
+ gs_plugin_get_name (plugin));
+ }
+ if (str_enabled->len > 2)
+ g_string_truncate (str_enabled, str_enabled->len - 2);
+ if (str_disabled->len > 2)
+ g_string_truncate (str_disabled, str_disabled->len - 2);
+ g_info ("enabled plugins: %s", str_enabled->str);
+ g_info ("disabled plugins: %s", str_disabled->str);
+}
+
+static void
+gs_plugin_loader_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ switch (prop_id) {
+ case PROP_EVENTS:
+ g_value_set_pointer (value, priv->events_by_id);
+ break;
+ case PROP_ALLOW_UPDATES:
+ g_value_set_boolean (value, gs_plugin_loader_get_allow_updates (plugin_loader));
+ break;
+ case PROP_NETWORK_AVAILABLE:
+ g_value_set_boolean (value, gs_plugin_loader_get_network_available (plugin_loader));
+ break;
+ case PROP_NETWORK_METERED:
+ g_value_set_boolean (value, gs_plugin_loader_get_network_metered (plugin_loader));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_loader_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ switch (prop_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_loader_dispose (GObject *object)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ if (priv->plugins != NULL) {
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DESTROY, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ gs_plugin_loader_run_results (helper, NULL, NULL);
+ g_clear_pointer (&priv->plugins, g_ptr_array_unref);
+ }
+ if (priv->updates_changed_id != 0) {
+ g_source_remove (priv->updates_changed_id);
+ priv->updates_changed_id = 0;
+ }
+ if (priv->network_changed_handler != 0) {
+ g_signal_handler_disconnect (priv->network_monitor,
+ priv->network_changed_handler);
+ priv->network_changed_handler = 0;
+ }
+ if (priv->network_available_notify_handler != 0) {
+ g_signal_handler_disconnect (priv->network_monitor,
+ priv->network_available_notify_handler);
+ priv->network_available_notify_handler = 0;
+ }
+ if (priv->network_metered_notify_handler != 0) {
+ g_signal_handler_disconnect (priv->network_monitor,
+ priv->network_metered_notify_handler);
+ priv->network_metered_notify_handler = 0;
+ }
+ if (priv->queued_ops_pool != NULL) {
+ /* stop accepting more requests and wait until any currently
+ * running ones are finished */
+ g_thread_pool_free (priv->queued_ops_pool, TRUE, TRUE);
+ priv->queued_ops_pool = NULL;
+ }
+ g_clear_object (&priv->network_monitor);
+ g_clear_object (&priv->soup_session);
+ g_clear_object (&priv->settings);
+ g_clear_pointer (&priv->pending_apps, g_ptr_array_unref);
+#ifdef HAVE_SYSPROF
+ g_clear_pointer (&priv->sysprof_writer, sysprof_capture_writer_unref);
+#endif
+
+ G_OBJECT_CLASS (gs_plugin_loader_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_loader_finalize (GObject *object)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ g_strfreev (priv->compatible_projects);
+ g_ptr_array_unref (priv->locations);
+ g_free (priv->locale);
+ g_free (priv->language);
+ g_ptr_array_unref (priv->file_monitors);
+ g_hash_table_unref (priv->events_by_id);
+ g_hash_table_unref (priv->disallow_updates);
+
+ g_mutex_clear (&priv->pending_apps_mutex);
+ g_mutex_clear (&priv->events_by_id_mutex);
+
+ G_OBJECT_CLASS (gs_plugin_loader_parent_class)->finalize (object);
+}
+
+static void
+gs_plugin_loader_class_init (GsPluginLoaderClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gs_plugin_loader_get_property;
+ object_class->set_property = gs_plugin_loader_set_property;
+ object_class->dispose = gs_plugin_loader_dispose;
+ object_class->finalize = gs_plugin_loader_finalize;
+
+ pspec = g_param_spec_string ("events", NULL, NULL,
+ NULL,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_EVENTS, pspec);
+
+ pspec = g_param_spec_boolean ("allow-updates", NULL, NULL,
+ TRUE,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_ALLOW_UPDATES, pspec);
+
+ pspec = g_param_spec_boolean ("network-available", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_NETWORK_AVAILABLE, pspec);
+
+ pspec = g_param_spec_boolean ("network-metered", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_NETWORK_METERED, pspec);
+
+ signals [SIGNAL_STATUS_CHANGED] =
+ g_signal_new ("status-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, status_changed),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_UINT);
+ signals [SIGNAL_PENDING_APPS_CHANGED] =
+ g_signal_new ("pending-apps-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, pending_apps_changed),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals [SIGNAL_UPDATES_CHANGED] =
+ g_signal_new ("updates-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, updates_changed),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals [SIGNAL_RELOAD] =
+ g_signal_new ("reload",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, reload),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals [SIGNAL_BASIC_AUTH_START] =
+ g_signal_new ("basic-auth-start",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, basic_auth_start),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER);
+}
+
+static void
+gs_plugin_loader_allow_updates_recheck (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (g_settings_get_boolean (priv->settings, "allow-updates")) {
+ g_hash_table_remove (priv->disallow_updates, plugin_loader);
+ } else {
+ g_hash_table_insert (priv->disallow_updates,
+ (gpointer) plugin_loader,
+ (gpointer) "GSettings");
+ }
+}
+
+static void
+gs_plugin_loader_settings_changed_cb (GSettings *settings,
+ const gchar *key,
+ GsPluginLoader *plugin_loader)
+{
+ if (g_strcmp0 (key, "allow-updates") == 0)
+ gs_plugin_loader_allow_updates_recheck (plugin_loader);
+}
+
+static gint
+get_max_parallel_ops (void)
+{
+ guint mem_total = gs_utils_get_memory_total ();
+ if (mem_total == 0)
+ return 8;
+ /* allow 1 op per GB of memory */
+ return (gint) MAX (round((gdouble) mem_total / 1024), 1.0);
+}
+
+static void
+gs_plugin_loader_init (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ const gchar *tmp;
+ gchar *match;
+ gchar **projects;
+ guint i;
+
+#ifdef HAVE_SYSPROF
+ priv->sysprof_writer = sysprof_capture_writer_new_from_env (0);
+#endif /* HAVE_SYSPROF */
+
+ priv->scale = 1;
+ priv->plugins = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ priv->pending_apps = g_ptr_array_new_with_free_func ((GFreeFunc) g_object_unref);
+ priv->queued_ops_pool = g_thread_pool_new (gs_plugin_loader_process_in_thread_pool_cb,
+ NULL,
+ get_max_parallel_ops (),
+ FALSE,
+ NULL);
+ priv->file_monitors = g_ptr_array_new_with_free_func ((GFreeFunc) g_object_unref);
+ priv->locations = g_ptr_array_new_with_free_func (g_free);
+ priv->settings = g_settings_new ("org.gnome.software");
+ g_signal_connect (priv->settings, "changed",
+ G_CALLBACK (gs_plugin_loader_settings_changed_cb), plugin_loader);
+ priv->events_by_id = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash,
+ (GEqualFunc) as_utils_unique_id_equal,
+ g_free,
+ (GDestroyNotify) g_object_unref);
+
+ /* share a soup session (also disable the double-compression) */
+ priv->soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (),
+ SOUP_SESSION_TIMEOUT, 10,
+ NULL);
+
+ /* get the locale */
+ tmp = g_getenv ("GS_SELF_TEST_LOCALE");
+ if (tmp != NULL) {
+ g_debug ("using self test locale of %s", tmp);
+ priv->locale = g_strdup (tmp);
+ } else {
+ priv->locale = g_strdup (setlocale (LC_MESSAGES, NULL));
+ }
+
+ /* the settings key sets the initial override */
+ priv->disallow_updates = g_hash_table_new (g_direct_hash, g_direct_equal);
+ gs_plugin_loader_allow_updates_recheck (plugin_loader);
+
+ /* get the language from the locale (i.e. strip the territory, codeset
+ * and modifier) */
+ priv->language = g_strdup (priv->locale);
+ match = strpbrk (priv->language, "._@");
+ if (match != NULL)
+ *match = '\0';
+
+ g_debug ("Using locale = %s, language = %s", priv->locale, priv->language);
+
+ g_mutex_init (&priv->pending_apps_mutex);
+ g_mutex_init (&priv->events_by_id_mutex);
+
+ /* monitor the network as the many UI operations need the network */
+ gs_plugin_loader_monitor_network (plugin_loader);
+
+ /* by default we only show project-less apps or compatible projects */
+ tmp = g_getenv ("GNOME_SOFTWARE_COMPATIBLE_PROJECTS");
+ if (tmp == NULL) {
+ projects = g_settings_get_strv (priv->settings,
+ "compatible-projects");
+ } else {
+ projects = g_strsplit (tmp, ",", -1);
+ }
+ for (i = 0; projects[i] != NULL; i++)
+ g_debug ("compatible-project: %s", projects[i]);
+ priv->compatible_projects = projects;
+}
+
+/**
+ * gs_plugin_loader_new:
+ *
+ * Return value: a new GsPluginLoader object.
+ **/
+GsPluginLoader *
+gs_plugin_loader_new (void)
+{
+ GsPluginLoader *plugin_loader;
+ plugin_loader = g_object_new (GS_TYPE_PLUGIN_LOADER, NULL);
+ return GS_PLUGIN_LOADER (plugin_loader);
+}
+
+static void
+gs_plugin_loader_app_installed_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ gboolean ret;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app = GS_APP (user_data);
+
+ ret = gs_plugin_loader_job_action_finish (plugin_loader,
+ res,
+ &error);
+ if (!ret) {
+ remove_app_from_install_queue (plugin_loader, app);
+ g_warning ("failed to install %s: %s",
+ gs_app_get_unique_id (app), error->message);
+ }
+}
+
+gboolean
+gs_plugin_loader_get_network_available (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-available=TRUE");
+ return TRUE;
+ }
+ return g_network_monitor_get_network_available (priv->network_monitor);
+}
+
+gboolean
+gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-metered=FALSE");
+ return FALSE;
+ }
+ return g_network_monitor_get_network_metered (priv->network_monitor);
+}
+
+static void
+gs_plugin_loader_network_changed_cb (GNetworkMonitor *monitor,
+ gboolean available,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ gboolean metered = g_network_monitor_get_network_metered (priv->network_monitor);
+
+ g_debug ("network status change: %s [%s]",
+ available ? "online" : "offline",
+ metered ? "metered" : "unmetered");
+
+ g_object_notify (G_OBJECT (plugin_loader), "network-available");
+ g_object_notify (G_OBJECT (plugin_loader), "network-metered");
+
+ if (available && !metered) {
+ g_autoptr(GsAppList) queue = NULL;
+ g_mutex_lock (&priv->pending_apps_mutex);
+ queue = gs_app_list_new ();
+ for (guint i = 0; i < priv->pending_apps->len; i++) {
+ GsApp *app = g_ptr_array_index (priv->pending_apps, i);
+ if (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL)
+ gs_app_list_add (queue, app);
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+ for (guint i = 0; i < gs_app_list_length (queue); i++) {
+ GsApp *app = gs_app_list_index (queue, i);
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ NULL,
+ gs_plugin_loader_app_installed_cb,
+ g_object_ref (app));
+ }
+ }
+}
+
+static void
+gs_plugin_loader_network_available_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GNetworkMonitor *monitor = G_NETWORK_MONITOR (obj);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+
+ gs_plugin_loader_network_changed_cb (monitor, g_network_monitor_get_network_available (monitor), plugin_loader);
+}
+
+static void
+gs_plugin_loader_network_metered_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GNetworkMonitor *monitor = G_NETWORK_MONITOR (obj);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+
+ gs_plugin_loader_network_changed_cb (monitor, g_network_monitor_get_network_available (monitor), plugin_loader);
+}
+
+static void
+gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GNetworkMonitor *network_monitor;
+
+ network_monitor = g_network_monitor_get_default ();
+ if (network_monitor == NULL || priv->network_changed_handler != 0)
+ return;
+ priv->network_monitor = g_object_ref (network_monitor);
+
+ priv->network_changed_handler =
+ g_signal_connect (priv->network_monitor, "network-changed",
+ G_CALLBACK (gs_plugin_loader_network_changed_cb), plugin_loader);
+ priv->network_available_notify_handler =
+ g_signal_connect (priv->network_monitor, "notify::network-available",
+ G_CALLBACK (gs_plugin_loader_network_available_notify_cb), plugin_loader);
+ priv->network_metered_notify_handler =
+ g_signal_connect (priv->network_monitor, "notify::network-metered",
+ G_CALLBACK (gs_plugin_loader_network_metered_notify_cb), plugin_loader);
+
+ gs_plugin_loader_network_changed_cb (priv->network_monitor,
+ g_network_monitor_get_network_available (priv->network_monitor),
+ plugin_loader);
+}
+
+/******************************************************************************/
+
+static AsIcon *
+_gs_app_get_icon_by_kind (GsApp *app, AsIconKind kind)
+{
+ GPtrArray *icons = gs_app_get_icons (app);
+ guint i;
+ for (i = 0; i < icons->len; i++) {
+ AsIcon *ic = g_ptr_array_index (icons, i);
+ if (as_icon_get_kind (ic) == kind)
+ return ic;
+ }
+ return NULL;
+}
+
+static void
+generic_update_cancelled_cb (GCancellable *cancellable, gpointer data)
+{
+ GCancellable *app_cancellable = G_CANCELLABLE (data);
+ g_cancellable_cancel (app_cancellable);
+}
+
+static gboolean
+gs_plugin_loader_generic_update (GsPluginLoader *plugin_loader,
+ GsPluginLoaderHelper *helper,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ guint cancel_handler_id = 0;
+ GsAppList *list;
+
+ /* run each plugin, per-app version */
+ list = gs_plugin_job_get_list (helper->plugin_job);
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPluginActionFunc plugin_app_func = NULL;
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_utils_error_convert_gio (error);
+ return FALSE;
+ }
+ plugin_app_func = gs_plugin_get_symbol (plugin, helper->function_name);
+ if (plugin_app_func == NULL)
+ continue;
+
+ /* for each app */
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GCancellable *app_cancellable;
+ GsApp *app = gs_app_list_index (list, j);
+ gboolean ret;
+ g_autoptr(GError) error_local = NULL;
+
+ /* if the whole operation should be cancelled */
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return FALSE;
+
+ /* already installed? */
+ if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED)
+ continue;
+
+ /* make sure that the app update is cancelled when the whole op is cancelled */
+ app_cancellable = gs_app_get_cancellable (app);
+ cancel_handler_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (generic_update_cancelled_cb),
+ g_object_ref (app_cancellable),
+ g_object_unref);
+
+ gs_plugin_job_set_app (helper->plugin_job, app);
+ ret = plugin_app_func (plugin, app, app_cancellable, &error_local);
+ g_cancellable_disconnect (cancellable, cancel_handler_id);
+
+ if (!ret) {
+ if (!gs_plugin_error_handle_failure (helper,
+ plugin,
+ error_local,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+ helper->anything_ran = TRUE;
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+ gs_utils_set_online_updates_timestamp (priv->settings);
+ return TRUE;
+}
+
+static void
+gs_plugin_loader_process_thread_cb (GTask *task,
+ gpointer object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GError *error = NULL;
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) task_data;
+ GsAppListFilterFlags dedupe_flags;
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsPluginRefineFlags filter_flags;
+ GsPluginRefineFlags refine_flags;
+ gboolean add_to_pending_array = FALSE;
+ guint max_results;
+ GsAppListSortFunc sort_func;
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* these change the pending count on the installed panel */
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ add_to_pending_array = TRUE;
+ break;
+ default:
+ break;
+ }
+
+ /* add to pending list */
+ if (add_to_pending_array)
+ gs_plugin_loader_pending_apps_add (plugin_loader, helper);
+
+ /* run each plugin */
+ if (action != GS_PLUGIN_ACTION_REFINE) {
+ if (!gs_plugin_loader_run_results (helper, cancellable, &error)) {
+ if (add_to_pending_array) {
+ gs_app_set_state_recover (gs_plugin_job_get_app (helper->plugin_job));
+ gs_plugin_loader_pending_apps_remove (plugin_loader, helper);
+ }
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ }
+
+ /* run per-app version */
+ if (action == GS_PLUGIN_ACTION_UPDATE) {
+ helper->function_name = "gs_plugin_update_app";
+ if (!gs_plugin_loader_generic_update (plugin_loader, helper,
+ cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ } else if (action == GS_PLUGIN_ACTION_DOWNLOAD) {
+ helper->function_name = "gs_plugin_download_app";
+ if (!gs_plugin_loader_generic_update (plugin_loader, helper,
+ cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ }
+
+ if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER)
+ gs_utils_set_online_updates_timestamp (priv->settings);
+
+ /* remove from pending list */
+ if (add_to_pending_array)
+ gs_plugin_loader_pending_apps_remove (plugin_loader, helper);
+
+ /* some functions are really required for proper operation */
+ switch (action) {
+ case GS_PLUGIN_ACTION_DESTROY:
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ case GS_PLUGIN_ACTION_INITIALIZE:
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ case GS_PLUGIN_ACTION_LAUNCH:
+ case GS_PLUGIN_ACTION_REFRESH:
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SETUP:
+ case GS_PLUGIN_ACTION_UPDATE:
+ if (!helper->anything_ran) {
+ g_set_error (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no plugin could handle %s",
+ gs_plugin_action_to_string (action));
+ g_task_return_error (task, error);
+ return;
+ }
+ break;
+ case GS_PLUGIN_ACTION_REFINE:
+ break;
+ default:
+ if (!helper->anything_ran) {
+ g_debug ("no plugin could handle %s",
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ }
+
+ /* unstage addons */
+ if (add_to_pending_array) {
+ GsAppList *addons;
+ addons = gs_app_get_addons (gs_plugin_job_get_app (helper->plugin_job));
+ for (guint i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon = gs_app_list_index (addons, i);
+ if (gs_app_get_to_be_installed (addon))
+ gs_app_set_to_be_installed (addon, FALSE);
+ }
+ }
+
+ /* modify the local app */
+ switch (action) {
+ case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
+ gs_app_add_review (gs_plugin_job_get_app (helper->plugin_job), gs_plugin_job_get_review (helper->plugin_job));
+ break;
+ case GS_PLUGIN_ACTION_REVIEW_REMOVE:
+ gs_app_remove_review (gs_plugin_job_get_app (helper->plugin_job), gs_plugin_job_get_review (helper->plugin_job));
+ break;
+ default:
+ break;
+ }
+
+ /* refine with enough data so that the sort_func in
+ * gs_plugin_loader_job_sorted_truncation() can do what it needs */
+ filter_flags = gs_plugin_job_get_filter_flags (helper->plugin_job);
+ max_results = gs_plugin_job_get_max_results (helper->plugin_job);
+ sort_func = gs_plugin_job_get_sort_func (helper->plugin_job);
+ if (filter_flags > 0 && max_results > 0 && sort_func != NULL) {
+ g_autoptr(GsPluginLoaderHelper) helper2 = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "list", list,
+ "refine-flags", filter_flags,
+ NULL);
+ helper2 = gs_plugin_loader_helper_new (helper->plugin_loader, plugin_job);
+ helper2->function_name_parent = helper->function_name;
+ g_debug ("running filter flags with early refine");
+ if (!gs_plugin_loader_run_refine_filter (helper2, list,
+ filter_flags,
+ cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ }
+
+ /* filter to reduce to a sane set */
+ gs_plugin_loader_job_sorted_truncation (helper);
+
+ /* set the local file on any of the returned results */
+ switch (action) {
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_local_file (app) == NULL)
+ gs_app_set_local_file (app, gs_plugin_job_get_file (helper->plugin_job));
+ }
+ default:
+ break;
+ }
+
+ /* pick up new source id */
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ gs_plugin_job_add_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION);
+ break;
+ default:
+ break;
+ }
+
+ /* run refine() on each one if required */
+ if (gs_plugin_job_get_refine_flags (helper->plugin_job) != 0) {
+ if (!gs_plugin_loader_run_refine (helper, list, cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ } else {
+ g_debug ("no refine flags set for transaction");
+ }
+
+ /* check the local files have an icon set */
+ switch (action) {
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (_gs_app_get_icon_by_kind (app, AS_ICON_KIND_STOCK) == NULL &&
+ _gs_app_get_icon_by_kind (app, AS_ICON_KIND_LOCAL) == NULL &&
+ _gs_app_get_icon_by_kind (app, AS_ICON_KIND_CACHED) == NULL) {
+ g_autoptr(AsIcon) ic = as_icon_new ();
+ as_icon_set_kind (ic, AS_ICON_KIND_STOCK);
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_HAS_SOURCE))
+ as_icon_set_name (ic, "x-package-repository");
+ else
+ as_icon_set_name (ic, "application-x-executable");
+ gs_app_add_icon (app, ic);
+ }
+ }
+
+ /* run refine() on each one again to pick up any icons */
+ refine_flags = gs_plugin_job_get_refine_flags (helper->plugin_job);
+ gs_plugin_job_set_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON);
+ if (!gs_plugin_loader_run_refine (helper, list, cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ /* restore the refine flags so that gs_app_list_filter sees the right thing */
+ gs_plugin_job_set_refine_flags (helper->plugin_job, refine_flags);
+ break;
+ default:
+ break;
+ }
+
+ /* filter package list */
+ switch (action) {
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ break;
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid_installed, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_FEATURED:
+ if (g_getenv ("GNOME_SOFTWARE_FEATURED") != NULL) {
+ gs_app_list_filter (list, gs_plugin_loader_featured_debug, NULL);
+ } else {
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid_updatable, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_non_compulsory, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ case GS_PLUGIN_ACTION_REFINE:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_POPULAR:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ default:
+ break;
+ }
+
+ /* only allow one result */
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP ||
+ action == GS_PLUGIN_ACTION_FILE_TO_APP) {
+ if (gs_app_list_length (list) == 0) {
+ g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_set_error (&error_local,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no application was created for %s", str);
+ event = gs_plugin_job_to_failed_event (helper->plugin_job, error_local);
+ gs_plugin_loader_add_event (plugin_loader, event);
+ g_task_return_error (task, g_steal_pointer (&error_local));
+ return;
+ }
+ if (gs_app_list_length (list) > 1) {
+ g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job);
+ g_debug ("more than one application was created for %s", str);
+ }
+ }
+
+ /* filter duplicates with priority, taking into account the source name
+ * & version, so we combine available updates with the installed app */
+ gs_app_list_filter (list, gs_plugin_loader_app_set_prio, plugin_loader);
+ dedupe_flags = gs_plugin_job_get_dedupe_flags (helper->plugin_job);
+ if (dedupe_flags != GS_APP_LIST_FILTER_FLAG_NONE)
+ gs_app_list_filter_duplicates (list, dedupe_flags);
+
+ /* sort these again as the refine may have added useful metadata */
+ gs_plugin_loader_job_sorted_truncation_again (helper);
+
+ /* if the plugin used updates-changed actually schedule it now */
+ if (priv->updates_changed_cnt > 0)
+ gs_plugin_loader_updates_changed (plugin_loader);
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = g_strconcat ("process-thread:", gs_plugin_action_to_string (action), NULL);
+ g_autofree gchar *sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* show elapsed time */
+ gs_plugin_loader_job_debug (helper);
+
+ /* success */
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+}
+
+static void
+gs_plugin_loader_process_in_thread_pool_cb (gpointer data,
+ gpointer user_data)
+{
+ GTask *task = data;
+ gpointer source_object = g_task_get_source_object (task);
+ gpointer task_data = g_task_get_task_data (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+
+ gs_ioprio_init ();
+
+ gs_plugin_loader_process_thread_cb (task, source_object, task_data, cancellable);
+ g_object_unref (task);
+}
+
+static gboolean
+gs_plugin_loader_job_timeout_cb (gpointer user_data)
+{
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+ /* call the cancellable */
+ g_debug ("cancelling job %s as it took longer than %u seconds",
+ helper->function_name,
+ gs_plugin_job_get_timeout (helper->plugin_job));
+ g_cancellable_cancel (helper->cancellable);
+
+ /* failed */
+ helper->timeout_triggered = TRUE;
+ helper->timeout_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_plugin_loader_cancelled_cb (GCancellable *cancellable, GsPluginLoaderHelper *helper)
+{
+ /* just proxy this forward */
+ g_debug ("Cancelling job with cancellable %p", helper->cancellable);
+ g_cancellable_cancel (helper->cancellable);
+}
+
+static void
+gs_plugin_loader_schedule_task (GsPluginLoader *plugin_loader,
+ GTask *task)
+{
+ GsPluginLoaderHelper *helper = g_task_get_task_data (task);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsApp *app = gs_plugin_job_get_app (helper->plugin_job);
+
+ if (app != NULL) {
+ /* set the pending-action to the app */
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ gs_app_set_pending_action (app, action);
+ }
+ g_thread_pool_push (priv->queued_ops_pool, g_object_ref (task), NULL);
+}
+
+/**
+ * gs_plugin_loader_job_process_async:
+ * @plugin_loader: A #GsPluginLoader
+ * @plugin_job: job to process
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: function to call when complete
+ * @user_data: user data to pass to @callback
+ *
+ * This method calls all plugins.
+ **/
+void
+gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAction action;
+ GsPluginLoaderHelper *helper;
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GCancellable) cancellable_job = g_cancellable_new ();
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_autofree gchar *task_name = NULL;
+#endif
+
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (GS_IS_PLUGIN_JOB (plugin_job));
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ action = gs_plugin_job_get_action (plugin_job);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ task_name = g_strdup_printf ("%s %s", G_STRFUNC, gs_plugin_action_to_string (action));
+#endif
+
+ /* check job has valid action */
+ if (action == GS_PLUGIN_ACTION_UNKNOWN) {
+ g_autofree gchar *job_str = gs_plugin_job_to_string (plugin_job);
+ task = g_task_new (plugin_loader, cancellable_job, callback, user_data);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_task_set_name (task, task_name);
+#endif
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "job has no valid action: %s", job_str);
+ return;
+ }
+
+ /* deal with the install queue */
+ if (action == GS_PLUGIN_ACTION_REMOVE) {
+ if (remove_app_from_install_queue (plugin_loader, gs_plugin_job_get_app (plugin_job))) {
+ GsAppList *list = gs_plugin_job_get_list (plugin_job);
+ task = g_task_new (plugin_loader, cancellable, callback, user_data);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_task_set_name (task, task_name);
+#endif
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+ return;
+ }
+ }
+
+ /* hardcoded, so resolve a set list */
+ if (action == GS_PLUGIN_ACTION_GET_POPULAR) {
+ g_auto(GStrv) apps = NULL;
+ if (g_getenv ("GNOME_SOFTWARE_POPULAR") != NULL) {
+ apps = g_strsplit (g_getenv ("GNOME_SOFTWARE_POPULAR"), ",", 0);
+ } else {
+ apps = g_settings_get_strv (priv->settings, "popular-overrides");
+ }
+ if (apps != NULL && g_strv_length (apps) > 0) {
+ GsAppList *list = gs_plugin_job_get_list (plugin_job);
+ for (guint i = 0; apps[i] != NULL; i++) {
+ g_autoptr(GsApp) app = gs_app_new (apps[i]);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ }
+ gs_plugin_job_set_action (plugin_job, GS_PLUGIN_ACTION_REFINE);
+ }
+ }
+
+ /* FIXME: the plugins should specify this, rather than hardcoding */
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME);
+ }
+
+ /* FIXME: this is probably a bug */
+ if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES ||
+ action == GS_PLUGIN_ACTION_GET_SOURCES) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION);
+ }
+
+ /* get alternates is unusual in that it needs an app input and a list
+ * output -- so undo the helpful app add in gs_plugin_job_set_app() */
+ if (action == GS_PLUGIN_ACTION_GET_ALTERNATES) {
+ GsAppList *list = gs_plugin_job_get_list (plugin_job);
+ gs_app_list_remove_all (list);
+ }
+
+ /* check required args */
+ task = g_task_new (plugin_loader, cancellable_job, callback, user_data);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_task_set_name (task, task_name);
+#endif
+
+ switch (action) {
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ if (gs_plugin_job_get_search (plugin_job) == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no valid search terms");
+ return;
+ }
+ break;
+ case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
+ case GS_PLUGIN_ACTION_REVIEW_UPVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_REPORT:
+ case GS_PLUGIN_ACTION_REVIEW_REMOVE:
+ case GS_PLUGIN_ACTION_REVIEW_DISMISS:
+ if (gs_plugin_job_get_review (plugin_job) == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no valid review object");
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ /* sorting fallbacks */
+ switch (action) {
+ case GS_PLUGIN_ACTION_SEARCH:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_match_value_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_kind_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_name_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_prio_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_DISTRO_UPDATES:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_version_cb);
+ }
+ break;
+ default:
+ break;
+ }
+
+ /* save helper */
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ g_task_set_task_data (task, helper, (GDestroyNotify) gs_plugin_loader_helper_free);
+
+ /* let the task cancel itself */
+ g_task_set_check_cancellable (task, FALSE);
+ g_task_set_return_on_cancel (task, FALSE);
+
+ /* pre-tokenize search */
+ if (action == GS_PLUGIN_ACTION_SEARCH) {
+ const gchar *search = gs_plugin_job_get_search (plugin_job);
+ helper->tokens = as_utils_search_tokenize (search);
+ if (helper->tokens == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "failed to tokenize %s", search);
+ return;
+ }
+ }
+
+ /* jobs always have a valid cancellable, so proxy the caller */
+ helper->cancellable = g_object_ref (cancellable_job);
+ g_debug ("Chaining cancellation from %p to %p", cancellable, cancellable_job);
+ if (cancellable != NULL) {
+ helper->cancellable_caller = g_object_ref (cancellable);
+ helper->cancellable_id =
+ g_cancellable_connect (helper->cancellable_caller,
+ G_CALLBACK (gs_plugin_loader_cancelled_cb),
+ helper, NULL);
+ }
+
+ /* set up a hang handler */
+ switch (action) {
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ case GS_PLUGIN_ACTION_GET_FEATURED:
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ case GS_PLUGIN_ACTION_GET_POPULAR:
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ helper->timeout_id =
+ g_timeout_add_seconds (gs_plugin_job_get_timeout (plugin_job),
+ gs_plugin_loader_job_timeout_cb,
+ helper);
+ break;
+ default:
+ break;
+ }
+
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_UPDATE:
+ case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD:
+ /* these actions must be performed by the thread pool because we
+ * want to limit the number of them running in parallel */
+ gs_plugin_loader_schedule_task (plugin_loader, task);
+ return;
+ default:
+ break;
+ }
+
+ /* run in a thread */
+ g_task_run_in_thread (task, gs_plugin_loader_process_thread_cb);
+}
+
+/******************************************************************************/
+
+/**
+ * gs_plugin_loader_get_plugin_supported:
+ * @plugin_loader: A #GsPluginLoader
+ * @function_name: a function name
+ *
+ * This function returns TRUE if the symbol is found in any enabled plugin.
+ */
+gboolean
+gs_plugin_loader_get_plugin_supported (GsPluginLoader *plugin_loader,
+ const gchar *function_name)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (gs_plugin_get_symbol (plugin, function_name) != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_plugin_loader_app_create:
+ * @plugin_loader: a #GsPluginLoader
+ * @unique_id: a unique_id
+ *
+ * Returns an application from the global cache, creating if required.
+ *
+ * Returns: (transfer full): a #GsApp
+ **/
+GsApp *
+gs_plugin_loader_app_create (GsPluginLoader *plugin_loader, const gchar *unique_id)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+
+ /* use the plugin loader to convert a wildcard app*/
+ app = gs_app_new (NULL);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_set_from_unique_id (app, unique_id);
+ gs_app_list_add (list, app);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_run_refine (helper, list, NULL, &error)) {
+ g_warning ("%s", error->message);
+ return NULL;
+ }
+
+ /* return the matching GsApp */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ if (g_strcmp0 (unique_id, gs_app_get_unique_id (app_tmp)) == 0)
+ return g_object_ref (app_tmp);
+ }
+
+ /* return the first returned app that's not a wildcard */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD)) {
+ g_debug ("returning imperfect match: %s != %s",
+ unique_id, gs_app_get_unique_id (app_tmp));
+ return g_object_ref (app_tmp);
+ }
+ }
+
+ /* does not exist */
+ g_warning ("failed to create an app for %s", unique_id);
+ return NULL;
+}
+
+/**
+ * gs_plugin_loader_get_system_app:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Returns the application that represents the currently installed OS.
+ *
+ * Returns: (transfer full): a #GsApp
+ **/
+GsApp *
+gs_plugin_loader_get_system_app (GsPluginLoader *plugin_loader)
+{
+ return gs_plugin_loader_app_create (plugin_loader, "*/*/*/*/system/*");
+}
+
+/**
+ * gs_plugin_loader_set_max_parallel_ops:
+ * @plugin_loader: a #GsPluginLoader
+ * @max_ops: the maximum number of parallel operations
+ *
+ * Sets the number of maximum number of queued operations (install/update/upgrade-download)
+ * to be processed at a time. If @max_ops is 0, then it will set the default maximum number.
+ */
+void
+gs_plugin_loader_set_max_parallel_ops (GsPluginLoader *plugin_loader,
+ guint max_ops)
+{
+ g_autoptr(GError) error = NULL;
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (max_ops == 0)
+ max_ops = get_max_parallel_ops ();
+ if (!g_thread_pool_set_max_threads (priv->queued_ops_pool, max_ops, &error))
+ g_warning ("Failed to set the maximum number of ops in parallel: %s",
+ error->message);
+}
+
+const gchar *
+gs_plugin_loader_get_locale (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ return priv->locale;
+}