/* * 模块名称:System Log (模拟器后端) — 运行/自检记录落盘 * 模块功能:宿主文件七份(sim_run / sim_selftest / sim_accid / sim_alarm / sim_start / * sim_oper / sim_yk,均 .log,工作目录),与 MCU 端同格式、同 id 口径 * (定长 64B 记录);单线程直写,无队列;含「清除记录」后端(与 MCU 端 * 同一套 API 与同一组判据:删后重建可继续写 / 计数归零 / 文件不在时幂等) * 适用平台:PC 模拟器 * 作者:王建锋 * 创建日期:2026-09-14 * 修改记录: * v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4) * v1.1 2026-10-26 王建锋 新增「记录删除」后端(道F task-4,与 sys_log_mcu.c * 同一份 sys_log.h 契约):syslog_request_clear() / * syslog_clear_state()。两端**时序有别、判据相同**: * 模拟器单线程(本文件里 syslog_post 也是直写不排队, * 同一条先例),请求在调用内同步落地,因此 * SYSLOG_CLEAR_WAIT 在本端永不出现;判据(幂等 0 / * 删后可重建继续写 / 失败不动原文件)逐条与 MCU 端一致 * v1.2 2026-10-26 王建锋 task-10 扩五类记录:s_paths/s_caps 同步到七项。 * **顺手修一处既有隐患**(Lead 批准,须在提交信息留痕): * 原 `s_paths[SYSLOG_COUNT][16]` 里 "sim_selftest.log" * 正好 16 字符 → 初值**装不满、没有 NUL 位**,而它又是 * 数组末行,后面没有任何保证为 0 的字节:stat()/remove() * 拿它当 C 串会读越过数组尾,目前纯粹是"后面那块 .rodata * 碰巧是 0"才没出事(未定义行为,换编译器布局即可能变成 * 打开一个错名字的文件)。行宽加到 24 并逐项写出, * 越界这条随扩容一起消失。 */ /* 头文件包含区 */ #include #include #include #include #include "sys_log.h" /* 静态变量定义区 */ /* 行宽 24 = 最长宿主名 "sim_selftest.log"(16) + NUL,留余量给后续类别。 * ⚠ 原来这里是 [16]:正好等于最长名的字符数,**没给 NUL 留位**(v1.2 修的隐患, * 详见文件头修改记录)——加宽数组是这条修复的一部分,不要"看着没用"再改回去。 * ⚠ 下标 = syslog_id_t 枚举序,与 sys_log_mcu.c 的 s_paths[] 逐项同序(两端一致是 * "宿主与真机行为等价"这条口径的前提)。 */ static const char s_paths[SYSLOG_COUNT][24] = { "sim_run.log", "sim_selftest.log", "sim_accid.log", "sim_alarm.log", "sim_start.log", "sim_oper.log", "sim_yk.log" }; static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX, SYSLOG_ACCIDENT_MAX, SYSLOG_ALARM_MAX, SYSLOG_START_MAX, SYSLOG_OPER_MAX, SYSLOG_YK_MAX }; /* 「清除记录」的结果字节(SYSLOG_CLEAR_NONE/OK/FAIL)。本端同步落地,没有 * "排队中"这一态,所以只需要结果一枚,MCU 端那枚 s_clear_req[] 在这里是多余的。 * 宿主进程态,不参与 MCU 的 RAM 预算台账。 */ static int8_t s_clear_st[SYSLOG_COUNT]; /* 函数定义区 */ /* * 函数功能:取当前宿主本地时间(ISO 8601 空格变体 + epoch) * 入口参数:p_ts - 输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * * p_epoch- 输出 epoch 秒(不可为 NULL) uint32_t * * 返回值:无 */ static void s_now(char *p_ts, uint32_t *p_epoch) { time_t t = time(NULL); struct tm tmv = {0}; /* 显式清零:localtime_s/r 是"输出型"参数,静态分析按未初始化报 */ #if defined(_WIN32) localtime_s(&tmv, &t); #else localtime_r(&t, &tmv); #endif (void)snprintf(p_ts, SYSLOG_TS_LEN + 1u, "%04d-%02d-%02d %02d:%02d:%02d", tmv.tm_year + 1900, tmv.tm_mon + 1, tmv.tm_mday, tmv.tm_hour, tmv.tm_min, tmv.tm_sec); *p_epoch = (uint32_t)t; } /* * 函数功能:追加一条记录(格式化 64B;容量满先删除重建) * 入口参数:id - 日志 ID syslog_id_t * desc - 描述串(UTF-8,超长截断至 SYSLOG_DESC_MAX) const char * * 返回值:0 - 成功;-1 - 写失败 int */ static int s_append(syslog_id_t id, const char *desc) { FILE *fp; uint8_t rec[SYSLOG_REC_SIZE]; char ts[SYSLOG_TS_LEN + 1u]; uint32_t epoch = 0u; size_t dl; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) { return -1; } if (syslog_count(id) >= s_caps[id]) { (void)remove(s_paths[id]); } memset(rec, 0u, sizeof(rec)); s_now(ts, &epoch); memcpy(rec, ts, SYSLOG_TS_LEN); rec[SYSLOG_TS_LEN] = ' '; /* +1 = 容纳 NUL;字段本身固定 10 位数字,与旧 sprintf 写入字节数完全一致 */ (void)snprintf((char *)(rec + SYSLOG_EPOCH_OFFS), SYSLOG_EPOCH_LEN + 1u, "%010u", (unsigned int)epoch); rec[SYSLOG_DESC_OFFS - 1u] = ' '; dl = strlen(desc); if (dl > SYSLOG_DESC_MAX) { dl = SYSLOG_DESC_MAX; } memcpy(rec + SYSLOG_DESC_OFFS, desc, dl); fp = fopen(s_paths[id], "ab"); if (fp == NULL) { return -1; } if (fwrite(rec, 1u, sizeof(rec), fp) != sizeof(rec)) { (void)fclose(fp); return -1; } (void)fclose(fp); return 0; } /* * 函数功能:读取第 idx 条原始记录(64B) * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t * p_rec- 输出缓冲(SYSLOG_REC_SIZE 字节,不可为 NULL) uint8_t * * 返回值:0 - 成功;-1 - 越界/读失败 int */ static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) { FILE *fp; if (p_rec == NULL || idx >= syslog_count(id)) { return -1; } fp = fopen(s_paths[id], "rb"); if (fp == NULL) { return -1; } if (fseek(fp, (long)idx * SYSLOG_REC_SIZE, SEEK_SET) != 0 || fread(p_rec, 1u, sizeof(uint8_t) * SYSLOG_REC_SIZE, fp) != SYSLOG_REC_SIZE) { (void)fclose(fp); return -1; } (void)fclose(fp); return 0; } /* * 函数功能:投递一条日志(单线程直写宿主文件) * 入口参数:id - 日志 ID syslog_id_t * desc - 描述串(UTF-8,超长截断) const char * * 返回值:无 */ void syslog_post(syslog_id_t id, const char *desc) { (void)s_append(id, desc); } /* * 函数功能:投递一条日志(零日志缓冲版本;宿主实现) * 入口参数:id - 日志 ID syslog_id_t * desc - 描述串(UTF-8,超长截断) const char * * 返回值:无 * 函数说明:MCU 端设这个入口是为了绕开 DBG 的内联行缓冲(protectTask / * iec104Task 的静态最坏链,理由写在 sys_log.h 同号声明处);宿主进程 * 没有那条硬线,也**没有队列与丢弃**,所以这里与 syslog_post 完全等价。 * 刻意不做成"宿主端另起一套语义"——五类记录的写入点在两端跑同一份 * 调用,语义差异越少,模拟器作为回归面的价值越高。 */ void syslog_post_quiet(syslog_id_t id, const char *desc) { (void)s_append(id, desc); } /* * 函数功能:取 quiet 通道累计丢弃条数(宿主实现) * 入口参数:无 * 返回值:恒 0——本端直写宿主文件、无队列无数组,不存在"满态丢弃"这条路径 uint32_t * 函数说明:不要为了"看起来对称"在这里造一个假计数;MCU 端那条路径的丢条数由 * test/host/sys_log_mcu 的 TC 直接注入队列满来验,宿主验不了也不该装。 */ uint32_t syslog_post_dropped(void) { return 0u; } /* * 函数功能:删掉本端的记录文件(「清除记录」的落地半边,宿主实现) * 入口参数:id - 日志 ID syslog_id_t * 返回值:0 - 已删除,或文件本就不存在(幂等成功,对齐 MCU 端的 FR_NO_FILE); * -1 - 删除失败,**原文件未被改动** int * 限定条件:调用方即本端写者(模拟器单线程,syslog_post 在这里也是直写不排队); * 本端没有卷锁,也就没有 MCU 端"卷占用 = 顺延"那一态 * 函数说明:先 stat 再 remove,是为了把"文件不在"与"删除失败"分开—— * remove() 对不存在的文件同样返回 -1,直接拿它的返回值判失败会把 * **幂等成功误报成故障**(与 MCU 端把 FR_NO_FILE 算成功不对称)。 * 与 MCU 端同一条纪律:只动自己那两个记录文件,不碰同目录里的其它文件。 */ static int s_clear_apply(syslog_id_t id) { struct stat st; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { return -1; } if (stat(s_paths[id], &st) != 0) { return 0; } return (remove(s_paths[id]) == 0) ? 0 : -1; } /* * 函数功能:登记「清除某日志全部记录」的请求(宿主后端:调用内同步落地) * 入口参数:id - 日志 ID syslog_id_t * 返回值:0 - 已执行(结果读 syslog_clear_state);-1 - id 越界(存量文件未动) int * 限定条件:任意调用点(模拟器单线程,这里没有"非写者上下文"可言) * 函数说明:与 MCU 端的**唯一**差异是时序:MCU 端只置请求位、等 defaultTask 的 * syslog_drain() 尾部才碰盘,本端在本调用内就删完了,因此 * SYSLOG_CLEAR_WAIT 在模拟器上永不出现(同 syslog_post:MCU 有队列与 * 丢弃、本端直写无队列)。判据一条没少:删后再投日志会重建文件并正常 * 写入、syslog_count() 归零、文件不在时幂等、失败不动原文件。 */ int syslog_request_clear(syslog_id_t id) { /* 与 MCU 端 s_clear_rec[] 同一组文案(一类一枚 ROM 常量、零拼接;改动必须 * 两处同改,两端"同格式同口径"是模拟器作为回归面的前提)。SYSLOG_OPER * 那一格同样永不被使用——清 oper.log 时不投递,否则"我清除了记录"这条会把 * 刚删空的 oper.log 又写成 1 条,砸掉「删除成功后 count 归零」这条判据。 */ static const char *const s_clear_rec[SYSLOG_COUNT] = { "clear run.log", "clear selftest.log", "clear accid.log", "clear alarm.log", "clear start.log", "clear oper.log", "clear yk.log" }; int rc; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { return -1; } rc = s_clear_apply(id); s_clear_st[id] = (rc == 0) ? (int8_t)SYSLOG_CLEAR_OK : (int8_t)SYSLOG_CLEAR_FAIL; /* 成功才留痕(失败那次什么都没改),且跳过 oper.log 自己——理由见上面表头。 * 本端 syslog_post_quiet 等价于直写,所以这条操作记录在函数返回前就已落盘, * 与 MCU 端"下一轮 drain 才落"时序不同(两端差异已登记在 * docs/测试说明/专项验证/模拟器差距清单.md,task-4 那次也写在那里)。 */ if ((rc == 0) && ((uint32_t)id != (uint32_t)SYSLOG_OPER)) { syslog_post_quiet(SYSLOG_OPER, s_clear_rec[id]); } return 0; } /* * 函数功能:查「清除记录」的状态(宿主后端) * 入口参数:id - 日志 ID syslog_id_t * 返回值:SYSLOG_CLEAR_NONE(0) / OK(1) / FAIL(-2);本端不产出 WAIT(-1), * id 越界按 NONE 返回 int * 限定条件:任意调用点(单线程只读一字节) * 函数说明:与 MCU 端共用同一组常量与同一段消费代码语义,UI 页面两端一份逻辑 * 就能同时处理"排队中"(真机)与"立即完成"(模拟器)。 */ int syslog_clear_state(syslog_id_t id) { if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { return (int)SYSLOG_CLEAR_NONE; } if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_OK) { return (int)SYSLOG_CLEAR_OK; } if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_FAIL) { return (int)SYSLOG_CLEAR_FAIL; } return (int)SYSLOG_CLEAR_NONE; } /* * 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1)) * 入口参数:id - 日志 ID syslog_id_t * 返回值:条数;文件缺失返回 0 uint32_t */ uint32_t syslog_count(syslog_id_t id) { struct stat st; if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) { return 0u; } if (stat(s_paths[id], &st) != 0) { return 0u; } return (uint32_t)st.st_size / SYSLOG_REC_SIZE; } /* * 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾) * 入口参数:id - 日志 ID syslog_id_t * idx - 记录序号(0 起) uint32_t * p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节,不可为 NULL) char * * ts_len - p_ts 容量 size_t * p_desc - 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节,不可为 NULL) char * * desc_len - p_desc 容量 size_t * 返回值:0 - 成功;-1 - 越界/读失败 int */ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len) { uint8_t rec[SYSLOG_REC_SIZE]; size_t dl = 0u; if (p_ts == NULL || p_desc == NULL || ts_len <= SYSLOG_TS_LEN || desc_len <= SYSLOG_DESC_MAX) { return -1; } if (s_read_raw(id, idx, rec) != 0) { return -1; } memcpy(p_ts, rec, SYSLOG_TS_LEN); p_ts[SYSLOG_TS_LEN] = '\0'; while (dl < SYSLOG_DESC_MAX && rec[SYSLOG_DESC_OFFS + dl] != '\0') { dl++; } if (dl >= desc_len) { dl = desc_len - 1u; } memcpy(p_desc, rec + SYSLOG_DESC_OFFS, dl); p_desc[dl] = '\0'; return 0; } /* * 函数功能:读取末条记录的 epoch 秒(上电/重启分类用) * 入口参数:id - 日志 ID syslog_id_t * p_epoch- 输出缓冲(不可为 NULL;无记录时置 0) uint32_t * * 返回值:0 - 成功读到末条;-1 - 文件缺失/空 int */ int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) { uint8_t rec[SYSLOG_REC_SIZE]; uint32_t v = 0u; uint32_t i; /* 先判空再写:旧写法 `if (p_epoch == NULL || ...)` 之后立刻 *p_epoch = 0, * 一旦调用方真传 NULL 就是空指针解引用(cppcheck nullPointerRedundantCheck 实报) */ if (p_epoch == NULL) { return -1; } if (syslog_count(id) == 0u) { *p_epoch = 0u; return -1; } if (s_read_raw(id, syslog_count(id) - 1u, rec) != 0) { *p_epoch = 0u; return -1; } for (i = 0u; i < SYSLOG_EPOCH_LEN; i++) { char c = (char)rec[SYSLOG_EPOCH_OFFS + i]; if (c >= '0' && c <= '9') { v = v * 10u + (uint32_t)(c - '0'); } } *p_epoch = v; return 0; }