根因(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 重定义),其余文件干净。
323 lines
15 KiB
C
323 lines
15 KiB
C
/* bg_page.c — 批③ 后台管理页(桌面顶栏居右按钮进入)。
|
||
*
|
||
* 容器:sub_page_t 脚手架(subpage.c,与 wifi_page/bt_page 同款)。顶栏=meow_topappbar:
|
||
* 返回箭头 + 标题「后台管理」+ **顶栏标题栏右侧「一键清除后台」按钮**(sub_page_t.top_action,
|
||
* act=bgm_top_action → bgmgr_kill_all,**不杀受保护/当前前台/非后台**,经 bgmgr_can_kill 门控)。
|
||
* 列表(live=1 约 2s 重扫,实时增删):
|
||
* - 每后台 App 一行 SUB_ROW:label=名字,sub=「CPU x% · yMB · 状态」,**行最右一个「杀」按钮**
|
||
* (sub_page_t.row_draw 行自绘 + row_tap 自定义命中:命中按钮区=kill 该 App;行体不响应)。
|
||
* - 行=MD3 圆角卡(meow_gfx_fill_round r=12 surfaceContainer;卡在行高内上下对称内缩
|
||
* BGM_CARD_GAP/2,行中心=卡中心,相邻卡 8px 外距——与主界面列表/ ncm 歌单同手法)。
|
||
* - 空态「无后台 App」。
|
||
* 数据源=amsd FG/BG 注册表(后台常驻集=launcher meow_bg 表,EV BG/FG 上报进 amsd)+ 每进程
|
||
* pcpu/rss(meow_svc_perf 同源 /proc 聚合,本页在 launcher 内同口径取)。
|
||
*
|
||
* 按钮化(相对 agent-425 的「点行 kill + 顶置清除动作行」):
|
||
* - kill:改「点整行」为「行最右独立杀按钮」——行自绘(bgm_row_draw)+ 自定义命中(bgm_row_tap),
|
||
* 命中几何=bgm_btn_rect(行右 46×34 圆角按钮);行体其它区域 point 回 0(不杀)。
|
||
* - 清:删顶置「一键清除后台」SUB_ACT 动作行,改为顶栏标题栏右侧 action 按钮(top_action)。
|
||
*/
|
||
#include "meow_launcher_internal.h"
|
||
#include "meow_bgplay.h"
|
||
#include "meow_bgmgr.h"
|
||
#include <ctype.h>
|
||
|
||
/* 行内杀按钮尺寸(圆心/圆角定位用;命中与绘制共用同一几何,保证所见即所点) */
|
||
#define BGM_BTN_W 46
|
||
#define BGM_BTN_H 34
|
||
|
||
/* 圆角卡列表项外边距(item margin/gap):相邻卡片间可见空隙 px。SDK listview 行距(=SUB_ITEM_H)
|
||
* 不变——卡片在其行高内上下对称内缩 BGM_CARD_GAP/2,故行内内容(按行高垂直居中)仍落卡中心;
|
||
* 相邻卡顶间距恒=SUB_ITEM_H=卡高+BGM_CARD_GAP。要求偶数,保证上下对称。 */
|
||
#define BGM_CARD_GAP 8
|
||
static inline int bgm_row_card_y(int row_top) { return row_top + BGM_CARD_GAP / 2; }
|
||
static inline int bgm_row_card_h(int h) { return h - BGM_CARD_GAP; }
|
||
|
||
typedef struct {
|
||
struct app *apps;
|
||
int napps;
|
||
bgmgr_row_t rows[MEOW_BGMR_ROW_MAX];
|
||
int n;
|
||
/* 行副文本存储:sub_row_t.sub 被 SDK 行池/自绘在整页会话内持续读取,
|
||
* 不能指向 bgm_rows 的栈帧(曾用函数内 char sub[80] → 悬垂,页面副标题为乱码)。 */
|
||
char subs[MEOW_BGMR_ROW_MAX][80];
|
||
} bgm_ctx_t;
|
||
|
||
/* 找 struct app(launcher 应用表)by id */
|
||
static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id)
|
||
{
|
||
if (!apps || !app_id) return NULL;
|
||
for (int i = 0; i < napps; i++)
|
||
if (apps[i].id[0] && !strcmp(apps[i].id, app_id)) return &apps[i];
|
||
return NULL;
|
||
}
|
||
|
||
/* ---- meow_bgmgr ops 注入(UI 侧真实实现)---- */
|
||
static int bgm_app_meta(void *ctx, const char *app_id, pid_t pid,
|
||
char *name, int nl, char *icon, int il,
|
||
char *protected_app, int *is_fg)
|
||
{
|
||
(void)pid;
|
||
bgm_ctx_t *b = ctx;
|
||
struct app *a = bgm_app_find(b->apps, b->napps, app_id);
|
||
*protected_app = 0;
|
||
*is_fg = 0;
|
||
if (!a) return -1; /* 未注册 → name 回退 app_id */
|
||
if (a->name[0]) snprintf(name, nl, "%s", a->name);
|
||
if (icon && il > 0) snprintf(icon, il, "%s", a->icon[0] ? a->icon : "");
|
||
/* 刻意**不**用 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;
|
||
}
|
||
|
||
static int bgm_enumerate(void *ctx, bgmgr_row_t *rows, int max)
|
||
{
|
||
(void)ctx;
|
||
/* 逐槽扫描并**压缩**(g_bg 可能有空洞),跳过已发出终止的条目:
|
||
* meow_bg_terminate 只置 term_at_ms 发 SIGTERM,真正的 reap/摘表在 meow_bg_tick(launcher
|
||
* 主循环),它在本页 meow_subpage_run 阻塞期间**不跑**——若不在此过滤,kill/清后旧行会一直
|
||
* 残留(pid 已死/0 CPU),页面上看就是「点不动」。过滤后 kill/一键清当下即从列表消失(可见反馈)。 */
|
||
int k = 0;
|
||
for (int i = 0; i < MEOW_BG_MAX && k < max; i++) {
|
||
const meow_bg_entry_t *e = meow_bg_at(i);
|
||
if (!e || !e->used || !e->app_id[0]) continue;
|
||
if (e->term_at_ms) continue; /* 已进入终止序列(kill/宽限回收/卡拔)→ 行不再显示 */
|
||
memset(&rows[k], 0, sizeof rows[k]);
|
||
snprintf(rows[k].app_id, sizeof rows[k].app_id, "%s", e->app_id);
|
||
rows[k].pid = e->pid;
|
||
rows[k].keepalive = (e->keepalive ? 1 : 0);
|
||
rows[k].on_card = (e->on_card ? 1 : 0);
|
||
rows[k].bg_since_ms = e->bg_since_ms;
|
||
k++;
|
||
}
|
||
return k;
|
||
}
|
||
|
||
/* proc_sample:/proc/<pid>/stat(utime+stime 差分) + /proc/<pid>/status(VmRSS)。
|
||
* 与 meow_svc_perf 897-983 每进程 pcpu/rss 聚合同口径(同 /proc 源)。 */
|
||
static long g_cpu_pid[MEOW_BG_MAX] = {0};
|
||
static unsigned long long g_cpu_tick[MEOW_BG_MAX] = {0};
|
||
|
||
static int bgm_proc_sample(void *ctx, pid_t pid, double *pcpu, long *rss_kb)
|
||
{
|
||
(void)ctx;
|
||
if (pid <= 0) return -1;
|
||
char path[64], line[512];
|
||
long rss = 0;
|
||
snprintf(path, sizeof path, "/proc/%d/status", pid);
|
||
int fd = open(path, O_RDONLY);
|
||
if (fd >= 0) {
|
||
ssize_t nr = read(fd, line, sizeof line - 1); close(fd);
|
||
if (nr > 0) { line[nr] = 0;
|
||
char *p = strstr(line, "VmRSS:");
|
||
if (p) p = strchr(p, ' ');
|
||
if (p) { while (*p == ' ' || *p == '\t') p++; rss = atol(p); }
|
||
}
|
||
}
|
||
double pc = 0.0;
|
||
snprintf(path, sizeof path, "/proc/%d/stat", pid);
|
||
fd = open(path, O_RDONLY);
|
||
if (fd >= 0) {
|
||
ssize_t nr = read(fd, line, sizeof line - 1); close(fd);
|
||
if (nr > 0) {
|
||
line[nr] = 0;
|
||
char *rp = strrchr(line, ')');
|
||
if (rp) {
|
||
char *p = rp + 2;
|
||
unsigned long long ut[2] = {0, 0};
|
||
for (int f = 0; f < 14; f++) {
|
||
while (*p == ' ') p++;
|
||
char *end; unsigned long long v = strtoull(p, &end, 10);
|
||
if (end == p) { p++; continue; }
|
||
p = end;
|
||
if (f == 11) ut[0] = v; /* utime */
|
||
if (f == 12) ut[1] = v; /* stime */
|
||
}
|
||
unsigned long long cur = ut[0] + ut[1];
|
||
static double hz = 0.0;
|
||
if (hz <= 0) { hz = (double)sysconf(_SC_CLK_TCK); if (hz <= 0) hz = 100.0; }
|
||
unsigned long long prev = 0; int had = 0, slot = -1;
|
||
for (int i = 0; i < MEOW_BG_MAX; i++) {
|
||
if (g_cpu_pid[i] == pid) { prev = g_cpu_tick[i]; had = 1; slot = i; break; }
|
||
}
|
||
if (!had) {
|
||
for (int i = 0; i < MEOW_BG_MAX; i++) if (g_cpu_pid[i] == 0) { slot = i; break; }
|
||
if (slot < 0) slot = 0;
|
||
}
|
||
if (slot >= 0) { g_cpu_pid[slot] = pid; g_cpu_tick[slot] = cur; }
|
||
if (had) { pc = 100.0 * (double)(cur - prev) / hz; if (pc < 0) pc = 0; }
|
||
}
|
||
}
|
||
}
|
||
*pcpu = pc;
|
||
*rss_kb = rss;
|
||
return 0;
|
||
}
|
||
|
||
/* ---- rows(填 sub_row_t 列表;每后台 App 一行,空态一行)---- */
|
||
static int bgm_rows(void *ctx, sub_row_t *rbuf, int max)
|
||
{
|
||
bgm_ctx_t *b = ctx;
|
||
bgmgr_ops_t ops;
|
||
memset(&ops, 0, sizeof ops);
|
||
ops.ctx = b;
|
||
ops.enumerate = bgm_enumerate;
|
||
ops.app_meta = bgm_app_meta;
|
||
ops.proc_sample = bgm_proc_sample;
|
||
b->n = bgmgr_collect(&ops, b->rows, MEOW_BGMR_ROW_MAX);
|
||
|
||
int r = 0;
|
||
if (b->n == 0) {
|
||
if (r < max) {
|
||
rbuf[r].kind = SUB_ROW;
|
||
rbuf[r].label = meow_str_zh("bgmgr.empty", "无后台 App");
|
||
rbuf[r].sub = NULL;
|
||
rbuf[r].act = -1;
|
||
r++;
|
||
}
|
||
return r;
|
||
}
|
||
for (int i = 0; i < b->n && r < max; i++) {
|
||
/* 副文本写进 ctx 的持久缓冲(b->subs):sub_row_t.sub 会被 SDK 行池在整页会话内反复读取,
|
||
* 指向本函数栈帧的 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]));
|
||
rbuf[r].kind = SUB_ROW;
|
||
rbuf[r].label = b->rows[i].name[0] ? b->rows[i].name : b->rows[i].app_id;
|
||
rbuf[r].sub = b->subs[i];
|
||
rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */
|
||
r++;
|
||
}
|
||
return r;
|
||
}
|
||
|
||
/* kill 通道(bgmgr_kill_all 的函数指针签名:void(*)(const char*)) */
|
||
static void bgm_kill_fn(const char *app_id) { meow_bg_terminate(app_id); }
|
||
|
||
/* ---- 行内杀按钮矩形(绘制与命中共用;行视口矩形 rx,ry,rw,rh → 右侧 46×34 圆角钮)----
|
||
* 卡在行高内上下对称内缩 BGM_CARD_GAP/2(行中心=卡中心);按钮垂直居中于卡内,故其中心
|
||
* =卡中心=行中心。命中矩形仍为整行距(row_tap 传整行视口),按钮几何不变 → 卡内缩后命中不破。 */
|
||
static void bgm_btn_rect(int rx, int ry, int rw, int rh, int *bx, int *by, int *bw, int *bh)
|
||
{
|
||
*bw = BGM_BTN_W;
|
||
*bh = BGM_BTN_H;
|
||
*bx = rx + rw - 12 - BGM_BTN_W;
|
||
*by = bgm_row_card_y(ry) + (bgm_row_card_h(rh) - BGM_BTN_H) / 2;
|
||
}
|
||
|
||
/* ---- 行完全自绘(sub_page_t.row_draw;覆盖默认 renderer)----
|
||
* 空态行(act=-1)居中画「无后台 App」;后台 App 行画 名字 + CPU/内存/状态 + 行右「杀」按钮。 */
|
||
static void bgm_row_draw(void *ctx, int idx, sub_row_t *r,
|
||
int x, int y, int w, int h, int state)
|
||
{
|
||
(void)idx;
|
||
bgm_ctx_t *b = ctx;
|
||
const meow_theme *mt = meow_theme_get();
|
||
int i = r->act;
|
||
|
||
/* MD3 圆角卡:行高内上下对称内缩 BGM_CARD_GAP/2(行中心=卡中心),替代扁平行+底部分隔线。 */
|
||
int cy = bgm_row_card_y(y);
|
||
int ch = bgm_row_card_h(h);
|
||
meow_gfx_fill_round(x, cy, w, ch, 12, mt->surfaceContainer);
|
||
if (state & MEOW_LISTVIEW_ST_PRESSED)
|
||
meow_gfx_fill_round_alpha(x, cy, w, ch, 12, mt->primary, 26); /* 按压态(实时,限卡内) */
|
||
|
||
if (i < 0 || i >= b->n) { /* 空态行 */
|
||
int th = meow_ui_text_height(18);
|
||
meow_ui_text_center(x + w / 2, cy + (ch - th) / 2,
|
||
r->label ? r->label : meow_str_zh("bgmgr.empty", "无后台 App"),
|
||
18, mt->onSurfaceVariant);
|
||
return;
|
||
}
|
||
int th = meow_ui_text_height(20), tsub = meow_ui_text_height(16);
|
||
int rx = x + 16; /* 左内边距 */
|
||
int ry = cy + (ch - (th + tsub + 4)) / 2; /* 内容在卡内垂直居中(行中心=卡中心) */
|
||
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);
|
||
/* 行内杀按钮(全形=胶囊(bh/2);「杀」;error=红=杀)——P1 按钮胶囊化。
|
||
* 不可杀行(受保护/前台/无进程,bgmgr_can_kill=0)→ 置灰禁点外观,避免误导(按钮可点但不可杀)。 */
|
||
int bx, by, bw, bh; bgm_btn_rect(x, y, w, h, &bx, &by, &bw, &bh);
|
||
uint32_t btn_col = mt->error; /* 可杀:error 红 */
|
||
if (i >= 0 && i < b->n && !b->rows[i].killable)
|
||
btn_col = mt->onSurfaceVariant; /* 禁点:置灰(onSurfaceVariant) */
|
||
meow_ui_draw_rect_round(bx, by, bw, bh, bh / 2, btn_col);
|
||
int bth = meow_ui_text_height(16);
|
||
if (bth > bh) bth = bh;
|
||
meow_ui_text_center(bx + bw / 2, by + (bh - bth) / 2, "杀", 16, btn_col);
|
||
}
|
||
|
||
/* ---- 行内自定义命中(sub_page_t.row_tap;仅按钮区 kill,行体不响应/不误杀)---- */
|
||
static int bgm_row_tap(void *ctx, int idx, sub_row_t *r, int x, int y,
|
||
int rx, int ry, int rw, int rh)
|
||
{
|
||
(void)idx;
|
||
bgm_ctx_t *b = ctx;
|
||
int i = r->act;
|
||
if (i < 0 || i >= b->n) return 0; /* 空态行无按钮 */
|
||
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 (!bgmgr_can_kill(&b->rows[i])) {
|
||
/* 禁点:受保护/前台/无进程 → 按钮已置灰,不 kill(返回 1=重绘)。
|
||
* 留一行日志:「按了没反应」当年因为没有这行日志,只能靠翻 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;
|
||
}
|
||
|
||
/* ---- 行体点击(sub_page_t.detail):不响应(kill 只认行内按钮)---- */
|
||
static int bgm_detail(void *ctx, int idx, int act)
|
||
{
|
||
(void)ctx; (void)idx; (void)act;
|
||
return 0;
|
||
}
|
||
|
||
/* ---- 顶栏右侧「一键清除后台」按钮(sub_page_t.top_action_cb)---- */
|
||
static int bgm_top_action(void *ctx)
|
||
{
|
||
bgm_ctx_t *b = ctx;
|
||
bgmgr_ops_t ops;
|
||
memset(&ops, 0, sizeof ops);
|
||
ops.ctx = b;
|
||
ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */
|
||
/* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤
|
||
* term_at_ms 让清后的行当下消失(可见反馈;不可杀行本就在上/一键不杀)。 */
|
||
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; /* 重绘:重扫行(已清行消失) */
|
||
}
|
||
|
||
/* ---- 入口:运行后台管理页(阻塞到返回)---- */
|
||
int run_bg_page(struct app *apps, int napps)
|
||
{
|
||
static bgm_ctx_t b;
|
||
memset(&b, 0, sizeof b);
|
||
b.apps = apps;
|
||
b.napps = napps;
|
||
sub_page_t p;
|
||
memset(&p, 0, sizeof p);
|
||
p.title = "后台管理";
|
||
p.rows = bgm_rows;
|
||
p.detail = bgm_detail;
|
||
p.live = 1; /* 后台表/进程变化实时重扫 */
|
||
p.ctx = &b;
|
||
p.top_action = "一键清除后台"; /* 顶栏标题栏右侧按钮 */
|
||
p.top_action_cb = bgm_top_action;
|
||
p.row_draw = bgm_row_draw; /* 行自绘(名字/CPU/内存/状态 + 行右杀按钮) */
|
||
p.row_tap = bgm_row_tap; /* 行内自定义命中(仅按钮区 kill) */
|
||
meow_subpage_run(&p);
|
||
|
||
/* 清除 CPU 差分缓存(页面会话结束) */
|
||
memset(g_cpu_pid, 0, sizeof g_cpu_pid);
|
||
memset(g_cpu_tick, 0, sizeof g_cpu_tick);
|
||
return 0;
|
||
}
|