938 lines
32 KiB
C
938 lines
32 KiB
C
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
|
||
* vi:set noexpandtab tabstop=8 shiftwidth=8:
|
||
*
|
||
* Copyright (C) 2018 Richard Hughes <richard@hughsie.com>
|
||
* Copyright (C) 2018 Kalev Lember <klember@redhat.com>
|
||
*
|
||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||
*/
|
||
|
||
#include <config.h>
|
||
|
||
#include "gs-flatpak-app.h"
|
||
#include "gs-flatpak-transaction.h"
|
||
|
||
struct _GsFlatpakTransaction {
|
||
FlatpakTransaction parent_instance;
|
||
GHashTable *refhash; /* ref:GsApp */
|
||
GError *first_operation_error;
|
||
gboolean stop_on_first_error;
|
||
FlatpakTransactionOperation *error_operation; /* (nullable) (owned) */
|
||
};
|
||
|
||
enum {
|
||
SIGNAL_REF_TO_APP,
|
||
LAST_SIGNAL
|
||
};
|
||
|
||
static guint signals[LAST_SIGNAL] = { 0 };
|
||
|
||
G_DEFINE_TYPE (GsFlatpakTransaction, gs_flatpak_transaction, FLATPAK_TYPE_TRANSACTION)
|
||
|
||
typedef enum {
|
||
PROP_STOP_ON_FIRST_ERROR = 1,
|
||
} GsFlatpakTransactionProperty;
|
||
|
||
static GParamSpec *props[PROP_STOP_ON_FIRST_ERROR + 1] = { NULL, };
|
||
|
||
static void
|
||
gs_flatpak_transaction_get_property (GObject *object,
|
||
guint prop_id,
|
||
GValue *value,
|
||
GParamSpec *pspec)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (object);
|
||
|
||
switch ((GsFlatpakTransactionProperty) prop_id) {
|
||
case PROP_STOP_ON_FIRST_ERROR:
|
||
g_value_set_boolean (value, self->stop_on_first_error);
|
||
break;
|
||
default:
|
||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static void
|
||
gs_flatpak_transaction_set_property (GObject *object,
|
||
guint prop_id,
|
||
const GValue *value,
|
||
GParamSpec *pspec)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (object);
|
||
|
||
switch ((GsFlatpakTransactionProperty) prop_id) {
|
||
case PROP_STOP_ON_FIRST_ERROR:
|
||
/* Construct only. */
|
||
self->stop_on_first_error = g_value_get_boolean (value);
|
||
g_object_notify_by_pspec (object, props[prop_id]);
|
||
break;
|
||
default:
|
||
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static void
|
||
gs_flatpak_transaction_dispose (GObject *object)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (object);
|
||
|
||
g_clear_object (&self->error_operation);
|
||
|
||
G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->dispose (object);
|
||
}
|
||
|
||
static void
|
||
gs_flatpak_transaction_finalize (GObject *object)
|
||
{
|
||
GsFlatpakTransaction *self;
|
||
g_return_if_fail (GS_IS_FLATPAK_TRANSACTION (object));
|
||
self = GS_FLATPAK_TRANSACTION (object);
|
||
|
||
g_assert (self != NULL);
|
||
g_hash_table_unref (self->refhash);
|
||
if (self->first_operation_error != NULL)
|
||
g_error_free (self->first_operation_error);
|
||
|
||
G_OBJECT_CLASS (gs_flatpak_transaction_parent_class)->finalize (object);
|
||
}
|
||
|
||
GsApp *
|
||
gs_flatpak_transaction_get_app_by_ref (FlatpakTransaction *transaction, const gchar *ref)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
return g_hash_table_lookup (self->refhash, ref);
|
||
}
|
||
|
||
static void
|
||
gs_flatpak_transaction_add_app_internal (GsFlatpakTransaction *self, GsApp *app)
|
||
{
|
||
g_autofree gchar *ref = gs_flatpak_app_get_ref_display (app);
|
||
g_hash_table_insert (self->refhash, g_steal_pointer (&ref), g_object_ref (app));
|
||
}
|
||
|
||
void
|
||
gs_flatpak_transaction_add_app (FlatpakTransaction *transaction, GsApp *app)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
gs_flatpak_transaction_add_app_internal (self, app);
|
||
if (gs_app_get_runtime (app) != NULL)
|
||
gs_flatpak_transaction_add_app_internal (self, gs_app_get_runtime (app));
|
||
}
|
||
|
||
static GsApp *
|
||
_ref_to_app (GsFlatpakTransaction *self, const gchar *ref)
|
||
{
|
||
GsApp *app = g_hash_table_lookup (self->refhash, ref);
|
||
if (app != NULL)
|
||
return g_object_ref (app);
|
||
g_signal_emit (self, signals[SIGNAL_REF_TO_APP], 0, ref, &app);
|
||
|
||
/* Cache the result */
|
||
if (app != NULL)
|
||
g_hash_table_insert (self->refhash, g_strdup (ref), g_object_ref (app));
|
||
|
||
return app;
|
||
}
|
||
|
||
static void
|
||
_transaction_operation_set_app (FlatpakTransactionOperation *op, GsApp *app)
|
||
{
|
||
g_object_set_data_full (G_OBJECT (op), "GsApp",
|
||
g_object_ref (app), (GDestroyNotify) g_object_unref);
|
||
}
|
||
|
||
static GsApp *
|
||
_transaction_operation_get_app (FlatpakTransactionOperation *op)
|
||
{
|
||
return g_object_get_data (G_OBJECT (op), "GsApp");
|
||
}
|
||
|
||
gboolean
|
||
gs_flatpak_transaction_run (FlatpakTransaction *transaction,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
g_autoptr(GError) error_local = NULL;
|
||
|
||
if (!flatpak_transaction_run (transaction, cancellable, &error_local)) {
|
||
/* whole transaction failed; restore the state for all the apps involved */
|
||
g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction);
|
||
for (GList *l = ops; l != NULL; l = l->next) {
|
||
FlatpakTransactionOperation *op = l->data;
|
||
const gchar *ref = flatpak_transaction_operation_get_ref (op);
|
||
g_autoptr(GsApp) app = _ref_to_app (self, ref);
|
||
if (app == NULL) {
|
||
g_warning ("failed to find app for %s", ref);
|
||
continue;
|
||
}
|
||
gs_app_set_state_recover (app);
|
||
}
|
||
|
||
if (self->first_operation_error != NULL) {
|
||
g_propagate_error (error, g_steal_pointer (&self->first_operation_error));
|
||
return FALSE;
|
||
} else {
|
||
g_propagate_error (error, g_steal_pointer (&error_local));
|
||
return FALSE;
|
||
}
|
||
}
|
||
|
||
return TRUE;
|
||
}
|
||
|
||
static gboolean
|
||
_transaction_ready (FlatpakTransaction *transaction)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
g_autolist(GObject) ops = NULL;
|
||
|
||
/* nothing to do */
|
||
ops = flatpak_transaction_get_operations (transaction);
|
||
if (ops == NULL)
|
||
return TRUE; // FIXME: error?
|
||
for (GList *l = ops; l != NULL; l = l->next) {
|
||
FlatpakTransactionOperation *op = l->data;
|
||
const gchar *ref = flatpak_transaction_operation_get_ref (op);
|
||
g_autoptr(GsApp) app = _ref_to_app (self, ref);
|
||
if (app != NULL) {
|
||
_transaction_operation_set_app (op, app);
|
||
/* if we're updating a component, then mark all the apps
|
||
* involved to ensure updating the button state */
|
||
if (flatpak_transaction_operation_get_operation_type (op) == FLATPAK_TRANSACTION_OPERATION_UPDATE) {
|
||
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN ||
|
||
gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
|
||
gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
|
||
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLING);
|
||
}
|
||
}
|
||
|
||
/* Debug dump. */
|
||
{
|
||
GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
|
||
g_autoptr(GString) debug_message = g_string_new ("");
|
||
|
||
g_string_append_printf (debug_message,
|
||
"%s: op %p, app %s (%p), download size %" G_GUINT64_FORMAT ", related-to:",
|
||
G_STRFUNC, op,
|
||
app ? gs_app_get_unique_id (app) : "?",
|
||
app,
|
||
flatpak_transaction_operation_get_download_size (op));
|
||
for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
|
||
FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
|
||
g_string_append_printf (debug_message,
|
||
"\n ├ %s (%p)", flatpak_transaction_operation_get_ref (related_to_op), related_to_op);
|
||
}
|
||
g_string_append (debug_message, "\n └ (end)");
|
||
g_assert (debug_message != NULL); /* -Wnull-dereference false positive */
|
||
g_debug ("%s", debug_message->str);
|
||
}
|
||
}
|
||
return TRUE;
|
||
}
|
||
|
||
typedef struct
|
||
{
|
||
GsFlatpakTransaction *transaction; /* (owned) */
|
||
FlatpakTransactionOperation *operation; /* (owned) */
|
||
GsApp *app; /* (owned) */
|
||
} ProgressData;
|
||
|
||
static void
|
||
progress_data_free (ProgressData *data)
|
||
{
|
||
g_clear_object (&data->operation);
|
||
g_clear_object (&data->app);
|
||
g_clear_object (&data->transaction);
|
||
g_free (data);
|
||
}
|
||
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (ProgressData, progress_data_free)
|
||
|
||
static gboolean
|
||
op_is_related_to_op (FlatpakTransactionOperation *op,
|
||
FlatpakTransactionOperation *root_op,
|
||
GHashTable *checked_ops)
|
||
{
|
||
GPtrArray *related_to_ops; /* (element-type FlatpakTransactionOperation) */
|
||
gpointer cached_is_related_to = NULL;
|
||
|
||
if (op == root_op)
|
||
return TRUE;
|
||
|
||
/* FIXME: Detect infinite loops. These indicate a bug in libflatpak,
|
||
* but have been hard to track down so far.
|
||
* See: https://gitlab.gnome.org/GNOME/gnome-software/-/issues/2689 */
|
||
if (g_hash_table_lookup_extended (checked_ops, op, NULL, &cached_is_related_to))
|
||
return GPOINTER_TO_INT (cached_is_related_to);
|
||
|
||
g_hash_table_insert (checked_ops, op, GINT_TO_POINTER (TRUE));
|
||
|
||
related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
|
||
for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
|
||
FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
|
||
if (related_to_op == root_op || op_is_related_to_op (related_to_op, root_op, checked_ops)) {
|
||
g_hash_table_insert (checked_ops, op, GINT_TO_POINTER (TRUE));
|
||
return TRUE;
|
||
}
|
||
}
|
||
|
||
g_hash_table_insert (checked_ops, op, GINT_TO_POINTER (FALSE));
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static guint64
|
||
saturated_uint64_add (guint64 a, guint64 b)
|
||
{
|
||
return (a <= G_MAXUINT64 - b) ? a + b : G_MAXUINT64;
|
||
}
|
||
|
||
/*
|
||
* update_progress_for_op:
|
||
* @self: a #GsFlatpakTransaction
|
||
* @current_progress: progress reporting object
|
||
* @ops: results of calling flatpak_transaction_get_operations() on @self, for performance
|
||
* @current_op: the #FlatpakTransactionOperation which the @current_progress is
|
||
* for; this is the operation currently being run by libflatpak
|
||
* @root_op: the #FlatpakTransactionOperation at the root of the operation subtree
|
||
* to calculate progress for
|
||
* @checked_ops: (element-type FlatpakTransactionOperation gboolean): hash table
|
||
* of already checked ops, to detect a dependency loop; an op being present in the table
|
||
* means it was already checked, and the stored value is the previous return value of
|
||
* op_is_related_to_op() for that op
|
||
*
|
||
* Calculate and update the #GsApp:progress for each app associated with
|
||
* @root_op in a flatpak transaction. This will include the #GsApp for the app
|
||
* being installed (for example), but also the #GsApps for all of its runtimes
|
||
* and locales, and any other dependencies of them.
|
||
*
|
||
* Each #GsApp:progress is calculated based on the sum of the progress of all
|
||
* the apps related to that one — so the progress for an app will factor in the
|
||
* progress for all its runtimes.
|
||
*/
|
||
static void
|
||
update_progress_for_op (GsFlatpakTransaction *self,
|
||
FlatpakTransactionProgress *current_progress,
|
||
GList *ops,
|
||
FlatpakTransactionOperation *current_op,
|
||
FlatpakTransactionOperation *root_op,
|
||
GHashTable *checked_ops)
|
||
{
|
||
g_autoptr(GsApp) root_app = NULL;
|
||
guint64 related_prior_download_bytes = 0;
|
||
guint64 related_download_bytes = 0;
|
||
guint64 current_bytes_transferred = flatpak_transaction_progress_get_bytes_transferred (current_progress);
|
||
gboolean seen_current_op = FALSE, seen_root_op = FALSE;
|
||
gboolean root_op_skipped = flatpak_transaction_operation_get_is_skipped (root_op);
|
||
guint percent;
|
||
|
||
/* If @root_op is being skipped and its GsApp isn't being
|
||
* installed/removed, don't update the progress on it. It may be that
|
||
* @root_op is the runtime of an app and the app is the thing the
|
||
* transaction was created for.
|
||
*/
|
||
if (root_op_skipped) {
|
||
/* _transaction_operation_set_app() is only called on non-skipped ops */
|
||
const gchar *ref = flatpak_transaction_operation_get_ref (root_op);
|
||
root_app = _ref_to_app (self, ref);
|
||
if (root_app == NULL) {
|
||
g_warning ("Couldn't find GsApp for transaction operation %s",
|
||
flatpak_transaction_operation_get_ref (root_op));
|
||
return;
|
||
}
|
||
if (gs_app_get_state (root_app) != GS_APP_STATE_INSTALLING &&
|
||
gs_app_get_state (root_app) != GS_APP_STATE_REMOVING &&
|
||
gs_app_get_state (root_app) != GS_APP_STATE_DOWNLOADING)
|
||
return;
|
||
} else {
|
||
GsApp *unskipped_root_app = _transaction_operation_get_app (root_op);
|
||
if (unskipped_root_app == NULL) {
|
||
g_warning ("Couldn't find GsApp for transaction operation %s",
|
||
flatpak_transaction_operation_get_ref (root_op));
|
||
return;
|
||
}
|
||
root_app = g_object_ref (unskipped_root_app);
|
||
}
|
||
|
||
/* This relies on ops in a #FlatpakTransaction being run in the order
|
||
* they’re returned by flatpak_transaction_get_operations(), which is true. */
|
||
for (GList *l = ops; l != NULL; l = l->next) {
|
||
FlatpakTransactionOperation *op = FLATPAK_TRANSACTION_OPERATION (l->data);
|
||
guint64 op_download_size = flatpak_transaction_operation_get_download_size (op);
|
||
|
||
if (op == current_op)
|
||
seen_current_op = TRUE;
|
||
if (op == root_op)
|
||
seen_root_op = TRUE;
|
||
|
||
/* Currently libflatpak doesn't return skipped ops in
|
||
* flatpak_transaction_get_operations(), but check just in case.
|
||
*/
|
||
if (op == root_op && root_op_skipped)
|
||
continue;
|
||
|
||
if (op_is_related_to_op (op, root_op, checked_ops)) {
|
||
/* Saturate instead of overflowing */
|
||
related_download_bytes = saturated_uint64_add (related_download_bytes, op_download_size);
|
||
if (!seen_current_op)
|
||
related_prior_download_bytes = saturated_uint64_add (related_prior_download_bytes, op_download_size);
|
||
}
|
||
}
|
||
|
||
g_assert (related_prior_download_bytes <= related_download_bytes);
|
||
g_assert (seen_root_op || root_op_skipped);
|
||
|
||
/* Avoid overflows when converting to percent, at the cost of losing
|
||
* some precision in the least significant digits. */
|
||
if (related_prior_download_bytes > G_MAXUINT64 / 100 ||
|
||
current_bytes_transferred > G_MAXUINT64 / 100) {
|
||
related_prior_download_bytes /= 100;
|
||
current_bytes_transferred /= 100;
|
||
related_download_bytes /= 100;
|
||
}
|
||
|
||
/* Update the progress of @root_app. */
|
||
if (related_download_bytes > 0)
|
||
percent = ((related_prior_download_bytes * 100 / related_download_bytes) +
|
||
(current_bytes_transferred * 100 / related_download_bytes));
|
||
else
|
||
percent = 0;
|
||
|
||
if (gs_app_get_progress (root_app) == 100 ||
|
||
gs_app_get_progress (root_app) == GS_APP_PROGRESS_UNKNOWN ||
|
||
gs_app_get_progress (root_app) <= percent) {
|
||
gs_app_set_progress (root_app, percent);
|
||
} else {
|
||
g_warning ("ignoring percentage %u%% -> %u%% as going down on app %s",
|
||
gs_app_get_progress (root_app), percent,
|
||
gs_app_get_unique_id (root_app));
|
||
}
|
||
}
|
||
|
||
static void
|
||
update_progress_for_op_recurse_up (GsFlatpakTransaction *self,
|
||
FlatpakTransactionProgress *progress,
|
||
GList *ops,
|
||
FlatpakTransactionOperation *current_op,
|
||
FlatpakTransactionOperation *root_op,
|
||
GHashTable *checked_ops)
|
||
{
|
||
GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (root_op);
|
||
|
||
if (g_hash_table_contains (checked_ops, root_op))
|
||
return;
|
||
|
||
/* Update progress for @root_op */
|
||
update_progress_for_op (self, progress, ops, current_op, root_op, checked_ops);
|
||
|
||
/* Update progress for ops related to @root_op, e.g. apps whose runtime is @root_op */
|
||
for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
|
||
FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
|
||
update_progress_for_op_recurse_up (self, progress, ops, current_op, related_to_op, checked_ops);
|
||
}
|
||
}
|
||
|
||
static void
|
||
_transaction_progress_changed_cb (FlatpakTransactionProgress *progress,
|
||
gpointer user_data)
|
||
{
|
||
ProgressData *data = user_data;
|
||
GsApp *app = data->app;
|
||
GsFlatpakTransaction *self = data->transaction;
|
||
g_autoptr(GHashTable) checked_ops = NULL;
|
||
g_autolist(FlatpakTransactionOperation) ops = NULL;
|
||
|
||
if (flatpak_transaction_progress_get_is_estimating (progress)) {
|
||
/* "Estimating" happens while fetching the metadata, which
|
||
* flatpak arbitrarily decides happens during the first 5% of
|
||
* each operation. At this point, no more detailed progress
|
||
* information is available. */
|
||
gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
|
||
return;
|
||
}
|
||
|
||
/* Update the progress on this app, and then do the same for each
|
||
* related parent app up the hierarchy. For example, @data->operation
|
||
* could be for a runtime which was added to the transaction because of
|
||
* an app — so we need to update the progress on the app too.
|
||
*
|
||
* It’s important to note that a new @data->progress is created by
|
||
* libflatpak for each @data->operation, and there are multiple
|
||
* operations in a transaction. There is no #FlatpakTransactionProgress
|
||
* which represents the progress of the whole transaction.
|
||
*
|
||
* There may be arbitrary many levels of related-to ops. For example,
|
||
* one common situation would be to install an app which needs a new
|
||
* runtime, and that runtime needs a locale to be installed, which would
|
||
* give three levels of related-to relation:
|
||
* locale → runtime → app → (null)
|
||
*
|
||
* In addition, libflatpak may decide to skip some operations (if they
|
||
* turn out to not be necessary). These skipped operations are not
|
||
* included in the list returned by flatpak_transaction_get_operations(),
|
||
* but they can be accessed via
|
||
* flatpak_transaction_operation_get_related_to_ops(), so have to be
|
||
* ignored manually.
|
||
*/
|
||
ops = flatpak_transaction_get_operations (FLATPAK_TRANSACTION (self));
|
||
checked_ops = g_hash_table_new (NULL, NULL);
|
||
update_progress_for_op_recurse_up (self, progress, ops, data->operation, data->operation, checked_ops);
|
||
}
|
||
|
||
static const gchar *
|
||
_flatpak_transaction_operation_type_to_string (FlatpakTransactionOperationType ot)
|
||
{
|
||
if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL)
|
||
return "install";
|
||
if (ot == FLATPAK_TRANSACTION_OPERATION_UPDATE)
|
||
return "update";
|
||
if (ot == FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE)
|
||
return "install-bundle";
|
||
if (ot == FLATPAK_TRANSACTION_OPERATION_UNINSTALL)
|
||
return "uninstall";
|
||
return NULL;
|
||
}
|
||
|
||
static void
|
||
progress_data_free_closure (gpointer user_data,
|
||
GClosure *closure)
|
||
{
|
||
progress_data_free (user_data);
|
||
}
|
||
|
||
static void
|
||
_transaction_new_operation (FlatpakTransaction *transaction,
|
||
FlatpakTransactionOperation *operation,
|
||
FlatpakTransactionProgress *progress)
|
||
{
|
||
GsApp *app;
|
||
g_autoptr(ProgressData) progress_data = NULL;
|
||
|
||
/* find app */
|
||
app = _transaction_operation_get_app (operation);
|
||
if (app == NULL) {
|
||
FlatpakTransactionOperationType ot;
|
||
ot = flatpak_transaction_operation_get_operation_type (operation);
|
||
g_warning ("failed to find app for %s during %s",
|
||
flatpak_transaction_operation_get_ref (operation),
|
||
_flatpak_transaction_operation_type_to_string (ot));
|
||
return;
|
||
}
|
||
|
||
/* report progress */
|
||
progress_data = g_new0 (ProgressData, 1);
|
||
progress_data->transaction = GS_FLATPAK_TRANSACTION (g_object_ref (transaction));
|
||
progress_data->app = g_object_ref (app);
|
||
progress_data->operation = g_object_ref (operation);
|
||
|
||
g_signal_connect_data (progress, "changed",
|
||
G_CALLBACK (_transaction_progress_changed_cb),
|
||
g_steal_pointer (&progress_data),
|
||
progress_data_free_closure,
|
||
0 /* flags */);
|
||
flatpak_transaction_progress_set_update_frequency (progress, 500); /* FIXME? */
|
||
|
||
/* set app status */
|
||
switch (flatpak_transaction_operation_get_operation_type (operation)) {
|
||
case FLATPAK_TRANSACTION_OPERATION_INSTALL:
|
||
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
|
||
gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLING);
|
||
break;
|
||
case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
|
||
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN)
|
||
gs_app_set_state (app, GS_APP_STATE_AVAILABLE_LOCAL);
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLING);
|
||
break;
|
||
case FLATPAK_TRANSACTION_OPERATION_UPDATE:
|
||
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN ||
|
||
gs_app_get_state (app) == GS_APP_STATE_INSTALLED)
|
||
gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLING);
|
||
break;
|
||
case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
|
||
gs_app_set_state (app, GS_APP_STATE_REMOVING);
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
later_op_also_related (GList *ops,
|
||
FlatpakTransactionOperation *current_op,
|
||
FlatpakTransactionOperation *related_to_current_op)
|
||
{
|
||
/* Here we're determining if anything in @ops which comes after
|
||
* @current_op is related to @related_to_current_op and not skipped
|
||
* (but all @ops are not skipped so no need to check explicitly)
|
||
*/
|
||
gboolean found_later_op = FALSE, seen_current_op = FALSE;
|
||
for (GList *l = ops; l != NULL; l = l->next) {
|
||
FlatpakTransactionOperation *op = l->data;
|
||
GPtrArray *related_to_ops;
|
||
if (current_op == op) {
|
||
seen_current_op = TRUE;
|
||
continue;
|
||
}
|
||
if (!seen_current_op)
|
||
continue;
|
||
|
||
related_to_ops = flatpak_transaction_operation_get_related_to_ops (op);
|
||
for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
|
||
FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
|
||
if (related_to_op == related_to_current_op) {
|
||
g_assert (flatpak_transaction_operation_get_is_skipped (related_to_op));
|
||
found_later_op = TRUE;
|
||
}
|
||
}
|
||
}
|
||
|
||
return found_later_op;
|
||
}
|
||
|
||
static void
|
||
set_skipped_related_apps_to_installed (GsFlatpakTransaction *self,
|
||
FlatpakTransaction *transaction,
|
||
FlatpakTransactionOperation *operation)
|
||
{
|
||
/* It's possible the thing being updated/installed, @operation, is a
|
||
* related ref (e.g. extension or runtime) of an app which itself doesn't
|
||
* need an update and therefore won't have _transaction_operation_done()
|
||
* called for it directly. So we have to set the main app to installed
|
||
* here.
|
||
*/
|
||
g_autolist(GObject) ops = flatpak_transaction_get_operations (transaction);
|
||
GPtrArray *related_to_ops = flatpak_transaction_operation_get_related_to_ops (operation);
|
||
|
||
for (gsize i = 0; related_to_ops != NULL && i < related_to_ops->len; i++) {
|
||
FlatpakTransactionOperation *related_to_op = g_ptr_array_index (related_to_ops, i);
|
||
if (flatpak_transaction_operation_get_is_skipped (related_to_op)) {
|
||
const gchar *ref;
|
||
g_autoptr(GsApp) related_to_app = NULL;
|
||
|
||
/* Check that no later op is also related to related_to_op, in
|
||
* which case we want to let that operation finish before setting
|
||
* the main app to installed.
|
||
*/
|
||
if (later_op_also_related (ops, operation, related_to_op))
|
||
continue;
|
||
|
||
ref = flatpak_transaction_operation_get_ref (related_to_op);
|
||
related_to_app = _ref_to_app (self, ref);
|
||
if (related_to_app != NULL)
|
||
gs_app_set_state (related_to_app, GS_APP_STATE_INSTALLED);
|
||
}
|
||
}
|
||
}
|
||
|
||
static void
|
||
_transaction_operation_done (FlatpakTransaction *transaction,
|
||
FlatpakTransactionOperation *operation,
|
||
const gchar *commit,
|
||
FlatpakTransactionResult details)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
|
||
/* invalidate */
|
||
GsApp *app = _transaction_operation_get_app (operation);
|
||
if (app == NULL) {
|
||
g_warning ("failed to find app for %s",
|
||
flatpak_transaction_operation_get_ref (operation));
|
||
return;
|
||
}
|
||
switch (flatpak_transaction_operation_get_operation_type (operation)) {
|
||
case FLATPAK_TRANSACTION_OPERATION_INSTALL:
|
||
case FLATPAK_TRANSACTION_OPERATION_INSTALL_BUNDLE:
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLED);
|
||
|
||
set_skipped_related_apps_to_installed (self, transaction, operation);
|
||
break;
|
||
case FLATPAK_TRANSACTION_OPERATION_UPDATE:
|
||
gs_app_set_version (app, gs_app_get_update_version (app));
|
||
gs_app_set_update_details_markup (app, NULL);
|
||
gs_app_set_update_urgency (app, AS_URGENCY_KIND_UNKNOWN);
|
||
gs_app_set_update_version (app, NULL);
|
||
/* force getting the new runtime */
|
||
gs_app_remove_kudo (app, GS_APP_KUDO_SANDBOXED);
|
||
/* downloaded, but not yet installed */
|
||
if (flatpak_transaction_get_no_deploy (transaction))
|
||
gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
|
||
else
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLED);
|
||
|
||
set_skipped_related_apps_to_installed (self, transaction, operation);
|
||
break;
|
||
case FLATPAK_TRANSACTION_OPERATION_UNINSTALL:
|
||
/* we don't actually know if this app is re-installable */
|
||
gs_flatpak_app_set_commit (app, NULL);
|
||
gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
|
||
break;
|
||
default:
|
||
gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
|
||
break;
|
||
}
|
||
}
|
||
|
||
static gboolean
|
||
_transaction_operation_error (FlatpakTransaction *transaction,
|
||
FlatpakTransactionOperation *operation,
|
||
const GError *error,
|
||
FlatpakTransactionErrorDetails detail)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
FlatpakTransactionOperationType operation_type = flatpak_transaction_operation_get_operation_type (operation);
|
||
GsApp *app = _transaction_operation_get_app (operation);
|
||
const gchar *ref = flatpak_transaction_operation_get_ref (operation);
|
||
|
||
gs_app_set_state_recover (app);
|
||
g_set_object (&self->error_operation, operation);
|
||
|
||
if (g_error_matches (error, FLATPAK_ERROR, FLATPAK_ERROR_SKIPPED)) {
|
||
g_debug ("skipped to %s %s: %s",
|
||
_flatpak_transaction_operation_type_to_string (operation_type),
|
||
ref,
|
||
error->message);
|
||
return TRUE; /* continue */
|
||
}
|
||
|
||
/* If the transaction has been cancelled, bail out early rather
|
||
* than continuing to try operations which are all cancelled. */
|
||
if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
|
||
g_debug ("Transaction cancelled; stopping it");
|
||
|
||
return FALSE; /* stop */
|
||
}
|
||
|
||
if (detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) {
|
||
g_warning ("failed to %s %s (non fatal): %s",
|
||
_flatpak_transaction_operation_type_to_string (operation_type),
|
||
ref,
|
||
error->message);
|
||
return TRUE; /* continue */
|
||
}
|
||
|
||
if (self->first_operation_error == NULL) {
|
||
g_propagate_error (&self->first_operation_error,
|
||
g_error_copy (error));
|
||
if (app != NULL)
|
||
gs_utils_error_add_app_id (&self->first_operation_error, app);
|
||
}
|
||
|
||
if (!(detail & FLATPAK_TRANSACTION_ERROR_DETAILS_NON_FATAL) &&
|
||
self->stop_on_first_error)
|
||
return FALSE; /* stop */
|
||
|
||
return TRUE; /* continue */
|
||
}
|
||
|
||
static int
|
||
_transaction_choose_remote_for_ref (FlatpakTransaction *transaction,
|
||
const char *for_ref,
|
||
const char *runtime_ref,
|
||
const char * const *remotes)
|
||
{
|
||
//FIXME: do something smarter
|
||
return 0;
|
||
}
|
||
|
||
static void
|
||
_transaction_end_of_lifed (FlatpakTransaction *transaction,
|
||
const gchar *ref,
|
||
const gchar *reason,
|
||
const gchar *rebase)
|
||
{
|
||
if (rebase) {
|
||
g_message ("%s is end-of-life, in favor of %s", ref, rebase);
|
||
} else if (reason) {
|
||
g_message ("%s is end-of-life, with reason: %s", ref, reason);
|
||
}
|
||
//FIXME: show something in the UI
|
||
}
|
||
|
||
static gboolean
|
||
_transaction_end_of_lifed_with_rebase (FlatpakTransaction *transaction,
|
||
const gchar *remote,
|
||
const gchar *ref,
|
||
const gchar *reason,
|
||
const gchar *rebased_to_ref,
|
||
const gchar **previous_ids)
|
||
{
|
||
GsFlatpakTransaction *self = GS_FLATPAK_TRANSACTION (transaction);
|
||
|
||
if (rebased_to_ref) {
|
||
g_message ("%s is end-of-life, in favor of %s", ref, rebased_to_ref);
|
||
} else if (reason) {
|
||
g_message ("%s is end-of-life, with reason: %s", ref, reason);
|
||
}
|
||
|
||
if (rebased_to_ref && remote) {
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
#if FLATPAK_CHECK_VERSION(1, 15, 6)
|
||
if (!flatpak_transaction_add_rebase_and_uninstall (transaction, remote, rebased_to_ref, ref,
|
||
NULL, previous_ids, &local_error)) {
|
||
#else
|
||
if (!flatpak_transaction_add_rebase (transaction, remote, rebased_to_ref,
|
||
NULL, previous_ids, &local_error) ||
|
||
!flatpak_transaction_add_uninstall (transaction, ref, &local_error)) {
|
||
/* NOT_INSTALLED error is expected in case the op that triggered this was install not update */
|
||
if (g_error_matches (local_error, FLATPAK_ERROR, FLATPAK_ERROR_NOT_INSTALLED))
|
||
g_clear_error (&local_error);
|
||
else
|
||
#endif
|
||
if (self->first_operation_error == NULL)
|
||
g_propagate_prefixed_error (&self->first_operation_error,
|
||
g_steal_pointer (&local_error),
|
||
"Failed to rebase %s to %s: ",
|
||
ref, rebased_to_ref);
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
/* Note: A message about the rename will be shown in the UI
|
||
* thanks to code in gs_flatpak_refine_appstream() which
|
||
* sets gs_app_set_renamed_from().
|
||
*/
|
||
return TRUE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static gboolean
|
||
_transaction_add_new_remote (FlatpakTransaction *transaction,
|
||
FlatpakTransactionRemoteReason reason,
|
||
const char *from_id,
|
||
const char *remote_name,
|
||
const char *url)
|
||
{
|
||
/* additional apps */
|
||
if (reason == FLATPAK_TRANSACTION_REMOTE_GENERIC_REPO) {
|
||
g_debug ("configuring %s as new generic remote", url);
|
||
return TRUE; //FIXME?
|
||
}
|
||
|
||
/* runtime deps always make sense */
|
||
if (reason == FLATPAK_TRANSACTION_REMOTE_RUNTIME_DEPS) {
|
||
g_debug ("configuring %s as new remote for deps", url);
|
||
return TRUE;
|
||
}
|
||
|
||
return FALSE;
|
||
}
|
||
|
||
static void
|
||
gs_flatpak_transaction_class_init (GsFlatpakTransactionClass *klass)
|
||
{
|
||
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
||
FlatpakTransactionClass *transaction_class = FLATPAK_TRANSACTION_CLASS (klass);
|
||
|
||
object_class->get_property = gs_flatpak_transaction_get_property;
|
||
object_class->set_property = gs_flatpak_transaction_set_property;
|
||
object_class->dispose = gs_flatpak_transaction_dispose;
|
||
object_class->finalize = gs_flatpak_transaction_finalize;
|
||
|
||
transaction_class->ready = _transaction_ready;
|
||
transaction_class->add_new_remote = _transaction_add_new_remote;
|
||
transaction_class->new_operation = _transaction_new_operation;
|
||
transaction_class->operation_done = _transaction_operation_done;
|
||
transaction_class->operation_error = _transaction_operation_error;
|
||
transaction_class->choose_remote_for_ref = _transaction_choose_remote_for_ref;
|
||
transaction_class->end_of_lifed = _transaction_end_of_lifed;
|
||
transaction_class->end_of_lifed_with_rebase = _transaction_end_of_lifed_with_rebase;
|
||
|
||
/**
|
||
* GsFlatpakTransaction:stop-on-first-error:
|
||
*
|
||
* Stop the transaction on the first fatal error. If %FALSE, the
|
||
* transaction will continue running and ignore subsequent errors. Some
|
||
* operations may be automatically skipped if they are related to
|
||
* operations which have errored.
|
||
*
|
||
* Typically this should be %TRUE. It may be %FALSE for transactions
|
||
* where lots of apps are being updated, as typically updates should be
|
||
* mostly independent of each other, and we want as many of them to
|
||
* be attempted as possible.
|
||
*
|
||
* Since: 44
|
||
*/
|
||
props[PROP_STOP_ON_FIRST_ERROR] =
|
||
g_param_spec_boolean ("stop-on-first-error",
|
||
"Stop on First Error",
|
||
"Stop the transaction on the first fatal error.",
|
||
TRUE,
|
||
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);
|
||
|
||
signals[SIGNAL_REF_TO_APP] =
|
||
g_signal_new ("ref-to-app",
|
||
G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
|
||
0, NULL, NULL, NULL, G_TYPE_OBJECT, 1, G_TYPE_STRING);
|
||
}
|
||
|
||
static void
|
||
gs_flatpak_transaction_init (GsFlatpakTransaction *self)
|
||
{
|
||
self->refhash = g_hash_table_new_full (g_str_hash, g_str_equal,
|
||
g_free, (GDestroyNotify) g_object_unref);
|
||
self->stop_on_first_error = TRUE;
|
||
}
|
||
|
||
FlatpakTransaction *
|
||
gs_flatpak_transaction_new (FlatpakInstallation *installation,
|
||
gboolean stop_on_first_error,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
GsFlatpakTransaction *self;
|
||
self = g_initable_new (GS_TYPE_FLATPAK_TRANSACTION,
|
||
cancellable, error,
|
||
"installation", installation,
|
||
"stop-on-first-error", stop_on_first_error,
|
||
NULL);
|
||
if (self == NULL)
|
||
return NULL;
|
||
return FLATPAK_TRANSACTION (self);
|
||
}
|
||
|
||
/**
|
||
* gs_flatpak_transaction_get_error_operation:
|
||
* @self: a #GsFlatpakTransaction
|
||
* @out_app: (out) (transfer none) (optional) (nullable): return location for
|
||
* the #GsApp associated with the returned transaction operation, or %NULL
|
||
* to ignore; the returned value may be %NULL if no error operation is set,
|
||
* or if there’s no app associated with it
|
||
*
|
||
* Get the #FlatpakTransactionOperation which caused the most recent error in
|
||
* the transaction.
|
||
*
|
||
* For transactions with #GsFlatpakTransaction:stop-on-first-error set, this
|
||
* will be the operation that caused the fatal error.
|
||
*
|
||
* Returns: (nullable) (transfer none): the operation which caused the error, or
|
||
* %NULL if none
|
||
* Since: 47
|
||
*/
|
||
FlatpakTransactionOperation *
|
||
gs_flatpak_transaction_get_error_operation (GsFlatpakTransaction *self,
|
||
GsApp **out_app)
|
||
{
|
||
g_return_val_if_fail (GS_IS_FLATPAK_TRANSACTION (self), NULL);
|
||
|
||
if (out_app != NULL) {
|
||
if (self->error_operation != NULL)
|
||
*out_app = _transaction_operation_get_app (self->error_operation);
|
||
else
|
||
*out_app = NULL;
|
||
}
|
||
|
||
return self->error_operation;
|
||
}
|