Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/app_launcher.c
T
MeowOS 3f57d7fd4f feat(sec): 二期签名鉴权 2A 最小闭环 (agent-185) — meow_verify(Ed25519验签CLI,公钥固化,TweetNaCl+SHA256单文件) + meow_sign(宿主keygen/sign,尾部180B footer签名块,私钥仅.keys/不入库) + nyai/meow_pkg_install 验签分级(有签必验/os.meow.*无签拒装/第三方直装告警/三重绑定/原签存.mpk.src导出保留) + svc_power IPC鉴权(SO_PEERCRED→/proc/pid/exe→包名/系统身份allowlist,系统动作仅os.meow.*或系统身份) + cmd_upgrade同步签名分级防绕过
meow_verify 37e50606; nyai f2938d28; svc_power e0494eb7; launcher 24c9ee5d(含热重载)
宿主验证12项全过(正例/篡负载/篡包名/无签系统包拒/第三方直装/降级拒/导出round-trip byte-identical/verify缺失宁缺勿装/footer容忍/E2E PERM/鉴权分类)
出入: TweetNaCl替orlp/ed25519(任务允许同等实现); 读级暂仍uid==0(用户随后定案放宽到任意调用者,待改); 系统身份allowlist为启发式
2026-08-31 04:33:46 +08:00

