fix(audiod): 长流卡死根治 — 防卡死护栏全量落地(AUDIOD_DESIGN §12)

根因(级联死锁): 内核驱动 wedge(hbc3000/cs43198 时钟域族, write/PREPARE/amixer ioctl
卡死在内核) → 播放线程永不返回 → ring 填满 → IO 线程在 ring_write 无超时 cond_wait
永久睡眠 → accept 停摆(backlog 满后 CLI ECONNREFUSED) → ncm 堵 socket 写 → 全链冻结。
同效路径: popen_read_tmo 无界 waitpid 随挂死的 amixer 子进程拖死持锁线程。

修法:
- ring 写有界等待(MEOW_AUDIO_RING_WAIT_MS 缺省 5s)超时丢弃+计数(dropped=), IO 永生;
  等待期 sid 代际守卫(抢占中等待的旧流 PCM 不再注入新流 ring)
- 全部 cond 等待 timedwait 化(播放 1s 周期自愈, g_cond 改 CLOCK_MONOTONIC);
  信号处理器只置 g_stop(不再调非 async-signal-safe 的 cond_signal)
- popen 收割有界: WNOHANG 300ms → SIGKILL 300ms → D 态放弃收割(泄漏换永生);
  路由线程 amixer 调用不持 g_mu; QUERY 改报 g_route_cur 缓存(IO 热路径不再 popen)
- ALSA 改 O_NONBLOCK + poll(POLLOUT,1s): 连续 3 次停摆判 wedge → PREPARE 自救无效即
  关卡进丢弃模式(stall=), 下一路 OPEN 重开重试
- server→client 下行帧单发 write 写不全即整帧丢弃(txdrop=), 杜绝半帧错位/慢客户端堵 IO
- 主线程 watchdog: IO/播放心跳 10s 静默告警; io 线程 poll 出错不退出(listener 不丢);
  backlog 4→8; RESUME/PAUSE 补 cond 唤醒
- SDK meow_ui_audio.c + CLI: connect 非阻塞+poll 2s, SO_SNDTIMEO/RCVTIMEO 2s
  (audiod 真死时 App 报错重开, 不再永久阻塞)

验证: 宿主 38/38 PASS(旧 19 回归 + 新增 T5 长流 3min 等效 31.7MB 持续灌 played==written
underrun/dropped=0 喂流中 listener 探活 15/15; T6 wedge 故障注入生存 — 旧代码同款注入
挂死(90s timeout 强制 kill), 新代码 IO/accept/preempt/SIGTERM 全活; T7 慢客户端 tx 丢弃);
qemu-mipsel 静态二进制 status/tone 全通(written=played=13230 underrun=0);
-Wall -Wextra 0 warning(host+mipsel+SDK 双端编译)。
meow_audiod md5 86a8b6793d516c90bbd12c2997b30d8f / meow_audio c23b96490842f54324b855fd9c2467a5
(fw_content.conf 已更)。暂不部署真机(主会话统一); meow_ui.a/App 重链留主会话。
This commit is contained in:
MeowOS committed 2026-09-01 10:32:15 +08:00
1 parent 243f328499
commit e1e1331e44
10 files changed
+477 -67

No files matched your search

