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

+3 -2
View File
@@ -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 <path>]`。验证:`meow_ipc hello`→ACK、播放器不动(MeowOS 模式下无播放器即满足)。
- ✅ **M1 路由表 + 解析**(0.7.10,M2.15):`app.json` 扁平行 `route.<id>=path` / `framework=true` / `service.<svc>=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 <app> <activity>` 返回链路(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`)。
+4 -4
View File
@@ -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_<bootts>.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 @@
- `<pkg>/` = 程序主体+app.json+资源;`MeowData/<pkg>/{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.<id>`/`awake.<id>`(keep_awake)/`service.<svc>`/`fullscreen`(app 级;`route.<id>.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.<id>`/`awake.<id>`(keep_awake)/`service.<svc>`(M4 起服务二进制=App 同目录 `<svc>` 文件)/**`background`/`background_audio`(M4 已落地:无头服务注册权限/后台播放保活)**/`fullscreen`(app 级;`route.<id>.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]`
+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();
+1 -1
View File
@@ -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
+22 -3
View File
@@ -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);
+509
View File
@@ -0,0 +1,509 @@
/* meow_bgplay.c — M4 后台播放保活 + 无头后台服务运行时表(见 meow_bgplay.h 头注释)。 */
#include "meow_bgplay.h"
#include <stdarg.h>
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
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);
}
+139
View File
@@ -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.<svc>` 声明的服务: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 <stdint.h>
#include <stddef.h>
#include <sys/types.h>
#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 */
+36 -3
View File
@@ -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.<svc>=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.<svc>) */
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];
@@ -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.<id> 行数 */
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.<svc>;无返回 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 二进制同目录下的 <svc> 名文件。 */
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);
Binary file not shown.
+376
View File
@@ -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 <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdint.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <poll.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#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;
}
+90
View File
@@ -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.<svc> 静态声明存在(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: {
+1 -1
View File
@@ -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)