1133 lines
54 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* app_launcher.c — MeowOS launcher 拆分模块(LAUNCHER_SPLIT_PLAN 拆分)。*/
/* 本文件由拆分工具从 meow_launcherd.c 按职责搬移;逻辑与原文 0-diff(仅 static 可见性按跨模块需求调整)。*/
#include "meow_launcher_internal.h"
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.<id>=1 / route.<id>.keep_awake=1;运行时同类)。
* 主界面(无上报/空)= 非 keep_awake(主页空闲照常超时息屏)。单一写者=主循环(IPC 排空处)。 */
static char g_fg_app[MEOW_APP_ID_MAX] = "";
static char g_fg_act[MEOW_ACT_MAX] = "";
void meow_fg_act_set(const char *app_id, const char *act)
{
if (!app_id) return;
strncpy(g_fg_app, app_id, MEOW_APP_ID_MAX - 1);
g_fg_app[MEOW_APP_ID_MAX - 1] = 0;
if (act) { strncpy(g_fg_act, act, MEOW_ACT_MAX - 1); g_fg_act[MEOW_ACT_MAX - 1] = 0; }
else g_fg_act[0] = 0;
logmsg("keep_awake: fg_act app=%s act=%s", g_fg_app, g_fg_act);
}
void meow_fg_act_back(void)
{
g_fg_app[0] = 0;
g_fg_act[0] = 0;
logmsg("keep_awake: fg_act back to home (cleared)");
}
/* 前台当前 activity 是否 keep_awake(无上报/未注册/未声明=0,即照常超时息屏)。 */
int fg_keep_awake(void)
{
if (!g_fg_app[0] || !g_fg_act[0]) return 0;
struct meow_acct *a = meow_acct_find(g_fg_app);
if (!a) return 0;
for (int i = 0; i < a->nroute; i++)
if (!strcmp(a->routes[i].act, g_fg_act)) return a->routes[i].keep_awake;
return 0;
}
/* 确保联网:wlan0 未 up 且尚无 wpa_supplicant 在跑 → 调用 /usr/bin/wifi_up.sh 连保存网络。
* 幂等:连上 / 已有 wpa_supplicant 则不动;每轮询重试,不阻塞主界面。
* M2.39:尊重 network.wifi_enabled —— 用户在设置页关 WiFi(=0)则不自动拉起(否则与
* 手动关冲突);取值失败(未 init)保默认 1=允许自动连网。 */
void ensure_wifi(void)
{
int enabled = 1;
if (meow_settings_get_bool("network.wifi_enabled", &enabled) == 0 && !enabled) return;
if (wlan_connected()) return;
if (proc_by_comm("wpa_supplicant")) return;
logmsg("wifi not up -> /usr/bin/wifi_up.sh");
system("PATH=/bin:/sbin:/usr/bin:/usr/sbin /usr/bin/wifi_up.sh");
}
/* 解析一个 app.json 流 → 填充 struct app 的 name/exec/icon/framework/routes/svcs。
* 极简:不做 JSON 校验,仅匹配 key 与路由/服务扁平行(JSON 或扁平皆可)。
* 不设 name/exec(由调用方从返回的 app 再复制),成功判定由调用方做。 */
void meow_parse_app_json(FILE *f, struct app *a)
{
char buf[256];
char name[17] = "", exec[129] = "", icon[129] = "";
char package[MEOW_APP_ID_MAX] = "";
int fram = 0, immersive = 0;
int fullscreen = 0; /* app.json fullscreen=<true|1>(注入 MEOW_FULLSCREEN env,SDK 预先设全屏) */
int fonts = 0, fonts_set = 0; /* app.json "fonts":<档>(0=默认4K常用 1=大字库GBK 2=Full;缺省 0) */
struct app tmp;
memset(&tmp, 0, sizeof tmp);
while (fgets(buf, sizeof buf, f)) {
if (!name[0]) json_str(buf, "name", name, sizeof name);
if (!exec[0]) json_str(buf, "exec", exec, sizeof exec);
if (!icon[0]) json_str(buf, "icon", icon, sizeof icon);
if (!package[0]) json_str(buf, "package", package, sizeof package); /* 安卓化权威 app_id */
/* route.<id>=<chain>(activity 路由,M1) */
char *rp = strstr(buf, "route.");
if (rp && tmp.nroute < MEOW_MAX_ROUTES) {
rp += 6; /* 跳过 "route." */
int idl = 0; char id[32] = ""; /* 读满整键(识别 ".fullscreen" 后缀,cap 31) */
while (*rp && *rp != '"' && *rp != ':' && *rp != '=' && idl < 31)
id[idl++] = *rp++;
id[idl] = 0;
if (idl > 0) {
char *vp = rp;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
/* route.<id>.fullscreen=<on>:活动级全屏声明(M1 顺带;M3 活动切换时消费)。
* 命中既有 route.<id> 置 fullscreen;不新建 route(避免 id 被截断误建 "…fulls")。 */
if (idl >= 11 && !strcmp(id + idl - 11, ".fullscreen")) {
id[idl - 11] = 0; /* 去掉 ".fullscreen" 还原 act */
char v[8] = ""; int k = 0;
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && k < 7) v[k++] = *vp++;
v[k] = 0;
int on = (!strcmp(v, "1") || !strcmp(v, "true") || !strcmp(v, "yes"));
for (int i = 0; i < tmp.nroute; i++)
if (!strcmp(tmp.routes[i].act, id)) { tmp.routes[i].fullscreen = on; break; }
} else {
int vl = 0; char chain[MEOW_CHAIN_MAX] = "";
while (*vp && *vp != '"' && *vp != ',' && *vp != '\n' &&
*vp != '\r' && vl < MEOW_CHAIN_MAX - 1)
chain[vl++] = *vp++;
chain[vl] = 0;
while (vl > 0 && (chain[vl - 1] == ' ' || chain[vl - 1] == '\t'))
chain[--vl] = 0;
strncpy(tmp.routes[tmp.nroute].act, id, 15);
tmp.routes[tmp.nroute].act[15] = 0; /* 显式收尾(原依赖 id 读长 ≤15) */
strncpy(tmp.routes[tmp.nroute].chain, chain, MEOW_CHAIN_MAX - 1);
tmp.nroute++;
}
}
}
/* awake.<id>=1(M2.17 keep_awake:该 activity 常亮,超时不自动息屏)。
* 命中的既有 route.<id> 置 keep_awake;无 route 则新建单 activity 路由。 */
char *ap = strstr(buf, "awake.");
if (ap && tmp.nroute < MEOW_MAX_ROUTES) {
ap += 6; /* 跳过 "awake." */
int al = 0; char aid[16] = "";
while (*ap && *ap != '"' && *ap != ':' && *ap != '=' && al < 15)
aid[al++] = *ap++;
aid[al] = 0;
if (al > 0) {
char *vp = ap;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
int on = atoi(vp);
int ri = -1;
for (int i = 0; i < tmp.nroute; i++)
if (!strcmp(tmp.routes[i].act, aid)) { ri = i; break; }
if (ri < 0) {
ri = tmp.nroute++;
strncpy(tmp.routes[ri].act, aid, 15);
strncpy(tmp.routes[ri].chain, aid, MEOW_CHAIN_MAX - 1); /* 单 activity 兜底链路 */
}
tmp.routes[ri].keep_awake = (on != 0);
}
}
/* framework=true(看门狗返回发 SIGUSR1,M1 解析存储) */
if (!fram && strstr(buf, "framework")) {
char v[8] = "";
if (json_str(buf, "framework", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
fram = 1;
}
}
/* immersive=true(隐藏虚拟返回键、返回手势移交 SDK;写 /tmp/meowapp_state) */
if (!immersive && strstr(buf, "immersive")) {
char v[8] = "";
if (json_str(buf, "immersive", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
immersive = 1;
}
}
/* fullscreen=true(app 级:注入 MEOW_FULLSCREEN=1,SDK 端 meow_ui_init 预先设全屏)。
* 与 immersive 是两回事:fullscreen 隐藏状态栏/沉浸,immersive 关虚拟返回键。 */
if (!fullscreen && strstr(buf, "fullscreen")) {
char v[8] = "";
if (json_str(buf, "fullscreen", v, sizeof v) > 0) {
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
fullscreen = 1;
}
}
/* "fonts":<档>(字库档:0=默认4K常用 1=大字库GBK 2=Full;可选,缺省 0)。
* launcher 注入 MEOW_FONT_TIER;大字库装载由后续 font 代理实现。 */
if (!fonts_set && strstr(buf, "fonts")) {
char v[8] = "";
if (json_str(buf, "fonts", v, sizeof v) > 0) {
if (!strcmp(v, "common") || !strcmp(v, "0")) fonts = 0;
else if (!strcmp(v, "gbk") || !strcmp(v, "1")) fonts = 1;
else if (!strcmp(v, "full") || !strcmp(v, "2")) fonts = 2;
else fonts = atoi(v); /* 数字兜底(越界收敛到 [0,2]) */
if (fonts < 0) fonts = 0;
if (fonts > 2) fonts = 2;
fonts_set = 1;
}
}
/* service.<svc>=mode:restart:max:window_ms(M1 解析存储,保活 M4) */
char *sp = strstr(buf, "service.");
if (sp && tmp.nsvc < MEOW_MAX_SVCS) {
sp += 8; /* 跳过 "service." */
int sl = 0; char nm[16] = "";
while (*sp && *sp != '"' && *sp != ':' && *sp != '=' && sl < 15)
nm[sl++] = *sp++;
nm[sl] = 0;
if (sl > 0) {
char *vp = sp;
while (*vp && *vp != ':' && *vp != '=') vp++;
if (*vp == ':' || *vp == '=') vp++;
while (*vp == ' ' || *vp == '\t') vp++;
if (*vp == '"') vp++;
int m = 0, r = 0, mx = 0, w = 0;
sscanf(vp, "%d:%d:%d:%d", &m, &r, &mx, &w);
strncpy(tmp.svcs[tmp.nsvc].name, nm, 15);
tmp.svcs[tmp.nsvc].mode = m;
tmp.svcs[tmp.nsvc].restart = r;
tmp.svcs[tmp.nsvc].max = mx;
tmp.svcs[tmp.nsvc].window_ms = w;
tmp.nsvc++;
}
}
}
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/<pkg>/<binary> → <pkg> */
static int meow_pkg_name(const char *exec, char *out, int n)
{
if (!exec || strncmp(exec, MEOWAPPS, strlen(MEOWAPPS)) != 0) { if (n>0) out[0]=0; return -1; }
const char *p = exec + strlen(MEOWAPPS);
if (*p != '/') { if (n>0) out[0]=0; return -1; }
p++;
const char *end = strchr(p, '/');
int len = end ? (int)(end - p) : (int)strlen(p);
if (len <= 0 || len >= n) { if (n>0) out[0]=0; return -1; }
memcpy(out, p, len); out[len] = 0;
return 0;
}
/* 启动应用前准备数据目录:建 MeowData/<pkg>/{data,cache}(如缺)。幂等。
* 失败只 log(MeowData 所在卡不存在/只读),绝不崩 —— 零卡依赖:应用需按
* APPS.md 规范容忍无数据目录。返回 0=就绪,-1=pkg 不在卡上/不能判名。 */
static int meow_app_prepare(const char *exec)
{
char pkg[64];
if (meow_pkg_name(exec, pkg, sizeof pkg) != 0) return -1;
if (!sd_device_present()) return -1; /* 卡不在,跳过(应用仍可跑非卡资源) */
char cmd[320];
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
int rc = system(cmd);
logmsg("app_prepare: pkg=%s rc=%d", pkg, rc);
return rc == 0 ? 0 : -1;
}
/* ---- II 期签名校验 / 导出保留签名(MPK_SIGNATURE_DESIGN §5.1/§5.4,shell-out meow_verify) ---- */
/* meow_verify --system <mpk> --package <pkg>。返回 0=有效签 /1=有签不通过 /2=无签名 /127=工具不可用。 */
static int pkg_verify_sig(const char *mpk, const char *pkg)
{
const char *vb = getenv("MEOW_VERIFY_BIN");
if (!vb || !*vb) vb = "/usr/bin/meow_verify";
char cmd[700];
snprintf(cmd, sizeof cmd, "'%s' --system '%s' --package '%s' >/dev/null 2>&1", vb, mpk, pkg);
int rc = system(cmd);
if (rc == -1) return 127;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return 127;
}
/* 读已解压 app.json 的某字符串字段(用 launcher 的 json_str;返回 0 成功)。 */
static int pkg_json_field(const char *appjs, const char *key, char *out, int n)
{
if (!appjs || !key || n <= 0) return -1;
FILE *f = fopen(appjs, "r");
if (!f) return -1;
static char j[16384];
size_t r = fread(j, 1, sizeof j - 1, f);
fclose(f);
j[r] = 0;
if (!r) return -1;
if (json_str(j, key, out, n) <= 0) { out[0] = 0; return -1; }
return 0;
}
/* 读 .mpk 内 app.json 的某字符串字段(gzip -dc | tar -xO app.json;返回 0 成功)。 */
static int pkg_json_field_mpk(const char *mpk, const char *key, char *out, int n)
{
char cmd[700];
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -xO app.json 2>/dev/null", mpk);
FILE *pf = popen(cmd, "r");
if (!pf) return -1;
static char j[16384];
size_t r = fread(j, 1, sizeof j - 1, pf);
pclose(pf);
if (!r) return -1;
j[r] = 0;
if (json_str(j, key, out, n) <= 0) { out[0] = 0; return -1; }
return 0;
}
/* 安装成功后,把原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名)。 */
static void pkg_save_src(const char *mpk, const char *pkg)
{
char dst[320], cmd[720];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' 2>/dev/null && "
"cp '%s' '%s' 2>/dev/null", pkg, mpk, dst);
if (system(cmd) != 0) logmsg("pkg_install: save .mpk.src fail for %s", pkg);
}
/* 导出保留签名:缓存 .mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装 → 直接复制(不二次签名)。
* 成功 0;否则 -1(调用方现场重打包,无签名)。 */
static int pkg_export_preserve_sig(const char *pkg, const char *dst)
{
char srcp[320], instj[280], cmd[760], tmp[340];
snprintf(srcp, sizeof srcp, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg);
if (access(srcp, R_OK) != 0) return -1;
if (pkg_verify_sig(srcp, pkg) != 0) { logmsg("pkg_export: cached .mpk.src sig fail %s", pkg); return -1; }
char cv[64], iv[64];
snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(instj, "version", iv, sizeof iv) != 0) return -1;
if (pkg_json_field_mpk(srcp, "version", cv, sizeof cv) != 0) return -1;
if (strcmp(iv, cv) != 0) { logmsg("pkg_export: cached version %s != installed %s", cv, iv); return -1; }
snprintf(tmp, sizeof tmp, "%s.tmp", dst);
snprintf(cmd, sizeof cmd, "cp '%s' '%s' 2>/dev/null", srcp, tmp);
if (system(cmd) != 0 || access(tmp, R_OK) != 0) return -1;
if (rename(tmp, dst) != 0) { unlink(tmp); return -1; }
return 0;
}
/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs/<pkg>/。
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{data,cache})。
* 返回 0 ok / 负=错误。 */
int meow_pkg_install(const char *mpk_path)
{
if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; }
if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; }
if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; }
/* 包名 = 文件名茎(去掉 .mpk) */
const char *bn = strrchr(mpk_path, '/');
bn = bn ? bn + 1 : mpk_path;
const char *dot = strrchr(bn, '.');
char pkg[64];
if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need <pkg>.mpk"); return -1; }
int len = (int)(dot - bn);
if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; }
memcpy(pkg, bn, len); pkg[len] = 0;
/* 目标 MeowAPPs/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 app.json */
char dst[256], bak[256], cmd[760];
snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid());
int replacing = (access(dst, F_OK) == 0);
/* II 期:签名分级先于解包(MPK_SIGNATURE_DESIGN §5.2:先验签再解包,失败不落盘)。
* 有签名必须验签(失败拒装);os.meow.* 必须有有效签名;无签名第三方放行(告警)。 */
int vr = pkg_verify_sig(mpk_path, pkg);
int sysp = (strncmp(pkg, "os.meow.", 8) == 0);
if (vr == 1) { logmsg("pkg_install: sig invalid, reject %s", pkg); return -3; }
else if (vr == 2) {
if (sysp) { logmsg("pkg_install: sys pkg %s must be signed, rejected", pkg); return -3; }
logmsg("pkg_install: %s unsigned (3rd party allowed)", pkg);
}
else if (vr != 0) {
if (sysp) { logmsg("pkg_install: meow_verify unavailable rc=%d, sys pkg reject", vr); return -3; }
logmsg("pkg_install: meow_verify unavailable rc=%d, %s unverified (3rd party)", vr, pkg);
}
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
snprintf(cmd, sizeof cmd,
"rm -rf '%s' 2>/dev/null; "
"[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; "
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
/* II 期:app.json.package 绑定(== 包名,禁改包名/目录改名;防第三方面具系统包)。 */
{
char appj[280], ph[64];
snprintf(appj, sizeof appj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
if (pkg_json_field(appj, "package", ph, sizeof ph) == 0 && strcmp(ph, pkg) != 0) {
logmsg("pkg_install: app.json.package %s != %s, reject", ph, pkg);
char rb[760];
snprintf(rb, sizeof rb, "rm -rf '%s' 2>/dev/null; [ -d '%s' ] && mv '%s' '%s' 2>/dev/null",
dst, bak, bak, dst);
system(rb);
return -3;
}
}
/* 数据 init:MeowData/<pkg>/{data,cache} */
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
system(cmd);
/* 有效签 → 原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
if (vr == 0) pkg_save_src(mpk_path, pkg);
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
return 0;
}
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
int meow_pkg_export(const char *pkg, const char *out_path)
{
if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) {
logmsg("pkg_export: unsafe path"); return -2;
}
if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; }
char src[256], dst[256];
snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; }
snprintf(dst, sizeof dst, "%s", out_path);
/* II 期:导出保留签名 —— 若安装时存了原签 .mpk.src 且其版本==已装 → 直接复制(不二次签名)。 */
if (pkg_export_preserve_sig(pkg, dst) == 0) {
logmsg("pkg_export: %s -> %s (signed, preserved)", pkg, dst);
return 0;
}
/* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */
char cmd[512];
snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && "
"mv '%s.tmp' '%s'", src, dst, dst, dst);
if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; }
logmsg("pkg_export: %s -> %s", pkg, dst);
return 0;
}
/* returns 0 on success (app ran and exited), -1 on launch failure */
/* GESTURE_BACK §2.4:把 app.json 的 immersive 声明写进 /tmp/meowapp_state,
* watchdog 据此决定是否画虚拟返回键 / 走 immersive 手势(watchdog 每 100ms 读一次)。
* immersive=1→"1";0→"0";<0→清空(应用退出,恢复默认非沉浸)。 */
static void meow_write_app_state(int immersive)
{
if (immersive < 0) { unlink("/tmp/meowapp_state"); return; }
int fd = open("/tmp/meowapp_state", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) return;
if (write(fd, immersive ? "1" : "0", 1) < 0) { /* ignore write error */ }
close(fd);
}
/* 读 App 顶条模式:0=展开 1=收合 2=隐藏;-1=无文件/读取失败(按展开处理,默认安全)。 */
static int app_sbar_mode_read(void)
{
int fd = open(MEOW_SBAR_MODE_FILE, O_RDONLY);
if (fd < 0) return -1;
char ch[8];
int n = read(fd, ch, sizeof ch - 1);
close(fd);
if (n <= 0) return -1;
if (ch[0] == '1') return 1;
if (ch[0] == '2') return 2;
return 0;
}
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/<pkg>/{data,cache}(幂等;卡不在/失败只 log 不崩)。
* 应用经环境变量 MEOW_DATA_DIR/MEOW_CACHE_DIR 定位数据(exFAT 无 symlink)。 */
meow_app_prepare(a->exec);
/* save the brightness the player had before the menu/app session, then
* drain input queues so the app starts clean */
saved_brightness = read_brightness();
if (saved_brightness <= 0) saved_brightness = 60; /* standby/unknown */
logmsg("saved brightness = %d", saved_brightness);
drain_input();
/* Bug 3: distinguish a REAL exec failure from a clean app exit. The child
* holds the write end of a CLOEXEC pipe; if execve() fails it writes the
* errno and _exit(127), if it succeeds the fd closes on exec so the parent
* read() returns EOF. Parent blocks on read() until the child either execs
* (success) or reports the error (failure); only then do we spawn the
* watchdog and waitpid — so an exec failure never leaves an orphan watchdog
* and never masquerades as "app ran and exited". */
int execpipe[2];
if (pipe(execpipe) < 0) { logmsg("exec pipe failed: %s", strerror(errno)); return -1; }
fcntl(execpipe[1], F_SETFD, FD_CLOEXEC);
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/<app_id>.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.<id>.fullscreen
* (meow_route.fullscreen)在 M3 活动切换时由后续代理消费(此处暂不下发)。
* 未声明则不注入(SDK 默认非全屏)。 */
if (a->fullscreen) {
snprintf(env_fs, sizeof env_fs, "MEOW_FULLSCREEN=1");
envp[ne++] = env_fs;
}
char pkg[64];
if (meow_pkg_name(a->exec, pkg, sizeof pkg) == 0) {
snprintf(env_data, sizeof env_data, "MEOW_DATA_DIR=/usr/data/mnt/sd_0/MeowData/%s/data", pkg);
snprintf(env_cache, sizeof env_cache, "MEOW_CACHE_DIR=/usr/data/mnt/sd_0/MeowData/%s/cache", pkg);
envp[ne++] = env_data;
envp[ne++] = env_cache;
}
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;
}