From 615bd76c9a58f22fd96017b109a495f6c9a69f8e Mon Sep 17 00:00:00 2001 From: ssahu777 Date: Wed, 8 Apr 2026 14:07:32 +0000 Subject: [PATCH 01/36] 1.6.2 release changelog updates --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd15ba9..f3d94520 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,9 +4,22 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [1.6.2](https://github.com/rdkcentral/rdkfwupdater/compare/1.6.1...1.6.2) + +- RDKEMW-10029 : Syncing of Gerrit commits that are required for security components [`#209`](https://github.com/rdkcentral/rdkfwupdater/pull/209) +- RDKEMW-15146 : Introduce mechanism to unlock debug service - read /opt/swupdate.conf [`#205`](https://github.com/rdkcentral/rdkfwupdater/pull/205) +- RDKTV-39830:Xumo TV devices got stuck with old firmware with http eror code 405 [`#213`](https://github.com/rdkcentral/rdkfwupdater/pull/213) +- Enable L2 For PKCS11 [`#192`](https://github.com/rdkcentral/rdkfwupdater/pull/192) +- RDKTV-39830:Xumo TV devices got stuck with old firmware with http error code 405- adding L1 tests [`3b9880b`](https://github.com/rdkcentral/rdkfwupdater/commit/3b9880b43a75053f159a8dfc20c874277eb83f63) +- RDKTV-39830:Xumo TV devices got stuck with old firmware with http error code 405 [`3beb5c8`](https://github.com/rdkcentral/rdkfwupdater/commit/3beb5c8638ed87bb500985d651a9b24b34aea0e1) +- RDKTV-39830:Xumo TV devices got stuck with old firmware with http error code 405 [`e261e90`](https://github.com/rdkcentral/rdkfwupdater/commit/e261e90b596f12c28be561e0d132059bc95cc2e7) + #### [1.6.1](https://github.com/rdkcentral/rdkfwupdater/compare/1.6.0...1.6.1) +> 11 March 2026 + - RDK-60527:Implement registerProcess and unregisterProcess APIs [`#194`](https://github.com/rdkcentral/rdkfwupdater/pull/194) +- 1.6.1 release changelog updates [`c1d0d29`](https://github.com/rdkcentral/rdkfwupdater/commit/c1d0d2983a68708984637eddce9d8ae4b9c436c2) - Merge tag '1.6.0' into develop [`cca2cb9`](https://github.com/rdkcentral/rdkfwupdater/commit/cca2cb9c2faaa01e3f9f39ddcdf71a77b1e1c717) #### [1.6.0](https://github.com/rdkcentral/rdkfwupdater/compare/1.5.2...1.6.0) From 7082ae19376ce7eb3d31277fe7984beec5aa300e Mon Sep 17 00:00:00 2001 From: Satya Sundar Sahu Date: Tue, 14 Apr 2026 16:15:00 +0530 Subject: [PATCH 02/36] Update rdkFwupdateMgr_api.c --- librdkFwupdateMgr/src/rdkFwupdateMgr_api.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index 261af3ce..edaa2985 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -149,7 +149,7 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, /* [5] Return immediately — app is unblocked */ return CHECK_FOR_UPDATE_SUCCESS; } - +#if 0 /* ======================================================================== * LIBRARY LIFECYCLE * ======================================================================== */ @@ -182,7 +182,7 @@ static void rdkFwupdateMgr_lib_deinit(void) internal_system_deinit(); FWUPMGR_INFO("=== rdkFwupdateMgr library unloaded ===\n"); } - +#endif /* ======================================================================== * DOWNLOAD FIRMWARE PUBLIC API * ======================================================================== From 68e4363072c5ad8a56c131b6ea8ddff009071f94 Mon Sep 17 00:00:00 2001 From: Satya Sundar Sahu Date: Tue, 14 Apr 2026 16:17:49 +0530 Subject: [PATCH 03/36] Update rdkFwupdateMgr_process.c --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 8e3c27ae..dbdb4581 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -363,6 +363,12 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib snprintf(handle_str, 32, "%" PRIu64, handler_id); FWUPMGR_INFO("Handle created: '%s'\n", handle_str); + FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen ===\n"); + if (internal_system_init() != 0) { + FWUPMGR_ERROR("rdkFwupdateMgr_lib_init: internal_system_init FAILED\n"); + } + FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen successfull ===\n"); + return (FirmwareInterfaceHandle)handle_str; } From 75d6941921475fb65b823cbff0c94eb7b8ad224b Mon Sep 17 00:00:00 2001 From: Satya Sundar Sahu Date: Tue, 14 Apr 2026 16:20:46 +0530 Subject: [PATCH 04/36] Update rdkFwupdateMgr_process.c --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index dbdb4581..2da34018 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -442,6 +442,9 @@ void unregisterProcess(FirmwareInterfaceHandle handler) } FWUPMGR_INFO(" handler_id: %"G_GUINT64_FORMAT"\n", handler_id); + FWUPMGR_INFO("=== rdkFwupdateMgr destroy thred unloading ===\n"); + internal_system_deinit(); + FWUPMGR_INFO("=== rdkFwupdateMgr destory thread ===\n"); // Create D-Bus proxy proxy = create_dbus_proxy(&error); From 898eb6009ba5c36ad68cbec8277e19ff3c4e66be Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 15 Apr 2026 11:39:30 +0000 Subject: [PATCH 05/36] RDKEMW-17052:Address review comments- change the thread start to the register part --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 2da34018..7e4d5ba3 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -69,6 +69,7 @@ */ #include "rdkFwupdateMgr_client.h" +#include "rdkFwupdateMgr_async_internal.h" #include "rdkFwupdateMgr_log.h" #include #include @@ -95,8 +96,7 @@ /** Maximum length for library version string */ #define MAX_LIB_VERSION_LEN 64 -/** Default D-Bus call timeout in milliseconds (10 seconds) */ -#define DBUS_TIMEOUT_MS 10000 +/* DBUS_TIMEOUT_MS is defined in rdkFwupdateMgr_async_internal.h */ /* ======================================================================== * INTERNAL CONTEXT STRUCTURE From 152d7d45e7819a02710ed4a1779c980d84ca35a4 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 15 Apr 2026 11:39:30 +0000 Subject: [PATCH 06/36] RDKEMW-17052:Address review comments- change the thread start to the register part --- Makefile.am | 1 - librdkFwupdateMgr/src/rdkFwupdateMgr_log.c | 159 ------------- librdkFwupdateMgr/src/rdkFwupdateMgr_log.h | 210 +++++++----------- .../src/rdkFwupdateMgr_process.c | 4 +- src/dbus/rdkFwupdateMgr_handlers.c | 2 +- src/rdkFwupdateMgr.c | 52 +++-- src/rdkv_upgrade.c | 3 - 7 files changed, 119 insertions(+), 312 deletions(-) delete mode 100755 librdkFwupdateMgr/src/rdkFwupdateMgr_log.c mode change 100755 => 100644 librdkFwupdateMgr/src/rdkFwupdateMgr_log.h diff --git a/Makefile.am b/Makefile.am index 58363925..b2a73b82 100644 --- a/Makefile.am +++ b/Makefile.am @@ -181,7 +181,6 @@ include_HEADERS = \ librdkFwupdateMgr_la_SOURCES = \ ${top_srcdir}/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c \ - ${top_srcdir}/librdkFwupdateMgr/src/rdkFwupdateMgr_log.c \ ${top_srcdir}/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c \ ${top_srcdir}/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_log.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_log.c deleted file mode 100755 index 9f8862ce..00000000 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_log.c +++ /dev/null @@ -1,159 +0,0 @@ -/* - * Copyright 2026 Comcast Cable Communications Management, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file rdkFwupdateMgr_log.c - * @brief Logging implementation for librdkFwupdateMgr client library - */ - -#include "rdkFwupdateMgr_log.h" -#include -#include -#include -#include -#include -#include -#include - -/* ======================================================================== - * INTERNAL STATE - * ======================================================================== */ - -static FILE *g_log_file = NULL; -static pthread_mutex_t g_log_mutex = PTHREAD_MUTEX_INITIALIZER; -static int g_log_initialized = 0; - -/* ======================================================================== - * LOGGING IMPLEMENTATION - * ======================================================================== */ - -/** - * @brief Initialize logging - */ -void fwupmgr_log_init(void) -{ - pthread_mutex_lock(&g_log_mutex); - - if (g_log_initialized) { - pthread_mutex_unlock(&g_log_mutex); - return; // Already initialized - } - - // Create log directory if it doesn't exist - mkdir("/opt/logs", 0755); // Ignore error if exists - - // Open log file in append mode - g_log_file = fopen(FWUPMGR_LOG_FILE, "a"); - if (!g_log_file) { - // Fallback to stderr if log file can't be opened - fprintf(stderr, "[%s] WARNING: Cannot open log file %s: %s\n", - FWUPMGR_LOG_MODULE, FWUPMGR_LOG_FILE, strerror(errno)); - fprintf(stderr, "[%s] Logging will go to stderr\n", FWUPMGR_LOG_MODULE); - } else { - // Make log file line-buffered for immediate writes - setlinebuf(g_log_file); - } - - g_log_initialized = 1; - pthread_mutex_unlock(&g_log_mutex); - - // Log initialization message - fwupmgr_log_internal("INFO", "Logging initialized\n"); -} - -/** - * @brief Close logging - */ -void fwupmgr_log_close(void) -{ - pthread_mutex_lock(&g_log_mutex); - - if (!g_log_initialized) { - pthread_mutex_unlock(&g_log_mutex); - return; // Not initialized - } - - if (g_log_file) { - // Write shutdown message directly to avoid deadlock - // (fwupmgr_log_internal would try to lock g_log_mutex again) - time_t now; - struct tm *tm_info; - char timestamp[64]; - - time(&now); - tm_info = localtime(&now); - if (strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info) == 0) { - snprintf(timestamp, sizeof(timestamp), "UNKNOWN-TIME"); - } - - fprintf(g_log_file, "%s [%s] INFO: Logging shutdown\n", - timestamp, FWUPMGR_LOG_MODULE); - fflush(g_log_file); - - fclose(g_log_file); - g_log_file = NULL; - } - - g_log_initialized = 0; - pthread_mutex_unlock(&g_log_mutex); -} - -/** - * @brief Internal logging function with timestamp and thread-safety - */ -void fwupmgr_log_internal(const char *level, const char *format, ...) -{ - time_t now; - struct tm *tm_info; - char timestamp[64]; - va_list args; - FILE *output; - - pthread_mutex_lock(&g_log_mutex); - - // Auto-initialize if not done - if (!g_log_initialized) { - pthread_mutex_unlock(&g_log_mutex); - fwupmgr_log_init(); - pthread_mutex_lock(&g_log_mutex); - } - - // Determine output stream (log file or stderr fallback) - output = g_log_file ? g_log_file : stderr; - - // Get current timestamp - time(&now); - tm_info = localtime(&now); - if (strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", tm_info) == 0) { - snprintf(timestamp, sizeof(timestamp), "UNKNOWN-TIME"); - } - - // Write log header: timestamp [MODULE] LEVEL: - fprintf(output, "%s [%s] %s: ", timestamp, FWUPMGR_LOG_MODULE, level); - - // Write log message - va_start(args, format); - vfprintf(output, format, args); - va_end(args); - - // Ensure immediate write - fflush(output); - - pthread_mutex_unlock(&g_log_mutex); -} - diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_log.h b/librdkFwupdateMgr/src/rdkFwupdateMgr_log.h old mode 100755 new mode 100644 index f9012fcf..8f58a3ff --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_log.h +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_log.h @@ -1,124 +1,86 @@ -/* - * Copyright 2025 Comcast Cable Communications Management, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -/** - * @file rdkFwupdateMgr_log.h - * @brief Logging macros for librdkFwupdateMgr client library - * - * This header provides logging macros that write to /opt/logs/rdkFwupdateMgr.log - * using the RDK logger infrastructure, similar to SWLOG_* macros used in the daemon. - */ - -#ifndef RDKFWUPDATEMGR_LOG_H -#define RDKFWUPDATEMGR_LOG_H - -#include -#include - -#ifdef __cplusplus -extern "C" { -#endif - -/* ======================================================================== - * LOGGING CONFIGURATION - * ======================================================================== */ - -/** Log file path - same as daemon for consistent logging */ -#define FWUPMGR_LOG_FILE "/opt/logs/rdkFwupdateMgr.log" - -/** Log module name for identification */ -#define FWUPMGR_LOG_MODULE "librdkFwupdateMgr" - -/* ======================================================================== - * LOGGING API - * ======================================================================== */ - -/** - * @brief Initialize logging for the library - * - * Opens the log file for appending. Should be called once at library init. - * Safe to call multiple times (no-op after first call). - */ -void fwupmgr_log_init(void); - -/** - * @brief Close logging resources - * - * Closes the log file. Should be called at library cleanup. - * Safe to call multiple times (no-op if already closed). - */ -void fwupmgr_log_close(void); - -/** - * @brief Internal logging function - * - * @param level Log level string ("INFO", "ERROR", "DEBUG", "WARN") - * @param format Printf-style format string - * @param ... Variable arguments for format string - */ -void fwupmgr_log_internal(const char *level, const char *format, ...); - -/* ======================================================================== - * LOGGING MACROS - Same pattern as SWLOG_* in daemon - * ======================================================================== */ - -/** - * @brief Log informational message - * - * Usage: FWUPMGR_INFO("Registered with handler: %s\n", handler_id); - */ -#define FWUPMGR_INFO(format, ...) \ - fwupmgr_log_internal("INFO", "[%s:%d] " format, __FUNCTION__, __LINE__, ##__VA_ARGS__) - -/** - * @brief Log error message - * - * Usage: FWUPMGR_ERROR("Registration failed: %s\n", error_msg); - */ -#define FWUPMGR_ERROR(format, ...) \ - fwupmgr_log_internal("ERROR", "[%s:%d] " format, __FUNCTION__, __LINE__, ##__VA_ARGS__) - -/** - * @brief Log debug message - * - * Usage: FWUPMGR_DEBUG("D-Bus proxy created: %p\n", proxy); - */ -#define FWUPMGR_DEBUG(format, ...) \ - fwupmgr_log_internal("DEBUG", "[%s:%d] " format, __FUNCTION__, __LINE__, ##__VA_ARGS__) - -/** - * @brief Log warning message - * - * Usage: FWUPMGR_WARN("Daemon not responding, retry recommended\n"); - */ -#define FWUPMGR_WARN(format, ...) \ - fwupmgr_log_internal("WARN", "[%s:%d] " format, __FUNCTION__, __LINE__, ##__VA_ARGS__) - -/** - * @brief Log fatal error message - * - * Usage: FWUPMGR_FATAL("Out of memory, cannot continue\n"); - */ -#define FWUPMGR_FATAL(format, ...) \ - fwupmgr_log_internal("FATAL", "[%s:%d] " format, __FUNCTION__, __LINE__, ##__VA_ARGS__) - -#ifdef __cplusplus -} -#endif - -#endif /* RDKFWUPDATEMGR_LOG_H */ +/* + * Copyright 2025 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file rdkFwupdateMgr_log.h + * @brief Logging macros for librdkFwupdateMgr client library + * + * FWUPMGR_* macros log directly with the "LOG.RDK.FWUPMGR" module + * (when RDK_LOGGER is enabled) so that library log lines appear as + * "[FWUPMGR]" in the output clearly distinguishable from daemon + * logs ("[FWUPG]") and common-utility logs ("[COMMONUTILITIES]") + * without any redundant double-tagging. + * + * The hosting application (example_plugin, unit-test harness, etc.) + * is responsible for calling log_init() before using this library + * and log_exit() on shutdown. The library does NOT own the log + * lifecycle. + * + * Usage: + * FWUPMGR_INFO("Registered with handler: %s\n", handler_id); + * FWUPMGR_ERROR("Registration failed: %s\n", error_msg); + * FWUPMGR_DEBUG("D-Bus proxy created: %p\n", proxy); + */ + +#ifndef RDKFWUPDATEMGR_LOG_H +#define RDKFWUPDATEMGR_LOG_H + +#include "rdkv_cdl_log_wrapper.h" /* SWLOG_*, log_init(), log_exit() */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* ======================================================================== + * Library code uses FWUPMGR_* macros - logs as [FWUPMGR] + * Example app defines EXAMPLE_* macros - logs as [EXAMPLE] + * ======================================================================== */ + +#if defined(RDK_LOGGER) +#include "rdk_debug.h" + +/* Generic base macro callers provide their own module name */ +#define FWUPMGR_LOG(level, module, format, ...) \ + RDK_LOG(level, module, format, ##__VA_ARGS__) + +/* Default library macros use LOG.RDK.FWUPMGR */ +#define FWUPMGR_TRACE(format, ...) FWUPMGR_LOG(RDK_LOG_TRACE1, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_FATAL(format, ...) FWUPMGR_LOG(RDK_LOG_FATAL, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) + +#else + + +/* Default library macros */ +#define FWUPMGR_TRACE(FORMAT...) FWUPMGR_LOG(FWUPMGR_LOG_INFO, "FWUPMGR", FORMAT) +#define FWUPMGR_DEBUG(FORMAT...) FWUPMGR_LOG(FWUPMGR_LOG_INFO, "FWUPMGR", FORMAT) +#define FWUPMGR_INFO(FORMAT...) FWUPMGR_LOG(FWUPMGR_LOG_INFO, "FWUPMGR", FORMAT) +#define FWUPMGR_WARN(FORMAT...) FWUPMGR_LOG(FWUPMGR_LOG_INFO, "FWUPMGR", FORMAT) +#define FWUPMGR_ERROR(FORMAT...) FWUPMGR_LOG(FWUPMGR_LOG_INFO, "FWUPMGR", FORMAT) +#define FWUPMGR_FATAL(FORMAT...) FWUPMGR_LOG(FWUPMGR_LOG_INFO, "FWUPMGR", FORMAT) + +#endif + +#ifdef __cplusplus +} +#endif + +#endif /* RDKFWUPDATEMGR_LOG_H */ diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 2da34018..7e4d5ba3 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -69,6 +69,7 @@ */ #include "rdkFwupdateMgr_client.h" +#include "rdkFwupdateMgr_async_internal.h" #include "rdkFwupdateMgr_log.h" #include #include @@ -95,8 +96,7 @@ /** Maximum length for library version string */ #define MAX_LIB_VERSION_LEN 64 -/** Default D-Bus call timeout in milliseconds (10 seconds) */ -#define DBUS_TIMEOUT_MS 10000 +/* DBUS_TIMEOUT_MS is defined in rdkFwupdateMgr_async_internal.h */ /* ======================================================================== * INTERNAL CONTEXT STRUCTURE diff --git a/src/dbus/rdkFwupdateMgr_handlers.c b/src/dbus/rdkFwupdateMgr_handlers.c index d80af84d..57921a01 100644 --- a/src/dbus/rdkFwupdateMgr_handlers.c +++ b/src/dbus/rdkFwupdateMgr_handlers.c @@ -477,7 +477,7 @@ static int fetch_xconf_firmware_info( XCONFRES *pResponse, int server_type, int Rfc_t local_rfc_list = {0}; getRFCSettings(&local_rfc_list); // Read actual RFC settings from system - const char *local_immed_reboot_flag = "false"; // Default daemon setting + const char *local_immed_reboot_flag = "true"; // Making true as default setting to make it work in Throttle enable mode. int local_delay_dwnl = 0; // Default daemon setting const char *local_lastrun = "0"; // Default daemon setting char *local_disableStatsUpdate = "false"; // Default daemon setting diff --git a/src/rdkFwupdateMgr.c b/src/rdkFwupdateMgr.c index ba589c29..efb6f097 100644 --- a/src/rdkFwupdateMgr.c +++ b/src/rdkFwupdateMgr.c @@ -1243,32 +1243,40 @@ int main(int argc, char *argv[]) { SWLOG_INFO("XCONF Download Fail\n"); } */ + } + else if(init_validate_status == INITIAL_VALIDATION_DWNL_COMPLETED){ + /** + * A previous firmware download+flash already completed + * (/tmp/fw_preparing_to_reboot was present). + * The file has been cleaned up by initialValidation(). + * In the daemon mode, we transition to IDLE and wait + * initialValidation() is also responsible for emitting + * the MAINT_FWDOWNLOAD_COMPLETE event for this case. + * In the daemon mode, we transition to IDLE and wait + * for the pending reboot or next D-Bus request. + */ + SWLOG_INFO("Software Update already completed (pending reboot). " + "Transitioning to IDLE.\n"); + currentState = STATE_IDLE; + } + else if(init_validate_status == INITIAL_VALIDATION_DWNL_INPROGRESS){ + /** + * Another instance is currently downloading firmware. + * In the daemon model, transition to IDLE and wait. + * The in-progress download will complete independently. + */ + SWLOG_INFO("Firmware download already in progress by another process. " + "Transitioning to IDLE.\n"); + if (0 == (strncmp(device_info.maint_status, "true", 4))) { + eventManager("MaintenanceMGR", MAINT_FWDOWNLOAD_INPROGRESS); + } + currentState = STATE_IDLE; } else{ - SWLOG_ERROR("Initial validation failed\n"); + /* INITIAL_VALIDATION_FAIL or unknown status */ + SWLOG_ERROR("Initial validation failed (status=%d)\n", init_validate_status); goto cleanup_and_exit; } - /*this is for sending the intermediate updates back to apps and */ - /* - if (init_validate_status == INITIAL_VALIDATION_DWNL_INPROGRESS){ - if (!(strncmp(device_info.maint_status, "true", 4))) { - eventManager("MaintenanceMGR", MAINT_FWDOWNLOAD_INPROGRESS); //Sending status to maintenance manager - } - }else if(init_validate_status == INITIAL_VALIDATION_DWNL_COMPLETED) { - SWLOG_INFO("Software Update is completed by AS/EPG, Exiting from firmware download.\n"); - }else if ((ret_curl_code != 0) || (json_res != 0)) { - if (!(strncmp(device_info.maint_status, "true", 4))) { - eventManager("MaintenanceMGR", MAINT_FWDOWNLOAD_ERROR); //Sending status to maintenance manager - } - if (trigger_type == 6) { - unsetStateRed(); - } - }else { - if (!(strncmp(device_info.maint_status, "true", 4))) { - eventManager("MaintenanceMGR", MAINT_FWDOWNLOAD_COMPLETE); //Sending status to maintenance manager - } - } - */ break; case STATE_IDLE: /** diff --git a/src/rdkv_upgrade.c b/src/rdkv_upgrade.c index 71fe70b4..a23cc8ba 100755 --- a/src/rdkv_upgrade.c +++ b/src/rdkv_upgrade.c @@ -983,9 +983,6 @@ int downloadFile( } if ((1 == (isThrottleEnabled(device_info->dev_name, immed_reboot_flag, app_mode)))) { - /* Coverity fix: NO_EFFECT - rfc_throttle is a char array, not a pointer. - * Removed redundant "!= NULL" check. Only check for non-empty string. - * Ensure rfc_list is valid before dereferencing. */ if (rfc_list != NULL && rfc_list->rfc_throttle[0] != '\0' && 0 == (strncmp(rfc_list->rfc_throttle, "true", 4))) { max_dwnl_speed = atoi(rfc_list->rfc_topspeed); From d66e1ac588a71d9f1f1ac216d527bcebeddec250 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Tue, 28 Apr 2026 03:21:24 +0000 Subject: [PATCH 07/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes --- librdkFwupdateMgr/examples/example_app.c | 211 +++++++++--------- .../src/rdkFwupdateMgr_process.c | 6 +- 2 files changed, 109 insertions(+), 108 deletions(-) diff --git a/librdkFwupdateMgr/examples/example_app.c b/librdkFwupdateMgr/examples/example_app.c index f2207880..12c2ba6b 100644 --- a/librdkFwupdateMgr/examples/example_app.c +++ b/librdkFwupdateMgr/examples/example_app.c @@ -39,6 +39,18 @@ //#include "rdkFwupdateMgr_process.h" /* registerProcess(), unregisterProcess() */ #include "rdkFwupdateMgr_client.h" /* checkForUpdate(), downloadFirmware(), updateFirmware(), all callbacks/enums */ +#include "rdkFwupdateMgr_log.h" /* FWUPMGR_LOG() generic base macro */ +#include "rdkv_cdl_log_wrapper.h" /* log_init(), log_exit() */ + +/* ======================================================================== + * EXAMPLE_* logging macros use FWUPMGR_LOG with LOG.RDK.EXAMPLE module. + * Keeps example_plugin logs as [EXAMPLE], distinguishable from [FWUPMGR] + * library logs and [FWUPG] daemon logs. + * ======================================================================== */ +#define EXAMPLE_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +#define EXAMPLE_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +#define EXAMPLE_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +#define EXAMPLE_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) #include #include #include @@ -104,11 +116,10 @@ static int g_exit_code = EXIT_SUCCESS; */ static void on_firmware_check_callback(const FwInfoData *event_data) { - printf("\n"); - printf("│ ✓ checkForUpdate Callback Received │\n"); + EXAMPLE_INFO("checkForUpdate Callback Received\n"); if (!event_data) { - fprintf(stderr, "[ERROR] event_data is NULL in callback!\n"); + EXAMPLE_ERROR("event_data is NULL in callback!\n"); pthread_mutex_lock(&g_check_mutex); g_check_status = FIRMWARE_CHECK_ERROR; g_check_done = 1; @@ -128,40 +139,40 @@ static void on_firmware_check_callback(const FwInfoData *event_data) case BYPASS_OPTOUT: status_str = "BYPASS_OPTOUT"; break; } - printf("\n === Basic Firmware Info ===\n"); - printf(" Handle : %s\n", g_handle ? g_handle : "(null)"); - printf(" Status Code : %s (%d)\n", status_str, event_data->status); - printf(" Current FW Version : %s\n", + EXAMPLE_INFO("=== Basic Firmware Info ===\n"); + EXAMPLE_INFO(" Handle : %s\n", g_handle ? g_handle : "(null)"); + EXAMPLE_INFO(" Status Code : %s (%d)\n", status_str, event_data->status); + EXAMPLE_INFO(" Current FW Version : %s\n", event_data->CurrFWVersion[0] ? event_data->CurrFWVersion : "(not provided)"); /* Print UpdateDetails if available (only when status == FIRMWARE_AVAILABLE) */ if (event_data->status == FIRMWARE_AVAILABLE && event_data->UpdateDetails) { - printf("\n === Update Details (Available!) ===\n"); - printf(" FwFileName : %s\n", + EXAMPLE_INFO("=== Update Details (Available!) ===\n"); + EXAMPLE_INFO(" FwFileName : %s\n", event_data->UpdateDetails->FwFileName[0] ? event_data->UpdateDetails->FwFileName : "null"); - printf(" FwUrl : %s\n", + EXAMPLE_INFO(" FwUrl : %s\n", event_data->UpdateDetails->FwUrl[0] ? event_data->UpdateDetails->FwUrl : "null"); - printf(" FwVersion : %s\n", + EXAMPLE_INFO(" FwVersion : %s\n", event_data->UpdateDetails->FwVersion[0] ? event_data->UpdateDetails->FwVersion : "null"); - printf(" RebootImmediately : %s\n", + EXAMPLE_INFO(" RebootImmediately : %s\n", event_data->UpdateDetails->RebootImmediately[0] ? event_data->UpdateDetails->RebootImmediately : "null"); - printf(" DelayDownload : %s\n", + EXAMPLE_INFO(" DelayDownload : %s\n", event_data->UpdateDetails->DelayDownload[0] ? event_data->UpdateDetails->DelayDownload : "null"); - printf(" PDRIVersion : %s\n", + EXAMPLE_INFO(" PDRIVersion : %s\n", event_data->UpdateDetails->PDRIVersion[0] ? event_data->UpdateDetails->PDRIVersion : "null"); - printf(" PeripheralFirmwares : %s\n", + EXAMPLE_INFO(" PeripheralFirmwares : %s\n", event_data->UpdateDetails->PeripheralFirmwares[0] ? event_data->UpdateDetails->PeripheralFirmwares : "null"); } else if (event_data->status == FIRMWARE_AVAILABLE && !event_data->UpdateDetails) { - printf("\n ⚠ WARNING: Status is FIRMWARE_AVAILABLE but UpdateDetails is NULL!\n"); + EXAMPLE_WARN("Status is FIRMWARE_AVAILABLE but UpdateDetails is NULL!\n"); } else { - printf("\n → No update details (status != FIRMWARE_AVAILABLE)\n"); + EXAMPLE_INFO("No update details (status != FIRMWARE_AVAILABLE)\n"); } /* Copy data to global state (data is only valid during this callback!) */ @@ -219,7 +230,7 @@ static void on_firmware_check_callback(const FwInfoData *event_data) pthread_cond_signal(&g_check_cond); pthread_mutex_unlock(&g_check_mutex); - printf("\n → Firmware check data saved. Main thread will proceed.\n"); + EXAMPLE_INFO("Firmware check data saved. Main thread will proceed.\n"); } /* ======================================================================== @@ -246,13 +257,8 @@ static void on_download_progress_callback(int progress_per, DownloadStatus fwdwn case DWNL_ERROR: status_str = "DWNL_ERROR"; break; } - /* Print progress bar: [████████░░░░░░░░░░░░] 40% DWNL_IN_PROGRESS */ - int bar_filled = progress_per / 5; /* 20 characters = 100% */ - printf(" ["); - for (int i = 0; i < 20; i++) { - printf(i < bar_filled ? "█" : "░"); - } - printf("] %3d%% %s\n", progress_per, status_str); + /* Print progress: 40% DWNL_IN_PROGRESS */ + EXAMPLE_INFO(" Download: %3d%% %s\n", progress_per, status_str); /* On terminal states (COMPLETED or ERROR), wake main thread */ if (fwdwnlstatus == DWNL_COMPLETED || fwdwnlstatus == DWNL_ERROR) { @@ -263,9 +269,9 @@ static void on_download_progress_callback(int progress_per, DownloadStatus fwdwn pthread_mutex_unlock(&g_download_mutex); if (fwdwnlstatus == DWNL_COMPLETED) { - printf("\n ✓ Download completed successfully!\n\n"); + EXAMPLE_INFO(" Download completed successfully!\n"); } else { - printf("\n ✗ Download failed!\n\n"); + EXAMPLE_ERROR(" Download failed!\n"); } } } @@ -294,13 +300,8 @@ static void on_update_progress_callback(int progress_per, UpdateStatus fwupdates case UPDATE_ERROR: status_str = "UPDATE_ERROR"; break; } - /* Print progress bar: [████████░░░░░░░░░░░░] 40% UPDATE_IN_PROGRESS */ - int bar_filled = progress_per / 5; /* 20 characters = 100% */ - printf(" ["); - for (int i = 0; i < 20; i++) { - printf(i < bar_filled ? "▓" : "░"); - } - printf("] %3d%% %s\n", progress_per, status_str); + /* Print progress: 40% UPDATE_IN_PROGRESS */ + EXAMPLE_INFO(" Flash: %3d%% %s\n", progress_per, status_str); /* On terminal states (COMPLETED or ERROR), wake main thread */ if (fwupdatestatus == UPDATE_COMPLETED || fwupdatestatus == UPDATE_ERROR) { @@ -311,9 +312,9 @@ static void on_update_progress_callback(int progress_per, UpdateStatus fwupdates pthread_mutex_unlock(&g_update_mutex); if (fwupdatestatus == UPDATE_COMPLETED) { - printf("\n ✓ Firmware flash completed successfully!\n\n"); + EXAMPLE_INFO(" Firmware flash completed successfully!\n"); } else { - printf("\n ✗ Firmware flash failed!\n\n"); + EXAMPLE_ERROR(" Firmware flash failed!\n"); } } } @@ -334,53 +335,57 @@ int main(void) struct timespec timeout; int rc; - printf("\n"); - printf("║ RDK Firmware Update Manager - Complete Workflow ║\n"); + /* Initialize logging — must be first. + * All EXAMPLE_* and FWUPMGR_* log output goes to stdout/stderr. + * Shell redirect puts it in the right file: + * example_plugin > /opt/logs/rdkFwupdateMgr.log 2>&1 + */ + log_init(); + + EXAMPLE_INFO("==============================\n"); + EXAMPLE_INFO("Application starting, PID: %d\n", getpid()); /* ==================================================================== * STEP 1: Register Process with Daemon * ==================================================================== */ - printf("│ STEP 1: Register with firmware daemon │\n"); - printf(" Process Name : ExampleApp\n"); - printf(" Lib Version : 1.0.0\n\n"); + EXAMPLE_INFO("STEP 1: Register with firmware daemon\n"); + EXAMPLE_INFO(" Process Name : ExampleApp\n"); + EXAMPLE_INFO(" Lib Version : 1.0.0\n"); g_handle = registerProcess("ExampleApp", "1.0.0"); if (g_handle == NULL) { - fprintf(stderr, "[ERROR] registerProcess() failed!\n"); - fprintf(stderr, " Ensure rdkFwupdateMgr daemon is running:\n"); - fprintf(stderr, " systemctl status rdkFwupdateMgr.service\n\n"); + EXAMPLE_ERROR("registerProcess() failed!\n"); + EXAMPLE_ERROR("Ensure rdkFwupdateMgr daemon is running:\n"); + EXAMPLE_ERROR("systemctl status rdkFwupdateMgr.service\n"); + log_exit(); return EXIT_FAILURE; } - printf(" ✓ Registered successfully\n"); - printf(" Handle: '%s'\n\n", g_handle); + EXAMPLE_INFO("Registered successfully\n"); + EXAMPLE_INFO(" Handle: '%s'\n", g_handle); /* ==================================================================== * STEP 2: Check for Firmware Updates (Async) * ==================================================================== */ - printf("│ STEP 2: Check for firmware updates │\n"); - printf(" Calling checkForUpdate()...\n"); - printf(" (API returns immediately; callback fires when XConf query completes)\n\n"); + EXAMPLE_INFO("STEP 2: Check for firmware updates\n"); + EXAMPLE_INFO(" Calling checkForUpdate()...\n"); + EXAMPLE_INFO(" (API returns immediately; callback fires when XConf query completes)\n"); CheckForUpdateResult cfu_result = checkForUpdate(g_handle, on_firmware_check_callback); if (cfu_result != CHECK_FOR_UPDATE_SUCCESS) { - fprintf(stderr, "[ERROR] checkForUpdate() returned FAIL!\n"); - fprintf(stderr, " Possible reasons:\n"); - fprintf(stderr, " - D-Bus connection error\n"); - fprintf(stderr, " - Daemon not responding\n"); - fprintf(stderr, " - Invalid handle\n\n"); + EXAMPLE_ERROR("checkForUpdate() returned FAIL!\n"); + EXAMPLE_ERROR("Possible reasons: D-Bus error, daemon not responding, invalid handle\n"); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } - printf(" ✓ checkForUpdate() returned SUCCESS\n"); - printf(" (Daemon ACK received - waiting for actual firmware data...)\n\n"); + EXAMPLE_INFO("checkForUpdate() returned SUCCESS\n"); + EXAMPLE_INFO(" (Daemon ACK received - waiting for actual firmware data...)\n"); /* Wait for callback with timeout (2 minutes for XConf query) */ - printf(" Waiting for firmware check callback"); - fflush(stdout); + EXAMPLE_INFO("Waiting for firmware check callback...\n"); clock_gettime(CLOCK_REALTIME, &timeout); timeout.tv_sec += 120; /* 2 minute timeout */ @@ -390,8 +395,8 @@ int main(void) rc = pthread_cond_timedwait(&g_check_cond, &g_check_mutex, &timeout); if (rc != 0) { pthread_mutex_unlock(&g_check_mutex); - fprintf(stderr, "\n[ERROR] Timeout waiting for checkForUpdate callback (120s)\n"); - fprintf(stderr, " XConf query may be taking longer than expected.\n\n"); + EXAMPLE_ERROR("Timeout waiting for checkForUpdate callback (120s)\n"); + EXAMPLE_ERROR("XConf query may be taking longer than expected.\n"); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } @@ -399,31 +404,30 @@ int main(void) pthread_mutex_unlock(&g_check_mutex); /* Check result */ - printf("\n"); if (g_check_status != FIRMWARE_AVAILABLE) { - printf(" ⚠ No firmware update available\n"); - printf(" Status: %d\n", g_check_status); - printf(" Current Version: %s\n", g_fw_current_version); + EXAMPLE_WARN("No firmware update available\n"); + EXAMPLE_INFO(" Status: %d\n", g_check_status); + EXAMPLE_INFO(" Current Version: %s\n", g_fw_current_version); if (g_check_status == FIRMWARE_NOT_AVAILABLE) { - printf(" → Already on latest version. No action needed.\n\n"); + EXAMPLE_INFO(" Already on latest version. No action needed.\n"); g_exit_code = EXIT_SUCCESS; } else { - printf(" → Cannot proceed with update.\n\n"); + EXAMPLE_ERROR(" Cannot proceed with update.\n"); g_exit_code = EXIT_FAILURE; } goto cleanup_unregister; } - printf(" ✓ Firmware update available!\n"); - printf(" Current Version : %s\n", g_fw_current_version); - printf(" Available Version: %s\n", g_fw_available_version); - printf(" → Proceeding to download...\n\n"); + EXAMPLE_INFO("Firmware update available!\n"); + EXAMPLE_INFO(" Current Version : %s\n", g_fw_current_version); + EXAMPLE_INFO(" Available Version: %s\n", g_fw_available_version); + EXAMPLE_INFO(" Proceeding to download...\n"); /* ==================================================================== * STEP 3: Download Firmware (Async) * ==================================================================== */ - printf("│ STEP 3: Download firmware image │\n"); + EXAMPLE_INFO("STEP 3: Download firmware image\n"); /* Prepare download request using data from checkForUpdate callback */ FwDwnlReq download_req; @@ -437,24 +441,23 @@ int main(void) download_req.downloadUrl = fw_url; download_req.TypeOfFirmware = "PCI"; /* Default to PCI type */ - printf(" Firmware Name : %s\n", download_req.firmwareName); - printf(" Download URL : %s\n", download_req.downloadUrl[0] ? download_req.downloadUrl : "(use XConf URL)"); - printf(" Firmware Type : %s\n\n", download_req.TypeOfFirmware); + EXAMPLE_INFO(" Firmware Name : %s\n", download_req.firmwareName); + EXAMPLE_INFO(" Download URL : %s\n", download_req.downloadUrl[0] ? download_req.downloadUrl : "(use XConf URL)"); + EXAMPLE_INFO(" Firmware Type : %s\n", download_req.TypeOfFirmware); - printf(" Calling downloadFirmware()...\n\n"); + EXAMPLE_INFO(" Calling downloadFirmware()...\n"); DownloadResult dl_result = downloadFirmware(g_handle, &download_req, on_download_progress_callback); if (dl_result != RDKFW_DWNL_SUCCESS) { - fprintf(stderr, "[ERROR] downloadFirmware() returned FAIL!\n\n"); + EXAMPLE_ERROR("downloadFirmware() returned FAIL!\n"); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } - printf(" ✓ downloadFirmware() returned SUCCESS\n"); - printf(" Waiting for download progress...\n\n"); - printf(" Download Progress:\n"); + EXAMPLE_INFO("downloadFirmware() returned SUCCESS\n"); + EXAMPLE_INFO(" Waiting for download progress...\n"); /* Wait for download completion with timeout (5 minutes) */ clock_gettime(CLOCK_REALTIME, &timeout); @@ -465,7 +468,7 @@ int main(void) rc = pthread_cond_timedwait(&g_download_cond, &g_download_mutex, &timeout); if (rc != 0) { pthread_mutex_unlock(&g_download_mutex); - fprintf(stderr, "[ERROR] Timeout waiting for download completion (5 min)\n\n"); + EXAMPLE_ERROR("Timeout waiting for download completion (5 min)\n"); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } @@ -474,17 +477,17 @@ int main(void) /* Check download result */ if (g_download_status != DWNL_COMPLETED) { - fprintf(stderr, "[ERROR] Download failed (status=%d)\n\n", g_download_status); + EXAMPLE_ERROR("Download failed (status=%d)\n", g_download_status); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } - printf(" → Download complete. Proceeding to flash...\n\n"); + EXAMPLE_INFO("Download complete. Proceeding to flash...\n"); /* ==================================================================== * STEP 4: Update/Flash Firmware (Async) * ==================================================================== */ - printf("│ STEP 4: Flash firmware to device │\n"); + EXAMPLE_INFO("STEP 4: Flash firmware to device\n"); /* Prepare update request */ FwUpdateReq update_req; @@ -506,25 +509,24 @@ int main(void) /* Reboot after flash: false for this example (so we can unregister cleanly) */ update_req.rebootImmediately = false; - printf(" Firmware Name : %s\n", update_req.firmwareName); - printf(" Firmware Type : %s\n", update_req.TypeOfFirmware); - printf(" Location : %s\n", update_req.LocationOfFirmware); - printf(" Reboot Now : %s\n\n", update_req.rebootImmediately ? "true" : "false"); + EXAMPLE_INFO(" Firmware Name : %s\n", update_req.firmwareName); + EXAMPLE_INFO(" Firmware Type : %s\n", update_req.TypeOfFirmware); + EXAMPLE_INFO(" Location : %s\n", update_req.LocationOfFirmware); + EXAMPLE_INFO(" Reboot Now : %s\n", update_req.rebootImmediately ? "true" : "false"); - printf(" Calling updateFirmware()...\n\n"); + EXAMPLE_INFO(" Calling updateFirmware()...\n"); UpdateResult upd_result = updateFirmware(g_handle, &update_req, on_update_progress_callback); if (upd_result != RDKFW_UPDATE_SUCCESS) { - fprintf(stderr, "[ERROR] updateFirmware() returned FAIL!\n\n"); + EXAMPLE_ERROR("updateFirmware() returned FAIL!\n"); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } - printf(" ✓ updateFirmware() returned SUCCESS\n"); - printf(" Waiting for flash progress...\n\n"); - printf(" Flash Progress:\n"); + EXAMPLE_INFO("updateFirmware() returned SUCCESS\n"); + EXAMPLE_INFO(" Waiting for flash progress...\n"); /* Wait for flash completion with timeout (10 minutes) */ clock_gettime(CLOCK_REALTIME, &timeout); @@ -535,7 +537,7 @@ int main(void) rc = pthread_cond_timedwait(&g_update_cond, &g_update_mutex, &timeout); if (rc != 0) { pthread_mutex_unlock(&g_update_mutex); - fprintf(stderr, "[ERROR] Timeout waiting for flash completion (10 min)\n\n"); + EXAMPLE_ERROR("Timeout waiting for flash completion (10 min)\n"); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } @@ -544,42 +546,41 @@ int main(void) /* Check flash result */ if (g_update_status != UPDATE_COMPLETED) { - fprintf(stderr, "[ERROR] Firmware flash failed (status=%d)\n\n", g_update_status); + EXAMPLE_ERROR("Firmware flash failed (status=%d)\n", g_update_status); g_exit_code = EXIT_FAILURE; goto cleanup_unregister; } - printf(" → Flash complete!\n\n"); + EXAMPLE_INFO("Flash complete!\n"); /* ==================================================================== * STEP 5: Unregister and Cleanup * ==================================================================== */ cleanup_unregister: - printf("│ STEP 5: Unregister from daemon │\n"); + EXAMPLE_INFO("STEP 5: Unregister from daemon\n"); if (g_handle != NULL) { - printf(" Calling unregisterProcess()...\n"); + EXAMPLE_INFO(" Calling unregisterProcess()...\n"); unregisterProcess(g_handle); g_handle = NULL; - printf(" ✓ Unregistered successfully\n\n"); + EXAMPLE_INFO(" Unregistered successfully\n"); } /* ==================================================================== * Final Status * ==================================================================== */ if (g_exit_code == EXIT_SUCCESS) { - printf("║ ✓ FIRMWARE UPDATE WORKFLOW COMPLETED ║\n"); + EXAMPLE_INFO("FIRMWARE UPDATE WORKFLOW COMPLETED\n"); if (g_update_status == UPDATE_COMPLETED) { - printf(" ⚠ NOTE: Firmware flashed successfully.\n"); - printf(" System reboot required to activate new firmware.\n"); - printf(" Use: systemctl reboot\n\n"); + EXAMPLE_INFO(" Firmware flashed successfully.\n"); + EXAMPLE_INFO(" System reboot required to activate new firmware.\n"); } } else { - printf("║ ✗ FIRMWARE UPDATE WORKFLOW FAILED ║\n"); - printf(" Check logs for details:\n"); - printf(" tail -f /opt/logs/rdkFwupdateMgr.log\n\n"); + EXAMPLE_ERROR("FIRMWARE UPDATE WORKFLOW FAILED\n"); + EXAMPLE_INFO(" Check logs for details: tail -f /opt/logs/rdkFwupdateMgr.log\n"); } + log_exit(); return g_exit_code; } diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 7e4d5ba3..4c16475d 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -61,7 +61,7 @@ * * ERROR HANDLING: * =============== - * - All errors logged via fprintf(stderr) for visibility + * - All errors logged via FWUPMGR_* macros (rdkv_cdl_log_wrapper backend) * - NULL checks on all pointer parameters * - D-Bus errors caught and handled gracefully * - Registration failures return NULL (safe to check) @@ -288,10 +288,10 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib return NULL; } - fprintf(stderr, "[rdkFwupdateMgr] D-Bus proxy created successfully\n"); + FWUPMGR_INFO("D-Bus proxy created successfully\n"); // Call RegisterProcess D-Bus method - fprintf(stderr, "[rdkFwupdateMgr] Calling RegisterProcess D-Bus method...\n"); + FWUPMGR_INFO("Calling RegisterProcess D-Bus method...\n"); result = g_dbus_proxy_call_sync( proxy, "RegisterProcess", From 26fbedd5279757333887f9937cb0ff093a4f304a Mon Sep 17 00:00:00 2001 From: mkadinti Date: Tue, 28 Apr 2026 04:03:01 +0000 Subject: [PATCH 08/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes --- Makefile.am | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile.am b/Makefile.am index b2a73b82..02772f88 100644 --- a/Makefile.am +++ b/Makefile.am @@ -257,13 +257,17 @@ example_plugin_SOURCES = \ example_plugin_CFLAGS = \ -I${top_srcdir}/librdkFwupdateMgr/include \ + -I${top_srcdir}/librdkFwupdateMgr/src \ + -I${top_srcdir}/common_utilities/utils \ $(AM_CFLAGS) \ $(GLIB_CFLAGS) example_plugin_LDADD = \ librdkFwupdateMgr.la \ $(GLIB_LIBS) \ - -lpthread + -lpthread \ + -lfwutils \ + -lrdkloggers example_plugin_LDFLAGS = \ -L$(PKG_CONFIG_SYSROOT_DIR)/$(libdir) From 6a51dbb8d143f6fc6b0dd5f8efb49419896812e2 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Tue, 28 Apr 2026 04:08:13 +0000 Subject: [PATCH 09/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes --- src/dbus/rdkv_dbus_server.c | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/dbus/rdkv_dbus_server.c b/src/dbus/rdkv_dbus_server.c index 1eaa2adf..dc463f8a 100644 --- a/src/dbus/rdkv_dbus_server.c +++ b/src/dbus/rdkv_dbus_server.c @@ -1896,23 +1896,18 @@ static void process_app_request(GDBusConnection *rdkv_conn_dbus, return; } - // Look up process info to validate ownership and get process name for logging + // Look up process info to get process name for logging ProcessInfo *process_info = g_hash_table_lookup(registered_processes, GINT_TO_POINTER(handler)); const gchar *process_name = process_info ? process_info->process_name : "UNKNOWN"; - // Validate ownership before attempting removal - if (process_info && g_strcmp0(process_info->sender_id, rdkv_req_caller_id) != 0) { - SWLOG_ERROR("[UNREGISTER] Access denied: Handler %"G_GUINT64_FORMAT" (process: %s) owned by '%s', but '%s' attempted to unregister\n", - handler, process_name, process_info->sender_id, rdkv_req_caller_id); - g_dbus_method_invocation_return_error(resp_ctx, - G_DBUS_ERROR, G_DBUS_ERROR_ACCESS_DENIED, - "Unregistration denied: Handler owned by different client"); - return; - } + // NOTE: Sender-ID ownership check is intentionally skipped for UnregisterProcess. + // The single-thread library model creates a new D-Bus connection per API call, + // resulting in different sender IDs for Register vs Unregister. + // The handler_id itself serves as the authorization token. - // Remove from tracking system (this will double-check ownership) + // Remove from tracking system by handler_id only SWLOG_INFO("[UNREGISTER] Attempting to remove process '%s' from tracking...\n", process_name); - if (remove_process_from_tracking(handler, rdkv_req_caller_id)) { + if (process_info && g_hash_table_remove(registered_processes, GINT_TO_POINTER(handler))) { SWLOG_INFO("[UNREGISTER] SUCCESS: Process '%s' unregistered successfully!\n", process_name); SWLOG_INFO("[UNREGISTER] - Removed Handler ID: %"G_GUINT64_FORMAT" (process: %s)\n", handler, process_name); SWLOG_INFO("[UNREGISTER] - Remaining registered processes: %d\n", g_hash_table_size(registered_processes)); From 6159133406a597228c5d5c4b7f0323bb0f255ebf Mon Sep 17 00:00:00 2001 From: mkadinti Date: Tue, 28 Apr 2026 06:01:07 +0000 Subject: [PATCH 10/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- UnregisterProcess --- src/dbus/rdkv_dbus_server.c | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/dbus/rdkv_dbus_server.c b/src/dbus/rdkv_dbus_server.c index dc463f8a..470aec36 100644 --- a/src/dbus/rdkv_dbus_server.c +++ b/src/dbus/rdkv_dbus_server.c @@ -399,27 +399,20 @@ static guint64 add_process_to_tracking(const gchar *process_name, * @brief Remove a process from the tracking system. * * Called when client invokes UnregisterProcess. Frees associated ProcessInfo. - * Validates that the requesting client is the owner of the handler_id. * * @param handler_id Handler ID to remove - * @param sender_id D-Bus sender ID of the requesting client - * @return TRUE if found and removed, FALSE if not found or access denied + * @param sender_id D-Bus sender ID of the requesting client (unused - kept for API compatibility) + * @return TRUE if found and removed, FALSE if not found */ static gboolean remove_process_from_tracking(guint64 handler_id, const gchar *sender_id) { + (void)sender_id; // Not used - single-thread library model creates new D-Bus connection per API call ProcessInfo *info = g_hash_table_lookup(registered_processes, GINT_TO_POINTER(handler_id)); if (!info) { SWLOG_INFO("[PROCESS_TRACKING] Handler %"G_GUINT64_FORMAT" not found\n", handler_id); return FALSE; } - // SECURITY: Validate that the requesting client owns this handler_id - if (g_strcmp0(info->sender_id, sender_id) != 0) { - SWLOG_ERROR("[PROCESS_TRACKING] Access denied: Handler %"G_GUINT64_FORMAT" owned by '%s', but '%s' attempted to unregister\n", - handler_id, info->sender_id, sender_id); - return FALSE; - } - SWLOG_INFO("[PROCESS_TRACKING] Removing: %s (handler: %"G_GUINT64_FORMAT")\n", info->process_name, handler_id); g_hash_table_remove(registered_processes, GINT_TO_POINTER(handler_id)); SWLOG_INFO("[PROCESS_TRACKING] Total registered: %d\n", g_hash_table_size(registered_processes)); @@ -1896,18 +1889,13 @@ static void process_app_request(GDBusConnection *rdkv_conn_dbus, return; } - // Look up process info to get process name for logging + // Look up process info to validate ownership and get process name for logging ProcessInfo *process_info = g_hash_table_lookup(registered_processes, GINT_TO_POINTER(handler)); const gchar *process_name = process_info ? process_info->process_name : "UNKNOWN"; - // NOTE: Sender-ID ownership check is intentionally skipped for UnregisterProcess. - // The single-thread library model creates a new D-Bus connection per API call, - // resulting in different sender IDs for Register vs Unregister. - // The handler_id itself serves as the authorization token. - - // Remove from tracking system by handler_id only + // Remove from tracking system SWLOG_INFO("[UNREGISTER] Attempting to remove process '%s' from tracking...\n", process_name); - if (process_info && g_hash_table_remove(registered_processes, GINT_TO_POINTER(handler))) { + if (remove_process_from_tracking(handler, rdkv_req_caller_id)) { SWLOG_INFO("[UNREGISTER] SUCCESS: Process '%s' unregistered successfully!\n", process_name); SWLOG_INFO("[UNREGISTER] - Removed Handler ID: %"G_GUINT64_FORMAT" (process: %s)\n", handler, process_name); SWLOG_INFO("[UNREGISTER] - Remaining registered processes: %d\n", g_hash_table_size(registered_processes)); From c3fc395203b0587489c5c832057e65570e9cd87b Mon Sep 17 00:00:00 2001 From: mkadinti Date: Tue, 28 Apr 2026 07:29:47 +0000 Subject: [PATCH 11/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Design docs --- librdkFwupdateMgr/DESIGN_DOCUMENT.md | 1389 +++++++++++++++++++ librdkFwupdateMgr/DOCUMENT_v1.md | 998 +++++++++++++ librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md | 1342 ++++++++++++++++++ 3 files changed, 3729 insertions(+) create mode 100755 librdkFwupdateMgr/DESIGN_DOCUMENT.md create mode 100755 librdkFwupdateMgr/DOCUMENT_v1.md create mode 100755 librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md diff --git a/librdkFwupdateMgr/DESIGN_DOCUMENT.md b/librdkFwupdateMgr/DESIGN_DOCUMENT.md new file mode 100755 index 00000000..6fc140a4 --- /dev/null +++ b/librdkFwupdateMgr/DESIGN_DOCUMENT.md @@ -0,0 +1,1389 @@ +# librdkFwupdateMgr — Engineering Design Document + +> **Document Version**: 2.0 +> **Date**: April 28, 2026 +> **Classification**: Internal Engineering — Pull Request Review +> **Repository**: `orig_rdkfwupdater` +> **Component**: `librdkFwupdateMgr` (shared library) +> **Author**: Senior Architect — rdkFwupdateMgr Team +> **Reviewers**: Firmware Platform Team, Security Review Board + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Why This Library Exists](#2-why-this-library-exists) +3. [Why Clients Must Not Directly Talk to the Daemon](#3-why-clients-must-not-directly-talk-to-the-daemon) +4. [Shared Library Responsibilities](#4-shared-library-responsibilities) +5. [Daemon Responsibilities](#5-daemon-responsibilities) +6. [High-Level Request Lifecycle](#6-high-level-request-lifecycle) +7. [Detailed Public API Reference](#7-detailed-public-api-reference) + - 7.1 [registerProcess()](#71-registerprocess) + - 7.2 [checkForUpdate()](#72-checkforupdate) + - 7.3 [downloadFirmware()](#73-downloadfirmware) + - 7.4 [updateFirmware()](#74-updatefirmware) + - 7.5 [unregisterProcess()](#75-unregisterprocess) +8. [Internal Helper Modules](#8-internal-helper-modules) +9. [IPC Communication Model](#9-ipc-communication-model) +10. [Retry and Timeout Strategy](#10-retry-and-timeout-strategy) +11. [Failure Scenarios and Recovery](#11-failure-scenarios-and-recovery) +12. [Logging Architecture](#12-logging-architecture) +13. [Security Considerations](#13-security-considerations) +14. [Performance Considerations](#14-performance-considerations) +15. [Scalability for Multiple Clients](#15-scalability-for-multiple-clients) +16. [Future Extensibility](#16-future-extensibility) + +--- + +## 1. Executive Summary + +`librdkFwupdateMgr` is a C shared library (`librdkFwupdateMgr.so`) that provides client applications on RDK-based embedded devices a clean, stable API for firmware lifecycle management. The library acts as a thin, intelligent client-side proxy that communicates with the `rdkFwupdateMgr` daemon over D-Bus (system bus IPC). + +### What Problem Does It Solve? + +Firmware updates on embedded devices require coordination between: +- Cloud infrastructure (XConf configuration server) +- Local device services (download managers, flash subsystems, reboot coordinators) +- Multiple client applications that may each need to initiate or observe firmware updates + +Without this library, every client application would need to: +- Know D-Bus method names, object paths, interface names, and GVariant type signatures +- Manage D-Bus connections, proxies, and signal subscriptions +- Handle threading for asynchronous signal reception +- Implement timeout and retry logic +- Parse raw D-Bus data into usable structures + +This library eliminates all of that complexity. A client app includes one header, links one library, calls five functions, and implements three callback signatures. + +### Key Design Characteristics + +| Characteristic | Decision | Rationale | +|---------------|----------|-----------| +| IPC mechanism | D-Bus (system bus) | Standard Linux IPC, D-Bus policy enforcement, well-understood | +| Threading model | Single background thread + per-call ephemeral connections | Simple, minimal resource usage | +| API style | Async fire-and-forget with callbacks | Non-blocking; suitable for event-driven and threaded apps | +| Memory model | Library owns handle; client owns callback data copies | Clear ownership boundaries | +| Connection model | Stateless (new D-Bus connection per API call) | No connection lifecycle management needed | +| Callback delivery | Background thread invocation | Deterministic delivery; client uses condvar to synchronize | + +--- + +## 2. Why This Library Exists + +### 2.1 The Fundamental Problem + +The `rdkFwupdateMgr` daemon manages all firmware operations on the device: checking for updates, downloading firmware images, flashing them to storage, and coordinating reboots. Multiple client applications need to interact with this daemon: + +- **example_plugin** — A reference one-shot firmware updater +- **TR-069/TR-181 agents** — Remote management protocols that trigger updates +- **WebUI services** — User-facing interfaces showing update status +- **Monitoring daemons** — Health-check services that poll firmware state + +Each of these would need to implement identical D-Bus client logic if they talked to the daemon directly. This is the classic "N-clients × M-operations" maintenance problem. + +### 2.2 What the Library Provides + +``` +WITHOUT library: WITH library: +───────────────── ───────────────── +Client A: 200 lines D-Bus code Client A: 30 lines using 5 API calls +Client B: 200 lines D-Bus code Client B: 30 lines using 5 API calls +Client C: 200 lines D-Bus code Client C: 30 lines using 5 API calls + Library: 1500 lines (maintained once) +``` + +### 2.3 Design Goals + +1. **Simplicity** — Five public functions. Three callback types. One header to include. +2. **Correctness** — Thread-safe, leak-free, handles all error paths. +3. **Stability** — Public API (header) changes require version bumps. Internal implementation can change freely. +4. **Observability** — Structured logging with module separation (`[FWUPMGR]`). +5. **Portability** — Works with or without RDK_LOGGER. Falls back to `fprintf` for unit testing. + +--- + +## 3. Why Clients Must Not Directly Talk to the Daemon + +This section documents the engineering rationale for mandating library usage rather than allowing direct D-Bus calls. + +### 3.1 Protocol Encapsulation + +The D-Bus interface between client and daemon is an **internal protocol**, not a public contract: + +| Aspect | Risk of Direct D-Bus Access | +|--------|---------------------------| +| Method signatures | `(ss) → (t)`, `(ssss)`, `(sssss)` — one typo = crash or silent failure | +| Signal signatures | `(tiissss)`, `(tsuss)`, `(tsiis)` — must parse correctly or lose data | +| Signal subscription setup | Must happen BEFORE method call, or response signal is lost | +| Update details format | Pipe-separated `Key:Value` string — undocumented, may change | +| Handler ID encoding | `uint64` on wire but `char*` in API — format details shouldn't leak | + +If the daemon team changes a signal signature (e.g., adds a field), only the library needs updating — not every client. + +### 3.2 Threading Complexity + +Receiving D-Bus signals requires: +- A dedicated GLib event loop running in a thread +- Signal subscription with correct object path and interface filters +- Proper GMainContext isolation (so the app's own GLib loop isn't disrupted) +- Mutex-protected callback dispatch with deadlock prevention + +No client developer should reimplement this. It's error-prone and already solved in the library. + +### 3.3 Connection Lifecycle + +The daemon identifies clients by `handler_id`, not by D-Bus sender address. This is because our library's stateless model creates a **new D-Bus connection per API call**, meaning each call gets a different sender ID (`:1.140`, `:1.141`, `:1.145`, etc.). + +A naive client attempting direct D-Bus calls would likely assume sender-ID stability — leading to authorization failures at the daemon's process tracking layer. + +### 3.4 Forward Compatibility + +The library provides a stable ABI boundary: +- Daemon protocol changes → library absorbs them internally +- New capabilities (e.g., cancel, pause) → added as new library functions +- Client code recompiles against same header, links same `.so` name + +--- + +## 4. Shared Library Responsibilities + +The library (`librdkFwupdateMgr.so`) is responsible for the following and **only** the following: + +| # | Responsibility | Implementation | +|---|---------------|----------------| +| 1 | **Input validation** | NULL checks, empty string checks, length limits on all public API parameters | +| 2 | **D-Bus transport** | Create connection, build GVariant payloads, send method calls, handle D-Bus errors | +| 3 | **Async signal reception** | Background thread with GLib event loop subscribed to daemon signals | +| 4 | **Callback management** | Three registries (check, download, update) with mutex protection | +| 5 | **Data transformation** | Convert daemon's wire format (GVariant) into typed C structs (`FwInfoData`, progress values) | +| 6 | **Handle lifecycle** | Allocate on register, validate on use, free on unregister | +| 7 | **Structured logging** | All operations logged under `LOG.RDK.FWUPMGR` module | +| 8 | **Resource cleanup** | Thread join, mutex destroy, memory free on deinit — no leaks | + +### What the Library Does NOT Do + +- Does NOT perform actual firmware downloads (daemon does this) +- Does NOT interact with XConf servers (daemon does this) +- Does NOT flash firmware (daemon delegates to HAL) +- Does NOT manage reboots (daemon handles this) +- Does NOT persist state across process restarts +- Does NOT retry failed operations (client's responsibility) +- Does NOT own the logging lifecycle (`log_init`/`log_exit` are caller's job) + +--- + +## 5. Daemon Responsibilities + +For context, here is what the `rdkFwupdateMgr` daemon does (the other side of the D-Bus): + +| # | Responsibility | Details | +|---|---------------|---------| +| 1 | **Process registration** | Assign unique handler_id, track client metadata, enforce one-registration-per-process-name | +| 2 | **XConf query** | HTTP request to XConf server with device model, current firmware version, MAC address | +| 3 | **Firmware download** | HTTP/HTTPS download from CDN, chunked transfer, integrity verification | +| 4 | **Flash coordination** | Write firmware to appropriate storage partition via device HAL | +| 5 | **Progress reporting** | Emit D-Bus signals with percentage and status as operations progress | +| 6 | **Multi-client orchestration** | Coalesce duplicate download requests (piggybacking), serialize flash operations | +| 7 | **Reboot management** | Coordinate post-flash reboot timing based on client's `rebootImmediately` flag | +| 8 | **Cleanup on client disconnect** | Handle ungraceful client exits (D-Bus name owner watching) | + +--- + +## 6. High-Level Request Lifecycle + +Every firmware update workflow follows this lifecycle: + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ CLIENT APPLICATION │ +│ │ +│ ┌──────────┐ ┌──────────────┐ ┌────────────┐ ┌──────┐│ +│ │ REGISTER │────►│ CHECK UPDATE │────►│ DOWNLOAD │────►│FLASH ││ +│ └──────────┘ └──────────────┘ └────────────┘ └──────┘│ +│ │ │ │ │ │ +│ │ callback fires callback fires callback │ +│ │ (once, with (many times, fires │ +│ │ firmware info) with progress) (many) │ +│ │ │ +│ ┌────────────┐ │ +│ │ UNREGISTER │◄───────────────────── (always, even on error) ────┘│ +│ └────────────┘ │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Lifecycle States + +``` + ┌────────────┐ + │ UNLINKED │ Library loaded but not registered + └─────┬──────┘ + │ registerProcess() succeeds + ▼ + ┌────────────┐ + │ REGISTERED │ Handle valid, background thread running, ready for API calls + └─────┬──────┘ + │ checkForUpdate() / downloadFirmware() / updateFirmware() + ▼ + ┌────────────┐ + │ ACTIVE │ One or more async operations pending + └─────┬──────┘ + │ All callbacks have fired (completed/errored) + ▼ + ┌────────────┐ + │ REGISTERED │ Back to idle, can call APIs again + └─────┬──────┘ + │ unregisterProcess() + ▼ + ┌────────────┐ + │ UNLINKED │ Handle freed, thread stopped, library dormant + └────────────┘ +``` + +### Complete Sequence (Normal Path) + +``` +Time Client App librdkFwupdateMgr Daemon +───── ────────── ───────────────── ────── +T+0s registerProcess() + ├─────────────────── D-Bus: RegisterProcess ──────────► assigns handler_id + │◄─────────────────── returns handler_id ──────────────┘ + ├─ internal_system_init() + │ └─ spawns background thread + │ ├─ subscribes to 3 signals + │ └─ enters g_main_loop_run() + └─ returns handle "12345" + +T+1s checkForUpdate(handle, cb) + ├─ validates inputs + ├─ registers cb in registry + ├────────────── D-Bus: CheckForUpdate ──────────────────► queries XConf + └─ returns SUCCESS immediately + +T+15s XConf response arrives + ◄── CheckForUpdateComplete ────┤ + dispatches cb(&fwinfo) + └─► cb runs in BG thread + +T+16s downloadFirmware(handle, req, dl_cb) + ├─ validates inputs + ├─ registers dl_cb in dwnl registry + ├────────── D-Bus: DownloadFirmware ────────────────────► starts download + └─ returns SUCCESS + +T+20s download 25% + ◄── DownloadProgress(25%) ─────┤ + dispatches dl_cb(25, IN_PROGRESS) +T+40s download 100% + ◄── DownloadProgress(100%) ────┤ + dispatches dl_cb(100, COMPLETED) + resets slot to IDLE + +T+41s updateFirmware(handle, req, upd_cb) + ├─ validates inputs + ├─ registers upd_cb in update registry + ├─────────── D-Bus: UpdateFirmware ─────────────────────► starts flash + └─ returns SUCCESS + +T+90s flash 100% + ◄── UpdateProgress(100%) ──────┤ + dispatches upd_cb(100, COMPLETED) + +T+91s unregisterProcess(handle) + ├─ internal_system_deinit() + │ ├─ g_main_loop_quit() + │ ├─ pthread_join() + │ └─ free registries + mutexes + ├────────── D-Bus: UnregisterProcess ───────────────────► removes ProcessInfo + └─ free(handle) +``` + +--- + +## 7. Detailed Public API Reference + +### 7.1 `registerProcess()` + +```c +FirmwareInterfaceHandle registerProcess(const char *processName, const char *libVersion); +``` + +#### Purpose + +Establishes a session with the firmware daemon. This is the mandatory first call before any other API can be used. It tells the daemon "I exist, here's my name, give me a session token." + +#### Inputs + +| Parameter | Type | Constraints | Example | +|-----------|------|-------------|---------| +| `processName` | `const char*` | Non-NULL, non-empty, ≤256 chars | `"VideoPlayer"` | +| `libVersion` | `const char*` | Non-NULL, ≤64 chars (empty OK) | `"1.0.0"` | + +#### Output + +| Return | Meaning | +|--------|---------| +| Non-NULL string (e.g., `"12345"`) | Success — this is your session handle | +| `NULL` | Failure — daemon not running, D-Bus error, or validation failure | + +#### Return Code Semantics + +This function doesn't use an enum return code — it returns the handle directly or `NULL`. + +#### Internal Steps (What Happens Inside) + +``` +Step 1: Validate processName + ├─ NULL check → FWUPMGR_ERROR, return NULL + ├─ Empty check → FWUPMGR_ERROR, return NULL + └─ Length check (>256) → FWUPMGR_ERROR, return NULL + +Step 2: Validate libVersion + ├─ NULL check → FWUPMGR_ERROR, return NULL + └─ Length check (>64) → FWUPMGR_ERROR, return NULL + +Step 3: Create D-Bus proxy + ├─ g_bus_get_sync(G_BUS_TYPE_SYSTEM) → GDBusConnection + │ └─ Failure: log error, return NULL + ├─ g_dbus_proxy_new_sync() → GDBusProxy + │ └─ Failure: log error, unref connection, return NULL + └─ Unref connection (proxy holds its own reference) + +Step 4: Call RegisterProcess D-Bus method + ├─ g_dbus_proxy_call_sync("RegisterProcess", (ss), timeout=5000ms) + │ └─ Failure: log D-Bus error message, unref proxy, return NULL + ├─ Extract handler_id (uint64) from reply GVariant (t) + └─ Unref result + proxy + +Step 5: Allocate handle string + ├─ malloc(32) → buffer for decimal string + │ └─ Failure: CRITICAL — registration succeeded but can't return handle + │ ├─ Create cleanup proxy + │ ├─ Call UnregisterProcess(handler_id) to undo daemon-side registration + │ └─ Return NULL + └─ snprintf(buffer, 32, "%" PRIu64, handler_id) + +Step 6: Start async engine + ├─ internal_system_init() + │ ├─ Initialize CallbackRegistry (mutex + zero array) + │ ├─ Initialize DwnlCallbackRegistry (mutex + zero array) + │ ├─ Initialize UpdateCbRegistry (mutex + zero array) + │ ├─ Create GMainContext (isolated from app's GLib) + │ ├─ Create GMainLoop + │ ├─ pthread_create(background_thread_func) + │ └─ Spin-wait (max 5s) until bg thread sets running=true + └─ Return handle string +``` + +#### Error Handling + +| Error Condition | Action | User-Visible Effect | +|----------------|--------|---------------------| +| NULL/empty processName | Log error, return NULL immediately | No D-Bus call made | +| D-Bus system bus unavailable | Log connection error, return NULL | Daemon may not be installed | +| Daemon not responding (timeout) | Log timeout, return NULL | Daemon may be crashed/overloaded | +| Daemon rejects registration | Log D-Bus error message, return NULL | Process name conflict or internal error | +| malloc failure after success | Best-effort UnregisterProcess, return NULL | Extremely rare (OOM condition) | +| Background thread fails to start | Log error, return NULL | System resource exhaustion | + +#### Thread Safety + +- Fully thread-safe for concurrent calls (different process names) +- GDBus synchronous calls are internally thread-safe +- No shared state until `internal_system_init()` creates registries (which are mutex-protected) + +#### Memory Ownership + +| Who | Owns What | +|-----|-----------| +| Library | The returned handle string (malloc'd) | +| Caller | NOTHING — do not free the handle; call `unregisterProcess()` instead | + +#### Typical Caller Usage + +```c +#include "rdkFwupdateMgr_client.h" + +int main(void) { + FirmwareInterfaceHandle handle = registerProcess("MyPlugin", "2.1.0"); + if (handle == NULL) { + fprintf(stderr, "Failed to register with daemon. Is it running?\n"); + return EXIT_FAILURE; + } + + printf("Registered! Handle: %s\n", handle); + + // ... use other APIs with this handle ... + + unregisterProcess(handle); // MUST call this before exit + return EXIT_SUCCESS; +} +``` + +--- + +### 7.2 `checkForUpdate()` + +```c +CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, UpdateEventCallback callback); +``` + +#### Purpose + +Initiates a non-blocking firmware availability check. The daemon queries the XConf cloud server to determine if a newer firmware version exists for this device. The result is delivered asynchronously via your callback — this function returns immediately. + +#### Inputs + +| Parameter | Type | Constraints | Example | +|-----------|------|-------------|---------| +| `handle` | `FirmwareInterfaceHandle` | Non-NULL, non-empty, from `registerProcess()` | `"12345"` | +| `callback` | `UpdateEventCallback` | Non-NULL function pointer | `my_check_callback` | + +#### Callback Signature + +```c +typedef void (*UpdateEventCallback)(const FwInfoData *fwinfodata); +``` + +The callback receives: +```c +typedef struct { + char CurrFWVersion[64]; // Current firmware version on device + UpdateDetails *UpdateDetails; // Non-NULL only when status == FIRMWARE_AVAILABLE + CheckForUpdateStatus status; // Result enum +} FwInfoData; + +typedef struct { + char FwFileName[128]; // e.g., "firmware_v2.0.bin" + char FwUrl[512]; // Download URL + char FwVersion[64]; // Available firmware version + char RebootImmediately[12]; // "true" or "false" + char DelayDownload[8]; // "true" or "false" + char PDRIVersion[64]; // PDRI image version (may be empty) + char PeripheralFirmwares[256]; // Peripheral versions (may be empty) +} UpdateDetails; +``` + +#### Output + +| Return Value | Meaning | +|-------------|---------| +| `CHECK_FOR_UPDATE_SUCCESS` (0) | Request sent to daemon. Callback will fire later. | +| `CHECK_FOR_UPDATE_FAIL` (1) | Request could not be sent. No callback will fire. | + +**Critical**: `SUCCESS` does NOT mean firmware is available. It means the request was accepted. Actual firmware availability comes through the callback. + +#### Internal Steps + +``` +Step 1: Validate handle (not NULL, not empty) → FAIL on error +Step 2: Validate callback (not NULL) → FAIL on error +Step 3: Connect to D-Bus system bus + └─ Failure: return FAIL (no stale registry entry created) +Step 4: Register callback in CallbackRegistry (slot: IDLE → PENDING) + └─ Failure (registry full, 30 slots): unref connection, return FAIL +Step 5: Fire-and-forget g_dbus_connection_call("CheckForUpdate", (s)handle) + └─ No reply expected — returns immediately +Step 6: Unref D-Bus connection +Step 7: Return CHECK_FOR_UPDATE_SUCCESS +``` + +#### Error Handling + +| Error | Action | Callback Fires? | +|-------|--------|----------------| +| Invalid handle | Return FAIL immediately | No | +| NULL callback | Return FAIL immediately | No | +| D-Bus connection failure | Return FAIL, no registry entry | No | +| Registry full (30 slots) | Return FAIL, cleanup connection | No | +| Daemon crashes after call sent | Callback never fires (client should timeout) | No — client uses condvar timeout | + +#### Thread Safety + +- Safe to call from multiple threads concurrently (registry mutex protects slot allocation) +- Same handle can have only one pending check at a time (existing slot is overwritten) +- Callback fires in the library's background thread, NOT the caller's thread + +#### Memory Ownership + +| Data | Lifetime | Owner | +|------|----------|-------| +| `FwInfoData*` passed to callback | Valid ONLY during callback execution | Library (stack-allocated in dispatch function) | +| `UpdateDetails*` inside FwInfoData | Valid ONLY during callback execution | Library (stack-allocated) | +| All strings in FwInfoData | Valid ONLY during callback execution | Library | + +**Rule**: If you need data after the callback returns, `strncpy()` it to your own buffers before the callback returns. + +#### Typical Caller Usage + +```c +static pthread_mutex_t check_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_cond_t check_cond = PTHREAD_COND_INITIALIZER; +static int check_done = 0; +static CheckForUpdateStatus check_result; + +void my_check_callback(const FwInfoData *fwinfo) { + // This runs in BACKGROUND THREAD — copy what you need, signal main thread + pthread_mutex_lock(&check_mutex); + check_result = fwinfo->status; + check_done = 1; + pthread_cond_signal(&check_cond); + pthread_mutex_unlock(&check_mutex); +} + +// In main thread: +CheckForUpdateResult rc = checkForUpdate(handle, my_check_callback); +if (rc != CHECK_FOR_UPDATE_SUCCESS) { + // Handle error — callback will NOT fire + return; +} + +// Wait for callback with 2-minute timeout +struct timespec timeout; +clock_gettime(CLOCK_REALTIME, &timeout); +timeout.tv_sec += 120; + +pthread_mutex_lock(&check_mutex); +while (!check_done) { + if (pthread_cond_timedwait(&check_cond, &check_mutex, &timeout) != 0) { + // Timeout — XConf query took too long + break; + } +} +pthread_mutex_unlock(&check_mutex); +``` + +--- + +### 7.3 `downloadFirmware()` + +```c +DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, + const FwDwnlReq *fwdwnlreq, + DownloadCallback callback); +``` + +#### Purpose + +Initiates a non-blocking firmware image download. The daemon downloads the specified firmware file from the CDN and reports progress through repeated callback invocations. Returns immediately. + +#### Inputs + +| Parameter | Type | Constraints | +|-----------|------|-------------| +| `handle` | `FirmwareInterfaceHandle` | Non-NULL, non-empty | +| `fwdwnlreq` | `const FwDwnlReq*` | Non-NULL; `firmwareName` must be non-NULL and non-empty | +| `callback` | `DownloadCallback` | Non-NULL | + +```c +typedef struct { + const char *firmwareName; // REQUIRED: "firmware_v2.bin" + const char *downloadUrl; // OPTIONAL: NULL or "" → daemon uses XConf URL + const char *TypeOfFirmware; // OPTIONAL: "PCI", "PDRI", or "PERIPHERAL" +} FwDwnlReq; +``` + +#### Callback Signature + +```c +typedef void (*DownloadCallback)(int download_progress, DownloadStatus fwdwnlstatus); +``` + +Called **multiple times**: +- `(10, DWNL_IN_PROGRESS)` — 10% done +- `(50, DWNL_IN_PROGRESS)` — halfway +- `(100, DWNL_COMPLETED)` — finished successfully +- OR `(X, DWNL_ERROR)` — failed at X% + +#### Output + +| Return Value | Meaning | +|-------------|---------| +| `RDKFW_DWNL_SUCCESS` (0) | Download request sent. Callbacks will fire. | +| `RDKFW_DWNL_FAILED` (1) | Could not send request. No callbacks will fire. | + +#### Internal Steps + +``` +Step 1: Validate handle, fwdwnlreq, fwdwnlreq->firmwareName, callback +Step 2: Connect to D-Bus system bus +Step 3: Register callback in DwnlCallbackRegistry (slot: IDLE → ACTIVE) +Step 4: Fire-and-forget: DownloadFirmware(s handle, s firmwareName, s url, s type) + ├─ url defaults to "" if NULL + └─ type defaults to "" if NULL +Step 5: Unref connection, return SUCCESS +``` + +#### Key Difference from checkForUpdate() + +| Aspect | checkForUpdate | downloadFirmware | +|--------|---------------|-----------------| +| Callback fires | Once | Multiple times (every progress signal) | +| Registry slot lifecycle | PENDING → DISPATCHED → IDLE | ACTIVE → ACTIVE → ... → IDLE | +| Slot reset trigger | After single dispatch | Only on DWNL_COMPLETED or DWNL_ERROR | + +#### Thread Safety + +- Safe for concurrent calls (own registry with own mutex) +- Same handle calling `downloadFirmware()` twice overwrites the previous slot (prevents stale callbacks) + +#### Typical Caller Usage + +```c +void my_download_cb(int progress, DownloadStatus status) { + printf("Download: %d%% [%s]\n", progress, + status == DWNL_COMPLETED ? "DONE" : + status == DWNL_ERROR ? "ERROR" : "IN_PROGRESS"); + + if (status == DWNL_COMPLETED || status == DWNL_ERROR) { + // Signal main thread — download finished + pthread_mutex_lock(&dl_mutex); + dl_done = 1; + dl_status = status; + pthread_cond_signal(&dl_cond); + pthread_mutex_unlock(&dl_mutex); + } +} + +FwDwnlReq req = { + .firmwareName = "firmware_v2.bin", + .downloadUrl = NULL, // Let daemon use XConf URL + .TypeOfFirmware = "PCI" +}; + +DownloadResult rc = downloadFirmware(handle, &req, my_download_cb); +``` + +--- + +### 7.4 `updateFirmware()` + +```c +UpdateResult updateFirmware(FirmwareInterfaceHandle handle, + const FwUpdateReq *fwupdatereq, + UpdateCallback callback); +``` + +#### Purpose + +Initiates non-blocking firmware flashing. The daemon writes the previously downloaded firmware image to the device's storage partition. Progress is reported through repeated callback invocations. + +**WARNING**: This operation modifies device firmware. It is irreversible once the flash begins. + +#### Inputs + +| Parameter | Type | Constraints | +|-----------|------|-------------| +| `handle` | `FirmwareInterfaceHandle` | Non-NULL, non-empty | +| `fwupdatereq` | `const FwUpdateReq*` | Non-NULL; `firmwareName` and `TypeOfFirmware` required | +| `callback` | `UpdateCallback` | Non-NULL | + +```c +typedef struct { + const char *firmwareName; // REQUIRED: must match downloaded file + const char *TypeOfFirmware; // REQUIRED: "PCI", "PDRI", or "PERIPHERAL" + const char *LocationOfFirmware; // OPTIONAL: NULL → use /etc/device.properties default + bool rebootImmediately; // true → device reboots when flash completes +} FwUpdateReq; +``` + +#### Callback Signature + +```c +typedef void (*UpdateCallback)(int update_progress, UpdateStatus fwupdatestatus); +``` + +#### Output + +| Return Value | Meaning | +|-------------|---------| +| `RDKFW_UPDATE_SUCCESS` (0) | Flash request sent. Callbacks will fire. | +| `RDKFW_UPDATE_FAILED` (1) | Could not send request. No callbacks. | + +#### D-Bus Wire Format + +The library converts the `FwUpdateReq` struct to a D-Bus method call with signature `(sssss)`: +``` +s handle — "12345" +s firmwareName — "firmware_v2.bin" +s LocationOfFirmware — "/opt/CDL" (or "" if NULL) +s TypeOfFirmware — "PCI" +s rebootImmediately — "true" or "false" (string, not bool!) +``` + +Note: `rebootImmediately` is a `bool` in the struct but transmitted as a string because the daemon's D-Bus method expects string arguments. + +#### Thread Safety + +Same as `downloadFirmware()` — own registry, own mutex, safe for concurrent use. + +--- + +### 7.5 `unregisterProcess()` + +```c +void unregisterProcess(FirmwareInterfaceHandle handler); +``` + +#### Purpose + +Terminates the session with the daemon, stops the background thread, frees all library resources, and frees the handle memory. This is the mandatory last call. + +#### Inputs + +| Parameter | Type | Constraints | +|-----------|------|-------------| +| `handler` | `FirmwareInterfaceHandle` | May be NULL (no-op) | + +#### Output + +Returns `void`. This function always succeeds from the caller's perspective (best-effort cleanup). + +#### Internal Steps + +``` +Step 1: NULL check → if NULL, log info and return (no-op, safe) + +Step 2: Parse handle string → uint64 handler_id + ├─ Uses strtoull() with strict endptr validation + ├─ Rejects: "123abc", " 123", "", "abc", overflow + └─ On invalid: FWUPMGR_ERROR, free(handler), return + +Step 3: internal_system_deinit() + ├─ g_main_loop_quit() → background thread wakes up from g_main_loop_run() + ├─ pthread_join() → wait for background thread to exit cleanly + ├─ g_main_loop_unref() + g_main_context_unref() + ├─ internal_dwnl_system_deinit() → free download registry + ├─ internal_update_system_deinit() → free update registry + ├─ Free all remaining handle_key strings in check registry + └─ pthread_mutex_destroy() × 4 (bg_thread, registry, dwnl, update) + +Step 4: Create D-Bus proxy (best-effort — may fail if daemon is already gone) + +Step 5: Call UnregisterProcess(t handler_id) on daemon + ├─ Success: daemon removes ProcessInfo + └─ Failure: logged but ignored (daemon may have crashed/restarted) + +Step 6: free(handler) — the string is released regardless of D-Bus call outcome +``` + +#### Error Handling + +This function is **deliberately tolerant of errors**: + +| Error | Action | Cleanup Continues? | +|-------|--------|-------------------| +| NULL handle | No-op, return | N/A | +| Invalid handle string | Log error, free handle, return | Yes | +| D-Bus proxy creation fails | Log warning, continue | Yes — free(handle) still happens | +| UnregisterProcess D-Bus call fails | Log warning, continue | Yes — free(handle) still happens | +| Daemon already crashed | D-Bus call times out, logged | Yes — local cleanup still happens | + +#### Why `internal_system_deinit()` Before D-Bus Call? + +1. After unregistering, daemon stops sending signals → background thread is useless +2. `pthread_join()` returns immediately since `g_main_loop_quit()` unblocks the thread +3. If we sent D-Bus first and the daemon is slow, the background thread would sit idle waiting for signals that will never come + +#### Memory Ownership + +After `unregisterProcess()` returns: +- The handle pointer is **freed and invalid** — do not use it +- All background resources are released +- Library is back to "unlinked" state — `registerProcess()` can be called again if needed + +#### Typical Caller Usage + +```c +// Always call before exit, even on error paths +cleanup: + if (handle != NULL) { + unregisterProcess(handle); + handle = NULL; // Defensive: mark as invalid + } + return exit_code; +``` + +--- + +## 8. Internal Helper Modules + +### 8.1 Module: `rdkFwupdateMgr_async.c` — The Async Engine + +This is the core internal module. It owns: + +| Component | Purpose | +|-----------|---------| +| `g_registry` (CallbackRegistry) | Stores pending `checkForUpdate` callbacks | +| `g_dwnl_registry` (DwnlCallbackRegistry) | Stores active download callbacks | +| `g_update_registry` (UpdateCbRegistry) | Stores active update callbacks | +| `g_bg_thread` (BackgroundThread) | Holds thread handle, GMainLoop, connection, subscription IDs | + +#### Key Internal Functions + +| Function | Called By | Purpose | +|----------|-----------|---------| +| `internal_system_init()` | `registerProcess()` | Start everything: registries + thread | +| `internal_system_deinit()` | `unregisterProcess()` | Stop everything: thread + registries | +| `internal_register_callback()` | `checkForUpdate()` | Add callback to check registry | +| `internal_dwnl_register_callback()` | `downloadFirmware()` | Add callback to download registry | +| `internal_update_register_callback()` | `updateFirmware()` | Add callback to update registry | +| `background_thread_func()` | `pthread_create()` | Thread entry point: connect + subscribe + loop | +| `on_check_complete_signal()` | GLib (signal dispatch) | Parse signal → dispatch callbacks | +| `on_download_progress_signal()` | GLib (signal dispatch) | Parse signal → dispatch callbacks | +| `on_update_progress_signal()` | GLib (signal dispatch) | Parse signal → dispatch callbacks | +| `dispatch_all_pending()` | `on_check_complete_signal()` | Two-phase dispatch for check callbacks | +| `dispatch_all_dwnl_active()` | `on_download_progress_signal()` | Two-phase dispatch for download callbacks | +| `dispatch_all_update_active()` | `on_update_progress_signal()` | Two-phase dispatch for update callbacks | +| `parse_update_details()` | `dispatch_all_pending()` | Parse pipe-separated firmware details string | + +### 8.2 Module: `rdkFwupdateMgr_process.c` — Registration Logic + +Contains `registerProcess()` and `unregisterProcess()` plus two helpers: + +| Function | Purpose | +|----------|---------| +| `create_dbus_proxy()` | Create a GDBusProxy connected to daemon (used by both register and unregister) | +| `validate_process_name()` | NULL, empty, and length checks | +| `validate_lib_version()` | NULL and length checks | + +### 8.3 Module: `rdkFwupdateMgr_log.h` — Logging Macros + +Header-only. Defines `FWUPMGR_*` macros. See [Section 12](#12-logging-architecture) for full details. + +### 8.4 Module: `rdkFwupdateMgr_async_internal.h` — Internal Types + +Header for internal use only. Defines: +- All `typedef struct` types for registries and signal data +- All `internal_*` function declarations +- D-Bus constants (`DBUS_SERVICE_NAME`, etc.) +- Architecture ASCII diagrams in comments + +--- + +## 9. IPC Communication Model + +### 9.1 Transport: D-Bus System Bus + +| Property | Value | +|----------|-------| +| Bus type | System bus (`G_BUS_TYPE_SYSTEM`) | +| Well-known name | `org.rdkfwupdater.Service` | +| Object path | `/org/rdkfwupdater/Service` | +| Interface | `org.rdkfwupdater.Interface` | + +### 9.2 Connection Pattern: Ephemeral Per-Call + +``` +registerProcess(): [Connect] → [Call] → [Disconnect] (unique sender :1.140) +checkForUpdate(): [Connect] → [Call] → [Disconnect] (unique sender :1.141) +downloadFirmware(): [Connect] → [Call] → [Disconnect] (unique sender :1.142) +updateFirmware(): [Connect] → [Call] → [Disconnect] (unique sender :1.143) +unregisterProcess(): [Connect] → [Call] → [Disconnect] (unique sender :1.145) +``` + +**Background thread**: Has its OWN **persistent** connection for signal subscriptions. This connection lives for the entire library lifecycle. + +### 9.3 Method Calls (Client → Daemon) + +| Method | GVariant Signature | Direction | Blocking? | +|--------|-------------------|-----------|-----------| +| `RegisterProcess` | IN: `(ss)` OUT: `(t)` | Synchronous | Yes (5s timeout) | +| `UnregisterProcess` | IN: `(t)` OUT: `(b)` | Synchronous | Yes (5s timeout) | +| `CheckForUpdate` | IN: `(s)` OUT: none | Fire-and-forget | No | +| `DownloadFirmware` | IN: `(ssss)` OUT: none | Fire-and-forget | No | +| `UpdateFirmware` | IN: `(sssss)` OUT: none | Fire-and-forget | No | + +### 9.4 Signals (Daemon → Client) + +| Signal | GVariant Signature | Delivery | +|--------|-------------------|----------| +| `CheckForUpdateComplete` | `(tiissss)` | Once per check | +| `DownloadProgress` | `(tsuss)` | Repeated (per progress %) | +| `UpdateProgress` | `(tsiis)` | Repeated (per progress %) | + +### 9.5 Why Fire-and-Forget for Async Operations? + +For `checkForUpdate`, `downloadFirmware`, and `updateFirmware`: +- The actual work takes seconds to minutes +- Blocking the caller for that duration defeats the purpose +- The daemon acknowledges receipt implicitly by starting work +- Results come as signals (broadcast notifications) +- If the call fails at D-Bus level, `g_dbus_connection_call()` still returns successfully (message queued) — the failure manifests as no signal ever arriving + +--- + +## 10. Retry and Timeout Strategy + +### 10.1 Library-Side Timeouts + +| Operation | Timeout | Location | Behavior on Timeout | +|-----------|---------|----------|---------------------| +| `RegisterProcess` D-Bus call | 5000ms | `DBUS_TIMEOUT_MS` constant | Returns NULL with error log | +| `UnregisterProcess` D-Bus call | 5000ms | Same constant | Logs warning, continues cleanup | +| Fire-and-forget calls | 5000ms | Same constant | GLib queues message; timeout only applies to queueing | +| Background thread startup | 5000ms | `internal_system_init()` spin-wait | Continues anyway (first API call will fail) | + +### 10.2 Library-Side Retry Policy + +**The library does NOT retry.** This is a deliberate design decision: + +- Retry logic belongs in the **caller**, not the transport layer +- The caller knows the right retry interval and max attempts for their use case +- A monitoring daemon might retry every 60 seconds; a user-facing app might retry once after 5 seconds +- Silent retries inside the library would hide failures from the caller + +### 10.3 Caller-Side Timeout Guidance + +| Operation | Recommended Timeout | Rationale | +|-----------|-------------------|-----------| +| `checkForUpdate` callback | 120 seconds (2 min) | XConf HTTP query + network latency | +| `downloadFirmware` callback | 300 seconds (5 min) | Large firmware images over cellular | +| `updateFirmware` callback | 600 seconds (10 min) | Flash operations vary by storage type | + +The `example_app.c` uses exactly these values with `pthread_cond_timedwait()`. + +### 10.4 Callback Timeout Detection (Internal) + +Each registry entry stores a `registered_time` timestamp. The async engine could use this for internal timeout detection (sweeping stale entries). Currently, timeout detection is NOT actively enforced — the `TIMED_OUT` state exists in the state machine but no sweeper thread runs. + +**Recommendation for future work**: Add a periodic sweep (every 60s) in the background thread via `g_timeout_add()` to reset stale PENDING/ACTIVE entries that have exceeded `CALLBACK_TIMEOUT_SECONDS` (60s). + +--- + +## 11. Failure Scenarios and Recovery + +### 11.1 Daemon Not Running + +| Symptom | Detection | Recovery | +|---------|-----------|----------| +| `registerProcess()` returns NULL | D-Bus error: "The name org.rdkfwupdater.Service was not provided by any .service files" | Client logs error, retries, or exits | +| Fire-and-forget calls appear to succeed | No signal ever arrives | Client's condvar times out | + +### 11.2 Daemon Crashes Mid-Operation + +| Symptom | Detection | Recovery | +|---------|-----------|----------| +| No more signals arrive | Client's condvar times out | Client calls `unregisterProcess()` (best-effort), then re-registers | +| Background thread's D-Bus connection emits "closed" signal | Not currently handled | **Future work**: detect and propagate error to pending callbacks | + +### 11.3 D-Bus System Bus Restart + +| Symptom | Detection | Recovery | +|---------|-----------|----------| +| All D-Bus connections become invalid | Next API call fails at `g_bus_get_sync()` | Client calls `unregisterProcess()`, waits, tries `registerProcess()` again | + +### 11.4 Client Crashes Without Unregistering + +| Symptom | Detection | Recovery | +|---------|-----------|----------| +| Daemon holds stale ProcessInfo | Daemon watches D-Bus name owner changes (NameOwnerChanged signal) | Daemon auto-removes registration when client's bus name disappears | + +**Note**: Due to the per-call connection model, this detection is unreliable — each API call has a different bus name that immediately disappears after the call. The daemon relies on explicit `UnregisterProcess` or periodic cleanup. + +### 11.5 Registry Full (30 Slots) + +| Symptom | Detection | Recovery | +|---------|-----------|----------| +| `checkForUpdate`/`downloadFirmware`/`updateFirmware` returns FAIL | `internal_*_register_callback()` returns false | Client should wait for pending operations to complete, then retry | + +### 11.6 Signal Arrives Before Callback Registered + +| Cause | Prevention | Impact | +|-------|-----------|--------| +| Daemon responds extremely fast | Library registers callback BEFORE sending D-Bus call | Cannot happen with current ordering | +| Connect → Register → Send ordering eliminates this race | N/A | N/A | + +--- + +## 12. Logging Architecture + +### 12.1 Three-Module Design + +``` +┌──────────────────────────────────────────────────┐ +│ LOG OUTPUT │ +│ /opt/logs/rdkFwupdateMgr.log │ +├──────────────────────────────────────────────────┤ +│ [EXAMPLE] App-level messages │ ← Client app +│ [FWUPMGR] Library internal messages │ ← This library +│ [FWUPG] Daemon operational messages │ ← Daemon +└──────────────────────────────────────────────────┘ +``` + +| RDK_LOGGER Module | Macro Prefix | Source Files | Purpose | +|------------------|--------------|--------------|---------| +| `LOG.RDK.FWUPMGR` | `FWUPMGR_*` | `_process.c`, `_api.c`, `_async.c` | Library internals | +| `LOG.RDK.FWUPG` | `SWLOG_*` | Daemon sources | Daemon operations | +| `LOG.RDK.EXAMPLE` | `EXAMPLE_*` | `example_app.c` | Example client app | + +### 12.2 Log Levels Used + +| Level | Macro | Usage | +|-------|-------|-------| +| TRACE | `FWUPMGR_TRACE` | Not used currently (reserved for future verbose tracing) | +| DEBUG | `FWUPMGR_DEBUG` | Detailed internal state (registry operations, GVariant parsing) | +| INFO | `FWUPMGR_INFO` | Normal operation flow (API entry/exit, handle values, signal reception) | +| WARN | `FWUPMGR_WARN` | Recoverable issues (D-Bus call failed in unregister, daemon already gone) | +| ERROR | `FWUPMGR_ERROR` | Failures that cause API to return error code (validation, connection, registry full) | +| FATAL | `FWUPMGR_FATAL` | Not used currently (reserved for unrecoverable states) | + +### 12.3 Log Lifecycle Ownership + +```c +// CALLER'S responsibility — library never calls these: +log_init(); // Before any library API call +// ... use library ... +log_exit(); // After unregisterProcess() +``` + +**Rationale**: A process should call `log_init()` exactly once. If both the library and the app call it, double-initialization could corrupt state. + +### 12.4 Macro Implementation + +```c +// rdkFwupdateMgr_log.h (when RDK_LOGGER is defined): +#define FWUPMGR_LOG(level, module, format, ...) \ + RDK_LOG(level, module, format, ##__VA_ARGS__) + +#define FWUPMGR_INFO(format, ...) \ + FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) + +// Client apps reuse the base macro with their own module: +#define MYAPP_INFO(format, ...) \ + FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.MYAPP", format, ##__VA_ARGS__) +``` + +### 12.5 Non-RDK_LOGGER Fallback + +For unit tests and development builds without RDK_LOGGER: +```c +#define FWUPMGR_LOG(level, module, FORMAT...) fprintf(stderr, "[%s] " FORMAT, module) +``` + +### 12.6 Build Dependencies + +```makefile +# In Makefile.am for any target that uses logging: +target_CFLAGS += -I${top_srcdir}/librdkFwupdateMgr/src # rdkFwupdateMgr_log.h +target_CFLAGS += -I${top_srcdir}/common_utilities/utils # rdkv_cdl_log_wrapper.h +target_LDADD += -lfwutils -lrdkloggers # Runtime implementations +``` + +--- + +## 13. Security Considerations + +### 13.1 D-Bus Policy Enforcement + +Access control is enforced at the D-Bus level via policy files: +```xml + + + + + +``` + +Only processes running as the configured user (typically root or a specific service account) can call methods or receive signals. + +### 13.2 Handler ID as Authorization Token + +The `handler_id` (e.g., `"12345"`) serves as the session authorization token: +- It's a daemon-generated 64-bit value (not guessable from outside) +- Required for all operations after registration +- The daemon validates that the handler_id exists before processing any request +- In this library model, sender-ID validation is not used (see Section 9.2 for why) + +**Limitation**: The handler_id is a sequential counter (not cryptographically random). In a multi-client environment, a malicious client could potentially guess another client's handler_id. For production hardening, consider using a random 128-bit token. + +### 13.3 Input Validation at Library Boundary + +All public API functions validate inputs before forwarding to D-Bus: +- NULL pointer checks on all parameters +- Empty string rejection +- Length limit enforcement (prevents buffer overflow in daemon's fixed-size buffers) +- Numeric parse validation in `unregisterProcess()` (strict `strtoull` with endptr check) + +### 13.4 No Credential Storage + +The library does not store, cache, or transmit any credentials. Authentication is entirely handled by D-Bus policy and daemon-side process tracking. + +### 13.5 Memory Safety + +- No `sprintf()` usage — all string formatting uses `snprintf()` with bounds +- All `strncpy()` usage explicitly null-terminates the destination +- Handle memory is freed exactly once (in `unregisterProcess()`) +- Registry entries are freed on slot reset — no dangling pointers +- GLib objects are unreffed on all code paths (including error paths) + +--- + +## 14. Performance Considerations + +### 14.1 Overhead Per API Call + +| Operation | Typical Latency | Bottleneck | +|-----------|----------------|------------| +| `registerProcess()` | 5-10ms | D-Bus round-trip | +| `checkForUpdate()` | <1ms (returns immediately) | D-Bus message queueing | +| `downloadFirmware()` | <1ms (returns immediately) | D-Bus message queueing | +| `updateFirmware()` | <1ms (returns immediately) | D-Bus message queueing | +| `unregisterProcess()` | 10-15ms | pthread_join + D-Bus round-trip | + +### 14.2 Memory Footprint + +| Component | Size | Notes | +|-----------|------|-------| +| Three registries (static arrays) | ~30 × 3 × (272 bytes per entry) ≈ 24 KB | Stack-like, no heap fragmentation | +| Background thread stack | 8 MB default (configurable) | Linux thread default | +| GMainLoop + GMainContext | ~2 KB | GLib internal | +| D-Bus connection (persistent) | ~4 KB | Background thread's connection | +| Per-call D-Bus proxy | ~2 KB | Freed immediately after call | + +**Total resident overhead**: ~35 KB (excluding thread stack, which is virtual memory only) + +### 14.3 Connection Overhead + +The per-call connection model has overhead: +- Each `g_bus_get_sync()` call performs a D-Bus handshake +- Each gets a unique sender name allocated by dbus-daemon +- Connection teardown releases the name + +For infrequent firmware operations (minutes/hours between calls), this overhead is negligible. If call frequency were high (>10/second), a persistent connection pool would be warranted. + +### 14.4 Signal Dispatch Efficiency + +The two-phase dispatch creates a stack-local snapshot array on every signal. With `MAX_PENDING_CALLBACKS=30` and ~272 bytes per snapshot entry, this is ~8KB of stack usage — well within typical 8MB thread stack limits. + +The linear scan of 30 entries is O(30) — negligible for this use case. A hash map would be over-engineering. + +--- + +## 15. Scalability for Multiple Clients + +### 15.1 Current Capacity + +| Resource | Limit | Constraint | +|----------|-------|------------| +| Concurrent registered processes | Limited by daemon's `registered_processes` hash table | Effectively unbounded (GHashTable) | +| Concurrent pending checkForUpdate callbacks | 30 | `MAX_PENDING_CALLBACKS` constant | +| Concurrent active downloads | 30 | Same constant for download registry | +| Concurrent active updates | 30 | Same constant for update registry | + +### 15.2 Multi-Client Signal Delivery + +D-Bus signals are **broadcast** — all connected clients receive them. The library's dispatch logic handles this: + +``` +Daemon emits CheckForUpdateComplete: + ├─ Client A's background thread receives it → dispatches to Client A's callback + ├─ Client B's background thread receives it → dispatches to Client B's callback + └─ Client C's background thread receives it → dispatches to Client C's callback +``` + +Each client has its own library instance (separate `.so` loaded into its process space), its own background thread, its own registries. They are completely independent. + +### 15.3 Daemon-Side Coalescing + +For download operations, the daemon implements **piggybacking**: if multiple clients request the same firmware file simultaneously, the daemon downloads it once and sends progress signals to all registered clients. The library dispatches these signals to all ACTIVE download callbacks transparently. + +### 15.4 Scaling Limitations + +- If more than 30 checkForUpdate calls are pending simultaneously (across the same process), the 31st will fail with `CHECK_FOR_UPDATE_FAIL`. This is unlikely in practice — 30 concurrent firmware checks from one process would be a design error. +- The background thread is single-threaded — if a callback takes a long time (e.g., client does heavy processing in the callback), other signals queue up in the GMainContext. Clients should keep callbacks short and signal their main thread for heavy work. + +--- + +## 16. Future Extensibility + +### 16.1 Potential New APIs + +| API | Purpose | Priority | +|-----|---------|----------| +| `cancelDownload(handle)` | Abort an in-progress download | High | +| `pauseDownload(handle)` / `resumeDownload(handle)` | Pause/resume for network bandwidth management | Medium | +| `getUpdateStatus(handle)` | Synchronous poll of current state (for apps that don't want callbacks) | Medium | +| `setUpdatePolicy(handle, policy)` | Configure auto-update behavior per client | Low | +| `subscribeToDeviceState(handle, cb)` | Get notified of device firmware state changes (not just self-initiated) | Low | + +### 16.2 HAL Integration (Planned) + +The `UpdateCallback` signature documentation notes: +> "The signature and behavior of this callback may change in future versions when HAL (Hardware Abstraction Layer) APIs become available." + +When HAL is integrated: +- More granular progress reporting (per-partition) +- Device-specific status codes +- Verification step callbacks (checksum validation) + +### 16.3 Persistent Connection Model (If Needed) + +If call frequency increases or the daemon implements per-connection state: +- Replace per-call `g_bus_get_sync()` with a connection pool +- Add reconnection logic on connection drop +- Add heartbeat/keepalive mechanism + +### 16.4 User-Data in Callbacks + +Current callback signatures don't include a `void *user_data` parameter (e.g., `UpdateEventCallback` takes only `const FwInfoData*`). Adding user_data would: +- Eliminate the need for global variables in client apps +- Allow multiple independent sessions in one process +- Be an ABI-breaking change (major version bump) + +### 16.5 Async/Await Style (C11 Atomics) + +For modern C codebases, consider offering a "future" API alongside callbacks: +```c +// Hypothetical future API: +FwCheckFuture *future = checkForUpdateAsync(handle); +// ... do other work ... +FwInfoData *result = awaitFwCheck(future, timeout_ms); // blocks until ready +freeFwCheckFuture(future); +``` + +This would be a convenience wrapper over the existing callback mechanism. + +--- + +## Appendix A: Complete D-Bus Interface Contract + +``` +Service: org.rdkfwupdater.Service +Path: /org/rdkfwupdater/Service +Interface: org.rdkfwupdater.Interface + +METHODS: + RegisterProcess(s processName, s libVersion) → (t handler_id) + UnregisterProcess(t handler_id) → (b success) + CheckForUpdate(s handle) + DownloadFirmware(s handle, s firmwareName, s downloadUrl, s typeOfFirmware) + UpdateFirmware(s handle, s firmwareName, s location, s type, s rebootImmediately) + +SIGNALS: + CheckForUpdateComplete(t handler_id, i result_code, i status_code, + s current_version, s available_version, + s update_details, s status_message) + + DownloadProgress(t handler_id, s firmware_name, u progress_percent, + s status_string, s message) + + UpdateProgress(t handler_id, s firmware_name, i progress_percent, + i status_code, s message) +``` + +--- + +## Appendix B: End-to-End Sequence Diagram + +``` + Client App (main) librdkFwupdateMgr BG Thread Daemon + ───────────────── ───────────────── ───────── ────── + │ │ │ │ + │ registerProcess() │ │ │ + │──────────────────────────►│ │ │ + │ │── D-Bus: Register ───────────────────────►│ + │ │◄──── handler_id ─────────────────────────┤ + │ │── internal_system_init() │ + │ │ └─ pthread_create() ──►│ │ + │ │ │─ subscribe ──►│ + │ │ │ signals │ + │ │◄── running=true ─────────┤ │ + │◄── handle "12345" ────────┤ │ │ + │ │ │ │ + │ checkForUpdate(h, cb) │ │ │ + │──────────────────────────►│ │ │ + │ │── register cb in reg. │ │ + │ │── D-Bus: CheckForUpdate ─────────────────►│ + │◄── SUCCESS ───────────────┤ │ │ + │ │ │ │ + │ [waiting on condvar] │ │ XConf... │ + │ │ │ │ + │ │ │◄── signal ────┤ + │ │ │ │ + │ │ on_check_complete_signal() │ + │ │ dispatch_all_pending() │ + │ │ │ │ + │◄──────────────────────────│────── cb(&fwinfo) ◄┘ │ + │ [condvar signaled] │ │ + │ │ │ + │ downloadFirmware(h,r,cb) │ │ + │──────────────────────────►│ │ + │ │── register dl_cb │ + │ │── D-Bus: DownloadFirmware ───────────────►│ + │◄── SUCCESS ───────────────┤ │ │ + │ │ │◄── 25% ───────┤ + │◄──── dl_cb(25,INPROG) ────│──────────────────────────┤ │ + │ │ │◄── 100% ──────┤ + │◄──── dl_cb(100,DONE) ─────│──────────────────────────┤ │ + │ │ │ + │ unregisterProcess(h) │ │ + │──────────────────────────►│ │ + │ │── internal_system_deinit() │ + │ │ └─ quit loop ──────────►│ exits │ + │ │ └─ join ◄──────────────┘ │ + │ │── D-Bus: Unregister ─────────────────────►│ + │ │── free(handle) │ + │◄── return ────────────────┤ │ + │ │ │ +``` + +--- + +## Appendix C: Build and Integration + +### Library Build (Autotools) + +```makefile +# librdkFwupdateMgr/Makefile.am +lib_LTLIBRARIES = librdkFwupdateMgr.la + +librdkFwupdateMgr_la_SOURCES = \ + src/rdkFwupdateMgr_process.c \ + src/rdkFwupdateMgr_api.c \ + src/rdkFwupdateMgr_async.c + +librdkFwupdateMgr_la_CFLAGS = \ + -I$(top_srcdir)/librdkFwupdateMgr/include \ + -I$(top_srcdir)/librdkFwupdateMgr/src \ + -I$(top_srcdir)/common_utilities/utils \ + $(GIO_CFLAGS) $(GLIB_CFLAGS) + +librdkFwupdateMgr_la_LIBADD = \ + $(GIO_LIBS) $(GLIB_LIBS) -lpthread + +# Installed public header +librdkFwupdateMgr_includedir = $(includedir)/rdkFwupdateMgr +librdkFwupdateMgr_include_HEADERS = include/rdkFwupdateMgr_client.h +``` + +### Client Linking + +```makefile +my_app_LDADD = -lrdkFwupdateMgr -lfwutils -lrdkloggers $(GIO_LIBS) +my_app_CFLAGS = -I$(includedir)/rdkFwupdateMgr +``` + +### Minimum Client Code + +```c +#include "rdkFwupdateMgr_client.h" +#include "rdkv_cdl_log_wrapper.h" + +int main(void) { + log_init(); + + FirmwareInterfaceHandle h = registerProcess("MyApp", LIB_VERSION); + if (!h) return 1; + + // ... use APIs ... + + unregisterProcess(h); + log_exit(); + return 0; +} +``` + +--- + +*End of Document* diff --git a/librdkFwupdateMgr/DOCUMENT_v1.md b/librdkFwupdateMgr/DOCUMENT_v1.md new file mode 100755 index 00000000..9f8c6452 --- /dev/null +++ b/librdkFwupdateMgr/DOCUMENT_v1.md @@ -0,0 +1,998 @@ +# librdkFwupdateMgr — Design, Code Documentation & Logging Architecture + +> **Version**: v1.0 +> **Date**: April 28, 2026 +> **Scope**: Pull Request documentation for the `librdkFwupdateMgr` shared library +> **Author**: Senior Engineer — rdkFwupdateMgr Team + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [High-Level Architecture](#2-high-level-architecture) +3. [File Structure](#3-file-structure) +4. [API-by-API Design](#4-api-by-api-design) + - 4.1 [registerProcess()](#41-registerprocessprocessname-libversion--firmwareinterfacehandle) + - 4.2 [checkForUpdate()](#42-checkforupdatehandle-callback--checkforupdateresult) + - 4.3 [downloadFirmware()](#43-downloadfirmwarehandle-fwdwnlreq-callback--downloadresult) + - 4.4 [updateFirmware()](#44-updatefirmwarehandle-fwupdatereq-callback--updateresult) + - 4.5 [unregisterProcess()](#45-unregisterprocesshandle) +5. [Internal Async Engine Design](#5-internal-async-engine-design) + - 5.1 [Three Callback Registries](#51-three-callback-registries) + - 5.2 [Background Thread](#52-background-thread) + - 5.3 [Two-Phase Signal Dispatch (Deadlock Prevention)](#53-two-phase-signal-dispatch-deadlock-prevention) + - 5.4 [Per-Call D-Bus Connections (Stateless Model)](#54-per-call-d-bus-connections-stateless-model) +6. [Memory Management](#6-memory-management) +7. [Thread Safety](#7-thread-safety) +8. [Error Handling Strategy](#8-error-handling-strategy) +9. [In-Code Documentation Guidelines](#9-in-code-documentation-guidelines) + - 9.1 [rdkFwupdateMgr_process.c — Key Inline Comments](#91-rdkfwupdatemgr_processc--key-inline-comments) + - 9.2 [rdkFwupdateMgr_api.c — Key Inline Comments](#92-rdkfwupdatemgr_apic--key-inline-comments) + - 9.3 [rdkFwupdateMgr_async.c — Key Inline Comments](#93-rdkfwupdatemgr_asyncc--key-inline-comments) +10. [Logging Architecture](#10-logging-architecture) + - 10.1 [Three Log Modules](#101-three-log-modules) + - 10.2 [Macro Definitions](#102-macro-definitions) + - 10.3 [Log Initialization Ownership](#103-log-initialization-ownership) + - 10.4 [Includes Required](#104-includes-required) + - 10.5 [Build Configuration (Makefile.am)](#105-build-configuration-makefileam) + - 10.6 [Sample Log Output](#106-sample-log-output) + +--- + +## 1. Executive Summary + +`librdkFwupdateMgr` is a shared library (`.so`) that provides a simple C API for client applications to perform firmware updates on RDK devices. It communicates with the `rdkFwupdateMgr` daemon over D-Bus. The library hides all D-Bus complexity — clients just call functions and receive results via callbacks. + +**In simple terms**: Think of it like ordering food through a delivery app. +- You place an order (`registerProcess`) and get an order number (`handle`). +- You ask "is my food ready?" (`checkForUpdate`) and get a notification later (`callback`). +- You say "deliver it" (`downloadFirmware`) and track the delivery (`progress callbacks`). +- You say "serve it" (`updateFirmware`) and watch it being plated (`progress callbacks`). +- When done, you close the app (`unregisterProcess`). + +The library handles all the complicated behind-the-scenes communication (D-Bus IPC) with the daemon that actually does the firmware work. + +--- + +## 2. High-Level Architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ CLIENT APPLICATION │ +│ (e.g., example_plugin) │ +│ │ +│ main() │ +│ ├─ registerProcess("MyApp", "1.0") → handle "12345" │ +│ ├─ checkForUpdate(handle, my_cb) → returns immediately │ +│ │ [waits on condvar] │ +│ │ ◄── my_cb(fwinfo) fires in BG thread │ +│ ├─ downloadFirmware(handle, req, dl_cb) → returns immed. │ +│ │ [waits on condvar] │ +│ │ ◄── dl_cb(progress%, status) fires repeatedly │ +│ ├─ updateFirmware(handle, req, upd_cb) → returns immed. │ +│ │ [waits on condvar] │ +│ │ ◄── upd_cb(progress%, status) fires repeatedly │ +│ └─ unregisterProcess(handle) │ +│ │ +├──────────────── librdkFwupdateMgr.so ───────────────────────┤ +│ │ +│ PUBLIC LAYER (rdkFwupdateMgr_api.c, _process.c) │ +│ • Input validation │ +│ • D-Bus proxy creation (per-call, stateless) │ +│ • Fire-and-forget D-Bus method calls │ +│ • Callback registration in internal registries │ +│ │ +│ ASYNC ENGINE (rdkFwupdateMgr_async.c) │ +│ • 3 callback registries (Check, Download, Update) │ +│ • 1 background thread running GLib event loop │ +│ • D-Bus signal subscriptions (3 signals) │ +│ • Two-phase dispatch (snapshot → invoke) │ +│ │ +├──────────────── D-Bus (system bus) ──────────────────────────┤ +│ │ +│ rdkFwupdateMgr DAEMON │ +│ • Registered as org.rdkfwupdater.Service │ +│ • Processes method calls (Register, Check, Download, │ +│ Update, Unregister) │ +│ • Emits signals back (CheckForUpdateComplete, │ +│ DownloadProgress, UpdateProgress) │ +└─────────────────────────────────────────────────────────────┘ +``` + +### How D-Bus Fits In (For Newcomers) + +D-Bus is a messaging system used on Linux devices for programs to talk to each other. Think of it like a shared phone line between applications: + +- The **daemon** (server) sits on the phone line and has a well-known name: `org.rdkfwupdater.Service` +- The **library** (client) calls methods on the daemon (like dialing a number and asking a question) +- The **daemon** sends **signals** back (like broadcast notifications) when work is done +- The **library's background thread** listens for these signals and routes them to the right callback + +--- + +## 3. File Structure + +| File | Layer | Purpose | +|------|-------|---------| +| `include/rdkFwupdateMgr_client.h` | **Public API** | All types, enums, callbacks, and function declarations a client needs. This is the ONLY header clients include. | +| `src/rdkFwupdateMgr_process.c` | **Public** | `registerProcess()` and `unregisterProcess()` — synchronous D-Bus calls | +| `src/rdkFwupdateMgr_api.c` | **Public** | `checkForUpdate()`, `downloadFirmware()`, `updateFirmware()` — async fire-and-forget APIs | +| `src/rdkFwupdateMgr_async.c` | **Internal** | The async engine — registries, background thread, signal handlers, dispatch logic | +| `src/rdkFwupdateMgr_async_internal.h` | **Internal** | Internal types and declarations — NOT part of public API, NOT shipped to clients | +| `src/rdkFwupdateMgr_log.h` | **Internal** | `FWUPMGR_*` logging macros (library code uses these) | +| `examples/example_app.c` | **Example** | Reference client demonstrating the full register→check→download→flash→unregister workflow | + +### What's Public vs Internal? + +- **Public** (`include/` folder): Headers that client applications `#include`. These define the API contract. Changing these requires a version bump. +- **Internal** (`src/` folder): Implementation details. Clients never see these. We can change them freely without breaking clients. + +--- + +## 4. API-by-API Design + +### 4.1 `registerProcess(processName, libVersion)` → `FirmwareInterfaceHandle` + +**What it does in plain English**: Tells the daemon "Hello, I'm a new client named [processName]. Please give me a session ID so I can do firmware operations." + +**Returns**: A string like `"12345"` (the session ID). Returns `NULL` on failure. + +#### Design Decisions + +- **Synchronous** — blocks until daemon responds (~5-10ms). Registration is fast so blocking is acceptable. +- Creates a **fresh D-Bus proxy per call** (stateless, no persistent connection). This means each call to any API gets a different D-Bus sender ID, which is a key characteristic of this library model. +- Daemon returns a `uint64` handler_id; library converts it to a `malloc()`'d string (e.g., `"12345"`). String representation provides ABI stability. +- Calls `internal_system_init()` to start the background thread that will listen for daemon signals. +- If `malloc()` fails after registration succeeds on the daemon side, performs **best-effort cleanup** by calling `UnregisterProcess` on the daemon to avoid leaking a registration. + +#### D-Bus Protocol + +``` +Method: RegisterProcess(s processName, s libVersion) → (t handler_id) +``` + +#### Flow + +``` +Client Thread Daemon +───────────── ────── +registerProcess("MyApp", "1.0") + │ + ├─ Validate processName (not NULL, not empty, ≤256 chars) + ├─ Validate libVersion (not NULL, ≤64 chars) + ├─ Create D-Bus proxy → connect to system bus + ├─ Call RegisterProcess("MyApp", "1.0") ──────────► Daemon receives call + │ │ Creates ProcessInfo + │ │ Assigns handler_id=12345 + │◄──── Returns handler_id=12345 ─────────────────────┘ + ├─ Convert 12345 → malloc'd string "12345" + ├─ internal_system_init() + │ ├─ Init 3 callback registries (mutexes + arrays) + │ ├─ Create isolated GLib context + event loop + │ ├─ Spawn background thread + │ └─ Wait until background thread is ready (signal subscriptions live) + └─ Return "12345" to caller +``` + +#### Lifecycle Impact + +This is the **start** of the library's lifecycle. Before `registerProcess()`: +- No background thread exists +- No callback registries exist +- No D-Bus signal subscriptions exist + +After `registerProcess()` returns successfully, the library is fully operational and ready for async API calls. + +--- + +### 4.2 `checkForUpdate(handle, callback)` → `CheckForUpdateResult` + +**What it does in plain English**: Asks the daemon "Is there new firmware available for this device?" The answer comes later through your callback — this function returns immediately. + +**Returns**: `CHECK_FOR_UPDATE_SUCCESS` (request started) or `CHECK_FOR_UPDATE_FAIL` (couldn't even start). + +#### Design Decisions + +- **Non-blocking** (fire-and-forget) — returns immediately after sending the D-Bus message +- **Callback fires exactly once** — when the daemon finishes querying the XConf server (5-30 seconds later) +- The callback receives a `FwInfoData` struct with firmware version info and update details + +#### The Connect → Register → Send Ordering + +This is a critical design detail: + +``` +Step 1: Connect to D-Bus ← Fail fast if daemon/bus is down +Step 2: Register callback ← Now we're ready to receive the signal +Step 3: Send method call ← Daemon starts working +``` + +**Why this order?** +- If we registered the callback first but D-Bus connect fails, we'd have a stale PENDING entry in the registry that would never be dispatched (no signal will ever arrive for it). +- If we sent the method first but hadn't registered the callback yet, the daemon might emit the signal before our callback is registered — the signal arrives, no matching entry found, result is silently lost. +- Connecting first, then registering, then sending gives us the safest ordering with the smallest race window. + +#### D-Bus Protocol + +``` +Method: CheckForUpdate(s handle) — fire-and-forget, no reply waited for +Signal: CheckForUpdateComplete(t handler_id, i result, i status, s currentVer, + s availableVer, s updateDetails, s message) +``` + +#### Flow + +``` +Client Thread Background Thread Daemon +───────────── ───────────────── ────── +checkForUpdate("12345", my_cb) + │ + ├─ Validate handle, callback + ├─ Connect to D-Bus + ├─ Register my_cb in CheckForUpdate registry + │ (slot state: IDLE → PENDING) + ├─ Fire-and-forget: CheckForUpdate("12345") ──────────► Daemon receives + └─ Return SUCCESS immediately │ Queries XConf... + │ (5-30 seconds) + [client is free to do │ + other work or wait] │ + │ + ◄── CheckForUpdateComplete ────┘ (signal emitted) + on_check_complete_signal(): + ├─ Parse GVariant → InternalSignalData + ├─ dispatch_all_pending(): + │ Phase 1 (mutex held): + │ Snapshot PENDING entries + │ Mark → DISPATCHED + │ Phase 2 (no mutex): + │ Build FwInfoData from signal + │ Call my_cb(&fwinfo_data) ──► Client's callback runs + │ Reset slot → IDLE + └─ Cleanup signal data +``` + +#### What the Callback Receives + +```c +typedef struct { + char CurrFWVersion[64]; // e.g., "1.0.0" + UpdateDetails *UpdateDetails; // Non-NULL only if FIRMWARE_AVAILABLE + CheckForUpdateStatus status; // FIRMWARE_AVAILABLE, NOT_AVAILABLE, etc. +} FwInfoData; +``` + +**Important**: The `FwInfoData` pointer and all its contents are only valid DURING the callback invocation. If you need the data later, copy it (which is exactly what `example_app.c` does with `strncpy` to globals). + +--- + +### 4.3 `downloadFirmware(handle, fwdwnlreq, callback)` → `DownloadResult` + +**What it does in plain English**: Tells the daemon "Download this firmware file from the server." Returns immediately; your callback fires repeatedly with progress updates (0%, 25%, 50%, 75%, 100%). + +**Returns**: `RDKFW_DWNL_SUCCESS` (download started) or `RDKFW_DWNL_FAILED` (couldn't start). + +#### Design Decisions + +- Same fire-and-forget pattern as `checkForUpdate()` +- **Key difference**: Callback fires **multiple times** (once per progress signal from daemon) +- Registry slot stays **ACTIVE** across all progress signals; only resets to IDLE on `DWNL_COMPLETED` or `DWNL_ERROR` +- Download URL can be `NULL`/empty string — daemon will use the URL from the XConf query + +#### D-Bus Protocol + +``` +Method: DownloadFirmware(s handle, s firmwareName, s downloadUrl, s TypeOfFirmware) +Signal: DownloadProgress(t handler_id, s firmwareName, u progress%, s status, s message) +``` + +#### Flow + +``` +Client Thread Background Thread Daemon +───────────── ───────────────── ────── +downloadFirmware("12345", &req, my_dl_cb) + │ + ├─ Validate handle, req, callback + ├─ Connect to D-Bus + ├─ Register my_dl_cb in Download registry + │ (slot state: IDLE → ACTIVE) + ├─ Fire-and-forget: DownloadFirmware(...) ─────────► Daemon starts download + └─ Return SUCCESS immediately │ + │ Download progress... + ◄── DownloadProgress(10%) ──┘ + dispatch_all_dwnl_active(): + Call my_dl_cb(10, IN_PROGRESS) ──► prints "10%" + (slot stays ACTIVE) + + ◄── DownloadProgress(50%) ── + Call my_dl_cb(50, IN_PROGRESS) ──► prints "50%" + + ◄── DownloadProgress(100%) ── + Call my_dl_cb(100, COMPLETED) ──► prints "100% done!" + Reset slot → IDLE (download finished) +``` + +--- + +### 4.4 `updateFirmware(handle, fwupdatereq, callback)` → `UpdateResult` + +**What it does in plain English**: Tells the daemon "Flash this downloaded firmware onto the device's storage." This modifies the device firmware. Returns immediately; callback fires with progress. + +**Returns**: `RDKFW_UPDATE_SUCCESS` (flash started) or `RDKFW_UPDATE_FAILED` (couldn't start). + +#### Design Decisions + +- Identical pattern to `downloadFirmware()` — ACTIVE slot, multiple callbacks, reset on terminal status +- `LocationOfFirmware` can be `NULL`/empty — daemon uses the default path from `/etc/device.properties` +- `rebootImmediately` is a `bool` in the struct but sent to daemon as string `"true"`/`"false"` (daemon D-Bus method expects string) +- This operation is **irreversible** once the flash starts — client should verify the firmware file first + +#### D-Bus Protocol + +``` +Method: UpdateFirmware(s handle, s firmwareName, s location, s type, s rebootImmediately) +Signal: UpdateProgress(t handler_id, s firmwareName, i progress%, i status, s message) +``` + +--- + +### 4.5 `unregisterProcess(handle)` + +**What it does in plain English**: Tells the daemon "I'm done, please clean up my registration." Also frees all library resources (background thread, registries, handle memory). + +**Returns**: Nothing (`void`). This is a best-effort cleanup. + +#### Design Decisions + +- **Synchronous** — blocks until daemon responds +- **Best-effort** — if D-Bus call fails, local cleanup still happens (daemon may have already cleaned up on its own) +- Calls `internal_system_deinit()` **FIRST** — stops background thread, frees registries, destroys mutexes +- Then sends D-Bus `UnregisterProcess`, then `free(handle)` +- **Idempotent**: safe to call with `NULL` handle (no-op) +- Strictly validates handle string: must be pure decimal digits, no leading/trailing whitespace, no garbage characters + +#### Why deinit Before D-Bus Call? + +After unregister, the daemon won't send any more signals for this client. There's no point keeping the background thread alive. Shutting it down first ensures a clean `pthread_join()` without waiting for signals that will never arrive. + +#### Flow + +``` +Client Thread Daemon +───────────── ────── +unregisterProcess("12345") + │ + ├─ Validate handle (not NULL) + ├─ Parse "12345" → uint64 12345 + ├─ internal_system_deinit() + │ ├─ g_main_loop_quit() → background thread wakes up + │ ├─ pthread_join() → wait for background thread to exit + │ ├─ Free GLib resources (loop, context) + │ ├─ Free download & update registries + │ ├─ Free check registry (any leftover handle_key strings) + │ └─ Destroy all mutexes + ├─ Create D-Bus proxy + ├─ Call UnregisterProcess(12345) ──────────────► Daemon removes ProcessInfo + │◄──── Returns success=true ──────────────────┘ + ├─ free(handle) ← The "12345" string is freed + └─ Return +``` + +--- + +## 5. Internal Async Engine Design + +### 5.1 Three Callback Registries + +The library maintains **three independent registries**, one per async API. They are separate because each has a different callback lifecycle: + +| Registry | C Type | Slot Lifecycle | How Many Times Callback Fires | +|----------|--------|----------------|-------------------------------| +| `CallbackRegistry` | `g_registry` | IDLE → PENDING → DISPATCHED → IDLE | **Once** (check result) | +| `DwnlCallbackRegistry` | `g_dwnl_registry` | IDLE → ACTIVE → IDLE | **Multiple** (progress updates) | +| `UpdateCbRegistry` | `g_update_registry` | IDLE → ACTIVE → IDLE | **Multiple** (progress updates) | + +Each registry is a **fixed-size array** of `MAX_PENDING_CALLBACKS` (30) slots. This means at most 30 concurrent pending callbacks across all clients of any given type. + +Each slot holds: +- **State**: IDLE, PENDING, ACTIVE, DISPATCHED, or TIMED_OUT +- **handle_key**: `strdup()`'d copy of the app's handle string +- **callback**: Function pointer to the app's callback +- **registered_time**: Timestamp for timeout detection + +Each registry has its **own `pthread_mutex_t`** — so checkForUpdate registrations don't block download progress dispatching. + +#### State Machine + +``` +CheckForUpdate slot: + IDLE ──(checkForUpdate called)──► PENDING ──(signal arrives)──► DISPATCHED ──► IDLE + └──(timeout)──► TIMED_OUT ──► IDLE + +Download/Update slot: + IDLE ──(download/updateFirmware called)──► ACTIVE ──(COMPLETED/ERROR)──► IDLE + │ + │ (fires callback on EVERY + │ progress signal while ACTIVE) + │ + └──(timeout)──► TIMED_OUT ──► IDLE +``` + +### 5.2 Background Thread + +A single `pthread` runs a private GLib event loop for the entire lifetime of the library (from `registerProcess()` to `unregisterProcess()`). + +#### Thread Startup Sequence + +``` +1. g_main_context_push_thread_default() + └─ Creates an ISOLATED GLib context for this thread + (won't interfere with app's own GLib loop if it has one) + +2. g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) + └─ Connect to system D-Bus (this is the background thread's + OWN connection — different from the per-call connections + used by the public API functions) + +3. g_dbus_connection_signal_subscribe() × 3 + ├─ CheckForUpdateComplete → on_check_complete_signal() + ├─ DownloadProgress → on_download_progress_signal() + └─ UpdateProgress → on_update_progress_signal() + +4. g_bg_thread.running = true + └─ Main thread sees this and stops spin-waiting + +5. g_main_loop_run() + └─ BLOCKS here until internal_system_deinit() calls g_main_loop_quit() + GLib dispatches signal callbacks within this loop. +``` + +#### Why Spin-Wait Instead of Condvar? + +The main thread waits for the background thread to be ready by polling `g_bg_thread.running` every 100ms. A condvar would be slightly more elegant, but: + +1. The wait is typically <100ms (thread starts fast) +2. Adding a condvar adds complexity and another resource to manage/destroy +3. This only happens **once per library lifetime** (at `registerProcess` time) +4. Max wait: 50 × 100ms = 5 seconds — if the thread hasn't started by then, something is seriously wrong + +### 5.3 Two-Phase Signal Dispatch (Deadlock Prevention) + +All three signal handlers use the same two-phase pattern. This is the most important design detail in the async engine. + +#### The Problem + +If we held the registry mutex while invoking a callback, and that callback called `checkForUpdate()` again, it would try to lock the same mutex → **deadlock**. + +#### The Solution + +``` +PHASE 1 — Snapshot (mutex HELD): + ├─ Scan registry for matching entries (PENDING or ACTIVE) + ├─ Copy callback pointer + handle into a local stack array (snapshot) + ├─ Mark slots appropriately (DISPATCHED for check, leave ACTIVE for download/update) + └─ RELEASE mutex + +PHASE 2 — Invoke (NO mutex held): + ├─ Build result struct from signal data + ├─ For each snapshot entry: + │ ├─ Call callback(result_data) + │ └─ If terminal state (COMPLETED/ERROR): re-lock mutex, reset slot → IDLE, unlock + └─ Done +``` + +**Why is this safe?** +- During Phase 2, the mutex is released, so if a callback calls `checkForUpdate()` → `internal_register_callback()`, it can acquire the mutex without deadlock. +- The snapshot is a local stack array, so even if the registry changes during Phase 2, our snapshot is stable. + +### 5.4 Per-Call D-Bus Connections (Stateless Model) + +Each public API call (`registerProcess`, `checkForUpdate`, etc.) creates a **fresh D-Bus connection and proxy** for that single call, then immediately releases it via `g_object_unref()`. + +#### Implications + +- **No persistent connection** between the client and daemon at the API layer +- Each call may get a **different D-Bus sender ID** (e.g., `:1.140` for register, `:1.141` for checkForUpdate, `:1.145` for unregister) +- The daemon **cannot rely on sender ID** to identify a client across calls — only the `handler_id` (the numeric handle) is stable +- This is why the daemon's `UnregisterProcess` handler doesn't validate sender-ID ownership — it would always fail because unregister comes from a different sender than register + +The background thread has its **OWN persistent connection** (for signal subscriptions), but the API-calling thread uses ephemeral connections. + +--- + +## 6. Memory Management + +| Resource | Owner | Allocation | Deallocation | +|----------|-------|------------|--------------| +| `FirmwareInterfaceHandle` (the `"12345"` string) | Library | `malloc()` in `registerProcess()` | `free()` in `unregisterProcess()` | +| Registry `handle_key` entries | Async engine | `strdup()` on callback registration | `free()` on slot reset to IDLE | +| D-Bus proxy/connection (per API call) | Caller's stack | `g_bus_get_sync()` + `g_dbus_proxy_new_sync()` | `g_object_unref()` at end of each API function | +| D-Bus connection (background thread) | Background thread | `g_bus_get_sync()` in thread func | `g_object_unref()` on thread exit | +| GMainLoop, GMainContext | Async engine | `g_main_loop_new()` / `g_main_context_new()` | `g_main_loop_unref()` / `g_main_context_unref()` in `deinit()` | +| `FwInfoData` + `UpdateDetails` in dispatch | Stack-allocated | `dispatch_all_pending()` local variables | Automatic (function returns) | +| `InternalSignalData` strings | Parse function | `strdup()` from GVariant data | `free()` in `internal_cleanup_signal_data()` | + +### Rules for Client Developers + +1. **Never `free()` the handle yourself.** Call `unregisterProcess()` and it handles everything. +2. **Copy callback data if you need it later.** The `FwInfoData*` pointer in your callback is only valid during the callback invocation. Use `strncpy()` to save values to your own buffers. +3. **Don't call library APIs from inside a callback.** The callback runs in the background thread. Re-entering the library is technically safe (due to two-phase dispatch) for `checkForUpdate`-style calls, but it's better practice to signal your main thread and make calls from there. + +--- + +## 7. Thread Safety + +| Operation | Thread-Safe? | Notes | +|-----------|-------------|-------| +| `registerProcess()` | Yes | Stateless per-call; GDBus sync calls are thread-safe | +| `unregisterProcess()` | Per-handle | Don't unregister the same handle from two threads simultaneously | +| `checkForUpdate()` | Yes | Registry mutex protects slot allocation | +| `downloadFirmware()` | Yes | Separate registry with its own mutex | +| `updateFirmware()` | Yes | Separate registry with its own mutex | +| Callbacks | N/A | Fire in background thread; app must use its own synchronization (mutex + condvar) to coordinate with main thread | + +### Callback Threading Model + +``` +Main Thread Background Thread +─────────── ───────────────── + on_check_complete_signal() + └─ dispatch_all_pending() + └─ your_callback(&fwinfo) ← RUNS HERE + ├─ Copy data to globals + ├─ pthread_mutex_lock(&your_mutex) + ├─ your_done_flag = 1 + ├─ pthread_cond_signal(&your_cond) + └─ pthread_mutex_unlock(&your_mutex) + +pthread_mutex_lock(&your_mutex) +while (!your_done_flag) + pthread_cond_timedwait(...) ← WAKES UP HERE +pthread_mutex_unlock(&your_mutex) +// Now use the copied data safely +``` + +This is exactly how `example_app.c` works — see `g_check_mutex`/`g_check_cond`/`g_check_done`. + +--- + +## 8. Error Handling Strategy + +The library follows a defense-in-depth approach: + +### Layer 1: Input Validation (API Boundary) + +Every public function validates ALL parameters before touching D-Bus: +- NULL checks on handles, callbacks, request structs +- Empty string checks +- Length limit checks (process name ≤256 chars, version ≤64 chars) +- Numeric validity for handle parsing in `unregisterProcess()` (uses `strtoull` with strict `endptr` checking) + +### Layer 2: D-Bus Connection Failures + +- Connection attempt happens BEFORE callback registration (Connect → Register → Send pattern) +- If D-Bus is down, function returns error immediately with no stale registry entries + +### Layer 3: Daemon Errors + +- D-Bus errors from the daemon are caught via `GError` +- Error message is logged via `FWUPMGR_ERROR` +- Error is propagated as a return code to the caller + +### Layer 4: Resource Leak Prevention + +- In `registerProcess()`: If registration succeeds on daemon but `malloc()` fails locally, a cleanup proxy is created to send `UnregisterProcess` to prevent leaking the registration +- In `unregisterProcess()`: Handle memory is freed regardless of whether the D-Bus call succeeds +- In all API functions: D-Bus connections/proxies are freed on all code paths (success and error) + +### Layer 5: Best-Effort Cleanup + +- `unregisterProcess()` is best-effort: if D-Bus call fails, local cleanup still happens +- This handles the case where the daemon has already crashed or been restarted + +--- + +## 9. In-Code Documentation Guidelines + +The source code already has extensive inline comments. Below are the **key documentation points** that every reviewer should understand, organized by file. + +### 9.1 `rdkFwupdateMgr_process.c` — Key Inline Comments + +#### `registerProcess()` — Why `internal_system_init()` is here + +```c +/* + * Start the background listener thread NOW (not at library load time). + * + * WHY HERE and not in __attribute__((constructor))? + * The constructor approach is #if 0'd out in rdkFwupdateMgr_api.c. + * We start the background thread at register time because: + * 1. The handle must exist before any async API can be called + * 2. The background thread needs a live D-Bus connection — doing it + * too early risks connecting before the system bus is ready + * (common during early boot on embedded devices) + * 3. It pairs naturally with internal_system_deinit() in unregisterProcess() + * + * This initializes: + * - CallbackRegistry (checkForUpdate callbacks) + * - DwnlCallbackRegistry (download callbacks) + * - UpdateCbRegistry (update callbacks) + * - BackgroundThread (GLib event loop, D-Bus signal subscriptions) + */ +``` + +#### `unregisterProcess()` — Why `internal_system_deinit()` comes before D-Bus call + +```c +/* + * Stop the background listener thread BEFORE sending UnregisterProcess. + * + * WHY BEFORE the D-Bus call? + * After unregister, the daemon won't send us any more signals. + * There's no point keeping the background thread alive. + * Shutting down first ensures a clean pthread_join() without + * waiting for signals that will never arrive. + * + * This tears down: + * - g_main_loop_quit() → background thread exits g_main_loop_run() + * - pthread_join() → waits for clean exit + * - Frees all 3 registries (any leftover handle_key strings) + * - Destroys all mutexes + */ +``` + +### 9.2 `rdkFwupdateMgr_api.c` — Key Inline Comments + +#### Connect → Register → Send ordering rationale + +```c +/* + * ORDERING MATTERS: Connect → Register → Send + * + * We could register the callback first, but then if D-Bus connection + * fails, we'd have a stale PENDING entry in the registry that would + * never be dispatched (no signal will ever arrive for it). + * + * We could send first, but then the daemon might emit the signal + * before our callback is registered → signal arrives, no matching + * entry found, result is silently lost. + * + * The correct order is: + * 1. Connect (fail fast if daemon/D-Bus is down) + * 2. Register callback (now we're ready to receive) + * 3. Send the method call (daemon starts working) + * + * The window between register and send is microseconds — acceptably + * small race window where the callback slot exists but the daemon + * hasn't been asked yet. + */ +``` + +### 9.3 `rdkFwupdateMgr_async.c` — Key Inline Comments + +#### Two-Phase Dispatch — Why we release the mutex before calling callbacks + +```c +/* + * TWO-PHASE DESIGN — avoids deadlock: + * + * PHASE 1 (mutex held): + * Scan registry → snapshot all PENDING entries into local array. + * Mark each found entry as DISPATCHED. + * Release mutex. + * + * PHASE 2 (mutex released): + * Build FwInfoData from signal_data. + * Invoke each snapshot callback: callback(&fwinfo_data) + * Re-acquire mutex briefly to reset each slot to IDLE. + * + * WHY RELEASE BEFORE CALLING CALLBACKS? + * If a callback called checkForUpdate() again, it would call + * internal_register_callback() which tries to lock the same mutex + * → deadlock. Releasing first makes re-entrant use safe. + */ +``` + +#### Background thread spin-wait rationale + +```c +/* + * Spin-wait for background thread to set running=true. + * Max wait: 50 × 100ms = 5 seconds. + * + * WHY SPIN-WAIT instead of condvar? + * 1. The wait is typically <100ms (thread starts fast) + * 2. Adding a condvar adds complexity and another resource to manage + * 3. This only happens once per library lifetime + * 4. 100ms sleep granularity is fine for a one-time init + * + * Ensures D-Bus signal subscription is live before checkForUpdate() + * can send a D-Bus method call — prevents missing the response signal. + */ +``` + +--- + +## 10. Logging Architecture + +### 10.1 Three Log Modules + +The system uses three distinct RDK_LOGGER modules so log output can be filtered by component: + +| Module Name | Macro Prefix | Used By | Log Tag in Output | +|------------|--------------|---------|-------------------| +| `LOG.RDK.FWUPMGR` | `FWUPMGR_*` | Library code (`_process.c`, `_api.c`, `_async.c`) | `[FWUPMGR]` | +| `LOG.RDK.FWUPG` | `SWLOG_*` | Daemon code (`rdkv_dbus_server.c`, `rdkv_main.c`, etc.) | `[FWUPG]` | +| `LOG.RDK.EXAMPLE` | `EXAMPLE_*` | Example app (`example_app.c`) | `[EXAMPLE]` | + +**Why three separate modules?** So you can filter logs in production: +- To see only library-side issues: `grep "\[FWUPMGR\]" /opt/logs/rdkFwupdateMgr.log` +- To see only daemon-side issues: `grep "\[FWUPG\]" /opt/logs/rdkFwupdateMgr.log` +- To see only client app issues: `grep "\[EXAMPLE\]" /opt/logs/rdkFwupdateMgr.log` + +### 10.2 Macro Definitions + +#### Library Macros — `rdkFwupdateMgr_log.h` + +```c +/* ── Base macro — callers provide their own module name ── */ +#define FWUPMGR_LOG(level, module, format, ...) \ + RDK_LOG(level, module, format, ##__VA_ARGS__) + +/* ── Library convenience macros ── */ +/* Used in rdkFwupdateMgr_process.c, rdkFwupdateMgr_api.c, rdkFwupdateMgr_async.c */ +#define FWUPMGR_TRACE(format, ...) FWUPMGR_LOG(RDK_LOG_TRACE1, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +#define FWUPMGR_FATAL(format, ...) FWUPMGR_LOG(RDK_LOG_FATAL, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) +``` + +#### Example App Macros — `example_app.c` + +```c +/* Reuses the FWUPMGR_LOG base macro but with a different module name */ +#define EXAMPLE_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +#define EXAMPLE_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +#define EXAMPLE_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +#define EXAMPLE_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) +``` + +#### Non-RDK_LOGGER Fallback + +When `RDK_LOGGER` is **not** defined (unit tests, standalone development builds), macros fall back to `fprintf`: + +```c +#define FWUPMGR_LOG(level, module, FORMAT...) fprintf(stderr, "[%s] " FORMAT, module) +``` + +This means logging works everywhere — just with different backends depending on the build configuration. + +#### How to Define Your Own Module (For New Client Apps) + +If you're writing a new client application (not using example_app.c), define your own macros: + +```c +#include "rdkFwupdateMgr_log.h" // Get FWUPMGR_LOG base macro +#include "rdkv_cdl_log_wrapper.h" // Get log_init(), log_exit() + +// Define your own module — logs will appear as [MYAPP] +#define MYAPP_INFO(fmt, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.MYAPP", fmt, ##__VA_ARGS__) +#define MYAPP_ERROR(fmt, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.MYAPP", fmt, ##__VA_ARGS__) + +int main(void) { + log_init(); // Initialize RDK logger + MYAPP_INFO("Starting up\n"); + // ... use library APIs ... + log_exit(); // Shutdown RDK logger +} +``` + +### 10.3 Log Initialization Ownership + +**The library does NOT own the log lifecycle.** The host application (whatever links to `librdkFwupdateMgr.so`) is responsible for calling: + +```c +log_init(); // BEFORE any library call (typically first line of main()) +log_exit(); // AFTER unregisterProcess() (typically last line before return) +``` + +Both `log_init()` and `log_exit()` are provided by `rdkv_cdl_log_wrapper.h` (in `common_utilities/utils/`). + +| Build Config | `log_init()` does | `log_exit()` does | +|-------------|-------------------|-------------------| +| `RDK_LOGGER` defined | Calls `rdk_logger_init()` with config file | Calls `rdk_logger_deinit()` | +| `RDK_LOGGER` not defined | No-op | No-op | + +**Why doesn't the library call `log_init()`?** +- A process should only call `log_init()` once. If the library called it, and the app also called it, that's a double-init which may cause issues. +- The app knows when it's ready to start logging. The library shouldn't make that decision. + +### 10.4 Includes Required + +#### For library source files (`_process.c`, `_api.c`, `_async.c`) + +```c +#include "rdkFwupdateMgr_log.h" // Provides FWUPMGR_* macros +``` + +That's all — the log header internally includes `rdkv_cdl_log_wrapper.h` and (if `RDK_LOGGER` is defined) `rdk_debug.h`. + +#### For client applications + +```c +#include "rdkFwupdateMgr_client.h" // Public API (types, functions) +#include "rdkFwupdateMgr_log.h" // FWUPMGR_LOG base macro (for defining your own module) +#include "rdkv_cdl_log_wrapper.h" // log_init(), log_exit() +``` + +### 10.5 Build Configuration (Makefile.am) + +The `example_plugin` target needs include paths for both log headers and link flags for the logger libraries: + +```makefile +# Include paths +example_plugin_CFLAGS += -I${top_srcdir}/librdkFwupdateMgr/src # rdkFwupdateMgr_log.h +example_plugin_CFLAGS += -I${top_srcdir}/common_utilities/utils # rdkv_cdl_log_wrapper.h + +# Link flags +example_plugin_LDADD += -lfwutils -lrdkloggers # log_init/log_exit implementations +``` + +### 10.6 Sample Log Output + +Below is what you'd see in `/opt/logs/rdkFwupdateMgr.log` during a typical firmware update workflow. Notice how the three tags (`[EXAMPLE]`, `[FWUPMGR]`, `[FWUPG]`) make it easy to trace what's happening at each layer: + +``` +[EXAMPLE] Application starting, PID: 1234 +[EXAMPLE] STEP 1: Register with firmware daemon +[FWUPMGR] registerProcess() called +[FWUPMGR] processName: 'ExampleApp' +[FWUPMGR] libVersion: '1.0.0' +[FWUPMGR] D-Bus proxy created successfully +[FWUPMGR] Calling RegisterProcess D-Bus method... +[FWUPG] [D-BUS] RegisterProcess received from ':1.140' +[FWUPG] [PROCESS_TRACKING] New client registered: ExampleApp (handler=12345) +[FWUPMGR] Registration successful +[FWUPMGR] handler_id: 12345 +[FWUPMGR] Handle created: '12345' +[FWUPMGR] internal_system_init: begin +[FWUPMGR] background_thread: starting +[FWUPMGR] background_thread: subscribed to CheckForUpdateComplete (id=1) +[FWUPMGR] background_thread: subscribed to DownloadProgress (id=2) +[FWUPMGR] background_thread: subscribed to UpdateProgress (id=3) +[FWUPMGR] internal_system_init: ready +[EXAMPLE] Registered successfully +[EXAMPLE] Handle: '12345' +[EXAMPLE] STEP 2: Check for firmware updates +[EXAMPLE] Calling checkForUpdate()... +[FWUPMGR] checkForUpdate: handle='12345' +[FWUPMGR] internal_register_callback: registered handle='12345' +[FWUPMGR] checkForUpdate: D-Bus call sent, returning SUCCESS. +[FWUPG] [D-BUS] CheckForUpdate received for handler 12345 +[FWUPG] [XCONF] Querying XConf server... +[FWUPG] [XCONF] Response: firmware_v2.bin available +[FWUPG] [SIGNAL] Emitting CheckForUpdateComplete +[FWUPMGR] on_check_complete_signal: received +[FWUPMGR] dispatch_all_pending: 1 callback(s) to fire +[FWUPMGR] dispatch_all_pending: invoking callback for handle='12345' +[EXAMPLE] checkForUpdate Callback Received +[EXAMPLE] Status Code: FIRMWARE_AVAILABLE (0) +[EXAMPLE] Current FW Version: 1.0.0 +[EXAMPLE] Available Version: 2.0.0 +[EXAMPLE] Firmware check data saved. Main thread will proceed. +[EXAMPLE] STEP 3: Download firmware image +[FWUPMGR] downloadFirmware: handle='12345' firmware='firmware_v2.bin' +[FWUPMGR] internal_dwnl_register_callback: registered handle='12345' +[FWUPMGR] downloadFirmware: D-Bus call sent, returning SUCCESS. +[FWUPMGR] on_download_progress_signal: progress=25% status='INPROGRESS' +[EXAMPLE] Download: 25% DWNL_IN_PROGRESS +[FWUPMGR] on_download_progress_signal: progress=50% status='INPROGRESS' +[EXAMPLE] Download: 50% DWNL_IN_PROGRESS +[FWUPMGR] on_download_progress_signal: progress=100% status='COMPLETED' +[EXAMPLE] Download: 100% DWNL_COMPLETED +[EXAMPLE] Download completed successfully! +``` + +--- + +## Appendix A: Complete Sequence Diagram + +``` + Main Thread Library BG Thread Daemon Process + ─────────── ───────────────── ────────────── + + [STEP 1: Register] + registerProcess("ExampleApp", "1.0.0") + │───── D-Bus: RegisterProcess ──────────────────► │ + │◄──── Returns handler_id=12345 ─────────────────┤ + g_handle = "12345" + internal_system_init() starts BG thread + │ subscribe CheckForUpdateComplete + │ subscribe DownloadProgress + │ subscribe UpdateProgress + │ running = true + │ g_main_loop_run() ← BLOCKS + + [STEP 2: Check for Update] + checkForUpdate("12345", on_check_cb) + │── register callback in g_registry + │───── D-Bus: CheckForUpdate("12345") ──────────► │ + │◄──── returns immediately │ query XConf... + │ waiting on condvar... │ + │ │ (5-30 seconds) + │ ◄── CheckForUpdateComplete ┤ + │ on_check_complete_signal(): │ + │ dispatch_all_pending() │ + │ on_check_cb(&fwinfo) ──►│ + │ signal condvar ───────►│ + │ condvar wakes up! │ + │ read g_check_status, g_fw_filename, etc. + + [STEP 3: Download] + downloadFirmware("12345", &req, on_dl_cb) + │── register callback in g_dwnl_registry + │───── D-Bus: DownloadFirmware(...) ────────────► │ + │◄──── returns immediately │ downloading... + │ waiting on condvar... │ + │ ◄── DownloadProgress(25%) ─┤ + │ on_dl_cb(25, IN_PROGRESS) │ + │ ◄── DownloadProgress(50%) ─┤ + │ on_dl_cb(50, IN_PROGRESS) │ + │ ◄── DownloadProgress(100%) ┤ + │ on_dl_cb(100, COMPLETED) │ + │ signal condvar ──────────►│ + │ condvar wakes up! + + [STEP 4: Flash] + updateFirmware("12345", &req, on_upd_cb) + │── register callback in g_update_registry + │───── D-Bus: UpdateFirmware(...) ──────────────► │ + │◄──── returns immediately │ flashing... + │ waiting on condvar... │ + │ ◄── UpdateProgress(50%) ──┤ + │ on_upd_cb(50, IN_PROGRESS) │ + │ ◄── UpdateProgress(100%) ─┤ + │ on_upd_cb(100, COMPLETED) │ + │ signal condvar ──────────►│ + │ condvar wakes up! + + [STEP 5: Unregister] + unregisterProcess("12345") + │── internal_system_deinit() + │ g_main_loop_quit() ──────────────► BG thread exits + │ pthread_join() ◄──────────────────┘ + │ free registries, destroy mutexes + │───── D-Bus: UnregisterProcess(12345) ─────────► │ + │◄──── Returns success=true ────────────────────┤ + │ free("12345") + │ done. +``` + +--- + +## Appendix B: D-Bus Interface Summary + +**Service**: `org.rdkfwupdater.Service` +**Object Path**: `/org/rdkfwupdater/Service` +**Interface**: `org.rdkfwupdater.Interface` + +### Methods (Client → Daemon) + +| Method | Signature | Description | +|--------|-----------|-------------| +| `RegisterProcess` | `(ss) → (t)` | Register client. Returns handler_id. | +| `UnregisterProcess` | `(t) → (b)` | Unregister client. Returns success. | +| `CheckForUpdate` | `(s)` | Fire-and-forget. No reply. | +| `DownloadFirmware` | `(ssss)` | Fire-and-forget. No reply. | +| `UpdateFirmware` | `(sssss)` | Fire-and-forget. No reply. | + +### Signals (Daemon → Client) + +| Signal | Signature | Description | +|--------|-----------|-------------| +| `CheckForUpdateComplete` | `(tiissss)` | Firmware check result. Fires once. | +| `DownloadProgress` | `(tsuss)` | Download progress. Fires repeatedly. | +| `UpdateProgress` | `(tsiis)` | Flash progress. Fires repeatedly. | diff --git a/librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md b/librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md new file mode 100755 index 00000000..a08461e8 --- /dev/null +++ b/librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md @@ -0,0 +1,1342 @@ +# librdkFwupdateMgr — Visual Engineering Documentation + +> **Document Version**: 1.0 +> **Date**: April 28, 2026 +> **Classification**: Internal Engineering — Pull Request Review +> **Component**: `librdkFwupdateMgr` (shared library) +> **Companion**: See [DESIGN_DOCUMENT.md](DESIGN_DOCUMENT.md) for full prose reference + +--- + +## Color Legend + +| Color | Meaning | Used For | +|-------|---------|----------| +| 🔵 Blue | Public API | Exported functions, client-visible interfaces | +| 🟢 Green | Success | Successful returns, normal completion paths | +| 🔴 Red | Error | Failures, error returns, exception paths | +| 🟡 Yellow | Validation | Input checks, parameter validation gates | +| 🟣 Purple | Logging | Log emission points, log module boundaries | +| ⬜ Gray | Internal Helpers | Private functions, internal state management | + +--- + +## Table of Contents + +1. [High-Level Architecture](#1-high-level-architecture) +2. [API Flowcharts](#2-api-flowcharts) + - 2.1 [registerProcess()](#21-registerprocess) + - 2.2 [checkForUpdate()](#22-checkforupdate) + - 2.3 [downloadFirmware()](#23-downloadfirmware) + - 2.4 [updateFirmware()](#24-updatefirmware) + - 2.5 [unregisterProcess()](#25-unregisterprocess) +3. [Sequence Diagrams](#3-sequence-diagrams) + - 3.1 [Complete Firmware Update Flow](#31-complete-firmware-update-flow) + - 3.2 [Daemon Unavailable + Retry](#32-daemon-unavailable--retry) + - 3.3 [Callback Registration & Delivery](#33-callback-registration--delivery) + - 3.4 [Timeout Recovery](#34-timeout-recovery) +4. [Thread Safety Diagram](#4-thread-safety-diagram) +5. [Memory Ownership Diagram](#5-memory-ownership-diagram) +6. [Logging Pipeline Diagram](#6-logging-pipeline-diagram) + +--- + +## 1. High-Level Architecture + +```mermaid +graph TB + subgraph CLIENT["🔵 Client Applications"] + direction LR + P1["example_plugin"] + P2["TR-069 Agent"] + P3["WebUI Service"] + end + + subgraph LIB["🔵 librdkFwupdateMgr.so"] + direction TB + API["Public API Layer
registerProcess · checkForUpdate
downloadFirmware · updateFirmware
unregisterProcess"] + VAL["🟡 Validation Gate
NULL checks · length limits
handle verification"] + REG["⬜ Callback Registries
g_registry (check)
g_dwnl_registry (download)
g_update_registry (update)"] + BGT["⬜ Background Thread
GMainLoop · signal handlers
two-phase dispatch"] + LOG_LIB["🟣 LOG.RDK.FWUPMGR"] + + API --> VAL + VAL --> REG + REG --> BGT + API -.-> LOG_LIB + VAL -.-> LOG_LIB + REG -.-> LOG_LIB + BGT -.-> LOG_LIB + end + + subgraph IPC["⬜ IPC Layer — D-Bus System Bus"] + direction LR + METHODS["Method Calls
RegisterProcess · UnregisterProcess
CheckForUpdate · DownloadFirmware
UpdateFirmware"] + SIGNALS["Signals (Broadcast)
CheckForUpdateComplete
DownloadProgress
UpdateProgress"] + end + + subgraph DAEMON["⬜ rdkFwupdateMgr Daemon"] + direction TB + DBUS_SRV["D-Bus Server
Method handler · Signal emitter"] + PROC_MGR["Process Manager
handler_id tracking
registration table"] + LOG_DMN["🟣 LOG.RDK.FWUPG"] + + DBUS_SRV --> PROC_MGR + DBUS_SRV -.-> LOG_DMN + PROC_MGR -.-> LOG_DMN + end + + subgraph FW["⬜ Firmware Subsystem"] + direction LR + XCONF["XConf Server
(cloud)"] + CDN["CDN
(firmware images)"] + HAL["Device HAL
(flash storage)"] + REBOOT["Reboot Manager"] + end + + P1 --> API + P2 --> API + P3 --> API + + API -- "ephemeral connection
per API call" --> METHODS + BGT -- "persistent connection
signal subscription" --> SIGNALS + + METHODS --> DBUS_SRV + DBUS_SRV --> SIGNALS + + PROC_MGR --> XCONF + PROC_MGR --> CDN + PROC_MGR --> HAL + PROC_MGR --> REBOOT + + style CLIENT fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style LIB fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style IPC fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style DAEMON fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style FW fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + + style API fill:#3b82f6,stroke:#1d4ed8,color:#fff + style VAL fill:#eab308,stroke:#a16207,color:#fff + style REG fill:#9ca3af,stroke:#4b5563,color:#fff + style BGT fill:#9ca3af,stroke:#4b5563,color:#fff + style LOG_LIB fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_DMN fill:#a855f7,stroke:#7e22ce,color:#fff + style DBUS_SRV fill:#9ca3af,stroke:#4b5563,color:#fff + style PROC_MGR fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +### Layer Responsibilities Summary + +```mermaid +graph LR + subgraph LAYERS["System Layers"] + direction TB + L1["🔵 Client Layer
Calls 5 APIs · implements 3 callbacks
owns condvar sync · owns log lifecycle"] + L2["🔵 Library Layer
Validates · Transports · Dispatches
owns handle · owns BG thread"] + L3["⬜ IPC Layer
D-Bus system bus
methods ↓ · signals ↑"] + L4["⬜ Daemon Layer
Orchestrates firmware ops
owns process table · emits signals"] + L5["⬜ Firmware Subsystem
XConf · CDN · HAL · Reboot"] + + L1 --> L2 --> L3 --> L4 --> L5 + end + + style L1 fill:#3b82f6,stroke:#1d4ed8,color:#fff + style L2 fill:#3b82f6,stroke:#1d4ed8,color:#fff + style L3 fill:#d1d5db,stroke:#6b7280,color:#374151 + style L4 fill:#d1d5db,stroke:#6b7280,color:#374151 + style L5 fill:#d1d5db,stroke:#6b7280,color:#374151 +``` + +--- + +## 2. API Flowcharts + +### 2.1 `registerProcess()` + +```mermaid +flowchart TD + START(["🔵 registerProcess(processName, libVersion)"]) + START --> LOG_ENTRY["🟣 FWUPMGR_INFO: Entry with processName"] + + LOG_ENTRY --> V1{"🟡 processName == NULL?"} + V1 -- Yes --> ERR1["🔴 FWUPMGR_ERROR: NULL processName"] + ERR1 --> RET_NULL1(["🔴 return NULL"]) + V1 -- No --> V2{"🟡 processName empty?"} + + V2 -- Yes --> ERR2["🔴 FWUPMGR_ERROR: empty processName"] + ERR2 --> RET_NULL2(["🔴 return NULL"]) + V2 -- No --> V3{"🟡 strlen > 256?"} + + V3 -- Yes --> ERR3["🔴 FWUPMGR_ERROR: name too long"] + ERR3 --> RET_NULL3(["🔴 return NULL"]) + V3 -- No --> V4{"🟡 libVersion == NULL?"} + + V4 -- Yes --> ERR4["🔴 FWUPMGR_ERROR: NULL libVersion"] + ERR4 --> RET_NULL4(["🔴 return NULL"]) + V4 -- No --> V5{"🟡 strlen(libVersion) > 64?"} + + V5 -- Yes --> ERR5["🔴 FWUPMGR_ERROR: version too long"] + ERR5 --> RET_NULL5(["🔴 return NULL"]) + V5 -- No --> DBUS_CONN["⬜ g_bus_get_sync(SYSTEM)"] + + DBUS_CONN --> CONN_OK{"Connection OK?"} + CONN_OK -- No --> ERR6["🔴 FWUPMGR_ERROR: D-Bus connect failed"] + ERR6 --> RET_NULL6(["🔴 return NULL"]) + CONN_OK -- Yes --> PROXY["⬜ g_dbus_proxy_new_sync()"] + + PROXY --> PROXY_OK{"Proxy OK?"} + PROXY_OK -- No --> ERR7["🔴 FWUPMGR_ERROR: proxy creation failed"] + ERR7 --> UNREF_CONN1["⬜ g_object_unref(connection)"] + UNREF_CONN1 --> RET_NULL7(["🔴 return NULL"]) + + PROXY_OK -- Yes --> CALL["⬜ g_dbus_proxy_call_sync
RegisterProcess(ss) → (t)
timeout: 5000ms"] + + CALL --> CALL_OK{"D-Bus reply OK?"} + CALL_OK -- No --> ERR8["🔴 FWUPMGR_ERROR: daemon rejected / timeout"] + ERR8 --> UNREF_PROXY1["⬜ g_object_unref(proxy)"] + UNREF_PROXY1 --> RET_NULL8(["🔴 return NULL"]) + + CALL_OK -- Yes --> EXTRACT["⬜ Extract handler_id (uint64)
from GVariant reply"] + EXTRACT --> UNREF_RES["⬜ Unref result + proxy"] + UNREF_RES --> MALLOC["⬜ malloc(32) for handle string"] + + MALLOC --> MALLOC_OK{"malloc OK?"} + MALLOC_OK -- No --> ROLLBACK["🔴 Best-effort: call UnregisterProcess
to undo daemon-side registration"] + ROLLBACK --> RET_NULL9(["🔴 return NULL"]) + + MALLOC_OK -- Yes --> SNPRINTF["⬜ snprintf(handle, 32, handler_id)"] + SNPRINTF --> INIT["⬜ internal_system_init()"] + + INIT --> INIT_MUT["⬜ Initialize 3 mutexes"] + INIT_MUT --> INIT_REG["⬜ Zero 3 callback registries"] + INIT_REG --> INIT_CTX["⬜ Create GMainContext + GMainLoop"] + INIT_CTX --> INIT_THR["⬜ pthread_create(bg_thread)"] + INIT_THR --> INIT_WAIT["⬜ Spin-wait: bg_thread running? (max 5s)"] + + INIT_WAIT --> LOG_EXIT["🟣 FWUPMGR_INFO: Registered, handle=X"] + LOG_EXIT --> RET_OK(["🟢 return handle"]) + + style START fill:#3b82f6,stroke:#1d4ed8,color:#fff + style RET_OK fill:#22c55e,stroke:#15803d,color:#fff + style RET_NULL1 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL2 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL3 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL4 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL5 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL6 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL7 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL8 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_NULL9 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR1 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR2 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR3 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR4 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR5 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR6 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR7 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR8 fill:#ef4444,stroke:#b91c1c,color:#fff + style ROLLBACK fill:#ef4444,stroke:#b91c1c,color:#fff + style V1 fill:#eab308,stroke:#a16207,color:#fff + style V2 fill:#eab308,stroke:#a16207,color:#fff + style V3 fill:#eab308,stroke:#a16207,color:#fff + style V4 fill:#eab308,stroke:#a16207,color:#fff + style V5 fill:#eab308,stroke:#a16207,color:#fff + style LOG_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_EXIT fill:#a855f7,stroke:#7e22ce,color:#fff + style DBUS_CONN fill:#9ca3af,stroke:#4b5563,color:#fff + style PROXY fill:#9ca3af,stroke:#4b5563,color:#fff + style CALL fill:#9ca3af,stroke:#4b5563,color:#fff + style EXTRACT fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_RES fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_CONN1 fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_PROXY1 fill:#9ca3af,stroke:#4b5563,color:#fff + style MALLOC fill:#9ca3af,stroke:#4b5563,color:#fff + style SNPRINTF fill:#9ca3af,stroke:#4b5563,color:#fff + style INIT fill:#9ca3af,stroke:#4b5563,color:#fff + style INIT_MUT fill:#9ca3af,stroke:#4b5563,color:#fff + style INIT_REG fill:#9ca3af,stroke:#4b5563,color:#fff + style INIT_CTX fill:#9ca3af,stroke:#4b5563,color:#fff + style INIT_THR fill:#9ca3af,stroke:#4b5563,color:#fff + style INIT_WAIT fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +--- + +### 2.2 `checkForUpdate()` + +```mermaid +flowchart TD + START(["🔵 checkForUpdate(handle, callback)"]) + START --> LOG_ENTRY["🟣 FWUPMGR_INFO: checkForUpdate entry"] + + LOG_ENTRY --> V1{"🟡 handle == NULL
or empty?"} + V1 -- Yes --> ERR1["🔴 FWUPMGR_ERROR: invalid handle"] + ERR1 --> RET_FAIL1(["🔴 return CHECK_FOR_UPDATE_FAIL"]) + V1 -- No --> V2{"🟡 callback == NULL?"} + + V2 -- Yes --> ERR2["🔴 FWUPMGR_ERROR: NULL callback"] + ERR2 --> RET_FAIL2(["🔴 return CHECK_FOR_UPDATE_FAIL"]) + V2 -- No --> DBUS["⬜ g_bus_get_sync(SYSTEM)"] + + DBUS --> DBUS_OK{"Connection OK?"} + DBUS_OK -- No --> ERR3["🔴 FWUPMGR_ERROR: D-Bus connect failed"] + ERR3 --> RET_FAIL3(["🔴 return CHECK_FOR_UPDATE_FAIL"]) + + DBUS_OK -- Yes --> LOCK["⬜ pthread_mutex_lock(registry)"] + LOCK --> REG{"⬜ Find IDLE slot
in g_registry
(max 30)"} + REG -- Full --> ERR4["🔴 FWUPMGR_ERROR: registry full"] + ERR4 --> UNLOCK_ERR["⬜ pthread_mutex_unlock"] + UNLOCK_ERR --> UNREF_ERR["⬜ g_object_unref(conn)"] + UNREF_ERR --> RET_FAIL4(["🔴 return CHECK_FOR_UPDATE_FAIL"]) + + REG -- Found --> STORE["⬜ Store callback + handle_key
slot state: IDLE → PENDING"] + STORE --> UNLOCK["⬜ pthread_mutex_unlock"] + + UNLOCK --> FIRE["⬜ g_dbus_connection_call
CheckForUpdate(s handle)
fire-and-forget, timeout=5000ms"] + + FIRE --> UNREF["⬜ g_object_unref(connection)"] + UNREF --> LOG_EXIT["🟣 FWUPMGR_INFO: request sent"] + LOG_EXIT --> RET_OK(["🟢 return CHECK_FOR_UPDATE_SUCCESS"]) + + style START fill:#3b82f6,stroke:#1d4ed8,color:#fff + style RET_OK fill:#22c55e,stroke:#15803d,color:#fff + style RET_FAIL1 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL2 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL3 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL4 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR1 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR2 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR3 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR4 fill:#ef4444,stroke:#b91c1c,color:#fff + style V1 fill:#eab308,stroke:#a16207,color:#fff + style V2 fill:#eab308,stroke:#a16207,color:#fff + style LOG_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_EXIT fill:#a855f7,stroke:#7e22ce,color:#fff + style DBUS fill:#9ca3af,stroke:#4b5563,color:#fff + style LOCK fill:#9ca3af,stroke:#4b5563,color:#fff + style REG fill:#9ca3af,stroke:#4b5563,color:#fff + style STORE fill:#9ca3af,stroke:#4b5563,color:#fff + style UNLOCK fill:#9ca3af,stroke:#4b5563,color:#fff + style UNLOCK_ERR fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_ERR fill:#9ca3af,stroke:#4b5563,color:#fff + style FIRE fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +--- + +### 2.3 `downloadFirmware()` + +```mermaid +flowchart TD + START(["🔵 downloadFirmware(handle, fwdwnlreq, callback)"]) + START --> LOG_ENTRY["🟣 FWUPMGR_INFO: downloadFirmware entry"] + + LOG_ENTRY --> V1{"🟡 handle == NULL
or empty?"} + V1 -- Yes --> ERR1["🔴 FWUPMGR_ERROR: invalid handle"] --> RET_FAIL1(["🔴 return RDKFW_DWNL_FAILED"]) + V1 -- No --> V2{"🟡 fwdwnlreq == NULL?"} + + V2 -- Yes --> ERR2["🔴 FWUPMGR_ERROR: NULL request"] --> RET_FAIL2(["🔴 return RDKFW_DWNL_FAILED"]) + V2 -- No --> V3{"🟡 firmwareName
== NULL or empty?"} + + V3 -- Yes --> ERR3["🔴 FWUPMGR_ERROR: no firmware name"] --> RET_FAIL3(["🔴 return RDKFW_DWNL_FAILED"]) + V3 -- No --> V4{"🟡 callback == NULL?"} + + V4 -- Yes --> ERR4["🔴 FWUPMGR_ERROR: NULL callback"] --> RET_FAIL4(["🔴 return RDKFW_DWNL_FAILED"]) + V4 -- No --> DBUS["⬜ g_bus_get_sync(SYSTEM)"] + + DBUS --> DBUS_OK{"Connection OK?"} + DBUS_OK -- No --> ERR5["🔴 FWUPMGR_ERROR: D-Bus failed"] --> RET_FAIL5(["🔴 return RDKFW_DWNL_FAILED"]) + + DBUS_OK -- Yes --> LOCK["⬜ pthread_mutex_lock(dwnl_registry)"] + LOCK --> REG["⬜ Find/overwrite slot
in g_dwnl_registry
state: IDLE → ACTIVE"] + REG --> UNLOCK["⬜ pthread_mutex_unlock"] + + UNLOCK --> DEFAULT["⬜ Default NULL fields to empty string
url = fwdwnlreq→downloadUrl ?? ''
type = fwdwnlreq→TypeOfFirmware ?? ''"] + + DEFAULT --> FIRE["⬜ g_dbus_connection_call
DownloadFirmware(s handle, s name, s url, s type)
fire-and-forget"] + + FIRE --> UNREF["⬜ g_object_unref(connection)"] + UNREF --> LOG_EXIT["🟣 FWUPMGR_INFO: download request sent"] + LOG_EXIT --> RET_OK(["🟢 return RDKFW_DWNL_SUCCESS"]) + + style START fill:#3b82f6,stroke:#1d4ed8,color:#fff + style RET_OK fill:#22c55e,stroke:#15803d,color:#fff + style RET_FAIL1 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL2 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL3 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL4 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_FAIL5 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR1 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR2 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR3 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR4 fill:#ef4444,stroke:#b91c1c,color:#fff + style ERR5 fill:#ef4444,stroke:#b91c1c,color:#fff + style V1 fill:#eab308,stroke:#a16207,color:#fff + style V2 fill:#eab308,stroke:#a16207,color:#fff + style V3 fill:#eab308,stroke:#a16207,color:#fff + style V4 fill:#eab308,stroke:#a16207,color:#fff + style LOG_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_EXIT fill:#a855f7,stroke:#7e22ce,color:#fff + style DBUS fill:#9ca3af,stroke:#4b5563,color:#fff + style LOCK fill:#9ca3af,stroke:#4b5563,color:#fff + style REG fill:#9ca3af,stroke:#4b5563,color:#fff + style UNLOCK fill:#9ca3af,stroke:#4b5563,color:#fff + style DEFAULT fill:#9ca3af,stroke:#4b5563,color:#fff + style FIRE fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +--- + +### 2.4 `updateFirmware()` + +```mermaid +flowchart TD + START(["🔵 updateFirmware(handle, fwupdatereq, callback)"]) + START --> LOG_ENTRY["🟣 FWUPMGR_INFO: updateFirmware entry"] + + LOG_ENTRY --> V1{"🟡 handle == NULL
or empty?"} + V1 -- Yes --> E1["🔴 FWUPMGR_ERROR: invalid handle"] --> F1(["🔴 return RDKFW_UPDATE_FAILED"]) + V1 -- No --> V2{"🟡 fwupdatereq == NULL?"} + + V2 -- Yes --> E2["🔴 FWUPMGR_ERROR: NULL request"] --> F2(["🔴 return RDKFW_UPDATE_FAILED"]) + V2 -- No --> V3{"🟡 firmwareName
NULL or empty?"} + + V3 -- Yes --> E3["🔴 FWUPMGR_ERROR: missing name"] --> F3(["🔴 return RDKFW_UPDATE_FAILED"]) + V3 -- No --> V4{"🟡 TypeOfFirmware
NULL or empty?"} + + V4 -- Yes --> E4["🔴 FWUPMGR_ERROR: missing type"] --> F4(["🔴 return RDKFW_UPDATE_FAILED"]) + V4 -- No --> V5{"🟡 callback == NULL?"} + + V5 -- Yes --> E5["🔴 FWUPMGR_ERROR: NULL callback"] --> F5(["🔴 return RDKFW_UPDATE_FAILED"]) + V5 -- No --> DBUS["⬜ g_bus_get_sync(SYSTEM)"] + + DBUS --> OK{"Connection OK?"} + OK -- No --> E6["🔴 FWUPMGR_ERROR: D-Bus failed"] --> F6(["🔴 return RDKFW_UPDATE_FAILED"]) + + OK -- Yes --> LOCK["⬜ pthread_mutex_lock(update_registry)"] + LOCK --> REG["⬜ Register callback
state: IDLE → ACTIVE"] + REG --> UNLOCK["⬜ pthread_mutex_unlock"] + + UNLOCK --> CONV["⬜ Convert rebootImmediately
bool → string: 'true'/'false'
Default location to '' if NULL"] + + CONV --> FIRE["⬜ g_dbus_connection_call
UpdateFirmware(s handle, s name,
s location, s type, s reboot)
fire-and-forget"] + + FIRE --> UNREF["⬜ g_object_unref(connection)"] + UNREF --> LOG_EXIT["🟣 FWUPMGR_INFO: update request sent"] + LOG_EXIT --> RET_OK(["🟢 return RDKFW_UPDATE_SUCCESS"]) + + style START fill:#3b82f6,stroke:#1d4ed8,color:#fff + style RET_OK fill:#22c55e,stroke:#15803d,color:#fff + style F1 fill:#ef4444,stroke:#b91c1c,color:#fff + style F2 fill:#ef4444,stroke:#b91c1c,color:#fff + style F3 fill:#ef4444,stroke:#b91c1c,color:#fff + style F4 fill:#ef4444,stroke:#b91c1c,color:#fff + style F5 fill:#ef4444,stroke:#b91c1c,color:#fff + style F6 fill:#ef4444,stroke:#b91c1c,color:#fff + style E1 fill:#ef4444,stroke:#b91c1c,color:#fff + style E2 fill:#ef4444,stroke:#b91c1c,color:#fff + style E3 fill:#ef4444,stroke:#b91c1c,color:#fff + style E4 fill:#ef4444,stroke:#b91c1c,color:#fff + style E5 fill:#ef4444,stroke:#b91c1c,color:#fff + style E6 fill:#ef4444,stroke:#b91c1c,color:#fff + style V1 fill:#eab308,stroke:#a16207,color:#fff + style V2 fill:#eab308,stroke:#a16207,color:#fff + style V3 fill:#eab308,stroke:#a16207,color:#fff + style V4 fill:#eab308,stroke:#a16207,color:#fff + style V5 fill:#eab308,stroke:#a16207,color:#fff + style LOG_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_EXIT fill:#a855f7,stroke:#7e22ce,color:#fff + style DBUS fill:#9ca3af,stroke:#4b5563,color:#fff + style LOCK fill:#9ca3af,stroke:#4b5563,color:#fff + style REG fill:#9ca3af,stroke:#4b5563,color:#fff + style UNLOCK fill:#9ca3af,stroke:#4b5563,color:#fff + style CONV fill:#9ca3af,stroke:#4b5563,color:#fff + style FIRE fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +--- + +### 2.5 `unregisterProcess()` + +```mermaid +flowchart TD + START(["🔵 unregisterProcess(handle)"]) + START --> V1{"🟡 handle == NULL?"} + + V1 -- Yes --> LOG_NULL["🟣 FWUPMGR_INFO: NULL handle, no-op"] + LOG_NULL --> RET_VOID1(["🟢 return (void)"]) + + V1 -- No --> PARSE["⬜ strtoull(handle) → handler_id"] + PARSE --> PARSE_OK{"🟡 Parse valid?
strict endptr check"} + + PARSE_OK -- No --> ERR1["🔴 FWUPMGR_ERROR: invalid handle format"] + ERR1 --> FREE_HANDLE_ERR["⬜ free(handle)"] + FREE_HANDLE_ERR --> RET_VOID2(["🔴 return (void)"]) + + PARSE_OK -- Yes --> LOG_DEINIT["🟣 FWUPMGR_INFO: deinit starting"] + LOG_DEINIT --> DEINIT["⬜ internal_system_deinit()"] + + DEINIT --> QUIT["⬜ g_main_loop_quit()"] + QUIT --> JOIN["⬜ pthread_join(bg_thread)"] + JOIN --> UNREF_LOOP["⬜ g_main_loop_unref()
g_main_context_unref()"] + UNREF_LOOP --> FREE_DWNL["⬜ internal_dwnl_system_deinit()"] + FREE_DWNL --> FREE_UPD["⬜ internal_update_system_deinit()"] + FREE_UPD --> FREE_REG["⬜ Free check registry handle_keys"] + FREE_REG --> DESTROY_MTX["⬜ pthread_mutex_destroy() × 4"] + + DESTROY_MTX --> PROXY["⬜ Create D-Bus proxy
(best-effort)"] + PROXY --> PROXY_OK{"Proxy OK?"} + + PROXY_OK -- No --> LOG_WARN["🟣 FWUPMGR_WARN: daemon unreachable"] + LOG_WARN --> FREE_HANDLE2["⬜ free(handle)"] + FREE_HANDLE2 --> RET_VOID3(["🟢 return (void)"]) + + PROXY_OK -- Yes --> CALL["⬜ g_dbus_proxy_call_sync
UnregisterProcess(t handler_id)"] + CALL --> CALL_OK{"D-Bus OK?"} + + CALL_OK -- No --> LOG_WARN2["🟣 FWUPMGR_WARN: unregister call failed"] + LOG_WARN2 --> UNREF_P2["⬜ g_object_unref(proxy)"] + UNREF_P2 --> FREE_HANDLE3["⬜ free(handle)"] + FREE_HANDLE3 --> RET_VOID4(["🟢 return (void)"]) + + CALL_OK -- Yes --> UNREF_ALL["⬜ g_object_unref(result + proxy)"] + UNREF_ALL --> LOG_OK["🟣 FWUPMGR_INFO: unregistered OK"] + LOG_OK --> FREE_HANDLE4["⬜ free(handle)"] + FREE_HANDLE4 --> RET_VOID5(["🟢 return (void)"]) + + style START fill:#3b82f6,stroke:#1d4ed8,color:#fff + style RET_VOID1 fill:#22c55e,stroke:#15803d,color:#fff + style RET_VOID2 fill:#ef4444,stroke:#b91c1c,color:#fff + style RET_VOID3 fill:#22c55e,stroke:#15803d,color:#fff + style RET_VOID4 fill:#22c55e,stroke:#15803d,color:#fff + style RET_VOID5 fill:#22c55e,stroke:#15803d,color:#fff + style ERR1 fill:#ef4444,stroke:#b91c1c,color:#fff + style V1 fill:#eab308,stroke:#a16207,color:#fff + style PARSE_OK fill:#eab308,stroke:#a16207,color:#fff + style LOG_NULL fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_DEINIT fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_WARN fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_WARN2 fill:#a855f7,stroke:#7e22ce,color:#fff + style LOG_OK fill:#a855f7,stroke:#7e22ce,color:#fff + style PARSE fill:#9ca3af,stroke:#4b5563,color:#fff + style DEINIT fill:#9ca3af,stroke:#4b5563,color:#fff + style QUIT fill:#9ca3af,stroke:#4b5563,color:#fff + style JOIN fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_LOOP fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_DWNL fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_UPD fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_REG fill:#9ca3af,stroke:#4b5563,color:#fff + style DESTROY_MTX fill:#9ca3af,stroke:#4b5563,color:#fff + style PROXY fill:#9ca3af,stroke:#4b5563,color:#fff + style CALL fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_ALL fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_HANDLE_ERR fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_HANDLE2 fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_HANDLE3 fill:#9ca3af,stroke:#4b5563,color:#fff + style FREE_HANDLE4 fill:#9ca3af,stroke:#4b5563,color:#fff + style UNREF_P2 fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +--- + +## 3. Sequence Diagrams + +### 3.1 Complete Firmware Update Flow + +```mermaid +sequenceDiagram + autonumber + participant App as 🔵 Client App + participant Lib as 🔵 Library API + participant BG as ⬜ BG Thread + participant Bus as ⬜ D-Bus + participant Dmn as ⬜ Daemon + participant XConf as ⬜ XConf Cloud + participant CDN as ⬜ CDN + participant HAL as ⬜ Device HAL + + Note over App,HAL: Phase 1 — Registration + + App->>+Lib: registerProcess("MyPlugin", "1.0") + Lib-->>Lib: 🟡 validate inputs + Lib->>+Bus: RegisterProcess(ss) + Bus->>+Dmn: RegisterProcess + Dmn-->>Dmn: assign handler_id=12345 + Dmn->>-Bus: reply (t) 12345 + Bus->>-Lib: GVariant reply + Lib-->>Lib: ⬜ malloc handle, internal_system_init() + Lib-->>BG: ⬜ pthread_create + BG-->>Bus: subscribe to 3 signals + BG-->>BG: ⬜ g_main_loop_run() + Lib->>-App: 🟢 handle "12345" + + Note over App,HAL: Phase 2 — Check for Update + + App->>+Lib: checkForUpdate(handle, my_cb) + Lib-->>Lib: 🟡 validate handle + callback + Lib-->>Lib: ⬜ register cb in g_registry [PENDING] + Lib->>Bus: CheckForUpdate(s "12345") [fire-and-forget] + Lib->>-App: 🟢 CHECK_FOR_UPDATE_SUCCESS + App-->>App: pthread_cond_timedwait (120s) + + Bus->>Dmn: CheckForUpdate + Dmn->>+XConf: HTTP GET /xconf?model=... + XConf->>-Dmn: firmware_v2.bin available + + Dmn->>Bus: signal: CheckForUpdateComplete(t,i,i,s,s,s,s) + Bus->>BG: deliver signal + BG-->>BG: ⬜ on_check_complete_signal() + BG-->>BG: ⬜ dispatch_all_pending() + BG->>App: 🟢 my_cb(&fwinfo) [status=AVAILABLE] + App-->>App: pthread_cond_signal (wake main) + + Note over App,HAL: Phase 3 — Download + + App->>+Lib: downloadFirmware(handle, req, dl_cb) + Lib-->>Lib: 🟡 validate inputs + Lib-->>Lib: ⬜ register dl_cb in g_dwnl_registry [ACTIVE] + Lib->>Bus: DownloadFirmware(ssss) [fire-and-forget] + Lib->>-App: 🟢 RDKFW_DWNL_SUCCESS + App-->>App: pthread_cond_timedwait (300s) + + Bus->>Dmn: DownloadFirmware + Dmn->>+CDN: HTTPS GET firmware_v2.bin + + loop Every progress update + CDN-->>Dmn: chunk received + Dmn->>Bus: signal: DownloadProgress(t,s,u,s,s) + Bus->>BG: deliver signal + BG->>App: dl_cb(progress%, IN_PROGRESS) + end + + CDN->>-Dmn: download complete + Dmn->>Bus: signal: DownloadProgress(100, COMPLETED) + Bus->>BG: deliver signal + BG->>App: 🟢 dl_cb(100, COMPLETED) + App-->>App: pthread_cond_signal + + Note over App,HAL: Phase 4 — Flash Update + + App->>+Lib: updateFirmware(handle, req, upd_cb) + Lib-->>Lib: 🟡 validate inputs + Lib-->>Lib: ⬜ register upd_cb in g_update_registry [ACTIVE] + Lib->>Bus: UpdateFirmware(sssss) [fire-and-forget] + Lib->>-App: 🟢 RDKFW_UPDATE_SUCCESS + App-->>App: pthread_cond_timedwait (600s) + + Bus->>Dmn: UpdateFirmware + Dmn->>+HAL: flash firmware_v2.bin + + loop Flash progress + HAL-->>Dmn: partition written + Dmn->>Bus: signal: UpdateProgress(t,s,i,i,s) + Bus->>BG: deliver signal + BG->>App: upd_cb(progress%, IN_PROGRESS) + end + + HAL->>-Dmn: flash complete + Dmn->>Bus: signal: UpdateProgress(100, COMPLETED) + Bus->>BG: deliver signal + BG->>App: 🟢 upd_cb(100, COMPLETED) + + Note over App,HAL: Phase 5 — Cleanup + + App->>+Lib: unregisterProcess(handle) + Lib-->>Lib: ⬜ internal_system_deinit() + Lib-->>BG: g_main_loop_quit() + BG-->>Lib: thread exits + Lib-->>Lib: ⬜ pthread_join, free registries + Lib->>Bus: UnregisterProcess(t 12345) + Bus->>Dmn: UnregisterProcess + Dmn-->>Dmn: remove ProcessInfo + Lib-->>Lib: ⬜ free(handle) + Lib->>-App: 🟢 return (void) +``` + +--- + +### 3.2 Daemon Unavailable + Retry + +```mermaid +sequenceDiagram + autonumber + participant App as 🔵 Client App + participant Lib as 🔵 Library + participant Bus as ⬜ D-Bus + + Note over App,Bus: Attempt 1 — Daemon not running + + App->>+Lib: registerProcess("MyPlugin", "1.0") + Lib-->>Lib: 🟡 validate inputs — OK + Lib->>+Bus: g_bus_get_sync(SYSTEM) + Bus->>-Lib: connection OK + Lib->>+Bus: g_dbus_proxy_new_sync(org.rdkfwupdater.Service) + Bus->>-Lib: 🔴 Error: "name not provided by any .service files" + Lib-->>Lib: 🟣 FWUPMGR_ERROR: proxy creation failed + Lib-->>Lib: ⬜ g_object_unref(connection) + Lib->>-App: 🔴 return NULL + + App-->>App: Log "daemon not running" + App-->>App: sleep(5) + + Note over App,Bus: Attempt 2 — Daemon started between retries + + App->>+Lib: registerProcess("MyPlugin", "1.0") + Lib-->>Lib: 🟡 validate inputs — OK + Lib->>+Bus: g_bus_get_sync(SYSTEM) + Bus->>-Lib: connection OK + Lib->>+Bus: g_dbus_proxy_new_sync(org.rdkfwupdater.Service) + Bus->>-Lib: 🟢 proxy OK + Lib->>+Bus: RegisterProcess(ss) + Bus->>-Lib: 🟢 handler_id = 42 + Lib-->>Lib: ⬜ internal_system_init() + Lib->>-App: 🟢 handle "42" + App-->>App: proceed with firmware operations +``` + +--- + +### 3.3 Callback Registration & Delivery + +```mermaid +sequenceDiagram + autonumber + participant Caller as 🔵 Caller Thread + participant API as 🔵 API Layer + participant Reg as ⬜ Registry (mutex) + participant Bus as ⬜ D-Bus + participant BG as ⬜ BG Thread + participant Dmn as ⬜ Daemon + + Note over Caller,Dmn: Step 1 — Register callback BEFORE sending D-Bus call + + Caller->>+API: checkForUpdate(handle, my_cb) + API->>+Reg: 🔒 lock(g_registry.mutex) + Reg-->>Reg: find IDLE slot + Reg-->>Reg: store {callback=my_cb, handle_key=handle, state=PENDING} + API->>-Reg: 🔓 unlock + API->>Bus: fire-and-forget: CheckForUpdate(handle) + API->>-Caller: 🟢 SUCCESS + + Note over Caller,Dmn: Step 2 — Signal arrives, two-phase dispatch + + Dmn->>Bus: signal: CheckForUpdateComplete(handler_id, ...) + Bus->>+BG: on_check_complete_signal() + + BG->>+Reg: 🔒 lock(g_registry.mutex) + Note over BG,Reg: Phase 1: Snapshot matching entries
Copy callback pointers + data to local array
Mark slots DISPATCHED + BG->>-Reg: 🔓 unlock + + Note over BG: Phase 2: Dispatch WITHOUT holding lock + BG->>Caller: my_cb(&fwinfo) — runs in BG thread context + Note over Caller: Callback copies data, signals condvar + + BG->>+Reg: 🔒 lock(g_registry.mutex) + Note over BG,Reg: Phase 3: Reset dispatched slots to IDLE + BG->>-Reg: 🔓 unlock + deactivate BG +``` + +--- + +### 3.4 Timeout Recovery + +```mermaid +sequenceDiagram + autonumber + participant App as 🔵 Client App + participant Lib as 🔵 Library + participant BG as ⬜ BG Thread + participant Dmn as ⬜ Daemon + + App->>Lib: checkForUpdate(handle, cb) + Lib-->>Lib: register cb [PENDING] + Lib->>Dmn: CheckForUpdate (fire-and-forget) + Lib->>App: 🟢 SUCCESS + + App-->>App: pthread_cond_timedwait(120s) + + Note over Dmn: Daemon is overloaded or
XConf is unreachable + + Note over App: ⏰ 120 seconds pass...
No signal arrives + + App-->>App: 🔴 timedwait returns ETIMEDOUT + + Note over App: Recovery: clean up and retry + + App->>+Lib: unregisterProcess(handle) + Lib-->>Lib: internal_system_deinit() + Lib-->>BG: g_main_loop_quit() + BG-->>Lib: thread exits + Lib-->>Lib: free registries (stale PENDING cleared) + Lib->>Dmn: UnregisterProcess + Lib-->>Lib: free(handle) + Lib->>-App: return + + App-->>App: sleep(30) — backoff + + App->>+Lib: registerProcess("MyPlugin", "1.0") + Lib->>Dmn: RegisterProcess + Dmn->>Lib: handler_id=99 + Lib-->>Lib: internal_system_init() + Lib->>-App: 🟢 handle "99" + + App->>Lib: checkForUpdate("99", cb) + Lib->>Dmn: CheckForUpdate + Lib->>App: 🟢 SUCCESS + App-->>App: pthread_cond_timedwait(120s) + + Dmn->>BG: signal: CheckForUpdateComplete + BG->>App: 🟢 cb(&fwinfo) + App-->>App: condvar signaled — success +``` + +--- + +## 4. Thread Safety Diagram + +### 4.1 Multi-Client Shared State Map + +```mermaid +graph TB + subgraph PROCESS["Single Process Address Space"] + subgraph MAIN["🔵 Main Thread"] + MT_REG["registerProcess()"] + MT_CHECK["checkForUpdate()"] + MT_DWNL["downloadFirmware()"] + MT_UPD["updateFirmware()"] + MT_UNREG["unregisterProcess()"] + end + + subgraph WORKER["🔵 Worker Thread (optional)"] + WT_CHECK["checkForUpdate()"] + WT_DWNL["downloadFirmware()"] + end + + subgraph SHARED["⬜ Shared State (mutex-protected)"] + subgraph REG1["g_registry
🔒 registry_mutex"] + R1_S["slots[0..29]
state | callback | handle_key"] + end + subgraph REG2["g_dwnl_registry
🔒 dwnl_mutex"] + R2_S["slots[0..29]
state | callback | handle_key"] + end + subgraph REG3["g_update_registry
🔒 update_mutex"] + R3_S["slots[0..29]
state | callback | handle_key"] + end + end + + subgraph BG_THREAD["⬜ Background Thread"] + LOOP["g_main_loop_run()"] + SIG1["on_check_complete_signal()"] + SIG2["on_download_progress_signal()"] + SIG3["on_update_progress_signal()"] + LOOP --> SIG1 + LOOP --> SIG2 + LOOP --> SIG3 + end + + MT_CHECK -- "🔒 lock → store → unlock" --> REG1 + WT_CHECK -- "🔒 lock → store → unlock" --> REG1 + SIG1 -- "🔒 lock → snapshot → unlock
dispatch WITHOUT lock
🔒 lock → reset → unlock" --> REG1 + + MT_DWNL -- "🔒 lock → store → unlock" --> REG2 + WT_DWNL -- "🔒 lock → store → unlock" --> REG2 + SIG2 -- "🔒 lock → snapshot → unlock
dispatch WITHOUT lock
🔒 lock → reset → unlock" --> REG2 + + MT_UPD -- "🔒 lock → store → unlock" --> REG3 + SIG3 -- "🔒 lock → snapshot → unlock
dispatch WITHOUT lock
🔒 lock → reset → unlock" --> REG3 + end + + style MAIN fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style WORKER fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style SHARED fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style BG_THREAD fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style REG1 fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f + style REG2 fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f + style REG3 fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f +``` + +### 4.2 Two-Phase Dispatch (Deadlock Prevention) + +```mermaid +flowchart LR + subgraph PHASE1["Phase 1: Under Lock"] + direction TB + P1_LOCK["🔒 mutex_lock"] + P1_SCAN["Scan slots:
find matching handler_id"] + P1_COPY["Copy callback ptr + data
to stack-local snapshot[]"] + P1_MARK["Mark slot DISPATCHED"] + P1_UNLOCK["🔓 mutex_unlock"] + P1_LOCK --> P1_SCAN --> P1_COPY --> P1_MARK --> P1_UNLOCK + end + + subgraph PHASE2["Phase 2: No Lock Held"] + direction TB + P2_LOOP["for each snapshot entry"] + P2_CALL["snapshot[i].callback(data)"] + P2_NOTE["Client callback executes
freely — can call any API
NO DEADLOCK RISK"] + P2_LOOP --> P2_CALL --> P2_NOTE + end + + subgraph PHASE3["Phase 3: Under Lock"] + direction TB + P3_LOCK["🔒 mutex_lock"] + P3_RESET["Reset DISPATCHED → IDLE
free handle_key"] + P3_UNLOCK["🔓 mutex_unlock"] + P3_LOCK --> P3_RESET --> P3_UNLOCK + end + + PHASE1 --> PHASE2 --> PHASE3 + + style PHASE1 fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f + style PHASE2 fill:#dcfce7,stroke:#16a34a,stroke-width:2px,color:#14532d + style PHASE3 fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#78350f +``` + +### 4.3 Connection Model — Why Each Call Is Independent + +```mermaid +sequenceDiagram + participant App as 🔵 Client + participant Lib as 🔵 Library + participant Bus as ⬜ D-Bus + + Note over App,Bus: Each API call creates + destroys its own connection + + App->>Lib: registerProcess() + Lib->>+Bus: g_bus_get_sync() → conn_1 (sender :1.140) + Lib->>Bus: RegisterProcess via conn_1 + Lib->>-Bus: g_object_unref(conn_1) — destroyed + + App->>Lib: checkForUpdate() + Lib->>+Bus: g_bus_get_sync() → conn_2 (sender :1.141) + Lib->>Bus: CheckForUpdate via conn_2 + Lib->>-Bus: g_object_unref(conn_2) — destroyed + + App->>Lib: downloadFirmware() + Lib->>+Bus: g_bus_get_sync() → conn_3 (sender :1.142) + Lib->>Bus: DownloadFirmware via conn_3 + Lib->>-Bus: g_object_unref(conn_3) — destroyed + + Note over Lib,Bus: BG Thread has separate PERSISTENT connection
for signal subscription (lives entire session) +``` + +--- + +## 5. Memory Ownership Diagram + +### 5.1 Ownership Boundaries + +```mermaid +graph TB + subgraph CALLER["🔵 Caller Allocates & Owns"] + C1["processName (const char*)
passed to registerProcess()"] + C2["libVersion (const char*)
passed to registerProcess()"] + C3["FwDwnlReq struct
passed to downloadFirmware()"] + C4["FwUpdateReq struct
passed to updateFirmware()"] + C5["callback function pointers"] + C6["condvar / mutex
for synchronization"] + C7["Copies of callback data
(strncpy'd inside callback)"] + end + + subgraph LIBRARY["🔵 Library Allocates & Owns"] + L1["FirmwareInterfaceHandle
malloc(32) in registerProcess()"] + L2["handle_key strings
strdup'd in registry slots"] + L3["CallbackRegistry
(static global array)"] + L4["DwnlCallbackRegistry
(static global array)"] + L5["UpdateCbRegistry
(static global array)"] + L6["GMainContext
+ GMainLoop"] + L7["Background pthread"] + L8["FwInfoData
(stack in dispatch — transient!)"] + L9["UpdateDetails
(stack in dispatch — transient!)"] + end + + subgraph DAEMON["⬜ Daemon Allocates & Owns"] + D1["handler_id counter
(uint64)"] + D2["ProcessInfo records
(GHashTable)"] + D3["GVariant signal payloads
(auto-freed after emission)"] + D4["Downloaded firmware file
(on-disk)"] + end + + subgraph DBUS["⬜ D-Bus / GLib Manages"] + B1["GDBusConnection
(per-call ephemeral)"] + B2["GDBusProxy objects"] + B3["GVariant method args
+ reply data"] + B4["BG thread persistent
GDBusConnection"] + end + + L1 -. "freed by
unregisterProcess()" .-> FREE1(("free()")) + L2 -. "freed on slot reset" .-> FREE2(("free()")) + L6 -. "unref'd in
internal_system_deinit()" .-> FREE3(("unref")) + B1 -. "unref'd after each API call" .-> FREE4(("unref")) + B2 -. "unref'd after each API call" .-> FREE5(("unref")) + + style CALLER fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style LIBRARY fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style DAEMON fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style DBUS fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style FREE1 fill:#ef4444,stroke:#b91c1c,color:#fff + style FREE2 fill:#ef4444,stroke:#b91c1c,color:#fff + style FREE3 fill:#ef4444,stroke:#b91c1c,color:#fff + style FREE4 fill:#ef4444,stroke:#b91c1c,color:#fff + style FREE5 fill:#ef4444,stroke:#b91c1c,color:#fff +``` + +### 5.2 Callback Data Lifetime + +```mermaid +flowchart LR + subgraph SIGNAL["Signal Arrives"] + SIG["D-Bus signal
GVariant payload"] + end + + subgraph PARSE["BG Thread Parses"] + P1["g_variant_get()
extract fields"] + P2["Populate FwInfoData
on STACK"] + P3["Populate UpdateDetails
on STACK"] + end + + subgraph DISPATCH["Callback Executes"] + CB["client_callback(&fwinfo)"] + COPY["🟢 Client MUST copy
any needed data NOW"] + WARN["🔴 After callback returns:
all pointers are INVALID"] + end + + subgraph CLEANUP["Stack Unwinds"] + CL["FwInfoData destroyed
UpdateDetails destroyed
GVariant unreffed"] + end + + SIG --> PARSE --> DISPATCH --> CLEANUP + + style SIGNAL fill:#f3f4f6,stroke:#6b7280,color:#374151 + style PARSE fill:#f3f4f6,stroke:#6b7280,color:#374151 + style DISPATCH fill:#dbeafe,stroke:#2563eb,color:#1e3a5f + style CLEANUP fill:#fecaca,stroke:#dc2626,color:#7f1d1d + style COPY fill:#22c55e,stroke:#15803d,color:#fff + style WARN fill:#ef4444,stroke:#b91c1c,color:#fff +``` + +### 5.3 Cleanup Sequence + +```mermaid +flowchart TD + UNREG(["🔵 unregisterProcess(handle)"]) + UNREG --> DEINIT["⬜ internal_system_deinit()"] + + DEINIT --> S1["⬜ g_main_loop_quit()
→ BG thread wakes up"] + S1 --> S2["⬜ pthread_join()
→ BG thread fully stopped"] + S2 --> S3["⬜ g_main_loop_unref()
g_main_context_unref()"] + S3 --> S4["⬜ internal_dwnl_system_deinit()
→ free dwnl registry handle_keys
→ destroy dwnl_mutex"] + S4 --> S5["⬜ internal_update_system_deinit()
→ free update registry handle_keys
→ destroy update_mutex"] + S5 --> S6["⬜ Free check registry handle_keys"] + S6 --> S7["⬜ pthread_mutex_destroy × 4
(bg_thread, registry, dwnl, update)"] + S7 --> S8["⬜ D-Bus: UnregisterProcess
(best-effort)"] + S8 --> S9["⬜ free(handle)
🔴 handle pointer now INVALID"] + S9 --> DONE(["🟢 All resources released"]) + + style UNREG fill:#3b82f6,stroke:#1d4ed8,color:#fff + style DONE fill:#22c55e,stroke:#15803d,color:#fff + style S9 fill:#ef4444,stroke:#b91c1c,color:#fff + style DEINIT fill:#9ca3af,stroke:#4b5563,color:#fff + style S1 fill:#9ca3af,stroke:#4b5563,color:#fff + style S2 fill:#9ca3af,stroke:#4b5563,color:#fff + style S3 fill:#9ca3af,stroke:#4b5563,color:#fff + style S4 fill:#9ca3af,stroke:#4b5563,color:#fff + style S5 fill:#9ca3af,stroke:#4b5563,color:#fff + style S6 fill:#9ca3af,stroke:#4b5563,color:#fff + style S7 fill:#9ca3af,stroke:#4b5563,color:#fff + style S8 fill:#9ca3af,stroke:#4b5563,color:#fff +``` + +--- + +## 6. Logging Pipeline Diagram + +### 6.1 Three-Module Logging Architecture + +```mermaid +graph TB + subgraph SOURCES["Log Sources"] + direction TB + subgraph CLIENT_LOG["🔵 Client Application"] + EX_INFO["EXAMPLE_INFO(...)"] + EX_ERR["EXAMPLE_ERROR(...)"] + EX_DBG["EXAMPLE_DEBUG(...)"] + end + + subgraph LIB_LOG["🔵 Library Internals"] + FW_INFO["FWUPMGR_INFO(...)"] + FW_ERR["FWUPMGR_ERROR(...)"] + FW_WARN["FWUPMGR_WARN(...)"] + FW_DBG["FWUPMGR_DEBUG(...)"] + end + + subgraph DMN_LOG["⬜ Daemon"] + SW_INFO["SWLOG_INFO(...)"] + SW_ERR["SWLOG_ERROR(...)"] + end + end + + subgraph MACROS["🟣 Macro Expansion Layer"] + M_CLIENT["Module: LOG.RDK.EXAMPLE"] + M_LIB["Module: LOG.RDK.FWUPMGR"] + M_DMN["Module: LOG.RDK.FWUPG"] + end + + subgraph BACKEND["🟣 Logging Backend"] + RDK{"RDK_LOGGER
defined?"} + YES["RDK_LOG(level, module, fmt, ...)
→ rdk_logger subsystem"] + NO["fprintf(stderr, [module] fmt, ...)
→ console fallback"] + end + + subgraph OUTPUT["🟣 Log Output"] + FILE["/opt/logs/rdkFwupdateMgr.log"] + CONSOLE["stderr (unit tests)"] + end + + EX_INFO & EX_ERR & EX_DBG --> M_CLIENT + FW_INFO & FW_ERR & FW_WARN & FW_DBG --> M_LIB + SW_INFO & SW_ERR --> M_DMN + + M_CLIENT & M_LIB & M_DMN --> RDK + + RDK -- "Yes (production)" --> YES --> FILE + RDK -- "No (unit test)" --> NO --> CONSOLE + + style CLIENT_LOG fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style LIB_LOG fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style DMN_LOG fill:#f3f4f6,stroke:#6b7280,stroke-width:2px,color:#374151 + style MACROS fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87 + style BACKEND fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87 + style OUTPUT fill:#f3e8ff,stroke:#9333ea,stroke-width:2px,color:#581c87 + style RDK fill:#a855f7,stroke:#7e22ce,color:#fff +``` + +### 6.2 Log Points by API Function + +```mermaid +graph LR + subgraph REGISTER["registerProcess()"] + R_ENTRY["🟣 INFO: Entry
(processName)"] + R_VAL["🟣 ERROR: Validation
failure details"] + R_DBUS["🟣 ERROR: D-Bus
connection/call failure"] + R_ID["🟣 INFO: Got handler_id"] + R_INIT["🟣 INFO: BG thread started"] + R_OK["🟣 INFO: Registered
handle=X"] + end + + subgraph CHECK["checkForUpdate()"] + C_ENTRY["🟣 INFO: Entry
(handle)"] + C_VAL["🟣 ERROR: Bad handle
or NULL callback"] + C_REG["🟣 DEBUG: Slot allocated
in registry"] + C_SEND["🟣 INFO: Request sent"] + end + + subgraph DOWNLOAD["downloadFirmware()"] + D_ENTRY["🟣 INFO: Entry
(handle, firmwareName)"] + D_VAL["🟣 ERROR: Missing
required fields"] + D_REG["🟣 DEBUG: Slot allocated
in dwnl_registry"] + D_SEND["🟣 INFO: Request sent"] + end + + subgraph UPDATE["updateFirmware()"] + U_ENTRY["🟣 INFO: Entry
(handle, firmwareName)"] + U_VAL["🟣 ERROR: Missing
name or type"] + U_REG["🟣 DEBUG: Slot allocated"] + U_SEND["🟣 INFO: Request sent"] + end + + subgraph UNREGISTER["unregisterProcess()"] + X_ENTRY["🟣 INFO: Entry
(handle)"] + X_PARSE["🟣 ERROR: Invalid
handle format"] + X_DEINIT["🟣 INFO: Deinit
starting"] + X_WARN["🟣 WARN: D-Bus call
failed (best-effort)"] + X_OK["🟣 INFO: Unregistered OK"] + end + + subgraph BG_SIGNALS["Background Thread Signals"] + S_RECV["🟣 DEBUG: Signal received
(handler_id, type)"] + S_DISPATCH["🟣 DEBUG: Dispatching
N callbacks"] + S_RESET["🟣 DEBUG: Slot reset
to IDLE"] + end + + style REGISTER fill:#dbeafe,stroke:#2563eb,color:#1e3a5f + style CHECK fill:#dbeafe,stroke:#2563eb,color:#1e3a5f + style DOWNLOAD fill:#dbeafe,stroke:#2563eb,color:#1e3a5f + style UPDATE fill:#dbeafe,stroke:#2563eb,color:#1e3a5f + style UNREGISTER fill:#dbeafe,stroke:#2563eb,color:#1e3a5f + style BG_SIGNALS fill:#f3f4f6,stroke:#6b7280,color:#374151 + + style R_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style R_VAL fill:#a855f7,stroke:#7e22ce,color:#fff + style R_DBUS fill:#a855f7,stroke:#7e22ce,color:#fff + style R_ID fill:#a855f7,stroke:#7e22ce,color:#fff + style R_INIT fill:#a855f7,stroke:#7e22ce,color:#fff + style R_OK fill:#a855f7,stroke:#7e22ce,color:#fff + style C_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style C_VAL fill:#a855f7,stroke:#7e22ce,color:#fff + style C_REG fill:#a855f7,stroke:#7e22ce,color:#fff + style C_SEND fill:#a855f7,stroke:#7e22ce,color:#fff + style D_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style D_VAL fill:#a855f7,stroke:#7e22ce,color:#fff + style D_REG fill:#a855f7,stroke:#7e22ce,color:#fff + style D_SEND fill:#a855f7,stroke:#7e22ce,color:#fff + style U_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style U_VAL fill:#a855f7,stroke:#7e22ce,color:#fff + style U_REG fill:#a855f7,stroke:#7e22ce,color:#fff + style U_SEND fill:#a855f7,stroke:#7e22ce,color:#fff + style X_ENTRY fill:#a855f7,stroke:#7e22ce,color:#fff + style X_PARSE fill:#a855f7,stroke:#7e22ce,color:#fff + style X_DEINIT fill:#a855f7,stroke:#7e22ce,color:#fff + style X_WARN fill:#a855f7,stroke:#7e22ce,color:#fff + style X_OK fill:#a855f7,stroke:#7e22ce,color:#fff + style S_RECV fill:#a855f7,stroke:#7e22ce,color:#fff + style S_DISPATCH fill:#a855f7,stroke:#7e22ce,color:#fff + style S_RESET fill:#a855f7,stroke:#7e22ce,color:#fff +``` + +### 6.3 Correlation — Tracing a Request by handler_id + +```mermaid +sequenceDiagram + participant App as 🔵 Client + participant Lib as 🔵 Library + participant Log as 🟣 Log File + participant Dmn as ⬜ Daemon + + App->>Lib: registerProcess("MyPlugin", "1.0") + Lib->>Log: [FWUPMGR] INFO: registerProcess entry, processName=MyPlugin + Lib->>Dmn: RegisterProcess + Dmn->>Log: [FWUPG] INFO: Registered MyPlugin, handler_id=12345 + Lib->>Log: [FWUPMGR] INFO: Got handler_id=12345 + Lib->>App: handle "12345" + App->>Log: [EXAMPLE] INFO: Registered with handle 12345 + + Note over Log: All subsequent logs include handler_id=12345
for end-to-end request correlation + + App->>Lib: checkForUpdate("12345", cb) + Lib->>Log: [FWUPMGR] INFO: checkForUpdate, handle=12345 + Lib->>Log: [FWUPMGR] DEBUG: registry slot 3 allocated, handler_id=12345 + Lib->>Dmn: CheckForUpdate("12345") + Dmn->>Log: [FWUPG] INFO: CheckForUpdate for handler_id=12345, querying XConf + + Note over Dmn: XConf responds + + Dmn->>Log: [FWUPG] INFO: CheckForUpdate result for 12345: AVAILABLE + Dmn->>Lib: signal CheckForUpdateComplete(12345, ...) + Lib->>Log: [FWUPMGR] DEBUG: signal received, handler_id=12345 + Lib->>Log: [FWUPMGR] DEBUG: dispatching 1 callback(s) for handler_id=12345 + Lib->>App: cb(&fwinfo) + App->>Log: [EXAMPLE] INFO: Firmware available for handle 12345 +``` + +### 6.4 Log Lifecycle Ownership + +```mermaid +flowchart TD + subgraph APP["🔵 Client Application (caller's responsibility)"] + INIT["log_init()
⚠️ MUST call before any library API"] + USE["Use library APIs
(all logging works)"] + EXIT["log_exit()
⚠️ MUST call after unregisterProcess()"] + INIT --> USE --> EXIT + end + + subgraph LIB_INTERNAL["🔵 Library (never calls log_init/exit)"] + LOG_CALL["FWUPMGR_INFO/ERROR/DEBUG/WARN
Just emits — assumes log is initialized"] + end + + USE -.-> LOG_CALL + + subgraph WRONG["🔴 WRONG — Double Init"] + BAD["Library calling log_init()
→ corrupts app's log state"] + end + + style APP fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style LIB_INTERNAL fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e3a5f + style WRONG fill:#fecaca,stroke:#dc2626,stroke-width:2px,color:#7f1d1d + style INIT fill:#22c55e,stroke:#15803d,color:#fff + style EXIT fill:#22c55e,stroke:#15803d,color:#fff + style BAD fill:#ef4444,stroke:#b91c1c,color:#fff +``` + +--- + +## Appendix: State Machine Diagrams + +### A.1 Check Callback Registry Slot States + +```mermaid +stateDiagram-v2 + [*] --> IDLE + + IDLE --> PENDING : checkForUpdate()
registers callback + PENDING --> DISPATCHED : Signal arrives
Phase 1 snapshots + DISPATCHED --> IDLE : Phase 3 resets
after callback returns + PENDING --> TIMED_OUT : Future: sweep thread
(not yet implemented) + TIMED_OUT --> IDLE : Cleanup + + state IDLE { + [*] : Slot available + } + state PENDING { + [*] : Callback stored, waiting for signal + } + state DISPATCHED { + [*] : Callback is being invoked + } + state TIMED_OUT { + [*] : Stale entry (future) + } +``` + +### A.2 Download/Update Registry Slot States + +```mermaid +stateDiagram-v2 + [*] --> IDLE + + IDLE --> ACTIVE : downloadFirmware() /
updateFirmware() + ACTIVE --> ACTIVE : Progress signal
(still in progress) + ACTIVE --> IDLE : Terminal signal
(COMPLETED or ERROR) + + state IDLE { + [*] : Slot available + } + state ACTIVE { + [*] : Callback registered,
receiving progress signals + } +``` + +### A.3 Library Handle Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> UNLINKED : Library loaded + + UNLINKED --> REGISTERED : registerProcess()
returns non-NULL handle + UNLINKED --> UNLINKED : registerProcess()
returns NULL (error) + + REGISTERED --> ACTIVE : checkForUpdate() /
downloadFirmware() /
updateFirmware() + ACTIVE --> ACTIVE : More API calls + ACTIVE --> REGISTERED : All callbacks complete + + REGISTERED --> UNLINKED : unregisterProcess() + ACTIVE --> UNLINKED : unregisterProcess()
(stale callbacks cleared) + + state UNLINKED { + [*] : No handle, no BG thread + } + state REGISTERED { + [*] : Handle valid,
BG thread running,
no pending ops + } + state ACTIVE { + [*] : Handle valid,
pending callbacks
in registries + } +``` + +--- + +*End of Visual Engineering Documentation* From 6cdd1ba26412541e71499aac69fa258acb4fc658 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Tue, 28 Apr 2026 08:52:29 +0000 Subject: [PATCH 12/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Design docs --- librdkFwupdateMgr/PR_REVIEW_DEFENSE.md | 746 +++++++++++++++++++++++++ 1 file changed, 746 insertions(+) create mode 100755 librdkFwupdateMgr/PR_REVIEW_DEFENSE.md diff --git a/librdkFwupdateMgr/PR_REVIEW_DEFENSE.md b/librdkFwupdateMgr/PR_REVIEW_DEFENSE.md new file mode 100755 index 00000000..09d9fa89 --- /dev/null +++ b/librdkFwupdateMgr/PR_REVIEW_DEFENSE.md @@ -0,0 +1,746 @@ +# librdkFwupdateMgr — Architectural Review Defense + +> **Date**: April 28, 2026 +> **Author**: Original Architect — rdkFwupdateMgr Team +> **Context**: Senior engineering review defense for PR introducing `librdkFwupdateMgr` shared library +> **Tone**: Confident, direct, technically precise — speaking as the designer defending every decision + +--- + +## 1. Why Library + Daemon Split Architecture Is Correct + +The split is not optional — it is the only correct architecture for this system. + +**The firmware update problem has two fundamentally different concerns:** + +| Concern | Characteristic | Who Handles | +|---------|---------------|-------------| +| Client interaction | Short-lived, per-app, needs callbacks | Library | +| Firmware operations | Long-lived, device-wide, privileged | Daemon | + +A firmware download takes 2–5 minutes. A flash operation takes 1–10 minutes. If a client process crashes halfway through a download, the download must continue — the daemon holds that responsibility. If the daemon were embedded in each client, a client crash would abort the download mid-stream, potentially leaving the device in a half-written firmware state. + +**Concrete justification:** + +1. **Process isolation**: Client crash does not corrupt daemon state. Daemon crash does not take down client apps (they detect it via callback timeout and re-register). +2. **Privilege separation**: Only the daemon needs root-level access to flash storage and reboot coordination. Client apps run at lower privilege. +3. **Resource sharing**: One daemon process manages the download cache, XConf session, and HAL interface. N client processes share it via IPC. Without the split, N copies of the download engine would compete for the same HTTP connection and storage. +4. **Lifecycle independence**: The daemon starts at boot via systemd (`rdkFwupdateMgr.service`) and runs indefinitely. Client apps start and stop independently. + +**What the alternative looks like (and why we rejected it):** + +If each client embedded the firmware engine: +- Each would need its own XConf query logic, HTTP download stack, flash HAL binding, and reboot coordinator +- Two clients requesting the same firmware simultaneously would download it twice +- Flash serialization would require cross-process locks (file locks or named semaphores — fragile on embedded Linux) +- A single bug in the firmware engine would need to be patched in every client binary + +The library + daemon split eliminates all of these problems. + +--- + +## 2. Why Not Expose the Daemon Directly to Clients + +"Just let apps call D-Bus directly — why add a library in between?" + +This question comes up in every review. Here is why direct D-Bus access is wrong for this system: + +### 2.1 Protocol Brittleness + +The D-Bus interface uses positional GVariant tuples, not named fields: + +``` +RegisterProcess: (ss) → (t) +CheckForUpdate: (s) → fire-and-forget +DownloadFirmware: (ssss) → fire-and-forget +UpdateFirmware: (sssss) → fire-and-forget + +Signals: + CheckForUpdateComplete: (tiissss) + DownloadProgress: (tsuss) + UpdateProgress: (tsiis) +``` + +One mistyped variant signature (`"(ss)"` instead of `"(si)"`) produces a GLib critical at runtime — silent corruption or crash. No compiler catches this. The library catches it once, in one place, with unit tests covering every signature. + +### 2.2 Signal Subscription Ordering + +If a client sends `CheckForUpdate` before subscribing to `CheckForUpdateComplete`, the response signal is lost forever. The library enforces correct ordering: `internal_system_init()` starts the background thread and subscribes to all three signals **before** `registerProcess()` returns. Every subsequent API call is guaranteed to have a live signal subscription. + +A direct D-Bus client would need to implement this ordering themselves. History shows developers get it wrong — the signal subscription race condition is the #1 bug in ad-hoc D-Bus client code. + +### 2.3 Connection Model Mismatch + +Our library creates an ephemeral D-Bus connection per API call. Each call gets a different unique sender ID (`:1.140`, `:1.141`, etc.). The daemon identifies clients by `handler_id`, not sender address. A developer writing direct D-Bus calls would naturally assume sender-ID stability across calls — and then wonder why the daemon doesn't correlate their requests. + +### 2.4 Data Format Parsing + +The `update_details` field in `CheckForUpdateComplete` is a pipe-separated `Key:Value` string: +``` +FwFileName:firmware_v2.bin|FwUrl:https://cdn.example.com/...|FwVersion:2.0|... +``` + +The library's `parse_update_details()` function handles this parsing, including missing fields, empty values, and malformed input. Every direct client would reimplement this parser — and introduce their own bugs. + +### 2.5 Forward Compatibility + +When we add fields to a signal (e.g., `CheckForUpdateComplete` adds a checksum field), only the library needs updating. All existing clients continue to work with the same API. Without the library, every client binary needs recompilation and redeployment. + +--- + +## 3. Why Shared Library API Abstraction Is Beneficial + +The API is exactly five functions with three callback types. This is the minimum surface that covers the firmware lifecycle: + +```c +FirmwareInterfaceHandle registerProcess(name, version); // Session start +CheckForUpdateResult checkForUpdate(handle, callback); // Query +DownloadResult downloadFirmware(handle, req, cb); // Fetch +UpdateResult updateFirmware(handle, req, cb); // Flash +void unregisterProcess(handle); // Session end +``` + +**Why this is the right abstraction level:** + +1. **Complete**: Covers the entire firmware update lifecycle — no missing operations. +2. **Minimal**: No convenience wrappers, no "download-and-flash" combo call, no polling APIs. Each function does exactly one thing. +3. **Symmetric**: Register/unregister bracket the session. Three async operations share the same pattern (validate → register callback → fire-and-forget D-Bus call → return). +4. **Typed**: Separate enums for each operation's status (`CheckForUpdateStatus`, `DownloadStatus`, `UpdateStatus`). The compiler catches type mismatches. +5. **Discoverable**: A developer reads the header top-to-bottom and knows the entire API in 5 minutes. No inheritance hierarchies, no vtables, no builder patterns. + +**What we deliberately excluded:** + +- `cancelDownload()` — not yet implemented in daemon; adding a no-op would be misleading +- `getStatus()` synchronous poll — encourages busy-waiting; callbacks are the correct model +- `downloadAndUpdate()` combo — couples two independent operations; client should control the flow between them + +--- + +## 4. Thread Safety Decisions and Justification + +### 4.1 Three Separate Registries, Three Separate Mutexes + +``` +g_registry (check) → g_registry.mutex +g_dwnl_registry (download) → g_dwnl_registry.mutex +g_update_registry (update) → g_update_registry.mutex +``` + +**Why not one global mutex?** Because operations are independent. A download signal arriving while a check callback is registering should not block. Separate mutexes maximize concurrency with zero additional complexity. + +**Why not per-slot locks?** 30 slots × 3 registries = 90 mutexes. The initialization, cleanup, and deadlock analysis complexity is not justified. The critical sections are sub-microsecond (array scan + pointer copy). Contention is effectively zero — firmware operations happen at human timescales (seconds to minutes), not microsecond intervals. + +### 4.2 Two-Phase Dispatch + +This is the most important thread-safety decision in the library. The `dispatch_all_pending()` function uses a two-phase approach: + +``` +Phase 1 (mutex held): Snapshot PENDING entries into stack-local array + Mark entries DISPATCHED + Release mutex + +Phase 2 (no mutex): Invoke each callback from snapshot + Callbacks can safely call checkForUpdate() etc. + Re-acquire mutex briefly to reset each slot to IDLE +``` + +**Why this pattern exists:** If we held the mutex while invoking callbacks, and a callback called `checkForUpdate()` (which calls `internal_register_callback()` which locks the same mutex) — deadlock. The two-phase design makes re-entrant library use safe without requiring recursive mutexes (which have their own pitfalls — forgetting to release the correct number of times). + +### 4.3 Isolated GMainContext + +The background thread creates its own `GMainContext`: +```c +g_bg_thread.context = g_main_context_new(); +g_bg_thread.main_loop = g_main_loop_new(g_bg_thread.context, FALSE); +g_main_context_push_thread_default(g_bg_thread.context); +``` + +**Why not use the default context?** If the client app runs its own GLib main loop (GTK application, WebKit service), our signal subscriptions would fire in the app's main thread — breaking the app's event model and causing thread-safety violations in the app's own code. Isolated context guarantees our signals fire in our thread, never in the app's. + +### 4.4 Spin-Wait at Initialization + +```c +for (int i = 0; i < 50; i++) { // Max 5 seconds + if (g_bg_thread.running) break; + nanosleep(100ms); +} +``` + +**Why spin-wait instead of a condition variable?** This runs exactly once per library session — during `registerProcess()`. A condvar would be more elegant but adds a third synchronization primitive to the initialization path. The spin-wait is bounded (5 seconds max), runs on the main thread which is already blocking on `registerProcess()`, and is dead-simple to audit. + +--- + +## 5. Why Chosen Logging Design Helps Production Debugging + +### 5.1 Module Separation + +Three distinct log modules allow filtering in production: + +| Module | Prefix | Source | +|--------|--------|--------| +| `LOG.RDK.FWUPMGR` | `[FWUPMGR]` | Library internals | +| `LOG.RDK.FWUPG` | `[FWUPG]` | Daemon operations | +| `LOG.RDK.EXAMPLE` | `[EXAMPLE]` | Example/reference app | + +In production, an operator can set `LOG.RDK.FWUPMGR` to DEBUG while keeping daemon logs at INFO. This isolates library behavior without drowning in daemon verbosity. + +### 5.2 handler_id as Correlation Key + +Every log message in the firmware update path includes the `handler_id`. This allows end-to-end request tracing: + +``` +[FWUPMGR] INFO: registerProcess() → handler_id=12345 +[FWUPMGR] INFO: checkForUpdate, handle=12345 +[FWUPG] INFO: CheckForUpdate for handler_id=12345, querying XConf +[FWUPG] INFO: CheckForUpdate result for 12345: AVAILABLE +[FWUPMGR] INFO: signal received, handler_id=12345 +[FWUPMGR] INFO: dispatching 1 callback(s) for handler_id=12345 +``` + +With `grep 12345 /opt/logs/rdkFwupdateMgr.log`, a field engineer can trace a single client's entire firmware update journey across library and daemon boundaries. + +### 5.3 Non-RDK_LOGGER Fallback + +```c +#ifdef RDK_LOGGER +#define FWUPMGR_LOG(level, module, FORMAT...) RDK_LOG(level, module, FORMAT) +#else +#define FWUPMGR_LOG(level, module, FORMAT...) fprintf(stderr, "[%s] " FORMAT, module) +#endif +``` + +Unit tests run without `RDK_LOGGER` installed. The `fprintf` fallback means every test still emits visible log output — no silent failures. This was a deliberate choice: we will never have a test where a log macro silently expands to nothing. + +### 5.4 Library Does Not Own Log Lifecycle + +`log_init()` and `log_exit()` are the caller's responsibility. The library never calls them. This prevents double-initialization when multiple libraries are loaded into the same process. + +--- + +## 6. IPC Design Tradeoffs + +### 6.1 D-Bus System Bus (Chosen) + +| Pro | Con | +|-----|-----| +| Standard Linux IPC — every RDK device has it | Overhead per message (~50μs) | +| Policy-based access control via conf files | GLib dependency | +| Signal broadcast — multi-client without daemon code changes | GVariant parsing boilerplate | +| Well-debugged (dbus-monitor for field diagnosis) | Message size limits (128MB default) | + +### 6.2 Alternatives Considered and Rejected + +**Unix domain sockets (raw):** Lower overhead, but we'd need to implement our own protocol framing, serialization, signal broadcast, and access control. Reinventing D-Bus poorly. + +**Shared memory + semaphores:** Lowest latency, but firmware operations are I/O-bound (network downloads, flash writes). Microsecond IPC savings are irrelevant when the operation takes minutes. The synchronization complexity (readers/writers, cleanup on crash) is not justified. + +**gRPC / Protocol Buffers:** Not available on target embedded Linux platform. Would add 15MB+ of runtime dependencies. + +**rbus (RDK Bus):** The codebase has `rbusInterface/` support. D-Bus was chosen because the daemon's existing implementation uses D-Bus, and dual-transport support would double the test surface for zero benefit. + +### 6.3 Ephemeral Connection Model + +Each API call creates and destroys its own D-Bus connection: + +``` +registerProcess(): [connect → call → disconnect] sender :1.140 +checkForUpdate(): [connect → call → disconnect] sender :1.141 +downloadFirmware(): [connect → call → disconnect] sender :1.142 +``` + +**Why not a persistent connection?** + +1. No connection lifecycle management needed — no reconnection logic, no heartbeats, no stale connection detection +2. Each call is fully self-contained — if D-Bus dies between calls, the next call discovers it immediately +3. The daemon uses `handler_id` for client identity, not D-Bus sender address — so sender instability is irrelevant +4. Firmware operations happen at most once per hour on a production device — the 5ms connection overhead is negligible + +**The background thread has a persistent connection** for signal subscription — this is correct because signal delivery requires a stable subscription, and the overhead is exactly one connection for the library's lifetime. + +--- + +## 7. Memory Management Rationale + +### 7.1 Ownership Rules + +| Resource | Allocator | Deallocator | Trigger | +|----------|-----------|-------------|---------| +| Handle string | `malloc(32)` in `registerProcess()` | `free()` in `unregisterProcess()` | Always, even on D-Bus failure | +| Registry `handle_key` | `strdup()` in `internal_register_callback()` | `free()` in `registry_reset_slot()` | Slot reset to IDLE | +| `FwInfoData` + `UpdateDetails` | Stack allocation in `dispatch_all_pending()` | Automatic (stack unwind) | Function return | +| GDBusProxy, GDBusConnection | `g_dbus_proxy_new_sync()` | `g_object_unref()` | Same function, all code paths | +| `InternalSignalData` strings | `strdup()` in parse | `free()` in `internal_cleanup_signal_data()` | After dispatch completes | + +### 7.2 Why Stack Allocation for Callback Data + +`FwInfoData` and `UpdateDetails` are allocated on the background thread's stack in `dispatch_all_pending()`. This means: + +1. **No malloc/free pairing to get wrong** — allocation and deallocation are automatic +2. **No ownership ambiguity** — the data is valid only during the callback, and this is documented in the header and enforced by stack scoping +3. **Cache-friendly** — stack is always in L1 cache on the background thread +4. **Stack usage: ~1KB** — well within the 8MB default thread stack + +The tradeoff is that clients must copy data they need. This is documented explicitly: + +> *"The pointer and strings inside are only valid during this callback. If you need the data later, copy it with strdup()"* — `rdkFwupdateMgr_client.h` + +This is the same pattern used by `getaddrinfo()`, `readdir()`, and every GLib signal handler. C developers expect it. + +### 7.3 Handle Allocation: Why malloc(32)? + +The handle is a decimal string representation of a `uint64_t`. Maximum value: `18446744073709551615` (20 digits). With null terminator: 21 bytes. We allocate 32 bytes for comfortable alignment and future-proofing (if the handle format ever includes a prefix). + +`snprintf(handle_str, 32, "%" PRIu64, handler_id)` — bounds-checked, null-terminated, no overflow possible. + +### 7.4 Best-Effort Cleanup in Error Paths + +The `registerProcess()` malloc failure path demonstrates our cleanup philosophy: + +```c +if (!handle_str) { + // Registration succeeded on daemon but we can't return handle. + // Must unregister to prevent resource leak on daemon side. + GDBusProxy *cleanup_proxy = create_dbus_proxy(&cleanup_error); + if (cleanup_proxy) { + g_dbus_proxy_call_sync(cleanup_proxy, "UnregisterProcess", ...); + } + return NULL; +} +``` + +We never silently leak daemon-side resources. If we can't clean up (proxy creation fails too), we log it explicitly so field engineers can diagnose. + +--- + +## 8. Failure Handling Strategy + +### 8.1 Fail-Fast at Boundaries + +Every public API validates all inputs before touching D-Bus or shared state: + +```c +if (!processName) → FWUPMGR_ERROR, return NULL +if (strlen(processName) == 0) → FWUPMGR_ERROR, return NULL +if (strlen(processName) > MAX) → FWUPMGR_ERROR, return NULL +``` + +No D-Bus connection is created, no registry slot is allocated, no mutex is locked — until validation passes. This prevents partial state pollution on invalid input. + +### 8.2 Error Propagation Model + +| Layer | Error Handling | +|-------|---------------| +| Public API | Returns error code (`NULL`, `FAIL`, etc.) + logs specific error | +| D-Bus transport | GLib `GError` captured, message logged, error freed, propagated as API-level failure | +| Background thread | Signal parse failure logged, callback not dispatched (client times out) | +| Unregister | Best-effort — errors logged as WARN, cleanup continues regardless | + +### 8.3 No Silent Failures + +Every error path logs before returning. There is no code path where the library returns an error without first logging exactly what went wrong. This was a deliberate coding standard enforced throughout the implementation. + +### 8.4 Unregister Is Tolerant + +`unregisterProcess()` is deliberately forgiving: +- NULL handle → no-op (logged at INFO) +- Invalid handle format → log error, `free(handle)`, return +- D-Bus proxy creation fails → log WARN, `free(handle)`, return +- D-Bus call fails → log WARN, `free(handle)`, return +- Daemon reports failure → log WARN, `free(handle)`, return + +The handle is **always freed**. Local resources are **always cleaned up**. The daemon can be down, crashed, or restarted — `unregisterProcess()` will not block or leak. + +--- + +## 9. Why Return Code Model Was Selected + +### 9.1 Two-Tier Result Model + +The API uses a two-tier result model: + +**Tier 1 (Synchronous):** Did the request succeed in being sent? +```c +CheckForUpdateResult rc = checkForUpdate(handle, callback); +if (rc == CHECK_FOR_UPDATE_FAIL) { /* Request not sent — no callback coming */ } +``` + +**Tier 2 (Asynchronous):** What was the actual firmware result? +```c +void my_callback(const FwInfoData *fwinfo) { + if (fwinfo->status == FIRMWARE_AVAILABLE) { /* New firmware exists */ } +} +``` + +### 9.2 Why Not Combine into One? + +Because the caller needs different error handling for each tier: + +- Tier 1 failure (FAIL): Retry immediately — daemon may not be running, D-Bus may be congested +- Tier 2 failure (ERROR status in callback): Application-level decision — firmware not available, network down, etc. + +Combining them would force the caller to handle transport errors and application errors in the same callback, complicating control flow. + +### 9.3 Why Separate Enums Per Operation? + +`CheckForUpdateResult`, `DownloadResult`, `UpdateResult` are separate enums, not a single `FwResult`. This is deliberate: + +1. **Type safety**: The compiler rejects `if (download_result == CHECK_FOR_UPDATE_SUCCESS)` — wrong enum type +2. **Clarity**: Each enum has exactly the values that operation can return — no "unused for this API" values +3. **Extensibility**: Adding `RDKFW_DWNL_PAUSED` to `DownloadResult` doesn't affect `CheckForUpdateResult` + +### 9.4 Why Not errno-Style? + +Setting a global `errno` would be thread-unsafe without TLS. The return-code-per-call model is the standard C pattern for thread-safe libraries (OpenSSL, libcurl, zlib all use it). + +--- + +## 10. How Design Supports Future APIs + +### 10.1 New Operations + +Adding a new operation (e.g., `cancelDownload()`) requires: + +1. New public function in header → `CancelResult cancelDownload(handle)` +2. New D-Bus method name in constants → `#define DBUS_METHOD_CANCEL "CancelDownload"` +3. New D-Bus call in API layer → same pattern as existing fire-and-forget calls + +No changes to: registries, background thread, signal handling, or existing APIs. + +### 10.2 New Signals + +The background thread already demonstrates the subscription pattern. Adding a fourth signal subscription is: one `g_dbus_connection_signal_subscribe()` call + one signal handler function + one registry (if it needs callbacks). + +### 10.3 New Callback Parameters + +The `UpdateCallback` header explicitly documents: + +> *"The signature and behavior of this callback may change in future versions when HAL APIs become available."* + +This sets expectation with clients. When HAL is integrated, we bump the major version and update the callback signature. + +### 10.4 New Transport + +The IPC layer is isolated behind `create_dbus_proxy()` and the fire-and-forget pattern. Switching from D-Bus to rbus would require changing the transport functions without touching the public API, the registries, or the dispatch logic. + +--- + +## 11. Why Implementation Is Maintainable + +### 11.1 Module Separation + +``` +rdkFwupdateMgr_process.c — registerProcess(), unregisterProcess() +rdkFwupdateMgr_api.c — checkForUpdate(), downloadFirmware(), updateFirmware() +rdkFwupdateMgr_async.c — Registries, background thread, signal handlers, dispatch +rdkFwupdateMgr_log.h — Logging macros (header-only) +rdkFwupdateMgr_async_internal.h — Internal types (never exposed to clients) +rdkFwupdateMgr_client.h — Public API (the ONLY file clients include) +``` + +A developer fixing a download bug reads exactly two files: `_api.c` (API entry point) and `_async.c` (dispatch logic). They never need to understand registration logic in `_process.c`. + +### 11.2 Consistent Patterns + +Every async API follows the same structure: +``` +1. Log entry +2. Validate inputs +3. Connect to D-Bus +4. Register callback in registry +5. Fire-and-forget D-Bus call +6. Unref connection +7. Log exit +8. Return SUCCESS +``` + +This is not accidental. It's a deliberate template. A developer who understands `checkForUpdate()` understands `downloadFirmware()` and `updateFirmware()` — they are structurally identical with different parameter lists. + +### 11.3 No Clever Code + +There are no macros that generate functions, no varargs tricks, no `__attribute__((cleanup))` magic, no `setjmp/longjmp` error handling. Every function is readable straight through. The most complex construct is the two-phase dispatch, and it has a 15-line comment explaining exactly why it exists. + +--- + +## 12. Why Code Is Junior-Friendly + +### 12.1 Header Documentation + +Every struct, enum, callback, and function in `rdkFwupdateMgr_client.h` has a plain-English comment: + +```c +/** + * FirmwareInterfaceHandle + * + * This is a string ID that the daemon gives you when you register. + * Think of it like a session ID or ticket number (e.g., "12345"). + */ +``` + +No jargon. No references to GLib internals. A developer with 6 months of C experience can read this header and write a client. + +### 12.2 Example App + +`examples/example_app.c` is a complete, working reference implementation. It demonstrates: +- Log initialization +- Registration +- Check → Download → Update flow +- Callback implementation with condvar synchronization +- Cleanup in all error paths + +A junior developer copies this file, changes the process name, and has a working firmware updater. + +### 12.3 Explicit Warnings in Comments + +```c +// WARNING: This operation modifies device firmware. It is irreversible once the flash begins. +// Don't call other library functions from inside this callback +// This runs in a background thread, not your main thread +``` + +The comments don't assume the reader knows threading, D-Bus, or firmware update semantics. + +### 12.4 Defensive NULL Handling + +`unregisterProcess(NULL)` is a no-op, not a crash. This forgives the common pattern: + +```c +cleanup: + unregisterProcess(handle); // Safe even if registerProcess() failed +``` + +--- + +## 13. Security Posture of the Design + +### 13.1 D-Bus Policy Enforcement + +Access to the daemon is controlled by D-Bus system bus policy: +```xml + + + +``` +Unprivileged processes cannot send method calls or receive signals. This is enforced by dbus-daemon, not by our code — correct separation of concerns. + +### 13.2 Input Validation at Every Boundary + +All public API inputs are validated before any IPC: +- NULL checks on all pointer parameters +- Empty string rejection on required fields +- Length limits on all strings (preventing buffer overflow in daemon's fixed-size buffers) +- Strict numeric parsing in `unregisterProcess()` with `strtoull()` + `endptr` validation + +### 13.3 No `sprintf()` Anywhere + +Every string format in the library uses `snprintf()` with explicit bounds. The `strncpy()` calls in `parse_update_details()` explicitly null-terminate. + +### 13.4 handler_id Limitations (Documented) + +The `handler_id` is a sequential counter, not a cryptographically random token. In the current deployment model (single device, trusted clients), this is acceptable. The design document explicitly flags this: + +> *"For production hardening, consider using a random 128-bit token."* + +We are transparent about the limitation rather than pretending it doesn't exist. + +### 13.5 No Credential Storage + +The library stores no passwords, tokens, certificates, or API keys. Authentication is entirely handled by D-Bus policy. + +--- + +## 14. Operational Reliability Benefits + +### 14.1 Graceful Degradation + +| Failure | Library Behavior | Client Impact | +|---------|-----------------|---------------| +| Daemon not running | `registerProcess()` returns NULL | Client knows immediately | +| Daemon crashes mid-download | No more progress signals arrive | Client's condvar times out | +| D-Bus bus restart | Next API call fails at connection | Client unregisters + re-registers | +| Client crash | Daemon detects name disappearance (best-effort) | No impact on other clients | +| Library background thread fails | No signals dispatched | Client times out on all callbacks | + +### 14.2 No Hung States + +Every blocking operation has a bounded timeout: +- D-Bus calls: 5000ms (`DBUS_TIMEOUT_MS`) +- Background thread startup: 5000ms (50 × 100ms spin-wait) +- Client callbacks: bounded by client's own `pthread_cond_timedwait()` + +There is no code path where the library blocks indefinitely. + +### 14.3 Clean Shutdown + +`unregisterProcess()` performs ordered cleanup: +1. Stop background thread (quit loop → join) +2. Free all registry entries +3. Destroy all mutexes +4. Best-effort daemon notification +5. Free handle + +Even if the daemon is unreachable, steps 1–3 and 5 always complete. No resources leak on the client side. + +### 14.4 Idempotent Cleanup + +Calling `unregisterProcess(NULL)` is safe. Calling it after the daemon has already cleaned up the registration is safe (logged as WARN, not an error). This means crash handlers and `atexit()` hooks can call it unconditionally. + +--- + +## Reviewer Likely Questions + +### Q1: Why use global static state for registries instead of instance-based allocation? + +**Answer:** The library manages exactly one daemon session per process. There is no use case for two simultaneous sessions — the daemon assigns one `handler_id` per process name. Instance-based allocation (passing a context pointer through every function) would add a parameter to every API call, complicate every internal function, and solve zero real problems. The registries are encapsulated in `_async.c` as `static` — they are not exposed to the client and not accessible outside the compilation unit. This is the idiomatic C approach for module-private state (used by `malloc` internals, `stdio`, `errno`, etc.). + +### Q2: Why not use a condition variable instead of spin-waiting for background thread startup? + +**Answer:** The spin-wait runs exactly once per session — during `registerProcess()`. It waits a maximum of 5 seconds with 100ms sleep intervals between checks. A condvar would save ~50μs of cumulative spin time over 50 iterations — in a function that already does a synchronous D-Bus round-trip (5-10ms). The engineering complexity of adding a condvar (init, signal, wait, destroy, error handling) is not justified for a one-time initialization path that the user is already blocking on. + +### Q3: What happens if the daemon crashes while a download is in progress? + +**Answer:** The download callback stops receiving progress signals. The client's `pthread_cond_timedwait()` expires after its configured timeout (recommended: 300 seconds for downloads). The client then calls `unregisterProcess()`, which performs local cleanup (thread join, registry free, mutex destroy) regardless of daemon availability. The D-Bus `UnregisterProcess` call fails with a timeout — logged as WARN, cleanup continues. The client can then re-register and retry. No resources leak. No hung threads. + +### Q4: Why fire-and-forget for async operations instead of waiting for a D-Bus reply? + +**Answer:** The daemon operations take seconds to minutes (XConf query: 1-15s, download: 30-300s, flash: 60-600s). Blocking the caller's thread for the duration defeats the async model. The D-Bus reply would only confirm "I received your request" — the actual result comes as a signal. We skip the synchronous receipt confirmation and let the signal be the sole result channel. If the request fails to even reach the daemon (D-Bus bus down), the `g_dbus_connection_call()` itself fails immediately and the API returns FAIL. + +### Q5: Why MAX_PENDING_CALLBACKS = 30? Is that enough? + +**Answer:** 30 is the maximum number of concurrent pending callbacks per registry (check, download, update). On a single device, there are at most 3-5 client applications making firmware requests. Even in a stress test with 30 concurrent `checkForUpdate()` calls from the same process, 30 slots suffice. The value was reduced from 64 to 30 specifically to keep the stack-local snapshot array in `dispatch_all_pending()` under 10KB (Coverity flagged the original 64-slot version for excessive stack usage). If a use case requires more, bumping the constant is a one-line change. + +### Q6: Why no versioned API (v1, v2)? + +**Answer:** The library version is embedded in `LIB_VERSION "1.0.0"` and passed to the daemon during registration. ABI compatibility is maintained through the shared library versioning mechanism (libtool `SONAME`). We don't need URL-style version prefixes (`v1_registerProcess`) because the library is a binary artifact, not a REST API. When breaking changes are needed, the SO major version bumps, old clients link against the old `.so`, and new clients link against the new one. Standard practice for C shared libraries. + +### Q7: Why separate mutexes per registry instead of one global lock? + +**Answer:** Independence. A download progress signal arriving while a check callback is being registered should not block. The three operations have no shared data. Separate mutexes allow full concurrency between check, download, and update paths. The cost is 3 `pthread_mutex_t` instances (~120 bytes total on Linux) — negligible. + +### Q8: Why doesn't the library retry failed D-Bus calls? + +**Answer:** Retry policy belongs to the caller, not the transport. A monitoring daemon might retry every 60 seconds indefinitely. A user-facing app might retry once after 5 seconds then show an error. The library cannot know the right policy. Embedding retry logic would also complicate the error model — does "FAIL" mean "failed after 3 retries" or "didn't try"? By not retrying, the error code has a clear, deterministic meaning: this call, right now, failed. + +### Q9: What prevents a malicious client from guessing another client's handler_id? + +**Answer:** The `handler_id` is a sequential uint64 counter — it is guessable. In the current deployment model (single device, all processes running as root or a dedicated service account, D-Bus policy restricting access), this is acceptable. The daemon validates that the `handler_id` exists in its registration table before processing any request, preventing random probing. For multi-tenant or security-hardened deployments, the design document explicitly recommends upgrading to cryptographically random 128-bit tokens. This is a future improvement, not a current vulnerability in the deployment context. + +### Q10: Why does unregisterProcess() call internal_system_deinit() BEFORE the D-Bus unregister call? + +**Answer:** After sending `UnregisterProcess` to the daemon, the daemon stops sending signals for this client. If we sent the D-Bus call first and then tried to join the background thread, the thread might be blocked in `g_main_loop_run()` waiting for signals that will never come — until the loop is explicitly quit. By calling `internal_system_deinit()` first, we quit the loop and join the thread immediately. The subsequent D-Bus call to the daemon is then a pure notification — "I'm already gone, clean up your side." + +### Q11: Why is the handle a string ("12345") instead of an opaque struct pointer? + +**Answer:** Because it crosses a D-Bus boundary. The handle is the `handler_id` the daemon assigned — a uint64. We encode it as a decimal string so it can be: (a) passed back to the daemon in D-Bus method calls (which expect string arguments for most operations), (b) logged without format specifier portability issues, (c) compared with `strcmp()` in the registry without type-punning. An opaque pointer would require a lookup table mapping pointers to handler_ids, adding complexity for zero benefit. + +### Q12: What if a signal arrives between callback registration and D-Bus call? + +**Answer:** Cannot happen. The ordering is: register callback → send D-Bus call. The daemon only emits the response signal after receiving the D-Bus call. Since the callback is already registered before the call is sent, the signal will always find a matching registry entry. This ordering is enforced in the API layer (`_api.c`), not left to the caller. + +### Q13: Why not use GCancellable for timeouts? + +**Answer:** `GCancellable` is designed for cancelling in-flight GIO operations. Our async operations are fire-and-forget — there is no in-flight operation to cancel after the `g_dbus_connection_call()` returns. The timeout is entirely on the client side (condvar timedwait), which is outside GLib's control. Using `GCancellable` would add a GLib object lifecycle with no functional benefit. + +### Q14: Why strdup() the handle_key in the registry instead of keeping a pointer? + +**Answer:** The handle string is owned by the caller's scope. Between the time we register the callback and the time the signal arrives, the caller may have passed the handle to another function, stored it in a struct, or (if buggy) freed it. By `strdup()`-ing the handle into the registry, the registry owns its own copy with a guaranteed lifetime. The 20-byte allocation cost per registration is negligible. + +### Q15: Why no timeout sweeper thread to clean up stale PENDING entries? + +**Answer:** The `TIMED_OUT` state and `registered_time` field exist in the design, but no sweeper is implemented. This is deliberate. The current cleanup model is: client times out → client calls `unregisterProcess()` → all entries freed. A sweeper thread would add a fourth thread, a timer mechanism (`g_timeout_add`), and complex questions about what to do with swept entries (invoke callback with error? silently discard?). For the current use case (single-digit concurrent operations, client-managed timeouts), the sweeper adds complexity without solving a real problem. It's listed as future work in the design document. + +### Q16: The background thread's signal subscriptions use sender=NULL. Doesn't this accept signals from any process? + +**Answer:** Yes, and this is correct. D-Bus signals are broadcast — the bus delivers them to all subscribed clients regardless of sender. Filtering by sender would require knowing the daemon's unique bus name (`:1.42`), which changes on every daemon restart. Using `sender=NULL` with interface and object path filtering is the standard D-Bus pattern. D-Bus policy files restrict which processes can emit signals on our interface — this is the correct enforcement layer. + +### Q17: Why does parse_update_details() use pipe-delimited strings instead of structured GVariant? + +**Answer:** The pipe-delimited `Key:Value` format is the daemon's existing wire protocol. We did not design it — we consume it. The library's job is to parse what the daemon sends. Changing the daemon's signal format is out of scope for this PR. The `parse_update_details()` function encapsulates this ugly parsing so no client ever sees it. + +### Q18: The download registry keeps slots ACTIVE across multiple signals, but check registry resets to IDLE after one dispatch. Why the inconsistency? + +**Answer:** It's not an inconsistency — it reflects fundamentally different signal semantics. `CheckForUpdateComplete` fires **once** — the check is done. `DownloadProgress` fires **repeatedly** (0%, 25%, 50%, 75%, 100%). If the download slot reset to IDLE after the first 0% signal, all subsequent progress signals would be silently dropped. The lifecycle difference (one-shot vs. streaming) mandates different slot management. This is explicitly documented in the internal header: + +> *"CheckForUpdate registry: slot goes PENDING → DISPATCHED → IDLE (fires ONCE)"* +> *"Download registry: slot stays ACTIVE until DWNL_COMPLETED or DWNL_ERROR (fires MULTIPLE TIMES)"* + +### Q19: Why no unit test mocks for D-Bus in the library's own unit tests? + +**Answer:** The `unittest/` directory contains GTest-based tests that mock D-Bus at the function level using fake implementations (`test_dbus_fake.c`). The library's async engine can be tested by directly calling `internal_register_callback()` and simulating signal delivery via `dispatch_all_pending()` with synthetic `InternalSignalData`. D-Bus is abstracted behind `create_dbus_proxy()` and `g_dbus_connection_call()` — both are mockable without a running dbus-daemon. + +### Q20: What if two processes register with the same processName? + +**Answer:** The daemon enforces one registration per process name. The second `RegisterProcess("MyPlugin", "1.0")` call will either return the existing `handler_id` (idempotent registration) or return an error (duplicate rejection) — this is a daemon policy decision. The library faithfully returns whatever the daemon provides: a valid handle or NULL with the error message logged. + +### Q21: Why not use atomic operations instead of mutexes for the registry? + +**Answer:** The registry operations are not single-word reads/writes. Registration involves: scan array → check existing → allocate string → update multiple fields. Dispatch involves: scan array → copy N entries → update N states. These are multi-step operations that cannot be expressed as atomic CAS operations without a lock-free data structure (which would be significantly more complex and harder to audit). The mutex critical sections are sub-microsecond — there is no performance bottleneck to optimize. + +### Q22: How does this library handle being loaded by a multi-threaded app that already uses GLib? + +**Answer:** The library creates its own `GMainContext` and pushes it as the thread-default for the background thread. This isolates our D-Bus signal handling from the app's GLib event loop. The app can run `gtk_main()`, `g_main_loop_run()`, or any other GLib loop on its own threads without interference. This is the documented GLib pattern for library-owned event loops. + +### Q23: Why does the library not check if the daemon version is compatible? + +**Answer:** The library passes `libVersion` to the daemon during registration. The daemon can reject incompatible versions by returning an error on the `RegisterProcess` call. Version compatibility enforcement is the daemon's responsibility — it knows which library versions it supports. The library's job is to report its version honestly and handle rejection gracefully (return NULL). + +### Q24: The CALLBACK_TIMEOUT_SECONDS constant (60s) is defined but never used. Dead code? + +**Answer:** Not dead code — it's planned infrastructure. The `registered_time` field in every registry entry records when the callback was registered. The timeout constant exists for a future sweeper that will clean stale entries. We ship the constant and the timestamp now so that enabling the sweeper later is a one-function addition, not a data model change. Removing them would save 8 bytes per entry and one `#define` — not worth the cost of re-adding them later. + +### Q25: Why not use function pointers in a vtable for the three API operations instead of three separate functions? + +**Answer:** Because the three operations have different signatures: + +```c +CheckForUpdateResult checkForUpdate(handle, UpdateEventCallback); +DownloadResult downloadFirmware(handle, FwDwnlReq*, DownloadCallback); +UpdateResult updateFirmware(handle, FwUpdateReq*, UpdateCallback); +``` + +Different parameter types, different return types, different callback signatures. A vtable would require casting to `void*` parameters and `int` returns — losing type safety for zero structural benefit. The three separate functions are explicit, typed, and impossible to call incorrectly. + +--- + +## Suggested PR Summary Comment + +--- + +**PR: Introduce `librdkFwupdateMgr` — Client Library for Firmware Update Daemon** + +This PR adds `librdkFwupdateMgr.so`, a shared C library that provides client applications a clean, thread-safe API for firmware lifecycle management via the `rdkFwupdateMgr` daemon. + +**What it does:** +- 5 public functions: `registerProcess`, `checkForUpdate`, `downloadFirmware`, `updateFirmware`, `unregisterProcess` +- 3 callback types for async result delivery (check, download progress, update progress) +- Single background thread with isolated GLib event loop for D-Bus signal reception +- Three mutex-protected callback registries with two-phase dispatch (deadlock-free) +- Ephemeral D-Bus connections per API call; persistent connection for signal subscription +- Comprehensive input validation at every public API boundary +- Structured logging under `LOG.RDK.FWUPMGR` module with handler_id correlation + +**What it does NOT do:** +- No firmware downloads, flash operations, or reboots (daemon's responsibility) +- No retry logic (caller's responsibility — different apps need different retry policies) +- No log lifecycle management (`log_init`/`log_exit` are caller's responsibility) + +**Key design decisions:** +- Library + daemon split for process isolation, privilege separation, and resource sharing +- Async fire-and-forget API — operations return immediately, results come via callbacks +- Stack-allocated callback data (valid only during callback) — zero heap allocation in the hot path +- Two-phase dispatch prevents deadlock when callbacks re-enter the library +- Best-effort cleanup in `unregisterProcess()` — never blocks, never leaks local resources + +**Files added:** +- `librdkFwupdateMgr/include/rdkFwupdateMgr_client.h` — Public API header +- `librdkFwupdateMgr/src/rdkFwupdateMgr_process.c` — Registration/unregistration +- `librdkFwupdateMgr/src/rdkFwupdateMgr_api.c` — Async API entry points +- `librdkFwupdateMgr/src/rdkFwupdateMgr_async.c` — Internal engine (registries, thread, dispatch) +- `librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h` — Internal types +- `librdkFwupdateMgr/src/rdkFwupdateMgr_log.h` — Logging macros +- `librdkFwupdateMgr/examples/example_app.c` — Reference client implementation + +**Testing:** Unit tests in `unittest/` cover registration flow, callback dispatch, error paths, and mock D-Bus interactions. + +--- + +*End of Review Defense Document* From 95394c0e69d2ae70655d56ac653173ed51e70565 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 01:53:03 +0000 Subject: [PATCH 13/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Documentation for Register and Unregister process APIs --- librdkFwupdateMgr/examples/example_app.c | 2 +- .../src/rdkFwupdateMgr_process.c | 947 ++++++++++++++++-- 2 files changed, 887 insertions(+), 62 deletions(-) diff --git a/librdkFwupdateMgr/examples/example_app.c b/librdkFwupdateMgr/examples/example_app.c index 12c2ba6b..2cdf0515 100644 --- a/librdkFwupdateMgr/examples/example_app.c +++ b/librdkFwupdateMgr/examples/example_app.c @@ -335,7 +335,7 @@ int main(void) struct timespec timeout; int rc; - /* Initialize logging — must be first. + /* Initialize logging must be first. * All EXAMPLE_* and FWUPMGR_* log output goes to stdout/stderr. * Shell redirect puts it in the right file: * example_plugin > /opt/logs/rdkFwupdateMgr.log 2>&1 diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 4c16475d..56b25d1a 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -236,52 +236,215 @@ static bool validate_lib_version(const char *libVersion) return true; } -/* ======================================================================== - * PUBLIC API IMPLEMENTATION - * ======================================================================== */ +/* PUBLIC API IMPLEMENTATION */ -/** - * @brief Register a process with the firmware update daemon +/* + * registerProcess - Register a client process with the firmware update daemon. * - * See rdkFwupdateMgr_process.h for full API documentation. + * OVERVIEW * - * IMPLEMENTATION NOTES: - * - Creates D-Bus proxy on-demand (no persistent connection) - * - Synchronous D-Bus call (blocks until daemon responds) - * - Timeout: 10 seconds (configurable via DBUS_TIMEOUT_MS) - * - Returns string handle (handler_id as decimal string) + * PURPOSE: + * This is the MANDATORY first call before using any other library API. + * It establishes a session with the rdkFwupdateMgr daemon by: + * 1. Sending the client's name and version to the daemon via D-Bus + * 2. Receiving a unique numeric handler_id from the daemon + * 3. Converting that ID into a string "handle" returned to caller + * 4. Spinning up the library's internal async engine (BG thread, + * callback registries, D-Bus signal subscriptions) * - * ERROR HANDLING: - * - Input validation: NULL/empty checks, length checks - * - D-Bus errors: Connection failures, daemon errors - * - Memory allocation failures - * - All errors return NULL with descriptive stderr messages + * WHAT THE CALLER GETS BACK: + * A FirmwareInterfaceHandle (which is just a typedef for char*). + * Example: "1", "42", "1023" -- it's the daemon-assigned handler_id + * as a decimal string. This handle must be passed to ALL subsequent + * API calls (checkForUpdate, downloadFirmware, updateFirmware) and + * eventually to unregisterProcess() to clean up. + * + * THREADING MODEL: + * - This function runs entirely on the CALLER'S thread + * - It BLOCKS (synchronous D-Bus call) for up to DBUS_TIMEOUT_MS (5s) + * - At the end, it spawns a background thread for signal reception + * - After return: 2 threads exist (caller's + library BG thread) + * + * D-BUS WIRE PROTOCOL: + * Method: "RegisterProcess" + * Input: GVariant type "(ss)" -- two strings: processName, libVersion + * Output: GVariant type "(t)" -- one uint64: handler_id + * The call creates an EPHEMERAL D-Bus connection (new connection each + * time, destroyed after use). The daemon identifies us by handler_id, + * NOT by D-Bus sender address. + * + * MEMORY CONTRACT: + * - Library OWNS the returned handle (malloc'd internally) + * - Caller must NEVER free() it directly + * - Caller must call unregisterProcess(handle) to release it + * + * POSSIBLE RETURN VALUES: + * Non-NULL string -- Success. Use this handle for all subsequent calls. + * NULL -- Failure. Check logs. Daemon might not be running. + * + * EXECUTION FLOW (step numbers match code comments below): + * + * [1] Log entry + parameter echo + * [2] Validate processName (NULL? empty? too long?) + * [3] Validate libVersion (NULL? too long?) + * [4] Create ephemeral D-Bus proxy to daemon + * [5] Call "RegisterProcess" method synchronously (BLOCKS here) + * [6] Extract handler_id (uint64) from daemon's reply + * [7] Allocate 32-byte string buffer on heap + * [8] Convert handler_id to decimal string + * [9] Initialize internal async engine: + * - 3 callback registries (check, download, update) + * - 4 mutexes + * - 1 background thread (subscribes to D-Bus signals) + * [10] Return handle to caller + * + * @param processName A human-readable name identifying this client process. + * Must be non-NULL, non-empty, max 256 chars. + * Examples: "example_plugin", "tr069_agent", "webui_service" + * The daemon enforces UNIQUENESS -- two different processes + * cannot register with the same name simultaneously. + * + * @param libVersion The version string of this library the client was built + * against. Must be non-NULL, max 64 chars (empty string OK). + * Typically pass the LIB_VERSION macro from the public header + * (currently "1.0.0"). Used by daemon for compatibility tracking. + * + * @return FirmwareInterfaceHandle (char*) on success -- the session handle. + * NULL on any failure (validation, D-Bus, daemon rejection, OOM). + * + * Note: This function is THREAD-SAFE for concurrent calls with different + * process names. Do NOT call it twice with the same processName -- + * the daemon will reject the second registration. + * + * Warning: After this returns non-NULL, a background thread is running. + * You MUST call unregisterProcess() before process exit, or the + * thread will be forcibly killed by the OS (potential resource leak). + * + * See also: unregisterProcess() -- The cleanup counterpart to this function. + * See also: internal_system_init() -- The async engine startup called at the end. + * See also: checkForUpdate() -- First API you'd typically call after registration. */ FirmwareInterfaceHandle registerProcess(const char *processName, const char *libVersion) { + /* + * LOCAL VARIABLE DECLARATIONS + * + * Why declare all at the top? + * - Makes it easy to see ALL resources this function manages + * - Every non-NULL pointer here must be freed/unreffed on every exit path + */ + + /* GDBusProxy: A GLib object representing a remote D-Bus interface. + * Think of it as a "remote method caller" -- it knows the bus name, + * object path, and interface, and can invoke methods on the daemon. + * Must be freed with g_object_unref() when done. */ GDBusProxy *proxy = NULL; + + /* GError: GLib's error reporting mechanism. When a GLib function fails, + * it allocates a GError and fills in a human-readable message + error code. + * We pass &error to functions, they set it on failure. + * Must be freed with g_error_free() if non-NULL. */ GError *error = NULL; + + /* GVariant: GLib's type-safe, immutable, reference-counted value container. + * D-Bus messages are encoded as GVariants. The daemon's reply will be a + * GVariant with type "(t)" -- a tuple containing one uint64. + * Must be freed with g_variant_unref() when done. */ GVariant *result = NULL; + + /* handler_id: The daemon's response -- a 64-bit unsigned integer that + * uniquely identifies our registration. The daemon increments a counter + * (starting at 1) for each new registration. So the first client gets 1, + * second gets 2, etc. We'll convert this to a string for the handle. */ guint64 handler_id = 0; + + /* handle_str: The actual string we return to the caller. Heap-allocated, + * 32 bytes (enough for the decimal representation of any uint64 value, + * since max uint64 = 18446744073709551615 = 20 digits + null terminator). + * This pointer IS the FirmwareInterfaceHandle. */ char *handle_str = NULL; + /* + * [STEP 1] LOGGING -- Entry point trace + * + * Why log the parameters? + * - For field debugging: if something goes wrong later, the log shows + * exactly what the client passed in. + * - The ternary (?:) guards against NULL dereference in printf. + * Without it, printf("%s", NULL) is UNDEFINED BEHAVIOR in C + * (crashes on some platforms, prints "(null)" on others). + */ FWUPMGR_INFO("registerProcess() called\n"); FWUPMGR_INFO(" processName: '%s'\n", processName ? processName : "NULL"); FWUPMGR_INFO(" libVersion: '%s'\n", libVersion ? libVersion : "NULL"); - // Validate inputs + /* + * [STEP 2] VALIDATE processName + * + * Why validate here (at the library boundary) instead of letting the + * daemon validate? + * 1. Fail fast -- no point creating a D-Bus connection for bad input + * 2. Saves a round-trip to the daemon (D-Bus call has ~5ms overhead) + * 3. Defense in depth -- even if daemon validates too, we catch it early + * 4. Better error messages (we know the context; daemon doesn't know + * which client messed up) + * + * validate_process_name() checks: + * - processName != NULL + * - strlen(processName) > 0 (not empty "") + * - strlen(processName) <= 256 (MAX_PROCESS_NAME_LEN) + * Returns false (and logs FWUPMGR_ERROR) if any check fails. + */ if (!validate_process_name(processName)) { + /* Validation failed -- error already logged inside validate_process_name(). + * Return NULL immediately. No resources to clean up (we haven't + * allocated anything yet). */ return NULL; } + /* + * [STEP 3] VALIDATE libVersion + * + * validate_lib_version() checks: + * - libVersion != NULL (empty string "" IS allowed -- means "unknown") + * - strlen(libVersion) <= 64 (MAX_LIB_VERSION_LEN) + */ if (!validate_lib_version(libVersion)) { return NULL; } - // Create D-Bus proxy + /* + * [STEP 4] CREATE D-BUS PROXY (ephemeral connection) + * + * What happens inside create_dbus_proxy(): + * 1. g_bus_get_sync(G_BUS_TYPE_SYSTEM) -- connects to the system D-Bus. + * This is a BLOCKING call. The D-Bus daemon (dbus-daemon) assigns us + * a unique connection name like ":1.140". + * 2. g_dbus_proxy_new_sync() -- creates a proxy object that can call + * methods on "org.rdkfwupdater.Service" at "/org/rdkfwupdater/Service" + * on interface "org.rdkfwupdater.Interface". + * 3. The raw GDBusConnection is unreffed (proxy keeps its own internal ref). + * + * Why "ephemeral"? + * This connection lives only for THIS function call. After we get the + * daemon's response, we destroy the proxy (and its connection) immediately. + * Next API call (checkForUpdate, etc.) will create a brand new connection + * with a DIFFERENT sender ID. The daemon tracks us by handler_id, not + * by D-Bus sender address -- that's why this stateless model works. + * + * Common failure causes: + * - dbus-daemon not running (container/chroot without D-Bus) + * - D-Bus policy denying our connection (security policy file) + * - System bus socket not accessible (/var/run/dbus/system_bus_socket) + */ proxy = create_dbus_proxy(&error); if (!proxy) { - // Error already logged by create_dbus_proxy + /* create_dbus_proxy() already logged the specific error. + * We just need to clean up the GError (if it was allocated). + * + * Why check 'error' for NULL? + * In extreme OOM conditions, GLib may fail without setting the + * GError pointer. Calling g_error_free(NULL) would crash. */ if (error) { g_error_free(error); } @@ -290,7 +453,51 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib FWUPMGR_INFO("D-Bus proxy created successfully\n"); - // Call RegisterProcess D-Bus method + /* + * [STEP 5] SEND "RegisterProcess" D-BUS METHOD CALL + * + * This is the CORE of the function -- the actual IPC with the daemon. + * + * g_dbus_proxy_call_sync() does: + * 1. Marshals our arguments into D-Bus wire format + * 2. Sends the message to the daemon via dbus-daemon + * 3. BLOCKS until the daemon replies (or timeout expires) + * 4. Returns the reply as a GVariant, or NULL + GError on failure + * + * Parameters explained: + * proxy -- The proxy we just created (knows where to send) + * "RegisterProcess" -- The D-Bus method name on the daemon's interface + * g_variant_new("(ss)", processName, libVersion) + * -- The arguments, encoded as a GVariant TUPLE of + * two strings. "(ss)" is the D-Bus TYPE SIGNATURE: + * '(' = start tuple, 's' = string, 's' = string, ')' = end + * GLib takes ownership of this GVariant (don't free it!) + * G_DBUS_CALL_FLAGS_NONE -- No special flags (could use NO_AUTO_START + * to prevent D-Bus activation, but we want + * the daemon to auto-start if not running) + * DBUS_TIMEOUT_MS -- 5000 ms (5 seconds). If daemon doesn't reply + * within this time, the call fails with timeout error. + * NULL -- No GCancellable (we can't cancel this operation) + * &error -- Where to store error details on failure + * + * WHAT THE DAEMON DOES WHEN IT RECEIVES THIS: + * 1. Extracts processName and libVersion from the message + * 2. Calls add_process_to_tracking() which: + * a. Checks if any existing registration has the same process_name + * -- REJECTS if name is already taken by different client + * b. Checks if this client (sender_id) is already registered + * -- REJECTS if trying to register a second name + * -- Returns existing handler_id if re-registering same name (idempotent) + * c. Otherwise: allocates ProcessInfo, assigns next_process_id++, + * stores in hash table, returns the new handler_id + * 3. Sends reply: GVariant "(t)" containing the handler_id (uint64) + * + * BLOCKING BEHAVIOR: + * Our thread is SUSPENDED here until: + * a) Daemon replies (typically <10ms) -- we get 'result' + * b) 5-second timeout expires -- we get NULL + timeout GError + * c) D-Bus daemon signals an error -- we get NULL + error GError + */ FWUPMGR_INFO("Calling RegisterProcess D-Bus method...\n"); result = g_dbus_proxy_call_sync( proxy, @@ -298,11 +505,20 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib g_variant_new("(ss)", processName, libVersion), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, - NULL, // GCancellable + NULL, /* GCancellable */ &error ); if (!result) { + /* D-Bus call failed. Common causes: + * - Daemon not running ("The name was not provided by any .service files") + * - Daemon crashed during handling + * - 5-second timeout expired (daemon overloaded) + * - Daemon explicitly rejected us (process name conflict, D-Bus error reply) + * + * RESOURCE CLEANUP: + * We must free: error (GError) + proxy (GDBusProxy) + * We do NOT have: result, handle_str, handler_id (never obtained) */ FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", error->message); g_error_free(error); @@ -310,25 +526,83 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib return NULL; } - // Extract handler_id from result + /* + * [STEP 6] EXTRACT handler_id FROM DAEMON'S REPLY + * + * The daemon replied with GVariant type "(t)": + * '(' = tuple start + * 't' = uint64 (guint64 in GLib) + * ')' = tuple end + * + * g_variant_get() deserializes the GVariant into our C variable. + * The format string "(t)" must EXACTLY match what the daemon sent, + * or we get undefined behavior (buffer overread/corruption). + * + * After extraction, we free the GVariant (result) and the proxy. + * At this point: + * - We have the handler_id (e.g., 1) + * - The D-Bus connection is GONE (proxy unreffed = connection closed) + * - The daemon holds a ProcessInfo record for us in its hash table + * - No way to communicate with daemon anymore until we create + * another connection (which checkForUpdate etc. will do) + */ g_variant_get(result, "(t)", &handler_id); - g_variant_unref(result); - g_object_unref(proxy); + g_variant_unref(result); /* Free the GVariant container -- we extracted the value */ + g_object_unref(proxy); /* Free the proxy -- closes underlying D-Bus connection */ FWUPMGR_INFO("Registration successful\n"); FWUPMGR_INFO(" handler_id: %"G_GUINT64_FORMAT"\n", handler_id); - // Convert handler_id to string (this becomes the handle) - handle_str = (char*)malloc(32); // Enough for uint64 as decimal string + /* + * [STEP 7] ALLOCATE HANDLE STRING ON HEAP + * + * Why 32 bytes? + * - Max uint64 decimal: 18446744073709551615 (20 chars) + * - Plus null terminator: 21 bytes needed + * - We allocate 32 for alignment and future-proofing + * + * Why malloc() and not a struct or stack buffer? + * - The handle must OUTLIVE this function (returned to caller) + * - Stack allocation would be invalid after return (dangling pointer!) + * - A simple string is the simplest possible handle format: + * - No struct versioning issues + * - Easy to log/print for debugging + * - Trivial to pass across thread boundaries + * - Can be validated with strtoull() in unregisterProcess() + * + * Why (char*) cast? + * - malloc() returns void*. In C this is implicitly convertible to + * char*, but the cast makes intent clear and satisfies C++ compilers + * if this file is ever compiled as C++ (unlikely but defensive). + */ + handle_str = (char*)malloc(32); /* Enough for uint64 as decimal string */ if (!handle_str) { + /* + * OOM (Out Of Memory) RECOVERY PATH + * + * SITUATION: We SUCCESSFULLY registered with the daemon (it holds + * a ProcessInfo for us), but we can't allocate 32 bytes of RAM. + * + * PROBLEM: If we just return NULL, the daemon keeps our registration + * forever (resource leak). The client can't call unregisterProcess() + * because they don't have a handle. Nobody will clean this up. + * + * SOLUTION: Best-effort UnregisterProcess call to the daemon. + * "Best-effort" means: if this cleanup also fails (e.g., because + * the OOM is so severe we can't even create a D-Bus proxy), we + * log it and accept the leak. This is an extremely rare edge case. + * + * Why create a NEW proxy? Because we already unreffed the old one + * in Step 6. We can't reuse it. + */ FWUPMGR_ERROR("Failed to allocate memory for handle\n"); - // ⚠️ CRITICAL: Registration succeeded on daemon, but we can't return handle. - // Must unregister to prevent resource leak on daemon side. + /* CRITICAL: Registration succeeded on daemon, but we can't return handle. + * Must unregister to prevent resource leak on daemon side. */ FWUPMGR_ERROR("Attempting best-effort cleanup: UnregisterProcess(%" PRIu64 ")\n", handler_id); - // Create new proxy for cleanup call (previous one was already freed) + /* Create new proxy for cleanup call (previous one was already freed) */ GError *cleanup_error = NULL; GDBusProxy *cleanup_proxy = create_dbus_proxy(&cleanup_error); if (cleanup_proxy) { @@ -359,44 +633,277 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib return NULL; } - //snprintf(handle_str, 32, "" %PRIu64, handler_id); + /* + * [STEP 8] CONVERT handler_id TO DECIMAL STRING + * + * snprintf(buffer, size, format, ...): + * - Writes at most 'size' bytes (including null terminator) + * - GUARANTEED null-terminated (unlike strncpy!) + * - Returns number of chars that WOULD have been written (for truncation detection) + * + * "%" PRIu64 expands to the platform-correct format specifier for uint64_t. + * On Linux/GLib: PRIu64 = "lu" (unsigned long), so format = "%lu" + * On Windows: PRIu64 = "I64u" + * Using PRIu64 instead of "%lu" ensures portability. + * + * After this line: handle_str = "1" (for the first registration ever) + * + * Example values: "1", "2", "42", "18446744073709551615" + */ snprintf(handle_str, 32, "%" PRIu64, handler_id); FWUPMGR_INFO("Handle created: '%s'\n", handle_str); + /* + * [STEP 9] START THE INTERNAL ASYNC ENGINE + * + * This is where the library's internal machinery comes to life. + * BEFORE this call: Only 1 thread exists (the caller's main thread). + * AFTER this call: 2 threads exist (caller's + library BG thread). + * + * internal_system_init() does ALL of the following: + * + * Phase A: Initialize Check Callback Registry + * memset(&g_registry, 0, sizeof(g_registry)); + * pthread_mutex_init(&g_registry.mutex, NULL); + * g_registry.initialized = true; + * + * Result: 30-slot array, all CB_STATE_IDLE, protected by mutex + * Purpose: Will store callbacks for checkForUpdate() later + * + * Phase B: Initialize Background Thread Sync + * memset(&g_bg_thread, 0, sizeof(g_bg_thread)); + * pthread_mutex_init(&g_bg_thread.mutex, NULL); + * g_bg_thread.running = false; + * + * This mutex protects ONE bool: g_bg_thread.running + * Used for startup handshake between main thread and BG thread + * + * Phase C: Create ISOLATED GLib Event Loop + * g_bg_thread.context = g_main_context_new(); + * g_bg_thread.main_loop = g_main_loop_new(context, FALSE); + * + * WHY a new context (not the default)? + * If the client app uses GTK or its own GMainLoop, we'd be + * injecting our signal handlers into THEIR event loop. Their UI + * callbacks and our firmware callbacks would run interleaved on + * the same thread -- causing thread-safety bugs. Our own context + * guarantees our signals fire ONLY in our BG thread. + * + * Phase D: Spawn Background Thread + * pthread_create(&g_bg_thread.thread, NULL, + * background_thread_func, NULL); + * + * The BG thread immediately: + * 1. Pushes our GMainContext as its thread-default + * 2. Creates a PERSISTENT D-Bus connection (different from ours!) + * 3. Subscribes to 3 signals: + * - CheckForUpdateComplete -> on_check_complete_signal() + * - DownloadProgress -> on_download_progress_signal() + * - UpdateProgress -> on_update_progress_signal() + * 4. Sets g_bg_thread.running = true (under mutex) + * 5. Calls g_main_loop_run() -- BLOCKS FOREVER waiting for signals + * + * Phase E: Main Thread Spin-Waits for BG Thread Readiness + * for (int i = 0; i < 50; i++) { max 50 x 100ms = 5 seconds + * lock(g_bg_thread.mutex); + * bool ready = g_bg_thread.running; + * unlock(g_bg_thread.mutex); + * if (ready) break; + * nanosleep(100ms); + * } + * + * WHY spin-wait instead of condvar? + * Simplicity. This is a one-time startup. A condvar would add + * complexity for ~200ms of waiting. Not worth it. + * + * Phase F: Initialize Download and Update Registries + * memset(&g_dwnl_registry, 0, sizeof(...)); + * pthread_mutex_init(&g_dwnl_registry.mutex, NULL); + * g_dwnl_registry.initialized = true; + * + * memset(&g_update_registry, 0, sizeof(...)); + * pthread_mutex_init(&g_update_registry.mutex, NULL); + * g_update_registry.initialized = true; + * + * AFTER internal_system_init() returns successfully: + * THREADS: 2 (caller's + BG thread blocked in g_main_loop_run) + * MUTEXES: 4 (g_registry.mutex, g_bg_thread.mutex, + * g_dwnl_registry.mutex, g_update_registry.mutex) + * REGISTRIES: 3 (all 30 slots IDLE, ready to accept callbacks) + * D-BUS: 1 persistent connection in BG thread (for signal reception) + * + * If internal_system_init() returns non-zero (failure): + * - We log an error but STILL return the handle + * - The handle is valid for the daemon, but callbacks won't work + * - This is a degraded state (TODO: consider returning NULL here) + */ FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen ===\n"); if (internal_system_init() != 0) { FWUPMGR_ERROR("rdkFwupdateMgr_lib_init: internal_system_init FAILED\n"); } FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen successfull ===\n"); - + + /* + * [STEP 10] RETURN THE HANDLE TO THE CALLER + * + * FirmwareInterfaceHandle is typedef'd as (char*) in the public header. + * We cast here to make the type explicit, even though char* to char* + * doesn't technically need a cast. It documents intent. + * + * What the caller receives: A pointer to a heap-allocated string like "1". + * + * STATE OF THE WORLD after this return: + * + * Caller's process: + * Main thread: running (has the handle, about to call checkForUpdate etc.) + * BG thread: BLOCKED in g_main_loop_run(), waiting for D-Bus signals + * Owns persistent D-Bus connection (e.g., :1.141) + * Subscribed to CheckForUpdateComplete, DownloadProgress, UpdateProgress + * + * Daemon process: + * registered_processes hash table contains: + * key=1 -> ProcessInfo { handler_id=1, process_name="example_plugin", + * lib_version="1.0.0", sender_id=":1.140" } + * + * D-Bus connections: + * :1.140 -- DEAD (was our ephemeral connection, already closed) + * :1.141 -- ALIVE (BG thread's persistent connection for signals) + */ return (FirmwareInterfaceHandle)handle_str; } -/** - * @brief Unregister a previously registered process +/* + * unregisterProcess - Tear down the library and deregister from the daemon. * - * See rdkFwupdateMgr_process.h for full API documentation. + * OVERVIEW * - * IMPLEMENTATION NOTES: - * - Best-effort cleanup (errors are logged but not propagated) - * - Frees handle memory regardless of D-Bus call success - * - Idempotent: Safe to call with NULL handle (no-op) - * - Daemon may already have removed the registration (connection lost) + * PURPOSE: + * This is the MANDATORY last call before the client process exits. + * It is the mirror image of registerProcess(). Where registerProcess() + * builds up the machinery (thread, mutexes, registries, D-Bus connection), + * this function tears it ALL down in the correct order. * - * ERROR HANDLING: - * - NULL handle: No-op, returns immediately - * - D-Bus errors: Logged to stderr, but cleanup continues - * - Memory freed regardless of D-Bus call success + * Specifically, it does three things: + * 1. Shuts down the library's internal async engine (kills the BG thread, + * destroys all 4 mutexes, frees all 3 callback registries) + * 2. Tells the daemon "I'm leaving" via a D-Bus call (best-effort) + * 3. Frees the handle string that registerProcess() malloc'd + * + * WHY THE ORDER MATTERS: + * LOCAL cleanup (step 1) happens BEFORE the D-Bus call (step 2). + * This is deliberate: + * - After we tell the daemon "I'm leaving", it stops sending signals. + * - If the BG thread was still alive, it would sit forever in + * g_main_loop_run() waiting for signals that never come. + * - pthread_join() would block indefinitely. + * So we kill the BG thread FIRST, then tell the daemon. + * The D-Bus call is best-effort anyway -- if it fails, the daemon + * eventually cleans up stale registrations on its own. + * + * THREADING MODEL: + * - This function runs on the CALLER'S thread (main thread of example_app) + * - BEFORE this call: 2 threads (caller's + BG thread) + * - AFTER this call: 1 thread (caller's only -- BG thread joined and dead) + * - The D-Bus call (step 2) is synchronous, blocks up to 5 seconds + * + * D-BUS WIRE PROTOCOL: + * Method: "UnregisterProcess" + * Input: GVariant type "(t)" -- one uint64: handler_id + * Output: GVariant type "(b)" -- one boolean: success + * This creates an EPHEMERAL D-Bus connection (different sender ID from + * every other call). The daemon matches by handler_id, not sender. + * + * MEMORY CONTRACT: + * - This function FREES the handle string (the pointer becomes invalid) + * - Caller must NOT use the handle after this call returns + * - Caller should set their local copy to NULL as defensive practice + * + * RETURN VALUE: + * void -- this function always succeeds from the caller's perspective. + * All errors are logged but swallowed. Best-effort cleanup. + * + * SAFE TO CALL WITH NULL: + * Passing NULL is a no-op. This allows the caller to do: + * unregisterProcess(handle); (where handle might be NULL) + * without needing a NULL check at every call site. + * + * EXECUTION FLOW (step numbers match code comments below): + * + * [1] NULL check -- if NULL, return immediately (no-op) + * [2] Parse handle string "1" to uint64 handler_id = 1 + * Uses strtoull() with strict validation (reject garbage) + * [3] internal_system_deinit() -- tear down the async engine: + * - g_main_loop_quit() wakes BG thread from g_main_loop_run() + * - pthread_join() waits for BG thread to exit + * - Free GMainLoop and GMainContext + * - Destroy g_bg_thread.mutex + * - Free download registry (handle_keys + mutex) + * - Free update registry (handle_keys + mutex) + * - Free check registry (handle_keys + mutex) + * After this: 1 thread, 0 mutexes, 0 D-Bus connections + * [4] Create ephemeral D-Bus proxy (best-effort) + * [5] Call "UnregisterProcess" on daemon (best-effort, blocks up to 5s) + * [6] Extract success boolean from daemon reply + * [7] free(handler) -- always, regardless of D-Bus outcome + * + * @param handler The handle returned by registerProcess(). May be NULL. + * After this call returns, this pointer is INVALID (freed). + * + * Note: This function is deliberately tolerant of errors. Every failure + * path still frees the handle and returns cleanly. The daemon's + * ProcessInfo entry may be orphaned if the D-Bus call fails, but + * that is the daemon's responsibility to clean up. + * + * Warning: Do NOT call this from multiple threads with the same handle. + * Do NOT use the handle after this call returns. + * + * See also: registerProcess() -- the setup counterpart to this function. + * See also: internal_system_deinit() -- the async engine teardown. */ void unregisterProcess(FirmwareInterfaceHandle handler) { + /* + * LOCAL VARIABLE DECLARATIONS + * + * Same pattern as registerProcess(): declare all at the top so you + * can see every resource this function manages at a glance. + */ + + /* GDBusProxy for the D-Bus call to the daemon. + * Created in step 4, freed with g_object_unref() in step 6. + * May be NULL if proxy creation fails (D-Bus down, daemon gone). */ GDBusProxy *proxy = NULL; + + /* GError for capturing GLib/D-Bus error details. + * Must be freed with g_error_free() if non-NULL. */ GError *error = NULL; + + /* GVariant holding the daemon's reply to UnregisterProcess. + * Type "(b)" -- a tuple containing one boolean (success/failure). + * Must be freed with g_variant_unref() when done. */ GVariant *result = NULL; + + /* The numeric handler_id we'll extract from the handle string. + * This is what the daemon uses to look up our ProcessInfo entry. + * Parsed from the handle string "1" -> handler_id = 1. */ guint64 handler_id = 0; + + /* The daemon's reply: TRUE if it found and removed our registration, + * FALSE if handler_id was not found (already removed, or invalid). */ gboolean success = FALSE; - // NULL check: Safe to unregister NULL handle (no-op) + /* + * [STEP 1] NULL CHECK -- safe no-op for NULL handles + * + * Why allow NULL? + * Client code often has cleanup paths like: + * cleanup: + * unregisterProcess(handle); + * If registerProcess() failed, handle is NULL. Making this a safe + * no-op avoids the caller needing "if (handle != NULL)" every time. + * + * No resources to clean up here -- we haven't allocated anything. + */ if (!handler) { FWUPMGR_INFO("unregisterProcess() called with NULL handle (no-op)\n"); return; @@ -405,29 +912,90 @@ void unregisterProcess(FirmwareInterfaceHandle handler) FWUPMGR_INFO("unregisterProcess() called\n"); FWUPMGR_INFO(" handle: '%s'\n", handler); - // Parse handler_id from string handle with strict validation - // Reject handles with leading/trailing whitespace or garbage characters - // Valid examples: "12345", "0" - // Invalid examples: "123abc", " 123", "123 ", "", "abc" + /* + * [STEP 2] PARSE THE HANDLE STRING TO uint64 + * + * The handle is a string like "1" or "42". We need to convert it + * back to a uint64 handler_id for the D-Bus call to the daemon. + * + * Why strtoull() instead of atoi() or sscanf()? + * - atoi() has NO error detection. atoi("abc") returns 0 silently. + * - sscanf() doesn't detect trailing garbage: sscanf("123abc", "%llu") + * happily returns 123 and ignores "abc". + * - strtoull() with endptr checking is the ONLY way in C to do + * strict numeric parsing: + * - Sets errno on overflow + * - endptr tells you exactly where parsing stopped + * - You can reject partial parses ("123abc") by checking *endptr + * + * Why set errno = 0 before the call? + * strtoull() only sets errno on error. If errno was already non-zero + * from some earlier unrelated call, we'd get a false positive. + * Always clear errno before calling strto* functions. + * + * What is endptr? + * After strtoull("123abc", &endptr, 10): + * - Return value = 123 + * - endptr points to 'a' (first character it couldn't parse) + * After strtoull("123", &endptr, 10): + * - Return value = 123 + * - endptr points to '\0' (end of string -- everything parsed) + * After strtoull("abc", &endptr, 10): + * - Return value = 0 + * - endptr == handler (didn't move -- nothing parsed) + */ errno = 0; char *endptr = NULL; handler_id = strtoull(handler, &endptr, 10); - - // Validation checks: - // 1. errno != 0: overflow or underflow occurred - // 2. endptr == handler: no digits were parsed (e.g., "abc") - // 3. *endptr != '\0': garbage after number (e.g., "123abc") - // 4. handler_id == 0: could be valid "0" or invalid input - check endptr + + /* + * VALIDATION CHECK 1: Numeric overflow + * + * If the string represents a number larger than ULLONG_MAX + * (18446744073709551615), strtoull() returns ULLONG_MAX and sets + * errno to ERANGE. This catches "99999999999999999999999". + */ if (errno != 0) { FWUPMGR_ERROR("Invalid handle: numeric overflow/underflow in '%s'\n", handler); free(handler); return; } + + /* + * VALIDATION CHECK 2: No digits at all + * + * If endptr == handler, strtoull() didn't find any digits. + * This catches "abc", "", and other non-numeric strings. + * + * Why is this separate from the *endptr check below? + * strtoull("abc") returns 0 with endptr pointing to 'a'. + * *endptr != '\0' would also catch it, but "endptr == handler" + * gives a more specific error message: "no digits found" + * vs "garbage after number". + */ if (endptr == handler) { FWUPMGR_ERROR("Invalid handle: no digits found in '%s'\n", handler); free(handler); return; } + + /* + * VALIDATION CHECK 3: Trailing garbage after the number + * + * If *endptr is not the null terminator, there are characters + * after the valid number. This catches: + * "123abc" (endptr points to 'a') + * "123 " (endptr points to ' ') + * " 123" (strtoull skips leading whitespace, so this actually + * parses as 123 with endptr at '\0' -- PASSES this check. + * But our registerProcess() never creates handles with + * leading spaces, so this is academic.) + * + * Why is this important? + * If the handle string is corrupted (memory corruption elsewhere), + * it might look like "1\x03garbage". We don't want to silently + * parse it as handler_id=1 and proceed -- that could mask a bug. + */ if (*endptr != '\0') { FWUPMGR_ERROR("Invalid handle: garbage characters after number in '%s' " "(parsed %" PRIu64 ", but '%s' remains)\n", @@ -435,18 +1003,147 @@ void unregisterProcess(FirmwareInterfaceHandle handler) free(handler); return; } + + /* + * VALIDATION CHECK 4: handler_id must be > 0 + * + * The daemon assigns handler_ids starting at 1 (next_process_id = 1, + * post-increment). A handler_id of 0 is NEVER valid. If we got 0, + * either the string was literally "0" or something went wrong. + * + * The daemon also rejects handler_id == 0 on its side (returns + * D-Bus error), but we catch it here to avoid a wasted round-trip. + */ if (handler_id == 0) { FWUPMGR_ERROR("Invalid handle: handler_id cannot be 0\n"); free(handler); return; } + /* + * NOTE ON free(handler) IN ALL ERROR PATHS ABOVE: + * + * Every validation failure path calls free(handler) before returning. + * This is critical: the handle string was malloc'd by registerProcess(). + * If we return without freeing it, that's a memory leak. The caller + * will likely set their pointer to NULL after this call, so nobody + * else will free it. + * + * Also note: internal_system_deinit() is NOT called in these paths. + * If the handle is corrupt, we don't know what state the system is in. + * The BG thread and mutexes leak, but that's acceptable -- handle + * corruption means something catastrophic happened, and the OS will + * reclaim everything when the process exits anyway. + */ + FWUPMGR_INFO(" handler_id: %"G_GUINT64_FORMAT"\n", handler_id); + + /* + * [STEP 3] TEAR DOWN THE INTERNAL ASYNC ENGINE + * + * This is the most critical step. We tear down EVERYTHING that + * internal_system_init() created during registerProcess(). + * + * BEFORE this call: + * Threads: 2 (main + BG) + * Mutexes: 4 (g_registry, g_bg_thread, g_dwnl_registry, g_update_registry) + * Registries: 3 (check, download, update -- all 30 slots each) + * D-Bus: 1 persistent connection in BG thread (:1.141) + * GLib: 1 GMainLoop + 1 GMainContext (owned by BG thread) + * + * AFTER this call: + * Threads: 1 (main only -- BG thread joined and dead) + * Mutexes: 0 (all 4 destroyed) + * Registries: 3 (handle_key strings freed, but struct memory is static) + * D-Bus: 0 (BG thread closed its connection during cleanup) + * GLib: 0 (main_loop and context unref'd) + * + * internal_system_deinit() does the following, in this exact order: + * + * 1. g_main_loop_quit(g_bg_thread.main_loop) + * Sends a "quit" signal to the GLib event loop that the BG thread + * is blocking in. This is THREAD-SAFE -- GLib explicitly allows + * calling quit from a different thread than the one running the loop. + * Internally, GLib writes to a wakeup pipe/eventfd. + * + * When quit fires, the BG thread's g_main_loop_run() returns. + * The BG thread then: + * a. Unsubscribes from all D-Bus signals (no more callbacks) + * b. g_object_unref(connection) -- closes :1.141 + * c. g_main_context_pop_thread_default() -- detaches context + * d. return NULL -- pthread exits + * + * 2. pthread_join(g_bg_thread.thread, NULL) + * BLOCKS the main thread until the BG thread has fully exited. + * After this returns: + * - The BG thread is DEAD (its stack is freed by the OS) + * - No more signal callbacks can fire + * - No more mutex contention on registries + * - It is safe to destroy mutexes + * + * WHY is pthread_join essential? + * If we skipped it and went straight to mutex_destroy, the BG + * thread might still be holding g_registry.mutex while dispatching + * a late-arriving signal. pthread_mutex_destroy on a locked mutex + * is UNDEFINED BEHAVIOR (potential crash or silent corruption). + * + * 3. g_main_loop_unref() + g_main_context_unref() + * Free the GLib event loop objects. The BG thread already popped + * the context, so these are the final references. + * + * 4. pthread_mutex_destroy(&g_bg_thread.mutex) + * Destroy the mutex that protected g_bg_thread.running. + * Nobody uses it anymore -- we just joined the only other thread. + * Mutexes remaining: 3 + * + * 5. internal_dwnl_system_deinit() + * Lock g_dwnl_registry.mutex, iterate all 30 download slots, + * free() any non-NULL handle_key strings (from strdup during + * downloadFirmware calls), unlock, then destroy the mutex. + * Mutexes remaining: 2 + * + * Why lock even though only 1 thread exists? Defensive coding. + * If someone refactors and this runs while threads are alive, + * the lock prevents a race. + * + * 6. internal_update_system_deinit() + * Same pattern as download. Lock, free handle_keys, destroy mutex. + * Mutexes remaining: 1 + * + * 7. Lock g_registry.mutex, free all check-registry handle_keys, + * unlock, destroy mutex. + * Mutexes remaining: 0 + * + * After internal_system_deinit() returns, the library is in a + * "dormant" state: no threads, no mutexes, no D-Bus connections. + * Only the handle string and handler_id still exist. + */ FWUPMGR_INFO("=== rdkFwupdateMgr destroy thred unloading ===\n"); internal_system_deinit(); FWUPMGR_INFO("=== rdkFwupdateMgr destory thread ===\n"); - // Create D-Bus proxy + /* + * [STEP 4] CREATE D-BUS PROXY (best-effort) + * + * Same as registerProcess() -- create_dbus_proxy() opens a NEW + * ephemeral D-Bus connection, gets a new unique sender name + * (e.g., :1.145), and creates a GDBusProxy for method calls. + * + * Why "best-effort"? + * If the daemon has crashed, the D-Bus bus is down, or the system + * bus socket is inaccessible, proxy creation fails. That's OK. + * The important cleanup (BG thread, mutexes, memory) was already + * done in step 3. The D-Bus call is just a courtesy to the daemon. + * + * Why FWUPMGR_WARN and not FWUPMGR_ERROR? + * WARN means "something unexpected happened but we can continue." + * ERROR means "we're returning a failure code to the caller." + * Since unregisterProcess() is void and always succeeds from the + * caller's perspective, failures here are warnings, not errors. + * + * Note: free(handler) is called even when proxy creation fails. + * The handle MUST be freed on every path. No exceptions. + */ proxy = create_dbus_proxy(&error); if (!proxy) { FWUPMGR_WARN("Failed to create D-Bus proxy for unregister\n"); @@ -454,12 +1151,58 @@ void unregisterProcess(FirmwareInterfaceHandle handler) FWUPMGR_WARN(" Error: %s\n", error->message); g_error_free(error); } - // Continue with cleanup even if D-Bus call fails + /* Continue with cleanup even if D-Bus call fails */ free(handler); return; } - // Call UnregisterProcess D-Bus method + /* + * [STEP 5] SEND "UnregisterProcess" D-BUS METHOD CALL + * + * g_dbus_proxy_call_sync() -- same as in registerProcess(), but + * with a different method name and argument type. + * + * Wire format: + * Method name: "UnregisterProcess" + * Arguments: g_variant_new("(t)", handler_id) + * "(t)" = a tuple containing one uint64 + * We send handler_id = 1 + * + * Expected reply: + * "(b)" = a tuple containing one boolean + * TRUE = daemon found the registration and removed it + * FALSE = daemon didn't find handler_id (already removed, or unknown) + * + * BLOCKING BEHAVIOR: + * Main thread blocks here for up to DBUS_TIMEOUT_MS (5 seconds). + * Typical response time: <5ms (just a hash table lookup + remove). + * + * WHAT THE DAEMON DOES: + * 1. Extracts handler_id from the message: g_variant_get("(t)", &handler) + * 2. Validates handler != 0 (rejects with D-Bus error if 0) + * 3. Looks up ProcessInfo in registered_processes hash table + * 4. Calls remove_process_from_tracking(handler_id): + * - g_hash_table_lookup(registered_processes, handler_id) + * - If found: g_hash_table_remove() which also calls g_free() + * on the ProcessInfo struct (freeing process_name, lib_version, + * sender_id strings that were g_strdup'd during registration) + * - Returns TRUE if found and removed, FALSE if not found + * 5. Sends reply: g_variant_new("(b)", TRUE/FALSE) + * + * NOTE: The daemon ignores the sender_id of this call. + * Our registerProcess was sent from :1.140, but this unregister + * comes from :1.145 (different ephemeral connection). The daemon + * matches ONLY by handler_id. The sender_id parameter in + * remove_process_from_tracking() is unused (cast to void). + * + * FAILURE HANDLING: + * If this call fails (timeout, daemon crashed, bus error): + * - Log a WARNING (not error -- best-effort) + * - Still free the proxy, still free the handle + * - Return -- caller's perspective: unregister succeeded + * The daemon will eventually clean up its stale ProcessInfo entry + * through its own periodic cleanup or on next daemon restart. + */ FWUPMGR_INFO("Calling UnregisterProcess D-Bus method...\n"); result = g_dbus_proxy_call_sync( proxy, @@ -467,22 +1210,60 @@ void unregisterProcess(FirmwareInterfaceHandle handler) g_variant_new("(t)", handler_id), G_DBUS_CALL_FLAGS_NONE, DBUS_TIMEOUT_MS, - NULL, // GCancellable + NULL, /* GCancellable */ &error ); if (!result) { + /* + * D-Bus call failed. Common causes: + * - Daemon not running (crashed or was stopped) + * - 5-second timeout expired + * - D-Bus bus itself restarted + * + * This is logged as WARN, not ERROR, because: + * 1. unregisterProcess() is void -- no error code to return + * 2. The important cleanup (thread, mutexes) already succeeded + * 3. The daemon's stale entry is the daemon's problem, not ours + * + * "This is OK if daemon already cleaned up" -- if the daemon + * crashed, it already lost all its ProcessInfo entries. When it + * restarts, it starts fresh. Our registration is already gone. + */ FWUPMGR_WARN("UnregisterProcess D-Bus call failed: %s\n", error->message); FWUPMGR_WARN(" (This is OK if daemon already cleaned up)\n"); g_error_free(error); g_object_unref(proxy); - // Continue with local cleanup + /* Continue with local cleanup */ free(handler); return; } - // Extract success flag from result + /* + * [STEP 6] EXTRACT THE SUCCESS FLAG FROM DAEMON'S REPLY + * + * The daemon replied with GVariant type "(b)": + * '(' = tuple start + * 'b' = gboolean (TRUE or FALSE) + * ')' = tuple end + * + * g_variant_get() deserializes into our 'success' variable. + * + * Then we free the GVariant reply and the proxy. The ephemeral + * D-Bus connection :1.145 is now closed. + * + * Possible values: + * success == TRUE: Daemon found handler_id=1, removed ProcessInfo, + * freed process_name/lib_version/sender_id strings. + * registered_processes is now empty (0 entries). + * + * success == FALSE: Daemon didn't find handler_id=1. This can happen if: + * - We already unregistered (double call) + * - Daemon restarted and lost its in-memory state + * - handler_id was somehow wrong + * We log a warning but don't treat it as fatal. + */ g_variant_get(result, "(b)", &success); g_variant_unref(result); g_object_unref(proxy); @@ -494,7 +1275,51 @@ void unregisterProcess(FirmwareInterfaceHandle handler) FWUPMGR_WARN(" (Handler may have already been unregistered)\n"); } - // Free handle memory (always, regardless of D-Bus call success) + /* + * [STEP 7] FREE THE HANDLE STRING + * + * This frees the 32-byte malloc'd string that registerProcess() + * created in its Step 7 (e.g., the string "1"). + * + * After this line, the 'handler' pointer is INVALID. Dereferencing + * it is undefined behavior (use-after-free). The caller must NOT + * use the handle after unregisterProcess() returns. + * + * Good practice in the caller: + * unregisterProcess(g_handle); + * g_handle = NULL; // prevent accidental use-after-free + * + * This free() happens on EVERY code path: + * - Normal success path (here) + * - D-Bus call failure (step 5 failure branch) + * - Proxy creation failure (step 4 failure branch) + * - Handle parse failure (step 2 failure branches) + * The ONLY path that doesn't free is the NULL check (step 1), + * because there's nothing to free. + * + * STATE OF THE WORLD AFTER THIS RETURNS: + * + * example_app process: + * Threads: 1 (main thread only) + * BG thread: DEAD (joined in step 3) + * Mutexes: 0 (all 4 destroyed in step 3) + * Registries: wiped (handle_keys freed) + * D-Bus: 0 connections (all closed) + * Handle: FREED and INVALID + * + * Daemon process: + * registered_processes: empty (ProcessInfo for "example_plugin" removed) + * next_process_id: 2 (monotonically increasing, never resets) + * + * D-Bus connections (all dead): + * :1.140 -- was registerProcess ephemeral (dead since registration) + * :1.141 -- was BG thread persistent (closed in step 3) + * :1.145 -- was this unregister ephemeral (just closed above) + * + * The library is back to "UNLINKED" state. If needed, the caller + * could call registerProcess() again to start a new session. + * The daemon would assign handler_id=2 this time. + */ free(handler); FWUPMGR_INFO("Handle memory freed\n"); } From 2dc58c7b1032c89900be605c7b7f49f208045b19 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 02:42:48 +0000 Subject: [PATCH 14/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Documentation for checkForUpdate API --- librdkFwupdateMgr/src/rdkFwupdateMgr_api.c | 317 +++++- librdkFwupdateMgr/src/rdkFwupdateMgr_async.c | 964 +++++++++++++++++-- 2 files changed, 1159 insertions(+), 122 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index edaa2985..6f965d63 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -49,37 +49,159 @@ #include #include -/* ======================================================================== - * checkForUpdate — SYNCHRONOUS implementation - * ======================================================================== */ - -/** - * @brief Check for firmware update — non-blocking, returns immediately +/* + * checkForUpdate - Initiate a non-blocking firmware availability check. + * + * OVERVIEW + * + * PURPOSE: + * This is the FIRST async API call in the firmware update workflow. + * After registerProcess() gives you a handle, you call this to ask + * the daemon "is there a new firmware version for this device?" + * + * This function is NON-BLOCKING. It returns immediately (in ~3ms) + * with CHECK_FOR_UPDATE_SUCCESS, meaning "your request was accepted." + * The actual answer (FIRMWARE_AVAILABLE or FIRMWARE_NOT_AVAILABLE) + * arrives later (5-30 seconds) via your callback function, which is + * invoked by the library's background thread. + * + * WHAT "FIRE-AND-FORGET" MEANS: + * The D-Bus call to the daemon is fire-and-forget: + * - We send the message and do NOT wait for a reply + * - The daemon's method response is silently discarded (we already + * closed our ephemeral D-Bus connection by then) + * - The real answer comes as a BROADCAST D-Bus signal: + * "CheckForUpdateComplete" -- caught by the BG thread + * + * Think of it like mailing a letter: you drop it in the mailbox + * (return SUCCESS) and walk away. The reply comes later by separate + * delivery (the callback). + * + * THREADING MODEL: + * - This function runs on the CALLER'S thread (main thread) + * - It does NOT block the caller + * - The callback fires on the BACKGROUND thread (created during + * registerProcess -> internal_system_init) + * - The caller typically sleeps on a condvar until the callback + * sets a flag and signals it + * + * D-BUS WIRE PROTOCOL: + * Method: "CheckForUpdate" + * Input: GVariant type "(s)" -- one string: the handle (e.g., "1") + * Reply: IGNORED (fire-and-forget -- three trailing NULLs) + * Signal: "CheckForUpdateComplete" type "(tiissss)" + * t handler_id (uint64) + * i result_code (int32) + * i status_code (int32: 0=available, 1=not available, 3=error) + * s current_version (e.g., "RDKV_7.0") + * s available_version (e.g., "RDKV_8.0") + * s update_details (pipe-separated "Key:Value|Key:Value|...") + * s status_message (human-readable) + * + * CONNECTION MODEL: + * This creates an EPHEMERAL D-Bus connection (e.g., :1.142) that + * lives only for the duration of this function call. The BG thread + * has its own PERSISTENT connection (:1.141) for receiving signals. + * These are completely independent. + * + * CALLBACK CONTRACT: + * - Fires exactly ONCE per checkForUpdate() call + * - Fires on the BG thread, NOT the caller's thread + * - Receives a const FwInfoData* that is STACK-ALLOCATED in the + * dispatch function -- valid ONLY during the callback + * - If you need data after the callback returns, you MUST copy it + * (e.g., strncpy to your own buffers) + * - If the daemon crashes or the signal never arrives, the callback + * NEVER fires -- the caller should use a condvar timeout (120s) * - * Sends CheckForUpdate(handle) to the daemon and returns immediately. - * The daemon will query the XConf server in the background (5-30 seconds) - * and emit a CheckForUpdateComplete signal when done. + * RETURN VALUES: + * CHECK_FOR_UPDATE_SUCCESS (0) -- Request sent. Callback will fire later. + * CHECK_FOR_UPDATE_FAIL (1) -- Request failed. Callback will NOT fire. + * IMPORTANT: SUCCESS does NOT mean firmware is available. It means + * the request was accepted. Actual availability comes in the callback. * - * The callback fires ONCE when the signal arrives with complete firmware info: - * - FwInfoData.status: FIRMWARE_AVAILABLE, FIRMWARE_NOT_AVAILABLE, etc. - * - FwInfoData.CurrFWVersion: Current firmware version - * - FwInfoData.UpdateDetails: Details about available update (if any) + * EXECUTION FLOW (step numbers match code comments below): * - * The callback is registered in the async registry before sending the D-Bus call - * to ensure the signal doesn't arrive before we're ready to receive it. + * [1] Validate handle and callback (reject NULL/empty) + * [2] Open ephemeral D-Bus connection (fail early if D-Bus is down) + * [3] Register callback in g_registry (mutex-protected) + * -- MUST happen BEFORE sending the D-Bus call to avoid race + * [4] Send fire-and-forget "CheckForUpdate" D-Bus method call + * [5] Close ephemeral connection, return SUCCESS + * + * [Later, 5-30 seconds -- on BG thread:] + * Daemon broadcasts "CheckForUpdateComplete" signal + * BG thread receives it in on_check_complete_signal() + * dispatch_all_pending() finds our PENDING slot, invokes our callback + * Slot is reset to IDLE after callback returns + * + * WHY REGISTER BEFORE SEND (Step 3 before Step 4): + * If we sent the D-Bus call FIRST and the daemon responded instantly + * (e.g., cached result), the BG thread would receive the signal before + * we registered the callback. dispatch_all_pending() would scan the + * registry, find zero PENDING entries, and discard the signal. + * Our callback would never fire. The app would hang on condvar forever. + * + * By registering FIRST, the callback is waiting in the registry before + * the daemon can possibly respond. Race condition eliminated. + * + * WHY CONNECT BEFORE REGISTER (Step 2 before Step 3): + * If we registered the callback FIRST and then D-Bus connection failed, + * we'd have a "ghost" PENDING entry that will never be dispatched + * (because the D-Bus call was never sent, so the signal will never + * arrive). The slot would stay PENDING forever, wasting 1 of 30 slots. + * + * By connecting FIRST, we know D-Bus is up before we touch the registry. + * If connection fails, we return FAIL with a clean registry. + * + * @param handle The handle returned by registerProcess(). Must be + * non-NULL and non-empty. e.g., "1" + * @param callback Function pointer to invoke when the daemon's signal + * arrives. Must be non-NULL. Signature: + * void callback(const FwInfoData *fwinfodata) * - * @param handle Valid FirmwareInterfaceHandle from registerProcess() - * @param callback Invoked when CheckForUpdateComplete signal arrives * @return CHECK_FOR_UPDATE_SUCCESS or CHECK_FOR_UPDATE_FAIL + * + * See also: on_check_complete_signal() -- BG thread signal handler + * See also: dispatch_all_pending() -- two-phase callback dispatch + * See also: internal_register_callback() -- registry slot allocation */ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, UpdateEventCallback callback) { - /* [1] Validate */ + /* + * [STEP 1] INPUT VALIDATION + * + * Reject obviously bad inputs before touching D-Bus or the registry. + * This is the library's input boundary -- validate everything here. + */ + + /* + * Check 1a: handle must not be NULL and must not be empty "". + * + * handle is the string "1" from registerProcess(). If the caller + * passes NULL (forgot to check registerProcess return value) or + * somehow has an empty string, reject immediately. + * + * handle[0] == '\0' catches the empty string case that a simple + * NULL check would miss. An empty handle would cause the daemon + * to reject the request anyway, but we catch it here to avoid + * a wasted D-Bus round-trip. + */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("checkForUpdate: invalid handle (NULL or empty)\n"); return CHECK_FOR_UPDATE_FAIL; } + + /* + * Check 1b: callback must not be NULL. + * + * If the caller passes NULL, they'll never receive the firmware + * check result. That's a programming error -- they probably forgot + * to pass their callback function. Catch it here with a clear + * error message rather than crashing later when we try to call + * through a NULL function pointer. + */ if (callback == NULL) { FWUPMGR_ERROR("checkForUpdate: callback is NULL\n"); return CHECK_FOR_UPDATE_FAIL; @@ -87,25 +209,82 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, FWUPMGR_INFO("checkForUpdate: handle='%s'\n", handle); - /* [2] Connect to D-Bus FIRST before registering callback + /* + * [STEP 2] CREATE EPHEMERAL D-BUS CONNECTION * - * This prevents stale registry entries if D-Bus connection fails. - * We only register the callback if we can successfully send the request. + * g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) opens a new connection to + * the system D-Bus bus. This connection gets a unique sender name + * like :1.142 -- different from every other connection. + * + * Why a NEW connection instead of reusing the BG thread's :1.141? + * The BG thread's connection is attached to the BG thread's + * GMainContext. Using it from the main thread would require + * cross-thread GLib context management -- complex and fragile. + * A fresh per-call connection is simpler and avoids any + * thread-safety issues with GLib internals. + * + * Why BEFORE registering the callback? + * If D-Bus is down (dbus-daemon crashed, socket missing), this + * call fails. We want to fail BEFORE polluting the callback + * registry with a PENDING entry that will never be dispatched. + * Clean failure: no registry entry, no dangling state. + * + * Cost: ~2ms for the D-Bus handshake. Negligible for a firmware + * check that takes 5-30 seconds total. */ GError *error = NULL; GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (conn == NULL) { + /* + * D-Bus connection failed. Common causes: + * - dbus-daemon not running + * - System bus socket missing (/var/run/dbus/system_bus_socket) + * - Permission denied (D-Bus policy rejects our user) + * + * Return FAIL -- the caller should check if the daemon is running. + * No registry entry was created, so nothing to clean up. + */ FWUPMGR_ERROR("checkForUpdate: D-Bus connect failed: %s\n", error ? error->message : "unknown"); if (error) g_error_free(error); return CHECK_FOR_UPDATE_FAIL; } - /* [3] Register callback AFTER D-Bus connection succeeds + /* + * [STEP 3] REGISTER CALLBACK IN THE CHECK-FOR-UPDATE REGISTRY * - * Register immediately before sending to avoid race condition where - * the daemon responds before we're ready to receive the signal. + * internal_register_callback() does the following (see _async.c): + * 1. Locks g_registry.mutex + * 2. Scans all 30 slots for: + * a. An existing PENDING entry with the same handle (dedup) + * b. The first IDLE slot (free slot) + * 3. If same handle found: overwrites it (prevents ghost callbacks) + * If free slot found: uses it + * If neither: returns false (registry full -- 30 concurrent checks!) + * 4. Populates the slot: + * - handle_key = strdup(handle) -- "1" (heap copy, freed on reset) + * - callback = our function pointer + * - state = CB_STATE_PENDING + * - registered_time = current unix timestamp + * 5. Unlocks g_registry.mutex + * 6. Returns true + * + * After this call, the registry has one PENDING entry. When the + * BG thread receives the "CheckForUpdateComplete" signal, it will + * find this entry and invoke the callback. + * + * Why BEFORE the D-Bus call? + * Race condition prevention. If the daemon responds faster than + * we can register (theoretically possible with cached results), + * the BG thread would find an empty registry and drop the signal. + * Registering first guarantees the callback is waiting. + * + * Failure case: registry full (30 concurrent pending checks). + * This means 30 different checkForUpdate() calls are all waiting + * for callbacks simultaneously. In practice this never happens -- + * a single client typically has 1 pending check at a time. + * If it does happen, we clean up the D-Bus connection and fail. */ if (!internal_register_callback(handle, callback)) { FWUPMGR_ERROR("checkForUpdate: registry full, handle='%s'\n", handle); @@ -113,14 +292,50 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, return CHECK_FOR_UPDATE_FAIL; } - /* [4] Fire-and-forget D-Bus CheckForUpdate method call + /* + * [STEP 4] SEND FIRE-AND-FORGET D-BUS METHOD CALL * - * Arguments: (s) - * s handle — identifies this app to the daemon + * g_dbus_connection_call() sends a D-Bus method call to the daemon. + * This is the ASYNC variant (not _sync). But we're not even using + * the async callback -- all three trailing NULLs mean "don't tell + * me what the daemon replied." * - * Three trailing NULLs = fire and forget (no reply waited for). - * g_dbus_connection_call() returns immediately. - * Daemon will emit CheckForUpdateComplete signal when XConf query finishes. + * Parameters to g_dbus_connection_call(): + * conn -- our ephemeral connection :1.142 + * DBUS_SERVICE_NAME -- "org.rdkfwupdater.Service" (daemon's well-known name) + * DBUS_OBJECT_PATH -- "/org/rdkfwupdater/Service" (daemon's object path) + * DBUS_INTERFACE_NAME -- "org.rdkfwupdater.Interface" + * DBUS_METHOD_CHECK -- "CheckForUpdate" (the method name) + * g_variant_new("(s)", handle) -- argument: the string "1" + * "(s)" means a tuple containing one string + * This tells the daemon which registered client is asking + * NULL -- expected reply type: we don't care + * G_DBUS_CALL_FLAGS_NONE -- no special flags + * DBUS_TIMEOUT_MS -- 5000ms (only for message queueing, not for reply) + * NULL -- GCancellable: no cancellation support + * NULL -- GAsyncReadyCallback: no reply callback + * NULL -- user_data for reply callback: N/A + * + * The three trailing NULLs (cancellable, callback, user_data) are what + * make this fire-and-forget. GLib queues the D-Bus message in the + * kernel's socket buffer and returns immediately. The daemon's reply + * (which it does send -- an immediate FIRMWARE_CHECK_ERROR meaning + * "I'm working on it") arrives at :1.142 but nobody reads it because + * we close the connection moments later. + * + * WHAT HAPPENS ON THE DAEMON SIDE (for context): + * 1. Daemon receives "CheckForUpdate" with argument "1" + * 2. Validates handler "1" is registered (yes, from registerProcess) + * 3. Sends immediate method reply: FIRMWARE_CHECK_ERROR (= "check in progress") + * -- This reply is DISCARDED because we're fire-and-forget + * 4. Creates a GTask and spawns a worker thread to query XConf + * 5. Worker thread does HTTP GET to XConf server (5-30 seconds) + * 6. When XConf responds, daemon broadcasts "CheckForUpdateComplete" signal + * -- THIS is what our BG thread is waiting for + * + * The daemon may also PIGGYBACK: if another client already triggered + * an XConf fetch, our request joins the waiting queue and gets the + * same result when the fetch completes. One HTTP request serves all. */ FWUPMGR_INFO("checkForUpdate: calling CheckForUpdate on daemon, handle='%s'\n", handle); @@ -140,13 +355,53 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, NULL /* user_data: none */ ); + /* + * [STEP 5] CLOSE EPHEMERAL CONNECTION AND RETURN + * + * g_object_unref(conn) closes our ephemeral D-Bus connection :1.142. + * The D-Bus message is already in the kernel socket buffer -- closing + * our end doesn't prevent delivery to the daemon. Think of it like + * dropping a letter in a mailbox and walking away. + * + * After this, the state of the world is: + * + * Main thread: + * - Returns CHECK_FOR_UPDATE_SUCCESS to the caller + * - Caller typically enters pthread_cond_timedwait (120s timeout) + * - Connection :1.142 is DEAD (just closed) + * + * BG thread: + * - Still sleeping in g_main_loop_run() on connection :1.141 + * - g_registry.entries[0] has our callback in PENDING state + * - Will wake up when CheckForUpdateComplete signal arrives + * + * Daemon: + * - Received our request, spawned XConf worker thread + * - Will broadcast signal when XConf responds (5-30s) + * + * Registry: + * entries[0] = { state=PENDING, handle_key="1", + * callback=on_firmware_check_callback } + * + * D-Bus connections: + * :1.140 -- registerProcess ephemeral (DEAD since registration) + * :1.141 -- BG thread persistent (ALIVE, listening for signals) + * :1.142 -- this checkForUpdate ephemeral (DEAD, just closed) + * + * SUCCESS here means: "I sent the request and registered your callback." + * It does NOT mean: "Firmware is available." or even "The daemon + * received the request." (Though it almost certainly did.) + * + * The callback will fire later on the BG thread. If it never fires + * (daemon crashed, XConf unreachable), the caller's condvar timeout + * will eventually expire and the caller can handle the timeout. + */ g_object_unref(conn); FWUPMGR_INFO("checkForUpdate: D-Bus call sent, returning SUCCESS. " "Callback will fire when CheckForUpdateComplete signal arrives. " "handle='%s'\n", handle); - /* [5] Return immediately — app is unblocked */ return CHECK_FOR_UPDATE_SUCCESS; } #if 0 diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c index f1ffcc46..57575d48 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c @@ -330,14 +330,66 @@ static void *background_thread_func(void *arg) * D-BUS SIGNAL HANDLER * ======================================================================== */ -/** - * @brief Called by GLib when CheckForUpdateComplete signal arrives +/* + * on_check_complete_signal - D-Bus signal handler for CheckForUpdateComplete. + * + * OVERVIEW + * + * PURPOSE: + * This function is called by GLib's D-Bus infrastructure when the daemon + * broadcasts the "CheckForUpdateComplete" signal. It is the ENTRY POINT + * for the "response" side of the checkForUpdate() async flow. + * + * WHEN DOES THIS FIRE? + * 5-30 seconds after checkForUpdate() was called. The daemon queried + * the XConf cloud server for firmware availability, got a response, + * and broadcast the result as a D-Bus signal to ALL listeners. + * + * WHICH THREAD RUNS THIS? + * The BACKGROUND THREAD. Not the main thread. This is critical. + * + * The BG thread is blocked in g_main_loop_run() waiting for events. + * When the signal arrives on the BG thread's persistent D-Bus connection + * (:1.141), GLib wakes the BG thread and dispatches to this handler. + * This handler was registered via g_dbus_connection_signal_subscribe() + * in background_thread_func() during registerProcess(). * - * Runs in the background thread context. + * PARAMETERS: + * conn -- the BG thread's persistent D-Bus connection (:1.141) + * sender -- the daemon's unique sender name (e.g., ":1.5") + * object_path -- "/org/rdkfwupdater/Service" + * interface_name -- "org.rdkfwupdater.Interface" + * signal_name -- "CheckForUpdateComplete" + * parameters -- GVariant of type "(tiissss)" containing the result + * user_data -- NULL (we use global state, not user_data) * - * 1. Parse GVariant payload → InternalSignalData - * 2. Dispatch to all PENDING registry entries - * 3. Free parsed signal data + * All parameters except 'parameters' are unused (cast to void). + * We only care about the GVariant payload. + * + * EXECUTION FLOW: + * 1. Parse the GVariant "(tiissss)" into an InternalSignalData struct + * (4 strdup'd strings: current_version, available_version, + * update_details, status_message) + * 2. Call dispatch_all_pending() which: + * a. Finds all PENDING registry entries + * b. Builds FwInfoData from the signal data + * c. Invokes each callback + * d. Resets each slot to IDLE + * 3. Free the 4 strdup'd strings via internal_cleanup_signal_data() + * + * ERROR HANDLING: + * If GVariant parsing fails (wrong type signature, corrupt data), + * we log an error and return without dispatching. Callbacks will + * NOT fire. The caller's condvar timeout will eventually expire. + * + * MEMORY: + * internal_parse_signal_data() allocates 4 strings via strdup(). + * internal_cleanup_signal_data() frees them after dispatch completes. + * The InternalSignalData struct itself is on the stack (this function's + * stack frame on the BG thread). + * + * After this function returns, the BG thread goes back to + * g_main_loop_run() and sleeps until the next signal. */ static void on_check_complete_signal(GDBusConnection *conn, const gchar *sender, @@ -347,71 +399,215 @@ static void on_check_complete_signal(GDBusConnection *conn, GVariant *parameters, gpointer user_data) { + /* + * Suppress "unused parameter" warnings. GLib's signal handler + * signature requires all 7 parameters, but we only use 'parameters'. + * (void) casts are the standard C idiom for this. + */ (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; FWUPMGR_INFO("on_check_complete_signal: received\n"); + /* + * Stack-allocated struct to hold the parsed signal data. + * memset to zero ensures all pointers start as NULL (important + * for cleanup -- free(NULL) is safe, free(garbage) is not). + */ InternalSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); + /* + * Parse the GVariant "(tiissss)" payload into our struct. + * See internal_parse_signal_data() below for details. + * + * If parsing fails (wrong signature, NULL parameters), we return + * without dispatching. The caller's condvar timeout will fire. + * This is the correct behavior -- we can't dispatch garbage data. + */ if (!internal_parse_signal_data(parameters, &signal_data)) { FWUPMGR_ERROR("on_check_complete_signal: parse failed\n"); return; } + /* + * Dispatch to all registered callbacks. This is where the actual + * callback invocation happens. See dispatch_all_pending() below + * for the detailed two-phase dispatch explanation. + */ dispatch_all_pending(&signal_data); + /* + * Free the 4 strdup'd strings in signal_data. + * After dispatch_all_pending() returns, all callbacks have completed + * and no one holds references to these strings anymore. + * + * internal_cleanup_signal_data() calls free() on each string + * and memset's the struct to zero (defensive cleanup). + */ internal_cleanup_signal_data(&signal_data); } -/** - * @brief Dispatch signal result to every PENDING callback +/* + * dispatch_all_pending - Find all PENDING callbacks and invoke them. * - * TWO-PHASE DESIGN — avoids deadlock: + * OVERVIEW * - * PHASE 1 (mutex held): - * Scan registry → snapshot all PENDING entries into local array. - * Mark each found entry as DISPATCHED. - * Release mutex. + * PURPOSE: + * This is the CORE of the async engine. When a CheckForUpdateComplete + * signal arrives, this function finds every PENDING callback in the + * registry, builds the FwInfoData struct from the signal payload, + * and invokes each callback. * - * PHASE 2 (mutex released): - * Build FwUpdateEventData from signal_data. - * Invoke each snapshot callback: callback(handle, &event_data) - * Re-acquire mutex briefly to reset each slot to IDLE. + * WHY "ALL PENDING" (NOT JUST ONE)? + * If multiple clients (or the same client calling checkForUpdate + * multiple times) have PENDING entries, they all get the same + * firmware check result. The daemon broadcasts ONE signal and + * ALL pending callbacks receive it. This is the "fan-out" pattern. * - * WHY RELEASE BEFORE CALLING CALLBACKS? - * If a callback called checkForUpdate() again, it would call - * internal_register_callback() which tries to lock the same mutex - * → deadlock. Releasing first makes re-entrant use safe. + * In the typical single-client case, there is exactly 1 PENDING entry. * - * @param signal_data Parsed signal payload (shared across all callbacks) + * TWO-PHASE DESIGN -- THE DEADLOCK PREVENTION PATTERN: + * + * PHASE 1 (mutex HELD): + * Lock g_registry.mutex. + * Scan all 30 entries. For each PENDING entry: + * - Copy its callback pointer and handle into a local stack array + * - Change its state from PENDING to DISPATCHED + * Unlock g_registry.mutex. + * + * PHASE 2 (mutex RELEASED): + * Build FwInfoData from signal_data (stack-allocated). + * For each entry in the snapshot: + * - Invoke: callback(&fwinfo_data) + * - After callback returns: lock mutex, reset slot to IDLE, unlock + * + * WHY NOT HOLD THE MUTEX DURING CALLBACK INVOCATION? + * + * Scenario that would deadlock WITHOUT two-phase: + * 1. BG thread holds g_registry.mutex + * 2. BG thread calls callback(&fwinfo_data) + * 3. Inside the callback, the app calls checkForUpdate() again + * (re-entrant use -- not common, but must be safe) + * 4. checkForUpdate() calls internal_register_callback() + * 5. internal_register_callback() calls pthread_mutex_lock(&g_registry.mutex) + * 6. DEADLOCK -- the BG thread is already holding that mutex + * (from step 1), and it's the same thread trying to re-acquire it + * + * With two-phase, the mutex is released BEFORE step 2, so step 5 + * would succeed (no one holds the mutex). + * + * Even if the callback does NOT call checkForUpdate() again, holding + * the mutex during a potentially slow callback (imagine the callback + * does heavy work -- file I/O, network, etc.) would block the main + * thread from registering new callbacks until the slow callback finishes. + * Two-phase keeps the critical section (Phase 1) fast: just a scan + * and copy, microseconds. + * + * WHY MARK AS DISPATCHED (NOT JUST SKIP IDLE)? + * DISPATCHED is an intermediate state between PENDING and IDLE. + * It means "we're about to call this callback but haven't finished yet." + * If another signal arrives while Phase 2 is running (very unlikely but + * possible), the next dispatch_all_pending() call would see DISPATCHED + * and skip it -- preventing double-dispatch of the same callback. + * + * MEMORY MODEL: + * - The Snapshot struct is stack-allocated (local array of 30 entries) + * - Each Snapshot copies the callback pointer and handle string + * - FwInfoData is stack-allocated in this function's frame + * - UpdateDetails is stack-allocated in this function's frame + * - ALL of this data is valid ONLY during callback execution + * - When this function returns, all stack data is gone + * - The callback MUST copy any data it needs before returning + * + * THREAD: Always runs on the BG thread (called from on_check_complete_signal). + * + * @param signal_data Parsed signal payload from internal_parse_signal_data(). + * Contains strdup'd strings -- valid until cleanup. + * + * Called by: on_check_complete_signal() + * Calls: internal_map_status_code(), parse_update_details(), + * each registered callback, registry_reset_slot() */ static void dispatch_all_pending(const InternalSignalData *signal_data) { - /* Local snapshot — avoids holding mutex during callback invocations */ + /* + * Local snapshot struct -- what we copy from each PENDING entry. + * + * typedef here (inside the function) because this struct is only + * used locally -- no other function needs it. + * + * callback: the function pointer to invoke + * handle_copy: a copy of the handle string (256 bytes, more than enough + * for handle strings like "1" or "12345") + * slot_index: which entry in g_registry.entries[] this came from + * (needed to reset the slot to IDLE after callback returns) + */ typedef struct { UpdateEventCallback callback; char handle_copy[256]; int slot_index; } Snapshot; + /* + * Stack-allocated array of snapshots. 30 entries x ~270 bytes each + * = ~8KB on the stack. Well within the BG thread's 8MB stack limit. + * + * count tracks how many PENDING entries we found. + */ Snapshot snapshots[MAX_PENDING_CALLBACKS]; int count = 0; - /* ---- PHASE 1: collect under mutex ---- */ + /* ================================================================ + * PHASE 1: COLLECT PENDING ENTRIES UNDER MUTEX + * + * This is the fast critical section. We hold the mutex for as short + * as possible: scan 30 entries, copy the ones we need, release. + * Total time: microseconds. + * ================================================================ */ pthread_mutex_lock(&g_registry.mutex); for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { CallbackEntry *e = &g_registry.entries[i]; + + /* + * Skip anything that isn't PENDING. + * IDLE -- empty slot, nothing to do + * DISPATCHED -- another dispatch is already handling this + * TIMED_OUT -- expired, will be cleaned up separately + */ if (e->state != CB_STATE_PENDING) continue; + /* + * Found a PENDING entry. Copy its essential data into our + * local snapshot so we can invoke it after releasing the mutex. + * + * We copy the function pointer (8 bytes) and the handle string + * (up to 256 bytes via snprintf). We also record the slot index + * so we can reset the exact slot to IDLE later. + * + * snprintf with "%s" safely copies the string with null termination. + * If handle_key is NULL (should never happen for PENDING entries, + * but defensive), we copy an empty string. + */ snapshots[count].callback = e->callback; snapshots[count].slot_index = i; snprintf(snapshots[count].handle_copy, sizeof(snapshots[count].handle_copy), "%s", e->handle_key ? e->handle_key : ""); + /* + * Mark the slot as DISPATCHED. This is the transition: + * PENDING -> DISPATCHED + * + * This prevents: + * 1. Another signal dispatch from calling this callback again + * 2. A new checkForUpdate() from overwriting this slot + * (dedup only matches PENDING, not DISPATCHED) + * + * The slot will go DISPATCHED -> IDLE after the callback returns + * (in Phase 2's loop below). + */ e->state = CB_STATE_DISPATCHED; count++; @@ -419,60 +615,156 @@ static void dispatch_all_pending(const InternalSignalData *signal_data) e->handle_key ? e->handle_key : "(null)"); } + /* + * Release the mutex. Phase 1 is done. + * From this point on, the main thread is free to register new + * callbacks via internal_register_callback() -- no blocking. + */ pthread_mutex_unlock(&g_registry.mutex); FWUPMGR_INFO("dispatch_all_pending: %d callback(s) to fire\n", count); - /* ---- PHASE 2: invoke callbacks, no mutex held ---- */ + /* ================================================================ + * PHASE 2: BUILD DATA AND INVOKE CALLBACKS (NO MUTEX HELD) + * + * Now we build the FwInfoData struct that callbacks receive, + * and invoke each callback in sequence. No mutex is held during + * any of this -- safe for re-entrant use. + * ================================================================ */ + /* + * Map the daemon's integer status_code to our public enum. + * + * The daemon sends status_code as an integer in the signal: + * 0 = FIRMWARE_AVAILABLE + * 1 = FIRMWARE_NOT_AVAILABLE + * 2 = UPDATE_NOT_ALLOWED + * 3 = FIRMWARE_CHECK_ERROR + * 4 = IGNORE_OPTOUT + * 5 = BYPASS_OPTOUT + * + * internal_map_status_code() converts this to our CheckForUpdateStatus enum. + * Unknown values map to FIRMWARE_CHECK_ERROR (safe default). + */ CheckForUpdateStatus status = internal_map_status_code(signal_data->status_code); /* - * Build FwInfoData with UpdateDetails for the callback. - * This matches the public API signature: UpdateEventCallback(const FwInfoData*) + * Build FwInfoData on the STACK. + * + * This is the struct that callbacks receive via their + * (const FwInfoData *fwinfodata) parameter. * - * MEMORY MANAGEMENT: - * - FwInfoData is stack-allocated (valid during callback invocations) - * - CurrFWVersion is copied from signal_data (array, not pointer) - * - UpdateDetails is stack-allocated if needed - * - All data valid until end of this function + * CRITICAL: This struct is stack-allocated. It exists only while + * this function is running. When dispatch_all_pending() returns + * (after all callbacks have been invoked and returned), this stack + * frame is destroyed and fwinfo_data becomes invalid. + * + * This is why callbacks MUST copy any data they need before returning. + * The example_app does this: + * strncpy(g_fw_filename, event_data->UpdateDetails->FwFileName, ...); + * + * memset to zero ensures all char arrays start as empty strings + * (first byte is '\0') and all pointers start as NULL. */ FwInfoData fwinfo_data; memset(&fwinfo_data, 0, sizeof(fwinfo_data)); - /* Copy current firmware version */ + /* + * Copy the current firmware version from the signal data into + * fwinfo_data.CurrFWVersion (a char[64] array). + * + * strncpy with sizeof()-1 ensures we never overflow the buffer. + * The explicit null-termination on the next line is a safety net + * in case signal_data->current_version is >= 63 characters + * (strncpy does NOT null-terminate if src >= n characters). + * + * If current_version is NULL (daemon didn't provide it), we skip + * the copy and the field stays as empty string from memset. + */ if (signal_data->current_version) { strncpy(fwinfo_data.CurrFWVersion, signal_data->current_version, sizeof(fwinfo_data.CurrFWVersion) - 1); fwinfo_data.CurrFWVersion[sizeof(fwinfo_data.CurrFWVersion) - 1] = '\0'; } - /* Set status */ + /* Set the status enum in the struct. */ fwinfo_data.status = status; - /* Parse and populate UpdateDetails if firmware is available */ + /* + * Parse UpdateDetails if firmware is available. + * + * The daemon sends update details as a pipe-separated string: + * "File:firmware_v8.bin|Location:http://cdn..|Version:RDKV_8.0|Reboot:false|..." + * + * We need to parse this into an UpdateDetails struct with individual + * fields (FwFileName, FwUrl, FwVersion, RebootImmediately, etc.). + * + * UpdateDetails is ALSO stack-allocated. It lives in this function's + * stack frame. fwinfo_data.UpdateDetails is a POINTER to this stack + * variable. After this function returns, both are gone. + * + * We only populate UpdateDetails when status == FIRMWARE_AVAILABLE. + * For all other statuses (NOT_AVAILABLE, ERROR, etc.), UpdateDetails + * is set to NULL -- there's nothing to update, so no details to show. + */ UpdateDetails update_details; if (status == FIRMWARE_AVAILABLE && signal_data->update_details) { memset(&update_details, 0, sizeof(update_details)); + /* + * parse_update_details() tokenizes the pipe-separated string + * and copies each Key:Value pair into the appropriate field + * of the UpdateDetails struct. See parse_update_details() below. + */ if (parse_update_details(signal_data->update_details, &update_details)) { - /* Point FwInfoData to our stack-allocated UpdateDetails */ + /* + * Point FwInfoData's UpdateDetails pointer to our stack variable. + * + * This is safe because: both fwinfo_data and update_details + * live on the same stack frame. The pointer is valid as long + * as this function is executing. The callback receives this + * pointer and MUST copy what it needs before returning. + */ fwinfo_data.UpdateDetails = &update_details; FWUPMGR_INFO("dispatch_all_pending: UpdateDetails populated\n"); FWUPMGR_INFO(" FwFileName: %s\n", update_details.FwFileName); FWUPMGR_INFO(" FwVersion: %s\n", update_details.FwVersion); } else { - /* Parse failed - set to NULL to indicate no details available */ + /* + * Parsing failed (malformed string, etc.). Set to NULL so + * the callback knows: "firmware is available but I couldn't + * parse the details." The callback should handle this gracefully. + */ fwinfo_data.UpdateDetails = NULL; FWUPMGR_ERROR("dispatch_all_pending: parse_update_details failed\n"); } } else { - /* Status is not FIRMWARE_AVAILABLE or no update_details string */ + /* + * Either firmware is NOT available, or the daemon didn't send + * an update_details string. Set pointer to NULL. + * + * The callback should check: if (event_data->UpdateDetails != NULL) + * before accessing any UpdateDetails fields. + */ fwinfo_data.UpdateDetails = NULL; } - /* Invoke all callbacks with the same FwInfoData */ + /* + * Invoke each callback from our snapshot. + * + * For each snapshot entry: + * 1. Call the callback with a pointer to our stack-allocated FwInfoData + * 2. After the callback returns, lock the mutex and reset the slot to IDLE + * + * The same FwInfoData struct is passed to ALL callbacks. They all see + * the same firmware check result (because the daemon broadcast one signal). + * + * Callbacks run SEQUENTIALLY on the BG thread. If there are 3 pending + * callbacks, they fire one after another (not in parallel). If one + * callback is slow, it delays the others. Callbacks should be fast -- + * typically just copy data and signal a condvar. + */ for (int i = 0; i < count; i++) { Snapshot *s = &snapshots[i]; @@ -480,15 +772,40 @@ static void dispatch_all_pending(const InternalSignalData *signal_data) s->handle_copy); /* - * Invoke callback with proper signature: - * UpdateEventCallback(const FwInfoData *fwinfodata) + * THE ACTUAL CALLBACK INVOCATION. + * + * s->callback is the function pointer stored during + * internal_register_callback(). For our example_app, this is + * on_firmware_check_callback(). + * + * &fwinfo_data is a pointer to our stack-allocated struct. + * The callback receives it as (const FwInfoData *event_data). + * "const" means the callback cannot modify it, but it can read + * all fields and copy them. * - * handle_copy is passed but callback signature doesn't use it anymore. - * We pass it to maintain compatibility with 2-param callbacks if needed. + * THIS CALL BLOCKS THE BG THREAD until the callback returns. + * While inside the callback: + * - The BG thread is busy (not listening for more signals) + * - If another signal arrives, GLib queues it in the GMainContext + * - The signal will be dispatched after this function returns + * and the BG thread goes back to g_main_loop_run() */ s->callback(&fwinfo_data); - /* Reset slot to IDLE */ + /* + * Callback has returned. Now reset the slot to IDLE. + * + * We must lock the mutex for this because dispatch_all_pending() + * could be called concurrently (another signal arrives while + * we're in Phase 2), or the main thread could be registering + * a new callback. + * + * registry_reset_slot() frees the strdup'd handle_key, sets + * callback to NULL, and sets state to CB_STATE_IDLE. + * + * After this, the slot is available for reuse by the next + * checkForUpdate() call. + */ pthread_mutex_lock(&g_registry.mutex); registry_reset_slot(&g_registry.entries[s->slot_index]); pthread_mutex_unlock(&g_registry.mutex); @@ -499,29 +816,110 @@ static void dispatch_all_pending(const InternalSignalData *signal_data) * REGISTRY OPERATIONS * ======================================================================== */ -/** - * @brief Register a pending callback keyed by handle (no user_data) +/* + * internal_register_callback - Store a callback in the check-for-update registry. * - * SAME HANDLE TWICE: - * If the same handle is still PENDING from a previous call, its slot - * is overwritten. Prevents ghost callbacks accumulating. + * OVERVIEW + * + * PURPOSE: + * Called by checkForUpdate() to record the caller's callback function + * in g_registry so that when the BG thread later receives the + * CheckForUpdateComplete D-Bus signal, it can find and invoke it. + * + * Think of it as writing your name and phone number on a waiting list. + * When the result arrives, the BG thread walks the list and calls + * everyone who signed up. + * + * REGISTRY STRUCTURE: + * g_registry is a static global CallbackRegistry: + * - entries[30] -- array of CallbackEntry structs (MAX_PENDING_CALLBACKS=30) + * - mutex -- pthread_mutex_t protecting the array + * - initialized -- bool (set by internal_system_init) + * + * Each CallbackEntry has: + * - state -- IDLE, PENDING, DISPATCHED, or TIMED_OUT + * - handle_key -- strdup'd copy of the handle string (e.g., "1") + * - callback -- function pointer to the caller's callback + * - registered_time -- unix timestamp for potential timeout detection + * + * SLOT LIFECYCLE: + * IDLE -- slot is empty, available for use + * PENDING -- callback registered, waiting for signal from daemon + * DISPATCHED -- signal received, callback is being invoked right now + * IDLE -- callback returned, slot reset and available again + * + * DEDUP BEHAVIOR: + * If the same handle already has a PENDING entry (the caller called + * checkForUpdate() twice before the first callback fired), the old + * entry is OVERWRITTEN with the new callback. This prevents "ghost" + * callbacks from accumulating. The old callback will never fire. * - * @param handle App's FirmwareInterfaceHandle (will be strdup'd) - * @param callback App's 2-param UpdateEventCallback - * @return true on success, false if registry is full + * THREAD SAFETY: + * Protected by g_registry.mutex. The main thread calls this function + * (to register). The BG thread calls dispatch_all_pending() (to read + * and dispatch). The mutex ensures they never see inconsistent state. + * + * MEMORY: + * handle_key is strdup'd here (heap allocation). It is freed either: + * a. When the slot is reset to IDLE (registry_reset_slot) + * b. When an existing entry is overwritten (dedup path) + * c. When internal_system_deinit cleans up all remaining entries + * + * @param handle The client's handle string (e.g., "1"). Will be + * strdup'd -- caller retains ownership of their copy. + * @param callback The function to call when the signal arrives. + * Signature: void callback(const FwInfoData *fwinfodata) + * @return true if registered successfully, false if registry is full (30 slots) + * + * Called by: checkForUpdate() in rdkFwupdateMgr_api.c + * Pairs with: dispatch_all_pending() which reads PENDING entries */ bool internal_register_callback(FirmwareInterfaceHandle handle, UpdateEventCallback callback) { + /* + * Lock the registry mutex before touching the entries array. + * + * Who else might be holding this mutex right now? + * - dispatch_all_pending() on the BG thread (Phase 1 -- collecting + * PENDING entries into a snapshot). But Phase 1 is very fast + * (microseconds), so contention is rare. + * - registry_reset_slot() on the BG thread (after invoking a + * callback, resetting the slot to IDLE). Also very fast. + * + * In practice, the main thread and BG thread almost never contend + * because the BG thread only touches the registry when a signal + * arrives, which is seconds apart. + */ pthread_mutex_lock(&g_registry.mutex); + /* + * Two scan targets: + * free_slot -- first IDLE entry (to use if no dedup match) + * existing_slot -- a PENDING entry with the same handle (dedup) + * + * We scan all 30 entries in one pass, looking for both simultaneously. + */ CallbackEntry *free_slot = NULL; CallbackEntry *existing_slot = NULL; for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { CallbackEntry *e = &g_registry.entries[i]; - /* Existing pending entry for same handle → overwrite it */ + /* + * Dedup check: is there already a PENDING entry for this handle? + * + * This happens when the caller calls checkForUpdate() twice + * with the same handle before the first callback fires. + * Without dedup, both entries would BOTH get dispatched when + * the signal arrives -- the callback would fire twice. The + * second invocation would be a "ghost" with stale data. + * + * By overwriting, only the latest callback survives. + * + * strcmp is safe here because handle_key is always a valid + * null-terminated string (set by strdup) or NULL (checked first). + */ if (e->state == CB_STATE_PENDING && e->handle_key != NULL && strcmp(e->handle_key, handle) == 0) { @@ -529,11 +927,30 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, break; } + /* + * Remember the first free slot we find, but keep scanning + * in case there's a dedup match later in the array. + * + * We only record the FIRST free slot (free_slot == NULL guard) + * to avoid unnecessary work. + */ if (free_slot == NULL && e->state == CB_STATE_IDLE) { free_slot = e; } } + /* + * Choose target: prefer dedup (overwrite existing) over new slot. + * + * If existing_slot != NULL: overwrite the existing PENDING entry. + * This is the dedup path -- same handle called checkForUpdate again. + * + * If existing_slot == NULL and free_slot != NULL: use the free slot. + * This is the normal first-call path. + * + * If both are NULL: registry is full. All 30 slots are occupied + * (some combination of PENDING and DISPATCHED). Return false. + */ CallbackEntry *target = existing_slot ? existing_slot : free_slot; if (target == NULL) { @@ -543,6 +960,13 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, return false; } + /* + * If overwriting an existing entry, free the old handle_key. + * + * The old strdup'd string must be freed to avoid a memory leak. + * The old callback function pointer is just overwritten -- function + * pointers don't need freeing, they point to code, not heap data. + */ if (existing_slot) { FWUPMGR_INFO("internal_register_callback: overwriting existing for handle='%s'\n", handle); @@ -550,6 +974,33 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, target->handle_key = NULL; } + /* + * Populate the slot: + * + * handle_key = strdup(handle): + * Creates a heap copy of the handle string "1". We need our own + * copy because the caller's 'handle' pointer belongs to them -- + * they could theoretically modify or free it later. strdup + * allocates strlen(handle)+1 bytes (2 bytes for "1\0"). + * This copy is freed in registry_reset_slot() when the slot + * returns to IDLE. + * + * callback = callback: + * Stores the function pointer. When dispatch_all_pending() runs, + * it will call this: callback(&fwinfo_data). + * Function pointers are just addresses -- no heap allocation. + * + * state = CB_STATE_PENDING: + * Marks this slot as "waiting for a signal." The BG thread's + * dispatch_all_pending() only looks at PENDING entries. + * IDLE entries are skipped, DISPATCHED entries are being processed. + * + * registered_time = time(NULL): + * Unix timestamp of when this callback was registered. Currently + * used only for logging/debugging, but could be used by a future + * timeout sweeper to detect stale entries that have been PENDING + * for too long (e.g., > CALLBACK_TIMEOUT_SECONDS = 60s). + */ target->handle_key = strdup(handle); target->callback = callback; target->state = CB_STATE_PENDING; @@ -561,16 +1012,59 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, return true; } -/** - * @brief Reset a registry slot to IDLE - * MUST be called with registry mutex held. +/* + * registry_reset_slot - Return a CallbackEntry to the IDLE state. + * + * PURPOSE: + * After a callback has been dispatched (invoked and returned), the + * registry slot must be cleaned up and made available for reuse. + * This function frees the strdup'd handle_key, clears the callback + * pointer, resets the timestamp, and sets state to IDLE. + * + * PRECONDITION: + * Caller MUST hold g_registry.mutex before calling this function. + * dispatch_all_pending() does this: lock -> reset -> unlock. + * internal_system_deinit() also calls this during cleanup. + * + * Why must the mutex be held? + * Without the mutex, a race could occur: + * - BG thread is resetting slot 0 (setting state to IDLE) + * - Main thread scans for free slots (sees IDLE in half-written state) + * - Main thread writes into slot 0 while BG thread is still clearing it + * The mutex ensures atomicity of the reset operation. + * + * MEMORY: + * handle_key was allocated by strdup() in internal_register_callback(). + * We free() it here. After this call, entry->handle_key is NULL. + * The callback function pointer is just zeroed (it points to code + * segment, not heap -- no need to free). + * + * STATE TRANSITION: + * DISPATCHED -> IDLE (normal flow after callback invocation) + * PENDING -> IDLE (during system deinit cleanup) + * Any state -> IDLE (this function doesn't check current state) + * + * @param entry Pointer to the CallbackEntry to reset. + * Must not be NULL. */ static void registry_reset_slot(CallbackEntry *entry) { + /* + * Free the strdup'd handle_key string (e.g., "1"). + * NULL check is defensive -- PENDING and DISPATCHED entries always + * have a non-NULL handle_key, but IDLE entries have NULL. + * free(NULL) is safe in C (no-op), but the explicit check avoids + * confusion and makes the intent clear. + */ if (entry->handle_key != NULL) { free(entry->handle_key); entry->handle_key = NULL; } + + /* + * Clear remaining fields. After this, the entry looks identical + * to a freshly memset'd entry from internal_system_init(). + */ entry->callback = NULL; entry->registered_time = 0; entry->state = CB_STATE_IDLE; @@ -580,35 +1074,112 @@ static void registry_reset_slot(CallbackEntry *entry) * SIGNAL DATA HELPERS * ======================================================================== */ -/** - * @brief Parse GVariant into InternalSignalData - * - * Expected signature: (tiissss) - * t handler_id (uint64) - identifies which client this is for - * i result_code - * i status_code - * s current_version - * s available_version - * s update_details - * s status_message +/* + * internal_parse_signal_data - Extract fields from CheckForUpdateComplete signal. + * + * PURPOSE: + * The daemon broadcasts a D-Bus signal with a GVariant of type "(tiissss)". + * This function unpacks that GVariant into an InternalSignalData struct + * with individual typed fields, making the data easy to work with. + * + * GVariant TYPE "(tiissss)" -- what each letter means: + * '(' and ')' = tuple delimiters (the whole thing is a tuple) + * 't' = uint64 (guint64) -- handler_id + * 'i' = int32 (gint32) -- result_code (0=success, 1=fail) + * 'i' = int32 (gint32) -- status_code (0=available, 1=not, 3=error) + * 's' = string (gchar*) -- current_version (e.g., "RDKV_7.0") + * 's' = string (gchar*) -- available_version (e.g., "RDKV_8.0") + * 's' = string (gchar*) -- update_details (pipe-separated "Key:Value|...") + * 's' = string (gchar*) -- status_message (human-readable text) + * + * WHY strdup() EACH STRING? + * g_variant_get() with 's' type returns pointers into the GVariant's + * internal buffer. Those pointers are only valid while the GVariant + * exists. After on_check_complete_signal() returns, GLib may free + * the GVariant. We strdup() to create our own heap copies that survive + * beyond the GVariant's lifetime. + * + * The strdup'd copies are freed later by internal_cleanup_signal_data(). + * + * VALIDATION: + * We check the GVariant type signature before extracting. If the daemon + * sends a signal with a different signature (protocol mismatch, daemon + * version skew), we reject it immediately rather than crashing on + * mismatched g_variant_get(). + * + * THREAD: Called on the BG thread (from on_check_complete_signal). + * + * @param parameters The GVariant payload from the D-Bus signal. + * Type must be "(tiissss)". + * @param out_data Output struct. Must be zero-initialized by caller. + * On success, contains result_code, status_code, and + * 4 strdup'd strings (any may be NULL if daemon sent NULL). + * @return true on success, false if parameters is NULL or wrong type. */ bool internal_parse_signal_data(GVariant *parameters, InternalSignalData *out_data) { if (parameters == NULL || out_data == NULL) return false; + /* + * Check the type signature before extracting. + * + * g_variant_get_type_string() returns the GVariant's type as a string. + * We expect "(tiissss)". If it's anything else, the daemon sent + * something unexpected -- possibly a newer/older protocol version. + * Extracting with the wrong format string would read garbage. + */ const gchar *sig = g_variant_get_type_string(parameters); if (strcmp(sig, "(tiissss)") != 0) { FWUPMGR_ERROR("internal_parse_signal_data: unexpected signature '%s'\n", sig); return false; } + /* + * Local variables to receive g_variant_get() output. + * + * For string types ('s'), g_variant_get() returns pointers into + * the GVariant's internal buffer. These are temporary -- we must + * strdup() them before the GVariant could be freed. + * + * For integer types ('t', 'i'), g_variant_get() copies the value + * directly into our local variables. + */ const gchar *cur = NULL, *avail = NULL, *details = NULL, *msg = NULL; guint64 handler_id = 0; gint32 result = 0, status = 0; + /* + * Extract all 7 fields from the GVariant tuple in one call. + * + * The format string "(tiissss)" must exactly match the GVariant type. + * Each format character corresponds to one pointer argument: + * &handler_id -- receives the uint64 + * &result -- receives the first int32 + * &status -- receives the second int32 + * &cur -- receives pointer to current_version string + * &avail -- receives pointer to available_version string + * &details -- receives pointer to update_details string + * &msg -- receives pointer to status_message string + */ g_variant_get(parameters, "(tiissss)", &handler_id, &result, &status, &cur, &avail, &details, &msg); + /* + * Copy extracted values into the output struct. + * + * Integers are copied directly (they're values, not pointers). + * + * Strings are strdup'd to create heap copies that we own. + * The ternary (cur ? strdup(cur) : NULL) handles the case where + * the daemon sent an empty string (glib may return "" not NULL) + * or truly NULL. strdup(NULL) is undefined behavior in C, so + * the NULL check is essential. + * + * Note: handler_id is extracted but not stored in InternalSignalData. + * We don't currently use it because the dispatch is broadcast to + * ALL pending callbacks, not filtered by handler_id. If we later + * need per-client filtering, we would add handler_id to the struct. + */ out_data->result_code = (int32_t)result; out_data->status_code = (int32_t)status; out_data->current_version = cur ? strdup(cur) : NULL; @@ -619,6 +1190,26 @@ bool internal_parse_signal_data(GVariant *parameters, InternalSignalData *out_da return true; } +/* + * internal_cleanup_signal_data - Free the strdup'd strings in InternalSignalData. + * + * PURPOSE: + * Called after dispatch_all_pending() has finished invoking all callbacks. + * Frees the 4 heap-allocated strings that internal_parse_signal_data() + * created via strdup(). Also zeroes the struct as a defensive measure. + * + * WHY memset AFTER free()? + * After freeing the pointers, the struct still contains the old pointer + * values (dangling pointers). If someone accidentally reads the struct + * after cleanup, they'd get use-after-free. memset to zero sets all + * pointers to NULL (safe to dereference for a NULL check) and all + * integers to 0. + * + * free(NULL) is safe in C -- it's a no-op. So if any string was NULL + * (daemon didn't send it), the free() call is harmless. + * + * @param data The InternalSignalData to clean up. Must not be NULL. + */ void internal_cleanup_signal_data(InternalSignalData *data) { free(data->current_version); @@ -628,6 +1219,32 @@ void internal_cleanup_signal_data(InternalSignalData *data) memset(data, 0, sizeof(InternalSignalData)); } +/* + * internal_map_status_code - Convert daemon's integer to our public enum. + * + * PURPOSE: + * The daemon sends status_code as a plain integer in the D-Bus signal. + * Our public API uses a typed enum (CheckForUpdateStatus). This function + * does the mapping. + * + * MAPPING: + * 0 -> FIRMWARE_AVAILABLE (new firmware exists, UpdateDetails populated) + * 1 -> FIRMWARE_NOT_AVAILABLE (device is on latest version) + * 2 -> UPDATE_NOT_ALLOWED (device policy prevents updates) + * 3 -> FIRMWARE_CHECK_ERROR (XConf query failed, network error, etc.) + * 4 -> IGNORE_OPTOUT (update available, ignore opt-out preference) + * 5 -> BYPASS_OPTOUT (update available, bypass opt-out preference) + * anything else -> FIRMWARE_CHECK_ERROR (unknown code = error) + * + * WHY DEFAULT TO FIRMWARE_CHECK_ERROR? + * Unknown status codes indicate a protocol mismatch (daemon version + * newer than library). Treating unknown as "error" is the safest + * default -- the caller will handle it as a failure case rather than + * proceeding with potentially incorrect firmware data. + * + * @param status_code Integer from the daemon's signal payload. + * @return Corresponding CheckForUpdateStatus enum value. + */ CheckForUpdateStatus internal_map_status_code(int32_t status_code) { switch (status_code) { @@ -638,7 +1255,7 @@ CheckForUpdateStatus internal_map_status_code(int32_t status_code) case 4: return IGNORE_OPTOUT; case 5: return BYPASS_OPTOUT; default: - FWUPMGR_ERROR("internal_map_status_code: unknown %d → FIRMWARE_CHECK_ERROR\n", + FWUPMGR_ERROR("internal_map_status_code: unknown %d -> FIRMWARE_CHECK_ERROR\n", status_code); return FIRMWARE_CHECK_ERROR; } @@ -1310,33 +1927,88 @@ UpdateStatus internal_map_update_status_code(int32_t status_code) * ======================================================================== */ /** - * @brief Parse update_details string into UpdateDetails structure + * parse_update_details - Parse pipe-separated firmware details into a struct. + * + * OVERVIEW + * + * PURPOSE: + * When the daemon reports FIRMWARE_AVAILABLE, it includes a string + * describing the available firmware. This string uses a custom + * pipe-separated Key:Value format: + * + * "File:firmware_v8.bin|Location:http://cdn.example.com/fw|Version:RDKV_8.0|Reboot:false|Delay:false|PDRI:N/A|Peripherals:N/A" * - * The update_details string from the daemon is a comma-separated key:value format: - * "FwFileName:filename.bin,FwUrl:https://...,FwVersion:1.0,..." + * This function tokenizes that string and copies each value into the + * appropriate field of an UpdateDetails struct, which the callback + * receives via FwInfoData->UpdateDetails. * - * This function safely parses it and populates the UpdateDetails structure. + * WHY PIPE-SEPARATED (NOT JSON)? + * This is a daemon-internal format, not a public protocol. It's simple, + * requires no JSON parser dependency, and is easy to tokenize with + * strtok_r(). The library translates this format into typed struct + * fields so callers never see the pipe-separated format. * - * @param update_details_str Comma-separated string from daemon (may be NULL) - * @param out_details Output UpdateDetails structure (must be allocated) - * @return true if parsing succeeded (even if string was NULL/empty), - * false only on critical errors + * STRING FORMAT: + * - Tokens separated by '|' (pipe) + * - Each token is "Key:Value" (colon-separated) + * - Known keys: File, Location, IPv6Location, Version, Reboot, + * Delay, PDRI, Peripherals, Protocol, CertBundle + * - "N/A" is treated as "not available" for PDRI and Peripherals + * - Unknown keys are logged and skipped (forward compatibility) * - * Thread safety: Safe - operates on local data only - * Memory: out_details is caller-allocated, this function fills arrays + * THREAD SAFETY: + * Safe -- operates only on local data. The work_str is a strdup'd + * copy (so strtok_r doesn't modify the original), and out_details + * is caller-provided (stack-allocated in dispatch_all_pending). + * + * MEMORY: + * work_str is strdup'd at the start and freed at the end. + * out_details fields are char arrays (not pointers) -- data is + * copied directly into the struct, no additional heap allocation. + * + * ROBUSTNESS: + * - NULL/empty input is valid (returns success with zeroed struct) + * - Malformed tokens (no colon) are logged and skipped + * - Unknown keys are logged and skipped + * - strdup failure returns false (out of memory) + * + * @param update_details_str The pipe-separated string from the daemon. + * May be NULL or empty (both are valid). + * @param out_details Output struct. Filled with parsed values. + * Caller must provide allocated storage. + * @return true on success (even if input was NULL/empty -- struct is zeroed), + * false only on critical errors (NULL out_details, OOM). + * + * Called by: dispatch_all_pending() (Phase 2, when status == FIRMWARE_AVAILABLE) */ static bool parse_update_details(const char *update_details_str, UpdateDetails *out_details) { + /* + * NULL check on the output struct. This is a programming error + * in the caller -- should never happen, but catch it defensively. + */ if (out_details == NULL) { FWUPMGR_ERROR("parse_update_details: out_details is NULL\n"); return false; } - /* Zero-initialize the output structure */ + /* + * Zero-initialize the output struct. + * + * All char arrays (FwFileName, FwUrl, etc.) start as empty strings + * (first byte '\0'). This ensures that if a key is missing from + * the daemon's string, the corresponding field is empty rather + * than containing garbage. + */ memset(out_details, 0, sizeof(UpdateDetails)); - /* Empty or NULL input is valid - just means no details available */ + /* + * NULL or empty input is valid -- it means the daemon has no + * details to share. Return success with a zeroed struct. + * The caller (dispatch_all_pending) will see empty strings in + * all fields and can handle accordingly. + */ if (update_details_str == NULL || update_details_str[0] == '\0') { FWUPMGR_INFO("parse_update_details: empty input, returning zeroed structure\n"); return true; @@ -1344,41 +2016,129 @@ static bool parse_update_details(const char *update_details_str, FWUPMGR_INFO("parse_update_details: parsing '%s'\n", update_details_str); - /* Make a working copy since strtok modifies the string */ + /* + * Create a working copy of the input string. + * + * Why? strtok_r() MODIFIES the string it tokenizes (it replaces + * delimiters with '\0'). The input string belongs to InternalSignalData + * (from strdup in internal_parse_signal_data). We must not modify it + * because internal_cleanup_signal_data() needs to free() the original + * pointer. Modifying the string would corrupt the pointer if strtok_r + * happened to insert '\0' at a different position. + * + * strdup() allocates strlen(update_details_str)+1 bytes on the heap. + * Freed at the end of this function. + */ char *work_str = strdup(update_details_str); if (work_str == NULL) { FWUPMGR_ERROR("parse_update_details: strdup failed\n"); return false; } - /* Parse pipe-separated key:value pairs (daemon uses | not ,) */ + /* + * Tokenize the pipe-separated string. + * + * strtok_r() is the REENTRANT version of strtok(). We use it + * instead of strtok() because: + * - strtok() uses a static internal buffer -- NOT thread-safe. + * If another thread called strtok() simultaneously, they'd + * corrupt each other's state. + * - strtok_r() uses the caller-provided 'saveptr' for state, + * making it thread-safe. + * + * First call: strtok_r(work_str, "|", &saveptr) + * Returns pointer to first token (everything before first '|') + * Replaces the '|' with '\0' in work_str + * Stores position in saveptr for next call + * + * Subsequent calls: strtok_r(NULL, "|", &saveptr) + * Returns pointer to next token + * NULL when no more tokens + * + * Example: + * Input: "File:fw.bin|Version:8.0|Reboot:false" + * Call 1: returns "File:fw.bin" + * Call 2: returns "Version:8.0" + * Call 3: returns "Reboot:false" + * Call 4: returns NULL (done) + */ char *saveptr = NULL; char *token = strtok_r(work_str, "|", &saveptr); while (token != NULL) { - /* Split on ':' to get key and value */ + /* + * Each token should be "Key:Value". Find the colon separator. + * + * strchr() returns a pointer to the first ':' in the token, + * or NULL if there is no colon (malformed token). + */ char *colon = strchr(token, ':'); if (colon == NULL) { - /* Malformed token, skip it */ + /* + * No colon found -- this token is malformed. Skip it. + * This is defensive: if the daemon sends garbage like + * "File:fw.bin|OOPS|Version:8.0", we skip "OOPS" and + * continue parsing the rest. + */ FWUPMGR_ERROR("parse_update_details: malformed token '%s' (no colon)\n", token); token = strtok_r(NULL, "|", &saveptr); continue; } - /* Null-terminate the key and get the value */ + /* + * Split the token into key and value by replacing ':' with '\0'. + * + * Before: token = "File:fw.bin" (colon points to ':') + * After: key = "File" (token, now null-terminated at colon) + * value = "fw.bin" (colon + 1, rest of original string) + * + * This is an in-place split -- we're modifying our work_str copy. + */ *colon = '\0'; const char *key = token; const char *value = colon + 1; - /* Match keys and copy values into appropriate fields - * Daemon uses: File, Location, Version, Reboot, Delay, PDRI, Peripherals - * We map them to our struct fields */ + /* + * Match the key to our struct fields and copy the value. + * + * strncpy with sizeof(field)-1 ensures we never overflow the + * destination buffer. The struct fields are fixed-size arrays + * (e.g., FwFileName[128], FwUrl[512]). The -1 leaves room for + * the null terminator. + * + * We don't need to explicitly null-terminate because memset + * zeroed the entire struct at the start (all bytes are '\0'). + * strncpy will write the value characters and NOT overwrite + * the trailing '\0' that's already there from memset, as long + * as the value is shorter than the buffer. + * + * KEY MAPPING: + * Daemon key -> Struct field + * "File" -> FwFileName + * "Location" -> FwUrl (IPv4 download URL) + * "IPv6Location" -> FwUrl (IPv6 fallback, used if Location is "N/A") + * "Version" -> FwVersion + * "Reboot" -> RebootImmediately ("true" or "false") + * "Delay" -> DelayDownload ("true" or "false") + * "PDRI" -> PDRIVersion (PDRI image version) + * "Peripherals" -> PeripheralFirmwares (peripheral versions) + * "Protocol" -> (no struct field -- skipped) + * "CertBundle" -> (no struct field -- skipped) + */ if (strcmp(key, "File") == 0) { strncpy(out_details->FwFileName, value, sizeof(out_details->FwFileName) - 1); } else if (strcmp(key, "Location") == 0 || strcmp(key, "IPv6Location") == 0) { - /* Use Location if not empty, fallback to IPv6Location */ + /* + * Use Location if it's a real URL (not "N/A" and not empty). + * IPv6Location is a fallback -- if Location was "N/A" but + * IPv6Location has a URL, we use that instead. + * + * We don't overwrite an already-set FwUrl. If Location came + * first and was valid, IPv6Location won't overwrite it. + * This depends on daemon field ordering (Location before IPv6Location). + */ if (strcmp(value, "N/A") != 0 && value[0] != '\0') { strncpy(out_details->FwUrl, value, sizeof(out_details->FwUrl) - 1); @@ -1397,29 +2157,51 @@ static bool parse_update_details(const char *update_details_str, sizeof(out_details->DelayDownload) - 1); } else if (strcmp(key, "PDRI") == 0) { + /* + * Skip "N/A" -- leave the field as empty string (from memset). + * "N/A" means the daemon has no PDRI version info, which is + * the common case for non-PDRI devices. + */ if (strcmp(value, "N/A") != 0) { strncpy(out_details->PDRIVersion, value, sizeof(out_details->PDRIVersion) - 1); } } else if (strcmp(key, "Peripherals") == 0) { + /* Same N/A handling as PDRI. */ if (strcmp(value, "N/A") != 0) { strncpy(out_details->PeripheralFirmwares, value, sizeof(out_details->PeripheralFirmwares) - 1); } } else if (strcmp(key, "Protocol") == 0 || strcmp(key, "CertBundle") == 0) { - /* These fields exist in daemon format but not in our struct - ignore */ + /* + * These keys exist in the daemon's format but our UpdateDetails + * struct doesn't have fields for them. Log and skip. + * If a future version needs these, add struct fields and + * copy them here. + */ FWUPMGR_INFO("parse_update_details: skipping field '%s'='%s'\n", key, value); } else { - /* Unknown key - log but don't fail */ + /* + * Unknown key -- forward compatibility. If the daemon adds + * new fields in a future version, we log and skip them + * rather than failing. This allows the library to work with + * newer daemons that send extra fields. + */ FWUPMGR_INFO("parse_update_details: unknown key '%s', ignoring\n", key); } + /* Advance to next pipe-separated token. */ token = strtok_r(NULL, "|", &saveptr); } + /* + * Free the working copy. All the data we needed has been copied + * into out_details struct fields (which are char arrays, not pointers + * into work_str). So freeing work_str is safe. + */ free(work_str); FWUPMGR_INFO("parse_update_details: parsed successfully\n"); From 7b5ebc58530cba6410869a248f893dd09e09b142 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 05:43:33 +0000 Subject: [PATCH 15/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Documentation for DownloadFirmware API --- librdkFwupdateMgr/src/rdkFwupdateMgr_api.c | 365 +++++++++-- librdkFwupdateMgr/src/rdkFwupdateMgr_async.c | 635 +++++++++++++++++-- 2 files changed, 891 insertions(+), 109 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index 6f965d63..b2cdc649 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -438,91 +438,305 @@ static void rdkFwupdateMgr_lib_deinit(void) FWUPMGR_INFO("=== rdkFwupdateMgr library unloaded ===\n"); } #endif -/* ======================================================================== - * DOWNLOAD FIRMWARE PUBLIC API - * ======================================================================== +/* + * downloadFirmware - Initiate a non-blocking firmware download. * - * Implements: - * DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, - * FwDwnlReq fwdwnlreq, - * DownloadCallback callback); + * OVERVIEW * - * FLOW: - * 1. Validate: handle not NULL/empty, firmwareName not empty, callback not NULL - * 2. Connect to D-Bus (fail early if connection fails) - * 3. Register callback in download registry (AFTER D-Bus connection succeeds) - * 4. Fire DownloadFirmware D-Bus method call to daemon (fire-and-forget) - * 5. Return RDKFW_DWNL_SUCCESS immediately - * - * [later — fires multiple times as download progresses] - * Daemon emits DownloadProgress(progress%, status) signal repeatedly - * → on_download_progress_signal() fires in background thread - * → dispatch_all_dwnl_active() calls every ACTIVE DownloadCallback - * → slot stays ACTIVE until DWNL_COMPLETED or DWNL_ERROR - * ======================================================================== */ - -/** - * @brief Initiate firmware download — non-blocking, returns immediately + * PURPOSE: + * This is the SECOND async API call in the firmware update workflow. + * After checkForUpdate() confirmed firmware IS available (status == + * FIRMWARE_AVAILABLE and you got the filename + URL from UpdateDetails), + * you call this to tell the daemon "start downloading that file." + * + * This function is NON-BLOCKING. It returns immediately (in ~3ms) with + * RDKFW_DWNL_SUCCESS, meaning "your request was accepted." The actual + * download progress (0%, 10%, 50%, 100%) arrives later -- REPEATEDLY -- + * via your callback function, which is invoked by the library's + * background thread each time the daemon emits a DownloadProgress signal. + * + * KEY DIFFERENCE FROM checkForUpdate(): + * checkForUpdate callback fires ONCE (one signal, one callback, done). + * downloadFirmware callback fires MANY TIMES (one per progress report). + * The registry slot stays ACTIVE across all progress signals and only + * goes IDLE when the download ends (COMPLETED or ERROR). + * + * WHAT "FIRE-AND-FORGET" MEANS (SAME PATTERN AS checkForUpdate): + * We send the D-Bus method call and do NOT wait for a reply. + * The daemon's method response is discarded (connection already closed). + * The real data comes as BROADCAST D-Bus signals: "DownloadProgress" + * -- caught by the BG thread's on_download_progress_signal() handler. + * + * THREADING MODEL: + * - This function runs on the CALLER'S thread (main thread) + * - It does NOT block the caller + * - The callback fires on the BACKGROUND thread (the one created + * during registerProcess -> internal_system_init) + * - The BG thread fires the callback MULTIPLE TIMES (once per signal) + * - The caller typically sleeps on a condvar until the callback + * sets g_download_done=1 on a terminal status (COMPLETED/ERROR) + * + * D-BUS WIRE PROTOCOL: + * Method: "DownloadFirmware" + * Input: GVariant type "(ssss)" -- four strings: + * s handle e.g., "1" (from registerProcess) + * s firmwareName e.g., "firmware_v8.bin" + * s downloadUrl e.g., "http://cdn.example.com/fw" or "" (use XConf) + * s TypeOfFirmware e.g., "PCI" or "PDRI" or "PERIPHERAL" + * Reply: IGNORED (fire-and-forget -- three trailing NULLs) + * + * Signal (arrives later, MULTIPLE times): + * Name: "DownloadProgress" + * GVariant type "(tsuss)": + * t handler_id (uint64 - which client) + * s firmware_name (string - filename being downloaded) + * u progress_percent (uint32 - 0 to 100) + * s status_string (string - "NOTSTARTED", "INPROGRESS", "COMPLETED", "ERROR") + * s message (string - human-readable message) + * + * CONNECTION MODEL: + * Creates an EPHEMERAL D-Bus connection (e.g., :1.143) that lives + * only for this function call. The BG thread has its own PERSISTENT + * connection (:1.141) for receiving signals. Completely independent. + * + * DOWNLOAD REGISTRY (g_dwnl_registry -- SEPARATE from g_registry): + * This API uses its OWN registry, independent from checkForUpdate's. + * g_dwnl_registry has its own mutex, its own 30 slots, its own + * state machine. The two registries never interfere with each other. + * + * Slot lifecycle: IDLE --> ACTIVE --> IDLE + * IDLE: Slot free, no callback registered. + * ACTIVE: Callback registered. Fires on EVERY DownloadProgress signal. + * Stays ACTIVE across multiple signals (0%, 10%, 50%...). + * IDLE: Reset when DWNL_COMPLETED or DWNL_ERROR is received. + * + * Compare with checkForUpdate's lifecycle: + * IDLE --> PENDING --> DISPATCHED --> IDLE (fires ONCE) + * Download has NO "DISPATCHED" state because the slot fires repeatedly. + * + * CALLBACK CONTRACT: + * - Fires MULTIPLE TIMES (once per DownloadProgress signal) + * - Fires on the BG thread, NOT the caller's thread + * - Signature: void callback(int progress_per, DownloadStatus status) + * - progress_per: 0 to 100 (percentage complete) + * - status: DWNL_IN_PROGRESS, DWNL_COMPLETED, or DWNL_ERROR + * - DWNL_COMPLETED means download finished successfully + * - DWNL_ERROR means download failed (network error, disk full, etc.) + * - After COMPLETED or ERROR, no more callbacks will fire + * - If daemon crashes mid-download, callback NEVER fires with + * COMPLETED/ERROR -- the caller's condvar timeout is the safety net + * + * RETURN VALUES: + * RDKFW_DWNL_SUCCESS (0) -- Request sent. Callbacks will fire later. + * RDKFW_DWNL_FAILED (1) -- Request failed. Callback will NOT fire. + * IMPORTANT: SUCCESS does NOT mean download started. It means the + * request was accepted. Actual progress comes in the callbacks. + * + * EXECUTION FLOW (step numbers match code comments below): + * + * [1] Validate handle, fwdwnlreq, firmwareName, callback (reject NULL/empty) + * [2] Open ephemeral D-Bus connection (fail early if D-Bus is down) + * [3] Register callback in g_dwnl_registry (state = ACTIVE) + * -- MUST happen BEFORE sending D-Bus call to avoid race + * [4] Send fire-and-forget "DownloadFirmware" D-Bus method call + * [5] Close ephemeral connection, return RDKFW_DWNL_SUCCESS + * + * [Later, repeatedly -- on BG thread:] + * Daemon broadcasts "DownloadProgress" signal (multiple times) + * BG thread receives it in on_download_progress_signal() + * dispatch_all_dwnl_active() finds our ACTIVE slot, invokes callback + * If status == COMPLETED or ERROR: slot is reset to IDLE + * Otherwise: slot stays ACTIVE for next signal + * + * WHY REGISTER BEFORE SEND (Step 3 before Step 4): + * Same race condition as checkForUpdate. If the daemon responds + * instantly (e.g., file already cached locally), the BG thread + * would receive the signal before we registered. The dispatch + * would find zero ACTIVE entries and silently drop the signal. + * Our callback would never fire. The app would hang forever. + * + * WHY CONNECT BEFORE REGISTER (Step 2 before Step 3): + * If we registered first and D-Bus connection then failed, we'd + * have a ghost ACTIVE entry that never fires (because the D-Bus + * call was never sent). The slot would stay ACTIVE forever, + * wasting 1 of 30 slots and never being cleaned up. + * + * @param handle The handle returned by registerProcess(). Must be + * non-NULL and non-empty. e.g., "1" + * @param fwdwnlreq Pointer to download request struct. Must be non-NULL. + * Contains firmwareName (required), downloadUrl (optional, + * "" means use XConf URL), TypeOfFirmware (optional). + * @param callback Function pointer invoked on each DownloadProgress signal. + * Must be non-NULL. Signature: + * void callback(int progress_per, DownloadStatus status) * - * @param handle Valid FirmwareInterfaceHandle from registerProcess() - * @param fwdwnlreq Download request (passed by value, library copies it) - * @param callback Invoked on each DownloadProgress signal * @return RDKFW_DWNL_SUCCESS or RDKFW_DWNL_FAILED + * + * See also: on_download_progress_signal() -- BG thread signal handler + * See also: dispatch_all_dwnl_active() -- two-phase callback dispatch + * See also: internal_dwnl_register_callback() -- registry slot allocation */ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, const FwDwnlReq *fwdwnlreq, DownloadCallback callback) { - /* [1] Validate */ + /* + * [STEP 1] INPUT VALIDATION + * + * Reject obviously bad inputs before touching D-Bus or the registry. + * This is the library's input boundary -- validate everything here. + * downloadFirmware has MORE validation than checkForUpdate because + * it also validates the request struct fields (not just handle+callback). + */ + + /* + * Check 1a: handle must not be NULL and must not be empty "". + * + * handle is the string "1" from registerProcess(). If the caller + * passes NULL (forgot to register first) or an empty string, + * reject immediately. The daemon would reject it too, but we + * save the D-Bus round-trip. + */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("downloadFirmware: invalid handle (NULL or empty)\n"); return RDKFW_DWNL_FAILED; } + /* + * Check 1b: the request struct pointer must not be NULL. + * + * This catches the case where the caller passes NULL instead of + * &download_req. Dereferencing NULL would crash. + */ if (fwdwnlreq == NULL) { FWUPMGR_ERROR("downloadFirmware: fwdwnlreq is NULL\n"); return RDKFW_DWNL_FAILED; } + /* + * Check 1c: firmwareName pointer must not be NULL. + * + * FwDwnlReq.firmwareName is a const char* -- it could be NULL if + * the caller forgot to set it. We need a filename to tell the + * daemon WHAT to download. + */ if (fwdwnlreq->firmwareName == NULL) { FWUPMGR_ERROR("downloadFirmware: firmwareName is NULL\n"); return RDKFW_DWNL_FAILED; } + /* + * Check 1d: firmwareName must not be an empty string "". + * + * An empty filename is meaningless -- the daemon can't download "". + * This catches the case where the caller did: + * download_req.firmwareName = ""; // accident + */ if (fwdwnlreq->firmwareName[0] == '\0') { FWUPMGR_ERROR("downloadFirmware: firmwareName is empty\n"); return RDKFW_DWNL_FAILED; } + /* + * Check 1e: callback must not be NULL. + * + * Without a callback, the app can't receive progress updates. + * It would never know when the download finishes. That's always + * a programming error. + */ if (callback == NULL) { FWUPMGR_ERROR("downloadFirmware: callback is NULL\n"); return RDKFW_DWNL_FAILED; } + /* + * Log what we're about to download. The ternary expressions handle + * optional fields: TypeOfFirmware and downloadUrl may be NULL + * (they're optional in FwDwnlReq). + */ FWUPMGR_INFO("downloadFirmware: handle='%s' firmware='%s' type='%s' url='%s'\n", handle, fwdwnlreq->firmwareName, (fwdwnlreq->TypeOfFirmware && fwdwnlreq->TypeOfFirmware[0]) ? fwdwnlreq->TypeOfFirmware : "(none)", (fwdwnlreq->downloadUrl && fwdwnlreq->downloadUrl[0]) ? fwdwnlreq->downloadUrl : "(use XConf)"); - /* [2] Connect to D-Bus FIRST before registering callback + /* + * [STEP 2] CREATE EPHEMERAL D-BUS CONNECTION * - * This prevents stale registry entries if D-Bus connection fails. + * g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) opens a new connection to + * the system D-Bus bus. Gets a unique sender name like :1.143. + * + * Why a NEW connection instead of reusing the BG thread's :1.141? + * The BG thread's connection is attached to the BG thread's + * GMainContext. Using it from the main thread would require + * cross-thread GLib context management -- complex and fragile. + * A fresh per-call connection is simpler and safe. + * + * Why BEFORE registering the callback? + * If D-Bus is down (dbus-daemon crashed, socket missing), this + * call fails. We want to fail BEFORE polluting the download + * registry with an ACTIVE entry that will never be dispatched. + * Clean failure: no registry entry, no dangling state. + * + * Cost: ~2ms for the D-Bus handshake. Negligible for a firmware + * download that takes minutes. */ GError *error = NULL; GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (conn == NULL) { + /* + * D-Bus connection failed. Common causes: + * - dbus-daemon not running + * - System bus socket missing (/var/run/dbus/system_bus_socket) + * - Permission denied (D-Bus policy rejects our user) + * + * Return FAILED -- no registry entry created, nothing to clean up. + */ FWUPMGR_ERROR("downloadFirmware: D-Bus connect failed: %s\n", error ? error->message : "unknown"); if (error) g_error_free(error); return RDKFW_DWNL_FAILED; } - /* [3] Register callback AFTER D-Bus connection succeeds, BEFORE sending + /* + * [STEP 3] REGISTER CALLBACK IN THE DOWNLOAD REGISTRY (g_dwnl_registry) * - * Register immediately before sending to avoid race condition where - * the daemon responds before we're ready to receive the signal. + * internal_dwnl_register_callback() does the following (see _async.c): + * 1. Locks g_dwnl_registry.mutex + * 2. Scans all 30 DwnlCallbackEntry slots for: + * a. An existing ACTIVE entry with the same handle (dedup/overwrite) + * b. The first IDLE slot (free slot) + * 3. If same handle found: overwrites it (prevents stale callbacks) + * If free slot found: uses it + * If neither: returns false (registry full -- 30 concurrent downloads!) + * 4. Populates the slot: + * - handle_key = strdup(handle) -- "1" (heap copy) + * - callback = our function pointer + * - state = DWNL_CB_STATE_ACTIVE -- NOTE: ACTIVE, not PENDING! + * - registered_time = current unix timestamp + * 5. Unlocks g_dwnl_registry.mutex + * 6. Returns true + * + * After this call, the registry has one ACTIVE entry. When the BG + * thread receives DownloadProgress signals, it will find this entry + * and invoke the callback on EVERY signal. + * + * STATE DIFFERENCE FROM checkForUpdate: + * checkForUpdate sets state = CB_STATE_PENDING (fires once) + * downloadFirmware sets state = DWNL_CB_STATE_ACTIVE (fires repeatedly) + * There is NO "DISPATCHED" intermediate state for download. + * + * Why BEFORE the D-Bus call? + * Race condition prevention. If the daemon starts downloading + * instantly (file already cached), the BG thread would receive + * the first DownloadProgress signal before we registered. + * dispatch_all_dwnl_active() would find zero ACTIVE entries + * and silently discard the signal. Our callback would never fire. + * + * Failure case: registry full (30 concurrent pending downloads). + * In practice never happens -- a device downloads one firmware + * at a time. If it does, clean up and fail. */ if (!internal_dwnl_register_callback(handle, callback)) { FWUPMGR_ERROR("downloadFirmware: registry full, handle='%s'\n", handle); @@ -530,16 +744,49 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, return RDKFW_DWNL_FAILED; } - /* [4] Fire-and-forget D-Bus DownloadFirmware method call + /* + * [STEP 4] SEND FIRE-AND-FORGET D-BUS METHOD CALL * - * Arguments: (ssss) - * s handle — identifies this app to the daemon - * s firmwareName — firmware image filename - * s downloadUrl — override URL or "" for XConf URL - * s TypeOfFirmware — "PCI" | "PDRI" | "PERIPHERAL" + * g_dbus_connection_call() sends a D-Bus method call to the daemon. * - * Three trailing NULLs = fire and forget (no reply waited for). - * g_dbus_connection_call() returns immediately. + * Parameters to g_dbus_connection_call(): + * conn -- our ephemeral connection :1.143 + * DBUS_SERVICE_NAME -- "org.rdkfwupdater.Service" (daemon's well-known name) + * DBUS_OBJECT_PATH -- "/org/rdkfwupdater/Service" (object path) + * DBUS_INTERFACE_NAME -- "org.rdkfwupdater.Interface" + * DBUS_METHOD_DOWNLOAD -- "DownloadFirmware" (the method name) + * g_variant_new("(ssss)", ...) -- 4-string argument tuple: + * "(ssss)" means a tuple containing four strings + * string 1: handle -- "1" (which registered client is asking) + * string 2: firmwareName -- "firmware_v8.bin" (what to download) + * string 3: downloadUrl -- URL or "" (where to download from) + * string 4: TypeOfFirmware -- "PCI" or "" (firmware type category) + * NULL -- expected reply type: we don't care + * G_DBUS_CALL_FLAGS_NONE -- no special flags + * DBUS_TIMEOUT_MS -- 5000ms (only for message queueing, not reply) + * NULL -- GCancellable: no cancellation support + * NULL -- GAsyncReadyCallback: no reply callback + * NULL -- user_data for reply callback: N/A + * + * The three trailing NULLs make this fire-and-forget. GLib queues + * the D-Bus message in the kernel's socket buffer and returns. + * + * NULL-COALESCING for optional fields: + * fwdwnlreq->downloadUrl ? fwdwnlreq->downloadUrl : "" + * If downloadUrl is NULL (caller didn't set it), we send "" + * to the daemon. The daemon treats "" as "use the XConf URL + * that was returned during checkForUpdate." Same for TypeOfFirmware. + * + * WHAT HAPPENS ON THE DAEMON SIDE: + * 1. Daemon receives "DownloadFirmware" with 4 string arguments + * 2. Validates handler "1" is registered (from registerProcess) + * 3. Starts downloading firmware_v8.bin from the URL + * 4. As download progresses, broadcasts DownloadProgress signals: + * - (1, "firmware_v8.bin", 0, "NOTSTARTED", "Download queued") + * - (1, "firmware_v8.bin", 10, "INPROGRESS", "10% downloaded") + * - (1, "firmware_v8.bin", 50, "INPROGRESS", "50% downloaded") + * - (1, "firmware_v8.bin", 100, "COMPLETED", "Download complete") + * 5. Our BG thread catches each signal and fires our callback */ g_dbus_connection_call( @@ -561,12 +808,48 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, NULL /* user_data: none */ ); + /* + * [STEP 5] CLOSE EPHEMERAL CONNECTION AND RETURN + * + * g_object_unref(conn) closes our ephemeral D-Bus connection :1.143. + * The D-Bus message is already in the kernel socket buffer -- closing + * our end doesn't prevent delivery to the daemon. + * + * After this, the state of the world is: + * + * Main thread: + * - Returns RDKFW_DWNL_SUCCESS to the caller + * - Caller enters pthread_cond_timedwait (typically 300s timeout) + * - Connection :1.143 is DEAD (just closed) + * + * BG thread: + * - Still sleeping in g_main_loop_run() on connection :1.141 + * - g_dwnl_registry.entries[0] has our callback in ACTIVE state + * - Will wake up on EVERY DownloadProgress signal + * - Will call our callback MULTIPLE TIMES + * + * Daemon: + * - Received our request, started downloading the firmware + * - Will broadcast DownloadProgress signals as download progresses + * + * g_dwnl_registry (download-specific, separate from g_registry): + * entries[0] = { state=ACTIVE, handle_key="1", + * callback=on_download_progress_callback } + * entries[1..29] = IDLE + * + * D-Bus connections: + * :1.141 -- BG thread persistent (ALIVE, listening for signals) + * :1.143 -- this downloadFirmware ephemeral (DEAD, just closed) + * + * SUCCESS here means: "I sent the request and registered your callback." + * It does NOT mean: "Download started." or "File exists on server." + * The callback will fire later with actual progress. + */ g_object_unref(conn); FWUPMGR_INFO("downloadFirmware: D-Bus call sent, returning SUCCESS. handle='%s'\n", handle); - /* [4] Return immediately — app is unblocked */ return RDKFW_DWNL_SUCCESS; } diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c index 57575d48..f855aa20 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c @@ -1281,17 +1281,56 @@ static void dwnl_registry_reset_slot(DwnlCallbackEntry *entry); /* ======================================================================== * DOWNLOAD REGISTRY CLEANUP - * - * Called from internal_system_deinit() to free download registry resources. - * Signal unsubscription is handled by the background thread. * ======================================================================== */ -/** - * @brief Cleanup download registry — called from internal_system_deinit() +/* + * internal_dwnl_system_deinit - Free all download registry resources. + * + * PURPOSE: + * Called from internal_system_deinit() during library unload (either + * via __attribute__((destructor)) or explicitly by unregisterProcess). + * Frees any strdup'd handle_key strings that are still in the registry + * (e.g., downloads that were in-progress when the app exits) and + * destroys the mutex. + * + * WHEN IS THIS CALLED? + * During orderly shutdown of the library. The BG thread has already + * been joined (stopped), so no concurrent access to g_dwnl_registry + * is possible. The mutex lock/unlock is purely defensive -- in theory + * no other thread can be using the registry at this point. + * + * WHY FREE handle_key's? + * If the app exits while a download is ACTIVE (e.g., download at 50% + * and app receives SIGTERM), the slot still holds a strdup'd handle_key + * that was never freed by dwnl_registry_reset_slot() (because the + * terminal COMPLETED/ERROR signal never arrived). We must free it here + * to avoid a memory leak reported by Valgrind/ASan. + * + * WHY pthread_mutex_destroy()? + * The mutex was initialized by pthread_mutex_init() in internal_system_init(). + * Every init must have a matching destroy for clean resource management. + * Destroying a locked mutex is undefined behavior (we unlock first). + * + * NOTE: Signal unsubscription (g_dbus_connection_signal_unsubscribe) is + * handled separately by the BG thread during its shutdown sequence, + * NOT here. This function only handles registry memory. + * + * @param none (operates on global g_dwnl_registry) */ static void internal_dwnl_system_deinit(void) { + /* + * Lock before freeing. Defensive -- no other thread should be + * active at this point, but the pattern is consistent with how + * all other registry operations lock before accessing entries[]. + */ pthread_mutex_lock(&g_dwnl_registry.mutex); + + /* + * Scan all 30 slots. Free any non-NULL handle_key strings. + * IDLE slots have handle_key == NULL (already freed or never set). + * ACTIVE slots (abandoned downloads) have handle_key != NULL. + */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { if (g_dwnl_registry.entries[i].handle_key != NULL) { free(g_dwnl_registry.entries[i].handle_key); @@ -1299,6 +1338,11 @@ static void internal_dwnl_system_deinit(void) } } pthread_mutex_unlock(&g_dwnl_registry.mutex); + + /* + * Destroy the mutex. After this, any attempt to lock it is UB. + * Since the library is unloading, no one should try. + */ pthread_mutex_destroy(&g_dwnl_registry.mutex); FWUPMGR_INFO("internal_dwnl_system_deinit: done\n"); @@ -1308,15 +1352,66 @@ static void internal_dwnl_system_deinit(void) * DOWNLOAD SIGNAL HANDLER * ======================================================================== */ -/** - * @brief Called by GLib when DownloadProgress signal arrives +/* + * on_download_progress_signal - BG thread entry point for DownloadProgress. + * + * PURPOSE: + * This function is called by GLib's D-Bus infrastructure when the daemon + * broadcasts the "DownloadProgress" signal. It is the ENTRY POINT for + * the "response" side of the downloadFirmware() async flow. * - * Runs in the background thread — same thread as on_check_complete_signal(). + * WHEN DOES THIS FIRE? + * Repeatedly, starting shortly after downloadFirmware() was called. + * The daemon emits a DownloadProgress signal each time it has a + * progress update (0%, 10%, 50%, 100%, or on error). Unlike + * checkForUpdate which fires ONCE, this fires MANY TIMES. + * + * WHICH THREAD RUNS THIS? + * The BACKGROUND THREAD. Same thread that handles CheckForUpdateComplete + * and UpdateProgress. All three signals are dispatched on the same + * single BG thread. Only one signal handler runs at a time because + * they all share the same GMainContext. + * + * WHY (void) CASTS? + * GLib's signal handler signature requires 7 parameters. We only need + * 'parameters' (the GVariant payload). The (void) casts suppress + * "unused parameter" compiler warnings for the other 6. + * + * SIGNAL PAYLOAD FORMAT: + * GVariant type "(tsuss)": + * t handler_id (uint64 -- which registered client) + * s firmware_name (string -- filename being downloaded) + * u progress_percent (uint32 -- 0 to 100) + * s status_string (string -- "NOTSTARTED"/"INPROGRESS"/"COMPLETED"/"ERROR") + * s message (string -- human-readable status message) + * + * IMPORTANT DIFFERENCE FROM checkForUpdate: + * checkForUpdate used integer status codes mapped by internal_map_status_code(). + * download uses STRING status values mapped by map_dwnl_status_string(). + * The daemon sends "INPROGRESS" not 0, "COMPLETED" not 1. + * + * MEMORY MANAGEMENT (DIFFERENT FROM checkForUpdate): + * checkForUpdate: internal_parse_signal_data() uses strdup() -> free() + * download: internal_parse_dwnl_signal_data() uses GLib's g_variant_get() + * with 's' format -> returns gchar* that caller must g_free(). + * + * The strings (firmware_name, status_string, message) are allocated by + * GLib during g_variant_get(). They stay valid until we g_free() them + * AFTER dispatch is complete. This ensures the strings are valid + * throughout all callback invocations. * * FLOW: - * 1. Parse GVariant payload → InternalDwnlSignalData - * 2. Dispatch to ALL ACTIVE download callbacks - * 3. If status is COMPLETED or ERROR → remove finished slots from registry + * 1. Parse GVariant -> InternalDwnlSignalData (extracts 5 fields) + * 2. Call dispatch_all_dwnl_active() -- fires all ACTIVE callbacks + * 3. g_free() the 3 GLib-allocated strings + * + * @param conn The BG thread's persistent D-Bus connection (:1.141) + * @param sender D-Bus sender address (ignored -- accept from any) + * @param object_path D-Bus object path of the signal source (ignored) + * @param interface_name D-Bus interface the signal belongs to (ignored) + * @param signal_name "DownloadProgress" (ignored -- we know from subscription) + * @param parameters The GVariant payload -- type "(tsuss)" + * @param user_data NULL (we use globals, no per-subscription user data) */ static void on_download_progress_signal(GDBusConnection *conn, const gchar *sender, @@ -1326,14 +1421,28 @@ static void on_download_progress_signal(GDBusConnection *conn, GVariant *parameters, gpointer user_data) { + /* Suppress "unused parameter" warnings for the 6 params we don't need. */ (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; FWUPMGR_INFO("on_download_progress_signal: received\n"); + /* + * Stack-allocate and zero-init the parsed data struct. + * After parsing, this holds: + * handler_id -- uint64 (which client the signal is for) + * firmware_name -- gchar* (GLib-allocated, must g_free) + * progress_percent -- uint32 (0-100) + * status_string -- gchar* (GLib-allocated, must g_free) + * message -- gchar* (GLib-allocated, must g_free) + */ InternalDwnlSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); + /* + * Parse the GVariant. If parsing fails (wrong type signature, + * NULL parameters), log and return. No callbacks fire. + */ if (!internal_parse_dwnl_signal_data(parameters, &signal_data)) { FWUPMGR_ERROR("on_download_progress_signal: parse failed\n"); return; @@ -1345,57 +1454,164 @@ static void on_download_progress_signal(GDBusConnection *conn, signal_data.progress_percent, signal_data.status_string ? signal_data.status_string : "(null)"); + /* + * Dispatch to all ACTIVE download callbacks. + * This is the two-phase dispatch (snapshot under mutex, invoke without). + * The callback may fire for progress (slot stays ACTIVE) or for + * terminal status (slot reset to IDLE). + */ dispatch_all_dwnl_active(&signal_data); - // Free allocated strings from g_variant_get + /* + * Free the GLib-allocated strings. + * + * g_free() is GLib's equivalent of free(). We use g_free() because + * the strings were allocated by GLib's g_variant_get() internally. + * Using standard free() on GLib-allocated memory is technically + * undefined behavior (though it works on most platforms). + * + * These strings were valid throughout dispatch_all_dwnl_active() + * because we only free them AFTER all callbacks have returned. + * The callbacks receive progress_percent (int, copied by value) + * and status (enum, copied by value), so they don't reference + * these strings directly. But the dispatch function does read + * status_string to determine is_final, so it must be valid then. + * + * g_free(NULL) is safe -- it's a no-op. + */ g_free(signal_data.firmware_name); g_free(signal_data.status_string); g_free(signal_data.message); } -/** - * @brief Dispatch DownloadProgress signal to every ACTIVE download callback - * - * SAME TWO-PHASE DESIGN as CheckForUpdate dispatch: - * - * PHASE 1 (mutex held): - * Snapshot all ACTIVE entries. - * Do NOT change state yet — slot must stay ACTIVE for future signals. - * EXCEPTION: if status is COMPLETED or ERROR, mark slot for removal. - * Release mutex. +/* + * dispatch_all_dwnl_active - Two-phase dispatch for download progress. * - * PHASE 2 (mutex released): - * Invoke each callback: callback(progress_per, status) - * Re-acquire mutex to reset completed/errored slots to IDLE. - * - * WHY KEEP SLOTS ACTIVE ACROSS MULTIPLE SIGNALS? - * Download progress fires many times: 1%, 5%, 20%...100%. - * If we reset to IDLE after the first callback, subsequent signals - * would find no registered callback and be silently dropped. - * The slot only becomes IDLE when the download ends. + * PURPOSE: + * Called by on_download_progress_signal() on the BG thread. + * Finds ALL ACTIVE entries in g_dwnl_registry and invokes each one's + * callback with the progress and status. If the download ended + * (COMPLETED or ERROR), resets the slot to IDLE after the callback. + * + * TWO-PHASE DESIGN (SAME PATTERN AS checkForUpdate's dispatch_all_pending): + * + * PHASE 1 -- SNAPSHOT (mutex held, ~microseconds): + * Lock g_dwnl_registry.mutex. + * Scan all 30 slots. For each ACTIVE entry: + * - Copy callback function pointer into stack-local snapshot + * - Copy handle string (snprintf into fixed buffer) + * - Record slot index (for IDLE reset later) + * - Record whether this is a terminal signal (is_final) + * Unlock mutex. + * + * PHASE 2 -- INVOKE (no mutex held, may take milliseconds): + * For each snapshot entry: + * - Call: callback(progress_percent, status) + * - If is_final: re-lock mutex, reset slot to IDLE, unlock + * + * WHY TWO PHASES? + * Same deadlock prevention as checkForUpdate. If we held the mutex + * while calling the app's callback, and the callback tried to call + * downloadFirmware() or unregisterProcess(), that would try to lock + * the same mutex -> DEADLOCK. By releasing before invoking, the + * callback can safely call any library API. + * + * KEY DIFFERENCE FROM checkForUpdate's dispatch_all_pending(): + * + * checkForUpdate: slot goes PENDING -> DISPATCHED -> IDLE after ONE callback. + * download: slot stays ACTIVE across MANY callbacks. Only goes IDLE when + * the status is DWNL_COMPLETED or DWNL_ERROR (is_final == true). + * + * For in-progress signals (is_final == false): + * - Phase 1: snapshot the ACTIVE slot, do NOT touch slot state. + * - Phase 2: invoke callback, do NOT reset slot. + * - Result: slot remains ACTIVE for the next DownloadProgress signal. + * + * For terminal signals (is_final == true): + * - Phase 1: same snapshot. + * - Phase 2: invoke callback, THEN lock mutex and reset slot to IDLE. + * - Result: slot is freed. No more callbacks will fire for this handle. + * + * STATUS MAPPING: + * The daemon sends status as a STRING ("INPROGRESS", "COMPLETED", etc.). + * map_dwnl_status_string() converts it to the DownloadStatus enum: + * "NOTSTARTED" or "INPROGRESS" -> DWNL_IN_PROGRESS + * "COMPLETED" -> DWNL_COMPLETED + * "ERROR" or "DWNL_ERROR" -> DWNL_ERROR + * + * is_final is true only for DWNL_COMPLETED or DWNL_ERROR. + * All other statuses (IN_PROGRESS) keep the slot alive. + * + * CALLBACK SIGNATURE: + * void callback(int progress_per, DownloadStatus status) + * - progress_per: 0-100 integer (percentage of download complete) + * - status: DWNL_IN_PROGRESS, DWNL_COMPLETED, or DWNL_ERROR + * - No handle parameter (different from checkForUpdate's callback) + * - Runs on BG thread, NOT the app's main thread + * + * THREAD: Runs entirely on the BG thread. + * + * @param signal_data Parsed DownloadProgress signal payload. + * Must remain valid for the duration of this function + * (strings freed by caller AFTER this returns). */ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) { + /* + * Stack-local snapshot struct for one ACTIVE entry. + * Same pattern as checkForUpdate's dispatch -- we copy what we need + * while the mutex is held, then work from the copy with no mutex. + * + * Fields: + * callback -- the app's function pointer (copied from slot) + * handle_copy -- snprintf'd copy of handle_key (for logging only) + * slot_index -- which slot in entries[] this came from + * is_final -- true if COMPLETED/ERROR (need to reset slot after) + */ typedef struct { DownloadCallback callback; char handle_copy[256]; int slot_index; - bool is_final; /* true if COMPLETED or ERROR — remove after firing */ + bool is_final; /* true if COMPLETED or ERROR -- remove after firing */ } DwnlSnapshot; DwnlSnapshot snapshots[MAX_PENDING_CALLBACKS]; int count = 0; + /* + * Map the status string to our enum BEFORE entering the mutex. + * This is a pure computation (strcmp calls) with no shared state. + * Doing it outside the mutex keeps the critical section shorter. + * + * is_final determines whether we reset the slot after the callback: + * true = COMPLETED or ERROR -> download ended, free the slot + * false = IN_PROGRESS -> download continuing, keep slot ACTIVE + */ DownloadStatus status = map_dwnl_status_string(signal_data->status_string); bool is_final = (status == DWNL_COMPLETED || status == DWNL_ERROR); /* ---- PHASE 1: snapshot under mutex ---- */ + + /* + * Lock the download registry. This blocks any concurrent call to + * internal_dwnl_register_callback() (from main thread) until we + * finish our snapshot. Hold time: microseconds (just scanning + copying). + */ pthread_mutex_lock(&g_dwnl_registry.mutex); + /* + * Scan all 30 slots. Only process entries in ACTIVE state. + * IDLE and TIMED_OUT entries are skipped. + */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { DwnlCallbackEntry *e = &g_dwnl_registry.entries[i]; if (e->state != DWNL_CB_STATE_ACTIVE) continue; + /* + * Copy the entry data into our stack-local snapshot. + * After we unlock, the slot might be modified by another thread + * (extremely unlikely, but the pattern is safe regardless). + */ snapshots[count].callback = e->callback; snapshots[count].slot_index = i; snapshots[count].is_final = is_final; @@ -1404,9 +1620,11 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) "%s", e->handle_key ? e->handle_key : ""); /* - * If this is the final signal (completed/error), mark the slot - * so we reset it to IDLE after the callback fires. - * For in-progress signals, leave the slot ACTIVE. + * NOTE: We do NOT change the slot state here. + * For in-progress signals, the slot must remain ACTIVE. + * For terminal signals, we'll reset it in Phase 2 AFTER + * the callback fires. This is different from checkForUpdate + * which sets state=DISPATCHED during Phase 1. */ count++; @@ -1415,11 +1633,17 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) signal_data->progress_percent, is_final); } + /* + * Unlock BEFORE invoking callbacks. + * This is the deadlock prevention: callbacks may call library APIs + * that need g_dwnl_registry.mutex, so we must not hold it. + */ pthread_mutex_unlock(&g_dwnl_registry.mutex); FWUPMGR_INFO("dispatch_all_dwnl_active: %d callback(s) to fire\n", count); /* ---- PHASE 2: invoke callbacks, no mutex held ---- */ + for (int i = 0; i < count; i++) { DwnlSnapshot *s = &snapshots[i]; @@ -1427,15 +1651,36 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) s->handle_copy); /* + * Invoke the app's download callback. + * * Callback signature: void fn(int progress_per, DownloadStatus status) - * No handle parameter — matches the DownloadCallback typedef exactly. + * progress_per = signal_data->progress_percent (0-100) + * status = mapped enum (DWNL_IN_PROGRESS, DWNL_COMPLETED, DWNL_ERROR) + * + * The app's callback (e.g., on_download_progress_callback in example_app) + * typically: + * - Logs the progress + * - On terminal status: locks its own condvar mutex, sets done=1, + * signals the condvar to wake the main thread + * - Returns + * + * This call may take microseconds to milliseconds depending on + * what the app does in the callback. We hold NO library mutex + * during this time. */ s->callback(signal_data->progress_percent, status); /* - * If download is done (COMPLETED or ERROR), reset slot to IDLE. - * This frees the handle_key and makes the slot available for reuse. - * For in-progress signals, leave slot ACTIVE for next signal. + * If this was a terminal signal (download ended), reset the slot. + * + * We re-acquire the mutex, call dwnl_registry_reset_slot(), then + * release. This frees the strdup'd handle_key and sets state=IDLE. + * + * After this, no more callbacks will fire for this handle. + * The slot is available for reuse by a future downloadFirmware() call. + * + * For in-progress signals, we skip this entirely. The slot stays + * ACTIVE and will be found again on the NEXT DownloadProgress signal. */ if (s->is_final) { pthread_mutex_lock(&g_dwnl_registry.mutex); @@ -1452,31 +1697,92 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) * DOWNLOAD REGISTRY OPERATIONS * ======================================================================== */ -/** - * @brief Register a download callback keyed by handle +/* + * internal_dwnl_register_callback - Allocate a download registry slot. * - * Sets slot state to ACTIVE. Slot will receive ALL subsequent - * DownloadProgress signals until DWNL_COMPLETED or DWNL_ERROR. + * PURPOSE: + * Called by downloadFirmware() (on the main thread) to register the + * app's DownloadCallback in g_dwnl_registry. After this call, the + * BG thread will invoke the callback on EVERY DownloadProgress signal + * until the download completes or errors. + * + * REGISTRY DETAILS (g_dwnl_registry -- SEPARATE from g_registry): + * - 30 slots (DwnlCallbackEntry entries[MAX_PENDING_CALLBACKS]) + * - Protected by g_dwnl_registry.mutex (its own mutex, independent) + * - State machine: IDLE -> ACTIVE -> IDLE + * - No PENDING or DISPATCHED states (unlike checkForUpdate) + * - Slot stays ACTIVE across MULTIPLE DownloadProgress signals * - * SAME HANDLE TWICE: - * Overwrites existing ACTIVE slot for the same handle. - * Prevents stale callbacks from a previous download session. + * SLOT LIFECYCLE: + * IDLE: Slot is empty. handle_key==NULL, callback==NULL, state==0. + * ACTIVE: Slot is registered. Callback fires on every DownloadProgress. + * Stays ACTIVE until DWNL_COMPLETED or DWNL_ERROR arrives. + * IDLE: Reset by dwnl_registry_reset_slot() after terminal signal. + * + * DEDUP / OVERWRITE: + * If the same handle already has an ACTIVE entry (e.g., the app called + * downloadFirmware() twice without waiting for the first to complete), + * we OVERWRITE the existing slot. This prevents: + * - Two ACTIVE entries for the same handle (double-firing callbacks) + * - Ghost entries from a previous download that was abandoned + * We free the old handle_key before replacing it. + * + * SCAN ORDER: + * Linear scan from slot 0 to 29. We look for two things simultaneously: + * 1. existing_slot: an ACTIVE entry with the same handle (overwrite it) + * 2. free_slot: the first IDLE entry (use it if no existing found) + * If existing_slot is found, we break immediately (priority: overwrite). + * If neither is found after scanning all 30, registry is full -> fail. + * + * THREAD SAFETY: + * Called on the MAIN thread (from downloadFirmware). + * g_dwnl_registry.mutex protects against concurrent access from: + * - Another main-thread downloadFirmware() call (unlikely, but safe) + * - The BG thread's dispatch_all_dwnl_active() reading the registry * - * @param handle App's FirmwareInterfaceHandle (strdup'd internally) - * @param callback App's DownloadCallback - * @return true on success, false if registry full + * MEMORY: + * handle is strdup'd (heap copy). Freed by dwnl_registry_reset_slot() + * when the slot is released (after COMPLETED/ERROR). + * + * @param handle The handle string (e.g., "1"). Will be strdup'd. + * @param callback The app's DownloadCallback function pointer. + * @return true if registered successfully, false if registry full. */ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, DownloadCallback callback) { + /* + * Lock the download registry mutex. + * This ensures only one thread modifies g_dwnl_registry at a time. + * The BG thread also locks this during Phase 1 of dispatch (snapshot) + * and during is_final slot reset. + */ pthread_mutex_lock(&g_dwnl_registry.mutex); DwnlCallbackEntry *free_slot = NULL; DwnlCallbackEntry *existing_slot = NULL; + /* + * Single-pass scan of all 30 slots. + * + * Priority 1: Find an ACTIVE entry with the same handle. + * -> We'll overwrite it (dedup). + * -> Break immediately when found (no need to continue). + * + * Priority 2: Remember the first IDLE slot encountered. + * -> We'll use it if no existing slot is found. + * -> Don't break -- keep scanning for existing_slot. + */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { DwnlCallbackEntry *e = &g_dwnl_registry.entries[i]; + /* + * Check for existing ACTIVE entry with same handle. + * Three conditions must all be true: + * 1. Slot is ACTIVE (not IDLE or TIMED_OUT) + * 2. handle_key is not NULL (defensive -- should always be set for ACTIVE) + * 3. handle_key matches our handle (string comparison) + */ if (e->state == DWNL_CB_STATE_ACTIVE && e->handle_key != NULL && strcmp(e->handle_key, handle) == 0) { @@ -1484,13 +1790,28 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, break; } + /* + * Remember first free slot (only if we haven't found one yet). + * We DON'T break here -- we keep scanning because an existing + * slot at a higher index takes priority over a free slot at + * a lower index. + */ if (free_slot == NULL && e->state == DWNL_CB_STATE_IDLE) { free_slot = e; } } + /* + * Decide which slot to use. + * existing_slot (overwrite) takes priority over free_slot (new). + */ DwnlCallbackEntry *target = existing_slot ? existing_slot : free_slot; + /* + * If neither was found, the registry is full. All 30 slots are ACTIVE + * (30 concurrent downloads -- should never happen in practice). + * Unlock and return false. + */ if (target == NULL) { FWUPMGR_ERROR("internal_dwnl_register_callback: registry full (max=%d)\n", MAX_PENDING_CALLBACKS); @@ -1498,6 +1819,10 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, return false; } + /* + * If overwriting an existing slot, free the old handle_key first. + * strdup'd memory must be freed to avoid a memory leak. + */ if (existing_slot) { FWUPMGR_INFO("internal_dwnl_register_callback: overwriting existing for handle='%s'\n", handle); @@ -1505,6 +1830,24 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, target->handle_key = NULL; } + /* + * Populate the slot with the new registration. + * + * strdup(handle): Creates a heap copy of the handle string. + * We need our own copy because the caller's handle may be freed + * or overwritten after downloadFirmware() returns. The slot + * must retain the handle for the entire download duration. + * + * callback: Store the function pointer directly. No copy needed -- + * function pointers are just addresses in the code segment. + * + * state = DWNL_CB_STATE_ACTIVE: The slot is now live. The BG thread + * will find it during its next dispatch_all_dwnl_active() call. + * NOTE: ACTIVE, not PENDING. There's no intermediate state. + * + * registered_time: Stored for potential timeout detection (not + * currently active, but the infrastructure is there for future use). + */ target->handle_key = strdup(handle); target->callback = callback; target->state = DWNL_CB_STATE_ACTIVE; @@ -1516,9 +1859,41 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, return true; } -/** - * @brief Reset a download registry slot to IDLE - * MUST be called with g_dwnl_registry.mutex held. +/* + * dwnl_registry_reset_slot - Clear a download registry slot back to IDLE. + * + * PURPOSE: + * Called after a terminal DownloadProgress signal (COMPLETED or ERROR) + * has been dispatched. Frees the strdup'd handle_key and resets all + * fields to zero/NULL/IDLE so the slot can be reused by a future + * downloadFirmware() call. + * + * PRECONDITION: + * MUST be called with g_dwnl_registry.mutex held by the caller. + * The caller (dispatch_all_dwnl_active Phase 2) acquires the mutex + * before calling this and releases it after. + * + * WHAT GETS FREED: + * - handle_key: strdup'd in internal_dwnl_register_callback(). + * Must be freed to avoid memory leak. Set to NULL after free. + * + * WHAT GETS ZEROED: + * - callback: set to NULL (dangling pointer prevention) + * - registered_time: set to 0 (slot has no registration timestamp) + * - state: set to DWNL_CB_STATE_IDLE (slot is free for reuse) + * + * AFTER THIS CALL: + * The slot looks exactly like it did after internal_system_init(): + * all zeros, state == IDLE, ready for a new registration. + * Subsequent DownloadProgress signals will skip this slot because + * dispatch_all_dwnl_active() only processes ACTIVE slots. + * + * free(NULL) SAFETY: + * If handle_key is already NULL (shouldn't happen, but defensive), + * free(NULL) is a safe no-op in C. + * + * @param entry Pointer to the DwnlCallbackEntry to reset. + * MUST NOT be NULL. */ static void dwnl_registry_reset_slot(DwnlCallbackEntry *entry) { @@ -1535,30 +1910,85 @@ static void dwnl_registry_reset_slot(DwnlCallbackEntry *entry) * DOWNLOAD SIGNAL DATA HELPERS * ======================================================================== */ -/** - * @brief Parse GVariant DownloadProgress signal payload +/* + * internal_parse_dwnl_signal_data - Extract fields from DownloadProgress signal. * - * Expected GVariant signature: (ii) - * i progress_percent (0–100) - * i status_code (maps to DownloadStatus) + * PURPOSE: + * The daemon broadcasts DownloadProgress as a GVariant of type "(tsuss)". + * This function unpacks that GVariant into an InternalDwnlSignalData struct + * with individual typed fields. + * + * GVariant TYPE "(tsuss)" -- what each letter means: + * '(' and ')' = tuple delimiters + * 't' = uint64 (guint64) -- handler_id (which client this is for) + * 's' = string (gchar*) -- firmware_name (file being downloaded) + * 'u' = uint32 (guint32) -- progress_percent (0-100) + * 's' = string (gchar*) -- status_string ("INPROGRESS", "COMPLETED", etc.) + * 's' = string (gchar*) -- message (human-readable) + * + * MEMORY MODEL (DIFFERENT FROM checkForUpdate): + * checkForUpdate used strdup() on strings returned by g_variant_get('s'). + * For download, we DON'T strdup. Instead, g_variant_get() with 's' format + * returns a NEWLY-ALLOCATED gchar* that the caller must g_free(). + * + * Wait -- isn't 's' supposed to return a pointer into the GVariant? + * Actually NO: GLib documentation says for g_variant_get(): + * 's' format: returns a newly-allocated copy (gchar*) that caller frees. + * '&s' format: returns a pointer into the GVariant (no allocation). + * We use 's' (not '&s'), so we get fresh allocations that outlive the GVariant. + * + * The caller (on_download_progress_signal) calls g_free() on all three + * string pointers AFTER dispatch is complete. + * + * VALIDATION: + * Checks the GVariant type is "(tsuss)" before extracting. If the daemon + * sends a different signature (version mismatch), we reject immediately. + * + * THREAD: Called on the BG thread (from on_download_progress_signal). + * + * @param parameters The GVariant payload from the DownloadProgress signal. + * Type must be "(tsuss)". + * @param out_data Output struct. Must be zero-initialized by caller. + * On success, contains handler_id, progress, and 3 strings. + * Strings are GLib-allocated -- caller must g_free() them. + * @return true on success, false if parameters is NULL or wrong type. */ bool internal_parse_dwnl_signal_data(GVariant *parameters, InternalDwnlSignalData *out_data) { if (parameters == NULL || out_data == NULL) return false; + /* + * Verify the type signature before extracting. + * "(tsuss)" is the expected format for DownloadProgress. + * If it's different, the daemon protocol changed -- reject. + */ const gchar *sig = g_variant_get_type_string(parameters); if (strcmp(sig, "(tsuss)") != 0) { FWUPMGR_ERROR("internal_parse_dwnl_signal_data: unexpected signature '%s' (expected '(tsuss)')\n", sig); return false; } + /* + * Local variables to receive g_variant_get() output. + * For 's' format: GLib allocates fresh strings (must g_free). + * For 't' and 'u' format: values copied directly into locals. + */ guint64 handler_id = 0; gchar *firmware_name = NULL; guint32 progress = 0; gchar *status_str = NULL; gchar *message_str = NULL; + /* + * Extract all 5 fields from the GVariant tuple. + * Format "(tsuss)" maps to: + * &handler_id -- receives uint64 + * &firmware_name -- receives gchar* (GLib-allocated) + * &progress -- receives uint32 + * &status_str -- receives gchar* (GLib-allocated) + * &message_str -- receives gchar* (GLib-allocated) + */ g_variant_get(parameters, "(tsuss)", &handler_id, &firmware_name, @@ -1566,42 +1996,107 @@ bool internal_parse_dwnl_signal_data(GVariant *parameters, &status_str, &message_str); + /* + * Copy into output struct. The string pointers are just transferred -- + * we don't strdup them again. Ownership passes to the caller. + * Caller is responsible for g_free() on firmware_name, status_string, message. + */ out_data->handler_id = handler_id; - out_data->firmware_name = firmware_name; // Caller must g_free + out_data->firmware_name = firmware_name; /* Caller must g_free */ out_data->progress_percent = progress; - out_data->status_string = status_str; // Caller must g_free - out_data->message = message_str; // Caller must g_free + out_data->status_string = status_str; /* Caller must g_free */ + out_data->message = message_str; /* Caller must g_free */ return true; } -/** - * @brief Map status string to DownloadStatus enum +/* + * internal_map_dwnl_status_code - Map integer status to DownloadStatus enum. + * + * PURPOSE: + * Legacy function kept for backward compatibility. In the current protocol, + * the daemon sends status as a STRING ("INPROGRESS", "COMPLETED", "ERROR") + * and the actual mapping is done by map_dwnl_status_string() below. + * + * This function exists for cases where an integer status code is received + * (older daemon versions or internal testing). + * + * MAPPING: + * 0 -> DWNL_IN_PROGRESS (download is actively happening) + * 1 -> DWNL_COMPLETED (download finished successfully) + * 2 -> DWNL_ERROR (download failed) + * anything else -> DWNL_ERROR (unknown = treat as failure) + * + * @param status_code Integer status from an older protocol format. + * @return Corresponding DownloadStatus enum value. */ DownloadStatus internal_map_dwnl_status_code(int32_t status_code) { - // This function is kept for backward compatibility but now receives - // a mapped value. The actual mapping happens in the caller. switch (status_code) { case 0: return DWNL_IN_PROGRESS; case 1: return DWNL_COMPLETED; case 2: return DWNL_ERROR; default: - FWUPMGR_ERROR("internal_map_dwnl_status_code: unknown %d → DWNL_ERROR\n", + FWUPMGR_ERROR("internal_map_dwnl_status_code: unknown %d -> DWNL_ERROR\n", status_code); return DWNL_ERROR; } } -/** - * @brief Map status string from daemon to DownloadStatus enum +/* + * map_dwnl_status_string - Map daemon's status string to DownloadStatus enum. + * + * PURPOSE: + * The daemon sends download status as a human-readable string in the + * DownloadProgress signal. This function converts that string to the + * typed DownloadStatus enum that the app's callback receives. + * + * WHY STRINGS INSTEAD OF INTEGERS? + * The daemon team chose strings for DownloadProgress (unlike + * CheckForUpdateComplete which uses integers). Strings are more + * debuggable in D-Bus tools (dbus-monitor shows "COMPLETED" not "1") + * but require strcmp-based mapping in the library. + * + * MAPPING: + * "INPROGRESS" -> DWNL_IN_PROGRESS (download actively downloading) + * "NOTSTARTED" -> DWNL_IN_PROGRESS (download queued, about to start) + * "COMPLETED" -> DWNL_COMPLETED (file fully downloaded) + * "ERROR" -> DWNL_ERROR (download failed) + * "DWNL_ERROR" -> DWNL_ERROR (alternate error string from daemon) + * NULL -> DWNL_ERROR (missing field = error) + * anything else -> DWNL_ERROR (unknown = error, with log) + * + * WHY "NOTSTARTED" MAPS TO IN_PROGRESS: + * "NOTSTARTED" is the daemon's first signal saying "I received your + * request and queued it." From the app's perspective, this is the + * beginning of the download process -- it's "in progress" even if + * bytes haven't started flowing yet. There's no separate enum for + * "queued but not started" -- the app just sees 0% IN_PROGRESS. + * + * TERMINAL vs NON-TERMINAL: + * The return value determines whether dispatch_all_dwnl_active() + * resets the slot: + * DWNL_IN_PROGRESS -> slot stays ACTIVE (more signals coming) + * DWNL_COMPLETED -> slot reset to IDLE (download ended) + * DWNL_ERROR -> slot reset to IDLE (download ended) + * + * @param status_str String from the daemon's signal. May be NULL. + * @return Corresponding DownloadStatus enum value. */ static DownloadStatus map_dwnl_status_string(const char *status_str) { + /* + * NULL means the field was missing from the signal (parse error + * or daemon bug). Treat as error -- something is wrong. + */ if (status_str == NULL) { return DWNL_ERROR; } + /* + * String comparisons for known values. + * "INPROGRESS" and "NOTSTARTED" both mean "not done yet." + */ if (strcmp(status_str, "INPROGRESS") == 0 || strcmp(status_str, "NOTSTARTED") == 0) { return DWNL_IN_PROGRESS; } else if (strcmp(status_str, "COMPLETED") == 0) { @@ -1610,7 +2105,11 @@ static DownloadStatus map_dwnl_status_string(const char *status_str) return DWNL_ERROR; } - FWUPMGR_ERROR("map_dwnl_status_string: unknown status '%s' → DWNL_ERROR\n", status_str); + /* + * Unknown string. Log it (for debugging daemon protocol changes) + * and treat as error. The slot will be reset to IDLE. + */ + FWUPMGR_ERROR("map_dwnl_status_string: unknown status '%s' -> DWNL_ERROR\n", status_str); return DWNL_ERROR; } From 6cffee42831f1db4baa48ed3a30260b6f5a00a2e Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 07:22:29 +0000 Subject: [PATCH 16/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Documentation for UpdateFirmware API --- librdkFwupdateMgr/src/rdkFwupdateMgr_api.c | 387 ++++++++-- librdkFwupdateMgr/src/rdkFwupdateMgr_async.c | 770 ++++++++++++++++++- 2 files changed, 1071 insertions(+), 86 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index b2cdc649..49f5750d 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -857,81 +857,229 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, * UPDATE FIRMWARE PUBLIC API * ======================================================================== * + * updateFirmware -- Initiate firmware flashing (non-blocking) + * * Implements: * UpdateResult updateFirmware(FirmwareInterfaceHandle handle, - * FwUpdateReq fwupdatereq, + * const FwUpdateReq *fwupdatereq, * UpdateCallback callback); * - * FLOW: - * 1. Validate: handle not NULL/empty, firmwareName not empty, - * TypeOfFirmware not empty, callback not NULL - * 2. Connect to D-Bus (fail early if connection fails) - * 3. Register callback in update registry (AFTER D-Bus connection succeeds) - * 4. Fire UpdateFirmware D-Bus method call to daemon (fire-and-forget) - * 5. Return RDKFW_UPDATE_SUCCESS immediately - * - * [later — fires multiple times as flashing progresses] - * Daemon emits UpdateProgress(progress%, status) signal repeatedly - * → on_update_progress_signal() fires in background thread - * → dispatch_all_update_active() calls every ACTIVE UpdateCallback - * → slot stays ACTIVE until UPDATE_COMPLETED or UPDATE_ERROR - * ======================================================================== */ - -/** - * @brief Initiate firmware flashing — non-blocking, returns immediately - * - * D-Bus arguments sent to daemon: (sssss) - * s handle — identifies this app - * s firmwareName — image filename to flash - * s LocationOfFirmware — path to image ("" = use device.properties) - * s TypeOfFirmware — "PCI" | "PDRI" | "PERIPHERAL" - * s rebootImmediately — "true" or "false" (daemon expects string) - * - * @param handle Valid FirmwareInterfaceHandle from registerProcess() - * @param fwupdatereq Update request (passed by value, library copies it) - * @param callback Invoked on each UpdateProgress signal + * PURPOSE: + * This is the third and final step in the firmware lifecycle: + * 1. checkForUpdate() -- ask the daemon if new firmware exists + * 2. downloadFirmware() -- download the firmware image + * 3. updateFirmware() -- flash the downloaded image onto the device + * + * updateFirmware() sends a fire-and-forget D-Bus method call to the + * daemon instructing it to flash the specified firmware image. The + * function returns IMMEDIATELY -- the actual flashing happens on the + * daemon side and may take minutes. Progress is delivered through + * repeated callbacks on the BG thread. + * + * RETURN VALUES: + * RDKFW_UPDATE_SUCCESS (0) -- Request sent. Callbacks will fire later. + * RDKFW_UPDATE_FAILED (1) -- Request failed. Callback will NOT fire. + * IMPORTANT: SUCCESS does NOT mean flashing started. It means the + * request was accepted. Actual progress comes in the callbacks. + * + * CALLBACK CONTRACT: + * The UpdateCallback is invoked MULTIPLE TIMES (like downloadFirmware, + * unlike checkForUpdate which fires once). Each invocation carries: + * int progress_per -- 0 to 100 (percent complete) + * UpdateStatus status -- UPDATE_IN_PROGRESS, UPDATE_COMPLETED, or + * UPDATE_ERROR + * The callback fires on the BG thread (NOT the main thread). If the + * app needs to update UI, it must marshal the call to the main thread. + * + * EXECUTION FLOW (step numbers match code comments below): + * + * [1] Validate handle, fwupdatereq, firmwareName, TypeOfFirmware, + * callback (reject NULL/empty). This has the MOST validations + * of all three APIs (7 checks vs 4 for checkForUpdate, 5 for + * downloadFirmware) because FwUpdateReq has more required fields. + * [2] Open ephemeral D-Bus connection (fail early if D-Bus is down) + * [3] Register callback in g_update_registry (state = ACTIVE) + * -- MUST happen BEFORE sending D-Bus call to avoid race + * [4] Send fire-and-forget "UpdateFirmware" D-Bus method call + * [5] Close ephemeral connection, return RDKFW_UPDATE_SUCCESS + * + * [Later, repeatedly -- on BG thread:] + * Daemon broadcasts "UpdateProgress" signal (multiple times) + * BG thread receives it in on_update_progress_signal() + * dispatch_all_update_active() finds our ACTIVE slot, invokes callback + * If status == UPDATE_COMPLETED or UPDATE_ERROR: slot is reset to IDLE + * Otherwise: slot stays ACTIVE for next signal + * + * WHY REGISTER BEFORE SEND (Step 3 before Step 4): + * Same race condition as checkForUpdate and downloadFirmware. If the + * daemon responds instantly (e.g., trivial flash operation), the BG + * thread would receive the signal before we registered. The dispatch + * would find zero ACTIVE entries and silently drop the signal. + * Our callback would never fire. The app would hang forever. + * + * WHY CONNECT BEFORE REGISTER (Step 2 before Step 3): + * If we registered first and D-Bus connection then failed, we'd + * have a ghost ACTIVE entry that never fires (because the D-Bus + * call was never sent). The slot would stay ACTIVE forever, + * wasting 1 of 30 slots and never being cleaned up. + * + * D-BUS ARGUMENTS: (sssss) -- five strings + * This is the ONLY API that sends 5 strings. For comparison: + * checkForUpdate: (s) -- 1 string (handle) + * downloadFirmware: (ssss) -- 4 strings (handle, name, url, type) + * updateFirmware: (sssss) -- 5 strings (handle, name, location, + * type, rebootImmediately) + * + * The 5th argument, rebootImmediately, is a boolean in FwUpdateReq + * but is sent as a string "true"/"false" because the daemon's D-Bus + * interface expects all arguments as strings. + * + * REGISTRY DIFFERENCES FROM checkForUpdate AND downloadFirmware: + * - Uses g_update_registry (third separate registry, not g_registry + * or g_dwnl_registry) + * - Slot state = UPDATE_CB_STATE_ACTIVE (fires repeatedly, like download) + * - Signal format = "(tsiis)" not "(tsuss)" like download + * t = handler_id (uint64), s = firmware_name, i = progress (int32), + * i = status_code (int32), s = message + * - Status mapping uses INTEGER codes (0=IN_PROGRESS, 1=COMPLETED, + * 2=ERROR) via internal_map_update_status_code(), not STRING codes + * like download's map_dwnl_status_string() + * + * @param handle The handle returned by registerProcess(). Must be + * non-NULL and non-empty. e.g., "1" + * @param fwupdatereq Pointer to update request struct. Must be non-NULL. + * Contains firmwareName (required), LocationOfFirmware + * (optional, "" means use device.properties path), + * TypeOfFirmware (required, e.g. "PCI"), + * rebootImmediately (bool, converted to "true"/"false"). + * @param callback Function pointer invoked on each UpdateProgress signal. + * Must be non-NULL. Signature: + * void callback(int progress_per, UpdateStatus status) + * * @return RDKFW_UPDATE_SUCCESS or RDKFW_UPDATE_FAILED - */ + * + * See also: on_update_progress_signal() -- BG thread signal handler + * See also: dispatch_all_update_active() -- two-phase callback dispatch + * See also: internal_update_register_callback() -- registry slot allocation + * See also: internal_map_update_status_code() -- integer->enum mapping + * ======================================================================== */ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, const FwUpdateReq *fwupdatereq, UpdateCallback callback) { - /* [1] Validate */ + /* + * [STEP 1] INPUT VALIDATION + * + * Reject obviously bad inputs before touching D-Bus or the registry. + * This is the library's input boundary -- validate everything here. + * + * updateFirmware has the MOST validation of all three APIs: + * checkForUpdate: 2 checks (handle, callback) + * downloadFirmware: 5 checks (handle, struct, firmwareName, empty, callback) + * updateFirmware: 7 checks (handle, struct, firmwareName, firmwareName + * empty, TypeOfFirmware, TypeOfFirmware empty, + * callback) + * + * TypeOfFirmware is required here (unlike downloadFirmware where it's + * optional) because the daemon needs to know HOW to flash the image + * (different flash paths for PCI vs PDRI vs PERIPHERAL). + */ + + /* + * Check 1a: handle must not be NULL and must not be empty "". + * + * handle is the string returned by registerProcess(), e.g. "1". + * If the caller passes NULL (forgot to register first) or an + * empty string, reject immediately. The daemon would reject it + * too, but we save the D-Bus round-trip. + */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("updateFirmware: invalid handle (NULL or empty)\n"); return RDKFW_UPDATE_FAILED; } + /* + * Check 1b: the request struct pointer must not be NULL. + * + * This catches the case where the caller passes NULL instead of + * &update_req. Dereferencing NULL would crash the process. + */ if (fwupdatereq == NULL) { FWUPMGR_ERROR("updateFirmware: fwupdatereq is NULL\n"); return RDKFW_UPDATE_FAILED; } + /* + * Check 1c: firmwareName pointer must not be NULL. + * + * FwUpdateReq.firmwareName is a const char* -- it could be NULL if + * the caller forgot to set it. We need a filename to tell the + * daemon WHAT image to flash. + */ if (fwupdatereq->firmwareName == NULL) { FWUPMGR_ERROR("updateFirmware: firmwareName is NULL\n"); return RDKFW_UPDATE_FAILED; } + /* + * Check 1d: firmwareName must not be an empty string "". + * + * An empty filename is meaningless -- the daemon can't flash "". + * This catches the case where the caller did: + * update_req.firmwareName = ""; // accident + */ if (fwupdatereq->firmwareName[0] == '\0') { FWUPMGR_ERROR("updateFirmware: firmwareName is empty\n"); return RDKFW_UPDATE_FAILED; } + /* + * Check 1e: TypeOfFirmware pointer must not be NULL. + * + * Unlike downloadFirmware (where TypeOfFirmware is optional), + * updateFirmware REQUIRES TypeOfFirmware because the daemon + * uses it to select the correct flash mechanism: + * "PCI" -- flash to the main chipset + * "PDRI" -- flash to PDRI (Platform Data Recovery Image) + * "PERIPHERAL" -- flash to a connected peripheral device + * Without this, the daemon doesn't know HOW to flash the image. + */ if (fwupdatereq->TypeOfFirmware == NULL) { FWUPMGR_ERROR("updateFirmware: TypeOfFirmware is NULL\n"); return RDKFW_UPDATE_FAILED; } + /* + * Check 1f: TypeOfFirmware must not be empty "". + * + * Same reasoning as check 1e -- an empty string provides no + * flash-type information. Catches: + * update_req.TypeOfFirmware = ""; // accident + */ if (fwupdatereq->TypeOfFirmware[0] == '\0') { FWUPMGR_ERROR("updateFirmware: TypeOfFirmware is empty\n"); return RDKFW_UPDATE_FAILED; } + /* + * Check 1g: callback must not be NULL. + * + * Without a callback, the app can't receive flash progress updates. + * It would never know when the update finishes (or if it failed). + * That's always a programming error. + */ if (callback == NULL) { FWUPMGR_ERROR("updateFirmware: callback is NULL\n"); return RDKFW_UPDATE_FAILED; } + /* + * Log what we're about to flash. The ternary expressions handle + * optional fields: + * LocationOfFirmware may be NULL (it's optional in FwUpdateReq). + * If NULL or empty, the daemon uses the path from device.properties. + * rebootImmediately is a bool, logged as "yes"/"no" for clarity. + */ FWUPMGR_INFO("updateFirmware: handle='%s' firmware='%s' type='%s' " "location='%s' reboot=%s\n", handle, @@ -942,41 +1090,152 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, : "(use device.properties path)", fwupdatereq->rebootImmediately ? "yes" : "no"); - /* [2] Connect to D-Bus FIRST before registering callback + /* + * [STEP 2] CREATE EPHEMERAL D-BUS CONNECTION + * + * g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) opens a new connection to + * the system D-Bus bus. Gets a unique sender name like :1.143. * - * This prevents stale registry entries if D-Bus connection fails. + * Why a NEW connection instead of reusing the BG thread's :1.141? + * The BG thread's connection is attached to the BG thread's + * GMainContext. Using it from the main thread would require + * cross-thread GLib context management -- complex and fragile. + * A fresh per-call connection is simpler and safe. + * + * Why BEFORE registering the callback? + * If D-Bus is down (dbus-daemon crashed, socket missing), this + * call fails. We want to fail BEFORE polluting the update + * registry with an ACTIVE entry that will never be dispatched. + * Clean failure: no registry entry, no dangling state. + * + * Cost: ~2ms for the D-Bus handshake. Negligible for a firmware + * update that takes minutes. */ GError *error = NULL; GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (conn == NULL) { + /* + * D-Bus connection failed. Common causes: + * - dbus-daemon not running + * - System bus socket missing (/var/run/dbus/system_bus_socket) + * - Permission denied (D-Bus policy rejects our user) + * + * Return FAILED -- no registry entry created, nothing to clean up. + */ FWUPMGR_ERROR("updateFirmware: D-Bus connect failed: %s\n", error ? error->message : "unknown"); if (error) g_error_free(error); return RDKFW_UPDATE_FAILED; } - /* [3] Register callback AFTER D-Bus connection succeeds, BEFORE sending + /* + * [STEP 3] REGISTER CALLBACK IN THE UPDATE REGISTRY (g_update_registry) * - * Register immediately before sending to avoid race condition where - * the daemon responds before we're ready to receive the signal. + * internal_update_register_callback() does the following (see _async.c): + * 1. Locks g_update_registry.mutex + * 2. Scans all 30 UpdateCbEntry slots for: + * a. An existing ACTIVE entry with the same handle (dedup/overwrite) + * b. The first IDLE slot (free slot) + * 3. If same handle found: overwrites it (prevents stale callbacks) + * If free slot found: uses it + * If neither: returns false (registry full -- 30 concurrent updates!) + * 4. Populates the slot: + * - handle_key = strdup(handle) -- "1" (heap copy) + * - callback = our function pointer + * - state = UPDATE_CB_STATE_ACTIVE -- NOTE: ACTIVE, not PENDING! + * - registered_time = current unix timestamp + * 5. Unlocks g_update_registry.mutex + * 6. Returns true + * + * After this call, the registry has one ACTIVE entry. When the BG + * thread receives UpdateProgress signals, it will find this entry + * and invoke the callback on EVERY signal. + * + * STATE DIFFERENCE FROM THE OTHER TWO REGISTRIES: + * checkForUpdate: g_registry -- state = CB_STATE_PENDING (fires ONCE) + * downloadFirmware: g_dwnl_registry -- state = DWNL_CB_STATE_ACTIVE (fires MANY) + * updateFirmware: g_update_registry -- state = UPDATE_CB_STATE_ACTIVE (fires MANY) + * Download and update both use the ACTIVE-until-terminal pattern. + * checkForUpdate uses the PENDING->DISPATCHED->IDLE one-shot pattern. + * + * Why BEFORE the D-Bus call? + * Race condition prevention. If the daemon starts flashing + * instantly, the BG thread would receive the first UpdateProgress + * signal before we registered. dispatch_all_update_active() would + * find zero ACTIVE entries and silently discard the signal. Our + * callback would never fire. The app would hang forever. + * + * Failure case: registry full (30 concurrent pending updates). + * In practice never happens -- a device flashes one firmware + * at a time. If it does, clean up the D-Bus connection and fail. */ if (!internal_update_register_callback(handle, callback)) { FWUPMGR_ERROR("updateFirmware: registry full, handle='%s'\n", handle); + /* + * Clean up: close the D-Bus connection we opened in Step 2. + * No registry entry was created, so no registry cleanup needed. + */ g_object_unref(conn); return RDKFW_UPDATE_FAILED; } - /* [4] Fire-and-forget D-Bus UpdateFirmware method call + /* + * [STEP 4] SEND FIRE-AND-FORGET D-BUS METHOD CALL + * + * g_dbus_connection_call() sends a D-Bus method call to the daemon. + * + * Parameters to g_dbus_connection_call(): + * conn -- our ephemeral connection :1.143 + * DBUS_SERVICE_NAME -- "org.rdkfwupdater.Service" (daemon's well-known name) + * DBUS_OBJECT_PATH -- "/org/rdkfwupdater/Service" (object path) + * DBUS_INTERFACE_NAME -- "org.rdkfwupdater.Interface" + * DBUS_METHOD_UPDATE -- "UpdateFirmware" (the method name) + * g_variant_new("(sssss)", ...) -- 5-string argument tuple: + * "(sssss)" means a tuple containing five strings + * string 1: handle -- "1" (which registered client is asking) + * string 2: firmwareName -- "firmware_v8.bin" (what to flash) + * string 3: LocationOfFirmware -- path to image or "" for default + * string 4: TypeOfFirmware -- "PCI" / "PDRI" / "PERIPHERAL" + * string 5: rebootImmediately -- "true" or "false" (see note below) + * NULL -- expected reply type: we don't care + * G_DBUS_CALL_FLAGS_NONE -- no special flags + * DBUS_TIMEOUT_MS -- 5000ms (only for message queueing, not reply) + * NULL -- GCancellable: no cancellation support + * NULL -- GAsyncReadyCallback: no reply callback + * NULL -- user_data for reply callback: N/A + * + * The three trailing NULLs make this fire-and-forget. GLib queues + * the D-Bus message in the kernel's socket buffer and returns. + * + * WHY 5 STRINGS (NOT 4 LIKE downloadFirmware): + * updateFirmware sends an extra argument: rebootImmediately. + * The FwUpdateReq struct has rebootImmediately as a bool (true/false), + * but the daemon's D-Bus interface is defined with all-string + * arguments. So we convert: true -> "true", false -> "false" + * using the ternary: fwupdatereq->rebootImmediately ? "true" : "false" * - * Arguments: (sssss) - * s handle — app's handler_id string - * s firmwareName — image to flash - * s LocationOfFirmware — path or "" for device.properties default - * s TypeOfFirmware — PCI / PDRI / PERIPHERAL - * s rebootImmediately — "true" or "false" (daemon expects string) + * NULL-COALESCING for optional field: + * fwupdatereq->LocationOfFirmware ? fwupdatereq->LocationOfFirmware : "" + * If LocationOfFirmware is NULL (caller didn't set it), we send "" + * to the daemon. The daemon treats "" as "use the path from + * device.properties." TypeOfFirmware is NOT coalesced because it's + * required (validated in Step 1e/1f above). * - * Three trailing NULLs = fire and forget. + * WHAT HAPPENS ON THE DAEMON SIDE: + * 1. Daemon receives "UpdateFirmware" with 5 string arguments + * 2. Validates handler "1" is registered (from registerProcess) + * 3. Starts flashing the firmware image using the appropriate method + * 4. As flashing progresses, broadcasts UpdateProgress signals: + * - (1, "firmware_v8.bin", 0, 0, "Flash started") + * - (1, "firmware_v8.bin", 25, 0, "Writing partition 1") + * - (1, "firmware_v8.bin", 50, 0, "Writing partition 2") + * - (1, "firmware_v8.bin", 100, 1, "Flash complete") + * Signal format: "(tsiis)" where: + * t = handler_id (uint64), s = firmware_name, + * i = progress (int32 0-100), i = status_code (0/1/2), + * s = message + * 5. Our BG thread catches each signal and fires our callback */ g_dbus_connection_call( @@ -985,7 +1244,7 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, DBUS_OBJECT_PATH, DBUS_INTERFACE_NAME, DBUS_METHOD_UPDATE, /* method: UpdateFirmware */ - g_variant_new("(sssss)", /* ✅ 5 strings now! */ + g_variant_new("(sssss)", /* 5 strings (see above) */ handle, /* app's handler_id string */ fwupdatereq->firmwareName, /* image to flash */ fwupdatereq->LocationOfFirmware ? fwupdatereq->LocationOfFirmware : "", /* path or "" */ @@ -999,11 +1258,47 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, NULL /* user_data: none */ ); + /* + * [STEP 5] CLOSE EPHEMERAL CONNECTION AND RETURN + * + * g_object_unref(conn) closes our ephemeral D-Bus connection :1.143. + * The D-Bus message is already in the kernel socket buffer -- closing + * our end doesn't prevent delivery to the daemon. + * + * After this, the state of the world is: + * + * Main thread: + * - Returns RDKFW_UPDATE_SUCCESS to the caller + * - Caller enters pthread_cond_timedwait (typically 300s+ timeout) + * - Connection :1.143 is DEAD (just closed) + * + * BG thread: + * - Still sleeping in g_main_loop_run() on connection :1.141 + * - g_update_registry.entries[0] has our callback in ACTIVE state + * - Will wake up on EVERY UpdateProgress signal + * - Will call our callback MULTIPLE TIMES + * + * Daemon: + * - Received our request, started flashing the firmware + * - Will broadcast UpdateProgress signals as flashing progresses + * + * g_update_registry (update-specific, third registry): + * entries[0] = { state=ACTIVE, handle_key="1", + * callback=on_update_progress_callback } + * entries[1..29] = IDLE + * + * D-Bus connections: + * :1.141 -- BG thread persistent (ALIVE, listening for signals) + * :1.143 -- this updateFirmware ephemeral (DEAD, just closed) + * + * SUCCESS here means: "I sent the request and registered your callback." + * It does NOT mean: "Flashing started." or "Image is valid." + * The callback will fire later with actual progress. + */ g_object_unref(conn); FWUPMGR_INFO("updateFirmware: D-Bus call sent, returning SUCCESS. " "handle='%s'\n", handle); - /* [4] Return immediately — app is unblocked */ return RDKFW_UPDATE_SUCCESS; } diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c index f855aa20..640ed771 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c @@ -2114,17 +2114,68 @@ static DownloadStatus map_dwnl_status_string(const char *status_str) } /* ======================================================================== - * UPDATE FIRMWARE — INTERNAL ENGINE + * UPDATE FIRMWARE -- INTERNAL ENGINE * ======================================================================== * - * Mirror of the DownloadFirmware engine above. - * Same registry pattern, same two-phase dispatch, same lifecycle. + * This section contains all the internal machinery that powers the + * updateFirmware() public API. It is the third and final async engine + * in the library, mirroring the download engine above. + * + * ARCHITECTURE OVERVIEW: + * + * updateFirmware() [_api.c, main thread] + * | + * +--> internal_update_register_callback() [registers in g_update_registry] + * +--> g_dbus_connection_call() [fire-and-forget to daemon] + * | + * [daemon flashes firmware, broadcasts UpdateProgress signals] + * | + * on_update_progress_signal() [BG thread, GLib callback] + * | + * +--> internal_parse_update_signal_data() [extract "(tsiis)" payload] + * +--> dispatch_all_update_active() [two-phase dispatch] + * | + * +--> internal_map_update_status_code() [int -> UpdateStatus] + * +--> callback(progress, status) [app's function] + * +--> update_registry_reset_slot() [if terminal] + * + * KEY DIFFERENCES FROM DOWNLOAD ENGINE: + * + * Signal format: + * Download: "(tsuss)" -- progress is uint32, status is STRING + * Update: "(tsiis)" -- progress is int32, status is INTEGER + * + * Status mapping: + * Download: map_dwnl_status_string() uses strcmp on strings + * Update: internal_map_update_status_code() uses switch on integers + * + * Strings to free after parsing: + * Download: 3 (firmware_name, status_string, message) + * Update: 2 (firmware_name, message) -- no status_string + * + * Registry: + * Download: g_dwnl_registry with DwnlCallbackEntry and DWNL_CB_STATE_* + * Update: g_update_registry with UpdateCbEntry and UPDATE_CB_STATE_* + * + * Slot lifecycle (same as download): + * IDLE -> ACTIVE (on register) -> ACTIVE (fires repeatedly) -> IDLE (on terminal) + * + * SIGNAL: UpdateProgress "(tsiis)" + * t handler_id -- uint64, identifies the registered client + * s firmware_name -- string, image being flashed + * i progress_percent -- int32, 0 to 100 + * i status_code -- int32, 0=IN_PROGRESS, 1=COMPLETED, 2=ERROR + * s message -- string, human-readable status * - * Signal: UpdateProgress (ii) — progress_percent, status_code * Registry slot: ACTIVE until UPDATE_COMPLETED or UPDATE_ERROR, then IDLE. * ======================================================================== */ /* ---- Forward declarations for helper functions ---- */ +/* + * These forward declarations allow the functions to be defined in a + * logical order (signal handler first, then dispatch, then helpers) + * even though the C compiler needs to see declarations before use. + */ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_data); static void update_registry_reset_slot(UpdateCbEntry *entry); @@ -2133,21 +2184,79 @@ static void update_registry_reset_slot(UpdateCbEntry *entry); * ======================================================================== */ /** - * @brief Cleanup update registry — frees all strdup'd handle_key strings + * @brief Cleanup update registry -- frees all strdup'd handle_key strings + * + * PURPOSE: + * Called from internal_system_deinit() during library shutdown + * (unregisterProcess -> internal_system_deinit -> this function). + * Walks all 30 registry slots and frees any handle_key strings that + * were allocated by strdup() in internal_update_register_callback(). + * + * WHY THIS IS NEEDED: + * When the library shuts down, any ACTIVE update callbacks are + * abandoned (no more signals will be dispatched). But the strdup'd + * handle_key strings are still on the heap. Without this cleanup, + * they would leak. Valgrind would report "definitely lost" blocks. + * + * WHAT ABOUT THE CALLBACKS THEMSELVES: + * Callback function pointers are not heap-allocated -- they're just + * pointers to compiled code. Setting callback=NULL is defensive but + * doesn't free anything. The ONLY heap allocation per slot is + * handle_key (from strdup). + * + * SIGNAL UNSUBSCRIPTION: + * This function does NOT unsubscribe from the UpdateProgress D-Bus + * signal. That's handled by the BG thread's cleanup code when it + * calls g_dbus_connection_signal_unsubscribe(). The signal + * subscription and the registry are independent concerns. + * + * MUTEX DESTRUCTION: + * After freeing all strings, pthread_mutex_destroy() is called to + * release the mutex's internal resources. After this, the mutex + * must NOT be used again -- any lock/unlock would be undefined behavior. + * + * THREAD SAFETY: + * Called during shutdown when the BG thread has already been stopped. + * The mutex lock/unlock is still used for correctness, even though + * no other thread should be accessing the registry at this point. * - * Called from internal_system_deinit(). Signal unsubscription is handled - * by the background thread. + * Called from: internal_system_deinit() (in this file) */ static void internal_update_system_deinit(void) { + /* + * Lock the mutex before modifying the registry. Even during + * shutdown, we follow the locking protocol for consistency. + */ pthread_mutex_lock(&g_update_registry.mutex); + + /* + * Walk all 30 slots and free any non-NULL handle_key strings. + * + * We don't check the slot's state -- even if a slot is somehow + * in an inconsistent state, we still free its handle_key to + * prevent leaks. A NULL handle_key means the slot was already + * clean (IDLE with no previous allocation). + */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { if (g_update_registry.entries[i].handle_key != NULL) { free(g_update_registry.entries[i].handle_key); - g_update_registry.entries[i].handle_key = NULL; + g_update_registry.entries[i].handle_key = NULL; /* Prevent dangling pointer */ } } + + /* + * Unlock the mutex before destroying it. pthread_mutex_destroy() + * requires the mutex to be unlocked. Destroying a locked mutex + * is undefined behavior on most POSIX implementations. + */ pthread_mutex_unlock(&g_update_registry.mutex); + + /* + * Destroy the mutex itself. This releases any OS resources + * associated with the mutex (e.g., kernel futex state on Linux). + * After this call, the mutex must NEVER be used again. + */ pthread_mutex_destroy(&g_update_registry.mutex); FWUPMGR_INFO("internal_update_system_deinit: done\n"); @@ -2155,13 +2264,66 @@ static void internal_update_system_deinit(void) /* ======================================================================== * UPDATE SIGNAL HANDLER + * + * When the daemon broadcasts an "UpdateProgress" D-Bus signal, GLib's + * event loop on the BG thread dispatches it to on_update_progress_signal(). + * That function parses the signal, then calls dispatch_all_update_active() + * to invoke all registered UpdateCallbacks. * ======================================================================== */ /** - * @brief Called by GLib when UpdateProgress signal arrives + * @brief Called by GLib when UpdateProgress signal arrives on D-Bus + * + * PURPOSE: + * This is the BG thread's entry point for handling firmware update + * progress signals. When the daemon flashes firmware, it periodically + * broadcasts UpdateProgress signals on D-Bus. GLib's GMainLoop on + * the BG thread receives these signals and invokes THIS function. + * + * EXECUTION CONTEXT: + * Runs on the BACKGROUND THREAD (not the main thread). + * Called by g_main_loop_run() -> GLib signal dispatch. + * The BG thread subscribed to "UpdateProgress" signals during + * internal_system_init() using g_dbus_connection_signal_subscribe(). + * + * SIGNAL FORMAT -- "(tsiis)": + * t handler_id -- uint64, identifies which registered client + * s firmware_name -- string, the image being flashed + * i progress_percent -- int32, 0 to 100 + * i status_code -- int32, 0=IN_PROGRESS, 1=COMPLETED, 2=ERROR + * s message -- string, human-readable status message + * + * NOTE: This is DIFFERENT from DownloadProgress's "(tsuss)": + * - Download uses uint32 for progress, update uses int32 + * - Download uses string for status ("INPROGRESS"), update uses int32 + * - Download has 3 g_free-able strings, update has 2 + * + * FLOW: + * 1. Suppress unused parameter warnings with (void) casts + * 2. Zero-initialize InternalUpdateSignalData on the stack + * 3. Call internal_parse_update_signal_data() to extract fields + * 4. Log the parsed data for debugging + * 5. Call dispatch_all_update_active() to invoke all ACTIVE callbacks + * 6. Free heap-allocated strings (firmware_name, message) from g_variant_get + * + * MEMORY OWNERSHIP: + * g_variant_get() with "s" format allocates new strings on the heap + * via g_strdup(). The caller (this function) MUST g_free() them. + * Two strings need freeing: firmware_name and message. + * (Compare: download has three -- firmware_name, status_string, message) * - * Runs in background thread. Parses payload and dispatches to all - * ACTIVE update callbacks. + * THREAD SAFETY: + * This function itself is single-threaded (only the BG thread calls it). + * But it calls dispatch_all_update_active() which accesses the shared + * g_update_registry under mutex protection. + * + * @param conn The BG thread's persistent D-Bus connection + * @param sender The D-Bus sender (daemon's unique name) + * @param object_path D-Bus object path ("/org/rdkfwupdater/Service") + * @param interface_name D-Bus interface ("org.rdkfwupdater.Interface") + * @param signal_name "UpdateProgress" + * @param parameters GVariant containing the "(tsiis)" payload + * @param user_data NULL (not used) */ static void on_update_progress_signal(GDBusConnection *conn, const gchar *sender, @@ -2171,28 +2333,82 @@ static void on_update_progress_signal(GDBusConnection *conn, GVariant *parameters, gpointer user_data) { + /* + * Suppress "unused parameter" compiler warnings. GLib's signal + * callback signature requires all 7 parameters, but we only + * need 'parameters' (the signal payload). The (void) cast tells + * the compiler "yes, I know I'm not using these." + */ (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; FWUPMGR_INFO("on_update_progress_signal: received\n"); + /* + * Stack-allocate the signal data struct and zero-initialize it. + * memset ensures all pointers start as NULL and all integers as 0. + * This is defensive -- if parsing fails partially, we don't have + * garbage values in the struct. + */ InternalUpdateSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); + /* + * Parse the GVariant payload "(tsiis)" into our struct. + * internal_parse_update_signal_data() validates the signature, + * extracts handler_id, firmware_name, progress, status_code, + * and message. Returns false if signature mismatch. + * + * If parsing fails, we bail out. No callbacks are invoked, no + * memory needs freeing (the struct was zero-initialized). + */ if (!internal_parse_update_signal_data(parameters, &signal_data)) { FWUPMGR_ERROR("on_update_progress_signal: parse failed\n"); return; } + /* + * Log the parsed signal data for debugging. This is invaluable + * when troubleshooting "callback never fired" bugs -- it proves + * whether the signal was received and what it contained. + * + * PRIu64 is the portable format specifier for uint64_t + * (avoids warnings on 32-bit vs 64-bit platforms). + */ FWUPMGR_INFO("on_update_progress_signal: handler=%" PRIu64 " firmware='%s' progress=%d%% status=%d\n", signal_data.handler_id, signal_data.firmware_name ? signal_data.firmware_name : "(null)", signal_data.progress_percent, signal_data.status_code); + /* + * Dispatch the parsed signal data to all ACTIVE update callbacks. + * dispatch_all_update_active() does the two-phase dispatch: + * Phase 1: snapshot ACTIVE entries under mutex + * Phase 2: invoke callbacks without mutex + * If terminal status: reset slot to IDLE + * + * After this call returns, all registered UpdateCallbacks have + * been invoked with the current progress and status. + */ dispatch_all_update_active(&signal_data); - // Free allocated strings from g_variant_get + /* + * Free the heap-allocated strings from g_variant_get(). + * + * g_variant_get() with the "s" format specifier allocates new + * strings via g_strdup(). We own these strings and must free them. + * + * Two strings to free: + * 1. firmware_name -- the image filename (e.g., "firmware_v8.bin") + * 2. message -- the status message (e.g., "Writing partition 2") + * + * Compare with download's on_download_progress_signal() which frees + * THREE strings (firmware_name, status_string, message). Update + * has no status_string because it uses an integer status_code instead. + * + * g_free(NULL) is safe (it's a no-op), so we don't need NULL checks. + */ g_free(signal_data.firmware_name); g_free(signal_data.message); } @@ -2200,20 +2416,75 @@ static void on_update_progress_signal(GDBusConnection *conn, /** * @brief Dispatch UpdateProgress signal to every ACTIVE update callback * - * TWO-PHASE DESIGN (identical to download dispatch): + * PURPOSE: + * Called by on_update_progress_signal() after parsing the D-Bus signal + * payload. Finds ALL ACTIVE entries in g_update_registry and invokes + * their callbacks with the current progress and status. * - * PHASE 1 (mutex held): - * Snapshot all ACTIVE entries. - * Mark is_final=true only if status is COMPLETED or ERROR. - * Release mutex. + * TWO-PHASE DESIGN (identical pattern to download dispatch): * - * PHASE 2 (mutex released): - * Invoke callback(progress_per, status) for each snapshot. - * If is_final: re-acquire mutex, reset slot to IDLE. - * If in-progress: leave slot ACTIVE for next signal. + * PHASE 1 (mutex HELD): + * - Lock g_update_registry.mutex + * - Scan all 30 slots for ACTIVE entries + * - For each ACTIVE entry, copy callback pointer, handle, and slot + * index into a local snapshot array on the stack + * - Determine if this is a terminal signal (COMPLETED or ERROR) + * - Unlock mutex + * + * PHASE 2 (mutex RELEASED): + * - Iterate through snapshot array + * - Invoke each callback(progress_percent, status) + * - If terminal signal: re-lock mutex, reset slot to IDLE, unlock + * - If in-progress: leave slot ACTIVE for the next signal + * + * WHY TWO PHASES (not one): + * If we held the mutex while invoking callbacks, the callbacks could + * not safely call any library function that touches the registry + * (e.g., updateFirmware() again, or unregisterProcess()). That would + * deadlock because our thread already holds the mutex. By releasing + * the mutex before invoking callbacks, we avoid this entirely. + * + * WHY SNAPSHOT (not direct access): + * Once we release the mutex, another thread could modify the registry + * (e.g., the main thread calling updateFirmware() to register a new + * callback). The snapshot freezes the state at scan time, so our + * iteration is safe regardless of concurrent modifications. + * + * TERMINAL vs IN-PROGRESS SIGNALS: + * - status == UPDATE_COMPLETED or UPDATE_ERROR -> TERMINAL + * The update is done (success or failure). Reset slot to IDLE so + * it can be reused for future updateFirmware() calls. + * - status == UPDATE_IN_PROGRESS -> IN-PROGRESS + * The update is still running. Leave slot ACTIVE so the NEXT + * UpdateProgress signal also dispatches to this callback. + * + * This is the key difference from checkForUpdate's dispatch: + * checkForUpdate: slot fires ONCE then goes to IDLE + * downloadFirmware: slot fires MANY times, IDLE on terminal + * updateFirmware: slot fires MANY times, IDLE on terminal (same) + * + * THREAD SAFETY: + * Called on the BG thread. Accesses g_update_registry under mutex. + * Phase 2 callbacks run WITHOUT mutex -- the app's callback function + * can safely call library APIs without deadlocking. + * + * @param signal_data Parsed signal data from on_update_progress_signal() */ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_data) { + /* + * Local snapshot struct -- stores one entry's callback info. + * We allocate an array of these on the stack (up to 30 entries). + * + * Fields: + * callback -- the UpdateCallback function pointer to invoke + * handle_copy -- string copy of handle_key (for logging only) + * slot_index -- which g_update_registry.entries[] index this is + * is_final -- true if this signal ends the update (COMPLETED/ERROR) + * + * handle_copy is a fixed-size char[256] buffer, not a heap allocation. + * This avoids malloc/free overhead for a temporary logging string. + */ typedef struct { UpdateCallback callback; char handle_copy[256]; @@ -2224,16 +2495,53 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da UpdateSnapshot snapshots[MAX_PENDING_CALLBACKS]; int count = 0; + /* + * Map the raw integer status_code to the UpdateStatus enum. + * internal_map_update_status_code() does: + * 0 -> UPDATE_IN_PROGRESS + * 1 -> UPDATE_COMPLETED + * 2 -> UPDATE_ERROR + * anything else -> UPDATE_ERROR (defensive default) + * + * Then determine if this is a terminal signal (update finished). + * A terminal signal means we should reset the slot to IDLE after + * invoking the callback, because no more signals are coming. + */ UpdateStatus status = internal_map_update_status_code(signal_data->status_code); bool is_final = (status == UPDATE_COMPLETED || status == UPDATE_ERROR); /* ---- PHASE 1: snapshot under mutex ---- */ + + /* + * Lock the registry. While we hold this lock, no other thread can + * modify g_update_registry (e.g., the main thread can't register + * a new callback via internal_update_register_callback()). + */ pthread_mutex_lock(&g_update_registry.mutex); + /* + * Scan all 30 slots. For each ACTIVE entry, copy its info into + * the snapshot array. We copy the callback pointer (not the entry + * pointer) because after releasing the mutex, the entry could be + * modified by another thread. + */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { UpdateCbEntry *e = &g_update_registry.entries[i]; + + /* + * Skip non-ACTIVE entries. IDLE slots have no callback to invoke. + * (There's no PENDING or DISPATCHED state for update -- only + * IDLE and ACTIVE.) + */ if (e->state != UPDATE_CB_STATE_ACTIVE) continue; + /* + * Copy this entry's data into the snapshot: + * callback -- the function pointer we'll invoke in Phase 2 + * slot_index -- needed to reset this specific slot in Phase 2 + * is_final -- same for all entries (determined by status_code) + * handle_copy -- snprintf'd for safe logging (truncated to 255 chars) + */ snapshots[count].callback = e->callback; snapshots[count].slot_index = i; snapshots[count].is_final = is_final; @@ -2249,11 +2557,22 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da signal_data->progress_percent, is_final); } + /* + * Release the mutex. From this point, the main thread can freely + * register new callbacks. Our snapshot is a frozen copy -- it won't + * be affected by concurrent registry modifications. + */ pthread_mutex_unlock(&g_update_registry.mutex); FWUPMGR_INFO("dispatch_all_update_active: %d callback(s) to fire\n", count); /* ---- PHASE 2: invoke callbacks, no mutex held ---- */ + + /* + * Iterate through the snapshot array and invoke each callback. + * No mutex is held during callback invocation -- the app's callback + * can safely call any library function without deadlocking. + */ for (int i = 0; i < count; i++) { UpdateSnapshot *s = &snapshots[i]; @@ -2261,21 +2580,41 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da "for handle='%s'\n", s->handle_copy); /* + * INVOKE THE APP'S CALLBACK + * * Callback signature: void fn(int progress_per, UpdateStatus status) - * Matches UpdateCallback typedef exactly. + * Matches the UpdateCallback typedef exactly. + * + * Arguments: + * signal_data->progress_percent -- 0 to 100 (how far along) + * status -- UPDATE_IN_PROGRESS, UPDATE_COMPLETED, or UPDATE_ERROR + * + * This runs on the BG thread. If the app needs to update UI, + * it must marshal the call to the main thread (e.g., via + * pthread_cond_signal, g_idle_add, or similar mechanism). + * + * The callback MUST NOT block for a long time, because it blocks + * this BG thread from processing further D-Bus signals. */ s->callback(signal_data->progress_percent, status); /* - * If this was the final signal (COMPLETED or ERROR), reset slot to IDLE. - * For in-progress signals, leave slot ACTIVE for the next signal. + * If this was a TERMINAL signal (COMPLETED or ERROR), reset + * the slot back to IDLE so it can be reused. + * + * We must RE-ACQUIRE the mutex to modify the registry. This is + * a brief lock/unlock pair -- just enough to reset one slot. + * + * If this was an IN-PROGRESS signal, we leave the slot ACTIVE. + * The next UpdateProgress signal will dispatch to the same + * callback again. */ if (s->is_final) { pthread_mutex_lock(&g_update_registry.mutex); update_registry_reset_slot(&g_update_registry.entries[s->slot_index]); pthread_mutex_unlock(&g_update_registry.mutex); - FWUPMGR_INFO("dispatch_all_update_active: slot %d → IDLE " + FWUPMGR_INFO("dispatch_all_update_active: slot %d -> IDLE " "(update ended)\n", s->slot_index); } } @@ -2283,61 +2622,186 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da /* ======================================================================== * UPDATE REGISTRY OPERATIONS + * + * These functions manage the g_update_registry -- allocating slots for + * new update callbacks (internal_update_register_callback) and cleaning + * up slots when updates complete (update_registry_reset_slot). + * + * The registry holds up to MAX_PENDING_CALLBACKS (30) entries. + * Each entry has two states: UPDATE_CB_STATE_IDLE (available) and + * UPDATE_CB_STATE_ACTIVE (callback registered, waiting for signals). * ======================================================================== */ /** * @brief Register an update callback keyed by handle * - * Sets slot to ACTIVE. Slot receives ALL subsequent UpdateProgress signals - * until UPDATE_COMPLETED or UPDATE_ERROR resets it to IDLE. + * PURPOSE: + * Called by updateFirmware() (in _api.c) AFTER the D-Bus connection + * succeeds but BEFORE the fire-and-forget D-Bus call is sent. + * Allocates a slot in g_update_registry so the BG thread can find + * the callback when UpdateProgress signals arrive. + * + * HOW IT WORKS: + * 1. Lock g_update_registry.mutex (prevents races with BG thread) + * 2. Scan all MAX_PENDING_CALLBACKS (30) slots looking for: + * a. An existing ACTIVE slot with the same handle (dedup case) + * b. The first IDLE slot (normal allocation case) + * 3. Pick the target: + * - If same handle found: overwrite it (existing_slot) + * - Else if free slot found: use it (free_slot) + * - Else: return false (registry full) + * 4. Populate the target slot: + * - handle_key = strdup(handle) -- heap copy of "1" + * - callback = the UpdateCallback function pointer + * - state = UPDATE_CB_STATE_ACTIVE -- ready for dispatch + * - registered_time = time(NULL) -- unix timestamp + * 5. Unlock mutex and return true * * SAME HANDLE TWICE: - * Overwrites existing ACTIVE slot for the same handle. + * If the same handle already has an ACTIVE slot (e.g., the app calls + * updateFirmware() again before the first update finishes), the old + * entry is OVERWRITTEN. The old handle_key string is freed first to + * avoid a memory leak. This means: + * - Only ONE active update callback per handle at a time + * - The NEW callback replaces the old one + * - The old callback will never fire again + * + * WHY ACTIVE (NOT PENDING): + * checkForUpdate uses PENDING -> DISPATCHED -> IDLE (fires once). + * updateFirmware uses ACTIVE -> IDLE (fires many times until terminal). + * The slot stays ACTIVE and the callback fires on EVERY UpdateProgress + * signal until the status is UPDATE_COMPLETED or UPDATE_ERROR, at + * which point dispatch_all_update_active() resets it to IDLE. + * + * THREAD SAFETY: + * Thread-safe. Protected by g_update_registry.mutex. + * Called from the main thread (inside updateFirmware()). + * The BG thread reads the same registry in dispatch_all_update_active(). + * The mutex ensures they never read/write the same slot simultaneously. + * + * MEMORY: + * handle_key = strdup(handle) -- heap allocated by THIS function. + * Freed by update_registry_reset_slot() when the slot returns to IDLE, + * or freed here if overwriting an existing entry. + * + * @param handle The handle string from registerProcess(), e.g. "1" + * @param callback The UpdateCallback function pointer to invoke later + * @return true if registered, false if registry full (all 30 slots occupied) */ bool internal_update_register_callback(FirmwareInterfaceHandle handle, UpdateCallback callback) { + /* + * Lock the update registry mutex. This mutex protects the entire + * g_update_registry.entries[] array from concurrent access. + * The BG thread also locks this mutex when dispatching callbacks. + */ pthread_mutex_lock(&g_update_registry.mutex); + /* + * We need to find a slot. Two pointers track our search: + * free_slot -- first IDLE entry found (available for use) + * existing_slot -- entry with same handle (dedup/overwrite case) + * + * Both start NULL. If neither is found after scanning, the registry + * is full of ACTIVE entries (all 30 slots occupied by other handles). + */ UpdateCbEntry *free_slot = NULL; UpdateCbEntry *existing_slot = NULL; + /* + * Linear scan through all 30 slots. We MUST scan the entire array + * even after finding a free slot, because we need to check for + * duplicate handles. An existing ACTIVE entry for the same handle + * takes priority over a free slot. + */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { UpdateCbEntry *e = &g_update_registry.entries[i]; + /* + * Check for DEDUP: is this an ACTIVE entry with the same handle? + * + * If the app calls updateFirmware() twice with the same handle + * before the first update finishes, we find the old entry here + * and overwrite it rather than wasting a new slot. + * + * strcmp(e->handle_key, handle) == 0 means exact string match. + * The handle_key NULL check prevents strcmp(NULL, ...) crash. + */ if (e->state == UPDATE_CB_STATE_ACTIVE && e->handle_key != NULL && strcmp(e->handle_key, handle) == 0) { existing_slot = e; - break; + break; /* Found duplicate -- no need to continue scanning */ } + /* + * Track the FIRST idle slot we encounter. We only save the + * first one (free_slot == NULL check) because we want the + * lowest-indexed available slot for consistency. + */ if (free_slot == NULL && e->state == UPDATE_CB_STATE_IDLE) { free_slot = e; } } + /* + * Pick the target slot: + * - Prefer existing_slot (dedup/overwrite case) + * - Fall back to free_slot (normal allocation case) + * - If both NULL: registry is full, fail + */ UpdateCbEntry *target = existing_slot ? existing_slot : free_slot; if (target == NULL) { + /* + * All 30 slots are ACTIVE with different handles. This should + * never happen in practice -- a device rarely has more than + * one or two concurrent update clients. But we handle it + * gracefully by returning false instead of crashing. + */ FWUPMGR_ERROR("internal_update_register_callback: registry full (max=%d)\n", MAX_PENDING_CALLBACKS); pthread_mutex_unlock(&g_update_registry.mutex); return false; } + /* + * If we're overwriting an existing entry (same handle), free the + * old handle_key string to avoid a memory leak. The old strdup'd + * string is on the heap and must be explicitly freed. + */ if (existing_slot) { FWUPMGR_INFO("internal_update_register_callback: " "overwriting existing for handle='%s'\n", handle); free(target->handle_key); - target->handle_key = NULL; + target->handle_key = NULL; /* Defensive: NULL before reassignment */ } + /* + * Populate the slot with the new callback registration. + * + * strdup(handle) creates a heap copy of the handle string "1". + * We need our own copy because the caller's string may go out of + * scope or be freed after updateFirmware() returns. + * + * UPDATE_CB_STATE_ACTIVE means this slot is ready to receive + * UpdateProgress signals. The BG thread's dispatch_all_update_active() + * will find it and invoke the callback. + * + * registered_time is recorded for diagnostic purposes (log how + * long a slot has been active). + */ target->handle_key = strdup(handle); target->callback = callback; target->state = UPDATE_CB_STATE_ACTIVE; target->registered_time = time(NULL); + /* + * Unlock the mutex. The slot is now visible to the BG thread. + * From this point, any UpdateProgress signal will find our ACTIVE + * entry and invoke the callback. + */ pthread_mutex_unlock(&g_update_registry.mutex); FWUPMGR_INFO("internal_update_register_callback: registered handle='%s'\n", @@ -2347,35 +2811,163 @@ bool internal_update_register_callback(FirmwareInterfaceHandle handle, /** * @brief Reset an update registry slot to IDLE - * MUST be called with g_update_registry.mutex held. + * + * PURPOSE: + * Returns a single UpdateCbEntry to the IDLE (empty) state so it + * can be reused by a future updateFirmware() call. Called in two + * situations: + * 1. dispatch_all_update_active() -- when a terminal signal arrives + * (UPDATE_COMPLETED or UPDATE_ERROR), the slot is reset after + * the callback is invoked. + * 2. internal_update_system_deinit() -- during library shutdown, + * all slots are cleaned up (though deinit frees handle_key + * directly rather than calling this function). + * + * WHAT IT DOES: + * 1. Frees the handle_key string (heap-allocated by strdup in + * internal_update_register_callback). Sets pointer to NULL. + * 2. Clears the callback function pointer to NULL. + * 3. Resets registered_time to 0. + * 4. Sets state back to UPDATE_CB_STATE_IDLE. + * + * After this call, the slot is indistinguishable from a never-used + * slot. It will be found by the next registry scan looking for a + * free slot. + * + * THREAD SAFETY: + * MUST be called with g_update_registry.mutex HELD by the caller. + * This function does NOT lock the mutex itself -- the caller is + * responsible for locking. This is because the caller typically + * needs to do the lock, call this function, then do other work + * before unlocking (or is already inside a locked section). + * + * MEMORY: + * Frees one heap allocation: handle_key (from strdup). + * Does NOT free the entry itself -- entries are array elements + * inside g_update_registry, not individually heap-allocated. + * + * @param entry Pointer to the UpdateCbEntry to reset. Must not be NULL. */ static void update_registry_reset_slot(UpdateCbEntry *entry) { + /* + * Free the handle_key string if it exists. + * + * handle_key was allocated by strdup() in + * internal_update_register_callback(). We must free it to avoid + * a memory leak. The NULL check is defensive -- an IDLE slot + * should already have handle_key == NULL, but we check anyway. + */ if (entry->handle_key != NULL) { free(entry->handle_key); - entry->handle_key = NULL; + entry->handle_key = NULL; /* Prevent dangling pointer / double-free */ } + + /* + * Clear the callback function pointer. Setting to NULL ensures + * that even if something accidentally tries to invoke this slot's + * callback, it will be a NULL dereference (crash) rather than + * calling a stale function pointer (undefined behavior / security risk). + */ entry->callback = NULL; + + /* + * Reset the registration timestamp. Not strictly necessary for + * correctness, but keeps the slot in a clean, known state. + */ entry->registered_time = 0; + + /* + * Set state back to IDLE. This is the critical line -- it's what + * makes the slot available for reuse by the next + * internal_update_register_callback() call. The linear scan in + * that function looks for UPDATE_CB_STATE_IDLE to find free slots. + */ entry->state = UPDATE_CB_STATE_IDLE; } /* ======================================================================== * UPDATE SIGNAL DATA HELPERS + * + * These helper functions handle the translation between D-Bus wire + * format and the library's internal types: + * + * internal_parse_update_signal_data() -- GVariant "(tsiis)" -> struct + * internal_map_update_status_code() -- int (0/1/2) -> UpdateStatus enum + * + * Both are pure functions with no side effects (except logging on error). * ======================================================================== */ /** - * @brief Parse GVariant UpdateProgress payload + * @brief Parse GVariant UpdateProgress payload into a struct + * + * PURPOSE: + * Extracts the five fields from the D-Bus UpdateProgress signal's + * GVariant payload and stores them in an InternalUpdateSignalData + * struct for easy access by the dispatch logic. + * + * EXPECTED GVariant SIGNATURE: "(tsiis)" + * t handler_id -- uint64: identifies which registered client + * s firmware_name -- string: the image being flashed (e.g., "firmware_v8.bin") + * i progress_percent -- int32: 0 to 100 + * i status_code -- int32: 0=IN_PROGRESS, 1=COMPLETED, 2=ERROR + * s message -- string: human-readable status message + * + * COMPARISON WITH DOWNLOAD SIGNAL: + * Download signal "(tsuss)": + * t handler_id, s firmware_name, u progress (uint32), + * s status_string ("INPROGRESS"/"COMPLETED"/"ERROR"), s message + * Update signal "(tsiis)": + * t handler_id, s firmware_name, i progress (int32), + * i status_code (0/1/2), s message + * + * Key differences: + * - Download: progress is uint32 (u), status is string (s) + * - Update: progress is int32 (i), status is int32 (i) + * - Download: 3 strings to g_free (firmware_name, status_string, message) + * - Update: 2 strings to g_free (firmware_name, message) + * + * MEMORY OWNERSHIP: + * g_variant_get() with "s" format ALLOCATES new strings on the heap + * (via g_strdup). The CALLER is responsible for freeing them with + * g_free() when done. This function sets: + * out_data->firmware_name -- caller must g_free() + * out_data->message -- caller must g_free() + * Integer fields (handler_id, progress_percent, status_code) are + * simple value copies -- no heap allocation. * - * Expected GVariant signature: (ii) - * i progress_percent (0–100) - * i status_code (maps to UpdateStatus) + * THREAD SAFETY: + * Safe -- operates only on its parameters (no global state). + * Called from on_update_progress_signal() on the BG thread. + * + * @param parameters The GVariant from the D-Bus signal. Must not be NULL. + * @param out_data Output struct to populate. Must not be NULL. + * Caller must g_free firmware_name and message. + * @return true on success, false if parameters is NULL, out_data is NULL, + * or the GVariant signature doesn't match "(tsiis)". */ bool internal_parse_update_signal_data(GVariant *parameters, InternalUpdateSignalData *out_data) { + /* + * NULL checks on both parameters. If either is NULL, we can't + * proceed -- return false immediately. No cleanup needed because + * nothing has been allocated yet. + */ if (parameters == NULL || out_data == NULL) return false; + /* + * Verify the GVariant's type signature matches what we expect. + * + * g_variant_get_type_string() returns the GVariant's type as a + * string, e.g. "(tsiis)". If the daemon sends a different format + * (e.g., due to a version mismatch), we'd get garbage if we tried + * to extract with the wrong format. This check catches mismatches + * early with a clear error message. + * + * strcmp returns 0 if the strings are equal. If NOT equal (non-zero), + * log the unexpected signature and return false. + */ const gchar *sig = g_variant_get_type_string(parameters); if (strcmp(sig, "(tsiis)") != 0) { FWUPMGR_ERROR("internal_parse_update_signal_data: " @@ -2383,12 +2975,37 @@ bool internal_parse_update_signal_data(GVariant *parameters, return false; } + /* + * Declare local variables to receive the extracted values. + * We use typed locals rather than extracting directly into out_data + * for clarity and to match the GLib API pattern. + * + * guint64 handler_id -- maps to 't' (uint64) + * gchar *firmware_name -- maps to 's' (string, heap-allocated by g_variant_get) + * gint32 progress -- maps to 'i' (int32) + * gint32 status -- maps to 'i' (int32) + * gchar *message_str -- maps to 's' (string, heap-allocated by g_variant_get) + */ guint64 handler_id = 0; gchar *firmware_name = NULL; gint32 progress = 0; gint32 status = 0; gchar *message_str = NULL; + /* + * Extract all five fields from the GVariant in one call. + * + * g_variant_get() is the inverse of g_variant_new(). The format + * string "(tsiis)" tells GLib how to interpret the binary data: + * t -> extract as guint64, store at &handler_id + * s -> extract as string, allocate copy, store at &firmware_name + * i -> extract as gint32, store at &progress + * i -> extract as gint32, store at &status + * s -> extract as string, allocate copy, store at &message_str + * + * The 's' format specifier ALWAYS allocates a new string (g_strdup). + * This is why the caller must g_free() firmware_name and message. + */ g_variant_get(parameters, "(tsiis)", &handler_id, &firmware_name, @@ -2396,27 +3013,100 @@ bool internal_parse_update_signal_data(GVariant *parameters, &status, &message_str); + /* + * Copy the extracted values into the output struct. + * + * For strings (firmware_name, message): we transfer OWNERSHIP of + * the heap-allocated string to the caller via the out_data struct. + * The caller (on_update_progress_signal) is responsible for calling + * g_free() on these pointers when done. + * + * For integers (handler_id, progress_percent, status_code): these + * are simple value copies. No heap allocation, no cleanup needed. + */ out_data->handler_id = handler_id; - out_data->firmware_name = firmware_name; // Caller must g_free + out_data->firmware_name = firmware_name; /* Caller must g_free */ out_data->progress_percent = progress; out_data->status_code = status; - out_data->message = message_str; // Caller must g_free + out_data->message = message_str; /* Caller must g_free */ return true; } /** - * @brief Map raw integer to UpdateStatus enum + * @brief Map raw integer status code to UpdateStatus enum + * + * PURPOSE: + * The daemon's UpdateProgress signal sends status as a raw integer + * (0, 1, or 2). The library's public API uses the UpdateStatus enum + * (UPDATE_IN_PROGRESS, UPDATE_COMPLETED, UPDATE_ERROR). This function + * translates between the two representations. + * + * COMPARISON WITH DOWNLOAD STATUS MAPPING: + * Download uses STRING codes: "INPROGRESS", "COMPLETED", "ERROR" + * -> mapped by map_dwnl_status_string() using strcmp() + * Update uses INTEGER codes: 0, 1, 2 + * -> mapped by THIS function using a switch statement + * + * The integer approach is simpler and faster (no string comparison), + * but less self-documenting in D-Bus traces. The two APIs evolved + * independently, which is why they use different conventions. + * + * MAPPING: + * 0 -> UPDATE_IN_PROGRESS (flashing is underway, more signals coming) + * 1 -> UPDATE_COMPLETED (flashing finished successfully) + * 2 -> UPDATE_ERROR (flashing failed) + * anything else -> UPDATE_ERROR (defensive default, with error log) + * + * WHY DEFAULT TO ERROR: + * If the daemon sends an unknown status code (e.g., 3), we treat it + * as an error. This is the SAFEST default because: + * - It causes the slot to be reset to IDLE (terminal status) + * - It notifies the app that something unexpected happened + * - It prevents the slot from staying ACTIVE forever + * If we defaulted to IN_PROGRESS, an unknown code would leave the + * slot ACTIVE indefinitely, leaking a registry slot. + * + * THREAD SAFETY: + * Pure function -- no side effects, no global state. Safe to call + * from any thread. + * + * @param status_code The raw integer from the D-Bus signal (0, 1, or 2) + * @return The corresponding UpdateStatus enum value */ UpdateStatus internal_map_update_status_code(int32_t status_code) { switch (status_code) { + /* + * 0 = IN_PROGRESS: The daemon is still flashing the firmware. + * More UpdateProgress signals will follow. The callback slot + * stays ACTIVE. + */ case 0: return UPDATE_IN_PROGRESS; + + /* + * 1 = COMPLETED: The firmware was flashed successfully. + * This is a TERMINAL status -- no more signals will come. + * dispatch_all_update_active() will reset the slot to IDLE. + */ case 1: return UPDATE_COMPLETED; + + /* + * 2 = ERROR: The firmware flash failed. + * This is a TERMINAL status -- no more signals will come. + * dispatch_all_update_active() will reset the slot to IDLE. + */ case 2: return UPDATE_ERROR; + + /* + * Unknown status code. This should never happen with a matching + * daemon version. But if the daemon is newer and adds status + * code 3 (e.g., "PAUSED"), we default to ERROR so the slot + * gets cleaned up rather than stuck ACTIVE forever. + */ default: FWUPMGR_ERROR("internal_map_update_status_code: " - "unknown %d → UPDATE_ERROR\n", status_code); + "unknown %d -> UPDATE_ERROR\n", status_code); return UPDATE_ERROR; } } From f0943d8584aa00b7bf0ca4e4937f9717892e708a Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 07:33:35 +0000 Subject: [PATCH 17/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Addressing copilot reviews - removed #if 0 block --- librdkFwupdateMgr/src/rdkFwupdateMgr_api.c | 34 ---------------------- 1 file changed, 34 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index 49f5750d..ff595656 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -404,40 +404,6 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, return CHECK_FOR_UPDATE_SUCCESS; } -#if 0 -/* ======================================================================== - * LIBRARY LIFECYCLE - * ======================================================================== */ - -/** - * @brief Library constructor — auto-called when .so is loaded - * - * Initializes the internal async engine (registry + background thread) - * before any app code runs. - */ -__attribute__((constructor)) -static void rdkFwupdateMgr_lib_init(void) -{ - FWUPMGR_INFO("=== rdkFwupdateMgr library loading ===\n"); - if (internal_system_init() != 0) { - FWUPMGR_ERROR("rdkFwupdateMgr_lib_init: internal_system_init FAILED\n"); - } - FWUPMGR_INFO("=== rdkFwupdateMgr library ready ===\n"); -} - -/** - * @brief Library destructor — auto-called when .so is unloaded - * - * Stops background thread and frees all resources cleanly. - */ -__attribute__((destructor)) -static void rdkFwupdateMgr_lib_deinit(void) -{ - FWUPMGR_INFO("=== rdkFwupdateMgr library unloading ===\n"); - internal_system_deinit(); - FWUPMGR_INFO("=== rdkFwupdateMgr library unloaded ===\n"); -} -#endif /* * downloadFirmware - Initiate a non-blocking firmware download. * From d257a4f5bcc7eb258e0e669adc3252a7fc6e9e80 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:06:37 +0530 Subject: [PATCH 18/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 56b25d1a..afe658d8 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -741,7 +741,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib if (internal_system_init() != 0) { FWUPMGR_ERROR("rdkFwupdateMgr_lib_init: internal_system_init FAILED\n"); } - FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen successfull ===\n"); + FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen successful ===\n"); /* * [STEP 10] RETURN THE HANDLE TO THE CALLER From b58e4c842a6c32154c352d65b8892b951af77e93 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 13:08:35 +0530 Subject: [PATCH 19/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/dbus/rdkFwupdateMgr_handlers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dbus/rdkFwupdateMgr_handlers.c b/src/dbus/rdkFwupdateMgr_handlers.c index 57921a01..06e75bf0 100644 --- a/src/dbus/rdkFwupdateMgr_handlers.c +++ b/src/dbus/rdkFwupdateMgr_handlers.c @@ -477,7 +477,7 @@ static int fetch_xconf_firmware_info( XCONFRES *pResponse, int server_type, int Rfc_t local_rfc_list = {0}; getRFCSettings(&local_rfc_list); // Read actual RFC settings from system - const char *local_immed_reboot_flag = "true"; // Making true as default setting to make it work in Throttle enable mode. + const char *local_immed_reboot_flag = "false"; // Keep throttle behavior enabled by default; set to "true" only when explicitly requested. int local_delay_dwnl = 0; // Default daemon setting const char *local_lastrun = "0"; // Default daemon setting char *local_disableStatsUpdate = "false"; // Default daemon setting From 2898f33b0697e41feaf50987a02a8c28629bc79a Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 08:25:07 +0000 Subject: [PATCH 20/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Addressing copilot reviews --- .../src/rdkFwupdateMgr_async_internal.h | 3 +++ librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 16 ++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h b/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h index f12de154..89570438 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h @@ -71,6 +71,9 @@ extern "C" { #define DBUS_SIGNAL_COMPLETE "CheckForUpdateComplete" #define DBUS_TIMEOUT_MS 5000 +/** D-Bus call timeout for synchronous register/unregister calls (10 seconds) */ +#define DBUS_SYNC_TIMEOUT_MS 10000 + /* ======================================================================== * CALLBACK ENTRY STATE * ======================================================================== */ diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 56b25d1a..e6a93085 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -96,7 +96,7 @@ /** Maximum length for library version string */ #define MAX_LIB_VERSION_LEN 64 -/* DBUS_TIMEOUT_MS is defined in rdkFwupdateMgr_async_internal.h */ +/* DBUS_SYNC_TIMEOUT_MS is defined in rdkFwupdateMgr_async_internal.h (10s) */ /* ======================================================================== * INTERNAL CONTEXT STRUCTURE @@ -261,7 +261,7 @@ static bool validate_lib_version(const char *libVersion) * * THREADING MODEL: * - This function runs entirely on the CALLER'S thread - * - It BLOCKS (synchronous D-Bus call) for up to DBUS_TIMEOUT_MS (5s) + * - It BLOCKS (synchronous D-Bus call) for up to DBUS_SYNC_TIMEOUT_MS (10s) * - At the end, it spawns a background thread for signal reception * - After return: 2 threads exist (caller's + library BG thread) * @@ -475,7 +475,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib * G_DBUS_CALL_FLAGS_NONE -- No special flags (could use NO_AUTO_START * to prevent D-Bus activation, but we want * the daemon to auto-start if not running) - * DBUS_TIMEOUT_MS -- 5000 ms (5 seconds). If daemon doesn't reply + * DBUS_SYNC_TIMEOUT_MS -- 10000 ms (10 seconds). If daemon doesn't reply * within this time, the call fails with timeout error. * NULL -- No GCancellable (we can't cancel this operation) * &error -- Where to store error details on failure @@ -495,7 +495,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib * BLOCKING BEHAVIOR: * Our thread is SUSPENDED here until: * a) Daemon replies (typically <10ms) -- we get 'result' - * b) 5-second timeout expires -- we get NULL + timeout GError + * b) 10-second timeout expires -- we get NULL + timeout GError * c) D-Bus daemon signals an error -- we get NULL + error GError */ FWUPMGR_INFO("Calling RegisterProcess D-Bus method...\n"); @@ -504,7 +504,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib "RegisterProcess", g_variant_new("(ss)", processName, libVersion), G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, + DBUS_SYNC_TIMEOUT_MS, NULL, /* GCancellable */ &error ); @@ -611,7 +611,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib "UnregisterProcess", g_variant_new("(t)", handler_id), G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, + DBUS_SYNC_TIMEOUT_MS, NULL, &cleanup_error ); @@ -1174,7 +1174,7 @@ void unregisterProcess(FirmwareInterfaceHandle handler) * FALSE = daemon didn't find handler_id (already removed, or unknown) * * BLOCKING BEHAVIOR: - * Main thread blocks here for up to DBUS_TIMEOUT_MS (5 seconds). + * Main thread blocks here for up to DBUS_SYNC_TIMEOUT_MS (10 seconds). * Typical response time: <5ms (just a hash table lookup + remove). * * WHAT THE DAEMON DOES: @@ -1209,7 +1209,7 @@ void unregisterProcess(FirmwareInterfaceHandle handler) "UnregisterProcess", g_variant_new("(t)", handler_id), G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, + DBUS_SYNC_TIMEOUT_MS, NULL, /* GCancellable */ &error ); From 04e49ab434877c9b9b964dd39985c4f6e0a99727 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 08:31:50 +0000 Subject: [PATCH 21/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Addressing copilot reviews --- .../src/rdkFwupdateMgr_process.c | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index f6962ebe..72b1a4b4 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -733,13 +733,49 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib * D-BUS: 1 persistent connection in BG thread (for signal reception) * * If internal_system_init() returns non-zero (failure): - * - We log an error but STILL return the handle - * - The handle is valid for the daemon, but callbacks won't work - * - This is a degraded state (TODO: consider returning NULL here) + * The async engine is partially or fully uninitialized. Returning + * the handle here would leave the library in a broken state: + * - Callbacks would never fire (no BG thread / no registries) + * - unregisterProcess() would call internal_system_deinit() on + * uninitialized globals (pthread_join on invalid thread = UB) + * So we roll back: best-effort unregister from daemon, free + * handle_str, return NULL to signal failure to the caller. */ FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen ===\n"); if (internal_system_init() != 0) { FWUPMGR_ERROR("rdkFwupdateMgr_lib_init: internal_system_init FAILED\n"); + + /* Best-effort: tell the daemon to drop our registration */ + FWUPMGR_ERROR("Attempting best-effort cleanup: UnregisterProcess(%" PRIu64 ")\n", + handler_id); + GError *cleanup_error = NULL; + GDBusProxy *cleanup_proxy = create_dbus_proxy(&cleanup_error); + if (cleanup_proxy) { + GVariant *cleanup_result = g_dbus_proxy_call_sync( + cleanup_proxy, + "UnregisterProcess", + g_variant_new("(t)", handler_id), + G_DBUS_CALL_FLAGS_NONE, + DBUS_SYNC_TIMEOUT_MS, + NULL, + &cleanup_error + ); + if (cleanup_result) { + FWUPMGR_INFO("Cleanup successful: process unregistered\n"); + g_variant_unref(cleanup_result); + } else { + FWUPMGR_ERROR("Cleanup failed: %s (registration may be leaked)\n", + cleanup_error ? cleanup_error->message : "unknown"); + if (cleanup_error) g_error_free(cleanup_error); + } + g_object_unref(cleanup_proxy); + } else { + FWUPMGR_ERROR("Cleanup proxy creation failed (registration leaked)\n"); + if (cleanup_error) g_error_free(cleanup_error); + } + + free(handle_str); + return NULL; } FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen successful ===\n"); From 32c465aadaf12f8141b12c593e903a69eb453c22 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:07:52 +0530 Subject: [PATCH 22/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 72b1a4b4..a116440a 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -1154,9 +1154,9 @@ void unregisterProcess(FirmwareInterfaceHandle handler) * "dormant" state: no threads, no mutexes, no D-Bus connections. * Only the handle string and handler_id still exist. */ - FWUPMGR_INFO("=== rdkFwupdateMgr destroy thred unloading ===\n"); + FWUPMGR_INFO("=== rdkFwupdateMgr destroy thread unloading ===\n"); internal_system_deinit(); - FWUPMGR_INFO("=== rdkFwupdateMgr destory thread ===\n"); + FWUPMGR_INFO("=== rdkFwupdateMgr destroy thread ===\n"); /* * [STEP 4] CREATE D-BUS PROXY (best-effort) From 0a1f337cb3cb01d79c1f0559826b50021593ca5c Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:11:08 +0530 Subject: [PATCH 23/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index a116440a..968869bd 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -513,7 +513,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib /* D-Bus call failed. Common causes: * - Daemon not running ("The name was not provided by any .service files") * - Daemon crashed during handling - * - 5-second timeout expired (daemon overloaded) + * - 10-second timeout expired (daemon overloaded) * - Daemon explicitly rejected us (process name conflict, D-Bus error reply) * * RESOURCE CLEANUP: From 39c2fdec39dd8c3d5943ef92f0abf75c6c29136a Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:11:37 +0530 Subject: [PATCH 24/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 968869bd..41463a61 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -878,7 +878,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib * - Free check registry (handle_keys + mutex) * After this: 1 thread, 0 mutexes, 0 D-Bus connections * [4] Create ephemeral D-Bus proxy (best-effort) - * [5] Call "UnregisterProcess" on daemon (best-effort, blocks up to 5s) + * [5] Call "UnregisterProcess" on daemon (best-effort, blocks up to 10s) * [6] Extract success boolean from daemon reply * [7] free(handler) -- always, regardless of D-Bus outcome * From 6605d4b20dfc26f1f7deeeecf1c2f95f7c554569 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:12:11 +0530 Subject: [PATCH 25/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- librdkFwupdateMgr/src/rdkFwupdateMgr_process.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 41463a61..a3353f81 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -1267,9 +1267,11 @@ void unregisterProcess(FirmwareInterfaceHandle handler) * restarts, it starts fresh. Our registration is already gone. */ FWUPMGR_WARN("UnregisterProcess D-Bus call failed: %s\n", - error->message); + error ? error->message : "unknown error (GError not set)"); FWUPMGR_WARN(" (This is OK if daemon already cleaned up)\n"); - g_error_free(error); + if (error) { + g_error_free(error); + } g_object_unref(proxy); /* Continue with local cleanup */ free(handler); From 8c23bbcb7db8b392d16f39dc925eee1f33697e67 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:12:50 +0530 Subject: [PATCH 26/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/dbus/rdkv_dbus_server.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dbus/rdkv_dbus_server.c b/src/dbus/rdkv_dbus_server.c index 470aec36..d70bfaa1 100644 --- a/src/dbus/rdkv_dbus_server.c +++ b/src/dbus/rdkv_dbus_server.c @@ -401,12 +401,14 @@ static guint64 add_process_to_tracking(const gchar *process_name, * Called when client invokes UnregisterProcess. Frees associated ProcessInfo. * * @param handler_id Handler ID to remove - * @param sender_id D-Bus sender ID of the requesting client (unused - kept for API compatibility) + * @param sender_id D-Bus sender ID of the requesting client; currently unused but + * retained to keep this internal helper aligned with its caller context and to + * allow future sender-based validation or auditing. * @return TRUE if found and removed, FALSE if not found */ static gboolean remove_process_from_tracking(guint64 handler_id, const gchar *sender_id) { - (void)sender_id; // Not used - single-thread library model creates new D-Bus connection per API call + (void)sender_id; // Intentionally unused for now; reserved for possible sender-aware checks ProcessInfo *info = g_hash_table_lookup(registered_processes, GINT_TO_POINTER(handler_id)); if (!info) { SWLOG_INFO("[PROCESS_TRACKING] Handler %"G_GUINT64_FORMAT" not found\n", handler_id); From adbf97c8a8598109652815041fcb94fd3b77dc8f Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:13:20 +0530 Subject: [PATCH 27/36] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/rdkFwupdateMgr.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/rdkFwupdateMgr.c b/src/rdkFwupdateMgr.c index efb6f097..5b23cdba 100644 --- a/src/rdkFwupdateMgr.c +++ b/src/rdkFwupdateMgr.c @@ -1249,11 +1249,10 @@ int main(int argc, char *argv[]) { * A previous firmware download+flash already completed * (/tmp/fw_preparing_to_reboot was present). * The file has been cleaned up by initialValidation(). - * In the daemon mode, we transition to IDLE and wait * initialValidation() is also responsible for emitting * the MAINT_FWDOWNLOAD_COMPLETE event for this case. - * In the daemon mode, we transition to IDLE and wait - * for the pending reboot or next D-Bus request. + * In daemon mode, we transition to IDLE and wait for + * the pending reboot or the next D-Bus request. */ SWLOG_INFO("Software Update already completed (pending reboot). " "Transitioning to IDLE.\n"); From 9330dc6143dc73ac8ff4b48a69acbe9793638994 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 09:25:34 +0000 Subject: [PATCH 28/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Addressing copilot reviews --- librdkFwupdateMgr/PR_REVIEW_DEFENSE.md | 746 ------------------ .../{ => docs}/DESIGN_DOCUMENT.md | 0 librdkFwupdateMgr/{ => docs}/DOCUMENT_v1.md | 0 .../{ => docs}/VISUAL_DESIGN_DOCUMENT.md | 0 4 files changed, 746 deletions(-) delete mode 100755 librdkFwupdateMgr/PR_REVIEW_DEFENSE.md rename librdkFwupdateMgr/{ => docs}/DESIGN_DOCUMENT.md (100%) rename librdkFwupdateMgr/{ => docs}/DOCUMENT_v1.md (100%) rename librdkFwupdateMgr/{ => docs}/VISUAL_DESIGN_DOCUMENT.md (100%) diff --git a/librdkFwupdateMgr/PR_REVIEW_DEFENSE.md b/librdkFwupdateMgr/PR_REVIEW_DEFENSE.md deleted file mode 100755 index 09d9fa89..00000000 --- a/librdkFwupdateMgr/PR_REVIEW_DEFENSE.md +++ /dev/null @@ -1,746 +0,0 @@ -# librdkFwupdateMgr — Architectural Review Defense - -> **Date**: April 28, 2026 -> **Author**: Original Architect — rdkFwupdateMgr Team -> **Context**: Senior engineering review defense for PR introducing `librdkFwupdateMgr` shared library -> **Tone**: Confident, direct, technically precise — speaking as the designer defending every decision - ---- - -## 1. Why Library + Daemon Split Architecture Is Correct - -The split is not optional — it is the only correct architecture for this system. - -**The firmware update problem has two fundamentally different concerns:** - -| Concern | Characteristic | Who Handles | -|---------|---------------|-------------| -| Client interaction | Short-lived, per-app, needs callbacks | Library | -| Firmware operations | Long-lived, device-wide, privileged | Daemon | - -A firmware download takes 2–5 minutes. A flash operation takes 1–10 minutes. If a client process crashes halfway through a download, the download must continue — the daemon holds that responsibility. If the daemon were embedded in each client, a client crash would abort the download mid-stream, potentially leaving the device in a half-written firmware state. - -**Concrete justification:** - -1. **Process isolation**: Client crash does not corrupt daemon state. Daemon crash does not take down client apps (they detect it via callback timeout and re-register). -2. **Privilege separation**: Only the daemon needs root-level access to flash storage and reboot coordination. Client apps run at lower privilege. -3. **Resource sharing**: One daemon process manages the download cache, XConf session, and HAL interface. N client processes share it via IPC. Without the split, N copies of the download engine would compete for the same HTTP connection and storage. -4. **Lifecycle independence**: The daemon starts at boot via systemd (`rdkFwupdateMgr.service`) and runs indefinitely. Client apps start and stop independently. - -**What the alternative looks like (and why we rejected it):** - -If each client embedded the firmware engine: -- Each would need its own XConf query logic, HTTP download stack, flash HAL binding, and reboot coordinator -- Two clients requesting the same firmware simultaneously would download it twice -- Flash serialization would require cross-process locks (file locks or named semaphores — fragile on embedded Linux) -- A single bug in the firmware engine would need to be patched in every client binary - -The library + daemon split eliminates all of these problems. - ---- - -## 2. Why Not Expose the Daemon Directly to Clients - -"Just let apps call D-Bus directly — why add a library in between?" - -This question comes up in every review. Here is why direct D-Bus access is wrong for this system: - -### 2.1 Protocol Brittleness - -The D-Bus interface uses positional GVariant tuples, not named fields: - -``` -RegisterProcess: (ss) → (t) -CheckForUpdate: (s) → fire-and-forget -DownloadFirmware: (ssss) → fire-and-forget -UpdateFirmware: (sssss) → fire-and-forget - -Signals: - CheckForUpdateComplete: (tiissss) - DownloadProgress: (tsuss) - UpdateProgress: (tsiis) -``` - -One mistyped variant signature (`"(ss)"` instead of `"(si)"`) produces a GLib critical at runtime — silent corruption or crash. No compiler catches this. The library catches it once, in one place, with unit tests covering every signature. - -### 2.2 Signal Subscription Ordering - -If a client sends `CheckForUpdate` before subscribing to `CheckForUpdateComplete`, the response signal is lost forever. The library enforces correct ordering: `internal_system_init()` starts the background thread and subscribes to all three signals **before** `registerProcess()` returns. Every subsequent API call is guaranteed to have a live signal subscription. - -A direct D-Bus client would need to implement this ordering themselves. History shows developers get it wrong — the signal subscription race condition is the #1 bug in ad-hoc D-Bus client code. - -### 2.3 Connection Model Mismatch - -Our library creates an ephemeral D-Bus connection per API call. Each call gets a different unique sender ID (`:1.140`, `:1.141`, etc.). The daemon identifies clients by `handler_id`, not sender address. A developer writing direct D-Bus calls would naturally assume sender-ID stability across calls — and then wonder why the daemon doesn't correlate their requests. - -### 2.4 Data Format Parsing - -The `update_details` field in `CheckForUpdateComplete` is a pipe-separated `Key:Value` string: -``` -FwFileName:firmware_v2.bin|FwUrl:https://cdn.example.com/...|FwVersion:2.0|... -``` - -The library's `parse_update_details()` function handles this parsing, including missing fields, empty values, and malformed input. Every direct client would reimplement this parser — and introduce their own bugs. - -### 2.5 Forward Compatibility - -When we add fields to a signal (e.g., `CheckForUpdateComplete` adds a checksum field), only the library needs updating. All existing clients continue to work with the same API. Without the library, every client binary needs recompilation and redeployment. - ---- - -## 3. Why Shared Library API Abstraction Is Beneficial - -The API is exactly five functions with three callback types. This is the minimum surface that covers the firmware lifecycle: - -```c -FirmwareInterfaceHandle registerProcess(name, version); // Session start -CheckForUpdateResult checkForUpdate(handle, callback); // Query -DownloadResult downloadFirmware(handle, req, cb); // Fetch -UpdateResult updateFirmware(handle, req, cb); // Flash -void unregisterProcess(handle); // Session end -``` - -**Why this is the right abstraction level:** - -1. **Complete**: Covers the entire firmware update lifecycle — no missing operations. -2. **Minimal**: No convenience wrappers, no "download-and-flash" combo call, no polling APIs. Each function does exactly one thing. -3. **Symmetric**: Register/unregister bracket the session. Three async operations share the same pattern (validate → register callback → fire-and-forget D-Bus call → return). -4. **Typed**: Separate enums for each operation's status (`CheckForUpdateStatus`, `DownloadStatus`, `UpdateStatus`). The compiler catches type mismatches. -5. **Discoverable**: A developer reads the header top-to-bottom and knows the entire API in 5 minutes. No inheritance hierarchies, no vtables, no builder patterns. - -**What we deliberately excluded:** - -- `cancelDownload()` — not yet implemented in daemon; adding a no-op would be misleading -- `getStatus()` synchronous poll — encourages busy-waiting; callbacks are the correct model -- `downloadAndUpdate()` combo — couples two independent operations; client should control the flow between them - ---- - -## 4. Thread Safety Decisions and Justification - -### 4.1 Three Separate Registries, Three Separate Mutexes - -``` -g_registry (check) → g_registry.mutex -g_dwnl_registry (download) → g_dwnl_registry.mutex -g_update_registry (update) → g_update_registry.mutex -``` - -**Why not one global mutex?** Because operations are independent. A download signal arriving while a check callback is registering should not block. Separate mutexes maximize concurrency with zero additional complexity. - -**Why not per-slot locks?** 30 slots × 3 registries = 90 mutexes. The initialization, cleanup, and deadlock analysis complexity is not justified. The critical sections are sub-microsecond (array scan + pointer copy). Contention is effectively zero — firmware operations happen at human timescales (seconds to minutes), not microsecond intervals. - -### 4.2 Two-Phase Dispatch - -This is the most important thread-safety decision in the library. The `dispatch_all_pending()` function uses a two-phase approach: - -``` -Phase 1 (mutex held): Snapshot PENDING entries into stack-local array - Mark entries DISPATCHED - Release mutex - -Phase 2 (no mutex): Invoke each callback from snapshot - Callbacks can safely call checkForUpdate() etc. - Re-acquire mutex briefly to reset each slot to IDLE -``` - -**Why this pattern exists:** If we held the mutex while invoking callbacks, and a callback called `checkForUpdate()` (which calls `internal_register_callback()` which locks the same mutex) — deadlock. The two-phase design makes re-entrant library use safe without requiring recursive mutexes (which have their own pitfalls — forgetting to release the correct number of times). - -### 4.3 Isolated GMainContext - -The background thread creates its own `GMainContext`: -```c -g_bg_thread.context = g_main_context_new(); -g_bg_thread.main_loop = g_main_loop_new(g_bg_thread.context, FALSE); -g_main_context_push_thread_default(g_bg_thread.context); -``` - -**Why not use the default context?** If the client app runs its own GLib main loop (GTK application, WebKit service), our signal subscriptions would fire in the app's main thread — breaking the app's event model and causing thread-safety violations in the app's own code. Isolated context guarantees our signals fire in our thread, never in the app's. - -### 4.4 Spin-Wait at Initialization - -```c -for (int i = 0; i < 50; i++) { // Max 5 seconds - if (g_bg_thread.running) break; - nanosleep(100ms); -} -``` - -**Why spin-wait instead of a condition variable?** This runs exactly once per library session — during `registerProcess()`. A condvar would be more elegant but adds a third synchronization primitive to the initialization path. The spin-wait is bounded (5 seconds max), runs on the main thread which is already blocking on `registerProcess()`, and is dead-simple to audit. - ---- - -## 5. Why Chosen Logging Design Helps Production Debugging - -### 5.1 Module Separation - -Three distinct log modules allow filtering in production: - -| Module | Prefix | Source | -|--------|--------|--------| -| `LOG.RDK.FWUPMGR` | `[FWUPMGR]` | Library internals | -| `LOG.RDK.FWUPG` | `[FWUPG]` | Daemon operations | -| `LOG.RDK.EXAMPLE` | `[EXAMPLE]` | Example/reference app | - -In production, an operator can set `LOG.RDK.FWUPMGR` to DEBUG while keeping daemon logs at INFO. This isolates library behavior without drowning in daemon verbosity. - -### 5.2 handler_id as Correlation Key - -Every log message in the firmware update path includes the `handler_id`. This allows end-to-end request tracing: - -``` -[FWUPMGR] INFO: registerProcess() → handler_id=12345 -[FWUPMGR] INFO: checkForUpdate, handle=12345 -[FWUPG] INFO: CheckForUpdate for handler_id=12345, querying XConf -[FWUPG] INFO: CheckForUpdate result for 12345: AVAILABLE -[FWUPMGR] INFO: signal received, handler_id=12345 -[FWUPMGR] INFO: dispatching 1 callback(s) for handler_id=12345 -``` - -With `grep 12345 /opt/logs/rdkFwupdateMgr.log`, a field engineer can trace a single client's entire firmware update journey across library and daemon boundaries. - -### 5.3 Non-RDK_LOGGER Fallback - -```c -#ifdef RDK_LOGGER -#define FWUPMGR_LOG(level, module, FORMAT...) RDK_LOG(level, module, FORMAT) -#else -#define FWUPMGR_LOG(level, module, FORMAT...) fprintf(stderr, "[%s] " FORMAT, module) -#endif -``` - -Unit tests run without `RDK_LOGGER` installed. The `fprintf` fallback means every test still emits visible log output — no silent failures. This was a deliberate choice: we will never have a test where a log macro silently expands to nothing. - -### 5.4 Library Does Not Own Log Lifecycle - -`log_init()` and `log_exit()` are the caller's responsibility. The library never calls them. This prevents double-initialization when multiple libraries are loaded into the same process. - ---- - -## 6. IPC Design Tradeoffs - -### 6.1 D-Bus System Bus (Chosen) - -| Pro | Con | -|-----|-----| -| Standard Linux IPC — every RDK device has it | Overhead per message (~50μs) | -| Policy-based access control via conf files | GLib dependency | -| Signal broadcast — multi-client without daemon code changes | GVariant parsing boilerplate | -| Well-debugged (dbus-monitor for field diagnosis) | Message size limits (128MB default) | - -### 6.2 Alternatives Considered and Rejected - -**Unix domain sockets (raw):** Lower overhead, but we'd need to implement our own protocol framing, serialization, signal broadcast, and access control. Reinventing D-Bus poorly. - -**Shared memory + semaphores:** Lowest latency, but firmware operations are I/O-bound (network downloads, flash writes). Microsecond IPC savings are irrelevant when the operation takes minutes. The synchronization complexity (readers/writers, cleanup on crash) is not justified. - -**gRPC / Protocol Buffers:** Not available on target embedded Linux platform. Would add 15MB+ of runtime dependencies. - -**rbus (RDK Bus):** The codebase has `rbusInterface/` support. D-Bus was chosen because the daemon's existing implementation uses D-Bus, and dual-transport support would double the test surface for zero benefit. - -### 6.3 Ephemeral Connection Model - -Each API call creates and destroys its own D-Bus connection: - -``` -registerProcess(): [connect → call → disconnect] sender :1.140 -checkForUpdate(): [connect → call → disconnect] sender :1.141 -downloadFirmware(): [connect → call → disconnect] sender :1.142 -``` - -**Why not a persistent connection?** - -1. No connection lifecycle management needed — no reconnection logic, no heartbeats, no stale connection detection -2. Each call is fully self-contained — if D-Bus dies between calls, the next call discovers it immediately -3. The daemon uses `handler_id` for client identity, not D-Bus sender address — so sender instability is irrelevant -4. Firmware operations happen at most once per hour on a production device — the 5ms connection overhead is negligible - -**The background thread has a persistent connection** for signal subscription — this is correct because signal delivery requires a stable subscription, and the overhead is exactly one connection for the library's lifetime. - ---- - -## 7. Memory Management Rationale - -### 7.1 Ownership Rules - -| Resource | Allocator | Deallocator | Trigger | -|----------|-----------|-------------|---------| -| Handle string | `malloc(32)` in `registerProcess()` | `free()` in `unregisterProcess()` | Always, even on D-Bus failure | -| Registry `handle_key` | `strdup()` in `internal_register_callback()` | `free()` in `registry_reset_slot()` | Slot reset to IDLE | -| `FwInfoData` + `UpdateDetails` | Stack allocation in `dispatch_all_pending()` | Automatic (stack unwind) | Function return | -| GDBusProxy, GDBusConnection | `g_dbus_proxy_new_sync()` | `g_object_unref()` | Same function, all code paths | -| `InternalSignalData` strings | `strdup()` in parse | `free()` in `internal_cleanup_signal_data()` | After dispatch completes | - -### 7.2 Why Stack Allocation for Callback Data - -`FwInfoData` and `UpdateDetails` are allocated on the background thread's stack in `dispatch_all_pending()`. This means: - -1. **No malloc/free pairing to get wrong** — allocation and deallocation are automatic -2. **No ownership ambiguity** — the data is valid only during the callback, and this is documented in the header and enforced by stack scoping -3. **Cache-friendly** — stack is always in L1 cache on the background thread -4. **Stack usage: ~1KB** — well within the 8MB default thread stack - -The tradeoff is that clients must copy data they need. This is documented explicitly: - -> *"The pointer and strings inside are only valid during this callback. If you need the data later, copy it with strdup()"* — `rdkFwupdateMgr_client.h` - -This is the same pattern used by `getaddrinfo()`, `readdir()`, and every GLib signal handler. C developers expect it. - -### 7.3 Handle Allocation: Why malloc(32)? - -The handle is a decimal string representation of a `uint64_t`. Maximum value: `18446744073709551615` (20 digits). With null terminator: 21 bytes. We allocate 32 bytes for comfortable alignment and future-proofing (if the handle format ever includes a prefix). - -`snprintf(handle_str, 32, "%" PRIu64, handler_id)` — bounds-checked, null-terminated, no overflow possible. - -### 7.4 Best-Effort Cleanup in Error Paths - -The `registerProcess()` malloc failure path demonstrates our cleanup philosophy: - -```c -if (!handle_str) { - // Registration succeeded on daemon but we can't return handle. - // Must unregister to prevent resource leak on daemon side. - GDBusProxy *cleanup_proxy = create_dbus_proxy(&cleanup_error); - if (cleanup_proxy) { - g_dbus_proxy_call_sync(cleanup_proxy, "UnregisterProcess", ...); - } - return NULL; -} -``` - -We never silently leak daemon-side resources. If we can't clean up (proxy creation fails too), we log it explicitly so field engineers can diagnose. - ---- - -## 8. Failure Handling Strategy - -### 8.1 Fail-Fast at Boundaries - -Every public API validates all inputs before touching D-Bus or shared state: - -```c -if (!processName) → FWUPMGR_ERROR, return NULL -if (strlen(processName) == 0) → FWUPMGR_ERROR, return NULL -if (strlen(processName) > MAX) → FWUPMGR_ERROR, return NULL -``` - -No D-Bus connection is created, no registry slot is allocated, no mutex is locked — until validation passes. This prevents partial state pollution on invalid input. - -### 8.2 Error Propagation Model - -| Layer | Error Handling | -|-------|---------------| -| Public API | Returns error code (`NULL`, `FAIL`, etc.) + logs specific error | -| D-Bus transport | GLib `GError` captured, message logged, error freed, propagated as API-level failure | -| Background thread | Signal parse failure logged, callback not dispatched (client times out) | -| Unregister | Best-effort — errors logged as WARN, cleanup continues regardless | - -### 8.3 No Silent Failures - -Every error path logs before returning. There is no code path where the library returns an error without first logging exactly what went wrong. This was a deliberate coding standard enforced throughout the implementation. - -### 8.4 Unregister Is Tolerant - -`unregisterProcess()` is deliberately forgiving: -- NULL handle → no-op (logged at INFO) -- Invalid handle format → log error, `free(handle)`, return -- D-Bus proxy creation fails → log WARN, `free(handle)`, return -- D-Bus call fails → log WARN, `free(handle)`, return -- Daemon reports failure → log WARN, `free(handle)`, return - -The handle is **always freed**. Local resources are **always cleaned up**. The daemon can be down, crashed, or restarted — `unregisterProcess()` will not block or leak. - ---- - -## 9. Why Return Code Model Was Selected - -### 9.1 Two-Tier Result Model - -The API uses a two-tier result model: - -**Tier 1 (Synchronous):** Did the request succeed in being sent? -```c -CheckForUpdateResult rc = checkForUpdate(handle, callback); -if (rc == CHECK_FOR_UPDATE_FAIL) { /* Request not sent — no callback coming */ } -``` - -**Tier 2 (Asynchronous):** What was the actual firmware result? -```c -void my_callback(const FwInfoData *fwinfo) { - if (fwinfo->status == FIRMWARE_AVAILABLE) { /* New firmware exists */ } -} -``` - -### 9.2 Why Not Combine into One? - -Because the caller needs different error handling for each tier: - -- Tier 1 failure (FAIL): Retry immediately — daemon may not be running, D-Bus may be congested -- Tier 2 failure (ERROR status in callback): Application-level decision — firmware not available, network down, etc. - -Combining them would force the caller to handle transport errors and application errors in the same callback, complicating control flow. - -### 9.3 Why Separate Enums Per Operation? - -`CheckForUpdateResult`, `DownloadResult`, `UpdateResult` are separate enums, not a single `FwResult`. This is deliberate: - -1. **Type safety**: The compiler rejects `if (download_result == CHECK_FOR_UPDATE_SUCCESS)` — wrong enum type -2. **Clarity**: Each enum has exactly the values that operation can return — no "unused for this API" values -3. **Extensibility**: Adding `RDKFW_DWNL_PAUSED` to `DownloadResult` doesn't affect `CheckForUpdateResult` - -### 9.4 Why Not errno-Style? - -Setting a global `errno` would be thread-unsafe without TLS. The return-code-per-call model is the standard C pattern for thread-safe libraries (OpenSSL, libcurl, zlib all use it). - ---- - -## 10. How Design Supports Future APIs - -### 10.1 New Operations - -Adding a new operation (e.g., `cancelDownload()`) requires: - -1. New public function in header → `CancelResult cancelDownload(handle)` -2. New D-Bus method name in constants → `#define DBUS_METHOD_CANCEL "CancelDownload"` -3. New D-Bus call in API layer → same pattern as existing fire-and-forget calls - -No changes to: registries, background thread, signal handling, or existing APIs. - -### 10.2 New Signals - -The background thread already demonstrates the subscription pattern. Adding a fourth signal subscription is: one `g_dbus_connection_signal_subscribe()` call + one signal handler function + one registry (if it needs callbacks). - -### 10.3 New Callback Parameters - -The `UpdateCallback` header explicitly documents: - -> *"The signature and behavior of this callback may change in future versions when HAL APIs become available."* - -This sets expectation with clients. When HAL is integrated, we bump the major version and update the callback signature. - -### 10.4 New Transport - -The IPC layer is isolated behind `create_dbus_proxy()` and the fire-and-forget pattern. Switching from D-Bus to rbus would require changing the transport functions without touching the public API, the registries, or the dispatch logic. - ---- - -## 11. Why Implementation Is Maintainable - -### 11.1 Module Separation - -``` -rdkFwupdateMgr_process.c — registerProcess(), unregisterProcess() -rdkFwupdateMgr_api.c — checkForUpdate(), downloadFirmware(), updateFirmware() -rdkFwupdateMgr_async.c — Registries, background thread, signal handlers, dispatch -rdkFwupdateMgr_log.h — Logging macros (header-only) -rdkFwupdateMgr_async_internal.h — Internal types (never exposed to clients) -rdkFwupdateMgr_client.h — Public API (the ONLY file clients include) -``` - -A developer fixing a download bug reads exactly two files: `_api.c` (API entry point) and `_async.c` (dispatch logic). They never need to understand registration logic in `_process.c`. - -### 11.2 Consistent Patterns - -Every async API follows the same structure: -``` -1. Log entry -2. Validate inputs -3. Connect to D-Bus -4. Register callback in registry -5. Fire-and-forget D-Bus call -6. Unref connection -7. Log exit -8. Return SUCCESS -``` - -This is not accidental. It's a deliberate template. A developer who understands `checkForUpdate()` understands `downloadFirmware()` and `updateFirmware()` — they are structurally identical with different parameter lists. - -### 11.3 No Clever Code - -There are no macros that generate functions, no varargs tricks, no `__attribute__((cleanup))` magic, no `setjmp/longjmp` error handling. Every function is readable straight through. The most complex construct is the two-phase dispatch, and it has a 15-line comment explaining exactly why it exists. - ---- - -## 12. Why Code Is Junior-Friendly - -### 12.1 Header Documentation - -Every struct, enum, callback, and function in `rdkFwupdateMgr_client.h` has a plain-English comment: - -```c -/** - * FirmwareInterfaceHandle - * - * This is a string ID that the daemon gives you when you register. - * Think of it like a session ID or ticket number (e.g., "12345"). - */ -``` - -No jargon. No references to GLib internals. A developer with 6 months of C experience can read this header and write a client. - -### 12.2 Example App - -`examples/example_app.c` is a complete, working reference implementation. It demonstrates: -- Log initialization -- Registration -- Check → Download → Update flow -- Callback implementation with condvar synchronization -- Cleanup in all error paths - -A junior developer copies this file, changes the process name, and has a working firmware updater. - -### 12.3 Explicit Warnings in Comments - -```c -// WARNING: This operation modifies device firmware. It is irreversible once the flash begins. -// Don't call other library functions from inside this callback -// This runs in a background thread, not your main thread -``` - -The comments don't assume the reader knows threading, D-Bus, or firmware update semantics. - -### 12.4 Defensive NULL Handling - -`unregisterProcess(NULL)` is a no-op, not a crash. This forgives the common pattern: - -```c -cleanup: - unregisterProcess(handle); // Safe even if registerProcess() failed -``` - ---- - -## 13. Security Posture of the Design - -### 13.1 D-Bus Policy Enforcement - -Access to the daemon is controlled by D-Bus system bus policy: -```xml - - - -``` -Unprivileged processes cannot send method calls or receive signals. This is enforced by dbus-daemon, not by our code — correct separation of concerns. - -### 13.2 Input Validation at Every Boundary - -All public API inputs are validated before any IPC: -- NULL checks on all pointer parameters -- Empty string rejection on required fields -- Length limits on all strings (preventing buffer overflow in daemon's fixed-size buffers) -- Strict numeric parsing in `unregisterProcess()` with `strtoull()` + `endptr` validation - -### 13.3 No `sprintf()` Anywhere - -Every string format in the library uses `snprintf()` with explicit bounds. The `strncpy()` calls in `parse_update_details()` explicitly null-terminate. - -### 13.4 handler_id Limitations (Documented) - -The `handler_id` is a sequential counter, not a cryptographically random token. In the current deployment model (single device, trusted clients), this is acceptable. The design document explicitly flags this: - -> *"For production hardening, consider using a random 128-bit token."* - -We are transparent about the limitation rather than pretending it doesn't exist. - -### 13.5 No Credential Storage - -The library stores no passwords, tokens, certificates, or API keys. Authentication is entirely handled by D-Bus policy. - ---- - -## 14. Operational Reliability Benefits - -### 14.1 Graceful Degradation - -| Failure | Library Behavior | Client Impact | -|---------|-----------------|---------------| -| Daemon not running | `registerProcess()` returns NULL | Client knows immediately | -| Daemon crashes mid-download | No more progress signals arrive | Client's condvar times out | -| D-Bus bus restart | Next API call fails at connection | Client unregisters + re-registers | -| Client crash | Daemon detects name disappearance (best-effort) | No impact on other clients | -| Library background thread fails | No signals dispatched | Client times out on all callbacks | - -### 14.2 No Hung States - -Every blocking operation has a bounded timeout: -- D-Bus calls: 5000ms (`DBUS_TIMEOUT_MS`) -- Background thread startup: 5000ms (50 × 100ms spin-wait) -- Client callbacks: bounded by client's own `pthread_cond_timedwait()` - -There is no code path where the library blocks indefinitely. - -### 14.3 Clean Shutdown - -`unregisterProcess()` performs ordered cleanup: -1. Stop background thread (quit loop → join) -2. Free all registry entries -3. Destroy all mutexes -4. Best-effort daemon notification -5. Free handle - -Even if the daemon is unreachable, steps 1–3 and 5 always complete. No resources leak on the client side. - -### 14.4 Idempotent Cleanup - -Calling `unregisterProcess(NULL)` is safe. Calling it after the daemon has already cleaned up the registration is safe (logged as WARN, not an error). This means crash handlers and `atexit()` hooks can call it unconditionally. - ---- - -## Reviewer Likely Questions - -### Q1: Why use global static state for registries instead of instance-based allocation? - -**Answer:** The library manages exactly one daemon session per process. There is no use case for two simultaneous sessions — the daemon assigns one `handler_id` per process name. Instance-based allocation (passing a context pointer through every function) would add a parameter to every API call, complicate every internal function, and solve zero real problems. The registries are encapsulated in `_async.c` as `static` — they are not exposed to the client and not accessible outside the compilation unit. This is the idiomatic C approach for module-private state (used by `malloc` internals, `stdio`, `errno`, etc.). - -### Q2: Why not use a condition variable instead of spin-waiting for background thread startup? - -**Answer:** The spin-wait runs exactly once per session — during `registerProcess()`. It waits a maximum of 5 seconds with 100ms sleep intervals between checks. A condvar would save ~50μs of cumulative spin time over 50 iterations — in a function that already does a synchronous D-Bus round-trip (5-10ms). The engineering complexity of adding a condvar (init, signal, wait, destroy, error handling) is not justified for a one-time initialization path that the user is already blocking on. - -### Q3: What happens if the daemon crashes while a download is in progress? - -**Answer:** The download callback stops receiving progress signals. The client's `pthread_cond_timedwait()` expires after its configured timeout (recommended: 300 seconds for downloads). The client then calls `unregisterProcess()`, which performs local cleanup (thread join, registry free, mutex destroy) regardless of daemon availability. The D-Bus `UnregisterProcess` call fails with a timeout — logged as WARN, cleanup continues. The client can then re-register and retry. No resources leak. No hung threads. - -### Q4: Why fire-and-forget for async operations instead of waiting for a D-Bus reply? - -**Answer:** The daemon operations take seconds to minutes (XConf query: 1-15s, download: 30-300s, flash: 60-600s). Blocking the caller's thread for the duration defeats the async model. The D-Bus reply would only confirm "I received your request" — the actual result comes as a signal. We skip the synchronous receipt confirmation and let the signal be the sole result channel. If the request fails to even reach the daemon (D-Bus bus down), the `g_dbus_connection_call()` itself fails immediately and the API returns FAIL. - -### Q5: Why MAX_PENDING_CALLBACKS = 30? Is that enough? - -**Answer:** 30 is the maximum number of concurrent pending callbacks per registry (check, download, update). On a single device, there are at most 3-5 client applications making firmware requests. Even in a stress test with 30 concurrent `checkForUpdate()` calls from the same process, 30 slots suffice. The value was reduced from 64 to 30 specifically to keep the stack-local snapshot array in `dispatch_all_pending()` under 10KB (Coverity flagged the original 64-slot version for excessive stack usage). If a use case requires more, bumping the constant is a one-line change. - -### Q6: Why no versioned API (v1, v2)? - -**Answer:** The library version is embedded in `LIB_VERSION "1.0.0"` and passed to the daemon during registration. ABI compatibility is maintained through the shared library versioning mechanism (libtool `SONAME`). We don't need URL-style version prefixes (`v1_registerProcess`) because the library is a binary artifact, not a REST API. When breaking changes are needed, the SO major version bumps, old clients link against the old `.so`, and new clients link against the new one. Standard practice for C shared libraries. - -### Q7: Why separate mutexes per registry instead of one global lock? - -**Answer:** Independence. A download progress signal arriving while a check callback is being registered should not block. The three operations have no shared data. Separate mutexes allow full concurrency between check, download, and update paths. The cost is 3 `pthread_mutex_t` instances (~120 bytes total on Linux) — negligible. - -### Q8: Why doesn't the library retry failed D-Bus calls? - -**Answer:** Retry policy belongs to the caller, not the transport. A monitoring daemon might retry every 60 seconds indefinitely. A user-facing app might retry once after 5 seconds then show an error. The library cannot know the right policy. Embedding retry logic would also complicate the error model — does "FAIL" mean "failed after 3 retries" or "didn't try"? By not retrying, the error code has a clear, deterministic meaning: this call, right now, failed. - -### Q9: What prevents a malicious client from guessing another client's handler_id? - -**Answer:** The `handler_id` is a sequential uint64 counter — it is guessable. In the current deployment model (single device, all processes running as root or a dedicated service account, D-Bus policy restricting access), this is acceptable. The daemon validates that the `handler_id` exists in its registration table before processing any request, preventing random probing. For multi-tenant or security-hardened deployments, the design document explicitly recommends upgrading to cryptographically random 128-bit tokens. This is a future improvement, not a current vulnerability in the deployment context. - -### Q10: Why does unregisterProcess() call internal_system_deinit() BEFORE the D-Bus unregister call? - -**Answer:** After sending `UnregisterProcess` to the daemon, the daemon stops sending signals for this client. If we sent the D-Bus call first and then tried to join the background thread, the thread might be blocked in `g_main_loop_run()` waiting for signals that will never come — until the loop is explicitly quit. By calling `internal_system_deinit()` first, we quit the loop and join the thread immediately. The subsequent D-Bus call to the daemon is then a pure notification — "I'm already gone, clean up your side." - -### Q11: Why is the handle a string ("12345") instead of an opaque struct pointer? - -**Answer:** Because it crosses a D-Bus boundary. The handle is the `handler_id` the daemon assigned — a uint64. We encode it as a decimal string so it can be: (a) passed back to the daemon in D-Bus method calls (which expect string arguments for most operations), (b) logged without format specifier portability issues, (c) compared with `strcmp()` in the registry without type-punning. An opaque pointer would require a lookup table mapping pointers to handler_ids, adding complexity for zero benefit. - -### Q12: What if a signal arrives between callback registration and D-Bus call? - -**Answer:** Cannot happen. The ordering is: register callback → send D-Bus call. The daemon only emits the response signal after receiving the D-Bus call. Since the callback is already registered before the call is sent, the signal will always find a matching registry entry. This ordering is enforced in the API layer (`_api.c`), not left to the caller. - -### Q13: Why not use GCancellable for timeouts? - -**Answer:** `GCancellable` is designed for cancelling in-flight GIO operations. Our async operations are fire-and-forget — there is no in-flight operation to cancel after the `g_dbus_connection_call()` returns. The timeout is entirely on the client side (condvar timedwait), which is outside GLib's control. Using `GCancellable` would add a GLib object lifecycle with no functional benefit. - -### Q14: Why strdup() the handle_key in the registry instead of keeping a pointer? - -**Answer:** The handle string is owned by the caller's scope. Between the time we register the callback and the time the signal arrives, the caller may have passed the handle to another function, stored it in a struct, or (if buggy) freed it. By `strdup()`-ing the handle into the registry, the registry owns its own copy with a guaranteed lifetime. The 20-byte allocation cost per registration is negligible. - -### Q15: Why no timeout sweeper thread to clean up stale PENDING entries? - -**Answer:** The `TIMED_OUT` state and `registered_time` field exist in the design, but no sweeper is implemented. This is deliberate. The current cleanup model is: client times out → client calls `unregisterProcess()` → all entries freed. A sweeper thread would add a fourth thread, a timer mechanism (`g_timeout_add`), and complex questions about what to do with swept entries (invoke callback with error? silently discard?). For the current use case (single-digit concurrent operations, client-managed timeouts), the sweeper adds complexity without solving a real problem. It's listed as future work in the design document. - -### Q16: The background thread's signal subscriptions use sender=NULL. Doesn't this accept signals from any process? - -**Answer:** Yes, and this is correct. D-Bus signals are broadcast — the bus delivers them to all subscribed clients regardless of sender. Filtering by sender would require knowing the daemon's unique bus name (`:1.42`), which changes on every daemon restart. Using `sender=NULL` with interface and object path filtering is the standard D-Bus pattern. D-Bus policy files restrict which processes can emit signals on our interface — this is the correct enforcement layer. - -### Q17: Why does parse_update_details() use pipe-delimited strings instead of structured GVariant? - -**Answer:** The pipe-delimited `Key:Value` format is the daemon's existing wire protocol. We did not design it — we consume it. The library's job is to parse what the daemon sends. Changing the daemon's signal format is out of scope for this PR. The `parse_update_details()` function encapsulates this ugly parsing so no client ever sees it. - -### Q18: The download registry keeps slots ACTIVE across multiple signals, but check registry resets to IDLE after one dispatch. Why the inconsistency? - -**Answer:** It's not an inconsistency — it reflects fundamentally different signal semantics. `CheckForUpdateComplete` fires **once** — the check is done. `DownloadProgress` fires **repeatedly** (0%, 25%, 50%, 75%, 100%). If the download slot reset to IDLE after the first 0% signal, all subsequent progress signals would be silently dropped. The lifecycle difference (one-shot vs. streaming) mandates different slot management. This is explicitly documented in the internal header: - -> *"CheckForUpdate registry: slot goes PENDING → DISPATCHED → IDLE (fires ONCE)"* -> *"Download registry: slot stays ACTIVE until DWNL_COMPLETED or DWNL_ERROR (fires MULTIPLE TIMES)"* - -### Q19: Why no unit test mocks for D-Bus in the library's own unit tests? - -**Answer:** The `unittest/` directory contains GTest-based tests that mock D-Bus at the function level using fake implementations (`test_dbus_fake.c`). The library's async engine can be tested by directly calling `internal_register_callback()` and simulating signal delivery via `dispatch_all_pending()` with synthetic `InternalSignalData`. D-Bus is abstracted behind `create_dbus_proxy()` and `g_dbus_connection_call()` — both are mockable without a running dbus-daemon. - -### Q20: What if two processes register with the same processName? - -**Answer:** The daemon enforces one registration per process name. The second `RegisterProcess("MyPlugin", "1.0")` call will either return the existing `handler_id` (idempotent registration) or return an error (duplicate rejection) — this is a daemon policy decision. The library faithfully returns whatever the daemon provides: a valid handle or NULL with the error message logged. - -### Q21: Why not use atomic operations instead of mutexes for the registry? - -**Answer:** The registry operations are not single-word reads/writes. Registration involves: scan array → check existing → allocate string → update multiple fields. Dispatch involves: scan array → copy N entries → update N states. These are multi-step operations that cannot be expressed as atomic CAS operations without a lock-free data structure (which would be significantly more complex and harder to audit). The mutex critical sections are sub-microsecond — there is no performance bottleneck to optimize. - -### Q22: How does this library handle being loaded by a multi-threaded app that already uses GLib? - -**Answer:** The library creates its own `GMainContext` and pushes it as the thread-default for the background thread. This isolates our D-Bus signal handling from the app's GLib event loop. The app can run `gtk_main()`, `g_main_loop_run()`, or any other GLib loop on its own threads without interference. This is the documented GLib pattern for library-owned event loops. - -### Q23: Why does the library not check if the daemon version is compatible? - -**Answer:** The library passes `libVersion` to the daemon during registration. The daemon can reject incompatible versions by returning an error on the `RegisterProcess` call. Version compatibility enforcement is the daemon's responsibility — it knows which library versions it supports. The library's job is to report its version honestly and handle rejection gracefully (return NULL). - -### Q24: The CALLBACK_TIMEOUT_SECONDS constant (60s) is defined but never used. Dead code? - -**Answer:** Not dead code — it's planned infrastructure. The `registered_time` field in every registry entry records when the callback was registered. The timeout constant exists for a future sweeper that will clean stale entries. We ship the constant and the timestamp now so that enabling the sweeper later is a one-function addition, not a data model change. Removing them would save 8 bytes per entry and one `#define` — not worth the cost of re-adding them later. - -### Q25: Why not use function pointers in a vtable for the three API operations instead of three separate functions? - -**Answer:** Because the three operations have different signatures: - -```c -CheckForUpdateResult checkForUpdate(handle, UpdateEventCallback); -DownloadResult downloadFirmware(handle, FwDwnlReq*, DownloadCallback); -UpdateResult updateFirmware(handle, FwUpdateReq*, UpdateCallback); -``` - -Different parameter types, different return types, different callback signatures. A vtable would require casting to `void*` parameters and `int` returns — losing type safety for zero structural benefit. The three separate functions are explicit, typed, and impossible to call incorrectly. - ---- - -## Suggested PR Summary Comment - ---- - -**PR: Introduce `librdkFwupdateMgr` — Client Library for Firmware Update Daemon** - -This PR adds `librdkFwupdateMgr.so`, a shared C library that provides client applications a clean, thread-safe API for firmware lifecycle management via the `rdkFwupdateMgr` daemon. - -**What it does:** -- 5 public functions: `registerProcess`, `checkForUpdate`, `downloadFirmware`, `updateFirmware`, `unregisterProcess` -- 3 callback types for async result delivery (check, download progress, update progress) -- Single background thread with isolated GLib event loop for D-Bus signal reception -- Three mutex-protected callback registries with two-phase dispatch (deadlock-free) -- Ephemeral D-Bus connections per API call; persistent connection for signal subscription -- Comprehensive input validation at every public API boundary -- Structured logging under `LOG.RDK.FWUPMGR` module with handler_id correlation - -**What it does NOT do:** -- No firmware downloads, flash operations, or reboots (daemon's responsibility) -- No retry logic (caller's responsibility — different apps need different retry policies) -- No log lifecycle management (`log_init`/`log_exit` are caller's responsibility) - -**Key design decisions:** -- Library + daemon split for process isolation, privilege separation, and resource sharing -- Async fire-and-forget API — operations return immediately, results come via callbacks -- Stack-allocated callback data (valid only during callback) — zero heap allocation in the hot path -- Two-phase dispatch prevents deadlock when callbacks re-enter the library -- Best-effort cleanup in `unregisterProcess()` — never blocks, never leaks local resources - -**Files added:** -- `librdkFwupdateMgr/include/rdkFwupdateMgr_client.h` — Public API header -- `librdkFwupdateMgr/src/rdkFwupdateMgr_process.c` — Registration/unregistration -- `librdkFwupdateMgr/src/rdkFwupdateMgr_api.c` — Async API entry points -- `librdkFwupdateMgr/src/rdkFwupdateMgr_async.c` — Internal engine (registries, thread, dispatch) -- `librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h` — Internal types -- `librdkFwupdateMgr/src/rdkFwupdateMgr_log.h` — Logging macros -- `librdkFwupdateMgr/examples/example_app.c` — Reference client implementation - -**Testing:** Unit tests in `unittest/` cover registration flow, callback dispatch, error paths, and mock D-Bus interactions. - ---- - -*End of Review Defense Document* diff --git a/librdkFwupdateMgr/DESIGN_DOCUMENT.md b/librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md similarity index 100% rename from librdkFwupdateMgr/DESIGN_DOCUMENT.md rename to librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md diff --git a/librdkFwupdateMgr/DOCUMENT_v1.md b/librdkFwupdateMgr/docs/DOCUMENT_v1.md similarity index 100% rename from librdkFwupdateMgr/DOCUMENT_v1.md rename to librdkFwupdateMgr/docs/DOCUMENT_v1.md diff --git a/librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md b/librdkFwupdateMgr/docs/VISUAL_DESIGN_DOCUMENT.md similarity index 100% rename from librdkFwupdateMgr/VISUAL_DESIGN_DOCUMENT.md rename to librdkFwupdateMgr/docs/VISUAL_DESIGN_DOCUMENT.md From 970cb261d3a3b0525971589931e6f917a802ce57 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 09:34:40 +0000 Subject: [PATCH 29/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Addressing copilot reviews --- .../docs/librdkFwupdateMgr_CodeReview.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100755 librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md diff --git a/librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md b/librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md new file mode 100755 index 00000000..f3a8bc12 --- /dev/null +++ b/librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md @@ -0,0 +1,125 @@ +# librdkFwupdateMgr.so Critical Design Review + +**Date**: April 29, 2026 +**Scope**: `orig_rdkfwupdater/librdkFwupdateMgr/src/` — async engine, process management, and public API +**Reviewer**: Senior Engineering (automated critical review) + +--- + +## Executive Summary + +* **Overall risk level**: Moderate. The library is well-structured with correct use of two-phase dispatch, proper mutex discipline, and thorough input validation. However, it has 3 genuine issues that warrant attention. +* **Production readiness status**: Conditionally ready. No crash-class bugs found in the normal operational path (registerProcess → checkForUpdate → downloadFirmware → updateFirmware → unregisterProcess). The issues found affect edge cases and shutdown paths. +* **Top 3 immediate concerns**: + 1. Download and update D-Bus signal subscriptions are **never unsubscribed** during BG thread shutdown — resource leak + 2. `internal_system_init()` has incomplete rollback on late-stage failure (dwnl/update mutex init) — leaves BG thread and g_registry leaked + 3. `strdup()` return values in all 3 `internal_*_register_callback()` functions are never NULL-checked — Coverity-class defect +* **Immediate remediation required**: No. None of these cause crashes in production's happy path. But Finding #1 and #3 should be fixed before the next release. + +--- + +## Findings + +### Finding 1: DownloadProgress and UpdateProgress signal subscriptions never unsubscribed + +**Severity**: Major +**Category**: Lifecycle / Memory +**Priority**: Next Release + +**Why this is dangerous in production:** +When the BG thread shuts down (after `g_main_loop_quit()`), it only unsubscribes `g_bg_thread.subscription_id` (the CheckForUpdateComplete subscription). The `dwnl_sub_id` and `update_sub_id` returned by `g_dbus_connection_signal_subscribe()` on lines 279 and 293 of `rdkFwupdateMgr_async.c` are local variables that go out of scope. They are never stored in `g_bg_thread` and never passed to `g_dbus_connection_signal_unsubscribe()`. + +**Realistic failure scenario:** +When `unregisterProcess()` is called, the BG thread exits `g_main_loop_run()`. On lines 316-319, only `g_bg_thread.subscription_id` is unsubscribed. Then `g_object_unref(connection)` is called. GLib closes the D-Bus connection, which *implicitly* removes all subscriptions for that connection. So functionally, the signals do get cleaned up — but only because `g_object_unref` does it as a side effect, not because the code explicitly manages the lifecycle. If the connection were ever shared or reused, the subscriptions would leak. + +**Root cause:** +`dwnl_sub_id` and `update_sub_id` are stored as local variables in `background_thread_func()` and never saved to the `BackgroundThread` struct. + +**Recommended fix:** +Add `guint dwnl_subscription_id` and `guint update_subscription_id` fields to the `BackgroundThread` struct in `rdkFwupdateMgr_async_internal.h`. Store the IDs and unsubscribe them alongside `subscription_id` in the thread cleanup block. + +**Mitigating factor:** The `g_object_unref(connection)` on line 320 closes the connection, which implicitly cleans up all subscriptions on it. So in practice this is a code correctness issue, not a resource leak. But it would fail a Coverity audit for asymmetric subscribe/unsubscribe. + +--- + +### Finding 2: `internal_system_init()` incomplete rollback on dwnl/update mutex init failure + +**Severity**: Major +**Category**: Lifecycle / Memory +**Priority**: Next Release + +**Why this is dangerous in production:** +On line 156 of `rdkFwupdateMgr_async.c`, if `pthread_mutex_init(&g_dwnl_registry.mutex)` fails, the function returns `-1` without cleaning up the already-created BG thread, `g_registry.mutex`, `g_bg_thread.mutex`, `g_bg_thread.main_loop`, `g_bg_thread.context`, or the running BG thread. Similarly on line 163, the update mutex failure only destroys `g_dwnl_registry.mutex` but not the BG thread or `g_registry`. + +**Realistic failure scenario:** +If either late-stage `pthread_mutex_init` fails (extremely rare — only under extreme kernel resource exhaustion), the caller in `registerProcess()` receives `-1` from `internal_system_init()`. The rollback in `registerProcess()` (lines 745-780 of `rdkFwupdateMgr_process.c`) does a best-effort daemon unregister and frees the handle, but the BG thread is orphaned — it's stuck in `g_main_loop_run()` with no way to quit it, and the `g_registry.mutex` is leaked. + +**Root cause:** +The early-return on `dwnl_registry.mutex` init failure (line 157) was added after the BG thread launch code and doesn't perform the same cleanup cascade that lines 131-136 do. + +**Recommended fix:** +On failure after BG thread creation, call the existing cleanup sequence: `g_main_loop_quit()` → `pthread_join()` → free GLib objects → destroy `g_bg_thread.mutex` → destroy `g_registry.mutex` → return `-1`. Or factor the cleanup into a helper function called from all failure paths. + +**Mitigating factor:** `pthread_mutex_init()` on Linux NPTL virtually never fails (it only fails for invalid attributes or extreme kernel memory exhaustion). This is a code correctness issue that Coverity would flag, not a practical production crash. + +--- + +### Finding 3: `strdup()` return value never checked in register_callback functions + +**Severity**: Major +**Category**: Static Analysis / Crash +**Priority**: Next Release + +**Why this is dangerous in production:** +Three locations assign `strdup(handle)` to `target->handle_key` without checking for NULL: +- Line 1004 in `internal_register_callback()` +- Line 1851 in `internal_dwnl_register_callback()` +- Line 2795 in `internal_update_register_callback()` + +If `strdup()` returns NULL (OOM), `handle_key` is NULL. Later, `dispatch_all_pending()` calls `strcmp(e->handle_key, ...)` which dereferences NULL → **SIGSEGV**. + +**Realistic failure scenario:** +Under severe memory pressure on embedded devices (common during firmware download when large buffers are allocated), `strdup()` of even a 2-byte string ("1") could fail. The next signal dispatch would crash the process. + +**Root cause:** +The `strdup()` calls predate the detailed documentation effort and were never augmented with NULL checks. + +**Recommended fix:** +After each `strdup(handle)`, check for NULL. If NULL, set state back to IDLE, unlock mutex, return `false`. The caller (API function) already handles `false` by cleaning up the D-Bus connection and returning failure. + +--- + +## Validation Against Mandatory Expectations + +| Expectation | Status | Notes | +|---|---|---| +| Coverity-grade clean | **Fail** | Finding #3: unchecked `strdup()` return (3 sites). Finding #1: asymmetric subscribe/unsubscribe. | +| No memory leak | **Pass** | All heap allocations (`strdup`, `malloc(32)`) have matching `free()` calls on all paths. `parse_update_details` frees its `work_str`. Signal data cleanup is thorough. | +| Thread safe | **Pass** | Two-phase dispatch prevents deadlock. All registry access is mutex-protected. Callback invocation happens outside critical sections. | +| Race condition safe | **Pass** | Register-before-send ordering prevents signal-before-registration race. Dedup prevents double-dispatch. `DISPATCHED` state prevents re-entry. | +| Critical section safe | **Pass** | No nested locking (each registry has its own independent mutex). Two-phase dispatch ensures short critical sections (~microseconds). No lock ordering dependency. | +| Positive/negative scenarios handled | **Pass** | NULL/empty handle checks, NULL callback checks, NULL struct checks, registry-full handling, D-Bus connection failure, daemon timeout, parse failure, OOM on `malloc(32)`. | +| Buffer safe | **Pass** | `strncpy` with `sizeof()-1` in `parse_update_details`. `snprintf` for handle_copy in snapshots (256-byte fixed buffers, handles are ~2 bytes). `FwInfoData.CurrFWVersion` copy is bounded. | + +--- + +## Areas Reviewed with No Major Concerns + +- **Two-phase dispatch pattern** (all 3 registries): Correctly prevents deadlock. Snapshot-under-mutex + invoke-without-mutex is textbook correct. +- **Callback lifecycle**: PENDING→DISPATCHED→IDLE (check), ACTIVE→IDLE (download/update) correctly tracks state transitions. Terminal status detection is sound. +- **`parse_update_details()`**: Uses `strtok_r` (thread-safe), works on a `strdup`'d copy (original preserved), bounded `strncpy`, handles malformed tokens gracefully. +- **`unregisterProcess()` validation**: `strtoull` with full `errno`/`endptr`/`*endptr`/zero checks. Correct `free(handler)` on all paths. +- **`registerProcess()` failure recovery**: OOM on `malloc(32)` and `internal_system_init()` failure both do best-effort daemon unregister before returning NULL. +- **Memory management across the library**: Every `strdup` has a matching `free` in the reset/deinit path. `GVariant` references are properly `g_variant_unref`'d. `GDBusProxy`/`GDBusConnection` properly `g_object_unref`'d. Signal data strings (`g_free`/`free`) are freed after dispatch completes. +- **D-Bus connection model**: Ephemeral per-call connections for API calls, persistent connection for BG thread signal reception. Clean separation, no cross-thread GLib context issues. +- **Shutdown ordering**: `g_main_loop_quit` → `pthread_join` → resource teardown. The join ensures the BG thread is dead before mutexes are destroyed. + +--- + +## Final Verdict + +The library is **production-ready for deployment** on the normal operational path. The two-phase dispatch design, mutex discipline, and input validation are solid engineering. The three findings are all edge-case defects that would be caught by static analysis tools (Coverity, Coverity SA, cppcheck) but do not affect the standard `register → check → download → update → unregister` flow. + +**Finding #3 (unchecked `strdup`)** is the most important to fix because it has a real (if unlikely) crash path under memory pressure. Findings #1 and #2 are code correctness issues that should be addressed for audit cleanliness but have no practical production impact due to mitigating factors (GLib implicit cleanup, near-impossibility of `pthread_mutex_init` failure). + +**No immediate ship-blocker. Fix Finding #3 before next release. Address #1 and #2 in planned hardening.** From e9b267519fc5b56ca4000a6e550b381ddb733f95 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:38:41 +0530 Subject: [PATCH 30/36] Update rdkFwupdateMgr_handlers.c --- src/dbus/rdkFwupdateMgr_handlers.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dbus/rdkFwupdateMgr_handlers.c b/src/dbus/rdkFwupdateMgr_handlers.c index 06e75bf0..fbed0e0f 100644 --- a/src/dbus/rdkFwupdateMgr_handlers.c +++ b/src/dbus/rdkFwupdateMgr_handlers.c @@ -473,11 +473,11 @@ static int fetch_xconf_firmware_info( XCONFRES *pResponse, int server_type, int xconf_context.dwlloc = &DwnLoc; xconf_context.pPostFields = pJSONStr; - //Have to revist these vars once - MADHU + Rfc_t local_rfc_list = {0}; getRFCSettings(&local_rfc_list); // Read actual RFC settings from system - const char *local_immed_reboot_flag = "false"; // Keep throttle behavior enabled by default; set to "true" only when explicitly requested. + const char *local_immed_reboot_flag = "true"; // Keeping it true as default setting to make it work in Throttle enable mode. int local_delay_dwnl = 0; // Default daemon setting const char *local_lastrun = "0"; // Default daemon setting char *local_disableStatsUpdate = "false"; // Default daemon setting From 3da778c988b900acf4b5ad2ca1ab569fec044559 Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:29:00 +0530 Subject: [PATCH 31/36] Update rdkFwupdateMgr_handlers.c --- src/dbus/rdkFwupdateMgr_handlers.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dbus/rdkFwupdateMgr_handlers.c b/src/dbus/rdkFwupdateMgr_handlers.c index fbed0e0f..d27eadcf 100644 --- a/src/dbus/rdkFwupdateMgr_handlers.c +++ b/src/dbus/rdkFwupdateMgr_handlers.c @@ -477,7 +477,7 @@ static int fetch_xconf_firmware_info( XCONFRES *pResponse, int server_type, int Rfc_t local_rfc_list = {0}; getRFCSettings(&local_rfc_list); // Read actual RFC settings from system - const char *local_immed_reboot_flag = "true"; // Keeping it true as default setting to make it work in Throttle enable mode. + const char *local_immed_reboot_flag = "NA"; // Keeping it as NA as default setting to make it work in Throttle enable mode. int local_delay_dwnl = 0; // Default daemon setting const char *local_lastrun = "0"; // Default daemon setting char *local_disableStatsUpdate = "false"; // Default daemon setting From da124006b2c425cc762c1393dde2e3e291a20a88 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Wed, 29 Apr 2026 11:02:51 +0000 Subject: [PATCH 32/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- Addressing copilot reviews --- librdkFwupdateMgr/docs/DOCUMENT_v1.md | 998 -------------------------- 1 file changed, 998 deletions(-) delete mode 100755 librdkFwupdateMgr/docs/DOCUMENT_v1.md diff --git a/librdkFwupdateMgr/docs/DOCUMENT_v1.md b/librdkFwupdateMgr/docs/DOCUMENT_v1.md deleted file mode 100755 index 9f8c6452..00000000 --- a/librdkFwupdateMgr/docs/DOCUMENT_v1.md +++ /dev/null @@ -1,998 +0,0 @@ -# librdkFwupdateMgr — Design, Code Documentation & Logging Architecture - -> **Version**: v1.0 -> **Date**: April 28, 2026 -> **Scope**: Pull Request documentation for the `librdkFwupdateMgr` shared library -> **Author**: Senior Engineer — rdkFwupdateMgr Team - ---- - -## Table of Contents - -1. [Executive Summary](#1-executive-summary) -2. [High-Level Architecture](#2-high-level-architecture) -3. [File Structure](#3-file-structure) -4. [API-by-API Design](#4-api-by-api-design) - - 4.1 [registerProcess()](#41-registerprocessprocessname-libversion--firmwareinterfacehandle) - - 4.2 [checkForUpdate()](#42-checkforupdatehandle-callback--checkforupdateresult) - - 4.3 [downloadFirmware()](#43-downloadfirmwarehandle-fwdwnlreq-callback--downloadresult) - - 4.4 [updateFirmware()](#44-updatefirmwarehandle-fwupdatereq-callback--updateresult) - - 4.5 [unregisterProcess()](#45-unregisterprocesshandle) -5. [Internal Async Engine Design](#5-internal-async-engine-design) - - 5.1 [Three Callback Registries](#51-three-callback-registries) - - 5.2 [Background Thread](#52-background-thread) - - 5.3 [Two-Phase Signal Dispatch (Deadlock Prevention)](#53-two-phase-signal-dispatch-deadlock-prevention) - - 5.4 [Per-Call D-Bus Connections (Stateless Model)](#54-per-call-d-bus-connections-stateless-model) -6. [Memory Management](#6-memory-management) -7. [Thread Safety](#7-thread-safety) -8. [Error Handling Strategy](#8-error-handling-strategy) -9. [In-Code Documentation Guidelines](#9-in-code-documentation-guidelines) - - 9.1 [rdkFwupdateMgr_process.c — Key Inline Comments](#91-rdkfwupdatemgr_processc--key-inline-comments) - - 9.2 [rdkFwupdateMgr_api.c — Key Inline Comments](#92-rdkfwupdatemgr_apic--key-inline-comments) - - 9.3 [rdkFwupdateMgr_async.c — Key Inline Comments](#93-rdkfwupdatemgr_asyncc--key-inline-comments) -10. [Logging Architecture](#10-logging-architecture) - - 10.1 [Three Log Modules](#101-three-log-modules) - - 10.2 [Macro Definitions](#102-macro-definitions) - - 10.3 [Log Initialization Ownership](#103-log-initialization-ownership) - - 10.4 [Includes Required](#104-includes-required) - - 10.5 [Build Configuration (Makefile.am)](#105-build-configuration-makefileam) - - 10.6 [Sample Log Output](#106-sample-log-output) - ---- - -## 1. Executive Summary - -`librdkFwupdateMgr` is a shared library (`.so`) that provides a simple C API for client applications to perform firmware updates on RDK devices. It communicates with the `rdkFwupdateMgr` daemon over D-Bus. The library hides all D-Bus complexity — clients just call functions and receive results via callbacks. - -**In simple terms**: Think of it like ordering food through a delivery app. -- You place an order (`registerProcess`) and get an order number (`handle`). -- You ask "is my food ready?" (`checkForUpdate`) and get a notification later (`callback`). -- You say "deliver it" (`downloadFirmware`) and track the delivery (`progress callbacks`). -- You say "serve it" (`updateFirmware`) and watch it being plated (`progress callbacks`). -- When done, you close the app (`unregisterProcess`). - -The library handles all the complicated behind-the-scenes communication (D-Bus IPC) with the daemon that actually does the firmware work. - ---- - -## 2. High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ CLIENT APPLICATION │ -│ (e.g., example_plugin) │ -│ │ -│ main() │ -│ ├─ registerProcess("MyApp", "1.0") → handle "12345" │ -│ ├─ checkForUpdate(handle, my_cb) → returns immediately │ -│ │ [waits on condvar] │ -│ │ ◄── my_cb(fwinfo) fires in BG thread │ -│ ├─ downloadFirmware(handle, req, dl_cb) → returns immed. │ -│ │ [waits on condvar] │ -│ │ ◄── dl_cb(progress%, status) fires repeatedly │ -│ ├─ updateFirmware(handle, req, upd_cb) → returns immed. │ -│ │ [waits on condvar] │ -│ │ ◄── upd_cb(progress%, status) fires repeatedly │ -│ └─ unregisterProcess(handle) │ -│ │ -├──────────────── librdkFwupdateMgr.so ───────────────────────┤ -│ │ -│ PUBLIC LAYER (rdkFwupdateMgr_api.c, _process.c) │ -│ • Input validation │ -│ • D-Bus proxy creation (per-call, stateless) │ -│ • Fire-and-forget D-Bus method calls │ -│ • Callback registration in internal registries │ -│ │ -│ ASYNC ENGINE (rdkFwupdateMgr_async.c) │ -│ • 3 callback registries (Check, Download, Update) │ -│ • 1 background thread running GLib event loop │ -│ • D-Bus signal subscriptions (3 signals) │ -│ • Two-phase dispatch (snapshot → invoke) │ -│ │ -├──────────────── D-Bus (system bus) ──────────────────────────┤ -│ │ -│ rdkFwupdateMgr DAEMON │ -│ • Registered as org.rdkfwupdater.Service │ -│ • Processes method calls (Register, Check, Download, │ -│ Update, Unregister) │ -│ • Emits signals back (CheckForUpdateComplete, │ -│ DownloadProgress, UpdateProgress) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### How D-Bus Fits In (For Newcomers) - -D-Bus is a messaging system used on Linux devices for programs to talk to each other. Think of it like a shared phone line between applications: - -- The **daemon** (server) sits on the phone line and has a well-known name: `org.rdkfwupdater.Service` -- The **library** (client) calls methods on the daemon (like dialing a number and asking a question) -- The **daemon** sends **signals** back (like broadcast notifications) when work is done -- The **library's background thread** listens for these signals and routes them to the right callback - ---- - -## 3. File Structure - -| File | Layer | Purpose | -|------|-------|---------| -| `include/rdkFwupdateMgr_client.h` | **Public API** | All types, enums, callbacks, and function declarations a client needs. This is the ONLY header clients include. | -| `src/rdkFwupdateMgr_process.c` | **Public** | `registerProcess()` and `unregisterProcess()` — synchronous D-Bus calls | -| `src/rdkFwupdateMgr_api.c` | **Public** | `checkForUpdate()`, `downloadFirmware()`, `updateFirmware()` — async fire-and-forget APIs | -| `src/rdkFwupdateMgr_async.c` | **Internal** | The async engine — registries, background thread, signal handlers, dispatch logic | -| `src/rdkFwupdateMgr_async_internal.h` | **Internal** | Internal types and declarations — NOT part of public API, NOT shipped to clients | -| `src/rdkFwupdateMgr_log.h` | **Internal** | `FWUPMGR_*` logging macros (library code uses these) | -| `examples/example_app.c` | **Example** | Reference client demonstrating the full register→check→download→flash→unregister workflow | - -### What's Public vs Internal? - -- **Public** (`include/` folder): Headers that client applications `#include`. These define the API contract. Changing these requires a version bump. -- **Internal** (`src/` folder): Implementation details. Clients never see these. We can change them freely without breaking clients. - ---- - -## 4. API-by-API Design - -### 4.1 `registerProcess(processName, libVersion)` → `FirmwareInterfaceHandle` - -**What it does in plain English**: Tells the daemon "Hello, I'm a new client named [processName]. Please give me a session ID so I can do firmware operations." - -**Returns**: A string like `"12345"` (the session ID). Returns `NULL` on failure. - -#### Design Decisions - -- **Synchronous** — blocks until daemon responds (~5-10ms). Registration is fast so blocking is acceptable. -- Creates a **fresh D-Bus proxy per call** (stateless, no persistent connection). This means each call to any API gets a different D-Bus sender ID, which is a key characteristic of this library model. -- Daemon returns a `uint64` handler_id; library converts it to a `malloc()`'d string (e.g., `"12345"`). String representation provides ABI stability. -- Calls `internal_system_init()` to start the background thread that will listen for daemon signals. -- If `malloc()` fails after registration succeeds on the daemon side, performs **best-effort cleanup** by calling `UnregisterProcess` on the daemon to avoid leaking a registration. - -#### D-Bus Protocol - -``` -Method: RegisterProcess(s processName, s libVersion) → (t handler_id) -``` - -#### Flow - -``` -Client Thread Daemon -───────────── ────── -registerProcess("MyApp", "1.0") - │ - ├─ Validate processName (not NULL, not empty, ≤256 chars) - ├─ Validate libVersion (not NULL, ≤64 chars) - ├─ Create D-Bus proxy → connect to system bus - ├─ Call RegisterProcess("MyApp", "1.0") ──────────► Daemon receives call - │ │ Creates ProcessInfo - │ │ Assigns handler_id=12345 - │◄──── Returns handler_id=12345 ─────────────────────┘ - ├─ Convert 12345 → malloc'd string "12345" - ├─ internal_system_init() - │ ├─ Init 3 callback registries (mutexes + arrays) - │ ├─ Create isolated GLib context + event loop - │ ├─ Spawn background thread - │ └─ Wait until background thread is ready (signal subscriptions live) - └─ Return "12345" to caller -``` - -#### Lifecycle Impact - -This is the **start** of the library's lifecycle. Before `registerProcess()`: -- No background thread exists -- No callback registries exist -- No D-Bus signal subscriptions exist - -After `registerProcess()` returns successfully, the library is fully operational and ready for async API calls. - ---- - -### 4.2 `checkForUpdate(handle, callback)` → `CheckForUpdateResult` - -**What it does in plain English**: Asks the daemon "Is there new firmware available for this device?" The answer comes later through your callback — this function returns immediately. - -**Returns**: `CHECK_FOR_UPDATE_SUCCESS` (request started) or `CHECK_FOR_UPDATE_FAIL` (couldn't even start). - -#### Design Decisions - -- **Non-blocking** (fire-and-forget) — returns immediately after sending the D-Bus message -- **Callback fires exactly once** — when the daemon finishes querying the XConf server (5-30 seconds later) -- The callback receives a `FwInfoData` struct with firmware version info and update details - -#### The Connect → Register → Send Ordering - -This is a critical design detail: - -``` -Step 1: Connect to D-Bus ← Fail fast if daemon/bus is down -Step 2: Register callback ← Now we're ready to receive the signal -Step 3: Send method call ← Daemon starts working -``` - -**Why this order?** -- If we registered the callback first but D-Bus connect fails, we'd have a stale PENDING entry in the registry that would never be dispatched (no signal will ever arrive for it). -- If we sent the method first but hadn't registered the callback yet, the daemon might emit the signal before our callback is registered — the signal arrives, no matching entry found, result is silently lost. -- Connecting first, then registering, then sending gives us the safest ordering with the smallest race window. - -#### D-Bus Protocol - -``` -Method: CheckForUpdate(s handle) — fire-and-forget, no reply waited for -Signal: CheckForUpdateComplete(t handler_id, i result, i status, s currentVer, - s availableVer, s updateDetails, s message) -``` - -#### Flow - -``` -Client Thread Background Thread Daemon -───────────── ───────────────── ────── -checkForUpdate("12345", my_cb) - │ - ├─ Validate handle, callback - ├─ Connect to D-Bus - ├─ Register my_cb in CheckForUpdate registry - │ (slot state: IDLE → PENDING) - ├─ Fire-and-forget: CheckForUpdate("12345") ──────────► Daemon receives - └─ Return SUCCESS immediately │ Queries XConf... - │ (5-30 seconds) - [client is free to do │ - other work or wait] │ - │ - ◄── CheckForUpdateComplete ────┘ (signal emitted) - on_check_complete_signal(): - ├─ Parse GVariant → InternalSignalData - ├─ dispatch_all_pending(): - │ Phase 1 (mutex held): - │ Snapshot PENDING entries - │ Mark → DISPATCHED - │ Phase 2 (no mutex): - │ Build FwInfoData from signal - │ Call my_cb(&fwinfo_data) ──► Client's callback runs - │ Reset slot → IDLE - └─ Cleanup signal data -``` - -#### What the Callback Receives - -```c -typedef struct { - char CurrFWVersion[64]; // e.g., "1.0.0" - UpdateDetails *UpdateDetails; // Non-NULL only if FIRMWARE_AVAILABLE - CheckForUpdateStatus status; // FIRMWARE_AVAILABLE, NOT_AVAILABLE, etc. -} FwInfoData; -``` - -**Important**: The `FwInfoData` pointer and all its contents are only valid DURING the callback invocation. If you need the data later, copy it (which is exactly what `example_app.c` does with `strncpy` to globals). - ---- - -### 4.3 `downloadFirmware(handle, fwdwnlreq, callback)` → `DownloadResult` - -**What it does in plain English**: Tells the daemon "Download this firmware file from the server." Returns immediately; your callback fires repeatedly with progress updates (0%, 25%, 50%, 75%, 100%). - -**Returns**: `RDKFW_DWNL_SUCCESS` (download started) or `RDKFW_DWNL_FAILED` (couldn't start). - -#### Design Decisions - -- Same fire-and-forget pattern as `checkForUpdate()` -- **Key difference**: Callback fires **multiple times** (once per progress signal from daemon) -- Registry slot stays **ACTIVE** across all progress signals; only resets to IDLE on `DWNL_COMPLETED` or `DWNL_ERROR` -- Download URL can be `NULL`/empty string — daemon will use the URL from the XConf query - -#### D-Bus Protocol - -``` -Method: DownloadFirmware(s handle, s firmwareName, s downloadUrl, s TypeOfFirmware) -Signal: DownloadProgress(t handler_id, s firmwareName, u progress%, s status, s message) -``` - -#### Flow - -``` -Client Thread Background Thread Daemon -───────────── ───────────────── ────── -downloadFirmware("12345", &req, my_dl_cb) - │ - ├─ Validate handle, req, callback - ├─ Connect to D-Bus - ├─ Register my_dl_cb in Download registry - │ (slot state: IDLE → ACTIVE) - ├─ Fire-and-forget: DownloadFirmware(...) ─────────► Daemon starts download - └─ Return SUCCESS immediately │ - │ Download progress... - ◄── DownloadProgress(10%) ──┘ - dispatch_all_dwnl_active(): - Call my_dl_cb(10, IN_PROGRESS) ──► prints "10%" - (slot stays ACTIVE) - - ◄── DownloadProgress(50%) ── - Call my_dl_cb(50, IN_PROGRESS) ──► prints "50%" - - ◄── DownloadProgress(100%) ── - Call my_dl_cb(100, COMPLETED) ──► prints "100% done!" - Reset slot → IDLE (download finished) -``` - ---- - -### 4.4 `updateFirmware(handle, fwupdatereq, callback)` → `UpdateResult` - -**What it does in plain English**: Tells the daemon "Flash this downloaded firmware onto the device's storage." This modifies the device firmware. Returns immediately; callback fires with progress. - -**Returns**: `RDKFW_UPDATE_SUCCESS` (flash started) or `RDKFW_UPDATE_FAILED` (couldn't start). - -#### Design Decisions - -- Identical pattern to `downloadFirmware()` — ACTIVE slot, multiple callbacks, reset on terminal status -- `LocationOfFirmware` can be `NULL`/empty — daemon uses the default path from `/etc/device.properties` -- `rebootImmediately` is a `bool` in the struct but sent to daemon as string `"true"`/`"false"` (daemon D-Bus method expects string) -- This operation is **irreversible** once the flash starts — client should verify the firmware file first - -#### D-Bus Protocol - -``` -Method: UpdateFirmware(s handle, s firmwareName, s location, s type, s rebootImmediately) -Signal: UpdateProgress(t handler_id, s firmwareName, i progress%, i status, s message) -``` - ---- - -### 4.5 `unregisterProcess(handle)` - -**What it does in plain English**: Tells the daemon "I'm done, please clean up my registration." Also frees all library resources (background thread, registries, handle memory). - -**Returns**: Nothing (`void`). This is a best-effort cleanup. - -#### Design Decisions - -- **Synchronous** — blocks until daemon responds -- **Best-effort** — if D-Bus call fails, local cleanup still happens (daemon may have already cleaned up on its own) -- Calls `internal_system_deinit()` **FIRST** — stops background thread, frees registries, destroys mutexes -- Then sends D-Bus `UnregisterProcess`, then `free(handle)` -- **Idempotent**: safe to call with `NULL` handle (no-op) -- Strictly validates handle string: must be pure decimal digits, no leading/trailing whitespace, no garbage characters - -#### Why deinit Before D-Bus Call? - -After unregister, the daemon won't send any more signals for this client. There's no point keeping the background thread alive. Shutting it down first ensures a clean `pthread_join()` without waiting for signals that will never arrive. - -#### Flow - -``` -Client Thread Daemon -───────────── ────── -unregisterProcess("12345") - │ - ├─ Validate handle (not NULL) - ├─ Parse "12345" → uint64 12345 - ├─ internal_system_deinit() - │ ├─ g_main_loop_quit() → background thread wakes up - │ ├─ pthread_join() → wait for background thread to exit - │ ├─ Free GLib resources (loop, context) - │ ├─ Free download & update registries - │ ├─ Free check registry (any leftover handle_key strings) - │ └─ Destroy all mutexes - ├─ Create D-Bus proxy - ├─ Call UnregisterProcess(12345) ──────────────► Daemon removes ProcessInfo - │◄──── Returns success=true ──────────────────┘ - ├─ free(handle) ← The "12345" string is freed - └─ Return -``` - ---- - -## 5. Internal Async Engine Design - -### 5.1 Three Callback Registries - -The library maintains **three independent registries**, one per async API. They are separate because each has a different callback lifecycle: - -| Registry | C Type | Slot Lifecycle | How Many Times Callback Fires | -|----------|--------|----------------|-------------------------------| -| `CallbackRegistry` | `g_registry` | IDLE → PENDING → DISPATCHED → IDLE | **Once** (check result) | -| `DwnlCallbackRegistry` | `g_dwnl_registry` | IDLE → ACTIVE → IDLE | **Multiple** (progress updates) | -| `UpdateCbRegistry` | `g_update_registry` | IDLE → ACTIVE → IDLE | **Multiple** (progress updates) | - -Each registry is a **fixed-size array** of `MAX_PENDING_CALLBACKS` (30) slots. This means at most 30 concurrent pending callbacks across all clients of any given type. - -Each slot holds: -- **State**: IDLE, PENDING, ACTIVE, DISPATCHED, or TIMED_OUT -- **handle_key**: `strdup()`'d copy of the app's handle string -- **callback**: Function pointer to the app's callback -- **registered_time**: Timestamp for timeout detection - -Each registry has its **own `pthread_mutex_t`** — so checkForUpdate registrations don't block download progress dispatching. - -#### State Machine - -``` -CheckForUpdate slot: - IDLE ──(checkForUpdate called)──► PENDING ──(signal arrives)──► DISPATCHED ──► IDLE - └──(timeout)──► TIMED_OUT ──► IDLE - -Download/Update slot: - IDLE ──(download/updateFirmware called)──► ACTIVE ──(COMPLETED/ERROR)──► IDLE - │ - │ (fires callback on EVERY - │ progress signal while ACTIVE) - │ - └──(timeout)──► TIMED_OUT ──► IDLE -``` - -### 5.2 Background Thread - -A single `pthread` runs a private GLib event loop for the entire lifetime of the library (from `registerProcess()` to `unregisterProcess()`). - -#### Thread Startup Sequence - -``` -1. g_main_context_push_thread_default() - └─ Creates an ISOLATED GLib context for this thread - (won't interfere with app's own GLib loop if it has one) - -2. g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) - └─ Connect to system D-Bus (this is the background thread's - OWN connection — different from the per-call connections - used by the public API functions) - -3. g_dbus_connection_signal_subscribe() × 3 - ├─ CheckForUpdateComplete → on_check_complete_signal() - ├─ DownloadProgress → on_download_progress_signal() - └─ UpdateProgress → on_update_progress_signal() - -4. g_bg_thread.running = true - └─ Main thread sees this and stops spin-waiting - -5. g_main_loop_run() - └─ BLOCKS here until internal_system_deinit() calls g_main_loop_quit() - GLib dispatches signal callbacks within this loop. -``` - -#### Why Spin-Wait Instead of Condvar? - -The main thread waits for the background thread to be ready by polling `g_bg_thread.running` every 100ms. A condvar would be slightly more elegant, but: - -1. The wait is typically <100ms (thread starts fast) -2. Adding a condvar adds complexity and another resource to manage/destroy -3. This only happens **once per library lifetime** (at `registerProcess` time) -4. Max wait: 50 × 100ms = 5 seconds — if the thread hasn't started by then, something is seriously wrong - -### 5.3 Two-Phase Signal Dispatch (Deadlock Prevention) - -All three signal handlers use the same two-phase pattern. This is the most important design detail in the async engine. - -#### The Problem - -If we held the registry mutex while invoking a callback, and that callback called `checkForUpdate()` again, it would try to lock the same mutex → **deadlock**. - -#### The Solution - -``` -PHASE 1 — Snapshot (mutex HELD): - ├─ Scan registry for matching entries (PENDING or ACTIVE) - ├─ Copy callback pointer + handle into a local stack array (snapshot) - ├─ Mark slots appropriately (DISPATCHED for check, leave ACTIVE for download/update) - └─ RELEASE mutex - -PHASE 2 — Invoke (NO mutex held): - ├─ Build result struct from signal data - ├─ For each snapshot entry: - │ ├─ Call callback(result_data) - │ └─ If terminal state (COMPLETED/ERROR): re-lock mutex, reset slot → IDLE, unlock - └─ Done -``` - -**Why is this safe?** -- During Phase 2, the mutex is released, so if a callback calls `checkForUpdate()` → `internal_register_callback()`, it can acquire the mutex without deadlock. -- The snapshot is a local stack array, so even if the registry changes during Phase 2, our snapshot is stable. - -### 5.4 Per-Call D-Bus Connections (Stateless Model) - -Each public API call (`registerProcess`, `checkForUpdate`, etc.) creates a **fresh D-Bus connection and proxy** for that single call, then immediately releases it via `g_object_unref()`. - -#### Implications - -- **No persistent connection** between the client and daemon at the API layer -- Each call may get a **different D-Bus sender ID** (e.g., `:1.140` for register, `:1.141` for checkForUpdate, `:1.145` for unregister) -- The daemon **cannot rely on sender ID** to identify a client across calls — only the `handler_id` (the numeric handle) is stable -- This is why the daemon's `UnregisterProcess` handler doesn't validate sender-ID ownership — it would always fail because unregister comes from a different sender than register - -The background thread has its **OWN persistent connection** (for signal subscriptions), but the API-calling thread uses ephemeral connections. - ---- - -## 6. Memory Management - -| Resource | Owner | Allocation | Deallocation | -|----------|-------|------------|--------------| -| `FirmwareInterfaceHandle` (the `"12345"` string) | Library | `malloc()` in `registerProcess()` | `free()` in `unregisterProcess()` | -| Registry `handle_key` entries | Async engine | `strdup()` on callback registration | `free()` on slot reset to IDLE | -| D-Bus proxy/connection (per API call) | Caller's stack | `g_bus_get_sync()` + `g_dbus_proxy_new_sync()` | `g_object_unref()` at end of each API function | -| D-Bus connection (background thread) | Background thread | `g_bus_get_sync()` in thread func | `g_object_unref()` on thread exit | -| GMainLoop, GMainContext | Async engine | `g_main_loop_new()` / `g_main_context_new()` | `g_main_loop_unref()` / `g_main_context_unref()` in `deinit()` | -| `FwInfoData` + `UpdateDetails` in dispatch | Stack-allocated | `dispatch_all_pending()` local variables | Automatic (function returns) | -| `InternalSignalData` strings | Parse function | `strdup()` from GVariant data | `free()` in `internal_cleanup_signal_data()` | - -### Rules for Client Developers - -1. **Never `free()` the handle yourself.** Call `unregisterProcess()` and it handles everything. -2. **Copy callback data if you need it later.** The `FwInfoData*` pointer in your callback is only valid during the callback invocation. Use `strncpy()` to save values to your own buffers. -3. **Don't call library APIs from inside a callback.** The callback runs in the background thread. Re-entering the library is technically safe (due to two-phase dispatch) for `checkForUpdate`-style calls, but it's better practice to signal your main thread and make calls from there. - ---- - -## 7. Thread Safety - -| Operation | Thread-Safe? | Notes | -|-----------|-------------|-------| -| `registerProcess()` | Yes | Stateless per-call; GDBus sync calls are thread-safe | -| `unregisterProcess()` | Per-handle | Don't unregister the same handle from two threads simultaneously | -| `checkForUpdate()` | Yes | Registry mutex protects slot allocation | -| `downloadFirmware()` | Yes | Separate registry with its own mutex | -| `updateFirmware()` | Yes | Separate registry with its own mutex | -| Callbacks | N/A | Fire in background thread; app must use its own synchronization (mutex + condvar) to coordinate with main thread | - -### Callback Threading Model - -``` -Main Thread Background Thread -─────────── ───────────────── - on_check_complete_signal() - └─ dispatch_all_pending() - └─ your_callback(&fwinfo) ← RUNS HERE - ├─ Copy data to globals - ├─ pthread_mutex_lock(&your_mutex) - ├─ your_done_flag = 1 - ├─ pthread_cond_signal(&your_cond) - └─ pthread_mutex_unlock(&your_mutex) - -pthread_mutex_lock(&your_mutex) -while (!your_done_flag) - pthread_cond_timedwait(...) ← WAKES UP HERE -pthread_mutex_unlock(&your_mutex) -// Now use the copied data safely -``` - -This is exactly how `example_app.c` works — see `g_check_mutex`/`g_check_cond`/`g_check_done`. - ---- - -## 8. Error Handling Strategy - -The library follows a defense-in-depth approach: - -### Layer 1: Input Validation (API Boundary) - -Every public function validates ALL parameters before touching D-Bus: -- NULL checks on handles, callbacks, request structs -- Empty string checks -- Length limit checks (process name ≤256 chars, version ≤64 chars) -- Numeric validity for handle parsing in `unregisterProcess()` (uses `strtoull` with strict `endptr` checking) - -### Layer 2: D-Bus Connection Failures - -- Connection attempt happens BEFORE callback registration (Connect → Register → Send pattern) -- If D-Bus is down, function returns error immediately with no stale registry entries - -### Layer 3: Daemon Errors - -- D-Bus errors from the daemon are caught via `GError` -- Error message is logged via `FWUPMGR_ERROR` -- Error is propagated as a return code to the caller - -### Layer 4: Resource Leak Prevention - -- In `registerProcess()`: If registration succeeds on daemon but `malloc()` fails locally, a cleanup proxy is created to send `UnregisterProcess` to prevent leaking the registration -- In `unregisterProcess()`: Handle memory is freed regardless of whether the D-Bus call succeeds -- In all API functions: D-Bus connections/proxies are freed on all code paths (success and error) - -### Layer 5: Best-Effort Cleanup - -- `unregisterProcess()` is best-effort: if D-Bus call fails, local cleanup still happens -- This handles the case where the daemon has already crashed or been restarted - ---- - -## 9. In-Code Documentation Guidelines - -The source code already has extensive inline comments. Below are the **key documentation points** that every reviewer should understand, organized by file. - -### 9.1 `rdkFwupdateMgr_process.c` — Key Inline Comments - -#### `registerProcess()` — Why `internal_system_init()` is here - -```c -/* - * Start the background listener thread NOW (not at library load time). - * - * WHY HERE and not in __attribute__((constructor))? - * The constructor approach is #if 0'd out in rdkFwupdateMgr_api.c. - * We start the background thread at register time because: - * 1. The handle must exist before any async API can be called - * 2. The background thread needs a live D-Bus connection — doing it - * too early risks connecting before the system bus is ready - * (common during early boot on embedded devices) - * 3. It pairs naturally with internal_system_deinit() in unregisterProcess() - * - * This initializes: - * - CallbackRegistry (checkForUpdate callbacks) - * - DwnlCallbackRegistry (download callbacks) - * - UpdateCbRegistry (update callbacks) - * - BackgroundThread (GLib event loop, D-Bus signal subscriptions) - */ -``` - -#### `unregisterProcess()` — Why `internal_system_deinit()` comes before D-Bus call - -```c -/* - * Stop the background listener thread BEFORE sending UnregisterProcess. - * - * WHY BEFORE the D-Bus call? - * After unregister, the daemon won't send us any more signals. - * There's no point keeping the background thread alive. - * Shutting down first ensures a clean pthread_join() without - * waiting for signals that will never arrive. - * - * This tears down: - * - g_main_loop_quit() → background thread exits g_main_loop_run() - * - pthread_join() → waits for clean exit - * - Frees all 3 registries (any leftover handle_key strings) - * - Destroys all mutexes - */ -``` - -### 9.2 `rdkFwupdateMgr_api.c` — Key Inline Comments - -#### Connect → Register → Send ordering rationale - -```c -/* - * ORDERING MATTERS: Connect → Register → Send - * - * We could register the callback first, but then if D-Bus connection - * fails, we'd have a stale PENDING entry in the registry that would - * never be dispatched (no signal will ever arrive for it). - * - * We could send first, but then the daemon might emit the signal - * before our callback is registered → signal arrives, no matching - * entry found, result is silently lost. - * - * The correct order is: - * 1. Connect (fail fast if daemon/D-Bus is down) - * 2. Register callback (now we're ready to receive) - * 3. Send the method call (daemon starts working) - * - * The window between register and send is microseconds — acceptably - * small race window where the callback slot exists but the daemon - * hasn't been asked yet. - */ -``` - -### 9.3 `rdkFwupdateMgr_async.c` — Key Inline Comments - -#### Two-Phase Dispatch — Why we release the mutex before calling callbacks - -```c -/* - * TWO-PHASE DESIGN — avoids deadlock: - * - * PHASE 1 (mutex held): - * Scan registry → snapshot all PENDING entries into local array. - * Mark each found entry as DISPATCHED. - * Release mutex. - * - * PHASE 2 (mutex released): - * Build FwInfoData from signal_data. - * Invoke each snapshot callback: callback(&fwinfo_data) - * Re-acquire mutex briefly to reset each slot to IDLE. - * - * WHY RELEASE BEFORE CALLING CALLBACKS? - * If a callback called checkForUpdate() again, it would call - * internal_register_callback() which tries to lock the same mutex - * → deadlock. Releasing first makes re-entrant use safe. - */ -``` - -#### Background thread spin-wait rationale - -```c -/* - * Spin-wait for background thread to set running=true. - * Max wait: 50 × 100ms = 5 seconds. - * - * WHY SPIN-WAIT instead of condvar? - * 1. The wait is typically <100ms (thread starts fast) - * 2. Adding a condvar adds complexity and another resource to manage - * 3. This only happens once per library lifetime - * 4. 100ms sleep granularity is fine for a one-time init - * - * Ensures D-Bus signal subscription is live before checkForUpdate() - * can send a D-Bus method call — prevents missing the response signal. - */ -``` - ---- - -## 10. Logging Architecture - -### 10.1 Three Log Modules - -The system uses three distinct RDK_LOGGER modules so log output can be filtered by component: - -| Module Name | Macro Prefix | Used By | Log Tag in Output | -|------------|--------------|---------|-------------------| -| `LOG.RDK.FWUPMGR` | `FWUPMGR_*` | Library code (`_process.c`, `_api.c`, `_async.c`) | `[FWUPMGR]` | -| `LOG.RDK.FWUPG` | `SWLOG_*` | Daemon code (`rdkv_dbus_server.c`, `rdkv_main.c`, etc.) | `[FWUPG]` | -| `LOG.RDK.EXAMPLE` | `EXAMPLE_*` | Example app (`example_app.c`) | `[EXAMPLE]` | - -**Why three separate modules?** So you can filter logs in production: -- To see only library-side issues: `grep "\[FWUPMGR\]" /opt/logs/rdkFwupdateMgr.log` -- To see only daemon-side issues: `grep "\[FWUPG\]" /opt/logs/rdkFwupdateMgr.log` -- To see only client app issues: `grep "\[EXAMPLE\]" /opt/logs/rdkFwupdateMgr.log` - -### 10.2 Macro Definitions - -#### Library Macros — `rdkFwupdateMgr_log.h` - -```c -/* ── Base macro — callers provide their own module name ── */ -#define FWUPMGR_LOG(level, module, format, ...) \ - RDK_LOG(level, module, format, ##__VA_ARGS__) - -/* ── Library convenience macros ── */ -/* Used in rdkFwupdateMgr_process.c, rdkFwupdateMgr_api.c, rdkFwupdateMgr_async.c */ -#define FWUPMGR_TRACE(format, ...) FWUPMGR_LOG(RDK_LOG_TRACE1, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_FATAL(format, ...) FWUPMGR_LOG(RDK_LOG_FATAL, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -``` - -#### Example App Macros — `example_app.c` - -```c -/* Reuses the FWUPMGR_LOG base macro but with a different module name */ -#define EXAMPLE_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -#define EXAMPLE_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -#define EXAMPLE_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -#define EXAMPLE_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -``` - -#### Non-RDK_LOGGER Fallback - -When `RDK_LOGGER` is **not** defined (unit tests, standalone development builds), macros fall back to `fprintf`: - -```c -#define FWUPMGR_LOG(level, module, FORMAT...) fprintf(stderr, "[%s] " FORMAT, module) -``` - -This means logging works everywhere — just with different backends depending on the build configuration. - -#### How to Define Your Own Module (For New Client Apps) - -If you're writing a new client application (not using example_app.c), define your own macros: - -```c -#include "rdkFwupdateMgr_log.h" // Get FWUPMGR_LOG base macro -#include "rdkv_cdl_log_wrapper.h" // Get log_init(), log_exit() - -// Define your own module — logs will appear as [MYAPP] -#define MYAPP_INFO(fmt, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.MYAPP", fmt, ##__VA_ARGS__) -#define MYAPP_ERROR(fmt, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.MYAPP", fmt, ##__VA_ARGS__) - -int main(void) { - log_init(); // Initialize RDK logger - MYAPP_INFO("Starting up\n"); - // ... use library APIs ... - log_exit(); // Shutdown RDK logger -} -``` - -### 10.3 Log Initialization Ownership - -**The library does NOT own the log lifecycle.** The host application (whatever links to `librdkFwupdateMgr.so`) is responsible for calling: - -```c -log_init(); // BEFORE any library call (typically first line of main()) -log_exit(); // AFTER unregisterProcess() (typically last line before return) -``` - -Both `log_init()` and `log_exit()` are provided by `rdkv_cdl_log_wrapper.h` (in `common_utilities/utils/`). - -| Build Config | `log_init()` does | `log_exit()` does | -|-------------|-------------------|-------------------| -| `RDK_LOGGER` defined | Calls `rdk_logger_init()` with config file | Calls `rdk_logger_deinit()` | -| `RDK_LOGGER` not defined | No-op | No-op | - -**Why doesn't the library call `log_init()`?** -- A process should only call `log_init()` once. If the library called it, and the app also called it, that's a double-init which may cause issues. -- The app knows when it's ready to start logging. The library shouldn't make that decision. - -### 10.4 Includes Required - -#### For library source files (`_process.c`, `_api.c`, `_async.c`) - -```c -#include "rdkFwupdateMgr_log.h" // Provides FWUPMGR_* macros -``` - -That's all — the log header internally includes `rdkv_cdl_log_wrapper.h` and (if `RDK_LOGGER` is defined) `rdk_debug.h`. - -#### For client applications - -```c -#include "rdkFwupdateMgr_client.h" // Public API (types, functions) -#include "rdkFwupdateMgr_log.h" // FWUPMGR_LOG base macro (for defining your own module) -#include "rdkv_cdl_log_wrapper.h" // log_init(), log_exit() -``` - -### 10.5 Build Configuration (Makefile.am) - -The `example_plugin` target needs include paths for both log headers and link flags for the logger libraries: - -```makefile -# Include paths -example_plugin_CFLAGS += -I${top_srcdir}/librdkFwupdateMgr/src # rdkFwupdateMgr_log.h -example_plugin_CFLAGS += -I${top_srcdir}/common_utilities/utils # rdkv_cdl_log_wrapper.h - -# Link flags -example_plugin_LDADD += -lfwutils -lrdkloggers # log_init/log_exit implementations -``` - -### 10.6 Sample Log Output - -Below is what you'd see in `/opt/logs/rdkFwupdateMgr.log` during a typical firmware update workflow. Notice how the three tags (`[EXAMPLE]`, `[FWUPMGR]`, `[FWUPG]`) make it easy to trace what's happening at each layer: - -``` -[EXAMPLE] Application starting, PID: 1234 -[EXAMPLE] STEP 1: Register with firmware daemon -[FWUPMGR] registerProcess() called -[FWUPMGR] processName: 'ExampleApp' -[FWUPMGR] libVersion: '1.0.0' -[FWUPMGR] D-Bus proxy created successfully -[FWUPMGR] Calling RegisterProcess D-Bus method... -[FWUPG] [D-BUS] RegisterProcess received from ':1.140' -[FWUPG] [PROCESS_TRACKING] New client registered: ExampleApp (handler=12345) -[FWUPMGR] Registration successful -[FWUPMGR] handler_id: 12345 -[FWUPMGR] Handle created: '12345' -[FWUPMGR] internal_system_init: begin -[FWUPMGR] background_thread: starting -[FWUPMGR] background_thread: subscribed to CheckForUpdateComplete (id=1) -[FWUPMGR] background_thread: subscribed to DownloadProgress (id=2) -[FWUPMGR] background_thread: subscribed to UpdateProgress (id=3) -[FWUPMGR] internal_system_init: ready -[EXAMPLE] Registered successfully -[EXAMPLE] Handle: '12345' -[EXAMPLE] STEP 2: Check for firmware updates -[EXAMPLE] Calling checkForUpdate()... -[FWUPMGR] checkForUpdate: handle='12345' -[FWUPMGR] internal_register_callback: registered handle='12345' -[FWUPMGR] checkForUpdate: D-Bus call sent, returning SUCCESS. -[FWUPG] [D-BUS] CheckForUpdate received for handler 12345 -[FWUPG] [XCONF] Querying XConf server... -[FWUPG] [XCONF] Response: firmware_v2.bin available -[FWUPG] [SIGNAL] Emitting CheckForUpdateComplete -[FWUPMGR] on_check_complete_signal: received -[FWUPMGR] dispatch_all_pending: 1 callback(s) to fire -[FWUPMGR] dispatch_all_pending: invoking callback for handle='12345' -[EXAMPLE] checkForUpdate Callback Received -[EXAMPLE] Status Code: FIRMWARE_AVAILABLE (0) -[EXAMPLE] Current FW Version: 1.0.0 -[EXAMPLE] Available Version: 2.0.0 -[EXAMPLE] Firmware check data saved. Main thread will proceed. -[EXAMPLE] STEP 3: Download firmware image -[FWUPMGR] downloadFirmware: handle='12345' firmware='firmware_v2.bin' -[FWUPMGR] internal_dwnl_register_callback: registered handle='12345' -[FWUPMGR] downloadFirmware: D-Bus call sent, returning SUCCESS. -[FWUPMGR] on_download_progress_signal: progress=25% status='INPROGRESS' -[EXAMPLE] Download: 25% DWNL_IN_PROGRESS -[FWUPMGR] on_download_progress_signal: progress=50% status='INPROGRESS' -[EXAMPLE] Download: 50% DWNL_IN_PROGRESS -[FWUPMGR] on_download_progress_signal: progress=100% status='COMPLETED' -[EXAMPLE] Download: 100% DWNL_COMPLETED -[EXAMPLE] Download completed successfully! -``` - ---- - -## Appendix A: Complete Sequence Diagram - -``` - Main Thread Library BG Thread Daemon Process - ─────────── ───────────────── ────────────── - - [STEP 1: Register] - registerProcess("ExampleApp", "1.0.0") - │───── D-Bus: RegisterProcess ──────────────────► │ - │◄──── Returns handler_id=12345 ─────────────────┤ - g_handle = "12345" - internal_system_init() starts BG thread - │ subscribe CheckForUpdateComplete - │ subscribe DownloadProgress - │ subscribe UpdateProgress - │ running = true - │ g_main_loop_run() ← BLOCKS - - [STEP 2: Check for Update] - checkForUpdate("12345", on_check_cb) - │── register callback in g_registry - │───── D-Bus: CheckForUpdate("12345") ──────────► │ - │◄──── returns immediately │ query XConf... - │ waiting on condvar... │ - │ │ (5-30 seconds) - │ ◄── CheckForUpdateComplete ┤ - │ on_check_complete_signal(): │ - │ dispatch_all_pending() │ - │ on_check_cb(&fwinfo) ──►│ - │ signal condvar ───────►│ - │ condvar wakes up! │ - │ read g_check_status, g_fw_filename, etc. - - [STEP 3: Download] - downloadFirmware("12345", &req, on_dl_cb) - │── register callback in g_dwnl_registry - │───── D-Bus: DownloadFirmware(...) ────────────► │ - │◄──── returns immediately │ downloading... - │ waiting on condvar... │ - │ ◄── DownloadProgress(25%) ─┤ - │ on_dl_cb(25, IN_PROGRESS) │ - │ ◄── DownloadProgress(50%) ─┤ - │ on_dl_cb(50, IN_PROGRESS) │ - │ ◄── DownloadProgress(100%) ┤ - │ on_dl_cb(100, COMPLETED) │ - │ signal condvar ──────────►│ - │ condvar wakes up! - - [STEP 4: Flash] - updateFirmware("12345", &req, on_upd_cb) - │── register callback in g_update_registry - │───── D-Bus: UpdateFirmware(...) ──────────────► │ - │◄──── returns immediately │ flashing... - │ waiting on condvar... │ - │ ◄── UpdateProgress(50%) ──┤ - │ on_upd_cb(50, IN_PROGRESS) │ - │ ◄── UpdateProgress(100%) ─┤ - │ on_upd_cb(100, COMPLETED) │ - │ signal condvar ──────────►│ - │ condvar wakes up! - - [STEP 5: Unregister] - unregisterProcess("12345") - │── internal_system_deinit() - │ g_main_loop_quit() ──────────────► BG thread exits - │ pthread_join() ◄──────────────────┘ - │ free registries, destroy mutexes - │───── D-Bus: UnregisterProcess(12345) ─────────► │ - │◄──── Returns success=true ────────────────────┤ - │ free("12345") - │ done. -``` - ---- - -## Appendix B: D-Bus Interface Summary - -**Service**: `org.rdkfwupdater.Service` -**Object Path**: `/org/rdkfwupdater/Service` -**Interface**: `org.rdkfwupdater.Interface` - -### Methods (Client → Daemon) - -| Method | Signature | Description | -|--------|-----------|-------------| -| `RegisterProcess` | `(ss) → (t)` | Register client. Returns handler_id. | -| `UnregisterProcess` | `(t) → (b)` | Unregister client. Returns success. | -| `CheckForUpdate` | `(s)` | Fire-and-forget. No reply. | -| `DownloadFirmware` | `(ssss)` | Fire-and-forget. No reply. | -| `UpdateFirmware` | `(sssss)` | Fire-and-forget. No reply. | - -### Signals (Daemon → Client) - -| Signal | Signature | Description | -|--------|-----------|-------------| -| `CheckForUpdateComplete` | `(tiissss)` | Firmware check result. Fires once. | -| `DownloadProgress` | `(tsuss)` | Download progress. Fires repeatedly. | -| `UpdateProgress` | `(tsiis)` | Flash progress. Fires repeatedly. | From 6fa433d4217088927041df7f7ee5a169ce75b28c Mon Sep 17 00:00:00 2001 From: mkadinti <101405874+mkadinti@users.noreply.github.com> Date: Sat, 2 May 2026 01:11:55 +0530 Subject: [PATCH 33/36] Delete librdkFwupdateMgr/docs/DOCUMENT_v1.md --- librdkFwupdateMgr/docs/DOCUMENT_v1.md | 998 -------------------------- 1 file changed, 998 deletions(-) delete mode 100755 librdkFwupdateMgr/docs/DOCUMENT_v1.md diff --git a/librdkFwupdateMgr/docs/DOCUMENT_v1.md b/librdkFwupdateMgr/docs/DOCUMENT_v1.md deleted file mode 100755 index 9f8c6452..00000000 --- a/librdkFwupdateMgr/docs/DOCUMENT_v1.md +++ /dev/null @@ -1,998 +0,0 @@ -# librdkFwupdateMgr — Design, Code Documentation & Logging Architecture - -> **Version**: v1.0 -> **Date**: April 28, 2026 -> **Scope**: Pull Request documentation for the `librdkFwupdateMgr` shared library -> **Author**: Senior Engineer — rdkFwupdateMgr Team - ---- - -## Table of Contents - -1. [Executive Summary](#1-executive-summary) -2. [High-Level Architecture](#2-high-level-architecture) -3. [File Structure](#3-file-structure) -4. [API-by-API Design](#4-api-by-api-design) - - 4.1 [registerProcess()](#41-registerprocessprocessname-libversion--firmwareinterfacehandle) - - 4.2 [checkForUpdate()](#42-checkforupdatehandle-callback--checkforupdateresult) - - 4.3 [downloadFirmware()](#43-downloadfirmwarehandle-fwdwnlreq-callback--downloadresult) - - 4.4 [updateFirmware()](#44-updatefirmwarehandle-fwupdatereq-callback--updateresult) - - 4.5 [unregisterProcess()](#45-unregisterprocesshandle) -5. [Internal Async Engine Design](#5-internal-async-engine-design) - - 5.1 [Three Callback Registries](#51-three-callback-registries) - - 5.2 [Background Thread](#52-background-thread) - - 5.3 [Two-Phase Signal Dispatch (Deadlock Prevention)](#53-two-phase-signal-dispatch-deadlock-prevention) - - 5.4 [Per-Call D-Bus Connections (Stateless Model)](#54-per-call-d-bus-connections-stateless-model) -6. [Memory Management](#6-memory-management) -7. [Thread Safety](#7-thread-safety) -8. [Error Handling Strategy](#8-error-handling-strategy) -9. [In-Code Documentation Guidelines](#9-in-code-documentation-guidelines) - - 9.1 [rdkFwupdateMgr_process.c — Key Inline Comments](#91-rdkfwupdatemgr_processc--key-inline-comments) - - 9.2 [rdkFwupdateMgr_api.c — Key Inline Comments](#92-rdkfwupdatemgr_apic--key-inline-comments) - - 9.3 [rdkFwupdateMgr_async.c — Key Inline Comments](#93-rdkfwupdatemgr_asyncc--key-inline-comments) -10. [Logging Architecture](#10-logging-architecture) - - 10.1 [Three Log Modules](#101-three-log-modules) - - 10.2 [Macro Definitions](#102-macro-definitions) - - 10.3 [Log Initialization Ownership](#103-log-initialization-ownership) - - 10.4 [Includes Required](#104-includes-required) - - 10.5 [Build Configuration (Makefile.am)](#105-build-configuration-makefileam) - - 10.6 [Sample Log Output](#106-sample-log-output) - ---- - -## 1. Executive Summary - -`librdkFwupdateMgr` is a shared library (`.so`) that provides a simple C API for client applications to perform firmware updates on RDK devices. It communicates with the `rdkFwupdateMgr` daemon over D-Bus. The library hides all D-Bus complexity — clients just call functions and receive results via callbacks. - -**In simple terms**: Think of it like ordering food through a delivery app. -- You place an order (`registerProcess`) and get an order number (`handle`). -- You ask "is my food ready?" (`checkForUpdate`) and get a notification later (`callback`). -- You say "deliver it" (`downloadFirmware`) and track the delivery (`progress callbacks`). -- You say "serve it" (`updateFirmware`) and watch it being plated (`progress callbacks`). -- When done, you close the app (`unregisterProcess`). - -The library handles all the complicated behind-the-scenes communication (D-Bus IPC) with the daemon that actually does the firmware work. - ---- - -## 2. High-Level Architecture - -``` -┌─────────────────────────────────────────────────────────────┐ -│ CLIENT APPLICATION │ -│ (e.g., example_plugin) │ -│ │ -│ main() │ -│ ├─ registerProcess("MyApp", "1.0") → handle "12345" │ -│ ├─ checkForUpdate(handle, my_cb) → returns immediately │ -│ │ [waits on condvar] │ -│ │ ◄── my_cb(fwinfo) fires in BG thread │ -│ ├─ downloadFirmware(handle, req, dl_cb) → returns immed. │ -│ │ [waits on condvar] │ -│ │ ◄── dl_cb(progress%, status) fires repeatedly │ -│ ├─ updateFirmware(handle, req, upd_cb) → returns immed. │ -│ │ [waits on condvar] │ -│ │ ◄── upd_cb(progress%, status) fires repeatedly │ -│ └─ unregisterProcess(handle) │ -│ │ -├──────────────── librdkFwupdateMgr.so ───────────────────────┤ -│ │ -│ PUBLIC LAYER (rdkFwupdateMgr_api.c, _process.c) │ -│ • Input validation │ -│ • D-Bus proxy creation (per-call, stateless) │ -│ • Fire-and-forget D-Bus method calls │ -│ • Callback registration in internal registries │ -│ │ -│ ASYNC ENGINE (rdkFwupdateMgr_async.c) │ -│ • 3 callback registries (Check, Download, Update) │ -│ • 1 background thread running GLib event loop │ -│ • D-Bus signal subscriptions (3 signals) │ -│ • Two-phase dispatch (snapshot → invoke) │ -│ │ -├──────────────── D-Bus (system bus) ──────────────────────────┤ -│ │ -│ rdkFwupdateMgr DAEMON │ -│ • Registered as org.rdkfwupdater.Service │ -│ • Processes method calls (Register, Check, Download, │ -│ Update, Unregister) │ -│ • Emits signals back (CheckForUpdateComplete, │ -│ DownloadProgress, UpdateProgress) │ -└─────────────────────────────────────────────────────────────┘ -``` - -### How D-Bus Fits In (For Newcomers) - -D-Bus is a messaging system used on Linux devices for programs to talk to each other. Think of it like a shared phone line between applications: - -- The **daemon** (server) sits on the phone line and has a well-known name: `org.rdkfwupdater.Service` -- The **library** (client) calls methods on the daemon (like dialing a number and asking a question) -- The **daemon** sends **signals** back (like broadcast notifications) when work is done -- The **library's background thread** listens for these signals and routes them to the right callback - ---- - -## 3. File Structure - -| File | Layer | Purpose | -|------|-------|---------| -| `include/rdkFwupdateMgr_client.h` | **Public API** | All types, enums, callbacks, and function declarations a client needs. This is the ONLY header clients include. | -| `src/rdkFwupdateMgr_process.c` | **Public** | `registerProcess()` and `unregisterProcess()` — synchronous D-Bus calls | -| `src/rdkFwupdateMgr_api.c` | **Public** | `checkForUpdate()`, `downloadFirmware()`, `updateFirmware()` — async fire-and-forget APIs | -| `src/rdkFwupdateMgr_async.c` | **Internal** | The async engine — registries, background thread, signal handlers, dispatch logic | -| `src/rdkFwupdateMgr_async_internal.h` | **Internal** | Internal types and declarations — NOT part of public API, NOT shipped to clients | -| `src/rdkFwupdateMgr_log.h` | **Internal** | `FWUPMGR_*` logging macros (library code uses these) | -| `examples/example_app.c` | **Example** | Reference client demonstrating the full register→check→download→flash→unregister workflow | - -### What's Public vs Internal? - -- **Public** (`include/` folder): Headers that client applications `#include`. These define the API contract. Changing these requires a version bump. -- **Internal** (`src/` folder): Implementation details. Clients never see these. We can change them freely without breaking clients. - ---- - -## 4. API-by-API Design - -### 4.1 `registerProcess(processName, libVersion)` → `FirmwareInterfaceHandle` - -**What it does in plain English**: Tells the daemon "Hello, I'm a new client named [processName]. Please give me a session ID so I can do firmware operations." - -**Returns**: A string like `"12345"` (the session ID). Returns `NULL` on failure. - -#### Design Decisions - -- **Synchronous** — blocks until daemon responds (~5-10ms). Registration is fast so blocking is acceptable. -- Creates a **fresh D-Bus proxy per call** (stateless, no persistent connection). This means each call to any API gets a different D-Bus sender ID, which is a key characteristic of this library model. -- Daemon returns a `uint64` handler_id; library converts it to a `malloc()`'d string (e.g., `"12345"`). String representation provides ABI stability. -- Calls `internal_system_init()` to start the background thread that will listen for daemon signals. -- If `malloc()` fails after registration succeeds on the daemon side, performs **best-effort cleanup** by calling `UnregisterProcess` on the daemon to avoid leaking a registration. - -#### D-Bus Protocol - -``` -Method: RegisterProcess(s processName, s libVersion) → (t handler_id) -``` - -#### Flow - -``` -Client Thread Daemon -───────────── ────── -registerProcess("MyApp", "1.0") - │ - ├─ Validate processName (not NULL, not empty, ≤256 chars) - ├─ Validate libVersion (not NULL, ≤64 chars) - ├─ Create D-Bus proxy → connect to system bus - ├─ Call RegisterProcess("MyApp", "1.0") ──────────► Daemon receives call - │ │ Creates ProcessInfo - │ │ Assigns handler_id=12345 - │◄──── Returns handler_id=12345 ─────────────────────┘ - ├─ Convert 12345 → malloc'd string "12345" - ├─ internal_system_init() - │ ├─ Init 3 callback registries (mutexes + arrays) - │ ├─ Create isolated GLib context + event loop - │ ├─ Spawn background thread - │ └─ Wait until background thread is ready (signal subscriptions live) - └─ Return "12345" to caller -``` - -#### Lifecycle Impact - -This is the **start** of the library's lifecycle. Before `registerProcess()`: -- No background thread exists -- No callback registries exist -- No D-Bus signal subscriptions exist - -After `registerProcess()` returns successfully, the library is fully operational and ready for async API calls. - ---- - -### 4.2 `checkForUpdate(handle, callback)` → `CheckForUpdateResult` - -**What it does in plain English**: Asks the daemon "Is there new firmware available for this device?" The answer comes later through your callback — this function returns immediately. - -**Returns**: `CHECK_FOR_UPDATE_SUCCESS` (request started) or `CHECK_FOR_UPDATE_FAIL` (couldn't even start). - -#### Design Decisions - -- **Non-blocking** (fire-and-forget) — returns immediately after sending the D-Bus message -- **Callback fires exactly once** — when the daemon finishes querying the XConf server (5-30 seconds later) -- The callback receives a `FwInfoData` struct with firmware version info and update details - -#### The Connect → Register → Send Ordering - -This is a critical design detail: - -``` -Step 1: Connect to D-Bus ← Fail fast if daemon/bus is down -Step 2: Register callback ← Now we're ready to receive the signal -Step 3: Send method call ← Daemon starts working -``` - -**Why this order?** -- If we registered the callback first but D-Bus connect fails, we'd have a stale PENDING entry in the registry that would never be dispatched (no signal will ever arrive for it). -- If we sent the method first but hadn't registered the callback yet, the daemon might emit the signal before our callback is registered — the signal arrives, no matching entry found, result is silently lost. -- Connecting first, then registering, then sending gives us the safest ordering with the smallest race window. - -#### D-Bus Protocol - -``` -Method: CheckForUpdate(s handle) — fire-and-forget, no reply waited for -Signal: CheckForUpdateComplete(t handler_id, i result, i status, s currentVer, - s availableVer, s updateDetails, s message) -``` - -#### Flow - -``` -Client Thread Background Thread Daemon -───────────── ───────────────── ────── -checkForUpdate("12345", my_cb) - │ - ├─ Validate handle, callback - ├─ Connect to D-Bus - ├─ Register my_cb in CheckForUpdate registry - │ (slot state: IDLE → PENDING) - ├─ Fire-and-forget: CheckForUpdate("12345") ──────────► Daemon receives - └─ Return SUCCESS immediately │ Queries XConf... - │ (5-30 seconds) - [client is free to do │ - other work or wait] │ - │ - ◄── CheckForUpdateComplete ────┘ (signal emitted) - on_check_complete_signal(): - ├─ Parse GVariant → InternalSignalData - ├─ dispatch_all_pending(): - │ Phase 1 (mutex held): - │ Snapshot PENDING entries - │ Mark → DISPATCHED - │ Phase 2 (no mutex): - │ Build FwInfoData from signal - │ Call my_cb(&fwinfo_data) ──► Client's callback runs - │ Reset slot → IDLE - └─ Cleanup signal data -``` - -#### What the Callback Receives - -```c -typedef struct { - char CurrFWVersion[64]; // e.g., "1.0.0" - UpdateDetails *UpdateDetails; // Non-NULL only if FIRMWARE_AVAILABLE - CheckForUpdateStatus status; // FIRMWARE_AVAILABLE, NOT_AVAILABLE, etc. -} FwInfoData; -``` - -**Important**: The `FwInfoData` pointer and all its contents are only valid DURING the callback invocation. If you need the data later, copy it (which is exactly what `example_app.c` does with `strncpy` to globals). - ---- - -### 4.3 `downloadFirmware(handle, fwdwnlreq, callback)` → `DownloadResult` - -**What it does in plain English**: Tells the daemon "Download this firmware file from the server." Returns immediately; your callback fires repeatedly with progress updates (0%, 25%, 50%, 75%, 100%). - -**Returns**: `RDKFW_DWNL_SUCCESS` (download started) or `RDKFW_DWNL_FAILED` (couldn't start). - -#### Design Decisions - -- Same fire-and-forget pattern as `checkForUpdate()` -- **Key difference**: Callback fires **multiple times** (once per progress signal from daemon) -- Registry slot stays **ACTIVE** across all progress signals; only resets to IDLE on `DWNL_COMPLETED` or `DWNL_ERROR` -- Download URL can be `NULL`/empty string — daemon will use the URL from the XConf query - -#### D-Bus Protocol - -``` -Method: DownloadFirmware(s handle, s firmwareName, s downloadUrl, s TypeOfFirmware) -Signal: DownloadProgress(t handler_id, s firmwareName, u progress%, s status, s message) -``` - -#### Flow - -``` -Client Thread Background Thread Daemon -───────────── ───────────────── ────── -downloadFirmware("12345", &req, my_dl_cb) - │ - ├─ Validate handle, req, callback - ├─ Connect to D-Bus - ├─ Register my_dl_cb in Download registry - │ (slot state: IDLE → ACTIVE) - ├─ Fire-and-forget: DownloadFirmware(...) ─────────► Daemon starts download - └─ Return SUCCESS immediately │ - │ Download progress... - ◄── DownloadProgress(10%) ──┘ - dispatch_all_dwnl_active(): - Call my_dl_cb(10, IN_PROGRESS) ──► prints "10%" - (slot stays ACTIVE) - - ◄── DownloadProgress(50%) ── - Call my_dl_cb(50, IN_PROGRESS) ──► prints "50%" - - ◄── DownloadProgress(100%) ── - Call my_dl_cb(100, COMPLETED) ──► prints "100% done!" - Reset slot → IDLE (download finished) -``` - ---- - -### 4.4 `updateFirmware(handle, fwupdatereq, callback)` → `UpdateResult` - -**What it does in plain English**: Tells the daemon "Flash this downloaded firmware onto the device's storage." This modifies the device firmware. Returns immediately; callback fires with progress. - -**Returns**: `RDKFW_UPDATE_SUCCESS` (flash started) or `RDKFW_UPDATE_FAILED` (couldn't start). - -#### Design Decisions - -- Identical pattern to `downloadFirmware()` — ACTIVE slot, multiple callbacks, reset on terminal status -- `LocationOfFirmware` can be `NULL`/empty — daemon uses the default path from `/etc/device.properties` -- `rebootImmediately` is a `bool` in the struct but sent to daemon as string `"true"`/`"false"` (daemon D-Bus method expects string) -- This operation is **irreversible** once the flash starts — client should verify the firmware file first - -#### D-Bus Protocol - -``` -Method: UpdateFirmware(s handle, s firmwareName, s location, s type, s rebootImmediately) -Signal: UpdateProgress(t handler_id, s firmwareName, i progress%, i status, s message) -``` - ---- - -### 4.5 `unregisterProcess(handle)` - -**What it does in plain English**: Tells the daemon "I'm done, please clean up my registration." Also frees all library resources (background thread, registries, handle memory). - -**Returns**: Nothing (`void`). This is a best-effort cleanup. - -#### Design Decisions - -- **Synchronous** — blocks until daemon responds -- **Best-effort** — if D-Bus call fails, local cleanup still happens (daemon may have already cleaned up on its own) -- Calls `internal_system_deinit()` **FIRST** — stops background thread, frees registries, destroys mutexes -- Then sends D-Bus `UnregisterProcess`, then `free(handle)` -- **Idempotent**: safe to call with `NULL` handle (no-op) -- Strictly validates handle string: must be pure decimal digits, no leading/trailing whitespace, no garbage characters - -#### Why deinit Before D-Bus Call? - -After unregister, the daemon won't send any more signals for this client. There's no point keeping the background thread alive. Shutting it down first ensures a clean `pthread_join()` without waiting for signals that will never arrive. - -#### Flow - -``` -Client Thread Daemon -───────────── ────── -unregisterProcess("12345") - │ - ├─ Validate handle (not NULL) - ├─ Parse "12345" → uint64 12345 - ├─ internal_system_deinit() - │ ├─ g_main_loop_quit() → background thread wakes up - │ ├─ pthread_join() → wait for background thread to exit - │ ├─ Free GLib resources (loop, context) - │ ├─ Free download & update registries - │ ├─ Free check registry (any leftover handle_key strings) - │ └─ Destroy all mutexes - ├─ Create D-Bus proxy - ├─ Call UnregisterProcess(12345) ──────────────► Daemon removes ProcessInfo - │◄──── Returns success=true ──────────────────┘ - ├─ free(handle) ← The "12345" string is freed - └─ Return -``` - ---- - -## 5. Internal Async Engine Design - -### 5.1 Three Callback Registries - -The library maintains **three independent registries**, one per async API. They are separate because each has a different callback lifecycle: - -| Registry | C Type | Slot Lifecycle | How Many Times Callback Fires | -|----------|--------|----------------|-------------------------------| -| `CallbackRegistry` | `g_registry` | IDLE → PENDING → DISPATCHED → IDLE | **Once** (check result) | -| `DwnlCallbackRegistry` | `g_dwnl_registry` | IDLE → ACTIVE → IDLE | **Multiple** (progress updates) | -| `UpdateCbRegistry` | `g_update_registry` | IDLE → ACTIVE → IDLE | **Multiple** (progress updates) | - -Each registry is a **fixed-size array** of `MAX_PENDING_CALLBACKS` (30) slots. This means at most 30 concurrent pending callbacks across all clients of any given type. - -Each slot holds: -- **State**: IDLE, PENDING, ACTIVE, DISPATCHED, or TIMED_OUT -- **handle_key**: `strdup()`'d copy of the app's handle string -- **callback**: Function pointer to the app's callback -- **registered_time**: Timestamp for timeout detection - -Each registry has its **own `pthread_mutex_t`** — so checkForUpdate registrations don't block download progress dispatching. - -#### State Machine - -``` -CheckForUpdate slot: - IDLE ──(checkForUpdate called)──► PENDING ──(signal arrives)──► DISPATCHED ──► IDLE - └──(timeout)──► TIMED_OUT ──► IDLE - -Download/Update slot: - IDLE ──(download/updateFirmware called)──► ACTIVE ──(COMPLETED/ERROR)──► IDLE - │ - │ (fires callback on EVERY - │ progress signal while ACTIVE) - │ - └──(timeout)──► TIMED_OUT ──► IDLE -``` - -### 5.2 Background Thread - -A single `pthread` runs a private GLib event loop for the entire lifetime of the library (from `registerProcess()` to `unregisterProcess()`). - -#### Thread Startup Sequence - -``` -1. g_main_context_push_thread_default() - └─ Creates an ISOLATED GLib context for this thread - (won't interfere with app's own GLib loop if it has one) - -2. g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) - └─ Connect to system D-Bus (this is the background thread's - OWN connection — different from the per-call connections - used by the public API functions) - -3. g_dbus_connection_signal_subscribe() × 3 - ├─ CheckForUpdateComplete → on_check_complete_signal() - ├─ DownloadProgress → on_download_progress_signal() - └─ UpdateProgress → on_update_progress_signal() - -4. g_bg_thread.running = true - └─ Main thread sees this and stops spin-waiting - -5. g_main_loop_run() - └─ BLOCKS here until internal_system_deinit() calls g_main_loop_quit() - GLib dispatches signal callbacks within this loop. -``` - -#### Why Spin-Wait Instead of Condvar? - -The main thread waits for the background thread to be ready by polling `g_bg_thread.running` every 100ms. A condvar would be slightly more elegant, but: - -1. The wait is typically <100ms (thread starts fast) -2. Adding a condvar adds complexity and another resource to manage/destroy -3. This only happens **once per library lifetime** (at `registerProcess` time) -4. Max wait: 50 × 100ms = 5 seconds — if the thread hasn't started by then, something is seriously wrong - -### 5.3 Two-Phase Signal Dispatch (Deadlock Prevention) - -All three signal handlers use the same two-phase pattern. This is the most important design detail in the async engine. - -#### The Problem - -If we held the registry mutex while invoking a callback, and that callback called `checkForUpdate()` again, it would try to lock the same mutex → **deadlock**. - -#### The Solution - -``` -PHASE 1 — Snapshot (mutex HELD): - ├─ Scan registry for matching entries (PENDING or ACTIVE) - ├─ Copy callback pointer + handle into a local stack array (snapshot) - ├─ Mark slots appropriately (DISPATCHED for check, leave ACTIVE for download/update) - └─ RELEASE mutex - -PHASE 2 — Invoke (NO mutex held): - ├─ Build result struct from signal data - ├─ For each snapshot entry: - │ ├─ Call callback(result_data) - │ └─ If terminal state (COMPLETED/ERROR): re-lock mutex, reset slot → IDLE, unlock - └─ Done -``` - -**Why is this safe?** -- During Phase 2, the mutex is released, so if a callback calls `checkForUpdate()` → `internal_register_callback()`, it can acquire the mutex without deadlock. -- The snapshot is a local stack array, so even if the registry changes during Phase 2, our snapshot is stable. - -### 5.4 Per-Call D-Bus Connections (Stateless Model) - -Each public API call (`registerProcess`, `checkForUpdate`, etc.) creates a **fresh D-Bus connection and proxy** for that single call, then immediately releases it via `g_object_unref()`. - -#### Implications - -- **No persistent connection** between the client and daemon at the API layer -- Each call may get a **different D-Bus sender ID** (e.g., `:1.140` for register, `:1.141` for checkForUpdate, `:1.145` for unregister) -- The daemon **cannot rely on sender ID** to identify a client across calls — only the `handler_id` (the numeric handle) is stable -- This is why the daemon's `UnregisterProcess` handler doesn't validate sender-ID ownership — it would always fail because unregister comes from a different sender than register - -The background thread has its **OWN persistent connection** (for signal subscriptions), but the API-calling thread uses ephemeral connections. - ---- - -## 6. Memory Management - -| Resource | Owner | Allocation | Deallocation | -|----------|-------|------------|--------------| -| `FirmwareInterfaceHandle` (the `"12345"` string) | Library | `malloc()` in `registerProcess()` | `free()` in `unregisterProcess()` | -| Registry `handle_key` entries | Async engine | `strdup()` on callback registration | `free()` on slot reset to IDLE | -| D-Bus proxy/connection (per API call) | Caller's stack | `g_bus_get_sync()` + `g_dbus_proxy_new_sync()` | `g_object_unref()` at end of each API function | -| D-Bus connection (background thread) | Background thread | `g_bus_get_sync()` in thread func | `g_object_unref()` on thread exit | -| GMainLoop, GMainContext | Async engine | `g_main_loop_new()` / `g_main_context_new()` | `g_main_loop_unref()` / `g_main_context_unref()` in `deinit()` | -| `FwInfoData` + `UpdateDetails` in dispatch | Stack-allocated | `dispatch_all_pending()` local variables | Automatic (function returns) | -| `InternalSignalData` strings | Parse function | `strdup()` from GVariant data | `free()` in `internal_cleanup_signal_data()` | - -### Rules for Client Developers - -1. **Never `free()` the handle yourself.** Call `unregisterProcess()` and it handles everything. -2. **Copy callback data if you need it later.** The `FwInfoData*` pointer in your callback is only valid during the callback invocation. Use `strncpy()` to save values to your own buffers. -3. **Don't call library APIs from inside a callback.** The callback runs in the background thread. Re-entering the library is technically safe (due to two-phase dispatch) for `checkForUpdate`-style calls, but it's better practice to signal your main thread and make calls from there. - ---- - -## 7. Thread Safety - -| Operation | Thread-Safe? | Notes | -|-----------|-------------|-------| -| `registerProcess()` | Yes | Stateless per-call; GDBus sync calls are thread-safe | -| `unregisterProcess()` | Per-handle | Don't unregister the same handle from two threads simultaneously | -| `checkForUpdate()` | Yes | Registry mutex protects slot allocation | -| `downloadFirmware()` | Yes | Separate registry with its own mutex | -| `updateFirmware()` | Yes | Separate registry with its own mutex | -| Callbacks | N/A | Fire in background thread; app must use its own synchronization (mutex + condvar) to coordinate with main thread | - -### Callback Threading Model - -``` -Main Thread Background Thread -─────────── ───────────────── - on_check_complete_signal() - └─ dispatch_all_pending() - └─ your_callback(&fwinfo) ← RUNS HERE - ├─ Copy data to globals - ├─ pthread_mutex_lock(&your_mutex) - ├─ your_done_flag = 1 - ├─ pthread_cond_signal(&your_cond) - └─ pthread_mutex_unlock(&your_mutex) - -pthread_mutex_lock(&your_mutex) -while (!your_done_flag) - pthread_cond_timedwait(...) ← WAKES UP HERE -pthread_mutex_unlock(&your_mutex) -// Now use the copied data safely -``` - -This is exactly how `example_app.c` works — see `g_check_mutex`/`g_check_cond`/`g_check_done`. - ---- - -## 8. Error Handling Strategy - -The library follows a defense-in-depth approach: - -### Layer 1: Input Validation (API Boundary) - -Every public function validates ALL parameters before touching D-Bus: -- NULL checks on handles, callbacks, request structs -- Empty string checks -- Length limit checks (process name ≤256 chars, version ≤64 chars) -- Numeric validity for handle parsing in `unregisterProcess()` (uses `strtoull` with strict `endptr` checking) - -### Layer 2: D-Bus Connection Failures - -- Connection attempt happens BEFORE callback registration (Connect → Register → Send pattern) -- If D-Bus is down, function returns error immediately with no stale registry entries - -### Layer 3: Daemon Errors - -- D-Bus errors from the daemon are caught via `GError` -- Error message is logged via `FWUPMGR_ERROR` -- Error is propagated as a return code to the caller - -### Layer 4: Resource Leak Prevention - -- In `registerProcess()`: If registration succeeds on daemon but `malloc()` fails locally, a cleanup proxy is created to send `UnregisterProcess` to prevent leaking the registration -- In `unregisterProcess()`: Handle memory is freed regardless of whether the D-Bus call succeeds -- In all API functions: D-Bus connections/proxies are freed on all code paths (success and error) - -### Layer 5: Best-Effort Cleanup - -- `unregisterProcess()` is best-effort: if D-Bus call fails, local cleanup still happens -- This handles the case where the daemon has already crashed or been restarted - ---- - -## 9. In-Code Documentation Guidelines - -The source code already has extensive inline comments. Below are the **key documentation points** that every reviewer should understand, organized by file. - -### 9.1 `rdkFwupdateMgr_process.c` — Key Inline Comments - -#### `registerProcess()` — Why `internal_system_init()` is here - -```c -/* - * Start the background listener thread NOW (not at library load time). - * - * WHY HERE and not in __attribute__((constructor))? - * The constructor approach is #if 0'd out in rdkFwupdateMgr_api.c. - * We start the background thread at register time because: - * 1. The handle must exist before any async API can be called - * 2. The background thread needs a live D-Bus connection — doing it - * too early risks connecting before the system bus is ready - * (common during early boot on embedded devices) - * 3. It pairs naturally with internal_system_deinit() in unregisterProcess() - * - * This initializes: - * - CallbackRegistry (checkForUpdate callbacks) - * - DwnlCallbackRegistry (download callbacks) - * - UpdateCbRegistry (update callbacks) - * - BackgroundThread (GLib event loop, D-Bus signal subscriptions) - */ -``` - -#### `unregisterProcess()` — Why `internal_system_deinit()` comes before D-Bus call - -```c -/* - * Stop the background listener thread BEFORE sending UnregisterProcess. - * - * WHY BEFORE the D-Bus call? - * After unregister, the daemon won't send us any more signals. - * There's no point keeping the background thread alive. - * Shutting down first ensures a clean pthread_join() without - * waiting for signals that will never arrive. - * - * This tears down: - * - g_main_loop_quit() → background thread exits g_main_loop_run() - * - pthread_join() → waits for clean exit - * - Frees all 3 registries (any leftover handle_key strings) - * - Destroys all mutexes - */ -``` - -### 9.2 `rdkFwupdateMgr_api.c` — Key Inline Comments - -#### Connect → Register → Send ordering rationale - -```c -/* - * ORDERING MATTERS: Connect → Register → Send - * - * We could register the callback first, but then if D-Bus connection - * fails, we'd have a stale PENDING entry in the registry that would - * never be dispatched (no signal will ever arrive for it). - * - * We could send first, but then the daemon might emit the signal - * before our callback is registered → signal arrives, no matching - * entry found, result is silently lost. - * - * The correct order is: - * 1. Connect (fail fast if daemon/D-Bus is down) - * 2. Register callback (now we're ready to receive) - * 3. Send the method call (daemon starts working) - * - * The window between register and send is microseconds — acceptably - * small race window where the callback slot exists but the daemon - * hasn't been asked yet. - */ -``` - -### 9.3 `rdkFwupdateMgr_async.c` — Key Inline Comments - -#### Two-Phase Dispatch — Why we release the mutex before calling callbacks - -```c -/* - * TWO-PHASE DESIGN — avoids deadlock: - * - * PHASE 1 (mutex held): - * Scan registry → snapshot all PENDING entries into local array. - * Mark each found entry as DISPATCHED. - * Release mutex. - * - * PHASE 2 (mutex released): - * Build FwInfoData from signal_data. - * Invoke each snapshot callback: callback(&fwinfo_data) - * Re-acquire mutex briefly to reset each slot to IDLE. - * - * WHY RELEASE BEFORE CALLING CALLBACKS? - * If a callback called checkForUpdate() again, it would call - * internal_register_callback() which tries to lock the same mutex - * → deadlock. Releasing first makes re-entrant use safe. - */ -``` - -#### Background thread spin-wait rationale - -```c -/* - * Spin-wait for background thread to set running=true. - * Max wait: 50 × 100ms = 5 seconds. - * - * WHY SPIN-WAIT instead of condvar? - * 1. The wait is typically <100ms (thread starts fast) - * 2. Adding a condvar adds complexity and another resource to manage - * 3. This only happens once per library lifetime - * 4. 100ms sleep granularity is fine for a one-time init - * - * Ensures D-Bus signal subscription is live before checkForUpdate() - * can send a D-Bus method call — prevents missing the response signal. - */ -``` - ---- - -## 10. Logging Architecture - -### 10.1 Three Log Modules - -The system uses three distinct RDK_LOGGER modules so log output can be filtered by component: - -| Module Name | Macro Prefix | Used By | Log Tag in Output | -|------------|--------------|---------|-------------------| -| `LOG.RDK.FWUPMGR` | `FWUPMGR_*` | Library code (`_process.c`, `_api.c`, `_async.c`) | `[FWUPMGR]` | -| `LOG.RDK.FWUPG` | `SWLOG_*` | Daemon code (`rdkv_dbus_server.c`, `rdkv_main.c`, etc.) | `[FWUPG]` | -| `LOG.RDK.EXAMPLE` | `EXAMPLE_*` | Example app (`example_app.c`) | `[EXAMPLE]` | - -**Why three separate modules?** So you can filter logs in production: -- To see only library-side issues: `grep "\[FWUPMGR\]" /opt/logs/rdkFwupdateMgr.log` -- To see only daemon-side issues: `grep "\[FWUPG\]" /opt/logs/rdkFwupdateMgr.log` -- To see only client app issues: `grep "\[EXAMPLE\]" /opt/logs/rdkFwupdateMgr.log` - -### 10.2 Macro Definitions - -#### Library Macros — `rdkFwupdateMgr_log.h` - -```c -/* ── Base macro — callers provide their own module name ── */ -#define FWUPMGR_LOG(level, module, format, ...) \ - RDK_LOG(level, module, format, ##__VA_ARGS__) - -/* ── Library convenience macros ── */ -/* Used in rdkFwupdateMgr_process.c, rdkFwupdateMgr_api.c, rdkFwupdateMgr_async.c */ -#define FWUPMGR_TRACE(format, ...) FWUPMGR_LOG(RDK_LOG_TRACE1, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -#define FWUPMGR_FATAL(format, ...) FWUPMGR_LOG(RDK_LOG_FATAL, "LOG.RDK.FWUPMGR", format, ##__VA_ARGS__) -``` - -#### Example App Macros — `example_app.c` - -```c -/* Reuses the FWUPMGR_LOG base macro but with a different module name */ -#define EXAMPLE_DEBUG(format, ...) FWUPMGR_LOG(RDK_LOG_DEBUG, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -#define EXAMPLE_INFO(format, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -#define EXAMPLE_WARN(format, ...) FWUPMGR_LOG(RDK_LOG_WARN, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -#define EXAMPLE_ERROR(format, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.EXAMPLE", format, ##__VA_ARGS__) -``` - -#### Non-RDK_LOGGER Fallback - -When `RDK_LOGGER` is **not** defined (unit tests, standalone development builds), macros fall back to `fprintf`: - -```c -#define FWUPMGR_LOG(level, module, FORMAT...) fprintf(stderr, "[%s] " FORMAT, module) -``` - -This means logging works everywhere — just with different backends depending on the build configuration. - -#### How to Define Your Own Module (For New Client Apps) - -If you're writing a new client application (not using example_app.c), define your own macros: - -```c -#include "rdkFwupdateMgr_log.h" // Get FWUPMGR_LOG base macro -#include "rdkv_cdl_log_wrapper.h" // Get log_init(), log_exit() - -// Define your own module — logs will appear as [MYAPP] -#define MYAPP_INFO(fmt, ...) FWUPMGR_LOG(RDK_LOG_INFO, "LOG.RDK.MYAPP", fmt, ##__VA_ARGS__) -#define MYAPP_ERROR(fmt, ...) FWUPMGR_LOG(RDK_LOG_ERROR, "LOG.RDK.MYAPP", fmt, ##__VA_ARGS__) - -int main(void) { - log_init(); // Initialize RDK logger - MYAPP_INFO("Starting up\n"); - // ... use library APIs ... - log_exit(); // Shutdown RDK logger -} -``` - -### 10.3 Log Initialization Ownership - -**The library does NOT own the log lifecycle.** The host application (whatever links to `librdkFwupdateMgr.so`) is responsible for calling: - -```c -log_init(); // BEFORE any library call (typically first line of main()) -log_exit(); // AFTER unregisterProcess() (typically last line before return) -``` - -Both `log_init()` and `log_exit()` are provided by `rdkv_cdl_log_wrapper.h` (in `common_utilities/utils/`). - -| Build Config | `log_init()` does | `log_exit()` does | -|-------------|-------------------|-------------------| -| `RDK_LOGGER` defined | Calls `rdk_logger_init()` with config file | Calls `rdk_logger_deinit()` | -| `RDK_LOGGER` not defined | No-op | No-op | - -**Why doesn't the library call `log_init()`?** -- A process should only call `log_init()` once. If the library called it, and the app also called it, that's a double-init which may cause issues. -- The app knows when it's ready to start logging. The library shouldn't make that decision. - -### 10.4 Includes Required - -#### For library source files (`_process.c`, `_api.c`, `_async.c`) - -```c -#include "rdkFwupdateMgr_log.h" // Provides FWUPMGR_* macros -``` - -That's all — the log header internally includes `rdkv_cdl_log_wrapper.h` and (if `RDK_LOGGER` is defined) `rdk_debug.h`. - -#### For client applications - -```c -#include "rdkFwupdateMgr_client.h" // Public API (types, functions) -#include "rdkFwupdateMgr_log.h" // FWUPMGR_LOG base macro (for defining your own module) -#include "rdkv_cdl_log_wrapper.h" // log_init(), log_exit() -``` - -### 10.5 Build Configuration (Makefile.am) - -The `example_plugin` target needs include paths for both log headers and link flags for the logger libraries: - -```makefile -# Include paths -example_plugin_CFLAGS += -I${top_srcdir}/librdkFwupdateMgr/src # rdkFwupdateMgr_log.h -example_plugin_CFLAGS += -I${top_srcdir}/common_utilities/utils # rdkv_cdl_log_wrapper.h - -# Link flags -example_plugin_LDADD += -lfwutils -lrdkloggers # log_init/log_exit implementations -``` - -### 10.6 Sample Log Output - -Below is what you'd see in `/opt/logs/rdkFwupdateMgr.log` during a typical firmware update workflow. Notice how the three tags (`[EXAMPLE]`, `[FWUPMGR]`, `[FWUPG]`) make it easy to trace what's happening at each layer: - -``` -[EXAMPLE] Application starting, PID: 1234 -[EXAMPLE] STEP 1: Register with firmware daemon -[FWUPMGR] registerProcess() called -[FWUPMGR] processName: 'ExampleApp' -[FWUPMGR] libVersion: '1.0.0' -[FWUPMGR] D-Bus proxy created successfully -[FWUPMGR] Calling RegisterProcess D-Bus method... -[FWUPG] [D-BUS] RegisterProcess received from ':1.140' -[FWUPG] [PROCESS_TRACKING] New client registered: ExampleApp (handler=12345) -[FWUPMGR] Registration successful -[FWUPMGR] handler_id: 12345 -[FWUPMGR] Handle created: '12345' -[FWUPMGR] internal_system_init: begin -[FWUPMGR] background_thread: starting -[FWUPMGR] background_thread: subscribed to CheckForUpdateComplete (id=1) -[FWUPMGR] background_thread: subscribed to DownloadProgress (id=2) -[FWUPMGR] background_thread: subscribed to UpdateProgress (id=3) -[FWUPMGR] internal_system_init: ready -[EXAMPLE] Registered successfully -[EXAMPLE] Handle: '12345' -[EXAMPLE] STEP 2: Check for firmware updates -[EXAMPLE] Calling checkForUpdate()... -[FWUPMGR] checkForUpdate: handle='12345' -[FWUPMGR] internal_register_callback: registered handle='12345' -[FWUPMGR] checkForUpdate: D-Bus call sent, returning SUCCESS. -[FWUPG] [D-BUS] CheckForUpdate received for handler 12345 -[FWUPG] [XCONF] Querying XConf server... -[FWUPG] [XCONF] Response: firmware_v2.bin available -[FWUPG] [SIGNAL] Emitting CheckForUpdateComplete -[FWUPMGR] on_check_complete_signal: received -[FWUPMGR] dispatch_all_pending: 1 callback(s) to fire -[FWUPMGR] dispatch_all_pending: invoking callback for handle='12345' -[EXAMPLE] checkForUpdate Callback Received -[EXAMPLE] Status Code: FIRMWARE_AVAILABLE (0) -[EXAMPLE] Current FW Version: 1.0.0 -[EXAMPLE] Available Version: 2.0.0 -[EXAMPLE] Firmware check data saved. Main thread will proceed. -[EXAMPLE] STEP 3: Download firmware image -[FWUPMGR] downloadFirmware: handle='12345' firmware='firmware_v2.bin' -[FWUPMGR] internal_dwnl_register_callback: registered handle='12345' -[FWUPMGR] downloadFirmware: D-Bus call sent, returning SUCCESS. -[FWUPMGR] on_download_progress_signal: progress=25% status='INPROGRESS' -[EXAMPLE] Download: 25% DWNL_IN_PROGRESS -[FWUPMGR] on_download_progress_signal: progress=50% status='INPROGRESS' -[EXAMPLE] Download: 50% DWNL_IN_PROGRESS -[FWUPMGR] on_download_progress_signal: progress=100% status='COMPLETED' -[EXAMPLE] Download: 100% DWNL_COMPLETED -[EXAMPLE] Download completed successfully! -``` - ---- - -## Appendix A: Complete Sequence Diagram - -``` - Main Thread Library BG Thread Daemon Process - ─────────── ───────────────── ────────────── - - [STEP 1: Register] - registerProcess("ExampleApp", "1.0.0") - │───── D-Bus: RegisterProcess ──────────────────► │ - │◄──── Returns handler_id=12345 ─────────────────┤ - g_handle = "12345" - internal_system_init() starts BG thread - │ subscribe CheckForUpdateComplete - │ subscribe DownloadProgress - │ subscribe UpdateProgress - │ running = true - │ g_main_loop_run() ← BLOCKS - - [STEP 2: Check for Update] - checkForUpdate("12345", on_check_cb) - │── register callback in g_registry - │───── D-Bus: CheckForUpdate("12345") ──────────► │ - │◄──── returns immediately │ query XConf... - │ waiting on condvar... │ - │ │ (5-30 seconds) - │ ◄── CheckForUpdateComplete ┤ - │ on_check_complete_signal(): │ - │ dispatch_all_pending() │ - │ on_check_cb(&fwinfo) ──►│ - │ signal condvar ───────►│ - │ condvar wakes up! │ - │ read g_check_status, g_fw_filename, etc. - - [STEP 3: Download] - downloadFirmware("12345", &req, on_dl_cb) - │── register callback in g_dwnl_registry - │───── D-Bus: DownloadFirmware(...) ────────────► │ - │◄──── returns immediately │ downloading... - │ waiting on condvar... │ - │ ◄── DownloadProgress(25%) ─┤ - │ on_dl_cb(25, IN_PROGRESS) │ - │ ◄── DownloadProgress(50%) ─┤ - │ on_dl_cb(50, IN_PROGRESS) │ - │ ◄── DownloadProgress(100%) ┤ - │ on_dl_cb(100, COMPLETED) │ - │ signal condvar ──────────►│ - │ condvar wakes up! - - [STEP 4: Flash] - updateFirmware("12345", &req, on_upd_cb) - │── register callback in g_update_registry - │───── D-Bus: UpdateFirmware(...) ──────────────► │ - │◄──── returns immediately │ flashing... - │ waiting on condvar... │ - │ ◄── UpdateProgress(50%) ──┤ - │ on_upd_cb(50, IN_PROGRESS) │ - │ ◄── UpdateProgress(100%) ─┤ - │ on_upd_cb(100, COMPLETED) │ - │ signal condvar ──────────►│ - │ condvar wakes up! - - [STEP 5: Unregister] - unregisterProcess("12345") - │── internal_system_deinit() - │ g_main_loop_quit() ──────────────► BG thread exits - │ pthread_join() ◄──────────────────┘ - │ free registries, destroy mutexes - │───── D-Bus: UnregisterProcess(12345) ─────────► │ - │◄──── Returns success=true ────────────────────┤ - │ free("12345") - │ done. -``` - ---- - -## Appendix B: D-Bus Interface Summary - -**Service**: `org.rdkfwupdater.Service` -**Object Path**: `/org/rdkfwupdater/Service` -**Interface**: `org.rdkfwupdater.Interface` - -### Methods (Client → Daemon) - -| Method | Signature | Description | -|--------|-----------|-------------| -| `RegisterProcess` | `(ss) → (t)` | Register client. Returns handler_id. | -| `UnregisterProcess` | `(t) → (b)` | Unregister client. Returns success. | -| `CheckForUpdate` | `(s)` | Fire-and-forget. No reply. | -| `DownloadFirmware` | `(ssss)` | Fire-and-forget. No reply. | -| `UpdateFirmware` | `(sssss)` | Fire-and-forget. No reply. | - -### Signals (Daemon → Client) - -| Signal | Signature | Description | -|--------|-----------|-------------| -| `CheckForUpdateComplete` | `(tiissss)` | Firmware check result. Fires once. | -| `DownloadProgress` | `(tsuss)` | Download progress. Fires repeatedly. | -| `UpdateProgress` | `(tsiis)` | Flash progress. Fires repeatedly. | From 2ee461388fb75bfe43bb594c1b016a2c1d2176fc Mon Sep 17 00:00:00 2001 From: mkadinti Date: Sat, 2 May 2026 05:40:16 +0000 Subject: [PATCH 34/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- modified code comments --- librdkFwupdateMgr/src/rdkFwupdateMgr_api.c | 640 +---------- librdkFwupdateMgr/src/rdkFwupdateMgr_async.c | 1021 +---------------- .../src/rdkFwupdateMgr_process.c | 736 +----------- 3 files changed, 94 insertions(+), 2303 deletions(-) diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index ff595656..eda8d15a 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -169,39 +169,12 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, UpdateEventCallback callback) { - /* - * [STEP 1] INPUT VALIDATION - * - * Reject obviously bad inputs before touching D-Bus or the registry. - * This is the library's input boundary -- validate everything here. - */ - - /* - * Check 1a: handle must not be NULL and must not be empty "". - * - * handle is the string "1" from registerProcess(). If the caller - * passes NULL (forgot to check registerProcess return value) or - * somehow has an empty string, reject immediately. - * - * handle[0] == '\0' catches the empty string case that a simple - * NULL check would miss. An empty handle would cause the daemon - * to reject the request anyway, but we catch it here to avoid - * a wasted D-Bus round-trip. - */ + /* Validate inputs */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("checkForUpdate: invalid handle (NULL or empty)\n"); return CHECK_FOR_UPDATE_FAIL; } - /* - * Check 1b: callback must not be NULL. - * - * If the caller passes NULL, they'll never receive the firmware - * check result. That's a programming error -- they probably forgot - * to pass their callback function. Catch it here with a clear - * error message rather than crashing later when we try to call - * through a NULL function pointer. - */ if (callback == NULL) { FWUPMGR_ERROR("checkForUpdate: callback is NULL\n"); return CHECK_FOR_UPDATE_FAIL; @@ -209,134 +182,25 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, FWUPMGR_INFO("checkForUpdate: handle='%s'\n", handle); - /* - * [STEP 2] CREATE EPHEMERAL D-BUS CONNECTION - * - * g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) opens a new connection to - * the system D-Bus bus. This connection gets a unique sender name - * like :1.142 -- different from every other connection. - * - * Why a NEW connection instead of reusing the BG thread's :1.141? - * The BG thread's connection is attached to the BG thread's - * GMainContext. Using it from the main thread would require - * cross-thread GLib context management -- complex and fragile. - * A fresh per-call connection is simpler and avoids any - * thread-safety issues with GLib internals. - * - * Why BEFORE registering the callback? - * If D-Bus is down (dbus-daemon crashed, socket missing), this - * call fails. We want to fail BEFORE polluting the callback - * registry with a PENDING entry that will never be dispatched. - * Clean failure: no registry entry, no dangling state. - * - * Cost: ~2ms for the D-Bus handshake. Negligible for a firmware - * check that takes 5-30 seconds total. - */ + /* Open ephemeral D-Bus connection (before registry to avoid ghost entries) */ GError *error = NULL; GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (conn == NULL) { - /* - * D-Bus connection failed. Common causes: - * - dbus-daemon not running - * - System bus socket missing (/var/run/dbus/system_bus_socket) - * - Permission denied (D-Bus policy rejects our user) - * - * Return FAIL -- the caller should check if the daemon is running. - * No registry entry was created, so nothing to clean up. - */ FWUPMGR_ERROR("checkForUpdate: D-Bus connect failed: %s\n", error ? error->message : "unknown"); if (error) g_error_free(error); return CHECK_FOR_UPDATE_FAIL; } - /* - * [STEP 3] REGISTER CALLBACK IN THE CHECK-FOR-UPDATE REGISTRY - * - * internal_register_callback() does the following (see _async.c): - * 1. Locks g_registry.mutex - * 2. Scans all 30 slots for: - * a. An existing PENDING entry with the same handle (dedup) - * b. The first IDLE slot (free slot) - * 3. If same handle found: overwrites it (prevents ghost callbacks) - * If free slot found: uses it - * If neither: returns false (registry full -- 30 concurrent checks!) - * 4. Populates the slot: - * - handle_key = strdup(handle) -- "1" (heap copy, freed on reset) - * - callback = our function pointer - * - state = CB_STATE_PENDING - * - registered_time = current unix timestamp - * 5. Unlocks g_registry.mutex - * 6. Returns true - * - * After this call, the registry has one PENDING entry. When the - * BG thread receives the "CheckForUpdateComplete" signal, it will - * find this entry and invoke the callback. - * - * Why BEFORE the D-Bus call? - * Race condition prevention. If the daemon responds faster than - * we can register (theoretically possible with cached results), - * the BG thread would find an empty registry and drop the signal. - * Registering first guarantees the callback is waiting. - * - * Failure case: registry full (30 concurrent pending checks). - * This means 30 different checkForUpdate() calls are all waiting - * for callbacks simultaneously. In practice this never happens -- - * a single client typically has 1 pending check at a time. - * If it does happen, we clean up the D-Bus connection and fail. - */ + /* Register callback BEFORE D-Bus call to prevent signal race */ if (!internal_register_callback(handle, callback)) { FWUPMGR_ERROR("checkForUpdate: registry full, handle='%s'\n", handle); g_object_unref(conn); return CHECK_FOR_UPDATE_FAIL; } - /* - * [STEP 4] SEND FIRE-AND-FORGET D-BUS METHOD CALL - * - * g_dbus_connection_call() sends a D-Bus method call to the daemon. - * This is the ASYNC variant (not _sync). But we're not even using - * the async callback -- all three trailing NULLs mean "don't tell - * me what the daemon replied." - * - * Parameters to g_dbus_connection_call(): - * conn -- our ephemeral connection :1.142 - * DBUS_SERVICE_NAME -- "org.rdkfwupdater.Service" (daemon's well-known name) - * DBUS_OBJECT_PATH -- "/org/rdkfwupdater/Service" (daemon's object path) - * DBUS_INTERFACE_NAME -- "org.rdkfwupdater.Interface" - * DBUS_METHOD_CHECK -- "CheckForUpdate" (the method name) - * g_variant_new("(s)", handle) -- argument: the string "1" - * "(s)" means a tuple containing one string - * This tells the daemon which registered client is asking - * NULL -- expected reply type: we don't care - * G_DBUS_CALL_FLAGS_NONE -- no special flags - * DBUS_TIMEOUT_MS -- 5000ms (only for message queueing, not for reply) - * NULL -- GCancellable: no cancellation support - * NULL -- GAsyncReadyCallback: no reply callback - * NULL -- user_data for reply callback: N/A - * - * The three trailing NULLs (cancellable, callback, user_data) are what - * make this fire-and-forget. GLib queues the D-Bus message in the - * kernel's socket buffer and returns immediately. The daemon's reply - * (which it does send -- an immediate FIRMWARE_CHECK_ERROR meaning - * "I'm working on it") arrives at :1.142 but nobody reads it because - * we close the connection moments later. - * - * WHAT HAPPENS ON THE DAEMON SIDE (for context): - * 1. Daemon receives "CheckForUpdate" with argument "1" - * 2. Validates handler "1" is registered (yes, from registerProcess) - * 3. Sends immediate method reply: FIRMWARE_CHECK_ERROR (= "check in progress") - * -- This reply is DISCARDED because we're fire-and-forget - * 4. Creates a GTask and spawns a worker thread to query XConf - * 5. Worker thread does HTTP GET to XConf server (5-30 seconds) - * 6. When XConf responds, daemon broadcasts "CheckForUpdateComplete" signal - * -- THIS is what our BG thread is waiting for - * - * The daemon may also PIGGYBACK: if another client already triggered - * an XConf fetch, our request joins the waiting queue and gets the - * same result when the fetch completes. One HTTP request serves all. - */ + /* Fire-and-forget D-Bus call (3 trailing NULLs = no reply) */ FWUPMGR_INFO("checkForUpdate: calling CheckForUpdate on daemon, handle='%s'\n", handle); @@ -355,47 +219,7 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, NULL /* user_data: none */ ); - /* - * [STEP 5] CLOSE EPHEMERAL CONNECTION AND RETURN - * - * g_object_unref(conn) closes our ephemeral D-Bus connection :1.142. - * The D-Bus message is already in the kernel socket buffer -- closing - * our end doesn't prevent delivery to the daemon. Think of it like - * dropping a letter in a mailbox and walking away. - * - * After this, the state of the world is: - * - * Main thread: - * - Returns CHECK_FOR_UPDATE_SUCCESS to the caller - * - Caller typically enters pthread_cond_timedwait (120s timeout) - * - Connection :1.142 is DEAD (just closed) - * - * BG thread: - * - Still sleeping in g_main_loop_run() on connection :1.141 - * - g_registry.entries[0] has our callback in PENDING state - * - Will wake up when CheckForUpdateComplete signal arrives - * - * Daemon: - * - Received our request, spawned XConf worker thread - * - Will broadcast signal when XConf responds (5-30s) - * - * Registry: - * entries[0] = { state=PENDING, handle_key="1", - * callback=on_firmware_check_callback } - * - * D-Bus connections: - * :1.140 -- registerProcess ephemeral (DEAD since registration) - * :1.141 -- BG thread persistent (ALIVE, listening for signals) - * :1.142 -- this checkForUpdate ephemeral (DEAD, just closed) - * - * SUCCESS here means: "I sent the request and registered your callback." - * It does NOT mean: "Firmware is available." or even "The daemon - * received the request." (Though it almost certainly did.) - * - * The callback will fire later on the BG thread. If it never fires - * (daemon crashed, XConf unreachable), the caller's condvar timeout - * will eventually expire and the caller can handle the timeout. - */ + /* Close ephemeral connection; message already in kernel socket buffer */ g_object_unref(conn); FWUPMGR_INFO("checkForUpdate: D-Bus call sent, returning SUCCESS. " @@ -546,214 +370,57 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, const FwDwnlReq *fwdwnlreq, DownloadCallback callback) { - /* - * [STEP 1] INPUT VALIDATION - * - * Reject obviously bad inputs before touching D-Bus or the registry. - * This is the library's input boundary -- validate everything here. - * downloadFirmware has MORE validation than checkForUpdate because - * it also validates the request struct fields (not just handle+callback). - */ - - /* - * Check 1a: handle must not be NULL and must not be empty "". - * - * handle is the string "1" from registerProcess(). If the caller - * passes NULL (forgot to register first) or an empty string, - * reject immediately. The daemon would reject it too, but we - * save the D-Bus round-trip. - */ + /* Validate inputs */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("downloadFirmware: invalid handle (NULL or empty)\n"); return RDKFW_DWNL_FAILED; } - /* - * Check 1b: the request struct pointer must not be NULL. - * - * This catches the case where the caller passes NULL instead of - * &download_req. Dereferencing NULL would crash. - */ if (fwdwnlreq == NULL) { FWUPMGR_ERROR("downloadFirmware: fwdwnlreq is NULL\n"); return RDKFW_DWNL_FAILED; } - /* - * Check 1c: firmwareName pointer must not be NULL. - * - * FwDwnlReq.firmwareName is a const char* -- it could be NULL if - * the caller forgot to set it. We need a filename to tell the - * daemon WHAT to download. - */ if (fwdwnlreq->firmwareName == NULL) { FWUPMGR_ERROR("downloadFirmware: firmwareName is NULL\n"); return RDKFW_DWNL_FAILED; } - /* - * Check 1d: firmwareName must not be an empty string "". - * - * An empty filename is meaningless -- the daemon can't download "". - * This catches the case where the caller did: - * download_req.firmwareName = ""; // accident - */ if (fwdwnlreq->firmwareName[0] == '\0') { FWUPMGR_ERROR("downloadFirmware: firmwareName is empty\n"); return RDKFW_DWNL_FAILED; } - /* - * Check 1e: callback must not be NULL. - * - * Without a callback, the app can't receive progress updates. - * It would never know when the download finishes. That's always - * a programming error. - */ if (callback == NULL) { FWUPMGR_ERROR("downloadFirmware: callback is NULL\n"); return RDKFW_DWNL_FAILED; } - /* - * Log what we're about to download. The ternary expressions handle - * optional fields: TypeOfFirmware and downloadUrl may be NULL - * (they're optional in FwDwnlReq). - */ FWUPMGR_INFO("downloadFirmware: handle='%s' firmware='%s' type='%s' url='%s'\n", handle, fwdwnlreq->firmwareName, (fwdwnlreq->TypeOfFirmware && fwdwnlreq->TypeOfFirmware[0]) ? fwdwnlreq->TypeOfFirmware : "(none)", (fwdwnlreq->downloadUrl && fwdwnlreq->downloadUrl[0]) ? fwdwnlreq->downloadUrl : "(use XConf)"); - /* - * [STEP 2] CREATE EPHEMERAL D-BUS CONNECTION - * - * g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) opens a new connection to - * the system D-Bus bus. Gets a unique sender name like :1.143. - * - * Why a NEW connection instead of reusing the BG thread's :1.141? - * The BG thread's connection is attached to the BG thread's - * GMainContext. Using it from the main thread would require - * cross-thread GLib context management -- complex and fragile. - * A fresh per-call connection is simpler and safe. - * - * Why BEFORE registering the callback? - * If D-Bus is down (dbus-daemon crashed, socket missing), this - * call fails. We want to fail BEFORE polluting the download - * registry with an ACTIVE entry that will never be dispatched. - * Clean failure: no registry entry, no dangling state. - * - * Cost: ~2ms for the D-Bus handshake. Negligible for a firmware - * download that takes minutes. - */ + /* Open ephemeral D-Bus connection (before registry to avoid ghost entries) */ GError *error = NULL; GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (conn == NULL) { - /* - * D-Bus connection failed. Common causes: - * - dbus-daemon not running - * - System bus socket missing (/var/run/dbus/system_bus_socket) - * - Permission denied (D-Bus policy rejects our user) - * - * Return FAILED -- no registry entry created, nothing to clean up. - */ FWUPMGR_ERROR("downloadFirmware: D-Bus connect failed: %s\n", error ? error->message : "unknown"); if (error) g_error_free(error); return RDKFW_DWNL_FAILED; } - /* - * [STEP 3] REGISTER CALLBACK IN THE DOWNLOAD REGISTRY (g_dwnl_registry) - * - * internal_dwnl_register_callback() does the following (see _async.c): - * 1. Locks g_dwnl_registry.mutex - * 2. Scans all 30 DwnlCallbackEntry slots for: - * a. An existing ACTIVE entry with the same handle (dedup/overwrite) - * b. The first IDLE slot (free slot) - * 3. If same handle found: overwrites it (prevents stale callbacks) - * If free slot found: uses it - * If neither: returns false (registry full -- 30 concurrent downloads!) - * 4. Populates the slot: - * - handle_key = strdup(handle) -- "1" (heap copy) - * - callback = our function pointer - * - state = DWNL_CB_STATE_ACTIVE -- NOTE: ACTIVE, not PENDING! - * - registered_time = current unix timestamp - * 5. Unlocks g_dwnl_registry.mutex - * 6. Returns true - * - * After this call, the registry has one ACTIVE entry. When the BG - * thread receives DownloadProgress signals, it will find this entry - * and invoke the callback on EVERY signal. - * - * STATE DIFFERENCE FROM checkForUpdate: - * checkForUpdate sets state = CB_STATE_PENDING (fires once) - * downloadFirmware sets state = DWNL_CB_STATE_ACTIVE (fires repeatedly) - * There is NO "DISPATCHED" intermediate state for download. - * - * Why BEFORE the D-Bus call? - * Race condition prevention. If the daemon starts downloading - * instantly (file already cached), the BG thread would receive - * the first DownloadProgress signal before we registered. - * dispatch_all_dwnl_active() would find zero ACTIVE entries - * and silently discard the signal. Our callback would never fire. - * - * Failure case: registry full (30 concurrent pending downloads). - * In practice never happens -- a device downloads one firmware - * at a time. If it does, clean up and fail. - */ + /* Register callback BEFORE D-Bus call to prevent signal race */ if (!internal_dwnl_register_callback(handle, callback)) { FWUPMGR_ERROR("downloadFirmware: registry full, handle='%s'\n", handle); g_object_unref(conn); return RDKFW_DWNL_FAILED; } - /* - * [STEP 4] SEND FIRE-AND-FORGET D-BUS METHOD CALL - * - * g_dbus_connection_call() sends a D-Bus method call to the daemon. - * - * Parameters to g_dbus_connection_call(): - * conn -- our ephemeral connection :1.143 - * DBUS_SERVICE_NAME -- "org.rdkfwupdater.Service" (daemon's well-known name) - * DBUS_OBJECT_PATH -- "/org/rdkfwupdater/Service" (object path) - * DBUS_INTERFACE_NAME -- "org.rdkfwupdater.Interface" - * DBUS_METHOD_DOWNLOAD -- "DownloadFirmware" (the method name) - * g_variant_new("(ssss)", ...) -- 4-string argument tuple: - * "(ssss)" means a tuple containing four strings - * string 1: handle -- "1" (which registered client is asking) - * string 2: firmwareName -- "firmware_v8.bin" (what to download) - * string 3: downloadUrl -- URL or "" (where to download from) - * string 4: TypeOfFirmware -- "PCI" or "" (firmware type category) - * NULL -- expected reply type: we don't care - * G_DBUS_CALL_FLAGS_NONE -- no special flags - * DBUS_TIMEOUT_MS -- 5000ms (only for message queueing, not reply) - * NULL -- GCancellable: no cancellation support - * NULL -- GAsyncReadyCallback: no reply callback - * NULL -- user_data for reply callback: N/A - * - * The three trailing NULLs make this fire-and-forget. GLib queues - * the D-Bus message in the kernel's socket buffer and returns. - * - * NULL-COALESCING for optional fields: - * fwdwnlreq->downloadUrl ? fwdwnlreq->downloadUrl : "" - * If downloadUrl is NULL (caller didn't set it), we send "" - * to the daemon. The daemon treats "" as "use the XConf URL - * that was returned during checkForUpdate." Same for TypeOfFirmware. - * - * WHAT HAPPENS ON THE DAEMON SIDE: - * 1. Daemon receives "DownloadFirmware" with 4 string arguments - * 2. Validates handler "1" is registered (from registerProcess) - * 3. Starts downloading firmware_v8.bin from the URL - * 4. As download progresses, broadcasts DownloadProgress signals: - * - (1, "firmware_v8.bin", 0, "NOTSTARTED", "Download queued") - * - (1, "firmware_v8.bin", 10, "INPROGRESS", "10% downloaded") - * - (1, "firmware_v8.bin", 50, "INPROGRESS", "50% downloaded") - * - (1, "firmware_v8.bin", 100, "COMPLETED", "Download complete") - * 5. Our BG thread catches each signal and fires our callback - */ + /* Fire-and-forget D-Bus call with 4 string args (handle, name, url, type) */ g_dbus_connection_call( conn, @@ -774,43 +441,7 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, NULL /* user_data: none */ ); - /* - * [STEP 5] CLOSE EPHEMERAL CONNECTION AND RETURN - * - * g_object_unref(conn) closes our ephemeral D-Bus connection :1.143. - * The D-Bus message is already in the kernel socket buffer -- closing - * our end doesn't prevent delivery to the daemon. - * - * After this, the state of the world is: - * - * Main thread: - * - Returns RDKFW_DWNL_SUCCESS to the caller - * - Caller enters pthread_cond_timedwait (typically 300s timeout) - * - Connection :1.143 is DEAD (just closed) - * - * BG thread: - * - Still sleeping in g_main_loop_run() on connection :1.141 - * - g_dwnl_registry.entries[0] has our callback in ACTIVE state - * - Will wake up on EVERY DownloadProgress signal - * - Will call our callback MULTIPLE TIMES - * - * Daemon: - * - Received our request, started downloading the firmware - * - Will broadcast DownloadProgress signals as download progresses - * - * g_dwnl_registry (download-specific, separate from g_registry): - * entries[0] = { state=ACTIVE, handle_key="1", - * callback=on_download_progress_callback } - * entries[1..29] = IDLE - * - * D-Bus connections: - * :1.141 -- BG thread persistent (ALIVE, listening for signals) - * :1.143 -- this downloadFirmware ephemeral (DEAD, just closed) - * - * SUCCESS here means: "I sent the request and registered your callback." - * It does NOT mean: "Download started." or "File exists on server." - * The callback will fire later with actual progress. - */ + /* Close ephemeral connection; message already in kernel socket buffer */ g_object_unref(conn); FWUPMGR_INFO("downloadFirmware: D-Bus call sent, returning SUCCESS. handle='%s'\n", @@ -933,119 +564,42 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, const FwUpdateReq *fwupdatereq, UpdateCallback callback) { - /* - * [STEP 1] INPUT VALIDATION - * - * Reject obviously bad inputs before touching D-Bus or the registry. - * This is the library's input boundary -- validate everything here. - * - * updateFirmware has the MOST validation of all three APIs: - * checkForUpdate: 2 checks (handle, callback) - * downloadFirmware: 5 checks (handle, struct, firmwareName, empty, callback) - * updateFirmware: 7 checks (handle, struct, firmwareName, firmwareName - * empty, TypeOfFirmware, TypeOfFirmware empty, - * callback) - * - * TypeOfFirmware is required here (unlike downloadFirmware where it's - * optional) because the daemon needs to know HOW to flash the image - * (different flash paths for PCI vs PDRI vs PERIPHERAL). - */ - - /* - * Check 1a: handle must not be NULL and must not be empty "". - * - * handle is the string returned by registerProcess(), e.g. "1". - * If the caller passes NULL (forgot to register first) or an - * empty string, reject immediately. The daemon would reject it - * too, but we save the D-Bus round-trip. - */ + /* Validate inputs (most checks of all 3 APIs -- TypeOfFirmware required) */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("updateFirmware: invalid handle (NULL or empty)\n"); return RDKFW_UPDATE_FAILED; } - /* - * Check 1b: the request struct pointer must not be NULL. - * - * This catches the case where the caller passes NULL instead of - * &update_req. Dereferencing NULL would crash the process. - */ if (fwupdatereq == NULL) { FWUPMGR_ERROR("updateFirmware: fwupdatereq is NULL\n"); return RDKFW_UPDATE_FAILED; } - /* - * Check 1c: firmwareName pointer must not be NULL. - * - * FwUpdateReq.firmwareName is a const char* -- it could be NULL if - * the caller forgot to set it. We need a filename to tell the - * daemon WHAT image to flash. - */ if (fwupdatereq->firmwareName == NULL) { FWUPMGR_ERROR("updateFirmware: firmwareName is NULL\n"); return RDKFW_UPDATE_FAILED; } - /* - * Check 1d: firmwareName must not be an empty string "". - * - * An empty filename is meaningless -- the daemon can't flash "". - * This catches the case where the caller did: - * update_req.firmwareName = ""; // accident - */ if (fwupdatereq->firmwareName[0] == '\0') { FWUPMGR_ERROR("updateFirmware: firmwareName is empty\n"); return RDKFW_UPDATE_FAILED; } - /* - * Check 1e: TypeOfFirmware pointer must not be NULL. - * - * Unlike downloadFirmware (where TypeOfFirmware is optional), - * updateFirmware REQUIRES TypeOfFirmware because the daemon - * uses it to select the correct flash mechanism: - * "PCI" -- flash to the main chipset - * "PDRI" -- flash to PDRI (Platform Data Recovery Image) - * "PERIPHERAL" -- flash to a connected peripheral device - * Without this, the daemon doesn't know HOW to flash the image. - */ if (fwupdatereq->TypeOfFirmware == NULL) { FWUPMGR_ERROR("updateFirmware: TypeOfFirmware is NULL\n"); return RDKFW_UPDATE_FAILED; } - /* - * Check 1f: TypeOfFirmware must not be empty "". - * - * Same reasoning as check 1e -- an empty string provides no - * flash-type information. Catches: - * update_req.TypeOfFirmware = ""; // accident - */ if (fwupdatereq->TypeOfFirmware[0] == '\0') { FWUPMGR_ERROR("updateFirmware: TypeOfFirmware is empty\n"); return RDKFW_UPDATE_FAILED; } - /* - * Check 1g: callback must not be NULL. - * - * Without a callback, the app can't receive flash progress updates. - * It would never know when the update finishes (or if it failed). - * That's always a programming error. - */ if (callback == NULL) { FWUPMGR_ERROR("updateFirmware: callback is NULL\n"); return RDKFW_UPDATE_FAILED; } - /* - * Log what we're about to flash. The ternary expressions handle - * optional fields: - * LocationOfFirmware may be NULL (it's optional in FwUpdateReq). - * If NULL or empty, the daemon uses the path from device.properties. - * rebootImmediately is a bool, logged as "yes"/"no" for clarity. - */ FWUPMGR_INFO("updateFirmware: handle='%s' firmware='%s' type='%s' " "location='%s' reboot=%s\n", handle, @@ -1056,153 +610,25 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, : "(use device.properties path)", fwupdatereq->rebootImmediately ? "yes" : "no"); - /* - * [STEP 2] CREATE EPHEMERAL D-BUS CONNECTION - * - * g_bus_get_sync(G_BUS_TYPE_SYSTEM, ...) opens a new connection to - * the system D-Bus bus. Gets a unique sender name like :1.143. - * - * Why a NEW connection instead of reusing the BG thread's :1.141? - * The BG thread's connection is attached to the BG thread's - * GMainContext. Using it from the main thread would require - * cross-thread GLib context management -- complex and fragile. - * A fresh per-call connection is simpler and safe. - * - * Why BEFORE registering the callback? - * If D-Bus is down (dbus-daemon crashed, socket missing), this - * call fails. We want to fail BEFORE polluting the update - * registry with an ACTIVE entry that will never be dispatched. - * Clean failure: no registry entry, no dangling state. - * - * Cost: ~2ms for the D-Bus handshake. Negligible for a firmware - * update that takes minutes. - */ + /* Open ephemeral D-Bus connection (before registry to avoid ghost entries) */ GError *error = NULL; GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); if (conn == NULL) { - /* - * D-Bus connection failed. Common causes: - * - dbus-daemon not running - * - System bus socket missing (/var/run/dbus/system_bus_socket) - * - Permission denied (D-Bus policy rejects our user) - * - * Return FAILED -- no registry entry created, nothing to clean up. - */ FWUPMGR_ERROR("updateFirmware: D-Bus connect failed: %s\n", error ? error->message : "unknown"); if (error) g_error_free(error); return RDKFW_UPDATE_FAILED; } - /* - * [STEP 3] REGISTER CALLBACK IN THE UPDATE REGISTRY (g_update_registry) - * - * internal_update_register_callback() does the following (see _async.c): - * 1. Locks g_update_registry.mutex - * 2. Scans all 30 UpdateCbEntry slots for: - * a. An existing ACTIVE entry with the same handle (dedup/overwrite) - * b. The first IDLE slot (free slot) - * 3. If same handle found: overwrites it (prevents stale callbacks) - * If free slot found: uses it - * If neither: returns false (registry full -- 30 concurrent updates!) - * 4. Populates the slot: - * - handle_key = strdup(handle) -- "1" (heap copy) - * - callback = our function pointer - * - state = UPDATE_CB_STATE_ACTIVE -- NOTE: ACTIVE, not PENDING! - * - registered_time = current unix timestamp - * 5. Unlocks g_update_registry.mutex - * 6. Returns true - * - * After this call, the registry has one ACTIVE entry. When the BG - * thread receives UpdateProgress signals, it will find this entry - * and invoke the callback on EVERY signal. - * - * STATE DIFFERENCE FROM THE OTHER TWO REGISTRIES: - * checkForUpdate: g_registry -- state = CB_STATE_PENDING (fires ONCE) - * downloadFirmware: g_dwnl_registry -- state = DWNL_CB_STATE_ACTIVE (fires MANY) - * updateFirmware: g_update_registry -- state = UPDATE_CB_STATE_ACTIVE (fires MANY) - * Download and update both use the ACTIVE-until-terminal pattern. - * checkForUpdate uses the PENDING->DISPATCHED->IDLE one-shot pattern. - * - * Why BEFORE the D-Bus call? - * Race condition prevention. If the daemon starts flashing - * instantly, the BG thread would receive the first UpdateProgress - * signal before we registered. dispatch_all_update_active() would - * find zero ACTIVE entries and silently discard the signal. Our - * callback would never fire. The app would hang forever. - * - * Failure case: registry full (30 concurrent pending updates). - * In practice never happens -- a device flashes one firmware - * at a time. If it does, clean up the D-Bus connection and fail. - */ + /*Register callback BEFORE D-Bus call to prevent signal race */ if (!internal_update_register_callback(handle, callback)) { FWUPMGR_ERROR("updateFirmware: registry full, handle='%s'\n", handle); - /* - * Clean up: close the D-Bus connection we opened in Step 2. - * No registry entry was created, so no registry cleanup needed. - */ g_object_unref(conn); return RDKFW_UPDATE_FAILED; } - /* - * [STEP 4] SEND FIRE-AND-FORGET D-BUS METHOD CALL - * - * g_dbus_connection_call() sends a D-Bus method call to the daemon. - * - * Parameters to g_dbus_connection_call(): - * conn -- our ephemeral connection :1.143 - * DBUS_SERVICE_NAME -- "org.rdkfwupdater.Service" (daemon's well-known name) - * DBUS_OBJECT_PATH -- "/org/rdkfwupdater/Service" (object path) - * DBUS_INTERFACE_NAME -- "org.rdkfwupdater.Interface" - * DBUS_METHOD_UPDATE -- "UpdateFirmware" (the method name) - * g_variant_new("(sssss)", ...) -- 5-string argument tuple: - * "(sssss)" means a tuple containing five strings - * string 1: handle -- "1" (which registered client is asking) - * string 2: firmwareName -- "firmware_v8.bin" (what to flash) - * string 3: LocationOfFirmware -- path to image or "" for default - * string 4: TypeOfFirmware -- "PCI" / "PDRI" / "PERIPHERAL" - * string 5: rebootImmediately -- "true" or "false" (see note below) - * NULL -- expected reply type: we don't care - * G_DBUS_CALL_FLAGS_NONE -- no special flags - * DBUS_TIMEOUT_MS -- 5000ms (only for message queueing, not reply) - * NULL -- GCancellable: no cancellation support - * NULL -- GAsyncReadyCallback: no reply callback - * NULL -- user_data for reply callback: N/A - * - * The three trailing NULLs make this fire-and-forget. GLib queues - * the D-Bus message in the kernel's socket buffer and returns. - * - * WHY 5 STRINGS (NOT 4 LIKE downloadFirmware): - * updateFirmware sends an extra argument: rebootImmediately. - * The FwUpdateReq struct has rebootImmediately as a bool (true/false), - * but the daemon's D-Bus interface is defined with all-string - * arguments. So we convert: true -> "true", false -> "false" - * using the ternary: fwupdatereq->rebootImmediately ? "true" : "false" - * - * NULL-COALESCING for optional field: - * fwupdatereq->LocationOfFirmware ? fwupdatereq->LocationOfFirmware : "" - * If LocationOfFirmware is NULL (caller didn't set it), we send "" - * to the daemon. The daemon treats "" as "use the path from - * device.properties." TypeOfFirmware is NOT coalesced because it's - * required (validated in Step 1e/1f above). - * - * WHAT HAPPENS ON THE DAEMON SIDE: - * 1. Daemon receives "UpdateFirmware" with 5 string arguments - * 2. Validates handler "1" is registered (from registerProcess) - * 3. Starts flashing the firmware image using the appropriate method - * 4. As flashing progresses, broadcasts UpdateProgress signals: - * - (1, "firmware_v8.bin", 0, 0, "Flash started") - * - (1, "firmware_v8.bin", 25, 0, "Writing partition 1") - * - (1, "firmware_v8.bin", 50, 0, "Writing partition 2") - * - (1, "firmware_v8.bin", 100, 1, "Flash complete") - * Signal format: "(tsiis)" where: - * t = handler_id (uint64), s = firmware_name, - * i = progress (int32 0-100), i = status_code (0/1/2), - * s = message - * 5. Our BG thread catches each signal and fires our callback - */ + /* Fire-and-forget D-Bus call with 5 string args (handle, name, location, type, reboot) */ g_dbus_connection_call( conn, @@ -1224,43 +650,7 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, NULL /* user_data: none */ ); - /* - * [STEP 5] CLOSE EPHEMERAL CONNECTION AND RETURN - * - * g_object_unref(conn) closes our ephemeral D-Bus connection :1.143. - * The D-Bus message is already in the kernel socket buffer -- closing - * our end doesn't prevent delivery to the daemon. - * - * After this, the state of the world is: - * - * Main thread: - * - Returns RDKFW_UPDATE_SUCCESS to the caller - * - Caller enters pthread_cond_timedwait (typically 300s+ timeout) - * - Connection :1.143 is DEAD (just closed) - * - * BG thread: - * - Still sleeping in g_main_loop_run() on connection :1.141 - * - g_update_registry.entries[0] has our callback in ACTIVE state - * - Will wake up on EVERY UpdateProgress signal - * - Will call our callback MULTIPLE TIMES - * - * Daemon: - * - Received our request, started flashing the firmware - * - Will broadcast UpdateProgress signals as flashing progresses - * - * g_update_registry (update-specific, third registry): - * entries[0] = { state=ACTIVE, handle_key="1", - * callback=on_update_progress_callback } - * entries[1..29] = IDLE - * - * D-Bus connections: - * :1.141 -- BG thread persistent (ALIVE, listening for signals) - * :1.143 -- this updateFirmware ephemeral (DEAD, just closed) - * - * SUCCESS here means: "I sent the request and registered your callback." - * It does NOT mean: "Flashing started." or "Image is valid." - * The callback will fire later with actual progress. - */ + /* Close ephemeral connection; message already in kernel socket buffer */ g_object_unref(conn); FWUPMGR_INFO("updateFirmware: D-Bus call sent, returning SUCCESS. " diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c index 640ed771..3bfb25ab 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c @@ -399,52 +399,23 @@ static void on_check_complete_signal(GDBusConnection *conn, GVariant *parameters, gpointer user_data) { - /* - * Suppress "unused parameter" warnings. GLib's signal handler - * signature requires all 7 parameters, but we only use 'parameters'. - * (void) casts are the standard C idiom for this. - */ + /* Suppress unused parameter warnings (GLib signal handler requires all 7) */ (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; FWUPMGR_INFO("on_check_complete_signal: received\n"); - /* - * Stack-allocated struct to hold the parsed signal data. - * memset to zero ensures all pointers start as NULL (important - * for cleanup -- free(NULL) is safe, free(garbage) is not). - */ + /* Parse signal payload "(tiissss)" into local struct */ InternalSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); - /* - * Parse the GVariant "(tiissss)" payload into our struct. - * See internal_parse_signal_data() below for details. - * - * If parsing fails (wrong signature, NULL parameters), we return - * without dispatching. The caller's condvar timeout will fire. - * This is the correct behavior -- we can't dispatch garbage data. - */ if (!internal_parse_signal_data(parameters, &signal_data)) { FWUPMGR_ERROR("on_check_complete_signal: parse failed\n"); return; } - /* - * Dispatch to all registered callbacks. This is where the actual - * callback invocation happens. See dispatch_all_pending() below - * for the detailed two-phase dispatch explanation. - */ + /* Dispatch to all registered callbacks, then free strdup'd strings */ dispatch_all_pending(&signal_data); - - /* - * Free the 4 strdup'd strings in signal_data. - * After dispatch_all_pending() returns, all callbacks have completed - * and no one holds references to these strings anymore. - * - * internal_cleanup_signal_data() calls free() on each string - * and memset's the struct to zero (defensive cleanup). - */ internal_cleanup_signal_data(&signal_data); } @@ -531,83 +502,31 @@ static void on_check_complete_signal(GDBusConnection *conn, */ static void dispatch_all_pending(const InternalSignalData *signal_data) { - /* - * Local snapshot struct -- what we copy from each PENDING entry. - * - * typedef here (inside the function) because this struct is only - * used locally -- no other function needs it. - * - * callback: the function pointer to invoke - * handle_copy: a copy of the handle string (256 bytes, more than enough - * for handle strings like "1" or "12345") - * slot_index: which entry in g_registry.entries[] this came from - * (needed to reset the slot to IDLE after callback returns) - */ + /* Stack-local snapshot: copied from each PENDING entry while mutex is held */ typedef struct { UpdateEventCallback callback; char handle_copy[256]; int slot_index; } Snapshot; - /* - * Stack-allocated array of snapshots. 30 entries x ~270 bytes each - * = ~8KB on the stack. Well within the BG thread's 8MB stack limit. - * - * count tracks how many PENDING entries we found. - */ Snapshot snapshots[MAX_PENDING_CALLBACKS]; int count = 0; - /* ================================================================ - * PHASE 1: COLLECT PENDING ENTRIES UNDER MUTEX - * - * This is the fast critical section. We hold the mutex for as short - * as possible: scan 30 entries, copy the ones we need, release. - * Total time: microseconds. - * ================================================================ */ + /* PHASE 1: Collect PENDING entries under mutex (fast critical section) */ pthread_mutex_lock(&g_registry.mutex); for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { CallbackEntry *e = &g_registry.entries[i]; - - /* - * Skip anything that isn't PENDING. - * IDLE -- empty slot, nothing to do - * DISPATCHED -- another dispatch is already handling this - * TIMED_OUT -- expired, will be cleaned up separately - */ if (e->state != CB_STATE_PENDING) continue; - /* - * Found a PENDING entry. Copy its essential data into our - * local snapshot so we can invoke it after releasing the mutex. - * - * We copy the function pointer (8 bytes) and the handle string - * (up to 256 bytes via snprintf). We also record the slot index - * so we can reset the exact slot to IDLE later. - * - * snprintf with "%s" safely copies the string with null termination. - * If handle_key is NULL (should never happen for PENDING entries, - * but defensive), we copy an empty string. - */ + /* Copy essential data into snapshot for mutex-free invocation */ snapshots[count].callback = e->callback; snapshots[count].slot_index = i; snprintf(snapshots[count].handle_copy, sizeof(snapshots[count].handle_copy), "%s", e->handle_key ? e->handle_key : ""); - /* - * Mark the slot as DISPATCHED. This is the transition: - * PENDING -> DISPATCHED - * - * This prevents: - * 1. Another signal dispatch from calling this callback again - * 2. A new checkForUpdate() from overwriting this slot - * (dedup only matches PENDING, not DISPATCHED) - * - * The slot will go DISPATCHED -> IDLE after the callback returns - * (in Phase 2's loop below). - */ + /* Mark DISPATCHED to prevent double-dispatch or re-entrant overwrite */ e->state = CB_STATE_DISPATCHED; count++; @@ -615,72 +534,21 @@ static void dispatch_all_pending(const InternalSignalData *signal_data) e->handle_key ? e->handle_key : "(null)"); } - /* - * Release the mutex. Phase 1 is done. - * From this point on, the main thread is free to register new - * callbacks via internal_register_callback() -- no blocking. - */ + /* Release mutex; main thread can now register new callbacks */ pthread_mutex_unlock(&g_registry.mutex); FWUPMGR_INFO("dispatch_all_pending: %d callback(s) to fire\n", count); - /* ================================================================ - * PHASE 2: BUILD DATA AND INVOKE CALLBACKS (NO MUTEX HELD) - * - * Now we build the FwInfoData struct that callbacks receive, - * and invoke each callback in sequence. No mutex is held during - * any of this -- safe for re-entrant use. - * ================================================================ */ + /* PHASE 2: Build FwInfoData and invoke callbacks (no mutex held) */ - /* - * Map the daemon's integer status_code to our public enum. - * - * The daemon sends status_code as an integer in the signal: - * 0 = FIRMWARE_AVAILABLE - * 1 = FIRMWARE_NOT_AVAILABLE - * 2 = UPDATE_NOT_ALLOWED - * 3 = FIRMWARE_CHECK_ERROR - * 4 = IGNORE_OPTOUT - * 5 = BYPASS_OPTOUT - * - * internal_map_status_code() converts this to our CheckForUpdateStatus enum. - * Unknown values map to FIRMWARE_CHECK_ERROR (safe default). - */ + /* Map daemon's integer status_code to public enum */ CheckForUpdateStatus status = internal_map_status_code(signal_data->status_code); - /* - * Build FwInfoData on the STACK. - * - * This is the struct that callbacks receive via their - * (const FwInfoData *fwinfodata) parameter. - * - * CRITICAL: This struct is stack-allocated. It exists only while - * this function is running. When dispatch_all_pending() returns - * (after all callbacks have been invoked and returned), this stack - * frame is destroyed and fwinfo_data becomes invalid. - * - * This is why callbacks MUST copy any data they need before returning. - * The example_app does this: - * strncpy(g_fw_filename, event_data->UpdateDetails->FwFileName, ...); - * - * memset to zero ensures all char arrays start as empty strings - * (first byte is '\0') and all pointers start as NULL. - */ + /* Build stack-allocated FwInfoData (valid only during callbacks) */ FwInfoData fwinfo_data; memset(&fwinfo_data, 0, sizeof(fwinfo_data)); - /* - * Copy the current firmware version from the signal data into - * fwinfo_data.CurrFWVersion (a char[64] array). - * - * strncpy with sizeof()-1 ensures we never overflow the buffer. - * The explicit null-termination on the next line is a safety net - * in case signal_data->current_version is >= 63 characters - * (strncpy does NOT null-terminate if src >= n characters). - * - * If current_version is NULL (daemon didn't provide it), we skip - * the copy and the field stays as empty string from memset. - */ + /* Copy current version from signal into fixed-size buffer */ if (signal_data->current_version) { strncpy(fwinfo_data.CurrFWVersion, signal_data->current_version, sizeof(fwinfo_data.CurrFWVersion) - 1); @@ -690,122 +558,35 @@ static void dispatch_all_pending(const InternalSignalData *signal_data) /* Set the status enum in the struct. */ fwinfo_data.status = status; - /* - * Parse UpdateDetails if firmware is available. - * - * The daemon sends update details as a pipe-separated string: - * "File:firmware_v8.bin|Location:http://cdn..|Version:RDKV_8.0|Reboot:false|..." - * - * We need to parse this into an UpdateDetails struct with individual - * fields (FwFileName, FwUrl, FwVersion, RebootImmediately, etc.). - * - * UpdateDetails is ALSO stack-allocated. It lives in this function's - * stack frame. fwinfo_data.UpdateDetails is a POINTER to this stack - * variable. After this function returns, both are gone. - * - * We only populate UpdateDetails when status == FIRMWARE_AVAILABLE. - * For all other statuses (NOT_AVAILABLE, ERROR, etc.), UpdateDetails - * is set to NULL -- there's nothing to update, so no details to show. - */ + /* Parse UpdateDetails from pipe-separated string if firmware is available */ UpdateDetails update_details; if (status == FIRMWARE_AVAILABLE && signal_data->update_details) { memset(&update_details, 0, sizeof(update_details)); - /* - * parse_update_details() tokenizes the pipe-separated string - * and copies each Key:Value pair into the appropriate field - * of the UpdateDetails struct. See parse_update_details() below. - */ if (parse_update_details(signal_data->update_details, &update_details)) { - /* - * Point FwInfoData's UpdateDetails pointer to our stack variable. - * - * This is safe because: both fwinfo_data and update_details - * live on the same stack frame. The pointer is valid as long - * as this function is executing. The callback receives this - * pointer and MUST copy what it needs before returning. - */ fwinfo_data.UpdateDetails = &update_details; FWUPMGR_INFO("dispatch_all_pending: UpdateDetails populated\n"); FWUPMGR_INFO(" FwFileName: %s\n", update_details.FwFileName); FWUPMGR_INFO(" FwVersion: %s\n", update_details.FwVersion); } else { - /* - * Parsing failed (malformed string, etc.). Set to NULL so - * the callback knows: "firmware is available but I couldn't - * parse the details." The callback should handle this gracefully. - */ fwinfo_data.UpdateDetails = NULL; FWUPMGR_ERROR("dispatch_all_pending: parse_update_details failed\n"); } } else { - /* - * Either firmware is NOT available, or the daemon didn't send - * an update_details string. Set pointer to NULL. - * - * The callback should check: if (event_data->UpdateDetails != NULL) - * before accessing any UpdateDetails fields. - */ fwinfo_data.UpdateDetails = NULL; } - /* - * Invoke each callback from our snapshot. - * - * For each snapshot entry: - * 1. Call the callback with a pointer to our stack-allocated FwInfoData - * 2. After the callback returns, lock the mutex and reset the slot to IDLE - * - * The same FwInfoData struct is passed to ALL callbacks. They all see - * the same firmware check result (because the daemon broadcast one signal). - * - * Callbacks run SEQUENTIALLY on the BG thread. If there are 3 pending - * callbacks, they fire one after another (not in parallel). If one - * callback is slow, it delays the others. Callbacks should be fast -- - * typically just copy data and signal a condvar. - */ + /* Invoke each callback sequentially, then reset slot to IDLE */ for (int i = 0; i < count; i++) { Snapshot *s = &snapshots[i]; FWUPMGR_INFO("dispatch_all_pending: invoking callback for handle='%s'\n", s->handle_copy); - /* - * THE ACTUAL CALLBACK INVOCATION. - * - * s->callback is the function pointer stored during - * internal_register_callback(). For our example_app, this is - * on_firmware_check_callback(). - * - * &fwinfo_data is a pointer to our stack-allocated struct. - * The callback receives it as (const FwInfoData *event_data). - * "const" means the callback cannot modify it, but it can read - * all fields and copy them. - * - * THIS CALL BLOCKS THE BG THREAD until the callback returns. - * While inside the callback: - * - The BG thread is busy (not listening for more signals) - * - If another signal arrives, GLib queues it in the GMainContext - * - The signal will be dispatched after this function returns - * and the BG thread goes back to g_main_loop_run() - */ s->callback(&fwinfo_data); - /* - * Callback has returned. Now reset the slot to IDLE. - * - * We must lock the mutex for this because dispatch_all_pending() - * could be called concurrently (another signal arrives while - * we're in Phase 2), or the main thread could be registering - * a new callback. - * - * registry_reset_slot() frees the strdup'd handle_key, sets - * callback to NULL, and sets state to CB_STATE_IDLE. - * - * After this, the slot is available for reuse by the next - * checkForUpdate() call. - */ + /* Reset slot to IDLE under mutex (frees strdup'd handle_key) */ pthread_mutex_lock(&g_registry.mutex); registry_reset_slot(&g_registry.entries[s->slot_index]); pthread_mutex_unlock(&g_registry.mutex); @@ -877,49 +658,16 @@ static void dispatch_all_pending(const InternalSignalData *signal_data) bool internal_register_callback(FirmwareInterfaceHandle handle, UpdateEventCallback callback) { - /* - * Lock the registry mutex before touching the entries array. - * - * Who else might be holding this mutex right now? - * - dispatch_all_pending() on the BG thread (Phase 1 -- collecting - * PENDING entries into a snapshot). But Phase 1 is very fast - * (microseconds), so contention is rare. - * - registry_reset_slot() on the BG thread (after invoking a - * callback, resetting the slot to IDLE). Also very fast. - * - * In practice, the main thread and BG thread almost never contend - * because the BG thread only touches the registry when a signal - * arrives, which is seconds apart. - */ pthread_mutex_lock(&g_registry.mutex); - /* - * Two scan targets: - * free_slot -- first IDLE entry (to use if no dedup match) - * existing_slot -- a PENDING entry with the same handle (dedup) - * - * We scan all 30 entries in one pass, looking for both simultaneously. - */ + /* Single-pass scan: look for dedup match (same handle) and first free slot */ CallbackEntry *free_slot = NULL; CallbackEntry *existing_slot = NULL; for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { CallbackEntry *e = &g_registry.entries[i]; - /* - * Dedup check: is there already a PENDING entry for this handle? - * - * This happens when the caller calls checkForUpdate() twice - * with the same handle before the first callback fires. - * Without dedup, both entries would BOTH get dispatched when - * the signal arrives -- the callback would fire twice. The - * second invocation would be a "ghost" with stale data. - * - * By overwriting, only the latest callback survives. - * - * strcmp is safe here because handle_key is always a valid - * null-terminated string (set by strdup) or NULL (checked first). - */ + /* Dedup: overwrite existing PENDING entry for same handle */ if (e->state == CB_STATE_PENDING && e->handle_key != NULL && strcmp(e->handle_key, handle) == 0) { @@ -927,30 +675,12 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, break; } - /* - * Remember the first free slot we find, but keep scanning - * in case there's a dedup match later in the array. - * - * We only record the FIRST free slot (free_slot == NULL guard) - * to avoid unnecessary work. - */ if (free_slot == NULL && e->state == CB_STATE_IDLE) { free_slot = e; } } - /* - * Choose target: prefer dedup (overwrite existing) over new slot. - * - * If existing_slot != NULL: overwrite the existing PENDING entry. - * This is the dedup path -- same handle called checkForUpdate again. - * - * If existing_slot == NULL and free_slot != NULL: use the free slot. - * This is the normal first-call path. - * - * If both are NULL: registry is full. All 30 slots are occupied - * (some combination of PENDING and DISPATCHED). Return false. - */ + /* Prefer dedup (overwrite) over new slot */ CallbackEntry *target = existing_slot ? existing_slot : free_slot; if (target == NULL) { @@ -960,13 +690,7 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, return false; } - /* - * If overwriting an existing entry, free the old handle_key. - * - * The old strdup'd string must be freed to avoid a memory leak. - * The old callback function pointer is just overwritten -- function - * pointers don't need freeing, they point to code, not heap data. - */ + /* If overwriting, free the old handle_key to avoid leak */ if (existing_slot) { FWUPMGR_INFO("internal_register_callback: overwriting existing for handle='%s'\n", handle); @@ -974,33 +698,7 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, target->handle_key = NULL; } - /* - * Populate the slot: - * - * handle_key = strdup(handle): - * Creates a heap copy of the handle string "1". We need our own - * copy because the caller's 'handle' pointer belongs to them -- - * they could theoretically modify or free it later. strdup - * allocates strlen(handle)+1 bytes (2 bytes for "1\0"). - * This copy is freed in registry_reset_slot() when the slot - * returns to IDLE. - * - * callback = callback: - * Stores the function pointer. When dispatch_all_pending() runs, - * it will call this: callback(&fwinfo_data). - * Function pointers are just addresses -- no heap allocation. - * - * state = CB_STATE_PENDING: - * Marks this slot as "waiting for a signal." The BG thread's - * dispatch_all_pending() only looks at PENDING entries. - * IDLE entries are skipped, DISPATCHED entries are being processed. - * - * registered_time = time(NULL): - * Unix timestamp of when this callback was registered. Currently - * used only for logging/debugging, but could be used by a future - * timeout sweeper to detect stale entries that have been PENDING - * for too long (e.g., > CALLBACK_TIMEOUT_SECONDS = 60s). - */ + /* Populate slot: strdup handle, store callback, mark PENDING */ target->handle_key = strdup(handle); target->callback = callback; target->state = CB_STATE_PENDING; @@ -1049,22 +747,10 @@ bool internal_register_callback(FirmwareInterfaceHandle handle, */ static void registry_reset_slot(CallbackEntry *entry) { - /* - * Free the strdup'd handle_key string (e.g., "1"). - * NULL check is defensive -- PENDING and DISPATCHED entries always - * have a non-NULL handle_key, but IDLE entries have NULL. - * free(NULL) is safe in C (no-op), but the explicit check avoids - * confusion and makes the intent clear. - */ if (entry->handle_key != NULL) { free(entry->handle_key); entry->handle_key = NULL; } - - /* - * Clear remaining fields. After this, the entry looks identical - * to a freshly memset'd entry from internal_system_init(). - */ entry->callback = NULL; entry->registered_time = 0; entry->state = CB_STATE_IDLE; @@ -1120,66 +806,21 @@ bool internal_parse_signal_data(GVariant *parameters, InternalSignalData *out_da { if (parameters == NULL || out_data == NULL) return false; - /* - * Check the type signature before extracting. - * - * g_variant_get_type_string() returns the GVariant's type as a string. - * We expect "(tiissss)". If it's anything else, the daemon sent - * something unexpected -- possibly a newer/older protocol version. - * Extracting with the wrong format string would read garbage. - */ + /* Verify type signature before extracting */ const gchar *sig = g_variant_get_type_string(parameters); if (strcmp(sig, "(tiissss)") != 0) { FWUPMGR_ERROR("internal_parse_signal_data: unexpected signature '%s'\n", sig); return false; } - /* - * Local variables to receive g_variant_get() output. - * - * For string types ('s'), g_variant_get() returns pointers into - * the GVariant's internal buffer. These are temporary -- we must - * strdup() them before the GVariant could be freed. - * - * For integer types ('t', 'i'), g_variant_get() copies the value - * directly into our local variables. - */ const gchar *cur = NULL, *avail = NULL, *details = NULL, *msg = NULL; guint64 handler_id = 0; gint32 result = 0, status = 0; - /* - * Extract all 7 fields from the GVariant tuple in one call. - * - * The format string "(tiissss)" must exactly match the GVariant type. - * Each format character corresponds to one pointer argument: - * &handler_id -- receives the uint64 - * &result -- receives the first int32 - * &status -- receives the second int32 - * &cur -- receives pointer to current_version string - * &avail -- receives pointer to available_version string - * &details -- receives pointer to update_details string - * &msg -- receives pointer to status_message string - */ g_variant_get(parameters, "(tiissss)", &handler_id, &result, &status, &cur, &avail, &details, &msg); - /* - * Copy extracted values into the output struct. - * - * Integers are copied directly (they're values, not pointers). - * - * Strings are strdup'd to create heap copies that we own. - * The ternary (cur ? strdup(cur) : NULL) handles the case where - * the daemon sent an empty string (glib may return "" not NULL) - * or truly NULL. strdup(NULL) is undefined behavior in C, so - * the NULL check is essential. - * - * Note: handler_id is extracted but not stored in InternalSignalData. - * We don't currently use it because the dispatch is broadcast to - * ALL pending callbacks, not filtered by handler_id. If we later - * need per-client filtering, we would add handler_id to the struct. - */ + /* strdup strings to outlive the GVariant; freed by internal_cleanup_signal_data() */ out_data->result_code = (int32_t)result; out_data->status_code = (int32_t)status; out_data->current_version = cur ? strdup(cur) : NULL; @@ -1319,18 +960,7 @@ static void dwnl_registry_reset_slot(DwnlCallbackEntry *entry); */ static void internal_dwnl_system_deinit(void) { - /* - * Lock before freeing. Defensive -- no other thread should be - * active at this point, but the pattern is consistent with how - * all other registry operations lock before accessing entries[]. - */ pthread_mutex_lock(&g_dwnl_registry.mutex); - - /* - * Scan all 30 slots. Free any non-NULL handle_key strings. - * IDLE slots have handle_key == NULL (already freed or never set). - * ACTIVE slots (abandoned downloads) have handle_key != NULL. - */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { if (g_dwnl_registry.entries[i].handle_key != NULL) { free(g_dwnl_registry.entries[i].handle_key); @@ -1338,11 +968,6 @@ static void internal_dwnl_system_deinit(void) } } pthread_mutex_unlock(&g_dwnl_registry.mutex); - - /* - * Destroy the mutex. After this, any attempt to lock it is UB. - * Since the library is unloading, no one should try. - */ pthread_mutex_destroy(&g_dwnl_registry.mutex); FWUPMGR_INFO("internal_dwnl_system_deinit: done\n"); @@ -1421,28 +1046,16 @@ static void on_download_progress_signal(GDBusConnection *conn, GVariant *parameters, gpointer user_data) { - /* Suppress "unused parameter" warnings for the 6 params we don't need. */ + /* Suppress unused parameter warnings */ (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; FWUPMGR_INFO("on_download_progress_signal: received\n"); - /* - * Stack-allocate and zero-init the parsed data struct. - * After parsing, this holds: - * handler_id -- uint64 (which client the signal is for) - * firmware_name -- gchar* (GLib-allocated, must g_free) - * progress_percent -- uint32 (0-100) - * status_string -- gchar* (GLib-allocated, must g_free) - * message -- gchar* (GLib-allocated, must g_free) - */ + /* Parse signal payload "(tsuss)" into local struct */ InternalDwnlSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); - /* - * Parse the GVariant. If parsing fails (wrong type signature, - * NULL parameters), log and return. No callbacks fire. - */ if (!internal_parse_dwnl_signal_data(parameters, &signal_data)) { FWUPMGR_ERROR("on_download_progress_signal: parse failed\n"); return; @@ -1454,31 +1067,10 @@ static void on_download_progress_signal(GDBusConnection *conn, signal_data.progress_percent, signal_data.status_string ? signal_data.status_string : "(null)"); - /* - * Dispatch to all ACTIVE download callbacks. - * This is the two-phase dispatch (snapshot under mutex, invoke without). - * The callback may fire for progress (slot stays ACTIVE) or for - * terminal status (slot reset to IDLE). - */ + /* Dispatch to all ACTIVE download callbacks */ dispatch_all_dwnl_active(&signal_data); - /* - * Free the GLib-allocated strings. - * - * g_free() is GLib's equivalent of free(). We use g_free() because - * the strings were allocated by GLib's g_variant_get() internally. - * Using standard free() on GLib-allocated memory is technically - * undefined behavior (though it works on most platforms). - * - * These strings were valid throughout dispatch_all_dwnl_active() - * because we only free them AFTER all callbacks have returned. - * The callbacks receive progress_percent (int, copied by value) - * and status (enum, copied by value), so they don't reference - * these strings directly. But the dispatch function does read - * status_string to determine is_final, so it must be valid then. - * - * g_free(NULL) is safe -- it's a no-op. - */ + /* Free GLib-allocated strings from g_variant_get() */ g_free(signal_data.firmware_name); g_free(signal_data.status_string); g_free(signal_data.message); @@ -1557,61 +1149,28 @@ static void on_download_progress_signal(GDBusConnection *conn, */ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) { - /* - * Stack-local snapshot struct for one ACTIVE entry. - * Same pattern as checkForUpdate's dispatch -- we copy what we need - * while the mutex is held, then work from the copy with no mutex. - * - * Fields: - * callback -- the app's function pointer (copied from slot) - * handle_copy -- snprintf'd copy of handle_key (for logging only) - * slot_index -- which slot in entries[] this came from - * is_final -- true if COMPLETED/ERROR (need to reset slot after) - */ typedef struct { DownloadCallback callback; char handle_copy[256]; int slot_index; - bool is_final; /* true if COMPLETED or ERROR -- remove after firing */ + bool is_final; } DwnlSnapshot; DwnlSnapshot snapshots[MAX_PENDING_CALLBACKS]; int count = 0; - /* - * Map the status string to our enum BEFORE entering the mutex. - * This is a pure computation (strcmp calls) with no shared state. - * Doing it outside the mutex keeps the critical section shorter. - * - * is_final determines whether we reset the slot after the callback: - * true = COMPLETED or ERROR -> download ended, free the slot - * false = IN_PROGRESS -> download continuing, keep slot ACTIVE - */ + /* Map status string to enum before entering mutex */ DownloadStatus status = map_dwnl_status_string(signal_data->status_string); bool is_final = (status == DWNL_COMPLETED || status == DWNL_ERROR); /* ---- PHASE 1: snapshot under mutex ---- */ - - /* - * Lock the download registry. This blocks any concurrent call to - * internal_dwnl_register_callback() (from main thread) until we - * finish our snapshot. Hold time: microseconds (just scanning + copying). - */ pthread_mutex_lock(&g_dwnl_registry.mutex); - /* - * Scan all 30 slots. Only process entries in ACTIVE state. - * IDLE and TIMED_OUT entries are skipped. - */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { DwnlCallbackEntry *e = &g_dwnl_registry.entries[i]; if (e->state != DWNL_CB_STATE_ACTIVE) continue; - /* - * Copy the entry data into our stack-local snapshot. - * After we unlock, the slot might be modified by another thread - * (extremely unlikely, but the pattern is safe regardless). - */ + /* Copy entry data into stack-local snapshot */ snapshots[count].callback = e->callback; snapshots[count].slot_index = i; snapshots[count].is_final = is_final; @@ -1619,13 +1178,6 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) sizeof(snapshots[count].handle_copy), "%s", e->handle_key ? e->handle_key : ""); - /* - * NOTE: We do NOT change the slot state here. - * For in-progress signals, the slot must remain ACTIVE. - * For terminal signals, we'll reset it in Phase 2 AFTER - * the callback fires. This is different from checkForUpdate - * which sets state=DISPATCHED during Phase 1. - */ count++; FWUPMGR_INFO("dispatch_all_dwnl_active: queued handle='%s' progress=%d%% final=%d\n", @@ -1633,55 +1185,21 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) signal_data->progress_percent, is_final); } - /* - * Unlock BEFORE invoking callbacks. - * This is the deadlock prevention: callbacks may call library APIs - * that need g_dwnl_registry.mutex, so we must not hold it. - */ + /* Unlock before invoking callbacks (deadlock prevention) */ pthread_mutex_unlock(&g_dwnl_registry.mutex); FWUPMGR_INFO("dispatch_all_dwnl_active: %d callback(s) to fire\n", count); /* ---- PHASE 2: invoke callbacks, no mutex held ---- */ - for (int i = 0; i < count; i++) { DwnlSnapshot *s = &snapshots[i]; FWUPMGR_INFO("dispatch_all_dwnl_active: invoking callback for handle='%s'\n", s->handle_copy); - /* - * Invoke the app's download callback. - * - * Callback signature: void fn(int progress_per, DownloadStatus status) - * progress_per = signal_data->progress_percent (0-100) - * status = mapped enum (DWNL_IN_PROGRESS, DWNL_COMPLETED, DWNL_ERROR) - * - * The app's callback (e.g., on_download_progress_callback in example_app) - * typically: - * - Logs the progress - * - On terminal status: locks its own condvar mutex, sets done=1, - * signals the condvar to wake the main thread - * - Returns - * - * This call may take microseconds to milliseconds depending on - * what the app does in the callback. We hold NO library mutex - * during this time. - */ s->callback(signal_data->progress_percent, status); - /* - * If this was a terminal signal (download ended), reset the slot. - * - * We re-acquire the mutex, call dwnl_registry_reset_slot(), then - * release. This frees the strdup'd handle_key and sets state=IDLE. - * - * After this, no more callbacks will fire for this handle. - * The slot is available for reuse by a future downloadFirmware() call. - * - * For in-progress signals, we skip this entirely. The slot stays - * ACTIVE and will be found again on the NEXT DownloadProgress signal. - */ + /* If terminal signal, reset slot to IDLE */ if (s->is_final) { pthread_mutex_lock(&g_dwnl_registry.mutex); dwnl_registry_reset_slot(&g_dwnl_registry.entries[s->slot_index]); @@ -1751,38 +1269,15 @@ static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, DownloadCallback callback) { - /* - * Lock the download registry mutex. - * This ensures only one thread modifies g_dwnl_registry at a time. - * The BG thread also locks this during Phase 1 of dispatch (snapshot) - * and during is_final slot reset. - */ pthread_mutex_lock(&g_dwnl_registry.mutex); DwnlCallbackEntry *free_slot = NULL; DwnlCallbackEntry *existing_slot = NULL; - /* - * Single-pass scan of all 30 slots. - * - * Priority 1: Find an ACTIVE entry with the same handle. - * -> We'll overwrite it (dedup). - * -> Break immediately when found (no need to continue). - * - * Priority 2: Remember the first IDLE slot encountered. - * -> We'll use it if no existing slot is found. - * -> Don't break -- keep scanning for existing_slot. - */ + /* Single-pass scan: look for dedup match (same handle) and first free slot */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { DwnlCallbackEntry *e = &g_dwnl_registry.entries[i]; - /* - * Check for existing ACTIVE entry with same handle. - * Three conditions must all be true: - * 1. Slot is ACTIVE (not IDLE or TIMED_OUT) - * 2. handle_key is not NULL (defensive -- should always be set for ACTIVE) - * 3. handle_key matches our handle (string comparison) - */ if (e->state == DWNL_CB_STATE_ACTIVE && e->handle_key != NULL && strcmp(e->handle_key, handle) == 0) { @@ -1790,28 +1285,13 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, break; } - /* - * Remember first free slot (only if we haven't found one yet). - * We DON'T break here -- we keep scanning because an existing - * slot at a higher index takes priority over a free slot at - * a lower index. - */ if (free_slot == NULL && e->state == DWNL_CB_STATE_IDLE) { free_slot = e; } } - /* - * Decide which slot to use. - * existing_slot (overwrite) takes priority over free_slot (new). - */ DwnlCallbackEntry *target = existing_slot ? existing_slot : free_slot; - /* - * If neither was found, the registry is full. All 30 slots are ACTIVE - * (30 concurrent downloads -- should never happen in practice). - * Unlock and return false. - */ if (target == NULL) { FWUPMGR_ERROR("internal_dwnl_register_callback: registry full (max=%d)\n", MAX_PENDING_CALLBACKS); @@ -1819,10 +1299,7 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, return false; } - /* - * If overwriting an existing slot, free the old handle_key first. - * strdup'd memory must be freed to avoid a memory leak. - */ + /* If overwriting, free old handle_key to avoid leak */ if (existing_slot) { FWUPMGR_INFO("internal_dwnl_register_callback: overwriting existing for handle='%s'\n", handle); @@ -1830,24 +1307,7 @@ bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, target->handle_key = NULL; } - /* - * Populate the slot with the new registration. - * - * strdup(handle): Creates a heap copy of the handle string. - * We need our own copy because the caller's handle may be freed - * or overwritten after downloadFirmware() returns. The slot - * must retain the handle for the entire download duration. - * - * callback: Store the function pointer directly. No copy needed -- - * function pointers are just addresses in the code segment. - * - * state = DWNL_CB_STATE_ACTIVE: The slot is now live. The BG thread - * will find it during its next dispatch_all_dwnl_active() call. - * NOTE: ACTIVE, not PENDING. There's no intermediate state. - * - * registered_time: Stored for potential timeout detection (not - * currently active, but the infrastructure is there for future use). - */ + /* Populate slot: strdup handle, store callback, mark ACTIVE */ target->handle_key = strdup(handle); target->callback = callback; target->state = DWNL_CB_STATE_ACTIVE; @@ -1958,37 +1418,19 @@ bool internal_parse_dwnl_signal_data(GVariant *parameters, { if (parameters == NULL || out_data == NULL) return false; - /* - * Verify the type signature before extracting. - * "(tsuss)" is the expected format for DownloadProgress. - * If it's different, the daemon protocol changed -- reject. - */ + /* Verify type signature before extracting */ const gchar *sig = g_variant_get_type_string(parameters); if (strcmp(sig, "(tsuss)") != 0) { FWUPMGR_ERROR("internal_parse_dwnl_signal_data: unexpected signature '%s' (expected '(tsuss)')\n", sig); return false; } - /* - * Local variables to receive g_variant_get() output. - * For 's' format: GLib allocates fresh strings (must g_free). - * For 't' and 'u' format: values copied directly into locals. - */ guint64 handler_id = 0; gchar *firmware_name = NULL; guint32 progress = 0; gchar *status_str = NULL; gchar *message_str = NULL; - /* - * Extract all 5 fields from the GVariant tuple. - * Format "(tsuss)" maps to: - * &handler_id -- receives uint64 - * &firmware_name -- receives gchar* (GLib-allocated) - * &progress -- receives uint32 - * &status_str -- receives gchar* (GLib-allocated) - * &message_str -- receives gchar* (GLib-allocated) - */ g_variant_get(parameters, "(tsuss)", &handler_id, &firmware_name, @@ -1996,16 +1438,12 @@ bool internal_parse_dwnl_signal_data(GVariant *parameters, &status_str, &message_str); - /* - * Copy into output struct. The string pointers are just transferred -- - * we don't strdup them again. Ownership passes to the caller. - * Caller is responsible for g_free() on firmware_name, status_string, message. - */ + /* Transfer ownership of GLib-allocated strings to caller (must g_free) */ out_data->handler_id = handler_id; - out_data->firmware_name = firmware_name; /* Caller must g_free */ + out_data->firmware_name = firmware_name; out_data->progress_percent = progress; - out_data->status_string = status_str; /* Caller must g_free */ - out_data->message = message_str; /* Caller must g_free */ + out_data->status_string = status_str; + out_data->message = message_str; return true; } @@ -2085,18 +1523,10 @@ DownloadStatus internal_map_dwnl_status_code(int32_t status_code) */ static DownloadStatus map_dwnl_status_string(const char *status_str) { - /* - * NULL means the field was missing from the signal (parse error - * or daemon bug). Treat as error -- something is wrong. - */ if (status_str == NULL) { return DWNL_ERROR; } - /* - * String comparisons for known values. - * "INPROGRESS" and "NOTSTARTED" both mean "not done yet." - */ if (strcmp(status_str, "INPROGRESS") == 0 || strcmp(status_str, "NOTSTARTED") == 0) { return DWNL_IN_PROGRESS; } else if (strcmp(status_str, "COMPLETED") == 0) { @@ -2105,10 +1535,7 @@ static DownloadStatus map_dwnl_status_string(const char *status_str) return DWNL_ERROR; } - /* - * Unknown string. Log it (for debugging daemon protocol changes) - * and treat as error. The slot will be reset to IDLE. - */ + /* Unknown string -- treat as error */ FWUPMGR_ERROR("map_dwnl_status_string: unknown status '%s' -> DWNL_ERROR\n", status_str); return DWNL_ERROR; } @@ -2224,39 +1651,14 @@ static void update_registry_reset_slot(UpdateCbEntry *entry); */ static void internal_update_system_deinit(void) { - /* - * Lock the mutex before modifying the registry. Even during - * shutdown, we follow the locking protocol for consistency. - */ pthread_mutex_lock(&g_update_registry.mutex); - - /* - * Walk all 30 slots and free any non-NULL handle_key strings. - * - * We don't check the slot's state -- even if a slot is somehow - * in an inconsistent state, we still free its handle_key to - * prevent leaks. A NULL handle_key means the slot was already - * clean (IDLE with no previous allocation). - */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { if (g_update_registry.entries[i].handle_key != NULL) { free(g_update_registry.entries[i].handle_key); - g_update_registry.entries[i].handle_key = NULL; /* Prevent dangling pointer */ + g_update_registry.entries[i].handle_key = NULL; } } - - /* - * Unlock the mutex before destroying it. pthread_mutex_destroy() - * requires the mutex to be unlocked. Destroying a locked mutex - * is undefined behavior on most POSIX implementations. - */ pthread_mutex_unlock(&g_update_registry.mutex); - - /* - * Destroy the mutex itself. This releases any OS resources - * associated with the mutex (e.g., kernel futex state on Linux). - * After this call, the mutex must NEVER be used again. - */ pthread_mutex_destroy(&g_update_registry.mutex); FWUPMGR_INFO("internal_update_system_deinit: done\n"); @@ -2333,82 +1735,31 @@ static void on_update_progress_signal(GDBusConnection *conn, GVariant *parameters, gpointer user_data) { - /* - * Suppress "unused parameter" compiler warnings. GLib's signal - * callback signature requires all 7 parameters, but we only - * need 'parameters' (the signal payload). The (void) cast tells - * the compiler "yes, I know I'm not using these." - */ + /* Suppress unused parameter warnings */ (void)conn; (void)sender; (void)object_path; (void)interface_name; (void)signal_name; (void)user_data; FWUPMGR_INFO("on_update_progress_signal: received\n"); - /* - * Stack-allocate the signal data struct and zero-initialize it. - * memset ensures all pointers start as NULL and all integers as 0. - * This is defensive -- if parsing fails partially, we don't have - * garbage values in the struct. - */ + /* Parse signal payload "(tsiis)" into local struct */ InternalUpdateSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); - /* - * Parse the GVariant payload "(tsiis)" into our struct. - * internal_parse_update_signal_data() validates the signature, - * extracts handler_id, firmware_name, progress, status_code, - * and message. Returns false if signature mismatch. - * - * If parsing fails, we bail out. No callbacks are invoked, no - * memory needs freeing (the struct was zero-initialized). - */ if (!internal_parse_update_signal_data(parameters, &signal_data)) { FWUPMGR_ERROR("on_update_progress_signal: parse failed\n"); return; } - /* - * Log the parsed signal data for debugging. This is invaluable - * when troubleshooting "callback never fired" bugs -- it proves - * whether the signal was received and what it contained. - * - * PRIu64 is the portable format specifier for uint64_t - * (avoids warnings on 32-bit vs 64-bit platforms). - */ FWUPMGR_INFO("on_update_progress_signal: handler=%" PRIu64 " firmware='%s' progress=%d%% status=%d\n", signal_data.handler_id, signal_data.firmware_name ? signal_data.firmware_name : "(null)", signal_data.progress_percent, signal_data.status_code); - /* - * Dispatch the parsed signal data to all ACTIVE update callbacks. - * dispatch_all_update_active() does the two-phase dispatch: - * Phase 1: snapshot ACTIVE entries under mutex - * Phase 2: invoke callbacks without mutex - * If terminal status: reset slot to IDLE - * - * After this call returns, all registered UpdateCallbacks have - * been invoked with the current progress and status. - */ + /* Dispatch to all ACTIVE update callbacks */ dispatch_all_update_active(&signal_data); - /* - * Free the heap-allocated strings from g_variant_get(). - * - * g_variant_get() with the "s" format specifier allocates new - * strings via g_strdup(). We own these strings and must free them. - * - * Two strings to free: - * 1. firmware_name -- the image filename (e.g., "firmware_v8.bin") - * 2. message -- the status message (e.g., "Writing partition 2") - * - * Compare with download's on_download_progress_signal() which frees - * THREE strings (firmware_name, status_string, message). Update - * has no status_string because it uses an integer status_code instead. - * - * g_free(NULL) is safe (it's a no-op), so we don't need NULL checks. - */ + /* Free GLib-allocated strings from g_variant_get() */ g_free(signal_data.firmware_name); g_free(signal_data.message); } @@ -2472,19 +1823,6 @@ static void on_update_progress_signal(GDBusConnection *conn, */ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_data) { - /* - * Local snapshot struct -- stores one entry's callback info. - * We allocate an array of these on the stack (up to 30 entries). - * - * Fields: - * callback -- the UpdateCallback function pointer to invoke - * handle_copy -- string copy of handle_key (for logging only) - * slot_index -- which g_update_registry.entries[] index this is - * is_final -- true if this signal ends the update (COMPLETED/ERROR) - * - * handle_copy is a fixed-size char[256] buffer, not a heap allocation. - * This avoids malloc/free overhead for a temporary logging string. - */ typedef struct { UpdateCallback callback; char handle_copy[256]; @@ -2495,53 +1833,18 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da UpdateSnapshot snapshots[MAX_PENDING_CALLBACKS]; int count = 0; - /* - * Map the raw integer status_code to the UpdateStatus enum. - * internal_map_update_status_code() does: - * 0 -> UPDATE_IN_PROGRESS - * 1 -> UPDATE_COMPLETED - * 2 -> UPDATE_ERROR - * anything else -> UPDATE_ERROR (defensive default) - * - * Then determine if this is a terminal signal (update finished). - * A terminal signal means we should reset the slot to IDLE after - * invoking the callback, because no more signals are coming. - */ + /* Map status code to enum before entering mutex */ UpdateStatus status = internal_map_update_status_code(signal_data->status_code); bool is_final = (status == UPDATE_COMPLETED || status == UPDATE_ERROR); /* ---- PHASE 1: snapshot under mutex ---- */ - - /* - * Lock the registry. While we hold this lock, no other thread can - * modify g_update_registry (e.g., the main thread can't register - * a new callback via internal_update_register_callback()). - */ pthread_mutex_lock(&g_update_registry.mutex); - /* - * Scan all 30 slots. For each ACTIVE entry, copy its info into - * the snapshot array. We copy the callback pointer (not the entry - * pointer) because after releasing the mutex, the entry could be - * modified by another thread. - */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { UpdateCbEntry *e = &g_update_registry.entries[i]; - - /* - * Skip non-ACTIVE entries. IDLE slots have no callback to invoke. - * (There's no PENDING or DISPATCHED state for update -- only - * IDLE and ACTIVE.) - */ if (e->state != UPDATE_CB_STATE_ACTIVE) continue; - /* - * Copy this entry's data into the snapshot: - * callback -- the function pointer we'll invoke in Phase 2 - * slot_index -- needed to reset this specific slot in Phase 2 - * is_final -- same for all entries (determined by status_code) - * handle_copy -- snprintf'd for safe logging (truncated to 255 chars) - */ + /* Copy entry data into stack-local snapshot */ snapshots[count].callback = e->callback; snapshots[count].slot_index = i; snapshots[count].is_final = is_final; @@ -2557,58 +1860,21 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da signal_data->progress_percent, is_final); } - /* - * Release the mutex. From this point, the main thread can freely - * register new callbacks. Our snapshot is a frozen copy -- it won't - * be affected by concurrent registry modifications. - */ + /* Unlock before invoking callbacks (deadlock prevention) */ pthread_mutex_unlock(&g_update_registry.mutex); FWUPMGR_INFO("dispatch_all_update_active: %d callback(s) to fire\n", count); /* ---- PHASE 2: invoke callbacks, no mutex held ---- */ - - /* - * Iterate through the snapshot array and invoke each callback. - * No mutex is held during callback invocation -- the app's callback - * can safely call any library function without deadlocking. - */ for (int i = 0; i < count; i++) { UpdateSnapshot *s = &snapshots[i]; FWUPMGR_INFO("dispatch_all_update_active: invoking callback " "for handle='%s'\n", s->handle_copy); - /* - * INVOKE THE APP'S CALLBACK - * - * Callback signature: void fn(int progress_per, UpdateStatus status) - * Matches the UpdateCallback typedef exactly. - * - * Arguments: - * signal_data->progress_percent -- 0 to 100 (how far along) - * status -- UPDATE_IN_PROGRESS, UPDATE_COMPLETED, or UPDATE_ERROR - * - * This runs on the BG thread. If the app needs to update UI, - * it must marshal the call to the main thread (e.g., via - * pthread_cond_signal, g_idle_add, or similar mechanism). - * - * The callback MUST NOT block for a long time, because it blocks - * this BG thread from processing further D-Bus signals. - */ s->callback(signal_data->progress_percent, status); - /* - * If this was a TERMINAL signal (COMPLETED or ERROR), reset - * the slot back to IDLE so it can be reused. - * - * We must RE-ACQUIRE the mutex to modify the registry. This is - * a brief lock/unlock pair -- just enough to reset one slot. - * - * If this was an IN-PROGRESS signal, we leave the slot ACTIVE. - * The next UpdateProgress signal will dispatch to the same - * callback again. - */ + /* If terminal signal, reset slot to IDLE */ if (s->is_final) { pthread_mutex_lock(&g_update_registry.mutex); update_registry_reset_slot(&g_update_registry.entries[s->slot_index]); @@ -2691,117 +1957,50 @@ static void dispatch_all_update_active(const InternalUpdateSignalData *signal_da bool internal_update_register_callback(FirmwareInterfaceHandle handle, UpdateCallback callback) { - /* - * Lock the update registry mutex. This mutex protects the entire - * g_update_registry.entries[] array from concurrent access. - * The BG thread also locks this mutex when dispatching callbacks. - */ pthread_mutex_lock(&g_update_registry.mutex); - /* - * We need to find a slot. Two pointers track our search: - * free_slot -- first IDLE entry found (available for use) - * existing_slot -- entry with same handle (dedup/overwrite case) - * - * Both start NULL. If neither is found after scanning, the registry - * is full of ACTIVE entries (all 30 slots occupied by other handles). - */ UpdateCbEntry *free_slot = NULL; UpdateCbEntry *existing_slot = NULL; - /* - * Linear scan through all 30 slots. We MUST scan the entire array - * even after finding a free slot, because we need to check for - * duplicate handles. An existing ACTIVE entry for the same handle - * takes priority over a free slot. - */ + /* Single-pass scan: look for dedup match (same handle) and first free slot */ for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { UpdateCbEntry *e = &g_update_registry.entries[i]; - /* - * Check for DEDUP: is this an ACTIVE entry with the same handle? - * - * If the app calls updateFirmware() twice with the same handle - * before the first update finishes, we find the old entry here - * and overwrite it rather than wasting a new slot. - * - * strcmp(e->handle_key, handle) == 0 means exact string match. - * The handle_key NULL check prevents strcmp(NULL, ...) crash. - */ if (e->state == UPDATE_CB_STATE_ACTIVE && e->handle_key != NULL && strcmp(e->handle_key, handle) == 0) { existing_slot = e; - break; /* Found duplicate -- no need to continue scanning */ + break; } - /* - * Track the FIRST idle slot we encounter. We only save the - * first one (free_slot == NULL check) because we want the - * lowest-indexed available slot for consistency. - */ if (free_slot == NULL && e->state == UPDATE_CB_STATE_IDLE) { free_slot = e; } } - /* - * Pick the target slot: - * - Prefer existing_slot (dedup/overwrite case) - * - Fall back to free_slot (normal allocation case) - * - If both NULL: registry is full, fail - */ UpdateCbEntry *target = existing_slot ? existing_slot : free_slot; if (target == NULL) { - /* - * All 30 slots are ACTIVE with different handles. This should - * never happen in practice -- a device rarely has more than - * one or two concurrent update clients. But we handle it - * gracefully by returning false instead of crashing. - */ FWUPMGR_ERROR("internal_update_register_callback: registry full (max=%d)\n", MAX_PENDING_CALLBACKS); pthread_mutex_unlock(&g_update_registry.mutex); return false; } - /* - * If we're overwriting an existing entry (same handle), free the - * old handle_key string to avoid a memory leak. The old strdup'd - * string is on the heap and must be explicitly freed. - */ + /* If overwriting, free old handle_key to avoid leak */ if (existing_slot) { FWUPMGR_INFO("internal_update_register_callback: " "overwriting existing for handle='%s'\n", handle); free(target->handle_key); - target->handle_key = NULL; /* Defensive: NULL before reassignment */ + target->handle_key = NULL; } - /* - * Populate the slot with the new callback registration. - * - * strdup(handle) creates a heap copy of the handle string "1". - * We need our own copy because the caller's string may go out of - * scope or be freed after updateFirmware() returns. - * - * UPDATE_CB_STATE_ACTIVE means this slot is ready to receive - * UpdateProgress signals. The BG thread's dispatch_all_update_active() - * will find it and invoke the callback. - * - * registered_time is recorded for diagnostic purposes (log how - * long a slot has been active). - */ + /* Populate slot: strdup handle, store callback, mark ACTIVE */ target->handle_key = strdup(handle); target->callback = callback; target->state = UPDATE_CB_STATE_ACTIVE; target->registered_time = time(NULL); - /* - * Unlock the mutex. The slot is now visible to the BG thread. - * From this point, any UpdateProgress signal will find our ACTIVE - * entry and invoke the callback. - */ pthread_mutex_unlock(&g_update_registry.mutex); FWUPMGR_INFO("internal_update_register_callback: registered handle='%s'\n", @@ -2850,39 +2049,12 @@ bool internal_update_register_callback(FirmwareInterfaceHandle handle, */ static void update_registry_reset_slot(UpdateCbEntry *entry) { - /* - * Free the handle_key string if it exists. - * - * handle_key was allocated by strdup() in - * internal_update_register_callback(). We must free it to avoid - * a memory leak. The NULL check is defensive -- an IDLE slot - * should already have handle_key == NULL, but we check anyway. - */ if (entry->handle_key != NULL) { free(entry->handle_key); - entry->handle_key = NULL; /* Prevent dangling pointer / double-free */ + entry->handle_key = NULL; } - - /* - * Clear the callback function pointer. Setting to NULL ensures - * that even if something accidentally tries to invoke this slot's - * callback, it will be a NULL dereference (crash) rather than - * calling a stale function pointer (undefined behavior / security risk). - */ entry->callback = NULL; - - /* - * Reset the registration timestamp. Not strictly necessary for - * correctness, but keeps the slot in a clean, known state. - */ entry->registered_time = 0; - - /* - * Set state back to IDLE. This is the critical line -- it's what - * makes the slot available for reuse by the next - * internal_update_register_callback() call. The linear scan in - * that function looks for UPDATE_CB_STATE_IDLE to find free slots. - */ entry->state = UPDATE_CB_STATE_IDLE; } @@ -2949,25 +2121,9 @@ static void update_registry_reset_slot(UpdateCbEntry *entry) bool internal_parse_update_signal_data(GVariant *parameters, InternalUpdateSignalData *out_data) { - /* - * NULL checks on both parameters. If either is NULL, we can't - * proceed -- return false immediately. No cleanup needed because - * nothing has been allocated yet. - */ if (parameters == NULL || out_data == NULL) return false; - /* - * Verify the GVariant's type signature matches what we expect. - * - * g_variant_get_type_string() returns the GVariant's type as a - * string, e.g. "(tsiis)". If the daemon sends a different format - * (e.g., due to a version mismatch), we'd get garbage if we tried - * to extract with the wrong format. This check catches mismatches - * early with a clear error message. - * - * strcmp returns 0 if the strings are equal. If NOT equal (non-zero), - * log the unexpected signature and return false. - */ + /* Verify type signature before extracting */ const gchar *sig = g_variant_get_type_string(parameters); if (strcmp(sig, "(tsiis)") != 0) { FWUPMGR_ERROR("internal_parse_update_signal_data: " @@ -2975,37 +2131,12 @@ bool internal_parse_update_signal_data(GVariant *parameters, return false; } - /* - * Declare local variables to receive the extracted values. - * We use typed locals rather than extracting directly into out_data - * for clarity and to match the GLib API pattern. - * - * guint64 handler_id -- maps to 't' (uint64) - * gchar *firmware_name -- maps to 's' (string, heap-allocated by g_variant_get) - * gint32 progress -- maps to 'i' (int32) - * gint32 status -- maps to 'i' (int32) - * gchar *message_str -- maps to 's' (string, heap-allocated by g_variant_get) - */ guint64 handler_id = 0; gchar *firmware_name = NULL; gint32 progress = 0; gint32 status = 0; gchar *message_str = NULL; - /* - * Extract all five fields from the GVariant in one call. - * - * g_variant_get() is the inverse of g_variant_new(). The format - * string "(tsiis)" tells GLib how to interpret the binary data: - * t -> extract as guint64, store at &handler_id - * s -> extract as string, allocate copy, store at &firmware_name - * i -> extract as gint32, store at &progress - * i -> extract as gint32, store at &status - * s -> extract as string, allocate copy, store at &message_str - * - * The 's' format specifier ALWAYS allocates a new string (g_strdup). - * This is why the caller must g_free() firmware_name and message. - */ g_variant_get(parameters, "(tsiis)", &handler_id, &firmware_name, @@ -3013,22 +2144,12 @@ bool internal_parse_update_signal_data(GVariant *parameters, &status, &message_str); - /* - * Copy the extracted values into the output struct. - * - * For strings (firmware_name, message): we transfer OWNERSHIP of - * the heap-allocated string to the caller via the out_data struct. - * The caller (on_update_progress_signal) is responsible for calling - * g_free() on these pointers when done. - * - * For integers (handler_id, progress_percent, status_code): these - * are simple value copies. No heap allocation, no cleanup needed. - */ + /* Transfer ownership of GLib-allocated strings to caller (must g_free) */ out_data->handler_id = handler_id; - out_data->firmware_name = firmware_name; /* Caller must g_free */ + out_data->firmware_name = firmware_name; out_data->progress_percent = progress; out_data->status_code = status; - out_data->message = message_str; /* Caller must g_free */ + out_data->message = message_str; return true; } @@ -3077,33 +2198,9 @@ bool internal_parse_update_signal_data(GVariant *parameters, UpdateStatus internal_map_update_status_code(int32_t status_code) { switch (status_code) { - /* - * 0 = IN_PROGRESS: The daemon is still flashing the firmware. - * More UpdateProgress signals will follow. The callback slot - * stays ACTIVE. - */ case 0: return UPDATE_IN_PROGRESS; - - /* - * 1 = COMPLETED: The firmware was flashed successfully. - * This is a TERMINAL status -- no more signals will come. - * dispatch_all_update_active() will reset the slot to IDLE. - */ case 1: return UPDATE_COMPLETED; - - /* - * 2 = ERROR: The firmware flash failed. - * This is a TERMINAL status -- no more signals will come. - * dispatch_all_update_active() will reset the slot to IDLE. - */ case 2: return UPDATE_ERROR; - - /* - * Unknown status code. This should never happen with a matching - * daemon version. But if the daemon is newer and adds status - * code 3 (e.g., "PAUSED"), we default to ERROR so the slot - * gets cleaned up rather than stuck ACTIVE forever. - */ default: FWUPMGR_ERROR("internal_map_update_status_code: " "unknown %d -> UPDATE_ERROR\n", status_code); diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index a3353f81..13717ecc 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -326,125 +326,30 @@ static bool validate_lib_version(const char *libVersion) */ FirmwareInterfaceHandle registerProcess(const char *processName, const char *libVersion) { - /* - * LOCAL VARIABLE DECLARATIONS - * - * Why declare all at the top? - * - Makes it easy to see ALL resources this function manages - * - Every non-NULL pointer here must be freed/unreffed on every exit path - */ - - /* GDBusProxy: A GLib object representing a remote D-Bus interface. - * Think of it as a "remote method caller" -- it knows the bus name, - * object path, and interface, and can invoke methods on the daemon. - * Must be freed with g_object_unref() when done. */ GDBusProxy *proxy = NULL; - - /* GError: GLib's error reporting mechanism. When a GLib function fails, - * it allocates a GError and fills in a human-readable message + error code. - * We pass &error to functions, they set it on failure. - * Must be freed with g_error_free() if non-NULL. */ GError *error = NULL; - - /* GVariant: GLib's type-safe, immutable, reference-counted value container. - * D-Bus messages are encoded as GVariants. The daemon's reply will be a - * GVariant with type "(t)" -- a tuple containing one uint64. - * Must be freed with g_variant_unref() when done. */ GVariant *result = NULL; - - /* handler_id: The daemon's response -- a 64-bit unsigned integer that - * uniquely identifies our registration. The daemon increments a counter - * (starting at 1) for each new registration. So the first client gets 1, - * second gets 2, etc. We'll convert this to a string for the handle. */ guint64 handler_id = 0; - - /* handle_str: The actual string we return to the caller. Heap-allocated, - * 32 bytes (enough for the decimal representation of any uint64 value, - * since max uint64 = 18446744073709551615 = 20 digits + null terminator). - * This pointer IS the FirmwareInterfaceHandle. */ char *handle_str = NULL; - /* - * [STEP 1] LOGGING -- Entry point trace - * - * Why log the parameters? - * - For field debugging: if something goes wrong later, the log shows - * exactly what the client passed in. - * - The ternary (?:) guards against NULL dereference in printf. - * Without it, printf("%s", NULL) is UNDEFINED BEHAVIOR in C - * (crashes on some platforms, prints "(null)" on others). - */ + /* Log entry */ FWUPMGR_INFO("registerProcess() called\n"); FWUPMGR_INFO(" processName: '%s'\n", processName ? processName : "NULL"); FWUPMGR_INFO(" libVersion: '%s'\n", libVersion ? libVersion : "NULL"); - /* - * [STEP 2] VALIDATE processName - * - * Why validate here (at the library boundary) instead of letting the - * daemon validate? - * 1. Fail fast -- no point creating a D-Bus connection for bad input - * 2. Saves a round-trip to the daemon (D-Bus call has ~5ms overhead) - * 3. Defense in depth -- even if daemon validates too, we catch it early - * 4. Better error messages (we know the context; daemon doesn't know - * which client messed up) - * - * validate_process_name() checks: - * - processName != NULL - * - strlen(processName) > 0 (not empty "") - * - strlen(processName) <= 256 (MAX_PROCESS_NAME_LEN) - * Returns false (and logs FWUPMGR_ERROR) if any check fails. - */ + /* Validate processName */ if (!validate_process_name(processName)) { - /* Validation failed -- error already logged inside validate_process_name(). - * Return NULL immediately. No resources to clean up (we haven't - * allocated anything yet). */ return NULL; } - /* - * [STEP 3] VALIDATE libVersion - * - * validate_lib_version() checks: - * - libVersion != NULL (empty string "" IS allowed -- means "unknown") - * - strlen(libVersion) <= 64 (MAX_LIB_VERSION_LEN) - */ + /* Validate libVersion */ if (!validate_lib_version(libVersion)) { return NULL; } - /* - * [STEP 4] CREATE D-BUS PROXY (ephemeral connection) - * - * What happens inside create_dbus_proxy(): - * 1. g_bus_get_sync(G_BUS_TYPE_SYSTEM) -- connects to the system D-Bus. - * This is a BLOCKING call. The D-Bus daemon (dbus-daemon) assigns us - * a unique connection name like ":1.140". - * 2. g_dbus_proxy_new_sync() -- creates a proxy object that can call - * methods on "org.rdkfwupdater.Service" at "/org/rdkfwupdater/Service" - * on interface "org.rdkfwupdater.Interface". - * 3. The raw GDBusConnection is unreffed (proxy keeps its own internal ref). - * - * Why "ephemeral"? - * This connection lives only for THIS function call. After we get the - * daemon's response, we destroy the proxy (and its connection) immediately. - * Next API call (checkForUpdate, etc.) will create a brand new connection - * with a DIFFERENT sender ID. The daemon tracks us by handler_id, not - * by D-Bus sender address -- that's why this stateless model works. - * - * Common failure causes: - * - dbus-daemon not running (container/chroot without D-Bus) - * - D-Bus policy denying our connection (security policy file) - * - System bus socket not accessible (/var/run/dbus/system_bus_socket) - */ + /* Create ephemeral D-Bus proxy */ proxy = create_dbus_proxy(&error); if (!proxy) { - /* create_dbus_proxy() already logged the specific error. - * We just need to clean up the GError (if it was allocated). - * - * Why check 'error' for NULL? - * In extreme OOM conditions, GLib may fail without setting the - * GError pointer. Calling g_error_free(NULL) would crash. */ if (error) { g_error_free(error); } @@ -453,51 +358,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib FWUPMGR_INFO("D-Bus proxy created successfully\n"); - /* - * [STEP 5] SEND "RegisterProcess" D-BUS METHOD CALL - * - * This is the CORE of the function -- the actual IPC with the daemon. - * - * g_dbus_proxy_call_sync() does: - * 1. Marshals our arguments into D-Bus wire format - * 2. Sends the message to the daemon via dbus-daemon - * 3. BLOCKS until the daemon replies (or timeout expires) - * 4. Returns the reply as a GVariant, or NULL + GError on failure - * - * Parameters explained: - * proxy -- The proxy we just created (knows where to send) - * "RegisterProcess" -- The D-Bus method name on the daemon's interface - * g_variant_new("(ss)", processName, libVersion) - * -- The arguments, encoded as a GVariant TUPLE of - * two strings. "(ss)" is the D-Bus TYPE SIGNATURE: - * '(' = start tuple, 's' = string, 's' = string, ')' = end - * GLib takes ownership of this GVariant (don't free it!) - * G_DBUS_CALL_FLAGS_NONE -- No special flags (could use NO_AUTO_START - * to prevent D-Bus activation, but we want - * the daemon to auto-start if not running) - * DBUS_SYNC_TIMEOUT_MS -- 10000 ms (10 seconds). If daemon doesn't reply - * within this time, the call fails with timeout error. - * NULL -- No GCancellable (we can't cancel this operation) - * &error -- Where to store error details on failure - * - * WHAT THE DAEMON DOES WHEN IT RECEIVES THIS: - * 1. Extracts processName and libVersion from the message - * 2. Calls add_process_to_tracking() which: - * a. Checks if any existing registration has the same process_name - * -- REJECTS if name is already taken by different client - * b. Checks if this client (sender_id) is already registered - * -- REJECTS if trying to register a second name - * -- Returns existing handler_id if re-registering same name (idempotent) - * c. Otherwise: allocates ProcessInfo, assigns next_process_id++, - * stores in hash table, returns the new handler_id - * 3. Sends reply: GVariant "(t)" containing the handler_id (uint64) - * - * BLOCKING BEHAVIOR: - * Our thread is SUSPENDED here until: - * a) Daemon replies (typically <10ms) -- we get 'result' - * b) 10-second timeout expires -- we get NULL + timeout GError - * c) D-Bus daemon signals an error -- we get NULL + error GError - */ + /* Synchronous D-Bus call: RegisterProcess(processName, libVersion) → handler_id */ FWUPMGR_INFO("Calling RegisterProcess D-Bus method...\n"); result = g_dbus_proxy_call_sync( proxy, @@ -510,15 +371,6 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib ); if (!result) { - /* D-Bus call failed. Common causes: - * - Daemon not running ("The name was not provided by any .service files") - * - Daemon crashed during handling - * - 10-second timeout expired (daemon overloaded) - * - Daemon explicitly rejected us (process name conflict, D-Bus error reply) - * - * RESOURCE CLEANUP: - * We must free: error (GError) + proxy (GDBusProxy) - * We do NOT have: result, handle_str, handler_id (never obtained) */ FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", error->message); g_error_free(error); @@ -526,75 +378,17 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib return NULL; } - /* - * [STEP 6] EXTRACT handler_id FROM DAEMON'S REPLY - * - * The daemon replied with GVariant type "(t)": - * '(' = tuple start - * 't' = uint64 (guint64 in GLib) - * ')' = tuple end - * - * g_variant_get() deserializes the GVariant into our C variable. - * The format string "(t)" must EXACTLY match what the daemon sent, - * or we get undefined behavior (buffer overread/corruption). - * - * After extraction, we free the GVariant (result) and the proxy. - * At this point: - * - We have the handler_id (e.g., 1) - * - The D-Bus connection is GONE (proxy unreffed = connection closed) - * - The daemon holds a ProcessInfo record for us in its hash table - * - No way to communicate with daemon anymore until we create - * another connection (which checkForUpdate etc. will do) - */ + /* Extract handler_id from daemon reply "(t)" and free D-Bus resources */ g_variant_get(result, "(t)", &handler_id); - g_variant_unref(result); /* Free the GVariant container -- we extracted the value */ - g_object_unref(proxy); /* Free the proxy -- closes underlying D-Bus connection */ + g_variant_unref(result); + g_object_unref(proxy); FWUPMGR_INFO("Registration successful\n"); FWUPMGR_INFO(" handler_id: %"G_GUINT64_FORMAT"\n", handler_id); - /* - * [STEP 7] ALLOCATE HANDLE STRING ON HEAP - * - * Why 32 bytes? - * - Max uint64 decimal: 18446744073709551615 (20 chars) - * - Plus null terminator: 21 bytes needed - * - We allocate 32 for alignment and future-proofing - * - * Why malloc() and not a struct or stack buffer? - * - The handle must OUTLIVE this function (returned to caller) - * - Stack allocation would be invalid after return (dangling pointer!) - * - A simple string is the simplest possible handle format: - * - No struct versioning issues - * - Easy to log/print for debugging - * - Trivial to pass across thread boundaries - * - Can be validated with strtoull() in unregisterProcess() - * - * Why (char*) cast? - * - malloc() returns void*. In C this is implicitly convertible to - * char*, but the cast makes intent clear and satisfies C++ compilers - * if this file is ever compiled as C++ (unlikely but defensive). - */ - handle_str = (char*)malloc(32); /* Enough for uint64 as decimal string */ + /* Allocate handle string (32 bytes: enough for max uint64 decimal) */ + handle_str = (char*)malloc(32); if (!handle_str) { - /* - * OOM (Out Of Memory) RECOVERY PATH - * - * SITUATION: We SUCCESSFULLY registered with the daemon (it holds - * a ProcessInfo for us), but we can't allocate 32 bytes of RAM. - * - * PROBLEM: If we just return NULL, the daemon keeps our registration - * forever (resource leak). The client can't call unregisterProcess() - * because they don't have a handle. Nobody will clean this up. - * - * SOLUTION: Best-effort UnregisterProcess call to the daemon. - * "Best-effort" means: if this cleanup also fails (e.g., because - * the OOM is so severe we can't even create a D-Bus proxy), we - * log it and accept the leak. This is an extremely rare edge case. - * - * Why create a NEW proxy? Because we already unreffed the old one - * in Step 6. We can't reuse it. - */ FWUPMGR_ERROR("Failed to allocate memory for handle\n"); /* CRITICAL: Registration succeeded on daemon, but we can't return handle. @@ -633,121 +427,14 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib return NULL; } - /* - * [STEP 8] CONVERT handler_id TO DECIMAL STRING - * - * snprintf(buffer, size, format, ...): - * - Writes at most 'size' bytes (including null terminator) - * - GUARANTEED null-terminated (unlike strncpy!) - * - Returns number of chars that WOULD have been written (for truncation detection) - * - * "%" PRIu64 expands to the platform-correct format specifier for uint64_t. - * On Linux/GLib: PRIu64 = "lu" (unsigned long), so format = "%lu" - * On Windows: PRIu64 = "I64u" - * Using PRIu64 instead of "%lu" ensures portability. - * - * After this line: handle_str = "1" (for the first registration ever) - * - * Example values: "1", "2", "42", "18446744073709551615" - */ + /* Convert handler_id to decimal string */ snprintf(handle_str, 32, "%" PRIu64, handler_id); FWUPMGR_INFO("Handle created: '%s'\n", handle_str); - /* - * [STEP 9] START THE INTERNAL ASYNC ENGINE - * - * This is where the library's internal machinery comes to life. - * BEFORE this call: Only 1 thread exists (the caller's main thread). - * AFTER this call: 2 threads exist (caller's + library BG thread). - * - * internal_system_init() does ALL of the following: - * - * Phase A: Initialize Check Callback Registry - * memset(&g_registry, 0, sizeof(g_registry)); - * pthread_mutex_init(&g_registry.mutex, NULL); - * g_registry.initialized = true; - * - * Result: 30-slot array, all CB_STATE_IDLE, protected by mutex - * Purpose: Will store callbacks for checkForUpdate() later - * - * Phase B: Initialize Background Thread Sync - * memset(&g_bg_thread, 0, sizeof(g_bg_thread)); - * pthread_mutex_init(&g_bg_thread.mutex, NULL); - * g_bg_thread.running = false; - * - * This mutex protects ONE bool: g_bg_thread.running - * Used for startup handshake between main thread and BG thread - * - * Phase C: Create ISOLATED GLib Event Loop - * g_bg_thread.context = g_main_context_new(); - * g_bg_thread.main_loop = g_main_loop_new(context, FALSE); - * - * WHY a new context (not the default)? - * If the client app uses GTK or its own GMainLoop, we'd be - * injecting our signal handlers into THEIR event loop. Their UI - * callbacks and our firmware callbacks would run interleaved on - * the same thread -- causing thread-safety bugs. Our own context - * guarantees our signals fire ONLY in our BG thread. - * - * Phase D: Spawn Background Thread - * pthread_create(&g_bg_thread.thread, NULL, - * background_thread_func, NULL); - * - * The BG thread immediately: - * 1. Pushes our GMainContext as its thread-default - * 2. Creates a PERSISTENT D-Bus connection (different from ours!) - * 3. Subscribes to 3 signals: - * - CheckForUpdateComplete -> on_check_complete_signal() - * - DownloadProgress -> on_download_progress_signal() - * - UpdateProgress -> on_update_progress_signal() - * 4. Sets g_bg_thread.running = true (under mutex) - * 5. Calls g_main_loop_run() -- BLOCKS FOREVER waiting for signals - * - * Phase E: Main Thread Spin-Waits for BG Thread Readiness - * for (int i = 0; i < 50; i++) { max 50 x 100ms = 5 seconds - * lock(g_bg_thread.mutex); - * bool ready = g_bg_thread.running; - * unlock(g_bg_thread.mutex); - * if (ready) break; - * nanosleep(100ms); - * } - * - * WHY spin-wait instead of condvar? - * Simplicity. This is a one-time startup. A condvar would add - * complexity for ~200ms of waiting. Not worth it. - * - * Phase F: Initialize Download and Update Registries - * memset(&g_dwnl_registry, 0, sizeof(...)); - * pthread_mutex_init(&g_dwnl_registry.mutex, NULL); - * g_dwnl_registry.initialized = true; - * - * memset(&g_update_registry, 0, sizeof(...)); - * pthread_mutex_init(&g_update_registry.mutex, NULL); - * g_update_registry.initialized = true; - * - * AFTER internal_system_init() returns successfully: - * THREADS: 2 (caller's + BG thread blocked in g_main_loop_run) - * MUTEXES: 4 (g_registry.mutex, g_bg_thread.mutex, - * g_dwnl_registry.mutex, g_update_registry.mutex) - * REGISTRIES: 3 (all 30 slots IDLE, ready to accept callbacks) - * D-BUS: 1 persistent connection in BG thread (for signal reception) - * - * If internal_system_init() returns non-zero (failure): - * The async engine is partially or fully uninitialized. Returning - * the handle here would leave the library in a broken state: - * - Callbacks would never fire (no BG thread / no registries) - * - unregisterProcess() would call internal_system_deinit() on - * uninitialized globals (pthread_join on invalid thread = UB) - * So we roll back: best-effort unregister from daemon, free - * handle_str, return NULL to signal failure to the caller. - */ + /* Initialize async engine: registries, mutexes, BG thread */ FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen ===\n"); if (internal_system_init() != 0) { FWUPMGR_ERROR("rdkFwupdateMgr_lib_init: internal_system_init FAILED\n"); - - /* Best-effort: tell the daemon to drop our registration */ - FWUPMGR_ERROR("Attempting best-effort cleanup: UnregisterProcess(%" PRIu64 ")\n", - handler_id); GError *cleanup_error = NULL; GDBusProxy *cleanup_proxy = create_dbus_proxy(&cleanup_error); if (cleanup_proxy) { @@ -779,32 +466,7 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib } FWUPMGR_INFO("=== rdkFwupdateMgr Creating thread for listen successful ===\n"); - /* - * [STEP 10] RETURN THE HANDLE TO THE CALLER - * - * FirmwareInterfaceHandle is typedef'd as (char*) in the public header. - * We cast here to make the type explicit, even though char* to char* - * doesn't technically need a cast. It documents intent. - * - * What the caller receives: A pointer to a heap-allocated string like "1". - * - * STATE OF THE WORLD after this return: - * - * Caller's process: - * Main thread: running (has the handle, about to call checkForUpdate etc.) - * BG thread: BLOCKED in g_main_loop_run(), waiting for D-Bus signals - * Owns persistent D-Bus connection (e.g., :1.141) - * Subscribed to CheckForUpdateComplete, DownloadProgress, UpdateProgress - * - * Daemon process: - * registered_processes hash table contains: - * key=1 -> ProcessInfo { handler_id=1, process_name="example_plugin", - * lib_version="1.0.0", sender_id=":1.140" } - * - * D-Bus connections: - * :1.140 -- DEAD (was our ephemeral connection, already closed) - * :1.141 -- ALIVE (BG thread's persistent connection for signals) - */ + /* Return handle to caller */ return (FirmwareInterfaceHandle)handle_str; } @@ -898,48 +560,13 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib */ void unregisterProcess(FirmwareInterfaceHandle handler) { - /* - * LOCAL VARIABLE DECLARATIONS - * - * Same pattern as registerProcess(): declare all at the top so you - * can see every resource this function manages at a glance. - */ - - /* GDBusProxy for the D-Bus call to the daemon. - * Created in step 4, freed with g_object_unref() in step 6. - * May be NULL if proxy creation fails (D-Bus down, daemon gone). */ GDBusProxy *proxy = NULL; - - /* GError for capturing GLib/D-Bus error details. - * Must be freed with g_error_free() if non-NULL. */ GError *error = NULL; - - /* GVariant holding the daemon's reply to UnregisterProcess. - * Type "(b)" -- a tuple containing one boolean (success/failure). - * Must be freed with g_variant_unref() when done. */ GVariant *result = NULL; - - /* The numeric handler_id we'll extract from the handle string. - * This is what the daemon uses to look up our ProcessInfo entry. - * Parsed from the handle string "1" -> handler_id = 1. */ guint64 handler_id = 0; - - /* The daemon's reply: TRUE if it found and removed our registration, - * FALSE if handler_id was not found (already removed, or invalid). */ gboolean success = FALSE; - /* - * [STEP 1] NULL CHECK -- safe no-op for NULL handles - * - * Why allow NULL? - * Client code often has cleanup paths like: - * cleanup: - * unregisterProcess(handle); - * If registerProcess() failed, handle is NULL. Making this a safe - * no-op avoids the caller needing "if (handle != NULL)" every time. - * - * No resources to clean up here -- we haven't allocated anything. - */ + /* NULL handle is a safe no-op */ if (!handler) { FWUPMGR_INFO("unregisterProcess() called with NULL handle (no-op)\n"); return; @@ -948,90 +575,23 @@ void unregisterProcess(FirmwareInterfaceHandle handler) FWUPMGR_INFO("unregisterProcess() called\n"); FWUPMGR_INFO(" handle: '%s'\n", handler); - /* - * [STEP 2] PARSE THE HANDLE STRING TO uint64 - * - * The handle is a string like "1" or "42". We need to convert it - * back to a uint64 handler_id for the D-Bus call to the daemon. - * - * Why strtoull() instead of atoi() or sscanf()? - * - atoi() has NO error detection. atoi("abc") returns 0 silently. - * - sscanf() doesn't detect trailing garbage: sscanf("123abc", "%llu") - * happily returns 123 and ignores "abc". - * - strtoull() with endptr checking is the ONLY way in C to do - * strict numeric parsing: - * - Sets errno on overflow - * - endptr tells you exactly where parsing stopped - * - You can reject partial parses ("123abc") by checking *endptr - * - * Why set errno = 0 before the call? - * strtoull() only sets errno on error. If errno was already non-zero - * from some earlier unrelated call, we'd get a false positive. - * Always clear errno before calling strto* functions. - * - * What is endptr? - * After strtoull("123abc", &endptr, 10): - * - Return value = 123 - * - endptr points to 'a' (first character it couldn't parse) - * After strtoull("123", &endptr, 10): - * - Return value = 123 - * - endptr points to '\0' (end of string -- everything parsed) - * After strtoull("abc", &endptr, 10): - * - Return value = 0 - * - endptr == handler (didn't move -- nothing parsed) - */ + /* Parse handle string to uint64 handler_id (strict validation) */ errno = 0; char *endptr = NULL; handler_id = strtoull(handler, &endptr, 10); - /* - * VALIDATION CHECK 1: Numeric overflow - * - * If the string represents a number larger than ULLONG_MAX - * (18446744073709551615), strtoull() returns ULLONG_MAX and sets - * errno to ERANGE. This catches "99999999999999999999999". - */ if (errno != 0) { FWUPMGR_ERROR("Invalid handle: numeric overflow/underflow in '%s'\n", handler); free(handler); return; } - /* - * VALIDATION CHECK 2: No digits at all - * - * If endptr == handler, strtoull() didn't find any digits. - * This catches "abc", "", and other non-numeric strings. - * - * Why is this separate from the *endptr check below? - * strtoull("abc") returns 0 with endptr pointing to 'a'. - * *endptr != '\0' would also catch it, but "endptr == handler" - * gives a more specific error message: "no digits found" - * vs "garbage after number". - */ if (endptr == handler) { FWUPMGR_ERROR("Invalid handle: no digits found in '%s'\n", handler); free(handler); return; } - /* - * VALIDATION CHECK 3: Trailing garbage after the number - * - * If *endptr is not the null terminator, there are characters - * after the valid number. This catches: - * "123abc" (endptr points to 'a') - * "123 " (endptr points to ' ') - * " 123" (strtoull skips leading whitespace, so this actually - * parses as 123 with endptr at '\0' -- PASSES this check. - * But our registerProcess() never creates handles with - * leading spaces, so this is academic.) - * - * Why is this important? - * If the handle string is corrupted (memory corruption elsewhere), - * it might look like "1\x03garbage". We don't want to silently - * parse it as handler_id=1 and proceed -- that could mask a bug. - */ if (*endptr != '\0') { FWUPMGR_ERROR("Invalid handle: garbage characters after number in '%s' " "(parsed %" PRIu64 ", but '%s' remains)\n", @@ -1040,146 +600,20 @@ void unregisterProcess(FirmwareInterfaceHandle handler) return; } - /* - * VALIDATION CHECK 4: handler_id must be > 0 - * - * The daemon assigns handler_ids starting at 1 (next_process_id = 1, - * post-increment). A handler_id of 0 is NEVER valid. If we got 0, - * either the string was literally "0" or something went wrong. - * - * The daemon also rejects handler_id == 0 on its side (returns - * D-Bus error), but we catch it here to avoid a wasted round-trip. - */ if (handler_id == 0) { FWUPMGR_ERROR("Invalid handle: handler_id cannot be 0\n"); free(handler); return; } - /* - * NOTE ON free(handler) IN ALL ERROR PATHS ABOVE: - * - * Every validation failure path calls free(handler) before returning. - * This is critical: the handle string was malloc'd by registerProcess(). - * If we return without freeing it, that's a memory leak. The caller - * will likely set their pointer to NULL after this call, so nobody - * else will free it. - * - * Also note: internal_system_deinit() is NOT called in these paths. - * If the handle is corrupt, we don't know what state the system is in. - * The BG thread and mutexes leak, but that's acceptable -- handle - * corruption means something catastrophic happened, and the OS will - * reclaim everything when the process exits anyway. - */ - FWUPMGR_INFO(" handler_id: %"G_GUINT64_FORMAT"\n", handler_id); - /* - * [STEP 3] TEAR DOWN THE INTERNAL ASYNC ENGINE - * - * This is the most critical step. We tear down EVERYTHING that - * internal_system_init() created during registerProcess(). - * - * BEFORE this call: - * Threads: 2 (main + BG) - * Mutexes: 4 (g_registry, g_bg_thread, g_dwnl_registry, g_update_registry) - * Registries: 3 (check, download, update -- all 30 slots each) - * D-Bus: 1 persistent connection in BG thread (:1.141) - * GLib: 1 GMainLoop + 1 GMainContext (owned by BG thread) - * - * AFTER this call: - * Threads: 1 (main only -- BG thread joined and dead) - * Mutexes: 0 (all 4 destroyed) - * Registries: 3 (handle_key strings freed, but struct memory is static) - * D-Bus: 0 (BG thread closed its connection during cleanup) - * GLib: 0 (main_loop and context unref'd) - * - * internal_system_deinit() does the following, in this exact order: - * - * 1. g_main_loop_quit(g_bg_thread.main_loop) - * Sends a "quit" signal to the GLib event loop that the BG thread - * is blocking in. This is THREAD-SAFE -- GLib explicitly allows - * calling quit from a different thread than the one running the loop. - * Internally, GLib writes to a wakeup pipe/eventfd. - * - * When quit fires, the BG thread's g_main_loop_run() returns. - * The BG thread then: - * a. Unsubscribes from all D-Bus signals (no more callbacks) - * b. g_object_unref(connection) -- closes :1.141 - * c. g_main_context_pop_thread_default() -- detaches context - * d. return NULL -- pthread exits - * - * 2. pthread_join(g_bg_thread.thread, NULL) - * BLOCKS the main thread until the BG thread has fully exited. - * After this returns: - * - The BG thread is DEAD (its stack is freed by the OS) - * - No more signal callbacks can fire - * - No more mutex contention on registries - * - It is safe to destroy mutexes - * - * WHY is pthread_join essential? - * If we skipped it and went straight to mutex_destroy, the BG - * thread might still be holding g_registry.mutex while dispatching - * a late-arriving signal. pthread_mutex_destroy on a locked mutex - * is UNDEFINED BEHAVIOR (potential crash or silent corruption). - * - * 3. g_main_loop_unref() + g_main_context_unref() - * Free the GLib event loop objects. The BG thread already popped - * the context, so these are the final references. - * - * 4. pthread_mutex_destroy(&g_bg_thread.mutex) - * Destroy the mutex that protected g_bg_thread.running. - * Nobody uses it anymore -- we just joined the only other thread. - * Mutexes remaining: 3 - * - * 5. internal_dwnl_system_deinit() - * Lock g_dwnl_registry.mutex, iterate all 30 download slots, - * free() any non-NULL handle_key strings (from strdup during - * downloadFirmware calls), unlock, then destroy the mutex. - * Mutexes remaining: 2 - * - * Why lock even though only 1 thread exists? Defensive coding. - * If someone refactors and this runs while threads are alive, - * the lock prevents a race. - * - * 6. internal_update_system_deinit() - * Same pattern as download. Lock, free handle_keys, destroy mutex. - * Mutexes remaining: 1 - * - * 7. Lock g_registry.mutex, free all check-registry handle_keys, - * unlock, destroy mutex. - * Mutexes remaining: 0 - * - * After internal_system_deinit() returns, the library is in a - * "dormant" state: no threads, no mutexes, no D-Bus connections. - * Only the handle string and handler_id still exist. - */ + /* Tear down async engine: kill BG thread, destroy mutexes, free registries */ FWUPMGR_INFO("=== rdkFwupdateMgr destroy thread unloading ===\n"); internal_system_deinit(); FWUPMGR_INFO("=== rdkFwupdateMgr destroy thread ===\n"); - /* - * [STEP 4] CREATE D-BUS PROXY (best-effort) - * - * Same as registerProcess() -- create_dbus_proxy() opens a NEW - * ephemeral D-Bus connection, gets a new unique sender name - * (e.g., :1.145), and creates a GDBusProxy for method calls. - * - * Why "best-effort"? - * If the daemon has crashed, the D-Bus bus is down, or the system - * bus socket is inaccessible, proxy creation fails. That's OK. - * The important cleanup (BG thread, mutexes, memory) was already - * done in step 3. The D-Bus call is just a courtesy to the daemon. - * - * Why FWUPMGR_WARN and not FWUPMGR_ERROR? - * WARN means "something unexpected happened but we can continue." - * ERROR means "we're returning a failure code to the caller." - * Since unregisterProcess() is void and always succeeds from the - * caller's perspective, failures here are warnings, not errors. - * - * Note: free(handler) is called even when proxy creation fails. - * The handle MUST be freed on every path. No exceptions. - */ + /*Create D-Bus proxy (best-effort -- local cleanup already done) */ proxy = create_dbus_proxy(&error); if (!proxy) { FWUPMGR_WARN("Failed to create D-Bus proxy for unregister\n"); @@ -1187,58 +621,11 @@ void unregisterProcess(FirmwareInterfaceHandle handler) FWUPMGR_WARN(" Error: %s\n", error->message); g_error_free(error); } - /* Continue with cleanup even if D-Bus call fails */ free(handler); return; } - /* - * [STEP 5] SEND "UnregisterProcess" D-BUS METHOD CALL - * - * g_dbus_proxy_call_sync() -- same as in registerProcess(), but - * with a different method name and argument type. - * - * Wire format: - * Method name: "UnregisterProcess" - * Arguments: g_variant_new("(t)", handler_id) - * "(t)" = a tuple containing one uint64 - * We send handler_id = 1 - * - * Expected reply: - * "(b)" = a tuple containing one boolean - * TRUE = daemon found the registration and removed it - * FALSE = daemon didn't find handler_id (already removed, or unknown) - * - * BLOCKING BEHAVIOR: - * Main thread blocks here for up to DBUS_SYNC_TIMEOUT_MS (10 seconds). - * Typical response time: <5ms (just a hash table lookup + remove). - * - * WHAT THE DAEMON DOES: - * 1. Extracts handler_id from the message: g_variant_get("(t)", &handler) - * 2. Validates handler != 0 (rejects with D-Bus error if 0) - * 3. Looks up ProcessInfo in registered_processes hash table - * 4. Calls remove_process_from_tracking(handler_id): - * - g_hash_table_lookup(registered_processes, handler_id) - * - If found: g_hash_table_remove() which also calls g_free() - * on the ProcessInfo struct (freeing process_name, lib_version, - * sender_id strings that were g_strdup'd during registration) - * - Returns TRUE if found and removed, FALSE if not found - * 5. Sends reply: g_variant_new("(b)", TRUE/FALSE) - * - * NOTE: The daemon ignores the sender_id of this call. - * Our registerProcess was sent from :1.140, but this unregister - * comes from :1.145 (different ephemeral connection). The daemon - * matches ONLY by handler_id. The sender_id parameter in - * remove_process_from_tracking() is unused (cast to void). - * - * FAILURE HANDLING: - * If this call fails (timeout, daemon crashed, bus error): - * - Log a WARNING (not error -- best-effort) - * - Still free the proxy, still free the handle - * - Return -- caller's perspective: unregister succeeded - * The daemon will eventually clean up its stale ProcessInfo entry - * through its own periodic cleanup or on next daemon restart. - */ + /* Best-effort D-Bus call: UnregisterProcess(handler_id) */ FWUPMGR_INFO("Calling UnregisterProcess D-Bus method...\n"); result = g_dbus_proxy_call_sync( proxy, @@ -1251,21 +638,6 @@ void unregisterProcess(FirmwareInterfaceHandle handler) ); if (!result) { - /* - * D-Bus call failed. Common causes: - * - Daemon not running (crashed or was stopped) - * - 5-second timeout expired - * - D-Bus bus itself restarted - * - * This is logged as WARN, not ERROR, because: - * 1. unregisterProcess() is void -- no error code to return - * 2. The important cleanup (thread, mutexes) already succeeded - * 3. The daemon's stale entry is the daemon's problem, not ours - * - * "This is OK if daemon already cleaned up" -- if the daemon - * crashed, it already lost all its ProcessInfo entries. When it - * restarts, it starts fresh. Our registration is already gone. - */ FWUPMGR_WARN("UnregisterProcess D-Bus call failed: %s\n", error ? error->message : "unknown error (GError not set)"); FWUPMGR_WARN(" (This is OK if daemon already cleaned up)\n"); @@ -1273,35 +645,11 @@ void unregisterProcess(FirmwareInterfaceHandle handler) g_error_free(error); } g_object_unref(proxy); - /* Continue with local cleanup */ free(handler); return; } - /* - * [STEP 6] EXTRACT THE SUCCESS FLAG FROM DAEMON'S REPLY - * - * The daemon replied with GVariant type "(b)": - * '(' = tuple start - * 'b' = gboolean (TRUE or FALSE) - * ')' = tuple end - * - * g_variant_get() deserializes into our 'success' variable. - * - * Then we free the GVariant reply and the proxy. The ephemeral - * D-Bus connection :1.145 is now closed. - * - * Possible values: - * success == TRUE: Daemon found handler_id=1, removed ProcessInfo, - * freed process_name/lib_version/sender_id strings. - * registered_processes is now empty (0 entries). - * - * success == FALSE: Daemon didn't find handler_id=1. This can happen if: - * - We already unregistered (double call) - * - Daemon restarted and lost its in-memory state - * - handler_id was somehow wrong - * We log a warning but don't treat it as fatal. - */ + /* Extract success flag from daemon reply "(b)" */ g_variant_get(result, "(b)", &success); g_variant_unref(result); g_object_unref(proxy); @@ -1313,51 +661,7 @@ void unregisterProcess(FirmwareInterfaceHandle handler) FWUPMGR_WARN(" (Handler may have already been unregistered)\n"); } - /* - * [STEP 7] FREE THE HANDLE STRING - * - * This frees the 32-byte malloc'd string that registerProcess() - * created in its Step 7 (e.g., the string "1"). - * - * After this line, the 'handler' pointer is INVALID. Dereferencing - * it is undefined behavior (use-after-free). The caller must NOT - * use the handle after unregisterProcess() returns. - * - * Good practice in the caller: - * unregisterProcess(g_handle); - * g_handle = NULL; // prevent accidental use-after-free - * - * This free() happens on EVERY code path: - * - Normal success path (here) - * - D-Bus call failure (step 5 failure branch) - * - Proxy creation failure (step 4 failure branch) - * - Handle parse failure (step 2 failure branches) - * The ONLY path that doesn't free is the NULL check (step 1), - * because there's nothing to free. - * - * STATE OF THE WORLD AFTER THIS RETURNS: - * - * example_app process: - * Threads: 1 (main thread only) - * BG thread: DEAD (joined in step 3) - * Mutexes: 0 (all 4 destroyed in step 3) - * Registries: wiped (handle_keys freed) - * D-Bus: 0 connections (all closed) - * Handle: FREED and INVALID - * - * Daemon process: - * registered_processes: empty (ProcessInfo for "example_plugin" removed) - * next_process_id: 2 (monotonically increasing, never resets) - * - * D-Bus connections (all dead): - * :1.140 -- was registerProcess ephemeral (dead since registration) - * :1.141 -- was BG thread persistent (closed in step 3) - * :1.145 -- was this unregister ephemeral (just closed above) - * - * The library is back to "UNLINKED" state. If needed, the caller - * could call registerProcess() again to start a new session. - * The daemon would assign handler_id=2 this time. - */ + /* Free the handle string (caller must not use handle after this) */ free(handler); FWUPMGR_INFO("Handle memory freed\n"); } From 14399d693d971bfda948e61573452373354f10a2 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Sat, 2 May 2026 05:59:30 +0000 Subject: [PATCH 35/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- modified code comments --- librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md b/librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md index 6fc140a4..3b4a8c0a 100755 --- a/librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md +++ b/librdkFwupdateMgr/docs/DESIGN_DOCUMENT.md @@ -1,12 +1,8 @@ # librdkFwupdateMgr — Engineering Design Document > **Document Version**: 2.0 -> **Date**: April 28, 2026 -> **Classification**: Internal Engineering — Pull Request Review -> **Repository**: `orig_rdkfwupdater` +> **Date**: May 02, 2026 > **Component**: `librdkFwupdateMgr` (shared library) -> **Author**: Senior Architect — rdkFwupdateMgr Team -> **Reviewers**: Firmware Platform Team, Security Review Board --- From 35c7de258f008e89c50ed921ca725843b5d57978 Mon Sep 17 00:00:00 2001 From: mkadinti Date: Sat, 2 May 2026 06:05:51 +0000 Subject: [PATCH 36/36] RDKEMW-17052:Refactor librdkFwupdateMgr from on-demand threading to a single-threaded design- bring defects'fixes- modified code comments --- .../docs/librdkFwupdateMgr_CodeReview.md | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100755 librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md diff --git a/librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md b/librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md deleted file mode 100755 index f3a8bc12..00000000 --- a/librdkFwupdateMgr/docs/librdkFwupdateMgr_CodeReview.md +++ /dev/null @@ -1,125 +0,0 @@ -# librdkFwupdateMgr.so Critical Design Review - -**Date**: April 29, 2026 -**Scope**: `orig_rdkfwupdater/librdkFwupdateMgr/src/` — async engine, process management, and public API -**Reviewer**: Senior Engineering (automated critical review) - ---- - -## Executive Summary - -* **Overall risk level**: Moderate. The library is well-structured with correct use of two-phase dispatch, proper mutex discipline, and thorough input validation. However, it has 3 genuine issues that warrant attention. -* **Production readiness status**: Conditionally ready. No crash-class bugs found in the normal operational path (registerProcess → checkForUpdate → downloadFirmware → updateFirmware → unregisterProcess). The issues found affect edge cases and shutdown paths. -* **Top 3 immediate concerns**: - 1. Download and update D-Bus signal subscriptions are **never unsubscribed** during BG thread shutdown — resource leak - 2. `internal_system_init()` has incomplete rollback on late-stage failure (dwnl/update mutex init) — leaves BG thread and g_registry leaked - 3. `strdup()` return values in all 3 `internal_*_register_callback()` functions are never NULL-checked — Coverity-class defect -* **Immediate remediation required**: No. None of these cause crashes in production's happy path. But Finding #1 and #3 should be fixed before the next release. - ---- - -## Findings - -### Finding 1: DownloadProgress and UpdateProgress signal subscriptions never unsubscribed - -**Severity**: Major -**Category**: Lifecycle / Memory -**Priority**: Next Release - -**Why this is dangerous in production:** -When the BG thread shuts down (after `g_main_loop_quit()`), it only unsubscribes `g_bg_thread.subscription_id` (the CheckForUpdateComplete subscription). The `dwnl_sub_id` and `update_sub_id` returned by `g_dbus_connection_signal_subscribe()` on lines 279 and 293 of `rdkFwupdateMgr_async.c` are local variables that go out of scope. They are never stored in `g_bg_thread` and never passed to `g_dbus_connection_signal_unsubscribe()`. - -**Realistic failure scenario:** -When `unregisterProcess()` is called, the BG thread exits `g_main_loop_run()`. On lines 316-319, only `g_bg_thread.subscription_id` is unsubscribed. Then `g_object_unref(connection)` is called. GLib closes the D-Bus connection, which *implicitly* removes all subscriptions for that connection. So functionally, the signals do get cleaned up — but only because `g_object_unref` does it as a side effect, not because the code explicitly manages the lifecycle. If the connection were ever shared or reused, the subscriptions would leak. - -**Root cause:** -`dwnl_sub_id` and `update_sub_id` are stored as local variables in `background_thread_func()` and never saved to the `BackgroundThread` struct. - -**Recommended fix:** -Add `guint dwnl_subscription_id` and `guint update_subscription_id` fields to the `BackgroundThread` struct in `rdkFwupdateMgr_async_internal.h`. Store the IDs and unsubscribe them alongside `subscription_id` in the thread cleanup block. - -**Mitigating factor:** The `g_object_unref(connection)` on line 320 closes the connection, which implicitly cleans up all subscriptions on it. So in practice this is a code correctness issue, not a resource leak. But it would fail a Coverity audit for asymmetric subscribe/unsubscribe. - ---- - -### Finding 2: `internal_system_init()` incomplete rollback on dwnl/update mutex init failure - -**Severity**: Major -**Category**: Lifecycle / Memory -**Priority**: Next Release - -**Why this is dangerous in production:** -On line 156 of `rdkFwupdateMgr_async.c`, if `pthread_mutex_init(&g_dwnl_registry.mutex)` fails, the function returns `-1` without cleaning up the already-created BG thread, `g_registry.mutex`, `g_bg_thread.mutex`, `g_bg_thread.main_loop`, `g_bg_thread.context`, or the running BG thread. Similarly on line 163, the update mutex failure only destroys `g_dwnl_registry.mutex` but not the BG thread or `g_registry`. - -**Realistic failure scenario:** -If either late-stage `pthread_mutex_init` fails (extremely rare — only under extreme kernel resource exhaustion), the caller in `registerProcess()` receives `-1` from `internal_system_init()`. The rollback in `registerProcess()` (lines 745-780 of `rdkFwupdateMgr_process.c`) does a best-effort daemon unregister and frees the handle, but the BG thread is orphaned — it's stuck in `g_main_loop_run()` with no way to quit it, and the `g_registry.mutex` is leaked. - -**Root cause:** -The early-return on `dwnl_registry.mutex` init failure (line 157) was added after the BG thread launch code and doesn't perform the same cleanup cascade that lines 131-136 do. - -**Recommended fix:** -On failure after BG thread creation, call the existing cleanup sequence: `g_main_loop_quit()` → `pthread_join()` → free GLib objects → destroy `g_bg_thread.mutex` → destroy `g_registry.mutex` → return `-1`. Or factor the cleanup into a helper function called from all failure paths. - -**Mitigating factor:** `pthread_mutex_init()` on Linux NPTL virtually never fails (it only fails for invalid attributes or extreme kernel memory exhaustion). This is a code correctness issue that Coverity would flag, not a practical production crash. - ---- - -### Finding 3: `strdup()` return value never checked in register_callback functions - -**Severity**: Major -**Category**: Static Analysis / Crash -**Priority**: Next Release - -**Why this is dangerous in production:** -Three locations assign `strdup(handle)` to `target->handle_key` without checking for NULL: -- Line 1004 in `internal_register_callback()` -- Line 1851 in `internal_dwnl_register_callback()` -- Line 2795 in `internal_update_register_callback()` - -If `strdup()` returns NULL (OOM), `handle_key` is NULL. Later, `dispatch_all_pending()` calls `strcmp(e->handle_key, ...)` which dereferences NULL → **SIGSEGV**. - -**Realistic failure scenario:** -Under severe memory pressure on embedded devices (common during firmware download when large buffers are allocated), `strdup()` of even a 2-byte string ("1") could fail. The next signal dispatch would crash the process. - -**Root cause:** -The `strdup()` calls predate the detailed documentation effort and were never augmented with NULL checks. - -**Recommended fix:** -After each `strdup(handle)`, check for NULL. If NULL, set state back to IDLE, unlock mutex, return `false`. The caller (API function) already handles `false` by cleaning up the D-Bus connection and returning failure. - ---- - -## Validation Against Mandatory Expectations - -| Expectation | Status | Notes | -|---|---|---| -| Coverity-grade clean | **Fail** | Finding #3: unchecked `strdup()` return (3 sites). Finding #1: asymmetric subscribe/unsubscribe. | -| No memory leak | **Pass** | All heap allocations (`strdup`, `malloc(32)`) have matching `free()` calls on all paths. `parse_update_details` frees its `work_str`. Signal data cleanup is thorough. | -| Thread safe | **Pass** | Two-phase dispatch prevents deadlock. All registry access is mutex-protected. Callback invocation happens outside critical sections. | -| Race condition safe | **Pass** | Register-before-send ordering prevents signal-before-registration race. Dedup prevents double-dispatch. `DISPATCHED` state prevents re-entry. | -| Critical section safe | **Pass** | No nested locking (each registry has its own independent mutex). Two-phase dispatch ensures short critical sections (~microseconds). No lock ordering dependency. | -| Positive/negative scenarios handled | **Pass** | NULL/empty handle checks, NULL callback checks, NULL struct checks, registry-full handling, D-Bus connection failure, daemon timeout, parse failure, OOM on `malloc(32)`. | -| Buffer safe | **Pass** | `strncpy` with `sizeof()-1` in `parse_update_details`. `snprintf` for handle_copy in snapshots (256-byte fixed buffers, handles are ~2 bytes). `FwInfoData.CurrFWVersion` copy is bounded. | - ---- - -## Areas Reviewed with No Major Concerns - -- **Two-phase dispatch pattern** (all 3 registries): Correctly prevents deadlock. Snapshot-under-mutex + invoke-without-mutex is textbook correct. -- **Callback lifecycle**: PENDING→DISPATCHED→IDLE (check), ACTIVE→IDLE (download/update) correctly tracks state transitions. Terminal status detection is sound. -- **`parse_update_details()`**: Uses `strtok_r` (thread-safe), works on a `strdup`'d copy (original preserved), bounded `strncpy`, handles malformed tokens gracefully. -- **`unregisterProcess()` validation**: `strtoull` with full `errno`/`endptr`/`*endptr`/zero checks. Correct `free(handler)` on all paths. -- **`registerProcess()` failure recovery**: OOM on `malloc(32)` and `internal_system_init()` failure both do best-effort daemon unregister before returning NULL. -- **Memory management across the library**: Every `strdup` has a matching `free` in the reset/deinit path. `GVariant` references are properly `g_variant_unref`'d. `GDBusProxy`/`GDBusConnection` properly `g_object_unref`'d. Signal data strings (`g_free`/`free`) are freed after dispatch completes. -- **D-Bus connection model**: Ephemeral per-call connections for API calls, persistent connection for BG thread signal reception. Clean separation, no cross-thread GLib context issues. -- **Shutdown ordering**: `g_main_loop_quit` → `pthread_join` → resource teardown. The join ensures the BG thread is dead before mutexes are destroyed. - ---- - -## Final Verdict - -The library is **production-ready for deployment** on the normal operational path. The two-phase dispatch design, mutex discipline, and input validation are solid engineering. The three findings are all edge-case defects that would be caught by static analysis tools (Coverity, Coverity SA, cppcheck) but do not affect the standard `register → check → download → update → unregister` flow. - -**Finding #3 (unchecked `strdup`)** is the most important to fix because it has a real (if unlikely) crash path under memory pressure. Findings #1 and #2 are code correctness issues that should be addressed for audit cleanliness but have no practical production impact due to mitigating factors (GLib implicit cleanup, near-impossibility of `pthread_mutex_init` failure). - -**No immediate ship-blocker. Fix Finding #3 before next release. Address #1 and #2 in planned hardening.**