| /* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
| /* |
| * Qfu-firmware-update -- Command line tool to update firmware in QFU 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 <http://www.gnu.org/licenses/>. |
| * |
| * Copyright (C) 2019 Zodiac Inflight Innovations |
| * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es> |
| */ |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/stat.h> |
| #include <termios.h> |
| #include <unistd.h> |
| |
| #include <glib-object.h> |
| #include <gio/gio.h> |
| |
| #include "qfu-log.h" |
| #include "qfu-firehose-message.h" |
| #include "qfu-sahara-message.h" |
| #include "qfu-sahara-device.h" |
| #include "qfu-utils.h" |
| #include "qfu-enum-types.h" |
| |
| static void initable_iface_init (GInitableIface *iface); |
| |
| G_DEFINE_TYPE_EXTENDED (QfuSaharaDevice, qfu_sahara_device, G_TYPE_OBJECT, 0, |
| G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)) |
| |
| enum { |
| PROP_0, |
| PROP_FILE, |
| PROP_LAST |
| }; |
| |
| static GParamSpec *properties[PROP_LAST]; |
| |
| #define DEFAULT_SECTOR_SIZE_IN_BYTES 4096 |
| #define DEFAULT_PAGES_IN_BLOCK 64 |
| #define MAX_PRINTABLE_SIZE 80 |
| |
| struct _QfuSaharaDevicePrivate { |
| GFile *file; |
| gint fd; |
| GByteArray *buffer; |
| |
| /* target and transfer settings */ |
| guint max_payload_size_to_target_in_bytes; |
| guint sector_size_in_bytes; |
| guint pages_in_block; |
| /* computed from settings */ |
| guint transfer_block_size; |
| /* number of images setup */ |
| guint n_setup_images; |
| }; |
| |
| /******************************************************************************/ |
| /* Split response into multiple XML messages */ |
| |
| #define XML_START_TAG "<?xml" |
| |
| static gchar ** |
| split_xml_document (const gchar *rsp) |
| { |
| GPtrArray *xml_docs = NULL; |
| const gchar *aux; |
| const gchar *next; |
| |
| xml_docs = g_ptr_array_new (); |
| aux = strstr (rsp, XML_START_TAG); |
| |
| while (aux) { |
| gchar *xmldoc; |
| |
| next = strstr (aux + 1, XML_START_TAG); |
| if (next) |
| xmldoc = g_strndup (aux, next - aux); |
| else |
| xmldoc = g_strdup (aux); |
| g_strdelimit (xmldoc, "\r\n", ' '); |
| g_ptr_array_add (xml_docs, xmldoc); |
| aux = next; |
| } |
| |
| if (xml_docs->len == 0) { |
| g_ptr_array_free (xml_docs, TRUE); |
| return NULL; |
| } |
| g_ptr_array_add (xml_docs, NULL); |
| return (gchar **) g_ptr_array_free (xml_docs, FALSE); |
| } |
| |
| /******************************************************************************/ |
| /* Send */ |
| |
| static gboolean |
| send_request (QfuSaharaDevice *self, |
| const guint8 *request, |
| gsize request_size, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| gssize wlen; |
| fd_set wr; |
| gint aux; |
| struct timeval tv = { |
| .tv_sec = 2, |
| .tv_usec = 0, |
| }; |
| |
| /* Wait for the fd to be writable and don't wait forever */ |
| FD_ZERO (&wr); |
| FD_SET (self->priv->fd, &wr); |
| aux = select (self->priv->fd + 1, NULL, &wr, NULL, &tv); |
| |
| if (g_cancellable_set_error_if_cancelled (cancellable, error)) |
| return FALSE; |
| |
| if (aux < 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error waiting to write: %s", |
| g_strerror (errno)); |
| return FALSE; |
| } |
| |
| if (aux == 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "timed out waiting to write"); |
| return FALSE; |
| } |
| |
| /* Debug output */ |
| if (qfu_log_get_verbose ()) { |
| gchar *printable; |
| gsize printable_size = request_size; |
| gboolean shorted = FALSE; |
| |
| if (printable_size > MAX_PRINTABLE_SIZE) { |
| printable_size = MAX_PRINTABLE_SIZE; |
| shorted = TRUE; |
| } |
| |
| printable = qfu_utils_str_hex (request, printable_size, ':'); |
| g_debug ("[qfu-sahara-device] >> %s%s [%" G_GSIZE_FORMAT "]", printable, shorted ? "..." : "", request_size); |
| g_free (printable); |
| |
| if (strncmp ((const gchar *)request, XML_START_TAG, strlen (XML_START_TAG)) == 0) { |
| printable = g_strdup ((const gchar *)request); |
| g_strdelimit (printable, "\r\n", ' '); |
| g_debug ("[qfu-sahara-device] >> %s", printable); |
| g_free (printable); |
| } |
| } |
| |
| wlen = write (self->priv->fd, request, request_size); |
| if (wlen < 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error writting: %s", |
| g_strerror (errno)); |
| return FALSE; |
| } |
| |
| /* We treat EINTR as an error, so we also treat as an error if not all bytes |
| * were wlen */ |
| if ((gsize)wlen != request_size) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error writing: only %" G_GSSIZE_FORMAT "/%" G_GSIZE_FORMAT " bytes written", |
| wlen, request_size); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| /* Receive */ |
| |
| static gssize |
| receive_response (QfuSaharaDevice *self, |
| guint timeout_secs, |
| guint8 **response, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| fd_set rd; |
| struct timeval tv; |
| gint aux; |
| gssize rlen; |
| |
| /* Use requested timeout */ |
| tv.tv_sec = timeout_secs; |
| tv.tv_usec = 0; |
| |
| FD_ZERO (&rd); |
| FD_SET (self->priv->fd, &rd); |
| aux = select (self->priv->fd + 1, &rd, NULL, NULL, &tv); |
| |
| if (g_cancellable_set_error_if_cancelled (cancellable, error)) |
| return -1; |
| |
| if (aux < 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error waiting to read response: %s", |
| g_strerror (errno)); |
| return -1; |
| } |
| |
| /* we may not always get a response, so just return 0 bytes read if we timeout */ |
| if (aux == 0) |
| return 0; |
| |
| /* Receive in the primary buffer |
| * Always leave room for setting next byte as NUL. */ |
| memset (self->priv->buffer->data, 0, self->priv->buffer->len); |
| rlen = read (self->priv->fd, self->priv->buffer->data, self->priv->buffer->len - 1); |
| if (rlen < 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "couldn't read response: %s", |
| g_strerror (errno)); |
| return -1; |
| } |
| |
| if (rlen == 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "couldn't read response: HUP detected"); |
| return -1; |
| } |
| |
| /* make sure that we can treat the response as a NUL-terminated string */ |
| g_assert ((guint)rlen <= self->priv->buffer->len - 1); |
| self->priv->buffer->data[rlen] = '\0'; |
| |
| /* Debug output */ |
| if (qfu_log_get_verbose ()) { |
| gchar *printable; |
| gsize printable_size = rlen; |
| gboolean shorted = FALSE; |
| |
| if (printable_size > MAX_PRINTABLE_SIZE) { |
| printable_size = MAX_PRINTABLE_SIZE; |
| shorted = TRUE; |
| } |
| |
| printable = qfu_utils_str_hex (self->priv->buffer->data, printable_size, ':'); |
| g_debug ("[qfu-sahara-device] << %s%s [%" G_GSIZE_FORMAT "]", printable, shorted ? "..." : "", rlen); |
| g_free (printable); |
| |
| if (strncmp ((const gchar *)self->priv->buffer->data, XML_START_TAG, strlen (XML_START_TAG)) == 0) { |
| printable = g_strdup ((const gchar *)self->priv->buffer->data); |
| g_strdelimit (printable, "\r\n", ' '); |
| g_debug ("[qfu-sahara-device] << %s", printable); |
| g_free (printable); |
| } |
| } |
| |
| if (response) |
| *response = self->priv->buffer->data; |
| |
| return rlen; |
| } |
| |
| /******************************************************************************/ |
| /* Send/receive */ |
| |
| static gssize |
| send_receive (QfuSaharaDevice *self, |
| const guint8 *request, |
| gsize request_size, |
| guint response_timeout_secs, |
| guint8 **response, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| gboolean sent; |
| |
| if (self->priv->fd < 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "device is closed"); |
| return -1; |
| } |
| |
| if (request_size > 0) { |
| sent = send_request (self, request, request_size, cancellable, error); |
| if (!sent) |
| return -1; |
| } |
| |
| if (!response) |
| return 0; |
| |
| return receive_response (self, response_timeout_secs, response, cancellable, error); |
| } |
| |
| /******************************************************************************/ |
| /* Common firehose state machine */ |
| |
| static gboolean |
| firehose_common_process_response_ack_message (const gchar *rsp, |
| const gchar *expected_value, |
| const gchar *expected_rawmode, |
| GError **error) |
| { |
| gchar *value = NULL; |
| gchar *rawmode = NULL; |
| |
| g_assert (expected_value); |
| |
| if (!qfu_firehose_message_parse_response_ack (rsp, &value, &rawmode)) |
| return FALSE; |
| |
| if ((g_strcmp0 (value, expected_value) == 0) && (!expected_rawmode || (rawmode && g_strcmp0 (rawmode, expected_rawmode) == 0))) |
| g_debug ("[qfu-sahara-device] firehose response received: value=%s, rawmode=%s", |
| value, rawmode ? rawmode : "n/a"); |
| else |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "unexpected firehose response received: value=%s, rawmode=%s", |
| value, rawmode ? rawmode : "n/a"); |
| |
| g_free (value); |
| g_free (rawmode); |
| return TRUE; |
| } |
| |
| static gboolean |
| firehose_common_process_log_message (const gchar *rsp) |
| { |
| gchar *value = NULL; |
| |
| if (!qfu_firehose_message_parse_log (rsp, &value)) |
| return FALSE; |
| |
| g_debug ("[qfu-sahara-device] firehose log: %s", value); |
| g_free (value); |
| return TRUE; |
| } |
| |
| typedef const gchar * (* PrepareRequestCallback) (QfuSaharaDevice *self, |
| gpointer user_data); |
| typedef gboolean (* ProcessResponseCallback) (QfuSaharaDevice *self, |
| const gchar *rsp, |
| gpointer user_data, |
| GError **error); |
| typedef gboolean (* CheckCompletionCallback) (QfuSaharaDevice *self, |
| gpointer user_data); |
| typedef void (* InitRetryCallback) (QfuSaharaDevice *self, |
| gpointer user_data); |
| |
| static gboolean |
| firehose_operation_run (QfuSaharaDevice *self, |
| PrepareRequestCallback prepare_request, |
| ProcessResponseCallback process_response, |
| CheckCompletionCallback check_completion, |
| InitRetryCallback init_retry, |
| guint max_retries, |
| guint timeout_secs, |
| gpointer user_data, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| GTimer *timer; |
| GError *inner_error = NULL; |
| guint n_retries = 0; |
| |
| g_assert ((max_retries && init_retry) || (!max_retries && !init_retry)); |
| |
| g_debug ("[qfu-sahara-device] running firehose operation..."); |
| |
| timer = g_timer_new (); |
| while (TRUE) { |
| const gchar *req = NULL; |
| gssize rsplen; |
| guint8 *rsp = NULL; |
| gchar **xmldocs; |
| guint i; |
| |
| /* check timeout */ |
| if (g_timer_elapsed (timer, NULL) > timeout_secs) { |
| /* retry? */ |
| if (max_retries && ++n_retries < max_retries) { |
| g_timer_reset (timer); |
| g_clear_error (&inner_error); |
| init_retry (self, user_data); |
| continue; |
| } |
| inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_TIMED_OUT, "operation timed out"); |
| break; |
| } |
| /* check cancellation */ |
| if (g_cancellable_is_cancelled (cancellable)) { |
| inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_CANCELLED, "operation cancelled"); |
| break; |
| } |
| |
| /* user-provided callback to prepare request, may return NULL if there's nothing to send */ |
| if (prepare_request) |
| req = prepare_request (self, user_data); |
| |
| rsplen = send_receive (self, |
| (const guint8 *)req, |
| req ? strlen (req) : 0, |
| 2, |
| &rsp, |
| cancellable, |
| &inner_error); |
| if (rsplen < 0) |
| break; |
| |
| /* timed out without response received */ |
| if (!rsplen) |
| continue; |
| |
| /* we may receive multiple XML documents on a single read() */ |
| xmldocs = split_xml_document ((const gchar *)rsp); |
| for (i = 0; xmldocs && xmldocs[i] && !inner_error; i++) { |
| /* user-provided callback to process response, may return FALSE and |
| * an error if the operation is detected as failed */ |
| process_response (self, xmldocs[i], user_data, &inner_error); |
| } |
| g_strfreev (xmldocs); |
| if (inner_error) { |
| /* retry? */ |
| if (max_retries && ++n_retries < max_retries) { |
| g_timer_reset (timer); |
| g_clear_error (&inner_error); |
| init_retry (self, user_data); |
| continue; |
| } |
| break; |
| } |
| |
| /* keep on operation? */ |
| if (check_completion (self, user_data)) |
| break; |
| } |
| |
| g_timer_destroy (timer); |
| |
| if (inner_error) { |
| g_debug ("[qfu-sahara-device] firehose operation failed: %s", inner_error->message); |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| |
| g_debug ("[qfu-sahara-device] firehose operation finished successfully"); |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| |
| static gboolean |
| validate_ascii_print (const guint8 *rsp, |
| gsize rsplen) |
| { |
| guint i; |
| |
| for (i = 0; i < rsplen; i++) { |
| if (!g_ascii_isprint (rsp[i])) |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| /* Firehose setup download */ |
| |
| #define FIREHOSE_SETUP_DOWNLOAD_TIMEOUT_SECS 10 |
| #define FIREHOSE_SETUP_DOWNLOAD_MAX_RETRIES 3 |
| |
| typedef struct { |
| guint n_partition_sectors; |
| gboolean sent; |
| gboolean acked; |
| } FirehoseSetupDownloadContext; |
| |
| static const gchar * |
| firehose_setup_download_prepare_request (QfuSaharaDevice *self, |
| FirehoseSetupDownloadContext *ctx) |
| { |
| if (!ctx->sent) { |
| ctx->sent = TRUE; |
| g_debug ("[qfu-sahara-device] sending firehose program request..."); |
| qfu_firehose_message_build_program (self->priv->buffer->data, |
| self->priv->buffer->len, |
| self->priv->pages_in_block, |
| self->priv->sector_size_in_bytes, |
| ctx->n_partition_sectors); |
| return (const gchar *)self->priv->buffer->data; |
| } |
| |
| return NULL; |
| } |
| |
| static gboolean |
| firehose_setup_download_process_response (QfuSaharaDevice *self, |
| const gchar *rsp, |
| FirehoseSetupDownloadContext *ctx, |
| GError **error) |
| { |
| GError *inner_error = NULL; |
| |
| if (firehose_common_process_log_message (rsp)) |
| return TRUE; |
| |
| if (firehose_common_process_response_ack_message (rsp, "ACK", "true", &inner_error)) { |
| if (inner_error) { |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| |
| ctx->acked = TRUE; |
| return TRUE; |
| } |
| |
| g_debug ("[qfu-sahara-device] unknown firehose message received"); |
| return TRUE; |
| } |
| |
| static gboolean |
| firehose_setup_download_check_completion (QfuSaharaDevice *self, |
| FirehoseSetupDownloadContext *ctx) |
| { |
| return (ctx->acked); |
| } |
| |
| static void |
| firehose_setup_download_init_retry (QfuSaharaDevice *self, |
| FirehoseSetupDownloadContext *ctx) |
| { |
| /* no need to cleanup n_partition_sectors */ |
| |
| ctx->sent = FALSE; |
| ctx->acked = FALSE; |
| } |
| |
| gboolean |
| qfu_sahara_device_firehose_setup_download (QfuSaharaDevice *self, |
| QfuImage *image, |
| guint *n_blocks, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| FirehoseSetupDownloadContext ctx = { |
| .n_partition_sectors = 0, |
| .sent = FALSE, |
| .acked = FALSE, |
| }; |
| goffset image_size; |
| guint n_transfer_blocks; |
| |
| /* NOTE: the firmware download process in Windows sends an additional |
| * configure message before the program request when the 2nd firmware |
| * image is downloaded... but it really doesn't seem to be required |
| * for anything, so we're explicitly avoiding that. Sending the |
| * program request seems to be enough. */ |
| |
| /* compute how many sectors are required */ |
| image_size = qfu_image_get_size (image); |
| ctx.n_partition_sectors = image_size / self->priv->sector_size_in_bytes; |
| if (image_size % self->priv->sector_size_in_bytes > 0) |
| ctx.n_partition_sectors++; |
| |
| /* compute how many transfer block are required, and set them as output right away */ |
| n_transfer_blocks = image_size / self->priv->transfer_block_size; |
| if (image_size % self->priv->transfer_block_size > 0) |
| n_transfer_blocks++; |
| if (n_blocks) |
| *n_blocks = n_transfer_blocks; |
| |
| g_debug ("Setting up firehose download for %" G_GOFFSET_FORMAT " bytes image...", image_size); |
| g_debug (" pages in block: %u", self->priv->pages_in_block); |
| g_debug (" sector size: %u", self->priv->sector_size_in_bytes); |
| g_debug (" num partition sectors: %u", ctx.n_partition_sectors); |
| g_debug (" transfer block size: %u (%u sectors/transfer)", self->priv->transfer_block_size, self->priv->transfer_block_size / self->priv->sector_size_in_bytes); |
| g_debug (" num transfers: %u", n_transfer_blocks); |
| |
| return firehose_operation_run (self, |
| (PrepareRequestCallback) firehose_setup_download_prepare_request, |
| (ProcessResponseCallback) firehose_setup_download_process_response, |
| (CheckCompletionCallback) firehose_setup_download_check_completion, |
| (InitRetryCallback) firehose_setup_download_init_retry, |
| FIREHOSE_SETUP_DOWNLOAD_MAX_RETRIES, |
| FIREHOSE_SETUP_DOWNLOAD_TIMEOUT_SECS, |
| &ctx, |
| cancellable, |
| error); |
| } |
| |
| /******************************************************************************/ |
| /* Firehose write block in raw mode */ |
| |
| #define END_OF_TRANSFER_BLOCK_SIZE 512 |
| |
| gboolean |
| qfu_sahara_device_firehose_write_block (QfuSaharaDevice *self, |
| QfuImage *image, |
| guint block_i, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| gssize reqlen; |
| goffset offset; |
| gsize size; |
| gboolean send_last = FALSE; |
| |
| g_debug ("[qfu-sahara-device] writing block %u...", block_i); |
| |
| g_assert (self->priv->transfer_block_size < self->priv->buffer->len); |
| memset (self->priv->buffer->data, 0, self->priv->transfer_block_size); |
| |
| offset = block_i * (goffset)self->priv->transfer_block_size; |
| size = qfu_image_get_size (image) - offset; |
| if (size >= self->priv->transfer_block_size) |
| size = self->priv->transfer_block_size; |
| else { |
| gsize last_block_size; |
| |
| /* we need to send an additional packet full of 0s after the last |
| * sector is transferred. */ |
| send_last = TRUE; |
| |
| /* last transfer block adjusted to sector size multiple */ |
| last_block_size = self->priv->sector_size_in_bytes; |
| while (last_block_size < size) |
| last_block_size += self->priv->sector_size_in_bytes; |
| size = last_block_size; |
| g_assert (size <= self->priv->transfer_block_size); |
| } |
| |
| reqlen = qfu_image_read (image, |
| offset, |
| size, |
| self->priv->buffer->data, |
| self->priv->buffer->len, |
| cancellable, |
| error); |
| if (reqlen < 0) { |
| g_prefix_error (error, "couldn't read transfer block %u", block_i); |
| return FALSE; |
| } |
| |
| g_assert ((guint)reqlen <= self->priv->transfer_block_size); |
| if (send_receive (self, |
| self->priv->buffer->data, |
| size, |
| 0, |
| NULL, |
| cancellable, |
| error) < 0) { |
| g_prefix_error (error, "couldn't send transfer block %u", block_i); |
| return FALSE; |
| } |
| |
| if (send_last) { |
| /* We're sending a last block to notify the end of the transmission, |
| * which seems to be a reliable way to tell the modem that it shouldn't |
| * expect more data. |
| * |
| * This block is full of 0s, but the modem seems to end up storing |
| * it and leaving it to be processed once the image has been processed, |
| * which will trigger a warning during the next firehose operation: |
| * ERROR: XML not formed correctly. Expected a < character at loc 0 |
| * And it will also fail the operation with a NAK... |
| * |
| * But, just retrying the operation (the program request for the next |
| * file to download, or the reset if no more files to download) is enough |
| * to make it work. |
| */ |
| memset (self->priv->buffer->data, 0, END_OF_TRANSFER_BLOCK_SIZE); |
| if (send_receive (self, |
| self->priv->buffer->data, |
| END_OF_TRANSFER_BLOCK_SIZE, |
| 0, |
| NULL, |
| cancellable, |
| error) < 0) { |
| g_prefix_error (error, "couldn't send last end-of-transfer block"); |
| return FALSE; |
| } |
| } |
| |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| /* Firehose teardown download */ |
| |
| #define FIREHOSE_TEARDOWN_DOWNLOAD_TIMEOUT_SECS 300 |
| |
| typedef struct { |
| gboolean acked; |
| } FirehoseTeardownDownloadContext; |
| |
| static gboolean |
| firehose_teardown_download_process_response (QfuSaharaDevice *self, |
| const gchar *rsp, |
| FirehoseTeardownDownloadContext *ctx, |
| GError **error) |
| { |
| GError *inner_error = NULL; |
| |
| if (firehose_common_process_log_message (rsp)) |
| return TRUE; |
| |
| if (firehose_common_process_response_ack_message (rsp, "ACK", "false", &inner_error)) { |
| /* We've seen in a EM7511 how the response to the download operation comes |
| * followed *right away* in the same read() with the "XML not formed correctly" |
| * warning plus an additional response with a NAK. In order to avoid failing |
| * the teardown operation with that second response, we'll ignore it completely |
| * if we have already detected a successful response earlier. |
| */ |
| if (ctx->acked) { |
| g_debug ("[qfu-sahara-device] ignoring additional response message detected"); |
| g_clear_error (&inner_error); |
| return TRUE; |
| } |
| if (inner_error) { |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| ctx->acked = TRUE; |
| return TRUE; |
| } |
| |
| g_debug ("[qfu-sahara-device] unknown firehose message received"); |
| return TRUE; |
| } |
| |
| static gboolean |
| firehose_teardown_download_check_completion (QfuSaharaDevice *self, |
| FirehoseTeardownDownloadContext *ctx) |
| { |
| return (ctx->acked); |
| } |
| |
| gboolean |
| qfu_sahara_device_firehose_teardown_download (QfuSaharaDevice *self, |
| QfuImage *image, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| FirehoseTeardownDownloadContext ctx = { |
| .acked = FALSE, |
| }; |
| |
| return firehose_operation_run (self, |
| NULL, /* PrepareRequestCallback */ |
| (ProcessResponseCallback) firehose_teardown_download_process_response, |
| (CheckCompletionCallback) firehose_teardown_download_check_completion, |
| NULL, /* InitRetryCallback */ |
| 0, /* max_retries */ |
| FIREHOSE_TEARDOWN_DOWNLOAD_TIMEOUT_SECS, |
| &ctx, |
| cancellable, |
| error); |
| } |
| |
| /******************************************************************************/ |
| /* Firehose reset */ |
| |
| #define FIREHOSE_RESET_TIMEOUT_SECS 10 |
| #define FIREHOSE_RESET_MAX_RETRIES 10 |
| |
| typedef struct { |
| gboolean sent; |
| gboolean acked; |
| } FirehoseResetContext; |
| |
| static const gchar * |
| firehose_reset_prepare_request (QfuSaharaDevice *self, |
| FirehoseResetContext *ctx) |
| { |
| if (!ctx->sent) { |
| ctx->sent = TRUE; |
| g_debug ("[qfu-sahara-device] sending firehose reset..."); |
| qfu_firehose_message_build_reset (self->priv->buffer->data, self->priv->buffer->len); |
| return (const gchar *)self->priv->buffer->data; |
| } |
| |
| return NULL; |
| } |
| |
| static gboolean |
| firehose_reset_process_response (QfuSaharaDevice *self, |
| const gchar *rsp, |
| FirehoseResetContext *ctx, |
| GError **error) |
| { |
| GError *inner_error = NULL; |
| |
| if (firehose_common_process_log_message (rsp)) |
| return TRUE; |
| |
| if (firehose_common_process_response_ack_message (rsp, "ACK", NULL, &inner_error)) { |
| if (inner_error) { |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| ctx->acked = TRUE; |
| return TRUE; |
| } |
| |
| g_debug ("[qfu-sahara-device] unknown firehose message received"); |
| return TRUE; |
| } |
| |
| static gboolean |
| firehose_reset_check_completion (QfuSaharaDevice *self, |
| FirehoseResetContext *ctx) |
| { |
| return (ctx->acked); |
| } |
| |
| static void |
| firehose_reset_init_retry (QfuSaharaDevice *self, |
| FirehoseResetContext *ctx) |
| { |
| ctx->sent = FALSE; |
| ctx->acked = FALSE; |
| } |
| |
| gboolean |
| qfu_sahara_device_firehose_reset (QfuSaharaDevice *self, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| FirehoseResetContext ctx = { |
| .sent = FALSE, |
| .acked = FALSE, |
| }; |
| |
| return firehose_operation_run (self, |
| (PrepareRequestCallback) firehose_reset_prepare_request, |
| (ProcessResponseCallback) firehose_reset_process_response, |
| (CheckCompletionCallback) firehose_reset_check_completion, |
| (InitRetryCallback) firehose_reset_init_retry, |
| FIREHOSE_RESET_MAX_RETRIES, |
| FIREHOSE_RESET_TIMEOUT_SECS, |
| &ctx, |
| cancellable, |
| error); |
| } |
| |
| /******************************************************************************/ |
| /* Firehose initialization */ |
| |
| #define FIREHOSE_INIT_TIMEOUT_SECS 10 |
| |
| typedef enum { |
| FIREHOSE_INIT_STEP_PING, |
| FIREHOSE_INIT_STEP_WAIT_PING, |
| FIREHOSE_INIT_STEP_CONFIGURE, |
| FIREHOSE_INIT_STEP_WAIT_CONFIGURE, |
| FIREHOSE_INIT_STEP_STORAGE_INFO, |
| FIREHOSE_INIT_STEP_WAIT_STORAGE_INFO, |
| FIREHOSE_INIT_STEP_LAST, |
| } FirehoseInitStep; |
| |
| typedef struct { |
| FirehoseInitStep step; |
| guint max_payload_size_to_target_in_bytes; |
| guint sector_size_in_bytes; |
| guint pages_in_block; |
| } FirehoseInitContext; |
| |
| static gboolean |
| firehose_init_context_process_log_message (const gchar *rsp, |
| FirehoseInitContext *ctx) |
| { |
| gchar *value = NULL; |
| gchar **strv; |
| |
| if (!qfu_firehose_message_parse_log (rsp, &value)) |
| return FALSE; |
| |
| /* The log message may contain specific settings that we want to read */ |
| strv = g_strsplit (value, "=", -1); |
| if (g_strv_length (strv) == 2) { |
| g_strstrip (strv[0]); |
| g_strstrip (strv[1]); |
| if (g_ascii_strcasecmp (strv[0], "sector_size_in_bytes") == 0) |
| ctx->sector_size_in_bytes = atoi (strv[1]); |
| else if (g_ascii_strcasecmp (strv[0], "pages_in_block") == 0) |
| ctx->pages_in_block = atoi (strv[1]); |
| } |
| g_strfreev (strv); |
| |
| g_debug ("[qfu-sahara-device] firehose log: %s", value); |
| g_free (value); |
| return TRUE; |
| } |
| |
| static gboolean |
| firehose_init_context_process_response_configure_message (const gchar *rsp, |
| FirehoseInitContext *ctx, |
| GError **error) |
| { |
| guint32 max_payload_size_to_target_in_bytes = 0; |
| |
| if (!qfu_firehose_message_parse_response_configure (rsp, &max_payload_size_to_target_in_bytes)) |
| return FALSE; |
| |
| if (max_payload_size_to_target_in_bytes > 0) { |
| g_debug ("[qfu-sahara-device] firehose requested max payload size: %u bytes", max_payload_size_to_target_in_bytes); |
| ctx->max_payload_size_to_target_in_bytes = max_payload_size_to_target_in_bytes; |
| } else { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, |
| "unexpected max payload size: %u", max_payload_size_to_target_in_bytes); |
| } |
| |
| return TRUE; |
| } |
| |
| static const gchar * |
| firehose_init_prepare_request (QfuSaharaDevice *self, |
| FirehoseInitContext *ctx) |
| { |
| switch (ctx->step) { |
| case FIREHOSE_INIT_STEP_PING: |
| g_debug ("[qfu-sahara-device] sending firehose ping..."); |
| qfu_firehose_message_build_ping (self->priv->buffer->data, self->priv->buffer->len); |
| ctx->step++; |
| return (const gchar *)self->priv->buffer->data; |
| case FIREHOSE_INIT_STEP_WAIT_PING: |
| /* not sending anything, just processing responses */ |
| return NULL; |
| case FIREHOSE_INIT_STEP_CONFIGURE: |
| g_debug ("[qfu-sahara-device] sending firehose configure..."); |
| qfu_firehose_message_build_configure (self->priv->buffer->data, self->priv->buffer->len, 0); |
| ctx->step++; |
| return (const gchar *)self->priv->buffer->data; |
| case FIREHOSE_INIT_STEP_WAIT_CONFIGURE: |
| /* not sending anything, just processing responses */ |
| return NULL; |
| case FIREHOSE_INIT_STEP_STORAGE_INFO: |
| g_debug ("[qfu-sahara-device] sending firehose storage info request..."); |
| qfu_firehose_message_build_get_storage_info (self->priv->buffer->data, self->priv->buffer->len); |
| ctx->step++; |
| return (const gchar *)self->priv->buffer->data; |
| case FIREHOSE_INIT_STEP_WAIT_STORAGE_INFO: |
| /* not sending anything, just processing responses */ |
| return NULL; |
| case FIREHOSE_INIT_STEP_LAST: |
| default: |
| g_assert_not_reached (); |
| return NULL; |
| } |
| } |
| |
| static gboolean |
| firehose_init_process_response (QfuSaharaDevice *self, |
| const gchar *rsp, |
| FirehoseInitContext *ctx, |
| GError **error) |
| { |
| GError *inner_error = NULL; |
| |
| if (firehose_init_context_process_log_message (rsp, ctx)) |
| return TRUE; |
| |
| if (firehose_common_process_response_ack_message (rsp, "ACK", NULL, &inner_error)) { |
| if (inner_error) { |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| /* if we were expecting a response, go on to next step */ |
| if (ctx->step == FIREHOSE_INIT_STEP_WAIT_PING || ctx->step == FIREHOSE_INIT_STEP_WAIT_STORAGE_INFO) |
| ctx->step++; |
| return TRUE; |
| } |
| |
| if (firehose_init_context_process_response_configure_message (rsp, ctx, &inner_error)) { |
| if (inner_error) { |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| /* if we were expecting a response, go on to next step */ |
| if (ctx->step == FIREHOSE_INIT_STEP_WAIT_CONFIGURE) |
| ctx->step++; |
| return TRUE; |
| } |
| |
| g_debug ("[qfu-sahara-device] unknown firehose message received"); |
| return TRUE; |
| } |
| |
| static gboolean |
| firehose_init_check_completion (QfuSaharaDevice *self, |
| FirehoseInitContext *ctx) |
| { |
| return (ctx->step == FIREHOSE_INIT_STEP_LAST); |
| } |
| |
| static gboolean |
| sahara_device_firehose_init (QfuSaharaDevice *self, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| FirehoseInitContext ctx = { |
| .step = FIREHOSE_INIT_STEP_PING, |
| .max_payload_size_to_target_in_bytes = 0, |
| .sector_size_in_bytes = 0, |
| .pages_in_block = 0, |
| }; |
| |
| if (!firehose_operation_run (self, |
| (PrepareRequestCallback) firehose_init_prepare_request, |
| (ProcessResponseCallback) firehose_init_process_response, |
| (CheckCompletionCallback) firehose_init_check_completion, |
| NULL, /* InitRetryCallback */ |
| 0, /* max_retries */ |
| FIREHOSE_INIT_TIMEOUT_SECS, |
| &ctx, |
| cancellable, |
| error)) |
| return FALSE; |
| |
| if (!ctx.sector_size_in_bytes) { |
| g_debug ("[qfu-sahara-device] using default sector size (%u bytes)", DEFAULT_SECTOR_SIZE_IN_BYTES); |
| self->priv->sector_size_in_bytes = DEFAULT_SECTOR_SIZE_IN_BYTES; |
| } else |
| self->priv->sector_size_in_bytes = ctx.sector_size_in_bytes; |
| |
| if (!ctx.pages_in_block) { |
| g_debug ("[qfu-sahara-device] using default pages in block (%u)", DEFAULT_PAGES_IN_BLOCK); |
| self->priv->pages_in_block = DEFAULT_PAGES_IN_BLOCK; |
| } else |
| self->priv->pages_in_block = ctx.pages_in_block; |
| |
| if (!ctx.max_payload_size_to_target_in_bytes) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unknown max payload size"); |
| return FALSE; |
| } |
| self->priv->max_payload_size_to_target_in_bytes = ctx.max_payload_size_to_target_in_bytes; |
| |
| /* compute transfer block size, which will be equal to the max payload size |
| * to target if it's multiple of sector size */ |
| self->priv->transfer_block_size = (self->priv->max_payload_size_to_target_in_bytes / self->priv->sector_size_in_bytes) * self->priv->sector_size_in_bytes; |
| g_assert (self->priv->transfer_block_size <= self->priv->max_payload_size_to_target_in_bytes); |
| g_assert (self->priv->transfer_block_size > 0); |
| |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| /* Sahara initialization */ |
| |
| #define SAHARA_MAX_PROTOCOL_STEP_ATTEMPTS 5 |
| |
| typedef enum { |
| SAHARA_PROTOCOL_STEP_UNKNOWN, |
| SAHARA_PROTOCOL_STEP_INIT, |
| SAHARA_PROTOCOL_STEP_HELLO, |
| SAHARA_PROTOCOL_STEP_SWITCH, |
| SAHARA_PROTOCOL_STEP_DATA, |
| SAHARA_PROTOCOL_STEP_LAST, |
| } SaharaProtocolStep; |
| |
| static gboolean |
| validate_firehose_switch_confirmation (const guint8 *rsp, |
| gsize rsplen, |
| GError **error) |
| { |
| if (!rsplen) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "no confirmation received"); |
| return FALSE; |
| } |
| |
| if (!validate_ascii_print (rsp, rsplen)) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "invalid confirmation data"); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| static SaharaProtocolStep |
| sahara_device_run_protocol_step (QfuSaharaDevice *self, |
| SaharaProtocolStep step, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| SaharaProtocolStep next_step = SAHARA_PROTOCOL_STEP_UNKNOWN; |
| gsize reqlen = 0; |
| gssize rsplen; |
| guint8 *rsp = NULL; |
| |
| memset (self->priv->buffer->data, 0, self->priv->buffer->len); |
| |
| switch (step) { |
| case SAHARA_PROTOCOL_STEP_INIT: |
| /* |
| * Just after opening the port we must NOT SEND anything to the device. |
| * If we do that, we would get the sahara hello back, but the initialization |
| * process would fail later on with a command-end-image-transfer message |
| * reporting that the 0x0000ff00 command to switch to firehose protocol is |
| * unsupported. |
| */ |
| break; |
| case SAHARA_PROTOCOL_STEP_HELLO: |
| reqlen = qfu_sahara_response_hello_build (self->priv->buffer->data, self->priv->buffer->len); |
| break; |
| case SAHARA_PROTOCOL_STEP_SWITCH: |
| reqlen = qfu_sahara_request_switch_build (self->priv->buffer->data, self->priv->buffer->len); |
| break; |
| case SAHARA_PROTOCOL_STEP_DATA: |
| reqlen = qfu_sahara_request_switch_data_build (self->priv->buffer->data, self->priv->buffer->len); |
| break; |
| case SAHARA_PROTOCOL_STEP_UNKNOWN: |
| case SAHARA_PROTOCOL_STEP_LAST: |
| default: |
| g_assert_not_reached (); |
| break; |
| } |
| |
| rsplen = send_receive (self, |
| reqlen ? self->priv->buffer->data : NULL, |
| reqlen, |
| 3, |
| &rsp, |
| cancellable, |
| error); |
| if (rsplen < 0) |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| |
| if (rsplen == 0) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "no sahara response received"); |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| } |
| |
| /* The sahara initialization finishes once the switch to firehose is confirmed. |
| * The EM7565 replies "confirmed" explicitly, but we'll just accept any printable |
| * ASCII string. */ |
| if (step == SAHARA_PROTOCOL_STEP_DATA) { |
| if (!validate_firehose_switch_confirmation (rsp, rsplen, error)) |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| |
| /* initialization finished */ |
| g_debug ("[qfu-sahara-device] sahara initialization finished: %.*s", (gint)rsplen, rsp); |
| return SAHARA_PROTOCOL_STEP_LAST; |
| } |
| |
| /* in case several messages are received together, parse and process them one by one */ |
| while (rsplen > 0) { |
| gsize msglen; |
| QfuSaharaHeader *msghdr; |
| |
| if ((gsize)rsplen < QFU_SAHARA_MESSAGE_MAX_HEADER_SIZE) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "sahara header not fully received: %" G_GSSIZE_FORMAT " < %" G_GSIZE_FORMAT, |
| rsplen, QFU_SAHARA_MESSAGE_MAX_HEADER_SIZE); |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| } |
| |
| msghdr = (QfuSaharaHeader *)rsp; |
| msglen = GUINT32_FROM_LE (msghdr->size); |
| if ((gsize)rsplen < msglen) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "sahara message not fully received: %" G_GSSIZE_FORMAT " < %" G_GSIZE_FORMAT, |
| rsplen, msglen); |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| } |
| |
| switch (GUINT32_FROM_LE (msghdr->cmd)) { |
| case QFU_SAHARA_CMD_HELLO_REQ: |
| if (!qfu_sahara_request_hello_parse (rsp, rsplen, error)) |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| g_debug ("[qfu-sahara-device] sahara hello request received"); |
| next_step = SAHARA_PROTOCOL_STEP_HELLO; |
| break; |
| case QFU_SAHARA_CMD_COMMAND_READY: |
| g_debug ("[qfu-sahara-device] module is ready for commands"); |
| next_step = SAHARA_PROTOCOL_STEP_SWITCH; |
| break; |
| case QFU_SAHARA_CMD_COMMAND_EXECUTE_RSP: |
| g_debug ("[qfu-sahara-device] request to switch to firehose accepted"); |
| if (!qfu_sahara_response_switch_parse (rsp, rsplen, error)) |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| next_step = SAHARA_PROTOCOL_STEP_DATA; |
| break; |
| case QFU_SAHARA_CMD_COMMAND_END_IMAGE_TRANSFER: |
| if (!qfu_sahara_response_end_image_transfer_parse (rsp, rsplen, error)) |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unexpected sahara message"); |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| default: |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "unsupported sahara message: '0x%08x'", GUINT32_FROM_LE (msghdr->cmd)); |
| return SAHARA_PROTOCOL_STEP_UNKNOWN; |
| } |
| |
| rsp += msglen; |
| rsplen -= msglen; |
| } |
| |
| g_assert (next_step != SAHARA_PROTOCOL_STEP_UNKNOWN); |
| return next_step; |
| } |
| |
| static gboolean |
| sahara_device_initialize (QfuSaharaDevice *self, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| SaharaProtocolStep step = SAHARA_PROTOCOL_STEP_INIT; |
| guint n_attempts = 0; |
| |
| while (step != SAHARA_PROTOCOL_STEP_LAST) { |
| SaharaProtocolStep next_step; |
| |
| /* check cancellation */ |
| if (g_cancellable_is_cancelled (cancellable)) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_CANCELLED, "operation cancelled"); |
| return FALSE; |
| } |
| |
| /* check step error */ |
| next_step = sahara_device_run_protocol_step (self, step, cancellable, error); |
| if (next_step == SAHARA_PROTOCOL_STEP_UNKNOWN) |
| return FALSE; |
| |
| /* retrying with same step? */ |
| if (next_step == step) { |
| if (++n_attempts == SAHARA_MAX_PROTOCOL_STEP_ATTEMPTS) { |
| g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "too many attempts"); |
| return FALSE; |
| } |
| } else |
| n_attempts = 0; |
| |
| /* continue to next step */ |
| step = next_step; |
| } |
| |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| |
| static gboolean |
| initable_init (GInitable *initable, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| QfuSaharaDevice *self; |
| struct termios terminal_data; |
| gchar *path = NULL; |
| GError *inner_error = NULL; |
| |
| self = QFU_SAHARA_DEVICE (initable); |
| |
| if (g_cancellable_set_error_if_cancelled (cancellable, &inner_error)) |
| goto out; |
| |
| path = g_file_get_path (self->priv->file); |
| g_debug ("[qfu-sahara-device] opening TTY: %s", path); |
| |
| self->priv->fd = open (path, O_RDWR | O_NOCTTY); |
| if (self->priv->fd < 0) { |
| inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error opening serial device: %s", |
| g_strerror (errno)); |
| goto out; |
| } |
| |
| g_debug ("[qfu-sahara-device] setting terminal in raw mode..."); |
| if (tcgetattr (self->priv->fd, &terminal_data) < 0) { |
| inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error getting serial port attributes: %s", |
| g_strerror (errno)); |
| goto out; |
| } |
| cfmakeraw (&terminal_data); |
| if (tcsetattr (self->priv->fd, TCSANOW, &terminal_data) < 0) { |
| inner_error = g_error_new (G_IO_ERROR, G_IO_ERROR_FAILED, |
| "error setting serial port attributes: %s", |
| g_strerror (errno)); |
| goto out; |
| } |
| |
| /* We need to give some time to the device before trying to initialize |
| * the sahara protocol, because otherwise the sequence won't work. If |
| * this wait time is not given, the initialization sequence will fail with |
| * a command-end-image-transfer message reporting that the 0x0000ff00 |
| * command to switch to firehose protocol is unsupported. |
| * |
| * 2 full seconds selected a bit arbitrarily, didn't get any failure when |
| * using this amount of time. */ |
| g_debug ("[qfu-sahara-device] waiting time for device to boot properly..."); |
| g_usleep (2 * G_USEC_PER_SEC); |
| |
| g_debug ("[qfu-sahara-device] initializing sahara protocol..."); |
| if (!sahara_device_initialize (self, cancellable, &inner_error)) |
| goto out; |
| |
| g_debug ("[qfu-sahara-device] initializing firehose protocol..."); |
| if (!sahara_device_firehose_init (self, cancellable, &inner_error)) |
| goto out; |
| |
| out: |
| g_free (path); |
| |
| if (inner_error) { |
| if (!(self->priv->fd < 0)) { |
| close (self->priv->fd); |
| self->priv->fd = -1; |
| } |
| g_propagate_error (error, inner_error); |
| return FALSE; |
| } |
| |
| return TRUE; |
| } |
| |
| /******************************************************************************/ |
| |
| QfuSaharaDevice * |
| qfu_sahara_device_new (GFile *file, |
| GCancellable *cancellable, |
| GError **error) |
| { |
| g_return_val_if_fail (G_IS_FILE (file), NULL); |
| |
| return QFU_SAHARA_DEVICE (g_initable_new (QFU_TYPE_SAHARA_DEVICE, |
| cancellable, |
| error, |
| "file", file, |
| NULL)); |
| } |
| |
| |
| static void |
| qfu_sahara_device_init (QfuSaharaDevice *self) |
| { |
| self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, QFU_TYPE_SAHARA_DEVICE, QfuSaharaDevicePrivate); |
| self->priv->fd = -1; |
| /* Long buffer for I/O, way more than ever needed when using sahara/firehose */ |
| self->priv->buffer = g_byte_array_new (); |
| g_byte_array_set_size (self->priv->buffer, QFU_IMAGE_CHUNK_SIZE); |
| } |
| |
| static void |
| set_property (GObject *object, |
| guint prop_id, |
| const GValue *value, |
| GParamSpec *pspec) |
| { |
| QfuSaharaDevice *self = QFU_SAHARA_DEVICE (object); |
| |
| switch (prop_id) { |
| case PROP_FILE: |
| self->priv->file = g_value_dup_object (value); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| get_property (GObject *object, |
| guint prop_id, |
| GValue *value, |
| GParamSpec *pspec) |
| { |
| QfuSaharaDevice *self = QFU_SAHARA_DEVICE (object); |
| |
| switch (prop_id) { |
| case PROP_FILE: |
| g_value_set_object (value, self->priv->file); |
| break; |
| default: |
| G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
| break; |
| } |
| } |
| |
| static void |
| dispose (GObject *object) |
| { |
| QfuSaharaDevice *self = QFU_SAHARA_DEVICE (object); |
| |
| if (!(self->priv->fd < 0)) { |
| close (self->priv->fd); |
| self->priv->fd = -1; |
| } |
| |
| G_OBJECT_CLASS (qfu_sahara_device_parent_class)->dispose (object); |
| } |
| |
| static void |
| initable_iface_init (GInitableIface *iface) |
| { |
| iface->init = initable_init; |
| } |
| |
| static void |
| qfu_sahara_device_class_init (QfuSaharaDeviceClass *klass) |
| { |
| GObjectClass *object_class = G_OBJECT_CLASS (klass); |
| |
| g_type_class_add_private (object_class, sizeof (QfuSaharaDevicePrivate)); |
| |
| object_class->dispose = dispose; |
| object_class->get_property = get_property; |
| object_class->set_property = set_property; |
| |
| properties[PROP_FILE] = |
| g_param_spec_object ("file", |
| "File", |
| "File object", |
| G_TYPE_FILE, |
| G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); |
| g_object_class_install_property (object_class, PROP_FILE, properties[PROP_FILE]); |
| } |