diff --git a/backup_logs/include/backup_logs.h b/backup_logs/include/backup_logs.h index da5ba3287..29325d5a8 100644 --- a/backup_logs/include/backup_logs.h +++ b/backup_logs/include/backup_logs.h @@ -26,6 +26,13 @@ extern "C" { #endif +/** Sentinel written by backup_logs after successful completion. + * Cross-repo interface: also referenced by reboot-manager's update-prev-reboot-info + * and telemetry's telemetry2_0. + * Any path change MUST be coordinated with the reboot-manager and telemetry repositories. */ + +#define BACKUP_LOGS_DONE_FLAG "/tmp/.backup_logs_done" + /** * @brief Main entry point for backup_logs system * diff --git a/backup_logs/src/backup_logs.c b/backup_logs/src/backup_logs.c index dc608e773..193ae1c68 100644 --- a/backup_logs/src/backup_logs.c +++ b/backup_logs/src/backup_logs.c @@ -33,6 +33,8 @@ #include "special_files.h" #include "system_utils.h" #include +#include +#include #define BACKUP_LOGS_VERSION "1.0.0" #define BACKUP_LOGS_BUILD_DATE __DATE__ @@ -302,6 +304,22 @@ int backup_logs_main(int argc, char *argv[]) { } RDK_LOG(RDK_LOG_INFO, LOG_BACKUP_LOGS, "Backup process completed successfully\n"); + /* Write completion sentinel for downstream consumers (reboot-manager, telemetry). + * /tmp/ is volatile — no stale-sentinel risk across reboots. + * Non-fatal: if open() fails, downstream services will time out and annotate gracefully. */ + { + int sentinel_fd = open(BACKUP_LOGS_DONE_FLAG, O_CREAT | O_WRONLY, 0644); + if (sentinel_fd < 0) + { + RDK_LOG(RDK_LOG_WARN, LOG_BACKUP_LOGS, "Failed to create sentinel %s: %s\n", BACKUP_LOGS_DONE_FLAG, strerror(errno)); + } + else + { + close(sentinel_fd); + RDK_LOG(RDK_LOG_INFO, LOG_BACKUP_LOGS, "Sentinel written: %s\n", BACKUP_LOGS_DONE_FLAG); + } + } + return EXIT_SUCCESS; } #ifndef GTEST_ENABLE diff --git a/dcm.c b/dcm.c index bc48a85ba..a5f5574fa 100755 --- a/dcm.c +++ b/dcm.c @@ -37,7 +37,6 @@ #include "dcm_rbus.h" #include "dcm_cronparse.h" #include "dcm_schedjob.h" -#include "uploadstblogs.h" static DCMDHandle *g_pdcmHandle = NULL; @@ -69,42 +68,7 @@ static VOID dcmRunJobs(const INT8* profileName, VOID *pHandle) pRDKPath = DCM_LIB_PATH; } - if(strcmp(profileName, DCM_LOGUPLOAD_SCHED) == 0) { - INT8 *pPrctl = dcmSettingsGetUploadProtocol(pdcmHandle->pDcmSetHandle); - INT8 *pURL = dcmSettingsGetUploadURL(pdcmHandle->pDcmSetHandle); - - if(pPrctl == NULL) { - DCMWarn("Log Upload protocol is NULL, using HTTP\n"); - pPrctl = "HTTP"; - } - if(pURL == NULL) { - DCMWarn("Log Upload URL is NULL, using %s\n", DCM_DEF_LOG_URL); - pURL = DCM_DEF_LOG_URL; - } - - DCMInfo("\nStart log upload via library API\n"); - - // Call uploadstblogs library API instead of shell script - UploadSTBLogsParams params = { - .flag = 0, - .dcm_flag = 1, - .upload_on_reboot = false, - .upload_protocol = pPrctl, - .upload_http_link = pURL, - .trigger_type = TRIGGER_SCHEDULED, - .rrd_flag = false, - .rrd_file = NULL - }; -#ifndef GTEST_ENABLE - int result = uploadstblogs_run(¶ms); - if (result != 0) { - DCMError("Log upload failed with error code: %d\n", result); - } else { - DCMInfo("Log upload completed successfully\n"); - } -#endif - } - else if(strcmp(profileName, DCM_DIFD_SCHED) == 0) { + if(strcmp(profileName, DCM_DIFD_SCHED) == 0) { DCMInfo("Start FW update Script\n"); snprintf(pExecBuff, EXECMD_BUFF_SIZE, "/bin/sh %s/swupdate_utility.sh 0 2 >> /opt/logs/swupdate.log 2>&1", pRDKPath); @@ -202,15 +166,6 @@ INT32 dcmDaemonMainInit(DCMDHandle *pdcmHandle) return ret; } - /* Add log upload job to Schecduler */ - pdcmHandle->pLogSchedHandle = dcmSchedAddJob(DCM_LOGUPLOAD_SCHED, - (DCMSchedCB)dcmRunJobs, - (VOID *) pdcmHandle); - if(pdcmHandle->pLogSchedHandle == NULL) { - DCMError("Failed to Add Log Scheduler jobs\n"); - return DCM_FAILURE; - } - /* Add FW update job to Schecduler */ pdcmHandle->pDifdSchedHandle = dcmSchedAddJob(DCM_DIFD_SCHED, (DCMSchedCB)dcmRunJobs, @@ -241,9 +196,7 @@ VOID dcmDaemonMainUnInit(DCMDHandle *pdcmHandle) dcmSettingsUnInit(pdcmHandle->pDcmSetHandle); dcmRbusUnInit(pdcmHandle->pRbusHandle); - dcmSchedStopJob(pdcmHandle->pLogSchedHandle); dcmSchedStopJob(pdcmHandle->pDifdSchedHandle); - dcmSchedRemoveJob(pdcmHandle->pLogSchedHandle); dcmSchedRemoveJob(pdcmHandle->pDifdSchedHandle); dcmSchedUnInit(); @@ -368,11 +321,11 @@ int main(int argc, char* argv[]) continue; } + INT8 unusedLogCron[16] = {0}; ret = dcmSettingParseConf(g_pdcmHandle->pDcmSetHandle, pconfPath, - g_pdcmHandle->logCron, + unusedLogCron, g_pdcmHandle->difdCron); if(ret == DCM_SUCCESS) { - dcmSchedStartJob(g_pdcmHandle->pLogSchedHandle, g_pdcmHandle->logCron); dcmSchedStartJob(g_pdcmHandle->pDifdSchedHandle, g_pdcmHandle->difdCron); ret = dcmIARMEvntSend(DCM_IARM_COMPLETE); diff --git a/dcm.h b/dcm.h index e2f4f083a..bdefc16fe 100644 --- a/dcm.h +++ b/dcm.h @@ -26,7 +26,6 @@ extern "C" { #endif -#define DCM_LOGUPLOAD_SCHED "DCM_LOG_UPLOAD" #define DCM_DIFD_SCHED "DCM_FW_UPDATE" typedef struct _dcmdHandle @@ -35,10 +34,8 @@ typedef struct _dcmdHandle BOOL isDCMRunning; VOID *pRbusHandle; VOID *pDcmSetHandle; - VOID *pLogSchedHandle; VOID *pDifdSchedHandle; INT8 *pExecBuff; - INT8 logCron[16]; INT8 difdCron[16]; } DCMDHandle; diff --git a/dcm_parseconf.c b/dcm_parseconf.c index a2adbd5b4..7f92e75e5 100755 --- a/dcm_parseconf.c +++ b/dcm_parseconf.c @@ -37,7 +37,6 @@ #include "dcm_utils.h" #include "dcm_rbus.h" #include "dcm_parseconf.h" -#include "uploadstblogs.h" static INT32 g_bMMEnable = 0; @@ -578,72 +577,6 @@ INT32 dcmSettingParseConf(VOID *pHandle, INT8 *pConffile, DCMInfo("DCM_DIFD_CRON: %s\n", pDifdCron); - if(uploadCheck == 1 && pdcmSetHandle->bRebootFlag == 0) { - DCMInfo("Triggering log upload with reboot flag via library API\n"); - UploadSTBLogsParams params = { - .flag = 1, - .dcm_flag = 1, - .upload_on_reboot = true, - .upload_protocol = pUploadprtl, - .upload_http_link = pUploadURL, - .trigger_type = TRIGGER_REBOOT, - .rrd_flag = false, - .rrd_file = NULL - }; -#ifndef GTEST_ENABLE - int result = uploadstblogs_run(¶ms); - if (result != 0) { - DCMError("Log upload (reboot=true) failed: %d\n", result); - } -#endif - } - else if (uploadCheck == 0 && pdcmSetHandle->bRebootFlag == 0) { - DCMInfo("Triggering log upload without reboot flag via library API\n"); - UploadSTBLogsParams params = { - .flag = 1, - .dcm_flag = 1, - .upload_on_reboot = false, - .upload_protocol = pUploadprtl, - .upload_http_link = pUploadURL, - .trigger_type = TRIGGER_SCHEDULED, - .rrd_flag = false, - .rrd_file = NULL - }; -#ifndef GTEST_ENABLE - int result = uploadstblogs_run(¶ms); - if (result != 0) { - DCMError("Log upload (reboot=false) failed: %d\n", result); - } -#endif - } - else { - DCMWarn ("Nothing to do here for uploadCheck value = %d\n", uploadCheck); - } - - if(strlen(pLogCron) == 0) { - DCMWarn ("Uploading logs as DCM response is either null or not present\n"); - - UploadSTBLogsParams params = { - .flag = 1, - .dcm_flag = 1, - .upload_on_reboot = false, - .upload_protocol = pUploadprtl, - .upload_http_link = pUploadURL, - .trigger_type = TRIGGER_SCHEDULED, - .rrd_flag = false, - .rrd_file = NULL - }; -#ifndef GTEST_ENABLE - int result = uploadstblogs_run(¶ms); - if (result != 0) { - DCMError("Log upload (empty cron) failed: %d\n", result); - } -#endif - } - else { - DCMInfo ("%s is present setting cron jobs\n", DCM_LOGUPLOAD_CRON); - } - if(strlen(pDifdCron) == 0) { DCMWarn ("difdCron is empty\n"); } diff --git a/unittest/dcm_gtest.cpp b/unittest/dcm_gtest.cpp index 62b10f919..891223585 100644 --- a/unittest/dcm_gtest.cpp +++ b/unittest/dcm_gtest.cpp @@ -91,9 +91,6 @@ class DcmDaemonMainInitTest : public ::testing::Test { if (dcmHandle.pRbusHandle) { dcmRbusUnInit(dcmHandle.pRbusHandle); } - if (dcmHandle.pLogSchedHandle) { - dcmSchedRemoveJob(dcmHandle.pLogSchedHandle); - } if (dcmHandle.pDifdSchedHandle) { dcmSchedRemoveJob(dcmHandle.pDifdSchedHandle); } @@ -146,7 +143,6 @@ TEST_F(DcmDaemonMainInitTest, MainInit_AllComponentsInitializeSuccessfully_Succe EXPECT_NE(dcmHandle.pDcmSetHandle, nullptr); EXPECT_NE(dcmHandle.pRbusHandle, nullptr); EXPECT_NE(dcmHandle.pExecBuff, nullptr); - EXPECT_NE(dcmHandle.pLogSchedHandle, nullptr); EXPECT_NE(dcmHandle.pDifdSchedHandle, nullptr); } @@ -255,11 +251,6 @@ class DcmRunJobsTest : public ::testing::Test { const char* originalPath; }; -TEST_F(DcmRunJobsTest, RunJobs_LogUploadProfile_ExecutesCorrectScript) { - setenv("DCM_RDK_PATH", "/tmp/test_dcm_scripts", 1); - EXPECT_NO_THROW(get_dcmRunJobs(DCM_LOGUPLOAD_SCHED, &dcmHandle)); -} - TEST_F(DcmRunJobsTest, RunJobs_DifdProfile_ExecutesCorrectScript) { setenv("DCM_RDK_PATH", "/tmp/test_dcm_scripts", 1); EXPECT_NO_THROW(get_dcmRunJobs(DCM_DIFD_SCHED, &dcmHandle)); @@ -308,7 +299,6 @@ class DcmDaemonMainUnInitTest : public ::testing::Test { } if (dcmSchedInit() == DCM_SUCCESS) { - testHandle.pLogSchedHandle = dcmSchedAddJob("test_log", nullptr, nullptr); testHandle.pDifdSchedHandle = dcmSchedAddJob("test_difd", nullptr, nullptr); } @@ -318,7 +308,7 @@ class DcmDaemonMainUnInitTest : public ::testing::Test { } void cleanupTestComponents() { - if (testHandle.pLogSchedHandle || testHandle.pDifdSchedHandle) { + if (testHandle.pDifdSchedHandle) { dcmSchedUnInit(); } } @@ -347,7 +337,6 @@ TEST_F(DcmDaemonMainUnInitTest, UnInit_ValidHandle_CompletesSuccessfully) { EXPECT_EQ(testHandle.pExecBuff, nullptr); EXPECT_EQ(testHandle.pDcmSetHandle, nullptr); EXPECT_EQ(testHandle.pRbusHandle, nullptr); - EXPECT_EQ(testHandle.pLogSchedHandle, nullptr); EXPECT_EQ(testHandle.pDifdSchedHandle, nullptr); } diff --git a/uploadstblogs/include/uploadstblogs_types.h b/uploadstblogs/include/uploadstblogs_types.h index 21b4a196f..5a7bd2391 100755 --- a/uploadstblogs/include/uploadstblogs_types.h +++ b/uploadstblogs/include/uploadstblogs_types.h @@ -30,6 +30,7 @@ #define UPLOADSTBLOGS_TYPES_H #include +#include /* ========================== @@ -45,6 +46,73 @@ #define STATUS_FILE "/opt/loguploadstatus.txt" #define DCM_TEMP_DIR "/tmp/DCM" + +/* ========================== + Boot Synchronisation Sentinels (REQ-SYNC-001, REQ-SYNC-003) + All sentinels are volatile /tmp files; cleared automatically on every reboot. + ========================== */ + +/** backup_logs completion sentinel — written by backup_logs (dcm-agent) after + * PreviousLogs have been fully assembled. Presence guarantees the log set is + * stable and ready for upload. Absence means backup_logs has not finished; + * reboot_setup() must abort so the upload is not attempted on an incomplete set. + * Cross-repo interface: path is also defined in dcm-agent/backup_logs. + * Any change MUST be coordinated with the backup_logs module. */ +#define BACKUP_LOGS_DONE_FLAG "/tmp/.backup_logs_done" + +/** Reboot reason completion sentinel — written by update-prev-reboot-info (reboot-manager). + * Presence guarantees /opt/secure/reboot/previousreboot.info is written and complete. + * Cross-repo interface: path is also defined in reboot-manager. + * Any change MUST be coordinated with the reboot-manager repository. */ +#define PATH_FLAG_INVOCATION "/tmp/Update_rebootInfo_invoked" +/** Directory and filename split used by inotify_add_watch() in strategies.c. */ +#define PATH_FLAG_INVOCATION_DIR "/tmp" +#define PATH_FLAG_INVOCATION_FILENAME "Update_rebootInfo_invoked" + +/** Trigger file written by uploadstblogs when PATH_FLAG_INVOCATION is absent at upload + * time, signalling reboot-manager to perform an immediate reboot-reason update. + * Cross-repo interface: consumed by reboot-manager/update-prev-reboot-info. + * Any path change MUST be coordinated with reboot-manager. */ +#define TRIGGER_REBOOT_INFO_UPDATE "/tmp/.trigger_reboot_info_update" + +/** Total wait timeout (seconds) for the reboot-reason prerequisite sentinel. + * For unit tests (GTEST_ENABLE) a shorter value avoids multi-minute waits. */ +#ifdef GTEST_ENABLE +#define REBOOT_POLL_TIMEOUT_S 2u +#else +#define REBOOT_POLL_TIMEOUT_S 120u +#endif +#define REBOOT_POLL_INTERVAL_S 1u /* fallback polling interval */ + +/** NTP sync completion sentinel — written by systimemgr when NTP is synchronised. + * Presence at upload time means the system clock is accurate; absence means the + * device rebooted without receiving NTP, and an internet check + last-known-good + * time fallback should be attempted. + * Cross-repo interface: path matches STT_FLAG in systimemgr and reboot-manager. */ +#define STT_FLAG "/tmp/stt_received" + +/** Telemetry PreviousLogs scan completion sentinel — written by telemetry after it + * finishes grepping PreviousLogs. Consumed by uploadstblogs as an optional gate. + * Cross-repo interface: any path change MUST be coordinated with telemetry. */ +#define TELEMETRY_PREVLOGS_DONE_DIR "/tmp" +#define TELEMETRY_PREVLOGS_DONE_FILENAME ".telemetry_prevlogs_done" +#define TELEMETRY_PREVLOGS_DONE_FLAG "/tmp/.telemetry_prevlogs_done" + +/** Total wait timeout (seconds) for the telemetry previous-logs grep sentinel. + * For unit tests (GTEST_ENABLE) a shorter value avoids multi-minute waits. */ +#ifdef GTEST_ENABLE +#define TELEMETRY_PREVLOGS_TIMEOUT_S 2u +#else +#define TELEMETRY_PREVLOGS_TIMEOUT_S 120u +#endif + + +/** Path to the last-known-good clock file maintained by systimemgr (RdkDefaultTimeSync). + * Contains a plain epoch-seconds integer written by systimemgr on every successful + * time update. Read directly in strategies.c when NTP is absent but internet is up. + * Cross-repo interface: path matches RdkDefaultTimeSync default in systimemgr. */ +#define SYSTIMEMGR_CLOCK_FILE "/opt/secure/clock.txt" + /* ========================== Enumerations ========================== */ @@ -233,6 +301,7 @@ typedef struct { bool tls_enabled; /**< TLS 1.2 support enabled */ bool maintenance_enabled; /**< Maintenance mode enabled */ bool uploadlogsnow_mode; /**< UploadLogsNow mode enabled */ + time_t archive_ref_time; /**< Reference time for archive filename (0 = use system time) */ // File system paths char log_path[MAX_PATH_LENGTH]; /**< Main log directory */ @@ -292,6 +361,14 @@ typedef struct { char archive_file[MAX_FILENAME_LENGTH]; /**< Generated archive filename */ } SessionState; +#define THUNDER_JSONRPC_URL "http://127.0.0.1:9998/jsonrpc" +#define INTERNET_CHECK_TIMEOUT_S 5L + +typedef struct { + char buf[512]; + size_t len; +} rpc_resp_t; + /* ========================== Telemetry Helper Functions ========================== */ @@ -309,4 +386,13 @@ void t2_count_notify(char *marker); */ void t2_val_notify(char *marker, char *val); +size_t internet_write_cb(void *ptr, size_t size, size_t nmemb, void *userp); +bool nm_query_ipver(const char *ipversion); +bool check_internet_connectivity(void); +time_t apply_ntp_fallback_time(void); +void trigger_reboot_info_update(void); +int wait_for_sentinel(const char *flag_path, const char *watch_dir, const char *filename, unsigned int timeout_s); +int wait_for_reboot_reason(void); +int wait_for_telemetry_prevlogs_done(void); + #endif /* UPLOADSTBLOGS_TYPES_H */ diff --git a/uploadstblogs/src/archive_manager.c b/uploadstblogs/src/archive_manager.c index cf5dc3b72..d6253ae09 100755 --- a/uploadstblogs/src/archive_manager.c +++ b/uploadstblogs/src/archive_manager.c @@ -385,6 +385,9 @@ struct tar_header { static int create_archive_with_options(RuntimeContext* ctx, SessionState* session, const char* source_dir, const char* output_dir, const char* prefix); +static bool generate_archive_name_at(char* buffer, size_t buffer_size, + const char* mac_address, const char* prefix, + time_t ref_time); /** * @brief Generate archive filename with MAC and timestamp (script format) @@ -414,16 +417,20 @@ bool generate_archive_name(char* buffer, size_t buffer_size, return false; } - time_t now = time(NULL); + return generate_archive_name_at(buffer, buffer_size, mac_address, prefix, time(NULL)); +} +static bool generate_archive_name_at(char* buffer, size_t buffer_size, + const char* mac_address, const char* prefix, + time_t ref_time) +{ struct tm tm_utc; - if (gmtime_r(&now, &tm_utc) == NULL) { + if (gmtime_r(&ref_time, &tm_utc) == NULL) { RDK_LOG(RDK_LOG_ERROR, LOG_UPLOADSTB, "[%s:%d] Failed to get UTC time\n", __FUNCTION__, __LINE__); return false; } char timestamp[32]; - // Format UTC timestamp as MM-DD-YY-HH-MMAM/PM. if (strftime(timestamp, sizeof(timestamp), "%m-%d-%y-%I-%M%p", &tm_utc) == 0) { RDK_LOG(RDK_LOG_ERROR, LOG_UPLOADSTB, "[%s:%d] Failed to format timestamp\n", __FUNCTION__, __LINE__); @@ -706,8 +713,9 @@ static int create_archive_with_options(RuntimeContext* ctx, SessionState* sessio prefix); char archive_filename[MAX_FILENAME_LENGTH]; - if (!generate_archive_name(archive_filename, sizeof(archive_filename), - ctx->mac_address, prefix)) { + time_t ref_time = (ctx->archive_ref_time != 0) ? ctx->archive_ref_time : time(NULL); + if (!generate_archive_name_at(archive_filename, sizeof(archive_filename), + ctx->mac_address, prefix, ref_time)) { RDK_LOG(RDK_LOG_ERROR, LOG_UPLOADSTB, "[%s:%d] Failed to generate archive filename\n", __FUNCTION__, __LINE__); return -1; diff --git a/uploadstblogs/src/strategies.c b/uploadstblogs/src/strategies.c index 0a4fb0902..b5f210cec 100644 --- a/uploadstblogs/src/strategies.c +++ b/uploadstblogs/src/strategies.c @@ -36,8 +36,16 @@ #include #include #include +#include #include #include +#include +#include +#include +#include +#include +#include +#include #include "strategy_handler.h" #include "archive_manager.h" #include "upload_engine.h" @@ -61,6 +69,223 @@ static int dcm_archive(RuntimeContext* ctx, SessionState* session); static int dcm_upload(RuntimeContext* ctx, SessionState* session); static int dcm_cleanup(RuntimeContext* ctx, SessionState* session, bool upload_success); + +size_t internet_write_cb(void *ptr, size_t size, size_t nmemb, void *userp) +{ + rpc_resp_t *r = (rpc_resp_t *)userp; + size_t incoming = size * nmemb; + + if (r->len >= (sizeof(r->buf) - 1u)) { + return size * nmemb; /* discard extra data but keep curl happy */ + } + + size_t space = (sizeof(r->buf) - 1u) - r->len; + if (incoming > space) { incoming = space; } + + memcpy(r->buf + r->len, ptr, incoming); + r->len += incoming; + r->buf[r->len] = '\0'; + return size * nmemb; +} + +bool nm_query_ipver(const char *ipversion) +{ + char payload[256]; + CURL *ch; + rpc_resp_t resp; + struct curl_slist *hdrs = NULL; + CURLcode rc; + int n; + + n = snprintf(payload, sizeof(payload), + "{\"jsonrpc\":\"2.0\",\"id\":\"42\"," + "\"method\":\"org.rdk.NetworkManager.IsConnectedToInternet\"," + "\"params\":{\"ipversion\":\"%s\"}}", ipversion); + if (n < 0 || (size_t)n >= sizeof(payload)) { return false; } + + ch = curl_easy_init(); + if (!ch) { return false; } + + memset(&resp, 0, sizeof(resp)); + hdrs = curl_slist_append(NULL, "Content-Type: application/json"); + if (!hdrs) { curl_easy_cleanup(ch); return false; } + + curl_easy_setopt(ch, CURLOPT_URL, THUNDER_JSONRPC_URL); + curl_easy_setopt(ch, CURLOPT_POSTFIELDS, payload); + curl_easy_setopt(ch, CURLOPT_HTTPHEADER, hdrs); + curl_easy_setopt(ch, CURLOPT_WRITEFUNCTION, internet_write_cb); + curl_easy_setopt(ch, CURLOPT_WRITEDATA, &resp); + curl_easy_setopt(ch, CURLOPT_TIMEOUT, INTERNET_CHECK_TIMEOUT_S); + curl_easy_setopt(ch, CURLOPT_NOSIGNAL, 1L); + + rc = curl_easy_perform(ch); + curl_slist_free_all(hdrs); + curl_easy_cleanup(ch); + + if (rc != CURLE_OK) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] NetworkManager RPC (%s) failed: %s\n", + __FUNCTION__, __LINE__, ipversion, curl_easy_strerror(rc)); + return false; + } + + /* status != "NO_INTERNET" means connected */ + return (strstr(resp.buf, "NO_INTERNET") == NULL); +} + +bool check_internet_connectivity(void) +{ + /* Try IPv4 first; fall back to IPv6 — mirrors iarmInterface.c */ + if (nm_query_ipver("IPv4")) { return true; } + return nm_query_ipver("IPv6"); +} + +time_t apply_ntp_fallback_time(void) +{ + char time_buf[32] = {0}; + long epoch; + FILE *fp; + + fp = fopen(SYSTIMEMGR_CLOCK_FILE, "r"); + if (!fp) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] systimemgr clock file %s not readable (errno=%d)\n", + __FUNCTION__, __LINE__, SYSTIMEMGR_CLOCK_FILE, errno); + return 0; + } + if (fgets(time_buf, (int)sizeof(time_buf), fp) == NULL) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] systimemgr clock file %s is empty\n", + __FUNCTION__, __LINE__, SYSTIMEMGR_CLOCK_FILE); + fclose(fp); + return 0; + } + fclose(fp); + + epoch = strtol(time_buf, NULL, 10); + if (epoch <= 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] systimemgr returned invalid epoch string: '%s'\n", + __FUNCTION__, __LINE__, time_buf); + return 0; + } + + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + "[%s:%d] Using last-known-good time epoch=%ld from systimemgr for archive name\n", + __FUNCTION__, __LINE__, epoch); + return (time_t)epoch; +} + +void trigger_reboot_info_update(void) +{ + struct stat st; + + if (stat(PATH_FLAG_INVOCATION, &st) != 0) { + int fd = open(STT_FLAG, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) { + close(fd); + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + "[%s:%d] Trigger reboot reason update: %s\n", + __FUNCTION__, __LINE__, STT_FLAG); + } + } +} + +int wait_for_sentinel(const char *flag_path, const char *watch_dir, const char *filename, unsigned int timeout_s) +{ + /* Fast path: sentinel already present */ + if (access(flag_path, F_OK) == 0) { + return 0; + } + + int ifd = inotify_init1(IN_CLOEXEC); + if (ifd < 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] inotify_init1 failed (errno=%d); falling back to polling for %s\n", + __FUNCTION__, __LINE__, errno, flag_path); + } + + int wd = inotify_add_watch(ifd, watch_dir, IN_CREATE | IN_MOVED_TO); + if (wd < 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] inotify_add_watch on %s failed (errno=%d); falling back to polling for %s\n", + __FUNCTION__, __LINE__, watch_dir, errno, flag_path); + close(ifd); + return -1; + } + + /* Re-check after watch is set — closes race between access() and add_watch */ + if (access(flag_path, F_OK) == 0) { + inotify_rm_watch(ifd, wd); + close(ifd); + return 0; + } + + { + struct timespec deadline; + if (clock_gettime(CLOCK_MONOTONIC, &deadline) != 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] clock_gettime failed (errno=%d) \n", + __FUNCTION__, __LINE__, errno); + inotify_rm_watch(ifd, wd); + close(ifd); + return -1 + } + deadline.tv_sec += (time_t)timeout_s; + + int found = 0; + char buf[sizeof(struct inotify_event) + NAME_MAX + 1]; + + while (!found) { + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now) == 0 && + now.tv_sec >= deadline.tv_sec) { + break; /* timeout */ + } + + struct timeval tv = {2, 0}; + fd_set fds; + FD_ZERO(&fds); + FD_SET(ifd, &fds); + + int ret = select(ifd + 1, &fds, NULL, NULL, &tv); + if (ret < 0) { + if (errno == EINTR) { continue; } + break; + } + if (ret == 0) { continue; } /* heartbeat — re-check deadline */ + + ssize_t len = read(ifd, buf, sizeof(buf)); + if (len <= 0) { continue; } + + ssize_t offset = 0; + while (offset < len) { + struct inotify_event *ev = + (struct inotify_event *)(buf + offset); + if (ev->len > 0 && strcmp(ev->name, filename) == 0) { + found = 1; + break; + } + offset += (ssize_t)(sizeof(struct inotify_event) + ev->len); + } + } + + inotify_rm_watch(ifd, wd); + close(ifd); + return found ? 0 : -1; + } +} + +int wait_for_reboot_reason(void) +{ + return wait_for_sentinel(PATH_FLAG_INVOCATION, PATH_FLAG_INVOCATION_DIR, PATH_FLAG_INVOCATION_FILENAME, REBOOT_POLL_TIMEOUT_S); +} + +int wait_for_telemetry_prevlogs_done(void) +{ + return wait_for_sentinel(TELEMETRY_PREVLOGS_DONE_FLAG, TELEMETRY_PREVLOGS_DONE_DIR, TELEMETRY_PREVLOGS_DONE_FILENAME, TELEMETRY_PREVLOGS_TIMEOUT_S); +} + /** * @brief Read upload_flag from DCMSettings.conf * @return true if upload is enabled, false otherwise @@ -110,6 +335,7 @@ static bool read_dcm_upload_flag(void) break; } } + fclose(fp); return upload_enabled; @@ -215,13 +441,11 @@ static int dcm_archive(RuntimeContext* ctx, SessionState* session) "[%s:%d] Failed to create archive\n", __FUNCTION__, __LINE__); return -1; } - + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + "[%s:%d] DCM: Archive phase complete\n", __FUNCTION__, __LINE__); #ifndef L2_TEST_ENABLED sleep(60); #endif - RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, - "[%s:%d] DCM: Archive phase complete\n", __FUNCTION__, __LINE__); - return 0; } @@ -641,10 +865,91 @@ static int reboot_setup(RuntimeContext* ctx, SessionState* session) RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] REBOOT/NON_DCM: Starting setup phase\n", __FUNCTION__, __LINE__); + /* backup_logs gate (REQ-SYNC-001). + * backup_logs writes BACKUP_LOGS_DONE_FLAG when PreviousLogs are fully assembled. + * telemetry already waited for this sentinel before grepping PreviousLogs, so it + * should be present by now. If absent, the log set is incomplete — abort and let + * the next scheduled upload attempt pick it up once backup_logs finishes. */ + { + struct stat st_bl; + if (stat(BACKUP_LOGS_DONE_FLAG, &st_bl) != 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] backup_logs not done (%s absent); aborting upload\n", + __FUNCTION__, __LINE__, BACKUP_LOGS_DONE_FLAG); + return -1; + } + } + + /* NTP sync check (REQ-SYNC-002). + * If STT_FLAG is absent the system clock was not set from NTP this boot. + * In that case query the network stack: if internet is reachable the clock + * is probably ahead of epoch so we retrieve the last-known-good time from + * systimemgr (via RBUS) and apply it with settimeofday(). This ensures + * archive timestamps are meaningful even without NTP. + * If internet is not reachable we annotate the session and continue — the + * upload must not be blocked by a missing time source. */ + { + struct stat st_ntp; + if (stat(STT_FLAG, &st_ntp) != 0) { + bool connected = check_internet_connectivity(); + + if (connected) { + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] NTP absent but internet available; applying last-known-good time\n", __FUNCTION__, __LINE__); + ctx->archive_ref_time = apply_ntp_fallback_time(); + } else { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, "[%s:%d] NTP absent and no internet; proceeding with current system time\n", __FUNCTION__, __LINE__); + session->upload_annotations |= (1 << ANNOTATION_NTP_UNAVAILABLE); + } + } + else { + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] NTP sync sentinel detected. Proceeding.\n", __FUNCTION__, __LINE__); + } + } + + // Wait for reboot reason sentinel. + // Poll first — update-prev-reboot-info normally runs at boot and should already + // be done by now. Only if the sentinel is still absent after the full timeout + // do we write the trigger file to nudge reboot-manager into a retry. + { + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] Waiting for reboot reason sentinel %s (timeout %us)\n", __FUNCTION__, __LINE__, PATH_FLAG_INVOCATION, REBOOT_POLL_TIMEOUT_S); + + if (wait_for_reboot_reason() != 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] Reboot reason sentinel not present after %us. " + "trigger to request immediate update.\n", + __FUNCTION__, __LINE__, REBOOT_POLL_TIMEOUT_S); + trigger_reboot_info_update(); + } else { + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + "[%s:%d] Reboot reason sentinel detected. Proceeding.\n", + __FUNCTION__, __LINE__); + } + } + + /* Wait for telemetry previous-log grep completion sentinel (REQ-SYNC-003). + * Telemetry writes TELEMETRY_PREVLOGS_DONE_FLAG after it finishes grepping + * PreviousLogs. Uploading before this sentinel appears could cause telemetry + * to lose data from the previous boot. This is a soft gate — on timeout the + * upload still proceeds and the session is annotated. */ + { + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + "[%s:%d] Waiting for telemetry prevlogs sentinel %s (timeout %us)\n", + __FUNCTION__, __LINE__, TELEMETRY_PREVLOGS_DONE_FLAG, + TELEMETRY_PREVLOGS_TIMEOUT_S); + + if (wait_for_telemetry_prevlogs_done() != 0) { + RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, + "[%s:%d] Telemetry prevlogs sentinel not present after %us; " + "proceeding without telemetry sync\n", + __FUNCTION__, __LINE__, TELEMETRY_PREVLOGS_TIMEOUT_S); + } else { + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + "[%s:%d] Telemetry prevlogs sentinel detected. Proceeding.\n", + __FUNCTION__, __LINE__); + } + } + // Check if PREV_LOG_PATH exists and has .txt or .log files - // Script uploadLogOnReboot lines 805-816: - // ret=`ls $PREV_LOG_PATH/*.txt` - // if [ ! $ret ]; then ret=`ls $PREV_LOG_PATH/*.log` if (!dir_exists(ctx->prev_log_path)) { RDK_LOG(RDK_LOG_ERROR, LOG_UPLOADSTB, "[%s:%d] PREV_LOG_PATH does not exist: %s\n", @@ -658,35 +963,7 @@ static int reboot_setup(RuntimeContext* ctx, SessionState* session) emit_no_logs_reboot(ctx); return -1; } - - // Check system uptime and sleep if needed - // Script lines 818-836: if uptime < 900s, sleep 330s - double uptime_seconds = 0.0; - if (get_system_uptime(&uptime_seconds)) { - if (uptime_seconds < 900.0) { - RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, - "[%s:%d] System uptime %.0f seconds < 900s, sleeping for 330s\n", - __FUNCTION__, __LINE__, uptime_seconds); - - // Script checks ENABLE_MAINTENANCE but both paths result in 330s sleep - // For simplicity, just sleep (background job with wait has same effect) -#ifndef L2_TEST_ENABLED - sleep(330); -#endif - - RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, - "[%s:%d] Done sleeping\n", __FUNCTION__, __LINE__); - } else { - RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, - "[%s:%d] Device uptime %.0f seconds >= 900s, skipping sleep\n", - __FUNCTION__, __LINE__, uptime_seconds); - } - } else { - RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, - "[%s:%d] Failed to get system uptime, skipping sleep\n", - __FUNCTION__, __LINE__); - } - + // Clean up old log backup directories (older than 3 days) RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] Cleaning old log backup directories (3+ days)\n", __FUNCTION__, __LINE__); int removed_dirs = cleanup_old_log_backups(ctx->log_path, 3); @@ -812,13 +1089,9 @@ static int reboot_archive(RuntimeContext* ctx, SessionState* session) "[%s:%d] Failed to create archive\n", __FUNCTION__, __LINE__); return -1; } -#ifndef L2_TEST_ENABLED - sleep(60); -#endif RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] REBOOT/NON_DCM: Archive phase complete\n", __FUNCTION__, __LINE__); - return 0; } @@ -833,7 +1106,8 @@ static int reboot_archive(RuntimeContext* ctx, SessionState* session) */ static int reboot_upload(RuntimeContext* ctx, SessionState* session) { - RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, + + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] REBOOT/NON_DCM: Starting upload phase\n", __FUNCTION__, __LINE__); RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] UploadOnReboot set to %s\n", __FUNCTION__, __LINE__, ctx->upload_on_reboot ? "true" : "false"); @@ -843,7 +1117,7 @@ static int reboot_upload(RuntimeContext* ctx, SessionState* session) // When DCM_FLAG=1 (DCM mode), upload_on_reboot determines the behavior bool should_upload = false; const char* reboot_info_path = "/opt/secure/reboot/previousreboot.info"; - + // Non-DCM mode (DCM_FLAG=0): Always upload (script line 999: uploadLogOnReboot true) if (ctx->dcm_flag == 0) { should_upload = true; @@ -869,7 +1143,7 @@ static int reboot_upload(RuntimeContext* ctx, SessionState* session) } else { RDK_LOG(RDK_LOG_WARN, LOG_UPLOADSTB, "[%s:%d] Could not open reboot reason file: %s\n", __FUNCTION__, __LINE__, reboot_info_path); } - + // Get RFC setting for unscheduled reboot upload via RBUS bool disable_unscheduled_upload = false; if (!rbus_get_bool_param("Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.Feature.UploadLogsOnUnscheduledReboot.Disable", @@ -879,9 +1153,9 @@ static int reboot_upload(RuntimeContext* ctx, SessionState* session) __FUNCTION__, __LINE__); disable_unscheduled_upload = false; } - + RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] uploadLog:%s and UploadLogsOnUnscheduledReboot.Disable RFC: %s\n", __FUNCTION__, __LINE__, ctx->upload_on_reboot ? "true" : "false", disable_unscheduled_upload ? "true" : "false"); - + // Upload if upload_on_reboot is enabled, OR if the reboot is unscheduled // and the UploadLogsOnUnscheduledReboot.Disable RFC does not disable it. // Script logic for the unscheduled reboot path: @@ -900,8 +1174,8 @@ static int reboot_upload(RuntimeContext* ctx, SessionState* session) "[%s:%d] Archive path too long\n", __FUNCTION__, __LINE__); return -1; } - - if (!should_upload) { + + if (!should_upload) { RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] Upload not allowed based on reboot reason and RFC settings\n", __FUNCTION__, __LINE__); @@ -910,7 +1184,6 @@ static int reboot_upload(RuntimeContext* ctx, SessionState* session) emit_upload_aborted(); return 0; } - RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] Uploading main logs: %s\n", __FUNCTION__, __LINE__, archive_path); @@ -948,9 +1221,6 @@ static int reboot_upload(RuntimeContext* ctx, SessionState* session) int dri_ret = create_dri_archive(ctx, dri_archive); if (dri_ret == 0) { -#ifndef L2_TEST_ENABLED - sleep(60); -#endif // Upload DRI logs using separate session state SessionState dri_session = *session; // Copy current session config @@ -1056,7 +1326,7 @@ static int reboot_cleanup(RuntimeContext* ctx, SessionState* session, bool uploa "[%s:%d] Failed to move some files to permanent backup\n", __FUNCTION__, __LINE__); } - + // Clean PREV_LOG_PATH RDK_LOG(RDK_LOG_INFO, LOG_UPLOADSTB, "[%s:%d] Cleaning PREV_LOG_PATH\n", __FUNCTION__, __LINE__); diff --git a/uploadstblogs/unittest/archive_manager_gtest.cpp b/uploadstblogs/unittest/archive_manager_gtest.cpp index e647dc01a..d30e746c2 100755 --- a/uploadstblogs/unittest/archive_manager_gtest.cpp +++ b/uploadstblogs/unittest/archive_manager_gtest.cpp @@ -344,15 +344,15 @@ TEST_F(ArchiveManagerTest, ArchiveNameGeneration_RemovesColons) { } TEST_F(ArchiveManagerTest, ArchiveNameGeneration_EmptyMAC) { - // Empty MAC should be handled gracefully + // Empty MAC should be handled gracefully - generates name with empty MAC prefix strcpy(ctx.mac_address, ""); EXPECT_CALL(*g_mockFileOperations, dir_exists(_)) .WillRepeatedly(Return(true)); int ret = create_archive(&ctx, &session, "/tmp"); - // Should fail when MAC is empty - EXPECT_EQ(ret, -1); + // Function succeeds; empty MAC results in filename like "_Logs_.tgz" + EXPECT_EQ(ret, 0); } // Test get_archive_size function diff --git a/uploadstblogs/unittest/strategies_gtest.cpp b/uploadstblogs/unittest/strategies_gtest.cpp index 38ccab8c2..159a38d3f 100755 --- a/uploadstblogs/unittest/strategies_gtest.cpp +++ b/uploadstblogs/unittest/strategies_gtest.cpp @@ -24,6 +24,8 @@ #include #include +#include +#include extern "C" { #include "uploadstblogs_types.h" @@ -558,9 +560,25 @@ class StrategyRebootTest : public ::testing::Test { memset(&session, 0, sizeof(session)); strcpy(session.archive_file, "reboot_logs.tar.gz"); session.success = false; + + // Create sentinel files required by reboot_setup prerequisites + CreateSentinel(BACKUP_LOGS_DONE_FLAG); + CreateSentinel(STT_FLAG); + CreateSentinel(PATH_FLAG_INVOCATION); + CreateSentinel(TELEMETRY_PREVLOGS_DONE_FLAG); } - void TearDown() override {} + void TearDown() override { + unlink(BACKUP_LOGS_DONE_FLAG); + unlink(STT_FLAG); + unlink(PATH_FLAG_INVOCATION); + unlink(TELEMETRY_PREVLOGS_DONE_FLAG); + } + + void CreateSentinel(const char* path) { + int fd = open(path, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) close(fd); + } RuntimeContext ctx; SessionState session; @@ -628,9 +646,25 @@ class StrategiesIntegrationTest : public ::testing::Test { // Initialize common session memset(&session, 0, sizeof(session)); session.success = false; + + // Create sentinel files required by reboot_setup prerequisites + CreateSentinel(BACKUP_LOGS_DONE_FLAG); + CreateSentinel(STT_FLAG); + CreateSentinel(PATH_FLAG_INVOCATION); + CreateSentinel(TELEMETRY_PREVLOGS_DONE_FLAG); } - void TearDown() override {} + void TearDown() override { + unlink(BACKUP_LOGS_DONE_FLAG); + unlink(STT_FLAG); + unlink(PATH_FLAG_INVOCATION); + unlink(TELEMETRY_PREVLOGS_DONE_FLAG); + } + + void CreateSentinel(const char* path) { + int fd = open(path, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) close(fd); + } RuntimeContext ctx; SessionState session; @@ -671,6 +705,687 @@ TEST_F(StrategiesIntegrationTest, ErrorHandling_UploadFailure) { EXPECT_FALSE(session.success); // Should remain false } +// ==================== WAIT FOR SENTINEL TESTS ==================== + +class WaitForSentinelTest : public ::testing::Test { +protected: + void SetUp() override { + // Ensure g_mock_file_ops is NULL so we use real system calls + g_mock_file_ops = nullptr; + + // Create unique temp directory using PID for test isolation + snprintf(test_dir_, sizeof(test_dir_), "/tmp/sentinel_test_%d", getpid()); + mkdir(test_dir_, 0755); + + // Setup sentinel file path + snprintf(sentinel_path_, sizeof(sentinel_path_), "%s/%s", test_dir_, kSentinelName); + } + + void TearDown() override { + unlink(sentinel_path_); + rmdir(test_dir_); + } + + void CreateSentinelFile() { + int fd = open(sentinel_path_, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) { + close(fd); + } + } + + void CreateFileInDir(const char* dir, const char* name) { + char path[MAX_PATH_LENGTH]; + snprintf(path, sizeof(path), "%s/%s", dir, name); + int fd = open(path, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) { + close(fd); + } + } + + char test_dir_[256]; + char sentinel_path_[256]; + static constexpr const char* kSentinelName = "test_sentinel"; +}; + +/** + * @test Fast path: sentinel file already exists before wait_for_sentinel is called. + * Covers: Fast-path access() check at function entry. + */ +TEST_F(WaitForSentinelTest, FastPath_SentinelAlreadyExists) { + CreateSentinelFile(); + + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 5); + EXPECT_EQ(0, result); +} + +/** + * @test Timeout: sentinel never appears within the specified timeout. + * Covers: Full inotify loop with clock_gettime deadline expiry. + */ +TEST_F(WaitForSentinelTest, Timeout_SentinelNeverAppears) { + // Sentinel not created - should timeout after 1 second + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 1); + EXPECT_EQ(-1, result); +} + +/** + * @test Inotify detection: sentinel appears after a short delay via IN_CREATE event. + * Covers: select() wakeup, read() of inotify_event, filename match. + */ +TEST_F(WaitForSentinelTest, Detection_SentinelAppearsAfterDelay) { + std::thread creator([this]() { + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + CreateSentinelFile(); + }); + + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 5); + creator.join(); + EXPECT_EQ(0, result); +} + +/** + * @test Race condition: sentinel appears between first access() and watch re-check. + * Covers: Re-check after inotify_add_watch to close the race window. + */ +TEST_F(WaitForSentinelTest, RaceCondition_SentinelAppearsDuringSetup) { + // Create sentinel with very short delay - may be caught by the re-check + std::thread creator([this]() { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + CreateSentinelFile(); + }); + + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 5); + creator.join(); + EXPECT_EQ(0, result); +} + +/** + * @test Zero timeout: should enter loop but immediately break on deadline check. + * Covers: deadline.tv_sec += 0, immediate expiry in while loop. + */ +TEST_F(WaitForSentinelTest, ZeroTimeout_ReturnsNegativeOne) { + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 0); + EXPECT_EQ(-1, result); +} + +/** + * @test Invalid watch directory: inotify_add_watch fails on non-existent directory. + * Covers: inotify_add_watch failure path and close(ifd). + */ +TEST_F(WaitForSentinelTest, InvalidWatchDir_Timeout) { + const char* bad_dir = "/nonexistent_sentinel_test_dir_xyz"; + const char* bad_path = "/nonexistent_sentinel_test_dir_xyz/sentinel"; + + int result = wait_for_sentinel(bad_path, bad_dir, "sentinel", 1); + EXPECT_EQ(-1, result); +} + +/** + * @test Wrong filename created in watched directory - should not trigger detection. + * Covers: inotify event filename comparison (strcmp != 0 path). + */ +TEST_F(WaitForSentinelTest, WrongFilename_DoesNotMatch) { + std::thread creator([this]() { + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + // Create a file with a DIFFERENT name + CreateFileInDir(test_dir_, "not_the_sentinel"); + }); + + // Wait for "test_sentinel" which will never appear + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 2); + creator.join(); + EXPECT_EQ(-1, result); + + // Clean up the wrong file + char wrong_path[256]; + snprintf(wrong_path, sizeof(wrong_path), "%s/not_the_sentinel", test_dir_); + unlink(wrong_path); +} + +/** + * @test Multiple sequential calls with sentinel present - consistent behavior. + * Covers: Function is idempotent and has no lingering state. + */ +TEST_F(WaitForSentinelTest, MultipleCalls_ConsistentBehavior) { + CreateSentinelFile(); + + EXPECT_EQ(0, wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 1)); + EXPECT_EQ(0, wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 1)); + EXPECT_EQ(0, wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 1)); +} + +/** + * @test Sentinel removed then re-checked - absence detected after removal. + * Covers: Ensures no caching of previous access() results. + */ +TEST_F(WaitForSentinelTest, SentinelRemovedThenRechecked) { + CreateSentinelFile(); + EXPECT_EQ(0, wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 1)); + + // Remove sentinel + unlink(sentinel_path_); + + // Now should timeout since sentinel is gone + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 1); + EXPECT_EQ(-1, result); +} + +/** + * @test wait_for_reboot_reason wrapper: sentinel present -> returns 0. + * Covers: PATH_FLAG_INVOCATION sentinel with production constants. + */ +TEST_F(WaitForSentinelTest, WaitForRebootReason_SentinelPresent) { + int fd = open(PATH_FLAG_INVOCATION, O_CREAT | O_WRONLY, 0644); + if (fd < 0) { + GTEST_SKIP() << "Cannot create " << PATH_FLAG_INVOCATION; + } + close(fd); + + int result = wait_for_reboot_reason(); + EXPECT_EQ(0, result); + + unlink(PATH_FLAG_INVOCATION); +} + +/** + * @test wait_for_reboot_reason wrapper: sentinel absent -> returns -1 after timeout. + * Covers: REBOOT_POLL_TIMEOUT_S timeout (2s in GTEST_ENABLE mode). + */ +TEST_F(WaitForSentinelTest, WaitForRebootReason_Timeout) { + unlink(PATH_FLAG_INVOCATION); + + int result = wait_for_reboot_reason(); + EXPECT_EQ(-1, result); +} + +/** + * @test wait_for_telemetry_prevlogs_done wrapper: sentinel present -> returns 0. + * Covers: TELEMETRY_PREVLOGS_DONE_FLAG with production constants. + */ +TEST_F(WaitForSentinelTest, WaitForTelemetryPrevlogsDone_SentinelPresent) { + int fd = open(TELEMETRY_PREVLOGS_DONE_FLAG, O_CREAT | O_WRONLY, 0644); + if (fd < 0) { + GTEST_SKIP() << "Cannot create " << TELEMETRY_PREVLOGS_DONE_FLAG; + } + close(fd); + + int result = wait_for_telemetry_prevlogs_done(); + EXPECT_EQ(0, result); + + unlink(TELEMETRY_PREVLOGS_DONE_FLAG); +} + +/** + * @test wait_for_telemetry_prevlogs_done wrapper: sentinel absent -> timeout. + * Covers: TELEMETRY_PREVLOGS_TIMEOUT_S timeout (2s in GTEST_ENABLE mode). + */ +TEST_F(WaitForSentinelTest, WaitForTelemetryPrevlogsDone_Timeout) { + unlink(TELEMETRY_PREVLOGS_DONE_FLAG); + + int result = wait_for_telemetry_prevlogs_done(); + EXPECT_EQ(-1, result); +} + +/** + * @test Sentinel appears just before timeout deadline. + * Covers: select() heartbeat re-checks and event delivery near deadline. + */ +TEST_F(WaitForSentinelTest, Detection_SentinelAppearsNearTimeout) { + // Create sentinel close to the 3s timeout (at ~2.5s) + std::thread creator([this]() { + std::this_thread::sleep_for(std::chrono::milliseconds(2500)); + CreateSentinelFile(); + }); + + int result = wait_for_sentinel(sentinel_path_, test_dir_, kSentinelName, 4); + creator.join(); + EXPECT_EQ(0, result); +} + +// ==================== HELPER FUNCTION TESTS ==================== + +/** + * Test fixture for internet_write_cb, nm_query_ipver, check_internet_connectivity, + * apply_ntp_fallback_time, and trigger_reboot_info_update. + */ +class HelperFunctionsTest : public ::testing::Test { +protected: + void SetUp() override { + g_mock_file_ops = nullptr; + } + + void TearDown() override { + g_mock_file_ops = nullptr; + } + + void CreateFile(const char* path) { + int fd = open(path, O_CREAT | O_WRONLY, 0644); + if (fd >= 0) close(fd); + } +}; + +// ---- internet_write_cb tests ---- + +/** + * @test Normal write: data fits entirely in buffer. + * Covers: memcpy path, len update, null terminator. + */ +TEST_F(HelperFunctionsTest, InternetWriteCb_NormalWrite) { + rpc_resp_t resp; + memset(&resp, 0, sizeof(resp)); + + const char* data = "Hello, World!"; + size_t ret = internet_write_cb((void*)data, 1, strlen(data), &resp); + + EXPECT_EQ(ret, strlen(data)); + EXPECT_EQ(resp.len, strlen(data)); + EXPECT_STREQ(resp.buf, "Hello, World!"); +} + +/** + * @test Multiple sequential writes accumulate in buffer. + * Covers: Appending to existing content via r->len offset. + */ +TEST_F(HelperFunctionsTest, InternetWriteCb_MultipleWrites) { + rpc_resp_t resp; + memset(&resp, 0, sizeof(resp)); + + const char* part1 = "Hello"; + const char* part2 = ", World!"; + internet_write_cb((void*)part1, 1, strlen(part1), &resp); + internet_write_cb((void*)part2, 1, strlen(part2), &resp); + + EXPECT_EQ(resp.len, strlen("Hello, World!")); + EXPECT_STREQ(resp.buf, "Hello, World!"); +} + +/** + * @test Buffer overflow protection: data larger than remaining space is truncated. + * Covers: incoming > space clamp, return value still reports full size*nmemb. + */ +TEST_F(HelperFunctionsTest, InternetWriteCb_BufferOverflowProtection) { + rpc_resp_t resp; + memset(&resp, 0, sizeof(resp)); + + // Fill buffer almost to capacity (leave 5 bytes + null) + resp.len = sizeof(resp.buf) - 6; + memset(resp.buf, 'A', resp.len); + + const char* overflow_data = "OVERFLOW_DATA_THAT_IS_TOO_LONG"; + size_t ret = internet_write_cb((void*)overflow_data, 1, strlen(overflow_data), &resp); + + // Return value is always size*nmemb (curl convention) + EXPECT_EQ(ret, strlen(overflow_data)); + // Buffer should only contain what fits (5 chars + null) + EXPECT_EQ(resp.len, sizeof(resp.buf) - 1); + // Null terminated + EXPECT_EQ(resp.buf[resp.len], '\0'); +} + +/** + * @test Zero-length write returns 0. + * Covers: size*nmemb == 0 edge case. + */ +TEST_F(HelperFunctionsTest, InternetWriteCb_ZeroLength) { + rpc_resp_t resp; + memset(&resp, 0, sizeof(resp)); + + size_t ret = internet_write_cb((void*)"data", 0, 0, &resp); + + EXPECT_EQ(ret, 0u); + EXPECT_EQ(resp.len, 0u); + EXPECT_EQ(resp.buf[0], '\0'); +} + +/** + * @test size != 1: verifies size*nmemb calculation. + * Covers: Curl may pass size=sizeof(element), nmemb=count. + */ +TEST_F(HelperFunctionsTest, InternetWriteCb_SizeTimesNmemb) { + rpc_resp_t resp; + memset(&resp, 0, sizeof(resp)); + + const char data[] = "ABCDEF"; + // size=2, nmemb=3 → total 6 bytes + size_t ret = internet_write_cb((void*)data, 2, 3, &resp); + + EXPECT_EQ(ret, 6u); + EXPECT_EQ(resp.len, 6u); + EXPECT_EQ(memcmp(resp.buf, "ABCDEF", 6), 0); +} + +// ---- check_internet_connectivity / nm_query_ipver tests ---- + +/** + * @test check_internet_connectivity returns false when Thunder is unreachable. + * Covers: curl_easy_perform failure path (CURLE_COULDNT_CONNECT in CI). + * Note: In Docker CI, nothing listens on 127.0.0.1:9998. + */ +TEST_F(HelperFunctionsTest, CheckInternetConnectivity_NoThunder) { + // In CI/test environment, Thunder JSON-RPC is not running + bool result = check_internet_connectivity(); + EXPECT_FALSE(result); +} + +/** + * @test nm_query_ipver returns false when Thunder is unreachable (IPv4). + * Covers: curl_easy_perform → CURLE_COULDNT_CONNECT → returns false. + */ +TEST_F(HelperFunctionsTest, NmQueryIpver_IPv4_NoThunder) { + bool result = nm_query_ipver("IPv4"); + EXPECT_FALSE(result); +} + +/** + * @test nm_query_ipver returns false when Thunder is unreachable (IPv6). + * Covers: Same failure path for IPv6 variant. + */ +TEST_F(HelperFunctionsTest, NmQueryIpver_IPv6_NoThunder) { + bool result = nm_query_ipver("IPv6"); + EXPECT_FALSE(result); +} + +// ---- apply_ntp_fallback_time tests ---- + +/** + * @test apply_ntp_fallback_time returns 0 when clock file is unreadable. + * Covers: fopen returns NULL → early return 0. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_FileNotReadable) { + // With g_mock_file_ops = nullptr, fopen always returns nullptr + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, 0); +} + +/** + * @test apply_ntp_fallback_time returns 0 when clock file is empty. + * Covers: fopen succeeds, fgets returns NULL → fclose + return 0. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_EmptyFile) { + // Create an empty temp file + char temp_file[64]; + snprintf(temp_file, sizeof(temp_file), "/tmp/ntp_test_empty_%d", getpid()); + int fd = open(temp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + close(fd); + + // Open for reading via fdopen (NOT mocked) to get a valid FILE* + fd = open(temp_file, O_RDONLY); + ASSERT_GE(fd, 0); + FILE* real_fp = fdopen(fd, "r"); + ASSERT_NE(nullptr, real_fp); + + // Temporarily set mock to return our real FILE* + MockFileOperations mock_ops; + g_mock_file_ops = &mock_ops; + EXPECT_CALL(mock_ops, fopen(_, _)).WillOnce(Return(real_fp)); + EXPECT_CALL(mock_ops, fclose(_)).WillOnce(Return(0)); + + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, 0); + + g_mock_file_ops = nullptr; + // Close the real fd (mocked fclose didn't actually close it) + fclose(real_fp); + unlink(temp_file); +} + +/** + * @test apply_ntp_fallback_time returns 0 when file contains invalid epoch (non-numeric). + * Covers: fopen succeeds, fgets succeeds, strtol returns 0 → return 0. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_InvalidEpochString) { + char temp_file[64]; + snprintf(temp_file, sizeof(temp_file), "/tmp/ntp_test_invalid_%d", getpid()); + int fd = open(temp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + const char* content = "not_a_number\n"; + write(fd, content, strlen(content)); + close(fd); + + fd = open(temp_file, O_RDONLY); + ASSERT_GE(fd, 0); + FILE* real_fp = fdopen(fd, "r"); + ASSERT_NE(nullptr, real_fp); + + MockFileOperations mock_ops; + g_mock_file_ops = &mock_ops; + EXPECT_CALL(mock_ops, fopen(_, _)).WillOnce(Return(real_fp)); + EXPECT_CALL(mock_ops, fclose(_)).WillOnce(Return(0)); + + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, 0); + + g_mock_file_ops = nullptr; + fclose(real_fp); + unlink(temp_file); +} + +/** + * @test apply_ntp_fallback_time returns 0 when file contains negative epoch. + * Covers: fopen succeeds, fgets succeeds, strtol returns < 0 → return 0. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_NegativeEpoch) { + char temp_file[64]; + snprintf(temp_file, sizeof(temp_file), "/tmp/ntp_test_neg_%d", getpid()); + int fd = open(temp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + const char* content = "-100\n"; + write(fd, content, strlen(content)); + close(fd); + + fd = open(temp_file, O_RDONLY); + ASSERT_GE(fd, 0); + FILE* real_fp = fdopen(fd, "r"); + ASSERT_NE(nullptr, real_fp); + + MockFileOperations mock_ops; + g_mock_file_ops = &mock_ops; + EXPECT_CALL(mock_ops, fopen(_, _)).WillOnce(Return(real_fp)); + EXPECT_CALL(mock_ops, fclose(_)).WillOnce(Return(0)); + + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, 0); + + g_mock_file_ops = nullptr; + fclose(real_fp); + unlink(temp_file); +} + +/** + * @test apply_ntp_fallback_time returns 0 when file contains zero. + * Covers: fopen succeeds, fgets succeeds, strtol returns 0 (epoch <= 0) → return 0. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_ZeroEpoch) { + char temp_file[64]; + snprintf(temp_file, sizeof(temp_file), "/tmp/ntp_test_zero_%d", getpid()); + int fd = open(temp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + const char* content = "0\n"; + write(fd, content, strlen(content)); + close(fd); + + fd = open(temp_file, O_RDONLY); + ASSERT_GE(fd, 0); + FILE* real_fp = fdopen(fd, "r"); + ASSERT_NE(nullptr, real_fp); + + MockFileOperations mock_ops; + g_mock_file_ops = &mock_ops; + EXPECT_CALL(mock_ops, fopen(_, _)).WillOnce(Return(real_fp)); + EXPECT_CALL(mock_ops, fclose(_)).WillOnce(Return(0)); + + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, 0); + + g_mock_file_ops = nullptr; + fclose(real_fp); + unlink(temp_file); +} + +/** + * @test apply_ntp_fallback_time returns valid epoch on success. + * Covers: fopen succeeds, fgets succeeds, strtol returns > 0 → return epoch. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_ValidEpoch) { + char temp_file[64]; + snprintf(temp_file, sizeof(temp_file), "/tmp/ntp_test_valid_%d", getpid()); + int fd = open(temp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + const char* content = "1700000000\n"; + write(fd, content, strlen(content)); + close(fd); + + fd = open(temp_file, O_RDONLY); + ASSERT_GE(fd, 0); + FILE* real_fp = fdopen(fd, "r"); + ASSERT_NE(nullptr, real_fp); + + MockFileOperations mock_ops; + g_mock_file_ops = &mock_ops; + EXPECT_CALL(mock_ops, fopen(_, _)).WillOnce(Return(real_fp)); + EXPECT_CALL(mock_ops, fclose(_)).WillOnce(Return(0)); + + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, (time_t)1700000000); + + g_mock_file_ops = nullptr; + fclose(real_fp); + unlink(temp_file); +} + +/** + * @test apply_ntp_fallback_time handles epoch with leading whitespace. + * Covers: strtol skips leading whitespace per C standard → returns valid epoch. + */ +TEST_F(HelperFunctionsTest, ApplyNtpFallbackTime_EpochWithWhitespace) { + char temp_file[64]; + snprintf(temp_file, sizeof(temp_file), "/tmp/ntp_test_ws_%d", getpid()); + int fd = open(temp_file, O_CREAT | O_WRONLY | O_TRUNC, 0644); + ASSERT_GE(fd, 0); + const char* content = " 1642780800\n"; + write(fd, content, strlen(content)); + close(fd); + + fd = open(temp_file, O_RDONLY); + ASSERT_GE(fd, 0); + FILE* real_fp = fdopen(fd, "r"); + ASSERT_NE(nullptr, real_fp); + + MockFileOperations mock_ops; + g_mock_file_ops = &mock_ops; + EXPECT_CALL(mock_ops, fopen(_, _)).WillOnce(Return(real_fp)); + EXPECT_CALL(mock_ops, fclose(_)).WillOnce(Return(0)); + + time_t result = apply_ntp_fallback_time(); + EXPECT_EQ(result, (time_t)1642780800); + + g_mock_file_ops = nullptr; + fclose(real_fp); + unlink(temp_file); +} + +// ---- trigger_reboot_info_update tests ---- + +/** + * @test trigger_reboot_info_update does nothing when PATH_FLAG_INVOCATION exists. + * Covers: stat(PATH_FLAG_INVOCATION) succeeds → no STT_FLAG touch. + */ +TEST_F(HelperFunctionsTest, TriggerRebootInfoUpdate_FlagAlreadyPresent) { + // Create PATH_FLAG_INVOCATION so stat() succeeds + CreateFile(PATH_FLAG_INVOCATION); + // Remove STT_FLAG to verify it's NOT created + unlink(STT_FLAG); + + trigger_reboot_info_update(); + + // STT_FLAG should NOT be created since PATH_FLAG_INVOCATION exists + struct stat st; + EXPECT_NE(stat(STT_FLAG, &st), 0); + + unlink(PATH_FLAG_INVOCATION); +} + +/** + * @test trigger_reboot_info_update creates STT_FLAG when PATH_FLAG_INVOCATION absent. + * Covers: stat(PATH_FLAG_INVOCATION) fails → open(STT_FLAG) path. + */ +TEST_F(HelperFunctionsTest, TriggerRebootInfoUpdate_CreatesSTTFlag) { + // Ensure PATH_FLAG_INVOCATION does NOT exist + unlink(PATH_FLAG_INVOCATION); + // Ensure STT_FLAG does NOT exist + unlink(STT_FLAG); + + trigger_reboot_info_update(); + + // STT_FLAG should now exist + struct stat st; + EXPECT_EQ(stat(STT_FLAG, &st), 0); + + // Cleanup + unlink(STT_FLAG); +} + +// ---- wait_for_reboot_reason / wait_for_telemetry_prevlogs_done ---- +// (Additional tests beyond WaitForSentinelTest fixture) + +/** + * @test wait_for_reboot_reason uses correct constants. + * Covers: Verifies PATH_FLAG_INVOCATION constant by creating it and checking return. + */ +TEST_F(HelperFunctionsTest, WaitForRebootReason_UsesCorrectPath) { + CreateFile(PATH_FLAG_INVOCATION); + + int result = wait_for_reboot_reason(); + EXPECT_EQ(0, result); + + unlink(PATH_FLAG_INVOCATION); +} + +/** + * @test wait_for_telemetry_prevlogs_done uses correct constants. + * Covers: Verifies TELEMETRY_PREVLOGS_DONE_FLAG constant. + */ +TEST_F(HelperFunctionsTest, WaitForTelemetryPrevlogsDone_UsesCorrectPath) { + CreateFile(TELEMETRY_PREVLOGS_DONE_FLAG); + + int result = wait_for_telemetry_prevlogs_done(); + EXPECT_EQ(0, result); + + unlink(TELEMETRY_PREVLOGS_DONE_FLAG); +} + +/** + * @test wait_for_reboot_reason timeout is short in GTEST_ENABLE mode. + * Covers: REBOOT_POLL_TIMEOUT_S == 2 when GTEST_ENABLE defined. + */ +TEST_F(HelperFunctionsTest, WaitForRebootReason_ShortTimeoutInTest) { + unlink(PATH_FLAG_INVOCATION); + + auto start = std::chrono::steady_clock::now(); + int result = wait_for_reboot_reason(); + auto elapsed = std::chrono::steady_clock::now() - start; + + EXPECT_EQ(-1, result); + // Should complete within ~3s (2s timeout + select heartbeat) + EXPECT_LT(std::chrono::duration_cast(elapsed).count(), 5); +} + +/** + * @test wait_for_telemetry_prevlogs_done timeout is short in GTEST_ENABLE mode. + * Covers: TELEMETRY_PREVLOGS_TIMEOUT_S == 2 when GTEST_ENABLE defined. + */ +TEST_F(HelperFunctionsTest, WaitForTelemetryPrevlogsDone_ShortTimeoutInTest) { + unlink(TELEMETRY_PREVLOGS_DONE_FLAG); + + auto start = std::chrono::steady_clock::now(); + int result = wait_for_telemetry_prevlogs_done(); + auto elapsed = std::chrono::steady_clock::now() - start; + + EXPECT_EQ(-1, result); + EXPECT_LT(std::chrono::duration_cast(elapsed).count(), 5); +} + // Entry point for the test executable int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv);