f754d59218
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/, 顶层只留新增的目录导航 README.md(归层判据 + 硬约束 + 本次处置记录)。 - 搬迁 28 个路径:测试数据三个目录 58 个文件为纯改名(R100,内容逐字节不变) - 全库引用同步 124 个文件:AGENTS / docs 索引 / skills / 门禁注释 / scripts - 门禁面:scripts/check_test_traceability.py 的 glob 改 rglob (分层后不递归扫描=追溯门禁直接瞎掉) - scripts/rename_ref.py:收尾的 ℹ 提示在 GBK 代码页抛 UnicodeEncodeError, 让进程以退出码 1 收场而改写其实已落盘(RC 与真实结果相反);改为把 stdout/stderr reconfigure 成 UTF-8,与 check_doc_links.py 等同法 - docs/文档规范.md §1.1 补「子目录文件过多时再分一层」;§3.6 补 rename_ref 搬整目录的两个实测坑:① 会改写目录内部文件的内容 ② 内部文件的 ../ 出链 不补层级(本次 23 个被误改的证据文件已按 HEAD 原始字节逐字节还原) - docs/README.md §6 改为与六个子目录一一对应,并补登原先漏登记的 「启动日志原子性复验流程」一行 - .cubemx_survival_baseline.json:App/util/task_watchdog.c 的注释路径也随分层更新, 该文件是存活审计的 watchdog-module 漂移锚点,按脚本自己的提示跑 --write 重建基线 (实测重建后仅 watchdog-module 一条哈希变化) - 铁律 4 例外(本次在任务树里改了单写者文件):AGENTS.md 与 .pre-commit-config.yaml 只被 rename_ref 改写路径字面量(后者全在注释行), 任务表块与钩子声明一字未动 验证:check_doc_links.py --sections-strict 覆盖全部 88 个改动 .md 全绿; check_test_traceability.py 输出与主树逐行一致;196 个文本文件无 BOM、无新增 CR、无 U+FFFD、无行尾空白;陈旧引用面由主树 25 处降到 6 处(无新增断链)。
355 lines
15 KiB
C
355 lines
15 KiB
C
/*
|
||
* 模块名称: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 <stdio.h>
|
||
#include <string.h>
|
||
#include <time.h>
|
||
#include <sys/stat.h>
|
||
|
||
#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;
|
||
}
|