Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_bgplay.c
T
MeowOS 5aabede0cd feat(launcher): M4 后台服务保活+后台播放(FRAMEWORK_DESIGN §5/§9 落地)
- app.json 新字段 background/background_audio 解析入 struct app/meow_acct 注册表
- meow_ipc.h:MSG_REGISTER(m_register_t)/SERVICE_OK(m_ack_t.status=pid)/SERVICE_STOP/
  MSG_SYS(m_sys_t, SHUTDOWN=1/SVC_DEAD=2) 负载结构落地
- 新模块 meow_bgplay.c/h(自包含+ops 注入,仿 meow_media,宿主可单测):
  · 后台播放保活——声明 background_audio 且媒体会话 PLAYING 的 App nav-exit 不终止,
    进程+音频流保留;UI 冻结走 §10.5 /tmp/meow_notif_active SDK present 自抑(非字面
    SIGSTOP:冻结同进程播放线程断供 PCM,§10.5 真机实证);点图标/媒体卡片回前台=
    原进程恢复(chain/picker 忽略),停播后再退出=真退 SIGTERM
  · 无头后台服务——MSG_REGISTER spawn/stop/reap + 退避重启 1/2/4/10s cap +
    max_restart 熔断(清该 App 通知 + 注册连接广播 MSG_SYS{SVC_DEAD},重注册复位)
  · 卡拔例外(卡上二进制后台 App/服务仍 SIGTERM 防 SIGBUS)、崩溃照收、
    关机前 meow_bgplay_kill_all 有界回收(含 MSG_SYS{SHUTDOWN} 广播)
- util.c:MSG_REGISTER/MSG_SERVICE_STOP 分发(权限/声明校验链)+ ipc_conn_alive 连接
  存活查询 + meow_ipc_process 末尾挂 meow_bg_housekeep(drain/reap/退避 tick)
- app_launcher.c:launch_app_chain 入口 resume(bg_take 原进程回前台)+ nav-exit
  保活分支 + 转后台出口(杀看门狗/写自抑标记/复位顶条,跳过退出尾)+ 冷启动自动
  拉起静态 keepalive(mode=0) 服务(exec 路径约定=App 同目录 <svc> 文件)
- main_menu.c:应用列表「·后台」标注(宫格/列表,1s tick 按 change_seq 刷新)+
  两条退出尾 kill_all + unlink NOTIF_ACTIVE
- 宿主单测 test_bgplay.c 50 项全过(真 fork 子进程走状态机 + 假时钟退避熔断);
  test_media/test_notif_panel/test_notif_tf/test_vol_ramp_jack 回归全过;
  zig-mipsel-cc -Wall -Wextra 0 warning。launcher md5=c07d011c0769fca14d3bfb752d735463
2026-09-01 18:09:09 +08:00

377 lines
15 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_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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#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;
}