feat(launcher): M4 后台服务保活+后台播放(FRAMEWORK_DESIGN §5/§9 落地)

- app.json 新字段 background/background_audio 解析入 struct app/meow_acct 注册表
- meow_ipc.h:MSG_REGISTER(m_register_t)/SERVICE_OK(m_ack_t.status=pid)/SERVICE_STOP/
  MSG_SYS(m_sys_t, SHUTDOWN=1/SVC_DEAD=2) 负载结构落地
- 新模块 meow_bgplay.c/h(自包含+ops 注入,仿 meow_media,宿主可单测):
  · 后台播放保活——声明 background_audio 且媒体会话 PLAYING 的 App nav-exit 不终止,
    进程+音频流保留;UI 冻结走 §10.5 /tmp/meow_notif_active SDK present 自抑(非字面
    SIGSTOP:冻结同进程播放线程断供 PCM,§10.5 真机实证);点图标/媒体卡片回前台=
    原进程恢复(chain/picker 忽略),停播后再退出=真退 SIGTERM
  · 无头后台服务——MSG_REGISTER spawn/stop/reap + 退避重启 1/2/4/10s cap +
    max_restart 熔断(清该 App 通知 + 注册连接广播 MSG_SYS{SVC_DEAD},重注册复位)
  · 卡拔例外(卡上二进制后台 App/服务仍 SIGTERM 防 SIGBUS)、崩溃照收、
    关机前 meow_bgplay_kill_all 有界回收(含 MSG_SYS{SHUTDOWN} 广播)
- util.c:MSG_REGISTER/MSG_SERVICE_STOP 分发(权限/声明校验链)+ ipc_conn_alive 连接
  存活查询 + meow_ipc_process 末尾挂 meow_bg_housekeep(drain/reap/退避 tick)
- app_launcher.c:launch_app_chain 入口 resume(bg_take 原进程回前台)+ nav-exit
  保活分支 + 转后台出口(杀看门狗/写自抑标记/复位顶条,跳过退出尾)+ 冷启动自动
  拉起静态 keepalive(mode=0) 服务(exec 路径约定=App 同目录 <svc> 文件)
- main_menu.c:应用列表「·后台」标注(宫格/列表,1s tick 按 change_seq 刷新)+
  两条退出尾 kill_all + unlink NOTIF_ACTIVE
- 宿主单测 test_bgplay.c 50 项全过(真 fork 子进程走状态机 + 假时钟退避熔断);
  test_media/test_notif_panel/test_notif_tf/test_vol_ramp_jack 回归全过;
  zig-mipsel-cc -Wall -Wextra 0 warning。launcher md5=c07d011c0769fca14d3bfb752d735463
This commit is contained in:
MeowOS committed 2026-09-01 18:09:09 +08:00
1 parent 7db8197f9c
commit 5aabede0cd
13 files changed
+1461 -40

No files matched your search

