Files
wandering1 f754d59218 docs(测试说明): 按用途分六个子目录并同步全库引用
分层:流程与规范/ 单元测试/ 板级测试/ 专项验证/ 测试工具/ 测试数据/,
顶层只留新增的目录导航 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 处(无新增断链)。
2026-10-30 07:16:07 +08:00

355 lines
15 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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;
}