fix(cfg): 热重载读侧三点一致性校验——掐断打回出厂与保存吞段(#73)
真机 #73:运行中改配置触发热重载,卷层(dhara)偶发返回旧/短 视图时,引擎把该视图当成文件全文:逐段按缺省解析后整盘打回 出厂默认且不自愈;此后任何一次保存又按缩小的视图做手术式 重写,文件越写越小(台架 1066B→986B,[sntp] 段整体丢失), 即真机对时静默 10.5 小时的元凶。 处置(引擎侧根因无关防御;卷层回滚归 #79/#81 已合并的 FTL 修复域): - poll:读完加三点校验(读长==stat.size、读前后 re-stat 的 size/mtime 一致),判为旧/短视图则 RAM 不动、基线不刷新、 每 2 拍自动重试自愈;首次拒绝留 1 条 DBG_ERROR+syslog, 之后每 30 拍一条 WARN 提示重启收敛。 - save:同型三点校验,视图与 stat 不符返回 -1 且文件不动, 调用方保留 dirty 下拍重试,吞段链在第一次保存即被掐断。 - 边界:「一致但陈旧」的整卷回滚引擎层无法区分,靠重启 收敛,已写入设计说明风险表与文档口径。 测试:新增 TC-CONFIG-058/059/060(旧视图保持 RAM、保存短 视图拒写、读中改写拒绝+日志限速)。cfg_file 239/239; iec104 179、run_par 67、adc_cal 117、sntp 98、 cfg_file_store_mcu 34、net_cfg 48、ftp_cfg 16、prot_set 90、 prot_dim 38 全绿;/keil-build 0 Error 0 Warning。 文档:配置文件设计说明 §(3)/§5.3/风险表/变更记录 v1.17; 单元测试说明 058–060 行;网络配置说明与产品需求 FR-CFG-3 同步口径;台账随新鲜 .map 由 ram_baseline --update 回填。
This commit is contained in:
@@ -53,12 +53,19 @@
|
||||
* TC-CONFIG-043 输出超长 -> 拒绝保存(-1) 且原文件不动
|
||||
* 受管键行超 CFG_LINE_TEST_MAX -> 拒绝保存(-1) 原文件不动;
|
||||
* 非受管键的超长行照旧放过(原样保留)
|
||||
* TC-CONFIG-059 读视图与 stat 基准不符(卷层旧/短视图)-> 拒绝保存(-1)
|
||||
* 原文件一字节不动 + 自检留痕;视图追平后可再存——2026-11-05 视图一致性
|
||||
* 守卫:断"短视图 → 外科重写 → 文件缩小非默认段被删"吞段链(issue #73)
|
||||
* —— poll 契约(cfg_file_poll)——
|
||||
* TC-CONFIG-050 首轮建立基线(不重载)
|
||||
* TC-CONFIG-051 沉降去抖:连续 2 次稳定才重载
|
||||
* 与基线一致不重载
|
||||
* TC-CONFIG-053 变更后重载 + 挂回调段差异触发回调
|
||||
* TC-CONFIG-054 溢出到文件超长 -> 工厂默认 + 重建模板 + 自检留痕(不触发回调)
|
||||
* TC-CONFIG-058 卷层旧视图(读长 != stat 长)-> 拒绝应用:RAM 保旧值、
|
||||
* 基线保留 2s 重试、视图追平即自愈应用 + 回调;拒绝期间无回调无二次 ERROR
|
||||
* TC-CONFIG-060 读中改写(读后复 stat 令牌变)-> HELD 后重新沉降自愈;
|
||||
* 持续不一致:全程仅 1 ERROR + 1 留痕,WARN 每 30 趟一条(日志限速)
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
|
||||
@@ -98,6 +105,19 @@ static int s_fail_write = 0; /* cfg_store_write_atomic 返回 -1 */
|
||||
static int s_fail_alloc = 0; /* cfg_store_alloc 返回 NULL */
|
||||
static int s_fail_stat = 0; /* cfg_store_stat 返回 -1 */
|
||||
|
||||
/* 视图不一致注入(2026-11-05 视图一致性守卫):s_view_stale 置位时 read 返回
|
||||
* s_vfs_stale 快照,stat 仍报真实盘的 (size, mtime)——等效真机 dhara/FTL 在
|
||||
* stat→read 窗口给出旧视图(台架 8/8 复现形态:读到旧/短内容、解析错误行
|
||||
* 一条不出)。vfs_mark_stale() 冻结当前盘为旧视图并开启注入;
|
||||
* vfs_clear_stale() 视图追平 = 自愈 */
|
||||
static uint8_t s_vfs_stale[VFS_CAP];
|
||||
static size_t s_vfs_stale_len = 0u;
|
||||
static int s_view_stale = 0;
|
||||
|
||||
/* 读期间文件被改写注入:read 完成后 mtime 前进 = 读到的内容不再对应变更令牌
|
||||
* (一次性,复现"读中改写",由读后复 stat 检出) */
|
||||
static int s_touch_on_read = 0;
|
||||
|
||||
/* 观测计数 */
|
||||
static int s_write_count = 0;
|
||||
static int s_read_count = 0;
|
||||
@@ -135,8 +155,21 @@ int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nre
|
||||
if (n > cap) {
|
||||
n = cap;
|
||||
}
|
||||
(void)memcpy(p_buf, s_vfs, n);
|
||||
if (s_view_stale) {
|
||||
n = s_vfs_stale_len; /* 旧视图:盘上真身已变,读仍返回冻结快照 */
|
||||
if (n > cap) {
|
||||
n = cap;
|
||||
}
|
||||
(void)memcpy(p_buf, s_vfs_stale, n);
|
||||
}
|
||||
else {
|
||||
(void)memcpy(p_buf, s_vfs, n);
|
||||
}
|
||||
*p_nread = n;
|
||||
if (s_touch_on_read) {
|
||||
s_touch_on_read = 0; /* 一次性:读后令牌变 = 读中改写 */
|
||||
s_vfs_mtime++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -263,6 +296,10 @@ static void vfs_reset(void) {
|
||||
s_vfs_mtime = 100u;
|
||||
s_vfs_exists = 0;
|
||||
s_fail_read = s_fail_write = s_fail_alloc = s_fail_stat = 0;
|
||||
s_view_stale = 0;
|
||||
s_vfs_stale_len = 0u;
|
||||
memset(s_vfs_stale, 0, sizeof(s_vfs_stale));
|
||||
s_touch_on_read = 0;
|
||||
s_write_count = s_read_count = 0;
|
||||
s_syslog_count = 0;
|
||||
g_cfgtest_err_count = 0;
|
||||
@@ -279,6 +316,7 @@ static void vfs_reset(void) {
|
||||
memset(s_snap, 0, sizeof(s_snap));
|
||||
memset(s_sec_hdr, 0, sizeof(s_sec_hdr));
|
||||
memset(s_key_line, 0, sizeof(s_key_line));
|
||||
s_reject_streak = 0u;
|
||||
/* 段 RAM 副本复位工厂默认 */
|
||||
s_net_data = s_net_def;
|
||||
s_sys_data = s_sys_def;
|
||||
@@ -309,6 +347,20 @@ static void vfs_put_notoken(const char *text) {
|
||||
s_vfs_exists = 1;
|
||||
}
|
||||
|
||||
/* 冻结当前盘内容为"旧视图"并开启注入:此后 read 一直返回这份快照,
|
||||
* stat/盘上真身照常变化 = 复现卷层 stat→read 窗口的旧视图滞后 */
|
||||
static void vfs_mark_stale(void) {
|
||||
s_vfs_stale_len = s_vfs_len;
|
||||
memcpy(s_vfs_stale, s_vfs, s_vfs_len);
|
||||
s_view_stale = 1;
|
||||
}
|
||||
|
||||
/* 视图追平盘上真身 = 卷层视图恢复(守卫自愈路径) */
|
||||
static void vfs_clear_stale(void) {
|
||||
s_view_stale = 0;
|
||||
s_vfs_stale_len = 0u;
|
||||
}
|
||||
|
||||
/* 取虚拟盘当前内容为 NUL 结尾字符串(供检索断言);返回静态缓冲 */
|
||||
static const char *vfs_get(void) {
|
||||
static char buf[VFS_CAP + 1];
|
||||
@@ -974,6 +1026,34 @@ static void tc_save(void) {
|
||||
CHECK(strstr(vfs_get(), "u8=9") != NULL, "managed key still rewritten");
|
||||
CHECK(strstr(vfs_get(), "vendor = kkk") != NULL, "unknown long line preserved");
|
||||
}
|
||||
|
||||
printf("[TC-CONFIG-059] save view != stat token -> reject, file untouched\n");
|
||||
{
|
||||
size_t before_len;
|
||||
int before_writes;
|
||||
|
||||
/* 台架实证吞段链:卷层在 stat→read 窗口给旧/短视图 → 旧实现照视图外科
|
||||
* 重写整档 → 文件变小、非默认段被静默删除(实测 1066B→986B)。
|
||||
* 守卫后:读视图与 stat 基准不符 = 拒存,原文件一字节不动 */
|
||||
vfs_reset();
|
||||
register_std();
|
||||
vfs_put("[net]\r\nu8=7\r\n[sys]\r\nbaud=115200\r\n");
|
||||
s_net_data.u8 = 9u;
|
||||
vfs_mark_stale(); /* 视图冻结在当前完整文件 */
|
||||
vfs_put("[net]\r\nu8=7\r\n"); /* 盘上真身变短(旧视图回滚形态) */
|
||||
before_len = s_vfs_len;
|
||||
before_writes = s_write_count;
|
||||
s_syslog_count = 0;
|
||||
g_cfgtest_err_count = 0;
|
||||
CHECK(cfg_file_save() == -1, "mismatched view rejected with -1");
|
||||
CHECK(s_vfs_len == before_len, "file bytes untouched");
|
||||
CHECK(s_write_count == before_writes, "no atomic write happened");
|
||||
CHECK(s_syslog_count >= 1, "view-stale self-test logged");
|
||||
CHECK(g_cfgtest_err_count == 1, "one ERROR line logged");
|
||||
vfs_clear_stale(); /* 视图追平盘上真身 */
|
||||
CHECK(cfg_file_save() == 0, "save succeeds again after view converges");
|
||||
CHECK(strstr(vfs_get(), "u8=9") != NULL, "RAM value written after convergence");
|
||||
}
|
||||
}
|
||||
|
||||
/* TC-CONFIG-050~054:poll 契约 */
|
||||
@@ -1057,6 +1137,77 @@ static void tc_poll(void) {
|
||||
CHECK(s_vfs_len <= CFG_FILE_MAX_LEN, "file regenerated within limit");
|
||||
CHECK(s_net_cb_count == 0, "no callback on oversized (online-repair channel kept)");
|
||||
}
|
||||
|
||||
printf("[TC-CONFIG-058] poll stale view -> reload HELD, RAM kept, self-heals\n");
|
||||
{
|
||||
vfs_reset();
|
||||
register_std();
|
||||
vfs_put("[net]\r\nu8=5\r\n");
|
||||
s_net_data.u8 = 5u; /* RAM = 旧值:既非默认(7)也非新值(88) */
|
||||
cfg_file_poll(); /* 基线 = 旧令牌(台架时序:先有基线再有变更) */
|
||||
vfs_mark_stale(); /* 视图冻结在 u8=5(13B) */
|
||||
vfs_put("[net]\r\nu8=88\r\n"); /* 盘上真实变更(14B,读长必不符) */
|
||||
cfg_file_poll(); /* st 变:s_last 记录,stable=1 */
|
||||
s_syslog_count = 0;
|
||||
g_cfgtest_err_count = 0;
|
||||
s_net_cb_count = 0;
|
||||
cfg_file_poll(); /* stable=2 -> 读视图 13B != stat 14B -> HELD */
|
||||
CHECK(s_net_data.u8 == 5u, "held: RAM keeps old value (not 88, not default)");
|
||||
CHECK(g_cfgtest_err_count == 1, "first reject logs one ERROR");
|
||||
CHECK(s_syslog_count == 1, "first reject self-test logged once");
|
||||
CHECK(s_net_cb_count == 0, "no callback while held");
|
||||
CHECK(s_base_valid == 1u, "baseline kept (retry not re-baseline)");
|
||||
cfg_file_poll(); /* 视图仍旧 -> 继续 HELD,静默不刷屏 */
|
||||
CHECK(g_cfgtest_err_count == 1, "repeat rejects stay silent");
|
||||
CHECK(s_syslog_count == 1, "self-test not spammed");
|
||||
vfs_clear_stale(); /* 卷层视图恢复 */
|
||||
cfg_file_poll(); /* 令牌未变 stable 饱和 -> 重试应用 */
|
||||
CHECK(s_net_data.u8 == 88u, "self-heal: value applied once view converges");
|
||||
CHECK(s_net_cb_count >= 1, "callback fires on healed apply");
|
||||
}
|
||||
|
||||
printf("[TC-CONFIG-060] mid-read rewrite -> HELD; WARN cadence rate-bounded\n");
|
||||
{
|
||||
int i;
|
||||
|
||||
/* 段一:读完成后令牌被推进(读中改写)→ 读后复 stat 检出 → 拒绝且自愈 */
|
||||
vfs_reset();
|
||||
register_std();
|
||||
vfs_put("[net]\r\nu8=5\r\n");
|
||||
s_net_data.u8 = 5u;
|
||||
cfg_file_poll(); /* baseline */
|
||||
vfs_put("[net]\r\nu8=88\r\n"); /* 外部变更 */
|
||||
cfg_file_poll(); /* stable=1 */
|
||||
s_touch_on_read = 1; /* 本次 read 结束即"被改写" */
|
||||
g_cfgtest_err_count = 0;
|
||||
s_syslog_count = 0;
|
||||
cfg_file_poll(); /* stable=2 -> 读中改写 -> HELD */
|
||||
CHECK(s_net_data.u8 == 5u, "mid-read rewrite held: RAM kept");
|
||||
CHECK(g_cfgtest_err_count == 1, "mid-read reject logged one ERROR");
|
||||
CHECK(s_syslog_count == 1, "mid-read reject self-test logged");
|
||||
cfg_file_poll(); /* 令牌已变:重新 debounce */
|
||||
cfg_file_poll(); /* 再沉降 -> 视图已一致 -> 应用 */
|
||||
CHECK(s_net_data.u8 == 88u, "applies after view settles again");
|
||||
|
||||
/* 段二:持续旧视图 -> 全程仅 1 ERROR + 1 留痕,WARN 每 30 趟一条(≈60s) */
|
||||
vfs_reset();
|
||||
register_std();
|
||||
vfs_put("[net]\r\nu8=5\r\n");
|
||||
s_net_data.u8 = 5u;
|
||||
cfg_file_poll(); /* 基线 = 旧令牌 */
|
||||
vfs_mark_stale();
|
||||
vfs_put("[net]\r\nu8=88888\r\n"); /* 与视图长度不同 -> 持续不一致 */
|
||||
g_cfgtest_err_count = 0;
|
||||
g_cfgtest_info_count = 0;
|
||||
s_syslog_count = 0;
|
||||
for (i = 0; i < 35; i++) {
|
||||
cfg_file_poll(); /* 沉降 1 + HELD 34(streak 1..34 含 30) */
|
||||
}
|
||||
CHECK(s_net_data.u8 == 5u, "persistent stale view never touches RAM");
|
||||
CHECK(g_cfgtest_err_count == 1, "episode logs exactly one ERROR");
|
||||
CHECK(s_syslog_count == 1, "episode logs exactly one self-test");
|
||||
CHECK(g_cfgtest_info_count == 1, "WARN cadence: one line per 30 rounds");
|
||||
}
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
|
||||
Reference in New Issue
Block a user