diff --git a/Makefile.am b/Makefile.am index 58363925..6fea9a09 100644 --- a/Makefile.am +++ b/Makefile.am @@ -181,14 +181,13 @@ 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 -librdkFwupdateMgr_la_CFLAGS = -fPIC -I${top_srcdir}/librdkFwupdateMgr/include -I${top_srcdir}/librdkFwupdateMgr/src $(AM_CFLAGS) $(GLIB_CFLAGS) -librdkFwupdateMgr_la_CPPFLAGS = -fPIC -I${top_srcdir}/librdkFwupdateMgr/include -I${top_srcdir}/librdkFwupdateMgr/src $(GLIB_CFLAGS) +librdkFwupdateMgr_la_CFLAGS = -fPIC -I${top_srcdir}/librdkFwupdateMgr/include -I${top_srcdir}/librdkFwupdateMgr/src -I${top_srcdir}/common_utilities/utils $(AM_CFLAGS) $(GLIB_CFLAGS) +librdkFwupdateMgr_la_CPPFLAGS = -fPIC -I${top_srcdir}/librdkFwupdateMgr/include -I${top_srcdir}/librdkFwupdateMgr/src -I${top_srcdir}/common_utilities/utils $(GLIB_CFLAGS) librdkFwupdateMgr_la_LDFLAGS = -shared -version-info 1:0:0 -librdkFwupdateMgr_la_LIBADD = $(GLIB_LIBS) -lpthread +librdkFwupdateMgr_la_LIBADD = $(GLIB_LIBS) -lpthread -lfwutils librdkFwupdateMgr_include_HEADERS = \ ${top_srcdir}/librdkFwupdateMgr/include/rdkFwupdateMgr_client.h @@ -258,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) diff --git a/librdkFwupdateMgr/examples/example_app.c b/librdkFwupdateMgr/examples/example_app.c index f2207880..6d37ba85 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 @@ -51,7 +63,7 @@ * ======================================================================== * Since callbacks don't support user_data, we use global variables to: * - Store firmware info from checkForUpdate callback - * - Track workflow progress (check → download → flash) + * - Track workflow progress (check -> download -> flash) * - Synchronize main thread with callback threads * ======================================================================== */ @@ -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_api.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c index 261af3ce..babc3ebd 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_api.c @@ -14,31 +14,47 @@ * @file rdkFwupdateMgr_api.c * @brief Public API implementations: checkForUpdate, downloadFirmware, updateFirmware * - * ALL THREE APIS USE THE SAME ASYNC PATTERN: - * =========================================== - * All APIs are NON-BLOCKING fire-and-forget calls that return immediately. - * Results are delivered asynchronously via D-Bus signals to registered callbacks. - * - * CHECKFORUPDATE: - * --------------- + * CHECKFORUPDATE (Phase 1 - on-demand worker thread): + * ==================================================== * 1. Validate handle and callback - * 2. Register callback in registry (BEFORE D-Bus call to avoid race) - * 3. Fire CheckForUpdate D-Bus method call (fire-and-forget) - * 4. Return CHECK_FOR_UPDATE_SUCCESS immediately - * - * [Later - typically 5-30 seconds] - * Daemon queries XConf server and emits CheckForUpdateComplete signal - * → on_check_complete_signal() fires in background thread - * → dispatch_all_pending() calls registered UpdateEventCallback - * → Callback receives FwInfoData with version info and update details + * 2. Reject if another checkForUpdate is already in progress + * 3. Allocate CheckRequestContext on heap + * 4. Spawn worker thread (internal_check_worker_thread) + * 5. Wait for worker to signal "ready" via condvar (~10-100ms) + * 6. Return SUCCESS or FAIL immediately + * + * [Later - typically 5-30 seconds, max 120 seconds] + * Worker thread receives CheckForUpdateComplete signal from daemon + * → Parses payload → Fires client callback with FwInfoData + * → Cleans up all resources → Thread exits + * + * DOWNLOAD FIRMWARE (Phase 2 - on-demand worker thread): + * ======================================================= + * 1. Validate handle, request, and callback + * 2. Reject if another downloadFirmware is already in progress + * 3. Allocate DownloadRequestContext on heap + * 4. Spawn worker thread (internal_download_worker_thread) + * 5. Wait for worker to signal "ready" via condvar (~50-200ms, includes daemon reply) + * 6. Return SUCCESS or FAIL (accurate — reflects daemon's accept/reject) + * + * [Later - typically 1-30 minutes, max 3600 seconds] + * Worker thread receives DownloadProgress signals from daemon + * → Fires client callback MULTIPLE TIMES (per progress signal) + * → Quits loop on COMPLETED/ERROR → Cleans up → Thread exits + * + * UPDATE FIRMWARE (Phase 3 - on-demand worker thread): + * ===================================================== + * 1. Validate handle, request, and callback + * 2. Reject if another updateFirmware is already in progress + * 3. Allocate UpdateRequestContext on heap + * 4. Spawn worker thread (internal_update_worker_thread) + * 5. Wait for worker to signal "ready" via condvar (~50-200ms, includes daemon reply) + * 6. Return SUCCESS or FAIL (accurate — reflects daemon's accept/reject) * - * DOWNLOAD / UPDATE FIRMWARE: - * ============================ - * Same pattern but with progress signals: - * - DownloadFirmware → DownloadProgress signals (multiple, 0%-100%) - * - UpdateFirmware → UpdateProgress signals (multiple, 0%-100%) - * - * Callbacks fire repeatedly until COMPLETED or ERROR status. + * [Later - typically 5-60 minutes, max 3600 seconds] + * Worker thread receives UpdateProgress signals from daemon + * → Fires client callback MULTIPLE TIMES (per progress signal) + * → Quits loop on COMPLETED/ERROR → Cleans up → Thread exits */ #include "rdkFwupdateMgr_client.h" @@ -48,25 +64,42 @@ #include #include #include +#include +#include + +/** + * Maximum time (seconds) to wait for the worker thread to signal readiness. + * + * The worker normally signals in <200ms (D-Bus connect + subscribe + optional + * sync method call). 10 seconds is extremely generous. If the worker hasn't + * signaled by then, it's dead or wedged — treat it as init failure. + */ +#define WORKER_READY_TIMEOUT_SEC 10 + +/* No extern globals needed — all state is accessed through + * internal_begin_*() / internal_end_*() / internal_abort_*() + * / internal_is_*_in_progress() declared in rdkFwupdateMgr_async_internal.h. + * The mutex and state variables are static inside rdkFwupdateMgr_async.c. + */ /* ======================================================================== - * checkForUpdate — SYNCHRONOUS implementation + * checkForUpdate - ON-DEMAND WORKER THREAD implementation (Phase 1) * ======================================================================== */ /** - * @brief Check for firmware update — non-blocking, returns immediately - * - * 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. + * @brief Check for firmware update - spawns on-demand worker thread * - * 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) + * Allocates a CheckRequestContext, spawns a worker thread that connects + * to D-Bus, subscribes to CheckForUpdateComplete signal, sends the + * CheckForUpdate method call, and waits for the response. The caller + * blocks briefly (typically <100ms) until the worker signals "ready", + * then returns immediately. The callback fires asynchronously in the + * worker thread when the daemon responds (5s to 2min+). * - * 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. + * INVARIANTS: + * - At most one checkForUpdate() in progress per process + * - Callback fires exactly once (on signal) or zero times (on timeout/error) + * - Worker thread is self-contained: creates and destroys all its resources * * @param handle Valid FirmwareInterfaceHandle from registerProcess() * @param callback Invoked when CheckForUpdateComplete signal arrives @@ -75,11 +108,13 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, UpdateEventCallback callback) { - /* [1] Validate */ + /* [1] Validate handle - must be non-NULL and non-empty (daemon would reject it anyway) */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("checkForUpdate: invalid handle (NULL or empty)\n"); return CHECK_FOR_UPDATE_FAIL; } + + /* [2] Validate callback - NULL callback means we'd have no way to deliver results */ if (callback == NULL) { FWUPMGR_ERROR("checkForUpdate: callback is NULL\n"); return CHECK_FOR_UPDATE_FAIL; @@ -87,66 +122,170 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, FWUPMGR_INFO("checkForUpdate: handle='%s'\n", handle); - /* [2] Connect to D-Bus FIRST before registering callback + /* [3] Reject duplicate: only one checkForUpdate at a time per process. * - * This prevents stale registry entries if D-Bus connection fails. - * We only register the callback if we can successfully send the request. + * If a worker thread is already running, a second checkForUpdate() + * would create two threads both listening for the same D-Bus signal. + * wasteful and confusing (the app would get duplicate callbacks with + * identical data). So we reject it immediately. */ - GError *error = NULL; - GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); - if (conn == NULL) { - FWUPMGR_ERROR("checkForUpdate: D-Bus connect failed: %s\n", - error ? error->message : "unknown"); - if (error) g_error_free(error); + /* [4] Allocate per-request context on heap + * + * TL;DR: We allocate FIRST, then call internal_begin_check() to atomically + * set in-progress + track ctx. This way there's no window where in-progress + * is true but ctx isn't ready. If allocation fails, we just free and return + * without touching any global state. + */ + CheckRequestContext *ctx = calloc(1, sizeof(CheckRequestContext)); + if (ctx == NULL) { + FWUPMGR_ERROR("checkForUpdate: calloc failed for ctx\n"); + return CHECK_FOR_UPDATE_FAIL; + } + + ctx->handle_key = strdup(handle); + if (ctx->handle_key == NULL) { + FWUPMGR_ERROR("checkForUpdate: strdup failed for handle\n"); + free(ctx); return CHECK_FOR_UPDATE_FAIL; } - /* [3] Register callback AFTER D-Bus connection succeeds + ctx->callback = callback; + ctx->is_ready = false; + ctx->init_failed = false; + + if (pthread_mutex_init(&ctx->ready_mutex, NULL) != 0) { + FWUPMGR_ERROR("checkForUpdate: ready_mutex init failed\n"); + free(ctx->handle_key); + free(ctx); + return CHECK_FOR_UPDATE_FAIL; + } + + if (pthread_cond_init(&ctx->ready_cond, NULL) != 0) { + FWUPMGR_ERROR("checkForUpdate: ready_cond init failed\n"); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx); + return CHECK_FOR_UPDATE_FAIL; + } + + /* [5] Atomically begin the check session: set in-progress + track ctx. + * + * TL;DR: internal_begin_check() does the duplicate rejection AND the + * context tracking in one mutex-protected operation. If another check + * is already active, it returns false and we clean up locally. The + * globals are never in an inconsistent state. + */ + if (!internal_begin_check(ctx)) { + FWUPMGR_WARN("checkForUpdate: already in progress, rejecting. " + "handle='%s'\n", handle); + pthread_cond_destroy(&ctx->ready_cond); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx); + return CHECK_FOR_UPDATE_FAIL; + } + + /* [7] Spawn worker thread — ownership of ctx transfers to worker * - * Register immediately before sending to avoid race condition where - * the daemon responds before we're ready to receive the signal. + * The worker thread will set up D-Bus, subscribe to signals, + * send the CheckForUpdate request, and wait for the daemon's response. + * If pthread_create fails, we undo the begin_check and return FAIL. */ - if (!internal_register_callback(handle, callback)) { - FWUPMGR_ERROR("checkForUpdate: registry full, handle='%s'\n", handle); - g_object_unref(conn); + if (pthread_create(&ctx->thread, NULL, internal_check_worker_thread, ctx) != 0) { + FWUPMGR_ERROR("checkForUpdate: pthread_create failed\n"); + internal_abort_check(); + pthread_cond_destroy(&ctx->ready_cond); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx); return CHECK_FOR_UPDATE_FAIL; } - /* [4] Fire-and-forget D-Bus CheckForUpdate method call + /* [8] Save thread handle locally BEFORE condvar wait. * - * Arguments: (s) - * s handle — identifies this app to the daemon + * CRITICAL: On the init-failure path, the worker signals is_ready=true + * and then immediately proceeds to cleanup (which destroys ready_mutex, + * ready_cond, and free(ctx)). If we read ctx->thread AFTER the condvar + * wake, ctx may already be freed → use-after-free. * - * 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. + * By copying pthread_t here (right after pthread_create, before any + * race can occur), our join on the failure path uses the local copy + * and never touches ctx again. */ - FWUPMGR_INFO("checkForUpdate: calling CheckForUpdate on daemon, handle='%s'\n", + pthread_t worker_thread = ctx->thread; + + /* [8b] Wait for worker to signal ready (bounded timeout) + * + * This blocks the caller for ~10-100ms while the worker sets up + * its D-Bus connection and signal subscription. The worker signals + * is_ready=true when it's either ready or has failed to init. + * + * We use pthread_cond_timedwait to prevent infinite hang if the + * worker crashes or exits without signaling. + */ + struct timespec deadline; + if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + FWUPMGR_ERROR("checkForUpdate: clock_gettime failed (errno=%d), " + "using fallback deadline\n", errno); + deadline.tv_sec = time(NULL); + deadline.tv_nsec = 0; + } + deadline.tv_sec += WORKER_READY_TIMEOUT_SEC; + + pthread_mutex_lock(&ctx->ready_mutex); + int wait_rc = 0; + while (!ctx->is_ready && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&ctx->ready_cond, &ctx->ready_mutex, + &deadline); + } + bool failed = ctx->init_failed || (wait_rc == ETIMEDOUT); + pthread_mutex_unlock(&ctx->ready_mutex); + + if (wait_rc == ETIMEDOUT) { + FWUPMGR_ERROR("checkForUpdate: worker thread did not signal ready " + "within %ds — treating as init failure. handle='%s'\n", + WORKER_READY_TIMEOUT_SEC, handle); + } + + /* [9] Check if worker failed to initialize + * + * The worker tried to connect to D-Bus and subscribe to signals. + * If that failed (D-Bus dead, system error), init_failed is true. + * We join the worker (it's already exiting) and return FAIL to the app. + * The worker handles its own cleanup — we just wait for it to finish. + * + * IMPORTANT: We use worker_thread (local copy), NOT ctx->thread. + * After the condvar wake, the worker may have already freed ctx. + */ + if (failed) { + FWUPMGR_ERROR("checkForUpdate: worker thread failed to initialize. " + "handle='%s'\n", handle); + pthread_join(worker_thread, NULL); + + /* After join, the worker has exited. If caller_owns_cleanup is set, + * the worker left the mutex/cond/ctx alive for us to clean up safely. + * If not set (e.g., timeout case where worker is still running), + * the worker will eventually clean up itself — but since we joined, + * the worker has already exited so ctx is safe to access. */ + pthread_mutex_destroy(&ctx->ready_mutex); + pthread_cond_destroy(&ctx->ready_cond); + free(ctx->handle_key); + free(ctx); + + return CHECK_FOR_UPDATE_FAIL; + } + + /* [10] Worker is running and listening. Return success to caller. + * + * From this point, the caller NEVER touches ctx again. + * The worker thread is the sole owner and will free it after + * the callback fires or the timeout expires. + */ + FWUPMGR_INFO("checkForUpdate: worker thread started, returning SUCCESS. " + "Callback will fire when daemon responds. handle='%s'\n", handle); - g_dbus_connection_call( - conn, - DBUS_SERVICE_NAME, - DBUS_OBJECT_PATH, - DBUS_INTERFACE_NAME, - DBUS_METHOD_CHECK, /* method: CheckForUpdate */ - g_variant_new("(s)", handle), /* app's handler_id string */ - NULL, /* expected reply type: none */ - G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, - NULL, /* GCancellable: none */ - NULL, /* reply callback: none */ - NULL /* user_data: none */ - ); - - 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; } @@ -157,60 +296,85 @@ CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, /** * @brief Library constructor — auto-called when .so is loaded * - * Initializes the internal async engine (registry + background thread) - * before any app code runs. + * Phase 3: No async infrastructure init needed. All three APIs use on-demand + * worker threads that create and destroy their own resources. The library is + * ready to use immediately after loading. */ __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"); - } + /* No internal_system_init() needed — all APIs use on-demand worker threads. + * Zero resource cost when idle: no background thread, no registries, + * no D-Bus connections until an API is actually called. + */ FWUPMGR_INFO("=== rdkFwupdateMgr library ready ===\n"); } /** * @brief Library destructor — auto-called when .so is unloaded * - * Stops background thread and frees all resources cleanly. + * Stops any active CheckForUpdate, DownloadFirmware, and UpdateFirmware + * worker threads. No persistent background thread to stop (removed in Phase 3). */ __attribute__((destructor)) static void rdkFwupdateMgr_lib_deinit(void) { FWUPMGR_INFO("=== rdkFwupdateMgr library unloading ===\n"); - internal_system_deinit(); + + /* Phase 1: Cancel and join any active CheckForUpdate worker thread. */ + internal_cancel_all_active_check_threads(); + + /* Phase 2: Cancel and join any active DownloadFirmware worker thread. */ + internal_cancel_all_active_download_threads(); + + /* Phase 3: Cancel and join any active UpdateFirmware worker thread. */ + internal_cancel_all_active_update_threads(); + + /* No internal_system_deinit() needed — no persistent BG thread or registry. */ + FWUPMGR_INFO("=== rdkFwupdateMgr library unloaded ===\n"); } /* ======================================================================== - * DOWNLOAD FIRMWARE PUBLIC API + * DOWNLOAD FIRMWARE PUBLIC API — ON-DEMAND WORKER THREAD (Phase 2) * ======================================================================== * * Implements: * DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, - * FwDwnlReq fwdwnlreq, + * const FwDwnlReq *fwdwnlreq, * DownloadCallback callback); * * 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 + * 1. Validate: handle, request fields, callback + * 2. Allocate DownloadRequestContext on heap + * 3. internal_begin_download(ctx) — reject if already active + * 4. Spawn worker thread (internal_download_worker_thread) + * 5. Wait for condvar — worker sets up D-Bus + calls daemon synchronously + * 6. Check daemon reply: accepted → SUCCESS, rejected → FAIL * - * [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 + * [later — fires multiple times over 1-30 minutes] + * Worker receives DownloadProgress signals → fires callback each time + * → quits loop on COMPLETED/ERROR → cleanup → thread exits * ======================================================================== */ /** - * @brief Initiate firmware download — non-blocking, returns immediately + * @brief Initiate firmware download — spawns on-demand worker thread + * + * Allocates a DownloadRequestContext, spawns a worker thread that connects + * to D-Bus, subscribes to DownloadProgress signal, sends DownloadFirmware + * method call SYNCHRONOUSLY, and reads the daemon's reply. The caller + * blocks briefly (~50-200ms) until the worker signals "ready", then + * returns immediately with an ACCURATE result reflecting the daemon's + * accept/reject decision. + * + * INVARIANTS: + * - At most one downloadFirmware() in progress per process + * - Callback fires N times (per progress signal) or 0 times (on error) + * - Worker thread is self-contained: creates and destroys all its resources * * @param handle Valid FirmwareInterfaceHandle from registerProcess() - * @param fwdwnlreq Download request (passed by value, library copies it) + * @param fwdwnlreq Download request details (firmware name, URL, type) * @param callback Invoked on each DownloadProgress signal * @return RDKFW_DWNL_SUCCESS or RDKFW_DWNL_FAILED */ @@ -218,12 +382,13 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, const FwDwnlReq *fwdwnlreq, DownloadCallback callback) { - /* [1] Validate */ + /* [1] Validate handle */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("downloadFirmware: invalid handle (NULL or empty)\n"); return RDKFW_DWNL_FAILED; } + /* [2] Validate request */ if (fwdwnlreq == NULL) { FWUPMGR_ERROR("downloadFirmware: fwdwnlreq is NULL\n"); return RDKFW_DWNL_FAILED; @@ -239,108 +404,250 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, return RDKFW_DWNL_FAILED; } + /* [3] Validate callback */ if (callback == NULL) { FWUPMGR_ERROR("downloadFirmware: callback is NULL\n"); return RDKFW_DWNL_FAILED; } FWUPMGR_INFO("downloadFirmware: handle='%s' firmware='%s' type='%s' url='%s'\n", - handle, + 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 + /* [4] Allocate per-request context on heap * - * This prevents stale registry entries if D-Bus connection fails. + * We allocate FIRST, then call internal_begin_download() to atomically + * set in-progress + track ctx. This way there's no window where in-progress + * is true but ctx isn't ready. */ - GError *error = NULL; - GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + DownloadRequestContext *ctx = calloc(1, sizeof(DownloadRequestContext)); + if (ctx == NULL) { + FWUPMGR_ERROR("downloadFirmware: calloc failed for ctx\n"); + return RDKFW_DWNL_FAILED; + } + + ctx->handle_key = strdup(handle); + if (ctx->handle_key == NULL) { + FWUPMGR_ERROR("downloadFirmware: strdup failed for handle\n"); + free(ctx); + return RDKFW_DWNL_FAILED; + } + + ctx->firmware_name = strdup(fwdwnlreq->firmwareName); + if (ctx->firmware_name == NULL) { + FWUPMGR_ERROR("downloadFirmware: strdup failed for firmwareName\n"); + free(ctx->handle_key); + free(ctx); + return RDKFW_DWNL_FAILED; + } + + ctx->firmware_url = NULL; + if (fwdwnlreq->downloadUrl != NULL) { + ctx->firmware_url = strdup(fwdwnlreq->downloadUrl); + if (ctx->firmware_url == NULL) { + FWUPMGR_ERROR("downloadFirmware: strdup failed for downloadUrl\n"); + free(ctx->firmware_name); + free(ctx->handle_key); + free(ctx); + return RDKFW_DWNL_FAILED; + } + } + + ctx->firmware_type = NULL; + if (fwdwnlreq->TypeOfFirmware != NULL) { + ctx->firmware_type = strdup(fwdwnlreq->TypeOfFirmware); + if (ctx->firmware_type == NULL) { + FWUPMGR_ERROR("downloadFirmware: strdup failed for TypeOfFirmware\n"); + free(ctx->firmware_url); + free(ctx->firmware_name); + free(ctx->handle_key); + free(ctx); + return RDKFW_DWNL_FAILED; + } + } - if (conn == NULL) { - FWUPMGR_ERROR("downloadFirmware: D-Bus connect failed: %s\n", - error ? error->message : "unknown"); - if (error) g_error_free(error); + ctx->callback = callback; + ctx->is_ready = false; + ctx->init_failed = false; + ctx->daemon_accepted = false; + ctx->daemon_reject_message = NULL; + + if (pthread_mutex_init(&ctx->ready_mutex, NULL) != 0) { + FWUPMGR_ERROR("downloadFirmware: ready_mutex init failed\n"); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_url); + free(ctx->firmware_type); + free(ctx); return RDKFW_DWNL_FAILED; } - /* [3] Register callback AFTER D-Bus connection succeeds, BEFORE sending + if (pthread_cond_init(&ctx->ready_cond, NULL) != 0) { + FWUPMGR_ERROR("downloadFirmware: ready_cond init failed\n"); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_url); + free(ctx->firmware_type); + free(ctx); + return RDKFW_DWNL_FAILED; + } + + /* [5] Atomically begin the download session: set in-progress + track ctx. * - * Register immediately before sending to avoid race condition where - * the daemon responds before we're ready to receive the signal. + * internal_begin_download() does the duplicate rejection AND the + * context tracking in one mutex-protected operation. If another download + * is already active, it returns false and we clean up locally. */ - if (!internal_dwnl_register_callback(handle, callback)) { - FWUPMGR_ERROR("downloadFirmware: registry full, handle='%s'\n", handle); - g_object_unref(conn); + if (!internal_begin_download(ctx)) { + FWUPMGR_WARN("downloadFirmware: already in progress, rejecting. " + "handle='%s'\n", handle); + pthread_cond_destroy(&ctx->ready_cond); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_url); + free(ctx->firmware_type); + free(ctx); return RDKFW_DWNL_FAILED; } - /* [4] Fire-and-forget D-Bus DownloadFirmware method call + /* [6] Spawn worker thread — ownership of ctx transfers to worker + * + * The worker thread will set up D-Bus, subscribe to signals, + * call daemon synchronously, and wait for progress signals. + * Thread is joinable (NOT detached) so destructor can join it. + */ + if (pthread_create(&ctx->thread, NULL, internal_download_worker_thread, ctx) != 0) { + FWUPMGR_ERROR("downloadFirmware: pthread_create failed\n"); + internal_abort_download(); + pthread_cond_destroy(&ctx->ready_cond); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_url); + free(ctx->firmware_type); + free(ctx); + return RDKFW_DWNL_FAILED; + } + + /* [7] Save thread handle locally BEFORE condvar wait. + * + * CRITICAL: Same UAF prevention as checkForUpdate — on init failure, + * the worker frees ctx after signaling ready. We must not read + * ctx->thread after the condvar wake. Save it now. + */ + pthread_t worker_thread = ctx->thread; + + /* [7b] Wait for worker to signal ready (bounded timeout) * - * 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" + * This blocks the caller for ~50-200ms while the worker sets up + * its D-Bus connection, subscribes to signals, and calls the daemon + * synchronously. The worker signals is_ready=true when it's either + * ready (daemon accepted) or has failed (D-Bus error or daemon rejected). * - * Three trailing NULLs = fire and forget (no reply waited for). - * g_dbus_connection_call() returns immediately. + * We use pthread_cond_timedwait to prevent infinite hang if the + * worker crashes or exits without signaling. */ + struct timespec deadline; + if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + FWUPMGR_ERROR("downloadFirmware: clock_gettime failed (errno=%d), " + "using fallback deadline\n", errno); + deadline.tv_sec = time(NULL); + deadline.tv_nsec = 0; + } + deadline.tv_sec += WORKER_READY_TIMEOUT_SEC; + + pthread_mutex_lock(&ctx->ready_mutex); + int wait_rc = 0; + while (!ctx->is_ready && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&ctx->ready_cond, &ctx->ready_mutex, + &deadline); + } + bool failed = ctx->init_failed || (wait_rc == ETIMEDOUT); + pthread_mutex_unlock(&ctx->ready_mutex); - g_dbus_connection_call( - conn, - DBUS_SERVICE_NAME, - DBUS_OBJECT_PATH, - DBUS_INTERFACE_NAME, - DBUS_METHOD_DOWNLOAD, /* method: DownloadFirmware */ - g_variant_new("(ssss)", - handle, /* app's handler_id string */ - fwdwnlreq->firmwareName, /* firmware image name */ - fwdwnlreq->downloadUrl ? fwdwnlreq->downloadUrl : "", /* override URL or "" */ - fwdwnlreq->TypeOfFirmware ? fwdwnlreq->TypeOfFirmware : ""), /* PCI / PDRI / PERIPHERAL */ - NULL, /* expected reply type: none */ - G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, - NULL, /* GCancellable: none */ - NULL, /* reply callback: none */ - NULL /* user_data: none */ - ); - - g_object_unref(conn); - - FWUPMGR_INFO("downloadFirmware: D-Bus call sent, returning SUCCESS. handle='%s'\n", + if (wait_rc == ETIMEDOUT) { + FWUPMGR_ERROR("downloadFirmware: worker thread did not signal ready " + "within %ds — treating as init failure. handle='%s'\n", + WORKER_READY_TIMEOUT_SEC, handle); + } + + /* [8] Check if worker failed to initialize or daemon rejected + * + * If init_failed is true, either D-Bus setup failed or the daemon + * rejected the download request. The worker thread is already + * cleaning itself up. We join it to avoid a zombie thread. + * + * IMPORTANT: We use worker_thread (local copy), NOT ctx->thread. + * After the condvar wake, the worker may have already freed ctx. + */ + if (failed) { + FWUPMGR_ERROR("downloadFirmware: worker init failed or daemon rejected. " + "handle='%s'\n", handle); + pthread_join(worker_thread, NULL); + + /* After join, the worker has exited. If caller_owns_cleanup is set, + * the worker left the mutex/cond/ctx alive for us to clean up safely. + * The worker already freed its own resources (daemon_reject_message, etc.) + * but left our strdup'd strings, mutex/cond, and ctx for us. */ + pthread_mutex_destroy(&ctx->ready_mutex); + pthread_cond_destroy(&ctx->ready_cond); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_url); + free(ctx->firmware_type); + free(ctx); + + return RDKFW_DWNL_FAILED; + } + + /* [9] Worker is running and listening for DownloadProgress signals. + * + * From this point, the caller NEVER touches ctx again. + * The worker thread is the sole owner and will free it after + * the download completes, errors, or times out. + */ + FWUPMGR_INFO("downloadFirmware: worker thread started, returning SUCCESS. " + "Callback will fire as download progresses. handle='%s'\n", handle); - /* [4] Return immediately — app is unblocked */ return RDKFW_DWNL_SUCCESS; } /* ======================================================================== - * UPDATE FIRMWARE PUBLIC API + * UPDATE FIRMWARE PUBLIC API — ON-DEMAND WORKER THREAD (Phase 3) * ======================================================================== * * 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 + * 1. Validate: handle, request fields, callback + * 2. Allocate UpdateRequestContext on heap + * 3. internal_begin_update(ctx) — reject if already active + * 4. Spawn worker thread (internal_update_worker_thread) + * 5. Wait for condvar — worker sets up D-Bus + calls daemon synchronously + * 6. Check daemon reply: accepted → SUCCESS, rejected → FAIL * - * [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 + * [later — fires multiple times over 5-60 minutes] + * Worker receives UpdateProgress signals → fires callback each time + * → quits loop on COMPLETED/ERROR → cleanup → thread exits * ======================================================================== */ /** - * @brief Initiate firmware flashing — non-blocking, returns immediately + * @brief Initiate firmware flashing — spawns on-demand worker thread + * + * Allocates an UpdateRequestContext, spawns a worker thread that connects + * to D-Bus, subscribes to UpdateProgress signal, sends UpdateFirmware + * method call SYNCHRONOUSLY, and reads the daemon's reply. The caller + * blocks briefly (~50-200ms) until the worker signals "ready", then + * returns immediately with an ACCURATE result reflecting the daemon's + * accept/reject decision. * * D-Bus arguments sent to daemon: (sssss) * s handle — identifies this app @@ -349,8 +656,13 @@ DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, * s TypeOfFirmware — "PCI" | "PDRI" | "PERIPHERAL" * s rebootImmediately — "true" or "false" (daemon expects string) * + * INVARIANTS: + * - At most one updateFirmware() in progress per process + * - Callback fires N times (per progress signal) or 0 times (on error) + * - Worker thread is self-contained: creates and destroys all its resources + * * @param handle Valid FirmwareInterfaceHandle from registerProcess() - * @param fwupdatereq Update request (passed by value, library copies it) + * @param fwupdatereq Update request (firmware name, type, location, reboot flag) * @param callback Invoked on each UpdateProgress signal * @return RDKFW_UPDATE_SUCCESS or RDKFW_UPDATE_FAILED */ @@ -358,12 +670,13 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, const FwUpdateReq *fwupdatereq, UpdateCallback callback) { - /* [1] Validate */ + /* [1] Validate handle */ if (handle == NULL || handle[0] == '\0') { FWUPMGR_ERROR("updateFirmware: invalid handle (NULL or empty)\n"); return RDKFW_UPDATE_FAILED; } + /* [2] Validate request */ if (fwupdatereq == NULL) { FWUPMGR_ERROR("updateFirmware: fwupdatereq is NULL\n"); return RDKFW_UPDATE_FAILED; @@ -389,6 +702,7 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, return RDKFW_UPDATE_FAILED; } + /* [3] Validate callback */ if (callback == NULL) { FWUPMGR_ERROR("updateFirmware: callback is NULL\n"); return RDKFW_UPDATE_FAILED; @@ -404,68 +718,216 @@ UpdateResult updateFirmware(FirmwareInterfaceHandle handle, : "(use device.properties path)", fwupdatereq->rebootImmediately ? "yes" : "no"); - /* [2] Connect to D-Bus FIRST before registering callback + /* [4] Allocate per-request context on heap * - * This prevents stale registry entries if D-Bus connection fails. + * We allocate FIRST, then call internal_begin_update() to atomically + * set in-progress + track ctx. This way there's no window where in-progress + * is true but ctx isn't ready. */ - GError *error = NULL; - GDBusConnection *conn = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + UpdateRequestContext *ctx = calloc(1, sizeof(UpdateRequestContext)); + if (ctx == NULL) { + FWUPMGR_ERROR("updateFirmware: calloc failed for ctx\n"); + return RDKFW_UPDATE_FAILED; + } + + ctx->handle_key = strdup(handle); + if (ctx->handle_key == NULL) { + FWUPMGR_ERROR("updateFirmware: strdup failed for handle\n"); + free(ctx); + return RDKFW_UPDATE_FAILED; + } + + ctx->firmware_name = strdup(fwupdatereq->firmwareName); + if (ctx->firmware_name == NULL) { + FWUPMGR_ERROR("updateFirmware: strdup failed for firmwareName\n"); + free(ctx->handle_key); + free(ctx); + return RDKFW_UPDATE_FAILED; + } - if (conn == NULL) { - FWUPMGR_ERROR("updateFirmware: D-Bus connect failed: %s\n", - error ? error->message : "unknown"); - if (error) g_error_free(error); + ctx->firmware_location = NULL; + if (fwupdatereq->LocationOfFirmware != NULL) { + ctx->firmware_location = strdup(fwupdatereq->LocationOfFirmware); + if (ctx->firmware_location == NULL) { + FWUPMGR_ERROR("updateFirmware: strdup failed for LocationOfFirmware\n"); + free(ctx->firmware_name); + free(ctx->handle_key); + free(ctx); + return RDKFW_UPDATE_FAILED; + } + } + + ctx->firmware_type = strdup(fwupdatereq->TypeOfFirmware); + if (ctx->firmware_type == NULL) { + FWUPMGR_ERROR("updateFirmware: strdup failed for TypeOfFirmware\n"); + free(ctx->firmware_location); + free(ctx->firmware_name); + free(ctx->handle_key); + free(ctx); + return RDKFW_UPDATE_FAILED; + } + + ctx->reboot_flag = strdup(fwupdatereq->rebootImmediately ? "true" : "false"); + if (ctx->reboot_flag == NULL) { + FWUPMGR_ERROR("updateFirmware: strdup failed for reboot_flag\n"); + free(ctx->firmware_type); + free(ctx->firmware_location); + free(ctx->firmware_name); + free(ctx->handle_key); + free(ctx); + return RDKFW_UPDATE_FAILED; + } + + ctx->callback = callback; + ctx->is_ready = false; + ctx->init_failed = false; + ctx->daemon_accepted = false; + ctx->daemon_reject_message = NULL; + + if (pthread_mutex_init(&ctx->ready_mutex, NULL) != 0) { + FWUPMGR_ERROR("updateFirmware: ready_mutex init failed\n"); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_location); + free(ctx->firmware_type); + free(ctx->reboot_flag); + free(ctx); + return RDKFW_UPDATE_FAILED; + } + + if (pthread_cond_init(&ctx->ready_cond, NULL) != 0) { + FWUPMGR_ERROR("updateFirmware: ready_cond init failed\n"); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_location); + free(ctx->firmware_type); + free(ctx->reboot_flag); + free(ctx); return RDKFW_UPDATE_FAILED; } - /* [3] Register callback AFTER D-Bus connection succeeds, BEFORE sending + /* [5] Atomically begin the update session: set in-progress + track ctx. * - * Register immediately before sending to avoid race condition where - * the daemon responds before we're ready to receive the signal. + * internal_begin_update() does the duplicate rejection AND the + * context tracking in one mutex-protected operation. If another update + * is already active, it returns false and we clean up locally. */ - if (!internal_update_register_callback(handle, callback)) { - FWUPMGR_ERROR("updateFirmware: registry full, handle='%s'\n", handle); - g_object_unref(conn); + if (!internal_begin_update(ctx)) { + FWUPMGR_WARN("updateFirmware: already in progress, rejecting. " + "handle='%s'\n", handle); + pthread_cond_destroy(&ctx->ready_cond); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_location); + free(ctx->firmware_type); + free(ctx->reboot_flag); + free(ctx); return RDKFW_UPDATE_FAILED; } - /* [4] Fire-and-forget D-Bus UpdateFirmware method call + /* [6] Spawn worker thread — ownership of ctx transfers to worker * - * 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) + * The worker thread will set up D-Bus, subscribe to signals, + * call daemon synchronously, and wait for progress signals. + * Thread is joinable (NOT detached) so destructor can join it. + */ + if (pthread_create(&ctx->thread, NULL, internal_update_worker_thread, ctx) != 0) { + FWUPMGR_ERROR("updateFirmware: pthread_create failed\n"); + internal_abort_update(); + pthread_cond_destroy(&ctx->ready_cond); + pthread_mutex_destroy(&ctx->ready_mutex); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_location); + free(ctx->firmware_type); + free(ctx->reboot_flag); + free(ctx); + return RDKFW_UPDATE_FAILED; + } + + /* [7] Save thread handle locally BEFORE condvar wait. * - * Three trailing NULLs = fire and forget. + * CRITICAL: Same UAF prevention as checkForUpdate/downloadFirmware — + * on init failure, the worker frees ctx after signaling ready. + * We must not read ctx->thread after the condvar wake. Save it now. */ + pthread_t worker_thread = ctx->thread; + + /* [7b] Wait for worker to signal ready (bounded timeout) + * + * This blocks the caller for ~50-200ms while the worker sets up + * its D-Bus connection, subscribes to signals, and calls the daemon + * synchronously. The worker signals is_ready=true when it's either + * ready (daemon accepted) or has failed (D-Bus error or daemon rejected). + * + * We use pthread_cond_timedwait to prevent infinite hang if the + * worker crashes or exits without signaling. + */ + struct timespec deadline; + if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + FWUPMGR_ERROR("updateFirmware: clock_gettime failed (errno=%d), " + "using fallback deadline\n", errno); + deadline.tv_sec = time(NULL); + deadline.tv_nsec = 0; + } + deadline.tv_sec += WORKER_READY_TIMEOUT_SEC; + + pthread_mutex_lock(&ctx->ready_mutex); + int wait_rc = 0; + while (!ctx->is_ready && wait_rc == 0) { + wait_rc = pthread_cond_timedwait(&ctx->ready_cond, &ctx->ready_mutex, + &deadline); + } + bool failed = ctx->init_failed || (wait_rc == ETIMEDOUT); + pthread_mutex_unlock(&ctx->ready_mutex); + + if (wait_rc == ETIMEDOUT) { + FWUPMGR_ERROR("updateFirmware: worker thread did not signal ready " + "within %ds — treating as init failure. handle='%s'\n", + WORKER_READY_TIMEOUT_SEC, handle); + } + + /* [8] Check if worker failed to initialize or daemon rejected + * + * If init_failed is true, either D-Bus setup failed or the daemon + * rejected the update request. The worker thread is already + * cleaning itself up. We join it to avoid a zombie thread. + * + * IMPORTANT: We use worker_thread (local copy), NOT ctx->thread. + * After the condvar wake, the worker may have already freed ctx. + */ + if (failed) { + FWUPMGR_ERROR("updateFirmware: worker init failed or daemon rejected. " + "handle='%s'\n", handle); + pthread_join(worker_thread, NULL); + + /* After join, the worker has exited. If caller_owns_cleanup is set, + * the worker left the mutex/cond/ctx alive for us to clean up safely. + * The worker already freed its own resources (daemon_reject_message, etc.) + * but left our strdup'd strings, mutex/cond, and ctx for us. */ + pthread_mutex_destroy(&ctx->ready_mutex); + pthread_cond_destroy(&ctx->ready_cond); + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_location); + free(ctx->firmware_type); + free(ctx->reboot_flag); + free(ctx); + + return RDKFW_UPDATE_FAILED; + } + + /* [9] Worker is running and listening for UpdateProgress signals. + * + * From this point, the caller NEVER touches ctx again. + * The worker thread is the sole owner and will free it after + * the update completes, errors, or times out. + */ + FWUPMGR_INFO("updateFirmware: worker thread started, returning SUCCESS. " + "Callback will fire as update progresses. handle='%s'\n", + handle); - g_dbus_connection_call( - conn, - DBUS_SERVICE_NAME, - DBUS_OBJECT_PATH, - DBUS_INTERFACE_NAME, - DBUS_METHOD_UPDATE, /* method: UpdateFirmware */ - g_variant_new("(sssss)", /* ✅ 5 strings now! */ - handle, /* app's handler_id string */ - fwupdatereq->firmwareName, /* image to flash */ - fwupdatereq->LocationOfFirmware ? fwupdatereq->LocationOfFirmware : "", /* path or "" */ - fwupdatereq->TypeOfFirmware, /* PCI / PDRI / PERIPHERAL */ - fwupdatereq->rebootImmediately ? "true" : "false"), /* reboot flag sent to daemon */ - NULL, /* expected reply: none */ - G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, - NULL, /* GCancellable: none */ - NULL, /* reply callback: none */ - NULL /* user_data: none */ - ); - - 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 f1ffcc46..a59ec2ca 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async.c @@ -12,13 +12,34 @@ /** * @file rdkFwupdateMgr_async.c - * @brief Internal engine: registry, background thread, signal dispatch + * @brief Internal engine: CheckForUpdate, DownloadFirmware, UpdateFirmware + * — all use on-demand worker threads (Phase 1+2+3) * - * Owns: - * - Global callback registry (one slot per pending checkForUpdate call) - * - Background GLib event loop thread - * - D-Bus signal subscription and handler - * - Dispatch: signal arrives → find all PENDING → fire each callback + * ARCHITECTURE (Phase 3 — all APIs on-demand): + * + * CheckForUpdate — ON-DEMAND WORKER THREAD (Phase 1): + * - internal_check_worker_thread(): spawned per checkForUpdate() call + * - on_check_signal_handler(): fires client callback directly + * - on_check_timeout(): 120s safety net + * - internal_is_check_in_progress(): query for session-state enforcement + * - internal_cancel_all_active_check_threads(): destructor cleanup + * + * DownloadFirmware — ON-DEMAND WORKER THREAD (Phase 2): + * - internal_download_worker_thread(): spawned per downloadFirmware() call + * - on_download_signal_handler(): fires client callback, quits on terminal + * - on_download_timeout(): 3600s safety net, fires DWNL_ERROR callback + * - internal_is_dwnl_in_progress(): query for session-state enforcement + * - internal_cancel_all_active_download_threads(): destructor cleanup + * + * UpdateFirmware — ON-DEMAND WORKER THREAD (Phase 3): + * - internal_update_worker_thread(): spawned per updateFirmware() call + * - on_update_signal_handler(): fires client callback, quits on terminal + * - on_update_timeout(): 3600s safety net, fires UPDATE_ERROR callback + * - internal_is_update_in_progress(): query for session-state enforcement + * - internal_cancel_all_active_update_threads(): destructor cleanup + * + * NO persistent background thread. NO registries. + * Zero resource cost when idle. * * Apps never interact with this file directly. * All entry points are through rdkFwupdateMgr_api.c. @@ -37,309 +58,745 @@ * GLOBAL STATE * ======================================================================== */ -static CallbackRegistry g_registry; -static BackgroundThread g_bg_thread; -static DwnlCallbackRegistry g_dwnl_registry; -static UpdateCbRegistry g_update_registry; +/* ---- CheckForUpdate on-demand thread state ---- */ +/* + * TL;DR: These are STATIC — only accessible through accessor functions below. + * This prevents other files from touching the mutex/flag/pointer directly, + * which would be fragile and race-prone. All access goes through: + * internal_is_check_in_progress() — query + * internal_begin_check() — set in-progress, track ctx + * internal_end_check() — clear in-progress, untrack ctx + * internal_cancel_all_active_check_threads() — destructor cleanup + */ +static pthread_mutex_t g_check_in_progress_mutex = PTHREAD_MUTEX_INITIALIZER; +static bool g_check_in_progress = false; +static CheckRequestContext *g_active_check_ctx = NULL; + +/* ---- DownloadFirmware on-demand thread state (Phase 2) ---- */ +/* + * Same encapsulation pattern as CheckForUpdate. All access goes through: + * internal_is_dwnl_in_progress() — query + * internal_begin_download() — set in-progress, track ctx + * internal_end_download() — clear in-progress, untrack ctx + * internal_abort_download() — clear on error paths in downloadFirmware() + * internal_cancel_all_active_download_threads() — destructor cleanup + */ +static pthread_mutex_t g_dwnl_in_progress_mutex = PTHREAD_MUTEX_INITIALIZER; +static bool g_dwnl_in_progress = false; +static DownloadRequestContext *g_active_dwnl_ctx = NULL; + +/* ---- UpdateFirmware on-demand thread state (Phase 3) ---- */ +/* + * Same encapsulation pattern as Check and Download. All access goes through: + * internal_is_update_in_progress() — query + * internal_begin_update() — set in-progress, track ctx + * internal_end_update() — clear in-progress, untrack ctx + * internal_abort_update() — clear on error paths in updateFirmware() + * internal_cancel_all_active_update_threads() — destructor cleanup + */ +static pthread_mutex_t g_update_in_progress_mutex = PTHREAD_MUTEX_INITIALIZER; +static bool g_update_in_progress = false; +static UpdateRequestContext *g_active_update_ctx = NULL; /* ======================================================================== * FORWARD DECLARATIONS * ======================================================================== */ -static void *background_thread_func(void *arg); +/* Forward declarations — CheckForUpdate on-demand worker thread */ +static void on_check_signal_handler(GDBusConnection *conn, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data); +static gboolean on_check_timeout(gpointer user_data); + +/* Forward declarations — DownloadFirmware on-demand worker thread (Phase 2) */ +static void on_download_signal_handler(GDBusConnection *conn, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data); +static gboolean on_download_timeout(gpointer user_data); +static DownloadStatus map_dwnl_status_string(const char *status_str); -static void on_check_complete_signal(GDBusConnection *conn, +/* Forward declarations — UpdateFirmware on-demand worker thread (Phase 3) */ +static void on_update_signal_handler(GDBusConnection *conn, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data); +static gboolean on_update_timeout(gpointer user_data); -static void on_download_progress_signal(GDBusConnection *conn, - const gchar *sender, - const gchar *object_path, - const gchar *interface_name, - const gchar *signal_name, - GVariant *parameters, - gpointer user_data); - -static void on_update_progress_signal(GDBusConnection *conn, - const gchar *sender, - const gchar *object_path, - const gchar *interface_name, - const gchar *signal_name, - GVariant *parameters, - gpointer user_data); - -static void dispatch_all_pending(const InternalSignalData *signal_data); -static void registry_reset_slot(CallbackEntry *entry); static bool parse_update_details(const char *update_details_str, UpdateDetails *out_details); -/* Forward declaration for download status mapping function */ -static DownloadStatus map_dwnl_status_string(const char *status_str); - -/* Forward declarations for cleanup functions */ -static void internal_dwnl_system_deinit(void); -static void internal_update_system_deinit(void); - /* ======================================================================== - * LIBRARY LIFECYCLE + * CHECKFORUPDATE — ON-DEMAND WORKER THREAD ENGINE (Phase 1) + * ======================================================================== + * + * Replaces the old registry-based signal dispatch for CheckForUpdate. + * Each checkForUpdate() call spawns a short-lived worker thread that: + * 1. Creates isolated GLib event loop + * 2. Subscribes to CheckForUpdateComplete signal + * 3. Sends CheckForUpdate D-Bus method call + * 4. Waits for signal (with 120s timeout) + * 5. Fires client callback directly + * 6. Cleans up and exits + * + * At most ONE worker thread per process (enforced by g_check_in_progress). * ======================================================================== */ /** - * @brief Initialize the internal system + * @brief Query whether a checkForUpdate() is currently in progress. * - * STEPS: - * 1. Zero and mutex-init the registry - * 2. Create isolated GLib context + event loop - * 3. Spawn background thread - * 4. Wait until background thread confirms it is ready - * (ensures signal subscription exists before any D-Bus call is fired) + * Thread-safe: protected by g_check_in_progress_mutex. */ -int internal_system_init(void) +bool internal_is_check_in_progress(void) { - FWUPMGR_INFO("internal_system_init: begin\n"); + pthread_mutex_lock(&g_check_in_progress_mutex); + bool result = g_check_in_progress; + pthread_mutex_unlock(&g_check_in_progress_mutex); + return result; +} - /* Registry */ - memset(&g_registry, 0, sizeof(g_registry)); - if (pthread_mutex_init(&g_registry.mutex, NULL) != 0) { - FWUPMGR_ERROR("internal_system_init: registry mutex init failed\n"); - return -1; - } - g_registry.initialized = true; - - /* Background thread state */ - memset(&g_bg_thread, 0, sizeof(g_bg_thread)); - if (pthread_mutex_init(&g_bg_thread.mutex, NULL) != 0) { - FWUPMGR_ERROR("internal_system_init: bg thread mutex init failed\n"); - pthread_mutex_destroy(&g_registry.mutex); - return -1; +/** + * @brief Atomically try to begin a checkForUpdate session and track the context. + * + * TL;DR: This is the single entry point for transitioning from "idle" to + * "check in progress." It combines the duplicate-rejection check, the flag + * set, and the context tracking into ONE mutex-protected operation. The caller + * (checkForUpdate in _api.c) never touches the mutex or globals directly. + * + * @param ctx The newly allocated CheckRequestContext to track. + * @return true if the check was started (no other check was active), + * false if a check was already in progress (caller should return FAIL). + */ +bool internal_begin_check(CheckRequestContext *ctx) +{ + pthread_mutex_lock(&g_check_in_progress_mutex); + if (g_check_in_progress) { + pthread_mutex_unlock(&g_check_in_progress_mutex); + return false; /* already in progress — reject */ } + g_check_in_progress = true; + g_active_check_ctx = ctx; + pthread_mutex_unlock(&g_check_in_progress_mutex); + return true; +} - /* - * Isolated GLib context: prevents interference with any GLib event loop - * the app may be running on its own main thread. - */ - g_bg_thread.context = g_main_context_new(); - g_bg_thread.main_loop = g_main_loop_new(g_bg_thread.context, FALSE); - g_bg_thread.running = false; - - if (pthread_create(&g_bg_thread.thread, NULL, background_thread_func, NULL) != 0) { - FWUPMGR_ERROR("internal_system_init: pthread_create failed\n"); - g_main_loop_unref(g_bg_thread.main_loop); - g_main_context_unref(g_bg_thread.context); - pthread_mutex_destroy(&g_bg_thread.mutex); - pthread_mutex_destroy(&g_registry.mutex); - return -1; +/** + * @brief Atomically end the checkForUpdate session and untrack the context. + * + * TL;DR: Called by the worker thread in cleanup_common, right before freeing + * ctx. After this returns, g_active_check_ctx is NULL and g_check_in_progress + * is false — the next checkForUpdate() call will be accepted. + * + * IMPORTANT: Must be called BEFORE free(ctx). The mutex ensures the destructor + * cannot read g_active_check_ctx while we're freeing it. + */ +void internal_end_check(void) +{ + pthread_mutex_lock(&g_check_in_progress_mutex); + g_check_in_progress = false; + g_active_check_ctx = NULL; + pthread_mutex_unlock(&g_check_in_progress_mutex); +} + +/** + * @brief Atomically clear in-progress flag WITHOUT untracking context. + * + * TL;DR: Used only on error paths in checkForUpdate() (in _api.c) when + * the context was never successfully tracked (e.g., calloc/strdup/mutex_init + * failed before internal_begin_check was called) or when pthread_create fails + * after begin_check. The caller will free ctx itself. + */ +void internal_abort_check(void) +{ + pthread_mutex_lock(&g_check_in_progress_mutex); + g_check_in_progress = false; + g_active_check_ctx = NULL; + pthread_mutex_unlock(&g_check_in_progress_mutex); +} + +/** + * @brief Cancel all active checkForUpdate worker threads and join them. + * + * Called from library destructor. Quits the worker's event loop so it + * exits cleanly, then joins the thread to ensure no code is executing + * in library memory when dlclose() unmaps us. + * + * RACE-SAFETY: We snapshot ctx, thread, and take a GMainLoop ref all + * under the mutex, then clear the global pointer (so the worker's + * internal_end_check() becomes a benign no-op). After unlock we can + * safely quit the loop and join the thread — even if the worker is + * concurrently in cleanup, because: + * - The GMainLoop ref we hold prevents premature destruction + * - The pthread_t is a value copy, valid until pthread_join returns + * - The worker still frees ctx (it owns the allocation) + */ +void internal_cancel_all_active_check_threads(void) +{ + pthread_t saved_thread; + GMainLoop *saved_loop = NULL; + + pthread_mutex_lock(&g_check_in_progress_mutex); + + if (g_active_check_ctx == NULL) { + pthread_mutex_unlock(&g_check_in_progress_mutex); + FWUPMGR_INFO("internal_cancel_all_active_check_threads: no active worker\n"); + return; } - /* - * Spin-wait for background thread to set running=true. - * Max wait: 50 × 100ms = 5 seconds. - * Ensures D-Bus signal subscription is live before checkForUpdate() - * can send a D-Bus method call — prevents missing the response signal. - */ - for (int i = 0; i < 50; i++) { - pthread_mutex_lock(&g_bg_thread.mutex); - bool ready = g_bg_thread.running; - pthread_mutex_unlock(&g_bg_thread.mutex); - if (ready) break; - - struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 * 1000 * 1000 }; - nanosleep(&ts, NULL); + /* Snapshot what we need under the lock */ + saved_thread = g_active_check_ctx->thread; + if (g_active_check_ctx->main_loop != NULL) { + saved_loop = g_main_loop_ref(g_active_check_ctx->main_loop); } - /* Initialize download and update registries */ - memset(&g_dwnl_registry, 0, sizeof(g_dwnl_registry)); - if (pthread_mutex_init(&g_dwnl_registry.mutex, NULL) != 0) { - FWUPMGR_ERROR("internal_system_init: dwnl mutex init failed\n"); - return -1; + /* Take ownership: clear global so worker's internal_end_check() is a no-op */ + g_check_in_progress = false; + g_active_check_ctx = NULL; + + pthread_mutex_unlock(&g_check_in_progress_mutex); + + FWUPMGR_INFO("internal_cancel_all_active_check_threads: " + "stopping active worker thread\n"); + + /* Quit the worker's event loop — this causes g_main_loop_run() to return. + * Safe: we hold an extra ref, so the loop object is valid even if the + * worker concurrently unrefs it. */ + if (saved_loop != NULL) { + g_main_loop_quit(saved_loop); + g_main_loop_unref(saved_loop); } - g_dwnl_registry.initialized = true; - memset(&g_update_registry, 0, sizeof(g_update_registry)); - if (pthread_mutex_init(&g_update_registry.mutex, NULL) != 0) { - FWUPMGR_ERROR("internal_system_init: update mutex init failed\n"); - pthread_mutex_destroy(&g_dwnl_registry.mutex); - return -1; + /* Wait for worker thread to finish cleanup and exit */ + pthread_join(saved_thread, NULL); + + FWUPMGR_INFO("internal_cancel_all_active_check_threads: " + "worker thread joined\n"); +} + +/** + * @brief Timeout handler for the worker thread's GMainLoop. + * + * Fires after CHECK_SIGNAL_TIMEOUT_SECONDS if the daemon never sends + * the CheckForUpdateComplete signal. Quits the event loop so the worker + * can proceed to cleanup. + * + * @param user_data CheckRequestContext* (NOT freed here — worker does it) + * @return G_SOURCE_REMOVE (fire once only) + */ +static gboolean on_check_timeout(gpointer user_data) +{ + CheckRequestContext *ctx = (CheckRequestContext *)user_data; + + FWUPMGR_WARN("on_check_timeout: %ds timeout expired, " + "daemon did not respond. handle='%s'\n", + CHECK_SIGNAL_TIMEOUT_SECONDS, + ctx->handle_key ? ctx->handle_key : "(null)"); + + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); } - g_update_registry.initialized = true; - FWUPMGR_INFO("internal_system_init: ready\n"); - return 0; + return G_SOURCE_REMOVE; } /** - * @brief Shut down the internal system + * @brief Signal handler for CheckForUpdateComplete — fires client callback. * - * STEPS: - * 1. Quit GLib event loop → background thread exits g_main_loop_run() - * 2. Join background thread (wait for clean exit) - * 3. Free GLib resources - * 4. Free any remaining strdup'd handle_key strings in registry - * 5. Destroy mutexes + * Called by GLib in the worker thread's GMainContext when the daemon emits + * the CheckForUpdateComplete signal. Parses the payload, builds FwInfoData, + * invokes the client callback, then quits the event loop. + * + * @param user_data CheckRequestContext* (NOT freed here — worker does it) */ -void internal_system_deinit(void) +static void on_check_signal_handler(GDBusConnection *conn, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) { - FWUPMGR_INFO("internal_system_deinit: begin\n"); + (void)conn; (void)sender; (void)object_path; + (void)interface_name; (void)signal_name; + + CheckRequestContext *ctx = (CheckRequestContext *)user_data; - if (g_bg_thread.main_loop != NULL) { - g_main_loop_quit(g_bg_thread.main_loop); + FWUPMGR_INFO("on_check_signal_handler: received CheckForUpdateComplete " + "for handle='%s'\n", + ctx->handle_key ? ctx->handle_key : "(null)"); + + /* Parse signal payload */ + InternalSignalData signal_data; + memset(&signal_data, 0, sizeof(signal_data)); + + if (!internal_parse_signal_data(parameters, &signal_data)) { + FWUPMGR_ERROR("on_check_signal_handler: parse failed\n"); + /* Quit loop even on parse failure — don't hang forever */ + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + return; } - pthread_join(g_bg_thread.thread, NULL); + /* Build FwInfoData for the callback */ + CheckForUpdateStatus status = internal_map_status_code(signal_data.status_code); + + FwInfoData fwinfo_data; + memset(&fwinfo_data, 0, sizeof(fwinfo_data)); + + /* Copy current firmware version */ + 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'; + } - if (g_bg_thread.main_loop) g_main_loop_unref(g_bg_thread.main_loop); - if (g_bg_thread.context) g_main_context_unref(g_bg_thread.context); - pthread_mutex_destroy(&g_bg_thread.mutex); + fwinfo_data.status = status; - /* Cleanup download and update registries */ - internal_dwnl_system_deinit(); - internal_update_system_deinit(); + /* Parse UpdateDetails if firmware is available */ + UpdateDetails update_details; + if (status == FIRMWARE_AVAILABLE && signal_data.update_details) { + memset(&update_details, 0, sizeof(update_details)); - /* Free any leftover handle_key strings from CheckForUpdate registry */ - pthread_mutex_lock(&g_registry.mutex); - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - if (g_registry.entries[i].handle_key != NULL) { - free(g_registry.entries[i].handle_key); - g_registry.entries[i].handle_key = NULL; + if (parse_update_details(signal_data.update_details, &update_details)) { + fwinfo_data.UpdateDetails = &update_details; + FWUPMGR_INFO("on_check_signal_handler: UpdateDetails populated\n"); + } else { + fwinfo_data.UpdateDetails = NULL; + FWUPMGR_ERROR("on_check_signal_handler: parse_update_details failed\n"); } + } else { + fwinfo_data.UpdateDetails = NULL; } - pthread_mutex_unlock(&g_registry.mutex); - pthread_mutex_destroy(&g_registry.mutex); - FWUPMGR_INFO("internal_system_deinit: done\n"); + /* Fire the client's callback + * + * TL;DR: This is THE moment — deliver the firmware check result to the app. + * The callback runs in the worker thread, NOT the app's main thread. + * After this call returns, we quit the event loop and clean up. + */ + FWUPMGR_INFO("on_check_signal_handler: invoking callback for handle='%s'\n", + ctx->handle_key ? ctx->handle_key : "(null)"); + + ctx->callback(&fwinfo_data); + + FWUPMGR_INFO("on_check_signal_handler: callback returned\n"); + + /* Cleanup parsed signal data — free strdup'd strings */ + internal_cleanup_signal_data(&signal_data); + + /* Quit the event loop — worker proceeds to cleanup. + * TL;DR: Break out of g_main_loop_run() in the worker thread. */ + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } } /* ======================================================================== - * BACKGROUND THREAD + * DOWNLOAD FIRMWARE — ON-DEMAND WORKER THREAD ENGINE (Phase 2) + * ======================================================================== + * + * Replaces the old registry-based signal dispatch for DownloadFirmware. + * Each downloadFirmware() call spawns a worker thread that: + * 1. Creates isolated GLib event loop + * 2. Subscribes to DownloadProgress signal + * 3. Sends DownloadFirmware D-Bus method call SYNCHRONOUSLY + * 4. Reads daemon's (sss) reply: accept or reject + * 5. If accepted: runs event loop, fires callback on each progress signal + * 6. Quits loop on COMPLETED/ERROR/timeout + * 7. Cleans up and exits + * + * At most ONE download worker thread per process (enforced by g_dwnl_in_progress). * ======================================================================== */ /** - * @brief Background thread entry point + * @brief Query whether a downloadFirmware() is currently in progress. + * + * Thread-safe: protected by g_dwnl_in_progress_mutex. + */ +bool internal_is_dwnl_in_progress(void) +{ + pthread_mutex_lock(&g_dwnl_in_progress_mutex); + bool result = g_dwnl_in_progress; + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); + return result; +} + +/** + * @brief Atomically try to begin a downloadFirmware session and track the context. + */ +bool internal_begin_download(DownloadRequestContext *ctx) +{ + pthread_mutex_lock(&g_dwnl_in_progress_mutex); + if (g_dwnl_in_progress) { + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); + return false; /* already in progress — reject */ + } + g_dwnl_in_progress = true; + g_active_dwnl_ctx = ctx; + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); + return true; +} + +/** + * @brief Atomically end the downloadFirmware session and untrack the context. + * + * Called by worker thread in cleanup, BEFORE freeing ctx. + */ +void internal_end_download(void) +{ + pthread_mutex_lock(&g_dwnl_in_progress_mutex); + g_dwnl_in_progress = false; + g_active_dwnl_ctx = NULL; + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); +} + +/** + * @brief Atomically clear download in-progress state on error paths. + */ +void internal_abort_download(void) +{ + pthread_mutex_lock(&g_dwnl_in_progress_mutex); + g_dwnl_in_progress = false; + g_active_dwnl_ctx = NULL; + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); +} + +/** + * @brief Cancel all active download worker threads and join them. * - * Runs for the lifetime of the library. + * Called from library destructor. Quits the worker's event loop so it + * exits cleanly, then joins the thread to ensure no code is executing + * in library memory when dlclose() unmaps us. * - * 1. Push isolated GLib context for this thread - * 2. Connect to system D-Bus - * 3. Subscribe to CheckForUpdateComplete signal - * 4. Signal main thread: ready - * 5. g_main_loop_run() — blocks until deinit calls g_main_loop_quit() - * 6. Cleanup: unsubscribe, release connection, pop context + * RACE-SAFETY: Same pattern as internal_cancel_all_active_check_threads(). + * We snapshot the thread handle and take a GMainLoop ref under the mutex, + * then clear the global pointer so the worker's internal_end_download() + * becomes a benign no-op. The worker still owns ctx and frees it. */ -static void *background_thread_func(void *arg) +void internal_cancel_all_active_download_threads(void) { - (void)arg; - FWUPMGR_INFO("background_thread: starting\n"); + pthread_t saved_thread; + GMainLoop *saved_loop = NULL; - g_main_context_push_thread_default(g_bg_thread.context); + pthread_mutex_lock(&g_dwnl_in_progress_mutex); - GError *error = NULL; - g_bg_thread.connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); - if (g_bg_thread.connection == NULL) { - FWUPMGR_ERROR("background_thread: D-Bus connect failed: %s\n", - error ? error->message : "unknown"); - if (error) g_error_free(error); - goto thread_exit; + if (g_active_dwnl_ctx == NULL) { + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); + FWUPMGR_INFO("internal_cancel_all_active_download_threads: no active worker\n"); + return; } - /* - * Subscribe to the CheckForUpdateComplete signal. - * - * sender = NULL → accept from any sender - * (daemon's well-known name may vary by deployment) - * arg0 = NULL → no filter on first argument - * - * GLib calls on_check_complete_signal() in THIS thread's context - * whenever the signal arrives. - */ - g_bg_thread.subscription_id = g_dbus_connection_signal_subscribe( - g_bg_thread.connection, - NULL, /* sender: any */ - DBUS_INTERFACE_NAME, /* interface */ - DBUS_SIGNAL_COMPLETE, /* signal: CheckForUpdateComplete */ - DBUS_OBJECT_PATH, /* object path */ - NULL, /* arg0 filter: none */ - G_DBUS_SIGNAL_FLAGS_NONE, - on_check_complete_signal, /* handler */ - NULL, /* user_data: not needed (use globals)*/ - NULL /* user_data destroy notify */ - ); - - FWUPMGR_INFO("background_thread: subscribed to CheckForUpdateComplete (id=%u)\n", - g_bg_thread.subscription_id); - - /* - * Subscribe to DownloadProgress and UpdateProgress signals. - * Must be done HERE in the background thread, not from main thread, - * because the connection belongs to this thread's GMainContext. - */ - guint dwnl_sub_id = g_dbus_connection_signal_subscribe( - g_bg_thread.connection, - NULL, /* sender: any */ - DBUS_INTERFACE_NAME, /* interface */ - DBUS_SIGNAL_DWNL_PROGRESS, /* signal: DownloadProgress */ - DBUS_OBJECT_PATH, /* object path */ - NULL, /* arg0 filter: none */ - G_DBUS_SIGNAL_FLAGS_NONE, - on_download_progress_signal, /* handler */ - NULL, - NULL - ); - FWUPMGR_INFO("background_thread: subscribed to DownloadProgress (id=%u)\n", dwnl_sub_id); - - guint update_sub_id = g_dbus_connection_signal_subscribe( - g_bg_thread.connection, - NULL, /* sender: any */ - DBUS_INTERFACE_NAME, /* interface */ - DBUS_SIGNAL_UPDATE_PROGRESS, /* signal: UpdateProgress */ - DBUS_OBJECT_PATH, /* object path */ - NULL, /* arg0 filter: none */ - G_DBUS_SIGNAL_FLAGS_NONE, - on_update_progress_signal, /* handler */ - NULL, - NULL - ); - FWUPMGR_INFO("background_thread: subscribed to UpdateProgress (id=%u)\n", update_sub_id); - - /* Signal main thread that we are ready */ - pthread_mutex_lock(&g_bg_thread.mutex); - g_bg_thread.running = true; - pthread_mutex_unlock(&g_bg_thread.mutex); - - /* Block here until internal_system_deinit() calls g_main_loop_quit() */ - g_main_loop_run(g_bg_thread.main_loop); - FWUPMGR_INFO("background_thread: event loop exited\n"); - - if (g_bg_thread.subscription_id != 0) { - g_dbus_connection_signal_unsubscribe(g_bg_thread.connection, - g_bg_thread.subscription_id); + /* Snapshot what we need under the lock */ + saved_thread = g_active_dwnl_ctx->thread; + if (g_active_dwnl_ctx->main_loop != NULL) { + saved_loop = g_main_loop_ref(g_active_dwnl_ctx->main_loop); } - g_object_unref(g_bg_thread.connection); - g_bg_thread.connection = NULL; -thread_exit: - g_main_context_pop_thread_default(g_bg_thread.context); - FWUPMGR_INFO("background_thread: exiting\n"); - return NULL; + /* Take ownership: clear global so worker's internal_end_download() is a no-op */ + g_dwnl_in_progress = false; + g_active_dwnl_ctx = NULL; + + pthread_mutex_unlock(&g_dwnl_in_progress_mutex); + + FWUPMGR_INFO("internal_cancel_all_active_download_threads: " + "stopping active worker thread\n"); + + /* Quit the worker's event loop — safe via extra ref */ + if (saved_loop != NULL) { + g_main_loop_quit(saved_loop); + g_main_loop_unref(saved_loop); + } + + /* Wait for worker thread to finish cleanup and exit */ + pthread_join(saved_thread, NULL); + + FWUPMGR_INFO("internal_cancel_all_active_download_threads: " + "worker thread joined\n"); +} + +/** + * @brief Timeout handler for the download worker thread's GMainLoop. + * + * Fires after DWNL_SIGNAL_TIMEOUT_SECONDS (3600s) if the download never + * completes or errors. Fires DWNL_ERROR callback so the client knows, + * then quits the event loop. + */ +static gboolean on_download_timeout(gpointer user_data) +{ + DownloadRequestContext *ctx = (DownloadRequestContext *)user_data; + + FWUPMGR_ERROR("on_download_timeout: %ds timeout expired, " + "download did not complete. handle='%s'\n", + DWNL_SIGNAL_TIMEOUT_SECONDS, + ctx->handle_key ? ctx->handle_key : "(null)"); + + /* Fire error callback so client knows the download failed/stalled */ + if (ctx->callback != NULL) { + ctx->callback(0, DWNL_ERROR); + } + + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Signal handler for DownloadProgress — fires client callback. + * + * Called by GLib in the worker thread's GMainContext when the daemon emits + * a DownloadProgress signal. Parses the payload, maps status, invokes + * the client's callback. Quits the event loop ONLY on terminal status + * (COMPLETED or ERROR). + * + * KEY DIFFERENCE FROM CheckForUpdate: + * CheckForUpdate: one signal → callback → quit loop → thread exits + * DownloadFirmware: many signals → callback each time → quit only on terminal + */ +static void on_download_signal_handler(GDBusConnection *conn, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *signal_name, + GVariant *parameters, + gpointer user_data) +{ + (void)conn; (void)sender; (void)object_path; + (void)interface_name; (void)signal_name; + + DownloadRequestContext *ctx = (DownloadRequestContext *)user_data; + + /* Parse signal payload */ + InternalDwnlSignalData signal_data; + memset(&signal_data, 0, sizeof(signal_data)); + + if (!internal_parse_dwnl_signal_data(parameters, &signal_data)) { + FWUPMGR_ERROR("on_download_signal_handler: parse failed\n"); + return; /* Don't quit loop on parse failure — wait for next signal */ + } + + FWUPMGR_INFO("on_download_signal_handler: handler=%" PRIu64 + " firmware='%s' progress=%u%% status='%s' handle='%s'\n", + signal_data.handler_id, + signal_data.firmware_name ? signal_data.firmware_name : "(null)", + signal_data.progress_percent, + signal_data.status_string ? signal_data.status_string : "(null)", + ctx->handle_key ? ctx->handle_key : "(null)"); + + /* Map status string to enum */ + DownloadStatus status = map_dwnl_status_string(signal_data.status_string); + + /* Fire the client's callback with progress and status */ + if (ctx->callback != NULL) { + ctx->callback((int)signal_data.progress_percent, status); + } + + /* Free parsed signal data strings (allocated by g_variant_get) */ + g_free(signal_data.firmware_name); + g_free(signal_data.status_string); + g_free(signal_data.message); + + /* Quit loop ONLY on terminal status — otherwise wait for next signal */ + if (status == DWNL_COMPLETED || status == DWNL_ERROR) { + FWUPMGR_INFO("on_download_signal_handler: terminal status (%s), " + "quitting loop. handle='%s'\n", + (status == DWNL_COMPLETED) ? "COMPLETED" : "ERROR", + ctx->handle_key ? ctx->handle_key : "(null)"); + + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + } } /* ======================================================================== - * D-BUS SIGNAL HANDLER + * UPDATE FIRMWARE — ON-DEMAND WORKER THREAD ENGINE (Phase 3) + * ======================================================================== + * + * Same pattern as DownloadFirmware. Each updateFirmware() call spawns a + * worker thread that: + * 1. Creates isolated GLib event loop + * 2. Subscribes to UpdateProgress signal + * 3. Sends UpdateFirmware D-Bus method call SYNCHRONOUSLY + * 4. Reads daemon's (sss) reply: accept or reject + * 5. If accepted: runs event loop, fires callback on each progress signal + * 6. Quits loop on COMPLETED/ERROR/timeout + * 7. Cleans up and exits + * + * At most ONE update worker thread per process (enforced by g_update_in_progress). * ======================================================================== */ /** - * @brief Called by GLib when CheckForUpdateComplete signal arrives + * @brief Query whether an updateFirmware() is currently in progress. + * + * Thread-safe: protected by g_update_in_progress_mutex. + */ +bool internal_is_update_in_progress(void) +{ + pthread_mutex_lock(&g_update_in_progress_mutex); + bool result = g_update_in_progress; + pthread_mutex_unlock(&g_update_in_progress_mutex); + return result; +} + +/** + * @brief Atomically try to begin an updateFirmware session and track the context. + * + * TL;DR: This is the single entry point for transitioning from "idle" to + * "update in progress." It combines the duplicate-rejection check, the flag + * set, and the context tracking into ONE mutex-protected operation. + * + * @param ctx The newly allocated UpdateRequestContext to track. + * @return true if the update was started (no other update was active), + * false if an update was already in progress (caller should return FAIL). + */ +bool internal_begin_update(UpdateRequestContext *ctx) +{ + pthread_mutex_lock(&g_update_in_progress_mutex); + if (g_update_in_progress) { + pthread_mutex_unlock(&g_update_in_progress_mutex); + return false; /* already in progress — reject */ + } + g_update_in_progress = true; + g_active_update_ctx = ctx; + pthread_mutex_unlock(&g_update_in_progress_mutex); + return true; +} + +/** + * @brief Atomically end the updateFirmware session and untrack the context. + * + * Called by worker thread in cleanup, BEFORE freeing ctx. + * After this returns, g_active_update_ctx is NULL and g_update_in_progress + * is false — the next updateFirmware() call will be accepted. + */ +void internal_end_update(void) +{ + pthread_mutex_lock(&g_update_in_progress_mutex); + g_update_in_progress = false; + g_active_update_ctx = NULL; + pthread_mutex_unlock(&g_update_in_progress_mutex); +} + +/** + * @brief Atomically clear update in-progress state on error paths. * - * Runs in the background thread context. + * Used when updateFirmware() itself fails (e.g., pthread_create fails + * after internal_begin_update succeeded). The caller will free ctx directly. + */ +void internal_abort_update(void) +{ + pthread_mutex_lock(&g_update_in_progress_mutex); + g_update_in_progress = false; + g_active_update_ctx = NULL; + pthread_mutex_unlock(&g_update_in_progress_mutex); +} + +/** + * @brief Cancel all active update worker threads and join them. * - * 1. Parse GVariant payload → InternalSignalData - * 2. Dispatch to all PENDING registry entries - * 3. Free parsed signal data + * Called from library destructor. Quits the worker's event loop so it + * exits cleanly, then joins the thread to ensure no code is executing + * in library memory when dlclose() unmaps us. + * + * RACE-SAFETY: Same pattern as internal_cancel_all_active_check_threads(). + * We snapshot the thread handle and take a GMainLoop ref under the mutex, + * then clear the global pointer so the worker's internal_end_update() + * becomes a benign no-op. The worker still owns ctx and frees it. */ -static void on_check_complete_signal(GDBusConnection *conn, +void internal_cancel_all_active_update_threads(void) +{ + pthread_t saved_thread; + GMainLoop *saved_loop = NULL; + + pthread_mutex_lock(&g_update_in_progress_mutex); + + if (g_active_update_ctx == NULL) { + pthread_mutex_unlock(&g_update_in_progress_mutex); + FWUPMGR_INFO("internal_cancel_all_active_update_threads: no active worker\n"); + return; + } + + /* Snapshot what we need under the lock */ + saved_thread = g_active_update_ctx->thread; + if (g_active_update_ctx->main_loop != NULL) { + saved_loop = g_main_loop_ref(g_active_update_ctx->main_loop); + } + + /* Take ownership: clear global so worker's internal_end_update() is a no-op */ + g_update_in_progress = false; + g_active_update_ctx = NULL; + + pthread_mutex_unlock(&g_update_in_progress_mutex); + + FWUPMGR_INFO("internal_cancel_all_active_update_threads: " + "stopping active worker thread\n"); + + /* Quit the worker's event loop — safe via extra ref */ + if (saved_loop != NULL) { + g_main_loop_quit(saved_loop); + g_main_loop_unref(saved_loop); + } + + /* Wait for worker thread to finish cleanup and exit */ + pthread_join(saved_thread, NULL); + + FWUPMGR_INFO("internal_cancel_all_active_update_threads: " + "worker thread joined\n"); +} + +/** + * @brief Timeout handler for the update worker thread's GMainLoop. + * + * Fires after UPDATE_SIGNAL_TIMEOUT_SECONDS (3600s) if the update never + * completes or errors. Fires UPDATE_ERROR callback so the client knows, + * then quits the event loop. + */ +static gboolean on_update_timeout(gpointer user_data) +{ + UpdateRequestContext *ctx = (UpdateRequestContext *)user_data; + + FWUPMGR_ERROR("on_update_timeout: %ds timeout expired, " + "update did not complete. handle='%s'\n", + UPDATE_SIGNAL_TIMEOUT_SECONDS, + ctx->handle_key ? ctx->handle_key : "(null)"); + + /* Fire error callback so client knows the update failed/stalled */ + if (ctx->callback != NULL) { + ctx->callback(0, UPDATE_ERROR); + } + + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + + return G_SOURCE_REMOVE; +} + +/** + * @brief Signal handler for UpdateProgress — fires client callback. + * + * Called by GLib in the worker thread's GMainContext when the daemon emits + * an UpdateProgress signal. Parses the (tsiis) payload, maps status, invokes + * the client's callback. Quits the event loop ONLY on terminal status + * (UPDATE_COMPLETED or UPDATE_ERROR). + * + * Same pattern as on_download_signal_handler: + * Many signals → callback each time → quit only on terminal + */ +static void on_update_signal_handler(GDBusConnection *conn, const gchar *sender, const gchar *object_path, const gchar *interface_name, @@ -348,232 +805,701 @@ static void on_check_complete_signal(GDBusConnection *conn, gpointer user_data) { (void)conn; (void)sender; (void)object_path; - (void)interface_name; (void)signal_name; (void)user_data; + (void)interface_name; (void)signal_name; - FWUPMGR_INFO("on_check_complete_signal: received\n"); + UpdateRequestContext *ctx = (UpdateRequestContext *)user_data; - InternalSignalData signal_data; + /* Parse signal payload — correct (tsiis) format */ + InternalUpdateSignalData signal_data; memset(&signal_data, 0, sizeof(signal_data)); - if (!internal_parse_signal_data(parameters, &signal_data)) { - FWUPMGR_ERROR("on_check_complete_signal: parse failed\n"); - return; + if (!internal_parse_update_signal_data(parameters, &signal_data)) { + FWUPMGR_ERROR("on_update_signal_handler: parse failed\n"); + return; /* Don't quit loop on parse failure — wait for next signal */ } - dispatch_all_pending(&signal_data); + FWUPMGR_INFO("on_update_signal_handler: handler=%" PRIu64 + " firmware='%s' progress=%d%% status=%d handle='%s'\n", + signal_data.handler_id, + signal_data.firmware_name ? signal_data.firmware_name : "(null)", + signal_data.progress_percent, + signal_data.status_code, + ctx->handle_key ? ctx->handle_key : "(null)"); - internal_cleanup_signal_data(&signal_data); + /* Map status code to enum */ + UpdateStatus status = internal_map_update_status_code(signal_data.status_code); + + /* Fire the client's callback with progress and status */ + if (ctx->callback != NULL) { + ctx->callback(signal_data.progress_percent, status); + } + + /* Free parsed signal data strings (allocated by g_variant_get) */ + g_free(signal_data.firmware_name); + g_free(signal_data.message); + + /* Quit loop ONLY on terminal status — otherwise wait for next signal */ + if (status == UPDATE_COMPLETED || status == UPDATE_ERROR) { + FWUPMGR_INFO("on_update_signal_handler: terminal status (%s), " + "quitting loop. handle='%s'\n", + (status == UPDATE_COMPLETED) ? "COMPLETED" : "ERROR", + ctx->handle_key ? ctx->handle_key : "(null)"); + + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + } } -/** - * @brief Dispatch signal result to every PENDING callback - * - * 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. +/* ======================================================================== + * WORKER THREAD IMPLEMENTATIONS + * ======================================================================== + * These are the actual thread entry points spawned by checkForUpdate(), + * downloadFirmware(), and updateFirmware() in rdkFwupdateMgr_api.c. * - * 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. + * Each worker: + * 1. Creates an isolated GLib event loop (per-thread GMainContext) + * 2. Connects to D-Bus (system bus) + * 3. Subscribes to the appropriate D-Bus signal + * 4. Sends the D-Bus method call (fire-and-forget or synchronous) + * 5. Signals the caller "ready" via condvar + * 6. Runs g_main_loop_run() to wait for signals (with timeout) + * 7. Cleans up ALL resources and exits * - * 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. + * OWNERSHIP: The caller (api.c) transfers ctx ownership to the worker. + * After condvar handshake, the caller never touches ctx again. + * The worker is responsible for freeing ctx and all its members. * - * @param signal_data Parsed signal payload (shared across all callbacks) - */ -static void dispatch_all_pending(const InternalSignalData *signal_data) + * CLEANUP ORDER (critical for no-leak, no-crash): + * 1. Unsubscribe from D-Bus signal (subscription_id) + * 2. Destroy timeout source (if any) + * 3. Quit and unref GMainLoop + * 4. Unref GMainContext (pop thread-default first) + * 5. Close D-Bus connection (g_object_unref — NOT g_dbus_connection_close) + * 6. internal_end_*() — clear in-progress flag BEFORE freeing ctx + * 7. Destroy condvar, mutex + * 8. Free all strdup'd strings + * 9. free(ctx) — last step + * ======================================================================== */ + +/* ======================================================================== + * internal_check_worker_thread — Phase 1: CheckForUpdate + * ======================================================================== */ + +void *internal_check_worker_thread(void *arg) { - /* Local snapshot — avoids holding mutex during callback invocations */ - typedef struct { - UpdateEventCallback callback; - char handle_copy[256]; - int slot_index; - } Snapshot; - - Snapshot snapshots[MAX_PENDING_CALLBACKS]; - int count = 0; - - /* ---- PHASE 1: collect under mutex ---- */ - pthread_mutex_lock(&g_registry.mutex); - - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - CallbackEntry *e = &g_registry.entries[i]; - if (e->state != CB_STATE_PENDING) continue; - - 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 : ""); - - e->state = CB_STATE_DISPATCHED; - count++; - - FWUPMGR_INFO("dispatch_all_pending: queued handle='%s'\n", - e->handle_key ? e->handle_key : "(null)"); + CheckRequestContext *ctx = (CheckRequestContext *)arg; + + FWUPMGR_INFO("internal_check_worker_thread: starting for handle='%s'\n", + ctx->handle_key ? ctx->handle_key : "(null)"); + + /* ---- Step 1: Create isolated GLib event loop ---- */ + ctx->context = g_main_context_new(); + g_main_context_push_thread_default(ctx->context); + ctx->main_loop = g_main_loop_new(ctx->context, FALSE); + + /* ---- Step 2: Connect to D-Bus system bus ---- */ + GError *error = NULL; + ctx->connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + + if (ctx->connection == NULL) { + FWUPMGR_ERROR("internal_check_worker_thread: D-Bus connect failed: %s\n", + error ? error->message : "unknown"); + if (error) g_error_free(error); + + /* Signal caller: init failed. + * CRITICAL: Set caller_owns_cleanup BEFORE signaling. After the signal, + * the caller may wake up, read init_failed, unlock the mutex, and + * pthread_join us. If we were to destroy the mutex/cond in cleanup, + * we'd race with the caller who is still holding/using them. + * By setting caller_owns_cleanup=true, we tell cleanup to skip + * mutex/cond destroy and free(ctx) — the caller does it after join. */ + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + goto cleanup; } - pthread_mutex_unlock(&g_registry.mutex); + FWUPMGR_INFO("internal_check_worker_thread: D-Bus connected\n"); - FWUPMGR_INFO("dispatch_all_pending: %d callback(s) to fire\n", count); + /* ---- Step 3: Subscribe to CheckForUpdateComplete signal ---- */ + ctx->subscription_id = g_dbus_connection_signal_subscribe( + ctx->connection, + DBUS_SERVICE_NAME, /* sender (daemon's bus name) */ + DBUS_INTERFACE_NAME, /* interface */ + DBUS_SIGNAL_COMPLETE, /* signal name */ + DBUS_OBJECT_PATH, /* object path */ + NULL, /* arg0 match (none) */ + G_DBUS_SIGNAL_FLAGS_NONE, + on_check_signal_handler, /* handler */ + ctx, /* user_data */ + NULL); /* user_data free func (we free manually) */ + + FWUPMGR_INFO("internal_check_worker_thread: subscribed to %s (id=%u)\n", + DBUS_SIGNAL_COMPLETE, ctx->subscription_id); + + /* ---- Step 4: Send CheckForUpdate D-Bus method call (fire-and-forget) ---- + * + * CheckForUpdate takes (s handler_process_name) and returns (issssi). + * We don't use the method return — the real result comes via signal. + * We use async call (fire-and-forget) to avoid blocking the condvar handshake. + */ + g_dbus_connection_call( + ctx->connection, + DBUS_SERVICE_NAME, + DBUS_OBJECT_PATH, + DBUS_INTERFACE_NAME, + DBUS_METHOD_CHECK, + g_variant_new("(s)", ctx->handle_key), + NULL, /* reply type (don't care) */ + G_DBUS_CALL_FLAGS_NONE, + DBUS_TIMEOUT_MS, + NULL, /* cancellable */ + NULL, /* callback (fire-and-forget) */ + NULL); /* user_data */ + + FWUPMGR_INFO("internal_check_worker_thread: CheckForUpdate method sent\n"); + + /* ---- Step 5: Add timeout source ---- */ + GSource *timeout_source = g_timeout_source_new_seconds(CHECK_SIGNAL_TIMEOUT_SECONDS); + g_source_set_callback(timeout_source, on_check_timeout, ctx, NULL); + g_source_attach(timeout_source, ctx->context); + g_source_unref(timeout_source); /* context holds ref now */ + + /* ---- Step 6: Signal caller "ready" ---- */ + pthread_mutex_lock(&ctx->ready_mutex); + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + FWUPMGR_INFO("internal_check_worker_thread: signaled ready, entering event loop\n"); + + /* ---- Step 7: Run event loop — wait for signal or timeout ---- */ + g_main_loop_run(ctx->main_loop); + + FWUPMGR_INFO("internal_check_worker_thread: event loop exited\n"); + +cleanup: + /* ---- Cleanup: release all resources in correct order ---- */ + FWUPMGR_INFO("internal_check_worker_thread: cleaning up\n"); + + /* Unsubscribe from signal */ + if (ctx->connection && ctx->subscription_id > 0) { + g_dbus_connection_signal_unsubscribe(ctx->connection, ctx->subscription_id); + } + + /* Quit and unref main loop */ + if (ctx->main_loop) { + if (g_main_loop_is_running(ctx->main_loop)) { + g_main_loop_quit(ctx->main_loop); + } + g_main_loop_unref(ctx->main_loop); + ctx->main_loop = NULL; + } + + /* Pop and unref context */ + if (ctx->context) { + g_main_context_pop_thread_default(ctx->context); + g_main_context_unref(ctx->context); + ctx->context = NULL; + } + + /* Release D-Bus connection (shared connection — unref only, don't close) */ + if (ctx->connection) { + g_object_unref(ctx->connection); + ctx->connection = NULL; + } - /* ---- PHASE 2: invoke callbacks, no mutex held ---- */ + /* Clear in-progress flag BEFORE freeing ctx */ + internal_end_check(); + + /* Destroy synchronization primitives and free ctx — BUT only if the worker + * owns cleanup. On init-failure paths, the caller (API function) owns these + * because it may still be inside pthread_cond_timedwait or about to call + * pthread_mutex_unlock on the same mutex. The caller will destroy/free + * after pthread_join returns. */ + if (!ctx->caller_owns_cleanup) { + pthread_mutex_destroy(&ctx->ready_mutex); + pthread_cond_destroy(&ctx->ready_cond); + + /* Free strdup'd strings */ + free(ctx->handle_key); + + /* Free context */ + free(ctx); + } else { + /* Worker does NOT own the sync primitives or ctx on init-failure. + * But we still need to free any GLib/strdup resources that we allocated + * in the worker thread before the failure. handle_key was allocated by + * the caller, so it will be freed by the caller after join. */ + FWUPMGR_INFO("internal_check_worker_thread: caller_owns_cleanup=true, " + "skipping mutex/cond destroy and free(ctx)\n"); + } + + FWUPMGR_INFO("internal_check_worker_thread: thread exiting\n"); + return NULL; +} + +/* ======================================================================== + * internal_download_worker_thread — Phase 2: DownloadFirmware + * ======================================================================== */ + +void *internal_download_worker_thread(void *arg) +{ + DownloadRequestContext *ctx = (DownloadRequestContext *)arg; - CheckForUpdateStatus status = internal_map_status_code(signal_data->status_code); + FWUPMGR_INFO("internal_download_worker_thread: starting for handle='%s' " + "firmware='%s'\n", + ctx->handle_key ? ctx->handle_key : "(null)", + ctx->firmware_name ? ctx->firmware_name : "(null)"); - /* - * Build FwInfoData with UpdateDetails for the callback. - * This matches the public API signature: UpdateEventCallback(const FwInfoData*) + /* ---- Step 1: Create isolated GLib event loop ---- */ + ctx->context = g_main_context_new(); + g_main_context_push_thread_default(ctx->context); + ctx->main_loop = g_main_loop_new(ctx->context, FALSE); + + /* ---- Step 2: Connect to D-Bus system bus ---- */ + GError *error = NULL; + ctx->connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + + if (ctx->connection == NULL) { + FWUPMGR_ERROR("internal_download_worker_thread: D-Bus connect failed: %s\n", + error ? error->message : "unknown"); + if (error) g_error_free(error); + + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + goto cleanup; + } + + FWUPMGR_INFO("internal_download_worker_thread: D-Bus connected\n"); + + /* ---- Step 3: Subscribe to DownloadProgress signal ---- */ + ctx->subscription_id = g_dbus_connection_signal_subscribe( + ctx->connection, + DBUS_SERVICE_NAME, + DBUS_INTERFACE_NAME, + DBUS_SIGNAL_DWNL_PROGRESS, + DBUS_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_download_signal_handler, + ctx, + NULL); + + FWUPMGR_INFO("internal_download_worker_thread: subscribed to %s (id=%u)\n", + DBUS_SIGNAL_DWNL_PROGRESS, ctx->subscription_id); + + /* ---- Step 4: Send DownloadFirmware D-Bus method call SYNCHRONOUSLY ---- + * + * D-Bus signature IN: (ssss) — handlerId, firmwareName, downloadUrl, typeOfFirmware + * D-Bus signature OUT: (sss) — result, status, message * - * 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 + * We call synchronously so we can read the daemon's accept/reject reply + * BEFORE signaling the caller. This gives the caller an ACCURATE return value. */ - FwInfoData fwinfo_data; - memset(&fwinfo_data, 0, sizeof(fwinfo_data)); + FWUPMGR_INFO("internal_download_worker_thread: calling DownloadFirmware " + "synchronously...\n"); + + GVariant *reply = g_dbus_connection_call_sync( + ctx->connection, + DBUS_SERVICE_NAME, + DBUS_OBJECT_PATH, + DBUS_INTERFACE_NAME, + DBUS_METHOD_DOWNLOAD, + g_variant_new("(ssss)", + ctx->handle_key, + ctx->firmware_name, + ctx->firmware_url ? ctx->firmware_url : "", + ctx->firmware_type ? ctx->firmware_type : ""), + G_VARIANT_TYPE("(sss)"), /* expected reply signature */ + G_DBUS_CALL_FLAGS_NONE, + 30000, /* 30s timeout for method call itself */ + NULL, /* cancellable */ + &error); + + if (reply == NULL) { + FWUPMGR_ERROR("internal_download_worker_thread: D-Bus call failed: %s\n", + error ? error->message : "unknown"); + if (error) g_error_free(error); - /* Copy current firmware version */ - 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'; + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + goto cleanup; } - /* Set status */ - fwinfo_data.status = status; + /* Parse daemon's (sss) reply: result, status, message */ + const gchar *result_str = NULL; + const gchar *status_str = NULL; + const gchar *message_str = NULL; + g_variant_get(reply, "(&s&s&s)", &result_str, &status_str, &message_str); + + FWUPMGR_INFO("internal_download_worker_thread: daemon reply: " + "result='%s' status='%s' message='%s'\n", + result_str ? result_str : "(null)", + status_str ? status_str : "(null)", + message_str ? message_str : "(null)"); + + /* Check if daemon accepted or rejected */ + if (result_str && strcmp(result_str, "RDKFW_DWNL_SUCCESS") == 0) { + ctx->daemon_accepted = true; + FWUPMGR_INFO("internal_download_worker_thread: daemon ACCEPTED download\n"); + } else { + ctx->daemon_accepted = false; + ctx->daemon_reject_message = (message_str && message_str[0]) + ? strdup(message_str) : NULL; + FWUPMGR_WARN("internal_download_worker_thread: daemon REJECTED download: %s\n", + message_str ? message_str : "(no message)"); + } + + g_variant_unref(reply); + + /* If daemon rejected, signal caller with failure and exit */ + if (!ctx->daemon_accepted) { + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + goto cleanup; + } + + /* ---- Step 5: Add timeout source (3600s) ---- */ + ctx->timeout_source = g_timeout_source_new_seconds(DWNL_SIGNAL_TIMEOUT_SECONDS); + g_source_set_callback(ctx->timeout_source, on_download_timeout, ctx, NULL); + g_source_attach(ctx->timeout_source, ctx->context); + g_source_unref(ctx->timeout_source); /* context holds ref now */ + ctx->timeout_source = NULL; /* don't double-unref in cleanup */ + + /* ---- Step 6: Signal caller "ready" with success ---- */ + pthread_mutex_lock(&ctx->ready_mutex); + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + FWUPMGR_INFO("internal_download_worker_thread: signaled ready, " + "entering event loop\n"); + + /* ---- Step 7: Run event loop — wait for DownloadProgress signals ---- */ + g_main_loop_run(ctx->main_loop); + + FWUPMGR_INFO("internal_download_worker_thread: event loop exited\n"); + +cleanup: + FWUPMGR_INFO("internal_download_worker_thread: cleaning up\n"); + + /* Unsubscribe from signal */ + if (ctx->connection && ctx->subscription_id > 0) { + g_dbus_connection_signal_unsubscribe(ctx->connection, ctx->subscription_id); + } + + /* Quit and unref main loop */ + if (ctx->main_loop) { + if (g_main_loop_is_running(ctx->main_loop)) { + g_main_loop_quit(ctx->main_loop); + } + g_main_loop_unref(ctx->main_loop); + ctx->main_loop = NULL; + } + + /* Pop and unref context */ + if (ctx->context) { + g_main_context_pop_thread_default(ctx->context); + g_main_context_unref(ctx->context); + ctx->context = NULL; + } + + /* Release D-Bus connection */ + if (ctx->connection) { + g_object_unref(ctx->connection); + ctx->connection = NULL; + } + + /* Clear in-progress flag BEFORE freeing ctx */ + internal_end_download(); + + /* Destroy synchronization primitives and free ctx — BUT only if the worker + * owns cleanup. On init-failure paths, the caller (API function) owns these + * because it may still be inside pthread_cond_timedwait or about to call + * pthread_mutex_unlock on the same mutex. The caller will destroy/free + * after pthread_join returns. */ + if (!ctx->caller_owns_cleanup) { + pthread_mutex_destroy(&ctx->ready_mutex); + pthread_cond_destroy(&ctx->ready_cond); + + /* Free strdup'd strings */ + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_url); + free(ctx->firmware_type); + free(ctx->daemon_reject_message); + + /* Free context */ + free(ctx); + } else { + /* Worker does NOT own the sync primitives or ctx on init-failure. + * The caller will free everything after pthread_join. + * We still need to free any worker-allocated resources (like + * daemon_reject_message which was strdup'd in the worker). */ + free(ctx->daemon_reject_message); + ctx->daemon_reject_message = NULL; + FWUPMGR_INFO("internal_download_worker_thread: caller_owns_cleanup=true, " + "skipping mutex/cond destroy and free(ctx)\n"); + } + + FWUPMGR_INFO("internal_download_worker_thread: thread exiting\n"); + return NULL; +} + +/* ======================================================================== + * internal_update_worker_thread — Phase 3: UpdateFirmware + * ======================================================================== */ + +void *internal_update_worker_thread(void *arg) +{ + UpdateRequestContext *ctx = (UpdateRequestContext *)arg; + + FWUPMGR_INFO("internal_update_worker_thread: starting for handle='%s' " + "firmware='%s' type='%s' location='%s' reboot='%s'\n", + ctx->handle_key ? ctx->handle_key : "(null)", + ctx->firmware_name ? ctx->firmware_name : "(null)", + ctx->firmware_type ? ctx->firmware_type : "(null)", + ctx->firmware_location ? ctx->firmware_location : "(default)", + ctx->reboot_flag ? ctx->reboot_flag : "(null)"); + + /* ---- Step 1: Create isolated GLib event loop ---- */ + ctx->context = g_main_context_new(); + g_main_context_push_thread_default(ctx->context); + ctx->main_loop = g_main_loop_new(ctx->context, FALSE); + + /* ---- Step 2: Connect to D-Bus system bus ---- */ + GError *error = NULL; + ctx->connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, NULL, &error); + + if (ctx->connection == NULL) { + FWUPMGR_ERROR("internal_update_worker_thread: D-Bus connect failed: %s\n", + error ? error->message : "unknown"); + if (error) g_error_free(error); + + /* CRITICAL: Set caller_owns_cleanup BEFORE signaling. After the signal, + * the caller may wake up, read init_failed, unlock the mutex, and + * pthread_join us. If we were to destroy the mutex/cond in cleanup, + * we'd race with the caller who is still holding/using them. + * By setting caller_owns_cleanup=true, we tell cleanup to skip + * mutex/cond destroy and free(ctx) — the caller does it after join. */ + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + goto cleanup; + } + + FWUPMGR_INFO("internal_update_worker_thread: D-Bus connected\n"); + + /* ---- Step 3: Subscribe to UpdateProgress signal ---- */ + ctx->subscription_id = g_dbus_connection_signal_subscribe( + ctx->connection, + DBUS_SERVICE_NAME, + DBUS_INTERFACE_NAME, + DBUS_SIGNAL_UPDATE_PROGRESS, + DBUS_OBJECT_PATH, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_update_signal_handler, + ctx, + NULL); + + FWUPMGR_INFO("internal_update_worker_thread: subscribed to %s (id=%u)\n", + DBUS_SIGNAL_UPDATE_PROGRESS, ctx->subscription_id); + + /* ---- Step 4: Send UpdateFirmware D-Bus method call SYNCHRONOUSLY ---- + * + * D-Bus signature IN: (sssss) — handlerId, firmwareName, LocationOfFirmware, + * TypeOfFirmware, rebootImmediately + * D-Bus signature OUT: (sss) — UpdateResult, UpdateStatus, message + * + * We call synchronously so we can read the daemon's accept/reject reply + * BEFORE signaling the caller. This gives the caller an ACCURATE return value. + */ + FWUPMGR_INFO("internal_update_worker_thread: calling UpdateFirmware " + "synchronously...\n"); + + GVariant *reply = g_dbus_connection_call_sync( + ctx->connection, + DBUS_SERVICE_NAME, + DBUS_OBJECT_PATH, + DBUS_INTERFACE_NAME, + DBUS_METHOD_UPDATE, + g_variant_new("(sssss)", + ctx->handle_key, + ctx->firmware_name, + ctx->firmware_location ? ctx->firmware_location : "", + ctx->firmware_type ? ctx->firmware_type : "", + ctx->reboot_flag ? ctx->reboot_flag : "false"), + G_VARIANT_TYPE("(sss)"), /* expected reply signature */ + G_DBUS_CALL_FLAGS_NONE, + 30000, /* 30s timeout for method call itself */ + NULL, /* cancellable */ + &error); + + if (reply == NULL) { + FWUPMGR_ERROR("internal_update_worker_thread: D-Bus call failed: %s\n", + error ? error->message : "unknown"); + if (error) g_error_free(error); + + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); + + goto cleanup; + } - /* Parse and populate UpdateDetails if firmware is available */ - UpdateDetails update_details; - if (status == FIRMWARE_AVAILABLE && signal_data->update_details) { - memset(&update_details, 0, sizeof(update_details)); - - if (parse_update_details(signal_data->update_details, &update_details)) { - /* Point FwInfoData to our stack-allocated UpdateDetails */ - 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 */ - fwinfo_data.UpdateDetails = NULL; - FWUPMGR_ERROR("dispatch_all_pending: parse_update_details failed\n"); - } + /* Parse daemon's (sss) reply: UpdateResult, UpdateStatus, message */ + const gchar *result_str = NULL; + const gchar *status_str = NULL; + const gchar *message_str = NULL; + g_variant_get(reply, "(&s&s&s)", &result_str, &status_str, &message_str); + + FWUPMGR_INFO("internal_update_worker_thread: daemon reply: " + "result='%s' status='%s' message='%s'\n", + result_str ? result_str : "(null)", + status_str ? status_str : "(null)", + message_str ? message_str : "(null)"); + + /* Check if daemon accepted or rejected */ + if (result_str && strcmp(result_str, "RDKFW_UPDATE_SUCCESS") == 0) { + ctx->daemon_accepted = true; + FWUPMGR_INFO("internal_update_worker_thread: daemon ACCEPTED update\n"); } else { - /* Status is not FIRMWARE_AVAILABLE or no update_details string */ - fwinfo_data.UpdateDetails = NULL; + ctx->daemon_accepted = false; + ctx->daemon_reject_message = (message_str && message_str[0]) + ? strdup(message_str) : NULL; + FWUPMGR_WARN("internal_update_worker_thread: daemon REJECTED update: %s\n", + message_str ? message_str : "(no message)"); } - /* Invoke all callbacks with the same FwInfoData */ - 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); - - /* - * Invoke callback with proper signature: - * UpdateEventCallback(const FwInfoData *fwinfodata) - * - * handle_copy is passed but callback signature doesn't use it anymore. - * We pass it to maintain compatibility with 2-param callbacks if needed. - */ - s->callback(&fwinfo_data); - - /* Reset slot to IDLE */ - pthread_mutex_lock(&g_registry.mutex); - registry_reset_slot(&g_registry.entries[s->slot_index]); - pthread_mutex_unlock(&g_registry.mutex); - } -} + g_variant_unref(reply); -/* ======================================================================== - * REGISTRY OPERATIONS - * ======================================================================== */ + /* If daemon rejected, signal caller with failure and exit */ + if (!ctx->daemon_accepted) { + pthread_mutex_lock(&ctx->ready_mutex); + ctx->caller_owns_cleanup = true; + ctx->init_failed = true; + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); -/** - * @brief Register a pending callback keyed by handle (no user_data) - * - * SAME HANDLE TWICE: - * If the same handle is still PENDING from a previous call, its slot - * is overwritten. Prevents ghost callbacks accumulating. - * - * @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 - */ -bool internal_register_callback(FirmwareInterfaceHandle handle, - UpdateEventCallback callback) -{ - pthread_mutex_lock(&g_registry.mutex); + goto cleanup; + } - CallbackEntry *free_slot = NULL; - CallbackEntry *existing_slot = NULL; + /* ---- Step 5: Add timeout source (3600s) ---- */ + ctx->timeout_source = g_timeout_source_new_seconds(UPDATE_SIGNAL_TIMEOUT_SECONDS); + g_source_set_callback(ctx->timeout_source, on_update_timeout, ctx, NULL); + g_source_attach(ctx->timeout_source, ctx->context); + g_source_unref(ctx->timeout_source); /* context holds ref now */ + ctx->timeout_source = NULL; /* don't double-unref in cleanup */ - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - CallbackEntry *e = &g_registry.entries[i]; + /* ---- Step 6: Signal caller "ready" with success ---- */ + pthread_mutex_lock(&ctx->ready_mutex); + ctx->is_ready = true; + pthread_cond_signal(&ctx->ready_cond); + pthread_mutex_unlock(&ctx->ready_mutex); - /* Existing pending entry for same handle → overwrite it */ - if (e->state == CB_STATE_PENDING && - e->handle_key != NULL && - strcmp(e->handle_key, handle) == 0) { - existing_slot = e; - break; - } + FWUPMGR_INFO("internal_update_worker_thread: signaled ready, " + "entering event loop\n"); - if (free_slot == NULL && e->state == CB_STATE_IDLE) { - free_slot = e; - } - } + /* ---- Step 7: Run event loop — wait for UpdateProgress signals ---- */ + g_main_loop_run(ctx->main_loop); - CallbackEntry *target = existing_slot ? existing_slot : free_slot; + FWUPMGR_INFO("internal_update_worker_thread: event loop exited\n"); - if (target == NULL) { - FWUPMGR_ERROR("internal_register_callback: registry full (max=%d)\n", - MAX_PENDING_CALLBACKS); - pthread_mutex_unlock(&g_registry.mutex); - return false; - } +cleanup: + FWUPMGR_INFO("internal_update_worker_thread: cleaning up\n"); - if (existing_slot) { - FWUPMGR_INFO("internal_register_callback: overwriting existing for handle='%s'\n", - handle); - free(target->handle_key); - target->handle_key = NULL; + /* Unsubscribe from signal */ + if (ctx->connection && ctx->subscription_id > 0) { + g_dbus_connection_signal_unsubscribe(ctx->connection, ctx->subscription_id); } - target->handle_key = strdup(handle); - target->callback = callback; - target->state = CB_STATE_PENDING; - target->registered_time = time(NULL); + /* Quit and unref main loop */ + if (ctx->main_loop) { + if (g_main_loop_is_running(ctx->main_loop)) { + g_main_loop_quit(ctx->main_loop); + } + g_main_loop_unref(ctx->main_loop); + ctx->main_loop = NULL; + } - pthread_mutex_unlock(&g_registry.mutex); + /* Pop and unref context */ + if (ctx->context) { + g_main_context_pop_thread_default(ctx->context); + g_main_context_unref(ctx->context); + ctx->context = NULL; + } - FWUPMGR_INFO("internal_register_callback: registered handle='%s'\n", handle); - return true; -} + /* Release D-Bus connection */ + if (ctx->connection) { + g_object_unref(ctx->connection); + ctx->connection = NULL; + } -/** - * @brief Reset a registry slot to IDLE - * MUST be called with registry mutex held. - */ -static void registry_reset_slot(CallbackEntry *entry) -{ - if (entry->handle_key != NULL) { - free(entry->handle_key); - entry->handle_key = NULL; + /* Clear in-progress flag BEFORE freeing ctx */ + internal_end_update(); + + /* Destroy synchronization primitives and free ctx — BUT only if the worker + * owns cleanup. On init-failure paths, the caller (API function) owns these + * because it may still be inside pthread_cond_timedwait or about to call + * pthread_mutex_unlock on the same mutex. The caller will destroy/free + * after pthread_join returns. */ + if (!ctx->caller_owns_cleanup) { + pthread_mutex_destroy(&ctx->ready_mutex); + pthread_cond_destroy(&ctx->ready_cond); + + /* Free strdup'd strings */ + free(ctx->handle_key); + free(ctx->firmware_name); + free(ctx->firmware_location); + free(ctx->firmware_type); + free(ctx->reboot_flag); + free(ctx->daemon_reject_message); + + /* Free context */ + free(ctx); + } else { + /* Worker does NOT own the sync primitives or ctx on init-failure. + * The caller will free everything after pthread_join. + * We still need to free any worker-allocated resources (like + * daemon_reject_message which was strdup'd in the worker). */ + free(ctx->daemon_reject_message); + ctx->daemon_reject_message = NULL; + FWUPMGR_INFO("internal_update_worker_thread: caller_owns_cleanup=true, " + "skipping mutex/cond destroy and free(ctx)\n"); } - entry->callback = NULL; - entry->registered_time = 0; - entry->state = CB_STATE_IDLE; + + FWUPMGR_INFO("internal_update_worker_thread: thread exiting\n"); + return NULL; } /* ======================================================================== @@ -645,285 +1571,19 @@ CheckForUpdateStatus internal_map_status_code(int32_t status_code) } -/* ======================================================================== - * DOWNLOAD FIRMWARE — INTERNAL ENGINE - * ======================================================================== - * - * Everything below is the DownloadFirmware equivalent of the - * CheckForUpdate engine above. Same patterns, different registry and signal. - * - * KEY DIFFERENCE: - * CheckForUpdate slot fires ONCE then goes IDLE. - * Download slot stays ACTIVE and fires on EVERY DownloadProgress signal - * until the daemon sends DWNL_COMPLETED or DWNL_ERROR. - * ======================================================================== */ - -/* ---- Forward declarations for helper functions ---- */ -static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data); -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() - */ -static void internal_dwnl_system_deinit(void) -{ - pthread_mutex_lock(&g_dwnl_registry.mutex); - 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); - g_dwnl_registry.entries[i].handle_key = NULL; - } - } - pthread_mutex_unlock(&g_dwnl_registry.mutex); - pthread_mutex_destroy(&g_dwnl_registry.mutex); - - FWUPMGR_INFO("internal_dwnl_system_deinit: done\n"); -} - -/* ======================================================================== - * DOWNLOAD SIGNAL HANDLER - * ======================================================================== */ - -/** - * @brief Called by GLib when DownloadProgress signal arrives - * - * Runs in the background thread — same thread as on_check_complete_signal(). - * - * 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 - */ -static void on_download_progress_signal(GDBusConnection *conn, - const gchar *sender, - const gchar *object_path, - const gchar *interface_name, - const gchar *signal_name, - GVariant *parameters, - gpointer user_data) -{ - (void)conn; (void)sender; (void)object_path; - (void)interface_name; (void)signal_name; (void)user_data; - - FWUPMGR_INFO("on_download_progress_signal: received\n"); - - InternalDwnlSignalData signal_data; - memset(&signal_data, 0, sizeof(signal_data)); - - if (!internal_parse_dwnl_signal_data(parameters, &signal_data)) { - FWUPMGR_ERROR("on_download_progress_signal: parse failed\n"); - return; - } - - FWUPMGR_INFO("on_download_progress_signal: handler=%" PRIu64 " firmware='%s' progress=%u%% status='%s'\n", - signal_data.handler_id, - signal_data.firmware_name ? signal_data.firmware_name : "(null)", - signal_data.progress_percent, - signal_data.status_string ? signal_data.status_string : "(null)"); - - dispatch_all_dwnl_active(&signal_data); - - // Free allocated strings from g_variant_get - 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. - * - * 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. - */ -static void dispatch_all_dwnl_active(const InternalDwnlSignalData *signal_data) -{ - typedef struct { - DownloadCallback callback; - char handle_copy[256]; - int slot_index; - bool is_final; /* true if COMPLETED or ERROR — remove after firing */ - } DwnlSnapshot; - - DwnlSnapshot snapshots[MAX_PENDING_CALLBACKS]; - int count = 0; - - DownloadStatus status = map_dwnl_status_string(signal_data->status_string); - bool is_final = (status == DWNL_COMPLETED || status == DWNL_ERROR); - - /* ---- PHASE 1: snapshot under mutex ---- */ - pthread_mutex_lock(&g_dwnl_registry.mutex); - - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - DwnlCallbackEntry *e = &g_dwnl_registry.entries[i]; - if (e->state != DWNL_CB_STATE_ACTIVE) continue; - - snapshots[count].callback = e->callback; - snapshots[count].slot_index = i; - snapshots[count].is_final = is_final; - snprintf(snapshots[count].handle_copy, - sizeof(snapshots[count].handle_copy), - "%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. - */ - count++; - - FWUPMGR_INFO("dispatch_all_dwnl_active: queued handle='%s' progress=%d%% final=%d\n", - e->handle_key ? e->handle_key : "(null)", - signal_data->progress_percent, is_final); - } - - 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); - - /* - * Callback signature: void fn(int progress_per, DownloadStatus status) - * No handle parameter — matches the DownloadCallback typedef exactly. - */ - 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 (s->is_final) { - pthread_mutex_lock(&g_dwnl_registry.mutex); - dwnl_registry_reset_slot(&g_dwnl_registry.entries[s->slot_index]); - pthread_mutex_unlock(&g_dwnl_registry.mutex); - - FWUPMGR_INFO("dispatch_all_dwnl_active: slot %d reset to IDLE (download ended)\n", - s->slot_index); - } - } -} - -/* ======================================================================== - * DOWNLOAD REGISTRY OPERATIONS - * ======================================================================== */ - -/** - * @brief Register a download callback keyed by handle - * - * Sets slot state to ACTIVE. Slot will receive ALL subsequent - * DownloadProgress signals until DWNL_COMPLETED or DWNL_ERROR. - * - * SAME HANDLE TWICE: - * Overwrites existing ACTIVE slot for the same handle. - * Prevents stale callbacks from a previous download session. - * - * @param handle App's FirmwareInterfaceHandle (strdup'd internally) - * @param callback App's DownloadCallback - * @return true on success, false if registry full - */ -bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, - DownloadCallback callback) -{ - pthread_mutex_lock(&g_dwnl_registry.mutex); - - DwnlCallbackEntry *free_slot = NULL; - DwnlCallbackEntry *existing_slot = NULL; - - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - DwnlCallbackEntry *e = &g_dwnl_registry.entries[i]; - - if (e->state == DWNL_CB_STATE_ACTIVE && - e->handle_key != NULL && - strcmp(e->handle_key, handle) == 0) { - existing_slot = e; - break; - } - - if (free_slot == NULL && e->state == DWNL_CB_STATE_IDLE) { - free_slot = e; - } - } - - DwnlCallbackEntry *target = existing_slot ? existing_slot : free_slot; - - if (target == NULL) { - FWUPMGR_ERROR("internal_dwnl_register_callback: registry full (max=%d)\n", - MAX_PENDING_CALLBACKS); - pthread_mutex_unlock(&g_dwnl_registry.mutex); - return false; - } - - if (existing_slot) { - FWUPMGR_INFO("internal_dwnl_register_callback: overwriting existing for handle='%s'\n", - handle); - free(target->handle_key); - target->handle_key = NULL; - } - - target->handle_key = strdup(handle); - target->callback = callback; - target->state = DWNL_CB_STATE_ACTIVE; - target->registered_time = time(NULL); - - pthread_mutex_unlock(&g_dwnl_registry.mutex); - - FWUPMGR_INFO("internal_dwnl_register_callback: registered handle='%s'\n", handle); - return true; -} - -/** - * @brief Reset a download registry slot to IDLE - * MUST be called with g_dwnl_registry.mutex held. - */ -static void dwnl_registry_reset_slot(DwnlCallbackEntry *entry) -{ - if (entry->handle_key != NULL) { - free(entry->handle_key); - entry->handle_key = NULL; - } - entry->callback = NULL; - entry->registered_time = 0; - entry->state = DWNL_CB_STATE_IDLE; -} - /* ======================================================================== * DOWNLOAD SIGNAL DATA HELPERS * ======================================================================== */ /** - * @brief Parse GVariant DownloadProgress signal payload + * @brief Parse GVariant DownloadProgress payload * - * Expected GVariant signature: (ii) - * i progress_percent (0–100) - * i status_code (maps to DownloadStatus) + * Expected GVariant signature: (tsuss) + * t handlerId (uint64) + * s firmwareName (string) + * u progressPercent (uint32) + * s status (string - "INPROGRESS", "COMPLETED", "ERROR") + * s message (string) */ bool internal_parse_dwnl_signal_data(GVariant *parameters, InternalDwnlSignalData *out_data) @@ -932,7 +1592,8 @@ bool internal_parse_dwnl_signal_data(GVariant *parameters, 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); + FWUPMGR_ERROR("internal_parse_dwnl_signal_data: " + "unexpected signature '%s' (expected '(tsuss)')\n", sig); return false; } @@ -942,40 +1603,22 @@ bool internal_parse_dwnl_signal_data(GVariant *parameters, gchar *status_str = NULL; gchar *message_str = NULL; - g_variant_get(parameters, "(tsuss)", - &handler_id, - &firmware_name, - &progress, - &status_str, + g_variant_get(parameters, "(tsuss)", + &handler_id, + &firmware_name, + &progress, + &status_str, &message_str); - out_data->handler_id = handler_id; - out_data->firmware_name = firmware_name; // Caller must g_free + out_data->handler_id = handler_id; + 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 - */ -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", - status_code); - return DWNL_ERROR; - } -} - /** * @brief Map status string from daemon to DownloadStatus enum */ @@ -993,257 +1636,10 @@ 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); + FWUPMGR_ERROR("map_dwnl_status_string: unknown status '%s'\n", status_str); return DWNL_ERROR; } -/* ======================================================================== - * UPDATE FIRMWARE — INTERNAL ENGINE - * ======================================================================== - * - * Mirror of the DownloadFirmware engine above. - * Same registry pattern, same two-phase dispatch, same lifecycle. - * - * Signal: UpdateProgress (ii) — progress_percent, status_code - * Registry slot: ACTIVE until UPDATE_COMPLETED or UPDATE_ERROR, then IDLE. - * ======================================================================== */ - -/* ---- Forward declarations for helper functions ---- */ -static void dispatch_all_update_active(const InternalUpdateSignalData *signal_data); -static void update_registry_reset_slot(UpdateCbEntry *entry); - -/* ======================================================================== - * UPDATE SUBSYSTEM LIFECYCLE - * ======================================================================== */ - -/** - * @brief Cleanup update registry — frees all strdup'd handle_key strings - * - * Called from internal_system_deinit(). Signal unsubscription is handled - * by the background thread. - */ -static void internal_update_system_deinit(void) -{ - pthread_mutex_lock(&g_update_registry.mutex); - 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; - } - } - pthread_mutex_unlock(&g_update_registry.mutex); - pthread_mutex_destroy(&g_update_registry.mutex); - - FWUPMGR_INFO("internal_update_system_deinit: done\n"); -} - -/* ======================================================================== - * UPDATE SIGNAL HANDLER - * ======================================================================== */ - -/** - * @brief Called by GLib when UpdateProgress signal arrives - * - * Runs in background thread. Parses payload and dispatches to all - * ACTIVE update callbacks. - */ -static void on_update_progress_signal(GDBusConnection *conn, - const gchar *sender, - const gchar *object_path, - const gchar *interface_name, - const gchar *signal_name, - GVariant *parameters, - gpointer user_data) -{ - (void)conn; (void)sender; (void)object_path; - (void)interface_name; (void)signal_name; (void)user_data; - - FWUPMGR_INFO("on_update_progress_signal: received\n"); - - InternalUpdateSignalData signal_data; - memset(&signal_data, 0, sizeof(signal_data)); - - if (!internal_parse_update_signal_data(parameters, &signal_data)) { - FWUPMGR_ERROR("on_update_progress_signal: parse failed\n"); - return; - } - - 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_all_update_active(&signal_data); - - // Free allocated strings from g_variant_get - g_free(signal_data.firmware_name); - g_free(signal_data.message); -} - -/** - * @brief Dispatch UpdateProgress signal to every ACTIVE update callback - * - * TWO-PHASE DESIGN (identical to download dispatch): - * - * PHASE 1 (mutex held): - * Snapshot all ACTIVE entries. - * Mark is_final=true only if status is COMPLETED or ERROR. - * Release mutex. - * - * 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. - */ -static void dispatch_all_update_active(const InternalUpdateSignalData *signal_data) -{ - typedef struct { - UpdateCallback callback; - char handle_copy[256]; - int slot_index; - bool is_final; - } UpdateSnapshot; - - UpdateSnapshot snapshots[MAX_PENDING_CALLBACKS]; - int count = 0; - - 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 ---- */ - pthread_mutex_lock(&g_update_registry.mutex); - - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - UpdateCbEntry *e = &g_update_registry.entries[i]; - if (e->state != UPDATE_CB_STATE_ACTIVE) continue; - - snapshots[count].callback = e->callback; - snapshots[count].slot_index = i; - snapshots[count].is_final = is_final; - snprintf(snapshots[count].handle_copy, - sizeof(snapshots[count].handle_copy), - "%s", e->handle_key ? e->handle_key : ""); - - count++; - - FWUPMGR_INFO("dispatch_all_update_active: queued handle='%s' " - "progress=%d%% final=%d\n", - e->handle_key ? e->handle_key : "(null)", - signal_data->progress_percent, is_final); - } - - 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 ---- */ - 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); - - /* - * Callback signature: void fn(int progress_per, UpdateStatus status) - * Matches UpdateCallback typedef exactly. - */ - 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 (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 " - "(update ended)\n", s->slot_index); - } - } -} - -/* ======================================================================== - * UPDATE REGISTRY OPERATIONS - * ======================================================================== */ - -/** - * @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. - * - * SAME HANDLE TWICE: - * Overwrites existing ACTIVE slot for the same handle. - */ -bool internal_update_register_callback(FirmwareInterfaceHandle handle, - UpdateCallback callback) -{ - pthread_mutex_lock(&g_update_registry.mutex); - - UpdateCbEntry *free_slot = NULL; - UpdateCbEntry *existing_slot = NULL; - - for (int i = 0; i < MAX_PENDING_CALLBACKS; i++) { - UpdateCbEntry *e = &g_update_registry.entries[i]; - - if (e->state == UPDATE_CB_STATE_ACTIVE && - e->handle_key != NULL && - strcmp(e->handle_key, handle) == 0) { - existing_slot = e; - break; - } - - if (free_slot == NULL && e->state == UPDATE_CB_STATE_IDLE) { - free_slot = e; - } - } - - UpdateCbEntry *target = existing_slot ? existing_slot : free_slot; - - if (target == NULL) { - FWUPMGR_ERROR("internal_update_register_callback: registry full (max=%d)\n", - MAX_PENDING_CALLBACKS); - pthread_mutex_unlock(&g_update_registry.mutex); - return false; - } - - 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 = strdup(handle); - target->callback = callback; - target->state = UPDATE_CB_STATE_ACTIVE; - target->registered_time = time(NULL); - - pthread_mutex_unlock(&g_update_registry.mutex); - - FWUPMGR_INFO("internal_update_register_callback: registered handle='%s'\n", - handle); - return true; -} - -/** - * @brief Reset an update registry slot to IDLE - * MUST be called with g_update_registry.mutex held. - */ -static void update_registry_reset_slot(UpdateCbEntry *entry) -{ - if (entry->handle_key != NULL) { - free(entry->handle_key); - entry->handle_key = NULL; - } - entry->callback = NULL; - entry->registered_time = 0; - entry->state = UPDATE_CB_STATE_IDLE; -} - /* ======================================================================== * UPDATE SIGNAL DATA HELPERS * ======================================================================== */ @@ -1251,9 +1647,12 @@ static void update_registry_reset_slot(UpdateCbEntry *entry) /** * @brief Parse GVariant UpdateProgress payload * - * Expected GVariant signature: (ii) - * i progress_percent (0–100) - * i status_code (maps to UpdateStatus) + * Expected GVariant signature: (tsiis) + * t handlerId (uint64) + * s firmwareName (string) + * i progressPercent (int32) + * i statusCode (int32) + * s message (string) */ bool internal_parse_update_signal_data(GVariant *parameters, InternalUpdateSignalData *out_data) @@ -1312,12 +1711,12 @@ UpdateStatus internal_map_update_status_code(int32_t status_code) /** * @brief Parse update_details string into UpdateDetails structure * - * The update_details string from the daemon is a comma-separated key:value format: - * "FwFileName:filename.bin,FwUrl:https://...,FwVersion:1.0,..." + * The update_details string from the daemon is a pipe-separated key:value format: + * "File:filename.bin|Location:https://...|Version:1.0|..." * * This function safely parses it and populates the UpdateDetails structure. * - * @param update_details_str Comma-separated string from daemon (may be NULL) + * @param update_details_str Pipe-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 @@ -1429,8 +1828,4 @@ static bool parse_update_details(const char *update_details_str, return true; } -/* ======================================================================== - * D-BUS SIGNAL HANDLERS - * ======================================================================== */ - diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h b/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h index f12de154..93be4e13 100644 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h @@ -14,32 +14,78 @@ * @file rdkFwupdateMgr_async_internal.h * @brief Internal types and declarations — NOT part of public API * - * ARCHITECTURE OVERVIEW: - * ====================== - * - * App A ──checkForUpdate(hdl_A, cb_A)──┐ - * App B ──checkForUpdate(hdl_B, cb_B)──┼──► Registry (keyed by handle) - * App C ──checkForUpdate(hdl_C, cb_C)──┘ │ - * │ background thread - * │ watches D-Bus - * ▼ - * Daemon emits CheckForUpdateComplete signal (ONCE) - * │ - * on_check_complete_signal() - * │ - * dispatch_all_pending() │ - * ├── cb_A(hdl_A, &event_data) - * ├── cb_B(hdl_B, &event_data) - * └── cb_C(hdl_C, &event_data) - * - * REGISTRY KEY: - * ============= - * Each entry keyed by FirmwareInterfaceHandle (string from registerProcess). - * One handle → one pending callback at a time. + * ARCHITECTURE OVERVIEW (Phase 1+2+3 — All APIs use on-demand worker threads): + * ============================================================================== + * + * CheckForUpdate (ON-DEMAND WORKER THREAD — Phase 1): + * + * App calls checkForUpdate(handle, callback) + * │ + * ├─ Allocate CheckRequestContext on heap + * ├─ pthread_create(internal_check_worker_thread, ctx) + * │ │ + * │ ├─ New GMainContext + GMainLoop (isolated) + * │ ├─ g_bus_get_sync() → D-Bus connection + * │ ├─ Subscribe to CheckForUpdateComplete signal + * │ ├─ Send CheckForUpdate D-Bus method call + * │ ├─ Signal caller "ready" via condvar + * │ ├─ g_main_loop_run() — waits for signal or 120s timeout + * │ ├─ Signal arrives → parse → callback(&fwinfo_data) + * │ └─ Cleanup everything, free(ctx), thread exits + * │ + * ├─ pthread_cond_wait() for ready signal + * └─ Return SUCCESS or FAIL + * + * DownloadFirmware (ON-DEMAND WORKER THREAD — Phase 2): + * + * App calls downloadFirmware(handle, request, callback) + * │ + * ├─ Allocate DownloadRequestContext on heap + * ├─ pthread_create(internal_download_worker_thread, ctx) + * │ │ + * │ ├─ New GMainContext + GMainLoop (isolated) + * │ ├─ g_bus_get_sync() → D-Bus connection + * │ ├─ Subscribe to DownloadProgress signal + * │ ├─ g_dbus_connection_call_sync("DownloadFirmware") — SYNCHRONOUS + * │ │ → reads daemon's (sss) reply: accept or reject + * │ ├─ Signal caller "ready" via condvar + * │ ├─ g_main_loop_run() — waits for DownloadProgress signals + * │ │ → callback fires MULTIPLE times (per progress signal) + * │ │ → quits loop ONLY on COMPLETED/ERROR (terminal status) + * │ └─ Cleanup everything, free(ctx), thread exits + * │ + * ├─ pthread_cond_wait() for ready signal (includes daemon reply) + * └─ Return SUCCESS or FAIL (accurate — reflects daemon's accept/reject) + * + * UpdateFirmware (ON-DEMAND WORKER THREAD — Phase 3): + * + * App calls updateFirmware(handle, request, callback) + * │ + * ├─ Allocate UpdateRequestContext on heap + * ├─ pthread_create(internal_update_worker_thread, ctx) + * │ │ + * │ ├─ New GMainContext + GMainLoop (isolated) + * │ ├─ g_bus_get_sync() → D-Bus connection + * │ ├─ Subscribe to UpdateProgress signal + * │ ├─ g_dbus_connection_call_sync("UpdateFirmware") — SYNCHRONOUS + * │ │ → reads daemon's (sss) reply: accept or reject + * │ ├─ Signal caller "ready" via condvar + * │ ├─ g_main_loop_run() — waits for UpdateProgress signals + * │ │ → callback fires MULTIPLE times (per progress signal) + * │ │ → quits loop ONLY on COMPLETED/ERROR (terminal status) + * │ └─ Cleanup everything, free(ctx), thread exits + * │ + * ├─ pthread_cond_wait() for ready signal (includes daemon reply) + * └─ Return SUCCESS or FAIL (accurate — reflects daemon's accept/reject) * * THREAD SAFETY: * ============== - * Registry protected by pthread_mutex. + * CheckForUpdate: per-request ctx protected by ctx->ready_mutex (handshake), + * g_check_in_progress protected by g_check_in_progress_mutex. + * DownloadFirmware: per-request ctx protected by ctx->ready_mutex (handshake), + * g_dwnl_in_progress protected by g_dwnl_in_progress_mutex. + * UpdateFirmware: per-request ctx protected by ctx->ready_mutex (handshake), + * g_update_in_progress protected by g_update_in_progress_mutex. * Callbacks invoked with mutex RELEASED (deadlock prevention). */ @@ -61,9 +107,6 @@ extern "C" { * CONSTANTS * ======================================================================== */ -#define MAX_PENDING_CALLBACKS 30 /* Reduced from 64 to keep stack usage < 10KB - Need to discuss the max number ; for now kept to 30 to resolve coverity issues*/ -#define CALLBACK_TIMEOUT_SECONDS 60 - #define DBUS_SERVICE_NAME "org.rdkfwupdater.Service" #define DBUS_OBJECT_PATH "/org/rdkfwupdater/Service" #define DBUS_INTERFACE_NAME "org.rdkfwupdater.Interface" @@ -71,22 +114,61 @@ extern "C" { #define DBUS_SIGNAL_COMPLETE "CheckForUpdateComplete" #define DBUS_TIMEOUT_MS 5000 +/* Timeout for worker thread waiting for daemon signal (seconds) */ +#define CHECK_SIGNAL_TIMEOUT_SECONDS 120 + /* ======================================================================== - * CALLBACK ENTRY STATE + * CHECKFORUPDATE — ON-DEMAND WORKER THREAD CONTEXT (Phase 1) * ======================================================================== */ /** - * @brief Lifecycle of one registry slot + * @brief Per-request context for on-demand CheckForUpdate worker thread. * - * IDLE ──(register)──► PENDING ──(signal)──► DISPATCHED ──► IDLE - * └──(timeout)──► TIMED_OUT ──► IDLE + * Lifecycle: + * - Allocated by checkForUpdate() (caller thread) via calloc + * - Ownership transferred to worker thread after condvar handshake + * - Freed by worker thread after callback fires (or timeout/error) + * + * Memory: ~100 bytes (excluding GLib objects) */ -typedef enum { - CB_STATE_IDLE = 0, - CB_STATE_PENDING = 1, - CB_STATE_DISPATCHED = 2, - CB_STATE_TIMED_OUT = 3 -} CallbackEntryState; +typedef struct { + /* Condvar handshake: worker signals "I'm ready" to caller */ + pthread_mutex_t ready_mutex; + pthread_cond_t ready_cond; + bool is_ready; /**< true = worker finished setup */ + bool init_failed; /**< true = D-Bus connect/subscribe failed */ + + /** + * Handshake lifetime ownership flag. + * + * When true, the CALLER (API function) owns cleanup of the sync primitives + * (ready_mutex, ready_cond) and the ctx allocation itself. The worker thread + * must NOT destroy the mutex/cond or free(ctx) — it only cleans up GLib + * resources and strdup'd strings. + * + * Set to true by the worker BEFORE signaling ready on init-failure paths. + * This prevents the race where the worker destroys the mutex/cond/ctx while + * the caller is still inside pthread_cond_timedwait or about to call + * pthread_mutex_unlock. + * + * On success paths, this remains false — the worker owns everything after + * the handshake completes and the caller never touches ctx again. + */ + bool caller_owns_cleanup; + + /* GLib event loop (isolated, per-thread) */ + GMainContext *context; + GMainLoop *main_loop; + GDBusConnection *connection; + guint subscription_id; + + /* Request data */ + char *handle_key; /**< strdup of FirmwareInterfaceHandle */ + UpdateEventCallback callback; /**< Client's callback function ptr */ + + /* Thread handle (for join in destructor) */ + pthread_t thread; +} CheckRequestContext; /* ======================================================================== * INTERNAL SIGNAL DATA @@ -108,91 +190,78 @@ typedef struct { } InternalSignalData; /* ======================================================================== - * CALLBACK REGISTRY ENTRY + * INTERNAL FUNCTION DECLARATIONS — CheckForUpdate * ======================================================================== */ /** - * @brief One slot in the callback registry + * @brief Worker thread entry point for on-demand CheckForUpdate. * - * Keyed by handle_key (strdup of app's FirmwareInterfaceHandle). - * No user_data — aligned to 2-param callback signature. + * Creates an isolated GLib event loop, connects to D-Bus, subscribes to + * CheckForUpdateComplete signal, sends CheckForUpdate D-Bus method call, + * then waits for the signal (with 120s timeout). Fires the client's + * callback when signal arrives, then cleans up all resources and exits. * - * MEMORY: - * handle_key is strdup'd on registration, freed on slot reset to IDLE. + * @param arg CheckRequestContext* (ownership transferred from caller) + * @return NULL */ -typedef struct { - CallbackEntryState state; /**< Current lifecycle state */ - char *handle_key; /**< strdup of app's handle */ - UpdateEventCallback callback; /**< App's 2-param callback */ - time_t registered_time; /**< For timeout detection */ -} CallbackEntry; - -/* ======================================================================== - * CALLBACK REGISTRY - * ======================================================================== */ +void *internal_check_worker_thread(void *arg); /** - * @brief Global registry — one instance per library load + * @brief Query whether a checkForUpdate() operation is currently in progress. + * + * Used by unregisterProcess() to enforce the session-state invariant: + * a client cannot unregister while it has outstanding operations. + * + * Thread-safe: protected by internal mutex. + * + * @return true if a checkForUpdate worker thread is active, false otherwise. */ -typedef struct { - CallbackEntry entries[MAX_PENDING_CALLBACKS]; - pthread_mutex_t mutex; - bool initialized; -} CallbackRegistry; - -/* ======================================================================== - * BACKGROUND THREAD - * ======================================================================== */ +bool internal_is_check_in_progress(void); /** - * @brief State for the background GLib event loop thread + * @brief Atomically begin a checkForUpdate session and track the context. * - * Started at library load. Subscribes to CheckForUpdateComplete signal. - * Runs until library unload. + * Sets g_check_in_progress = true and stores ctx in g_active_check_ctx. + * If a check is already in progress, returns false without modifying state. + * + * TL;DR: Replaces direct extern access to g_check_in_progress + g_active_check_ctx. + * All mutex handling is internal — callers never touch the mutex. + * + * @param ctx The newly allocated CheckRequestContext to track. + * @return true if session started, false if another check is already active. */ -typedef struct { - pthread_t thread; - GMainLoop *main_loop; - GMainContext *context; - GDBusConnection *connection; - guint subscription_id; - bool running; - pthread_mutex_t mutex; -} BackgroundThread; - -/* ======================================================================== - * INTERNAL FUNCTION DECLARATIONS - * ======================================================================== */ +bool internal_begin_check(CheckRequestContext *ctx); /** - * @brief Initialize registry and start background thread - * Called from library __attribute__((constructor)). - * @return 0 on success, -1 on error + * @brief Atomically end the checkForUpdate session and untrack the context. + * + * Sets g_check_in_progress = false and g_active_check_ctx = NULL. + * Called by the worker thread in cleanup, BEFORE freeing ctx. */ -int internal_system_init(void); +void internal_end_check(void); /** - * @brief Stop background thread and free all resources - * Called from library __attribute__((destructor)). + * @brief Atomically clear in-progress state on error paths. + * + * Same as internal_end_check() but used when checkForUpdate() itself fails + * (e.g., pthread_create fails after internal_begin_check succeeded). + * The caller will free ctx directly. */ -void internal_system_deinit(void); +void internal_abort_check(void); /** - * @brief Register a pending callback keyed by handle - * - * No user_data — matches the 2-param UpdateEventCallback signature. + * @brief Cancel all active checkForUpdate worker threads and join them. * - * @param handle App's FirmwareInterfaceHandle (will be strdup'd) - * @param callback App's UpdateEventCallback (2-param) - * @return true on success, false if registry is full + * Called from library destructor to ensure no threads are running + * when library code is unmapped. */ -bool internal_register_callback(FirmwareInterfaceHandle handle, - UpdateEventCallback callback); +void internal_cancel_all_active_check_threads(void); /** * @brief Parse GVariant signal into InternalSignalData * - * Expected GVariant signature: (iissss) + * Expected GVariant signature: (tiissss) + * t handler_id (uint64) * i result_code * i status_code * s current_version @@ -218,52 +287,46 @@ void internal_cleanup_signal_data(InternalSignalData *data); CheckForUpdateStatus internal_map_status_code(int32_t status_code); /* ======================================================================== - * DOWNLOAD FIRMWARE — INTERNAL TYPES AND DECLARATIONS + * DOWNLOAD FIRMWARE — ON-DEMAND WORKER THREAD (Phase 2) * ======================================================================== * * ARCHITECTURE: * - * App A ──downloadFirmware(hdl_A, req_A, cb_A)──┐ - * App B ──downloadFirmware(hdl_B, req_B, cb_B)──┼──► DwnlRegistry (keyed by handle) - * App C ──downloadFirmware(hdl_C, req_C, cb_C)──┘ │ - * │ same background thread - * │ now also subscribed to - * │ DownloadProgress signal - * ▼ - * Daemon emits DownloadProgress(progress%, status) REPEATEDLY - * │ - * on_download_progress_signal() - * │ - * dispatch_all_dwnl_pending() │ - * ├── cb_A(progress%, status) - * ├── cb_B(progress%, status) - * └── cb_C(progress%, status) + * App calls downloadFirmware(handle, request, callback) + * │ + * ├─ Allocate DownloadRequestContext on heap + * ├─ internal_begin_download(ctx) — reject if already active + * ├─ pthread_create(internal_download_worker_thread, ctx) + * │ │ + * │ ├─ New GMainContext + GMainLoop (isolated) + * │ ├─ g_bus_get_sync() → D-Bus connection + * │ ├─ Subscribe to DownloadProgress signal + * │ ├─ g_dbus_connection_call_sync("DownloadFirmware") + * │ │ → daemon reply (sss): result, status, message + * │ │ → if FAILED: set init_failed, signal ready, cleanup + * │ │ → if SUCCESS: set daemon_accepted + * │ ├─ Add 3600s timeout + * │ ├─ Signal caller "ready" via condvar + * │ ├─ g_main_loop_run() — receives DownloadProgress signals + * │ │ → callback fires MULTIPLE times (per-signal) + * │ │ → quits loop ONLY on COMPLETED/ERROR + * │ └─ Cleanup everything, free(ctx), thread exits + * │ + * ├─ pthread_cond_wait() for ready signal + * └─ Return SUCCESS or FAIL (accurate — reflects daemon reply) * * KEY DIFFERENCE FROM CheckForUpdate: - * CheckForUpdate registry: slot goes PENDING → DISPATCHED → IDLE (fires ONCE) - * Download registry: slot stays ACTIVE until DWNL_COMPLETED or DWNL_ERROR - * (fires MULTIPLE TIMES — once per progress signal) + * CheckForUpdate: callback fires ONCE, then thread exits. + * DownloadFirmware: callback fires MULTIPLE TIMES (per progress signal), + * thread exits only on terminal status (COMPLETED/ERROR). * * ======================================================================== */ -#define DBUS_METHOD_DOWNLOAD "DownloadFirmware" -#define DBUS_SIGNAL_DWNL_PROGRESS "DownloadProgress" +#define DBUS_METHOD_DOWNLOAD "DownloadFirmware" +#define DBUS_SIGNAL_DWNL_PROGRESS "DownloadProgress" -/** - * @brief Lifecycle state of one download callback registry slot - * - * IDLE ──(register)──► ACTIVE ──(COMPLETED/ERROR signal)──► IDLE - * │ - * │ (fires callback on EVERY DownloadProgress signal - * │ while in ACTIVE state) - * │ - * └──(timeout)──► TIMED_OUT ──► IDLE - */ -typedef enum { - DWNL_CB_STATE_IDLE = 0, /**< Slot free and reusable */ - DWNL_CB_STATE_ACTIVE = 1, /**< Receiving progress signals */ - DWNL_CB_STATE_TIMED_OUT = 2 /**< Timed out waiting for completion */ -} DwnlCallbackState; +/* Timeout for download worker thread (seconds) — 1 hour */ +#define DWNL_SIGNAL_TIMEOUT_SECONDS 3600 /** * @brief Parsed payload from DownloadProgress D-Bus signal @@ -273,7 +336,7 @@ typedef enum { * t handlerId (uint64 - handler ID) * s firmwareName (string - firmware filename) * u progress (uint32 - 0-100 percent) - * s status (string - "INPROGRESS", "COMPLETED", "NOTSTARTED") + * s status (string - "INPROGRESS", "COMPLETED", "ERROR") * s message (string - human-readable message) */ typedef struct { @@ -285,53 +348,132 @@ typedef struct { } InternalDwnlSignalData; /** - * @brief One slot in the download callback registry + * @brief Per-request context for on-demand DownloadFirmware worker thread. + * + * Lifecycle: + * - Allocated in downloadFirmware() (caller thread) via calloc + * - Ownership transferred to worker thread after condvar handshake + * - Freed by worker thread after download completes/fails (or timeout) * - * Keyed by handle_key (strdup of FirmwareInterfaceHandle). - * Stays ACTIVE across multiple DownloadProgress signal deliveries. - * Reset to IDLE only when DWNL_COMPLETED or DWNL_ERROR is received. + * Key differences from CheckRequestContext: + * - callback fires MULTIPLE times (per-progress-signal), not just once + * - worker quits loop ONLY on terminal status (COMPLETED/ERROR) + * - daemon_accepted: worker reads daemon's synchronous reply + * - longer timeout (3600s vs 120s) + * + * Memory: ~200 bytes (excluding GLib objects) */ typedef struct { - DwnlCallbackState state; /**< IDLE or ACTIVE */ - char *handle_key; /**< strdup of app's handle */ - DownloadCallback callback; /**< App's progress callback */ - time_t registered_time; /**< For timeout detection */ -} DwnlCallbackEntry; + /* Condvar handshake: worker signals "I'm ready" to caller */ + pthread_mutex_t ready_mutex; + pthread_cond_t ready_cond; + bool is_ready; /**< true = worker finished setup */ + bool init_failed; /**< true = D-Bus failed or daemon rejected */ + + /** + * Handshake lifetime ownership flag. + * + * When true, the CALLER (API function) owns cleanup of the sync primitives + * (ready_mutex, ready_cond) and the ctx allocation itself. The worker thread + * must NOT destroy the mutex/cond or free(ctx). + * + * Set to true by the worker BEFORE signaling ready on init-failure paths. + * This prevents the race where the worker destroys the mutex/cond/ctx while + * the caller is still inside pthread_cond_timedwait or pthread_mutex_unlock. + * + * On success paths, this remains false — the worker owns everything. + */ + bool caller_owns_cleanup; + + /* GLib event loop (isolated, per-thread) */ + GMainContext *context; + GMainLoop *main_loop; + GDBusConnection *connection; + guint subscription_id; + + /* Request data (all strdup'd — owned by worker thread) */ + char *handle_key; /**< strdup of FirmwareInterfaceHandle */ + char *firmware_name; /**< strdup of request->firmwareName */ + char *firmware_url; /**< strdup of request->downloadUrl */ + char *firmware_type; /**< strdup of request->TypeOfFirmware */ + DownloadCallback callback; /**< Client's callback function ptr */ + + /* Daemon reply (from synchronous D-Bus method return) */ + bool daemon_accepted; /**< true if daemon returned RDKFW_DWNL_SUCCESS */ + char *daemon_reject_message; /**< strdup of daemon's error message (if rejected) */ + + /* Timeout tracking */ + GSource *timeout_source; /**< For cancellation in cleanup */ + + /* Thread handle (for join in destructor) */ + pthread_t thread; +} DownloadRequestContext; + +/* ---- Download internal function declarations ---- */ /** - * @brief Global registry for all active download callbacks + * @brief Worker thread entry point for on-demand DownloadFirmware. * - * Separate from the CheckForUpdate registry — different lifecycle. - * Protected by its own mutex. + * Creates an isolated GLib event loop, connects to D-Bus, subscribes to + * DownloadProgress signal, sends DownloadFirmware D-Bus method call + * synchronously, then waits for progress signals (with 3600s timeout). + * Fires the client's callback on every progress signal, quits loop on + * COMPLETED or ERROR, then cleans up all resources and exits. + * + * @param arg DownloadRequestContext* (ownership transferred from caller) + * @return NULL */ -typedef struct { - DwnlCallbackEntry entries[MAX_PENDING_CALLBACKS]; - pthread_mutex_t mutex; - bool initialized; -} DwnlCallbackRegistry; +void *internal_download_worker_thread(void *arg); -/* ---- Download internal function declarations ---- */ +/** + * @brief Atomically begin a downloadFirmware session and track the context. + * + * Sets g_dwnl_in_progress = true and stores ctx in g_active_dwnl_ctx. + * If a download is already in progress, returns false without modifying state. + * + * @param ctx The newly allocated DownloadRequestContext to track. + * @return true if session started, false if another download is already active. + */ +bool internal_begin_download(DownloadRequestContext *ctx); -/* ======================================================================== - * DOWNLOAD CALLBACK REGISTRATION - * ======================================================================== */ +/** + * @brief Atomically end the downloadFirmware session and untrack the context. + * + * Sets g_dwnl_in_progress = false and g_active_dwnl_ctx = NULL. + * Called by the worker thread in cleanup, BEFORE freeing ctx. + */ +void internal_end_download(void); /** - * @brief Register a download callback keyed by handle + * @brief Atomically clear download in-progress state on error paths. * - * @param handle App's FirmwareInterfaceHandle (strdup'd internally) - * @param callback App's DownloadCallback - * @return true on success, false if registry full + * Same as internal_end_download() but used when downloadFirmware() itself + * fails (e.g., pthread_create fails after internal_begin_download succeeded). */ -bool internal_dwnl_register_callback(FirmwareInterfaceHandle handle, - DownloadCallback callback); +void internal_abort_download(void); + +/** + * @brief Query whether a downloadFirmware() operation is currently in progress. + * + * Used by unregisterProcess() to enforce the session-state invariant. + * Thread-safe: protected by internal mutex. + * + * @return true if a download worker thread is active, false otherwise. + */ +bool internal_is_dwnl_in_progress(void); + +/** + * @brief Cancel all active download worker threads and join them. + * + * Called from library destructor to ensure no threads are running + * when library code is unmapped. + */ +void internal_cancel_all_active_download_threads(void); /** * @brief Parse GVariant DownloadProgress signal payload * - * Expected GVariant signature: (ii) - * i progress_percent - * i status_code + * Expected GVariant signature: (tsuss) * * @param parameters GVariant from D-Bus signal * @param out_data Output (must be zeroed before call) @@ -346,47 +488,46 @@ bool internal_parse_dwnl_signal_data(GVariant *parameters, DownloadStatus internal_map_dwnl_status_code(int32_t status_code); /* ======================================================================== - * UPDATE FIRMWARE — INTERNAL TYPES AND DECLARATIONS + * UPDATE FIRMWARE — ON-DEMAND WORKER THREAD (Phase 3) * ======================================================================== * * ARCHITECTURE: * - * App A ──updateFirmware(hdl_A, req_A, cb_A)──┐ - * App B ──updateFirmware(hdl_B, req_B, cb_B)──┼──► UpdateRegistry (keyed by handle) - * App C ──updateFirmware(hdl_C, req_C, cb_C)──┘ │ - * │ same background thread - * │ subscribed to UpdateProgress - * ▼ - * Daemon emits UpdateProgress(progress%, status) REPEATEDLY - * │ - * on_update_progress_signal() - * │ - * dispatch_all_update_active() │ - * ├── cb_A(progress%, status) - * ├── cb_B(progress%, status) - * └── cb_C(progress%, status) - * - * IDENTICAL LIFECYCLE TO DOWNLOAD: - * Slot stays ACTIVE across multiple signals. - * Reset to IDLE only on UPDATE_COMPLETED or UPDATE_ERROR. + * App calls updateFirmware(handle, request, callback) + * │ + * ├─ Allocate UpdateRequestContext on heap + * ├─ internal_begin_update(ctx) — reject if already active + * ├─ pthread_create(internal_update_worker_thread, ctx) + * │ │ + * │ ├─ New GMainContext + GMainLoop (isolated) + * │ ├─ g_bus_get_sync() → D-Bus connection + * │ ├─ Subscribe to UpdateProgress signal + * │ ├─ g_dbus_connection_call_sync("UpdateFirmware") + * │ │ → daemon reply (sss): result, status, message + * │ │ → if FAILED: set init_failed, signal ready, cleanup + * │ │ → if SUCCESS: set daemon_accepted + * │ ├─ Add 3600s timeout + * │ ├─ Signal caller "ready" via condvar + * │ ├─ g_main_loop_run() — receives UpdateProgress signals + * │ │ → callback fires MULTIPLE times (per-signal) + * │ │ → quits loop ONLY on COMPLETED/ERROR + * │ └─ Cleanup everything, free(ctx), thread exits + * │ + * ├─ pthread_cond_wait() for ready signal + * └─ Return SUCCESS or FAIL (accurate — reflects daemon reply) + * + * KEY SIMILARITY TO DownloadFirmware: + * Both APIs: callback fires MULTIPLE TIMES (per progress signal), + * thread exits only on terminal status (COMPLETED/ERROR). + * Both use condvar handshake with daemon synchronous reply for accurate return. + * * ======================================================================== */ -#define DBUS_METHOD_UPDATE "UpdateFirmware" -#define DBUS_SIGNAL_UPDATE_PROGRESS "UpdateProgress" +#define DBUS_METHOD_UPDATE "UpdateFirmware" +#define DBUS_SIGNAL_UPDATE_PROGRESS "UpdateProgress" -/** - * @brief Lifecycle state of one update callback registry slot - * - * IDLE ──(register)──► ACTIVE ──(COMPLETED/ERROR signal)──► IDLE - * │ - * │ (fires callback on EVERY UpdateProgress signal) - * └──(timeout)──► TIMED_OUT ──► IDLE - */ -typedef enum { - UPDATE_CB_STATE_IDLE = 0, /**< Slot free and reusable */ - UPDATE_CB_STATE_ACTIVE = 1, /**< Receiving update progress signals */ - UPDATE_CB_STATE_TIMED_OUT = 2 /**< Timed out waiting for completion */ -} UpdateCbState; +/* Timeout for update worker thread (seconds) — 1 hour */ +#define UPDATE_SIGNAL_TIMEOUT_SECONDS 3600 /** * @brief Parsed payload from UpdateProgress D-Bus signal @@ -408,48 +549,138 @@ typedef struct { } InternalUpdateSignalData; /** - * @brief One slot in the update callback registry + * @brief Per-request context for on-demand UpdateFirmware worker thread. + * + * Lifecycle: + * - Allocated in updateFirmware() (caller thread) via calloc + * - Ownership transferred to worker thread after condvar handshake + * - Freed by worker thread after update completes/fails (or timeout) * - * Keyed by handle_key. Stays ACTIVE until UPDATE_COMPLETED or UPDATE_ERROR. + * Same pattern as DownloadRequestContext: + * - callback fires MULTIPLE times (per-progress-signal) + * - worker quits loop ONLY on terminal status (COMPLETED/ERROR) + * - daemon_accepted: worker reads daemon's synchronous reply + * - 3600s timeout + * + * Memory: ~200 bytes (excluding GLib objects) */ typedef struct { - UpdateCbState state; /**< IDLE or ACTIVE */ - char *handle_key; /**< strdup of app's handle */ - UpdateCallback callback; /**< App's progress callback */ - time_t registered_time; /**< For timeout detection */ -} UpdateCbEntry; + /* Condvar handshake: worker signals "I'm ready" to caller */ + pthread_mutex_t ready_mutex; + pthread_cond_t ready_cond; + bool is_ready; /**< true = worker finished setup */ + bool init_failed; /**< true = D-Bus failed or daemon rejected */ + + /** + * Handshake lifetime ownership flag. + * + * When true, the CALLER (API function) owns cleanup of the sync primitives + * (ready_mutex, ready_cond) and the ctx allocation itself. The worker thread + * must NOT destroy the mutex/cond or free(ctx). + * + * Set to true by the worker BEFORE signaling ready on init-failure paths. + * This prevents the race where the worker destroys the mutex/cond/ctx while + * the caller is still inside pthread_cond_timedwait or pthread_mutex_unlock. + * + * On success paths, this remains false — the worker owns everything. + */ + bool caller_owns_cleanup; + + /* GLib event loop (isolated, per-thread) */ + GMainContext *context; + GMainLoop *main_loop; + GDBusConnection *connection; + guint subscription_id; + + /* Request data (all strdup'd — owned by worker thread) */ + char *handle_key; /**< strdup of FirmwareInterfaceHandle */ + char *firmware_name; /**< strdup of request->firmwareName */ + char *firmware_location; /**< strdup of request->LocationOfFirmware */ + char *firmware_type; /**< strdup of request->TypeOfFirmware */ + char *reboot_flag; /**< "true" or "false" string */ + UpdateCallback callback; /**< Client's callback function ptr */ + + /* Daemon reply (from synchronous D-Bus method return) */ + bool daemon_accepted; /**< true if daemon returned RDKFW_UPDATE_SUCCESS */ + char *daemon_reject_message; /**< strdup of daemon's error message (if rejected) */ + + /* Timeout tracking */ + GSource *timeout_source; /**< For cancellation in cleanup */ + + /* Thread handle (for join in destructor) */ + pthread_t thread; +} UpdateRequestContext; + +/* ---- Update internal function declarations ---- */ /** - * @brief Global registry for all active update callbacks + * @brief Worker thread entry point for on-demand UpdateFirmware. + * + * Creates an isolated GLib event loop, connects to D-Bus, subscribes to + * UpdateProgress signal, sends UpdateFirmware D-Bus method call + * synchronously, then waits for progress signals (with 3600s timeout). + * Fires the client's callback on every progress signal, quits loop on + * COMPLETED or ERROR, then cleans up all resources and exits. + * + * @param arg UpdateRequestContext* (ownership transferred from caller) + * @return NULL */ -typedef struct { - UpdateCbEntry entries[MAX_PENDING_CALLBACKS]; - pthread_mutex_t mutex; - bool initialized; -} UpdateCbRegistry; +void *internal_update_worker_thread(void *arg); -/* ---- Update internal function declarations ---- */ +/** + * @brief Atomically begin an updateFirmware session and track the context. + * + * Sets g_update_in_progress = true and stores ctx in g_active_update_ctx. + * If an update is already in progress, returns false without modifying state. + * + * @param ctx The newly allocated UpdateRequestContext to track. + * @return true if session started, false if another update is already active. + */ +bool internal_begin_update(UpdateRequestContext *ctx); -/* ======================================================================== - * UPDATE CALLBACK REGISTRATION - * ======================================================================== */ +/** + * @brief Atomically end the updateFirmware session and untrack the context. + * + * Sets g_update_in_progress = false and g_active_update_ctx = NULL. + * Called by the worker thread in cleanup, BEFORE freeing ctx. + */ +void internal_end_update(void); + +/** + * @brief Atomically clear update in-progress state on error paths. + * + * Same as internal_end_update() but used when updateFirmware() itself + * fails (e.g., pthread_create fails after internal_begin_update succeeded). + */ +void internal_abort_update(void); /** - * @brief Register an update callback keyed by handle + * @brief Query whether an updateFirmware() operation is currently in progress. + * + * Used by unregisterProcess() to enforce the session-state invariant. + * Thread-safe: protected by internal mutex. * - * @param handle App's FirmwareInterfaceHandle (strdup'd internally) - * @param callback App's UpdateCallback - * @return true on success, false if registry full + * @return true if an update worker thread is active, false otherwise. */ -bool internal_update_register_callback(FirmwareInterfaceHandle handle, - UpdateCallback callback); +bool internal_is_update_in_progress(void); + +/** + * @brief Cancel all active update worker threads and join them. + * + * Called from library destructor to ensure no threads are running + * when library code is unmapped. + */ +void internal_cancel_all_active_update_threads(void); /** * @brief Parse GVariant UpdateProgress signal payload * - * Expected GVariant signature: (ii) - * i progress_percent - * i status_code + * Expected GVariant signature: (tsiis) + * t handlerId (uint64) + * s firmwareName (string) + * i progressPercent (int32) + * i statusCode (int32) + * s message (string) * * @param parameters GVariant from D-Bus signal * @param out_data Output (must be zeroed before call) 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 index f9012fcf..68de8c14 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_log.h +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_log.h @@ -20,102 +20,64 @@ * @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. + * 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 -#include +#include "rdkv_cdl_log_wrapper.h" /* SWLOG_*, log_init(), log_exit() */ #ifdef __cplusplus extern "C" { #endif /* ======================================================================== - * LOGGING CONFIGURATION + * Library code uses FWUPMGR_* macros - logs as [FWUPMGR] + * Example app defines EXAMPLE_* macros - logs as [EXAMPLE] * ======================================================================== */ -/** 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, ...); +#if defined(RDK_LOGGER) +#include "rdk_debug.h" -/* ======================================================================== - * LOGGING MACROS - Same pattern as SWLOG_* in daemon - * ======================================================================== */ +/* Generic base macro callers provide their own module name */ +#define FWUPMGR_LOG(level, module, format, ...) \ + RDK_LOG(level, module, format, ##__VA_ARGS__) -/** - * @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__) +/* 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__) -/** - * @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__) +#else -/** - * @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__) +/* 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) -/** - * @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__) +#endif #ifdef __cplusplus } diff --git a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 8e3c27ae..070864a6 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) @@ -70,6 +70,7 @@ #include "rdkFwupdateMgr_client.h" #include "rdkFwupdateMgr_log.h" +#include "rdkFwupdateMgr_async_internal.h" /* for internal_is_check_in_progress() */ #include #include #include @@ -96,7 +97,7 @@ #define MAX_LIB_VERSION_LEN 64 /** Default D-Bus call timeout in milliseconds (10 seconds) */ -#define DBUS_TIMEOUT_MS 10000 +#define DBUS_TIMEOUT_MSEC 10000 /* ======================================================================== * INTERNAL CONTEXT STRUCTURE @@ -248,7 +249,7 @@ static bool validate_lib_version(const char *libVersion) * 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) + * - Timeout: 10 seconds (configurable via DBUS_TIMEOUT_MSEC) * - Returns string handle (handler_id as decimal string) * * ERROR HANDLING: @@ -288,24 +289,24 @@ 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", g_variant_new("(ss)", processName, libVersion), G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, + DBUS_TIMEOUT_MSEC, NULL, // GCancellable &error ); if (!result) { FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", - error->message); - g_error_free(error); + error ? error->message : "unknown error (GError not set)"); + if (error) g_error_free(error); g_object_unref(proxy); return NULL; } @@ -337,7 +338,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_TIMEOUT_MSEC, NULL, &cleanup_error ); @@ -390,12 +391,76 @@ void unregisterProcess(FirmwareInterfaceHandle handler) guint64 handler_id = 0; gboolean success = FALSE; - // NULL check: Safe to unregister NULL handle (no-op) + /* NULL check first: always a no-op, regardless of in-progress state. + * + * The public API contract says "Safe to call with NULL handle (no-op)". + * This must be honored unconditionally — even during active operations. + * A NULL handle means there's nothing to unregister; the in-progress + * guards below only apply when the caller has a real handle. + */ if (!handler) { FWUPMGR_INFO("unregisterProcess() called with NULL handle (no-op)\n"); return; } + /* Session state validation: reject if checkForUpdate() is active. + * + * You can't hang up the phone while waiting for an answer. + * registerProcess() = start session, checkForUpdate() = ask a question, + * unregisterProcess() = end session. If we let the app end the session + * while the daemon is still processing the firmware check, the daemon- + * client relationship enters an undefined state. So we reject the call + * and tell the app to wait for the callback first, then unregister. + * + * We return without freeing the handle - caller still owns it and can + * retry after the checkForUpdate callback fires (bounded by 120s timeout). + * + * Note: void return type means we can't return an error code. The app + * must check logs. A future API revision will add a return type. + */ + if (internal_is_check_in_progress()) { + FWUPMGR_ERROR("unregisterProcess: REJECTED - checkForUpdate() is in " + "progress. Wait for the callback to fire, then retry " + "unregisterProcess().\n"); + return; + } + + /* Session state validation: reject if downloadFirmware() is active. + * + * Same rationale as checkForUpdate: you can't end the session while a + * firmware download is in progress. Downloads can take 1-30 minutes, + * but the app should wait for the DWNL_COMPLETED or DWNL_ERROR callback + * before unregistering. If the app receives SIGTERM, it should just exit() + * — the daemon detects the D-Bus peer disconnect and cleans up. + * + * We return without freeing the handle — caller still owns it and can + * retry after the download callback fires with a terminal status. + */ + if (internal_is_dwnl_in_progress()) { + FWUPMGR_ERROR("unregisterProcess: REJECTED - downloadFirmware() is in " + "progress. Wait for the DWNL_COMPLETED or DWNL_ERROR " + "callback, then retry unregisterProcess().\n"); + return; + } + + /* Session state validation: reject if updateFirmware() is active. + * + * Same rationale as downloadFirmware: you can't end the session while a + * firmware flash is in progress. Flashing can take 5-60 minutes, + * but the app should wait for the UPDATE_COMPLETED or UPDATE_ERROR callback + * before unregistering. If the app receives SIGTERM, it should just exit() + * — the daemon detects the D-Bus peer disconnect and cleans up. + * + * We return without freeing the handle — caller still owns it and can + * retry after the update callback fires with a terminal status. + */ + if (internal_is_update_in_progress()) { + FWUPMGR_ERROR("unregisterProcess: REJECTED - updateFirmware() is in " + "progress. Wait for the UPDATE_COMPLETED or UPDATE_ERROR " + "callback, then retry unregisterProcess().\n"); + return; + } + FWUPMGR_INFO("unregisterProcess() called\n"); FWUPMGR_INFO(" handle: '%s'\n", handler); @@ -457,16 +522,16 @@ void unregisterProcess(FirmwareInterfaceHandle handler) "UnregisterProcess", g_variant_new("(t)", handler_id), G_DBUS_CALL_FLAGS_NONE, - DBUS_TIMEOUT_MS, + DBUS_TIMEOUT_MSEC, NULL, // GCancellable &error ); if (!result) { 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); 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..0f3e7719 100644 --- a/src/rdkFwupdateMgr.c +++ b/src/rdkFwupdateMgr.c @@ -1244,32 +1244,40 @@ int main(int argc, char *argv[]) { } */ } + 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; + break; case STATE_IDLE: /** * Main operational state - D-Bus event loop. 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);