Show translated firmware release notes when provided

Send the users locale to the daemon so that it can be used to prefer
the localized update text over the default en_US version.

    $ LANG=fr_FR.UTF8 fwupdmgr get-details test.cab
    ...
    └─ACME Plan 9:
          Nouvelle version: 0.0.5
          Licence:          Propriétaire
          Urgence:          Faible
          Fournisseur:      ACME Ltd.
          Description:      Cette version stable corrige des bugs.

I decided to send the locale to the daemon rather than change the
`Description` to return GVariant to `a{ss}` as we also probably want
to support things like localized summary and URLs too in the future.
This commit is contained in:
Richard Hughes 2021-10-23 18:03:46 +01:00
parent 3c8339c9eb
commit 70f9124545
8 changed files with 211 additions and 21 deletions

View File

@ -34,6 +34,9 @@
#include "fwupd-request-private.h" #include "fwupd-request-private.h"
#include "fwupd-security-attr-private.h" #include "fwupd-security-attr-private.h"
static void
fwupd_client_fixup_dbus_error(GError *error);
typedef GObject *(*FwupdClientObjectNewFunc)(void); typedef GObject *(*FwupdClientObjectNewFunc)(void);
#define FWUPD_CLIENT_DBUS_PROXY_TIMEOUT 180000 /* ms */ #define FWUPD_CLIENT_DBUS_PROXY_TIMEOUT 180000 /* ms */
@ -66,6 +69,7 @@ typedef struct {
GDBusProxy *proxy; GDBusProxy *proxy;
GProxyResolver *proxy_resolver; GProxyResolver *proxy_resolver;
gchar *user_agent; gchar *user_agent;
GHashTable *hints; /* str:str */
#ifdef SOUP_SESSION_COMPAT #ifdef SOUP_SESSION_COMPAT
GObject *soup_session; GObject *soup_session;
GModule *soup_module; /* we leak this */ GModule *soup_module; /* we leak this */
@ -594,12 +598,40 @@ fwupd_client_curl_new(FwupdClient *self, GError **error)
} }
#endif #endif
static void
fwupd_client_set_hints_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
g_autoptr(GTask) task = G_TASK(user_data);
g_autoptr(GError) error = NULL;
g_autoptr(GVariant) val = NULL;
val = g_dbus_proxy_call_finish(G_DBUS_PROXY(source), res, &error);
if (val == NULL) {
/* new libfwupd and old daemon, just swallow the error */
if (g_error_matches(error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD)) {
g_debug("ignoring %s", error->message);
g_task_return_boolean(task, TRUE);
return;
}
fwupd_client_fixup_dbus_error(error);
g_task_return_error(task, g_steal_pointer(&error));
return;
}
/* success */
g_task_return_boolean(task, TRUE);
}
static void static void
fwupd_client_connect_get_proxy_cb(GObject *source, GAsyncResult *res, gpointer user_data) fwupd_client_connect_get_proxy_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{ {
g_autoptr(GTask) task = G_TASK(user_data); g_autoptr(GTask) task = G_TASK(user_data);
GVariantBuilder builder;
GHashTableIter iter;
gpointer key, value;
FwupdClient *self = g_task_get_source_object(task); FwupdClient *self = g_task_get_source_object(task);
FwupdClientPrivate *priv = GET_PRIVATE(self); FwupdClientPrivate *priv = GET_PRIVATE(self);
GCancellable *cancellable = g_task_get_cancellable(task);
g_autoptr(GDBusProxy) proxy = NULL; g_autoptr(GDBusProxy) proxy = NULL;
g_autoptr(GError) error = NULL; g_autoptr(GError) error = NULL;
g_autoptr(GVariant) val = NULL; g_autoptr(GVariant) val = NULL;
@ -653,8 +685,21 @@ fwupd_client_connect_get_proxy_cb(GObject *source, GAsyncResult *res, gpointer u
if (val7 != NULL) if (val7 != NULL)
fwupd_client_set_host_security_id(self, g_variant_get_string(val7, NULL)); fwupd_client_set_host_security_id(self, g_variant_get_string(val7, NULL));
/* success */ /* build client hints */
g_task_return_boolean(task, TRUE); g_variant_builder_init(&builder, G_VARIANT_TYPE_DICTIONARY);
g_hash_table_iter_init(&iter, priv->hints);
while (g_hash_table_iter_next(&iter, &key, &value))
g_variant_builder_add(&builder, "{ss}", (const gchar *)key, (const gchar *)value);
/* only supported on fwupd >= 1.7.1 */
g_dbus_proxy_call(priv->proxy,
"SetHints",
g_variant_new("(a{ss})", &builder),
G_DBUS_CALL_FLAGS_NONE,
FWUPD_CLIENT_DBUS_PROXY_TIMEOUT,
cancellable,
fwupd_client_set_hints_cb,
g_steal_pointer(&task));
} }
/** /**
@ -4794,6 +4839,27 @@ fwupd_client_upload_bytes_finish(FwupdClient *self, GAsyncResult *res, GError **
return g_task_propagate_pointer(G_TASK(res), error); return g_task_propagate_pointer(G_TASK(res), error);
} }
/**
* fwupd_client_add_hint:
* @self: a #FwupdClient
* @key: the key, e.g. `locale`
* @value: (nullable): the value @key should be set
*
* Sets optional hints from the client that may affect the list of devices.
*
* Since: 1.7.1
**/
void
fwupd_client_add_hint(FwupdClient *self, const gchar *key, const gchar *value)
{
FwupdClientPrivate *priv = GET_PRIVATE(self);
g_return_if_fail(FWUPD_IS_CLIENT(self));
g_return_if_fail(key != NULL);
g_hash_table_insert(priv->hints, g_strdup(key), g_strdup(value));
}
#ifdef SOUP_SESSION_COMPAT #ifdef SOUP_SESSION_COMPAT
/* this is bad; we dlopen libsoup-2.4.so.1 and get the gtype manually /* this is bad; we dlopen libsoup-2.4.so.1 and get the gtype manually
* to avoid deps on both libcurl and libsoup whilst preserving ABI */ * to avoid deps on both libcurl and libsoup whilst preserving ABI */
@ -5177,6 +5243,10 @@ fwupd_client_init(FwupdClient *self)
priv->idle_sources = priv->idle_sources =
g_ptr_array_new_with_free_func((GDestroyNotify)fwupd_client_context_helper_free); g_ptr_array_new_with_free_func((GDestroyNotify)fwupd_client_context_helper_free);
priv->proxy_resolver = g_proxy_resolver_get_default(); priv->proxy_resolver = g_proxy_resolver_get_default();
priv->hints = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
/* we get this one for free */
fwupd_client_add_hint(self, "locale", g_getenv("LANG"));
} }
static void static void
@ -5191,6 +5261,7 @@ fwupd_client_finalize(GObject *object)
g_free(priv->host_product); g_free(priv->host_product);
g_free(priv->host_machine_id); g_free(priv->host_machine_id);
g_free(priv->host_security_id); g_free(priv->host_security_id);
g_hash_table_unref(priv->hints);
g_mutex_clear(&priv->idle_mutex); g_mutex_clear(&priv->idle_mutex);
if (priv->idle_id != 0) if (priv->idle_id != 0)
g_source_remove(priv->idle_id); g_source_remove(priv->idle_id);