+257 -26
View File
@@ -89,20 +89,27 @@ static void ams_report_launch(const struct app *a, const char *chain, pid_t pid)
if (l > 0) ams_send(line, l);
}
/* exit 上报:kind(exit|sig)/code/dur_ms。异常退出由 amsd 判 crash + 转发 meow_crash。 */
static void ams_report_exit(const struct app *a, int status, long long dur_ms)
/* 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 (!a) return;
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",
a->id, kind, code, dur_ms, (long)time(NULL), ams_uptime());
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;
@@ -119,17 +126,17 @@ static void ams_out_flush(void)
}
/* 追加一行(line 为无 \n 的 NUL 串)到 flush 缓冲;满了先 flush。 */
static void ams_out_line(const struct app *a, pid_t pid, const char *line)
static void ams_out_line(const char *app_id, pid_t pid, const char *line)
{
if (!a || !line) return;
if (!app_id || !line) return;
int need = snprintf(NULL, 0, "EV OUT %s %d %ld %ld %s\n",
a->id, (int)pid, (long)time(NULL), ams_uptime(), line);
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",
a->id, (int)pid, (long)time(NULL), ams_uptime(), line);
app_id, (int)pid, (long)time(NULL), ams_uptime(), line);
}
/* 单次排空上限:App 被 OOM/信号杀后,其子孙进程可能仍继承持有 out_pipe 写端
@@ -143,8 +150,11 @@ static void ams_out_line(const struct app *a, pid_t pid, const char *line)
#define EXEC_CONFIRM_MS 2000
/* 排空读端尽可能多的字节,按行切分转发;超长行截断标记。有界:单次调用最多
* 读 AMS_DRAIN_MAX 字节(见上)。读端已 O_NONBLOCK;App 死且写端全关 → read 返 0=EOF。 */
static void ams_drain_stdout(const struct app *a, int fd, pid_t pid)
* 读 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;
@@ -155,19 +165,19 @@ static void ams_drain_stdout(const struct app *a, int fd, pid_t pid)
for (ssize_t i = 0; i < n; i++) {
char c = tmp[i];
if (c == '\n') {
g_ams_out_line[g_ams_out_n] = '\0';
ams_out_line(a, pid, g_ams_out_line);
g_ams_out_n = 0;
linebuf[*line_n] = '\0';
ams_out_line(app_id, pid, linebuf);
*line_n = 0;
} else if (c == '\r') {
/* 去尾 \r */
} else if (g_ams_out_n < (int)sizeof g_ams_out_line - 1) {
g_ams_out_line[g_ams_out_n++] = c;
} else if (*line_n < 1024 - 1) {
linebuf[(*line_n)++] = c;
} else {
/* 超长行:截断标记 + 重置(当前字符丢弃) */
g_ams_out_line[ (int)sizeof g_ams_out_line - 2 ] = '\0';
ams_out_line(a, pid, g_ams_out_line);
ams_out_line(a, pid, "[...trunc]");
g_ams_out_n = 0;
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; /* 有界:孤儿持续写也不卡死主循环 */
@@ -311,6 +321,8 @@ static void meow_acct_sync(const char *app_id, const struct app *ap)
struct meow_acct *a = meow_acct_upsert(app_id);
if (!a) return;
a->framework = ap->framework;
a->background = ap->background; /* M4 */
a->background_audio = ap->background_audio;
strncpy(a->icon, ap->icon, 128);
strncpy(a->name, ap->name, 16);
strncpy(a->exec, ap->exec, 128);
@@ -361,6 +373,119 @@ void meow_acct_declare(const char *app_id, const char *entries, int count)
}
/* ---- 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)
{
@@ -525,6 +650,8 @@ void meow_parse_app_json(FILE *f, struct app *a)
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)) {
@@ -638,6 +765,24 @@ void meow_parse_app_json(FILE *f, struct app *a)
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) {
@@ -670,6 +815,8 @@ void meow_parse_app_json(FILE *f, struct app *a)
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);
@@ -1295,6 +1442,25 @@ static int launch_app_chain(struct app *a, const char *chain)
g_ams_pick_armed = 0;
}
if (!a->ok) { logmsg("launch blocked: exec not ok"); return -1; }
/* 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) {
meow_nav_begin(a->id, chain); /* M3:冷启动/裸启动都以当前链深初始化导航栈(chain 空→depth 0) */
/* M2.12:启动前准备 MeowData/<pkg>/{data,cache}(幂等;卡不在/失败只 log 不崩)。
@@ -1331,7 +1497,7 @@ static int launch_app_chain(struct app *a, const char *chain)
fcntl(out_pipe[1], F_SETFD, FD_CLOEXEC);
}
pid_t pid = fork();
pid = fork();
if (pid < 0) {
close(execpipe[0]); close(execpipe[1]);
if (out_pipe[0] >= 0) close(out_pipe[0]);
@@ -1494,7 +1660,7 @@ static int launch_app_chain(struct app *a, const char *chain)
}
close(execpipe[1]);
if (out_pipe[1] >= 0) close(out_pipe[1]); /* parent: 关闭写端 */
int out_fd = out_pipe[0]; /* 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);
@@ -1527,13 +1693,35 @@ static int launch_app_chain(struct app *a, const char *chain)
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 捕获会话。 */
ams_report_launch(a, chain, pid);
/* 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;
@@ -1580,6 +1768,7 @@ static int launch_app_chain(struct app *a, const char *chain)
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;
@@ -1621,7 +1810,7 @@ static int launch_app_chain(struct app *a, const char *chain)
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, out_fd, pid);
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();
@@ -1635,6 +1824,18 @@ static int launch_app_chain(struct app *a, const char *chain)
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);
@@ -1791,6 +1992,36 @@ static int launch_app_chain(struct app *a, const char *chain)
* 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();
@@ -1798,10 +2029,10 @@ static int launch_app_chain(struct app *a, const char *chain)
* 先转发 amsd,再报 EXIT / crash。仅当有读端(管道建立成功)才 drain。 */
if (out_fd >= 0) {
g_ams_out_had = 0;
ams_drain_stdout(a, out_fd, pid); /* 排空全部剩余(App 死 → read 返回 0=EOF) */
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, pid, g_ams_out_line);
ams_out_line(a->id, pid, g_ams_out_line);
g_ams_out_n = 0;
}
ams_out_flush();