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:
1 parent
243f328499
commit
e1e1331e44
10 files changed
+477
-67
No files matched your search
Binary file not shown.
@@ -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.
@@ -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.
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user