From 6b52740ff1810a6ba7c1d4e7791370120e79c515 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 4 Nov 2026 20:42:49 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=90=8E=E5=8F=B0=E9=A1=B5=E6=9D=80?= =?UTF-8?q?=E6=8C=89=E9=92=AE=E7=82=B9=E4=B8=8D=E5=8A=A8=20=E2=80=94=20app?= =?UTF-8?q?.json=20framework=3D1=20=E8=A2=AB=E8=AF=AF=E5=BD=93=E3=80=8C?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E6=9D=80=E3=80=8D=E9=97=A8=E6=8E=A7=20+=20am?= =?UTF-8?q?=20kill=20=E6=97=A0=E5=89=8D=E5=8F=B0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E4=B8=8D=E5=8F=AF=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因(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 重定义),其余文件干净。 --- patch_v2/meow_amsd/meow_amsd.c | 71 ++++++++++++++++++----- patch_v2/meow_launcherd/bg_page.c | 43 +++++++++----- patch_v2/meow_launcherd/meow_bgmgr.c | 6 +- patch_v2/meow_launcherd/meow_bgmgr.h | 17 ++++-- patch_v2/meow_launcherd/meow_bgplay.c | 9 ++- patch_v2/meow_launcherd/meow_bgplay.h | 3 +- patch_v2/meow_launcherd/meow_ipc.h | 7 ++- patch_v2/meow_launcherd/test_bgmgr.c | 41 ++++++++----- patch_v2/meow_launcherd/test_bgpage.c | 83 ++++++++++++++++++++------- patch_v2/meow_launcherd/test_bgplay.c | 37 +++++++++++- patch_v2/meow_launcherd/util.c | 16 ++++-- 11 files changed, 249 insertions(+), 84 deletions(-) diff --git a/patch_v2/meow_amsd/meow_amsd.c b/patch_v2/meow_amsd/meow_amsd.c index 774f133..2891d82 100644 --- a/patch_v2/meow_amsd/meow_amsd.c +++ b/patch_v2/meow_amsd/meow_amsd.c @@ -784,11 +784,35 @@ static void out_append(const char *pkg, pid_t pid, long epoch, long up, const ch } /* ---- 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, uint8_t *rbuf, uint32_t rcap) { + g_ipc_err = IPC_ERR_NONE; 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 的阻塞 * read 会永远挂起,冻结整个单线程守护。设 SO_SNDTIMEO/SO_RCVTIMEO 让单次转发 * 有界,超时即返回失败 → 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); a.sun_family = AF_UNIX; 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]; uint32_t magic = MEOW_IPC_MAGIC, rmagic, rlen; 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; while (off < sizeof hdr) { 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; } if (len && payload) { size_t p = 0; while (p < len) { 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; } } off = 0; while (off < sizeof hdr) { 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; } memcpy(&rmagic, hdr, 4); memcpy(&rver, hdr + 4, 2); 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; off = 0; while (off < rlen) { 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; } 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; 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。 */ if (attempt + 1 < AMS_IPC_RETRY) 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; } @@ -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`),非二补。 */ snprintf(detail, sizeof detail, "busy (app running)"); 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 { - snprintf(detail, sizeof detail, "launcher offline"); - emit_log("AMS start %s -> launcher offline", pkg); + /* rt!=0 只代表「这次没拿到回执」,不等于 launcher 死了(同 cmd_kill 注释)。 + * 响应串保留 "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", pkg, act[0] ? act : "(default)"); @@ -1251,14 +1285,21 @@ static int cmd_kill(const char *pkg, char *resp, int rn) uint32_t code = 0; int rt = ams_request_ctl(MSG_AMS_KILL, &k, sizeof k, &code); 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); - 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) { - 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) { - emit_log("AMS kill %s -> launcher refused code=%u", pkg, code); - return snprintf(resp, rn, "ERR launcher refused: %s (code=%u)\n", pkg, code); + /* launcher 明确拒绝:不改判为直杀(pid 可能已被回收,盲杀会打错进程),如实回错。 + * 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()); int m = snprintf(resp, rn, "OK terminate %s\n", pkg); diff --git a/patch_v2/meow_launcherd/bg_page.c b/patch_v2/meow_launcherd/bg_page.c index b94bf8c..f96a3e1 100644 --- a/patch_v2/meow_launcherd/bg_page.c +++ b/patch_v2/meow_launcherd/bg_page.c @@ -2,7 +2,7 @@ * * 容器: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 门控)。 + * 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;行体不响应)。 @@ -38,6 +38,9 @@ typedef struct { 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 */ @@ -52,17 +55,20 @@ static struct app *bgm_app_find(struct app *apps, int napps, const char *app_id) /* ---- 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 *framework, int *is_fg) + 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); - *framework = 0; - *is_fg = 0; + *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 : ""); - *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; } @@ -174,12 +180,13 @@ static int bgm_rows(void *ctx, sub_row_t *rbuf, int max) return r; } for (int i = 0; i < b->n && r < max; i++) { - char sub[80]; - snprintf(sub, sizeof sub, "CPU %.1f%% · %s · %s", b->rows[i].pcpu, + /* 副文本写进 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 = sub; + rbuf[r].sub = b->subs[i]; rbuf[r].act = i; /* row_tap/row_draw 用 i 定位行 */ 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->sub) meow_ui_text(rx, ry + th + 4, r->sub, 16, mt->onSurfaceVariant); /* 行内杀按钮(全形=胶囊(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); uint32_t btn_col = mt->error; /* 可杀:error 红 */ 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; /* 空态行无按钮 */ 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])) - return 1; /* 禁点:不可杀(框架/前台/无进程)→ 按钮已置灰,不 kill(返回 1=重绘) */ - meow_bg_terminate(b->rows[i].app_id); /* 置 term_at_ms → bgm_enumerate 下一跳行消失(可见反馈) */ + 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; } @@ -274,7 +289,9 @@ static int bgm_top_action(void *ctx) ops.enumerate = bgm_enumerate; /* 行已由 bgm_rows 建 */ /* kill 只置 term_at_ms(真退/摘表在 launcher 主循环 bgm_tick),此处由 bgm_enumerate 过滤 * 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; /* 重绘:重扫行(已清行消失) */ } diff --git a/patch_v2/meow_launcherd/meow_bgmgr.c b/patch_v2/meow_launcherd/meow_bgmgr.c index e2f71dc..5b92e66 100644 --- a/patch_v2/meow_launcherd/meow_bgmgr.c +++ b/patch_v2/meow_launcherd/meow_bgmgr.c @@ -13,7 +13,7 @@ int bgmgr_collect(const bgmgr_ops_t *ops, bgmgr_row_t *rows, int max) if (n > max) n = max; for (int i = 0; i < n; i++) { bgmgr_row_t *r = &rows[i]; - r->framework = 0; + r->protected_app = 0; r->is_fg = 0; r->name[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; ops->app_meta(ops->ctx, r->app_id, r->pid, 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 (ops->proc_sample && ops->proc_sample(ops->ctx, r->pid, &r->pcpu, &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) { 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->pid <= 0) return 0; /* 非后台/无进程 */ return 1; diff --git a/patch_v2/meow_launcherd/meow_bgmgr.h b/patch_v2/meow_launcherd/meow_bgmgr.h index a24c6ef..a06c689 100644 --- a/patch_v2/meow_launcherd/meow_bgmgr.h +++ b/patch_v2/meow_launcherd/meow_bgmgr.h @@ -4,7 +4,10 @@ * 便于宿主单测(test_bgmgr.c)与 launcher 页面(bg_page.c)复用。同 meow_bgplay 的 ops 注入手法: * 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。 */ #ifndef MEOW_BGMGR_H @@ -23,7 +26,9 @@ typedef struct bgmgr_row { char app_id[MEOW_APP_ID_MAX]; char name[49]; /* display name(回退 app_id) */ 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 后台进程 */ int keepalive; /* 1=声明常驻(background_keepalive,无限期不清) */ int is_fg; /* 1=当前前台 App(不 kill,标注「前台」) */ @@ -39,10 +44,10 @@ typedef struct bgmgr_ops { /* 枚举后台常驻集:每调一次填 rows 的 app_id/pid/keepalive/on_card/bg_since_ms, * 返回行数(0..max;<0 视 0)。UI=读 launcher meow_bg 表;单测=假表。 */ 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, 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)。 */ int (*proc_sample)(void *ctx, pid_t pid, double *pcpu, long *rss_kb); void (*log)(const char *fmt, ...); /* 可空=静默 */ @@ -53,11 +58,11 @@ typedef struct bgmgr_ops { * 行序=enumerate 返回序(UI 端期望 bg_since_ms 升序=最久在前)。 */ 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); /* 一键清:遍历 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, void (*kill_fn)(const char *app_id)); diff --git a/patch_v2/meow_launcherd/meow_bgplay.c b/patch_v2/meow_launcherd/meow_bgplay.c index f242cb0..d4e165d 100644 --- a/patch_v2/meow_launcherd/meow_bgplay.c +++ b/patch_v2/meow_launcherd/meow_bgplay.c @@ -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→摘表), - * 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。 */ + * 与宽限超时/卡拔回收同一条终止通道,**不在此处直接 reap**(保持单一写者 tick 收口)。 + * 调用者:bg_page.c 行内「杀」/「一键清除」+ util.c MSG_AMS_KILL(`am kill`,无前台会话时) + * ——两个入口同一通道,故页面按钮与面板/CLI kill 行为对称。 */ int meow_bg_terminate(const char *app_id) { 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]; if (e->used && e->pid > 0 && !strcmp(e->app_id, app_id)) { 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); e->term_at_ms = bg_now_ms(); } diff --git a/patch_v2/meow_launcherd/meow_bgplay.h b/patch_v2/meow_launcherd/meow_bgplay.h index 82ef954..8fbe582 100644 --- a/patch_v2/meow_launcherd/meow_bgplay.h +++ b/patch_v2/meow_launcherd/meow_bgplay.h @@ -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); /* 该 App 是否在后台保活。 */ 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→摘表。 * 返回 0=已受理(在后台表且 pid>0);MEOW_ERR_NOENT=不在后台/无进程;MEOW_ERR_INVAL=app_id 空。 */ int meow_bg_terminate(const char *app_id); diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index b263b7a..bbf3574 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -93,7 +93,8 @@ enum { /* ---- amsd 控制(AMSD_DESIGN §4.2):amsd→launcher,申请拉起/终止某 App ---- * 语义:amsd 为「启动/终止」决策者 → launcher(执行器)执行。payload 见下。 */ 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 ---- * 语义:App 只经 SDK 客户端 API(meow_ui_audio.c)喂 PCM,audiod 独占 hw:0,0。 * 独占+抢占(后开赢);软混音已否决(MIPS32 软 SRC 伤音质,留混音器接口位)。 */ @@ -463,7 +464,9 @@ typedef struct m_ams_start { char picker_caller[MEOW_APP_ID_MAX]; /* 32:发起方 app_id(""=系统/CLI) */ } 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 { char app_id[MEOW_APP_ID_MAX]; /* 32 */ } m_ams_kill_t; /* 32 B */ diff --git a/patch_v2/meow_launcherd/test_bgmgr.c b/patch_v2/meow_launcherd/test_bgmgr.c index 0ebfe44..e9f262b 100644 --- a/patch_v2/meow_launcherd/test_bgmgr.c +++ b/patch_v2/meow_launcherd/test_bgmgr.c @@ -1,11 +1,12 @@ /* 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。 - * 2) bgmgr_can_kill 门控矩阵:framework=1 → 0(**框架 App 不 kill**);is_fg=1 → 0(当前前台不 kill); - * pid<=0 → 0(非后台/无进程);普通后台(非框架/非前台/pid>0)→ 1。 - * 3) bgmgr_kill_all 一键清:框架 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。 + * 2) bgmgr_can_kill 门控矩阵:protected_app=1 → 0(**受保护 App 不 kill**);is_fg=1 → 0(当前前台不 kill); + * pid<=0 → 0(非后台/无进程);普通后台(非受保护/非前台/pid>0)→ 1。 + * 3) bgmgr_kill_all 一键清:受保护 App 不杀、当前前台不杀、非后台不杀;其余逐一 kill;返回 kill 数。 + * 3b) 回归:app.json framework=1(SDK 框架 App 标志,现有 App 全写 1)**不影响**可杀性。 * 4) bgmgr_status_str / bgmgr_mem_str 文案。 * 只依赖 meow_bgmgr.c + meow_ipc.h(纯 C);宿主 gcc 直编: * gcc -Wall -Wextra -std=gnu99 test_bgmgr.c meow_bgmgr.c -o test_bgmgr @@ -17,7 +18,9 @@ #include "meow_bgmgr.h" static int g_fails = 0; +static int g_checks = 0; #define CHECK(cond, msg) do { \ + g_checks++; \ if (cond) printf("PASS: %s\n", msg); \ 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; if (max < 4) return 0; - /* 4 个后台行:hello=普通、reader=常驻、sysguard=framework、fgapp=当前前台 */ + /* 4 个后台行:hello=普通、reader=常驻、sysguard=受保护、fgapp=当前前台 */ memset(&rows[0], 0, sizeof rows[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; @@ -52,10 +55,10 @@ static int fake_enumerate(void *ctx, bgmgr_row_t *rows, int max) 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, char *name, int nl, char *icon, int il, - char *framework, int *is_fg) + char *protected_app, int *is_fg) { (void)ctx; if (!strcmp(app_id, "os.meow.hello")) snprintf(name, nl, "播放"); @@ -64,8 +67,8 @@ 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 return -1; /* 未注册 → name 回退 app_id */ snprintf(icon, il, "/tmp/icon_%s.png", app_id); - *framework = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0; - *is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */ + *protected_app = (!strcmp(app_id, "os.meow.sysguard")) ? '1' : 0; + *is_fg = (pid == 1004) ? 1 : 0; /* fgapp 标前台 */ return 0; } @@ -112,9 +115,9 @@ int main(void) printf("=== 1) collect ===\n"); CHECK(n == 4, "collect returns 4 rows"); 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(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[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"); @@ -132,7 +135,7 @@ int main(void) } 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[0]) == 1, "normal 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.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_bg == 1, "kill_all killed a real bg app"); 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"); CHECK(!strcmp(bgmgr_status_str(&rows[1]), "常驻"), "status_str keepalive=常驻"); 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(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); return g_fails ? 1 : 0; } diff --git a/patch_v2/meow_launcherd/test_bgpage.c b/patch_v2/meow_launcherd/test_bgpage.c index 9399e70..2b842f3 100644 --- a/patch_v2/meow_launcherd/test_bgpage.c +++ b/patch_v2/meow_launcherd/test_bgpage.c @@ -1,14 +1,16 @@ /* test_bgpage.c — 批③ 后台管理页 UI 按钮化 宿主子测(bg_page.c 按钮命中/顶栏清除命中)。 * * 通过 #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 定位); - * P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**;框架 App - * 按钮点击 → 不 terminate(!framework 门控);空态行不响应; - * P4 bgm_top_action 顶栏清除:kill_all **不杀框架 App**(sysguard 不 terminate),杀普通后台; - * P5 门控矩阵:bgmgr_can_kill(framework/fg/pid<=0/普通)。 + * P3 bgm_row_tap 行内命中:**仅按钮区 kill(bgm_terminate 一次),行体/按钮外不误杀**; + * **app.json framework=1(SDK 框架 App)照样可杀**(2026-11-04 真机回归); + * 受保护/前台行按钮点击 → 不 terminate(门控,留诊断日志);空态行不响应; + * 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 剔除。 * * 编译(在 patch_v2/meow_launcherd 下): @@ -44,6 +46,16 @@ int meow_bg_terminate(const char *app_id) } unsigned long long now_ms(void) { return 0; } 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 用) ---------- */ 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_checks = 0; #define CHECK(cond, msg) do { \ + g_checks++; \ if (cond) printf("PASS: %s\n", msg); \ else { printf("FAIL: %s\n", msg); g_fails++; } } while (0) @@ -86,21 +100,31 @@ int main(void) { static bgm_ctx_t 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(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]; - printf("=== P1) bgm_rows 行骨架 ===\n"); - bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002); + printf("=== P1) bgm_rows 行骨架 + 副文本持久化 ===\n"); + bg_reset(2, "os.meow.hello", 1001, "dev.player.ncm", 7520); int rn = bgm_rows(&b, rows, SUB_ROWS_MAX); CHECK(rn == 2, "2 后台 App → 2 行(无空态/无清除动作行)"); CHECK(rows[0].kind == SUB_ROW && rows[0].act == 0, "row0 act=0"); CHECK(!strcmp(rows[0].label, "播放"), "row0 label=名字(播放)"); CHECK(rows[1].kind == SUB_ROW && rows[1].act == 1, "row1 act=1"); - CHECK(!strcmp(rows[1].label, "系统守护"), "row1 label=系统守护"); - CHECK(!strcmp(rows[1].sub, "") || rows[1].sub[0] != '\0', "row1 sub 非空(CPU/内存/状态)"); + CHECK(!strcmp(rows[1].label, "流媒体客户端"), "row1 label=流媒体客户端"); + CHECK(rows[1].sub != NULL && !strncmp(rows[1].sub, "CPU ", 4), "row1 sub=CPU/内存/状态"); 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"); { @@ -111,7 +135,7 @@ int main(void) } 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); /* 按钮区命中:行(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(重绘)"); @@ -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], 475, 28, 0, 0, 480, 56) == 0, "按钮右侧(475)→不杀"); CHECK(bgm_row_tap(&b, 0, &rows[0], 445, 60, 0, 0, 480, 56) == 0, "按钮下方(60)→不杀"); - /* 框架 App:按钮命中但不 terminate(!framework 门控) */ - CHECK(bgm_row_tap(&b, 1, &rows[1], 445, 28, 0, 0, 480, 56) == 1, "框架App按钮→返回1(不可杀提示)"); - CHECK(term_count_app("os.meow.sysguard") == 0, "框架App按钮点击→不 terminate"); + /* 回归(2026-11-04 真机「杀后台按钮按下去没反应」):dev.player.ncm 的 app.json 写 framework:1 + * (= 用 SDK 框架,现有 App 全写 1),**必须照杀**——旧实现把它当受保护位,按钮置灰点不动。 */ + 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"); 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(不杀)"); g_term_calls = 0; - printf("=== P4) 顶栏清除(一键清除后台)不杀框架/前台 ===\n"); + printf("=== P4) 顶栏清除(一键清除后台)不杀受保护/前台 ===\n"); bg_reset(2, "os.meow.hello", 1001, "os.meow.sysguard", 1002); bgm_rows(&b, rows, SUB_ROWS_MAX); + b.rows[1].protected_app = '1'; CHECK(bgm_top_action(&b) == 1, "顶栏清除按钮:返回 1(重绘约定)"); 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 标前台再清 */ bg_reset(1, "os.meow.hello", 1001, NULL, 0); bgm_rows(&b, rows, SUB_ROWS_MAX); @@ -153,12 +194,12 @@ int main(void) { bgmgr_row_t r; memset(&r, 0, sizeof r); r.pid = 5; CHECK(bgmgr_can_kill(&r) == 1, "普通后台可杀"); - r.framework = '1'; CHECK(bgmgr_can_kill(&r) == 0, "framework 不可杀"); - r.framework = 0; r.is_fg = 1; CHECK(bgmgr_can_kill(&r) == 0, "前台不可杀"); + r.protected_app = '1'; CHECK(bgmgr_can_kill(&r) == 0, "protected_app 不可杀"); + 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 不可杀"); } - 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); return g_fails ? 1 : 0; } diff --git a/patch_v2/meow_launcherd/test_bgplay.c b/patch_v2/meow_launcherd/test_bgplay.c index 6ffe659..044edbc 100644 --- a/patch_v2/meow_launcherd/test_bgplay.c +++ b/patch_v2/meow_launcherd/test_bgplay.c @@ -7,6 +7,8 @@ * 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 @@ -138,11 +140,17 @@ static int cond_svc_gone(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" +/* 服务测试用长命可执行: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); @@ -271,6 +279,31 @@ int main(void) "卡拔 → 卡上后台 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]; diff --git a/patch_v2/meow_launcherd/util.c b/patch_v2/meow_launcherd/util.c index a3f00e7..635b24d 100644 --- a/patch_v2/meow_launcherd/util.c +++ b/patch_v2/meow_launcherd/util.c @@ -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); k.app_id[MEOW_APP_ID_MAX - 1] = 0; 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); a.code = MEOW_OK; } 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); - 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; } /* AMSD_DESIGN §6.2 二期:picker 跨 App 调用(App A → launcher → amsd 路由)。