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