1
0
Fork 0
gnome-software/plugins/eos-updater/gs-plugin-eos-updater.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

1627 lines
58 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) 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"
#include "gs-plugin-eos-updater.h"
#include "gs-plugin-private.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 updates 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 update or upgrade in the UI.
*
* The #GsApp instance is returned by
* `gs_plugin_eos_updater_list_distro_upgrades_async()` or
* `gs_plugin_eos_updater_list_apps_async()` depending on whether it contains significant
* user visible changes, as determined by the `update-is-user-visible` property
* on the proxy. (This in turn is set from information on the release OSTree
* commit.) The #GsApp will be returned by at most one of these vfuncs.
*
* Calling gs_plugin_eos_updater_refresh_metadata_async() will result in this
* plugin calling the `Poll()` method on the `eos-updater` daemon to check for a
* new update.
*
* Calling GsPluginClass::download_upgrade_async() will result in this plugin calling
* a sequence of methods on the `eos-updater` daemon to check for, download and
* apply an update. Typically, GsPluginClass::download_upgrade_async() 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 cant get reasonable progress data out of OSTree.
*
* The proxy object (`updater_proxy`) uses the thread-default main context from
* the gs_plugin_eos_updater_setup() function, which is currently the global default main
* context from gnome-softwares main thread. This means all the signal
* callbacks from the proxy will be executed in the main thread, and *must not
* block*.
*
* Asynchronous plugin vfuncs (such as
* gs_plugin_eos_updater_refresh_metadata_async()) are run in gnome-softwares
* main thread and *must not block*. As they all call D-Bus methods, the work
* they do is minimal and hence is OK to happen in the main thread.
*
* The other functions 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.
*
* `updater_proxy`, `os_upgrade` and `cancellable` are only set in
* gs_plugin_eos_updater_setup(), and are both internally thread-safe — so they can both be
* dereferenced and have their methods called from any thread without any
* locking.
*
* 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 ("Cant 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 (GsPluginEosUpdater *self);
struct _GsPluginEosUpdater
{
GsPlugin parent;
/* These members are only set once in gs_plugin_eos_updater_setup(), and are
* internally thread-safe, so can be accessed without any locking. */
GsEosUpdater *updater_proxy; /* (owned) */
GsApp *os_upgrade; /* (owned); represents both large upgrades and small updates */
GCancellable *cancellable; /* (owned) */
gulong cancelled_id;
/* These members must only ever be accessed from the main thread, so
* can be accessed without any locking. */
gfloat upgrade_fake_progress;
guint upgrade_fake_progress_handler;
};
G_DEFINE_TYPE (GsPluginEosUpdater, gs_plugin_eos_updater, GS_TYPE_PLUGIN)
static void
os_upgrade_cancelled_cb (GCancellable *cancellable,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (user_data);
g_debug ("%s: Cancelling upgrade", G_STRFUNC);
gs_eos_updater_call_cancel (self->updater_proxy,
/* never interactive */
G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
NULL, NULL, NULL);
}
static gboolean
should_add_os_update_or_upgrade (GsAppState state)
{
switch (state) {
case GS_APP_STATE_AVAILABLE:
case GS_APP_STATE_AVAILABLE_LOCAL:
case GS_APP_STATE_UPDATABLE:
case GS_APP_STATE_QUEUED_FOR_INSTALL:
case GS_APP_STATE_INSTALLING:
case GS_APP_STATE_DOWNLOADING:
case GS_APP_STATE_UPDATABLE_LIVE:
return TRUE;
case GS_APP_STATE_UNKNOWN:
case GS_APP_STATE_INSTALLED:
case GS_APP_STATE_UNAVAILABLE:
case GS_APP_STATE_REMOVING:
default:
return FALSE;
}
}
static gboolean
should_add_os_upgrade (GsPluginEosUpdater *self)
{
return (should_add_os_update_or_upgrade (gs_app_get_state (self->os_upgrade)) &&
gs_app_has_quirk (self->os_upgrade, GS_APP_QUIRK_IS_PROXY));
}
static gboolean
should_add_os_update (GsPluginEosUpdater *self)
{
return (should_add_os_update_or_upgrade (gs_app_get_state (self->os_upgrade)) &&
!gs_app_has_quirk (self->os_upgrade, GS_APP_QUIRK_IS_PROXY));
}
/* 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,
GsAppState new_state)
{
GsAppState old_state = gs_app_get_state (app);
if (new_state == old_state)
return;
gs_app_set_state (app, new_state);
if (should_add_os_update_or_upgrade (old_state) !=
should_add_os_update_or_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);
}
static void
app_set_update_is_user_visible (GsApp *app,
gboolean update_is_user_visible)
{
g_debug ("%s: Setting OS update as %s", G_STRFUNC,
update_is_user_visible ? "containing significant user visible changes" : "only containing non-user visible changes");
/* If the update contains significant user visible changes, we want to
* show it using the OS upgrade banner (#GsUpgradeBanner), which means
* it needs a certain set of metadata set on it.
*
* If it doesnt contain significant user visible changes, we want to
* show it as a normal update row (a row in a #GsUpdatesSection), which
* means it needs different metadata.
*
* Other parts of the code in this plugin use the presence of
* #GS_APP_QUIRK_IS_PROXY to distinguish these two states. */
if (update_is_user_visible) {
gs_app_add_quirk (app, GS_APP_QUIRK_IS_PROXY);
gs_app_set_special_kind (app, GS_APP_SPECIAL_KIND_OS_UPDATE);
} else {
gs_app_remove_quirk (app, GS_APP_QUIRK_IS_PROXY);
gs_app_set_special_kind (app, GS_APP_SPECIAL_KIND_NONE);
}
}
/* This will be invoked in the main thread. */
static void
updater_state_changed (GsPluginEosUpdater *self)
{
g_debug ("%s", G_STRFUNC);
sync_state_from_updater (self);
}
/* This will be invoked in the main thread. */
static void
updater_downloaded_bytes_changed (GsPluginEosUpdater *self)
{
sync_state_from_updater (self);
}
/* This will be invoked in the main thread. */
static void
updater_version_changed (GsPluginEosUpdater *self)
{
const gchar *version = gs_eos_updater_get_version (self->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 (self->os_upgrade, version);
}
/* This will be invoked in the main thread. */
static void
updater_update_is_user_visible_changed (GsPluginEosUpdater *self)
{
gboolean update_is_user_visible = gs_eos_updater_get_update_is_user_visible (self->updater_proxy);
app_set_update_is_user_visible (self->os_upgrade, update_is_user_visible);
}
/* This will be invoked in the main thread. */
static void
updater_release_notes_uri_changed (GsPluginEosUpdater *self)
{
const gchar *release_notes_uri = gs_eos_updater_get_release_notes_uri (self->updater_proxy);
/* @release_notes_uri may be the empty string, in which case we want to remove the URL */
if (release_notes_uri != NULL && *release_notes_uri == '\0')
release_notes_uri = NULL;
gs_app_set_url (self->os_upgrade, AS_URL_KIND_HOMEPAGE, release_notes_uri);
}
/* This will be invoked in the main thread. */
static gboolean
fake_os_upgrade_progress_cb (gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (user_data);
gfloat normal_step;
guint new_progress;
const gfloat fake_progress_max = 99.0;
if (gs_eos_updater_get_state (self->updater_proxy) != EOS_UPDATER_STATE_APPLYING_UPDATE ||
self->upgrade_fake_progress > fake_progress_max) {
self->upgrade_fake_progress = 0;
self->upgrade_fake_progress_handler = 0;
return G_SOURCE_REMOVE;
}
normal_step = (gfloat) upgrade_apply_progress_range /
(upgrade_apply_max_time / upgrade_apply_step_time);
self->upgrade_fake_progress += normal_step;
new_progress = max_progress_for_update +
(guint) round (self->upgrade_fake_progress);
gs_app_set_progress (self->os_upgrade,
MIN (new_progress, (guint) fake_progress_max));
g_debug ("OS upgrade fake progress: %f", self->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. */
static void
sync_state_from_updater (GsPluginEosUpdater *self)
{
GsPlugin *plugin = GS_PLUGIN (self);
GsApp *app = self->os_upgrade;
EosUpdaterState state;
GsAppState previous_app_state = gs_app_get_state (app);
GsAppState current_app_state;
/* in case the OS upgrade has been disabled */
if (self->updater_proxy == NULL) {
g_debug ("%s: Updater disabled", G_STRFUNC);
return;
}
state = gs_eos_updater_get_state (self->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, GS_APP_STATE_UNKNOWN);
break;
} case EOS_UPDATER_STATE_POLLING: {
/* Nothing to do here. */
break;
} case EOS_UPDATER_STATE_UPDATE_AVAILABLE: {
gint64 total_size;
app_set_update_is_user_visible (app, gs_eos_updater_get_update_is_user_visible (self->updater_proxy));
app_set_state (plugin, app, GS_APP_STATE_AVAILABLE);
/* The property returns -1 to indicate unknown size */
total_size = gs_eos_updater_get_download_size (self->updater_proxy);
if (total_size >= 0)
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, total_size);
else
gs_app_set_size_download (app, GS_SIZE_TYPE_UNKNOWN, 0);
break;
}
case EOS_UPDATER_STATE_FETCHING: {
gint64 total_size = 0;
gint64 downloaded = 0;
guint progress = 0;
/* FIXME: Set to QUEUED_FOR_INSTALL if were waiting for metered
* data permission. */
app_set_state (plugin, app, GS_APP_STATE_DOWNLOADING);
downloaded = gs_eos_updater_get_downloaded_bytes (self->updater_proxy);
total_size = gs_eos_updater_get_download_size (self->updater_proxy);
if (total_size == 0) {
g_debug ("OS upgrade %s total size is 0!",
gs_app_get_unique_id (app));
progress = GS_APP_PROGRESS_UNKNOWN;
} else if (downloaded < 0 || total_size < 0) {
/* Both properties return -1 to indicate unknown */
progress = GS_APP_PROGRESS_UNKNOWN;
} 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, progress);
break;
}
case EOS_UPDATER_STATE_UPDATE_READY: {
app_set_state (plugin, app, GS_APP_STATE_UPDATABLE);
/* Nothing further to download. */
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
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, GS_APP_STATE_INSTALLING);
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
/* 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 (self->upgrade_fake_progress_handler != 0)
g_source_remove (self->upgrade_fake_progress_handler);
self->upgrade_fake_progress = 0;
self->upgrade_fake_progress_handler =
g_timeout_add ((guint) (1000.0 * upgrade_apply_step_time),
(GSourceFunc) fake_os_upgrade_progress_cb,
self);
break;
}
case EOS_UPDATER_STATE_UPDATE_APPLIED: {
app_set_state (plugin, app, GS_APP_STATE_UPDATABLE);
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
break;
}
case EOS_UPDATER_STATE_ERROR: {
const gchar *error_name;
const gchar *error_message;
error_name = gs_eos_updater_get_error_name (self->updater_proxy);
error_message = gs_eos_updater_get_error_message (self->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 cant 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, GS_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 (self->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, gs_app_state_to_string (previous_app_state),
gs_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_update_or_upgrade (previous_app_state) !=
should_add_os_update_or_upgrade (current_app_state)) {
g_debug ("%s: Calling gs_plugin_updates_changed()", G_STRFUNC);
gs_plugin_updates_changed (plugin);
}
}
static void proxy_new_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
/* This is called in the main thread, so will end up creating an @updater_proxy
* which is tied to the main threads #GMainContext. */
static void
gs_plugin_eos_updater_setup_async (GsPlugin *plugin,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin);
g_autoptr(GTask) task = NULL;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_setup_async);
g_debug ("%s", G_STRFUNC);
self->cancellable = g_cancellable_new ();
self->cancelled_id =
g_cancellable_connect (self->cancellable,
G_CALLBACK (os_upgrade_cancelled_cb),
self, NULL);
/* Check that the proxy exists (and is owned; it should auto-start) so
* we can disable the plugin for systems which dont 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. */
gs_eos_updater_proxy_new (gs_plugin_get_system_bus_connection (plugin),
G_DBUS_PROXY_FLAGS_NONE,
"com.endlessm.Updater",
"/com/endlessm/Updater",
cancellable,
proxy_new_cb,
g_steal_pointer (&task));
}
static void
proxy_new_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr(GTask) task = g_steal_pointer (&user_data);
GsPluginEosUpdater *self = g_task_get_source_object (task);
g_autofree gchar *name_owner = NULL;
g_autoptr(GsApp) app = NULL;
g_autoptr(GIcon) ic = NULL;
g_autofree gchar *background_filename = NULL;
g_autofree gchar *css = NULL;
g_autofree gchar *summary = NULL;
g_autofree gchar *version = NULL;
gboolean update_is_user_visible = FALSE;
g_autoptr(GsOsRelease) os_release = NULL;
g_autoptr(GError) local_error = NULL;
const gchar *os_name, *os_logo;
self->updater_proxy = gs_eos_updater_proxy_new_finish (result, &local_error);
if (self->updater_proxy == NULL) {
gs_eos_updater_error_convert (&local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->updater_proxy));
if (name_owner == NULL) {
g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
"Couldnt create EOS Updater proxy: couldnt get name owner");
return;
}
g_signal_connect_object (self->updater_proxy, "notify::state",
G_CALLBACK (updater_state_changed),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->updater_proxy,
"notify::downloaded-bytes",
G_CALLBACK (updater_downloaded_bytes_changed),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->updater_proxy, "notify::version",
G_CALLBACK (updater_version_changed),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->updater_proxy, "notify::update-is-user-visible",
G_CALLBACK (updater_update_is_user_visible_changed),
self, G_CONNECT_SWAPPED);
g_signal_connect_object (self->updater_proxy, "notify::release-notes-uri",
G_CALLBACK (updater_release_notes_uri_changed),
self, G_CONNECT_SWAPPED);
/* prepare EOS upgrade app + sync initial state */
/* Check for a background image in the standard location. */
background_filename = gs_utils_get_upgrade_background (NULL);
if (background_filename != NULL)
css = g_strconcat ("background: url('file://", background_filename, "');"
"background-size: 100% 100%;", NULL);
os_release = gs_os_release_new (&local_error);
if (local_error) {
g_warning ("Failed to get OS release information: %s", local_error->message);
/* Just a fallback, do not localize */
os_name = "Endless OS";
os_logo = NULL;
g_clear_error (&local_error);
} else {
os_name = gs_os_release_get_name (os_release);
os_logo = gs_os_release_get_logo (os_release);
}
g_object_get (G_OBJECT (self->updater_proxy),
"version", &version,
"update-is-user-visible", &update_is_user_visible,
"update-message", &summary,
NULL);
if (summary == NULL || *summary == '\0') {
g_clear_pointer (&summary, g_free);
g_object_get (G_OBJECT (self->updater_proxy),
"update-label", &summary,
NULL);
}
if (summary == NULL || *summary == '\0') {
g_clear_pointer (&summary, g_free);
/* Translators: The '%s' is replaced with the OS name, like "Endless OS" */
summary = g_strdup_printf (_("%s update with new features and fixes."), os_name);
}
/* use stock icon */
ic = g_themed_icon_new ((os_logo != NULL) ? os_logo : "system-component-os-updates");
/* create the OS upgrade */
app = gs_app_new ("com.endlessm.EOS.upgrade");
gs_app_add_icon (app, ic);
gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM);
gs_app_set_kind (app, AS_COMPONENT_KIND_OPERATING_SYSTEM);
gs_app_set_name (app, GS_APP_QUALITY_LOWEST, os_name);
gs_app_set_summary (app, GS_APP_QUALITY_NORMAL, summary);
gs_app_set_version (app, version == NULL ? "" : version);
app_set_update_is_user_visible (app, update_is_user_visible);
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 (self));
gs_app_set_metadata (app, "GnomeSoftware::UpgradeBanner-css", css);
self->os_upgrade = g_steal_pointer (&app);
/* sync initial state */
sync_state_from_updater (self);
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_eos_updater_setup_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void
gs_plugin_eos_updater_init (GsPluginEosUpdater *self)
{
}
static void
gs_plugin_eos_updater_dispose (GObject *object)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (object);
if (self->upgrade_fake_progress_handler != 0) {
g_source_remove (self->upgrade_fake_progress_handler);
self->upgrade_fake_progress_handler = 0;
}
if (self->updater_proxy != NULL) {
g_signal_handlers_disconnect_by_func (self->updater_proxy,
G_CALLBACK (updater_state_changed),
self);
g_signal_handlers_disconnect_by_func (self->updater_proxy,
G_CALLBACK (updater_downloaded_bytes_changed),
self);
g_signal_handlers_disconnect_by_func (self->updater_proxy,
G_CALLBACK (updater_version_changed),
self);
g_signal_handlers_disconnect_by_func (self->updater_proxy,
G_CALLBACK (updater_update_is_user_visible_changed),
self);
g_signal_handlers_disconnect_by_func (self->updater_proxy,
G_CALLBACK (updater_release_notes_uri_changed),
self);
}
g_cancellable_cancel (self->cancellable);
if (self->cancellable != NULL && self->cancelled_id != 0)
g_cancellable_disconnect (self->cancellable, self->cancelled_id);
g_clear_object (&self->cancellable);
g_clear_object (&self->updater_proxy);
g_clear_object (&self->os_upgrade);
G_OBJECT_CLASS (gs_plugin_eos_updater_parent_class)->dispose (object);
}
static void poll_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
/* Called in the main thread. */
static void
gs_plugin_eos_updater_refresh_metadata_async (GsPlugin *plugin,
guint64 cache_age_secs,
GsPluginRefreshMetadataFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin);
EosUpdaterState updater_state;
g_autoptr(GTask) task = NULL;
gboolean interactive = flags & GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_refresh_metadata_async);
/* We let the eos-updater daemon do its own caching, so ignore the
* @cache_age_secs, unless its %G_MAXUINT64, which signifies startup of g-s.
* In that case, its probably just going to load the system too much to
* do an update check now. We can wait. */
g_debug ("%s: cache_age_secs: %" G_GUINT64_FORMAT, G_STRFUNC, cache_age_secs);
if (cache_age_secs == G_MAXUINT64) {
g_task_return_boolean (task, TRUE);
return;
}
/* check if the OS upgrade has been disabled */
if (self->updater_proxy == NULL) {
g_debug ("%s: Updater disabled", G_STRFUNC);
g_task_return_boolean (task, TRUE);
return;
}
/* poll in the error/none/ready states to check if there's an
* update available */
updater_state = gs_eos_updater_get_state (self->updater_proxy);
switch (updater_state) {
case EOS_UPDATER_STATE_ERROR:
case EOS_UPDATER_STATE_NONE:
case EOS_UPDATER_STATE_READY:
gs_eos_updater_call_poll (self->updater_proxy,
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
poll_cb,
g_steal_pointer (&task));
return;
default:
g_debug ("%s: Updater in state %s; not polling",
G_STRFUNC, eos_updater_state_to_str (updater_state));
g_task_return_boolean (task, TRUE);
return;
}
}
static void
poll_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GsEosUpdater *updater_proxy = GS_EOS_UPDATER (source_object);
g_autoptr(GTask) task = g_steal_pointer (&user_data);
g_autoptr(GError) local_error = NULL;
if (!gs_eos_updater_call_poll_finish (updater_proxy, result, &local_error)) {
gs_eos_updater_error_convert (&local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
} else {
g_task_return_boolean (task, TRUE);
}
}
static gboolean
gs_plugin_eos_updater_refresh_metadata_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
/* Called in the main thread. */
static void
gs_plugin_eos_updater_list_distro_upgrades_async (GsPlugin *plugin,
GsPluginListDistroUpgradesFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin);
g_autoptr(GTask) task = NULL;
g_autoptr(GsAppList) list = gs_app_list_new ();
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_list_distro_upgrades_async);
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 (self->os_upgrade, GS_APP_STATE_AVAILABLE);
gs_app_list_add (list, self->os_upgrade);
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
return;
}
/* check if the OS upgrade has been disabled */
if (self->updater_proxy == NULL) {
g_debug ("%s: Updater disabled", G_STRFUNC);
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
return;
}
if (should_add_os_upgrade (self)) {
g_debug ("Adding EOS upgrade as user visible OS upgrade: %s",
gs_app_get_unique_id (self->os_upgrade));
gs_app_list_add (list, self->os_upgrade);
} else {
g_debug ("Not adding EOS upgrade as user visible OS upgrade");
}
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
}
static GsAppList *
gs_plugin_eos_updater_list_distro_upgrades_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_pointer (G_TASK (result), error);
}
static void
gs_plugin_eos_updater_list_apps_async (GsPlugin *plugin,
GsAppQuery *query,
GsPluginListAppsFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin);
g_autoptr(GTask) task = NULL;
GsAppQueryTristate is_for_update = GS_APP_QUERY_TRISTATE_UNSET;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_list_apps_async);
g_debug ("%s", G_STRFUNC);
/* check if the OS upgrade has been disabled */
if (self->updater_proxy == NULL) {
g_debug ("%s: Updater disabled", G_STRFUNC);
g_task_return_boolean (task, TRUE);
return;
}
if (query != NULL)
is_for_update = gs_app_query_get_is_for_update (query);
/* Currently only support a subset of query properties, and only one set at once. */
if (is_for_update == GS_APP_QUERY_TRISTATE_UNSET ||
is_for_update == GS_APP_QUERY_TRISTATE_FALSE ||
gs_app_query_get_n_properties_set (query) != 1) {
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
"Unsupported query");
return;
}
if (is_for_update == GS_APP_QUERY_TRISTATE_TRUE) {
g_autoptr(GsAppList) list = gs_app_list_new ();
if (should_add_os_update (self)) {
g_debug ("Adding EOS upgrade as non-user visible OS update: %s",
gs_app_get_unique_id (self->os_upgrade));
gs_app_list_add (list, self->os_upgrade);
} else {
g_debug ("Not adding EOS upgrade as non-user visible OS update");
}
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
} else {
g_assert_not_reached ();
}
}
static GsAppList *
gs_plugin_eos_updater_list_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_pointer (G_TASK (result), error);
}
typedef struct {
EosUpdaterState old_state;
gulong notify_id;
gulong cancelled_id;
guint idle_id;
} WaitForStateChangeData;
static void
wait_for_state_change_data_free (WaitForStateChangeData *data)
{
/* These two should have been cleared already */
g_assert (data->notify_id == 0);
g_assert (data->cancelled_id == 0);
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (WaitForStateChangeData, wait_for_state_change_data_free);
static void wait_for_state_change_cb (GTask *task);
static void wait_for_state_change_notify_cb (GObject *object,
GParamSpec *pspec,
gpointer user_data);
static void wait_for_state_change_cancelled_cb (GCancellable *cancellable,
gpointer user_data);
static void
object_unref_closure (gpointer data,
GClosure *closure)
{
GObject *obj = G_OBJECT (data);
g_object_unref (obj);
}
static void
wait_for_state_change_async (GsEosUpdater *updater_proxy,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(GTask) task = NULL;
g_autoptr(WaitForStateChangeData) data_owned = NULL;
WaitForStateChangeData *data;
task = g_task_new (updater_proxy, cancellable, callback, user_data);
g_task_set_source_tag (task, wait_for_state_change_async);
/* Store the initial state to compare against later. */
data = data_owned = g_new0 (WaitForStateChangeData, 1);
data->old_state = gs_eos_updater_get_state (updater_proxy);
g_debug ("%s: Old state %s", G_STRFUNC, eos_updater_state_to_str (data->old_state));
g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) wait_for_state_change_data_free);
/* Listen for state changes. Connect late in the emission process so
* that the callback is invoked after the main updater_state_changed(),
* because that updates a load of internal state which the function
* calling this one might need. */
data->notify_id = g_signal_connect_data (updater_proxy, "notify::state",
G_CALLBACK (wait_for_state_change_notify_cb),
g_object_ref (task), object_unref_closure,
G_CONNECT_AFTER);
data->cancelled_id = g_cancellable_connect (cancellable,
G_CALLBACK (wait_for_state_change_cancelled_cb),
g_object_ref (task), g_object_unref);
}
static void
wait_for_state_change_cb (GTask *task_unowned)
{
g_autoptr(GTask) task = g_object_ref (task_unowned);
WaitForStateChangeData *data = g_task_get_task_data (task);
GCancellable *cancellable = g_task_get_cancellable (task);
GsEosUpdater *updater_proxy = g_task_get_source_object (task);
EosUpdaterState old_state, new_state;
old_state = GPOINTER_TO_INT (g_task_get_task_data (task));
new_state = gs_eos_updater_get_state (updater_proxy);
if (new_state == old_state &&
!g_cancellable_is_cancelled (cancellable))
return;
/* State has changed, or the wait has been cancelled. Disconnect, and
* return. */
g_clear_signal_handler (&data->notify_id, updater_proxy);
g_cancellable_disconnect (cancellable, data->cancelled_id);
data->cancelled_id = 0;
if (data->idle_id != 0) {
g_source_remove (data->idle_id);
data->idle_id = 0;
}
if (g_task_return_error_if_cancelled (task)) {
g_debug ("%s: Cancelled", G_STRFUNC);
} else {
g_debug ("%s: New state %s", G_STRFUNC, eos_updater_state_to_str (new_state));
g_task_return_boolean (task, TRUE);
}
}
static void
wait_for_state_change_notify_cb (GObject *object,
GParamSpec *pspec,
gpointer user_data)
{
GTask *task = G_TASK (user_data);
wait_for_state_change_cb (task);
}
static gboolean
wait_for_state_change_idle_cb (gpointer user_data)
{
g_autoptr(GTask) task = G_TASK (user_data);
WaitForStateChangeData *data = g_task_get_task_data (task);
/* it can be zeroed when the "state change" had been called meanwhile;
in that case the task is finished already */
if (data->idle_id != 0) {
data->idle_id = 0;
wait_for_state_change_cb (task);
}
return G_SOURCE_REMOVE;
}
static void
wait_for_state_change_cancelled_cb (GCancellable *cancellable,
gpointer user_data)
{
GTask *task = G_TASK (user_data);
WaitForStateChangeData *data = g_task_get_task_data (task);
if (data->idle_id != 0)
return;
/* cannot call wait_for_state_change_cb() from the GCancellable::cancelled signal,
because it calls g_cancellable_disconnect(), which leads to deadlock, thus
postpone this to an idle callback */
data->idle_id = g_idle_add_full (G_PRIORITY_HIGH_IDLE,
wait_for_state_change_idle_cb,
g_object_ref (task),
g_object_unref);
}
static gboolean
wait_for_state_change_finish (GsEosUpdater *updater_proxy,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
/* Could be executed in any thread. No need to hold a lock since we dont
* access anything which is not thread-safe. */
static void
cancelled_cb (GCancellable *ui_cancellable,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (user_data);
/* Chain cancellation. */
g_debug ("Propagating OS download cancellation from %p to %p",
ui_cancellable, self->cancellable);
g_cancellable_cancel (self->cancellable);
}
/* State tracking for a single call to a D-Bus method on the updater proxy.
*
* This is designed so that multiple different async calls can all end up back
* in download_iterate_state_machine_cb(), which then advances the updaters
* state machine.
*
* Given that different async calls have different `*_finish()` functions, a
* pointer to the finish function is needed: `finish_func()`. This is called
* to get the results of each async call. */
typedef struct {
/* Input arguments. */
GsApp *app; /* (not nullable) (owned) */
GCancellable *cancellable; /* (nullable) (not owned) */
gulong cancelled_id;
gboolean interactive;
/* State. */
gboolean done;
gboolean allow_restart;
/* Completion callback. */
gboolean (*finish_func) (GsEosUpdater *updater_proxy,
GAsyncResult *result,
GError **error); /* (nullable) */
} UpgradeDownloadState;
static void
upgrade_download_state_free (UpgradeDownloadState *data)
{
g_clear_object (&data->app);
if (data->cancellable != NULL && data->cancelled_id != 0) {
g_debug ("Disconnecting cancellable %p", data->cancellable);
g_cancellable_disconnect (data->cancellable, data->cancelled_id);
data->cancellable = NULL;
data->cancelled_id = 0;
}
g_free (data);
}
G_DEFINE_AUTOPTR_CLEANUP_FUNC (UpgradeDownloadState, upgrade_download_state_free)
static void download_iterate_state_machine_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
/* Called in a #GTask worker thread or in the main thread. */
static void
gs_plugin_eos_updater_app_upgrade_download_async (GsPluginEosUpdater *self,
GsApp *app,
gboolean interactive,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
g_autoptr(GTask) task = NULL;
g_autoptr(UpgradeDownloadState) data_owned = NULL;
UpgradeDownloadState *data;
EosUpdaterState state;
task = g_task_new (self, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_app_upgrade_download_async);
/* only process this app if was created by this plugin */
if (!gs_app_has_management_plugin (app, GS_PLUGIN (self))) {
g_task_return_boolean (task, TRUE);
return;
}
/* if the OS upgrade has been disabled */
if (self->updater_proxy == NULL) {
g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
"The OS upgrade has been disabled in the EOS plugin");
return;
}
g_assert (app == self->os_upgrade);
/* Set up some state. */
data = data_owned = g_new0 (UpgradeDownloadState, 1);
data->app = g_object_ref (app);
data->interactive = interactive;
g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) upgrade_download_state_free);
/* Set up cancellation. */
g_debug ("Chaining cancellation from %p to %p", cancellable, self->cancellable);
if (cancellable != NULL) {
data->cancellable = cancellable; /* ref is held by @task */
data->cancelled_id = g_cancellable_connect (cancellable,
G_CALLBACK (cancelled_cb),
self, NULL);
}
/* Step through the state machine until we are finished downloading and
* applying the update, or until an error occurs.
*
* Each step is a call to download_iterate_state_machine_cb(). The first
* call is below, and subsequent calls come from async function
* completions.
*
* `data->done` is %TRUE once we reach the `UPDATE_APPLIED` state.
*
* `data->allow_restart` indicates whether the state machine can be
* restarted to clear an error condition, or whether the error should be
* propagated (because the machine has already been restarted). This
* prevents infinite loops. */
state = gs_eos_updater_get_state (self->updater_proxy);
data->done = FALSE;
data->allow_restart = (state == EOS_UPDATER_STATE_NONE ||
state == EOS_UPDATER_STATE_READY ||
state == EOS_UPDATER_STATE_ERROR);
download_iterate_state_machine_cb (G_OBJECT (self->updater_proxy), NULL, g_steal_pointer (&task));
}
static gboolean
is_wrong_state_error (const GError *error)
{
g_autofree gchar *remote_error = NULL;
if (!g_dbus_error_is_remote_error (error))
return FALSE;
remote_error = g_dbus_error_get_remote_error (error);
return g_str_equal (remote_error, "com.endlessm.Updater.Error.WrongState");
}
static void
download_iterate_state_machine_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
g_autoptr(GTask) task = g_steal_pointer (&user_data);
GsPluginEosUpdater *self = g_task_get_source_object (task);
UpgradeDownloadState *data = g_task_get_task_data (task);
GCancellable *cancellable = g_task_get_cancellable (task);
EosUpdaterState state;
/* Call the finish function from the asynchronous method call which has
* just completed and brought us back into
* download_iterate_state_machine_cb().
*
* This may be %NULL if the state machine is just being started. */
if (data->finish_func != NULL) {
g_autoptr(GError) local_error = NULL;
if (!data->finish_func (self->updater_proxy, result, &local_error)) {
/* Ignore WrongState errors, since we explicitly synchronise
* to the daemons state again below, so should be able
* to recover from them. The user cant do anything
* about them anyway. */
if (is_wrong_state_error (local_error)) {
g_debug ("Got WrongState error from eos-updater daemon; ignoring.");
g_clear_error (&local_error);
} else {
gs_eos_updater_error_convert (&local_error);
g_task_return_error (task, g_steal_pointer (&local_error));
return;
}
}
data->finish_func = NULL;
} else {
g_assert (result == NULL);
}
/* Iterate the state machine one step. */
while (!data->done && !g_cancellable_is_cancelled (cancellable)) {
state = gs_eos_updater_get_state (self->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
* weve drifted out of sync with the updater process
* due to it dying. In that case, only restart once
* before giving up, so we dont end up in an endless
* loop (say, if eos-updater always died 50% of the way
* through a download). */
if (data->allow_restart) {
data->allow_restart = FALSE;
g_debug ("Restarting OS upgrade from none/ready state");
data->finish_func = gs_eos_updater_call_poll_finish;
gs_eos_updater_call_poll (self->updater_proxy,
data->interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
download_iterate_state_machine_cb,
g_steal_pointer (&task));
return;
} else {
/* Display an error to the user. */
g_autoptr(GError) error_local = NULL;
g_autoptr(GsPluginEvent) event = NULL;
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);
event = gs_plugin_event_new ("app", data->app,
"action", GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD,
"error", error_local,
NULL);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
if (data->interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_report_event (GS_PLUGIN (self), event);
/* Error out. */
data->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);
data->finish_func = gs_eos_updater_call_fetch_full_finish;
gs_eos_updater_call_fetch_full (self->updater_proxy,
g_variant_dict_end (&options_dict),
data->interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
download_iterate_state_machine_cb,
g_steal_pointer (&task));
return;
}
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 (data->app, max_progress_for_update);
/* Nothing further to download. */
gs_app_set_size_download (data->app, GS_SIZE_TYPE_VALID, 0);
data->finish_func = gs_eos_updater_call_apply_finish;
gs_eos_updater_call_apply (self->updater_proxy,
data->interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
download_iterate_state_machine_cb,
g_steal_pointer (&task));
return;
}
case EOS_UPDATER_STATE_APPLYING_UPDATE: {
/* Nothing to do here. */
break;
}
case EOS_UPDATER_STATE_UPDATE_APPLIED: {
/* Done! */
data->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 (self->updater_proxy);
error_message = gs_eos_updater_get_error_message (self->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 = NULL;
gs_eos_updater_error_convert (&error_local);
event = gs_plugin_event_new ("app", data->app,
"action", GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD,
"error", error_local,
NULL);
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
if (data->interactive)
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
gs_plugin_report_event (GS_PLUGIN (self), 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 by setting data->done,
* though, to prevent possible endless loops between the
* Poll/Error states. */
data->allow_restart = FALSE;
data->done = TRUE;
g_debug ("Restarting OS upgrade on error");
data->finish_func = gs_eos_updater_call_poll_finish;
gs_eos_updater_call_poll (self->updater_proxy,
data->interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
-1 /* timeout */,
cancellable,
download_iterate_state_machine_cb,
g_steal_pointer (&task));
return;
}
default:
g_warning ("Encountered unknown eos-updater state: %u", state);
break;
}
/* Block on the next state change. */
if (!data->done) {
data->finish_func = wait_for_state_change_finish;
wait_for_state_change_async (self->updater_proxy, cancellable, download_iterate_state_machine_cb, g_steal_pointer (&task));
return;
}
}
/* Process the final state. */
if (gs_eos_updater_get_state (self->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 (self->updater_proxy);
error_message = gs_eos_updater_get_error_message (self->updater_proxy);
error_local = g_dbus_error_new_for_dbus_error (error_name, error_message);
gs_eos_updater_error_convert (&error_local);
g_task_return_error (task, g_steal_pointer (&error_local));
} else if (!g_task_return_error_if_cancelled (task)) {
g_task_return_boolean (task, TRUE);
}
}
static gboolean
gs_plugin_eos_updater_app_upgrade_download_finish (GsPluginEosUpdater *self,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void upgrade_download_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data);
/* Called in the main thread.
*
* Its used to download the update if its been listed in the UI as a major
* upgrade. The download process is the same. */
static void
gs_plugin_eos_updater_download_upgrade_async (GsPlugin *plugin,
GsApp *app,
GsPluginDownloadUpgradeFlags flags,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin);
g_autoptr(GTask) task = NULL;
gboolean interactive = (flags & GS_PLUGIN_DOWNLOAD_UPGRADE_FLAGS_INTERACTIVE) != 0;
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_download_upgrade_async);
g_debug ("%s", G_STRFUNC);
gs_plugin_eos_updater_app_upgrade_download_async (self, app, interactive, cancellable, upgrade_download_cb, g_steal_pointer (&task));
}
static gboolean
gs_plugin_eos_updater_download_upgrade_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
/* Called in the main thread.
*
* Its used to download the update if its been listed in the UI as a minor
* update. The download process is the same. */
static void
gs_plugin_eos_updater_update_apps_async (GsPlugin *plugin,
GsAppList *apps,
GsPluginUpdateAppsFlags flags,
GsPluginProgressCallback progress_callback,
gpointer progress_user_data,
GsPluginAppNeedsUserActionCallback app_needs_user_action_callback,
gpointer app_needs_user_action_data,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (plugin);
g_autoptr(GTask) task = NULL;
GsApp *app;
guint n_managed_apps = 0;
gboolean interactive = (flags & GS_PLUGIN_UPDATE_APPS_FLAGS_INTERACTIVE);
task = g_task_new (plugin, cancellable, callback, user_data);
g_task_set_source_tag (task, gs_plugin_eos_updater_update_apps_async);
g_debug ("%s", G_STRFUNC);
/* check if the OS upgrade has been disabled */
if (self->updater_proxy == NULL) {
g_debug ("%s: Updater disabled", G_STRFUNC);
g_task_return_boolean (task, TRUE);
return;
}
/* Find the app for the OS upgrade in the list of apps. It might not be present. */
for (guint i = 0; i < gs_app_list_length (apps); i++) {
GsApp *app_i = gs_app_list_index (apps, i);
if (gs_app_has_management_plugin (app_i, plugin)) {
app = app_i;
n_managed_apps++;
}
}
if (n_managed_apps == 0) {
g_task_return_boolean (task, TRUE);
return;
}
g_assert (n_managed_apps == 1);
if (!(flags & GS_PLUGIN_UPDATE_APPS_FLAGS_NO_DOWNLOAD)) {
/* Download the update.
* FIXME: Progress reporting */
gs_plugin_eos_updater_app_upgrade_download_async (self, app, interactive, cancellable, upgrade_download_cb, g_steal_pointer (&task));
} else {
g_task_return_boolean (task, TRUE);
}
}
static void
upgrade_download_cb (GObject *source_object,
GAsyncResult *result,
gpointer user_data)
{
GsPluginEosUpdater *self = GS_PLUGIN_EOS_UPDATER (source_object);
g_autoptr(GTask) task = g_steal_pointer (&user_data);
g_autoptr(GError) local_error = NULL;
if (!gs_plugin_eos_updater_app_upgrade_download_finish (self, result, &local_error))
g_task_return_error (task, g_steal_pointer (&local_error));
else
g_task_return_boolean (task, TRUE);
}
static gboolean
gs_plugin_eos_updater_update_apps_finish (GsPlugin *plugin,
GAsyncResult *result,
GError **error)
{
return g_task_propagate_boolean (G_TASK (result), error);
}
static void
gs_plugin_eos_updater_class_init (GsPluginEosUpdaterClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
object_class->dispose = gs_plugin_eos_updater_dispose;
plugin_class->setup_async = gs_plugin_eos_updater_setup_async;
plugin_class->setup_finish = gs_plugin_eos_updater_setup_finish;
plugin_class->refresh_metadata_async = gs_plugin_eos_updater_refresh_metadata_async;
plugin_class->refresh_metadata_finish = gs_plugin_eos_updater_refresh_metadata_finish;
plugin_class->list_distro_upgrades_async = gs_plugin_eos_updater_list_distro_upgrades_async;
plugin_class->list_distro_upgrades_finish = gs_plugin_eos_updater_list_distro_upgrades_finish;
plugin_class->update_apps_async = gs_plugin_eos_updater_update_apps_async;
plugin_class->update_apps_finish = gs_plugin_eos_updater_update_apps_finish;
plugin_class->list_apps_async = gs_plugin_eos_updater_list_apps_async;
plugin_class->list_apps_finish = gs_plugin_eos_updater_list_apps_finish;
plugin_class->download_upgrade_async = gs_plugin_eos_updater_download_upgrade_async;
plugin_class->download_upgrade_finish = gs_plugin_eos_updater_download_upgrade_finish;
}
GType
gs_plugin_query_type (void)
{
return GS_TYPE_PLUGIN_EOS_UPDATER;
}