View File

@ -478,5 +478,7 @@ fwupd_client_upload_bytes_finish(FwupdClient *self,
GError **error) G_GNUC_WARN_UNUSED_RESULT; GError **error) G_GNUC_WARN_UNUSED_RESULT;
gboolean gboolean
fwupd_client_ensure_networking(FwupdClient *self, GError **error) G_GNUC_WARN_UNUSED_RESULT; fwupd_client_ensure_networking(FwupdClient *self, GError **error) G_GNUC_WARN_UNUSED_RESULT;
void
fwupd_client_add_hint(FwupdClient *self, const gchar *key, const gchar *value);
G_END_DECLS G_END_DECLS

View File

@ -709,3 +709,9 @@ LIBFWUPD_1.7.0 {
fwupd_security_attr_has_guid; fwupd_security_attr_has_guid;
local: *; local: *;
} LIBFWUPD_1.6.2; } LIBFWUPD_1.6.2;
LIBFWUPD_1.7.1 {
global:
fwupd_client_add_hint;
local: *;
} LIBFWUPD_1.7.0;

View File

@ -15,6 +15,7 @@ struct _FuEngineRequest {
FuEngineRequestKind kind; FuEngineRequestKind kind;
FwupdFeatureFlags feature_flags; FwupdFeatureFlags feature_flags;
FwupdDeviceFlags device_flags; FwupdDeviceFlags device_flags;
gchar *locale;
}; };
G_DEFINE_TYPE(FuEngineRequest, fu_engine_request, G_TYPE_OBJECT) G_DEFINE_TYPE(FuEngineRequest, fu_engine_request, G_TYPE_OBJECT)
@ -26,6 +27,13 @@ fu_engine_request_get_feature_flags(FuEngineRequest *self)
return self->feature_flags; return self->feature_flags;
} }
const gchar *
fu_engine_request_get_locale(FuEngineRequest *self)
{
g_return_val_if_fail(FU_IS_ENGINE_REQUEST(self), NULL);
return self->locale;
}
FuEngineRequestKind FuEngineRequestKind
fu_engine_request_get_kind(FuEngineRequest *self) fu_engine_request_get_kind(FuEngineRequest *self)
{ {
@ -40,6 +48,17 @@ fu_engine_request_set_feature_flags(FuEngineRequest *self, FwupdFeatureFlags fea
self->feature_flags = feature_flags; self->feature_flags = feature_flags;
} }
void
fu_engine_request_set_locale(FuEngineRequest *self, const gchar *locale)
{
g_return_if_fail(FU_IS_ENGINE_REQUEST(self));
self->locale = g_strdup(locale);
/* remove the UTF8 suffix as it is not present in the XML */
if (self->locale != NULL)
g_strdelimit(self->locale, ".", '\0');
}
gboolean gboolean
fu_engine_request_has_feature_flag(FuEngineRequest *self, FwupdFeatureFlags feature_flag) fu_engine_request_has_feature_flag(FuEngineRequest *self, FwupdFeatureFlags feature_flag)
{ {

View File

@ -25,6 +25,10 @@ FwupdFeatureFlags
fu_engine_request_get_feature_flags(FuEngineRequest *self); fu_engine_request_get_feature_flags(FuEngineRequest *self);
void void
fu_engine_request_set_feature_flags(FuEngineRequest *self, FwupdFeatureFlags feature_flags); fu_engine_request_set_feature_flags(FuEngineRequest *self, FwupdFeatureFlags feature_flags);
const gchar *
fu_engine_request_get_locale(FuEngineRequest *self);
void
fu_engine_request_set_locale(FuEngineRequest *self, const gchar *locale);
gboolean gboolean
fu_engine_request_has_feature_flag(FuEngineRequest *self, FwupdFeatureFlags feature_flag); fu_engine_request_has_feature_flag(FuEngineRequest *self, FwupdFeatureFlags feature_flag);
FwupdDeviceFlags FwupdDeviceFlags

View File

@ -452,6 +452,25 @@ fu_engine_set_release_from_artifact(FuEngine *self,
return TRUE; return TRUE;
} }
static gchar *
fu_engine_request_get_localized_xpath(FuEngineRequest *request, const gchar *element)
{
GString *xpath = g_string_new(element);
const gchar *locale = NULL;
/* optional; not set in tests */
if (request != NULL)
locale = fu_engine_request_get_locale(request);
/* prefer the users locale if set */
if (locale != NULL) {
g_autofree gchar *xpath_locale = NULL;
xpath_locale = g_strdup_printf("%s[@xml:lang='%s']|", element, locale);
g_string_prepend(xpath, xpath_locale);
}
return g_string_free(xpath, FALSE);
}
static gboolean static gboolean
fu_engine_set_release_from_appstream(FuEngine *self, fu_engine_set_release_from_appstream(FuEngine *self,
FuEngineRequest *request, FuEngineRequest *request,
@ -465,6 +484,7 @@ fu_engine_set_release_from_appstream(FuEngine *self,
const gchar *tmp; const gchar *tmp;
const gchar *remote_id; const gchar *remote_id;
guint64 tmp64; guint64 tmp64;
g_autofree gchar *description_xpath = NULL;
g_autofree gchar *version_rel = NULL; g_autofree gchar *version_rel = NULL;
g_autoptr(GPtrArray) cats = NULL; g_autoptr(GPtrArray) cats = NULL;
g_autoptr(GPtrArray) issues = NULL; g_autoptr(GPtrArray) issues = NULL;
@ -516,7 +536,8 @@ fu_engine_set_release_from_appstream(FuEngine *self,
if (!fu_engine_set_release_from_artifact(self, rel, remote, artifact, error)) if (!fu_engine_set_release_from_artifact(self, rel, remote, artifact, error))
return FALSE; return FALSE;
} }
description = xb_node_query_first(release, "description", NULL); description_xpath = fu_engine_request_get_localized_xpath(request, "description");
description = xb_node_query_first(release, description_xpath, NULL);
if (description != NULL) { if (description != NULL) {
g_autofree gchar *xml = NULL; g_autofree gchar *xml = NULL;
g_autoptr(GString) str = NULL; g_autoptr(GString) str = NULL;
@ -4209,6 +4230,7 @@ fu_engine_get_result_from_component(FuEngine *self,
GError **error) GError **error)
{ {
FwupdReleaseFlags release_flags = FWUPD_RELEASE_FLAG_NONE; FwupdReleaseFlags release_flags = FWUPD_RELEASE_FLAG_NONE;
g_autofree gchar *description_xpath = NULL;
g_autoptr(FuInstallTask) task = NULL; g_autoptr(FuInstallTask) task = NULL;
g_autoptr(FuDevice) dev = NULL; g_autoptr(FuDevice) dev = NULL;
g_autoptr(FwupdRelease) rel = NULL; g_autoptr(FwupdRelease) rel = NULL;
@ -4300,7 +4322,8 @@ fu_engine_get_result_from_component(FuEngine *self,
} }
/* create a result with all the metadata in */ /* create a result with all the metadata in */
description = xb_node_query_first(component, "description", NULL); description_xpath = fu_engine_request_get_localized_xpath(request, "description");
description = xb_node_query_first(component, description_xpath, NULL);
if (description != NULL) { if (description != NULL) {
g_autofree gchar *xml = NULL; g_autofree gchar *xml = NULL;
xml = xb_node_export(description, XB_NODE_EXPORT_FLAG_ONLY_CHILDREN, NULL); xml = xb_node_export(description, XB_NODE_EXPORT_FLAG_ONLY_CHILDREN, NULL);

View File

@ -58,13 +58,18 @@ typedef enum {
FU_MAIN_MACHINE_KIND_CONTAINER, FU_MAIN_MACHINE_KIND_CONTAINER,
} FuMainMachineKind; } FuMainMachineKind;
typedef struct {
FwupdFeatureFlags feature_flags;
GHashTable *hints; /* str:str */
} FuSenderItem;
typedef struct { typedef struct {
GDBusConnection *connection; GDBusConnection *connection;
GDBusNodeInfo *introspection_daemon; GDBusNodeInfo *introspection_daemon;
GDBusProxy *proxy_uid; GDBusProxy *proxy_uid;
GMainLoop *loop; GMainLoop *loop;
GFileMonitor *argv0_monitor; GFileMonitor *argv0_monitor;
GHashTable *sender_features; /* sender:FwupdFeatureFlags */ GHashTable *sender_items; /* sender:FuSenderItem */
#if GLIB_CHECK_VERSION(2, 63, 3) #if GLIB_CHECK_VERSION(2, 63, 3)
GMemoryMonitor *memory_monitor; GMemoryMonitor *memory_monitor;
#endif #endif
@ -235,7 +240,7 @@ fu_main_engine_percentage_changed_cb(FuEngine *engine, guint percentage, FuMainP
static FuEngineRequest * static FuEngineRequest *
fu_main_create_request(FuMainPrivate *priv, const gchar *sender, GError **error) fu_main_create_request(FuMainPrivate *priv, const gchar *sender, GError **error)
{ {
FwupdFeatureFlags *feature_flags; FuSenderItem *sender_item;
FwupdDeviceFlags device_flags = FWUPD_DEVICE_FLAG_NONE; FwupdDeviceFlags device_flags = FWUPD_DEVICE_FLAG_NONE;
uid_t calling_uid = 0; uid_t calling_uid = 0;
g_autoptr(FuEngineRequest) request = fu_engine_request_new(FU_ENGINE_REQUEST_KIND_ACTIVE); g_autoptr(FuEngineRequest) request = fu_engine_request_new(FU_ENGINE_REQUEST_KIND_ACTIVE);
@ -243,10 +248,14 @@ fu_main_create_request(FuMainPrivate *priv, const gchar *sender, GError **error)
g_return_val_if_fail(sender != NULL, NULL); g_return_val_if_fail(sender != NULL, NULL);
/* did the client set the list of supported feature */ /* did the client set the list of supported features or any hints */
feature_flags = g_hash_table_lookup(priv->sender_features, sender); sender_item = g_hash_table_lookup(priv->sender_items, sender);
if (feature_flags != NULL) if (sender_item != NULL) {
fu_engine_request_set_feature_flags(request, *feature_flags); const gchar *locale = g_hash_table_lookup(sender_item->hints, "locale");
if (locale != NULL)
fu_engine_request_set_locale(request, locale);
fu_engine_request_set_feature_flags(request, sender_item->feature_flags);
}
/* are we root and therefore trusted? */ /* are we root and therefore trusted? */
value = g_dbus_proxy_call_sync(priv->proxy_uid, value = g_dbus_proxy_call_sync(priv->proxy_uid,
@ -906,6 +915,19 @@ fu_main_install_with_helper(FuMainAuthHelper *helper_ref, GError **error)
return TRUE; return TRUE;
} }
static FuSenderItem *
fu_main_ensure_sender_item(FuMainPrivate *priv, const gchar *sender)
{
FuSenderItem *sender_item;
sender_item = g_hash_table_lookup(priv->sender_items, sender);
if (sender_item == NULL) {
sender_item = g_new0(FuSenderItem, 1);
sender_item->hints = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
g_hash_table_insert(priv->sender_items, g_strdup(sender), sender_item);
}
return sender_item;
}
static gboolean static gboolean
fu_main_device_id_valid(const gchar *device_id, GError **error) fu_main_device_id_valid(const gchar *device_id, GError **error)
{ {
@ -1510,23 +1532,37 @@ fu_main_daemon_method_call(GDBusConnection *connection,
return; return;
} }
if (g_strcmp0(method_name, "SetFeatureFlags") == 0) { if (g_strcmp0(method_name, "SetFeatureFlags") == 0) {
FwupdFeatureFlags feature_flags; FuSenderItem *sender_item;
guint64 feature_flags_u64 = 0; guint64 feature_flags_u64 = 0;
g_variant_get(parameters, "(t)", &feature_flags_u64); g_variant_get(parameters, "(t)", &feature_flags_u64);
g_debug("Called %s(%" G_GUINT64_FORMAT ")", method_name, feature_flags_u64); g_debug("Called %s(%" G_GUINT64_FORMAT ")", method_name, feature_flags_u64);
/* old flags for the same sender will be automatically destroyed */ /* old flags for the same sender will be automatically destroyed */
feature_flags = feature_flags_u64; sender_item = fu_main_ensure_sender_item(priv, sender);
g_hash_table_insert(priv->sender_features, sender_item->feature_flags = feature_flags_u64;
g_strdup(sender),
#if GLIB_CHECK_VERSION(2, 67, 4)
g_memdup2(&feature_flags, sizeof(feature_flags)));
#else
g_memdup(&feature_flags, sizeof(feature_flags)));
#endif
g_dbus_method_invocation_return_value(invocation, NULL); g_dbus_method_invocation_return_value(invocation, NULL);
return; return;
} }
if (g_strcmp0(method_name, "SetHints") == 0) {
FuSenderItem *sender_item;
const gchar *prop_key;
const gchar *prop_value;
g_autoptr(GVariantIter) iter = NULL;
g_variant_get(parameters, "(a{ss})", &iter);
g_debug("Called %s()", method_name);
sender_item = fu_main_ensure_sender_item(priv, sender);
while (g_variant_iter_next(iter, "{&s&s}", &prop_key, &prop_value)) {
g_debug("got hint %s=%s", prop_key, prop_value);
g_hash_table_insert(sender_item->hints,
g_strdup(prop_key),
g_strdup(prop_value));
}
g_dbus_method_invocation_return_value(invocation, NULL);
return;
}
if (g_strcmp0(method_name, "Install") == 0) { if (g_strcmp0(method_name, "Install") == 0) {
GVariant *prop_value; GVariant *prop_value;
const gchar *device_id = NULL; const gchar *device_id = NULL;
@ -1845,7 +1881,7 @@ fu_main_is_container(void)
static void static void
fu_main_private_free(FuMainPrivate *priv) fu_main_private_free(FuMainPrivate *priv)
{ {
g_hash_table_unref(priv->sender_features); g_hash_table_unref(priv->sender_items);
if (priv->loop != NULL) if (priv->loop != NULL)
g_main_loop_unref(priv->loop); g_main_loop_unref(priv->loop);
if (priv->owner_id > 0) if (priv->owner_id > 0)
@ -1878,6 +1914,13 @@ fu_main_private_free(FuMainPrivate *priv)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuMainPrivate, fu_main_private_free) G_DEFINE_AUTOPTR_CLEANUP_FUNC(FuMainPrivate, fu_main_private_free)
#pragma clang diagnostic pop #pragma clang diagnostic pop
static void
fu_main_sender_item_free(FuSenderItem *sender_item)
{
g_hash_table_unref(sender_item->hints);
g_free(sender_item);
}
int int
main(int argc, char *argv[]) main(int argc, char *argv[])
{ {
@ -1926,7 +1969,10 @@ main(int argc, char *argv[])
/* create new objects */ /* create new objects */
priv = g_new0(FuMainPrivate, 1); priv = g_new0(FuMainPrivate, 1);
priv->sender_features = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); priv->sender_items = g_hash_table_new_full(g_str_hash,
g_str_equal,
g_free,
(GDestroyNotify)fu_main_sender_item_free);
priv->loop = g_main_loop_new(NULL, FALSE); priv->loop = g_main_loop_new(NULL, FALSE);
/* load engine */ /* load engine */

View File

@ -307,6 +307,25 @@
</arg> </arg>
</method> </method>
<!--***********************************************************-->
<method name='SetHints'>
<doc:doc>
<doc:description>
<doc:para>
Sets optional hints from the client that may affect the list of devices.
A typical hint might be <doc:tt>locale</doc:tt> and unknown hints should be ignored.
</doc:para>
</doc:description>
</doc:doc>
<arg type='a{ss}' name='hints' direction='in'>
<doc:doc>
<doc:summary>
<doc:para>An array of string key values.</doc:para>
</doc:summary>
</doc:doc>
</arg>
</method>
<!--***********************************************************--> <!--***********************************************************-->
<method name='Install'> <method name='Install'>
<doc:doc> <doc:doc>