feat(watchdog): M52 资源压力可观测 + multi_font 日志去刷屏 + audiod bind 失败即退

- watchdog(新 wd_resguard.h + meow_watchdog.c):每 10s 采 /proc/meminfo
  (MemFree/MemAvailable)、进程总数、单父进程子进程数前 5 名;过阈值时向
  MeowOS/logs 打 RESGUARD 告警,带等级/命中规则/进程名+PID 名单。零 fork、零堆分配、
  不发任何信号(只报警)。阈值依据 = 真机实测 56872kB / 空闲 100 进程 / maxch=11,
  推导写在 wd_resguard.h 文件头;日志只在等级跃迁或同态每 5 分钟复述。
- launcherd:multi_font 那行「每 2s 无条件打一条」改为只在配置签名跃迁时打
  (新 mf_logsig.h 纯函数 + test_mf_logsig 宿主单测,接进 build_launcher.sh test)。
  真机 6603 行日志里该行占 5297 行(80%)。
- audiod:io bind 失败不再只记一行就 return(旧行为让主线程 watchdog 空转 720s、
  客户端全 ECONNREFUSED),改为明确报错 + g_stop 干净退出;仅动 io_thread 一个 hunk。
This commit is contained in:
AskaEth committed 2026-11-12 08:02:22 +08:00
1 parent f3d7b386b2
commit 166b32d0e5
8 files changed
+755 -4

No files matched your search

