mirror of
				https://git.proxmox.com/git/fwupd
				synced 2025-11-04 04:54:16 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			795 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			795 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
/*
 | 
						|
 * Copyright (C) 2019 Aleksander Morgado <aleksander@aleksander.es>
 | 
						|
 *
 | 
						|
 * SPDX-License-Identifier: LGPL-2.1+
 | 
						|
 */
 | 
						|
 | 
						|
#include "config.h"
 | 
						|
 | 
						|
#include <fwupdplugin.h>
 | 
						|
 | 
						|
#include <string.h>
 | 
						|
 | 
						|
#include "fu-qmi-pdc-updater.h"
 | 
						|
 | 
						|
#define FU_QMI_PDC_MAX_OPEN_ATTEMPTS 8
 | 
						|
 | 
						|
struct _FuQmiPdcUpdater {
 | 
						|
	GObject parent_instance;
 | 
						|
	gchar *qmi_port;
 | 
						|
	QmiDevice *qmi_device;
 | 
						|
	QmiClientPdc *qmi_client;
 | 
						|
};
 | 
						|
 | 
						|
G_DEFINE_TYPE(FuQmiPdcUpdater, fu_qmi_pdc_updater, G_TYPE_OBJECT)
 | 
						|
 | 
						|
typedef struct {
 | 
						|
	GMainLoop *mainloop;
 | 
						|
	QmiDevice *qmi_device;
 | 
						|
	QmiClientPdc *qmi_client;
 | 
						|
	GError *error;
 | 
						|
	guint open_attempts;
 | 
						|
} OpenContext;
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_open_attempt(OpenContext *ctx);
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_open_abort_ready(GObject *qmi_device,
 | 
						|
					       GAsyncResult *res,
 | 
						|
					       gpointer user_data)
 | 
						|
{
 | 
						|
	OpenContext *ctx = (OpenContext *)user_data;
 | 
						|
 | 
						|
	g_warn_if_fail(ctx->error != NULL);
 | 
						|
 | 
						|
	/* ignore errors when aborting open */
 | 
						|
	qmi_device_close_finish(QMI_DEVICE(qmi_device), res, NULL);
 | 
						|
 | 
						|
	ctx->open_attempts--;
 | 
						|
	if (ctx->open_attempts == 0) {
 | 
						|
		g_clear_object(&ctx->qmi_client);
 | 
						|
		g_clear_object(&ctx->qmi_device);
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	/* retry */
 | 
						|
	g_clear_error(&ctx->error);
 | 
						|
	fu_qmi_pdc_updater_qmi_device_open_attempt(ctx);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_open_abort(OpenContext *ctx)
 | 
						|
{
 | 
						|
	qmi_device_close_async(ctx->qmi_device,
 | 
						|
			       15,
 | 
						|
			       NULL,
 | 
						|
			       fu_qmi_pdc_updater_qmi_device_open_abort_ready,
 | 
						|
			       ctx);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_allocate_client_ready(GObject *qmi_device,
 | 
						|
						    GAsyncResult *res,
 | 
						|
						    gpointer user_data)
 | 
						|
{
 | 
						|
	OpenContext *ctx = (OpenContext *)user_data;
 | 
						|
 | 
						|
	ctx->qmi_client = QMI_CLIENT_PDC(
 | 
						|
	    qmi_device_allocate_client_finish(QMI_DEVICE(qmi_device), res, &ctx->error));
 | 
						|
	if (ctx->qmi_client == NULL) {
 | 
						|
		fu_qmi_pdc_updater_open_abort(ctx);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	g_main_loop_quit(ctx->mainloop);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_open_ready(GObject *qmi_device, GAsyncResult *res, gpointer user_data)
 | 
						|
{
 | 
						|
	OpenContext *ctx = (OpenContext *)user_data;
 | 
						|
 | 
						|
	if (!qmi_device_open_finish(QMI_DEVICE(qmi_device), res, &ctx->error)) {
 | 
						|
		fu_qmi_pdc_updater_open_abort(ctx);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	qmi_device_allocate_client(ctx->qmi_device,
 | 
						|
				   QMI_SERVICE_PDC,
 | 
						|
				   QMI_CID_NONE,
 | 
						|
				   5,
 | 
						|
				   NULL,
 | 
						|
				   fu_qmi_pdc_updater_qmi_device_allocate_client_ready,
 | 
						|
				   ctx);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_open_attempt(OpenContext *ctx)
 | 
						|
{
 | 
						|
	QmiDeviceOpenFlags open_flags = QMI_DEVICE_OPEN_FLAGS_NONE;
 | 
						|
 | 
						|
	/* automatically detect QMI and MBIM ports */
 | 
						|
	open_flags |= QMI_DEVICE_OPEN_FLAGS_AUTO;
 | 
						|
	/* qmi pdc requires indications, so enable them by default */
 | 
						|
	open_flags |= QMI_DEVICE_OPEN_FLAGS_EXPECT_INDICATIONS;
 | 
						|
	/* all communication through the proxy */
 | 
						|
	open_flags |= QMI_DEVICE_OPEN_FLAGS_PROXY;
 | 
						|
 | 
						|
	g_debug("trying to open QMI device...");
 | 
						|
	qmi_device_open(ctx->qmi_device,
 | 
						|
			open_flags,
 | 
						|
			5,
 | 
						|
			NULL,
 | 
						|
			fu_qmi_pdc_updater_qmi_device_open_ready,
 | 
						|
			ctx);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_new_ready(GObject *source, GAsyncResult *res, gpointer user_data)
 | 
						|
{
 | 
						|
	OpenContext *ctx = (OpenContext *)user_data;
 | 
						|
 | 
						|
	ctx->qmi_device = qmi_device_new_finish(res, &ctx->error);
 | 
						|
	if (ctx->qmi_device == NULL) {
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	fu_qmi_pdc_updater_qmi_device_open_attempt(ctx);
 | 
						|
}
 | 
						|
 | 
						|
gboolean
 | 
						|
fu_qmi_pdc_updater_open(FuQmiPdcUpdater *self, GError **error)
 | 
						|
{
 | 
						|
	g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE);
 | 
						|
	g_autoptr(GFile) qmi_device_file = g_file_new_for_path(self->qmi_port);
 | 
						|
	OpenContext ctx = {
 | 
						|
	    .mainloop = mainloop,
 | 
						|
	    .qmi_device = NULL,
 | 
						|
	    .qmi_client = NULL,
 | 
						|
	    .error = NULL,
 | 
						|
	    .open_attempts = FU_QMI_PDC_MAX_OPEN_ATTEMPTS,
 | 
						|
	};
 | 
						|
 | 
						|
	qmi_device_new(qmi_device_file, NULL, fu_qmi_pdc_updater_qmi_device_new_ready, &ctx);
 | 
						|
	g_main_loop_run(mainloop);
 | 
						|
 | 
						|
	/* either we have all device, client and config list  set, or otherwise error is set */
 | 
						|
 | 
						|
	if ((ctx.qmi_device != NULL) && (ctx.qmi_client != NULL)) {
 | 
						|
		g_warn_if_fail(!ctx.error);
 | 
						|
		self->qmi_device = ctx.qmi_device;
 | 
						|
		self->qmi_client = ctx.qmi_client;
 | 
						|
		/* success */
 | 
						|
		return TRUE;
 | 
						|
	}
 | 
						|
 | 
						|
	g_warn_if_fail(ctx.error != NULL);
 | 
						|
	g_warn_if_fail(ctx.qmi_device == NULL);
 | 
						|
	g_warn_if_fail(ctx.qmi_client == NULL);
 | 
						|
	g_propagate_error(error, ctx.error);
 | 
						|
	return FALSE;
 | 
						|
}
 | 
						|
 | 
						|
typedef struct {
 | 
						|
	GMainLoop *mainloop;
 | 
						|
	QmiDevice *qmi_device;
 | 
						|
	QmiClientPdc *qmi_client;
 | 
						|
	GError *error;
 | 
						|
} CloseContext;
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_close_ready(GObject *qmi_device,
 | 
						|
					  GAsyncResult *res,
 | 
						|
					  gpointer user_data)
 | 
						|
{
 | 
						|
	CloseContext *ctx = (CloseContext *)user_data;
 | 
						|
 | 
						|
	/* ignore errors when closing if we had one already set when releasing client */
 | 
						|
	qmi_device_close_finish(QMI_DEVICE(qmi_device),
 | 
						|
				res,
 | 
						|
				(ctx->error == NULL) ? &ctx->error : NULL);
 | 
						|
	g_clear_object(&ctx->qmi_device);
 | 
						|
	g_main_loop_quit(ctx->mainloop);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_qmi_device_release_client_ready(GObject *qmi_device,
 | 
						|
						   GAsyncResult *res,
 | 
						|
						   gpointer user_data)
 | 
						|
{
 | 
						|
	CloseContext *ctx = (CloseContext *)user_data;
 | 
						|
 | 
						|
	qmi_device_release_client_finish(QMI_DEVICE(qmi_device), res, &ctx->error);
 | 
						|
	g_clear_object(&ctx->qmi_client);
 | 
						|
 | 
						|
	qmi_device_close_async(ctx->qmi_device,
 | 
						|
			       15,
 | 
						|
			       NULL,
 | 
						|
			       fu_qmi_pdc_updater_qmi_device_close_ready,
 | 
						|
			       ctx);
 | 
						|
}
 | 
						|
 | 
						|
gboolean
 | 
						|
fu_qmi_pdc_updater_close(FuQmiPdcUpdater *self, GError **error)
 | 
						|
{
 | 
						|
	g_autoptr(GMainLoop) mainloop = g_main_loop_new(NULL, FALSE);
 | 
						|
	CloseContext ctx = {
 | 
						|
	    .mainloop = mainloop,
 | 
						|
	    .qmi_device = g_steal_pointer(&self->qmi_device),
 | 
						|
	    .qmi_client = g_steal_pointer(&self->qmi_client),
 | 
						|
	};
 | 
						|
 | 
						|
	qmi_device_release_client(ctx.qmi_device,
 | 
						|
				  QMI_CLIENT(ctx.qmi_client),
 | 
						|
				  QMI_DEVICE_RELEASE_CLIENT_FLAGS_RELEASE_CID,
 | 
						|
				  5,
 | 
						|
				  NULL,
 | 
						|
				  fu_qmi_pdc_updater_qmi_device_release_client_ready,
 | 
						|
				  &ctx);
 | 
						|
	g_main_loop_run(mainloop);
 | 
						|
 | 
						|
	/* we should always have both device and client cleared, and optionally error set */
 | 
						|
 | 
						|
	g_warn_if_fail(ctx.qmi_device == NULL);
 | 
						|
	g_warn_if_fail(ctx.qmi_client == NULL);
 | 
						|
 | 
						|
	if (ctx.error != NULL) {
 | 
						|
		g_propagate_error(error, ctx.error);
 | 
						|
		return FALSE;
 | 
						|
	}
 | 
						|
 | 
						|
	return TRUE;
 | 
						|
}
 | 
						|
 | 
						|
#define QMI_LOAD_CHUNK_SIZE 0x400
 | 
						|
 | 
						|
typedef struct {
 | 
						|
	GMainLoop *mainloop;
 | 
						|
	QmiClientPdc *qmi_client;
 | 
						|
	GError *error;
 | 
						|
	gulong indication_id;
 | 
						|
	guint timeout_id;
 | 
						|
	GBytes *blob;
 | 
						|
	GArray *digest;
 | 
						|
	gsize offset;
 | 
						|
	guint token;
 | 
						|
} WriteContext;
 | 
						|
 | 
						|
#if !QMI_CHECK_VERSION(1, 26, 0)
 | 
						|
#pragma clang diagnostic push
 | 
						|
#pragma clang diagnostic ignored "-Wunused-function"
 | 
						|
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QmiMessagePdcLoadConfigInput, qmi_message_pdc_load_config_input_unref)
 | 
						|
G_DEFINE_AUTOPTR_CLEANUP_FUNC(QmiMessagePdcLoadConfigOutput,
 | 
						|
			      qmi_message_pdc_load_config_output_unref)
 | 
						|
#pragma clang diagnostic pop
 | 
						|
#endif
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_load_config(WriteContext *ctx);
 | 
						|
 | 
						|
static gboolean
 | 
						|
fu_qmi_pdc_updater_load_config_timeout(gpointer user_data)
 | 
						|
{
 | 
						|
	WriteContext *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 load mcfg: timed out");
 | 
						|
	g_main_loop_quit(ctx->mainloop);
 | 
						|
 | 
						|
	return G_SOURCE_REMOVE;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_load_config_indication(QmiClientPdc *client,
 | 
						|
					  QmiIndicationPdcLoadConfigOutput *output,
 | 
						|
					  WriteContext *ctx)
 | 
						|
{
 | 
						|
	gboolean frame_reset;
 | 
						|
	guint32 remaining_size;
 | 
						|
	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_load_config_output_get_indication_result(output,
 | 
						|
									 &error_code,
 | 
						|
									 &ctx->error)) {
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (error_code != 0) {
 | 
						|
		/* when a given mcfg file already exists in the device, an "invalid id" error is
 | 
						|
		 * returned; the error naming here is a bit off, as the same protocol error number
 | 
						|
		 * is used both for 'invalid id' and 'invalid qos id'
 | 
						|
		 */
 | 
						|
		if (error_code == QMI_PROTOCOL_ERROR_INVALID_QOS_ID) {
 | 
						|
			g_debug("file already available in device");
 | 
						|
			g_main_loop_quit(ctx->mainloop);
 | 
						|
			return;
 | 
						|
		}
 | 
						|
 | 
						|
		g_set_error(&ctx->error,
 | 
						|
			    G_IO_ERROR,
 | 
						|
			    G_IO_ERROR_FAILED,
 | 
						|
			    "couldn't load mcfg: %s",
 | 
						|
			    qmi_protocol_error_get_string((QmiProtocolError)error_code));
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (qmi_indication_pdc_load_config_output_get_frame_reset(output, &frame_reset, NULL) &&
 | 
						|
	    frame_reset) {
 | 
						|
		g_set_error(&ctx->error,
 | 
						|
			    G_IO_ERROR,
 | 
						|
			    G_IO_ERROR_FAILED,
 | 
						|
			    "couldn't load mcfg: sent data discarded");
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (!qmi_indication_pdc_load_config_output_get_remaining_size(output,
 | 
						|
								      &remaining_size,
 | 
						|
								      &ctx->error)) {
 | 
						|
		g_prefix_error(&ctx->error, "couldn't load remaining size: ");
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (remaining_size == 0) {
 | 
						|
		g_debug("finished loading mcfg");
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	g_debug("loading next chunk (%u bytes remaining)", remaining_size);
 | 
						|
	fu_qmi_pdc_updater_load_config(ctx);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_load_config_ready(GObject *qmi_client, GAsyncResult *res, gpointer user_data)
 | 
						|
{
 | 
						|
	WriteContext *ctx = (WriteContext *)user_data;
 | 
						|
	g_autoptr(QmiMessagePdcLoadConfigOutput) output = NULL;
 | 
						|
 | 
						|
	output = qmi_client_pdc_load_config_finish(QMI_CLIENT_PDC(qmi_client), res, &ctx->error);
 | 
						|
	if (output == NULL) {
 | 
						|
		g_main_loop_quit(ctx->mainloop);
 | 
						|
		return;
 | 
						|
	}
 | 
						|
 | 
						|
	if (!qmi_message_pdc_load_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,
 | 
						|
					      "load-config",
 | 
						|
					      G_CALLBACK(fu_qmi_pdc_updater_load_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_load_config_timeout, ctx);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_load_config(WriteContext *ctx)
 | 
						|
{
 | 
						|
	g_autoptr(QmiMessagePdcLoadConfigInput) input = NULL;
 | 
						|
	g_autoptr(GArray) chunk = NULL;
 | 
						|
	gsize full_size;
 | 
						|
	gsize chunk_size;
 | 
						|
	g_autoptr(GError) error = NULL;
 | 
						|
 | 
						|
	input = qmi_message_pdc_load_config_input_new();
 | 
						|
	qmi_message_pdc_load_config_input_set_token(input, ctx->token++, NULL);
 | 
						|
 | 
						|
	full_size = g_bytes_get_size(ctx->blob);
 | 
						|
	if ((ctx->offset + QMI_LOAD_CHUNK_SIZE) > full_size)
 | 
						|
		chunk_size = full_size - ctx->offset;
 | 
						|
	else
 | 
						|
		chunk_size = QMI_LOAD_CHUNK_SIZE;
 | 
						|
 | 
						|
	chunk = g_array_sized_new(FALSE, FALSE, sizeof(guint8), chunk_size);
 | 
						|
	g_array_set_size(chunk, chunk_size);
 | 
						|
	if (!fu_memcpy_safe((guint8 *)chunk->data,
 | 
						|
			    chunk_size,
 | 
						|
			    0x0,					       /* dst */
 | 
						|
			    (const guint8 *)g_bytes_get_data(ctx->blob, NULL), /* src */
 | 
						|
			    g_bytes_get_size(ctx->blob),
 | 
						|
			    ctx->offset,
 | 
						|
			    chunk_size,
 | 
						|
			    &error)) {
 | 
						|
		g_critical("failed to copy chunk: %s", error->message);
 | 
						|
	}
 | 
						|
 | 
						|
	qmi_message_pdc_load_config_input_set_config_chunk(input,
 | 
						|
							   QMI_PDC_CONFIGURATION_TYPE_SOFTWARE,
 | 
						|
							   ctx->digest,
 | 
						|
							   full_size,
 | 
						|
							   chunk,
 | 
						|
							   NULL);
 | 
						|
	ctx->offset += chunk_size;
 | 
						|
 | 
						|
	qmi_client_pdc_load_config(ctx->qmi_client,
 | 
						|
				   input,
 | 
						|
				   10,
 | 
						|
				   NULL,
 | 
						|
				   fu_qmi_pdc_updater_load_config_ready,
 | 
						|
				   ctx);
 | 
						|
}
 | 
						|
 | 
						|
static GArray *
 | 
						|
fu_qmi_pdc_updater_get_checksum(GBytes *blob)
 | 
						|
{
 | 
						|
	gsize file_size;
 | 
						|
	gsize hash_size;
 | 
						|
	GArray *digest;
 | 
						|
	g_autoptr(GChecksum) checksum = NULL;
 | 
						|
 | 
						|
	/* get checksum, to be used as unique id */
 | 
						|
	file_size = g_bytes_get_size(blob);
 | 
						|
	hash_size = g_checksum_type_get_length(G_CHECKSUM_SHA1);
 | 
						|
	checksum = g_checksum_new(G_CHECKSUM_SHA1);
 | 
						|
	g_checksum_update(checksum, g_bytes_get_data(blob, NULL), file_size);
 | 
						|
	/* libqmi expects a GArray of bytes, not a GByteArray */
 | 
						|
	digest = g_array_sized_new(FALSE, FALSE, sizeof(guint8), hash_size);
 | 
						|
	g_array_set_size(digest, hash_size);
 | 
						|
	g_checksum_get_digest(checksum, (guint8 *)digest->data, &hash_size);
 | 
						|
 | 
						|
	return digest;
 | 
						|
}
 | 
						|
 | 
						|
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,
 | 
						|
	    .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;
 | 
						|
 | 
						|
#if !QMI_CHECK_VERSION(1, 26, 0)
 | 
						|
#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
 | 
						|
#endif
 | 
						|
 | 
						|
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
 | 
						|
		 * reset 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);
 | 
						|
}
 | 
						|
 | 
						|
#if !QMI_CHECK_VERSION(1, 26, 0)
 | 
						|
#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
 | 
						|
#endif
 | 
						|
 | 
						|
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;
 | 
						|
	}
 | 
						|
 | 
						|
	return TRUE;
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_init(FuQmiPdcUpdater *self)
 | 
						|
{
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_finalize(GObject *object)
 | 
						|
{
 | 
						|
	FuQmiPdcUpdater *self = FU_QMI_PDC_UPDATER(object);
 | 
						|
	g_warn_if_fail(self->qmi_client == NULL);
 | 
						|
	g_warn_if_fail(self->qmi_device == NULL);
 | 
						|
	g_free(self->qmi_port);
 | 
						|
	G_OBJECT_CLASS(fu_qmi_pdc_updater_parent_class)->finalize(object);
 | 
						|
}
 | 
						|
 | 
						|
static void
 | 
						|
fu_qmi_pdc_updater_class_init(FuQmiPdcUpdaterClass *klass)
 | 
						|
{
 | 
						|
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
						|
	object_class->finalize = fu_qmi_pdc_updater_finalize;
 | 
						|
}
 | 
						|
 | 
						|
FuQmiPdcUpdater *
 | 
						|
fu_qmi_pdc_updater_new(const gchar *path)
 | 
						|
{
 | 
						|
	FuQmiPdcUpdater *self = g_object_new(FU_TYPE_QMI_PDC_UPDATER, NULL);
 | 
						|
	self->qmi_port = g_strdup(path);
 | 
						|
	return self;
 | 
						|
}
 |