Files
MeowOS-For-R3P2/patch_v2/meow_netmgrd/test_bt_ctl.c
T
AskaEth 1ccb3c7a93 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。
2026-11-12 09:02:07 +08:00

957 lines
52 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* test_bt_ctl.c — meow_netmgrd 蓝牙控制面 纯逻辑宿主自测(本程序非生产代码)。
*
* 通过 #include 生产源文件(meow_netmgrd.c,用 #define main 改名避免入口冲突),同一 TU 直接访问
* FILE-STATIC 函数/全局:三个纯解析器(bt_scan_line_parse / bt_pcm_paths_parse+bt_pcm_info_parse /
* bt_acl_parse)、run_cmd_ex 四态、bt_cmd_is_write / bt_write_authorize、bt_ctl_execute
* (status/paired/connected/a2dp/devices/on/off/connect/disconnect/pair/unpair/scan)。
* 后端工具经有状态桩脚本(g_bluetoothctl/g_hcitool/g_bluealsa_cli/g_hciconfig/g_bt_scan_cache)
* 覆盖,不依赖真实蓝牙栈。
*
* 字节样本来源:2026-11-11 真机 `od -c /tmp/meow_bt_scan.txt`(0.8.49,BluetoothCtl 5.54 交互输出)。
*
* 用法: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
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.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)
static void wf(const char *p, const char *s)
{
FILE *f = fopen(p, "w");
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"
"[\001\033[0;92m\002NEW\001\033[0m\002] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n";
static const char *S_CHG_POWERED =
"\033[0;94m[bluetooth]\033[0m# \r\033[K"
"[\001\033[0;93m\002CHG\001\033[0m\002] Controller 4C:BC:98:B0:98:DB Powered: no\n";
static const char *S_DEL_LINE = "[DEL] Device 00:11:22:33:44:55\n";
static const char *S_PROMPT_DEVICES = "\033[0;94m[bluetooth]\033[0m# devices\n";
static const char *S_PLAIN_BLOCK = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
static const char *S_TRUNC_MAC =
"\033[0;94m[bluetooth]\033[0m# \r\033[K[NEW] Device 33:DE:2E:21:A";
/* ---- 真机 `hcitool con` 原始字节样本(两条都可从仓库内自证)----
*
* S_CON_INBOUND —— 0.8.50 / 设备 10.58.215.50 / 2026-11-11 20:53 / **耳机自主回连**现场,
* `hcitool con 2>&1 | od -c` 的原样输出(每行 16 字节;偏移是八进制,末行 0000120 = 80 字节,
* 与本字面量长度一致;每字节右侧的空格是 od 的字段填充,不是数据):
* 0000000 C o n n e c t i o n s : \n \t >
* 0000020 A C L 3 3 : D E : 2 E : 2 1 :
* 0000040 A D : 3 0 h a n d l e 1 1
* 0000060 s t a t e 1 l m M A S T E
* 0000100 R A U T H E N C R Y P T \n
* 0000120
* 逐字节读法:`Connections:` + LF + TAB + `>` + SPC + `ACL` + SPC + 17 字符 MAC +
* ` handle 11 ` + `state 1 lm MASTER AUTH ENCRYPT` + SPC + LF ⇒ 字面量见下方 S_CON_INBOUND。
* 复核方法:`printf '<上面的字面量>' | od -c`,与本块除「行尾字段填充空格」外逐行相同
* (本块为免源码尾随空格删掉了那点填充;数据以 80 字节 / 末偏移 0000120 为准)。
* 这次采集没有单独落盘成证据文件(`devdocs/evidence/` 只有 m50 那一份),上面这块 od 输出
* 就是它的原样记录;同一条命令的「`Connections:` 表头 + 行首 TAB + 行尾 SPC 后 LF」框架字节
* 可与 S_CON_OUTBOUND 的来源(下面那份 `cat -A` 证据)互相印证。
*
* S_CON_OUTBOUND —— devdocs/evidence/m50_bt_result_20261111.log:7-8(同机同耳机,本端发起)。
* `cat -A` 核为 `Connections:$` + `^I< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER $`,
* 与下面的字面量逐字节一致(行首 TAB、行尾一个空格再 LF)。 */
static const char *S_CON_INBOUND =
"Connections:\n"
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n";
static const char *S_CON_OUTBOUND =
"Connections:\n"
"\t< ACL 33:DE:2E:21:AD:30 handle 0 state 5 lm MASTER \n";
int main(void)
{
/* 宿主临时目录:Termux/Android 上 /tmp 不可写,优先 $TMPDIR(mktemp 同规则) */
const char *base = getenv("TMPDIR");
if (!base || !base[0]) base = "/tmp";
static char Tbuf[256];
snprintf(Tbuf, sizeof Tbuf, "%s/nmg_bt", base);
const char *T = Tbuf;
char p[256];
char cmd[512];
/* 可重复运行(r1 评审 P2):上一轮遗留的 conn_ / paired_ 状态文件会把「无连接」断言推翻。
* 每次开跑先整棵删掉重来,退出时也清(见文件末尾)。 */
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", Tbuf); (void)!system(rm); }
mkdir(Tbuf, 0755);
/* 假 /sys/class/bluetooth/hci0(available/powered/address) */
{ snprintf(p, sizeof p, "%s/bt", T); mkdir(p, 0755);
snprintf(p, sizeof p, "%s/bt/hci0", T); mkdir(p, 0755);
snprintf(p, sizeof p, "%s/bt/hci0/address", T); wf(p, "4c:bc:98:b0:98:db\n");
snprintf(p, sizeof p, "%s/bt/hci0/power", T); wf(p, "off\n"); }
/* 假 rfkill:真机有两个 bluetooth 节点(rfkill0/rfkill3),任一仍 block 就等于没开 */
{ snprintf(p, sizeof p, "%s/rfkill", T); mkdir(p, 0755);
for (int i = 0; i < 2; i++) {
char d[300];
snprintf(d, sizeof d, "%s/rfkill/rfkill%d", T, i ? 3 : 0); mkdir(d, 0755);
snprintf(p, sizeof p, "%s/type", d); wf(p, "bluetooth\n");
snprintf(p, sizeof p, "%s/soft", d); wf(p, "1\n");
} }
/* 有状态桩的状态目录 */
snprintf(p, sizeof p, "%s/state", T); mkdir(p, 0755);
/* 桩:bluetoothctl(paired-devices/devices/info/connect/disconnect/pair/remove,靠状态文件变迁)
* 注意:生产代码给后端命令前置 PATH=/bin:/sbin:/usr/bin:/usr/sbin,Termux 上 cat/basename/grep
* 都不在这几个目录里 —— 桩脚本只能用 shell 内建(read/echo/[ /参数展开),否则测试会假失败。 */
snprintf(p, sizeof p, "%s/bluetoothctl", T);
{
char sh[3200];
snprintf(sh, sizeof sh,
"#!/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"
" info)\n"
" mac=\"$2\"\n"
" if [ ! -f \"$S/paired_$mac\" ]; then echo \"Device $mac not available\"; exit 1; fi\n"
" pv=no; IFS= read -r pv < \"$S/paired_$mac\"\n"
" if [ \"$pv\" = gone ]; then echo \"Device $mac not available\"; exit 1; fi\n"
" cv=no\n"
" if [ -f \"$S/conn_$mac\" ]; then IFS= read -r cv < \"$S/conn_$mac\"; fi\n"
" echo \"Device $mac\"\n"
" echo \" Name: Pudding\"\n"
" echo \" Paired: $pv\"\n"
" echo \" Connected: $cv\"\n"
" exit 0;;\n"
" connect)\n"
" case \"$2\" in CC:*) sleep 5;; esac\n"
" case \"$2\" in AA:*) echo 'Failed to connect: org.bluez.Error.Failed br-connection-refused'; exit 1;; esac\n"
/* 已入站连接时 connect 是幂等 no-op:真机上内核 ACL 仍是 `>` 行,别把方向改成 `<` */
" prev=; [ -f \"$S/conn_$2\" ] && IFS= read -r prev < \"$S/conn_$2\"\n"
" [ \"$prev\" = in ] || echo yes > \"$S/conn_$2\"\n"
" echo yes > \"$S/paired_$2\"\n"
" echo 'Attempting to connect'; echo 'Connection successful'; exit 0;;\n"
" disconnect)\n"
" case \"$2\" in AA:*) echo 'Failed to disconnect: not connected'; exit 1;; esac\n"
" case \"$2\" in BB:*) echo 'Successful disconnected'; exit 0;; esac\n"
" echo no > \"$S/conn_$2\"; echo 'Successful disconnected'; exit 0;;\n"
" pair)\n"
" case \"$2\" in CC:*) sleep 5;; esac\n"
" case \"$2\" in AA:*) echo 'Failed to pair: org.bluez.Error.AuthenticationFailed'; exit 1;; esac\n"
" 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);
}
/* 桩:hcitool con —— 按 conn_<mac> 文件内容选方向/链路类型(真机字节形态):
* in → "\t> ACL … "(远端发起,耳机自主回连,M54 真机 od -c 原文)
* sco → "\t< SCO … "(SCO 链路,不是 ACL)
* 其它非空值(yes)→ "\t< ACL … "(本端发起) */
snprintf(p, sizeof p, "%s/hcitool", T);
{
char sh[1200];
snprintf(sh, sizeof sh,
"#!/bin/sh\n"
"S='%s/state'\n"
"echo 'Connections:'\n"
"for f in \"$S\"/conn_*; do\n"
" [ -f \"$f\" ] || continue\n"
" v=no; IFS= read -r v < \"$f\"\n"
" case \"$v\" in yes|in|sco) ;; *) continue;; esac\n"
" m=\"${f##*/}\"; m=\"${m#conn_}\"\n"
" case \"$v\" in\n"
" in) echo \"\t> ACL $m handle 11 state 1 lm MASTER AUTH ENCRYPT \";;\n"
" sco) echo \"\t< SCO $m handle 13 state 1 lm MASTER \";;\n"
" *) echo \"\t< ACL $m handle 12 state 1 lm MASTER \";;\n"
" esac\n"
"done\n"
"exit 0\n", T);
wf(p, sh); chmod(p, 0755);
}
/* 桩:hciconfig —— `hci0 up/down` 改 sysfs power,`hci0` 打印状态 */
snprintf(p, sizeof p, "%s/hciconfig", T);
{
char sh[900];
snprintf(sh, sizeof sh,
"#!/bin/sh\n"
"S='%s'\n"
"if [ -f \"$S/state/hang_hciconfig\" ]; then sleep 5; fi\n"
"blocked() { for t in \"$S\"/rfkill/*; do\n"
" [ -f \"$t/type\" ] || continue\n"
" v=\"\"; IFS= read -r v < \"$t/type\"; [ \"$v\" = bluetooth ] || continue\n"
" b=0; IFS= read -r b < \"$t/soft\"; [ \"$b\" = 1 ] && return 0\n"
" done; return 1; }\n"
"case \"$2\" in\n"
" up) if blocked; then echo 'hci0: Operation not possible due to RF-kill' >&2; exit 1; fi\n"
" echo on > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' UP RUNNING';;\n"
" down) echo off > \"$S/bt/hci0/power\"; echo 'hci0: Type: Primary Bus: UART'; echo ' DOWN';;\n"
" *) echo 'hci0: Type: Primary Bus: UART'\n"
" v=off; if [ -f \"$S/bt/hci0/power\" ]; then IFS= read -r v < \"$S/bt/hci0/power\"; fi\n"
" if [ \"$v\" = on ]; then echo ' UP RUNNING'; else echo ' DOWN'; fi;;\n"
"esac\n"
"exit 0\n", T);
wf(p, sh); chmod(p, 0755);
}
/* 桩:bluealsa-cli —— list-pcms 端点由 $S/pcms.txt 控制,info 按路径返回属性 */
snprintf(p, sizeof p, "%s/bluealsa-cli", T);
{
char sh[3200];
snprintf(sh, sizeof sh,
"#!/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"
"case \"$1\" in\n"
" list-pcms) catf \"$S/pcms.txt\"; exit 0;;\n"
" info)\n"
" if [ -f \"$S/hang_info\" ]; then sleep 5; fi\n"
" path=\"$2\"; found=0\n"
" if [ -f \"$S/pcms.txt\" ]; then\n"
" while IFS= read -r l || [ -n \"$l\" ]; do\n"
" if [ \"$l\" = \"$path\" ]; then found=1; fi\n"
" done < \"$S/pcms.txt\"\n"
" fi\n"
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) found=0;; esac\n"
" if [ \"$found\" != 1 ]; then\n"
" echo \"bluealsa-cli: E: CMD \\\"info\\\": Couldn't get BlueALSA PCM: Object path not found: '$path'\" >&2\n"
" exit 1\n"
" fi\n"
" case \"$path\" in *dev_33_DE_2E_21_AD_30*) ;; *) exit 1;; esac\n"
" if [ -f \"$S/running\" ]; then run=true; else run=false; fi\n"
" echo \"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\"\n" /* ← 真机原文(M50 2026-11-11 18:26) */
" echo 'Sequence: 0'\n"
" echo 'Transport: A2DP-source'\n"
" echo 'Mode: sink'\n"
" echo \"Running: $run\"\n"
" echo 'Format: S16_LE'\n"
" echo 'Channels: 2'\n"
" echo 'Sampling: 48000 Hz'\n"
" echo 'Available codecs: SBC AAC'\n"
" echo 'Selected codec: AAC'\n"
" echo 'Delay: 15.0 ms'\n"
" echo 'SoftVolume: false'\n"
" echo 'Volume: L: 127 R: 127'\n"
" echo 'Muted: L: false R: false'\n"
" exit 0;;\n"
"esac\n"
"exit 0\n", T);
wf(p, sh); chmod(p, 0755);
}
/* 初始状态:两台已配对(真机 MOONDROP / 17OP15),无 ACL */
snprintf(p, sizeof p, "%s/state/paired_list.txt", T);
wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n");
snprintf(p, sizeof p, "%s/state/dev_list.txt", T);
wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n"
"Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n");
snprintf(p, sizeof p, "%s/state/paired_33:DE:2E:21:AD:30", T); wf(p, "yes\n");
snprintf(p, sizeof p, "%s/state/paired_E0:42:6D:79:C5:98", T); wf(p, "yes\n");
/* 指向桩(g_* 是 const char*,用静态缓冲承载) */
static char sb_bt[256], sb_hci[256], sb_ba[256], sb_hc[256], sb_cache[256];
snprintf(sb_bt, sizeof sb_bt, "%s/bluetoothctl", T);
snprintf(sb_hci, sizeof sb_hci, "%s/hcitool", T);
snprintf(sb_ba, sizeof sb_ba, "%s/bluealsa-cli", T);
snprintf(sb_hc, sizeof sb_hc, "%s/hciconfig", T);
snprintf(sb_cache, sizeof sb_cache, "%s/bt_scan.txt", T);
g_bluetoothctl = sb_bt;
g_hcitool = sb_hci;
g_bluealsa_cli = sb_ba;
g_hciconfig = sb_hc;
g_bt_scan_cache = sb_cache;
g_bt_dir[0] = 0; snprintf(g_bt_dir, sizeof g_bt_dir, "%s/bt", T);
g_rfkill_dir[0] = 0; snprintf(g_rfkill_dir, sizeof g_rfkill_dir, "%s/rfkill", T);
ctl_result_t r;
/* ================= 1. run_cmd_ex 四态(M49 新增) ================= */
printf("== run_cmd_ex 四态:成功有输出/成功无输出/非 0 退出/超时/命令不存在/stderr 抓取 ==\n");
{
runcmd_res_t rr;
char b[128];
int n = run_cmd_ex(b, sizeof b, 1500, "echo hi", &rr);
CHECK(n == 3);
CHECK(rr.status == RUNCMD_OK);
CHECK(rr.exit_code == 0);
n = run_cmd_ex(b, sizeof b, 1500, "true", &rr);
CHECK(n == 0);
CHECK(rr.status == RUNCMD_EMPTY); /* 成功但无输出 ≠ 失败 */
n = run_cmd_ex(b, sizeof b, 1500, "exit 3", &rr);
CHECK(n == 0);
CHECK(rr.status == RUNCMD_EXIT);
CHECK(rr.exit_code == 3);
n = run_cmd_ex(b, sizeof b, 1500, "nosuchcmd_m49_xyz", &rr);
CHECK(rr.status == RUNCMD_EXIT);
CHECK(rr.exit_code == 127); /* 命令不存在(sh 报 127) */
n = run_cmd_ex(b, sizeof b, 150, "sleep 5", &rr);
CHECK(rr.status == RUNCMD_TIMEOUT); /* 限时内不退出 */
CHECK(n >= 0);
n = run_cmd_ex(b, sizeof b, 1500, "echo boom >&2; exit 4", &rr);
CHECK(rr.status == RUNCMD_EXIT);
CHECK(rr.exit_code == 4);
CHECK(strstr(rr.err, "boom") != NULL); /* stderr 不再丢 /dev/null */
int isfail = 0;
const char *why = run_cmd_why(&rr, &isfail);
CHECK(isfail == 1);
CHECK(strstr(why, "exit 4") != NULL);
CHECK(strstr(why, "boom") != NULL);
CHECK(run_cmd(b, sizeof b, 1500, "echo ok") == 3); /* 旧外壳契约不变 */
}
/* ================= 2. 扫描缓存解析(真机字节) ================= */
printf("== bt_clean_ansi / bt_scan_line_parse(真机 od 字节样本) ==\n");
{
char clean[512], mac[24], name[64];
bt_clean_ansi(S_NEW_PUDDING, clean, sizeof clean);
CHECK(strstr(clean, "\033") == NULL);
CHECK(strstr(clean, "\001") == NULL && strstr(clean, "\002") == NULL);
CHECK(strstr(clean, "\r") == NULL);
CHECK(strcmp(clean, "[bluetooth]# [NEW] Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63") == 0);
CHECK(bt_scan_line_parse(S_NEW_PUDDING, mac, sizeof mac, name, sizeof name) == 1);
CHECK(strcmp(mac, "79:85:D6:7B:73:63") == 0);
CHECK(strcmp(name, "79-85-D6-7B-73-63") == 0);
CHECK(bt_scan_line_parse(S_PLAIN_BLOCK, mac, sizeof mac, name, sizeof name) == 1);
CHECK(strcmp(mac, "33:DE:2E:21:AD:30") == 0);
CHECK(strcmp(name, "MOONDROP Pudding") == 0);
/* [CHG]/[DEL] 不是「附近有这台」 */
CHECK(bt_scan_line_parse(S_CHG_POWERED, mac, sizeof mac, name, sizeof name) == 0);
CHECK(bt_scan_line_parse(S_DEL_LINE, mac, sizeof mac, name, sizeof name) == 0);
/* 提示符回显行 / 空行 / 截断到半截 MAC / 无关行 */
CHECK(bt_scan_line_parse(S_PROMPT_DEVICES, mac, sizeof mac, name, sizeof name) == 0);
CHECK(bt_scan_line_parse("", mac, sizeof mac, name, sizeof name) == 0);
CHECK(bt_scan_line_parse(S_TRUNC_MAC, mac, sizeof mac, name, sizeof name) == 0);
CHECK(bt_scan_line_parse("Waiting to connect to bluetoothd...\r\n", mac, sizeof mac, name, sizeof name) == 0);
/* MAC 后无名字(截断输出)也要给出设备 */
CHECK(bt_scan_line_parse("[NEW] Device AA:BB:CC:DD:EE:FF\n", mac, sizeof mac, name, sizeof name) == 1);
CHECK(strcmp(mac, "AA:BB:CC:DD:EE:FF") == 0);
CHECK(name[0] == 0);
}
/* ================= 3. hcitool con 解析 ================= */
printf("== bt_acl_parse(hcitool con 真机字节:> / < / SCO / 表头 / 空 / 截断 / 大小写) ==\n");
{
char macs[8][24];
/* 入站(远端发起):M54 的 bug 现场 —— 只认 "< ACL" 时这里返回 0 */
CHECK(bt_acl_parse(S_CON_INBOUND, macs, 8) == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* 出站(本端发起):M49 起就该过,修入站时不许回归 */
CHECK(bt_acl_parse(S_CON_OUTBOUND, macs, 8) == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* 两副耳机混排(一副自主回连、一副本端发起)→ 按输出顺序全收 */
int n = bt_acl_parse("Connections:\n"
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER AUTH ENCRYPT \n"
"\t< ACL 11:22:33:44:55:66 handle 12 state 1 lm SLAVE \n", macs, 8);
CHECK(n == 2);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
CHECK(strcmp(macs[1], "11:22:33:44:55:66") == 0);
/* SCO 链路不是 ACL:两个方向都不收 */
CHECK(bt_acl_parse("\t< SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
CHECK(bt_acl_parse("\t> SCO 33:DE:2E:21:AD:30 handle 13 state 1 lm MASTER \n", macs, 8) == 0);
/* 表头 / 空 / NULL / 无方向符(裸 "ACL" 不能被当连接) */
CHECK(bt_acl_parse("Connections:\n", macs, 8) == 0);
CHECK(bt_acl_parse("", macs, 8) == 0);
CHECK(bt_acl_parse(NULL, macs, 8) == 0);
CHECK(bt_acl_parse("\tACL 33:DE:2E:21:AD:30 handle 1 state 1 lm MASTER \n", macs, 8) == 0);
CHECK(bt_acl_parse("no ACL connection\n", macs, 8) == 0);
/* 截断 MAC(行中 / 行尾都算)与「MAC 后面还跟 hex/':'」的伪 MAC */
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:A\n", macs, 8) == 0);
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:3", macs, 8) == 0);
CHECK(bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30:99 handle 1 state 1\n", macs, 8) == 0);
/* 大小写归一(hcitool 理论上恒大写,别让下游 join 因大小写错过) */
n = bt_acl_parse("\t> ACL 33:de:2e:21:ad:30 handle 11 state 1 lm MASTER \n", macs, 8);
CHECK(n == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* CRLF 行尾(终端/串口转发形态) */
n = bt_acl_parse("\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \r\n", macs, 8);
CHECK(n == 1);
CHECK(strcmp(macs[0], "33:DE:2E:21:AD:30") == 0);
/* max 上限裁剪 */
n = bt_acl_parse("Connections:\n"
"\t> ACL 33:DE:2E:21:AD:30 handle 11 state 1 lm MASTER \n"
"\t< ACL 11:22:33:44:55:66 handle 12 state 1 lm SLAVE \n"
"\t> ACL 22:33:44:55:66:77 handle 13 state 1 lm SLAVE \n", macs, 2);
CHECK(n == 2);
}
/* ================= 4. list-pcms / info 解析 ================= */
printf("== bt_pcm_paths_parse / bt_path_mac / bt_pcm_info_parse ==\n");
{
char paths[4][80];
int n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n", paths, 4);
CHECK(n == 1);
CHECK(strcmp(paths[0], "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc") == 0);
n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n"
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk\r\n"
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/sco\n", paths, 4);
CHECK(n == 2); /* SCO/HFP 端点不算 A2DP */
CHECK(strcmp(paths[1], "/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk") == 0);
CHECK(bt_pcm_paths_parse("", paths, 4) == 0);
/* 真机 bluealsa v4.x 端点路径带末端段,且**结尾字面量不可依赖**:
* 塔 2026-11-11 首连实测是 `/a2dpsrc/sink`(不是 /source),M50 的旧 bt_dev_detect_mac()
* 按「最后一个 / 之后」取 token 就会拿到 "sink" 而不是 MAC。此处两种结尾都验。 */
n = bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_E0_42_6D_79_C5_98/a2dpsnk/source\n", paths, 4);
CHECK(n == 2);
CHECK(strcmp(paths[0], "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink") == 0);
char mac2[24];
CHECK(bt_path_mac(paths[0], mac2, sizeof mac2) == 1);
CHECK(strcmp(mac2, "33:DE:2E:21:AD:30") == 0); /* 不是 "sink" */
CHECK(bt_path_mac(paths[1], mac2, sizeof mac2) == 1);
CHECK(strcmp(mac2, "E0:42:6D:79:C5:98") == 0);
char mac3[24]; /* 无末端段的老形态也兼容 */
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc", mac3, sizeof mac3) == 1);
CHECK(strcmp(mac3, "33:DE:2E:21:AD:30") == 0);
/* 截断输出(半截路径)也不许崩、不许当成端点 */
bt_pcm_paths_parse("/org/bluealsa/hci0/dev_33_D", paths, 4);
char mac[24];
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc", mac, sizeof mac) == 1);
CHECK(strcmp(mac, "33:DE:2E:21:AD:30") == 0);
CHECK(bt_path_mac("/org/bluealsa/hci0/dev_33_DE_2E_21_AD_3", mac, sizeof mac) == 0);
CHECK(bt_path_mac("/org/bluealsa/hci0/nothing", mac, sizeof mac) == 0);
/* 真机原文(M50 2026-11-11 18:26 实测,idle 与 running 各一份),逐字照抄 */
static const char *REAL_IDLE =
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
"Sequence: 0\n"
"Transport: A2DP-source\n"
"Mode: sink\n"
"Running: false\n"
"Format: S16_LE\n"
"Channels: 2\n"
"Sampling: 48000 Hz\n"
"Available codecs: SBC AAC\n"
"Selected codec: AAC\n"
"Delay: 15.0 ms\n"
"SoftVolume: false\n"
"Volume: L: 127 R: 127\n"
"Muted: L: false R: false\n";
static const char *REAL_RUNNING =
"Device: /org/bluez/hci0/dev_33_DE_2E_21_AD_30\n"
"Sequence: 3\n"
"Transport: A2DP-source\n"
"Mode: sink\n"
"Running: true\n"
"Format: S16_LE\n"
"Channels: 2\n"
"Sampling: 48000 Hz\n"
"Available codecs: SBC AAC\n"
"Selected codec: AAC\n"
"Delay: 17.2 ms\n"
"SoftVolume: false\n"
"Volume: L: 127 R: 127\n"
"Muted: L: false R: false\n";
bt_pcm_info_t info;
bt_pcm_info_parse(REAL_IDLE, &info);
CHECK(info.has == 1);
CHECK(strcmp(info.codec, "AAC") == 0); /* Selected codec,不是 Available 列表 */
CHECK(strcmp(info.rate, "48000 Hz") == 0);
CHECK(strcmp(info.channels, "2") == 0);
CHECK(strcmp(info.bits, "16") == 0); /* 从 Format: S16_LE 推出来 */
CHECK(info.running == 0); /* Running: false */
bt_pcm_info_parse(REAL_RUNNING, &info);
CHECK(info.running == 1); /* Running: true 才代表真在传 */
CHECK(strcmp(info.codec, "AAC") == 0);
CHECK(strcmp(info.bits, "16") == 0);
/* 兼容旧/其它形态(有人给的是 Codec:/Bits per Sample:/State: RUNNING) */
bt_pcm_info_parse(" Transport: A2DP-source\n"
" Codec: SBC\n"
" CodecConfiguration: 0x3f\n"
" Sampling: 44100 Hz\n"
" Channels: 2\n"
" Bits per Sample: 16\n"
" State: RUNNING\n", &info);
CHECK(strcmp(info.codec, "SBC") == 0); /* 不被 CodecConfiguration 抢走 */
CHECK(strcmp(info.rate, "44100 Hz") == 0);
CHECK(strcmp(info.bits, "16") == 0);
CHECK(info.running == 1);
/* 只有 Format 一行(属性打包)时的兜底 */
bt_pcm_info_parse(" Format: 48000 Hz, 2 channels, 24 bits\n", &info);
CHECK(info.has == 1);
CHECK(strcmp(info.rate, "48000") == 0);
CHECK(strcmp(info.channels, "2") == 0);
CHECK(strcmp(info.bits, "24") == 0);
CHECK(info.running == -1);
/* 空输出 / 错误输出(endpoint 不存在)→ has=0,不许编造 */
bt_pcm_info_parse("", &info);
CHECK(info.has == 0);
bt_pcm_info_parse("bluealsa-cli: E: CMD \"info\": Couldn't get BlueALSA PCM: "
"Object path not found: '/org/bluealsa/hci0/dev_00/a2dpsrc'\n", &info);
CHECK(info.has == 0);
}
/* ================= 5. 读写分类与授权 ================= */
printf("== bt_cmd_is_write 分类(读/写级) ==\n");
CHECK(bt_cmd_is_write("status") == 0);
CHECK(bt_cmd_is_write("paired") == 0);
CHECK(bt_cmd_is_write("connected") == 0);
CHECK(bt_cmd_is_write("a2dp") == 0);
CHECK(bt_cmd_is_write("devices") == 0);
CHECK(bt_cmd_is_write("on") == 1);
CHECK(bt_cmd_is_write("off") == 1);
CHECK(bt_cmd_is_write("connect") == 1);
CHECK(bt_cmd_is_write("disconnect") == 1);
CHECK(bt_cmd_is_write("pair") == 1);
CHECK(bt_cmd_is_write("unpair") == 1);
CHECK(bt_cmd_is_write("scan") == 1);
/* ================= 6. 读级(status/paired/connected/a2dp/devices) ================= */
printf("== 读级:status/paired/connected/a2dp/devices ==\n");
bt_ctl_execute("status", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "paired=2") != NULL); /* paired-devices 计数 */
CHECK(strstr(r.text, "connected=0") != NULL); /* 内核 ACL 权威:当前无连接 */
bt_ctl_execute("paired", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
CHECK(strstr(r.text, "Device E0:42:6D:79:C5:98 17OP15") != NULL);
/* 没有已配对设备 = 空列表(不是错误) */
snprintf(p, sizeof p, "%s/state/paired_list.txt", T); wf(p, "");
bt_ctl_execute("paired", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(r.text[0] == 0);
snprintf(p, sizeof p, "%s/state/paired_list.txt", T);
wf(p, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice E0:42:6D:79:C5:98 17OP15\n");
/* a2dp:list-pcms → info,无端点时 "A2DP none" */
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc\n");
bt_ctl_execute("a2dp", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "A2DP 33:DE:2E:21:AD:30") != NULL);
CHECK(strstr(r.text, "codec=AAC") != NULL); /* 真机 Selected codec */
CHECK(strstr(r.text, "rate=48000 Hz") != NULL);
CHECK(strstr(r.text, "channels=2") != NULL);
CHECK(strstr(r.text, "bits=16") != NULL);
CHECK(strstr(r.text, "acquired=0") != NULL); /* Running: false(空闲,端点还在) */
CHECK(strstr(r.text, "role=src") != NULL);
{ /* Running: true → acquired=1(权威布尔) */
snprintf(p, sizeof p, "%s/state/running", T); wf(p, "1\n");
bt_ctl_execute("a2dp", NULL, &r);
CHECK(strstr(r.text, "acquired=1") != NULL);
unlink(p);
}
/* 列表里有端点但 info 查不到(刚断开)→ 跳过,不编造 */
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
wf(p, "/org/bluealsa/hci0/dev_00_00_00_00_00_00/a2dpsrc\n");
bt_ctl_execute("a2dp", NULL, &r);
CHECK(strcmp(r.text, "A2DP none\n") == 0);
wf(p, "");
bt_ctl_execute("a2dp", NULL, &r);
CHECK(strcmp(r.text, "A2DP none\n") == 0);
printf("== A2DP 预算:端点全卡死时不许把守护拖住(r1 评审 P2-1) ==\n");
{
char pcms[512];
snprintf(pcms, sizeof pcms,
"/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_11_22_33_44_55_66/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_AA_BB_CC_DD_EE_FF/a2dpsrc/sink\n"
"/org/bluealsa/hci0/dev_12_34_56_78_9A_BC/a2dpsnk/source\n");
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, pcms);
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
long t0 = mono_ms();
bt_ctl_execute("a2dp", NULL, &r);
long dt = mono_ms() - t0;
printf(" budget-a2dp(4 端点全卡死) = %ld ms\n", dt);
CHECK(dt < 1200); /* 旧实现实测 6053ms */
CHECK(r.code == MEOW_OK);
unlink(p);
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
/* status JSON 的 a2dp 摘要:无连接→0 次 fork;有连接→TTL 缓存(第二次几乎免费) */
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "yes\n");
snprintf(p, sizeof p, "%s/state/pcms.txt", T);
wf(p, "/org/bluealsa/hci0/dev_33_DE_2E_21_AD_30/a2dpsrc/sink\n");
snprintf(p, sizeof p, "%s/state/hang_info", T); wf(p, "1\n");
static char js[8192];
g_bt_a2dp_cache_ms = 0;
long t1 = mono_ms();
build_status_json(js, sizeof js);
long d1 = mono_ms() - t1;
long t2 = mono_ms();
build_status_json(js, sizeof js);
long d2 = mono_ms() - t2;
printf(" budget-status-json 1st = %ld ms, 2nd(cache) = %ld ms\n", d1, d2);
CHECK(d1 < 700); /* 单次刷新受 BT_A2DP_STATUS_BUDGET_MS 约束 */
CHECK(d2 < 60); /* 缓存命中:不再 fork */
CHECK(strstr(js, "\"a2dp_codec\"") != NULL); /* 字段保留(MeowPanel btCard 依赖) */
unlink(p);
unlink(snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T) ? p : p);
snprintf(p, sizeof p, "%s/state/pcms.txt", T); wf(p, "");
}
printf("== 扫描缓存 → bt devices(真机 ANSI 字节 + devices 尾块 + 去重) ==\n");
{
char cache[4096];
/* 结构与真机 /tmp/meow_bt_scan.txt 一致:开头等待行 + ESC[P + 提示符,实时 NEW 行,
* 末尾 devices 块,最后一行被截断(没有换行)。 */
snprintf(cache, sizeof cache,
"Waiting to connect to bluetoothd...\r\033[P\033[P\033[P"
"\033[0;94m[bluetooth]\033[0m# \033[K\r\033[KAgent registered\n"
"\033[0;94m[bluetooth]\033[0m# scan on\n"
"%s"
"%s"
"\033[0;94m[bluetooth]\033[0m# scan off\n"
"\033[0;94m[bluetooth]\033[0m# devices\n"
"Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n"
"Device E0:42:6D:79:C5:98 17OP15\n"
"Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63\n"
"\033[0;94m[bluetooth]\033[0m# quit\n"
"\033[0;94m[bluetooth]\033[0m# \r\033[K"
"[\001\033[0;92m\002NEW\001\033[0m\002] Device AA:BB:CC:DD:EE:FF Earbuds",
S_NEW_PUDDING, S_CHG_POWERED);
snprintf(p, sizeof p, "%s/bt_scan.txt", T); wf(p, cache);
bt_ctl_execute("devices", NULL, &r);
CHECK(r.code == MEOW_OK);
/* 实时 NEW 行(ANSI 开头)现在能解析出来 */
CHECK(strstr(r.text, "Device 79:85:D6:7B:73:63 79-85-D6-7B-73-63") != NULL);
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
CHECK(strstr(r.text, "Device E0:42:6D:79:C5:98 17OP15") != NULL);
/* 截断的最后一行(无换行)也要收;[CHG] 行不许混进来 */
CHECK(strstr(r.text, "Device AA:BB:CC:DD:EE:FF Earbuds") != NULL);
CHECK(strstr(r.text, "Controller 4C:BC:98") == NULL);
CHECK(strstr(r.text, "Powered") == NULL);
/* 去重:79:.. 实时行与 devices 块各一次,输出只一行 */
{ int cnt = 0; const char *q = r.text;
while ((q = strstr(q, "79:85:D6:7B:73:63")) != NULL) { cnt++; q += 17; }
CHECK(cnt == 1); }
/* 缓存不存在 → 空文本 */
unlink(p);
bt_ctl_execute("devices", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(r.text[0] == 0);
}
/* ================= 7. 写级:on/off + connect/disconnect/pair/unpair ================= */
printf("== bt on/off:按 power 真态定稿 + rfkill 顺序(先 unblock 再 up / 先 down 再 block) ==\n");
/* 初始两个 bluetooth rfkill 节点 soft=1(真机形态)。旧实现在这里必败:
* 先 hciconfig up 被 rfkill 拦下,再 unblock 也不会重试 → 报「未生效」(假失败)。 */
bt_ctl_execute("on", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已复查") != NULL);
CHECK(bt_powered_now(1500) == 1);
{ /* 两个节点都必须被解 block(只写首个会让另一个仍 block) */
int all_unblocked = 1;
for (int i = 0; i < 2; i++) {
char f[300], v[8] = "1";
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
FILE *fp = fopen(f, "r");
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
if (strcmp(v, "0") != 0) all_unblocked = 0;
}
CHECK(all_unblocked == 1);
}
bt_ctl_execute("off", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(bt_powered_now(1500) == 0);
{ /* 关也要写全部节点 */
int all_blocked = 1;
for (int i = 0; i < 2; i++) {
char f[300], v[8] = "";
snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0);
FILE *fp = fopen(f, "r");
if (fp) { if (fscanf(fp, "%7s", v) != 1) v[0] = 0; fclose(fp); }
if (strcmp(v, "1") != 0) all_blocked = 0;
}
CHECK(all_blocked == 1);
}
/* 反向验证:桩确实会在 blocked 时拒绝 up(否则上面的断言是空转) */
{ char f[300]; snprintf(f, sizeof f, "%s/rfkill/rfkill0/soft", T); wf(f, "1\n");
snprintf(f, sizeof f, "%s/rfkill/rfkill3/soft", T); wf(f, "1\n");
char out[128]; snprintf(out, sizeof out, "%s/hciconfig hci0 up 2>&1", sb_hc);
runcmd_res_t rr; run_cmd_ex(out, sizeof out, 800, out, &rr);
CHECK(rr.status != RUNCMD_OK); }
{ char f[300]; snprintf(f, sizeof f, "%s/bt/hci0/power", T); wf(f, "off\n");
for (int i = 0; i < 2; i++) { snprintf(f, sizeof f, "%s/rfkill/rfkill%d/soft", T, i ? 3 : 0); wf(f, "0\n"); } }
printf("== 预算上界:命令卡死时写操作必须落在客户端 1500ms IPC 窗口内(r1 评审 P1) ==\n");
{ /* connect:bluetoothctl 卡住 5s(耳机没上电的真实形态)→ 总耗时必须 < 1450ms */
long t0 = mono_ms();
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
long dt = mono_ms() - t0;
printf(" budget-connect = %ld ms (code=%d)\n", dt, r.code);
CHECK(dt < 1450);
CHECK(r.code == MEOW_ERR_NODEV); /* 未连上就必须报未生效 */
CHECK(strstr(r.text, "未生效") != NULL);
}
{ long t0 = mono_ms();
bt_ctl_execute("pair", "CC:CC:CC:CC:CC:CC", &r);
long dt = mono_ms() - t0;
printf(" budget-pair = %ld ms (code=%d)\n", dt, r.code);
CHECK(dt < 1450);
CHECK(r.code == MEOW_ERR_NODEV);
}
{ /* bt on:hciconfig 卡住 5s → 同样必须 < 1450ms(旧实现实测 2424ms) */
char f[300]; snprintf(f, sizeof f, "%s/state/hang_hciconfig", T); wf(f, "1\n");
long t0 = mono_ms();
bt_ctl_execute("on", NULL, &r);
long dt = mono_ms() - t0;
printf(" budget-bt-on = %ld ms (code=%d)\n", dt, r.code);
CHECK(dt < 1450);
CHECK(r.code == MEOW_ERR_NODEV); /* 卡住未生效:如实报 */
unlink(f);
}
{ /* 熔断:连续 3 次超时后,第 4 次必须**立刻**拒绝、不再 fork(防 55 个挂死 bluetoothctl
* 吃光进程槽的事故)。冷却 30s,测试里手工复位继续后续用例。 */
CHECK(g_bt_tmo_streak >= BT_TMO_STREAK_MAX);
long t0 = mono_ms();
bt_ctl_execute("connect", "CC:CC:CC:CC:CC:CC", &r);
long dt = mono_ms() - t0;
printf(" circuit-breaker 4th = %ld ms, code=%d\n", dt, r.code);
CHECK(dt < 100); /* 没再起子进程 */
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "熔断中") != NULL);
g_bt_tmo_streak = 0; /* 测试专用复位(生产靠 30s 冷却+成功清零) */
g_bt_tmo_until = 0;
}
{ /* 恢复:正常 on,供后面的用例使用;同时验证「非超时返回会清零熔断计数」 */
bt_ctl_execute("on", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(g_bt_tmo_streak == 0);
}
printf("== 入站回连(hcitool con 的 `> ACL`):connected / status / connect 复查都必须认 ==\n");
{ /* 真机复现场景(M54):耳机自己回连,行首是 '>'。修前:connected 空、connected=0、
* connect 报「未生效(hcitool con 无该 ACL)」。桩按 conn 文件内容生成方向。 */
snprintf(p, sizeof p, "%s/state/conn_33:DE:2E:21:AD:30", T); wf(p, "in\n");
bt_ctl_execute("connected", NULL, &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "Device 33:DE:2E:21:AD:30 MOONDROP Pudding") != NULL);
bt_ctl_execute("status", NULL, &r);
CHECK(strstr(r.text, "connected=1") != NULL);
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1); /* 写后复查走的也是这个解析器 */
/* 已连(入站)时再点连接:ACL 仍是 '>' 行,必须报已连接,不许报「未生效」 */
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已连接") != NULL);
CHECK(strstr(r.text, "未生效") == NULL);
/* 只改了链路类型(SCO 行)→ 不算连接:防止「把 SCO 当 ACL」的宽匹配回归 */
wf(p, "sco\n");
bt_ctl_execute("status", NULL, &r);
CHECK(strstr(r.text, "connected=0") != NULL);
bt_ctl_execute("connected", NULL, &r);
CHECK(r.text[0] == 0);
unlink(p);
}
printf("== bt connect:成功路径以 hcitool con 复查 ==\n");
bt_ctl_execute("connect", "33:DE:2E:21:AD:30", &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已连接") != NULL);
CHECK(strstr(r.text, "hcitool con") != NULL);
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 1);
printf("== bt disconnect:成功路径以 hcitool con 复查 ==\n");
bt_ctl_execute("disconnect", "33:DE:2E:21:AD:30", &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已断开") != NULL);
CHECK(bt_acl_has_tri("33:DE:2E:21:AD:30", 1500) == 0);
printf("== bt connect 失败:bluetoothctl 报错 + 无 ACL → ERR_NODEV(旧实现恒报成功) ==\n");
bt_ctl_execute("connect", "AA:AA:AA:AA:AA:AA", &r);
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "未生效") != NULL);
CHECK(strstr(r.text, "br-connection-refused") != NULL); /* 失败原因来自 stderr/stdout 片段 */
printf("== bt disconnect 假成功:命令 exit 0 但 ACL 仍在 → 仍报 ERR_NODEV ==\n");
/* BB:* 的 disconnect 桩打印成功但不删 ACL(模拟「命令说成功、链路其实还在」)。 */
snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "in\n");
snprintf(p, sizeof p, "%s/state/paired_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r); /* 入站 ACL 残留也必须被看见 */
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
snprintf(p, sizeof p, "%s/state/conn_BB:BB:BB:BB:BB:BB", T); wf(p, "yes\n");
bt_ctl_execute("disconnect", "BB:BB:BB:BB:BB:BB", &r); /* 出站形态同样要看见 */
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "仍有该 ACL") != NULL);
bt_ctl_execute("disconnect", "AA:AA:AA:AA:AA:AA", &r); /* 本来就没 ACL:真态=已断开 */
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已断开") != NULL);
bt_ctl_execute("connect", "11:22:33:44:55:66", &r);
CHECK(r.code == MEOW_OK);
printf("== bt pair/unpair:以 bluetoothctl info Paired: 复查 ==\n");
bt_ctl_execute("pair", "11:22:33:44:55:66", &r);
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已配对") != NULL);
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired", 1500) == 1);
bt_ctl_execute("unpair", "11:22:33:44:55:66", &r); /* remove → info 查不到也算解除 */
CHECK(r.code == MEOW_OK);
CHECK(strstr(r.text, "已解除配对") != NULL);
CHECK(bt_info_flag("11:22:33:44:55:66", "Paired", 1500) == -1);
bt_ctl_execute("pair", "AA:AA:AA:AA:AA:AA", &r);
CHECK(r.code == MEOW_ERR_NODEV);
CHECK(strstr(r.text, "未生效") != NULL);
bt_ctl_execute("connect", "", &r);
CHECK(r.code == MEOW_ERR_INVAL); /* 缺 mac → INVAL */
printf("== bt scan on/off(写级;桩 bluetoothctl 现在被 scan 真正调用) ==\n");
bt_ctl_execute("scan", NULL, &r);
CHECK(r.code == MEOW_OK);
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;
caller_t sys; memset(&sys, 0, sizeof sys); sys.kind = CALL_SYS;
snprintf(sys.pkg, sizeof sys.pkg, "meow_net");
CHECK(bt_write_authorize(&other, 1, "connect") == MEOW_ERR_PERM); /* 非系统 → 拒 */
CHECK(bt_write_authorize(&sys, 1, "connect") == MEOW_OK); /* 系统+meow → 放 */
CHECK(bt_write_authorize(&sys, 0, "connect") == MEOW_ERR_PERM); /* 系统+hiby → 拒 */
CHECK(bt_write_authorize(&other, 1, "status") == MEOW_OK); /* 读级任意 → 放 */
(void)cmd;
{ char rm[320]; snprintf(rm, sizeof rm, "rm -rf '%s'", T); (void)!system(rm); } /* 退出即清,保证可重复 */
if (g_fail) { printf("\nBT_CTL TEST: %d FAIL\n", g_fail); return 1; }
printf("\nBT_CTL TEST: ALL PASS\n");
return 0;
}