diff --git a/plugins/modem-manager/fu-mm-device.c b/plugins/modem-manager/fu-mm-device.c index d2e817f26..449edf88d 100644 --- a/plugins/modem-manager/fu-mm-device.c +++ b/plugins/modem-manager/fu-mm-device.c @@ -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 * diff --git a/plugins/modem-manager/fu-plugin-modem-manager.c b/plugins/modem-manager/fu-plugin-modem-manager.c index 8ef1bb434..d581e144c 100644 --- a/plugins/modem-manager/fu-plugin-modem-manager.c +++ b/plugins/modem-manager/fu-plugin-modem-manager.c @@ -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; } diff --git a/plugins/modem-manager/fu-qmi-pdc-updater.c b/plugins/modem-manager/fu-qmi-pdc-updater.c index b02a6c92f..7e997a9a9 100644 --- a/plugins/modem-manager/fu-qmi-pdc-updater.c +++ b/plugins/modem-manager/fu-qmi-pdc-updater.c @@ -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; diff --git a/plugins/modem-manager/fu-qmi-pdc-updater.h b/plugins/modem-manager/fu-qmi-pdc-updater.h index 32e3ca19e..d1465cb4b 100644 --- a/plugins/modem-manager/fu-qmi-pdc-updater.h +++ b/plugins/modem-manager/fu-qmi-pdc-updater.h @@ -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