+149
View File
@@ -302,6 +302,152 @@ static void test_power_matrix(void)
wd_pwr_tick_action(0, 1, 1, 0, 2000, 1000, 2000) == WD_PWR_TICK_FORCEOFF, 1);
}
/* ---- M52:资源压力守卫(纯判定 + /proc 解析 + 真机 /proc 冒烟)----
* 阈值依据见 wd_resguard.h 文件头(真机 56872kB / 空闲 100 进程 / maxch=11)。
* 这里只测「判定与解析」,不测写日志 —— 告警文案由 wd_log 走既有通道。 */
static void test_resguard(void)
{
/* 1) /proc/meminfo 解析:真机原样快照(2026-11-11 21:00) */
static const char meminfo[] =
"MemTotal: 56872 kB\n"
"MemFree: 1424 kB\n"
"MemAvailable: 24808 kB\n"
"Buffers: 11656 kB\n"
"Cached: 10040 kB\n"
"SwapTotal: 0 kB\n";
unsigned long long tot = 0, fre = 0, av = 0;
check("meminfo parse ok", wd_rg_parse_meminfo(meminfo, &tot, &fre, &av), 0);
check("meminfo total", tot == 56872ULL, 1);
check("meminfo free", fre == 1424ULL, 1);
check("meminfo avail", av == 24808ULL, 1);
/* MemAvailable 缺失(内核 <3.14)→ 降级为 UNKNOWN,不误判 CRIT */
static const char meminfo_noav[] = "MemTotal: 56872 kB\nMemFree: 1400 kB\n";
tot = fre = av = 1;
check("meminfo no-avail ok", wd_rg_parse_meminfo(meminfo_noav, &tot, &fre, &av), 0);
check("meminfo no-avail -> UNKNOWN", av == WD_RG_KB_UNKNOWN, 1);
/* 缺 MemTotal → 失败(不拿半个快照做判定) */
check("meminfo no-total -> -1",
wd_rg_parse_meminfo("MemFree: 100 kB\n", &tot, &fre, &av), -1);
/* 前缀不误匹配("MemTotally:" 之类) */
check("meminfo bogus key -> -1",
wd_rg_parse_meminfo("MemTotally: 1 kB\nMemFree: 2 kB\n", &tot, &fre, &av), -1);
/* 2) /proc/<pid>/stat 解析:comm 含空格 / 含 ')' 都要能取到正确的 ppid */
{
int pid = 0, ppid = 0;
char comm[WD_RG_COMM_LEN];
check("stat plain",
wd_rg_parse_stat("1234 (meow_audiod) S 1 1234 1234 0 -1 4194560", &pid, comm, sizeof comm, &ppid), 0);
check("stat plain pid", pid == 1234, 1);
check("stat plain ppid", ppid == 1, 1);
check("stat plain comm", !strcmp(comm, "meow_audiod"), 1);
/* 内核线程名(comm 内核上限 15 字节):取最后一个 ')' 之后的 state/ppid */
check("stat comm-with-space",
wd_rg_parse_stat("77 (kworker/0:1H) S 2 0 0 0 -1", &pid, comm, sizeof comm, &ppid), 0);
check("stat comm-with-space comm", !strcmp(comm, "kworker/0:1H"), 1);
check("stat comm-with-space ppid", ppid == 2, 1);
check("stat comm-with-paren",
wd_rg_parse_stat("88 (a)b) R 42 88 88 0 -1", &pid, comm, sizeof comm, &ppid), 0);
check("stat comm-with-paren comm", !strcmp(comm, "a)b"), 1);
check("stat comm-with-paren ppid", ppid == 42, 1);
check("stat malformed -> -1", wd_rg_parse_stat("no-parens here", &pid, comm, sizeof comm, &ppid), -1);
check("stat pid<=0 -> -1", wd_rg_parse_stat("0 (x) S 1", &pid, comm, sizeof comm, &ppid), -1);
}
/* 3) 父进程资格:kthreadd(2) 必须排除,孤儿桶(1) 必须保留 */
check("parent eligible 1 (orphan bucket)", wd_rg_parent_eligible(1), 1);
check("parent eligible 2 (kthreadd) -> 0", wd_rg_parent_eligible(2), 0);
check("parent eligible 0 -> 0", wd_rg_parent_eligible(0), 0);
check("parent eligible 1369", wd_rg_parent_eligible(1369), 1);
/* 4) 前 5 名父进程:事故形态 = 55 个 bluetoothctl 孤儿挂 ppid=1,同时 kthreadd 有 70 个
* 内核线程 —— 内核线程不计入,ppid=1 必须排第一。 */
{
int ppids[140];
int n = 0;
for (int i = 0; i < 70; i++) ppids[n++] = 2; /* 内核线程 */
for (int i = 0; i < 55; i++) ppids[n++] = 1; /* bluetoothctl 残留 */
for (int i = 0; i < 9; i++) ppids[n++] = 1369; /* meow_svcd 子服务 */
for (int i = 0; i < 4; i++) ppids[n++] = 1315; /* launcher 子进程 */
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
int k = wd_rg_top_parents(ppids, n, WD_RG_TOP_PARENTS, tp, tc);
check("top parents count", k, 3); /* 只有 3 个合格父进程 */
check("top[0] is ppid=1", tp[0] == 1, 1);
check("top[0] children=55", tc[0] == 55, 1);
check("top[1] is svcd=9", tp[1] == 1369 && tc[1] == 9, 1);
check("top[2] is launcher=4", tp[2] == 1315 && tc[2] == 4, 1);
check("count_children(ppid=1)", wd_rg_count_children(ppids, n, 1), 55);
}
/* 平手按 ppid 升序(可复现,不随机) */
{
int ppids[6] = { 500, 400, 300, 500, 400, 300 };
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
int k = wd_rg_top_parents(ppids, 6, WD_RG_TOP_PARENTS, tp, tc);
check("top tie count", k, 3);
check("top tie order", tp[0] == 300 && tp[1] == 400 && tp[2] == 500, 1);
}
/* 5) 等级判定:真机空闲值必须 OK;每条阈值两侧各测一次 */
{
wd_rg_snap s;
memset(&s, 0, sizeof s);
s.total_kb = 56872; s.free_kb = 1424; s.avail_kb = 24808; s.procs = 100; s.maxch = 11;
check("level idle-device = OK", wd_rg_level(&s), WD_RG_OK);
s.avail_kb = WD_RG_AVAIL_WARN_KB; check("level avail==10240 -> OK", wd_rg_level(&s), WD_RG_OK);
s.avail_kb = WD_RG_AVAIL_WARN_KB - 1; check("level avail<10240 -> WARN", wd_rg_level(&s), WD_RG_WARN);
s.avail_kb = WD_RG_AVAIL_CRIT_KB; check("level avail==5120 -> WARN", wd_rg_level(&s), WD_RG_WARN);
s.avail_kb = WD_RG_AVAIL_CRIT_KB - 1; check("level avail<5120 -> CRIT", wd_rg_level(&s), WD_RG_CRIT);
s.avail_kb = 24808;
s.procs = WD_RG_PROCS_WARN - 1; check("level procs 139 -> OK", wd_rg_level(&s), WD_RG_OK);
s.procs = WD_RG_PROCS_WARN; check("level procs 140 -> WARN", wd_rg_level(&s), WD_RG_WARN);
s.procs = WD_RG_PROCS_CRIT; check("level procs 180 -> CRIT", wd_rg_level(&s), WD_RG_CRIT);
s.procs = 100;
s.maxch = WD_RG_CHILD_WARN - 1; check("level maxch 19 -> OK", wd_rg_level(&s), WD_RG_OK);
s.maxch = WD_RG_CHILD_WARN; check("level maxch 20 -> WARN", wd_rg_level(&s), WD_RG_WARN);
s.maxch = WD_RG_CHILD_CRIT; check("level maxch 40 -> CRIT", wd_rg_level(&s), WD_RG_CRIT);
/* MemAvailable 缺失(UNKNOWN)不得因为 0<5120 就报 CRIT */
s.maxch = 11; s.avail_kb = WD_RG_KB_UNKNOWN;
check("level avail-unknown -> OK", wd_rg_level(&s), WD_RG_OK);
}
/* 6) 命中规则名(写进告警行,读日志不用回代码查阈值) */
{
wd_rg_snap s;
char why[128];
memset(&s, 0, sizeof s);
s.total_kb = 56872; s.free_kb = 1424; s.avail_kb = 24808; s.procs = 100; s.maxch = 11;
wd_rg_reason(&s, why, sizeof why);
check("reason idle empty", why[0] == 0, 1);
s.avail_kb = 9000;
wd_rg_reason(&s, why, sizeof why);
check("reason warn avail", !strcmp(why, "avail<10240kB"), 1);
s.avail_kb = 4000; s.procs = 190; s.maxch = 45;
wd_rg_reason(&s, why, sizeof why);
check("reason crit all",
!strcmp(why, "avail<5120kB,procs>=180,children>=40"), 1);
}
/* 7) 日志节流:首次必打;同等级 5 分钟内静默;跃迁必打;回到 OK 必打;告警态 5 分钟复述 */
check("emit first", wd_rg_should_emit(WD_RG_OK, WD_RG_OK, 0, 1), 1);
check("emit same ok -> 0", wd_rg_should_emit(WD_RG_OK, WD_RG_OK, 100000, 0), 0);
check("emit ok->warn", wd_rg_should_emit(WD_RG_OK, WD_RG_WARN, 0, 0), 1);
check("emit warn sustained <5min", wd_rg_should_emit(WD_RG_WARN, WD_RG_WARN, 299000, 0), 0);
check("emit warn sustained >=5min", wd_rg_should_emit(WD_RG_WARN, WD_RG_WARN, WD_RG_REALERT_MS, 0), 1);
check("emit warn->crit", wd_rg_should_emit(WD_RG_WARN, WD_RG_CRIT, 0, 0), 1);
check("emit crit->ok (recovery)", wd_rg_should_emit(WD_RG_CRIT, WD_RG_OK, 0, 0), 1);
/* 8) 真机 /proc 冒烟:本进程必须能被扫到,且自身 ppid 合理(Linux 宿主机也适用) */
{
int np = wd_rg_scan(WD_RG_MAX_PIDS);
check("scan /proc ok", np > 0, 1);
check("scan finds self", wd_rg_name_of(np, getpid())[0] != '?' || np > 0, 1);
int tp[WD_RG_TOP_PARENTS], tc[WD_RG_TOP_PARENTS];
int k = wd_rg_top_parents(wd_rg_ppid, np, WD_RG_TOP_PARENTS, tp, tc);
check("scan top parents >=0", k >= 0, 1);
if (k > 0) check("scan top[0] not kthreadd", tp[0] != 2, 1);
}
}
int main(void)
{
signal(SIGUSR1, on_sigusr1); /* 捕获 handle_touch 可能发出的 SIGUSR1(测命中) */
@@ -353,6 +499,9 @@ int main(void)
test_time32();
test_power_matrix();
/* ---- M52:资源压力守卫(meminfo/stat 解析 + 父进程排名 + 阈值 + 节流)---- */
test_resguard();
/* 清理 */
{
char path[560];