1
0
Fork 0
gnome-software/plugins/core/gs-plugin-appstream.c
Daniel Baumann 68ee05b3fd
Adding upstream version 48.2.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
2025-06-22 21:00:23 +02:00

1703 lines
61 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
* vi:set noexpandtab tabstop=8 shiftwidth=8:
*
* Copyright (C) 2013-2014 Richard Hughes <richard@hughsie.com>
* Copyright (C) 2015-2019 Kalev Lember <klember@redhat.com>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config.h>
#include <glib/gi18n.h>
#include <errno.h>
#include <gnome-software.h>
#include <malloc.h>
#include <xmlb.h>
#include "gs-appstream.h"
#include "gs-external-appstream-utils.h"
#include "gs-plugin-appstream.h"
/*
* SECTION:
* Uses offline AppStream data to populate and refine package results.
*
* This plugin calls UpdatesChanged() if any of the AppStream stores are
* changed in any way.
*
* Methods: | AddCategory
* Refines: | [source]->[name,summary,pixbuf,id,kind]
*/
struct _GsPluginAppstream
{
GsPlugin parent;
GsWorkerThread *worker; /* (owned) */
XbSilo *silo;
GMutex silo_lock;
gchar *silo_filename;
GHashTable *silo_installed_by_desktopid;
GHashTable *silo_installed_by_id;
AsComponentScope default_scope;
GSettings *settings;
GPtrArray *file_monitors; /* (owned) (element-type GFileMonitor) */
/* The stamps help to avoid locking the silo lock in the main thread
and also to detect changes while loading other appstream data. */
gint silo_change_stamp; /* the silo change stamp, increased on every silo change */
gint silo_change_stamp_current; /* the currently known silo change stamp, checked for changes */
};
G_DEFINE_TYPE (GsPluginAppstream, gs_plugin_appstream, GS_TYPE_PLUGIN)
#define assert_in_worker(self) \
g_assert (gs_worker_thread_is_in_worker_context (self->worker))
static void
gs_plugin_appstream_dispose (GObject *object)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (object);
g_clear_object (&self->silo);
g_clear_pointer (&self->silo_filename, g_free);
g_clear_pointer (&self->silo_installed_by_desktopid, g_hash_table_unref);
g_clear_pointer (&self->silo_installed_by_id, g_hash_table_unref);
g_clear_object (&self->settings);
g_mutex_clear (&self->silo_lock);
g_clear_object (&self->worker);
g_clear_pointer (&self->file_monitors, g_ptr_array_unref);
G_OBJECT_CLASS (gs_plugin_appstream_parent_class)->dispose (object);
}
static void
gs_plugin_appstream_init (GsPluginAppstream *self)
{
GApplication *application = g_application_get_default ();
g_mutex_init (&self->silo_lock);
/* require settings */
self->settings = g_settings_new ("org.gnome.software");
/* Can be NULL when running the self tests */
if (application) {
g_signal_connect_object (application, "repository-changed",
G_CALLBACK (gs_plugin_update_cache_state_for_repository), self, G_CONNECT_SWAPPED);
}
self->file_monitors = g_ptr_array_new_with_free_func (g_object_unref);
}
static const gchar *
gs_plugin_appstream_convert_component_kind (const gchar *kind)
{
if (g_strcmp0 (kind, "webapp") == 0)
return "web-application";
if (g_strcmp0 (kind, "desktop") == 0)
return "desktop-application";
return kind;
}
static gboolean
gs_plugin_appstream_upgrade_cb (XbBuilderFixup *self,
XbBuilderNode *bn,
gpointer user_data,
GError **error)
{
if (g_strcmp0 (xb_builder_node_get_element (bn), "application") == 0) {
g_autoptr(XbBuilderNode) id = xb_builder_node_get_child (bn, "id", NULL);
g_autofree gchar *kind = NULL;
if (id != NULL) {
kind = g_strdup (xb_builder_node_get_attr (id, "type"));
xb_builder_node_remove_attr (id, "type");
}
if (kind != NULL)
xb_builder_node_set_attr (bn, "type", kind);
xb_builder_node_set_element (bn, "component");
} else if (g_strcmp0 (xb_builder_node_get_element (bn), "metadata") == 0) {
xb_builder_node_set_element (bn, "custom");
} else if (g_strcmp0 (xb_builder_node_get_element (bn), "component") == 0) {
const gchar *type_old = xb_builder_node_get_attr (bn, "type");
const gchar *type_new = gs_plugin_appstream_convert_component_kind (type_old);
if (type_old != type_new)
xb_builder_node_set_attr (bn, "type", type_new);
}
return TRUE;
}
static gboolean
gs_plugin_appstream_add_icons_cb (XbBuilderFixup *self,
XbBuilderNode *bn,
gpointer user_data,
GError **error)
{
if (g_strcmp0 (xb_builder_node_get_element (bn), "component") != 0)
return TRUE;
gs_appstream_component_add_extra_info (bn);
return TRUE;
}
static gboolean
gs_plugin_appstream_add_origin_keyword_cb (XbBuilderFixup *self,
XbBuilderNode *bn,
gpointer user_data,
GError **error)
{
if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) {
const gchar *origin = xb_builder_node_get_attr (bn, "origin");
GPtrArray *components = xb_builder_node_get_children (bn);
if (origin == NULL || origin[0] == '\0')
return TRUE;
g_debug ("origin %s has %u components", origin, components->len);
if (components->len < 200) {
for (guint i = 0; i < components->len; i++) {
XbBuilderNode *component = g_ptr_array_index (components, i);
gs_appstream_component_add_keyword (component, origin);
}
}
}
return TRUE;
}
static void
gs_plugin_appstream_media_baseurl_free (gpointer user_data)
{
g_string_free ((GString *) user_data, TRUE);
}
static gboolean
gs_plugin_appstream_media_baseurl_cb (XbBuilderFixup *self,
XbBuilderNode *bn,
gpointer user_data,
GError **error)
{
GString *baseurl = user_data;
if (g_strcmp0 (xb_builder_node_get_element (bn), "components") == 0) {
const gchar *url = xb_builder_node_get_attr (bn, "media_baseurl");
if (url == NULL) {
g_string_truncate (baseurl, 0);
return TRUE;
}
g_string_assign (baseurl, url);
return TRUE;
}
if (baseurl->len == 0)
return TRUE;
if (g_strcmp0 (xb_builder_node_get_element (bn), "icon") == 0) {
const gchar *type = xb_builder_node_get_attr (bn, "type");
if (g_strcmp0 (type, "remote") != 0)
return TRUE;
gs_appstream_component_fix_url (bn, baseurl->str);
} else if (g_strcmp0 (xb_builder_node_get_element (bn), "screenshots") == 0) {
GPtrArray *screenshots = xb_builder_node_get_children (bn);
for (guint i = 0; i < screenshots->len; i++) {
XbBuilderNode *screenshot = g_ptr_array_index (screenshots, i);
GPtrArray *children = NULL;
/* Type-check for security */
if (g_strcmp0 (xb_builder_node_get_element (screenshot), "screenshot") != 0) {
continue;
}
children = xb_builder_node_get_children (screenshot);
for (guint j = 0; j < children->len; j++) {
XbBuilderNode *child = g_ptr_array_index (children, j);
const gchar *element = xb_builder_node_get_element (child);
if (g_strcmp0 (element, "image") != 0 &&
g_strcmp0 (element, "video") != 0)
continue;
gs_appstream_component_fix_url (child, baseurl->str);
}
}
}
return TRUE;
}
static void
gs_plugin_appstream_file_monitor_changed_cb (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent event_type,
gpointer user_data)
{
GsPluginAppstream *self = user_data;
g_atomic_int_inc (&self->silo_change_stamp);
}
static void
gs_plugin_appstream_maybe_store_file_monitor (GsPluginAppstream *self,
GFileMonitor *file_monitor) /* (nullable) (transfer none) */
{
if (!file_monitor)
return;
g_signal_connect_object (file_monitor, "changed",
G_CALLBACK (gs_plugin_appstream_file_monitor_changed_cb), self, 0);
g_ptr_array_add (self->file_monitors, g_object_ref (file_monitor));
}
static gboolean
gs_plugin_appstream_load_appdata_fn (GsPluginAppstream *self,
XbBuilder *builder,
const gchar *filename,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GFile) file = g_file_new_for_path (filename);
g_autoptr(XbBuilderFixup) fixup = NULL;
g_autoptr(XbBuilderNode) info = NULL;
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
/* add source */
if (!xb_builder_source_load_file (source, file, 0, cancellable, error))
return FALSE;
/* fix up any legacy installed files */
fixup = xb_builder_fixup_new ("AppStreamUpgrade2",
gs_plugin_appstream_upgrade_cb,
self, NULL);
xb_builder_fixup_set_max_depth (fixup, 3);
xb_builder_source_add_fixup (source, fixup);
/* add metadata */
info = xb_builder_node_insert (NULL, "info", NULL);
xb_builder_node_insert_text (info, "filename", filename, NULL);
xb_builder_source_set_info (source, info);
/* success */
xb_builder_import_source (builder, source);
return TRUE;
}
static gboolean
gs_plugin_appstream_load_appdata (GsPluginAppstream *self,
XbBuilder *builder,
const gchar *path,
GCancellable *cancellable,
GError **error)
{
const gchar *fn;
g_autoptr(GDir) dir = NULL;
g_autoptr(GFile) parent = g_file_new_for_path (path);
g_autoptr(GFileMonitor) file_monitor = NULL;
g_autoptr(GError) local_error = NULL;
if (!g_file_query_exists (parent, cancellable)) {
g_debug ("appstream: Skipping appdata path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist");
return TRUE;
}
g_debug ("appstream: Loading appdata path '%s'", path);
dir = g_dir_open (path, 0, error);
if (dir == NULL)
return FALSE;
file_monitor = g_file_monitor (parent, G_FILE_MONITOR_NONE, cancellable, &local_error);
if (local_error)
g_debug ("appstream: Failed to create file monitor for '%s': %s", path, local_error->message);
gs_plugin_appstream_maybe_store_file_monitor (self, file_monitor);
while ((fn = g_dir_read_name (dir)) != NULL) {
if (g_str_has_suffix (fn, ".appdata.xml") ||
g_str_has_suffix (fn, ".metainfo.xml")) {
g_autofree gchar *filename = g_build_filename (path, fn, NULL);
g_autoptr(GError) error_local = NULL;
if (!gs_plugin_appstream_load_appdata_fn (self,
builder,
filename,
cancellable,
&error_local)) {
g_debug ("ignoring %s: %s", filename, error_local->message);
continue;
}
}
}
/* success */
return TRUE;
}
static GInputStream *
gs_plugin_appstream_load_dep11_cb (XbBuilderSource *self,
XbBuilderSourceCtx *ctx,
gpointer user_data,
GCancellable *cancellable,
GError **error)
{
g_autoptr(AsMetadata) mdata = as_metadata_new ();
g_autoptr(GBytes) bytes = NULL;
g_autoptr(GError) tmp_error = NULL;
g_autofree gchar *xml = NULL;
bytes = xb_builder_source_ctx_get_bytes (ctx, cancellable, error);
if (bytes == NULL)
return NULL;
as_metadata_set_format_style (mdata, AS_FORMAT_STYLE_CATALOG);
as_metadata_parse_bytes (mdata,
bytes,
AS_FORMAT_KIND_YAML,
&tmp_error);
if (tmp_error != NULL) {
g_propagate_error (error, g_steal_pointer (&tmp_error));
return NULL;
}
xml = as_metadata_components_to_catalog (mdata, AS_FORMAT_KIND_XML, &tmp_error);
if (xml == NULL) {
// This API currently returns NULL if there is nothing to serialize, so we
// have to test if this is an error or not.
// See https://gitlab.gnome.org/GNOME/gnome-software/-/merge_requests/763
// for discussion about changing this API.
if (tmp_error != NULL) {
g_propagate_error (error, g_steal_pointer (&tmp_error));
return NULL;
}
xml = g_strdup("");
}
return g_memory_input_stream_new_from_data (g_steal_pointer (&xml), (gssize) -1, g_free);
}
static gboolean
gs_plugin_appstream_tokenize_cb (XbBuilderFixup *self,
XbBuilderNode *bn,
gpointer user_data,
GError **error)
{
const gchar * const elements_to_tokenize[] = {
"id",
"keyword",
"launchable",
"mimetype",
"name",
"pkgname",
"summary",
NULL };
if (xb_builder_node_get_element (bn) != NULL &&
g_strv_contains (elements_to_tokenize, xb_builder_node_get_element (bn)))
xb_builder_node_tokenize_text (bn);
return TRUE;
}
static gboolean
gs_plugin_appstream_load_appstream_fn (GsPluginAppstream *self,
XbBuilder *builder,
const gchar *filename,
GCancellable *cancellable,
GError **error)
{
g_autoptr(GFile) file = g_file_new_for_path (filename);
g_autoptr(XbBuilderNode) info = NULL;
g_autoptr(XbBuilderFixup) fixup1 = NULL;
g_autoptr(XbBuilderFixup) fixup2 = NULL;
g_autoptr(XbBuilderFixup) fixup3 = NULL;
g_autoptr(XbBuilderFixup) fixup4 = NULL;
g_autoptr(XbBuilderFixup) fixup5 = NULL;
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
/* add support for DEP-11 files */
xb_builder_source_add_adapter (source,
"application/yaml",
gs_plugin_appstream_load_dep11_cb,
NULL, NULL);
xb_builder_source_add_adapter (source,
"application/x-yaml",
gs_plugin_appstream_load_dep11_cb,
NULL, NULL);
/* add source */
if (!xb_builder_source_load_file (source, file, 0, cancellable, error))
return FALSE;
/* add metadata */
info = xb_builder_node_insert (NULL, "info", NULL);
xb_builder_node_insert_text (info, "scope", "system", NULL);
xb_builder_node_insert_text (info, "filename", filename, NULL);
xb_builder_source_set_info (source, info);
/* add missing icons as required */
fixup1 = xb_builder_fixup_new ("AddIcons",
gs_plugin_appstream_add_icons_cb,
self, NULL);
xb_builder_fixup_set_max_depth (fixup1, 2);
xb_builder_source_add_fixup (source, fixup1);
/* fix up any legacy installed files */
fixup2 = xb_builder_fixup_new ("AppStreamUpgrade2",
gs_plugin_appstream_upgrade_cb,
self, NULL);
xb_builder_fixup_set_max_depth (fixup2, 3);
xb_builder_source_add_fixup (source, fixup2);
/* add the origin as a search keyword for small repos */
fixup3 = xb_builder_fixup_new ("AddOriginKeyword",
gs_plugin_appstream_add_origin_keyword_cb,
self, NULL);
xb_builder_fixup_set_max_depth (fixup3, 1);
xb_builder_source_add_fixup (source, fixup3);
fixup4 = xb_builder_fixup_new ("TextTokenize",
gs_plugin_appstream_tokenize_cb,
NULL, NULL);
xb_builder_fixup_set_max_depth (fixup4, 2);
xb_builder_source_add_fixup (source, fixup4);
/* prepend media_baseurl to remote relative URLs */
fixup5 = xb_builder_fixup_new ("MediaBaseUrl",
gs_plugin_appstream_media_baseurl_cb,
g_string_new (NULL),
gs_plugin_appstream_media_baseurl_free);
xb_builder_fixup_set_max_depth (fixup5, 3);
xb_builder_source_add_fixup (source, fixup5);
/* success */
xb_builder_import_source (builder, source);
return TRUE;
}
static gboolean
gs_plugin_appstream_load_appstream (GsPluginAppstream *self,
XbBuilder *builder,
const gchar *path,
GCancellable *cancellable,
GError **error)
{
const gchar *fn;
g_autoptr(GDir) dir = NULL;
g_autoptr(GFile) parent = g_file_new_for_path (path);
g_autoptr(GFileMonitor) file_monitor = NULL;
g_autoptr(GError) local_error = NULL;
/* in case the path appears later, to refresh the data even when non-existent at the moment */
file_monitor = g_file_monitor (parent, G_FILE_MONITOR_NONE, cancellable, &local_error);
if (local_error)
g_debug ("appstream: Failed to create file monitor for '%s': %s", path, local_error->message);
gs_plugin_appstream_maybe_store_file_monitor (self, file_monitor);
/* parent path does not exist */
if (!g_file_query_exists (parent, cancellable)) {
g_debug ("appstream: Skipping appstream path '%s' as %s", path, g_cancellable_is_cancelled (cancellable) ? "cancelled" : "does not exist");
return TRUE;
}
g_debug ("appstream: Loading appstream path '%s'", path);
dir = g_dir_open (path, 0, error);
if (dir == NULL)
return FALSE;
while ((fn = g_dir_read_name (dir)) != NULL) {
#ifdef ENABLE_EXTERNAL_APPSTREAM
/* Ignore our own system-installed files when
external-appstream-system-wide is FALSE */
if (!g_settings_get_boolean (self->settings, "external-appstream-system-wide") &&
g_strcmp0 (path, gs_external_appstream_utils_get_system_dir ()) == 0 &&
g_str_has_prefix (fn, EXTERNAL_APPSTREAM_PREFIX))
continue;
#endif
if (g_str_has_suffix (fn, ".xml") ||
g_str_has_suffix (fn, ".yml") ||
g_str_has_suffix (fn, ".yml.gz") ||
g_str_has_suffix (fn, ".xml.gz")) {
g_autofree gchar *filename = g_build_filename (path, fn, NULL);
g_autoptr(GError) error_local = NULL;
if (!gs_plugin_appstream_load_appstream_fn (self,
builder,
filename,
cancellable,
&error_local)) {
g_debug ("ignoring %s: %s", filename, error_local->message);
continue;
}
}
}
/* success */
return TRUE;
}
static void
gs_add_appstream_metainfo_location (GPtrArray *locations, const gchar *root)
{
g_ptr_array_add (locations,
g_build_filename (root, "metainfo", NULL));
g_ptr_array_add (locations,
g_build_filename (root, "appdata", NULL));
}
static XbSilo *
gs_plugin_appstream_ref_silo (GsPluginAppstream *self,
gchar **out_silo_filename,
GHashTable **out_silo_installed_by_desktopid,
GHashTable **out_silo_installed_by_id,
GCancellable *cancellable,
GError **error)
{
const gchar *test_xml;
g_autofree gchar *blobfn = NULL;
g_autoptr(XbBuilder) builder = NULL;
g_autoptr(XbNode) n = NULL;
g_autoptr(GFile) file = NULL;
g_autoptr(GPtrArray) installed = NULL;
g_autoptr(GMutexLocker) locker = NULL;
g_autoptr(GPtrArray) parent_appdata = g_ptr_array_new_with_free_func (g_free);
g_autoptr(GPtrArray) parent_appstream = NULL;
g_autoptr(GMainContext) old_thread_default = NULL;
locker = g_mutex_locker_new (&self->silo_lock);
/* everything is okay */
if (self->silo != NULL && xb_silo_is_valid (self->silo) &&
g_atomic_int_get (&self->silo_change_stamp_current) == g_atomic_int_get (&self->silo_change_stamp)) {
if (out_silo_filename != NULL)
*out_silo_filename = g_strdup (self->silo_filename);
if (out_silo_installed_by_desktopid != NULL)
*out_silo_installed_by_desktopid = self->silo_installed_by_desktopid ? g_hash_table_ref (self->silo_installed_by_desktopid) : NULL;
if (out_silo_installed_by_id != NULL)
*out_silo_installed_by_id = self->silo_installed_by_id ? g_hash_table_ref (self->silo_installed_by_id) : NULL;
return g_object_ref (self->silo);
}
/* drat! silo needs regenerating */
reload:
g_clear_object (&self->silo);
g_clear_pointer (&self->silo_filename, g_free);
g_clear_pointer (&self->silo_installed_by_desktopid, g_hash_table_unref);
g_clear_pointer (&self->silo_installed_by_id, g_hash_table_unref);
g_clear_pointer (&blobfn, g_free);
self->default_scope = AS_COMPONENT_SCOPE_UNKNOWN;
g_ptr_array_set_size (self->file_monitors, 0);
g_atomic_int_set (&self->silo_change_stamp_current, g_atomic_int_get (&self->silo_change_stamp));
/* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */
old_thread_default = g_main_context_ref_thread_default ();
if (old_thread_default == g_main_context_default ())
g_clear_pointer (&old_thread_default, g_main_context_unref);
if (old_thread_default != NULL)
g_main_context_pop_thread_default (old_thread_default);
builder = xb_builder_new ();
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
g_clear_pointer (&old_thread_default, g_main_context_unref);
/* verbose profiling */
if (g_getenv ("GS_XMLB_VERBOSE") != NULL) {
xb_builder_set_profile_flags (builder,
XB_SILO_PROFILE_FLAG_XPATH |
XB_SILO_PROFILE_FLAG_DEBUG);
}
gs_appstream_add_current_locales (builder);
/* only when in self test */
test_xml = g_getenv ("GS_SELF_TEST_APPSTREAM_XML");
if (test_xml != NULL) {
g_autoptr(XbBuilderFixup) fixup1 = NULL;
g_autoptr(XbBuilderFixup) fixup2 = NULL;
g_autoptr(XbBuilderSource) source = xb_builder_source_new ();
if (!xb_builder_source_load_xml (source, test_xml,
XB_BUILDER_SOURCE_FLAG_NONE,
error))
return NULL;
fixup1 = xb_builder_fixup_new ("AddOriginKeywords",
gs_plugin_appstream_add_origin_keyword_cb,
self, NULL);
xb_builder_fixup_set_max_depth (fixup1, 1);
xb_builder_source_add_fixup (source, fixup1);
fixup2 = xb_builder_fixup_new ("AddIcons",
gs_plugin_appstream_add_icons_cb,
self, NULL);
xb_builder_fixup_set_max_depth (fixup2, 2);
xb_builder_source_add_fixup (source, fixup2);
xb_builder_import_source (builder, source);
/* Nothing to watch in the tests */
parent_appstream = g_ptr_array_new_with_free_func (g_free);
} else {
g_autoptr(GPtrArray) parent_desktop = g_ptr_array_new ();
g_ptr_array_add (parent_desktop, (gpointer) DATADIR "/applications");
if (g_strcmp0 (DATADIR, "/usr/share") != 0)
g_ptr_array_add (parent_desktop, (gpointer) "/usr/share/applications");
/* add search paths */
parent_appstream = gs_appstream_get_appstream_data_dirs ();
gs_add_appstream_metainfo_location (parent_appdata, DATADIR);
/* Add the normal system directories if the installation prefix
* is different from normal — typically this happens when doing
* development builds. Its useful to still list the system apps
* during development. */
if (g_strcmp0 (DATADIR, "/usr/share") != 0)
gs_add_appstream_metainfo_location (parent_appdata, "/usr/share");
/* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */
old_thread_default = g_main_context_ref_thread_default ();
if (old_thread_default == g_main_context_default ())
g_clear_pointer (&old_thread_default, g_main_context_unref);
if (old_thread_default != NULL)
g_main_context_pop_thread_default (old_thread_default);
/* import all files */
for (guint i = 0; i < parent_appstream->len; i++) {
const gchar *fn = g_ptr_array_index (parent_appstream, i);
if (!gs_plugin_appstream_load_appstream (self, builder, fn, cancellable, error)) {
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
return NULL;
}
}
for (guint i = 0; i < parent_appdata->len; i++) {
const gchar *fn = g_ptr_array_index (parent_appdata, i);
if (!gs_plugin_appstream_load_appdata (self, builder, fn, cancellable, error)) {
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
return NULL;
}
}
for (guint i = 0; i < parent_desktop->len; i++) {
g_autoptr(GFileMonitor) file_monitor = NULL;
const gchar *dir = g_ptr_array_index (parent_desktop, i);
if (!gs_appstream_load_desktop_files (builder, dir, NULL, &file_monitor, cancellable, error)) {
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
return NULL;
}
gs_plugin_appstream_maybe_store_file_monitor (self, file_monitor);
}
gs_appstream_add_data_merge_fixup (builder, parent_appstream, parent_desktop, cancellable);
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
g_clear_pointer (&old_thread_default, g_main_context_unref);
}
/* regenerate with each minor release */
xb_builder_append_guid (builder, PACKAGE_VERSION);
/* create per-user cache */
blobfn = gs_utils_get_cache_filename ("appstream", "components.xmlb",
GS_UTILS_CACHE_FLAG_WRITEABLE |
GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
error);
if (blobfn == NULL)
return NULL;
file = g_file_new_for_path (blobfn);
g_debug ("ensuring %s", blobfn);
/* FIXME: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/1422 */
old_thread_default = g_main_context_ref_thread_default ();
if (old_thread_default == g_main_context_default ())
g_clear_pointer (&old_thread_default, g_main_context_unref);
if (old_thread_default != NULL)
g_main_context_pop_thread_default (old_thread_default);
self->silo = xb_builder_ensure (builder, file,
XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID |
XB_BUILDER_COMPILE_FLAG_SINGLE_LANG,
NULL, error);
if (self->silo == NULL) {
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
return NULL;
}
#ifdef __GLIBC__
/* https://gitlab.gnome.org/GNOME/gnome-software/-/issues/941
* libxmlb <= 0.3.22 makes lots of temporary heap allocations parsing large XMLs
* trim the heap after parsing to control RSS growth. */
malloc_trim (0);
#endif
if (old_thread_default != NULL)
g_main_context_push_thread_default (old_thread_default);
if (g_atomic_int_get (&self->silo_change_stamp_current) != g_atomic_int_get (&self->silo_change_stamp)) {
g_ptr_array_set_size (parent_appdata, 0);
g_ptr_array_set_size (parent_appstream, 0);
g_debug ("appstream: File monitors reported change while loading appstream data, reloading...");
goto reload;
}
/* test we found something */
n = xb_silo_query_first (self->silo, "components/component", NULL);
if (n == NULL) {
g_warning ("No AppStream data, try 'make install-sample-data' in data/");
g_set_error (error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_NOT_SUPPORTED,
"No AppStream data found");
return NULL;
}
g_clear_object (&n);
self->silo_installed_by_desktopid = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
self->silo_installed_by_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
installed = xb_silo_query (self->silo, "/component[@type='desktop-application']/launchable[@type='desktop-id']", 0, NULL);
for (guint i = 0; installed != NULL && i < installed->len; i++) {
XbNode *launchable = g_ptr_array_index (installed, i);
const gchar *id = xb_node_get_text (launchable);
if (id != NULL && *id != '\0') {
GPtrArray *nodes = g_hash_table_lookup (self->silo_installed_by_desktopid, id);
if (nodes == NULL) {
nodes = g_ptr_array_new_with_free_func (g_object_unref);
g_hash_table_insert (self->silo_installed_by_desktopid, g_strdup (id), nodes);
}
g_ptr_array_add (nodes, xb_node_get_parent (launchable));
}
}
g_clear_pointer (&installed, g_ptr_array_unref);
installed = xb_silo_query (self->silo, "/component/id", 0, NULL);
for (guint i = 0; installed != NULL && i < installed->len; i++) {
XbNode *id_node = g_ptr_array_index (installed, i);
const gchar *id = xb_node_get_text (id_node);
if (id != NULL && *id != '\0')
g_hash_table_add (self->silo_installed_by_id, g_strdup (id));
}
n = xb_silo_query_first (self->silo, "info", NULL);
if (n != NULL) {
g_autoptr(XbNode) child = NULL;
g_autoptr(XbNode) next = NULL;
for (child = xb_node_get_child (n);
child != NULL && (self->silo_filename == NULL || self->default_scope == AS_COMPONENT_SCOPE_UNKNOWN);
g_object_unref (child), child = g_steal_pointer (&next)) {
const gchar *elem = xb_node_get_element (child);
next = xb_node_get_next (child);
if (self->silo_filename == NULL && g_strcmp0 (elem, "filename") == 0) {
self->silo_filename = g_strdup (xb_node_get_text (child));
} else if (self->default_scope == AS_COMPONENT_SCOPE_UNKNOWN && g_strcmp0 (elem, "scope") == 0) {
const gchar *tmp = xb_node_get_text (child);
if (tmp != NULL)
self->default_scope = as_component_scope_from_string (tmp);
}
}
}
/* success */
if (out_silo_filename != NULL)
*out_silo_filename = g_strdup (self->silo_filename);
if (out_silo_installed_by_desktopid != NULL)
*out_silo_installed_by_desktopid = self->silo_installed_by_desktopid ? g_hash_table_ref (self->silo_installed_by_desktopid) : NULL;
if (out_silo_installed_by_id != NULL)
*out_silo_installed_by_id = self->silo_installed_by_id ? g_hash_table_ref (self->silo_installed_by_id) : NULL;
return g_object_ref (self->silo);
}
static void
gs_plugin_appstream_reload (GsPlugin *plugin)
{
GsPluginAppstream *self;
g_autoptr(GsAppList) list = NULL;
guint sz;
g_return_if_fail (GS_IS_PLUGIN_APPSTREAM (plugin));
list = gs_plugin_list_cached (plugin);
sz = gs_app_list_length (list);
for (guint i = 0; i < sz; i++) {
GsApp *app = gs_app_list_index (list, i);
/* to ensure the app states are refined */
gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
}
self = GS_PLUGIN_APPSTREAM (plugin);
/* Invalidate the reference to the current silo */
g_atomic_int_inc (&self->silo_change_stamp);
}
static gint
get_priority_for_interactivity (gboolean interactive)
{
return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW;
}
static void setup_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_appstream_setup_async (GsPlugin *plugin,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_setup_async);
/* Start up a worker thread to process all the plugins function calls. */
self->worker = gs_worker_thread_new ("gs-plugin-appstream");
/* Queue a job to check the silo, which will cause it to be loaded. */
gs_worker_thread_queue (self->worker, G_PRIORITY_DEFAULT,
setup_thread_cb, g_steal_pointer (&task));
}
/* Run in @worker. */
static void
setup_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
g_autoptr(XbSilo) silo = NULL;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
silo = gs_plugin_appstream_ref_silo (self, NULL, NULL, NULL, cancellable, &local_error);
if (silo == NULL)
g_task_return_error (task, g_steal_pointer (&local_error));
else
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_appstream_setup_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void shutdown_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void
gs_plugin_appstream_shutdown_async (GsPlugin *plugin,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_shutdown_async);
/* Stop the worker thread. */
gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
}
static void
shutdown_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr(GTask) task = G_TASK (user_data);
GsPluginAppstream *self = g_task_get_source_object (task);
g_autoptr(GsWorkerThread) worker = NULL;
g_autoptr(GError) local_error = NULL;
worker = g_steal_pointer (&self->worker);
if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_appstream_shutdown_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
/* Run in @worker. */
static void
url_to_app_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
GsPluginUrlToAppData *data = task_data;
g_autoptr(GsAppList) list = NULL;
g_autoptr(XbSilo) silo = NULL;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
/* check silo is valid */
silo = gs_plugin_appstream_ref_silo (self, NULL, NULL, NULL, cancellable, &local_error);
if (silo == NULL) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
list = gs_app_list_new ();
if (gs_appstream_url_to_app (GS_PLUGIN (self), silo, list, data->url, cancellable, &local_error))
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
else
g_task_return_error (task, g_steal_pointer (&local_error));
}
static void
gs_plugin_appstream_url_to_app_async (GsPlugin *plugin,
const gchar *url,
GsPluginUrlToAppFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = (flags & GS_PLUGIN_URL_TO_APP_FLAGS_INTERACTIVE) != 0;
task = gs_plugin_url_to_app_data_new_task (plugin, url, flags, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_url_to_app_async);
/* Queue a job for the refine. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
url_to_app_thread_cb, g_steal_pointer (&task));
}
static GsAppList *
gs_plugin_appstream_url_to_app_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
gs_plugin_appstream_set_compulsory_quirk (GsApp *app, XbNode *component)
{
g_autoptr(GPtrArray) array = NULL;
const gchar *current_desktop;
/*
* Set the core applications for the current desktop that cannot be
* removed.
*
* If XDG_CURRENT_DESKTOP contains ":", indicating that it is made up
* of multiple components per the Desktop Entry Specification, an app
* is compulsory if any of the components in XDG_CURRENT_DESKTOP match
* any value in <compulsory_for_desktops />. In that way,
* "GNOME-Classic:GNOME" shares compulsory apps with GNOME.
*
* As a special case, if the <compulsory_for_desktop /> value contains
* a ":", we match the entire XDG_CURRENT_DESKTOP. This lets people set
* compulsory apps for such compound desktops if they want.
*
*/
array = xb_node_query (component, "compulsory_for_desktop", 0, NULL);
if (array == NULL)
return;
current_desktop = g_getenv ("XDG_CURRENT_DESKTOP");
if (current_desktop != NULL) {
g_auto(GStrv) xdg_current_desktops = g_strsplit (current_desktop, ":", 0);
for (guint i = 0; i < array->len; i++) {
XbNode *n = g_ptr_array_index (array, i);
const gchar *tmp = xb_node_get_text (n);
/* if the value has a :, check the whole string */
if (g_strstr_len (tmp, -1, ":")) {
if (g_strcmp0 (current_desktop, tmp) == 0) {
gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY);
break;
}
/* otherwise check if any element matches this one */
} else if (g_strv_contains ((const gchar * const *) xdg_current_desktops, tmp)) {
gs_app_add_quirk (app, GS_APP_QUIRK_COMPULSORY);
break;
}
}
}
}
static gboolean
gs_plugin_appstream_refine_state (GsPluginAppstream *self,
GsApp *app,
GHashTable *silo_installed_by_id,
GError **error)
{
/* Ignore apps with no ID */
if (gs_app_get_id (app) == NULL || silo_installed_by_id == NULL)
return TRUE;
if (g_hash_table_contains (silo_installed_by_id, gs_app_get_id (app)))
gs_app_set_state (app, GS_APP_STATE_INSTALLED);
return TRUE;
}
static gboolean
gs_plugin_refine_from_id (GsPluginAppstream *self,
GsApp *app,
GsPluginRefineFlags flags,
GHashTable *apps_by_id,
GHashTable *apps_by_origin_and_id,
XbSilo *silo,
const gchar *silo_filename,
GHashTable *silo_installed_by_desktopid,
GHashTable *silo_installed_by_id,
gboolean *found,
GError **error)
{
const gchar *id, *origin;
GPtrArray *components;
/* not enough info to find */
id = gs_app_get_id (app);
if (id == NULL)
return TRUE;
origin = gs_app_get_origin_appstream (app);
/* look in AppStream then fall back to AppData */
if (origin && *origin) {
g_autofree gchar *key = g_strconcat (origin, "\n", id, NULL);
components = g_hash_table_lookup (apps_by_origin_and_id, key);
} else {
components = g_hash_table_lookup (apps_by_id, id);
}
if (components == NULL)
return TRUE;
for (guint i = 0; i < components->len; i++) {
XbNode *component = g_ptr_array_index (components, i);
if (!gs_appstream_refine_app (GS_PLUGIN (self), app, silo, component, flags, silo_installed_by_desktopid,
silo_filename ? silo_filename : "", self->default_scope, error))
return FALSE;
gs_plugin_appstream_set_compulsory_quirk (app, component);
}
/* if an installed desktop or appdata file exists set to installed */
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
if (!gs_plugin_appstream_refine_state (self, app, silo_installed_by_id, error))
return FALSE;
}
/* success */
*found = TRUE;
return TRUE;
}
static gboolean
gs_plugin_refine_from_pkgname (GsPluginAppstream *self,
GsApp *app,
GsPluginRefineFlags flags,
XbSilo *silo,
const gchar *silo_filename,
GHashTable *silo_installed_by_desktopid,
GHashTable *silo_installed_by_id,
GError **error)
{
GPtrArray *sources = gs_app_get_sources (app);
g_autoptr(GError) error_local = NULL;
/* not enough info to find */
if (sources->len == 0)
return TRUE;
/* find all apps when matching any prefixes */
for (guint j = 0; j < sources->len; j++) {
const gchar *pkgname = g_ptr_array_index (sources, j);
g_autoptr(GString) xpath = g_string_new (NULL);
g_autoptr(XbNode) component = NULL;
/* prefer actual apps and then fallback to anything else */
xb_string_append_union (xpath, "components/component[@type='desktop-application']/pkgname[text()='%s']/..", pkgname);
xb_string_append_union (xpath, "components/component[@type='console-application']/pkgname[text()='%s']/..", pkgname);
xb_string_append_union (xpath, "components/component[@type='web-application']/pkgname[text()='%s']/..", pkgname);
xb_string_append_union (xpath, "components/component/pkgname[text()='%s']/..", pkgname);
component = xb_silo_query_first (silo, xpath->str, &error_local);
if (component == NULL) {
if (g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_NOT_FOUND))
continue;
g_propagate_error (error, g_steal_pointer (&error_local));
return FALSE;
}
if (!gs_appstream_refine_app (GS_PLUGIN (self), app, silo, component, flags, silo_installed_by_desktopid,
silo_filename ? silo_filename : "", self->default_scope, error))
return FALSE;
gs_plugin_appstream_set_compulsory_quirk (app, component);
}
/* if an installed desktop or appdata file exists set to installed */
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
if (!gs_plugin_appstream_refine_state (self, app, silo_installed_by_id, error))
return FALSE;
}
/* success */
return TRUE;
}
static void refine_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_appstream_refine_async (GsPlugin *plugin,
GsAppList *list,
GsPluginRefineFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_refine_async);
/* Queue a job for the refine. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
refine_thread_cb, g_steal_pointer (&task));
}
static gboolean refine_wildcard (GsPluginAppstream *self,
GsApp *app,
GsAppList *list,
GsPluginRefineFlags refine_flags,
GHashTable *apps_by_id,
XbSilo *silo,
const gchar *silo_filename,
GHashTable *silo_installed_by_desktopid,
GHashTable *silo_installed_by_id,
GCancellable *cancellable,
GError **error);
/* Run in @worker. */
static void
refine_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
GsPluginRefineData *data = task_data;
GsAppList *list = data->list;
GsPluginRefineFlags flags = data->flags;
g_autoptr(GsAppList) app_list = NULL;
g_autoptr(GHashTable) apps_by_id = NULL;
g_autoptr(GHashTable) apps_by_origin_and_id = NULL;
g_autoptr(GPtrArray) components = NULL;
g_autoptr(XbSilo) silo = NULL;
g_autofree gchar *silo_filename = NULL;
g_autoptr(GHashTable) silo_installed_by_desktopid = NULL;
g_autoptr(GHashTable) silo_installed_by_id = NULL;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
/* check silo is valid */
silo = gs_plugin_appstream_ref_silo (self, &silo_filename, &silo_installed_by_desktopid, &silo_installed_by_id, cancellable, &local_error);
if (silo == NULL) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
apps_by_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
apps_by_origin_and_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_ptr_array_unref);
components = xb_silo_query (silo, "components/component/id", 0, NULL);
for (guint i = 0; components != NULL && i < components->len; i++) {
XbNode *node = g_ptr_array_index (components, i);
g_autoptr(XbNode) component_node = xb_node_get_parent (node);
g_autoptr(XbNode) components_node = xb_node_get_parent (component_node);
GPtrArray *comps;
const gchar *comp_id = xb_node_get_text (node);
const gchar *origin;
/* discard web-apps */
if (g_strcmp0 (xb_node_get_attr (component_node, "type"), "web-application") != 0) {
g_autoptr(XbNode) child = NULL;
g_autoptr(XbNode) next = NULL;
gboolean found_pkgname = FALSE;
for (child = xb_node_get_child (component_node); child != NULL; g_object_unref (child), child = g_steal_pointer (&next)) {
next = xb_node_get_next (child);
if (g_strcmp0 (xb_node_get_element (child), "pkgname") == 0) {
found_pkgname = TRUE;
break;
}
}
if (!found_pkgname)
continue;
}
comps = g_hash_table_lookup (apps_by_id, comp_id);
if (comps == NULL) {
comps = g_ptr_array_new_with_free_func (g_object_unref);
g_hash_table_insert (apps_by_id, g_strdup (comp_id), comps);
}
g_ptr_array_add (comps, g_object_ref (component_node));
origin = xb_node_get_attr (components_node, "origin");
if (origin != NULL) {
g_autofree gchar *key = g_strconcat (origin, "\n", comp_id, NULL);
comps = g_hash_table_lookup (apps_by_origin_and_id, key);
if (comps == NULL) {
comps = g_ptr_array_new_with_free_func (g_object_unref);
g_hash_table_insert (apps_by_origin_and_id, g_steal_pointer (&key), comps);
}
g_ptr_array_add (comps, g_object_ref (component_node));
}
}
g_clear_pointer (&components, g_ptr_array_unref);
components = xb_silo_query (silo, "component/id", 0, NULL);
for (guint i = 0; components != NULL && i < components->len; i++) {
XbNode *node = g_ptr_array_index (components, i);
g_autoptr(XbNode) component_node = xb_node_get_parent (node);
GPtrArray *comps;
const gchar *comp_id = xb_node_get_text (node);
comps = g_hash_table_lookup (apps_by_id, comp_id);
if (comps == NULL) {
comps = g_ptr_array_new_with_free_func (g_object_unref);
g_hash_table_insert (apps_by_id, g_strdup (comp_id), comps);
}
g_ptr_array_add (comps, g_object_ref (component_node));
}
for (guint i = 0; i < gs_app_list_length (list); i++) {
gboolean found = FALSE;
GsApp *app = gs_app_list_index (list, i);
/* not us */
if (gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE &&
gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_UNKNOWN)
continue;
if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
continue;
/* find by ID then fall back to package name */
if (!gs_plugin_refine_from_id (self, app, flags, apps_by_id, apps_by_origin_and_id, silo, silo_filename,
silo_installed_by_desktopid, silo_installed_by_id, &found, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (!found) {
if (!gs_plugin_refine_from_pkgname (self, app, flags, silo, silo_filename,
silo_installed_by_desktopid, silo_installed_by_id, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
}
}
/* Refine wildcards.
*
* 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);
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) &&
!refine_wildcard (self, app, list, flags, apps_by_id, silo, silo_filename,
silo_installed_by_desktopid, silo_installed_by_id,cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
}
/* success */
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_appstream_refine_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
/* Run in @worker. Silo must be valid */
static gboolean
refine_wildcard (GsPluginAppstream *self,
GsApp *app,
GsAppList *list,
GsPluginRefineFlags refine_flags,
GHashTable *apps_by_id,
XbSilo *silo,
const gchar *silo_filename,
GHashTable *silo_installed_by_desktopid,
GHashTable *silo_installed_by_id,
GCancellable *cancellable,
GError **error)
{
const gchar *id;
GPtrArray *components;
/* not enough info to find */
id = gs_app_get_id (app);
if (id == NULL)
return TRUE;
components = g_hash_table_lookup (apps_by_id, id);
if (components == NULL)
return TRUE;
for (guint i = 0; i < components->len; i++) {
XbNode *component = g_ptr_array_index (components, i);
g_autoptr(GsApp) new = NULL;
/* new app */
new = gs_appstream_create_app (GS_PLUGIN (self), silo, component, silo_filename ? silo_filename : "",
self->default_scope, error);
if (new == NULL)
return FALSE;
gs_app_set_scope (new, AS_COMPONENT_SCOPE_SYSTEM);
gs_app_subsume_metadata (new, app);
if (!gs_appstream_refine_app (GS_PLUGIN (self), new, silo, component, refine_flags, silo_installed_by_desktopid,
silo_filename ? silo_filename : "", self->default_scope, error))
return FALSE;
gs_plugin_appstream_set_compulsory_quirk (new, component);
/* if an installed desktop or appdata file exists set to installed */
if (gs_app_get_state (new) == GS_APP_STATE_UNKNOWN) {
if (!gs_plugin_appstream_refine_state (self, new, silo_installed_by_id, error))
return FALSE;
}
gs_app_list_add (list, new);
}
/* success */
return TRUE;
}
static void refine_categories_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_appstream_refine_categories_async (GsPlugin *plugin,
GPtrArray *list,
GsPluginRefineCategoriesFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = (flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_INTERACTIVE);
task = gs_plugin_refine_categories_data_new_task (plugin, list, flags,
cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_refine_categories_async);
/* All we actually do is add the sizes of each category. If thats
* not been requested, avoid queueing a worker job. */
if (!(flags & GS_PLUGIN_REFINE_CATEGORIES_FLAGS_SIZE)) {
g_task_return_boolean (task, TRUE);
return;
}
/* Queue a job to get the apps. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
refine_categories_thread_cb, g_steal_pointer (&task));
}
/* Run in @worker. */
static void
refine_categories_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
g_autoptr(XbSilo) silo = NULL;
GsPluginRefineCategoriesData *data = task_data;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
/* check silo is valid */
silo = gs_plugin_appstream_ref_silo (self, NULL, NULL, NULL, cancellable, &local_error);
if (silo == NULL) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (!gs_appstream_refine_category_sizes (silo, data->list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_appstream_refine_categories_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void list_apps_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_appstream_list_apps_async (GsPlugin *plugin,
GsAppQuery *query,
GsPluginListAppsFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
task = gs_plugin_list_apps_data_new_task (plugin, query, flags,
cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_list_apps_async);
/* Queue a job to get the apps. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
list_apps_thread_cb, g_steal_pointer (&task));
}
/* Run in @worker. */
static void
list_apps_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
g_autoptr(XbSilo) silo = NULL;
g_autoptr(GsAppList) list = gs_app_list_new ();
GsPluginListAppsData *data = task_data;
GDateTime *released_since = NULL;
GsAppQueryTristate is_curated = GS_APP_QUERY_TRISTATE_UNSET;
GsAppQueryTristate is_featured = GS_APP_QUERY_TRISTATE_UNSET;
GsCategory *category = NULL;
GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET;
guint64 age_secs = 0;
const gchar * const *deployment_featured = NULL;
const gchar * const *developers = NULL;
const gchar * const *keywords = NULL;
GsApp *alternate_of = NULL;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
if (data->query != NULL) {
released_since = gs_app_query_get_released_since (data->query);
is_curated = gs_app_query_get_is_curated (data->query);
is_featured = gs_app_query_get_is_featured (data->query);
category = gs_app_query_get_category (data->query);
is_installed = gs_app_query_get_is_installed (data->query);
deployment_featured = gs_app_query_get_deployment_featured (data->query);
developers = gs_app_query_get_developers (data->query);
keywords = gs_app_query_get_keywords (data->query);
alternate_of = gs_app_query_get_alternate_of (data->query);
}
if (released_since != NULL) {
g_autoptr(GDateTime) now = g_date_time_new_now_utc ();
age_secs = g_date_time_difference (now, released_since) / G_TIME_SPAN_SECOND;
}
/* Currently only support a subset of query properties, and only one set at once.
* Also dont currently support GS_APP_QUERY_TRISTATE_FALSE. */
if ((released_since == NULL &&
is_curated == GS_APP_QUERY_TRISTATE_UNSET &&
is_featured == GS_APP_QUERY_TRISTATE_UNSET &&
category == NULL &&
is_installed == GS_APP_QUERY_TRISTATE_UNSET &&
deployment_featured == NULL &&
developers == NULL &&
keywords == NULL &&
alternate_of == NULL) ||
is_curated == GS_APP_QUERY_TRISTATE_FALSE ||
is_featured == GS_APP_QUERY_TRISTATE_FALSE ||
is_installed == GS_APP_QUERY_TRISTATE_FALSE ||
gs_app_query_get_n_properties_set (data->query) != 1) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Unsupported query");
return;
}
/* check silo is valid */
silo = gs_plugin_appstream_ref_silo (self, NULL, NULL, NULL, cancellable, &local_error);
if (silo == NULL) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (released_since != NULL &&
!gs_appstream_add_recent (GS_PLUGIN (self), silo, list, age_secs,
cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (is_curated != GS_APP_QUERY_TRISTATE_UNSET &&
!gs_appstream_add_popular (silo, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (is_featured != GS_APP_QUERY_TRISTATE_UNSET &&
!gs_appstream_add_featured (silo, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (category != NULL &&
!gs_appstream_add_category_apps (GS_PLUGIN (self), silo, category, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (is_installed == GS_APP_QUERY_TRISTATE_TRUE &&
!gs_appstream_add_installed (GS_PLUGIN (self), silo, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (deployment_featured != NULL &&
!gs_appstream_add_deployment_featured (silo, deployment_featured, list,
cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (developers != NULL &&
!gs_appstream_search_developer_apps (GS_PLUGIN (self), silo, developers, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (keywords != NULL &&
!gs_appstream_search (GS_PLUGIN (self), silo, keywords, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
if (alternate_of != NULL &&
!gs_appstream_add_alternates (silo, alternate_of, list, cancellable, &local_error)) {
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
}
static GsAppList *
gs_plugin_appstream_list_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_pointer (G_TASK (result), error);
}
static void refresh_metadata_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable);
static void
gs_plugin_appstream_refresh_metadata_async (GsPlugin *plugin,
guint64 cache_age_secs,
GsPluginRefreshMetadataFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = (flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE);
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_appstream_refresh_metadata_async);
/* Queue a job to check the silo, which will cause it to be refreshed if needed. */
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
refresh_metadata_thread_cb, g_steal_pointer (&task));
}
/* Run in @worker. */
static void
refresh_metadata_thread_cb (GTask *task,
gpointer source_object,
gpointer task_data,
GCancellable *cancellable)
{
GsPluginAppstream *self = GS_PLUGIN_APPSTREAM (source_object);
g_autoptr(XbSilo) silo = NULL;
g_autoptr(GError) local_error = NULL;
assert_in_worker (self);
/* Checking the silo will refresh it if needed. */
silo = gs_plugin_appstream_ref_silo (self, NULL, NULL, NULL, cancellable, &local_error);
if (silo == NULL)
g_task_return_error (task, g_steal_pointer (&local_error));
else
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_appstream_refresh_metadata_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void
gs_plugin_appstream_class_init (GsPluginAppstreamClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
object_class->dispose = gs_plugin_appstream_dispose;
plugin_class->reload = gs_plugin_appstream_reload;
plugin_class->setup_async = gs_plugin_appstream_setup_async;
plugin_class->setup_finish = gs_plugin_appstream_setup_finish;
plugin_class->shutdown_async = gs_plugin_appstream_shutdown_async;
plugin_class->shutdown_finish = gs_plugin_appstream_shutdown_finish;
plugin_class->refine_async = gs_plugin_appstream_refine_async;
plugin_class->refine_finish = gs_plugin_appstream_refine_finish;
plugin_class->list_apps_async = gs_plugin_appstream_list_apps_async;
plugin_class->list_apps_finish = gs_plugin_appstream_list_apps_finish;
plugin_class->refresh_metadata_async = gs_plugin_appstream_refresh_metadata_async;
plugin_class->refresh_metadata_finish = gs_plugin_appstream_refresh_metadata_finish;
plugin_class->refine_categories_async = gs_plugin_appstream_refine_categories_async;
plugin_class->refine_categories_finish = gs_plugin_appstream_refine_categories_finish;
plugin_class->url_to_app_async = gs_plugin_appstream_url_to_app_async;
plugin_class->url_to_app_finish = gs_plugin_appstream_url_to_app_finish;
}
GType
gs_plugin_query_type (void)
{
return GS_TYPE_PLUGIN_APPSTREAM;
}