1
0
Fork 0
gnome-software/lib/gs-plugin-job-install-apps.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

609 lines
20 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) 2023 Endless OS Foundation LLC
* Copyright (C) 2024 GNOME Foundation, Inc.
*
* Author: Philip Withnall <pwithnall@endlessos.org>
*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
/**
* SECTION:gs-plugin-job-install-apps
* @short_description: A plugin job to install apps
*
* #GsPluginJobInstallApps is a #GsPluginJob representing an operation to
* install apps.
*
* This class is a wrapper around #GsPluginClass.install_apps_async(),
* calling it for all loaded plugins.
*
* Unless %GS_PLUGIN_INSTALL_APPS_FLAGS_NO_DOWNLOAD is specified, the first step
* of this job downloads the app and any dependencies, ready to be installed or
* updated.
*
* Plugins are expected to schedule downloads using the system download
* scheduler if appropriate (if the download is not guaranteed to be under a few
* hundred kilobytes, for example), so that the users metered data preferences
* are honoured.
*
* Plugins are expected to send progress notifications to the UI by calling the
* provided #GsPluginProgressCallback function. Plugins may also call
* gs_app_set_progress() on apps as they are updated, but this method will
* eventually be removed as it cannot represent progress in multiple ongoing
* operations.
*
* Callbacks from this job will be executed in the #GMainContext which was
* thread-default at the time when #GsPluginJob.run_async() was called on the
* #GsPluginJobInstallApps. For plugins, this means that callbacks must be
* executed in the same #GMainContext which called
* #GsPluginClass.install_apps_async().
*
* If the app is already downloaded, the first step of this job is a no-op.
*
* Unless %GS_PLUGIN_INSTALL_APPS_FLAGS_NO_APPLY is specified, the second step of
* this job applies the app and any dependencies (installing the app, and updating
* dependencies or installing missing dependencies).
*
* Once that is completed, the apps will typically be set to the state
* %GS_APP_STATE_INSTALLED, or %GS_APP_STATE_UNKNOWN.
*
* On failure the error message returned will usually only be shown on the
* console, but they can also be retrieved using gs_plugin_loader_get_events().
*
* See also: #GsPluginClass.install_apps_async()
* Since: 47
*/
#include "config.h"
#include <glib.h>
#include <glib-object.h>
#include <glib/gi18n.h>
#ifdef HAVE_SYSPROF
#include <sysprof-capture.h>
#endif
#include "gs-enums.h"
#include "gs-plugin-job-private.h"
#include "gs-plugin-job-install-apps.h"
#include "gs-plugin-types.h"
#include "gs-profiler.h"
#include "gs-utils.h"
struct _GsPluginJobInstallApps
{
GsPluginJob parent;
/* Input arguments. */
GsAppList *apps;
GsPluginInstallAppsFlags flags;
/* In-progress data. */
GError *saved_error; /* (owned) (nullable) */
guint n_pending_ops;
GHashTable *plugins_progress; /* (element-type GsPlugin guint) (owned) (nullable) */
GSource *progress_source; /* (owned) (nullable) */
guint last_reported_progress;
#ifdef HAVE_SYSPROF
gint64 begin_time_nsec;
#endif
};
G_DEFINE_TYPE (GsPluginJobInstallApps, gs_plugin_job_install_apps, GS_TYPE_PLUGIN_JOB)
typedef enum {
PROP_APPS = 1,
PROP_FLAGS,
} GsPluginJobInstallAppsProperty;
static GParamSpec *props[PROP_FLAGS + 1] = { NULL, };
typedef enum {
SIGNAL_APP_NEEDS_USER_ACTION,
SIGNAL_PROGRESS,
} GsPluginJobInstallAppsSignal;
static guint signals[SIGNAL_PROGRESS + 1] = { 0, };
static void
gs_plugin_job_install_apps_dispose (GObject *object)
{
GsPluginJobInstallApps *self = GS_PLUGIN_JOB_INSTALL_APPS (object);
g_assert (self->saved_error == NULL);
g_assert (self->n_pending_ops == 0);
/* Progress reporting should have been stopped by now. */
if (self->progress_source != NULL) {
g_assert (g_source_is_destroyed (self->progress_source));
g_clear_pointer (&self->progress_source, g_source_unref);
}
g_clear_pointer (&self->plugins_progress, g_hash_table_unref);
g_clear_object (&self->apps);
G_OBJECT_CLASS (gs_plugin_job_install_apps_parent_class)->dispose (object);
}
static void
gs_plugin_job_install_apps_get_property (GObject *object,
guint prop_id,
GValue *value,
GParamSpec *pspec)
{
GsPluginJobInstallApps *self = GS_PLUGIN_JOB_INSTALL_APPS (object);
switch ((GsPluginJobInstallAppsProperty) prop_id) {
case PROP_APPS:
g_value_set_object (value, self->apps);
break;
case PROP_FLAGS:
g_value_set_flags (value, self->flags);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
gs_plugin_job_install_apps_set_property (GObject *object,
guint prop_id,
const GValue *value,
GParamSpec *pspec)
{
GsPluginJobInstallApps *self = GS_PLUGIN_JOB_INSTALL_APPS (object);
switch ((GsPluginJobInstallAppsProperty) prop_id) {
case PROP_APPS:
/* Construct only. */
g_assert (self->apps == NULL);
self->apps = g_value_dup_object (value);
g_assert (self->apps != NULL);
g_object_notify_by_pspec (object, props[prop_id]);
break;
case PROP_FLAGS:
/* Construct only. */
g_assert (self->flags == 0);
self->flags = g_value_get_flags (value);
/* Perhaps we could eventually allow both of these to be
* specified at the same time, but for now it would over
* complicate the implementation of plugins, for no benefit. */
g_assert (!(self->flags & GS_PLUGIN_INSTALL_APPS_FLAGS_NO_DOWNLOAD) ||
!(self->flags & GS_PLUGIN_INSTALL_APPS_FLAGS_NO_APPLY));
g_object_notify_by_pspec (object, props[prop_id]);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
break;
}
}
static void
app_needs_user_action_cb (GsPlugin *plugin,
GsApp *app,
AsScreenshot *action_screenshot,
gpointer user_data)
{
GTask *task = G_TASK (user_data);
GsPluginJobInstallApps *self = g_task_get_source_object (task);
g_assert (g_main_context_is_owner (g_task_get_context (task)));
g_signal_emit (self, signals[SIGNAL_APP_NEEDS_USER_ACTION], 0, app, action_screenshot);
}
static void plugin_progress_cb (GsPlugin *plugin,
guint progress,
gpointer user_data);
static gboolean progress_cb (gpointer user_data);
static void plugin_install_apps_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
static void finish_op (GTask *task,
GError *error);
static void
gs_plugin_job_install_apps_run_async (GsPluginJob *job,
GsPluginLoader *plugin_loader,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginJobInstallApps *self = GS_PLUGIN_JOB_INSTALL_APPS (job);
g_autoptr(GTask) task = NULL;
GPtrArray *plugins; /* (element-type GsPlugin) */
gboolean any_plugins_ran = FALSE;
g_autoptr(GError) local_error = NULL;
/* Chosen to allow a few UI updates per second without updating the
* progress label so often its unreadable. */
const guint progress_update_period_ms = 300;
task = g_task_new (job, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_job_install_apps_run_async);
g_task_set_task_data (task, g_object_ref (plugin_loader), (GDestroyNotify) g_object_unref);
/* Set up the progress timeout. This periodically sums up the progress
* tuples in `self->plugins_progress` and reports them to the calling
* function via the #GsPluginJobInstallApps::progress signal, giving
* an overall progress for all the parallel operations. */
self->plugins_progress = g_hash_table_new (g_direct_hash, g_direct_equal);
self->last_reported_progress = GS_APP_PROGRESS_UNKNOWN;
self->progress_source = g_timeout_source_new (progress_update_period_ms);
g_source_set_callback (self->progress_source, progress_cb, self, NULL);
g_source_attach (self->progress_source, g_main_context_get_thread_default ());
/* run each plugin, keeping a counter of pending operations which is
* initialised to 1 until all the operations are started */
self->n_pending_ops = 1;
plugins = gs_plugin_loader_get_plugins (plugin_loader);
#ifdef HAVE_SYSPROF
self->begin_time_nsec = SYSPROF_CAPTURE_CURRENT_TIME;
#endif
for (guint i = 0; i < plugins->len; i++) {
GsPlugin *plugin = g_ptr_array_index (plugins, i);
GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin);
if (!gs_plugin_get_enabled (plugin))
continue;
if (plugin_class->install_apps_async == NULL)
continue;
/* at least one plugin supports this vfunc */
any_plugins_ran = TRUE;
/* Handle cancellation */
if (g_cancellable_set_error_if_cancelled (cancellable, &local_error))
break;
/* Set up progress reporting for this plugin. */
g_hash_table_insert (self->plugins_progress, plugin, GUINT_TO_POINTER (0));
/* run the plugin */
self->n_pending_ops++;
plugin_class->install_apps_async (plugin,
self->apps,
self->flags,
plugin_progress_cb,
task,
app_needs_user_action_cb,
task,
cancellable,
plugin_install_apps_cb,
g_object_ref (task));
}
/* some functions are really required for proper operation */
if (!any_plugins_ran) {
g_set_error_literal (&local_error,
GS_PLUGIN_ERROR,
GS_PLUGIN_ERROR_NOT_SUPPORTED,
"no plugin could handle installing apps");
}
finish_op (task, g_steal_pointer (&local_error));
}
/* Called in the same thread as gs_plugin_job_install_apps_run_async(), to
* report the progress for the given plugin. */
static void
plugin_progress_cb (GsPlugin *plugin,
guint progress,
gpointer user_data)
{
GTask *task = G_TASK (user_data);
GsPluginJobInstallApps *self = g_task_get_source_object (task);
g_assert (g_main_context_is_owner (g_task_get_context (task)));
g_hash_table_replace (self->plugins_progress, plugin, GUINT_TO_POINTER (progress));
}
static gboolean
progress_cb (gpointer user_data)
{
GsPluginJobInstallApps *self = GS_PLUGIN_JOB_INSTALL_APPS (user_data);
gdouble progress;
guint n_portions;
GHashTableIter iter;
gpointer plugin_progress_ptr;
gboolean all_unknown = TRUE;
/* Sum up the progress for all parallel operations.
*
* Allocate each operation an equal portion of 100 percentage points. In
* this context, an operation is a call to a plugins
* install_apps_async() vfunc. */
n_portions = g_hash_table_size (self->plugins_progress);
progress = 0.0;
g_hash_table_iter_init (&iter, self->plugins_progress);
while (g_hash_table_iter_next (&iter, NULL, &plugin_progress_ptr)) {
guint plugin_progress = GPOINTER_TO_UINT (plugin_progress_ptr);
if (plugin_progress == GS_APP_PROGRESS_UNKNOWN)
continue;
else
all_unknown = FALSE;
progress += (100.0 / n_portions) * ((gdouble) plugin_progress / 100.0);
}
if (all_unknown)
progress = GS_APP_PROGRESS_UNKNOWN;
if ((guint) progress != self->last_reported_progress) {
/* Report progress via signal emission. */
/* FIXME: In future we could add explicit signals to notify that a
* download operation is blocked on waiting for metered data permission
* to download, so the UI can represent that better. */
g_signal_emit (self, signals[SIGNAL_PROGRESS], 0, (guint) progress);
self->last_reported_progress = progress;
}
return G_SOURCE_CONTINUE;
}
static void
plugin_install_apps_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GsPlugin *plugin = GS_PLUGIN (source_object);
GsPluginClass *plugin_class = GS_PLUGIN_GET_CLASS (plugin);
g_autoptr(GTask) task = G_TASK (user_data);
GsPluginJobInstallApps *self = g_task_get_source_object (task);
g_autoptr(GError) local_error = NULL;
/* Forward cancellation errors, but ignore all other errors so
* that other plugins dont get blocked.
*
* If plugins produce errors which should be reported to the user, they
* should report them directly by calling gs_plugin_report_event().
* #GsPluginJobInstallApps cannot do this as it doesnt know which errors
* are interesting to the user and which are useless. */
if (!plugin_class->install_apps_finish (plugin, result, &local_error) &&
!g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
!g_error_matches (local_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
g_debug ("Plugin %s failed to install apps: %s",
gs_plugin_get_name (plugin), local_error->message);
g_clear_error (&local_error);
}
gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
GS_PROFILER_ADD_MARK_TAKE (PluginJobInstallApps,
self->begin_time_nsec,
g_strdup_printf ("%s:%s",
G_OBJECT_TYPE_NAME (self),
gs_plugin_get_name (plugin)),
NULL);
/* Update progress reporting. */
g_hash_table_replace (self->plugins_progress, plugin, GUINT_TO_POINTER (100));
finish_op (task, g_steal_pointer (&local_error));
}
/* @error is (transfer full) if non-%NULL */
static void
finish_op (GTask *task,
GError *error)
{
GsPluginJobInstallApps *self = g_task_get_source_object (task);
g_autoptr(GError) error_owned = g_steal_pointer (&error);
g_autofree gchar *job_debug = NULL;
if (error_owned != NULL && self->saved_error == NULL)
self->saved_error = g_steal_pointer (&error_owned);
else if (error_owned != NULL)
g_debug ("Additional error while updating apps: %s", error_owned->message);
g_assert (self->n_pending_ops > 0);
self->n_pending_ops--;
if (self->n_pending_ops > 0)
return;
/* Emit one final progress update, then stop any further ones.
* Ensure the emission is in the right #GMainContext. */
g_assert (g_main_context_is_owner (g_task_get_context (task)));
progress_cb (self);
g_source_destroy (self->progress_source);
g_clear_pointer (&self->plugins_progress, g_hash_table_unref);
/* Get the results of the parallel ops. */
if (self->saved_error != NULL) {
g_task_return_error (task, g_steal_pointer (&self->saved_error));
g_signal_emit_by_name (G_OBJECT (self), "completed");
return;
}
/* show elapsed time */
job_debug = gs_plugin_job_to_string (GS_PLUGIN_JOB (self));
g_debug ("%s", job_debug);
/* Check the intermediate working values are all cleared. */
g_assert (self->saved_error == NULL);
g_assert (self->n_pending_ops == 0);
/* success */
g_task_return_boolean (task, TRUE);
g_signal_emit_by_name (G_OBJECT (self), "completed");
GS_PROFILER_ADD_MARK (PluginJobInstallApps,
self->begin_time_nsec,
G_OBJECT_TYPE_NAME (self),
NULL);
}
static gboolean
gs_plugin_job_install_apps_run_finish (GsPluginJob *self,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void
gs_plugin_job_install_apps_class_init (GsPluginJobInstallAppsClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GsPluginJobClass *job_class = GS_PLUGIN_JOB_CLASS (klass);
object_class->dispose = gs_plugin_job_install_apps_dispose;
object_class->get_property = gs_plugin_job_install_apps_get_property;
object_class->set_property = gs_plugin_job_install_apps_set_property;
job_class->run_async = gs_plugin_job_install_apps_run_async;
job_class->run_finish = gs_plugin_job_install_apps_run_finish;
/**
* GsPluginJobInstallApps:apps:
*
* List of apps to install.
*
* Since: 47
*/
props[PROP_APPS] =
g_param_spec_object ("apps", "Apps",
"List of apps to install.",
GS_TYPE_APP_LIST,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
/**
* GsPluginJobInstallApps:flags:
*
* Flags to specify how the install job should behave.
*
* Currently, it is forbidden to specify both
* %GS_PLUGIN_INSTALL_APPS_FLAGS_NO_DOWNLOAD and
* %GS_PLUGIN_INSTALL_APPS_FLAGS_NO_APPLY at the same time.
*
* Since: 47
*/
props[PROP_FLAGS] =
g_param_spec_flags ("flags", "Flags",
"Flags to specify how the install job should behave.",
GS_TYPE_PLUGIN_INSTALL_APPS_FLAGS, GS_PLUGIN_INSTALL_APPS_FLAGS_NONE,
G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY |
G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);
/**
* GsPluginJobInstallApps::app-needs-user-action:
* @app: (not nullable): the app which needs user action
* @action_screenshot: (not nullable): an image and caption explaining what action is needed
*
* Emitted during #GsPluginJob.run_async() if an app needs user action
* to upgrade.
*
* This is typically used for firmware upgrades where a piece of
* hardware needs user interaction to accept a firmware upgrade, such as
* being turned on and off, or having a button pressed.
*
* The image in @action_screenshot should explain to the user what to do
* to the device.
*
* Its emitted in the thread which is running the #GMainContext which
* was the thread-default context when #GsPluginJob.run_async() was
* called.
*
* Since: 47
*/
signals[SIGNAL_APP_NEEDS_USER_ACTION] =
g_signal_new ("app-needs-user-action",
G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
0, NULL, NULL, g_cclosure_marshal_generic,
G_TYPE_NONE, 2, GS_TYPE_APP, AS_TYPE_SCREENSHOT);
/**
* GsPluginJobInstallApps::progress:
* @progress_percent: percentage completion of the job, [0, 100], or
* %G_MAXUINT to indicate that progress is unknown
*
* Emitted during #GsPluginJob.run_async() when progress is made.
*
* Its emitted in the thread which is running the #GMainContext which
* was the thread-default context when #GsPluginJob.run_async() was
* called.
*
* Since: 47
*/
signals[SIGNAL_PROGRESS] =
g_signal_new ("progress",
G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
0, NULL, NULL, g_cclosure_marshal_VOID__UINT,
G_TYPE_NONE, 1, G_TYPE_UINT);
}
static void
gs_plugin_job_install_apps_init (GsPluginJobInstallApps *self)
{
}
/**
* gs_plugin_job_install_apps_new:
* @apps: (transfer none) (not nullable): list of apps to install
* @flags: flags to affect the install
*
* Create a new #GsPluginJobInstallApps for updating apps, or pre-downloading
* apps for installation.
*
* Returns: (transfer full): a new #GsPluginJobInstallApps
* Since: 47
*/
GsPluginJob *
gs_plugin_job_install_apps_new (GsAppList *apps,
GsPluginInstallAppsFlags flags)
{
g_return_val_if_fail (GS_IS_APP_LIST (apps), NULL);
return g_object_new (GS_TYPE_PLUGIN_JOB_INSTALL_APPS,
"apps", apps,
"flags", flags,
NULL);
}
/**
* gs_plugin_job_install_apps_get_apps:
* @self: a #GsPluginJobInstallApps
*
* Get the set of apps being installed by this #GsPluginJobInstallApps.
*
* Returns: apps being installed
* Since: 47
*/
GsAppList *
gs_plugin_job_install_apps_get_apps (GsPluginJobInstallApps *self)
{
g_return_val_if_fail (GS_IS_PLUGIN_JOB_INSTALL_APPS (self), NULL);
return self->apps;
}
/**
* gs_plugin_job_install_apps_get_flags:
* @self: a #GsPluginJobInstallApps
*
* Get the flags affecting the behaviour of this #GsPluginJobInstallApps.
*
* Returns: flags for the job
* Since: 47
*/
GsPluginInstallAppsFlags
gs_plugin_job_install_apps_get_flags (GsPluginJobInstallApps *self)
{
g_return_val_if_fail (GS_IS_PLUGIN_JOB_INSTALL_APPS (self), GS_PLUGIN_INSTALL_APPS_FLAGS_NONE);
return self->flags;
}