/* app_launcher.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/ /* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/ #include "meow_launcher_internal.h" static struct meow_acct g_accts[MEOW_MAX_ACTS]; /* 主循环单写者 */ int g_naccts = 0; /* ---- 阶段 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; 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; } } /* 链路深度 = ';' 分隔段数(根屏→目标屏的逻辑链长)。 */ 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; } /* ---- 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[17] = "", 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) */ 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; } } /* 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++; } } } 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 */ 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); } 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'; } 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; } 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); } 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) */ int launch_app(struct app *a) { logmsg("launch: %s (%s)", a->name, a->exec); if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; } /* 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); pid_t pid = fork(); if (pid < 0) { close(execpipe[0]); close(execpipe[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 分离写 apps/.log 并做级别过滤。 */ close(execpipe[0]); char *argv[] = { a->exec, NULL }; char env_path[320], env_log[320], env_data[320], env_cache[320]; 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 *envp[40]; /* PATH,HOME,log_dir,log_app,log_lvl,font 全局(3),多字体(1),tier(1),class(≤12),fullscreen(1),data,cache */ 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"; if (log_dir[0]) { snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", 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); 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; } 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]); int exe_err = 0; ssize_t er = read(execpipe[0], &exe_err, sizeof exe_err); close(execpipe[0]); if (er > 0) { logmsg("launch FAILED execve(%s): %s (errno %d)", a->exec, strerror(exe_err), exe_err); int st = 0; waitpid(pid, &st, 0); /* reap the failed child */ return -1; } /* GESTURE_BACK §2.4:应用启动即写 immersive 状态,watchdog 依此隐藏/保留虚拟返回键 */ meow_write_app_state(a->immersive == '1' ? 1 : 0); /* watchdog for virtual back key / gestures */ char wd1[192], wd2[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) { /* 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; 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); for (;;) { int r = waitpid(pid, &status, WNOHANG); if (r == pid) break; if (r < 0 && errno != EINTR) break; if (sigterm_flag) { logmsg("sigterm during app"); kill(pid, SIGTERM); 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); waitpid(pid, &status, 0); } 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(); } } 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(); } } 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; if (app_seq_hits >= 3) { logmsg("kill seq: VOL_DOWN x3 -> SIGTERM app"); vol_restore(app_vol0); kill(pid, SIGTERM); usleep(500000); kill(pid, SIGKILL); waitpid(pid, &status, 0); break; } } 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。 */ usleep(meow_ui_statusbar_animating() ? 20000 : 100000); } 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(); logmsg("app %s exited status=%d", a->name, WEXITSTATUS(status)); return 0; }