diff --git a/Makefile.am b/Makefile.am index 58363925..34225549 100644 --- a/Makefile.am +++ b/Makefile.am @@ -270,6 +270,28 @@ example_plugin_LDFLAGS = \ -L$(PKG_CONFIG_SYSROOT_DIR)/$(libdir) +# Build and install the KnowWhereItBreaks developer test utility +# Binary is named kwib_test_utility to avoid collision with the +# kwib_src/ source directory (KnowWhereItBreaks sources) during the build. +bin_PROGRAMS += kwib_test_utility + +kwib_test_utility_SOURCES = \ + ${top_srcdir}/kwib_src/KnowWhereItBreaks.c + +kwib_test_utility_CFLAGS = \ + -I${top_srcdir}/librdkFwupdateMgr/include \ + $(AM_CFLAGS) \ + $(GLIB_CFLAGS) + +kwib_test_utility_LDADD = \ + librdkFwupdateMgr.la \ + $(GLIB_LIBS) \ + -lpthread + +kwib_test_utility_LDFLAGS = \ + -L$(PKG_CONFIG_SYSROOT_DIR)/$(libdir) + + if INSTALL_TEST_FWUPGRADER bin_PROGRAMS += testrdkvfwupgrader diff --git a/docs/CODEREVIEW_REPORT.txt b/docs/CODEREVIEW_REPORT.txt new file mode 100755 index 00000000..e266691f --- /dev/null +++ b/docs/CODEREVIEW_REPORT.txt @@ -0,0 +1,513 @@ +# Code Review Report + +**PR:** [#225 β€” RDKEMW-15498: Implement software update service layer library (Fix review comments)](https://github.com/rdkcentral/rdkfwupdater/pull/225) +**Repository:** `rdkcentral/rdkfwupdater` +**Branch:** `topic/RDKEMW-15498` β†’ `develop` +**Reviewer:** @copilot (AI-assisted review) +**Date:** 2026-04-01 +**Author:** @mkadinti + +--- + +## Files Reviewed + +| # | File | Purpose | +|---|------|---------| +| 1 | `librdkFwupdateMgr/src/rdkFwupdateMgr_api.c` | Public API: `checkForUpdate`, `downloadFirmware`, `updateFirmware`, library lifecycle | +| 2 | `librdkFwupdateMgr/src/rdkFwupdateMgr_async.c` | Internal async engine: worker threads, signal handlers, state management | +| 3 | `librdkFwupdateMgr/src/rdkFwupdateMgr_async_internal.h` | Internal header: type definitions, constants, function declarations | +| 4 | `librdkFwupdateMgr/src/rdkFwupdateMgr_process.c` | Process registration/unregistration D-Bus client logic | + +--- + +## Review Summary + +| Review Category | Status | Details | +|-----------------|--------|---------| +| **1. Coverity Issues** | 🟑 Mostly Resolved | 2 HIGH fixed (CID-01/02); 1 MEDIUM fixed (CID-03); 3 LOW remaining | +| **2. Memory Leaks** | βœ… Verified | LEAK-01 verified correct; LEAK-02 correct; LEAK-03 NOT APPLICABLE (char[] not char*) | +| **3. Thread Safety** | βœ… Well Handled | Solid mutex-based protection | +| **4. Race Conditions** | βœ… Well Handled | RACE-01 correct; RACE-02 LOW (client-contract violation) | +| **5. Critical Sections** | βœ… Well Handled | Good encapsulation pattern | +| **6. Positive/Negative Tests** | ⚠️ Gaps Found | ~4 unhandled edge cases (documentation/test coverage) | +| **7. Buffer Overflow/Underflow** | βœ… Well Handled | strncpy used properly | + +**Overall Assessment:** οΏ½ **Approved β€” all HIGH/MEDIUM findings fixed or verified; LOW-priority follow-ups remain** + +--- + +## 1. Coverity Issues + +### CID-01: Potential NULL Pointer Dereference in `registerProcess()` (HIGH) β€” FIXED + +**File:** `rdkFwupdateMgr_process.c`, **Line:** 307-308 + +```c +if (!result) { + FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", + error->message); // ← error may be NULL + g_error_free(error); + ... +} +``` + +**Problem:** If `g_dbus_proxy_call_sync()` returns `NULL` but fails to set `error` (e.g., extreme OOM), `error->message` dereferences a NULL pointer. This is inconsistent with the defensive pattern used elsewhere in the same file (e.g., `create_dbus_proxy()` where `(error && *error)` is checked). + +**Fix:** +```c +if (!result) { + FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", + error ? error->message : "unknown error (GError not set)"); + if (error) g_error_free(error); + g_object_unref(proxy); + return NULL; +} +``` + +--- + +### CID-02: Potential NULL Pointer Dereference in `unregisterProcess()` (HIGH) β€” FIXED + +**File:** `rdkFwupdateMgr_process.c`, **Line:** 531-534 + +```c +if (!result) { + FWUPMGR_WARN("UnregisterProcess D-Bus call failed: %s\n", + error->message); // ← error may be NULL + ... + g_error_free(error); +``` + +**Problem:** Same pattern as CID-01. `error` may be NULL if GLib fails to populate it. + +**Fix:** +```c +if (!result) { + FWUPMGR_WARN("UnregisterProcess D-Bus call failed: %s\n", + error ? error->message : "unknown error"); + if (error) g_error_free(error); + ... +} +``` + +--- + +### CID-03: `clock_gettime()` Return Value Unchecked (MEDIUM) β€” FIXED + +**File:** `rdkFwupdateMgr_api.c`, **Line:** 228 + +```c +clock_gettime(CLOCK_REALTIME, &deadline); +``` + +**Problem:** `clock_gettime()` can fail (returns -1), leaving `deadline` with uninitialized/stale values. A corrupted deadline could cause `pthread_cond_timedwait()` to return immediately or wait forever. + +**Fix:** +```c +if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + FWUPMGR_ERROR("checkForUpdate: clock_gettime failed (errno=%d)\n", errno); + pthread_join(worker_thread, NULL); + return CHECK_FOR_UPDATE_FAIL; +} +``` + +> **Note:** This same pattern applies to `downloadFirmware()` and `updateFirmware()` in the same file. + +--- + +### CID-04: `DBUS_SERVICE_NAME`, `DBUS_OBJECT_PATH`, `DBUS_INTERFACE_NAME` Redefined (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 85-91 +**Also defined in:** `rdkFwupdateMgr_async_internal.h`, **Lines:** 110-112 + +```c +// In rdkFwupdateMgr_process.c: +#define DBUS_SERVICE_NAME "org.rdkfwupdater.Service" +#define DBUS_OBJECT_PATH "/org/rdkfwupdater/Service" +#define DBUS_INTERFACE_NAME "org.rdkfwupdater.Interface" + +// In rdkFwupdateMgr_async_internal.h (included by process.c): +#define DBUS_SERVICE_NAME "org.rdkfwupdater.Service" +#define DBUS_OBJECT_PATH "/org/rdkfwupdater/Service" +#define DBUS_INTERFACE_NAME "org.rdkfwupdater.Interface" +``` + +**Problem:** Duplicate macro definitions. While values are identical (no compile error with `-Werror`), this is a maintenance hazard. If one is updated and the other is not, silent behavioral divergence occurs. Coverity reports this as `MACRO_REDEFINED`. + +**Fix:** Remove the duplicate definitions in `rdkFwupdateMgr_process.c` and rely solely on the ones from the included header `rdkFwupdateMgr_async_internal.h`. + +--- + +### CID-05: Debug `fprintf(stderr)` Calls Mixed with Logging Macros (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 292, 295 + +```c +fprintf(stderr, "[rdkFwupdateMgr] D-Bus proxy created successfully\n"); +fprintf(stderr, "[rdkFwupdateMgr] Calling RegisterProcess D-Bus method...\n"); +``` + +**Problem:** All other logging uses `FWUPMGR_INFO` / `FWUPMGR_ERROR` macros, but these two lines use raw `fprintf(stderr)`. This bypasses log-level filtering and is likely leftover debug code. Coverity may flag as `DEAD_CODE` or coding standards violation. + +**Fix:** Replace with `FWUPMGR_INFO(...)` or remove entirely. + +--- + +### CID-06: Commented-Out Code Left in Source (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 124-127, 363 + +```c +//typedef struct _FirmwareInterfaceContext { + // uint64_t handler_id; + // char handler_id_str[32]; +//} FirmwareInterfaceContext; + +//snprintf(handle_str, 32, "" %PRIu64, handler_id); +``` + +**Problem:** Commented-out code is a Coverity/MISRA coding standards violation. It indicates incomplete refactoring and confuses reviewers about intent. + +**Fix:** Remove all commented-out code blocks. + +--- + +## 2. Memory Leak Analysis + +### LEAK-01: `daemon_reject_message` Not Freed in `downloadFirmware()` Error Paths (MEDIUM) β€” VERIFIED CORRECT (NO LEAK) + +**File:** `rdkFwupdateMgr_api.c`, **Lines:** 459 + +```c +ctx->daemon_reject_message = NULL; +``` + +**Context:** The field `daemon_reject_message` is set by the worker thread (strdup'd from daemon reply). If the worker sets it before signaling init failure, and the caller joins the worker on the failure path (line ~259 equivalent in download), the worker thread is responsible for cleanup. **However**, the worker thread cleanup code must ensure `daemon_reject_message` is freed in all paths. + +**Recommendation:** Audit the worker thread cleanup in `internal_download_worker_thread()` to confirm `free(ctx->daemon_reject_message)` is called in every exit path (success, failure, timeout, cancellation). + +--- + +### LEAK-02: `registerProcess()` Best-Effort Cleanup May Leak `cleanup_error` (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 332-358 + +In the cleanup block when `malloc` fails for `handle_str`, the code creates a `cleanup_proxy`. If `g_dbus_proxy_call_sync` fails AND sets `cleanup_error`, but the proxy call succeeds on a second path, `cleanup_error` might not get freed. + +**Current code is actually correct** β€” all branches free `cleanup_error` when non-NULL. No action needed, but worth noting the complexity of this error-handling block. + +--- + +### LEAK-03: `on_check_signal_handler()` Parse Failure Path β€” `signal_data` Not Cleaned (MEDIUM) β€” NOT APPLICABLE (struct uses char[] not char*) + +**File:** `rdkFwupdateMgr_async.c`, **Lines:** 342-349 + +```c +if (!internal_parse_signal_data(parameters, &signal_data)) { + FWUPMGR_ERROR("on_check_signal_handler: parse failed\n"); + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + return; // ← signal_data may have partial allocations +} +``` + +**Problem:** If `internal_parse_signal_data()` partially succeeds (e.g., allocates `current_version` but fails on `update_details`), the partially-allocated fields are leaked because `internal_cleanup_signal_data()` is never called on the failure path. + +**Fix:** +```c +if (!internal_parse_signal_data(parameters, &signal_data)) { + FWUPMGR_ERROR("on_check_signal_handler: parse failed\n"); + internal_cleanup_signal_data(&signal_data); // ← add this + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + return; +} +``` + +> **Note:** This same pattern likely exists in `on_download_signal_handler()` and `on_update_signal_handler()` β€” audit those as well. + +--- + +## 3. Thread Safety Analysis + +### βœ… Strengths + +| Aspect | Assessment | +|--------|------------| +| Global state encapsulation | Excellent β€” all `g_*_in_progress` variables are `static` and accessed only through mutex-protected accessor functions | +| Mutex initialization | Correct β€” uses `PTHREAD_MUTEX_INITIALIZER` for static mutexes (no runtime init failure) | +| Per-request condvar handshake | Correct β€” `ready_mutex`/`ready_cond` are per-context, eliminating cross-request interference | +| Callback invocation | Correct β€” callbacks invoked with global mutex **released** (no deadlock risk) | +| Destructor cancellation | Correct β€” snapshots thread handle and takes GMainLoop ref under mutex before unlocking | + +### TS-01: `pthread_cond_timedwait()` Uses `CLOCK_REALTIME` (LOW) + +**File:** `rdkFwupdateMgr_api.c`, **Line:** 228-235 + +```c +clock_gettime(CLOCK_REALTIME, &deadline); +deadline.tv_sec += WORKER_READY_TIMEOUT_SEC; +// ... +pthread_cond_timedwait(&ctx->ready_cond, &ctx->ready_mutex, &deadline); +``` + +**Problem:** `CLOCK_REALTIME` is affected by NTP time adjustments. If the system clock jumps forward by 10+ seconds during the wait, the timeout fires immediately. If it jumps backward, the wait extends by that amount. + +**Fix (for a future iteration):** Use `CLOCK_MONOTONIC` with `pthread_condattr_setclock()`: +```c +pthread_condattr_t attr; +pthread_condattr_init(&attr); +pthread_condattr_setclock(&attr, CLOCK_MONOTONIC); +pthread_cond_init(&ctx->ready_cond, &attr); +pthread_condattr_destroy(&attr); +// Then use clock_gettime(CLOCK_MONOTONIC, &deadline); +``` + +--- + +## 4. Race Condition Analysis + +### RACE-01: Window Between `pthread_create()` and `worker_thread` Copy (LOW) + +**File:** `rdkFwupdateMgr_api.c`, **Lines:** 195-216 + +```c +if (pthread_create(&ctx->thread, NULL, internal_check_worker_thread, ctx) != 0) { ... } + +pthread_t worker_thread = ctx->thread; // ← potential race? +``` + +**Analysis:** The code correctly copies `ctx->thread` *before* entering the condvar wait. `pthread_create()` writes to `ctx->thread` before returning, so `worker_thread` is valid. The comment at line 205-215 correctly explains why this is done. **No actual race**, but the pattern requires the copy to happen before `pthread_mutex_lock(&ctx->ready_mutex)`. Current code is correct. + +**Status:** βœ… No action needed β€” code is correct, comments are thorough. + +--- + +### RACE-02: `unregisterProcess()` TOCTOU on In-Progress Checks (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 421-462 + +```c +if (internal_is_check_in_progress()) { return; } +if (internal_is_dwnl_in_progress()) { return; } +if (internal_is_update_in_progress()) { return; } +// ... proceed with unregister +``` + +**Problem:** Between checking `internal_is_check_in_progress()` and the actual D-Bus `UnregisterProcess` call, another thread could call `checkForUpdate()`. This is a Time-Of-Check-Time-Of-Use (TOCTOU) race. + +**Mitigation:** The three checks use separate mutexes. A new `checkForUpdate()` call could start between the `is_check_in_progress()` check returning false and the D-Bus call. + +**Risk Assessment:** LOW β€” The `unregisterProcess()` API documentation explicitly states it should be called only after all operations complete. This is a client-contract violation, not a library defect. However, for defensive coding: + +**Recommendation (optional):** Consider adding an atomic `g_unregister_in_progress` flag that `checkForUpdate`/`downloadFirmware`/`updateFirmware` also check, creating a bidirectional lock. + +--- + +## 5. Critical Section Handling + +### βœ… Strengths + +| Pattern | Assessment | +|---------|------------| +| `internal_begin_*()` / `internal_end_*()` / `internal_abort_*()` | Excellent β€” atomic state transitions under single mutex | +| Destructor cancel pattern (snapshot-under-lock) | Excellent β€” prevents use-after-free of ctx while joining thread | +| GMainLoop ref counting in destructor | Correct β€” prevents premature GMainLoop destruction | +| `internal_end_*()` called BEFORE `free(ctx)` | Correct ordering documented and enforced | + +### CS-01: No `pthread_mutex_destroy()` for Static Mutexes (INFO) + +**File:** `rdkFwupdateMgr_async.c`, **Lines:** 71, 84, 97 + +```c +static pthread_mutex_t g_check_in_progress_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t g_dwnl_in_progress_mutex = PTHREAD_MUTEX_INITIALIZER; +static pthread_mutex_t g_update_in_progress_mutex = PTHREAD_MUTEX_INITIALIZER; +``` + +**Analysis:** These are statically-initialized and never destroyed. For `PTHREAD_MUTEX_INITIALIZER`, `pthread_mutex_destroy()` is optional per POSIX. The library destructor (`rdkFwupdateMgr_lib_deinit`) does not destroy them. This is **acceptable** since the process is either exiting or the shared library is being unloaded. + +**Status:** βœ… No action needed β€” POSIX-compliant for static initialization. + +--- + +## 6. Positive and Negative Test Case Coverage + +### βœ… Well-Handled Cases + +| Test Case | API | Status | +|-----------|-----|--------| +| NULL handle | `checkForUpdate`, `downloadFirmware`, `updateFirmware` | βœ… Returns FAIL | +| Empty handle | `checkForUpdate`, `downloadFirmware`, `updateFirmware` | βœ… Returns FAIL | +| NULL callback | `checkForUpdate`, `downloadFirmware` | βœ… Returns FAIL | +| NULL request struct | `downloadFirmware`, `updateFirmware` | βœ… Returns FAIL | +| NULL firmwareName | `downloadFirmware` | βœ… Returns FAIL | +| Empty firmwareName | `downloadFirmware` | βœ… Returns FAIL | +| Duplicate concurrent call | All APIs | βœ… Second call rejected | +| calloc failure | All APIs | βœ… Returns FAIL, no leak | +| strdup failure | All APIs | βœ… Cascading cleanup correct | +| pthread_mutex_init failure | All APIs | βœ… Cleanup correct | +| pthread_cond_init failure | All APIs | βœ… Cleanup correct | +| pthread_create failure | All APIs | βœ… abort_*() called, cleanup correct | +| Worker init failure (D-Bus dead) | All APIs | βœ… join + return FAIL | +| Worker timeout | All APIs | βœ… ETIMEDOUT handled | +| NULL handle for unregister | `unregisterProcess` | βœ… No-op | +| Invalid handle (garbage chars) | `unregisterProcess` | βœ… Detected and rejected | +| Numeric overflow in handle | `unregisterProcess` | βœ… errno checked | +| D-Bus proxy creation failure | `registerProcess`, `unregisterProcess` | βœ… Graceful failure | + +### ⚠️ Missing / Incomplete Test Cases + +#### TC-01: `downloadFirmware()` with NULL `downloadUrl` and NULL `TypeOfFirmware` (MEDIUM) + +**File:** `rdkFwupdateMgr_api.c` + +The code handles `NULL` for these optional fields (skips strdup), but there is **no explicit validation or documentation** that these are truly optional in the API contract. If the daemon requires them, the error would surface asynchronously in the worker thread, making debugging difficult. + +**Recommendation:** Add a comment or validation documenting which request fields are mandatory vs. optional. + +--- + +#### TC-02: `checkForUpdate()` Callback Crashes/Throws (MEDIUM) + +**File:** `rdkFwupdateMgr_async.c`, **Line:** 391 + +```c +ctx->callback(&fwinfo_data); +``` + +If the client's callback crashes (segfault, throws C++ exception), the worker thread dies without calling `internal_end_check()`, permanently locking the `g_check_in_progress` flag to `true`. All subsequent `checkForUpdate()` calls will be rejected. + +**Recommendation:** While you cannot prevent client crashes, consider wrapping the callback in a signal handler or adding a comment documenting this as a known limitation. This applies to all three APIs. + +--- + +#### TC-03: `unregisterProcess()` with `handler_id == 0` (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 497-501 + +```c +if (handler_id == 0) { + FWUPMGR_ERROR("Invalid handle: handler_id cannot be 0\n"); + free(handler); + return; +} +``` + +**Analysis:** The daemon could theoretically return 0 as a valid handler_id. If that ever happens, the client would have a "valid" handle it can never unregister. The `registerProcess()` code does not validate `handler_id != 0`. + +**Recommendation:** Either validate `handler_id != 0` in `registerProcess()` immediately after the D-Bus call, or document that the daemon guarantees non-zero handler IDs. + +--- + +#### TC-04: Library Destructor Called While Worker Thread in Callback (LOW) + +**File:** `rdkFwupdateMgr_api.c`, **Lines:** 304-321 + +If `dlclose()` is called while a worker thread is inside the client's callback, the destructor calls `internal_cancel_all_active_*_threads()`, which quits the GMainLoop and joins the thread. However, the thread is blocked inside the callback β€” `g_main_loop_quit()` won't take effect until the callback returns and control returns to `g_main_loop_run()`. + +**Status:** The code handles this correctly via `pthread_join()`, which blocks the destructor until the worker exits. The GMainLoop ref prevents destruction. **No action needed**, but worth documenting as a known blocking behavior. + +--- + +## 7. Buffer Overflow / Underflow Analysis + +### βœ… Strengths + +| Pattern | Assessment | +|---------|------------| +| `strncpy` with `sizeof(buf) - 1` | βœ… Correct (e.g., `fwinfo_data.CurrFWVersion`) | +| Explicit null-termination after `strncpy` | βœ… Correct | +| `snprintf(handle_str, 32, ...)` | βœ… Bounded | +| `strdup()` for heap copies | βœ… Correct β€” no buffer size assumptions | +| `calloc()` for struct allocation | βœ… Zero-initialized β€” no uninitialized field access | +| `memset()` before use of stack structs | βœ… Defensive | + +### BUF-01: `malloc(32)` Magic Number (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Line:** 323 + +```c +handle_str = (char*)malloc(32); +``` + +**Problem:** 32 is a magic number. `PRIu64` can produce at most 20 digits (`18446744073709551615`), so 32 is sufficient, but the intent is unclear without a comment. Also, the corresponding `snprintf(handle_str, 32, ...)` hardcodes the same magic number. + +**Fix:** +```c +#define HANDLER_ID_STR_SIZE 32 /* uint64 max = 20 digits + NUL + margin */ +handle_str = (char *)malloc(HANDLER_ID_STR_SIZE); +// ... +snprintf(handle_str, HANDLER_ID_STR_SIZE, "%" PRIu64, handler_id); +``` + +--- + +## Additional Observations + +### OBS-01: `validate_process_name()` Calls `strlen()` Twice (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 200, 205 + +```c +if (strlen(processName) == 0) { ... } +if (strlen(processName) > MAX_PROCESS_NAME_LEN) { ... } +``` + +**Fix:** Cache the result: +```c +size_t len = strlen(processName); +if (len == 0) { ... } +if (len > MAX_PROCESS_NAME_LEN) { ... } +``` + +--- + +### OBS-02: `validate_lib_version()` Same Double `strlen()` (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 231 + +Same pattern β€” cache the `strlen()` result. + +--- + +### OBS-03: `DBUS_TIMEOUT_MS` vs `DBUS_TIMEOUT_MSEC` Naming Inconsistency (LOW) + +- `rdkFwupdateMgr_async_internal.h` defines `DBUS_TIMEOUT_MS 5000` +- `rdkFwupdateMgr_process.c` defines `DBUS_TIMEOUT_MSEC 10000` + +These are different values for different purposes, but the similar names are confusing. Consider renaming for clarity: +- `DBUS_METHOD_TIMEOUT_MS` (for process registration β€” 10s) +- `DBUS_CHECK_TIMEOUT_MS` (for check worker β€” 5s) + +--- + +## Summary of Required Actions + +| Priority | ID | File | Description | +|----------|----|------|-------------| +| πŸ”΄ HIGH | CID-01 | `_process.c:307` | NULL deref: guard `error->message` in RegisterProcess failure | **FIXED** | +| πŸ”΄ HIGH | CID-02 | `_process.c:531` | NULL deref: guard `error->message` in UnregisterProcess failure | **FIXED** | +| 🟑 MEDIUM | CID-03 | `_api.c:228` | Check `clock_gettime()` return value | **FIXED** | +| 🟑 MEDIUM | LEAK-03 | `_async.c:342` | Call `internal_cleanup_signal_data()` on parse failure path | **NOT APPLICABLE** | +| 🟑 MEDIUM | LEAK-01 | `_async.c` (worker) | Audit `daemon_reject_message` freed in all worker exit paths | **VERIFIED CORRECT** | +| 🟒 LOW | CID-04 | `_process.c:85-91` | Remove duplicate macro definitions | +| 🟒 LOW | CID-05 | `_process.c:292,295` | Replace `fprintf(stderr)` with `FWUPMGR_INFO` | +| 🟒 LOW | CID-06 | `_process.c:124-127,363` | Remove commented-out code | +| 🟒 LOW | BUF-01 | `_process.c:323` | Replace magic number 32 with named constant | +| 🟒 LOW | OBS-01/02 | `_process.c:200-205` | Cache `strlen()` result | +| 🟒 LOW | OBS-03 | `_async_internal.h` / `_process.c` | Rename timeout macros for clarity | + +--- + +## Conclusion + +The codebase demonstrates **strong engineering practices** overall: +- Thread safety is well-architected with proper mutex encapsulation +- Memory management follows consistent allocate-on-caller / free-on-worker ownership patterns +- Buffer handling uses safe `strncpy` / `snprintf` patterns throughout +- Error paths are thoroughly handled with cascading cleanup + +The **2 HIGH-priority findings** (NULL pointer dereferences in `_process.c`) should be fixed before merge as they would be flagged by Coverity in production scans. The **2 MEDIUM findings** (unchecked `clock_gettime()` and partial signal data leak) are strongly recommended for this PR cycle. LOW-priority items can be addressed in a follow-up. \ No newline at end of file diff --git a/docs/DBUS_codereview.txt b/docs/DBUS_codereview.txt new file mode 100755 index 00000000..5cc55779 --- /dev/null +++ b/docs/DBUS_codereview.txt @@ -0,0 +1,741 @@ + +## Scope of Review + +### Server-side (Daemon) β€” `src/dbus/` + +| # | File | Size | Purpose | +|---|------|------|---------| +| 1 | `rdkv_dbus_server.c` | ~149 KB | D-Bus service: method dispatch, process tracking, async task management, signal emission | +| 2 | `rdkv_dbus_server.h` | ~19 KB | Header: type definitions (ProcessInfo, TaskContext, AsyncDownloadContext, ProgressUpdate, CurrentDownloadState) | +| 3 | `rdkFwupdateMgr_handlers.c` | ~96 KB | Business logic: XConf fetch, cache management, firmware validation, download/flash workers | +| 4 | `rdkFwupdateMgr_handlers.h` | ~14 KB | Header: CheckUpdateResponse, DownloadFirmwareResult, API declarations | +| 5 | `xconf_comm_status.c` | ~10 KB | Thread-safe XConf communication status module (mutex-protected boolean) | +| 6 | `xconf_comm_status.h` | ~5 KB | Header: initXConfCommStatus, getXConfCommStatus, trySetXConfCommStatus | + +### Client-side (Library) β€” `librdkFwupdateMgr/src/` + +| # | File | Purpose | +|---|------|---------| +| 7 | `rdkFwupdateMgr_api.c` | Public API: checkForUpdate, downloadFirmware, updateFirmware, library lifecycle | +| 8 | `rdkFwupdateMgr_async.c` | Internal async engine: worker threads, signal handlers, state management | +| 9 | `rdkFwupdateMgr_async_internal.h` | Internal header: type definitions, constants, function declarations | +| 10 | `rdkFwupdateMgr_process.c` | Process registration/unregistration D-Bus client logic | + +--- + +## Review Summary + +| Review Category | Status | Findings | +|-----------------|--------|----------| +| **1. Coverity Issues** | οΏ½ Mostly Resolved | 3 HIGH fixed (CID-03/04/05); 2 HIGH reassessed to LOW (CID-01/02); ~7 LOW remaining | +| **2. Memory Leaks** | βœ… Verified | LEAK-01, LEAK-02 verified NOT APPLICABLE (code already handles); LEAK-03 NOT APPLICABLE (char[] not char*); 2 LOW remaining | +| **3. Thread Safety** | βœ… Well Handled | Strong in library; daemon gap (IsFlashInProgress) reassessed LOW due to GLib main loop serialization | +| **4. Race Conditions** | βœ… Mostly Resolved | RACE-01 reassessed LOW (GLib serialization); RACE-02/03 LOW (client-contract or architectural constraint) | +| **5. Critical Sections** | βœ… Well Handled | Good mutex encapsulation patterns on both sides | +| **6. Positive/Negative Tests** | ⚠️ Gaps Found | ~5 unhandled edge cases (test coverage recommendations) | +| **7. Buffer Overflow/Underflow** | βœ… Well Handled | BUF-01 reassessed LOW (no current misuse); BUF-02/03 LOW | + +**Overall Assessment:** οΏ½ **Conditionally Approved β€” all HIGH findings fixed or reassessed; LOW-priority follow-ups remain** + +--- + +## 1. Coverity Issues + +### CID-01: `IsFlashInProgress` Unprotected Multi-Thread Access (HIGH β€” Data Race) β€” SEVERITY REASSESSED: LOW + +**File:** `rdkv_dbus_server.c`, **Line:** 133 +```c +gboolean IsFlashInProgress = FALSE; // Non-static: accessed by worker thread cleanup +``` + +**Original concern:** Data race between main thread reads and worker thread writes. + +**Validation analysis:** The write in `cleanup_flash_state_idle()` is invoked via `g_idle_add()` +(line 1952 of `rdkFwupdateMgr_handlers.c`), which schedules it to run on the **main GLib loop thread** β€” +the same thread that handles D-Bus method calls. GLib's main loop serializes all dispatched callbacks, +so the write and the read in the D-Bus handler **never execute concurrently**. The flash worker thread +itself does NOT directly write `IsFlashInProgress` β€” it only calls `g_idle_add(cleanup_flash_state_idle)`. + +**Reassessment:** No actual data race exists in the current architecture due to GLib main loop +serialization. However, the variable is `extern` (non-static) which makes it fragile if the architecture +ever changes to multi-threaded D-Bus handling. Good defensive practice to add atomic protection, but +**downgraded from HIGH to LOW** for actual risk. + +**Fix (defensive β€” for Coverity compliance):** Apply atomic operations: +```c +// Option A: Use g_atomic_int operations (simplest) +static volatile gint IsFlashInProgress = 0; +// Read: g_atomic_int_get(&IsFlashInProgress) +// Write: g_atomic_int_set(&IsFlashInProgress, TRUE/FALSE) + +// Option B: Create a flash_status module mirroring xconf_comm_status +``` + +--- + +### CID-02: Unsafe Macro Definitions β€” Missing Parentheses (HIGH β€” Expression Evaluation) β€” SEVERITY REASSESSED: LOW + +**File:** `rdkv_dbus_server.c`, **Lines:** 60-62 +```c +#define DWNL_PATH_FILE_LENGTH DWNL_PATH_FILE_LEN + 32 +#define MAX_URL_LEN 512 +#define MAX_URL_LEN1 MAX_URL_LEN + 128 +``` + +**Original concern:** Operator precedence error if used in multiplication. + +**Validation analysis:** Both macros are only used in array declarations: +- `char download_path[DWNL_PATH_FILE_LENGTH];` (line 2849) +- `char imageHTTPURL[MAX_URL_LEN1];` (line 2676) +Array size declarations evaluate the full additive expression correctly β€” no precedence issue. +`sizeof(download_path)` is used later, which evaluates the actual array size, not the macro. +**No buffer overflow exists with current usage.** + +**Reassessment:** Downgraded from HIGH to LOW. The fix is trivially correct and good practice +(prevents future misuse in multiplication contexts), but no current code triggers the bug. + +**Fix:** +```c +#define DWNL_PATH_FILE_LENGTH (DWNL_PATH_FILE_LEN + 32) +#define MAX_URL_LEN1 (MAX_URL_LEN + 128) +``` + +--- + +### CID-03: NULL Pointer Dereference in `registerProcess()` Error Path (HIGH) β€” FIXED + +**File:** `rdkFwupdateMgr_process.c`, **Line:** ~307-308 +```c +if (!result) { + FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", + error->message); // ← error may be NULL + g_error_free(error); +``` + +**Problem:** If `g_dbus_proxy_call_sync()` returns `NULL` but fails to set `error` (e.g., extreme OOM), `error->message` dereferences a NULL pointer. The defensive pattern used in `create_dbus_proxy()` checks `(error && *error)` but is not applied here. + +**Fix:** +```c +if (!result) { + FWUPMGR_ERROR("RegisterProcess D-Bus call failed: %s\n", + error ? error->message : "unknown error (GError not set)"); + if (error) g_error_free(error); + g_object_unref(proxy); + return NULL; +} +``` + +--- + +### CID-04: NULL Pointer Dereference in `unregisterProcess()` Error Path (HIGH) β€” FIXED + +**File:** `rdkFwupdateMgr_process.c`, **Line:** ~531-534 + +Same pattern as CID-03. `error->message` accessed without NULL guard in `UnregisterProcess` D-Bus call failure path. + +**Fix:** Same as CID-03 β€” guard `error` before dereferencing. + +--- + +### CID-05: `clock_gettime()` Return Value Unchecked (MEDIUM) β€” FIXED + +**File:** `rdkFwupdateMgr_api.c`, **Line:** ~228 +```c +clock_gettime(CLOCK_REALTIME, &deadline); +``` + +**Problem:** `clock_gettime()` can fail (returns -1), leaving `deadline` with stale values. A corrupted deadline causes `pthread_cond_timedwait()` to return immediately or wait indefinitely. + +**Fix:** +```c +if (clock_gettime(CLOCK_REALTIME, &deadline) != 0) { + FWUPMGR_ERROR("checkForUpdate: clock_gettime failed (errno=%d)\n", errno); + pthread_join(worker_thread, NULL); + return CHECK_FOR_UPDATE_FAIL; +} +``` + +> **Note:** This same issue applies to `downloadFirmware()` and `updateFirmware()`. + +--- + +### CID-06: Duplicate Macro Definitions Across Files (MEDIUM) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 85-91 β€” also in `rdkFwupdateMgr_async_internal.h`, **Lines:** 110-112 + +```c +// Both files define: +#define DBUS_SERVICE_NAME "org.rdkfwupdater.Service" +#define DBUS_OBJECT_PATH "/org/rdkfwupdater/Service" +#define DBUS_INTERFACE_NAME "org.rdkfwupdater.Interface" +``` + +**Problem:** Duplicate macro definitions. While values are identical today (no compile error), if one is updated and the other is not, silent behavioral divergence occurs. Coverity reports as `MACRO_REDEFINED`. + +**Fix:** Remove duplicates in `rdkFwupdateMgr_process.c`; rely solely on `rdkFwupdateMgr_async_internal.h` which is already included. + +--- + +### CID-07: `DBUS_TIMEOUT_MS` vs `DBUS_TIMEOUT_MSEC` Naming Confusion (MEDIUM) + +- `rdkFwupdateMgr_async_internal.h` defines `DBUS_TIMEOUT_MS 5000` (5s) +- `rdkFwupdateMgr_process.c` defines `DBUS_TIMEOUT_MSEC 10000` (10s) + +**Problem:** These are different values for different purposes, but the near-identical names create a maintenance hazard. If a developer uses the wrong one, sync calls get 5s timeout (too short) or async calls get 10s timeout (too long). + +**Fix:** Rename for clarity: +```c +#define DBUS_ASYNC_CHECK_TIMEOUT_MS 5000 // For async worker D-Bus calls +#define DBUS_SYNC_METHOD_TIMEOUT_MS 10000 // For synchronous register/unregister +``` + +--- + +### CID-08: `fprintf(stderr)` Mixed with Logging Macros (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** ~292, 295 +```c +fprintf(stderr, "[rdkFwupdateMgr] D-Bus proxy created successfully\n"); +fprintf(stderr, "[rdkFwupdateMgr] Calling RegisterProcess D-Bus method...\n"); +``` + +**Problem:** All other logging uses `FWUPMGR_INFO`/`FWUPMGR_ERROR` macros, but these two lines use raw `fprintf(stderr)`. This bypasses log-level filtering and is likely leftover debug code. + +**Fix:** Replace with `FWUPMGR_INFO(...)` or remove entirely. + +--- + +### CID-09: Commented-Out Code Blocks (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 124-127 +```c +//typedef struct _FirmwareInterfaceContext { + // uint64_t handler_id; + // char handler_id_str[32]; +//} FirmwareInterfaceContext; +``` + +**File:** `rdkv_dbus_server.c`, **Line:** 96 +```c +//gboolean* stop_flag; // Atomic flag to signal thread shutdown +``` + +**File:** `xconf_comm_status.c`, **Line:** 162 +```c +// gboolean old_status = IsCheckUpdateInProgress; +``` + +**Problem:** Commented-out code indicates incomplete refactoring and is a Coverity/MISRA coding standards violation. + +**Fix:** Remove all commented-out code blocks. + +--- + +### CID-10: Duplicate `#define` Constants Across Server Files (LOW) β€” NOT APPLICABLE + +**File:** `rdkFwupdateMgr_handlers.c`, **Lines:** 162-165 β€” duplicated from definitions elsewhere +```c +#define CURL_PROGRESS_FILE "/opt/curl_progress" +#define PROGRESS_POLL_INTERVAL_MS 100 +#define PROGRESS_THROTTLE_PERCENT 1.0 +#define PROGRESS_MONITOR_TIMEOUT_SEC 600 +``` + +**Original concern:** Constants duplicated across files, risk of value divergence. + +**Validation analysis:** These constants are ONLY defined in `rdkFwupdateMgr_handlers.c`. They are NOT +duplicated in `rdkv_dbus_server.c` or any other file. The `ProgressMonitorContext` struct is defined in +`rdkv_dbus_server.c` but does not define these constants. **Finding is incorrect.** + +**Status:** ❌ Not applicable β€” constants are not duplicated. Centralizing in a header is still good +practice but is not a correctness issue. + +--- + +### CID-11: `xconf_status_initialized` Non-Atomic Read Before Mutex (LOW) + +**File:** `xconf_comm_status.c`, **Lines:** 84, 121, 156 +```c +if (xconf_status_initialized) { ... } // initXConfCommStatus +if (!xconf_status_initialized) { ... } // getXConfCommStatus +if (!xconf_status_initialized) { ... } // setXConfCommStatus +``` + +**Problem:** `xconf_status_initialized` is read without mutex protection. If `initXConfCommStatus()` were called from two threads simultaneously (violating the API contract), this would be a data race. The documentation says "NOT thread-safe. Call from main thread only" β€” but Coverity static analysis doesn't read comments. + +**Fix (defensive):** +```c +static volatile gboolean xconf_status_initialized = FALSE; +// Or use g_once_init_enter/g_once_init_leave for one-time initialization +``` + +--- + +### CID-12: Non-`extern` Declarations of External Functions (LOW) + +**File:** `rdkv_dbus_server.c`, **Lines:** 107-109 +```c +gboolean emit_flash_progress_idle(gpointer user_data); +gboolean cleanup_flash_state_idle(gpointer user_data); +gpointer rdkfw_flash_worker_thread(gpointer user_data); +``` + +**Problem:** These are forward declarations of functions implemented in `rdkFwupdateMgr_handlers.c` but declared without `extern` keyword in the .c file (not in a header). While C defaults to external linkage, this bypasses header-based type checking. + +**Fix:** Move these declarations to `rdkFwupdateMgr_handlers.h` and include the header, or add explicit `extern`. + +--- + +## 2. Memory Leak Analysis + +### LEAK-01: `AsyncXconfFetchContext` Leak on GTask Creation Failure (MEDIUM) β€” NOT APPLICABLE + +**File:** `rdkv_dbus_server.c`, in `CheckForUpdate` handler (around the async fetch path) + +**Original concern:** If `g_task_new()` returns NULL (OOM), the `handler_id` string inside the context leaks. + +**Validation analysis:** The code ALREADY handles GTask creation failure properly at lines 866-872: +```c +if (!task) { + SWLOG_ERROR("[CHECK_UPDATE] CRITICAL: g_task_new() returned NULL!\n"); + g_free(async_ctx->handler_id); + g_free(async_ctx); + setXConfCommStatus(FALSE); + return; +} +``` +The `handler_id` string and context are both freed on the failure path. **Finding is incorrect β€” cleanup +is already in place.** + +**Status:** ❌ Not applicable β€” GTask failure path already correctly frees all resources. + +--- + +### LEAK-02: `ProgressMonitorContext` Leak on Thread Creation Failure (MEDIUM) β€” NOT APPLICABLE + +**File:** `rdkv_dbus_server.c`, `rdkfw_download_worker()` β€” around progress monitor thread creation + +**Original concern:** If `g_thread_new()` fails, the allocated context members leak. + +**Validation analysis:** The code ALREADY handles thread creation failure properly at lines 3063-3080: +```c +if (monitor_thread == NULL) { + // Cleanup on thread creation failure + if (monitor_ctx->handler_id) { g_free(monitor_ctx->handler_id); } + if (monitor_ctx->firmware_name) { g_free(monitor_ctx->firmware_name); } + // Clear and free mutex... + g_free(monitor_ctx); + monitor_ctx = NULL; +} +``` +All `g_strdup`'d strings, the mutex, and the context itself are properly freed on the failure path. +**Finding is incorrect β€” cleanup is already in place.** + +**Status:** ❌ Not applicable β€” thread creation failure path already correctly frees all resources. + +--- + +### LEAK-03: `on_check_signal_handler()` Partial Parse Failure (MEDIUM) + +**File:** `rdkFwupdateMgr_async.c`, **Line:** ~342-349 +```c +if (!internal_parse_signal_data(parameters, &signal_data)) { + FWUPMGR_ERROR("on_check_signal_handler: parse failed\n"); + if (ctx->main_loop != NULL) { + g_main_loop_quit(ctx->main_loop); + } + return; // ← signal_data may have partial allocations +} +``` + +**Problem:** If `internal_parse_signal_data()` partially succeeds (allocates some strings but fails on others), partially-allocated fields leak because `internal_cleanup_signal_data()` is never called. + +**Fix:** +```c +if (!internal_parse_signal_data(parameters, &signal_data)) { + internal_cleanup_signal_data(&signal_data); // ← add + ... +} +``` + +> **Note:** Same pattern likely exists in `on_download_signal_handler()` and `on_update_signal_handler()`. + +--- + +### LEAK-04: `waiting_checkUpdate_ids` / `waiting_download_ids` Queue Cleanup (LOW) + +**File:** `rdkv_dbus_server.c`, **Lines:** 136-137 + +If the daemon shuts down while there are waiting clients in the queues, the `g_strdup`'d handler IDs in the GSList are never freed. The `cleanup_dbus()` / `cleanup_process_tracking()` functions should drain these queues. + +**Fix:** In `cleanup_dbus()` or daemon shutdown: +```c +g_slist_free_full(waiting_checkUpdate_ids, g_free); +waiting_checkUpdate_ids = NULL; +g_slist_free_full(waiting_download_ids, g_free); +waiting_download_ids = NULL; +``` + +--- + +### LEAK-05: `daemon_reject_message` in Worker Thread Exit Paths (LOW) + +**File:** `rdkFwupdateMgr_async.c` β€” download/update worker threads + +The field `ctx->daemon_reject_message` is set by the worker thread via `strdup`. All exit paths must ensure `free(ctx->daemon_reject_message)` is called. + +**Recommendation:** Audit all worker thread exit paths (success, failure, timeout, cancellation) to confirm cleanup. + +--- + +## 3. Thread Safety Analysis + +### βœ… Strengths + +| Component | Assessment | +|-----------|------------| +| **Client library** (`rdkFwupdateMgr_async.c`) | Excellent β€” all `g_*_in_progress` variables protected by `PTHREAD_MUTEX_INITIALIZER` mutexes with clean accessor pattern | +| **XConf status module** (`xconf_comm_status.c`) | Excellent β€” `trySetXConfCommStatus()` provides atomic CAS to prevent TOCTOU races | +| **XConf data cache** (`rdkFwupdateMgr_handlers.c`) | Good β€” `G_LOCK_DEFINE_STATIC(xconf_data_cache)` + `G_LOCK_DEFINE_STATIC(xconf_cache)` with `CACHE_UNLOCK_AND_RETURN` macro | +| **Per-request condvar handshake** (`rdkFwupdateMgr_api.c`) | Correct β€” `ready_mutex`/`ready_cond` per-context eliminates cross-request interference | +| **Callback invocation** | Correct β€” callbacks invoked with global mutex released (deadlock prevention) | + +### TS-01: `IsFlashInProgress` β€” Unprotected Cross-Thread Access (HIGH) + +*See CID-01 above.* This is both a Coverity issue and a thread-safety issue. The variable is read on the main GLib thread (D-Bus handler) and written from the flash worker thread cleanup idle callback. While `cleanup_flash_state_idle()` runs on the main loop (via `g_idle_add`), the worker thread itself may also access it directly. + +--- + +### TS-02: `current_download` Global State Pointer (MEDIUM) + +**File:** `rdkv_dbus_server.c`, **Line:** 132 +```c +static CurrentDownloadState *current_download = NULL; +``` + +**Problem:** This pointer is set when a download begins and cleared when it ends. The comment claims "Main thread only", but the download worker thread accesses it to emit progress signals. If the main thread clears `current_download` (e.g., on a D-Bus disconnection cleanup) while the worker thread is reading it, a use-after-free occurs. + +**Recommendation:** Protect with `G_LOCK_DEFINE_STATIC` or ensure all worker thread access goes through `g_idle_add` callbacks that run on the main loop. + +--- + +### TS-03: `pthread_cond_timedwait()` Uses `CLOCK_REALTIME` (LOW) + +**File:** `rdkFwupdateMgr_api.c`, **Line:** ~228 + +NTP time adjustments can cause spurious timeouts or extended waits. Consider `CLOCK_MONOTONIC` with `pthread_condattr_setclock()` in a future iteration. + +--- + +## 4. Race Condition Analysis + +### RACE-01: `IsFlashInProgress` TOCTOU in UpdateFirmware Handler (HIGH) + +**File:** `rdkv_dbus_server.c`, UpdateFirmware D-Bus handler + +``` +Thread 1 (main loop): if (IsFlashInProgress) β†’ FALSE + // context switch +Thread 2 (flash worker): g_idle_add(cleanup_flash_state_idle) β†’ sets IsFlashInProgress = FALSE +Thread 1 (main loop): IsFlashInProgress = TRUE; spawn_flash_worker() +``` + +**Problem:** Without atomic operations, the check-then-set on `IsFlashInProgress` is a TOCTOU race. Even though the comments claim "Main thread only" for the D-Bus handler, the variable is `extern` (non-static) and accessible by any thread. + +**Fix:** Use the same `trySet` CAS pattern as `trySetXConfCommStatus()`: +```c +// New module: flash_status.c (mirrors xconf_comm_status.c) +gboolean trySetFlashInProgress(void); +void clearFlashInProgress(void); +``` + +--- + +### RACE-02: `unregisterProcess()` TOCTOU with In-Progress Checks (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** ~421-462 + +```c +if (internal_is_check_in_progress()) { return; } +if (internal_is_dwnl_in_progress()) { return; } +if (internal_is_update_in_progress()) { return; } +// ... proceed with unregister D-Bus call +``` + +**Problem:** Between checking `internal_is_check_in_progress()` returning false and the D-Bus call, another thread could call `checkForUpdate()`. The three checks use separate mutexes. + +**Risk Assessment:** LOW β€” API docs state `unregisterProcess()` should only be called after all operations complete. This is a client-contract violation scenario. + +--- + +### RACE-03: Daemon Queue Processing After CheckForUpdate Completes (LOW) + +**File:** `rdkv_dbus_server.c`, `rdkfw_xconf_fetch_done()` β€” waiting client processing + +When `rdkfw_xconf_fetch_done()` processes the `waiting_checkUpdate_ids` queue, it calls `setXConfCommStatus(FALSE)` then iterates the queue. If a new `CheckForUpdate` D-Bus call arrives between `setXConfCommStatus(FALSE)` and queue processing completing, the new request could either start a second XConf fetch (contention) or be incorrectly queued. + +**Mitigation:** Since `rdkfw_xconf_fetch_done()` runs on the main loop and D-Bus handlers are serialized by GLib, this race cannot actually occur with the current single-threaded main loop architecture. **However**, document this constraint prominently β€” any future move to threaded D-Bus handlers would expose this. + +--- + +## 5. Critical Section Handling + +### βœ… Strengths β€” Client Library + +| Pattern | Assessment | +|---------|------------| +| `internal_begin_*()` / `internal_end_*()` / `internal_abort_*()` | Excellent β€” atomic state transitions under single mutex | +| Destructor cancel pattern (snapshot-under-lock) | Excellent β€” prevents use-after-free of ctx while joining thread | +| GMainLoop ref counting in destructor | Correct β€” prevents premature GMainLoop destruction | +| `internal_end_*()` called BEFORE `free(ctx)` | Correct ordering documented and enforced | + +### βœ… Strengths β€” Server Daemon + +| Pattern | Assessment | +|---------|------------| +| `G_LOCK_DEFINE_STATIC(xconf_data_cache)` | Correct β€” protects in-memory cache from concurrent read/write | +| `G_LOCK_DEFINE_STATIC(xconf_cache)` | Correct β€” protects file I/O cache operations | +| `CACHE_UNLOCK_AND_RETURN` macro | Excellent β€” ensures mutex released on all error paths | +| `trySetXConfCommStatus()` CAS pattern | Excellent β€” eliminates TOCTOU for CheckForUpdate | +| `initXConfCommStatus()` / `cleanupXConfCommStatus()` lifecycle | Correct β€” init before threads, cleanup after join | + +### CS-01: No `pthread_mutex_destroy()` for Client Library Static Mutexes (INFO) + +**File:** `rdkFwupdateMgr_async.c`, **Lines:** 71, 84, 97 + +These are statically-initialized with `PTHREAD_MUTEX_INITIALIZER` and never destroyed. Per POSIX, this is acceptable for static initialization. No action needed. + +--- + +## 6. Positive / Negative Test Case Coverage + +### βœ… Well-Handled Cases + +#### Client Library + +| Test Case | API | Status | +|-----------|-----|--------| +| NULL handle | `checkForUpdate`, `downloadFirmware`, `updateFirmware` | βœ… Returns FAIL | +| Empty handle | `checkForUpdate`, `downloadFirmware`, `updateFirmware` | βœ… Returns FAIL | +| NULL callback | `checkForUpdate`, `downloadFirmware` | βœ… Returns FAIL | +| NULL request struct | `downloadFirmware`, `updateFirmware` | βœ… Returns FAIL | +| NULL/empty firmwareName | `downloadFirmware` | βœ… Returns FAIL | +| Duplicate concurrent call | All APIs | βœ… Second call rejected | +| calloc / strdup failure | All APIs | βœ… Cascading cleanup | +| pthread_create failure | All APIs | βœ… `abort_*()` called | +| Worker init failure (D-Bus dead) | All APIs | βœ… join + return FAIL | +| Worker timeout (120s/3600s) | All APIs | βœ… Safety net timeout fires | +| NULL handle for unregister | `unregisterProcess` | βœ… No-op | +| Invalid handle string | `unregisterProcess` | βœ… Detected, rejected | + +#### Server Daemon + +| Test Case | Handler | Status | +|-----------|---------|--------| +| Unregistered handler_id | CheckForUpdate, DownloadFirmware, UpdateFirmware | βœ… Rejected with D-Bus error | +| Duplicate process name | RegisterProcess | βœ… Rejected with reason | +| Empty process name | RegisterProcess | βœ… Validation rejects | +| Process name > 256 chars | RegisterProcess | βœ… Validation rejects | +| XConf cache hit | CheckForUpdate | βœ… Returns cached result immediately | +| XConf cache miss | CheckForUpdate | βœ… Spawns async GTask | +| Concurrent CheckForUpdate | CheckForUpdate | βœ… Queued via `waiting_checkUpdate_ids` | +| Concurrent Download | DownloadFirmware | βœ… Queued via `waiting_download_ids` | + +### ⚠️ Missing / Incomplete Test Cases + +#### TC-01: XConf Server Returns Malformed JSON (MEDIUM) + +**File:** `rdkFwupdateMgr_handlers.c`, `fetch_xconf_firmware_info()` + +If the XConf server returns HTTP 200 with invalid JSON, the parsing function should return a clear error. Verify that `json_process.h` functions handle: +- Empty body +- Truncated JSON +- Valid JSON but missing required fields + +**Recommendation:** Add defensive checks after JSON parsing to validate all required fields before populating `XCONFRES`. + +--- + +#### TC-02: D-Bus Disconnection During Async Operation (MEDIUM) + +**Files:** `rdkv_dbus_server.c` + `rdkFwupdateMgr_async.c` + +If the D-Bus connection drops while: +- A GTask worker is running (XConf fetch, download) +- A client worker thread is waiting for a signal + +**Server side:** The GTask completion callback may not fire. `IsCheckUpdateInProgress` / `IsDownloadInProgress` would remain TRUE permanently. + +**Client side:** The worker thread's `g_main_loop_run()` should exit when the connection is lost, but verify the signal subscription handles disconnection gracefully. + +**Recommendation:** Add a D-Bus connection closed handler that clears all in-progress flags and drains waiting queues. + +--- + +#### TC-03: Client Callback Crashes During Execution (MEDIUM) + +**File:** `rdkFwupdateMgr_async.c`, signal handlers + +If the client's callback segfaults or throws a C++ exception, the worker thread dies without calling `internal_end_check()`, permanently locking the `g_check_in_progress` flag to `true`. + +**Recommendation:** Document as a known limitation. Consider wrapping callbacks in a signal handler or adding an `atexit` cleanup. + +--- + +#### TC-04: Firmware Download with Zero-Length File (LOW) + +**File:** `rdkv_dbus_server.c`, `rdkfw_download_worker()` + +If the download completes but the file is 0 bytes (corrupt CDN response, network truncation), verify the completion handler detects this as an error rather than reporting `DOWNLOAD_SUCCESS`. + +--- + +#### TC-05: Flash Worker Receives Signal During flashImage() (LOW) + +**File:** `rdkFwupdateMgr_handlers.c`, `rdkfw_flash_worker_thread()` + +If the daemon receives SIGTERM while `flashImage()` is executing, the flash operation may be interrupted mid-write, potentially bricking the device. Verify that signal handlers defer shutdown until flash completes. + +--- + +## 7. Buffer Overflow / Underflow Analysis + +### βœ… Strengths + +| Pattern | Assessment | +|---------|------------| +| `strncpy` with `sizeof(buf) - 1` | βœ… Correct (used in `fwinfo_data` population) | +| Explicit null-termination after `strncpy` | βœ… Correct | +| `snprintf` for string formatting | βœ… Bounded | +| `g_strdup` / `g_strdup_printf` for heap strings | βœ… Correct (GLib handles allocation) | +| `calloc()` for struct allocation | βœ… Zero-initialized | +| `memcpy` with `sizeof(struct)` for cache copy | βœ… Fixed-size struct copy | + +### BUF-01: Unparenthesized Macros Create Buffer Underallocation Risk (HIGH) + +*See CID-02 above.* `DWNL_PATH_FILE_LENGTH` and `MAX_URL_LEN1` without parentheses can cause undersized buffers when used in multiplication expressions. + +--- + +### BUF-02: `malloc(32)` Magic Number for Handler ID String (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Line:** ~323 +```c +handle_str = (char*)malloc(32); +``` + +**Problem:** 32 is a magic number. `PRIu64` produces max 20 digits (`18446744073709551615`), so 32 is sufficient, but the intent is unclear. + +**Fix:** +```c +#define HANDLER_ID_STR_SIZE 32 /* uint64 max = 20 digits + NUL + margin */ +handle_str = (char *)malloc(HANDLER_ID_STR_SIZE); +snprintf(handle_str, HANDLER_ID_STR_SIZE, "%" PRIu64, handler_id); +``` + +--- + +### BUF-03: `JSON_STR_LEN` Hardcoded at 1000 Bytes (LOW) + +**File:** `rdkFwupdateMgr_handlers.c`, **Line:** 57 +```c +#define JSON_STR_LEN 1000 +``` + +**Problem:** If the XConf JSON response exceeds 1000 bytes (realistic for complex firmware metadata), any stack buffer sized with `JSON_STR_LEN` would overflow. Verify all uses dynamically allocate or use `g_malloc` with proper sizing. + +--- + +## Additional Observations + +### OBS-01: `validate_process_name()` Calls `strlen()` Twice (LOW) + +**File:** `rdkFwupdateMgr_process.c`, **Lines:** 200, 205 +```c +if (strlen(processName) == 0) { ... } +if (strlen(processName) > MAX_PROCESS_NAME_LEN) { ... } +``` + +**Fix:** Cache the result: `size_t len = strlen(processName);` + +--- + +### OBS-02: File Uses Windows Line Endings (CRLF) (INFO) + +**Files:** `rdkFwupdateMgr_process.c`, `xconf_comm_status.c`, `xconf_comm_status.h` + +These files use `\r\n` (CRLF) while other files use `\n` (LF). Mixed line endings cause noisy diffs and potential build issues on some systems. + +**Fix:** Normalize to LF. Add `.gitattributes`: +``` +*.c text eol=lf +*.h text eol=lf +``` + +--- + +### OBS-03: 3400+ Line File (`rdkv_dbus_server.c`) β€” Maintainability Concern (INFO) + +At ~149 KB, `rdkv_dbus_server.c` is extremely large. Consider splitting into: +- `rdkv_dbus_process_mgmt.c` β€” RegisterProcess/UnregisterProcess + tracking +- `rdkv_dbus_check_update.c` β€” CheckForUpdate handler + XConf fetch +- `rdkv_dbus_download.c` β€” DownloadFirmware handler + progress monitoring +- `rdkv_dbus_update.c` β€” UpdateFirmware handler + flash management +- `rdkv_dbus_server.c` β€” Main loop, D-Bus registration, signal setup + +--- + +## Summary of Required Actions + +### πŸ”΄ HIGH Priority β€” Must Fix Before Merge + +| ID | File | Description | Status | +|----|------|-------------|--------| +| CID-01 / RACE-01 | `rdkv_dbus_server.c:133` | `IsFlashInProgress` needs mutex or atomic protection | **REASSESSED LOW** β€” GLib main loop serializes all access; no real data race | +| CID-02 / BUF-01 | `rdkv_dbus_server.c:60-62` | Add parentheses to `DWNL_PATH_FILE_LENGTH` and `MAX_URL_LEN1` macros | **REASSESSED LOW** β€” only used in array declarations; no precedence issue | +| CID-03 | `rdkFwupdateMgr_process.c:~307` | NULL deref: guard `error->message` in RegisterProcess failure | βœ… **FIXED** | +| CID-04 | `rdkFwupdateMgr_process.c:~531` | NULL deref: guard `error->message` in UnregisterProcess failure | βœ… **FIXED** | + +### 🟑 MEDIUM Priority β€” Strongly Recommended + +| ID | File | Description | Status | +|----|------|-------------|--------| +| CID-05 | `rdkFwupdateMgr_api.c:~228` | Check `clock_gettime()` return value (all 3 APIs) | βœ… **FIXED** | +| CID-06 | `rdkFwupdateMgr_process.c:85-91` | Remove duplicate D-Bus macro definitions | πŸ”² Open (LOW risk) | +| CID-07 | `_async_internal.h` / `_process.c` | Rename `DBUS_TIMEOUT_MS` vs `DBUS_TIMEOUT_MSEC` to avoid confusion | πŸ”² Open (LOW risk) | +| TS-02 | `rdkv_dbus_server.c:132` | Protect `current_download` pointer access | πŸ”² Open | +| LEAK-01 | `rdkv_dbus_server.c` | `AsyncXconfFetchContext` leak on GTask failure | ❌ **NOT APPLICABLE** β€” code already frees resources | +| LEAK-02 | `rdkv_dbus_server.c` | `ProgressMonitorContext` leak on thread creation failure | ❌ **NOT APPLICABLE** β€” code already frees resources | +| LEAK-03 | `rdkFwupdateMgr_async.c:~342` | Partial signal data leak on parse failure | ❌ **NOT APPLICABLE** β€” struct uses char[] not char* | +| TC-02 | `rdkv_dbus_server.c` | Add D-Bus connection-closed handler to clear in-progress flags | πŸ”² Open (test coverage) | + +### 🟒 LOW Priority β€” Follow-Up Items + +| ID | File | Description | Status | +|----|------|-------------|--------| +| CID-08 | `rdkFwupdateMgr_process.c:~292` | Replace `fprintf(stderr)` with `FWUPMGR_INFO` | πŸ”² Open | +| CID-09 | Multiple files | Remove all commented-out code blocks | πŸ”² Open | +| CID-10 | `rdkFwupdateMgr_handlers.c:162-165` | Centralize progress monitoring constants | ❌ **NOT APPLICABLE** β€” constants not duplicated | +| CID-11 | `xconf_comm_status.c:84` | Consider `volatile` or `g_once_init` for init flag | πŸ”² Open | +| CID-12 | `rdkv_dbus_server.c:107-109` | Move extern declarations to header | πŸ”² Open | +| LEAK-04 | `rdkv_dbus_server.c:136-137` | Free waiting queues on daemon shutdown | πŸ”² Open | +| BUF-02 | `rdkFwupdateMgr_process.c:~323` | Replace magic number 32 with named constant | πŸ”² Open | +| OBS-01 | `rdkFwupdateMgr_process.c:200` | Cache `strlen()` result | πŸ”² Open | +| OBS-02 | Multiple files | Normalize line endings (CRLF β†’ LF) | πŸ”² Open | +| OBS-03 | `rdkv_dbus_server.c` | Consider splitting 3400+ line file | πŸ”² Open | + +--- + +## Conclusion + +The codebase demonstrates **strong architectural design** overall: + +- **Client library** (`librdkFwupdateMgr/src/`): Excellent thread safety with well-encapsulated mutex patterns, clean per-request context lifecycle, and comprehensive input validation. The on-demand worker thread design is well-executed. + +- **Server daemon** (`src/dbus/`): Good async task management with GTask, proper XConf cache synchronization via `G_LOCK_DEFINE_STATIC`, and well-designed queue piggybacking for concurrent clients. The `xconf_comm_status` module is a clean example of proper thread-safe state management. + +- **Key fixes applied**: CID-03/CID-04 (NULL pointer dereference guards in `registerProcess()`/`unregisterProcess()`) and CID-05 (`clock_gettime()` return value checked with fallback in all 3 API functions) have been fixed and verified. CID-01/CID-02 have been reassessed from HIGH to LOW after validating that GLib main loop serialization prevents the theoretical race/precedence issues in current usage. + +- **Findings validated as NOT APPLICABLE**: LEAK-01 (GTask failure path already frees resources), LEAK-02 (thread creation failure path already frees resources), LEAK-03 (signal data struct uses `char[]` not `char*`), CID-10 (constants not actually duplicated). + +- **Remaining items** are all LOW priority: code cleanup (commented-out code, `fprintf(stderr)`, magic numbers), naming consistency, line ending normalization, and maintainability improvements (splitting large files). These are suitable for follow-up PRs. diff --git a/docs/KNOWWHEREITBREAKS_README.md b/docs/KNOWWHEREITBREAKS_README.md new file mode 100755 index 00000000..737078b0 --- /dev/null +++ b/docs/KNOWWHEREITBREAKS_README.md @@ -0,0 +1,580 @@ +# KnowWhereItBreaks (KWIB) β€” Developer Test Utility + +**Source:** `librdkFwupdateMgr/examples/KnowWhereItBreaks.c` +**Binary:** `KnowWhereItBreaks` +**Version:** 1.0 +**Date:** April 2026 + +--- + +## Table of Contents + +1. [What Is It?](#1-what-is-it) +2. [KWIB vs example\_app β€” What's the Difference?](#2-kwib-vs-example_app--whats-the-difference) +3. [Prerequisites](#3-prerequisites) +4. [Build](#4-build) +5. [Usage](#5-usage) + - [Interactive Mode](#51-interactive-mode) + - [Automated Modes (CI/Scripts)](#52-automated-modes-ciscripts) +6. [Test Suites](#6-test-suites) + - [Error / Validation Tests](#61-error--validation-tests) + - [Happy Path Tests](#62-happy-path-tests) + - [Lifecycle Tests](#63-lifecycle-tests) +7. [Complete Test Case Catalog](#7-complete-test-case-catalog) +8. [Understanding the Output](#8-understanding-the-output) +9. [What Each Test Validates](#9-what-each-test-validates) +10. [Troubleshooting](#10-troubleshooting) +11. [Adding New Tests](#11-adding-new-tests) + +--- + +## 1. What Is It? + +**KnowWhereItBreaks** (KWIB) is a developer test binary that exercises every code path in `librdkFwupdateMgr.so` β€” the firmware update client library. It's your **find-the-bugs-before-they-find-you** tool. + +Think of it this way: + +| Tool | Purpose | Audience | +|------|---------|----------| +| `example_app` | "Here's how to use the library" | External teams, new developers | +| **`KnowWhereItBreaks`** | "Here's how to **break** the library" | Library developers, QA, CI | + +KWIB systematically tests: + +- βœ… **Input validation** β€” What happens when you pass NULL, empty strings, garbage? +- βœ… **Concurrency guards** β€” What happens when you call the same API twice simultaneously? +- βœ… **Session guards** β€” What happens when you unregister while an operation is running? +- βœ… **Happy paths** β€” Do register, check, download, and flash actually work end-to-end? +- βœ… **Callback correctness** β€” Does the callback fire the right number of times with valid data? +- βœ… **Progress monotonicity** β€” Do download/flash progress percentages always go up, never backward? +- βœ… **Rapid retry** β€” Can you immediately call an API again after the previous one completes? +- βœ… **Full lifecycle** β€” Register β†’ Check β†’ Download β†’ Flash β†’ Unregister in one shot + +--- + +## 2. KWIB vs example_app β€” What's the Difference? + +| Aspect | `example_app` | `KnowWhereItBreaks` | +|--------|--------------|---------------------| +| **Purpose** | Clean reference app for external teams | Comprehensive developer test utility | +| **Approach** | One happy path, start to finish | 39 test cases covering every edge case | +| **Error injection** | None β€” expects everything to work | Deliberately passes NULL, empty, duplicates | +| **Daemon required?** | Yes β€” always | Partially β€” error tests run without daemon | +| **Output** | Pretty workflow boxes | PASS/FAIL/SKIP with test counts | +| **Modes** | Run and watch | Interactive menu + 4 automated modes | +| **CI friendly?** | No (interactive) | Yes (`--auto-all` returns exit code 0/1) | +| **Tests guards?** | No | Yes β€” duplicate calls, unregister during op | +| **Tests callbacks?** | Informational print | Validates count, data, monotonicity | + +**Rule of thumb:** Use `example_app` to *see how things work*. Use `KnowWhereItBreaks` to *make sure they still work after you change something*. + +--- + +## 3. Prerequisites + +### The firmware daemon must be running (for happy path / lifecycle tests) + +```bash +systemctl status rdkFwupdateMgr +# If not running: +systemctl start rdkFwupdateMgr +``` + +> **Note:** Error/validation tests (TC03, TC06–TC08, TC13–TC17, TC22, TC24–TC28, TC33) run without the daemon. They only test the library's local input validation. + +### The library must be installed and findable + +```bash +ls -l /usr/lib/librdkFwupdateMgr.so* + +# If in a non-standard path: +export LD_LIBRARY_PATH=/path/to/lib:$LD_LIBRARY_PATH +``` + +### D-Bus permissions must allow the call + +```bash +cat /etc/dbus-1/system.d/rdkFwupdateMgr.conf +# Ensure your user (or root) is permitted +``` + +--- + +## 4. Build + +### Quick native build on-device + +```bash +cd librdkFwupdateMgr/examples + +gcc KnowWhereItBreaks.c \ + -o KnowWhereItBreaks \ + -I../include \ + -L/usr/lib \ + -lrdkFwupdateMgr \ + $(pkg-config --cflags --libs gio-2.0) \ + -lpthread +``` + +### Through autotools (cross-build / Yocto) + +```bash +make KnowWhereItBreaks +make install # installs to $(DESTDIR)$(bindir)/KnowWhereItBreaks +``` + +--- + +## 5. Usage + +### 5.1 Interactive Mode + +```bash +./KnowWhereItBreaks +``` + +This shows a menu where you can run individual tests or full suites: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ KnowWhereItBreaks v1.0 β€” librdkFwupdateMgr Test Utility β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Handle: (not registered) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ AUTOMATED SUITES β”‚ +β”‚ 10 All Error/Validation Tests (fast) β”‚ +β”‚ 11 All Happy Path Tests (daemon needed) β”‚ +β”‚ 12 Full Lifecycle Tests (daemon needed) β”‚ +β”‚ 13 ALL Tests (everything) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ REGISTER / UNREGISTER CHECKFORUPDATE β”‚ +β”‚ 1 TC01 Register happy 5 TC05 Check happy β”‚ +β”‚ 2 TC02 Unregister happy 6 TC06 NULL handle β”‚ +β”‚ ... ... β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ DOWNLOAD FIRMWARE UPDATE FIRMWARE β”‚ +β”‚ 50 TC12 Download happy 60 TC23 Update happy β”‚ +β”‚ ... ... β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ GUARDS / LIFECYCLE β”‚ +β”‚ 80 TC34 Unreg during check 90 TC37 Full lifecycle β”‚ +β”‚ ... ... β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 0 Exit (print results) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + Choice: _ +``` + +Type a number and press Enter to run that test. Type `0` to exit and see the summary. + +### 5.2 Automated Modes (CI/Scripts) + +For CI pipelines, cron jobs, or scripted testing, use command-line flags: + +| Command | What It Runs | Daemon Needed? | Duration | +|---------|-------------|----------------|----------| +| `./KnowWhereItBreaks --auto-error` | All error/validation + guard tests | Partially (registration needs daemon) | ~30s | +| `./KnowWhereItBreaks --auto-happy` | All happy path tests | Yes | ~15–20 min | +| `./KnowWhereItBreaks --full-lifecycle` | End-to-end lifecycle tests | Yes | ~20–30 min | +| `./KnowWhereItBreaks --auto-all` | Everything (error + happy + lifecycle) | Yes | ~40–60 min | + +**Exit code:** +- `0` = All tests passed (or skipped) +- `1` = One or more tests failed + +**CI example:** +```bash +#!/bin/bash +systemctl start rdkFwupdateMgr +sleep 2 +./KnowWhereItBreaks --auto-all +exit $? +``` + +--- + +## 6. Test Suites + +### 6.1 Error / Validation Tests (`--auto-error`) + +These tests verify the library **rejects bad inputs immediately** without crashing, leaking, or talking to the daemon unnecessarily. + +**What runs (in order):** + +| Phase | Tests | What It Checks | +|-------|-------|----------------| +| Input validation (no daemon) | TC03, TC06, TC08, TC13, TC22, TC24, TC33 | NULL handle, empty handle β†’ FAIL | +| Input validation (with daemon) | TC07, TC14–TC17, TC25–TC28 | NULL callback, NULL request, NULL/empty firmware name β†’ FAIL | +| Duplicate call guards | TC09, TC18, TC29 | Second concurrent call to same API β†’ rejected | +| Unregister guards | TC34, TC35, TC36 | Unregister during active check/download/update β†’ blocked | + +**Typical time:** ~30 seconds (most tests return immediately) + +### 6.2 Happy Path Tests (`--auto-happy`) + +These tests verify **everything works correctly when inputs are valid** and the daemon is running. + +**What runs (in order):** + +| Phase | Tests | What It Checks | +|-------|-------|----------------| +| Register | TC01, TC04 | Single and double registration succeed | +| CheckForUpdate | TC05, TC11, TC10 | Callback fires once with valid data; rapid retry works | +| DownloadFirmware | TC12, TC20, TC21, TC19 | Download completes; progress is monotonic; terminal status correct; rapid retry works | +| UpdateFirmware | TC23, TC31, TC32, TC30 | Flash completes; progress is monotonic; terminal status correct; rapid retry works | +| Unregister | TC02 | Clean unregistration | + +**Typical time:** 15–20 minutes (depends on firmware download/flash speed) + +### 6.3 Lifecycle Tests (`--full-lifecycle`) + +These tests verify the **complete workflow** works end-to-end. + +| Test | What It Does | +|------|-------------| +| TC37 | Register β†’ Check β†’ Download β†’ Flash β†’ Unregister (with `sleep(1)` between steps) | +| TC38 | Same as TC37, but **no sleeps** between steps (stress test for worker cleanup timing) | +| TC39 | Start `checkForUpdate()` and `downloadFirmware()` simultaneously (independent guards) | + +**Typical time:** 20–30 minutes + +--- + +## 7. Complete Test Case Catalog + +### Register / Unregister + +| ID | Menu # | Test Name | Input | Expected Result | Daemon? | +|----|--------|-----------|-------|-----------------|---------| +| TC01 | 1 | Register Happy Path | Valid name + version | Non-NULL handle returned | Yes | +| TC02 | 2 | Unregister Happy Path | Valid handle | Completes without crash | Yes | +| TC03 | 3 | Unregister NULL Handle | `NULL` | No crash (no-op) | No | +| TC04 | 4 | Double Register | Two different process names | Both get unique handles | Yes | + +### CheckForUpdate + +| ID | Menu # | Test Name | Input | Expected Result | Daemon? | +|----|--------|-----------|-------|-----------------|---------| +| TC05 | 5 | Check Happy Path | Valid handle + callback | Callback fires with valid FwInfoData | Yes | +| TC06 | 6 | NULL Handle | `NULL` handle | Returns `CHECK_FOR_UPDATE_FAIL` | No | +| TC07 | 7 | NULL Callback | `NULL` callback | Returns `CHECK_FOR_UPDATE_FAIL` | Yes* | +| TC08 | 8 | Empty Handle | `""` | Returns `CHECK_FOR_UPDATE_FAIL` | No | +| TC09 | 9 | Duplicate Call | Call twice simultaneously | Second call returns FAIL | Yes | +| TC10 | 40 | Rapid Retry | Call again immediately after first completes | Second call succeeds | Yes | +| TC11 | 41 | Callback Data Validation | Normal check | Callback fires exactly once; status in valid range | Yes | + +*\* Needs a registered handle, which needs daemon* + +### DownloadFirmware + +| ID | Menu # | Test Name | Input | Expected Result | Daemon? | +|----|--------|-----------|-------|-----------------|---------| +| TC12 | 50 | Download Happy Path | Valid request | Download completes (DWNL_COMPLETED) | Yes | +| TC13 | 51 | NULL Handle | `NULL` handle | Returns `RDKFW_DWNL_FAILED` | No | +| TC14 | 52 | NULL Request | `NULL` FwDwnlReq | Returns `RDKFW_DWNL_FAILED` | Yes* | +| TC15 | 53 | NULL Callback | `NULL` callback | Returns `RDKFW_DWNL_FAILED` | Yes* | +| TC16 | 54 | NULL Firmware Name | `firmwareName = NULL` | Returns `RDKFW_DWNL_FAILED` | Yes* | +| TC17 | 55 | Empty Firmware Name | `firmwareName = ""` | Returns `RDKFW_DWNL_FAILED` | Yes* | +| TC18 | 56 | Duplicate Call | Call twice simultaneously | Second call returns FAIL | Yes | +| TC19 | 57 | Rapid Retry | Call again after first completes | Second call succeeds | Yes | +| TC20 | 58 | Progress Monotonicity | (uses TC12 data) | Progress never decreases | β€” | +| TC21 | 59 | Terminal Status | (uses TC12 data) | Last callback has COMPLETED or ERROR | β€” | +| TC22 | β€” | Empty Handle | `""` handle | Returns `RDKFW_DWNL_FAILED` | No | + +### UpdateFirmware + +| ID | Menu # | Test Name | Input | Expected Result | Daemon? | +|----|--------|-----------|-------|-----------------|---------| +| TC23 | 60 | Update Happy Path | Valid request | Flash completes (UPDATE_COMPLETED) | Yes | +| TC24 | 61 | NULL Handle | `NULL` handle | Returns `RDKFW_UPDATE_FAILED` | No | +| TC25 | 62 | NULL Request | `NULL` FwUpdateReq | Returns `RDKFW_UPDATE_FAILED` | Yes* | +| TC26 | 63 | NULL Callback | `NULL` callback | Returns `RDKFW_UPDATE_FAILED` | Yes* | +| TC27 | 64 | NULL Firmware Name | `firmwareName = NULL` | Returns `RDKFW_UPDATE_FAILED` | Yes* | +| TC28 | 65 | Empty Firmware Name | `firmwareName = ""` | Returns `RDKFW_UPDATE_FAILED` | Yes* | +| TC29 | 66 | Duplicate Call | Call twice simultaneously | Second call returns FAIL | Yes | +| TC30 | 67 | Rapid Retry | Call again after first completes | Second call succeeds | Yes | +| TC31 | 68 | Progress Monotonicity | (uses TC23 data) | Progress never decreases | β€” | +| TC32 | 69 | Terminal Status | (uses TC23 data) | Last callback has COMPLETED or ERROR | β€” | +| TC33 | 70 | Empty Handle | `""` handle | Returns `RDKFW_UPDATE_FAILED` | No | + +### Guard Tests + +| ID | Menu # | Test Name | What It Does | Expected Result | Daemon? | +|----|--------|-----------|-------------|-----------------|---------| +| TC34 | 80 | Unregister During Check | Start check, then immediately unregister | Unregister blocked; callback still fires | Yes | +| TC35 | 81 | Unregister During Download | Start download, then immediately unregister | Unregister blocked; callbacks still fire | Yes | +| TC36 | 82 | Unregister During Update | Start update, then immediately unregister | Unregister blocked; callbacks still fire | Yes | + +### Full Lifecycle + +| ID | Menu # | Test Name | Steps | Daemon? | +|----|--------|-----------|-------|---------| +| TC37 | 90 | Full Lifecycle | Register β†’ Check β†’ Download β†’ Flash β†’ Unregister (with sleeps) | Yes | +| TC38 | 91 | No-Sleep Lifecycle | Same as TC37 but no sleeps between steps | Yes | +| TC39 | 92 | Simultaneous Check + Download | Start check and download at the same time | Yes | + +--- + +## 8. Understanding the Output + +### Test result indicators + +``` + [PASS] TC06 β€” NULL handle rejected ← Green: test passed + [FAIL] TC05 β€” checkForUpdate() β€” Callback never fired (130s timeout) + ← Red: test failed (with reason) + [SKIP] TC12 β€” No handle ← Yellow: test skipped (precondition not met) + [INFO] Waiting for callback (max 130s)... ← Informational message +``` + +### Callback trace + +During active operations, you'll see real-time callback output: + +``` + [CB:Check] #1 status=0 current='RDKV_2.5.0' + [CB:Dwnl] #1 progress=0% status=0 + [CB:Dwnl] #2 progress=25% status=0 + [CB:Dwnl] #3 progress=50% status=0 + [CB:Dwnl] #4 progress=75% status=0 + [CB:Dwnl] #5 progress=100% status=1 + [CB:Update] #1 progress=0% status=0 + [CB:Update] #2 progress=100% status=1 +``` + +### Final summary + +``` +══════════════════════════════════════════════════════════════ + KnowWhereItBreaks β€” TEST RESULTS +══════════════════════════════════════════════════════════════ + Total: 39 + Passed: 37 + Failed: 0 + Skipped: 2 +══════════════════════════════════════════════════════════════ + βœ… ALL TESTS PASSED +══════════════════════════════════════════════════════════════ +``` + +--- + +## 9. What Each Test Validates + +### Layer 1: Library Input Validation (no IPC at all) + +These tests verify the library rejects bad inputs **before** any D-Bus call happens. + +``` +Your App Library Daemon +──────── ─────── ────── +checkForUpdate(NULL) ──▢ if (handle == NULL) + return FAIL; (never contacted) +``` + +**Tests:** TC03, TC06, TC07, TC08, TC13–TC17, TC22, TC24–TC28, TC33 + +### Layer 2: Library Concurrency Guards + +These tests verify the library rejects duplicate concurrent calls. + +``` +Thread A: checkForUpdate() β†’ SUCCESS (worker running) +Thread B: checkForUpdate() β†’ FAIL (g_check_in_progress == true) +``` + +**Tests:** TC09, TC18, TC29 + +### Layer 3: Library Session Guards + +These tests verify the library blocks `unregisterProcess()` while operations are active. + +``` +checkForUpdate() β†’ SUCCESS (worker running) +unregisterProcess() β†’ BLOCKED (internal_is_check_in_progress() == true) + Callback still fires normally. +``` + +**Tests:** TC34, TC35, TC36 + +### Layer 4: Condvar Handshake + Daemon Communication + +These tests verify the worker thread starts correctly, talks to the daemon, and the return value accurately reflects the daemon's reply. + +**Tests:** TC01, TC04, TC05, TC12, TC23 + +### Layer 5: Callback Correctness + +These tests verify callbacks fire the right number of times with valid data. + +| API | Expected callback count | Validated by | +|-----|------------------------|-------------| +| `checkForUpdate` | Exactly 1 | TC11 | +| `downloadFirmware` | Multiple (β‰₯2), monotonically increasing progress | TC20, TC21 | +| `updateFirmware` | Multiple (β‰₯2), monotonically increasing progress | TC31, TC32 | + +### Layer 6: Worker Thread Cleanup + +These tests verify that after an operation completes, the in-progress flag is properly cleared and a new call can proceed immediately. + +**Tests:** TC10, TC19, TC30, TC38 + +### Layer 7: Full End-to-End + +These tests verify the complete register β†’ check β†’ download β†’ flash β†’ unregister workflow. + +**Tests:** TC37, TC38, TC39 + +--- + +## 10. Troubleshooting + +### All tests show SKIP + +**Cause:** Daemon is not running. `registerProcess()` returns NULL, so all subsequent tests are skipped. + +**Fix:** +```bash +systemctl start rdkFwupdateMgr +``` + +### TC05/TC12/TC23 timeout (callback never fired) + +**Possible causes:** +1. Daemon accepted the request but the operation failed silently +2. D-Bus signal was emitted but not received (subscription issue) +3. Worker thread crashed before firing the callback + +**Debug steps:** +```bash +# Check daemon logs +journalctl -u rdkFwupdateMgr -f + +# Check D-Bus traffic +sudo dbus-monitor --system "interface='org.rdkfwupdater.Interface'" + +# Check for crashed worker threads +# (look for FWUPMGR_ERROR messages in stderr) +./KnowWhereItBreaks --auto-happy 2>&1 | grep -i error +``` + +### TC09/TC18/TC29 FAIL ("Second call was NOT rejected") + +**Cause:** The in-progress guard is broken. The second call was accepted while the first is still running. + +**This is a real bug.** Check `internal_begin_check()` / `internal_begin_download()` / `internal_begin_update()` in `rdkFwupdateMgr_async.c`. + +### TC10/TC19/TC30 FAIL ("Rejected β€” in-progress flag not cleared?") + +**Cause:** The worker thread from the previous operation didn't call `internal_end_*()` before exiting. The in-progress flag is stuck at `true`. + +**This is a real bug.** Check the worker thread cleanup path in `rdkFwupdateMgr_async.c` β€” ensure `internal_end_*()` is called in ALL exit paths (success, failure, timeout). + +### TC34/TC35/TC36 FAIL ("Callback never fired β€” unregister may have succeeded") + +**Cause:** The `unregisterProcess()` guard (`internal_is_*_in_progress()`) didn't block the unregister, and the handle was freed while the worker was still using it. + +**This is a real bug.** Check the guard sequence in `rdkFwupdateMgr_process.c` `unregisterProcess()`. + +### TC20/TC31 FAIL ("Progress decreased at some point") + +**Cause:** The daemon emitted a progress signal with a lower percentage than a previous one (e.g., went from 50% back to 25%). + +**This is a daemon bug.** Check the progress monitoring logic in `rdkFwupdateMgr_handlers.c`. + +--- + +## 11. Adding New Tests + +To add a new test case: + +### 1. Write the test function + +```c +static void tc40_my_new_test(void) +{ + printf("\n--- TC40: My New Test ---\n"); + + // Setup + if (!ensure_registered()) { TEST_SKIP("TC40", "No handle"); return; } + + // Action + // ... your test logic ... + + // Assertion + if (/* success condition */) + TEST_PASS("TC40 β€” description of what passed"); + else + TEST_FAIL("TC40 β€” description", "what went wrong"); +} +``` + +### 2. Add it to the appropriate suite runner + +```c +static void run_happy_tests(void) +{ + // ...existing tests... + tc40_my_new_test(); // ← add here + sleep(2); +} +``` + +### 3. Add it to the interactive menu switch + +```c +case 93: tc40_my_new_test(); break; +``` + +### 4. Update the menu display + +```c +printf("β”‚ 93 TC40 My new test β”‚\n"); +``` + +### Test naming convention + +- `tcXX__` β€” e.g., `tc40_download_null_url` +- Use `TEST_PASS` / `TEST_FAIL` / `TEST_SKIP` macros for consistent output +- Always call `reset_all()` before tests that use callbacks +- Always call `ensure_registered()` if the test needs a handle +- Use `wait_flag()` with appropriate timeouts for async operations + +--- + +## Quick Reference + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ KnowWhereItBreaks Quick Reference β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ RUN MODES: β”‚ +β”‚ ./KnowWhereItBreaks Interactive menu β”‚ +β”‚ ./KnowWhereItBreaks --auto-error Error tests only (~30s) β”‚ +β”‚ ./KnowWhereItBreaks --auto-happy Happy paths (~15-20min) β”‚ +β”‚ ./KnowWhereItBreaks --full-lifecycle End-to-end (~20-30min) β”‚ +β”‚ ./KnowWhereItBreaks --auto-all Everything (~40-60min) β”‚ +β”‚ β”‚ +β”‚ EXIT CODES: β”‚ +β”‚ 0 = All tests passed 1 = One or more failed β”‚ +β”‚ β”‚ +β”‚ TEST COUNT: 39 test cases β”‚ +β”‚ TC01–TC04 Register / Unregister (4 tests) β”‚ +β”‚ TC05–TC11 CheckForUpdate (7 tests) β”‚ +β”‚ TC12–TC22 DownloadFirmware (11 tests) β”‚ +β”‚ TC23–TC33 UpdateFirmware (11 tests) β”‚ +β”‚ TC34–TC36 Unregister-during-op guards (3 tests) β”‚ +β”‚ TC37–TC39 Full lifecycle (3 tests) β”‚ +β”‚ β”‚ +β”‚ WHAT IT TESTS (7 layers): β”‚ +β”‚ L1: Input validation (NULL, empty, bad args) β”‚ +β”‚ L2: Concurrency guards (duplicate call rejection) β”‚ +β”‚ L3: Session guards (unregister blocked during ops) β”‚ +β”‚ L4: Condvar handshake + daemon communication β”‚ +β”‚ L5: Callback correctness (count, data, monotonicity) β”‚ +β”‚ L6: Worker cleanup (in-progress flag cleared for retry) β”‚ +β”‚ L7: Full end-to-end lifecycle β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` diff --git a/docs/KnowWhereItBreaks.md b/docs/KnowWhereItBreaks.md new file mode 100644 index 00000000..cca4c5ff --- /dev/null +++ b/docs/KnowWhereItBreaks.md @@ -0,0 +1,861 @@ +# KnowWhereItBreaks β€” Comprehensive Test Utility for librdkFwupdateMgr.so + +## Document Version + +| Version | Date | Author | Description | +|---------|------------|--------|------------------------------------------| +| 1.0 | 2026-03-27 | β€” | Initial design, test plan, and implementation guide | + +--- + +## Table of Contents + +1. [Purpose](#1-purpose) +2. [Architecture](#2-architecture) +3. [Build & Run](#3-build--run) +4. [Test Categories & Scenarios](#4-test-categories--scenarios) +5. [Error Path Deep Dive](#5-error-path-deep-dive) +6. [Interactive Menu Reference](#6-interactive-menu-reference) +7. [Automated Mode Reference](#7-automated-mode-reference) +8. [Test Result Tracking](#8-test-result-tracking) +9. [How Each Test Works Internally](#9-how-each-test-works-internally) +10. [Manual-Only Test Scenarios](#10-manual-only-test-scenarios) +11. [Troubleshooting](#11-troubleshooting) + +--- + +## 1. Purpose + +`KnowWhereItBreaks` is a **developer test utility** for exercising every code path +in `librdkFwupdateMgr.so` and the `rdkFwupdateMgr` daemon. + +It is **NOT** the example_plugin (which stays clean for external teams). +It is your personal weapon for finding bugs before they find you. + +### What it tests + +| Layer | What gets exercised | +|-------|-------------------| +| **Library input validation** | NULL handles, NULL callbacks, NULL requests, empty strings | +| **Library guards** | Duplicate same-process calls (in-progress rejection) | +| **Library session guards** | Unregister blocked during active operations | +| **Condvar handshake** | Caller gets accurate SUCCESS/FAILED from daemon reply | +| **Worker thread lifecycle** | Thread creates, runs event loop, cleans up, exits | +| **D-Bus method calls** | RegisterProcess, CheckForUpdate, DownloadFirmware, UpdateFirmware, UnregisterProcess | +| **D-Bus signal reception** | CheckForUpdateComplete, DownloadProgress, UpdateProgress | +| **Daemon rejection paths** | Already in progress, invalid handle, unknown firmware | +| **Timeout paths** | Worker thread timeout when daemon doesn't respond | +| **Callback correctness** | Right data in callback, right number of invocations | +| **Full lifecycle** | Register β†’ Check β†’ Download β†’ Update β†’ Unregister | +| **Rapid retry** | Call again immediately after previous completes/fails | +| **Cross-API interaction** | Download + Check simultaneously, unregister during ops | + +### What it does NOT test (needs manual testing) + +| Scenario | Why manual | +|----------|-----------| +| Daemon crash mid-operation | Requires `kill -9` of daemon process at right moment | +| Cross-process rejection | Requires two separate processes running simultaneously | +| Disk full during download | Requires filling filesystem | +| Network failure during download | Requires network manipulation | +| Library unload during active operation | Requires `dlclose()` test program | + +--- + +## 2. Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ KnowWhereItBreaks β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ Test Engine β”‚ β”‚ Result Track β”‚ β”‚ Callback Trackβ”‚ β”‚ +β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ β”‚ +β”‚ β”‚ run_test() β”‚ β”‚ PASS/FAIL/ β”‚ β”‚ fired? β”‚ β”‚ +β”‚ β”‚ TEST_PASS() β”‚ β”‚ SKIP counts β”‚ β”‚ status? β”‚ β”‚ +β”‚ β”‚ TEST_FAIL() β”‚ β”‚ final report β”‚ β”‚ progress? β”‚ β”‚ +β”‚ β”‚ TEST_SKIP() β”‚ β”‚ β”‚ β”‚ message? β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ β”‚ +β”‚ Public API calls β”‚ +β”‚ β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ librdkFwupdateMgr.so β”‚ β”‚ +β”‚ β”‚ (library under test)β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ D-Bus β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ rdkFwupdateMgr β”‚ β”‚ +β”‚ β”‚ (daemon) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### Key Design Principles + +1. **Each test is self-contained** β€” sets up its own state, cleans up after itself +2. **Callback tracking via volatile globals** β€” worker threads fire callbacks on + different threads, `volatile` ensures visibility +3. **Wait-with-timeout** β€” never blocks forever; every wait has a max timeout +4. **Test isolation** β€” each test resets callback state before running +5. **Ordered execution** β€” tests run in dependency order (register before check, etc.) +6. **Three modes** β€” interactive menu, automated suites, command-line flags + +--- + +## 3. Build & Run + +### 3.1 Directory Structure + +``` +rdkfwupdater/ +β”œβ”€β”€ example_plugin/ ← Clean example for external teams (UNCHANGED) +β”‚ β”œβ”€β”€ CMakeLists.txt +β”‚ └── src/ +β”‚ └── example_plugin.c +β”‚ +β”œβ”€β”€ KnowWhereItBreaks/ ← NEW: Developer test utility +β”‚ β”œβ”€β”€ CMakeLists.txt +β”‚ └── src/ +β”‚ └── KnowWhereItBreaks.c +β”‚ +β”œβ”€β”€ librdkFwupdateMgr/ ← Library under test +└── src/ ← Daemon +``` + +### 3.2 CMakeLists.txt + +```cmake +# KnowWhereItBreaks/CMakeLists.txt +project(KnowWhereItBreaks) +cmake_minimum_required(VERSION 3.10) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GLIB REQUIRED glib-2.0 gio-2.0) + +add_executable(KnowWhereItBreaks src/KnowWhereItBreaks.c) + +target_include_directories(KnowWhereItBreaks PRIVATE + ${CMAKE_SOURCE_DIR}/librdkFwupdateMgr/include + ${GLIB_INCLUDE_DIRS} +) + +target_link_libraries(KnowWhereItBreaks PRIVATE + rdkFwupdateMgr + ${GLIB_LIBRARIES} + pthread +) +``` + +### 3.3 Running + +```bash +# Prerequisites: daemon must be running for happy path tests +systemctl start rdkFwupdateMgr + +# Interactive menu (explore tests one by one) +./KnowWhereItBreaks + +# Automated: only error/validation tests (daemon NOT needed for most) +./KnowWhereItBreaks --auto-error + +# Automated: happy path tests (daemon REQUIRED) +./KnowWhereItBreaks --auto-happy + +# Automated: full lifecycle (daemon REQUIRED) +./KnowWhereItBreaks --full-lifecycle + +# Automated: run EVERYTHING +./KnowWhereItBreaks --auto-all + +# Exit code: 0 = all passed, 1 = at least one failure +echo $? +``` + +--- + +## 4. Test Categories & Scenarios + +### Category 1: RegisterProcess / UnregisterProcess (9 tests) + +| ID | Test Name | Needs Daemon | What It Validates | +|----|-----------|:---:|-------------------| +| 1.1 | `register_happy_path` | βœ… | `registerProcess()` returns SUCCESS, handle is non-NULL non-empty | +| 1.2 | `register_daemon_down` | ❌ | Stop daemon β†’ `registerProcess()` returns FAILED | +| 1.3 | `double_register` | βœ… | Call `registerProcess()` twice β†’ both succeed with different handles | +| 1.4 | `unregister_happy_path` | βœ… | `unregisterProcess(handle)` returns SUCCESS | +| 1.5 | `unregister_invalid_handle` | βœ… | `unregisterProcess("invalid_99")` returns FAILED | +| 1.6 | `unregister_null_handle` | ❌ | `unregisterProcess(NULL)` returns FAILED (library validation) | +| 1.7 | `unregister_during_check` | βœ… | Start check β†’ unregister β†’ FAILED (guard blocks it) | +| 1.8 | `unregister_during_download` | βœ… | Start download β†’ unregister β†’ FAILED (guard blocks it) | +| 1.9 | `unregister_during_update` | βœ… | Start update β†’ unregister β†’ FAILED (guard blocks it) | + +### Category 2: CheckForUpdate (7 tests) + +| ID | Test Name | Needs Daemon | What It Validates | +|----|-----------|:---:|-------------------| +| 2.1 | `check_happy_path` | βœ… | Returns SUCCESS, callback fires with valid data | +| 2.2 | `check_null_handle` | ❌ | `checkForUpdate(NULL, cb)` returns FAILED | +| 2.3 | `check_null_callback` | ❌ | `checkForUpdate(handle, NULL)` returns FAILED | +| 2.4 | `check_duplicate_same_process` | βœ… | First call SUCCESS, immediate second call FAILED (library guard) | +| 2.5 | `check_rapid_retry` | βœ… | Call β†’ wait β†’ call again β†’ SUCCESS (guard cleared) | +| 2.6 | `check_unregistered_handle` | βœ… | `checkForUpdate("bad_handle", cb)` returns FAILED | +| 2.7 | `check_callback_data_valid` | βœ… | Callback data has non-garbage version strings | + +### Category 3: DownloadFirmware (12 tests) + +| ID | Test Name | Needs Daemon | What It Validates | +|----|-----------|:---:|-------------------| +| 3.1 | `download_happy_path` | βœ… | Returns SUCCESS, progress callbacks fire, terminal = COMPLETED | +| 3.2 | `download_null_handle` | ❌ | Returns FAILED | +| 3.3 | `download_null_request` | ❌ | Returns FAILED | +| 3.4 | `download_null_callback` | ❌ | Returns FAILED | +| 3.5 | `download_empty_firmware_name` | ❌ | Returns FAILED (library validates non-empty) | +| 3.6 | `download_empty_url` | ❌ | Returns FAILED (library validates non-empty) | +| 3.7 | `download_duplicate_same_process` | βœ… | Second call FAILED (library guard) | +| 3.8 | `download_rapid_retry` | βœ… | After completion β†’ retry β†’ SUCCESS | +| 3.9 | `download_daemon_rejects` | βœ… | Invalid firmware β†’ daemon rejects β†’ FAILED return | +| 3.10 | `download_progress_increments` | βœ… | Multiple callbacks fire, progress values increase | +| 3.11 | `download_terminal_status` | βœ… | Final callback has COMPLETED or ERROR (not INPROGRESS) | +| 3.12 | `download_unregistered_handle` | βœ… | Returns FAILED | + +### Category 4: UpdateFirmware (12 tests) + +| ID | Test Name | Needs Daemon | What It Validates | +|----|-----------|:---:|-------------------| +| 4.1 | `update_happy_path` | βœ… | Returns SUCCESS, progress callbacks fire, terminal = COMPLETED | +| 4.2 | `update_null_handle` | ❌ | Returns FAILED | +| 4.3 | `update_null_request` | ❌ | Returns FAILED | +| 4.4 | `update_null_callback` | ❌ | Returns FAILED | +| 4.5 | `update_empty_firmware_name` | ❌ | Returns FAILED | +| 4.6 | `update_duplicate_same_process` | βœ… | Second call FAILED (library guard) | +| 4.7 | `update_rapid_retry` | βœ… | After completion β†’ retry β†’ SUCCESS | +| 4.8 | `update_daemon_rejects` | βœ… | Already updating β†’ daemon rejects β†’ FAILED | +| 4.9 | `update_progress_increments` | βœ… | Multiple callbacks, progress increases | +| 4.10 | `update_terminal_status` | βœ… | Final callback = COMPLETED or ERROR | +| 4.11 | `update_reboot_flag_false` | βœ… | No reboot after update | +| 4.12 | `update_unregistered_handle` | βœ… | Returns FAILED | + +### Category 5: Cross-API / Lifecycle (5 tests) + +| ID | Test Name | Needs Daemon | What It Validates | +|----|-----------|:---:|-------------------| +| 5.1 | `full_lifecycle` | βœ… | Register β†’ Check β†’ Download β†’ Update β†’ Unregister (all succeed) | +| 5.2 | `full_lifecycle_no_sleeps` | βœ… | Same as 5.1 but no sleep() between calls | +| 5.3 | `check_and_download_simultaneous` | βœ… | Both calls succeed (different guards) | +| 5.4 | `download_then_update_sequential` | βœ… | Download completes, then update succeeds | +| 5.5 | `double_register_full_lifecycle` | βœ… | Register twice, run ops on both handles | + +**Total: 45 automated tests** + +--- + +## 5. Error Path Deep Dive + +### 5.1 Library-Level Error Paths (No Daemon Needed) + +These are caught by the library's input validation BEFORE any D-Bus call: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Library Input Validation Layer β”‚ +β”‚ β”‚ +β”‚ checkForUpdate(): β”‚ +β”‚ β”œβ”€ handle == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ callback == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ └─ handle not registered β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ downloadFirmware(): β”‚ +β”‚ β”œβ”€ handle == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ request == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ callback == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ firmwareName empty β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ downloadUrl empty β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ └─ handle not registered β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ updateFirmware(): β”‚ +β”‚ β”œβ”€ handle == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ request == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ callback == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ firmwareName empty β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ └─ handle not registered β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ unregisterProcess(): β”‚ +β”‚ β”œβ”€ handle == NULL β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ check in progress β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”œβ”€ download in progress β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ └─ update in progress β†’ RDKFW_UPDATE_FAILED β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 5.2 Library Guard Paths (No Daemon Needed) + +These are caught by the per-API in-progress guards: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Library In-Progress Guards β”‚ +β”‚ β”‚ +β”‚ checkForUpdate() while check active: β”‚ +β”‚ internal_begin_check() β†’ false β†’ RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ downloadFirmware() while download active: β”‚ +β”‚ internal_begin_download() β†’ false β†’ RDKFW_UPDATE_FAILEDβ”‚ +β”‚ β”‚ +β”‚ updateFirmware() while update active: β”‚ +β”‚ internal_begin_update() β†’ false β†’ RDKFW_UPDATE_FAILED β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 5.3 Daemon-Level Rejection Paths (Daemon Needed) + +The daemon receives the D-Bus method call and may reject it: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Daemon Rejection Paths β”‚ +β”‚ β”‚ +β”‚ DownloadFirmware: β”‚ +β”‚ β”œβ”€ Another download already active β”‚ +β”‚ β”‚ β†’ reply (sss): "RDKFW_DWNL_FAILED", β”‚ +β”‚ β”‚ "REJECTED", β”‚ +β”‚ β”‚ "Download already in progress" β”‚ +β”‚ β”‚ β†’ library returns RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ β”‚ +β”‚ └─ Invalid/unknown parameters β”‚ +β”‚ β†’ daemon may accept but download fails later β”‚ +β”‚ β†’ DownloadProgress signal with ERROR status β”‚ +β”‚ β”‚ +β”‚ UpdateFirmware: β”‚ +β”‚ β”œβ”€ Another update already active β”‚ +β”‚ β”‚ β†’ reply (sss): "RDKFW_UPDATE_FAILED", β”‚ +β”‚ β”‚ "REJECTED", β”‚ +β”‚ β”‚ "Update already in progress" β”‚ +β”‚ β”‚ β†’ library returns RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ β”‚ +β”‚ └─ No firmware downloaded yet β”‚ +β”‚ β†’ daemon rejects or update fails β”‚ +β”‚ β†’ UpdateProgress signal with ERROR status β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### 5.4 Worker Thread Error Paths + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Worker Thread Internal Error Paths β”‚ +β”‚ β”‚ +β”‚ pthread_create() fails: β”‚ +β”‚ β†’ internal_abort_*() clears in-progress flag β”‚ +β”‚ β†’ ctx freed by caller β”‚ +β”‚ β†’ return RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ g_bus_get_sync() fails (daemon not running): β”‚ +β”‚ β†’ init_failed = true β”‚ +β”‚ β†’ cond_signal(ready) β”‚ +β”‚ β†’ skip g_main_loop_run() β”‚ +β”‚ β†’ cleanup, free ctx, thread exits β”‚ +β”‚ β†’ caller returns RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ g_dbus_connection_call_sync() fails (D-Bus error): β”‚ +β”‚ β†’ init_failed = true β”‚ +β”‚ β†’ cond_signal(ready) β”‚ +β”‚ β†’ skip loop, cleanup, exit β”‚ +β”‚ β†’ caller returns RDKFW_UPDATE_FAILED β”‚ +β”‚ β”‚ +β”‚ Timeout (no signal received within 120s/3600s): β”‚ +β”‚ β†’ timeout callback fires β”‚ +β”‚ β†’ build error response, fire client callback β”‚ +β”‚ β†’ g_main_loop_quit() β”‚ +β”‚ β†’ cleanup, free, exit β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 6. Interactive Menu Reference + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ KnowWhereItBreaks β€” librdkFwupdateMgr Test Utility β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Handle: (not registered) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ INDIVIDUAL OPERATIONS β”‚ +β”‚ 1 Register 6 CheckForUpdate β”‚ +β”‚ 2 Unregister 7 DownloadFirmware β”‚ +β”‚ 3 Full Lifecycle 8 UpdateFirmware β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ AUTOMATED SUITES β”‚ +β”‚ 10 All Error/Validation Tests (fast, no daemon needed) β”‚ +β”‚ 11 All Happy Path Tests (daemon required) β”‚ +β”‚ 12 Full Lifecycle Test (daemon required) β”‚ +β”‚ 13 ALL Tests (everything) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ INPUT VALIDATION TESTS β”‚ +β”‚ 20 NULL handle tests (all APIs) β”‚ +β”‚ 21 NULL callback tests (all APIs) β”‚ +β”‚ 22 NULL request tests (download + update) β”‚ +β”‚ 23 Empty string tests (firmware name, URL) β”‚ +β”‚ 24 Unregistered handle tests (all APIs) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ LIBRARY GUARD TESTS β”‚ +β”‚ 30 Duplicate CheckForUpdate (same process) β”‚ +β”‚ 31 Duplicate Download (same process) β”‚ +β”‚ 32 Duplicate Update (same process) β”‚ +β”‚ 33 Unregister during Check β”‚ +β”‚ 34 Unregister during Download β”‚ +β”‚ 35 Unregister during Update β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ RAPID RETRY TESTS β”‚ +β”‚ 40 Check β†’ complete β†’ Check again β”‚ +β”‚ 41 Download β†’ complete β†’ Download again β”‚ +β”‚ 42 Update β†’ complete β†’ Update again β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ CALLBACK VALIDATION TESTS β”‚ +β”‚ 50 Check callback data validation β”‚ +β”‚ 51 Download progress increments β”‚ +β”‚ 52 Download terminal status validation β”‚ +β”‚ 53 Update progress increments β”‚ +β”‚ 54 Update terminal status validation β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ CROSS-API TESTS β”‚ +β”‚ 60 Check + Download simultaneously β”‚ +β”‚ 61 Download then Update sequentially β”‚ +β”‚ 62 Double register + parallel ops β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 0 Exit (prints results summary) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## 7. Automated Mode Reference + +### 7.1 `--auto-error` (No daemon needed for most tests) + +Runs all tests that validate **library-level input validation and guards**. +These tests do NOT make D-Bus calls (or expect them to fail gracefully): + +``` +Error Path Tests (no registration needed): + β”œβ”€ 1.6 unregister_null_handle + β”œβ”€ 1.5 unregister_invalid_handle + β”œβ”€ 2.2 check_null_handle + β”œβ”€ 2.3 check_null_callback + β”œβ”€ 3.2 download_null_handle + β”œβ”€ 3.3 download_null_request + β”œβ”€ 3.4 download_null_callback + β”œβ”€ 4.2 update_null_handle + β”œβ”€ 4.3 update_null_request + └─ 4.4 update_null_callback + +Error Path Tests (with registration β€” daemon needed): + β”œβ”€ [register] + β”œβ”€ 3.5 download_empty_firmware_name + β”œβ”€ 3.6 download_empty_url + β”œβ”€ 4.5 update_empty_firmware_name + β”œβ”€ 2.4 check_duplicate_same_process + β”œβ”€ 3.7 download_duplicate_same_process + β”œβ”€ 4.6 update_duplicate_same_process + β”œβ”€ 1.7 unregister_during_check + β”œβ”€ 1.8 unregister_during_download + β”œβ”€ 1.9 unregister_during_update + └─ [unregister] +``` + +### 7.2 `--auto-happy` (Daemon required) + +``` +Happy Path Tests: + β”œβ”€ [register] + β”œβ”€ 2.1 check_happy_path + β”œβ”€ 2.7 check_callback_data_valid + β”œβ”€ 2.5 check_rapid_retry + β”œβ”€ 3.1 download_happy_path + β”œβ”€ 3.10 download_progress_increments + β”œβ”€ 3.11 download_terminal_status + β”œβ”€ 3.8 download_rapid_retry + β”œβ”€ 4.1 update_happy_path + β”œβ”€ 4.9 update_progress_increments + β”œβ”€ 4.10 update_terminal_status + β”œβ”€ 4.7 update_rapid_retry + └─ [unregister] +``` + +### 7.3 `--full-lifecycle` (Daemon required) + +``` +Full Lifecycle: + β”œβ”€ 5.1 Register β†’ Check β†’ Download β†’ Update β†’ Unregister + β”œβ”€ 5.2 Same but with no sleep() between calls + └─ 5.4 Download β†’ Update sequential (verify update works after download) +``` + +### 7.4 `--auto-all` (Daemon required) + +Runs `--auto-error` then `--auto-happy` then `--full-lifecycle`. +Prints combined results at end. + +--- + +## 8. Test Result Tracking + +### Output Format + +``` +══════════════════════════════════════════════════════════════ + INPUT VALIDATION TESTS +══════════════════════════════════════════════════════════════ + +--- Test 1.6: Unregister NULL Handle --- + [PASS] unregister_null_handle β€” unregisterProcess(NULL) returned FAILED as expected + +--- Test 2.2: CheckForUpdate NULL Handle --- + [PASS] check_null_handle β€” checkForUpdate(NULL, cb) returned FAILED as expected + +--- Test 3.5: DownloadFirmware Empty Firmware Name --- + [PASS] download_empty_firmware_name β€” Rejected empty firmwareName + +--- Test 2.4: Duplicate CheckForUpdate --- + [PASS] check_duplicate_same_process β€” Second call rejected by library guard + [INFO] Waiting for first check to complete... + [CB:Check] status=0, available=v2.0, current=v1.0 + [PASS] First check completed cleanly + +══════════════════════════════════════════════════════════════ + TEST RESULTS +══════════════════════════════════════════════════════════════ + Total: 45 + Passed: 43 + Failed: 1 + Skipped: 1 +══════════════════════════════════════════════════════════════ + ❌ 1 TEST(S) FAILED +══════════════════════════════════════════════════════════════ +``` + +### Exit Code + +| Code | Meaning | +|------|---------| +| 0 | All tests passed (or only skipped) | +| 1 | At least one test failed | + +--- + +## 9. How Each Test Works Internally + +### 9.1 Input Validation Test Pattern + +```c +// Every input validation test follows this pattern: +static void test_check_null_handle(void) +{ + reset_callback_state(); // Clear all volatile flags + RdkFwUpdateStatus ret = checkForUpdate(NULL, check_callback); + if (ret == RDKFW_UPDATE_FAILED) { + TEST_PASS("check_null_handle"); // Library rejected it + } else { + TEST_FAIL("check_null_handle", + "Expected FAILED but got SUCCESS"); // BUG: library didn't validate + } +} +``` + +**Why it works:** The library's `checkForUpdate()` checks `handle == NULL` and +returns `RDKFW_UPDATE_FAILED` before creating any thread or D-Bus connection. +No daemon interaction occurs. Fast. Deterministic. + +### 9.2 Library Guard Test Pattern + +```c +static void test_check_duplicate_same_process(void) +{ + reset_callback_state(); + + // First call β€” should succeed and start worker thread + RdkFwUpdateStatus ret1 = checkForUpdate(g_handle, check_callback); + assert(ret1 == RDKFW_UPDATE_SUCCESS); + + // IMMEDIATELY call again β€” worker thread is still active + // internal_begin_check() will see g_check_in_progress == true + RdkFwUpdateStatus ret2 = checkForUpdate(g_handle, check_callback); + + if (ret2 == RDKFW_UPDATE_FAILED) { + TEST_PASS("duplicate rejected"); // Library guard working + } else { + TEST_FAIL("duplicate NOT rejected"); // BUG: guard missing + } + + // Wait for first to complete (cleanup) + wait_for_callback(&g_check_callback_fired, 130); +} +``` + +**Why it works:** The first `checkForUpdate()` call sets `g_check_in_progress = true` +via `internal_begin_check()` and starts the worker thread. When the second call +arrives (microseconds later), `internal_begin_check()` sees `g_check_in_progress == true` +and returns `false`, causing the second call to return `RDKFW_UPDATE_FAILED`. + +### 9.3 Happy Path Test Pattern + +```c +static void test_download_happy_path(void) +{ + reset_callback_state(); + + DownloadRequest req = { .firmwareName = "test.bin", .downloadUrl = "http://..." }; + + // This call blocks briefly (condvar wait), then returns daemon's decision + RdkFwUpdateStatus ret = downloadFirmware(g_handle, &req, download_callback); + + if (ret != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("download_happy_path", "Daemon rejected"); + return; + } + + // Worker thread is now running g_main_loop_run(), receiving DownloadProgress signals. + // Our download_callback fires on each signal. + // Wait for terminal callback (COMPLETED or ERROR). + + if (wait_for_callback(&g_dwnl_callback_fired, 3700)) { + if (g_dwnl_status == RDKFW_DWNL_COMPLETED) { + TEST_PASS("download completed"); + } else { + TEST_FAIL("download_happy_path", "Terminal was ERROR not COMPLETED"); + } + } else { + TEST_FAIL("download_happy_path", "Timeout β€” no terminal callback"); + } +} +``` + +**Why it works:** `downloadFirmware()` returns `SUCCESS` only if the daemon +accepted the request (condvar handshake confirms daemon's reply). Then the +worker thread runs the GLib event loop, receiving `DownloadProgress` D-Bus signals. +Each signal fires `download_callback()`, which updates the volatile globals. +`wait_for_callback()` polls `g_dwnl_callback_fired` every 100ms until the terminal +callback sets it to `true`. + +### 9.4 Callback Tracking + +```c +// Global tracking state (volatile for cross-thread visibility) +static volatile bool g_dwnl_callback_fired = false; // Terminal callback received +static volatile int g_dwnl_callback_count = 0; // Total callback invocations +static volatile int g_dwnl_last_progress = -1; // Last progress value +static volatile int g_dwnl_status = -1; // Last status value +static volatile bool g_dwnl_progress_monotonic = true; // Progress only increased + +static void download_callback(DownloadResponse *response) +{ + int prev = g_dwnl_last_progress; + g_dwnl_callback_count++; + g_dwnl_last_progress = response->progress; + g_dwnl_status = response->status; + + // Track monotonicity: progress should never decrease + if (prev >= 0 && (int)response->progress < prev) { + g_dwnl_progress_monotonic = false; + } + + printf(" [CB:Download] #%d progress=%u%% status=%d msg=%s\n", + g_dwnl_callback_count, response->progress, + response->status, response->statusMessage); + + // Mark terminal + if (response->status == RDKFW_DWNL_COMPLETED || + response->status == RDKFW_DWNL_ERROR) { + g_dwnl_callback_fired = true; + } +} +``` + +### 9.5 Unregister-During-Operation Test Pattern + +```c +static void test_unregister_during_download(void) +{ + reset_callback_state(); + + DownloadRequest req = { ... }; + RdkFwUpdateStatus ret1 = downloadFirmware(g_handle, &req, download_callback); + // ret1 == SUCCESS means worker thread is active, download is happening + + // IMMEDIATELY try to unregister (should be blocked) + RdkFwUpdateStatus ret2 = unregisterProcess(g_handle); + + if (ret2 == RDKFW_UPDATE_FAILED) { + TEST_PASS("unregister_during_download blocked"); + } else { + TEST_FAIL("unregister_during_download", + "Unregister was NOT blocked β€” handle removed while download active!"); + } + + // Let download finish cleanly + wait_for_callback(&g_dwnl_callback_fired, 3700); +} +``` + +**Why it works:** `unregisterProcess()` calls `internal_is_dwnl_in_progress()`, +which checks `g_dwnl_in_progress` under mutex. Since the download worker thread +is still running, this returns `true`, and `unregisterProcess()` returns `FAILED`. + +### 9.6 Rapid Retry Test Pattern + +```c +static void test_check_rapid_retry(void) +{ + // First call + reset_callback_state(); + checkForUpdate(g_handle, check_callback); + wait_for_callback(&g_check_callback_fired, 130); + // Worker thread sets g_check_in_progress = false in cleanup + + sleep(1); // Brief pause to let thread fully exit + + // Retry β€” should succeed because in-progress was cleared + reset_callback_state(); + RdkFwUpdateStatus ret = checkForUpdate(g_handle, check_callback); + + if (ret == RDKFW_UPDATE_SUCCESS) { + TEST_PASS("rapid_retry accepted"); + wait_for_callback(&g_check_callback_fired, 130); + } else { + TEST_FAIL("rapid_retry", "in-progress flag was not cleared after completion"); + } +} +``` + +**Why it works:** After the first check completes, the worker thread calls +`internal_end_check()` which sets `g_check_in_progress = false`. The 1-second +sleep ensures the worker thread has fully exited (joined). The second call's +`internal_begin_check()` sees `false` and succeeds. + +--- + +## 10. Manual-Only Test Scenarios + +These cannot be automated in KnowWhereItBreaks because they require external +actions. Document the steps for manual execution: + +### 10.1 Daemon Crash During Download + +```bash +# Terminal 1: Start download +./KnowWhereItBreaks +> 1 (register) +> 7 (download β€” enter valid firmware/URL) +# Download starts, progress callbacks appear... + +# Terminal 2: Kill daemon mid-download +kill -9 $(pidof rdkFwupdateMgr) + +# Terminal 1: Observe +# Expected: After timeout (up to 3600s β€” or sooner if D-Bus detects disconnect), +# callback fires with ERROR status. +# Worker thread cleans up and exits. +# g_dwnl_in_progress is cleared. +# Subsequent operations work after daemon restart. +``` + +### 10.2 Cross-Process Rejection + +```bash +# Terminal 1: +./KnowWhereItBreaks +> 1 (register) +> 7 (download β€” starts downloading) + +# Terminal 2 (simultaneously): +./KnowWhereItBreaks +> 1 (register β€” succeeds, different handle) +> 7 (download β€” same firmware) +# Expected: downloadFirmware() returns FAILED +# (daemon rejects: "Download already in progress") +``` + +### 10.3 Daemon Down (All APIs) + +```bash +# Stop daemon +systemctl stop rdkFwupdateMgr + +# Run error tests +./KnowWhereItBreaks --auto-happy + +# Expected: ALL happy path tests should return FAILED (D-Bus connect fails) +# No crashes, no hangs, no memory leaks +``` + +### 10.4 Valgrind Memory Check + +```bash +valgrind --leak-check=full --track-origins=yes --show-leak-kinds=all \ + ./KnowWhereItBreaks --auto-all + +# Expected: 0 bytes lost +# Note: GLib may show "still reachable" blocks β€” these are GLib's global +# type system caches and are NOT leaks. +``` + +### 10.5 Thread Sanitizer + +```bash +# Rebuild with -fsanitize=thread +./KnowWhereItBreaks --auto-all + +# Expected: No data race warnings +``` + +--- + +## 11. Troubleshooting + +### Test hangs on wait_for_callback() + +**Cause:** Worker thread is stuck in `g_main_loop_run()` β€” daemon never sent +the expected signal. + +**Fix:** Check daemon logs (`/opt/logs/rdkFwupdateMgr.log`). The daemon may +have crashed, rejected the request, or the signal format changed. + +### All happy path tests return FAILED + +**Cause:** Daemon is not running. + +**Fix:** `systemctl start rdkFwupdateMgr` or run daemon manually. + +### Duplicate call test passes but shouldn't + +**Cause:** The first `checkForUpdate()` completed so fast (before the second call) +that `g_check_in_progress` was already cleared. + +**Fix:** This is actually correct behavior β€” the guard works, the operation was +just fast. On embedded devices with slower D-Bus, the timing will be more reliable. + +### Unregister-during-operation test fails + +**Cause:** `unregisterProcess()` does not check `internal_is_*_in_progress()`. + +**Fix:** Verify `rdkFwupdateMgr_process.c` has the in-progress guards for all +three APIs (check, download, update). + +### Progress is not monotonically increasing + +**Cause:** Daemon sent progress=50 then progress=30. This is a daemon bug, +not a library bug. + +**Fix:** Report to daemon team. Library correctly relays whatever daemon sends. + +### Callback data has garbage values + +**Cause:** Signal parse function has wrong GVariant format string. + +**Fix:** Verify: +- CheckForUpdateComplete: `(tiissss)` +- DownloadProgress: `(tsuss)` +- UpdateProgress: `(tsiis)` + +Match these against daemon's `g_variant_new()` calls. diff --git a/docs/LIBRARY_API_DESIGN_GUIDE.md b/docs/LIBRARY_API_DESIGN_GUIDE.md new file mode 100755 index 00000000..cb092abe --- /dev/null +++ b/docs/LIBRARY_API_DESIGN_GUIDE.md @@ -0,0 +1,849 @@ +# librdkFwupdateMgr β€” API & Design Guide + +**Library:** `librdkFwupdateMgr.so` +**Header:** `rdkFwupdateMgr_client.h` +**Date:** April 2026 +**Audience:** Anyone β€” developers, testers, architects, or anyone curious about how this library works. + +--- + +## Table of Contents + +1. [What Is This Library?](#1-what-is-this-library) +2. [The Big Picture β€” How It All Fits Together](#2-the-big-picture--how-it-all-fits-together) +3. [Analogy: The Post Office](#3-analogy-the-post-office) +4. [API Reference](#4-api-reference) + - [registerProcess()](#41-registerprocess) + - [checkForUpdate()](#42-checkforupdate) + - [downloadFirmware()](#43-downloadfirmware) + - [updateFirmware()](#44-updatefirmware) + - [unregisterProcess()](#45-unregisterprocess) +5. [The On-Demand Worker Thread Model](#5-the-on-demand-worker-thread-model) + - [What Problem Does It Solve?](#51-what-problem-does-it-solve) + - [How It Works β€” The Restaurant Analogy](#52-how-it-works--the-restaurant-analogy) + - [Technical Deep Dive](#53-technical-deep-dive) + - [The Condvar Handshake](#54-the-condvar-handshake) + - [Why On-Demand Instead of Persistent?](#55-why-on-demand-instead-of-persistent) +6. [Complete Workflow Example](#6-complete-workflow-example) +7. [Thread Safety β€” How the Library Keeps Things Safe](#7-thread-safety--how-the-library-keeps-things-safe) +8. [Memory Ownership β€” Who Frees What?](#8-memory-ownership--who-frees-what) +9. [Error Handling β€” What Can Go Wrong?](#9-error-handling--what-can-go-wrong) +10. [Callbacks β€” The Rules](#10-callbacks--the-rules) +11. [Quick Reference Card](#11-quick-reference-card) + +--- + +## 1. What Is This Library? + +`librdkFwupdateMgr` is a **C shared library** that lets applications manage firmware updates on RDK devices. It talks to a background system service (the **firmware daemon**) over D-Bus to: + +- **Check** if new firmware is available +- **Download** firmware from a server +- **Flash** (install) firmware onto the device + +Think of it as a remote control for firmware updates. Your app tells the library what to do, the library talks to the daemon, and the daemon does the heavy lifting. + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Your App │──────▢│ librdkFwupdateMgr │──────▢│ Firmware Daemon β”‚ +β”‚ │◀──────│ (this library) │◀──────│ (rdkfwupdater) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + API calls D-Bus IPC XConf, curl, flash +``` + +--- + +## 2. The Big Picture β€” How It All Fits Together + +A typical firmware update follows five steps, always in this order: + +``` +Step 1: Register β†’ "Hey daemon, I'm here. Give me an ID." +Step 2: Check β†’ "Is there new firmware for me?" +Step 3: Download β†’ "Download that firmware file." +Step 4: Flash β†’ "Install the firmware on the device." +Step 5: Unregister β†’ "I'm done. Bye." +``` + +Steps 2, 3, and 4 are **asynchronous** β€” they return immediately and deliver results later through **callbacks** (functions you provide that the library calls when something happens). + +``` + TIME ─────────────────────────────────────────────▢ + +Your App: register ─── check ─── download ─── flash ─── unregister + β”‚ β”‚ β”‚ + β”‚ β”‚ β”‚ + β–Ό β–Ό β–Ό +Callbacks: "Update "50%..." "Flashing..." + found!" "100%!" "Done!" +``` + +--- + +## 3. Analogy: The Post Office + +If this all sounds abstract, here's a simple analogy: + +| Firmware Update | Post Office Analogy | +|----------------|---------------------| +| `registerProcess()` | Walk into the post office and get a ticket number | +| Your **handle** (ID) | The ticket number on your receipt | +| `checkForUpdate()` | Ask the clerk: "Do I have any packages?" | +| Your **callback** | The clerk calls your name when they find your package | +| `downloadFirmware()` | "Please bring my package from the warehouse" | +| Download **progress callbacks** | The clerk shouts: "25% loaded... 50%... 100%!" | +| `updateFirmware()` | "Please install/unwrap the package for me" | +| Flash **progress callbacks** | The clerk shouts: "Unpacking... Installing... Done!" | +| `unregisterProcess()` | Leave the post office and throw away your ticket | + +**Key insight:** After you ask a question at the counter, you **don't stand there and wait**. You go sit down, and the clerk calls you when they have an answer. That's exactly how the async APIs work. + +--- + +## 4. API Reference + +### 4.1 `registerProcess()` + +> **"Get your ticket."** β€” This is the first thing you call. + +```c +FirmwareInterfaceHandle registerProcess(const char *processName, + const char *libVersion); +``` + +**What it does:** +- Connects to the firmware daemon over D-Bus +- Tells the daemon: "Hi, I'm [processName], using library version [libVersion]" +- The daemon gives you back a unique ID number (like "42857") + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `processName` | `const char*` | Your app's name (e.g. `"VideoPlayer"`, `"SettingsApp"`) | +| `libVersion` | `const char*` | Your app's version (e.g. `"1.0"`, `"2.3.1"`) | + +**Returns:** +- **Success:** A string ID like `"42857"` β€” save this, you need it for everything else +- **Failure:** `NULL` β€” daemon not running, D-Bus error, or invalid inputs + +**Behavior:** +- βœ… Synchronous β€” blocks until the daemon responds (typically < 10ms) +- βœ… No background threads created +- ⚠️ The returned string belongs to the library β€” **never call `free()` on it** +- ⚠️ The string becomes invalid after `unregisterProcess()` + +**Example:** +```c +FirmwareInterfaceHandle handle = registerProcess("MyApp", "1.0"); +if (handle == NULL) { + printf("Registration failed! Is the daemon running?\n"); + return; +} +printf("Registered! My handle: %s\n", handle); +// handle might print: "42857" +``` + +--- + +### 4.2 `checkForUpdate()` + +> **"Is there new firmware?"** β€” Ask the daemon, get a callback later. + +```c +CheckForUpdateResult checkForUpdate(FirmwareInterfaceHandle handle, + UpdateEventCallback callback); +``` + +**What it does:** +- Asks the daemon to check the XConf server for available firmware +- **Returns immediately** (doesn't wait for the answer) +- When the daemon finds out, it calls **your callback function** with the result + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `handle` | `FirmwareInterfaceHandle` | Your ID from `registerProcess()` | +| `callback` | `UpdateEventCallback` | Your function that receives the firmware info | + +**Your callback signature:** +```c +void my_callback(const FwInfoData *fwinfo); +``` + +The `FwInfoData` struct tells you: +- `status` β€” Was firmware found? (`FIRMWARE_AVAILABLE`, `FIRMWARE_NOT_AVAILABLE`, `FIRMWARE_CHECK_ERROR`, etc.) +- `CurrFWVersion` β€” Current firmware version on this device +- `UpdateDetails` β€” If available: filename, download URL, version, reboot flag, etc. + +**Returns:** +| Value | Meaning | +|-------|---------| +| `CHECK_FOR_UPDATE_SUCCESS` | Request started OK β€” your callback will fire later | +| `CHECK_FOR_UPDATE_FAIL` | Couldn't start β€” bad handle, NULL callback, already in progress, etc. | + +**Behavior:** +- βœ… Returns immediately (non-blocking) +- βœ… Callback fires **exactly once** (or zero times if timeout/error) +- βœ… Callback fires in a **background thread** (not your thread) +- ⚠️ Only **one** `checkForUpdate()` at a time per process β€” second call is rejected +- ⚠️ Data in `FwInfoData` is only valid **during** the callback β€” copy what you need +- πŸ• Typical callback delay: 5–30 seconds (max 120 seconds safety timeout) + +**Timeline:** +``` +Your App Thread Worker Thread (created by library) +─────────────── ───────────────────────────────── +checkForUpdate(handle, my_cb) + β”‚ Spawns worker thread ──────────▢ Connect to D-Bus + β”‚ Waits ~100ms for worker ready Subscribe to signals + β”‚ Returns SUCCESS Send request to daemon + β”‚ ...waiting for daemon (5-30s)... + β”‚ (you can do other things) + β”‚ Daemon responds! + β”‚ Parse result + β”‚ ──▢ my_cb(&fwinfo) ◀── YOUR CALLBACK + β”‚ Cleanup resources + β”‚ Thread exits + β–Ό +``` + +**Example:** +```c +void on_check_result(const FwInfoData *info) { + if (info->status == FIRMWARE_AVAILABLE) { + printf("New firmware available: %s\n", + info->UpdateDetails->FwVersion); + } else { + printf("No update available.\n"); + } +} + +// Start the check +CheckForUpdateResult result = checkForUpdate(handle, on_check_result); +if (result == CHECK_FOR_UPDATE_SUCCESS) { + printf("Check started! Waiting for callback...\n"); +} +``` + +--- + +### 4.3 `downloadFirmware()` + +> **"Download that firmware."** β€” Start a download, get progress callbacks. + +```c +DownloadResult downloadFirmware(FirmwareInterfaceHandle handle, + const FwDwnlReq *fwdwnlreq, + DownloadCallback callback); +``` + +**What it does:** +- Tells the daemon to download a firmware file from the server +- **Returns immediately** (doesn't wait for the download to finish) +- Your callback fires **multiple times** as the download progresses (0%, 25%, 50%, 75%, 100%) + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `handle` | `FirmwareInterfaceHandle` | Your ID from `registerProcess()` | +| `fwdwnlreq` | `const FwDwnlReq*` | What to download (name, URL, type) | +| `callback` | `DownloadCallback` | Your function that tracks progress | + +**The `FwDwnlReq` struct:** +```c +typedef struct { + const char *firmwareName; // "firmware_v2.bin" β€” REQUIRED + const char *downloadUrl; // URL to download from (NULL = let daemon decide) + const char *TypeOfFirmware; // "PCI", "PDRI", or "PERIPHERAL" (NULL = default) +} FwDwnlReq; +``` + +**Your callback signature:** +```c +void my_download_cb(int progress_percent, DownloadStatus status); +``` + +| `status` value | Meaning | +|----------------|---------| +| `DWNL_IN_PROGRESS` | Still downloading β€” `progress_percent` is 0–99 | +| `DWNL_COMPLETED` | Done! `progress_percent` is 100 | +| `DWNL_ERROR` | Download failed | + +**Returns:** +| Value | Meaning | +|-------|---------| +| `RDKFW_DWNL_SUCCESS` | Daemon accepted the download β€” callbacks will fire | +| `RDKFW_DWNL_FAILED` | Couldn't start β€” bad handle, daemon rejected, already downloading, etc. | + +**Behavior:** +- βœ… Returns immediately +- βœ… Return value is **accurate** β€” it reflects whether the daemon actually accepted the request +- βœ… Callback fires **multiple times** (once per progress update) +- ⚠️ Only **one** download at a time β€” second call is rejected +- πŸ• Typical download time: 1–30 minutes (max 1 hour safety timeout) + +**Timeline:** +``` +Your App Thread Worker Thread +─────────────── ───────────── +downloadFirmware(handle, req, cb) + β”‚ Spawns worker thread ──────▢ Connect to D-Bus + β”‚ Waits ~200ms for daemon Subscribe to signals + β”‚ reply (accept/reject) Call daemon (synchronous) + β”‚ Returns SUCCESS Daemon: "Accepted, downloading..." + β”‚ ...receiving progress signals... + β”‚ (you can do other things) + β”‚ ──▢ cb(10, DWNL_IN_PROGRESS) + β”‚ ──▢ cb(25, DWNL_IN_PROGRESS) + β”‚ ──▢ cb(50, DWNL_IN_PROGRESS) + β”‚ ──▢ cb(75, DWNL_IN_PROGRESS) + β”‚ ──▢ cb(100, DWNL_COMPLETED) + β”‚ Cleanup, thread exits + β–Ό +``` + +**Example:** +```c +void on_download_progress(int percent, DownloadStatus status) { + if (status == DWNL_IN_PROGRESS) { + printf("Downloading: %d%%\n", percent); + } else if (status == DWNL_COMPLETED) { + printf("Download complete!\n"); + } else { + printf("Download failed!\n"); + } +} + +FwDwnlReq request = { + .firmwareName = "firmware_v2.bin", + .downloadUrl = NULL, // let daemon use XConf URL + .TypeOfFirmware = "PCI" +}; + +DownloadResult result = downloadFirmware(handle, &request, on_download_progress); +if (result == RDKFW_DWNL_SUCCESS) { + printf("Download started!\n"); +} +``` + +--- + +### 4.4 `updateFirmware()` + +> **"Flash it."** β€” Install firmware on the device. + +```c +UpdateResult updateFirmware(FirmwareInterfaceHandle handle, + const FwUpdateReq *fwupdatereq, + UpdateCallback callback); +``` + +**What it does:** +- Tells the daemon to flash (install) downloaded firmware onto the device +- **Returns immediately** (doesn't wait for flash to complete) +- Your callback fires **multiple times** as flashing progresses +- ⚠️ **This modifies your device's firmware β€” irreversible!** + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `handle` | `FirmwareInterfaceHandle` | Your ID from `registerProcess()` | +| `fwupdatereq` | `const FwUpdateReq*` | What to flash (name, type, location, reboot) | +| `callback` | `UpdateCallback` | Your function that tracks progress | + +**The `FwUpdateReq` struct:** +```c +typedef struct { + const char *firmwareName; // "firmware_v2.bin" β€” REQUIRED + const char *TypeOfFirmware; // "PCI", "PDRI", or "PERIPHERAL" β€” REQUIRED + const char *LocationOfFirmware; // Path to file (NULL = default from device.properties) + bool rebootImmediately; // true = reboot when done; false = you reboot manually +} FwUpdateReq; +``` + +**Your callback signature:** +```c +void my_update_cb(int progress_percent, UpdateStatus status); +``` + +| `status` value | Meaning | +|----------------|---------| +| `UPDATE_IN_PROGRESS` | Still flashing β€” `progress_percent` is 0–99 | +| `UPDATE_COMPLETED` | Done! Firmware installed successfully | +| `UPDATE_ERROR` | Flash failed | + +**Returns:** +| Value | Meaning | +|-------|---------| +| `RDKFW_UPDATE_SUCCESS` | Daemon accepted β€” flash starting | +| `RDKFW_UPDATE_FAILED` | Couldn't start β€” bad handle, daemon rejected, already flashing, etc. | + +**Behavior:** +- βœ… Returns immediately +- βœ… Return value is **accurate** β€” reflects daemon's accept/reject decision +- βœ… Callback fires **multiple times** (once per progress update) +- ⚠️ Only **one** flash at a time β€” second call is rejected +- ⚠️ If `rebootImmediately` is true, the device **will reboot** after flash completes +- πŸ• Typical flash time: 5–60 minutes (max 1 hour safety timeout) + +**Example:** +```c +void on_flash_progress(int percent, UpdateStatus status) { + if (status == UPDATE_IN_PROGRESS) { + printf("Flashing: %d%%\n", percent); + } else if (status == UPDATE_COMPLETED) { + printf("Flash complete! Firmware installed.\n"); + } else { + printf("Flash FAILED!\n"); + } +} + +FwUpdateReq request = { + .firmwareName = "firmware_v2.bin", + .TypeOfFirmware = "PCI", + .LocationOfFirmware = NULL, // use default path + .rebootImmediately = false // don't reboot automatically +}; + +UpdateResult result = updateFirmware(handle, &request, on_flash_progress); +``` + +--- + +### 4.5 `unregisterProcess()` + +> **"I'm leaving."** β€” Disconnect from the daemon. + +```c +void unregisterProcess(FirmwareInterfaceHandle handler); +``` + +**What it does:** +- Tells the daemon you're disconnecting +- Frees the handle memory +- After this, your handle is **invalid** β€” don't use it again + +**Parameters:** +| Name | Type | Description | +|------|------|-------------| +| `handler` | `FirmwareInterfaceHandle` | Your ID from `registerProcess()` | + +**Behavior:** +- βœ… Safe to call with `NULL` (does nothing) +- βœ… Synchronous β€” blocks briefly for D-Bus call +- βœ… Best-effort β€” if D-Bus is dead, it still frees local memory +- β›” **Will REFUSE** to unregister if a `checkForUpdate()`, `downloadFirmware()`, or `updateFirmware()` is still in progress β€” you must wait for the callback first +- ⚠️ After this call, your handle is **gone** β€” don't use it for anything + +**Example:** +```c +// Always call before your app exits +unregisterProcess(handle); +handle = NULL; // good practice: avoid using stale handle +``` + +--- + +## 5. The On-Demand Worker Thread Model + +This is the heart of the library's design. Understanding this section explains *why* the APIs work the way they do. + +### 5.1 What Problem Does It Solve? + +The library needs to do two things at once: +1. **Return quickly** to your app (so your app isn't frozen) +2. **Wait for the daemon** (which can take seconds, minutes, or even an hour) + +You can't do both in one thread. If your app's thread waits for the daemon, your app is stuck. If you return immediately, who listens for the daemon's answer? + +**Answer: A worker thread.** The library creates a temporary background thread to wait for the daemon, while your app continues running. + +### 5.2 How It Works β€” The Restaurant Analogy + +Imagine you're at a restaurant: + +``` +1. You (the app) sit down at a table. + +2. You call the waiter (the library API): + "I'd like to order the firmware check, please." + +3. The waiter writes down your order and hands it to a RUNNER + (the worker thread). The runner goes to the kitchen (the daemon). + +4. The waiter comes back to you immediately: + "Your order has been placed!" (API returns SUCCESS) + +5. You're free to chat, check your phone, whatever. + (Your app continues running) + +6. Meanwhile, the runner is standing in the kitchen, + waiting for the chef to finish cooking. + (Worker thread waits for D-Bus signal from daemon) + +7. When the food is ready, the runner brings it to your table + and calls out: "Your firmware check result is here!" + (Worker thread fires your callback) + +8. The runner's job is done. They clock out and go home. + (Worker thread cleans up and exits) +``` + +**Key insight:** The runner (worker thread) only exists for the duration of your order. There's no runner standing around when nobody has ordered anything. This is the "on-demand" part. + +### 5.3 Technical Deep Dive + +Here's what actually happens under the hood when you call, say, `checkForUpdate()`: + +``` +YOUR APP THREAD WORKER THREAD (created per call) +═══════════════ ════════════════════════════════ + +checkForUpdate(handle, my_callback) + β”‚ + β”œβ”€ 1. Validate inputs + β”‚ (is handle valid? is callback non-NULL?) + β”‚ + β”œβ”€ 2. Allocate context (ctx) on the heap + β”‚ ctx = { + β”‚ handle_key: "42857" (copied from your handle) + β”‚ callback: my_callback (pointer to your function) + β”‚ ready_mutex / ready_cond (for synchronization) + β”‚ is_ready: false + β”‚ init_failed: false + β”‚ } + β”‚ + β”œβ”€ 3. Reject if already in progress + β”‚ (only one check at a time allowed) + β”‚ + β”œβ”€ 4. pthread_create() ──────────────────▢ WORKER STARTS + β”‚ β”‚ + β”‚ β”œβ”€ Create isolated GLib event loop + β”‚ β”‚ (GMainContext + GMainLoop) + β”‚ β”‚ + β”‚ β”œβ”€ Connect to D-Bus system bus + β”‚ β”‚ (g_bus_get_sync) + β”‚ β”‚ + β”‚ β”œβ”€ Subscribe to daemon signal: + β”‚ β”‚ "CheckForUpdateComplete" + β”‚ β”‚ + β”‚ β”œβ”€ Send D-Bus method call: + β”‚ β”‚ "CheckForUpdate" with handle + β”‚ β”‚ + β”‚ ◀─── 5. Worker signals "READY" ─────────── + β”‚ (condvar: is_ready = true) β”‚ + β”‚ β”œβ”€ Start event loop: g_main_loop_run() + β”œβ”€ 6. Check: did worker init OK? β”‚ ...waiting for signal from daemon... + β”‚ YES β†’ return SUCCESS β”‚ ...could take 5-120 seconds... + β”‚ NO β†’ join thread, return FAIL β”‚ + β”‚ β”‚ + β”‚ YOUR APP IS FREE HERE β”‚ + β”‚ (do whatever you want) β”‚ + β”‚ β”‚ SIGNAL ARRIVES from daemon! + β”‚ β”œβ”€ Parse signal data + β”‚ β”‚ (version, status, details) + β”‚ β”‚ + β”‚ β”œβ”€ Build FwInfoData struct + β”‚ β”‚ + β”‚ β”œβ”€ ═══▢ my_callback(&fwinfo) ◀═══ + β”‚ β”‚ YOUR CODE RUNS HERE + β”‚ β”‚ (in the worker thread) + β”‚ β”‚ + β”‚ β”œβ”€ Cleanup: + β”‚ β”‚ - Unsubscribe from signals + β”‚ β”‚ - Close D-Bus connection + β”‚ β”‚ - Destroy event loop + β”‚ β”‚ - Free ctx and all strings + β”‚ β”‚ + β”‚ └─ WORKER EXITS + β–Ό +``` + +### 5.4 The Condvar Handshake + +The trickiest part of the design is **step 5** β€” the "ready" handshake. This is how your app knows the worker started successfully. + +**The problem:** Your app needs to know *right away* if the check will work. But the worker needs a moment to connect to D-Bus and set things up. How do you coordinate? + +**The solution: A condition variable (condvar).** + +Think of it like a walkie-talkie: + +``` +Your App: "Worker, are you set up? Over." (pthread_cond_wait) + ... waiting ... +Worker: "Roger that, I'm connected to (pthread_cond_signal) + D-Bus and listening. Over." +Your App: "Great, returning SUCCESS to (function returns) + the caller." +``` + +If the worker can't connect (D-Bus is dead, system error): + +``` +Your App: "Worker, are you set up? Over." (pthread_cond_wait) + ... waiting ... +Worker: "Negative. D-Bus connection (init_failed = true, + failed. Over and out." pthread_cond_signal) +Your App: "Copy. Returning FAIL to the (function returns FAIL) + caller." +``` + +There's also a **safety timeout** (10 seconds). If the worker doesn't respond at all (crashed, stuck), the app gives up and returns FAIL rather than hanging forever. + +### 5.5 Why On-Demand Instead of Persistent? + +The library used to have a **persistent background thread** β€” a thread that was created when the library loaded and ran continuously until the library unloaded. Here's why on-demand is better: + +| Aspect | Persistent Thread (Old) | On-Demand Thread (Current) | +|--------|------------------------|---------------------------| +| **Resource usage when idle** | Thread + D-Bus connection + event loop running 24/7 | **Zero** β€” nothing running | +| **Thread lifetime** | Minutes to hours (entire app lifetime) | Seconds to minutes (one operation) | +| **Complexity** | Registry of callbacks, signal routing, subscription management | Simple: one thread, one job, self-contained | +| **Multi-client isolation** | Shared thread serving multiple registrations | Each call gets its own isolated thread | +| **Cleanup** | Complex shutdown: stop loop, drain queues, join thread | Simple: thread cleans up after itself | +| **Failure blast radius** | If BG thread dies, ALL pending operations fail | If one thread dies, only THAT operation fails | + +**The key principle:** *"If nobody is checking for updates right now, there should be zero threads, zero D-Bus connections, and zero event loops running."* + +--- + +## 6. Complete Workflow Example + +Here's how a real application uses the library from start to finish: + +```c +#include "rdkFwupdateMgr_client.h" +#include +#include + +/* Step 1: Define your callbacks */ + +void on_check(const FwInfoData *info) { + printf("Check result: %d\n", info->status); + if (info->status == FIRMWARE_AVAILABLE) { + printf(" New version: %s\n", info->UpdateDetails->FwVersion); + printf(" Filename: %s\n", info->UpdateDetails->FwFileName); + // Signal main thread to proceed with download... + } +} + +void on_download(int percent, DownloadStatus status) { + printf("Download: %d%% [%s]\n", percent, + status == DWNL_COMPLETED ? "DONE" : + status == DWNL_ERROR ? "FAILED" : "..."); + // Signal main thread when terminal status received... +} + +void on_flash(int percent, UpdateStatus status) { + printf("Flash: %d%% [%s]\n", percent, + status == UPDATE_COMPLETED ? "DONE" : + status == UPDATE_ERROR ? "FAILED" : "..."); + // Signal main thread when terminal status received... +} + +int main() { + /* Step 2: Register */ + FirmwareInterfaceHandle handle = registerProcess("MyApp", LIB_VERSION); + if (!handle) { return 1; } + + /* Step 3: Check for update */ + checkForUpdate(handle, on_check); + // on_check fires later in a background thread... + // (your app waits for callback via pthread_cond_wait or similar) + + /* Step 4: Download (after check callback says FIRMWARE_AVAILABLE) */ + FwDwnlReq dl = { .firmwareName = "firmware_v2.bin" }; + downloadFirmware(handle, &dl, on_download); + // on_download fires multiple times as download progresses... + + /* Step 5: Flash (after download callback says DWNL_COMPLETED) */ + FwUpdateReq upd = { + .firmwareName = "firmware_v2.bin", + .TypeOfFirmware = "PCI", + .rebootImmediately = false + }; + updateFirmware(handle, &upd, on_flash); + // on_flash fires multiple times as flash progresses... + + /* Step 6: Unregister (after flash callback says UPDATE_COMPLETED) */ + unregisterProcess(handle); + handle = NULL; + + return 0; +} +``` + +--- + +## 7. Thread Safety β€” How the Library Keeps Things Safe + +Multiple threads accessing shared data can cause chaos. Here's how the library prevents it: + +### 7.1 One-at-a-Time Enforcement + +Each operation type (`check`, `download`, `update`) has a **global boolean flag** guarded by a **mutex**: + +``` +g_check_in_progress ──── protected by g_check_in_progress_mutex +g_dwnl_in_progress ──── protected by g_dwnl_in_progress_mutex +g_update_in_progress ──── protected by g_update_in_progress_mutex +``` + +When you call `checkForUpdate()`, the library does this atomically (under lock): +1. Check: is `g_check_in_progress` true? β†’ If yes, REJECT (return FAIL) +2. Set `g_check_in_progress = true` +3. Store reference to the active context + +When the worker finishes, it atomically sets `g_check_in_progress = false`. + +This means: **you can never have two checks running at the same time**, even if two threads call `checkForUpdate()` simultaneously. + +### 7.2 State Transition Functions + +Instead of exposing raw mutexes, the library uses clean accessor functions: + +| Function | What it does | +|----------|-------------| +| `internal_begin_check(ctx)` | Atomically: if idle β†’ set in-progress + track ctx; if busy β†’ reject | +| `internal_end_check()` | Atomically: set idle + clear ctx (worker calls this when done) | +| `internal_abort_check()` | Same as end, but for error paths (API call itself failed) | +| `internal_is_check_in_progress()` | Atomically: return whether a check is active | + +The same pattern exists for `download` and `update`. Nobody directly touches the booleans or mutexes β€” they go through these functions. + +### 7.3 Per-Request Isolation + +Each API call creates its own: +- **GMainContext** (GLib event loop context β€” isolated from other threads) +- **GMainLoop** (event loop β€” only this thread's signals are dispatched here) +- **GDBusConnection** (D-Bus connection β€” not shared with anyone) +- **Condvar** (`ready_mutex` / `ready_cond` β€” only this call's handshake) + +This means worker threads are completely isolated from each other. A check thread and a download thread running at the same time won't interfere. + +--- + +## 8. Memory Ownership β€” Who Frees What? + +Memory bugs (leaks, double-frees, use-after-free) are a major source of crashes. Here are the clear ownership rules: + +### 8.1 The Handle + +``` +registerProcess() ──▢ malloc(handle_str) ──▢ You use it ──▢ unregisterProcess() ──▢ free(handle_str) +``` + +- **Created by:** `registerProcess()` (via `malloc`) +- **Owned by:** The library (you have read-only access) +- **Freed by:** `unregisterProcess()` (via `free`) +- **Your responsibility:** Never call `free()` on the handle yourself + +### 8.2 The Request Context (`ctx`) + +``` +API call ──▢ calloc(ctx) ──▢ pthread_create ──▢ [condvar handshake] ──▢ ... + +PATH A (success): + API returns SUCCESS ──▢ ctx now owned by WORKER THREAD ──▢ worker frees ctx when done + +PATH B (init failure): + Worker signals failure ──▢ API joins worker ──▢ API frees ctx +``` + +- **Created by:** The API function (`checkForUpdate`, `downloadFirmware`, `updateFirmware`) +- **Ownership transfer:** After the condvar handshake, the worker thread owns `ctx` +- **Freed by:** Whoever owns it at that point + +### 8.3 Callback Data + +- `FwInfoData*` in `UpdateEventCallback` β†’ **valid only during the callback**. Copy what you need! +- Progress values (`int percent`, `DownloadStatus`) β†’ simple values, no memory to manage + +--- + +## 9. Error Handling β€” What Can Go Wrong? + +| Scenario | What happens | +|----------|-------------| +| Daemon not running | `registerProcess()` returns `NULL`; async APIs return FAIL | +| D-Bus system bus down | Same as above β€” all D-Bus operations fail | +| Invalid/NULL handle | API returns FAIL immediately (input validation) | +| NULL callback | API returns FAIL immediately | +| Already in progress | API returns FAIL (one-at-a-time enforcement) | +| Worker thread can't connect | Condvar handshake reports failure β†’ API returns FAIL | +| Daemon rejects request | `downloadFirmware()`/`updateFirmware()` return FAIL (accurate) | +| Daemon signal never arrives | Safety timeout fires (120s for check, 3600s for download/flash) | +| `malloc`/`calloc` fails | Cascading cleanup β€” everything allocated so far is freed, returns FAIL | +| `pthread_create` fails | In-progress flag reset via `abort_*()`, ctx freed, returns FAIL | +| Library unloaded while worker running | Destructor joins all active worker threads before unloading | +| App crashes in callback | Worker thread dies; in-progress flag stays `true` permanently (known limitation) | + +--- + +## 10. Callbacks β€” The Rules + +Since callbacks are the main way you receive results, here are the important rules: + +### βœ… DO: +- Copy any data you need from the callback arguments (data is temporary) +- Keep callbacks **short** β€” don't do heavy work inside them +- Use mutexes/condvars to signal your main thread from the callback +- Handle both success and error cases + +### ❌ DON'T: +- **Don't call other library APIs** from inside a callback (can deadlock) +- Don't call `unregisterProcess()` from inside a callback +- Don't assume which thread the callback runs on (it's a worker thread) +- Don't store the `FwInfoData*` pointer β€” it's invalid after the callback returns +- Don't let your callback crash (the worker thread will die, and the in-progress flag stays stuck) + +### Callback Summary Table: + +| API | Callback Type | # of Calls | When | +|-----|--------------|------------|------| +| `checkForUpdate` | `UpdateEventCallback` | **1** (or 0 on timeout) | When daemon finishes checking XConf | +| `downloadFirmware` | `DownloadCallback` | **Many** | Each progress update (0%β†’100%) | +| `updateFirmware` | `UpdateCallback` | **Many** | Each progress update (0%β†’100%) | + +--- + +## 11. Quick Reference Card + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ librdkFwupdateMgr Quick Reference β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ β”‚ +β”‚ LIFECYCLE: β”‚ +β”‚ handle = registerProcess("AppName", "1.0") β†’ get your ticket β”‚ +β”‚ unregisterProcess(handle) β†’ give it back β”‚ +β”‚ β”‚ +β”‚ CHECK: β”‚ +β”‚ checkForUpdate(handle, callback) β†’ is there an update? β”‚ +β”‚ callback fires ONCE with FwInfoData* β”‚ +β”‚ β”‚ +β”‚ DOWNLOAD: β”‚ +β”‚ downloadFirmware(handle, &request, callback) β†’ download firmware β”‚ +β”‚ callback fires MANY TIMES with (percent, status) β”‚ +β”‚ β”‚ +β”‚ FLASH: β”‚ +β”‚ updateFirmware(handle, &request, callback) β†’ flash firmware β”‚ +β”‚ callback fires MANY TIMES with (percent, status) β”‚ +β”‚ β”‚ +β”‚ RULES: β”‚ +β”‚ βœ“ One operation per type at a time β”‚ +β”‚ βœ“ All async APIs return immediately β”‚ +β”‚ βœ“ Results come through callbacks (in background thread) β”‚ +β”‚ βœ“ Copy callback data β€” it's temporary β”‚ +β”‚ βœ— Don't call APIs from inside callbacks β”‚ +β”‚ βœ— Don't free the handle yourself β”‚ +β”‚ βœ— Don't use handle after unregisterProcess() β”‚ +β”‚ β”‚ +β”‚ THREAD MODEL: β”‚ +β”‚ On-demand workers: thread created per API call, exits when done β”‚ +β”‚ Zero cost when idle: no threads, no connections, no event loops β”‚ +β”‚ Condvar handshake: API waits ~100ms for worker setup confirmation β”‚ +β”‚ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` diff --git a/kwib_src/KnowWhereItBreaks.c b/kwib_src/KnowWhereItBreaks.c new file mode 100755 index 00000000..db9aeffc --- /dev/null +++ b/kwib_src/KnowWhereItBreaks.c @@ -0,0 +1,1328 @@ +/* + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file KnowWhereItBreaks.c + * @brief Comprehensive developer test utility for librdkFwupdateMgr.so + * + * This is NOT the example_plugin (which is a clean reference for external teams). + * This is YOUR developer weapon for exercising every code path in the library + * and daemon before bugs find you. + * + * Tests every layer: + * - Library input validation (NULL/empty args β†’ immediate FAIL) + * - Library in-progress guards (duplicate same-process call β†’ rejected) + * - Library session guards (unregister during active op β†’ blocked) + * - Condvar handshake accuracy (return code matches daemon reply) + * - Worker thread lifecycle (create β†’ run β†’ cleanup β†’ exit, no leaks) + * - D-Bus method calls and signal reception + * - Callback correctness (right data, right count, right thread) + * - Full lifecycle (register β†’ check β†’ download β†’ update β†’ unregister) + * - Rapid retry (call again immediately after previous completes) + * + * Usage: + * ./KnowWhereItBreaks (interactive menu) + * ./KnowWhereItBreaks --auto-error (error/validation tests) + * ./KnowWhereItBreaks --auto-happy (happy path β€” daemon required) + * ./KnowWhereItBreaks --full-lifecycle (end-to-end β€” daemon required) + * ./KnowWhereItBreaks --auto-all (everything) + * + * See KnowWhereItBreaks_README.md for full documentation. + */ + +#include "rdkFwupdateMgr_client.h" +#include +#include +#include +#include +#include +#include +#include + +/* ======================================================================== + * TEST INFRASTRUCTURE + * ======================================================================== */ + +typedef struct { + int total; + int passed; + int failed; + int skipped; +} TestResults; + +static TestResults g_results = {0, 0, 0, 0}; + +#define TEST_PASS(name) do { \ + g_results.total++; g_results.passed++; \ + printf(" [\033[32mPASS\033[0m] %s\n", name); \ +} while(0) + +#define TEST_FAIL(name, reason) do { \ + g_results.total++; g_results.failed++; \ + printf(" [\033[31mFAIL\033[0m] %s β€” %s\n", name, reason); \ +} while(0) + +#define TEST_SKIP(name, reason) do { \ + g_results.total++; g_results.skipped++; \ + printf(" [\033[33mSKIP\033[0m] %s β€” %s\n", name, reason); \ +} while(0) + +#define TEST_INFO(fmt, ...) printf(" [INFO] " fmt "\n", ##__VA_ARGS__) + +/* ======================================================================== + * CALLBACK TRACKING STATE + * ======================================================================== + * Volatile: callbacks fire from worker threads, main thread polls these. + * ======================================================================== */ + +/* CheckForUpdate tracking */ +static volatile bool g_check_cb_fired = false; +static volatile int g_check_cb_count = 0; +static volatile int g_check_status = -1; +static char g_check_current_ver[MAX_FW_VERSION_SIZE] = {0}; + +/* DownloadFirmware tracking */ +static volatile bool g_dwnl_cb_terminal = false; +static volatile int g_dwnl_cb_count = 0; +static volatile int g_dwnl_status = -1; +static volatile int g_dwnl_last_progress = -1; +static volatile bool g_dwnl_progress_mono = true; + +/* UpdateFirmware tracking */ +static volatile bool g_update_cb_terminal = false; +static volatile int g_update_cb_count = 0; +static volatile int g_update_status = -1; +static volatile int g_update_last_progress = -1; +static volatile bool g_update_progress_mono = true; + +/* Global handle */ +static FirmwareInterfaceHandle g_handle = NULL; + +/* ======================================================================== + * CALLBACKS + * ======================================================================== */ + +static void check_callback(const FwInfoData *info) +{ + g_check_cb_count++; + if (info) { + g_check_status = info->status; + if (info->CurrFWVersion[0] != '\0') { + strncpy(g_check_current_ver, info->CurrFWVersion, + sizeof(g_check_current_ver) - 1); + } + printf(" [CB:Check] #%d status=%d current='%s'\n", + g_check_cb_count, info->status, info->CurrFWVersion); + } else { + printf(" [CB:Check] #%d β€” NULL info!\n", g_check_cb_count); + } + g_check_cb_fired = true; +} + +static void download_callback(int progress, DownloadStatus status) +{ + int prev = g_dwnl_last_progress; + g_dwnl_cb_count++; + g_dwnl_last_progress = progress; + g_dwnl_status = (int)status; + + /* Track monotonicity */ + if (prev >= 0 && progress < prev) { + g_dwnl_progress_mono = false; + } + + printf(" [CB:Dwnl] #%d progress=%d%% status=%d\n", + g_dwnl_cb_count, progress, (int)status); + + if (status == DWNL_COMPLETED || status == DWNL_ERROR) { + g_dwnl_cb_terminal = true; + } +} + +static void update_callback(int progress, UpdateStatus status) +{ + int prev = g_update_last_progress; + g_update_cb_count++; + g_update_last_progress = progress; + g_update_status = (int)status; + + /* Track monotonicity */ + if (prev >= 0 && progress < prev) { + g_update_progress_mono = false; + } + + printf(" [CB:Update] #%d progress=%d%% status=%d\n", + g_update_cb_count, progress, (int)status); + + if (status == UPDATE_COMPLETED || status == UPDATE_ERROR) { + g_update_cb_terminal = true; + } +} + +/* ======================================================================== + * HELPERS + * ======================================================================== */ + +static void reset_all(void) +{ + g_check_cb_fired = false; + g_check_cb_count = 0; + g_check_status = -1; + g_check_current_ver[0] = '\0'; + + g_dwnl_cb_terminal = false; + g_dwnl_cb_count = 0; + g_dwnl_status = -1; + g_dwnl_last_progress = -1; + g_dwnl_progress_mono = true; + + g_update_cb_terminal = false; + g_update_cb_count = 0; + g_update_status = -1; + g_update_last_progress = -1; + g_update_progress_mono = true; +} + +/** Poll a volatile bool with timeout. Returns true if flag set before timeout. */ +static bool wait_flag(volatile bool *flag, int timeout_sec) +{ + for (int i = 0; i < timeout_sec * 10; i++) { + if (*flag) return true; + usleep(100000); /* 100ms */ + } + return false; +} + +static void separator(const char *title) +{ + printf("\n"); + printf("══════════════════════════════════════════════════════════════\n"); + printf(" %s\n", title); + printf("══════════════════════════════════════════════════════════════\n"); +} + +/** Register if not already registered. Returns true if handle is available. */ +static bool ensure_registered(void) +{ + if (g_handle != NULL) return true; + g_handle = registerProcess("KnowWhereItBreaks", LIB_VERSION); + if (g_handle != NULL) { + TEST_INFO("Auto-registered: handle='%s'", g_handle); + return true; + } + TEST_INFO("Auto-register FAILED β€” daemon may be down"); + return false; +} + +static void ensure_unregistered(void) +{ + if (g_handle != NULL) { + unregisterProcess(g_handle); + g_handle = NULL; + } +} + +/* ======================================================================== + * TC01–TC04: REGISTER / UNREGISTER + * ======================================================================== */ + +static void tc01_register_happy(void) +{ + printf("\n--- TC01: Register Happy Path ---\n"); + FirmwareInterfaceHandle h = registerProcess("KnowWhereItBreaks", LIB_VERSION); + if (h != NULL && strlen(h) > 0) { + TEST_PASS("TC01 β€” registerProcess() returned valid handle"); + printf(" Handle: '%s'\n", h); + g_handle = h; + } else { + TEST_FAIL("TC01 β€” registerProcess()", "Returned NULL or empty handle"); + } +} + +static void tc02_unregister_happy(void) +{ + printf("\n--- TC02: Unregister Happy Path ---\n"); + if (!g_handle) { TEST_SKIP("TC02", "No handle available"); return; } + /* unregisterProcess returns void β€” if it doesn't crash, it passed */ + unregisterProcess(g_handle); + TEST_PASS("TC02 β€” unregisterProcess() completed without crash"); + g_handle = NULL; +} + +static void tc03_unregister_null(void) +{ + printf("\n--- TC03: Unregister NULL Handle ---\n"); + /* unregisterProcess(NULL) should not crash (safe to call with NULL per docs) */ + unregisterProcess(NULL); + TEST_PASS("TC03 β€” unregisterProcess(NULL) did not crash"); +} + +static void tc04_double_register(void) +{ + printf("\n--- TC04: Double Register ---\n"); + FirmwareInterfaceHandle h1 = registerProcess("KWIB_Test1", LIB_VERSION); + FirmwareInterfaceHandle h2 = registerProcess("KWIB_Test2", LIB_VERSION); + + if (h1 != NULL && h2 != NULL) { + TEST_PASS("TC04 β€” Both registrations succeeded"); + printf(" Handle1='%s' Handle2='%s'\n", h1, h2); + unregisterProcess(h2); + if (!g_handle) g_handle = h1; + else unregisterProcess(h1); + } else { + TEST_FAIL("TC04 β€” double_register", "One or both returned NULL"); + if (h1) unregisterProcess(h1); + if (h2) unregisterProcess(h2); + } +} + +/* ======================================================================== + * TC05–TC11: CHECKFORUPDATE + * ======================================================================== */ + +static void tc05_check_happy(void) +{ + printf("\n--- TC05: CheckForUpdate Happy Path ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC05", "No handle"); return; } + + CheckForUpdateResult ret = checkForUpdate(g_handle, check_callback); + if (ret != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC05 β€” checkForUpdate()", "API returned FAIL"); + return; + } + TEST_INFO("Waiting for callback (max 130s)..."); + if (wait_flag(&g_check_cb_fired, 130)) { + TEST_PASS("TC05 β€” checkForUpdate callback received"); + printf(" Status: %d CurrentVer: '%s'\n", g_check_status, g_check_current_ver); + } else { + TEST_FAIL("TC05 β€” checkForUpdate()", "Callback never fired (130s timeout)"); + } +} + +static void tc06_check_null_handle(void) +{ + printf("\n--- TC06: CheckForUpdate NULL Handle ---\n"); + CheckForUpdateResult ret = checkForUpdate(NULL, check_callback); + if (ret == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC06 β€” NULL handle rejected"); + else + TEST_FAIL("TC06 β€” NULL handle", "Expected FAIL"); +} + +static void tc07_check_null_callback(void) +{ + printf("\n--- TC07: CheckForUpdate NULL Callback ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC07", "No handle"); return; } + CheckForUpdateResult ret = checkForUpdate(g_handle, NULL); + if (ret == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC07 β€” NULL callback rejected"); + else + TEST_FAIL("TC07 β€” NULL callback", "Expected FAIL"); +} + +static void tc08_check_empty_handle(void) +{ + printf("\n--- TC08: CheckForUpdate Empty Handle ---\n"); + CheckForUpdateResult ret = checkForUpdate("", check_callback); + if (ret == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC08 β€” empty handle rejected"); + else + TEST_FAIL("TC08 β€” empty handle", "Expected FAIL"); +} + +static void tc09_check_duplicate(void) +{ + printf("\n--- TC09: CheckForUpdate Duplicate (Same Process) ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC09", "No handle"); return; } + + CheckForUpdateResult r1 = checkForUpdate(g_handle, check_callback); + if (r1 != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC09 β€” first call", "First checkForUpdate failed"); + return; + } + + /* Immediately call again β€” library guard should reject */ + CheckForUpdateResult r2 = checkForUpdate(g_handle, check_callback); + if (r2 == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC09 β€” duplicate call rejected by library guard"); + else + TEST_FAIL("TC09 β€” duplicate call", "Second call was NOT rejected"); + + TEST_INFO("Waiting for first check to complete..."); + wait_flag(&g_check_cb_fired, 130); +} + +static void tc10_check_rapid_retry(void) +{ + printf("\n--- TC10: CheckForUpdate Rapid Retry ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC10", "No handle"); return; } + + /* First call */ + CheckForUpdateResult r1 = checkForUpdate(g_handle, check_callback); + if (r1 != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC10 β€” first call", "Failed"); + return; + } + TEST_INFO("Waiting for first callback..."); + if (!wait_flag(&g_check_cb_fired, 130)) { + TEST_FAIL("TC10", "First callback never fired"); + return; + } + + sleep(1); /* Let worker thread fully exit */ + reset_all(); + + /* Retry β€” should succeed (guard cleared after previous completed) */ + CheckForUpdateResult r2 = checkForUpdate(g_handle, check_callback); + if (r2 == CHECK_FOR_UPDATE_SUCCESS) { + TEST_PASS("TC10 β€” rapid retry accepted (guard was cleared)"); + wait_flag(&g_check_cb_fired, 130); + } else { + TEST_FAIL("TC10 β€” rapid retry", "Rejected (in-progress flag not cleared?)"); + } +} + +static void tc11_check_callback_data(void) +{ + printf("\n--- TC11: CheckForUpdate Callback Data Validation ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC11", "No handle"); return; } + + CheckForUpdateResult ret = checkForUpdate(g_handle, check_callback); + if (ret != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC11", "API returned FAIL"); + return; + } + if (!wait_flag(&g_check_cb_fired, 130)) { + TEST_FAIL("TC11", "Callback never fired"); + return; + } + + if (g_check_cb_count == 1) + TEST_PASS("TC11 β€” callback fired exactly once"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Callback fired %d times (expected 1)", g_check_cb_count); + TEST_FAIL("TC11", msg); + } + + if (g_check_status >= 0 && g_check_status <= 5) + TEST_PASS("TC11 β€” status is valid CheckForUpdateStatus enum"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "status=%d not in [0..5]", g_check_status); + TEST_FAIL("TC11", msg); + } +} + +/* ======================================================================== + * TC12–TC22: DOWNLOAD FIRMWARE + * ======================================================================== */ + +static FwDwnlReq make_dwnl_req(void) +{ + FwDwnlReq req; + memset(&req, 0, sizeof(req)); + req.firmwareName = "test_firmware.bin"; + req.downloadUrl = "http://localhost:8080/firmware/test_firmware.bin"; + req.TypeOfFirmware = "PCI"; + return req; +} + +static void tc12_download_happy(void) +{ + printf("\n--- TC12: DownloadFirmware Happy Path ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC12", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware(g_handle, &req, download_callback); + if (ret != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC12", "API returned FAILED (daemon rejected?)"); + return; + } + TEST_PASS("TC12 β€” daemon accepted download request"); + + TEST_INFO("Waiting for terminal callback (max 600s)..."); + if (wait_flag(&g_dwnl_cb_terminal, 600)) { + if (g_dwnl_status == (int)DWNL_COMPLETED) + TEST_PASS("TC12 β€” download completed successfully"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Terminal status=%d (expected COMPLETED=%d)", g_dwnl_status, DWNL_COMPLETED); + TEST_FAIL("TC12", msg); + } + } else { + TEST_FAIL("TC12", "Terminal callback never fired (600s timeout)"); + } +} + +static void tc13_download_null_handle(void) +{ + printf("\n--- TC13: DownloadFirmware NULL Handle ---\n"); + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware(NULL, &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC13 β€” NULL handle rejected"); + else TEST_FAIL("TC13", "Expected FAILED"); +} + +static void tc14_download_null_request(void) +{ + printf("\n--- TC14: DownloadFirmware NULL Request ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC14", "No handle"); return; } + DownloadResult ret = downloadFirmware(g_handle, NULL, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC14 β€” NULL request rejected"); + else TEST_FAIL("TC14", "Expected FAILED"); +} + +static void tc15_download_null_callback(void) +{ + printf("\n--- TC15: DownloadFirmware NULL Callback ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC15", "No handle"); return; } + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware(g_handle, &req, NULL); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC15 β€” NULL callback rejected"); + else TEST_FAIL("TC15", "Expected FAILED"); +} + +static void tc16_download_null_firmware_name(void) +{ + printf("\n--- TC16: DownloadFirmware NULL Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC16", "No handle"); return; } + FwDwnlReq req = make_dwnl_req(); + req.firmwareName = NULL; + DownloadResult ret = downloadFirmware(g_handle, &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC16 β€” NULL firmwareName rejected"); + else TEST_FAIL("TC16", "Expected FAILED"); +} + +static void tc17_download_empty_firmware_name(void) +{ + printf("\n--- TC17: DownloadFirmware Empty Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC17", "No handle"); return; } + FwDwnlReq req = make_dwnl_req(); + req.firmwareName = ""; + DownloadResult ret = downloadFirmware(g_handle, &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC17 β€” empty firmwareName rejected"); + else TEST_FAIL("TC17", "Expected FAILED"); +} + +static void tc18_download_duplicate(void) +{ + printf("\n--- TC18: DownloadFirmware Duplicate (Same Process) ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC18", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + DownloadResult r1 = downloadFirmware(g_handle, &req, download_callback); + if (r1 != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC18 β€” first call", "First download failed"); + return; + } + + DownloadResult r2 = downloadFirmware(g_handle, &req, download_callback); + if (r2 == RDKFW_DWNL_FAILED) + TEST_PASS("TC18 β€” duplicate download rejected by library guard"); + else + TEST_FAIL("TC18", "Second call was NOT rejected"); + + TEST_INFO("Waiting for first download to complete..."); + wait_flag(&g_dwnl_cb_terminal, 600); +} + +static void tc19_download_rapid_retry(void) +{ + printf("\n--- TC19: DownloadFirmware Rapid Retry ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC19", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + + DownloadResult r1 = downloadFirmware(g_handle, &req, download_callback); + if (r1 != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC19 β€” first call", "Failed"); + return; + } + TEST_INFO("Waiting for first download to complete..."); + if (!wait_flag(&g_dwnl_cb_terminal, 600)) { + TEST_FAIL("TC19", "First terminal never fired"); + return; + } + + sleep(1); + reset_all(); + + DownloadResult r2 = downloadFirmware(g_handle, &req, download_callback); + if (r2 == RDKFW_DWNL_SUCCESS) { + TEST_PASS("TC19 β€” rapid retry accepted (guard was cleared)"); + wait_flag(&g_dwnl_cb_terminal, 600); + } else { + TEST_FAIL("TC19", "Rejected (in-progress flag not cleared?)"); + } +} + +static void tc20_download_progress_mono(void) +{ + printf("\n--- TC20: Download Progress Monotonicity ---\n"); + /* Uses data from the most recent download. Run TC12 first. */ + if (g_dwnl_cb_count == 0) { + TEST_SKIP("TC20", "No download has run yet β€” run TC12 first"); + return; + } + if (g_dwnl_cb_count > 1) + TEST_PASS("TC20 β€” multiple progress callbacks fired"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Only %d callback(s)", g_dwnl_cb_count); + TEST_FAIL("TC20", msg); + } + if (g_dwnl_progress_mono) + TEST_PASS("TC20 β€” progress values were monotonically increasing"); + else + TEST_FAIL("TC20", "Progress decreased at some point (daemon bug?)"); +} + +static void tc21_download_terminal(void) +{ + printf("\n--- TC21: Download Terminal Status ---\n"); + if (g_dwnl_cb_count == 0) { + TEST_SKIP("TC21", "No download has run yet"); + return; + } + if (g_dwnl_status == (int)DWNL_COMPLETED || g_dwnl_status == (int)DWNL_ERROR) + TEST_PASS("TC21 β€” final callback had terminal status"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Final status=%d (not COMPLETED or ERROR)", g_dwnl_status); + TEST_FAIL("TC21", msg); + } +} + +static void tc22_download_empty_handle(void) +{ + printf("\n--- TC22: DownloadFirmware Empty Handle ---\n"); + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware("", &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC22 β€” empty handle rejected"); + else TEST_FAIL("TC22", "Expected FAILED"); +} + +/* ======================================================================== + * TC23–TC33: UPDATE FIRMWARE + * ======================================================================== */ + +static FwUpdateReq make_update_req(void) +{ + FwUpdateReq req; + memset(&req, 0, sizeof(req)); + req.firmwareName = "test_firmware.bin"; + req.TypeOfFirmware = "PCI"; + req.LocationOfFirmware = "/opt/CDL"; + req.rebootImmediately = false; + return req; +} + +static void tc23_update_happy(void) +{ + printf("\n--- TC23: UpdateFirmware Happy Path ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC23", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware(g_handle, &req, update_callback); + if (ret != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC23", "API returned FAILED (daemon rejected?)"); + return; + } + TEST_PASS("TC23 β€” daemon accepted update request"); + + TEST_INFO("Waiting for terminal callback (max 600s)..."); + if (wait_flag(&g_update_cb_terminal, 600)) { + if (g_update_status == (int)UPDATE_COMPLETED) + TEST_PASS("TC23 β€” update completed successfully"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Terminal status=%d (expected COMPLETED=%d)", g_update_status, UPDATE_COMPLETED); + TEST_FAIL("TC23", msg); + } + } else { + TEST_FAIL("TC23", "Terminal callback never fired (600s timeout)"); + } +} + +static void tc24_update_null_handle(void) +{ + printf("\n--- TC24: UpdateFirmware NULL Handle ---\n"); + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware(NULL, &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC24 β€” NULL handle rejected"); + else TEST_FAIL("TC24", "Expected FAILED"); +} + +static void tc25_update_null_request(void) +{ + printf("\n--- TC25: UpdateFirmware NULL Request ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC25", "No handle"); return; } + UpdateResult ret = updateFirmware(g_handle, NULL, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC25 β€” NULL request rejected"); + else TEST_FAIL("TC25", "Expected FAILED"); +} + +static void tc26_update_null_callback(void) +{ + printf("\n--- TC26: UpdateFirmware NULL Callback ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC26", "No handle"); return; } + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware(g_handle, &req, NULL); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC26 β€” NULL callback rejected"); + else TEST_FAIL("TC26", "Expected FAILED"); +} + +static void tc27_update_null_firmware_name(void) +{ + printf("\n--- TC27: UpdateFirmware NULL Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC27", "No handle"); return; } + FwUpdateReq req = make_update_req(); + req.firmwareName = NULL; + UpdateResult ret = updateFirmware(g_handle, &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC27 β€” NULL firmwareName rejected"); + else TEST_FAIL("TC27", "Expected FAILED"); +} + +static void tc28_update_empty_firmware_name(void) +{ + printf("\n--- TC28: UpdateFirmware Empty Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC28", "No handle"); return; } + FwUpdateReq req = make_update_req(); + req.firmwareName = ""; + UpdateResult ret = updateFirmware(g_handle, &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC28 β€” empty firmwareName rejected"); + else TEST_FAIL("TC28", "Expected FAILED"); +} + +static void tc29_update_duplicate(void) +{ + printf("\n--- TC29: UpdateFirmware Duplicate (Same Process) ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC29", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + UpdateResult r1 = updateFirmware(g_handle, &req, update_callback); + if (r1 != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC29 β€” first call", "First update failed"); + return; + } + + UpdateResult r2 = updateFirmware(g_handle, &req, update_callback); + if (r2 == RDKFW_UPDATE_FAILED) + TEST_PASS("TC29 β€” duplicate update rejected by library guard"); + else + TEST_FAIL("TC29", "Second call was NOT rejected"); + + TEST_INFO("Waiting for first update to complete..."); + wait_flag(&g_update_cb_terminal, 600); +} + +static void tc30_update_rapid_retry(void) +{ + printf("\n--- TC30: UpdateFirmware Rapid Retry ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC30", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + + UpdateResult r1 = updateFirmware(g_handle, &req, update_callback); + if (r1 != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC30 β€” first call", "Failed"); + return; + } + TEST_INFO("Waiting for first update to complete..."); + if (!wait_flag(&g_update_cb_terminal, 600)) { + TEST_FAIL("TC30", "First terminal never fired"); + return; + } + + sleep(1); + reset_all(); + + UpdateResult r2 = updateFirmware(g_handle, &req, update_callback); + if (r2 == RDKFW_UPDATE_SUCCESS) { + TEST_PASS("TC30 β€” rapid retry accepted (guard was cleared)"); + wait_flag(&g_update_cb_terminal, 600); + } else { + TEST_FAIL("TC30", "Rejected (in-progress flag not cleared?)"); + } +} + +static void tc31_update_progress_mono(void) +{ + printf("\n--- TC31: Update Progress Monotonicity ---\n"); + if (g_update_cb_count == 0) { + TEST_SKIP("TC31", "No update has run yet β€” run TC23 first"); + return; + } + if (g_update_cb_count > 1) + TEST_PASS("TC31 β€” multiple progress callbacks fired"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Only %d callback(s)", g_update_cb_count); + TEST_FAIL("TC31", msg); + } + if (g_update_progress_mono) + TEST_PASS("TC31 β€” progress values were monotonically increasing"); + else + TEST_FAIL("TC31", "Progress decreased (daemon bug?)"); +} + +static void tc32_update_terminal(void) +{ + printf("\n--- TC32: Update Terminal Status ---\n"); + if (g_update_cb_count == 0) { + TEST_SKIP("TC32", "No update has run yet"); + return; + } + if (g_update_status == (int)UPDATE_COMPLETED || g_update_status == (int)UPDATE_ERROR) + TEST_PASS("TC32 β€” final callback had terminal status"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Final status=%d", g_update_status); + TEST_FAIL("TC32", msg); + } +} + +static void tc33_update_empty_handle(void) +{ + printf("\n--- TC33: UpdateFirmware Empty Handle ---\n"); + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware("", &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC33 β€” empty handle rejected"); + else TEST_FAIL("TC33", "Expected FAILED"); +} + +/* ======================================================================== + * TC34–TC36: UNREGISTER DURING ACTIVE OPERATION + * ======================================================================== */ + +static void tc34_unreg_during_check(void) +{ + printf("\n--- TC34: Unregister During CheckForUpdate ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC34", "No handle"); return; } + + CheckForUpdateResult r1 = checkForUpdate(g_handle, check_callback); + if (r1 != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC34 β€” start check", "Failed"); + return; + } + + /* Immediately try unregister β€” should be blocked by in-progress guard */ + /* Note: unregisterProcess returns void, so we check if handle is still valid afterward */ + /* If guard works, unregister does nothing and we can still wait for callback */ + unregisterProcess(g_handle); + + /* If guard worked, callback should still fire (handle was NOT removed) */ + TEST_INFO("Waiting for check callback (if guard worked, it should still fire)..."); + if (wait_flag(&g_check_cb_fired, 130)) { + TEST_PASS("TC34 β€” callback still fired (unregister was blocked during active check)"); + } else { + TEST_FAIL("TC34", "Callback never fired (unregister may have succeeded during active op!)"); + } + + /* Re-register since handle may be invalidated */ + g_handle = NULL; + ensure_registered(); +} + +static void tc35_unreg_during_download(void) +{ + printf("\n--- TC35: Unregister During Download ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC35", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + DownloadResult r1 = downloadFirmware(g_handle, &req, download_callback); + if (r1 != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC35 β€” start download", "Failed"); + return; + } + + unregisterProcess(g_handle); + + TEST_INFO("Waiting for download terminal callback..."); + if (wait_flag(&g_dwnl_cb_terminal, 600)) { + TEST_PASS("TC35 β€” callback still fired (unregister was blocked during active download)"); + } else { + TEST_FAIL("TC35", "Terminal callback never fired"); + } + + g_handle = NULL; + ensure_registered(); +} + +static void tc36_unreg_during_update(void) +{ + printf("\n--- TC36: Unregister During Update ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC36", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + UpdateResult r1 = updateFirmware(g_handle, &req, update_callback); + if (r1 != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC36 β€” start update", "Failed"); + return; + } + + unregisterProcess(g_handle); + + TEST_INFO("Waiting for update terminal callback..."); + if (wait_flag(&g_update_cb_terminal, 600)) { + TEST_PASS("TC36 β€” callback still fired (unregister was blocked during active update)"); + } else { + TEST_FAIL("TC36", "Terminal callback never fired"); + } + + g_handle = NULL; + ensure_registered(); +} + +/* ======================================================================== + * TC37–TC39: FULL LIFECYCLE + * ======================================================================== */ + +static void tc37_full_lifecycle(void) +{ + separator("TC37: FULL LIFECYCLE β€” Register β†’ Check β†’ Download β†’ Update β†’ Unregister"); + + /* Step 1: Register */ + printf("\n Step 1: Register\n"); + FirmwareInterfaceHandle h = registerProcess("KWIB_Lifecycle", LIB_VERSION); + if (h == NULL) { + TEST_FAIL("TC37 β€” register", "registerProcess() returned NULL"); + return; + } + TEST_PASS("TC37 β€” register"); + printf(" Handle: '%s'\n", h); + + /* Step 2: Check */ + printf("\n Step 2: CheckForUpdate\n"); + reset_all(); + CheckForUpdateResult cr = checkForUpdate(h, check_callback); + if (cr != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC37 β€” checkForUpdate", "API returned FAIL"); + unregisterProcess(h); return; + } + if (!wait_flag(&g_check_cb_fired, 130)) { + TEST_FAIL("TC37 β€” checkForUpdate", "Callback timeout"); + unregisterProcess(h); return; + } + TEST_PASS("TC37 β€” checkForUpdate completed"); + sleep(1); + + /* Step 3: Download */ + printf("\n Step 3: DownloadFirmware\n"); + reset_all(); + FwDwnlReq dreq = make_dwnl_req(); + DownloadResult dr = downloadFirmware(h, &dreq, download_callback); + if (dr != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC37 β€” downloadFirmware", "API returned FAILED"); + unregisterProcess(h); return; + } + if (!wait_flag(&g_dwnl_cb_terminal, 600)) { + TEST_FAIL("TC37 β€” downloadFirmware", "Terminal callback timeout"); + unregisterProcess(h); return; + } + if (g_dwnl_status == (int)DWNL_COMPLETED) + TEST_PASS("TC37 β€” download completed"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "status=%d", g_dwnl_status); + TEST_FAIL("TC37 β€” download", msg); + unregisterProcess(h); return; + } + sleep(1); + + /* Step 4: Update */ + printf("\n Step 4: UpdateFirmware\n"); + reset_all(); + FwUpdateReq ureq = make_update_req(); + UpdateResult ur = updateFirmware(h, &ureq, update_callback); + if (ur != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC37 β€” updateFirmware", "API returned FAILED"); + unregisterProcess(h); return; + } + if (!wait_flag(&g_update_cb_terminal, 600)) { + TEST_FAIL("TC37 β€” updateFirmware", "Terminal callback timeout"); + unregisterProcess(h); return; + } + if (g_update_status == (int)UPDATE_COMPLETED) + TEST_PASS("TC37 β€” update completed"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "status=%d", g_update_status); + TEST_FAIL("TC37 β€” update", msg); + unregisterProcess(h); return; + } + sleep(1); + + /* Step 5: Unregister */ + printf("\n Step 5: Unregister\n"); + unregisterProcess(h); + TEST_PASS("TC37 β€” unregister"); +} + +static void tc38_lifecycle_no_sleeps(void) +{ + separator("TC38: FULL LIFECYCLE β€” No sleeps between calls"); + + FirmwareInterfaceHandle h = registerProcess("KWIB_NoSleep", LIB_VERSION); + if (!h) { TEST_FAIL("TC38 β€” register", "NULL"); return; } + TEST_PASS("TC38 β€” register"); + + /* Check */ + reset_all(); + if (checkForUpdate(h, check_callback) != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC38 β€” check", "FAIL"); unregisterProcess(h); return; + } + wait_flag(&g_check_cb_fired, 130); + TEST_PASS("TC38 β€” check done"); + + /* Immediately download (no sleep) */ + reset_all(); + FwDwnlReq dreq = make_dwnl_req(); + DownloadResult dr = downloadFirmware(h, &dreq, download_callback); + if (dr == RDKFW_DWNL_SUCCESS) { + TEST_PASS("TC38 β€” download accepted immediately after check"); + wait_flag(&g_dwnl_cb_terminal, 600); + } else { + TEST_FAIL("TC38 β€” download", "Rejected (check worker may not have exited yet)"); + } + + /* Immediately update (no sleep) */ + reset_all(); + FwUpdateReq ureq = make_update_req(); + UpdateResult ur = updateFirmware(h, &ureq, update_callback); + if (ur == RDKFW_UPDATE_SUCCESS) { + TEST_PASS("TC38 β€” update accepted immediately after download"); + wait_flag(&g_update_cb_terminal, 600); + } else { + TEST_FAIL("TC38 β€” update", "Rejected (download worker may not have exited yet)"); + } + + unregisterProcess(h); + TEST_PASS("TC38 β€” lifecycle complete"); +} + +static void tc39_check_and_download_simultaneous(void) +{ + separator("TC39: Simultaneous Check + Download (different guards)"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC39", "No handle"); return; } + + /* Start check */ + CheckForUpdateResult cr = checkForUpdate(g_handle, check_callback); + if (cr != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC39 β€” check", "FAIL"); return; + } + + /* Immediately start download (different guard β€” should succeed) */ + FwDwnlReq dreq = make_dwnl_req(); + DownloadResult dr = downloadFirmware(g_handle, &dreq, download_callback); + if (dr == RDKFW_DWNL_SUCCESS) + TEST_PASS("TC39 β€” download accepted while check is active (independent guards)"); + else + TEST_FAIL("TC39 β€” download", "Rejected while check active (guards may be coupled?)"); + + /* Wait for both to finish */ + wait_flag(&g_check_cb_fired, 130); + wait_flag(&g_dwnl_cb_terminal, 600); +} + +/* ======================================================================== + * AUTOMATED SUITE RUNNERS + * ======================================================================== */ + +static void run_error_tests(void) +{ + separator("ERROR TESTS β€” INPUT VALIDATION (library-level, fast)"); + + tc03_unregister_null(); + tc06_check_null_handle(); + tc08_check_empty_handle(); + tc13_download_null_handle(); + tc22_download_empty_handle(); + tc24_update_null_handle(); + tc33_update_empty_handle(); + + separator("ERROR TESTS β€” WITH REGISTRATION (daemon needed)"); + + if (!ensure_registered()) { + TEST_INFO("Cannot continue β€” registration failed (daemon down?)"); + return; + } + + tc07_check_null_callback(); + tc14_download_null_request(); + tc15_download_null_callback(); + tc16_download_null_firmware_name(); + tc17_download_empty_firmware_name(); + tc25_update_null_request(); + tc26_update_null_callback(); + tc27_update_null_firmware_name(); + tc28_update_empty_firmware_name(); + + separator("GUARD TESTS β€” DUPLICATE CALLS"); + + tc09_check_duplicate(); + sleep(2); + tc18_download_duplicate(); + sleep(2); + tc29_update_duplicate(); + sleep(2); + + separator("GUARD TESTS β€” UNREGISTER DURING OPERATION"); + + tc34_unreg_during_check(); + sleep(2); + tc35_unreg_during_download(); + sleep(2); + tc36_unreg_during_update(); + sleep(2); + + ensure_unregistered(); +} + +static void run_happy_tests(void) +{ + separator("HAPPY PATH TESTS (daemon required)"); + + tc01_register_happy(); + if (!g_handle) { TEST_INFO("Cannot continue β€” register failed"); return; } + + tc04_double_register(); + + tc05_check_happy(); + sleep(2); + tc11_check_callback_data(); + sleep(2); + tc10_check_rapid_retry(); + sleep(2); + + tc12_download_happy(); + tc20_download_progress_mono(); + tc21_download_terminal(); + sleep(2); + tc19_download_rapid_retry(); + sleep(2); + + tc23_update_happy(); + tc31_update_progress_mono(); + tc32_update_terminal(); + sleep(2); + tc30_update_rapid_retry(); + sleep(2); + + tc02_unregister_happy(); +} + +static void run_lifecycle_tests(void) +{ + tc37_full_lifecycle(); + sleep(2); + tc38_lifecycle_no_sleeps(); + sleep(2); + tc39_check_and_download_simultaneous(); + sleep(2); + ensure_unregistered(); +} + +static void print_results(void) +{ + printf("\n"); + printf("══════════════════════════════════════════════════════════════\n"); + printf(" KnowWhereItBreaks β€” TEST RESULTS\n"); + printf("══════════════════════════════════════════════════════════════\n"); + printf(" Total: %d\n", g_results.total); + printf(" \033[32mPassed: %d\033[0m\n", g_results.passed); + printf(" \033[31mFailed: %d\033[0m\n", g_results.failed); + printf(" \033[33mSkipped: %d\033[0m\n", g_results.skipped); + printf("══════════════════════════════════════════════════════════════\n"); + if (g_results.failed == 0) + printf(" \033[32mβœ… ALL TESTS PASSED\033[0m\n"); + else + printf(" \033[31m❌ %d TEST(S) FAILED\033[0m\n", g_results.failed); + printf("══════════════════════════════════════════════════════════════\n\n"); +} + +/* ======================================================================== + * INTERACTIVE MENU + * ======================================================================== */ + +static void print_menu(void) +{ + printf("\n"); + printf("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n"); + printf("β”‚ KnowWhereItBreaks v1.0 β€” librdkFwupdateMgr Test Utility β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ Handle: %-49sβ”‚\n", g_handle ? g_handle : "(not registered)"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ AUTOMATED SUITES β”‚\n"); + printf("β”‚ 10 All Error/Validation Tests (fast) β”‚\n"); + printf("β”‚ 11 All Happy Path Tests (daemon needed) β”‚\n"); + printf("β”‚ 12 Full Lifecycle Tests (daemon needed) β”‚\n"); + printf("β”‚ 13 ALL Tests (everything) β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ REGISTER / UNREGISTER CHECKFORUPDATE β”‚\n"); + printf("β”‚ 1 TC01 Register happy 5 TC05 Check happy β”‚\n"); + printf("β”‚ 2 TC02 Unregister happy 6 TC06 NULL handle β”‚\n"); + printf("β”‚ 3 TC03 Unregister NULL 7 TC07 NULL callback β”‚\n"); + printf("β”‚ 4 TC04 Double register 8 TC08 Empty handle β”‚\n"); + printf("β”‚ 9 TC09 Duplicate β”‚\n"); + printf("β”‚ 40 TC10 Rapid retry β”‚\n"); + printf("β”‚ 41 TC11 Callback data β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ DOWNLOAD FIRMWARE UPDATE FIRMWARE β”‚\n"); + printf("β”‚ 50 TC12 Download happy 60 TC23 Update happy β”‚\n"); + printf("β”‚ 51 TC13 NULL handle 61 TC24 NULL handle β”‚\n"); + printf("β”‚ 52 TC14 NULL request 62 TC25 NULL request β”‚\n"); + printf("β”‚ 53 TC15 NULL callback 63 TC26 NULL callback β”‚\n"); + printf("β”‚ 54 TC16 NULL fw name 64 TC27 NULL fw name β”‚\n"); + printf("β”‚ 55 TC17 Empty fw name 65 TC28 Empty fw name β”‚\n"); + printf("β”‚ 56 TC18 Duplicate 66 TC29 Duplicate β”‚\n"); + printf("β”‚ 57 TC19 Rapid retry 67 TC30 Rapid retry β”‚\n"); + printf("β”‚ 58 TC20 Progress mono 68 TC31 Progress mono β”‚\n"); + printf("β”‚ 59 TC21 Terminal status 69 TC32 Terminal status β”‚\n"); + printf("β”‚ 70 TC33 Empty handle β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ GUARDS / LIFECYCLE β”‚\n"); + printf("β”‚ 80 TC34 Unreg during check 90 TC37 Full lifecycle β”‚\n"); + printf("β”‚ 81 TC35 Unreg during download 91 TC38 No-sleep lifecy β”‚\n"); + printf("β”‚ 82 TC36 Unreg during update 92 TC39 Check+Dwnl sim β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ 0 Exit (print results) β”‚\n"); + printf("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n"); + printf(" Choice: "); +} + +/* ======================================================================== + * MAIN + * ======================================================================== */ + +int main(int argc, char *argv[]) +{ + printf("\n"); + printf("╔══════════════════════════════════════════════════════════════╗\n"); + printf("β•‘ KnowWhereItBreaks v1.0 β•‘\n"); + printf("β•‘ Comprehensive Test Utility for librdkFwupdateMgr.so β•‘\n"); + printf("β•‘ Build: %s %s β•‘\n", __DATE__, __TIME__); + printf("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n"); + + /* Automated modes (for CI/scripts) */ + if (argc > 1) { + if (strcmp(argv[1], "--auto-error") == 0) { + run_error_tests(); print_results(); + return g_results.failed > 0 ? 1 : 0; + } + if (strcmp(argv[1], "--auto-happy") == 0) { + run_happy_tests(); print_results(); + return g_results.failed > 0 ? 1 : 0; + } + if (strcmp(argv[1], "--full-lifecycle") == 0) { + run_lifecycle_tests(); print_results(); + return g_results.failed > 0 ? 1 : 0; + } + if (strcmp(argv[1], "--auto-all") == 0) { + run_error_tests(); + sleep(2); g_handle = NULL; + run_happy_tests(); + sleep(2); + run_lifecycle_tests(); + print_results(); + return g_results.failed > 0 ? 1 : 0; + } + printf("Unknown option: %s\n", argv[1]); + printf("Usage: %s [--auto-error|--auto-happy|--full-lifecycle|--auto-all]\n", argv[0]); + return 1; + } + + /* Interactive mode */ + int choice; + char line[32]; + + while (1) { + print_menu(); + if (!fgets(line, sizeof(line), stdin)) break; + choice = atoi(line); + + switch (choice) { + case 0: + ensure_unregistered(); + print_results(); + return g_results.failed > 0 ? 1 : 0; + + /* Register / Unregister */ + case 1: tc01_register_happy(); break; + case 2: tc02_unregister_happy(); break; + case 3: tc03_unregister_null(); break; + case 4: tc04_double_register(); break; + + /* CheckForUpdate */ + case 5: tc05_check_happy(); break; + case 6: tc06_check_null_handle(); break; + case 7: tc07_check_null_callback(); break; + case 8: tc08_check_empty_handle(); break; + case 9: tc09_check_duplicate(); break; + case 40: tc10_check_rapid_retry(); break; + case 41: tc11_check_callback_data(); break; + + /* Download */ + case 50: tc12_download_happy(); break; + case 51: tc13_download_null_handle(); break; + case 52: tc14_download_null_request(); break; + case 53: tc15_download_null_callback(); break; + case 54: tc16_download_null_firmware_name(); break; + case 55: tc17_download_empty_firmware_name(); break; + case 56: tc18_download_duplicate(); break; + case 57: tc19_download_rapid_retry(); break; + case 58: tc20_download_progress_mono(); break; + case 59: tc21_download_terminal(); break; + + /* Update */ + case 60: tc23_update_happy(); break; + case 61: tc24_update_null_handle(); break; + case 62: tc25_update_null_request(); break; + case 63: tc26_update_null_callback(); break; + case 64: tc27_update_null_firmware_name(); break; + case 65: tc28_update_empty_firmware_name(); break; + case 66: tc29_update_duplicate(); break; + case 67: tc30_update_rapid_retry(); break; + case 68: tc31_update_progress_mono(); break; + case 69: tc32_update_terminal(); break; + case 70: tc33_update_empty_handle(); break; + + /* Guards / Lifecycle */ + case 80: tc34_unreg_during_check(); break; + case 81: tc35_unreg_during_download(); break; + case 82: tc36_unreg_during_update(); break; + case 90: tc37_full_lifecycle(); break; + case 91: tc38_lifecycle_no_sleeps(); break; + case 92: tc39_check_and_download_simultaneous(); break; + + /* Automated suites */ + case 10: run_error_tests(); print_results(); break; + case 11: run_happy_tests(); print_results(); break; + case 12: run_lifecycle_tests(); print_results(); break; + case 13: + run_error_tests(); sleep(2); g_handle = NULL; + run_happy_tests(); sleep(2); + run_lifecycle_tests(); + print_results(); + break; + + default: printf(" Invalid choice.\n"); break; + } + } + + return 0; +} diff --git a/kwib_src/KnowWhereItBreaks_README.md b/kwib_src/KnowWhereItBreaks_README.md new file mode 100755 index 00000000..ea40cec0 --- /dev/null +++ b/kwib_src/KnowWhereItBreaks_README.md @@ -0,0 +1,272 @@ +# KnowWhereItBreaks β€” Developer Test Utility for librdkFwupdateMgr.so + +## Overview + +`KnowWhereItBreaks` is a **comprehensive developer test utility** that exercises every code path in `librdkFwupdateMgr.so` and the `rdkFwupdateMgr` daemon. + +It is **NOT** the `example_plugin` (which stays clean as a reference for external teams). +It is your personal weapon for finding bugs before they find you. + +**39 test cases** covering input validation, library guards, daemon rejection, happy paths, callback correctness, rapid retry, cross-API interaction, and full lifecycle. + +--- + +## Build & Install + +The binary is built alongside `example_plugin` via the top-level `Makefile.am`: + +```bash +# Standard build (Autotools) +./configure && make + +# Binary produced: KnowWhereItBreaks +# Installed to rootfs alongside example_plugin, rdkFwupdateMgr, etc. +``` + +No separate build step needed β€” it compiles and installs as part of the standard build. + +--- + +## Usage + +```bash +# Prerequisites: daemon must be running for happy path tests +systemctl start rdkFwupdateMgr + +# Interactive menu β€” explore and run tests one by one +./KnowWhereItBreaks + +# Automated: error/validation tests (fast, most don't need daemon) +./KnowWhereItBreaks --auto-error + +# Automated: happy path tests (daemon REQUIRED) +./KnowWhereItBreaks --auto-happy + +# Automated: full lifecycle tests (daemon REQUIRED) +./KnowWhereItBreaks --full-lifecycle + +# Automated: run EVERYTHING +./KnowWhereItBreaks --auto-all + +# Exit code: 0 = all passed, 1 = at least one failure +echo $? +``` + +--- + +## Test Case Catalog + +### Category 1: Register / Unregister (TC01–TC04) + +| TC ID | Name | Daemon? | Description | +|-------|------|:-------:|-------------| +| **TC01** | Register Happy Path | βœ… | Call `registerProcess("KnowWhereItBreaks", "1.0.0")`. Verify it returns a non-NULL, non-empty handle string. This proves the daemon is running, D-Bus communication works, and the daemon successfully allocated a handler ID for this client. | +| **TC02** | Unregister Happy Path | βœ… | After a successful registration, call `unregisterProcess(handle)`. Verify it completes without crash. This proves the daemon accepts the unregistration and deallocates the handler. | +| **TC03** | Unregister NULL Handle | ❌ | Call `unregisterProcess(NULL)`. Per the API contract ("safe to call with NULL"), this must not crash or cause undefined behavior. Validates the library's NULL guard in the unregister path. | +| **TC04** | Double Register | βœ… | Call `registerProcess()` twice with different process names. Both should succeed with different handles. Proves the daemon supports multiple simultaneous clients. Both handles are unregistered after the test. | + +### Category 2: CheckForUpdate (TC05–TC11) + +| TC ID | Name | Daemon? | Description | +|-------|------|:-------:|-------------| +| **TC05** | CheckForUpdate Happy Path | βœ… | Call `checkForUpdate(handle, callback)` with a valid handle. Verify it returns `CHECK_FOR_UPDATE_SUCCESS`. Wait up to 130s for the callback. Verify the callback fires. This exercises the full path: library creates worker thread β†’ D-Bus method call β†’ daemon queries XConf β†’ daemon emits CheckForUpdateComplete signal β†’ worker thread receives signal β†’ worker parses GVariant `(tiissss)` β†’ callback fires with `FwInfoData`. | +| **TC06** | NULL Handle | ❌ | Call `checkForUpdate(NULL, callback)`. Must return `CHECK_FOR_UPDATE_FAIL` immediately. Validates the library's input validation at the very first line of `checkForUpdate()` in `rdkFwupdateMgr_api.c`. No D-Bus call, no thread created. | +| **TC07** | NULL Callback | ❌ | Call `checkForUpdate(handle, NULL)`. Must return `CHECK_FOR_UPDATE_FAIL`. A NULL callback would mean no way to deliver results β€” the library correctly rejects this before spawning a worker thread. | +| **TC08** | Empty Handle | ❌ | Call `checkForUpdate("", callback)`. Must return `CHECK_FOR_UPDATE_FAIL`. An empty string handle is not a valid session ID β€” the daemon would reject it anyway, but the library catches it early. | +| **TC09** | Duplicate (Same Process) | βœ… | Call `checkForUpdate()` twice in rapid succession from the same process. The first call should return SUCCESS. The second call (made while the first worker thread is still active) should return FAIL. This validates the `internal_begin_check()` guard: the library sets `g_check_in_progress = true` atomically, and the second call sees it and rejects. After the first completes, we wait for the callback so state is clean for subsequent tests. | +| **TC10** | Rapid Retry | βœ… | Call `checkForUpdate()`, wait for callback, then immediately call it again. The second call should succeed (the guard was cleared when the first worker thread called `internal_end_check()` in cleanup). Validates that the in-progress flag is properly reset after operation completes β€” no permanent lockout. | +| **TC11** | Callback Data Validation | βœ… | Call `checkForUpdate()` and examine the callback's data: (a) callback fired exactly once (not zero, not twice), (b) `status` field is a valid `CheckForUpdateStatus` enum value (0–5). This validates signal parsing, GVariant deserialization, and the `internal_map_status_code()` mapping. | + +### Category 3: DownloadFirmware (TC12–TC22) + +| TC ID | Name | Daemon? | Description | +|-------|------|:-------:|-------------| +| **TC12** | Download Happy Path | βœ… | Call `downloadFirmware(handle, &req, callback)` with valid inputs. Verify it returns `RDKFW_DWNL_SUCCESS` (meaning the daemon accepted the download request via synchronous D-Bus reply). Wait for terminal callback (`DWNL_COMPLETED` or `DWNL_ERROR`). This exercises: library creates worker thread β†’ D-Bus `DownloadFirmware` method call (synchronous, reads `(sss)` reply) β†’ condvar handshake β†’ worker enters event loop β†’ daemon emits DownloadProgress signals `(tsuss)` β†’ callback fires multiple times β†’ loop quits on terminal status β†’ cleanup. | +| **TC13** | NULL Handle | ❌ | `downloadFirmware(NULL, &req, cb)` β†’ `RDKFW_DWNL_FAILED`. Library validation. | +| **TC14** | NULL Request | ❌ | `downloadFirmware(handle, NULL, cb)` β†’ `RDKFW_DWNL_FAILED`. Library validation. | +| **TC15** | NULL Callback | ❌ | `downloadFirmware(handle, &req, NULL)` β†’ `RDKFW_DWNL_FAILED`. Library validation. | +| **TC16** | NULL Firmware Name | ❌ | `req.firmwareName = NULL` β†’ `RDKFW_DWNL_FAILED`. Library checks `firmwareName != NULL`. | +| **TC17** | Empty Firmware Name | ❌ | `req.firmwareName = ""` β†’ `RDKFW_DWNL_FAILED`. Library checks `firmwareName[0] != '\0'`. | +| **TC18** | Duplicate (Same Process) | βœ… | Two `downloadFirmware()` calls in rapid succession. First returns SUCCESS. Second returns FAILED (`internal_begin_download()` sees `g_dwnl_in_progress == true`). Validates the per-API in-progress guard is working independently from CheckForUpdate's guard. | +| **TC19** | Rapid Retry | βœ… | Download β†’ wait for completion β†’ immediately download again. Second call should succeed (guard cleared by `internal_end_download()` in first worker's cleanup). | +| **TC20** | Progress Monotonicity | βœ… | After TC12 runs, examine the recorded progress values. Verify: (a) multiple callbacks fired (not just one), (b) progress values never decreased. A non-monotonic progress indicates a daemon bug (library just relays what daemon sends). | +| **TC21** | Terminal Status | βœ… | After TC12 runs, verify the final callback had a terminal status (`DWNL_COMPLETED` or `DWNL_ERROR`), not `DWNL_IN_PROGRESS`. This validates that `on_download_signal_handler()` correctly identifies terminal states and quits the event loop. | +| **TC22** | Empty Handle | ❌ | `downloadFirmware("", &req, cb)` β†’ `RDKFW_DWNL_FAILED`. | + +### Category 4: UpdateFirmware (TC23–TC33) + +| TC ID | Name | Daemon? | Description | +|-------|------|:-------:|-------------| +| **TC23** | Update Happy Path | βœ… | Call `updateFirmware(handle, &req, callback)` with valid inputs. Verify `RDKFW_UPDATE_SUCCESS` return (daemon accepted). Wait for terminal callback (`UPDATE_COMPLETED`). Exercises: worker thread β†’ synchronous D-Bus `UpdateFirmware` call β†’ `(sss)` reply parsing β†’ condvar handshake β†’ event loop β†’ `UpdateProgress` signals `(tsiis)` β†’ callback N times β†’ terminal quit β†’ cleanup. | +| **TC24** | NULL Handle | ❌ | β†’ `RDKFW_UPDATE_FAILED`. | +| **TC25** | NULL Request | ❌ | β†’ `RDKFW_UPDATE_FAILED`. | +| **TC26** | NULL Callback | ❌ | β†’ `RDKFW_UPDATE_FAILED`. | +| **TC27** | NULL Firmware Name | ❌ | `req.firmwareName = NULL` β†’ `RDKFW_UPDATE_FAILED`. | +| **TC28** | Empty Firmware Name | ❌ | `req.firmwareName = ""` β†’ `RDKFW_UPDATE_FAILED`. | +| **TC29** | Duplicate (Same Process) | βœ… | Two `updateFirmware()` calls. Second rejected by `internal_begin_update()`. | +| **TC30** | Rapid Retry | βœ… | Update β†’ wait β†’ update again. Second succeeds (guard cleared). | +| **TC31** | Progress Monotonicity | βœ… | Multiple callbacks, progress never decreases. | +| **TC32** | Terminal Status | βœ… | Final callback = `UPDATE_COMPLETED` or `UPDATE_ERROR`. | +| **TC33** | Empty Handle | ❌ | β†’ `RDKFW_UPDATE_FAILED`. | + +### Category 5: Unregister During Active Operations (TC34–TC36) + +| TC ID | Name | Daemon? | Description | +|-------|------|:-------:|-------------| +| **TC34** | Unregister During Check | βœ… | Start `checkForUpdate()`, then immediately call `unregisterProcess()`. The library's `unregisterProcess()` checks `internal_is_check_in_progress()` β€” if active, the unregister should be blocked (no-op). Verify by checking that the check callback still fires (handle was not invalidated). This proves the session guard in `rdkFwupdateMgr_process.c` prevents mid-operation handle destruction. | +| **TC35** | Unregister During Download | βœ… | Same pattern as TC34 but with `downloadFirmware()`. Start download β†’ immediately unregister β†’ verify download terminal callback still fires. | +| **TC36** | Unregister During Update | βœ… | Same pattern but with `updateFirmware()`. Start update β†’ immediately unregister β†’ verify update terminal callback still fires. | + +### Category 6: Full Lifecycle & Cross-API (TC37–TC39) + +| TC ID | Name | Daemon? | Description | +|-------|------|:-------:|-------------| +| **TC37** | Full Lifecycle | βœ… | **The integration test.** Register β†’ CheckForUpdate (wait for callback) β†’ DownloadFirmware (wait for completion) β†’ UpdateFirmware (wait for completion) β†’ Unregister. All five steps must succeed sequentially. This validates the entire library + daemon interaction end-to-end, including: handle lifetime, worker thread creation/destruction for all three APIs, D-Bus signal subscription/unsubscription, condvar handshake accuracy, callback delivery, and cleanup. | +| **TC38** | Lifecycle No Sleeps | βœ… | Same as TC37 but with NO `sleep()` between API calls. After checkForUpdate callback fires, immediately call downloadFirmware (no 1s pause). After download completes, immediately call updateFirmware. This stress-tests worker thread cleanup timing: the previous worker must have called `internal_end_*()` and fully exited before the next call's `internal_begin_*()` succeeds. If the library has a cleanup race, this test will catch it. | +| **TC39** | Check + Download Simultaneous | βœ… | Start `checkForUpdate()`, then immediately start `downloadFirmware()` (while check worker is still active). Both should succeed because they use **independent** in-progress guards (`g_check_in_progress` vs `g_dwnl_in_progress`). If the library incorrectly uses a single global guard, the second call would be rejected. | + +--- + +## Test Summary + +| Category | TCs | Tests | What's validated | +|----------|:---:|:-----:|-----------------| +| Register/Unregister | TC01–TC04 | 4 | Handle creation, cleanup, NULL safety, multi-client | +| CheckForUpdate | TC05–TC11 | 7 | Happy path, input validation, guard, retry, callback data | +| DownloadFirmware | TC12–TC22 | 11 | Happy path, input validation, guard, retry, progress, terminal | +| UpdateFirmware | TC23–TC33 | 11 | Happy path, input validation, guard, retry, progress, terminal | +| Unregister Guards | TC34–TC36 | 3 | Session protection during active operations | +| Full Lifecycle | TC37–TC39 | 3 | End-to-end integration, timing, cross-API independence | +| **Total** | | **39** | | + +--- + +## Automated Modes + +### `--auto-error` (Fast β€” most tests don't need daemon) + +Runs: TC03, TC06, TC08, TC13, TC22, TC24, TC33, TC07, TC14–TC17, TC25–TC28, TC09, TC18, TC29, TC34–TC36 + +Tests library-level input validation and in-progress guards. The NULL/empty/missing-field tests don't create worker threads or D-Bus connections. The duplicate and unregister-during-operation tests need the daemon. + +### `--auto-happy` (Daemon required) + +Runs: TC01, TC04, TC05, TC11, TC10, TC12, TC20, TC21, TC19, TC23, TC31, TC32, TC30, TC02 + +Full happy path with callback validation and retry tests. Takes longer (waits for daemon responses). + +### `--full-lifecycle` (Daemon required) + +Runs: TC37, TC38, TC39 + +End-to-end integration tests. + +### `--auto-all` + +Runs all three suites sequentially. Use for pre-commit validation. + +--- + +## Output Format + +``` +══════════════════════════════════════════════════════════════ + ERROR TESTS β€” INPUT VALIDATION (library-level, fast) +══════════════════════════════════════════════════════════════ + +--- TC03: Unregister NULL Handle --- + [PASS] TC03 β€” unregisterProcess(NULL) did not crash + +--- TC06: CheckForUpdate NULL Handle --- + [PASS] TC06 β€” NULL handle rejected + +--- TC09: CheckForUpdate Duplicate (Same Process) --- + [PASS] TC09 β€” duplicate call rejected by library guard + [INFO] Waiting for first check to complete... + [CB:Check] #1 status=0 current='v1.0.0' + +══════════════════════════════════════════════════════════════ + KnowWhereItBreaks β€” TEST RESULTS +══════════════════════════════════════════════════════════════ + Total: 39 + Passed: 37 + Failed: 1 + Skipped: 1 +══════════════════════════════════════════════════════════════ + ❌ 1 TEST(S) FAILED +══════════════════════════════════════════════════════════════ +``` + +Exit code: `0` = all passed, `1` = at least one failure. + +--- + +## Manual-Only Scenarios (Not in Automated Tests) + +These require external actions that cannot be automated in KnowWhereItBreaks: + +| Scenario | How to Test | +|----------|-------------| +| **Daemon down** | Stop daemon β†’ `./KnowWhereItBreaks --auto-happy` β†’ all happy paths should return FAIL. No crashes, no hangs. | +| **Daemon crash mid-download** | Start download (menu option 50) β†’ in another terminal: `kill -9 $(pidof rdkFwupdateMgr)` β†’ verify callback eventually fires with `DWNL_ERROR` (timeout path). | +| **Daemon crash mid-update** | Same but with update (menu option 60). | +| **Cross-process rejection** | Run two instances of KnowWhereItBreaks. Both register. Both try to download simultaneously. One should succeed, the other should get `RDKFW_DWNL_FAILED` (daemon rejects). | +| **Memory leak check** | `valgrind --leak-check=full ./KnowWhereItBreaks --auto-all` β€” expect 0 bytes lost (GLib "still reachable" is normal). | +| **Thread sanitizer** | Rebuild with `-fsanitize=thread`, run `--auto-all`, expect no data race warnings. | +| **Network failure during download** | Disconnect network after download starts β†’ verify `DWNL_ERROR` callback fires. | + +--- + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ KnowWhereItBreaks β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ 39 Test β”‚ β”‚ Result Track β”‚ β”‚ Callback Trackingβ”‚ β”‚ +β”‚ β”‚ Cases β”‚ β”‚ PASS/FAIL/ β”‚ β”‚ volatile bools β”‚ β”‚ +β”‚ β”‚ TC01..39 β”‚ β”‚ SKIP counts β”‚ β”‚ fired? status? β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ progress? count? β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ Public API calls β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ librdkFwupdateMgr.so β”‚ ← Library under test β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ D-Bus β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ rdkFwupdateMgr β”‚ ← Daemon β”‚ +β”‚ β”‚ (daemon process) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| All happy path tests return FAIL | Daemon not running | `systemctl start rdkFwupdateMgr` | +| Test hangs on "Waiting for callback" | Daemon not sending expected signal | Check `/opt/logs/rdkFwupdateMgr.log` | +| TC09 duplicate test passes when it shouldn't | First check completed before second call (fast daemon) | Normal β€” guard worked, operation was just fast | +| TC34–TC36 unregister guard tests fail | `unregisterProcess()` missing in-progress checks | Fix `rdkFwupdateMgr_process.c` | +| TC20/TC31 progress not monotonic | Daemon sending non-monotonic values | Daemon bug β€” library relays accurately | +| TC38 no-sleep lifecycle fails | Worker thread cleanup race | Check `internal_end_*()` call timing in worker cleanup | + +--- + +## Comparison with example_plugin + +| Aspect | example_plugin | KnowWhereItBreaks | +|--------|---------------|-------------------| +| **Audience** | External teams (reference app) | Internal developer (testing) | +| **Lines** | ~700 | ~1100 | +| **Test cases** | 0 (it's an example) | 39 | +| **Error injection** | None | NULL/empty/invalid for every API | +| **Automation** | None (single run) | 4 automated modes + interactive menu | +| **Result tracking** | None | PASS/FAIL/SKIP with final summary | +| **Guard testing** | None | Duplicate calls, unregister during ops | +| **Retry testing** | None | Rapid retry after completion | +| **Exit code** | 0 or 1 | 0 = all pass, 1 = failures | diff --git a/kwib_src/USAGE_KWIB.md b/kwib_src/USAGE_KWIB.md new file mode 100755 index 00000000..eaf18966 --- /dev/null +++ b/kwib_src/USAGE_KWIB.md @@ -0,0 +1,582 @@ +# How to Use `kwib_test_utility` (KnowWhereItBreaks) + +`kwib_test_utility` is the compiled binary of `KnowWhereItBreaks.c`. It is a +**comprehensive developer test utility** that exercises every code path in +`librdkFwupdateMgr.so` and the `rdkFwupdateMgr` daemon β€” 39 test cases +covering input validation, library guards, callback correctness, rapid retry, +cross-API independence, and full end-to-end lifecycle. + +> **Not the example.** `example_plugin` is the clean reference for external +> teams. `kwib_test_utility` is your developer weapon for finding bugs before +> they find you. + +--- + +## Prerequisites + +Before running `kwib_test_utility`, three things must be true on the target +device (same as `example_plugin`). + +### 1. The daemon is running + +```bash +systemctl status rdkFwupdateMgr +``` + +If it is not running: + +```bash +systemctl start rdkFwupdateMgr +``` + +If it has never been enabled: + +```bash +systemctl enable --now rdkFwupdateMgr +``` + +> **Note:** Error/validation tests (TC03, TC06–TC08, TC13–TC17, TC22, +> TC24–TC28, TC33) do NOT need the daemon. They test the library's input +> guards locally. All other tests require the daemon to be running. + +### 2. The library is installed and visible + +`librdkFwupdateMgr.so` must be findable at runtime: + +```bash +# Confirm it is installed +ls -l /usr/lib/librdkFwupdateMgr.so* + +# If library is in a non-standard path +export LD_LIBRARY_PATH=/path/to/librdkFwupdateMgr:$LD_LIBRARY_PATH +``` + +### 3. D-Bus system bus is running + +```bash +systemctl status dbus +``` + +--- + +## Build & Install + +The binary is compiled and installed **exactly like `example_plugin`** β€” same +`bin_PROGRAMS` list, same CFLAGS pattern, same rootfs destination. + +In `Makefile.am`: + +```makefile +bin_PROGRAMS += kwib_test_utility + +kwib_test_utility_SOURCES = \ + ${top_srcdir}/KnowWhereItBreaks/KnowWhereItBreaks.c + +kwib_test_utility_CFLAGS = \ + -I${top_srcdir}/librdkFwupdateMgr/include \ + $(AM_CFLAGS) \ + $(GLIB_CFLAGS) + +kwib_test_utility_LDADD = \ + librdkFwupdateMgr.la \ + $(GLIB_LIBS) \ + -lpthread + +kwib_test_utility_LDFLAGS = \ + -L$(PKG_CONFIG_SYSROOT_DIR)/$(libdir) +``` + +Standard build: + +```bash +./configure && make +``` + +The binary is produced as `kwib_test_utility` and installed to `/usr/bin/` +alongside `example_plugin`, `rdkFwupdateMgr`, `rdkvfwupgrader`, etc. + +> **Why `kwib_test_utility` instead of `KnowWhereItBreaks`?** +> The source lives in the `KnowWhereItBreaks/` directory. Automake produces +> binaries in the top-level build directory, and a file cannot have the same +> name as an existing directory. The binary is therefore named +> `kwib_test_utility`. + +--- + +## Quick Start + +```bash +# 1. Make sure daemon is running +systemctl start rdkFwupdateMgr + +# 2. Run in interactive mode β€” pick tests from the menu +kwib_test_utility + +# 3. Or run all error/validation tests (fast, mostly no daemon) +kwib_test_utility --auto-error + +# 4. Or run all happy-path tests (daemon required) +kwib_test_utility --auto-happy + +# 5. Or run full lifecycle tests (daemon required) +kwib_test_utility --full-lifecycle + +# 6. Or run EVERYTHING +kwib_test_utility --auto-all + +# 7. Check exit code (CI-friendly) +echo $? # 0 = all passed, 1 = at least one failure +``` + +--- + +## Running Modes + +### Interactive Mode (no arguments) + +```bash +kwib_test_utility +``` + +Displays a menu showing all 39 test cases grouped by category. Type a number +and press Enter to run that test. The menu re-displays after each test. Type +`0` to exit and see the final results summary. + +The menu shows: + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ KnowWhereItBreaks v1.0 β€” librdkFwupdateMgr Test Utility β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Handle: (not registered) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ AUTOMATED SUITES β”‚ +β”‚ 10 All Error/Validation Tests (fast) β”‚ +β”‚ 11 All Happy Path Tests (daemon needed) β”‚ +β”‚ 12 Full Lifecycle Tests (daemon needed) β”‚ +β”‚ 13 ALL Tests (everything) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ REGISTER / UNREGISTER CHECKFORUPDATE β”‚ +β”‚ 1 TC01 Register happy 5 TC05 Check happy β”‚ +β”‚ 2 TC02 Unregister happy 6 TC06 NULL handle β”‚ +β”‚ 3 TC03 Unregister NULL 7 TC07 NULL callback β”‚ +β”‚ 4 TC04 Double register 8 TC08 Empty handle β”‚ +β”‚ 9 TC09 Duplicate β”‚ +β”‚ 40 TC10 Rapid retry β”‚ +β”‚ 41 TC11 Callback data β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ DOWNLOAD FIRMWARE UPDATE FIRMWARE β”‚ +β”‚ 50 TC12 Download happy 60 TC23 Update happy β”‚ +β”‚ 51 TC13 NULL handle 61 TC24 NULL handle β”‚ +β”‚ 52 TC14 NULL request 62 TC25 NULL request β”‚ +β”‚ 53 TC15 NULL callback 63 TC26 NULL callback β”‚ +β”‚ 54 TC16 NULL fw name 64 TC27 NULL fw name β”‚ +β”‚ 55 TC17 Empty fw name 65 TC28 Empty fw name β”‚ +β”‚ 56 TC18 Duplicate 66 TC29 Duplicate β”‚ +β”‚ 57 TC19 Rapid retry 67 TC30 Rapid retry β”‚ +β”‚ 58 TC20 Progress mono 68 TC31 Progress mono β”‚ +β”‚ 59 TC21 Terminal status 69 TC32 Terminal status β”‚ +β”‚ 70 TC33 Empty handle β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ GUARDS / LIFECYCLE β”‚ +β”‚ 80 TC34 Unreg during check 90 TC37 Full lifecycle β”‚ +β”‚ 81 TC35 Unreg during download 91 TC38 No-sleep lifecy β”‚ +β”‚ 82 TC36 Unreg during update 92 TC39 Check+Dwnl sim β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ 0 Exit (print results) β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + Choice: +``` + +**Typical interactive workflow:** + +1. Type `1` β†’ Register (get a handle) +2. Type `5` β†’ CheckForUpdate happy path +3. Type `6` β†’ CheckForUpdate NULL handle (error path) +4. Type `9` β†’ Duplicate check (guard test) +5. Type `0` β†’ Exit and see results + +### `--auto-error` (Fast β€” most don't need daemon) + +```bash +kwib_test_utility --auto-error +``` + +**Runs:** TC03, TC06, TC08, TC13, TC22, TC24, TC33, TC07, TC14–TC17, +TC25–TC28, TC09, TC18, TC29, TC34–TC36 + +Tests library-level input validation and in-progress guards. The NULL/empty +tests don't create worker threads or D-Bus connections. The duplicate and +unregister-during-operation tests need the daemon. + +**When to use:** After any change to input validation logic in +`rdkFwupdateMgr_api.c` or guard logic in `rdkFwupdateMgr_async.c`. + +### `--auto-happy` (Daemon required) + +```bash +kwib_test_utility --auto-happy +``` + +**Runs:** TC01, TC04, TC05, TC11, TC10, TC12, TC20, TC21, TC19, TC23, +TC31, TC32, TC30, TC02 + +Full happy path with callback validation and retry tests. Takes longer +because it waits for real daemon responses. + +**When to use:** After any change to the async worker thread logic, D-Bus +method calls, signal handlers, or callback delivery. + +### `--full-lifecycle` (Daemon required) + +```bash +kwib_test_utility --full-lifecycle +``` + +**Runs:** TC37, TC38, TC39 + +End-to-end integration tests: complete registerβ†’checkβ†’downloadβ†’updateβ†’unregister +sequences plus cross-API simultaneous operation. + +**When to use:** Before any release or after any structural refactoring. + +### `--auto-all` (Everything) + +```bash +kwib_test_utility --auto-all +``` + +Runs all three suites sequentially: error β†’ happy β†’ lifecycle. + +**When to use:** Pre-commit validation or CI pipeline. + +--- + +## Output Format + +Every test prints a colored result line: + +``` + [PASS] TC06 β€” NULL handle rejected ← Green: test passed + [FAIL] TC05 β€” checkForUpdate() β€” Callback never fired (130s timeout) ← Red: test failed + [SKIP] TC12 β€” No handle ← Yellow: skipped (prerequisite missing) + [INFO] Waiting for callback (max 130s)... ← Informational +``` + +Callback activity is printed in real-time: + +``` + [CB:Check] #1 status=0 current='v1.0.0' + [CB:Dwnl] #3 progress=60% status=0 + [CB:Update] #5 progress=100% status=1 +``` + +Final summary at exit: + +``` +══════════════════════════════════════════════════════════════ + KnowWhereItBreaks β€” TEST RESULTS +══════════════════════════════════════════════════════════════ + Total: 39 + Passed: 37 + Failed: 1 + Skipped: 1 +══════════════════════════════════════════════════════════════ + ❌ 1 TEST(S) FAILED +══════════════════════════════════════════════════════════════ +``` + +**Exit code:** `0` = all passed, `1` = at least one failure. Use in CI: + +```bash +kwib_test_utility --auto-all || echo "TESTS FAILED" +``` + +--- + +## Test Case Catalog β€” Complete Reference + +### Category 1: Register / Unregister (TC01–TC04) + +| TC | Menu | Name | Daemon? | What It Tests | +|----|:----:|------|:-------:|---------------| +| **TC01** | 1 | Register Happy Path | βœ… | Calls `registerProcess("KnowWhereItBreaks", "1.0.0")`. Verifies it returns a non-NULL, non-empty handle string. **Proves:** daemon is running, D-Bus round-trip works, daemon allocated a handler ID. | +| **TC02** | 2 | Unregister Happy Path | βœ… | After successful registration, calls `unregisterProcess(handle)`. Verifies no crash. **Proves:** daemon accepts the unregistration and deallocates the handler cleanly. | +| **TC03** | 3 | Unregister NULL Handle | ❌ | Calls `unregisterProcess(NULL)`. Must not crash. **Proves:** library NULL guard in the unregister path (`rdkFwupdateMgr_process.c`). | +| **TC04** | 4 | Double Register | βœ… | Calls `registerProcess()` twice with different process names. Both must succeed with different handles. **Proves:** daemon supports multiple simultaneous clients. Both handles are cleaned up after test. | + +### Category 2: CheckForUpdate (TC05–TC11) + +| TC | Menu | Name | Daemon? | What It Tests | +|----|:----:|------|:-------:|---------------| +| **TC05** | 5 | CheckForUpdate Happy Path | βœ… | Calls `checkForUpdate(handle, callback)` with valid inputs. Verifies `CHECK_FOR_UPDATE_SUCCESS` return. Waits up to 130s for callback. **Proves:** full path works β€” library creates worker thread β†’ D-Bus method call β†’ daemon queries XConf β†’ daemon emits `CheckForUpdateComplete` signal β†’ worker receives signal β†’ worker parses GVariant `(tiissss)` β†’ callback fires with `FwInfoData`. | +| **TC06** | 6 | NULL Handle | ❌ | `checkForUpdate(NULL, callback)` β†’ must return `CHECK_FOR_UPDATE_FAIL` immediately. **Proves:** library input validation in `checkForUpdate()` at the very first line β€” no D-Bus call made, no thread created. | +| **TC07** | 7 | NULL Callback | ❌ | `checkForUpdate(handle, NULL)` β†’ must return `CHECK_FOR_UPDATE_FAIL`. **Proves:** library rejects NULL callback before spawning worker thread (no way to deliver results). | +| **TC08** | 8 | Empty Handle | ❌ | `checkForUpdate("", callback)` β†’ must return `CHECK_FOR_UPDATE_FAIL`. **Proves:** empty string is caught early by library (daemon would reject it too, but library is faster). | +| **TC09** | 9 | Duplicate (Same Process) | βœ… | Calls `checkForUpdate()` twice in rapid succession. First returns SUCCESS. Second (while first worker is active) returns FAIL. **Proves:** `internal_begin_check()` guard works β€” sets `g_check_in_progress = true` atomically, second call sees it and rejects. Waits for first callback to clean state. | +| **TC10** | 40 | Rapid Retry | βœ… | Calls `checkForUpdate()`, waits for callback, then immediately calls again. Second call must succeed. **Proves:** in-progress flag is properly reset by `internal_end_check()` in worker cleanup β€” no permanent lockout after operation completes. | +| **TC11** | 41 | Callback Data Validation | βœ… | Calls `checkForUpdate()` and examines callback data: (a) callback fired exactly once, (b) `status` field is valid `CheckForUpdateStatus` enum (0–5). **Proves:** signal parsing, GVariant deserialization, and `internal_map_status_code()` mapping are correct. | + +### Category 3: DownloadFirmware (TC12–TC22) + +| TC | Menu | Name | Daemon? | What It Tests | +|----|:----:|------|:-------:|---------------| +| **TC12** | 50 | Download Happy Path | βœ… | Calls `downloadFirmware(handle, &req, callback)` with valid inputs. Verifies `RDKFW_DWNL_SUCCESS` (daemon accepted via sync D-Bus reply). Waits for terminal callback. **Proves:** library creates worker β†’ D-Bus `DownloadFirmware` method call β†’ `(sss)` reply β†’ condvar handshake β†’ event loop β†’ daemon emits `DownloadProgress` signals `(tsuss)` β†’ callback fires N times β†’ loop quits on terminal status β†’ cleanup. | +| **TC13** | 51 | NULL Handle | ❌ | `downloadFirmware(NULL, &req, cb)` β†’ `RDKFW_DWNL_FAILED`. **Proves:** library input validation. | +| **TC14** | 52 | NULL Request | ❌ | `downloadFirmware(handle, NULL, cb)` β†’ `RDKFW_DWNL_FAILED`. **Proves:** library NULL-checks the request struct pointer. | +| **TC15** | 53 | NULL Callback | ❌ | `downloadFirmware(handle, &req, NULL)` β†’ `RDKFW_DWNL_FAILED`. **Proves:** library rejects NULL callback. | +| **TC16** | 54 | NULL Firmware Name | ❌ | `req.firmwareName = NULL` β†’ `RDKFW_DWNL_FAILED`. **Proves:** library validates individual struct fields, not just the struct pointer. | +| **TC17** | 55 | Empty Firmware Name | ❌ | `req.firmwareName = ""` β†’ `RDKFW_DWNL_FAILED`. **Proves:** library checks `firmwareName[0] != '\0'`, not just `!= NULL`. | +| **TC18** | 56 | Duplicate (Same Process) | βœ… | Two `downloadFirmware()` calls in rapid succession. Second returns FAILED. **Proves:** `internal_begin_download()` guard β€” `g_dwnl_in_progress` flag works independently from CheckForUpdate's guard. | +| **TC19** | 57 | Rapid Retry | βœ… | Download β†’ wait for completion β†’ immediately download again. Second succeeds. **Proves:** `internal_end_download()` properly clears guard in worker cleanup β€” no permanent lockout. | +| **TC20** | 58 | Progress Monotonicity | βœ… | After TC12, examines recorded progress values. Checks: (a) multiple callbacks fired, (b) progress never decreased. **Proves:** daemon sends monotonically increasing progress. A regression here means daemon bug (library just relays). | +| **TC21** | 59 | Terminal Status | βœ… | After TC12, verifies final callback had terminal status (`DWNL_COMPLETED` or `DWNL_ERROR`), not `DWNL_IN_PROGRESS`. **Proves:** `on_download_signal_handler()` correctly identifies terminal states and quits the GLib event loop. | +| **TC22** | β€” | Empty Handle | ❌ | `downloadFirmware("", &req, cb)` β†’ `RDKFW_DWNL_FAILED`. **Proves:** empty string validation (same as TC08 pattern for download). | + +### Category 4: UpdateFirmware (TC23–TC33) + +| TC | Menu | Name | Daemon? | What It Tests | +|----|:----:|------|:-------:|---------------| +| **TC23** | 60 | Update Happy Path | βœ… | Calls `updateFirmware(handle, &req, callback)` with valid inputs. Verifies `RDKFW_UPDATE_SUCCESS`. Waits for terminal callback. **Proves:** worker thread β†’ sync D-Bus `UpdateFirmware` call β†’ `(sss)` reply β†’ condvar handshake β†’ event loop β†’ `UpdateProgress` signals `(tsiis)` β†’ callback N times β†’ terminal quit β†’ cleanup. | +| **TC24** | 61 | NULL Handle | ❌ | β†’ `RDKFW_UPDATE_FAILED`. **Proves:** input validation. | +| **TC25** | 62 | NULL Request | ❌ | β†’ `RDKFW_UPDATE_FAILED`. **Proves:** NULL request struct check. | +| **TC26** | 63 | NULL Callback | ❌ | β†’ `RDKFW_UPDATE_FAILED`. **Proves:** NULL callback check. | +| **TC27** | 64 | NULL Firmware Name | ❌ | `req.firmwareName = NULL` β†’ `RDKFW_UPDATE_FAILED`. **Proves:** field-level validation. | +| **TC28** | 65 | Empty Firmware Name | ❌ | `req.firmwareName = ""` β†’ `RDKFW_UPDATE_FAILED`. **Proves:** empty-string check. | +| **TC29** | 66 | Duplicate (Same Process) | βœ… | Two `updateFirmware()` calls. Second rejected by `internal_begin_update()`. **Proves:** `g_update_in_progress` guard. | +| **TC30** | 67 | Rapid Retry | βœ… | Update β†’ wait β†’ update again. Second succeeds. **Proves:** `internal_end_update()` clears guard. | +| **TC31** | 68 | Progress Monotonicity | βœ… | Multiple callbacks, progress never decreases. **Proves:** daemon sends correct progress sequence. | +| **TC32** | 69 | Terminal Status | βœ… | Final callback = `UPDATE_COMPLETED` or `UPDATE_ERROR`. **Proves:** event loop quit logic for update signals. | +| **TC33** | 70 | Empty Handle | ❌ | β†’ `RDKFW_UPDATE_FAILED`. **Proves:** empty string validation for update path. | + +### Category 5: Unregister During Active Operations (TC34–TC36) + +| TC | Menu | Name | Daemon? | What It Tests | +|----|:----:|------|:-------:|---------------| +| **TC34** | 80 | Unregister During Check | βœ… | Starts `checkForUpdate()`, then immediately calls `unregisterProcess()`. If session guard works, unregister is blocked (no-op) and callback still fires. **Proves:** `unregisterProcess()` calls `internal_is_check_in_progress()` and refuses to destroy the handle while a check worker thread is active. Prevents use-after-free. | +| **TC35** | 81 | Unregister During Download | βœ… | Same pattern with `downloadFirmware()`. Start download β†’ immediately unregister β†’ verify terminal callback still fires. **Proves:** download in-progress guard blocks premature handle destruction. | +| **TC36** | 82 | Unregister During Update | βœ… | Same pattern with `updateFirmware()`. Start update β†’ immediately unregister β†’ verify terminal callback still fires. **Proves:** update in-progress guard blocks premature handle destruction. | + +### Category 6: Full Lifecycle & Cross-API (TC37–TC39) + +| TC | Menu | Name | Daemon? | What It Tests | +|----|:----:|------|:-------:|---------------| +| **TC37** | 90 | Full Lifecycle | βœ… | **The integration test.** Register β†’ CheckForUpdate (wait) β†’ DownloadFirmware (wait) β†’ UpdateFirmware (wait) β†’ Unregister. All five steps must succeed. **Proves:** entire library + daemon interaction end-to-end β€” handle lifetime, worker thread create/destroy for all 3 APIs, D-Bus signal subscribe/unsubscribe, condvar handshake accuracy, callback delivery, and cleanup. If anything leaks or leaves stale state, this catches it. | +| **TC38** | 91 | Lifecycle No Sleeps | βœ… | Same as TC37 but with **NO `sleep()` between API calls**. After check callback fires β†’ immediately call download. After download completes β†’ immediately call update. **Proves:** worker thread cleanup timing β€” previous worker must have called `internal_end_*()` and fully exited before next `internal_begin_*()` succeeds. If the library has a cleanup race condition, this test will catch it. | +| **TC39** | 92 | Check + Download Simultaneous | βœ… | Starts `checkForUpdate()`, then immediately starts `downloadFirmware()` while check is still active. Both must succeed. **Proves:** the three APIs use **independent** in-progress guards (`g_check_in_progress` vs `g_dwnl_in_progress`). If library incorrectly uses a single global lock, the second call would be rejected. | + +--- + +## Test Summary Table + +| Category | TC Range | Count | What's Validated | +|----------|:--------:|:-----:|-----------------| +| Register / Unregister | TC01–TC04 | 4 | Handle creation, cleanup, NULL safety, multi-client | +| CheckForUpdate | TC05–TC11 | 7 | Happy path, input validation, guard, retry, callback data | +| DownloadFirmware | TC12–TC22 | 11 | Happy path, input validation, guard, retry, progress, terminal | +| UpdateFirmware | TC23–TC33 | 11 | Happy path, input validation, guard, retry, progress, terminal | +| Unregister Guards | TC34–TC36 | 3 | Session protection during active operations | +| Full Lifecycle | TC37–TC39 | 3 | End-to-end integration, timing, cross-API independence | +| **Total** | | **39** | | + +--- + +## Timeout Configuration + +| Operation | Timeout | Why | +|-----------|:-------:|-----| +| `checkForUpdate` callback | 130 seconds | XConf query time varies by network | +| `downloadFirmware` terminal | 600 seconds | Depends on firmware size and network speed | +| `updateFirmware` terminal | 600 seconds | Depends on flash hardware speed | +| Flag poll interval | 100ms | Balance between responsiveness and CPU usage | + +These are hardcoded in `KnowWhereItBreaks.c` in the `wait_flag()` function +and the `wait_flag()` call sites. Adjust if your environment is slower. + +--- + +## Manual-Only Test Scenarios + +These require external actions that cannot be automated inside the utility: + +| Scenario | How to Run | Expected Behavior | +|----------|-----------|-------------------| +| **Daemon down** | Stop daemon β†’ `kwib_test_utility --auto-happy` | All happy paths return FAIL. No crashes, no hangs. Exit code 1. | +| **Daemon crash mid-download** | Start TC12 (menu 50) β†’ in another terminal: `kill -9 $(pidof rdkFwupdateMgr)` | Callback eventually fires with `DWNL_ERROR` via timeout path. | +| **Daemon crash mid-update** | Start TC23 (menu 60) β†’ kill daemon | Callback eventually fires with `UPDATE_ERROR`. | +| **Cross-process rejection** | Run two instances of `kwib_test_utility`. Both register. Both try TC12 simultaneously. | One succeeds, the other gets `RDKFW_DWNL_FAILED` (daemon-level rejection). | +| **Memory leak check** | `valgrind --leak-check=full kwib_test_utility --auto-all` | 0 bytes definitely lost. GLib "still reachable" is expected and harmless. | +| **Thread sanitizer** | Rebuild with `-fsanitize=thread`, run `--auto-all` | No data race warnings. | +| **Network failure during download** | Disconnect network after TC12 starts | `DWNL_ERROR` callback fires. | + +--- + +## Architecture + +``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ kwib_test_utility (process) β”‚ +β”‚ β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ 39 Test β”‚ β”‚ Result Track β”‚ β”‚ Callback Track β”‚ β”‚ +β”‚ β”‚ Functions β”‚ β”‚ PASS / FAIL / β”‚ β”‚ volatile bools β”‚ β”‚ +β”‚ β”‚ tc01() β”‚ β”‚ SKIP counters β”‚ β”‚ cb_fired? β”‚ β”‚ +β”‚ β”‚ ... β”‚ β”‚ β”‚ β”‚ status? β”‚ β”‚ +β”‚ β”‚ tc39() β”‚ β”‚ g_results β”‚ β”‚ progress? β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ count? β”‚ β”‚ +β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ Public API calls β”‚ +β”‚ β–Ό β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ librdkFwupdateMgr.so β”‚ ← Library under test β”‚ +β”‚ β”‚ (linked at build time) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β”‚ β”‚ D-Bus (system bus) β”‚ +β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ +β”‚ β”‚ rdkFwupdateMgr β”‚ ← Daemon process β”‚ +β”‚ β”‚ (separate process) β”‚ β”‚ +β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +**Data flow for a happy-path test (e.g., TC05):** + +``` +kwib_test_utility librdkFwupdateMgr.so rdkFwupdateMgr daemon +───────────────── ──────────────────── ──────────────────── +tc05_check_happy() + β”‚ + β”œβ”€ checkForUpdate(handle, cb) ──► input validation + β”‚ internal_begin_check() + β”‚ pthread_create(worker) + β”‚ β”‚ + β”‚ ◄── CHECK_FOR_UPDATE_SUCCESS β”€β”€β”€β”˜ + β”‚ worker thread: + β”‚ g_bus_get_sync() + β”‚ g_dbus_connection_call_sync() ──► CheckForUpdate method + β”‚ β”‚ + β”‚ g_dbus_connection_signal_subscribe() + β”‚ g_main_loop_run() ◄── XConf query + β”‚ β”‚ β”‚ + β”‚ β”‚ ◄── CheckForUpdateComplete signal β”˜ + β”‚ β”‚ + β”‚ on_check_signal_handler() + β”‚ parse GVariant (tiissss) + β”‚ cb(&fwInfoData) ──────────────────┐ + β”‚ β”‚ + β”‚ check_callback() fires β—„β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ g_check_cb_fired = true + β”‚ g_check_status = status + β”‚ + β”œβ”€ wait_flag(&g_check_cb_fired) + β”‚ ... polling 100ms ... + β”‚ flag is true! + β”‚ + β”œβ”€ TEST_PASS("TC05") + β”‚ +``` + +--- + +## Troubleshooting + +| Symptom | Likely Cause | Fix | +|---------|-------------|-----| +| All happy-path tests return FAIL | Daemon not running | `systemctl start rdkFwupdateMgr` | +| Binary not found after `make` | Forgot to re-run `./configure` after Makefile.am change | `autoreconf -fi && ./configure [flags] && make` | +| `rm: cannot remove 'KnowWhereItBreaks': Is a directory` | Old Makefile still has `bin_PROGRAMS += KnowWhereItBreaks` | Run `make clean && autoreconf -fi && ./configure [flags] && make` | +| Test hangs on "Waiting for callback" | Daemon not sending expected signal | Check `/opt/logs/rdkFwupdateMgr.log` or `journalctl -u rdkFwupdateMgr -f` | +| TC09/TC18/TC29 duplicate test PASS when shouldn't | First operation completed before second call (fast daemon) | Normal β€” guard worked, operation was just fast. Not a bug. | +| TC34–TC36 unregister guard tests FAIL | `unregisterProcess()` missing in-progress checks | Fix guard logic in `rdkFwupdateMgr_process.c` | +| TC20/TC31 progress not monotonic | Daemon sending non-monotonic values | Daemon bug β€” library just relays accurately | +| TC38 no-sleep lifecycle FAIL | Worker thread cleanup race | Check `internal_end_*()` call timing in worker cleanup path | +| TC39 simultaneous check+download FAIL | Library using single global guard instead of per-API guards | Fix `rdkFwupdateMgr_async.c` β€” each API needs its own `g_*_in_progress` | +| TC10/TC19/TC30 rapid retry FAIL | `internal_end_*()` not being called in cleanup | Check worker thread cleanup path calls `internal_end_*()` in all exit branches | +| `error while loading shared libraries` | `librdkFwupdateMgr.so` not in linker path | `export LD_LIBRARY_PATH=/usr/lib:$LD_LIBRARY_PATH` or run `ldconfig` | + +--- + +## Comparison: kwib_test_utility vs example_plugin + +| Aspect | `example_plugin` | `kwib_test_utility` | +|--------|------------------|---------------------| +| **Purpose** | Clean reference for external teams | Developer test utility for finding bugs | +| **Source** | `librdkFwupdateMgr/examples/example_app.c` | `KnowWhereItBreaks/KnowWhereItBreaks.c` | +| **Audience** | Plugin/app developers | Library/daemon developers | +| **Test cases** | 0 (it's an example workflow) | 39 | +| **Error injection** | None | NULL/empty/invalid for every API parameter | +| **Automation** | None (single one-shot run) | 4 automated modes + interactive menu | +| **Result tracking** | None | PASS/FAIL/SKIP with colored summary | +| **Guard testing** | None | Duplicate calls, unregister during active ops | +| **Retry testing** | None | Rapid retry after completion | +| **Cross-API testing** | None | Simultaneous check+download | +| **Lifecycle testing** | Single workflow | Multiple lifecycle patterns (with/without sleeps) | +| **CI integration** | N/A | Exit code 0/1, `--auto-all` for pipelines | +| **Lines** | ~700 | ~1300 | +| **Build rule** | `bin_PROGRAMS += example_plugin` | `bin_PROGRAMS += kwib_test_utility` | +| **Installed to** | `/usr/bin/example_plugin` | `/usr/bin/kwib_test_utility` | +| **Links against** | `librdkFwupdateMgr.la` | `librdkFwupdateMgr.la` | + +--- + +## Files + +| File | Purpose | +|------|---------| +| `KnowWhereItBreaks/KnowWhereItBreaks.c` | Source code β€” all 39 test cases, callbacks, menu, automation | +| `KnowWhereItBreaks/KnowWhereItBreaks_README.md` | Technical reference β€” test case catalog, architecture, internals | +| `KnowWhereItBreaks/USAGE_KWIB.md` | **This file** β€” how to build, run, and interpret results | +| `Makefile.am` | Build rule: `kwib_test_utility` target (lines 275–293) | + +--- + +## CI / Scripting Examples + +### Run in CI pipeline (fail build on test failure) + +```bash +#!/bin/bash +systemctl start rdkFwupdateMgr +sleep 2 + +kwib_test_utility --auto-all +exit_code=$? + +if [ $exit_code -ne 0 ]; then + echo "ERROR: kwib_test_utility reported test failures" + exit 1 +fi + +echo "All kwib tests passed" +``` + +### Run only error tests (no daemon needed for most) + +```bash +kwib_test_utility --auto-error +``` + +### Run with valgrind + +```bash +valgrind --leak-check=full --show-leak-kinds=all \ + kwib_test_utility --auto-all 2>&1 | tee valgrind_kwib.log +``` + +### Run with thread sanitizer + +```bash +# Rebuild with sanitizer +export CFLAGS="-fsanitize=thread -g" +make clean && make + +kwib_test_utility --auto-all 2>&1 | tee tsan_kwib.log +``` + +--- + +**Version**: 1.0 +**Binary**: `kwib_test_utility` +**Source**: `KnowWhereItBreaks/KnowWhereItBreaks.c` +**Last Updated**: March 2026 +**Status**: Complete β€” 39 test cases covering all public API paths diff --git a/librdkFwupdateMgr/examples/KnowWhereItBreaks.c b/librdkFwupdateMgr/examples/KnowWhereItBreaks.c new file mode 100755 index 00000000..db9aeffc --- /dev/null +++ b/librdkFwupdateMgr/examples/KnowWhereItBreaks.c @@ -0,0 +1,1328 @@ +/* + * 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 + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file KnowWhereItBreaks.c + * @brief Comprehensive developer test utility for librdkFwupdateMgr.so + * + * This is NOT the example_plugin (which is a clean reference for external teams). + * This is YOUR developer weapon for exercising every code path in the library + * and daemon before bugs find you. + * + * Tests every layer: + * - Library input validation (NULL/empty args β†’ immediate FAIL) + * - Library in-progress guards (duplicate same-process call β†’ rejected) + * - Library session guards (unregister during active op β†’ blocked) + * - Condvar handshake accuracy (return code matches daemon reply) + * - Worker thread lifecycle (create β†’ run β†’ cleanup β†’ exit, no leaks) + * - D-Bus method calls and signal reception + * - Callback correctness (right data, right count, right thread) + * - Full lifecycle (register β†’ check β†’ download β†’ update β†’ unregister) + * - Rapid retry (call again immediately after previous completes) + * + * Usage: + * ./KnowWhereItBreaks (interactive menu) + * ./KnowWhereItBreaks --auto-error (error/validation tests) + * ./KnowWhereItBreaks --auto-happy (happy path β€” daemon required) + * ./KnowWhereItBreaks --full-lifecycle (end-to-end β€” daemon required) + * ./KnowWhereItBreaks --auto-all (everything) + * + * See KnowWhereItBreaks_README.md for full documentation. + */ + +#include "rdkFwupdateMgr_client.h" +#include +#include +#include +#include +#include +#include +#include + +/* ======================================================================== + * TEST INFRASTRUCTURE + * ======================================================================== */ + +typedef struct { + int total; + int passed; + int failed; + int skipped; +} TestResults; + +static TestResults g_results = {0, 0, 0, 0}; + +#define TEST_PASS(name) do { \ + g_results.total++; g_results.passed++; \ + printf(" [\033[32mPASS\033[0m] %s\n", name); \ +} while(0) + +#define TEST_FAIL(name, reason) do { \ + g_results.total++; g_results.failed++; \ + printf(" [\033[31mFAIL\033[0m] %s β€” %s\n", name, reason); \ +} while(0) + +#define TEST_SKIP(name, reason) do { \ + g_results.total++; g_results.skipped++; \ + printf(" [\033[33mSKIP\033[0m] %s β€” %s\n", name, reason); \ +} while(0) + +#define TEST_INFO(fmt, ...) printf(" [INFO] " fmt "\n", ##__VA_ARGS__) + +/* ======================================================================== + * CALLBACK TRACKING STATE + * ======================================================================== + * Volatile: callbacks fire from worker threads, main thread polls these. + * ======================================================================== */ + +/* CheckForUpdate tracking */ +static volatile bool g_check_cb_fired = false; +static volatile int g_check_cb_count = 0; +static volatile int g_check_status = -1; +static char g_check_current_ver[MAX_FW_VERSION_SIZE] = {0}; + +/* DownloadFirmware tracking */ +static volatile bool g_dwnl_cb_terminal = false; +static volatile int g_dwnl_cb_count = 0; +static volatile int g_dwnl_status = -1; +static volatile int g_dwnl_last_progress = -1; +static volatile bool g_dwnl_progress_mono = true; + +/* UpdateFirmware tracking */ +static volatile bool g_update_cb_terminal = false; +static volatile int g_update_cb_count = 0; +static volatile int g_update_status = -1; +static volatile int g_update_last_progress = -1; +static volatile bool g_update_progress_mono = true; + +/* Global handle */ +static FirmwareInterfaceHandle g_handle = NULL; + +/* ======================================================================== + * CALLBACKS + * ======================================================================== */ + +static void check_callback(const FwInfoData *info) +{ + g_check_cb_count++; + if (info) { + g_check_status = info->status; + if (info->CurrFWVersion[0] != '\0') { + strncpy(g_check_current_ver, info->CurrFWVersion, + sizeof(g_check_current_ver) - 1); + } + printf(" [CB:Check] #%d status=%d current='%s'\n", + g_check_cb_count, info->status, info->CurrFWVersion); + } else { + printf(" [CB:Check] #%d β€” NULL info!\n", g_check_cb_count); + } + g_check_cb_fired = true; +} + +static void download_callback(int progress, DownloadStatus status) +{ + int prev = g_dwnl_last_progress; + g_dwnl_cb_count++; + g_dwnl_last_progress = progress; + g_dwnl_status = (int)status; + + /* Track monotonicity */ + if (prev >= 0 && progress < prev) { + g_dwnl_progress_mono = false; + } + + printf(" [CB:Dwnl] #%d progress=%d%% status=%d\n", + g_dwnl_cb_count, progress, (int)status); + + if (status == DWNL_COMPLETED || status == DWNL_ERROR) { + g_dwnl_cb_terminal = true; + } +} + +static void update_callback(int progress, UpdateStatus status) +{ + int prev = g_update_last_progress; + g_update_cb_count++; + g_update_last_progress = progress; + g_update_status = (int)status; + + /* Track monotonicity */ + if (prev >= 0 && progress < prev) { + g_update_progress_mono = false; + } + + printf(" [CB:Update] #%d progress=%d%% status=%d\n", + g_update_cb_count, progress, (int)status); + + if (status == UPDATE_COMPLETED || status == UPDATE_ERROR) { + g_update_cb_terminal = true; + } +} + +/* ======================================================================== + * HELPERS + * ======================================================================== */ + +static void reset_all(void) +{ + g_check_cb_fired = false; + g_check_cb_count = 0; + g_check_status = -1; + g_check_current_ver[0] = '\0'; + + g_dwnl_cb_terminal = false; + g_dwnl_cb_count = 0; + g_dwnl_status = -1; + g_dwnl_last_progress = -1; + g_dwnl_progress_mono = true; + + g_update_cb_terminal = false; + g_update_cb_count = 0; + g_update_status = -1; + g_update_last_progress = -1; + g_update_progress_mono = true; +} + +/** Poll a volatile bool with timeout. Returns true if flag set before timeout. */ +static bool wait_flag(volatile bool *flag, int timeout_sec) +{ + for (int i = 0; i < timeout_sec * 10; i++) { + if (*flag) return true; + usleep(100000); /* 100ms */ + } + return false; +} + +static void separator(const char *title) +{ + printf("\n"); + printf("══════════════════════════════════════════════════════════════\n"); + printf(" %s\n", title); + printf("══════════════════════════════════════════════════════════════\n"); +} + +/** Register if not already registered. Returns true if handle is available. */ +static bool ensure_registered(void) +{ + if (g_handle != NULL) return true; + g_handle = registerProcess("KnowWhereItBreaks", LIB_VERSION); + if (g_handle != NULL) { + TEST_INFO("Auto-registered: handle='%s'", g_handle); + return true; + } + TEST_INFO("Auto-register FAILED β€” daemon may be down"); + return false; +} + +static void ensure_unregistered(void) +{ + if (g_handle != NULL) { + unregisterProcess(g_handle); + g_handle = NULL; + } +} + +/* ======================================================================== + * TC01–TC04: REGISTER / UNREGISTER + * ======================================================================== */ + +static void tc01_register_happy(void) +{ + printf("\n--- TC01: Register Happy Path ---\n"); + FirmwareInterfaceHandle h = registerProcess("KnowWhereItBreaks", LIB_VERSION); + if (h != NULL && strlen(h) > 0) { + TEST_PASS("TC01 β€” registerProcess() returned valid handle"); + printf(" Handle: '%s'\n", h); + g_handle = h; + } else { + TEST_FAIL("TC01 β€” registerProcess()", "Returned NULL or empty handle"); + } +} + +static void tc02_unregister_happy(void) +{ + printf("\n--- TC02: Unregister Happy Path ---\n"); + if (!g_handle) { TEST_SKIP("TC02", "No handle available"); return; } + /* unregisterProcess returns void β€” if it doesn't crash, it passed */ + unregisterProcess(g_handle); + TEST_PASS("TC02 β€” unregisterProcess() completed without crash"); + g_handle = NULL; +} + +static void tc03_unregister_null(void) +{ + printf("\n--- TC03: Unregister NULL Handle ---\n"); + /* unregisterProcess(NULL) should not crash (safe to call with NULL per docs) */ + unregisterProcess(NULL); + TEST_PASS("TC03 β€” unregisterProcess(NULL) did not crash"); +} + +static void tc04_double_register(void) +{ + printf("\n--- TC04: Double Register ---\n"); + FirmwareInterfaceHandle h1 = registerProcess("KWIB_Test1", LIB_VERSION); + FirmwareInterfaceHandle h2 = registerProcess("KWIB_Test2", LIB_VERSION); + + if (h1 != NULL && h2 != NULL) { + TEST_PASS("TC04 β€” Both registrations succeeded"); + printf(" Handle1='%s' Handle2='%s'\n", h1, h2); + unregisterProcess(h2); + if (!g_handle) g_handle = h1; + else unregisterProcess(h1); + } else { + TEST_FAIL("TC04 β€” double_register", "One or both returned NULL"); + if (h1) unregisterProcess(h1); + if (h2) unregisterProcess(h2); + } +} + +/* ======================================================================== + * TC05–TC11: CHECKFORUPDATE + * ======================================================================== */ + +static void tc05_check_happy(void) +{ + printf("\n--- TC05: CheckForUpdate Happy Path ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC05", "No handle"); return; } + + CheckForUpdateResult ret = checkForUpdate(g_handle, check_callback); + if (ret != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC05 β€” checkForUpdate()", "API returned FAIL"); + return; + } + TEST_INFO("Waiting for callback (max 130s)..."); + if (wait_flag(&g_check_cb_fired, 130)) { + TEST_PASS("TC05 β€” checkForUpdate callback received"); + printf(" Status: %d CurrentVer: '%s'\n", g_check_status, g_check_current_ver); + } else { + TEST_FAIL("TC05 β€” checkForUpdate()", "Callback never fired (130s timeout)"); + } +} + +static void tc06_check_null_handle(void) +{ + printf("\n--- TC06: CheckForUpdate NULL Handle ---\n"); + CheckForUpdateResult ret = checkForUpdate(NULL, check_callback); + if (ret == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC06 β€” NULL handle rejected"); + else + TEST_FAIL("TC06 β€” NULL handle", "Expected FAIL"); +} + +static void tc07_check_null_callback(void) +{ + printf("\n--- TC07: CheckForUpdate NULL Callback ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC07", "No handle"); return; } + CheckForUpdateResult ret = checkForUpdate(g_handle, NULL); + if (ret == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC07 β€” NULL callback rejected"); + else + TEST_FAIL("TC07 β€” NULL callback", "Expected FAIL"); +} + +static void tc08_check_empty_handle(void) +{ + printf("\n--- TC08: CheckForUpdate Empty Handle ---\n"); + CheckForUpdateResult ret = checkForUpdate("", check_callback); + if (ret == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC08 β€” empty handle rejected"); + else + TEST_FAIL("TC08 β€” empty handle", "Expected FAIL"); +} + +static void tc09_check_duplicate(void) +{ + printf("\n--- TC09: CheckForUpdate Duplicate (Same Process) ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC09", "No handle"); return; } + + CheckForUpdateResult r1 = checkForUpdate(g_handle, check_callback); + if (r1 != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC09 β€” first call", "First checkForUpdate failed"); + return; + } + + /* Immediately call again β€” library guard should reject */ + CheckForUpdateResult r2 = checkForUpdate(g_handle, check_callback); + if (r2 == CHECK_FOR_UPDATE_FAIL) + TEST_PASS("TC09 β€” duplicate call rejected by library guard"); + else + TEST_FAIL("TC09 β€” duplicate call", "Second call was NOT rejected"); + + TEST_INFO("Waiting for first check to complete..."); + wait_flag(&g_check_cb_fired, 130); +} + +static void tc10_check_rapid_retry(void) +{ + printf("\n--- TC10: CheckForUpdate Rapid Retry ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC10", "No handle"); return; } + + /* First call */ + CheckForUpdateResult r1 = checkForUpdate(g_handle, check_callback); + if (r1 != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC10 β€” first call", "Failed"); + return; + } + TEST_INFO("Waiting for first callback..."); + if (!wait_flag(&g_check_cb_fired, 130)) { + TEST_FAIL("TC10", "First callback never fired"); + return; + } + + sleep(1); /* Let worker thread fully exit */ + reset_all(); + + /* Retry β€” should succeed (guard cleared after previous completed) */ + CheckForUpdateResult r2 = checkForUpdate(g_handle, check_callback); + if (r2 == CHECK_FOR_UPDATE_SUCCESS) { + TEST_PASS("TC10 β€” rapid retry accepted (guard was cleared)"); + wait_flag(&g_check_cb_fired, 130); + } else { + TEST_FAIL("TC10 β€” rapid retry", "Rejected (in-progress flag not cleared?)"); + } +} + +static void tc11_check_callback_data(void) +{ + printf("\n--- TC11: CheckForUpdate Callback Data Validation ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC11", "No handle"); return; } + + CheckForUpdateResult ret = checkForUpdate(g_handle, check_callback); + if (ret != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC11", "API returned FAIL"); + return; + } + if (!wait_flag(&g_check_cb_fired, 130)) { + TEST_FAIL("TC11", "Callback never fired"); + return; + } + + if (g_check_cb_count == 1) + TEST_PASS("TC11 β€” callback fired exactly once"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Callback fired %d times (expected 1)", g_check_cb_count); + TEST_FAIL("TC11", msg); + } + + if (g_check_status >= 0 && g_check_status <= 5) + TEST_PASS("TC11 β€” status is valid CheckForUpdateStatus enum"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "status=%d not in [0..5]", g_check_status); + TEST_FAIL("TC11", msg); + } +} + +/* ======================================================================== + * TC12–TC22: DOWNLOAD FIRMWARE + * ======================================================================== */ + +static FwDwnlReq make_dwnl_req(void) +{ + FwDwnlReq req; + memset(&req, 0, sizeof(req)); + req.firmwareName = "test_firmware.bin"; + req.downloadUrl = "http://localhost:8080/firmware/test_firmware.bin"; + req.TypeOfFirmware = "PCI"; + return req; +} + +static void tc12_download_happy(void) +{ + printf("\n--- TC12: DownloadFirmware Happy Path ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC12", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware(g_handle, &req, download_callback); + if (ret != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC12", "API returned FAILED (daemon rejected?)"); + return; + } + TEST_PASS("TC12 β€” daemon accepted download request"); + + TEST_INFO("Waiting for terminal callback (max 600s)..."); + if (wait_flag(&g_dwnl_cb_terminal, 600)) { + if (g_dwnl_status == (int)DWNL_COMPLETED) + TEST_PASS("TC12 β€” download completed successfully"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Terminal status=%d (expected COMPLETED=%d)", g_dwnl_status, DWNL_COMPLETED); + TEST_FAIL("TC12", msg); + } + } else { + TEST_FAIL("TC12", "Terminal callback never fired (600s timeout)"); + } +} + +static void tc13_download_null_handle(void) +{ + printf("\n--- TC13: DownloadFirmware NULL Handle ---\n"); + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware(NULL, &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC13 β€” NULL handle rejected"); + else TEST_FAIL("TC13", "Expected FAILED"); +} + +static void tc14_download_null_request(void) +{ + printf("\n--- TC14: DownloadFirmware NULL Request ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC14", "No handle"); return; } + DownloadResult ret = downloadFirmware(g_handle, NULL, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC14 β€” NULL request rejected"); + else TEST_FAIL("TC14", "Expected FAILED"); +} + +static void tc15_download_null_callback(void) +{ + printf("\n--- TC15: DownloadFirmware NULL Callback ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC15", "No handle"); return; } + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware(g_handle, &req, NULL); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC15 β€” NULL callback rejected"); + else TEST_FAIL("TC15", "Expected FAILED"); +} + +static void tc16_download_null_firmware_name(void) +{ + printf("\n--- TC16: DownloadFirmware NULL Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC16", "No handle"); return; } + FwDwnlReq req = make_dwnl_req(); + req.firmwareName = NULL; + DownloadResult ret = downloadFirmware(g_handle, &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC16 β€” NULL firmwareName rejected"); + else TEST_FAIL("TC16", "Expected FAILED"); +} + +static void tc17_download_empty_firmware_name(void) +{ + printf("\n--- TC17: DownloadFirmware Empty Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC17", "No handle"); return; } + FwDwnlReq req = make_dwnl_req(); + req.firmwareName = ""; + DownloadResult ret = downloadFirmware(g_handle, &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC17 β€” empty firmwareName rejected"); + else TEST_FAIL("TC17", "Expected FAILED"); +} + +static void tc18_download_duplicate(void) +{ + printf("\n--- TC18: DownloadFirmware Duplicate (Same Process) ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC18", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + DownloadResult r1 = downloadFirmware(g_handle, &req, download_callback); + if (r1 != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC18 β€” first call", "First download failed"); + return; + } + + DownloadResult r2 = downloadFirmware(g_handle, &req, download_callback); + if (r2 == RDKFW_DWNL_FAILED) + TEST_PASS("TC18 β€” duplicate download rejected by library guard"); + else + TEST_FAIL("TC18", "Second call was NOT rejected"); + + TEST_INFO("Waiting for first download to complete..."); + wait_flag(&g_dwnl_cb_terminal, 600); +} + +static void tc19_download_rapid_retry(void) +{ + printf("\n--- TC19: DownloadFirmware Rapid Retry ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC19", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + + DownloadResult r1 = downloadFirmware(g_handle, &req, download_callback); + if (r1 != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC19 β€” first call", "Failed"); + return; + } + TEST_INFO("Waiting for first download to complete..."); + if (!wait_flag(&g_dwnl_cb_terminal, 600)) { + TEST_FAIL("TC19", "First terminal never fired"); + return; + } + + sleep(1); + reset_all(); + + DownloadResult r2 = downloadFirmware(g_handle, &req, download_callback); + if (r2 == RDKFW_DWNL_SUCCESS) { + TEST_PASS("TC19 β€” rapid retry accepted (guard was cleared)"); + wait_flag(&g_dwnl_cb_terminal, 600); + } else { + TEST_FAIL("TC19", "Rejected (in-progress flag not cleared?)"); + } +} + +static void tc20_download_progress_mono(void) +{ + printf("\n--- TC20: Download Progress Monotonicity ---\n"); + /* Uses data from the most recent download. Run TC12 first. */ + if (g_dwnl_cb_count == 0) { + TEST_SKIP("TC20", "No download has run yet β€” run TC12 first"); + return; + } + if (g_dwnl_cb_count > 1) + TEST_PASS("TC20 β€” multiple progress callbacks fired"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Only %d callback(s)", g_dwnl_cb_count); + TEST_FAIL("TC20", msg); + } + if (g_dwnl_progress_mono) + TEST_PASS("TC20 β€” progress values were monotonically increasing"); + else + TEST_FAIL("TC20", "Progress decreased at some point (daemon bug?)"); +} + +static void tc21_download_terminal(void) +{ + printf("\n--- TC21: Download Terminal Status ---\n"); + if (g_dwnl_cb_count == 0) { + TEST_SKIP("TC21", "No download has run yet"); + return; + } + if (g_dwnl_status == (int)DWNL_COMPLETED || g_dwnl_status == (int)DWNL_ERROR) + TEST_PASS("TC21 β€” final callback had terminal status"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Final status=%d (not COMPLETED or ERROR)", g_dwnl_status); + TEST_FAIL("TC21", msg); + } +} + +static void tc22_download_empty_handle(void) +{ + printf("\n--- TC22: DownloadFirmware Empty Handle ---\n"); + FwDwnlReq req = make_dwnl_req(); + DownloadResult ret = downloadFirmware("", &req, download_callback); + if (ret == RDKFW_DWNL_FAILED) TEST_PASS("TC22 β€” empty handle rejected"); + else TEST_FAIL("TC22", "Expected FAILED"); +} + +/* ======================================================================== + * TC23–TC33: UPDATE FIRMWARE + * ======================================================================== */ + +static FwUpdateReq make_update_req(void) +{ + FwUpdateReq req; + memset(&req, 0, sizeof(req)); + req.firmwareName = "test_firmware.bin"; + req.TypeOfFirmware = "PCI"; + req.LocationOfFirmware = "/opt/CDL"; + req.rebootImmediately = false; + return req; +} + +static void tc23_update_happy(void) +{ + printf("\n--- TC23: UpdateFirmware Happy Path ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC23", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware(g_handle, &req, update_callback); + if (ret != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC23", "API returned FAILED (daemon rejected?)"); + return; + } + TEST_PASS("TC23 β€” daemon accepted update request"); + + TEST_INFO("Waiting for terminal callback (max 600s)..."); + if (wait_flag(&g_update_cb_terminal, 600)) { + if (g_update_status == (int)UPDATE_COMPLETED) + TEST_PASS("TC23 β€” update completed successfully"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Terminal status=%d (expected COMPLETED=%d)", g_update_status, UPDATE_COMPLETED); + TEST_FAIL("TC23", msg); + } + } else { + TEST_FAIL("TC23", "Terminal callback never fired (600s timeout)"); + } +} + +static void tc24_update_null_handle(void) +{ + printf("\n--- TC24: UpdateFirmware NULL Handle ---\n"); + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware(NULL, &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC24 β€” NULL handle rejected"); + else TEST_FAIL("TC24", "Expected FAILED"); +} + +static void tc25_update_null_request(void) +{ + printf("\n--- TC25: UpdateFirmware NULL Request ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC25", "No handle"); return; } + UpdateResult ret = updateFirmware(g_handle, NULL, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC25 β€” NULL request rejected"); + else TEST_FAIL("TC25", "Expected FAILED"); +} + +static void tc26_update_null_callback(void) +{ + printf("\n--- TC26: UpdateFirmware NULL Callback ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC26", "No handle"); return; } + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware(g_handle, &req, NULL); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC26 β€” NULL callback rejected"); + else TEST_FAIL("TC26", "Expected FAILED"); +} + +static void tc27_update_null_firmware_name(void) +{ + printf("\n--- TC27: UpdateFirmware NULL Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC27", "No handle"); return; } + FwUpdateReq req = make_update_req(); + req.firmwareName = NULL; + UpdateResult ret = updateFirmware(g_handle, &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC27 β€” NULL firmwareName rejected"); + else TEST_FAIL("TC27", "Expected FAILED"); +} + +static void tc28_update_empty_firmware_name(void) +{ + printf("\n--- TC28: UpdateFirmware Empty Firmware Name ---\n"); + if (!ensure_registered()) { TEST_SKIP("TC28", "No handle"); return; } + FwUpdateReq req = make_update_req(); + req.firmwareName = ""; + UpdateResult ret = updateFirmware(g_handle, &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC28 β€” empty firmwareName rejected"); + else TEST_FAIL("TC28", "Expected FAILED"); +} + +static void tc29_update_duplicate(void) +{ + printf("\n--- TC29: UpdateFirmware Duplicate (Same Process) ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC29", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + UpdateResult r1 = updateFirmware(g_handle, &req, update_callback); + if (r1 != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC29 β€” first call", "First update failed"); + return; + } + + UpdateResult r2 = updateFirmware(g_handle, &req, update_callback); + if (r2 == RDKFW_UPDATE_FAILED) + TEST_PASS("TC29 β€” duplicate update rejected by library guard"); + else + TEST_FAIL("TC29", "Second call was NOT rejected"); + + TEST_INFO("Waiting for first update to complete..."); + wait_flag(&g_update_cb_terminal, 600); +} + +static void tc30_update_rapid_retry(void) +{ + printf("\n--- TC30: UpdateFirmware Rapid Retry ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC30", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + + UpdateResult r1 = updateFirmware(g_handle, &req, update_callback); + if (r1 != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC30 β€” first call", "Failed"); + return; + } + TEST_INFO("Waiting for first update to complete..."); + if (!wait_flag(&g_update_cb_terminal, 600)) { + TEST_FAIL("TC30", "First terminal never fired"); + return; + } + + sleep(1); + reset_all(); + + UpdateResult r2 = updateFirmware(g_handle, &req, update_callback); + if (r2 == RDKFW_UPDATE_SUCCESS) { + TEST_PASS("TC30 β€” rapid retry accepted (guard was cleared)"); + wait_flag(&g_update_cb_terminal, 600); + } else { + TEST_FAIL("TC30", "Rejected (in-progress flag not cleared?)"); + } +} + +static void tc31_update_progress_mono(void) +{ + printf("\n--- TC31: Update Progress Monotonicity ---\n"); + if (g_update_cb_count == 0) { + TEST_SKIP("TC31", "No update has run yet β€” run TC23 first"); + return; + } + if (g_update_cb_count > 1) + TEST_PASS("TC31 β€” multiple progress callbacks fired"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Only %d callback(s)", g_update_cb_count); + TEST_FAIL("TC31", msg); + } + if (g_update_progress_mono) + TEST_PASS("TC31 β€” progress values were monotonically increasing"); + else + TEST_FAIL("TC31", "Progress decreased (daemon bug?)"); +} + +static void tc32_update_terminal(void) +{ + printf("\n--- TC32: Update Terminal Status ---\n"); + if (g_update_cb_count == 0) { + TEST_SKIP("TC32", "No update has run yet"); + return; + } + if (g_update_status == (int)UPDATE_COMPLETED || g_update_status == (int)UPDATE_ERROR) + TEST_PASS("TC32 β€” final callback had terminal status"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "Final status=%d", g_update_status); + TEST_FAIL("TC32", msg); + } +} + +static void tc33_update_empty_handle(void) +{ + printf("\n--- TC33: UpdateFirmware Empty Handle ---\n"); + FwUpdateReq req = make_update_req(); + UpdateResult ret = updateFirmware("", &req, update_callback); + if (ret == RDKFW_UPDATE_FAILED) TEST_PASS("TC33 β€” empty handle rejected"); + else TEST_FAIL("TC33", "Expected FAILED"); +} + +/* ======================================================================== + * TC34–TC36: UNREGISTER DURING ACTIVE OPERATION + * ======================================================================== */ + +static void tc34_unreg_during_check(void) +{ + printf("\n--- TC34: Unregister During CheckForUpdate ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC34", "No handle"); return; } + + CheckForUpdateResult r1 = checkForUpdate(g_handle, check_callback); + if (r1 != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC34 β€” start check", "Failed"); + return; + } + + /* Immediately try unregister β€” should be blocked by in-progress guard */ + /* Note: unregisterProcess returns void, so we check if handle is still valid afterward */ + /* If guard works, unregister does nothing and we can still wait for callback */ + unregisterProcess(g_handle); + + /* If guard worked, callback should still fire (handle was NOT removed) */ + TEST_INFO("Waiting for check callback (if guard worked, it should still fire)..."); + if (wait_flag(&g_check_cb_fired, 130)) { + TEST_PASS("TC34 β€” callback still fired (unregister was blocked during active check)"); + } else { + TEST_FAIL("TC34", "Callback never fired (unregister may have succeeded during active op!)"); + } + + /* Re-register since handle may be invalidated */ + g_handle = NULL; + ensure_registered(); +} + +static void tc35_unreg_during_download(void) +{ + printf("\n--- TC35: Unregister During Download ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC35", "No handle"); return; } + + FwDwnlReq req = make_dwnl_req(); + DownloadResult r1 = downloadFirmware(g_handle, &req, download_callback); + if (r1 != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC35 β€” start download", "Failed"); + return; + } + + unregisterProcess(g_handle); + + TEST_INFO("Waiting for download terminal callback..."); + if (wait_flag(&g_dwnl_cb_terminal, 600)) { + TEST_PASS("TC35 β€” callback still fired (unregister was blocked during active download)"); + } else { + TEST_FAIL("TC35", "Terminal callback never fired"); + } + + g_handle = NULL; + ensure_registered(); +} + +static void tc36_unreg_during_update(void) +{ + printf("\n--- TC36: Unregister During Update ---\n"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC36", "No handle"); return; } + + FwUpdateReq req = make_update_req(); + UpdateResult r1 = updateFirmware(g_handle, &req, update_callback); + if (r1 != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC36 β€” start update", "Failed"); + return; + } + + unregisterProcess(g_handle); + + TEST_INFO("Waiting for update terminal callback..."); + if (wait_flag(&g_update_cb_terminal, 600)) { + TEST_PASS("TC36 β€” callback still fired (unregister was blocked during active update)"); + } else { + TEST_FAIL("TC36", "Terminal callback never fired"); + } + + g_handle = NULL; + ensure_registered(); +} + +/* ======================================================================== + * TC37–TC39: FULL LIFECYCLE + * ======================================================================== */ + +static void tc37_full_lifecycle(void) +{ + separator("TC37: FULL LIFECYCLE β€” Register β†’ Check β†’ Download β†’ Update β†’ Unregister"); + + /* Step 1: Register */ + printf("\n Step 1: Register\n"); + FirmwareInterfaceHandle h = registerProcess("KWIB_Lifecycle", LIB_VERSION); + if (h == NULL) { + TEST_FAIL("TC37 β€” register", "registerProcess() returned NULL"); + return; + } + TEST_PASS("TC37 β€” register"); + printf(" Handle: '%s'\n", h); + + /* Step 2: Check */ + printf("\n Step 2: CheckForUpdate\n"); + reset_all(); + CheckForUpdateResult cr = checkForUpdate(h, check_callback); + if (cr != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC37 β€” checkForUpdate", "API returned FAIL"); + unregisterProcess(h); return; + } + if (!wait_flag(&g_check_cb_fired, 130)) { + TEST_FAIL("TC37 β€” checkForUpdate", "Callback timeout"); + unregisterProcess(h); return; + } + TEST_PASS("TC37 β€” checkForUpdate completed"); + sleep(1); + + /* Step 3: Download */ + printf("\n Step 3: DownloadFirmware\n"); + reset_all(); + FwDwnlReq dreq = make_dwnl_req(); + DownloadResult dr = downloadFirmware(h, &dreq, download_callback); + if (dr != RDKFW_DWNL_SUCCESS) { + TEST_FAIL("TC37 β€” downloadFirmware", "API returned FAILED"); + unregisterProcess(h); return; + } + if (!wait_flag(&g_dwnl_cb_terminal, 600)) { + TEST_FAIL("TC37 β€” downloadFirmware", "Terminal callback timeout"); + unregisterProcess(h); return; + } + if (g_dwnl_status == (int)DWNL_COMPLETED) + TEST_PASS("TC37 β€” download completed"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "status=%d", g_dwnl_status); + TEST_FAIL("TC37 β€” download", msg); + unregisterProcess(h); return; + } + sleep(1); + + /* Step 4: Update */ + printf("\n Step 4: UpdateFirmware\n"); + reset_all(); + FwUpdateReq ureq = make_update_req(); + UpdateResult ur = updateFirmware(h, &ureq, update_callback); + if (ur != RDKFW_UPDATE_SUCCESS) { + TEST_FAIL("TC37 β€” updateFirmware", "API returned FAILED"); + unregisterProcess(h); return; + } + if (!wait_flag(&g_update_cb_terminal, 600)) { + TEST_FAIL("TC37 β€” updateFirmware", "Terminal callback timeout"); + unregisterProcess(h); return; + } + if (g_update_status == (int)UPDATE_COMPLETED) + TEST_PASS("TC37 β€” update completed"); + else { + char msg[64]; snprintf(msg, sizeof(msg), "status=%d", g_update_status); + TEST_FAIL("TC37 β€” update", msg); + unregisterProcess(h); return; + } + sleep(1); + + /* Step 5: Unregister */ + printf("\n Step 5: Unregister\n"); + unregisterProcess(h); + TEST_PASS("TC37 β€” unregister"); +} + +static void tc38_lifecycle_no_sleeps(void) +{ + separator("TC38: FULL LIFECYCLE β€” No sleeps between calls"); + + FirmwareInterfaceHandle h = registerProcess("KWIB_NoSleep", LIB_VERSION); + if (!h) { TEST_FAIL("TC38 β€” register", "NULL"); return; } + TEST_PASS("TC38 β€” register"); + + /* Check */ + reset_all(); + if (checkForUpdate(h, check_callback) != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC38 β€” check", "FAIL"); unregisterProcess(h); return; + } + wait_flag(&g_check_cb_fired, 130); + TEST_PASS("TC38 β€” check done"); + + /* Immediately download (no sleep) */ + reset_all(); + FwDwnlReq dreq = make_dwnl_req(); + DownloadResult dr = downloadFirmware(h, &dreq, download_callback); + if (dr == RDKFW_DWNL_SUCCESS) { + TEST_PASS("TC38 β€” download accepted immediately after check"); + wait_flag(&g_dwnl_cb_terminal, 600); + } else { + TEST_FAIL("TC38 β€” download", "Rejected (check worker may not have exited yet)"); + } + + /* Immediately update (no sleep) */ + reset_all(); + FwUpdateReq ureq = make_update_req(); + UpdateResult ur = updateFirmware(h, &ureq, update_callback); + if (ur == RDKFW_UPDATE_SUCCESS) { + TEST_PASS("TC38 β€” update accepted immediately after download"); + wait_flag(&g_update_cb_terminal, 600); + } else { + TEST_FAIL("TC38 β€” update", "Rejected (download worker may not have exited yet)"); + } + + unregisterProcess(h); + TEST_PASS("TC38 β€” lifecycle complete"); +} + +static void tc39_check_and_download_simultaneous(void) +{ + separator("TC39: Simultaneous Check + Download (different guards)"); + reset_all(); + if (!ensure_registered()) { TEST_SKIP("TC39", "No handle"); return; } + + /* Start check */ + CheckForUpdateResult cr = checkForUpdate(g_handle, check_callback); + if (cr != CHECK_FOR_UPDATE_SUCCESS) { + TEST_FAIL("TC39 β€” check", "FAIL"); return; + } + + /* Immediately start download (different guard β€” should succeed) */ + FwDwnlReq dreq = make_dwnl_req(); + DownloadResult dr = downloadFirmware(g_handle, &dreq, download_callback); + if (dr == RDKFW_DWNL_SUCCESS) + TEST_PASS("TC39 β€” download accepted while check is active (independent guards)"); + else + TEST_FAIL("TC39 β€” download", "Rejected while check active (guards may be coupled?)"); + + /* Wait for both to finish */ + wait_flag(&g_check_cb_fired, 130); + wait_flag(&g_dwnl_cb_terminal, 600); +} + +/* ======================================================================== + * AUTOMATED SUITE RUNNERS + * ======================================================================== */ + +static void run_error_tests(void) +{ + separator("ERROR TESTS β€” INPUT VALIDATION (library-level, fast)"); + + tc03_unregister_null(); + tc06_check_null_handle(); + tc08_check_empty_handle(); + tc13_download_null_handle(); + tc22_download_empty_handle(); + tc24_update_null_handle(); + tc33_update_empty_handle(); + + separator("ERROR TESTS β€” WITH REGISTRATION (daemon needed)"); + + if (!ensure_registered()) { + TEST_INFO("Cannot continue β€” registration failed (daemon down?)"); + return; + } + + tc07_check_null_callback(); + tc14_download_null_request(); + tc15_download_null_callback(); + tc16_download_null_firmware_name(); + tc17_download_empty_firmware_name(); + tc25_update_null_request(); + tc26_update_null_callback(); + tc27_update_null_firmware_name(); + tc28_update_empty_firmware_name(); + + separator("GUARD TESTS β€” DUPLICATE CALLS"); + + tc09_check_duplicate(); + sleep(2); + tc18_download_duplicate(); + sleep(2); + tc29_update_duplicate(); + sleep(2); + + separator("GUARD TESTS β€” UNREGISTER DURING OPERATION"); + + tc34_unreg_during_check(); + sleep(2); + tc35_unreg_during_download(); + sleep(2); + tc36_unreg_during_update(); + sleep(2); + + ensure_unregistered(); +} + +static void run_happy_tests(void) +{ + separator("HAPPY PATH TESTS (daemon required)"); + + tc01_register_happy(); + if (!g_handle) { TEST_INFO("Cannot continue β€” register failed"); return; } + + tc04_double_register(); + + tc05_check_happy(); + sleep(2); + tc11_check_callback_data(); + sleep(2); + tc10_check_rapid_retry(); + sleep(2); + + tc12_download_happy(); + tc20_download_progress_mono(); + tc21_download_terminal(); + sleep(2); + tc19_download_rapid_retry(); + sleep(2); + + tc23_update_happy(); + tc31_update_progress_mono(); + tc32_update_terminal(); + sleep(2); + tc30_update_rapid_retry(); + sleep(2); + + tc02_unregister_happy(); +} + +static void run_lifecycle_tests(void) +{ + tc37_full_lifecycle(); + sleep(2); + tc38_lifecycle_no_sleeps(); + sleep(2); + tc39_check_and_download_simultaneous(); + sleep(2); + ensure_unregistered(); +} + +static void print_results(void) +{ + printf("\n"); + printf("══════════════════════════════════════════════════════════════\n"); + printf(" KnowWhereItBreaks β€” TEST RESULTS\n"); + printf("══════════════════════════════════════════════════════════════\n"); + printf(" Total: %d\n", g_results.total); + printf(" \033[32mPassed: %d\033[0m\n", g_results.passed); + printf(" \033[31mFailed: %d\033[0m\n", g_results.failed); + printf(" \033[33mSkipped: %d\033[0m\n", g_results.skipped); + printf("══════════════════════════════════════════════════════════════\n"); + if (g_results.failed == 0) + printf(" \033[32mβœ… ALL TESTS PASSED\033[0m\n"); + else + printf(" \033[31m❌ %d TEST(S) FAILED\033[0m\n", g_results.failed); + printf("══════════════════════════════════════════════════════════════\n\n"); +} + +/* ======================================================================== + * INTERACTIVE MENU + * ======================================================================== */ + +static void print_menu(void) +{ + printf("\n"); + printf("β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”\n"); + printf("β”‚ KnowWhereItBreaks v1.0 β€” librdkFwupdateMgr Test Utility β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ Handle: %-49sβ”‚\n", g_handle ? g_handle : "(not registered)"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ AUTOMATED SUITES β”‚\n"); + printf("β”‚ 10 All Error/Validation Tests (fast) β”‚\n"); + printf("β”‚ 11 All Happy Path Tests (daemon needed) β”‚\n"); + printf("β”‚ 12 Full Lifecycle Tests (daemon needed) β”‚\n"); + printf("β”‚ 13 ALL Tests (everything) β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ REGISTER / UNREGISTER CHECKFORUPDATE β”‚\n"); + printf("β”‚ 1 TC01 Register happy 5 TC05 Check happy β”‚\n"); + printf("β”‚ 2 TC02 Unregister happy 6 TC06 NULL handle β”‚\n"); + printf("β”‚ 3 TC03 Unregister NULL 7 TC07 NULL callback β”‚\n"); + printf("β”‚ 4 TC04 Double register 8 TC08 Empty handle β”‚\n"); + printf("β”‚ 9 TC09 Duplicate β”‚\n"); + printf("β”‚ 40 TC10 Rapid retry β”‚\n"); + printf("β”‚ 41 TC11 Callback data β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ DOWNLOAD FIRMWARE UPDATE FIRMWARE β”‚\n"); + printf("β”‚ 50 TC12 Download happy 60 TC23 Update happy β”‚\n"); + printf("β”‚ 51 TC13 NULL handle 61 TC24 NULL handle β”‚\n"); + printf("β”‚ 52 TC14 NULL request 62 TC25 NULL request β”‚\n"); + printf("β”‚ 53 TC15 NULL callback 63 TC26 NULL callback β”‚\n"); + printf("β”‚ 54 TC16 NULL fw name 64 TC27 NULL fw name β”‚\n"); + printf("β”‚ 55 TC17 Empty fw name 65 TC28 Empty fw name β”‚\n"); + printf("β”‚ 56 TC18 Duplicate 66 TC29 Duplicate β”‚\n"); + printf("β”‚ 57 TC19 Rapid retry 67 TC30 Rapid retry β”‚\n"); + printf("β”‚ 58 TC20 Progress mono 68 TC31 Progress mono β”‚\n"); + printf("β”‚ 59 TC21 Terminal status 69 TC32 Terminal status β”‚\n"); + printf("β”‚ 70 TC33 Empty handle β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ GUARDS / LIFECYCLE β”‚\n"); + printf("β”‚ 80 TC34 Unreg during check 90 TC37 Full lifecycle β”‚\n"); + printf("β”‚ 81 TC35 Unreg during download 91 TC38 No-sleep lifecy β”‚\n"); + printf("β”‚ 82 TC36 Unreg during update 92 TC39 Check+Dwnl sim β”‚\n"); + printf("β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€\n"); + printf("β”‚ 0 Exit (print results) β”‚\n"); + printf("β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜\n"); + printf(" Choice: "); +} + +/* ======================================================================== + * MAIN + * ======================================================================== */ + +int main(int argc, char *argv[]) +{ + printf("\n"); + printf("╔══════════════════════════════════════════════════════════════╗\n"); + printf("β•‘ KnowWhereItBreaks v1.0 β•‘\n"); + printf("β•‘ Comprehensive Test Utility for librdkFwupdateMgr.so β•‘\n"); + printf("β•‘ Build: %s %s β•‘\n", __DATE__, __TIME__); + printf("β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•\n"); + + /* Automated modes (for CI/scripts) */ + if (argc > 1) { + if (strcmp(argv[1], "--auto-error") == 0) { + run_error_tests(); print_results(); + return g_results.failed > 0 ? 1 : 0; + } + if (strcmp(argv[1], "--auto-happy") == 0) { + run_happy_tests(); print_results(); + return g_results.failed > 0 ? 1 : 0; + } + if (strcmp(argv[1], "--full-lifecycle") == 0) { + run_lifecycle_tests(); print_results(); + return g_results.failed > 0 ? 1 : 0; + } + if (strcmp(argv[1], "--auto-all") == 0) { + run_error_tests(); + sleep(2); g_handle = NULL; + run_happy_tests(); + sleep(2); + run_lifecycle_tests(); + print_results(); + return g_results.failed > 0 ? 1 : 0; + } + printf("Unknown option: %s\n", argv[1]); + printf("Usage: %s [--auto-error|--auto-happy|--full-lifecycle|--auto-all]\n", argv[0]); + return 1; + } + + /* Interactive mode */ + int choice; + char line[32]; + + while (1) { + print_menu(); + if (!fgets(line, sizeof(line), stdin)) break; + choice = atoi(line); + + switch (choice) { + case 0: + ensure_unregistered(); + print_results(); + return g_results.failed > 0 ? 1 : 0; + + /* Register / Unregister */ + case 1: tc01_register_happy(); break; + case 2: tc02_unregister_happy(); break; + case 3: tc03_unregister_null(); break; + case 4: tc04_double_register(); break; + + /* CheckForUpdate */ + case 5: tc05_check_happy(); break; + case 6: tc06_check_null_handle(); break; + case 7: tc07_check_null_callback(); break; + case 8: tc08_check_empty_handle(); break; + case 9: tc09_check_duplicate(); break; + case 40: tc10_check_rapid_retry(); break; + case 41: tc11_check_callback_data(); break; + + /* Download */ + case 50: tc12_download_happy(); break; + case 51: tc13_download_null_handle(); break; + case 52: tc14_download_null_request(); break; + case 53: tc15_download_null_callback(); break; + case 54: tc16_download_null_firmware_name(); break; + case 55: tc17_download_empty_firmware_name(); break; + case 56: tc18_download_duplicate(); break; + case 57: tc19_download_rapid_retry(); break; + case 58: tc20_download_progress_mono(); break; + case 59: tc21_download_terminal(); break; + + /* Update */ + case 60: tc23_update_happy(); break; + case 61: tc24_update_null_handle(); break; + case 62: tc25_update_null_request(); break; + case 63: tc26_update_null_callback(); break; + case 64: tc27_update_null_firmware_name(); break; + case 65: tc28_update_empty_firmware_name(); break; + case 66: tc29_update_duplicate(); break; + case 67: tc30_update_rapid_retry(); break; + case 68: tc31_update_progress_mono(); break; + case 69: tc32_update_terminal(); break; + case 70: tc33_update_empty_handle(); break; + + /* Guards / Lifecycle */ + case 80: tc34_unreg_during_check(); break; + case 81: tc35_unreg_during_download(); break; + case 82: tc36_unreg_during_update(); break; + case 90: tc37_full_lifecycle(); break; + case 91: tc38_lifecycle_no_sleeps(); break; + case 92: tc39_check_and_download_simultaneous(); break; + + /* Automated suites */ + case 10: run_error_tests(); print_results(); break; + case 11: run_happy_tests(); print_results(); break; + case 12: run_lifecycle_tests(); print_results(); break; + case 13: + run_error_tests(); sleep(2); g_handle = NULL; + run_happy_tests(); sleep(2); + run_lifecycle_tests(); + print_results(); + break; + + default: printf(" Invalid choice.\n"); break; + } + } + + return 0; +} 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_process.c b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c index 8e3c27ae..95013e57 100755 --- a/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c +++ b/librdkFwupdateMgr/src/rdkFwupdateMgr_process.c @@ -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: @@ -297,15 +298,15 @@ FirmwareInterfaceHandle registerProcess(const char *processName, const char *lib "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);