Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/test_bgplay.c
T
AskaEth 6b52740ff1 fix: 后台页杀按钮点不动 — app.json framework=1 被误当「不可杀」门控 + am kill 无前台会话不可执行
根因(2026-11-04 真机 dev.player.ncm pid=7520):
- bg_page.c bgm_app_meta 把 app.json 的 framework 字段映射进 bgmgr_row.framework,而
  bgmgr_can_kill 把它当「系统/框架 App 禁止 kill」。但 framework=1 在 MeowOS 里意为「用
  meow_ui.a 的 SDK 框架 App」(docs/APP_DEV_GUIDE.md §9,推荐值,真机 4 个 App 全写 1)
  → 后台页每行「杀」按钮被置灰,bgm_row_tap 命中后直接 return 1、不调 meow_bg_terminate;
  「一键清除后台」同理 0 行可杀。整条链路无日志,故只能靠翻 app.json 才定位(全部设备日志里
  "后台管理页终止后台App" 出现 0 次 = 4 次进页从未真正杀过)。
- 面板 killfg 正常是因为走 am kill → amsd → MSG_AMS_KILL,无 framework 门控;但它其实靠
  amsd 1200ms×2 超时兜底 kill(pid,SIGTERM) 才杀成:util.c 原本在无前台 App 会话时只回
  ACK -2((uint32_t)MEOW_ERR_NOENT),ACK 及时到达时 amsd 判「launcher refused」完全不杀
  (ams_20260915_031339.log 实证),ACK 迟到才侥幸成功(2026-11-04 真机)。

修复:
- meow_bgmgr: 门控位 framework → protected_app(系统级受保护标记;当前无 App 置位,launcher
  自身组件本就不在 App 表/后台表内)。bg_page 不再把 a->framework 送进门控。
- bg_page: 行副文本改存 ctx 持久缓冲(原 rbuf[].sub 指向 bgm_rows 栈帧 → 悬垂,副标题乱码);
  门控拒绝/未命中后台表各留一行 logmsg,「点了没反应」从此可查。
- util.c MSG_AMS_KILL: 先查后台保活表 → meow_bg_terminate 就地走真退序列(与页面「杀」按钮
  同一通道,故 am kill 与页面按钮行为对称),无前台会话也能杀;否则仍走 pending / ACK -2。
- amsd cmd_kill/cmd_start: ipc 失败原因细分(connect failed / timeout, no reply / ...),日志
  不再笼统写 "launcher offline"(响应串保留该字样,MeowPanel api_ams_ctl 依此映射 503)。
- meow_bgplay: meow_bg_terminate 注释/日志写明「后台管理页 / am kill」双入口同通道。

测试(宿主 gcc,产物在 $TMPDIR):
- test_bgpage 40 checks:新增 framework=1 仍判可杀且按钮真 terminate(本 bug 回归)、
  受保护行按钮不杀但留诊断日志、sub 持久化 + 栈抖动不变(悬垂回归)。
- test_bgmgr 30 checks:protected_app 门控矩阵 + framework=1 可杀回归。
- test_bgplay 92 PASS:新增 4b meow_bg_terminate 全链路(受理→term_at_ms→tick SIGTERM→
  on_bg_dead→清槽;空/不在表分别 INVAL/NOENT);TEST_SVC_PATH 改取 $TMPDIR
  (Termux 无 /tmp,原 fputs(NULL) 直接 abort)。
- amsd 宿主集成(fake launcher 桩,4 例):ACK 0→terminate 且不盲杀;ACK -2→refused 不盲杀;
  无 launcher→direct SIGTERM (connect failed);挂起不回→direct SIGTERM (timeout, no reply)。
0warning:util.c 与 main 同告警逐行一致(仅 SDK 头既有 typedef 重定义),其余文件干净。
2026-11-04 21:00:40 +08:00

