Merge branch 'feat/m65-p1-popen-timeout-and-bt-scan-unit-fi'

This commit is contained in:
AskaEth committed 2026-11-12 09:09:31 +08:00
commit 9be447087d
7 files changed
+839 -86

No files matched your search

+38 -42
View File
@@ -59,6 +59,7 @@
#include <sys/un.h> #include <sys/un.h>
#include <sys/wait.h> #include <sys/wait.h>
#include "meow_ipc.h" #include "meow_ipc.h"
#include "popen_tmo.h" /* M65 P1-2:有界读子进程 stdout(缓冲归调用方,无 static) */
#include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */ #include "bt_dev.h" /* M50:端点探测 / PCM 名构造 / 回落退避(纯逻辑,不改任何配置文件) */
#include "vol_gain.h" /* M55:BT 写路径软件增益(与 DAC 同一锥度,全整数) */ #include "vol_gain.h" /* M55:BT 写路径软件增益(与 DAC 同一锥度,全整数) */
@@ -570,11 +571,18 @@ static void ring_rebuild(void)
} }
/* ---- amixer 封装(popen 限时;无 amixer → 优雅失败) ---- */ /* ---- amixer 封装(popen 限时;无 amixer → 优雅失败) ---- */
/* 读子进程 stdout(非阻塞+poll 限时)。收割**必须有界**:驱动 wedge 时 amixer 可能 /* 读子进程 stdout 到**调用方给的缓冲**(非阻塞 + poll 限时;缓冲不再 static)。
* 挂在内核 ioctl 里,无条件 waitpid 会把调用线程(路由线程持 g_mu / IO 线程)一起拖死 *
* ——真机长流卡死的级联路径之一。策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms; * M65 P1-2(M59 审计 P1-2):旧实现是 `static char buf[4096]; … return buf;` —— 本进程有
* 仍不死(D 态)→ 放弃收割(泄漏一个子进程,换线程不卡死;已记日志)。 */ * 4 个线程(playback / route / io / vol),route_thread 在调 amixer_get(9,1) 之前**故意放掉
static char *popen_read_tmo(int tmo_ms, const char *cmd) * 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]; int p[2];
if (pipe(p) < 0) return NULL; 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); } 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]); close(p[1]);
fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK); fcntl(p[0], F_SETFL, fcntl(p[0], F_GETFL, 0) | O_NONBLOCK);
static char buf[4096]; size_t len = 0;
size_t off = 0; int rc = popen_tmo_read_all(p[0], tmo_ms, &out, &cap, 0, &len, 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;
}
close(p[0]); close(p[0]);
int reaped = 0; if (rc == POPEN_TMO_OVERFLOW)
for (int i = 0; i < 30; i++) { /* 300ms 优雅退出窗口 */ logmsg("popen: output over %zu bytes, kept the head only (%s)", cap, cmd);
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; } /* 收割**必须有界**:驱动 wedge 时 amixer 可能挂在内核 ioctl 里,无条件 waitpid 会把调用
struct timespec ts = { 0, 10 * 1000000L }; * 线程(路由线程持 g_mu / IO 线程)一起拖死 —— 真机长流卡死的级联路径之一。
nanosleep(&ts, NULL); * 策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms;仍不死(D 态)→ 放弃收割
} * (泄漏一个子进程,换线程不卡死;已记日志)。 */
if (!reaped) { if (waitpid(pid, NULL, WNOHANG) == 0) {
logmsg("popen: child %ld did not exit in 300ms, SIGKILL (%s)", (long)pid, cmd);
kill(pid, SIGKILL); kill(pid, SIGKILL);
for (int i = 0; i < 30; i++) { if (!popen_tmo_reap_bounded(pid))
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
if (!reaped)
logmsg("popen: child %ld unkillable (D-state, driver wedged?) — abandon reap", (long)pid); logmsg("popen: child %ld unkillable (D-state, driver wedged?) — abandon reap", (long)pid);
} }
buf[off] = 0; return out;
return buf;
} }
static int amixer_set(int numid, int val, int card) 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]; char cmd[160];
snprintf(cmd, sizeof cmd, "%s%s cset numid=%d %d >/dev/null 2>&1", snprintf(cmd, sizeof cmd, "%s%s cset numid=%d %d >/dev/null 2>&1",
g_amixer, (card ? " -c 0" : ""), numid, val); 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; if (!o) return -1;
/* 测试钩子(仅 env MEOW_AUDIO_AMIXER_TRACE=<path>):把每次 amixer cset 追加到文件, /* 测试钩子(仅 env MEOW_AUDIO_AMIXER_TRACE=<path>):把每次 amixer cset 追加到文件,
* 供宿主断言「音量双组写」PCM+DSD_VOLUME 是否成对下发(真机无 amixer 时不写/不报错)。 */ * 供宿主断言「音量双组写」PCM+DSD_VOLUME 是否成对下发(真机无 amixer 时不写/不报错)。 */
@@ -636,7 +627,7 @@ static int amixer_set(int numid, int val, int card)
close(tfd); close(tfd);
} }
} }
/* popen_read_tmo 用的是静态缓冲,这里不 free(与 meow_route.c 同权衡) */ /* 缓冲是上面的栈数组:无 static、无锁,多个线程各用各的(M65 P1-2) */
return 0; return 0;
} }
@@ -644,7 +635,8 @@ static int amixer_get(int numid, int card)
{ {
char cmd[160]; char cmd[160];
snprintf(cmd, sizeof cmd, "%s%s cget numid=%d 2>/dev/null", g_amixer, (card ? " -c 0" : ""), numid); 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; if (!o) return -1;
const char *ln = o; const char *ln = o;
while (ln && *ln) { while (ln && *ln) {
@@ -993,7 +985,8 @@ static int bt_collect_candidates(char (*cand)[18], int max)
{ {
if (!cand || max <= 0) return 0; if (!cand || max <= 0) return 0;
int n = 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]) { if (o && o[0]) {
char m[18]; char m[18];
if (bt_dev_detect_mac(o, m, sizeof m) == 0) { 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); 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) { if (o) {
char macs[4][18]; char macs[4][18];
int k = bt_dev_acl_macs(o, macs, 4); 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) static void bt_refresh_endpoint(void)
{ {
g_bt_pcm[0] = 0; 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; if (!o) return;
char path[128]; char path[128];
if (bt_dev_pick_endpoint(o, g_bt_mac, path, sizeof path) == 0) 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; } if (!pcm[0]) { logmsg("bt: avrcp set-max skipped (no endpoint path for %s)", g_bt_mac); return; }
char cmd[256]; char cmd[256];
snprintf(cmd, sizeof cmd, "%s volume %s 127 >/dev/null 2>&1", BT_VOL_FULL_PCM, pcm); 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)"); 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; if (!g_bt_pcm[0]) return;
char cmd[256]; char cmd[256];
snprintf(cmd, sizeof cmd, "%s info %s 2>/dev/null", BT_VOL_FULL_PCM, g_bt_pcm); 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; if (!o) return;
char codec[16]; char codec[16];
if (bt_dev_info_codec(o, codec, sizeof codec) == 0) { if (bt_dev_info_codec(o, codec, sizeof codec) == 0) {
@@ -57,6 +57,15 @@ run_tests() {
rc=1 rc=1
fi 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 if [ $rc -eq 0 ]; then echo "== ALL TESTS PASS =="; else echo "!! TESTS FAILED"; fi
return $rc return $rc
} }
+24 -28
View File
@@ -1,6 +1,7 @@
/* net.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* net.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h" #include "meow_launcher_internal.h"
#include "popen_tmo.h" /* M65 P1-1:有界读子进程 stdout(deadline 在循环顶部 + 256KB 上限) */
/* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */ /* wlan0 是否已联网(链路 up,与状态栏 WIFI 指示一致) */
@@ -76,7 +77,16 @@ int bt_role(char *desc, int len); /* 前向声明;实现在网络段(用 p
* 避免绘制热点反复 spawn。 */ * 避免绘制热点反复 spawn。 */
/* 带超时跑一条命令并读 stdout(防 wpa_supplicant/BlueZ 未跑时命令挂起)。 /* 带超时跑一条命令并读 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) char *popen_read_tmo(int tmo_ms, const char *cmd)
{ {
int fds[2]; int fds[2];
@@ -96,34 +106,20 @@ char *popen_read_tmo(int tmo_ms, const char *cmd)
int fl = fcntl(fd, F_GETFL, 0); int fl = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, fl | O_NONBLOCK); fcntl(fd, F_SETFL, fl | O_NONBLOCK);
char *out = NULL; size_t cap = 0, len = 0; char *out = NULL; size_t cap = 0, len = 0;
char tmp[256]; int rc = popen_tmo_read_all(fd, tmo_ms, &out, &cap, POPEN_TMO_MAX, &len, 1);
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); }
close(fd); 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; return out;
} }
+133
View File
@@ -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 */
+340
View File
@@ -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;
}
+158 -16
View File
@@ -114,6 +114,18 @@ static char g_net_policy[160]= "/data/meow/netmgrd/net_policy.conf"; /* MEOW_NET
* 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */ * 「熔断中」;任何非超时返回清零。范围只限 BT 写操作,不影响 WiFi/DNS 调用点。 */
#define BT_TMO_STREAK_MAX 3 #define BT_TMO_STREAK_MAX 3
#define BT_TMO_COOLDOWN_SEC 30 #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 IFACE_MAX 16
#define RFKILL_MAX 8 #define RFKILL_MAX 8
#define DNS_MAX 8 #define DNS_MAX 8
@@ -1437,24 +1449,147 @@ static void bt_devices_text(char *buf, int cap)
/* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。 /* 扫描生命周期(§七:进「附近查找」才 scan on;退页/超时 off)。默认 7s 节流。
* M49:改用 g_bluetoothctl(原来硬编码 "bluetoothctl",MEOW_BLUETOOTHCTL 桩对 scan 无效, * M49:改用 g_bluetoothctl(原来硬编码 "bluetoothctl",MEOW_BLUETOOTHCTL 桩对 scan 无效,
* 宿主测试没法覆盖扫描链路)。 */ * 宿主测试没法覆盖扫描链路)。
static long g_bt_scan_ms = 0; * M65 P1-3:scan 不再走 `system()`。
static void bt_scan_on(void) * - 旧 `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(); struct timespec tp;
if (g_bt_scan_ms && t - g_bt_scan_ms < 7000) return; clock_gettime(CLOCK_MONOTONIC, &tp);
g_bt_scan_ms = t; return (long long)tp.tv_sec * 1000LL + tp.tv_nsec / 1000000LL;
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);
} }
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]; char cmd[192];
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin %s scan off 2>/dev/null", g_bluetoothctl); 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:统一软开关 ---------------- */ /* ---------------- 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; } 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); bt_write_execute(cmd, arg, r);
} else if (!strcmp(cmd, "scan")) { } else if (!strcmp(cmd, "scan")) {
if (!arg || !strcmp(arg, "on")) { bt_scan_on(); ctl_sets(r, MEOW_OK, "bt scan on: 后台节流扫描已触发"); } if (!arg || !strcmp(arg, "on")) {
else if (!strcmp(arg, "off")) { bt_scan_off(); ctl_sets(r, MEOW_OK, "bt scan off: 扫描已停"); } int rc = bt_scan_on();
else ctl_sets(r, MEOW_ERR_INVAL, "bt scan: 用法 on|off"); 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 { } else {
ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令"); ctl_sets(r, MEOW_ERR_INVAL, "bt: 未知子命令");
} }
@@ -4183,6 +4324,7 @@ static int run_daemon(const char *sock)
daemon_tick(); daemon_tick();
next_tick = now + g_poll_sec; next_tick = now + g_poll_sec;
} }
bt_scan_reap(); /* M65 P1-3:每拍收割「附近设备」扫描 worker(超时整组强杀) */
int wait_ms = (int)((next_tick - mono_now()) * 1000); int wait_ms = (int)((next_tick - mono_now()) * 1000);
if (wait_ms < 50) wait_ms = 50; if (wait_ms < 50) wait_ms = 50;
if (wait_ms > g_poll_sec * 1000) wait_ms = g_poll_sec * 1000; if (wait_ms > g_poll_sec * 1000) wait_ms = g_poll_sec * 1000;
+137
View File
@@ -11,6 +11,10 @@
* *
* 用法:cc -Wall -Wextra -I../meow_launcherd test_bt_ctl.c -o test_bt_ctl && ./test_bt_ctl * 用法: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 #define main meow_netmgrd_prod_main_unused
#include "meow_netmgrd.c" #include "meow_netmgrd.c"
#undef main #undef main
@@ -31,6 +35,37 @@ static void wf(const char *p, const char *s)
if (f) { fputs(s, f); fclose(f); } 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 / 提示符) ---- */ /* ---- 真机原始字节样本(含 ANSI ESC / \001\002 标记 / \r\033[K / 提示符) ---- */
static const char *S_NEW_PUDDING = static const char *S_NEW_PUDDING =
"\033[0;94m[bluetooth]\033[0m# \r\033[K" "\033[0;94m[bluetooth]\033[0m# \r\033[K"
@@ -113,6 +148,9 @@ int main(void)
"#!/bin/sh\n" "#!/bin/sh\n"
"S='%s/state'\n" "S='%s/state'\n"
"catf() { [ -f \"$1\" ] || return 0; while IFS= read -r l || [ -n \"$l\" ]; do echo \"$l\"; done < \"$1\"; }\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" "case \"$1\" in\n"
" paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n" " paired-devices) catf \"$S/paired_list.txt\"; exit 0;;\n"
" devices) catf \"$S/dev_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" " echo yes > \"$S/paired_$2\"; echo 'Pairing successful'; exit 0;;\n"
" remove)\n" " remove)\n"
" echo gone > \"$S/paired_$2\"; echo no > \"$S/conn_$2\"; echo 'Device has been removed'; exit 0;;\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" "esac\n"
"exit 0\n", T); "exit 0\n", T);
wf(p, sh); chmod(p, 0755); wf(p, sh); chmod(p, 0755);
@@ -801,6 +845,99 @@ int main(void)
CHECK(strstr(r.text, "scan on") != NULL); CHECK(strstr(r.text, "scan on") != NULL);
bt_ctl_execute("scan", "off", &r); bt_ctl_execute("scan", "off", &r);
CHECK(r.code == MEOW_OK); 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"); printf("== bt_write_authorize(系统身份 + MeowOS 才放行写级) ==\n");
caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER; caller_t other; memset(&other, 0, sizeof other); other.kind = CALL_OTHER;