fix(daemon): M65 P1 三条 + P2-1 — popen 超时/共享缓冲、bt scan 有界收割与毫秒节流
来自 M59 内存与资源安全审计(这 3 条 P1 很可能就是 2026-11-11 18:44 设备用户态崩塌的 成因:当时 sshd 连 fork 都做不到、ICMP 仍应答)。执行顺序按审计要求 P1-3 → P2-1 → P1-1 → P1-2。 P1-1 launcherd net.c:popen_read_tmo —— 对持续输出的子进程永不超时 + 堆无限增长 - 新增 meow_launcherd/popen_tmo.h:读循环的 deadline 移到**循环顶部**,输出加 256KB 硬 上限(超限返回 OVERFLOW,调用方 kill 子进程并按失败返回 NULL,绝不静默截断),缓冲交给 调用方或由本函数持有(无 static)。 - net.c 收尸改为 popen_tmo_reap_bounded(300ms + SIGKILL + 300ms):旧实现 SIGKILL 之后是 无条件 waitpid(…,0),子进程卡 D 态照样钉死 UI 线程。调用面(malloc 返回值 + free)不变, 14 个调用点零改动。 P1-2 audiod popen_read_tmo 的 static char buf[4096] 被 4 个线程共用 - 改成调用方传缓冲(`char *popen_read_tmo(tmo, cmd, out, cap)`),函数内无共享状态、不加锁 —— M55 刚把音量写路径做成无锁,音频线程上不能拿锁。7 个调用点各用自己的栈缓冲。 - 顺带修掉旧循环的 `remain` 不递减问题:旧写法每次 poll 都能重等满 tmo,最坏总时长 ≈ 4095 次 × 1500ms;现在总时长就是一个 tmo。 P1-3 netmgrd bt_scan_on/off 走无超时 system()(单线程主循环 + 55 个残留 bluetoothctl 的形态) - scan off:直接 run_cmd_ex(BT_SCAN_OFF_TMO_MS=600),超时如实回 MEOW_ERR_NODEV 并计入既有 bt_circuit_note。 - scan on:fork 一个 setsid 的 scan worker(在飞上限 1),worker 内部用 run_cmd_ex 跑同一条 pipeline(超时 + 输出上限);父进程只记账,bt_scan_reap() 在主循环每拍收割,过 deadline 杀**整个进程组**(pipeline 的 bluetoothctl 是孙进程,只杀 sh 会留孤儿)。 - 超时计入既有熔断(连 3 次 → 30s 内不再 fork);熔断中 scan on 不再起进程。 - 不在主循环里同步跑整条 pipeline:它要 ~5s(sleep 1 + dwell 4),同步跑等于每 7s 把 DNS 代理停 5s,是另一种挂死。 P2-1 扫描节流失效 7000 秒(mono_now() 返回秒,却与 7000 毫秒比较) - 节流改用 64 位毫秒时钟(bt_scan_now_ms / 函数指针钩子供宿主单测注入可控时钟),抽成纯 函数 bt_scan_throttle(now_ms);实际节流从 ~1.94 小时恢复到 7s。 测试(每条都有反向控制) - 新增 meow_launcherd/test_popen_tmo.c(已接入 build_launcher.sh test): 持续输出且永不退出的生产者 → 新循环 300ms 到点返回,旧循环 1500ms 内不返回; `yes`+64KB 上限 → 立刻 OVERFLOW;同一份 300KB 有界输入 → 旧循环照单全收 307200 字节、 新循环在上限处报 OVERFLOW;两线程交错读 → 旧 static 缓冲 A 读到 B 的内容,调用方缓冲各 读各的;收尸 313ms 有界且无残留子进程。 - meow_netmgrd/test_bt_ctl.c 增补:7s / 6.999s / 7.1s 边界(含走真实入口 bt_scan_on 的可控 时钟用例,秒/毫秒单位回归会在这里翻车);wedge 桩(scan 一律 exec sleep 60)下 scan off 611ms 有界返回且 ECHILD、scan on 的 worker 1200ms 窗口到期被整组收割、孙进程 全收(ECHILD)、超时进熔断并最终熔断拒绝 fork。 验证:netmgrd 宿主回归 79 PASS / 0 FAIL;test_bt_ctl ALL PASS;launcherd `build_launcher.sh test` ALL TESTS PASS(含新增用例);meow_netmgrd mipsel 交叉编译通过; audiod host 构建通过,test_audiod_host 的失败集合与改动前**逐条相同**(41 条,T8 SEGV 是 测试自身 strstr(q8,"rms=")+4 的既有缺陷,见 M59 审计 §5)。 仅源码:无二进制、未改 scripts/build/fw_content.conf。
This commit is contained in:
1 parent
1936cf7b91
commit
1ccb3c7a93
7 files changed
+839
-86
No files matched your search
@@ -59,6 +59,7 @@
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include "meow_ipc.h"
|
||||
#include "popen_tmo.h" /* M65 P1-2:有界读子进程 stdout(缓冲归调用方,无 static) */
|
||||
#include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */
|
||||
#include "vol_gain.h" /* M55:BT 写路径软件增益(与 DAC 同一锥度,全整数) */
|
||||
|
||||
@@ -570,11 +571,18 @@ static void ring_rebuild(void)
|
||||
}
|
||||
|
||||
/* ---- amixer 封装(popen 限时;无 amixer → 优雅失败) ---- */
|
||||
/* 读子进程 stdout(非阻塞+poll 限时)。收割**必须有界**:驱动 wedge 时 amixer 可能
|
||||
* 挂在内核 ioctl 里,无条件 waitpid 会把调用线程(路由线程持 g_mu / IO 线程)一起拖死
|
||||
* ——真机长流卡死的级联路径之一。策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms;
|
||||
* 仍不死(D 态)→ 放弃收割(泄漏一个子进程,换线程不卡死;已记日志)。 */
|
||||
static char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||
/* 读子进程 stdout 到**调用方给的缓冲**(非阻塞 + poll 限时;缓冲不再 static)。
|
||||
*
|
||||
* M65 P1-2(M59 审计 P1-2):旧实现是 `static char buf[4096]; … return buf;` —— 本进程有
|
||||
* 4 个线程(playback / route / io / vol),route_thread 在调 amixer_get(9,1) 之前**故意放掉
|
||||
* g_mu**(注释:amixer popen 不持 g_mu,防 wedge 级联),于是两个线程会同时写/解析同一个
|
||||
* 4KB 缓冲:route 线程可能读到被 playback 线程改写一半的 numid=9 回读值,误判「端口已武装」
|
||||
* 而跳过 apply_route —— 而这个函数的注释自己写着「FPGA 未烧 + enable 下开 PCM 会楔死整机」。
|
||||
* 修法:缓冲由调用方给(各线程各用自己的栈缓冲),函数内**无共享状态、不加锁** ——
|
||||
* M55 刚把音量写路径做成无锁,音频线程上不能拿锁。
|
||||
* 返回 out(非 NULL:命令起得来就把缓冲交回,可能是空串);pipe/fork 失败返回 NULL。
|
||||
* 输出超过 cap 时保留已读部分并记一条日志(旧实现是静默截断,这里行为不放松但不再无声)。 */
|
||||
static char *popen_read_tmo(int tmo_ms, const char *cmd, char *out, size_t cap)
|
||||
{
|
||||
int p[2];
|
||||
if (pipe(p) < 0) return NULL;
|
||||
@@ -583,39 +591,21 @@ static char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||
if (pid == 0) { dup2(p[1], 1); close(p[0]); close(p[1]); execl("/bin/sh", "sh", "-c", cmd, (char *)0); _exit(127); }
|
||||
close(p[1]);
|
||||
fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK);
|
||||
static char buf[4096];
|
||||
size_t off = 0;
|
||||
int remain = tmo_ms;
|
||||
while (remain > 0 && off < sizeof buf - 1) {
|
||||
struct pollfd pf = { p[0], POLLIN, 0 };
|
||||
int r = poll(&pf, 1, remain);
|
||||
if (r < 0) { if (errno == EINTR) continue; break; }
|
||||
if (r == 0) break;
|
||||
ssize_t rd = read(p[0], buf + off, sizeof buf - 1 - off);
|
||||
if (rd == 0) break;
|
||||
if (rd < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) continue; break; }
|
||||
off += (size_t)rd;
|
||||
}
|
||||
size_t len = 0;
|
||||
int rc = popen_tmo_read_all(p[0], tmo_ms, &out, &cap, 0, &len, 0);
|
||||
close(p[0]);
|
||||
int reaped = 0;
|
||||
for (int i = 0; i < 30; i++) { /* 300ms 优雅退出窗口 */
|
||||
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
if (!reaped) {
|
||||
logmsg("popen: child %ld did not exit in 300ms, SIGKILL (%s)", (long)pid, cmd);
|
||||
if (rc == POPEN_TMO_OVERFLOW)
|
||||
logmsg("popen: output over %zu bytes, kept the head only (%s)", cap, cmd);
|
||||
/* 收割**必须有界**:驱动 wedge 时 amixer 可能挂在内核 ioctl 里,无条件 waitpid 会把调用
|
||||
* 线程(路由线程持 g_mu / IO 线程)一起拖死 —— 真机长流卡死的级联路径之一。
|
||||
* 策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms;仍不死(D 态)→ 放弃收割
|
||||
* (泄漏一个子进程,换线程不卡死;已记日志)。 */
|
||||
if (waitpid(pid, NULL, WNOHANG) == 0) {
|
||||
kill(pid, SIGKILL);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
if (!reaped)
|
||||
if (!popen_tmo_reap_bounded(pid))
|
||||
logmsg("popen: child %ld unkillable (D-state, driver wedged?) — abandon reap", (long)pid);
|
||||
}
|
||||
buf[off] = 0;
|
||||
return buf;
|
||||
return out;
|
||||
}
|
||||
|
||||
static int amixer_set(int numid, int val, int card)
|
||||
@@ -623,7 +613,8 @@ static int amixer_set(int numid, int val, int card)
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd, "%s%s cset numid=%d %d >/dev/null 2>&1",
|
||||
g_amixer, (card ? " -c 0" : ""), numid, val);
|
||||
char *o = popen_read_tmo(1500, cmd);
|
||||
char ob[64];
|
||||
char *o = popen_read_tmo(1500, cmd, ob, sizeof ob);
|
||||
if (!o) return -1;
|
||||
/* 测试钩子(仅 env MEOW_AUDIO_AMIXER_TRACE=<path>):把每次 amixer cset 追加到文件,
|
||||
* 供宿主断言「音量双组写」PCM+DSD_VOLUME 是否成对下发(真机无 amixer 时不写/不报错)。 */
|
||||
@@ -636,7 +627,7 @@ static int amixer_set(int numid, int val, int card)
|
||||
close(tfd);
|
||||
}
|
||||
}
|
||||
/* popen_read_tmo 用的是静态缓冲,这里不 free(与 meow_route.c 同权衡) */
|
||||
/* 缓冲是上面的栈数组:无 static、无锁,多个线程各用各的(M65 P1-2) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -644,7 +635,8 @@ static int amixer_get(int numid, int card)
|
||||
{
|
||||
char cmd[160];
|
||||
snprintf(cmd, sizeof cmd, "%s%s cget numid=%d 2>/dev/null", g_amixer, (card ? " -c 0" : ""), numid);
|
||||
char *o = popen_read_tmo(1500, cmd);
|
||||
char ob[4096];
|
||||
char *o = popen_read_tmo(1500, cmd, ob, sizeof ob);
|
||||
if (!o) return -1;
|
||||
const char *ln = o;
|
||||
while (ln && *ln) {
|
||||
@@ -993,7 +985,8 @@ static int bt_collect_candidates(char (*cand)[18], int max)
|
||||
{
|
||||
if (!cand || max <= 0) return 0;
|
||||
int n = 0;
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||
char ob[4096];
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null", ob, sizeof ob);
|
||||
if (o && o[0]) {
|
||||
char m[18];
|
||||
if (bt_dev_detect_mac(o, m, sizeof m) == 0) {
|
||||
@@ -1001,7 +994,7 @@ static int bt_collect_candidates(char (*cand)[18], int max)
|
||||
logmsg("bt: candidate#%d %s (source=list-pcms endpoint)", n, m);
|
||||
}
|
||||
}
|
||||
o = popen_read_tmo(1200, "hcitool con 2>/dev/null");
|
||||
o = popen_read_tmo(1200, "hcitool con 2>/dev/null", ob, sizeof ob);
|
||||
if (o) {
|
||||
char macs[4][18];
|
||||
int k = bt_dev_acl_macs(o, macs, 4);
|
||||
@@ -1022,7 +1015,8 @@ static int bt_collect_candidates(char (*cand)[18], int max)
|
||||
static void bt_refresh_endpoint(void)
|
||||
{
|
||||
g_bt_pcm[0] = 0;
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null");
|
||||
char ob[4096];
|
||||
char *o = popen_read_tmo(1500, "bluealsa-cli list-pcms 2>/dev/null", ob, sizeof ob);
|
||||
if (!o) return;
|
||||
char path[128];
|
||||
if (bt_dev_pick_endpoint(o, g_bt_mac, path, sizeof path) == 0)
|
||||
@@ -1186,7 +1180,8 @@ static void bt_vol_full(void)
|
||||
if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no endpoint path for %s)", g_bt_mac); return; }
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm);
|
||||
char *r = popen_read_tmo(1500, cmd);
|
||||
char ob[64];
|
||||
char *r = popen_read_tmo(1500, cmd, ob, sizeof ob);
|
||||
logmsg("bt: avrcp volume set max(127) (%s)%s", pcm, r ? " ok" : " (exec failed; AVRCP still controls)");
|
||||
}
|
||||
|
||||
@@ -1222,7 +1217,8 @@ static void bt_read_used_codec(void)
|
||||
if (!g_bt_pcm[0]) return;
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof cmd, "%s info %s 2>/dev/null", BT_VOL_FULL_PCM, g_bt_pcm);
|
||||
char *o = popen_read_tmo(1500, cmd);
|
||||
char ob[4096];
|
||||
char *o = popen_read_tmo(1500, cmd, ob, sizeof ob);
|
||||
if (!o) return;
|
||||
char codec[16];
|
||||
if (bt_dev_info_codec(o, codec, sizeof codec) == 0) {
|
||||
|
||||
@@ -57,6 +57,15 @@ run_tests() {
|
||||
rc=1
|
||||
fi
|
||||
|
||||
# M65:子进程 stdout 有界读取(popen_tmo.h;launcherd/net.c 与 audiod 共用)
|
||||
# 含反向控制(旧循环在持续输出下永不返回 / 无输出上限 / static 缓冲串写)
|
||||
echo "== host test: test_popen_tmo =="
|
||||
if "$HOSTCC" -I. -Wall -Wextra test_popen_tmo.c -o "$TMPD/test_popen_tmo" -lpthread; then
|
||||
"$TMPD/test_popen_tmo" || rc=1
|
||||
else
|
||||
rc=1
|
||||
fi
|
||||
|
||||
if [ $rc -eq 0 ]; then echo "== ALL TESTS PASS =="; else echo "!! TESTS FAILED"; fi
|
||||
return $rc
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* net.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
|
||||
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
|
||||
#include "meow_launcher_internal.h"
|
||||
#include "popen_tmo.h" /* M65 P1-1:有界读子进程 stdout(deadline 在循环顶部 + 256KB 上限) */
|
||||
|
||||
|
||||
/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */
|
||||
@@ -76,7 +77,16 @@ int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 p
|
||||
* 避免绘制热点反复 spawn。 */
|
||||
|
||||
/* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。
|
||||
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时被 SIGKILL。 */
|
||||
* 返回 malloc 字符串(调用方 free);超时/失败返回 NULL;子进程超时/超限被 SIGKILL。
|
||||
*
|
||||
* M65 P1-1(M59 审计:'收益最大、改动最小'的一条):读循环搬进 popen_tmo.h,判据是
|
||||
* ① deadline 在**循环顶部**判 —— 旧实现只在 poll 超时分支里判,而持续输出的子进程
|
||||
* 让 poll 每次立刻返回,deadline 永远轮不到(审计 `yes` 复现:300ms 超时下 8s 不返回);
|
||||
* ② 输出**硬上限 256KB**,超限按失败返回 NULL(杀掉子进程),**不静默截断** ——
|
||||
* 静默截断会让上层以为拿到了完整输出;
|
||||
* ③ 收尸有界(300ms + SIGKILL + 300ms):旧实现 SIGKILL 之后是无条件 waitpid(…, 0),
|
||||
* 子进程若卡在 D 态照样把 UI 线程永久钉住。
|
||||
* 缓冲仍是本函数 malloc、调用方 free(调用面不变,14 个调用点零改动)。 */
|
||||
char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||
{
|
||||
int fds[2];
|
||||
@@ -96,34 +106,20 @@ char *popen_read_tmo(int tmo_ms, const char *cmd)
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
char *out = NULL; size_t cap = 0, len = 0;
|
||||
char tmp[256];
|
||||
int done = 0;
|
||||
unsigned long long deadline = now_ms() + tmo_ms;
|
||||
while (!done) {
|
||||
struct pollfd pfd = { fd, POLLIN, 0 };
|
||||
int pr = poll(&pfd, 1, 25);
|
||||
if (pr < 0) break;
|
||||
if (pr == 0) { if (now_ms() > deadline) break; continue; }
|
||||
ssize_t r = read(fd, tmp, sizeof tmp);
|
||||
if (r > 0) {
|
||||
if (len + (size_t)r + 1 > cap) {
|
||||
size_t nc = cap ? cap * 2 : 512;
|
||||
char *nn = realloc(out, nc);
|
||||
if (!nn) { free(out); out = NULL; break; }
|
||||
out = nn; cap = nc;
|
||||
}
|
||||
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
|
||||
} else if (r == 0) {
|
||||
done = 1;
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时)则 SIGKILL,防僵尸。 */
|
||||
int status = 0;
|
||||
pid_t rp = waitpid(pid, &status, WNOHANG);
|
||||
if (rp == 0) { kill(pid, SIGKILL); waitpid(pid, &status, 0); }
|
||||
int rc = popen_tmo_read_all(fd, tmo_ms, &out, &cap, POPEN_TMO_MAX, &len, 1);
|
||||
close(fd);
|
||||
if (rc == POPEN_TMO_OVERFLOW) {
|
||||
/* 超上限 = 子进程在无限吐(或输出异常大):按失败处理,绝不返回半截当完整。 */
|
||||
kill(pid, SIGKILL);
|
||||
free(out);
|
||||
out = NULL;
|
||||
}
|
||||
/* 子进程收尸:已退出则不杀;仍在跑(读到 EOF 前超时/超限)则 SIGKILL,防僵尸。 */
|
||||
if (waitpid(pid, NULL, WNOHANG) == 0) {
|
||||
kill(pid, SIGKILL);
|
||||
if (!popen_tmo_reap_bounded(pid))
|
||||
fprintf(stderr, "[net] popen: child %ld unkillable (D-state) — abandon reap\n", (long)pid);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/* popen_tmo.h — 子进程 stdout 的**有界**读取(M65;launcherd/net.c 与 audiod/meow_audiod.c 共用)。
|
||||
*
|
||||
* 三条不变量,都是 M59 内存/资源安全审计里的 P1(原文:
|
||||
* .tower/comms/inbox/20261112-survey-m59-memsafety-tower-survey-summary.md):
|
||||
*
|
||||
* 1. 超时判定在**循环顶部**(P1-1)。子进程持续吐数据时 poll 每次立刻返回,若只在
|
||||
* 「poll 超时」分支里看 deadline,deadline 就永远轮不到 → 调用线程永久挂死。
|
||||
* 审计用 `yes` 定向复现:300ms 超时下 8s 不返回,RSS 236MB→397MB→657MB。
|
||||
* 2. 输出有**硬上限**,超限**报错**而不是静默截断(P1-1 / P2-7)。静默截断会让上层
|
||||
* 以为拿到了完整输出 —— 那是另一种假成功。超限返回 POPEN_TMO_OVERFLOW,调用方必须
|
||||
* 按失败处理并杀掉子进程。
|
||||
* 3. **无 static 状态**(P1-2)。缓冲一律由调用方提供或由本函数用 *buf 持有,多个线程
|
||||
* 各用各的缓冲;旧实现 `static char buf[4096]; … return buf;` 被 audiod 的 4 个线程
|
||||
* 并发使用 → amixer cget 读回值可能被另一个线程改写一半。
|
||||
*
|
||||
* 只依赖 POSIX,便于宿主单测直接 include(patch_v2/meow_launcherd/test_popen_tmo.c)。
|
||||
*/
|
||||
#ifndef MEOW_POPEN_TMO_H
|
||||
#define MEOW_POPEN_TMO_H
|
||||
|
||||
#include <poll.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <signal.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
/* 输出硬上限:256 KB。设备总内存 56 MB;最大的正常输出(wpa_cli scan_results /
|
||||
* bluetoothctl devices)也就几 KB。越过这个量级只可能是子进程在无限吐。 */
|
||||
#define POPEN_TMO_MAX (256 * 1024)
|
||||
|
||||
enum {
|
||||
POPEN_TMO_EOF = 0, /* 读到 EOF:*len 是完整输出 */
|
||||
POPEN_TMO_DEADLINE = 1, /* 到 deadline:*len 是已读到的部分 */
|
||||
POPEN_TMO_OVERFLOW = 2, /* 越过硬上限:*len 是上限内已读到的部分(调用方按失败处理) */
|
||||
POPEN_TMO_ERROR = 3 /* poll/read 硬错误;*len 是已读到的部分 */
|
||||
};
|
||||
|
||||
static long long popen_tmo_now_ms(void)
|
||||
{
|
||||
struct timespec tp;
|
||||
clock_gettime(CLOCK_MONOTONIC, &tp);
|
||||
return (long long)tp.tv_sec * 1000LL + tp.tv_nsec / 1000000LL;
|
||||
}
|
||||
|
||||
/* 从 fd 读子进程 stdout,最多等 tmo_ms 毫秒。
|
||||
* grow=0:定长缓冲 —— *buf 由调用方给,*cap 是容量,最多保留 *cap-1 字节;
|
||||
* grow=1:*buf 由本函数 realloc 增长,*cap 是当前容量,总量不得超过 hard_max;
|
||||
* grow=0 且 *cap==0(或 *buf==NULL):**排空丢弃**模式,只保证子进程不因 pipe 满而阻塞。
|
||||
* hard_max==0 时按 POPEN_TMO_MAX 处理。
|
||||
* 返回 POPEN_TMO_*;*len 是已保留字节数;非丢弃模式下 *buf 末尾总有 NUL。 */
|
||||
static int popen_tmo_read_all(int fd, int tmo_ms, char **buf, size_t *cap,
|
||||
size_t hard_max, size_t *len, int grow)
|
||||
{
|
||||
size_t off = 0;
|
||||
int rc = POPEN_TMO_EOF;
|
||||
int discard = (!grow && (!buf || !*buf || *cap == 0));
|
||||
if (hard_max == 0) hard_max = POPEN_TMO_MAX;
|
||||
if (grow) { *buf = NULL; *cap = 0; }
|
||||
else if (!discard && *buf) (*buf)[0] = 0;
|
||||
long long deadline = popen_tmo_now_ms() + (tmo_ms > 0 ? tmo_ms : 1);
|
||||
char chunk[512];
|
||||
|
||||
for (;;) {
|
||||
/* ① 循环顶部判超时:子进程一直吐数据时也必须到期返回(P1-1 的核心)。 */
|
||||
if (popen_tmo_now_ms() >= deadline) { rc = POPEN_TMO_DEADLINE; break; }
|
||||
long long remain = deadline - popen_tmo_now_ms();
|
||||
struct pollfd pf = { fd, POLLIN, 0 };
|
||||
int wait = (int)(remain > 25 ? 25 : (remain > 0 ? remain : 1)); /* 25ms 一片,保持可打断 */
|
||||
int pr = poll(&pf, 1, wait);
|
||||
if (pr < 0) { if (errno == EINTR) continue; rc = POPEN_TMO_ERROR; break; }
|
||||
if (pr == 0) continue; /* 这一小段没数据:回顶部看 deadline */
|
||||
ssize_t rd = read(fd, chunk, sizeof chunk);
|
||||
if (rd < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) continue;
|
||||
rc = POPEN_TMO_ERROR;
|
||||
break;
|
||||
}
|
||||
if (rd == 0) { rc = POPEN_TMO_EOF; break; }
|
||||
size_t n = (size_t)rd;
|
||||
if (discard) continue; /* 丢弃模式:只排水 */
|
||||
if (grow) {
|
||||
if (off + n + 1 > hard_max) { rc = POPEN_TMO_OVERFLOW; break; } /* ② 硬上限 */
|
||||
if (off + n + 1 > *cap) {
|
||||
size_t nc = *cap ? *cap : 512;
|
||||
while (nc < off + n + 1) nc *= 2;
|
||||
if (nc > hard_max) nc = hard_max;
|
||||
char *nb = realloc(*buf, nc);
|
||||
if (!nb) { rc = POPEN_TMO_ERROR; break; }
|
||||
*buf = nb; *cap = nc;
|
||||
}
|
||||
memcpy(*buf + off, chunk, n);
|
||||
off += n;
|
||||
(*buf)[off] = 0;
|
||||
} else {
|
||||
size_t room = *cap - 1 - off;
|
||||
if (n > room) { /* ② 定长:写满即超限,不静默截断 */
|
||||
memcpy(*buf + off, chunk, room);
|
||||
off += room;
|
||||
(*buf)[off] = 0;
|
||||
rc = POPEN_TMO_OVERFLOW;
|
||||
break;
|
||||
}
|
||||
memcpy(*buf + off, chunk, n);
|
||||
off += n;
|
||||
(*buf)[off] = 0;
|
||||
}
|
||||
}
|
||||
if (len) *len = off;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 有界收尸:先 WNOHANG 等 300ms;不死 SIGKILL 再等 300ms;仍不死(D 态,驱动 wedge)
|
||||
* 放弃收割(泄漏一个子进程换调用线程不被拖死,与 audiod/netmgrd 同权衡)。返回 1=已收。 */
|
||||
static int popen_tmo_reap_bounded(pid_t pid)
|
||||
{
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (waitpid(pid, NULL, WNOHANG) == pid) return 1;
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
kill(pid, SIGKILL);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (waitpid(pid, NULL, WNOHANG) == pid) return 1;
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif /* MEOW_POPEN_TMO_H */
|
||||
@@ -0,0 +1,340 @@
|
||||
/* test_popen_tmo.c — M65 P1-1 / P1-2 宿主验证(本程序非生产代码)。
|
||||
*
|
||||
* 被测对象是 popen_tmo.h(生产代码 launcherd/net.c 的 popen_read_tmo 与 audiod/meow_audiod.c
|
||||
* 的 popen_read_tmo 都只用这一个读循环)。每组断言都配**反向控制**:把 M59 审计里的旧实现
|
||||
* 原样抄进来(old_read / old_static_read_cmd),证明旧代码在同样输入下确实失败 ——
|
||||
* 只看「新代码返回了」不算证明,必须同时证明「旧代码不会返回」。
|
||||
*
|
||||
* 反向控制 A(P1-1 永不超时):持续输出但**非常慢**的生产者(2B/20ms)。旧循环的 deadline
|
||||
* 只在 poll 超时分支检查 → 永不触发;新循环在**顶部**检查 → 到期就返回。
|
||||
* 用慢生产者而不是 `yes`:审计里 `yes` 版 5s 就涨到 657MB(设备 56MB),在这里会打死宿主。
|
||||
* 反向控制 B(P1-1 无上限):让子进程输出 300KB(有界,宿主安全)。旧循环照单全收
|
||||
* (>256KB 也不管),新实现在硬上限处**主动**返回 OVERFLOW —— 上限确实是新加的。
|
||||
* 反向控制 C(P1-2 共享缓冲):两个线程用旧式 static 缓冲交错读,A 读到的是 B 的内容
|
||||
* (这就是 audiod 里 amixer cget numid=9 被改写一半的模型);同样的交错用调用方缓冲
|
||||
* 则各读各的。
|
||||
*
|
||||
* 编译(在 patch_v2/meow_launcherd 下):cc -Wall -Wextra test_popen_tmo.c -o test_popen_tmo -lpthread
|
||||
* 运行:./test_popen_tmo
|
||||
*/
|
||||
#include "popen_tmo.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond) do { if (!(cond)) { printf(" FAIL: %s (line %d)\n", #cond, __LINE__); g_fail++; } \
|
||||
else { printf(" ok : %s\n", #cond); } } while (0)
|
||||
|
||||
/* 持续输出且**永不退出**的生产者:先一次性灌 32KB 把 pipe 填满(保证 poll 每次都有数据,
|
||||
* 旧循环因此永远进不到 deadline 分支),之后 1B/10ms 慢慢滴(内存涨不起来,反向控制可以在
|
||||
* 宿主上安全地跑到被外部杀掉)。注意不能用 `yes`:审计里 5s 就 657MB,宿主会被打死。 */
|
||||
static char g_slow_cmd[512];
|
||||
static void build_slow_producer(void)
|
||||
{
|
||||
char chunk[129];
|
||||
memset(chunk, 'x', 128);
|
||||
chunk[128] = 0;
|
||||
snprintf(g_slow_cmd, sizeof g_slow_cmd,
|
||||
"c='%s'; i=0; while [ $i -lt 256 ]; do printf '%%s' \"$c\"; i=$((i+1)); done; "
|
||||
"while :; do printf 'y'; sleep 0.01; done", chunk);
|
||||
}
|
||||
|
||||
/* fork 一个生产者,返回读端 fd(非阻塞),*pid 是子进程。失败返回 -1。 */
|
||||
static int spawn_cmd(const char *cmd, pid_t *pid)
|
||||
{
|
||||
int p[2];
|
||||
if (pipe(p) != 0) return -1;
|
||||
pid_t c = fork();
|
||||
if (c < 0) { close(p[0]); close(p[1]); return -1; }
|
||||
if (c == 0) {
|
||||
close(p[0]);
|
||||
dup2(p[1], 1);
|
||||
close(p[1]);
|
||||
execl("/bin/sh", "sh", "-c", cmd, (char *)0);
|
||||
_exit(127);
|
||||
}
|
||||
close(p[1]);
|
||||
fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK);
|
||||
*pid = c;
|
||||
return p[0];
|
||||
}
|
||||
|
||||
static void kill_reap(pid_t pid)
|
||||
{
|
||||
if (pid <= 0) return;
|
||||
kill(pid, SIGKILL);
|
||||
(void)popen_tmo_reap_bounded(pid);
|
||||
}
|
||||
|
||||
/* ---------------- 反向控制:旧 launcherd/net.c 的读循环(原文,仅 now_ms→popen_tmo_now_ms) ---------------- */
|
||||
static char *old_read(int fd, int tmo_ms)
|
||||
{
|
||||
char *out = NULL; size_t cap = 0, len = 0;
|
||||
char tmp[256];
|
||||
int done = 0;
|
||||
long long deadline = popen_tmo_now_ms() + tmo_ms;
|
||||
while (!done) {
|
||||
struct pollfd pfd = { fd, POLLIN, 0 };
|
||||
int pr = poll(&pfd, 1, 25);
|
||||
if (pr < 0) break;
|
||||
if (pr == 0) { if (popen_tmo_now_ms() > deadline) break; continue; } /* ← 只在 poll 超时分支看 deadline */
|
||||
ssize_t r = read(fd, tmp, sizeof tmp);
|
||||
if (r > 0) {
|
||||
if (len + (size_t)r + 1 > cap) {
|
||||
size_t nc = cap ? cap * 2 : 512; /* ← realloc 没有字节上限 */
|
||||
char *nn = realloc(out, nc);
|
||||
if (!nn) { free(out); return NULL; }
|
||||
out = nn; cap = nc;
|
||||
}
|
||||
memcpy(out + len, tmp, (size_t)r); len += (size_t)r; out[len] = 0;
|
||||
} else if (r == 0) {
|
||||
done = 1;
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
done = 1;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* 在子进程里跑旧循环(旧循环可能吃很多内存,隔一层进程隔离)。返回 1=wait_ms 内自己退出,
|
||||
* 0=超时仍在跑(已被 kill+reap)。*out_len = 旧循环读回的字节数(-1=返回 NULL/没跑完)。 */
|
||||
static int old_loop_in_child(const char *cmd, int tmo_ms, int wait_ms, long *out_len)
|
||||
{
|
||||
int rp[2];
|
||||
if (pipe(rp) != 0) return -1;
|
||||
fflush(NULL);
|
||||
pid_t c = fork();
|
||||
if (c < 0) { close(rp[0]); close(rp[1]); return -1; }
|
||||
if (c == 0) {
|
||||
close(rp[0]);
|
||||
pid_t pp = 0;
|
||||
int fd = spawn_cmd(cmd, &pp);
|
||||
char *o = (fd >= 0) ? old_read(fd, tmo_ms) : NULL;
|
||||
if (fd >= 0) close(fd);
|
||||
if (pp > 0) kill_reap(pp);
|
||||
long l = o ? (long)strlen(o) : -1;
|
||||
if (write(rp[1], &l, sizeof l) < 0) { }
|
||||
_exit(o ? 0 : 7);
|
||||
}
|
||||
close(rp[1]);
|
||||
int st = 0, finished = 0;
|
||||
for (int i = 0; i < wait_ms / 10; i++) {
|
||||
if (waitpid(c, &st, WNOHANG) == c) { finished = 1; break; }
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
if (!finished) kill_reap(c);
|
||||
long l = -1;
|
||||
if (read(rp[0], &l, sizeof l) != (ssize_t)sizeof l) l = -1;
|
||||
close(rp[0]);
|
||||
if (out_len) *out_len = l;
|
||||
return finished;
|
||||
}
|
||||
|
||||
/* ---------------- T1:deadline 在循环顶部(P1-1 主案) ---------------- */
|
||||
static void t1_deadline_at_top(void)
|
||||
{
|
||||
printf("== T1 持续输出子进程:deadline 必须在循环顶部(tmo=300ms) ==\n");
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd(g_slow_cmd, &pid);
|
||||
CHECK(fd >= 0);
|
||||
char *buf = NULL; size_t cap = 0, len = 0;
|
||||
long long t0 = popen_tmo_now_ms();
|
||||
int rc = popen_tmo_read_all(fd, 300, &buf, &cap, POPEN_TMO_MAX, &len, 1);
|
||||
long long el = popen_tmo_now_ms() - t0;
|
||||
close(fd);
|
||||
kill_reap(pid);
|
||||
printf(" [info] 新实现:rc=%d len=%zu 滞回=%lldms\n", rc, len, el);
|
||||
CHECK(rc == POPEN_TMO_DEADLINE);
|
||||
CHECK(el >= 300);
|
||||
CHECK(el < 1200); /* 到期就回,不再等子进程 EOF */
|
||||
CHECK(len < POPEN_TMO_MAX); /* 有界:32KB 灌入 + 滴流,不会无限涨 */
|
||||
CHECK(buf != NULL && strlen(buf) == len);
|
||||
free(buf);
|
||||
|
||||
/* 反向控制:同一个输入,旧循环永不返回。 */
|
||||
long oldlen = 0;
|
||||
int finished = old_loop_in_child(g_slow_cmd, 300, 1500, &oldlen);
|
||||
printf(" [info] 旧实现:1500ms 内%s退出(永不返回=BUG)\n", finished ? "已" : "未");
|
||||
CHECK(finished == 0); /* ← 反向控制:旧代码不返回 */
|
||||
}
|
||||
|
||||
/* ---------------- T2:输出硬上限(P1-1 后半 / P2-7) ---------------- */
|
||||
static void t2_hard_cap(void)
|
||||
{
|
||||
printf("== T2 无限输出:grow 模式 64KB 硬上限 / 定长模式写满即超限 ==\n");
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("yes", &pid);
|
||||
CHECK(fd >= 0);
|
||||
char *buf = NULL; size_t cap = 0, len = 0;
|
||||
long long t0 = popen_tmo_now_ms();
|
||||
int rc = popen_tmo_read_all(fd, 5000, &buf, &cap, 64 * 1024, &len, 1); /* tmo 远大于触发点 */
|
||||
long long el = popen_tmo_now_ms() - t0;
|
||||
close(fd);
|
||||
kill_reap(pid);
|
||||
printf(" [info] grow: rc=%d len=%zu cap=%zu 滞回=%lldms\n", rc, len, cap, el);
|
||||
CHECK(rc == POPEN_TMO_OVERFLOW); /* 超限**报错**,不是静默截断 */
|
||||
CHECK(len > 0 && len < 64 * 1024);
|
||||
CHECK(cap <= 64 * 1024);
|
||||
CHECK(el < 1500); /* 是「上限触发」而不是「超时触发」 */
|
||||
free(buf);
|
||||
|
||||
pid_t pid2 = 0;
|
||||
int fd2 = spawn_cmd("yes", &pid2);
|
||||
CHECK(fd2 >= 0);
|
||||
char fixed[1024];
|
||||
size_t len2 = 0;
|
||||
char *p = fixed; size_t cp = sizeof fixed;
|
||||
rc = popen_tmo_read_all(fd2, 5000, &p, &cp, 0, &len2, 0);
|
||||
close(fd2);
|
||||
kill_reap(pid2);
|
||||
printf(" [info] fixed: rc=%d len=%zu\n", rc, len2);
|
||||
CHECK(rc == POPEN_TMO_OVERFLOW);
|
||||
CHECK(len2 == sizeof fixed - 1);
|
||||
CHECK(fixed[sizeof fixed - 1] == 0); /* 总是 NUL 结尾,解析不会越界 */
|
||||
|
||||
/* 反向控制:同一份 300KB 有界输出,旧循环照单全收(>256KB 也返回),没有上限。 */
|
||||
long oldlen = 0;
|
||||
int finished = old_loop_in_child("yes | head -c 307200", 5000, 8000, &oldlen);
|
||||
printf(" [info] 旧实现读到 %ld 字节(上限 %d)\n", oldlen, POPEN_TMO_MAX);
|
||||
CHECK(finished == 1);
|
||||
CHECK(oldlen > POPEN_TMO_MAX); /* ← 反向控制:旧实现没有上限 */
|
||||
/* 同一输入走新实现的**生产上限**(256KB):必须报 OVERFLOW 而不是带回 300KB。 */
|
||||
pid_t pid3 = 0;
|
||||
int fd3 = spawn_cmd("yes | head -c 307200", &pid3);
|
||||
CHECK(fd3 >= 0);
|
||||
char *buf3 = NULL; size_t cap3 = 0, len3 = 0;
|
||||
rc = popen_tmo_read_all(fd3, 5000, &buf3, &cap3, POPEN_TMO_MAX, &len3, 1);
|
||||
close(fd3);
|
||||
kill_reap(pid3);
|
||||
printf(" [info] 新实现同输入:rc=%d len=%zu\n", rc, len3);
|
||||
CHECK(rc == POPEN_TMO_OVERFLOW);
|
||||
CHECK(len3 < POPEN_TMO_MAX);
|
||||
free(buf3);
|
||||
}
|
||||
|
||||
/* ---------------- T3:调用方缓冲 vs static 缓冲(P1-2) ---------------- */
|
||||
static pthread_barrier_t g_b1, g_b2;
|
||||
static char g_shared[4096]; /* 旧实现的 static 缓冲(模型) */
|
||||
static char g_seen_old[64], g_seen_new[64];
|
||||
|
||||
/* 旧 audiod 形态:static 缓冲 + return 指针。 */
|
||||
static char *old_static_read_cmd(const char *cmd)
|
||||
{
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd(cmd, &pid);
|
||||
if (fd < 0) return NULL;
|
||||
char *b = g_shared; size_t cap = sizeof g_shared, len = 0;
|
||||
popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0);
|
||||
close(fd);
|
||||
kill_reap(pid);
|
||||
return g_shared;
|
||||
}
|
||||
|
||||
static void *thr_old_a(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char *o = old_static_read_cmd("echo AAAAAAAAAA");
|
||||
pthread_barrier_wait(&g_b1);
|
||||
pthread_barrier_wait(&g_b2); /* 等 B 读完(旧代码里这就是「A 还没解析」的窗口) */
|
||||
snprintf(g_seen_old, sizeof g_seen_old, "%s", o ? o : "");
|
||||
return NULL;
|
||||
}
|
||||
static void *thr_old_b(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
pthread_barrier_wait(&g_b1);
|
||||
old_static_read_cmd("echo BBBBBBBBBB");
|
||||
pthread_barrier_wait(&g_b2);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void *thr_new_a(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
char buf[256];
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("echo AAAAAAAAAA", &pid);
|
||||
size_t len = 0;
|
||||
char *b = buf; size_t cap = sizeof buf;
|
||||
if (fd >= 0) { popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); close(fd); kill_reap(pid); }
|
||||
pthread_barrier_wait(&g_b1);
|
||||
pthread_barrier_wait(&g_b2);
|
||||
snprintf(g_seen_new, sizeof g_seen_new, "%s", buf);
|
||||
return NULL;
|
||||
}
|
||||
static void *thr_new_b(void *arg)
|
||||
{
|
||||
(void)arg;
|
||||
pthread_barrier_wait(&g_b1);
|
||||
char buf[256];
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("echo BBBBBBBBBB", &pid);
|
||||
size_t len = 0;
|
||||
char *b = buf; size_t cap = sizeof buf;
|
||||
if (fd >= 0) { popen_tmo_read_all(fd, 1500, &b, &cap, 0, &len, 0); close(fd); kill_reap(pid); }
|
||||
pthread_barrier_wait(&g_b2);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void t3_caller_buffer(void)
|
||||
{
|
||||
printf("== T3 并发读:旧 static 缓冲会串(P1-2),调用方缓冲各归各 ==\n");
|
||||
pthread_t a, b;
|
||||
pthread_barrier_init(&g_b1, NULL, 2);
|
||||
pthread_barrier_init(&g_b2, NULL, 2);
|
||||
g_seen_old[0] = g_seen_new[0] = 0;
|
||||
|
||||
pthread_create(&a, NULL, thr_old_a, NULL);
|
||||
pthread_create(&b, NULL, thr_old_b, NULL);
|
||||
pthread_join(a, NULL); pthread_join(b, NULL);
|
||||
printf(" [info] 旧实现 A 读到:%s\n", g_seen_old);
|
||||
CHECK(strncmp(g_seen_old, "AAAAAAAAAA", 10) != 0); /* ← 反向控制:A 读到的是 B 的内容 */
|
||||
|
||||
pthread_barrier_destroy(&g_b1);
|
||||
pthread_barrier_destroy(&g_b2);
|
||||
pthread_barrier_init(&g_b1, NULL, 2);
|
||||
pthread_barrier_init(&g_b2, NULL, 2);
|
||||
pthread_create(&a, NULL, thr_new_a, NULL);
|
||||
pthread_create(&b, NULL, thr_new_b, NULL);
|
||||
pthread_join(a, NULL); pthread_join(b, NULL);
|
||||
printf(" [info] 新实现 A 读到:%s", g_seen_new);
|
||||
CHECK(strncmp(g_seen_new, "AAAAAAAAAA", 10) == 0); /* 调用方缓冲:不串 */
|
||||
pthread_barrier_destroy(&g_b1);
|
||||
pthread_barrier_destroy(&g_b2);
|
||||
}
|
||||
|
||||
/* ---------------- T4:有界收尸 ---------------- */
|
||||
static void t4_reap_bounded(void)
|
||||
{
|
||||
printf("== T4 收尸有界:挂住的子进程 SIGKILL 后 ≤700ms 收完 ==\n");
|
||||
pid_t pid = 0;
|
||||
int fd = spawn_cmd("sleep 60", &pid);
|
||||
CHECK(fd >= 0);
|
||||
long long t0 = popen_tmo_now_ms();
|
||||
int ok = popen_tmo_reap_bounded(pid);
|
||||
long long el = popen_tmo_now_ms() - t0;
|
||||
close(fd);
|
||||
printf(" [info] reap_bounded=%d 滞回=%lldms\n", ok, el);
|
||||
CHECK(ok == 1);
|
||||
CHECK(el < 700);
|
||||
errno = 0;
|
||||
CHECK(waitpid(-1, NULL, WNOHANG) == -1 && errno == ECHILD); /* 没有残留子进程 */
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
setbuf(stdout, NULL);
|
||||
build_slow_producer();
|
||||
t1_deadline_at_top();
|
||||
t2_hard_cap();
|
||||
t3_caller_buffer();
|
||||
t4_reap_bounded();
|
||||
if (g_fail) { printf("\nPOPEN_TMO TEST: %d FAIL\n", g_fail); return 1; }
|
||||
printf("\nPOPEN_TMO TEST: ALL PASS\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -114,6 +114,18 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
|
||||
* 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */
|
||||
#define BT_TMO_STREAK_MAX 3
|
||||
#define BT_TMO_COOLDOWN_SEC 30
|
||||
/* M65 P1-3/P2-1:「附近设备」扫描(bt scan on/off)也纳入同一套有界收割 + 熔断 + 在飞计数。
|
||||
* 旧实现是 `system("... | bluetoothctl > cache &")`:不收割、不计数、不走熔断,bluetoothctl
|
||||
* 一 wedge 就永久挂住单线程主循环并留下常驻子进程(M50 现场清过 55 个)。 */
|
||||
#define BT_SCAN_MIN_GAP_MS 7000 /* 两次 scan on 的最小间隔(**毫秒**;P2-1:旧实现拿秒值比 7000 → 实际 7000s) */
|
||||
#ifndef BT_SCAN_TMO_MS
|
||||
#define BT_SCAN_TMO_MS 6500 /* 扫描窗口上限:pipeline 自带 ~5s(sleep 1 + dwell + 收尾) */
|
||||
#endif
|
||||
#ifndef BT_SCAN_DWELL_S
|
||||
#define BT_SCAN_DWELL_S 4 /* scan 时长;宿主单测缩窗用 */
|
||||
#endif
|
||||
#define BT_SCAN_OFF_TMO_MS 600 /* `scan off` 单命令上限(同 BT_ACT_SHORT_MS 量级) */
|
||||
#define BT_SCAN_EXIT_TIMEOUT 124 /* scan worker 因超时退出的退出码(与 timeout(1) 同约定) */
|
||||
#define IFACE_MAX 16
|
||||
#define RFKILL_MAX 8
|
||||
#define DNS_MAX 8
|
||||
@@ -1437,24 +1449,147 @@ static void bt_devices_text(char *buf, int cap)
|
||||
|
||||
/* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。
|
||||
* M49:改用 g_bluetoothctl(原来硬编码 "bluetoothctl",MEOW_BLUETOOTHCTL 桩对 scan 无效,
|
||||
* 宿主测试没法覆盖扫描链路)。 */
|
||||
static long g_bt_scan_ms = 0;
|
||||
static void bt_scan_on(void)
|
||||
* 宿主测试没法覆盖扫描链路)。
|
||||
* M65 P1-3:scan 不再走 `system()`。
|
||||
* - 旧 `scan on` 是 `system("(…) | bluetoothctl > cache &")`:fork 出去的 pipeline 不收割、
|
||||
* 不计在飞数量、不走熔断;bluetoothctl wedge 一次就留一个常驻子进程(M50 现场 55 个的形态)。
|
||||
* - 旧 `scan off` 是**无超时** `system()`:守护是单线程 poll 循环,卡住即 DNS 代理/status 全停。
|
||||
* 现在:scan on 由本进程 fork 一个 scan worker(setsid 自成进程组),worker 内部用同文件的
|
||||
* run_cmd_ex(超时 + 输出上限)跑同一条 pipeline;父进程只记账,收割/超时强杀由 bt_scan_reap()
|
||||
* 在主循环每拍完成。为什么不在主循环里直接 run_cmd_ex:pipeline 本身要 5s(sleep 1 + dwell),
|
||||
* 同步跑等于每 7s 把 DNS 代理停 5s —— 那是另一种「挂死」,所以交给受管的 worker。
|
||||
* scan off 是快命令,直接在主循环里 run_cmd_ex(BT_SCAN_OFF_TMO_MS 上限)。
|
||||
* M65 P2-1:节流按**毫秒**比较(旧实现拿 mono_now() 的秒值比 7000 → 实际 7000 秒 ≈ 1.94 小时)。 */
|
||||
static long long g_bt_scan_ms = 0; /* 上次放行时刻(毫秒) */
|
||||
/* 64 位毫秒时钟:mono_ms() 是 long(mipsel ILP32 下 32 位,开机 ~24.8 天后溢出),
|
||||
* 节流不该跟着溢出,故这里单独取一份 64 位时间;函数指针是**宿主单测注入可控时钟**的钩子
|
||||
* (test_bt_ctl.c 用它验 7s / 7.1s 边界 —— 秒/毫秒单位回归唯一能防住的地方)。 */
|
||||
static long long bt_scan_now_ms(void)
|
||||
{
|
||||
long t = mono_now();
|
||||
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return;
|
||||
g_bt_scan_ms = t;
|
||||
char cmd[320];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"(sleep 1; echo 'scan on'; sleep 4; echo 'scan off'; echo 'devices'; echo 'quit') | "
|
||||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s > '%s' 2>&1 &", g_bluetoothctl, g_bt_scan_cache);
|
||||
if (system(cmd) != 0) svc_log("bt scan on: spawn failed (%s)", g_bt_scan_cache);
|
||||
struct timespec tp;
|
||||
clock_gettime(CLOCK_MONOTONIC, &tp);
|
||||
return (long long)tp.tv_sec * 1000LL + tp.tv_nsec / 1000000LL;
|
||||
}
|
||||
static void bt_scan_off(void)
|
||||
static long long (*g_bt_scan_clock_ms)(void) = bt_scan_now_ms;
|
||||
static pid_t g_bt_scan_pid = 0; /* 在飞 scan worker(0=空闲;上限 1 个) */
|
||||
static long long g_bt_scan_deadline = 0; /* worker 的最后收割时刻(毫秒) */
|
||||
static int bt_circuit_open(void); /* 定义在后(熔断器只服务 BT 写操作) */
|
||||
static void bt_circuit_note(runcmd_status_t st);
|
||||
static int g_bt_tmo_streak; /* 熔断器状态,定义在后(仅用于日志) */
|
||||
|
||||
/* 节流判定(纯函数,便于宿主单测用可控时钟验单位):1=放行并记账,0=7s 内不再扫。
|
||||
* 入参**必须**是毫秒(P2-1)。 */
|
||||
static int bt_scan_throttle(long long now_ms)
|
||||
{
|
||||
if (g_bt_scan_ms && now_ms - g_bt_scan_ms < BT_SCAN_MIN_GAP_MS) return 0;
|
||||
g_bt_scan_ms = now_ms;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* scan worker 主体(fork 出来的子进程):自成进程组,stdout/stderr 丢 /dev/null,
|
||||
* 用 run_cmd_ex 跑 pipeline(超时/输出上限都在那里),超时以 BT_SCAN_EXIT_TIMEOUT 退出。 */
|
||||
static void bt_scan_worker(const char *cmd)
|
||||
{
|
||||
setsid();
|
||||
int dn = open("/dev/null", O_WRONLY);
|
||||
if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); if (dn > 2) close(dn); }
|
||||
/* 别攥着守护的监听 socket / DNS socket:否则守护重启时端口还被子进程占着。 */
|
||||
long mx = sysconf(_SC_OPEN_MAX);
|
||||
if (mx < 0 || mx > 1024) mx = 1024;
|
||||
for (int fd = 3; fd < (int)mx; fd++) close(fd);
|
||||
char sink[64];
|
||||
runcmd_res_t rr;
|
||||
run_cmd_ex(sink, sizeof sink, BT_SCAN_TMO_MS, cmd, &rr);
|
||||
_exit(rr.status == RUNCMD_TIMEOUT ? BT_SCAN_EXIT_TIMEOUT : 0);
|
||||
}
|
||||
|
||||
/* 收割 scan worker(主循环每拍 + 每次 scan on 之前调用;守护单线程,无需加锁)。
|
||||
* 已退出 → 记账(超时进既有熔断);过 deadline 还活着 → 杀整个进程组再有界收尸 300ms;
|
||||
* 仍不死(D 态)放弃收割但**不占槽位**(下次扫描不会被永久挡住),并入熔断计数。 */
|
||||
static void bt_scan_reap(void)
|
||||
{
|
||||
if (g_bt_scan_pid <= 0) return;
|
||||
int st = 0;
|
||||
pid_t rp = waitpid(g_bt_scan_pid, &st, WNOHANG);
|
||||
if (rp < 0) {
|
||||
if (errno == EINTR) return; /* 信号打断:下一拍再来 */
|
||||
g_bt_scan_pid = 0; g_bt_scan_deadline = 0; /* ECHILD:已被收走 */
|
||||
return;
|
||||
}
|
||||
int timed_out = 0;
|
||||
if (rp == 0) {
|
||||
if (bt_scan_now_ms() < g_bt_scan_deadline) return; /* 还在窗口内,下一拍再看 */
|
||||
timed_out = 1;
|
||||
/* 整组杀:worker + 它 fork 的 sh + pipeline 一起收,避免留下常驻 bluetoothctl。 */
|
||||
kill(-g_bt_scan_pid, SIGKILL);
|
||||
kill(g_bt_scan_pid, SIGKILL);
|
||||
for (int i = 0; i < 30; i++) {
|
||||
if (waitpid(g_bt_scan_pid, &st, WNOHANG) == g_bt_scan_pid) { rp = g_bt_scan_pid; break; }
|
||||
struct timespec ts = { 0, 10 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
if (rp != g_bt_scan_pid) {
|
||||
svc_log("bt scan: worker %ld unkillable in 300ms (D-state?) — abandon reap", (long)g_bt_scan_pid);
|
||||
g_bt_scan_pid = 0;
|
||||
g_bt_scan_deadline = 0;
|
||||
bt_circuit_note(RUNCMD_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!timed_out && WIFEXITED(st) && WEXITSTATUS(st) == BT_SCAN_EXIT_TIMEOUT) timed_out = 1;
|
||||
if (timed_out) {
|
||||
/* worker 自己退出(exit 124)也**再扫一遍进程组**:pipeline 的 bluetoothctl 是孙进程
|
||||
* (sh 的子进程),run_cmd_ex 只杀它的直接子进程,孙进程会活到自己的 sleep 结束 ——
|
||||
* 那正是「55 个残留 bluetoothctl」的另一半成因。 */
|
||||
kill(-g_bt_scan_pid, SIGKILL);
|
||||
svc_log("bt scan: worker %ld timed out (streak=%d)", (long)g_bt_scan_pid, g_bt_tmo_streak + 1);
|
||||
bt_circuit_note(RUNCMD_TIMEOUT);
|
||||
}
|
||||
g_bt_scan_pid = 0;
|
||||
g_bt_scan_deadline = 0;
|
||||
}
|
||||
|
||||
/* 返回 1=已起扫描 / 0=节流或上一轮未完(跳过)/ -1=熔断或起不来。 */
|
||||
static int bt_scan_on(void)
|
||||
{
|
||||
bt_scan_reap(); /* 先把上一轮收干净(含超时强杀) */
|
||||
if (g_bt_scan_pid > 0) return 0; /* 在飞上限 1:窗口没结束就不重复 fork */
|
||||
if (!bt_scan_throttle(g_bt_scan_clock_ms())) return 0;
|
||||
if (bt_circuit_open()) { /* 连续超时:不再 fork,如实回熔断 */
|
||||
svc_log("bt scan on: 熔断中(连续 %d 次命令超时),跳过", g_bt_tmo_streak);
|
||||
return -1;
|
||||
}
|
||||
char cmd[384];
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"(sleep 1; echo 'scan on'; sleep %d; echo 'scan off'; echo 'devices'; echo 'quit') | "
|
||||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin %s > '%s' 2>&1",
|
||||
BT_SCAN_DWELL_S, g_bluetoothctl, g_bt_scan_cache);
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) { svc_log("bt scan on: fork failed (%s)", strerror(errno)); return -1; }
|
||||
if (pid == 0) bt_scan_worker(cmd);
|
||||
g_bt_scan_pid = pid;
|
||||
g_bt_scan_deadline = bt_scan_now_ms() + BT_SCAN_TMO_MS + 500;
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* 返回 0=后端已接受(或本来就没在扫)/ -1=超时或失败(已计入熔断)。 */
|
||||
static int bt_scan_off(void)
|
||||
{
|
||||
char cmd[192];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan off 2>/dev/null", g_bluetoothctl);
|
||||
system(cmd);
|
||||
char out[64];
|
||||
runcmd_res_t res;
|
||||
run_cmd_ex(out, sizeof out, BT_SCAN_OFF_TMO_MS, cmd, &res);
|
||||
if (res.status == RUNCMD_TIMEOUT) {
|
||||
svc_log("bt scan off: 超时(%dms 未返回),已 SIGKILL 收割", BT_SCAN_OFF_TMO_MS);
|
||||
bt_circuit_note(RUNCMD_TIMEOUT);
|
||||
return -1;
|
||||
}
|
||||
if (res.status == RUNCMD_EXIT || res.status == RUNCMD_EXEC) {
|
||||
svc_log("bt scan off: 后端失败(%s)", run_cmd_why(&res, NULL));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---------------- rfkill:统一软开关 ---------------- */
|
||||
@@ -3798,9 +3933,15 @@ static void bt_ctl_execute(const char *cmd, const char *arg, ctl_result_t *r)
|
||||
if (!arg || !arg[0]) { char m[48]; snprintf(m, sizeof m, "bt %s: 需 <mac>", cmd); ctl_sets(r, MEOW_ERR_INVAL, m); return; }
|
||||
bt_write_execute(cmd, arg, r);
|
||||
} else if (!strcmp(cmd, "scan")) {
|
||||
if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); }
|
||||
else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); }
|
||||
else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off");
|
||||
if (!arg || !strcmp(arg, "on")) {
|
||||
int rc = bt_scan_on();
|
||||
if (rc > 0) ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发");
|
||||
else if (rc == 0) ctl_sets(r, MEOW_OK, "bt scan on: 节流中或上一轮扫描未结束,本轮跳过");
|
||||
else ctl_sets(r, MEOW_ERR_NODEV, "bt scan on: 蓝牙栈熔断/无法起扫描");
|
||||
} else if (!strcmp(arg, "off")) {
|
||||
if (bt_scan_off() == 0) ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停");
|
||||
else ctl_sets(r, MEOW_ERR_NODEV, "bt scan off: 后端超时或失败(已强制收割)");
|
||||
} else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off");
|
||||
} else {
|
||||
ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令");
|
||||
}
|
||||
@@ -4183,6 +4324,7 @@ static int run_daemon(const char *sock)
|
||||
daemon_tick();
|
||||
next_tick = now + g_poll_sec;
|
||||
}
|
||||
bt_scan_reap(); /* M65 P1-3:每拍收割「附近设备」扫描 worker(超时整组强杀) */
|
||||
int wait_ms = (int)((next_tick - mono_now()) * 1000);
|
||||
if (wait_ms < 50) wait_ms = 50;
|
||||
if (wait_ms > g_poll_sec * 1000) wait_ms = g_poll_sec * 1000;
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
*
|
||||
* 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl
|
||||
*/
|
||||
/* M65:扫描窗口在单测里缩短 —— pipeline 的 dwell 取 0(整条 ~1s 结束),超时压到 1200ms。
|
||||
* 这样既能验「超时强杀 + 不漏孙进程」,又不用为每个用例等生产的 6.5s。 */
|
||||
#define BT_SCAN_TMO_MS 1200
|
||||
#define BT_SCAN_DWELL_S 0
|
||||
#define main meow_netmgrd_prod_main_unused
|
||||
#include "meow_netmgrd.c"
|
||||
#undef main
|
||||
@@ -31,6 +35,37 @@ static void wf(const char *p, const char *s)
|
||||
if (f) { fputs(s, f); fclose(f); }
|
||||
}
|
||||
|
||||
/* ---- M65 单测辅助:可控时钟 + 在飞 scan worker 的确定性收尾 ---- */
|
||||
static long long g_fake_now_ms = 0;
|
||||
static long long fake_clock_ms(void) { return g_fake_now_ms; }
|
||||
|
||||
/* 立刻收掉在飞 scan worker(整组杀 + 阻塞收尸):单测要确定性,不等生产的 ~5s 窗口。 */
|
||||
static void kill_scan_worker(void)
|
||||
{
|
||||
if (g_bt_scan_pid > 0) {
|
||||
kill(-g_bt_scan_pid, SIGKILL);
|
||||
kill(g_bt_scan_pid, SIGKILL);
|
||||
int st = 0;
|
||||
while (waitpid(g_bt_scan_pid, &st, 0) < 0 && errno == EINTR) { }
|
||||
}
|
||||
g_bt_scan_pid = 0;
|
||||
g_bt_scan_deadline = 0;
|
||||
}
|
||||
|
||||
/* 等 worker 由 bt_scan_reap() 自己收完(超时/正常都算),返回耗时 ms;超时返回 -1。 */
|
||||
static long drain_scan_worker(int max_ms)
|
||||
{
|
||||
long t0 = (long)bt_scan_now_ms();
|
||||
while (g_bt_scan_pid > 0) {
|
||||
bt_scan_reap();
|
||||
if (g_bt_scan_pid <= 0) break;
|
||||
if ((long)bt_scan_now_ms() - t0 > max_ms) return -1;
|
||||
struct timespec ts = { 0, 20 * 1000000L };
|
||||
nanosleep(&ts, NULL);
|
||||
}
|
||||
return (long)bt_scan_now_ms() - t0;
|
||||
}
|
||||
|
||||
/* ---- 真机原始字节样本(含 ANSI ESC / \001\002 标记 / \r\033[K / 提示符) ---- */
|
||||
static const char *S_NEW_PUDDING =
|
||||
"\033[0;94m[bluetooth]\033[0m# \r\033[K"
|
||||
@@ -113,6 +148,9 @@ int main(void)
|
||||
"#!/bin/sh\n"
|
||||
"S='%s/state'\n"
|
||||
"catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\n"
|
||||
/* M65:scan 的 wedge 桩。交互形态(无参数,scan pipeline 喂 stdin)与 `scan off`
|
||||
* 两种入口都要挂住;用 exec 让 sleep 就是那个进程本身(杀掉父 shell 不会留孤儿)。 */
|
||||
"if [ $# -eq 0 ] && [ -f \"$S/hang_scan\" ]; then exec sleep 60; fi\n"
|
||||
"case \"$1\" in\n"
|
||||
" paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n"
|
||||
" devices) catf \"$S/dev_list.txt\"; exit 0;;\n"
|
||||
@@ -146,6 +184,12 @@ int main(void)
|
||||
" echo yes > \"$S/paired_$2\"; echo 'Pairing successful'; exit 0;;\n"
|
||||
" remove)\n"
|
||||
" echo gone > \"$S/paired_$2\"; echo no > \"$S/conn_$2\"; echo 'Device has been removed'; exit 0;;\n"
|
||||
/* M65:scan 的 wedge 桩 —— 真的挂住(exec 让 sleep 就是那个进程本身,模拟 bluetoothctl
|
||||
* 卡在 HCI/D-Bus 里;若改成子进程 sleep,杀父进程会留下孤儿,断言就不诚实了)。 */
|
||||
" scan)\n"
|
||||
" if [ -f \"$S/hang_scan\" ]; then exec sleep 60; fi\n"
|
||||
" if [ \"$2\" = on ]; then echo 'Discovery started'; else echo 'Discovery stopped'; fi\n"
|
||||
" exit 0;;\n"
|
||||
"esac\n"
|
||||
"exit 0\n", T);
|
||||
wf(p, sh); chmod(p, 0755);
|
||||
@@ -801,6 +845,99 @@ int main(void)
|
||||
CHECK(strstr(r.text, "scan on") != NULL);
|
||||
bt_ctl_execute("scan", "off", &r);
|
||||
CHECK(r.code == MEOW_OK);
|
||||
kill_scan_worker(); /* 1s 窗口不必等,直接收 */
|
||||
|
||||
/* ================= M65 P2-1:扫描节流单位(7s / 7.1s;可控时钟) ================= */
|
||||
/* 回归防线:旧实现 `mono_now()`(秒)比 `7000`(毫秒)→ 实际 7000 秒 ≈ 1.94 小时一次。
|
||||
* 两个入口都验:①纯判定 bt_scan_throttle(入参毫秒)②真实入口 bt_scan_on(只认
|
||||
* g_bt_scan_clock_ms 钩子 —— 若有人把时钟改回 mono_now(),钩子失效,这里的 +6999 用例
|
||||
* 会看到真实时间几乎没动 → 判「放行」→ 断言失败)。 */
|
||||
printf("== M65 P2-1: bt scan 节流按毫秒(7s 放行 / 6.999s 拒 / 7.1s 放行) ==\n");
|
||||
{
|
||||
g_bt_scan_ms = 0;
|
||||
g_bt_scan_pid = 0; g_bt_scan_deadline = 0;
|
||||
g_bt_tmo_streak = 0; g_bt_tmo_until = 0;
|
||||
|
||||
CHECK(bt_scan_throttle(1000000) == 1); /* 首次:放行 */
|
||||
CHECK(bt_scan_throttle(1000000 + 6999) == 0); /* 6.999s:拒 */
|
||||
CHECK(bt_scan_throttle(1000000 + 7000) == 1); /* 7.000s:放行(秒混毫秒会等到 7000s) */
|
||||
CHECK(bt_scan_throttle(1000000 + 7000 + 99) == 0); /* 0.099s:拒 */
|
||||
CHECK(bt_scan_throttle(1000000 + 7000 + 7100) == 1); /* 7.1s:放行 */
|
||||
|
||||
/* 真实入口:时钟走钩子 */
|
||||
g_bt_scan_clock_ms = fake_clock_ms;
|
||||
g_bt_scan_ms = 0; g_bt_scan_pid = 0; g_bt_scan_deadline = 0;
|
||||
g_fake_now_ms = 3000000;
|
||||
CHECK(bt_scan_on() == 1); /* t=3.000s:放行并起 worker */
|
||||
kill_scan_worker();
|
||||
g_fake_now_ms = 3000000 + 6999;
|
||||
CHECK(bt_scan_on() == 0); /* +6.999s:节流(不起 worker) */
|
||||
CHECK(g_bt_scan_pid <= 0);
|
||||
g_fake_now_ms = 3000000 + 7000;
|
||||
CHECK(bt_scan_on() == 1); /* +7.000s:放行 */
|
||||
kill_scan_worker();
|
||||
g_fake_now_ms = 3000000 + 7000 + 7100;
|
||||
CHECK(bt_scan_on() == 1); /* +7.1s:再放行 */
|
||||
kill_scan_worker();
|
||||
g_bt_scan_clock_ms = bt_scan_now_ms; /* 复原真时钟 */
|
||||
g_bt_scan_ms = 0;
|
||||
}
|
||||
|
||||
/* ================= M65 P1-3:scan 有界收割(不挂死、不漏子进程) ================= */
|
||||
printf("== M65 P1-3: scan on/off 对「永不返回」的 bluetoothctl 有界收割、不漏子进程 ==\n");
|
||||
{
|
||||
snprintf(p, sizeof p, "%s/state/hang_scan", T); wf(p, "1\n"); /* 桩:scan → exec sleep 60 */
|
||||
g_bt_scan_ms = 0; g_bt_scan_pid = 0; g_bt_scan_deadline = 0;
|
||||
g_bt_tmo_streak = 0; g_bt_tmo_until = 0;
|
||||
|
||||
/* ① scan off 是同步路径:旧实现 system() 会永远阻塞在这个 wedge 上。 */
|
||||
long t0 = (long)bt_scan_now_ms();
|
||||
int rc = bt_scan_off();
|
||||
long el = (long)bt_scan_now_ms() - t0;
|
||||
printf(" [info] bt_scan_off 滞回 %ldms (上限 %dms)\n", el, BT_SCAN_OFF_TMO_MS);
|
||||
CHECK(rc == -1); /* 如实报失败,不假成功 */
|
||||
CHECK(el >= BT_SCAN_OFF_TMO_MS - 50); /* 确实等到了超时点 */
|
||||
CHECK(el < BT_SCAN_OFF_TMO_MS + 1200); /* 但有界(旧 system() 无界) */
|
||||
{ int st2 = 0; errno = 0;
|
||||
CHECK(waitpid(-1, &st2, WNOHANG) == -1 && errno == ECHILD); } /* 无残留子进程 */
|
||||
|
||||
/* ② scan on:worker 在飞,主循环不被阻塞;过 deadline 整组强杀(含 pipeline 孙进程)。 */
|
||||
g_bt_scan_ms = 0;
|
||||
t0 = (long)bt_scan_now_ms();
|
||||
CHECK(bt_scan_on() == 1);
|
||||
long fork_el = (long)bt_scan_now_ms() - t0;
|
||||
CHECK(fork_el < 300); /* 起扫描本身绝不阻塞 */
|
||||
CHECK(g_bt_scan_pid > 0);
|
||||
CHECK(bt_scan_on() == 0); /* 在飞上限 1:不重复 fork */
|
||||
long dr = drain_scan_worker(BT_SCAN_TMO_MS + 3000);
|
||||
el = (long)bt_scan_now_ms() - t0; /* 从起扫描算起,含 fork + 窗口 */
|
||||
printf(" [info] scan worker 从起扫描到收割 %ldms(drain 轮询 %ldms;窗口 %dms)\n",
|
||||
el, dr, BT_SCAN_TMO_MS);
|
||||
CHECK(dr >= 0); /* 有界收完(没有永久卡住) */
|
||||
CHECK(el >= BT_SCAN_TMO_MS); /* 是「等到超时才收」而不是立刻杀 */
|
||||
CHECK(el < BT_SCAN_TMO_MS + 2500);
|
||||
CHECK(g_bt_scan_pid == 0); /* 槽位已释放 */
|
||||
CHECK(g_bt_tmo_streak >= 1); /* 超时计入既有熔断计数 */
|
||||
{ int st2 = 0; errno = 0;
|
||||
CHECK(waitpid(-1, &st2, WNOHANG) == -1 && errno == ECHILD); } /* 孙进程也被整组收掉 */
|
||||
|
||||
/* ③ 熔断联动:再来一次超时就把 streak 顶到上限 → bt_circuit_open(),不再 fork。 */
|
||||
g_bt_tmo_streak = BT_TMO_STREAK_MAX - 1;
|
||||
g_bt_tmo_until = 0;
|
||||
g_bt_scan_ms = 0;
|
||||
CHECK(bt_scan_on() == 1);
|
||||
CHECK(drain_scan_worker(BT_SCAN_TMO_MS + 3000) >= 0);
|
||||
CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX);
|
||||
CHECK(bt_circuit_open() == 1);
|
||||
g_bt_scan_ms = 0;
|
||||
CHECK(bt_scan_on() == -1); /* 熔断中:不再 fork(不烧进程槽) */
|
||||
CHECK(g_bt_scan_pid <= 0);
|
||||
|
||||
/* 复原:摘掉 wedge 钩子,熔断器清零 */
|
||||
{ char ub[320]; snprintf(ub, sizeof ub, "rm -f '%s/state/hang_scan'", T); (void)!system(ub); }
|
||||
g_bt_tmo_streak = 0; g_bt_tmo_until = 0; g_bt_scan_ms = 0;
|
||||
bt_scan_off(); /* 桩恢复正常:现在能返回 0 */
|
||||
}
|
||||
|
||||
printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n");
|
||||
caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;
|
||||
|
||||
Reference in new issue
Block a user