525 lines
23 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);
* 4b) meow_bg_terminate(后台管理页「杀」按钮 / `am kill` 共用通道):受理置 term_at_ms →
* tick SIGTERM 回收 + on_bg_dead;不在表/空 id 分别 NOENT/INVAL;
* 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
* (不回收声明常驻);pid<=0 → MEOW_ERR_INVAL;重复 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)。
* 路径取 $TMPDIR(Termux/Android 主机没有 /tmp),缺省回退 /tmp。 */
static char g_svc_path[256];
#define TEST_SVC_PATH g_svc_path
static void write_test_svc(void)
{
const char *tmp = getenv("TMPDIR");
if (!tmp || !tmp[0]) tmp = "/tmp";
snprintf(g_svc_path, sizeof g_svc_path, "%s/meow_bgplay_test_svc.sh", tmp);
FILE *f = fopen(TEST_SVC_PATH, "w");
if (!f) { printf("FAIL: 无法创建测试脚本 %s\n", TEST_SVC_PATH); exit(1); }
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.media_resident = meow_media_app_resident; /* 批①+宽限:媒体活跃判定(宽限回收/槽满LRU) */
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 判定矩阵(批①三参:bg_audio, media_resident, keepalive) ---- */
CHECK(meow_bg_should_keep(0, 0, 0) == 0, "should_keep(未声明,未活跃,非常驻)=0");
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(声明bg_audio,无会话)=0");
CHECK(meow_bg_should_keep(0, 1, 0) == 0, "should_keep(未声明bg_audio,活跃)=0");
CHECK(meow_bg_should_keep(1, 1, 0) == 1, "should_keep(bg_audio+活跃)=1");
CHECK(meow_bg_should_keep(0, 0, 1) == 1, "should_keep(keepalive恒保)=1");
CHECK(meow_bg_should_keep(0, 1, 1) == 1, "should_keep(keepalive恒保,忽略media)=1");
CHECK(meow_bg_should_keep(1, 0, 1) == 1, "should_keep(keepalive恒保,无会话)=1");
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(bg_audio+resident=0 真退)=0");
/* ---- 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_media_app_resident("dev.test.bg1") == 1, "app_resident(bg1)=1");
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 1,
"播放中退出 → 应保活");
pid_t kid = spawn_sleeper();
CHECK(kid > 0, "fork 假 App 子进程");
CHECK(meow_bg_add("dev.test.bg1", kid, sp[0], 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");
{
const meow_bg_entry_t *be = meow_bg_at(0);
CHECK(be && be->bg_since_ms > 0, "bg 槽记 bg_since_ms(转后台时刻)");
CHECK(be && be->keepalive == 0, "bg_audio 类 keepalive=0");
}
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_resident("dev.test.bg1"), 0) == 0,
"停播后再退出 → should_keep=0(真退 SIGTERM)");
kill(kid, SIGKILL); waitpid(kid, NULL, 0);
close(sp[0]); close(sp[1]);
meow_media_clear("dev.test.bg1");
/* ---- 2b) keepalive 类(background_keepalive)入槽标记 ---- */
{
pid_t kk = spawn_sleeper();
CHECK(meow_bg_add("dev.test.keep", kk, -1, 0, 1) == 0, "bg_add(keepalive=1)");
const meow_bg_entry_t *bk = meow_bg_at(0);
CHECK(bk && bk->keepalive == 1, "keepalive 类槽 keepalive=1");
CHECK(bk && bk->bg_since_ms > 0, "keepalive 类槽 bg_since_ms>0");
meow_bg_take("dev.test.keep", NULL, NULL);
kill(kk, SIGKILL); waitpid(kk, NULL, 0);
CHECK(meow_bg_count() == 0, "keepalive 槽 take 后清槽");
}
/* ---- 3) 后台 App 崩溃照收 ---- */
g_bg_dead = 0;
kid = spawn_sleeper();
CHECK(meow_bg_add("dev.test.crash", kid, -1, 0, 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) == 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;
/* ---- 4b) meow_bg_terminate:后台管理页「杀」按钮 / `am kill` 共用的终止通道 ---- */
{
g_bg_dead = 0;
pid_t tk = spawn_sleeper();
CHECK(meow_bg_add("dev.test.term", tk, -1, 0, 0) == 0, "bg_add(term)");
CHECK(meow_bg_terminate("") == MEOW_ERR_INVAL, "terminate 空 app_id → INVAL");
CHECK(meow_bg_terminate("dev.test.nothere") == MEOW_ERR_NOENT, "terminate 不在后台表 → NOENT");
CHECK(meow_bg_terminate("dev.test.term") == 0, "terminate 受理(在后台表且 pid>0)");
{
int found = 0;
for (int i = 0; i < MEOW_BG_MAX; i++) {
const meow_bg_entry_t *be = meow_bg_at(i);
if (be && !strcmp(be->app_id, "dev.test.term")) {
CHECK(be->term_at_ms > 0, "terminate 置 term_at_ms(bgm_enumerate 据此隐藏行)");
found = 1;
}
}
CHECK(found == 1, "terminate 后槽仍在(等 tick 回收)");
}
pump_bg_until(cond_bg_dead, NULL);
CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.term") == 0,
"terminate → tick SIGTERM 回收 + on_bg_dead 上报 amsd");
CHECK(meow_bg_count() == 0, "terminate 回收后清槽");
}
/* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
{
pid_t k[MEOW_BG_MAX];
char id[MEOW_APP_ID_MAX];
int i;
pid_t gextra = 0;
for (i = 0; i < MEOW_BG_MAX; i++) {
k[i] = spawn_sleeper();
snprintf(id, sizeof id, "dev.test.lru%d", i);
CHECK(meow_bg_add(id, k[i], -1, 0, 0) == 0, "lru 填表(未声明 keepalive=0)");
}
/* 5.1 表满 + 全未声明 → 新增 LRU 回收最旧(lru0,bg_since_ms 最小),新 App 入表成功 */
{
pid_t e2 = spawn_sleeper();
gextra = e2;
CHECK(meow_bg_add("dev.test.lrunew", e2, -1, 0, 0) == 0,
"表满全未声明 → LRU 回收最旧槽并成功新增");
CHECK(meow_bg_count() == MEOW_BG_MAX, "回收后仍满 4 槽");
CHECK(meow_bg_is_backgrounded("dev.test.lrunew") == 1, "新增 App 在后台表");
CHECK(meow_bg_is_backgrounded("dev.test.lru0") == 0, "最久未激活未声明 lru0 被回收摘表");
CHECK(kill(k[0], 0) < 0, "lru0 进程已终止(SIGTERM 真退回收)");
}
/* 5.2 重复 add → EXISTS;pid<=0 → INVAL(lru0 已回收,表内为 lrunew/lru1/lru2/lru3) */
CHECK(meow_bg_add("dev.test.lrunew", 999999, -1, 0, 0) == MEOW_ERR_EXISTS, "重复 app_id → EXISTS");
CHECK(meow_bg_add("dev.test.lrunew", 0, -1, 0, 0) == MEOW_ERR_INVAL, "pid<=0 → INVAL");
/* 5.3 清空剩余(take + 杀进程防泄漏) */
for (i = 1; i < MEOW_BG_MAX; i++) {
snprintf(id, sizeof id, "dev.test.lru%d", i);
meow_bg_take(id, NULL, NULL);
kill(k[i], SIGKILL); waitpid(k[i], NULL, 0);
}
meow_bg_take("dev.test.lrunew", NULL, NULL);
kill(gextra, SIGKILL); waitpid(gextra, NULL, 0);
CHECK(meow_bg_count() == 0, "清空后 bg 表空(未声明)");
}
/* ---- 5b) 表满 + 全为声明常驻(keepalive=1)→ 拒绝新入 NOMEM(不回收声明常驻,保安全) ---- */
{
pid_t d[MEOW_BG_MAX];
char id[MEOW_APP_ID_MAX];
int i;
for (i = 0; i < MEOW_BG_MAX; i++) {
d[i] = spawn_sleeper();
snprintf(id, sizeof id, "dev.test.dec%d", i);
CHECK(meow_bg_add(id, d[i], -1, 0, 1) == 0, "声明常驻填表(keepalive=1)");
}
{
pid_t e2 = spawn_sleeper();
CHECK(meow_bg_add("dev.test.decnew", e2, -1, 0, 0) == MEOW_ERR_NOMEM,
"全声明常驻满 → 新增 NOMEM(不回退真退,保安全)");
kill(e2, SIGKILL); waitpid(e2, NULL, 0); /* 未入表,就地清理 */
}
for (i = 0; i < MEOW_BG_MAX; i++) {
snprintf(id, sizeof id, "dev.test.dec%d", i);
meow_bg_take(id, NULL, NULL);
kill(d[i], SIGKILL); waitpid(d[i], NULL, 0);
}
CHECK(meow_bg_count() == 0, "声明常驻清空后 bg 表空");
}
/* ================= 批①+宽限:宽限超时回收 / 声明常驻与媒体活跃不清 ================= */
g_ms = 100000; /* 本组从干净时间基准起(后续 svc 段用相对 g_ms,不受影响) */
/* 5c) 未声明 + 媒体不活跃:grace 内保活;恰等 MEOW_BG_GRACE_MS 不回收(严格 >);超界回收 */
{
pid_t s1 = spawn_sleeper();
g_bg_dead = 0;
CHECK(meow_bg_add("dev.test.grace1", s1, -1, 0, 0) == 0, "grace1 入表(未声明 keepalive=0)");
g_ms = 100000u + MEOW_BG_GRACE_MS; /* 恰好等于宽限(非严格大于) */
meow_bg_tick();
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 1 && g_bg_dead == 0,
"grace 边界:恰等 MEOW_BG_GRACE_MS 不回收(严格 > 才回收)");
g_ms = 100000u + MEOW_BG_GRACE_MS + 1; /* 超界 1ms */
pump_bg_until(cond_bg_dead, NULL);
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 0 && meow_bg_count() == 0,
"超 grace 未再启动 → 回收摘表");
CHECK(g_bg_dead == 1, "宽限回收触发 on_bg_dead(通知 amsd)+ 进程经 SIGTERM 退出");
}
/* 5d) 声明常驻(keepalive=1)超 grace 不清(无限期) */
{
pid_t k1 = spawn_sleeper();
CHECK(meow_bg_add("dev.test.gracekeep", k1, -1, 0, 1) == 0, "gracekeep 入表(keepalive=1)");
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
meow_bg_tick();
CHECK(meow_bg_is_backgrounded("dev.test.gracekeep") == 1,
"声明常驻 keepalive=1 超 grace 不清(无限期)");
meow_bg_take("dev.test.gracekeep", NULL, NULL);
kill(k1, SIGKILL); waitpid(k1, NULL, 0);
}
/* 5e) 媒体活跃(META PLAYING,keepalive=0)超 grace 不清(resident 保护) */
{
int sp2[2];
pid_t m1;
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp2) == 0, "grace media socketpair");
{
m_meta_t meta;
memset(&meta, 0, sizeof meta);
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.gracemedia");
meta.state = MEOW_MEDIA_PLAYING;
meow_media_meta(&meta, sp2[0]);
}
m1 = spawn_sleeper();
CHECK(meow_bg_add("dev.test.gracemedia", m1, -1, 0, 0) == 0,
"gracemedia 入表(keepalive=0 但媒体活跃)");
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
meow_bg_tick();
CHECK(meow_media_app_resident("dev.test.gracemedia") == 1, "gracemedia media_resident=1");
CHECK(meow_bg_is_backgrounded("dev.test.gracemedia") == 1,
"媒体活跃超 grace 不清(resident 保护)");
meow_bg_take("dev.test.gracemedia", NULL, NULL);
kill(m1, SIGKILL); waitpid(m1, NULL, 0);
meow_media_clear("dev.test.gracemedia");
close(sp2[0]); close(sp2[1]);
}
/* ================= 无头后台服务 ================= */
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, 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;
}