/* -*- 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 * Copyright (C) 2014-2020 Kalev Lember * * SPDX-License-Identifier: GPL-2.0-or-later */ /** * 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 #include #include #include "gs-app-list-private.h" #include "gs-download-utils.h" #include "gs-enums.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; GsPluginFlags flags; GPtrArray *rules[GS_PLUGIN_RULE_LAST]; GHashTable *vfuncs; /* string:pointer */ GMutex vfuncs_mutex; gboolean enabled; guint interactive_cnt; GMutex interactive_mutex; gchar *language; /* allow-none */ gchar *name; gchar *appstream_id; guint scale; guint order; guint priority; guint timer_id; GMutex timer_mutex; GNetworkMonitor *network_monitor; GDBusConnection *session_bus_connection; /* (owned) (not nullable) */ GDBusConnection *system_bus_connection; /* (owned) (not nullable) */ } GsPluginPrivate; G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (GsPlugin, gs_plugin, G_TYPE_OBJECT) G_DEFINE_QUARK (gs-plugin-error-quark, gs_plugin_error) typedef enum { PROP_FLAGS = 1, PROP_SCALE, PROP_SESSION_BUS_CONNECTION, PROP_SYSTEM_BUS_CONNECTION, } GsPluginProperty; static GParamSpec *obj_props[PROP_SYSTEM_BUS_CONNECTION + 1] = { NULL, }; enum { SIGNAL_UPDATES_CHANGED, SIGNAL_STATUS_CHANGED, SIGNAL_RELOAD, SIGNAL_REPORT_EVENT, SIGNAL_ALLOW_UPDATES, SIGNAL_BASIC_AUTH_START, SIGNAL_REPOSITORY_CHANGED, SIGNAL_ASK_UNTRUSTED, 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 * @session_bus_connection: (not nullable) (transfer none): a session bus * connection to use * @system_bus_connection: (not nullable) (transfer none): a system bus * connection to use * @error: a #GError, or %NULL * * Creates a new plugin from an external module. * * Returns: (transfer full): the #GsPlugin, or %NULL on error * * Since: 43 **/ GsPlugin * gs_plugin_create (const gchar *filename, GDBusConnection *session_bus_connection, GDBusConnection *system_bus_connection, GError **error) { GsPlugin *plugin = NULL; GsPluginPrivate *priv; g_autofree gchar *basename = NULL; GModule *module = NULL; GType (*query_type_function) (void) = NULL; GType plugin_type; /* 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 */ module = g_module_open (filename, 0); if (module == NULL || !g_module_symbol (module, "gs_plugin_query_type", (gpointer *) &query_type_function)) { g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED, "failed to open plugin %s: %s", filename, g_module_error ()); if (module != NULL) g_module_close (module); return NULL; } /* Make the module resident so it can’t be unloaded: without using a * full #GTypePlugin implementation for the modules, it’s not safe to * re-load a module and re-register its types with GObject, as that will * confuse the GType system. */ g_module_make_resident (module); plugin_type = query_type_function (); g_assert (g_type_is_a (plugin_type, GS_TYPE_PLUGIN)); plugin = g_object_new (plugin_type, "session-bus-connection", session_bus_connection, "system-bus-connection", system_bus_connection, NULL); priv = gs_plugin_get_instance_private (plugin); priv->module = g_steal_pointer (&module); gs_plugin_set_name (plugin, basename + 13); return plugin; } static void gs_plugin_dispose (GObject *object) { GsPlugin *plugin = GS_PLUGIN (object); GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); g_clear_object (&priv->session_bus_connection); g_clear_object (&priv->system_bus_connection); G_OBJECT_CLASS (gs_plugin_parent_class)->dispose (object); } 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->language); 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); if (priv->module != NULL) g_module_close (priv->module); G_OBJECT_CLASS (gs_plugin_parent_class)->finalize (object); } /** * 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 the init function for a #GsPlugin instance. * * 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); if (priv->scale != scale) { priv->scale = scale; g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_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_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_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_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_INTERACTIVE * * 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_INTERACTIVE * * 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; g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_FLAGS]); } /** * gs_plugin_remove_flags: * @plugin: a #GsPlugin * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_INTERACTIVE * * 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; g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_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 { GWeakRef plugin_weak; /* (element-type GsPlugin) */ GsApp *app; /* (owned) */ GsPluginStatus status; guint percentage; } GsPluginStatusHelper; static void gs_plugin_status_helper_free (GsPluginStatusHelper *helper) { g_weak_ref_clear (&helper->plugin_weak); g_clear_object (&helper->app); g_slice_free (GsPluginStatusHelper, helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginStatusHelper, gs_plugin_status_helper_free) static gboolean gs_plugin_status_update_cb (gpointer user_data) { GsPluginStatusHelper *helper = (GsPluginStatusHelper *) user_data; g_autoptr(GsPlugin) plugin = NULL; /* Does the plugin still exist? */ plugin = g_weak_ref_get (&helper->plugin_weak); if (plugin != NULL) g_signal_emit (plugin, signals[SIGNAL_STATUS_CHANGED], 0, helper->app, helper->status); return G_SOURCE_REMOVE; } /** * 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) { g_autoptr(GsPluginStatusHelper) helper = NULL; g_autoptr(GSource) idle_source = NULL; helper = g_slice_new0 (GsPluginStatusHelper); g_weak_ref_init (&helper->plugin_weak, 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, g_steal_pointer (&helper), (GDestroyNotify) gs_plugin_status_helper_free); 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 const gchar * get_desktop_id_to_launch (GsApp *app) { const gchar *desktop_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID); if (desktop_id == NULL) desktop_id = gs_app_get_id (app); return desktop_id; } static gboolean launch_app_info (GAppInfo *appinfo, GError **error) { GdkDisplay *display; g_autoptr(GAppLaunchContext) context = NULL; g_assert (appinfo != NULL); display = gdk_display_get_default (); context = G_APP_LAUNCH_CONTEXT (gdk_display_get_app_launch_context (display)); return g_app_info_launch (appinfo, NULL, context, error); } /** * gs_plugin_app_launch_async: * @plugin: a #GsPlugin * @app: a #GsApp * @flags: a bit-or of #GsPluginLaunchFlags * @cancellable: a #GCancellable, or %NULL * @callback: (not nullable): a #GAsyncReadyCallback to call when the request is satisfied * @user_data: (closure callback) (scope async): data to pass to @callback * * Asynchronously launches the application using #GAppInfo. * Finish the call with gs_plugin_app_launch_finish(). * * The function also verifies whether the @plugin can handle the @app, * in a sense of gs_app_has_management_plugin(), and if not then does * nothing. * * Since: 47 **/ void gs_plugin_app_launch_async (GsPlugin *plugin, GsApp *app, GsPluginLaunchFlags flags, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { const gchar *desktop_id; g_autoptr(GTask) task = NULL; g_autoptr(GAppInfo) appinfo = NULL; g_return_if_fail (GS_IS_PLUGIN (plugin)); g_return_if_fail (GS_IS_APP (app)); g_return_if_fail (callback != NULL); task = g_task_new (plugin, cancellable, callback, user_data); g_task_set_source_tag (task, gs_plugin_app_launch_async); /* only process this app if was created by this plugin */ if (!gs_app_has_management_plugin (app, plugin)) { g_task_return_pointer (task, NULL, NULL); return; } desktop_id = get_desktop_id_to_launch (app); if (desktop_id == NULL) { g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, "no desktop file for app: %s", gs_app_get_name (app)); return; } appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id)); if (appinfo == NULL) { g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, "no such desktop file: %s", desktop_id); return; } /* the actual launch happens in the _finish() function, which should be in the main thread */ g_task_return_pointer (task, g_steal_pointer (&appinfo), g_object_unref); } /** * gs_plugin_app_launch_finish: * @plugin: a #GsPlugin * @result: an async result * @error: a #GError or %NULL * * Finishes operation started by gs_plugin_app_launch_async(). * This function should be called from the main thread. * * Returns: whether succeeded * * Since: 47 **/ gboolean gs_plugin_app_launch_finish (GsPlugin *plugin, GAsyncResult *result, GError **error) { g_autoptr(GAppInfo) appinfo = NULL; g_return_val_if_fail (g_task_is_valid (G_TASK (result), plugin), FALSE); g_return_val_if_fail (g_async_result_is_tagged (result, gs_plugin_app_launch_async), FALSE); appinfo = g_task_propagate_pointer (G_TASK (result), error); if (appinfo == NULL) return TRUE; return launch_app_info (appinfo, error); } static GDesktopAppInfo * check_directory_for_desktop_file (GsPlugin *plugin, GsApp *app, GsPluginPickDesktopFileCallback cb, gpointer user_data, const gchar *desktop_id, const gchar *data_dir) { g_autofree gchar *filename = NULL; g_autoptr(GKeyFile) key_file = NULL; gboolean found, any_found = FALSE; filename = g_build_filename (data_dir, "applications", desktop_id, NULL); key_file = g_key_file_new (); found = g_key_file_load_from_file (key_file, filename, G_KEY_FILE_KEEP_TRANSLATIONS, NULL); if (found && cb (plugin, app, filename, key_file, user_data)) { g_autoptr(GDesktopAppInfo) appinfo = NULL; g_debug ("Found '%s' for app '%s' and picked it", filename, desktop_id); /* use the filename, not the key_file, to enable bus activation from the .desktop file */ appinfo = g_desktop_app_info_new_from_filename (filename); if (appinfo != NULL) return g_steal_pointer (&appinfo); g_debug ("Failed to load '%s' as a GDesktopAppInfo", filename); return NULL; } else if (found) { g_debug ("Found '%s' for app '%s', but did not pick it", filename, desktop_id); any_found = TRUE; } if (!g_str_has_suffix (desktop_id, ".desktop")) { g_autofree gchar *desktop_filename = g_strconcat (filename, ".desktop", NULL); found = g_key_file_load_from_file (key_file, desktop_filename, G_KEY_FILE_KEEP_TRANSLATIONS, NULL); if (found && cb (plugin, app, desktop_filename, key_file, user_data)) { g_autoptr(GDesktopAppInfo) appinfo = NULL; g_debug ("Found '%s' for app '%s' and picked it", desktop_filename, desktop_id); /* use the filename, not the key_file, to enable bus activation from the .desktop file */ appinfo = g_desktop_app_info_new_from_filename (desktop_filename); if (appinfo != NULL) return g_steal_pointer (&appinfo); g_debug ("Failed to load '%s' as a GDesktopAppInfo", desktop_filename); return NULL; } else if (found) { g_debug ("Found '%s' for app '%s', but did not pick it", desktop_filename, desktop_id); any_found = TRUE; } } if (!any_found) g_debug ("Did not find any appropriate .desktop file for '%s' in '%s/applications/'", desktop_id, data_dir); return NULL; } typedef struct { GsApp *app; /* (owned) */ GsPluginPickDesktopFileCallback cb; gpointer cb_user_data; GAppInfo *appinfo; /* (owned) (nullable) (out) */ } LaunchFilteredData; static void launch_filtered_data_free (LaunchFilteredData *data) { g_clear_object (&data->app); g_clear_object (&data->appinfo); g_free (data); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (LaunchFilteredData, launch_filtered_data_free) static void launch_filtered_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable) { g_autoptr(GDesktopAppInfo) appinfo = NULL; GsPlugin *plugin = GS_PLUGIN (source_object); LaunchFilteredData *data = task_data; const gchar *desktop_id; desktop_id = get_desktop_id_to_launch (data->app); /* the caller verified it's set */ g_assert (desktop_id != NULL); /* First, the configs. Highest priority: the user's ~/.config */ appinfo = check_directory_for_desktop_file (plugin, data->app, data->cb, data->cb_user_data, desktop_id, g_get_user_config_dir ()); if (appinfo == NULL) { /* Next, the system configs (/etc/xdg, and so on). */ const gchar * const *dirs; dirs = g_get_system_config_dirs (); for (guint i = 0; dirs[i] && appinfo == NULL; i++) { appinfo = check_directory_for_desktop_file (plugin, data->app, data->cb, data->cb_user_data, desktop_id, dirs[i]); } } if (appinfo == NULL) { /* Now the data. Highest priority: the user's ~/.local/share/applications */ appinfo = check_directory_for_desktop_file (plugin, data->app, data->cb, data->cb_user_data, desktop_id, g_get_user_data_dir ()); } if (appinfo == NULL) { /* Following that, XDG_DATA_DIRS/applications, in order */ const gchar * const *dirs; dirs = g_get_system_data_dirs (); for (guint i = 0; dirs[i] && appinfo == NULL; i++) { appinfo = check_directory_for_desktop_file (plugin, data->app, data->cb, data->cb_user_data, desktop_id, dirs[i]); } } if (appinfo == NULL) { g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, "no appropriate desktop file found: %s", desktop_id); return; } /* the actual launch happens in the _finish() function, which should be in the main thread */ data->appinfo = (GAppInfo *) g_steal_pointer (&appinfo); g_task_return_boolean (task, TRUE); } /** * gs_plugin_app_launch_filtered_async: * @plugin: a #GsPlugin * @app: a #GsApp to launch * @flags: a bit-or of #GsPluginLaunchFlags * @cb: a callback to pick the correct .desktop file * @cb_user_data: (closure cb) (scope async): user data for the @cb * @cancellable: a #GCancellable or %NULL * @async_callback: (not nullable): async call ready callback * @async_user_data: (closure async_callback) (scope async): user data for the @async_callback * * Asynchronosuly launches @app, using the .desktop file picked by the @cb. * This can help in case multiple versions of the @app are installed * in the system (like a Flatpak and RPM versions). * Finish the call with gs_plugin_app_launch_filtered_finish(). * * The function also verifies whether the @plugin can handle the @app, * in a sense of gs_app_has_management_plugin(), and if not then does * nothing. * * Since: 47 **/ void gs_plugin_app_launch_filtered_async (GsPlugin *plugin, GsApp *app, GsPluginLaunchFlags flags, GsPluginPickDesktopFileCallback cb, gpointer cb_user_data, GCancellable *cancellable, GAsyncReadyCallback async_callback, gpointer async_user_data) { const gchar *desktop_id; g_autoptr(GTask) task = NULL; g_autoptr(LaunchFilteredData) data = NULL; g_return_if_fail (GS_IS_PLUGIN (plugin)); g_return_if_fail (GS_IS_APP (app)); g_return_if_fail (cb != NULL); g_return_if_fail (async_callback != NULL); task = g_task_new (plugin, cancellable, async_callback, async_user_data); g_task_set_source_tag (task, gs_plugin_app_launch_filtered_async); /* only process this app if was created by this plugin */ if (!gs_app_has_management_plugin (app, plugin)) { g_task_return_boolean (task, TRUE); return; } desktop_id = get_desktop_id_to_launch (app); if (desktop_id == NULL) { g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED, "no desktop file for app: %s", gs_app_get_name (app)); return; } data = g_new0 (LaunchFilteredData, 1); data->app = g_object_ref (app); data->cb = cb; data->cb_user_data = cb_user_data; g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) launch_filtered_data_free); g_task_run_in_thread (task, launch_filtered_thread); } /** * gs_plugin_app_launch_filtered_finish: * @plugin: a #GsPlugin * @result: an async result * @error: a #GError or %NULL * * Finishes operation started by gs_plugin_app_launch_finltered_async(). * This function should be called from the main thread. * * Returns: whether succeeded * * Since: 47 **/ gboolean gs_plugin_app_launch_filtered_finish (GsPlugin *plugin, GAsyncResult *result, GError **error) { GTask *task = G_TASK (result); LaunchFilteredData *data = NULL; g_return_val_if_fail (g_task_is_valid (task, plugin), FALSE); g_return_val_if_fail (g_async_result_is_tagged (result, gs_plugin_app_launch_filtered_async), FALSE); if (!g_task_propagate_boolean (task, error)) return FALSE; data = g_task_get_task_data (task); /* the plugin does not manage the provided app */ if (data == NULL) return TRUE; return launch_app_info (data->appinfo, error); } static void weak_ref_free (GWeakRef *weak) { g_weak_ref_clear (weak); g_free (weak); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (GWeakRef, weak_ref_free) /* @obj is a gpointer rather than a GObject* to avoid the need for casts */ static GWeakRef * weak_ref_new (gpointer obj) { g_autoptr(GWeakRef) weak = g_new0 (GWeakRef, 1); g_weak_ref_init (weak, obj); return g_steal_pointer (&weak); } static gboolean gs_plugin_updates_changed_cb (gpointer user_data) { GWeakRef *plugin_weak = user_data; g_autoptr(GsPlugin) plugin = NULL; plugin = g_weak_ref_get (plugin_weak); if (plugin != NULL) g_signal_emit (plugin, signals[SIGNAL_UPDATES_CHANGED], 0); return G_SOURCE_REMOVE; } /** * 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_full (G_PRIORITY_DEFAULT_IDLE, gs_plugin_updates_changed_cb, weak_ref_new (plugin), (GDestroyNotify) weak_ref_free); } static gboolean gs_plugin_reload_cb (gpointer user_data) { GWeakRef *plugin_weak = user_data; g_autoptr(GsPlugin) plugin = NULL; plugin = g_weak_ref_get (plugin_weak); if (plugin != NULL) g_signal_emit (plugin, signals[SIGNAL_RELOAD], 0); return G_SOURCE_REMOVE; } /** * 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 %s::reload in idle", gs_plugin_get_name (plugin)); g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, gs_plugin_reload_cb, weak_ref_new (plugin), (GDestroyNotify) weak_ref_free); } /** * 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_lookup_by_state: * @plugin: a #GsPlugin * @list: a #GsAppList to add applications to * @state: a #GsAppState * * Adds each cached #GsApp with state @state into the @list. * When the state is %GS_APP_STATE_UNKNOWN, then adds all * cached applications. * * Since: 40 **/ void gs_plugin_cache_lookup_by_state (GsPlugin *plugin, GsAppList *list, GsAppState state) { GsPluginPrivate *priv; GHashTableIter iter; gpointer value; g_autoptr(GMutexLocker) locker = NULL; g_return_if_fail (GS_IS_PLUGIN (plugin)); g_return_if_fail (GS_IS_APP_LIST (list)); priv = gs_plugin_get_instance_private (plugin); locker = g_mutex_locker_new (&priv->cache_mutex); g_hash_table_iter_init (&iter, priv->cache); while (g_hash_table_iter_next (&iter, NULL, &value)) { GsApp *app = value; if (state == GS_APP_STATE_UNKNOWN || state == gs_app_get_state (app)) gs_app_list_add (list, 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_list_cached: * @plugin: a #GsPlugin * * Lists all apps cached by the @plugin. * * Returns: (transfer full): a #GsAppList with all currently cached apps * * Since: 46 **/ GsAppList * gs_plugin_list_cached (GsPlugin *plugin) { GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); GsAppList *list = NULL; GHashTableIter iter; gpointer value = NULL; g_autoptr(GMutexLocker) locker = NULL; g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL); locker = g_mutex_locker_new (&priv->cache_mutex); list = gs_app_list_new (); g_hash_table_iter_init (&iter, priv->cache); while (g_hash_table_iter_next (&iter, NULL, &value)) { GsApp *app = value; gs_app_list_add (list, app); } return list; } /** * 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 * * 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_GET_LANGPACKS) return "gs_plugin_add_langpacks"; return NULL; } /** * gs_plugin_action_to_string: * @action: a #GsPluginAction * * 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_UPGRADE_DOWNLOAD) return "upgrade-download"; if (action == GS_PLUGIN_ACTION_LAUNCH) return "launch"; 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_LANGPACKS) return "get-langpacks"; if (action == GS_PLUGIN_ACTION_INSTALL_REPO) return "repo-install"; if (action == GS_PLUGIN_ACTION_REMOVE_REPO) return "repo-remove"; if (action == GS_PLUGIN_ACTION_ENABLE_REPO) return "repo-enable"; if (action == GS_PLUGIN_ACTION_DISABLE_REPO) return "repo-disable"; return NULL; } /** * gs_plugin_action_from_string: * @action: a #GsPluginAction, e.g. "install" * * Converts the string to an enumerated action. * * Returns: a #GsPluginAction. * * Since: 3.26 **/ GsPluginAction gs_plugin_action_from_string (const gchar *action) { if (g_strcmp0 (action, "upgrade-download") == 0) return GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD; if (g_strcmp0 (action, "launch") == 0) return GS_PLUGIN_ACTION_LAUNCH; 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-langpacks") == 0) return GS_PLUGIN_ACTION_GET_LANGPACKS; if (g_strcmp0 (action, "repo-install") == 0) return GS_PLUGIN_ACTION_INSTALL_REPO; if (g_strcmp0 (action, "repo-remove") == 0) return GS_PLUGIN_ACTION_REMOVE_REPO; if (g_strcmp0 (action, "repo-enable") == 0) return GS_PLUGIN_ACTION_ENABLE_REPO; if (g_strcmp0 (action, "repo-disable") == 0) return GS_PLUGIN_ACTION_DISABLE_REPO; 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_REQUIRE_ID) g_ptr_array_add (cstrs, (gpointer) "require-id"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE) g_ptr_array_add (cstrs, (gpointer) "require-license"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL) g_ptr_array_add (cstrs, (gpointer) "require-url"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION) g_ptr_array_add (cstrs, (gpointer) "require-description"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) g_ptr_array_add (cstrs, (gpointer) "require-size"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING) g_ptr_array_add (cstrs, (gpointer) "require-rating"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION) g_ptr_array_add (cstrs, (gpointer) "require-version"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY) g_ptr_array_add (cstrs, (gpointer) "require-history"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION) g_ptr_array_add (cstrs, (gpointer) "require-setup-action"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS) g_ptr_array_add (cstrs, (gpointer) "require-update-details"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN) g_ptr_array_add (cstrs, (gpointer) "require-origin"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED) g_ptr_array_add (cstrs, (gpointer) "require-related"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS) g_ptr_array_add (cstrs, (gpointer) "require-addons"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES) g_ptr_array_add (cstrs, (gpointer) "require-allow-packages"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY) g_ptr_array_add (cstrs, (gpointer) "require-update-severity"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED) g_ptr_array_add (cstrs, (gpointer) "require-upgrade-removed"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE) g_ptr_array_add (cstrs, (gpointer) "require-provenance"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS) g_ptr_array_add (cstrs, (gpointer) "require-reviews"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS) g_ptr_array_add (cstrs, (gpointer) "require-review-ratings"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON) g_ptr_array_add (cstrs, (gpointer) "require-icon"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS) g_ptr_array_add (cstrs, (gpointer) "require-permissions"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME) g_ptr_array_add (cstrs, (gpointer) "require-origin-hostname"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI) g_ptr_array_add (cstrs, (gpointer) "require-origin-ui"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME) g_ptr_array_add (cstrs, (gpointer) "require-runtime"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS) g_ptr_array_add (cstrs, (gpointer) "require-screenshots"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES) g_ptr_array_add (cstrs, (gpointer) "require-categories"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP) g_ptr_array_add (cstrs, (gpointer) "require-project-group"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME) g_ptr_array_add (cstrs, (gpointer) "require-developer-name"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS) g_ptr_array_add (cstrs, (gpointer) "require-kudos"); if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING) g_ptr_array_add (cstrs, (gpointer) "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_constructed (GObject *object) { GsPlugin *plugin = GS_PLUGIN (object); GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin); G_OBJECT_CLASS (gs_plugin_parent_class)->constructed (object); /* Check all required properties have been set. */ g_assert (priv->session_bus_connection != NULL); g_assert (priv->system_bus_connection != NULL); } 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 ((GsPluginProperty) prop_id) { case PROP_FLAGS: priv->flags = g_value_get_flags (value); g_object_notify_by_pspec (G_OBJECT (plugin), obj_props[PROP_FLAGS]); break; case PROP_SCALE: gs_plugin_set_scale (plugin, g_value_get_uint (value)); break; case PROP_SESSION_BUS_CONNECTION: /* Construct only */ g_assert (priv->session_bus_connection == NULL); priv->session_bus_connection = g_value_dup_object (value); break; case PROP_SYSTEM_BUS_CONNECTION: /* Construct only */ g_assert (priv->system_bus_connection == NULL); priv->system_bus_connection = g_value_dup_object (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gs_plugin_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 ((GsPluginProperty) prop_id) { case PROP_FLAGS: g_value_set_flags (value, priv->flags); break; case PROP_SCALE: g_value_set_uint (value, gs_plugin_get_scale (plugin)); break; case PROP_SESSION_BUS_CONNECTION: g_value_set_object (value, priv->session_bus_connection); break; case PROP_SYSTEM_BUS_CONNECTION: g_value_set_object (value, priv->system_bus_connection); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gs_plugin_class_init (GsPluginClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->constructed = gs_plugin_constructed; object_class->set_property = gs_plugin_set_property; object_class->get_property = gs_plugin_get_property; object_class->dispose = gs_plugin_dispose; object_class->finalize = gs_plugin_finalize; /** * GsPlugin:flags: * * Flags which indicate various boolean properties of the plugin. * * These may change during the plugin’s lifetime. */ obj_props[PROP_FLAGS] = g_param_spec_flags ("flags", NULL, NULL, GS_TYPE_PLUGIN_FLAGS, GS_PLUGIN_FLAGS_NONE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * GsPlugin:scale: * * The window scale factor. * * These may change during the plugin’s lifetime. * * Since: 48 */ obj_props[PROP_SCALE] = g_param_spec_uint ("scale", NULL, NULL, 1, G_MAXUINT, 1, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * GsPlugin:session-bus-connection: (not nullable) * * A connection to the D-Bus session bus. * * This must be set at construction time and will not be %NULL * afterwards. * * Since: 43 */ obj_props[PROP_SESSION_BUS_CONNECTION] = g_param_spec_object ("session-bus-connection", NULL, NULL, G_TYPE_DBUS_CONNECTION, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); /** * GsPlugin:system-bus-connection: (not nullable) * * A connection to the D-Bus system bus. * * This must be set at construction time and will not be %NULL * afterwards. * * Since: 43 */ obj_props[PROP_SYSTEM_BUS_CONNECTION] = g_param_spec_object ("system-bus-connection", NULL, NULL, G_TYPE_DBUS_CONNECTION, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props); signals [SIGNAL_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); signals [SIGNAL_REPOSITORY_CHANGED] = g_signal_new ("repository-changed", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GsPluginClass, repository_changed), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_NONE, 1, GS_TYPE_APP); signals [SIGNAL_ASK_UNTRUSTED] = g_signal_new ("ask-untrusted", G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (GsPluginClass, ask_untrusted), NULL, NULL, g_cclosure_marshal_generic, G_TYPE_BOOLEAN, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); } static void gs_plugin_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_data_id_hash, (GEqualFunc) as_utils_data_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: * @session_bus_connection: (not nullable) (transfer none): a session bus * connection to use * @system_bus_connection: (not nullable) (transfer none): a system bus * connection to use * * Creates a new plugin. * * Returns: a #GsPlugin * * Since: 43 **/ GsPlugin * gs_plugin_new (GDBusConnection *session_bus_connection, GDBusConnection *system_bus_connection) { g_return_val_if_fail (G_IS_DBUS_CONNECTION (session_bus_connection), NULL); g_return_val_if_fail (G_IS_DBUS_CONNECTION (system_bus_connection), NULL); return g_object_new (GS_TYPE_PLUGIN, "session-bus-connection", session_bus_connection, "system-bus-connection", system_bus_connection, NULL); } typedef struct { GWeakRef plugin_weak; /* (owned) (element-type GsPlugin) */ GsApp *repository; /* (owned) */ } GsPluginRepositoryChangedHelper; static void gs_plugin_repository_changed_helper_free (GsPluginRepositoryChangedHelper *helper) { g_clear_object (&helper->repository); g_weak_ref_clear (&helper->plugin_weak); g_slice_free (GsPluginRepositoryChangedHelper, helper); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (GsPluginRepositoryChangedHelper, gs_plugin_repository_changed_helper_free) static gboolean gs_plugin_repository_changed_cb (gpointer user_data) { GsPluginRepositoryChangedHelper *helper = user_data; g_autoptr(GsPlugin) plugin = NULL; plugin = g_weak_ref_get (&helper->plugin_weak); if (plugin != NULL) g_signal_emit (plugin, signals[SIGNAL_REPOSITORY_CHANGED], 0, helper->repository); return G_SOURCE_REMOVE; } /** * gs_plugin_repository_changed: * @plugin: a #GsPlugin * @repository: a #GsApp representing the repository * * Emit the "repository-changed" signal in the main thread. * * Since: 40 **/ void gs_plugin_repository_changed (GsPlugin *plugin, GsApp *repository) { g_autoptr(GsPluginRepositoryChangedHelper) helper = NULL; g_autoptr(GSource) idle_source = NULL; g_return_if_fail (GS_IS_PLUGIN (plugin)); g_return_if_fail (GS_IS_APP (repository)); helper = g_slice_new0 (GsPluginRepositoryChangedHelper); g_weak_ref_init (&helper->plugin_weak, plugin); helper->repository = g_object_ref (repository); idle_source = g_idle_source_new (); g_source_set_callback (idle_source, gs_plugin_repository_changed_cb, g_steal_pointer (&helper), (GDestroyNotify) gs_plugin_repository_changed_helper_free); g_source_attach (idle_source, NULL); } /** * gs_plugin_update_cache_state_for_repository: * @plugin: a #GsPlugin * @repository: a #GsApp representing a repository, which changed * * Update state of the all cached #GsApp instances related * to the @repository. * * Since: 40 **/ void gs_plugin_update_cache_state_for_repository (GsPlugin *plugin, GsApp *repository) { GsPluginPrivate *priv; GHashTableIter iter; g_autoptr(GMutexLocker) locker = NULL; g_autoptr(GsPlugin) repo_plugin = NULL; gpointer value; const gchar *repo_id; GsAppState repo_state; g_return_if_fail (GS_IS_PLUGIN (plugin)); g_return_if_fail (GS_IS_APP (repository)); priv = gs_plugin_get_instance_private (plugin); repo_id = gs_app_get_id (repository); repo_state = gs_app_get_state (repository); repo_plugin = gs_app_dup_management_plugin (repository); locker = g_mutex_locker_new (&priv->cache_mutex); g_hash_table_iter_init (&iter, priv->cache); while (g_hash_table_iter_next (&iter, NULL, &value)) { GsApp *app = value; GsAppState app_state = gs_app_get_state (app); g_autoptr(GsPlugin) app_plugin = gs_app_dup_management_plugin (app); if (app_plugin != repo_plugin || gs_app_get_scope (app) != gs_app_get_scope (repository) || gs_app_get_bundle_kind (app) != gs_app_get_bundle_kind (repository)) continue; if (((app_state == GS_APP_STATE_AVAILABLE && repo_state != GS_APP_STATE_INSTALLED) || (app_state == GS_APP_STATE_UNAVAILABLE && repo_state == GS_APP_STATE_INSTALLED)) && g_strcmp0 (gs_app_get_origin (app), repo_id) == 0) { /* First reset the state, because move from 'available' to 'unavailable' is not correct */ gs_app_set_state (app, GS_APP_STATE_UNKNOWN); gs_app_set_state (app, repo_state == GS_APP_STATE_INSTALLED ? GS_APP_STATE_AVAILABLE : GS_APP_STATE_UNAVAILABLE); } } } /** * gs_plugin_ask_untrusted: * @plugin: a #GsPlugin * @title: the title for the question * @msg: the message for the question * @details: (nullable): the detailed error message, or %NULL for none * @accept_label: (nullable): a label of the 'accept' button, or %NULL to use 'Accept' * * Asks the user whether he/she accepts an untrusted package install/download/update, * as described by @title and @msg, eventually with the @details. * * Note: This is a blocking call and can be called only from the main/GUI thread. * * Returns: whether the user accepted the question * * Since: 42 **/ gboolean gs_plugin_ask_untrusted (GsPlugin *plugin, const gchar *title, const gchar *msg, const gchar *details, const gchar *accept_label) { gboolean accepts = FALSE; g_signal_emit (plugin, signals[SIGNAL_ASK_UNTRUSTED], 0, title, msg, details, accept_label, &accepts); return accepts; } /** * gs_plugin_get_session_bus_connection: * @self: a #GsPlugin * * Get the D-Bus session bus connection in use by the plugin. * * Returns: (transfer none) (not nullable): a D-Bus connection * Since: 43 */ GDBusConnection * gs_plugin_get_session_bus_connection (GsPlugin *self) { GsPluginPrivate *priv = gs_plugin_get_instance_private (self); g_return_val_if_fail (GS_IS_PLUGIN (self), NULL); return priv->session_bus_connection; } /** * gs_plugin_get_system_bus_connection: * @self: a #GsPlugin * * Get the D-Bus system bus connection in use by the plugin. * * Returns: (transfer none) (not nullable): a D-Bus connection * Since: 43 */ GDBusConnection * gs_plugin_get_system_bus_connection (GsPlugin *self) { GsPluginPrivate *priv = gs_plugin_get_instance_private (self); g_return_val_if_fail (GS_IS_PLUGIN (self), NULL); return priv->system_bus_connection; }