mirror of
https://git.proxmox.com/git/fwupd
synced 2025-06-26 02:53:00 +00:00
572 lines
16 KiB
C
572 lines
16 KiB
C
/*
|
|
* Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
|
|
*
|
|
* SPDX-License-Identifier: LGPL-2.1+
|
|
*/
|
|
|
|
#define G_LOG_DOMAIN "FuRemoteList"
|
|
|
|
#include "config.h"
|
|
|
|
#include <glib/gi18n.h>
|
|
#include <xmlb.h>
|
|
|
|
#ifdef HAVE_INOTIFY_H
|
|
#include <errno.h>
|
|
#include <sys/inotify.h>
|
|
#endif
|
|
|
|
#include "fwupd-remote-private.h"
|
|
|
|
#include "fu-remote-list.h"
|
|
|
|
enum { SIGNAL_CHANGED, SIGNAL_LAST };
|
|
|
|
static guint signals[SIGNAL_LAST] = {0};
|
|
|
|
static void
|
|
fu_remote_list_finalize(GObject *obj);
|
|
|
|
struct _FuRemoteList {
|
|
GObject parent_instance;
|
|
GPtrArray *array; /* (element-type FwupdRemote) */
|
|
GPtrArray *monitors; /* (element-type GFileMonitor) */
|
|
GHashTable *hash_unfound; /* utf8 : NULL */
|
|
XbSilo *silo;
|
|
};
|
|
|
|
G_DEFINE_TYPE(FuRemoteList, fu_remote_list, G_TYPE_OBJECT)
|
|
|
|
static void
|
|
fu_remote_list_emit_changed(FuRemoteList *self)
|
|
{
|
|
g_debug("::remote_list changed");
|
|
g_signal_emit(self, signals[SIGNAL_CHANGED], 0);
|
|
}
|
|
|
|
static void
|
|
fu_remote_list_monitor_changed_cb(GFileMonitor *monitor,
|
|
GFile *file,
|
|
GFile *other_file,
|
|
GFileMonitorEvent event_type,
|
|
gpointer user_data)
|
|
{
|
|
FuRemoteList *self = FU_REMOTE_LIST(user_data);
|
|
g_autoptr(GError) error = NULL;
|
|
g_autofree gchar *filename = g_file_get_path(file);
|
|
g_debug("%s changed, reloading all remotes", filename);
|
|
if (!fu_remote_list_reload(self, &error))
|
|
g_warning("failed to rescan remotes: %s", error->message);
|
|
fu_remote_list_emit_changed(self);
|
|
}
|
|
|
|
static guint64
|
|
_fwupd_remote_get_mtime(FwupdRemote *remote)
|
|
{
|
|
g_autoptr(GFile) file = NULL;
|
|
g_autoptr(GFileInfo) info = NULL;
|
|
|
|
file = g_file_new_for_path(fwupd_remote_get_filename_cache(remote));
|
|
if (!g_file_query_exists(file, NULL))
|
|
return G_MAXUINT64;
|
|
info = g_file_query_info(file,
|
|
G_FILE_ATTRIBUTE_TIME_MODIFIED,
|
|
G_FILE_QUERY_INFO_NONE,
|
|
NULL,
|
|
NULL);
|
|
if (info == NULL)
|
|
return G_MAXUINT64;
|
|
return g_file_info_get_attribute_uint64(info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
|
|
}
|
|
|
|
/* GLib only returns the very unhelpful "Unable to find default local file monitor type"
|
|
* when /proc/sys/fs/inotify/max_user_instances is set too low; detect this and set a proper
|
|
* error prefix to aid debugging when the daemon fails to start */
|
|
static void
|
|
fu_remote_list_fixup_inotify_error(GError **error)
|
|
{
|
|
#ifdef HAVE_INOTIFY_H
|
|
int fd;
|
|
int wd;
|
|
const gchar *fn = "/proc/sys/fs/inotify/max_user_instances";
|
|
|
|
fd = inotify_init();
|
|
if (fd == -1) {
|
|
g_prefix_error(error, "Could not initialize inotify, check %s: ", fn);
|
|
return;
|
|
}
|
|
wd = inotify_add_watch(fd, "/", 0);
|
|
if (wd < 0) {
|
|
if (errno == ENOSPC)
|
|
g_prefix_error(error, "No space for inotify, check %s: ", fn);
|
|
} else {
|
|
inotify_rm_watch(fd, wd);
|
|
}
|
|
close(fd);
|
|
#endif
|
|
}
|
|
|
|
static gboolean
|
|
fu_remote_list_add_inotify(FuRemoteList *self, const gchar *filename, GError **error)
|
|
{
|
|
GFileMonitor *monitor;
|
|
g_autoptr(GFile) file = g_file_new_for_path(filename);
|
|
|
|
/* set up a notify watch */
|
|
monitor = g_file_monitor(file, G_FILE_MONITOR_NONE, NULL, error);
|
|
if (monitor == NULL) {
|
|
fu_remote_list_fixup_inotify_error(error);
|
|
return FALSE;
|
|
}
|
|
g_signal_connect(G_FILE_MONITOR(monitor),
|
|
"changed",
|
|
G_CALLBACK(fu_remote_list_monitor_changed_cb),
|
|
self);
|
|
g_ptr_array_add(self->monitors, monitor);
|
|
return TRUE;
|
|
}
|
|
|
|
static GString *
|
|
_fwupd_remote_get_agreement_default(FwupdRemote *self, GError **error)
|
|
{
|
|
GString *str = g_string_new(NULL);
|
|
|
|
/* this is designed as a fallback; the actual warning should ideally
|
|
* come from the LVFS instance that is serving the remote */
|
|
g_string_append_printf(str,
|
|
"<p>%s</p>",
|
|
/* TRANSLATORS: show the user a warning */
|
|
_("Your distributor may not have verified any of "
|
|
"the firmware updates for compatibility with your "
|
|
"system or connected devices."));
|
|
g_string_append_printf(str,
|
|
"<p>%s</p>",
|
|
/* TRANSLATORS: show the user a warning */
|
|
_("Enabling this remote is done at your own risk."));
|
|
return str;
|
|
}
|
|
|
|
static GString *
|
|
_fwupd_remote_get_agreement_for_app(FwupdRemote *self, XbNode *component, GError **error)
|
|
{
|
|
g_autofree gchar *tmp = NULL;
|
|
g_autoptr(GError) error_local = NULL;
|
|
g_autoptr(XbNode) n = NULL;
|
|
|
|
/* manually find the first agreement section */
|
|
n = xb_node_query_first(component,
|
|
"agreement/agreement_section/description/*",
|
|
&error_local);
|
|
if (n == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_FOUND,
|
|
"No agreement description found: %s",
|
|
error_local->message);
|
|
return NULL;
|
|
}
|
|
tmp = xb_node_export(n, XB_NODE_EXPORT_FLAG_INCLUDE_SIBLINGS, error);
|
|
if (tmp == NULL)
|
|
return NULL;
|
|
return g_string_new(tmp);
|
|
}
|
|
|
|
static gchar *
|
|
_fwupd_remote_build_component_id(FwupdRemote *remote)
|
|
{
|
|
return g_strdup_printf("org.freedesktop.fwupd.remotes.%s", fwupd_remote_get_id(remote));
|
|
}
|
|
|
|
static gboolean
|
|
fu_remote_list_add_for_path(FuRemoteList *self, const gchar *path, GError **error)
|
|
{
|
|
const gchar *tmp;
|
|
g_autofree gchar *path_remotes = NULL;
|
|
g_autoptr(GDir) dir = NULL;
|
|
g_autoptr(GHashTable) os_release = NULL;
|
|
|
|
path_remotes = g_build_filename(path, "remotes.d", NULL);
|
|
if (!g_file_test(path_remotes, G_FILE_TEST_EXISTS)) {
|
|
g_debug("path %s does not exist", path_remotes);
|
|
return TRUE;
|
|
}
|
|
if (!fu_remote_list_add_inotify(self, path_remotes, error))
|
|
return FALSE;
|
|
dir = g_dir_open(path_remotes, 0, error);
|
|
if (dir == NULL)
|
|
return FALSE;
|
|
os_release = fwupd_get_os_release(error);
|
|
if (os_release == NULL)
|
|
return FALSE;
|
|
while ((tmp = g_dir_read_name(dir)) != NULL) {
|
|
g_autofree gchar *filename = g_build_filename(path_remotes, tmp, NULL);
|
|
g_autoptr(FwupdRemote) remote = fwupd_remote_new();
|
|
g_autofree gchar *remotesdir = NULL;
|
|
|
|
/* skip invalid files */
|
|
if (!g_str_has_suffix(tmp, ".conf")) {
|
|
g_debug("skipping invalid file %s", filename);
|
|
continue;
|
|
}
|
|
|
|
/* set directory to store data */
|
|
remotesdir = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_METADATA);
|
|
fwupd_remote_set_remotes_dir(remote, remotesdir);
|
|
|
|
/* load from keyfile */
|
|
g_debug("loading remote from %s", filename);
|
|
if (!fwupd_remote_load_from_filename(remote, filename, NULL, error)) {
|
|
g_prefix_error(error, "failed to load %s: ", filename);
|
|
return FALSE;
|
|
}
|
|
if (!fwupd_remote_setup(remote, error)) {
|
|
g_prefix_error(error, "failed to setup %s: ", filename);
|
|
return FALSE;
|
|
}
|
|
|
|
/* watch the remote_list file and the XML file itself */
|
|
if (!fu_remote_list_add_inotify(self, filename, error))
|
|
return FALSE;
|
|
if (!fu_remote_list_add_inotify(self,
|
|
fwupd_remote_get_filename_cache(remote),
|
|
error))
|
|
return FALSE;
|
|
|
|
/* try to find a custom agreement, falling back to a generic warning */
|
|
if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD) {
|
|
g_autoptr(GString) agreement_markup = NULL;
|
|
g_autofree gchar *component_id = _fwupd_remote_build_component_id(remote);
|
|
g_autoptr(XbNode) component = NULL;
|
|
g_autofree gchar *xpath = NULL;
|
|
|
|
xpath = g_strdup_printf("component/id[text()='%s']/..", component_id);
|
|
component = xb_silo_query_first(self->silo, xpath, NULL);
|
|
if (component != NULL) {
|
|
agreement_markup =
|
|
_fwupd_remote_get_agreement_for_app(remote, component, error);
|
|
} else {
|
|
agreement_markup =
|
|
_fwupd_remote_get_agreement_default(remote, error);
|
|
}
|
|
if (agreement_markup == NULL)
|
|
return FALSE;
|
|
|
|
/* replace any dynamic values from os-release */
|
|
tmp = g_hash_table_lookup(os_release, "NAME");
|
|
if (tmp == NULL)
|
|
tmp = "this distribution";
|
|
fu_string_replace(agreement_markup, "$OS_RELEASE:NAME$", tmp);
|
|
tmp = g_hash_table_lookup(os_release, "BUG_REPORT_URL");
|
|
if (tmp == NULL)
|
|
tmp = "https://github.com/fwupd/fwupd/issues";
|
|
fu_string_replace(agreement_markup, "$OS_RELEASE:BUG_REPORT_URL$", tmp);
|
|
fwupd_remote_set_agreement(remote, agreement_markup->str);
|
|
}
|
|
|
|
/* set mtime */
|
|
fwupd_remote_set_mtime(remote, _fwupd_remote_get_mtime(remote));
|
|
g_ptr_array_add(self->array, g_steal_pointer(&remote));
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
fu_remote_list_set_key_value(FuRemoteList *self,
|
|
const gchar *remote_id,
|
|
const gchar *key,
|
|
const gchar *value,
|
|
GError **error)
|
|
{
|
|
FwupdRemote *remote;
|
|
const gchar *filename;
|
|
g_autofree gchar *value_old = NULL;
|
|
g_autoptr(GKeyFile) keyfile = g_key_file_new();
|
|
|
|
/* check remote is valid */
|
|
remote = fu_remote_list_get_by_id(self, remote_id);
|
|
if (remote == NULL) {
|
|
g_set_error(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_NOT_FOUND,
|
|
"remote %s not found",
|
|
remote_id);
|
|
return FALSE;
|
|
}
|
|
|
|
/* modify the remote */
|
|
filename = fwupd_remote_get_filename_source(remote);
|
|
if (!g_key_file_load_from_file(keyfile, filename, G_KEY_FILE_KEEP_COMMENTS, error)) {
|
|
g_prefix_error(error, "failed to load %s: ", filename);
|
|
return FALSE;
|
|
}
|
|
value_old = g_key_file_get_string(keyfile, "fwupd Remote", key, NULL);
|
|
if (g_strcmp0(value_old, value) == 0)
|
|
return TRUE;
|
|
g_key_file_set_string(keyfile, "fwupd Remote", key, value);
|
|
if (!g_key_file_save_to_file(keyfile, filename, error))
|
|
return FALSE;
|
|
|
|
/* reload values */
|
|
if (!fwupd_remote_load_from_filename(remote, filename, NULL, error)) {
|
|
g_prefix_error(error, "failed to load %s: ", filename);
|
|
return FALSE;
|
|
}
|
|
fu_remote_list_emit_changed(self);
|
|
return TRUE;
|
|
}
|
|
|
|
static gint
|
|
fu_remote_list_sort_cb(gconstpointer a, gconstpointer b)
|
|
{
|
|
FwupdRemote *remote_a = *((FwupdRemote **)a);
|
|
FwupdRemote *remote_b = *((FwupdRemote **)b);
|
|
|
|
/* use priority first */
|
|
if (fwupd_remote_get_priority(remote_a) < fwupd_remote_get_priority(remote_b))
|
|
return 1;
|
|
if (fwupd_remote_get_priority(remote_a) > fwupd_remote_get_priority(remote_b))
|
|
return -1;
|
|
|
|
/* fall back to name */
|
|
return g_strcmp0(fwupd_remote_get_id(remote_a), fwupd_remote_get_id(remote_b));
|
|
}
|
|
|
|
static guint
|
|
fu_remote_list_depsolve_with_direction(FuRemoteList *self, gint inc)
|
|
{
|
|
guint cnt = 0;
|
|
|
|
for (guint i = 0; i < self->array->len; i++) {
|
|
FwupdRemote *remote = g_ptr_array_index(self->array, i);
|
|
gchar **order = inc < 0 ? fwupd_remote_get_order_after(remote)
|
|
: fwupd_remote_get_order_before(remote);
|
|
if (order == NULL)
|
|
continue;
|
|
for (guint j = 0; order[j] != NULL; j++) {
|
|
FwupdRemote *remote2;
|
|
if (g_strcmp0(order[j], fwupd_remote_get_id(remote)) == 0) {
|
|
g_debug("ignoring self-dep remote %s", order[j]);
|
|
continue;
|
|
}
|
|
remote2 = fu_remote_list_get_by_id(self, order[j]);
|
|
if (remote2 == NULL) {
|
|
if (g_hash_table_contains(self->hash_unfound, order[j]))
|
|
continue;
|
|
g_debug("ignoring unfound remote %s", order[j]);
|
|
g_hash_table_insert(self->hash_unfound, g_strdup(order[j]), NULL);
|
|
continue;
|
|
}
|
|
if (fwupd_remote_get_priority(remote) > fwupd_remote_get_priority(remote2))
|
|
continue;
|
|
g_debug("ordering %s=%s+%i",
|
|
fwupd_remote_get_id(remote),
|
|
fwupd_remote_get_id(remote2),
|
|
inc);
|
|
fwupd_remote_set_priority(remote, fwupd_remote_get_priority(remote2) + inc);
|
|
|
|
/* increment changes counter */
|
|
cnt++;
|
|
}
|
|
}
|
|
return cnt;
|
|
}
|
|
|
|
gboolean
|
|
fu_remote_list_reload(FuRemoteList *self, GError **error)
|
|
{
|
|
guint depsolve_check;
|
|
g_autofree gchar *remotesdir = NULL;
|
|
g_autofree gchar *remotesdir_mut = NULL;
|
|
|
|
/* clear */
|
|
g_ptr_array_set_size(self->array, 0);
|
|
g_ptr_array_set_size(self->monitors, 0);
|
|
|
|
/* use sysremotes, and then fall back to /etc */
|
|
remotesdir = fu_path_from_kind(FU_PATH_KIND_SYSCONFDIR_PKG);
|
|
if (!fu_remote_list_add_for_path(self, remotesdir, error))
|
|
return FALSE;
|
|
remotesdir_mut = fu_path_from_kind(FU_PATH_KIND_LOCALSTATEDIR_PKG);
|
|
if (!fu_remote_list_add_for_path(self, remotesdir_mut, error))
|
|
return FALSE;
|
|
|
|
/* depsolve */
|
|
for (depsolve_check = 0; depsolve_check < 100; depsolve_check++) {
|
|
guint cnt = 0;
|
|
cnt += fu_remote_list_depsolve_with_direction(self, 1);
|
|
cnt += fu_remote_list_depsolve_with_direction(self, -1);
|
|
if (cnt == 0)
|
|
break;
|
|
}
|
|
if (depsolve_check == 100) {
|
|
g_set_error_literal(error,
|
|
FWUPD_ERROR,
|
|
FWUPD_ERROR_INTERNAL,
|
|
"Cannot depsolve remotes ordering");
|
|
return FALSE;
|
|
}
|
|
|
|
/* order these by priority, then name */
|
|
g_ptr_array_sort(self->array, fu_remote_list_sort_cb);
|
|
|
|
/* success */
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
fu_remote_list_load_metainfos(XbBuilder *builder, GError **error)
|
|
{
|
|
const gchar *fn;
|
|
g_autofree gchar *datadir = NULL;
|
|
g_autofree gchar *metainfo_path = NULL;
|
|
g_autoptr(GDir) dir = NULL;
|
|
|
|
/* pkg metainfo dir */
|
|
datadir = fu_path_from_kind(FU_PATH_KIND_DATADIR_PKG);
|
|
metainfo_path = g_build_filename(datadir, "metainfo", NULL);
|
|
if (!g_file_test(metainfo_path, G_FILE_TEST_EXISTS))
|
|
return TRUE;
|
|
|
|
g_debug("loading %s", metainfo_path);
|
|
dir = g_dir_open(metainfo_path, 0, error);
|
|
if (dir == NULL)
|
|
return FALSE;
|
|
while ((fn = g_dir_read_name(dir)) != NULL) {
|
|
if (g_str_has_suffix(fn, ".metainfo.xml")) {
|
|
g_autofree gchar *filename = g_build_filename(metainfo_path, fn, NULL);
|
|
g_autoptr(GFile) file = g_file_new_for_path(filename);
|
|
g_autoptr(XbBuilderSource) source = xb_builder_source_new();
|
|
if (!xb_builder_source_load_file(source,
|
|
file,
|
|
XB_BUILDER_SOURCE_FLAG_NONE,
|
|
NULL,
|
|
error))
|
|
return FALSE;
|
|
xb_builder_import_source(builder, source);
|
|
}
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
gboolean
|
|
fu_remote_list_load(FuRemoteList *self, FuRemoteListLoadFlags flags, GError **error)
|
|
{
|
|
const gchar *const *locales = g_get_language_names();
|
|
g_autoptr(GFile) xmlb = NULL;
|
|
g_autoptr(XbBuilder) builder = xb_builder_new();
|
|
XbBuilderCompileFlags compile_flags =
|
|
XB_BUILDER_COMPILE_FLAG_SINGLE_LANG | XB_BUILDER_COMPILE_FLAG_IGNORE_INVALID;
|
|
|
|
g_return_val_if_fail(FU_IS_REMOTE_LIST(self), FALSE);
|
|
g_return_val_if_fail(self->silo == NULL, FALSE);
|
|
|
|
/* load AppStream about the remote_list */
|
|
if (!fu_remote_list_load_metainfos(builder, error))
|
|
return FALSE;
|
|
|
|
/* add the locales, which is really only going to be 'C' or 'en' */
|
|
for (guint i = 0; locales[i] != NULL; i++)
|
|
xb_builder_add_locale(builder, locales[i]);
|
|
|
|
/* on a read-only filesystem don't care about the cache GUID */
|
|
if (flags & FU_REMOTE_LIST_LOAD_FLAG_READONLY_FS)
|
|
compile_flags |= XB_BUILDER_COMPILE_FLAG_IGNORE_GUID;
|
|
|
|
/* build the metainfo silo */
|
|
if (flags & FU_REMOTE_LIST_LOAD_FLAG_NO_CACHE) {
|
|
g_autoptr(GFileIOStream) iostr = NULL;
|
|
xmlb = g_file_new_tmp(NULL, &iostr, error);
|
|
if (xmlb == NULL)
|
|
return FALSE;
|
|
} else {
|
|
g_autofree gchar *cachedirpkg = fu_path_from_kind(FU_PATH_KIND_CACHEDIR_PKG);
|
|
g_autofree gchar *xmlbfn = g_build_filename(cachedirpkg, "metainfo.xmlb", NULL);
|
|
xmlb = g_file_new_for_path(xmlbfn);
|
|
}
|
|
self->silo = xb_builder_ensure(builder, xmlb, compile_flags, NULL, error);
|
|
if (self->silo == NULL)
|
|
return FALSE;
|
|
|
|
/* load remote_list */
|
|
return fu_remote_list_reload(self, error);
|
|
}
|
|
|
|
GPtrArray *
|
|
fu_remote_list_get_all(FuRemoteList *self)
|
|
{
|
|
g_return_val_if_fail(FU_IS_REMOTE_LIST(self), NULL);
|
|
return self->array;
|
|
}
|
|
|
|
FwupdRemote *
|
|
fu_remote_list_get_by_id(FuRemoteList *self, const gchar *remote_id)
|
|
{
|
|
g_return_val_if_fail(FU_IS_REMOTE_LIST(self), NULL);
|
|
for (guint i = 0; i < self->array->len; i++) {
|
|
FwupdRemote *remote = g_ptr_array_index(self->array, i);
|
|
if (g_strcmp0(remote_id, fwupd_remote_get_id(remote)) == 0)
|
|
return remote;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
static void
|
|
fu_remote_list_class_init(FuRemoteListClass *klass)
|
|
{
|
|
GObjectClass *object_class = G_OBJECT_CLASS(klass);
|
|
object_class->finalize = fu_remote_list_finalize;
|
|
|
|
/**
|
|
* FuRemoteList::changed:
|
|
* @self: the #FuRemoteList instance that emitted the signal
|
|
*
|
|
* The ::changed signal is emitted when the list of remotes has
|
|
* changed, for instance when a remote has been added or removed.
|
|
**/
|
|
signals[SIGNAL_CHANGED] = g_signal_new("changed",
|
|
G_TYPE_FROM_CLASS(object_class),
|
|
G_SIGNAL_RUN_LAST,
|
|
0,
|
|
NULL,
|
|
NULL,
|
|
g_cclosure_marshal_VOID__VOID,
|
|
G_TYPE_NONE,
|
|
0);
|
|
}
|
|
|
|
static void
|
|
fu_remote_list_monitor_unref(GFileMonitor *monitor)
|
|
{
|
|
g_file_monitor_cancel(monitor);
|
|
g_object_unref(monitor);
|
|
}
|
|
|
|
static void
|
|
fu_remote_list_init(FuRemoteList *self)
|
|
{
|
|
self->array = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);
|
|
self->monitors =
|
|
g_ptr_array_new_with_free_func((GDestroyNotify)fu_remote_list_monitor_unref);
|
|
self->hash_unfound = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
|
|
}
|
|
|
|
static void
|
|
fu_remote_list_finalize(GObject *obj)
|
|
{
|
|
FuRemoteList *self = FU_REMOTE_LIST(obj);
|
|
if (self->silo != NULL)
|
|
g_object_unref(self->silo);
|
|
g_ptr_array_unref(self->array);
|
|
g_ptr_array_unref(self->monitors);
|
|
g_hash_table_unref(self->hash_unfound);
|
|
G_OBJECT_CLASS(fu_remote_list_parent_class)->finalize(obj);
|
|
}
|
|
|
|
FuRemoteList *
|
|
fu_remote_list_new(void)
|
|
{
|
|
FuRemoteList *self;
|
|
self = g_object_new(FU_TYPE_REMOTE_LIST, NULL);
|
|
return FU_REMOTE_LIST(self);
|
|
}
|