modem-manager: implement qmi pdc active config selection as attach()

When we install the MCFG carrier config files with QMI PDC, we were
not explicitly selecting one, and that would end up reporting the
"DF" (default) config is in use. Instead, we should explicitly select
the carrier configuration that we were using before the firmware
upgrade operation.

For example, if the device originally was running with the Vodafone
specific carrier configuration (e.g. T77W968.F1.0.0.3.7.VF.009) and we
trigger the upgrade to the next available firmware associated to
the Vodafone carrier (e.g. T77W968.F1.0.0.3.8.VF.009), we would want
the device to boot with the Vodafone carrier config selected, instead
of booting without any config selected (T77W968.F1.0.0.3.8.DF.009).

This also fixes several upgrade problems detected by fwupd, because it
may end up complaining that the target firmware that was selected to
be installed (e.g. VF variant) is not the one actually reported by the
device after the upgrade (e.g. DF variant).

The selection of which is the config to activate is based on mapping
the mcfg file name with the firmware version reported by the module
before the upgrade. E.g. if the VF variant is reported by the module
(T77W968.F1.0.0.3.7.VF.009), fwupd will look for a MCFG file named
with the "mcfg.VF." prefix.
This commit is contained in:
Aleksander Morgado 2019-04-05 11:24:21 +02:00 committed by Richard Hughes
parent 6c0e9ce6b4
commit ffb8fd5edc
4 changed files with 403 additions and 22 deletions

View File

