/* 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) */ 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 读) */ /* ---- 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); } /* ---- 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; strncpy(a->icon, ap->icon, 128); strncpy(a->name, ap->name, 16); 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.(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 二进制同目录下的 名文件 * (app.json `service.=...` 只声明策略,二进制位置由此约定派生)。 */ 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.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.=1 / route..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=(注入 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(播放中退出转后台保活) */ 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.=(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..fullscreen=:活动级全屏声明(M1 顺带;M3 活动切换时消费)。 * 命中既有 route. 置 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.=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。 * 命中的既有 route. 置 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; } } /* service.=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=[:][,...]:二进制依赖声明(meow_dep.c 解析)。 * 与行式解析同风格(JSON "depends":"..." 或扁平 depends=... 均可);不引 JSON 解析器。 * 缺省 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; } } } strncpy(a->name, name, 16); 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:后台播放保活 */ 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// → */ 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//{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 --package 。返回 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//data/.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//。 * 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 .mpk。 * 同名先备份;解压后校验 app.json;随后数据 init(MeowData//{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 .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//;同名先备份(.bak_),再解压,再校验 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//{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//data/.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//(数据目录本就由 env 指向 MeowData,MeowAPPs/ 下无 * data/cache 实体,直接全打包)→ .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 sig=` / `app exit=`(参照 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: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//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 对电源长按让位;会话结束统一 unlink。 */ int nfd = open(NOTIF_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (nfd >= 0) { write(nfd, "1", 1); close(nfd); } 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 统一重绘 */ unlink(NOTIF_ACTIVE); unlink(POWMENU_ACTIVE); logmsg("notif-overlay: app died during session -> unified exit path"); return 1; } /* 恢复 App 帧(run_notif_center 关面板后 back_buf 已还原为背景=App 帧;再拍一次兜底 * 覆盖非动画关闭路径)。媒体保活路径(keep_running):先恢复帧再 unlink 标记—— * App 未被冻结仍在跑,标记多留一拍可让 SDK present 自抑覆盖到恢复帧上屏之后, * 避免「标记先消失 → App 抢在恢复帧前 present 新帧 → 又被旧帧盖回」的一帧回闪。 */ pm_capture_app_frame(); meow_ui_flush(); unlink(NOTIF_ACTIVE); unlink(POWMENU_ACTIVE); if (!keep_running) { kill(pid, SIGCONT); logmsg("notif-overlay: closed -> SIGCONT app"); } else { logmsg("notif-overlay: closed -> app never stopped (media keep-alive), no SIGCONT"); } 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); } } } int nfd = open(NOTIF_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (nfd >= 0) { write(nfd, "1", 1); close(nfd); } 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(); int lr = run_lock_screen(); pm_capture_app_frame(); meow_ui_flush(); unlink(NOTIF_ACTIVE); unlink(POWMENU_ACTIVE); if (!keep_running) { kill(pid, SIGCONT); logmsg("lock-overlay: closed (lr=%d) -> SIGCONT app", lr); } else { logmsg("lock-overlay: closed (lr=%d) -> app never stopped (media keep-alive), no SIGCONT", lr); } 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); /* 回前台:清 §10.5 SDK present 自抑标记(无其他 bg App 且无叠加会话时) */ if (meow_bg_count() == 0 && g_notif_ctrl_pid <= 0) unlink(NOTIF_ACTIVE); drain_input(); meow_nav_begin(a->id, NULL); /* 转后台时导航栈已清;回前台从裸态重建 */ } 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; } } meow_nav_begin(a->id, chain); /* M3:冷启动/裸启动都以当前链深初始化导航栈(chain 空→depth 0) */ /* M2.12:启动前准备 MeowData//{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//_.log 并做级别过滤。 * M2.x 简化:MEOW_LOG_DIR 从 logs/apps//logs 改注入 logs/(目录不存在则建)。 */ 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=(链路顶端) */ char env_chain[MEOW_CHAIN_MAX + 16]; /* M3:MEOW_CHAIN=;;...; */ 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=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..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 二进制同目录下的 名文件(见 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_seq_hits = 0; unsigned long long app_seq_last = 0; struct vol_snap app_vol0 = { -1, -1 }; /* 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(); 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()) { /* M4 后台播放保活:声明 background_audio 且该 App 媒体会话 PLAYING → 不终止, * 进程+音频流保留转后台(UI 冻结走 §10.5 SDK present 自抑,**非 SIGSTOP**—— * SIGSTOP 会冻结同进程播放线程断供 PCM,§10.5 真机实证)。登记失败 *(槽满/已存在)回落下方真退序列。卡拔例外不适用保活(bg 表 tick 照杀)。 */ if (meow_bg_should_keep(a->background_audio == '1', meow_media_app_playing(a->id)) && meow_bg_add(a->id, pid, out_fd, app_on_card) == 0) { logmsg("bg: app %s nav-exit -> 后台保活(播放中,pid=%d 保留,SDK present 自抑)", a->name, (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; } /* M2.17 自动息屏(app 会话):任意物理键边沿重置计时;空闲>timeout 且非 keep_awake → 息屏; * 息屏后再按键 → 唤醒(fb_pan_buf0_force 兜底 LCDC 落 buffer0,内容随应用输入重绘)。 */ { 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(); if (meow_auto_slept) { logmsg("auto screen-off (app): key wake"); meow_panel_on(); meow_auto_slept = 0; fb_pan_buf0_force(); /* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用 */ if (lck_wake_should_lock()) run_app_lock_overlay(a, pid); } } 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) { logmsg("auto screen-off (app): touch wake"); meow_panel_on(); meow_auto_slept = 0; fb_pan_buf0_force(); /* M2.x 锁屏:App 前台唤醒后若需锁屏 → SIGSTOP 叠加锁屏页;解锁/SIGCONT 回应用 */ if (lck_wake_should_lock()) run_app_lock_overlay(a, pid); } } 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 `。 */ 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(); 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 天然续播。 */ int nfd = open(NOTIF_ACTIVE, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (nfd >= 0) { write(nfd, "1", 1); close(nfd); } 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 本体栈内存活;回前台时裸态重建) */ return 0; } /* §10.5:App 前台会话结束,清会话标记(ipc_dispatch 不再受理 MSG_NOTIF_OPEN) */ g_app_session_active = 0; meow_notif_open_clear(); /* 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:前台应用会话结束,清导航栈 */ /* 阶段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); }