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
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user