diff options
Diffstat (limited to 'lib')
44 files changed, 19976 insertions, 0 deletions
diff --git a/lib/README.md b/lib/README.md new file mode 100644 index 0000000..628c58e --- /dev/null +++ b/lib/README.md @@ -0,0 +1,7 @@ +libgnomesoftware +================ + +This is a static library, and is not all API stable. + +Only the plugin headers installed into /usr/include/gnome-software should be +considered API. diff --git a/lib/gnome-software-private.h b/lib/gnome-software-private.h new file mode 100644 index 0000000..4cff5c7 --- /dev/null +++ b/lib/gnome-software-private.h @@ -0,0 +1,23 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#ifndef I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#define I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#endif + +#include <gnome-software.h> + +#include <gs-app-list-private.h> +#include <gs-app-private.h> +#include <gs-category-private.h> +#include <gs-os-release.h> +#include <gs-plugin-loader.h> +#include <gs-plugin-loader-sync.h> +#include <gs-plugin-private.h> diff --git a/lib/gnome-software.h b/lib/gnome-software.h new file mode 100644 index 0000000..4ceeda1 --- /dev/null +++ b/lib/gnome-software.h @@ -0,0 +1,24 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#ifndef I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#error You have to define I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE +#endif + +#include <gs-app.h> +#include <gs-app-list.h> +#include <gs-app-collation.h> +#include <gs-autocleanups.h> +#include <gs-category.h> +#include <gs-metered.h> +#include <gs-os-release.h> +#include <gs-plugin.h> +#include <gs-plugin-vfuncs.h> +#include <gs-utils.h> diff --git a/lib/gnome-software.pc.in b/lib/gnome-software.pc.in new file mode 100644 index 0000000..cde7122 --- /dev/null +++ b/lib/gnome-software.pc.in @@ -0,0 +1,12 @@ +prefix=@prefix@ +libdir=@libdir@ +includedir=@includedir@ +plugindir=@libdir@/gs-plugins-@GS_PLUGIN_API_VERSION@ + +Name: gnome-software +Description: GNOME Software is a software center for GNOME +Version: @VERSION@ +Requires.private: gthread-2.0 atk +Requires: gobject-2.0 gdk-3.0 appstream-glib libsoup-2.4 gio-unix-2.0 +Libs: -L${libdir} +Cflags: -I${includedir}/gnome-software diff --git a/lib/gs-app-collation.h b/lib/gs-app-collation.h new file mode 100644 index 0000000..892c755 --- /dev/null +++ b/lib/gs-app-collation.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app-list.h" + +G_BEGIN_DECLS + +GsAppList *gs_app_get_related (GsApp *app); +GsAppList *gs_app_get_addons (GsApp *app); +GsAppList *gs_app_get_history (GsApp *app); + +G_END_DECLS diff --git a/lib/gs-app-list-private.h b/lib/gs-app-list-private.h new file mode 100644 index 0000000..ea92b13 --- /dev/null +++ b/lib/gs-app-list-private.h @@ -0,0 +1,79 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app-list.h" + +G_BEGIN_DECLS + +/** + * GsAppListFlags: + * @GS_APP_LIST_FLAG_NONE: No flags set + * @GS_APP_LIST_FLAG_IS_RANDOMIZED: List has been randomized + * @GS_APP_LIST_FLAG_IS_TRUNCATED: List has been truncated + * @GS_APP_LIST_FLAG_WATCH_APPS: Applications will be monitored + * @GS_APP_LIST_FLAG_WATCH_APPS_RELATED: Applications related apps will be monitored + * @GS_APP_LIST_FLAG_WATCH_APPS_ADDONS: Applications addon apps will be monitored + * + * Flags used to describe the list. + **/ +typedef enum { + GS_APP_LIST_FLAG_NONE = 0, + GS_APP_LIST_FLAG_IS_RANDOMIZED = 1 << 0, + GS_APP_LIST_FLAG_IS_TRUNCATED = 1 << 1, + GS_APP_LIST_FLAG_WATCH_APPS = 1 << 2, + GS_APP_LIST_FLAG_WATCH_APPS_RELATED = 1 << 3, + GS_APP_LIST_FLAG_WATCH_APPS_ADDONS = 1 << 4, + /*< private >*/ + GS_APP_LIST_FLAG_LAST +} GsAppListFlags; + +/** + * GsAppListFilterFlags: + * @GS_APP_LIST_FILTER_FLAG_NONE: No flags set + * @GS_APP_LIST_FILTER_FLAG_KEY_ID: Filter by ID + * @GS_APP_LIST_FILTER_FLAG_KEY_SOURCE: Filter by default source + * @GS_APP_LIST_FILTER_FLAG_KEY_VERSION: Filter by version + * @GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED: Prefer installed applications + * @GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES: Filter using the provides ID + * + * Flags to use when filtering. The priority of each #GsApp is used to choose + * which application object to keep. + **/ +typedef enum { + GS_APP_LIST_FILTER_FLAG_NONE = 0, + GS_APP_LIST_FILTER_FLAG_KEY_ID = 1 << 0, + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE = 1 << 1, + GS_APP_LIST_FILTER_FLAG_KEY_VERSION = 1 << 2, + GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED= 1 << 3, + GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES = 1 << 4, + /*< private >*/ + GS_APP_LIST_FILTER_FLAG_LAST, + GS_APP_LIST_FILTER_FLAG_MASK = G_MAXUINT64 +} GsAppListFilterFlags; + +/* All the properties which use #GsAppListFilterFlags are guint64s. */ +G_STATIC_ASSERT (sizeof (GsAppListFilterFlags) == sizeof (guint64)); + +GsAppList *gs_app_list_copy (GsAppList *list); +guint gs_app_list_get_size_peak (GsAppList *list); +void gs_app_list_filter_duplicates (GsAppList *list, + GsAppListFilterFlags flags); +void gs_app_list_randomize (GsAppList *list); +void gs_app_list_remove_all (GsAppList *list); +void gs_app_list_truncate (GsAppList *list, + guint length); +gboolean gs_app_list_has_flag (GsAppList *list, + GsAppListFlags flag); +void gs_app_list_add_flag (GsAppList *list, + GsAppListFlags flag); +AsAppState gs_app_list_get_state (GsAppList *list); +guint gs_app_list_get_progress (GsAppList *list); + +G_END_DECLS diff --git a/lib/gs-app-list.c b/lib/gs-app-list.c new file mode 100644 index 0000000..f2f599b --- /dev/null +++ b/lib/gs-app-list.c @@ -0,0 +1,963 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-app-list + * @title: GsAppList + * @include: gnome-software.h + * @stability: Unstable + * @short_description: An application list + * + * These functions provide a refcounted list of #GsApp objects. + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-app-private.h" +#include "gs-app-list-private.h" +#include "gs-app-collation.h" + +struct _GsAppList +{ + GObject parent_instance; + GPtrArray *array; + GMutex mutex; + guint size_peak; + GsAppListFlags flags; + AsAppState state; + guint progress; /* 0–100 inclusive, or %GS_APP_PROGRESS_UNKNOWN */ +}; + +G_DEFINE_TYPE (GsAppList, gs_app_list, G_TYPE_OBJECT) + +enum { + PROP_STATE = 1, + PROP_PROGRESS, + PROP_LAST +}; + +/** + * gs_app_list_get_state: + * @list: A #GsAppList + * + * Gets the state of the list. + * + * This method will only return a valid result if gs_app_list_add_flag() has + * been called with %GS_APP_LIST_FLAG_WATCH_APPS. + * + * Returns: the #AsAppState, e.g. %AS_APP_STATE_INSTALLED + * + * Since: 3.30 + **/ +AsAppState +gs_app_list_get_state (GsAppList *list) +{ + g_return_val_if_fail (GS_IS_APP_LIST (list), AS_APP_STATE_UNKNOWN); + return list->state; +} + +/** + * gs_app_list_get_progress: + * @list: A #GsAppList + * + * Gets the average percentage completion of all apps in the list. If any of the + * apps in the list has progress %GS_APP_PROGRESS_UNKNOWN, or if the app list + * is empty, %GS_APP_PROGRESS_UNKNOWN will be returned. + * + * This method will only return a valid result if gs_app_list_add_flag() has + * been called with %GS_APP_LIST_FLAG_WATCH_APPS. + * + * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown + * + * Since: 3.30 + **/ +guint +gs_app_list_get_progress (GsAppList *list) +{ + g_return_val_if_fail (GS_IS_APP_LIST (list), GS_APP_PROGRESS_UNKNOWN); + return list->progress; +} + +static void +gs_app_list_add_watched_for_app (GsAppList *list, GPtrArray *apps, GsApp *app) +{ + if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS) + g_ptr_array_add (apps, app); + if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_ADDONS) { + GsAppList *list2 = gs_app_get_addons (app); + for (guint i = 0; i < gs_app_list_length (list2); i++) { + GsApp *app2 = gs_app_list_index (list2, i); + g_ptr_array_add (apps, app2); + } + } + if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_RELATED) { + GsAppList *list2 = gs_app_get_related (app); + for (guint i = 0; i < gs_app_list_length (list2); i++) { + GsApp *app2 = gs_app_list_index (list2, i); + g_ptr_array_add (apps, app2); + } + } +} + +static GPtrArray * +gs_app_list_get_watched_for_app (GsAppList *list, GsApp *app) +{ + GPtrArray *apps = g_ptr_array_new (); + gs_app_list_add_watched_for_app (list, apps, app); + return apps; +} + +static GPtrArray * +gs_app_list_get_watched (GsAppList *list) +{ + GPtrArray *apps = g_ptr_array_new (); + for (guint i = 0; i < list->array->len; i++) { + GsApp *app_tmp = g_ptr_array_index (list->array, i); + gs_app_list_add_watched_for_app (list, apps, app_tmp); + } + return apps; +} + +static void +gs_app_list_invalidate_progress (GsAppList *self) +{ + guint progress = 0; + g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self); + + /* find the average percentage complete of the list */ + if (apps->len > 0) { + guint64 pc_cnt = 0; + gboolean unknown_seen = FALSE; + + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + guint app_progress = gs_app_get_progress (app_tmp); + + if (app_progress == GS_APP_PROGRESS_UNKNOWN) { + unknown_seen = TRUE; + break; + } + pc_cnt += gs_app_get_progress (app_tmp); + } + + progress = (!unknown_seen) ? pc_cnt / apps->len : GS_APP_PROGRESS_UNKNOWN; + } else { + progress = GS_APP_PROGRESS_UNKNOWN; + } + + if (self->progress != progress) { + self->progress = progress; + g_object_notify (G_OBJECT (self), "progress"); + } +} + +static void +gs_app_list_invalidate_state (GsAppList *self) +{ + AsAppState state = AS_APP_STATE_UNKNOWN; + g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self); + + /* find any action state of the list */ + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + AsAppState state_tmp = gs_app_get_state (app_tmp); + if (state_tmp == AS_APP_STATE_INSTALLING || + state_tmp == AS_APP_STATE_REMOVING) { + state = state_tmp; + break; + } + } + if (self->state != state) { + self->state = state; + g_object_notify (G_OBJECT (self), "state"); + } +} + +static void +gs_app_list_progress_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self) +{ + gs_app_list_invalidate_progress (self); +} + +static void +gs_app_list_state_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self) +{ + gs_app_list_invalidate_state (self); +} + +static void +gs_app_list_maybe_watch_app (GsAppList *list, GsApp *app) +{ + g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app); + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + g_signal_connect_object (app_tmp, "notify::progress", + G_CALLBACK (gs_app_list_progress_notify_cb), + list, 0); + g_signal_connect_object (app_tmp, "notify::state", + G_CALLBACK (gs_app_list_state_notify_cb), + list, 0); + } +} + +static void +gs_app_list_maybe_unwatch_app (GsAppList *list, GsApp *app) +{ + g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app); + for (guint i = 0; i < apps->len; i++) { + GsApp *app_tmp = g_ptr_array_index (apps, i); + g_signal_handlers_disconnect_by_data (app_tmp, list); + } +} + +/** + * gs_app_list_get_size_peak: + * @list: A #GsAppList + * + * Returns the largest size the list has ever been. + * + * Returns: integer + * + * Since: 3.24 + **/ +guint +gs_app_list_get_size_peak (GsAppList *list) +{ + return list->size_peak; +} + +static GsApp * +gs_app_list_lookup_safe (GsAppList *list, const gchar *unique_id) +{ + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = g_ptr_array_index (list->array, i); + if (as_utils_unique_id_equal (gs_app_get_unique_id (app), unique_id)) + return app; + } + return NULL; +} + +/** + * gs_app_list_lookup: + * @list: A #GsAppList + * @unique_id: A unique_id + * + * Finds the first matching application in the list using the usual wildcard + * rules allowed in unique_ids. + * + * Returns: (transfer none): a #GsApp, or %NULL if not found + * + * Since: 3.22 + **/ +GsApp * +gs_app_list_lookup (GsAppList *list, const gchar *unique_id) +{ + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&list->mutex); + return gs_app_list_lookup_safe (list, unique_id); +} + +/** + * gs_app_list_has_flag: + * @list: A #GsAppList + * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED + * + * Gets if a specific flag is set. + * + * Returns: %TRUE if the flag is set + * + * Since: 3.24 + **/ +gboolean +gs_app_list_has_flag (GsAppList *list, GsAppListFlags flag) +{ + return (list->flags & flag) > 0; +} + +/** + * gs_app_list_add_flag: + * @list: A #GsAppList + * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED + * + * Gets if a specific flag is set. + * + * Returns: %TRUE if the flag is set + * + * Since: 3.30 + **/ +void +gs_app_list_add_flag (GsAppList *list, GsAppListFlags flag) +{ + if (list->flags & flag) + return; + list->flags |= flag; + + /* turn this on for existing apps */ + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = g_ptr_array_index (list->array, i); + gs_app_list_maybe_watch_app (list, app); + } +} + +static gboolean +gs_app_list_check_for_duplicate (GsAppList *list, GsApp *app) +{ + GsApp *app_old; + const gchar *id; + + /* adding a wildcard */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + for (guint i = 0; i < list->array->len; i++) { + GsApp *app_tmp = g_ptr_array_index (list->array, i); + if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD)) + continue; + /* not adding exactly the same wildcard */ + if (g_strcmp0 (gs_app_get_unique_id (app_tmp), + gs_app_get_unique_id (app)) == 0) + return FALSE; + } + return TRUE; + } + + /* does not exist */ + id = gs_app_get_unique_id (app); + if (id == NULL) { + for (guint i = 0; i < list->array->len; i++) { + GsApp *app_tmp = g_ptr_array_index (list->array, i); + if (app_tmp == app) + return FALSE; + } + /* not much else we can do... */ + return TRUE; + } + + /* existing app is a wildcard */ + app_old = gs_app_list_lookup_safe (list, id); + if (app_old == NULL) + return TRUE; + if (gs_app_has_quirk (app_old, GS_APP_QUIRK_IS_WILDCARD)) + return TRUE; + + /* already exists */ + return FALSE; +} + +typedef enum { + GS_APP_LIST_ADD_FLAG_NONE = 0, + GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE = 1 << 0, + GS_APP_LIST_ADD_FLAG_LAST +} GsAppListAddFlag; + +static void +gs_app_list_add_safe (GsAppList *list, GsApp *app, GsAppListAddFlag flag) +{ + const gchar *id; + + /* check for duplicate */ + if ((flag & GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE) > 0 && + !gs_app_list_check_for_duplicate (list, app)) + return; + + /* if we're lazy-loading the ID then we can't use the ID hash */ + id = gs_app_get_unique_id (app); + if (id == NULL) { + gs_app_list_maybe_watch_app (list, app); + g_ptr_array_add (list->array, g_object_ref (app)); + return; + } + + /* just use the ref */ + gs_app_list_maybe_watch_app (list, app); + g_ptr_array_add (list->array, g_object_ref (app)); + + /* update the historical max */ + if (list->array->len > list->size_peak) + list->size_peak = list->array->len; +} + +/** + * gs_app_list_add: + * @list: A #GsAppList + * @app: A #GsApp + * + * If the application does not already exist in the list then it is added, + * incrementing the reference count. + * If the application already exists then a warning is printed to the console. + * + * Applications that have the application ID lazy-loaded will always be added + * to the list, and to clean these up the plugin loader will also call the + * gs_app_list_filter_duplicates() method when all plugins have run. + * + * Since: 3.22 + **/ +void +gs_app_list_add (GsAppList *list, GsApp *app) +{ + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&list->mutex); + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE); + + /* recalculate global state */ + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_remove: + * @list: A #GsAppList + * @app: A #GsApp + * + * Removes an application from the list. If the application does not exist the + * request is ignored. + * + * Since: 3.22 + **/ +void +gs_app_list_remove (GsAppList *list, GsApp *app) +{ + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&list->mutex); + g_ptr_array_remove (list->array, app); + gs_app_list_maybe_unwatch_app (list, app); + + /* recalculate global state */ + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_add_list: + * @list: A #GsAppList + * @donor: Another #GsAppList + * + * Adds all the applications in @donor to @list. + * + * Since: 3.22 + **/ +void +gs_app_list_add_list (GsAppList *list, GsAppList *donor) +{ + guint i; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (GS_IS_APP_LIST (donor)); + g_return_if_fail (list != donor); + + locker = g_mutex_locker_new (&list->mutex); + + /* add each app */ + for (i = 0; i < donor->array->len; i++) { + GsApp *app = gs_app_list_index (donor, i); + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE); + } + + /* recalculate global state */ + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_index: + * @list: A #GsAppList + * @idx: An index into the list + * + * Gets an application at a specific position in the list. + * + * Returns: (transfer none): a #GsApp, or %NULL if invalid + * + * Since: 3.22 + **/ +GsApp * +gs_app_list_index (GsAppList *list, guint idx) +{ + return GS_APP (g_ptr_array_index (list->array, idx)); +} + +/** + * gs_app_list_length: + * @list: A #GsAppList + * + * Gets the length of the application list. + * + * Returns: Integer + * + * Since: 3.22 + **/ +guint +gs_app_list_length (GsAppList *list) +{ + g_return_val_if_fail (GS_IS_APP_LIST (list), 0); + return list->array->len; +} + +static void +gs_app_list_remove_all_safe (GsAppList *list) +{ + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = g_ptr_array_index (list->array, i); + gs_app_list_maybe_unwatch_app (list, app); + } + g_ptr_array_set_size (list->array, 0); + gs_app_list_invalidate_state (list); + gs_app_list_invalidate_progress (list); +} + +/** + * gs_app_list_remove_all: + * @list: A #GsAppList + * + * Removes all applications from the list. + * + * Since: 3.22 + **/ +void +gs_app_list_remove_all (GsAppList *list) +{ + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP_LIST (list)); + locker = g_mutex_locker_new (&list->mutex); + gs_app_list_remove_all_safe (list); +} + +/** + * gs_app_list_filter: + * @list: A #GsAppList + * @func: A #GsAppListFilterFunc + * @user_data: the user pointer to pass to @func + * + * If func() returns TRUE for the GsApp, then the app is kept. + * + * Since: 3.22 + **/ +void +gs_app_list_filter (GsAppList *list, GsAppListFilterFunc func, gpointer user_data) +{ + guint i; + GsApp *app; + g_autoptr(GsAppList) old = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (func != NULL); + + locker = g_mutex_locker_new (&list->mutex); + + /* deep copy to a temp list and clear the current one */ + old = gs_app_list_copy (list); + gs_app_list_remove_all_safe (list); + + /* see if any of the apps need filtering */ + for (i = 0; i < old->array->len; i++) { + app = gs_app_list_index (old, i); + if (func (app, user_data)) + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE); + } +} + +typedef struct { + GsAppListSortFunc func; + gpointer user_data; +} GsAppListSortHelper; + +static gint +gs_app_list_sort_cb (gconstpointer a, gconstpointer b, gpointer user_data) +{ + GsApp *app1 = GS_APP (*(GsApp **) a); + GsApp *app2 = GS_APP (*(GsApp **) b); + GsAppListSortHelper *helper = (GsAppListSortHelper *) user_data; + return helper->func (app1, app2, user_data); +} + +/** + * gs_app_list_sort: + * @list: A #GsAppList + * @func: A #GCompareFunc + * @user_data: user data to pass to @func + * + * Sorts the application list. + * + * Since: 3.22 + **/ +void +gs_app_list_sort (GsAppList *list, GsAppListSortFunc func, gpointer user_data) +{ + g_autoptr(GMutexLocker) locker = NULL; + GsAppListSortHelper helper; + g_return_if_fail (GS_IS_APP_LIST (list)); + locker = g_mutex_locker_new (&list->mutex); + helper.func = func; + helper.user_data = user_data; + g_ptr_array_sort_with_data (list->array, gs_app_list_sort_cb, &helper); +} + +/** + * gs_app_list_truncate: + * @list: A #GsAppList + * @length: the new length + * + * Truncates the application list. It is an error if @length is larger than the + * size of the list. + * + * Since: 3.24 + **/ +void +gs_app_list_truncate (GsAppList *list, guint length) +{ + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + g_return_if_fail (length <= list->array->len); + + /* mark this list as unworthy */ + list->flags |= GS_APP_LIST_FLAG_IS_TRUNCATED; + + /* everything */ + if (length == 0) { + gs_app_list_remove_all (list); + return; + } + + /* remove the apps in the positions larger than the length */ + locker = g_mutex_locker_new (&list->mutex); + g_ptr_array_set_size (list->array, length); +} + +static gint +gs_app_list_randomize_cb (gconstpointer a, gconstpointer b, gpointer user_data) +{ + GsApp *app1 = GS_APP (*(GsApp **) a); + GsApp *app2 = GS_APP (*(GsApp **) b); + const gchar *k1; + const gchar *k2; + g_autofree gchar *key = NULL; + + key = g_strdup_printf ("Plugin::sort-key[%p]", user_data); + k1 = gs_app_get_metadata_item (app1, key); + k2 = gs_app_get_metadata_item (app2, key); + return g_strcmp0 (k1, k2); +} + +/** + * gs_app_list_randomize: + * @list: A #GsAppList + * + * Randomize the order of the list, but don't change the order until + * the next day. + * + * Since: 3.22 + **/ +void +gs_app_list_randomize (GsAppList *list) +{ + guint i; + GRand *rand; + GsApp *app; + gchar sort_key[] = { '\0', '\0', '\0', '\0' }; + g_autoptr(GDateTime) date = NULL; + g_autofree gchar *key = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + + locker = g_mutex_locker_new (&list->mutex); + + /* mark this list as random */ + list->flags |= GS_APP_LIST_FLAG_IS_RANDOMIZED; + + key = g_strdup_printf ("Plugin::sort-key[%p]", list); + rand = g_rand_new (); + date = g_date_time_new_now_utc (); + g_rand_set_seed (rand, (guint32) g_date_time_get_day_of_year (date)); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + sort_key[0] = (gchar) g_rand_int_range (rand, (gint32) 'A', (gint32) 'Z'); + sort_key[1] = (gchar) g_rand_int_range (rand, (gint32) 'A', (gint32) 'Z'); + sort_key[2] = (gchar) g_rand_int_range (rand, (gint32) 'A', (gint32) 'Z'); + gs_app_set_metadata (app, key, sort_key); + } + g_ptr_array_sort_with_data (list->array, gs_app_list_randomize_cb, list); + for (i = 0; i < gs_app_list_length (list); i++) { + app = gs_app_list_index (list, i); + gs_app_set_metadata (app, key, NULL); + } + g_rand_free (rand); +} + +static gboolean +gs_app_list_filter_app_is_better (GsApp *app, GsApp *found, GsAppListFilterFlags flags) +{ + /* optional 1st layer sort */ + if ((flags & GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED) > 0) { + if (gs_app_is_installed (app) && !gs_app_is_installed (found)) + return TRUE; + if (!gs_app_is_installed (app) && gs_app_is_installed (found)) + return FALSE; + } + + /* 2nd layer, priority and bundle kind */ + if (gs_app_compare_priority (app, found) < 0) + return TRUE; + + /* assume is worse */ + return FALSE; +} + +static GPtrArray * +gs_app_list_filter_app_get_keys (GsApp *app, GsAppListFilterFlags flags) +{ + GPtrArray *keys = g_ptr_array_new_with_free_func (g_free); + g_autoptr(GString) key = NULL; + + /* just use the unique ID */ + if (flags == GS_APP_LIST_FILTER_FLAG_NONE) { + if (gs_app_get_unique_id (app) != NULL) + g_ptr_array_add (keys, g_strdup (gs_app_get_unique_id (app))); + return keys; + } + + /* use the ID and any provides */ + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES) { + GPtrArray *provides = gs_app_get_provides (app); + g_ptr_array_add (keys, g_strdup (gs_app_get_id (app))); + for (guint i = 0; i < provides->len; i++) { + AsProvide *prov = g_ptr_array_index (provides, i); + if (as_provide_get_kind (prov) != AS_PROVIDE_KIND_ID) + continue; + g_ptr_array_add (keys, g_strdup (as_provide_get_value (prov))); + } + return keys; + } + + /* specific compound type */ + key = g_string_new (NULL); + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID) { + const gchar *tmp = gs_app_get_id (app); + if (tmp != NULL) + g_string_append (key, gs_app_get_id (app)); + } + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_SOURCE) { + const gchar *tmp = gs_app_get_source_default (app); + if (tmp != NULL) + g_string_append_printf (key, ":%s", tmp); + } + if (flags & GS_APP_LIST_FILTER_FLAG_KEY_VERSION) { + const gchar *tmp = gs_app_get_version (app); + if (tmp != NULL) + g_string_append_printf (key, ":%s", tmp); + } + if (key->len == 0) + return keys; + g_ptr_array_add (keys, g_string_free (g_steal_pointer (&key), FALSE)); + return keys; +} + +/** + * gs_app_list_filter_duplicates: + * @list: A #GsAppList + * @flags: a #GsAppListFilterFlags, e.g. GS_APP_LIST_FILTER_KEY_ID + * + * Filter any duplicate applications from the list. + * + * Since: 3.22 + **/ +void +gs_app_list_filter_duplicates (GsAppList *list, GsAppListFilterFlags flags) +{ + g_autoptr(GHashTable) hash = NULL; + g_autoptr(GHashTable) kept_apps = NULL; + g_autoptr(GsAppList) old = NULL; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP_LIST (list)); + + locker = g_mutex_locker_new (&list->mutex); + + /* a hash table to hold apps with unique app ids */ + hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + /* a hash table containing apps we want to keep */ + kept_apps = g_hash_table_new (g_direct_hash, g_direct_equal); + + for (guint i = 0; i < list->array->len; i++) { + GsApp *app = gs_app_list_index (list, i); + GsApp *found = NULL; + g_autoptr(GPtrArray) keys = NULL; + + /* get all the keys used to identify this app */ + keys = gs_app_list_filter_app_get_keys (app, flags); + for (guint j = 0; j < keys->len; j++) { + const gchar *key = g_ptr_array_index (keys, j); + found = g_hash_table_lookup (hash, key); + if (found != NULL) + break; + } + + /* new app */ + if (found == NULL) { + for (guint j = 0; j < keys->len; j++) { + const gchar *key = g_ptr_array_index (keys, j); + g_hash_table_insert (hash, g_strdup (key), app); + } + g_hash_table_add (kept_apps, app); + continue; + } + + /* better? */ + if (flags != GS_APP_LIST_FILTER_FLAG_NONE) { + if (gs_app_list_filter_app_is_better (app, found, flags)) { + for (guint j = 0; j < keys->len; j++) { + const gchar *key = g_ptr_array_index (keys, j); + g_hash_table_insert (hash, g_strdup (key), app); + } + g_hash_table_remove (kept_apps, found); + g_hash_table_add (kept_apps, app); + continue; + } + continue; + } + continue; + } + + /* deep copy to a temp list and clear the current one */ + old = gs_app_list_copy (list); + gs_app_list_remove_all_safe (list); + + /* add back the apps we want to keep */ + for (guint i = 0; i < old->array->len; i++) { + GsApp *app = gs_app_list_index (old, i); + if (g_hash_table_contains (kept_apps, app)) + gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE); + } +} + +/** + * gs_app_list_copy: + * @list: A #GsAppList + * + * Returns a deep copy of the application list. + * + * Returns: A newly allocated #GsAppList + * + * Since: 3.22 + **/ +GsAppList * +gs_app_list_copy (GsAppList *list) +{ + GsAppList *new; + guint i; + + g_return_val_if_fail (GS_IS_APP_LIST (list), NULL); + + new = gs_app_list_new (); + for (i = 0; i < gs_app_list_length (list); i++) { + GsApp *app = gs_app_list_index (list, i); + gs_app_list_add_safe (new, app, GS_APP_LIST_ADD_FLAG_NONE); + } + return new; +} + +static void +gs_app_list_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsAppList *self = GS_APP_LIST (object); + switch (prop_id) { + case PROP_STATE: + g_value_set_uint (value, self->state); + break; + case PROP_PROGRESS: + g_value_set_uint (value, self->progress); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_list_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_app_list_finalize (GObject *object) +{ + GsAppList *list = GS_APP_LIST (object); + g_ptr_array_unref (list->array); + g_mutex_clear (&list->mutex); + G_OBJECT_CLASS (gs_app_list_parent_class)->finalize (object); +} + +static void +gs_app_list_class_init (GsAppListClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->get_property = gs_app_list_get_property; + object_class->set_property = gs_app_list_set_property; + object_class->finalize = gs_app_list_finalize; + + /** + * GsAppList:state: + */ + pspec = g_param_spec_uint ("state", NULL, NULL, + AS_APP_STATE_UNKNOWN, + AS_APP_STATE_LAST, + AS_APP_STATE_UNKNOWN, + G_PARAM_READABLE); + g_object_class_install_property (object_class, PROP_STATE, pspec); + + /** + * GsAppList:progress: + * + * A percentage (0–100, inclusive) indicating the progress through the + * current task on this app list. The value may otherwise be + * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide + * confidence interval on any app, or if the app list is empty. + */ + pspec = g_param_spec_uint ("progress", NULL, NULL, + 0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN, + G_PARAM_READABLE); + g_object_class_install_property (object_class, PROP_PROGRESS, pspec); +} + +static void +gs_app_list_init (GsAppList *list) +{ + g_mutex_init (&list->mutex); + list->array = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); +} + +/** + * gs_app_list_new: + * + * Creates a new list. + * + * Returns: A newly allocated #GsAppList + * + * Since: 3.22 + **/ +GsAppList * +gs_app_list_new (void) +{ + GsAppList *list; + list = g_object_new (GS_TYPE_APP_LIST, NULL); + return GS_APP_LIST (list); +} diff --git a/lib/gs-app-list.h b/lib/gs-app-list.h new file mode 100644 index 0000000..abb405b --- /dev/null +++ b/lib/gs-app-list.h @@ -0,0 +1,46 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" + +G_BEGIN_DECLS + +#define GS_TYPE_APP_LIST (gs_app_list_get_type ()) + +G_DECLARE_FINAL_TYPE (GsAppList, gs_app_list, GS, APP_LIST, GObject) + +typedef gboolean (*GsAppListSortFunc) (GsApp *app1, + GsApp *app2, + gpointer user_data); +typedef gboolean (*GsAppListFilterFunc) (GsApp *app, + gpointer user_data); + +GsAppList *gs_app_list_new (void); +void gs_app_list_add (GsAppList *list, + GsApp *app); +void gs_app_list_add_list (GsAppList *list, + GsAppList *donor); +void gs_app_list_remove (GsAppList *list, + GsApp *app); +GsApp *gs_app_list_index (GsAppList *list, + guint idx); +GsApp *gs_app_list_lookup (GsAppList *list, + const gchar *unique_id); +guint gs_app_list_length (GsAppList *list); +void gs_app_list_sort (GsAppList *list, + GsAppListSortFunc func, + gpointer user_data); +void gs_app_list_filter (GsAppList *list, + GsAppListFilterFunc func, + gpointer user_data); + +G_END_DECLS diff --git a/lib/gs-app-private.h b/lib/gs-app-private.h new file mode 100644 index 0000000..1163bf3 --- /dev/null +++ b/lib/gs-app-private.h @@ -0,0 +1,30 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +void gs_app_set_priority (GsApp *app, + guint priority); +guint gs_app_get_priority (GsApp *app); +void gs_app_set_unique_id (GsApp *app, + const gchar *unique_id); +void gs_app_remove_addon (GsApp *app, + GsApp *addon); +GCancellable *gs_app_get_cancellable (GsApp *app); +GsPluginAction gs_app_get_pending_action (GsApp *app); +void gs_app_set_pending_action (GsApp *app, + GsPluginAction action); +gint gs_app_compare_priority (GsApp *app1, + GsApp *app2); + +G_END_DECLS diff --git a/lib/gs-app.c b/lib/gs-app.c new file mode 100644 index 0000000..4a9e6d2 --- /dev/null +++ b/lib/gs-app.c @@ -0,0 +1,4680 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-app + * @title: GsApp + * @include: gnome-software.h + * @stability: Unstable + * @short_description: An application that is either installed or that can be installed + * + * This object represents a 1:1 mapping to a .desktop file. The design is such + * so you can't have different GsApp's for different versions or architectures + * of a package. This rule really only applies to GsApps of kind %AS_APP_KIND_DESKTOP + * and %AS_APP_KIND_GENERIC. We allow GsApps of kind %AS_APP_KIND_OS_UPDATE or + * %AS_APP_KIND_GENERIC, which don't correspond to desktop files, but instead + * represent a system update and its individual components. + * + * The #GsPluginLoader de-duplicates the GsApp instances that are produced by + * plugins to ensure that there is a single instance of GsApp for each id, making + * the id the primary key for this object. This ensures that actions triggered on + * a #GsApp in different parts of gnome-software can be observed by connecting to + * signals on the #GsApp. + * + * Information about other #GsApp objects can be stored in this object, for + * instance in the gs_app_add_related() method or gs_app_get_history(). + */ + +#include "config.h" + +#include <string.h> +#include <gtk/gtk.h> +#include <glib/gi18n.h> + +#include "gs-app-collation.h" +#include "gs-app-private.h" +#include "gs-os-release.h" +#include "gs-plugin.h" +#include "gs-utils.h" + +typedef struct +{ + GObject parent_instance; + + GMutex mutex; + gchar *id; + gchar *unique_id; + gboolean unique_id_valid; + gchar *branch; + gchar *name; + GsAppQuality name_quality; + GPtrArray *icons; + GPtrArray *sources; + GPtrArray *source_ids; + gchar *project_group; + gchar *developer_name; + gchar *agreement; + gchar *version; + gchar *version_ui; + gchar *summary; + GsAppQuality summary_quality; + gchar *summary_missing; + gchar *description; + GsAppQuality description_quality; + GPtrArray *screenshots; + GPtrArray *categories; + GPtrArray *key_colors; + GHashTable *urls; + GHashTable *launchables; + gchar *license; + GsAppQuality license_quality; + gchar **menu_path; + gchar *origin; + gchar *origin_appstream; + gchar *origin_hostname; + gchar *update_version; + gchar *update_version_ui; + gchar *update_details; + AsUrgencyKind update_urgency; + GsAppPermissions update_permissions; + gchar *management_plugin; + guint match_value; + guint priority; + gint rating; + GArray *review_ratings; + GPtrArray *reviews; /* of AsReview */ + GPtrArray *provides; /* of AsProvide */ + guint64 size_installed; + guint64 size_download; + AsAppKind kind; + AsAppState state; + AsAppState state_recover; + AsAppScope scope; + AsBundleKind bundle_kind; + guint progress; /* integer 0–100 (inclusive), or %GS_APP_PROGRESS_UNKNOWN */ + gboolean allow_cancel; + GHashTable *metadata; + GsAppList *addons; + GsAppList *related; + GsAppList *history; + guint64 install_date; + guint64 kudos; + gboolean to_be_installed; + GsAppQuirk quirk; + gboolean license_is_free; + GsApp *runtime; + GFile *local_file; + AsContentRating *content_rating; + GdkPixbuf *pixbuf; /* (nullable) (owned) */ + AsScreenshot *action_screenshot; /* (nullable) (owned) */ + GCancellable *cancellable; + GsPluginAction pending_action; + GsAppPermissions permissions; + gboolean is_update_downloaded; +} GsAppPrivate; + +enum { + PROP_0, + PROP_ID, + PROP_NAME, + PROP_VERSION, + PROP_SUMMARY, + PROP_DESCRIPTION, + PROP_RATING, + PROP_KIND, + PROP_STATE, + PROP_PROGRESS, + PROP_CAN_CANCEL_INSTALLATION, + PROP_INSTALL_DATE, + PROP_QUIRK, + PROP_PENDING_ACTION, + PROP_KEY_COLORS, + PROP_IS_UPDATE_DOWNLOADED, + PROP_LAST +}; + +static GParamSpec *obj_props[PROP_LAST] = { NULL, }; + +G_DEFINE_TYPE_WITH_PRIVATE (GsApp, gs_app, G_TYPE_OBJECT) + +static gboolean +_g_set_str (gchar **str_ptr, const gchar *new_str) +{ + if (*str_ptr == new_str || g_strcmp0 (*str_ptr, new_str) == 0) + return FALSE; + g_free (*str_ptr); + *str_ptr = g_strdup (new_str); + return TRUE; +} + +static gboolean +_g_set_strv (gchar ***strv_ptr, gchar **new_strv) +{ + if (*strv_ptr == new_strv) + return FALSE; + g_strfreev (*strv_ptr); + *strv_ptr = g_strdupv (new_strv); + return TRUE; +} + +static gboolean +_g_set_ptr_array (GPtrArray **array_ptr, GPtrArray *new_array) +{ + if (*array_ptr == new_array) + return FALSE; + if (*array_ptr != NULL) + g_ptr_array_unref (*array_ptr); + *array_ptr = g_ptr_array_ref (new_array); + return TRUE; +} + +static gboolean +_g_set_array (GArray **array_ptr, GArray *new_array) +{ + if (*array_ptr == new_array) + return FALSE; + if (*array_ptr != NULL) + g_array_unref (*array_ptr); + *array_ptr = g_array_ref (new_array); + return TRUE; +} + +static void +gs_app_kv_lpad (GString *str, const gchar *key, const gchar *value) +{ + gs_utils_append_key_value (str, 20, key, value); +} + +static void +gs_app_kv_size (GString *str, const gchar *key, guint64 value) +{ + g_autofree gchar *tmp = NULL; + if (value == GS_APP_SIZE_UNKNOWABLE) { + gs_app_kv_lpad (str, key, "unknowable"); + return; + } + tmp = g_format_size (value); + gs_app_kv_lpad (str, key, tmp); +} + +G_GNUC_PRINTF (3, 4) +static void +gs_app_kv_printf (GString *str, const gchar *key, const gchar *fmt, ...) +{ + va_list args; + g_autofree gchar *tmp = NULL; + va_start (args, fmt); + tmp = g_strdup_vprintf (fmt, args); + va_end (args); + gs_app_kv_lpad (str, key, tmp); +} + +static const gchar * +_as_app_quirk_flag_to_string (GsAppQuirk quirk) +{ + switch (quirk) { + case GS_APP_QUIRK_PROVENANCE: + return "provenance"; + case GS_APP_QUIRK_COMPULSORY: + return "compulsory"; + case GS_APP_QUIRK_HAS_SOURCE: + return "has-source"; + case GS_APP_QUIRK_IS_WILDCARD: + return "is-wildcard"; + case GS_APP_QUIRK_NEEDS_REBOOT: + return "needs-reboot"; + case GS_APP_QUIRK_NOT_REVIEWABLE: + return "not-reviewable"; + case GS_APP_QUIRK_HAS_SHORTCUT: + return "has-shortcut"; + case GS_APP_QUIRK_NOT_LAUNCHABLE: + return "not-launchable"; + case GS_APP_QUIRK_NEEDS_USER_ACTION: + return "needs-user-action"; + case GS_APP_QUIRK_IS_PROXY: + return "is-proxy"; + case GS_APP_QUIRK_REMOVABLE_HARDWARE: + return "removable-hardware"; + case GS_APP_QUIRK_DEVELOPER_VERIFIED: + return "developer-verified"; + case GS_APP_QUIRK_PARENTAL_FILTER: + return "parental-filter"; + case GS_APP_QUIRK_NEW_PERMISSIONS: + return "new-permissions"; + case GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE: + return "parental-not-launchable"; + case GS_APP_QUIRK_HIDE_FROM_SEARCH: + return "hide-from-search"; + case GS_APP_QUIRK_HIDE_EVERYWHERE: + return "hide-everywhere"; + case GS_APP_QUIRK_DO_NOT_AUTO_UPDATE: + return "do-not-auto-update"; + default: + return NULL; + } +} + +/* mutex must be held */ +static const gchar * +gs_app_get_unique_id_unlocked (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + /* invalid */ + if (priv->id == NULL) + return NULL; + + /* hmm, do what we can */ + if (priv->unique_id == NULL || !priv->unique_id_valid) { + g_free (priv->unique_id); + priv->unique_id = as_utils_unique_id_build (priv->scope, + priv->bundle_kind, + priv->origin, + priv->kind, + priv->id, + priv->branch); + priv->unique_id_valid = TRUE; + } + return priv->unique_id; +} + +/** + * gs_app_compare_priority: + * @app1: a #GsApp + * @app2: a #GsApp + * + * Compares two applications using their priority. + * + * Use `gs_plugin_add_rule(plugin,GS_PLUGIN_RULE_BETTER_THAN,"plugin-name")` + * to set the application priority values. + * + * Returns: a negative value if @app1 is less than @app2, a positive value if + * @app1 is greater than @app2, and zero if @app1 is equal to @app2 + **/ +gint +gs_app_compare_priority (GsApp *app1, GsApp *app2) +{ + GsAppPrivate *priv1 = gs_app_get_instance_private (app1); + GsAppPrivate *priv2 = gs_app_get_instance_private (app2); + + /* prefer prio */ + if (priv1->priority > priv2->priority) + return -1; + if (priv1->priority < priv2->priority) + return 1; + + /* fall back to bundle kind */ + if (priv1->bundle_kind < priv2->bundle_kind) + return -1; + if (priv1->bundle_kind > priv2->bundle_kind) + return 1; + return 0; +} + +/** + * gs_app_quirk_to_string: + * @quirk: a #GsAppQuirk + * + * Returns the quirk bitfield as a string. + * + * Returns: (transfer full): a string + **/ +static gchar * +gs_app_quirk_to_string (GsAppQuirk quirk) +{ + GString *str = g_string_new (""); + guint64 i; + + /* nothing set */ + if (quirk == GS_APP_QUIRK_NONE) { + g_string_append (str, "none"); + return g_string_free (str, FALSE); + } + + /* get flags */ + for (i = 1; i < GS_APP_QUIRK_LAST; i *= 2) { + if ((quirk & i) == 0) + continue; + g_string_append_printf (str, "%s,", + _as_app_quirk_flag_to_string (i)); + } + + /* nothing recognised */ + if (str->len == 0) { + g_string_append (str, "unknown"); + return g_string_free (str, FALSE); + } + + /* remove trailing comma */ + g_string_truncate (str, str->len - 1); + return g_string_free (str, FALSE); +} + +static gchar * +gs_app_kudos_to_string (guint64 kudos) +{ + g_autoptr(GPtrArray) array = g_ptr_array_new (); + if ((kudos & GS_APP_KUDO_MY_LANGUAGE) > 0) + g_ptr_array_add (array, "my-language"); + if ((kudos & GS_APP_KUDO_RECENT_RELEASE) > 0) + g_ptr_array_add (array, "recent-release"); + if ((kudos & GS_APP_KUDO_FEATURED_RECOMMENDED) > 0) + g_ptr_array_add (array, "featured-recommended"); + if ((kudos & GS_APP_KUDO_MODERN_TOOLKIT) > 0) + g_ptr_array_add (array, "modern-toolkit"); + if ((kudos & GS_APP_KUDO_SEARCH_PROVIDER) > 0) + g_ptr_array_add (array, "search-provider"); + if ((kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0) + g_ptr_array_add (array, "installs-user-docs"); + if ((kudos & GS_APP_KUDO_USES_NOTIFICATIONS) > 0) + g_ptr_array_add (array, "uses-notifications"); + if ((kudos & GS_APP_KUDO_HAS_KEYWORDS) > 0) + g_ptr_array_add (array, "has-keywords"); + if ((kudos & GS_APP_KUDO_HAS_SCREENSHOTS) > 0) + g_ptr_array_add (array, "has-screenshots"); + if ((kudos & GS_APP_KUDO_POPULAR) > 0) + g_ptr_array_add (array, "popular"); + if ((kudos & GS_APP_KUDO_HIGH_CONTRAST) > 0) + g_ptr_array_add (array, "high-contrast"); + if ((kudos & GS_APP_KUDO_HI_DPI_ICON) > 0) + g_ptr_array_add (array, "hi-dpi-icon"); + if ((kudos & GS_APP_KUDO_SANDBOXED) > 0) + g_ptr_array_add (array, "sandboxed"); + if ((kudos & GS_APP_KUDO_SANDBOXED_SECURE) > 0) + g_ptr_array_add (array, "sandboxed-secure"); + g_ptr_array_add (array, NULL); + return g_strjoinv ("|", (gchar **) array->pdata); +} + +/** + * gs_app_to_string: + * @app: a #GsApp + * + * Converts the application to a string. + * This is not designed to serialize the object but to produce a string suitable + * for debugging. + * + * Returns: A multi-line string + * + * Since: 3.22 + **/ +gchar * +gs_app_to_string (GsApp *app) +{ + GString *str = g_string_new ("GsApp:"); + gs_app_to_string_append (app, str); + if (str->len > 0) + g_string_truncate (str, str->len - 1); + return g_string_free (str, FALSE); +} + +/** + * gs_app_to_string_append: + * @app: a #GsApp + * @str: a #GString + * + * Appends the application to an existing string. + * + * Since: 3.26 + **/ +void +gs_app_to_string_append (GsApp *app, GString *str) +{ + GsAppClass *klass = GS_APP_GET_CLASS (app); + GsAppPrivate *priv = gs_app_get_instance_private (app); + AsImage *im; + GList *keys; + const gchar *tmp; + guint i; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (str != NULL); + + g_string_append_printf (str, " [%p]\n", app); + gs_app_kv_lpad (str, "kind", as_app_kind_to_string (priv->kind)); + gs_app_kv_lpad (str, "state", as_app_state_to_string (priv->state)); + if (priv->quirk > 0) { + g_autofree gchar *qstr = gs_app_quirk_to_string (priv->quirk); + gs_app_kv_lpad (str, "quirk", qstr); + } + if (priv->progress == GS_APP_PROGRESS_UNKNOWN) + gs_app_kv_printf (str, "progress", "unknown"); + else + gs_app_kv_printf (str, "progress", "%u%%", priv->progress); + if (priv->id != NULL) + gs_app_kv_lpad (str, "id", priv->id); + if (priv->unique_id != NULL) + gs_app_kv_lpad (str, "unique-id", gs_app_get_unique_id (app)); + if (priv->scope != AS_APP_SCOPE_UNKNOWN) + gs_app_kv_lpad (str, "scope", as_app_scope_to_string (priv->scope)); + if (priv->bundle_kind != AS_BUNDLE_KIND_UNKNOWN) { + gs_app_kv_lpad (str, "bundle-kind", + as_bundle_kind_to_string (priv->bundle_kind)); + } + if (priv->kudos > 0) { + g_autofree gchar *kudo_str = NULL; + kudo_str = gs_app_kudos_to_string (priv->kudos); + gs_app_kv_lpad (str, "kudos", kudo_str); + } + gs_app_kv_printf (str, "kudo-percentage", "%u", + gs_app_get_kudos_percentage (app)); + if (priv->name != NULL) + gs_app_kv_lpad (str, "name", priv->name); + if (priv->pixbuf != NULL) + gs_app_kv_printf (str, "pixbuf", "%p", priv->pixbuf); + if (priv->action_screenshot != NULL) + gs_app_kv_printf (str, "action-screenshot", "%p", priv->action_screenshot); + for (i = 0; i < priv->icons->len; i++) { + AsIcon *icon = g_ptr_array_index (priv->icons, i); + gs_app_kv_lpad (str, "icon-kind", + as_icon_kind_to_string (as_icon_get_kind (icon))); + if (as_icon_get_pixbuf (icon) != NULL) { + gs_app_kv_printf (str, "icon-pixbuf", "%p", + as_icon_get_pixbuf (icon)); + } + if (as_icon_get_name (icon) != NULL) + gs_app_kv_lpad (str, "icon-name", + as_icon_get_name (icon)); + if (as_icon_get_prefix (icon) != NULL) + gs_app_kv_lpad (str, "icon-prefix", + as_icon_get_prefix (icon)); + if (as_icon_get_filename (icon) != NULL) + gs_app_kv_lpad (str, "icon-filename", + as_icon_get_filename (icon)); + } + if (priv->match_value != 0) + gs_app_kv_printf (str, "match-value", "%05x", priv->match_value); + if (priv->priority != 0) + gs_app_kv_printf (str, "priority", "%u", priv->priority); + if (priv->version != NULL) + gs_app_kv_lpad (str, "version", priv->version); + if (priv->version_ui != NULL) + gs_app_kv_lpad (str, "version-ui", priv->version_ui); + if (priv->update_version != NULL) + gs_app_kv_lpad (str, "update-version", priv->update_version); + if (priv->update_version_ui != NULL) + gs_app_kv_lpad (str, "update-version-ui", priv->update_version_ui); + if (priv->update_details != NULL) + gs_app_kv_lpad (str, "update-details", priv->update_details); + if (priv->update_urgency != AS_URGENCY_KIND_UNKNOWN) { + gs_app_kv_printf (str, "update-urgency", "%u", + priv->update_urgency); + } + if (priv->summary != NULL) + gs_app_kv_lpad (str, "summary", priv->summary); + if (priv->description != NULL) + gs_app_kv_lpad (str, "description", priv->description); + for (i = 0; i < priv->screenshots->len; i++) { + AsScreenshot *ss = g_ptr_array_index (priv->screenshots, i); + g_autofree gchar *key = NULL; + tmp = as_screenshot_get_caption (ss, NULL); + im = as_screenshot_get_image (ss, 0, 0); + if (im == NULL) + continue; + key = g_strdup_printf ("screenshot-%02u", i); + gs_app_kv_printf (str, key, "%s [%s]", + as_image_get_url (im), + tmp != NULL ? tmp : "<none>"); + } + for (i = 0; i < priv->sources->len; i++) { + g_autofree gchar *key = NULL; + tmp = g_ptr_array_index (priv->sources, i); + key = g_strdup_printf ("source-%02u", i); + gs_app_kv_lpad (str, key, tmp); + } + for (i = 0; i < priv->source_ids->len; i++) { + g_autofree gchar *key = NULL; + tmp = g_ptr_array_index (priv->source_ids, i); + key = g_strdup_printf ("source-id-%02u", i); + gs_app_kv_lpad (str, key, tmp); + } + if (priv->local_file != NULL) { + g_autofree gchar *fn = g_file_get_path (priv->local_file); + gs_app_kv_lpad (str, "local-filename", fn); + } + if (priv->content_rating != NULL) { + guint age = as_content_rating_get_minimum_age (priv->content_rating); + if (age != G_MAXUINT) { + g_autofree gchar *value = g_strdup_printf ("%u", age); + gs_app_kv_lpad (str, "content-age", value); + } + gs_app_kv_lpad (str, "content-rating", + as_content_rating_get_kind (priv->content_rating)); + } + tmp = g_hash_table_lookup (priv->urls, as_url_kind_to_string (AS_URL_KIND_HOMEPAGE)); + if (tmp != NULL) + gs_app_kv_lpad (str, "url{homepage}", tmp); + keys = g_hash_table_get_keys (priv->launchables); + for (GList *l = keys; l != NULL; l = l->next) { + g_autofree gchar *key = NULL; + key = g_strdup_printf ("launchable{%s}", (const gchar *) l->data); + tmp = g_hash_table_lookup (priv->launchables, l->data); + gs_app_kv_lpad (str, key, tmp); + } + g_list_free (keys); + if (priv->license != NULL) { + gs_app_kv_lpad (str, "license", priv->license); + gs_app_kv_lpad (str, "license-is-free", + gs_app_get_license_is_free (app) ? "yes" : "no"); + } + if (priv->management_plugin != NULL) + gs_app_kv_lpad (str, "management-plugin", priv->management_plugin); + if (priv->summary_missing != NULL) + gs_app_kv_lpad (str, "summary-missing", priv->summary_missing); + if (priv->menu_path != NULL && + priv->menu_path[0] != NULL && + priv->menu_path[0][0] != '\0') { + g_autofree gchar *path = g_strjoinv (" → ", priv->menu_path); + gs_app_kv_lpad (str, "menu-path", path); + } + if (priv->branch != NULL) + gs_app_kv_lpad (str, "branch", priv->branch); + if (priv->origin != NULL && priv->origin[0] != '\0') + gs_app_kv_lpad (str, "origin", priv->origin); + if (priv->origin_appstream != NULL && priv->origin_appstream[0] != '\0') + gs_app_kv_lpad (str, "origin-appstream", priv->origin_appstream); + if (priv->origin_hostname != NULL && priv->origin_hostname[0] != '\0') + gs_app_kv_lpad (str, "origin-hostname", priv->origin_hostname); + if (priv->rating != -1) + gs_app_kv_printf (str, "rating", "%i", priv->rating); + if (priv->review_ratings != NULL) { + for (i = 0; i < priv->review_ratings->len; i++) { + guint32 rat = g_array_index (priv->review_ratings, guint32, i); + gs_app_kv_printf (str, "review-rating", "[%u:%u]", + i, rat); + } + } + if (priv->reviews != NULL) + gs_app_kv_printf (str, "reviews", "%u", priv->reviews->len); + if (priv->provides != NULL) + gs_app_kv_printf (str, "provides", "%u", priv->provides->len); + if (priv->install_date != 0) { + gs_app_kv_printf (str, "install-date", "%" + G_GUINT64_FORMAT "", + priv->install_date); + } + if (priv->size_installed != 0) + gs_app_kv_size (str, "size-installed", priv->size_installed); + if (priv->size_download != 0) + gs_app_kv_size (str, "size-download", gs_app_get_size_download (app)); + for (i = 0; i < gs_app_list_length (priv->related); i++) { + GsApp *app_tmp = gs_app_list_index (priv->related, i); + const gchar *id = gs_app_get_unique_id (app_tmp); + if (id == NULL) + id = gs_app_get_source_default (app_tmp); + gs_app_kv_lpad (str, "related", id); + } + for (i = 0; i < gs_app_list_length (priv->history); i++) { + GsApp *app_tmp = gs_app_list_index (priv->history, i); + gs_app_kv_lpad (str, "history", gs_app_get_unique_id (app_tmp)); + } + for (i = 0; i < priv->categories->len; i++) { + tmp = g_ptr_array_index (priv->categories, i); + gs_app_kv_lpad (str, "category", tmp); + } + for (i = 0; i < priv->key_colors->len; i++) { + GdkRGBA *color = g_ptr_array_index (priv->key_colors, i); + g_autofree gchar *key = NULL; + key = g_strdup_printf ("key-color-%02u", i); + gs_app_kv_printf (str, key, "%.0f,%.0f,%.0f", + color->red * 255.f, + color->green * 255.f, + color->blue * 255.f); + } + keys = g_hash_table_get_keys (priv->metadata); + for (GList *l = keys; l != NULL; l = l->next) { + GVariant *val; + const GVariantType *val_type; + g_autofree gchar *key = NULL; + g_autofree gchar *val_str = NULL; + + key = g_strdup_printf ("{%s}", (const gchar *) l->data); + val = g_hash_table_lookup (priv->metadata, l->data); + val_type = g_variant_get_type (val); + if (g_variant_type_equal (val_type, G_VARIANT_TYPE_STRING)) { + val_str = g_variant_dup_string (val, NULL); + } else if (g_variant_type_equal (val_type, G_VARIANT_TYPE_BOOLEAN)) { + val_str = g_strdup (g_variant_get_boolean (val) ? "True" : "False"); + } else if (g_variant_type_equal (val_type, G_VARIANT_TYPE_UINT32)) { + val_str = g_strdup_printf ("%" G_GUINT32_FORMAT, + g_variant_get_uint32 (val)); + } else { + val_str = g_strdup_printf ("unknown type of %s", + g_variant_get_type_string (val)); + } + gs_app_kv_lpad (str, key, val_str); + } + g_list_free (keys); + + /* add subclassed info */ + if (klass->to_string != NULL) + klass->to_string (app, str); + + /* print runtime data too */ + if (priv->runtime != NULL) { + g_string_append (str, "\n\tRuntime:\n\t"); + gs_app_to_string_append (priv->runtime, str); + } + g_string_append_printf (str, "\n"); +} + +typedef struct { + GsApp *app; + GParamSpec *pspec; +} AppNotifyData; + +static gboolean +notify_idle_cb (gpointer data) +{ + AppNotifyData *notify_data = data; + + g_object_notify_by_pspec (G_OBJECT (notify_data->app), notify_data->pspec); + + g_object_unref (notify_data->app); + g_free (notify_data); + + return G_SOURCE_REMOVE; +} + +static void +gs_app_queue_notify (GsApp *app, GParamSpec *pspec) +{ + AppNotifyData *notify_data; + + notify_data = g_new (AppNotifyData, 1); + notify_data->app = g_object_ref (app); + notify_data->pspec = pspec; + + g_idle_add (notify_idle_cb, notify_data); +} + +/** + * gs_app_get_id: + * @app: a #GsApp + * + * Gets the application ID. + * + * Returns: The whole ID, e.g. "gimp.desktop" + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_id (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->id; +} + +/** + * gs_app_set_id: + * @app: a #GsApp + * @id: a application ID, e.g. "gimp.desktop" + * + * Sets the application ID. + */ +void +gs_app_set_id (GsApp *app, const gchar *id) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (_g_set_str (&priv->id, id)) + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_scope: + * @app: a #GsApp + * + * Gets the scope of the application. + * + * Returns: the #AsAppScope, e.g. %AS_APP_SCOPE_USER + * + * Since: 3.22 + **/ +AsAppScope +gs_app_get_scope (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_APP_SCOPE_UNKNOWN); + return priv->scope; +} + +/** + * gs_app_set_scope: + * @app: a #GsApp + * @scope: a #AsAppScope, e.g. AS_APP_SCOPE_SYSTEM + * + * This sets the scope of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_scope (GsApp *app, AsAppScope scope) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if (scope == priv->scope) + return; + + priv->scope = scope; + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_bundle_kind: + * @app: a #GsApp + * + * Gets the bundle kind of the application. + * + * Returns: the #AsAppScope, e.g. %AS_BUNDLE_KIND_FLATPAK + * + * Since: 3.22 + **/ +AsBundleKind +gs_app_get_bundle_kind (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_BUNDLE_KIND_UNKNOWN); + return priv->bundle_kind; +} + +/** + * gs_app_set_bundle_kind: + * @app: a #GsApp + * @bundle_kind: a #AsAppScope, e.g. AS_BUNDLE_KIND_FLATPAK + * + * This sets the bundle kind of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_bundle_kind (GsApp *app, AsBundleKind bundle_kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if (bundle_kind == priv->bundle_kind) + return; + + priv->bundle_kind = bundle_kind; + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_state: + * @app: a #GsApp + * + * Gets the state of the application. + * + * Returns: the #AsAppState, e.g. %AS_APP_STATE_INSTALLED + * + * Since: 3.22 + **/ +AsAppState +gs_app_get_state (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_APP_STATE_UNKNOWN); + return priv->state; +} + +/** + * gs_app_get_progress: + * @app: a #GsApp + * + * Gets the percentage completion. + * + * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown + * + * Since: 3.22 + **/ +guint +gs_app_get_progress (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), GS_APP_PROGRESS_UNKNOWN); + return priv->progress; +} + +/** + * gs_app_get_allow_cancel: + * @app: a #GsApp + * + * Gets whether the app's installation or upgrade can be cancelled. + * + * Returns: TRUE if cancellation is possible, FALSE otherwise. + * + * Since: 3.26 + **/ +gboolean +gs_app_get_allow_cancel (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->allow_cancel; +} + +/** + * gs_app_set_state_recover: + * @app: a #GsApp + * + * Sets the application state to the last status value that was not + * transient. + * + * Since: 3.22 + **/ +void +gs_app_set_state_recover (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + if (priv->state_recover == AS_APP_STATE_UNKNOWN) + return; + if (priv->state_recover == priv->state) + return; + + g_debug ("recovering state on %s from %s to %s", + priv->id, + as_app_state_to_string (priv->state), + as_app_state_to_string (priv->state_recover)); + + /* make sure progress gets reset when recovering state, to prevent + * confusing initial states when going through more than one attempt */ + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + + priv->state = priv->state_recover; + gs_app_queue_notify (app, obj_props[PROP_STATE]); +} + +/* mutex must be held */ +static gboolean +gs_app_set_state_internal (GsApp *app, AsAppState state) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + gboolean state_change_ok = FALSE; + + /* same */ + if (priv->state == state) + return FALSE; + + /* check the state change is allowed */ + switch (priv->state) { + case AS_APP_STATE_UNKNOWN: + /* unknown has to go into one of the stable states */ + if (state == AS_APP_STATE_INSTALLED || + state == AS_APP_STATE_QUEUED_FOR_INSTALL || + state == AS_APP_STATE_AVAILABLE || + state == AS_APP_STATE_AVAILABLE_LOCAL || + state == AS_APP_STATE_UPDATABLE || + state == AS_APP_STATE_UPDATABLE_LIVE || + state == AS_APP_STATE_UNAVAILABLE) + state_change_ok = TRUE; + break; + case AS_APP_STATE_INSTALLED: + /* installed has to go into an action state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_REMOVING || + state == AS_APP_STATE_UNAVAILABLE || + state == AS_APP_STATE_UPDATABLE || + state == AS_APP_STATE_UPDATABLE_LIVE) + state_change_ok = TRUE; + break; + case AS_APP_STATE_QUEUED_FOR_INSTALL: + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_INSTALLING || + state == AS_APP_STATE_AVAILABLE) + state_change_ok = TRUE; + break; + case AS_APP_STATE_AVAILABLE: + /* available has to go into an action state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_QUEUED_FOR_INSTALL || + state == AS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case AS_APP_STATE_INSTALLING: + /* installing has to go into an stable state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_INSTALLED || + state == AS_APP_STATE_UPDATABLE || + state == AS_APP_STATE_UPDATABLE_LIVE || + state == AS_APP_STATE_AVAILABLE) + state_change_ok = TRUE; + break; + case AS_APP_STATE_REMOVING: + /* removing has to go into an stable state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_AVAILABLE || + state == AS_APP_STATE_INSTALLED) + state_change_ok = TRUE; + break; + case AS_APP_STATE_UPDATABLE: + /* updatable has to go into an action state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_REMOVING || + state == AS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case AS_APP_STATE_UPDATABLE_LIVE: + /* updatable-live has to go into an action state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_REMOVING || + state == AS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + case AS_APP_STATE_UNAVAILABLE: + /* updatable has to go into an action state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_AVAILABLE) + state_change_ok = TRUE; + break; + case AS_APP_STATE_AVAILABLE_LOCAL: + /* local has to go into an action state */ + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_INSTALLING) + state_change_ok = TRUE; + break; + default: + g_warning ("state %s unhandled", + as_app_state_to_string (priv->state)); + g_assert_not_reached (); + } + + /* this state change was unexpected */ + if (!state_change_ok) { + g_warning ("State change on %s from %s to %s is not OK", + gs_app_get_unique_id_unlocked (app), + as_app_state_to_string (priv->state), + as_app_state_to_string (state)); + } + + priv->state = state; + + if (state == AS_APP_STATE_UNKNOWN || + state == AS_APP_STATE_AVAILABLE_LOCAL || + state == AS_APP_STATE_AVAILABLE) + priv->install_date = 0; + + /* save this to simplify error handling in the plugins */ + switch (state) { + case AS_APP_STATE_INSTALLING: + case AS_APP_STATE_REMOVING: + case AS_APP_STATE_QUEUED_FOR_INSTALL: + /* transient, so ignore */ + break; + default: + if (priv->state_recover != state) + priv->state_recover = state; + break; + } + + return TRUE; +} + +/** + * gs_app_set_progress: + * @app: a #GsApp + * @percentage: a percentage progress (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN + * + * This sets the progress completion of the application. Use + * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide confidence + * interval. + * + * If called more than once with the same value then subsequent calls + * will be ignored. + * + * Since: 3.22 + **/ +void +gs_app_set_progress (GsApp *app, guint percentage) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (priv->progress == percentage) + return; + if (percentage != GS_APP_PROGRESS_UNKNOWN && percentage > 100) { + g_warning ("cannot set %u%% for %s, setting instead: 100%%", + percentage, gs_app_get_unique_id_unlocked (app)); + percentage = 100; + } + priv->progress = percentage; + gs_app_queue_notify (app, obj_props[PROP_PROGRESS]); +} + +/** + * gs_app_set_allow_cancel: + * @app: a #GsApp + * @allow_cancel: if the installation or upgrade can be cancelled or not + * + * This sets a flag indicating whether the operation can be cancelled or not. + * This is used by the UI to set the "Cancel" button insensitive as + * appropriate. + * + * Since: 3.26 + **/ +void +gs_app_set_allow_cancel (GsApp *app, gboolean allow_cancel) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (priv->allow_cancel == allow_cancel) + return; + priv->allow_cancel = allow_cancel; + gs_app_queue_notify (app, obj_props[PROP_CAN_CANCEL_INSTALLATION]); +} + +static void +gs_app_set_pending_action_internal (GsApp *app, + GsPluginAction action) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + if (priv->pending_action == action) + return; + + priv->pending_action = action; + gs_app_queue_notify (app, obj_props[PROP_PENDING_ACTION]); +} + +/** + * gs_app_set_state: + * @app: a #GsApp + * @state: a #AsAppState, e.g. AS_APP_STATE_UPDATABLE_LIVE + * + * This sets the state of the application. + * The following state diagram explains the typical states. + * All applications start in state %AS_APP_STATE_UNKNOWN, + * but the frontend is not supposed to see GsApps with this state. + * + * Plugins are responsible for changing the state to one of the other + * states before the GsApp is passed to the frontend. + * + * |[ + * UPDATABLE --> INSTALLING --> INSTALLED + * UPDATABLE --> REMOVING --> AVAILABLE + * INSTALLED --> REMOVING --> AVAILABLE + * AVAILABLE --> INSTALLING --> INSTALLED + * AVAILABLE <--> QUEUED --> INSTALLING --> INSTALLED + * UNKNOWN --> UNAVAILABLE + * ]| + * + * Since: 3.22 + **/ +void +gs_app_set_state (GsApp *app, AsAppState state) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (gs_app_set_state_internal (app, state)) { + /* since the state changed, and the pending-action refers to + * actions that usually change the state, we assign it to the + * appropriate action here */ + GsPluginAction action = GS_PLUGIN_ACTION_UNKNOWN; + if (priv->state == AS_APP_STATE_QUEUED_FOR_INSTALL) + action = GS_PLUGIN_ACTION_INSTALL; + gs_app_set_pending_action_internal (app, action); + + gs_app_queue_notify (app, obj_props[PROP_STATE]); + } +} + +/** + * gs_app_get_kind: + * @app: a #GsApp + * + * Gets the kind of the application. + * + * Returns: the #AsAppKind, e.g. %AS_APP_KIND_UNKNOWN + * + * Since: 3.22 + **/ +AsAppKind +gs_app_get_kind (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_APP_KIND_UNKNOWN); + return priv->kind; +} + +/** + * gs_app_set_kind: + * @app: a #GsApp + * @kind: a #AsAppKind, e.g. #AS_APP_KIND_DESKTOP + * + * This sets the kind of the application. + * The following state diagram explains the typical states. + * All applications start with kind %AS_APP_KIND_UNKNOWN. + * + * |[ + * PACKAGE --> NORMAL + * PACKAGE --> SYSTEM + * NORMAL --> SYSTEM + * ]| + * + * Since: 3.22 + **/ +void +gs_app_set_kind (GsApp *app, AsAppKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + gboolean state_change_ok = FALSE; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (priv->kind == kind) + return; + + /* trying to change */ + if (priv->kind != AS_APP_KIND_UNKNOWN && + kind == AS_APP_KIND_UNKNOWN) { + g_warning ("automatically prevented from changing " + "kind on %s from %s to %s!", + gs_app_get_unique_id_unlocked (app), + as_app_kind_to_string (priv->kind), + as_app_kind_to_string (kind)); + return; + } + + /* check the state change is allowed */ + switch (priv->kind) { + case AS_APP_KIND_UNKNOWN: + case AS_APP_KIND_GENERIC: + /* all others derive from generic */ + state_change_ok = TRUE; + break; + case AS_APP_KIND_DESKTOP: + /* desktop has to be reset to override */ + if (kind == AS_APP_KIND_UNKNOWN) + state_change_ok = TRUE; + break; + default: + /* this can never change state */ + break; + } + + /* this state change was unexpected */ + if (!state_change_ok) { + g_warning ("Kind change on %s from %s to %s is not OK", + priv->id, + as_app_kind_to_string (priv->kind), + as_app_kind_to_string (kind)); + return; + } + + priv->kind = kind; + gs_app_queue_notify (app, obj_props[PROP_KIND]); + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_unique_id: + * @app: a #GsApp + * + * Gets the unique application ID used for de-duplication. + * If nothing has been set the value from gs_app_get_id() will be used. + * + * Returns: The unique ID, e.g. `system/package/fedora/desktop/gimp.desktop/i386/master`, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_unique_id (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return gs_app_get_unique_id_unlocked (app); +} + +/** + * gs_app_set_unique_id: + * @app: a #GsApp + * @unique_id: a unique application ID, e.g. `system/package/fedora/desktop/gimp.desktop/i386/master` + * + * Sets the unique application ID. Any #GsApp using the same ID will be + * deduplicated. This means that applications that can exist from more than + * one plugin should use this method. + */ +void +gs_app_set_unique_id (GsApp *app, const gchar *unique_id) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* check for sanity */ + if (!as_utils_unique_id_valid (unique_id)) + g_warning ("unique_id %s not valid", unique_id); + + g_free (priv->unique_id); + priv->unique_id = g_strdup (unique_id); + priv->unique_id_valid = TRUE; +} + +/** + * gs_app_get_name: + * @app: a #GsApp + * + * Gets the application name. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_name (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->name; +} + +/** + * gs_app_set_name: + * @app: a #GsApp + * @quality: A #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST + * @name: The short localized name, e.g. "Calculator" + * + * Sets the application name. + * + * Since: 3.22 + **/ +void +gs_app_set_name (GsApp *app, GsAppQuality quality, const gchar *name) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality < priv->name_quality) + return; + priv->name_quality = quality; + if (_g_set_str (&priv->name, name)) + g_object_notify_by_pspec (G_OBJECT (app), obj_props[PROP_NAME]); +} + +/** + * gs_app_get_branch: + * @app: a #GsApp + * + * Gets the application branch. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_branch (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->branch; +} + +/** + * gs_app_set_branch: + * @app: a #GsApp + * @branch: The branch, e.g. "master" + * + * Sets the application branch. + * + * Since: 3.22 + **/ +void +gs_app_set_branch (GsApp *app, const gchar *branch) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (_g_set_str (&priv->branch, branch)) + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_source_default: + * @app: a #GsApp + * + * Gets the default source. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_source_default (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + if (priv->sources->len == 0) + return NULL; + return g_ptr_array_index (priv->sources, 0); +} + +/** + * gs_app_add_source: + * @app: a #GsApp + * @source: a source name + * + * Adds a source name for the application. + * + * Since: 3.22 + **/ +void +gs_app_add_source (GsApp *app, const gchar *source) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (source != NULL); + + locker = g_mutex_locker_new (&priv->mutex); + + /* check source doesn't already exist */ + for (i = 0; i < priv->sources->len; i++) { + tmp = g_ptr_array_index (priv->sources, i); + if (g_strcmp0 (tmp, source) == 0) + return; + } + g_ptr_array_add (priv->sources, g_strdup (source)); +} + +/** + * gs_app_get_sources: + * @app: a #GsApp + * + * Gets the list of sources for the application. + * + * Returns: (element-type utf8) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_sources (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->sources; +} + +/** + * gs_app_set_sources: + * @app: a #GsApp + * @sources: The non-localized short names, e.g. ["gnome-calculator"] + * + * This name is used for the update page if the application is collected into + * the 'OS Updates' group. + * It is typically the package names, although this should not be relied upon. + * + * Since: 3.22 + **/ +void +gs_app_set_sources (GsApp *app, GPtrArray *sources) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->sources, sources); +} + +/** + * gs_app_get_source_id_default: + * @app: a #GsApp + * + * Gets the default source ID. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_source_id_default (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + if (priv->source_ids->len == 0) + return NULL; + return g_ptr_array_index (priv->source_ids, 0); +} + +/** + * gs_app_get_source_ids: + * @app: a #GsApp + * + * Gets the list of source IDs. + * + * Returns: (element-type utf8) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_source_ids (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->source_ids; +} + +/** + * gs_app_clear_source_ids: + * @app: a #GsApp + * + * Clear the list of source IDs. + * + * Since: 3.22 + **/ +void +gs_app_clear_source_ids (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_set_size (priv->source_ids, 0); +} + +/** + * gs_app_set_source_ids: + * @app: a #GsApp + * @source_ids: The source-id, e.g. ["gnome-calculator;0.134;fedora"] + * or ["/home/hughsie/.local/share/applications/0ad.desktop"] + * + * This ID is used internally to the controlling plugin. + * + * Since: 3.22 + **/ +void +gs_app_set_source_ids (GsApp *app, GPtrArray *source_ids) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->source_ids, source_ids); +} + +/** + * gs_app_add_source_id: + * @app: a #GsApp + * @source_id: a source ID, e.g. "gnome-calculator;0.134;fedora" + * + * Adds a source ID to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_source_id (GsApp *app, const gchar *source_id) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (source_id != NULL); + + /* only add if not already present */ + for (i = 0; i < priv->source_ids->len; i++) { + tmp = g_ptr_array_index (priv->source_ids, i); + if (g_strcmp0 (tmp, source_id) == 0) + return; + } + g_ptr_array_add (priv->source_ids, g_strdup (source_id)); +} + +/** + * gs_app_get_project_group: + * @app: a #GsApp + * + * Gets a project group for the application. + * Applications belonging to other project groups may not be shown in + * this software center. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_project_group (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->project_group; +} + +/** + * gs_app_get_developer_name: + * @app: a #GsApp + * + * Gets the developer name for the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_developer_name (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->developer_name; +} + +/** + * gs_app_set_project_group: + * @app: a #GsApp + * @project_group: The non-localized project group, e.g. "GNOME" or "KDE" + * + * Sets a project group for the application. + * + * Since: 3.22 + **/ +void +gs_app_set_project_group (GsApp *app, const gchar *project_group) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->project_group, project_group); +} + +/** + * gs_app_set_developer_name: + * @app: a #GsApp + * @developer_name: The developer name, e.g. "Richard Hughes" + * + * Sets a developer name for the application. + * + * Since: 3.22 + **/ +void +gs_app_set_developer_name (GsApp *app, const gchar *developer_name) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->developer_name, developer_name); +} + +/** + * gs_app_get_pixbuf: + * @app: a #GsApp + * + * Gets a pixbuf to represent the application. + * + * Returns: (transfer none) (nullable): a #GdkPixbuf, or %NULL + * + * Since: 3.22 + **/ +GdkPixbuf * +gs_app_get_pixbuf (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->pixbuf; +} + +/** + * gs_app_get_action_screenshot: + * @app: a #GsApp + * + * Gets a screenshot for the pending user action. + * + * Returns: (transfer none) (nullable): a #AsScreenshot, or %NULL + * + * Since: 3.38 + **/ +AsScreenshot * +gs_app_get_action_screenshot (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->action_screenshot; +} + +/** + * gs_app_get_icons: + * @app: a #GsApp + * + * Gets the icons for the application. + * + * Returns: (transfer none) (element-type AsIcon): an array of icons + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_icons (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->icons; +} + +/** + * gs_app_add_icon: + * @app: a #GsApp + * @icon: a #AsIcon, or %NULL to remove all icons + * + * Adds an icon to use for the application. + * If the first icon added cannot be loaded then the next one is tried. + * + * Since: 3.22 + **/ +void +gs_app_add_icon (GsApp *app, AsIcon *icon) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (icon == NULL) { + g_ptr_array_set_size (priv->icons, 0); + return; + } + g_ptr_array_add (priv->icons, g_object_ref (icon)); +} + +/** + * gs_app_get_use_drop_shadow: + * @app: a #GsApp + * + * Uses a heuristic to work out if the application pixbuf should have a drop + * shadow applied. + * + * Returns: %TRUE if a drop shadow should be applied + * + * Since: 3.34 + **/ +gboolean +gs_app_get_use_drop_shadow (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + AsIcon *ic; + + /* guess */ + if (priv->icons->len == 0) + return TRUE; + + /* stock, and symbolic */ + ic = g_ptr_array_index (priv->icons, 0); + return as_icon_get_kind (ic) != AS_ICON_KIND_STOCK || + !g_str_has_suffix (as_icon_get_name (ic), "-symbolic"); +} + +/** + * gs_app_get_agreement: + * @app: a #GsApp + * + * Gets the agreement text for the application. + * + * Returns: a string in AppStream description format, or %NULL for unset + * + * Since: 3.28 + **/ +const gchar * +gs_app_get_agreement (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->agreement; +} + +/** + * gs_app_set_agreement: + * @app: a #GsApp + * @agreement: The agreement text, e.g. "<p>Foobar</p>" + * + * Sets the application end-user agreement (e.g. a EULA) in AppStream + * description format. + * + * Since: 3.28 + **/ +void +gs_app_set_agreement (GsApp *app, const gchar *agreement) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->agreement, agreement); +} + +/** + * gs_app_get_local_file: + * @app: a #GsApp + * + * Gets the file that backs this application, for instance this might + * be a local file in ~/Downloads that we are installing. + * + * Returns: (transfer none): a #GFile, or %NULL + * + * Since: 3.22 + **/ +GFile * +gs_app_get_local_file (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->local_file; +} + +/** + * gs_app_set_local_file: + * @app: a #GsApp + * @local_file: a #GFile, or %NULL + * + * Sets the file that backs this application, for instance this might + * be a local file in ~/Downloads that we are installing. + * + * Since: 3.22 + **/ +void +gs_app_set_local_file (GsApp *app, GFile *local_file) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->local_file, local_file); +} + +/** + * gs_app_get_content_rating: + * @app: a #GsApp + * + * Gets the content rating for this application. + * + * Returns: (transfer none): a #AsContentRating, or %NULL + * + * Since: 3.24 + **/ +AsContentRating * +gs_app_get_content_rating (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->content_rating; +} + +/** + * gs_app_set_content_rating: + * @app: a #GsApp + * @content_rating: a #AsContentRating, or %NULL + * + * Sets the content rating for this application. + * + * Since: 3.24 + **/ +void +gs_app_set_content_rating (GsApp *app, AsContentRating *content_rating) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->content_rating, content_rating); +} + +/** + * gs_app_get_runtime: + * @app: a #GsApp + * + * Gets the runtime for the installed application. + * + * Returns: (transfer none): a #GsApp, or %NULL for unset + * + * Since: 3.22 + **/ +GsApp * +gs_app_get_runtime (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->runtime; +} + +/** + * gs_app_set_runtime: + * @app: a #GsApp + * @runtime: a #GsApp + * + * Sets the runtime that the installed application requires. + * + * Since: 3.22 + **/ +void +gs_app_set_runtime (GsApp *app, GsApp *runtime) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (app != runtime); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->runtime, runtime); +} + +/** + * gs_app_set_pixbuf: + * @app: a #GsApp + * @pixbuf: (transfer none) (nullable): a #GdkPixbuf, or %NULL + * + * Sets a pixbuf used to represent the application. + * + * Since: 3.22 + **/ +void +gs_app_set_pixbuf (GsApp *app, GdkPixbuf *pixbuf) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->pixbuf, pixbuf); +} + +/** + * gs_app_set_action_screenshot: + * @app: a #GsApp + * @action_screenshot: (transfer none) (nullable): a #AsScreenshot, or %NULL + * + * Sets a screenshot used to represent the action. + * + * Since: 3.38 + **/ +void +gs_app_set_action_screenshot (GsApp *app, AsScreenshot *action_screenshot) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_set_object (&priv->action_screenshot, action_screenshot); +} + +typedef enum { + GS_APP_VERSION_FIXUP_RELEASE = 1, + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX = 2, + GS_APP_VERSION_FIXUP_GIT_SUFFIX = 4, + GS_APP_VERSION_FIXUP_LAST, +} GsAppVersionFixup; + +/** + * gs_app_get_ui_version: + * + * convert 1:1.6.2-7.fc17 into "Version 1.6.2" + **/ +static gchar * +gs_app_get_ui_version (const gchar *version, guint64 flags) +{ + guint i; + gchar *new; + gchar *f; + + /* nothing set */ + if (version == NULL) + return NULL; + + /* first remove any epoch */ + for (i = 0; version[i] != '\0'; i++) { + if (version[i] == ':') { + version = &version[i+1]; + break; + } + if (!g_ascii_isdigit (version[i])) + break; + } + + /* then remove any distro suffix */ + new = g_strdup (version); + if ((flags & GS_APP_VERSION_FIXUP_DISTRO_SUFFIX) > 0) { + f = g_strstr_len (new, -1, ".fc"); + if (f != NULL) + *f= '\0'; + f = g_strstr_len (new, -1, ".el"); + if (f != NULL) + *f= '\0'; + } + + /* then remove any release */ + if ((flags & GS_APP_VERSION_FIXUP_RELEASE) > 0) { + f = g_strrstr_len (new, -1, "-"); + if (f != NULL) + *f= '\0'; + } + + /* then remove any git suffix */ + if ((flags & GS_APP_VERSION_FIXUP_GIT_SUFFIX) > 0) { + f = g_strrstr_len (new, -1, ".2012"); + if (f != NULL) + *f= '\0'; + f = g_strrstr_len (new, -1, ".2013"); + if (f != NULL) + *f= '\0'; + } + + return new; +} + +static void +gs_app_ui_versions_invalidate (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_free (priv->version_ui); + g_free (priv->update_version_ui); + priv->version_ui = NULL; + priv->update_version_ui = NULL; +} + +static void +gs_app_ui_versions_populate (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + guint i; + guint64 flags[] = { GS_APP_VERSION_FIXUP_RELEASE | + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX | + GS_APP_VERSION_FIXUP_GIT_SUFFIX, + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX | + GS_APP_VERSION_FIXUP_GIT_SUFFIX, + GS_APP_VERSION_FIXUP_DISTRO_SUFFIX, + 0 }; + + /* try each set of bitfields in order */ + for (i = 0; flags[i] != 0; i++) { + priv->version_ui = gs_app_get_ui_version (priv->version, flags[i]); + priv->update_version_ui = gs_app_get_ui_version (priv->update_version, flags[i]); + if (g_strcmp0 (priv->version_ui, priv->update_version_ui) != 0) { + gs_app_queue_notify (app, obj_props[PROP_VERSION]); + return; + } + gs_app_ui_versions_invalidate (app); + } + + /* we tried, but failed */ + priv->version_ui = g_strdup (priv->version); + priv->update_version_ui = g_strdup (priv->update_version); +} + +/** + * gs_app_get_version: + * @app: a #GsApp + * + * Gets the exact version for the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_version (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->version; +} + +/** + * gs_app_get_version_ui: + * @app: a #GsApp + * + * Gets a version string that can be displayed in a UI. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_version_ui (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* work out the two version numbers */ + if (priv->version != NULL && + priv->version_ui == NULL) { + gs_app_ui_versions_populate (app); + } + + return priv->version_ui; +} + +/** + * gs_app_set_version: + * @app: a #GsApp + * @version: The version, e.g. "2:1.2.3.fc19" + * + * This saves the version after stripping out any non-friendly parts, such as + * distro tags, git revisions and that kind of thing. + * + * Since: 3.22 + **/ +void +gs_app_set_version (GsApp *app, const gchar *version) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + if (_g_set_str (&priv->version, version)) { + gs_app_ui_versions_invalidate (app); + gs_app_queue_notify (app, obj_props[PROP_VERSION]); + } +} + +/** + * gs_app_get_summary: + * @app: a #GsApp + * + * Gets the single-line description of the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_summary (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->summary; +} + +/** + * gs_app_set_summary: + * @app: a #GsApp + * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST + * @summary: a string, e.g. "A graphical calculator for GNOME" + * + * The medium length one-line localized name. + * + * Since: 3.22 + **/ +void +gs_app_set_summary (GsApp *app, GsAppQuality quality, const gchar *summary) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality < priv->summary_quality) + return; + priv->summary_quality = quality; + if (_g_set_str (&priv->summary, summary)) + g_object_notify_by_pspec (G_OBJECT (app), obj_props[PROP_SUMMARY]); +} + +/** + * gs_app_get_description: + * @app: a #GsApp + * + * Gets the long multi-line description of the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_description (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->description; +} + +/** + * gs_app_set_description: + * @app: a #GsApp + * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST + * @description: a string, e.g. "GNOME Calculator is a graphical calculator for GNOME..." + * + * Sets the long multi-line description of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_description (GsApp *app, GsAppQuality quality, const gchar *description) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality < priv->description_quality) + return; + priv->description_quality = quality; + _g_set_str (&priv->description, description); +} + +/** + * gs_app_get_url: + * @app: a #GsApp + * @kind: a #AsUrlKind, e.g. %AS_URL_KIND_HOMEPAGE + * + * Gets a web address of a specific type. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_url (GsApp *app, AsUrlKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_val_if_fail (GS_IS_APP (app), NULL); + locker = g_mutex_locker_new (&priv->mutex); + return g_hash_table_lookup (priv->urls, as_url_kind_to_string (kind)); +} + +/** + * gs_app_set_url: + * @app: a #GsApp + * @kind: a #AsUrlKind, e.g. %AS_URL_KIND_HOMEPAGE + * @url: a web URL, e.g. "http://www.hughsie.com/" + * + * Sets a web address of a specific type. + * + * Since: 3.22 + **/ +void +gs_app_set_url (GsApp *app, AsUrlKind kind, const gchar *url) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_hash_table_insert (priv->urls, + g_strdup (as_url_kind_to_string (kind)), + g_strdup (url)); +} + +/** + * gs_app_get_launchable: + * @app: a #GsApp + * @kind: a #AsLaunchableKind, e.g. %AS_LAUNCHABLE_KIND_DESKTOP_ID + * + * Gets a launchable of a specific type. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.28 + **/ +const gchar * +gs_app_get_launchable (GsApp *app, AsLaunchableKind kind) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return g_hash_table_lookup (priv->launchables, + as_launchable_kind_to_string (kind)); +} + +/** + * gs_app_set_launchable: + * @app: a #GsApp + * @kind: a #AsLaunchableKind, e.g. %AS_LAUNCHABLE_KIND_DESKTOP_ID + * @launchable: a way to launch, e.g. "org.gnome.Sysprof2.desktop" + * + * Sets a launchable of a specific type. + * + * Since: 3.28 + **/ +void +gs_app_set_launchable (GsApp *app, AsLaunchableKind kind, const gchar *launchable) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_hash_table_insert (priv->launchables, + g_strdup (as_launchable_kind_to_string (kind)), + g_strdup (launchable)); +} + +/** + * gs_app_get_license: + * @app: a #GsApp + * + * Gets the project license of the application. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_license (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->license; +} + +/** + * gs_app_get_license_is_free: + * @app: a #GsApp + * + * Returns if the application is free software. + * + * Returns: %TRUE if the application is free software + * + * Since: 3.22 + **/ +gboolean +gs_app_get_license_is_free (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->license_is_free; +} + +static gboolean +gs_app_get_license_token_is_nonfree (const gchar *token) +{ + /* grammar */ + if (g_strcmp0 (token, "(") == 0) + return FALSE; + if (g_strcmp0 (token, ")") == 0) + return FALSE; + + /* a token, but still nonfree */ + if (g_str_has_prefix (token, "@LicenseRef-proprietary")) + return TRUE; + + /* if it has a prefix, assume it is free */ + return token[0] != '@'; +} + +/** + * gs_app_set_license: + * @app: a #GsApp + * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_NORMAL + * @license: a SPDX license string, e.g. "GPL-3.0 AND LGPL-2.0+" + * + * Sets the project licenses used in the application. + * + * Since: 3.22 + **/ +void +gs_app_set_license (GsApp *app, GsAppQuality quality, const gchar *license) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + guint i; + g_auto(GStrv) tokens = NULL; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* only save this if the data is sufficiently high quality */ + if (quality <= priv->license_quality) + return; + if (license == NULL) + return; + priv->license_quality = quality; + + /* assume free software until we find a nonfree SPDX token */ + priv->license_is_free = TRUE; + tokens = as_utils_spdx_license_tokenize (license); + for (i = 0; tokens[i] != NULL; i++) { + if (g_strcmp0 (tokens[i], "&") == 0 || + g_strcmp0 (tokens[i], "+") == 0 || + g_strcmp0 (tokens[i], "|") == 0) + continue; + if (gs_app_get_license_token_is_nonfree (tokens[i])) { + priv->license_is_free = FALSE; + break; + } + } + _g_set_str (&priv->license, license); +} + +/** + * gs_app_get_summary_missing: + * @app: a #GsApp + * + * Gets the one-line summary to use when this application is missing. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_summary_missing (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->summary_missing; +} + +/** + * gs_app_set_summary_missing: + * @app: a #GsApp + * @summary_missing: a string, or %NULL + * + * Sets the one-line summary to use when this application is missing. + * + * Since: 3.22 + **/ +void +gs_app_set_summary_missing (GsApp *app, const gchar *summary_missing) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->summary_missing, summary_missing); +} + +/** + * gs_app_get_menu_path: + * @app: a #GsApp + * + * Returns the menu path which is an array of path elements. + * The resulting array is an internal structure and must not be + * modified or freed. + * + * Returns: a %NULL-terminated array of strings + * + * Since: 3.22 + **/ +gchar ** +gs_app_get_menu_path (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->menu_path; +} + +/** + * gs_app_set_menu_path: + * @app: a #GsApp + * @menu_path: a %NULL-terminated array of strings + * + * Sets the new menu path. The menu path is an array of path elements. + * This function creates a deep copy of the path. + * + * Since: 3.22 + **/ +void +gs_app_set_menu_path (GsApp *app, gchar **menu_path) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_strv (&priv->menu_path, menu_path); +} + +/** + * gs_app_get_origin: + * @app: a #GsApp + * + * Gets the origin for the application, e.g. "fedora". + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_origin (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->origin; +} + +/** + * gs_app_set_origin: + * @app: a #GsApp + * @origin: a string, or %NULL + * + * The origin is the original source of the application e.g. "fedora-updates" + * + * Since: 3.22 + **/ +void +gs_app_set_origin (GsApp *app, const gchar *origin) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (g_strcmp0 (origin, priv->origin) == 0) + return; + + /* trying to change */ + if (priv->origin != NULL && origin != NULL) { + g_warning ("automatically prevented from changing " + "origin on %s from %s to %s!", + gs_app_get_unique_id_unlocked (app), + priv->origin, origin); + return; + } + + g_free (priv->origin); + priv->origin = g_strdup (origin); + + /* no longer valid */ + priv->unique_id_valid = FALSE; +} + +/** + * gs_app_get_origin_appstream: + * @app: a #GsApp + * + * Gets the appstream origin for the application, e.g. "fedora". + * + * Returns: a string, or %NULL for unset + * + * Since: 3.28 + **/ +const gchar * +gs_app_get_origin_appstream (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->origin_appstream; +} + +/** + * gs_app_set_origin_appstream: + * @app: a #GsApp + * @origin_appstream: a string, or %NULL + * + * The appstream origin is the appstream source of the application e.g. "fedora" + * + * Since: 3.28 + **/ +void +gs_app_set_origin_appstream (GsApp *app, const gchar *origin_appstream) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (g_strcmp0 (origin_appstream, priv->origin_appstream) == 0) + return; + + g_free (priv->origin_appstream); + priv->origin_appstream = g_strdup (origin_appstream); +} + +/** + * gs_app_get_origin_hostname: + * @app: a #GsApp + * + * Gets the hostname of the origin used to install the application, e.g. + * "fedoraproject.org" or "sdk.gnome.org". + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_origin_hostname (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->origin_hostname; +} + +/** + * gs_app_set_origin_hostname: + * @app: a #GsApp + * @origin_hostname: a string, or %NULL + * + * The origin is the hostname of the source used to install the application + * e.g. "fedoraproject.org" + * + * You can also use a full URL as @origin_hostname and this will be parsed and + * the hostname extracted. This process will also remove any unnecessary DNS + * prefixes like "download" or "mirrors". + * + * Since: 3.22 + **/ +void +gs_app_set_origin_hostname (GsApp *app, const gchar *origin_hostname) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(SoupURI) uri = NULL; + guint i; + const gchar *prefixes[] = { "download.", "mirrors.", NULL }; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* same */ + if (g_strcmp0 (origin_hostname, priv->origin_hostname) == 0) + return; + g_free (priv->origin_hostname); + + /* use libsoup to convert a URL */ + uri = soup_uri_new (origin_hostname); + if (uri != NULL) + origin_hostname = soup_uri_get_host (uri); + + /* remove some common prefixes */ + for (i = 0; prefixes[i] != NULL; i++) { + if (g_str_has_prefix (origin_hostname, prefixes[i])) + origin_hostname += strlen (prefixes[i]); + } + + /* fallback for localhost */ + if (g_strcmp0 (origin_hostname, "") == 0) + origin_hostname = "localhost"; + + /* success */ + priv->origin_hostname = g_strdup (origin_hostname); +} + +/** + * gs_app_add_screenshot: + * @app: a #GsApp + * @screenshot: a #AsScreenshot + * + * Adds a screenshot to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_screenshot (GsApp *app, AsScreenshot *screenshot) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (AS_IS_SCREENSHOT (screenshot)); + + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_add (priv->screenshots, g_object_ref (screenshot)); +} + +/** + * gs_app_get_screenshots: + * @app: a #GsApp + * + * Gets the list of screenshots. + * + * Returns: (element-type AsScreenshot) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_screenshots (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->screenshots; +} + +/** + * gs_app_get_update_version: + * @app: a #GsApp + * + * Gets the newest update version. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_update_version (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->update_version; +} + +/** + * gs_app_get_update_version_ui: + * @app: a #GsApp + * + * Gets the update version for the UI. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_update_version_ui (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + + /* work out the two version numbers */ + if (priv->update_version != NULL && + priv->update_version_ui == NULL) { + gs_app_ui_versions_populate (app); + } + + return priv->update_version_ui; +} + +static void +gs_app_set_update_version_internal (GsApp *app, const gchar *update_version) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + if (_g_set_str (&priv->update_version, update_version)) + gs_app_ui_versions_invalidate (app); +} + +/** + * gs_app_set_update_version: + * @app: a #GsApp + * @update_version: a string, e.g. "0.1.2.3" + * + * Sets the new version number of the update. + * + * Since: 3.22 + **/ +void +gs_app_set_update_version (GsApp *app, const gchar *update_version) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + gs_app_set_update_version_internal (app, update_version); + gs_app_queue_notify (app, obj_props[PROP_VERSION]); +} + +/** + * gs_app_get_update_details: + * @app: a #GsApp + * + * Gets the multi-line description for the update. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_update_details (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->update_details; +} + +/** + * gs_app_set_update_details: + * @app: a #GsApp + * @update_details: a string + * + * Sets the multi-line description for the update. + * + * Since: 3.22 + **/ +void +gs_app_set_update_details (GsApp *app, const gchar *update_details) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_str (&priv->update_details, update_details); +} + +/** + * gs_app_get_update_urgency: + * @app: a #GsApp + * + * Gets the update urgency. + * + * Returns: a #AsUrgencyKind, or %AS_URGENCY_KIND_UNKNOWN for unset + * + * Since: 3.22 + **/ +AsUrgencyKind +gs_app_get_update_urgency (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), AS_URGENCY_KIND_UNKNOWN); + return priv->update_urgency; +} + +/** + * gs_app_set_update_urgency: + * @app: a #GsApp + * @update_urgency: a #AsUrgencyKind + * + * Sets the update urgency. + * + * Since: 3.22 + **/ +void +gs_app_set_update_urgency (GsApp *app, AsUrgencyKind update_urgency) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (update_urgency == priv->update_urgency) + return; + priv->update_urgency = update_urgency; +} + +/** + * gs_app_get_management_plugin: + * @app: a #GsApp + * + * Gets the management plugin. + * This is some metadata about the application which is used to work out + * which plugin should handle the install, remove or upgrade actions. + * + * Typically plugins will just set this to the plugin name using + * gs_plugin_get_name(). + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_management_plugin (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->management_plugin; +} + +/** + * gs_app_set_management_plugin: + * @app: a #GsApp + * @management_plugin: a string, or %NULL, e.g. "fwupd" + * + * The management plugin is the plugin that can handle doing install and remove + * operations on the #GsApp. + * Typical values include "packagekit" and "flatpak" + * + * It is an error to attempt to change the management plugin once it has been + * previously set or to try to use this function on a wildcard application. + * + * Since: 3.22 + **/ +void +gs_app_set_management_plugin (GsApp *app, const gchar *management_plugin) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* plugins cannot adopt wildcard packages */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + g_warning ("plugins should not set the management plugin on " + "%s to %s -- create a new GsApp in refine()!", + gs_app_get_unique_id_unlocked (app), + management_plugin); + return; + } + + /* same */ + if (g_strcmp0 (priv->management_plugin, management_plugin) == 0) + return; + + /* trying to change */ + if (priv->management_plugin != NULL && management_plugin != NULL) { + g_warning ("automatically prevented from changing " + "management plugin on %s from %s to %s!", + gs_app_get_unique_id_unlocked (app), + priv->management_plugin, + management_plugin); + return; + } + + g_free (priv->management_plugin); + priv->management_plugin = g_strdup (management_plugin); +} + +/** + * gs_app_get_rating: + * @app: a #GsApp + * + * Gets the percentage rating of the application, where 100 is 5 stars. + * + * Returns: a percentage, or -1 for unset + * + * Since: 3.22 + **/ +gint +gs_app_get_rating (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), -1); + return priv->rating; +} + +/** + * gs_app_set_rating: + * @app: a #GsApp + * @rating: a percentage, or -1 for invalid + * + * Gets the percentage rating of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_rating (GsApp *app, gint rating) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + if (rating == priv->rating) + return; + priv->rating = rating; + gs_app_queue_notify (app, obj_props[PROP_RATING]); +} + +/** + * gs_app_get_review_ratings: + * @app: a #GsApp + * + * Gets the review ratings. + * + * Returns: (element-type guint32) (transfer none): a list + * + * Since: 3.22 + **/ +GArray * +gs_app_get_review_ratings (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->review_ratings; +} + +/** + * gs_app_set_review_ratings: + * @app: a #GsApp + * @review_ratings: (element-type guint32): a list + * + * Sets the review ratings. + * + * Since: 3.22 + **/ +void +gs_app_set_review_ratings (GsApp *app, GArray *review_ratings) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_array (&priv->review_ratings, review_ratings); +} + +/** + * gs_app_get_reviews: + * @app: a #GsApp + * + * Gets all the user-submitted reviews for the application. + * + * Returns: (element-type AsReview) (transfer none): the list of reviews + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_reviews (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->reviews; +} + +/** + * gs_app_add_review: + * @app: a #GsApp + * @review: a #AsReview + * + * Adds a user-submitted review to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_review (GsApp *app, AsReview *review) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (AS_IS_REVIEW (review)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_add (priv->reviews, g_object_ref (review)); +} + +/** + * gs_app_remove_review: + * @app: a #GsApp + * @review: a #AsReview + * + * Removes a user-submitted review to the application. + * + * Since: 3.22 + **/ +void +gs_app_remove_review (GsApp *app, AsReview *review) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_remove (priv->reviews, review); +} + +/** + * gs_app_get_provides: + * @app: a #GsApp + * + * Gets all the provides for the application. + * + * Returns: (element-type AsProvide) (transfer none): the list of provides + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_provides (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->provides; +} + +/** + * gs_app_add_provide: + * @app: a #GsApp + * @provide: a #AsProvide + * + * Adds a provide to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_provide (GsApp *app, AsProvide *provide) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (AS_IS_PROVIDE (provide)); + locker = g_mutex_locker_new (&priv->mutex); + g_ptr_array_add (priv->provides, g_object_ref (provide)); +} + +/** + * gs_app_get_size_download: + * @app: A #GsApp + * + * Gets the size of the total download needed to either install an available + * application, or update an already installed one. + * + * If there is a runtime not yet installed then this is also added. + * + * Returns: number of bytes, 0 for unknown, or %GS_APP_SIZE_UNKNOWABLE for invalid + * + * Since: 3.22 + **/ +guint64 +gs_app_get_size_download (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + guint64 sz; + + g_return_val_if_fail (GS_IS_APP (app), G_MAXUINT64); + + /* this app */ + sz = priv->size_download; + + /* add the runtime if this is not installed */ + if (priv->runtime != NULL) { + if (gs_app_get_state (priv->runtime) == AS_APP_STATE_AVAILABLE) + sz += gs_app_get_size_installed (priv->runtime); + } + + /* add related apps */ + for (guint i = 0; i < gs_app_list_length (priv->related); i++) { + GsApp *app_related = gs_app_list_index (priv->related, i); + sz += gs_app_get_size_download (app_related); + } + + return sz; +} + +/** + * gs_app_set_size_download: + * @app: a #GsApp + * @size_download: size in bytes, or %GS_APP_SIZE_UNKNOWABLE for invalid + * + * Sets the download size of the application, not including any + * required runtime. + * + * Since: 3.22 + **/ +void +gs_app_set_size_download (GsApp *app, guint64 size_download) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (size_download == priv->size_download) + return; + priv->size_download = size_download; +} + +/** + * gs_app_get_size_installed: + * @app: a #GsApp + * + * Gets the size on disk, either for an existing application of one that could + * be installed. + * + * Returns: size in bytes, 0 for unknown, or %GS_APP_SIZE_UNKNOWABLE for invalid. + * + * Since: 3.22 + **/ +guint64 +gs_app_get_size_installed (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + guint64 sz; + + g_return_val_if_fail (GS_IS_APP (app), G_MAXUINT64); + + /* this app */ + sz = priv->size_installed; + + /* add related apps */ + for (guint i = 0; i < gs_app_list_length (priv->related); i++) { + GsApp *app_related = gs_app_list_index (priv->related, i); + sz += gs_app_get_size_installed (app_related); + } + + return sz; +} + +/** + * gs_app_set_size_installed: + * @app: a #GsApp + * @size_installed: size in bytes, or %GS_APP_SIZE_UNKNOWABLE for invalid + * + * Sets the installed size of the application. + * + * Since: 3.22 + **/ +void +gs_app_set_size_installed (GsApp *app, guint64 size_installed) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (size_installed == priv->size_installed) + return; + priv->size_installed = size_installed; +} + +/** + * gs_app_get_metadata_item: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::device-id" + * + * Gets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata, + * for example `fwupd::device-id`. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.22 + **/ +const gchar * +gs_app_get_metadata_item (GsApp *app, const gchar *key) +{ + GVariant *tmp; + g_return_val_if_fail (GS_IS_APP (app), NULL); + g_return_val_if_fail (key != NULL, NULL); + tmp = gs_app_get_metadata_variant (app, key); + if (tmp == NULL) + return NULL; + return g_variant_get_string (tmp, NULL); +} + +/** + * gs_app_set_metadata: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::DeviceID" + * @value: a string, e.g. "fubar" + * + * Sets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata. + * + * Since: 3.22 + **/ +void +gs_app_set_metadata (GsApp *app, const gchar *key, const gchar *value) +{ + g_autoptr(GVariant) tmp = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (key != NULL); + if (value != NULL) + tmp = g_variant_new_string (value); + gs_app_set_metadata_variant (app, key, tmp); +} + +/** + * gs_app_get_metadata_variant: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::device-id" + * + * Gets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata. + * + * Returns: a string, or %NULL for unset + * + * Since: 3.26 + **/ +GVariant * +gs_app_get_metadata_variant (GsApp *app, const gchar *key) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + g_return_val_if_fail (key != NULL, NULL); + return g_hash_table_lookup (priv->metadata, key); +} + +/** + * gs_app_set_metadata_variant: + * @app: a #GsApp + * @key: a string, e.g. "fwupd::DeviceID" + * @value: a #GVariant + * + * Sets some metadata for the application. + * Is is expected that plugins namespace any plugin-specific metadata, + * for example `fwupd::device-id`. + * + * Since: 3.26 + **/ +void +gs_app_set_metadata_variant (GsApp *app, const gchar *key, GVariant *value) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + GVariant *found; + + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* if no value, then remove the key */ + if (value == NULL) { + g_hash_table_remove (priv->metadata, key); + return; + } + + /* check we're not overwriting */ + found = g_hash_table_lookup (priv->metadata, key); + if (found != NULL) { + if (g_variant_equal (found, value)) + return; + if (g_variant_type_equal (g_variant_get_type (value), G_VARIANT_TYPE_STRING) && + g_variant_type_equal (g_variant_get_type (found), G_VARIANT_TYPE_STRING)) { + g_debug ("tried overwriting %s key %s from %s to %s", + priv->id, key, + g_variant_get_string (found, NULL), + g_variant_get_string (value, NULL)); + } else { + g_debug ("tried overwriting %s key %s (%s->%s)", + priv->id, key, + g_variant_get_type_string (found), + g_variant_get_type_string (value)); + } + return; + } + g_hash_table_insert (priv->metadata, g_strdup (key), g_variant_ref (value)); +} + +/** + * gs_app_get_addons: + * @app: a #GsApp + * + * Gets the list of addons for the application. + * + * Returns: (transfer none): a list of addons + * + * Since: 3.22 + **/ +GsAppList * +gs_app_get_addons (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->addons; +} + +/** + * gs_app_add_addon: + * @app: a #GsApp + * @addon: a #GsApp + * + * Adds an addon to the list of application addons. + * + * Since: 3.22 + **/ +void +gs_app_add_addon (GsApp *app, GsApp *addon) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (addon)); + + locker = g_mutex_locker_new (&priv->mutex); + gs_app_list_add (priv->addons, addon); +} + +/** + * gs_app_remove_addon: + * @app: a #GsApp + * @addon: a #GsApp + * + * Removes an addon from the list of application addons. + * + * Since: 3.22 + **/ +void +gs_app_remove_addon (GsApp *app, GsApp *addon) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (addon)); + locker = g_mutex_locker_new (&priv->mutex); + gs_app_list_remove (priv->addons, addon); +} + +/** + * gs_app_get_related: + * @app: a #GsApp + * + * Gets any related applications. + * + * Returns: (transfer none): a list of applications + * + * Since: 3.22 + **/ +GsAppList * +gs_app_get_related (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->related; +} + +/** + * gs_app_add_related: + * @app: a #GsApp + * @app2: a #GsApp + * + * Adds a related application. + * + * Since: 3.22 + **/ +void +gs_app_add_related (GsApp *app, GsApp *app2) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + GsAppPrivate *priv2 = gs_app_get_instance_private (app2); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (app2)); + + locker = g_mutex_locker_new (&priv->mutex); + + /* if the app is updatable-live and any related app is not then + * degrade to the offline state */ + if (priv->state == AS_APP_STATE_UPDATABLE_LIVE && + priv2->state == AS_APP_STATE_UPDATABLE) + priv->state = priv2->state; + + gs_app_list_add (priv->related, app2); +} + +/** + * gs_app_get_history: + * @app: a #GsApp + * + * Gets the history of this application. + * + * Returns: (transfer none): a list + * + * Since: 3.22 + **/ +GsAppList * +gs_app_get_history (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->history; +} + +/** + * gs_app_add_history: + * @app: a #GsApp + * @app2: a #GsApp + * + * Adds a history item for this package. + * + * Since: 3.22 + **/ +void +gs_app_add_history (GsApp *app, GsApp *app2) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (GS_IS_APP (app2)); + locker = g_mutex_locker_new (&priv->mutex); + gs_app_list_add (priv->history, app2); +} + +/** + * gs_app_get_install_date: + * @app: a #GsApp + * + * Gets the date that an application was installed. + * + * Returns: A UNIX epoch, or 0 for unset + * + * Since: 3.22 + **/ +guint64 +gs_app_get_install_date (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->install_date; +} + +/** + * gs_app_set_install_date: + * @app: a #GsApp + * @install_date: an epoch, or %GS_APP_INSTALL_DATE_UNKNOWN + * + * Sets the date that an application was installed. + * + * Since: 3.22 + **/ +void +gs_app_set_install_date (GsApp *app, guint64 install_date) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (install_date == priv->install_date) + return; + priv->install_date = install_date; +} + +/** + * gs_app_is_installed: + * @app: a #GsApp + * + * Gets whether the app is installed or not. + * + * Returns: %TRUE if the app is installed, %FALSE otherwise. + * + * Since: 3.22 + **/ +gboolean +gs_app_is_installed (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return (priv->state == AS_APP_STATE_INSTALLED) || + (priv->state == AS_APP_STATE_UPDATABLE) || + (priv->state == AS_APP_STATE_UPDATABLE_LIVE) || + (priv->state == AS_APP_STATE_REMOVING); +} + +/** + * gs_app_is_updatable: + * @app: a #GsApp + * + * Gets whether the app is updatable or not. + * + * Returns: %TRUE if the app is updatable, %FALSE otherwise. + * + * Since: 3.22 + **/ +gboolean +gs_app_is_updatable (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + if (priv->kind == AS_APP_KIND_OS_UPGRADE) + return TRUE; + return (priv->state == AS_APP_STATE_UPDATABLE) || + (priv->state == AS_APP_STATE_UPDATABLE_LIVE); +} + +/** + * gs_app_get_categories: + * @app: a #GsApp + * + * Gets the list of categories for an application. + * + * Returns: (element-type utf8) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_categories (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->categories; +} + +/** + * gs_app_has_category: + * @app: a #GsApp + * @category: a category ID, e.g. "AudioVideo" + * + * Checks if the application is in a specific category. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_app_has_category (GsApp *app, const gchar *category) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + /* find the category */ + for (i = 0; i < priv->categories->len; i++) { + tmp = g_ptr_array_index (priv->categories, i); + if (g_strcmp0 (tmp, category) == 0) + return TRUE; + } + return FALSE; +} + +/** + * gs_app_set_categories: + * @app: a #GsApp + * @categories: a set of categories + * + * Set the list of categories for an application. + * + * Since: 3.22 + **/ +void +gs_app_set_categories (GsApp *app, GPtrArray *categories) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (categories != NULL); + locker = g_mutex_locker_new (&priv->mutex); + _g_set_ptr_array (&priv->categories, categories); +} + +/** + * gs_app_add_category: + * @app: a #GsApp + * @category: a category ID, e.g. "AudioVideo" + * + * Adds a category ID to an application. + * + * Since: 3.22 + **/ +void +gs_app_add_category (GsApp *app, const gchar *category) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (category != NULL); + locker = g_mutex_locker_new (&priv->mutex); + if (gs_app_has_category (app, category)) + return; + g_ptr_array_add (priv->categories, g_strdup (category)); +} + +/** + * gs_app_remove_category: + * @app: a #GsApp + * @category: a category ID, e.g. "AudioVideo" + * + * Removes an category ID from an application, it exists. + * + * Returns: %TRUE for success + * + * Since: 3.24 + **/ +gboolean +gs_app_remove_category (GsApp *app, const gchar *category) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + const gchar *tmp; + guint i; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + locker = g_mutex_locker_new (&priv->mutex); + + for (i = 0; i < priv->categories->len; i++) { + tmp = g_ptr_array_index (priv->categories, i); + if (g_strcmp0 (tmp, category) != 0) + continue; + g_ptr_array_remove_index_fast (priv->categories, i); + return TRUE; + } + return FALSE; +} + +/** + * gs_app_set_is_update_downloaded: + * @app: a #GsApp + * @is_update_downloaded: Whether a new update is already downloaded locally + * + * Sets if the new update is already downloaded for the app. + * + * Since: 3.36 + **/ +void +gs_app_set_is_update_downloaded (GsApp *app, gboolean is_update_downloaded) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->is_update_downloaded = is_update_downloaded; +} + +/** + * gs_app_get_is_update_downloaded: + * @app: a #GsApp + * + * Gets if the new update is already downloaded for the app and + * is locally available. + * + * Returns: (element-type gboolean): Whether a new update for the #GsApp is already downloaded. + * + * Since: 3.36 + **/ +gboolean +gs_app_get_is_update_downloaded (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return priv->is_update_downloaded; +} + +/** + * gs_app_get_key_colors: + * @app: a #GsApp + * + * Gets the key colors used in the application icon. + * + * Returns: (element-type GdkRGBA) (transfer none): a list + * + * Since: 3.22 + **/ +GPtrArray * +gs_app_get_key_colors (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), NULL); + return priv->key_colors; +} + +/** + * gs_app_set_key_colors: + * @app: a #GsApp + * @key_colors: (element-type GdkRGBA): a set of key colors + * + * Sets the key colors used in the application icon. + * + * Since: 3.22 + **/ +void +gs_app_set_key_colors (GsApp *app, GPtrArray *key_colors) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (key_colors != NULL); + locker = g_mutex_locker_new (&priv->mutex); + if (_g_set_ptr_array (&priv->key_colors, key_colors)) + gs_app_queue_notify (app, obj_props[PROP_KEY_COLORS]); +} + +/** + * gs_app_add_key_color: + * @app: a #GsApp + * @key_color: a #GdkRGBA + * + * Adds a key colors used in the application icon. + * + * Since: 3.22 + **/ +void +gs_app_add_key_color (GsApp *app, GdkRGBA *key_color) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (key_color != NULL); + g_ptr_array_add (priv->key_colors, gdk_rgba_copy (key_color)); + gs_app_queue_notify (app, obj_props[PROP_KEY_COLORS]); +} + +/** + * gs_app_add_kudo: + * @app: a #GsApp + * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE + * + * Adds a kudo to the application. + * + * Since: 3.22 + **/ +void +gs_app_add_kudo (GsApp *app, GsAppKudo kudo) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + if (kudo & GS_APP_KUDO_SANDBOXED_SECURE) + kudo |= GS_APP_KUDO_SANDBOXED; + priv->kudos |= kudo; +} + +/** + * gs_app_remove_kudo: + * @app: a #GsApp + * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE + * + * Removes a kudo from the application. + * + * Since: 3.30 + **/ +void +gs_app_remove_kudo (GsApp *app, GsAppKudo kudo) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->kudos &= ~kudo; +} + +/** + * gs_app_has_kudo: + * @app: a #GsApp + * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE + * + * Finds out if a kudo has been awarded by the application. + * + * Returns: %TRUE if the app has the specified kudo + * + * Since: 3.22 + **/ +gboolean +gs_app_has_kudo (GsApp *app, GsAppKudo kudo) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + return (priv->kudos & kudo) > 0; +} + +/** + * gs_app_get_kudos: + * @app: a #GsApp + * + * Gets all the kudos the application has been awarded. + * + * Returns: the kudos, as a bitfield + * + * Since: 3.22 + **/ +guint64 +gs_app_get_kudos (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->kudos; +} + +/** + * gs_app_get_kudos_percentage: + * @app: a #GsApp + * + * Gets the kudos, as a percentage value. + * + * Returns: a percentage, with 0 for no kudos and a maximum of 100. + * + * Since: 3.22 + **/ +guint +gs_app_get_kudos_percentage (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + guint percentage = 0; + + g_return_val_if_fail (GS_IS_APP (app), 0); + + if ((priv->kudos & GS_APP_KUDO_MY_LANGUAGE) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_RECENT_RELEASE) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_FEATURED_RECOMMENDED) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_MODERN_TOOLKIT) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_SEARCH_PROVIDER) > 0) + percentage += 10; + if ((priv->kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0) + percentage += 10; + if ((priv->kudos & GS_APP_KUDO_USES_NOTIFICATIONS) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_HAS_KEYWORDS) > 0) + percentage += 5; + if ((priv->kudos & GS_APP_KUDO_HAS_SCREENSHOTS) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_HIGH_CONTRAST) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_HI_DPI_ICON) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_SANDBOXED) > 0) + percentage += 20; + if ((priv->kudos & GS_APP_KUDO_SANDBOXED_SECURE) > 0) + percentage += 20; + + /* popular apps should be at *least* 50% */ + if ((priv->kudos & GS_APP_KUDO_POPULAR) > 0) + percentage = MAX (percentage, 50); + + return MIN (percentage, 100); +} + +/** + * gs_app_get_to_be_installed: + * @app: a #GsApp + * + * Gets if the application is queued for installation. + * + * This is only set for addons when the user has selected some addons to be + * installed before installing the main application. + * Plugins should check all the addons for this property when installing + * main applications so that the chosen set of addons is also installed at the + * same time. This is never set when applications do not have addons. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_app_get_to_be_installed (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + return priv->to_be_installed; +} + +/** + * gs_app_set_to_be_installed: + * @app: a #GsApp + * @to_be_installed: if the app is due to be installed + * + * Sets if the application is queued for installation. + * + * Since: 3.22 + **/ +void +gs_app_set_to_be_installed (GsApp *app, gboolean to_be_installed) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + + priv->to_be_installed = to_be_installed; +} + +/** + * gs_app_has_quirk: + * @app: a #GsApp + * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY + * + * Finds out if an application has a specific quirk. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_app_has_quirk (GsApp *app, GsAppQuirk quirk) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), FALSE); + + return (priv->quirk & quirk) > 0; +} + +/** + * gs_app_add_quirk: + * @app: a #GsApp + * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY + * + * Adds a quirk to an application. + * + * Since: 3.22 + **/ +void +gs_app_add_quirk (GsApp *app, GsAppQuirk quirk) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if ((priv->quirk & quirk) > 0) + return; + + locker = g_mutex_locker_new (&priv->mutex); + priv->quirk |= quirk; + gs_app_queue_notify (app, obj_props[PROP_QUIRK]); +} + +/** + * gs_app_remove_quirk: + * @app: a #GsApp + * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY + * + * Removes a quirk from an application. + * + * Since: 3.22 + **/ +void +gs_app_remove_quirk (GsApp *app, GsAppQuirk quirk) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = NULL; + g_return_if_fail (GS_IS_APP (app)); + + /* same */ + if ((priv->quirk & quirk) == 0) + return; + + locker = g_mutex_locker_new (&priv->mutex); + priv->quirk &= ~quirk; + gs_app_queue_notify (app, obj_props[PROP_QUIRK]); +} + +/** + * gs_app_set_match_value: + * @app: a #GsApp + * @match_value: a value + * + * Set a match quality value, where higher values correspond to a + * "better" search match, and should be shown above lower results. + * + * Since: 3.22 + **/ +void +gs_app_set_match_value (GsApp *app, guint match_value) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->match_value = match_value; +} + +/** + * gs_app_get_match_value: + * @app: a #GsApp + * + * Get a match quality value, where higher values correspond to a + * "better" search match, and should be shown above lower results. + * + * Note: This value is only valid when processing the result set + * and may be overwritten on subsequent searches if the plugin is using + * a cache. + * + * Returns: a value, where higher is better + * + * Since: 3.22 + **/ +guint +gs_app_get_match_value (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->match_value; +} + +/** + * gs_app_set_priority: + * @app: a #GsApp + * @priority: a value + * + * Set a priority value. + * + * Since: 3.22 + **/ +void +gs_app_set_priority (GsApp *app, guint priority) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_if_fail (GS_IS_APP (app)); + priv->priority = priority; +} + +/** + * gs_app_get_priority: + * @app: a #GsApp + * + * Get a priority value, where higher values will be chosen where + * multiple #GsApp's match a specific rule. + * + * Returns: a value, where higher is better + * + * Since: 3.22 + **/ +guint +gs_app_get_priority (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_return_val_if_fail (GS_IS_APP (app), 0); + return priv->priority; +} + +/** + * gs_app_get_cancellable: + * @app: a #GsApp + * + * Get a cancellable to be used with operations related to the #GsApp. This is a + * way for views to be able to cancel an on-going operation. If the #GCancellable + * is canceled, it will be unreferenced and renewed before returning it, i.e. the + * cancellable object will always be ready to use for new operations. So be sure + * to keep a reference to it if you do more than just passing the cancellable to + * a process. + * + * Returns: a #GCancellable + * + * Since: 3.28 + **/ +GCancellable * +gs_app_get_cancellable (GsApp *app) +{ + g_autoptr(GCancellable) cancellable = NULL; + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + + if (priv->cancellable == NULL || g_cancellable_is_cancelled (priv->cancellable)) { + cancellable = g_cancellable_new (); + g_set_object (&priv->cancellable, cancellable); + } + return priv->cancellable; +} + +/** + * gs_app_get_pending_action: + * @app: a #GsApp + * + * Get the pending action for this #GsApp, or %NULL if no action is pending. + * + * Returns: the #GsAppAction of the @app. + **/ +GsPluginAction +gs_app_get_pending_action (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + return priv->pending_action; +} + +/** + * gs_app_set_pending_action: + * @app: a #GsApp + * @action: a #GsPluginAction + * + * Set an action that is pending on this #GsApp. + **/ +void +gs_app_set_pending_action (GsApp *app, + GsPluginAction action) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex); + gs_app_set_pending_action_internal (app, action); +} + +static void +gs_app_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + switch (prop_id) { + case PROP_ID: + g_value_set_string (value, priv->id); + break; + case PROP_NAME: + g_value_set_string (value, priv->name); + break; + case PROP_VERSION: + g_value_set_string (value, priv->version); + break; + case PROP_SUMMARY: + g_value_set_string (value, priv->summary); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, priv->description); + break; + case PROP_RATING: + g_value_set_int (value, priv->rating); + break; + case PROP_KIND: + g_value_set_uint (value, priv->kind); + break; + case PROP_STATE: + g_value_set_uint (value, priv->state); + break; + case PROP_PROGRESS: + g_value_set_uint (value, priv->progress); + break; + case PROP_CAN_CANCEL_INSTALLATION: + g_value_set_boolean (value, priv->allow_cancel); + break; + case PROP_INSTALL_DATE: + g_value_set_uint64 (value, priv->install_date); + break; + case PROP_QUIRK: + g_value_set_uint64 (value, priv->quirk); + break; + case PROP_KEY_COLORS: + g_value_set_boxed (value, priv->key_colors); + break; + case PROP_IS_UPDATE_DOWNLOADED: + g_value_set_boolean (value, priv->is_update_downloaded); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + switch (prop_id) { + case PROP_ID: + gs_app_set_id (app, g_value_get_string (value)); + break; + case PROP_NAME: + gs_app_set_name (app, + GS_APP_QUALITY_UNKNOWN, + g_value_get_string (value)); + break; + case PROP_VERSION: + gs_app_set_version (app, g_value_get_string (value)); + break; + case PROP_SUMMARY: + gs_app_set_summary (app, + GS_APP_QUALITY_UNKNOWN, + g_value_get_string (value)); + break; + case PROP_DESCRIPTION: + gs_app_set_description (app, + GS_APP_QUALITY_UNKNOWN, + g_value_get_string (value)); + break; + case PROP_RATING: + gs_app_set_rating (app, g_value_get_int (value)); + break; + case PROP_KIND: + gs_app_set_kind (app, g_value_get_uint (value)); + break; + case PROP_STATE: + gs_app_set_state_internal (app, g_value_get_uint (value)); + break; + case PROP_PROGRESS: + gs_app_set_progress (app, g_value_get_uint (value)); + break; + case PROP_CAN_CANCEL_INSTALLATION: + priv->allow_cancel = g_value_get_boolean (value); + break; + case PROP_INSTALL_DATE: + gs_app_set_install_date (app, g_value_get_uint64 (value)); + break; + case PROP_QUIRK: + priv->quirk = g_value_get_uint64 (value); + break; + case PROP_KEY_COLORS: + gs_app_set_key_colors (app, g_value_get_boxed (value)); + break; + case PROP_IS_UPDATE_DOWNLOADED: + gs_app_set_is_update_downloaded (app, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_app_dispose (GObject *object) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_clear_object (&priv->runtime); + + g_clear_pointer (&priv->addons, g_object_unref); + g_clear_pointer (&priv->history, g_object_unref); + g_clear_pointer (&priv->related, g_object_unref); + g_clear_pointer (&priv->screenshots, g_ptr_array_unref); + g_clear_pointer (&priv->review_ratings, g_array_unref); + g_clear_pointer (&priv->reviews, g_ptr_array_unref); + g_clear_pointer (&priv->provides, g_ptr_array_unref); + g_clear_pointer (&priv->icons, g_ptr_array_unref); + + G_OBJECT_CLASS (gs_app_parent_class)->dispose (object); +} + +static void +gs_app_finalize (GObject *object) +{ + GsApp *app = GS_APP (object); + GsAppPrivate *priv = gs_app_get_instance_private (app); + + g_mutex_clear (&priv->mutex); + g_free (priv->id); + g_free (priv->unique_id); + g_free (priv->branch); + g_free (priv->name); + g_hash_table_unref (priv->urls); + g_hash_table_unref (priv->launchables); + g_free (priv->license); + g_strfreev (priv->menu_path); + g_free (priv->origin); + g_free (priv->origin_appstream); + g_free (priv->origin_hostname); + g_ptr_array_unref (priv->sources); + g_ptr_array_unref (priv->source_ids); + g_free (priv->project_group); + g_free (priv->developer_name); + g_free (priv->agreement); + g_free (priv->version); + g_free (priv->version_ui); + g_free (priv->summary); + g_free (priv->summary_missing); + g_free (priv->description); + g_free (priv->update_version); + g_free (priv->update_version_ui); + g_free (priv->update_details); + g_free (priv->management_plugin); + g_hash_table_unref (priv->metadata); + g_ptr_array_unref (priv->categories); + g_ptr_array_unref (priv->key_colors); + g_clear_object (&priv->cancellable); + if (priv->local_file != NULL) + g_object_unref (priv->local_file); + if (priv->content_rating != NULL) + g_object_unref (priv->content_rating); + if (priv->pixbuf != NULL) + g_object_unref (priv->pixbuf); + if (priv->action_screenshot != NULL) + g_object_unref (priv->action_screenshot); + + G_OBJECT_CLASS (gs_app_parent_class)->finalize (object); +} + +static void +gs_app_class_init (GsAppClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->dispose = gs_app_dispose; + object_class->finalize = gs_app_finalize; + object_class->get_property = gs_app_get_property; + object_class->set_property = gs_app_set_property; + + /** + * GsApp:id: + */ + obj_props[PROP_ID] = g_param_spec_string ("id", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:name: + */ + obj_props[PROP_NAME] = g_param_spec_string ("name", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:version: + */ + obj_props[PROP_VERSION] = g_param_spec_string ("version", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:summary: + */ + obj_props[PROP_SUMMARY] = g_param_spec_string ("summary", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:description: + */ + obj_props[PROP_DESCRIPTION] = g_param_spec_string ("description", NULL, NULL, + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:rating: + */ + obj_props[PROP_RATING] = g_param_spec_int ("rating", NULL, NULL, + -1, 100, -1, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:kind: + */ + obj_props[PROP_KIND] = g_param_spec_uint ("kind", NULL, NULL, + AS_APP_KIND_UNKNOWN, + AS_APP_KIND_LAST, + AS_APP_KIND_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:state: + */ + obj_props[PROP_STATE] = g_param_spec_uint ("state", NULL, NULL, + AS_APP_STATE_UNKNOWN, + AS_APP_STATE_LAST, + AS_APP_STATE_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:progress: + * + * A percentage (0–100, inclusive) indicating the progress through the + * current task on this app. The value may otherwise be + * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide + * confidence interval. + */ + obj_props[PROP_PROGRESS] = g_param_spec_uint ("progress", NULL, NULL, + 0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:allow-cancel: + */ + obj_props[PROP_CAN_CANCEL_INSTALLATION] = + g_param_spec_boolean ("allow-cancel", NULL, NULL, TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:install-date: + */ + obj_props[PROP_INSTALL_DATE] = g_param_spec_uint64 ("install-date", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:quirk: + */ + obj_props[PROP_QUIRK] = g_param_spec_uint64 ("quirk", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * GsApp:pending-action: + */ + obj_props[PROP_PENDING_ACTION] = g_param_spec_uint64 ("pending-action", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READABLE); + + /** + * GsApp:key-colors: + */ + obj_props[PROP_KEY_COLORS] = g_param_spec_boxed ("key-colors", NULL, NULL, + G_TYPE_PTR_ARRAY, G_PARAM_READWRITE); + + /** + * GsApp:is-update-downloaded: + */ + obj_props[PROP_IS_UPDATE_DOWNLOADED] = g_param_spec_boolean ("is-update-downloaded", NULL, NULL, + FALSE, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, PROP_LAST, obj_props); +} + +static void +gs_app_init (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + priv->rating = -1; + priv->sources = g_ptr_array_new_with_free_func (g_free); + priv->source_ids = g_ptr_array_new_with_free_func (g_free); + priv->categories = g_ptr_array_new_with_free_func (g_free); + priv->key_colors = g_ptr_array_new_with_free_func ((GDestroyNotify) gdk_rgba_free); + priv->addons = gs_app_list_new (); + priv->related = gs_app_list_new (); + priv->history = gs_app_list_new (); + priv->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->provides = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + priv->metadata = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_variant_unref); + priv->urls = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_free); + priv->launchables = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_free); + priv->allow_cancel = TRUE; + g_mutex_init (&priv->mutex); +} + +/** + * gs_app_new: + * @id: an application ID, or %NULL, e.g. "org.gnome.Software.desktop" + * + * Creates a new application object. + * + * The ID should only be set when the application ID (with optional prefix) is + * known; it is perfectly valid to use gs_app_new() with an @id of %NULL, and + * then relying on another plugin to set the @id using gs_app_set_id() based on + * some other information. + * + * For instance, a #GsApp is created with no ID when returning results from the + * packagekit plugin, but with the default source name set as the package name. + * The source name is read by the appstream plugin, and if matched in the + * AppStream XML the correct ID is set, along with other higher quality data + * like the application icon and long description. + * + * Returns: a new #GsApp + * + * Since: 3.22 + **/ +GsApp * +gs_app_new (const gchar *id) +{ + GsApp *app; + app = g_object_new (GS_TYPE_APP, + "id", id, + NULL); + return GS_APP (app); +} + +/** + * gs_app_set_from_unique_id: + * @app: a #GsApp + * @unique_id: an application unique ID, e.g. + * `system/flatpak/gnome/desktop/org.gnome.Software.desktop/master` + * + * Sets details on an application object. + * + * The unique ID will be parsed to set some information in the application such + * as the scope, bundle kind, id, etc. + * + * Since: 3.26 + **/ +void +gs_app_set_from_unique_id (GsApp *app, const gchar *unique_id) +{ + g_auto(GStrv) split = NULL; + + g_return_if_fail (GS_IS_APP (app)); + g_return_if_fail (unique_id != NULL); + + split = g_strsplit (unique_id, "/", -1); + if (g_strv_length (split) != 6) + return; + if (g_strcmp0 (split[0], "*") != 0) + gs_app_set_scope (app, as_app_scope_from_string (split[0])); + if (g_strcmp0 (split[1], "*") != 0) + gs_app_set_bundle_kind (app, as_bundle_kind_from_string (split[1])); + if (g_strcmp0 (split[2], "*") != 0) + gs_app_set_origin (app, split[2]); + if (g_strcmp0 (split[3], "*") != 0) + gs_app_set_kind (app, as_app_kind_from_string (split[3])); + if (g_strcmp0 (split[4], "*") != 0) + gs_app_set_id (app, split[4]); + if (g_strcmp0 (split[5], "*") != 0) + gs_app_set_branch (app, split[5]); +} + +/** + * gs_app_new_from_unique_id: + * @unique_id: an application unique ID, e.g. + * `system/flatpak/gnome/desktop/org.gnome.Software.desktop/master` + * + * Creates a new application object. + * + * The unique ID will be parsed to set some information in the application such + * as the scope, bundle kind, id, etc. Unlike gs_app_new(), it cannot take a + * %NULL argument. + * + * Returns: a new #GsApp + * + * Since: 3.22 + **/ +GsApp * +gs_app_new_from_unique_id (const gchar *unique_id) +{ + GsApp *app; + g_return_val_if_fail (unique_id != NULL, NULL); + app = gs_app_new (NULL); + gs_app_set_from_unique_id (app, unique_id); + return app; +} + +/** + * gs_app_get_origin_ui: + * @app: a #GsApp + * + * Gets the package origin that's suitable for UI use. + * + * Returns: The package origin for UI use + * + * Since: 3.32 + **/ +gchar * +gs_app_get_origin_ui (GsApp *app) +{ + /* use the distro name for official packages */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) { + g_autoptr(GsOsRelease) os_release = gs_os_release_new (NULL); + if (os_release != NULL) + return g_strdup (gs_os_release_get_name (os_release)); + } + + /* use "Local file" rather than the filename for local files */ + if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL) { + /* TRANSLATORS: this is a locally downloaded package */ + return g_strdup (_("Local file")); + } + + /* capitalize "Flathub" and "Flathub Beta" */ + if (g_strcmp0 (gs_app_get_origin (app), "flathub") == 0) { + return g_strdup ("Flathub"); + } else if (g_strcmp0 (gs_app_get_origin (app), "flathub-beta") == 0) { + return g_strdup ("Flathub Beta"); + } + + /* fall back to origin */ + return g_strdup (gs_app_get_origin (app)); +} + +/** + * gs_app_get_packaging_format: + * @app: a #GsApp + * + * Gets the packaging format, e.g. 'RPM' or 'Flatpak'. + * + * Returns: The packaging format + * + * Since: 3.32 + **/ +gchar * +gs_app_get_packaging_format (GsApp *app) +{ + AsBundleKind bundle_kind; + const gchar *bundle_kind_ui; + const gchar *packaging_format; + + /* does the app have packaging format set? */ + packaging_format = gs_app_get_metadata_item (app, "GnomeSoftware::PackagingFormat"); + if (packaging_format != NULL) + return g_strdup (packaging_format); + + /* fall back to bundle kind */ + bundle_kind = gs_app_get_bundle_kind (app); + switch (bundle_kind) { + case AS_BUNDLE_KIND_UNKNOWN: + bundle_kind_ui = NULL; + break; + case AS_BUNDLE_KIND_LIMBA: + bundle_kind_ui = "Limba"; + break; + case AS_BUNDLE_KIND_FLATPAK: + bundle_kind_ui = "Flatpak"; + break; + case AS_BUNDLE_KIND_SNAP: + bundle_kind_ui = "Snap"; + break; + case AS_BUNDLE_KIND_PACKAGE: + bundle_kind_ui = _("Package"); + break; + case AS_BUNDLE_KIND_CABINET: + bundle_kind_ui = "Cabinet"; + break; + case AS_BUNDLE_KIND_APPIMAGE: + bundle_kind_ui = "AppImage"; + break; + default: + g_warning ("unhandled bundle kind %s", as_bundle_kind_to_string (bundle_kind)); + bundle_kind_ui = as_bundle_kind_to_string (bundle_kind); + } + + return g_strdup (bundle_kind_ui); +} + +/** + * gs_app_subsume_metadata: + * @app: a #GsApp + * @donor: another #GsApp + * + * Copies any metadata from @donor to @app. + * + * Since: 3.32 + **/ +void +gs_app_subsume_metadata (GsApp *app, GsApp *donor) +{ + GsAppPrivate *priv = gs_app_get_instance_private (donor); + g_autoptr(GList) keys = g_hash_table_get_keys (priv->metadata); + for (GList *l = keys; l != NULL; l = l->next) { + const gchar *key = l->data; + GVariant *tmp = gs_app_get_metadata_variant (donor, key); + if (gs_app_get_metadata_variant (app, key) != NULL) + continue; + gs_app_set_metadata_variant (app, key, tmp); + } +} + +GsAppPermissions +gs_app_get_permissions (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + return priv->permissions; +} + +void +gs_app_set_permissions (GsApp *app, GsAppPermissions permissions) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + priv->permissions = permissions; +} + +GsAppPermissions +gs_app_get_update_permissions (GsApp *app) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + return priv->update_permissions; +} + +void +gs_app_set_update_permissions (GsApp *app, GsAppPermissions update_permissions) +{ + GsAppPrivate *priv = gs_app_get_instance_private (app); + + priv->update_permissions = update_permissions; +} diff --git a/lib/gs-app.h b/lib/gs-app.h new file mode 100644 index 0000000..4c199dc --- /dev/null +++ b/lib/gs-app.h @@ -0,0 +1,405 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gdk/gdk.h> +#include <gdk-pixbuf/gdk-pixbuf.h> +#include <appstream-glib.h> + +G_BEGIN_DECLS + +#define GS_TYPE_APP (gs_app_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsApp, gs_app, GS, APP, GObject) + +struct _GsAppClass +{ + GObjectClass parent_class; + void (*to_string) (GsApp *app, + GString *str); + gpointer padding[30]; +}; + +/** + * GsAppKudo: + * @GS_APP_KUDO_MY_LANGUAGE: Localised in my language + * @GS_APP_KUDO_RECENT_RELEASE: Released recently + * @GS_APP_KUDO_FEATURED_RECOMMENDED: Chosen for the front page + * @GS_APP_KUDO_MODERN_TOOLKIT: Uses a modern toolkit + * @GS_APP_KUDO_SEARCH_PROVIDER: Provides a search provider + * @GS_APP_KUDO_INSTALLS_USER_DOCS: Installs user docs + * @GS_APP_KUDO_USES_NOTIFICATIONS: Registers notifications + * @GS_APP_KUDO_HAS_KEYWORDS: Has at least 1 keyword + * @GS_APP_KUDO_HAS_SCREENSHOTS: Supplies screenshots + * @GS_APP_KUDO_POPULAR: Is popular + * @GS_APP_KUDO_HIGH_CONTRAST: Installs a high contrast icon + * @GS_APP_KUDO_HI_DPI_ICON: Installs a HiDPI icon + * @GS_APP_KUDO_SANDBOXED: Application is sandboxed + * @GS_APP_KUDO_SANDBOXED_SECURE: Application is sandboxed securely + * + * Any awards given to the application. + **/ +typedef enum { + GS_APP_KUDO_MY_LANGUAGE = 1 << 0, + GS_APP_KUDO_RECENT_RELEASE = 1 << 1, + GS_APP_KUDO_FEATURED_RECOMMENDED = 1 << 2, + GS_APP_KUDO_MODERN_TOOLKIT = 1 << 3, + GS_APP_KUDO_SEARCH_PROVIDER = 1 << 4, + GS_APP_KUDO_INSTALLS_USER_DOCS = 1 << 5, + GS_APP_KUDO_USES_NOTIFICATIONS = 1 << 6, + GS_APP_KUDO_HAS_KEYWORDS = 1 << 7, + GS_APP_KUDO_HAS_SCREENSHOTS = 1 << 9, + GS_APP_KUDO_POPULAR = 1 << 10, + GS_APP_KUDO_HIGH_CONTRAST = 1 << 13, + GS_APP_KUDO_HI_DPI_ICON = 1 << 14, + GS_APP_KUDO_SANDBOXED = 1 << 15, + GS_APP_KUDO_SANDBOXED_SECURE = 1 << 16, + /*< private >*/ + GS_APP_KUDO_LAST +} GsAppKudo; + +/** + * GsAppQuirk: + * @GS_APP_QUIRK_NONE: No special attributes + * @GS_APP_QUIRK_PROVENANCE: Installed by OS vendor + * @GS_APP_QUIRK_COMPULSORY: Cannot be removed + * @GS_APP_QUIRK_HAS_SOURCE: Has a source to allow staying up-to-date + * @GS_APP_QUIRK_IS_WILDCARD: Matches applications from any plugin + * @GS_APP_QUIRK_NEEDS_REBOOT: A reboot is required after the action + * @GS_APP_QUIRK_NOT_REVIEWABLE: The app is not reviewable + * @GS_APP_QUIRK_HAS_SHORTCUT: The app has a shortcut in the system + * @GS_APP_QUIRK_NOT_LAUNCHABLE: The app is not launchable (run-able) + * @GS_APP_QUIRK_NEEDS_USER_ACTION: The component requires some kind of user action + * @GS_APP_QUIRK_IS_PROXY: Is a proxy app that operates on other applications + * @GS_APP_QUIRK_REMOVABLE_HARDWARE: The device is unusable whilst the action is performed + * @GS_APP_QUIRK_DEVELOPER_VERIFIED: The app developer has been verified + * @GS_APP_QUIRK_PARENTAL_FILTER: The app has been filtered by parental controls, and should be hidden + * @GS_APP_QUIRK_NEW_PERMISSIONS: The update requires new permissions + * @GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE: The app cannot be run by the current user due to parental controls, and should not be launchable + * @GS_APP_QUIRK_HIDE_FROM_SEARCH: The app should not be shown in search results + * @GS_APP_QUIRK_HIDE_EVERYWHERE: The app should not be shown anywhere (it’s blocklisted) + * @GS_APP_QUIRK_DO_NOT_AUTO_UPDATE: The app should not be automatically updated + * + * The application attributes. + **/ +typedef enum { + GS_APP_QUIRK_NONE = 0, /* Since: 3.32 */ + GS_APP_QUIRK_PROVENANCE = 1 << 0, /* Since: 3.32 */ + GS_APP_QUIRK_COMPULSORY = 1 << 1, /* Since: 3.32 */ + GS_APP_QUIRK_HAS_SOURCE = 1 << 2, /* Since: 3.32 */ + GS_APP_QUIRK_IS_WILDCARD = 1 << 3, /* Since: 3.32 */ + GS_APP_QUIRK_NEEDS_REBOOT = 1 << 4, /* Since: 3.32 */ + GS_APP_QUIRK_NOT_REVIEWABLE = 1 << 5, /* Since: 3.32 */ + GS_APP_QUIRK_HAS_SHORTCUT = 1 << 6, /* Since: 3.32 */ + GS_APP_QUIRK_NOT_LAUNCHABLE = 1 << 7, /* Since: 3.32 */ + GS_APP_QUIRK_NEEDS_USER_ACTION = 1 << 8, /* Since: 3.32 */ + GS_APP_QUIRK_IS_PROXY = 1 << 9, /* Since: 3.32 */ + GS_APP_QUIRK_REMOVABLE_HARDWARE = 1 << 10, /* Since: 3.32 */ + GS_APP_QUIRK_DEVELOPER_VERIFIED = 1 << 11, /* Since: 3.32 */ + GS_APP_QUIRK_PARENTAL_FILTER = 1 << 12, /* Since: 3.32 */ + GS_APP_QUIRK_NEW_PERMISSIONS = 1 << 13, /* Since: 3.32 */ + GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE = 1 << 14, /* Since: 3.32 */ + GS_APP_QUIRK_HIDE_FROM_SEARCH = 1 << 15, /* Since: 3.32 */ + GS_APP_QUIRK_HIDE_EVERYWHERE = 1 << 16, /* Since: 3.36 */ + GS_APP_QUIRK_DO_NOT_AUTO_UPDATE = 1 << 17, /* Since: 3.36 */ + /*< private >*/ + GS_APP_QUIRK_LAST +} GsAppQuirk; + +#define GS_APP_INSTALL_DATE_UNSET 0 +#define GS_APP_INSTALL_DATE_UNKNOWN 1 /* 1s past the epoch */ +#define GS_APP_SIZE_UNKNOWABLE G_MAXUINT64 + +/** + * GsAppQuality: + * @GS_APP_QUALITY_UNKNOWN: The quality value is unknown + * @GS_APP_QUALITY_LOWEST: Lowest quality + * @GS_APP_QUALITY_NORMAL: Normal quality + * @GS_APP_QUALITY_HIGHEST: Highest quality + * + * Any awards given to the application. + **/ +typedef enum { + GS_APP_QUALITY_UNKNOWN, + GS_APP_QUALITY_LOWEST, + GS_APP_QUALITY_NORMAL, + GS_APP_QUALITY_HIGHEST, + /*< private >*/ + GS_APP_QUALITY_LAST +} GsAppQuality; + +typedef enum { + GS_APP_PERMISSIONS_UNKNOWN = 0, + GS_APP_PERMISSIONS_NONE = 1 << 0, + GS_APP_PERMISSIONS_NETWORK = 1 << 1, + GS_APP_PERMISSIONS_SYSTEM_BUS = 1 << 2, + GS_APP_PERMISSIONS_SESSION_BUS = 1 << 3, + GS_APP_PERMISSIONS_DEVICES = 1 << 4, + GS_APP_PERMISSIONS_HOME_FULL = 1 << 5, + GS_APP_PERMISSIONS_HOME_READ = 1 << 6, + GS_APP_PERMISSIONS_FILESYSTEM_FULL = 1 << 7, + GS_APP_PERMISSIONS_FILESYSTEM_READ = 1 << 8, + GS_APP_PERMISSIONS_DOWNLOADS_FULL = 1 << 9, + GS_APP_PERMISSIONS_DOWNLOADS_READ = 1 << 10, + GS_APP_PERMISSIONS_SETTINGS = 1 << 11, + GS_APP_PERMISSIONS_X11 = 1 << 12, + GS_APP_PERMISSIONS_ESCAPE_SANDBOX = 1 << 13, + /*< private >*/ + GS_APP_PERMISSIONS_LAST +} GsAppPermissions; + +#define LIMITED_PERMISSIONS (GS_APP_PERMISSIONS_SETTINGS | \ + GS_APP_PERMISSIONS_NETWORK | \ + GS_APP_PERMISSIONS_DOWNLOADS_READ | \ + GS_APP_PERMISSIONS_DOWNLOADS_FULL) +#define MEDIUM_PERMISSIONS (LIMITED_PERMISSIONS | \ + GS_APP_PERMISSIONS_X11) + +/** + * GS_APP_PROGRESS_UNKNOWN: + * + * A value returned by gs_app_get_progress() if the app’s progress is unknown + * or has a wide confidence interval. Typically this would be represented in the + * UI using a pulsing progress bar or spinner. + * + * Since: 3.38 + */ +#define GS_APP_PROGRESS_UNKNOWN G_MAXUINT + +GsApp *gs_app_new (const gchar *id); +G_DEPRECATED_FOR(gs_app_set_from_unique_id) +GsApp *gs_app_new_from_unique_id (const gchar *unique_id); +void gs_app_set_from_unique_id (GsApp *app, + const gchar *unique_id); +gchar *gs_app_to_string (GsApp *app); +void gs_app_to_string_append (GsApp *app, + GString *str); + +const gchar *gs_app_get_id (GsApp *app); +void gs_app_set_id (GsApp *app, + const gchar *id); +AsAppKind gs_app_get_kind (GsApp *app); +void gs_app_set_kind (GsApp *app, + AsAppKind kind); +AsAppState gs_app_get_state (GsApp *app); +void gs_app_set_state (GsApp *app, + AsAppState state); +AsAppScope gs_app_get_scope (GsApp *app); +void gs_app_set_scope (GsApp *app, + AsAppScope scope); +AsBundleKind gs_app_get_bundle_kind (GsApp *app); +void gs_app_set_bundle_kind (GsApp *app, + AsBundleKind bundle_kind); +void gs_app_set_state_recover (GsApp *app); +guint gs_app_get_progress (GsApp *app); +void gs_app_set_progress (GsApp *app, + guint percentage); +gboolean gs_app_get_allow_cancel (GsApp *app); +void gs_app_set_allow_cancel (GsApp *app, + gboolean allow_cancel); +const gchar *gs_app_get_unique_id (GsApp *app); +const gchar *gs_app_get_branch (GsApp *app); +void gs_app_set_branch (GsApp *app, + const gchar *branch); +const gchar *gs_app_get_name (GsApp *app); +void gs_app_set_name (GsApp *app, + GsAppQuality quality, + const gchar *name); +const gchar *gs_app_get_source_default (GsApp *app); +void gs_app_add_source (GsApp *app, + const gchar *source); +GPtrArray *gs_app_get_sources (GsApp *app); +void gs_app_set_sources (GsApp *app, + GPtrArray *sources); +const gchar *gs_app_get_source_id_default (GsApp *app); +void gs_app_add_source_id (GsApp *app, + const gchar *source_id); +GPtrArray *gs_app_get_source_ids (GsApp *app); +void gs_app_set_source_ids (GsApp *app, + GPtrArray *source_ids); +void gs_app_clear_source_ids (GsApp *app); +const gchar *gs_app_get_project_group (GsApp *app); +void gs_app_set_project_group (GsApp *app, + const gchar *project_group); +const gchar *gs_app_get_developer_name (GsApp *app); +void gs_app_set_developer_name (GsApp *app, + const gchar *developer_name); +const gchar *gs_app_get_agreement (GsApp *app); +void gs_app_set_agreement (GsApp *app, + const gchar *agreement); +const gchar *gs_app_get_version (GsApp *app); +const gchar *gs_app_get_version_ui (GsApp *app); +void gs_app_set_version (GsApp *app, + const gchar *version); +const gchar *gs_app_get_summary (GsApp *app); +void gs_app_set_summary (GsApp *app, + GsAppQuality quality, + const gchar *summary); +const gchar *gs_app_get_summary_missing (GsApp *app); +void gs_app_set_summary_missing (GsApp *app, + const gchar *summary_missing); +const gchar *gs_app_get_description (GsApp *app); +void gs_app_set_description (GsApp *app, + GsAppQuality quality, + const gchar *description); +const gchar *gs_app_get_url (GsApp *app, + AsUrlKind kind); +void gs_app_set_url (GsApp *app, + AsUrlKind kind, + const gchar *url); +const gchar *gs_app_get_launchable (GsApp *app, + AsLaunchableKind kind); +void gs_app_set_launchable (GsApp *app, + AsLaunchableKind kind, + const gchar *launchable); +const gchar *gs_app_get_license (GsApp *app); +gboolean gs_app_get_license_is_free (GsApp *app); +void gs_app_set_license (GsApp *app, + GsAppQuality quality, + const gchar *license); +gchar **gs_app_get_menu_path (GsApp *app); +void gs_app_set_menu_path (GsApp *app, + gchar **menu_path); +const gchar *gs_app_get_origin (GsApp *app); +void gs_app_set_origin (GsApp *app, + const gchar *origin); +const gchar *gs_app_get_origin_appstream (GsApp *app); +void gs_app_set_origin_appstream (GsApp *app, + const gchar *origin_appstream); +const gchar *gs_app_get_origin_hostname (GsApp *app); +void gs_app_set_origin_hostname (GsApp *app, + const gchar *origin_hostname); +GPtrArray *gs_app_get_screenshots (GsApp *app); +void gs_app_add_screenshot (GsApp *app, + AsScreenshot *screenshot); +AsScreenshot *gs_app_get_action_screenshot (GsApp *app); +void gs_app_set_action_screenshot (GsApp *app, + AsScreenshot *screenshot); +const gchar *gs_app_get_update_version (GsApp *app); +const gchar *gs_app_get_update_version_ui (GsApp *app); +void gs_app_set_update_version (GsApp *app, + const gchar *update_version); +const gchar *gs_app_get_update_details (GsApp *app); +void gs_app_set_update_details (GsApp *app, + const gchar *update_details); +AsUrgencyKind gs_app_get_update_urgency (GsApp *app); +void gs_app_set_update_urgency (GsApp *app, + AsUrgencyKind update_urgency); +const gchar *gs_app_get_management_plugin (GsApp *app); +void gs_app_set_management_plugin (GsApp *app, + const gchar *management_plugin); +GdkPixbuf *gs_app_get_pixbuf (GsApp *app); +void gs_app_set_pixbuf (GsApp *app, + GdkPixbuf *pixbuf); +GPtrArray *gs_app_get_icons (GsApp *app); +void gs_app_add_icon (GsApp *app, + AsIcon *icon); +gboolean gs_app_get_use_drop_shadow (GsApp *app); +GFile *gs_app_get_local_file (GsApp *app); +void gs_app_set_local_file (GsApp *app, + GFile *local_file); +AsContentRating *gs_app_get_content_rating (GsApp *app); +void gs_app_set_content_rating (GsApp *app, + AsContentRating *content_rating); +GsApp *gs_app_get_runtime (GsApp *app); +void gs_app_set_runtime (GsApp *app, + GsApp *runtime); +const gchar *gs_app_get_metadata_item (GsApp *app, + const gchar *key); +GVariant *gs_app_get_metadata_variant (GsApp *app, + const gchar *key); +void gs_app_set_metadata (GsApp *app, + const gchar *key, + const gchar *value); +void gs_app_set_metadata_variant (GsApp *app, + const gchar *key, + GVariant *value); +gint gs_app_get_rating (GsApp *app); +void gs_app_set_rating (GsApp *app, + gint rating); +GArray *gs_app_get_review_ratings (GsApp *app); +void gs_app_set_review_ratings (GsApp *app, + GArray *review_ratings); +GPtrArray *gs_app_get_reviews (GsApp *app); +void gs_app_add_review (GsApp *app, + AsReview *review); +void gs_app_remove_review (GsApp *app, + AsReview *review); +GPtrArray *gs_app_get_provides (GsApp *app); +void gs_app_add_provide (GsApp *app, + AsProvide *provide); +guint64 gs_app_get_size_installed (GsApp *app); +void gs_app_set_size_installed (GsApp *app, + guint64 size_installed); +guint64 gs_app_get_size_download (GsApp *app); +void gs_app_set_size_download (GsApp *app, + guint64 size_download); +void gs_app_add_related (GsApp *app, + GsApp *app2); +void gs_app_add_addon (GsApp *app, + GsApp *addon); +void gs_app_add_history (GsApp *app, + GsApp *app2); +guint64 gs_app_get_install_date (GsApp *app); +void gs_app_set_install_date (GsApp *app, + guint64 install_date); +GPtrArray *gs_app_get_categories (GsApp *app); +void gs_app_set_categories (GsApp *app, + GPtrArray *categories); +GPtrArray *gs_app_get_key_colors (GsApp *app); +void gs_app_set_key_colors (GsApp *app, + GPtrArray *key_colors); +void gs_app_add_key_color (GsApp *app, + GdkRGBA *key_color); +void gs_app_set_is_update_downloaded (GsApp *app, + gboolean is_update_downloaded); +gboolean gs_app_get_is_update_downloaded (GsApp *app); +gboolean gs_app_has_category (GsApp *app, + const gchar *category); +void gs_app_add_category (GsApp *app, + const gchar *category); +gboolean gs_app_remove_category (GsApp *app, + const gchar *category); +void gs_app_add_kudo (GsApp *app, + GsAppKudo kudo); +void gs_app_remove_kudo (GsApp *app, + GsAppKudo kudo); +gboolean gs_app_has_kudo (GsApp *app, + GsAppKudo kudo); +guint64 gs_app_get_kudos (GsApp *app); +guint gs_app_get_kudos_percentage (GsApp *app); +gboolean gs_app_get_to_be_installed (GsApp *app); +void gs_app_set_to_be_installed (GsApp *app, + gboolean to_be_installed); +void gs_app_set_match_value (GsApp *app, + guint match_value); +guint gs_app_get_match_value (GsApp *app); + +gboolean gs_app_has_quirk (GsApp *app, + GsAppQuirk quirk); +void gs_app_add_quirk (GsApp *app, + GsAppQuirk quirk); +void gs_app_remove_quirk (GsApp *app, + GsAppQuirk quirk); +gboolean gs_app_is_installed (GsApp *app); +gboolean gs_app_is_updatable (GsApp *app); +gchar *gs_app_get_origin_ui (GsApp *app); +gchar *gs_app_get_packaging_format (GsApp *app); +void gs_app_subsume_metadata (GsApp *app, + GsApp *donor); +GsAppPermissions gs_app_get_permissions (GsApp *app); +void gs_app_set_permissions (GsApp *app, + GsAppPermissions permissions); +GsAppPermissions gs_app_get_update_permissions (GsApp *app); +void gs_app_set_update_permissions (GsApp *app, + GsAppPermissions update_permissions); + +G_END_DECLS diff --git a/lib/gs-autocleanups.h b/lib/gs-autocleanups.h new file mode 100644 index 0000000..aaba9e7 --- /dev/null +++ b/lib/gs-autocleanups.h @@ -0,0 +1,52 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +G_BEGIN_DECLS + +#if !GLIB_CHECK_VERSION(2, 61, 1) + +/* Backported GRWLock autoptr support for older glib versions */ + +typedef void GRWLockWriterLocker; + +static inline GRWLockWriterLocker * +g_rw_lock_writer_locker_new (GRWLock *rw_lock) +{ + g_rw_lock_writer_lock (rw_lock); + return (GRWLockWriterLocker *) rw_lock; +} + +static inline void +g_rw_lock_writer_locker_free (GRWLockWriterLocker *locker) +{ + g_rw_lock_writer_unlock ((GRWLock *) locker); +} + +typedef void GRWLockReaderLocker; + +static inline GRWLockReaderLocker * +g_rw_lock_reader_locker_new (GRWLock *rw_lock) +{ + g_rw_lock_reader_lock (rw_lock); + return (GRWLockReaderLocker *) rw_lock; +} + +static inline void +g_rw_lock_reader_locker_free (GRWLockReaderLocker *locker) +{ + g_rw_lock_reader_unlock ((GRWLock *) locker); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRWLockWriterLocker, g_rw_lock_writer_locker_free) +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRWLockReaderLocker, g_rw_lock_reader_locker_free) + +#endif + +G_END_DECLS diff --git a/lib/gs-category-private.h b/lib/gs-category-private.h new file mode 100644 index 0000000..1c9c292 --- /dev/null +++ b/lib/gs-category-private.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-category.h" + +G_BEGIN_DECLS + +void gs_category_sort_children (GsCategory *category); +void gs_category_set_size (GsCategory *category, + guint size); +gchar *gs_category_to_string (GsCategory *category); + +G_END_DECLS diff --git a/lib/gs-category.c b/lib/gs-category.c new file mode 100644 index 0000000..9da2c73 --- /dev/null +++ b/lib/gs-category.c @@ -0,0 +1,527 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-category + * @short_description: An category that contains applications + * + * This object provides functionality that allows a plugin to create + * a tree structure of categories that each contain #GsApp's. + */ + +#include "config.h" + +#include <glib/gi18n.h> + +#include "gs-category-private.h" + +struct _GsCategory +{ + GObject parent_instance; + + gchar *id; + gchar *name; + gchar *icon; + gint score; + GPtrArray *desktop_groups; + GsCategory *parent; + guint size; + GPtrArray *children; +}; + +G_DEFINE_TYPE (GsCategory, gs_category, G_TYPE_OBJECT) + +/** + * gs_category_to_string: + * @category: a #GsCategory + * + * Returns a string representation of the category + * + * Returns: a string + * + * Since: 3.22 + **/ +gchar * +gs_category_to_string (GsCategory *category) +{ + guint i; + GString *str = g_string_new (NULL); + g_string_append_printf (str, "GsCategory[%p]:\n", category); + g_string_append_printf (str, " id: %s\n", + category->id); + if (category->name != NULL) { + g_string_append_printf (str, " name: %s\n", + category->name); + } + if (category->icon != NULL) { + g_string_append_printf (str, " icon: %s\n", + category->icon); + } + g_string_append_printf (str, " size: %u\n", + category->size); + g_string_append_printf (str, " desktop-groups: %u\n", + category->desktop_groups->len); + if (category->parent != NULL) { + g_string_append_printf (str, " parent: %s\n", + gs_category_get_id (category->parent)); + } + g_string_append_printf (str, " score: %i\n", category->score); + if (category->children->len == 0) { + g_string_append_printf (str, " children: %u\n", + category->children->len); + } else { + g_string_append (str, " children:\n"); + for (i = 0; i < category->children->len; i++) { + GsCategory *child = g_ptr_array_index (category->children, i); + g_string_append_printf (str, " - %s\n", + gs_category_get_id (child)); + } + } + return g_string_free (str, FALSE); +} + +/** + * gs_category_get_size: + * @category: a #GsCategory + * + * Returns how many applications the category could contain. + * + * NOTE: This may over-estimate the number if duplicate applications are + * filtered or core applications are not shown. + * + * Returns: the number of apps in the category + * + * Since: 3.22 + **/ +guint +gs_category_get_size (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), 0); + return category->size; +} + +/** + * gs_category_set_size: + * @category: a #GsCategory + * @size: the number of applications + * + * Sets the number of applications in the category. + * Most plugins do not need to call this function. + * + * Since: 3.22 + **/ +void +gs_category_set_size (GsCategory *category, guint size) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + category->size = size; +} + +/** + * gs_category_increment_size: + * @category: a #GsCategory + * + * Adds one to the size count if an application is available + * + * Since: 3.22 + **/ +void +gs_category_increment_size (GsCategory *category) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + category->size++; +} + +/** + * gs_category_get_id: + * @category: a #GsCategory + * + * Gets the category ID. + * + * Returns: the string, e.g. "other" + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_id (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + return category->id; +} + +/** + * gs_category_get_name: + * @category: a #GsCategory + * + * Gets the category name. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_name (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + /* special case, we don't want translations in the plugins */ + if (g_strcmp0 (category->id, "other") == 0) { + /* TRANSLATORS: this is where all applications that don't + * fit in other groups are put */ + return _("Other"); + } + if (g_strcmp0 (category->id, "all") == 0) { + /* TRANSLATORS: this is a subcategory matching all the + * different apps in the parent category, e.g. "Games" */ + return _("All"); + } + if (g_strcmp0 (category->id, "featured") == 0) { + /* TRANSLATORS: this is a subcategory of featured apps */ + return _("Featured"); + } + + return category->name; +} + +/** + * gs_category_set_name: + * @category: a #GsCategory + * @name: a category name, or %NULL + * + * Sets the category name. + * + * Since: 3.22 + **/ +void +gs_category_set_name (GsCategory *category, const gchar *name) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_free (category->name); + category->name = g_strdup (name); +} + +/** + * gs_category_get_icon: + * @category: a #GsCategory + * + * Gets the category icon. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_category_get_icon (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + + /* special case */ + if (g_strcmp0 (category->id, "other") == 0) + return "emblem-system-symbolic"; + if (g_strcmp0 (category->id, "all") == 0) + return "emblem-default-symbolic"; + if (g_strcmp0 (category->id, "featured") == 0) + return "emblem-favorite-symbolic"; + + return category->icon; +} + +/** + * gs_category_set_icon: + * @category: a #GsCategory + * @icon: a category icon, or %NULL + * + * Sets the category icon. + * + * Since: 3.22 + **/ +void +gs_category_set_icon (GsCategory *category, const gchar *icon) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_free (category->icon); + category->icon = g_strdup (icon); +} + +/** + * gs_category_get_score: + * @category: a #GsCategory + * + * Gets if the category score. + * Important categories may be shown before other categories, or tagged in a + * different way, for example with color or in a different section. + * + * Returns: the string, or %NULL + * + * Since: 3.22 + **/ +gint +gs_category_get_score (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + return category->score; +} + +/** + * gs_category_set_score: + * @category: a #GsCategory + * @score: a category score, or %NULL + * + * Sets the category score, where larger numbers get sorted before lower + * numbers. + * + * Since: 3.22 + **/ +void +gs_category_set_score (GsCategory *category, gint score) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + category->score = score; +} + +/** + * gs_category_get_desktop_groups: + * @category: a #GsCategory + * + * Gets the list of AppStream groups for the category. + * + * Returns: (element-type utf8) (transfer none): An array + * + * Since: 3.22 + **/ +GPtrArray * +gs_category_get_desktop_groups (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + return category->desktop_groups; +} + +/** + * gs_category_has_desktop_group: + * @category: a #GsCategory + * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player" + * + * Finds out if the category has the specific AppStream desktop group. + * + * Returns: %TRUE if found, %FALSE otherwise + * + * Since: 3.22 + **/ +gboolean +gs_category_has_desktop_group (GsCategory *category, const gchar *desktop_group) +{ + guint i; + + g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE); + g_return_val_if_fail (desktop_group != NULL, FALSE); + + for (i = 0; i < category->desktop_groups->len; i++) { + const gchar *tmp = g_ptr_array_index (category->desktop_groups, i); + if (g_strcmp0 (tmp, desktop_group) == 0) + return TRUE; + } + return FALSE; +} + +/** + * gs_category_add_desktop_group: + * @category: a #GsCategory + * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player" + * + * Adds a desktop group to the category. + * A desktop group is a set of category strings that all must exist. + * + * Since: 3.22 + **/ +void +gs_category_add_desktop_group (GsCategory *category, const gchar *desktop_group) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_return_if_fail (desktop_group != NULL); + + /* add if not already found */ + if (gs_category_has_desktop_group (category, desktop_group)) + return; + g_ptr_array_add (category->desktop_groups, g_strdup (desktop_group)); +} + +/** + * gs_category_find_child: + * @category: a #GsCategory + * @id: a category ID, e.g. "other" + * + * Find a child category with a specific ID. + * + * Returns: (transfer none): the #GsCategory, or %NULL + * + * Since: 3.22 + **/ +GsCategory * +gs_category_find_child (GsCategory *category, const gchar *id) +{ + GsCategory *tmp; + guint i; + + /* find the subcategory */ + for (i = 0; i < category->children->len; i++) { + tmp = GS_CATEGORY (g_ptr_array_index (category->children, i)); + if (g_strcmp0 (id, gs_category_get_id (tmp)) == 0) + return tmp; + } + return NULL; +} + +/** + * gs_category_get_parent: + * @category: a #GsCategory + * + * Gets the parent category. + * + * Returns: the #GsCategory or %NULL + * + * Since: 3.22 + **/ +GsCategory * +gs_category_get_parent (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + return category->parent; +} + +/** + * gs_category_get_children: + * @category: a #GsCategory + * + * Gets the list if children for a category. + * + * Return value: (element-type GsApp) (transfer none): A list of children + * + * Since: 3.22 + **/ +GPtrArray * +gs_category_get_children (GsCategory *category) +{ + g_return_val_if_fail (GS_IS_CATEGORY (category), NULL); + return category->children; +} + +/** + * gs_category_add_child: + * @category: a #GsCategory + * @subcategory: a #GsCategory + * + * Adds a child category to a parent category. + * + * Since: 3.22 + **/ +void +gs_category_add_child (GsCategory *category, GsCategory *subcategory) +{ + g_return_if_fail (GS_IS_CATEGORY (category)); + g_return_if_fail (GS_IS_CATEGORY (subcategory)); + + /* FIXME: do we need this? */ + subcategory->parent = category; + g_object_add_weak_pointer (G_OBJECT (subcategory->parent), + (gpointer *) &subcategory->parent); + + g_ptr_array_add (category->children, + g_object_ref (subcategory)); +} + +static gchar * +gs_category_get_sort_key (GsCategory *category) +{ + guint sort_order = 5; + if (g_strcmp0 (gs_category_get_id (category), "featured") == 0) + sort_order = 0; + else if (g_strcmp0 (gs_category_get_id (category), "all") == 0) + sort_order = 2; + else if (g_strcmp0 (gs_category_get_id (category), "other") == 0) + sort_order = 9; + return g_strdup_printf ("%u:%s", + sort_order, + gs_category_get_name (category)); +} + +static gint +gs_category_sort_children_cb (gconstpointer a, gconstpointer b) +{ + GsCategory *ca = GS_CATEGORY (*(GsCategory **) a); + GsCategory *cb = GS_CATEGORY (*(GsCategory **) b); + g_autofree gchar *id_a = gs_category_get_sort_key (ca); + g_autofree gchar *id_b = gs_category_get_sort_key (cb); + return g_strcmp0 (id_a, id_b); +} + +/** + * gs_category_sort_children: + * @category: a #GsCategory + * + * Sorts the list of children. + * + * Since: 3.22 + **/ +void +gs_category_sort_children (GsCategory *category) +{ + g_ptr_array_sort (category->children, + gs_category_sort_children_cb); +} + +static void +gs_category_finalize (GObject *object) +{ + GsCategory *category = GS_CATEGORY (object); + + if (category->parent != NULL) + g_object_remove_weak_pointer (G_OBJECT (category->parent), + (gpointer *) &category->parent); + g_ptr_array_unref (category->children); + g_ptr_array_unref (category->desktop_groups); + g_free (category->id); + g_free (category->name); + g_free (category->icon); + + G_OBJECT_CLASS (gs_category_parent_class)->finalize (object); +} + +static void +gs_category_class_init (GsCategoryClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_category_finalize; +} + +static void +gs_category_init (GsCategory *category) +{ + category->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + category->desktop_groups = g_ptr_array_new_with_free_func (g_free); +} + +/** + * gs_category_new: + * @id: an ID, e.g. "all" + * + * Creates a new category object. + * + * Returns: the new #GsCategory + * + * Since: 3.22 + **/ +GsCategory * +gs_category_new (const gchar *id) +{ + GsCategory *category; + category = g_object_new (GS_TYPE_CATEGORY, NULL); + category->id = g_strdup (id); + return GS_CATEGORY (category); +} diff --git a/lib/gs-category.h b/lib/gs-category.h new file mode 100644 index 0000000..e60bef2 --- /dev/null +++ b/lib/gs-category.h @@ -0,0 +1,51 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com> + * Copyright (C) 2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gdk/gdk.h> + +G_BEGIN_DECLS + +#define GS_TYPE_CATEGORY (gs_category_get_type ()) + +G_DECLARE_FINAL_TYPE (GsCategory, gs_category, GS, CATEGORY, GObject) + +GsCategory *gs_category_new (const gchar *id); +const gchar *gs_category_get_id (GsCategory *category); +GsCategory *gs_category_get_parent (GsCategory *category); + +const gchar *gs_category_get_name (GsCategory *category); +void gs_category_set_name (GsCategory *category, + const gchar *name); +const gchar *gs_category_get_icon (GsCategory *category); +void gs_category_set_icon (GsCategory *category, + const gchar *icon); +gint gs_category_get_score (GsCategory *category); +void gs_category_set_score (GsCategory *category, + gint score); + +GPtrArray *gs_category_get_desktop_groups (GsCategory *category); +gboolean gs_category_has_desktop_group (GsCategory *category, + const gchar *desktop_group); +void gs_category_add_desktop_group (GsCategory *category, + const gchar *desktop_group); + +GsCategory *gs_category_find_child (GsCategory *category, + const gchar *id); +GPtrArray *gs_category_get_children (GsCategory *category); +void gs_category_add_child (GsCategory *category, + GsCategory *subcategory); + +guint gs_category_get_size (GsCategory *category); +void gs_category_increment_size (GsCategory *category); + +G_END_DECLS diff --git a/lib/gs-cmd.c b/lib/gs-cmd.c new file mode 100644 index 0000000..bc01c95 --- /dev/null +++ b/lib/gs-cmd.c @@ -0,0 +1,699 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib/gi18n.h> +#include <gtk/gtk.h> +#include <locale.h> + +#include "gnome-software-private.h" + +#include "gs-debug.h" + +typedef struct { + GsPluginLoader *plugin_loader; + guint64 refine_flags; + guint max_results; +} GsCmdSelf; + +static void +gs_cmd_show_results_apps (GsAppList *list) +{ + for (guint j = 0; j < gs_app_list_length (list); j++) { + GsApp *app = gs_app_list_index (list, j); + GsAppList *related = gs_app_get_related (app); + g_autofree gchar *tmp = gs_app_to_string (app); + g_print ("%s\n", tmp); + for (guint i = 0; i < gs_app_list_length (related); i++) { + g_autofree gchar *tmp_rel = NULL; + GsApp *app_rel = GS_APP (gs_app_list_index (related, i)); + tmp_rel = gs_app_to_string (app_rel); + g_print ("\t%s\n", tmp_rel); + } + } +} + +static gchar * +gs_cmd_pad_spaces (const gchar *text, guint length) +{ + gsize i; + GString *str; + str = g_string_sized_new (length + 1); + g_string_append (str, text); + for (i = strlen (text); i < length; i++) + g_string_append_c (str, ' '); + return g_string_free (str, FALSE); +} + +static void +gs_cmd_show_results_categories (GPtrArray *list) +{ + for (guint i = 0; i < list->len; i++) { + GsCategory *cat = GS_CATEGORY (g_ptr_array_index (list, i)); + GsCategory *parent = gs_category_get_parent (cat); + g_autofree gchar *tmp = NULL; + if (parent != NULL){ + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s/%s [%u]", + gs_category_get_id (parent), + gs_category_get_id (cat), + gs_category_get_size (cat)); + tmp = gs_cmd_pad_spaces (id, 32); + g_print ("%s : %s\n", + tmp, gs_category_get_name (cat)); + } else { + GPtrArray *subcats = gs_category_get_children (cat); + tmp = gs_cmd_pad_spaces (gs_category_get_id (cat), 32); + g_print ("%s : %s\n", + tmp, gs_category_get_name (cat)); + gs_cmd_show_results_categories (subcats); + } + } +} + +static GsPluginRefineFlags +gs_cmd_refine_flag_from_string (const gchar *flag, GError **error) +{ + if (g_strcmp0 (flag, "all") == 0) + return G_MAXINT32; + if (g_strcmp0 (flag, "license") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE; + if (g_strcmp0 (flag, "url") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL; + if (g_strcmp0 (flag, "description") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION; + if (g_strcmp0 (flag, "size") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE; + if (g_strcmp0 (flag, "rating") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING; + if (g_strcmp0 (flag, "version") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION; + if (g_strcmp0 (flag, "history") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY; + if (g_strcmp0 (flag, "setup-action") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION; + if (g_strcmp0 (flag, "update-details") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS; + if (g_strcmp0 (flag, "origin") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN; + if (g_strcmp0 (flag, "related") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED; + if (g_strcmp0 (flag, "menu-path") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH; + if (g_strcmp0 (flag, "upgrade-removed") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED; + if (g_strcmp0 (flag, "provenance") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE; + if (g_strcmp0 (flag, "reviews") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS; + if (g_strcmp0 (flag, "review-ratings") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS; + if (g_strcmp0 (flag, "key-colors") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS; + if (g_strcmp0 (flag, "icon") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON; + if (g_strcmp0 (flag, "permissions") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS; + if (g_strcmp0 (flag, "origin-hostname") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME; + if (g_strcmp0 (flag, "origin-ui") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI; + if (g_strcmp0 (flag, "runtime") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME; + if (g_strcmp0 (flag, "categories") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES; + if (g_strcmp0 (flag, "project-group") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP; + if (g_strcmp0 (flag, "developer-name") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME; + if (g_strcmp0 (flag, "kudos") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS; + if (g_strcmp0 (flag, "content-rating") == 0) + return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING; + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "GsPluginRefineFlag '%s' not recognised", flag); + return 0; +} + +static guint64 +gs_cmd_parse_refine_flags (const gchar *extra, GError **error) +{ + GsPluginRefineFlags tmp; + guint i; + guint64 refine_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT; + g_auto(GStrv) split = NULL; + + if (extra == NULL) + return GS_PLUGIN_REFINE_FLAGS_DEFAULT; + + split = g_strsplit (extra, ",", -1); + for (i = 0; split[i] != NULL; i++) { + tmp = gs_cmd_refine_flag_from_string (split[i], error); + if (tmp == 0) + return G_MAXUINT64; + refine_flags |= tmp; + } + return refine_flags; +} + +static guint +gs_cmd_prompt_for_number (guint maxnum) +{ + gint retval; + guint answer = 0; + + do { + char buffer[64]; + + /* swallow the \n at end of line too */ + if (!fgets (buffer, sizeof (buffer), stdin)) + break; + if (strlen (buffer) == sizeof (buffer) - 1) + continue; + + /* get a number */ + retval = sscanf (buffer, "%u", &answer); + + /* positive */ + if (retval == 1 && answer > 0 && answer <= maxnum) + break; + + /* TRANSLATORS: the user isn't reading the question */ + g_print (_("Please enter a number from 1 to %u: "), maxnum); + } while (TRUE); + return answer; +} + +static gboolean +gs_cmd_action_exec (GsCmdSelf *self, GsPluginAction action, const gchar *name, GError **error) +{ + g_autoptr(GsApp) app = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GsAppList) list_filtered = NULL; + g_autoptr(GsPluginJob) plugin_job2 = NULL; + g_autoptr(GsPluginJob) plugin_job = NULL; + gboolean show_installed = TRUE; + + /* ensure set */ + self->refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON; + self->refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION; + + /* do search */ + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", name, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, error); + if (list == NULL) + return FALSE; + if (gs_app_list_length (list) == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "no components matched '%s'", + name); + return FALSE; + } + + /* filter */ + if (action == GS_PLUGIN_ACTION_INSTALL) + show_installed = FALSE; + list_filtered = gs_app_list_new (); + for (guint i = 0; i < gs_app_list_length (list); i++) { + GsApp *app_tmp = gs_app_list_index (list, i); + if (gs_app_is_installed (app_tmp) == show_installed) + gs_app_list_add (list_filtered, app_tmp); + } + + /* nothing */ + if (gs_app_list_length (list_filtered) == 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "no components were in the correct state for '%s %s'", + gs_plugin_action_to_string (action), name); + return FALSE; + } + + /* get one GsApp */ + if (gs_app_list_length (list_filtered) == 1) { + app = g_object_ref (gs_app_list_index (list_filtered, 0)); + } else { + guint idx; + /* TRANSLATORS: asking the user to choose an app from a list */ + g_print ("%s\n", _("Choose an application:")); + for (guint i = 0; i < gs_app_list_length (list_filtered); i++) { + GsApp *app_tmp = gs_app_list_index (list_filtered, i); + g_print ("%u.\t%s\n", + i + 1, + gs_app_get_unique_id (app_tmp)); + } + idx = gs_cmd_prompt_for_number (gs_app_list_length (list_filtered)); + app = g_object_ref (gs_app_list_index (list_filtered, idx - 1)); + } + + /* install */ + plugin_job2 = gs_plugin_job_newv (action, "app", app, NULL); + return gs_plugin_loader_job_action (self->plugin_loader, plugin_job2, + NULL, error); +} + +static void +gs_cmd_self_free (GsCmdSelf *self) +{ + if (self->plugin_loader != NULL) + g_object_unref (self->plugin_loader); + g_free (self); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsCmdSelf, gs_cmd_self_free) + +int +main (int argc, char **argv) +{ + g_autoptr(GOptionContext) context = NULL; + gboolean prefer_local = FALSE; + gboolean ret; + gboolean show_results = FALSE; + gboolean verbose = FALSE; + gint i; + guint cache_age = 0; + gint repeat = 1; + g_auto(GStrv) plugin_blocklist = NULL; + g_auto(GStrv) plugin_allowlist = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsAppList) list = NULL; + g_autoptr(GPtrArray) categories = NULL; + g_autoptr(GsDebug) debug = gs_debug_new (); + g_autofree gchar *plugin_blocklist_str = NULL; + g_autofree gchar *plugin_allowlist_str = NULL; + g_autofree gchar *refine_flags_str = NULL; + g_autoptr(GsApp) app = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GsCmdSelf) self = g_new0 (GsCmdSelf, 1); + const GOptionEntry options[] = { + { "show-results", '\0', 0, G_OPTION_ARG_NONE, &show_results, + "Show the results for the action", NULL }, + { "refine-flags", '\0', 0, G_OPTION_ARG_STRING, &refine_flags_str, + "Set any refine flags required for the action", NULL }, + { "repeat", '\0', 0, G_OPTION_ARG_INT, &repeat, + "Repeat the action this number of times", NULL }, + { "cache-age", '\0', 0, G_OPTION_ARG_INT, &cache_age, + "Use this maximum cache age in seconds", NULL }, + { "max-results", '\0', 0, G_OPTION_ARG_INT, &self->max_results, + "Return a maximum number of results", NULL }, + { "prefer-local", '\0', 0, G_OPTION_ARG_NONE, &prefer_local, + "Prefer local file sources to AppStream", NULL }, + { "plugin-blocklist", '\0', 0, G_OPTION_ARG_STRING, &plugin_blocklist_str, + "Do not load specific plugins", NULL }, + { "plugin-allowlist", '\0', 0, G_OPTION_ARG_STRING, &plugin_allowlist_str, + "Only load specific plugins", NULL }, + { "verbose", '\0', 0, G_OPTION_ARG_NONE, &verbose, + "Show verbose debugging information", NULL }, + { NULL} + }; + + setlocale (LC_ALL, ""); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + gtk_init (&argc, &argv); + + context = g_option_context_new (NULL); + g_option_context_set_summary (context, "GNOME Software Test Program"); + g_option_context_add_main_entries (context, options, NULL); + g_option_context_add_group (context, gtk_get_option_group (TRUE)); + ret = g_option_context_parse (context, &argc, &argv, &error); + if (!ret) { + g_print ("Failed to parse options: %s\n", error->message); + return EXIT_FAILURE; + } + if (verbose) + g_setenv ("GS_DEBUG", "1", TRUE); + + /* prefer local sources */ + if (prefer_local) + g_setenv ("GNOME_SOFTWARE_PREFER_LOCAL", "true", TRUE); + + /* parse any refine flags */ + self->refine_flags = gs_cmd_parse_refine_flags (refine_flags_str, &error); + if (self->refine_flags == G_MAXUINT64) { + g_print ("Flag unknown: %s\n", error->message); + return EXIT_FAILURE; + } + + /* load plugins */ + self->plugin_loader = gs_plugin_loader_new (); + if (g_file_test (LOCALPLUGINDIR, G_FILE_TEST_EXISTS)) + gs_plugin_loader_add_location (self->plugin_loader, LOCALPLUGINDIR); + if (plugin_allowlist_str != NULL) + plugin_allowlist = g_strsplit (plugin_allowlist_str, ",", -1); + if (plugin_blocklist_str != NULL) + plugin_blocklist = g_strsplit (plugin_blocklist_str, ",", -1); + ret = gs_plugin_loader_setup (self->plugin_loader, + plugin_allowlist, + plugin_blocklist, + NULL, + &error); + if (!ret) { + g_print ("Failed to setup plugins: %s\n", error->message); + return EXIT_FAILURE; + } + gs_plugin_loader_dump_state (self->plugin_loader); + + /* ensure that at least some metadata of any age is present, and also + * spin up the plugins enough as to prime caches */ + if (g_getenv ("GS_CMD_NO_INITIAL_REFRESH") == NULL) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) G_MAXUINT, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (!ret) { + g_print ("Failed to refresh plugins: %s\n", error->message); + return EXIT_FAILURE; + } + } + + /* do action */ + if (argc == 2 && g_strcmp0 (argv[1], "installed") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_INSTALLED, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 3 && g_strcmp0 (argv[1], "search") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH, + "search", argv[2], + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 3 && g_strcmp0 (argv[1], "get-alternates") == 0) { + app = gs_app_new (argv[2]); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_ALTERNATES, + "app", app, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 4 && g_strcmp0 (argv[1], "action") == 0) { + GsPluginAction action = gs_plugin_action_from_string (argv[2]); + if (action == GS_PLUGIN_ACTION_UNKNOWN) { + ret = FALSE; + g_set_error (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Did not recognise action '%s'", argv[2]); + } else { + ret = gs_cmd_action_exec (self, action, argv[3], &error); + } + } else if (argc == 3 && g_strcmp0 (argv[1], "action-upgrade-download") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + app = gs_app_new (argv[2]); + gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (ret) + gs_app_list_add (list, app); + } else if (argc == 3 && g_strcmp0 (argv[1], "refine") == 0) { + app = gs_app_new (argv[2]); + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, + "app", app, + "refine-flags", self->refine_flags, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (!ret) + break; + } + list = gs_app_list_new (); + gs_app_list_add (list, app); + } else if (argc == 3 && g_strcmp0 (argv[1], "launch") == 0) { + app = gs_app_new (argv[2]); + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH, + "app", app, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + if (!ret) + break; + } + } else if (argc == 3 && g_strcmp0 (argv[1], "filename-to-app") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + file = g_file_new_for_path (argv[2]); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP, + "file", file, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + app = gs_plugin_loader_job_process_app (self->plugin_loader, plugin_job, NULL, &error); + if (app == NULL) { + ret = FALSE; + } else { + list = gs_app_list_new (); + gs_app_list_add (list, app); + } + } else if (argc == 3 && g_strcmp0 (argv[1], "url-to-app") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP, + "search", argv[2], + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + app = gs_plugin_loader_job_process_app (self->plugin_loader, plugin_job, + NULL, &error); + if (app == NULL) { + ret = FALSE; + } else { + list = gs_app_list_new (); + gs_app_list_add (list, app); + } + } else if (argc == 2 && g_strcmp0 (argv[1], "updates") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "upgrades") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "sources") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, + plugin_job, + NULL, + &error); + if (list == NULL) + ret = FALSE; + } else if (argc == 2 && g_strcmp0 (argv[1], "popular") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_POPULAR, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "featured") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_FEATURED, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "recent") == 0) { + if (cache_age == 0) + cache_age = 60 * 60 * 24 * 60; + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_RECENT, + "age", (guint64) cache_age, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, + NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 2 && g_strcmp0 (argv[1], "get-categories") == 0) { + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (categories != NULL) + g_ptr_array_unref (categories); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORIES, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + categories = gs_plugin_loader_job_get_categories (self->plugin_loader, + plugin_job, + NULL, &error); + if (categories == NULL) { + ret = FALSE; + break; + } + } + } else if (argc == 3 && g_strcmp0 (argv[1], "get-category-apps") == 0) { + g_autoptr(GsCategory) category = NULL; + g_autoptr(GsCategory) parent = NULL; + g_auto(GStrv) split = NULL; + split = g_strsplit (argv[2], "/", 2); + if (g_strv_length (split) == 1) { + category = gs_category_new (split[0]); + } else { + parent = gs_category_new (split[0]); + category = gs_category_new (split[1]); + gs_category_add_child (parent, category); + } + for (i = 0; i < repeat; i++) { + g_autoptr(GsPluginJob) plugin_job = NULL; + if (list != NULL) + g_object_unref (list); + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORY_APPS, + "category", category, + "refine-flags", self->refine_flags, + "max-results", self->max_results, + NULL); + list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error); + if (list == NULL) { + ret = FALSE; + break; + } + } + } else if (argc >= 2 && g_strcmp0 (argv[1], "refresh") == 0) { + g_autoptr(GsPluginJob) plugin_job = NULL; + plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH, + "age", (guint64) cache_age, + NULL); + ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job, + NULL, &error); + } else if (argc >= 1 && g_strcmp0 (argv[1], "user-hash") == 0) { + g_autofree gchar *user_hash = gs_utils_get_user_hash (&error); + if (user_hash == NULL) { + ret = FALSE; + } else { + g_print ("%s\n", user_hash); + ret = TRUE; + } + } else { + ret = FALSE; + g_set_error_literal (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Did not recognise option, use 'installed', " + "'updates', 'popular', 'get-categories', " + "'get-category-apps', 'get-alternates', 'filename-to-app', " + "'action install', 'action remove', " + "'sources', 'refresh', 'launch' or 'search'"); + } + if (!ret) { + g_print ("Failed: %s\n", error->message); + return EXIT_FAILURE; + } + + if (show_results) { + if (list != NULL) + gs_cmd_show_results_apps (list); + if (categories != NULL) + gs_cmd_show_results_categories (categories); + } + return EXIT_SUCCESS; +} diff --git a/lib/gs-debug.c b/lib/gs-debug.c new file mode 100644 index 0000000..ad429a4 --- /dev/null +++ b/lib/gs-debug.c @@ -0,0 +1,190 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <stdio.h> +#include <unistd.h> + +#include "gs-debug.h" + +struct _GsDebug +{ + GObject parent_instance; + GMutex mutex; + gboolean use_time; +}; + +G_DEFINE_TYPE (GsDebug, gs_debug, G_TYPE_OBJECT) + +static GLogWriterOutput +gs_log_writer_console (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + GsDebug *debug = GS_DEBUG (user_data); + const gchar *log_domain = NULL; + const gchar *log_message = NULL; + g_autofree gchar *tmp = NULL; + g_autoptr(GMutexLocker) locker = NULL; + g_autoptr(GString) domain = NULL; + + /* enabled */ + if (g_getenv ("GS_DEBUG") == NULL && + log_level == G_LOG_LEVEL_DEBUG) + return G_LOG_WRITER_HANDLED; + + /* get data from arguments */ + for (gsize i = 0; i < n_fields; i++) { + if (g_strcmp0 (fields[i].key, "MESSAGE") == 0) { + log_message = fields[i].value; + continue; + } + if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0) { + log_domain = fields[i].value; + continue; + } + } + + /* this is really verbose */ + if (g_strcmp0 (log_domain, "dconf") == 0 && + log_level == G_LOG_LEVEL_DEBUG) + return G_LOG_WRITER_HANDLED; + + /* make threadsafe */ + locker = g_mutex_locker_new (&debug->mutex); + g_assert (locker != NULL); + + /* time header */ + if (debug->use_time) { + g_autoptr(GDateTime) dt = g_date_time_new_now_utc (); + tmp = g_strdup_printf ("%02i:%02i:%02i:%04i", + g_date_time_get_hour (dt), + g_date_time_get_minute (dt), + g_date_time_get_second (dt), + g_date_time_get_microsecond (dt) / 1000); + } + + /* make these shorter */ + if (g_strcmp0 (log_domain, "PackageKit") == 0) { + log_domain = "PK"; + } else if (g_strcmp0 (log_domain, "GsPlugin") == 0) { + log_domain = "Gs"; + } + + /* pad out domain */ + domain = g_string_new (log_domain); + for (guint i = domain->len; i < 3; i++) + g_string_append (domain, " "); + + switch (log_level) { + case G_LOG_LEVEL_ERROR: + case G_LOG_LEVEL_CRITICAL: + case G_LOG_LEVEL_WARNING: + /* to screen */ + if (isatty (fileno (stderr)) == 1) { + /* critical in red */ + if (tmp != NULL) + g_printerr ("%c[%dm%s ", 0x1B, 32, tmp); + g_printerr ("%s ", domain->str); + g_printerr ("%c[%dm%s\n%c[%dm", 0x1B, 31, log_message, 0x1B, 0); + } else { /* to file */ + if (tmp != NULL) + g_print ("%s ", tmp); + g_print ("%s ", domain->str); + g_print ("%s\n", log_message); + } + break; + default: + /* to screen */ + if (isatty (fileno (stdout)) == 1) { + /* debug in blue */ + if (tmp != NULL) + g_print ("%c[%dm%s ", 0x1B, 32, tmp); + g_print ("%s ", domain->str); + g_print ("%c[%dm%s\n%c[%dm", 0x1B, 34, log_message, 0x1B, 0); + break; + } else { /* to file */ + if (tmp != NULL) + g_print ("%s ", tmp); + g_print ("%s ", domain->str); + g_print ("%s\n", log_message); + } + } + + /* success */ + return G_LOG_WRITER_HANDLED; +} + +static GLogWriterOutput +gs_log_writer_journald (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + /* important enough to force to the journal */ + switch (log_level) { + case G_LOG_LEVEL_ERROR: + case G_LOG_LEVEL_CRITICAL: + case G_LOG_LEVEL_WARNING: + case G_LOG_LEVEL_INFO: + return g_log_writer_journald (log_level, fields, n_fields, user_data); + break; + default: + break; + } + + return G_LOG_WRITER_UNHANDLED; +} + +static GLogWriterOutput +gs_debug_log_writer (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + if (g_log_writer_is_journald (fileno (stderr))) + return gs_log_writer_journald (log_level, fields, n_fields, user_data); + else + return gs_log_writer_console (log_level, fields, n_fields, user_data); +} + +static void +gs_debug_finalize (GObject *object) +{ + GsDebug *debug = GS_DEBUG (object); + + g_mutex_clear (&debug->mutex); + + G_OBJECT_CLASS (gs_debug_parent_class)->finalize (object); +} + +static void +gs_debug_class_init (GsDebugClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_debug_finalize; +} + +static void +gs_debug_init (GsDebug *debug) +{ + g_mutex_init (&debug->mutex); + debug->use_time = g_getenv ("GS_DEBUG_NO_TIME") == NULL; + g_log_set_writer_func (gs_debug_log_writer, + g_object_ref (debug), + (GDestroyNotify) g_object_unref); +} + +GsDebug * +gs_debug_new (void) +{ + return GS_DEBUG (g_object_new (GS_TYPE_DEBUG, NULL)); +} diff --git a/lib/gs-debug.h b/lib/gs-debug.h new file mode 100644 index 0000000..6dd529d --- /dev/null +++ b/lib/gs-debug.h @@ -0,0 +1,21 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define GS_TYPE_DEBUG (gs_debug_get_type ()) + +G_DECLARE_FINAL_TYPE (GsDebug, gs_debug, GS, DEBUG, GObject) + +GsDebug *gs_debug_new (void); + +G_END_DECLS diff --git a/lib/gs-ioprio.c b/lib/gs-ioprio.c new file mode 100644 index 0000000..11b9fa6 --- /dev/null +++ b/lib/gs-ioprio.c @@ -0,0 +1,152 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2005, Novell, Inc. + * Copyright (C) 2006, Jamie McCracken <jamiemcc@gnome.org> + * Copyright (C) 2006, Anders Aagaard + * + * Based mostly on code by Robert Love <rml@novell.com> + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + */ + +#include "config.h" + +#ifdef __linux__ + +#include <stdio.h> +#include <errno.h> + +#ifdef HAVE_LINUX_UNISTD_H +#include <linux/unistd.h> +#endif + +#include <sys/syscall.h> +#include <unistd.h> + +#include <glib/gstdio.h> + +#endif /* __linux__ */ + +#include "gs-ioprio.h" + +/* We assume ALL linux architectures have the syscalls defined here */ +#ifdef __linux__ + +/* Make sure the system call is supported */ +#ifndef __NR_ioprio_set + +#if defined(__i386__) +#define __NR_ioprio_set 289 +#define __NR_ioprio_get 290 +#elif defined(__powerpc__) || defined(__powerpc64__) +#define __NR_ioprio_set 273 +#define __NR_ioprio_get 274 +#elif defined(__x86_64__) +#define __NR_ioprio_set 251 +#define __NR_ioprio_get 252 +#elif defined(__ia64__) +#define __NR_ioprio_set 1274 +#define __NR_ioprio_get 1275 +#elif defined(__alpha__) +#define __NR_ioprio_set 442 +#define __NR_ioprio_get 443 +#elif defined(__s390x__) || defined(__s390__) +#define __NR_ioprio_set 282 +#define __NR_ioprio_get 283 +#elif defined(__SH4__) +#define __NR_ioprio_set 288 +#define __NR_ioprio_get 289 +#elif defined(__SH5__) +#define __NR_ioprio_set 316 +#define __NR_ioprio_get 317 +#elif defined(__sparc__) || defined(__sparc64__) +#define __NR_ioprio_set 196 +#define __NR_ioprio_get 218 +#elif defined(__arm__) +#define __NR_ioprio_set 314 +#define __NR_ioprio_get 315 +#else +#error "Unsupported architecture!" +#endif + +#endif /* __NR_ioprio_set */ + +enum { + IOPRIO_CLASS_NONE, + IOPRIO_CLASS_RT, + IOPRIO_CLASS_BE, + IOPRIO_CLASS_IDLE, +}; + +enum { + IOPRIO_WHO_PROCESS = 1, + IOPRIO_WHO_PGRP, + IOPRIO_WHO_USER, +}; + +#define IOPRIO_CLASS_SHIFT 13 + +static inline int +ioprio_set (int which, int who, int ioprio_val) +{ + return syscall (__NR_ioprio_set, which, who, ioprio_val); +} + +static int +set_io_priority_idle (void) +{ + int ioprio, ioclass; + + ioprio = 7; /* priority is ignored with idle class */ + ioclass = IOPRIO_CLASS_IDLE << IOPRIO_CLASS_SHIFT; + + return ioprio_set (IOPRIO_WHO_PROCESS, 0, ioprio | ioclass); +} + +static int +set_io_priority_best_effort (int ioprio_val) +{ + int ioclass; + + ioclass = IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT; + + return ioprio_set (IOPRIO_WHO_PROCESS, 0, ioprio_val | ioclass); +} + +void +gs_ioprio_init (void) +{ + if (set_io_priority_idle () == -1) { + g_message ("Could not set idle IO priority, attempting best effort of 7"); + + if (set_io_priority_best_effort (7) == -1) { + g_message ("Could not set best effort IO priority either, giving up"); + } + } +} + +#else /* __linux__ */ + +void +gs_ioprio_init (void) +{ +} + +#endif /* __linux__ */ diff --git a/lib/gs-ioprio.h b/lib/gs-ioprio.h new file mode 100644 index 0000000..b876afe --- /dev/null +++ b/lib/gs-ioprio.h @@ -0,0 +1,31 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2006, Anders Aagaard + * Copyright (C) 2008, Nokia <ivan.frade@nokia.com> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#pragma once + +#include <glib.h> + +G_BEGIN_DECLS + +void gs_ioprio_init (void); + +G_END_DECLS diff --git a/lib/gs-metered.c b/lib/gs-metered.c new file mode 100644 index 0000000..d4afec4 --- /dev/null +++ b/lib/gs-metered.c @@ -0,0 +1,267 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Endless Mobile, Inc. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-metered + * @title: Metered Data Utilities + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Utility functions to help with metered data handling + * + * Metered data handling is provided by Mogwai, which implements a download + * scheduler to control when, and in which order, large downloads happen on + * the system. + * + * All large downloads from gs_plugin_download() or gs_plugin_download_app() + * calls should be scheduled using Mogwai, which will notify gnome-software + * when those downloads can start and stop, according to system policy. + * + * The functions in this file make interacting with the scheduling daemon a + * little simpler. Since all #GsPlugin method calls happen in worker threads, + * typically without a #GMainContext, all interaction with the scheduler should + * be blocking. libmogwai-schedule-client was designed to be asynchronous; so + * these helpers make it synchronous. + * + * Since: 2.34 + */ + +#include "config.h" + +#include <glib.h> + +#ifdef HAVE_MOGWAI +#include <libmogwai-schedule-client/scheduler.h> +#endif + +#include "gs-metered.h" + + +#ifdef HAVE_MOGWAI + +/* FIXME: Backported from https://gitlab.gnome.org/GNOME/glib/merge_requests/983. + * Drop once we can depend on a version of GLib which includes it .*/ +typedef void MainContextPusher; + +static inline MainContextPusher * +main_context_pusher_new (GMainContext *main_context) +{ + g_main_context_push_thread_default (main_context); + return (MainContextPusher *) main_context; +} + +static inline void +main_context_pusher_free (MainContextPusher *pusher) +{ + g_main_context_pop_thread_default ((GMainContext *) pusher); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MainContextPusher, main_context_pusher_free) + + +typedef struct +{ + gboolean *out_download_now; /* (unowned) */ + GMainContext *context; /* (unowned) */ +} DownloadNowData; + +static void +download_now_cb (GObject *obj, + GParamSpec *pspec, + gpointer user_data) +{ + DownloadNowData *data = user_data; + *data->out_download_now = mwsc_schedule_entry_get_download_now (MWSC_SCHEDULE_ENTRY (obj)); + g_main_context_wakeup (data->context); +} + +typedef struct +{ + GError **out_error; /* (unowned) */ + GMainContext *context; /* (unowned) */ +} InvalidatedData; + +static void +invalidated_cb (MwscScheduleEntry *entry, + const GError *error, + gpointer user_data) +{ + InvalidatedData *data = user_data; + *data->out_error = g_error_copy (error); + g_main_context_wakeup (data->context); +} + +#endif /* HAVE_MOGWAI */ + +/** + * gs_metered_block_on_download_scheduler: + * @parameters: (nullable): a #GVariant of type `a{sv}` specifying parameters + * for the schedule entry, or %NULL to pass no parameters + * @cancellable: (nullable): a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Create a schedule entry with the given @parameters, and block until + * permission is given to download. + * + * FIXME: This will currently ignore later revocations of that download + * permission, and does not support creating a schedule entry per app. + * + * If a schedule entry cannot be created, or if @cancellable is cancelled, + * an error will be set and %FALSE returned. + * + * The keys understood by @parameters are listed in the documentation for + * mwsc_scheduler_schedule_async(). + * + * This function will likely be called from a #GsPluginLoader worker thread. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 2.34 + */ +gboolean +gs_metered_block_on_download_scheduler (GVariant *parameters, + GCancellable *cancellable, + GError **error) +{ +#ifdef HAVE_MOGWAI + g_autoptr(MwscScheduler) scheduler = NULL; + g_autoptr(MwscScheduleEntry) schedule_entry = NULL; + g_autofree gchar *parameters_str = NULL; + g_autoptr(GMainContext) context = NULL; + g_autoptr(MainContextPusher) pusher = NULL; + + parameters_str = (parameters != NULL) ? g_variant_print (parameters, TRUE) : g_strdup ("(none)"); + g_debug ("%s: Waiting with parameters: %s", G_STRFUNC, parameters_str); + + /* Push the context early so that the #MwscScheduler is created to run within it. */ + context = g_main_context_new (); + pusher = main_context_pusher_new (context); + + /* Wait until the download can be scheduled. + * FIXME: In future, downloads could be split up by app, so they can all + * be scheduled separately and, for example, higher priority ones could + * be scheduled with a higher priority. This would have to be aware of + * dependencies. */ + scheduler = mwsc_scheduler_new (cancellable, error); + if (scheduler == NULL) + return FALSE; + + /* Create a schedule entry for the group of downloads. + * FIXME: The underlying OSTree code supports resuming downloads + * (at a granularity of individual objects), so it should be + * possible to plumb through here. */ + schedule_entry = mwsc_scheduler_schedule (scheduler, parameters, cancellable, + error); + if (schedule_entry == NULL) + return FALSE; + + /* Wait until the download is allowed to proceed. */ + if (!mwsc_schedule_entry_get_download_now (schedule_entry)) { + gboolean download_now = FALSE; + g_autoptr(GError) invalidated_error = NULL; + gulong notify_id, invalidated_id; + DownloadNowData download_now_data = { &download_now, context }; + InvalidatedData invalidated_data = { &invalidated_error, context }; + + notify_id = g_signal_connect (schedule_entry, "notify::download-now", + (GCallback) download_now_cb, &download_now_data); + invalidated_id = g_signal_connect (schedule_entry, "invalidated", + (GCallback) invalidated_cb, &invalidated_data); + + while (!download_now && invalidated_error == NULL && + !g_cancellable_is_cancelled (cancellable)) + g_main_context_iteration (context, TRUE); + + g_signal_handler_disconnect (schedule_entry, invalidated_id); + g_signal_handler_disconnect (schedule_entry, notify_id); + + if (!download_now && invalidated_error != NULL) { + g_propagate_error (error, g_steal_pointer (&invalidated_error)); + return FALSE; + } else if (!download_now && g_cancellable_set_error_if_cancelled (cancellable, error)) { + return FALSE; + } + + g_assert (download_now); + } + + g_debug ("%s: Allowed to download", G_STRFUNC); +#else /* if !HAVE_MOGWAI */ + g_debug ("%s: Allowed to download (Mogwai support compiled out)", G_STRFUNC); +#endif /* !HAVE_MOGWAI */ + + return TRUE; +} + +/** + * gs_metered_block_app_on_download_scheduler: + * @app: a #GsApp to get the scheduler parameters from + * @cancellable: a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Version of gs_metered_block_on_download_scheduler() which extracts the + * download parameters from the given @app. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 2.34 + */ +gboolean +gs_metered_block_app_on_download_scheduler (GsApp *app, + GCancellable *cancellable, + GError **error) +{ + g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL); + g_autoptr(GVariant) parameters = NULL; + guint64 download_size = gs_app_get_size_download (app); + + /* Currently no plugins support resumable downloads. This may change in + * future, in which case this parameter should be refactored. */ + g_variant_dict_insert (¶meters_dict, "resumable", "b", FALSE); + + if (download_size != 0 && download_size != GS_APP_SIZE_UNKNOWABLE) { + g_variant_dict_insert (¶meters_dict, "size-minimum", "t", download_size); + g_variant_dict_insert (¶meters_dict, "size-maximum", "t", download_size); + } + + parameters = g_variant_ref_sink (g_variant_dict_end (¶meters_dict)); + + return gs_metered_block_on_download_scheduler (parameters, cancellable, error); +} + +/** + * gs_metered_block_app_list_on_download_scheduler: + * @app_list: a #GsAppList to get the scheduler parameters from + * @cancellable: a #GCancellable, or %NULL + * @error: return location for a #GError, or %NULL + * + * Version of gs_metered_block_on_download_scheduler() which extracts the + * download parameters from the apps in the given @app_list. + * + * Returns: %TRUE on success, %FALSE otherwise + * Since: 2.34 + */ +gboolean +gs_metered_block_app_list_on_download_scheduler (GsAppList *app_list, + GCancellable *cancellable, + GError **error) +{ + g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL); + g_autoptr(GVariant) parameters = NULL; + + /* Currently no plugins support resumable downloads. This may change in + * future, in which case this parameter should be refactored. */ + g_variant_dict_insert (¶meters_dict, "resumable", "b", FALSE); + + /* FIXME: Currently this creates a single Mogwai schedule entry for the + * entire app list. Eventually, we probably want one schedule entry per + * app being downloaded, so that they can be individually prioritised. + * However, that requires much deeper integration into the download + * code, and Mogwai does not currently support that level of + * prioritisation, so go with this simple implementation for now. */ + parameters = g_variant_ref_sink (g_variant_dict_end (¶meters_dict)); + + return gs_metered_block_on_download_scheduler (parameters, cancellable, error); +} diff --git a/lib/gs-metered.h b/lib/gs-metered.h new file mode 100644 index 0000000..f8cdc1f --- /dev/null +++ b/lib/gs-metered.h @@ -0,0 +1,29 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2019 Endless Mobile, Inc. + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> +#include <gio/gio.h> + +#include "gs-app.h" +#include "gs-app-list.h" + +G_BEGIN_DECLS + +gboolean gs_metered_block_on_download_scheduler (GVariant *parameters, + GCancellable *cancellable, + GError **error); +gboolean gs_metered_block_app_on_download_scheduler (GsApp *app, + GCancellable *cancellable, + GError **error); +gboolean gs_metered_block_app_list_on_download_scheduler (GsAppList *app_list, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/lib/gs-os-release.c b/lib/gs-os-release.c new file mode 100644 index 0000000..305c5bb --- /dev/null +++ b/lib/gs-os-release.c @@ -0,0 +1,335 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +/** + * SECTION:gs-os-release + * @title: GsOsRelease + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Data from os-release + * + * This object allows plugins to parse /etc/os-release for distribution + * metadata information. + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-os-release.h" + +struct _GsOsRelease +{ + GObject parent_instance; + gchar *name; + gchar *version; + gchar *id; + gchar **id_like; + gchar *version_id; + gchar *pretty_name; + gchar *cpe_name; + gchar *distro_codename; + gchar *home_url; +}; + +static void gs_os_release_initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (GsOsRelease, gs_os_release, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, gs_os_release_initable_iface_init)) + +static gboolean +gs_os_release_initable_init (GInitable *initable, + GCancellable *cancellable, + GError **error) +{ + GsOsRelease *os_release = GS_OS_RELEASE (initable); + const gchar *filename; + g_autofree gchar *data = NULL; + g_auto(GStrv) lines = NULL; + guint i; + + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* get contents */ + filename = g_getenv ("GS_SELF_TEST_OS_RELEASE_FILENAME"); + if (filename == NULL) { + filename = "/etc/os-release"; + if (!g_file_test (filename, G_FILE_TEST_EXISTS)) + filename = "/usr/lib/os-release"; + } + if (!g_file_get_contents (filename, &data, NULL, error)) + return FALSE; + + /* parse */ + lines = g_strsplit (data, "\n", -1); + for (i = 0; lines[i] != NULL; i++) { + gchar *tmp; + + /* split the line up into two halves */ + tmp = g_strstr_len (lines[i], -1, "="); + if (tmp == NULL) + continue; + *tmp = '\0'; + tmp++; + + /* ignore trailing quote */ + if (tmp[0] == '\"') + tmp++; + + /* ignore trailing quote */ + g_strdelimit (tmp, "\"", '\0'); + + /* match fields we're interested in */ + if (g_strcmp0 (lines[i], "NAME") == 0) { + os_release->name = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "VERSION") == 0) { + os_release->version = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "ID") == 0) { + os_release->id = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "ID_LIKE") == 0) { + os_release->id_like = g_strsplit (tmp, " ", 0); + continue; + } + if (g_strcmp0 (lines[i], "VERSION_ID") == 0) { + os_release->version_id = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "PRETTY_NAME") == 0) { + os_release->pretty_name = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "CPE_NAME") == 0) { + os_release->cpe_name = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "UBUNTU_CODENAME") == 0) { + os_release->distro_codename = g_strdup (tmp); + continue; + } + if (g_strcmp0 (lines[i], "HOME_URL") == 0) { + os_release->home_url = g_strdup (tmp); + continue; + } + } + return TRUE; +} + +/** + * gs_os_release_get_name: + * @os_release: A #GsOsRelease + * + * Gets the name from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_name (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->name; +} + +/** + * gs_os_release_get_version: + * @os_release: A #GsOsRelease + * + * Gets the version from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_version (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->version; +} + +/** + * gs_os_release_get_id: + * @os_release: A #GsOsRelease + * + * Gets the ID from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_id (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->id; +} + +/** + * gs_os_release_get_id_like: + * @os_release: A #GsOsRelease + * + * Gets the ID_LIKE from the os-release parser. This is a list of operating + * systems that are "closely related" to the local operating system, possibly + * by being a derivative distribution. + * + * Returns: a %NULL terminated list + * + * Since: 3.26.2 + **/ +const gchar * const * +gs_os_release_get_id_like (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return (const gchar * const *) os_release->id_like; +} + +/** + * gs_os_release_get_version_id: + * @os_release: A #GsOsRelease + * + * Gets the version ID from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_version_id (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->version_id; +} + +/** + * gs_os_release_get_pretty_name: + * @os_release: A #GsOsRelease + * + * Gets the pretty name from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_pretty_name (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->pretty_name; +} + +/** + * gs_os_release_get_cpe_name: + * @os_release: A #GsOsRelease + * + * Gets the pretty name from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_cpe_name (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->cpe_name; +} + +/** + * gs_os_release_get_distro_codename: + * @os_release: A #GsOsRelease + * + * Gets the distro codename from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_distro_codename (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->distro_codename; +} + +/** + * gs_os_release_get_home_url: + * @os_release: A #GsOsRelease + * + * Gets the home URL from the os-release parser. + * + * Returns: a string, or %NULL + * + * Since: 3.22 + **/ +const gchar * +gs_os_release_get_home_url (GsOsRelease *os_release) +{ + g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL); + return os_release->home_url; +} + +static void +gs_os_release_finalize (GObject *object) +{ + GsOsRelease *os_release = GS_OS_RELEASE (object); + g_free (os_release->name); + g_free (os_release->version); + g_free (os_release->id); + g_strfreev (os_release->id_like); + g_free (os_release->version_id); + g_free (os_release->pretty_name); + g_free (os_release->cpe_name); + g_free (os_release->distro_codename); + g_free (os_release->home_url); + G_OBJECT_CLASS (gs_os_release_parent_class)->finalize (object); +} + +static void +gs_os_release_class_init (GsOsReleaseClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_os_release_finalize; +} + +static void +gs_os_release_initable_iface_init (GInitableIface *iface) +{ + iface->init = gs_os_release_initable_init; +} + +static void +gs_os_release_init (GsOsRelease *os_release) +{ +} + +/** + * gs_os_release_new: + * @error: a #GError, or %NULL + * + * Creates a new os_release. + * + * Returns: (transfer full): A newly allocated #GsOsRelease, or %NULL for error + * + * Since: 3.22 + **/ +GsOsRelease * +gs_os_release_new (GError **error) +{ + GsOsRelease *os_release; + os_release = g_initable_new (GS_TYPE_OS_RELEASE, NULL, error, NULL); + return GS_OS_RELEASE (os_release); +} diff --git a/lib/gs-os-release.h b/lib/gs-os-release.h new file mode 100644 index 0000000..336f17c --- /dev/null +++ b/lib/gs-os-release.h @@ -0,0 +1,33 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: LGPL-2.1+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" + +G_BEGIN_DECLS + +#define GS_TYPE_OS_RELEASE (gs_os_release_get_type ()) + +G_DECLARE_FINAL_TYPE (GsOsRelease, gs_os_release, GS, OS_RELEASE, GObject) + +GsOsRelease *gs_os_release_new (GError **error); +const gchar *gs_os_release_get_name (GsOsRelease *os_release); +const gchar *gs_os_release_get_version (GsOsRelease *os_release); +const gchar *gs_os_release_get_id (GsOsRelease *os_release); +const gchar * const *gs_os_release_get_id_like (GsOsRelease *os_release); +const gchar *gs_os_release_get_version_id (GsOsRelease *os_release); +const gchar *gs_os_release_get_pretty_name (GsOsRelease *os_release); +const gchar *gs_os_release_get_cpe_name (GsOsRelease *os_release); +const gchar *gs_os_release_get_distro_codename (GsOsRelease *os_release); +const gchar *gs_os_release_get_home_url (GsOsRelease *os_release); + +G_END_DECLS diff --git a/lib/gs-plugin-event.c b/lib/gs-plugin-event.c new file mode 100644 index 0000000..8d1fc83 --- /dev/null +++ b/lib/gs-plugin-event.c @@ -0,0 +1,313 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2016 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin-event + * @title: GsPluginEvent + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Information about a plugin event + * + * These functions provide a way for plugins to tell the UI layer about events + * that may require displaying to the user. Plugins should not assume that a + * specific event is actually shown to the user as it may be ignored + * automatically. + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-plugin-private.h" +#include "gs-plugin-event.h" + +struct _GsPluginEvent +{ + GObject parent_instance; + GsApp *app; + GsApp *origin; + GsPluginAction action; + GError *error; + GsPluginEventFlag flags; + gchar *unique_id; +}; + +G_DEFINE_TYPE (GsPluginEvent, gs_plugin_event, G_TYPE_OBJECT) + +/** + * gs_plugin_event_set_app: + * @event: A #GsPluginEvent + * @app: A #GsApp + * + * Set the application (or source, or whatever component) that caused the event + * to be created. + * + * Since: 3.22 + **/ +void +gs_plugin_event_set_app (GsPluginEvent *event, GsApp *app) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + g_return_if_fail (GS_IS_APP (app)); + g_set_object (&event->app, app); +} + +/** + * gs_plugin_event_get_app: + * @event: A #GsPluginEvent + * + * Gets an application that created the event. + * + * Returns: (transfer none): a #GsApp, or %NULL if unset + * + * Since: 3.22 + **/ +GsApp * +gs_plugin_event_get_app (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL); + return event->app; +} + +/** + * gs_plugin_event_set_origin: + * @event: A #GsPluginEvent + * @origin: A #GsApp + * + * Set the origin that caused the event to be created. + * + * Since: 3.22 + **/ +void +gs_plugin_event_set_origin (GsPluginEvent *event, GsApp *origin) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + g_return_if_fail (GS_IS_APP (origin)); + g_set_object (&event->origin, origin); +} + +/** + * gs_plugin_event_get_origin: + * @event: A #GsPluginEvent + * + * Gets an origin that created the event. + * + * Returns: (transfer none): a #GsApp, or %NULL if unset + * + * Since: 3.22 + **/ +GsApp * +gs_plugin_event_get_origin (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL); + return event->origin; +} + +/** + * gs_plugin_event_set_action: + * @event: A #GsPluginEvent + * @action: A #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE + * + * Set the action that caused the event to be created. + * + * Since: 3.22 + **/ +void +gs_plugin_event_set_action (GsPluginEvent *event, GsPluginAction action) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + event->action = action; +} + +/** + * gs_plugin_event_get_action: + * @event: A #GsPluginEvent + * + * Gets an action that created the event. + * + * Returns: (transfer none): a #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE + * + * Since: 3.22 + **/ +GsPluginAction +gs_plugin_event_get_action (GsPluginEvent *event) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), 0); + return event->action; +} + +/** + * gs_plugin_event_get_unique_id: + * @event: A #GsPluginEvent + * + * Gets the unique ID for the event. In most cases (if an app has been set) + * this will just be the actual #GsApp unique-id. In the cases where only error + * has been set a virtual (but plausible) ID will be generated. + * + * Returns: a string, or %NULL for invalid + * + * Since: 3.22 + **/ +const gchar * +gs_plugin_event_get_unique_id (GsPluginEvent *event) +{ + /* just proxy */ + if (event->origin != NULL && + gs_app_get_unique_id (event->origin) != NULL) { + return gs_app_get_unique_id (event->origin); + } + if (event->app != NULL && + gs_app_get_unique_id (event->app) != NULL) { + return gs_app_get_unique_id (event->app); + } + + /* generate from error */ + if (event->error != NULL) { + if (event->unique_id == NULL) { + g_autofree gchar *id = NULL; + id = g_strdup_printf ("%s.error", + gs_plugin_error_to_string (event->error->code)); + event->unique_id = as_utils_unique_id_build (AS_APP_SCOPE_UNKNOWN, + AS_BUNDLE_KIND_UNKNOWN, + NULL, + AS_APP_KIND_UNKNOWN, + id, + NULL); + } + return event->unique_id; + } + + /* failed */ + return NULL; +} + +/** + * gs_plugin_event_get_kind: + * @event: A #GsPluginEvent + * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID + * + * Adds a flag to the event. + * + * Since: 3.22 + **/ +void +gs_plugin_event_add_flag (GsPluginEvent *event, GsPluginEventFlag flag) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + event->flags |= flag; +} + +/** + * gs_plugin_event_set_kind: + * @event: A #GsPluginEvent + * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID + * + * Removes a flag from the event. + * + * Since: 3.22 + **/ +void +gs_plugin_event_remove_flag (GsPluginEvent *event, GsPluginEventFlag flag) +{ + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + event->flags &= ~flag; +} + +/** + * gs_plugin_event_has_flag: + * @event: A #GsPluginEvent + * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID + * + * Finds out if the event has a specific flag. + * + * Returns: %TRUE if the flag is set + * + * Since: 3.22 + **/ +gboolean +gs_plugin_event_has_flag (GsPluginEvent *event, GsPluginEventFlag flag) +{ + g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), FALSE); + return ((event->flags & flag) > 0); +} + +/** + * gs_plugin_event_set_error: + * @event: A #GsPluginEvent + * @error: A #GError + * + * Sets the event error. + * + * Since: 3.22 + **/ +void +gs_plugin_event_set_error (GsPluginEvent *event, const GError *error) +{ + g_clear_error (&event->error); + event->error = g_error_copy (error); +} + +/** + * gs_plugin_event_get_error: + * @event: A #GsPluginEvent + * + * Gets the event error. + * + * Returns: a #GError, or %NULL for unset + * + * Since: 3.22 + **/ +const GError * +gs_plugin_event_get_error (GsPluginEvent *event) +{ + return event->error; +} + +static void +gs_plugin_event_finalize (GObject *object) +{ + GsPluginEvent *event = GS_PLUGIN_EVENT (object); + if (event->error != NULL) + g_error_free (event->error); + if (event->app != NULL) + g_object_unref (event->app); + if (event->origin != NULL) + g_object_unref (event->origin); + g_free (event->unique_id); + G_OBJECT_CLASS (gs_plugin_event_parent_class)->finalize (object); +} + +static void +gs_plugin_event_class_init (GsPluginEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_plugin_event_finalize; +} + +static void +gs_plugin_event_init (GsPluginEvent *event) +{ +} + +/** + * gs_plugin_event_new: + * + * Creates a new event. + * + * Returns: A newly allocated #GsPluginEvent + * + * Since: 3.22 + **/ +GsPluginEvent * +gs_plugin_event_new (void) +{ + GsPluginEvent *event; + event = g_object_new (GS_TYPE_PLUGIN_EVENT, NULL); + return GS_PLUGIN_EVENT (event); +} diff --git a/lib/gs-plugin-event.h b/lib/gs-plugin-event.h new file mode 100644 index 0000000..dc74486 --- /dev/null +++ b/lib/gs-plugin-event.h @@ -0,0 +1,67 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_EVENT (gs_plugin_event_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginEvent, gs_plugin_event, GS, PLUGIN_EVENT, GObject) + +/** + * GsPluginEventFlag: + * @GS_PLUGIN_EVENT_FLAG_NONE: No special flags set + * @GS_PLUGIN_EVENT_FLAG_INVALID: Event is no longer valid, e.g. was dismissed + * @GS_PLUGIN_EVENT_FLAG_VISIBLE: Event is is visible on the screen + * @GS_PLUGIN_EVENT_FLAG_WARNING: Event should be shown with more urgency + * @GS_PLUGIN_EVENT_FLAG_INTERACTIVE: The plugin job was created with interactive=True + * + * Any flags an event can have. + **/ +typedef enum { + GS_PLUGIN_EVENT_FLAG_NONE = 0, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_INVALID = 1 << 0, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_VISIBLE = 1 << 1, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_WARNING = 1 << 2, /* Since: 3.22 */ + GS_PLUGIN_EVENT_FLAG_INTERACTIVE = 1 << 3, /* Since: 3.30 */ + /*< private >*/ + GS_PLUGIN_EVENT_FLAG_LAST +} GsPluginEventFlag; + +GsPluginEvent *gs_plugin_event_new (void); + +const gchar *gs_plugin_event_get_unique_id (GsPluginEvent *event); + +void gs_plugin_event_set_app (GsPluginEvent *event, + GsApp *app); +GsApp *gs_plugin_event_get_app (GsPluginEvent *event); +void gs_plugin_event_set_origin (GsPluginEvent *event, + GsApp *origin); +GsApp *gs_plugin_event_get_origin (GsPluginEvent *event); +void gs_plugin_event_set_action (GsPluginEvent *event, + GsPluginAction action); +GsPluginAction gs_plugin_event_get_action (GsPluginEvent *event); + +void gs_plugin_event_set_error (GsPluginEvent *event, + const GError *error); +const GError *gs_plugin_event_get_error (GsPluginEvent *event); + +void gs_plugin_event_add_flag (GsPluginEvent *event, + GsPluginEventFlag flag); +void gs_plugin_event_remove_flag (GsPluginEvent *event, + GsPluginEventFlag flag); +gboolean gs_plugin_event_has_flag (GsPluginEvent *event, + GsPluginEventFlag flag); + +G_END_DECLS diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h new file mode 100644 index 0000000..863d890 --- /dev/null +++ b/lib/gs-plugin-job-private.h @@ -0,0 +1,44 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +GsPluginAction gs_plugin_job_get_action (GsPluginJob *self); +GsPluginRefineFlags gs_plugin_job_get_filter_flags (GsPluginJob *self); +GsAppListFilterFlags gs_plugin_job_get_dedupe_flags (GsPluginJob *self); +GsPluginRefineFlags gs_plugin_job_get_refine_flags (GsPluginJob *self); +gboolean gs_plugin_job_has_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +void gs_plugin_job_add_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +void gs_plugin_job_remove_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +gboolean gs_plugin_job_get_interactive (GsPluginJob *self); +guint gs_plugin_job_get_max_results (GsPluginJob *self); +guint gs_plugin_job_get_timeout (GsPluginJob *self); +guint64 gs_plugin_job_get_age (GsPluginJob *self); +GsAppListSortFunc gs_plugin_job_get_sort_func (GsPluginJob *self); +gpointer gs_plugin_job_get_sort_func_data (GsPluginJob *self); +const gchar *gs_plugin_job_get_search (GsPluginJob *self); +GsApp *gs_plugin_job_get_app (GsPluginJob *self); +GsAppList *gs_plugin_job_get_list (GsPluginJob *self); +GFile *gs_plugin_job_get_file (GsPluginJob *self); +GsPlugin *gs_plugin_job_get_plugin (GsPluginJob *self); +GsCategory *gs_plugin_job_get_category (GsPluginJob *self); +AsReview *gs_plugin_job_get_review (GsPluginJob *self); +gchar *gs_plugin_job_to_string (GsPluginJob *self); +void gs_plugin_job_set_action (GsPluginJob *self, + GsPluginAction action); + +G_END_DECLS diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c new file mode 100644 index 0000000..1ce320f --- /dev/null +++ b/lib/gs-plugin-job.c @@ -0,0 +1,618 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include <glib.h> + +#include "gs-plugin-private.h" +#include "gs-plugin-job-private.h" + +struct _GsPluginJob +{ + GObject parent_instance; + GsPluginRefineFlags refine_flags; + GsPluginRefineFlags filter_flags; + GsAppListFilterFlags dedupe_flags; + gboolean interactive; + guint max_results; + guint timeout; + guint64 age; + GsPlugin *plugin; + GsPluginAction action; + GsAppListSortFunc sort_func; + gpointer sort_func_data; + gchar *search; + GsApp *app; + GsAppList *list; + GFile *file; + GsCategory *category; + AsReview *review; + gint64 time_created; +}; + +enum { + PROP_0, + PROP_ACTION, + PROP_AGE, + PROP_SEARCH, + PROP_REFINE_FLAGS, + PROP_FILTER_FLAGS, + PROP_DEDUPE_FLAGS, + PROP_INTERACTIVE, + PROP_APP, + PROP_LIST, + PROP_FILE, + PROP_CATEGORY, + PROP_REVIEW, + PROP_MAX_RESULTS, + PROP_TIMEOUT, + PROP_LAST +}; + +G_DEFINE_TYPE (GsPluginJob, gs_plugin_job, G_TYPE_OBJECT) + +gchar * +gs_plugin_job_to_string (GsPluginJob *self) +{ + GString *str = g_string_new (NULL); + gint64 time_now = g_get_monotonic_time (); + g_string_append_printf (str, "running %s", + gs_plugin_action_to_string (self->action)); + if (self->plugin != NULL) { + g_string_append_printf (str, " on plugin=%s", + gs_plugin_get_name (self->plugin)); + } + if (self->filter_flags > 0) { + g_autofree gchar *tmp = gs_plugin_refine_flags_to_string (self->filter_flags); + g_string_append_printf (str, " with filter-flags=%s", tmp); + } + if (self->dedupe_flags > 0) + g_string_append_printf (str, " with dedupe-flags=%" G_GUINT64_FORMAT, self->dedupe_flags); + if (self->refine_flags > 0) { + g_autofree gchar *tmp = gs_plugin_refine_flags_to_string (self->refine_flags); + g_string_append_printf (str, " with refine-flags=%s", tmp); + } + if (self->interactive) + g_string_append_printf (str, " with interactive=True"); + if (self->timeout > 0) + g_string_append_printf (str, " with timeout=%u", self->timeout); + if (self->max_results > 0) + g_string_append_printf (str, " with max-results=%u", self->max_results); + if (self->age != 0) { + if (self->age == G_MAXUINT) { + g_string_append (str, " with cache age=any"); + } else { + g_string_append_printf (str, " with cache age=%" G_GUINT64_FORMAT, + self->age); + } + } + if (self->search != NULL) { + g_string_append_printf (str, " with search=%s", + self->search); + } + if (self->category != NULL) { + GsCategory *parent = gs_category_get_parent (self->category); + if (parent != NULL) { + g_string_append_printf (str, " with category=%s/%s", + gs_category_get_id (parent), + gs_category_get_id (self->category)); + } else { + g_string_append_printf (str, " with category=%s", + gs_category_get_id (self->category)); + } + } + if (self->review != NULL) { + g_string_append_printf (str, " with review=%s", + as_review_get_id (self->review)); + } + if (self->file != NULL) { + g_autofree gchar *path = g_file_get_path (self->file); + g_string_append_printf (str, " with file=%s", path); + } + if (self->list != NULL && gs_app_list_length (self->list) > 0) { + g_autofree const gchar **unique_ids = NULL; + g_autofree gchar *unique_ids_str = NULL; + unique_ids = g_new0 (const gchar *, gs_app_list_length (self->list) + 1); + for (guint i = 0; i < gs_app_list_length (self->list); i++) { + GsApp *app = gs_app_list_index (self->list, i); + unique_ids[i] = gs_app_get_unique_id (app); + } + unique_ids_str = g_strjoinv (",", (gchar**) unique_ids); + g_string_append_printf (str, " on apps %s", unique_ids_str); + } + if (time_now - self->time_created > 1000) { + g_string_append_printf (str, ", elapsed time since creation %" G_GINT64_FORMAT "ms", + (time_now - self->time_created) / 1000); + } + return g_string_free (str, FALSE); +} + +void +gs_plugin_job_set_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->refine_flags = refine_flags; +} + +void +gs_plugin_job_set_filter_flags (GsPluginJob *self, GsPluginRefineFlags filter_flags) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->filter_flags = filter_flags; +} + +void +gs_plugin_job_set_dedupe_flags (GsPluginJob *self, GsAppListFilterFlags dedupe_flags) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->dedupe_flags = dedupe_flags; +} + +GsPluginRefineFlags +gs_plugin_job_get_refine_flags (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->refine_flags; +} + +GsPluginRefineFlags +gs_plugin_job_get_filter_flags (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->filter_flags; +} + +GsAppListFilterFlags +gs_plugin_job_get_dedupe_flags (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->dedupe_flags; +} + +gboolean +gs_plugin_job_has_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE); + return (self->refine_flags & refine_flags) > 0; +} + +void +gs_plugin_job_add_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->refine_flags |= refine_flags; +} + +void +gs_plugin_job_remove_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->refine_flags &= ~refine_flags; +} + +void +gs_plugin_job_set_interactive (GsPluginJob *self, gboolean interactive) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->interactive = interactive; +} + +gboolean +gs_plugin_job_get_interactive (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE); + return self->interactive; +} + +void +gs_plugin_job_set_max_results (GsPluginJob *self, guint max_results) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->max_results = max_results; +} + +guint +gs_plugin_job_get_max_results (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->max_results; +} + +void +gs_plugin_job_set_timeout (GsPluginJob *self, guint timeout) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->timeout = timeout; +} + +guint +gs_plugin_job_get_timeout (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->timeout; +} + +void +gs_plugin_job_set_age (GsPluginJob *self, guint64 age) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->age = age; +} + +guint64 +gs_plugin_job_get_age (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->age; +} + +void +gs_plugin_job_set_action (GsPluginJob *self, GsPluginAction action) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->action = action; +} + +GsPluginAction +gs_plugin_job_get_action (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->action; +} + +void +gs_plugin_job_set_sort_func (GsPluginJob *self, GsAppListSortFunc sort_func) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->sort_func = sort_func; +} + +GsAppListSortFunc +gs_plugin_job_get_sort_func (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0); + return self->sort_func; +} + +void +gs_plugin_job_set_sort_func_data (GsPluginJob *self, gpointer sort_func_data) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + self->sort_func_data = sort_func_data; +} + +gpointer +gs_plugin_job_get_sort_func_data (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->sort_func_data; +} + +void +gs_plugin_job_set_search (GsPluginJob *self, const gchar *search) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_free (self->search); + self->search = g_strdup (search); +} + +const gchar * +gs_plugin_job_get_search (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->search; +} + +void +gs_plugin_job_set_app (GsPluginJob *self, GsApp *app) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&self->app, app); + + /* ensure we can always operate on a list object */ + if (self->list != NULL && app != NULL && gs_app_list_length (self->list) == 0) + gs_app_list_add (self->list, self->app); +} + +GsApp * +gs_plugin_job_get_app (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->app; +} + +void +gs_plugin_job_set_list (GsPluginJob *self, GsAppList *list) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + if (list == NULL) + g_warning ("trying to set list to NULL, not a good idea"); + g_set_object (&self->list, list); +} + +GsAppList * +gs_plugin_job_get_list (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->list; +} + +void +gs_plugin_job_set_file (GsPluginJob *self, GFile *file) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&self->file, file); +} + +GFile * +gs_plugin_job_get_file (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->file; +} + +void +gs_plugin_job_set_plugin (GsPluginJob *self, GsPlugin *plugin) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&self->plugin, plugin); +} + +GsPlugin * +gs_plugin_job_get_plugin (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->plugin; +} + +void +gs_plugin_job_set_category (GsPluginJob *self, GsCategory *category) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&self->category, category); +} + +GsCategory * +gs_plugin_job_get_category (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->category; +} + +void +gs_plugin_job_set_review (GsPluginJob *self, AsReview *review) +{ + g_return_if_fail (GS_IS_PLUGIN_JOB (self)); + g_set_object (&self->review, review); +} + +AsReview * +gs_plugin_job_get_review (GsPluginJob *self) +{ + g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL); + return self->review; +} + +static void +gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsPluginJob *self = GS_PLUGIN_JOB (obj); + + switch (prop_id) { + case PROP_ACTION: + g_value_set_uint (value, self->action); + break; + case PROP_AGE: + g_value_set_uint64 (value, self->age); + break; + case PROP_REFINE_FLAGS: + g_value_set_uint64 (value, self->refine_flags); + break; + case PROP_FILTER_FLAGS: + g_value_set_uint64 (value, self->filter_flags); + break; + case PROP_DEDUPE_FLAGS: + g_value_set_uint64 (value, self->dedupe_flags); + break; + case PROP_INTERACTIVE: + g_value_set_boolean (value, self->interactive); + break; + case PROP_SEARCH: + g_value_set_string (value, self->search); + break; + case PROP_APP: + g_value_set_object (value, self->app); + break; + case PROP_LIST: + g_value_set_object (value, self->list); + break; + case PROP_FILE: + g_value_set_object (value, self->file); + break; + case PROP_CATEGORY: + g_value_set_object (value, self->category); + break; + case PROP_REVIEW: + g_value_set_object (value, self->review); + break; + case PROP_MAX_RESULTS: + g_value_set_uint (value, self->max_results); + break; + case PROP_TIMEOUT: + g_value_set_uint (value, self->timeout); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsPluginJob *self = GS_PLUGIN_JOB (obj); + + switch (prop_id) { + case PROP_ACTION: + gs_plugin_job_set_action (self, g_value_get_uint (value)); + break; + case PROP_AGE: + gs_plugin_job_set_age (self, g_value_get_uint64 (value)); + break; + case PROP_REFINE_FLAGS: + gs_plugin_job_set_refine_flags (self, g_value_get_uint64 (value)); + break; + case PROP_FILTER_FLAGS: + gs_plugin_job_set_filter_flags (self, g_value_get_uint64 (value)); + break; + case PROP_DEDUPE_FLAGS: + gs_plugin_job_set_dedupe_flags (self, g_value_get_uint64 (value)); + break; + case PROP_INTERACTIVE: + gs_plugin_job_set_interactive (self, g_value_get_boolean (value)); + break; + case PROP_SEARCH: + gs_plugin_job_set_search (self, g_value_get_string (value)); + break; + case PROP_APP: + gs_plugin_job_set_app (self, g_value_get_object (value)); + break; + case PROP_LIST: + gs_plugin_job_set_list (self, g_value_get_object (value)); + break; + case PROP_FILE: + gs_plugin_job_set_file (self, g_value_get_object (value)); + break; + case PROP_CATEGORY: + gs_plugin_job_set_category (self, g_value_get_object (value)); + break; + case PROP_REVIEW: + gs_plugin_job_set_review (self, g_value_get_object (value)); + break; + case PROP_MAX_RESULTS: + gs_plugin_job_set_max_results (self, g_value_get_uint (value)); + break; + case PROP_TIMEOUT: + gs_plugin_job_set_timeout (self, g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +gs_plugin_job_finalize (GObject *obj) +{ + GsPluginJob *self = GS_PLUGIN_JOB (obj); + g_free (self->search); + g_clear_object (&self->app); + g_clear_object (&self->list); + g_clear_object (&self->file); + g_clear_object (&self->plugin); + g_clear_object (&self->category); + g_clear_object (&self->review); + G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj); +} + +static void +gs_plugin_job_class_init (GsPluginJobClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + object_class->finalize = gs_plugin_job_finalize; + object_class->get_property = gs_plugin_job_get_property; + object_class->set_property = gs_plugin_job_set_property; + + pspec = g_param_spec_uint ("action", NULL, NULL, + GS_PLUGIN_ACTION_UNKNOWN, + GS_PLUGIN_ACTION_LAST, + GS_PLUGIN_ACTION_UNKNOWN, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_ACTION, pspec); + + pspec = g_param_spec_uint64 ("age", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_AGE, pspec); + + pspec = g_param_spec_uint64 ("refine-flags", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_REFINE_FLAGS, pspec); + + pspec = g_param_spec_uint64 ("filter-flags", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_FILTER_FLAGS, pspec); + + pspec = g_param_spec_uint64 ("dedupe-flags", NULL, NULL, + 0, G_MAXUINT64, 0, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_DEDUPE_FLAGS, pspec); + + pspec = g_param_spec_boolean ("interactive", NULL, NULL, + FALSE, + G_PARAM_READWRITE); + + g_object_class_install_property (object_class, PROP_INTERACTIVE, pspec); + + pspec = g_param_spec_string ("search", NULL, NULL, + NULL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_SEARCH, pspec); + + pspec = g_param_spec_object ("app", NULL, NULL, + GS_TYPE_APP, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_APP, pspec); + + pspec = g_param_spec_object ("list", NULL, NULL, + GS_TYPE_APP_LIST, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_LIST, pspec); + + pspec = g_param_spec_object ("file", NULL, NULL, + G_TYPE_FILE, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_FILE, pspec); + + pspec = g_param_spec_object ("category", NULL, NULL, + GS_TYPE_CATEGORY, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_CATEGORY, pspec); + + pspec = g_param_spec_object ("review", NULL, NULL, + AS_TYPE_REVIEW, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_REVIEW, pspec); + + pspec = g_param_spec_uint ("max-results", NULL, NULL, + 0, G_MAXUINT, 0, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_MAX_RESULTS, pspec); + + pspec = g_param_spec_uint ("timeout", NULL, NULL, + 0, G_MAXUINT, 60, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + g_object_class_install_property (object_class, PROP_TIMEOUT, pspec); +} + +static void +gs_plugin_job_init (GsPluginJob *self) +{ + self->refine_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT; + self->filter_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT; + self->dedupe_flags = GS_APP_LIST_FILTER_FLAG_KEY_ID | + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE | + GS_APP_LIST_FILTER_FLAG_KEY_VERSION; + self->list = gs_app_list_new (); + self->time_created = g_get_monotonic_time (); +} diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h new file mode 100644 index 0000000..885be1c --- /dev/null +++ b/lib/gs-plugin-job.h @@ -0,0 +1,58 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app-list-private.h" +#include "gs-category.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_JOB (gs_plugin_job_get_type ()) + +G_DECLARE_FINAL_TYPE (GsPluginJob, gs_plugin_job, GS, PLUGIN_JOB, GObject) + +void gs_plugin_job_set_refine_flags (GsPluginJob *self, + GsPluginRefineFlags refine_flags); +void gs_plugin_job_set_filter_flags (GsPluginJob *self, + GsPluginRefineFlags filter_flags); +void gs_plugin_job_set_dedupe_flags (GsPluginJob *self, + GsAppListFilterFlags dedupe_flags); +void gs_plugin_job_set_interactive (GsPluginJob *self, + gboolean interactive); +void gs_plugin_job_set_max_results (GsPluginJob *self, + guint max_results); +void gs_plugin_job_set_timeout (GsPluginJob *self, + guint timeout); +void gs_plugin_job_set_age (GsPluginJob *self, + guint64 age); +void gs_plugin_job_set_sort_func (GsPluginJob *self, + GsAppListSortFunc sort_func); +void gs_plugin_job_set_sort_func_data (GsPluginJob *self, + gpointer sort_func_data); +void gs_plugin_job_set_search (GsPluginJob *self, + const gchar *search); +void gs_plugin_job_set_app (GsPluginJob *self, + GsApp *app); +void gs_plugin_job_set_list (GsPluginJob *self, + GsAppList *list); +void gs_plugin_job_set_file (GsPluginJob *self, + GFile *file); +void gs_plugin_job_set_plugin (GsPluginJob *self, + GsPlugin *plugin); +void gs_plugin_job_set_category (GsPluginJob *self, + GsCategory *category); +void gs_plugin_job_set_review (GsPluginJob *self, + AsReview *review); + +#define gs_plugin_job_newv(a,...) GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__)) + +G_END_DECLS diff --git a/lib/gs-plugin-loader-sync.c b/lib/gs-plugin-loader-sync.c new file mode 100644 index 0000000..bf6f076 --- /dev/null +++ b/lib/gs-plugin-loader-sync.c @@ -0,0 +1,208 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2017 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gs-plugin-loader-sync.h" + +/* tiny helper to help us do the async operation */ +typedef struct { + GAsyncResult *res; + GMainContext *context; + GMainLoop *loop; +} GsPluginLoaderHelper; + +static void +_job_process_finish_sync (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsPluginLoaderHelper *helper) +{ + helper->res = g_object_ref (res); + g_main_loop_quit (helper->loop); +} + +GsAppList * +gs_plugin_loader_job_process (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + GsAppList *list; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job, + cancellable, + (GAsyncReadyCallback) _job_process_finish_sync, + &helper); + g_main_loop_run (helper.loop); + list = gs_plugin_loader_job_process_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return list; +} + +static void +_job_get_categories_finish_sync (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsPluginLoaderHelper *helper) +{ + helper->res = g_object_ref (res); + g_main_loop_quit (helper->loop); +} + +GPtrArray * +gs_plugin_loader_job_get_categories (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + GPtrArray *catlist; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_get_categories_async (plugin_loader, + plugin_job, + cancellable, + (GAsyncReadyCallback) _job_get_categories_finish_sync, + &helper); + g_main_loop_run (helper.loop); + catlist = gs_plugin_loader_job_get_categories_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return catlist; +} + +static void +_job_action_finish_sync (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GsPluginLoaderHelper *helper) +{ + helper->res = g_object_ref (res); + g_main_loop_quit (helper->loop); +} + +gboolean +gs_plugin_loader_job_action (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + gboolean ret; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job, + cancellable, + (GAsyncReadyCallback) _job_action_finish_sync, + &helper); + g_main_loop_run (helper.loop); + ret = gs_plugin_loader_job_action_finish (plugin_loader, + helper.res, + error); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return ret; +} + +static void +_job_process_app_finish_sync (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data; + + helper->res = g_object_ref (res); + g_main_loop_quit (helper->loop); +} + +GsApp * +gs_plugin_loader_job_process_app (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error) +{ + GsPluginLoaderHelper helper; + g_autoptr(GsAppList) list = NULL; + GsApp *app = NULL; + + /* create temp object */ + helper.res = NULL; + helper.context = g_main_context_new (); + helper.loop = g_main_loop_new (helper.context, FALSE); + + g_main_context_push_thread_default (helper.context); + + /* run async method */ + gs_plugin_loader_job_process_async (plugin_loader, + plugin_job, + cancellable, + _job_process_app_finish_sync, + &helper); + g_main_loop_run (helper.loop); + list = gs_plugin_loader_job_process_finish (plugin_loader, + helper.res, + error); + if (list != NULL) + app = g_object_ref (gs_app_list_index (list, 0)); + + g_main_context_pop_thread_default (helper.context); + + g_main_loop_unref (helper.loop); + g_main_context_unref (helper.context); + if (helper.res != NULL) + g_object_unref (helper.res); + + return app; +} diff --git a/lib/gs-plugin-loader-sync.h b/lib/gs-plugin-loader-sync.h new file mode 100644 index 0000000..b428ba9 --- /dev/null +++ b/lib/gs-plugin-loader-sync.h @@ -0,0 +1,34 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2007-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-plugin-loader.h" + +G_BEGIN_DECLS + +GsAppList *gs_plugin_loader_job_process (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); +GsApp *gs_plugin_loader_job_process_app (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); +gboolean gs_plugin_loader_job_action (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); +GPtrArray *gs_plugin_loader_job_get_categories (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GError **error); + +G_END_DECLS 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; +} diff --git a/lib/gs-plugin-loader.h b/lib/gs-plugin-loader.h new file mode 100644 index 0000000..f729572 --- /dev/null +++ b/lib/gs-plugin-loader.h @@ -0,0 +1,100 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2007-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +#include "gs-app.h" +#include "gs-category.h" +#include "gs-plugin-event.h" +#include "gs-plugin-private.h" +#include "gs-plugin-job.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN_LOADER (gs_plugin_loader_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsPluginLoader, gs_plugin_loader, GS, PLUGIN_LOADER, GObject) + +struct _GsPluginLoaderClass +{ + GObjectClass parent_class; + void (*status_changed) (GsPluginLoader *plugin_loader, + GsApp *app, + GsPluginStatus status); + void (*pending_apps_changed) (GsPluginLoader *plugin_loader); + void (*updates_changed) (GsPluginLoader *plugin_loader); + void (*reload) (GsPluginLoader *plugin_loader); + void (*basic_auth_start) (GsPluginLoader *plugin_loader, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data); +}; + +GsPluginLoader *gs_plugin_loader_new (void); +void gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GsAppList *gs_plugin_loader_job_process_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +gboolean gs_plugin_loader_job_action_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +void gs_plugin_loader_job_get_categories_async (GsPluginLoader *plugin_loader, + GsPluginJob *plugin_job, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +GPtrArray *gs_plugin_loader_job_get_categories_finish (GsPluginLoader *plugin_loader, + GAsyncResult *res, + GError **error); +gboolean gs_plugin_loader_setup (GsPluginLoader *plugin_loader, + gchar **allowlist, + gchar **blocklist, + GCancellable *cancellable, + GError **error); +void gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader, + const gchar *plugin_name); +void gs_plugin_loader_add_location (GsPluginLoader *plugin_loader, + const gchar *location); +guint gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader); +void gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader, + guint scale); +GsAppList *gs_plugin_loader_get_pending (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_allow_updates (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_network_available (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader); +gboolean gs_plugin_loader_get_plugin_supported (GsPluginLoader *plugin_loader, + const gchar *function_name); + +GPtrArray *gs_plugin_loader_get_events (GsPluginLoader *plugin_loader); +GsPluginEvent *gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader); +void gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader); + +GsApp *gs_plugin_loader_app_create (GsPluginLoader *plugin_loader, + const gchar *unique_id); +GsApp *gs_plugin_loader_get_system_app (GsPluginLoader *plugin_loader); + +/* only useful from the self tests */ +void gs_plugin_loader_setup_again (GsPluginLoader *plugin_loader); +void gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader); +GsPlugin *gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader, + const gchar *plugin_name); +void gs_plugin_loader_set_max_parallel_ops (GsPluginLoader *plugin_loader, + guint max_ops); + +const gchar *gs_plugin_loader_get_locale (GsPluginLoader *plugin_loader); + +G_END_DECLS diff --git a/lib/gs-plugin-private.h b/lib/gs-plugin-private.h new file mode 100644 index 0000000..1defc98 --- /dev/null +++ b/lib/gs-plugin-private.h @@ -0,0 +1,55 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <appstream-glib.h> +#include <glib-object.h> +#include <gmodule.h> +#include <libsoup/soup.h> + +#include "gs-plugin.h" + +G_BEGIN_DECLS + +GsPlugin *gs_plugin_new (void); +GsPlugin *gs_plugin_create (const gchar *filename, + GError **error); +const gchar *gs_plugin_error_to_string (GsPluginError error); +const gchar *gs_plugin_action_to_string (GsPluginAction action); +GsPluginAction gs_plugin_action_from_string (const gchar *action); +const gchar *gs_plugin_action_to_function_name (GsPluginAction action); + +void gs_plugin_clear_data (GsPlugin *plugin); +void gs_plugin_set_scale (GsPlugin *plugin, + guint scale); +guint gs_plugin_get_order (GsPlugin *plugin); +void gs_plugin_set_order (GsPlugin *plugin, + guint order); +guint gs_plugin_get_priority (GsPlugin *plugin); +void gs_plugin_set_priority (GsPlugin *plugin, + guint priority); +void gs_plugin_set_name (GsPlugin *plugin, + const gchar *name); +void gs_plugin_set_locale (GsPlugin *plugin, + const gchar *locale); +void gs_plugin_set_language (GsPlugin *plugin, + const gchar *language); +void gs_plugin_set_auth_array (GsPlugin *plugin, + GPtrArray *auth_array); +GPtrArray *gs_plugin_get_rules (GsPlugin *plugin, + GsPluginRule rule); +gpointer gs_plugin_get_symbol (GsPlugin *plugin, + const gchar *function_name); +void gs_plugin_interactive_inc (GsPlugin *plugin); +void gs_plugin_interactive_dec (GsPlugin *plugin); +gchar *gs_plugin_refine_flags_to_string (GsPluginRefineFlags refine_flags); +void gs_plugin_set_network_monitor (GsPlugin *plugin, + GNetworkMonitor *monitor); + +G_END_DECLS diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h new file mode 100644 index 0000000..b4a3e3b --- /dev/null +++ b/lib/gs-plugin-types.h @@ -0,0 +1,280 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2018 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <glib-object.h> + +G_BEGIN_DECLS + +/** + * GsPluginStatus: + * @GS_PLUGIN_STATUS_UNKNOWN: Unknown status + * @GS_PLUGIN_STATUS_WAITING: Waiting + * @GS_PLUGIN_STATUS_FINISHED: Finished + * @GS_PLUGIN_STATUS_SETUP: Setup in progress + * @GS_PLUGIN_STATUS_DOWNLOADING: Downloading in progress + * @GS_PLUGIN_STATUS_QUERYING: Querying in progress + * @GS_PLUGIN_STATUS_INSTALLING: Installing in progress + * @GS_PLUGIN_STATUS_REMOVING: Removing in progress + * + * The ststus of the plugin. + **/ +typedef enum { + GS_PLUGIN_STATUS_UNKNOWN, + GS_PLUGIN_STATUS_WAITING, + GS_PLUGIN_STATUS_FINISHED, + GS_PLUGIN_STATUS_SETUP, + GS_PLUGIN_STATUS_DOWNLOADING, + GS_PLUGIN_STATUS_QUERYING, + GS_PLUGIN_STATUS_INSTALLING, + GS_PLUGIN_STATUS_REMOVING, + /*< private >*/ + GS_PLUGIN_STATUS_LAST +} GsPluginStatus; + +/** + * GsPluginFlags: + * @GS_PLUGIN_FLAGS_NONE: No flags set + * @GS_PLUGIN_FLAGS_INTERACTIVE: User initiated the job + * + * The flags for the plugin at this point in time. + **/ +#define GS_PLUGIN_FLAGS_NONE (0u) +#define GS_PLUGIN_FLAGS_INTERACTIVE (1u << 4) +typedef guint64 GsPluginFlags; + +/** + * GsPluginError: + * @GS_PLUGIN_ERROR_FAILED: Generic failure + * @GS_PLUGIN_ERROR_NOT_SUPPORTED: Action not supported + * @GS_PLUGIN_ERROR_CANCELLED: Action was cancelled + * @GS_PLUGIN_ERROR_NO_NETWORK: No network connection available + * @GS_PLUGIN_ERROR_NO_SECURITY: Security policy forbid action + * @GS_PLUGIN_ERROR_NO_SPACE: No disk space to allow action + * @GS_PLUGIN_ERROR_AUTH_REQUIRED: Authentication was required + * @GS_PLUGIN_ERROR_AUTH_INVALID: Provided authentication was invalid + * @GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED: The plugins installed are incompatible + * @GS_PLUGIN_ERROR_DOWNLOAD_FAILED: The download action failed + * @GS_PLUGIN_ERROR_WRITE_FAILED: The save-to-disk failed + * @GS_PLUGIN_ERROR_INVALID_FORMAT: The data format is invalid + * @GS_PLUGIN_ERROR_DELETE_FAILED: The delete action failed + * @GS_PLUGIN_ERROR_RESTART_REQUIRED: A restart is required + * @GS_PLUGIN_ERROR_AC_POWER_REQUIRED: AC power is required + * @GS_PLUGIN_ERROR_TIMED_OUT: The job timed out + * @GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: The system battery level is too low + * + * The failure error types. + **/ +typedef enum { + GS_PLUGIN_ERROR_FAILED, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + GS_PLUGIN_ERROR_CANCELLED, + GS_PLUGIN_ERROR_NO_NETWORK, + GS_PLUGIN_ERROR_NO_SECURITY, + GS_PLUGIN_ERROR_NO_SPACE, + GS_PLUGIN_ERROR_AUTH_REQUIRED, + GS_PLUGIN_ERROR_AUTH_INVALID, + GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + GS_PLUGIN_ERROR_WRITE_FAILED, + GS_PLUGIN_ERROR_INVALID_FORMAT, + GS_PLUGIN_ERROR_DELETE_FAILED, + GS_PLUGIN_ERROR_RESTART_REQUIRED, + GS_PLUGIN_ERROR_AC_POWER_REQUIRED, + GS_PLUGIN_ERROR_TIMED_OUT, + GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW, + /*< private >*/ + GS_PLUGIN_ERROR_LAST +} GsPluginError; + +/** + * GsPluginRefineFlags: + * @GS_PLUGIN_REFINE_FLAGS_DEFAULT: No explicit flags set + * @GS_PLUGIN_REFINE_FLAGS_USE_HISTORY: Get the historical view (unused) + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE: Require the license + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL: Require the URL + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION: Require the long description + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE: Require the installed and download sizes + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING: Require the rating + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION: Require the version + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY: Require the history + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION: Require enough to install or remove the package + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS: Require update details + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN: Require the origin + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED: Require related packages + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH: Require the menu path + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS: Require available addons + * @GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES: Allow packages to be returned + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY: Require update severity + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED: Require distro upgrades + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE: Require the provenance + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS: Require user-reviews + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS: Require user-ratings + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS: Require the key colors + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON: Require the icon to be loaded + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS: Require the needed permissions + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME: Require the origin hostname + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI: Require the origin for UI + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME: Require the runtime + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS: Require screenshot information + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES: Require categories + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP: Require project group + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME: Require developer name + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS: Require kudos + * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING: Require content rating + * + * The refine flags. + **/ +#define GS_PLUGIN_REFINE_FLAGS_DEFAULT ((guint64) 0) +#define GS_PLUGIN_REFINE_FLAGS_USE_HISTORY ((guint64) 1 << 0) /* unused, TODO: perhaps ->STATE */ +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE ((guint64) 1 << 1) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL ((guint64) 1 << 2) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION ((guint64) 1 << 3) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE ((guint64) 1 << 4) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING ((guint64) 1 << 5) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION ((guint64) 1 << 6) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY ((guint64) 1 << 7) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION ((guint64) 1 << 8) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS ((guint64) 1 << 9) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN ((guint64) 1 << 10) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED ((guint64) 1 << 11) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH ((guint64) 1 << 12) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS ((guint64) 1 << 13) +#define GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES ((guint64) 1 << 14) /* TODO: move to request */ +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY ((guint64) 1 << 15) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED ((guint64) 1 << 16) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE ((guint64) 1 << 17) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS ((guint64) 1 << 18) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS ((guint64) 1 << 19) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS ((guint64) 1 << 20) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON ((guint64) 1 << 21) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS ((guint64) 1 << 22) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME ((guint64) 1 << 23) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI ((guint64) 1 << 24) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME ((guint64) 1 << 25) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS ((guint64) 1 << 26) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES ((guint64) 1 << 27) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP ((guint64) 1 << 28) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME ((guint64) 1 << 29) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS ((guint64) 1 << 30) +#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING ((guint64) 1 << 31) +typedef guint64 GsPluginRefineFlags; + +/** + * GsPluginRule: + * @GS_PLUGIN_RULE_CONFLICTS: The plugin conflicts with another + * @GS_PLUGIN_RULE_RUN_AFTER: Order the plugin after another + * @GS_PLUGIN_RULE_RUN_BEFORE: Order the plugin before another + * @GS_PLUGIN_RULE_BETTER_THAN: Results are better than another + * + * The rules used for ordering plugins. + * Plugins are expected to add rules in gs_plugin_initialize(). + **/ +typedef enum { + GS_PLUGIN_RULE_CONFLICTS, + GS_PLUGIN_RULE_RUN_AFTER, + GS_PLUGIN_RULE_RUN_BEFORE, + GS_PLUGIN_RULE_BETTER_THAN, + /*< private >*/ + GS_PLUGIN_RULE_LAST +} GsPluginRule; + +/** + * GsPluginAction: + * @GS_PLUGIN_ACTION_UNKNOWN: Action is unknown + * @GS_PLUGIN_ACTION_SETUP: Plugin setup (internal) + * @GS_PLUGIN_ACTION_INSTALL: Install an application + * @GS_PLUGIN_ACTION_REMOVE: Remove an application + * @GS_PLUGIN_ACTION_UPDATE: Update an application + * @GS_PLUGIN_ACTION_SET_RATING: Set rating on an application + * @GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: Download a distro upgrade + * @GS_PLUGIN_ACTION_UPGRADE_TRIGGER: Trigger a distro upgrade + * @GS_PLUGIN_ACTION_LAUNCH: Launch an application + * @GS_PLUGIN_ACTION_UPDATE_CANCEL: Cancel the update + * @GS_PLUGIN_ACTION_ADD_SHORTCUT: Add a shortcut to an application + * @GS_PLUGIN_ACTION_REMOVE_SHORTCUT: Remove a shortcut to an application + * @GS_PLUGIN_ACTION_REVIEW_SUBMIT: Submit a new review + * @GS_PLUGIN_ACTION_REVIEW_UPVOTE: Upvote an existing review + * @GS_PLUGIN_ACTION_REVIEW_DOWNVOTE: Downvote an existing review + * @GS_PLUGIN_ACTION_REVIEW_REPORT: Report an existing review + * @GS_PLUGIN_ACTION_REVIEW_REMOVE: Remove a review written by the user + * @GS_PLUGIN_ACTION_REVIEW_DISMISS: Dismiss (ignore) a review when moderating + * @GS_PLUGIN_ACTION_GET_UPDATES: Get the list of updates + * @GS_PLUGIN_ACTION_GET_DISTRO_UPDATES: Get the list of distro updates + * @GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS: Get the list of moderatable reviews + * @GS_PLUGIN_ACTION_GET_SOURCES: Get the list of sources + * @GS_PLUGIN_ACTION_GET_INSTALLED: Get the list of installed applications + * @GS_PLUGIN_ACTION_GET_POPULAR: Get the list of popular applications + * @GS_PLUGIN_ACTION_GET_FEATURED: Get the list of featured applications + * @GS_PLUGIN_ACTION_SEARCH: Get the search results for a query + * @GS_PLUGIN_ACTION_SEARCH_FILES: Get the search results for a file query + * @GS_PLUGIN_ACTION_SEARCH_PROVIDES: Get the search results for a provide query + * @GS_PLUGIN_ACTION_GET_CATEGORIES: Get the list of categories + * @GS_PLUGIN_ACTION_GET_CATEGORY_APPS: Get the apps for a specific category + * @GS_PLUGIN_ACTION_REFINE: Refine the application + * @GS_PLUGIN_ACTION_REFRESH: Refresh all the sources + * @GS_PLUGIN_ACTION_FILE_TO_APP: Convert the file to an application + * @GS_PLUGIN_ACTION_URL_TO_APP: Convert the URI to an application + * @GS_PLUGIN_ACTION_GET_RECENT: Get the apps recently released + * @GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL: Get the list of historical updates + * @GS_PLUGIN_ACTION_INITIALIZE: Initialize the plugin + * @GS_PLUGIN_ACTION_DESTROY: Destroy the plugin + * @GS_PLUGIN_ACTION_DOWNLOAD: Download an application + * @GS_PLUGIN_ACTION_GET_ALTERNATES: Get the alternates for a specific application + * @GS_PLUGIN_ACTION_GET_LANGPACKS: Get appropriate language pack + * + * The plugin action. + **/ +typedef enum { + GS_PLUGIN_ACTION_UNKNOWN, + GS_PLUGIN_ACTION_SETUP, + GS_PLUGIN_ACTION_INSTALL, + GS_PLUGIN_ACTION_REMOVE, + GS_PLUGIN_ACTION_UPDATE, + GS_PLUGIN_ACTION_SET_RATING, + GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD, + GS_PLUGIN_ACTION_UPGRADE_TRIGGER, + GS_PLUGIN_ACTION_LAUNCH, + GS_PLUGIN_ACTION_UPDATE_CANCEL, + GS_PLUGIN_ACTION_ADD_SHORTCUT, + GS_PLUGIN_ACTION_REMOVE_SHORTCUT, + GS_PLUGIN_ACTION_REVIEW_SUBMIT, + GS_PLUGIN_ACTION_REVIEW_UPVOTE, + GS_PLUGIN_ACTION_REVIEW_DOWNVOTE, + GS_PLUGIN_ACTION_REVIEW_REPORT, + GS_PLUGIN_ACTION_REVIEW_REMOVE, + GS_PLUGIN_ACTION_REVIEW_DISMISS, + GS_PLUGIN_ACTION_GET_UPDATES, + GS_PLUGIN_ACTION_GET_DISTRO_UPDATES, + GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS, + GS_PLUGIN_ACTION_GET_SOURCES, + GS_PLUGIN_ACTION_GET_INSTALLED, + GS_PLUGIN_ACTION_GET_POPULAR, + GS_PLUGIN_ACTION_GET_FEATURED, + GS_PLUGIN_ACTION_SEARCH, + GS_PLUGIN_ACTION_SEARCH_FILES, + GS_PLUGIN_ACTION_SEARCH_PROVIDES, + GS_PLUGIN_ACTION_GET_CATEGORIES, + GS_PLUGIN_ACTION_GET_CATEGORY_APPS, + GS_PLUGIN_ACTION_REFINE, + GS_PLUGIN_ACTION_REFRESH, + GS_PLUGIN_ACTION_FILE_TO_APP, + GS_PLUGIN_ACTION_URL_TO_APP, + GS_PLUGIN_ACTION_GET_RECENT, + GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL, + GS_PLUGIN_ACTION_INITIALIZE, + GS_PLUGIN_ACTION_DESTROY, + GS_PLUGIN_ACTION_DOWNLOAD, + GS_PLUGIN_ACTION_GET_ALTERNATES, + GS_PLUGIN_ACTION_GET_LANGPACKS, + /*< private >*/ + GS_PLUGIN_ACTION_LAST +} GsPluginAction; + +G_END_DECLS diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h new file mode 100644 index 0000000..05772f2 --- /dev/null +++ b/lib/gs-plugin-vfuncs.h @@ -0,0 +1,942 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2017 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +/** + * SECTION:gs-plugin-vfuncs + * @title: GsPlugin Exports + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Vfuncs that plugins can implement + */ + +#include <appstream-glib.h> +#include <glib-object.h> +#include <gmodule.h> +#include <gio/gio.h> +#include <libsoup/soup.h> + +#include "gs-app.h" +#include "gs-app-list.h" +#include "gs-category.h" + +G_BEGIN_DECLS + +/** + * gs_plugin_initialize: + * @plugin: a #GsPlugin + * + * Checks if the plugin should run, and if initializes it. If the plugin should + * not be run then gs_plugin_set_enabled() should be called. + * This is also the place to call gs_plugin_alloc_data() if private data is + * required for the plugin. + * + * NOTE: Do not do any failable actions in this function; use gs_plugin_setup() + * instead. + **/ +void gs_plugin_initialize (GsPlugin *plugin); + +/** + * gs_plugin_destroy: + * @plugin: a #GsPlugin + * + * Called when the plugin should destroy any private data. + **/ +void gs_plugin_destroy (GsPlugin *plugin); + +/** + * gs_plugin_adopt_app: + * @plugin: a #GsPlugin + * @app: a #GsApp + * + * Called when an #GsApp has not been claimed (i.e. a management plugin has not + * been set). + * + * A claimed application means other plugins will not try to perform actions + * such as install, remove or update. Most applications are claimed when they + * are created. + * + * If a plugin can adopt this application then it should call + * gs_app_set_management_plugin() on @app. + **/ +void gs_plugin_adopt_app (GsPlugin *plugin, + GsApp *app); + +/** + * gs_plugin_add_search: + * @plugin: a #GsPlugin + * @values: a NULL terminated list of search terms, e.g. [ "gnome", "software" ] + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get search results for a specific query. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_search (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_search_files: + * @plugin: a #GsPlugin + * @values: a list of filenames, e.g. [ "/usr/share/help/gimp/index.html" ] + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Called when searching for an application that provides a specific filename + * on the filesystem. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_search_files (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_search_what_provides + * @plugin: a list of tags, e.g. [ "text/rtf" ] + * @values: a #GStrv + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Called when searching for an application that provides specific defined tags, + * for instance a codec string or mime-type. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_search_what_provides (GsPlugin *plugin, + gchar **values, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_alternates + * @plugin: a #GsPlugin + * @app: a #GsApp + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Called when trying to find alternates to a specific app, for instance + * finding a flatpak version of an existing distro packaged application. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_alternates (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_setup: + * @plugin: a #GsPlugin + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Called when the plugin should set up the initial state, and with the write + * lock held. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * This function will also not be called if gs_plugin_initialize() self-disabled. + * + * Returns: %TRUE for success + **/ +gboolean gs_plugin_setup (GsPlugin *plugin, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_installed: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of installed applications. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_installed (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_updates: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of updates. + * + * NOTE: Actually downloading the updates can be done in gs_plugin_download_app() + * or in gs_plugin_download(). + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_updates (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_distro_upgrades: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of distribution upgrades. Due to the download size, these + * should not be downloaded until the user has explicitly opted-in. + * + * Plugins are expected to add new apps using gs_app_list_add() of type + * %AS_APP_KIND_OS_UPGRADE. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_distro_upgrades (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_sources: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of sources, for example the repos listed in /etc/yum.repos.d + * or the remotes configured in flatpak. + * + * Plugins are expected to add new apps using gs_app_list_add() of type + * %AS_APP_KIND_SOURCE. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_sources (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_updates_historical + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the list of historical updates, i.e. the updates that have just been + * installed. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_updates_historical (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_categories: + * @plugin: a #GsPlugin + * @list: (element-type GsCategory): a #GPtrArray + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get the category tree, for instance Games->Action or Internet->Email. + * + * Plugins are expected to add new categories using g_ptr_array_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_categories (GsPlugin *plugin, + GPtrArray *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_category_apps: + * @plugin: a #GsPlugin + * @category: a #GsCategory + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get all the applications that match a specific category. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_category_apps (GsPlugin *plugin, + GsCategory *category, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_recent: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @age: a number of seconds + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Return all the applications that have had upstream releases recently. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_recent (GsPlugin *plugin, + GsAppList *list, + guint64 age, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_popular: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get popular applications that should be featured on the main page as + * "Editors Picks". + * This is expected to be a curated list of applications that are high quality + * and feature-complete. + * + * The returned list of popular applications are not sorted, but each #GsApp has + * to be valid, for instance having a known state and a valid icon. + * If an insufficient number of applications are added by plugins then the + * section on the overview shell may be hidden. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_popular (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_featured: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Get applications that should be featured as a large full-width banner on the + * overview page. + * This is expected to be a curated list of applications that are high quality + * and feature-complete. + * + * The returned list of popular applications are randomized in a way so that + * the same application is featured for the entire calendar day. + * + * NOTE: The UI code may expect that applications have additional metadata set on + * results, for instance <code>GnomeSoftware::FeatureTile-css</code>. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_featured (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_unvoted_reviews: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Gets the list of unvoted reviews. Only applications should be returned where + * there are reviews, and where the user has not previously moderated them. + * This function is supposed to be used to display a moderation panel for + * reviewers. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_unvoted_reviews (GsPlugin *plugin, + GsAppList *list, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_refine: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @flags: a #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Adds required information to a list of #GsApp's. It allows requests to be + * batched up, which allows better performance than individual calls per app. + * + * An example for when this is useful would be in the PackageKit plugin where + * we want to do one transaction of GetDetails with multiple source-ids rather + * than scheduling a large number of pending requests. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_refine (GsPlugin *plugin, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_refine_wildcard: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @list: a #GsAppList + * @flags: a #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Adds applications that match the wildcard specified in @app. + * + * The general idea is that plugins create and add *new* applications rather + * than all trying to fight over the wildcard application. + * This allows the plugin loader to filter using the #GsApp priority value. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_refine_wildcard (GsPlugin *plugin, + GsApp *app, + GsAppList *list, + GsPluginRefineFlags flags, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_launch: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Launch the specified application using a plugin-specific method. + * This is normally setting some environment or launching a specific binary. + * + * Plugins can simply use gs_plugin_app_launch() if no plugin-specific + * functionality is required. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_launch (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_shortcut: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Adds a shortcut for the application in a desktop-defined location. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_shortcut (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_remove_shortcut: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Removes a shortcut for the application in a desktop-defined location. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_remove_shortcut (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_update_cancel: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Cancels the offline update of @app. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_update_cancel (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_install: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Install the application. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * NOTE: Once the action is complete, the plugin must set the new state of @app + * to %AS_APP_STATE_INSTALLED. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_install (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_remove: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Remove the application. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * NOTE: Once the action is complete, the plugin must set the new state of @app + * to %AS_APP_STATE_AVAILABLE or %AS_APP_STATE_UNKNOWN if not known. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_remove (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_set_rating: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Gets any ratings for the applications. + * + * Plugins are expected to call gs_app_set_rating() on @app. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_set_rating (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_update_app: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Update the application live. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * NOTE: Once the action is complete, the plugin must set the new state of @app + * to %AS_APP_STATE_INSTALLED or %AS_APP_STATE_UNKNOWN if not known. + * + * If %GS_APP_QUIRK_IS_PROXY is set on the application then the actual #GsApp + * set in @app will be the related application of the parent. Plugins do not + * need to manually iterate on the related list of applications. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_update_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_download_app: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads the application and any dependencies ready to be installed or + * updated. + * + * Plugins are expected to schedule downloads using the system download + * scheduler if appropriate (if the download is not guaranteed to be under a few + * hundred kilobytes, for example), so that the user’s metered data preferences + * are honoured. + * + * Plugins are expected to send progress notifications to the UI using + * gs_app_set_progress() using the passed in @app. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * If the @app is already downloaded, do not return an error and return %TRUE. + * + * On failure the error message returned will usually only be shown on the + * console, but they can also be retrieved using gs_plugin_loader_get_events(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_download_app (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_download: + * @plugin: a #GsPlugin + * @apps: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads a list of applications ready to be installed or updated. + * + * Plugins are expected to schedule downloads using the system download + * scheduler if appropriate (if the download is not guaranteed to be under a few + * hundred kilobytes, for example), so that the user’s metered data preferences + * are honoured. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_download (GsPlugin *plugin, + GsAppList *apps, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_upgrade_download: + * @plugin: a #GsPlugin + * @app: a #GsApp, with kind %AS_APP_KIND_OS_UPGRADE + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Starts downloading a distribution upgrade in the background. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_upgrade_download (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_app_upgrade_trigger: + * @plugin: a #GsPlugin + * @app: a #GsApp, with kind %AS_APP_KIND_OS_UPGRADE + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Triggers the distribution upgrade to be installed on next boot. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_app_upgrade_trigger (GsPlugin *plugin, + GsApp *app, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_review_submit: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @review: a #AsReview + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Submits a new end-user application review. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_review_submit (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_review_upvote: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @review: a #AsReview + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Upvote a specific review to indicate the review is helpful. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_review_upvote (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_review_downvote: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @review: a #AsReview + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downvote a specific review to indicate the review is unhelpful. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_review_downvote (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_review_report: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @review: a #AsReview + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Report a review that is not suitable in some way. + * It is expected that this action flags a review to be checked by a moderator + * and that the review won't be shown to any users until this happens. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_review_report (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_review_remove: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @review: a #AsReview + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Remove a review that the user wrote. + * NOTE: Users should only be able to remove reviews with %AS_REVIEW_FLAG_SELF. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_review_remove (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_review_dismiss: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @review: a #AsReview + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Dismisses a review, i.e. hide it from future moderated views. + * This action is useful when the moderator is unable to speak the language of + * the review for example. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_review_dismiss (GsPlugin *plugin, + GsApp *app, + AsReview *review, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_refresh: + * @plugin: a #GsPlugin + * @cache_age: the acceptable cache age in seconds, or MAXUINT for "any" + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Refreshes the state of all the plugins. Plugins should make sure + * there's enough metadata to start the application, for example lists of + * available applications. + * + * All functions can block, but should sent progress notifications, e.g. using + * gs_app_set_progress() if they will take more than tens of milliseconds + * to complete. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_refresh (GsPlugin *plugin, + guint cache_age, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_file_to_app: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @file: a #GFile + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Converts a local file to a #GsApp. It's expected that only one plugin will + * match the mimetype of @file and that a single #GsApp will be in the returned + * list. If no plugins can handle the file, the list will be empty. + * + * For example, the PackageKit plugin can turn a .rpm file into a application + * of kind %AS_APP_KIND_UNKNOWN but that in some cases it will be further refined + * into a %AS_APP_KIND_DESKTOP (with all the extra metadata) by the appstream + * plugin. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_file_to_app (GsPlugin *plugin, + GsAppList *list, + GFile *file, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_url_to_app: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @url: a #URL, e.g. "apt://gimp" + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Converts a URL to a #GsApp. It's expected that only one plugin will + * match the scheme of @url and that a single #GsApp will be in the returned + * list. If no plugins can handle the file, the list will be empty. + * + * For example, the apt plugin can turn apt://gimp into a application. + * + * Plugins are expected to add new apps using gs_app_list_add(). + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_url_to_app (GsPlugin *plugin, + GsAppList *list, + const gchar *url, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_update: + * @plugin: a #GsPlugin + * @apps: a #GsAppList + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Updates a list of applications, typically scheduling them for offline update. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_update (GsPlugin *plugin, + GsAppList *apps, + GCancellable *cancellable, + GError **error); + +/** + * gs_plugin_add_langpacks: + * @plugin: a #GsPlugin + * @list: a #GsAppList + * @locale: a #LANGUAGE_CODE or #LOCALE, e.g. "ja" or "ja_JP" + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Returns a list of language packs, as per input language code or locale. + * + * Returns: %TRUE for success or if not relevant + **/ +gboolean gs_plugin_add_langpacks (GsPlugin *plugin, + GsAppList *list, + const gchar *locale, + GCancellable *cancellable, + GError **error); + +G_END_DECLS diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c new file mode 100644 index 0000000..c517de0 --- /dev/null +++ b/lib/gs-plugin.c @@ -0,0 +1,2069 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-plugin + * @title: GsPlugin Helpers + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Runtime-loaded modules providing functionality + * + * Plugins are modules that are loaded at runtime to provide information + * about requests and to service user actions like installing, removing + * and updating. + * This allows different distributions to pick and choose how the + * application installer gathers data. + * + * Plugins also have a priority system where the largest number gets + * run first. That means if one plugin requires some property or + * metadata set by another plugin then it **must** depend on the other + * plugin to be run in the correct order. + * + * As a general rule, try to make plugins as small and self-contained + * as possible and remember to cache as much data as possible for speed. + * Memory is cheap, time less so. + */ + +#include "config.h" + +#include <gio/gdesktopappinfo.h> +#include <gdk/gdk.h> +#include <string.h> + +#ifdef USE_VALGRIND +#include <valgrind.h> +#endif + +#include "gs-app-list-private.h" +#include "gs-os-release.h" +#include "gs-plugin-private.h" +#include "gs-plugin.h" +#include "gs-utils.h" + +typedef struct +{ + GHashTable *cache; + GMutex cache_mutex; + GModule *module; + GsPluginData *data; /* for gs-plugin-{name}.c */ + GsPluginFlags flags; + SoupSession *soup_session; + GPtrArray *rules[GS_PLUGIN_RULE_LAST]; + GHashTable *vfuncs; /* string:pointer */ + GMutex vfuncs_mutex; + gboolean enabled; + guint interactive_cnt; + GMutex interactive_mutex; + gchar *locale; /* allow-none */ + gchar *language; /* allow-none */ + gchar *name; + gchar *appstream_id; + guint scale; + guint order; + guint priority; + guint timer_id; + GMutex timer_mutex; + GNetworkMonitor *network_monitor; +} GsPluginPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (GsPlugin, gs_plugin, G_TYPE_OBJECT) + +G_DEFINE_QUARK (gs-plugin-error-quark, gs_plugin_error) + +enum { + PROP_0, + PROP_FLAGS, + PROP_LAST +}; + +enum { + SIGNAL_UPDATES_CHANGED, + SIGNAL_STATUS_CHANGED, + SIGNAL_RELOAD, + SIGNAL_REPORT_EVENT, + SIGNAL_ALLOW_UPDATES, + SIGNAL_BASIC_AUTH_START, + SIGNAL_LAST +}; + +static guint signals [SIGNAL_LAST] = { 0 }; + +typedef const gchar **(*GsPluginGetDepsFunc) (GsPlugin *plugin); + +/** + * gs_plugin_status_to_string: + * @status: a #GsPluginStatus, e.g. %GS_PLUGIN_STATUS_DOWNLOADING + * + * Converts the #GsPluginStatus enum to a string. + * + * Returns: the string representation, or "unknown" + * + * Since: 3.22 + **/ +const gchar * +gs_plugin_status_to_string (GsPluginStatus status) +{ + if (status == GS_PLUGIN_STATUS_WAITING) + return "waiting"; + if (status == GS_PLUGIN_STATUS_FINISHED) + return "finished"; + if (status == GS_PLUGIN_STATUS_SETUP) + return "setup"; + if (status == GS_PLUGIN_STATUS_DOWNLOADING) + return "downloading"; + if (status == GS_PLUGIN_STATUS_QUERYING) + return "querying"; + if (status == GS_PLUGIN_STATUS_INSTALLING) + return "installing"; + if (status == GS_PLUGIN_STATUS_REMOVING) + return "removing"; + return "unknown"; +} + +/** + * gs_plugin_set_name: + * @plugin: a #GsPlugin + * @name: a plugin name + * + * Sets the name of the plugin. + * + * Plugins are not required to set the plugin name as it is automatically set + * from the `.so` filename. + * + * Since: 3.26 + **/ +void +gs_plugin_set_name (GsPlugin *plugin, const gchar *name) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + if (priv->name != NULL) + g_free (priv->name); + priv->name = g_strdup (name); +} + +/** + * gs_plugin_create: + * @filename: an absolute filename + * @error: a #GError, or %NULL + * + * Creates a new plugin from an external module. + * + * Returns: the #GsPlugin or %NULL + * + * Since: 3.22 + **/ +GsPlugin * +gs_plugin_create (const gchar *filename, GError **error) +{ + GsPlugin *plugin = NULL; + GsPluginPrivate *priv; + g_autofree gchar *basename = NULL; + + /* get the plugin name from the basename */ + basename = g_path_get_basename (filename); + if (!g_str_has_prefix (basename, "libgs_plugin_")) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "plugin filename has wrong prefix: %s", + filename); + return NULL; + } + g_strdelimit (basename, ".", '\0'); + + /* create new plugin */ + plugin = gs_plugin_new (); + priv = gs_plugin_get_instance_private (plugin); + priv->module = g_module_open (filename, 0); + if (priv->module == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "failed to open plugin %s: %s", + filename, g_module_error ()); + return NULL; + } + gs_plugin_set_name (plugin, basename + 13); + return plugin; +} + +static void +gs_plugin_finalize (GObject *object) +{ + GsPlugin *plugin = GS_PLUGIN (object); + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + guint i; + + for (i = 0; i < GS_PLUGIN_RULE_LAST; i++) + g_ptr_array_unref (priv->rules[i]); + + if (priv->timer_id > 0) + g_source_remove (priv->timer_id); + g_free (priv->name); + g_free (priv->appstream_id); + g_free (priv->data); + g_free (priv->locale); + g_free (priv->language); + if (priv->soup_session != NULL) + g_object_unref (priv->soup_session); + if (priv->network_monitor != NULL) + g_object_unref (priv->network_monitor); + g_hash_table_unref (priv->cache); + g_hash_table_unref (priv->vfuncs); + g_mutex_clear (&priv->cache_mutex); + g_mutex_clear (&priv->interactive_mutex); + g_mutex_clear (&priv->timer_mutex); + g_mutex_clear (&priv->vfuncs_mutex); +#ifndef RUNNING_ON_VALGRIND + if (priv->module != NULL) + g_module_close (priv->module); +#endif + + G_OBJECT_CLASS (gs_plugin_parent_class)->finalize (object); +} + +/** + * gs_plugin_get_data: + * @plugin: a #GsPlugin + * + * Gets the private data for the plugin if gs_plugin_alloc_data() has + * been called. + * + * Returns: the #GsPluginData, or %NULL + * + * Since: 3.22 + **/ +GsPluginData * +gs_plugin_get_data (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_assert (priv->data != NULL); + return priv->data; +} + +/** + * gs_plugin_alloc_data: + * @plugin: a #GsPlugin + * @sz: the size of data to allocate, e.g. `sizeof(FooPluginPrivate)` + * + * Allocates a private data area for the plugin which can be retrieved + * using gs_plugin_get_data(). + * This is normally called in gs_plugin_initialize() and the data should + * not be manually freed. + * + * Returns: the #GsPluginData, cleared to NUL bytes + * + * Since: 3.22 + **/ +GsPluginData * +gs_plugin_alloc_data (GsPlugin *plugin, gsize sz) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_assert (priv->data == NULL); + priv->data = g_malloc0 (sz); + return priv->data; +} + +/** + * gs_plugin_clear_data: + * @plugin: a #GsPlugin + * + * Clears and resets the private data. Only run this from the self tests. + **/ +void +gs_plugin_clear_data (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + if (priv->data == NULL) + return; + g_clear_pointer (&priv->data, g_free); +} + +/** + * gs_plugin_get_symbol: (skip) + * @plugin: a #GsPlugin + * @function_name: a symbol name + * + * Gets the symbol from the module that backs the plugin. If the plugin is not + * enabled then no symbol is returned. + * + * Returns: the pointer to the symbol, or %NULL + * + * Since: 3.22 + **/ +gpointer +gs_plugin_get_symbol (GsPlugin *plugin, const gchar *function_name) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + gpointer func = NULL; + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->vfuncs_mutex); + + g_return_val_if_fail (function_name != NULL, NULL); + + /* disabled plugins shouldn't be checked */ + if (!priv->enabled) + return NULL; + + /* look up the symbol from the cache */ + if (g_hash_table_lookup_extended (priv->vfuncs, function_name, NULL, &func)) + return func; + + /* look up the symbol using the elf headers */ + g_module_symbol (priv->module, function_name, &func); + g_hash_table_insert (priv->vfuncs, g_strdup (function_name), func); + + return func; +} + +/** + * gs_plugin_get_enabled: + * @plugin: a #GsPlugin + * + * Gets if the plugin is enabled. + * + * Returns: %TRUE if enabled + * + * Since: 3.22 + **/ +gboolean +gs_plugin_get_enabled (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->enabled; +} + +/** + * gs_plugin_set_enabled: + * @plugin: a #GsPlugin + * @enabled: the enabled state + * + * Enables or disables a plugin. + * This is normally only called from gs_plugin_initialize(). + * + * Since: 3.22 + **/ +void +gs_plugin_set_enabled (GsPlugin *plugin, gboolean enabled) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + priv->enabled = enabled; +} + +void +gs_plugin_interactive_inc (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->interactive_mutex); + priv->interactive_cnt++; + gs_plugin_add_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); +} + +void +gs_plugin_interactive_dec (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->interactive_mutex); + if (priv->interactive_cnt > 0) + priv->interactive_cnt--; + if (priv->interactive_cnt == 0) + gs_plugin_remove_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE); +} + +/** + * gs_plugin_get_name: + * @plugin: a #GsPlugin + * + * Gets the plugin name. + * + * Returns: a string, e.g. "fwupd" + * + * Since: 3.22 + **/ +const gchar * +gs_plugin_get_name (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->name; +} + +/** + * gs_plugin_get_appstream_id: + * @plugin: a #GsPlugin + * + * Gets the plugin AppStream ID. + * + * Returns: a string, e.g. `org.gnome.Software.Plugin.Epiphany` + * + * Since: 3.24 + **/ +const gchar * +gs_plugin_get_appstream_id (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->appstream_id; +} + +/** + * gs_plugin_set_appstream_id: + * @plugin: a #GsPlugin + * @appstream_id: an appstream ID, e.g. `org.gnome.Software.Plugin.Epiphany` + * + * Sets the plugin AppStream ID. + * + * Since: 3.24 + **/ +void +gs_plugin_set_appstream_id (GsPlugin *plugin, const gchar *appstream_id) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_free (priv->appstream_id); + priv->appstream_id = g_strdup (appstream_id); +} + +/** + * gs_plugin_get_scale: + * @plugin: a #GsPlugin + * + * Gets the window scale factor. + * + * Returns: the factor, usually 1 for standard screens or 2 for HiDPI + * + * Since: 3.22 + **/ +guint +gs_plugin_get_scale (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->scale; +} + +/** + * gs_plugin_set_scale: + * @plugin: a #GsPlugin + * @scale: the window scale factor, usually 1 for standard screens or 2 for HiDPI + * + * Sets the window scale factor. + * + * Since: 3.22 + **/ +void +gs_plugin_set_scale (GsPlugin *plugin, guint scale) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + priv->scale = scale; +} + +/** + * gs_plugin_get_order: + * @plugin: a #GsPlugin + * + * Gets the plugin order, where higher numbers are run after lower + * numbers. + * + * Returns: the integer value + * + * Since: 3.22 + **/ +guint +gs_plugin_get_order (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->order; +} + +/** + * gs_plugin_set_order: + * @plugin: a #GsPlugin + * @order: a integer value + * + * Sets the plugin order, where higher numbers are run after lower + * numbers. + * + * Since: 3.22 + **/ +void +gs_plugin_set_order (GsPlugin *plugin, guint order) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + priv->order = order; +} + +/** + * gs_plugin_get_priority: + * @plugin: a #GsPlugin + * + * Gets the plugin priority, where higher values will be chosen where + * multiple #GsApp's match a specific rule. + * + * Returns: the integer value + * + * Since: 3.22 + **/ +guint +gs_plugin_get_priority (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->priority; +} + +/** + * gs_plugin_set_priority: + * @plugin: a #GsPlugin + * @priority: a integer value + * + * Sets the plugin priority, where higher values will be chosen where + * multiple #GsApp's match a specific rule. + * + * Since: 3.22 + **/ +void +gs_plugin_set_priority (GsPlugin *plugin, guint priority) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + priv->priority = priority; +} + +/** + * gs_plugin_get_locale: + * @plugin: a #GsPlugin + * + * Gets the user locale. This is in the form documented in `man 3 setlocale`: + * ``` + * language[_territory][.codeset][@modifier] + * ``` + * where `language` is an + * [ISO 639 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes), + * `territory` is an + * [ISO 3166 country code](https://en.wikipedia.org/wiki/ISO_3166-1), and + * `codeset` is a character set or encoding identifier like `ISO-8859-1` or + * `UTF-8`. For a list of all supported locales, run `locale -a`. + * + * Returns: the locale string, e.g. `en_GB` or `uz_UZ.utf8@cyrillic` + * + * Since: 3.22 + **/ +const gchar * +gs_plugin_get_locale (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->locale; +} + +/** + * gs_plugin_get_language: + * @plugin: a #GsPlugin + * + * Gets the user language from the locale. This is the first component of the + * locale. + * + * Typically you should use the full locale rather than the language, as the + * same language can be used quite differently in different territories. + * + * Returns: the language string, e.g. `fr` + * + * Since: 3.22 + **/ +const gchar * +gs_plugin_get_language (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->language; +} + +/** + * gs_plugin_set_locale: + * @plugin: a #GsPlugin + * @locale: a locale string, e.g. "en_GB" + * + * Sets the plugin locale. + * + * Since: 3.22 + **/ +void +gs_plugin_set_locale (GsPlugin *plugin, const gchar *locale) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_free (priv->locale); + priv->locale = g_strdup (locale); +} + +/** + * gs_plugin_set_language: + * @plugin: a #GsPlugin + * @language: a language string, e.g. "fr" + * + * Sets the plugin language. + * + * Since: 3.22 + **/ +void +gs_plugin_set_language (GsPlugin *plugin, const gchar *language) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_free (priv->language); + priv->language = g_strdup (language); +} + +/** + * gs_plugin_get_soup_session: + * @plugin: a #GsPlugin + * + * Gets the soup session that this plugin can use when downloading. + * + * Returns: the #SoupSession + * + * Since: 3.22 + **/ +SoupSession * +gs_plugin_get_soup_session (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->soup_session; +} + +/** + * gs_plugin_set_soup_session: + * @plugin: a #GsPlugin + * @soup_session: a #SoupSession + * + * Sets the soup session that this plugin will use when downloading. + * + * Since: 3.22 + **/ +void +gs_plugin_set_soup_session (GsPlugin *plugin, SoupSession *soup_session) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_set_object (&priv->soup_session, soup_session); +} + +/** + * gs_plugin_set_network_monitor: + * @plugin: a #GsPlugin + * @monitor: a #GNetworkMonitor + * + * Sets the network monitor so that plugins can check the state of the network. + * + * Since: 3.28 + **/ +void +gs_plugin_set_network_monitor (GsPlugin *plugin, GNetworkMonitor *monitor) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_set_object (&priv->network_monitor, monitor); +} + +/** + * gs_plugin_get_network_available: + * @plugin: a #GsPlugin + * + * Gets whether a network connectivity is available. + * + * Returns: %TRUE if a network is available. + * + * Since: 3.28 + **/ +gboolean +gs_plugin_get_network_available (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + 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); +} + +/** + * gs_plugin_has_flags: + * @plugin: a #GsPlugin + * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF + * + * Finds out if a plugin has a specific flag set. + * + * Returns: TRUE if the flag is set + * + * Since: 3.22 + **/ +gboolean +gs_plugin_has_flags (GsPlugin *plugin, GsPluginFlags flags) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return (priv->flags & flags) > 0; +} + +/** + * gs_plugin_add_flags: + * @plugin: a #GsPlugin + * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF + * + * Adds specific flags to the plugin. + * + * Since: 3.22 + **/ +void +gs_plugin_add_flags (GsPlugin *plugin, GsPluginFlags flags) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + priv->flags |= flags; +} + +/** + * gs_plugin_remove_flags: + * @plugin: a #GsPlugin + * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF + * + * Removes specific flags from the plugin. + * + * Since: 3.22 + **/ +void +gs_plugin_remove_flags (GsPlugin *plugin, GsPluginFlags flags) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + priv->flags &= ~flags; +} + +/** + * gs_plugin_add_rule: + * @plugin: a #GsPlugin + * @rule: a #GsPluginRule, e.g. %GS_PLUGIN_RULE_CONFLICTS + * @name: a plugin name, e.g. "appstream" + * + * If the plugin name is found, the rule will be used to sort the plugin list, + * for example the plugin specified by @name will be ordered after this plugin + * when %GS_PLUGIN_RULE_RUN_AFTER is used. + * + * NOTE: The depsolver is iterative and may not solve overly-complicated rules; + * If depsolving fails then gnome-software will not start. + * + * Since: 3.22 + **/ +void +gs_plugin_add_rule (GsPlugin *plugin, GsPluginRule rule, const gchar *name) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_ptr_array_add (priv->rules[rule], g_strdup (name)); +} + +/** + * gs_plugin_get_rules: + * @plugin: a #GsPlugin + * @rule: a #GsPluginRule, e.g. %GS_PLUGIN_RULE_CONFLICTS + * + * Gets the plugin IDs that should be run after this plugin. + * + * Returns: (element-type utf8) (transfer none): the list of plugin names, e.g. ['appstream'] + * + * Since: 3.22 + **/ +GPtrArray * +gs_plugin_get_rules (GsPlugin *plugin, GsPluginRule rule) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + return priv->rules[rule]; +} + +/** + * gs_plugin_check_distro_id: + * @plugin: a #GsPlugin + * @distro_id: a distro ID, e.g. "fedora" + * + * Checks if the distro is compatible. + * + * Returns: %TRUE if compatible + * + * Since: 3.22 + **/ +gboolean +gs_plugin_check_distro_id (GsPlugin *plugin, const gchar *distro_id) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + const gchar *id = NULL; + + /* load /etc/os-release */ + os_release = gs_os_release_new (&error); + if (os_release == NULL) { + g_debug ("could not parse os-release: %s", error->message); + return FALSE; + } + + /* check that we are running on Fedora */ + id = gs_os_release_get_id (os_release); + if (id == NULL) { + g_debug ("could not get distro ID"); + return FALSE; + } + if (g_strcmp0 (id, distro_id) != 0) + return FALSE; + return TRUE; +} + +typedef struct { + GsPlugin *plugin; + GsApp *app; + GsPluginStatus status; + guint percentage; +} GsPluginStatusHelper; + +static gboolean +gs_plugin_status_update_cb (gpointer user_data) +{ + GsPluginStatusHelper *helper = (GsPluginStatusHelper *) user_data; + g_signal_emit (helper->plugin, + signals[SIGNAL_STATUS_CHANGED], 0, + helper->app, + helper->status); + if (helper->app != NULL) + g_object_unref (helper->app); + g_slice_free (GsPluginStatusHelper, helper); + return FALSE; +} + +/** + * gs_plugin_status_update: + * @plugin: a #GsPlugin + * @app: a #GsApp, or %NULL + * @status: a #GsPluginStatus, e.g. %GS_PLUGIN_STATUS_DOWNLOADING + * + * Update the state of the plugin so any UI can be updated. + * + * Since: 3.22 + **/ +void +gs_plugin_status_update (GsPlugin *plugin, GsApp *app, GsPluginStatus status) +{ + GsPluginStatusHelper *helper; + g_autoptr(GSource) idle_source = NULL; + + helper = g_slice_new0 (GsPluginStatusHelper); + helper->plugin = plugin; + helper->status = status; + if (app != NULL) + helper->app = g_object_ref (app); + idle_source = g_idle_source_new (); + g_source_set_callback (idle_source, gs_plugin_status_update_cb, helper, NULL); + g_source_attach (idle_source, NULL); +} + +typedef struct { + GsPlugin *plugin; + gchar *remote; + gchar *realm; + GCallback callback; + gpointer user_data; +} GsPluginBasicAuthHelper; + +static gboolean +gs_plugin_basic_auth_start_cb (gpointer user_data) +{ + GsPluginBasicAuthHelper *helper = user_data; + g_signal_emit (helper->plugin, + signals[SIGNAL_BASIC_AUTH_START], 0, + helper->remote, + helper->realm, + helper->callback, + helper->user_data); + g_free (helper->remote); + g_free (helper->realm); + g_slice_free (GsPluginBasicAuthHelper, helper); + return FALSE; +} + +/** + * gs_plugin_basic_auth_start: + * @plugin: a #GsPlugin + * @remote: a string + * @realm: a string + * @callback: callback to invoke to submit the user/password + * @user_data: callback data to pass to the callback + * + * Emit the basic-auth-start signal in the main thread. + * + * Since: 3.38 + **/ +void +gs_plugin_basic_auth_start (GsPlugin *plugin, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data) +{ + GsPluginBasicAuthHelper *helper; + g_autoptr(GSource) idle_source = NULL; + + helper = g_slice_new0 (GsPluginBasicAuthHelper); + helper->plugin = plugin; + helper->remote = g_strdup (remote); + helper->realm = g_strdup (realm); + helper->callback = callback; + helper->user_data = user_data; + + idle_source = g_idle_source_new (); + g_source_set_callback (idle_source, gs_plugin_basic_auth_start_cb, helper, NULL); + g_source_attach (idle_source, NULL); +} + +static gboolean +gs_plugin_app_launch_cb (gpointer user_data) +{ + GAppInfo *appinfo = (GAppInfo *) user_data; + GdkDisplay *display; + g_autoptr(GAppLaunchContext) context = NULL; + g_autoptr(GError) error = NULL; + + display = gdk_display_get_default (); + context = G_APP_LAUNCH_CONTEXT (gdk_display_get_app_launch_context (display)); + if (!g_app_info_launch (appinfo, NULL, context, &error)) + g_warning ("Failed to launch: %s", error->message); + + return FALSE; +} + +/** + * gs_plugin_app_launch: + * @plugin: a #GsPlugin + * @app: a #GsApp + * @error: a #GError, or %NULL + * + * Launches the application using #GAppInfo. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_plugin_app_launch (GsPlugin *plugin, GsApp *app, GError **error) +{ + const gchar *desktop_id; + g_autoptr(GAppInfo) appinfo = NULL; + + desktop_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID); + if (desktop_id == NULL) + desktop_id = gs_app_get_id (app); + if (desktop_id == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no such desktop file: %s", + desktop_id); + return FALSE; + } + appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id)); + if (appinfo == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no such desktop file: %s", + desktop_id); + return FALSE; + } + g_idle_add_full (G_PRIORITY_DEFAULT, + gs_plugin_app_launch_cb, + g_object_ref (appinfo), + (GDestroyNotify) g_object_unref); + return TRUE; +} + +static gboolean +gs_plugin_updates_changed_cb (gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + g_signal_emit (plugin, signals[SIGNAL_UPDATES_CHANGED], 0); + return FALSE; +} + +/** + * gs_plugin_updates_changed: + * @plugin: a #GsPlugin + * + * Emit a signal that tells the plugin loader that the list of updates + * may have changed. + * + * Since: 3.22 + **/ +void +gs_plugin_updates_changed (GsPlugin *plugin) +{ + g_idle_add (gs_plugin_updates_changed_cb, plugin); +} + +static gboolean +gs_plugin_reload_cb (gpointer user_data) +{ + GsPlugin *plugin = GS_PLUGIN (user_data); + g_signal_emit (plugin, signals[SIGNAL_RELOAD], 0); + return FALSE; +} + +/** + * gs_plugin_reload: + * @plugin: a #GsPlugin + * + * Plugins that call this function should expect that all panels will + * reload after a small delay, causing mush flashing, wailing and + * gnashing of teeth. + * + * Plugins should not call this unless absolutely required. + * + * Since: 3.22 + **/ +void +gs_plugin_reload (GsPlugin *plugin) +{ + g_debug ("emitting ::reload in idle"); + g_idle_add (gs_plugin_reload_cb, plugin); +} + +typedef struct { + GsPlugin *plugin; + GsApp *app; + GCancellable *cancellable; +} GsPluginDownloadHelper; + +static void +gs_plugin_download_chunk_cb (SoupMessage *msg, SoupBuffer *chunk, + GsPluginDownloadHelper *helper) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (helper->plugin); + guint percentage; + goffset header_size; + goffset body_length; + + /* cancelled? */ + if (g_cancellable_is_cancelled (helper->cancellable)) { + g_debug ("cancelling download of %s", + gs_app_get_id (helper->app)); + soup_session_cancel_message (priv->soup_session, + msg, + SOUP_STATUS_CANCELLED); + return; + } + + /* if it's returning "Found" or an error, ignore the percentage */ + if (msg->status_code != SOUP_STATUS_OK) { + g_debug ("ignoring status code %u (%s)", + msg->status_code, msg->reason_phrase); + return; + } + + /* get data */ + body_length = msg->response_body->length; + header_size = soup_message_headers_get_content_length (msg->response_headers); + + /* size is not known */ + if (header_size < body_length) + return; + + /* calculate percentage */ + percentage = (guint) ((100 * body_length) / header_size); + g_debug ("%s progress: %u%%", gs_app_get_id (helper->app), percentage); + gs_app_set_progress (helper->app, percentage); + gs_plugin_status_update (helper->plugin, + helper->app, + GS_PLUGIN_STATUS_DOWNLOADING); +} + +/** + * gs_plugin_download_data: + * @plugin: a #GsPlugin + * @app: a #GsApp, or %NULL + * @uri: a remote URI + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads data. + * + * Returns: the downloaded data, or %NULL + * + * Since: 3.22 + **/ +GBytes * +gs_plugin_download_data (GsPlugin *plugin, + GsApp *app, + const gchar *uri, + GCancellable *cancellable, + GError **error) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + GsPluginDownloadHelper helper; + guint status_code; + g_autoptr(SoupMessage) msg = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); + g_return_val_if_fail (uri != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* local */ + if (g_str_has_prefix (uri, "file://")) { + gsize length = 0; + g_autofree gchar *contents = NULL; + g_autoptr(GError) error_local = NULL; + g_debug ("copying %s from plugin %s", uri, priv->name); + if (!g_file_get_contents (uri + 7, &contents, &length, &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed to copy %s: %s", + uri, error_local->message); + return NULL; + } + return g_bytes_new (contents, length); + } + + /* remote */ + g_debug ("downloading %s from plugin %s", uri, priv->name); + msg = soup_message_new (SOUP_METHOD_GET, uri); + if (app != NULL) { + helper.plugin = plugin; + helper.app = app; + helper.cancellable = cancellable; + g_signal_connect (msg, "got-chunk", + G_CALLBACK (gs_plugin_download_chunk_cb), + &helper); + } + status_code = soup_session_send_message (priv->soup_session, msg); + if (status_code != SOUP_STATUS_OK) { + g_autoptr(GString) str = g_string_new (NULL); + g_string_append (str, soup_status_get_phrase (status_code)); + if (msg->response_body->data != NULL) { + g_string_append (str, ": "); + g_string_append (str, msg->response_body->data); + } + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed to download %s: %s", + uri, str->str); + return NULL; + } + return g_bytes_new (msg->response_body->data, + (gsize) msg->response_body->length); +} + +/** + * gs_plugin_download_file: + * @plugin: a #GsPlugin + * @app: a #GsApp, or %NULL + * @uri: a remote URI + * @filename: a local filename + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads data and saves it to a file. + * + * Returns: %TRUE for success + * + * Since: 3.22 + **/ +gboolean +gs_plugin_download_file (GsPlugin *plugin, + GsApp *app, + const gchar *uri, + const gchar *filename, + GCancellable *cancellable, + GError **error) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + GsPluginDownloadHelper helper; + guint status_code; + g_autoptr(GError) error_local = NULL; + g_autoptr(SoupMessage) msg = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE); + g_return_val_if_fail (uri != NULL, FALSE); + g_return_val_if_fail (filename != NULL, FALSE); + g_return_val_if_fail (error == NULL || *error == NULL, FALSE); + + /* local */ + if (g_str_has_prefix (uri, "file://")) { + gsize length = 0; + g_autofree gchar *contents = NULL; + g_debug ("copying %s from plugin %s", uri, priv->name); + if (!g_file_get_contents (uri + 7, &contents, &length, &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed to copy %s: %s", + uri, error_local->message); + return FALSE; + } + if (!g_file_set_contents (filename, contents, length, &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_WRITE_FAILED, + "Failed to save file: %s", + error_local->message); + return FALSE; + } + return TRUE; + } + + /* remote */ + g_debug ("downloading %s to %s from plugin %s", uri, filename, priv->name); + msg = soup_message_new (SOUP_METHOD_GET, uri); + if (msg == NULL) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed to parse URI %s", uri); + return FALSE; + } + if (app != NULL) { + helper.plugin = plugin; + helper.app = app; + helper.cancellable = cancellable; + g_signal_connect (msg, "got-chunk", + G_CALLBACK (gs_plugin_download_chunk_cb), + &helper); + } + status_code = soup_session_send_message (priv->soup_session, msg); + if (status_code != SOUP_STATUS_OK) { + g_autoptr(GString) str = g_string_new (NULL); + g_string_append (str, soup_status_get_phrase (status_code)); + if (msg->response_body->data != NULL) { + g_string_append (str, ": "); + g_string_append (str, msg->response_body->data); + } + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed to download %s: %s", + uri, str->str); + return FALSE; + } + if (!gs_mkdir_parent (filename, error)) + return FALSE; + if (!g_file_set_contents (filename, + msg->response_body->data, + msg->response_body->length, + &error_local)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_WRITE_FAILED, + "Failed to save file: %s", + error_local->message); + return FALSE; + } + return TRUE; +} + +static gchar * +gs_plugin_download_rewrite_resource_uri (GsPlugin *plugin, + GsApp *app, + const gchar *uri, + GCancellable *cancellable, + GError **error) +{ + g_autofree gchar *cachefn = NULL; + + /* local files */ + if (g_str_has_prefix (uri, "file://")) + uri += 7; + if (g_str_has_prefix (uri, "/")) { + if (!g_file_test (uri, G_FILE_TEST_EXISTS)) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "Failed to find file: %s", uri); + return NULL; + } + return g_strdup (uri); + } + + /* get cache location */ + cachefn = gs_utils_get_cache_filename ("cssresource", uri, + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_USE_HASH, + error); + if (cachefn == NULL) + return NULL; + + /* already exists */ + if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) + return g_steal_pointer (&cachefn); + + /* download */ + if (!gs_plugin_download_file (plugin, app, uri, cachefn, + cancellable, error)) { + return NULL; + } + return g_steal_pointer (&cachefn); +} + +/** + * gs_plugin_download_rewrite_resource: + * @plugin: a #GsPlugin + * @app: a #GsApp, or %NULL + * @resource: the CSS resource + * @cancellable: a #GCancellable, or %NULL + * @error: a #GError, or %NULL + * + * Downloads remote assets and rewrites a CSS resource to use cached local URIs. + * + * Returns: %TRUE for success + * + * Since: 3.26 + **/ +gchar * +gs_plugin_download_rewrite_resource (GsPlugin *plugin, + GsApp *app, + const gchar *resource, + GCancellable *cancellable, + GError **error) +{ + guint start = 0; + g_autoptr(GString) resource_str = g_string_new (resource); + g_autoptr(GString) str = g_string_new (NULL); + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); + g_return_val_if_fail (resource != NULL, NULL); + g_return_val_if_fail (error == NULL || *error == NULL, NULL); + + /* replace datadir */ + as_utils_string_replace (resource_str, "@datadir@", DATADIR); + resource = resource_str->str; + + /* look in string for any url() links */ + for (guint i = 0; resource[i] != '\0'; i++) { + if (i > 4 && strncmp (resource + i - 4, "url(", 4) == 0) { + start = i; + continue; + } + if (start == 0) { + g_string_append_c (str, resource[i]); + continue; + } + if (resource[i] == ')') { + guint len; + g_autofree gchar *cachefn = NULL; + g_autofree gchar *uri = NULL; + + /* remove optional single quotes */ + if (resource[start] == '\'' || resource[start] == '"') + start++; + len = i - start; + if (i > 0 && (resource[i - 1] == '\'' || resource[i - 1] == '"')) + len--; + uri = g_strndup (resource + start, len); + + /* download them to per-user cache */ + cachefn = gs_plugin_download_rewrite_resource_uri (plugin, + app, + uri, + cancellable, + error); + if (cachefn == NULL) + return NULL; + g_string_append_printf (str, "'%s'", cachefn); + g_string_append_c (str, resource[i]); + start = 0; + } + } + return g_strdup (str->str); +} + +/** + * gs_plugin_cache_lookup: + * @plugin: a #GsPlugin + * @key: a string + * + * Looks up an application object from the per-plugin cache + * + * Returns: (transfer full) (nullable): the #GsApp, or %NULL + * + * Since: 3.22 + **/ +GsApp * +gs_plugin_cache_lookup (GsPlugin *plugin, const gchar *key) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + GsApp *app; + g_autoptr(GMutexLocker) locker = NULL; + + g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); + g_return_val_if_fail (key != NULL, NULL); + + locker = g_mutex_locker_new (&priv->cache_mutex); + app = g_hash_table_lookup (priv->cache, key); + if (app == NULL) + return NULL; + return g_object_ref (app); +} + +/** + * gs_plugin_cache_remove: + * @plugin: a #GsPlugin + * @key: a key which matches + * + * Removes an application from the per-plugin cache. + * + * Since: 3.22 + **/ +void +gs_plugin_cache_remove (GsPlugin *plugin, const gchar *key) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_PLUGIN (plugin)); + g_return_if_fail (key != NULL); + + locker = g_mutex_locker_new (&priv->cache_mutex); + g_hash_table_remove (priv->cache, key); +} + +/** + * gs_plugin_cache_add: + * @plugin: a #GsPlugin + * @key: a string, or %NULL if the unique ID should be used + * @app: a #GsApp + * + * Adds an application to the per-plugin cache. This is optional, + * and the plugin can use the cache however it likes. + * + * Since: 3.22 + **/ +void +gs_plugin_cache_add (GsPlugin *plugin, const gchar *key, GsApp *app) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_PLUGIN (plugin)); + g_return_if_fail (GS_IS_APP (app)); + + locker = g_mutex_locker_new (&priv->cache_mutex); + + /* the user probably doesn't want to do this */ + if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) { + g_warning ("adding wildcard app %s to plugin cache", + gs_app_get_unique_id (app)); + } + + /* default */ + if (key == NULL) + key = gs_app_get_unique_id (app); + + g_return_if_fail (key != NULL); + + if (g_hash_table_lookup (priv->cache, key) == app) + return; + g_hash_table_insert (priv->cache, g_strdup (key), g_object_ref (app)); +} + +/** + * gs_plugin_cache_invalidate: + * @plugin: a #GsPlugin + * + * Invalidate the per-plugin cache by marking all entries as invalid. + * This is optional, and the plugin can evict the cache whenever it + * likes. Using this function may mean the front-end and the plugin + * may be operating on a different GsApp with the same cache ID. + * + * Most plugins do not need to call this function; if a suitable cache + * key is being used the old cache item can remain. + * + * Since: 3.22 + **/ +void +gs_plugin_cache_invalidate (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + g_autoptr(GMutexLocker) locker = NULL; + + g_return_if_fail (GS_IS_PLUGIN (plugin)); + + locker = g_mutex_locker_new (&priv->cache_mutex); + g_hash_table_remove_all (priv->cache); +} + +/** + * gs_plugin_report_event: + * @plugin: a #GsPlugin + * @event: a #GsPluginEvent + * + * Report a non-fatal event to the UI. Plugins should not assume that a + * specific event is actually shown to the user as it may be ignored + * automatically. + * + * Since: 3.24 + **/ +void +gs_plugin_report_event (GsPlugin *plugin, GsPluginEvent *event) +{ + g_return_if_fail (GS_IS_PLUGIN (plugin)); + g_return_if_fail (GS_IS_PLUGIN_EVENT (event)); + g_signal_emit (plugin, signals[SIGNAL_REPORT_EVENT], 0, event); +} + +/** + * gs_plugin_set_allow_updates: + * @plugin: a #GsPlugin + * @allow_updates: boolean + * + * This allows plugins to inhibit the showing of the updates panel. + * This will typically be used when the required permissions are not possible + * to obtain, or when a LiveUSB image is low on space. + * + * By default, the updates panel is shown so plugins do not need to call this + * function unless they called gs_plugin_set_allow_updates() with %FALSE. + * + * Since: 3.24 + **/ +void +gs_plugin_set_allow_updates (GsPlugin *plugin, gboolean allow_updates) +{ + g_return_if_fail (GS_IS_PLUGIN (plugin)); + g_signal_emit (plugin, signals[SIGNAL_ALLOW_UPDATES], 0, allow_updates); +} + +/** + * gs_plugin_error_to_string: + * @error: a #GsPluginError, e.g. %GS_PLUGIN_ERROR_NO_NETWORK + * + * Converts the enumerated error to a string. + * + * Returns: a string, or %NULL for invalid + **/ +const gchar * +gs_plugin_error_to_string (GsPluginError error) +{ + if (error == GS_PLUGIN_ERROR_FAILED) + return "failed"; + if (error == GS_PLUGIN_ERROR_NOT_SUPPORTED) + return "not-supported"; + if (error == GS_PLUGIN_ERROR_CANCELLED) + return "cancelled"; + if (error == GS_PLUGIN_ERROR_NO_NETWORK) + return "no-network"; + if (error == GS_PLUGIN_ERROR_NO_SECURITY) + return "no-security"; + if (error == GS_PLUGIN_ERROR_NO_SPACE) + return "no-space"; + if (error == GS_PLUGIN_ERROR_AUTH_REQUIRED) + return "auth-required"; + if (error == GS_PLUGIN_ERROR_AUTH_INVALID) + return "auth-invalid"; + if (error == GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED) + return "plugin-depsolve-failed"; + if (error == GS_PLUGIN_ERROR_DOWNLOAD_FAILED) + return "download-failed"; + if (error == GS_PLUGIN_ERROR_WRITE_FAILED) + return "write-failed"; + if (error == GS_PLUGIN_ERROR_INVALID_FORMAT) + return "invalid-format"; + if (error == GS_PLUGIN_ERROR_DELETE_FAILED) + return "delete-failed"; + if (error == GS_PLUGIN_ERROR_RESTART_REQUIRED) + return "restart-required"; + if (error == GS_PLUGIN_ERROR_AC_POWER_REQUIRED) + return "ac-power-required"; + if (error == GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW) + return "battery-level-too-low"; + if (error == GS_PLUGIN_ERROR_TIMED_OUT) + return "timed-out"; + return NULL; +} + +/** + * gs_plugin_action_to_function_name: (skip) + * @action: a #GsPluginAction, e.g. %GS_PLUGIN_ERROR_NO_NETWORK + * + * Converts the enumerated action to the vfunc name. + * + * Returns: a string, or %NULL for invalid + **/ +const gchar * +gs_plugin_action_to_function_name (GsPluginAction action) +{ + if (action == GS_PLUGIN_ACTION_REFRESH) + return "gs_plugin_refresh"; + if (action == GS_PLUGIN_ACTION_REVIEW_SUBMIT) + return "gs_plugin_review_submit"; + if (action == GS_PLUGIN_ACTION_REVIEW_UPVOTE) + return "gs_plugin_review_upvote"; + if (action == GS_PLUGIN_ACTION_REVIEW_DOWNVOTE) + return "gs_plugin_review_downvote"; + if (action == GS_PLUGIN_ACTION_REVIEW_REPORT) + return "gs_plugin_review_report"; + if (action == GS_PLUGIN_ACTION_REVIEW_REMOVE) + return "gs_plugin_review_remove"; + if (action == GS_PLUGIN_ACTION_REVIEW_DISMISS) + return "gs_plugin_review_dismiss"; + if (action == GS_PLUGIN_ACTION_INSTALL) + return "gs_plugin_app_install"; + if (action == GS_PLUGIN_ACTION_REMOVE) + return "gs_plugin_app_remove"; + if (action == GS_PLUGIN_ACTION_SET_RATING) + return "gs_plugin_app_set_rating"; + if (action == GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD) + return "gs_plugin_app_upgrade_download"; + if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER) + return "gs_plugin_app_upgrade_trigger"; + if (action == GS_PLUGIN_ACTION_LAUNCH) + return "gs_plugin_launch"; + if (action == GS_PLUGIN_ACTION_UPDATE_CANCEL) + return "gs_plugin_update_cancel"; + if (action == GS_PLUGIN_ACTION_ADD_SHORTCUT) + return "gs_plugin_add_shortcut"; + if (action == GS_PLUGIN_ACTION_REMOVE_SHORTCUT) + return "gs_plugin_remove_shortcut"; + if (action == GS_PLUGIN_ACTION_REFINE) + return "gs_plugin_refine"; + if (action == GS_PLUGIN_ACTION_UPDATE) + return "gs_plugin_update"; + if (action == GS_PLUGIN_ACTION_DOWNLOAD) + return "gs_plugin_download"; + if (action == GS_PLUGIN_ACTION_FILE_TO_APP) + return "gs_plugin_file_to_app"; + if (action == GS_PLUGIN_ACTION_URL_TO_APP) + return "gs_plugin_url_to_app"; + if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES) + return "gs_plugin_add_distro_upgrades"; + if (action == GS_PLUGIN_ACTION_GET_SOURCES) + return "gs_plugin_add_sources"; + if (action == GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS) + return "gs_plugin_add_unvoted_reviews"; + if (action == GS_PLUGIN_ACTION_GET_INSTALLED) + return "gs_plugin_add_installed"; + if (action == GS_PLUGIN_ACTION_GET_FEATURED) + return "gs_plugin_add_featured"; + if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL) + return "gs_plugin_add_updates_historical"; + if (action == GS_PLUGIN_ACTION_GET_UPDATES) + return "gs_plugin_add_updates"; + if (action == GS_PLUGIN_ACTION_GET_POPULAR) + return "gs_plugin_add_popular"; + if (action == GS_PLUGIN_ACTION_GET_RECENT) + return "gs_plugin_add_recent"; + if (action == GS_PLUGIN_ACTION_SEARCH) + return "gs_plugin_add_search"; + if (action == GS_PLUGIN_ACTION_SEARCH_FILES) + return "gs_plugin_add_search_files"; + if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES) + return "gs_plugin_add_search_what_provides"; + if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS) + return "gs_plugin_add_category_apps"; + if (action == GS_PLUGIN_ACTION_GET_CATEGORIES) + return "gs_plugin_add_categories"; + if (action == GS_PLUGIN_ACTION_SETUP) + return "gs_plugin_setup"; + if (action == GS_PLUGIN_ACTION_INITIALIZE) + return "gs_plugin_initialize"; + if (action == GS_PLUGIN_ACTION_DESTROY) + return "gs_plugin_destroy"; + if (action == GS_PLUGIN_ACTION_GET_ALTERNATES) + return "gs_plugin_add_alternates"; + if (action == GS_PLUGIN_ACTION_GET_LANGPACKS) + return "gs_plugin_add_langpacks"; + return NULL; +} + +/** + * gs_plugin_action_to_string: + * @action: a #GsPluginAction, e.g. %GS_PLUGIN_ERROR_NO_NETWORK + * + * Converts the enumerated action to a string. + * + * Returns: a string, or %NULL for invalid + **/ +const gchar * +gs_plugin_action_to_string (GsPluginAction action) +{ + if (action == GS_PLUGIN_ACTION_UNKNOWN) + return "unknown"; + if (action == GS_PLUGIN_ACTION_SETUP) + return "setup"; + if (action == GS_PLUGIN_ACTION_INSTALL) + return "install"; + if (action == GS_PLUGIN_ACTION_DOWNLOAD) + return "download"; + if (action == GS_PLUGIN_ACTION_REMOVE) + return "remove"; + if (action == GS_PLUGIN_ACTION_UPDATE) + return "update"; + if (action == GS_PLUGIN_ACTION_SET_RATING) + return "set-rating"; + if (action == GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD) + return "upgrade-download"; + if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER) + return "upgrade-trigger"; + if (action == GS_PLUGIN_ACTION_LAUNCH) + return "launch"; + if (action == GS_PLUGIN_ACTION_UPDATE_CANCEL) + return "update-cancel"; + if (action == GS_PLUGIN_ACTION_ADD_SHORTCUT) + return "add-shortcut"; + if (action == GS_PLUGIN_ACTION_REMOVE_SHORTCUT) + return "remove-shortcut"; + if (action == GS_PLUGIN_ACTION_REVIEW_SUBMIT) + return "review-submit"; + if (action == GS_PLUGIN_ACTION_REVIEW_UPVOTE) + return "review-upvote"; + if (action == GS_PLUGIN_ACTION_REVIEW_DOWNVOTE) + return "review-downvote"; + if (action == GS_PLUGIN_ACTION_REVIEW_REPORT) + return "review-report"; + if (action == GS_PLUGIN_ACTION_REVIEW_REMOVE) + return "review-remove"; + if (action == GS_PLUGIN_ACTION_REVIEW_DISMISS) + return "review-dismiss"; + if (action == GS_PLUGIN_ACTION_GET_UPDATES) + return "get-updates"; + if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES) + return "get-distro-updates"; + if (action == GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS) + return "get-unvoted-reviews"; + if (action == GS_PLUGIN_ACTION_GET_SOURCES) + return "get-sources"; + if (action == GS_PLUGIN_ACTION_GET_INSTALLED) + return "get-installed"; + if (action == GS_PLUGIN_ACTION_GET_POPULAR) + return "get-popular"; + if (action == GS_PLUGIN_ACTION_GET_FEATURED) + return "get-featured"; + if (action == GS_PLUGIN_ACTION_SEARCH) + return "search"; + if (action == GS_PLUGIN_ACTION_SEARCH_FILES) + return "search-files"; + if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES) + return "search-provides"; + if (action == GS_PLUGIN_ACTION_GET_CATEGORIES) + return "get-categories"; + if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS) + return "get-category-apps"; + if (action == GS_PLUGIN_ACTION_REFINE) + return "refine"; + if (action == GS_PLUGIN_ACTION_REFRESH) + return "refresh"; + if (action == GS_PLUGIN_ACTION_FILE_TO_APP) + return "file-to-app"; + if (action == GS_PLUGIN_ACTION_URL_TO_APP) + return "url-to-app"; + if (action == GS_PLUGIN_ACTION_GET_RECENT) + return "get-recent"; + if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL) + return "get-updates-historical"; + if (action == GS_PLUGIN_ACTION_INITIALIZE) + return "initialize"; + if (action == GS_PLUGIN_ACTION_DESTROY) + return "destroy"; + if (action == GS_PLUGIN_ACTION_GET_ALTERNATES) + return "get-alternates"; + if (action == GS_PLUGIN_ACTION_GET_LANGPACKS) + return "get-langpacks"; + return NULL; +} + +/** + * gs_plugin_action_from_string: + * @action: a #GsPluginAction, e.g. "install" + * + * Converts the string to an enumerated action. + * + * Returns: a GsPluginAction, e.g. %GS_PLUGIN_ACTION_INSTALL + * + * Since: 3.26 + **/ +GsPluginAction +gs_plugin_action_from_string (const gchar *action) +{ + if (g_strcmp0 (action, "setup") == 0) + return GS_PLUGIN_ACTION_SETUP; + if (g_strcmp0 (action, "install") == 0) + return GS_PLUGIN_ACTION_INSTALL; + if (g_strcmp0 (action, "download") == 0) + return GS_PLUGIN_ACTION_DOWNLOAD; + if (g_strcmp0 (action, "remove") == 0) + return GS_PLUGIN_ACTION_REMOVE; + if (g_strcmp0 (action, "update") == 0) + return GS_PLUGIN_ACTION_UPDATE; + if (g_strcmp0 (action, "set-rating") == 0) + return GS_PLUGIN_ACTION_SET_RATING; + if (g_strcmp0 (action, "upgrade-download") == 0) + return GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD; + if (g_strcmp0 (action, "upgrade-trigger") == 0) + return GS_PLUGIN_ACTION_UPGRADE_TRIGGER; + if (g_strcmp0 (action, "launch") == 0) + return GS_PLUGIN_ACTION_LAUNCH; + if (g_strcmp0 (action, "update-cancel") == 0) + return GS_PLUGIN_ACTION_UPDATE_CANCEL; + if (g_strcmp0 (action, "add-shortcut") == 0) + return GS_PLUGIN_ACTION_ADD_SHORTCUT; + if (g_strcmp0 (action, "remove-shortcut") == 0) + return GS_PLUGIN_ACTION_REMOVE_SHORTCUT; + if (g_strcmp0 (action, "review-submit") == 0) + return GS_PLUGIN_ACTION_REVIEW_SUBMIT; + if (g_strcmp0 (action, "review-upvote") == 0) + return GS_PLUGIN_ACTION_REVIEW_UPVOTE; + if (g_strcmp0 (action, "review-downvote") == 0) + return GS_PLUGIN_ACTION_REVIEW_DOWNVOTE; + if (g_strcmp0 (action, "review-report") == 0) + return GS_PLUGIN_ACTION_REVIEW_REPORT; + if (g_strcmp0 (action, "review-remove") == 0) + return GS_PLUGIN_ACTION_REVIEW_REMOVE; + if (g_strcmp0 (action, "review-dismiss") == 0) + return GS_PLUGIN_ACTION_REVIEW_DISMISS; + if (g_strcmp0 (action, "get-updates") == 0) + return GS_PLUGIN_ACTION_GET_UPDATES; + if (g_strcmp0 (action, "get-distro-updates") == 0) + return GS_PLUGIN_ACTION_GET_DISTRO_UPDATES; + if (g_strcmp0 (action, "get-unvoted-reviews") == 0) + return GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS; + if (g_strcmp0 (action, "get-sources") == 0) + return GS_PLUGIN_ACTION_GET_SOURCES; + if (g_strcmp0 (action, "get-installed") == 0) + return GS_PLUGIN_ACTION_GET_INSTALLED; + if (g_strcmp0 (action, "get-popular") == 0) + return GS_PLUGIN_ACTION_GET_POPULAR; + if (g_strcmp0 (action, "get-featured") == 0) + return GS_PLUGIN_ACTION_GET_FEATURED; + if (g_strcmp0 (action, "search") == 0) + return GS_PLUGIN_ACTION_SEARCH; + if (g_strcmp0 (action, "search-files") == 0) + return GS_PLUGIN_ACTION_SEARCH_FILES; + if (g_strcmp0 (action, "search-provides") == 0) + return GS_PLUGIN_ACTION_SEARCH_PROVIDES; + if (g_strcmp0 (action, "get-categories") == 0) + return GS_PLUGIN_ACTION_GET_CATEGORIES; + if (g_strcmp0 (action, "get-category-apps") == 0) + return GS_PLUGIN_ACTION_GET_CATEGORY_APPS; + if (g_strcmp0 (action, "refine") == 0) + return GS_PLUGIN_ACTION_REFINE; + if (g_strcmp0 (action, "refresh") == 0) + return GS_PLUGIN_ACTION_REFRESH; + if (g_strcmp0 (action, "file-to-app") == 0) + return GS_PLUGIN_ACTION_FILE_TO_APP; + if (g_strcmp0 (action, "url-to-app") == 0) + return GS_PLUGIN_ACTION_URL_TO_APP; + if (g_strcmp0 (action, "get-recent") == 0) + return GS_PLUGIN_ACTION_GET_RECENT; + if (g_strcmp0 (action, "get-updates-historical") == 0) + return GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL; + if (g_strcmp0 (action, "initialize") == 0) + return GS_PLUGIN_ACTION_INITIALIZE; + if (g_strcmp0 (action, "destroy") == 0) + return GS_PLUGIN_ACTION_DESTROY; + if (g_strcmp0 (action, "get-alternates") == 0) + return GS_PLUGIN_ACTION_GET_ALTERNATES; + if (g_strcmp0 (action, "get-langpacks") == 0) + return GS_PLUGIN_ACTION_GET_LANGPACKS; + return GS_PLUGIN_ACTION_UNKNOWN; +} + +/** + * gs_plugin_refine_flags_to_string: + * @refine_flags: some #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE + * + * Converts the flags to a string. + * + * Returns: a string + **/ +gchar * +gs_plugin_refine_flags_to_string (GsPluginRefineFlags refine_flags) +{ + g_autoptr(GPtrArray) cstrs = g_ptr_array_new (); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_USE_HISTORY) + g_ptr_array_add (cstrs, "use-history"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) + g_ptr_array_add (cstrs, "require-license"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) + g_ptr_array_add (cstrs, "require-url"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) + g_ptr_array_add (cstrs, "require-description"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) + g_ptr_array_add (cstrs, "require-size"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) + g_ptr_array_add (cstrs, "require-rating"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) + g_ptr_array_add (cstrs, "require-version"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY) + g_ptr_array_add (cstrs, "require-history"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION) + g_ptr_array_add (cstrs, "require-setup-action"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) + g_ptr_array_add (cstrs, "require-update-details"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) + g_ptr_array_add (cstrs, "require-origin"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED) + g_ptr_array_add (cstrs, "require-related"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH) + g_ptr_array_add (cstrs, "require-menu-path"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) + g_ptr_array_add (cstrs, "require-addons"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES) + g_ptr_array_add (cstrs, "require-allow-packages"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY) + g_ptr_array_add (cstrs, "require-update-severity"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED) + g_ptr_array_add (cstrs, "require-upgrade-removed"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) + g_ptr_array_add (cstrs, "require-provenance"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) + g_ptr_array_add (cstrs, "require-reviews"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS) + g_ptr_array_add (cstrs, "require-review-ratings"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS) + g_ptr_array_add (cstrs, "require-key-colors"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) + g_ptr_array_add (cstrs, "require-icon"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) + g_ptr_array_add (cstrs, "require-permissions"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) + g_ptr_array_add (cstrs, "require-origin-hostname"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI) + g_ptr_array_add (cstrs, "require-origin-ui"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) + g_ptr_array_add (cstrs, "require-runtime"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) + g_ptr_array_add (cstrs, "require-screenshots"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) + g_ptr_array_add (cstrs, "require-categories"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) + g_ptr_array_add (cstrs, "require-project-group"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) + g_ptr_array_add (cstrs, "require-developer-name"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) + g_ptr_array_add (cstrs, "require-kudos"); + if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING) + g_ptr_array_add (cstrs, "content-rating"); + if (cstrs->len == 0) + return g_strdup ("none"); + g_ptr_array_add (cstrs, NULL); + return g_strjoinv (",", (gchar**) cstrs->pdata); +} + +static void +gs_plugin_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) +{ + GsPlugin *plugin = GS_PLUGIN (object); + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + switch (prop_id) { + case PROP_FLAGS: + priv->flags = g_value_get_uint64 (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) +{ + GsPlugin *plugin = GS_PLUGIN (object); + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + switch (prop_id) { + case PROP_FLAGS: + g_value_set_uint64 (value, priv->flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gs_plugin_class_init (GsPluginClass *klass) +{ + GParamSpec *pspec; + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = gs_plugin_set_property; + object_class->get_property = gs_plugin_get_property; + object_class->finalize = gs_plugin_finalize; + + pspec = g_param_spec_uint64 ("flags", NULL, NULL, + 0, G_MAXUINT64, 0, G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_FLAGS, pspec); + + signals [SIGNAL_UPDATES_CHANGED] = + g_signal_new ("updates-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsPluginClass, updates_changed), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals [SIGNAL_STATUS_CHANGED] = + g_signal_new ("status-changed", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsPluginClass, status_changed), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 2, GS_TYPE_APP, G_TYPE_UINT); + + signals [SIGNAL_RELOAD] = + g_signal_new ("reload", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsPluginClass, reload), + NULL, NULL, g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals [SIGNAL_REPORT_EVENT] = + g_signal_new ("report-event", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsPluginClass, report_event), + NULL, NULL, g_cclosure_marshal_generic, + G_TYPE_NONE, 1, GS_TYPE_PLUGIN_EVENT); + + signals [SIGNAL_ALLOW_UPDATES] = + g_signal_new ("allow-updates", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsPluginClass, allow_updates), + NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); + + signals [SIGNAL_BASIC_AUTH_START] = + g_signal_new ("basic-auth-start", + G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (GsPluginClass, 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_init (GsPlugin *plugin) +{ + GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); + guint i; + + for (i = 0; i < GS_PLUGIN_RULE_LAST; i++) + priv->rules[i] = g_ptr_array_new_with_free_func (g_free); + + priv->enabled = TRUE; + priv->scale = 1; + priv->cache = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash, + (GEqualFunc) as_utils_unique_id_equal, + g_free, + (GDestroyNotify) g_object_unref); + priv->vfuncs = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, NULL); + g_mutex_init (&priv->cache_mutex); + g_mutex_init (&priv->interactive_mutex); + g_mutex_init (&priv->timer_mutex); + g_mutex_init (&priv->vfuncs_mutex); +} + +/** + * gs_plugin_new: + * + * Creates a new plugin. + * + * Returns: a #GsPlugin + * + * Since: 3.22 + **/ +GsPlugin * +gs_plugin_new (void) +{ + GsPlugin *plugin; + plugin = g_object_new (GS_TYPE_PLUGIN, NULL); + return plugin; +} diff --git a/lib/gs-plugin.h b/lib/gs-plugin.h new file mode 100644 index 0000000..e211931 --- /dev/null +++ b/lib/gs-plugin.h @@ -0,0 +1,132 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2020 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <appstream-glib.h> +#include <glib-object.h> +#include <gmodule.h> +#include <gio/gio.h> +#include <libsoup/soup.h> + +#include "gs-app.h" +#include "gs-app-list.h" +#include "gs-category.h" +#include "gs-plugin-event.h" +#include "gs-plugin-types.h" + +G_BEGIN_DECLS + +#define GS_TYPE_PLUGIN (gs_plugin_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (GsPlugin, gs_plugin, GS, PLUGIN, GObject) + +struct _GsPluginClass +{ + GObjectClass parent_class; + void (*updates_changed) (GsPlugin *plugin); + void (*status_changed) (GsPlugin *plugin, + GsApp *app, + guint status); + void (*reload) (GsPlugin *plugin); + void (*report_event) (GsPlugin *plugin, + GsPluginEvent *event); + void (*allow_updates) (GsPlugin *plugin, + gboolean allow_updates); + void (*basic_auth_start) (GsPlugin *plugin, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data); + gpointer padding[25]; +}; + +typedef struct GsPluginData GsPluginData; + +/* helpers */ +#define GS_PLUGIN_ERROR gs_plugin_error_quark () + +GQuark gs_plugin_error_quark (void); + +/* public getters and setters */ +GsPluginData *gs_plugin_alloc_data (GsPlugin *plugin, + gsize sz); +GsPluginData *gs_plugin_get_data (GsPlugin *plugin); +const gchar *gs_plugin_get_name (GsPlugin *plugin); +const gchar *gs_plugin_get_appstream_id (GsPlugin *plugin); +void gs_plugin_set_appstream_id (GsPlugin *plugin, + const gchar *appstream_id); +gboolean gs_plugin_get_enabled (GsPlugin *plugin); +void gs_plugin_set_enabled (GsPlugin *plugin, + gboolean enabled); +gboolean gs_plugin_has_flags (GsPlugin *plugin, + GsPluginFlags flags); +void gs_plugin_add_flags (GsPlugin *plugin, + GsPluginFlags flags); +void gs_plugin_remove_flags (GsPlugin *plugin, + GsPluginFlags flags); +guint gs_plugin_get_scale (GsPlugin *plugin); +const gchar *gs_plugin_get_locale (GsPlugin *plugin); +const gchar *gs_plugin_get_language (GsPlugin *plugin); +SoupSession *gs_plugin_get_soup_session (GsPlugin *plugin); +void gs_plugin_set_soup_session (GsPlugin *plugin, + SoupSession *soup_session); +void gs_plugin_add_rule (GsPlugin *plugin, + GsPluginRule rule, + const gchar *name); + +/* helpers */ +GBytes *gs_plugin_download_data (GsPlugin *plugin, + GsApp *app, + const gchar *uri, + GCancellable *cancellable, + GError **error); +gboolean gs_plugin_download_file (GsPlugin *plugin, + GsApp *app, + const gchar *uri, + const gchar *filename, + GCancellable *cancellable, + GError **error); +gchar *gs_plugin_download_rewrite_resource (GsPlugin *plugin, + GsApp *app, + const gchar *resource, + GCancellable *cancellable, + GError **error); + +gboolean gs_plugin_check_distro_id (GsPlugin *plugin, + const gchar *distro_id); +GsApp *gs_plugin_cache_lookup (GsPlugin *plugin, + const gchar *key); +void gs_plugin_cache_add (GsPlugin *plugin, + const gchar *key, + GsApp *app); +void gs_plugin_cache_remove (GsPlugin *plugin, + const gchar *key); +void gs_plugin_cache_invalidate (GsPlugin *plugin); +void gs_plugin_status_update (GsPlugin *plugin, + GsApp *app, + GsPluginStatus status); +gboolean gs_plugin_app_launch (GsPlugin *plugin, + GsApp *app, + GError **error); +void gs_plugin_updates_changed (GsPlugin *plugin); +void gs_plugin_reload (GsPlugin *plugin); +const gchar *gs_plugin_status_to_string (GsPluginStatus status); +void gs_plugin_report_event (GsPlugin *plugin, + GsPluginEvent *event); +void gs_plugin_set_allow_updates (GsPlugin *plugin, + gboolean allow_updates); +gboolean gs_plugin_get_network_available (GsPlugin *plugin); +void gs_plugin_basic_auth_start (GsPlugin *plugin, + const gchar *remote, + const gchar *realm, + GCallback callback, + gpointer user_data); + +G_END_DECLS diff --git a/lib/gs-self-test.c b/lib/gs-self-test.c new file mode 100644 index 0000000..c6c20ed --- /dev/null +++ b/lib/gs-self-test.c @@ -0,0 +1,835 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include "config.h" + +#include "gnome-software-private.h" + +#include "gs-test.h" + +static gboolean +gs_app_list_filter_cb (GsApp *app, gpointer user_data) +{ + if (g_strcmp0 (gs_app_get_id (app), "a") == 0) + return FALSE; + if (g_strcmp0 (gs_app_get_id (app), "c") == 0) + return FALSE; + return TRUE; +} + +static void +gs_utils_url_func (void) +{ + g_autofree gchar *path1 = NULL; + g_autofree gchar *path2 = NULL; + g_autofree gchar *path3 = NULL; + g_autofree gchar *scheme1 = NULL; + g_autofree gchar *scheme2 = NULL; + + scheme1 = gs_utils_get_url_scheme ("appstream://gimp.desktop"); + g_assert_cmpstr (scheme1, ==, "appstream"); + scheme2 = gs_utils_get_url_scheme ("appstream:gimp.desktop"); + g_assert_cmpstr (scheme2, ==, "appstream"); + + path1 = gs_utils_get_url_path ("appstream://gimp.desktop"); + g_assert_cmpstr (path1, ==, "gimp.desktop"); + path2 = gs_utils_get_url_path ("appstream:gimp.desktop"); + g_assert_cmpstr (path2, ==, "gimp.desktop"); + path3 = gs_utils_get_url_path ("apt:/gimp"); + g_assert_cmpstr (path3, ==, "gimp"); +} + +static void +gs_utils_wilson_func (void) +{ + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 0), ==, -1); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 400), ==, 100); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (10, 0, 0, 0, 400), ==, 98); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 1), ==, 76); + g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (5, 4, 20, 100, 400), ==, 93); +} + +static void +gs_os_release_func (void) +{ + g_autofree gchar *fn = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsOsRelease) os_release = NULL; + + fn = gs_test_get_filename (TESTDATADIR, "tests/os-release"); + g_assert (fn != NULL); + g_setenv ("GS_SELF_TEST_OS_RELEASE_FILENAME", fn, TRUE); + + os_release = gs_os_release_new (&error); + g_assert_no_error (error); + g_assert (os_release != NULL); + g_assert_cmpstr (gs_os_release_get_id (os_release), ==, "fedora"); + g_assert_cmpstr (gs_os_release_get_name (os_release), ==, "Fedora"); + g_assert_cmpstr (gs_os_release_get_version (os_release), ==, "25 (Workstation Edition)"); + g_assert_cmpstr (gs_os_release_get_version_id (os_release), ==, "25"); + g_assert_cmpstr (gs_os_release_get_pretty_name (os_release), ==, "Fedora 25 (Workstation Edition)"); +} + +static void +gs_utils_append_kv_func (void) +{ + g_autoptr(GString) str = g_string_new (NULL); + + /* normal */ + gs_utils_append_key_value (str, 5, "key", "val"); + g_assert_cmpstr (str->str, ==, "key: val\n"); + + /* oversize */ + g_string_truncate (str, 0); + gs_utils_append_key_value (str, 5, "longkey", "val"); + g_assert_cmpstr (str->str, ==, "longkey: val\n"); + + /* null key */ + g_string_truncate (str, 0); + gs_utils_append_key_value (str, 5, NULL, "val"); + g_assert_cmpstr (str->str, ==, " val\n"); + + /* zero align key */ + g_string_truncate (str, 0); + gs_utils_append_key_value (str, 0, "key", "val"); + g_assert_cmpstr (str->str, ==, "key: val\n"); +} + +static void +gs_utils_cache_func (void) +{ + g_autofree gchar *fn1 = NULL; + g_autofree gchar *fn2 = NULL; + g_autoptr(GError) error = NULL; + + fn1 = gs_utils_get_cache_filename ("test", + "http://www.foo.bar/baz", + GS_UTILS_CACHE_FLAG_WRITEABLE, + &error); + g_assert_no_error (error); + g_assert_cmpstr (fn1, !=, NULL); + g_assert (g_str_has_prefix (fn1, g_get_user_cache_dir ())); + g_assert (g_str_has_suffix (fn1, "test/baz")); + + fn2 = gs_utils_get_cache_filename ("test", + "http://www.foo.bar/baz", + GS_UTILS_CACHE_FLAG_WRITEABLE | + GS_UTILS_CACHE_FLAG_USE_HASH, + &error); + g_assert_no_error (error); + g_assert_cmpstr (fn2, !=, NULL); + g_assert (g_str_has_prefix (fn2, g_get_user_cache_dir ())); + g_assert (g_str_has_suffix (fn2, "test/295099f59d12b3eb0b955325fcb699cd23792a89-baz")); +} + +static void +gs_utils_error_func (void) +{ + g_autofree gchar *app_id = NULL; + g_autofree gchar *origin_id = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsApp) app = gs_app_new ("gimp.desktop"); + g_autoptr(GsApp) origin = gs_app_new ("gimp-repo"); + + for (guint i = 0; i < GS_PLUGIN_ERROR_LAST; i++) + g_assert (gs_plugin_error_to_string (i) != NULL); + + /* noop */ + gs_utils_error_add_app_id (&error, app); + gs_utils_error_add_origin_id (&error, origin); + + g_set_error (&error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DOWNLOAD_FAILED, + "failed"); + g_assert_cmpstr (error->message, ==, "failed"); + gs_utils_error_add_app_id (&error, app); + gs_utils_error_add_origin_id (&error, origin); + g_assert_cmpstr (error->message, ==, "[*/*/*/*/gimp-repo/*] {*/*/*/*/gimp.desktop/*} failed"); + + /* 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); + if (origin_id == NULL) + origin_id = gs_utils_error_strip_origin_id (error); + } + + g_assert_cmpstr (app_id, ==, "*/*/*/*/gimp.desktop/*"); + g_assert_cmpstr (origin_id, ==, "*/*/*/*/gimp-repo/*"); + g_assert_cmpstr (error->message, ==, "failed"); +} + +static void +gs_utils_parse_evr_func (void) +{ + gboolean ret; + + { + g_autofree gchar *epoch = NULL; + g_autofree gchar *version = NULL; + g_autofree gchar *release = NULL; + + ret = gs_utils_parse_evr ("3.26.0-1.fc27", &epoch, &version, &release); + g_assert (ret); + g_assert_cmpstr (epoch, ==, "0"); + g_assert_cmpstr (version, ==, "3.26.0"); + g_assert_cmpstr (release, ==, "1.fc27"); + } + + { + g_autofree gchar *epoch = NULL; + g_autofree gchar *version = NULL; + g_autofree gchar *release = NULL; + + ret = gs_utils_parse_evr ("1:3.26.0-1.fc27", &epoch, &version, &release); + g_assert (ret); + g_assert_cmpstr (epoch, ==, "1"); + g_assert_cmpstr (version, ==, "3.26.0"); + g_assert_cmpstr (release, ==, "1.fc27"); + } + + { + g_autofree gchar *epoch = NULL; + g_autofree gchar *version = NULL; + g_autofree gchar *release = NULL; + + ret = gs_utils_parse_evr ("234", &epoch, &version, &release); + g_assert (ret); + g_assert_cmpstr (epoch, ==, "0"); + g_assert_cmpstr (version, ==, "234"); + g_assert_cmpstr (release, ==, "0"); + } + + { + g_autofree gchar *epoch = NULL; + g_autofree gchar *version = NULL; + g_autofree gchar *release = NULL; + + ret = gs_utils_parse_evr ("3:1.6~git20131207+dfsg-2ubuntu1~14.04.3", &epoch, &version, &release); + g_assert (ret); + g_assert_cmpstr (epoch, ==, "3"); + g_assert_cmpstr (version, ==, "1.6~git20131207+dfsg"); + g_assert_cmpstr (release, ==, "2ubuntu1~14.04.3"); + } + + { + g_autofree gchar *epoch = NULL; + g_autofree gchar *version = NULL; + g_autofree gchar *release = NULL; + + ret = gs_utils_parse_evr ("1-2-3-4-5-6", &epoch, &version, &release); + g_assert (!ret); + } + + { + g_autofree gchar *epoch = NULL; + g_autofree gchar *version = NULL; + g_autofree gchar *release = NULL; + + ret = gs_utils_parse_evr ("", &epoch, &version, &release); + g_assert (!ret); + } +} + +static void +gs_plugin_download_rewrite_func (void) +{ + g_autofree gchar *css = NULL; + g_autoptr(GError) error = NULL; + g_autoptr(GsPlugin) plugin = NULL; + const gchar *resource = "background:\n" + " url('file://" DATADIR "/gnome-software/featured-maps.png')\n" + " url('file://" DATADIR "/gnome-software/featured-maps-bg.png')\n" + " bottom center / contain no-repeat;\n"; + + /* only when installed */ + if (!g_file_test (DATADIR "/gnome-software/featured-maps.png", G_FILE_TEST_EXISTS)) { + g_test_skip ("not installed"); + return; + } + + /* test rewrite */ + plugin = gs_plugin_new (); + gs_plugin_set_name (plugin, "self-test"); + css = gs_plugin_download_rewrite_resource (plugin, + NULL, /* app */ + resource, + NULL, + &error); + g_assert_no_error (error); + g_assert (css != NULL); +} + +static void +gs_plugin_func (void) +{ + GsAppList *list; + GsAppList *list_dup; + GsAppList *list_remove; + GsApp *app; + g_autoptr(AsProvide) prov = as_provide_new (); + + /* check enums converted */ + for (guint i = 0; i < GS_PLUGIN_ACTION_LAST; i++) { + const gchar *tmp = gs_plugin_action_to_string (i); + if (tmp == NULL) + g_critical ("failed to convert %u", i); + g_assert_cmpint (gs_plugin_action_from_string (tmp), ==, i); + } + for (guint i = 1; i < GS_PLUGIN_ACTION_LAST; i++) { + const gchar *tmp = gs_plugin_action_to_function_name (i); + if (tmp == NULL) + g_critical ("failed to convert %u", i); + } + + /* add a couple of duplicate IDs */ + app = gs_app_new ("a"); + list = gs_app_list_new (); + gs_app_list_add (list, app); + g_object_unref (app); + + /* test refcounting */ + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list, 0)), ==, "a"); + list_dup = gs_app_list_copy (list); + g_object_unref (list); + g_assert_cmpint (gs_app_list_length (list_dup), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_dup, 0)), ==, "a"); + g_object_unref (list_dup); + + /* test removing objects */ + app = gs_app_new ("a"); + list_remove = gs_app_list_new (); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new ("c"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 3); + gs_app_list_filter (list_remove, gs_app_list_filter_cb, NULL); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_remove, 0)), ==, "b"); + + /* test removing duplicates at runtime */ + app = gs_app_new ("b"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list_remove, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_remove, 0)), ==, "b"); + g_object_unref (list_remove); + + /* test removing duplicates when lazy-loading */ + list_remove = gs_app_list_new (); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_set_id (app, "e"); + g_object_unref (app); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_set_id (app, "e"); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 2); + gs_app_list_filter_duplicates (list_remove, GS_APP_LIST_FILTER_FLAG_NONE); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 1); + g_object_unref (list_remove); + + /* test removing duplicates when some apps have no app ID */ + list_remove = gs_app_list_new (); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + g_object_unref (app); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_set_id (app, "e"); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 3); + gs_app_list_filter_duplicates (list_remove, GS_APP_LIST_FILTER_FLAG_NONE); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 3); + g_object_unref (list_remove); + + /* remove lazy-loaded app */ + list_remove = gs_app_list_new (); + app = gs_app_new (NULL); + gs_app_list_add (list_remove, app); + gs_app_list_remove (list_remove, app); + g_assert_cmpint (gs_app_list_length (list_remove), ==, 0); + g_object_unref (app); + g_object_unref (list_remove); + + /* respect priority when deduplicating */ + list = gs_app_list_new (); + app = gs_app_new ("e"); + gs_app_set_unique_id (app, "user/foo/*/*/e/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 0); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_set_unique_id (app, "user/bar/*/*/e/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 99); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_set_unique_id (app, "user/baz/*/*/e/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 50); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 3); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/bar/*/*/e/*"); + g_object_unref (list); + + /* respect priority (using name and version) when deduplicating */ + list = gs_app_list_new (); + app = gs_app_new ("e"); + gs_app_add_source (app, "foo"); + gs_app_set_version (app, "1.2.3"); + gs_app_set_unique_id (app, "user/foo/repo/*/*/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 0); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_add_source (app, "foo"); + gs_app_set_version (app, "1.2.3"); + gs_app_set_unique_id (app, "user/foo/repo-security/*/*/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 99); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_add_source (app, "foo"); + gs_app_set_version (app, "1.2.3"); + gs_app_set_unique_id (app, "user/foo/repo-universe/*/*/*"); + gs_app_list_add (list, app); + gs_app_set_priority (app, 50); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 3); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID | + GS_APP_LIST_FILTER_FLAG_KEY_SOURCE | + GS_APP_LIST_FILTER_FLAG_KEY_VERSION); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/foo/repo-security/*/*/*"); + g_object_unref (list); + + /* prefer installed applications */ + list = gs_app_list_new (); + app = gs_app_new ("e"); + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + gs_app_set_unique_id (app, "user/foo/*/*/e/*"); + gs_app_set_priority (app, 0); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("e"); + gs_app_set_state (app, AS_APP_STATE_AVAILABLE); + gs_app_set_unique_id (app, "user/bar/*/*/e/*"); + gs_app_set_priority (app, 100); + gs_app_list_add (list, app); + g_object_unref (app); + gs_app_list_filter_duplicates (list, + GS_APP_LIST_FILTER_FLAG_KEY_ID | + GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/foo/*/*/e/*"); + g_object_unref (list); + + /* use the provides ID to dedupe */ + list = gs_app_list_new (); + app = gs_app_new ("gimp.desktop"); + gs_app_set_unique_id (app, "user/fedora/*/*/gimp.desktop/*"); + gs_app_set_priority (app, 0); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("org.gimp.GIMP"); + as_provide_set_kind (prov, AS_PROVIDE_KIND_ID); + as_provide_set_value (prov, "gimp.desktop"); + gs_app_add_provide (app, prov); + gs_app_set_unique_id (app, "user/flathub/*/*/org.gimp.GIMP/*"); + gs_app_set_priority (app, 100); + gs_app_list_add (list, app); + g_object_unref (app); + gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, + "user/flathub/*/*/org.gimp.GIMP/*"); + g_object_unref (list); + + /* use globs when adding */ + list = gs_app_list_new (); + app = gs_app_new ("b"); + gs_app_set_unique_id (app, "a/b/c/d/e/f"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_set_unique_id (app, "a/b/c/*/e/f"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list, 0)), ==, "b"); + g_object_unref (list); + + /* lookup with a wildcard */ + list = gs_app_list_new (); + app = gs_app_new ("b"); + gs_app_set_unique_id (app, "a/b/c/d/e/f"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert (gs_app_list_lookup (list, "a/b/c/d/e/f") != NULL); + g_assert (gs_app_list_lookup (list, "a/b/c/d/e/*") != NULL); + g_assert (gs_app_list_lookup (list, "*/b/c/d/e/f") != NULL); + g_assert (gs_app_list_lookup (list, "x/x/x/x/x/x") == NULL); + g_object_unref (list); + + /* allow duplicating a wildcard */ + list = gs_app_list_new (); + app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("gimp.desktop"); + gs_app_set_unique_id (app, "system/flatpak/*/*/gimp.desktop/stable"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 2); + g_object_unref (list); + + /* allow duplicating a wildcard */ + list = gs_app_list_new (); + app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("gimp.desktop"); + gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + g_object_unref (list); + + /* add a list to a list */ + list = gs_app_list_new (); + list_dup = gs_app_list_new (); + app = gs_app_new ("a"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list_dup, app); + g_object_unref (app); + gs_app_list_add_list (list, list_dup); + g_assert_cmpint (gs_app_list_length (list), ==, 2); + g_assert_cmpint (gs_app_list_length (list_dup), ==, 1); + g_object_unref (list); + g_object_unref (list_dup); + + /* remove apps from the list */ + list = gs_app_list_new (); + app = gs_app_new ("a"); + gs_app_list_add (list, app); + gs_app_list_remove (list, app); + g_object_unref (app); + g_assert_cmpint (gs_app_list_length (list), ==, 0); + g_object_unref (list); + + /* truncate list */ + list = gs_app_list_new (); + app = gs_app_new ("a"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("b"); + gs_app_list_add (list, app); + g_object_unref (app); + app = gs_app_new ("c"); + gs_app_list_add (list, app); + g_object_unref (app); + g_assert (!gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)); + g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3); + gs_app_list_truncate (list, 3); + g_assert_cmpint (gs_app_list_length (list), ==, 3); + g_assert (gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED)); + g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3); + gs_app_list_truncate (list, 2); + g_assert_cmpint (gs_app_list_length (list), ==, 2); + gs_app_list_truncate (list, 1); + g_assert_cmpint (gs_app_list_length (list), ==, 1); + gs_app_list_truncate (list, 0); + g_assert_cmpint (gs_app_list_length (list), ==, 0); + g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3); + g_object_unref (list); +} + +static gpointer +gs_app_thread_cb (gpointer data) +{ + GsApp *app = GS_APP (data); + for (guint i = 0; i < 10000; i++) { + g_assert_cmpstr (gs_app_get_unique_id (app), !=, NULL); + gs_app_set_branch (app, "master"); + g_assert_cmpstr (gs_app_get_unique_id (app), !=, NULL); + gs_app_set_branch (app, "stable"); + } + return NULL; +} + +static void +gs_app_thread_func (void) +{ + GThread *thread1; + GThread *thread2; + g_autoptr(GsApp) app = gs_app_new ("gimp.desktop"); + + /* try really hard to cause a threading problem */ + g_setenv ("G_MESSAGES_DEBUG", "", TRUE); + thread1 = g_thread_new ("thread1", gs_app_thread_cb, app); + thread2 = g_thread_new ("thread2", gs_app_thread_cb, app); + g_thread_join (thread1); /* consumes the reference */ + g_thread_join (thread2); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); +} + +static void +gs_app_unique_id_func (void) +{ + g_autoptr(GsApp) app = gs_app_new (NULL); + const gchar *unique_id; + + unique_id = "system/flatpak/gnome/desktop/org.gnome.Software.desktop/master"; + gs_app_set_from_unique_id (app, unique_id); + g_assert (GS_IS_APP (app)); + g_assert_cmpint (gs_app_get_scope (app), ==, AS_APP_SCOPE_SYSTEM); + g_assert_cmpint (gs_app_get_bundle_kind (app), ==, AS_BUNDLE_KIND_FLATPAK); + g_assert_cmpstr (gs_app_get_origin (app), ==, "gnome"); + g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP); + g_assert_cmpstr (gs_app_get_id (app), ==, "org.gnome.Software.desktop"); + g_assert_cmpstr (gs_app_get_branch (app), ==, "master"); +} + +static void +gs_app_addons_func (void) +{ + g_autoptr(GsApp) app = gs_app_new ("test.desktop"); + GsApp *addon; + + /* create, add then drop ref, so @app has the only refcount of addon */ + addon = gs_app_new ("test.desktop"); + gs_app_add_addon (app, addon); + g_object_unref (addon); + + gs_app_remove_addon (app, addon); +} + +static void +gs_app_func (void) +{ + g_autoptr(GsApp) app = NULL; + + app = gs_app_new ("gnome-software.desktop"); + g_assert (GS_IS_APP (app)); + g_assert_cmpstr (gs_app_get_id (app), ==, "gnome-software.desktop"); + + /* check we clean up the version, but not at the expense of having + * the same string as the update version */ + gs_app_set_version (app, "2.8.6-3.fc20"); + gs_app_set_update_version (app, "2.8.6-4.fc20"); + g_assert_cmpstr (gs_app_get_version (app), ==, "2.8.6-3.fc20"); + g_assert_cmpstr (gs_app_get_update_version (app), ==, "2.8.6-4.fc20"); + g_assert_cmpstr (gs_app_get_version_ui (app), ==, "2.8.6-3"); + g_assert_cmpstr (gs_app_get_update_version_ui (app), ==, "2.8.6-4"); + + /* check the quality stuff works */ + gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "dave"); + g_assert_cmpstr (gs_app_get_name (app), ==, "dave"); + gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "brian"); + g_assert_cmpstr (gs_app_get_name (app), ==, "dave"); + gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, "hugh"); + g_assert_cmpstr (gs_app_get_name (app), ==, "hugh"); + + /* check non-transient state saving */ + gs_app_set_state (app, AS_APP_STATE_INSTALLED); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + gs_app_set_state (app, AS_APP_STATE_REMOVING); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_REMOVING); + gs_app_set_state_recover (app); // simulate an error + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + + /* try again */ + gs_app_set_state (app, AS_APP_STATE_REMOVING); + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_REMOVING); + gs_app_set_state_recover (app); // simulate an error + g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED); + + /* correctly parse URL */ + gs_app_set_origin_hostname (app, "https://mirrors.fedoraproject.org/metalink"); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "fedoraproject.org"); + gs_app_set_origin_hostname (app, "file:///home/hughsie"); + g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost"); + + /* check setting the progress */ + gs_app_set_progress (app, 42); + g_assert_cmpuint (gs_app_get_progress (app), ==, 42); + gs_app_set_progress (app, 0); + g_assert_cmpuint (gs_app_get_progress (app), ==, 0); + gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN); + g_assert_cmpuint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN); + g_assert_false ((gint) 0 <= (gint) GS_APP_PROGRESS_UNKNOWN && GS_APP_PROGRESS_UNKNOWN <= 100); + + /* check pending action */ + g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UNKNOWN); + gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE); + gs_app_set_pending_action (app, GS_PLUGIN_ACTION_UPDATE); + g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UPDATE); + gs_app_set_state (app, AS_APP_STATE_INSTALLING); + g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UNKNOWN); + gs_app_set_state_recover (app); +} + +static void +gs_app_progress_clamping_func (void) +{ + g_autoptr(GsApp) app = NULL; + + if (g_test_subprocess ()) { + app = gs_app_new ("gnome-software.desktop"); + gs_app_set_progress (app, 142); + g_assert_cmpuint (gs_app_get_progress (app), ==, 100); + } else { + g_test_trap_subprocess (NULL, 0, 0); + g_test_trap_assert_failed (); + g_test_trap_assert_stderr ("*WARNING*cannot set 142% for *, setting instead: 100%*"); + } +} + +static void +gs_app_list_wildcard_dedupe_func (void) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsApp) app1 = gs_app_new ("app"); + g_autoptr(GsApp) app2 = gs_app_new ("app"); + + gs_app_add_quirk (app1, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app1); + gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD); + gs_app_list_add (list, app2); + g_assert_cmpint (gs_app_list_length (list), ==, 1); +} + +static void +gs_app_list_func (void) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsApp) app1 = gs_app_new ("app1"); + g_autoptr(GsApp) app2 = gs_app_new ("app2"); + + /* turn on */ + gs_app_list_add_flag (list, GS_APP_LIST_FLAG_WATCH_APPS); + + g_assert_cmpint (gs_app_list_get_progress (list), ==, 0); + g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_UNKNOWN); + gs_app_list_add (list, app1); + gs_app_set_progress (app1, 75); + gs_app_set_state (app1, AS_APP_STATE_AVAILABLE); + gs_app_set_state (app1, AS_APP_STATE_INSTALLING); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 75); + g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_INSTALLING); + + gs_app_list_add (list, app2); + gs_app_set_progress (app2, 25); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 50); + g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_INSTALLING); + + gs_app_list_remove (list, app1); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 25); + g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_UNKNOWN); +} + +static void +gs_app_list_performance_func (void) +{ + g_autoptr(GPtrArray) apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GTimer) timer = NULL; + + /* create a few apps */ + for (guint i = 0; i < 500; i++) { + g_autofree gchar *id = g_strdup_printf ("%03u.desktop", i); + g_ptr_array_add (apps, gs_app_new (id)); + } + + /* add them to the list */ + timer = g_timer_new (); + for (guint i = 0; i < apps->len; i++) { + GsApp *app = g_ptr_array_index (apps, i); + gs_app_list_add (list, app); + } + g_print ("%.2fms ", g_timer_elapsed (timer, NULL) * 1000); +} + +static void +gs_app_list_related_func (void) +{ + g_autoptr(GsAppList) list = gs_app_list_new (); + g_autoptr(GsApp) app = gs_app_new ("app"); + g_autoptr(GsApp) related = gs_app_new ("related"); + + /* turn on */ + gs_app_list_add_flag (list, + GS_APP_LIST_FLAG_WATCH_APPS | + GS_APP_LIST_FLAG_WATCH_APPS_RELATED); + gs_app_add_related (app, related); + gs_app_list_add (list, app); + + gs_app_set_progress (app, 75); + gs_app_set_progress (related, 25); + gs_test_flush_main_context (); + g_assert_cmpint (gs_app_list_get_progress (list), ==, 50); +} + +int +main (int argc, char **argv) +{ + g_test_init (&argc, &argv, +#if GLIB_CHECK_VERSION(2, 60, 0) + G_TEST_OPTION_ISOLATE_DIRS, +#endif + NULL); + g_setenv ("G_MESSAGES_DEBUG", "all", TRUE); + + /* only critical and error are fatal */ + g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL); + + /* tests go here */ + g_test_add_func ("/gnome-software/lib/utils{url}", gs_utils_url_func); + g_test_add_func ("/gnome-software/lib/utils{wilson}", gs_utils_wilson_func); + g_test_add_func ("/gnome-software/lib/utils{error}", gs_utils_error_func); + g_test_add_func ("/gnome-software/lib/utils{cache}", gs_utils_cache_func); + g_test_add_func ("/gnome-software/lib/utils{append-kv}", gs_utils_append_kv_func); + g_test_add_func ("/gnome-software/lib/utils{parse-evr}", gs_utils_parse_evr_func); + g_test_add_func ("/gnome-software/lib/os-release", gs_os_release_func); + g_test_add_func ("/gnome-software/lib/app", gs_app_func); + g_test_add_func ("/gnome-software/lib/app/progress-clamping", gs_app_progress_clamping_func); + g_test_add_func ("/gnome-software/lib/app{addons}", gs_app_addons_func); + g_test_add_func ("/gnome-software/lib/app{unique-id}", gs_app_unique_id_func); + g_test_add_func ("/gnome-software/lib/app{thread}", gs_app_thread_func); + g_test_add_func ("/gnome-software/lib/app{list}", gs_app_list_func); + g_test_add_func ("/gnome-software/lib/app{list-wildcard-dedupe}", gs_app_list_wildcard_dedupe_func); + g_test_add_func ("/gnome-software/lib/app{list-performance}", gs_app_list_performance_func); + g_test_add_func ("/gnome-software/lib/app{list-related}", gs_app_list_related_func); + g_test_add_func ("/gnome-software/lib/plugin", gs_plugin_func); + g_test_add_func ("/gnome-software/lib/plugin{download-rewrite}", gs_plugin_download_rewrite_func); + + return g_test_run (); +} diff --git a/lib/gs-test.c b/lib/gs-test.c new file mode 100644 index 0000000..1f79e6a --- /dev/null +++ b/lib/gs-test.c @@ -0,0 +1,71 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#include <stdlib.h> + +#include "gs-test.h" + +gchar * +gs_test_get_filename (const gchar *testdatadir, const gchar *filename) +{ + gchar *tmp; + char full_tmp[PATH_MAX]; + g_autofree gchar *path = NULL; + path = g_build_filename (testdatadir, filename, NULL); + g_debug ("looking in %s", path); + tmp = realpath (path, full_tmp); + if (tmp == NULL) + return NULL; + return g_strdup (full_tmp); +} + +void +gs_test_flush_main_context (void) +{ + guint cnt = 0; + while (g_main_context_iteration (NULL, FALSE)) { + if (cnt == 0) + g_debug ("clearing pending events..."); + cnt++; + } + if (cnt > 0) + g_debug ("cleared %u events", cnt); +} + +/** + * gs_test_expose_icon_theme_paths: + * + * Calculate and set the `GS_SELF_TEST_ICON_THEME_PATH` environment variable + * to include the current system icon theme paths. This is designed to be called + * before calling `g_test_init()` with `G_TEST_OPTION_ISOLATE_DIRS`, which will + * clear the system icon theme paths. + * + * As this function calls `g_setenv()`, it must not be called after threads have + * been spawned. + * + * Calling this function is an explicit acknowledgement that the code under test + * should be accessing the icon theme. + * + * Since: 3.38 + */ +void +gs_test_expose_icon_theme_paths (void) +{ + const gchar * const *data_dirs; + g_autoptr(GString) data_dirs_str = NULL; + g_autofree gchar *data_dirs_joined = NULL; + + data_dirs = g_get_system_data_dirs (); + data_dirs_str = g_string_new (""); + for (gsize i = 0; data_dirs[i] != NULL; i++) + g_string_append_printf (data_dirs_str, "%s%s/icons", + (data_dirs_str->len > 0) ? ":" : "", + data_dirs[i]); + data_dirs_joined = g_string_free (g_steal_pointer (&data_dirs_str), FALSE); + g_setenv ("GS_SELF_TEST_ICON_THEME_PATH", data_dirs_joined, TRUE); +} diff --git a/lib/gs-test.h b/lib/gs-test.h new file mode 100644 index 0000000..82a33b4 --- /dev/null +++ b/lib/gs-test.h @@ -0,0 +1,20 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include "gs-app.h" + +G_BEGIN_DECLS + +void gs_test_flush_main_context (void); +gchar *gs_test_get_filename (const gchar *testdatadir, + const gchar *filename); +void gs_test_expose_icon_theme_paths (void); + +G_END_DECLS diff --git a/lib/gs-utils.c b/lib/gs-utils.c new file mode 100644 index 0000000..1ba5976 --- /dev/null +++ b/lib/gs-utils.c @@ -0,0 +1,1226 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +/** + * SECTION:gs-utils + * @title: GsUtils + * @include: gnome-software.h + * @stability: Unstable + * @short_description: Utilities that plugins can use + * + * These functions provide useful functionality that makes it easy to + * add new plugin functions. + */ + +#include "config.h" + +#include <errno.h> +#include <fnmatch.h> +#include <math.h> +#include <string.h> +#include <glib/gstdio.h> +#include <json-glib/json-glib.h> + +#if defined(__linux__) +#include <sys/sysinfo.h> +#elif defined(__FreeBSD__) +#include <sys/types.h> +#include <sys/sysctl.h> +#endif + +#ifdef HAVE_POLKIT +#include <polkit/polkit.h> +#endif + +#include "gs-app.h" +#include "gs-utils.h" +#include "gs-plugin.h" + +#define MB_IN_BYTES (1024 * 1024) + +/** + * gs_mkdir_parent: + * @path: A full pathname + * @error: A #GError, or %NULL + * + * Creates any required directories, including any parent directories. + * + * Returns: %TRUE for success + **/ +gboolean +gs_mkdir_parent (const gchar *path, GError **error) +{ + g_autofree gchar *parent = NULL; + + parent = g_path_get_dirname (path); + if (g_mkdir_with_parents (parent, 0755) == -1) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_FAILED, + "Failed to create '%s': %s", + parent, g_strerror (errno)); + return FALSE; + } + return TRUE; +} + +/** + * gs_utils_get_file_age: + * @file: A #GFile + * + * Gets a file age. + * + * Returns: The time in seconds since the file was modified, or %G_MAXUINT for error + */ +guint +gs_utils_get_file_age (GFile *file) +{ + guint64 now; + guint64 mtime; + g_autoptr(GFileInfo) info = NULL; + + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_TIME_MODIFIED, + G_FILE_QUERY_INFO_NONE, + NULL, + NULL); + if (info == NULL) + return G_MAXUINT; + mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED); + now = (guint64) g_get_real_time () / G_USEC_PER_SEC; + if (mtime > now) + return G_MAXUINT; + if (now - mtime > G_MAXUINT) + return G_MAXUINT; + return (guint) (now - mtime); +} + +static gchar * +gs_utils_filename_array_return_newest (GPtrArray *array) +{ + const gchar *filename_best = NULL; + guint age_lowest = G_MAXUINT; + guint i; + for (i = 0; i < array->len; i++) { + const gchar *fn = g_ptr_array_index (array, i); + g_autoptr(GFile) file = g_file_new_for_path (fn); + guint age_tmp = gs_utils_get_file_age (file); + if (age_tmp < age_lowest) { + age_lowest = age_tmp; + filename_best = fn; + } + } + return g_strdup (filename_best); +} + +/** + * gs_utils_get_cache_filename: + * @kind: A cache kind, e.g. "fwupd" or "screenshots/123x456" + * @resource: A resource, e.g. "system.bin" or "http://foo.bar/baz.bin" + * @flags: Some #GsUtilsCacheFlags, e.g. %GS_UTILS_CACHE_FLAG_WRITEABLE + * @error: A #GError, or %NULL + * + * Returns a filename that points into the cache. + * This may be per-system or per-user, the latter being more likely + * when %GS_UTILS_CACHE_FLAG_WRITEABLE is specified in @flags. + * + * If %GS_UTILS_CACHE_FLAG_USE_HASH is set in @flags then the returned filename + * will contain the hashed version of @resource. + * + * If there is more than one match, the file that has been modified last is + * returned. + * + * If a plugin requests a file to be saved in the cache it is the plugins + * responsibility to remove the file when it is no longer valid or is too old + * -- gnome-software will not ever clean the cache for the plugin. + * For this reason it is a good idea to use the plugin name as @kind. + * + * Returns: The full path and filename, which may or may not exist, or %NULL + **/ +gchar * +gs_utils_get_cache_filename (const gchar *kind, + const gchar *resource, + GsUtilsCacheFlags flags, + GError **error) +{ + const gchar *tmp; + g_autofree gchar *basename = NULL; + g_autofree gchar *cachedir = NULL; + g_autoptr(GFile) cachedir_file = NULL; + g_autoptr(GPtrArray) candidates = g_ptr_array_new_with_free_func (g_free); + + /* in the self tests */ + tmp = g_getenv ("GS_SELF_TEST_CACHEDIR"); + if (tmp != NULL) + return g_build_filename (tmp, kind, resource, NULL); + + /* get basename */ + if (flags & GS_UTILS_CACHE_FLAG_USE_HASH) { + g_autofree gchar *basename_tmp = g_path_get_basename (resource); + g_autofree gchar *hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, + resource, -1); + basename = g_strdup_printf ("%s-%s", hash, basename_tmp); + } else { + basename = g_path_get_basename (resource); + } + + /* not writable, so try the system cache first */ + if ((flags & GS_UTILS_CACHE_FLAG_WRITEABLE) == 0) { + g_autofree gchar *cachefn = NULL; + cachefn = g_build_filename (LOCALSTATEDIR, + "cache", + "gnome-software", + kind, + basename, + NULL); + if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) { + g_ptr_array_add (candidates, + g_steal_pointer (&cachefn)); + } + } + + /* not writable, so try the system cache first */ + if ((flags & GS_UTILS_CACHE_FLAG_WRITEABLE) == 0) { + g_autofree gchar *cachefn = NULL; + cachefn = g_build_filename (DATADIR, + "gnome-software", + "cache", + kind, + basename, + NULL); + if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) { + g_ptr_array_add (candidates, + g_steal_pointer (&cachefn)); + } + } + + /* create the cachedir in a per-release location, creating + * if it does not already exist */ + cachedir = g_build_filename (g_get_user_cache_dir (), + "gnome-software", + kind, + NULL); + cachedir_file = g_file_new_for_path (cachedir); + if (g_file_query_exists (cachedir_file, NULL) && + flags & GS_UTILS_CACHE_FLAG_ENSURE_EMPTY) { + if (!gs_utils_rmtree (cachedir, error)) + return NULL; + } + if (!g_file_query_exists (cachedir_file, NULL) && + !g_file_make_directory_with_parents (cachedir_file, NULL, error)) + return NULL; + g_ptr_array_add (candidates, g_build_filename (cachedir, basename, NULL)); + + /* common case: we only have one option */ + if (candidates->len == 1) + return g_strdup (g_ptr_array_index (candidates, 0)); + + /* return the newest (i.e. one with least age) */ + return gs_utils_filename_array_return_newest (candidates); +} + +/** + * gs_utils_get_user_hash: + * @error: A #GError, or %NULL + * + * This SHA1 hash is composed of the contents of machine-id and your + * username and is also salted with a hardcoded value. + * + * This provides an identifier that can be used to identify a specific + * user on a machine, allowing them to cast only one vote or perform + * one review on each application. + * + * There is no known way to calculate the machine ID or username from + * the machine hash and there should be no privacy issue. + * + * Returns: The user hash, or %NULL on error + */ +gchar * +gs_utils_get_user_hash (GError **error) +{ + g_autofree gchar *data = NULL; + g_autofree gchar *salted = NULL; + + if (!g_file_get_contents ("/etc/machine-id", &data, NULL, error)) + return NULL; + + salted = g_strdup_printf ("gnome-software[%s:%s]", + g_get_user_name (), data); + return g_compute_checksum_for_string (G_CHECKSUM_SHA1, salted, -1); +} + +/** + * gs_utils_get_permission: + * @id: A PolicyKit ID, e.g. "org.gnome.Desktop" + * @cancellable: A #GCancellable, or %NULL + * @error: A #GError, or %NULL + * + * Gets a permission object for an ID. + * + * Returns: a #GPermission, or %NULL if this if not possible. + **/ +GPermission * +gs_utils_get_permission (const gchar *id, GCancellable *cancellable, GError **error) +{ +#ifdef HAVE_POLKIT + g_autoptr(GPermission) permission = NULL; + permission = polkit_permission_new_sync (id, NULL, cancellable, error); + if (permission == NULL) { + g_prefix_error (error, "failed to create permission %s: ", id); + gs_utils_error_convert_gio (error); + return NULL; + } + return g_steal_pointer (&permission); +#else + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_NOT_SUPPORTED, + "no PolicyKit, so can't return GPermission for %s", id); + return NULL; +#endif +} + +/** + * gs_utils_get_content_type: + * @file: A GFile + * @cancellable: A #GCancellable, or %NULL + * @error: A #GError, or %NULL + * + * Gets the standard content type for a file. + * + * Returns: the content type, or %NULL, e.g. "text/plain" + */ +gchar * +gs_utils_get_content_type (GFile *file, + GCancellable *cancellable, + GError **error) +{ + const gchar *tmp; + g_autoptr(GFileInfo) info = NULL; + + /* get content type */ + info = g_file_query_info (file, + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + cancellable, + error); + if (info == NULL) + return NULL; + tmp = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE); + if (tmp == NULL) + return NULL; + return g_strdup (tmp); +} + +/** + * gs_utils_strv_fnmatch: + * @strv: A NUL-terminated list of strings + * @str: A string + * + * Matches a string against a list of globs. + * + * Returns: %TRUE if the list matches + */ +gboolean +gs_utils_strv_fnmatch (gchar **strv, const gchar *str) +{ + guint i; + + /* empty */ + if (strv == NULL) + return FALSE; + + /* look at each one */ + for (i = 0; strv[i] != NULL; i++) { + if (fnmatch (strv[i], str, 0) == 0) + return TRUE; + } + return FALSE; +} + +/** + * gs_utils_sort_key: + * @str: A string to convert to a sort key + * + * Useful to sort strings in a locale-sensitive, presentational way. + * Case is ignored and utf8 collation is used (e.g. accents are ignored). + * + * Returns: a newly allocated string sort key + */ +gchar * +gs_utils_sort_key (const gchar *str) +{ + g_autofree gchar *casefolded = g_utf8_casefold (str, -1); + return g_utf8_collate_key (casefolded, -1); +} + +/** + * gs_utils_sort_strcmp: + * @str1: (nullable): A string to compare + * @str2: (nullable): A string to compare + * + * Compares two strings in a locale-sensitive, presentational way. + * Case is ignored and utf8 collation is used (e.g. accents are ignored). %NULL + * is sorted before all non-%NULL strings, and %NULLs compare equal. + * + * Returns: < 0 if str1 is before str2, 0 if equal, > 0 if str1 is after str2 + */ +gint +gs_utils_sort_strcmp (const gchar *str1, const gchar *str2) +{ + g_autofree gchar *key1 = (str1 != NULL) ? gs_utils_sort_key (str1) : NULL; + g_autofree gchar *key2 = (str2 != NULL) ? gs_utils_sort_key (str2) : NULL; + return g_strcmp0 (key1, key2); +} + +/** + * gs_utils_get_desktop_app_info: + * @id: A desktop ID, e.g. "gimp.desktop" + * + * Gets a a #GDesktopAppInfo taking into account the kde4- prefix. + * If the given @id doesn not have a ".desktop" suffix, it will add one to it + * for convenience. + * + * Returns: a #GDesktopAppInfo for a specific ID, or %NULL + */ +GDesktopAppInfo * +gs_utils_get_desktop_app_info (const gchar *id) +{ + GDesktopAppInfo *app_info; + g_autofree gchar *desktop_id = NULL; + + /* for convenience, if the given id doesn't have the required .desktop + * suffix, we add it here */ + if (!g_str_has_suffix (id, ".desktop")) { + desktop_id = g_strconcat (id, ".desktop", NULL); + id = desktop_id; + } + + /* try to get the standard app-id */ + app_info = g_desktop_app_info_new (id); + + /* KDE is a special project because it believes /usr/share/applications + * isn't KDE enough. For this reason we support falling back to the + * "kde4-" prefixed ID to avoid educating various self-righteous + * upstreams about the correct ID to use in the AppData file. */ + if (app_info == NULL) { + g_autofree gchar *kde_id = NULL; + kde_id = g_strdup_printf ("%s-%s", "kde4", id); + app_info = g_desktop_app_info_new (kde_id); + } + + return app_info; +} + +/** + * gs_utils_symlink: + * @target: the full path of the symlink to create + * @linkpath: where the symlink should point to + * @error: A #GError, or %NULL + * + * Creates a symlink that can cross filesystem boundaries. + * Any parent directories needed for target to exist are also created. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_symlink (const gchar *target, const gchar *linkpath, GError **error) +{ + if (!gs_mkdir_parent (target, error)) + return FALSE; + if (symlink (target, linkpath) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_WRITE_FAILED, + "failed to create symlink from %s to %s", + linkpath, target); + return FALSE; + } + return TRUE; +} + +/** + * gs_utils_unlink: + * @filename: A full pathname to delete + * @error: A #GError, or %NULL + * + * Deletes a file from disk. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_unlink (const gchar *filename, GError **error) +{ + if (g_unlink (filename) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DELETE_FAILED, + "failed to delete %s", + filename); + return FALSE; + } + return TRUE; +} + +static gboolean +gs_utils_rmtree_real (const gchar *directory, GError **error) +{ + const gchar *filename; + g_autoptr(GDir) dir = NULL; + + /* try to open */ + dir = g_dir_open (directory, 0, error); + if (dir == NULL) + return FALSE; + + /* find each */ + while ((filename = g_dir_read_name (dir))) { + g_autofree gchar *src = NULL; + src = g_build_filename (directory, filename, NULL); + if (g_file_test (src, G_FILE_TEST_IS_DIR) && + !g_file_test (src, G_FILE_TEST_IS_SYMLINK)) { + if (!gs_utils_rmtree_real (src, error)) + return FALSE; + } else { + if (g_unlink (src) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DELETE_FAILED, + "Failed to delete: %s", src); + return FALSE; + } + } + } + + if (g_rmdir (directory) != 0) { + g_set_error (error, + GS_PLUGIN_ERROR, + GS_PLUGIN_ERROR_DELETE_FAILED, + "Failed to remove: %s", directory); + return FALSE; + } + return TRUE; +} + +/** + * gs_utils_rmtree: + * @directory: A full directory pathname to delete + * @error: A #GError, or %NULL + * + * Deletes a directory from disk and all its contents. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_rmtree (const gchar *directory, GError **error) +{ + g_debug ("recursively removing directory '%s'", directory); + return gs_utils_rmtree_real (directory, error); +} + +static gdouble +pnormaldist (gdouble qn) +{ + static gdouble b[11] = { 1.570796288, 0.03706987906, -0.8364353589e-3, + -0.2250947176e-3, 0.6841218299e-5, 0.5824238515e-5, + -0.104527497e-5, 0.8360937017e-7, -0.3231081277e-8, + 0.3657763036e-10, 0.6936233982e-12 }; + gdouble w1, w3; + guint i; + + if (qn < 0 || qn > 1) + return 0; // This is an error case + if (qn == 0.5) + return 0; + + w1 = qn; + if (qn > 0.5) + w1 = 1.0 - w1; + w3 = -log (4.0 * w1 * (1.0 - w1)); + w1 = b[0]; + for (i = 1; i < 11; i++) + w1 = w1 + (b[i] * pow (w3, i)); + + if (qn > 0.5) + return sqrt (w1 * w3); + else + return -sqrt (w1 * w3); +} + +static gdouble +wilson_score (gdouble value, gdouble n, gdouble power) +{ + gdouble z, phat; + if (value == 0) + return 0; + z = pnormaldist (1 - power / 2); + phat = value / n; + return (phat + z * z / (2 * n) - + z * sqrt ((phat * (1 - phat) + z * z / (4 * n)) / n)) / + (1 + z * z / n); +} + +/** + * gs_utils_get_wilson_rating: + * @star1: The number of 1 star reviews + * @star2: The number of 2 star reviews + * @star3: The number of 3 star reviews + * @star4: The number of 4 star reviews + * @star5: The number of 5 star reviews + * + * Returns the lower bound of Wilson score confidence interval for a + * Bernoulli parameter. This ensures small numbers of ratings don't give overly + * high scores. + * See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval + * for details. + * + * Returns: Wilson rating percentage, or -1 for error + **/ +gint +gs_utils_get_wilson_rating (guint64 star1, + guint64 star2, + guint64 star3, + guint64 star4, + guint64 star5) +{ + gdouble val; + guint64 star_sum = star1 + star2 + star3 + star4 + star5; + if (star_sum == 0) + return -1; + + /* get score */ + val = (wilson_score ((gdouble) star1, (gdouble) star_sum, 0.2) * -2); + val += (wilson_score ((gdouble) star2, (gdouble) star_sum, 0.2) * -1); + val += (wilson_score ((gdouble) star4, (gdouble) star_sum, 0.2) * 1); + val += (wilson_score ((gdouble) star5, (gdouble) star_sum, 0.2) * 2); + + /* normalize from -2..+2 to 0..5 */ + val += 3; + + /* multiply to a percentage */ + val *= 20; + + /* return rounded up integer */ + return (gint) ceil (val); +} + +/** + * gs_utils_error_add_app_id: + * @error: a #GError + * @app: a #GsApp + * + * Adds app unique ID prefix to the error. + * + * Since: 3.30 + **/ +void +gs_utils_error_add_app_id (GError **error, GsApp *app) +{ + g_return_if_fail (GS_APP (app)); + if (error == NULL || *error == NULL) + return; + g_prefix_error (error, "{%s} ", gs_app_get_unique_id (app)); +} + +/** + * gs_utils_error_add_origin_id: + * @error: a #GError + * @origin: a #GsApp + * + * Adds origin unique ID prefix to the error. + * + * Since: 3.30 + **/ +void +gs_utils_error_add_origin_id (GError **error, GsApp *origin) +{ + g_return_if_fail (GS_APP (origin)); + if (error == NULL || *error == NULL) + return; + g_prefix_error (error, "[%s] ", gs_app_get_unique_id (origin)); +} + +/** + * gs_utils_error_strip_app_id: + * @error: a #GError + * + * Removes a possible app ID prefix from the error, and returns the removed + * app ID. + * + * Returns: A newly allocated string with the app ID + * + * Since: 3.30 + **/ +gchar * +gs_utils_error_strip_app_id (GError *error) +{ + g_autofree gchar *app_id = NULL; + g_autofree gchar *msg = NULL; + + if (error == NULL || error->message == NULL) + return FALSE; + + if (g_str_has_prefix (error->message, "{")) { + const gchar *endp = strstr (error->message + 1, "} "); + if (endp != NULL) { + app_id = g_strndup (error->message + 1, + endp - (error->message + 1)); + msg = g_strdup (endp + 2); + } + } + + if (msg != NULL) { + g_free (error->message); + error->message = g_steal_pointer (&msg); + } + + return g_steal_pointer (&app_id); +} + +/** + * gs_utils_error_strip_origin_id: + * @error: a #GError + * + * Removes a possible origin ID prefix from the error, and returns the removed + * origin ID. + * + * Returns: A newly allocated string with the origin ID + * + * Since: 3.30 + **/ +gchar * +gs_utils_error_strip_origin_id (GError *error) +{ + g_autofree gchar *origin_id = NULL; + g_autofree gchar *msg = NULL; + + if (error == NULL || error->message == NULL) + return FALSE; + + if (g_str_has_prefix (error->message, "[")) { + const gchar *endp = strstr (error->message + 1, "] "); + if (endp != NULL) { + origin_id = g_strndup (error->message + 1, + endp - (error->message + 1)); + msg = g_strdup (endp + 2); + } + } + + if (msg != NULL) { + g_free (error->message); + error->message = g_steal_pointer (&msg); + } + + return g_steal_pointer (&origin_id); +} + +/** + * gs_utils_error_convert_gdbus: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GDBusError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gdbus (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != G_DBUS_ERROR) + return FALSE; + switch (error->code) { + case G_DBUS_ERROR_FAILED: + case G_DBUS_ERROR_NO_REPLY: + case G_DBUS_ERROR_TIMEOUT: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case G_DBUS_ERROR_IO_ERROR: + case G_DBUS_ERROR_NAME_HAS_NO_OWNER: + case G_DBUS_ERROR_NOT_SUPPORTED: + case G_DBUS_ERROR_SERVICE_UNKNOWN: + case G_DBUS_ERROR_UNKNOWN_INTERFACE: + case G_DBUS_ERROR_UNKNOWN_METHOD: + case G_DBUS_ERROR_UNKNOWN_OBJECT: + case G_DBUS_ERROR_UNKNOWN_PROPERTY: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case G_DBUS_ERROR_NO_MEMORY: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case G_DBUS_ERROR_ACCESS_DENIED: + case G_DBUS_ERROR_AUTH_FAILED: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case G_DBUS_ERROR_NO_NETWORK: + error->code = GS_PLUGIN_ERROR_NO_NETWORK; + break; + case G_DBUS_ERROR_INVALID_FILE_CONTENT: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_gio: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GIOError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gio (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != G_IO_ERROR) + return FALSE; + switch (error->code) { + case G_IO_ERROR_FAILED: + case G_IO_ERROR_NOT_FOUND: + case G_IO_ERROR_EXISTS: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case G_IO_ERROR_TIMED_OUT: + error->code = GS_PLUGIN_ERROR_TIMED_OUT; + break; + case G_IO_ERROR_NOT_SUPPORTED: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case G_IO_ERROR_CANCELLED: + error->code = GS_PLUGIN_ERROR_CANCELLED; + break; + case G_IO_ERROR_NO_SPACE: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + case G_IO_ERROR_PERMISSION_DENIED: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case G_IO_ERROR_HOST_NOT_FOUND: + case G_IO_ERROR_HOST_UNREACHABLE: + case G_IO_ERROR_CONNECTION_REFUSED: + case G_IO_ERROR_PROXY_FAILED: + case G_IO_ERROR_PROXY_AUTH_FAILED: + case G_IO_ERROR_PROXY_NOT_ALLOWED: + error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + break; + case G_IO_ERROR_NETWORK_UNREACHABLE: + error->code = GS_PLUGIN_ERROR_NO_NETWORK; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_gresolver: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GResolverError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gresolver (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != G_RESOLVER_ERROR) + return FALSE; + switch (error->code) { + case G_RESOLVER_ERROR_INTERNAL: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case G_RESOLVER_ERROR_NOT_FOUND: + case G_RESOLVER_ERROR_TEMPORARY_FAILURE: + error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_gdk_pixbuf: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #GdkPixbufError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_gdk_pixbuf (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != GDK_PIXBUF_ERROR) + return FALSE; + switch (error->code) { + case GDK_PIXBUF_ERROR_UNSUPPORTED_OPERATION: + case GDK_PIXBUF_ERROR_UNKNOWN_TYPE: + error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED; + break; + case GDK_PIXBUF_ERROR_FAILED: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + case GDK_PIXBUF_ERROR_CORRUPT_IMAGE: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + default: + g_warning ("can't reliably fixup error code %i in domain %s", + error->code, g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_json_glib: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the #JsonParserError to an error with a GsPluginError domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_json_glib (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + if (error->domain != JSON_PARSER_ERROR) + return FALSE; + switch (error->code) { + case JSON_PARSER_ERROR_UNKNOWN: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + default: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_error_convert_appstream: + * @perror: a pointer to a #GError, or %NULL + * + * Converts the various AppStream error types to an error with a GsPluginError + * domain. + * + * Returns: %TRUE if the error was converted, or already correct + **/ +gboolean +gs_utils_error_convert_appstream (GError **perror) +{ + GError *error = perror != NULL ? *perror : NULL; + + /* not set */ + if (error == NULL) + return FALSE; + if (error->domain == GS_PLUGIN_ERROR) + return TRUE; + + /* custom to this plugin */ + if (error->domain == AS_UTILS_ERROR) { + switch (error->code) { + case AS_UTILS_ERROR_INVALID_TYPE: + error->code = GS_PLUGIN_ERROR_INVALID_FORMAT; + break; + case AS_UTILS_ERROR_FAILED: + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == AS_STORE_ERROR) { + switch (error->code) { + case AS_UTILS_ERROR_FAILED: + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == AS_ICON_ERROR) { + switch (error->code) { + case AS_ICON_ERROR_FAILED: + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else if (error->domain == G_FILE_ERROR) { + switch (error->code) { + case G_FILE_ERROR_EXIST: + case G_FILE_ERROR_ACCES: + case G_FILE_ERROR_PERM: + error->code = GS_PLUGIN_ERROR_NO_SECURITY; + break; + case G_FILE_ERROR_NOSPC: + error->code = GS_PLUGIN_ERROR_NO_SPACE; + break; + default: + error->code = GS_PLUGIN_ERROR_FAILED; + break; + } + } else { + g_warning ("can't reliably fixup error from domain %s", + g_quark_to_string (error->domain)); + error->code = GS_PLUGIN_ERROR_FAILED; + } + error->domain = GS_PLUGIN_ERROR; + return TRUE; +} + +/** + * gs_utils_get_url_scheme: + * @url: A URL, e.g. "appstream://gimp.desktop" + * + * Gets the scheme from the URL string. + * + * Returns: the URL scheme, e.g. "appstream" + */ +gchar * +gs_utils_get_url_scheme (const gchar *url) +{ + g_autoptr(SoupURI) uri = NULL; + + /* no data */ + if (url == NULL) + return NULL; + + /* create URI from URL */ + uri = soup_uri_new (url); + if (!SOUP_URI_IS_VALID (uri)) + return NULL; + + /* success */ + return g_strdup (soup_uri_get_scheme (uri)); +} + +/** + * gs_utils_get_url_path: + * @url: A URL, e.g. "appstream://gimp.desktop" + * + * Gets the path from the URL string, removing any leading slashes. + * + * Returns: the URL path, e.g. "gimp.desktop" + */ +gchar * +gs_utils_get_url_path (const gchar *url) +{ + g_autoptr(SoupURI) uri = NULL; + const gchar *host; + const gchar *path; + + uri = soup_uri_new (url); + if (!SOUP_URI_IS_VALID (uri)) + return NULL; + + /* foo://bar -> scheme: foo, host: bar, path: / */ + /* foo:bar -> scheme: foo, host: (empty string), path: /bar */ + host = soup_uri_get_host (uri); + path = soup_uri_get_path (uri); + if (host != NULL && (strlen (host) > 0)) + path = host; + + /* trim any leading slashes */ + while (*path == '/') + path++; + + /* success */ + return g_strdup (path); +} + +/** + * gs_user_agent: + * + * Gets the user agent to use for remote requests. + * + * Returns: the user-agent, e.g. "gnome-software/3.22.1" + */ +const gchar * +gs_user_agent (void) +{ + return PACKAGE_NAME "/" PACKAGE_VERSION; +} + +/** + * gs_utils_append_key_value: + * @str: A #GString + * @align_len: The alignment of the @value compared to the @key + * @key: The text to use as a title + * @value: The text to use as a value + * + * Adds a line to an existing string, padding the key to a set number of spaces. + * + * Since: 3.26 + */ +void +gs_utils_append_key_value (GString *str, gsize align_len, + const gchar *key, const gchar *value) +{ + gsize len = 0; + + g_return_if_fail (str != NULL); + g_return_if_fail (value != NULL); + + if (key != NULL) { + len = strlen (key) + 2; + g_string_append (str, key); + g_string_append (str, ": "); + } + for (gsize i = len; i < align_len + 1; i++) + g_string_append (str, " "); + g_string_append (str, value); + g_string_append (str, "\n"); +} + +guint +gs_utils_get_memory_total (void) +{ +#if defined(__linux__) + struct sysinfo si = { 0 }; + sysinfo (&si); + if (si.mem_unit > 0) + return si.totalram / MB_IN_BYTES / si.mem_unit; + return 0; +#elif defined(__FreeBSD__) + unsigned long physmem; + sysctl ((int[]){ CTL_HW, HW_PHYSMEM }, 2, &physmem, &(size_t){ sizeof (physmem) }, NULL, 0); + return physmem / MB_IN_BYTES; +#else +#error "Please implement gs_utils_get_memory_total for your system." +#endif +} + +/** + * gs_utils_parse_evr: + * @evr: an EVR version string + * @out_epoch: (out): return location for the epoch string + * @out_version: (out): return location for the version string + * @out_release: (out): return location for the release string + * + * Splits EVR into epoch-version-release strings. + * + * Returns: %TRUE for success + **/ +gboolean +gs_utils_parse_evr (const gchar *evr, + gchar **out_epoch, + gchar **out_version, + gchar **out_release) +{ + const gchar *version_release; + g_auto(GStrv) split_colon = NULL; + g_auto(GStrv) split_dash = NULL; + + /* split on : to get epoch */ + split_colon = g_strsplit (evr, ":", -1); + switch (g_strv_length (split_colon)) { + case 1: + /* epoch is 0 when not set */ + *out_epoch = g_strdup ("0"); + version_release = split_colon[0]; + break; + case 2: + /* epoch set */ + *out_epoch = g_strdup (split_colon[0]); + version_release = split_colon[1]; + break; + default: + /* error */ + return FALSE; + } + + /* split on - to get version and release */ + split_dash = g_strsplit (version_release, "-", -1); + switch (g_strv_length (split_dash)) { + case 1: + /* all of the string is version */ + *out_version = g_strdup (split_dash[0]); + *out_release = g_strdup ("0"); + break; + case 2: + /* both version and release set */ + *out_version = g_strdup (split_dash[0]); + *out_release = g_strdup (split_dash[1]); + break; + default: + /* error */ + return FALSE; + } + + g_assert (*out_epoch != NULL); + g_assert (*out_version != NULL); + g_assert (*out_release != NULL); + return TRUE; +} + +/** + * gs_utils_set_online_updates_timestamp: + * + * Sets the value of online-updates-timestamp to current epoch. "online-updates-timestamp" represents + * the last time the system was online and got any updates. + * + **/ +void +gs_utils_set_online_updates_timestamp (GSettings *settings) +{ + g_autoptr(GDateTime) now = NULL; + + g_return_if_fail (settings != NULL); + + now = g_date_time_new_now_local (); + g_settings_set (settings, "online-updates-timestamp", "x", g_date_time_to_unix (now)); +} + +/* vim: set noexpandtab: */ diff --git a/lib/gs-utils.h b/lib/gs-utils.h new file mode 100644 index 0000000..3964f00 --- /dev/null +++ b/lib/gs-utils.h @@ -0,0 +1,96 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- + * vi:set noexpandtab tabstop=8 shiftwidth=8: + * + * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com> + * Copyright (C) 2018 Kalev Lember <klember@redhat.com> + * + * SPDX-License-Identifier: GPL-2.0+ + */ + +#pragma once + +#include <gio/gdesktopappinfo.h> +#include <gtk/gtk.h> + +#include "gs-app.h" + +G_BEGIN_DECLS + +/** + * GsUtilsCacheFlags: + * @GS_UTILS_CACHE_FLAG_NONE: No flags set + * @GS_UTILS_CACHE_FLAG_WRITEABLE: A writable directory is required + * @GS_UTILS_CACHE_FLAG_USE_HASH: Prefix a hash to the filename + * @GS_UTILS_CACHE_FLAG_ENSURE_EMPTY: Clear existing cached items + * + * The cache flags. + **/ +typedef enum { + GS_UTILS_CACHE_FLAG_NONE = 0, + GS_UTILS_CACHE_FLAG_WRITEABLE = 1 << 0, + GS_UTILS_CACHE_FLAG_USE_HASH = 1 << 1, + GS_UTILS_CACHE_FLAG_ENSURE_EMPTY = 1 << 2, + /*< private >*/ + GS_UTILS_CACHE_FLAG_LAST +} GsUtilsCacheFlags; + +guint gs_utils_get_file_age (GFile *file); +gchar *gs_utils_get_content_type (GFile *file, + GCancellable *cancellable, + GError **error); +gboolean gs_utils_symlink (const gchar *target, + const gchar *linkpath, + GError **error); +gboolean gs_utils_unlink (const gchar *filename, + GError **error); +gboolean gs_mkdir_parent (const gchar *path, + GError **error); +gchar *gs_utils_get_cache_filename (const gchar *kind, + const gchar *resource, + GsUtilsCacheFlags flags, + GError **error); +gchar *gs_utils_get_user_hash (GError **error); +GPermission *gs_utils_get_permission (const gchar *id, + GCancellable *cancellable, + GError **error); +gboolean gs_utils_strv_fnmatch (gchar **strv, + const gchar *str); +gchar *gs_utils_sort_key (const gchar *str); +gint gs_utils_sort_strcmp (const gchar *str1, + const gchar *str2); +GDesktopAppInfo *gs_utils_get_desktop_app_info (const gchar *id); +gboolean gs_utils_rmtree (const gchar *directory, + GError **error); +gint gs_utils_get_wilson_rating (guint64 star1, + guint64 star2, + guint64 star3, + guint64 star4, + guint64 star5); +void gs_utils_error_add_app_id (GError **error, + GsApp *app); +void gs_utils_error_add_origin_id (GError **error, + GsApp *origin); +gchar *gs_utils_error_strip_app_id (GError *error); +gchar *gs_utils_error_strip_origin_id (GError *error); +gboolean gs_utils_error_convert_gio (GError **perror); +gboolean gs_utils_error_convert_gresolver (GError **perror); +gboolean gs_utils_error_convert_gdbus (GError **perror); +gboolean gs_utils_error_convert_gdk_pixbuf(GError **perror); +gboolean gs_utils_error_convert_json_glib (GError **perror); +gboolean gs_utils_error_convert_appstream (GError **perror); + +gchar *gs_utils_get_url_scheme (const gchar *url); +gchar *gs_utils_get_url_path (const gchar *url); +const gchar *gs_user_agent (void); +void gs_utils_append_key_value (GString *str, + gsize align_len, + const gchar *key, + const gchar *value); +guint gs_utils_get_memory_total (void); +gboolean gs_utils_parse_evr (const gchar *evr, + gchar **out_epoch, + gchar **out_version, + gchar **out_release); +void gs_utils_set_online_updates_timestamp (GSettings *settings); + +G_END_DECLS diff --git a/lib/meson.build b/lib/meson.build new file mode 100644 index 0000000..0ef4936 --- /dev/null +++ b/lib/meson.build @@ -0,0 +1,142 @@ +cargs = ['-DG_LOG_DOMAIN="Gs"'] +cargs += ['-DLOCALPLUGINDIR=""'] + +install_headers([ + 'gnome-software.h', + 'gs-app.h', + 'gs-app-collation.h', + 'gs-app-list.h', + 'gs-autocleanups.h', + 'gs-category.h', + 'gs-ioprio.h', + 'gs-metered.h', + 'gs-os-release.h', + 'gs-plugin.h', + 'gs-plugin-event.h', + 'gs-plugin-job.h', + 'gs-plugin-loader.h', + 'gs-plugin-loader-sync.h', + 'gs-plugin-types.h', + 'gs-plugin-vfuncs.h', + 'gs-utils.h' + ], + subdir : 'gnome-software' +) + +librarydeps = [ + appstream_glib, + gio_unix, + glib, + gmodule, + goa, + gtk, + json_glib, + libm, + libsoup, + libsysprof_capture_dep, + valgrind, +] + +if get_option('mogwai') + librarydeps += mogwai_schedule_client +endif + +if get_option('polkit') + librarydeps += polkit +endif + +libgnomesoftware = static_library( + 'gnomesoftware', + sources : [ + 'gs-app.c', + 'gs-app-list.c', + 'gs-category.c', + 'gs-debug.c', + 'gs-ioprio.c', + 'gs-ioprio.h', + 'gs-metered.c', + 'gs-os-release.c', + 'gs-plugin.c', + 'gs-plugin-event.c', + 'gs-plugin-job.c', + 'gs-plugin-loader.c', + 'gs-plugin-loader-sync.c', + 'gs-test.c', + 'gs-utils.c', + ], + include_directories : [ + include_directories('..'), + ], + dependencies : librarydeps, + c_args : cargs, + install: true, +) + +pkg = import('pkgconfig') + +pkg.generate( + libgnomesoftware, + description : 'GNOME Software is a software center for GNOME', + filebase : 'gnome-software', + name : 'gnome-software', + subdirs : 'gnome-software', + variables : 'plugindir=${libdir}/gs-plugins-' + gs_plugin_api_version, +) + +executable( + 'gnome-software-cmd', + sources : [ + 'gs-cmd.c', + ], + include_directories : [ + include_directories('..'), + ], + dependencies : [ + appstream_glib, + gio_unix, + glib, + gmodule, + goa, + gtk, + json_glib, + libm, + libsoup, + valgrind, + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs, + install : true, + install_dir : get_option('libexecdir') +) + +if get_option('tests') + cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), '..', 'data') + '"'] + e = executable( + 'gs-self-test', + compiled_schemas, + sources : [ + 'gs-self-test.c' + ], + include_directories : [ + include_directories('..'), + ], + dependencies : [ + appstream_glib, + gio_unix, + glib, + gmodule, + goa, + gtk, + json_glib, + libm, + libsoup + ], + link_with : [ + libgnomesoftware + ], + c_args : cargs + ) + test('gs-self-test-lib', e, suite: ['lib'], env: test_env, timeout : 120) +endif |