@ -44,8 +44,17 @@ struct _FuMmDevice {
/* qmi-pdc update logic */
gchar *port_qmi;
FuQmiPdcUpdater *qmi_pdc_updater;
GArray *qmi_pdc_active_id;
guint attach_idle;
};
enum {
SIGNAL_ATTACH_FINISHED,
SIGNAL_LAST
};
static guint signals [SIGNAL_LAST] = { 0 };
G_DEFINE_TYPE (FuMmDevice, fu_mm_device, FU_TYPE_DEVICE)
static void
@ -422,13 +431,16 @@ fu_mm_device_detach (FuDevice *device, GError **error)
}
typedef struct {
gchar *filename;
GBytes *bytes;
gchar *filename;
GBytes *bytes;
GArray *digest;
gboolean active;
} FuMmFileInfo;
static void
fu_mm_file_info_free (FuMmFileInfo *file_info)
{
g_clear_pointer (&file_info->digest, g_array_unref);
g_free (file_info->filename);
g_bytes_unref (file_info->bytes);
g_free (file_info);
@ -442,6 +454,34 @@ typedef struct {
gsize total_bytes;
} FuMmArchiveIterateCtx;
static gboolean
fu_mm_should_be_active (const gchar *version,
const gchar *filename)
{
g_auto(GStrv) split = NULL;
g_autofree gchar *carrier_id = NULL;
/* The filename of the mcfg file is composed of a "mcfg." prefix, then the
* carrier code, followed by the carrier version, and finally a ".mbn"
* prefix. Here we try to guess, based on the carrier code, whether the
* specific mcfg file should be activated after the firmware upgrade
* operation.
*
* This logic requires that the previous device version includes the carrier
* code also embedded in the version string. E.g. "xxxx.VF.xxxx". If we find
* this match, we assume this is the active config to use.
*/
split = g_strsplit (filename, ".", -1);
if (g_strv_length (split) < 4)
return FALSE;
if (g_strcmp0 (split[0], "mcfg") != 0)
return FALSE;
carrier_id = g_strdup_printf (".%s.", split[1]);
return (g_strstr_len (version, -1, carrier_id) != NULL);
}
static void
fu_mm_qmi_pdc_archive_iterate_mcfg (FuArchive *archive,
const gchar *filename,
@ -458,6 +498,7 @@ fu_mm_qmi_pdc_archive_iterate_mcfg (FuArchive *archive,
file_info = g_new0 (FuMmFileInfo, 1);
file_info->filename = g_strdup (filename);
file_info->bytes = g_bytes_ref (bytes);
file_info->active = fu_mm_should_be_active (fu_device_get_version (FU_DEVICE (ctx->device)), filename);
g_ptr_array_add (ctx->file_infos, file_info);
ctx->total_bytes += g_bytes_get_size (file_info->bytes);
}
@ -479,11 +520,22 @@ fu_mm_device_qmi_close (FuMmDevice *self, GError **error)
}
static gboolean
fu_mm_device_write_firmware_qmi_pdc (FuDevice *device, GBytes *fw, GError **error)
fu_mm_device_qmi_close_no_error (FuMmDevice *self, GError **error)
{
g_autoptr(FuQmiPdcUpdater) updater = NULL;
updater = g_steal_pointer (&self->qmi_pdc_updater);
fu_qmi_pdc_updater_close (updater, NULL);
return TRUE;
}
static gboolean
fu_mm_device_write_firmware_qmi_pdc (FuDevice *device, GBytes *fw, GArray **active_id, GError **error)
{
g_autoptr(FuArchive) archive = NULL;
g_autoptr(FuDeviceLocker) locker = NULL;
g_autoptr(GPtrArray) file_infos = g_ptr_array_new_with_free_func ((GDestroyNotify)fu_mm_file_info_free);
gint active_i = -1;
FuMmArchiveIterateCtx archive_context = {
.device = FU_MM_DEVICE (device),
.error = NULL,
@ -510,14 +562,26 @@ fu_mm_device_write_firmware_qmi_pdc (FuDevice *device, GBytes *fw, GError **erro
for (guint i = 0; i < file_infos->len; i++) {
FuMmFileInfo *file_info = g_ptr_array_index (file_infos, i);
if (!fu_qmi_pdc_updater_write (archive_context.device->qmi_pdc_updater,
file_info->filename,
file_info->bytes,
&archive_context.error)) {
file_info->digest = fu_qmi_pdc_updater_write (archive_context.device->qmi_pdc_updater,
file_info->filename,
file_info->bytes,
&archive_context.error);
if (file_info->digest == NULL) {
g_prefix_error (&archive_context.error,
"Failed to write file '%s':", file_info->filename);
break;
}
/* if we wrongly detect more than one, just assume the latest one; this
* is not critical, it may just take a bit more time to perform the
* automatic carrier config switching in ModemManager */
if (file_info->active)
active_i = i;
}
/* set expected active configuration */
if (active_i >= 0 && active_id != NULL) {
FuMmFileInfo *file_info = g_ptr_array_index (file_infos, active_i);
*active_id = g_array_ref (file_info->digest);
}
if (archive_context.error != NULL) {
@ -543,16 +607,65 @@ fu_mm_device_write_firmware (FuDevice *device, GBytes *fw, GError **error)
/* qmi pdc write operation */
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC)
return fu_mm_device_write_firmware_qmi_pdc (device, fw, error);
return fu_mm_device_write_firmware_qmi_pdc (device, fw, &self->qmi_pdc_active_id, error);
g_set_error (error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED,
"unsupported update method");
return FALSE;
}
static gboolean
fu_mm_device_attach_qmi_pdc (FuMmDevice *self, GError **error)
{
g_autoptr(FuDeviceLocker) locker = NULL;
/* ignore action if there is no active id specified */
if (self->qmi_pdc_active_id == NULL)
return TRUE;
/* errors closing may be expected if the device really reboots itself */
locker = fu_device_locker_new_full (self,
(FuDeviceLockerFunc) fu_mm_device_qmi_open,
(FuDeviceLockerFunc) fu_mm_device_qmi_close_no_error,
error);
if (locker == NULL)
return FALSE;
if (!fu_qmi_pdc_updater_activate (self->qmi_pdc_updater, self->qmi_pdc_active_id, error))
return FALSE;
return TRUE;
}
static gboolean
fu_mm_device_attach_noop_idle (gpointer user_data)
{
FuMmDevice *self = FU_MM_DEVICE (user_data);
self->attach_idle = 0;
g_signal_emit (self, signals [SIGNAL_ATTACH_FINISHED], 0);
return G_SOURCE_REMOVE;
}
static gboolean
fu_mm_device_attach_qmi_pdc_idle (gpointer user_data)
{
FuMmDevice *self = FU_MM_DEVICE (user_data);
g_autoptr(GError) error = NULL;
if (!fu_mm_device_attach_qmi_pdc (self, &error))
g_warning ("qmi-pdc attach operation failed: %s", error->message);
else
g_debug ("qmi-pdc attach operation successful");
self->attach_idle = 0;
g_signal_emit (self, signals [SIGNAL_ATTACH_FINISHED], 0);
return G_SOURCE_REMOVE;
}
static gboolean
fu_mm_device_attach (FuDevice *device, GError **error)
{
FuMmDevice *self = FU_MM_DEVICE (device);
g_autoptr(FuDeviceLocker) locker = NULL;
/* lock device */
@ -560,6 +673,13 @@ fu_mm_device_attach (FuDevice *device, GError **error)
if (locker == NULL)
return FALSE;
/* we want this attach operation to be triggered asynchronously, because the engine
* must learn that it has to wait for replug before we actually trigger the reset. */
if (self->update_methods & MM_MODEM_FIRMWARE_UPDATE_METHOD_QMI_PDC)
self->attach_idle = g_idle_add ((GSourceFunc) fu_mm_device_attach_qmi_pdc_idle, self);
else
self->attach_idle = g_idle_add ((GSourceFunc) fu_mm_device_attach_noop_idle, self);
/* wait for re-probing after uninhibiting */
fu_device_set_remove_delay (device, FU_MM_DEVICE_REMOVE_DELAY_REPROBE);
fu_device_add_flag (device, FWUPD_DEVICE_FLAG_WAIT_FOR_REPLUG);
@ -579,6 +699,10 @@ static void
fu_mm_device_finalize (GObject *object)
{
FuMmDevice *self = FU_MM_DEVICE (object);
if (self->attach_idle)
g_source_remove (self->attach_idle);
if (self->qmi_pdc_active_id)
g_array_unref (self->qmi_pdc_active_id);
g_object_unref (self->manager);
if (self->omodem != NULL)
g_object_unref (self->omodem);
@ -600,6 +724,12 @@ fu_mm_device_class_init (FuMmDeviceClass *klass)
klass_device->detach = fu_mm_device_detach;
klass_device->write_firmware = fu_mm_device_write_firmware;
klass_device->attach = fu_mm_device_attach;
signals [SIGNAL_ATTACH_FINISHED] =
g_signal_new ("attach-finished",
G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
0, NULL, NULL, g_cclosure_marshal_VOID__VOID,
G_TYPE_NONE, 0);
}
FuMmDevice *

View File

@ -396,12 +396,11 @@ fu_plugin_update (FuPlugin *plugin,
return fu_device_write_firmware (device, blob_fw, error);
}
static gboolean
fu_plugin_mm_uninhibit_device_idle (gpointer user_data)
static void
fu_plugin_mm_device_attach_finished (gpointer user_data)
{
FuPlugin *plugin = FU_PLUGIN (user_data);
fu_plugin_mm_uninhibit_device (plugin);
return G_SOURCE_REMOVE;
}
gboolean
@ -414,10 +413,16 @@ fu_plugin_update_attach (FuPlugin *plugin, FuDevice *device, GError **error)
if (locker == NULL)
return FALSE;
/* must be done in idle so that the engine explicitly
* waits for the device to be redetected */
g_idle_add (fu_plugin_mm_uninhibit_device_idle, plugin);
/* schedule device attach asynchronously, which is extremely important
* so that engine can setup the device "waiting" logic before the actual
* attach procedure happens (which will reset the module if it worked
* properly) */
if (!fu_device_attach (device, error))
return FALSE;
/* reset */
return fu_device_attach (FU_DEVICE (device), error);
/* this signal will always be emitted asynchronously */
g_signal_connect_swapped (device, "attach-finished",
G_CALLBACK (fu_plugin_mm_device_attach_finished), plugin);
return TRUE;
}

View File

@ -390,21 +390,264 @@ fu_qmi_pdc_updater_get_checksum (GBytes *blob)
return digest;
}
gboolean
GArray *
fu_qmi_pdc_updater_write (FuQmiPdcUpdater *self, const gchar *filename, GBytes *blob, GError **error)
{
g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE);
g_autoptr(GArray) digest = fu_qmi_pdc_updater_get_checksum (blob);
WriteContext ctx = {
.mainloop = mainloop,
.qmi_client = self->qmi_client,
.blob = blob,
.digest = fu_qmi_pdc_updater_get_checksum (blob),
.error = NULL,
.indication_id = 0,
.timeout_id = 0,
.blob = blob,
.digest = digest,
.offset = 0,
.token = 0,
};
fu_qmi_pdc_updater_load_config (&ctx);
g_main_loop_run (mainloop);
if (ctx.error != NULL) {
g_propagate_error (error, ctx.error);
return NULL;
}
return g_steal_pointer (&digest);
}
typedef struct {
GMainLoop *mainloop;
QmiClientPdc *qmi_client;
GError *error;
gulong indication_id;
guint timeout_id;
GArray *digest;
guint token;
} ActivateContext;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QmiMessagePdcActivateConfigInput, qmi_message_pdc_activate_config_input_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QmiMessagePdcActivateConfigOutput, qmi_message_pdc_activate_config_output_unref)
#pragma clang diagnostic pop
static gboolean
fu_qmi_pdc_updater_activate_config_timeout (gpointer user_data)
{
ActivateContext *ctx = user_data;
ctx->timeout_id = 0;
g_signal_handler_disconnect (ctx->qmi_client, ctx->indication_id);
ctx->indication_id = 0;
/* not an error, the device may go away without sending the indication */
g_main_loop_quit (ctx->mainloop);
return G_SOURCE_REMOVE;
}
static void
fu_qmi_pdc_updater_activate_config_indication (QmiClientPdc *client,
QmiIndicationPdcActivateConfigOutput *output,
ActivateContext *ctx)
{
guint16 error_code = 0;
g_source_remove (ctx->timeout_id);
ctx->timeout_id = 0;
g_signal_handler_disconnect (ctx->qmi_client, ctx->indication_id);
ctx->indication_id = 0;
if (!qmi_indication_pdc_activate_config_output_get_indication_result (output, &error_code, &ctx->error)) {
g_main_loop_quit (ctx->mainloop);
return;
}
if (error_code != 0) {
g_set_error (&ctx->error, G_IO_ERROR, G_IO_ERROR_FAILED,
"couldn't activate config: %s", qmi_protocol_error_get_string ((QmiProtocolError) error_code));
g_main_loop_quit (ctx->mainloop);
return;
}
/* assume ok */
g_debug ("successful activate configuration indication: assuming device reset is ongoing");
g_main_loop_quit (ctx->mainloop);
}
static void
fu_qmi_pdc_updater_activate_config_ready (GObject *qmi_client, GAsyncResult *res, gpointer user_data)
{
ActivateContext *ctx = (ActivateContext *) user_data;
g_autoptr(QmiMessagePdcActivateConfigOutput) output = NULL;
output = qmi_client_pdc_activate_config_finish (QMI_CLIENT_PDC (qmi_client), res, &ctx->error);
if (output == NULL) {
/* If we didn't receive a response, this is a good indication that the device
* reseted itself, we can consider this a successful operation.
* Note: not using g_error_matches() to avoid matching the domain, because the
* error may be either QMI_CORE_ERROR_TIMEOUT or MBIM_CORE_ERROR_TIMEOUT (same
* numeric value), and we don't want to build-depend on libmbim just for this.
*/
if (ctx->error->code == QMI_CORE_ERROR_TIMEOUT) {
g_debug ("request to activate configuration timed out: assuming device reset is ongoing");
g_clear_error (&ctx->error);
}
g_main_loop_quit (ctx->mainloop);
return;
}
if (!qmi_message_pdc_activate_config_output_get_result (output, &ctx->error)) {
g_main_loop_quit (ctx->mainloop);
return;
}
/* When we activate the config, if the operation is successful, we'll just
* see the modem going away completely. So, do not consider an error the timeout
* waiting for the Activate Config indication, as that is actually a good
* thing.
*/
g_warn_if_fail (ctx->indication_id == 0);
ctx->indication_id = g_signal_connect (ctx->qmi_client, "activate-config",
G_CALLBACK (fu_qmi_pdc_updater_activate_config_indication), ctx);
/* don't wait forever */
g_warn_if_fail (ctx->timeout_id == 0);
ctx->timeout_id = g_timeout_add_seconds (5, fu_qmi_pdc_updater_activate_config_timeout, ctx);
}
static void
fu_qmi_pdc_updater_activate_config (ActivateContext *ctx)
{
g_autoptr(QmiMessagePdcActivateConfigInput) input = NULL;
input = qmi_message_pdc_activate_config_input_new ();
qmi_message_pdc_activate_config_input_set_config_type (input, QMI_PDC_CONFIGURATION_TYPE_SOFTWARE, NULL);
qmi_message_pdc_activate_config_input_set_token (input, ctx->token++, NULL);
g_debug ("activating selected configuration...");
qmi_client_pdc_activate_config (ctx->qmi_client, input, 5, NULL,
fu_qmi_pdc_updater_activate_config_ready, ctx);
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunused-function"
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QmiMessagePdcSetSelectedConfigInput, qmi_message_pdc_set_selected_config_input_unref)
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QmiMessagePdcSetSelectedConfigOutput, qmi_message_pdc_set_selected_config_output_unref)
#pragma clang diagnostic pop
static gboolean
fu_qmi_pdc_updater_set_selected_config_timeout (gpointer user_data)
{
ActivateContext *ctx = user_data;
ctx->timeout_id = 0;
g_signal_handler_disconnect (ctx->qmi_client, ctx->indication_id);
ctx->indication_id = 0;
g_set_error_literal (&ctx->error, G_IO_ERROR, G_IO_ERROR_FAILED,
"couldn't set selected config: timed out");
g_main_loop_quit (ctx->mainloop);
return G_SOURCE_REMOVE;
}
static void
fu_qmi_pdc_updater_set_selected_config_indication (QmiClientPdc *client,
QmiIndicationPdcSetSelectedConfigOutput *output,
ActivateContext *ctx)
{
guint16 error_code = 0;
g_source_remove (ctx->timeout_id);
ctx->timeout_id = 0;
g_signal_handler_disconnect (ctx->qmi_client, ctx->indication_id);
ctx->indication_id = 0;
if (!qmi_indication_pdc_set_selected_config_output_get_indication_result (output, &error_code, &ctx->error)) {
g_main_loop_quit (ctx->mainloop);
return;
}
if (error_code != 0) {
g_set_error (&ctx->error, G_IO_ERROR, G_IO_ERROR_FAILED,
"couldn't set selected config: %s", qmi_protocol_error_get_string ((QmiProtocolError) error_code));
g_main_loop_quit (ctx->mainloop);
return;
}
g_debug ("current configuration successfully selected...");
/* now activate config */
fu_qmi_pdc_updater_activate_config (ctx);
}
static void
fu_qmi_pdc_updater_set_selected_config_ready (GObject *qmi_client, GAsyncResult *res, gpointer user_data)
{
ActivateContext *ctx = (ActivateContext *) user_data;
g_autoptr(QmiMessagePdcSetSelectedConfigOutput) output = NULL;
output = qmi_client_pdc_set_selected_config_finish (QMI_CLIENT_PDC (qmi_client), res, &ctx->error);
if (output == NULL) {
g_main_loop_quit (ctx->mainloop);
return;
}
if (!qmi_message_pdc_set_selected_config_output_get_result (output, &ctx->error)) {
g_main_loop_quit (ctx->mainloop);
return;
}
/* after receiving the response to our request, we now expect an indication
* with the actual result of the operation */
g_warn_if_fail (ctx->indication_id == 0);
ctx->indication_id = g_signal_connect (ctx->qmi_client, "set-selected-config",
G_CALLBACK (fu_qmi_pdc_updater_set_selected_config_indication), ctx);
/* don't wait forever */
g_warn_if_fail (ctx->timeout_id == 0);
ctx->timeout_id = g_timeout_add_seconds (5, fu_qmi_pdc_updater_set_selected_config_timeout, ctx);
}
static void
fu_qmi_pdc_updater_set_selected_config (ActivateContext *ctx)
{
g_autoptr(QmiMessagePdcSetSelectedConfigInput) input = NULL;
QmiConfigTypeAndId type_and_id;
type_and_id.config_type = QMI_PDC_CONFIGURATION_TYPE_SOFTWARE;
type_and_id.id = ctx->digest;
input = qmi_message_pdc_set_selected_config_input_new ();
qmi_message_pdc_set_selected_config_input_set_type_with_id (input, &type_and_id, NULL);
qmi_message_pdc_set_selected_config_input_set_token (input, ctx->token++, NULL);
g_debug ("selecting current configuration...");
qmi_client_pdc_set_selected_config (ctx->qmi_client, input, 10, NULL,
fu_qmi_pdc_updater_set_selected_config_ready, ctx);
}
gboolean
fu_qmi_pdc_updater_activate (FuQmiPdcUpdater *self, GArray *digest, GError **error)
{
g_autoptr(GMainLoop) mainloop = g_main_loop_new (NULL, FALSE);
ActivateContext ctx = {
.mainloop = mainloop,
.qmi_client = self->qmi_client,
.error = NULL,
.indication_id = 0,
.timeout_id = 0,
.digest = digest,
.token = 0,
};
fu_qmi_pdc_updater_set_selected_config (&ctx);
g_main_loop_run (mainloop);
if (ctx.error != NULL) {
g_propagate_error (error, ctx.error);
return FALSE;

View File

@ -15,13 +15,16 @@ G_BEGIN_DECLS
G_DECLARE_FINAL_TYPE (FuQmiPdcUpdater, fu_qmi_pdc_updater, FU, QMI_PDC_UPDATER, GObject)
FuQmiPdcUpdater *fu_qmi_pdc_updater_new (const gchar *qmi_port);
gboolean fu_qmi_pdc_updater_open (FuQmiPdcUpdater *self,
gboolean fu_qmi_pdc_updater_open (FuQmiPdcUpdater *self,
GError **error);
gboolean fu_qmi_pdc_updater_write (FuQmiPdcUpdater *self,
GArray *fu_qmi_pdc_updater_write (FuQmiPdcUpdater *self,
const gchar *filename,
GBytes *blob,
GError **error);
gboolean fu_qmi_pdc_updater_close (FuQmiPdcUpdater *self,
gboolean fu_qmi_pdc_updater_activate (FuQmiPdcUpdater *self,
GArray *digest,
GError **error);
gboolean fu_qmi_pdc_updater_close (FuQmiPdcUpdater *self,
GError **error);
G_END_DECLS