summaryrefslogtreecommitdiffstats
path: root/plugins/fwupd
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--plugins/fwupd/gs-fwupd-app.c309
-rw-r--r--plugins/fwupd/gs-fwupd-app.h31
-rw-r--r--plugins/fwupd/gs-plugin-fwupd.c1414
-rw-r--r--plugins/fwupd/gs-plugin-fwupd.h22
-rw-r--r--plugins/fwupd/gs-self-test.c111
-rw-r--r--plugins/fwupd/meson.build54
-rw-r--r--plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in12
-rwxr-xr-xplugins/fwupd/tests/build-cab.sh4
-rw-r--r--plugins/fwupd/tests/chiron-0.2.cabbin0 -> 34376 bytes
-rw-r--r--plugins/fwupd/tests/firmware.dfubin0 -> 32784 bytes
-rw-r--r--plugins/fwupd/tests/firmware.dfu.asc11
-rw-r--r--plugins/fwupd/tests/firmware.metainfo.xml30
12 files changed, 1998 insertions, 0 deletions
diff --git a/plugins/fwupd/gs-fwupd-app.c b/plugins/fwupd/gs-fwupd-app.c
new file mode 100644
index 0000000..c27f5a5
--- /dev/null
+++ b/plugins/fwupd/gs-fwupd-app.c
@@ -0,0 +1,309 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib/gi18n.h>
+
+#include "gs-fwupd-app.h"
+
+const gchar *
+gs_fwupd_app_get_device_id (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "fwupd::DeviceID");
+}
+
+const gchar *
+gs_fwupd_app_get_update_uri (GsApp *app)
+{
+ return gs_app_get_metadata_item (app, "fwupd::UpdateID");
+}
+
+gboolean
+gs_fwupd_app_get_is_locked (GsApp *app)
+{
+ GVariant *tmp = gs_app_get_metadata_variant (app, "fwupd::IsLocked");
+ if (tmp == NULL)
+ return FALSE;
+ return g_variant_get_boolean (tmp);
+}
+
+void
+gs_fwupd_app_set_device_id (GsApp *app, const gchar *device_id)
+{
+ gs_app_set_metadata (app, "fwupd::DeviceID", device_id);
+}
+
+void
+gs_fwupd_app_set_update_uri (GsApp *app, const gchar *update_uri)
+{
+ gs_app_set_metadata (app, "fwupd::UpdateID", update_uri);
+}
+
+void
+gs_fwupd_app_set_is_locked (GsApp *app, gboolean is_locked)
+{
+ g_autoptr(GVariant) tmp = g_variant_new_boolean (is_locked);
+ gs_app_set_metadata_variant (app, "fwupd::IsLocked", tmp);
+}
+
+void
+gs_fwupd_app_set_from_device (GsApp *app, FwupdDevice *dev)
+{
+ GPtrArray *guids;
+
+ /* something can be done */
+ if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_UPDATABLE))
+ gs_app_set_state (app, GS_APP_STATE_UPDATABLE_LIVE);
+
+ /* only can be applied in systemd-offline */
+ if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_ONLY_OFFLINE))
+ gs_app_set_metadata (app, "fwupd::OnlyOffline", "");
+
+
+ /* reboot required to apply update */
+ if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_REBOOT))
+ gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT);
+
+ /* is removable */
+ if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_INTERNAL))
+ gs_app_add_quirk (app, GS_APP_QUIRK_REMOVABLE_HARDWARE);
+
+ guids = fwupd_device_get_guids (dev);
+ if (guids->len > 0) {
+ g_autofree gchar *guid_str = NULL;
+ g_auto(GStrv) tmp = g_new0 (gchar *, guids->len + 1);
+ for (guint i = 0; i < guids->len; i++)
+ tmp[i] = g_strdup (g_ptr_array_index (guids, i));
+ guid_str = g_strjoinv (",", tmp);
+ gs_app_set_metadata (app, "fwupd::Guid", guid_str);
+ }
+ if (fwupd_device_get_name (dev) != NULL) {
+ g_autofree gchar *vendor_name = NULL;
+ if (fwupd_device_get_vendor (dev) == NULL ||
+ g_str_has_prefix (fwupd_device_get_name (dev),
+ fwupd_device_get_vendor (dev))) {
+ vendor_name = g_strdup (fwupd_device_get_name (dev));
+ } else {
+ vendor_name = g_strdup_printf ("%s %s",
+ fwupd_device_get_vendor (dev),
+ fwupd_device_get_name (dev));
+ }
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, vendor_name);
+ }
+ if (fwupd_device_get_summary (dev) != NULL) {
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL,
+ fwupd_device_get_summary (dev));
+ }
+ if (fwupd_device_get_version (dev) != NULL) {
+ gs_app_set_version (app, fwupd_device_get_version (dev));
+ }
+ if (fwupd_device_get_created (dev) != 0)
+ gs_app_set_install_date (app, fwupd_device_get_created (dev));
+ if (fwupd_device_get_description (dev) != NULL) {
+ g_autofree gchar *tmp = NULL;
+ tmp = as_markup_convert_simple (fwupd_device_get_description (dev), NULL);
+ if (tmp != NULL)
+ gs_app_set_description (app, GS_APP_QUALITY_NORMAL, tmp);
+ }
+
+ /* needs action */
+ if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_NEEDS_BOOTLOADER))
+ gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION);
+ else
+ gs_app_remove_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION);
+}
+
+static gchar *
+gs_fwupd_release_get_name (FwupdRelease *release)
+{
+ const gchar *name = fwupd_release_get_name (release);
+ GPtrArray *cats = fwupd_release_get_categories (release);
+
+ for (guint i = 0; i < cats->len; i++) {
+ const gchar *cat = g_ptr_array_index (cats, i);
+ if (g_strcmp0 (cat, "X-Device") == 0) {
+ /* TRANSLATORS: a specific part of hardware,
+ * the first %s is the device name, e.g. 'Unifying Receiver` */
+ return g_strdup_printf (_("%s Device Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-System") == 0) {
+ /* TRANSLATORS: the entire system, e.g. all internal devices,
+ * the first %s is the device name, e.g. 'ThinkPad P50` */
+ return g_strdup_printf (_("%s System Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-EmbeddedController") == 0) {
+ /* TRANSLATORS: the EC is typically the keyboard controller chip,
+ * the first %s is the device name, e.g. 'ThinkPad P50` */
+ return g_strdup_printf (_("%s Embedded Controller Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-ManagementEngine") == 0) {
+ /* TRANSLATORS: ME stands for Management Engine, the Intel AMT thing,
+ * the first %s is the device name, e.g. 'ThinkPad P50` */
+ return g_strdup_printf (_("%s ME Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-CorporateManagementEngine") == 0) {
+ /* TRANSLATORS: ME stands for Management Engine (with Intel AMT),
+ * where the first %s is the device name, e.g. 'ThinkPad P50` */
+ return g_strdup_printf (_("%s Corporate ME Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-ConsumerManagementEngine") == 0) {
+ /* TRANSLATORS: ME stands for Management Engine, where
+ * the first %s is the device name, e.g. 'ThinkPad P50` */
+ return g_strdup_printf (_("%s Consumer ME Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Controller") == 0) {
+ /* TRANSLATORS: the controller is a device that has other devices
+ * plugged into it, for example ThunderBolt, FireWire or USB,
+ * the first %s is the device name, e.g. 'Intel ThunderBolt` */
+ return g_strdup_printf (_("%s Controller Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-ThunderboltController") == 0) {
+ /* TRANSLATORS: the Thunderbolt controller is a device that
+ * has other high speed Thunderbolt devices plugged into it;
+ * the first %s is the system name, e.g. 'ThinkPad P50` */
+ return g_strdup_printf (_("%s Thunderbolt Controller Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-CpuMicrocode") == 0) {
+ /* TRANSLATORS: the CPU microcode is firmware loaded onto the CPU
+ * at system bootup */
+ return g_strdup_printf (_("%s CPU Microcode Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Configuration") == 0) {
+ /* TRANSLATORS: configuration refers to hardware state,
+ * e.g. a security database or a default power value */
+ return g_strdup_printf (_("%s Configuration Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Battery") == 0) {
+ /* TRANSLATORS: battery refers to the system power source */
+ return g_strdup_printf (_("%s Battery Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Camera") == 0) {
+ /* TRANSLATORS: camera can refer to the laptop internal
+ * camera in the bezel or external USB webcam */
+ return g_strdup_printf (_("%s Camera Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-TPM") == 0) {
+ /* TRANSLATORS: TPM refers to a Trusted Platform Module */
+ return g_strdup_printf (_("%s TPM Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Touchpad") == 0) {
+ /* TRANSLATORS: TouchPad refers to a flat input device */
+ return g_strdup_printf (_("%s Touchpad Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Mouse") == 0) {
+ /* TRANSLATORS: Mouse refers to a handheld input device */
+ return g_strdup_printf (_("%s Mouse Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-Keyboard") == 0) {
+ /* TRANSLATORS: Keyboard refers to an input device for typing */
+ return g_strdup_printf (_("%s Keyboard Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-StorageController") == 0) {
+ /* TRANSLATORS: Storage Controller is typically a RAID or SAS adapter */
+ return g_strdup_printf (_("%s Storage Controller Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-NetworkInterface") == 0) {
+ /* TRANSLATORS: Network Interface refers to the physical
+ * PCI card, not the logical wired connection */
+ return g_strdup_printf (_("%s Network Interface Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-VideoDisplay") == 0) {
+ /* TRANSLATORS: Video Display refers to the laptop internal display or
+ * external monitor */
+ return g_strdup_printf (_("%s Display Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-BaseboardManagementController") == 0) {
+ /* TRANSLATORS: BMC refers to baseboard management controller which
+ * is the device that updates all the other firmware on the system */
+ return g_strdup_printf (_("%s BMC Update"), name);
+ }
+ if (g_strcmp0 (cat, "X-UsbReceiver") == 0) {
+ /* TRANSLATORS: Receiver refers to a radio device, e.g. a tiny Bluetooth
+ * device that stays in the USB port so the wireless peripheral works */
+ return g_strdup_printf (_("%s USB Receiver Update"), name);
+ }
+ }
+
+ /* default fallback */
+ return g_strdup (name);
+}
+
+static AsUrgencyKind
+gs_fwupd_release_urgency_to_as_urgency_kind (FwupdReleaseUrgency urgency)
+{
+ switch (urgency) {
+ case FWUPD_RELEASE_URGENCY_LOW:
+ return AS_URGENCY_KIND_LOW;
+ case FWUPD_RELEASE_URGENCY_MEDIUM:
+ return AS_URGENCY_KIND_MEDIUM;
+ case FWUPD_RELEASE_URGENCY_HIGH:
+ return AS_URGENCY_KIND_HIGH;
+ case FWUPD_RELEASE_URGENCY_CRITICAL:
+ return AS_URGENCY_KIND_CRITICAL;
+ case FWUPD_RELEASE_URGENCY_UNKNOWN:
+ default:
+ return AS_URGENCY_KIND_UNKNOWN;
+ }
+}
+
+void
+gs_fwupd_app_set_from_release (GsApp *app, FwupdRelease *rel)
+{
+ GPtrArray *locations = fwupd_release_get_locations (rel);
+
+ if (fwupd_release_get_name (rel) != NULL) {
+ g_autofree gchar *tmp = gs_fwupd_release_get_name (rel);
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, tmp);
+ }
+ if (fwupd_release_get_summary (rel) != NULL) {
+ gs_app_set_summary (app, GS_APP_QUALITY_NORMAL,
+ fwupd_release_get_summary (rel));
+ }
+ if (fwupd_release_get_homepage (rel) != NULL) {
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE,
+ fwupd_release_get_homepage (rel));
+ }
+ if (fwupd_release_get_size (rel) != 0) {
+ gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, 0);
+ gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, fwupd_release_get_size (rel));
+ }
+ if (fwupd_release_get_version (rel) != NULL)
+ gs_app_set_update_version (app, fwupd_release_get_version (rel));
+ if (fwupd_release_get_license (rel) != NULL) {
+ gs_app_set_license (app, GS_APP_QUALITY_NORMAL,
+ fwupd_release_get_license (rel));
+ }
+ if (locations->len > 0) {
+ const gchar *uri = g_ptr_array_index (locations, 0);
+ /* typically the first URI will be the main HTTP mirror, and we
+ * don't have the capability to use an IPFS/IPNS URL anyway */
+ gs_app_set_origin_hostname (app, uri);
+ gs_fwupd_app_set_update_uri (app, uri);
+ }
+ if (fwupd_release_get_description (rel) != NULL) {
+ g_autofree gchar *tmp = NULL;
+ tmp = as_markup_convert_simple (fwupd_release_get_description (rel), NULL);
+ if (tmp != NULL)
+ gs_app_set_update_details_text (app, tmp);
+ }
+ if (fwupd_release_get_detach_image (rel) != NULL) {
+ g_autoptr(AsScreenshot) ss = as_screenshot_new ();
+ g_autoptr(AsImage) im = as_image_new ();
+ as_image_set_kind (im, AS_IMAGE_KIND_SOURCE);
+ as_image_set_url (im, fwupd_release_get_detach_image (rel));
+ as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_DEFAULT);
+ as_screenshot_add_image (ss, im);
+ if (fwupd_release_get_detach_caption (rel) != NULL)
+ as_screenshot_set_caption (ss, fwupd_release_get_detach_caption (rel), NULL);
+ gs_app_set_action_screenshot (app, ss);
+ }
+
+ gs_app_set_update_urgency (app, gs_fwupd_release_urgency_to_as_urgency_kind (fwupd_release_get_urgency (rel)));
+}
diff --git a/plugins/fwupd/gs-fwupd-app.h b/plugins/fwupd/gs-fwupd-app.h
new file mode 100644
index 0000000..5a3e9f2
--- /dev/null
+++ b/plugins/fwupd/gs-fwupd-app.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gnome-software.h>
+#include <fwupd.h>
+
+G_BEGIN_DECLS
+
+const gchar *gs_fwupd_app_get_device_id (GsApp *app);
+const gchar *gs_fwupd_app_get_update_uri (GsApp *app);
+gboolean gs_fwupd_app_get_is_locked (GsApp *app);
+
+void gs_fwupd_app_set_device_id (GsApp *app,
+ const gchar *device_id);
+void gs_fwupd_app_set_update_uri (GsApp *app,
+ const gchar *update_uri);
+void gs_fwupd_app_set_is_locked (GsApp *app,
+ gboolean is_locked);
+void gs_fwupd_app_set_from_device (GsApp *app,
+ FwupdDevice *dev);
+void gs_fwupd_app_set_from_release (GsApp *app,
+ FwupdRelease *rel);
+
+G_END_DECLS
diff --git a/plugins/fwupd/gs-plugin-fwupd.c b/plugins/fwupd/gs-plugin-fwupd.c
new file mode 100644
index 0000000..b5d99b7
--- /dev/null
+++ b/plugins/fwupd/gs-plugin-fwupd.c
@@ -0,0 +1,1414 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <fwupd.h>
+#include <fcntl.h>
+#include <gio/gio.h>
+#include <gio/gunixfdlist.h>
+#include <glib/gi18n.h>
+#include <glib/gstdio.h>
+
+#include <gnome-software.h>
+
+#include "gs-fwupd-app.h"
+#include "gs-metered.h"
+
+#include "gs-plugin-fwupd.h"
+
+/*
+ * SECTION:
+ * Queries for new firmware and schedules it to be installed as required.
+ *
+ * This plugin calls UpdatesChanged() if any updatable devices are
+ * added or removed or if a device has been updated live.
+ *
+ * Since fwupd is a daemon accessible over D-Bus, this plugin basically
+ * translates every job into one or more D-Bus calls, and all the real work is
+ * done in the fwupd daemon. FIXME: This means the plugin can therefore execute
+ * entirely in the main thread, making asynchronous D-Bus calls, once all the
+ * vfuncs have been ported.
+ */
+
+struct _GsPluginFwupd {
+ GsPlugin parent;
+
+ FwupdClient *client;
+ GsApp *app_current;
+ GsApp *cached_origin;
+};
+
+G_DEFINE_TYPE (GsPluginFwupd, gs_plugin_fwupd, GS_TYPE_PLUGIN)
+
+static void
+gs_plugin_fwupd_error_convert (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return;
+
+ /* already correct */
+ if (error->domain == GS_PLUGIN_ERROR)
+ return;
+
+ /* this are allowed for low-level errors */
+ if (gs_utils_error_convert_gio (perror))
+ return;
+
+ /* this are allowed for low-level errors */
+ if (gs_utils_error_convert_gdbus (perror))
+ return;
+
+ /* custom to this plugin */
+ if (error->domain == FWUPD_ERROR) {
+ switch (error->code) {
+ case FWUPD_ERROR_ALREADY_PENDING:
+ case FWUPD_ERROR_INVALID_FILE:
+ case FWUPD_ERROR_NOT_SUPPORTED:
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ break;
+ case FWUPD_ERROR_AUTH_FAILED:
+ error->code = GS_PLUGIN_ERROR_AUTH_INVALID;
+ break;
+ case FWUPD_ERROR_SIGNATURE_INVALID:
+ error->code = GS_PLUGIN_ERROR_NO_SECURITY;
+ break;
+ case FWUPD_ERROR_AC_POWER_REQUIRED:
+ error->code = GS_PLUGIN_ERROR_AC_POWER_REQUIRED;
+ break;
+ case FWUPD_ERROR_BATTERY_LEVEL_TOO_LOW:
+ error->code = GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW;
+ break;
+ default:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ } else {
+ g_warning ("can't reliably fixup error from domain %s",
+ g_quark_to_string (error->domain));
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+}
+
+static void
+gs_plugin_fwupd_init (GsPluginFwupd *self)
+{
+ self->client = fwupd_client_new ();
+
+ /* set name of MetaInfo file */
+ gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Fwupd");
+}
+
+static void
+gs_plugin_fwupd_dispose (GObject *object)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (object);
+
+ g_clear_object (&self->cached_origin);
+ g_clear_object (&self->client);
+
+ G_OBJECT_CLASS (gs_plugin_fwupd_parent_class)->dispose (object);
+}
+
+void
+gs_plugin_adopt_app (GsPlugin *plugin, GsApp *app)
+{
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FIRMWARE)
+ gs_app_set_management_plugin (app, plugin);
+}
+
+static void
+gs_plugin_fwupd_changed_cb (FwupdClient *client, GsPlugin *plugin)
+{
+}
+
+static void
+gs_plugin_fwupd_device_changed_cb (FwupdClient *client,
+ FwupdDevice *dev,
+ GsPlugin *plugin)
+{
+ /* limit number of UI refreshes */
+ if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_SUPPORTED)) {
+ g_debug ("%s changed (not supported) so ignoring",
+ fwupd_device_get_id (dev));
+ return;
+ }
+
+ /* If the flag is set the device matches something in the
+ * metadata as therefor is worth refreshing the update list */
+ g_debug ("%s changed (supported) so reloading",
+ fwupd_device_get_id (dev));
+ gs_plugin_updates_changed (plugin);
+}
+
+static void
+gs_plugin_fwupd_notify_percentage_cb (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (user_data);
+
+ /* nothing in progress */
+ if (self->app_current == NULL) {
+ g_debug ("fwupd percentage: %u%%",
+ fwupd_client_get_percentage (self->client));
+ return;
+ }
+ g_debug ("fwupd percentage for %s: %u%%",
+ gs_app_get_unique_id (self->app_current),
+ fwupd_client_get_percentage (self->client));
+ gs_app_set_progress (self->app_current,
+ fwupd_client_get_percentage (self->client));
+}
+
+static void
+gs_plugin_fwupd_notify_status_cb (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (user_data);
+
+ /* nothing in progress */
+ if (self->app_current == NULL) {
+ g_debug ("fwupd status: %s",
+ fwupd_status_to_string (fwupd_client_get_status (self->client)));
+ return;
+ }
+
+ g_debug ("fwupd status for %s: %s",
+ gs_app_get_unique_id (self->app_current),
+ fwupd_status_to_string (fwupd_client_get_status (self->client)));
+ switch (fwupd_client_get_status (self->client)) {
+ case FWUPD_STATUS_DECOMPRESSING:
+ case FWUPD_STATUS_DEVICE_RESTART:
+ case FWUPD_STATUS_DEVICE_WRITE:
+ case FWUPD_STATUS_DEVICE_VERIFY:
+ gs_app_set_state (self->app_current, GS_APP_STATE_INSTALLING);
+ break;
+ case FWUPD_STATUS_IDLE:
+ g_clear_object (&self->app_current);
+ break;
+ default:
+ break;
+ }
+}
+
+static gchar *
+gs_plugin_fwupd_get_file_checksum (const gchar *filename,
+ GChecksumType checksum_type,
+ GError **error)
+{
+ gsize len;
+ g_autofree gchar *data = NULL;
+
+ if (!g_file_get_contents (filename, &data, &len, error)) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ return g_compute_checksum_for_data (checksum_type, (const guchar *)data, len);
+}
+
+static void setup_connect_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void setup_features_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+
+static void
+gs_plugin_fwupd_setup_async (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_fwupd_setup_async);
+
+ /* connect a proxy */
+ fwupd_client_connect_async (self->client, cancellable, setup_connect_cb,
+ g_steal_pointer (&task));
+}
+
+static void
+setup_connect_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ GsPluginFwupd *self = g_task_get_source_object (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ g_autoptr(GError) local_error = NULL;
+
+ if (!fwupd_client_connect_finish (self->client, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* send our implemented feature set */
+ fwupd_client_set_feature_flags_async (self->client,
+ FWUPD_FEATURE_FLAG_UPDATE_ACTION |
+ FWUPD_FEATURE_FLAG_DETACH_ACTION,
+ cancellable, setup_features_cb,
+ g_steal_pointer (&task));
+}
+
+static void
+setup_features_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ GsPluginFwupd *self = g_task_get_source_object (task);
+ GsPlugin *plugin = GS_PLUGIN (self);
+ g_autoptr(GError) local_error = NULL;
+
+ if (!fwupd_client_set_feature_flags_finish (self->client, result, &local_error))
+ g_debug ("Failed to set front-end features: %s", local_error->message);
+ g_clear_error (&local_error);
+
+ /* we know the runtime daemon version now */
+ fwupd_client_set_user_agent_for_package (self->client, PACKAGE_NAME, PACKAGE_VERSION);
+ if (!fwupd_client_ensure_networking (self->client, &local_error)) {
+ gs_plugin_fwupd_error_convert (&local_error);
+ g_prefix_error (&local_error, "Failed to setup networking: ");
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* add source */
+ self->cached_origin = gs_app_new (gs_plugin_get_name (plugin));
+ gs_app_set_kind (self->cached_origin, AS_COMPONENT_KIND_REPOSITORY);
+ gs_app_set_bundle_kind (self->cached_origin, AS_BUNDLE_KIND_CABINET);
+ gs_app_set_management_plugin (self->cached_origin, plugin);
+
+ /* add the source to the plugin cache which allows us to match the
+ * unique ID to a GsApp when creating an event */
+ gs_plugin_cache_add (plugin,
+ gs_app_get_unique_id (self->cached_origin),
+ self->cached_origin);
+
+ /* register D-Bus errors */
+ fwupd_error_quark ();
+ g_signal_connect (self->client, "changed",
+ G_CALLBACK (gs_plugin_fwupd_changed_cb), plugin);
+ g_signal_connect (self->client, "device-added",
+ G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin);
+ g_signal_connect (self->client, "device-removed",
+ G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin);
+ g_signal_connect (self->client, "device-changed",
+ G_CALLBACK (gs_plugin_fwupd_device_changed_cb), plugin);
+ g_signal_connect (self->client, "notify::percentage",
+ G_CALLBACK (gs_plugin_fwupd_notify_percentage_cb), self);
+ g_signal_connect (self->client, "notify::status",
+ G_CALLBACK (gs_plugin_fwupd_notify_status_cb), self);
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+gs_plugin_fwupd_setup_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static GsApp *
+gs_plugin_fwupd_new_app_from_device (GsPlugin *plugin, FwupdDevice *dev)
+{
+ FwupdRelease *rel = fwupd_device_get_release_default (dev);
+ GsApp *app;
+ g_autofree gchar *id = NULL;
+ g_autoptr(GIcon) icon = NULL;
+
+ /* older versions of fwups didn't record this for historical devices */
+ if (fwupd_release_get_appstream_id (rel) == NULL)
+ return NULL;
+
+ /* get from cache */
+ id = gs_utils_build_unique_id (AS_COMPONENT_SCOPE_SYSTEM,
+ AS_BUNDLE_KIND_UNKNOWN,
+ NULL, /* origin */
+ fwupd_release_get_appstream_id (rel),
+ NULL);
+ app = gs_plugin_cache_lookup (plugin, id);
+ if (app == NULL) {
+ app = gs_app_new (id);
+ gs_plugin_cache_add (plugin, id, app);
+ }
+
+ /* default stuff */
+ gs_app_set_kind (app, AS_COMPONENT_KIND_FIRMWARE);
+ gs_app_set_bundle_kind (app, AS_BUNDLE_KIND_CABINET);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE);
+ gs_app_set_management_plugin (app, plugin);
+ gs_app_add_category (app, "System");
+ gs_fwupd_app_set_device_id (app, fwupd_device_get_id (dev));
+
+ /* create icon */
+ icon = g_themed_icon_new ("system-component-firmware");
+ gs_app_add_icon (app, icon);
+ gs_fwupd_app_set_from_device (app, dev);
+ gs_fwupd_app_set_from_release (app, rel);
+
+ if (fwupd_release_get_appstream_id (rel) != NULL)
+ gs_app_set_id (app, fwupd_release_get_appstream_id (rel));
+
+ /* the same as we have already */
+ if (g_strcmp0 (fwupd_device_get_version (dev),
+ fwupd_release_get_version (rel)) == 0) {
+ g_warning ("same firmware version as installed");
+ }
+
+ return app;
+}
+
+static gchar *
+gs_plugin_fwupd_build_device_id (FwupdDevice *dev)
+{
+ g_autofree gchar *tmp = g_strdup (fwupd_device_get_id (dev));
+ g_strdelimit (tmp, "/", '_');
+ return g_strdup_printf ("org.fwupd.%s.device", tmp);
+}
+
+static GsApp *
+gs_plugin_fwupd_new_app_from_device_raw (GsPlugin *plugin, FwupdDevice *device)
+{
+ GPtrArray *icons;
+ g_autofree gchar *id = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* create a GsApp based on the device, not the release */
+ id = gs_plugin_fwupd_build_device_id (device);
+ app = gs_app_new (id);
+ gs_app_set_kind (app, AS_COMPONENT_KIND_FIRMWARE);
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ gs_app_add_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE);
+ gs_app_set_version (app, fwupd_device_get_version (device));
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_name (device));
+ gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_summary (device));
+ gs_app_set_description (app, GS_APP_QUALITY_LOWEST, fwupd_device_get_description (device));
+ gs_app_set_origin (app, fwupd_device_get_vendor (device));
+ gs_fwupd_app_set_device_id (app, fwupd_device_get_id (device));
+ gs_app_set_management_plugin (app, plugin);
+
+ /* create icon */
+ icons = fwupd_device_get_icons (device);
+ for (guint j = 0; j < icons->len; j++) {
+ const gchar *icon_str = g_ptr_array_index (icons, j);
+ g_autoptr(GIcon) icon = NULL;
+ if (g_str_has_prefix (icon_str, "/")) {
+ g_autoptr(GFile) icon_file = g_file_new_for_path (icon_str);
+ icon = g_file_icon_new (icon_file);
+ } else {
+ icon = g_themed_icon_new (icon_str);
+ }
+ gs_app_add_icon (app, icon);
+ }
+ return g_steal_pointer (&app);
+}
+
+static GsApp *
+gs_plugin_fwupd_new_app (GsPlugin *plugin, FwupdDevice *dev, GError **error)
+{
+ FwupdRelease *rel = fwupd_device_get_release_default (dev);
+ GPtrArray *checksums;
+ GPtrArray *locations = fwupd_release_get_locations (rel);
+ const gchar *update_uri = NULL;
+ g_autofree gchar *basename = NULL;
+ g_autofree gchar *filename_cache = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* update unsupported */
+ app = gs_plugin_fwupd_new_app_from_device (plugin, dev);
+ if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "%s [%s] cannot be updated",
+ gs_app_get_name (app), gs_app_get_id (app));
+ return NULL;
+ }
+
+ /* some missing */
+ if (gs_app_get_id (app) == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "fwupd: No id for firmware");
+ return NULL;
+ }
+ if (gs_app_get_version (app) == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "fwupd: No version! for %s!", gs_app_get_id (app));
+ return NULL;
+ }
+ if (gs_app_get_update_version (app) == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "fwupd: No update-version! for %s!", gs_app_get_id (app));
+ return NULL;
+ }
+ checksums = fwupd_release_get_checksums (rel);
+ if (checksums->len == 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NO_SECURITY,
+ "%s [%s] (%s) has no checksums, ignoring as unsafe",
+ gs_app_get_name (app),
+ gs_app_get_id (app),
+ gs_app_get_update_version (app));
+ return NULL;
+ }
+
+ /* typically the first URI will be the main HTTP mirror, and we
+ * don't have the capability to use an IPFS/IPNS URL anyway */
+ if (locations->len > 0)
+ update_uri = g_ptr_array_index (locations, 0);
+
+ if (update_uri == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "no location available for %s [%s]",
+ gs_app_get_name (app), gs_app_get_id (app));
+ return NULL;
+ }
+
+ /* does the firmware already exist in the cache? */
+ basename = g_path_get_basename (update_uri);
+ filename_cache = gs_utils_get_cache_filename ("fwupd",
+ basename,
+ GS_UTILS_CACHE_FLAG_CREATE_DIRECTORY,
+ error);
+ if (filename_cache == NULL)
+ return NULL;
+
+ /* delete the file if the checksum does not match */
+ if (g_file_test (filename_cache, G_FILE_TEST_EXISTS)) {
+ const gchar *checksum_tmp = NULL;
+ g_autofree gchar *checksum = NULL;
+
+ /* we can migrate to something better than SHA1 when the LVFS
+ * starts producing metadata with multiple hash types */
+ checksum_tmp = fwupd_checksum_get_by_kind (checksums,
+ G_CHECKSUM_SHA1);
+ if (checksum_tmp == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "No valid checksum for %s",
+ filename_cache);
+ }
+ checksum = gs_plugin_fwupd_get_file_checksum (filename_cache,
+ G_CHECKSUM_SHA1,
+ error);
+ if (checksum == NULL)
+ return NULL;
+ if (g_strcmp0 (checksum_tmp, checksum) != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "%s does not match checksum, expected %s got %s",
+ filename_cache, checksum_tmp, checksum);
+ g_unlink (filename_cache);
+ return NULL;
+ }
+ }
+
+ /* already downloaded, so overwrite */
+ if (g_file_test (filename_cache, G_FILE_TEST_EXISTS))
+ gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
+
+ /* actually add the application */
+ file = g_file_new_for_path (filename_cache);
+ gs_app_set_local_file (app, file);
+ return g_steal_pointer (&app);
+}
+
+gboolean
+gs_plugin_add_updates_historical (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(FwupdDevice) dev = NULL;
+
+ /* get historical updates */
+ dev = fwupd_client_get_results (self->client,
+ FWUPD_DEVICE_ID_ANY,
+ cancellable,
+ &error_local);
+ if (dev == NULL) {
+ if (g_error_matches (error_local,
+ FWUPD_ERROR,
+ FWUPD_ERROR_NOTHING_TO_DO))
+ return TRUE;
+ if (g_error_matches (error_local,
+ FWUPD_ERROR,
+ FWUPD_ERROR_NOT_FOUND))
+ return TRUE;
+ g_propagate_error (error, g_steal_pointer (&error_local));
+ gs_plugin_fwupd_error_convert (error);
+ return FALSE;
+ }
+
+ /* parse */
+ app = gs_plugin_fwupd_new_app_from_device (plugin, dev);
+ if (app == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "failed to build result for %s",
+ fwupd_device_get_id (dev));
+ return FALSE;
+ }
+ gs_app_list_add (list, app);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_updates (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) devices = NULL;
+
+ /* get current list of updates */
+ devices = fwupd_client_get_devices (self->client, cancellable, &error_local);
+ if (devices == NULL) {
+ if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) ||
+ g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) ||
+ g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
+ g_debug ("no devices (%s)", error_local->message);
+ return TRUE;
+ }
+ g_debug ("Failed to get devices: %s", error_local->message);
+ return TRUE;
+ }
+ for (guint i = 0; i < devices->len; i++) {
+ FwupdDevice *dev = g_ptr_array_index (devices, i);
+ FwupdRelease *rel_newest;
+ g_autoptr(GError) error_local2 = NULL;
+ g_autoptr(GPtrArray) rels = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* locked device that needs unlocking */
+ if (fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_LOCKED)) {
+ app = gs_plugin_fwupd_new_app_from_device_raw (plugin, dev);
+ gs_fwupd_app_set_is_locked (app, TRUE);
+ gs_app_list_add (list, app);
+ continue;
+ }
+
+ /* not going to have results, so save a D-Bus round-trip */
+ if (!fwupd_device_has_flag (dev, FWUPD_DEVICE_FLAG_SUPPORTED))
+ continue;
+
+ /* get the releases for this device and filter for validity */
+ rels = fwupd_client_get_upgrades (self->client,
+ fwupd_device_get_id (dev),
+ cancellable, &error_local2);
+ if (rels == NULL) {
+ if (g_error_matches (error_local2,
+ FWUPD_ERROR,
+ FWUPD_ERROR_NOTHING_TO_DO)) {
+ g_debug ("no updates for %s", fwupd_device_get_id (dev));
+ continue;
+ }
+ if (g_error_matches (error_local2,
+ FWUPD_ERROR,
+ FWUPD_ERROR_NOT_SUPPORTED)) {
+ g_debug ("not supported for %s", fwupd_device_get_id (dev));
+ continue;
+ }
+ g_warning ("failed to get upgrades for %s: %s]",
+ fwupd_device_get_id (dev),
+ error_local2->message);
+ continue;
+ }
+
+ /* normal device update */
+ rel_newest = g_ptr_array_index (rels, 0);
+ fwupd_device_add_release (dev, rel_newest);
+ app = gs_plugin_fwupd_new_app (plugin, dev, &error_local2);
+ if (app == NULL) {
+ g_debug ("%s", error_local2->message);
+ continue;
+ }
+
+ /* add update descriptions for all releases inbetween */
+ if (rels->len > 1) {
+ g_autoptr(GString) update_desc = g_string_new (NULL);
+ for (guint j = 0; j < rels->len; j++) {
+ FwupdRelease *rel = g_ptr_array_index (rels, j);
+ g_autofree gchar *desc = NULL;
+ if (fwupd_release_get_description (rel) == NULL)
+ continue;
+ desc = as_markup_convert_simple (fwupd_release_get_description (rel), NULL);
+ if (desc == NULL)
+ continue;
+ g_string_append_printf (update_desc,
+ "Version %s:\n%s\n\n",
+ fwupd_release_get_version (rel),
+ desc);
+ }
+ if (update_desc->len > 2) {
+ g_string_truncate (update_desc, update_desc->len - 2);
+ gs_app_set_update_details_text (app, update_desc->str);
+ }
+ }
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+static gboolean
+remote_cache_is_expired (FwupdRemote *remote,
+ guint64 cache_age_secs)
+{
+ /* check cache age */
+ if (cache_age_secs > 0) {
+ guint64 age = fwupd_remote_get_age (remote);
+ if (age < cache_age_secs) {
+ g_debug ("fwupd remote is only %" G_GUINT64_FORMAT " seconds old, so ignoring refresh", age);
+ return FALSE;
+ }
+ }
+
+ return TRUE;
+}
+
+typedef struct {
+ /* Input data. */
+ guint64 cache_age_secs;
+
+ /* In-progress state. */
+ guint n_operations_pending;
+ GError *error; /* (owned) (nullable) */
+} RefreshMetadataData;
+
+static void
+refresh_metadata_data_free (RefreshMetadataData *data)
+{
+ g_clear_error (&data->error);
+ g_free (data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (RefreshMetadataData, refresh_metadata_data_free)
+
+static void get_remotes_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void refresh_remote_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data);
+static void finish_refresh_metadata_op (GTask *task);
+
+static void
+gs_plugin_fwupd_refresh_metadata_async (GsPlugin *plugin,
+ guint64 cache_age_secs,
+ GsPluginRefreshMetadataFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(RefreshMetadataData) data = NULL;
+
+ task = g_task_new (plugin, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_fwupd_refresh_metadata_async);
+
+ data = g_new0 (RefreshMetadataData, 1);
+ data->cache_age_secs = cache_age_secs;
+ g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) refresh_metadata_data_free);
+
+ /* get the list of enabled remotes */
+ fwupd_client_get_remotes_async (self->client, cancellable, get_remotes_cb, g_steal_pointer (&task));
+}
+
+static void
+get_remotes_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ FwupdClient *client = FWUPD_CLIENT (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ RefreshMetadataData *data = g_task_get_task_data (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GPtrArray) remotes = NULL;
+
+ remotes = fwupd_client_get_remotes_finish (client, result, &error_local);
+
+ if (remotes == NULL) {
+ g_debug ("No remotes found: %s", error_local ? error_local->message : "Unknown error");
+ if (g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOTHING_TO_DO) ||
+ g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED) ||
+ g_error_matches (error_local, FWUPD_ERROR, FWUPD_ERROR_NOT_FOUND)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ gs_plugin_fwupd_error_convert (&error_local);
+ g_task_return_error (task, g_steal_pointer (&error_local));
+ return;
+ }
+
+ /* Refresh each of the remotes in parallel. Keep the pending operation
+ * count incremented until all operations have been started, so that
+ * the overall operation doesn’t complete too early. */
+ data->n_operations_pending = 1;
+
+ for (guint i = 0; i < remotes->len; i++) {
+ FwupdRemote *remote = g_ptr_array_index (remotes, i);
+
+ if (!fwupd_remote_get_enabled (remote))
+ continue;
+ if (fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_DOWNLOAD)
+ continue;
+ if (!remote_cache_is_expired (remote, data->cache_age_secs))
+ continue;
+
+ data->n_operations_pending++;
+ fwupd_client_refresh_remote_async (client, remote, cancellable,
+ refresh_remote_cb, g_object_ref (task));
+ }
+
+ finish_refresh_metadata_op (task);
+}
+
+static void
+refresh_remote_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ FwupdClient *client = FWUPD_CLIENT (source_object);
+ g_autoptr(GTask) task = g_steal_pointer (&user_data);
+ RefreshMetadataData *data = g_task_get_task_data (task);
+ g_autoptr(GError) local_error = NULL;
+
+ if (!fwupd_client_refresh_remote_finish (client, result, &local_error)) {
+ gs_plugin_fwupd_error_convert (&local_error);
+ if (data->error == NULL)
+ data->error = g_steal_pointer (&local_error);
+ else
+ g_debug ("Another remote refresh error: %s", local_error->message);
+ }
+
+ finish_refresh_metadata_op (task);
+}
+
+static void
+finish_refresh_metadata_op (GTask *task)
+{
+ RefreshMetadataData *data = g_task_get_task_data (task);
+
+ g_assert (data->n_operations_pending > 0);
+ data->n_operations_pending--;
+
+ if (data->n_operations_pending == 0) {
+ if (data->error != NULL)
+ g_task_return_error (task, g_steal_pointer (&data->error));
+ else
+ g_task_return_boolean (task, TRUE);
+ }
+}
+
+static gboolean
+gs_plugin_fwupd_refresh_metadata_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static gboolean
+gs_plugin_fwupd_install (GsPluginFwupd *self,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *device_id;
+ FwupdInstallFlags install_flags = 0;
+ GFile *local_file;
+ g_autofree gchar *filename = NULL;
+ gboolean downloaded_to_cache = FALSE;
+ g_autoptr(FwupdDevice) dev = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* not set */
+ local_file = gs_app_get_local_file (app);
+ if (local_file == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "not enough data for fwupd %s",
+ filename);
+ return FALSE;
+ }
+
+ /* file does not yet exist */
+ filename = g_file_get_path (local_file);
+ if (!g_file_query_exists (local_file, cancellable)) {
+ const gchar *uri = gs_fwupd_app_get_update_uri (app);
+ g_autoptr(GFile) file = g_file_new_for_path (filename);
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ if (!fwupd_client_download_file (self->client,
+ uri, file,
+ FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
+ cancellable,
+ error)) {
+ gs_plugin_fwupd_error_convert (error);
+ return FALSE;
+ }
+
+ downloaded_to_cache = TRUE;
+ }
+
+ /* limit to single device? */
+ device_id = gs_fwupd_app_get_device_id (app);
+ if (device_id == NULL)
+ device_id = FWUPD_DEVICE_ID_ANY;
+
+ /* set the last object */
+ g_set_object (&self->app_current, app);
+
+ /* only offline supported */
+ if (gs_app_get_metadata_item (app, "fwupd::OnlyOffline") != NULL)
+ install_flags |= FWUPD_INSTALL_FLAG_OFFLINE;
+
+ gs_app_set_state (app, GS_APP_STATE_INSTALLING);
+ if (!fwupd_client_install (self->client, device_id,
+ filename, install_flags,
+ cancellable, error)) {
+ gs_plugin_fwupd_error_convert (error);
+ gs_app_set_state_recover (app);
+ return FALSE;
+ }
+
+ /* delete the file from the cache */
+ gs_app_set_state (app, GS_APP_STATE_INSTALLED);
+ if (downloaded_to_cache) {
+ if (!g_file_delete (local_file, cancellable, error))
+ return FALSE;
+ }
+
+ /* does the device have an update message */
+ dev = fwupd_client_get_device_by_id (self->client, device_id,
+ cancellable, &error_local);
+ if (dev == NULL) {
+ /* NOTE: this is probably entirely fine; some devices do not
+ * re-enumerate until replugged manually or the machine is
+ * rebooted -- and the metadata to know that is only available
+ * in a too-new-to-depend-on fwupd version */
+ g_debug ("failed to find device after install: %s", error_local->message);
+ } else {
+ if (fwupd_device_get_update_message (dev) != NULL) {
+ g_autoptr(AsScreenshot) ss = as_screenshot_new ();
+
+ /* image is optional */
+ if (fwupd_device_get_update_image (dev) != NULL) {
+ g_autoptr(AsImage) im = as_image_new ();
+ as_image_set_kind (im, AS_IMAGE_KIND_SOURCE);
+ as_image_set_url (im, fwupd_device_get_update_image (dev));
+ as_screenshot_add_image (ss, im);
+ }
+
+ /* caption is required */
+ as_screenshot_set_kind (ss, AS_SCREENSHOT_KIND_DEFAULT);
+ as_screenshot_set_caption (ss, fwupd_device_get_update_message (dev), NULL);
+ gs_app_set_action_screenshot (app, ss);
+
+ /* require the dialog */
+ gs_app_add_quirk (app, GS_APP_QUIRK_NEEDS_USER_ACTION);
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static void
+gs_plugin_fwupd_modify_source_ready_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GTask) task = user_data;
+ GsPluginFwupd *self = g_task_get_source_object (task);
+ GsApp *repository = g_task_get_task_data (task);
+
+ if (!fwupd_client_modify_remote_finish (FWUPD_CLIENT (source_object), result, &local_error)) {
+ gs_app_set_state_recover (repository);
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ if (gs_app_get_state (repository) == GS_APP_STATE_INSTALLING)
+ gs_app_set_state (repository, GS_APP_STATE_INSTALLED);
+ else if (gs_app_get_state (repository) == GS_APP_STATE_REMOVING)
+ gs_app_set_state (repository, GS_APP_STATE_AVAILABLE);
+
+ gs_plugin_repository_changed (GS_PLUGIN (self), repository);
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+gs_plugin_fwupd_modify_source_async (GsPluginFwupd *self,
+ GsApp *repository,
+ gboolean enabled,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = NULL;
+ const gchar *remote_id;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_task_data (task, g_object_ref (repository), g_object_unref);
+ g_task_set_source_tag (task, gs_plugin_fwupd_modify_source_async);
+
+ if (!gs_app_has_management_plugin (repository, GS_PLUGIN (self))) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ /* source -> remote */
+ g_assert (gs_app_get_kind (repository) == AS_COMPONENT_KIND_REPOSITORY);
+
+ remote_id = gs_app_get_metadata_item (repository, "fwupd::remote-id");
+ if (remote_id == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "not enough data for fwupd %s",
+ gs_app_get_unique_id (repository));
+ return;
+ }
+ gs_app_set_state (repository, enabled ?
+ GS_APP_STATE_INSTALLING : GS_APP_STATE_REMOVING);
+ fwupd_client_modify_remote_async (self->client,
+ remote_id,
+ "Enabled",
+ enabled ? "true" : "false",
+ cancellable,
+ gs_plugin_fwupd_modify_source_ready_cb,
+ g_steal_pointer (&task));
+}
+
+static gboolean
+gs_plugin_fwupd_modify_source_finish (GsPluginFwupd *self,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+gboolean
+gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (app, plugin))
+ return TRUE;
+
+ /* source -> remote, handled by dedicated function */
+ g_return_val_if_fail (gs_app_get_kind (app) != AS_COMPONENT_KIND_REPOSITORY, FALSE);
+
+ /* firmware */
+ return gs_plugin_fwupd_install (self, app, cancellable, error);
+}
+
+gboolean
+gs_plugin_download_app (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ GFile *local_file;
+ g_autofree gchar *filename = NULL;
+ gpointer schedule_entry_handle = NULL;
+ g_autoptr(GError) error_local = NULL;
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (app, plugin))
+ return TRUE;
+
+ /* not set */
+ local_file = gs_app_get_local_file (app);
+ if (local_file == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "not enough data for fwupd %s",
+ filename);
+ return FALSE;
+ }
+
+ /* file does not yet exist */
+ filename = g_file_get_path (local_file);
+ if (!g_file_query_exists (local_file, cancellable)) {
+ const gchar *uri = gs_fwupd_app_get_update_uri (app);
+ g_autoptr(GFile) file = g_file_new_for_path (filename);
+ gboolean download_success;
+
+ if (!gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE)) {
+ if (!gs_metered_block_app_on_download_scheduler (app, &schedule_entry_handle, cancellable, &error_local)) {
+ g_warning ("Failed to block on download scheduler: %s",
+ error_local->message);
+ g_clear_error (&error_local);
+ }
+ }
+
+ download_success = fwupd_client_download_file (self->client,
+ uri, file,
+ FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
+ cancellable,
+ error);
+ if (!download_success)
+ gs_plugin_fwupd_error_convert (error);
+
+ if (!gs_metered_remove_from_download_scheduler (schedule_entry_handle, NULL, &error_local))
+ g_warning ("Failed to remove schedule entry: %s", error_local->message);
+
+ if (!download_success)
+ return FALSE;
+ }
+ gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
+ return TRUE;
+}
+
+gboolean
+gs_plugin_update_app (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (app, plugin))
+ return TRUE;
+
+ /* locked devices need unlocking, rather than installing */
+ if (gs_fwupd_app_get_is_locked (app)) {
+ const gchar *device_id;
+ device_id = gs_fwupd_app_get_device_id (app);
+ if (device_id == NULL) {
+ g_set_error_literal (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ "not enough data for fwupd unlock");
+ return FALSE;
+ }
+ if (!fwupd_client_unlock (self->client, device_id,
+ cancellable, error)) {
+ gs_plugin_fwupd_error_convert (error);
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /* update means install */
+ if (!gs_plugin_fwupd_install (self, app, cancellable, error)) {
+ gs_plugin_fwupd_error_convert (error);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_file_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autofree gchar *content_type = NULL;
+ g_autofree gchar *filename = NULL;
+ g_autoptr(GPtrArray) devices = NULL;
+ const gchar *mimetypes[] = {
+ "application/vnd.ms-cab-compressed",
+ NULL };
+
+ /* does this match any of the mimetypes we support */
+ content_type = gs_utils_get_content_type (file, cancellable, error);
+ if (content_type == NULL)
+ return FALSE;
+ if (!g_strv_contains (mimetypes, content_type))
+ return TRUE;
+
+ /* get results */
+ filename = g_file_get_path (file);
+ devices = fwupd_client_get_details (self->client,
+ filename,
+ cancellable,
+ error);
+ if (devices == NULL) {
+ gs_plugin_fwupd_error_convert (error);
+ return FALSE;
+ }
+ for (guint i = 0; i < devices->len; i++) {
+ FwupdDevice *dev = g_ptr_array_index (devices, i);
+ g_autoptr(GsApp) app = NULL;
+
+ /* create each app */
+ app = gs_plugin_fwupd_new_app_from_device (plugin, dev);
+
+ /* we *might* have no update view for local files */
+ gs_app_set_version (app, gs_app_get_update_version (app));
+ gs_app_set_description (app, GS_APP_QUALITY_LOWEST,
+ gs_app_get_update_details_markup (app));
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+gboolean
+gs_plugin_add_sources (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autoptr(GPtrArray) remotes = NULL;
+
+ /* find all remotes */
+ remotes = fwupd_client_get_remotes (self->client, cancellable, error);
+ if (remotes == NULL)
+ return FALSE;
+ for (guint i = 0; i < remotes->len; i++) {
+ FwupdRemote *remote = g_ptr_array_index (remotes, i);
+ g_autofree gchar *id = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* ignore these, they're built in */
+ if (fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_DOWNLOAD)
+ continue;
+
+ /* create something that we can use to enable/disable */
+ id = g_strdup_printf ("org.fwupd.%s.remote", fwupd_remote_get_id (remote));
+ app = gs_app_new (id);
+ gs_app_set_kind (app, AS_COMPONENT_KIND_REPOSITORY);
+ gs_app_set_scope (app, AS_COMPONENT_SCOPE_SYSTEM);
+ gs_app_set_state (app, fwupd_remote_get_enabled (remote) ?
+ GS_APP_STATE_INSTALLED : GS_APP_STATE_AVAILABLE);
+ gs_app_add_quirk (app, GS_APP_QUIRK_NOT_LAUNCHABLE);
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST,
+ fwupd_remote_get_title (remote));
+ gs_app_set_agreement (app, fwupd_remote_get_agreement (remote));
+ gs_app_set_url (app, AS_URL_KIND_HOMEPAGE,
+ fwupd_remote_get_metadata_uri (remote));
+ gs_app_set_metadata (app, "fwupd::remote-id",
+ fwupd_remote_get_id (remote));
+ gs_app_set_management_plugin (app, plugin);
+ gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", "fwupd");
+ gs_app_set_metadata (app, "GnomeSoftware::SortKey", "800");
+ gs_app_set_origin_ui (app, _("Firmware"));
+ gs_app_list_add (list, app);
+ }
+ return TRUE;
+}
+
+static void
+gs_plugin_fwupd_enable_repository_remote_refresh_ready_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) local_error = NULL;
+
+ if (!fwupd_client_refresh_remote_finish (FWUPD_CLIENT (source_object), result, &local_error))
+ g_debug ("Failed to refresh remote after enable: %s", local_error ? local_error->message : "Unknown error");
+
+ /* Silently ignore refresh errors */
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+gs_plugin_fwupd_enable_repository_get_remotes_ready_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) local_error = NULL;
+ g_autoptr(GPtrArray) remotes = NULL;
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (g_task_get_source_object (task));
+ GsApp *repository = g_task_get_task_data (task);
+ const gchar *remote_id;
+ guint cache_age = 1;
+
+ remotes = fwupd_client_get_remotes_finish (FWUPD_CLIENT (source_object), result, &local_error);
+ if (remotes == NULL) {
+ g_debug ("No remotes found after remote enable: %s", local_error ? local_error->message : "Unknown error");
+ /* Silently ignore refresh errors */
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ remote_id = gs_app_get_metadata_item (repository, "fwupd::remote-id");
+ g_assert (remote_id != NULL);
+
+ for (guint i = 0; i < remotes->len; i++) {
+ FwupdRemote *remote = g_ptr_array_index (remotes, i);
+ if (g_strcmp0 (remote_id, fwupd_remote_get_id (remote)) == 0) {
+ if (fwupd_remote_get_enabled (remote) &&
+ fwupd_remote_get_kind (remote) != FWUPD_REMOTE_KIND_LOCAL &&
+ !remote_cache_is_expired (remote, cache_age)) {
+ GCancellable *cancellable = g_task_get_cancellable (task);
+ fwupd_client_refresh_remote_async (self->client, remote, cancellable,
+ gs_plugin_fwupd_enable_repository_remote_refresh_ready_cb,
+ g_steal_pointer (&task));
+ return;
+ }
+ break;
+ }
+ }
+
+ g_task_return_boolean (task, TRUE);
+}
+
+static void
+gs_plugin_fwupd_enable_repository_ready_cb (GObject *source_object,
+ GAsyncResult *result,
+ gpointer user_data)
+{
+ g_autoptr(GTask) task = user_data;
+ g_autoptr(GError) local_error = NULL;
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (g_task_get_source_object (task));
+ GCancellable *cancellable = g_task_get_cancellable (task);
+
+ if (!gs_plugin_fwupd_modify_source_finish (self, result, &local_error)) {
+ g_task_return_error (task, g_steal_pointer (&local_error));
+ return;
+ }
+
+ /* This can fail silently, it's only to update necessary caches, to provide
+ * up-to-date information after the successful repository enable/install. */
+ fwupd_client_get_remotes_async (self->client,
+ cancellable,
+ gs_plugin_fwupd_enable_repository_get_remotes_ready_cb,
+ g_steal_pointer (&task));
+}
+
+static void
+gs_plugin_fwupd_enable_repository_async (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginManageRepositoryFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_task_data (task, g_object_ref (repository), g_object_unref);
+ g_task_set_source_tag (task, gs_plugin_fwupd_enable_repository_async);
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (repository, plugin)) {
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ gs_plugin_fwupd_modify_source_async (self, repository, TRUE, cancellable,
+ gs_plugin_fwupd_enable_repository_ready_cb, g_steal_pointer (&task));
+}
+
+static gboolean
+gs_plugin_fwupd_enable_repository_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+gs_plugin_fwupd_disable_repository_async (GsPlugin *plugin,
+ GsApp *repository,
+ GsPluginManageRepositoryFlags flags,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+
+ /* only process this app if was created by this plugin */
+ if (!gs_app_has_management_plugin (repository, plugin)) {
+ g_autoptr(GTask) task = NULL;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_fwupd_disable_repository_async);
+ g_task_return_boolean (task, TRUE);
+ return;
+ }
+
+ gs_plugin_fwupd_modify_source_async (self, repository, FALSE, cancellable, callback, user_data);
+}
+
+static gboolean
+gs_plugin_fwupd_disable_repository_finish (GsPlugin *plugin,
+ GAsyncResult *result,
+ GError **error)
+{
+ GsPluginFwupd *self = GS_PLUGIN_FWUPD (plugin);
+ return gs_plugin_fwupd_modify_source_finish (self, result, error);
+}
+
+static void
+gs_plugin_fwupd_class_init (GsPluginFwupdClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
+
+ object_class->dispose = gs_plugin_fwupd_dispose;
+
+ plugin_class->setup_async = gs_plugin_fwupd_setup_async;
+ plugin_class->setup_finish = gs_plugin_fwupd_setup_finish;
+ plugin_class->refresh_metadata_async = gs_plugin_fwupd_refresh_metadata_async;
+ plugin_class->refresh_metadata_finish = gs_plugin_fwupd_refresh_metadata_finish;
+ plugin_class->enable_repository_async = gs_plugin_fwupd_enable_repository_async;
+ plugin_class->enable_repository_finish = gs_plugin_fwupd_enable_repository_finish;
+ plugin_class->disable_repository_async = gs_plugin_fwupd_disable_repository_async;
+ plugin_class->disable_repository_finish = gs_plugin_fwupd_disable_repository_finish;
+}
+
+GType
+gs_plugin_query_type (void)
+{
+ return GS_TYPE_PLUGIN_FWUPD;
+}
diff --git a/plugins/fwupd/gs-plugin-fwupd.h b/plugins/fwupd/gs-plugin-fwupd.h
new file mode 100644
index 0000000..6114814
--- /dev/null
+++ b/plugins/fwupd/gs-plugin-fwupd.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2021 Endless OS Foundation LLC
+ *
+ * Author: Philip Withnall <pwithnall@endlessos.org>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib.h>
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_FWUPD (gs_plugin_fwupd_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginFwupd, gs_plugin_fwupd, GS, PLUGIN_FWUPD, GsPlugin)
+
+G_END_DECLS
diff --git a/plugins/fwupd/gs-self-test.c b/plugins/fwupd/gs-self-test.c
new file mode 100644
index 0000000..f02a9f0
--- /dev/null
+++ b/plugins/fwupd/gs-self-test.c
@@ -0,0 +1,111 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <fwupd.h>
+
+#include "gnome-software-private.h"
+
+#include "gs-test.h"
+
+static void
+gs_plugins_fwupd_func (GsPluginLoader *plugin_loader)
+{
+ g_autofree gchar *fn = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsSizeType size_download_type;
+ guint64 size_download_bytes;
+
+ /* no fwupd, abort */
+ if (!gs_plugin_loader_get_enabled (plugin_loader, "fwupd")) {
+ g_test_skip ("not enabled");
+ return;
+ }
+
+ /* load local file */
+ fn = gs_test_get_filename (TESTDATADIR, "chiron-0.2.cab");
+ g_assert_nonnull (fn);
+ file = g_file_new_for_path (fn);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ NULL);
+ app = gs_plugin_loader_job_process_app (plugin_loader, plugin_job, NULL, &error);
+ gs_test_flush_main_context ();
+ g_assert_no_error (error);
+ g_assert_nonnull (app);
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_FIRMWARE);
+ g_assert_nonnull (gs_app_get_license (app));
+ g_assert_true (gs_app_has_category (app, "System"));
+ g_assert_cmpstr (gs_app_get_id (app), ==, "com.test.chiron.firmware");
+ g_assert_cmpstr (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE), ==, "http://127.0.0.1/");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "Chiron");
+ g_assert_cmpstr (gs_app_get_summary (app), ==, "Single line synopsis");
+ g_assert_cmpstr (gs_app_get_version (app), ==, "0.2");
+ size_download_type = gs_app_get_size_download (app, &size_download_bytes);
+ g_assert_cmpint (size_download_type, ==, GS_SIZE_TYPE_VALID);
+ g_assert_cmpuint (size_download_bytes, ==, 32784);
+ g_assert_cmpstr (gs_app_get_description (app), ==,
+ "This is the first paragraph in the example "
+ "cab file.\n\nThis is the second paragraph.");
+#if FWUPD_CHECK_VERSION(1, 7, 1) && !FWUPD_CHECK_VERSION(1, 8, 0)
+ /* Changes introduced in fwupd commit d3706e0e0b0fc210796da839b84ac391f7a251f8 and
+ removed for 1.8.0 with https://github.com/fwupd/fwupd/commit/0eeaad76ec79562ea3790bb377d847d5be02182f */
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==,
+ "Some of the platform secrets may be invalidated when "
+ "updating this firmware. Please ensure you have the "
+ "volume recovery key before continuing.\n\nLatest "
+ "firmware release.");
+#else
+ g_assert_cmpstr (gs_app_get_update_details_markup (app), ==,
+ "Latest firmware release.");
+#endif
+
+ /* seems wrong, but this is only set if the update is available */
+ g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_UNKNOWN);
+}
+
+int
+main (int argc, char **argv)
+{
+ gboolean ret;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsPluginLoader) plugin_loader = NULL;
+ const gchar * const allowlist[] = {
+ "fwupd",
+ NULL
+ };
+
+ /* While we use %G_TEST_OPTION_ISOLATE_DIRS to create temporary directories
+ * for each of the tests, we want to use the system MIME registry, assuming
+ * that it exists and correctly has shared-mime-info installed. */
+ g_content_type_set_mime_dirs (NULL);
+
+ gs_test_init (&argc, &argv);
+
+ /* we can only load this once per process */
+ plugin_loader = gs_plugin_loader_new (NULL, NULL);
+ gs_plugin_loader_add_location (plugin_loader, LOCALPLUGINDIR);
+ ret = gs_plugin_loader_setup (plugin_loader,
+ allowlist,
+ NULL,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+ g_assert_true (ret);
+
+ /* plugin tests go here */
+ g_test_add_data_func ("/gnome-software/plugins/fwupd",
+ plugin_loader,
+ (GTestDataFunc) gs_plugins_fwupd_func);
+
+ return g_test_run ();
+}
diff --git a/plugins/fwupd/meson.build b/plugins/fwupd/meson.build
new file mode 100644
index 0000000..8ed030b
--- /dev/null
+++ b/plugins/fwupd/meson.build
@@ -0,0 +1,54 @@
+cargs = ['-DG_LOG_DOMAIN="GsPluginFwupd"']
+cargs += ['-DLOCALPLUGINDIR="' + meson.current_build_dir() + '"']
+deps = [
+ plugin_libs,
+ fwupd,
+]
+
+if get_option('mogwai')
+ deps += [mogwai_schedule_client]
+endif
+
+shared_module(
+ 'gs_plugin_fwupd',
+ sources : [
+ 'gs-fwupd-app.c',
+ 'gs-plugin-fwupd.c',
+ ],
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ install : true,
+ install_dir: plugin_dir,
+ c_args : cargs,
+ dependencies : deps,
+)
+metainfo = 'org.gnome.Software.Plugin.Fwupd.metainfo.xml'
+
+i18n.merge_file(
+ input: metainfo + '.in',
+ output: metainfo,
+ type: 'xml',
+ po_dir: join_paths(meson.project_source_root(), 'po'),
+ install: true,
+ install_dir: join_paths(get_option('datadir'), 'metainfo')
+)
+
+if get_option('tests')
+ cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), 'tests') + '"']
+ e = executable(
+ 'gs-self-test-fwupd',
+ compiled_schemas,
+ sources : [
+ 'gs-self-test.c'
+ ],
+ include_directories : [
+ include_directories('../..'),
+ include_directories('../../lib'),
+ ],
+ dependencies : deps,
+ c_args : cargs,
+ )
+ test('gs-self-test-fwupd', e, suite: ['plugins', 'fwupd'], env: test_env)
+endif
diff --git a/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in b/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in
new file mode 100644
index 0000000..3f2067d
--- /dev/null
+++ b/plugins/fwupd/org.gnome.Software.Plugin.Fwupd.metainfo.xml.in
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2013-2016 Richard Hughes <richard@hughsie.com> -->
+<component type="addon">
+ <id>org.gnome.Software.Plugin.Fwupd</id>
+ <extends>org.gnome.Software.desktop</extends>
+ <name>Firmware Upgrade Support</name>
+ <summary>Provides support for firmware upgrades</summary>
+ <url type="homepage">http://www.fwupd.org</url>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <update_contact>richard_at_hughsie.com</update_contact>
+</component>
diff --git a/plugins/fwupd/tests/build-cab.sh b/plugins/fwupd/tests/build-cab.sh
new file mode 100755
index 0000000..ea7bed6
--- /dev/null
+++ b/plugins/fwupd/tests/build-cab.sh
@@ -0,0 +1,4 @@
+gcab --create chiron-0.2.cab \
+ firmware.dfu \
+ firmware.dfu.asc \
+ firmware.metainfo.xml
diff --git a/plugins/fwupd/tests/chiron-0.2.cab b/plugins/fwupd/tests/chiron-0.2.cab
new file mode 100644
index 0000000..6618361
--- /dev/null
+++ b/plugins/fwupd/tests/chiron-0.2.cab
Binary files differ
diff --git a/plugins/fwupd/tests/firmware.dfu b/plugins/fwupd/tests/firmware.dfu
new file mode 100644
index 0000000..50f00c0
--- /dev/null
+++ b/plugins/fwupd/tests/firmware.dfu
Binary files differ
diff --git a/plugins/fwupd/tests/firmware.dfu.asc b/plugins/fwupd/tests/firmware.dfu.asc
new file mode 100644
index 0000000..0ea79a7
--- /dev/null
+++ b/plugins/fwupd/tests/firmware.dfu.asc
@@ -0,0 +1,11 @@
+-----BEGIN PGP SIGNATURE-----
+Version: GnuPG v2.0.14 (GNU/Linux)
+
+iQEcBAABAgAGBQJWUy6EAAoJEEim2A5FOLrCxokIAJJtLVkuXZHgEu2C2Eq9jGrR
+FZ9/z9XtsMgs33teLdmYUAQwvnNIbtIb6z7JViP8llCREP8y2fH+1OjrOOdtuS/A
+bIJ0r40c9wYeH97ZcXBdHZiYVEFO+etbMBUg5ifuRO5VPjD9H1NqL05Wx9kUg/1T
+a1fwgHopXR0T4jYcg5aijp3mdgfmg4boIklDaRV/g2c93W+0VhDZ2h5sKwBxxlFS
+TrptclTMCvRYmVvL1CDOsBtgzu3jGo03wV9rcnSKzeBWvINcvlRLdS0ejlPaRYDK
+MUY4MBVz3fDW1vFsqLpU80XMOYk0bxtQqQ2MsrlXWp9qazB+A6mC7kOnJQfx0yI=
+=A3W8
+-----END PGP SIGNATURE-----
diff --git a/plugins/fwupd/tests/firmware.metainfo.xml b/plugins/fwupd/tests/firmware.metainfo.xml
new file mode 100644
index 0000000..d942fd8
--- /dev/null
+++ b/plugins/fwupd/tests/firmware.metainfo.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Richard Hughes <richard@hughsie.com> -->
+<component type="firmware">
+ <id>com.test.chiron.firmware</id>
+ <name>Chiron</name>
+ <summary>Single line synopsis</summary>
+ <description>
+ <p>
+ This is the first paragraph in the example cab file.
+ </p>
+ <p>
+ This is the second paragraph.
+ </p>
+ </description>
+ <provides>
+ <firmware type="flashed">fd9f37b4-36fb-5245-86a8-4d5993bb153b</firmware>
+ </provides>
+ <url type="homepage">http://127.0.0.1/</url>
+ <metadata_license>CC0-1.0</metadata_license>
+ <project_license>GPL-2.0+</project_license>
+ <developer_name>ACME Corp</developer_name>
+ <releases>
+ <release urgency="medium" version="0.2" timestamp="1447353015">
+ <checksum target="content" filename="firmware.dfu"/>
+ <description>
+ <p>Latest firmware release.</p>
+ </description>
+ </release>
+ </releases>
+</component>