Merge branch 'feat/launcher-bg-kill-button-dead'

This commit is contained in:
AskaEth committed 2026-11-04 21:00:45 +08:00
commit 132622cbc1
11 files changed
+247 -82

No files matched your search

+56 -15
View File
@@ -784,11 +784,35 @@ static void out_append(const char *pkg, pid_t pid, long epoch, long up, const ch
} }
/* ---- launcher IPC 转发(xchg 一帧,读回一帧) ---- */ /* ---- launcher IPC 转发(xchg 一帧,读回一帧) ---- */
/* 最近一次 ipc_xchg 的失败原因:区分「连不上」与「连上但没及时回」。此前两者都记成
* "launcher offline",2026-11-04 真机即误诊——launcher 活着且回了 ACK,只是晚于 1200ms 超时。 */
enum { IPC_ERR_NONE = 0, IPC_ERR_CONN, IPC_ERR_IO, IPC_ERR_TIMEOUT, IPC_ERR_PROTO };
static int g_ipc_err = IPC_ERR_NONE;
static const char *ipc_err_str(void)
{
switch (g_ipc_err) {
case IPC_ERR_CONN: return "connect failed";
case IPC_ERR_IO: return "io error";
case IPC_ERR_TIMEOUT: return "timeout, no reply";
case IPC_ERR_PROTO: return "unexpected reply";
default: return "ok";
}
}
static int ipc_fail(int fd, int err)
{
if (fd >= 0) close(fd);
g_ipc_err = err;
return -1;
}
static int ipc_xchg(uint16_t type, const void *payload, uint32_t len, static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
uint8_t *rbuf, uint32_t rcap) uint8_t *rbuf, uint32_t rcap)
{ {
g_ipc_err = IPC_ERR_NONE;
int fd = socket(AF_UNIX, SOCK_STREAM, 0); int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1; if (fd < 0) { g_ipc_err = IPC_ERR_IO; return -1; }
/* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞 /* 收发加超时:launcher 刚刚重启/主循环未就绪(接受但不回包)时,amsd 的阻塞
* read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发 * read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发
* 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。 * 有界,超时即返回失败 → ams_request_ctl 退避重连,绝不无限阻塞。
@@ -802,7 +826,7 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
memset(&a, 0, sizeof a); memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX; a.sun_family = AF_UNIX;
strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1); strncpy(a.sun_path, g_ipc_sock, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { close(fd); return -1; } if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) return ipc_fail(fd, IPC_ERR_CONN);
uint8_t hdr[MEOW_IPC_HDR_LEN]; uint8_t hdr[MEOW_IPC_HDR_LEN];
uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen; uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen;
uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */ uint32_t reqid = 0; /* amsd→launcher 控制请求无需回显 */
@@ -813,31 +837,35 @@ static int ipc_xchg(uint16_t type, const void *payload, uint32_t len,
size_t off = 0; size_t off = 0;
while (off < sizeof hdr) { while (off < sizeof hdr) {
ssize_t w = write(fd, hdr + off, sizeof hdr - off); ssize_t w = write(fd, hdr + off, sizeof hdr - off);
if (w <= 0) { close(fd); return -1; } if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
off += (size_t)w; off += (size_t)w;
} }
if (len && payload) { if (len && payload) {
size_t p = 0; size_t p = 0;
while (p < len) { while (p < len) {
ssize_t w = write(fd, (const uint8_t *)payload + p, len - p); ssize_t w = write(fd, (const uint8_t *)payload + p, len - p);
if (w <= 0) { close(fd); return -1; } if (w <= 0) return ipc_fail(fd, (w < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
p += (size_t)w; p += (size_t)w;
} }
} }
off = 0; off = 0;
while (off < sizeof hdr) { while (off < sizeof hdr) {
ssize_t r = read(fd, hdr + off, sizeof hdr - off); ssize_t r = read(fd, hdr + off, sizeof hdr - off);
if (r <= 0) { close(fd); return -1; } if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
off += (size_t)r; off += (size_t)r;
} }
memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2); memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2);
memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4); memcpy(&rtype, hdr + 6, 2); memcpy(&rlen, hdr + 12, 4);
if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) { close(fd); return -1; } if (rmagic != MEOW_IPC_MAGIC || rver != MEOW_IPC_VERSION) return ipc_fail(fd, IPC_ERR_PROTO);
if (rlen > rcap) rlen = rcap; if (rlen > rcap) rlen = rcap;
off = 0; off = 0;
while (off < rlen) { while (off < rlen) {
ssize_t r = read(fd, rbuf + off, rlen - off); ssize_t r = read(fd, rbuf + off, rlen - off);
if (r <= 0) { close(fd); return -1; } if (r <= 0) return ipc_fail(fd, (r < 0 && (errno == EAGAIN || errno == EWOULDBLOCK))
? IPC_ERR_TIMEOUT : IPC_ERR_IO);
off += (size_t)r; off += (size_t)r;
} }
close(fd); close(fd);
@@ -858,11 +886,12 @@ static int ams_request_ctl(uint16_t type, const void *payload, uint32_t len, uin
if (code) *code = a.code; if (code) *code = a.code;
return 0; return 0;
} }
if (rt == MSG_ERR) return MEOW_ERR_INTERNAL; if (rt == MSG_ERR) { g_ipc_err = IPC_ERR_PROTO; return MEOW_ERR_INTERNAL; }
/* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */ /* 一次连接失败/超时(launcher 未就绪)→ 短退避再试;仍失败则返回 -1。 */
if (attempt + 1 < AMS_IPC_RETRY) if (attempt + 1 < AMS_IPC_RETRY)
usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000); usleep((useconds_t)(AMS_IPC_BACKOFF_MS * (attempt + 1)) * 1000);
} }
if (g_ipc_err == IPC_ERR_NONE) g_ipc_err = IPC_ERR_PROTO; /* 收到非 ACK/ERR 帧:不是「无回执」 */
return -1; return -1;
} }
@@ -1228,9 +1257,14 @@ static int cmd_start(const char *spec, char *resp, int rn)
/* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */ /* launcher ACK ack.code 携带正幅度(busy=8,见 util.c `rc<0?-rc:MEOW_OK`),非二补。 */
snprintf(detail, sizeof detail, "busy (app running)"); snprintf(detail, sizeof detail, "busy (app running)");
emit_log("AMS start %s -> BUSY (app running)", pkg); emit_log("AMS start %s -> BUSY (app running)", pkg);
} else if (rt == 0) {
snprintf(detail, sizeof detail, "launcher refused code=%d", (int)code);
emit_log("AMS start %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
} else { } else {
snprintf(detail, sizeof detail, "launcher offline"); /* rt!=0 只代表「这次没拿到回执」,不等于 launcher 死了(同 cmd_kill 注释)。
emit_log("AMS start %s -> launcher offline", pkg); * 响应串保留 "launcher offline"(MeowPanel 据此映射 503),日志补真实原因。 */
snprintf(detail, sizeof detail, "launcher offline (%s)", ipc_err_str());
emit_log("AMS start %s -> launcher offline (%s)", pkg, ipc_err_str());
} }
snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR", snprintf(resp, rn, "%s %s/%s\n", strstr(detail, "ok forwarded") ? "OK" : "ERR",
pkg, act[0] ? act : "(default)"); pkg, act[0] ? act : "(default)");
@@ -1251,14 +1285,21 @@ static int cmd_kill(const char *pkg, char *resp, int rn)
uint32_t code = 0; uint32_t code = 0;
int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code); int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code);
if (rt != 0 && a->pid > 0) { if (rt != 0 && a->pid > 0) {
/* 兜底:launcher 不在,直接 SIGTERM(与 launcher 的 SIGTERM 语义一致) */ /* 兜底:launcher 没给回执 → 直接 SIGTERM(与 launcher 的 SIGTERM 语义一致)。
* 注意 rt!=0 **不等于 launcher 死了**:主循环被 IPC 洪泛拖慢时 ACK 会晚于
* AMS_IPC_TIMEOUT_MS,故日志写实际原因(connect failed / timeout, no reply / ...),
* 而不是笼统的 "launcher offline"(2026-11-04 真机即此误判)。 */
kill(a->pid, SIGTERM); kill(a->pid, SIGTERM);
emit_log("AMS kill %s -> direct SIGTERM (launcher offline)", pkg); emit_log("AMS kill %s -> direct SIGTERM (launcher %s)", pkg, ipc_err_str());
} else if (rt != 0) { } else if (rt != 0) {
return snprintf(resp, rn, "ERR launcher offline: %s\n", pkg); /* 响应串保留 "launcher offline" 字样(MeowPanel api_ams_ctl 依此映射 503 AMS_UNAVAILABLE),
* 只在括号里补真实原因,便于区分「连不上」与「连上但超时」。 */
return snprintf(resp, rn, "ERR launcher offline (%s): %s\n", ipc_err_str(), pkg);
} else if (code != MEOW_OK) { } else if (code != MEOW_OK) {
emit_log("AMS kill %s -> launcher refused code=%u", pkg, code); /* launcher 明确拒绝:不改判为直杀(pid 可能已被回收,盲杀会打错进程),如实回错。
return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code); * code 是负错误码的 uint32 呈现(-2 → 4294967294),括号里给有符号值便于对照。 */
emit_log("AMS kill %s -> launcher refused code=%u(%d)", pkg, code, (int)code);
return snprintf(resp, rn, "ERR launcher refused: %s (code=%d)\n", pkg, (int)code);
} }
hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec()); hist_push(a, "ams_kill", "terminate (via launcher)", (long)time(NULL), read_uptime_sec());
int m = snprintf(resp, rn, "OK terminate %s\n", pkg); int m = snprintf(resp, rn, "OK terminate %s\n", pkg);
+29 -12
View File
@@ -2,7 +2,7 @@
* *
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar: * 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action, * 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
* act=bgm_top_action → bgmgr_kill_all,**不杀框架/当前前台/非后台**,经 bgmgr_can_kill 门控)。 * act=bgm_top_action → bgmgr_kill_all,**不杀受保护/当前前台/非后台**,经 bgmgr_can_kill 门控)。
* 列表(live=1 约 2s 重扫,实时增删): * 列表(live=1 约 2s 重扫,实时增删):
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮** * - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。 * (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
@@ -38,6 +38,9 @@ typedef struct {
int napps; int napps;
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX]; bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
int n; int n;
/* 行副文本存储:sub_row_t.sub 被 SDK 行池/自绘在整页会话内持续读取,
* 不能指向 bgm_rows 的栈帧(曾用函数内 char sub[80] → 悬垂,页面副标题为乱码)。 */
char subs[MEOW_BGMR_ROW_MAX][80];
} bgm_ctx_t; } bgm_ctx_t;
/* 找 struct app(launcher 应用表)by id */ /* 找 struct app(launcher 应用表)by id */
@@ -52,17 +55,20 @@ static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id)
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */ /* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid, static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
char *name, int nl, char *icon, int il, char *name, int nl, char *icon, int il,
char *framework, int *is_fg) char *protected_app, int *is_fg)
{ {
(void)pid; (void)pid;
bgm_ctx_t *b = ctx; bgm_ctx_t *b = ctx;
struct app *a = bgm_app_find(b->apps, b->napps, app_id); struct app *a = bgm_app_find(b->apps, b->napps, app_id);
*framework = 0; *protected_app = 0;
*is_fg = 0; *is_fg = 0;
if (!a) return -1; /* 未注册 → name 回退 app_id */ if (!a) return -1; /* 未注册 → name 回退 app_id */
if (a->name[0]) snprintf(name, nl, "%s", a->name); if (a->name[0]) snprintf(name, nl, "%s", a->name);
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : ""); if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
*framework = (a->framework == '1') ? '1' : 0; /* 刻意**不**用 a->framework:app.json 的 framework=1 意为「用 meow_ui.a 的 SDK 框架 App」
* (docs/APP_DEV_GUIDE.md §9,推荐且现有 App 全写 1),不是「系统 App」。曾把它当「禁止 kill」
* 门控 → 后台页所有行的「杀」按钮被置灰、点了不杀也不留日志(2026-11-04 真机 dev.player.ncm)。
* launcher 自身组件(状态栏/设置页/子页…)不在 App 表也不在后台表,无需此位保护。 */
return 0; return 0;
} }
@@ -174,12 +180,13 @@ static int bgm_rows(void *ctx, sub_row_t *rbuf, int max)
return r; return r;
} }
for (int i = 0; i < b->n && r < max; i++) { for (int i = 0; i < b->n && r < max; i++) {
char sub[80]; /* 副文本写进 ctx 的持久缓冲(b->subs):sub_row_t.sub 会被 SDK 行池在整页会话内反复读取,
snprintf(sub, sizeof sub, "CPU %.1f%% · %s · %s", b->rows[i].pcpu, * 指向本函数栈帧的 buffer 会在返回后变成乱码/越界(真机实证:副标题全是乱码字节)。 */
snprintf(b->subs[i], sizeof b->subs[i], "CPU %.1f%% · %s · %s", b->rows[i].pcpu,
bgmgr_mem_str(b->rows[i].rss_kb), bgmgr_status_str(&b->rows[i])); bgmgr_mem_str(b->rows[i].rss_kb), bgmgr_status_str(&b->rows[i]));
rbuf[r].kind = SUB_ROW; rbuf[r].kind = SUB_ROW;
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id; rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
rbuf[r].sub = sub; rbuf[r].sub = b->subs[i];
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */ rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
r++; r++;
} }
@@ -230,7 +237,7 @@ static void bgm_row_draw(void *ctx, int idx, sub_row_t *r,
if (r->label) meow_ui_text(rx, ry, r->label, 20, mt->onSurface); if (r->label) meow_ui_text(rx, ry, r->label, 20, mt->onSurface);
if (r->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant); if (r->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant);
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。 /* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。
* 不可杀行(框架/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */ * 不可杀行(受保护/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */
int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh); int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh);
uint32_t btn_col = mt->error; /* 可杀:error 红 */ uint32_t btn_col = mt->error; /* 可杀:error 红 */
if (i >= 0 && i < b->n && !b->rows[i].killable) if (i >= 0 && i < b->n && !b->rows[i].killable)
@@ -251,9 +258,17 @@ static int bgm_row_tap(void *ctx, int idx, sub_row_t *r, int x, int y,
if (i < 0 || i >= b->n) return 0; /* 空态行无按钮 */ if (i < 0 || i >= b->n) return 0; /* 空态行无按钮 */
int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh); int bx, by, bw, bh; bgm_btn_rect(rx, ry, rw, rh, &bx, &by, &bw, &bh);
if (x < bx || x >= bx + bw || y < by || y >= by + bh) return 0; /* 行体/按钮外 → 不杀 */ if (x < bx || x >= bx + bw || y < by || y >= by + bh) return 0; /* 行体/按钮外 → 不杀 */
if (!bgmgr_can_kill(&b->rows[i])) if (!bgmgr_can_kill(&b->rows[i])) {
return 1; /* 禁点:不可杀(框架/前台/无进程)→ 按钮已置灰,不 kill(返回 1=重绘) */ /* 禁点:受保护/前台/无进程 → 按钮已置灰,不 kill(返回 1=重绘)。
meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */ * 留一行日志:「按了没反应」当年因为没有这行日志,只能靠翻 app.json 才查出是门控。 */
logmsg("bgpage: kill 被门控 app=%s pid=%d fg=%d protected=%d",
b->rows[i].app_id, (int)b->rows[i].pid, b->rows[i].is_fg,
b->rows[i].protected_app ? 1 : 0);
return 1;
}
int rc = meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */
if (rc != 0)
logmsg("bgpage: kill app=%s 未命中后台表 rc=%d(列表/后台表不同步)", b->rows[i].app_id, rc);
return 1; return 1;
} }
@@ -274,7 +289,9 @@ static int bgm_top_action(void *ctx)
ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */ ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */
/* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤 /* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤
* term_at_ms 让清后的行当下消失(可见反馈;不可杀行本就在上/一键不杀)。 */ * term_at_ms 让清后的行当下消失(可见反馈;不可杀行本就在上/一键不杀)。 */
bgmgr_kill_all(&ops, b->rows, b->n, bgm_kill_fn); int k = bgmgr_kill_all(&ops, b->rows, b->n, bgm_kill_fn);
if (k < b->n)
logmsg("bgpage: 一键清除 杀 %d/%d(其余为前台/无进程/受保护)", k, b->n);
return 1; /* 重绘:重扫行(已清行消失) */ return 1; /* 重绘:重扫行(已清行消失) */
} }
+3 -3
View File
@@ -13,7 +13,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
if (n > max) n = max; if (n > max) n = max;
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
bgmgr_row_t *r = &rows[i]; bgmgr_row_t *r = &rows[i];
r->framework = 0; r->protected_app = 0;
r->is_fg = 0; r->is_fg = 0;
r->name[0] = 0; r->name[0] = 0;
r->icon[0] = 0; r->icon[0] = 0;
@@ -21,7 +21,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
r->rss_kb = 0; r->rss_kb = 0;
ops->app_meta(ops->ctx, r->app_id, r->pid, ops->app_meta(ops->ctx, r->app_id, r->pid,
r->name, (int)sizeof r->name, r->icon, (int)sizeof r->icon, r->name, (int)sizeof r->name, r->icon, (int)sizeof r->icon,
&r->framework, &r->is_fg); &r->protected_app, &r->is_fg);
if (!r->name[0]) snprintf(r->name, sizeof r->name, "%s", r->app_id); if (!r->name[0]) snprintf(r->name, sizeof r->name, "%s", r->app_id);
if (ops->proc_sample && ops->proc_sample(ops->ctx, r->pid, &r->pcpu, &r->rss_kb) < 0) { if (ops->proc_sample && ops->proc_sample(ops->ctx, r->pid, &r->pcpu, &r->rss_kb) < 0) {
r->pcpu = 0.0; r->rss_kb = 0; r->pcpu = 0.0; r->rss_kb = 0;
@@ -34,7 +34,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max)
int bgmgr_can_kill(const bgmgr_row_t *r) int bgmgr_can_kill(const bgmgr_row_t *r)
{ {
if (!r) return 0; if (!r) return 0;
if (r->framework) return 0; /* 框架/系统 App 不 kill(铁律) */ if (r->protected_app) return 0; /* 系统级受保护 App 不 kill(铁律;framework 是 SDK 标志,不在此列) */
if (r->is_fg) return 0; /* 当前前台 App 不 kill */ if (r->is_fg) return 0; /* 当前前台 App 不 kill */
if (r->pid <= 0) return 0; /* 非后台/无进程 */ if (r->pid <= 0) return 0; /* 非后台/无进程 */
return 1; return 1;
+11 -6
View File
@@ -4,7 +4,10 @@
* 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法: * 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法:
* UI 提供真实 enumerate/app_meta/proc_sample;单测用假件。**单一写者=launcher 主循环**,无锁。 * UI 提供真实 enumerate/app_meta/proc_sample;单测用假件。**单一写者=launcher 主循环**,无锁。
* *
* 铁律遵循:kill/一键清 带 !framework 门控(防误杀框架/系统 App,对齐 app_launcher.c:2088 教训); * 铁律遵循:kill/一键清 带 !protected_app 门控(防误杀系统/受保护 App,对齐 app_launcher.c:2088 教训)。
* **收口:门控位不是 app.json 的 `framework`**——那是「用 meow_ui.a 的 SDK 框架 App」标志
* (docs/APP_DEV_GUIDE.md §9,推荐且现有 App 全写 1),拿它当「禁止 kill」会让所有后台 App 变成
* 不可杀(2026-11-04 真机:dev.player.ncm 声明 framework:1 → 后台页杀按钮全被置灰点不动)。
* 不新建 daemon;不引 GDBus;不碰 /data/recovery_all。 * 不新建 daemon;不引 GDBus;不碰 /data/recovery_all。
*/ */
#ifndef MEOW_BGMGR_H #ifndef MEOW_BGMGR_H
@@ -23,7 +26,9 @@ typedef struct bgmgr_row {
char app_id[MEOW_APP_ID_MAX]; char app_id[MEOW_APP_ID_MAX];
char name[49]; /* display name(回退 app_id) */ char name[49]; /* display name(回退 app_id) */
char icon[129]; /* 图标 path(空=无,页面用首字/占位) */ char icon[129]; /* 图标 path(空=无,页面用首字/占位) */
char framework; /* '1'=framework=true(看门狗/系统 App,禁止 kill) */ char protected_app; /* '1'=系统级受保护 App(禁止 kill)。**非** app.json framework;
* 目前 launcher 自身组件(状态栏/设置页…)不在 App 表/后台表里,
* 故实际恒为 0,保留为将来受保护组件的显式标记位。 */
pid_t pid; /* >0 后台进程 */ pid_t pid; /* >0 后台进程 */
int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */ int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */
int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */ int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */
@@ -39,10 +44,10 @@ typedef struct bgmgr_ops {
/* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms, /* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms,
* 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */ * 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */
int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max); int (*enumerate)(void *ctx, bgmgr_row_t *rows, int max);
/* 按 app_id 补 App 元数据:name/icon/framework/is_fg。返回 0=找到;<0=未注册(name 回退 app_id)。 */ /* 按 app_id 补 App 元数据:name/icon/protected_app/is_fg。返回 0=找到;<0=未注册(name 回退 app_id)。 */
int (*app_meta)(void *ctx, const char *app_id, pid_t pid, int (*app_meta)(void *ctx, const char *app_id, pid_t pid,
char *name, int nl, char *icon, int il, char *name, int nl, char *icon, int il,
char *framework, int *is_fg); char *protected_app, int *is_fg);
/* 每进程 CPU/RSS 取样:返回 0=ok(写 pcpu 与 rss_kb);<0=无数据(pcpu=0/rss=0)。 */ /* 每进程 CPU/RSS 取样:返回 0=ok(写 pcpu 与 rss_kb);<0=无数据(pcpu=0/rss=0)。 */
int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb); int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb);
void (*log)(const char *fmt, ...); /* 可空=静默 */ void (*log)(const char *fmt, ...); /* 可空=静默 */
@@ -53,11 +58,11 @@ typedef struct bgmgr_ops {
* 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */ * 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */
int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max); int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max);
/* !framework 门控 + 非前台 + pid>0:返回 1=可 kill;0=禁止(框架/非后台/当前前台/无进程)。 */ /* !protected_app 门控 + 非前台 + pid>0:返回 1=可 kill;0=禁止(受保护/当前前台/无进程)。 */
int bgmgr_can_kill(const bgmgr_row_t *r); int bgmgr_can_kill(const bgmgr_row_t *r);
/* 一键清:遍历 rows,bgmgr_can_kill() 的条目逐一调 kill_fn(app_id)。返回发出 kill 数。 /* 一键清:遍历 rows,bgmgr_can_kill() 的条目逐一调 kill_fn(app_id)。返回发出 kill 数。
* **不杀框架 App/当前前台/非后台**;调用方负责对返回 0 弹「无后台 App 可清」之类提示。 */ * **不杀受保护 App/当前前台/非后台**;调用方负责对返回 0 弹「无后台 App 可清」之类提示。 */
int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n, int bgmgr_kill_all(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int n,
void (*kill_fn)(const char *app_id)); void (*kill_fn)(const char *app_id));
+6 -3
View File
@@ -270,9 +270,11 @@ void meow_bg_tick(void)
} }
} }
/* 批③后台管理页:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。 /* 批③后台管理页 / `am kill`:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。
* 只置 term_at_ms(tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表), * 只置 term_at_ms(tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表),
* 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。 */ * 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。
* 调用者:bg_page.c 行内「杀」/「一键清除」+ util.c MSG_AMS_KILL(`am kill`,无前台会话时)
* ——两个入口同一通道,故页面按钮与面板/CLI kill 行为对称。 */
int meow_bg_terminate(const char *app_id) int meow_bg_terminate(const char *app_id)
{ {
if (!app_id || !app_id[0]) return MEOW_ERR_INVAL; if (!app_id || !app_id[0]) return MEOW_ERR_INVAL;
@@ -281,7 +283,8 @@ int meow_bg_terminate(const char *app_id)
meow_bg_entry_t *e = &g_bg[i]; meow_bg_entry_t *e = &g_bg[i];
if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) { if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) {
if (e->term_at_ms == 0) { if (e->term_at_ms == 0) {
bg_log("[bgplay] 后台管理页终止后台App: %s pid=%d", app_id, (int)e->pid); bg_log("[bgplay] 用户终止后台App: %s pid=%d(后台管理页/am kill 同通道)",
app_id, (int)e->pid);
kill(e->pid, SIGTERM); kill(e->pid, SIGTERM);
e->term_at_ms = bg_now_ms(); e->term_at_ms = bg_now_ms();
} }
+2 -1
View File
@@ -122,7 +122,8 @@ int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int kee
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid); int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid);
/* 该 App 是否在后台保活。 */ /* 该 App 是否在后台保活。 */
int meow_bg_is_backgrounded(const char *app_id); int meow_bg_is_backgrounded(const char *app_id);
/* 批③后台管理页:请求终止某个后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。 /* 用户终止后台保活 App(走既有 SIGTERM→SIGKILL 真退序列)。调用者:后台管理页行内「杀」/
* 「一键清除」+ launcher IPC MSG_AMS_KILL(`am kill`)——两入口同通道,页面与面板/CLI 行为对称。
* 置 e->term_at_ms=now 由 meow_bg_tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表。 * 置 e->term_at_ms=now 由 meow_bg_tick 续做 SIGTERM→TERM_GRACE_MS→SIGKILL→reap→on_bg_dead→摘表。
* 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */ * 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */
int meow_bg_terminate(const char *app_id); int meow_bg_terminate(const char *app_id);
+5 -2
View File
@@ -93,7 +93,8 @@ enum {
/* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ---- /* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ----
* 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */ * 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */
MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */ MSG_AMS_START = 0x00A0, /* → 请求 launcher 拉起某 App(pkg + 可选 activity) */
MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义) */ MSG_AMS_KILL = 0x00A1, /* → 请求 launcher 终止某 App(SIGTERM 语义):后台保活表命中→就地真退序列;
* 否则有前台会话→置 pending 交会话消费;都没有→ACK -2(MEOW_ERR_NOENT) */
/* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ---- /* ---- audiod 独占音频服务(devdocs/plan/AUDIOD_DESIGN.md §2):0x00C0-0x00CA ----
* 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。 * 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。
* 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */ * 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */
@@ -463,7 +464,9 @@ typedef struct m_ams_start {
char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */ char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */
} m_ams_start_t; /* 160 B */ } m_ams_start_t; /* 160 B */
/* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。 */ /* MSG_AMS_KILL →:请求 launcher 终止某 App(SIGTERM 语义,执行器=launcher)。
* 后台保活 App:launcher 就地走 meow_bg_terminate 真退序列(与后台管理页「杀」同一通道);
* 前台 App:置 pending 交前台会话消费;两者皆非 → ACK code=(uint32_t)MEOW_ERR_NOENT(-2)。 */
typedef struct m_ams_kill { typedef struct m_ams_kill {
char app_id[MEOW_APP_ID_MAX]; /* 32 */ char app_id[MEOW_APP_ID_MAX]; /* 32 */
} m_ams_kill_t; /* 32 B */ } m_ams_kill_t; /* 32 B */
+26 -13
View File
@@ -1,11 +1,12 @@
/* test_bgmgr.c — 批③ 后台管理页 逻辑层宿主单测(meow_bgmgr)。 /* test_bgmgr.c — 批③ 后台管理页 逻辑层宿主单测(meow_bgmgr)。
* *
* 验证: * 验证:
* 1) bgmgr_collect:enumerate 后台表 → app_meta 补 name/icon/framework/is_fg → proc_sample 补 * 1) bgmgr_collect:enumerate 后台表 → app_meta 补 name/icon/protected_app/is_fg → proc_sample 补
* pcpu/rss_kb;行 killable 预判正确;name 回退 app_id。 * pcpu/rss_kb;行 killable 预判正确;name 回退 app_id。
* 2) bgmgr_can_kill 门控矩阵:framework=1 → 0(**框架 App 不 kill**);is_fg=1 → 0(当前前台不 kill); * 2) bgmgr_can_kill 门控矩阵:protected_app=1 → 0(**受保护 App 不 kill**);is_fg=1 → 0(当前前台不 kill);
* pid<=0 → 0(非后台/无进程);普通后台(非框架/非前台/pid>0)→ 1。 * pid<=0 → 0(非后台/无进程);普通后台(非受保护/非前台/pid>0)→ 1。
* 3) bgmgr_kill_all 一键清:框架 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。 * 3) bgmgr_kill_all 一键清:受保护 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。
* 3b) 回归:app.json framework=1(SDK 框架 App 标志,现有 App 全写 1)**不影响**可杀性。
* 4) bgmgr_status_str / bgmgr_mem_str 文案。 * 4) bgmgr_status_str / bgmgr_mem_str 文案。
* 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编: * 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编:
* gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr * gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr
@@ -17,7 +18,9 @@
#include "meow_bgmgr.h" #include "meow_bgmgr.h"
static int g_fails = 0; static int g_fails = 0;
static int g_checks = 0;
#define CHECK(cond, msg) do { \ #define CHECK(cond, msg) do { \
g_checks++; \
if (cond) printf("PASS: %s\n", msg); \ if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0) else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
@@ -33,7 +36,7 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
{ {
(void)ctx; (void)ctx;
if (max < 4) return 0; if (max < 4) return 0;
/* 4 个后台行:hello=普通、reader=常驻、sysguard=framework、fgapp=当前前台 */ /* 4 个后台行:hello=普通、reader=常驻、sysguard=受保护、fgapp=当前前台 */
memset(&rows[0], 0, sizeof rows[0]); memset(&rows[0], 0, sizeof rows[0]);
snprintf(rows[0].app_id, sizeof rows[0].app_id, "%s", g_meta_app[0]); snprintf(rows[0].app_id, sizeof rows[0].app_id, "%s", g_meta_app[0]);
rows[0].pid = 1001; rows[0].keepalive = 0; rows[0].on_card = 0; rows[0].bg_since_ms = 1000; rows[0].pid = 1001; rows[0].keepalive = 0; rows[0].on_card = 0; rows[0].bg_since_ms = 1000;
@@ -52,10 +55,10 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max)
return 4; return 4;
} }
/* meta:hello/reader/sysguard/fgapp 各给名字;sysguard 标 framework;pid==1004 标前台。 */ /* meta:hello/reader/sysguard/fgapp 各给名字;sysguard 标受保护;pid==1004 标前台。 */
static int fake_app_meta(void *ctx, const char *app_id, pid_t pid, static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
char *name, int nl, char *icon, int il, char *name, int nl, char *icon, int il,
char *framework, int *is_fg) char *protected_app, int *is_fg)
{ {
(void)ctx; (void)ctx;
if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放"); if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放");
@@ -64,7 +67,7 @@ static int fake_app_meta(void *ctx, const char *app_id, pid_t pid,
else if (!strcmp(app_id, "os.meow.fgapp")) snprintf(name, nl, "前台应用"); else if (!strcmp(app_id, "os.meow.fgapp")) snprintf(name, nl, "前台应用");
else return -1; /* 未注册 → name 回退 app_id */ else return -1; /* 未注册 → name 回退 app_id */
snprintf(icon, il, "/tmp/icon_%s.png", app_id); snprintf(icon, il, "/tmp/icon_%s.png", app_id);
*framework = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0; *protected_app = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0;
*is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */ *is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */
return 0; return 0;
} }
@@ -112,9 +115,9 @@ int main(void)
printf("=== 1) collect ===\n"); printf("=== 1) collect ===\n");
CHECK(n == 4, "collect returns 4 rows"); CHECK(n == 4, "collect returns 4 rows");
CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid"); CHECK(!strcmp(rows[0].name, "播放") && rows[0].pid == 1001, "row0 hello name/pid");
CHECK(rows[0].framework == 0, "row0 hello not framework"); CHECK(rows[0].protected_app == 0, "row0 hello not protected");
CHECK(!strcmp(rows[1].name, "阅读") && rows[1].keepalive == 1, "row1 reader keepalive"); CHECK(!strcmp(rows[1].name, "阅读") && rows[1].keepalive == 1, "row1 reader keepalive");
CHECK(rows[2].framework == '1', "row2 sysguard framework"); CHECK(rows[2].protected_app == '1', "row2 sysguard protected");
CHECK(rows[3].is_fg == 1, "row3 fgapp is_fg"); CHECK(rows[3].is_fg == 1, "row3 fgapp is_fg");
CHECK(rows[0].pcpu > 0 && rows[0].rss_kb > 0, "row0 pcpu/rss populated"); CHECK(rows[0].pcpu > 0 && rows[0].rss_kb > 0, "row0 pcpu/rss populated");
CHECK(rows[0].killable == 1 && rows[2].killable == 0, "killable prefilled by can_kill"); CHECK(rows[0].killable == 1 && rows[2].killable == 0, "killable prefilled by can_kill");
@@ -132,7 +135,7 @@ int main(void)
} }
printf("=== 2) can_kill gating ===\n"); printf("=== 2) can_kill gating ===\n");
CHECK(bgmgr_can_kill(&rows[2]) == 0, "framework app NOT killable"); CHECK(bgmgr_can_kill(&rows[2]) == 0, "protected app NOT killable");
CHECK(bgmgr_can_kill(&rows[3]) == 0, "fg app NOT killable (is_fg)"); CHECK(bgmgr_can_kill(&rows[3]) == 0, "fg app NOT killable (is_fg)");
CHECK(bgmgr_can_kill(&rows[0]) == 1, "normal bg app killable"); CHECK(bgmgr_can_kill(&rows[0]) == 1, "normal bg app killable");
CHECK(bgmgr_can_kill(&rows[1]) == 1, "keepalive bg app killable"); CHECK(bgmgr_can_kill(&rows[1]) == 1, "keepalive bg app killable");
@@ -155,11 +158,21 @@ int main(void)
if (!strcmp(g_killed_app[i], "os.meow.fgapp")) killed_fg = 1; if (!strcmp(g_killed_app[i], "os.meow.fgapp")) killed_fg = 1;
if (!strcmp(g_killed_app[i], "os.meow.hello")) killed_bg = 1; if (!strcmp(g_killed_app[i], "os.meow.hello")) killed_bg = 1;
} }
CHECK(killed_fw == 0, "kill_all did NOT kill framework app"); CHECK(killed_fw == 0, "kill_all did NOT kill protected app");
CHECK(killed_fg == 0, "kill_all did NOT kill fg app"); CHECK(killed_fg == 0, "kill_all did NOT kill fg app");
CHECK(killed_bg == 1, "kill_all killed a real bg app"); CHECK(killed_bg == 1, "kill_all killed a real bg app");
CHECK(bgmgr_kill_all(&ops, rows, n, NULL) == 0, "kill_all NULL kill_fn -> 0"); CHECK(bgmgr_kill_all(&ops, rows, n, NULL) == 0, "kill_all NULL kill_fn -> 0");
/* 回归(2026-11-04 真机):app.json framework=1 是「SDK 框架 App」标志(现有 App 全写 1),
* **不是**受保护位。旧实现把它当门控 → dev.player.ncm 等所有后台 App 都不可杀、按钮置灰。 */
printf("=== 3b) framework(SDK) ≠ 受保护 ===\n");
{
bgmgr_row_t sdk; memset(&sdk, 0, sizeof sdk);
snprintf(sdk.app_id, sizeof sdk.app_id, "dev.player.ncm");
sdk.pid = 7520; sdk.protected_app = 0;
CHECK(bgmgr_can_kill(&sdk) == 1, "SDK 框架 App(framework=1)后台仍可杀");
}
printf("=== 4) status/mem str ===\n"); printf("=== 4) status/mem str ===\n");
CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻"); CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻");
CHECK(!strcmp(bgmgr_status_str(&rows[0]), "后台"), "status_str normal=后台"); CHECK(!strcmp(bgmgr_status_str(&rows[0]), "后台"), "status_str normal=后台");
@@ -167,7 +180,7 @@ int main(void)
CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB"); CHECK(!strcmp(bgmgr_mem_str(512), "512KB"), "mem_str(<1MB)=KB");
CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB"); CHECK(!strcmp(bgmgr_mem_str(2048), "2.0MB"), "mem_str(>=1MB)=MB");
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", 20); if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
else printf("\n%d FAILED\n", g_fails); else printf("\n%d FAILED\n", g_fails);
return g_fails ? 1 : 0; return g_fails ? 1 : 0;
} }
+62 -21
View File
@@ -1,14 +1,16 @@
/* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。 /* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。
* *
* 通过 #include bg_page.c 把其 FILE-STATIC 的 bgm_rows/bgm_btn_rect/bgm_row_tap/bgm_top_action * 通过 #include bg_page.c 把其 FILE-STATIC 的 bgm_rows/bgm_btn_rect/bgm_row_tap/bgm_top_action
* 平移进本 TU;桩掉 launcher 后台表(meow_bg_count/at/terminate)+ 时钟 + meow_str, * 平移进本 TU;桩掉 launcher 后台表(meow_bg_count/at/terminate)+ 时钟 + meow_str/logmsg,
* 直接验证: * 直接验证:
* P1 bgm_rows 行骨架:后台 App 行(act=i)+ 空态行(act=-1);name/framework 经 app_meta 正确; * P1 bgm_rows 行骨架:后台 App 行(act=i)+ 空态行(act=-1);name 经 app_meta 正确;
* **行副文本指向 ctx 持久缓冲且扛得住栈抖动**(曾指向 bgm_rows 栈帧 → 真机副标题乱码);
* P2 bgm_btn_rect 按钮几何:行右 46×34 圆角钮(rx+rw-12-46 定位); * P2 bgm_btn_rect 按钮几何:行右 46×34 圆角钮(rx+rw-12-46 定位);
* P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;框架 App * P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;
* 按钮点击 → 不 terminate(!framework 门控);空态行不响应; * **app.json framework=1(SDK 框架 App)照样可杀**(2026-11-04 真机回归);
* P4 bgm_top_action 顶栏清除:kill_all **不杀框架 App**(sysguard 不 terminate),杀普通后台; * 受保护/前台行按钮点击 → 不 terminate(门控,留诊断日志);空态行不响应;
* P5 门控矩阵:bgmgr_can_kill(framework/fg/pid<=0/普通)。 * P4 bgm_top_action 顶栏清除:kill_all 不杀受保护/前台、杀普通后台(含 framework=1 的);
* P5 门控矩阵:bgmgr_can_kill(protected_app/is_fg/pid<=0/普通)。
* 不调用行自绘 bgm_row_draw(纯逻辑),故无需桩 SDK 绘制;行自绘段经 -ffunction-sections/--gc-sections 剔除。 * 不调用行自绘 bgm_row_draw(纯逻辑),故无需桩 SDK 绘制;行自绘段经 -ffunction-sections/--gc-sections 剔除。
* *
* 编译(在 patch_v2/meow_launcherd 下): * 编译(在 patch_v2/meow_launcherd 下):
@@ -44,6 +46,16 @@ int meow_bg_terminate(const char *app_id)
} }
unsigned long long now_ms(void) { return 0; } unsigned long long now_ms(void) { return 0; }
const char *meow_str(const char *id) { (void)id; return NULL; } /* 未知 id → meow_str_zh 回退 zh */ const char *meow_str(const char *id) { (void)id; return NULL; } /* 未知 id → meow_str_zh 回退 zh */
static int g_log_n;
void logmsg(const char *fmt, ...) { (void)fmt; g_log_n++; } /* 只计数:门控/未命中应留诊断日志 */
/* 栈抖动:bgm_rows 返回后调用,用来暴露「sub 指向栈帧」的悬垂指针。 */
static void stack_clobber(void)
{
volatile char buf[256];
memset((void *)buf, 'X', sizeof buf);
buf[0] = buf[0]; /* 防优化掉 */
}
/* ---------- 测试态:launcher 应用表(app_meta 用) ---------- */ /* ---------- 测试态:launcher 应用表(app_meta 用) ---------- */
static struct app g_apps[4]; static struct app g_apps[4];
@@ -71,7 +83,9 @@ static void bg_reset(int n, const char *id0, pid_t pid0, const char *id1, pid_t
} }
static int g_fails = 0; static int g_fails = 0;
static int g_checks = 0;
#define CHECK(cond, msg) do { \ #define CHECK(cond, msg) do { \
g_checks++; \
if (cond) printf("PASS: %s\n", msg); \ if (cond) printf("PASS: %s\n", msg); \
else { printf("FAIL: %s\n", msg); g_fails++; } } while (0) else { printf("FAIL: %s\n", msg); g_fails++; } } while (0)
@@ -86,21 +100,31 @@ int main(void)
{ {
static bgm_ctx_t b; static bgm_ctx_t b;
memset(&b, 0, sizeof b); memset(&b, 0, sizeof b);
b.apps = g_apps; b.napps = 2; b.apps = g_apps; b.napps = 3;
set_app(0, "os.meow.hello", "播放", 0); set_app(0, "os.meow.hello", "播放", 0);
set_app(1, "os.meow.sysguard", "系统守护", '1'); set_app(1, "dev.player.ncm", "流媒体客户端", '1'); /* SDK 框架 App(真机 app.json framework:1) */
set_app(2, "os.meow.sysguard", "系统守护", 0);
static sub_row_t rows[SUB_ROWS_MAX]; static sub_row_t rows[SUB_ROWS_MAX];
printf("=== P1) bgm_rows 行骨架 ===\n"); printf("=== P1) bgm_rows 行骨架 + 副文本持久化 ===\n");
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002); bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
int rn = bgm_rows(&b, rows, SUB_ROWS_MAX); int rn = bgm_rows(&b, rows, SUB_ROWS_MAX);
CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)"); CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)");
CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0"); CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0");
CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)"); CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)");
CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1"); CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1");
CHECK(!strcmp(rows[1].label, "系统守护"), "row1 label=系统守护"); CHECK(!strcmp(rows[1].label, "流媒体客户端"), "row1 label=流媒体客户端");
CHECK(!strcmp(rows[1].sub, "") || rows[1].sub[0] != '\0', "row1 sub 非空(CPU/内存/状态)"); CHECK(rows[1].sub != NULL && !strncmp(rows[1].sub, "CPU ", 4), "row1 sub=CPU/内存/状态");
CHECK(b.n == 2, "bgm_ctx.n=2"); CHECK(b.n == 2, "bgm_ctx.n=2");
/* 悬垂指针回归:sub 必须落在 ctx 缓冲里,且栈被改写后内容不变 */
uintptr_t subp = (uintptr_t)rows[1].sub;
uintptr_t lob = (uintptr_t)(char *)&b;
uintptr_t hib = lob + sizeof b;
CHECK(subp >= lob && subp < hib, "sub 指向 ctx 持久缓冲(不是 bgm_rows 栈帧)");
char sub_before[80];
snprintf(sub_before, sizeof sub_before, "%s", rows[1].sub);
stack_clobber();
CHECK(!strcmp(rows[1].sub, sub_before), "栈抖动后 sub 内容不变(无悬垂)");
printf("=== P2) bgm_btn_rect 按钮几何(行右 46×34) ===\n"); printf("=== P2) bgm_btn_rect 按钮几何(行右 46×34) ===\n");
{ {
@@ -111,7 +135,7 @@ int main(void)
} }
printf("=== P3) bgm_row_tap 行内杀按钮命中(仅按钮区 kill,行区不误杀) ===\n"); printf("=== P3) bgm_row_tap 行内杀按钮命中(仅按钮区 kill,行区不误杀) ===\n");
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002); bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
bgm_rows(&b, rows, SUB_ROWS_MAX); bgm_rows(&b, rows, SUB_ROWS_MAX);
/* 按钮区命中:行(0,0,480,56),按钮 x 422-468, y 11-45 */ /* 按钮区命中:行(0,0,480,56),按钮 x 422-468, y 11-45 */
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 1, "按钮点击→返回1(重绘)"); CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 1, "按钮点击→返回1(重绘)");
@@ -122,9 +146,19 @@ int main(void)
CHECK(bgm_row_tap(&b, 0, &rows[0], 410, 28, 0, 0, 480, 56) == 0, "按钮左侧缘(410)→不杀"); CHECK(bgm_row_tap(&b, 0, &rows[0], 410, 28, 0, 0, 480, 56) == 0, "按钮左侧缘(410)→不杀");
CHECK(bgm_row_tap(&b, 0, &rows[0], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀"); CHECK(bgm_row_tap(&b, 0, &rows[0], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀");
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀"); CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀");
/* 框架 App:按钮命中但不 terminate(!framework 门控) */ /* 回归(2026-11-04 真机「杀后台按钮按下去没反应」):dev.player.ncm 的 app.json 写 framework:1
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "框架App按钮→返回1(不可杀提示)"); * (= 用 SDK 框架,现有 App 全写 1),**必须照杀**——旧实现把它当受保护位,按钮置灰点不动。 */
CHECK(term_count_app("os.meow.sysguard") == 0, "框架App按钮点击→不 terminate"); CHECK(b.rows[1].killable == 1, "framework=1(SDK 框架)App 仍判可杀");
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "framework=1 App 按钮→返回1");
CHECK(term_count_app("dev.player.ncm") == 1, "framework=1 App 按钮→真 terminate(无反应 bug 回归)");
/* 真·受保护行(未来系统组件/前台):按钮命中但不 terminate,且留诊断日志 */
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
bgm_rows(&b, rows, SUB_ROWS_MAX);
b.rows[1].protected_app = '1';
g_log_n = 0;
CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "受保护行按钮→返回1(重绘)");
CHECK(term_count_app("os.meow.sysguard") == 0, "受保护行按钮→不 terminate");
CHECK(g_log_n >= 1, "受保护行按钮→留一行诊断日志(可查「点了没反应」)");
printf("=== P3b) 空态行不响应 ===\n"); printf("=== P3b) 空态行不响应 ===\n");
bg_reset(0, NULL, 0, NULL, 0); bg_reset(0, NULL, 0, NULL, 0);
@@ -133,12 +167,19 @@ int main(void)
CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)"); CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 28, 0, 0, 480, 56) == 0, "空态行点击→返回0(不杀)");
g_term_calls = 0; g_term_calls = 0;
printf("=== P4) 顶栏清除(一键清除后台)不杀框架/前台 ===\n"); printf("=== P4) 顶栏清除(一键清除后台)不杀受保护/前台 ===\n");
bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002); bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002);
bgm_rows(&b, rows, SUB_ROWS_MAX); bgm_rows(&b, rows, SUB_ROWS_MAX);
b.rows[1].protected_app = '1';
CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)"); CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)");
CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello"); CHECK(term_count_app("os.meow.hello") == 1, "清除→terminate 普通后台 hello");
CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀框架 sysguard"); CHECK(term_count_app("os.meow.sysguard") == 0, "清除→不杀受保护 sysguard");
/* 回归:framework=1 的 SDK 框架 App 也在「一键清」范围内 */
bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520);
bgm_rows(&b, rows, SUB_ROWS_MAX);
g_term_calls = 0;
CHECK(bgm_top_action(&b) == 1, "framework=1 App:一键清返回 1");
CHECK(term_count_app("dev.player.ncm") == 1, "一键清→terminate framework=1 App(回归)");
/* 前台 App(is_fg)不杀:给 hello 标前台再清 */ /* 前台 App(is_fg)不杀:给 hello 标前台再清 */
bg_reset(1, "os.meow.hello", 1001, NULL, 0); bg_reset(1, "os.meow.hello", 1001, NULL, 0);
bgm_rows(&b, rows, SUB_ROWS_MAX); bgm_rows(&b, rows, SUB_ROWS_MAX);
@@ -153,12 +194,12 @@ int main(void)
{ {
bgmgr_row_t r; memset(&r, 0, sizeof r); bgmgr_row_t r; memset(&r, 0, sizeof r);
r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀"); r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀");
r.framework = '1'; CHECK(bgmgr_can_kill(&r) == 0, "framework 不可杀"); r.protected_app = '1'; CHECK(bgmgr_can_kill(&r) == 0, "protected_app 不可杀");
r.framework = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀"); r.protected_app = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀");
r.is_fg = 0; r.pid = 0; CHECK(bgmgr_can_kill(&r) == 0, "pid<=0 不可杀"); r.is_fg = 0; r.pid = 0; CHECK(bgmgr_can_kill(&r) == 0, "pid<=0 不可杀");
} }
if (g_fails == 0) printf("\nALL PASS (%d checks)\n", 32); if (g_fails == 0) printf("\nALL PASS (%d checks)\n", g_checks);
else printf("\n%d FAILED\n", g_fails); else printf("\n%d FAILED\n", g_fails);
return g_fails ? 1 : 0; return g_fails ? 1 : 0;
} }
+35 -2
View File
@@ -7,6 +7,8 @@
* should_keep=0(停播后再退出=真退); * should_keep=0(停播后再退出=真退);
* 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽; * 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽;
* 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS); * 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 回收(含恰等边界不回收); * 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清; * 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM * 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
@@ -138,11 +140,17 @@ static int cond_svc_gone(void *ctx)
return meow_svc_find("dev.test.svc", "daemon") == NULL; return meow_svc_find("dev.test.svc", "daemon") == NULL;
} }
/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。 */ /* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。
#define TEST_SVC_PATH "/tmp/meow_bgplay_test_svc.sh" * 路径取 $TMPDIR(Termux/Android 主机没有 /tmp),缺省回退 /tmp。 */
static char g_svc_path[256];
#define TEST_SVC_PATH g_svc_path
static void write_test_svc(void) 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"); 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); fputs("#!/bin/sh\nexec sleep 300\n", f);
fclose(f); fclose(f);
chmod(TEST_SVC_PATH, 0755); chmod(TEST_SVC_PATH, 0755);
@@ -271,6 +279,31 @@ int main(void)
"卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)"); "卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)");
g_card = 1; 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 / 重复 / 参数 ---- */ /* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
{ {
pid_t k[MEOW_BG_MAX]; pid_t k[MEOW_BG_MAX];
+12 -4
View File
@@ -1466,15 +1466,23 @@ static void ipc_dispatch(struct meow_ipc_msg *m)
memcpy(&k, m->payload, m->plen < (uint16_t)sizeof k ? m->plen : (uint16_t)sizeof k); memcpy(&k, m->payload, m->plen < (uint16_t)sizeof k ? m->plen : (uint16_t)sizeof k);
k.app_id[MEOW_APP_ID_MAX - 1] = 0; k.app_id[MEOW_APP_ID_MAX - 1] = 0;
m_ack_t a; m_ack_t a;
if (g_app_session_active) { a.status = 0;
/* 先打后台保活表:后台 App 的终止由主循环单写者就地执行(meow_bg_terminate 置
* term_at_ms + SIGTERM,meow_bg_tick 续 SIGKILL→reap→on_bg_dead 上报 amsd)——
* 与后台管理页「杀」按钮**同一条通道**,故 `am kill` 与页面按钮现行为对称。
* 此前只置 pending 交前台会话消费:用户不在 App 里时(桌面/后台页/面板)没有前台会话
* → 只能回 -2(MEOW_ERR_NOENT),amsd 判「拒绝」不杀(ACK 及时)或超时后盲发
* SIGTERM(ACK 迟到)——2026-11-04 真机 ACK 0xFFFFFFFE 即此,行为随主循环负载漂移。 */
if (meow_bg_terminate(k.app_id) == 0) {
a.code = MEOW_OK;
} else if (g_app_session_active) {
meow_ams_pending_kill_set(k.app_id); meow_ams_pending_kill_set(k.app_id);
a.code = MEOW_OK; a.code = MEOW_OK;
} else { } else {
a.code = (uint32_t)MEOW_ERR_NOENT; /* 无前台 App 可终止 */ a.code = (uint32_t)MEOW_ERR_NOENT; /* 既非后台保活 App,也无前台 App 会话 */
} }
a.status = 0;
ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a);
logmsg("ipc: AMS_KILL app=%s -> ACK code=%u", k.app_id, a.code); logmsg("ipc: AMS_KILL app=%s -> ACK code=%u(%d)", k.app_id, a.code, (int)a.code);
break; break;
} }
/* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。 /* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。