Binary file not shown.
+23 -1
View File
@@ -22,6 +22,8 @@
#include <poll.h>
#include <math.h>
#include <time.h>
#include <fcntl.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/un.h>
@@ -54,15 +56,35 @@ static int read_all(int fd, uint8_t *p, size_t n)
return 0;
}
/* 连接 audiod:connect 非阻塞 + poll 2s 有界(对端 backlog 满/wedge 时绝不永久阻塞);
* 建连后 SO_SNDTIMEO 2s(tone 长写在 audiod 停摆时报错退出而非挂死)。 */
static int connect_sock(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", g_sock);
if (connect(fd, (struct sockaddr *)&a, sizeof a) < 0) { close(fd); return -1; }
int r = connect(fd, (struct sockaddr *)&a, sizeof a);
if (r < 0 && errno == EINPROGRESS) {
struct pollfd p;
p.fd = fd; p.events = POLLOUT; p.revents = 0;
if (poll(&p, 1, 2000) > 0) {
int err = 0;
socklen_t el = sizeof err;
if (getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
} else {
r = -1;
}
}
if (r < 0) { close(fd); return -1; }
if (fl >= 0) fcntl(fd, F_SETFL, fl);
struct timeval tv;
tv.tv_sec = 2; tv.tv_usec = 0;
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
return fd;
}
Binary file not shown.
Binary file not shown.
+248 -59
View File
@@ -20,6 +20,17 @@
* (HibyOS 模式下 audiod 物理不存在,绝不碰声卡)。测试/宿主用 env MEOW_AUDIO_FORCE=1
* 跳过门控。
*
* 防卡死护栏(2026-09-01,真机长流卡死教训——驱动 wedge 时绝不允许级联冻死全守护):
* - ring 写**有界等待**(MEOW_AUDIO_RING_WAIT_MS,缺省 5000ms):超时丢弃该 PCM 块并计数
* (dropped=),IO 线程永不永久睡眠 → accept/读循环永生;
* - 所有 cond 等待均为 timedwait(1s 周期自愈,防丢失唤醒);
* - popen(amixer) 收割有界:WNOHANG + SIGKILL + 仍不死则放弃收割(宁可泄漏不卡线程);
* - ALSA 写改 O_NONBLOCK + poll(POLLOUT,1s):连续 3 次停摆判定设备 wedge → 关卡进入
* 丢弃模式(stall=),下一路 OPEN 重开重试;
* - server→client 下行帧单发 write,写不全即整帧丢弃(txdrop=),绝不因客户端不读堵 IO 循环;
* - 主线程 watchdog:IO/播放线程心跳 10s 无活动即告警日志;
* - io 线程 poll 出错只记日志不退出(listener 不丢)。
*
* 音频路径双形态:
* - 真机/ALSA:env MEOW_AUDIO_SINK 缺省自动检测 /dev/snd/pcmC0D0p ∈ 存在 → open ALSA UAPI
* (非 mmap:HW_PARAMS + PREPARE + write)。musl 的 sound/asound.h masks/intervals 是
@@ -70,6 +81,11 @@
#define VOL_DEFAULT 60 /* settings.conf audio.volume 缺省 */
#define RING_MIN 4096
#define RING_MAX (1u << 20)
#define RING_WAIT_DEFAULT 5000 /* ring 满时 IO 线程等待上限 ms(超时丢弃,防背压变死锁) */
#define COND_WAIT_MS 1000 /* 播放线程 cond 周期醒(自愈丢失唤醒 + 响应 g_stop) */
#define ALSA_POLL_MS 1000 /* ALSA 可写 poll 超时;连续 ALSA_STALL_MAX 次判定 wedge */
#define ALSA_STALL_MAX 3
#define WD_SILENT_SEC 10 /* watchdog:线程心跳静默上限(> RING_WAIT 余量) */
/* 客户端流状态 */
enum { CLS_OPEN = 0, CLS_PAUSED = 1, CLS_DRAINING = 2 }; /* DRAINING=被抢占,PCM 丢弃 */
@@ -94,12 +110,12 @@ static int g_logfd = -1;
static volatile sig_atomic_t g_stop = 0;
/* ---- 共享态(g_mu 保护 + g_cond 唤醒) ---- */
/* ---- 共享态(g_mu 保护 + g_cond 唤醒;g_cond 用 CLOCK_MONOTONIC,main 中初始化) ---- */
/* 前向声明(供 cli_free/dispatch 使用;定义见下) */
static void g_sid_reset(void);
static void _audio_mark_reconfig_needed(void);
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t g_cond = PTHREAD_COND_INITIALIZER;
static pthread_cond_t g_cond;
struct client {
int used;
@@ -128,6 +144,21 @@ static uint32_t g_next_sid = 1;
static uint64_t g_written_frames = 0; /* 当前流:客户端写入 ring 的帧 */
static uint64_t g_played_frames = 0; /* 当前流:播放线程写到设备的帧 */
static uint32_t g_underrun = 0;
static uint64_t g_drop_frames = 0; /* ring 满超时丢弃的帧(背压护栏,QUERY dropped=) */
static uint32_t g_stall = 0; /* ALSA 可写 poll 超时次数(设备停摆;QUERY stall=) */
static uint32_t g_txdrop = 0; /* server→client 下行帧丢弃(客户端不读;QUERY txdrop=) */
/* ring 写等待上限(env MEOW_AUDIO_RING_WAIT_MS 覆盖;测试用小值) */
static int g_ring_wait_ms = RING_WAIT_DEFAULT;
/* 线程心跳(watchdog 用;宽松计数,不加锁) */
static volatile unsigned g_hb_io = 0;
static volatile unsigned g_hb_play = 0;
/* 测试钩子(仅 env 显式开启;MEOW_AUDIO_TEST_WRITE_DELAY_MS / _WEDGE_AFTER_BYTES) */
static int g_test_write_delay_ms = 0;
static long long g_test_wedge_after = -1; /* dev_write 累计字节≥此值后模拟设备 wedge(睡到 g_stop) */
static uint64_t g_dev_bytes = 0;
/* ring(SPSC:IO 线程写,播放线程读;g_mu + g_cond 保护) */
static uint8_t *g_ring = NULL;
@@ -206,31 +237,43 @@ static int boot_mode_ok(void)
}
/* ---- 帧发送(仅 IO 线程用:OPEN_ACK/STATE/PREEMPTED/ACK) ---- */
static int write_all(int fd, const uint8_t *p, size_t n)
/* 单发即丢:客户端 fd 是 O_NONBLOCK;拼装整帧一次 write(),写不全(对端不读/缓冲满)
* 就整帧丢弃并计数——绝不为慢客户端堵 IO 循环,也绝不写出半个帧让对端协议错位。
* 丢 OPEN_ACK/ACK 的后果 = 客户端 3s 超时判流死重连(可接受);丢 PREEMPTED 由 App 侧
* 播放停摆自检兜底。 */
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
{
size_t off = 0;
while (off < n) {
ssize_t w = write(fd, p + off, n - off);
if (w < 0) { if (errno == EINTR) continue; return -1; }
if (w == 0) return -1;
off += (size_t)w;
if (len > MEOW_IPC_MAX) return -1;
uint8_t frame[MEOW_IPC_HDR_LEN + MEOW_IPC_MAX];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(frame + 0, &magic, 4);
memcpy(frame + 4, &ver, 2);
memcpy(frame + 6, &type, 2);
memcpy(frame + 8, &req_id, 4);
memcpy(frame + 12, &len, 4);
if (len && payload) memcpy(frame + MEOW_IPC_HDR_LEN, payload, len);
size_t total = MEOW_IPC_HDR_LEN + len;
ssize_t w = write(fd, frame, total);
if (w != (ssize_t)total) {
g_txdrop++;
if ((g_txdrop & 63u) == 1u)
logmsg("tx drop: fd=%d type=0x%04x len=%u wrote=%zd (total x%u, client not reading?)",
fd, (unsigned)type, (unsigned)len, w, (unsigned)g_txdrop);
return -1;
}
return 0;
}
static int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len)
/* cond 有界等待(g_cond 为 CLOCK_MONOTONIC;调用方已持 g_mu)。 */
static void cond_wait_tmo(int ms)
{
uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC;
uint16_t ver = MEOW_IPC_VERSION;
memcpy(hdr + 0, &magic, 4);
memcpy(hdr + 4, &ver, 2);
memcpy(hdr + 6, &type, 2);
memcpy(hdr + 8, &req_id, 4);
memcpy(hdr + 12, &len, 4);
if (write_all(fd, hdr, sizeof hdr) != 0) return -1;
if (len && payload && write_all(fd, payload, len) != 0) return -1;
return 0;
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
ts.tv_sec += ms / 1000;
ts.tv_nsec += (long)(ms % 1000) * 1000000L;
if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; }
pthread_cond_timedwait(&g_cond, &g_mu, &ts);
}
/* ---- frame 大小/位深 ---- */
@@ -279,22 +322,40 @@ static size_t ring_read_locked(uint8_t *dst, size_t want)
return n;
}
/* IO 线程写整块(调用方已持 g_mu,但可能阻塞等待空间——返回后释放由调用方负责)。
* 返回 0=写入;-1=流已失效(被抢占/停止,丢弃)。 */
static int ring_write_locked(const uint8_t *src, size_t want)
/* IO 线程写整块(调用方已持 g_mu)。**有界等待**:ring 满最多等 g_ring_wait_ms,
* 超时丢弃该块(返回 0)——背压绝不能退化成 IO 线程永久睡眠(真机长流卡死根因:
* 播放线程堵死 → ring 满 → IO cond_wait 永不醒 → accept 停摆 → 全链冻结)。
* sid 为写入方开流时的代际:等待期间被抢占(g_st.sid 变化)立即丢弃(返回 0),
* 防止旧流 PCM 注入新流 ring(ring_rebuild 后错位)。
* 返回实际写入字节数;0=被丢弃(超时/流死/被抢占)。 */
static size_t ring_write_locked(const uint8_t *src, size_t want, uint32_t sid)
{
size_t fs = (size_t)frame_size();
if (fs == 0) fs = 1;
size_t n = (want / fs) * fs;
if (n == 0) return 0;
if (!g_ring || g_ring_cap < n) return 0; /* ring 未建/异常小:直接丢 */
struct timespec deadline;
clock_gettime(CLOCK_MONOTONIC, &deadline);
deadline.tv_sec += g_ring_wait_ms / 1000;
deadline.tv_nsec += (long)(g_ring_wait_ms % 1000) * 1000000L;
if (deadline.tv_nsec >= 1000000000L) { deadline.tv_sec++; deadline.tv_nsec -= 1000000000L; }
for (;;) {
if (g_stop) return -1;
/* 流失效:非当前授权/被抢占/暂停(PAUSE 不新增,但已缓冲保留) */
if (!g_st.active)
return -1;
if (g_stop) return 0;
/* 流失效/代际变化:非当前授权、被抢占、被停止 → 丢弃 */
if (!g_st.active || g_st.sid != sid)
return 0;
size_t freeb = g_ring_cap - ring_bytes();
if (freeb >= n) break;
pthread_cond_wait(&g_cond, &g_mu);
int wr = pthread_cond_timedwait(&g_cond, &g_mu, &deadline);
if (wr == ETIMEDOUT) {
g_drop_frames += n / fs;
static unsigned s_droplog = 0;
if ((s_droplog++ & 63u) == 0)
logmsg("ring full >%dms: dropped %zu bytes (total dropped=%llu frames, playback stuck?)",
g_ring_wait_ms, n, (unsigned long long)g_drop_frames);
return 0;
}
}
size_t wpos = g_wpos;
size_t first = g_ring_cap - wpos;
@@ -303,7 +364,7 @@ static int ring_write_locked(const uint8_t *src, size_t want)
if (take < n) memcpy(g_ring, src + take, n - take);
g_wpos = (g_wpos + n) % g_ring_cap;
pthread_cond_signal(&g_cond);
return 0;
return n;
}
/* 重建 ring(调用方已持 g_mu;按当前授权流参数算 ~200ms 容量;重建=清空旧流缓冲) */
@@ -327,6 +388,10 @@ static void ring_rebuild(void)
}
/* ---- amixer 封装(popen 限时;无 amixer → 优雅失败) ---- */
/* 读子进程 stdout(非阻塞+poll 限时)。收割**必须有界**:驱动 wedge 时 amixer 可能
* 挂在内核 ioctl 里,无条件 waitpid 会把调用线程(路由线程持 g_mu / IO 线程)一起拖死
* ——真机长流卡死的级联路径之一。策略:先 WNOHANG 等 300ms;不死 → SIGKILL 再等 300ms;
* 仍不死(D 态)→ 放弃收割(泄漏一个子进程,换线程不卡死;已记日志)。 */
static char *popen_read_tmo(int tmo_ms, const char *cmd)
{
int p[2];
@@ -350,7 +415,23 @@ static char *popen_read_tmo(int tmo_ms, const char *cmd)
off += (size_t)rd;
}
close(p[0]);
waitpid(pid, NULL, 0);
int reaped = 0;
for (int i = 0; i < 30; i++) { /* 300ms 优雅退出窗口 */
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
if (!reaped) {
logmsg("popen: child %ld did not exit in 300ms, SIGKILL (%s)", (long)pid, cmd);
kill(pid, SIGKILL);
for (int i = 0; i < 30; i++) {
if (waitpid(pid, NULL, WNOHANG) == pid) { reaped = 1; break; }
struct timespec ts = { 0, 10 * 1000000L };
nanosleep(&ts, NULL);
}
if (!reaped)
logmsg("popen: child %ld unkillable (D-state, driver wedged?) — abandon reap", (long)pid);
}
buf[off] = 0;
return buf;
}
@@ -411,7 +492,9 @@ static const char *port_name(int port)
}
}
/* 应用路由(numid=9)。port 0=idle;写后 cget 校验,失败记日志不重试风暴。 */
/* 应用路由(numid=9)。port 0=idle;写后 cget 校验,失败记日志不重试风暴。
* g_route_cur 为 plain int 松散写(路由线程不持 g_mu 调本函数——amixer popen 虽已有界,
* 仍不该在持锁状态执行外部命令;int 写对 MIPS32 天然原子,竞争最坏是 QUERY 读到上一拍值)。 */
static void apply_route(int port)
{
if (amixer_set(9, port, 1) < 0) {
@@ -437,10 +520,11 @@ static int read_switch(const char *path)
return atoi(buf);
}
/* 路由评估(移植自 meow_route.c):4.4 优先;都不插不动;写失败不重试风暴。 */
static void route_evaluate(int *last_hb, int *last_bal, int *last_target)
/* 路由评估(移植自 meow_route.c):4.4 优先;都不插不动;写失败不重试风暴。
* manual:调用方快照的 g_route_manual(本函数不持 g_mu、不读全局)。 */
static void route_evaluate(int manual, int *last_hb, int *last_bal, int *last_target)
{
if (g_route_manual != -1) return; /* 手动模式:不自动切 */
if (manual != -1) return; /* 手动模式:不自动切 */
int hb = read_switch("/sys/class/switch/headset/state");
int bal = read_switch("/sys/class/switch/balance/state");
if (hb < 0 || bal < 0) return;
@@ -614,12 +698,12 @@ static int hw_probe(void)
if (g_sink != SINK_ALSA) { g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; return 0; }
if (g_hw_probed) return 0;
#ifdef AUDIOD_HAVE_ALSA
int fd = open(DEV_PCM, O_RDWR);
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd >= 0) {
if (alsa_hw_params(fd, 44100, 2, SNDRV_PCM_FORMAT_S16_LE) == 0) { g_hw_fmt = AUDIO_FMT_S16; close(fd); g_hw_probed = 1; return 0; }
close(fd);
}
fd = open(DEV_PCM, O_RDWR);
fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd >= 0) {
if (alsa_hw_params(fd, 44100, 2, SNDRV_PCM_FORMAT_S32_LE) == 0) { g_hw_fmt = AUDIO_FMT_S32; close(fd); g_hw_probed = 1; return 0; }
close(fd);
@@ -652,6 +736,7 @@ static void wav_header(int fd, int rate, int ch, int bits, uint32_t data_len)
}
/* ---- 播放线程设备管理(dev_open/close/write 皆在播放线程调用) ---- */
static void dev_close(void); /* 前向:dev_write 的 wedge 判死路径要用 */
static int dev_open(void)
{
@@ -659,7 +744,9 @@ static int dev_open(void)
int bits = fmt_bps((int)fmt) * 8;
if (g_sink == SINK_ALSA) {
#ifdef AUDIOD_HAVE_ALSA
int fd = open(DEV_PCM, O_RDWR);
/* O_NONBLOCK:写路径走 poll(POLLOUT)+限时(dev_write),write 本身永不睡眠——
* 驱动 wedge 时绝不能把播放线程永久压进内核(真机长流卡死根因防线)。 */
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
if (fd < 0) { logmsg("dev open %s: %s", DEV_PCM, strerror(errno)); return -1; }
if (alsa_hw_params(fd, rate, ch, alsa_fmt_to_snd((int)fmt)) != 0) {
logmsg("dev hw_params %u/%u/%s failed", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt));
@@ -683,11 +770,29 @@ static int dev_open(void)
static void dev_write(uint8_t *buf, size_t n)
{
/* 测试钩子(仅 env 开启):写延迟(模拟慢设备/实时 pacing)与 wedge(模拟驱动卡死——
* 睡到 g_stop,复现真机「播放线程永不返回」故障注入,验证 IO 线程/ring 不死锁)。 */
if (g_test_write_delay_ms > 0) {
struct timespec ts = { g_test_write_delay_ms / 1000, (long)(g_test_write_delay_ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
if (g_test_wedge_after >= 0) {
if (g_dev_bytes >= (uint64_t)g_test_wedge_after) {
static int s_wedged = 0;
if (!s_wedged) { s_wedged = 1; logmsg("TEST: device wedged (simulated after %llu bytes)", (unsigned long long)g_dev_bytes); }
while (!g_stop) {
struct timespec ts = { 0, 100 * 1000000L };
nanosleep(&ts, NULL);
}
return;
}
g_dev_bytes += n;
}
if (g_sink == SINK_NULL) { g_played_frames += n / (size_t)frame_size(); return; }
if (g_sink == SINK_ALSA) {
#ifdef AUDIOD_HAVE_ALSA
if (g_dev_fd < 0) {
/* 设备开失败(hw_params 拒/无卡):丢弃 PCM 防 ring 堵死 + 背压卡死客户端;
/* 设备开失败(hw_params 拒/无卡/wedge 后被判死):丢弃 PCM 防 ring 堵死 + 背压卡死客户端;
* 日志限频(每 256 次一记),避免 write(-1) 错误风暴(真机踩过)。 */
static unsigned s_drop = 0;
if ((s_drop++ % 256) == 0)
@@ -695,10 +800,33 @@ static void dev_write(uint8_t *buf, size_t n)
g_played_frames += n / (size_t)frame_size();
return;
}
/* O_NONBLOCK + poll(POLLOUT) 模式:写前等可写(上限 ALSA_POLL_MS)。
* 连续 ALSA_STALL_MAX 次超时 = 设备停摆(驱动 wedge 特征)→ PREPARE 自救仍无效
* 则关卡进入丢弃模式(g_dev_open=0),下一路 OPEN 重开重试——播放线程绝不永久
* 堵在内核 write 里(真机长流卡死的级联源头)。 */
int stalls = 0;
size_t off = 0;
while (off < n) {
if (g_stop) break;
struct pollfd pf;
pf.fd = g_dev_fd; pf.events = POLLOUT; pf.revents = 0;
int pr = poll(&pf, 1, ALSA_POLL_MS);
if (pr == 0) {
g_stall++;
stalls++;
logmsg("alsa stall #%u (poll %dms no space, xrun-ish) -> PREPARE",
(unsigned)g_stall, ALSA_POLL_MS);
ioctl(g_dev_fd, SNDRV_PCM_IOCTL_PREPARE, 0);
if (stalls >= ALSA_STALL_MAX) {
logmsg("alsa WEDGED: %d consecutive stalls, close device + drop PCM until next OPEN", stalls);
dev_close();
return;
}
continue;
}
if (pr < 0) { if (errno == EINTR) continue; logmsg("alsa poll err: %s", strerror(errno)); break; }
ssize_t w = write(g_dev_fd, buf + off, n - off);
if (w > 0) { off += (size_t)w; continue; }
if (w > 0) { off += (size_t)w; stalls = 0; continue; }
if (w < 0) {
if (errno == EINTR || errno == EAGAIN) continue;
if (errno == EPIPE) { /* xrun:重备 + 恢复(设计 §2) */
@@ -765,18 +893,21 @@ static void reconfigure_locked(void)
/* 播放线程主体。
* 语义:只有当前授权流(g_st.active)喂 DAC;暂停时不读 ring(保留已缓冲);
* 停止/EOF 时不丢尾——先排空 ring 再关设备(否则 tone 末尾/缓冲尾被截断)。 */
* 停止/EOF 时不丢尾——先排空 ring 再关设备(否则 tone 末尾/缓冲尾被截断)。
* 所有等待为 COND_WAIT_MS 周期 timedwait:丢失唤醒最多迟 1s 自愈,SIGTERM 也能及时退。 */
static void *playback_thread(void *arg)
{
(void)arg;
static uint8_t buf[READ_CHUNK];
for (;;) {
g_hb_play++;
int have = 0;
pthread_mutex_lock(&g_mu);
for (;;) {
g_hb_play++;
if (g_stop) { have = -1; break; }
int fs = frame_size();
if (fs <= 0) { pthread_cond_wait(&g_cond, &g_mu); continue; } /* 无流 param → 等 */
if (fs <= 0) { cond_wait_tmo(COND_WAIT_MS); continue; } /* 无流 param → 等 */
if (g_reconfig_pending) { have = 1; break; } /* 重配 */
int draining = !g_st.active;
int paused = g_st.active && g_paused;
@@ -784,11 +915,11 @@ static void *playback_thread(void *arg)
if ((draining && av < (size_t)fs) || paused) {
/* 排空完毕(或暂停):若设备仍开且已排空 → 关;否则等待 */
if (draining && g_dev_open) dev_close();
pthread_cond_wait(&g_cond, &g_mu);
cond_wait_tmo(COND_WAIT_MS);
continue;
}
if (draining || av >= (size_t)fs) { have = 2; break; } /* 有数据可读(或排空态) */
pthread_cond_wait(&g_cond, &g_mu);
cond_wait_tmo(COND_WAIT_MS);
}
if (have == -1) { pthread_mutex_unlock(&g_mu); break; }
if (have == 1) { reconfigure_locked(); pthread_mutex_unlock(&g_mu); continue; }
@@ -902,6 +1033,9 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
g_paused = 0;
g_written_frames = g_played_frames = 0;
g_underrun = 0;
g_drop_frames = 0; /* 每流计数(txdrop 为全局客户端级,不随流清) */
g_stall = 0;
g_dev_bytes = 0; /* 测试钩子计数随新流归零 */
/* 重建 ring + 标记重配 */
ring_rebuild();
_audio_mark_reconfig_needed();
@@ -919,12 +1053,10 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
case MSG_AUDIO_WRITE: {
/* 仅当前授权流、非 draining、非 paused 才入 ring;否则丢弃 */
if (g_st.active && c->sid == g_st.sid && c->state != CLS_DRAINING && !g_paused && c->state != CLS_PAUSED) {
if (g_ring) {
ring_write_locked(pl, len);
int fs = frame_size();
if (fs > 0)
g_written_frames += len / (uint32_t)fs;
}
size_t wn = ring_write_locked(pl, len, g_st.sid);
int fs = frame_size();
if (wn && fs > 0)
g_written_frames += wn / (uint32_t)fs;
}
break;
}
@@ -936,6 +1068,10 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
if (sid == g_st.sid && g_st.active) {
if (type == MSG_AUDIO_PAUSE) { g_paused = 1; c->state = CLS_PAUSED; logmsg("pause sid=%u", (unsigned)sid); }
else if (type == MSG_AUDIO_RESUME){ g_paused = 0; c->state = CLS_OPEN; logmsg("resume sid=%u", (unsigned)sid); }
/* PAUSE/RESUME 必须唤醒播放线程(它可能正 timedwait 在 paused 分支;
* 旧代码 RESUME 不 signal,恢复后要等下一块 WRITE 才醒) */
if (type != MSG_AUDIO_STOP)
pthread_cond_signal(&g_cond);
else /* STOP */ {
logmsg("STOP sid=%u (app=%s)", (unsigned)sid, g_st.app_id);
g_st.active = 0;
@@ -954,11 +1090,13 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
}
case MSG_AUDIO_QUERY: {
char txt[1024];
int rb = amixer_get(9, 1);
/* 路由口用缓存值 g_route_cur(路由线程维护),不在 IO 热路径 popen amixer——
* 驱动 wedge 时 amixer 虽有界但仍会拖慢 IO 循环 1.5s/次。 */
int rb = g_route_cur;
int vol = g_vol_pct;
snprintf(txt, sizeof txt,
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s "
"written=%llu played=%llu underrun=%u paused=%d "
"written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d "
"route_mode=%s port=%d vol=%d\n",
g_st.active ? 1 : 0,
g_st.active ? g_st.app_id : "-",
@@ -966,7 +1104,9 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
g_st.active ? fmt_name((int)g_st.fmt) : "-",
(unsigned long long)g_written_frames,
(unsigned long long)g_played_frames,
(unsigned)g_underrun, g_paused,
(unsigned)g_underrun,
(unsigned long long)g_drop_frames,
(unsigned)g_stall, (unsigned)g_txdrop, g_paused,
g_route_manual == -1 ? "auto" : "manual",
(g_route_manual != -1 ? g_route_manual : rb),
vol);
@@ -1066,10 +1206,11 @@ static void *io_thread(void *arg)
return NULL;
}
chmod(g_sock, 0666);
if (listen(lfd, 4) != 0) { logmsg("io: listen: %s", strerror(errno)); close(lfd); return NULL; }
if (listen(lfd, 8) != 0) { logmsg("io: listen: %s", strerror(errno)); close(lfd); return NULL; }
logmsg("io thread listening on %s", g_sock);
for (;;) {
g_hb_io++;
if (g_stop) break;
struct pollfd pfds[CLI_MAX + 1];
int n = 1;
@@ -1080,7 +1221,16 @@ static void *io_thread(void *arg)
n++;
}
int pr = poll(pfds, (nfds_t)n, 200);
if (pr < 0) { if (errno == EINTR) continue; break; }
if (pr < 0) {
if (errno == EINTR) continue;
/* poll 出错绝不退出线程(listener 一丢就是全守护失联,CLI ECONNREFUSED);
* 记日志(限频)+ 短睡后继续。 */
static unsigned s_pollerr = 0;
if ((s_pollerr++ & 15u) == 0) logmsg("io: poll err: %s (x%u, keep going)", strerror(errno), s_pollerr);
struct timespec ts = { 0, 100 * 1000000L };
nanosleep(&ts, NULL);
continue;
}
/* pfds 是本轮新数组(revents 本轮清零),直接以 revents 为准(238 教训:每圈重建,
* 杜绝陈旧 revents。勿在此清零)。 */
struct pollfd snaps[CLI_MAX + 1];
@@ -1140,8 +1290,9 @@ static void *route_thread(void *arg)
}
while (!g_stop) {
pthread_mutex_lock(&g_mu);
route_evaluate(&last_hb, &last_bal, &last_target);
int manual = g_route_manual; /* 快照后放锁:amixer popen 不持 g_mu(防 wedge 级联) */
pthread_mutex_unlock(&g_mu);
route_evaluate(manual, &last_hb, &last_bal, &last_target);
int naps = 5; /* 1000ms / 200ms 分段,即时响应 SIGTERM */
for (int k = 0; k < naps && !g_stop; k++) {
struct timespec ts = { 0, 200 * 1000000L };
@@ -1154,8 +1305,10 @@ static void *route_thread(void *arg)
static void on_sig(int sig)
{
(void)sig;
/* 只置标志(async-signal-safe):所有等待均为有界 timedwait(播放 1s / IO poll 200ms /
* 路由 200ms 分段 / ring 写 ≤g_ring_wait_ms),线程自行看到 g_stop 退出——
* 不在信号处理器里调 pthread_cond_signal(非 async-signal-safe)。 */
g_stop = 1;
pthread_cond_signal(&g_cond);
}
int main(int argc, char **argv)
@@ -1166,7 +1319,10 @@ int main(int argc, char **argv)
if (!strcmp(argv[i], "--sock") && i + 1 < argc) snprintf(g_sock, sizeof g_sock, "%s", argv[++i]);
else if (!strcmp(argv[i], "--out") && i + 1 < argc) snprintf(g_out_wav, sizeof g_out_wav, "%s", argv[++i]);
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
printf("meow_audiod [--sock <path>] [--out <wav>]\n env: MEOW_AUDIO_SOCK/MEOW_AUDIO_OUT/MEOW_AUDIO_SINK(alsa|file|null)/MEOW_AUDIO_AMIXER/MEOW_AUDIO_FORCE=1\n");
printf("meow_audiod [--sock <path>] [--out <wav>]\n"
" env: MEOW_AUDIO_SOCK/MEOW_AUDIO_OUT/MEOW_AUDIO_SINK(alsa|file|null)/MEOW_AUDIO_AMIXER/MEOW_AUDIO_FORCE=1\n"
" MEOW_AUDIO_RING_WAIT_MS (ring 满写等待上限, 缺省 5000)\n"
" MEOW_AUDIO_TEST_WRITE_DELAY_MS / MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES (故障注入, 仅测试)\n");
return 0;
}
}
@@ -1174,6 +1330,9 @@ int main(int argc, char **argv)
if ((e = getenv("MEOW_AUDIO_OUT")) && e[0]) snprintf(g_out_wav, sizeof g_out_wav, "%s", e);
if ((e = getenv("MEOW_AUDIO_AMIXER")) && e[0]) g_amixer = e;
if ((e = getenv("MEOW_AUDIO_FORCE")) && e[0] == '1') g_force = 1;
if ((e = getenv("MEOW_AUDIO_RING_WAIT_MS")) && atoi(e) > 0) g_ring_wait_ms = atoi(e);
if ((e = getenv("MEOW_AUDIO_TEST_WRITE_DELAY_MS")) && atoi(e) > 0) g_test_write_delay_ms = atoi(e);
if ((e = getenv("MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES")) && atoll(e) > 0) g_test_wedge_after = atoll(e);
const char *sink = getenv("MEOW_AUDIO_SINK");
if (sink && sink[0]) {
if (!strcmp(sink, "file")) g_sink = SINK_FILE;
@@ -1196,19 +1355,49 @@ int main(int argc, char **argv)
}
log_open();
logmsg("meow_audiod start: sock=%s sink=%d wav=%s force=%d", g_sock, g_sink, g_out_wav, g_force);
logmsg("meow_audiod start: sock=%s sink=%d wav=%s force=%d ring_wait=%dms%s%s",
g_sock, g_sink, g_out_wav, g_force, g_ring_wait_ms,
g_test_write_delay_ms > 0 ? " TEST-DELAY" : "",
g_test_wedge_after >= 0 ? " TEST-WEDGE" : "");
if (g_sink == SINK_ALSA) hw_probe();
g_vol_pct = settings_get_volume();
logmsg("volume init = %d%%", g_vol_pct);
/* g_cond 用 CLOCK_MONOTONIC(所有 timedwait 的基准;防系统时间跳变影响护栏超时) */
{
pthread_condattr_t ca;
pthread_condattr_init(&ca);
pthread_condattr_setclock(&ca, CLOCK_MONOTONIC);
pthread_cond_init(&g_cond, &ca);
pthread_condattr_destroy(&ca);
}
pthread_t ti, tp, tr;
pthread_create(&ti, NULL, io_thread, NULL);
pthread_create(&tp, NULL, playback_thread, NULL);
pthread_create(&tr, NULL, route_thread, NULL);
/* 主线程 = watchdog:心跳静默 >WD_SILENT_SEC 秒即告警(只告警不强杀——
* 对卡在内核的线程任何外部干预都不安全;护栏保证其它线程/连接继续服务)。 */
unsigned last_io = 0, last_play = 0;
int io_silent = 0, play_silent = 0;
while (!g_stop) {
struct timespec ts = { 1, 0 };
nanosleep(&ts, NULL);
if (g_stop) break;
unsigned io = g_hb_io, pl = g_hb_play;
io_silent = (io == last_io) ? io_silent + 1 : 0; last_io = io;
play_silent = (pl == last_play) ? play_silent + 1 : 0; last_play = pl;
if (io_silent == WD_SILENT_SEC || (io_silent > WD_SILENT_SEC && io_silent % 30 == 0))
logmsg("WATCHDOG: io thread silent %ds (accept/read loop stuck?)", io_silent);
if (play_silent == WD_SILENT_SEC || (play_silent > WD_SILENT_SEC && play_silent % 30 == 0)) {
pthread_mutex_lock(&g_mu);
int act = g_st.active;
uint64_t wr = g_written_frames, plf = g_played_frames;
pthread_mutex_unlock(&g_mu);
logmsg("WATCHDOG: playback thread silent %ds (active=%d written=%llu played=%llu, device wedged?)",
play_silent, act, (unsigned long long)wr, (unsigned long long)plf);
}
}
logmsg("meow_audiod shutdown");
pthread_join(ti, NULL);
Binary file not shown.
Binary file not shown.
+178 -4
View File
@@ -18,10 +18,12 @@
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <math.h>
#include <poll.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/wait.h>
@@ -38,17 +40,36 @@ static void check(int cond, const char *name)
else { printf(" FAIL %s\n", name); g_fail++; }
}
/* ---- 原始 IPC 客户端 ---- */
/* ---- 原始 IPC 客户端(全有界:connect 非阻塞 2s + SO_SNDTIMEO 2s——回归卡死时
* 测试 FAIL 而不是跟着挂死) ---- */
struct cli { int fd; };
static int cli_connect(struct cli *c)
{
c->fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (c->fd < 0) return -1;
int fl = fcntl(c->fd, F_GETFL, 0);
if (fl >= 0) fcntl(c->fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
snprintf(a.sun_path, sizeof a.sun_path, "%s", TSOCK);
if (connect(c->fd, (struct sockaddr *)&a, sizeof a) < 0) { close(c->fd); return -1; }
int r = connect(c->fd, (struct sockaddr *)&a, sizeof a);
if (r < 0 && errno == EINPROGRESS) {
struct pollfd p;
p.fd = c->fd; p.events = POLLOUT; p.revents = 0;
if (poll(&p, 1, 2000) > 0) {
int err = 0;
socklen_t el = sizeof err;
if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &err, &el) == 0 && err != 0) { errno = err; r = -1; }
} else {
r = -1;
}
}
if (r < 0) { close(c->fd); return -1; }
if (fl >= 0) fcntl(c->fd, F_SETFL, fl);
struct timeval tv;
tv.tv_sec = 2; tv.tv_usec = 0;
setsockopt(c->fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
return 0;
}
static int w_all(int fd, const void *p, size_t n)
@@ -96,20 +117,42 @@ static void stop_audiod(void)
if (g_audiod_pid > 0) { kill(g_audiod_pid, SIGTERM); waitpid(g_audiod_pid, NULL, 0); g_audiod_pid = -1; }
}
static int start_audiod(void)
/* 带超时的停止:SIGTERM 后最多等 tmo_ms;不退 → SIGKILL 并返回 0(卡死回归显形)。 */
static int stop_audiod_tmo(int tmo_ms)
{
if (g_audiod_pid <= 0) return 1;
kill(g_audiod_pid, SIGTERM);
int ok = 0;
for (int i = 0; i < tmo_ms / 50; i++) {
if (waitpid(g_audiod_pid, NULL, WNOHANG) == g_audiod_pid) { ok = 1; break; }
usleep(50000);
}
if (!ok) { kill(g_audiod_pid, SIGKILL); waitpid(g_audiod_pid, NULL, 0); }
g_audiod_pid = -1;
return ok;
}
static int start_audiod_ex(const char *sink, const char *wedge_after, const char *ring_wait_ms)
{
pid_t pid = fork();
if (pid == 0) {
setenv("MEOW_AUDIO_SINK", "file", 1);
setenv("MEOW_AUDIO_SINK", sink, 1);
setenv("MEOW_AUDIO_OUT", TWAV, 1);
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
setenv("MEOW_AUDIO_FORCE", "1", 1);
if (wedge_after) setenv("MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES", wedge_after, 1);
if (ring_wait_ms) setenv("MEOW_AUDIO_RING_WAIT_MS", ring_wait_ms, 1);
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
_exit(127);
}
return pid;
}
static int start_audiod(void)
{
return start_audiod_ex("file", NULL, NULL);
}
/* 直接客户端 open */
static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack)
{
@@ -289,6 +332,65 @@ int main(void)
check(strstr(q, "active=0") != NULL, "EOF recycle -> inactive");
close(c3q.fd);
/* ---- T5:长流回归(3 分钟等效 PCM 持续灌;FILE sink 全速排空 → 加速时钟等效)。
* 回归目标:真机「长流卡死」——喂流期间 listener/IO 循环必须持续可服务,
* 帧数零丢失、零 underrun、零丢弃,STOP 后排空落盘完整。 ---- */
printf("[T5 long stream (3min equivalent)]\n");
struct cli l5;
check(cli_connect(&l5) == 0, "connect L5");
m_audio_open_ack_t ackL5;
check(do_open(&l5, 44100, 2, AUDIO_FMT_S16, &ackL5) == 0 && ackL5.err == MEOW_OK, "open L5");
uint32_t fs5 = (uint32_t)ackL5.grant_ch * (ackL5.grant_fmt == AUDIO_FMT_S16 ? 2u : 4u);
if (fs5 == 0) fs5 = 4;
uint64_t total_frames = 44100ULL * 180; /* 3 分钟等效流 */
uint64_t total_bytes = total_frames * fs5;
uint8_t chunk[4096];
memset(chunk, 0x5A, sizeof chunk); /* 内容无关:测完整性/不卡死 */
size_t per = (sizeof chunk / fs5) * fs5;
uint64_t sent = 0;
int werr = 0, probe_n = 0, probe_ok = 0;
while (sent < total_bytes) {
uint64_t w = total_bytes - sent;
if (w > per) w = per;
if (do_write(&l5, chunk, (uint32_t)w) != 0) { werr = 1; break; }
sent += w;
if (sent % (512 * per) == 0) { /* 每 2MB 探一次 listener 活性 */
struct cli s5;
s5.fd = -1;
probe_n++;
if (cli_connect(&s5) == 0) {
char q5[1024];
if (do_query(&s5, q5, sizeof q5) == 0 && strstr(q5, "active=1")) probe_ok++;
close(s5.fd);
}
}
}
check(!werr && sent == total_bytes, "long feed completed (io never stalled)");
printf(" fed %llu bytes (%llu frames), mid-feed status probes %d/%d alive\n",
(unsigned long long)sent, (unsigned long long)(sent / fs5), probe_ok, probe_n);
check(probe_n >= 10 && probe_ok == probe_n, "listener/io alive during long feed");
do_ctrl(&l5, MSG_AUDIO_STOP, ackL5.stream_id);
char expect[64];
snprintf(expect, sizeof expect, "played=%llu", (unsigned long long)total_frames);
int drained = 0;
char q5[1024];
for (int i = 0; i < 300; i++) { /* 最多 30s 等排空 */
if (do_query(&l5, q5, sizeof q5) == 0 && strstr(q5, expect)) { drained = 1; break; }
usleep(100000);
}
check(drained, "long stream fully played (played == fed frames)");
if (drained)
check(strstr(q5, "underrun=0") != NULL && strstr(q5, "dropped=0") != NULL,
"no underrun/drop on long stream");
usleep(300000); /* 等 dev_close 回填 WAV 头 */
close(l5.fd);
{
FILE *f = fopen(TWAV, "rb");
long sz = -1;
if (f) { fseek(f, 0, SEEK_END); sz = ftell(f); fclose(f); }
check(sz == (long)(44 + total_bytes), "long WAV size == 44 + fed bytes");
}
/* ---- T4:tone 写 WAV(由 CLI 走完整 IPC 链路;mipsel/host CLI 共用) ---- */
printf("[T4 tone -> WAV]\n");
unlink(TWAV);
@@ -305,6 +407,78 @@ int main(void)
{ FILE *f = fopen(TWAV, "rb"); if (f) { fseek(f, 0, SEEK_END); printf("%ld bytes\n", ftell(f)); fclose(f); } }
stop_audiod();
/* ---- T6:设备 wedge 生存(故障注入复现真机长流卡死场景)。
* MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES=128KB → 播放线程永久堵在 dev_write(睡到 g_stop)。
* 旧代码:ring 满 → IO 线程 ring_write cond_wait 永不醒 → accept 停摆 → 全链冻结。
* 护栏后:ring 写有界(RING_WAIT_MS=50ms)超时丢弃,IO/accept/preempt 全部存活。 ---- */
printf("[T6 device wedge survival]\n");
unlink(TSOCK);
g_audiod_pid = start_audiod_ex("null", "131072", "50");
usleep(400000);
struct cli w6;
check(cli_connect(&w6) == 0, "connect W6");
m_audio_open_ack_t ackW6;
check(do_open(&w6, 44100, 2, AUDIO_FMT_S16, &ackW6) == 0 && ackW6.err == MEOW_OK, "open W6");
int werr6 = 0;
uint64_t sent6 = 0;
for (int i = 0; i < 100; i++) { /* 400KB:128KB 后 wedge,其余全靠丢弃 */
if (do_write(&w6, chunk, (uint32_t)per) != 0) { werr6 = 1; break; }
sent6 += per;
}
check(!werr6 && sent6 == 100 * (uint64_t)per, "feed continues across wedge (no deadlock)");
struct cli s6;
s6.fd = -1;
char q6[1024];
int io_alive = 0;
if (cli_connect(&s6) == 0) {
if (do_query(&s6, q6, sizeof q6) == 0 && strstr(q6, "route_mode=")) io_alive = 1;
close(s6.fd);
}
check(io_alive, "io loop alive after wedge (connect+query served)");
check(io_alive && strstr(q6, "dropped=") != NULL && atoi(strstr(q6, "dropped=") + 8) > 0,
"drop policy engaged (dropped>0)");
struct cli p6;
p6.fd = -1;
int pre6 = 0;
if (cli_connect(&p6) == 0) {
m_audio_open_ack_t ak6;
if (do_open(&p6, 48000, 2, AUDIO_FMT_S16, &ak6) == 0 && ak6.err == MEOW_OK) pre6 = 1;
close(p6.fd);
}
check(pre6, "preempt OPEN granted while wedged");
close(w6.fd);
check(stop_audiod_tmo(5000) != 0, "wedged audiod exits on SIGTERM within 5s");
/* ---- T7:慢/不读客户端下行丢弃(server→client 写绝不堵 IO 循环)。
* Z 狂发 QUERY 从不读响应 → server 下行缓冲灌满 → send_frame 单发即丢(txdrop++)。
* 护栏断言:server 不卡死、新连接照常服务、txdrop 有计数。 ---- */
printf("[T7 slow-client tx drop]\n");
unlink(TSOCK);
g_audiod_pid = start_audiod_ex("null", NULL, NULL);
usleep(400000);
struct cli z7;
check(cli_connect(&z7) == 0, "connect Z7");
int serr7 = 0, sent7 = 0;
for (int i = 0; i < 2000; i++) {
if (sf(z7.fd, MSG_AUDIO_QUERY, 9, NULL, 0) != 0) { serr7 = 1; break; }
sent7++;
}
check(!serr7 && sent7 == 2000, "query spam sent (client never reads)");
struct cli f7;
f7.fd = -1;
char q7[1024];
int alive7 = 0;
if (cli_connect(&f7) == 0) {
if (do_query(&f7, q7, sizeof q7) == 0 && strstr(q7, "route_mode=")) alive7 = 1;
close(f7.fd);
}
check(alive7, "server responsive after tx drops");
check(alive7 && strstr(q7, "txdrop=") != NULL && atoi(strstr(q7, "txdrop=") + 7) > 0,
"txdrop counted");
close(z7.fd);
stop_audiod();
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
return g_fail ? 1 : 0;
}