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