/* -*- 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 * Philip Withnall * * 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 #include #include #include #include #include #include #include #include #include #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 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 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 can’t 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-software’s 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-software’s * 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 ("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 (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 doesn’t 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 we’re 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 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, 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 thread’s #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 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. */ 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, "Couldn’t create EOS Updater proxy: couldn’t 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 it’s %G_MAXUINT64, 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_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 don’t * 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 updater’s * 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 daemon’s state again below, so should be able * to recover from them. The user can’t 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 * 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 (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. * * It’s used to download the update if it’s 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. * * It’s used to download the update if it’s 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; }