Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/app_launcher.c
T
AskaEth 2826c1e905 feat: M6.40批 — 阅读器UI改版(书架list·grid+空态骨架+界面设置+App设置页+header·footer+背景黑默认) + 系统级音量翻页映射 + P1 MD3(select面板28/对话框按钮胶囊) + MeowPanel写端点护栏 + SDK公开仓·文档同步
- reader: app.c/shelf.c/reader.c 全套UI; 新增 reader_conf(reader.conf)/settings(App设置页)/reader_ipc(音量翻页IPC响应); app.json加volume_page
- launcher: keys.c meow_volpage_*门 + util.c app_id→fd映射(HELLO记fd/断开清) + app_launcher.c 前台三处接线(进入sink/按键路径volpage_key/离开clear) → 前台声明volume_page的App音量键→MSG_KEY_PAGEUP/PAGEDOWN翻页(非阅读器前台音量照旧)
- sdk: meow_select.c 底部面板28 + meow_ui_dialog.c 对话框按钮胶囊(h/2) + test_meow_ui_select.c修采样
- meowpanel: /api/net/policy·/api/power/policy 加防自锁+policy默认关两条护栏(panel.policy_enabled默认0)
- 文档: MEOW_APP_SDK.md §12.6 + meowpanel README端点表

0warning;宿主测试全过;二进制留批量批次未入库
2026-09-04 18:13:18 +08:00

