summaryrefslogtreecommitdiffstats
path: root/lib/gs-plugin-loader.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gs-plugin-loader.c')
-rw-r--r--lib/gs-plugin-loader.c4172
1 files changed, 4172 insertions, 0 deletions
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
new file mode 100644
index 0000000..d10b1be
--- /dev/null
+++ b/lib/gs-plugin-loader.c
@@ -0,0 +1,4172 @@
+/* -*- 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 <glib/gstdio.h>
+#include <appstream.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-manager.h"
+#include "gs-category-private.h"
+#include "gs-external-appstream-utils.h"
+#include "gs-ioprio.h"
+#include "gs-os-release.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 */
+
+struct _GsPluginLoader
+{
+ GObject parent;
+
+ gboolean setup_complete;
+ GCancellable *setup_complete_cancellable; /* (nullable) (owned) */
+
+ GPtrArray *plugins;
+ GPtrArray *locations;
+ gchar *language;
+ gboolean plugin_dir_dirty;
+ GPtrArray *file_monitors;
+ GsPluginStatus global_status_last;
+
+ GMutex pending_apps_mutex;
+ GsAppList *pending_apps; /* (nullable) (owned) */
+ GCancellable *pending_apps_cancellable; /* (nullable) (owned) */
+
+ GThreadPool *queued_ops_pool;
+ gint active_jobs;
+
+ 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;
+
+ GsCategoryManager *category_manager;
+ GsOdrsProvider *odrs_provider; /* (owned) (nullable) */
+
+#ifdef HAVE_SYSPROF
+ SysprofCaptureWriter *sysprof_writer; /* (owned) (nullable) */
+#endif
+
+ GDBusConnection *session_bus_connection; /* (owned); (not nullable) after setup */
+ GDBusConnection *system_bus_connection; /* (owned); (not nullable) after setup */
+};
+
+static void gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader);
+static void add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app);
+static gboolean remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app);
+static void gs_plugin_loader_process_in_thread_pool_cb (gpointer data, gpointer user_data);
+static void gs_plugin_loader_status_changed_cb (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginStatus status,
+ GsPluginLoader *plugin_loader);
+static void async_result_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+G_DEFINE_TYPE (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_ASK_UNTRUSTED,
+ SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+typedef enum {
+ PROP_EVENTS = 1,
+ PROP_ALLOW_UPDATES,
+ PROP_NETWORK_AVAILABLE,
+ PROP_NETWORK_METERED,
+ PROP_SESSION_BUS_CONNECTION,
+ PROP_SYSTEM_BUS_CONNECTION,
+} GsPluginLoaderProperty;
+
+static GParamSpec *obj_props[PROP_SYSTEM_BUS_CONNECTION + 1] = { NULL, };
+
+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 (*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;
+ const gchar *function_name;
+ const gchar *function_name_parent;
+ GPtrArray *catlist;
+ GsPluginJob *plugin_job;
+ gboolean anything_ran;
+ 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)
+{
+ g_autoptr(GsAppList) addons = gs_app_dup_addons (app);
+ GsAppList *related = gs_app_get_related (app);
+
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+
+ for (guint i = 0; addons != NULL && 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;
+ }
+
+ g_object_unref (helper->plugin_loader);
+ if (helper->plugin_job != NULL)
+ g_object_unref (helper->plugin_job);
+ if (helper->catlist != NULL)
+ g_ptr_array_unref (helper->catlist);
+ g_strfreev (helper->tokens);
+ g_slice_free (GsPluginLoaderHelper, helper);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPluginLoaderHelper, gs_plugin_loader_helper_free)
+
+GsPlugin *
+gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name)
+{
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->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_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_EVENTS]);
+ return FALSE;
+}
+
+void
+gs_plugin_loader_add_event (GsPluginLoader *plugin_loader, GsPluginEvent *event)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->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 (plugin_loader->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);
+}
+
+/**
+ * gs_plugin_loader_claim_error:
+ * @plugin_loader: a #GsPluginLoader
+ * @plugin: (nullable): a #GsPlugin to get an application from, or %NULL
+ * @action: a #GsPluginAction associated with the @error
+ * @app: (nullable): a #GsApp for the event, or %NULL
+ * @interactive: whether to set interactive flag
+ * @error: a #GError to claim
+ *
+ * Convert the @error into a plugin event and add it to the queue.
+ *
+ * The @plugin is used only if the @error contains a reference
+ * to a concrete application, in which case any cached application
+ * overrides the passed in @app.
+ *
+ * The %GS_PLUGIN_ERROR_CANCELLED and %G_IO_ERROR_CANCELLED errors
+ * are automatically ignored.
+ *
+ * Since: 41
+ **/
+void
+gs_plugin_loader_claim_error (GsPluginLoader *plugin_loader,
+ GsPlugin *plugin,
+ GsPluginAction action,
+ GsApp *app,
+ gboolean interactive,
+ const GError *error)
+{
+ g_autoptr(GError) error_copy = NULL;
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *origin_id = NULL;
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_autoptr(GsApp) event_app = NULL;
+ g_autoptr(GsApp) event_origin = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (error != NULL);
+
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ return;
+
+ /* find and strip any unique IDs from the error message */
+ error_copy = g_error_copy (error);
+
+ for (guint i = 0; i < 2; i++) {
+ if (app_id == NULL)
+ app_id = gs_utils_error_strip_app_id (error_copy);
+ if (origin_id == NULL)
+ origin_id = gs_utils_error_strip_origin_id (error_copy);
+ }
+
+ /* invalid */
+ if (error_copy->domain != GS_PLUGIN_ERROR) {
+ g_warning ("not GsPlugin error %s:%i: %s",
+ g_quark_to_string (error_copy->domain),
+ error_copy->code,
+ error_copy->message);
+ error_copy->domain = GS_PLUGIN_ERROR;
+ error_copy->code = GS_PLUGIN_ERROR_FAILED;
+ }
+
+ /* set the app and origin IDs if we managed to scrape them from the error above */
+ if (app != NULL)
+ event_app = g_object_ref (app);
+ event_origin = NULL;
+
+ if (plugin != NULL && as_utils_data_id_valid (app_id)) {
+ g_autoptr(GsApp) cached_app = gs_plugin_cache_lookup (plugin, app_id);
+ if (cached_app != NULL) {
+ g_debug ("found app %s in error", app_id);
+ g_set_object (&event_app, cached_app);
+ } else {
+ g_debug ("no unique ID found for app %s", app_id);
+ }
+ }
+ if (plugin != NULL && as_utils_data_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);
+ g_set_object (&event_origin, origin);
+ } else {
+ g_debug ("no unique ID found for origin %s", origin_id);
+ }
+ }
+
+ /* create event which is handled by the GsShell */
+ event = gs_plugin_event_new ("error", error_copy,
+ "action", action,
+ "app", event_app,
+ "origin", event_origin,
+ NULL);
+ if (interactive)
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+
+ /* add event to the queue */
+ gs_plugin_loader_add_event (plugin_loader, event);
+}
+
+/**
+ * gs_plugin_loader_claim_job_error:
+ * @plugin_loader: a #GsPluginLoader
+ * @plugin: (nullable): a #GsPlugin to get an application from, or %NULL
+ * @job: a #GsPluginJob for the @error
+ * @error: a #GError to claim
+ *
+ * The same as gs_plugin_loader_claim_error(), only reads the information
+ * from the @job.
+ *
+ * Since: 41
+ **/
+void
+gs_plugin_loader_claim_job_error (GsPluginLoader *plugin_loader,
+ GsPlugin *plugin,
+ GsPluginJob *job,
+ const GError *error)
+{
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (GS_IS_PLUGIN_JOB (job));
+ g_return_if_fail (error != NULL);
+
+ gs_plugin_loader_claim_error (plugin_loader, plugin,
+ gs_plugin_job_get_action (job),
+ gs_plugin_job_get_app (job),
+ gs_plugin_job_get_interactive (job),
+ error);
+}
+
+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_autoptr(GError) error_local_copy = NULL;
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *origin_id = 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;
+ }
+
+ if (gs_plugin_job_get_propagate_error (helper->plugin_job)) {
+ g_propagate_error (error, g_error_copy (error_local));
+ return FALSE;
+ }
+
+ /* 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 */
+ error_local_copy = g_error_copy (error_local);
+
+ for (guint i = 0; i < 2; i++) {
+ if (app_id == NULL)
+ app_id = gs_utils_error_strip_app_id (error_local_copy);
+ if (origin_id == NULL)
+ origin_id = gs_utils_error_strip_origin_id (error_local_copy);
+ }
+
+ /* fatal error */
+ if (gs_plugin_loader_is_error_fatal (error_local_copy) ||
+ g_getenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD") != NULL) {
+ if (error != NULL)
+ *error = g_steal_pointer (&error_local_copy);
+ return FALSE;
+ }
+
+ gs_plugin_loader_claim_job_error (helper->plugin_loader, plugin, helper->plugin_job, error_local);
+
+ return TRUE;
+}
+
+/**
+ * gs_plugin_loader_run_adopt:
+ * @plugin_loader: a #GsPluginLoader
+ * @list: list of apps to try and adopt
+ *
+ * Call the gs_plugin_adopt_app() function on each plugin on each app in @list
+ * to try and find the plugin which should manage each app.
+ *
+ * This function is intended to be used by internal gnome-software code.
+ *
+ * Since: 42
+ */
+void
+gs_plugin_loader_run_adopt (GsPluginLoader *plugin_loader, GsAppList *list)
+{
+ guint i;
+ guint j;
+
+ /* go through each plugin in order */
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPluginAdoptAppFunc adopt_app_func = NULL;
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->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_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ if (!gs_app_has_management_plugin (app, NULL))
+ continue;
+
+ adopt_app_func (plugin, app);
+
+ if (!gs_app_has_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_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ if (!gs_app_has_management_plugin (app, NULL))
+ continue;
+
+ g_debug ("nothing adopted %s", gs_app_get_unique_id (app));
+ }
+}
+
+static gboolean
+gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
+ GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoader *plugin_loader = helper->plugin_loader;
+ 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;
+
+ /* at least one plugin supports this vfunc */
+ helper->anything_ran = 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_NONE)
+ 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_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_UPGRADE_DOWNLOAD:
+ case GS_PLUGIN_ACTION_UPGRADE_TRIGGER:
+ case GS_PLUGIN_ACTION_LAUNCH:
+ case GS_PLUGIN_ACTION_UPDATE_CANCEL:
+ {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ case GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL:
+ case GS_PLUGIN_ACTION_GET_SOURCES:
+ {
+ GsPluginResultsFunc plugin_func = func;
+ ret = plugin_func (plugin, list, 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) {
+ 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) == GS_APP_STATE_QUEUED_FOR_INSTALL) {
+ add_app_to_install_queue (plugin_loader, app);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (plugin_loader->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 (plugin_loader->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) {
+ g_log_structured_standard (G_LOG_DOMAIN, G_LOG_LEVEL_DEBUG,
+ __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));
+ }
+
+ return TRUE;
+}
+
+static void
+gs_plugin_loader_job_sorted_truncation_again (GsPluginJob *plugin_job,
+ GsAppList *list)
+{
+ GsAppListSortFunc sort_func;
+ gpointer sort_func_data;
+
+ /* not valid */
+ if (list == NULL)
+ return;
+
+ /* unset */
+ sort_func = gs_plugin_job_get_sort_func (plugin_job, &sort_func_data);
+ if (sort_func == NULL)
+ return;
+ gs_app_list_sort (list, sort_func, sort_func_data);
+}
+
+static void
+gs_plugin_loader_job_sorted_truncation (GsPluginJob *plugin_job,
+ GsAppList *list)
+{
+ GsAppListSortFunc sort_func;
+ gpointer sort_func_data;
+ guint max_results;
+
+ /* not valid */
+ if (list == NULL)
+ return;
+
+ /* unset */
+ max_results = gs_plugin_job_get_max_results (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 (plugin_job, &sort_func_data);
+ if (sort_func == NULL) {
+ GsPluginAction action = gs_plugin_job_get_action (plugin_job);
+ g_debug ("no ->sort_func() set for %s, using random!",
+ gs_plugin_action_to_string (action));
+ gs_app_list_randomize (list);
+ } else {
+ 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)
+{
+ GsPluginLoader *plugin_loader = helper->plugin_loader;
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* Refining is done separately as it’s a special action */
+ g_assert (!GS_IS_PLUGIN_JOB_REFINE (helper->plugin_job));
+
+ /* run each plugin */
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->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_NONE,
+ cancellable, error)) {
+ return FALSE;
+ }
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (plugin_loader->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 (plugin_loader->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>";
+}
+
+gboolean
+gs_plugin_loader_app_is_valid (GsApp *app,
+ GsPluginRefineFlags refine_flags)
+{
+ /* never show addons */
+ if (gs_app_get_kind (app) == AS_COMPONENT_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_COMPONENT_KIND_CONSOLE_APP) {
+ 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) == GS_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_COMPONENT_KIND_UNKNOWN &&
+ gs_app_get_state (app) == GS_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_COMPONENT_KIND_REPOSITORY) {
+ 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_COMPONENT_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 (!(refine_flags & GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES) &&
+ gs_app_get_kind (app) == AS_COMPONENT_KIND_GENERIC &&
+ gs_app_get_special_kind (app) == GS_APP_SPECIAL_KIND_NONE) {
+ g_debug ("app invalid as only a %s: %s",
+ as_component_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_filter (GsApp *app,
+ gpointer user_data)
+{
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+ return gs_plugin_loader_app_is_valid (app, gs_plugin_job_get_refine_flags (helper->plugin_job));
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid_updatable (GsApp *app, gpointer user_data)
+{
+ return gs_plugin_loader_app_is_valid_filter (app, user_data) &&
+ (gs_app_is_updatable (app) || gs_app_get_state (app) == GS_APP_STATE_INSTALLING);
+}
+
+gboolean
+gs_plugin_loader_app_is_compatible (GsPluginLoader *plugin_loader,
+ GsApp *app)
+{
+ 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; plugin_loader->compatible_projects[i] != NULL; i++) {
+ if (g_strcmp0 (tmp, plugin_loader->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;
+}
+
+/******************************************************************************/
+
+/**
+ * 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 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);
+
+ 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);
+ switch (gs_plugin_job_get_action (helper->plugin_job)) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ if (gs_app_get_state (app) != GS_APP_STATE_AVAILABLE_LOCAL)
+ add_app_to_install_queue (plugin_loader, app);
+ /* make sure the progress is properly initialized */
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+ break;
+ case GS_PLUGIN_ACTION_REMOVE:
+ remove_app_from_install_queue (plugin_loader, app);
+ break;
+ default:
+ break;
+ }
+ }
+ 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);
+
+ 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);
+ remove_app_from_install_queue (plugin_loader, app);
+
+ /* check the app is not still in an action helper */
+ switch (gs_app_get_state (app)) {
+ case GS_APP_STATE_INSTALLING:
+ case GS_APP_STATE_REMOVING:
+ g_warning ("application %s left in %s helper",
+ gs_app_get_unique_id (app),
+ gs_app_state_to_string (gs_app_get_state (app)));
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+ break;
+ default:
+ break;
+ }
+
+ }
+ g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+}
+
+static void
+async_result_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GAsyncResult **result_out = user_data;
+
+ *result_out = g_object_ref (result);
+ g_main_context_wakeup (g_main_context_get_thread_default ());
+}
+
+/* This will load the install queue and add it to #GsPluginLoader.pending_apps,
+ * but it won’t refine the loaded apps. */
+static GsAppList *
+load_install_queue (GsPluginLoader *plugin_loader,
+ GError **error)
+{
+ 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 gs_app_list_new ();
+
+ g_debug ("loading install queue from %s", file);
+ if (!g_file_get_contents (file, &contents, NULL, error))
+ return NULL;
+
+ /* 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;
+ g_auto(GStrv) split = g_strsplit (names[i], "\t", -1);
+ if (split[0] == NULL || split[1] == NULL)
+ continue;
+ app = gs_app_new (NULL);
+ gs_app_set_from_unique_id (app, split[0], as_component_kind_from_string (split[1]));
+ gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ }
+
+ /* add to pending list */
+ g_mutex_lock (&plugin_loader->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));
+ if (plugin_loader->pending_apps == NULL)
+ plugin_loader->pending_apps = gs_app_list_new ();
+ gs_app_list_add (plugin_loader->pending_apps, app);
+ }
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+
+ return g_steal_pointer (&list);
+}
+
+static void
+save_install_queue (GsPluginLoader *plugin_loader)
+{
+ gboolean ret;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GString) s = NULL;
+ g_autofree gchar *file = NULL;
+
+ s = g_string_new ("");
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ for (guint i = 0; plugin_loader->pending_apps != NULL && i < gs_app_list_length (plugin_loader->pending_apps); i++) {
+ GsApp *app = gs_app_list_index (plugin_loader->pending_apps, i);
+ if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) {
+ g_string_append (s, gs_app_get_unique_id (app));
+ g_string_append_c (s, '\t');
+ g_string_append (s, as_component_kind_to_string (gs_app_get_kind (app)));
+ g_string_append_c (s, '\n');
+ }
+ }
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+
+ /* save file */
+ file = g_build_filename (g_get_user_data_dir (),
+ "gnome-software",
+ "install-queue",
+ NULL);
+ if (s->len == 0) {
+ if (g_unlink (file) == -1 && errno != ENOENT) {
+ gint errn = errno;
+ g_warning ("Failed to unlink '%s': %s", file, g_strerror (errn));
+ }
+ return;
+ }
+
+ 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)
+{
+ g_autoptr(GsAppList) addons = NULL;
+ g_autoptr(GSource) source = NULL;
+ guint i;
+
+ /* queue the app itself */
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ if (plugin_loader->pending_apps == NULL)
+ plugin_loader->pending_apps = gs_app_list_new ();
+ gs_app_list_add (plugin_loader->pending_apps, app);
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+
+ gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL);
+
+ source = g_idle_source_new ();
+ g_source_set_callback (source, emit_pending_apps_idle, g_object_ref (plugin_loader), NULL);
+ g_source_set_name (source, "[gnome-software] emit_pending_apps_idle");
+ g_source_attach (source, NULL);
+
+ save_install_queue (plugin_loader);
+
+ /* recursively queue any addons */
+ addons = gs_app_dup_addons (app);
+ for (i = 0; addons != NULL && 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)
+{
+ g_autoptr(GsAppList) addons = NULL;
+ gboolean ret;
+ guint i;
+
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ ret = plugin_loader->pending_apps != NULL && gs_app_list_remove (plugin_loader->pending_apps, app);
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+
+ if (ret) {
+ g_autoptr(GSource) source = NULL;
+
+ if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL)
+ gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
+
+ source = g_idle_source_new ();
+ g_source_set_callback (source, emit_pending_apps_idle, g_object_ref (plugin_loader), NULL);
+ g_source_set_name (source, "[gnome-software] emit_pending_apps_idle");
+ g_source_attach (source, NULL);
+
+ save_install_queue (plugin_loader);
+
+ /* recursively remove any queued addons */
+ addons = gs_app_dup_addons (app);
+ for (i = 0; addons != NULL && 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)
+{
+ GHashTableIter iter;
+ gpointer value;
+
+ /* nothing */
+ if (g_hash_table_size (plugin_loader->disallow_updates) == 0)
+ return TRUE;
+
+ /* list */
+ g_hash_table_iter_init (&iter, plugin_loader->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)
+{
+ GsAppList *array;
+
+ array = gs_app_list_new ();
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ if (plugin_loader->pending_apps != NULL)
+ gs_app_list_add_list (array, plugin_loader->pending_apps);
+ g_mutex_unlock (&plugin_loader->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)
+{
+ GPtrArray *events = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* just add everything */
+ g_hash_table_iter_init (&iter, plugin_loader->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)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* just add everything */
+ g_hash_table_iter_init (&iter, plugin_loader->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)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&plugin_loader->events_by_id_mutex);
+ g_hash_table_remove_all (plugin_loader->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)
+{
+ gboolean changed = FALSE;
+
+ /* plugin now allowing gnome-software to show updates panel */
+ if (allow_updates) {
+ if (g_hash_table_remove (plugin_loader->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 (plugin_loader->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_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_ALLOW_UPDATES]);
+}
+
+static void
+gs_plugin_loader_status_changed_cb (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginStatus status,
+ GsPluginLoader *plugin_loader)
+{
+ /* nothing specific */
+ if (app == NULL || gs_app_get_id (app) == NULL) {
+ if (plugin_loader->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);
+ plugin_loader->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_ask_untrusted_cb (GsPlugin *plugin,
+ const gchar *title,
+ const gchar *msg,
+ const gchar *details,
+ const gchar *accept_label,
+ GsPluginLoader *plugin_loader)
+{
+ gboolean accepts = FALSE;
+ g_debug ("emitting ask-untrusted title:'%s', msg:'%s' details:'%s' accept-label:'%s'", title, msg, details, accept_label);
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_ASK_UNTRUSTED], 0,
+ title, msg, details, accept_label, &accepts);
+ return accepts;
+}
+
+static gboolean
+gs_plugin_loader_job_updates_changed_delay_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+
+ /* notify shells */
+ g_debug ("updates-changed");
+ g_signal_emit (plugin_loader, signals[SIGNAL_UPDATES_CHANGED], 0);
+ plugin_loader->updates_changed_id = 0;
+ plugin_loader->updates_changed_cnt = 0;
+
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_updates_changed (GsPluginLoader *plugin_loader)
+{
+ if (plugin_loader->updates_changed_id != 0)
+ return;
+ plugin_loader->updates_changed_id =
+ g_timeout_add_seconds_full (G_PRIORITY_DEFAULT,
+ GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY,
+ gs_plugin_loader_job_updates_changed_delay_cb,
+ g_object_ref (plugin_loader),
+ g_object_unref);
+}
+
+static void
+gs_plugin_loader_job_updates_changed_cb (GsPlugin *plugin,
+ GsPluginLoader *plugin_loader)
+{
+ plugin_loader->updates_changed_cnt++;
+
+ /* Schedule emit of updates changed when no job is active.
+ This helps to avoid a race condition when a plugin calls
+ updates-changed at the end of the job, but the job is
+ finished before the callback gets called in the main thread. */
+ if (!g_atomic_int_get (&plugin_loader->active_jobs))
+ gs_plugin_loader_updates_changed (plugin_loader);
+}
+
+static gboolean
+gs_plugin_loader_reload_delay_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+
+ /* notify shells */
+ g_debug ("emitting ::reload");
+ g_signal_emit (plugin_loader, signals[SIGNAL_RELOAD], 0);
+ plugin_loader->reload_id = 0;
+
+ g_object_unref (plugin_loader);
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_reload_cb (GsPlugin *plugin,
+ GsPluginLoader *plugin_loader)
+{
+ if (plugin_loader->reload_id != 0)
+ return;
+ plugin_loader->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_repository_changed_cb (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginLoader *plugin_loader)
+{
+ GApplication *application = g_application_get_default ();
+
+ /* Can be NULL when running the self tests */
+ if (application) {
+ g_signal_emit_by_name (application,
+ "repository-changed",
+ repository);
+ }
+}
+
+static void
+gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
+ const gchar *filename)
+{
+ GsPlugin *plugin;
+ g_autoptr(GError) error = NULL;
+
+ /* create plugin from file */
+ plugin = gs_plugin_create (filename,
+ plugin_loader->session_bus_connection,
+ plugin_loader->system_bus_connection,
+ &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_updates_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);
+ g_signal_connect (plugin, "repository-changed",
+ G_CALLBACK (gs_plugin_loader_repository_changed_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "ask-untrusted",
+ G_CALLBACK (gs_plugin_loader_ask_untrusted_cb),
+ plugin_loader);
+ gs_plugin_set_language (plugin, plugin_loader->language);
+ gs_plugin_set_scale (plugin, gs_plugin_loader_get_scale (plugin_loader));
+ gs_plugin_set_network_monitor (plugin, plugin_loader->network_monitor);
+ g_debug ("opened plugin %s: %s", filename, gs_plugin_get_name (plugin));
+
+ /* add to array */
+ g_ptr_array_add (plugin_loader->plugins, plugin);
+}
+
+static void
+gs_plugin_loader_remove_all_plugins (GsPluginLoader *plugin_loader)
+{
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = GS_PLUGIN (plugin_loader->plugins->pdata[i]);
+ g_signal_handlers_disconnect_by_data (plugin, plugin_loader);
+ }
+
+ g_ptr_array_set_size (plugin_loader->plugins, 0);
+}
+
+void
+gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader, guint scale)
+{
+ /* save globally, and update each plugin */
+ plugin_loader->scale = scale;
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i);
+ gs_plugin_set_scale (plugin, scale);
+ }
+}
+
+guint
+gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader)
+{
+ return plugin_loader->scale;
+}
+
+void
+gs_plugin_loader_add_location (GsPluginLoader *plugin_loader, const gchar *location)
+{
+ for (guint i = 0; i < plugin_loader->locations->len; i++) {
+ const gchar *location_tmp = g_ptr_array_index (plugin_loader->locations, i);
+ if (g_strcmp0 (location_tmp, location) == 0)
+ return;
+ }
+ g_info ("adding plugin location %s", location);
+ g_ptr_array_add (plugin_loader->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_software_app_created_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_autoptr(GError) error = NULL;
+
+ app = gs_plugin_loader_app_create_finish (plugin_loader, result, NULL);
+
+ g_set_error_literal (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_RESTART_REQUIRED,
+ "A restart is required");
+ event = gs_plugin_event_new ("action", GS_PLUGIN_ACTION_UNKNOWN,
+ "app", app,
+ "error", error,
+ NULL);
+
+ gs_plugin_loader_add_event (plugin_loader, event);
+}
+
+static void
+gs_plugin_loader_plugin_dir_changed_cb (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ GsPluginLoader *plugin_loader)
+{
+ /* already triggered */
+ if (plugin_loader->plugin_dir_dirty)
+ return;
+
+ gs_plugin_loader_app_create_async (plugin_loader, "system/*/*/org.gnome.Software.desktop/*",
+ NULL, gs_plugin_loader_software_app_created_cb, NULL);
+
+ plugin_loader->plugin_dir_dirty = TRUE;
+}
+
+void
+gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader)
+{
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i);
+ gs_plugin_cache_invalidate (plugin);
+ }
+}
+
+static void
+gs_plugin_loader_remove_all_file_monitors (GsPluginLoader *plugin_loader)
+{
+ for (guint i = 0; i < plugin_loader->file_monitors->len; i++) {
+ GFileMonitor *file_monitor = G_FILE_MONITOR (plugin_loader->file_monitors->pdata[i]);
+
+ g_signal_handlers_disconnect_by_data (file_monitor, plugin_loader);
+ g_file_monitor_cancel (file_monitor);
+ }
+
+ g_ptr_array_set_size (plugin_loader->file_monitors, 0);
+}
+
+typedef struct {
+ GsPluginLoader *plugin_loader; /* (unowned) */
+ GMainContext *context; /* (owned) */
+ guint n_pending;
+} ShutdownData;
+
+static void plugin_shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+/**
+ * gs_plugin_loader_shutdown:
+ * @plugin_loader: a #GsPluginLoader
+ * @cancellable: a #GCancellable, or %NULL
+ *
+ * Shut down the plugins.
+ *
+ * This blocks until the operation is complete. It may be refactored in future
+ * to be asynchronous.
+ *
+ * Since: 42
+ */
+void
+gs_plugin_loader_shutdown (GsPluginLoader *plugin_loader,
+ GCancellable *cancellable)
+{
+ ShutdownData shutdown_data;
+
+ shutdown_data.plugin_loader = plugin_loader;
+ shutdown_data.n_pending = 1; /* incremented until all operations have been started */
+ shutdown_data.context = g_main_context_new ();
+
+ g_main_context_push_thread_default (shutdown_data.context);
+
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = GS_PLUGIN (plugin_loader->plugins->pdata[i]);
+
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+
+ if (GS_PLUGIN_GET_CLASS (plugin)->shutdown_async != NULL) {
+ GS_PLUGIN_GET_CLASS (plugin)->shutdown_async (plugin, cancellable,
+ plugin_shutdown_cb, &shutdown_data);
+ shutdown_data.n_pending++;
+ }
+ }
+
+ /* Wait for shutdown to complete in all plugins. */
+ shutdown_data.n_pending--;
+
+ while (shutdown_data.n_pending > 0)
+ g_main_context_iteration (shutdown_data.context, TRUE);
+
+ g_main_context_pop_thread_default (shutdown_data.context);
+ g_clear_pointer (&shutdown_data.context, g_main_context_unref);
+
+ /* Clear some internal data structures. */
+ gs_plugin_loader_remove_all_plugins (plugin_loader);
+ gs_plugin_loader_remove_all_file_monitors (plugin_loader);
+ plugin_loader->setup_complete = FALSE;
+ g_clear_object (&plugin_loader->setup_complete_cancellable);
+ plugin_loader->setup_complete_cancellable = g_cancellable_new ();
+}
+
+static void
+plugin_shutdown_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (source_object);
+ ShutdownData *data = user_data;
+ g_autoptr(GError) local_error = NULL;
+
+ g_assert (GS_PLUGIN_GET_CLASS (plugin)->shutdown_finish != NULL);
+
+ if (!GS_PLUGIN_GET_CLASS (plugin)->shutdown_finish (plugin, result, &local_error)) {
+ g_debug ("disabling %s as shutdown failed: %s",
+ gs_plugin_get_name (plugin),
+ local_error->message);
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+
+ /* Indicate this plugin has finished shutting down. */
+ data->n_pending--;
+ g_main_context_wakeup (data->context);
+}
+
+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);
+}
+
+typedef struct {
+ guint n_pending;
+ gchar **allowlist;
+ gchar **blocklist;
+#ifdef HAVE_SYSPROF
+ gint64 setup_begin_time_nsec;
+ gint64 plugins_begin_time_nsec;
+#endif
+} SetupData;
+
+static void
+setup_data_free (SetupData *data)
+{
+ g_clear_pointer (&data->allowlist, g_strfreev);
+ g_clear_pointer (&data->blocklist, g_strfreev);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (SetupData, setup_data_free)
+
+static void get_session_bus_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void get_system_bus_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void finish_setup_get_bus (GTask *task);
+static void plugin_setup_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void finish_setup_op (GTask *task);
+static void finish_setup_install_queue_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+/* Mark the asynchronous setup operation as complete. This will notify any
+ * waiting tasks by cancelling the #GCancellable. It’s safe to clear the
+ * #GCancellable as each waiting task holds its own reference. */
+static void
+notify_setup_complete (GsPluginLoader *plugin_loader)
+{
+ plugin_loader->setup_complete = TRUE;
+ g_cancellable_cancel (plugin_loader->setup_complete_cancellable);
+ g_clear_object (&plugin_loader->setup_complete_cancellable);
+}
+
+/**
+ * gs_plugin_loader_setup_async:
+ * @plugin_loader: a #GsPluginLoader
+ * @allowlist: list of plugin names, or %NULL
+ * @blocklist: list of plugin names, or %NULL
+ * @cancellable: A #GCancellable, or %NULL
+ * @callback: callback to indicate completion of the asynchronous operation
+ * @user_data: data to pass to @callback
+ *
+ * Sets up the plugin loader ready for use.
+ *
+ * Since: 42
+ */
+void
+gs_plugin_loader_setup_async (GsPluginLoader *plugin_loader,
+ const gchar * const *allowlist,
+ const gchar * const *blocklist,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ SetupData *setup_data;
+ g_autoptr(SetupData) setup_data_owned = NULL;
+ g_autoptr(GTask) task = NULL;
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ task = g_task_new (plugin_loader, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_loader_setup_async);
+
+ /* If setup is already complete, return immediately. */
+ if (plugin_loader->setup_complete) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* Setup data closure. */
+ setup_data = setup_data_owned = g_new0 (SetupData, 1);
+ setup_data->allowlist = g_strdupv ((gchar **) allowlist);
+ setup_data->blocklist = g_strdupv ((gchar **) blocklist);
+#ifdef HAVE_SYSPROF
+ setup_data->setup_begin_time_nsec = begin_time_nsec;
+#endif
+
+ g_task_set_task_data (task, g_steal_pointer (&setup_data_owned), (GDestroyNotify) setup_data_free);
+
+ /* Connect to D-Bus if connections haven’t been provided at construction
+ * time. */
+ if (plugin_loader->session_bus_connection == NULL)
+ g_bus_get (G_BUS_TYPE_SESSION, cancellable, get_session_bus_cb, g_object_ref (task));
+ if (plugin_loader->system_bus_connection == NULL)
+ g_bus_get (G_BUS_TYPE_SYSTEM, cancellable, get_system_bus_cb, g_object_ref (task));
+
+ finish_setup_get_bus (task);
+}
+
+static void
+get_session_bus_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ g_autoptr(GError) local_error = NULL;
+
+ plugin_loader->session_bus_connection = g_bus_get_finish (result, &local_error);
+ if (plugin_loader->session_bus_connection == NULL) {
+ notify_setup_complete (plugin_loader);
+ g_prefix_error_literal (&local_error, "Error getting session bus: ");
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_SESSION_BUS_CONNECTION]);
+
+ finish_setup_get_bus (task);
+}
+
+static void
+get_system_bus_cb (GObject *object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ g_autoptr(GError) local_error = NULL;
+
+ plugin_loader->system_bus_connection = g_bus_get_finish (result, &local_error);
+ if (plugin_loader->system_bus_connection == NULL) {
+ notify_setup_complete (plugin_loader);
+ g_prefix_error_literal (&local_error, "Error getting system bus: ");
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_SYSTEM_BUS_CONNECTION]);
+
+ finish_setup_get_bus (task);
+}
+
+static void
+finish_setup_get_bus (GTask *task)
+{
+ SetupData *data = g_task_get_task_data (task);
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ const gchar *plugin_name;
+ gboolean changes;
+ GPtrArray *deps;
+ GsPlugin *dep;
+ GsPlugin *plugin;
+ guint dep_loop_check = 0;
+ guint i;
+ guint j;
+ g_autoptr(GPtrArray) locations = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ /* Wait until we’ve got all the buses we need. */
+ if (plugin_loader->session_bus_connection == NULL ||
+ plugin_loader->system_bus_connection == NULL)
+ return;
+
+ /* use the default, but this requires a 'make install' */
+ if (plugin_loader->locations->len == 0) {
+ g_autofree gchar *filename = NULL;
+ filename = g_strdup_printf ("plugins-%s", GS_PLUGIN_API_VERSION);
+ locations = g_ptr_array_new_with_free_func (g_free);
+ g_ptr_array_add (locations, g_build_filename (LIBDIR, "gnome-software", filename, NULL));
+ } else {
+ locations = g_ptr_array_ref (plugin_loader->locations);
+ }
+
+ for (i = 0; i < locations->len; i++) {
+ GFileMonitor *monitor;
+ const gchar *location = g_ptr_array_index (locations, i);
+ g_autoptr(GFile) plugin_dir = g_file_new_for_path (location);
+ g_debug ("monitoring plugin location %s", location);
+ monitor = g_file_monitor_directory (plugin_dir,
+ G_FILE_MONITOR_NONE,
+ cancellable,
+ &local_error);
+ if (monitor == NULL) {
+ notify_setup_complete (plugin_loader);
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ g_signal_connect (monitor, "changed",
+ G_CALLBACK (gs_plugin_loader_plugin_dir_changed_cb), plugin_loader);
+ g_ptr_array_add (plugin_loader->file_monitors, monitor);
+ }
+
+ /* search for plugins */
+ for (i = 0; i < locations->len; i++) {
+ const gchar *location = g_ptr_array_index (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, &local_error);
+ if (fns == NULL) {
+ notify_setup_complete (plugin_loader);
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ 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 (data->allowlist != NULL) {
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ gboolean ret;
+ plugin = g_ptr_array_index (plugin_loader->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ ret = g_strv_contains ((const gchar * const *) data->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 (data->blocklist != NULL) {
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ gboolean ret;
+ plugin = g_ptr_array_index (plugin_loader->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ ret = g_strv_contains ((const gchar * const *) data->blocklist,
+ gs_plugin_get_name (plugin));
+ if (ret)
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+ }
+
+ /* order by deps */
+ do {
+ changes = FALSE;
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ plugin = g_ptr_array_index (plugin_loader->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 < plugin_loader->plugins->len; i++) {
+ plugin = g_ptr_array_index (plugin_loader->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) {
+ notify_setup_complete (plugin_loader);
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ "got stuck in dep loop");
+ return;
+ }
+ } while (changes);
+
+ /* check for conflicts */
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ plugin = g_ptr_array_index (plugin_loader->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 (plugin_loader->plugins,
+ gs_plugin_loader_plugin_sort_fn);
+
+ /* assign priority values */
+ do {
+ changes = FALSE;
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ plugin = g_ptr_array_index (plugin_loader->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) {
+ notify_setup_complete (plugin_loader);
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ "got stuck in priority loop");
+ return;
+ }
+ } while (changes);
+
+ /* run setup */
+ data->n_pending = 1; /* incremented until all operations have been started */
+#ifdef HAVE_SYSPROF
+ data->plugins_begin_time_nsec = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ for (i = 0; i < plugin_loader->plugins->len; i++) {
+ plugin = GS_PLUGIN (plugin_loader->plugins->pdata[i]);
+
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+
+ if (GS_PLUGIN_GET_CLASS (plugin)->setup_async != NULL) {
+ data->n_pending++;
+ GS_PLUGIN_GET_CLASS (plugin)->setup_async (plugin, cancellable,
+ plugin_setup_cb, g_object_ref (task));
+ }
+ }
+
+ finish_setup_op (task);
+}
+
+static void
+plugin_setup_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ g_autoptr(GError) local_error = NULL;
+#ifdef HAVE_SYSPROF
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ SetupData *data = g_task_get_task_data (task);
+#endif /* HAVE_SYSPROF */
+
+ g_assert (GS_PLUGIN_GET_CLASS (plugin)->setup_finish != NULL);
+
+ if (!GS_PLUGIN_GET_CLASS (plugin)->setup_finish (plugin, result, &local_error)) {
+ g_debug ("disabling %s as setup failed: %s",
+ gs_plugin_get_name (plugin),
+ local_error->message);
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (plugin_loader->sysprof_writer != NULL) {
+ sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer,
+ data->plugins_begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - data->plugins_begin_time_nsec,
+ "gnome-software",
+ "setup-plugin",
+ NULL);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* Indicate this plugin has finished setting up. */
+ finish_setup_op (task);
+}
+
+static void
+finish_setup_op (GTask *task)
+{
+ SetupData *data = g_task_get_task_data (task);
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ g_autoptr(GsAppList) install_queue = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ g_assert (data->n_pending > 0);
+ data->n_pending--;
+
+ if (data->n_pending > 0)
+ return;
+
+ /* now we can load the install-queue */
+ install_queue = load_install_queue (plugin_loader, &local_error);
+ if (install_queue == NULL) {
+ notify_setup_complete (plugin_loader);
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* Mark setup as complete as it’s now safe for other jobs to be
+ * processed. Indeed, the final step in setup is to refine the install
+ * queue apps, which requires @setup_complete to be %TRUE. */
+ notify_setup_complete (plugin_loader);
+
+#ifdef HAVE_SYSPROF
+ if (plugin_loader->sysprof_writer != NULL) {
+ sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer,
+ data->setup_begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - data->setup_begin_time_nsec,
+ "gnome-software",
+ "setup",
+ NULL);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* Refine the install queue. */
+ if (gs_app_list_length (install_queue) > 0) {
+ g_autoptr(GsPluginJob) refine_job = NULL;
+
+ /* Require ID and Origin to get complete unique IDs */
+ refine_job = gs_plugin_job_refine_new (install_queue, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
+ GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING);
+ gs_plugin_loader_job_process_async (plugin_loader, refine_job,
+ cancellable,
+ finish_setup_install_queue_cb,
+ g_object_ref (task));
+ } else {
+ g_task_return_boolean (task, TRUE);
+ }
+}
+
+static void gs_plugin_loader_maybe_flush_pending_install_queue (GsPluginLoader *plugin_loader);
+
+static void
+finish_setup_install_queue_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ g_autoptr(GsAppList) new_list = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ new_list = gs_plugin_loader_job_process_finish (plugin_loader, result, &local_error);
+ if (new_list == NULL) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ } else {
+ g_autoptr(GsAppList) old_pending_apps = NULL;
+ gboolean has_pending_apps = FALSE;
+ gboolean changed;
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ changed = plugin_loader->pending_apps != NULL;
+ /* Merge the existing and newly-loaded lists, in case pending apps were added
+ while the install-queue file was being loaded */
+ old_pending_apps = g_steal_pointer (&plugin_loader->pending_apps);
+ if (old_pending_apps != NULL && gs_app_list_length (new_list) > 0) {
+ g_autoptr(GHashTable) expected_unique_ids = g_hash_table_new (g_str_hash, g_str_equal);
+ for (guint i = 0; i < gs_app_list_length (old_pending_apps); i++) {
+ GsApp *app = gs_app_list_index (old_pending_apps, i);
+ if (gs_app_get_unique_id (app) != NULL)
+ g_hash_table_add (expected_unique_ids, (gpointer) gs_app_get_unique_id (app));
+ }
+ for (guint i = 0; i < gs_app_list_length (new_list); i++) {
+ GsApp *app = gs_app_list_index (new_list, i);
+ if (gs_app_get_state (app) == GS_APP_STATE_AVAILABLE &&
+ gs_app_get_unique_id (app) != NULL &&
+ g_hash_table_contains (expected_unique_ids, gs_app_get_unique_id (app))) {
+ if (plugin_loader->pending_apps == NULL)
+ plugin_loader->pending_apps = gs_app_list_new ();
+ gs_app_set_state (app, GS_APP_STATE_QUEUED_FOR_INSTALL);
+ gs_app_set_pending_action (app, GS_PLUGIN_ACTION_INSTALL);
+ gs_app_list_add (plugin_loader->pending_apps, app);
+ }
+ }
+ has_pending_apps = plugin_loader->pending_apps != NULL;
+ changed = TRUE;
+ }
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+ g_task_return_boolean (task, TRUE);
+
+ if (changed)
+ save_install_queue (plugin_loader);
+ if (has_pending_apps)
+ gs_plugin_loader_maybe_flush_pending_install_queue (plugin_loader);
+ }
+}
+
+/**
+ * gs_plugin_loader_setup_finish:
+ * @plugin_loader: a #GsPluginLoader
+ * @result: result of the asynchronous operation
+ * @error: return location for a #GError, or %NULL
+ *
+ * Finish an asynchronous setup operation started with
+ * gs_plugin_loader_setup_async().
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 42
+ */
+gboolean
+gs_plugin_loader_setup_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *result,
+ GError **error)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), FALSE);
+ g_return_val_if_fail (g_task_is_valid (result, plugin_loader), FALSE);
+ g_return_val_if_fail (g_async_result_is_tagged (result, gs_plugin_loader_setup_async), FALSE);
+
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+void
+gs_plugin_loader_dump_state (GsPluginLoader *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 < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->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);
+
+ switch ((GsPluginLoaderProperty) prop_id) {
+ case PROP_EVENTS:
+ g_value_set_pointer (value, plugin_loader->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;
+ case PROP_SESSION_BUS_CONNECTION:
+ g_value_set_object (value, plugin_loader->session_bus_connection);
+ break;
+ case PROP_SYSTEM_BUS_CONNECTION:
+ g_value_set_object (value, plugin_loader->system_bus_connection);
+ 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)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+
+ switch ((GsPluginLoaderProperty) prop_id) {
+ case PROP_EVENTS:
+ case PROP_ALLOW_UPDATES:
+ case PROP_NETWORK_AVAILABLE:
+ case PROP_NETWORK_METERED:
+ /* Read only */
+ g_assert_not_reached ();
+ break;
+ case PROP_SESSION_BUS_CONNECTION:
+ /* Construct only */
+ g_assert (plugin_loader->session_bus_connection == NULL);
+ plugin_loader->session_bus_connection = g_value_dup_object (value);
+ break;
+ case PROP_SYSTEM_BUS_CONNECTION:
+ /* Construct only */
+ g_assert (plugin_loader->system_bus_connection == NULL);
+ plugin_loader->system_bus_connection = g_value_dup_object (value);
+ break;
+ 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);
+
+ g_cancellable_cancel (plugin_loader->pending_apps_cancellable);
+
+ if (plugin_loader->plugins != NULL) {
+ /* Shut down all the plugins first. */
+ gs_plugin_loader_shutdown (plugin_loader, NULL);
+
+ g_clear_pointer (&plugin_loader->plugins, g_ptr_array_unref);
+ }
+ if (plugin_loader->updates_changed_id != 0) {
+ g_source_remove (plugin_loader->updates_changed_id);
+ plugin_loader->updates_changed_id = 0;
+ }
+ if (plugin_loader->network_changed_handler != 0) {
+ g_signal_handler_disconnect (plugin_loader->network_monitor,
+ plugin_loader->network_changed_handler);
+ plugin_loader->network_changed_handler = 0;
+ }
+ if (plugin_loader->network_available_notify_handler != 0) {
+ g_signal_handler_disconnect (plugin_loader->network_monitor,
+ plugin_loader->network_available_notify_handler);
+ plugin_loader->network_available_notify_handler = 0;
+ }
+ if (plugin_loader->network_metered_notify_handler != 0) {
+ g_signal_handler_disconnect (plugin_loader->network_monitor,
+ plugin_loader->network_metered_notify_handler);
+ plugin_loader->network_metered_notify_handler = 0;
+ }
+ if (plugin_loader->queued_ops_pool != NULL) {
+ /* stop accepting more requests and wait until any currently
+ * running ones are finished */
+ g_thread_pool_free (plugin_loader->queued_ops_pool, TRUE, TRUE);
+ plugin_loader->queued_ops_pool = NULL;
+ }
+ g_clear_object (&plugin_loader->network_monitor);
+ g_clear_object (&plugin_loader->settings);
+ g_clear_object (&plugin_loader->pending_apps);
+ g_clear_object (&plugin_loader->category_manager);
+ g_clear_object (&plugin_loader->odrs_provider);
+ g_clear_object (&plugin_loader->setup_complete_cancellable);
+ g_clear_object (&plugin_loader->pending_apps_cancellable);
+
+#ifdef HAVE_SYSPROF
+ g_clear_pointer (&plugin_loader->sysprof_writer, sysprof_capture_writer_unref);
+#endif
+
+ g_clear_object (&plugin_loader->session_bus_connection);
+ g_clear_object (&plugin_loader->system_bus_connection);
+
+ 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);
+
+ g_strfreev (plugin_loader->compatible_projects);
+ g_ptr_array_unref (plugin_loader->locations);
+ g_free (plugin_loader->language);
+ g_ptr_array_unref (plugin_loader->file_monitors);
+ g_hash_table_unref (plugin_loader->events_by_id);
+ g_hash_table_unref (plugin_loader->disallow_updates);
+
+ g_mutex_clear (&plugin_loader->pending_apps_mutex);
+ g_mutex_clear (&plugin_loader->events_by_id_mutex);
+
+ G_OBJECT_CLASS (gs_plugin_loader_parent_class)->finalize (object);
+}
+
+static void
+gs_plugin_loader_class_init (GsPluginLoaderClass *klass)
+{
+ 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;
+
+ /**
+ * GsPluginLoader:events:
+ *
+ * Events added on the plugin loader using gs_plugin_loader_add_event().
+ */
+ obj_props[PROP_EVENTS] =
+ g_param_spec_string ("events", NULL, NULL,
+ NULL,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPluginLoader:allow-updates:
+ *
+ * Whether updates and upgrades are managed by gnome-software.
+ *
+ * If not, the updates UI should be hidden and no automatic updates
+ * performed.
+ */
+ obj_props[PROP_ALLOW_UPDATES] =
+ g_param_spec_boolean ("allow-updates", NULL, NULL,
+ TRUE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPluginLoader:network-available:
+ *
+ * Whether the network is considered available.
+ *
+ * This has the same semantics as #GNetworkMonitor:network-available.
+ */
+ obj_props[PROP_NETWORK_AVAILABLE] =
+ g_param_spec_boolean ("network-available", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPluginLoader:network-metered:
+ *
+ * Whether the network is considered metered.
+ *
+ * This has the same semantics as #GNetworkMonitor:network-metered.
+ */
+ obj_props[PROP_NETWORK_METERED] =
+ g_param_spec_boolean ("network-metered", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPluginLoader:session-bus-connection: (nullable)
+ *
+ * A connection to the D-Bus session bus.
+ *
+ * This may be %NULL at construction time. If so, the default session
+ * bus connection will be used (and returned as the value of this
+ * property) after gs_plugin_loader_setup_async() is called.
+ *
+ * Since: 43
+ */
+ obj_props[PROP_SESSION_BUS_CONNECTION] =
+ g_param_spec_object ("session-bus-connection", NULL, NULL,
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * GsPluginLoader:system-bus-connection: (not nullable)
+ *
+ * A connection to the D-Bus system bus.
+ *
+ * This may be %NULL at construction time. If so, the default system
+ * bus connection will be used (and returned as the value of this
+ * property) after gs_plugin_loader_setup_async() is called.
+ *
+ * Since: 43
+ */
+ obj_props[PROP_SYSTEM_BUS_CONNECTION] =
+ g_param_spec_object ("system-bus-connection", NULL, NULL,
+ G_TYPE_DBUS_CONNECTION,
+ G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);
+
+ signals [SIGNAL_STATUS_CHANGED] =
+ g_signal_new ("status-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, 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,
+ 0, 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,
+ 0, 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,
+ 0, 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,
+ 0, NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER);
+ signals [SIGNAL_ASK_UNTRUSTED] =
+ g_signal_new ("ask-untrusted",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_BOOLEAN, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING);
+}
+
+static void
+gs_plugin_loader_allow_updates_recheck (GsPluginLoader *plugin_loader)
+{
+ if (g_settings_get_boolean (plugin_loader->settings, "allow-updates")) {
+ g_hash_table_remove (plugin_loader->disallow_updates, plugin_loader);
+ } else {
+ g_hash_table_insert (plugin_loader->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)
+{
+ const gchar *tmp;
+ gchar *match;
+ gchar **projects;
+ guint i;
+ g_autofree gchar *review_server = NULL;
+ g_autofree gchar *user_hash = NULL;
+ g_autoptr(GError) local_error = NULL;
+ const guint64 odrs_review_max_cache_age_secs = 237000; /* 1 week */
+ const guint odrs_review_n_results_max = 20;
+ const gchar *locale;
+
+#ifdef HAVE_SYSPROF
+ plugin_loader->sysprof_writer = sysprof_capture_writer_new_from_env (0);
+#endif /* HAVE_SYSPROF */
+
+ plugin_loader->setup_complete_cancellable = g_cancellable_new ();
+ plugin_loader->scale = 1;
+ plugin_loader->plugins = g_ptr_array_new_with_free_func (g_object_unref);
+ plugin_loader->pending_apps = NULL;
+ plugin_loader->queued_ops_pool = g_thread_pool_new (gs_plugin_loader_process_in_thread_pool_cb,
+ NULL,
+ get_max_parallel_ops (),
+ FALSE,
+ NULL);
+ plugin_loader->file_monitors = g_ptr_array_new_with_free_func (g_object_unref);
+ plugin_loader->locations = g_ptr_array_new_with_free_func (g_free);
+ plugin_loader->settings = g_settings_new ("org.gnome.software");
+ g_signal_connect (plugin_loader->settings, "changed",
+ G_CALLBACK (gs_plugin_loader_settings_changed_cb), plugin_loader);
+ plugin_loader->events_by_id = g_hash_table_new_full ((GHashFunc) as_utils_data_id_hash,
+ (GEqualFunc) as_utils_data_id_equal,
+ g_free,
+ (GDestroyNotify) g_object_unref);
+
+ /* get the category manager */
+ plugin_loader->category_manager = gs_category_manager_new ();
+
+ /* set up the ODRS provider */
+
+ /* get the machine+user ID hash value */
+ user_hash = gs_utils_get_user_hash (&local_error);
+ if (user_hash == NULL) {
+ g_warning ("Failed to get machine+user hash: %s", local_error->message);
+ plugin_loader->odrs_provider = NULL;
+ } else {
+ review_server = g_settings_get_string (plugin_loader->settings, "review-server");
+
+ if (review_server != NULL && *review_server != '\0') {
+ const gchar *distro = NULL;
+ g_autoptr(GsOsRelease) os_release = NULL;
+ g_autoptr(SoupSession) odrs_soup_session = NULL;
+
+ /* get the distro name (e.g. 'Fedora') but allow a fallback */
+ os_release = gs_os_release_new (&local_error);
+ if (os_release != NULL) {
+ distro = gs_os_release_get_name (os_release);
+ if (distro == NULL)
+ g_warning ("no distro name specified");
+ } else {
+ g_warning ("failed to get distro name: %s", local_error->message);
+ }
+
+ /* Fallback */
+ if (distro == NULL)
+ distro = C_("Distribution name", "Unknown");
+
+ odrs_soup_session = gs_build_soup_session ();
+ plugin_loader->odrs_provider = gs_odrs_provider_new (review_server,
+ user_hash,
+ distro,
+ odrs_review_max_cache_age_secs,
+ odrs_review_n_results_max,
+ odrs_soup_session);
+ }
+ }
+
+ /* the settings key sets the initial override */
+ plugin_loader->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) */
+ locale = setlocale (LC_MESSAGES, NULL);
+ plugin_loader->language = g_strdup (locale);
+ match = strpbrk (plugin_loader->language, "._@");
+ if (match != NULL)
+ *match = '\0';
+
+ g_debug ("Using locale = %s, language = %s", locale, plugin_loader->language);
+
+ g_mutex_init (&plugin_loader->pending_apps_mutex);
+ g_mutex_init (&plugin_loader->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 (plugin_loader->settings,
+ "compatible-projects");
+ } else {
+ projects = g_strsplit (tmp, ",", -1);
+ }
+ for (i = 0; projects[i] != NULL; i++)
+ g_debug ("compatible-project: %s", projects[i]);
+ plugin_loader->compatible_projects = projects;
+}
+
+/**
+ * gs_plugin_loader_new:
+ * @session_bus_connection: (nullable) (transfer none): a D-Bus session bus
+ * connection to use, or %NULL to use the default
+ * @system_bus_connection: (nullable) (transfer none): a D-Bus system bus
+ * connection to use, or %NULL to use the default
+ *
+ * Create a new #GsPluginLoader.
+ *
+ * The D-Bus connection arguments should typically be %NULL, and only be
+ * non-%NULL when doing unit tests.
+ *
+ * Return value: (transfer full) (not nullable): a new #GsPluginLoader
+ * Since: 43
+ **/
+GsPluginLoader *
+gs_plugin_loader_new (GDBusConnection *session_bus_connection,
+ GDBusConnection *system_bus_connection)
+{
+ g_return_val_if_fail (session_bus_connection == NULL || G_IS_DBUS_CONNECTION (session_bus_connection), NULL);
+ g_return_val_if_fail (system_bus_connection == NULL || G_IS_DBUS_CONNECTION (system_bus_connection), NULL);
+
+ return g_object_new (GS_TYPE_PLUGIN_LOADER,
+ "session-bus-connection", session_bus_connection,
+ "system-bus-connection", system_bus_connection,
+ NULL);
+}
+
+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);
+ remove_app_from_install_queue (plugin_loader, app);
+ if (!ret) {
+ gs_app_set_state_recover (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)
+{
+ if (plugin_loader->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-available=TRUE");
+ return TRUE;
+ }
+ return g_network_monitor_get_network_available (plugin_loader->network_monitor);
+}
+
+gboolean
+gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader)
+{
+ if (plugin_loader->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-metered=FALSE");
+ return FALSE;
+ }
+ return g_network_monitor_get_network_metered (plugin_loader->network_monitor);
+}
+
+static void
+gs_plugin_loader_pending_apps_refined_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ g_autoptr(GsAppList) old_queue = GS_APP_LIST (user_data);
+ g_autoptr(GsAppList) refined_queue = NULL;
+ g_autoptr(GError) error = NULL;
+
+ refined_queue = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
+
+ if (refined_queue == NULL) {
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+ !g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+ g_debug ("failed to refine pending apps: %s", error->message);
+
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ g_clear_object (&plugin_loader->pending_apps);
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+
+ save_install_queue (plugin_loader);
+ }
+ return;
+ }
+
+ for (guint i = 0; i < gs_app_list_length (old_queue); i++) {
+ GsApp *app = gs_app_list_index (old_queue, i);
+
+ if (gs_app_list_lookup (refined_queue, gs_app_get_unique_id (app)) == NULL)
+ remove_app_from_install_queue (plugin_loader, app);
+ }
+
+ for (guint i = 0; i < gs_app_list_length (refined_queue); i++) {
+ GsApp *app = gs_app_list_index (refined_queue, i);
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_REPOSITORY) {
+ plugin_job = gs_plugin_job_manage_repository_new (app,
+ GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INTERACTIVE |
+ GS_PLUGIN_MANAGE_REPOSITORY_FLAGS_INSTALL);
+ } else {
+ /* The 'interactive' is needed for credentials prompt, otherwise it just fails */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ "interactive", TRUE,
+ NULL);
+ }
+
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ gs_app_get_cancellable (app),
+ gs_plugin_loader_app_installed_cb,
+ g_object_ref (app));
+ }
+
+ g_clear_object (&plugin_loader->pending_apps_cancellable);
+}
+
+static void
+gs_plugin_loader_maybe_flush_pending_install_queue (GsPluginLoader *plugin_loader)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GsAppList) obsolete = NULL;
+ g_autoptr(GsAppList) queue = NULL;
+
+ if (!gs_plugin_loader_get_network_available (plugin_loader) ||
+ gs_plugin_loader_get_network_metered (plugin_loader)) {
+ /* Print the debug message only when had anything to skip */
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ if (plugin_loader->pending_apps != NULL) {
+ g_debug ("Cannot flush pending install queue, because is %sonline and is %smetered",
+ !gs_plugin_loader_get_network_available (plugin_loader) ? "not " : "",
+ gs_plugin_loader_get_network_metered (plugin_loader) ? "" : "not ");
+ }
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+ return;
+ }
+
+ /* Already flushing pending queue */
+ if (plugin_loader->pending_apps_cancellable)
+ return;
+
+ queue = gs_app_list_new ();
+ obsolete = gs_app_list_new ();
+ g_mutex_lock (&plugin_loader->pending_apps_mutex);
+ for (guint i = 0; plugin_loader->pending_apps != NULL && i < gs_app_list_length (plugin_loader->pending_apps); i++) {
+ GsApp *app = gs_app_list_index (plugin_loader->pending_apps, i);
+ if (gs_app_get_state (app) == GS_APP_STATE_QUEUED_FOR_INSTALL) {
+ gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+ gs_app_list_add (queue, app);
+ } else {
+ gs_app_list_add (obsolete, app);
+ }
+ }
+ g_mutex_unlock (&plugin_loader->pending_apps_mutex);
+ for (guint i = 0; i < gs_app_list_length (obsolete); i++) {
+ GsApp *app = gs_app_list_index (obsolete, i);
+ remove_app_from_install_queue (plugin_loader, app);
+ }
+
+ plugin_loader->pending_apps_cancellable = g_cancellable_new ();
+
+ plugin_job = gs_plugin_job_refine_new (queue, GS_PLUGIN_REFINE_FLAGS_NONE);
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ plugin_loader->pending_apps_cancellable,
+ gs_plugin_loader_pending_apps_refined_cb,
+ g_steal_pointer (&queue));
+}
+
+static void
+gs_plugin_loader_network_changed_cb (GNetworkMonitor *monitor,
+ gboolean available,
+ GsPluginLoader *plugin_loader)
+{
+ gboolean metered = g_network_monitor_get_network_metered (plugin_loader->network_monitor);
+
+ g_debug ("network status change: %s [%s]",
+ available ? "online" : "offline",
+ metered ? "metered" : "unmetered");
+
+ g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_NETWORK_AVAILABLE]);
+ g_object_notify_by_pspec (G_OBJECT (plugin_loader), obj_props[PROP_NETWORK_METERED]);
+
+ gs_plugin_loader_maybe_flush_pending_install_queue (plugin_loader);
+}
+
+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)
+{
+ GNetworkMonitor *network_monitor;
+
+ network_monitor = g_network_monitor_get_default ();
+ if (network_monitor == NULL || plugin_loader->network_changed_handler != 0)
+ return;
+ plugin_loader->network_monitor = g_object_ref (network_monitor);
+
+ plugin_loader->network_changed_handler =
+ g_signal_connect (plugin_loader->network_monitor, "network-changed",
+ G_CALLBACK (gs_plugin_loader_network_changed_cb), plugin_loader);
+ plugin_loader->network_available_notify_handler =
+ g_signal_connect (plugin_loader->network_monitor, "notify::network-available",
+ G_CALLBACK (gs_plugin_loader_network_available_notify_cb), plugin_loader);
+ plugin_loader->network_metered_notify_handler =
+ g_signal_connect (plugin_loader->network_monitor, "notify::network-metered",
+ G_CALLBACK (gs_plugin_loader_network_metered_notify_cb), plugin_loader);
+
+ gs_plugin_loader_network_changed_cb (plugin_loader->network_monitor,
+ g_network_monitor_get_network_available (plugin_loader->network_monitor),
+ plugin_loader);
+}
+
+/******************************************************************************/
+
+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)
+{
+ 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 < plugin_loader->plugins->len; i++) {
+ GsPluginActionFunc plugin_app_func = NULL;
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->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) == GS_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);
+ }
+
+ if (gs_plugin_job_get_action (helper->plugin_job) == GS_PLUGIN_ACTION_UPDATE)
+ gs_utils_set_online_updates_timestamp (plugin_loader->settings);
+
+ return TRUE;
+}
+
+static void
+gs_plugin_loader_inherit_list_props (GsAppList *des_list,
+ GsAppList *src_list)
+{
+ if (gs_app_list_has_flag (src_list, GS_APP_LIST_FLAG_IS_TRUNCATED))
+ gs_app_list_add_flag (des_list, GS_APP_LIST_FLAG_IS_TRUNCATED);
+
+ gs_app_list_set_size_peak (des_list, gs_app_list_get_size_peak (src_list));
+}
+
+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;
+ g_autoptr(GsAppList) list = g_object_ref (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);
+ gboolean add_to_pending_array = FALSE;
+ g_autoptr(GMainContext) context = g_main_context_new ();
+ g_autoptr(GMainContextPusher) pusher = g_main_context_pusher_new (context);
+ g_autofree gchar *job_debug = NULL;
+#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 (!GS_IS_PLUGIN_JOB_REFINE (helper->plugin_job)) {
+ 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;
+ }
+
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP) {
+ const gchar *search = gs_plugin_job_get_search (helper->plugin_job);
+ if (search && g_ascii_strncasecmp (search, "file://", 7) == 0 && (
+ gs_plugin_job_get_list (helper->plugin_job) == NULL ||
+ gs_app_list_length (gs_plugin_job_get_list (helper->plugin_job)) == 0)) {
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GFile) file = NULL;
+ file = g_file_new_for_uri (search);
+ gs_plugin_job_set_action (helper->plugin_job, GS_PLUGIN_ACTION_FILE_TO_APP);
+ gs_plugin_job_set_file (helper->plugin_job, file);
+ helper->function_name = gs_plugin_action_to_function_name (GS_PLUGIN_ACTION_FILE_TO_APP);
+ if (gs_plugin_loader_run_results (helper, cancellable, &local_error)) {
+ 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));
+ }
+ } else {
+ g_debug ("Failed to convert file:// URI to app using file-to-app action: %s", local_error->message);
+ }
+ gs_plugin_job_set_action (helper->plugin_job, GS_PLUGIN_ACTION_URL_TO_APP);
+ gs_plugin_job_set_file (helper->plugin_job, NULL);
+ }
+ }
+ }
+
+ /* 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 (plugin_loader->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_GET_UPDATES:
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ case GS_PLUGIN_ACTION_LAUNCH:
+ case GS_PLUGIN_ACTION_REMOVE:
+ 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;
+ default:
+ if (!helper->anything_ran && !GS_IS_PLUGIN_JOB_REFINE (helper->plugin_job)) {
+ g_debug ("no plugin could handle %s",
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ }
+
+ /* unstage addons */
+ if (add_to_pending_array) {
+ g_autoptr(GsAppList) addons = gs_app_dup_addons (gs_plugin_job_get_app (helper->plugin_job));
+
+ for (guint i = 0; addons != NULL && 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);
+ }
+ }
+
+ /* filter to reduce to a sane set */
+ gs_plugin_loader_job_sorted_truncation (helper->plugin_job, list);
+
+ /* 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 &&
+ list != NULL &&
+ gs_app_list_length (list) > 0) {
+ g_autoptr(GsPluginJob) refine_job = NULL;
+ g_autoptr(GAsyncResult) refine_result = NULL;
+ g_autoptr(GsAppList) new_list = NULL;
+
+ refine_job = gs_plugin_job_refine_new (list, gs_plugin_job_get_refine_flags (helper->plugin_job) | GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING);
+ gs_plugin_loader_job_process_async (plugin_loader, refine_job,
+ cancellable,
+ async_result_cb,
+ &refine_result);
+
+ /* FIXME: Make this sync until the enclosing function is
+ * refactored to be async. */
+ while (refine_result == NULL)
+ g_main_context_iteration (g_main_context_get_thread_default (), TRUE);
+
+ new_list = gs_plugin_loader_job_process_finish (plugin_loader, refine_result, &error);
+ if (new_list == NULL) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ gs_plugin_loader_inherit_list_props (new_list, list);
+
+ /* Update the app list in case the refine resolved any wildcards. */
+ g_set_object (&list, new_list);
+ } 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: {
+ g_autoptr(GsPluginJob) refine_job = NULL;
+ g_autoptr(GAsyncResult) refine_result = NULL;
+ g_autoptr(GsAppList) new_list = NULL;
+
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_icons (app) == NULL) {
+ g_autoptr(GIcon) ic = NULL;
+ const gchar *icon_name;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_HAS_SOURCE))
+ icon_name = "x-package-repository";
+ else
+ icon_name = "system-component-application";
+ ic = g_themed_icon_new (icon_name);
+ gs_app_add_icon (app, ic);
+ }
+ }
+
+ refine_job = gs_plugin_job_refine_new (list, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON | GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING);
+ gs_plugin_loader_job_process_async (plugin_loader, refine_job,
+ cancellable,
+ async_result_cb,
+ &refine_result);
+
+ /* FIXME: Make this sync until the enclosing function is
+ * refactored to be async. */
+ while (refine_result == NULL)
+ g_main_context_iteration (g_main_context_get_thread_default (), TRUE);
+
+ new_list = gs_plugin_loader_job_process_finish (plugin_loader, refine_result, &error);
+ if (new_list == NULL) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, g_steal_pointer (&error));
+ return;
+ }
+
+ gs_plugin_loader_inherit_list_props (new_list, list);
+
+ /* Update the app list in case the refine resolved any wildcards. */
+ g_set_object (&list, new_list);
+
+ 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_filter, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid_updatable, helper);
+ 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_set_error (&error_local,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no application was created for %s", str);
+ if (!gs_plugin_job_get_propagate_error (helper->plugin_job))
+ gs_plugin_loader_claim_job_error (plugin_loader, NULL, helper->plugin_job, error_local);
+ 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 */
+ 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->plugin_job, list);
+
+#ifdef HAVE_SYSPROF
+ if (plugin_loader->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 (plugin_loader->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 */
+ job_debug = gs_plugin_job_to_string (helper->plugin_job);
+ g_debug ("%s", job_debug);
+
+ /* 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);
+ GsPluginLoaderHelper *helper = g_task_get_task_data (task);
+ GsApp *app = gs_plugin_job_get_app (helper->plugin_job);
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+
+ gs_ioprio_set (G_PRIORITY_LOW);
+
+ gs_plugin_loader_process_thread_cb (task, source_object, task_data, cancellable);
+
+ /* Clear any pending action set in gs_plugin_loader_schedule_task() */
+ if (app != NULL && gs_app_get_pending_action (app) == action)
+ gs_app_set_pending_action (app, GS_PLUGIN_ACTION_UNKNOWN);
+
+ g_object_unref (task);
+}
+
+static void
+gs_plugin_loader_cancelled_cb (GCancellable *cancellable,
+ gpointer user_data)
+{
+ GCancellable *child_cancellable = G_CANCELLABLE (user_data);
+
+ /* just proxy this forward */
+ g_debug ("Cancelling job with cancellable %p", child_cancellable);
+ g_cancellable_cancel (child_cancellable);
+}
+
+static void
+gs_plugin_loader_schedule_task (GsPluginLoader *plugin_loader,
+ GTask *task)
+{
+ GsPluginLoaderHelper *helper = g_task_get_task_data (task);
+ 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);
+
+ if (action == GS_PLUGIN_ACTION_INSTALL &&
+ gs_app_get_state (app) != GS_APP_STATE_AVAILABLE_LOCAL)
+ add_app_to_install_queue (plugin_loader, app);
+ }
+ g_thread_pool_push (plugin_loader->queued_ops_pool, g_object_ref (task), NULL);
+}
+
+static void
+run_job_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ GsPluginJob *plugin_job = GS_PLUGIN_JOB (source_object);
+ GsPluginJobClass *job_class;
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ g_autoptr(GError) local_error = NULL;
+#ifdef HAVE_SYSPROF
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ gint64 begin_time_nsec = GPOINTER_TO_SIZE (g_task_get_task_data (task));
+
+ if (plugin_loader->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = g_strconcat ("process-thread:", G_OBJECT_TYPE_NAME (plugin_job), NULL);
+ g_autofree gchar *sysprof_message = gs_plugin_job_to_string (plugin_job);
+ sysprof_capture_writer_add_mark (plugin_loader->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* FIXME: This will eventually go away when
+ * gs_plugin_loader_job_process_finish() is removed. */
+ job_class = GS_PLUGIN_JOB_GET_CLASS (plugin_job);
+
+ g_assert (job_class->run_finish != NULL);
+
+ if (!job_class->run_finish (plugin_job, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (GS_IS_PLUGIN_JOB_REFINE (plugin_job)) {
+ GsAppList *list = gs_plugin_job_refine_get_result_list (GS_PLUGIN_JOB_REFINE (plugin_job));
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+ return;
+ } else if (GS_IS_PLUGIN_JOB_LIST_APPS (plugin_job)) {
+ GsAppList *list = gs_plugin_job_list_apps_get_result_list (GS_PLUGIN_JOB_LIST_APPS (plugin_job));
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+ return;
+ } else if (GS_IS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (plugin_job)) {
+ GsAppList *list = gs_plugin_job_list_distro_upgrades_get_result_list (GS_PLUGIN_JOB_LIST_DISTRO_UPGRADES (plugin_job));
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+ return;
+ } else if (GS_IS_PLUGIN_JOB_REFRESH_METADATA (plugin_job)) {
+ /* FIXME: For some reason, existing callers of refresh jobs
+ * expect a #GsAppList instance back, even though it’s empty and
+ * they don’t use its contents. It’s just used to distinguish
+ * against returning an error. This will go away when
+ * job_process_async() does. */
+ g_task_return_pointer (task, gs_app_list_new (), g_object_unref);
+ return;
+ } else if (GS_IS_PLUGIN_JOB_MANAGE_REPOSITORY (plugin_job) ||
+ GS_IS_PLUGIN_JOB_LIST_CATEGORIES (plugin_job)) {
+ /* FIXME: The gs_plugin_loader_job_action_finish() expects a #GsAppList
+ * pointer on success, thus return it. */
+ g_task_return_pointer (task, gs_app_list_new (), g_object_unref);
+ return;
+ }
+
+ g_assert_not_reached ();
+}
+
+typedef struct {
+ GWeakRef parent_cancellable_weak;
+ gulong handler_id;
+} CancellableData;
+
+static void
+cancellable_data_free (CancellableData *data)
+{
+ g_autoptr(GCancellable) parent_cancellable = g_weak_ref_get (&data->parent_cancellable_weak);
+
+ if (parent_cancellable != NULL)
+ g_cancellable_disconnect (parent_cancellable, data->handler_id);
+
+ g_weak_ref_clear (&data->parent_cancellable_weak);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (CancellableData, cancellable_data_free)
+
+static void
+plugin_loader_task_freed_cb (gpointer user_data,
+ GObject *freed_object)
+{
+ g_autoptr(GsPluginLoader) plugin_loader = user_data;
+ if (g_atomic_int_dec_and_test (&plugin_loader->active_jobs)) {
+ /* if the plugin used updates-changed during its job, actually schedule
+ * the signal emission now */
+ if (plugin_loader->updates_changed_cnt > 0)
+ gs_plugin_loader_updates_changed (plugin_loader);
+ }
+}
+
+static gboolean job_process_setup_complete_cb (GCancellable *cancellable,
+ gpointer user_data);
+static void job_process_cb (GTask *task);
+
+/**
+ * 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.
+ *
+ * If the #GsPluginLoader is still being set up, this function will wait until
+ * setup is complete before running.
+ **/
+void
+gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginJobClass *job_class;
+ GsPluginAction action;
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GCancellable) cancellable_job = NULL;
+ g_autofree gchar *task_name = 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));
+
+ job_class = GS_PLUGIN_JOB_GET_CLASS (plugin_job);
+ action = gs_plugin_job_get_action (plugin_job);
+
+ if (job_class->run_async != NULL) {
+ task_name = g_strdup_printf ("%s %s", G_STRFUNC, G_OBJECT_TYPE_NAME (plugin_job));
+ cancellable_job = (cancellable != NULL) ? g_object_ref (cancellable) : NULL;
+ } else {
+ task_name = g_strdup_printf ("%s %s", G_STRFUNC, gs_plugin_action_to_string (action));
+ cancellable_job = g_cancellable_new ();
+
+ /* Old-style jobs always have a valid cancellable, so proxy the caller */
+ g_debug ("Chaining cancellation from %p to %p", cancellable, cancellable_job);
+ if (cancellable != NULL) {
+ g_autoptr(CancellableData) cancellable_data = NULL;
+
+ cancellable_data = g_new0 (CancellableData, 1);
+ g_weak_ref_init (&cancellable_data->parent_cancellable_weak, cancellable);
+ cancellable_data->handler_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (gs_plugin_loader_cancelled_cb),
+ cancellable_job, NULL);
+
+ g_object_set_data_full (G_OBJECT (cancellable_job),
+ "gs-cancellable-chain",
+ g_steal_pointer (&cancellable_data),
+ (GDestroyNotify) cancellable_data_free);
+ }
+ }
+
+ task = g_task_new (plugin_loader, cancellable_job, callback, user_data);
+ g_task_set_name (task, task_name);
+ g_task_set_task_data (task, g_object_ref (plugin_job), (GDestroyNotify) g_object_unref);
+
+ g_atomic_int_inc (&plugin_loader->active_jobs);
+ g_object_weak_ref (G_OBJECT (task),
+ plugin_loader_task_freed_cb, g_object_ref (plugin_loader));
+
+ /* Wait until the plugin has finished setting up.
+ *
+ * Do this using a #GCancellable. While we’re not using the #GCancellable
+ * to cancel anything, it is a reliable way to signal between threads
+ * without polling, waking up all waiting #GMainContexts when it’s
+ * ‘cancelled’. */
+ if (plugin_loader->setup_complete) {
+ job_process_cb (task);
+ } else {
+ g_autoptr(GSource) cancellable_source = g_cancellable_source_new (plugin_loader->setup_complete_cancellable);
+ g_task_attach_source (task, cancellable_source, G_SOURCE_FUNC (job_process_setup_complete_cb));
+ }
+}
+
+static gboolean
+job_process_setup_complete_cb (GCancellable *cancellable,
+ gpointer user_data)
+{
+ GTask *task = G_TASK (user_data);
+
+ job_process_cb (task);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+job_process_cb (GTask *task)
+{
+ g_autoptr(GsPluginJob) plugin_job = g_object_ref (g_task_get_task_data (task));
+ GsPluginLoader *plugin_loader = g_task_get_source_object (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ GsPluginJobClass *job_class;
+ GsPluginAction action;
+ GsPluginLoaderHelper *helper;
+
+ job_class = GS_PLUGIN_JOB_GET_CLASS (plugin_job);
+ action = gs_plugin_job_get_action (plugin_job);
+
+ /* If the job provides a more specific async run function, use that.
+ *
+ * FIXME: This will eventually go away when
+ * gs_plugin_loader_job_process_async() is removed. */
+
+ if (job_class->run_async != NULL) {
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+
+ g_task_set_task_data (task, GSIZE_TO_POINTER (begin_time_nsec), NULL);
+#endif
+
+ job_class->run_async (plugin_job, plugin_loader, cancellable,
+ run_job_cb, g_object_ref (task));
+ return;
+ }
+
+ /* check job has valid action */
+ if (action == GS_PLUGIN_ACTION_UNKNOWN) {
+ g_autofree gchar *job_str = gs_plugin_job_to_string (plugin_job);
+ 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);
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+ return;
+ }
+ }
+
+ /* FIXME: the plugins should specify this, rather than hardcoding */
+ 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_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_SOURCES) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION);
+ }
+
+ /* check required args */
+ switch (action) {
+ 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;
+ 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);
+
+ 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)
+{
+ for (guint i = 0; i < plugin_loader->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (plugin_loader->plugins, i);
+ if (gs_plugin_get_symbol (plugin, function_name) != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_plugin_loader_get_plugins:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Get the set of currently loaded plugins.
+ *
+ * This includes disabled plugins, which should be checked for using
+ * gs_plugin_get_enabled().
+ *
+ * This is intended to be used by internal gnome-software code. Plugin and UI
+ * code should typically use #GsPluginJob to run operations.
+ *
+ * Returns: (transfer none) (element-type GsPlugin): list of #GsPlugins
+ * Since: 42
+ */
+GPtrArray *
+gs_plugin_loader_get_plugins (GsPluginLoader *plugin_loader)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+
+ return plugin_loader->plugins;
+}
+
+static void app_create_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+/**
+ * gs_plugin_loader_app_create_async:
+ * @plugin_loader: a #GsPluginLoader
+ * @unique_id: a unique_id
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: function to call when complete
+ * @user_data: user data to pass to @callback
+ *
+ * Create a #GsApp identified by @unique_id asynchronously.
+ * Finish the call with gs_plugin_loader_app_create_finish().
+ *
+ * If the #GsPluginLoader is still being set up, this function will wait until
+ * setup is complete before running.
+ *
+ * Since: 41
+ **/
+void
+gs_plugin_loader_app_create_async (GsPluginLoader *plugin_loader,
+ const gchar *unique_id,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GsPluginJob) refine_job = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (unique_id != NULL);
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ task = g_task_new (plugin_loader, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_loader_app_create_async);
+ g_task_set_task_data (task, g_strdup (unique_id), g_free);
+
+ /* 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, AS_COMPONENT_KIND_UNKNOWN);
+ gs_app_list_add (list, app);
+
+ /* Refine the wildcard app. */
+ refine_job = gs_plugin_job_refine_new (list, GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID | GS_PLUGIN_REFINE_FLAGS_DISABLE_FILTERING);
+ gs_plugin_loader_job_process_async (plugin_loader, refine_job,
+ cancellable,
+ app_create_cb,
+ g_steal_pointer (&task));
+}
+
+static void
+app_create_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (g_task_get_source_object (task));
+ const gchar *unique_id = g_task_get_task_data (task);
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GError) local_error = NULL;
+
+ list = gs_plugin_loader_job_process_finish (plugin_loader, result, &local_error);
+ if (list == NULL) {
+ g_prefix_error (&local_error, "Failed to refine '%s': ", unique_id);
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* 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) {
+ g_task_return_pointer (task, g_object_ref (app_tmp), g_object_unref);
+ return;
+ }
+ }
+
+ /* 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));
+ g_task_return_pointer (task, g_object_ref (app_tmp), g_object_unref);
+ return;
+ }
+ }
+
+ /* does not exist */
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to create an app for '%s'", unique_id);
+}
+
+/**
+ * gs_plugin_loader_app_create_finish:
+ * @plugin_loader: a #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Finishes call to gs_plugin_loader_app_create_async().
+ *
+ * Returns: (transfer full): a #GsApp, or %NULL on error.
+ *
+ * Since: 41
+ **/
+GsApp *
+gs_plugin_loader_app_create_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ GsApp *app;
+
+ 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);
+
+ app = g_task_propagate_pointer (G_TASK (res), error);
+ gs_utils_error_convert_gio (error);
+ return app;
+}
+
+/**
+ * gs_plugin_loader_get_system_app_async:
+ * @plugin_loader: a #GsPluginLoader
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: function to call when complete
+ * @user_data: user data to pass to @callback
+ *
+ * Get the application that represents the currently installed OS
+ * asynchronously. Finish the call with gs_plugin_loader_get_system_app_finish().
+ *
+ * Since: 41
+ **/
+void
+gs_plugin_loader_get_system_app_async (GsPluginLoader *plugin_loader,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ gs_plugin_loader_app_create_async (plugin_loader, "*/*/*/system/*", cancellable, callback, user_data);
+}
+
+/**
+ * gs_plugin_loader_get_system_app_finish:
+ * @plugin_loader: a #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Finishes call to gs_plugin_loader_get_system_app_async().
+ *
+ * Returns: (transfer full): a #GsApp, which represents
+ * the currently installed OS, or %NULL on error.
+ *
+ * Since: 41
+ **/
+GsApp *
+gs_plugin_loader_get_system_app_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ return gs_plugin_loader_app_create_finish (plugin_loader, res, error);
+}
+
+/**
+ * gs_plugin_loader_get_odrs_provider:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Get the singleton #GsOdrsProvider which provides access to ratings and
+ * reviews data from ODRS.
+ *
+ * Returns: (transfer none) (nullable): a #GsOdrsProvider, or %NULL if disabled
+ * Since: 41
+ */
+GsOdrsProvider *
+gs_plugin_loader_get_odrs_provider (GsPluginLoader *plugin_loader)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+
+ return plugin_loader->odrs_provider;
+}
+
+/**
+ * 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;
+ if (max_ops == 0)
+ max_ops = get_max_parallel_ops ();
+ if (!g_thread_pool_set_max_threads (plugin_loader->queued_ops_pool, max_ops, &error))
+ g_warning ("Failed to set the maximum number of ops in parallel: %s",
+ error->message);
+}
+
+/**
+ * gs_plugin_loader_get_category_manager:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Get the category manager singleton.
+ *
+ * Returns: (transfer none): a category manager
+ * Since: 40
+ */
+GsCategoryManager *
+gs_plugin_loader_get_category_manager (GsPluginLoader *plugin_loader)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+
+ return plugin_loader->category_manager;
+}
+
+/**
+ * gs_plugin_loader_emit_updates_changed:
+ * @self: a #GsPluginLoader
+ *
+ * Emits the #GsPluginLoader:updates-changed signal in the nearest
+ * idle in the main thread.
+ *
+ * Since: 43
+ **/
+void
+gs_plugin_loader_emit_updates_changed (GsPluginLoader *self)
+{
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (self));
+
+ if (self->updates_changed_id != 0)
+ g_source_remove (self->updates_changed_id);
+
+ self->updates_changed_id =
+ g_idle_add_full (G_PRIORITY_HIGH_IDLE,
+ gs_plugin_loader_job_updates_changed_delay_cb,
+ g_object_ref (self), g_object_unref);
+}