1163 lines
55 KiB
C
1163 lines
55 KiB
C
/* 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);
|
||
}
|
||
|
||
/* 防降级门禁(与 nyai 同规范,APPS.md §3.8):仅按 version_code 比较。
|
||
* 新版 code < 已装 → 拒;相等 → 允许重装;任一侧缺 version_code → 拒。 */
|
||
if (replacing) {
|
||
char nc[32], oc[32];
|
||
long long ncode = -1, ocode = -1;
|
||
int nok = 0, ok2 = 0;
|
||
if (pkg_json_field_mpk(mpk_path, "version_code", nc, sizeof nc) == 0) {
|
||
char *ep; long long v = strtoll(nc, &ep, 10);
|
||
if (ep != nc && *ep == '\0') { ncode = v; nok = 1; }
|
||
}
|
||
char instj[280];
|
||
snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
|
||
if (pkg_json_field(instj, "version_code", oc, sizeof oc) == 0) {
|
||
char *ep; long long v = strtoll(oc, &ep, 10);
|
||
if (ep != oc && *ep == '\0') { ocode = v; ok2 = 1; }
|
||
}
|
||
if (!nok || !ok2) { logmsg("pkg_install: %s version_code missing, reject", pkg); return -4; }
|
||
if (ncode < ocode) { logmsg("pkg_install: downgrade %s new %lld < old %lld, reject", pkg, ncode, ocode); return -4; }
|
||
}
|
||
|
||
/* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */
|
||
snprintf(cmd, sizeof cmd,
|
||
"rm -rf '%s' 2>/dev/null; "
|
||
"[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; "
|
||
"mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && "
|
||
"[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst);
|
||
if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; }
|
||
|
||
/* II 期:app.json.package 绑定(== 包名,禁改包名/目录改名;防第三方面具系统包)。 */
|
||
{
|
||
char appj[280], ph[64];
|
||
snprintf(appj, sizeof appj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg);
|
||
if (pkg_json_field(appj, "package", ph, sizeof ph) == 0 && strcmp(ph, pkg) != 0) {
|
||
logmsg("pkg_install: app.json.package %s != %s, reject", ph, pkg);
|
||
char rb[760];
|
||
snprintf(rb, sizeof rb, "rm -rf '%s' 2>/dev/null; [ -d '%s' ] && mv '%s' '%s' 2>/dev/null",
|
||
dst, bak, bak, dst);
|
||
system(rb);
|
||
return -3;
|
||
}
|
||
}
|
||
|
||
/* 数据 init:MeowData/<pkg>/{data,cache} */
|
||
snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' "
|
||
"'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg);
|
||
system(cmd);
|
||
/* 有效签 → 原签 .mpk 另存 MeowData/<pkg>/data/<pkg>.mpk.src(导出保留签名) */
|
||
if (vr == 0) pkg_save_src(mpk_path, pkg);
|
||
logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak);
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* .mpk 导出:打包 MeowAPPs/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
|
||
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
|
||
int meow_pkg_export(const char *pkg, const char *out_path)
|
||
{
|
||
if (!pkg || !out_path || strpbrk(pkg, " ;`$'\"&|<>/" ) || strpbrk(out_path, " ;`$'\"&|<>")) {
|
||
logmsg("pkg_export: unsafe path"); return -2;
|
||
}
|
||
if (!sd_mounted()) { logmsg("pkg_export: card not mounted"); return -1; }
|
||
char src[256], dst[256];
|
||
snprintf(src, sizeof src, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg);
|
||
if (access(src, R_OK) != 0) { logmsg("pkg_export: no such pkg"); return -1; }
|
||
snprintf(dst, sizeof dst, "%s", out_path);
|
||
/* II 期:导出保留签名 —— 若安装时存了原签 .mpk.src 且其版本==已装 → 直接复制(不二次签名)。 */
|
||
if (pkg_export_preserve_sig(pkg, dst) == 0) {
|
||
logmsg("pkg_export: %s -> %s (signed, preserved)", pkg, dst);
|
||
return 0;
|
||
}
|
||
/* BusyBox tar 无 -z:tar -c | gzip -c > out(先写 .tmp 再 mv,防半成品)。 */
|
||
char cmd[512];
|
||
snprintf(cmd, sizeof cmd, "tar -c -C '%s' . | gzip -c > '%s.tmp' 2>/dev/null && "
|
||
"mv '%s.tmp' '%s'", src, dst, dst, dst);
|
||
if (system(cmd) != 0) { logmsg("pkg_export: tar/gzip fail"); return -1; }
|
||
logmsg("pkg_export: %s -> %s", pkg, dst);
|
||
return 0;
|
||
}
|
||
|
||
|
||
/* returns 0 on success (app ran and exited), -1 on launch failure */
|
||
/* GESTURE_BACK §2.4:把 app.json 的 immersive 声明写进 /tmp/meowapp_state,
|
||
* watchdog 据此决定是否画虚拟返回键 / 走 immersive 手势(watchdog 每 100ms 读一次)。
|
||
* immersive=1→"1";0→"0";<0→清空(应用退出,恢复默认非沉浸)。 */
|
||
static void meow_write_app_state(int immersive)
|
||
{
|
||
if (immersive < 0) { unlink("/tmp/meowapp_state"); return; }
|
||
int fd = open("/tmp/meowapp_state", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||
if (fd < 0) return;
|
||
if (write(fd, immersive ? "1" : "0", 1) < 0) { /* ignore write error */ }
|
||
close(fd);
|
||
}
|
||
|
||
|
||
/* 读 App 顶条模式:0=展开 1=收合 2=隐藏;-1=无文件/读取失败(按展开处理,默认安全)。 */
|
||
static int app_sbar_mode_read(void)
|
||
{
|
||
int fd = open(MEOW_SBAR_MODE_FILE, O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
char ch[8];
|
||
int n = read(fd, ch, sizeof ch - 1);
|
||
close(fd);
|
||
if (n <= 0) return -1;
|
||
if (ch[0] == '1') return 1;
|
||
if (ch[0] == '2') return 2;
|
||
return 0;
|
||
}
|
||
|
||
|
||
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 分离写 logs/<id>/<ts>_<pid>.log 并做级别过滤。
|
||
* M2.x 简化:MEOW_LOG_DIR 从 logs/apps/<id>/logs 改注入 logs/<id>(目录不存在则建)。 */
|
||
close(execpipe[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 *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";
|
||
/* M2.x 简化:MEOW_LOG_DIR 注入 logs/<id>(id=app_id,空则 "app",与 SDK app 分离前缀一致)。
|
||
* 目录不存在则用 meow_mkdir_p 建(幂等),失败不阻塞注入——SDK 侧还有 mkdir_p 兜底。 */
|
||
if (log_dir[0]) {
|
||
const char *logid = a->id[0] ? a->id : "app";
|
||
snprintf(app_log_dir, sizeof app_log_dir, "%s/%s", log_dir, logid);
|
||
meow_mkdir_p(app_log_dir);
|
||
snprintf(env_log, sizeof env_log, "MEOW_LOG_DIR=%s", app_log_dir);
|
||
envp[ne++] = env_log;
|
||
}
|
||
/* M2.52:MEOW_LOG_APP=app_id(=app.json.package,缺省回退目录名;与注册表一致;无 id 则跳过,SDK 兜底 "app") */
|
||
if (a->id[0]) { snprintf(env_app, sizeof env_app, "MEOW_LOG_APP=%s", a->id); envp[ne++] = env_app; }
|
||
/* M2.52:MEOW_LOG_LEVEL 读全局 log.level(error/warn/info/debug→索引0/1/2/3),
|
||
* SDK 只认大写字符串名(ERROR/WARN/INFO/DEBUG),故转大写注入;缺省兜底 info。 */
|
||
{
|
||
static const char *lvl_name[] = { "ERROR", "WARN", "INFO", "DEBUG" };
|
||
int lv = 2; /* 默认 info */
|
||
meow_settings_get_enum("log.level", &lv);
|
||
if (lv < 0 || lv > 3) lv = 2;
|
||
snprintf(env_lvl, sizeof env_lvl, "MEOW_LOG_LEVEL=%s", lvl_name[lv]);
|
||
envp[ne++] = env_lvl;
|
||
}
|
||
/* M2.x:注入「当前生效字体」= 系统 font.file(launcher 已 load 的那套),
|
||
* SDK meow_ui_gfx.load_font 优先读 MEOW_FONT_PNG/IDX,app 未自带字库(§5.7 load_handle
|
||
* 未实现)时跟随系统字体,不再硬编码 MeowAPPs/fonts。无卡/无可用字体则跳过注入,
|
||
* SDK 自行回退卡默认→固件→点阵。 */
|
||
{
|
||
static const char *cls_env_png[MEOW_FCLS_COUNT] = {
|
||
"MEOW_FONT_CLS_CJK_PNG", "MEOW_FONT_CLS_CJK_SYM_PNG", "MEOW_FONT_CLS_NUM_PNG",
|
||
"MEOW_FONT_CLS_LATIN_PNG", "MEOW_FONT_CLS_LATIN_SYM_PNG", "MEOW_FONT_CLS_OTHER_PNG" };
|
||
static const char *cls_env_idx[MEOW_FCLS_COUNT] = {
|
||
"MEOW_FONT_CLS_CJK_IDX", "MEOW_FONT_CLS_CJK_SYM_IDX", "MEOW_FONT_CLS_NUM_IDX",
|
||
"MEOW_FONT_CLS_LATIN_IDX", "MEOW_FONT_CLS_LATIN_SYM_IDX", "MEOW_FONT_CLS_OTHER_IDX" };
|
||
static const char *cls_keys[MEOW_FCLS_COUNT] = {
|
||
"font.class_cjk", "font.class_cjk_sym", "font.class_num",
|
||
"font.class_latin", "font.class_latin_sym", "font.class_other" };
|
||
char fpng[400], fidx[400], fam[96];
|
||
int have_global;
|
||
int mf_on = 0;
|
||
meow_settings_get_switch("display.multi_font", &mf_on);
|
||
snprintf(env_mf, sizeof env_mf, "MEOW_MULTI_FONT=%d", mf_on ? 1 : 0);
|
||
envp[ne++] = env_mf;
|
||
/* M2.5x:字库档 MEOW_FONT_TIER=<0|1|2> —— 按 app.json "fonts" 声明注入,并合并
|
||
* 「加载大字库」开关(font.load_full → 强制 ≥GBK)得**生效档**;0=默认4K常用,
|
||
* 1=大字库GBK,2=Full。SDK meow_ui_gfx 读入存档并按档装载(见
|
||
* meow_font_effective_paths_tiered;真正装载由 meow_font_reload_tier 实现)。 */
|
||
int eff_tier = meow_font_effective_tier(a->fonts);
|
||
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;
|
||
}
|
||
|