/* test_bgplay.c — 宿主单测:meow_bgplay 后台播放保活 + 无头后台服务状态机(M4)。 * * 验证: * 1) meow_bg_should_keep 判定矩阵(background_audio × media_playing); * 2) 后台播放保活全链路(真 fork 子进程):META PLAYING → should_keep=1 → bg_add → * 进程存活 + PLAYCTL 经会话 fd 往返(后台化不断连)→ take 恢复 → META STOPPED → * should_keep=0(停播后再退出=真退); * 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽; * 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS); * 5) bg 表满(MEOW_BG_MAX)→ MEOW_ERR_NOMEM;重复 add → MEOW_ERR_EXISTS; * 6) 服务注册:exec 不可执行 → NOENT;同名在跑 → EXISTS; * 7) 退避重启 1s/2s + max_restart=2 熔断(假时钟注入):清通知回调 + 注册连接收到 * MSG_SYS{SVC_DEAD=2} + circuit 后不再拉起;重注册复位熔断重新拉起; * 8) MSG_SERVICE_STOP:SIGTERM + stopping,不触发重启,槽释放; * 9) kill_all:bg+svc 全回收 + 注册连接收到 MSG_SYS{SHUTDOWN=1}。 * 只依赖 meow_bgplay.c + meow_media.c + meow_ipc.h(纯 C),宿主 gcc 直编: * gcc -Wall -Wextra -std=gnu99 test_bgplay.c meow_bgplay.c meow_media.c -o test_bgplay */ #include #include #include #include #include #include #include #include #include #include #include #include "meow_bgplay.h" #include "meow_media.h" #include "meow_ipc.h" static int g_fails = 0; #define CHECK(cond, msg) do { \ if (cond) printf("PASS: %s\n", msg); \ else { printf("FAIL: %s\n", msg); g_fails++; } } while (0) /* ---- 假件(ops 注入)---- */ static uint64_t g_ms = 100000; /* 假时钟(退避断言用,手动推进) */ static int g_card = 1; /* 卡在位 */ static int g_bg_dead = 0; /* on_bg_dead 计数 */ static int g_bg_dead_status = 0; static char g_bg_dead_app[MEOW_APP_ID_MAX]; static int g_notif_cleared = 0; /* notif_clear_app 计数 */ static char g_notif_cleared_app[MEOW_APP_ID_MAX]; static uint64_t fake_now_ms(void) { return g_ms; } static int fake_card(void) { return g_card; } static void fake_bg_dead(const char *app_id, pid_t pid, int status) { (void)pid; g_bg_dead++; g_bg_dead_status = status; snprintf(g_bg_dead_app, sizeof g_bg_dead_app, "%s", app_id); } static int fake_notif_clear(const char *app_id) { g_notif_cleared++; snprintf(g_notif_cleared_app, sizeof g_notif_cleared_app, "%s", app_id); return 1; } /* send_frame:整帧写到 fd(socketpair 一端),对端读回校验。 */ static size_t fake_send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len) { uint8_t hdr[MEOW_IPC_HDR_LEN]; uint32_t magic = MEOW_IPC_MAGIC, rid = req_id, l = len; uint16_t ver = MEOW_IPC_VERSION, t = type; size_t off = 0; memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2); memcpy(hdr + 8, &rid, 4); memcpy(hdr + 12, &l, 4); if (write(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return 0; off = sizeof hdr; if (len && payload && write(fd, payload, len) > 0) off += len; return off; } /* media ops.send 同 fake_send_frame(PLAYCTL 下发校验用) */ /* 从 socketpair 读一帧(poll 500ms 防挂)。返回负载字节数,<0=超时/失败。 */ static int read_frame(int fd, uint16_t *type, void *pay, int paycap) { struct pollfd pf = { .fd = fd, .events = POLLIN, .revents = 0 }; uint8_t hdr[MEOW_IPC_HDR_LEN]; uint32_t len; if (poll(&pf, 1, 500) <= 0) return -1; if (read(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return -1; memcpy(type, hdr + 6, 2); memcpy(&len, hdr + 12, 4); if (len > (uint32_t)paycap) return -1; if (len && read(fd, pay, len) != (ssize_t)len) return -1; return (int)len; } /* fork 一个长眠子进程(300s),测试末统一 SIGKILL 清理。 */ static pid_t spawn_sleeper(void) { pid_t pid = fork(); if (pid == 0) { sleep(300); _exit(0); } return pid; } /* 泵 tick 直到 cond 或超时(~2s):子进程死亡到 WNOHANG 可见有调度延迟。 */ static int pump_bg_until(int (*cond)(void *), void *ctx) { for (int i = 0; i < 100; i++) { meow_bg_tick(); if (cond(ctx)) return 1; usleep(20000); } return 0; } static int pump_svc_until(int (*cond)(void *), void *ctx) { for (int i = 0; i < 100; i++) { meow_svc_tick(); if (cond(ctx)) return 1; usleep(20000); } return 0; } static int cond_bg_dead(void *ctx) { (void)ctx; return g_bg_dead > 0; } static int cond_svc_reaped(void *ctx) { const meow_svc_rt_t *s; (void)ctx; s = meow_svc_find("dev.test.svc", "daemon"); return s && s->pid == 0; } static int cond_svc_gone(void *ctx) { (void)ctx; return meow_svc_find("dev.test.svc", "daemon") == NULL; } /* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。 */ #define TEST_SVC_PATH "/tmp/meow_bgplay_test_svc.sh" static void write_test_svc(void) { FILE *f = fopen(TEST_SVC_PATH, "w"); fputs("#!/bin/sh\nexec sleep 300\n", f); fclose(f); chmod(TEST_SVC_PATH, 0755); } int main(void) { meow_bgplay_ops ops; meow_media_ops mops; memset(&ops, 0, sizeof ops); ops.now_ms = fake_now_ms; ops.card_present = fake_card; ops.on_bg_dead = fake_bg_dead; ops.notif_clear_app = fake_notif_clear; ops.send_frame = fake_send_frame; ops.log_dir = NULL; meow_bgplay_init(&ops); memset(&mops, 0, sizeof mops); mops.send = fake_send_frame; meow_media_init(&mops); /* ---- 1) should_keep 判定矩阵 ---- */ CHECK(meow_bg_should_keep(0, 0) == 0, "should_keep(未声明,未播放)=0"); CHECK(meow_bg_should_keep(1, 0) == 0, "should_keep(声明,未播放)=0"); CHECK(meow_bg_should_keep(0, 1) == 0, "should_keep(未声明,播放)=0"); CHECK(meow_bg_should_keep(1, 1) == 1, "should_keep(声明,播放)=1"); /* ---- 2) 后台播放保活全链路 ---- */ int sp[2]; CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0, "socketpair 建立"); { m_meta_t meta; memset(&meta, 0, sizeof meta); snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1"); meta.state = MEOW_MEDIA_PLAYING; meta.dur_ms = 200000; CHECK(meow_media_meta(&meta, sp[0]) == 0, "META 上报 PLAYING 注册媒体会话"); } CHECK(meow_media_app_playing("dev.test.bg1") == 1, "app_playing(bg1)=1"); CHECK(meow_bg_should_keep(1, meow_media_app_playing("dev.test.bg1")) == 1, "播放中退出 → 应保活"); pid_t kid = spawn_sleeper(); CHECK(kid > 0, "fork 假 App 子进程"); CHECK(meow_bg_add("dev.test.bg1", kid, sp[0], 0) == 0, "bg_add 转后台保活"); CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 1, "is_backgrounded=1"); CHECK(meow_bg_count() == 1, "bg count=1"); CHECK(kill(kid, 0) == 0, "后台化后进程仍存活(未 SIGTERM/SIGSTOP)"); /* PLAYCTL 经会话 fd 往返(后台化不断连:进程/fd 均保留) */ CHECK(meow_media_playctl_to("dev.test.bg1", 6, 0) == 0, "PLAYCTL 下发到后台 App 会话"); { uint16_t t = 0; m_playctl_t pc; int n = read_frame(sp[1], &t, &pc, sizeof pc); CHECK(n == (int)sizeof pc && t == MSG_PLAYCTL && pc.cmd == 6, "对端收到 MSG_PLAYCTL cmd=6(控制通道后台不断连)"); } { int rfd = -1; pid_t rpid = 0; CHECK(meow_bg_take("dev.test.bg1", &rfd, &rpid) == 0 && rpid == kid && rfd == sp[0], "bg_take 回前台(pid/fd 原样归还)"); } CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 0 && meow_bg_count() == 0, "take 后清槽"); meow_media_clear("dev.test.bg1"); { m_meta_t meta; memset(&meta, 0, sizeof meta); snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1"); meta.state = MEOW_MEDIA_STOPPED; meow_media_meta(&meta, sp[0]); } CHECK(meow_bg_should_keep(1, meow_media_app_playing("dev.test.bg1")) == 0, "停播后再退出 → should_keep=0(真退 SIGTERM)"); kill(kid, SIGKILL); waitpid(kid, NULL, 0); close(sp[0]); close(sp[1]); meow_media_clear("dev.test.bg1"); /* ---- 3) 后台 App 崩溃照收 ---- */ g_bg_dead = 0; kid = spawn_sleeper(); CHECK(meow_bg_add("dev.test.crash", kid, -1, 0) == 0, "bg_add(crash)"); kill(kid, SIGKILL); { int ok = pump_bg_until(cond_bg_dead, NULL); (void)ok; } CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.crash") == 0 && WIFSIGNALED(g_bg_dead_status) && WTERMSIG(g_bg_dead_status) == SIGKILL, "崩溃子进程被 tick 回收 + on_bg_dead(SIGKILL) 回调"); CHECK(meow_bg_count() == 0, "崩溃回收后清槽"); /* ---- 4) 卡拔例外:on_card=1 + 卡不在 → SIGTERM 回收 ---- */ g_bg_dead = 0; kid = spawn_sleeper(); CHECK(meow_bg_add("dev.test.card", kid, -1, 1) == 0, "bg_add(on_card=1)"); g_card = 0; pump_bg_until(cond_bg_dead, NULL); CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.card") == 0, "卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)"); g_card = 1; /* ---- 5) 表满 / 重复 ---- */ { pid_t k[MEOW_BG_MAX]; char id[MEOW_APP_ID_MAX]; int i, full = 0; for (i = 0; i < MEOW_BG_MAX; i++) { k[i] = spawn_sleeper(); snprintf(id, sizeof id, "dev.test.full%d", i); meow_bg_add(id, k[i], -1, 0); } snprintf(id, sizeof id, "dev.test.full%d", MEOW_BG_MAX); { pid_t extra = spawn_sleeper(); full = (meow_bg_add(id, extra, -1, 0) == MEOW_ERR_NOMEM); kill(extra, SIGKILL); waitpid(extra, NULL, 0); /* 未入表,就地清理 */ } CHECK(full, "bg 表满 → MEOW_ERR_NOMEM"); snprintf(id, sizeof id, "dev.test.full%d", 0); CHECK(meow_bg_add(id, 0, -1, 0) == MEOW_ERR_INVAL, "pid<=0 → INVAL"); CHECK(meow_bg_add(id, k[0], -1, 0) == MEOW_ERR_EXISTS, "重复 app_id → EXISTS"); for (i = 0; i < MEOW_BG_MAX; i++) { snprintf(id, sizeof id, "dev.test.full%d", i); meow_bg_take(id, NULL, NULL); kill(k[i], SIGKILL); waitpid(k[i], NULL, 0); } } CHECK(meow_bg_count() == 0, "take 全部后 bg 表空"); /* ================= 无头后台服务 ================= */ write_test_svc(); /* ---- 6) 注册校验 ---- */ CHECK(meow_svc_register("dev.test.svc", "daemon", "/nonexistent/x", 1, 2, 60000, 0, -1) == MEOW_ERR_NOENT, "exec 不可执行 → NOENT(不留尸槽)"); CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "NOENT 后无槽残留"); int sp_svc[2]; CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp_svc) == 0, "svc socketpair 建立"); int pid1 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 2, 60000, 0, sp_svc[0]); CHECK(pid1 > 0, "register 拉起服务"); CHECK(meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 2, 60000, 0, sp_svc[0]) == MEOW_ERR_EXISTS, "同名服务在跑 → EXISTS"); /* ---- 7) 退避 1s/2s + max=2 熔断 ---- */ const meow_svc_rt_t *sl; kill(pid1, SIGKILL); pump_svc_until(cond_svc_reaped, NULL); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->pid == 0 && sl->restart_count == 1 && sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_1, "第 1 次死 → 1s 退避排程(restart_count=1)"); g_ms += MEOW_SVC_BACKOFF_1; meow_svc_tick(); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->pid > 0 && sl->pid != pid1, "到点重启:新 pid"); pid_t pid2 = sl->pid; kill(pid2, SIGKILL); pump_svc_until(cond_svc_reaped, NULL); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->pid == 0 && sl->restart_count == 2 && sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_2, "第 2 次死 → 2s 退避排程(restart_count=2)"); g_ms += MEOW_SVC_BACKOFF_2; meow_svc_tick(); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->pid > 0, "第 2 次重启拉起"); g_notif_cleared = 0; kill(sl->pid, SIGKILL); pump_svc_until(cond_svc_reaped, NULL); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->pid == 0 && sl->circuit == 1 && sl->restart_count == 3, "第 3 次死超 max_restart=2 → 熔断 circuit=1"); CHECK(g_notif_cleared == 1 && strcmp(g_notif_cleared_app, "dev.test.svc") == 0, "熔断清该 App 通知(notif_clear_app 回调)"); { uint16_t t = 0; m_sys_t m; int n = read_frame(sp_svc[1], &t, &m, sizeof m); CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SVC_DEAD && strcmp(m.svc, "daemon") == 0, "注册连接收到 MSG_SYS{SVC_DEAD=2} 熔断广播"); } g_ms += 60000; meow_svc_tick(); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->pid == 0, "熔断后不再拉起"); /* 重注册复位熔断 */ int pid4 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 2, 60000, 0, sp_svc[0]); CHECK(pid4 > 0, "熔断槽重注册 → 复位重新拉起"); sl = meow_svc_find("dev.test.svc", "daemon"); CHECK(sl && sl->circuit == 0 && sl->restart_count == 0, "熔断复位(circuit/count 清零)"); /* ---- 8) SERVICE_STOP:不重启 ---- */ CHECK(meow_svc_stop("dev.test.svc", "daemon") == 0, "svc_stop 受理"); pump_svc_until(cond_svc_gone, NULL); CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "stop 后槽释放(不重启)"); CHECK(meow_svc_stop("dev.test.svc", "daemon") == MEOW_ERR_NOENT, "重复 stop → NOENT"); close(sp_svc[0]); close(sp_svc[1]); /* ---- 9) kill_all:全回收 + SHUTDOWN 广播 ---- */ { int sp2[2]; socketpair(AF_UNIX, SOCK_STREAM, 0, sp2); int spid = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 3, 60000, 0, sp2[0]); pid_t bgk = spawn_sleeper(); meow_bg_add("dev.test.bg9", bgk, -1, 0); CHECK(spid > 0 && meow_bg_count() == 1, "kill_all 前置:1 svc + 1 bg"); meow_bgplay_kill_all(); CHECK(meow_bg_count() == 0 && meow_svc_find("dev.test.svc", "daemon") == NULL, "kill_all 后 bg/svc 全回收"); CHECK(kill(spid, 0) < 0 && kill(bgk, 0) < 0, "子进程确已终止"); uint16_t t = 0; m_sys_t m; int n = read_frame(sp2[1], &t, &m, sizeof m); CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SHUTDOWN, "注册连接收到 MSG_SYS{SHUTDOWN=1}(关机前回收广播)"); close(sp2[0]); close(sp2[1]); } unlink(TEST_SVC_PATH); printf(g_fails ? "== FAILURES: %d ==\n" : "== ALL PASS ==\n", g_fails); return g_fails ? 1 : 0; }