2353 lines
119 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* app_launcher.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
#include "meow_ams_start.h" /* MSG_AMS_START pending/拉起决策(meow_ams_start.c) */
#include "overlay_gate.h" /* SIGSTOP 叠加会话关闭顺序/死亡回退纯决策(§310 三连 bug 收敛点) */
#include "meow_utf8.h" /* UTF-8 整序列有界拷贝(显示名不劈多字节字符,问题2) */
static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */
int g_naccts = 0;
volatile int g_app_session_active = 0; /* §10.5:1=launch_app waitpid 前台会话中(util.c 读) */
/* 显示名 UTF-8 整序列有界拷贝(dst 容量 cap 含 NUL)。
* M6.3「name[49]+json_str 整序列」只把 json 读入局部 name[49] 与 a->name[49] 放大了,**漏改
* strncpy(a->name, name, 16)**——仍把显示名硬截 16 字节,UTF-8 下「流媒体客户端」(18B) 变
* 「流媒体客户」+ 半个「端」(16B = 5 汉字 + 首字节),真机主界面应用名截成 5 字(用户 M6.3 实测)。
* 本助手(= meow_utf8_copy_bounded)snprintf 到 cap 后回退掉被截断的半截多字节字符。 */
static void app_name_copy(char *dst, int cap, const char *src)
{
meow_utf8_copy_bounded(dst, cap, src);
}
/* ---- amsd 上报 / 控制挂点(AMSD_DESIGN §4):best-effort,amsd 不在不影响 launcher ---- */
#define MEOW_AMS_SOCK_DEFAULT "/usr/data/meow_ams.sock"
/* amsd socket 路径:运行时读 env MEOW_AMS_SOCK(宿主/qemu 覆写),缺省用默认。 */
static const char *ams_sock_path(void)
{
static char p[108];
static int once = 0;
if (!once) {
const char *e = getenv("MEOW_AMS_SOCK");
snprintf(p, sizeof p, "%s", (e && e[0]) ? e : MEOW_AMS_SOCK_DEFAULT);
once = 1;
}
return p;
}
/* up 秒(/proc/uptime 首字段)。失败 0。 */
static long ams_uptime(void)
{
FILE *f = fopen("/proc/uptime", "r");
if (!f) return 0;
char b[64];
long up = 0;
if (fgets(b, sizeof b, f)) up = (long)atof(b);
fclose(f);
return up;
}
/* 向 amsd 发一条多行负载:非阻塞 connect + 单次 write + close;失败静默丢。 */
static void ams_send(const char *payload, int len)
{
if (!payload || len <= 0) return;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, ams_sock_path(), sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) {
if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
struct pollfd pf;
pf.fd = fd; pf.events = POLLOUT; pf.revents = 0;
int r = poll(&pf, 1, 150);
if (r <= 0 || !(pf.revents & POLLOUT)) { close(fd); return; }
} else { close(fd); return; }
}
ssize_t w = write(fd, payload, (size_t)len);
(void)w;
close(fd);
}
/* 公开 amsd 上报通道包装(AMSD_DESIGN §6.2 二期):util.c 转发运行时 declare / picker
* call/result 给 amsd 用。语义与 ams_send 一致:best-effort、单次 connect+write+close、
* 失败静默丢(amsd 不在不影响 launcher)。 */
void meow_ams_send(const char *payload, int len)
{
ams_send(payload, len);
}
/* launch 上报:pkg/activity/chain/pid。amsd 建表项 + 开 stdout 会话。 */
static void ams_report_launch(const struct app *a, const char *chain, pid_t pid)
{
if (!a) return;
char act[MEOW_ACT_MAX + 1] = "";
char ch[MEOW_CHAIN_MAX + 1] = "-";
if (chain && chain[0]) {
const char *top = strrchr(chain, ';');
top = top ? top + 1 : chain;
snprintf(act, sizeof act, "%s", top);
snprintf(ch, sizeof ch, "%s", chain);
}
char line[512];
int l = snprintf(line, sizeof line, "EV LAUNCH %s %s %s %d %ld %ld\n",
a->id, act, ch, (int)pid, (long)time(NULL), ams_uptime());
if (l > 0) ams_send(line, l);
}
/* exit 上报:kind(exit|sig)/code/dur_ms。异常退出由 amsd 判 crash + 转发 meow_crash。
* _id 变体供 M4 后台保活回收路径用(彼时 struct app 可能已不在手边)。 */
static void ams_report_exit_id(const char *app_id, int status, long long dur_ms)
{
if (!app_id || !app_id[0]) return;
const char *kind;
int code;
if (WIFSIGNALED(status)) { kind = "sig"; code = WTERMSIG(status); }
else { kind = "exit"; code = WIFEXITED(status) ? WEXITSTATUS(status) : 1; }
char line[256];
int l = snprintf(line, sizeof line, "EV EXIT %s %s %d %lld %ld %ld\n",
app_id, kind, code, dur_ms, (long)time(NULL), ams_uptime());
if (l > 0) ams_send(line, l);
}
static void ams_report_exit(const struct app *a, int status, long long dur_ms)
{
if (!a) return;
ams_report_exit_id(a->id, status, dur_ms);
}
/* 批①后台常驻上报(单向 best-effort):转后台 `EV BG <app> <pid>`、回前台 `EV FG <app>`。
* amsd 收到后置 AMS_BG/AMS_FG(批③后台管理页元数据来源);旧 launcher 不发、amsd 不识别也不崩。 */
static void ams_report_bg(const char *app_id, pid_t pid)
{
if (!app_id || !app_id[0]) return;
char line[256];
int l = snprintf(line, sizeof line, "EV BG %s %d\n", app_id, (int)pid);
if (l > 0) ams_send(line, l);
}
static void ams_report_fg(const char *app_id)
{
if (!app_id || !app_id[0]) return;
char line[256];
int l = snprintf(line, sizeof line, "EV FG %s\n", app_id);
if (l > 0) ams_send(line, l);
}
/* ---- stdout/stderr 管道捕获(AMSD_DESIGN §5):前台 waitpid 循环排空 + 按行转发 ---- */
static char g_ams_out_line[1024]; /* 未完结行缓冲(无 \n) */
static int g_ams_out_n = 0;
static char g_ams_out_flush[4096]; /* 已完结、待转发行 */
static int g_ams_out_flush_n = 0;
static int g_ams_out_had = 0; /* 本次迭代是否排空到数据(驱动短睡) */
static void ams_out_flush(void)
{
if (g_ams_out_flush_n > 0) {
ams_send(g_ams_out_flush, g_ams_out_flush_n);
g_ams_out_flush_n = 0;
}
}
/* 追加一行(line 为无 \n 的 NUL 串)到 flush 缓冲;满了先 flush。 */
static void ams_out_line(const char *app_id, pid_t pid, const char *line)
{
if (!app_id || !line) return;
int need = snprintf(NULL, 0, "EV OUT %s %d %ld %ld %s\n",
app_id, (int)pid, (long)time(NULL), ams_uptime(), line);
if (g_ams_out_flush_n + need + 1 > (int)sizeof g_ams_out_flush) ams_out_flush();
if (need < (int)sizeof g_ams_out_flush - 1 - g_ams_out_flush_n)
g_ams_out_flush_n += snprintf(g_ams_out_flush + g_ams_out_flush_n,
sizeof g_ams_out_flush - g_ams_out_flush_n,
"EV OUT %s %d %ld %ld %s\n",
app_id, (int)pid, (long)time(NULL), ams_uptime(), line);
}
/* 单次排空上限:App 被 OOM/信号杀后,其子孙进程可能仍继承持有 out_pipe 写端
* (fd1/fd2),并继续向管道写数据(读端永远不会 EOF)。此时若排空不设限会一直
* 忙转(主循环卡在 ams_drain_stdout,UI 假死)。设上界保证「无论孤儿怎么写,
* 一次排空最多处理 64KB」即放行,主循环依旧能回主界面。 */
#define AMS_DRAIN_MAX (64 * 1024)
/* exec 状态确认超时:fork 后 child 到 execve 的等待上界。超过即视为 exec 正常进行,
* 放行到 waitpid 循环(见 launch_app_chain)。有界,杜绝裸阻塞 read 的 pipe_wait 死锁。 */
#define EXEC_CONFIRM_MS 2000
/* 排空读端尽可能多的字节,按行切分转发;超长行截断标记。有界:单次调用最多
* 读 AMS_DRAIN_MAX 字节(见上)。读端已 O_NONBLOCK;App 死且写端全关 → read 返 0=EOF。
* linebuf/line_n:未完结行缓冲外提——前台会话用全局 g_ams_out_line/g_ams_out_n;
* M4 后台保活 drain(meow_bg_housekeep)用独立缓冲,防与前台未完结行串扰。 */
static void ams_drain_stdout(const char *app_id, int fd, pid_t pid,
char *linebuf, int *line_n)
{
char tmp[2048];
ssize_t n;
size_t drained = 0;
while ((n = read(fd, tmp, sizeof tmp)) > 0) {
g_ams_out_had = 1;
drained += (size_t)n;
for (ssize_t i = 0; i < n; i++) {
char c = tmp[i];
if (c == '\n') {
linebuf[*line_n] = '\0';
ams_out_line(app_id, pid, linebuf);
*line_n = 0;
} else if (c == '\r') {
/* 去尾 \r */
} else if (*line_n < 1024 - 1) {
linebuf[(*line_n)++] = c;
} else {
/* 超长行:截断标记 + 重置(当前字符丢弃) */
linebuf[1024 - 2] = '\0';
ams_out_line(app_id, pid, linebuf);
ams_out_line(app_id, pid, "[...trunc]");
*line_n = 0;
}
}
if (drained >= AMS_DRAIN_MAX) break; /* 有界:孤儿持续写也不卡死主循环 */
}
}
/* waitpid 兜底:SIGKILL 后回收孩子。绝不裸阻塞 waitpid(pid, …, 0)——孩子若因 I/O 卡在
* D-state(不可杀不可收)会无限阻塞 → launcher 卡死。改为 WNOHANG 轮询 + 上界,
* 超时即放弃(孩子交给 init/subreaper 稍后收割,launcher 无条件回主界面)。
* 返回 1=已回收。超时上限 REAP_WAIT_MS。 */
#define REAP_WAIT_MS 2000
static int reap_wait(pid_t pid, int *status)
{
unsigned long long t0 = now_ms();
for (;;) {
int r = waitpid(pid, status, WNOHANG);
if (r == pid) return 1;
if (r < 0 && errno != EINTR) return 0;
if (now_ms() - t0 >= REAP_WAIT_MS) return 0;
usleep(5000);
}
}
/* ---- amsd kill 挂点(主循环单写者;start 侧见 meow_ams_start.c)---- */
static struct { char pkg[MEOW_APP_ID_MAX]; int pending; } g_ams_kill;
void meow_ams_pending_kill_set(const char *pkg)
{
if (!pkg || !pkg[0]) return;
snprintf(g_ams_kill.pkg, sizeof g_ams_kill.pkg, "%s", pkg);
g_ams_kill.pending = 1;
}
int meow_ams_kill_pending(const char *pkg)
{
if (!g_ams_kill.pending) return 0;
if (pkg && strcmp(g_ams_kill.pkg, pkg) != 0) return 0;
g_ams_kill.pending = 0;
g_ams_kill.pkg[0] = 0;
return 1;
}
/* ---- amsd-start ops 组装(launch_app/launch_activity + resolve_chain + logmsg)----
* meow_ams_start_consume 经 ops.launch 复用既有启动链路真正拉起 App(chain 空=裸启动、
* 非空=activity 链)。ops.launch 的 idx→struct app 映射经 g_ams_ctx 完成(run_menu 每次
* 消费前 meow_ams_ctx_set(apps, napps))。 */
static struct app *g_ams_ctx_apps;
static int g_ams_ctx_napps;
void meow_ams_ctx_set(struct app *apps, int napps)
{
g_ams_ctx_apps = apps;
g_ams_ctx_napps = napps;
}
static int ams_launch_cb(int idx, const char *chain)
{
if (!g_ams_ctx_apps || idx < 0 || idx >= g_ams_ctx_napps) return -MEOW_ERR_NOENT;
if (chain && chain[0]) return launch_activity(&g_ams_ctx_apps[idx], chain);
return launch_app(&g_ams_ctx_apps[idx]);
}
static int ams_resolve_cb(const char *app_id, const char *activity, char *chain, int *depth)
{
return resolve_chain(app_id, activity, chain, depth);
}
void meow_ams_launcher_init(void)
{
static struct meow_ams_ops ops;
ops.launch = ams_launch_cb;
ops.resolve = ams_resolve_cb;
ops.log = logmsg;
meow_ams_ops_init(&ops);
}
/* 子进程 exec 前注入 picker env(action/req/caller 已由 launch_app_chain 入口捕获并解除全局武装,
* 即使启动失败不再错配到下一次普通启动)。返回注入后的 ne。 */
static int ams_push_picker_env(char **envp, int ne, const char *action,
uint32_t req, const char *caller)
{
if (!action || !action[0]) return ne;
/* envp 容量 48:此处 +3 仍在界内(见 launch_app_chain envp[48] 注释) */
static char e_act[40], e_req[40], e_caller[MEOW_APP_ID_MAX + 20];
snprintf(e_act, sizeof e_act, "MEOW_PICKER_ACTION=%s", action);
snprintf(e_req, sizeof e_req, "MEOW_PICKER_REQ_ID=%u", req);
snprintf(e_caller, sizeof e_caller, "MEOW_PICKER_CALLER=%s", caller ? caller : "");
envp[ne++] = e_act;
envp[ne++] = e_req;
envp[ne++] = e_caller;
logmsg("launch: picker env action=%s req=%u caller=%s", action, req, caller ? caller : "");
return ne;
}
/* ---- 阶段 D · M1:路由注册表辅助(主循环单写者,无锁)---- */
struct meow_acct *meow_acct_find(const char *app_id)
{
if (!app_id) return NULL;
for (int i = 0; i < g_naccts; i++)
if (!strcmp(g_accts[i].app_id, app_id)) return &g_accts[i];
return NULL;
}
/* 取(或在末尾新建)app_id 对应的注册表项。返回 NULL 当满。 */
static struct meow_acct *meow_acct_upsert(const char *app_id)
{
if (!app_id || !app_id[0]) return NULL;
struct meow_acct *a = meow_acct_find(app_id);
if (a) return a;
if (g_naccts >= MEOW_MAX_ACTS) { logmsg("act: registry full, drop %s", app_id); return NULL; }
a = &g_accts[g_naccts++];
memset(a, 0, sizeof *a);
strncpy(a->app_id, app_id, MEOW_APP_ID_MAX - 1);
return a;
}
/* 由 scan_apps 调用:把 app.json 静态路由整体同步进注册表(扫描即权威,替换)。 */
static void meow_acct_sync(const char *app_id, const struct app *ap)
{
if (!ap) return;
struct meow_acct *a = meow_acct_upsert(app_id);
if (!a) return;
a->framework = ap->framework;
a->background = ap->background; /* M4 */
a->background_audio = ap->background_audio;
a->background_keepalive = ap->background_keepalive; /* 批① */
strncpy(a->icon, ap->icon, 128);
app_name_copy(a->name, (int)sizeof a->name, ap->name);
strncpy(a->exec, ap->exec, 128);
a->ok = ap->ok;
a->nroute = ap->nroute > MEOW_MAX_ROUTES ? MEOW_MAX_ROUTES : ap->nroute;
for (int i = 0; i < a->nroute; i++) a->routes[i] = ap->routes[i];
a->nsvc = ap->nsvc > MEOW_MAX_SVCS ? MEOW_MAX_SVCS : ap->nsvc;
for (int i = 0; i < a->nsvc; i++) a->svcs[i] = ap->svcs[i];
}
/* 运行时 HELLO:声明 app 存在(补入注册表),记录声明源。 */
void meow_acct_hello(const char *app_id, uint32_t version, uint32_t flags)
{
struct meow_acct *a = meow_acct_upsert(app_id);
if (a) { a->declared = 1; (void)version; (void)flags; }
logmsg("act: hello app=%s ver=%u flags=%u", app_id, version, flags);
}
/* 运行时 ACT_DECLARE:并集覆盖静态路由。entries=count 个 NUL 结尾 "id=chain" 串。 */
void meow_acct_declare(const char *app_id, const char *entries, int count)
{
struct meow_acct *a = meow_acct_upsert(app_id);
if (!a || !entries || count <= 0) return;
a->declared = 1;
const char *p = entries;
for (int k = 0; k < count && *p; k++) {
const char *eq = strchr(p, '=');
if (eq && eq > p) {
int idlen = (int)(eq - p);
if (idlen >= 16) idlen = 15;
char id[16];
memcpy(id, p, idlen); id[idlen] = 0;
const char *chain = eq + 1;
int ri = -1;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, id)) { ri = i; break; }
if (ri < 0) {
if (a->nroute >= MEOW_MAX_ROUTES) { p += strlen(p) + 1; continue; }
ri = a->nroute++;
}
strncpy(a->routes[ri].act, id, 15);
strncpy(a->routes[ri].chain, chain, MEOW_CHAIN_MAX - 1);
}
p += strlen(p) + 1;
}
}
/* ---- M4 后台保活 / 无头后台服务胶水(meow_bgplay ops 注入实现)----
* 挂载:meow_bgplay_launcher_init() 由 util.c meow_ipc_init 末尾调一次;
* meow_bg_housekeep() 挂 meow_ipc_process 末尾(主循环单写者排空点,四处循环都经它)。 */
static void notify_app_crash(const char *pkg, int status); /* 定义见下(MEOW_CRASH_SOCK) */
/* 查静态声明的 service.<svc>(app.json 解析进 meow_acct.svcs);无返回 NULL。 */
const struct meow_svc *meow_acct_svc_find(const struct meow_acct *ac, const char *svc)
{
if (!ac || !svc || !svc[0]) return NULL;
for (int i = 0; i < ac->nsvc; i++)
if (!strcmp(ac->svcs[i].name, svc)) return &ac->svcs[i];
return NULL;
}
/* 后台权限位:bit0=background(可注册无头服务)bit1=background_audio(后台音频)。 */
int meow_acct_background_perm(const struct meow_acct *ac)
{
if (!ac) return 0;
return (ac->background == '1' ? 1 : 0) | (ac->background_audio == '1' ? 2 : 0);
}
/* M4 服务可执行路径约定:App 二进制同目录下的 <svc> 名文件
* (app.json `service.<svc>=...` 只声明策略,二进制位置由此约定派生)。 */
void meow_svc_exec_path(const char *app_exec, const char *svc, char *out, int olen)
{
char dir[144];
char *sl;
snprintf(dir, sizeof(dir), "%s", app_exec ? app_exec : "");
sl = strrchr(dir, '/');
if (sl) sl[1] = 0;
else dir[0] = 0;
snprintf(out, (size_t)olen, "%s%s", dir, svc ? svc : "");
}
/* 后台保活 drain 的独立行缓冲(防与前台会话的 g_ams_out_line 未完结行串扰,一 App 一组) */
static char g_bg_out_line[MEOW_BG_MAX][1024];
static int g_bg_out_n[MEOW_BG_MAX];
/* meow_bgplay on_bg_dead:后台保活 App 进程退出(崩溃照收/停播后自退/卡拔 SIGTERM 走完)。 */
static void bg_on_dead(const char *app_id, pid_t pid, int status)
{
(void)pid;
ams_report_exit_id(app_id, status, 0); /* dur 未知(保活起点未记),记 0 */
if (WIFSIGNALED(status) ||
(WIFEXITED(status) && WEXITSTATUS(status) != 0))
notify_app_crash(app_id, status); /* 崩溃照收(SIGTERM=卡拔/关机回收也算信号,
* meow_crash 侧自行判重;与前台同口径) */
meow_media_clear(app_id); /* 媒体会话随源死清槽 */
/* §10.5 自抑标记:仅当「再无后台保活 App 且无通知中心叠加会话」才清——
* 多 bg App 共存时标记粒度为全局(v1 定案:叠加会话与后台保活共用同一标记)。 */
if (meow_bg_count() == 0 && g_notif_ctrl_pid <= 0)
unlink(NOTIF_ACTIVE);
}
/* meow_bgplay notif_clear_app:熔断时清该 App 全部活跃通知(先收集 id 再 dismiss,
* 防遍历时改表)。返回清除条数。 */
struct bg_clear_ctx { uint32_t ids[16]; int n; };
static int bg_notif_collect_cb(const meow_notif_t *nt, void *ctx)
{
struct bg_clear_ctx *c = ctx;
if (c->n < 16) c->ids[c->n++] = nt->id;
return c->n >= 16; /* 集满即中止遍历 */
}
static int bg_notif_clear_app(const char *app_id)
{
struct bg_clear_ctx c;
memset(&c, 0, sizeof(c));
meow_notif_list(app_id, bg_notif_collect_cb, &c);
for (int i = 0; i < c.n; i++) meow_notif_dismiss(c.ids[i]);
return c.n;
}
/* meow_bgplay send_frame:熔断广播投递前判 IPC 连接存活(注册方可能已断开)。 */
static size_t bg_send_frame(int fd, uint16_t type, uint32_t req_id,
const void *payload, uint32_t len)
{
if (!meow_ipc_conn_alive(fd)) return 0;
return meow_ipc_send_pub(fd, type, req_id, payload, len);
}
void meow_bgplay_launcher_init(void)
{
meow_bgplay_ops ops;
memset(&ops, 0, sizeof(ops));
ops.log = logmsg;
ops.now_ms = NULL; /* 内部 CLOCK_MONOTONIC */
ops.card_present = sd_device_present;
ops.on_bg_dead = bg_on_dead;
ops.media_resident = meow_media_app_resident; /* 批①+宽限:媒体活跃判定(宽限回收/槽满LRU) */
ops.on_svc_dead = NULL; /* 服务消亡日志 meow_bgplay 内已记 */
ops.notif_clear_app = bg_notif_clear_app;
ops.send_frame = bg_send_frame;
ops.log_dir = log_dir[0] ? log_dir : NULL;
meow_bgplay_init(&ops);
}
/* 后台表周期维护(meow_ipc_process 末尾调):drain 各 bg App stdout(防写满管道阻塞
* 播放线程)+ 回收/卡拔 tick + 服务退避重启 tick。 */
void meow_bg_housekeep(void)
{
for (int i = 0; i < MEOW_BG_MAX; i++) {
const meow_bg_entry_t *e = meow_bg_at(i);
if (e && e->out_fd >= 0)
ams_drain_stdout(e->app_id, e->out_fd, e->pid,
g_bg_out_line[i], &g_bg_out_n[i]);
}
meow_bg_tick();
meow_svc_tick();
}
/* 链路深度 = ';' 分隔段数(根屏→目标屏的逻辑链长)。 */
int chain_depth(const char *c)
{
if (!c || !*c) return 0;
int d = 1;
for (; *c; c++) if (*c == ';') d++;
return d;
}
/* 解析 activity 链路:填 out_chain(root..target)与 depth。
* 返回 0=成功;MEOW_ERR_NOENT=app 未注册 / activity 未注册。
* 规则(FRAMEWORK_DESIGN §2):route 缺失(app 未声明任何路由)→ chain=activity、深度 1 兜底;
* 声明了路由但 activity 非 route 键 → MEOW_ERR_NOENT(activity 未注册)。 */
int resolve_chain(const char *app_id, const char *activity, char *out, int *depth)
{
if (!out || !depth) return MEOW_ERR_INVAL;
struct meow_acct *a = meow_acct_find(app_id);
if (!a) return MEOW_ERR_NOENT;
if (a->nroute == 0) {
strncpy(out, activity, MEOW_CHAIN_MAX - 1);
*depth = 1;
return MEOW_OK;
}
for (int i = 0; i < a->nroute; i++) {
if (!strcmp(a->routes[i].act, activity)) {
strncpy(out, a->routes[i].chain, MEOW_CHAIN_MAX - 1);
*depth = chain_depth(a->routes[i].chain);
return MEOW_OK;
}
}
return MEOW_ERR_NOENT;
}
/* ---- M3 导航栈(FRAMEWORK_DESIGN §3):每 app ≤16 深,v1 同一时刻单前台应用 ----
* launcher 侧镜像:launch_* 冷启动时以链路深度初始化;App 经 MSG_NAV{REPORT,depth,current}
* 上报栈深变化(launcher 更新);MSG_NAV{EXIT} → 置退出请求,前台 waitpid 循环终止 app 回主屏。
* 单一写者=主循环(meow_ipc_process 排空处 / launch_* 前台循环),无需锁。 */
static struct {
char app_id[MEOW_APP_ID_MAX];
int depth; /* 0=无/未上报;1..MEOW_NAV_MAX_DEPTH */
char top[MEOW_ACT_MAX]; /* 当前顶层 activity_id(REPORT 上报) */
int active; /* 1=前台应用有导航栈 */
} g_nav;
static volatile int g_nav_exit_req = 0;
void meow_nav_begin(const char *app_id, const char *chain)
{
if (!app_id) return;
memset(&g_nav, 0, sizeof g_nav);
strncpy(g_nav.app_id, app_id, MEOW_APP_ID_MAX - 1);
g_nav.app_id[MEOW_APP_ID_MAX - 1] = 0;
if (chain && chain[0]) {
g_nav.depth = chain_depth(chain);
if (g_nav.depth > MEOW_NAV_MAX_DEPTH) g_nav.depth = MEOW_NAV_MAX_DEPTH;
const char *top = strrchr(chain, ';');
top = top ? top + 1 : chain;
strncpy(g_nav.top, top, MEOW_ACT_MAX - 1);
g_nav.top[MEOW_ACT_MAX - 1] = 0;
}
g_nav.active = 1;
logmsg("nav: begin app=%s chain=\"%s\" depth=%d top=%s",
app_id, chain ? chain : "(bare)", g_nav.depth, g_nav.top);
}
void meow_nav_report(const char *app_id, uint32_t depth, const char *current)
{
if (!app_id) return;
if (depth == 0 || depth > MEOW_NAV_MAX_DEPTH) { meow_nav_exit(); return; }
g_nav.active = 1;
strncpy(g_nav.app_id, app_id, MEOW_APP_ID_MAX - 1);
g_nav.app_id[MEOW_APP_ID_MAX - 1] = 0;
g_nav.depth = (int)depth;
if (current) { strncpy(g_nav.top, current, MEOW_ACT_MAX - 1); g_nav.top[MEOW_ACT_MAX - 1] = 0; }
logmsg("nav: report app=%s depth=%d top=%s", app_id, g_nav.depth, g_nav.top);
}
void meow_nav_exit(void)
{
g_nav.active = 0;
g_nav.depth = 0;
g_nav.top[0] = 0;
logmsg("nav: exit (cleared)");
}
void meow_nav_request_exit(void)
{
g_nav_exit_req = 1;
meow_nav_exit();
logmsg("nav: EXIT requested");
}
int meow_nav_exit_pending(void)
{
int r = g_nav_exit_req;
g_nav_exit_req = 0;
return r;
}
/* ---- M2.17 keep_awake:前台当前顶层 activity 追踪 ----
* 前台应用经 IPC MSG_ACT_FOCUS/BACK 上报其当前顶层 activity;fg_keep_awake() 依注册表判断该
* activity 是否声明 keep_awake(app.json awake.<id>=1 / route.<id>.keep_awake=1;运行时同类)。
* 主界面(无上报/空)= 非 keep_awake(主页空闲照常超时息屏)。单一写者=主循环(IPC 排空处)。 */
static char g_fg_app[MEOW_APP_ID_MAX] = "";
static char g_fg_act[MEOW_ACT_MAX] = "";
void meow_fg_act_set(const char *app_id, const char *act)
{
if (!app_id) return;
strncpy(g_fg_app, app_id, MEOW_APP_ID_MAX - 1);
g_fg_app[MEOW_APP_ID_MAX - 1] = 0;
if (act) { strncpy(g_fg_act, act, MEOW_ACT_MAX - 1); g_fg_act[MEOW_ACT_MAX - 1] = 0; }
else g_fg_act[0] = 0;
logmsg("keep_awake: fg_act app=%s act=%s", g_fg_app, g_fg_act);
}
void meow_fg_act_back(void)
{
g_fg_app[0] = 0;
g_fg_act[0] = 0;
logmsg("keep_awake: fg_act back to home (cleared)");
}
/* 前台当前 activity 是否 keep_awake(无上报/未注册/未声明=0,即照常超时息屏)。 */
int fg_keep_awake(void)
{
if (!g_fg_app[0] || !g_fg_act[0]) return 0;
struct meow_acct *a = meow_acct_find(g_fg_app);
if (!a) return 0;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, g_fg_act)) return a->routes[i].keep_awake;
return 0;
}
/* 确保联网:wlan0 未 up 且尚无 wpa_supplicant 在跑 → 调用 /usr/bin/wifi_up.sh 连保存网络。
* 幂等:连上 / 已有 wpa_supplicant 则不动;每轮询重试,不阻塞主界面。
* M2.39:尊重 network.wifi_enabled —— 用户在设置页关 WiFi(=0)则不自动拉起(否则与
* 手动关冲突);取值失败(未 init)保默认 1=允许自动连网。 */
void ensure_wifi(void)
{
int enabled = 1;
if (meow_settings_get_bool("network.wifi_enabled", &enabled) == 0 && !enabled) return;
if (wlan_connected()) return;
if (proc_by_comm("wpa_supplicant")) return;
logmsg("wifi not up -> /usr/bin/wifi_up.sh");
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
}
/* 解析一个 app.json 流 → 填充 struct app 的 name/exec/icon/framework/routes/svcs。
* 极简:不做 JSON 校验,仅匹配 key 与路由/服务扁平行(JSON 或扁平皆可)。
* 不设 name/exec(由调用方从返回的 app 再复制),成功判定由调用方做。 */
void meow_parse_app_json(FILE *f, struct app *a)
{
char buf[256];
char name[49] = "", exec[129] = "", icon[129] = "";
char package[MEOW_APP_ID_MAX] = "";
int fram = 0, immersive = 0;
int fullscreen = 0; /* app.json fullscreen=<true|1>(注入 MEOW_FULLSCREEN env,SDK 预先设全屏) */
int fonts = 0, fonts_set = 0; /* app.json "fonts":<档>(0=默认4K常用 1=大字库GBK 2=Full;缺省 0) */
int background = 0; /* M4:app.json background=true(可注册无头后台服务) */
int bg_audio = 0; /* M4:app.json background_audio=true(播放中退出转后台保活) */
int bg_keepalive = 0; /* 批①:app.json background_keepalive=true(进入后台即保活,不依赖媒体会话) */
struct app tmp;
memset(&tmp, 0, sizeof tmp);
while (fgets(buf, sizeof buf, f)) {
if (!name[0]) json_str(buf, "name", name, sizeof name);
if (!exec[0]) json_str(buf, "exec", exec, sizeof exec);
if (!icon[0]) json_str(buf, "icon", icon, sizeof icon);
if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */
/* route.<id>=<chain>(activity 路由,M1) */
char *rp = strstr(buf, "route.");
if (rp && tmp.nroute < MEOW_MAX_ROUTES) {
rp += 6; /* 跳过 "route." */
int idl = 0; char id[32] = ""; /* 读满整键(识别 ".fullscreen" 后缀,cap 31) */
while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 31)
id[idl++] = *rp++;
id[idl] = 0;
if (idl > 0) {
char *vp = rp;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
/* route.<id>.fullscreen=<on>:活动级全屏声明(M1 顺带;M3 活动切换时消费)。
* 命中既有 route.<id> 置 fullscreen;不新建 route(避免 id 被截断误建 "…fulls")。 */
if (idl >= 11 && !strcmp(id + idl - 11, ".fullscreen")) {
id[idl - 11] = 0; /* 去掉 ".fullscreen" 还原 act */
char v[8] = ""; int k = 0;
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && k < 7) v[k++] = *vp++;
v[k] = 0;
int on = (!strcmp(v, "1") || !strcmp(v, "true") || !strcmp(v, "yes"));
for (int i = 0; i < tmp.nroute; i++)
if (!strcmp(tmp.routes[i].act, id)) { tmp.routes[i].fullscreen = on; break; }
} else {
int vl = 0; char chain[MEOW_CHAIN_MAX] = "";
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
chain[vl++] = *vp++;
chain[vl] = 0;
while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t'))
chain[--vl] = 0;
strncpy(tmp.routes[tmp.nroute].act, id, 15);
tmp.routes[tmp.nroute].act[15] = 0; /* 显式收尾(原依赖 id 读长 ≤15) */
strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1);
tmp.nroute++;
}
}
}
/* awake.<id>=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。
* 命中的既有 route.<id> 置 keep_awake;无 route 则新建单 activity 路由。 */
char *ap = strstr(buf, "awake.");
if (ap && tmp.nroute < MEOW_MAX_ROUTES) {
ap += 6; /* 跳过 "awake." */
int al = 0; char aid[16] = "";
while (*ap && *ap != '"' && *ap != ':' && *ap != '=' && al < 15)
aid[al++] = *ap++;
aid[al] = 0;
if (al > 0) {
char *vp = ap;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
int on = atoi(vp);
int ri = -1;
for (int i = 0; i < tmp.nroute; i++)
if (!strcmp(tmp.routes[i].act, aid)) { ri = i; break; }
if (ri < 0) {
ri = tmp.nroute++;
strncpy(tmp.routes[ri].act, aid, 15);
strncpy(tmp.routes[ri].chain, aid, MEOW_CHAIN_MAX - 1); /* 单 activity 兜底链路 */
}
tmp.routes[ri].keep_awake = (on != 0);
}
}
/* framework=true(看门狗返回发 SIGUSR1,M1 解析存储) */
if (!fram && strstr(buf, "framework")) {
char v[8] = "";
if (json_str(buf, "framework", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
fram = 1;
}
}
/* immersive=true(隐藏虚拟返回键、返回手势移交 SDK;写 /tmp/meowapp_state) */
if (!immersive && strstr(buf, "immersive")) {
char v[8] = "";
if (json_str(buf, "immersive", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
immersive = 1;
}
}
/* fullscreen=true(app 级:注入 MEOW_FULLSCREEN=1,SDK 端 meow_ui_init 预先设全屏)。
* 与 immersive 是两回事:fullscreen 隐藏状态栏/沉浸,immersive 关虚拟返回键。 */
if (!fullscreen && strstr(buf, "fullscreen")) {
char v[8] = "";
if (json_str(buf, "fullscreen", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
fullscreen = 1;
}
}
/* "fonts":<档>(字库档:0=默认4K常用 1=大字库GBK 2=Full;可选,缺省 0)。
* launcher 注入 MEOW_FONT_TIER;大字库装载由后续 font 代理实现。 */
if (!fonts_set && strstr(buf, "fonts")) {
char v[8] = "";
if (json_str(buf, "fonts", v, sizeof v) > 0) {
if (!strcmp(v, "common") || !strcmp(v, "0")) fonts = 0;
else if (!strcmp(v, "gbk") || !strcmp(v, "1")) fonts = 1;
else if (!strcmp(v, "full") || !strcmp(v, "2")) fonts = 2;
else fonts = atoi(v); /* 数字兜底(越界收敛到 [0,2]) */
if (fonts < 0) fonts = 0;
if (fonts > 2) fonts = 2;
fonts_set = 1;
}
}
/* "background":true(M4:可注册无头后台服务,MSG_REGISTER 权限位0)。
* 注意 "background" 是 "background_audio" 的前缀:反向误命中(本 key 匹配到
* background_audio 行)时 json_str 读出 "_audio…" ≠ true,判定自然为 0,无害。 */
if (!background && strstr(buf, "background")) {
char v[8] = "";
if (json_str(buf, "background", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
background = 1;
}
}
/* "background_audio":true(M4:播放中退出转后台保活,进程+音频流保留,§9) */
if (!bg_audio && strstr(buf, "background_audio")) {
char v[8] = "";
if (json_str(buf, "background_audio", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
bg_audio = 1;
}
}
/* "background_keepalive":true(批①:进入后台即保活,不依赖媒体会话=通用常驻)。
* 注意 "background_keepalive" 是 "background" 的前缀:若本行先命中上方 background 分支,
* json_str(buf,"background") 读出的是 "_keepalive…" ≠ true → 自然留 0,无害;
* json_str 键前缀独立性(util.c:544 ident_ch)保证 "background"/"background_audio" 独立匹配不受影响。 */
if (!bg_keepalive && strstr(buf, "background_keepalive")) {
char v[8] = "";
if (json_str(buf, "background_keepalive", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
bg_keepalive = 1;
}
}
/* service.<svc>=mode:restart:max:window_ms(M1 解析存储,保活 M4) */
char *sp = strstr(buf, "service.");
if (sp && tmp.nsvc < MEOW_MAX_SVCS) {
sp += 8; /* 跳过 "service." */
int sl = 0; char nm[16] = "";
while (*sp && *sp != '"' && *sp != ':' && *sp != '=' && sl < 15)
nm[sl++] = *sp++;
nm[sl] = 0;
if (sl > 0) {
char *vp = sp;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
int m = 0, r = 0, mx = 0, w = 0;
sscanf(vp, "%d:%d:%d:%d", &m, &r, &mx, &w);
strncpy(tmp.svcs[tmp.nsvc].name, nm, 15);
tmp.svcs[tmp.nsvc].mode = m;
tmp.svcs[tmp.nsvc].restart = r;
tmp.svcs[tmp.nsvc].max = mx;
tmp.svcs[tmp.nsvc].window_ms = w;
tmp.nsvc++;
}
}
/* depends=<name>[:<mod>][,...]:二进制依赖声明(meow_dep.c 解析)。
* 与行式解析同风格(JSON "depends":"..." 或扁平 depends=... 均可);不引 JSON 解析器。
* <mod> 缺省 required;:opt/:req 等价 optional/required;未知修饰符按 required 处理并 WARN。 */
char *dp = strstr(buf, "depends");
if (dp) {
char *after = dp + 7; /* strlen("depends") */
while (*after == ' ' || *after == '\t') after++;
/* 紧跟 '=' / ':' / '"' 才算 depends 键(防 description 等文本误命中) */
if ((*after == '=' || *after == ':' || *after == '"') &&
tmp.ndep < MEOW_MAX_DEPS) {
char *vp = after;
while (*vp && *vp != ':' && *vp != '=') vp++; /* 跳到值分隔符 */
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++; /* 剥 JSON 值开引号 */
int got = meow_parse_depends(vp, tmp.deps + tmp.ndep,
MEOW_MAX_DEPS - tmp.ndep);
tmp.ndep += got;
}
}
}
app_name_copy(a->name, (int)sizeof a->name, name);
strncpy(a->exec, exec, 128);
strncpy(a->package, package, sizeof a->package - 1);
a->framework = fram ? '1' : 0;
a->immersive = immersive ? '1' : 0;
a->fullscreen = fullscreen ? '1' : 0; /* 注入 MEOW_FULLSCREEN env */
a->fonts = (char)(fonts < 0 ? 0 : (fonts > 2 ? 2 : fonts)); /* 注入 MEOW_FONT_TIER env */
a->background = background ? '1' : 0; /* M4:后台服务注册权限 */
a->background_audio = bg_audio ? '1' : 0; /* M4:后台播放保活 */
a->background_keepalive = bg_keepalive ? '1' : 0; /* 批①:后台即保活(通用常驻) */
strncpy(a->icon, icon, 128);
a->nroute = tmp.nroute;
if (tmp.nroute) memcpy(a->routes, tmp.routes, sizeof(a->routes[0]) * (size_t)tmp.nroute);
a->nsvc = tmp.nsvc;
if (tmp.nsvc) memcpy(a->svcs, tmp.svcs, sizeof(a->svcs[0]) * (size_t)tmp.nsvc);
a->ndep = tmp.ndep;
if (tmp.ndep) memcpy(a->deps, tmp.deps, sizeof(a->deps[0]) * (size_t)tmp.ndep);
}
int scan_apps(struct app apps[MAX_APPS])
{
memset(apps, 0, sizeof(struct app) * MAX_APPS);
if (!card_present()) return -1; /* state 1 */
DIR *d = opendir(MEOWAPPS);
if (!d) return -2; /* state 2 */
int n = 0;
struct dirent *de;
while ((de = readdir(d)) && n < MAX_APPS) {
if (de->d_name[0] == '.') continue;
char jpath[192];
snprintf(jpath, sizeof jpath, "%s/%s/app.json", MEOWAPPS, de->d_name);
FILE *f = fopen(jpath, "r");
if (!f) continue;
struct app a0;
memset(&a0, 0, sizeof a0);
meow_parse_app_json(f, &a0);
fclose(f);
if (!a0.name[0] || !a0.exec[0]) continue; /* corrupt -> skip */
struct app *a = &apps[n];
memset(a, 0, sizeof *a);
memcpy(a, &a0, sizeof a0);
a->ok = (access(a->exec, X_OK) == 0);
/* 安卓化:app_id=app.json 的 package(权威),缺省回退目录名 de->d_name;
* 两者都存在且不同则 WARN(正常情况二者相等)。launch_app 注入 MEOW_LOG_APP 用 */
const char *aid = a0.package[0] ? a0.package : de->d_name;
strncpy(a->id, aid, sizeof a->id - 1);
if (a0.package[0] && strcmp(a0.package, de->d_name) != 0)
logmsg("WARN: app '%s' package '%s' != dir '%s'", a0.name, a0.package, de->d_name);
/* M1 路由注册表同步(app_id = a->id,即 package 优先) */
meow_acct_sync(a->id, a);
n++;
}
closedir(d);
return n; /* 0 -> state 2 */
}
/* ================================================================== */
/* hiby_player management */
/* ================================================================== */
int find_hiby_pid(void)
{
/* scan /proc/N/exe for /usr/bin/hiby_player */
DIR *d = opendir("/proc");
if (!d) return 0;
struct dirent *de;
int pid = 0;
while ((de = readdir(d))) {
if (de->d_name[0] < '0' || de->d_name[0] > '9') continue;
char path[64], exe[256];
snprintf(path, sizeof path, "/proc/%s/exe", de->d_name);
ssize_t n = readlink(path, exe, sizeof exe - 1);
if (n <= 0) continue;
exe[n] = 0;
if (!strcmp(exe, "/usr/bin/hiby_player")) { pid = atoi(de->d_name); break; }
}
closedir(d);
return pid;
}
/* ---- MeowOS 常驻主界面判定(M2.2 修复批次)----
* Launcher 被 MeowOS 拉起(bootmenu MeowOS 分支 exec,或热键切 MeowOS 后 exec)时,
* 播放器已死(find_hiby_pid()==0)。此时 launcher == MeowOS 主界面常驻:必须自绘
* fb0(画 buffer0 + FBIOPAN yoffset=0),绝不走旧「备份播放器帧/冻结/SIGCONT」路径,
* 绝不遗留/恢复播放器死帧。仅当存在存活播放器(会话覆盖场景)才保留旧菜单覆盖语义。 */
int meow_mode = 0;
int is_meow_mode(void)
{
return find_hiby_pid() <= 0;
}
int player_stopped = 0;
static int player_pid = 0;
static int proc_is_stopped(int pid)
{
char path[64], line[512];
snprintf(path, sizeof path, "/proc/%d/stat", pid);
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
ssize_t n = read(fd, line, sizeof line - 1);
close(fd);
if (n <= 0) return 0;
line[n] = 0;
/* comm may contain spaces; find last ')' */
char *p = strrchr(line, ')');
if (!p || p + 2 >= line + n) return 0;
return p[2] == 'T';
}
/* /proc/pid/stat 状态位 == 'Z'(僵尸):子进程已死、launcher 未回收。§10.5:App 前台叠加
* 会话期间据此「立即结束」通知中心面板(不回收,回收仍由 waitpid 循环统一做)。 */
int proc_is_zombie(int pid)
{
char path[64], line[512];
snprintf(path, sizeof path, "/proc/%d/stat", pid);
int fd = open(path, O_RDONLY);
if (fd < 0) return 0;
ssize_t n = read(fd, line, sizeof line - 1);
close(fd);
if (n <= 0) return 0;
line[n] = 0;
char *p = strrchr(line, ')');
if (!p || p + 2 >= line + n) return 0;
return p[2] == 'Z';
}
int stop_player(void)
{
player_pid = find_hiby_pid();
if (!player_pid) { logmsg("hiby_player not found, menu without stop"); return 0; }
if (kill(player_pid, SIGSTOP) < 0) {
logmsg("SIGSTOP %d failed: %s", player_pid, strerror(errno));
return -1;
}
/* wait for state T */
for (int i = 0; i < 100; i++) {
if (proc_is_stopped(player_pid)) break;
usleep(10000);
}
player_stopped = proc_is_stopped(player_pid);
logmsg("hiby_player pid=%d stopped=%d", player_pid, player_stopped);
return player_stopped ? 0 : -1;
}
void cont_player(void)
{
if (player_stopped && player_pid > 0) {
kill(player_pid, SIGCONT);
logmsg("SIGCONT %d", player_pid);
}
player_stopped = 0;
}
/* ================================================================== */
/* M2.12 MeowAPPs 包数据目录 + .mpk 接口 */
/* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */
/* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */
/* ================================================================== */
/* 由 exec 绝对路径取包名:/usr/data/mnt/sd_0/MeowAPPs/<pkg>/<binary> → <pkg> */
static int meow_pkg_name(const char *exec, char *out, int n)
{
if (!exec || strncmp(exec, MEOWAPPS, strlen(MEOWAPPS)) != 0) { if (n>0) out[0]=0; return -1; }
const char *p = exec + strlen(MEOWAPPS);
if (*p != '/') { if (n>0) out[0]=0; return -1; }
p++;
const char *end = strchr(p, '/');
int len = end ? (int)(end - p) : (int)strlen(p);
if (len <= 0 || len >= n) { if (n>0) out[0]=0; return -1; }
memcpy(out, p, len); out[len] = 0;
return 0;
}
/* 启动应用前准备数据目录:建 MeowData/<pkg>/{data,cache}(如缺)。幂等。
* 失败只 log(MeowData 所在卡不存在/只读),绝不崩 —— 零卡依赖:应用需按
* APPS.md 规范容忍无数据目录。返回 0=就绪,-1=pkg 不在卡上/不能判名。 */
static int meow_app_prepare(const char *exec)
{
char pkg[64];
if (meow_pkg_name(exec, pkg, sizeof pkg) != 0) return -1;
if (!sd_device_present()) return -1; /* 卡不在,跳过(应用仍可跑非卡资源) */
char cmd[320];
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
int rc = system(cmd);
logmsg("app_prepare: pkg=%s rc=%d", pkg, rc);
return rc == 0 ? 0 : -1;
}
/* ---- II 期签名校验 / 导出保留签名(MPK_SIGNATURE_DESIGN §5.1/§5.4,shell-out meow_verify) ---- */
/* meow_verify --system <mpk> --package <pkg>。返回 0=有效签 /1=有签不通过 /2=无签名 /127=工具不可用。 */
static int pkg_verify_sig(const char *mpk, const char *pkg)
{
const char *vb = getenv("MEOW_VERIFY_BIN");
if (!vb || !*vb) vb = "/usr/bin/meow_verify";
char cmd[700];
snprintf(cmd, sizeof cmd, "'%s' --system '%s' --package '%s' >/dev/null 2>&1", vb, mpk, pkg);
int rc = system(cmd);
if (rc == -1) return 127;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 127;
}
/* 读已解压 app.json 的某字符串字段(用 launcher 的 json_str;返回 0 成功)。 */
static int pkg_json_field(const char *appjs, const char *key, char *out, int n)
{
if (!appjs || !key || n <= 0) return -1;
FILE *f = fopen(appjs, "r");
if (!f) return -1;
static char j[16384];
size_t r = fread(j, 1, sizeof j - 1, f);
fclose(f);
j[r] = 0;
if (!r) return -1;
if (json_str(j, key, out, n) <= 0) { out[0] = 0; return -1; }
return 0;
}
/* 读 .mpk 内 app.json 的某字符串字段(gzip -dc | tar -xO app.json;返回 0 成功)。 */
static int pkg_json_field_mpk(const char *mpk, const char *key, char *out, int n)
{
char cmd[700];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -xO app.json 2>/dev/null", mpk);
FILE *pf = popen(cmd, "r");
if (!pf) return -1;
static char j[16384];
size_t r = fread(j, 1, sizeof j - 1, pf);
pclose(pf);
if (!r) return -1;
j[r] = 0;
if (json_str(j, key, out, n) <= 0) { out[0] = 0; return -1; }
return 0;
}
/* 安装成功后,把原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名)。 */
static void pkg_save_src(const char *mpk, const char *pkg)
{
char dst[320], cmd[720];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' 2>/dev/null && "
"cp '%s' '%s' 2>/dev/null", pkg, mpk, dst);
if (system(cmd) != 0) logmsg("pkg_install: save .mpk.src fail for %s", pkg);
}
/* 导出保留签名:缓存 .mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装 → 直接复制(不二次签名)。
* 成功 0;否则 -1(调用方现场重打包,无签名)。 */
static int pkg_export_preserve_sig(const char *pkg, const char *dst)
{
char srcp[320], instj[280], cmd[760], tmp[340];
snprintf(srcp, sizeof srcp, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
if (access(srcp, R_OK) != 0) return -1;
if (pkg_verify_sig(srcp, pkg) != 0) { logmsg("pkg_export: cached .mpk.src sig fail %s", pkg); return -1; }
char cv[64], iv[64];
snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(instj, "version", iv, sizeof iv) != 0) return -1;
if (pkg_json_field_mpk(srcp, "version", cv, sizeof cv) != 0) return -1;
if (strcmp(iv, cv) != 0) { logmsg("pkg_export: cached version %s != installed %s", cv, iv); return -1; }
snprintf(tmp, sizeof tmp, "%s.tmp", dst);
snprintf(cmd, sizeof cmd, "cp '%s' '%s' 2>/dev/null", srcp, tmp);
if (system(cmd) != 0 || access(tmp, R_OK) != 0) return -1;
if (rename(tmp, dst) != 0) { unlink(tmp); return -1; }
return 0;
}
/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs/<pkg>/。
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{data,cache})。
* 返回 0 ok / 负=错误。 */
int meow_pkg_install(const char *mpk_path)
{
if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; }
if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; }
if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; }
/* 包名 = 文件名茎(去掉 .mpk) */
const char *bn = strrchr(mpk_path, '/');
bn = bn ? bn + 1 : mpk_path;
const char *dot = strrchr(bn, '.');
char pkg[64];
if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need <pkg>.mpk"); return -1; }
int len = (int)(dot - bn);
if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; }
memcpy(pkg, bn, len); pkg[len] = 0;
/* 目标 MeowAPPs/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 app.json */
char dst[256], bak[256], cmd[760];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid());
int replacing = (access(dst, F_OK) == 0);
/* II 期:签名分级先于解包(MPK_SIGNATURE_DESIGN §5.2:先验签再解包,失败不落盘)。
* 有签名必须验签(失败拒装);os.meow.* 必须有有效签名;无签名第三方放行(告警)。 */
int vr = pkg_verify_sig(mpk_path, pkg);
int sysp = (strncmp(pkg, "os.meow.", 8) == 0);
if (vr == 1) { logmsg("pkg_install: sig invalid, reject %s", pkg); return -3; }
else if (vr == 2) {
if (sysp) { logmsg("pkg_install: sys pkg %s must be signed, rejected", pkg); return -3; }
logmsg("pkg_install: %s unsigned (3rd party allowed)", pkg);
}
else if (vr != 0) {
if (sysp) { logmsg("pkg_install: meow_verify unavailable rc=%d, sys pkg reject", vr); return -3; }
logmsg("pkg_install: meow_verify unavailable rc=%d, %s unverified (3rd party)", vr, pkg);
}
/* 防降级门禁(与 nyai 同规范,APPS.md §3.8):仅按 version_code 比较。
* 新版 code < 已装 → 拒;相等 → 允许重装;任一侧缺 version_code → 拒。 */
if (replacing) {
char nc[32], oc[32];
long long ncode = -1, ocode = -1;
int nok = 0, ok2 = 0;
if (pkg_json_field_mpk(mpk_path, "version_code", nc, sizeof nc) == 0) {
char *ep; long long v = strtoll(nc, &ep, 10);
if (ep != nc && *ep == '\0') { ncode = v; nok = 1; }
}
char instj[280];
snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(instj, "version_code", oc, sizeof oc) == 0) {
char *ep; long long v = strtoll(oc, &ep, 10);
if (ep != oc && *ep == '\0') { ocode = v; ok2 = 1; }
}
if (!nok || !ok2) { logmsg("pkg_install: %s version_code missing, reject", pkg); return -4; }
if (ncode < ocode) { logmsg("pkg_install: downgrade %s new %lld < old %lld, reject", pkg, ncode, ocode); return -4; }
}
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
snprintf(cmd, sizeof cmd,
"rm -rf '%s' 2>/dev/null; "
"[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; "
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
/* II 期:app.json.package 绑定(== 包名,禁改包名/目录改名;防第三方面具系统包)。 */
{
char appj[280], ph[64];
snprintf(appj, sizeof appj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(appj, "package", ph, sizeof ph) == 0 && strcmp(ph, pkg) != 0) {
logmsg("pkg_install: app.json.package %s != %s, reject", ph, pkg);
char rb[760];
snprintf(rb, sizeof rb, "rm -rf '%s' 2>/dev/null; [ -d '%s' ] && mv '%s' '%s' 2>/dev/null",
dst, bak, bak, dst);
system(rb);
return -3;
}
}
/* 数据 init:MeowData/<pkg>/{data,cache} */
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
system(cmd);
/* 有效签 → 原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
if (vr == 0) pkg_save_src(mpk_path, pkg);
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
return 0;
}
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
int meow_pkg_export(const char *pkg, const char *out_path)
{
if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) {
logmsg("pkg_export: unsafe path"); return -2;
}
if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; }
char src[256], dst[256];
snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; }
snprintf(dst, sizeof dst, "%s", out_path);
/* II 期:导出保留签名 —— 若安装时存了原签 .mpk.src 且其版本==已装 → 直接复制(不二次签名)。 */
if (pkg_export_preserve_sig(pkg, dst) == 0) {
logmsg("pkg_export: %s -> %s (signed, preserved)", pkg, dst);
return 0;
}
/* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */
char cmd[512];
snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && "
"mv '%s.tmp' '%s'", src, dst, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; }
logmsg("pkg_export: %s -> %s", pkg, dst);
return 0;
}
/* returns 0 on success (app ran and exited), -1 on launch failure */
/* GESTURE_BACK §2.4:把 app.json 的 immersive 声明写进 /tmp/meowapp_state,
* watchdog 据此决定是否画虚拟返回键 / 走 immersive 手势(watchdog 每 100ms 读一次)。
* immersive=1→"1";0→"0";<0→清空(应用退出,恢复默认非沉浸)。 */
static void meow_write_app_state(int immersive)
{
if (immersive < 0) { unlink("/tmp/meowapp_state"); return; }
int fd = open("/tmp/meowapp_state", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return;
if (write(fd, immersive ? "1" : "0", 1) < 0) { /* ignore write error */ }
close(fd);
}
/* 读 App 顶条模式:0=展开 1=收合 2=隐藏;-1=无文件/读取失败(按展开处理,默认安全)。 */
static int app_sbar_mode_read(void)
{
int fd = open(MEOW_SBAR_MODE_FILE, O_RDONLY);
if (fd < 0) return -1;
char ch[8];
int n = read(fd, ch, sizeof ch - 1);
close(fd);
if (n <= 0) return -1;
if (ch[0] == '1') return 1;
if (ch[0] == '2') return 2;
return 0;
}
/* App 前台会话:App 进程(SDK 输入层音量键 / App 自身设置)改音量/亮度只写 settings.conf+DAC,
* launcher 的 meow_settings 内存表与 g_sys 缓存不感知 → 状态栏叠加层 VOL/亮度数字停旧值(真机 bug)。
* 修法:1s 节流 stat settings.conf,mtime 变化才 meow_settings_init() 重读 + 同步缓存全局
* (不在热路径每秒全量重读;顶条动画期 ~30ms 快刷也不会放大 stat 频率——节流在 stat 之前)。 */
static void app_overlay_settings_poll(void)
{
static unsigned long long last_poll_ms = 0;
static long long last_mtime = -1;
unsigned long long now = now_ms();
if (last_poll_ms && now - last_poll_ms < 1000) return;
last_poll_ms = now;
struct stat st;
if (stat(meow_settings_file_path(), &st) != 0) return;
if (last_mtime >= 0 && (long long)st.st_mtime == last_mtime) return;
int first = (last_mtime < 0);
last_mtime = (long long)st.st_mtime;
if (first) return; /* 首调只锚定 mtime(启动已 settings_load 过) */
meow_settings_init(); /* 幂等:重置默认 + 重读 settings.conf */
settings_sync_cache_from_backend(); /* audio.volume 等 → settings_volume 等缓存 */
g_sys.vol = settings_volume_get();
g_sys.brightness = meow_settings_brightness_clamped();
logmsg("app-overlay: settings.conf changed -> reload vol=%d bri=%d",
g_sys.vol, g_sys.brightness);
}
static void app_overlay_statusbar(void)
{
if (!back_buf_ok || !fb) return; /* 无 SDK back_buf(直写模式):无法叠加,跳过 */
int mode = app_sbar_mode_read();
if (mode == 2) { /* 全屏隐藏(immersive,inset=0):launcher 不叠顶条 */
meow_ui_set_fullscreen(1);
meow_ui_statusbar_set_collapsed(0);
return; /* 无内容可叠:交 App 全屏(其 present inset0 已写满屏) */
}
if (mode == 1) { /* 收合:SDK 切收合(上移过渡 + 提示线),无时钟/图标/值 */
meow_ui_set_fullscreen(0);
meow_ui_statusbar_set_collapsed(1);
meow_ui_statusbar_animate(MEOW_SBAR_ANIM_MS); /* 平滑上移/恢复过渡时长 */
} else { /* 展开(默认/无文件):整条 28px */
meow_ui_set_fullscreen(0);
meow_ui_statusbar_set_collapsed(0);
}
app_overlay_settings_poll(); /* settings.conf mtime 变 → 重读(App 侧改音量/亮度跟进) */
meow_sys_light(); /* 刷新 g_sys(电池/WiFi/音量/蓝牙;唯一权威源) */
meow_theme_reload(); /* SDK 状态栏主题随 launcher 换肤(重读 settings) */
meow_ui_feed f;
f.batt = g_sys.batt;
f.charging = g_sys.charging;
f.wifi_on = g_sys.wifi_on;
f.wifi_connected = g_sys.wifi_connected;
f.wifi_level = g_sys.wifi_level;
f.bt_seen = g_sys.bt_seen;
f.bt_on = g_sys.bt_on;
f.vol = g_sys.vol;
f.brightness = g_sys.brightness;
meow_ui_sys_feed(&f); /* 喂权威快照 + 置 force(顶条必重画) */
/* 撤销 set_collapsed 可能引发的「整帧脏」:launcher 在 App 上叠加只应 blit 顶条,绝不能整帧 blit
* (否则会把自身 back_buf 的 stale 菜单内容盖到 App 内容上,属灾难性回归——agent-146)。
* 随后只标「心跳」脏(顶条左上角 1px)唤醒 present();实际 blit 区由 SDK 渲染在 present() 内标脏决定
* (render_off 画 [0, 28-off) 可见条高 / 收合稳定画 [0,4) 提示线 / 展开画 [0,28) 整条),故上移时
* 只 blit 可见条高、其余行保留 App 内容 —— 平滑上收无残留、不黑框。 */
meow_gfx_dirty_reset();
meow_dirty_add(0, SB_BAR_Y, 1, 1);
meow_ui_flush(); /* SDK present:vsync + 原子 blit(区随渲染标脏) */
}
/* (definition follows) */
#define MEOW_CRASH_SOCK "/tmp/meow_crash.sock" /* meow_crash 崩溃上报 socket(best-effort,见 meow_crash.md §2)*/
/* App 异常退出(信号杀 / 非 0 退出码)→ best-effort 直报 meow_crash。
* 格式:`app <pkg> sig=<n>` / `app <pkg> exit=<n>`(参照 svcd notify_crash(),但用 app 域前缀)。
* 全程非阻塞:socket O_NONBLOCK;connect 失败即弃;EINPROGRESS 短 poll(POLLOUT) ≤150ms 排空,
* 仍不可写即丢弃;单行小消息一次 write(失败即弃),绝不重试。meow_crash 不在线 → 静默丢,
* 绝不拖垮 launcher。调用方负责只对异常退出调用(干净 exit(0) 不上报)。 */
static void notify_app_crash(const char *pkg, int status)
{
const char *kind;
int num;
if (WIFSIGNALED(status)) { kind = "sig"; num = WTERMSIG(status); }
else { kind = "exit"; num = WIFEXITED(status) ? WEXITSTATUS(status) : 1; }
char msg[192];
int len = snprintf(msg, sizeof msg, "app %s %s=%d\n", pkg, kind, num);
if (len <= 0) return;
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return;
int fl = fcntl(fd, F_GETFL, 0);
if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK);
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
strncpy(a.sun_path, MEOW_CRASH_SOCK, sizeof a.sun_path - 1);
if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) {
if (errno == EINPROGRESS || errno == EWOULDBLOCK) {
struct pollfd pf;
pf.fd = fd; pf.events = POLLOUT; pf.revents = 0;
int r = poll(&pf, 1, 150);
if (r <= 0 || !(pf.revents & POLLOUT)) { close(fd); return; }
} else {
close(fd);
return; /* crash 不在线:静默丢 */
}
}
ssize_t w = write(fd, msg, (size_t)len);
(void)w;
close(fd);
}
/* 写 §10.5 SDK 自抑标记(/tmp/meow_notif_active)。**内容 = 当前前台屏幕归属者 app_id**;
* 空(传 NULL / "")= 菜单前台 / 叠加层覆盖 / 无前台单元(SDK 判「抑制全部 App」)。
* SDK(meow_ui_gfx.c / meow_ui_input.c 共享判定 meow_ui_overlay_suppressed_active)据内容判
* 本进程是否前台:内容==本进程 app_id → 前台不抑制;内容为空 → 全抑制;内容为其它 app_id
* → 后台常驻/非前台 → 自抑上屏 + 排空输入。
* 批①多 App 后台常驻:取代旧「单标志写 1」——单标志无法区分前台/后台,清标志会连后台 App
* 一起放行(真机「前台 Hello 叠后台 ncm 帧 + 触摸穿透一次交互双响应」的根因)。 */
static void notif_active_set(const char *owner)
{
int nfd = open(NOTIF_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (nfd >= 0) {
if (owner && owner[0]) (void)write(nfd, owner, strlen(owner));
close(nfd);
}
}
/* §10.5:App 前台顶部下滑 → 展开「由 launcher 渲染」的通知中心叠加会话。
* 复用电源菜单对 App 的冻结模板(show_power_menu 的 SIGSTOP/读回/叠加/接管/恢复):
* ① SIGSTOP App → ② 写 /tmp/meow_notif_active(+ /tmp/meow_power_menu_active 让 watchdog 让位)
* → ③ pm_capture_app_frame 读回 App 帧当背景 → ④ run_notif_center(apps={a},1) 叠加渲染
* + 接管输入 → ⑤ 关闭后恢复 App 帧 → unlink active → SIGCONT App。
* 媒体保活分支(定案):当前媒体源==本 App 且 PLAYING(meow_media_app_playing)→ ①不 SIGSTOP、
* ⑤不 SIGCONT;App 继续跑保 PCM 供流,上屏由 SDK present 层见 ②的标记自抑,输入由 SDK
* 输入线程见标记排空(防面板点击被 App 双重消费)。非播放中 App 维持 SIGSTOP 模板。
* 差异:背景=App 帧(非应用列表);关闭后回 App(非回 run_menu);点通知首期 = dismiss + 关面板
* + SIGCONT 回当前 App(不跨 app 启动,见 NOTIF_CENTER_APP_OVERLAY_PLAN §3.2)。
* App 崩溃探测:run_notif_center 循环内 read /proc/<pid>/stat 状态位(zombie)周期探测
* (g_notif_ctrl_pid),发现死 → g_notif_ctrl_died → 立即结束会话(不 SIGCONT 死进程、不恢复死帧);
* 回收仍由 waitpid 循环统一做(捕获真实 exit 状态);返回 1 让 waitpid 循环走统一应用退出路径
* 归位(launcher 不崩)。返回 0=正常回 App。 */
static int run_app_notif_overlay(struct app *a, pid_t pid)
{
if (!back_buf_ok || !fb) { logmsg("notif-overlay: no back_buf (direct mode) skip"); return 0; }
/* 媒体保活(定案):当前媒体源==本前台 App 且 state==PLAYING → 不 SIGSTOP。ncm 等播放类
* App 的播放线程同进程,冻结即 PCM 断供(真机报告:播放中滑下通知中心音乐停)。改为:
* App 继续跑(音频照常喂 audiod、媒体上报照常),SDK present 层见 /tmp/meow_notif_active
* 自抑上屏(只画进 back_buf 不 present,防重绘穿透叠加层);关闭时不补 SIGCONT(没 STOP 过)。
* 非播放中 App 维持现状 SIGSTOP。 */
int keep_running = meow_media_app_playing(a->id);
if (keep_running) {
logmsg("notif-overlay: open (app=%s pid=%d) PLAYING -> no SIGSTOP (SDK present suppressed)",
a->name, pid);
} else {
logmsg("notif-overlay: open (app=%s pid=%d) SIGSTOP", a->name, pid);
if (kill(pid, SIGSTOP) < 0) {
logmsg("notif-overlay: SIGSTOP %d fail: %s", pid, strerror(errno));
} else {
/* 等 App 进入 stopped 态(/proc/pid/stat 状态位 'T';超时兜底);frozen 后才能读回帧 */
for (int i = 0; i < 100; i++) {
if (proc_is_stopped(pid)) break;
usleep(10000);
}
}
}
/* 让位标记:/tmp/meow_notif_active(§10.5 语义同 powmenu_active)。当前 watchdog 只认
* POWMENU_ACTIVE,故同时写它让 watchdog 对电源长按让位;会话结束统一恢复通知中心前台。
* 内容为空=抑制全部 App(叠加层覆盖前台 App + 后台常驻 App 一并自抑)。 */
notif_active_set(""); /* 叠加期:全抑制(面板/锁屏盖在前台 App 上) */
int pfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (pfd >= 0) { write(pfd, "1", 1); close(pfd); }
meow_sys_light(); /* 顶条数据新鲜(电量/WiFi/音量/蓝牙) */
meow_theme_reload(); /* SDK 状态栏主题随 launcher 换肤 */
pm_capture_app_frame(); /* 读回 App 帧(fb0 buffer0)→ back_buf 当背景 */
struct app app1[1];
app1[0] = *a;
notif_click_t click;
g_notif_ctrl_pid = (int)pid; /* run_notif_center 周期探测 App 存活 */
g_notif_ctrl_died = 0;
run_notif_center(app1, 1, &click, 0, 0, 0);
g_notif_ctrl_pid = 0;
int died = g_notif_ctrl_died;
if (died) {
/* App 已死:不 SIGCONT、不恢复死帧;back_buf 交还 run_menu 统一重绘。
* 标记置空(全抑制):菜单前台,后台常驻 App 仍自抑(不再放行帧/输入)。 */
notif_active_set("");
unlink(POWMENU_ACTIVE);
logmsg("notif-overlay: app died during session -> unified exit path");
return 1;
}
/* 恢复 App 帧(run_notif_center 关面板后 back_buf 已还原为背景=App 帧;再拍一次兜底
* 覆盖非动画关闭路径)。媒体保活路径(keep_running):先恢复帧再把标记更新为前台归属
* (a->id)——App 未被冻结仍在跑,标记多留一拍可让 SDK present 自抑覆盖到恢复帧上屏之后,
* 避免「标记先消失 → App 抢在恢复帧前 present 新帧 → 又被旧帧盖回」的一帧回闪。
* 标记翻回 a->id:本 App 恢复前台不抑制,后台常驻 App(如 ncm)随之继续自抑(不叠帧/不穿输入)。 */
pm_capture_app_frame();
meow_ui_flush();
if (!keep_running) {
/* §310 输入穿透定案(与 lock-overlay 同源):App 被 SIGSTOP 冻结过 → 在标记仍存在时
* 先 SIGCONT,让 SDK 输入线程排空冻结期缓冲的 evdev 事件 + 对齐 EVIOCGKEY,再等
* OVERLAY_DRAIN_WINDOW_MS 后把标记翻回前台——防关面板瞬间把积累触摸回放成 App 内点击。 */
kill(pid, SIGCONT);
usleep(OVERLAY_DRAIN_WINDOW_MS * 1000);
logmsg("notif-overlay: closed -> SIGCONT app, drain %dms, then mark fg",
OVERLAY_DRAIN_WINDOW_MS);
} else {
logmsg("notif-overlay: closed -> app never stopped (media keep-alive), no SIGCONT");
}
notif_active_set(a ? a->id : ""); /* 标记翻回前台归属(非空→本 App 解除自抑) */
unlink(POWMENU_ACTIVE);
return 0;
}
/* M2.x 锁屏:App 前台会话唤醒后的锁屏叠加(复用电源菜单/通知中心 SIGSTOP+读回+叠加+接管模板)。
* 语义与 run_app_notif_overlay 同构:媒体保活(本 App 播放中)不 SIGSTOP(断供 PCM),SDK present
* 见 /tmp/meow_notif_active 自抑上屏;否则 SIGSTOP 冻结 App,run_lock_screen 全屏接管;解锁/SIGCONT。
* App 死亡不在本函数内探测(run_lock_screen 阻塞,waitpid 循环在返回后统一回收,杀死进程 SIGCONT 无害)。
* 返回 0(锁屏已结束;是否解锁/息屏由 run_lock_screen 返回值决定,本层不关心)。 */
static int run_app_lock_overlay(struct app *a, pid_t pid)
{
if (!back_buf_ok || !fb) { logmsg("lock-overlay: no back_buf (direct mode) skip"); return 0; }
int keep_running = meow_media_app_playing(a->id);
if (keep_running) {
logmsg("lock-overlay: open (app=%s pid=%d) PLAYING -> no SIGSTOP (SDK present suppressed)",
a->name, pid);
} else {
logmsg("lock-overlay: open (app=%s pid=%d) SIGSTOP", a->name, pid);
if (kill(pid, SIGSTOP) < 0) {
logmsg("lock-overlay: SIGSTOP %d fail: %s", pid, strerror(errno));
} else {
for (int i = 0; i < 100; i++) {
if (proc_is_stopped(pid)) break;
usleep(10000);
}
}
}
/* 标记置空=全抑制(锁屏叠在前台 App + 后台常驻 App 上;解锁后翻回前台归属)。 */
notif_active_set("");
int pfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (pfd >= 0) { write(pfd, "1", 1); close(pfd); }
meow_sys_light();
meow_theme_reload();
pm_capture_app_frame();
/* 置位叠加期 App pid:run_lock_screen 每圈探测该 App 是否在锁屏期崩溃(§310 残影),
* 崩溃即中止锁屏返回 LCK_OVERLAY_RET_DIED,走下方统一退出路径。 */
g_lock_overlay_pid = (int)pid;
int lr = run_lock_screen();
g_lock_overlay_pid = 0;
if (lr == LCK_OVERLAY_RET_DIED) {
/* 叠加期 App 已死:不 SIGCONT(死进程)、不恢复死帧;标记置空(菜单前台,后台常驻 App
* 仍自抑)交回 waitpid 循环回收,run_menu 统一重绘主屏(含状态栏)。launcher 不崩。 */
notif_active_set("");
unlink(POWMENU_ACTIVE);
logmsg("lock-overlay: app pid %d died during lock -> unified exit path", pid);
return 1;
}
pm_capture_app_frame();
meow_ui_flush();
if (!keep_running) {
/* §310 输入穿透定案(overlay_gate.h):App 被 SIGSTOP 冻结过 → 在 /tmp/meow_notif_active
* 仍未更新时**先 SIGCONT**,让 SDK 输入线程排空冻结期缓冲的 evdev 事件副本 + 对齐
* EVIOCGKEY,再等 OVERLAY_DRAIN_WINDOW_MS(> SDK 200ms 节流窗)后把标记翻回前台归属——
* 防解锁瞬间把积累的触摸回放成 App 内点击(锁屏穿透→「启动奇怪界面」→ 状态栏消失的触发源)。 */
kill(pid, SIGCONT);
usleep(OVERLAY_DRAIN_WINDOW_MS * 1000);
logmsg("lock-overlay: closed (lr=%d) -> SIGCONT app, drain %dms, then mark fg",
lr, OVERLAY_DRAIN_WINDOW_MS);
} else {
logmsg("lock-overlay: closed (lr=%d) -> app never stopped (media keep-alive), no SIGCONT", lr);
}
/* 标记翻回前台归属(a->id):本 App 解除自抑恢复前台;后台常驻 App(如 ncm)随之继续自抑
* (不叠帧 / 不穿输入——修复「锁屏解锁后 ncm 歌单帧叠加在 Hello 上 + 交互双响应」)。 */
notif_active_set(a ? a->id : "");
unlink(POWMENU_ACTIVE);
/* §310 状态栏恢复:解锁后强制 g_sb_mode_last=-1 + 立即重叠加顶条(不等 app 循环 1s 慢刷),
* 消除「解锁回 App 无状态栏」的残留态。overlay_force_sb_resync() = 定案恒真。 */
if (overlay_force_sb_resync()) {
g_sb_mode_last = -1;
if (!a->fullscreen) app_overlay_statusbar();
}
return 0;
}
/* launch_app_chain:launch_app / launch_activity 的共享核心。
* chain 非空 → 向子进程注入 MEOW_ACTIVITY/MEOW_CHAIN 并初始化 nav_stack(M3);空/NULL → 裸启动。 */
static int launch_app_chain(struct app *a, const char *chain)
{
logmsg("launch: %s (%s) chain=\"%s\"", a->name, a->exec, chain ? chain : "");
/* §6.2 二期:入口即捕获 picker 上下文并解除武装(meow_ams_take_picker)——即便后续
* exec 失败(!a->ok)返回,武装位也已清,不会错配到下一次普通启动(单飞不泄漏)。 */
char pick_action[24] = "";
char pick_caller[MEOW_APP_ID_MAX] = "";
uint32_t pick_req = 0;
meow_ams_take_picker(pick_action, sizeof pick_action,
pick_caller, sizeof pick_caller, &pick_req);
if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; }
meow_dep_clear_reason(); /* 每次 launch 入口清 stale 依赖阻断原因(防上一 app 残留) */
/* M4 后台保活恢复:该 App 驻留后台(播放中经 nav-exit 转过)→ 原进程回前台,
* 不 fork/exec(进程仍存无法注入新 env:chain/picker 上下文忽略,记日志)。
* pid/out_fd 外提:冷启动与恢复两路填充,其后 watchdog/waitpid 循环共用。 */
pid_t pid = 0;
int out_fd = -1;
int resumed = 0;
if (meow_bg_take(a->id, &out_fd, &pid) == 0) {
resumed = 1;
logmsg("launch: %s resume from background (pid=%d)", a->name, (int)pid);
if (chain && chain[0]) logmsg("launch: resume ignores chain=\"%s\"", chain);
if (pick_action[0]) logmsg("launch: resume ignores picker action=%s", pick_action);
/* 回前台:标记内容置为前台归属(a->id)——本 App 解除自抑恢复前台;仍驻留后台的其它
* App(如 ncm)随内容≠自身而继续自抑(不叠帧/不穿输入;批①多 App 常驻关键)。 */
notif_active_set(a->id);
drain_input();
meow_nav_begin(a->id, NULL); /* 转后台时导航栈已清;回前台从裸态重建 */
ams_report_fg(a->id); /* 批①:单向 best-effort 上报 amsd 置 AMS_FG */
}
if (!resumed) {
/* 二进制依赖启动前检查(app.json depends):required 缺失阻断启动,optional 缺失放行。
* 只在冷启动时做(不做周期复查);resumed 进程不重查(依赖无变化、无需再 fork)。 */
{
char dep_missing[32] = "";
if (meow_check_app_depends(a, dep_missing, sizeof dep_missing) != 0) {
logmsg("launch blocked: dep '%s' missing (CANNOT START: %s)", dep_missing, a->name);
return -1;
}
}
/* 冷启动:标记内容置为前台归属(a->id),让本 App 首帧即不被自抑(后台常驻 App 继续自抑)。
* 旧版此处不写标记:若已有后台常驻 App(ncm)残留「1/空」标记,新前台 App 会被误自抑
* → 主界面点 Hello 渲染卡死(状态栏正常)的根因。 */
notif_active_set(a->id);
meow_nav_begin(a->id, chain); /* M3:冷启动/裸启动都以当前链深初始化导航栈(chain 空→depth 0) */
/* M2.12:启动前准备 MeowData/<pkg>/{data,cache}(幂等;卡不在/失败只 log 不崩)。
* 应用经环境变量 MEOW_DATA_DIR/MEOW_CACHE_DIR 定位数据(exFAT 无 symlink)。 */
meow_app_prepare(a->exec);
/* save the brightness the player had before the menu/app session, then
* drain input queues so the app starts clean */
saved_brightness = read_brightness();
if (saved_brightness <= 0) saved_brightness = 60; /* standby/unknown */
logmsg("saved brightness = %d", saved_brightness);
drain_input();
/* Bug 3: distinguish a REAL exec failure from a clean app exit. The child
* holds the write end of a CLOEXEC pipe; if execve() fails it writes the
* errno and _exit(127), if it succeeds the fd closes on exec so the parent
* read() returns EOF. Parent blocks on read() until the child either execs
* (success) or reports the error (failure); only then do we spawn the
* watchdog and waitpid — so an exec failure never leaves an orphan watchdog
* and never masquerades as "app ran and exited". */
int execpipe[2];
if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; }
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
/* AMSD_DESIGN §5:stdout/stderr 管道捕获(logcat 思路)。fork 前建管道;子进程 dup2 到
* stdout/stderr;父进程保留读端(O_NONBLOCK)+ 前台 waitpid 循环排空转发 amsd。
* 管道建失败仅 log 不崩(App 照常无捕获运行)。写端设 CLOEXEC:exec 成功后关闭写端,
* App 死 → 读端 EOF(崩溃尾巴可排空)。 */
int out_pipe[2] = { -1, -1 };
if (pipe(out_pipe) != 0) {
out_pipe[0] = -1; out_pipe[1] = -1;
logmsg("ams: stdout pipe fail: %s", strerror(errno));
} else {
fcntl(out_pipe[1], F_SETFD, FD_CLOEXEC);
}
pid = fork();
if (pid < 0) {
close(execpipe[0]); close(execpipe[1]);
if (out_pipe[0] >= 0) close(out_pipe[0]);
if (out_pipe[1] >= 0) close(out_pipe[1]);
logmsg("fork failed: %s", strerror(errno)); return -1;
}
if (pid == 0) {
/* child: exec the app. M2.12 注入 MEOW_LOG_DIR + MEOW_DATA_DIR/MEOW_CACHE_DIR
* (exFAT 无 symlink,数据目录用 env 传给应用)。PATH 继承 launcher(含卡上 opt/bin
* 若在),卡不在则回退普通 PATH。M2.52 追加注入 MEOW_LOG_APP(app_id)+MEOW_LOG_LEVEL
* (读全局 log.level),使 SDK meow_log() 按 app 分离写 logs/<id>/<ts>_<pid>.log 并做级别过滤。
* M2.x 简化:MEOW_LOG_DIR 从 logs/apps/<id>/logs 改注入 logs/<id>(目录不存在则建)。 */
close(execpipe[0]);
/* AMSD_DESIGN §5:stdout/stderr 合并重定向到管道写端(dup2 不清 CLOEXEC),
* 原始 out_pipe 两根 fd 关闭;成功 exec 后仅 fd1/2 持有写端,App 死 → EOF。 */
if (out_pipe[1] >= 0) {
dup2(out_pipe[1], STDOUT_FILENO);
dup2(out_pipe[1], STDERR_FILENO);
close(out_pipe[1]);
close(out_pipe[0]);
}
char *argv[] = { a->exec, NULL };
char env_path[320], env_log[320], env_data[320], env_cache[320];
char app_log_dir[288];
char env_app[MEOW_APP_ID_MAX + 16], env_lvl[48];
char env_fpng[400], env_fidx[400], env_fapp[96];
char env_mf[24];
char env_tier[24];
char env_fs[20];
char env_cls[MEOW_FCLS_COUNT][2][400];
char env_act[MEOW_ACT_MAX + 16]; /* M3:MEOW_ACTIVITY=<target>(链路顶端) */
char env_chain[MEOW_CHAIN_MAX + 16]; /* M3:MEOW_CHAIN=<id0>;<id1>;...;<top> */
char *envp[48]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),tier(1),class(≤12),fullscreen(1),data,cache,act,chain */
int ne = 0;
const char *cp = getenv("PATH");
snprintf(env_path, sizeof env_path, "PATH=%s", cp ? cp : "/usr/bin:/bin");
envp[ne++] = env_path;
envp[ne++] = "HOME=/root";
/* M2.x 简化:MEOW_LOG_DIR 注入 logs/<id>(id=app_id,空则 "app",与 SDK app 分离前缀一致)。
* 目录不存在则用 meow_mkdir_p 建(幂等),失败不阻塞注入——SDK 侧还有 mkdir_p 兜底。 */
if (log_dir[0]) {
const char *logid = a->id[0] ? a->id : "app";
snprintf(app_log_dir, sizeof app_log_dir, "%s/%s", log_dir, logid);
meow_mkdir_p(app_log_dir);
snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", app_log_dir);
envp[ne++] = env_log;
}
/* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */
if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; }
/* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3),
* SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */
{
static const char *lvl_name[] = { "ERROR", "WARN", "INFO", "DEBUG" };
int lv = 2; /* 默认 info */
meow_settings_get_enum("log.level", &lv);
if (lv < 0 || lv > 3) lv = 2;
snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]);
envp[ne++] = env_lvl;
}
/* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套),
* SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle
* 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入,
* SDK 自行回退卡默认→固件→点阵。 */
{
static const char *cls_env_png[MEOW_FCLS_COUNT] = {
"MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG",
"MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" };
static const char *cls_env_idx[MEOW_FCLS_COUNT] = {
"MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX",
"MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" };
static const char *cls_keys[MEOW_FCLS_COUNT] = {
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
"font.class_latin", "font.class_latin_sym", "font.class_other" };
char fpng[400], fidx[400], fam[96];
int have_global;
int mf_on = 0;
meow_settings_get_switch("display.multi_font", &mf_on);
snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0);
envp[ne++] = env_mf;
/* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并
* 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用,
* 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见
* meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */
int eff_tier = meow_font_effective_tier(a->fonts);
/* M2.85 内存护栏:大字库(gbk 驻留 ~17MB)在低内存时会连 App 一起被 OOM 杀
* (真机实测 ncm anon-rss 22MB 被 oom_kill)。MemAvailable 低于阈值 → 强制降 common,
* App 大字库需求让位于「能跑起来」(215 建议策略,launcher 层首选落点)。 */
if (eff_tier > MEOW_FONT_TIER_COMMON) {
long av = sys_mem_available_kb();
if (av >= 0 && av < 24 * 1024) {
logmsg("launch: MemAvailable=%ldKB < 24MB, font tier %d -> common (OOM guard)", av, eff_tier);
eff_tier = MEOW_FONT_TIER_COMMON;
}
}
snprintf(env_tier, sizeof env_tier, "MEOW_FONT_TIER=%d", eff_tier);
envp[ne++] = env_tier;
have_global = (meow_font_effective_paths_tiered(eff_tier, fpng, sizeof fpng,
fidx, sizeof fidx, fam, sizeof fam) == 0);
if (have_global) {
snprintf(env_fpng, sizeof env_fpng, "MEOW_FONT_PNG=%s", fpng);
snprintf(env_fidx, sizeof env_fidx, "MEOW_FONT_IDX=%s", fidx);
snprintf(env_fapp, sizeof env_fapp, "MEOW_FONT_APP=%s", fam);
envp[ne++] = env_fpng;
envp[ne++] = env_fidx;
envp[ne++] = env_fapp;
logmsg("launch: font env %s/%s (family=%s)", fpng, fidx, fam);
}
/* M2.5x 多字体:每类注入 png/idx(命中则覆盖全局;类值为空=跟随全局,不注入)。
* SDK 读这些 env 构建多字体映射;同 family 去重由 SDK 侧 meow_font_config_multifont 处理。 */
if (mf_on)
for (int c = 0; c < MEOW_FCLS_COUNT; c++) {
char ref[MEOW_SET_MAX_LEN], abs[400], idxabs[400];
if (meow_settings_get_str(cls_keys[c], ref, sizeof ref) != 0 || !ref[0]) continue;
if (meow_font_resolve_path(ref, abs, sizeof abs) != 0 || access(abs, R_OK) != 0) continue;
snprintf(idxabs, sizeof idxabs, "%s", abs);
size_t n4 = strlen(idxabs);
if (n4 > 4 && !strcmp(idxabs + n4 - 4, ".png")) {
idxabs[n4-4]='.'; idxabs[n4-3]='i'; idxabs[n4-2]='d'; idxabs[n4-1]='x'; idxabs[n4]=0;
}
snprintf(env_cls[c][0], sizeof env_cls[c][0], "%s=%s", cls_env_png[c], abs);
snprintf(env_cls[c][1], sizeof env_cls[c][1], "%s=%s", cls_env_idx[c], idxabs);
envp[ne++] = env_cls[c][0];
envp[ne++] = env_cls[c][1];
}
}
/* 全屏声明:app.json "fullscreen"(app 级)→ 注入 MEOW_FULLSCREEN=1,SDK 端
* meow_ui_init 据此预先设全屏。基座只接 app 级;活动级 route.<id>.fullscreen
* (meow_route.fullscreen)在 M3 活动切换时由后续代理消费(此处暂不下发)。
* 未声明则不注入(SDK 默认非全屏)。 */
if (a->fullscreen) {
snprintf(env_fs, sizeof env_fs, "MEOW_FULLSCREEN=1");
envp[ne++] = env_fs;
}
char pkg[64];
if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) {
snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg);
snprintf(env_cache, sizeof env_cache, "MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/%s/cache", pkg);
envp[ne++] = env_data;
envp[ne++] = env_cache;
}
/* M3:冷启动/通知跳转注入 activity 链(FRAMEWORK_DESIGN §3)。chain 非空才注入;
* MEOW_ACTIVITY=链路顶端(最后一段=target),MEOW_CHAIN=完整链路。裸启动不注入
* (App 走 SDK 默认首页)。 */
if (chain && chain[0]) {
const char *top = strrchr(chain, ';');
top = top ? top + 1 : chain;
snprintf(env_act, sizeof env_act, "MEOW_ACTIVITY=%s", top);
snprintf(env_chain, sizeof env_chain, "MEOW_CHAIN=%s", chain);
envp[ne++] = env_act;
envp[ne++] = env_chain;
logmsg("launch: M3 env %s / %s", env_act, env_chain);
}
/* AMSD_DESIGN §6.2 二期:picker handler 启动注入(捕获值来自 launch_app_chain 入口)。 */
ne = ams_push_picker_env(envp, ne, pick_action, pick_req, pick_caller);
envp[ne] = NULL;
execve(a->exec, argv, envp);
int e = errno;
(void)write(execpipe[1], &e, sizeof e); /* reached only on exec failure */
_exit(127);
}
close(execpipe[1]);
if (out_pipe[1] >= 0) close(out_pipe[1]); /* parent: 关闭写端 */
out_fd = out_pipe[0]; /* parent 保留读端,前台循环排空 */
if (out_fd >= 0) {
int fl = fcntl(out_fd, F_GETFL, 0);
if (fl >= 0) fcntl(out_fd, F_SETFL, fl | O_NONBLOCK);
else fcntl(out_fd, F_SETFL, O_NONBLOCK); /* 兜底:绝不因 F_GETFL 失败退回阻塞读 */
}
/* M2.84-bugfix:exec 状态确认绝不能是裸阻塞 read。child 在 execve 前要做
* meow_mkdir_p(app_log_dir) / 字库定向 / 设置读取等预处理;TF 卡慢/挂起时 child 迟迟
* 到不了 exec,execpipe[1](CLOEXEC)写端一直开着 → 裸 read 永久阻塞在 pipe_wait
* (App 暴毙后 launcher 卡死、电源键无响应、UI 全死)。改为非阻塞 + 有界 poll:
* 超时即视为「exec 正常进行」放行到 waitpid 循环——若真 exec 失败 child 会写 errno
* 后 _exit(127),waitpid 循环照常回收并上报非 0 退出,不再漏错误、更不再卡死。 */
fcntl(execpipe[0], F_SETFL, O_NONBLOCK); /* 读端 O_NONBLOCK;写端 CLOEXEC 由 child 持有 */
int exe_err = 0;
int exec_failed = 0; /* 1=已确认 exec 失败(拿到 errno) */
{
struct pollfd epf; epf.fd = execpipe[0]; epf.events = POLLIN; epf.revents = 0;
int pr = poll(&epf, 1, EXEC_CONFIRM_MS);
if (pr > 0 && (epf.revents & (POLLIN | POLLHUP))) {
ssize_t er = read(execpipe[0], &exe_err, sizeof exe_err);
if (er > 0) exec_failed = 1; /* child 写回 exec 失败 errno */
/* er==0(EOF)= exec 成功,写端 CLOEXEC 关闭;er<0(EAGAIN 竞态)= 未定,放行 */
}
}
close(execpipe[0]);
if (exec_failed) {
logmsg("launch FAILED execve(%s): %s (errno %d)", a->exec,
strerror(exe_err), exe_err);
int st = 0;
reap_wait(pid, &st); /* reap the failed child(有界) */
if (out_fd >= 0) close(out_fd);
return -1;
}
} /* end if (!resumed):冷启动分支(prepare/fork/exec/确认) */
/* AMSD_DESIGN §4.1:启动上报(pkg/activity/chain/pid),amsd 建表项 + 开 stdout 捕获会话。
* M4:两路都报(恢复路径诚实记为新一次 LAUNCH;chain 被忽略故报 NULL)。 */
ams_report_launch(a, resumed ? NULL : chain, pid);
/* GESTURE_BACK §2.4:应用启动即写 immersive 状态,watchdog 依此隐藏/保留虚拟返回键 */
meow_write_app_state(a->immersive == '1' ? 1 : 0);
/* M4:冷启动成功且声明 background=true → 自动拉起其静态声明的 keepalive(mode=0)
* 无头服务(reg_fd=-1=静态拉起无注册连接;同名在跑/退避排程中 → EXISTS 静默跳过)。
* 服务 exec 路径约定 = App 二进制同目录下的 <svc> 名文件(见 meow_svc_exec_path)。 */
if (!resumed && a->background == '1') {
for (int i = 0; i < a->nsvc; i++) {
char sexec[160];
int rc;
if (a->svcs[i].mode != 0) continue;
meow_svc_exec_path(a->exec, a->svcs[i].name, sexec, sizeof sexec);
rc = meow_svc_register(a->id, a->svcs[i].name, sexec,
a->svcs[i].restart, a->svcs[i].max,
a->svcs[i].window_ms,
strncmp(sexec, MEOWAPPS, strlen(MEOWAPPS)) == 0,
-1);
if (rc == MEOW_ERR_EXISTS) continue; /* 在跑/排程中:静默跳过 */
logmsg("launch: auto-svc %s/%s -> %s", a->id, a->svcs[i].name,
rc > 0 ? "spawned" : "FAILED");
}
}
/* watchdog for virtual back key / gestures */
char wd1[192], pidstr[16];
pid_t wpid = 0;
snprintf(pidstr, sizeof pidstr, "%d", pid);
if (access(WATCHDOG1, X_OK) == 0)
snprintf(wd1, sizeof wd1, "%s", WATCHDOG1);
else if (access(WATCHDOG2, X_OK) == 0)
snprintf(wd1, sizeof wd1, "%s", WATCHDOG2);
else
wd1[0] = 0;
if (wd1[0]) {
wpid = fork();
if (wpid == 0) {
/* M2.84-bugfix 看门狗不持有不该有的 fd:fork 继承的 launcher 管道读端
* out_fd(App stdout/stderr 捕获读端)与日志 fd log_fd 全部关闭——execve
* 后只带它自己要的(MEOW_LOG_FILE/MEOW_LOG_DIR env 另开自身 log)。
* 防看门狗成为某条管道的读端/写端持有者,干扰 App 死后 EOF 判定。 */
if (out_fd >= 0) close(out_fd);
if (log_fd >= 0) close(log_fd);
/* pass the same boot log file + dir so all MeowAPPs components
* log to one place */
char *argv[] = { wd1, pidstr, NULL };
char *envp[] = { "PATH=/usr/bin:/bin", NULL, NULL, NULL };
char file_env[320], dir_env[256];
if (log_file[0]) {
snprintf(file_env, sizeof file_env, "MEOW_LOG_FILE=%s", log_file);
envp[1] = file_env;
}
if (log_dir[0]) {
snprintf(dir_env, sizeof dir_env, "MEOW_LOG_DIR=%s", log_dir);
envp[2] = dir_env;
}
execve(wd1, argv, envp);
_exit(0);
}
if (wpid > 0) logmsg("watchdog %s pid=%d", wd1, wpid);
}
/* wait for app; in-app kill = VOL_DOWN x3 quick taps (the old
* PREVIOUS+VOL_UP hold combo is retired — the PREVIOUS key's GPIO
* never fires on this unit, §18.12). Volume is snapped on the first
* tap and restored when the sequence completes. */
int status = 0;
unsigned long long ams_t0 = now_ms(); /* AMSD_DESIGN §4.1:运行时长起点 */
int ams_status_valid = 0; /* 仅真实 waitpid 回收才置 1(规避 sigterm/重启路径 status=0 假干净退出) */
int nav_clean_exit = 0; /* M3:MSG_NAV{EXIT} 主动退出 → 不按崩溃上报 */
int to_background = 0; /* M4:nav-exit 转后台保活(进程保留,跳过退出尾) */
int app_prev_vold = 0;
int app_prev_volu = 0; /* E:VOL_UP 上一边沿(翻页门下边沿判定) */
int fg_volpage = 0; /* E:前台 App 是否声明 volume_page(会话期恒定) */
int app_seq_hits = 0;
unsigned long long app_seq_last = 0;
struct vol_snap app_vol0 = { -1, -1 };
/* M6.5 §310 问题1:watchdog 电源键手动息屏在 App 内直接 blank 面板(背光 0),**不置**
* meow_auto_slept(那是本循环超时息屏路径的私有标志)。故手动息屏→电源键唤醒时机内
* meow_auto_slept==0,唤醒分支不锁屏(超时路径天然有锁、手动路径丢了——用户真机实测)。
* 修法:本循环以「背光==0 且非超时」补判手动息屏,唤醒时按 meow_auto_slept||manual 一并触发
* lck_wake_should_lock 锁屏,两条路径对齐。 */
int app_panel_manual_off = 0;
/* M2.11 D:应用二进制/资源若在卡上,卡被拔可能 SIGBUS 崩。启动前记录,运行期监测卡移除。
* (拷贝到内存/板载 staging 的方案先不做,仅记录 APPS.md 规范。) */
int app_on_card = (strncmp(a->exec, MEOWAPPS, strlen(MEOWAPPS)) == 0);
if (app_on_card) logmsg("app on card (%s): monitoring card removal during run", a->exec);
struct keypoll kp_power, kp_keys;
kp_open(&kp_power, DEV_POWER);
kp_open(&kp_keys, DEV_KEYS);
/* M2.17 前台应用会话:系统仍统一控制息屏(应用不自管)。launcher 无法看到应用内触摸,
* 故以「可物理键」为 idle 信号;前台当前 activity(经 IPC MSG_ACT_FOCUS 上报)若 keep_awake
* 则不解屏(fg_keep_awake)。唤醒时应用随其输入重绘,launcher 仅 fb_pan_buf0_force 兜底
* (M2.8 唤醒重绘路径的首步)。见 §18.46。 */
unsigned long long app_last_input = now_ms();
unsigned long save_pow[KEYBUF_WORDS], save_keys[KEYBUF_WORDS];
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
/* 「同一个状态栏」闭环:App 前台状态栏由 launcher 常驻叠加(顶条 28px、非模态、不 SIGSTOP App)。
* 立即叠一次(App 首帧前顶条就有),其后每 ~1s 重画(时钟每秒变 + 数据新鲜)。app 级
* fullscreen 声明(沉浸,整条隐藏)时不叠。agent-146:读 App 顶条模式(meowapp_sbar),App 运行时
* 全屏/收合 → launcher 顶条切收合提示线(无时钟/图标/值),热区事件与渲染分离。 */
unsigned long long last_sb_overlay_ms = 0;
g_sb_mode_last = -1; /* 强制首帧同步(进入新 App 会话清零,避免沿用上一 App 的缓存态) */
unlink(MEOW_SBAR_MODE_FILE); /* 清可能残留的上一 App 顶条模式(崩溃/被杀未清),防新 App 误收合 */
if (!a->fullscreen) app_overlay_statusbar();
/* M2.24:清上一次会话可能残留的电源菜单标记,只认本次新 watchdog 生成的请求。 */
unlink(POWMENU_MARKER);
unlink(POWMENU_ACTIVE);
/* §10.5:进入 App 前台会话。置位让 ipc_dispatch 的 MSG_NOTIF_OPEN 可入队(否则回 ERR);
* 清残留 pending(防上一会话未消费的 OPEN 被本会话误展开)。 */
g_app_session_active = 1;
meow_notif_open_clear();
/* E:系统级音量翻页——前台 App 声明 volume_page 时武装 sink(fd 就绪即生效;断线重连由
* 本循环每圈重试 meow_volpage_sink 幂等更新,keys.c 门只消费 VOL 按下且已武装)。 */
fg_volpage = meow_volpage_app_allows(a->id) ? 1 : 0;
if (fg_volpage) {
int vfd = meow_ipc_app_fd(a->id);
if (vfd >= 0) meow_volpage_sink(a->id, vfd);
}
for (;;) {
int r = waitpid(pid, &status, WNOHANG);
if (r == pid) { ams_status_valid = 1; break; }
if (r < 0 && errno != EINTR) break;
if (sigterm_flag) { logmsg("sigterm during app"); kill(pid, SIGTERM); break; }
/* AMSD_DESIGN §5.1:排空 App stdout/stderr 管道(非阻塞),按行转发 amsd;App 写满不阻塞。 */
g_ams_out_had = 0;
if (out_fd >= 0) ams_drain_stdout(a->id, out_fd, pid, g_ams_out_line, &g_ams_out_n);
/* M3 导航栈:前台会话排空 IPC(App 经 MSG_NAV REPORT/EXIT 上报导航/请求退出)。
* REPORT 更新 nav_stack(meow_ipc_process 内单写者);EXIT → 终止应用回主屏。 */
meow_ipc_process();
/* AMSD_DESIGN §4.3:amsd `am kill` → 前台循环消费 → SIGTERM(执行器=launcher)。 */
if (meow_ams_kill_pending(a->id)) {
logmsg("ams: kill %s -> SIGTERM", a->name);
kill(pid, SIGTERM);
usleep(300000);
if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); reap_wait(pid, &status); }
ams_status_valid = 1;
break;
}
if (meow_nav_exit_pending()) {
/* 批①+宽限后台保活:**全 App** nav-exit 入后台保活表(进程保留转后台,UI 冻结走 §10.5
* SDK present 自抑,**非 SIGSTOP**——SIGSTOP 会冻结同进程播放线程断供 PCM,§10.5 实证)。
* 分两类:
* - 声明常驻(background_keepalive=1)或媒体活跃(background_audio+PLAYING/PAUSED/BUFFER,
* 见 meow_media_app_resident)→ **无限期保活**(不清,除非用户手动/一键清);
* - 其余(未声明/媒体不活跃)→ **宽限期**(bg_grace_ms,默认30min)内保活,超时未再启动
* 由 meow_bg_tick 走真退序列回收。
* meow_bg_add 内含槽满 LRU 回收(最久未激活的未声明槽);全为声明常驻/媒体活跃 → NOMEM
* 回落下方真退序列(保安全)。kill 通道带 !a->framework 门控(批③)。 */
int bg_resident = meow_media_app_resident(a->id);
int r = meow_bg_add(a->id, pid, out_fd, app_on_card,
a->background_keepalive == '1');
if (r == 0 || r == MEOW_ERR_EXISTS) { /* 0=入表;EXISTS=本就驻留(幂等不重复登记不杀) */
logmsg("bg: app %s nav-exit -> 后台保活(%s,pid=%d 保留,SDK present 自抑)",
a->name,
((a->background_keepalive == '1') || bg_resident) ?
"声明常驻/媒体活跃(无限期)" : "宽限期(默认30min)",
(int)pid);
to_background = 1;
break;
}
logmsg("nav: app %s MSG_NAV{EXIT} -> terminate, return to home(后台表满且无未声明槽可回收)",
a->name);
nav_clean_exit = 1;
kill(pid, SIGTERM);
usleep(300000);
if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); reap_wait(pid, &status); }
ams_status_valid = 1;
break;
}
/* §10.5:App 前台顶部下滑(MSG_NOTIF_OPEN)→ 展开通知中心叠加会话。复用电源菜单
* SIGSTOP/读回/叠加/接管/恢复模板;App 死 → run_app_notif_overlay 返回 1,走统一退出。 */
{
m_notif_open_t nopen;
if (meow_notif_open_take(&nopen)) {
int nr = run_app_notif_overlay(a, pid);
/* 输入基线重播(与电源菜单同款):面板期间按键可能已变化,重播防误判 key_edge */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
app_last_input = now_ms(); /* overlay 交互即输入,重置 App 会话息屏计时 */
if (nr != 0) {
/* App 会话中崩溃(overlay 内 proc_is_zombie 探测):回收僵尸取真实状态,
* 走统一应用退出路径(不 SIGCONT 死进程、不恢复死帧;launcher 不崩)。 */
reap_wait(pid, &status); /* 有界,防 D-state 挂死 */
ams_status_valid = 1;
break;
}
}
}
/* M2.11 D:卡被拔(设备节点消失)且前台应用在卡上 → 主动 SIGTERM 干净退出,
* 避免应用等 SIGBUS 崩。回菜单走正常应用退出路径。 */
if (app_on_card && !sd_device_present()) {
logmsg("hotplug: card removed during app run -> SIGTERM app ");
kill(pid, SIGTERM);
usleep(300000);
if (waitpid(pid, &status, WNOHANG) != pid) { kill(pid, SIGKILL); reap_wait(pid, &status); }
ams_status_valid = 1;
break;
}
/* in-app kill: VOL_DOWN x3 quick taps */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
int vold = 0;
for (int w = 0; w < KEYBUF_WORDS; w++) {
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEDOWN % 32))) vold = 1;
}
/* E:系统级音量翻页——前台 App 声明 volume_page 时,VOL_UP/VOL_DOWN 按下边沿 →
* 系统翻页信号(keys.c 门,已武装才消费下发;未武装返回 0 放行,音量/物理键语义全保留)。
* 每圈幂等重试武装(reader 持久连接 HELLO 后 fd 就绪;断线重连换 fd 自动更新)。 */
if (fg_volpage) {
int vfd = meow_ipc_app_fd(a->id);
if (vfd >= 0) meow_volpage_sink(a->id, vfd);
}
{
int volu = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_keys.cur[w] & (1UL << (KEY_VOLUMEUP % 32))) volu = 1;
if (volu && !app_prev_volu) (void)meow_volpage_key(KEY_VOLUMEUP, 1);
if (vold && !app_prev_vold) (void)meow_volpage_key(KEY_VOLUMEDOWN, 1);
app_prev_volu = volu;
}
/* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏;
* 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。
* M6.5 §310 问题1:watchdog 电源键手动息屏(背光 0、不置 meow_auto_slept)→ 唤醒时按
* app_panel_manual_off 一并锁屏,与超时路径(meow_auto_slept)对齐。 */
{
/* 背光==0 => 面板已关(超时路径已置 auto_slept;手动路径靠此补判)。
* read_brightness() 失败返回 -1(≠0),不误置位。 */
if (read_brightness() == 0) app_panel_manual_off = 1;
int key_edge = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] != save_pow[w] || kp_keys.cur[w] != save_keys[w]) { key_edge = 1; break; }
if (key_edge) {
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
app_last_input = now_ms();
/* M6.24 bug:App 前台单按电源息屏后,同一次按压的**松开**边沿被误判为唤醒——
* watchdog 在松开边沿执行 wd_panel_off(背光 0),下一圈本循环 read_brightness()
* ==0 置 app_panel_manual_off=1,而 save_* 基线仍是「按下」态,于是这次「松开」
* 被 key_edge 捕获,再命中 manual_off → meow_panel_on + 锁屏 overlay → 用户看到
* 「黑一下又自动亮 PIN;再按才真正熄屏」。修法=唤醒必须来自**新的按压**:要求
* 当前确有键按下(cur 任一 bit),松散开边沿(无键按住)不唤醒。 */
if (meow_auto_slept || app_panel_manual_off) {
int any_down = 0;
for (int w = 0; w < KEYBUF_WORDS; w++)
if (kp_power.cur[w] || kp_keys.cur[w]) { any_down = 1; break; }
if (any_down) {
logmsg("screen-off (app): key wake (auto=%d manual=%d)",
meow_auto_slept, app_panel_manual_off);
meow_panel_on();
meow_auto_slept = 0;
app_panel_manual_off = 0;
fb_pan_buf0_force();
/* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用。
* §310:叠加期 App 崩溃(LR==LCK_OVERLAY_RET_DIED)→ 回收僵尸走统一退出路径
* (launcher 不崩、run_menu 重绘主屏),同 run_app_notif_overlay 的 died 分支。 */
if (lck_wake_should_lock()) {
int lr2 = run_app_lock_overlay(a, pid);
if (lr2 != 0) {
reap_wait(pid, &status);
ams_status_valid = 1;
break;
}
}
}
}
}
if (screen_off_timeout > 0 && meow_screen_on && !meow_auto_slept && !fg_keep_awake()) {
if (now_ms() - app_last_input >= (unsigned long long)screen_off_timeout * 1000) {
meow_panel_off();
meow_auto_slept = 1;
logmsg("auto screen-off (app %s) after %ds idle (mode=%s keep_awake=%d)",
a->name, screen_off_timeout, screen_off_mode ? "black" : "panel",
fg_keep_awake());
}
}
}
/* M2.71-bug5:App 前台触摸/交互(launcher 看不到应用内触摸,SDK 写标记)→ 重置自动息屏
* 计时;若已自动息屏,按 key-wake 同款路径唤醒(面板上电 + 落 buffer0)。 */
if (access(MEOW_APP_ACTIVITY_FILE, F_OK) == 0) {
unlink(MEOW_APP_ACTIVITY_FILE);
app_last_input = now_ms();
if (meow_auto_slept || app_panel_manual_off) {
logmsg("screen-off (app): touch wake (auto=%d manual=%d)",
meow_auto_slept, app_panel_manual_off);
meow_panel_on();
meow_auto_slept = 0;
app_panel_manual_off = 0;
fb_pan_buf0_force();
/* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用。
* §310:叠加期 App 崩溃 → 回收僵尸走统一退出路径(launcher 不崩、run_menu 重绘主屏)。 */
if (lck_wake_should_lock()) {
int lr2 = run_app_lock_overlay(a, pid);
if (lr2 != 0) {
reap_wait(pid, &status);
ams_status_valid = 1;
break;
}
}
}
}
if (vold && !app_prev_vold) {
unsigned long long nw = now_ms();
if (app_seq_hits == 0) app_vol0 = vol_snapshot();
else if (nw - app_seq_last > (unsigned)seq_gap_ms) {
app_seq_hits = 0;
app_vol0 = vol_snapshot();
}
app_seq_hits++;
app_seq_last = nw;
/* REPORT §30(真机四连症状④):VOL_DOWN x3 是「应用内强杀」快捷键,但和普通
* 调音量(连按音量减几档)冲突——用户连按音量下键会误触发 SIGTERM→SIGKILL,
* 前台 App 闪退(ams 日志 `EXIT dev.player.ncm sig=9`)。框架 App(framework=1,
* 如 dev.player.ncm)有 SDK 级左缘横扫返回退出(D10 默认,app.c 根页
* meow_ui_request_exit),无需此强杀快捷键 → 门控到裸 App(framework=0)才触发,
* 框架 App 连按音量键只调音量不再被杀。裸 App 仍保留 VOL_DOWN x3 强杀出口
* (无框架返回手势),挂死 App 另可 `meow_ctl am kill <pkg>`。 */
if (app_seq_hits >= 3 && !a->framework) {
logmsg("kill seq: VOL_DOWN x3 -> SIGTERM app");
vol_restore(app_vol0);
kill(pid, SIGTERM);
usleep(500000);
kill(pid, SIGKILL);
reap_wait(pid, &status); /* 有界,防 D-state 挂死 */
ams_status_valid = 1;
break;
} else if (app_seq_hits >= 3) {
logmsg("VOL_DOWN x3 ignored: framework app (swipe-back exits, §30)");
}
}
app_prev_vold = vold;
if (app_seq_hits > 0 && now_ms() - app_seq_last > (unsigned)seq_gap_ms * 2)
app_seq_hits = 0;
/* M2.24 全面按键接管:watchdog 检测到电源长按≥3s 写 /tmp/meow_power_menu → 此处消费:
* SIGSTOP 应用冻结 → 写 powmenu_active 让 watchdog 让位 → 统一电源菜单(launcher 一处绘制,
* 复用 show_power_menu,含触摸/音量导航/电源确认/8s 强关/pm_interacted 保护)→ 菜单关 →
* 清 active → SIGCONT 应用续跑。 */
if (access(POWMENU_MARKER, F_OK) == 0) {
unlink(POWMENU_MARKER);
logmsg("watchdog: power menu request (app %s) -> SIGSTOP + menu", a->name);
kill(pid, SIGSTOP);
int mfd = open(POWMENU_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (mfd >= 0) { write(mfd, "1", 1); close(mfd); }
int pm_ret = show_power_menu();
unlink(POWMENU_ACTIVE);
kill(pid, SIGCONT);
logmsg("watchdog: power menu closed (ret=%d) -> SIGCONT app", pm_ret);
/* M2.25:菜单期间按键可能已变化,重播 save_pow/save_keys 防误判 key_edge
* (避免菜单关闭瞬间误触发「键唤醒」或刷新自动息屏计时)。 */
kp_poll(&kp_power, NULL, NULL);
kp_poll(&kp_keys, NULL, NULL);
memcpy(save_pow, kp_power.cur, sizeof save_pow);
memcpy(save_keys, kp_keys.cur, sizeof save_keys);
app_last_input = now_ms(); /* M2.17:菜单交互即输入,重置应用会话自动息屏计时 */
if (pm_ret != 0) break; /* 关机/重启/回HibyOS:系统通常已 reboot,防御性退出 */
}
/* 「同一个状态栏」闭环:常驻顶条叠加(agent-146 全屏收合接入)。
* - 展开态每 ~1s 慢刷(时钟/数据新鲜;仅标脏顶条上屏,不碰 App 内容);
* - App 顶条模式新变(进入/退出全屏收合)→ 立即叠一次(即时切换,消除「半秒闪」);
* - 收合/恢复过渡期(meow_ui_statusbar_animating)每 ~30ms 快刷驱动平滑上移出屏。 */
if (!a->fullscreen) {
unsigned long long now2 = now_ms();
int m = app_sbar_mode_read();
if (m != g_sb_mode_last) {
g_sb_mode_last = m;
last_sb_overlay_ms = now2;
app_overlay_statusbar();
} else if (meow_ui_statusbar_animating()) {
if (now2 - last_sb_overlay_ms >= 30) {
last_sb_overlay_ms = now2;
app_overlay_statusbar();
}
} else if (now2 - last_sb_overlay_ms >= 1000) {
last_sb_overlay_ms = now2;
app_overlay_statusbar();
}
}
/* 顶条收合/恢复过渡期把循环睡得短些(~20ms),驱动更平滑的上移出屏;稳定后回 100ms。
* AMSD_DESIGN §5.1:本轮排空到 stdout 数据 → 也睡短些,防止 App 写满管道被阻塞。 */
usleep((meow_ui_statusbar_animating() || g_ams_out_had) ? 20000 : 100000);
}
/* M4:转后台保活出口 —— 跳过退出尾(不 ams EXIT/不 crash 上报/不清媒体会话/
* 不关 out_fd:进程仍存活,out_fd 归 bg 表由 meow_bg_housekeep 继续 drain)。
* 停播后再退出走上方正常退出尾 = 真退。 */
if (to_background) {
g_app_session_active = 0; /* 前台会话结束(ipc_dispatch 不再受理 NOTIF_OPEN) */
meow_notif_open_clear();
meow_volpage_sink_clear(); /* E:离开前台撤翻页 sink(音量键交还系统) */
if (wpid > 0) {
/* 看门狗随前台会话结束回收(后台无手势监听;有界等防挂死) */
kill(wpid, SIGTERM);
unsigned long long wstart = now_ms();
while (now_ms() - wstart < 2000) {
if (waitpid(wpid, NULL, WNOHANG) == wpid) break;
usleep(50000);
}
}
/* 写 §10.5 SDK 自抑标记(内容空=菜单前台全抑制):App 进程见 /tmp/meow_notif_active
* 且内容≠自身 → present 画进 back_buf 不上屏 + 输入线程排空(=UI 冻结,不叠前台/不穿输入);
* 音频经 audiod socket 天然续播。 */
notif_active_set("");
meow_write_app_state(-1);
unlink(MEOW_SBAR_MODE_FILE); /* 顶条模式文件由 App SDK 写;转后台清理复位菜单整条 */
g_sb_mode_last = -1;
meow_ui_statusbar_set_collapsed(0);
meow_ui_statusbar_animate(0);
meow_ui_set_fullscreen(0);
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H);
settings_load();
meow_nav_exit(); /* 前台会话导航栈清栈(App 本体栈内存活;回前台时裸态重建) */
ams_report_bg(a ? a->id : "", pid); /* 批①:单向 best-effort 上报 amsd 置 AMS_BG */
return 0;
}
/* §10.5:App 前台会话结束,清会话标记(ipc_dispatch 不再受理 MSG_NOTIF_OPEN) */
g_app_session_active = 0;
meow_notif_open_clear();
meow_volpage_sink_clear(); /* E:离开前台撤翻页 sink(音量键交还系统) */
/* M2.17 兜底:App 会话结束(waitpid 回收/崩溃/被杀,回主界面)清 fg_act ——
* App 在 keep_awake activity(如 ncm 播放页)被 OOM/崩溃杀死且未弹回 home 时,g_fg_act 会
* 滞留 "<act>",使主页 fg_keep_awake()==1 不再自动息屏。此处兜底清空(App 已退出/死亡,
* 主界面非 keep_awake);App 存活时 fg_act 保持不清,仅会话整体结束才清。正常路径 App
* 弹回 home 已由 ACT_FOCUS(home) 更新(非 keep_awake),此兜底与其幂等不冲突。 */
meow_fg_act_back();
/* AMSD_DESIGN §5.1:崩溃尾巴不丢 —— App 已死,终局排空剩余 stdout/stderr(含未完结行)
* 先转发 amsd,再报 EXIT / crash。仅当有读端(管道建立成功)才 drain。 */
if (out_fd >= 0) {
g_ams_out_had = 0;
ams_drain_stdout(a->id, out_fd, pid, g_ams_out_line, &g_ams_out_n); /* 排空全部剩余(App 死 → read 返回 0=EOF) */
if (g_ams_out_n > 0) { /* 未完结尾行:强制提交 */
g_ams_out_line[g_ams_out_n] = '\0';
ams_out_line(a->id, pid, g_ams_out_line);
g_ams_out_n = 0;
}
ams_out_flush();
close(out_fd);
out_fd = -1;
}
/* AMSD_DESIGN §4.1:waitpid 回收上报(真实回收才报;规避 sigterm/电源菜单重启 status=0 的
* 假干净退出)。amsd 依 kind/code 判 crash 并转发 meow_crash(SVC 格式)。 */
if (ams_status_valid)
ams_report_exit(a, status, (long long)(now_ms() - ams_t0));
/* M2.x:App 异常退出(信号杀 / 非 0 退出码)→ best-effort 直报 meow_crash。
* 判据与 svcd reap_children 一致:failed = !(WIFEXITED && WEXITSTATUS==0)。干净 exit(0)
* 不上报;sigterm 关停/电源菜单重启路径 status 保持 0(未 waitpid)→ 视为干净退出不上报。 */
if (!(WIFEXITED(status) && WEXITSTATUS(status) == 0) && !nav_clean_exit)
notify_app_crash(a->id, status);
if (wpid > 0) {
/* reap the watchdog: SIGTERM then wait up to 2s (it exits when the
* app dies; bounded so we never hang if it is stuck) */
kill(wpid, SIGTERM);
unsigned long long wstart = now_ms();
while (now_ms() - wstart < 2000) {
if (waitpid(wpid, NULL, WNOHANG) == wpid) break;
usleep(50000);
}
}
meow_write_app_state(-1); /* GESTURE_BACK §2.4:应用退出,清 immersive 状态 */
/* agent-146:App 退出 → 清顶条模式文件 + 复位 launcher SDK 收合/全屏态(菜单恢复整条 28px)。
* 模式文件由 App 进程 SDK 写;此处 launcher 在退出时清理,防下一 App 读到残留 '1'(收合) 误收合。 */
unlink(MEOW_SBAR_MODE_FILE);
g_sb_mode_last = -1;
meow_ui_statusbar_set_collapsed(0);
meow_ui_statusbar_animate(0); /* 回菜单:顶条恢复整条,瞬切(不做上移动画) */
meow_ui_set_fullscreen(0);
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H); /* 菜单状态栏需重画整条(恢复展开) */
/* M2.24:应用运行期音量由 watchdog 写(settings.conf 同源)。返回菜单前 re-load,
* 让菜单音量/状态栏与真实 DAC 一致(watchdog 可能已改)。 */
settings_load();
meow_nav_exit(); /* M3:前台应用会话结束,清导航栈 */
/* 回菜单:标记置空(菜单前台全抑制)——若仍驻留后台常驻 App(批①),其继续自抑不叠帧/不穿输入;
* 若已无后台 App,置空标记无害(SDK 无抑制目标)。 */
notif_active_set("");
/* 阶段D附录B:源 App 退出 → 清其媒体会话(如该 App 上报过 META),通知中心/播放页回空态 */
meow_media_clear(a->id);
logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status));
return 0;
}
/* M3:裸启动(无 activity 链)。保留旧签名,向下兼容不发 MSG_NAV 的 App。 */
int launch_app(struct app *a)
{
return launch_app_chain(a, NULL);
}
/* M3:以 activity 链路冷启动。注入 MEOW_ACTIVITY/MEOW_CHAIN + 设 nav_stack 栈深。 */
int launch_activity(struct app *a, const char *chain)
{
return launch_app_chain(a, chain);
}