/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ /* * qmi-firmware-update -- Command line tool to update firmware in QMI devices * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Copyright (C) 2016 Zodiac Inflight Innovations * Copyright (C) 2016 Aleksander Morgado */ #include #include #include #include #include #include "qfu-log.h" #include "qfu-image-factory.h" #include "qfu-updater.h" #include "qfu-reseter.h" #include "qfu-utils.h" #include "qfu-udev-helpers.h" #include "qfu-qdl-device.h" #include "qfu-enum-types.h" G_DEFINE_TYPE (QfuUpdater, qfu_updater, G_TYPE_OBJECT) #define CLEAR_LINE "\33[2K\r" typedef enum { UPDATER_TYPE_UNKNOWN, UPDATER_TYPE_GENERIC, UPDATER_TYPE_QDL, } UpdaterType; struct _QfuUpdaterPrivate { UpdaterType type; QfuDeviceSelection *device_selection; gchar *firmware_version; gchar *config_version; gchar *carrier; gboolean device_open_proxy; gboolean device_open_mbim; gboolean ignore_version_errors; gboolean override_download; guint8 modem_storage_index; gboolean skip_validation; }; /******************************************************************************/ static const gchar *progress[] = { "(*-----)", "(-*----)", "(--*---)", "(---*--)", "(----*-)", "(-----*)", "(----*-)", "(---*--)", "(--*---)", "(-*----)" }; /******************************************************************************/ /* Run */ /* Wait time after the upgrade has been done, before using the cdc-wdm port */ #define WAIT_FOR_BOOT_TIMEOUT_SECS 5 #define WAIT_FOR_BOOT_RETRIES 12 typedef enum { RUN_CONTEXT_STEP_QMI_CLIENT, RUN_CONTEXT_STEP_GET_FIRMWARE_PREFERENCE, RUN_CONTEXT_STEP_SET_FIRMWARE_PREFERENCE, RUN_CONTEXT_STEP_POWER_CYCLE, RUN_CONTEXT_STEP_CLEANUP_QMI_DEVICE, RUN_CONTEXT_STEP_WAIT_FOR_TTY, RUN_CONTEXT_STEP_QDL_DEVICE, RUN_CONTEXT_STEP_SELECT_IMAGE, RUN_CONTEXT_STEP_DOWNLOAD_IMAGE, RUN_CONTEXT_STEP_CLEANUP_IMAGE, RUN_CONTEXT_STEP_CLEANUP_QDL_DEVICE, RUN_CONTEXT_STEP_WAIT_FOR_CDC_WDM, RUN_CONTEXT_STEP_WAIT_FOR_BOOT, RUN_CONTEXT_STEP_QMI_CLIENT_AFTER, RUN_CONTEXT_STEP_LAST } RunContextStep; typedef struct { /* Device selection */ GFile *cdc_wdm_file; GFile *serial_file; /* Context step */ RunContextStep step; /* Old/New info and capabilities */ gchar *revision; gboolean supports_stored_image_management; guint8 max_modem_storage_index; gboolean supports_firmware_preference_management; QmiMessageDmsGetFirmwarePreferenceOutput *firmware_preference; QmiMessageDmsSwiGetCurrentFirmwareOutput *current_firmware; gchar *new_revision; gboolean new_supports_stored_image_management; gboolean new_supports_firmware_preference_management; QmiMessageDmsGetFirmwarePreferenceOutput *new_firmware_preference; QmiMessageDmsSwiGetCurrentFirmwareOutput *new_current_firmware; /* List of pending QfuImages to download, and the current one being * processed. */ GList *pending_images; QfuImage *current_image; /* QMI device and client */ QmiDevice *qmi_device; QmiClientDms *qmi_client; /* QDL device */ QfuQdlDevice *qdl_device; /* Reset configuration */ gboolean boothold_reset; /* Information gathered from the firmware images themselves */ gchar *firmware_version; gchar *config_version; gchar *carrier; /* Waiting for boot */ guint wait_for_boot_seconds_elapsed; guint wait_for_boot_retries; } RunContext; static void run_context_free (RunContext *ctx) { if (ctx->current_firmware) qmi_message_dms_swi_get_current_firmware_output_unref (ctx->current_firmware); if (ctx->new_current_firmware) qmi_message_dms_swi_get_current_firmware_output_unref (ctx->new_current_firmware); if (ctx->firmware_preference) qmi_message_dms_get_firmware_preference_output_unref (ctx->firmware_preference); if (ctx->new_firmware_preference) qmi_message_dms_get_firmware_preference_output_unref (ctx->new_firmware_preference); g_free (ctx->new_revision); g_free (ctx->revision); g_free (ctx->firmware_version); g_free (ctx->config_version); g_free (ctx->carrier); if (ctx->qdl_device) g_object_unref (&ctx->qdl_device); if (ctx->qmi_client) { g_assert (ctx->qmi_device); /* This release only happens when cleaning up from an error, * therefore always release CID */ qmi_device_release_client (ctx->qmi_device, QMI_CLIENT (ctx->qmi_client), QMI_DEVICE_RELEASE_CLIENT_FLAGS_RELEASE_CID, 10, NULL, NULL, NULL); g_object_unref (ctx->qmi_client); } if (ctx->qmi_device) { qmi_device_close (ctx->qmi_device, NULL); g_object_unref (ctx->qmi_device); } if (ctx->current_image) g_object_unref (ctx->current_image); g_list_free_full (ctx->pending_images, (GDestroyNotify) g_object_unref); if (ctx->serial_file) g_object_unref (ctx->serial_file); if (ctx->cdc_wdm_file) g_object_unref (ctx->cdc_wdm_file); g_slice_free (RunContext, ctx); } gboolean qfu_updater_run_finish (QfuUpdater *self, GAsyncResult *res, GError **error) { return g_task_propagate_boolean (G_TASK (res), error); } static void print_firmware_preference (QmiMessageDmsGetFirmwarePreferenceOutput *firmware_preference, const gchar *prefix) { GArray *array; guint i; qmi_message_dms_get_firmware_preference_output_get_list (firmware_preference, &array, NULL); if (array->len > 0) { for (i = 0; i < array->len; i++) { QmiMessageDmsGetFirmwarePreferenceOutputListImage *image; gchar *unique_id_str; image = &g_array_index (array, QmiMessageDmsGetFirmwarePreferenceOutputListImage, i); unique_id_str = qfu_utils_get_firmware_image_unique_id_printable (image->unique_id); g_print ("%simage '%s': unique id '%s', build id '%s'\n", prefix, qmi_dms_firmware_image_type_get_string (image->type), unique_id_str, image->build_id); g_free (unique_id_str); } } else g_debug ("%sno preference specified\n", prefix); } static void print_current_firmware (QmiMessageDmsSwiGetCurrentFirmwareOutput *current_firmware, const gchar *prefix) { const gchar *model = NULL; const gchar *boot_version = NULL; const gchar *amss_version = NULL; const gchar *sku_id = NULL; const gchar *package_id = NULL; const gchar *carrier_id = NULL; const gchar *pri_version = NULL; const gchar *carrier = NULL; const gchar *config_version = NULL; qmi_message_dms_swi_get_current_firmware_output_get_model (current_firmware, &model, NULL); qmi_message_dms_swi_get_current_firmware_output_get_boot_version (current_firmware, &boot_version, NULL); qmi_message_dms_swi_get_current_firmware_output_get_amss_version (current_firmware, &amss_version, NULL); qmi_message_dms_swi_get_current_firmware_output_get_sku_id (current_firmware, &sku_id, NULL); qmi_message_dms_swi_get_current_firmware_output_get_package_id (current_firmware, &package_id, NULL); qmi_message_dms_swi_get_current_firmware_output_get_carrier_id (current_firmware, &carrier_id, NULL); qmi_message_dms_swi_get_current_firmware_output_get_pri_version (current_firmware, &pri_version, NULL); qmi_message_dms_swi_get_current_firmware_output_get_carrier (current_firmware, &carrier, NULL); qmi_message_dms_swi_get_current_firmware_output_get_config_version (current_firmware, &config_version, NULL); if (model) g_print ("%sModel: %s\n", prefix, model); if (boot_version) g_print ("%sBoot version: %s\n", prefix, boot_version); if (amss_version) g_print ("%sAMSS version: %s\n", prefix, amss_version); if (sku_id) g_print ("%sSKU ID: %s\n", prefix, sku_id); if (package_id) g_print ("%sPackage ID: %s\n", prefix, package_id); if (carrier_id) g_print ("%sCarrier ID: %s\n", prefix, carrier_id); if (config_version) g_print ("%sConfig version: %s\n", prefix, config_version); } static void run_context_step_last (GTask *task) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); /* Dump output report */ g_print ("\n" "------------------------------------------------------------------------\n"); g_print ("\n" " original firmware revision was:\n" " %s\n", ctx->revision ? ctx->revision : "unknown"); if (ctx->current_firmware) { g_print (" original running firmware details:\n"); print_current_firmware (ctx->current_firmware, " "); } if (ctx->firmware_preference) { g_print (" original firmware preference details:\n"); print_firmware_preference (ctx->firmware_preference, " "); } g_print ("\n" " new firmware revision is:\n" " %s\n", ctx->new_revision ? ctx->new_revision : "unknown"); if (ctx->new_current_firmware) { g_print (" new running firmware details:\n"); print_current_firmware (ctx->new_current_firmware, " "); } if (ctx->new_firmware_preference) { g_print (" new firmware preference details:\n"); print_firmware_preference (ctx->new_firmware_preference, " "); } if (ctx->new_supports_stored_image_management) g_print ("\n" " NOTE: this device supports stored image management\n" " with qmicli operations:\n" " --dms-list-stored-images\n" " --dms-select-stored-image\n" " --dms-delete-stored-image\n"); if (ctx->new_supports_firmware_preference_management) g_print ("\n" " NOTE: this device supports firmware preference management\n" " with qmicli operations:\n" " --dms-get-firmware-preference\n" " --dms-set-firmware-preference\n"); g_print ("\n" "------------------------------------------------------------------------\n" "\n"); g_task_return_boolean (task, TRUE); g_object_unref (task); } static void run_context_step (GTask *task); static gboolean run_context_step_cb (GTask *task) { run_context_step (task); return FALSE; } static void run_context_step_next (GTask *task, RunContextStep next) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); ctx->step = next; /* Schedule next step in an idle */ g_idle_add ((GSourceFunc) run_context_step_cb, task); } static void run_context_step_next_no_idle (GTask *task, RunContextStep next) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); ctx->step = next; /* Run right away */ run_context_step (task); } static void new_client_dms_after_ready (gpointer unused, GAsyncResult *res, GTask *task) { RunContext *ctx; GError *error = NULL; ctx = (RunContext *) g_task_get_task_data (task); g_assert (!ctx->qmi_device); g_assert (!ctx->qmi_client); /* After the upgrade, non-fatal error if DMS client couldn't be created */ if (!qfu_utils_new_client_dms_finish (res, &ctx->qmi_device, &ctx->qmi_client, &ctx->new_revision, &ctx->new_supports_stored_image_management, NULL, /* we don't care about the max number of images */ &ctx->new_supports_firmware_preference_management, &ctx->new_firmware_preference, &ctx->new_current_firmware, &error)) { if (ctx->wait_for_boot_retries == WAIT_FOR_BOOT_RETRIES) { g_warning ("couldn't create DMS client after upgrade: %s", error->message); run_context_step_next (task, ctx->step + 1); } else { g_debug ("couldn't create DMS client after upgrade: %s (will retry)", error->message); run_context_step_next (task, RUN_CONTEXT_STEP_WAIT_FOR_BOOT); } g_error_free (error); return; } /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_qmi_client_after (GTask *task) { RunContext *ctx; QfuUpdater *self; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); ctx->wait_for_boot_retries++; g_print ("loading device information after the update (%u/%u)...\n", ctx->wait_for_boot_retries, WAIT_FOR_BOOT_RETRIES); g_debug ("[qfu-updater] creating QMI DMS client after upgrade..."); g_assert (ctx->cdc_wdm_file); qfu_utils_new_client_dms (ctx->cdc_wdm_file, 1, /* single try to allocate DMS client */ self->priv->device_open_proxy, self->priv->device_open_mbim, TRUE, g_task_get_cancellable (task), (GAsyncReadyCallback) new_client_dms_after_ready, task); } static gboolean wait_for_boot_ready (GTask *task) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); ctx->wait_for_boot_seconds_elapsed++; if (ctx->wait_for_boot_seconds_elapsed < WAIT_FOR_BOOT_TIMEOUT_SECS) { if (!qfu_log_get_verbose_stdout ()) g_print (CLEAR_LINE "%s %u", progress[ctx->wait_for_boot_seconds_elapsed % G_N_ELEMENTS (progress)], WAIT_FOR_BOOT_TIMEOUT_SECS - ctx->wait_for_boot_seconds_elapsed); return G_SOURCE_CONTINUE; } if (!qfu_log_get_verbose_stdout ()) g_print (CLEAR_LINE); /* Go on */ run_context_step_next (task, ctx->step + 1); return G_SOURCE_REMOVE; } static void run_context_step_wait_for_boot (GTask *task) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); ctx->wait_for_boot_seconds_elapsed = 0; g_debug ("[qfu-updater] waiting some time (%us) before accessing the cdc-wdm device...", WAIT_FOR_BOOT_TIMEOUT_SECS); if (!qfu_log_get_verbose_stdout ()) { g_print ("waiting some time for the device to boot...\n"); g_print ("%s %u", progress[0], WAIT_FOR_BOOT_TIMEOUT_SECS); } g_timeout_add_seconds (1, (GSourceFunc) wait_for_boot_ready, task); } static void wait_for_cdc_wdm_ready (QfuDeviceSelection *device_selection, GAsyncResult *res, GTask *task) { GError *error = NULL; RunContext *ctx; QfuUpdater *self; gchar *path; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); g_assert (!ctx->cdc_wdm_file); ctx->cdc_wdm_file = qfu_device_selection_wait_for_cdc_wdm_finish (device_selection, res, &error); if (!ctx->cdc_wdm_file) { g_prefix_error (&error, "error waiting for cdc-wdm: "); g_task_return_error (task, error); g_object_unref (task); return; } path = g_file_get_path (ctx->cdc_wdm_file); g_debug ("[qfu-updater] cdc-wdm device found: %s", path); g_free (path); g_print ("normal mode detected\n"); /* If no need to validate, we're done */ if (self->priv->skip_validation) { run_context_step_next (task, RUN_CONTEXT_STEP_LAST); return; } g_print ("\n" "------------------------------------------------------------------------\n" " NOTE: in order to validate which is the firmware running in the\n" " module, the program will wait for a complete boot; this process\n" " may take some time and several retries.\n" "------------------------------------------------------------------------\n" "\n"); /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_wait_for_cdc_wdm (GTask *task) { QfuUpdater *self; self = g_task_get_source_object (task); g_debug ("[qfu-updater] now waiting for cdc-wdm device..."); qfu_device_selection_wait_for_cdc_wdm (self->priv->device_selection, g_task_get_cancellable (task), (GAsyncReadyCallback) wait_for_cdc_wdm_ready, task); } static void run_context_step_cleanup_qdl_device (GTask *task) { RunContext *ctx; QfuUpdater *self; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); g_assert (ctx->qdl_device); g_assert (ctx->serial_file); g_debug ("[qfu-updater] QDL reset"); qfu_qdl_device_reset (ctx->qdl_device, g_task_get_cancellable (task), NULL); g_clear_object (&ctx->qdl_device); g_clear_object (&ctx->serial_file); g_print ("rebooting in normal mode...\n"); /* If we were running in QDL mode, we don't even wait for the reboot to finish */ if (self->priv->type == UPDATER_TYPE_QDL) { run_context_step_next (task, RUN_CONTEXT_STEP_LAST); return; } run_context_step_next (task, ctx->step + 1); } static void run_context_step_cleanup_image (GTask *task) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); g_assert (ctx->current_image); g_clear_object (&ctx->current_image); /* Select next image */ if (ctx->pending_images) { run_context_step_next (task, RUN_CONTEXT_STEP_SELECT_IMAGE); return; } /* If no more files to download, we're done! */ g_debug ("[qfu-updater] no more files to download"); run_context_step_next (task, ctx->step + 1); } static void run_context_step_download_image (GTask *task) { RunContext *ctx; guint16 sequence; guint16 n_chunks; GError *error = NULL; GCancellable *cancellable; GTimer *timer; gdouble elapsed; gchar *aux; ctx = (RunContext *) g_task_get_task_data (task); cancellable = g_task_get_cancellable (task); timer = g_timer_new (); aux = g_format_size ((guint64) qfu_image_get_size (ctx->current_image)); g_print ("downloading %s image: %s (%s)...\n", qfu_image_type_get_string (qfu_image_get_image_type (ctx->current_image)), qfu_image_get_display_name (ctx->current_image), aux); g_free (aux); if (!qfu_qdl_device_hello (ctx->qdl_device, cancellable, &error)) { g_prefix_error (&error, "couldn't send greetings to device: "); goto out; } if (!qfu_qdl_device_ufopen (ctx->qdl_device, ctx->current_image, cancellable, &error)) { g_prefix_error (&error, "couldn't open session: "); goto out; } n_chunks = qfu_image_get_n_data_chunks (ctx->current_image); for (sequence = 0; sequence < n_chunks; sequence++) { if (!qfu_log_get_verbose_stdout ()) { /* Use n-1 chunks for progress reporting; because the last one will take * a lot longer. */ if (n_chunks > 1 && sequence < (n_chunks - 1)) g_print (CLEAR_LINE "%s %04.1lf%%", progress[sequence % G_N_ELEMENTS (progress)], 100.0 * ((gdouble) sequence / (gdouble) (n_chunks - 1))); else if (sequence == (n_chunks - 1)) g_print (CLEAR_LINE "finalizing download... (may take more than one minute, be patient)\n"); } if (!qfu_qdl_device_ufwrite (ctx->qdl_device, ctx->current_image, sequence, cancellable, &error)) { g_prefix_error (&error, "couldn't write in session: "); goto out; } } g_debug ("[qfu-updater] all chunks ack-ed"); if (!qfu_log_get_verbose_stdout ()) g_print (CLEAR_LINE); if (!qfu_qdl_device_ufclose (ctx->qdl_device, cancellable, &error)) { g_prefix_error (&error, "couldn't close session: "); goto out; } out: elapsed = g_timer_elapsed (timer, NULL); g_timer_destroy (timer); if (error) { g_prefix_error (&error, "error downloading image: "); g_task_return_error (task, error); return; } aux = g_format_size ((guint64) ((qfu_image_get_size (ctx->current_image)) / elapsed)); g_print ("successfully downloaded in %.2lfs (%s/s)\n", elapsed, aux); g_free (aux); /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_select_image (GTask *task) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); g_assert (!ctx->current_image); g_assert (ctx->pending_images); /* Select new current image */ ctx->current_image = QFU_IMAGE (ctx->pending_images->data); ctx->pending_images = g_list_delete_link (ctx->pending_images, ctx->pending_images); g_debug ("[qfu-updater] selected file '%s' (%" G_GOFFSET_FORMAT " bytes)", qfu_image_get_display_name (ctx->current_image), qfu_image_get_size (ctx->current_image)); /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_qdl_device (GTask *task) { RunContext *ctx; GError *error = NULL; ctx = (RunContext *) g_task_get_task_data (task); g_assert (ctx->serial_file); g_assert (!ctx->qdl_device); ctx->qdl_device = qfu_qdl_device_new (ctx->serial_file, g_task_get_cancellable (task), &error); if (!ctx->qdl_device) { g_prefix_error (&error, "error creating device: "); g_task_return_error (task, error); g_object_unref (task); return; } run_context_step_next (task, ctx->step + 1); } static void wait_for_tty_ready (QfuDeviceSelection *device_selection, GAsyncResult *res, GTask *task) { GError *error = NULL; RunContext *ctx; gchar *path; ctx = (RunContext *) g_task_get_task_data (task); g_assert (!ctx->serial_file); ctx->serial_file = qfu_device_selection_wait_for_tty_finish (device_selection, res, &error); if (!ctx->serial_file) { g_prefix_error (&error, "error waiting for TTY: "); g_task_return_error (task, error); g_object_unref (task); return; } path = g_file_get_path (ctx->serial_file); g_debug ("[qfu-updater] TTY device found: %s", path); g_free (path); g_print ("download mode detected\n"); /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_wait_for_tty (GTask *task) { QfuUpdater *self; self = g_task_get_source_object (task); g_print ("rebooting in download mode...\n"); g_debug ("[qfu-updater] reset requested, now waiting for TTY device..."); qfu_device_selection_wait_for_tty (self->priv->device_selection, g_task_get_cancellable (task), (GAsyncReadyCallback) wait_for_tty_ready, task); } static void run_context_step_cleanup_qmi_device (GTask *task) { RunContext *ctx; QmiDevice *tmp; ctx = (RunContext *) g_task_get_task_data (task); g_debug ("[qfu-updater] cleaning up QMI device..."); /* We don't release CID as we're going to reset anyway */ qmi_device_release_client (ctx->qmi_device, QMI_CLIENT (ctx->qmi_client), QMI_DEVICE_RELEASE_CLIENT_FLAGS_NONE, 10, NULL, NULL, NULL); g_clear_object (&ctx->qmi_client); /* We want to close and unref the QmiDevice only AFTER having set the wait * tasks for cdc-wdm or tty devices. This is because the close operation may * take a long time if doing QMI over MBIM (as the MBIM close async * operation is run internally). If we don't do this in this sequence, we * may end up getting udev events reported before the wait have started. */ tmp = g_object_ref (ctx->qmi_device); g_clear_object (&ctx->qmi_device); g_clear_object (&ctx->cdc_wdm_file); /* If nothing to download we don't need to wait for QDL download mode */ if (!ctx->pending_images) run_context_step_next_no_idle (task, RUN_CONTEXT_STEP_WAIT_FOR_CDC_WDM); else /* Go on */ run_context_step_next_no_idle (task, ctx->step + 1); /* After the wait operation has been started, we do run the close */ qmi_device_close (tmp, NULL); g_object_unref (tmp); } static void reseter_run_ready (QfuReseter *reseter, GAsyncResult *res, GTask *task) { GError *error = NULL; RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); if (!qfu_reseter_run_finish (reseter, res, &error)) { g_prefix_error (&error, "boothold reseter operation failed: "); g_task_return_error (task, error); g_object_unref (task); return; } g_debug ("[qfu-updater] boothold reset requested successfully..."); /* Go on */ run_context_step_next (task, ctx->step + 1); } static void power_cycle_ready (QmiClientDms *qmi_client, GAsyncResult *res, GTask *task) { GError *error = NULL; RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); if (!qfu_utils_power_cycle_finish (qmi_client, res, &error)) { g_task_return_error (task, error); g_object_unref (task); return; } g_debug ("[qfu-updater] reset requested successfully..."); /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_power_cycle (GTask *task) { RunContext *ctx; QfuUpdater *self; QfuReseter *reseter; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); g_debug ("[qfu-updater] power cycling..."); if (!ctx->boothold_reset) { qfu_utils_power_cycle (ctx->qmi_client, g_task_get_cancellable (task), (GAsyncReadyCallback) power_cycle_ready, task); return; } /* Boothold is required when firmware preference isn't supported; and if so, * there must always be images to download. The only case in which we don't * have images listed for download is when we're told that there is nothing * to download via firmware preference. */ g_assert (ctx->pending_images != NULL); /* Boothold reset */ reseter = qfu_reseter_new (self->priv->device_selection, ctx->qmi_client, FALSE, FALSE); qfu_reseter_run (reseter, g_task_get_cancellable (task), (GAsyncReadyCallback) reseter_run_ready, task); g_object_unref (reseter); } static void set_firmware_preference_ready (QmiClientDms *client, GAsyncResult *res, GTask *task) { RunContext *ctx; QmiMessageDmsSetFirmwarePreferenceOutput *output; GError *error = NULL; GArray *array = NULL; guint next_step; ctx = (RunContext *) g_task_get_task_data (task); output = qmi_client_dms_set_firmware_preference_finish (client, res, &error); if (!output) { g_prefix_error (&error, "QMI operation failed, couldn't set firmware preference: "); g_task_return_error (task, error); g_object_unref (task); return; } if (!qmi_message_dms_set_firmware_preference_output_get_result (output, &error)) { g_prefix_error (&error, "couldn't set firmware preference: "); g_task_return_error (task, error); g_object_unref (task); qmi_message_dms_set_firmware_preference_output_unref (output); return; } /* We'll go to next step, unless told to finish early */ next_step = ctx->step + 1; /* list images we need to download? */ if (qmi_message_dms_set_firmware_preference_output_get_image_download_list (output, &array, &error)) { if (!array->len) { g_print ("device already contains the given firmware/config version: no download needed\n"); g_print ("forcing the download may be requested with the --override-download option\n"); g_print ("now power cycling to apply the new firmware preference...\n"); g_list_free_full (ctx->pending_images, (GDestroyNotify) g_object_unref); ctx->pending_images = NULL; } else { GString *images = NULL; QmiDmsFirmwareImageType type; guint i; images = g_string_new (""); for (i = 0; i < array->len; i++) { type = g_array_index (array, QmiDmsFirmwareImageType, i); g_string_append (images, qmi_dms_firmware_image_type_get_string (type)); if (i < array->len -1) g_string_append (images, ", "); } g_debug ("[qfu-updater] need to download the following images: %s", images->str); g_string_free (images, TRUE); } } qmi_message_dms_set_firmware_preference_output_unref (output); /* Go on */ run_context_step_next (task, next_step); } static void run_context_step_set_firmware_preference (GTask *task) { QfuUpdater *self; RunContext *ctx; QmiMessageDmsSetFirmwarePreferenceInput *input; GArray *array; QmiMessageDmsSetFirmwarePreferenceInputListImage modem_image_id; QmiMessageDmsSetFirmwarePreferenceInputListImage pri_image_id; const gchar *firmware_version; const gchar *config_version; const gchar *carrier; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); firmware_version = self->priv->firmware_version ? self->priv->firmware_version : ctx->firmware_version; config_version = self->priv->config_version ? self->priv->config_version : ctx->config_version; carrier = self->priv->carrier ? self->priv->carrier : ctx->carrier; g_assert (firmware_version); g_assert (config_version); g_assert (carrier); g_print ("setting firmware preference:\n"); g_print (" firmware version: '%s'\n", firmware_version); g_print (" config version: '%s'\n", config_version); g_print (" carrier: '%s'\n", carrier); /* Set modem image info */ modem_image_id.type = QMI_DMS_FIRMWARE_IMAGE_TYPE_MODEM; modem_image_id.unique_id = g_array_sized_new (FALSE, TRUE, sizeof (gchar), 16); g_array_insert_vals (modem_image_id.unique_id, 0, "?_?", 3); g_array_set_size (modem_image_id.unique_id, 16); modem_image_id.build_id = g_strdup_printf ("%s_?", firmware_version); /* Set pri image info */ pri_image_id.type = QMI_DMS_FIRMWARE_IMAGE_TYPE_PRI; pri_image_id.unique_id = g_array_sized_new (FALSE, TRUE, sizeof (gchar), 16); g_array_insert_vals (pri_image_id.unique_id, 0, config_version, strlen (config_version)); g_array_set_size (pri_image_id.unique_id, 16); pri_image_id.build_id = g_strdup_printf ("%s_%s", firmware_version, carrier); array = g_array_sized_new (FALSE, FALSE, sizeof (QmiMessageDmsSetFirmwarePreferenceInputListImage), 2); g_array_append_val (array, modem_image_id); g_array_append_val (array, pri_image_id); input = qmi_message_dms_set_firmware_preference_input_new (); qmi_message_dms_set_firmware_preference_input_set_list (input, array, NULL); g_array_unref (array); if (self->priv->override_download) qmi_message_dms_set_firmware_preference_input_set_download_override (input, TRUE, NULL); if (self->priv->modem_storage_index > 0) qmi_message_dms_set_firmware_preference_input_set_modem_storage_index (input, (guint8) self->priv->modem_storage_index, NULL); g_debug ("[qfu-updater] setting firmware preference..."); g_debug ("[qfu-updater] modem image: unique id '%.16s', build id '%s'", (gchar *) (modem_image_id.unique_id->data), modem_image_id.build_id); g_debug ("[qfu-updater] pri image: unique id '%.16s', build id '%s'", (gchar *) (pri_image_id.unique_id->data), pri_image_id.build_id); g_debug ("[qfu-updater] override download: %s", self->priv->override_download ? "yes" : "no"); qmi_client_dms_set_firmware_preference (ctx->qmi_client, input, 10, g_task_get_cancellable (task), (GAsyncReadyCallback) set_firmware_preference_ready, task); g_array_unref (modem_image_id.unique_id); g_free (modem_image_id.build_id); g_array_unref (pri_image_id.unique_id); g_free (pri_image_id.build_id); qmi_message_dms_set_firmware_preference_input_unref (input); } static gboolean validate_firmware_config_carrier (QfuUpdater *self, RunContext *ctx, GError **error) { GList *l; /* Try to preload information like firmware/config/carrier from CWE images */ for (l = ctx->pending_images; l; l = g_list_next (l)) { const gchar *firmware_version; const gchar *config_version; const gchar *carrier; QfuImageCwe *image; if (!QFU_IS_IMAGE_CWE (l->data)) continue; image = QFU_IMAGE_CWE (l->data); firmware_version = qfu_image_cwe_get_parsed_firmware_version (image); config_version = qfu_image_cwe_get_parsed_config_version (image); carrier = qfu_image_cwe_get_parsed_carrier (image); if (firmware_version) { if (!ctx->firmware_version) ctx->firmware_version = g_strdup (firmware_version); else if (!g_str_equal (firmware_version, ctx->firmware_version)) { if (!self->priv->ignore_version_errors) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "couldn't detect firmware version: " "firmware version strings don't match on specified images: " "'%s' != '%s'", firmware_version, ctx->firmware_version); return FALSE; } g_warning ("firmware version strings don't match on specified images: " "'%s' != '%s' (IGNORED with --ignore-version-errors)", firmware_version, ctx->firmware_version); } } if (config_version) { if (!ctx->config_version) ctx->config_version = g_strdup (config_version); else if (!g_str_equal (config_version, ctx->config_version)) { if (!self->priv->ignore_version_errors) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "couldn't detect config version: " "config version strings don't match on specified images: " "'%s' != '%s'", config_version, ctx->config_version); return FALSE; } g_warning ("[qfu-updater] config version strings don't match on specified images: " "'%s' != '%s' (IGNORED with --ignore-version-errors)", config_version, ctx->config_version); } } if (carrier) { if (!ctx->carrier) ctx->carrier = g_strdup (carrier); else if (!g_str_equal (carrier, ctx->carrier)) { if (!self->priv->ignore_version_errors) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "couldn't detect carrier: " "carrier strings don't match on specified images: " "'%s' != '%s'", carrier, ctx->carrier); return FALSE; } g_warning ("[qfu-updater] carrier strings don't match on specified images: " "'%s' != '%s' (IGNORED with --ignore-version-errors)", carrier, ctx->carrier); } } } /* If given firmware version doesn't match the one in the image, error out */ if (self->priv->firmware_version && (g_strcmp0 (self->priv->firmware_version, ctx->firmware_version) != 0)) { if (!self->priv->ignore_version_errors) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "error validating firmware version: " "user provided firmware version doesn't match the one in the specified images: " "'%s' != '%s'", self->priv->firmware_version, ctx->firmware_version); return FALSE; } g_warning ("[qfu-updater] user provided firmware version doesn't match the one in the specified images: " "'%s' != '%s' (IGNORED with --ignore-version-errors)", self->priv->firmware_version, ctx->firmware_version); } /* If given config version doesn't match the one in the image, error out */ if (self->priv->config_version && (g_strcmp0 (self->priv->config_version, ctx->config_version) != 0)) { if (!self->priv->ignore_version_errors) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "error validating firmware version: " "user provided firmware version doesn't match the one in the specified images: " "'%s' != '%s'", self->priv->config_version, ctx->config_version); return FALSE; } g_warning ("[qfu-updater] user provided config version doesn't match the one in the specified images: " "'%s' != '%s' (IGNORED with --ignore-version-errors)", self->priv->firmware_version, ctx->firmware_version); } /* If given carrier doesn't match the one in the image, error out */ if (self->priv->carrier && (g_strcmp0 (self->priv->carrier, ctx->carrier) != 0)) { if (!self->priv->ignore_version_errors) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_PERMISSION_DENIED, "error validating carrier: " "user provided carrier doesn't match the one in the specified images: " "'%s' != '%s'", self->priv->carrier, ctx->carrier); } g_warning ("[qfu-updater] user provided carrier doesn't match the one in the specified images: " "'%s' != '%s' (IGNORED with --ignore-version-errors)", self->priv->carrier, ctx->carrier); return FALSE; } /* No firmware version? */ if (!self->priv->firmware_version && !ctx->firmware_version) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "firmware version required"); return FALSE; } /* No config version? */ if (!self->priv->config_version && !ctx->config_version) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "config version required"); return FALSE; } /* No carrier? */ if (!self->priv->carrier && !ctx->carrier) { g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "carrier required"); return FALSE; } return TRUE; } static void run_context_step_get_firmware_preference (GTask *task) { RunContext *ctx; QfuUpdater *self; GError *error = NULL; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); if (!ctx->supports_firmware_preference_management) { /* Firmware preference setting not supported; fail if we got those settings explicitly */ if (self->priv->firmware_version || self->priv->config_version || self->priv->carrier) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "setting firmware/config/carrier is not supported by this device"); g_object_unref (task); return; } /* Jump to the reset step and run boothold there */ ctx->boothold_reset = TRUE; run_context_step_next (task, RUN_CONTEXT_STEP_POWER_CYCLE); return; } /* Firmware preference setting is supported so we require firmware/config/carrier */ if (!validate_firmware_config_carrier (self, ctx, &error)) { g_task_return_error (task, error); g_object_unref (task); return; } /* Go on */ run_context_step_next (task, ctx->step + 1); } static void new_client_dms_ready (gpointer unused, GAsyncResult *res, GTask *task) { RunContext *ctx; QfuUpdater *self; GError *error = NULL; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); g_assert (!ctx->qmi_device); g_assert (!ctx->qmi_client); if (!qfu_utils_new_client_dms_finish (res, &ctx->qmi_device, &ctx->qmi_client, &ctx->revision, &ctx->supports_stored_image_management, &ctx->max_modem_storage_index, &ctx->supports_firmware_preference_management, &ctx->firmware_preference, &ctx->current_firmware, &error)) { g_task_return_error (task, error); g_object_unref (task); return; } /* Validate modem storage index, if one specified */ if (self->priv->modem_storage_index > ctx->max_modem_storage_index) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "modem storage index out of bounds (%u > %u)", self->priv->modem_storage_index, ctx->max_modem_storage_index); g_object_unref (task); return; } /* Go on */ run_context_step_next (task, ctx->step + 1); } static void run_context_step_qmi_client (GTask *task) { RunContext *ctx; QfuUpdater *self; ctx = (RunContext *) g_task_get_task_data (task); self = g_task_get_source_object (task); g_print ("loading device information before the update...\n"); g_debug ("[qfu-updater] creating QMI DMS client..."); g_assert (ctx->cdc_wdm_file); qfu_utils_new_client_dms (ctx->cdc_wdm_file, 3, /* initially, up to 3 tries to allocate DMS client */ self->priv->device_open_proxy, self->priv->device_open_mbim, TRUE, g_task_get_cancellable (task), (GAsyncReadyCallback) new_client_dms_ready, task); } typedef void (* RunContextStepFunc) (GTask *task); static const RunContextStepFunc run_context_step_func[] = { [RUN_CONTEXT_STEP_QMI_CLIENT] = run_context_step_qmi_client, [RUN_CONTEXT_STEP_GET_FIRMWARE_PREFERENCE] = run_context_step_get_firmware_preference, [RUN_CONTEXT_STEP_SET_FIRMWARE_PREFERENCE] = run_context_step_set_firmware_preference, [RUN_CONTEXT_STEP_POWER_CYCLE] = run_context_step_power_cycle, [RUN_CONTEXT_STEP_CLEANUP_QMI_DEVICE] = run_context_step_cleanup_qmi_device, [RUN_CONTEXT_STEP_WAIT_FOR_TTY] = run_context_step_wait_for_tty, [RUN_CONTEXT_STEP_QDL_DEVICE] = run_context_step_qdl_device, [RUN_CONTEXT_STEP_SELECT_IMAGE] = run_context_step_select_image, [RUN_CONTEXT_STEP_DOWNLOAD_IMAGE] = run_context_step_download_image, [RUN_CONTEXT_STEP_CLEANUP_IMAGE] = run_context_step_cleanup_image, [RUN_CONTEXT_STEP_CLEANUP_QDL_DEVICE] = run_context_step_cleanup_qdl_device, [RUN_CONTEXT_STEP_WAIT_FOR_CDC_WDM] = run_context_step_wait_for_cdc_wdm, [RUN_CONTEXT_STEP_QMI_CLIENT_AFTER] = run_context_step_qmi_client_after, [RUN_CONTEXT_STEP_WAIT_FOR_BOOT] = run_context_step_wait_for_boot, }; G_STATIC_ASSERT (G_N_ELEMENTS (run_context_step_func) == RUN_CONTEXT_STEP_LAST); static void run_context_step (GTask *task) { RunContext *ctx; ctx = (RunContext *) g_task_get_task_data (task); /* Early halt operation if cancelled */ if (g_task_return_error_if_cancelled (task)) { g_object_unref (task); return; } if (ctx->step < G_N_ELEMENTS (run_context_step_func)) { run_context_step_func [ctx->step] (task); return; } g_debug ("[qfu-updater] operation finished"); run_context_step_last (task); } static gint image_sort_by_size (QfuImage *a, QfuImage *b) { return qfu_image_get_size (b) - qfu_image_get_size (a); } static gboolean preload_images (RunContext *ctx, GList *image_file_list, GCancellable *cancellable, GError **error) { GList *l; /* Build QfuImage objects for each image file given */ for (l = image_file_list; l; l = g_list_next (l)) { QfuImage *image; image = qfu_image_factory_build (G_FILE (l->data), cancellable, error); if (!image) return FALSE; ctx->pending_images = g_list_append (ctx->pending_images, image); } /* Sort by size, we want to download bigger images first, as that is usually * the use case anyway, first flash e.g. the .cwe file, then the .nvu one. */ ctx->pending_images = g_list_sort (ctx->pending_images, (GCompareFunc) image_sort_by_size); return TRUE; } void qfu_updater_run (QfuUpdater *self, GList *image_file_list, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { RunContext *ctx; GTask *task; GError *error = NULL; g_assert (image_file_list); ctx = g_slice_new0 (RunContext); task = g_task_new (self, cancellable, callback, user_data); g_task_set_task_data (task, ctx, (GDestroyNotify) run_context_free); if (!preload_images (ctx, image_file_list, cancellable, &error)) { g_task_return_error (task, error); g_object_unref (task); return; } switch (self->priv->type) { case UPDATER_TYPE_GENERIC: ctx->step = RUN_CONTEXT_STEP_QMI_CLIENT; ctx->cdc_wdm_file = qfu_device_selection_get_single_cdc_wdm (self->priv->device_selection); if (!ctx->cdc_wdm_file) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "No cdc-wdm device found to run update operation"); g_object_unref (task); return; } break; case UPDATER_TYPE_QDL: ctx->step = RUN_CONTEXT_STEP_QDL_DEVICE; ctx->serial_file = qfu_device_selection_get_single_tty (self->priv->device_selection); if (!ctx->serial_file) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, "No serial device found to run QDL update operation"); g_object_unref (task); return; } break; default: g_assert_not_reached (); } run_context_step (task); } /******************************************************************************/ QfuUpdater * qfu_updater_new (QfuDeviceSelection *device_selection, const gchar *firmware_version, const gchar *config_version, const gchar *carrier, gboolean device_open_proxy, gboolean device_open_mbim, gboolean ignore_version_errors, gboolean override_download, guint8 modem_storage_index, gboolean skip_validation) { QfuUpdater *self; g_assert (QFU_IS_DEVICE_SELECTION (device_selection)); self = g_object_new (QFU_TYPE_UPDATER, NULL); self->priv->type = UPDATER_TYPE_GENERIC; self->priv->device_selection = g_object_ref (device_selection); self->priv->device_open_proxy = device_open_proxy; self->priv->device_open_mbim = device_open_mbim; self->priv->firmware_version = (firmware_version ? g_strdup (firmware_version) : NULL); self->priv->config_version = (config_version ? g_strdup (config_version) : NULL); self->priv->carrier = (carrier ? g_strdup (carrier) : NULL); self->priv->ignore_version_errors = ignore_version_errors; self->priv->override_download = override_download; self->priv->modem_storage_index = modem_storage_index; self->priv->skip_validation = skip_validation; return self; } QfuUpdater * qfu_updater_new_qdl (QfuDeviceSelection *device_selection) { QfuUpdater *self; g_assert (QFU_IS_DEVICE_SELECTION (device_selection)); self = g_object_new (QFU_TYPE_UPDATER, NULL); self->priv->type = UPDATER_TYPE_QDL; self->priv->device_selection = g_object_ref (device_selection); return self; } static void qfu_updater_init (QfuUpdater *self) { self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, QFU_TYPE_UPDATER, QfuUpdaterPrivate); self->priv->type = UPDATER_TYPE_UNKNOWN; } static void dispose (GObject *object) { QfuUpdater *self = QFU_UPDATER (object); g_clear_object (&self->priv->device_selection); G_OBJECT_CLASS (qfu_updater_parent_class)->dispose (object); } static void finalize (GObject *object) { QfuUpdater *self = QFU_UPDATER (object); g_free (self->priv->firmware_version); g_free (self->priv->config_version); g_free (self->priv->carrier); G_OBJECT_CLASS (qfu_updater_parent_class)->finalize (object); } static void qfu_updater_class_init (QfuUpdaterClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); g_type_class_add_private (object_class, sizeof (QfuUpdaterPrivate)); object_class->dispose = dispose; object_class->finalize = finalize; }