summaryrefslogtreecommitdiffstats
path: root/plugins/eos-updater/gs-plugin-eos-updater.c
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/eos-updater/gs-plugin-eos-updater.c')
-rw-r--r--plugins/eos-updater/gs-plugin-eos-updater.c962
1 files changed, 962 insertions, 0 deletions
diff --git a/plugins/eos-updater/gs-plugin-eos-updater.c b/plugins/eos-updater/gs-plugin-eos-updater.c
new file mode 100644
index 0000000..a8aba16
--- /dev/null
+++ b/plugins/eos-updater/gs-plugin-eos-updater.c
@@ -0,0 +1,962 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016-2019 Endless Mobile, Inc
+ *
+ * Authors:
+ * Joaquim Rocha <jrocha@endlessm.com>
+ * Philip Withnall <withnall@endlessm.com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include <config.h>
+#include <gio/gio.h>
+#include <glib.h>
+#include <glib/gi18n.h>
+#include <glib-object.h>
+#include <gnome-software.h>
+#include <gs-plugin.h>
+#include <gs-utils.h>
+#include <math.h>
+#include <ostree.h>
+
+#include "gs-eos-updater-generated.h"
+
+/*
+ * SECTION:
+ * Plugin to poll for, download and apply OS updates using the `eos-updater`
+ * service when running on Endless OS.
+ *
+ * This plugin is only useful on Endless OS.
+ *
+ * It creates a proxy for the `eos-updater` D-Bus service, which implements a
+ * basic state machine which progresses through several states in order to
+ * download updates: `Ready` (doing nothing) → `Poll` (checking for updates) →
+ * `Fetch` (downloading an update) → `Apply` (deploying the update’s OSTree,
+ * before a reboot). Any state may transition to the `Error` state at any time,
+ * and the daemon may disappear at any time.
+ *
+ * This plugin follows the state transitions signalled by the daemon, and
+ * updates the state of a single #GsApp instance (`os_upgrade`) to reflect the
+ * OS upgrade in the UI.
+ *
+ * Calling gs_plugin_refresh() will result in this plugin calling the `Poll()`
+ * method on the `eos-updater` daemon to check for a new update.
+ *
+ * Calling gs_plugin_app_upgrade_download() will result in this plugin calling
+ * a sequence of methods on the `eos-updater` daemon to check for, download and
+ * apply an update. Typically, gs_plugin_app_upgrade_download() should be called
+ * once `eos-updater` is already in the `UpdateAvailable` state. It will report
+ * progress information, with the first 75 percentage points of the progress
+ * reporting the download progress, and the final 25 percentage points reporting
+ * the OSTree deployment progress. The final 25 percentage points are currently
+ * faked because we can’t get reasonable progress data out of OSTree.
+ *
+ * The proxy object (`updater_proxy`) uses the thread-default main context from
+ * the gs_plugin_setup() function, which is currently the global default main
+ * context from gnome-software’s main thread. This means all the signal
+ * callbacks from the proxy will be executed in the main thread, and *must not
+ * block*.
+ *
+ * The other functions (gs_plugin_refresh(), gs_plugin_app_upgrade_download(),
+ * etc.) are called in #GTask worker threads. They are allowed to call methods
+ * on the proxy; the main thread is only allowed to receive signals and check
+ * properties on the proxy, to avoid blocking. Consequently, worker threads need
+ * to block on the main thread receiving state change signals from
+ * `eos-updater`. Receipt of these signals is notified through
+ * `state_change_cond`. This means that all functions which access the
+ * `GsPluginData` must lock it using the `mutex`.
+ *
+ * `updater_proxy`, `os_upgrade` and `cancellable` are only set in
+ * gs_plugin_setup(), and are both internally thread-safe — so they can both be
+ * dereferenced and have their methods called from any thread without
+ * necessarily holding `mutex`.
+ *
+ * Cancellation of any operations on the `eos-updater` daemon (polling, fetching
+ * or applying) is implemented by calling the `Cancel()` method on it. This is
+ * permanently connected to the private `cancellable` #GCancellable instance,
+ * which persists for the lifetime of the plugin. The #GCancellable instances
+ * for various operations can be temporarily chained to it for the duration of
+ * each operation.
+ */
+
+static const guint max_progress_for_update = 75; /* percent */
+
+typedef enum {
+ EOS_UPDATER_STATE_NONE = 0,
+ EOS_UPDATER_STATE_READY,
+ EOS_UPDATER_STATE_ERROR,
+ EOS_UPDATER_STATE_POLLING,
+ EOS_UPDATER_STATE_UPDATE_AVAILABLE,
+ EOS_UPDATER_STATE_FETCHING,
+ EOS_UPDATER_STATE_UPDATE_READY,
+ EOS_UPDATER_STATE_APPLYING_UPDATE,
+ EOS_UPDATER_STATE_UPDATE_APPLIED,
+} EosUpdaterState;
+#define EOS_UPDATER_N_STATES (EOS_UPDATER_STATE_UPDATE_APPLIED + 1)
+
+static const gchar *
+eos_updater_state_to_str (EosUpdaterState state)
+{
+ const gchar * const eos_updater_state_str[] = {
+ "None",
+ "Ready",
+ "Error",
+ "Polling",
+ "UpdateAvailable",
+ "Fetching",
+ "UpdateReady",
+ "ApplyingUpdate",
+ "UpdateApplied",
+ };
+
+ G_STATIC_ASSERT (G_N_ELEMENTS (eos_updater_state_str) == EOS_UPDATER_N_STATES);
+
+ g_return_val_if_fail ((gint) state < EOS_UPDATER_N_STATES, "unknown");
+ return eos_updater_state_str[state];
+}
+
+static void
+gs_eos_updater_error_convert (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return;
+
+ /* parse remote eos-updater error */
+ if (g_dbus_error_is_remote_error (error)) {
+ g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
+
+ g_dbus_error_strip_remote_error (error);
+
+ if (g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongState")) {
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.LiveBoot") ||
+ g_str_equal (remote_error, "com.endlessm.Updater.Error.NotOstreeSystem") ||
+ g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) {
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongConfiguration")) {
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.Fetching")) {
+ error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED;
+ } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.MalformedAutoinstallSpec") ||
+ g_str_equal (remote_error, "com.endlessm.Updater.Error.UnknownEntryInAutoinstallSpec") ||
+ g_str_equal (remote_error, "com.endlessm.Updater.Error.FlatpakRemoteConflict")) {
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.MeteredConnection")) {
+ error->code = GS_PLUGIN_ERROR_NO_NETWORK;
+ } else if (g_str_equal (remote_error, "com.endlessm.Updater.Error.Cancelled")) {
+ error->code = GS_PLUGIN_ERROR_CANCELLED;
+ } else {
+ g_warning ("Can’t reliably fixup remote error ‘%s’", remote_error);
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return;
+ }
+
+ /* this is allowed for low-level errors */
+ if (gs_utils_error_convert_gio (perror))
+ return;
+
+ /* this is allowed for low-level errors */
+ if (gs_utils_error_convert_gdbus (perror))
+ return;
+}
+
+/* the percentage of the progress bar to use for applying the OS upgrade;
+ * we need to fake the progress in this percentage because applying the OS upgrade
+ * can take a long time and we don't want the user to think that the upgrade has
+ * stalled */
+static const guint upgrade_apply_progress_range = 100 - max_progress_for_update; /* percent */
+static const gfloat upgrade_apply_max_time = 600.0; /* sec */
+static const gfloat upgrade_apply_step_time = 0.250; /* sec */
+
+static void sync_state_from_updater_unlocked (GsPlugin *plugin);
+
+struct GsPluginData
+{
+ /* These members are only set once in gs_plugin_setup(), and are
+ * internally thread-safe, so can be accessed without holding @mutex: */
+ GsEosUpdater *updater_proxy; /* (owned) */
+ GsApp *os_upgrade; /* (owned) */
+ GCancellable *cancellable; /* (owned) */
+ gulong cancelled_id;
+
+ /* These members must only ever be accessed from the main thread, so
+ * can be accessed without holding @mutex: */
+ gfloat upgrade_fake_progress;
+ guint upgrade_fake_progress_handler;
+
+ /* State synchronisation between threads: */
+ GMutex mutex;
+ GCond state_change_cond; /* locked by @mutex */
+};
+
+static void
+os_upgrade_cancelled_cb (GCancellable *cancellable,
+ GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ g_debug ("%s: Cancelling upgrade", G_STRFUNC);
+ gs_eos_updater_call_cancel (priv->updater_proxy, NULL, NULL, NULL);
+}
+
+static gboolean
+should_add_os_upgrade (AsAppState state)
+{
+ switch (state) {
+ case AS_APP_STATE_AVAILABLE:
+ case AS_APP_STATE_AVAILABLE_LOCAL:
+ case AS_APP_STATE_UPDATABLE:
+ case AS_APP_STATE_QUEUED_FOR_INSTALL:
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ return TRUE;
+ case AS_APP_STATE_UNKNOWN:
+ case AS_APP_STATE_INSTALLED:
+ case AS_APP_STATE_UNAVAILABLE:
+ case AS_APP_STATE_REMOVING:
+ default:
+ return FALSE;
+ }
+}
+
+/* Wrapper around gs_app_set_state() which ensures we also notify of update
+ * changes if we change between non-upgradable and upgradable states, so that
+ * the app is notified to appear in the UI. */
+static void
+app_set_state (GsPlugin *plugin,
+ GsApp *app,
+ AsAppState new_state)
+{
+ AsAppState old_state = gs_app_get_state (app);
+
+ if (new_state == old_state)
+ return;
+
+ gs_app_set_state (app, new_state);
+
+ if (should_add_os_upgrade (old_state) !=
+ should_add_os_upgrade (new_state)) {
+ g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC);
+ gs_plugin_updates_changed (plugin);
+ }
+}
+
+static gboolean
+eos_updater_error_is_cancelled (const gchar *error_name)
+{
+ return (g_strcmp0 (error_name, "com.endlessm.Updater.Error.Cancelled") == 0);
+}
+
+/* This will be invoked in the main thread. */
+static void
+updater_state_changed (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ g_debug ("%s", G_STRFUNC);
+
+ sync_state_from_updater_unlocked (plugin);
+
+ /* Signal any blocked threads; typically this will be
+ * gs_plugin_app_upgrade_download() in a #GTask worker thread. */
+ g_cond_broadcast (&priv->state_change_cond);
+}
+
+/* This will be invoked in the main thread. */
+static void
+updater_downloaded_bytes_changed (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ sync_state_from_updater_unlocked (plugin);
+}
+
+/* This will be invoked in the main thread, but doesn’t currently need to hold
+ * `mutex` since it only accesses `priv->updater_proxy` and `priv->os_upgrade`,
+ * both of which are internally thread-safe. */
+static void
+updater_version_changed (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ const gchar *version = gs_eos_updater_get_version (priv->updater_proxy);
+
+ /* If eos-updater goes away, we want to retain the previously set value
+ * of the version, for use in error messages. */
+ if (version != NULL)
+ gs_app_set_version (priv->os_upgrade, version);
+}
+
+/* This will be invoked in the main thread, but doesn’t currently need to hold
+ * `mutex` since `priv->updater_proxy` and `priv->os_upgrade` are both
+ * thread-safe, and `priv->upgrade_fake_progress` and
+ * `priv->upgrade_fake_progress_handler` are only ever accessed from the main
+ * thread. */
+static gboolean
+fake_os_upgrade_progress_cb (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ gfloat normal_step;
+ guint new_progress;
+ const gfloat fake_progress_max = 99.0;
+
+ if (gs_eos_updater_get_state (priv->updater_proxy) != EOS_UPDATER_STATE_APPLYING_UPDATE ||
+ priv->upgrade_fake_progress > fake_progress_max) {
+ priv->upgrade_fake_progress = 0;
+ priv->upgrade_fake_progress_handler = 0;
+ return G_SOURCE_REMOVE;
+ }
+
+ normal_step = (gfloat) upgrade_apply_progress_range /
+ (upgrade_apply_max_time / upgrade_apply_step_time);
+
+ priv->upgrade_fake_progress += normal_step;
+
+ new_progress = max_progress_for_update +
+ (guint) round (priv->upgrade_fake_progress);
+ gs_app_set_progress (priv->os_upgrade,
+ MIN (new_progress, (guint) fake_progress_max));
+
+ g_debug ("OS upgrade fake progress: %f", priv->upgrade_fake_progress);
+
+ return G_SOURCE_CONTINUE;
+}
+
+/* This method deals with the synchronization between the EOS updater's states
+ * (D-Bus service) and the OS upgrade's states (GsApp), in order to show the user
+ * what is happening and what they can do.
+ *
+ * It must be called with priv->mutex already locked. */
+static void
+sync_state_from_updater_unlocked (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ GsApp *app = priv->os_upgrade;
+ EosUpdaterState state;
+ AsAppState previous_app_state = gs_app_get_state (app);
+ AsAppState current_app_state;
+
+ /* in case the OS upgrade has been disabled */
+ if (priv->updater_proxy == NULL) {
+ g_debug ("%s: Updater disabled", G_STRFUNC);
+ return;
+ }
+
+ state = gs_eos_updater_get_state (priv->updater_proxy);
+ g_debug ("EOS Updater state changed: %s", eos_updater_state_to_str (state));
+
+ switch (state) {
+ case EOS_UPDATER_STATE_NONE:
+ case EOS_UPDATER_STATE_READY: {
+ app_set_state (plugin, app, AS_APP_STATE_UNKNOWN);
+ break;
+ } case EOS_UPDATER_STATE_POLLING: {
+ /* Nothing to do here. */
+ break;
+ } case EOS_UPDATER_STATE_UPDATE_AVAILABLE: {
+ guint64 total_size;
+
+ app_set_state (plugin, app, AS_APP_STATE_AVAILABLE);
+
+ total_size = gs_eos_updater_get_download_size (priv->updater_proxy);
+ gs_app_set_size_download (app, total_size);
+
+ break;
+ }
+ case EOS_UPDATER_STATE_FETCHING: {
+ guint64 total_size = 0;
+ guint64 downloaded = 0;
+ gfloat progress = 0;
+
+ /* FIXME: Set to QUEUED_FOR_INSTALL if we’re waiting for metered
+ * data permission. */
+ app_set_state (plugin, app, AS_APP_STATE_INSTALLING);
+
+ downloaded = gs_eos_updater_get_downloaded_bytes (priv->updater_proxy);
+ total_size = gs_eos_updater_get_download_size (priv->updater_proxy);
+
+ if (total_size == 0) {
+ g_debug ("OS upgrade %s total size is 0!",
+ gs_app_get_unique_id (app));
+ } else {
+ /* set progress only up to a max percentage, leaving the
+ * remaining for applying the update */
+ progress = (gfloat) downloaded / (gfloat) total_size *
+ (gfloat) max_progress_for_update;
+ }
+ gs_app_set_progress (app, (guint) progress);
+
+ break;
+ }
+ case EOS_UPDATER_STATE_UPDATE_READY: {
+ app_set_state (plugin, app, AS_APP_STATE_UPDATABLE);
+ break;
+ }
+ case EOS_UPDATER_STATE_APPLYING_UPDATE: {
+ /* set as 'installing' because if it is applying the update, we
+ * want to show the progress bar */
+ app_set_state (plugin, app, AS_APP_STATE_INSTALLING);
+
+ /* set up the fake progress to inform the user that something
+ * is still being done (we don't get progress reports from
+ * deploying updates) */
+ if (priv->upgrade_fake_progress_handler != 0)
+ g_source_remove (priv->upgrade_fake_progress_handler);
+ priv->upgrade_fake_progress = 0;
+ priv->upgrade_fake_progress_handler =
+ g_timeout_add ((guint) (1000.0 * upgrade_apply_step_time),
+ (GSourceFunc) fake_os_upgrade_progress_cb,
+ plugin);
+
+ break;
+ }
+ case EOS_UPDATER_STATE_UPDATE_APPLIED: {
+ app_set_state (plugin, app, AS_APP_STATE_UPDATABLE);
+
+ break;
+ }
+ case EOS_UPDATER_STATE_ERROR: {
+ const gchar *error_name;
+ const gchar *error_message;
+
+ error_name = gs_eos_updater_get_error_name (priv->updater_proxy);
+ error_message = gs_eos_updater_get_error_message (priv->updater_proxy);
+
+ /* unless the error is because the user cancelled the upgrade,
+ * we should make sure it gets in the journal */
+ if (!eos_updater_error_is_cancelled (error_name))
+ g_warning ("Got OS upgrade error state with name '%s': %s",
+ error_name, error_message);
+
+ /* We can’t recover the app state since eos-updater needs to
+ * go through the ready → poll → fetch → apply loop again in
+ * order to recover its state. So go back to ‘unknown’. */
+ app_set_state (plugin, app, AS_APP_STATE_UNKNOWN);
+
+ /* Cancelling anything in the updater will result in a
+ * transition to the Error state. Use that as a cue to reset
+ * our #GCancellable ready for next time. */
+ g_cancellable_reset (priv->cancellable);
+
+ break;
+ }
+ default:
+ g_warning ("Encountered unknown eos-updater state: %u", state);
+ break;
+ }
+
+ current_app_state = gs_app_get_state (app);
+
+ g_debug ("%s: Old app state: %s; new app state: %s",
+ G_STRFUNC, as_app_state_to_string (previous_app_state),
+ as_app_state_to_string (current_app_state));
+
+ /* if the state changed from or to 'unknown', we need to notify that a
+ * new update should be shown */
+ if (should_add_os_upgrade (previous_app_state) !=
+ should_add_os_upgrade (current_app_state)) {
+ g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC);
+ gs_plugin_updates_changed (plugin);
+ }
+}
+
+/* This is called in the main thread, so will end up creating an @updater_proxy
+ * which is tied to the main thread’s #GMainContext. */
+gboolean
+gs_plugin_setup (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ g_autoptr(GError) error_local = NULL;
+ g_autofree gchar *name_owner = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(AsIcon) ic = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_debug ("%s", G_STRFUNC);
+
+ g_mutex_init (&priv->mutex);
+ g_cond_init (&priv->state_change_cond);
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ priv->cancellable = g_cancellable_new ();
+ priv->cancelled_id =
+ g_cancellable_connect (priv->cancellable,
+ G_CALLBACK (os_upgrade_cancelled_cb),
+ plugin, NULL);
+
+ /* Check that the proxy exists (and is owned; it should auto-start) so
+ * we can disable the plugin for systems which don’t have eos-updater.
+ * Throughout the rest of the plugin, errors from the daemon
+ * (particularly where it has disappeared off the bus) are ignored, and
+ * the poll/fetch/apply sequence is run through again to recover from
+ * the error. This is the only point in the plugin where we consider an
+ * error from eos-updater to be fatal to the plugin. */
+ priv->updater_proxy = gs_eos_updater_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ "com.endlessm.Updater",
+ "/com/endlessm/Updater",
+ cancellable,
+ error);
+ if (priv->updater_proxy == NULL) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+
+ name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (priv->updater_proxy));
+
+ if (name_owner == NULL) {
+ g_set_error_literal (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "Couldn’t create EOS Updater proxy: couldn’t get name owner");
+ return FALSE;
+ }
+
+ g_signal_connect_object (priv->updater_proxy, "notify::state",
+ G_CALLBACK (updater_state_changed),
+ plugin, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->updater_proxy,
+ "notify::downloaded-bytes",
+ G_CALLBACK (updater_downloaded_bytes_changed),
+ plugin, G_CONNECT_SWAPPED);
+ g_signal_connect_object (priv->updater_proxy, "notify::version",
+ G_CALLBACK (updater_version_changed),
+ plugin, G_CONNECT_SWAPPED);
+
+ /* prepare EOS upgrade app + sync initial state */
+
+ /* use stock icon */
+ ic = as_icon_new ();
+ as_icon_set_kind (ic, AS_ICON_KIND_STOCK);
+ as_icon_set_name (ic, "application-x-addon");
+
+ /* create the OS upgrade */
+ app = gs_app_new ("com.endlessm.EOS.upgrade");
+ gs_app_add_icon (app, ic);
+ gs_app_set_scope (app, AS_APP_SCOPE_SYSTEM);
+ gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE);
+ /* TRANSLATORS: ‘Endless OS’ is a brand name; https://endlessos.com/ */
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST, _("Endless OS"));
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL,
+ /* TRANSLATORS: ‘Endless OS’ is a brand name; https://endlessos.com/ */
+ _("An Endless OS update with new features and fixes."));
+ /* ensure that the version doesn't appear as (NULL) in the banner, it
+ * should be changed to the right value when it changes in the eos-updater */
+ gs_app_set_version (app, "");
+ gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
+ gs_app_add_quirk (app, GS_APP_QUIRK_PROVENANCE);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_REVIEWABLE);
+ gs_app_set_management_plugin (app, gs_plugin_get_name (plugin));
+ gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css",
+ "background: url('" DATADIR "/gnome-software/upgrade-bg.png');"
+ "background-size: 100% 100%;");
+
+ priv->os_upgrade = g_steal_pointer (&app);
+
+ /* sync initial state */
+ sync_state_from_updater_unlocked (plugin);
+
+ return TRUE;
+}
+
+void
+gs_plugin_initialize (GsPlugin *plugin)
+{
+ gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+}
+
+void
+gs_plugin_destroy (GsPlugin *plugin)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ if (priv->upgrade_fake_progress_handler != 0) {
+ g_source_remove (priv->upgrade_fake_progress_handler);
+ priv->upgrade_fake_progress_handler = 0;
+ }
+
+ if (priv->updater_proxy != NULL) {
+ g_signal_handlers_disconnect_by_func (priv->updater_proxy,
+ G_CALLBACK (updater_state_changed),
+ plugin);
+ g_signal_handlers_disconnect_by_func (priv->updater_proxy,
+ G_CALLBACK (updater_downloaded_bytes_changed),
+ plugin);
+ g_signal_handlers_disconnect_by_func (priv->updater_proxy,
+ G_CALLBACK (updater_version_changed),
+ plugin);
+ }
+
+ g_cancellable_cancel (priv->cancellable);
+ if (priv->cancellable != NULL && priv->cancelled_id != 0)
+ g_cancellable_disconnect (priv->cancellable, priv->cancelled_id);
+ g_clear_object (&priv->cancellable);
+
+ g_clear_object (&priv->updater_proxy);
+
+ g_clear_object (&priv->os_upgrade);
+
+ g_cond_clear (&priv->state_change_cond);
+ g_mutex_clear (&priv->mutex);
+}
+
+/* Called in a #GTask worker thread, but it can run without holding
+ * `priv->mutex` since it doesn’t need to synchronise on state. */
+gboolean
+gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ EosUpdaterState updater_state;
+ gboolean success;
+
+ /* We let the eos-updater daemon do its own caching, so ignore the
+ * @cache_age, unless it’s %G_MAXUINT, which signifies startup of g-s.
+ * In that case, it’s probably just going to load the system too much to
+ * do an update check now. We can wait. */
+ g_debug ("%s: cache_age: %u", G_STRFUNC, cache_age);
+
+ if (cache_age == G_MAXUINT)
+ return TRUE;
+
+ /* check if the OS upgrade has been disabled */
+ if (priv->updater_proxy == NULL) {
+ g_debug ("%s: Updater disabled", G_STRFUNC);
+ return TRUE;
+ }
+
+ /* poll in the error/none/ready states to check if there's an
+ * update available */
+ updater_state = gs_eos_updater_get_state (priv->updater_proxy);
+ switch (updater_state) {
+ case EOS_UPDATER_STATE_ERROR:
+ case EOS_UPDATER_STATE_NONE:
+ case EOS_UPDATER_STATE_READY:
+ /* This sync call will block the job thread, which is OK. */
+ success = gs_eos_updater_call_poll_sync (priv->updater_proxy,
+ cancellable, error);
+ gs_eos_updater_error_convert (error);
+ return success;
+ default:
+ g_debug ("%s: Updater in state %s; not polling",
+ G_STRFUNC, eos_updater_state_to_str (updater_state));
+ return TRUE;
+ }
+}
+
+/* Called in a #GTask worker thread, but it can run without holding
+ * `priv->mutex` since it doesn’t need to synchronise on state. */
+gboolean
+gs_plugin_add_distro_upgrades (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ g_debug ("%s", G_STRFUNC);
+
+ /* if we are testing the plugin, then always add the OS upgrade */
+ if (g_getenv ("GS_PLUGIN_EOS_TEST") != NULL) {
+ gs_app_set_state (priv->os_upgrade, AS_APP_STATE_AVAILABLE);
+ gs_app_list_add (list, priv->os_upgrade);
+ return TRUE;
+ }
+
+ /* check if the OS upgrade has been disabled */
+ if (priv->updater_proxy == NULL) {
+ g_debug ("%s: Updater disabled", G_STRFUNC);
+ return TRUE;
+ }
+
+ if (should_add_os_upgrade (gs_app_get_state (priv->os_upgrade))) {
+ g_debug ("Adding EOS upgrade: %s",
+ gs_app_get_unique_id (priv->os_upgrade));
+ gs_app_list_add (list, priv->os_upgrade);
+ } else {
+ g_debug ("Not adding EOS upgrade");
+ }
+
+ return TRUE;
+}
+
+/* Must be called with priv->mutex already locked. */
+static gboolean
+wait_for_state_change_unlocked (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ EosUpdaterState old_state, new_state;
+
+ old_state = new_state = gs_eos_updater_get_state (priv->updater_proxy);
+ g_debug ("%s: Old state ‘%s’", G_STRFUNC, eos_updater_state_to_str (old_state));
+
+ while (new_state == old_state &&
+ !g_cancellable_is_cancelled (cancellable)) {
+ g_cond_wait (&priv->state_change_cond, &priv->mutex);
+ new_state = gs_eos_updater_get_state (priv->updater_proxy);
+ }
+
+ if (!g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ g_debug ("%s: New state ‘%s’", G_STRFUNC, eos_updater_state_to_str (new_state));
+ return TRUE;
+ } else {
+ g_debug ("%s: Cancelled", G_STRFUNC);
+ return FALSE;
+ }
+}
+
+/* Could be executed in any thread. No need to hold `priv->mutex` since we don’t
+ * access anything which is not thread-safe. */
+static void
+cancelled_cb (GCancellable *ui_cancellable,
+ gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+
+ /* Chain cancellation. */
+ g_debug ("Propagating OS download cancellation from %p to %p",
+ ui_cancellable, priv->cancellable);
+ g_cancellable_cancel (priv->cancellable);
+
+ /* And wake up anything blocking on a state change. */
+ g_cond_broadcast (&priv->state_change_cond);
+}
+
+/* Called in a #GTask worker thread, and it needs to hold `priv->mutex` due to
+ * synchronising on state with the main thread. */
+gboolean
+gs_plugin_app_upgrade_download (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginData *priv = gs_plugin_get_data (plugin);
+ gulong cancelled_id = 0;
+ EosUpdaterState state;
+ gboolean done, allow_restart;
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ /* only process this app if was created by this plugin */
+ if (g_strcmp0 (gs_app_get_management_plugin (app),
+ gs_plugin_get_name (plugin)) != 0)
+ return TRUE;
+
+ /* if the OS upgrade has been disabled */
+ if (priv->updater_proxy == NULL) {
+ g_set_error (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
+ "The OS upgrade has been disabled in the EOS plugin");
+ return FALSE;
+ }
+
+ g_assert (app == priv->os_upgrade);
+
+ /* Set up cancellation. */
+ g_debug ("Chaining cancellation from %p to %p", cancellable, priv->cancellable);
+ if (cancellable != NULL) {
+ cancelled_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (cancelled_cb),
+ plugin, NULL);
+ }
+
+ /* Step through the state machine until we are finished downloading and
+ * applying the update, or until an error occurs. All of the D-Bus calls
+ * here will block until the method call is complete. */
+ state = gs_eos_updater_get_state (priv->updater_proxy);
+
+ done = FALSE;
+ allow_restart = (state == EOS_UPDATER_STATE_NONE ||
+ state == EOS_UPDATER_STATE_READY ||
+ state == EOS_UPDATER_STATE_ERROR);
+
+ while (!done && !g_cancellable_is_cancelled (cancellable)) {
+ state = gs_eos_updater_get_state (priv->updater_proxy);
+ g_debug ("%s: State ‘%s’", G_STRFUNC, eos_updater_state_to_str (state));
+
+ switch (state) {
+ case EOS_UPDATER_STATE_NONE:
+ case EOS_UPDATER_STATE_READY: {
+ /* Poll for an update. This typically only happens if
+ * we’ve drifted out of sync with the updater process
+ * due to it dying. In that case, only restart once
+ * before giving up, so we don’t end up in an endless
+ * loop (say, if eos-updater always died 50% of the way
+ * through a download). */
+ if (allow_restart) {
+ allow_restart = FALSE;
+ g_debug ("Restarting OS upgrade from none/ready state");
+ if (!gs_eos_updater_call_poll_sync (priv->updater_proxy,
+ cancellable, error)) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+ } else {
+ /* Display an error to the user. */
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
+ g_set_error_literal (&error_local, GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ _("EOS update service could not fetch and apply the update."));
+ gs_eos_updater_error_convert (&error_local);
+ gs_plugin_event_set_app (event, app);
+ gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD);
+ gs_plugin_event_set_error (event, error_local);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+
+ /* Error out. */
+ done = TRUE;
+ }
+
+ break;
+ } case EOS_UPDATER_STATE_POLLING: {
+ /* Nothing to do here. */
+ break;
+ } case EOS_UPDATER_STATE_UPDATE_AVAILABLE: {
+ g_auto(GVariantDict) options_dict = G_VARIANT_DICT_INIT (NULL);
+
+ /* when the OS upgrade was started by the user and the
+ * updater reports an available update, (meaning we were
+ * polling before), we should readily call fetch */
+ g_variant_dict_insert (&options_dict, "force", "b", TRUE);
+
+ if (!gs_eos_updater_call_fetch_full_sync (priv->updater_proxy,
+ g_variant_dict_end (&options_dict),
+ cancellable, error)) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+
+ break;
+ }
+ case EOS_UPDATER_STATE_FETCHING: {
+ /* Nothing to do here. */
+ break;
+ }
+ case EOS_UPDATER_STATE_UPDATE_READY: {
+ /* if there's an update ready to deployed, and it was started by
+ * the user, we should proceed to applying the upgrade */
+ gs_app_set_progress (app, max_progress_for_update);
+
+ if (!gs_eos_updater_call_apply_sync (priv->updater_proxy,
+ cancellable, error)) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+
+ break;
+ }
+ case EOS_UPDATER_STATE_APPLYING_UPDATE: {
+ /* Nothing to do here. */
+ break;
+ }
+ case EOS_UPDATER_STATE_UPDATE_APPLIED: {
+ /* Done! */
+ done = TRUE;
+ break;
+ }
+ case EOS_UPDATER_STATE_ERROR: {
+ const gchar *error_name;
+ const gchar *error_message;
+ g_autoptr(GError) error_local = NULL;
+
+ error_name = gs_eos_updater_get_error_name (priv->updater_proxy);
+ error_message = gs_eos_updater_get_error_message (priv->updater_proxy);
+ error_local = g_dbus_error_new_for_dbus_error (error_name, error_message);
+
+ /* Display an error to the user, unless they cancelled
+ * the download. */
+ if (!eos_updater_error_is_cancelled (error_name)) {
+ g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
+ gs_eos_updater_error_convert (&error_local);
+ gs_plugin_event_set_app (event, app);
+ gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD);
+ gs_plugin_event_set_error (event, error_local);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ gs_plugin_report_event (plugin, event);
+ }
+
+ /* Unconditionally call Poll() to get the updater out
+ * of the error state and to allow the update to be
+ * displayed in the UI again and retried. Exit the
+ * state change loop immediately, though, to prevent
+ * possible endless loops between the Poll/Error
+ * states. */
+ allow_restart = FALSE;
+ g_debug ("Restarting OS upgrade on error");
+ if (!gs_eos_updater_call_poll_sync (priv->updater_proxy,
+ cancellable, error)) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+
+ /* Error out. */
+ done = TRUE;
+
+ break;
+ }
+ default:
+ g_warning ("Encountered unknown eos-updater state: %u", state);
+ break;
+ }
+
+ /* Block on the next state change. */
+ if (!done &&
+ !wait_for_state_change_unlocked (plugin, cancellable, error)) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+ }
+
+ if (cancellable != NULL && cancelled_id != 0) {
+ g_debug ("Disconnecting cancellable %p", cancellable);
+ g_cancellable_disconnect (cancellable, cancelled_id);
+ }
+
+ /* Process the final state. */
+ if (gs_eos_updater_get_state (priv->updater_proxy) == EOS_UPDATER_STATE_ERROR) {
+ const gchar *error_name;
+ const gchar *error_message;
+ g_autoptr(GError) error_local = NULL;
+
+ error_name = gs_eos_updater_get_error_name (priv->updater_proxy);
+ error_message = gs_eos_updater_get_error_message (priv->updater_proxy);
+ error_local = g_dbus_error_new_for_dbus_error (error_name, error_message);
+ gs_eos_updater_error_convert (&error_local);
+ g_propagate_error (error, g_steal_pointer (&error_local));
+
+ return FALSE;
+ } else if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_eos_updater_error_convert (error);
+ return FALSE;
+ }
+
+ return TRUE;
+}