diff --git a/FRAMEWORK_DESIGN.md b/FRAMEWORK_DESIGN.md index 044fe32..ac0848a 100644 --- a/FRAMEWORK_DESIGN.md +++ b/FRAMEWORK_DESIGN.md @@ -1,6 +1,6 @@ # MeowOS 阶段 D:Activity / 通知 IPC 框架设计文档 -> 状态:协议/架构参考稿(2026-08-28 起草;**以下协议已随 M2.15-M2.96 各里程碑落地**,消息号以 `patch_v2/meow_launcherd/meow_ipc.h` 为准)。本阶段的「只设计、不实现」为起草期口径;M0-M3/M5/§10.5/媒体会话/amsd/meow_audiod(独占音频守护,规范 `devdocs/plan/AUDIOD_DESIGN.md`)已实现,M4 后台服务保活与 M6 加固待排期(见 §9 进度)。 +> 状态:协议/架构参考稿(2026-08-28 起草;**以下协议已随 M2.15-M2.96 各里程碑落地**,消息号以 `patch_v2/meow_launcherd/meow_ipc.h` 为准)。本阶段的「只设计、不实现」为起草期口径;M0-M3/M5/§10.5/媒体会话/amsd/meow_audiod(独占音频守护,规范 `devdocs/plan/AUDIOD_DESIGN.md`)已实现,**M4 后台服务保活+后台播放已落地(2026-09-01,见 §9 进度)**,M6 加固待排期。 > 前置:ANALYSIS.md §18(Launcher 全架构)、§11(播放器 unix socket 先例)、APPS.md、DEVELOPMENT_PLAN.md 阶段 0/D。 > 实现语言:启动器 C(musl 静态 MIPS32r2,zig-mipsel-cc);应用同为 C 静态 MIPS。 > 补充:本文件由 plan 子代理产出设计,主会话落盘,并附「后台运行/后台播放权限」(用户追加)。 @@ -117,7 +117,8 @@ M0 IPC 骨架(含 meow_ipc 测试钩子)→ M1 路由表+解析 → M2 通 ### 进度(阶段 D) - ✅ **M0 IPC 骨架**(0.7.10,M2.15):`meow_launcherd` 加 IPC 线程 + AF_UNIX `/usr/data/meow_ipc.sock`(unlink+bind+chmod0666+listen4)+ 帧解析(16B 头 magic=0x4D4F5701/version=1/type/req_id/len,LE)+ `MSG_HELLO/ACK/ERR` + eventfd 唤醒主循环 + 无锁 SPSC 内存环(`__sync`)+ 日志埋点。遵守 §1/§6.2:IPC 线程只读帧+解析+入队,主循环 `meow_ipc_process()` 单写者改状态,跨线程 eventfd,主循环 10ms 热键轮询每圈非阻塞排空(eventfd 失败时 10ms 轮询兜底)。测试钩子:`meow_ipc`(`patch_v2/meow_launcherd/meow_ipc.c`,静态 MIPS,与 evtool 同定位)+ launcher `--ipc-only N [--ipc-sock ]`。验证:`meow_ipc hello`→ACK、播放器不动(MeowOS 模式下无播放器即满足)。 - ✅ **M1 路由表 + 解析**(0.7.10,M2.15):`app.json` 扁平行 `route.=path` / `framework=true` / `service.=mode:restart:max:window_ms` / `icon` 解析入 `g_accts` 注册表(主循环单写者);`MSG_ACT_DECLARE`(运行时并集覆盖静态)/`MSG_ACT_QUERY`/`MSG_ACT_RESULT`(深度+链路);`resolve_chain(app,activity)` 返回 `[root..target]` 链路与深度——`route` 缺失(app 未声明路由)→ 深度 1 兜底 `chain=activity`;声明了路由但 activity 非 route 键 → `MEOW_ERR_NOENT`(activity 未注册)。验证:`meow_ipc query ` 返回链路(qemu + 真机 MeowOS 模式均通)。 -- ✅ **M2 通知存储+分发**、**M3 导航+前台会话改造+看门狗返回**、**M5 通知中心 UI 整合**(详见下,M2.65-M2.82 已落地);⬜ **M4 后台服务保活**(`MSG_REGISTER` 后台服务登记)、**M6 加固**:待排期。 +- ✅ **M2 通知存储+分发**、**M3 导航+前台会话改造+看门狗返回**、**M5 通知中心 UI 整合**(详见下,M2.65-M2.82 已落地);✅ **M4 后台服务保活**(2026-09-01 落地,见下);⬜ **M6 加固**:待排期。 + - **M4 已落地(2026-09-01)**:`app.json` 新增 `background`/`background_audio` 解析入注册表(`struct app`/`meow_acct` 同名字段);`MSG_REGISTER`/`MSG_SERVICE_OK`/`MSG_SERVICE_STOP`/`MSG_SYS` 负载结构落地(`m_register_t`/`m_service_stop_t`/`m_sys_t`,MEOW_SYS_SHUTDOWN=1/SVC_DEAD=2);新模块 `meow_bgplay.c/h`(自包含+ops 注入,宿主可单测):**后台播放保活**——声明 `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} 广播);主界面应用列表「·后台」标注(1s tick 按 change_seq 刷新);tick 挂载点=`meow_ipc_process` 末尾(主循环单写者排空点)。宿主单测 `test_bgplay.c` 50 项全过;amsd 侧 BG 状态展示=§6.3 顺延。 - **M3 已落地(M2.80,agent-202 · 85b4aea)**:`nav_stack`(≤16 深)+ `MSG_NAV` REPORT/EXIT + `launch_activity` 链路注入(env `MEOW_ACTIVITY`/`MEOW_CHAIN`)+ 栈根退出回主屏;通知点击按 `route_status`(0 拉起/1 NO ROUTE/2 SOURCE NOT FOUND/-1 NONE 关面板不动作);裸 App 兼容。 - **amsd(一期 agent-212 · 5fbf307 + 二期 agent-225)已落地**:活动管理器守护(svcd 托管 `trigger=boot`)+ `am` CLI(status/list/history/start/kill/`resolve`/`picker`)。二期将路由表迁 amsd(amsd 路由权威)+ **picker 契约**(`MSG_PICKER_CALL=0x70`/`MSG_PICKER_RESULT=0x71`,`am picker`/`picker-status`;files_app 注册 `picker.pick_file` 选文件返回路径)+ `MSG_AMS_START/KILL=0x00A0/0x00A1`(amsd→launcher 决策执行)。参见 `devdocs/plan/AMSD_DESIGN.md`。 - **M5 通知中心 UI 整合已落地(M2.65-M2.81)**:通知中心 + 媒体卡片 + Now Playing(媒体会话见附录 B,已落地:`meow_media.c` 最近活跃源、30s 超时回收、`MSG_META`/`MSG_PLAYCTL`)。 diff --git a/docs/MEOWOS_SPEC_REF.md b/docs/MEOWOS_SPEC_REF.md index b1fcb22..05002fb 100644 --- a/docs/MEOWOS_SPEC_REF.md +++ b/docs/MEOWOS_SPEC_REF.md @@ -57,7 +57,7 @@ ### 2.2 meow_ipc 消息协议(AF_UNIX,全小端 LE) - 帧头 16B:`magic=0x4D4F5701`('MEOW'|1)/`version=1`/`type`/`req_id`/`len`;`len` 上限 **MEOW_IPC_MAX=4096**;列表编码 `[u16 count]`+NUL 串 `[FRAMEWORK_DESIGN §1]` -- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑ / SERVICE_OK 0x0011↓ / SERVICE_STOP 0x0012↑ / MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / NOTIFY_PROGRESS 0x0024↑ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / ACT_FOCUS 0x0033↑ / ACT_BACK 0x0034↑ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / PERF_QUERY 0x0050↑ / PERF_RESULT 0x0051↓ / POWER_ACTION 0x0060↑ / POWER_STATUS 0x0061↓ / SYS 0x00F0↓` `[FRAMEWORK_DESIGN §1][meow_ipc.h]` +- 消息类型:`MSG_HELLO 0x0001↑ / ACK 0x0002↓ / ERR 0x0003↓ / REGISTER 0x0010↑(M4 已落地:无头后台服务注册,m_register_t,保持连接)/ SERVICE_OK 0x0011↓(M4:m_ack_t status=服务 pid)/ SERVICE_STOP 0x0012↑(M4:SIGTERM 不重启)/ MSG_NOTIFY 0x0020↑ / NOTIFY_ACK 0x0021↓ / NOTIFY_DISMISS 0x0022↑ / NOTIFY_LIST 0x0023↓ / NOTIFY_PROGRESS 0x0024↑ / ACT_DECLARE 0x0030↑ / ACT_QUERY 0x0031↑ / ACT_RESULT 0x0032↓ / ACT_FOCUS 0x0033↑ / ACT_BACK 0x0034↑ / NAV 0x0040↑ / SHOW_ACTIVITY 0x0041↓ / PERF_QUERY 0x0050↑ / PERF_RESULT 0x0051↓ / POWER_ACTION 0x0060↑ / POWER_STATUS 0x0061↓ / SYS 0x00F0↓(M4:m_sys_t,SHUTDOWN=1/SVC_DEAD=2 熔断广播)` `[FRAMEWORK_DESIGN §1][meow_ipc.h]` - 媒体:`MSG_META`(元数据)、`MSG_PLAYCTL`(cmd 1=播放2=暂停3=上一首4=下一首5=seek6=播放/暂停toggle——物理键164恒发6由源侧按当前态翻转,launcher 透传)、`MSG_PROGRESS` `[附录 B]` - IPC 线程**只读帧+解析+入队**;状态变更主循环单写者(eventfd 唤醒,10ms 热键轮询非阻塞排空)`[FRAMEWORK_DESIGN §0][§6]` @@ -152,7 +152,7 @@ - `meow_settings_*`(设置后端,已定)/`meowcli`/`meow_sys_devattr`/`meow_devattr_set` `[MEOWOS_API]` - **独占音频守护 `meow_audiod`(M2.90 已落地,规范 `devdocs/plan/AUDIOD_DESIGN.md`)**:板载 `/usr/bin/meow_audiod`,svcd 托管(板载 `services.d/audiod.conf`,trigger=manual;launcher 确认 MeowOS 模式后拉起;**启动门控:mode≠meow → exit(0),HibyOS 下不存在**)。IPC `/usr/data/meow_audio.sock`(帧层复用 meow_ipc.h,号段 0x00C0-0x00CA:OPEN/OPEN_ACK/WRITE/PAUSE/RESUME/STOP/PREEMPTED/QUERY/STATE/ROUTE/VOLUME)。**独占+抢占**(后开赢,旧流收 PREEMPTED;socket EOF=回收;软混音已否决,留接口位)。**DAC 原生率跟随不做 SRC**;格式归一 S16 直通/S32 升位/mono 复制;切率 mute-重配-恢复。**meow_route 已并入为路由线程并退役独立服务**(⚠️ 退役服务要清板载+TF 双源 conf,否则旧 conf 会双写路由)。**CLI `meow_audio`**(root 免鉴权):`status`/`tone [freq] [ms] [rate]`(正弦测试音,排查无声不开 App)/`route <0|2|3>`/`vol [0-100]`。SDK 客户端 `meow_audio_open/write/pause/close/query/on_preempt`(`meow_ui_audio.c`,懒连接+EPIPE 标死;无 audiod → -ENOENT 不隐式直开,`MEOW_AUDIO_DIRECT=1` 仅调试)。**M2.91-97 加固批**:①**开机武装 hbc3000 FPGA**(M2.91:路由线程启动无条件写 Output Port Switch 4.4→3/3.5→2/无→4,同值先写 5 强制重烧——根治「未烧 FPGA 时 PCM 第二次 `hbc3000_start` 内核硬挂死」,调查底稿 `devdocs/plan/AUDIO_HANG_INVESTIGATION.md`)②**长流护栏**(M2.93:ring 有界等待/cond timedwait/popen 有界收割/O_NONBLOCK+poll 写/wedge 自救/心跳 watchdog)+**几何收窄 period=1024×4**(M2.94:驱动默认巨 buffer 致桥周期中断稀缺 wedge、播 ~12s 停摆;tonepcm2 矩阵 60s 零 stall 实证)③**音量反转**(M2.95:CS43198 音量寄存器=衰减量,四路统一 `dac=255-pct*255/100`)④**炸耳安全**(M2.96:`dac_vol_mute` 写 255 真静音,旧写 0=最大响度)⑤**电平监控**(261,待 M2.97 固件化:播放线程 ~100ms 窗口整数 RMS/peak dBFS(deci-dB,逐帧判边界,-990=-inf)、`MSG_AUDIO_STATE` 追加 `rms=/peak=/lvlseq=`(ABI 兼容)、流活跃时 1s 节拍写 TF `MeowOS/logs/audio_monitor_.log`、CLI `meow_audio monitor [秒]` 文本电平条)。`[patch_v2/meow_audiod/]` - **音频硬件事实(sndprobe/tonepcm2 真机实测,2026-08-31)**:播放 PCM = `/dev/snd/pcmC0D0p`(`cs43198-hifi-0`);`pcmC0D1p`=snd-soc-dummy(勿用)。能力:S16_LE/S24_LE、1-2ch、8k-768k、buffer 2048-1048576 帧、period 512-262144。⚠️ **ALSA UAPI 两坑**(ncm 无声根因,勿再踩):①hw_params 必须「全放行」初始化(mask 全 1、interval [0,UINT_MAX],params_any 语义)再收窄——memset 0 的 interval=「只能取 0」求交必空 → HW_PARAMS 恒 EINVAL;②access/format/subformat mask 必须**先清零再单点**——留全 1 会被内核 choose 成 MMAP_INTERLEAVED,ch>1 时 write 恒 EINVAL。**CS43198 音量寄存器=衰减量(官方数据手册 DS1039 实证,用户供)**:PCM_VOLUME_A/B(0x0B/0x0C),0=0dB 满幅最大声、255=-127.5dB≈静音(0.5dB/步;DSD 通道独立 DSD_VOLUME_A/B)。**混音器 L/R Playback Volume 读回恒 0 是死镜像**(官方播放器走 sa_hgl_dma 硬直通不经此控制,HibyOS 对照实测同读 0),勿据此判音量。**音量寄存器=衰减量(0=最响 255=静音,M2.95 真机实测方向)**:四路(launcher/audio.c+audiod+watchdog+SDK vol)统一 `dac=255-pct*255/100`;mute 必须写 255 真静音(M2.96 炸耳修复)。混音器清单:numid=1/2 L/R Vol、3 Mute Output、4 Digital Filter、5 Soft Mute、6 NOS_EN、7 DRE_EN、8 Balance Lineout En、9 Output Port Switch(0 空闲/1 lineout/2 3.5/3 4.4/4 SPDIF;0 危险勿写;**5 = audiod 内部「强制重烧 FPGA」约定值**(M2.91 开机武装同值改写路径),非常规路由值,App/面板勿写)、10 DOP_EN。`[patch_v2/tools/sndprobe.c][tonepcm2.c][APPS §3.7]` -- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`;`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]` +- 后台权限:`MSG_HELLO/MSG_REGISTER.flags` 扩展 `MF_BACKGROUND`/`MF_PLAYBACK`(**已落地 M4**:app.json `background`/`background_audio` 字段 + MSG_REGISTER 校验链;播放中退出转后台保活见 FRAMEWORK_DESIGN §9);`PERF_BACKGROUND`/`PERF_AUDIO_BACKGROUND`(规划)`[FRAMEWORK_DESIGN 附录 A]` --- @@ -215,7 +215,7 @@ - `/` = 程序主体+app.json+资源;`MeowData//{data,cache}` = 数据;首次启动自动建 data/cache 并经 env 注入,exFAT 无软链 `[APPS 附录 §1/§2]` - `.mpk` = tar.gz(app.json + meow 二进制 + 资源),**不含 data/cache**;导入校验 app.json/哈希→解压→数据 init→登入列表 `[APPS 附录 §3]` - 包管理 **CLI `nyai`**(静态 MIPS,部署 `MeowOS/opt/bin/nyai`):install/remove/list/info/search/index/export/upgrade/scan;源 `MeowOS/opt/nyai/sources.list`;索引 `Packages`(Package/Version/Filename/Size/SHA256/Depends/Description)`[APPS 附录 §5]` -- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + **`package`(反域名权威 app_id;==目录名;缺省回退目录名)** + 扩展 `framework`/`immersive`/`icon`/`route.`/`awake.`(keep_awake)/`service.`/`fullscreen`(app 级;`route..fullscreen`=activity 级;**本次实现**:launcher 启动/切 activity 据声明预设 SDK 全屏,见 §5.1/§3.1/§A-12)/`fonts`(字体档位 `"full"|"gbk"|"common"`,需大字库时声明,见 §5.7.8) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30][源:用户 2026-08-30]` +- 应用数上限 **8**(单屏 8 行);扫描深度 1 级;`app.json` 字段:name(仅 ASCII)/exec(静态 MIPS32r2 ELF)/version/author + **`package`(反域名权威 app_id;==目录名;缺省回退目录名)** + 扩展 `framework`/`immersive`/`icon`/`route.`/`awake.`(keep_awake)/`service.`(M4 起服务二进制=App 同目录 `` 文件)/**`background`/`background_audio`(M4 已落地:无头服务注册权限/后台播放保活)**/`fullscreen`(app 级;`route..fullscreen`=activity 级;**本次实现**:launcher 启动/切 activity 据声明预设 SDK 全屏,见 §5.1/§3.1/§A-12)/`fonts`(字体档位 `"full"|"gbk"|"common"`,需大字库时声明,见 §5.7.8) `[APPS §2][MEOW_APP_SDK §9][FRAMEWORK_DESIGN §2][源:审计 2026-08-30][源:用户 2026-08-30]` ### 5.6 冷启动充电屏 - 触发(**三者同时**):冷启动(`poweroff` 写 `/data/meow_cold_boot=1`;`reboot` 写 0)+ 在充电(AC online=1 或 battery status=Charging)+(开机瞬间一次性快照,判定后不回头)`[CHARGING_SCREEN §0]` @@ -350,7 +350,7 @@ ## 6. 标志性 TODO / 未决(文档未定,勿凭猜实现) - OPEN_QUESTIONS.md 待定 ❓:多数已由后续定案/落地覆盖——#1 背景图加载(「启动加载一次+预渲染快照不每帧缩放」已落地)、#2 图处理(**cover 居中裁剪**)、#3 叠加层颜色(**固定跟明暗主题**)、#4 背景色板枚举(**已清结**:MD3+MIUIx 色板 M2.90 落地,`display.background` 按 style×mode 派生)、#7 应用图标(**已做,app.json icon 加载缓存+首字母占位**)、#8 通知来源(**系统事件 + MeowAPP IPC + D-Bus 桥 三路**,中心 UI 已落地)、#9 下拉手势(**顶部下滑=通知中心**,与左滑返回按起始位置分流)、#10 通知存储(**TF 归档 CSV + 10MB 滚动 + 环形 200 条**)、#11 媒体活跃会话(**active_seq 最近活跃源,30s 超时回收**)、#12 多 app 音频(**已被 meow_audiod 取代(M2.90):守护独占 hw:0,0 + 抢占,App 经 SDK meow_audio_*,软混音否决**)、#13 web 实时画面(**`/api/screen.jpeg` + 增量帧**)、#14 输入注入(**本机触摸优先 5s 仲裁**)、#16 主页左滑=侧边栏 vs 返回(**Now Playing 横划页,左缘返回保留**)、#17 音频输出设置(**底层已由 audiod 收口:路由自动+CLI;设置页 UI 项留后续**)。**仍待定**:#5 浅色文字对比度(待实机观感验收)+ 2026-09-01 新增批(~~PCM 内核挂死~~**已根治**:M2.91 FPGA 开机武装 + M2.94 几何收窄/WiFi 录入 UI/amsd DECLARE 残余(`am start` ACK 后 launcher 未真拉起 App)/流媒体客户端出声终验(链路技术指标已全绿:FPGA 武装+几何+监控 hwrate 健康,待用户耳机终验)/流媒体客户端 UI 专项/感知音量曲线 dB taper) `[OPEN_QUESTIONS]` -- DEVELOPMENT_PLAN:**阶段 0(bootmenu 双系统)已落地(现网即运行中)**;阶段 B/E/F 达成;阶段 D M0-M3/M5 已落地、**M4 后台服务保活 / M6 加固 待排期** `[DEVELOPMENT_PLAN]` +- DEVELOPMENT_PLAN:**阶段 0(bootmenu 双系统)已落地(现网即运行中)**;阶段 B/E/F 达成;阶段 D M0-M5 已落地(**M4 后台服务保活+后台播放 2026-09-01 落地**:MSG_REGISTER/SERVICE_STOP/MSG_SYS + 退避熔断 + nav-exit 后台保活,见 FRAMEWORK_DESIGN §9)、**M6 加固 待排期** `[DEVELOPMENT_PLAN]` - 未定/待拍板:MeowOS 音频输出设置(只读还是可配,OPEN_QUESTIONS #17);OTA 在线「检查更新/下载」依赖 MeowPanel(本地/检查升级已可用,URL 下载待定案 #18)`[WORK_STATE][OPEN_QUESTIONS #17/#18]` - **meow_svcd 后续(M2.54 已落地大部分)**:~~接入 supervisor 启动链~~(已改 `etc/init.d/S89meow_svcd` 开机自启;bootmenu/launcher `fork+exec`+`waitpid` 仍为备选);~~写 `MeowOS/services.d/mdnsd.conf` 落地 mDNS~~(已改**板载** `/usr/data/meow/services.d/mdnsd.conf` 预置,`trigger=boot`)。剩余:`cap` setcap 真机实装(cap_net_bind_service,cap_net_raw);`wifi-up` 判定收紧。**services.d/svcd 已接入开机链(S89)**,但真机行为待验证。 `[WORK_STATE][meow_svcd.md]` diff --git a/patch_v2/meow_launcherd/app_launcher.c b/patch_v2/meow_launcherd/app_launcher.c index 2b26e06..7aa047f 100644 --- a/patch_v2/meow_launcherd/app_launcher.c +++ b/patch_v2/meow_launcherd/app_launcher.c @@ -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.(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 二进制同目录下的 名文件 + * (app.json `service.=...` 只声明策略,二进制位置由此约定派生)。 */ +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=(注入 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.=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//{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 二进制同目录下的 名文件(见 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(); diff --git a/patch_v2/meow_launcherd/build_launcher.sh b/patch_v2/meow_launcherd/build_launcher.sh index 2e4434c..094e887 100755 --- a/patch_v2/meow_launcherd/build_launcher.sh +++ b/patch_v2/meow_launcherd/build_launcher.sh @@ -27,7 +27,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf" notif_center.c app_launcher.c audio.c backlight.c net.c about.c \ subpage.c wifi_page.c bt_page.c now_playing.c \ font_theme.c keys.c util.c \ - meow_touch.c meow_settings.c meow_notif.c meow_media.c \ + meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \ "../sdk/meow_ui.a" "$SYSROOT/lib/libz.a" -lpthread -o meow_launcherd echo "== meow_launcherd ==" ls -la meow_launcherd diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c index b6551ef..1e63a3d 100644 --- a/patch_v2/meow_launcherd/main_menu.c +++ b/patch_v2/meow_launcherd/main_menu.c @@ -467,6 +467,11 @@ static void menu_draw_grid_cell(const struct app *a, meow_img *icon, int x, int if (!a->ok) { int ew = ui_width("[缺失]", 14); ui_draw(x + (w - ew) / 2, name_y + meow_font_line_height(18) + 2, "[缺失]", th->error, 14); + } else if (meow_bg_is_backgrounded(a->id)) { + /* M4:后台保活中标注(播放中转后台的 App;run_menu 1s tick 按 change_seq 刷新) */ + int bw = ui_width("·后台", 14); + ui_draw(x + (w - bw) / 2, name_y + meow_font_line_height(18) + 2, "·后台", + th->on_surface_variant, 14); } } @@ -496,9 +501,11 @@ static void menu_draw_list_cell(const struct app *a, meow_img *icon, int x, int sub_y = y + 30; } ui_draw(tx, name_y, nm, a->ok ? th->on_surface : th->error, 20); - /* 副标题:package(无则 id),on_surface_variant */ - char sub[40]; - snprintf(sub, sizeof sub, "%s", a->package[0] ? a->package : (a->id[0] ? a->id : "")); + /* 副标题:package(无则 id),on_surface_variant;M4 后台保活中前缀「·后台 」 */ + char sub[48]; + snprintf(sub, sizeof sub, "%s%s", + meow_bg_is_backgrounded(a->id) ? "·后台 " : "", + a->package[0] ? a->package : (a->id[0] ? a->id : "")); if (sub[0]) { char subf[44]; menu_name_fit(sub, w - (tx - x) - 12, 14, subf, sizeof subf); @@ -1621,6 +1628,12 @@ static void run_menu(struct app apps[MAX_APPS], int napps, int state, draw_clock(); /* Stage-One:喂 SDK 全局状态栏 + 标脏顶条;SDK present() 重绘 */ fb_norm_buf0(); meow_ui_flush(); /* blit 状态栏整条,不动列表 */ + /* M4:后台保活表变化(转后台/回前台/回收)→ 重绘列表刷新「·后台」标注 */ + { + static uint32_t bg_seq_last = 0; + uint32_t seq = meow_bgplay_change_seq(); + if (seq != bg_seq_last) { bg_seq_last = seq; dirty = 1; } + } } usleep(POLL_MS * 1000); @@ -1964,6 +1977,10 @@ int main(int argc, char **argv) "(state=%d napps=%d), drain-to-home immediately", state, napps); run_menu(apps, napps, state, sim_tap, sim_exit); logmsg("=== meow_launcherd exit (MeowOS mode) ==="); + /* M4:关机/退出前回收后台保活 App + 无头服务(有界 SIGTERM→SIGKILL→reap); + * 清 §10.5 自抑标记,防 launcher 重启后残留标记抑制后续 App present。 */ + meow_bgplay_kill_all(); + unlink(NOTIF_ACTIVE); if (log_fd >= 0) { close(log_fd); log_fd = -1; } ipc_shutdown = 1; unlink(ipc_sock_path); unlink(PID_PATH); @@ -2168,6 +2185,8 @@ int main(int argc, char **argv) if (player_stopped) { fb_restore_player(); restore_backlight(); } cont_player(); logmsg("=== meow_launcherd exit ==="); + meow_bgplay_kill_all(); /* M4:退出前回收后台保活 App + 无头服务(有界) */ + unlink(NOTIF_ACTIVE); /* M4:清 §10.5 自抑标记,防残留抑制后续 App present */ if (log_fd >= 0) { close(log_fd); log_fd = -1; } ipc_shutdown = 1; unlink(ipc_sock_path); unlink(PID_PATH); diff --git a/patch_v2/meow_launcherd/meow_bgplay.c b/patch_v2/meow_launcherd/meow_bgplay.c new file mode 100644 index 0000000..94367bc --- /dev/null +++ b/patch_v2/meow_launcherd/meow_bgplay.c @@ -0,0 +1,509 @@ +/* meow_bgplay.c — M4 后台播放保活 + 无头后台服务运行时表(见 meow_bgplay.h 头注释)。 */ +#include "meow_bgplay.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static meow_bgplay_ops g_ops; +static meow_bg_entry_t g_bg[MEOW_BG_MAX]; +static meow_svc_rt_t g_svc[MEOW_SVC_RT_MAX]; +static uint32_t g_change_seq; + +static const uint32_t k_backoff[] = { + MEOW_SVC_BACKOFF_1, MEOW_SVC_BACKOFF_2, MEOW_SVC_BACKOFF_4, MEOW_SVC_BACKOFF_10 +}; +#define BACKOFF_STEPS ((int)(sizeof(k_backoff) / sizeof(k_backoff[0]))) + +/* SIGTERM 后 SIGKILL 兜底宽限(ms) */ +#define TERM_GRACE_MS 500u + +/* ------------------------------------------------------------------ */ + +static void bg_log(const char *fmt, ...) +{ + va_list ap; + char buf[384]; + + if (!g_ops.log) return; + va_start(ap, fmt); + vsnprintf(buf, sizeof(buf), fmt, ap); + va_end(ap); + g_ops.log("%s", buf); +} + +static uint64_t bg_now_ms(void) +{ + struct timespec ts; + + if (g_ops.now_ms) return g_ops.now_ms(); + clock_gettime(CLOCK_MONOTONIC, &ts); + return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u; +} + +static int bg_card_present(void) +{ + return g_ops.card_present ? g_ops.card_present() : 1; +} + +void meow_bgplay_init(const meow_bgplay_ops *ops) +{ + memset(&g_ops, 0, sizeof(g_ops)); + memset(g_bg, 0, sizeof(g_bg)); + memset(g_svc, 0, sizeof(g_svc)); + g_change_seq = 0; + if (ops) g_ops = *ops; +} + +/* ================= 后台播放保活 ================= */ + +int meow_bg_should_keep(int bg_audio_declared, int media_playing) +{ + return bg_audio_declared && media_playing; +} + +static meow_bg_entry_t *bg_find(const char *app_id) +{ + int i; + + for (i = 0; i < MEOW_BG_MAX; i++) + if (g_bg[i].used && strcmp(g_bg[i].app_id, app_id) == 0) + return &g_bg[i]; + return NULL; +} + +int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card) +{ + int i; + + if (!app_id || !app_id[0] || pid <= 0) return MEOW_ERR_INVAL; + if (bg_find(app_id)) return MEOW_ERR_EXISTS; + for (i = 0; i < MEOW_BG_MAX; i++) { + if (!g_bg[i].used) { + meow_bg_entry_t *e = &g_bg[i]; + + memset(e, 0, sizeof(*e)); + e->used = 1; + snprintf(e->app_id, sizeof(e->app_id), "%s", app_id); + e->pid = pid; + e->out_fd = out_fd; + e->on_card = on_card; + g_change_seq++; + bg_log("[bgplay] 转后台保活: %s pid=%d on_card=%d", app_id, (int)pid, on_card); + return 0; + } + } + return MEOW_ERR_NOMEM; +} + +int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid) +{ + meow_bg_entry_t *e; + + if (!app_id || !app_id[0]) return -1; + e = bg_find(app_id); + if (!e) return -1; + if (out_fd) *out_fd = e->out_fd; + if (pid) *pid = e->pid; + bg_log("[bgplay] 回前台: %s pid=%d", app_id, (int)e->pid); + memset(e, 0, sizeof(*e)); + g_change_seq++; + return 0; +} + +int meow_bg_is_backgrounded(const char *app_id) +{ + return app_id && app_id[0] && bg_find(app_id) != NULL; +} + +int meow_bg_count(void) +{ + int i, n = 0; + + for (i = 0; i < MEOW_BG_MAX; i++) + if (g_bg[i].used) n++; + return n; +} + +const meow_bg_entry_t *meow_bg_at(int i) +{ + if (i < 0 || i >= MEOW_BG_MAX || !g_bg[i].used) return NULL; + return &g_bg[i]; +} + +uint32_t meow_bgplay_change_seq(void) +{ + return g_change_seq; +} + +void meow_bg_tick(void) +{ + uint64_t now = bg_now_ms(); + int i; + + for (i = 0; i < MEOW_BG_MAX; i++) { + meow_bg_entry_t *e = &g_bg[i]; + int st; + pid_t r; + + if (!e->used) continue; + + /* 卡拔例外:二进制在卡上 → SIGTERM(防 SIGBUS),500ms 未死 SIGKILL */ + if (e->on_card && !bg_card_present()) { + if (e->term_at_ms == 0) { + bg_log("[bgplay] 卡拔出,终止卡上后台 App: %s pid=%d", + e->app_id, (int)e->pid); + kill(e->pid, SIGTERM); + e->term_at_ms = now; + } else if (now - e->term_at_ms >= TERM_GRACE_MS) { + kill(e->pid, SIGKILL); + e->term_at_ms = now; /* 500ms 节流重复 SIGKILL 直至 reap */ + } + } + + r = waitpid(e->pid, &st, WNOHANG); + if (r == e->pid) { + char id[MEOW_APP_ID_MAX]; + pid_t pid = e->pid; + + snprintf(id, sizeof(id), "%s", e->app_id); + memset(e, 0, sizeof(*e)); + g_change_seq++; + bg_log("[bgplay] 后台 App 退出: %s pid=%d status=0x%x", id, (int)pid, st); + if (g_ops.on_bg_dead) g_ops.on_bg_dead(id, pid, st); + } + } +} + +/* ================= 无头后台服务 ================= */ + +static meow_svc_rt_t *svc_find_mut(const char *app_id, const char *svc) +{ + int i; + + for (i = 0; i < MEOW_SVC_RT_MAX; i++) + if (g_svc[i].used && strcmp(g_svc[i].app_id, app_id) == 0 && + strcmp(g_svc[i].svc, svc) == 0) + return &g_svc[i]; + return NULL; +} + +const meow_svc_rt_t *meow_svc_find(const char *app_id, const char *svc) +{ + if (!app_id || !svc) return NULL; + return svc_find_mut(app_id, svc); +} + +/* spawn 服务子进程:stdio→/dev/null,env={PATH,HOME,MEOW_SERVICE[,MEOW_LOG_DIR/MEOW_LOG_APP]}。 + * 父进程返回 pid;子进程不返回(execve 或 _exit(127))。 */ +static pid_t svc_spawn(meow_svc_rt_t *s) +{ + char env_service[32]; + char env_logdir[160]; + char env_logapp[64]; + char *envp[6]; + int n = 0; + pid_t pid; + + envp[n++] = (char *)"PATH=/usr/bin:/bin:/usr/sbin:/sbin"; + envp[n++] = (char *)"HOME=/root"; + snprintf(env_service, sizeof(env_service), "MEOW_SERVICE=%s", s->svc); + envp[n++] = env_service; + if (g_ops.log_dir && g_ops.log_dir[0]) { + snprintf(env_logdir, sizeof(env_logdir), "MEOW_LOG_DIR=%s", g_ops.log_dir); + envp[n++] = env_logdir; + snprintf(env_logapp, sizeof(env_logapp), "MEOW_LOG_APP=%s", s->app_id); + envp[n++] = env_logapp; + } + envp[n] = NULL; + + pid = fork(); + if (pid != 0) return pid; /* 父:pid 或 -1 */ + + /* 子进程:脱离终端会话,stdio 全部重定向 /dev/null(无头服务) */ + setsid(); + { + int dn = open("/dev/null", O_RDWR); + if (dn >= 0) { + dup2(dn, STDIN_FILENO); + dup2(dn, STDOUT_FILENO); + dup2(dn, STDERR_FILENO); + if (dn > STDERR_FILENO) close(dn); + } + } + { + char *argv[2]; + + argv[0] = s->exec; + argv[1] = NULL; + execve(s->exec, argv, envp); + } + _exit(127); /* exec 失败:父进程 tick 回收后按退避/熔断处理 */ +} + +int meow_svc_register(const char *app_id, const char *svc, const char *exec, + int restart, int max_restart, int window_ms, + int on_card, int reg_fd) +{ + meow_svc_rt_t *s; + pid_t pid; + int i; + + if (!app_id || !app_id[0] || !svc || !svc[0] || !exec || !exec[0]) + return MEOW_ERR_INVAL; + if (access(exec, X_OK) != 0) return MEOW_ERR_NOENT; + + s = svc_find_mut(app_id, svc); + if (s) { + if (s->pid > 0) return MEOW_ERR_EXISTS; + if (!s->circuit) return MEOW_ERR_EXISTS; /* 退避排程中也视为在册 */ + /* 熔断槽重注册:复位熔断,重新武装 */ + bg_log("[bgplay] 服务熔断复位重注册: %s/%s", app_id, svc); + s->circuit = 0; + s->stopping = 0; + s->restart_count = 0; + s->window_start_ms = bg_now_ms(); + s->next_retry_ms = 0; + s->reg_fd = reg_fd; + s->on_card = on_card; + } else { + for (i = 0; i < MEOW_SVC_RT_MAX; i++) + if (!g_svc[i].used) break; + if (i >= MEOW_SVC_RT_MAX) return MEOW_ERR_NOMEM; + s = &g_svc[i]; + memset(s, 0, sizeof(*s)); + s->used = 1; + snprintf(s->app_id, sizeof(s->app_id), "%s", app_id); + snprintf(s->svc, sizeof(s->svc), "%s", svc); + snprintf(s->exec, sizeof(s->exec), "%s", exec); + s->restart = restart; + s->max_restart = max_restart; + s->window_ms = window_ms; + s->on_card = on_card; + s->reg_fd = reg_fd; + s->window_start_ms = bg_now_ms(); + } + + pid = svc_spawn(s); + if (pid < 0) { + if (s->pid == 0 && s->restart_count == 0 && !s->circuit) + memset(s, 0, sizeof(*s)); /* 新槽首次 fork 即败:不留尸槽 */ + return MEOW_ERR_IO; + } + s->pid = pid; + s->term_at_ms = 0; + bg_log("[bgplay] 服务拉起: %s/%s pid=%d restart=%d max=%d", + app_id, svc, (int)pid, restart, max_restart); + return (int)pid; +} + +int meow_svc_stop(const char *app_id, const char *svc) +{ + meow_svc_rt_t *s; + + if (!app_id || !svc) return MEOW_ERR_INVAL; + s = svc_find_mut(app_id, svc); + if (!s) return MEOW_ERR_NOENT; + bg_log("[bgplay] 服务停止: %s/%s pid=%d", app_id, svc, (int)s->pid); + if (s->pid > 0) { + s->stopping = 1; + kill(s->pid, SIGTERM); + s->term_at_ms = bg_now_ms(); + } else { + /* 未在跑(退避中/熔断):直接清槽 */ + if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc); + memset(s, 0, sizeof(*s)); + } + return 0; +} + +/* 服务死亡后的退避/熔断判定。返回 1=已排程重启;0=槽已清或熔断。 */ +static int svc_on_exit(meow_svc_rt_t *s, int st) +{ + uint64_t now = bg_now_ms(); + + bg_log("[bgplay] 服务退出: %s/%s pid=%d status=0x%x stopping=%d", + s->app_id, s->svc, (int)s->pid, st, s->stopping); + s->pid = 0; + + if (s->stopping || !s->restart) { + if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc); + memset(s, 0, sizeof(*s)); + return 0; + } + + /* 滑窗:超窗复位计数 */ + if (s->window_ms > 0 && now - s->window_start_ms >= (uint64_t)s->window_ms) { + s->restart_count = 0; + s->window_start_ms = now; + } + s->restart_count++; + + if (s->max_restart > 0 && s->restart_count > s->max_restart) { + /* 熔断:清通知 + 广播 MSG_SYS{SVC_DEAD},槽保留待重注册复位 */ + m_sys_t m; + + s->circuit = 1; + s->next_retry_ms = 0; + bg_log("[bgplay] 服务熔断: %s/%s 窗口内重启 %d 次超 max=%d", + s->app_id, s->svc, s->restart_count, s->max_restart); + if (g_ops.notif_clear_app) + g_ops.notif_clear_app(s->app_id); + if (g_ops.send_frame && s->reg_fd >= 0) { + memset(&m, 0, sizeof(m)); + m.event = MEOW_SYS_SVC_DEAD; + snprintf(m.app_id, sizeof(m.app_id), "%s", s->app_id); + snprintf(m.svc, sizeof(m.svc), "%s", s->svc); + g_ops.send_frame(s->reg_fd, MSG_SYS, 0, &m, sizeof(m)); + } + if (g_ops.on_svc_dead) g_ops.on_svc_dead(s->app_id, s->svc); + return 0; + } + + { + int step = s->restart_count - 1; + uint32_t dly; + + if (step >= BACKOFF_STEPS) step = BACKOFF_STEPS - 1; + dly = k_backoff[step]; + s->next_retry_ms = now + dly; + bg_log("[bgplay] 服务退避重启: %s/%s 第 %d 次,%ums 后拉起", + s->app_id, s->svc, s->restart_count, (unsigned)dly); + } + return 1; +} + +void meow_svc_tick(void) +{ + uint64_t now = bg_now_ms(); + int i; + + for (i = 0; i < MEOW_SVC_RT_MAX; i++) { + meow_svc_rt_t *s = &g_svc[i]; + + if (!s->used) continue; + + /* 卡拔例外:exec 在卡上 → SIGTERM + stopping=1(不重启防空转死循环) */ + if (s->on_card && s->pid > 0 && !bg_card_present() && !s->stopping) { + bg_log("[bgplay] 卡拔出,终止卡上服务: %s/%s pid=%d", + s->app_id, s->svc, (int)s->pid); + s->stopping = 1; + kill(s->pid, SIGTERM); + s->term_at_ms = now; + } + /* SIGTERM 宽限超时 → SIGKILL */ + if (s->stopping && s->pid > 0 && s->term_at_ms && + now - s->term_at_ms >= TERM_GRACE_MS) { + kill(s->pid, SIGKILL); + s->term_at_ms = now; + } + + if (s->pid > 0) { + int st; + pid_t r = waitpid(s->pid, &st, WNOHANG); + + if (r == s->pid) svc_on_exit(s, st); + continue; + } + + /* 退避到点 → 重新拉起 */ + if (!s->circuit && !s->stopping && s->restart && + s->next_retry_ms && now >= s->next_retry_ms) { + pid_t pid = svc_spawn(s); + + s->next_retry_ms = 0; + if (pid > 0) { + s->pid = pid; + bg_log("[bgplay] 服务重启: %s/%s pid=%d(第 %d 次)", + s->app_id, s->svc, (int)pid, s->restart_count); + } else { + /* fork 失败:压到下一档重试 */ + s->next_retry_ms = now + k_backoff[BACKOFF_STEPS - 1]; + } + } + } +} + +/* ================= 关机前回收 ================= */ + +void meow_bgplay_kill_all(void) +{ + uint64_t deadline; + int i; + + /* best-effort 广播 SHUTDOWN */ + if (g_ops.send_frame) { + m_sys_t m; + + memset(&m, 0, sizeof(m)); + m.event = MEOW_SYS_SHUTDOWN; + for (i = 0; i < MEOW_SVC_RT_MAX; i++) + if (g_svc[i].used && g_svc[i].reg_fd >= 0) + g_ops.send_frame(g_svc[i].reg_fd, MSG_SYS, 0, &m, sizeof(m)); + } + + for (i = 0; i < MEOW_BG_MAX; i++) + if (g_bg[i].used && g_bg[i].pid > 0) kill(g_bg[i].pid, SIGTERM); + for (i = 0; i < MEOW_SVC_RT_MAX; i++) + if (g_svc[i].used && g_svc[i].pid > 0) kill(g_svc[i].pid, SIGTERM); + + /* 有界等 1s(轮询 reap,期间 tick 语义不在——直接 waitpid) */ + deadline = bg_now_ms() + 1000u; + while (bg_now_ms() < deadline) { + int alive = 0; + + for (i = 0; i < MEOW_BG_MAX; i++) { + if (!g_bg[i].used) continue; + if (waitpid(g_bg[i].pid, NULL, WNOHANG) == g_bg[i].pid) + memset(&g_bg[i], 0, sizeof(g_bg[i])); + else + alive++; + } + for (i = 0; i < MEOW_SVC_RT_MAX; i++) { + if (!g_svc[i].used) continue; + if (waitpid(g_svc[i].pid, NULL, WNOHANG) == g_svc[i].pid) + memset(&g_svc[i], 0, sizeof(g_svc[i])); + else + alive++; + } + if (!alive) break; + usleep(50 * 1000); + } + + /* 兜底 SIGKILL + 有界 reap(waitpid 阻塞上限由 SIGKILL 必死保证) */ + for (i = 0; i < MEOW_BG_MAX; i++) + if (g_bg[i].used) { + kill(g_bg[i].pid, SIGKILL); + waitpid(g_bg[i].pid, NULL, 0); + memset(&g_bg[i], 0, sizeof(g_bg[i])); + } + for (i = 0; i < MEOW_SVC_RT_MAX; i++) + if (g_svc[i].used) { + kill(g_svc[i].pid, SIGKILL); + waitpid(g_svc[i].pid, NULL, 0); + memset(&g_svc[i], 0, sizeof(g_svc[i])); + } + g_change_seq++; + bg_log("[bgplay] kill_all 完成(关机/退出回收)"); +} + +void meow_bgplay_dump(const char *prefix) +{ + int i; + + for (i = 0; i < MEOW_BG_MAX; i++) + if (g_bg[i].used) + bg_log("%s bg[%d] %s pid=%d on_card=%d", prefix, i, + g_bg[i].app_id, (int)g_bg[i].pid, g_bg[i].on_card); + for (i = 0; i < MEOW_SVC_RT_MAX; i++) + if (g_svc[i].used) + bg_log("%s svc[%d] %s/%s pid=%d restart=%d count=%d circuit=%d", + prefix, i, g_svc[i].app_id, g_svc[i].svc, (int)g_svc[i].pid, + g_svc[i].restart, g_svc[i].restart_count, g_svc[i].circuit); +} diff --git a/patch_v2/meow_launcherd/meow_bgplay.h b/patch_v2/meow_launcherd/meow_bgplay.h new file mode 100644 index 0000000..04b2d92 --- /dev/null +++ b/patch_v2/meow_launcherd/meow_bgplay.h @@ -0,0 +1,139 @@ +/* meow_bgplay.h — MeowOS M4:后台播放保活 + 无头后台服务运行时表(FRAMEWORK_DESIGN §9)。 + * + * 定位: + * ① 后台播放保活——退出播放中(声明 background_audio=true 且媒体会话 PLAYING)的 App + * 不 SIGTERM:进程保留(音频经 audiod socket 天然续播),UI 冻结走 §10.5 的 + * /tmp/meow_notif_active SDK 自抑机制(present 画进 back_buf 不上屏 + 输入排空)。 + * **不做字面 SIGSTOP**:SIGSTOP 会冻结同进程播放线程、断供 PCM(§10.5 真机实证的坑)。 + * 点图标/媒体卡片回前台=take 恢复原进程;停播后再退出=真退(走原 SIGTERM 序列)。 + * ② 无头后台服务——MSG_REGISTER 注册 app.json `service.` 声明的服务:launcher + * spawn 子进程(无 UI,stdio→/dev/null),死亡按 1/2/4/10s 退避重启,超 max_restart + * 熔断(清该 App 通知 + 经注册连接广播 MSG_SYS{SVC_DEAD}),重注册可复位熔断。 + * + * 边界: + * - 卡拔出例外:二进制在 TF 卡上的后台 App/服务仍 SIGTERM(防 SIGBUS),不适用保活。 + * - 崩溃照收:waitpid WNOHANG 回收,异常退出经 on_bg_dead 回调走 crash 上报。 + * - 关机前回收:meow_bgplay_kill_all() 有界 SIGTERM→SIGKILL→reap。 + * + * 自包含:不依赖 launcher static 符号;日志/时钟/卡检测/通知清理/发帧全经 + * meow_bgplay_ops 注入(launcher 用 logmsg/sd_device_present/ipc_send;宿主单测用假件)。 + * 单一写者=launcher 主循环(meow_ipc_process 排空点挂 tick),无锁。可独立单测(test_bgplay.c)。 + */ +#ifndef MEOW_BGPLAY_H +#define MEOW_BGPLAY_H + +#include +#include +#include +#include "meow_ipc.h" + +/* 后台保活 App 上限(同时驻留后台的播放中 App) */ +#define MEOW_BG_MAX 4 +/* 无头后台服务运行时槽上限(与 meow_acct.svcs 的 MEOW_MAX_SVCS 对齐) */ +#define MEOW_SVC_RT_MAX 8 + +/* 退避重启档位(ms):1s/2s/4s/10s 封顶 */ +#define MEOW_SVC_BACKOFF_1 1000u +#define MEOW_SVC_BACKOFF_2 2000u +#define MEOW_SVC_BACKOFF_4 4000u +#define MEOW_SVC_BACKOFF_10 10000u + +/* 后台保活 App 槽。out_fd=其 stdout 管道读端(保活期继续 drain,防 App 写满管道阻塞)。 */ +typedef struct meow_bg_entry { + int used; + char app_id[MEOW_APP_ID_MAX]; + pid_t pid; + int out_fd; + int on_card; /* 二进制在 TF 卡上(卡拔需 SIGTERM 防 SIGBUS) */ + uint64_t term_at_ms; /* 卡拔 SIGTERM 发出时刻(0=未发;SIGKILL 兜底计时) */ +} meow_bg_entry_t; + +/* 无头后台服务运行时槽(静态声明在 meow_acct.svcs,此处为运行态)。 */ +typedef struct meow_svc_rt { + int used; + char app_id[MEOW_APP_ID_MAX]; + char svc[16]; + char exec[129]; + pid_t pid; /* >0 在跑;0=等待退避重启/已熔断 */ + int reg_fd; /* 注册方 IPC 连接 fd(熔断广播投递用;-1=静态自动拉起) */ + int restart; /* 1=死亡后退避重启 */ + int max_restart; /* 熔断阈值(窗口内重启次数上限;0=不熔断) */ + int window_ms; /* 重启窗口(0=累计不滑窗) */ + int restart_count; /* 当前窗口内已重启次数 */ + int circuit; /* 1=已熔断(清通知+广播过,不再拉起;重注册复位) */ + int stopping; /* 1=主动停止/卡拔终止中(SIGTERM 已发,不重启) */ + int on_card; /* exec 在 TF 卡上 */ + uint64_t window_start_ms; /* 滑窗起点 */ + uint64_t next_retry_ms; /* 退避下次拉起时刻(0=无排程) */ + uint64_t term_at_ms; /* SIGTERM 发出时刻(0=未发;500ms 未死 SIGKILL) */ +} meow_svc_rt_t; + +/* 依赖注入。全 NULL 可用(log 静默、now_ms 内部 CLOCK_MONOTONIC、card_present 恒 1、 + * 通知清理/发帧跳过)。 + * on_bg_dead:后台保活 App 进程退出(status=waitpid 原始状态;正常/异常由回调方判); + * on_svc_dead:服务槽最终消亡(stopping 走完/不重启死亡;熔断不算消亡,槽保留待复位); + * notif_clear_app:熔断时清该 App 全部通知,返回清除条数(<0=失败); + * send_frame:熔断广播 MSG_SYS(launcher 侧须先查连接存活再发); + * log_dir:svc 子进程 MEOW_LOG_DIR 注入(NULL 不注入)。 */ +typedef struct meow_bgplay_ops { + void (*log)(const char *fmt, ...); + uint64_t (*now_ms)(void); + int (*card_present)(void); + void (*on_bg_dead)(const char *app_id, pid_t pid, int status); + void (*on_svc_dead)(const char *app_id, const char *svc); + int (*notif_clear_app)(const char *app_id); + size_t (*send_frame)(int fd, uint16_t type, uint32_t req_id, + const void *payload, uint32_t len); + const char *log_dir; +} meow_bgplay_ops; + +/* 注入 ops(拷贝);NULL=全清空复位(含两张表)。launcher 在 meow_ipc_init 末尾调一次。 */ +void meow_bgplay_init(const meow_bgplay_ops *ops); + +/* ---- 后台播放保活判定(纯函数,无副作用)---- */ +/* 退出时是否转后台保活:声明 background_audio 且该 App 媒体会话正在播放。 */ +int meow_bg_should_keep(int bg_audio_declared, int media_playing); + +/* ---- 后台保活 App 表 ---- */ +/* 转后台:登记 app_id/pid/out_fd。返回 0=成功;MEOW_ERR_EXISTS=已在后台;MEOW_ERR_NOMEM=满。 */ +int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card); +/* 回前台:按 app_id 摘除槽(进程归还前台会话)。成功返回 0 且 *out_fd(可 NULL)带回 + * 管道读端;未在后台返回 -1。 */ +int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid); +/* 该 App 是否在后台保活。 */ +int meow_bg_is_backgrounded(const char *app_id); +/* 当前后台保活数。 */ +int meow_bg_count(void); +/* 取第 i 槽(0..MEOW_BG_MAX-1)只读指针;空槽/越界返回 NULL。供 drain out_fd。 */ +const meow_bg_entry_t *meow_bg_at(int i); +/* 表变化计数(add/take/回收 递增);菜单 1s tick 比对刷新「·后台」标注用。 */ +uint32_t meow_bgplay_change_seq(void); + +/* 周期调(主循环 ~10ms 排空点):waitpid WNOHANG 回收死亡后台 App(→on_bg_dead 清槽); + * 卡拔例外:on_card 槽在卡不在时 SIGTERM,500ms 未死 SIGKILL。 */ +void meow_bg_tick(void); + +/* ---- 无头后台服务 ---- */ +/* 注册/拉起服务。exec 须可执行(先 access(X_OK),拒绝返回 MEOW_ERR_NOENT 不污染槽)。 + * 返回 pid(>0)=已拉起;MEOW_ERR_EXISTS=同名服务在跑;MEOW_ERR_NOENT=exec 不可执行; + * MEOW_ERR_NOMEM=槽满;MEOW_ERR_IO=fork 失败。同 svc 已熔断槽重注册=复位熔断重新拉起。 */ +int meow_svc_register(const char *app_id, const char *svc, const char *exec, + int restart, int max_restart, int window_ms, + int on_card, int reg_fd); +/* 请求停止服务:SIGTERM,标记 stopping(不触发重启)。返回 0=已受理;MEOW_ERR_NOENT=无此服务。 */ +int meow_svc_stop(const char *app_id, const char *svc); +/* 查服务槽(只读);无返回 NULL。 */ +const meow_svc_rt_t *meow_svc_find(const char *app_id, const char *svc); +/* 周期调:回收死亡子进程;按 restart/退避档 1/2/4/10s 重启;滑窗复位计数;超 + * max_restart 熔断(notif_clear_app + send_frame MSG_SYS{SVC_DEAD} + circuit=1); + * 卡拔:on_card 槽 SIGTERM + stopping=1(exec 随卡消失,不重启防空转)。 */ +void meow_svc_tick(void); + +/* 关机前回收:向各服务注册连接广播 MSG_SYS{SHUTDOWN}(best-effort),随后全部 + * bg/svc 子进程 SIGTERM→有界等 1s→SIGKILL→有界 reap。调用方负责 unlink NOTIF_ACTIVE。 */ +void meow_bgplay_kill_all(void); + +/* 调试打印(经 ops->log,每槽一行)。 */ +void meow_bgplay_dump(const char *prefix); + +#endif /* MEOW_BGPLAY_H */ diff --git a/patch_v2/meow_launcherd/meow_ipc.h b/patch_v2/meow_launcherd/meow_ipc.h index b4963ed..e9172a9 100644 --- a/patch_v2/meow_launcherd/meow_ipc.h +++ b/patch_v2/meow_launcherd/meow_ipc.h @@ -13,10 +13,11 @@ * uint32_t len; = payload 字节数 * }; * - * 本阶段(D · M0+M1+M2+M3+M4 前置)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT + + * 本阶段(D · M0+M1+M2+M3+M4)通讯子集:MSG_HELLO/ACK/ERR + MSG_ACT_DECLARE/QUERY/RESULT + * MSG_ACT_FOCUS/BACK + MSG_NOTIFY/PROGRESS/QUERY/DISMISS* + MSG_NAV(导航栈上报/退出) - * + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B)。 - * picker(0x0070/0x0071)仅预留协议位,本批不实现。 + * + MSG_META/MSG_PLAYCTL(媒体会话,阶段 D 附录 B) + * + MSG_REGISTER/SERVICE_OK/SERVICE_STOP/MSG_SYS(后台服务注册/停止/熔断事件,M4)。 + * picker(0x0070/0x0071)已落地(二期)。 */ #ifndef MEOW_IPC_H #define MEOW_IPC_H @@ -157,6 +158,38 @@ typedef struct m_err { char msg[64]; } m_err_t; +/* ================= 后台服务注册(M4,FRAMEWORK_DESIGN §9) = */ + +/* MSG_REGISTER ↑:无头后台服务注册。调用方(App 前台进程)须: + * ① app.json 声明 `background=true` 或 HELLO flags 含 MF_BACKGROUND(否则 ERR_PERM); + * ② app.json `service.=mode:restart:max:window_ms` 静态声明该服务(否则 ERR_NOENT); + * ③ 注册后保持 IPC 连接不断(launcher 以连接存活做熔断广播的投递判定)。 + * svc=NUL 结尾服务名(≤15 字符);flags 按位:MF_BACKGROUND(申请后台常驻)。 */ +typedef struct m_register { + char app_id[MEOW_APP_ID_MAX]; /* 32 */ + char svc[16]; /* 服务名(app.json service.) */ + uint32_t flags; /* MF_BACKGROUND 等 */ +} m_register_t; /* 52 B */ + +/* MSG_SERVICE_OK ↓:用 m_ack_t(code=0;status=服务子进程 pid)。 */ + +/* MSG_SERVICE_STOP ↑:请求停止自己已注册的服务(SIGTERM,不触发重启)。 */ +typedef struct m_service_stop { + char app_id[MEOW_APP_ID_MAX]; /* 32 */ + char svc[16]; +} m_service_stop_t; /* 48 B */ + +/* MSG_SYS ↓:系统事件广播(守护关闭/服务死亡/播放器状态)。 + * event:MEOW_SYS_*;app_id/svc 为事件主体(SHUTDOWN 时可空)。 */ +enum { MEOW_SYS_SHUTDOWN = 1, /* 守护进程即将关闭(关机/重启前回收) */ + MEOW_SYS_SVC_DEAD = 2, /* 后台服务熔断(超 max_restart 不再拉起) */ + MEOW_SYS_PLAYER_STATE = 3 }; /* 播放器状态变更(预留) */ +typedef struct m_sys { + uint32_t event; /* MEOW_SYS_* */ + char app_id[MEOW_APP_ID_MAX]; /* 32 */ + char svc[16]; /* SVC_DEAD 时为熔断服务名 */ +} m_sys_t; /* 52 B */ + /* MSG_ACT_QUERY ↑:解析某 app 的 activity 链路 */ typedef struct m_act_query { char app_id[MEOW_APP_ID_MAX]; diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index 36e946a..3b1df20 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -37,6 +37,7 @@ extern "C" { #include "meow_settings.h" /* B2a:统一系统设置后端(SYSTEM_SETTINGS.md §3/MEOWOS_API §2) */ #include "meow_notif.h" /* M2a:通知存储 + 分发(NOTIFICATION_SYSTEM.md §4/§7/§8) */ #include "meow_media.h" /* 阶段D附录B:媒体会话注册表(MSG_META/MSG_PLAYCTL/最近活跃源) */ +#include "meow_bgplay.h" /* M4:后台播放保活 + 无头后台服务运行时表(§9) */ #include "meow_ui.h" /* A-1/A-2:launcher 用 SDK 渲染栈(meow_ui_fill_rect/text/flush/theme) */ #include "meow_ui_internal.h" /* A-1/A-2:launcher 与 SDK 共用 back_buf + 手动脏模型桥接(mark_full/back_buf) */ #include "meow_notif_panel.h" /* 阶段B:通知中心面板纯逻辑(header-only,可单测) */ @@ -305,6 +306,8 @@ struct app { char immersive; /* '1'=immersive=true(隐藏虚拟返回键,返回手势归 SDK) */ char fullscreen; /* '1'=app.json fullscreen=true(注入 MEOW_FULLSCREEN=1,SDK 预先设全屏) */ char fonts; /* 字库档:0=默认4K常用 1=大字库GBK 2=Full(app.json "fonts";注入 MEOW_FONT_TIER) */ + char background; /* M4:'1'=app.json background=true(可注册无头后台服务) */ + char background_audio; /* M4:'1'=background_audio=true(播放中退出转后台保活,§9) */ char icon[129]; /* icon= 路径 */ int nroute; /* route. 行数 */ struct meow_route routes[MEOW_MAX_ROUTES]; @@ -327,6 +330,8 @@ struct meow_acct { int nsvc; struct meow_svc svcs[MEOW_MAX_SVCS]; int declared; /* 是否经 HELLO/ACT_DECLARE 运行时声明过 */ + char background; /* M4:'1'=app.json background=true(MSG_REGISTER 权限位0) */ + char background_audio; /* M4:'1'=background_audio=true(MSG_REGISTER MF_PLAYBACK 权限位1) */ }; @@ -663,6 +668,19 @@ int meow_ams_pending_start_set_ex(const char *pkg, const char *act, * util.c 转发运行时 declare 与 picker call/result 给 amsd 用。 */ void meow_ams_send(const char *payload, int len); +/* ---- M4 后台保活/后台服务胶水(app_launcher.c 实现;util.c/main_menu.c 消费)---- */ +/* launcher 侧 meow_bgplay ops 组装(meow_ipc_init 末尾调一次)。 */ +void meow_bgplay_launcher_init(void); +/* 后台表周期维护:drain 各 bg App 的 stdout 管道 + meow_bg_tick + meow_svc_tick。 + * 挂载点=meow_ipc_process 末尾(主循环单写者排空点,四处循环都经它)。 */ +void meow_bg_housekeep(void); +/* 查静态声明的 service.;无返回 NULL。 */ +const struct meow_svc *meow_acct_svc_find(const struct meow_acct *ac, const char *svc); +/* 后台权限位:bit0=background(可注册服务)bit1=background_audio(后台音频)。 */ +int meow_acct_background_perm(const struct meow_acct *ac); +/* M4:服务 exec 路径约定 = App 二进制同目录下的 名文件。 */ +void meow_svc_exec_path(const char *app_exec, const char *svc, char *out, int olen); + void meow_dac_vol_write(int pct); void meow_dac_vol_write_throttled(void); void meow_dirty_add(int x, int y, int w, int h); @@ -680,6 +698,11 @@ void meow_font_scan_presets(void); void meow_font_scan_presets_root(const char *card_root); int meow_ipc_init(void); void meow_ipc_process(void); +/* M4:IPC 连接存活查询(熔断广播投递前判连接未断;util.c ipc_conns 表查 fd)。 */ +int meow_ipc_conn_alive(int fd); +/* M4:ipc_send 公开包装(meow_bgplay send_frame 回调用;同 meow_media 的注入口径)。 */ +size_t meow_ipc_send_pub(int fd, uint16_t type, uint32_t req_id, + const void *payload, uint32_t len); /* §10.5:读并清除待处理的 MSG_NOTIF_OPEN(App 前台顶部下滑请求展开通知中心叠加会话)。 * 调用方=launch_app waitpid 循环;返回 1 并填 *out(待处理),0=无。 */ int meow_notif_open_take(m_notif_open_t *out); diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 90d73ac..43a7ab9 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/test_bgplay.c b/patch_v2/meow_launcherd/test_bgplay.c new file mode 100644 index 0000000..c33e479 --- /dev/null +++ b/patch_v2/meow_launcherd/test_bgplay.c @@ -0,0 +1,376 @@ +/* test_bgplay.c — 宿主单测:meow_bgplay 后台播放保活 + 无头后台服务状态机(M4)。 + * + * 验证: + * 1) meow_bg_should_keep 判定矩阵(background_audio × media_playing); + * 2) 后台播放保活全链路(真 fork 子进程):META PLAYING → should_keep=1 → bg_add → + * 进程存活 + PLAYCTL 经会话 fd 往返(后台化不断连)→ take 恢复 → META STOPPED → + * should_keep=0(停播后再退出=真退); + * 3) 后台 App 崩溃照收:子进程被杀 → bg_tick WNOHANG 回收 → on_bg_dead + 清槽; + * 4) 卡拔例外:on_card=1 + card_present 假 0 → tick SIGTERM 回收(防 SIGBUS); + * 5) bg 表满(MEOW_BG_MAX)→ MEOW_ERR_NOMEM;重复 add → MEOW_ERR_EXISTS; + * 6) 服务注册:exec 不可执行 → NOENT;同名在跑 → EXISTS; + * 7) 退避重启 1s/2s + max_restart=2 熔断(假时钟注入):清通知回调 + 注册连接收到 + * MSG_SYS{SVC_DEAD=2} + circuit 后不再拉起;重注册复位熔断重新拉起; + * 8) MSG_SERVICE_STOP:SIGTERM + stopping,不触发重启,槽释放; + * 9) kill_all:bg+svc 全回收 + 注册连接收到 MSG_SYS{SHUTDOWN=1}。 + * 只依赖 meow_bgplay.c + meow_media.c + meow_ipc.h(纯 C),宿主 gcc 直编: + * gcc -Wall -Wextra -std=gnu99 test_bgplay.c meow_bgplay.c meow_media.c -o test_bgplay + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "meow_bgplay.h" +#include "meow_media.h" +#include "meow_ipc.h" + +static int g_fails = 0; +#define CHECK(cond, msg) do { \ + if (cond) printf("PASS: %s\n", msg); \ + else { printf("FAIL: %s\n", msg); g_fails++; } } while (0) + +/* ---- 假件(ops 注入)---- */ +static uint64_t g_ms = 100000; /* 假时钟(退避断言用,手动推进) */ +static int g_card = 1; /* 卡在位 */ +static int g_bg_dead = 0; /* on_bg_dead 计数 */ +static int g_bg_dead_status = 0; +static char g_bg_dead_app[MEOW_APP_ID_MAX]; +static int g_notif_cleared = 0; /* notif_clear_app 计数 */ +static char g_notif_cleared_app[MEOW_APP_ID_MAX]; + +static uint64_t fake_now_ms(void) { return g_ms; } +static int fake_card(void) { return g_card; } +static void fake_bg_dead(const char *app_id, pid_t pid, int status) +{ + (void)pid; + g_bg_dead++; + g_bg_dead_status = status; + snprintf(g_bg_dead_app, sizeof g_bg_dead_app, "%s", app_id); +} +static int fake_notif_clear(const char *app_id) +{ + g_notif_cleared++; + snprintf(g_notif_cleared_app, sizeof g_notif_cleared_app, "%s", app_id); + return 1; +} +/* send_frame:整帧写到 fd(socketpair 一端),对端读回校验。 */ +static size_t fake_send_frame(int fd, uint16_t type, uint32_t req_id, + const void *payload, uint32_t len) +{ + uint8_t hdr[MEOW_IPC_HDR_LEN]; + uint32_t magic = MEOW_IPC_MAGIC, rid = req_id, l = len; + uint16_t ver = MEOW_IPC_VERSION, t = type; + size_t off = 0; + + memcpy(hdr + 0, &magic, 4); memcpy(hdr + 4, &ver, 2); memcpy(hdr + 6, &t, 2); + memcpy(hdr + 8, &rid, 4); memcpy(hdr + 12, &l, 4); + if (write(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return 0; + off = sizeof hdr; + if (len && payload && write(fd, payload, len) > 0) off += len; + return off; +} +/* media ops.send 同 fake_send_frame(PLAYCTL 下发校验用) */ + +/* 从 socketpair 读一帧(poll 500ms 防挂)。返回负载字节数,<0=超时/失败。 */ +static int read_frame(int fd, uint16_t *type, void *pay, int paycap) +{ + struct pollfd pf = { .fd = fd, .events = POLLIN, .revents = 0 }; + uint8_t hdr[MEOW_IPC_HDR_LEN]; + uint32_t len; + + if (poll(&pf, 1, 500) <= 0) return -1; + if (read(fd, hdr, sizeof hdr) != (ssize_t)sizeof hdr) return -1; + memcpy(type, hdr + 6, 2); + memcpy(&len, hdr + 12, 4); + if (len > (uint32_t)paycap) return -1; + if (len && read(fd, pay, len) != (ssize_t)len) return -1; + return (int)len; +} + +/* fork 一个长眠子进程(300s),测试末统一 SIGKILL 清理。 */ +static pid_t spawn_sleeper(void) +{ + pid_t pid = fork(); + if (pid == 0) { sleep(300); _exit(0); } + return pid; +} + +/* 泵 tick 直到 cond 或超时(~2s):子进程死亡到 WNOHANG 可见有调度延迟。 */ +static int pump_bg_until(int (*cond)(void *), void *ctx) +{ + for (int i = 0; i < 100; i++) { + meow_bg_tick(); + if (cond(ctx)) return 1; + usleep(20000); + } + return 0; +} +static int pump_svc_until(int (*cond)(void *), void *ctx) +{ + for (int i = 0; i < 100; i++) { + meow_svc_tick(); + if (cond(ctx)) return 1; + usleep(20000); + } + return 0; +} +static int cond_bg_dead(void *ctx) { (void)ctx; return g_bg_dead > 0; } +static int cond_svc_reaped(void *ctx) +{ + const meow_svc_rt_t *s; + (void)ctx; + s = meow_svc_find("dev.test.svc", "daemon"); + return s && s->pid == 0; +} +static int cond_svc_gone(void *ctx) +{ + (void)ctx; + return meow_svc_find("dev.test.svc", "daemon") == NULL; +} + +/* 服务测试用长命可执行:shell 脚本 exec sleep 300(svc_spawn 内部 execve 走 shebang)。 */ +#define TEST_SVC_PATH "/tmp/meow_bgplay_test_svc.sh" +static void write_test_svc(void) +{ + FILE *f = fopen(TEST_SVC_PATH, "w"); + fputs("#!/bin/sh\nexec sleep 300\n", f); + fclose(f); + chmod(TEST_SVC_PATH, 0755); +} + +int main(void) +{ + meow_bgplay_ops ops; + meow_media_ops mops; + + memset(&ops, 0, sizeof ops); + ops.now_ms = fake_now_ms; + ops.card_present = fake_card; + ops.on_bg_dead = fake_bg_dead; + ops.notif_clear_app = fake_notif_clear; + ops.send_frame = fake_send_frame; + ops.log_dir = NULL; + meow_bgplay_init(&ops); + + memset(&mops, 0, sizeof mops); + mops.send = fake_send_frame; + meow_media_init(&mops); + + /* ---- 1) should_keep 判定矩阵 ---- */ + CHECK(meow_bg_should_keep(0, 0) == 0, "should_keep(未声明,未播放)=0"); + CHECK(meow_bg_should_keep(1, 0) == 0, "should_keep(声明,未播放)=0"); + CHECK(meow_bg_should_keep(0, 1) == 0, "should_keep(未声明,播放)=0"); + CHECK(meow_bg_should_keep(1, 1) == 1, "should_keep(声明,播放)=1"); + + /* ---- 2) 后台播放保活全链路 ---- */ + int sp[2]; + CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp) == 0, "socketpair 建立"); + { + m_meta_t meta; + memset(&meta, 0, sizeof meta); + snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1"); + meta.state = MEOW_MEDIA_PLAYING; + meta.dur_ms = 200000; + CHECK(meow_media_meta(&meta, sp[0]) == 0, "META 上报 PLAYING 注册媒体会话"); + } + CHECK(meow_media_app_playing("dev.test.bg1") == 1, "app_playing(bg1)=1"); + CHECK(meow_bg_should_keep(1, meow_media_app_playing("dev.test.bg1")) == 1, + "播放中退出 → 应保活"); + + pid_t kid = spawn_sleeper(); + CHECK(kid > 0, "fork 假 App 子进程"); + CHECK(meow_bg_add("dev.test.bg1", kid, sp[0], 0) == 0, "bg_add 转后台保活"); + CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 1, "is_backgrounded=1"); + CHECK(meow_bg_count() == 1, "bg count=1"); + CHECK(kill(kid, 0) == 0, "后台化后进程仍存活(未 SIGTERM/SIGSTOP)"); + /* PLAYCTL 经会话 fd 往返(后台化不断连:进程/fd 均保留) */ + CHECK(meow_media_playctl_to("dev.test.bg1", 6, 0) == 0, "PLAYCTL 下发到后台 App 会话"); + { + uint16_t t = 0; + m_playctl_t pc; + int n = read_frame(sp[1], &t, &pc, sizeof pc); + CHECK(n == (int)sizeof pc && t == MSG_PLAYCTL && pc.cmd == 6, + "对端收到 MSG_PLAYCTL cmd=6(控制通道后台不断连)"); + } + { + int rfd = -1; + pid_t rpid = 0; + CHECK(meow_bg_take("dev.test.bg1", &rfd, &rpid) == 0 && rpid == kid && rfd == sp[0], + "bg_take 回前台(pid/fd 原样归还)"); + } + CHECK(meow_bg_is_backgrounded("dev.test.bg1") == 0 && meow_bg_count() == 0, + "take 后清槽"); + meow_media_clear("dev.test.bg1"); + { + m_meta_t meta; + memset(&meta, 0, sizeof meta); + snprintf(meta.app_id, sizeof meta.app_id, "dev.test.bg1"); + meta.state = MEOW_MEDIA_STOPPED; + meow_media_meta(&meta, sp[0]); + } + CHECK(meow_bg_should_keep(1, meow_media_app_playing("dev.test.bg1")) == 0, + "停播后再退出 → should_keep=0(真退 SIGTERM)"); + kill(kid, SIGKILL); waitpid(kid, NULL, 0); + close(sp[0]); close(sp[1]); + meow_media_clear("dev.test.bg1"); + + /* ---- 3) 后台 App 崩溃照收 ---- */ + g_bg_dead = 0; + kid = spawn_sleeper(); + CHECK(meow_bg_add("dev.test.crash", kid, -1, 0) == 0, "bg_add(crash)"); + kill(kid, SIGKILL); + { + int ok = pump_bg_until(cond_bg_dead, NULL); + (void)ok; + } + CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.crash") == 0 && + WIFSIGNALED(g_bg_dead_status) && WTERMSIG(g_bg_dead_status) == SIGKILL, + "崩溃子进程被 tick 回收 + on_bg_dead(SIGKILL) 回调"); + CHECK(meow_bg_count() == 0, "崩溃回收后清槽"); + + /* ---- 4) 卡拔例外:on_card=1 + 卡不在 → SIGTERM 回收 ---- */ + g_bg_dead = 0; + kid = spawn_sleeper(); + CHECK(meow_bg_add("dev.test.card", kid, -1, 1) == 0, "bg_add(on_card=1)"); + g_card = 0; + pump_bg_until(cond_bg_dead, NULL); + CHECK(g_bg_dead == 1 && strcmp(g_bg_dead_app, "dev.test.card") == 0, + "卡拔 → 卡上后台 App SIGTERM 回收(防 SIGBUS)"); + g_card = 1; + + /* ---- 5) 表满 / 重复 ---- */ + { + pid_t k[MEOW_BG_MAX]; + char id[MEOW_APP_ID_MAX]; + int i, full = 0; + for (i = 0; i < MEOW_BG_MAX; i++) { + k[i] = spawn_sleeper(); + snprintf(id, sizeof id, "dev.test.full%d", i); + meow_bg_add(id, k[i], -1, 0); + } + snprintf(id, sizeof id, "dev.test.full%d", MEOW_BG_MAX); + { + pid_t extra = spawn_sleeper(); + full = (meow_bg_add(id, extra, -1, 0) == MEOW_ERR_NOMEM); + kill(extra, SIGKILL); waitpid(extra, NULL, 0); /* 未入表,就地清理 */ + } + CHECK(full, "bg 表满 → MEOW_ERR_NOMEM"); + snprintf(id, sizeof id, "dev.test.full%d", 0); + CHECK(meow_bg_add(id, 0, -1, 0) == MEOW_ERR_INVAL, "pid<=0 → INVAL"); + CHECK(meow_bg_add(id, k[0], -1, 0) == MEOW_ERR_EXISTS, "重复 app_id → EXISTS"); + for (i = 0; i < MEOW_BG_MAX; i++) { + snprintf(id, sizeof id, "dev.test.full%d", i); + meow_bg_take(id, NULL, NULL); + kill(k[i], SIGKILL); waitpid(k[i], NULL, 0); + } + } + CHECK(meow_bg_count() == 0, "take 全部后 bg 表空"); + + /* ================= 无头后台服务 ================= */ + write_test_svc(); + + /* ---- 6) 注册校验 ---- */ + CHECK(meow_svc_register("dev.test.svc", "daemon", "/nonexistent/x", 1, 2, 60000, 0, -1) + == MEOW_ERR_NOENT, "exec 不可执行 → NOENT(不留尸槽)"); + CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "NOENT 后无槽残留"); + + int sp_svc[2]; + CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp_svc) == 0, "svc socketpair 建立"); + int pid1 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, + 1, 2, 60000, 0, sp_svc[0]); + CHECK(pid1 > 0, "register 拉起服务"); + CHECK(meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, 1, 2, 60000, 0, sp_svc[0]) + == MEOW_ERR_EXISTS, "同名服务在跑 → EXISTS"); + + /* ---- 7) 退避 1s/2s + max=2 熔断 ---- */ + const meow_svc_rt_t *sl; + kill(pid1, SIGKILL); + pump_svc_until(cond_svc_reaped, NULL); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->pid == 0 && sl->restart_count == 1 && + sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_1, + "第 1 次死 → 1s 退避排程(restart_count=1)"); + g_ms += MEOW_SVC_BACKOFF_1; + meow_svc_tick(); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->pid > 0 && sl->pid != pid1, "到点重启:新 pid"); + + pid_t pid2 = sl->pid; + kill(pid2, SIGKILL); + pump_svc_until(cond_svc_reaped, NULL); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->pid == 0 && sl->restart_count == 2 && + sl->next_retry_ms == g_ms + MEOW_SVC_BACKOFF_2, + "第 2 次死 → 2s 退避排程(restart_count=2)"); + g_ms += MEOW_SVC_BACKOFF_2; + meow_svc_tick(); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->pid > 0, "第 2 次重启拉起"); + + g_notif_cleared = 0; + kill(sl->pid, SIGKILL); + pump_svc_until(cond_svc_reaped, NULL); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->pid == 0 && sl->circuit == 1 && sl->restart_count == 3, + "第 3 次死超 max_restart=2 → 熔断 circuit=1"); + CHECK(g_notif_cleared == 1 && strcmp(g_notif_cleared_app, "dev.test.svc") == 0, + "熔断清该 App 通知(notif_clear_app 回调)"); + { + uint16_t t = 0; + m_sys_t m; + int n = read_frame(sp_svc[1], &t, &m, sizeof m); + CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SVC_DEAD && + strcmp(m.svc, "daemon") == 0, + "注册连接收到 MSG_SYS{SVC_DEAD=2} 熔断广播"); + } + g_ms += 60000; + meow_svc_tick(); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->pid == 0, "熔断后不再拉起"); + + /* 重注册复位熔断 */ + int pid4 = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, + 1, 2, 60000, 0, sp_svc[0]); + CHECK(pid4 > 0, "熔断槽重注册 → 复位重新拉起"); + sl = meow_svc_find("dev.test.svc", "daemon"); + CHECK(sl && sl->circuit == 0 && sl->restart_count == 0, "熔断复位(circuit/count 清零)"); + + /* ---- 8) SERVICE_STOP:不重启 ---- */ + CHECK(meow_svc_stop("dev.test.svc", "daemon") == 0, "svc_stop 受理"); + pump_svc_until(cond_svc_gone, NULL); + CHECK(meow_svc_find("dev.test.svc", "daemon") == NULL, "stop 后槽释放(不重启)"); + CHECK(meow_svc_stop("dev.test.svc", "daemon") == MEOW_ERR_NOENT, "重复 stop → NOENT"); + close(sp_svc[0]); close(sp_svc[1]); + + /* ---- 9) kill_all:全回收 + SHUTDOWN 广播 ---- */ + { + int sp2[2]; + socketpair(AF_UNIX, SOCK_STREAM, 0, sp2); + int spid = meow_svc_register("dev.test.svc", "daemon", TEST_SVC_PATH, + 1, 3, 60000, 0, sp2[0]); + pid_t bgk = spawn_sleeper(); + meow_bg_add("dev.test.bg9", bgk, -1, 0); + CHECK(spid > 0 && meow_bg_count() == 1, "kill_all 前置:1 svc + 1 bg"); + meow_bgplay_kill_all(); + CHECK(meow_bg_count() == 0 && meow_svc_find("dev.test.svc", "daemon") == NULL, + "kill_all 后 bg/svc 全回收"); + CHECK(kill(spid, 0) < 0 && kill(bgk, 0) < 0, "子进程确已终止"); + uint16_t t = 0; + m_sys_t m; + int n = read_frame(sp2[1], &t, &m, sizeof m); + CHECK(n == (int)sizeof m && t == MSG_SYS && m.event == MEOW_SYS_SHUTDOWN, + "注册连接收到 MSG_SYS{SHUTDOWN=1}(关机前回收广播)"); + close(sp2[0]); close(sp2[1]); + } + + unlink(TEST_SVC_PATH); + printf(g_fails ? "== FAILURES: %d ==\n" : "== ALL PASS ==\n", g_fails); + return g_fails ? 1 : 0; +} diff --git a/patch_v2/meow_launcherd/util.c b/patch_v2/meow_launcherd/util.c index 792c78f..ee79f4f 100644 --- a/patch_v2/meow_launcherd/util.c +++ b/patch_v2/meow_launcherd/util.c @@ -870,6 +870,22 @@ static pthread_t ipc_pthread; static struct meow_ipc_conn ipc_conns[MEOW_IPC_CONNS]; +/* M4:连接存活跟踪(ipc_thread 单写者维护计数;主循环 meow_ipc_conn_alive 读)。 + * 竞态最坏=误判一帧(best-effort 熔断广播可接受;ipc_conns 前 ipc_conn_count 项恒为 + * 最新存活集,因关闭时shift压实)。 */ +static volatile int ipc_conn_count = 0; + +/* M4:查 fd 是否为当前存活 IPC 连接(熔断广播 MSG_SYS 投递前判定,防发到已断连接)。 */ +int meow_ipc_conn_alive(int fd) +{ + int n = ipc_conn_count; + if (fd < 0) return 0; + if (n > MEOW_IPC_CONNS) n = MEOW_IPC_CONNS; + for (int i = 0; i < n; i++) + if (ipc_conns[i].fd == fd) return 1; + return 0; +} + /* §10.5:待处理的 App 前台「展开通知中心」请求(ipc_dispatch 单写者置位;launch_app waitpid * 循环 meow_notif_open_take 读取并清除)。仅当 g_app_session_active(App 前台会话)期间才置位, * 否则回 ERR(无 App 前台,不展开)。 */ @@ -1046,6 +1062,7 @@ static void *ipc_thread(void *arg) fds[nfds].events = POLLIN; fds[nfds].revents = 0; nfds++; + ipc_conn_count = nfds - 1; logmsg("ipc: conn %d accept (nfds=%d)", c, nfds - 1); } else { close(c); @@ -1066,6 +1083,7 @@ static void *ipc_thread(void *arg) ipc_conns[j - 1] = ipc_conns[j]; } nfds--; + ipc_conn_count = nfds - 1; continue; } } @@ -1091,6 +1109,9 @@ int meow_ipc_init(void) g_media_ops.log = logmsg; meow_media_init(&g_media_ops); + /* M4:注入后台保活/后台服务 ops(logmsg/sd_device_present/通知清理/熔断发帧)。 */ + meow_bgplay_launcher_init(); + ipc_event_fd = eventfd(0, EFD_NONBLOCK); if (ipc_event_fd < 0) logmsg("ipc: eventfd fail %s (10ms poll drains anyway)", strerror(errno)); @@ -1119,6 +1140,9 @@ void meow_ipc_process(void) ipc_head++; } __sync_synchronize(); + /* M4:后台表周期维护挂点(主循环单写者排空点,run_menu/前台会话/idle/ipc-only + * 四处循环都经此处)——drain 后台 App stdout + 回收/卡拔 + 服务退避重启。 */ + meow_bg_housekeep(); } @@ -1269,6 +1293,72 @@ static void ipc_dispatch(struct meow_ipc_msg *m) n.app_id, n.nav_type, n.depth, n.current); break; } + /* M4 后台服务注册(FRAMEWORK_DESIGN §5/§9):无头服务 spawn/退避重启/熔断。 + * 校验链:acct 存在且 exec 非空(NOENT)→ 权限(app.json background=true 或 + * flags 含 MF_BACKGROUND,否则 PERM)→ service. 静态声明存在(NOENT)→ + * meow_svc_register 拉起。成功回 MSG_SERVICE_OK(m_ack_t:code=0,status=pid); + * 失败回 MSG_ERR(仿 ACT_QUERY 模式)。注册后调用方须保持连接(熔断广播投递判定)。 */ + case MSG_REGISTER: { + m_register_t r; + memset(&r, 0, sizeof r); + memcpy(&r, m->payload, m->plen < (uint16_t)sizeof r ? m->plen : (uint16_t)sizeof r); + r.app_id[MEOW_APP_ID_MAX - 1] = 0; + r.svc[15] = 0; + struct meow_acct *ac = meow_acct_find(r.app_id); + const struct meow_svc *sv = ac ? meow_acct_svc_find(ac, r.svc) : NULL; + int code = MEOW_OK; + int pid = 0; + if (!ac || !ac->exec[0]) + code = MEOW_ERR_NOENT; + else if (!(meow_acct_background_perm(ac) & 1) && !(r.flags & MF_BACKGROUND)) + code = MEOW_ERR_PERM; + else if (!sv) + code = MEOW_ERR_NOENT; + else { + char sexec[160]; + meow_svc_exec_path(ac->exec, r.svc, sexec, sizeof sexec); + pid = meow_svc_register(r.app_id, r.svc, sexec, + sv->restart, sv->max, sv->window_ms, + strncmp(sexec, MEOWAPPS, strlen(MEOWAPPS)) == 0, + m->fd); + if (pid < 0) { code = pid; pid = 0; } + } + if (code == MEOW_OK) { + m_ack_t a; + a.code = MEOW_OK; + a.status = (uint32_t)pid; /* 服务子进程 pid */ + ipc_send(m->fd, MSG_SERVICE_OK, m->req_id, &a, sizeof a); + logmsg("ipc: REGISTER app=%s svc=%s -> SERVICE_OK pid=%d", r.app_id, r.svc, pid); + } else { + m_err_t e; + memset(&e, 0, sizeof e); + e.code = (uint32_t)code; + const char *msg = (code == MEOW_ERR_PERM) ? "no background permission" + : (code == MEOW_ERR_EXISTS) ? "service already running" + : (code == MEOW_ERR_NOMEM) ? "service slots full" + : "app/service not found"; + e.msg_len = (uint16_t)strlen(msg); + memcpy(e.msg, msg, e.msg_len); + ipc_send(m->fd, MSG_ERR, m->req_id, &e, sizeof e); + logmsg("ipc: REGISTER app=%s svc=%s -> ERR(%d)", r.app_id, r.svc, code); + } + break; + } + /* M4:请求停止自己已注册的无头服务(SIGTERM,标记 stopping 不触发退避重启)。 */ + case MSG_SERVICE_STOP: { + m_service_stop_t s; + memset(&s, 0, sizeof s); + memcpy(&s, m->payload, m->plen < (uint16_t)sizeof s ? m->plen : (uint16_t)sizeof s); + s.app_id[MEOW_APP_ID_MAX - 1] = 0; + s.svc[15] = 0; + int rc = meow_svc_stop(s.app_id, s.svc); + m_ack_t a; + a.code = (uint32_t)(rc < 0 ? -rc : MEOW_OK); + a.status = 0; + ipc_send(m->fd, MSG_ACK, m->req_id, &a, sizeof a); + logmsg("ipc: SERVICE_STOP app=%s svc=%s -> ACK code=%u", s.app_id, s.svc, a.code); + break; + } /* AMSD_DESIGN §4.3:amsd `am start`/`am kill` 控制请求。主循环单写者仅置位 pending * (run_menu 前台循环 / launch_* waitpid 循环消费),不回执启动结果(一期 best-effort)。 */ case MSG_AMS_START: { diff --git a/scripts/build/fw_content.conf b/scripts/build/fw_content.conf index f0d4716..cbb919d 100644 --- a/scripts/build/fw_content.conf +++ b/scripts/build/fw_content.conf @@ -28,7 +28,7 @@ # ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)---- patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|4441822d87e76bfce2ce584604ac3830|引导选单 M2.85(sysui让位 agent-216) -patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|1332c67f4dba8727feae1c93f22fc848|主屏 launcher M2.95+(§10.5 叠加媒体保活:前台 App 播放中开通知中心不 SIGSTOP,SDK present/输入见 /tmp/meow_notif_active 自抑) +patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|c07d011c0769fca14d3bfb752d735463|主屏 launcher M4(后台服务保活+后台播放:MSG_REGISTER/SERVICE_STOP/MSG_SYS 退避熔断;nav-exit 播放中转后台保活,SDK present 自抑非 SIGSTOP) patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|553840cdc79d8d7bed766a5e3981ac57|应用看门狗 M2.95(+音量反转修复) patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187) mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194)