Compare commits
6
Commits
7e68313981
...
2f22e26de7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f22e26de7 | ||
|
|
3c5612f7c9 | ||
|
|
45c633868f | ||
|
|
effce6d0db | ||
|
|
2dc4cad454 | ||
|
|
f3ae06d7cd |
No files matched your search
+133
-5
@@ -163,9 +163,12 @@ meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解
|
||||
meow_img *meow_img_load_mem(const void *buf, int len);
|
||||
void meow_img_free(meow_img *img);
|
||||
|
||||
/* 查询 */
|
||||
int meow_img_w(meow_img *img);
|
||||
int meow_img_h(meow_img *img);
|
||||
/* 查询(const 正确) */
|
||||
int meow_img_w(const meow_img *img);
|
||||
int meow_img_h(const meow_img *img);
|
||||
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。
|
||||
* meow_img 对外 opaque,本函数为其唯一像素读入口;NULL 返回 NULL。 */
|
||||
const unsigned char *meow_img_pixels(const meow_img *img);
|
||||
|
||||
/* 绘制:整图 / 缩放 / 带 alpha */
|
||||
void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */
|
||||
@@ -177,6 +180,33 @@ void meow_ui_img_cover(int x, int y, int size, meow_img *img); /* 居
|
||||
```
|
||||
|
||||
> **裁剪/缩放**:软渲用最近邻(快),不做高质量插值(省 CPU)。封面用 `meow_ui_img_cover` 居中裁方 + 缩放到目标尺寸即可。
|
||||
>
|
||||
> **内存瘦身**:封面/背景大图驻留会耗尽 anon(设备 RAM 仅 56MB);`meow_img_downscale(im, maxdim)` 最近邻等比缩放到最长边 ≤maxdim(原图已够小返回 NULL),调用方负责 free 旧/新图。
|
||||
|
||||
### 4.1 封面取色(`meow_img_color_*`,批5)
|
||||
|
||||
独立公共头 `meow_img_color.h`(仿 `meow_lyric_view.h` 模式,**不随 `meow_ui.h` 伞头发布**;App/launcher 显式 `#include "meow_img_color.h"` 并单链 `meow_img_color.c`——**不并入 `meow_ui.a`**,避免并发重编/四 App 重链)。从解码后的 RGBA 像素或 `meow_img` 句柄提取「主色 + 次色」,供播放页/歌词页动态主题(背景渐变/文字 accent)。
|
||||
|
||||
```c
|
||||
/* 从 RGBA 像素缓冲取主色+次色(纯算法;样本副采样)。返回 0=成功;-1=失败。 */
|
||||
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
|
||||
uint32_t *main, uint32_t *second);
|
||||
/* 从 meow_img 句柄取主色+次色(经 meow_img_pixels 读像素)。返回 0=成功;-1=失败。 */
|
||||
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second);
|
||||
/* 由主/次色派生播放页 + 歌词页封面主题(纯函数;th 提供默认回退色)。 */
|
||||
typedef struct meow_img_color_theme {
|
||||
uint32_t bg_top, bg_bottom; /* 播放页渐变顶/底(主/次各压暗 55%) */
|
||||
uint32_t fg, fg_dim; /* 背景上可读文字色 + 次要文字色 */
|
||||
uint32_t accent, accent_fg; /* 强调色(控件/进度)+ 其上可读图标色 */
|
||||
meow_lyric_view_colors lyr; /* 歌词页 set_colors 覆盖(scrim=主色压暗 α176) */
|
||||
} meow_img_color_theme;
|
||||
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
|
||||
meow_img_color_theme *out);
|
||||
```
|
||||
|
||||
- **取色语义**:输入 RGBA(stb_image 输出,4 通道 straight alpha),最多遍历 ~3.6k 采样(内部自适应副采样步进,等效缩略图成本,大图/缩略图成本恒定);色域分桶 **4×4×4=64**(RGB 各 4 段),跳过近黑(luma<24)/近白(luma>244)/透明(alpha<64),按「像素数 + 饱和度」加权(偏鲜艳);**主色**=加权最大桶中心;**次色**=与主色 RGB 色距最大的显著桶(≥主桶 1/7≈14%)中心;无显著桶 → 次色=主色缩暗 2/3(渐变互补)。返回 -1=参数非法/有效采样不足(调用方回退全局 `meow_theme`)。
|
||||
- **派生 `meow_img_color_theme`**:`bg_top/bg_bottom`(主/次各压暗 55% 防刺眼 + 保文字可读)、`fg`(主/次平均亮度选黑/白)、`fg_dim`(向背景靠拢的次要文字)、`accent=次色`、`accent_fg`(accent 上可读图标色)、`lyr`(歌词页 `meow_lyric_view_set_colors` 覆盖:scrim=主色压暗(α176)、cur=fg,gray 留 0 由 lyric_view 以 cur 自动递减)。
|
||||
- **经 `meow_img_pixels` 读像素**(本 TU 已上收 agent-402 遗留的「局部补全结构体」):`meow_img_duocolor` 不再局部补全 opaque 布局,改用 §4 正式访问器。
|
||||
|
||||
---
|
||||
|
||||
@@ -933,15 +963,20 @@ MeowOS 的声卡由**独占音频守护 `meow_audiod`** 持有(`hw:0,0`;M2.9
|
||||
|
||||
```c
|
||||
int meow_audio_open(int rate, int ch, int fmt); /* 返回 stream_id(>0);<0=错误(-EBUSY 被占且被抢等) */
|
||||
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags); /* 带 flags(A3+DSD:flags=AUDIO_FLAG_DSD 且 fmt=AUDIO_FMT_DOP → DoP DSD 流) */
|
||||
int meow_audio_write(const void *pcm, int frames);/* 阻塞(socket 背压);返回写入帧数, <0=流已死 */
|
||||
int meow_audio_pause(int sid, int paused); /* 1=暂停 0=恢复 */
|
||||
int meow_audio_close(int sid); /* 关流 */
|
||||
int meow_audio_query(meow_audio_state *out); /* 当前流/路由/音量/underrun 快照 */
|
||||
int meow_audio_set_volume(int pct); /* audiod 唯一音量权威:写 DAC + DSD_VOLUME 双组 + 持久化 */
|
||||
/* 被抢占回调(可选):收到 MSG_AUDIO_PREEMPTED 时调(SDK 内部读线程分发) */
|
||||
void meow_audio_on_preempt(void (*cb)(const char *by_app, void *ctx), void *ctx);
|
||||
```
|
||||
|
||||
- **fmt 枚举**:1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE(v1 实装 S16/S24/S32);服务端做**格式归一 + DAC 原生率跟随**(无 SRC,44.1-192k 全覆盖)。
|
||||
- **fmt 枚举**:1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE 5=AUDIO_FMT_DOP(v1 实装 S16/S24/S32;FLOAT v1 拒绝);服务端做**格式归一 + DAC 原生率跟随**(无 SRC,44.1-192k 全覆盖)。
|
||||
- **`meow_audio_open_ex` / DoP DSD(A3+DSD 定案,勿回退)**:比 `meow_audio_open` 多 `flags`。`flags=AUDIO_FLAG_DSD(0x1)` 且 `fmt=AUDIO_FMT_DOP(=5)` → **DoP DSD 流**:App 侧把 DSD 解码封装为 DoP(每样本 32-bit 容器:bits[31:16]=16bit DSD 数据、bits[15:8]=标记 0xFA/0x05、bits[7:0]=0;独立格式值避免 S24 名义(3B)/实际(4B)歧义);audiod **只透传**(不引 dsd2pcm、不做 DOP_EN 之外处理);DSD 打不开/不支持 → open 返回负值、报错关流(**不降级**,勿静默回退 PCM 伪装)。
|
||||
- **`meow_audio_state.dsd`(追加字段,ABI 兼容)**:当前流是否为 DoP DSD(`fmt==AUDIO_FMT_DOP` 且 audiod 已授权)。`meow_audio_state` 其它字段不变,此键为追加、旧结构体兼容。
|
||||
- **`meow_audio_set_volume`(音量收编 v2,audiod 唯一权威)**:写 DAC numid=1/2 + DSD_VOLUME **双组**(保切换连续)+ 持久化 `settings.conf audio.volume`(与 launcher/watchdog 同源同键)。BT 输出目标时音量由 AVRCP 侧控,audiod 不下发 DAC 反转。返回 0 成功;<0=audiod 不在。
|
||||
- **独占+抢占**:后开流赢,旧流收 `MSG_AUDIO_PREEMPTED`(App 收到自行暂停 UI);**socket EOF=回收**(客户端崩溃/被 SIGKILL 不泄漏)。流绑定 socket 连接而非 App 前台状态(后台播放见 AUDIOD_DESIGN §3)。
|
||||
- **懒连接 + 断线重连**:open 时连 `/usr/data/meow_audio.sock`;write 遇 EPIPE → 标记流死,App 需重新 open。**不做隐式直开回退**:audiod 不在 → open 返回 -ENOENT;调试可 `MEOW_AUDIO_DIRECT=1` 允许 App 自带直开路径(仅开发用,非正式能力)。
|
||||
- **调试 CLI**(root 免鉴权,ssh 直呼):`meow_audio status` / `meow_audio tone [freq] [ms] [rate]`(正弦测试音,排查无声不用开 App)/ `route <0|2|3>` / `vol [0-100]` / **`monitor [秒]`**(261,终端实时电平表:200ms 刷新 rms/peak dBFS + 文本电平条,缺省 10s,0=持续)。
|
||||
@@ -1326,11 +1361,20 @@ patch_v2/meow_launcherd/meow_font.a # 复用(静态链接)
|
||||
"version": "0.1",
|
||||
"framework": 1, // 1=走 meow_ui(框架承载);缺省/0=裸自绘(旧)
|
||||
"immersive": 0, // 1=沉浸模式(隐藏虚拟返回键,左缘滑动返回)
|
||||
"icon": "apps/os.meow.hello/icon.png" // 可选:列表图标(供图片层)
|
||||
"icon": "apps/os.meow.hello/icon.png", // 可选:列表图标(供图片层)
|
||||
"background": 0, // M4:可注册无头后台服务(MSG_REGISTER 权限位0)
|
||||
"background_audio": 1, // M4:播放中退出转后台保活(媒体会话活跃=保活,见 §10.5)
|
||||
"background_keepalive": 0 // 批①:进入后台即保活(不依赖媒体会话)=通用常驻
|
||||
}
|
||||
```
|
||||
上例 `<pkg>`=`os.meow.hello`(= `apps/<pkg>/` 目录名 = `package` 字段值);exec 即 `<挂载点>/MeowAPPs/<pkg>/<binary>`。
|
||||
|
||||
> **app 后台常驻语义(M4 + 批①,launcher `meow_bgplay.c` 判定,勿与 `background`(无头服务注册)混用)**:
|
||||
> - `background_audio=true`(M4):媒体会话**活跃**(PLAYING/PAUSED/BUFFER)即保活;暂停也驻留(补 M4 仅 PLAYING 缺口,根治「声明后台播放的 App 一暂停就被杀」)。ncm 已声明。
|
||||
> - `background_keepalive=true`(批①):**进入后台即保活**,不依赖媒体会话=**通用常驻**(声音/无媒体会话的 App 亦可常驻)。是 `background_audio` 的超集(`meow_bg_should_keep`:keepalive=1 恒驻留;否则 bg_audio && media_resident)。
|
||||
> - `background=true`(M4):仅注册**无头后台服务**(不同义于保活;`service.<svc>` 声明服务策略)。
|
||||
> - 解析器注意 3 键前缀关系(`background` 是 `background_audio` 前缀、`background_keepalive` 又是 `background` 前缀):键匹配独立(`json_str` ident 前缀隔离),命中顺序靠 `strstr` 先行匹配无副作用——`"background_keepalive":true` 行即使先被 `"background"` 分支命中,`json_str(buf,"background")` 读出的也是 `_keepalive…`≠`true`,自然留 0,无害。**枚举/语义见 `devdocs/plan/APP_BACKGROUND_KEEPALIVE_P1.md`**。
|
||||
|
||||
> **app 字体来源(M2.x 已落地)**:app 若**自带字库**(`load_handle`,见 §5.7.3,**未实现**)则用它;否则 SDK `meow_ui_gfx.c load_font()` **优先读 launcher 注入的环境变量** `MEOW_FONT_PNG`/`MEOW_FONT_IDX`(=系统 `font.file` 当前生效字体,绝对路径)/`MEOW_FONT_APP`(family),无 env 则回退卡默认 `MeowOS/fonts/default/regular/font.png`(M2.6 字重结构)→ 固件。因此**app 未自带字库时跟随系统 `font.file`**,不再硬编码 `MeowAPPs/fonts`。
|
||||
>
|
||||
> **app 多字体(M2.5x,`display.multi_font`)**:launcher 注入 `MEOW_MULTI_FONT=0/1` 与 6 类 `MEOW_FONT_CLS_<类>_PNG/IDX`(命中则覆盖全局;类值为空=跟随全局不注入)。SDK `meow_ui_gfx` 的 `meow_gfx_load_multifont()` 读 env → 调 `meow_font_config_multifont()`(同 png 去重、≤4 套上限、缺类回退全局)构建**逐字符分类路由**:`meow_font_drawtext/width_text/line_height` 按 `meow_font_class_of(cp)`(`MEOW_FCLS_CJK/_CJK_SYM/_NUM/_LATIN/_LATIN_SYM/_OTHER`)用对应字库取字形与度量;`meow_font_has_glyph(cp)` 可预判缺字。`display.multi_font=0`/无 env → 单字体(现状,零额外开销)。底层接口见 meow_font.h:`meow_font_load_slot`/`meow_font_unload_slot`/`meow_font_enable_multifont`/`meow_font_set_class_slot`/`meow_font_config_multifont`;`MEOW_MAX_SLOTS=5`。
|
||||
@@ -1746,5 +1790,89 @@ int meow_cachegov_backoff_level(void); /* 0=正常 1=keep_hot 放
|
||||
- [D6](M7.0a)新增 `meow_iconbtn_create`(圆形/位图图标按钮,§5.8.6)+ `meow_theme_export`/`meow_theme_inject`(§8.1.5 色板导出/注入);`meow_view_dispatch_event` 接入 `meow_ui_run` 主循环(§6.4 之上,TAP/KEY/SWIPE/LONG_PRESS 先经 view 路由、命中即消费、未命中回落 `on_*`);`meow_view` 的 enter/leave 递归 children 已实现 + `meow_view_add_child` 在父进入后挂子立即进入;`meow_view_sub_push/pop` 标注为废弃(设计定案 b 不做页内子导航栈)。
|
||||
- `meow_ui.h` 为对外权威签名;本文件 §3-§8 作为设计蓝本。
|
||||
|
||||
---
|
||||
|
||||
## 16. 歌词组件(`meow_lyric` / `meow_lyric_view` / `meow_media_lyric`,批1-3)
|
||||
|
||||
三件套把网易云 ncm 的歌词能力**通用化上收**,供 launcher Now Playing 与 ncm 等任意 App 复用;launcher 与 ncm 共用同一份实现,避免两端漂移。
|
||||
|
||||
### 16.1 `meow_lyric`(批1:LRC 时间轴解析 / 查询 / 折行)
|
||||
|
||||
独立公共头 `meow_lyric.h`(**不随 `meow_ui.h` 伞头发布**;App 显式 `#include "meow_lyric.h"`)。纯 libc + pthread、**不依赖其它 SDK 对象**(不含 meow_log,便于 non-meow_ui.a 使用方独立链入);宿主 gcc 可直接编译单测。
|
||||
|
||||
```c
|
||||
void meow_lyric_clear(void); /* 清空全部数据 */
|
||||
int meow_lyric_parse(const char *lrc, const char *tlrc); /* 解析原文+翻译,按时间升序排序,返回行数 */
|
||||
|
||||
int meow_lyric_count(void);
|
||||
int meow_lyric_line_at(int64_t ms); /* last timestamp<=ms;首戳前=0;末戳后=n-1 */
|
||||
const char *meow_lyric_line_text(int i); /* 第 i 行原文;越界 "" */
|
||||
const char *meow_lyric_line_tl(int i); /* 第 i 行翻译;越界/无翻译 "" */
|
||||
int64_t meow_lyric_line_time(int i); /* 第 i 行时间戳(ms);越界 -1(供「点行跳转」seek) */
|
||||
void meow_lyric_set_max_lines(int max); /* 行上限 [1,MEOW_LYRIC_LINES_MAX],下次 parse 生效 */
|
||||
|
||||
int meow_lyric_wrap(const char *txt, int size, int maxw,
|
||||
char seg[][MEOW_LYRIC_SEG_SZ], int max_rows,
|
||||
meow_lyric_width_fn width, void *ctx); /* 折行 ≤max_rows 段(宽测经回调) */
|
||||
```
|
||||
|
||||
- **容量/静态存储**:行数硬上限 `MEOW_LYRIC_LINES_MAX`(600);单行文本(原文/翻译)上限 `MEOW_LYRIC_TEXT_MAX`(192) 字节;折行单段 `MEOW_LYRIC_SEG_SZ`(256)。
|
||||
- **输入**:标准 LRC 行 `[mm:ss.xx]正文`;lrc=原文、tlrc=翻译,翻译按相同 timestamp 合并到对应原文行(存 tl),无匹配忽略。服务器字面序列:网易云把多行文本用字面 `\n`/`\r` 塞进一行(渲染豆腐/凌乱)→ 按空格替换(`\r\n`/`\n\r` 折单空格,语义同 ncm copy_lrc_text)。
|
||||
- **时间戳小数位自适应**:1 位=0.1s(×100)、2 位=0.01s(×10,标准 LRC 厘秒)、3 位=0.001s(×1,网易云 LRC 毫秒)、>3 位截断到毫秒——避免把网易云 3 位毫秒当 2 位厘秒解析导致时间放大。
|
||||
- **线程安全(单写多读,同 ncm lyric.c 交错安全)**:数据后台线程写(parse/clear/set_max_lines)、UI 线程读(count/line_at/line_text/line_tl/line_time);全部函数内部持同一 pthread 互斥、函数级粒度、无嵌套(清空/解析用 nolock 私有版防自锁死)。**读返回指针指向静态存储,在下次 parse/clear 前有效**(调用方不得跨重建持有)。
|
||||
|
||||
### 16.2 `meow_lyric_view`(批2:歌词页渲染控件)
|
||||
|
||||
独立公共头 `meow_lyric_view.h`(仿 meow_ui_view.h 模式,不随 `meow_ui.h` 伞头发布)。把 ncm 歌词页布局上收成 SDK 控件:活动行纯白(居中偏上锚定)、上下行按行距逐级灰阶、翻译行紧跟原文不叠行(块高含翻译行)、超宽折行(中西文兼容)、上方保底 ≥above_min 行已过、总视觉行数 ≤max_rows、点行 seek 命中与绘制一致。
|
||||
|
||||
```c
|
||||
meow_lyric_view *meow_lyric_view_create(int x,int y,int w,int h,meow_lyric_width_fn width,void *width_ctx);
|
||||
void meow_lyric_view_free(meow_lyric_view *lv);
|
||||
meow_widget *meow_lyric_view_widget(const meow_lyric_view *lv); /* 底层 W_CUSTOM 控件 */
|
||||
|
||||
void meow_lyric_view_set_pos(meow_lyric_view *lv, int64_t pos_ms); /* 播放器位置 */
|
||||
void meow_lyric_view_set_colors(meow_lyric_view *lv, const meow_lyric_view_colors *c); /* NULL=回主题默认 */
|
||||
void meow_lyric_view_set_layout(meow_lyric_view *lv, const meow_lyric_view_layout *ls); /* NULL=回内建默认 */
|
||||
void meow_lyric_view_set_empty(meow_lyric_view *lv, const char *txt); /* 无歌词占位;NULL=默认 */
|
||||
void meow_lyric_view_set_on_seek(meow_lyric_view *lv, meow_lyric_seek_fn cb, void *ctx); /* 点行 seek */
|
||||
|
||||
uint32_t meow_lyric_view_cur_color(const meow_lyric_view *lv); /* 主题默认或覆盖的活动行色 */
|
||||
uint32_t meow_lyric_view_line_color(const meow_lyric_view *lv, int dist); /* 距活动行 dist(0..5) 行色 */
|
||||
int meow_lyric_view_hit_line(meow_lyric_view *lv, int x, int y); /* 命中行下标;未命中 -1 */
|
||||
int meow_lyric_view_visible(meow_lyric_view *lv, int64_t pos_ms, meow_lyric_view_vis *out, int cap);
|
||||
```
|
||||
|
||||
- **数据来源**:绑定批1 `meow_lyric`(全局单例,线程安全)+ 播放位置(`set_pos`);宽度测量用回调注入(`meow_lyric_width_fn`,包 meow_ui_text_width;宿主测试传自测函数)。App 可只用查询接口(`visible`/`hit_line`)在自绘流程里复用布局逻辑。
|
||||
- **主题**:SDK 全局 `meow_theme` 为默认(明暗背景 scrim、活动行 onSurface=dark 纯白、灰阶递减);`meow_lyric_view_colors` 为批5 封面取色覆盖入口(**字段 0=用主题默认**):`scrim`(黑叠加封面之上)、`cur`(活动行色)、`gray[6]`(距活动行 0..5 灰阶;gray[0]=cur,字段 0=自动递减)、`scrim_alpha`(<0=默认176,0=不画 scrim)。
|
||||
- **布局参数**(`meow_lyric_view_layout`,NULL=内建默认):`pad`(24)/`sub_lh`(44)/`cur_sz`(28)/`oth_sz`(24)/`tl_sz`(20)/`max_rows`(9,≤`MEOW_LYRIC_VIEW_MAX_ROWS`=9)/`above_min`(2)/`above_off`(50 偏上)。
|
||||
- **可见条目**(`meow_lyric_view_vis`,布局/命中/绘制共用同一算法,保证「点行 seek 命中与绘制一致」):`ent`=歌词行下标、`rw`=原文折行段数、`bh`=块高(子行数)=rw+当前行翻译占 1、`sz`=字号、`y`=块顶 y(绝对屏幕坐标)。
|
||||
|
||||
### 16.3 `meow_media_lyric_*`(批3:媒体会话 live 歌词源)
|
||||
|
||||
`meow_media.h`(launcher 侧**媒体会话注册表**,launcher/ncm 共用)。App 声明 MF_PLAYBACK + 上报 `MSG_META` → 成为媒体源;最近活跃者=当前源;通知中心媒体卡片 / Now Playing / 物理媒体键 / 控制器统一消费。
|
||||
|
||||
```c
|
||||
int meow_media_meta(const m_meta_t *m, int fd); /* 按 app_id 归并会话;-1=会话满/参数非法 */
|
||||
const meow_media_session_t *meow_media_current(void); /* 最近活跃源;无返回 NULL */
|
||||
int meow_media_has_source(void); /* 当前源存在且 state!=STOPPED */
|
||||
int meow_media_app_playing(const char *app_id); /* 叠加媒体保活(WORK_STATE §10.5):state==PLAYING 且匹配 */
|
||||
int meow_media_app_resident(const char *app_id); /* 批①常驻判定:会话存在且 state!=STOPPED */
|
||||
int meow_media_playctl(uint32_t cmd, int64_t seek_ms); /* 向当前源下发 PLAYCTL */
|
||||
int meow_media_playctl_to(const char *app_id, uint32_t cmd, int64_t seek_ms);
|
||||
void meow_media_clear(const char *app_id); /* 清某 app 会话;空/NULL=清全部 */
|
||||
void meow_media_tick(void); /* ~1s:播放/缓冲超时清槽回空态 */
|
||||
uint32_t meow_media_display_pos(const meow_media_session_t *s);/* 播放中按墙钟推进(clamp);暂停/缓冲原样 */
|
||||
|
||||
/* 歌词(批3 · live lyric 数据源) */
|
||||
int meow_media_lyric(const m_lyric_t *l); /* 上报当前曲歌词分片;-1=会话不存在/参数非法 */
|
||||
int meow_media_lyric_valid(const meow_media_session_t *s); /* 会话携带有效歌词(lrc 非空) */
|
||||
const char *meow_media_lyric_lrc(const meow_media_session_t *s); /* 原文;无则 "" */
|
||||
const char *meow_media_lyric_tlrc(const meow_media_session_t *s); /* 翻译;无则 "" */
|
||||
```
|
||||
|
||||
- **会话**:一个 App 一个槽(`MEOW_MEDIA_MAX`=4,app_id 归并防占满);当前源=最近活跃(`last_active_ms`/`active_seq` 最大)。源死亡判定:播放中(1)/缓冲(3) 超 `MEOW_MEDIA_STALE_MS`(30000) 无 META → 清槽回空态(App 死/服务断);暂停(2)/停止(0) 不超时。`state`:STOPPED=0 / PLAYING=1 / PAUSED=2 / BUFFER=3。
|
||||
- **live 歌词(批3)**:App 经 `MSG_META_LYRIC` 分片上报当前曲歌词 → 按 app_id 归并会话内有序缓冲(`lyric_lrc`/`lyric_tlrc`,超 `MEOW_MEDIA_LRC_MAX`/`TLRC_MAX` 截断);`lyric_seq`=歌曲代际(新歌/新词自增,launcher 见新 seq 清旧词)。**取数链**:桌面歌词页 `meow_media_lyric_lrc/tlrc` 取文本 → 批1 `meow_lyric_parse` 解析 → `meow_lyric_line_at(meow_media_display_pos(s))` → 批2 `meow_lyric_view` 实时滚动。
|
||||
- **依赖注入**:发送/日志经 `meow_media_ops`(launcher 用 `ipc_send`/`logmsg`;宿主单测用 socket write/NULL),可独立单测(`test_media.c`)。发送 `MSG_META_LYRIC` 前 App 须先报 `MSG_META` 注册会话,否则歌词被丢弃(防 fd=0 槽)。
|
||||
|
||||
---
|
||||
*文档基于 2026-08-29 现有渲染基建(M2.16 无闪烁地基 + meow_font + blend565)编写;API 名会随 B1 实现微调,实现时以 `meow_ui.h` 为准。*
|
||||
@@ -956,6 +956,41 @@ static void handle_out(char **t, int nt)
|
||||
out_append(pkg, pid, epoch, up, text);
|
||||
}
|
||||
|
||||
/* 批①后台常驻上报(单向 best-effort,launcher → amsd):
|
||||
* EV BG <app> <pid> → AMS_BG(转后台保活)
|
||||
* EV FG <app> → AMS_FG(回前台)。旧 launcher 不发、本层不识别也不崩。 */
|
||||
static void handle_bg(char **t, int nt)
|
||||
{
|
||||
if (nt < 2) return;
|
||||
const char *pkg = t[0];
|
||||
pid_t pid = (pid_t)atoi(t[1]);
|
||||
long epoch = (nt > 2) ? atol(t[2]) : (long)time(NULL);
|
||||
long up = (nt > 3) ? atol(t[3]) : read_uptime_sec();
|
||||
struct ams_app *a = app_get(pkg);
|
||||
if (!a) return;
|
||||
a->state = AMS_BG;
|
||||
a->pid = pid;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof detail, "pid=%ld", (long)pid);
|
||||
hist_push(a, "bg", detail, epoch, up);
|
||||
emit_log("BG %s pid=%ld", pkg, (long)pid);
|
||||
}
|
||||
|
||||
static void handle_fg(char **t, int nt)
|
||||
{
|
||||
if (nt < 1) return;
|
||||
const char *pkg = t[0];
|
||||
long epoch = (nt > 1) ? atol(t[1]) : (long)time(NULL);
|
||||
long up = (nt > 2) ? atol(t[2]) : read_uptime_sec();
|
||||
struct ams_app *a = app_find(pkg);
|
||||
if (!a) return;
|
||||
a->state = AMS_FG;
|
||||
char detail[192];
|
||||
snprintf(detail, sizeof detail, "bg->fg");
|
||||
hist_push(a, "fg", detail, epoch, up);
|
||||
emit_log("FG %s", pkg);
|
||||
}
|
||||
|
||||
/* ---- §6.2 二期:picker 编排(amsd 路由权威) ---- */
|
||||
|
||||
static struct ams_picker *picker_find(const char *action)
|
||||
@@ -1252,6 +1287,8 @@ static int dispatch_line(char *line, char *resp, int rn)
|
||||
if (!strcmp(tok[0], "LAUNCH")) { handle_launch(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "EXIT")) { handle_exit(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "OUT")) { handle_out(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "BG")) { handle_bg(tok + 1, nt - 1); }
|
||||
else if (!strcmp(tok[0], "FG")) { handle_fg(tok + 1, nt - 1); }
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,8 @@ static int fmt_bps(int fmt)
|
||||
case AUDIO_FMT_S16: return 2;
|
||||
case AUDIO_FMT_S24:
|
||||
case AUDIO_FMT_S32:
|
||||
case AUDIO_FMT_FLOAT: return 4;
|
||||
case AUDIO_FMT_FLOAT:
|
||||
case AUDIO_FMT_DOP: return 4;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
@@ -150,6 +151,7 @@ static const char *fmt_name(int fmt)
|
||||
case AUDIO_FMT_S24: return "S24";
|
||||
case AUDIO_FMT_S32: return "S32";
|
||||
case AUDIO_FMT_FLOAT: return "FLOAT";
|
||||
case AUDIO_FMT_DOP: return "DoP";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
@@ -318,14 +320,14 @@ static size_t gen_sine(uint8_t *buf, size_t bufcap, int grant_fmt, int grant_ch,
|
||||
return done;
|
||||
}
|
||||
|
||||
static int cmd_tone(int freq, int ms, int rate)
|
||||
static int cmd_tone(int freq, int ms, int rate, int fmt)
|
||||
{
|
||||
m_audio_open_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "audio-cli");
|
||||
o.rate = (uint32_t)rate;
|
||||
o.ch = 2;
|
||||
o.fmt = AUDIO_FMT_S16;
|
||||
o.fmt = (uint16_t)fmt;
|
||||
uint8_t rbuf[512]; uint16_t rt;
|
||||
int n = req_resp(MSG_AUDIO_OPEN, &o, sizeof o, &rt, rbuf, sizeof rbuf);
|
||||
if (n < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
|
||||
@@ -386,7 +388,7 @@ static int cmd_tone(int freq, int ms, int rate)
|
||||
|
||||
static void usage(const char *argv0)
|
||||
{
|
||||
fprintf(stderr, "usage: %s status | tone [freq] [ms] [rate] | route <0|2|3> | vol [0-100] | output <dac|bt> | monitor [秒]\n"
|
||||
fprintf(stderr, "usage: %s status | tone [freq] [ms] [rate] [S16|S24|S32] | route <0|2|3> | vol [0-100] | output <dac|bt> | monitor [秒]\n"
|
||||
" output: 切换输出目标(dac=本地 hw:0,0 / bt=蓝牙 A2DP source;bt 时音量=AVRCP 侧控)\n"
|
||||
" monitor: 终端实时电平表(200ms 刷新+文本电平条;缺省 10s,0=持续到 Ctrl-C)\n"
|
||||
" env: MEOW_AUDIO_SOCK (socket 缺省 %s)\n", argv0, AUD_SOCK_DEFAULT);
|
||||
@@ -432,9 +434,16 @@ int main(int argc, char **argv)
|
||||
int freq = (argc >= 3) ? atoi(argv[2]) : 440;
|
||||
int ms = (argc >= 4) ? atoi(argv[3]) : 500;
|
||||
int rate = (argc >= 5) ? atoi(argv[4]) : 44100;
|
||||
int fmt = AUDIO_FMT_S16;
|
||||
if (argc >= 6) {
|
||||
if (!strcmp(argv[5], "S16")) fmt = AUDIO_FMT_S16;
|
||||
else if (!strcmp(argv[5], "S24")) fmt = AUDIO_FMT_S24;
|
||||
else if (!strcmp(argv[5], "S32")) fmt = AUDIO_FMT_S32;
|
||||
else { fprintf(stderr, "tone: fmt S16|S24|S32(DoP 由 App 侧生成,CLI 不产 DoP)\n"); return 2; }
|
||||
}
|
||||
if (freq <= 0 || ms <= 0 || rate <= 0) { fprintf(stderr, "tone: bad params\n"); return 2; }
|
||||
if (g_fd < 0) { fprintf(stderr, "meow_audio: no audiod (%s) — 音频服务不可用\n", g_sock); return 1; }
|
||||
return cmd_tone(freq, ms, rate);
|
||||
return cmd_tone(freq, ms, rate, fmt);
|
||||
} else if (!strcmp(cmd, "monitor")) {
|
||||
int secs = (argc >= 3) ? atoi(argv[2]) : 10;
|
||||
if (secs < 0) secs = 10;
|
||||
|
||||
@@ -169,10 +169,11 @@ static int g_ring_wait_ms = RING_WAIT_DEFAULT;
|
||||
static volatile unsigned g_hb_io = 0;
|
||||
static volatile unsigned g_hb_play = 0;
|
||||
|
||||
/* 测试钩子(仅 env 显式开启;MEOW_AUDIO_TEST_WRITE_DELAY_MS / _WEDGE_AFTER_BYTES) */
|
||||
/* 测试钩子(仅 env 显式开启;MEOW_AUDIO_TEST_WRITE_DELAY_MS / _WEDGE_AFTER_BYTES / _AMIXER_TRACE) */
|
||||
static int g_test_write_delay_ms = 0;
|
||||
static long long g_test_wedge_after = -1; /* dev_write 累计字节≥此值后模拟设备 wedge(睡到 g_stop) */
|
||||
static uint64_t g_dev_bytes = 0;
|
||||
static char g_amixer_trace_path[256] = ""; /* MEOW_AUDIO_AMIXER_TRACE:记录每次 amixer cset */
|
||||
|
||||
/* ring(SPSC:IO 线程写,播放线程读;g_mu + g_cond 保护) */
|
||||
static uint8_t *g_ring = NULL;
|
||||
@@ -183,6 +184,7 @@ static size_t g_rpos = 0, g_wpos = 0;
|
||||
static int g_dev_fd = -1;
|
||||
static int g_dev_open = 0;
|
||||
static uint32_t g_dev_rate = 0, g_dev_ch = 0, g_dev_fmt = 0;
|
||||
static int g_dev_dsd = 0; /* 当前打开设备为 DoP DSD(决定 DOP_EN 硬件时序;g_mu 保护) */
|
||||
static int g_reconfig_pending = 0;
|
||||
|
||||
/* ---- 蓝牙 A2DP source 运行时状态(g_mu 保护;仅 g_target==OUT_BT && g_sink==SINK_ALSA 时用) ----
|
||||
@@ -323,7 +325,8 @@ static int fmt_bps(int fmt)
|
||||
case AUDIO_FMT_S16: return 2;
|
||||
case AUDIO_FMT_S24:
|
||||
case AUDIO_FMT_S32:
|
||||
case AUDIO_FMT_FLOAT: return 4;
|
||||
case AUDIO_FMT_FLOAT:
|
||||
case AUDIO_FMT_DOP: return 4; /* DOP=32-bit DoP 容器(16bit DSD + 8bit 标记 + 8bit 0),与 S32 同宽 */
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
@@ -334,6 +337,7 @@ static const char *fmt_name(int fmt)
|
||||
case AUDIO_FMT_S24: return "S24";
|
||||
case AUDIO_FMT_S32: return "S32";
|
||||
case AUDIO_FMT_FLOAT: return "FLOAT";
|
||||
case AUDIO_FMT_DOP: return "DoP";
|
||||
default: return "?";
|
||||
}
|
||||
}
|
||||
@@ -345,12 +349,17 @@ static const char *target_name(void)
|
||||
return (g_target == AUDIO_OUT_BT) ? "bt" : "dac";
|
||||
}
|
||||
|
||||
/* 授权给客户端的硬件最优格式。BT(A2DP source)=S16(codec 基座,pcm.bluealsa 前置 plug 层自动转
|
||||
* codec 协商率);DAC=hw 探测最优(FILE/NULL 宿主=S16)。 */
|
||||
static int target_grant_fmt(void)
|
||||
/* 授权给客户端的硬件最优格式。native_fmt=客户端请求格式。BT(A2DP source)=S16(codec 基座,
|
||||
* pcm.bluealsa 前置 plug 层自动转 codec 协商率);DAC=hw 探测最优(S32 升位/S16 直通);
|
||||
* FILE/NULL 宿主 sink:S24/S32 按请求**byte-exact 透传**(高清实测保真),S16 缺省,DOP 恒透传。 */
|
||||
static int target_grant_fmt(int native_fmt)
|
||||
{
|
||||
if (native_fmt == AUDIO_FMT_DOP) return AUDIO_FMT_DOP; /* DoP 恒透传(audiod 只做管道) */
|
||||
if (g_target == AUDIO_OUT_BT) return AUDIO_FMT_S16;
|
||||
return (g_sink == SINK_ALSA) ? g_hw_fmt : AUDIO_FMT_S16;
|
||||
if (g_sink == SINK_ALSA) return g_hw_fmt; /* DAC 探测最优(S32 偏好) */
|
||||
if (native_fmt == AUDIO_FMT_S24 || native_fmt == AUDIO_FMT_S32)
|
||||
return native_fmt; /* FILE/NULL:高保真记录 */
|
||||
return AUDIO_FMT_S16;
|
||||
}
|
||||
|
||||
/* 音量控制归属(QUERY volctl=):dac=反转控制;avrcp=蓝牙 AVRCP 侧控。 */
|
||||
@@ -570,6 +579,17 @@ static int amixer_set(int numid, int val, int card)
|
||||
g_amixer, (card ? " -c 0" : ""), numid, val);
|
||||
char *o = popen_read_tmo(1500, cmd);
|
||||
if (!o) return -1;
|
||||
/* 测试钩子(仅 env MEOW_AUDIO_AMIXER_TRACE=<path>):把每次 amixer cset 追加到文件,
|
||||
* 供宿主断言「音量双组写」PCM+DSD_VOLUME 是否成对下发(真机无 amixer 时不写/不报错)。 */
|
||||
if (g_amixer_trace_path[0]) {
|
||||
int tfd = open(g_amixer_trace_path, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
if (tfd >= 0) {
|
||||
char tb[64];
|
||||
int tn = snprintf(tb, sizeof tb, "cset numid=%d %d\n", numid, val);
|
||||
if (tn > 0) { if (write(tfd, tb, (size_t)tn) < 0) { } }
|
||||
close(tfd);
|
||||
}
|
||||
}
|
||||
/* popen_read_tmo 用的是静态缓冲,这里不 free(与 meow_route.c 同权衡) */
|
||||
return 0;
|
||||
}
|
||||
@@ -592,6 +612,15 @@ static int amixer_get(int numid, int card)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ---- DSD(DoP)相关常量 + 状态 ----
|
||||
* ⚠️ 真机核实项(AUDIOD_A3_DSD_PLAN §五,未做):DSD_VOLUME_A/B 寄存器地址/control、DoP 标记字节
|
||||
* (0x05/0xFA vs 0xA5/0x5A)。下述 numid 为设计定案占位:DOP_EN=numid 10(plan §三「cset numid=10 1」),
|
||||
* DSD_VOLUME 单 control 占位 numid 11(真实为 A/B 两路或 write_reg_val,待 DS1039 核实)。
|
||||
* 未核实前 DSD_VOLUME 写为 best-effort(amixer 出错仅日志,绝不掩盖;正确地址真机定后再上)。 */
|
||||
#define DOP_EN_NUMID 10
|
||||
#define DSD_VOL_NUMID 11
|
||||
static int g_dsd_active = 0; /* 当前授权流为 DoP DSD(g_mu 保护;决定 DOP_EN=1 与 DSD 音量作用域) */
|
||||
|
||||
static void dac_vol_set(int pct)
|
||||
{
|
||||
int t = 100 - pct, dac;
|
||||
@@ -599,9 +628,12 @@ static void dac_vol_set(int pct)
|
||||
else dac = t * t * 192 / 10000; /* 感知平方锥度:dB 衰减=(1-pct)^2×96dB */
|
||||
if (dac > 255) dac = 255;
|
||||
if (dac < 0) dac = 0; /* CS43198=衰减量(0最响255静音)+平方锥度(听感线性) */
|
||||
/* 音量收编 v2:**泛化双组写**——同刻写 PCM(numid=1/2) 与 DSD_VOLUME,保证 DSD↔PCM 切换
|
||||
* 音量连续 + 防爆音闭环(决策①,勿只写 PCM)。锥度沿用平方(决策②)。 */
|
||||
amixer_set(1, dac, 1);
|
||||
amixer_set(2, dac, 1);
|
||||
logmsg("DAC vol -> %d%% (L/R=%d)", pct, dac);
|
||||
amixer_set(DSD_VOL_NUMID, dac, 1);
|
||||
logmsg("DAC/DSD vol -> %d%% (PCM L/R=%d, DSD=%d)", pct, dac, dac);
|
||||
}
|
||||
|
||||
static void dac_vol_mute(void)
|
||||
@@ -610,6 +642,30 @@ static void dac_vol_mute(void)
|
||||
* 旧版写 0 在闪退/重配瞬间造成满幅炸音(真机事故 2026-09-01)。真静音写 255。 */
|
||||
amixer_set(1, 255, 1);
|
||||
amixer_set(2, 255, 1);
|
||||
amixer_set(DSD_VOL_NUMID, 255, 1); /* DSD 同步真静音(防爆音闭环:DSD 也 255) */
|
||||
}
|
||||
|
||||
/* ---- DSD(DoP)管道:audiod 只做 DOP_EN 时序 + DSD 音量 + 透传;不引 dsd2pcm(许可未核实,避风险)。
|
||||
* 流级判定 vs 设备级判定分拆:
|
||||
* - g_dsd_active:当前授权流为 DoP DSD(dispatch 授 DOP 时置位,session_clean 复位)——供 QUERY/monitor。
|
||||
* - g_dev_dsd:当前**打开的设备**为 DSD(reconfigure/dev_open 后置位)——决定 DOP_EN 硬件时序。
|
||||
* 进入 DSD(plan §三时序)由播放线程 reconfigure 执行:mute(PCM+DSD) → DOP_EN=1 → hw_params(容器率)
|
||||
* → dac_vol_set(DSD_VOLUME) → unmute。本函数只做「流级可入性」判定:BT 拒(A2DP 不能承载 DoP),
|
||||
* 非 ALSA(FILE/NULL)=纯透传管道允许。失败回退=报错关流(案 A:DSD 打不开/不支持→报错并关闭,绝不静默退化)。 */
|
||||
static int dsd_enter(void)
|
||||
{
|
||||
if (g_target == AUDIO_OUT_BT) {
|
||||
logmsg("dsd: enter rejected (BT/A2DP cannot carry DoP) -> close stream");
|
||||
return -1;
|
||||
}
|
||||
if (g_sink != SINK_ALSA) {
|
||||
logmsg("dsd: enter ok (sink=%d passthrough, no DOP_EN)", g_sink);
|
||||
g_dsd_active = 1;
|
||||
return 0;
|
||||
}
|
||||
g_dsd_active = 1; /* 流级置位;DOP_EN 硬件时序由播放线程 reconfigure 执行 */
|
||||
logmsg("dsd: stream enters DoP DSD (DOP_EN sequenced on device reconfigure)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char *port_name(int port)
|
||||
@@ -1165,31 +1221,43 @@ static int alsa_fmt_to_snd(int fmt)
|
||||
case AUDIO_FMT_S16: return SNDRV_PCM_FORMAT_S16_LE;
|
||||
case AUDIO_FMT_S24: return SNDRV_PCM_FORMAT_S24_LE;
|
||||
case AUDIO_FMT_S32: return SNDRV_PCM_FORMAT_S32_LE;
|
||||
case AUDIO_FMT_DOP: return SNDRV_PCM_FORMAT_S32_LE; /* DoP 走 32-bit 容器(16bit DSD + 标记 + 0) */
|
||||
case AUDIO_FMT_FLOAT: return SNDRV_PCM_FORMAT_FLOAT_LE;
|
||||
default: return SNDRV_PCM_FORMAT_S16_LE;
|
||||
}
|
||||
}
|
||||
|
||||
/* 探测硬件最优格式(设备空闲时调用;缓存 g_hw_fmt)。返回 0=确定,-1=无法探测。 */
|
||||
/* 探测硬件最优格式(设备空闲时调用;缓存 g_hw_fmt)。返回 0=确定,-1=无法探测。
|
||||
* A3(24/32bit 高清流):**偏好 S32**——CS43198 为 32bit DAC,S32 可顺带把 16bit 内容做无损升位、
|
||||
* 24/32bit 直通不降位(plan §一「S16 直通/S32 升位」)。按高清率优先探测 S32(192k→176.4k→44.1k),
|
||||
* 全失败再退 S16。旧实现只在 44.1k 试 S16 并**立即返回**(永远拿不到 S32,高清降位)。
|
||||
* ⚠️ 探测只 open+hw_params(非 start),与 FPGA 武装无关;真机此路径仍需 AUDIO 实测复核。 */
|
||||
static int hw_probe(void)
|
||||
{
|
||||
if (g_sink != SINK_ALSA) { g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; return 0; }
|
||||
if (g_hw_probed) return 0;
|
||||
#ifdef AUDIOD_HAVE_ALSA
|
||||
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
if (alsa_hw_params(fd, 44100, 2, SNDRV_PCM_FORMAT_S16_LE) == 0) { g_hw_fmt = AUDIO_FMT_S16; close(fd); g_hw_probed = 1; return 0; }
|
||||
close(fd);
|
||||
static const unsigned rates[3] = { 192000u, 176400u, 44100u };
|
||||
for (int r = 0; r < 3 && !g_hw_probed; r++) {
|
||||
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
if (alsa_hw_params(fd, rates[r], 2, SNDRV_PCM_FORMAT_S32_LE) == 0)
|
||||
{ g_hw_fmt = AUDIO_FMT_S32; g_hw_probed = 1; }
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
if (alsa_hw_params(fd, 44100, 2, SNDRV_PCM_FORMAT_S32_LE) == 0) { g_hw_fmt = AUDIO_FMT_S32; close(fd); g_hw_probed = 1; return 0; }
|
||||
close(fd);
|
||||
for (int r = 0; r < 3 && !g_hw_probed; r++) {
|
||||
int fd = open(DEV_PCM, O_RDWR | O_NONBLOCK);
|
||||
if (fd >= 0) {
|
||||
if (alsa_hw_params(fd, rates[r], 2, SNDRV_PCM_FORMAT_S16_LE) == 0)
|
||||
{ g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; }
|
||||
close(fd);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
g_hw_fmt = AUDIO_FMT_S16;
|
||||
g_hw_probed = 1;
|
||||
return -1;
|
||||
if (!g_hw_probed) { g_hw_fmt = AUDIO_FMT_S16; g_hw_probed = 1; }
|
||||
logmsg("hw probe: optimal fmt=%s", fmt_name(g_hw_fmt));
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ---- FILE/NULL sink ---- */
|
||||
@@ -1360,26 +1428,50 @@ static void dev_close(void)
|
||||
g_dev_open = 0;
|
||||
}
|
||||
|
||||
/* 播放线程:处理一次重配(静音 → 关旧 → 开新 → 复原)。调用方已持 g_mu。 */
|
||||
/* 播放线程:处理一次重配(静音 → 关旧 → 开新 → 复原)。调用方已持 g_mu。
|
||||
* DSD(audiod 只透传):DoP 时序「mute(PCM+DSD) → DOP_EN=1 → hw_params(容器率) → DSD_VOLUME → unmute」。
|
||||
* DOP_EN 必须早于 dev_open 的 hw_params(plan §三),离开 DSD 时 DOP_EN=0 须早于关设备。 */
|
||||
static void reconfigure_locked(void)
|
||||
{
|
||||
uint32_t rate = g_st.rate, ch = g_st.ch, fmt = g_st.fmt;
|
||||
const char *tgt = (g_target == AUDIO_OUT_BT) ? "bt" : "dac";
|
||||
logmsg("reconfigure device: %u/%u/%s target=%s", (unsigned)rate, (unsigned)ch, fmt_name((int)fmt), tgt);
|
||||
int saved_vol = g_vol_pct;
|
||||
/* 防爆音仅 DAC(重配前静音 + 复原);BT 独立目标,跳过 DAC 音量(音量由 AVRCP 侧控)。 */
|
||||
if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_mute();
|
||||
int new_dsd = (fmt == AUDIO_FMT_DOP);
|
||||
int do_dsd_seq = (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA); /* DOP_EN 仅 DAC 物理路径 */
|
||||
if (do_dsd_seq) {
|
||||
if (new_dsd) {
|
||||
dac_vol_mute(); /* ① mute(PCM+DSD 双组) */
|
||||
/* ② DOP_EN 写后校验:**DSD 打不开/不支持→报错关流(案 A,不静默退化)**——DOP_EN 未确认
|
||||
* 为 1 则丢弃本流(active=0→播放线程不再喂 DAC),客户端经 STATE active=0 感知并重开/换目标。 */
|
||||
if (amixer_set(DOP_EN_NUMID, 1, 1) < 0 || amixer_get(DOP_EN_NUMID, 1) != 1) {
|
||||
logmsg("dsd: DOP_EN (numid=%d) not confirmed -> close stream (no silent degrade)", DOP_EN_NUMID);
|
||||
dac_vol_mute();
|
||||
amixer_set(DOP_EN_NUMID, 0, 1); /* 确保离开 DOP(兼容旧 DSD 设备残留) */
|
||||
session_end_locked();
|
||||
g_dev_dsd = 0;
|
||||
g_reconfig_pending = 0;
|
||||
return;
|
||||
}
|
||||
logmsg("dsd: DOP_EN -> 1 (DoP container)");
|
||||
} else if (g_dev_dsd) {
|
||||
logmsg("dsd: DOP_EN -> 0 (leave DoP)"); /* 离开 DSD:DOP_EN=0 后关设备 */
|
||||
amixer_set(DOP_EN_NUMID, 0, 1);
|
||||
}
|
||||
}
|
||||
dev_close();
|
||||
g_dev_fd = dev_open();
|
||||
if (g_dev_fd >= 0 || (g_sink == SINK_NULL && g_dev_fd == -2)) {
|
||||
g_dev_open = 1;
|
||||
g_dev_rate = rate; g_dev_ch = ch; g_dev_fmt = fmt;
|
||||
if (g_dev_fd == -2) g_dev_fd = -1; /* NULL sink:无真实 fd */
|
||||
if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原 */
|
||||
logmsg("device open ok: %u/%u/%s sink=%d target=%s", (unsigned)rate, (unsigned)ch,
|
||||
fmt_name((int)fmt), g_sink, tgt);
|
||||
g_dev_dsd = (do_dsd_seq && new_dsd);
|
||||
if (g_target == AUDIO_OUT_DAC && g_sink == SINK_ALSA) dac_vol_set(saved_vol); /* 复原(含 DSD_VOLUME) */
|
||||
logmsg("device open ok: %u/%u/%s sink=%d target=%s%s", (unsigned)rate, (unsigned)ch,
|
||||
fmt_name((int)fmt), g_sink, tgt, g_dev_dsd ? " dsd" : "");
|
||||
} else {
|
||||
logmsg("device open FAILED !!");
|
||||
g_dev_dsd = 0;
|
||||
}
|
||||
g_reconfig_pending = 0;
|
||||
}
|
||||
@@ -1408,6 +1500,11 @@ static void *playback_thread(void *arg)
|
||||
if ((draining && av < (size_t)fs) || paused) {
|
||||
/* 排空完毕(或暂停):若设备仍开且已排空 → 关;否则等待 */
|
||||
if (draining && g_dev_open) dev_close();
|
||||
if (draining && g_dev_dsd && g_sink == SINK_ALSA && g_target == AUDIO_OUT_DAC) {
|
||||
logmsg("dsd: DOP_EN -> 0 (stream drained)");
|
||||
amixer_set(DOP_EN_NUMID, 0, 1);
|
||||
g_dev_dsd = 0;
|
||||
}
|
||||
if (draining) session_clean_locked(); /* 流已死且排空完成:会话统计彻底归零 */
|
||||
cond_wait_tmo(COND_WAIT_MS);
|
||||
continue;
|
||||
@@ -1490,6 +1587,8 @@ static void session_clean_locked(void)
|
||||
g_st.rate = 0;
|
||||
g_st.ch = 0;
|
||||
g_st.fmt = 0;
|
||||
g_dsd_active = 0; /* 流级 DSD 位归零(DOP_EN 设备位由 drain/reconfigure 时序负责) */
|
||||
g_dev_dsd = 0;
|
||||
g_written_frames = 0;
|
||||
g_played_frames = 0;
|
||||
g_underrun = 0;
|
||||
@@ -1511,10 +1610,20 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
||||
if (len < sizeof(m_audio_open_t)) { m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_INVAL; send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a); return; }
|
||||
m_audio_open_t o;
|
||||
memcpy(&o, pl, sizeof o);
|
||||
if (o.ch > 2 || o.fmt < AUDIO_FMT_S16 || o.fmt > AUDIO_FMT_FLOAT || o.fmt == AUDIO_FMT_FLOAT || o.rate == 0) {
|
||||
/* 合法 fmt:S16/S24/S32/DOP;FLOAT 拒(v1 未支持)。DOP 须带 AUDIO_FLAG_DSD(DoP 独立格式值)。 */
|
||||
int fmt_inval = (o.fmt != AUDIO_FMT_S16 && o.fmt != AUDIO_FMT_S24 &&
|
||||
o.fmt != AUDIO_FMT_S32 && o.fmt != AUDIO_FMT_DOP);
|
||||
if (o.ch > 2 || o.ch < 1 || fmt_inval || (o.fmt == AUDIO_FMT_DOP && !(o.flags & AUDIO_FLAG_DSD)) || o.rate == 0) {
|
||||
m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_INVAL;
|
||||
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
|
||||
logmsg("open rejected: ch=%u fmt=%d rate=%u", (unsigned)o.ch, o.fmt, (unsigned)o.rate);
|
||||
logmsg("open rejected: ch=%u fmt=%d rate=%u flags=0x%x", (unsigned)o.ch, o.fmt, (unsigned)o.rate, (unsigned)o.flags);
|
||||
return;
|
||||
}
|
||||
/* DSD 可入性判定(BT 拒 / FILE-NULL 透传允许);失败=报错关流(案 A,绝不静默退化) */
|
||||
if (o.fmt == AUDIO_FMT_DOP && dsd_enter() != 0) {
|
||||
m_audio_open_ack_t a; memset(&a, 0, sizeof a); a.err = MEOW_ERR_NODEV;
|
||||
send_frame(c->fd, MSG_AUDIO_OPEN_ACK, req_id, &a, sizeof a);
|
||||
logmsg("open rejected: DoP DSD not supported (target=%s sink=%d)", target_name(), g_sink);
|
||||
return;
|
||||
}
|
||||
/* 输出目标=BT:不依赖 DAC hw 探测(BT 走 pcm.bluealsa,与 hw:0,0/FGA numid=9 解耦)——
|
||||
@@ -1528,7 +1637,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
||||
return;
|
||||
}
|
||||
}
|
||||
int grant_fmt = target_grant_fmt(); /* DAC=hw 最优;BT=S16(plug 层转 codec 协商率) */
|
||||
int grant_fmt = target_grant_fmt(o.fmt); /* DAC=hw 最优;BT=S16(plug 层转 codec 协商率);FILE/NULL=保真透传 */
|
||||
uint32_t sid = g_next_sid++;
|
||||
/* 抢占:已有当前流 → 旧客户端收 PREEMPTED */
|
||||
if (g_st.active) {
|
||||
@@ -1561,9 +1670,9 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
||||
g_drop_frames = 0; /* 每流计数(txdrop 为全局客户端级,不随流清) */
|
||||
g_stall = 0;
|
||||
g_dev_bytes = 0; /* 测试钩子计数随新流归零 */
|
||||
/* 电平表复位 + 窗口目标(~100ms) */
|
||||
g_m_win = g_st.rate / 10;
|
||||
if (!g_m_win) g_m_win = 1;
|
||||
/* 电平表复位 + 窗口目标(~100ms;DoP DSD 无 PCM 意义,窗口=0 即跳过——meter_feed early-return) */
|
||||
g_m_win = (grant_fmt == AUDIO_FMT_DOP) ? 0 : (g_st.rate / 10);
|
||||
if (!g_m_win && grant_fmt != AUDIO_FMT_DOP) g_m_win = 1;
|
||||
g_m_sumsq = 0; g_m_peak = 0; g_m_frames = 0;
|
||||
g_lvl_rms_db10 = g_lvl_peak_db10 = METER_FLOOR_DB10;
|
||||
g_lvl_seq++;
|
||||
@@ -1626,7 +1735,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
||||
int rb = g_route_cur;
|
||||
int vol = g_vol_pct;
|
||||
snprintf(txt, sizeof txt,
|
||||
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s "
|
||||
"active=%d app=%s sid=%u rate=%u ch=%u fmt=%s dsd=%d "
|
||||
"written=%llu played=%llu underrun=%u dropped=%llu stall=%u txdrop=%u paused=%d "
|
||||
"route_mode=%s port=%d vol=%d volctl=%s rms=%d peak=%d lvlseq=%u "
|
||||
"target=%s codec=%s\n",
|
||||
@@ -1634,6 +1743,7 @@ static void dispatch(struct client *c, uint16_t type, uint32_t req_id, const uin
|
||||
g_st.active ? g_st.app_id : "-",
|
||||
(unsigned)g_st.sid, (unsigned)g_st.rate, (unsigned)g_st.ch,
|
||||
g_st.active ? fmt_name((int)g_st.fmt) : "-",
|
||||
g_dsd_active ? 1 : 0,
|
||||
(unsigned long long)g_written_frames,
|
||||
(unsigned long long)g_played_frames,
|
||||
(unsigned)g_underrun,
|
||||
@@ -1934,10 +2044,12 @@ static void mon_tick(void)
|
||||
int port = (rmanual != -1) ? rmanual : rcur;
|
||||
char line[512];
|
||||
int n = snprintf(line, sizeof line,
|
||||
"%s app=%s sid=%u %uHz/%uch/%s rms=%sdBFS peak=%sdBFS "
|
||||
"%s app=%s sid=%u %uHz/%uch/%s dsd=%d "
|
||||
"rms=%sdBFS peak=%sdBFS "
|
||||
"written=%llu played=%llu underrun=%u stall=%u dropped=%llu txdrop=%u "
|
||||
"route=%d(%s) fpga=%s vol=%d%% hwrate=%lluf/s%s\n",
|
||||
tbuf, app, (unsigned)sid, (unsigned)rate, ch, fmt_name((int)fmt),
|
||||
(fmt == AUDIO_FMT_DOP ? 1 : 0),
|
||||
rmsb, pkb,
|
||||
(unsigned long long)wr, (unsigned long long)pl,
|
||||
ur, st, (unsigned long long)dr, tx,
|
||||
@@ -1970,7 +2082,7 @@ int main(int argc, char **argv)
|
||||
" MEOW_AUDIO_SETTINGS (settings.conf 路径, 缺省 %s; 宿主测覆盖)\n"
|
||||
" MEOW_AUDIO_TARGET (0=dac 1=bt, 覆盖 settings audio.output_bt)\n"
|
||||
" MEOW_AUDIO_RING_WAIT_MS (ring 满写等待上限, 缺省 5000)\n"
|
||||
" MEOW_AUDIO_TEST_WRITE_DELAY_MS / MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES (故障注入, 仅测试)\n",
|
||||
" MEOW_AUDIO_TEST_WRITE_DELAY_MS / MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES / MEOW_AUDIO_AMIXER_TRACE (故障注入/amixer 轨迹, 仅测试)\n",
|
||||
SETTINGS_CONF);
|
||||
return 0;
|
||||
}
|
||||
@@ -1982,6 +2094,7 @@ int main(int argc, char **argv)
|
||||
if ((e = getenv("MEOW_AUDIO_RING_WAIT_MS")) && atoi(e) > 0) g_ring_wait_ms = atoi(e);
|
||||
if ((e = getenv("MEOW_AUDIO_TEST_WRITE_DELAY_MS")) && atoi(e) > 0) g_test_write_delay_ms = atoi(e);
|
||||
if ((e = getenv("MEOW_AUDIO_TEST_WEDGE_AFTER_BYTES")) && atoll(e) > 0) g_test_wedge_after = atoll(e);
|
||||
if ((e = getenv("MEOW_AUDIO_AMIXER_TRACE")) && e[0]) snprintf(g_amixer_trace_path, sizeof g_amixer_trace_path, "%s", e);
|
||||
if ((e = getenv("MEOW_AUDIO_SETTINGS")) && e[0]) snprintf(g_settings_path, sizeof g_settings_path, "%s", e);
|
||||
const char *sink = getenv("MEOW_AUDIO_SINK");
|
||||
if (sink && sink[0]) {
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#define TSOCK "/tmp/meow_audio_test.sock"
|
||||
#define TWAV "/tmp/meow_audio_test.wav"
|
||||
@@ -169,12 +170,29 @@ static int start_audiod_settings(const char *sink, const char *settings_path)
|
||||
return pid;
|
||||
}
|
||||
|
||||
/* 直接客户端 open */
|
||||
static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack)
|
||||
/* 带 amixer 追踪的启动(A3/DSD:断言「音量双组写」PCM+DSD_VOLUME 成对下发)。settings_path 可空。 */
|
||||
static int start_audiod_trace(const char *sink, const char *settings_path, const char *trace_path)
|
||||
{
|
||||
pid_t pid = fork();
|
||||
if (pid == 0) {
|
||||
setenv("MEOW_AUDIO_SINK", sink, 1);
|
||||
setenv("MEOW_AUDIO_OUT", TWAV, 1);
|
||||
setenv("MEOW_AUDIO_SOCK", TSOCK, 1);
|
||||
setenv("MEOW_AUDIO_FORCE", "1", 1);
|
||||
if (settings_path) setenv("MEOW_AUDIO_SETTINGS", settings_path, 1);
|
||||
if (trace_path) setenv("MEOW_AUDIO_AMIXER_TRACE", trace_path, 1);
|
||||
execl("./meow_audiod_host", "meow_audiod_host", (char *)0);
|
||||
_exit(127);
|
||||
}
|
||||
return pid;
|
||||
}
|
||||
|
||||
/* 直接客户端 open(带 flags:AUDIO_FLAG_DSD 用于 DoP) */
|
||||
static int do_open_f(struct cli *c, int rate, int ch, int fmt, uint32_t flags, m_audio_open_ack_t *ack)
|
||||
{
|
||||
m_audio_open_t o; memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "test");
|
||||
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; o.flags = 0;
|
||||
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; o.flags = flags;
|
||||
if (sf(c->fd, MSG_AUDIO_OPEN, 1, &o, sizeof o)) return -1;
|
||||
uint8_t rbuf[256]; uint16_t rt;
|
||||
if (rf(c->fd, &rt, rbuf, sizeof rbuf) < 0) return -1;
|
||||
@@ -182,6 +200,11 @@ static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t
|
||||
memcpy(ack, rbuf, sizeof *ack);
|
||||
return 0;
|
||||
}
|
||||
/* 直接客户端 open */
|
||||
static int do_open(struct cli *c, int rate, int ch, int fmt, m_audio_open_ack_t *ack)
|
||||
{
|
||||
return do_open_f(c, rate, ch, fmt, 0, ack);
|
||||
}
|
||||
|
||||
static int do_write(struct cli *c, const void *pcm, uint32_t len)
|
||||
{
|
||||
@@ -266,6 +289,45 @@ static int verify_wav(const char *path, int rate_expect, int ch_expect, int bits
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* 字节保真校验(A3 高清 / DoP 透传):WAV 头 rate/ch/bits 精确匹配 + data 段与 payload 逐字节一致。
|
||||
* 返回 0=合法且逐字节等同;-1=文件/头非法;-2=data_len 或内容不匹配。 */
|
||||
static int wav_byte_exact(const char *path, int rate_expect, int ch_expect, int bits_expect,
|
||||
const uint8_t *payload, size_t plen)
|
||||
{
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) return -1;
|
||||
unsigned char h[44];
|
||||
if (fread(h, 1, 44, f) != 44) { fclose(f); return -1; }
|
||||
if (memcmp(h, "RIFF", 4) || memcmp(h + 8, "WAVE", 4) || memcmp(h + 12, "fmt ", 4)) { fclose(f); return -1; }
|
||||
uint16_t ch, bits; uint32_t rate, data_len, byterate;
|
||||
memcpy(&ch, h + 22, 2); memcpy(&rate, h + 24, 4); memcpy(&byterate, h + 28, 4); memcpy(&bits, h + 34, 2);
|
||||
memcpy(&data_len, h + 40, 4);
|
||||
if (ch != (uint16_t)ch_expect || rate != (uint32_t)rate_expect || bits != (uint16_t)bits_expect) { fclose(f); return -1; }
|
||||
if (byterate != rate * ch * bits / 8) { fclose(f); return -1; }
|
||||
if (data_len != (uint32_t)plen) { fclose(f); return -2; }
|
||||
uint8_t *body = malloc(plen ? plen : 1);
|
||||
if (!body) { fclose(f); return -1; }
|
||||
size_t rd = fread(body, 1, plen, f);
|
||||
fclose(f);
|
||||
int ok = (rd == plen) && (memcmp(body, payload, plen) == 0);
|
||||
free(body);
|
||||
return ok ? 0 : -2;
|
||||
}
|
||||
|
||||
/* 等 WAV 头出现(设备已 dev_open 写入 44B 头):**STOP 前调用**——修复 OPEN→write→STOP 过快时
|
||||
* 播放线程尚未来得及 reconfigure(设备未打开,STOP 的 session_end 清 reconfig → 空 WAV)的竞态。
|
||||
* 返回 0=头已出现;-1=超时。 */
|
||||
static int wait_wav_open(const char *path, int tmo_ms)
|
||||
{
|
||||
for (int i = 0; i < tmo_ms / 50; i++) {
|
||||
struct stat st;
|
||||
if (stat(path, &st) < 0) { usleep(50000); continue; }
|
||||
if (st.st_size >= 44) return 0;
|
||||
usleep(50000);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
@@ -778,6 +840,246 @@ int main(void)
|
||||
close(b11.fd);
|
||||
stop_audiod();
|
||||
|
||||
/* ---- T12:24/32bit 高清流 byte-exact(A3 高清实测;FILE sink 按请求保真,S32 升位不降位)。
|
||||
* ① S32@192000:open 应 grant S32(FILE/NULL sink 按 native 高保真透传);写已知 32-bit 交织帧,
|
||||
* 验证 WAV 为 32-bit 且 data 段与 payload 逐字节相等(无降位/无重排)。
|
||||
* ② S24@88200(32-in-32):grant S24,WAV 32-bit 逐字节等同。鉴真机 DAC 高清率由 hw_probe 决定。 */
|
||||
printf("[T12 24/32bit hi-res byte-exact]\n");
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
g_audiod_pid = start_audiod(); /* file sink */
|
||||
usleep(400000);
|
||||
{
|
||||
struct cli h12;
|
||||
check(cli_connect(&h12) == 0, "connect H12");
|
||||
m_audio_open_ack_t ack12;
|
||||
check(do_open_f(&h12, 192000, 2, AUDIO_FMT_S32, 0, &ack12) == 0 && ack12.err == MEOW_OK,
|
||||
"open S32@192000");
|
||||
check(ack12.grant_fmt == AUDIO_FMT_S32, "grant S32 for S32 request (FILE byte-exact)");
|
||||
check(ack12.grant_rate == 192000, "grant_rate=192000 (no SRC)");
|
||||
uint8_t pl[4096];
|
||||
size_t fs12 = 2 * 4; /* stereo S32 */
|
||||
size_t nframes = sizeof pl / fs12;
|
||||
uint8_t *op = pl;
|
||||
for (size_t i = 0; i < nframes; i++) {
|
||||
uint32_t l = (uint32_t)(((i * 7u) & 0xFFFFu) << 16);
|
||||
uint32_t r = (uint32_t)((0x10000u - ((i * 7u) & 0xFFFFu)) << 16);
|
||||
memcpy(op, &l, 4); op += 4; memcpy(op, &r, 4); op += 4;
|
||||
}
|
||||
do_write(&h12, pl, (uint32_t)sizeof pl);
|
||||
char q12[1024];
|
||||
do_query(&h12, q12, sizeof q12); /* 活动态快照 + 给播放线程时间重配设备 */
|
||||
check(strstr(q12, "fmt=S32") != NULL, "query fmt=S32 (hi-res format exposed)");
|
||||
wait_wav_open(TWAV, 3000); /* 等设备开(WAV 头已在),再 STOP */
|
||||
do_ctrl(&h12, MSG_AUDIO_STOP, ack12.stream_id);
|
||||
for (int i = 0; i < 300; i++) { /* ≤30s 等排空 + WAV 头回填 */
|
||||
usleep(100000);
|
||||
if (wav_byte_exact(TWAV, 192000, 2, 32, pl, sizeof pl) == 0) break;
|
||||
}
|
||||
check(wav_byte_exact(TWAV, 192000, 2, 32, pl, sizeof pl) == 0,
|
||||
"S32@192000 WAV byte-exact (32-bit, no downshift)");
|
||||
/* ② S24@88200(32-in-32) */
|
||||
unlink(TWAV);
|
||||
struct cli h12b;
|
||||
check(cli_connect(&h12b) == 0, "connect H12b");
|
||||
m_audio_open_ack_t ack12b;
|
||||
check(do_open_f(&h12b, 88200, 2, AUDIO_FMT_S24, 0, &ack12b) == 0 && ack12b.err == MEOW_OK,
|
||||
"open S24@88200");
|
||||
check(ack12b.grant_fmt == AUDIO_FMT_S24, "grant S24 for S24 request (32-in-32)");
|
||||
uint8_t pl2[4096];
|
||||
size_t fs24 = 2 * 4;
|
||||
size_t n2 = sizeof pl2 / fs24;
|
||||
uint8_t *op2 = pl2;
|
||||
for (size_t i = 0; i < n2; i++) {
|
||||
uint32_t l = (uint32_t)((i * 11u) << 8); /* 24-bit 区在 [31:8] */
|
||||
uint32_t r = (uint32_t)(0x1000000u - ((i * 11u) << 8));
|
||||
memcpy(op2, &l, 4); op2 += 4; memcpy(op2, &r, 4); op2 += 4;
|
||||
}
|
||||
do_write(&h12b, pl2, (uint32_t)sizeof pl2);
|
||||
char q12b[1024];
|
||||
do_query(&h12b, q12b, sizeof q12b); /* 活动态快照:fmt 未清 */
|
||||
check(strstr(q12b, "fmt=S24") != NULL, "query fmt=S24 (hi-res format exposed)");
|
||||
wait_wav_open(TWAV, 3000); /* 等设备开(WAV 头已在),再 STOP */
|
||||
do_ctrl(&h12b, MSG_AUDIO_STOP, ack12b.stream_id);
|
||||
for (int i = 0; i < 300; i++) {
|
||||
usleep(100000);
|
||||
if (wav_byte_exact(TWAV, 88200, 2, 32, pl2, sizeof pl2) == 0) break;
|
||||
}
|
||||
check(wav_byte_exact(TWAV, 88200, 2, 32, pl2, sizeof pl2) == 0,
|
||||
"S24@88200 WAV byte-exact (32-in-32)");
|
||||
close(h12.fd); close(h12b.fd);
|
||||
}
|
||||
stop_audiod();
|
||||
|
||||
/* ---- T13:DoP DSD 透传(A3+DSD:fmt=DOP+flags=DSD,audiod 只透传;不做 dsd2pcm)。
|
||||
* 写已知 DoP 帧(32-bit:bits[31:16]=16bit DSD,bits[15:8]=标记 0xFA(L)/0x05(R)),
|
||||
* 验证 grant=DOP、WAV 32-bit 逐字节等同(透传保真)、query fmt=DoP dsd=1。 */
|
||||
printf("[T13 DoP DSD passthrough]\n");
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
g_audiod_pid = start_audiod();
|
||||
usleep(400000);
|
||||
{
|
||||
struct cli d13;
|
||||
check(cli_connect(&d13) == 0, "connect D13");
|
||||
m_audio_open_ack_t ack13;
|
||||
check(do_open_f(&d13, 176400, 2, AUDIO_FMT_DOP, AUDIO_FLAG_DSD, &ack13) == 0 && ack13.err == MEOW_OK,
|
||||
"open DoP@176400 (DSD64)");
|
||||
check(ack13.grant_fmt == AUDIO_FMT_DOP, "grant=DoP (DOP passthrough)");
|
||||
check(ack13.grant_rate == 176400, "grant_rate=176400 (container rate, no SRC)");
|
||||
/* DoP 帧:16bit DSD 数据(左/右不同)+ 标记字节 */
|
||||
uint8_t dop[4096];
|
||||
size_t fsd = 2 * 4;
|
||||
size_t nd = sizeof dop / fsd;
|
||||
uint8_t *od = dop;
|
||||
for (size_t i = 0; i < nd; i++) {
|
||||
uint32_t wl = ((uint32_t)(i & 0xFFFFu) << 16) | (0xFAu << 8); /* 标记 0xFA=左 */
|
||||
uint32_t wr = ((uint32_t)(~i & 0xFFFFu) << 16) | (0x05u << 8); /* 标记 0x05=右 */
|
||||
memcpy(od, &wl, 4); od += 4; memcpy(od, &wr, 4); od += 4;
|
||||
}
|
||||
do_write(&d13, dop, (uint32_t)sizeof dop);
|
||||
char q13[1024];
|
||||
do_query(&d13, q13, sizeof q13); /* 活动态快照:fmt=DoP/dsd=1 */
|
||||
check(strstr(q13, "fmt=DoP") != NULL, "query fmt=DoP");
|
||||
check(strstr(q13, "dsd=1") != NULL, "query dsd=1 (DoP stream active)");
|
||||
wait_wav_open(TWAV, 3000); /* 等设备开(WAV 头已在),再 STOP */
|
||||
do_ctrl(&d13, MSG_AUDIO_STOP, ack13.stream_id);
|
||||
for (int i = 0; i < 300; i++) {
|
||||
usleep(100000);
|
||||
if (wav_byte_exact(TWAV, 176400, 2, 32, dop, sizeof dop) == 0) break;
|
||||
}
|
||||
check(wav_byte_exact(TWAV, 176400, 2, 32, dop, sizeof dop) == 0,
|
||||
"DoP WAV byte-exact (32-bit container, marker preserved)");
|
||||
/* 无 DSD 标志的 DOP 请求 → 拒(INVAL):DoP 必须带 AUDIO_FLAG_DSD */
|
||||
struct cli d13b;
|
||||
check(cli_connect(&d13b) == 0, "connect D13b");
|
||||
m_audio_open_ack_t ack13b;
|
||||
check(do_open_f(&d13b, 176400, 2, AUDIO_FMT_DOP, 0, &ack13b) == 0 && ack13b.err == MEOW_ERR_INVAL,
|
||||
"DoP without AUDIO_FLAG_DSD rejected (INVAL)");
|
||||
close(d13.fd); close(d13b.fd);
|
||||
}
|
||||
stop_audiod();
|
||||
|
||||
/* ---- T14:DSD 失败回退 = 报错关流(案 A;不引 dsd2pcm、不静默退化)。
|
||||
* output_bt=1(BT 目标)下 DoP open → MEOW_ERR_NODEV(BT/A2DP 不能承载 DoP);
|
||||
* 此后普通 PCM(S16)open 仍正常(BT 目标不变),证明失败只关该 DSD 流、不殃及目标。 */
|
||||
printf("[T14 DSD fallback (BT) = error/close]\n");
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
const char *tconf14 = "/tmp/meow_test_settings14.conf";
|
||||
FILE *tf14 = fopen(tconf14, "w");
|
||||
if (tf14) { fprintf(tf14, "# meow-settings 1\naudio.volume = 60\naudio.output_bt = 1\n"); fclose(tf14); }
|
||||
g_audiod_pid = start_audiod_settings("file", tconf14);
|
||||
usleep(400000);
|
||||
{
|
||||
struct cli d14;
|
||||
check(cli_connect(&d14) == 0, "connect D14");
|
||||
m_audio_open_ack_t ack14;
|
||||
check(do_open_f(&d14, 176400, 2, AUDIO_FMT_DOP, AUDIO_FLAG_DSD, &ack14) == 0 &&
|
||||
ack14.err == MEOW_ERR_NODEV, "DoP on BT target rejected (NODEV)");
|
||||
/* 失败后 state 归干净(无流基线);同目标普通 PCM 正常开 */
|
||||
struct cli d14b;
|
||||
check(cli_connect(&d14b) == 0, "connect D14b");
|
||||
m_audio_open_ack_t ack14b;
|
||||
check(do_open(&d14b, 44100, 2, AUDIO_FMT_S16, &ack14b) == 0 && ack14b.err == MEOW_OK,
|
||||
"PCM open still ok on BT after DSD reject");
|
||||
do_ctrl(&d14b, MSG_AUDIO_STOP, ack14b.stream_id);
|
||||
close(d14.fd); close(d14b.fd);
|
||||
}
|
||||
stop_audiod();
|
||||
|
||||
/* ---- T15:音量收编 v2 双组写 + 权威(A3:audiod 唯一音量权威;泛化双组写 PCM+DSD_VOLUME;
|
||||
* 维持平方锥度;audio.volume 由 audiod 读写权威)。amixer 轨迹断言:一次 vol set 同发 numid=1/2(DSD)。
|
||||
* 全部经 MSG_AUDIO_VOLUME —— 权威路径;host FILE sink 无真实 amixer(popen 失败仍记 trace)。 */
|
||||
printf("[T15 volume v2 dual-group write + authority]\n");
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
const char *tconf15 = "/tmp/meow_test_settings15.conf";
|
||||
FILE *tf15 = fopen(tconf15, "w");
|
||||
if (tf15) { fprintf(tf15, "# meow-settings 1\naudio.volume = 60\n"); fclose(tf15); }
|
||||
const char *trace15 = "/tmp/meow_amixer_trace15.txt";
|
||||
unlink(trace15);
|
||||
g_audiod_pid = start_audiod_trace("file", tconf15, trace15);
|
||||
usleep(400000);
|
||||
{
|
||||
struct cli v15;
|
||||
check(cli_connect(&v15) == 0, "connect V15");
|
||||
m_audio_open_ack_t ack15;
|
||||
check(do_open(&v15, 44100, 2, AUDIO_FMT_S16, &ack15) == 0 && ack15.err == MEOW_OK, "open V15");
|
||||
/* 权威写音量(非 BT→DAC):设置 40 */
|
||||
{
|
||||
int32_t pct = 40;
|
||||
if (sf(v15.fd, MSG_AUDIO_VOLUME, 15, &pct, sizeof pct) == 0) {
|
||||
uint8_t ab[64]; uint16_t at; rf(v15.fd, &at, ab, sizeof ab);
|
||||
}
|
||||
}
|
||||
usleep(150000);
|
||||
char q15[1024];
|
||||
check(do_query(&v15, q15, sizeof q15) == 0 && strstr(q15, "vol=40"),
|
||||
"authority vol set -> g_vol_pct=40 (audiod authoritative)");
|
||||
/* 持久化 audio.volume(audiod 读写权威,与 launcher/watchdog 同源同键) */
|
||||
{
|
||||
FILE *chk = fopen(tconf15, "r"); int has40 = 0; char l[128];
|
||||
if (chk) { while (fgets(l, sizeof l, chk)) if (strstr(l, "audio.volume = 40")) has40 = 1; fclose(chk); }
|
||||
check(has40, "audio.volume persisted = 40 (audiod authority)");
|
||||
}
|
||||
/* 双组写断言:trace 含 numid=1、numid=2、且 DSD_VOLUME(11) 同值(40→锥度≈40×40×192/10000≈30) */
|
||||
{
|
||||
char trb[4096]; int trl = 0;
|
||||
int fd = open(trace15, O_RDONLY);
|
||||
if (fd >= 0) { trl = (int)read(fd, trb, sizeof trb - 1); close(fd); if (trl > 0) trb[trl] = 0; }
|
||||
check(trl > 0 && strstr(trb, "cset numid=1 ") && strstr(trb, "cset numid=2 ") &&
|
||||
strstr(trb, "cset numid=11 "), "dual-group write: PCM numid=1/2 + DSD_VOLUME numid=11");
|
||||
/* 平方锥度:pct=40 → dac = (100-40)^2*192/10000 = 69.12 → 69(±1 容差) */
|
||||
int dac40 = (100 - 40) * (100 - 40) * 192 / 10000;
|
||||
/* 从 trace 找 numid=2 的值,应与 dac40 一致 */
|
||||
const char *p2 = strstr(trb, "cset numid=2 ");
|
||||
int got2 = p2 ? atoi(p2 + strlen("cset numid=2 ")) : -1;
|
||||
check(got2 == dac40, "square taper kept (numid=2 == (100-40)^2*192/10000)");
|
||||
}
|
||||
do_ctrl(&v15, MSG_AUDIO_STOP, ack15.stream_id);
|
||||
close(v15.fd);
|
||||
}
|
||||
stop_audiod();
|
||||
|
||||
/* ---- T16:监控日志记录高清/DoP 实际率 + 格式(A3:monitor 记录实际率;DSD 标记)。
|
||||
* 生命周期同 T8:流活跃期写 /tmp/audio_monitor_<bootts>.log,行含 %uHz/%uch/DoP 与 hwrate/vol。 */
|
||||
printf("[T16 monitor logs hi-res/DoP rate+fmt]\n");
|
||||
unlink(TSOCK); unlink(TWAV);
|
||||
g_audiod_pid = start_audiod();
|
||||
usleep(400000);
|
||||
{
|
||||
struct cli m16;
|
||||
check(cli_connect(&m16) == 0, "connect M16");
|
||||
/* 先清空 monitor 文件(复用同一 bootts 名;先前测试 T8-T15 已写入大量旧行),
|
||||
* 保证下面的检查只针对本 DoP 流新写行(append 模式将在文件尾追加)。 */
|
||||
char bootts16[64] = "";
|
||||
{ int fd = open("/tmp/meow_bootts", O_RDONLY); if (fd >= 0) { ssize_t r = read(fd, bootts16, 63); close(fd); if (r > 0) bootts16[r] = 0; } }
|
||||
char mp16[160]; snprintf(mp16, sizeof mp16, "/tmp/audio_monitor_%s.log", bootts16);
|
||||
unlink(mp16);
|
||||
m_audio_open_ack_t ack16;
|
||||
check(do_open_f(&m16, 176400, 2, AUDIO_FMT_DOP, AUDIO_FLAG_DSD, &ack16) == 0 && ack16.err == MEOW_OK,
|
||||
"open DoP@176400 for monitor");
|
||||
uint8_t dop16[4096];
|
||||
size_t fs16 = 2 * 4;
|
||||
size_t nd16 = sizeof dop16 / fs16;
|
||||
uint8_t *od16 = dop16;
|
||||
for (size_t i = 0; i < nd16; i++) {
|
||||
uint32_t wl = ((uint32_t)(i & 0xFFFFu) << 16) | (0xFAu << 8);
|
||||
uint32_t wr = ((uint32_t)(~i & 0xFFFFu) << 16) | (0x05u << 8);
|
||||
memcpy(od16, &wl, 4); od16 += 4; memcpy(od16, &wr, 4); od16 += 4;
|
||||
}
|
||||
for (int i = 0; i < 12; i++) do_write(&m16, dop16, (uint32_t)sizeof dop16);
|
||||
char mb16[4096]; int ml16 = 0;
|
||||
for (int i = 0; i < 30; i++) {
|
||||
usleep(200000); /* 等 ≥2 个 1s 节拍写 monitor 行 */
|
||||
int fd = open(mp16, O_RDONLY);
|
||||
if (fd >= 0) { ml16 = (int)read(fd, mb16, sizeof mb16 - 1); close(fd); if (ml16 > 0) mb16[ml16] = 0; }
|
||||
if (ml16 > 0 && strstr(mb16, "176400Hz/2ch/DoP") && strstr(mb16, "dsd=1")) break;
|
||||
}
|
||||
check(ml16 > 0 && strstr(mb16, "176400Hz/2ch/DoP") && strstr(mb16, "dsd=1"),
|
||||
"monitor log records container rate 176400 + fmt DoP + dsd=1");
|
||||
do_ctrl(&m16, MSG_AUDIO_STOP, ack16.stream_id);
|
||||
close(m16.fd);
|
||||
}
|
||||
stop_audiod();
|
||||
|
||||
printf("=== SUMMARY: pass=%d fail=%d ===\n", g_pass, g_fail);
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
}
|
||||
@@ -123,6 +123,24 @@ static void ams_report_exit(const struct app *a, int status, long long dur_ms)
|
||||
ams_report_exit_id(a->id, status, dur_ms);
|
||||
}
|
||||
|
||||
/* 批①后台常驻上报(单向 best-effort):转后台 `EV BG <app> <pid>`、回前台 `EV FG <app>`。
|
||||
* amsd 收到后置 AMS_BG/AMS_FG(批③后台管理页元数据来源);旧 launcher 不发、amsd 不识别也不崩。 */
|
||||
static void ams_report_bg(const char *app_id, pid_t pid)
|
||||
{
|
||||
if (!app_id || !app_id[0]) return;
|
||||
char line[256];
|
||||
int l = snprintf(line, sizeof line, "EV BG %s %d\n", app_id, (int)pid);
|
||||
if (l > 0) ams_send(line, l);
|
||||
}
|
||||
|
||||
static void ams_report_fg(const char *app_id)
|
||||
{
|
||||
if (!app_id || !app_id[0]) return;
|
||||
char line[256];
|
||||
int l = snprintf(line, sizeof line, "EV FG %s\n", app_id);
|
||||
if (l > 0) ams_send(line, l);
|
||||
}
|
||||
|
||||
/* ---- stdout/stderr 管道捕获(AMSD_DESIGN §5):前台 waitpid 循环排空 + 按行转发 ---- */
|
||||
static char g_ams_out_line[1024]; /* 未完结行缓冲(无 \n) */
|
||||
static int g_ams_out_n = 0;
|
||||
@@ -319,6 +337,7 @@ static void meow_acct_sync(const char *app_id, const struct app *ap)
|
||||
a->framework = ap->framework;
|
||||
a->background = ap->background; /* M4 */
|
||||
a->background_audio = ap->background_audio;
|
||||
a->background_keepalive = ap->background_keepalive; /* 批① */
|
||||
strncpy(a->icon, ap->icon, 128);
|
||||
app_name_copy(a->name, (int)sizeof a->name, ap->name);
|
||||
strncpy(a->exec, ap->exec, 128);
|
||||
@@ -460,6 +479,7 @@ void meow_bgplay_launcher_init(void)
|
||||
ops.now_ms = NULL; /* 内部 CLOCK_MONOTONIC */
|
||||
ops.card_present = sd_device_present;
|
||||
ops.on_bg_dead = bg_on_dead;
|
||||
ops.media_resident = meow_media_app_resident; /* 批①+宽限:媒体活跃判定(宽限回收/槽满LRU) */
|
||||
ops.on_svc_dead = NULL; /* 服务消亡日志 meow_bgplay 内已记 */
|
||||
ops.notif_clear_app = bg_notif_clear_app;
|
||||
ops.send_frame = bg_send_frame;
|
||||
@@ -648,6 +668,7 @@ void meow_parse_app_json(FILE *f, struct app *a)
|
||||
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(播放中退出转后台保活) */
|
||||
int bg_keepalive = 0; /* 批①:app.json background_keepalive=true(进入后台即保活,不依赖媒体会话) */
|
||||
struct app tmp;
|
||||
memset(&tmp, 0, sizeof tmp);
|
||||
while (fgets(buf, sizeof buf, f)) {
|
||||
@@ -779,6 +800,17 @@ void meow_parse_app_json(FILE *f, struct app *a)
|
||||
bg_audio = 1;
|
||||
}
|
||||
}
|
||||
/* "background_keepalive":true(批①:进入后台即保活,不依赖媒体会话=通用常驻)。
|
||||
* 注意 "background_keepalive" 是 "background" 的前缀:若本行先命中上方 background 分支,
|
||||
* json_str(buf,"background") 读出的是 "_keepalive…" ≠ true → 自然留 0,无害;
|
||||
* json_str 键前缀独立性(util.c:544 ident_ch)保证 "background"/"background_audio" 独立匹配不受影响。 */
|
||||
if (!bg_keepalive && strstr(buf, "background_keepalive")) {
|
||||
char v[8] = "";
|
||||
if (json_str(buf, "background_keepalive", v, sizeof v) > 0) {
|
||||
if (!strcmp(v, "true") || !strcmp(v, "1") || !strcmp(v, "yes"))
|
||||
bg_keepalive = 1;
|
||||
}
|
||||
}
|
||||
/* service.<svc>=mode:restart:max:window_ms(M1 解析存储,保活 M4) */
|
||||
char *sp = strstr(buf, "service.");
|
||||
if (sp && tmp.nsvc < MEOW_MAX_SVCS) {
|
||||
@@ -833,6 +865,7 @@ void meow_parse_app_json(FILE *f, struct app *a)
|
||||
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:后台播放保活 */
|
||||
a->background_keepalive = bg_keepalive ? '1' : 0; /* 批①:后台即保活(通用常驻) */
|
||||
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);
|
||||
@@ -1551,6 +1584,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
unlink(NOTIF_ACTIVE);
|
||||
drain_input();
|
||||
meow_nav_begin(a->id, NULL); /* 转后台时导航栈已清;回前台从裸态重建 */
|
||||
ams_report_fg(a->id); /* 批①:单向 best-effort 上报 amsd 置 AMS_FG */
|
||||
}
|
||||
if (!resumed) {
|
||||
/* 二进制依赖启动前检查(app.json depends):required 缺失阻断启动,optional 缺失放行。
|
||||
@@ -1931,19 +1965,29 @@ 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);
|
||||
/* 批①+宽限后台保活:**全 App** nav-exit 入后台保活表(进程保留转后台,UI 冻结走 §10.5
|
||||
* SDK present 自抑,**非 SIGSTOP**——SIGSTOP 会冻结同进程播放线程断供 PCM,§10.5 实证)。
|
||||
* 分两类:
|
||||
* - 声明常驻(background_keepalive=1)或媒体活跃(background_audio+PLAYING/PAUSED/BUFFER,
|
||||
* 见 meow_media_app_resident)→ **无限期保活**(不清,除非用户手动/一键清);
|
||||
* - 其余(未声明/媒体不活跃)→ **宽限期**(bg_grace_ms,默认30min)内保活,超时未再启动
|
||||
* 由 meow_bg_tick 走真退序列回收。
|
||||
* meow_bg_add 内含槽满 LRU 回收(最久未激活的未声明槽);全为声明常驻/媒体活跃 → NOMEM
|
||||
* 回落下方真退序列(保安全)。kill 通道带 !a->framework 门控(批③)。 */
|
||||
int bg_resident = meow_media_app_resident(a->id);
|
||||
int r = meow_bg_add(a->id, pid, out_fd, app_on_card,
|
||||
a->background_keepalive == '1');
|
||||
if (r == 0 || r == MEOW_ERR_EXISTS) { /* 0=入表;EXISTS=本就驻留(幂等不重复登记不杀) */
|
||||
logmsg("bg: app %s nav-exit -> 后台保活(%s,pid=%d 保留,SDK present 自抑)",
|
||||
a->name,
|
||||
((a->background_keepalive == '1') || bg_resident) ?
|
||||
"声明常驻/媒体活跃(无限期)" : "宽限期(默认30min)",
|
||||
(int)pid);
|
||||
to_background = 1;
|
||||
break;
|
||||
}
|
||||
logmsg("nav: app %s MSG_NAV{EXIT} -> terminate, return to home", a->name);
|
||||
logmsg("nav: app %s MSG_NAV{EXIT} -> terminate, return to home(后台表满且无未声明槽可回收)",
|
||||
a->name);
|
||||
nav_clean_exit = 1;
|
||||
kill(pid, SIGTERM);
|
||||
usleep(300000);
|
||||
@@ -2177,6 +2221,7 @@ static int launch_app_chain(struct app *a, const char *chain)
|
||||
meow_dirty_add(0, SB_BAR_Y, fb_xres, SB_BAR_H);
|
||||
settings_load();
|
||||
meow_nav_exit(); /* 前台会话导航栈清栈(App 本体栈内存活;回前台时裸态重建) */
|
||||
ams_report_bg(a ? a->id : "", pid); /* 批①:单向 best-effort 上报 amsd 置 AMS_BG */
|
||||
return 0;
|
||||
}
|
||||
/* §10.5:App 前台会话结束,清会话标记(ipc_dispatch 不再受理 MSG_NOTIF_OPEN) */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/wait.h>
|
||||
#include <fcntl.h>
|
||||
@@ -63,9 +64,10 @@ void meow_bgplay_init(const meow_bgplay_ops *ops)
|
||||
|
||||
/* ================= 后台播放保活 ================= */
|
||||
|
||||
int meow_bg_should_keep(int bg_audio_declared, int media_playing)
|
||||
int meow_bg_should_keep(int bg_audio_declared, int media_resident, int keepalive_declared)
|
||||
{
|
||||
return bg_audio_declared && media_playing;
|
||||
if (keepalive_declared) return 1; /* 通用常驻:进后台即保活 */
|
||||
return bg_audio_declared && media_resident; /* 播放类:播放/暂停/缓冲都保活 */
|
||||
}
|
||||
|
||||
static meow_bg_entry_t *bg_find(const char *app_id)
|
||||
@@ -78,27 +80,100 @@ static meow_bg_entry_t *bg_find(const char *app_id)
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card)
|
||||
/* 填一槽(add 与槽满 LRU 复用)。 */
|
||||
static void bg_fill(int i, const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive)
|
||||
{
|
||||
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;
|
||||
e->bg_since_ms = bg_now_ms(); /* 批①:转后台时刻(宽限/槽满 LRU 掐最久用的锚点) */
|
||||
e->keepalive = keepalive ? 1 : 0; /* 批①+宽限:1=声明常驻(无限期);0=宽限期类 */
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] 转后台保活: %s pid=%d on_card=%d keepalive=%d",
|
||||
app_id, (int)pid, on_card, e->keepalive);
|
||||
}
|
||||
|
||||
/* 真实单调毫秒(不随 ops.now_ms 注入的假时钟走)。槽满 LRU 回收需要真实的有界等超时——
|
||||
* 注入假时钟的宿主测试若用它算 deadline 则永不超时(假时钟不随 usleep 推进),故独立取真时钟。 */
|
||||
static uint64_t bg_real_now_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
|
||||
}
|
||||
|
||||
/* 槽满 LRU 回收:找 bg_since_ms 最旧的**未声明且媒体不活跃**槽,走真退序列 SIGTERM→有界等→
|
||||
* SIGKILL→reap→on_bg_dead(通知 amsd + 清媒体会话)→ 摘表。返回被回收槽下标;-1=无可回收
|
||||
* (全为声明常驻/媒体活跃,按保安全拒绝新入)。 */
|
||||
static int bg_evict_lru(void)
|
||||
{
|
||||
uint64_t oldest = UINT64_MAX;
|
||||
int cand = -1;
|
||||
int i;
|
||||
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
if (!g_bg[i].used) continue;
|
||||
if (g_bg[i].keepalive) continue; /* 声明常驻不回收 */
|
||||
if (g_ops.media_resident && g_ops.media_resident(g_bg[i].app_id))
|
||||
continue; /* 媒体活跃不回收 */
|
||||
if (g_bg[i].bg_since_ms < oldest) { oldest = g_bg[i].bg_since_ms; cand = i; }
|
||||
}
|
||||
if (cand < 0) return -1;
|
||||
|
||||
{
|
||||
meow_bg_entry_t *e = &g_bg[cand];
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
pid_t pid = e->pid;
|
||||
int ofd = e->out_fd;
|
||||
int st = 0;
|
||||
uint64_t deadline;
|
||||
|
||||
snprintf(id, sizeof(id), "%s", e->app_id);
|
||||
bg_log("[bgplay] 后台表满 LRU 回收: %s pid=%d(最久未激活·未声明)", id, (int)pid);
|
||||
kill(pid, SIGTERM);
|
||||
deadline = bg_real_now_ms() + TERM_GRACE_MS;
|
||||
for (;;) {
|
||||
pid_t r = waitpid(pid, &st, WNOHANG);
|
||||
if (r == pid) break;
|
||||
if (r < 0 && errno != EINTR) break; /* 已回收/非子进程:不再等 */
|
||||
if (bg_real_now_ms() >= deadline) {
|
||||
kill(pid, SIGKILL);
|
||||
deadline = bg_real_now_ms() + 200u; /* SIGKILL 后非阻塞兜底,防 D-state 挂死 */
|
||||
while (bg_real_now_ms() < deadline) {
|
||||
if (waitpid(pid, &st, WNOHANG) == pid) break;
|
||||
usleep(10000);
|
||||
}
|
||||
break;
|
||||
}
|
||||
usleep(5000);
|
||||
}
|
||||
if (ofd >= 0) close(ofd); /* 被回收 App 已死,其 stdout 读端一并关 */
|
||||
if (g_ops.on_bg_dead) g_ops.on_bg_dead(id, pid, st); /* 通知 amsd + 清媒体会话 */
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] LRU 回收完成: %s 槽释放", id);
|
||||
memset(e, 0, sizeof(*e));
|
||||
}
|
||||
return cand;
|
||||
}
|
||||
|
||||
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (!g_bg[i].used) { bg_fill(i, app_id, pid, out_fd, on_card, keepalive); return 0; }
|
||||
/* 槽满:LRU 回收最久未激活的未声明槽;全为声明常驻/媒体活跃 → NOMEM(调用方回落真退)。 */
|
||||
if (bg_evict_lru() >= 0)
|
||||
for (i = 0; i < MEOW_BG_MAX; i++)
|
||||
if (!g_bg[i].used) { bg_fill(i, app_id, pid, out_fd, on_card, keepalive); return 0; }
|
||||
return MEOW_ERR_NOMEM;
|
||||
}
|
||||
|
||||
@@ -154,25 +229,39 @@ void meow_bg_tick(void)
|
||||
|
||||
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);
|
||||
/* 触发终止(SIGTERM→SIGKILL 真退序列)的两类条件,仅当尚未进入终止序列(term_at_ms==0):
|
||||
* a) 卡拔例外:on_card 且卡不在(防 SIGBUS);
|
||||
* b) 批①+宽限 超时回收:未声明(keepalive=0)且媒体不活跃(media_resident=0),转后台
|
||||
* 超 MEOW_BG_GRACE_MS 未再启动 → 回收。声明常驻(keepalive=1)/媒体活跃**不清**。 */
|
||||
if (e->term_at_ms == 0) {
|
||||
int grace = (e->keepalive == 0 &&
|
||||
!(g_ops.media_resident ? g_ops.media_resident(e->app_id) : 0) &&
|
||||
now - e->bg_since_ms > (uint64_t)MEOW_BG_GRACE_MS);
|
||||
if ((e->on_card && !bg_card_present()) || grace) {
|
||||
if (grace)
|
||||
bg_log("[bgplay] 宽限期超时回收后台App: %s pid=%d(超 %ums 未再启动)",
|
||||
e->app_id, (int)e->pid, (unsigned)MEOW_BG_GRACE_MS);
|
||||
else
|
||||
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 */
|
||||
}
|
||||
}
|
||||
/* SIGTERM 后 SIGKILL 兜底(TERM_GRACE_MS 节流重复发直至 reap;卡拔/宽限/槽满回收共用) */
|
||||
if (e->term_at_ms && now - e->term_at_ms >= TERM_GRACE_MS) {
|
||||
kill(e->pid, SIGKILL);
|
||||
e->term_at_ms = now;
|
||||
}
|
||||
|
||||
r = waitpid(e->pid, &st, WNOHANG);
|
||||
if (r == e->pid) {
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
pid_t pid = e->pid;
|
||||
int ofd = e->out_fd;
|
||||
|
||||
snprintf(id, sizeof(id), "%s", e->app_id);
|
||||
if (ofd >= 0) close(ofd); /* 回收:后台 App 已死,其 stdout 管道读端一并关(防 fd 泄漏) */
|
||||
memset(e, 0, sizeof(*e));
|
||||
g_change_seq++;
|
||||
bg_log("[bgplay] 后台 App 退出: %s pid=%d status=0x%x", id, (int)pid, st);
|
||||
|
||||
@@ -29,6 +29,12 @@
|
||||
|
||||
/* 后台保活 App 上限(同时驻留后台的播放中 App) */
|
||||
#define MEOW_BG_MAX 4
|
||||
/* 批①+宽限:无声明/媒体不活跃 App 入后台的宽限期(默认 30 分钟)。
|
||||
* 语义:转后台(bg_since_ms)后超 MEOW_BG_GRACE_MS 未再启动 → meow_bg_tick 走真退序列
|
||||
* SIGTERM→SIGKILL 回收(摘表 + 通知 amsd)。**声明常驻(keepalive=1)或媒体活跃
|
||||
* (meow_media_app_resident=1)不清**。本批为编译期常量(30*60*1000 = 1800000ms);
|
||||
* 后续可进设置/按 App 可配(待拍板)。 */
|
||||
#define MEOW_BG_GRACE_MS 1800000u
|
||||
/* 无头后台服务运行时槽上限(与 meow_acct.svcs 的 MEOW_MAX_SVCS 对齐) */
|
||||
#define MEOW_SVC_RT_MAX 8
|
||||
|
||||
@@ -45,7 +51,10 @@ typedef struct meow_bg_entry {
|
||||
pid_t pid;
|
||||
int out_fd;
|
||||
int on_card; /* 二进制在 TF 卡上(卡拔需 SIGTERM 防 SIGBUS) */
|
||||
uint64_t term_at_ms; /* 卡拔 SIGTERM 发出时刻(0=未发;SIGKILL 兜底计时) */
|
||||
uint64_t term_at_ms; /* 卡拔/宽限超时/槽满回收 SIGTERM 发出时刻(0=未发;SIGKILL 兜底计时) */
|
||||
uint64_t bg_since_ms; /* 批①:转后台时刻(monotonic ms;宽限超时回收/槽满 LRU 掐最久用) */
|
||||
char keepalive; /* 批①+宽限:1=声明常驻(background_keepalive,无限期不清);0=宽限期类
|
||||
* (未声明/background_audio,媒体不活跃时超 bg_grace_ms 回收) */
|
||||
} meow_bg_entry_t;
|
||||
|
||||
/* 无头后台服务运行时槽(静态声明在 meow_acct.svcs,此处为运行态)。 */
|
||||
@@ -71,6 +80,8 @@ typedef struct meow_svc_rt {
|
||||
/* 依赖注入。全 NULL 可用(log 静默、now_ms 内部 CLOCK_MONOTONIC、card_present 恒 1、
|
||||
* 通知清理/发帧跳过)。
|
||||
* on_bg_dead:后台保活 App 进程退出(status=waitpid 原始状态;正常/异常由回调方判);
|
||||
* media_resident:该 App 媒体会话是否「活跃可驻留」(launcher 注入 meow_media_app_resident)。
|
||||
* 批①+宽限用:宽限超时回收 / 槽满 LRU 判「媒体活跃不清不回收」;NULL=恒 0(视为不活跃)。
|
||||
* on_svc_dead:服务槽最终消亡(stopping 走完/不重启死亡;熔断不算消亡,槽保留待复位);
|
||||
* notif_clear_app:熔断时清该 App 全部通知,返回清除条数(<0=失败);
|
||||
* send_frame:熔断广播 MSG_SYS(launcher 侧须先查连接存活再发);
|
||||
@@ -80,6 +91,7 @@ typedef struct meow_bgplay_ops {
|
||||
uint64_t (*now_ms)(void);
|
||||
int (*card_present)(void);
|
||||
void (*on_bg_dead)(const char *app_id, pid_t pid, int status);
|
||||
int (*media_resident)(const char *app_id);
|
||||
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,
|
||||
@@ -91,12 +103,20 @@ typedef struct meow_bgplay_ops {
|
||||
void meow_bgplay_init(const meow_bgplay_ops *ops);
|
||||
|
||||
/* ---- 后台播放保活判定(纯函数,无副作用)---- */
|
||||
/* 退出时是否转后台保活:声明 background_audio 且该 App 媒体会话正在播放。 */
|
||||
int meow_bg_should_keep(int bg_audio_declared, int media_playing);
|
||||
/* 退出时是否转后台保活(批①三参化):
|
||||
* keepalive_declared=1(声明 background_keepalive)→ 恒保活(通用常驻,不依赖媒体会话);
|
||||
* 否则 bg_audio_declared(声明 background_audio)且 media_resident(该 App 媒体会话活跃
|
||||
* =PLAYING/PAUSED/BUFFER,见 meow_media_app_resident)→ 保活。
|
||||
* 未声明/无活跃会话 → 0(走原真退序列)。 */
|
||||
int meow_bg_should_keep(int bg_audio_declared, int media_resident, int keepalive_declared);
|
||||
|
||||
/* ---- 后台保活 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/pid/out_fd,keepalive 标记常驻类型(1=background_keepalive 类=无限期;
|
||||
* 0=宽限期类=未声明/background_audio,媒体不活跃时超 bg_grace_ms 回收)。批①+宽限:**全 App**
|
||||
* nav-exit 都经此入表;槽满(MEOW_BG_MAX=4)时 LRU 回收最久未激活(bg_since_ms 最旧)的**未声明**
|
||||
* 槽(不回收声明常驻/媒体活跃);全为声明常驻/媒体活跃 → MEOW_ERR_NOMEM(调用方回落真退,保安全)。
|
||||
* 返回 0=成功;MEOW_ERR_EXISTS=已在后台;MEOW_ERR_NOMEM=满且无可回收。 */
|
||||
int meow_bg_add(const char *app_id, pid_t pid, int out_fd, int on_card, int keepalive);
|
||||
/* 回前台:按 app_id 摘除槽(进程归还前台会话)。成功返回 0 且 *out_fd(可 NULL)带回
|
||||
* 管道读端;未在后台返回 -1。 */
|
||||
int meow_bg_take(const char *app_id, int *out_fd, pid_t *pid);
|
||||
@@ -109,8 +129,11 @@ 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。 */
|
||||
/* 周期调(主循环 ~10ms 排空点):waitpid WNOHANG 回收死亡后台 App(→on_bg_dead 清槽)。
|
||||
* 终止触发(SIGTERM,TERM_GRACE_MS 后 SIGKILL):
|
||||
* - 卡拔例外:on_card 槽在卡不在时终止(防 SIGBUS);
|
||||
* - 批①+宽限 超时回收:未声明(keepalive=0)且媒体不活跃(media_resident=0)的槽,转后台超
|
||||
* MEOW_BG_GRACE_MS 未再启动 → 回收(走真退序列 + 摘表 + 通知 amsd)。声明常驻/媒体活跃不清。 */
|
||||
void meow_bg_tick(void);
|
||||
|
||||
/* ---- 无头后台服务 ---- */
|
||||
|
||||
@@ -485,24 +485,32 @@ typedef struct m_picker_result {
|
||||
|
||||
/* ================= audiod 独占音频服务(AUDIOD_DESIGN.md) = */
|
||||
|
||||
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE。
|
||||
* v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32 处理;FLOAT v1 拒绝。 */
|
||||
/* PCM 格式枚举(客户端原生 fmt 与 grant_fmt 共用):1=S16_LE 2=S24_LE 3=S32_LE 4=FLOAT_LE
|
||||
* 5=AUDIO_FMT_DOP(DoP DSD 透明容器格式)。v1 实装 S16/S32(S16 直通 + S32 升位);S24 按 32-in-32
|
||||
* 处理;FLOAT v1 拒绝。AUDIO_FMT_DOP 为 **独立格式值**(A3+DSD 定案:避免 S24 名义(3B)/实际(4B)歧义),
|
||||
* 仅在 MSG_AUDIO_OPEN.flags 带 AUDIO_FLAG_DSD 时合法;audiod 只做 DoP 容器透传(不引 dsd2pcm)。 */
|
||||
enum {
|
||||
AUDIO_FMT_S16 = 1,
|
||||
AUDIO_FMT_S24 = 2,
|
||||
AUDIO_FMT_S32 = 3,
|
||||
AUDIO_FMT_FLOAT = 4
|
||||
AUDIO_FMT_FLOAT = 4,
|
||||
AUDIO_FMT_DOP = 5 /* DoP DSD(32-bit 容器:bits[31:16]=16bit DSD 数据,bits[15:8]=标记 0xFA/0x05,bits[7:0]=0),
|
||||
容器率 = DSD 位率/16(DSD64→176400 / DSD128→352800 / DSD256→705600) */
|
||||
};
|
||||
|
||||
/* MSG_AUDIO_OPEN.flags 位(预留曾恒 0;A3+DSD 起使用) */
|
||||
#define AUDIO_FLAG_DSD 0x00000001u /* fmt==AUDIO_FMT_DOP:本流为 DoP DSD(App 侧已解码),audiod 只透传 */
|
||||
|
||||
/* MSG_AUDIO_OPEN ↑:请求开流(App 原生声源参数)。grant_*(ACK)回 audiod 实际授权给客户端的
|
||||
* 产出格式(率/声道/位深)——客户端必须按 grant 格式产出 PCM(SDK meow_ui_audio 负责归一:
|
||||
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。 */
|
||||
* mono→stereo 复制 / S16↔S32 定值升降位)。ch>2 或 FLOAT → ERR_INVAL。fmt==AUDIO_FMT_DOP
|
||||
* 须带 AUDIO_FLAG_DSD(否则 ERR_INVAL);DSD 经 DoP 透传,audiod 不引 dsd2pcm。 */
|
||||
typedef struct m_audio_open {
|
||||
char app_id[MEOW_APP_ID_MAX]; /* 32 */
|
||||
uint32_t rate; /* 原生采样率(应用源,如 44100/48000) */
|
||||
uint32_t rate; /* 原生采样率(应用源,如 44100/48000;DSD 容器率) */
|
||||
uint16_t ch; /* 原生声道数(1=mono 2=stereo) */
|
||||
uint16_t fmt; /* 原生格式(AUDIO_FMT_*) */
|
||||
uint32_t flags; /* 预留(0) */
|
||||
uint16_t fmt; /* 原生格式(AUDIO_FMT_*;DSD 用 AUDIO_FMT_DOP) */
|
||||
uint32_t flags; /* AUDIO_FLAG_DSD 等 */
|
||||
} m_audio_open_t; /* 32+4+2+2+4 = 44 B */
|
||||
|
||||
/* MSG_AUDIO_OPEN_ACK ↓:授权结果。err=MEOW_OK 才有效(stream_id>0 且 grant_* 为实际授权值);
|
||||
@@ -533,8 +541,9 @@ typedef struct m_audio_route {
|
||||
} m_audio_route_t; /* 4 B */
|
||||
|
||||
/* MSG_AUDIO_VOLUME ↑:音量查询/设置。pct<0(-1)=查询当前音量;0-100=设置(写 DAC numid=1/2
|
||||
* + 持久化 settings.conf `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时
|
||||
* **不下发 DAC 音量反转**(音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
|
||||
* + **DSD_VOLUME(音量收编 v2 泛化双组写,保证 DSD↔PCM 切换连续/防爆音)** + 持久化 settings.conf
|
||||
* `audio.volume`,与 watchdog/launcher 同源同键)。BT 输出目标时**不下发 DAC 音量反转**
|
||||
* (音量由 AVRCP/耳机侧控,见 BLUETOOTH_FULL_OVERHAUL_PLAN §五)。 */
|
||||
typedef struct m_audio_volume {
|
||||
int32_t pct; /* -1=查询;0-100=设置 */
|
||||
} m_audio_volume_t; /* 4 B */
|
||||
|
||||
@@ -329,6 +329,7 @@ struct app {
|
||||
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 background_keepalive; /* 批①:'1'=background_keepalive=true(进入后台即保活,不依赖媒体会话=通用常驻) */
|
||||
char icon[129]; /* icon= 路径 */
|
||||
int nroute; /* route.<id> 行数 */
|
||||
struct meow_route routes[MEOW_MAX_ROUTES];
|
||||
@@ -355,6 +356,7 @@ struct meow_acct {
|
||||
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) */
|
||||
char background_keepalive; /* 批①:'1'=background_keepalive=true(进入后台即保活,不依赖媒体会话) */
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -127,6 +127,12 @@ int meow_media_app_playing(const char *app_id)
|
||||
return (s->state == MEOW_MEDIA_PLAYING && strcmp(s->app_id, app_id) == 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
int meow_media_app_resident(const char *app_id)
|
||||
{
|
||||
const meow_media_session_t *s = sess_find(app_id);
|
||||
return (s && s->state != MEOW_MEDIA_STOPPED) ? 1 : 0;
|
||||
}
|
||||
|
||||
static int send_playctl(const meow_media_session_t *s, uint32_t cmd, int64_t seek_ms)
|
||||
{
|
||||
if (!s || !s->used || !g_ops || !g_ops->send) return -1;
|
||||
|
||||
@@ -84,6 +84,11 @@ int meow_media_has_source(void);
|
||||
* launcher 据此决定通知中心叠加不 SIGSTOP 该 App(保 PCM 供流),SDK present 层自抑上屏。 */
|
||||
int meow_media_app_playing(const char *app_id);
|
||||
|
||||
/* 批①后台常驻判定:该 app 的媒体会话「活跃可驻留」= 会话存在且 state != STOPPED
|
||||
* (PLAYING/PAUSED/BUFFER 皆常驻)。按 app 自身会话判(sess_find),不读最近活跃源——
|
||||
* 多源时不误判「当前源≠本 App」。launcher 转后台保活依据。 */
|
||||
int meow_media_app_resident(const char *app_id);
|
||||
|
||||
/* 向当前源下发 PLAYCTL(卡片按钮/播放页/媒体键/控制器)。返回 0=已发,-1=无源/发送失败。 */
|
||||
int meow_media_playctl(uint32_t cmd, int64_t seek_ms);
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/* test_bg_keep.c — 批①`background_keepalive` 声明层宿主单测。
|
||||
*
|
||||
* 验证 meow_parse_app_json 对 app.json 后台三位(background / background_audio /
|
||||
* background_keepalive)的解析与 meow_acct_sync 透传,重点覆盖**前缀不串扰**:
|
||||
* - "background_keepalive":true → a->background_keepalive=='1';且 background/background_audio
|
||||
* 在该行不被误命中(分别是 0/0);
|
||||
* - background:true → a->background=='1'(独立键匹配不受影响);
|
||||
* - background_audio:true → a->background_audio=='1';
|
||||
* - 一行三键并存:三者各归其位;
|
||||
* - meow_acct_sync 透传 background_keepalive 到注册表。
|
||||
* 实现手法同 test_fg_act.c:真实 include app_launcher.c,-ffunction-sections -Wl,--gc-sections
|
||||
* 只保留 parseInt/acct_sync/upsert/find 触及函数;json_str/meow_parse_depends/logmsg 由本文件提供
|
||||
* 忠实桩(json_str 复刻 util.c 语义,含键前缀独立 ident_ch + 整体 UTF-8 截断)。
|
||||
*
|
||||
* 运行(patch_v2/meow_launcherd 下):
|
||||
* gcc -D_GNU_SOURCE -Wall -Wextra -ffunction-sections -Wl,--gc-sections \
|
||||
* -I. -o test_bg_keep test_bg_keep.c && ./test_bg_keep
|
||||
* 全 PASS -> exit 0。
|
||||
*/
|
||||
#ifndef _GNU_SOURCE
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/* ---- logmsg:app_launcher.c 依赖,minimal 桩 ---- */
|
||||
void logmsg(const char *fmt, ...)
|
||||
{
|
||||
(void)fmt;
|
||||
}
|
||||
|
||||
/* ---- json_str:复刻 util.c 语义(键前缀独立 + 值提取 + UTF-8 截断)---- */
|
||||
static int ident_ch(char c)
|
||||
{
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '.';
|
||||
}
|
||||
static void utf8_trim(char *s)
|
||||
{
|
||||
size_t n = strlen(s), i = 0, last_good = 0;
|
||||
while (i < n) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
int len = (c < 0x80) ? 1 : (c >= 0xF0) ? 4 : (c >= 0xE0) ? 3 : (c >= 0xC0) ? 2 : 1;
|
||||
if (i + (size_t)len > n) break;
|
||||
i += (size_t)len;
|
||||
last_good = i;
|
||||
}
|
||||
s[last_good] = '\0';
|
||||
}
|
||||
int json_str(const char *buf, const char *key, char *out, int olen)
|
||||
{
|
||||
out[0] = 0;
|
||||
if (!buf || !key || olen <= 0) return 0;
|
||||
const char *p = strstr(buf, key);
|
||||
if (!p) return 0;
|
||||
if (p > buf && ident_ch((unsigned char)p[-1])) return 0; /* 键前缀需独立 */
|
||||
p += strlen(key);
|
||||
if (*p == '"') p++; /* JSON 键自身右引号 */
|
||||
while (*p && (*p == ':' || *p == '=')) p++;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p == '"') p++; /* 值左引号 */
|
||||
int i = 0;
|
||||
while (*p && *p != '"' && *p != ',' && *p != '\n' && *p != '\r' && i < olen - 1)
|
||||
out[i++] = *p++;
|
||||
out[i] = 0;
|
||||
while (i > 0 && (out[i - 1] == ' ' || out[i - 1] == '\t')) out[--i] = 0;
|
||||
utf8_trim(out);
|
||||
return (int)strlen(out);
|
||||
}
|
||||
|
||||
#include "app_launcher.c" /* 真实实现:meow_parse_app_json / meow_acct_sync / meow_acct_upsert */
|
||||
|
||||
/* meow_parse_depends:本测试 app.json 无 depends 行,桩返回 0(struct meow_dep 此时已可见)。 */
|
||||
int meow_parse_depends(const char *val, struct meow_dep *deps, int max)
|
||||
{
|
||||
(void)val; (void)deps; (void)max;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int g_fail = 0;
|
||||
#define CHECK(cond, msg) do { \
|
||||
if (!(cond)) { fprintf(stderr, "FAIL: %s (%s:%d)\n", msg, __FILE__, __LINE__); g_fail++; } \
|
||||
else fprintf(stderr, "ok: %s\n", msg); \
|
||||
} while (0)
|
||||
|
||||
/* 用一段 app.json 内容解析出 struct app。返回 0=成功。 */
|
||||
static int parse_json(const char *content, struct app *a)
|
||||
{
|
||||
FILE *f = tmpfile();
|
||||
if (!f) return -1;
|
||||
fputs(content, f);
|
||||
rewind(f);
|
||||
memset(a, 0, sizeof *a);
|
||||
meow_parse_app_json(f, a);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
/* ---- 1) 单独 background_keepalive:true ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_keepalive\":true\n}", &a);
|
||||
CHECK(a.background_keepalive == '1', "background_keepalive:true -> '1'");
|
||||
CHECK(a.background == 0, "keepalive 行不误命中 background (==0)");
|
||||
CHECK(a.background_audio == 0, "keepalive 行不误命中 background_audio (==0)");
|
||||
}
|
||||
/* ---- 2) 单独 background:true ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background\":true\n}", &a);
|
||||
CHECK(a.background == '1', "background:true -> '1'(独立键匹配不受影响)");
|
||||
CHECK(a.background_keepalive == 0, "background 行不串 to keepalive (==0)");
|
||||
CHECK(a.background_audio == 0, "background 行不串 to background_audio (==0)");
|
||||
}
|
||||
/* ---- 3) 单独 background_audio:true ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_audio\":true\n}", &a);
|
||||
CHECK(a.background_audio == '1', "background_audio:true -> '1'");
|
||||
CHECK(a.background == 0, "background_audio 行不误命中 background (==0)");
|
||||
CHECK(a.background_keepalive == 0, "background_audio 行不串 to keepalive (==0)");
|
||||
}
|
||||
/* ---- 4) 三键并存:各归其位(逐行格式,值后跟 , 或换行,同真实 app.json) ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background\":true,\n"
|
||||
"\"background_audio\":true,\n\"background_keepalive\":true\n}", &a);
|
||||
CHECK(a.background == '1', "三键并存 background=='1'");
|
||||
CHECK(a.background_audio == '1', "三键并存 background_audio=='1'");
|
||||
CHECK(a.background_keepalive == '1', "三键并存 background_keepalive=='1'");
|
||||
}
|
||||
/* ---- 5) 假值 / 前导噪声不应置位 ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n\"background_keepalive\":false,\n"
|
||||
"\"background_audio\":0\n}", &a);
|
||||
CHECK(a.background_keepalive == 0, "background_keepalive:false -> 0");
|
||||
CHECK(a.background_audio == 0, "background_audio:0 -> 0");
|
||||
CHECK(a.background == 0, "无 background 键 -> 0");
|
||||
}
|
||||
/* ---- 6) meow_acct_sync 透传 ---- */
|
||||
{
|
||||
struct app a;
|
||||
parse_json("{\n\"name\":\"X\",\n\"exec\":\"/usr/bin/x\",\n"
|
||||
"\"background_keepalive\":true,\n\"background_audio\":true\n}", &a);
|
||||
meow_acct_sync("dev.test.keep", &a); /* 以 app_id 建/更注册表项 */
|
||||
struct meow_acct *ac = meow_acct_find("dev.test.keep");
|
||||
CHECK(ac != NULL, "meow_acct_sync 建注册表项");
|
||||
CHECK(ac && ac->background_keepalive == '1', "透传 background_keepalive=='1'");
|
||||
CHECK(ac && ac->background_audio == '1', "透传 background_audio=='1'");
|
||||
CHECK(ac && ac->background == 0, "透传 background==0");
|
||||
}
|
||||
|
||||
if (g_fail) { fprintf(stderr, "\n== %d assertion(s) FAILED ==\n", g_fail); return 1; }
|
||||
fprintf(stderr, "\n== test_bg_keep ALL PASS ==\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
* 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;
|
||||
* 4b) 批①+宽限:未声明(keepalive=0)媒体不活跃超 MEOW_BG_GRACE_MS 回收(含恰等边界不回收);
|
||||
* 声明常驻(keepalive=1)或媒体活跃(META PLAYING)超 grace 不清;
|
||||
* 5) bg 表满(MEOW_BG_MAX)→ LRU 回收最久未激活的未声明槽新增成功;全为声明常驻 → MEOW_ERR_NOMEM
|
||||
* (不回收声明常驻);pid<=0 → MEOW_ERR_INVAL;重复 add → MEOW_ERR_EXISTS;
|
||||
* 6) 服务注册:exec 不可执行 → NOENT;同名在跑 → EXISTS;
|
||||
* 7) 退避重启 1s/2s + max_restart=2 熔断(假时钟注入):清通知回调 + 注册连接收到
|
||||
* MSG_SYS{SVC_DEAD=2} + circuit 后不再拉起;重注册复位熔断重新拉起;
|
||||
@@ -154,6 +157,7 @@ int main(void)
|
||||
ops.now_ms = fake_now_ms;
|
||||
ops.card_present = fake_card;
|
||||
ops.on_bg_dead = fake_bg_dead;
|
||||
ops.media_resident = meow_media_app_resident; /* 批①+宽限:媒体活跃判定(宽限回收/槽满LRU) */
|
||||
ops.notif_clear_app = fake_notif_clear;
|
||||
ops.send_frame = fake_send_frame;
|
||||
ops.log_dir = NULL;
|
||||
@@ -163,11 +167,15 @@ int main(void)
|
||||
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");
|
||||
/* ---- 1) should_keep 判定矩阵(批①三参:bg_audio, media_resident, keepalive) ---- */
|
||||
CHECK(meow_bg_should_keep(0, 0, 0) == 0, "should_keep(未声明,未活跃,非常驻)=0");
|
||||
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(声明bg_audio,无会话)=0");
|
||||
CHECK(meow_bg_should_keep(0, 1, 0) == 0, "should_keep(未声明bg_audio,活跃)=0");
|
||||
CHECK(meow_bg_should_keep(1, 1, 0) == 1, "should_keep(bg_audio+活跃)=1");
|
||||
CHECK(meow_bg_should_keep(0, 0, 1) == 1, "should_keep(keepalive恒保)=1");
|
||||
CHECK(meow_bg_should_keep(0, 1, 1) == 1, "should_keep(keepalive恒保,忽略media)=1");
|
||||
CHECK(meow_bg_should_keep(1, 0, 1) == 1, "should_keep(keepalive恒保,无会话)=1");
|
||||
CHECK(meow_bg_should_keep(1, 0, 0) == 0, "should_keep(bg_audio+resident=0 真退)=0");
|
||||
|
||||
/* ---- 2) 后台播放保活全链路 ---- */
|
||||
int sp[2];
|
||||
@@ -181,14 +189,20 @@ int main(void)
|
||||
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,
|
||||
CHECK(meow_media_app_resident("dev.test.bg1") == 1, "app_resident(bg1)=1");
|
||||
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 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_add("dev.test.bg1", kid, sp[0], 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");
|
||||
{
|
||||
const meow_bg_entry_t *be = meow_bg_at(0);
|
||||
CHECK(be && be->bg_since_ms > 0, "bg 槽记 bg_since_ms(转后台时刻)");
|
||||
CHECK(be && be->keepalive == 0, "bg_audio 类 keepalive=0");
|
||||
}
|
||||
CHECK(kill(kid, 0) == 0, "后台化后进程仍存活(未 SIGTERM/SIGSTOP)");
|
||||
/* PLAYCTL 经会话 fd 往返(后台化不断连:进程/fd 均保留) */
|
||||
CHECK(meow_media_playctl_to("dev.test.bg1", 6, 0) == 0, "PLAYCTL 下发到后台 App 会话");
|
||||
@@ -215,16 +229,28 @@ int main(void)
|
||||
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,
|
||||
CHECK(meow_bg_should_keep(1, meow_media_app_resident("dev.test.bg1"), 0) == 0,
|
||||
"停播后再退出 → should_keep=0(真退 SIGTERM)");
|
||||
kill(kid, SIGKILL); waitpid(kid, NULL, 0);
|
||||
close(sp[0]); close(sp[1]);
|
||||
meow_media_clear("dev.test.bg1");
|
||||
|
||||
/* ---- 2b) keepalive 类(background_keepalive)入槽标记 ---- */
|
||||
{
|
||||
pid_t kk = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.keep", kk, -1, 0, 1) == 0, "bg_add(keepalive=1)");
|
||||
const meow_bg_entry_t *bk = meow_bg_at(0);
|
||||
CHECK(bk && bk->keepalive == 1, "keepalive 类槽 keepalive=1");
|
||||
CHECK(bk && bk->bg_since_ms > 0, "keepalive 类槽 bg_since_ms>0");
|
||||
meow_bg_take("dev.test.keep", NULL, NULL);
|
||||
kill(kk, SIGKILL); waitpid(kk, NULL, 0);
|
||||
CHECK(meow_bg_count() == 0, "keepalive 槽 take 后清槽");
|
||||
}
|
||||
|
||||
/* ---- 3) 后台 App 崩溃照收 ---- */
|
||||
g_bg_dead = 0;
|
||||
kid = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.crash", kid, -1, 0) == 0, "bg_add(crash)");
|
||||
CHECK(meow_bg_add("dev.test.crash", kid, -1, 0, 0) == 0, "bg_add(crash)");
|
||||
kill(kid, SIGKILL);
|
||||
{
|
||||
int ok = pump_bg_until(cond_bg_dead, NULL);
|
||||
@@ -238,40 +264,129 @@ int main(void)
|
||||
/* ---- 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)");
|
||||
CHECK(meow_bg_add("dev.test.card", kid, -1, 1, 0) == 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) 表满 / 重复 ---- */
|
||||
/* ---- 5) 表满:LRU 回收最久未激活的未声明槽 / 全常驻满→NOMEM / 重复 / 参数 ---- */
|
||||
{
|
||||
pid_t k[MEOW_BG_MAX];
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
int i, full = 0;
|
||||
int i;
|
||||
pid_t gextra = 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.lru%d", i);
|
||||
CHECK(meow_bg_add(id, k[i], -1, 0, 0) == 0, "lru 填表(未声明 keepalive=0)");
|
||||
}
|
||||
snprintf(id, sizeof id, "dev.test.full%d", MEOW_BG_MAX);
|
||||
/* 5.1 表满 + 全未声明 → 新增 LRU 回收最旧(lru0,bg_since_ms 最小),新 App 入表成功 */
|
||||
{
|
||||
pid_t extra = spawn_sleeper();
|
||||
full = (meow_bg_add(id, extra, -1, 0) == MEOW_ERR_NOMEM);
|
||||
kill(extra, SIGKILL); waitpid(extra, NULL, 0); /* 未入表,就地清理 */
|
||||
pid_t e2 = spawn_sleeper();
|
||||
gextra = e2;
|
||||
CHECK(meow_bg_add("dev.test.lrunew", e2, -1, 0, 0) == 0,
|
||||
"表满全未声明 → LRU 回收最旧槽并成功新增");
|
||||
CHECK(meow_bg_count() == MEOW_BG_MAX, "回收后仍满 4 槽");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.lrunew") == 1, "新增 App 在后台表");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.lru0") == 0, "最久未激活未声明 lru0 被回收摘表");
|
||||
CHECK(kill(k[0], 0) < 0, "lru0 进程已终止(SIGTERM 真退回收)");
|
||||
}
|
||||
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);
|
||||
/* 5.2 重复 add → EXISTS;pid<=0 → INVAL(lru0 已回收,表内为 lrunew/lru1/lru2/lru3) */
|
||||
CHECK(meow_bg_add("dev.test.lrunew", 999999, -1, 0, 0) == MEOW_ERR_EXISTS, "重复 app_id → EXISTS");
|
||||
CHECK(meow_bg_add("dev.test.lrunew", 0, -1, 0, 0) == MEOW_ERR_INVAL, "pid<=0 → INVAL");
|
||||
/* 5.3 清空剩余(take + 杀进程防泄漏) */
|
||||
for (i = 1; i < MEOW_BG_MAX; i++) {
|
||||
snprintf(id, sizeof id, "dev.test.lru%d", i);
|
||||
meow_bg_take(id, NULL, NULL);
|
||||
kill(k[i], SIGKILL); waitpid(k[i], NULL, 0);
|
||||
}
|
||||
meow_bg_take("dev.test.lrunew", NULL, NULL);
|
||||
kill(gextra, SIGKILL); waitpid(gextra, NULL, 0);
|
||||
CHECK(meow_bg_count() == 0, "清空后 bg 表空(未声明)");
|
||||
}
|
||||
|
||||
/* ---- 5b) 表满 + 全为声明常驻(keepalive=1)→ 拒绝新入 NOMEM(不回收声明常驻,保安全) ---- */
|
||||
{
|
||||
pid_t d[MEOW_BG_MAX];
|
||||
char id[MEOW_APP_ID_MAX];
|
||||
int i;
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
d[i] = spawn_sleeper();
|
||||
snprintf(id, sizeof id, "dev.test.dec%d", i);
|
||||
CHECK(meow_bg_add(id, d[i], -1, 0, 1) == 0, "声明常驻填表(keepalive=1)");
|
||||
}
|
||||
{
|
||||
pid_t e2 = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.decnew", e2, -1, 0, 0) == MEOW_ERR_NOMEM,
|
||||
"全声明常驻满 → 新增 NOMEM(不回退真退,保安全)");
|
||||
kill(e2, SIGKILL); waitpid(e2, NULL, 0); /* 未入表,就地清理 */
|
||||
}
|
||||
for (i = 0; i < MEOW_BG_MAX; i++) {
|
||||
snprintf(id, sizeof id, "dev.test.dec%d", i);
|
||||
meow_bg_take(id, NULL, NULL);
|
||||
kill(d[i], SIGKILL); waitpid(d[i], NULL, 0);
|
||||
}
|
||||
CHECK(meow_bg_count() == 0, "声明常驻清空后 bg 表空");
|
||||
}
|
||||
|
||||
/* ================= 批①+宽限:宽限超时回收 / 声明常驻与媒体活跃不清 ================= */
|
||||
g_ms = 100000; /* 本组从干净时间基准起(后续 svc 段用相对 g_ms,不受影响) */
|
||||
|
||||
/* 5c) 未声明 + 媒体不活跃:grace 内保活;恰等 MEOW_BG_GRACE_MS 不回收(严格 >);超界回收 */
|
||||
{
|
||||
pid_t s1 = spawn_sleeper();
|
||||
g_bg_dead = 0;
|
||||
CHECK(meow_bg_add("dev.test.grace1", s1, -1, 0, 0) == 0, "grace1 入表(未声明 keepalive=0)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS; /* 恰好等于宽限(非严格大于) */
|
||||
meow_bg_tick();
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 1 && g_bg_dead == 0,
|
||||
"grace 边界:恰等 MEOW_BG_GRACE_MS 不回收(严格 > 才回收)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS + 1; /* 超界 1ms */
|
||||
pump_bg_until(cond_bg_dead, NULL);
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.grace1") == 0 && meow_bg_count() == 0,
|
||||
"超 grace 未再启动 → 回收摘表");
|
||||
CHECK(g_bg_dead == 1, "宽限回收触发 on_bg_dead(通知 amsd)+ 进程经 SIGTERM 退出");
|
||||
}
|
||||
|
||||
/* 5d) 声明常驻(keepalive=1)超 grace 不清(无限期) */
|
||||
{
|
||||
pid_t k1 = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.gracekeep", k1, -1, 0, 1) == 0, "gracekeep 入表(keepalive=1)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
|
||||
meow_bg_tick();
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.gracekeep") == 1,
|
||||
"声明常驻 keepalive=1 超 grace 不清(无限期)");
|
||||
meow_bg_take("dev.test.gracekeep", NULL, NULL);
|
||||
kill(k1, SIGKILL); waitpid(k1, NULL, 0);
|
||||
}
|
||||
|
||||
/* 5e) 媒体活跃(META PLAYING,keepalive=0)超 grace 不清(resident 保护) */
|
||||
{
|
||||
int sp2[2];
|
||||
pid_t m1;
|
||||
CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sp2) == 0, "grace media socketpair");
|
||||
{
|
||||
m_meta_t meta;
|
||||
memset(&meta, 0, sizeof meta);
|
||||
snprintf(meta.app_id, sizeof meta.app_id, "dev.test.gracemedia");
|
||||
meta.state = MEOW_MEDIA_PLAYING;
|
||||
meow_media_meta(&meta, sp2[0]);
|
||||
}
|
||||
m1 = spawn_sleeper();
|
||||
CHECK(meow_bg_add("dev.test.gracemedia", m1, -1, 0, 0) == 0,
|
||||
"gracemedia 入表(keepalive=0 但媒体活跃)");
|
||||
g_ms = 100000u + MEOW_BG_GRACE_MS + 100000;
|
||||
meow_bg_tick();
|
||||
CHECK(meow_media_app_resident("dev.test.gracemedia") == 1, "gracemedia media_resident=1");
|
||||
CHECK(meow_bg_is_backgrounded("dev.test.gracemedia") == 1,
|
||||
"媒体活跃超 grace 不清(resident 保护)");
|
||||
meow_bg_take("dev.test.gracemedia", NULL, NULL);
|
||||
kill(m1, SIGKILL); waitpid(m1, NULL, 0);
|
||||
meow_media_clear("dev.test.gracemedia");
|
||||
close(sp2[0]); close(sp2[1]);
|
||||
}
|
||||
CHECK(meow_bg_count() == 0, "take 全部后 bg 表空");
|
||||
|
||||
/* ================= 无头后台服务 ================= */
|
||||
write_test_svc();
|
||||
@@ -356,7 +471,7 @@ int main(void)
|
||||
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);
|
||||
meow_bg_add("dev.test.bg9", bgk, -1, 0, 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,
|
||||
|
||||
@@ -191,6 +191,25 @@ int main(void)
|
||||
CHECK(meow_media_app_playing("com.ncm") == 0, "app_playing: not current source == 0");
|
||||
meow_media_clear(NULL);
|
||||
|
||||
/* 9) 批①后台常驻判定 meow_media_app_resident(launcher 转后台保活判据:会话存在且 state≠STOPPED) */
|
||||
CHECK(meow_media_app_resident("com.ncm") == 0, "resident: no source == 0");
|
||||
fill_meta(&m, "com.ncm", MEOW_MEDIA_PLAYING, 1000, 240000, "Song A", "Artist A");
|
||||
meow_media_meta(&m, sv[0]);
|
||||
CHECK(meow_media_app_resident("com.ncm") == 1, "resident: PLAYING == 1");
|
||||
CHECK(meow_media_app_resident("com.other") == 0, "resident: other app (no session) == 0");
|
||||
fill_meta(&m, "com.ncm", MEOW_MEDIA_PAUSED, 1000, 240000, "Song A", "Artist A");
|
||||
meow_media_meta(&m, sv[0]);
|
||||
CHECK(meow_media_app_resident("com.ncm") == 1, "resident: PAUSED == 1 (暂停也常驻)");
|
||||
fill_meta(&m, "com.ncm", MEOW_MEDIA_BUFFER, 1000, 240000, "Song A", "Artist A");
|
||||
meow_media_meta(&m, sv[0]);
|
||||
CHECK(meow_media_app_resident("com.ncm") == 1, "resident: BUFFER == 1");
|
||||
fill_meta(&m, "com.ncm", MEOW_MEDIA_STOPPED, 0, 0, "Song A", "Artist A");
|
||||
meow_media_meta(&m, sv[0]);
|
||||
CHECK(meow_media_app_resident("com.ncm") == 0, "resident: STOPPED == 0");
|
||||
CHECK(meow_media_app_resident("") == 0, "resident: empty app_id == 0");
|
||||
CHECK(meow_media_app_resident(NULL) == 0, "resident: NULL app_id == 0");
|
||||
meow_media_clear(NULL);
|
||||
|
||||
printf("\n%s (%d fails)\n", g_fails ? "FAILED" : "ALL PASS", g_fails);
|
||||
return g_fails ? 1 : 0;
|
||||
}
|
||||
@@ -287,9 +287,9 @@ int main(void)
|
||||
/* 播放页:左缘右滑 → 回桌面(保留 M6.26) */
|
||||
CHECK(np_gesture_decide(NP_PAGE_PLAY, 8, 100, 80, 5) == NP_GEST_EXIT,
|
||||
"play: edge right-swipe -> exit");
|
||||
/* 播放页:非左缘右滑 → 播放页无上一页,忽略(不误切/不误退) */
|
||||
CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 100, 80, 5) == NP_GEST_NONE,
|
||||
"play: non-edge right-swipe -> none");
|
||||
/* 播放页:任意右滑 → 返回桌面(用户定案「播放页任意右滑回桌面」,含非左缘;np_gesture.h 恒 EXIT) */
|
||||
CHECK(np_gesture_decide(NP_PAGE_PLAY, 200, 100, 80, 5) == NP_GEST_EXIT,
|
||||
"play: non-edge right-swipe -> EXIT");
|
||||
/* 歌词页:左→右滑 → 回播放页(不是回桌面) */
|
||||
CHECK(np_gesture_decide(NP_PAGE_LYRIC, 200, 100, 60, 5) == NP_GEST_PREV,
|
||||
"lyric: right-swipe -> play");
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
# 用法:
|
||||
# ./build_meowpanel.sh # 交叉编译 → patch_v2/meowpanel/meowpanel(mipsel 静态)
|
||||
# ./build_meowpanel.sh host # 宿主编译 → patch_v2/meowpanel/meowpanel_host
|
||||
# 源码:meowpanel.c + mp_jpeg.c(最小 baseline JPEG 编码器,阶段 C 实时画面)。
|
||||
# 源码:src/*.c(模块化拆分,见 mp.h 共享内部头)+ mp_jpeg.c(最小 baseline JPEG 编码器,阶段 C 实时画面)。
|
||||
# 说明:meowpanel.c 为拆分前的单体源码(保留作参考,**不在构建源列表**;构建以 src/ 模块为准)。
|
||||
# 字体库导入/导出走 busybox `gzip -dc | tar` 管道(与 nyai .mpk 同构),零额外源文件。
|
||||
set -e
|
||||
cd "$(dirname "$0")"
|
||||
@@ -17,14 +18,14 @@ I_HDR="$ROOT/patch_v2/meow_launcherd" # meow_ipc.h(帧/m_perf_query_t/m_perf
|
||||
|
||||
if [ "$1" = "host" ]; then
|
||||
HOST_CC="${CC_HOST:-cc}"
|
||||
"$HOST_CC" -Os -Wall -Wextra -I"$I_HDR" meowpanel.c mp_jpeg.c -o meowpanel_host
|
||||
"$HOST_CC" -Os -Wall -Wextra -I. -I"$I_HDR" src/*.c mp_jpeg.c -o meowpanel_host
|
||||
echo "== host build =="
|
||||
ls -la meowpanel_host
|
||||
file meowpanel_host
|
||||
exit 0
|
||||
fi
|
||||
|
||||
"$CC" -Os -Wall -Wextra -static -I"$I_HDR" meowpanel.c mp_jpeg.c -o meowpanel
|
||||
"$CC" -Os -Wall -Wextra -static -I. -I"$I_HDR" src/*.c mp_jpeg.c -o meowpanel
|
||||
echo "== meowpanel (mipsel static) =="
|
||||
ls -la meowpanel
|
||||
md5sum meowpanel
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
/* mp.h — MeowPanel 后端共享内部头(模块化拆分后各 .c 共同包含)。
|
||||
* 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件
|
||||
* (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 services.d 启动。**不进固件**。
|
||||
* 这是拆分后的单一共享内部头:共享常量/类型/外部全局 + 所有跨模块函数原型。
|
||||
* 各 api_* 域 / store / ipc / util / json / server 均以 `#include "mp.h"` 起步。
|
||||
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。 */
|
||||
#ifndef MP_H
|
||||
#define MP_H
|
||||
#define _GNU_SOURCE /* gethostname/strcasestr/strnlen 等 GNU/musl 扩展 */
|
||||
|
||||
/* meowpanel.c — MeowOS Web 面板后端(MeowPanel)。
|
||||
*
|
||||
* 定位(MEOWPANEL_DESIGN §1/§2):外置于 TF 卡 `MeowOS/opt/MeowPanel/` 的系统组件
|
||||
* (HTTP/JSON 后端 + 前端静态托管),由 meow_svcd 读 TF 卡 `MeowOS/services.d/meowpanel.conf`
|
||||
* 启动、meow_sysui 监管。**不进固件**;无卡时系统核心绝不对它有任何依赖。
|
||||
*
|
||||
* 架构(MEOWPANEL_PROTOCOL §1/§2 + SYSTEM_FRAMEWORK_SERVICES §4):
|
||||
* - 单进程单线程 + poll 串行处理(面板访问量极小);内嵌 HTTP server。
|
||||
* - 读侧(/api/status,/api/perf,/api/system)经 meow_svc_perf IPC(MSG_PERF_QUERY→RESULT)
|
||||
* 聚合,**绝不各自爬 /proc**(单一权威源)。
|
||||
* - 动作侧(/api/system/{reboot,shutdown,ota})经 meow_svc_power IPC(MSG_POWER_ACTION→STATUS),
|
||||
* 不再直连 poweroff/写 mtd5。
|
||||
* - 设置镜像(/api/settings*,/api/theme*)直接读写板载 /usr/data/meow/settings.conf——
|
||||
* **与 meow_ui_vol_read / meow_svc_power settings_get_int_trim 同款解析**('=' 切分 +
|
||||
* key 去首尾空白精确匹配;写用 "key = value\n")。后端是独立进程,无法链接 launcher 内的
|
||||
* meow_settings_*(那是 launcher 进程内存表),故直接镜像文件,保持同一存储契约。
|
||||
* - 文件/应用/日志/通知:白名单根 /usr/data/mnt/sd_0 下 MeowOS/Music/MeowData/MeowAPPs/book。
|
||||
*
|
||||
* 编译(mipsel 静态):build_meowpanel.sh;宿主编译 build_meowpanel.sh host。
|
||||
* 依赖:仅 POSIX + libc(含 linux/input.h 内核头),零第三方库(mp_jpeg.c 自研最小 JPEG;
|
||||
* 字体库导入/导出走 busybox `gzip -dc | tar` 管道——与 nyai .mpk 同构,设备必带 tar/gzip)。
|
||||
* 设备无 python3/lua。
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h> /* strcasecmp/strncasecmp */
|
||||
#include <stdarg.h>
|
||||
#include <stdint.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <signal.h>
|
||||
#include <poll.h>
|
||||
#include <time.h>
|
||||
#include <ctype.h>
|
||||
#include <dirent.h>
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <linux/input.h> /* KEY_* / EV_KEY / EV_SYN / struct input_event(按键注入) */
|
||||
|
||||
#include "meow_ipc.h" /* 帧格式/消息号 + m_perf_query_t/m_perf_result_t + POWER_* 枚举 */
|
||||
#include "mp_jpeg.h" /* 实时画面:RGB565 → 最小 baseline JPEG(自包含,零依赖) */
|
||||
|
||||
/* 实时画面 / 触摸注入(阶段 C)。fb ioctl 常量按 launcher fb_compositor 同款手工定义
|
||||
* (避免 libc/kernel struct ABI 差异,读入裸字节缓冲再按偏移取字段)。 */
|
||||
#ifndef FBIOGET_VSCREENINFO
|
||||
#define FBIOGET_VSCREENINFO 0x4600
|
||||
#define FBIOGET_FSCREENINFO 0x4602
|
||||
#endif
|
||||
#ifndef FB_DEV
|
||||
#define FB_DEV "/dev/fb0" /* 16bpp RGB565,480x720,双层虚拟缓冲 */
|
||||
#endif
|
||||
#ifndef TOUCH_DEV
|
||||
#define TOUCH_DEV "/dev/input/event1" /* goodix-ts:接收 write() 注入(本机实测可投递) */
|
||||
#endif
|
||||
|
||||
/* ---------------- 常量 ---------------- */
|
||||
#define PANEL_VERSION "0.1"
|
||||
#define DEFAULT_PORT 8080
|
||||
#define SETTINGS_FILE "/usr/data/meow/settings.conf"
|
||||
#define SETTINGS_HDR "# meow-settings 1"
|
||||
#define TF_ROOT g_tf_root_buf
|
||||
#define TF_CARD_DIR g_tf_card
|
||||
#define MEOWAPPS_DIR g_apps_dir
|
||||
#define LOGS_DIR g_logs_dir
|
||||
#define PERF_SOCK "/usr/data/meow_perf.sock"
|
||||
#define POWER_SOCK "/usr/data/meow_power.sock"
|
||||
#define LAUNCHER_SOCK "/usr/data/meow_ipc.sock" /* launcher 主 IPC(通知活跃集查询/撤清) */
|
||||
#define AUDIO_SOCK "/usr/data/meow_audio.sock" /* meow_audiod 状态快照(MSG_AUDIO_QUERY→STATE) */
|
||||
#define NET_SOCK "/usr/data/meow_net.sock" /* meow_netmgrd 网络管理(MSG_NET_*,期2 观测层/DNS 代理) */
|
||||
#define PM_SOCK "/usr/data/meow_pm.sock" /* meow_powermgr 功耗管理(MSG_PM_QUERY→RESULT) */
|
||||
#define NYAI_PATH "/usr/bin/nyai" /* 固件内置(权威) */
|
||||
#define NYAI_PATH_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/nyai" /* 旧卡上路径(回退) */
|
||||
#define AMS_BIN "/usr/bin/am" /* 固件内置 amsd 用户侧 CLI(权威) */
|
||||
#define AMS_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/am" /* 旧卡上路径(回退) */
|
||||
#define SVCD_BIN "/usr/bin/meow_svcd" /* 固件内置 svcd 服务表 CLI(权威) */
|
||||
#define SVCD_BIN_CARD "/usr/data/mnt/sd_0/MeowOS/opt/bin/meow_svcd" /* 旧卡上路径(回退) */
|
||||
#define BOOT_DEFAULT_FILE "/data/meow_boot_default"
|
||||
#define VERSION_FILE "/etc/meowos_version"
|
||||
#define HOSTNAME_FILE ""
|
||||
#define IPC_TIMEOUT_MS 2000
|
||||
#define MAX_HTTP_HEADER (64 * 1024)
|
||||
#define MAX_HTTP_BODY (16 * 1024 * 1024) /* 通用 body 上限(JSON/小件) */
|
||||
#define MAX_UPLOAD_BODY (8 * 1024 * 1024) /* multipart 上传上限(14MB 内存约束:不整读超 8MB) */
|
||||
#define MAX_FONT_UPLOAD (32 * 1024 * 1024) /* 字体库 tar.gz 上传上限(gbk 字库 png+idx 可能 10-20MB) */
|
||||
#define MAX_JSON_IN 65536 /* JSON body 解析上限 */
|
||||
#define MAX_STATIC_FREEBUF (2 * 1024 * 1024)
|
||||
#define LOG_MAX_LINES 1000
|
||||
#define NOTIF_PREVIEW_MAX 200 /* 通知列表返回上限(每条约 0.2KB,总 <50KB) */
|
||||
#define TEXT_PREVIEW_MAX (1024 * 1024) /* 文本预览上限(超出截断,truncated=true;14MB 约束下 1MB 缓冲安全) */
|
||||
#define PANEL_VOL_MAX_DEFAULT 100 /* 音频音量上限默认(对齐 meow_launcher MEOW_VOL_MAX_DEFAULT;设备可经 settings.conf audio.volume_max 覆盖) */
|
||||
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
|
||||
#define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0]))
|
||||
#define SCREEN_BLOCK 8 /* 比对块边长(像素) */
|
||||
#ifndef MSG_PM_POLICY_GET
|
||||
#define MSG_PM_POLICY_GET 0x00D2 /* ↑ 策略读(空负载 → MSG_PM_RESULT policy JSON;读级任意调用者) */
|
||||
#define MSG_PM_POLICY_SET 0x00D3 /* ↑ 策略写(req: m_pm_policy_set_t;动作级=root+系统身份) */
|
||||
#define MSG_PM_HISTORY 0x00D4 /* ↑ 历史采样查询(req: u32 count,0=默认) */
|
||||
#define MSG_PM_HISTORY_RESULT 0x00D5 /* ↓ 历史 JSON */
|
||||
#define MSG_PM_EVENTS 0x00D6 /* ↑ 事件流查询(req: u32 count,0=默认) */
|
||||
#define MSG_PM_EVENTS_RESULT 0x00D7 /* ↓ 事件 JSON */
|
||||
#define MSG_PM_CALIB_START 0x00D8 /* ↑ 标定启动(动作级;本期面板不暴露,预留) */
|
||||
#define MSG_PM_CALIB_STOP 0x00D9 /* ↑ 标定停止(动作级;预留) */
|
||||
#endif
|
||||
#define AUTH_TOKEN_BYTES 16 /* 128bit → 32 hex */
|
||||
#define AUTH_TOKEN_TTL (3 * 86400) /* 3 天(固定,不滑动续期) */
|
||||
#define AUTH_MAX_SESS 8 /* 内存最多保存的并发会话数 */
|
||||
#define AUTH_HASH_LEN 64 /* sha256 hex 长度 */
|
||||
|
||||
/* ---------------- 有界动态字符串(sbuf) ---------------- */
|
||||
typedef struct { char *b; size_t n; size_t cap; } sbuf_t;
|
||||
|
||||
/* ---------------- 最简 JSON 解析器(无第三方库) ----------------
|
||||
* 只覆盖 MeowPanel 实际用到的:对象 / 数组 / 字符串(含转义) / 数字 / bool / null。
|
||||
* value 树:J_OBJ 的 child 为键值对链(str=key,child=value,next=下个 pair);
|
||||
* J_ARR 的 child 为元素链(next=下个元素)。
|
||||
*/
|
||||
enum { J_NULL, J_BOOL, J_NUM, J_STR, J_ARR, J_OBJ };
|
||||
|
||||
typedef struct jv {
|
||||
int type;
|
||||
int bval; /* J_BOOL */
|
||||
long long inum; /* J_NUM 整数值 */
|
||||
double dnum; /* J_NUM 浮点值 */
|
||||
char *str; /* J_STR: 值; J_OBJ: key */
|
||||
struct jv *child; /* J_ARR: 头; J_OBJ: 头 pair */
|
||||
struct jv *next; /* 链 */
|
||||
} jv_t;
|
||||
|
||||
typedef struct { const char *p; const char *end; int err; } jvp_t;
|
||||
|
||||
enum { ST_INT, ST_STR, ST_ENUM, ST_BOOL };
|
||||
|
||||
typedef struct setdef {
|
||||
const char *key;
|
||||
int type;
|
||||
const char *def;
|
||||
int min, max; /* ST_INT 取值域;max<=0 表示无上界 */
|
||||
const char *enum_names; /* ST_ENUM 逗号名 */
|
||||
const char *enum_labels; /* 对应中文 */
|
||||
const char *group; /* 分组标签(协议 groups 用) */
|
||||
} setdef_t;
|
||||
|
||||
/* 期 3 · 策略写负载(item="profile"/"low-pct"/"crit-pct"/"bri-cap"/"scr-cap")。 */
|
||||
typedef struct m_pm_policy_set { char item[16]; char val[32]; } m_pm_policy_set_t; /* 48 B */
|
||||
|
||||
|
||||
struct fent { char name[256]; int isdir; long long size, mtime; };
|
||||
|
||||
/* ---------------- HTTP 请求读取 ---------------- */
|
||||
typedef struct {
|
||||
char method[8];
|
||||
char path[512]; /* 解码后的路径(不含 query) */
|
||||
char query[1024]; /* 原始 query(key=value&…,未解码;取值时用 qparam) */
|
||||
char *body;
|
||||
size_t body_len;
|
||||
char content_type[128]; /* 请求 Content-Type(若非空) */
|
||||
char authorization[256]; /* Authorization 头(Bearer <token>) */
|
||||
char x_meow_token[256]; /* X-MeowToken 头 */
|
||||
char cookie[512]; /* Cookie 头(meow_token=<token> 等) */
|
||||
} req_t;
|
||||
|
||||
typedef struct { char path[1400]; char filename[320]; } upfile_t;
|
||||
|
||||
typedef struct {
|
||||
long long ms; /* created_ms(epoch ms) */
|
||||
unsigned id;
|
||||
char app_id[40];
|
||||
char app_icon[64];
|
||||
char category[40];
|
||||
int urgency;
|
||||
int progress; /* -1=无 */
|
||||
char summary[96];
|
||||
char body[128];
|
||||
char target[64];
|
||||
int ongoing, media, dismissed;
|
||||
} notif_rec_t;
|
||||
|
||||
typedef struct { uint32_t state[8]; uint64_t count; uint8_t buf[64]; size_t buflen; } mp_sha256_ctx;
|
||||
|
||||
/* ---- 会话(**纯内存**:重启自然失效,无卡上持久化) ---- */
|
||||
typedef struct { char token[65]; time_t expires; } auth_sess_t;
|
||||
|
||||
/* ---------------- 跨模块全局(extern;定义见各拥有模块 .c) ---------------- */
|
||||
extern const char *g_settings_file;
|
||||
extern char g_tf_root_buf[1024];
|
||||
extern char g_tf_card_buf[2048], g_apps_dir_buf[2048], g_logs_dir_buf[2048], g_notif_log_buf[2048];
|
||||
extern const char *g_tf_card, *g_apps_dir, *g_logs_dir, *g_notif_log;
|
||||
extern const char *g_allowed_dirs[];
|
||||
extern const char *g_sensitive_names[];
|
||||
extern const char *g_sock_perf, *g_sock_power, *g_sock_launcher, *g_sock_audio, *g_sock_net, *g_sock_pm;
|
||||
extern time_t g_auth_ttl;
|
||||
|
||||
/* ---------------- 函数原型(全部跨模块;无 static,避免 -Wunused-function) ---------------- */
|
||||
void init_tf_paths(void);
|
||||
int sb_reserve(sbuf_t *s, size_t extra);
|
||||
void sA(sbuf_t *s, const char *fmt, ...);
|
||||
int utf8_seq_len(unsigned char lead);
|
||||
int json_strict_balanced(const char *b, size_t n);
|
||||
int utf8_seq_ok(const char *p, int len);
|
||||
int utf8_valid_n(const char *s, size_t n);
|
||||
int utf8_sanitize(char *dst, const char *src, size_t n);
|
||||
void sA_str_n(sbuf_t *s, const char *v, size_t n);
|
||||
void sA_str(sbuf_t *s, const char *v);
|
||||
void sA_append(sbuf_t *s, const char *b, size_t n);
|
||||
void sbuf_free(sbuf_t *s);
|
||||
jv_t *jv_alloc(int type);
|
||||
void jv_free(jv_t *v);
|
||||
void jvp_skip_ws(jvp_t *t);
|
||||
char *jv_parse_string(jvp_t *t);
|
||||
jv_t *jv_parse_object(jvp_t *t);
|
||||
jv_t *jv_parse_array(jvp_t *t);
|
||||
jv_t *jv_parse_value(jvp_t *t);
|
||||
jv_t *jv_parse(const char *text, int len);
|
||||
jv_t *jv_obj_get(const jv_t *obj, const char *key);
|
||||
void jv_to_json(sbuf_t *s, const jv_t *v);
|
||||
const setdef_t *setdef_find(const char *key);
|
||||
void settings_mkdir_p(void);
|
||||
char *settings_get_value(const char *key);
|
||||
int settings_get_int(const char *key, int defv);
|
||||
int brightness_clamp(int v);
|
||||
int settings_write_one(const char *key, const char *val);
|
||||
int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap);
|
||||
int settings_reset_all(void);
|
||||
int x_read_full(int fd, uint8_t *buf, size_t n);
|
||||
int x_write_full(int fd, const uint8_t *buf, size_t n);
|
||||
int send_frame(int fd, uint16_t type, uint32_t req_id, const void *payload, uint32_t len);
|
||||
int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap);
|
||||
int ipc_connect_tmo(const char *sock, int tmo_ms);
|
||||
int ipc_hello(int fd);
|
||||
const char *perf_code_str(int code);
|
||||
int perf_subset_for_path(const char *path);
|
||||
int ipc_perf_query(int subset, char *out, int cap);
|
||||
const char *power_code_str(int code);
|
||||
int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st);
|
||||
void svc_msg_kv(const char *msg, const char *key, char *out, size_t cap);
|
||||
const char *audio_kv(const char *txt, const char *key, char *out, size_t cap);
|
||||
long long audio_kv_ll(const char *txt, const char *key, long long defv);
|
||||
int audio_vol_max(void);
|
||||
void audio_fill(sbuf_t *s);
|
||||
int api_audio(int sock);
|
||||
int audio_write_cmd(uint16_t type, const void *pl, uint32_t len, m_ack_t *out);
|
||||
int api_audio_route(int sock, const char *body, size_t body_len);
|
||||
int api_audio_volume(int sock, const char *body, size_t body_len);
|
||||
int api_audio_monitor(int sock, const char *query);
|
||||
int ipc_pm_query(uint32_t subset, char *out, int cap);
|
||||
int api_power(int sock);
|
||||
int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen, uint16_t *rtype, char *out, int cap);
|
||||
int api_power_policy_get(int sock);
|
||||
int api_power_policy_set(int sock, const char *body, size_t body_len);
|
||||
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail);
|
||||
int api_power_history(int sock);
|
||||
int api_power_events(int sock);
|
||||
const char *ams_bin(void);
|
||||
const char *svcd_bin(void);
|
||||
int net_ipc(uint16_t req_type, const void *req, uint32_t req_len, uint16_t *ret_type, uint32_t *code, uint16_t *meta, char *out, size_t cap);
|
||||
int api_net_status(int sock);
|
||||
int net_conns_parse_body(sbuf_t *s, const char *txt);
|
||||
int api_net_conns(int sock, const char *query);
|
||||
int net_dns_obj_fill(sbuf_t *s);
|
||||
int api_net_dns(int sock);
|
||||
int net_block_fill(sbuf_t *s);
|
||||
int api_net_dns_flush(int sock);
|
||||
int api_net_log(int sock);
|
||||
int api_net_ifaces(int sock);
|
||||
void net_policy_parse(const char *txt, sbuf_t *s);
|
||||
int api_net_policy_get(int sock);
|
||||
int api_net_policy_set(int sock, const char *body, size_t body_len);
|
||||
void panel_strip_ansi(char *s);
|
||||
int txt_tokens(char *buf, char **toks, int maxtoks);
|
||||
void join_tokens(char **toks, int from, int to, char *out, size_t cap);
|
||||
void ams_fill(sbuf_t *s);
|
||||
void ams_fg_fill(sbuf_t *s);
|
||||
int api_ams(int sock);
|
||||
int ams_pkg_ok(const char *pkg);
|
||||
void ams_history_parse(char *out, sbuf_t *s, const char *pkg);
|
||||
void ams_history_fill(sbuf_t *s, const char *pkg);
|
||||
int api_ams_history(int sock, const char *pkg);
|
||||
int api_ams_ctl(int sock, const char *pkg, int kill);
|
||||
void svcd_fill(sbuf_t *s);
|
||||
int api_svcd(int sock);
|
||||
int api_overview(int sock);
|
||||
int tf_present(void);
|
||||
int path_in_whitelist(const char *abs);
|
||||
int is_sensitive_name(const char *base);
|
||||
int normalize_path(const char *in, char *buf, size_t buflen);
|
||||
void make_rel(const char *abs, char *buf, size_t buflen);
|
||||
int open_safe_for_read(const char *abs, int follow_rel);
|
||||
void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime);
|
||||
int nat_strcmp(const char *a, const char *b);
|
||||
int fent_cmp(const void *a, const void *b);
|
||||
int files_list(int sock, const char *relpath);
|
||||
char *read_text_file_limited(const char *abs, long long *size, int *truncated);
|
||||
int keycode_to_device(int code, const char **dev);
|
||||
int inject_key_event(const char *dev, int code, int down);
|
||||
int inject_key_release(int code, int press_ms);
|
||||
int read_small_file(const char *path, char *buf, int cap);
|
||||
int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail);
|
||||
void req_free(req_t *r);
|
||||
int is_hex(char c);
|
||||
int hexval(char c);
|
||||
void url_decode(const char *in, char *buf, int buflen);
|
||||
int qparam(const char *query, const char *key, char *out, int outcap);
|
||||
int path_segment(const char *prefix, const char *full, char *out, int outcap);
|
||||
void split_path_query(const char *src, char *path, int plc, char *query, int qlc);
|
||||
ptrdiff_t find_header_end(const char *buf, size_t n);
|
||||
int read_request(int sock, req_t *r);
|
||||
void mp_sha256_init(mp_sha256_ctx *c);
|
||||
void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p);
|
||||
void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len);
|
||||
void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32]);
|
||||
int auth_read_random(unsigned char *buf, size_t n);
|
||||
void auth_hex(const unsigned char *raw, size_t n, char *out);
|
||||
int auth_token_gen(char *out, int outcap);
|
||||
int auth_make_salt(char *out, int outcap);
|
||||
int auth_pass_hash(const char *salt, const char *password, char *out, int outcap);
|
||||
int auth_make_hash(const char *password, char *out, int outcap);
|
||||
int hash_matches(const char *stored, const char *password);
|
||||
int pin_set(void);
|
||||
int auth_pass_set(void);
|
||||
int auth_verify(const char *password);
|
||||
int pin_verify(const char *pin);
|
||||
int pin_validate(const char *pin);
|
||||
long long pin_lock_remaining(void);
|
||||
void pin_lock_reset(void);
|
||||
void pin_record_fail(void);
|
||||
int sess_find(const char *token);
|
||||
int sess_valid(const char *token);
|
||||
void sess_add(const char *token, time_t expires);
|
||||
void sess_remove(const char *token);
|
||||
void auth_cp(char *dst, int dcap, const char *src);
|
||||
int extract_token(const req_t *r, char *out, int outcap);
|
||||
int require_auth(int sock, const req_t *r);
|
||||
int api_auth_status(int sock, const req_t *r);
|
||||
int api_auth_login(int sock, const req_t *r);
|
||||
int api_auth_setup(int sock, const req_t *r);
|
||||
int api_auth_logout(int sock, const req_t *r);
|
||||
int write_full(int sock, const void *b, size_t n);
|
||||
int send_head(int sock, int status, const char *ctype, long long clen, const char *extra);
|
||||
int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen);
|
||||
int send_ok(int sock, const char *data);
|
||||
int send_err(int sock, int status, const char *code, const char *msg);
|
||||
int send_json(int sock, int status, const char *data);
|
||||
int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename);
|
||||
const char *mem_find(const void *hay, size_t hlen, const char *needle);
|
||||
int safe_filename(const char *name, char *out, size_t cap);
|
||||
int parse_multipart(const req_t *r, upfile_t *files, int maxfiles);
|
||||
void panel_log(const char *fmt, ...);
|
||||
int str_truthy(const char *v);
|
||||
int int_meta_max(const setdef_t *d);
|
||||
const char *csv_at(const char *s, int idx, char *buf, int buflen);
|
||||
int csv_count(const char *s);
|
||||
void enum_options_json(sbuf_t *s, const char *names, const char *labels);
|
||||
void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val);
|
||||
char *settings_current_value(const setdef_t *d);
|
||||
int api_settings_get_any(int sock, const char *key);
|
||||
int api_settings_get_all(int sock);
|
||||
int api_settings_put(int sock, const char *key, const req_t *r);
|
||||
int api_settings_batch(int sock, const req_t *r);
|
||||
int api_settings_restore(int sock);
|
||||
void theme_json(sbuf_t *s);
|
||||
int api_theme_get(int sock);
|
||||
int api_theme_put(int sock, const req_t *r);
|
||||
int api_perf(int sock, const char *path);
|
||||
int perf_extract_network(const char *json, char *out, int cap);
|
||||
int api_system(int sock);
|
||||
int api_pin_unlock_once(int sock);
|
||||
int api_system_action(int sock, const char *which, const req_t *r);
|
||||
int api_key_single(int sock, int code, const req_t *r);
|
||||
int api_key_batch(int sock, const req_t *r);
|
||||
const char *log_group(const char *name);
|
||||
int api_logs_list(int sock);
|
||||
int api_logs_read(int sock, const req_t *r);
|
||||
int api_logs_crash(int sock);
|
||||
int notif_csv_field(const char **pp, char *out, int outcap);
|
||||
int notif_parse_line(const char *line, notif_rec_t *r);
|
||||
int notif_load_list(notif_rec_t *recs, int maxrec);
|
||||
const char *notif_app_name(const char *app_id);
|
||||
void notif_emit_item(sbuf_t *out, const notif_rec_t *r);
|
||||
int notif_ipc_query(notif_rec_t *recs, int maxrec);
|
||||
int notif_ipc_dismiss(unsigned id);
|
||||
int notif_ipc_dismiss_all(int *out_count);
|
||||
int api_notifications(int sock);
|
||||
int notif_rewrite(int mode, unsigned del_id);
|
||||
int api_notification_delete(int sock, unsigned id);
|
||||
int api_notification_dismiss_all(int sock);
|
||||
int api_files_list(int sock, const req_t *r);
|
||||
int api_files_read(int sock, const req_t *r);
|
||||
int api_files_download(int sock, const req_t *r);
|
||||
int api_files_upload(int sock, const req_t *r);
|
||||
int rm_recursive(const char *path);
|
||||
int api_files_mkdir(int sock, const req_t *r);
|
||||
int api_files_rename(int sock, const req_t *r);
|
||||
int api_files_delete(int sock, const req_t *r);
|
||||
void fonts_root(char *buf, size_t cap);
|
||||
int safe_dir_segment(const char *s, char *out, size_t cap);
|
||||
int unsafe_path(const char *s);
|
||||
int has_dotdot_comp(const char *p);
|
||||
int sanitize_member(const char *in, char *out, int n);
|
||||
int mkdir_p(const char *path);
|
||||
const jv_t *font_json_str(const jv_t *root, const char *key);
|
||||
int api_fonts_list(int sock);
|
||||
int sha256_file_hex(const char *path, char out[65]);
|
||||
int api_fonts_import(int sock, const req_t *r);
|
||||
int api_fonts_export(int sock, const req_t *r);
|
||||
int run_cmd(const char *cmd, char *out, int outcap);
|
||||
int api_apps_list(int sock);
|
||||
int api_apps_info(int sock, const char *pkg);
|
||||
int multipart_extract_file(const req_t *r, const char *outpath);
|
||||
const char *nyai_bin(void);
|
||||
int api_apps_install(int sock, const req_t *r);
|
||||
int api_apps_export(int sock, const char *pkg);
|
||||
int api_apps_remove(int sock, const char *pkg, const req_t *r);
|
||||
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h);
|
||||
int fb_dims(const char *dev, int *w, int *h);
|
||||
void screen_prev_store(const uint8_t *frame, int w, int h);
|
||||
int screen_diff_blocks(const uint8_t *cur, const uint8_t *prev, int w, int h, int *bx, int *by, int *bw, int *bh, int *total_blocks);
|
||||
int api_screen_jpeg(int sock, const req_t *r);
|
||||
int64_t now_mono_ms(void);
|
||||
int64_t touch_last_local_ms(const char *dev);
|
||||
int inject_tap(const char *dev, int x, int y);
|
||||
int api_input_tap(int sock, const req_t *r);
|
||||
const char *web_root(void);
|
||||
const char *mime_for(const char *path);
|
||||
int serve_static(int sock, const char *path);
|
||||
int handle(int sock, req_t *r);
|
||||
int read_port_config(void);
|
||||
void handle_conn(int sock);
|
||||
int panel_selftest(void);
|
||||
int main(int argc, char **argv);
|
||||
|
||||
#endif /* MP_H */
|
||||
@@ -0,0 +1,196 @@
|
||||
/* mp_ams.c — App 运行表域:am list 解析 + amd 历史 + start/kill 控制。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
const char *ams_bin(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_AMS_BIN");
|
||||
if (e && e[0]) return e;
|
||||
if (!access(AMS_BIN, X_OK)) return AMS_BIN;
|
||||
return AMS_BIN_CARD;
|
||||
}
|
||||
|
||||
void ams_fill(sbuf_t *s)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof cmd, "%s list 2>/dev/null", ams_bin());
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || !strstr(out, "APP")) { sA(s, "{\"available\":false}"); return; }
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
sbuf_t apps = {0};
|
||||
int first = 1, count = 0;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "APP ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 9) {
|
||||
/* 布局:APP <pkg> <name...> <state> <pid> <launches> <crashes> <last_kind> <last_code>
|
||||
* tok[0]="APP" 前缀;自右向左固定:state=tok[n-6], pid=n-5, launches=n-4, crashes=n-3,
|
||||
* last_kind=n-2, last_code=n-1;pkg=tok[1],name=join(tok[2..n-6))。 */
|
||||
char nm[128];
|
||||
join_tokens(toks, 2, n - 6, nm, sizeof nm);
|
||||
if (!first) sA(&apps, ",");
|
||||
first = 0; count++;
|
||||
sA(&apps, "{\"pkg\":"); sA_str(&apps, toks[1]);
|
||||
sA(&apps, ",\"name\":"); sA_str(&apps, nm);
|
||||
sA(&apps, ",\"state\":"); sA_str(&apps, toks[n-6]);
|
||||
sA(&apps, ",\"pid\":%d", atoi(toks[n-5]));
|
||||
sA(&apps, ",\"launches\":%d", atoi(toks[n-4]));
|
||||
sA(&apps, ",\"crashes\":%d", atoi(toks[n-3]));
|
||||
sA(&apps, ",\"last_kind\":"); sA_str(&apps, toks[n-2]);
|
||||
sA(&apps, ",\"last_code\":%d}", atoi(toks[n-1]));
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, "{\"available\":true,\"count\":%d,\"apps\":[%s]", count, apps.b ? apps.b : "");
|
||||
sbuf_free(&apps);
|
||||
ams_fg_fill(s); /* 追加 `,"fg":{..},"running":[..]`(首页前台 App 高亮;additive) */
|
||||
sA(s, "}");
|
||||
}
|
||||
|
||||
void ams_fg_fill(sbuf_t *s)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof cmd, "%s status 2>/dev/null", ams_bin());
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || !strstr(out, "RUN")) { sA(s, ",\"fg\":null,\"running\":[]"); return; }
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
char fg_pkg[128] = "", fg_act[160] = ""; int fg_pid = 0;
|
||||
sbuf_t run = {0}; int first = 1;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "RUN ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 6) { /* RUN pkg state pid dur activity... */
|
||||
char act[160];
|
||||
join_tokens(toks, 5, n, act, sizeof act);
|
||||
const char *st = toks[2];
|
||||
if (!first) sA(&run, ",");
|
||||
first = 0;
|
||||
sA(&run, "{\"pkg\":"); sA_str(&run, toks[1]);
|
||||
sA(&run, ",\"state\":"); sA_str(&run, st);
|
||||
sA(&run, ",\"pid\":%d", atoi(toks[3]));
|
||||
sA(&run, ",\"activity\":"); sA_str(&run, act);
|
||||
sA(&run, "}");
|
||||
if (strcmp(st, "fg") == 0 && !fg_pkg[0]) {
|
||||
snprintf(fg_pkg, sizeof fg_pkg, "%s", toks[1]);
|
||||
snprintf(fg_act, sizeof fg_act, "%s", act);
|
||||
fg_pid = atoi(toks[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, ",\"fg\":");
|
||||
if (fg_pkg[0]) { sA(s, "{\"pkg\":"); sA_str(s, fg_pkg); sA(s, ",\"activity\":"); sA_str(s, fg_act); sA(s, ",\"pid\":%d}", fg_pid); }
|
||||
else sA(s, "null");
|
||||
sA(s, ",\"running\":[%s]", run.b ? run.b : "");
|
||||
sbuf_free(&run);
|
||||
}
|
||||
|
||||
int api_ams(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
ams_fill(&s);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int ams_pkg_ok(const char *pkg)
|
||||
{
|
||||
if (!pkg || !pkg[0]) return 0;
|
||||
if (strlen(pkg) >= MEOW_APP_ID_MAX) return 0;
|
||||
for (const char *p = pkg; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
if (c == '/' || c == ':' || c == ' ' || c == '\t' || c == '\n' || c == '\r') return 0;
|
||||
if (c < 0x21 || c > 0x7e) return 0; /* 包名仅 ASCII 可见字符 */
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ams_history_parse(char *out, sbuf_t *s, const char *pkg) /* out 被 panel_strip_ansi 就地改 */
|
||||
{
|
||||
if (!utf8_valid_n(out, strlen(out))) { sA(s, "{\"available\":false}"); return; } /* 崩溃 detail 坏字节 → 降级 */
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
sbuf_t ev = {0};
|
||||
int first = 1, count = 0;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
if (strncmp(ln, "EVT ", 4) == 0) {
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
if (n >= 5) { /* EVT epoch up kind pkg detail... */
|
||||
char det[256];
|
||||
join_tokens(toks, 5, n, det, sizeof det);
|
||||
if (!first) sA(&ev, ",");
|
||||
first = 0; count++;
|
||||
sA(&ev, "{\"ts\":%ld,\"uptime_sec\":%ld,\"kind\":", atol(toks[1]), atol(toks[2]));
|
||||
sA_str(&ev, toks[3]);
|
||||
sA(&ev, ",\"detail\":"); sA_str(&ev, det);
|
||||
sA(&ev, "}");
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, "{\"available\":true,\"pkg\":"); sA_str(s, pkg);
|
||||
sA(s, ",\"count\":%d,\"events\":[%s]}", count, ev.b ? ev.b : "");
|
||||
sbuf_free(&ev);
|
||||
}
|
||||
|
||||
void ams_history_fill(sbuf_t *s, const char *pkg)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[320];
|
||||
snprintf(cmd, sizeof cmd, "%s history %s 2>/dev/null", ams_bin(), pkg);
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || !strstr(out, "EVT")) { sA(s, "{\"available\":false}"); return; }
|
||||
ams_history_parse(out, s, pkg);
|
||||
}
|
||||
|
||||
int api_ams_history(int sock, const char *pkg)
|
||||
{
|
||||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
sbuf_t s = {0};
|
||||
ams_history_fill(&s, pkg);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_ams_ctl(int sock, const char *pkg, int kill)
|
||||
{
|
||||
if (!ams_pkg_ok(pkg)) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
char out[256];
|
||||
char cmd[320];
|
||||
snprintf(cmd, sizeof cmd, "%s %s %s 2>/dev/null", ams_bin(), kill ? "kill" : "start", pkg);
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code == 0 && strstr(out, "OK ")) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"op\":\"%s\",\"pkg\":", kill ? "kill" : "start");
|
||||
sA_str(&s, pkg);
|
||||
sA(&s, ",\"message\":"); sA_str(&s, out);
|
||||
sA(&s, "}");
|
||||
int rc2 = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc2;
|
||||
}
|
||||
/* am CLI 以文本报告失败(ERR ...);按内容映射 HTTP 码(如实,绝不掩盖)。 */
|
||||
const char *detail = out[0] ? out : (kill ? "am kill 失败" : "am start 失败");
|
||||
if (strstr(detail, "not registered")) return send_err(sock, 404, "NOT_FOUND", detail);
|
||||
if (strstr(detail, "busy")) return send_err(sock, 409, "CONFLICT", detail);
|
||||
if (strstr(detail, "not running")) return send_err(sock, 409, "CONFLICT", detail);
|
||||
if (strstr(detail, "launcher offline")) return send_err(sock, 503, "AMS_UNAVAILABLE", "amsd/launcher 不可达");
|
||||
return send_err(sock, 500, "INTERNAL", detail);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
/* mp_apps.c — 应用域:MeowAPPs 扫描 + nyai 安装/导出/移除。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
int apps_scan_dir(const char *appdir, const char *name, sbuf_t *out, int want_detail)
|
||||
{
|
||||
char pj[1024];
|
||||
snprintf(pj, sizeof pj, "%s/%s/app.json", appdir, name);
|
||||
char buf[8192];
|
||||
if (read_small_file(pj, buf, sizeof buf) <= 0) {
|
||||
/* 解析失败项:invalid */
|
||||
if (!want_detail) {
|
||||
sA(out, "{\"package\":");
|
||||
sA_str(out, name);
|
||||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
jv_t *root = jv_parse(buf, (int)strlen(buf));
|
||||
if (!root) {
|
||||
if (!want_detail) {
|
||||
sA(out, "{\"package\":");
|
||||
sA_str(out, name);
|
||||
sA(out, ",\"name\":null,\"version\":null,\"size\":0,\"installed\":true,\"invalid\":true}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
jv_t *jpkg = jv_obj_get(root, "package");
|
||||
jv_t *jname = jv_obj_get(root, "name");
|
||||
jv_t *jver = jv_obj_get(root, "version");
|
||||
jv_t *jexec = jv_obj_get(root, "exec");
|
||||
jv_t *jfw = jv_obj_get(root, "framework");
|
||||
jv_t *jim = jv_obj_get(root, "immersive");
|
||||
jv_t *jfull = jv_obj_get(root, "fullscreen");
|
||||
const char *pkg = (jpkg && jpkg->type == J_STR) ? jpkg->str : name;
|
||||
const char *nm = (jname && jname->type == J_STR) ? jname->str : "";
|
||||
const char *ver = (jver && jver->type == J_STR) ? jver->str : "";
|
||||
const char *exec = (jexec && jexec->type == J_STR) ? jexec->str : "";
|
||||
/* 目录大小(递归统计,限 8 层防爆) -- 简化:stat 直接子项合计 */
|
||||
long long dsize = 0;
|
||||
snprintf(pj, sizeof pj, "%s/%s", appdir, name);
|
||||
DIR *d = opendir(pj);
|
||||
if (d) {
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s/%s", appdir, name, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) == 0) dsize += (long long)st.st_size;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
if (!want_detail) {
|
||||
sA(out, "{\"package\":");
|
||||
sA_str(out, pkg);
|
||||
sA(out, ",\"name\":");
|
||||
sA_str(out, nm[0] ? nm : NULL);
|
||||
sA(out, ",\"version\":");
|
||||
sA_str(out, ver[0] ? ver : NULL);
|
||||
sA(out, ",\"exec\":");
|
||||
sA_str(out, exec);
|
||||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"size\":%lld,\"installed\":true,\"mtime\":%lld}",
|
||||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, dsize, (long long)time(NULL));
|
||||
} else {
|
||||
sA(out, "{\"package\":");
|
||||
sA_str(out, pkg);
|
||||
sA(out, ",\"name\":");
|
||||
sA_str(out, nm[0] ? nm : NULL);
|
||||
sA(out, ",\"version\":");
|
||||
sA_str(out, ver[0] ? ver : NULL);
|
||||
sA(out, ",\"exec\":");
|
||||
sA_str(out, exec);
|
||||
sA(out, ",\"framework\":%lld,\"immersive\":%lld,\"fullscreen\":%lld,\"size\":%lld,\"path\":",
|
||||
jfw ? jfw->inum : 0, jim ? jim->inum : 0, jfull ? jfull->inum : 0, dsize);
|
||||
sA_str(out, pj);
|
||||
sA(out, ",\"routes\":{},\"services\":[],\"depends\":[],\"description\":\"\"}");
|
||||
}
|
||||
jv_free(root);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_apps_list(int sock)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
DIR *d = opendir(MEOWAPPS_DIR);
|
||||
if (!d) return send_err(sock, 404, "NOT_FOUND", "MeowAPPs 目录不存在");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"count\":0,\"apps\":[");
|
||||
int first = 1, count = 0;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
if (!strcmp(e->d_name, "fonts") || !strcmp(e->d_name, "logs") || !strcmp(e->d_name, "backgrounds")) continue;
|
||||
if (strstr(e->d_name, ".bak")) continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", MEOWAPPS_DIR, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) < 0 || !S_ISDIR(st.st_mode)) continue;
|
||||
if (!first) sA(&s, ",");
|
||||
first = 0;
|
||||
apps_scan_dir(MEOWAPPS_DIR, e->d_name, &s, 0);
|
||||
count++;
|
||||
}
|
||||
closedir(d);
|
||||
sA(&s, "]}");
|
||||
/* count 前端不强制,但协议要求;用两次扫描不可取,故以 "count" 占位并回填:改为在 s 中替换 */
|
||||
/* 简化:直接输出 apps 数组,前端只读 d.apps(count 忽略)。 */
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
(void)count;
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_apps_info(int sock, const char *pkg)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
char appdir[1024];
|
||||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, pkg);
|
||||
struct stat st;
|
||||
if (stat(appdir, &st) < 0 || !S_ISDIR(st.st_mode)) return send_err(sock, 404, "NOT_FOUND", "应用未安装");
|
||||
sbuf_t s = {0};
|
||||
if (apps_scan_dir(MEOWAPPS_DIR, pkg, &s, 1) < 0)
|
||||
return send_err(sock, 404, "NOT_FOUND", "app.json 解析失败");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
const char *nyai_bin(void)
|
||||
{
|
||||
/* nyai 已进固件(/usr/bin,权威);旧版固件回退卡上 MeowOS/opt/bin */
|
||||
if (!access(NYAI_PATH, X_OK)) return NYAI_PATH;
|
||||
return NYAI_PATH_CARD;
|
||||
}
|
||||
|
||||
int api_apps_install(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (access(nyai_bin(), X_OK) < 0) return send_err(sock, 500, "INTERNAL", "nyai 不可用(固件与卡上均无)");
|
||||
char mpkpath[1024];
|
||||
int ispath = 0;
|
||||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0) {
|
||||
/* JSON {path} */
|
||||
if (r->body && r->body[0]) {
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *pj = jv_obj_get(root, "path");
|
||||
if (pj && pj->type == J_STR) {
|
||||
snprintf(mpkpath, sizeof mpkpath, "%s", pj->str);
|
||||
ispath = 1;
|
||||
}
|
||||
jv_free(root);
|
||||
}
|
||||
if (!ispath) return send_err(sock, 400, "BAD_REQUEST", "缺 path 或 multipart file");
|
||||
} else {
|
||||
/* 保存上传 file 到临时文件 */
|
||||
snprintf(mpkpath, sizeof mpkpath, "%s/tmp", TF_CARD_DIR);
|
||||
mkdir(mpkpath, 0777);
|
||||
char tmp[1024];
|
||||
snprintf(tmp, sizeof tmp, "%s/tmp/upload.mpk", TF_CARD_DIR);
|
||||
if (multipart_extract_file(r, tmp) < 0) return send_err(sock, 400, "BAD_REQUEST", "上传解析失败");
|
||||
snprintf(mpkpath, sizeof mpkpath, "%s", tmp);
|
||||
}
|
||||
if (access(mpkpath, R_OK) < 0) return send_err(sock, 404, "NOT_FOUND", ".mpk 文件不存在");
|
||||
char cmd[1400];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' install '%s' 2>&1", nyai_bin(), mpkpath);
|
||||
char out[4096];
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0) return send_err(sock, 422, "UNSUPPORTED", out[0] ? out : "nyai install 失败");
|
||||
/* 包名 = 文件名茎;版本从 app.json 读 */
|
||||
char stem[256];
|
||||
const char *base = strrchr(mpkpath, '/');
|
||||
base = base ? base + 1 : mpkpath;
|
||||
snprintf(stem, sizeof stem, "%.*s", (int)(sizeof stem - 1), base);
|
||||
char *dot = strrchr(stem, '.');
|
||||
if (dot) *dot = 0;
|
||||
char appdir[1024], pj[1100];
|
||||
snprintf(appdir, sizeof appdir, "%s/%s", MEOWAPPS_DIR, stem);
|
||||
jv_t *approot = NULL;
|
||||
char version[16] = "";
|
||||
int upgraded = 0;
|
||||
snprintf(pj, sizeof pj, "%s/app.json", appdir);
|
||||
char jbuf[8192];
|
||||
if (access(pj, R_OK) == 0 && read_small_file(pj, jbuf, sizeof jbuf) > 0)
|
||||
approot = jv_parse(jbuf, (int)strlen(jbuf));
|
||||
if (approot) {
|
||||
jv_t *v = jv_obj_get(approot, "version");
|
||||
if (v && v->type == J_STR) snprintf(version, sizeof version, "%s", v->str);
|
||||
jv_free(approot);
|
||||
}
|
||||
if (access(mpkpath, R_OK) == 0 && ispath && strstr(out, "upgrad")) upgraded = 1;
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"package\":");
|
||||
sA_str(&s, stem);
|
||||
sA(&s, ",\"version\":");
|
||||
sA_str(&s, version[0] ? version : NULL);
|
||||
sA(&s, ",\"installed\":true,\"upgraded\":%s,\"message\":\"已安装\"}", upgraded ? "true" : "false");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_apps_export(int sock, const char *pkg)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
char tmp[1100];
|
||||
char card_tmp[1200];
|
||||
snprintf(card_tmp, sizeof card_tmp, "%s/tmp", TF_CARD_DIR);
|
||||
mkdir(card_tmp, 0777);
|
||||
snprintf(tmp, sizeof tmp, "%s/tmp/%s.mpk", TF_CARD_DIR, pkg);
|
||||
unlink(tmp);
|
||||
char cmd[1400];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' export '%s' '%s' 2>&1", nyai_bin(), pkg, tmp);
|
||||
char out[1024];
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0 || access(tmp, R_OK) < 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai export 失败");
|
||||
char dn[256];
|
||||
snprintf(dn, sizeof dn, "%s.mpk", pkg);
|
||||
return send_file(sock, 200, "application/octet-stream", tmp, dn);
|
||||
}
|
||||
|
||||
int api_apps_remove(int sock, const char *pkg, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (strchr(pkg, '/') || strchr(pkg, ':')) return send_err(sock, 400, "BAD_REQUEST", "包名非法");
|
||||
char purgebuf[8] = "0";
|
||||
if (r->query[0]) qparam(r->query, "purge", purgebuf, sizeof purgebuf);
|
||||
int use_purge = atoi(purgebuf);
|
||||
char cmd[1400];
|
||||
snprintf(cmd, sizeof cmd, "PATH=/bin:/sbin:/usr/bin:/usr/sbin '%s' remove '%s'%s 2>&1", nyai_bin(), pkg, use_purge ? " --purge" : "");
|
||||
char out[1024];
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0) return send_err(sock, 500, "INTERNAL", out[0] ? out : "nyai remove 失败");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"removed\":true,\"purged\":%s}", use_purge ? "true" : "false");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/* mp_audio.c — 音频域:meow_audiod IPC(状态快照 + 写命令 route/volume)。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
const char *audio_kv(const char *txt, const char *key, char *out, size_t cap)
|
||||
{
|
||||
size_t kl = strlen(key);
|
||||
const char *p = txt;
|
||||
while (p && *p) {
|
||||
if (!strncmp(p, key, kl) && p[kl] == '=') {
|
||||
p += kl + 1;
|
||||
size_t n = strcspn(p, " \t\n");
|
||||
if (n >= cap) n = cap - 1;
|
||||
memcpy(out, p, n);
|
||||
out[n] = 0;
|
||||
return out;
|
||||
}
|
||||
p = strchr(p, ' ');
|
||||
if (p) p++;
|
||||
}
|
||||
out[0] = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
long long audio_kv_ll(const char *txt, const char *key, long long defv)
|
||||
{
|
||||
char b[64];
|
||||
if (!audio_kv(txt, key, b, sizeof b) || !b[0]) return defv;
|
||||
return strtoll(b, NULL, 10);
|
||||
}
|
||||
|
||||
int audio_vol_max(void)
|
||||
{
|
||||
int v = settings_get_int("audio.volume_max", PANEL_VOL_MAX_DEFAULT);
|
||||
if (v < 0) v = 0;
|
||||
if (v > 100) v = 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
void audio_fill(sbuf_t *s)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) { sA(s, "{\"available\":false}"); return; }
|
||||
uint8_t buf[MEOW_IPC_MAX];
|
||||
uint16_t type = 0; uint32_t rid = 0;
|
||||
int n = -1;
|
||||
if (send_frame(fd, MSG_AUDIO_QUERY, 5, NULL, 0) == 0)
|
||||
n = recv_frame(fd, &type, &rid, buf, sizeof buf - 1);
|
||||
close(fd);
|
||||
if (n <= 0 || type != MSG_AUDIO_STATE) { sA(s, "{\"available\":false}"); return; }
|
||||
buf[n] = 0;
|
||||
const char *txt = (const char *)buf;
|
||||
char app[MEOW_APP_ID_MAX + 8], fmt[32], rmode[16], target[16], volctl[16], codec[32];
|
||||
audio_kv(txt, "app", app, sizeof app);
|
||||
audio_kv(txt, "fmt", fmt, sizeof fmt);
|
||||
audio_kv(txt, "route_mode", rmode, sizeof rmode);
|
||||
/* 输出目标/音量控制/蓝牙编解码器(M6.32 面板透传:audiod 已发 target/volctl/codec,
|
||||
* 前端蓝牙卡据此点亮「输出目标 DAC|蓝牙 / 音量控制 avrcp|dac / 编解码器」)。缺失时前端优雅降级「—」。 */
|
||||
audio_kv(txt, "target", target, sizeof target);
|
||||
audio_kv(txt, "volctl", volctl, sizeof volctl);
|
||||
audio_kv(txt, "codec", codec, sizeof codec);
|
||||
sA(s, "{\"available\":true,\"active\":%lld,\"app\":", audio_kv_ll(txt, "active", 0));
|
||||
sA_str(s, app);
|
||||
sA(s, ",\"sid\":%lld,\"rate\":%lld,\"ch\":%lld,\"fmt\":",
|
||||
audio_kv_ll(txt, "sid", 0), audio_kv_ll(txt, "rate", 0), audio_kv_ll(txt, "ch", 0));
|
||||
sA_str(s, fmt);
|
||||
sA(s, ",\"written_frames\":%lld,\"played_frames\":%lld,\"underrun\":%lld,\"paused\":%lld,\"route_mode\":",
|
||||
audio_kv_ll(txt, "written", 0), audio_kv_ll(txt, "played", 0),
|
||||
audio_kv_ll(txt, "underrun", 0), audio_kv_ll(txt, "paused", 0));
|
||||
sA_str(s, rmode);
|
||||
sA(s, ",\"port\":%lld,\"vol\":%lld",
|
||||
audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1));
|
||||
sA(s, ",\"target\":");
|
||||
sA_str(s, target);
|
||||
sA(s, ",\"volctl\":");
|
||||
sA_str(s, volctl);
|
||||
sA(s, ",\"codec\":");
|
||||
sA_str(s, codec);
|
||||
/* 电平监控(261):rms/peak deci-dB(-990=-inf),lvlseq=快照代际;透传时别漏(ABI 追加键)。 */
|
||||
sA(s, ",\"rms\":%lld,\"peak\":%lld,\"lvlseq\":%lld,\"vol_max\":%d}",
|
||||
audio_kv_ll(txt, "rms", -990), audio_kv_ll(txt, "peak", -990), audio_kv_ll(txt, "lvlseq", 0), audio_vol_max());
|
||||
}
|
||||
|
||||
int api_audio(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
audio_fill(&s);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int audio_write_cmd(uint16_t type, const void *pl, uint32_t len, m_ack_t *out)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_audio, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return -1;
|
||||
uint8_t buf[64];
|
||||
uint16_t rtype = 0; uint32_t rid = 0;
|
||||
int n = -1;
|
||||
if (send_frame(fd, type, 0x22, pl, len) == 0)
|
||||
n = recv_frame(fd, &rtype, &rid, buf, sizeof buf);
|
||||
close(fd);
|
||||
if (n < (int)sizeof(m_ack_t) || rtype != MSG_ACK) return -1;
|
||||
if (out) memcpy(out, buf, sizeof *out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_audio_route(int sock, const char *body, size_t body_len)
|
||||
{
|
||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(body, (int)body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jp = jv_obj_get(root, "port");
|
||||
if (!jp || jp->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 port 数字"); }
|
||||
long port = (long)jp->inum;
|
||||
jv_free(root);
|
||||
if (port != 0 && port != 2 && port != 3)
|
||||
return send_err(sock, 422, "UNSUPPORTED", "非法路由口(仅允许 0=auto/2=3.5/3=4.4;4=SPDIF、5=audiod 内部值禁止)");
|
||||
m_audio_route_t a; a.port = (uint32_t)port;
|
||||
m_ack_t st;
|
||||
if (audio_write_cmd(MSG_AUDIO_ROUTE, &a, sizeof a, &st) < 0)
|
||||
return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"port\":%ld}", port);
|
||||
int out = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return out;
|
||||
}
|
||||
|
||||
int api_audio_volume(int sock, const char *body, size_t body_len)
|
||||
{
|
||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(body, (int)body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jv = jv_obj_get(root, "value");
|
||||
if (!jv || jv->type != J_NUM) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "缺 value 数字"); }
|
||||
long v = (long)jv->inum;
|
||||
jv_free(root);
|
||||
int vmax = audio_vol_max();
|
||||
if (v < 0) v = 0;
|
||||
if (v > vmax) v = vmax; /* 关键 clamp 红线 */
|
||||
m_audio_volume_t a; a.pct = (int32_t)v;
|
||||
m_ack_t st;
|
||||
if (audio_write_cmd(MSG_AUDIO_VOLUME, &a, sizeof a, &st) < 0)
|
||||
return send_err(sock, 503, "AUDIO_UNAVAILABLE", "音频服务不可用(audiod 未运行/IPC 不可达)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"value\":%ld,\"applied\":%u,\"vol_max\":%d}", v, st.status, vmax);
|
||||
int out = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* ---- /api/audio/monitor(A3 面板音频页增强):读 audiod「全面监控日志」最新
|
||||
* audio_monitor_<bootts>.log 尾部 N 行(默认 200,?lines=N 覆盖)。监控日志由 audiod 主线程 1s
|
||||
* 节拍写(流活跃才写):时间戳+app+参数+rms/peak dBFS+帧计数+underrun/stall/dropped/txdrop+
|
||||
* 路由口+FPGA+音量%+hwrate+dsd 标记(见 meow_audiod.c mon_tick)。面板只读展示,写口一律不开放
|
||||
* (A3 定案);audiod 不写/无卡/无文件 → {"available":false}(HTTP 200,不报错)。
|
||||
* 复用 mp_logs 的「找最新 <前缀> 文件 + 尾读」模式,分页=尾部 N 行(前端滚动翻页)。 ---- */
|
||||
#define AUD_MON_TAIL_MAX (256 * 1024) /* 监控日志单次读取上限(内存约束,不整读大文件) */
|
||||
int api_audio_monitor(int sock, const char *query)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
int maxlines = 200;
|
||||
char ql[16];
|
||||
if (query && query[0] && qparam(query, "lines", ql, sizeof ql)) {
|
||||
int n = atoi(ql);
|
||||
if (n > 0) maxlines = n;
|
||||
if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES;
|
||||
}
|
||||
DIR *d = opendir(LOGS_DIR);
|
||||
if (!d) return send_json(sock, 200, "{\"available\":false}");
|
||||
char best[512] = ""; long best_mt = -1;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (strncmp(e->d_name, "audio_monitor_", 14) != 0) continue;
|
||||
if (e->d_name[0] == '.') continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||||
if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
|
||||
}
|
||||
closedir(d);
|
||||
if (!best[0]) return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
|
||||
int fd = open(full, O_RDONLY);
|
||||
struct stat st;
|
||||
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
|
||||
if (fd >= 0) close(fd);
|
||||
return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||||
}
|
||||
off_t off = st.st_size > AUD_MON_TAIL_MAX ? st.st_size - AUD_MON_TAIL_MAX : 0;
|
||||
size_t buflen = (size_t)(st.st_size - off);
|
||||
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
|
||||
if (off) (void)lseek(fd, off, SEEK_SET);
|
||||
char *buf = malloc(buflen + 1);
|
||||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||||
ssize_t n = read(fd, buf, buflen);
|
||||
close(fd);
|
||||
if (n <= 0) { free(buf); return send_json(sock, 200, "{\"available\":false,\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"available\":true,\"file\":");
|
||||
sA_str(&s, best);
|
||||
sA(&s, ",\"lines\":[");
|
||||
const char *p = buf, *end = buf + n;
|
||||
int linecnt = 0;
|
||||
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
|
||||
if (linecnt > 0 && end[-1] != '\n') linecnt++;
|
||||
int startrow = linecnt - maxlines;
|
||||
if (startrow < 0) startrow = 0;
|
||||
int truncated = (off > 0 || startrow > 0) ? 1 : 0;
|
||||
int rowidx = 0, gotlines = 0;
|
||||
p = buf;
|
||||
while (p < end) {
|
||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char *line_end = nl ? nl : end;
|
||||
if (rowidx >= startrow) {
|
||||
if (gotlines) sA(&s, ",");
|
||||
sA_str_n(&s, p, (size_t)(line_end - p)); /* 转义 + UTF-8 消毒 */
|
||||
gotlines++;
|
||||
}
|
||||
rowidx++;
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
free(buf);
|
||||
sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
/* mp_auth.c — 访问级 PIN 鉴权(panel.pin)+ 内联 SHA-256 + 内存会话。 */
|
||||
#include "mp.h"
|
||||
|
||||
time_t g_auth_ttl = AUTH_TOKEN_TTL;
|
||||
#define PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
|
||||
#define PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
|
||||
static const uint32_t mp_sha256_k[64] = {
|
||||
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
|
||||
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
|
||||
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
|
||||
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
|
||||
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
|
||||
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
|
||||
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
|
||||
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
|
||||
};
|
||||
static int g_pin_fails = 0;
|
||||
static time_t g_pin_locked_until = 0;
|
||||
static auth_sess_t g_sess[AUTH_MAX_SESS];
|
||||
static int g_sess_n = 0;
|
||||
#define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||||
#define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
|
||||
#define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||||
#define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22))
|
||||
#define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25))
|
||||
#define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3))
|
||||
#define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10))
|
||||
|
||||
|
||||
void mp_sha256_init(mp_sha256_ctx *c)
|
||||
{
|
||||
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
|
||||
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
|
||||
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
|
||||
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
|
||||
c->count = 0; c->buflen = 0;
|
||||
}
|
||||
|
||||
void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p)
|
||||
{
|
||||
uint32_t w[64];
|
||||
int i;
|
||||
for (i = 0; i < 16; i++)
|
||||
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
|
||||
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
|
||||
for (i = 16; i < 64; i++)
|
||||
w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16];
|
||||
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
|
||||
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
|
||||
for (i = 0; i < 64; i++) {
|
||||
uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i];
|
||||
uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc);
|
||||
h = g; g = f; f = e; e = d + t1;
|
||||
d = cc; cc = b; b = a; a = t1 + t2;
|
||||
}
|
||||
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
|
||||
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
|
||||
}
|
||||
|
||||
void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len)
|
||||
{
|
||||
c->count += (uint64_t)len;
|
||||
if (c->buflen) {
|
||||
size_t need = 64 - c->buflen;
|
||||
size_t take = len < need ? len : need;
|
||||
memcpy(c->buf + c->buflen, data, take);
|
||||
c->buflen += take; data += take; len -= take;
|
||||
if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; }
|
||||
}
|
||||
while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; }
|
||||
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
|
||||
}
|
||||
|
||||
void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32])
|
||||
{
|
||||
uint64_t bitlen = c->count * 8;
|
||||
uint8_t *b = c->buf;
|
||||
size_t bl = c->buflen;
|
||||
int i;
|
||||
b[bl++] = 0x80;
|
||||
if (bl > 56) {
|
||||
while (bl < 64) b[bl++] = 0;
|
||||
mp_sha256_block(c, b);
|
||||
bl = 0;
|
||||
}
|
||||
while (bl < 56) b[bl++] = 0;
|
||||
for (i = 0; i < 8; i++)
|
||||
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
|
||||
mp_sha256_block(c, b);
|
||||
for (i = 0; i < 8; i++) {
|
||||
out[i*4] = (uint8_t)(c->state[i] >> 24);
|
||||
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
|
||||
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
|
||||
out[i*4+3] = (uint8_t)(c->state[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int auth_read_random(unsigned char *buf, size_t n)
|
||||
{
|
||||
int fd = open("/dev/urandom", O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
size_t got = 0;
|
||||
while (got < n) {
|
||||
ssize_t rd = read(fd, buf + got, n - got);
|
||||
if (rd <= 0) { close(fd); return -1; }
|
||||
got += (size_t)rd;
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void auth_hex(const unsigned char *raw, size_t n, char *out)
|
||||
{
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
out[i*2] = hex[raw[i] >> 4];
|
||||
out[i*2+1] = hex[raw[i] & 0xf];
|
||||
}
|
||||
out[n*2] = 0;
|
||||
}
|
||||
|
||||
int auth_token_gen(char *out, int outcap) /* 16B → 32 hex(128bit) */
|
||||
{
|
||||
unsigned char raw[AUTH_TOKEN_BYTES];
|
||||
if (outcap < AUTH_TOKEN_BYTES * 2 + 1) return -1;
|
||||
if (auth_read_random(raw, sizeof raw) < 0) return -1;
|
||||
auth_hex(raw, sizeof raw, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int auth_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
|
||||
{
|
||||
unsigned char raw[16];
|
||||
if (outcap < 33) return -1;
|
||||
if (auth_read_random(raw, sizeof raw) < 0) return -1;
|
||||
auth_hex(raw, sizeof raw, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int auth_pass_hash(const char *salt, const char *password, char *out, int outcap)
|
||||
{
|
||||
mp_sha256_ctx c;
|
||||
uint8_t digest[32];
|
||||
if (outcap < AUTH_HASH_LEN + 1) return -1;
|
||||
mp_sha256_init(&c);
|
||||
mp_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
|
||||
mp_sha256_update(&c, (const uint8_t *)password, strlen(password));
|
||||
mp_sha256_final(&c, digest);
|
||||
auth_hex(digest, sizeof digest, out);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int auth_make_hash(const char *password, char *out, int outcap) /* salt:hex */
|
||||
{
|
||||
char salt[33], h[AUTH_HASH_LEN + 1];
|
||||
if (auth_make_salt(salt, sizeof salt) < 0) return -1;
|
||||
if (auth_pass_hash(salt, password, h, sizeof h) < 0) return -1;
|
||||
snprintf(out, outcap, "%s:%s", salt, h);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hash_matches(const char *stored, const char *password)
|
||||
{
|
||||
if (!stored || !stored[0] || !password) return 0;
|
||||
char tmp[160];
|
||||
snprintf(tmp, sizeof tmp, "%s", stored);
|
||||
char *colon = strchr(tmp, ':');
|
||||
if (!colon) return 0;
|
||||
*colon = 0;
|
||||
char want[AUTH_HASH_LEN + 1];
|
||||
if (auth_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
|
||||
if (strlen(colon + 1) != AUTH_HASH_LEN) return 0;
|
||||
return memcmp(want, colon + 1, AUTH_HASH_LEN) == 0;
|
||||
}
|
||||
|
||||
int pin_set(void) /* panel.pin 是否已设(访问屏障开启) */
|
||||
{
|
||||
char *v = settings_get_value("panel.pin");
|
||||
int s = (v && v[0]) ? 1 : 0;
|
||||
free(v);
|
||||
return s;
|
||||
}
|
||||
|
||||
int auth_pass_set(void) /* 旧密码是否已设(兼容迁移用) */
|
||||
{
|
||||
char *v = settings_get_value("panel.auth_pass");
|
||||
int s = (v && v[0]) ? 1 : 0;
|
||||
free(v);
|
||||
return s;
|
||||
}
|
||||
|
||||
int auth_verify(const char *password) /* 旧密码校验(迁移期;见 login) */
|
||||
{
|
||||
char *stored = settings_get_value("panel.auth_pass");
|
||||
int ok = hash_matches(stored, password);
|
||||
free(stored);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int pin_verify(const char *pin) /* PIN 校验 */
|
||||
{
|
||||
char *stored = settings_get_value("panel.pin");
|
||||
int ok = hash_matches(stored, pin);
|
||||
free(stored);
|
||||
return ok;
|
||||
}
|
||||
|
||||
int pin_validate(const char *pin)
|
||||
{
|
||||
if (!pin) return 0;
|
||||
size_t l = strlen(pin);
|
||||
if (l != 4) return 0;
|
||||
for (size_t i = 0; i < l; i++)
|
||||
if (pin[i] < '0' || pin[i] > '9') return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
long long pin_lock_remaining(void) /* 剩余锁定秒;0=未锁 */
|
||||
{
|
||||
time_t now = time(NULL);
|
||||
if (g_pin_locked_until <= now) return 0;
|
||||
return (long long)(g_pin_locked_until - now);
|
||||
}
|
||||
|
||||
void pin_lock_reset(void)
|
||||
{
|
||||
g_pin_fails = 0;
|
||||
g_pin_locked_until = 0;
|
||||
}
|
||||
|
||||
void pin_record_fail(void)
|
||||
{
|
||||
if (++g_pin_fails >= PIN_MAX_FAILS) {
|
||||
g_pin_locked_until = time(NULL) + PIN_LOCK_SEC;
|
||||
g_pin_fails = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int sess_find(const char *token)
|
||||
{
|
||||
for (int i = 0; i < g_sess_n; i++)
|
||||
if (!strcmp(g_sess[i].token, token)) return i;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int sess_valid(const char *token)
|
||||
{
|
||||
int i = sess_find(token);
|
||||
if (i < 0) return 0;
|
||||
if (g_sess[i].expires > 0 && time(NULL) >= g_sess[i].expires) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void sess_add(const char *token, time_t expires)
|
||||
{
|
||||
int i = sess_find(token);
|
||||
if (i >= 0) { g_sess[i].expires = expires; return; }
|
||||
if (g_sess_n >= AUTH_MAX_SESS) { /* 满:淘汰最早过期的一条 */
|
||||
int e = 0;
|
||||
for (int k = 1; k < g_sess_n; k++)
|
||||
if (g_sess[k].expires < g_sess[e].expires) e = k;
|
||||
if (e < g_sess_n - 1)
|
||||
memmove(&g_sess[e], &g_sess[e + 1], (size_t)(g_sess_n - 1 - e) * sizeof(auth_sess_t));
|
||||
g_sess_n--;
|
||||
}
|
||||
snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", token);
|
||||
g_sess[g_sess_n].expires = expires;
|
||||
g_sess_n++;
|
||||
}
|
||||
|
||||
void sess_remove(const char *token)
|
||||
{
|
||||
int i = sess_find(token);
|
||||
if (i < 0) return;
|
||||
if (i < g_sess_n - 1)
|
||||
memmove(&g_sess[i], &g_sess[i + 1], (size_t)(g_sess_n - 1 - i) * sizeof(auth_sess_t));
|
||||
g_sess_n--;
|
||||
}
|
||||
|
||||
void auth_cp(char *dst, int dcap, const char *src) /* 有界拷贝(防 -Wformat-truncation) */
|
||||
{
|
||||
size_t l = strlen(src);
|
||||
if (l >= (size_t)dcap) l = (size_t)dcap - 1;
|
||||
memcpy(dst, src, l);
|
||||
dst[l] = 0;
|
||||
}
|
||||
|
||||
int extract_token(const req_t *r, char *out, int outcap)
|
||||
{
|
||||
out[0] = 0;
|
||||
if (r->x_meow_token[0]) { auth_cp(out, outcap, r->x_meow_token); return out[0] ? 1 : 0; }
|
||||
if (!strncasecmp(r->authorization, "Bearer ", 7)) {
|
||||
auth_cp(out, outcap, r->authorization + 7);
|
||||
return out[0] ? 1 : 0;
|
||||
}
|
||||
if (r->cookie[0]) {
|
||||
const char *p = strstr(r->cookie, "meow_token=");
|
||||
if (p) {
|
||||
p += 11;
|
||||
const char *e = p;
|
||||
while (*e && *e != ';' && *e != ' ' && *e != '\t') e++;
|
||||
size_t l = (size_t)(e - p);
|
||||
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
|
||||
memcpy(out, p, l);
|
||||
out[l] = 0;
|
||||
return out[0] ? 1 : 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int require_auth(int sock, const req_t *r)
|
||||
{
|
||||
const char *p = r->path;
|
||||
if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") ||
|
||||
!strcmp(p, "/api/auth/status") || !strcmp(p, "/api/auth/setup")) return 1;
|
||||
char t[65];
|
||||
if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1;
|
||||
return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效");
|
||||
}
|
||||
|
||||
int api_auth_status(int sock, const req_t *r)
|
||||
{
|
||||
int ps = pin_set();
|
||||
int lg = auth_pass_set();
|
||||
int sess = 0;
|
||||
char t[65];
|
||||
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
|
||||
long long rem = pin_lock_remaining();
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"auth_enabled\":true,\"session\":%s,\"pin_set\":%s,\"legacy\":%s,\"locked\":%s",
|
||||
sess ? "true" : "false", ps ? "true" : "false", lg ? "true" : "false", rem > 0 ? "true" : "false");
|
||||
if (rem > 0) sA(&s, ",\"lock_remaining_ms\":%lld", rem * 1000LL);
|
||||
sA(&s, "}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_auth_login(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jpin = jv_obj_get(root, "pin");
|
||||
jv_t *jpw = jv_obj_get(root, "password");
|
||||
char pin[8], pw[256];
|
||||
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0;
|
||||
if (jpw && jpw->type == J_STR && jpw->str) snprintf(pw, sizeof pw, "%s", jpw->str); else pw[0] = 0;
|
||||
jv_free(root);
|
||||
|
||||
if (pin_lock_remaining() > 0) { /* 已在锁定期 */
|
||||
sbuf_t e = {0};
|
||||
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"已锁定,请稍后重试\"},\"lock_remaining_ms\":%lld}",
|
||||
pin_lock_remaining() * 1000LL);
|
||||
int rc = send_json(sock, 429, e.b ? e.b : "");
|
||||
sbuf_free(&e);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int use_legacy = 0;
|
||||
const char *cred = NULL;
|
||||
if (pin_set()) { cred = pin; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 pin 字段"); }
|
||||
else if (auth_pass_set()) { use_legacy = 1; cred = pw; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); }
|
||||
else return send_err(sock, 400, "BAD_REQUEST", "请先设置面板 PIN(/api/auth/setup)");
|
||||
|
||||
int ok = use_legacy ? auth_verify(cred) : pin_verify(cred);
|
||||
if (!ok) {
|
||||
pin_record_fail();
|
||||
if (pin_lock_remaining() > 0) { /* 本错已触发锁定 */
|
||||
sbuf_t e = {0};
|
||||
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"连续错误次数过多,已锁定 5 分钟\"},\"lock_remaining_ms\":%lld}",
|
||||
pin_lock_remaining() * 1000LL);
|
||||
int rc = send_json(sock, 429, e.b ? e.b : "");
|
||||
sbuf_free(&e);
|
||||
return rc;
|
||||
}
|
||||
return send_err(sock, 401, "UNAUTHORIZED", "PIN 错误");
|
||||
}
|
||||
pin_lock_reset();
|
||||
char token[65];
|
||||
if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败");
|
||||
time_t exp = time(NULL) + g_auth_ttl; /* 固定 3 天(MEOW_AUTH_TTL 可覆盖测试用),不滑动续期 */
|
||||
sess_add(token, exp);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"token\":");
|
||||
sA_str(&s, token);
|
||||
sA(&s, ",\"expires_at\":%lld,\"auth_enabled\":true}", (long long)exp);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_auth_setup(int sock, const req_t *r)
|
||||
{
|
||||
if (pin_set()) return send_err(sock, 409, "CONFLICT", "面板 PIN 已设置,请直接登录");
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jpin = jv_obj_get(root, "pin");
|
||||
char pin[8];
|
||||
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str);
|
||||
else pin[0] = 0;
|
||||
jv_free(root);
|
||||
if (!pin_validate(pin)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
|
||||
char h[200];
|
||||
if (auth_make_hash(pin, h, sizeof h) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
|
||||
if (settings_write_one("panel.pin", h) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||||
pin_lock_reset();
|
||||
return send_json(sock, 200, "{\"pin_set\":true}");
|
||||
}
|
||||
|
||||
int api_auth_logout(int sock, const req_t *r)
|
||||
{
|
||||
char t[65];
|
||||
if (extract_token(r, t, sizeof t) && t[0]) sess_remove(t);
|
||||
return send_json(sock, 200, "{\"logged_out\":true}");
|
||||
}
|
||||
|
||||
#undef MP_ROR
|
||||
#undef MP_CH
|
||||
#undef MP_MAJ
|
||||
#undef MP_BSIG0
|
||||
#undef MP_BSIG1
|
||||
#undef MP_SSIG0
|
||||
#undef MP_SSIG1
|
||||
@@ -0,0 +1,619 @@
|
||||
/* mp_files.c — 文件域:路径安全/白名单 + /api/files* + multipart 上传解析 + 卡根派生路径。 */
|
||||
#include "mp.h"
|
||||
|
||||
char g_tf_root_buf[1024] = "/usr/data/mnt/sd_0";
|
||||
char g_tf_card_buf[2048], g_apps_dir_buf[2048], g_logs_dir_buf[2048], g_notif_log_buf[2048];
|
||||
const char *g_tf_card = NULL, *g_apps_dir = NULL, *g_logs_dir = NULL, *g_notif_log = NULL;
|
||||
const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
|
||||
#define ALLOWED_COUNT ((int)(sizeof g_allowed_dirs / sizeof g_allowed_dirs[0]))
|
||||
const char *g_sensitive_names[] = {
|
||||
"settings.conf", "setting.bak", ".env", "passwd", "shadow",
|
||||
"meow.conf", "user.ini", "config.json",
|
||||
};
|
||||
#define SENSITIVE_COUNT ((int)(sizeof g_sensitive_names / sizeof g_sensitive_names[0]))
|
||||
|
||||
|
||||
void init_tf_paths(void)
|
||||
{
|
||||
const char *env = getenv("MEOW_TF_ROOT");
|
||||
if (env && env[0]) {
|
||||
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", env);
|
||||
/* 去掉末尾 `/`(规范化处统一按无尾斜杠处理) */
|
||||
size_t l = strlen(g_tf_root_buf);
|
||||
while (l > 1 && g_tf_root_buf[l - 1] == '/') g_tf_root_buf[--l] = 0;
|
||||
}
|
||||
snprintf(g_tf_card_buf, sizeof g_tf_card_buf, "%s/MeowOS", TF_ROOT);
|
||||
snprintf(g_apps_dir_buf, sizeof g_apps_dir_buf, "%s/MeowAPPs", TF_ROOT);
|
||||
snprintf(g_logs_dir_buf, sizeof g_logs_dir_buf, "%s/MeowOS/logs", TF_ROOT);
|
||||
snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s/MeowOS/notifications/notifications.log", TF_ROOT);
|
||||
env = getenv("MEOW_NOTIF_LOG");
|
||||
if (env && env[0]) snprintf(g_notif_log_buf, sizeof g_notif_log_buf, "%s", env);
|
||||
g_tf_card = g_tf_card_buf;
|
||||
g_apps_dir = g_apps_dir_buf;
|
||||
g_logs_dir = g_logs_dir_buf;
|
||||
g_notif_log = g_notif_log_buf;
|
||||
}
|
||||
|
||||
int tf_present(void)
|
||||
{
|
||||
if (getenv("MEOW_TF_ROOT")) return 1; /* 宿主测试显式指定卡根(env 覆盖),信任其存在 */
|
||||
FILE *f = fopen("/proc/mounts", "r");
|
||||
if (!f) return 0;
|
||||
char line[512];
|
||||
int present = 0;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
if (strstr(line, TF_ROOT)) { present = 1; break; }
|
||||
}
|
||||
fclose(f);
|
||||
return present;
|
||||
}
|
||||
|
||||
int path_in_whitelist(const char *abs)
|
||||
{
|
||||
size_t rl = strlen(TF_ROOT);
|
||||
if (strncmp(abs, TF_ROOT, rl) != 0) return 0;
|
||||
if (abs[rl] == 0) return 1; /* 根自身 */
|
||||
if (abs[rl] != '/') return 0;
|
||||
const char *rest = abs + rl + 1;
|
||||
for (int i = 0; i < ALLOWED_COUNT; i++) {
|
||||
size_t dl = strlen(g_allowed_dirs[i]);
|
||||
if (strncmp(rest, g_allowed_dirs[i], dl) == 0) {
|
||||
if (rest[dl] == 0 || rest[dl] == '/') return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int is_sensitive_name(const char *base)
|
||||
{
|
||||
for (int i = 0; i < SENSITIVE_COUNT; i++)
|
||||
if (!strcmp(base, g_sensitive_names[i])) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int normalize_path(const char *in, char *buf, size_t buflen)
|
||||
{
|
||||
size_t rl = strlen(TF_ROOT);
|
||||
if (strlen(in) > 900) return -1; /* 拒绝超长路径(防御;真实路径远小于此) */
|
||||
char joined[2048];
|
||||
if (in[0] == '/') {
|
||||
if (strncmp(in, TF_ROOT, rl) == 0 && (in[rl] == 0 || in[rl] == '/')) {
|
||||
snprintf(joined, sizeof joined, "%s", in);
|
||||
} else {
|
||||
/* 相对 root 的展示路径,如 /MeowOS/x → 拼到 root */
|
||||
if (in[0] == '/' && in[1] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in + 1);
|
||||
else if (in[0] == '/') snprintf(joined, sizeof joined, "%s%s", TF_ROOT, in);
|
||||
else snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||||
}
|
||||
} else {
|
||||
snprintf(joined, sizeof joined, "%s/%s", TF_ROOT, in);
|
||||
}
|
||||
/* 逐分量规范化成**绝对**路径:/a/b/../c → /a/c;拒绝逃逸出 root。
|
||||
* 用绝对 out(以 '/' 开头)构建:首段即带前导 '/', 保证 path_in_whitelist 的绝对前缀比对成立。 */
|
||||
char out[2048];
|
||||
size_t n = 0;
|
||||
out[0] = '/';
|
||||
out[1] = 0;
|
||||
n = 1;
|
||||
char *p = joined;
|
||||
while (*p) {
|
||||
while (*p == '/') p++;
|
||||
if (!*p) break;
|
||||
char seg[256];
|
||||
int k = 0;
|
||||
while (*p && *p != '/') { if (k < 255) seg[k++] = *p; p++; }
|
||||
seg[k] = 0;
|
||||
if (!strcmp(seg, ".") || !strcmp(seg, "")) continue;
|
||||
if (!strcmp(seg, "..")) {
|
||||
/* 弹出一级;已在根(n==1)再 .. → 逃逸出 root,拒绝 */
|
||||
if (n <= 1) return -1;
|
||||
while (n > 1 && out[n - 1] != '/') n--;
|
||||
if (n > 1) n--; /* 去掉结尾 /;out[n] 归属下一段或结束 */
|
||||
out[n] = 0;
|
||||
continue;
|
||||
}
|
||||
if (k > 0) {
|
||||
size_t need = n + ((n > 1) ? 1 : 0) + (size_t)k + 1;
|
||||
if (need >= sizeof out) return -1;
|
||||
if (n > 1) { out[n++] = '/'; out[n] = 0; }
|
||||
memcpy(out + n, seg, (size_t)k);
|
||||
n += (size_t)k;
|
||||
out[n] = 0;
|
||||
}
|
||||
}
|
||||
/* 空/根(n==1,仅 "/")→ 卡根 */
|
||||
if (n <= 1) snprintf(out, sizeof out, "%s", TF_ROOT);
|
||||
size_t ol = strlen(out);
|
||||
if (ol >= buflen) return -1;
|
||||
memcpy(buf, out, ol + 1); /* 已证明 ol<buflen,用 memcpy 避免 %s 截断告警 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
void make_rel(const char *abs, char *buf, size_t buflen)
|
||||
{
|
||||
size_t rl = strlen(TF_ROOT);
|
||||
if (strncmp(abs, TF_ROOT, rl) != 0) { buf[0] = 0; return; }
|
||||
const char *rest = abs + rl;
|
||||
if (*rest == 0) snprintf(buf, buflen, "/");
|
||||
else snprintf(buf, buflen, "%s", rest);
|
||||
}
|
||||
|
||||
int open_safe_for_read(const char *abs, int follow_rel)
|
||||
{
|
||||
(void)follow_rel;
|
||||
if (!path_in_whitelist(abs)) return -1;
|
||||
const char *base = strrchr(abs, '/');
|
||||
base = base ? base + 1 : abs;
|
||||
if (is_sensitive_name(base)) return -1;
|
||||
struct stat st;
|
||||
if (stat(abs, &st) < 0) return -1;
|
||||
if (!S_ISREG(st.st_mode)) return -1;
|
||||
return open(abs, O_RDONLY);
|
||||
}
|
||||
|
||||
void fmt_json_item(sbuf_t *s, const char *name, int isdir, long long size, long long mtime)
|
||||
{
|
||||
sA(s, "{\"name\":");
|
||||
sA_str(s, name);
|
||||
sA(s, ",\"type\":\"%s\",\"size\":%lld,\"mtime\":%lld}",
|
||||
isdir ? "dir" : "file", size, mtime);
|
||||
}
|
||||
|
||||
int nat_strcmp(const char *a, const char *b)
|
||||
{
|
||||
const unsigned char *pa = (const unsigned char *)a;
|
||||
const unsigned char *pb = (const unsigned char *)b;
|
||||
while (*pa && *pb) {
|
||||
unsigned char ca = *pa, cb = *pb;
|
||||
if (ca >= '0' && ca <= '9' && cb >= '0' && cb <= '9') {
|
||||
const unsigned char *na = pa, *nb = pb;
|
||||
while (*na == '0') na++;
|
||||
while (*nb == '0') nb++;
|
||||
const unsigned char *da = na, *db = nb;
|
||||
while (*da >= '0' && *da <= '9') da++;
|
||||
while (*db >= '0' && *db <= '9') db++;
|
||||
int la = (int)(da - na), lb = (int)(db - nb); /* 有效数字位数(去前导0) */
|
||||
if (la != lb) return la - lb; /* 位数小者数小 */
|
||||
int c = memcmp(na, nb, (size_t)la);
|
||||
if (c) return c;
|
||||
int ta = (int)(da - pa), tb = (int)(db - pb); /* 原始数字串长(含前导0) */
|
||||
if (ta != tb) return ta - tb;
|
||||
pa = da; pb = db;
|
||||
continue; /* 数字段完全相同,继续 */
|
||||
}
|
||||
unsigned char la = (ca >= 'A' && ca <= 'Z') ? (unsigned char)(ca + 32) : ca;
|
||||
unsigned char lbc = (cb >= 'A' && cb <= 'Z') ? (unsigned char)(cb + 32) : cb;
|
||||
if (la != lbc) return (int)la - (int)lbc;
|
||||
if (ca != cb) return (int)ca - (int)cb; /* 大小写 tie-break:大写在前 */
|
||||
pa++; pb++;
|
||||
}
|
||||
return (int)*pa - (int)*pb;
|
||||
}
|
||||
|
||||
int fent_cmp(const void *a, const void *b)
|
||||
{
|
||||
const struct fent *fa = a, *fb = b;
|
||||
if (fa->isdir != fb->isdir) return fb->isdir - fa->isdir;
|
||||
return nat_strcmp(fa->name, fb->name);
|
||||
}
|
||||
|
||||
int files_list(int sock, const char *relpath)
|
||||
{
|
||||
char abs[1024], rel[1024];
|
||||
if (normalize_path(relpath, abs, sizeof abs) < 0)
|
||||
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||
if (!path_in_whitelist(abs))
|
||||
return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||
struct stat st;
|
||||
if (stat(abs, &st) < 0)
|
||||
return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||||
if (!S_ISDIR(st.st_mode))
|
||||
return send_err(sock, 404, "NOT_FOUND", "非目录");
|
||||
DIR *d = opendir(abs);
|
||||
if (!d)
|
||||
return send_err(sock, 500, "INTERNAL", "读目录失败");
|
||||
make_rel(abs, rel, sizeof rel);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"root\":");
|
||||
sA_str(&s, TF_ROOT);
|
||||
sA(&s, ",\"path\":");
|
||||
sA_str(&s, abs);
|
||||
sA(&s, ",\"rel\":");
|
||||
sA_str(&s, rel);
|
||||
sA(&s, ",\"allowed\":[");
|
||||
for (int i = 0; i < ALLOWED_COUNT; i++) { if (i) sA(&s, ","); sA_str(&s, g_allowed_dirs[i]); }
|
||||
sA(&s, "],\"items\":[");
|
||||
/* M2.78:排序后输出(原 readdir 原始顺序混杂难用)——目录优先,同组按名称
|
||||
* 不区分大小写排序;条目上限 1024(与 files App 一致,防巨目录爆内存)。 */
|
||||
int cap = 1024, n = 0;
|
||||
struct fent *ents = malloc((size_t)cap * sizeof *ents);
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
char full[1400];
|
||||
snprintf(full, sizeof full, "%s/%s", abs, e->d_name);
|
||||
struct stat fs;
|
||||
if (stat(full, &fs) < 0) continue;
|
||||
if (n >= cap) break;
|
||||
struct fent *en = &ents[n++];
|
||||
snprintf(en->name, sizeof en->name, "%s", e->d_name);
|
||||
en->isdir = S_ISDIR(fs.st_mode);
|
||||
en->size = en->isdir ? 0 : (long long)fs.st_size;
|
||||
en->mtime = (long long)fs.st_mtime;
|
||||
}
|
||||
closedir(d);
|
||||
qsort(ents, (size_t)n, sizeof *ents, fent_cmp);
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (i) sA(&s, ",");
|
||||
fmt_json_item(&s, ents[i].name, ents[i].isdir, ents[i].size, ents[i].mtime);
|
||||
}
|
||||
free(ents);
|
||||
sA(&s, "]}");
|
||||
return send_json(sock, 200, s.b ? s.b : "");
|
||||
}
|
||||
|
||||
char *read_text_file_limited(const char *abs, long long *size, int *truncated)
|
||||
{
|
||||
int fd = open_safe_for_read(abs, 0);
|
||||
if (fd < 0) return NULL;
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) < 0 || st.st_size < 0) { close(fd); return NULL; }
|
||||
long long total = (long long)st.st_size;
|
||||
size_t cap = total > TEXT_PREVIEW_MAX ? (size_t)TEXT_PREVIEW_MAX : (size_t)total;
|
||||
char *buf = malloc(cap + 1);
|
||||
if (!buf) { close(fd); return NULL; }
|
||||
ssize_t n = read(fd, buf, cap);
|
||||
close(fd);
|
||||
if (n < 0) { free(buf); return NULL; }
|
||||
buf[n] = 0;
|
||||
/* 二进制检测:含 NUL 或大量控制字符 */
|
||||
int ctrl = 0;
|
||||
for (ssize_t i = 0; i < n; i++) {
|
||||
unsigned char c = (unsigned char)buf[i];
|
||||
if (c == 0) { free(buf); return (char *)-2; } /* 二进制 */
|
||||
if (c < 0x09) ctrl++;
|
||||
}
|
||||
if (ctrl > n / 10) { free(buf); return (char *)-2; }
|
||||
if (size) *size = total;
|
||||
if (truncated) *truncated = (total > (long long)cap) ? 1 : 0;
|
||||
return buf;
|
||||
}
|
||||
|
||||
int safe_filename(const char *name, char *out, size_t cap)
|
||||
{
|
||||
if (!name || !name[0]) return -1;
|
||||
const char *b = name;
|
||||
/* 取最后一个路径分隔符之后(同时兼容 '/' 与 '\\') */
|
||||
const char *slash = strrchr(b, '/');
|
||||
const char *bsl = strrchr(b, '\\');
|
||||
if (bsl && (!slash || bsl > slash)) slash = bsl;
|
||||
if (slash) b = slash + 1;
|
||||
if (!b[0]) return -1;
|
||||
if (!strcmp(b, ".") || !strcmp(b, "..")) return -1;
|
||||
for (const char *p = b; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1; /* 禁止分隔符/控制字符 */
|
||||
}
|
||||
snprintf(out, cap, "%s", b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int parse_multipart(const req_t *r, upfile_t *files, int maxfiles)
|
||||
{
|
||||
/* 取 boundary */
|
||||
const char *ct = r->content_type;
|
||||
const char *b = strstr(ct, "boundary=");
|
||||
if (!b) return -1;
|
||||
b += 9;
|
||||
if (*b == '"') { b++; }
|
||||
char boundary[128];
|
||||
int bi = 0;
|
||||
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
|
||||
boundary[bi] = 0;
|
||||
char dmark[160];
|
||||
snprintf(dmark, sizeof dmark, "--%s", boundary);
|
||||
char emark[160];
|
||||
snprintf(emark, sizeof emark, "--%s--", boundary);
|
||||
/* 块间以 \r\n--boundary 分隔;用 strstr 扫描 */
|
||||
const char *p = r->body;
|
||||
const char *end = r->body + r->body_len;
|
||||
int nfiles = 0;
|
||||
char curpath[1024] = "";
|
||||
while (p < end) {
|
||||
const char *fstart = mem_find(p, (size_t)(end - p), dmark);
|
||||
if (!fstart) break;
|
||||
fstart += strlen(dmark);
|
||||
if (*fstart == '-') break; /* 结束标记 --boundary-- */
|
||||
if (*fstart == '\r' || *fstart == '\n') fstart++;
|
||||
/* 解析该 part 的头部(直至空行) */
|
||||
const char *headers_end = strstr(fstart, "\r\n\r\n");
|
||||
if (!headers_end) break;
|
||||
char hdr[512];
|
||||
size_t hl = (size_t)(headers_end - fstart);
|
||||
if (hl >= sizeof hdr) hl = sizeof hdr - 1;
|
||||
memcpy(hdr, fstart, hl);
|
||||
hdr[hl] = 0;
|
||||
const char *data_start = headers_end + 4;
|
||||
const char *name = NULL, *fname = NULL;
|
||||
char *save = NULL;
|
||||
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||||
char *l = ln;
|
||||
while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||||
/* name 与 filename 常同行(name="file"; filename="up.txt")。
|
||||
* 先取 filename 再取 name——若先截断 name 收尾引号会把同行的 filename 一并截断。 */
|
||||
char *fn = strstr(l, "filename=\"");
|
||||
if (fn) { fn += 10; char *q = strchr(fn, '"'); if (q) { *q = 0; fname = fn; } }
|
||||
char *nm = strstr(l, "name=\"");
|
||||
if (nm) { nm += 6; char *q = strchr(nm, '"'); if (q) { *q = 0; name = nm; } }
|
||||
}
|
||||
}
|
||||
/* 数据结束:下一个 "--boundary" 之前(二进制安全) */
|
||||
const char *data_end = mem_find(data_start, (size_t)(end - data_start), dmark);
|
||||
if (!data_end) data_end = end;
|
||||
/* 去掉数据尾部 \r\n */
|
||||
while (data_end > data_start && (data_end[-1] == '\r' || data_end[-1] == '\n')) data_end--;
|
||||
if (name && !strcmp(name, "path")) {
|
||||
size_t dl = (size_t)(data_end - data_start);
|
||||
if (dl >= sizeof curpath) dl = sizeof curpath - 1;
|
||||
memcpy(curpath, data_start, dl);
|
||||
curpath[dl] = 0;
|
||||
} else if (name && !strcmp(name, "file") && fname && nfiles < maxfiles) {
|
||||
size_t dl = (size_t)(data_end - data_start);
|
||||
char safe[260];
|
||||
if (safe_filename(fname, safe, sizeof safe) < 0) { p = data_end; continue; } /* 文件名非法:跳过该 part */
|
||||
/* 目标目录规范化 + 白名单校验 */
|
||||
char absdir[1024];
|
||||
if (normalize_path(curpath, absdir, sizeof absdir) < 0 || !path_in_whitelist(absdir)) { p = data_end; continue; }
|
||||
/* 拼绝对路径后再规范化 + 白名单复校(防拼接后残留逃逸) */
|
||||
char joined[1400];
|
||||
snprintf(joined, sizeof joined, "%s/%s", absdir, safe);
|
||||
char abs[1400];
|
||||
if (normalize_path(joined, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { p = data_end; continue; }
|
||||
snprintf(files[nfiles].filename, sizeof files[nfiles].filename, "%s", safe);
|
||||
snprintf(files[nfiles].path, sizeof files[nfiles].path, "%s", abs);
|
||||
int fd = open(abs, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd >= 0) {
|
||||
write_full(fd, data_start, dl);
|
||||
close(fd);
|
||||
nfiles++;
|
||||
}
|
||||
/* fd<0(不可写)→ 不计数,跳过该文件 */
|
||||
}
|
||||
p = data_end;
|
||||
}
|
||||
return nfiles; /* >=0:已保存文件数;-1 边界缺失/解析失败 */
|
||||
}
|
||||
|
||||
int api_files_list(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char path[1024] = "/";
|
||||
if (r->query[0]) qparam(r->query, "path", path, sizeof path);
|
||||
return files_list(sock, path);
|
||||
}
|
||||
|
||||
int api_files_read(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char path[1024];
|
||||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||||
char abs[1024];
|
||||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||
long long size; int trunc;
|
||||
char *txt = read_text_file_limited(abs, &size, &trunc);
|
||||
if (txt == (char *)-2) return send_err(sock, 415, "UNSUPPORTED_MEDIA", "二进制文件不支持文本读取");
|
||||
if (!txt) return send_err(sock, 404, "NOT_FOUND", "文件不存在或过大");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"path\":");
|
||||
sA_str(&s, abs);
|
||||
sA(&s, ",\"content\":");
|
||||
sA_str(&s, txt);
|
||||
sA(&s, ",\"size\":%lld,\"truncated\":%s}", size, trunc ? "true" : "false");
|
||||
free(txt);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_files_download(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char path[1024];
|
||||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||||
char abs[1024];
|
||||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||
const char *base = strrchr(abs, '/');
|
||||
base = base ? base + 1 : abs;
|
||||
if (is_sensitive_name(base)) return send_err(sock, 403, "FORBIDDEN", "敏感文件");
|
||||
return send_file(sock, 200, "application/octet-stream", abs, base);
|
||||
}
|
||||
|
||||
int api_files_upload(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (strncasecmp(r->content_type, "multipart/form-data", 19) != 0)
|
||||
return send_err(sock, 415, "UNSUPPORTED_MEDIA", "需 multipart/form-data");
|
||||
upfile_t files[16];
|
||||
memset(files, 0, sizeof files);
|
||||
int n = parse_multipart(r, files, 16);
|
||||
if (n < 0) return send_err(sock, 400, "BAD_REQUEST", "multipart 解析失败");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"item\":[");
|
||||
int first = 1;
|
||||
for (int i = 0; i < n && i < 16; i++) {
|
||||
if (!files[i].filename[0]) continue;
|
||||
if (!strstr(files[i].path, "/")) continue; /* 无目标目录的跳过 */
|
||||
struct stat st;
|
||||
if (stat(files[i].path, &st) < 0) continue;
|
||||
const char *base = strrchr(files[i].path, '/');
|
||||
base = base ? base + 1 : files[i].path;
|
||||
if (!first) sA(&s, ",");
|
||||
first = 0;
|
||||
sA(&s, "{\"name\":");
|
||||
sA_str(&s, base);
|
||||
sA(&s, ",\"type\":\"file\",\"size\":%lld,\"mtime\":%lld}",
|
||||
(long long)st.st_size, (long long)st.st_mtime);
|
||||
}
|
||||
sA(&s, "]}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int rm_recursive(const char *path)
|
||||
{
|
||||
struct stat st;
|
||||
if (lstat(path, &st) < 0) return -1;
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
DIR *d = opendir(path);
|
||||
if (!d) return -1;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", path, e->d_name);
|
||||
rm_recursive(full);
|
||||
}
|
||||
closedir(d);
|
||||
return rmdir(path);
|
||||
}
|
||||
return unlink(path);
|
||||
}
|
||||
|
||||
int api_files_mkdir(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *pj = jv_obj_get(root, "path");
|
||||
if (!pj || pj->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path"); }
|
||||
char abs[1024];
|
||||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||
struct stat st;
|
||||
if (stat(abs, &st) == 0) { jv_free(root); return send_err(sock, 409, "CONFLICT", "目录已存在"); }
|
||||
if (mkdir(abs, 0755) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "创建目录失败"); }
|
||||
const char *base = strrchr(abs, '/');
|
||||
const char *b = base ? base + 1 : abs;
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"item\":{\"name\":");
|
||||
sA_str(&s, b);
|
||||
sA(&s, ",\"type\":\"dir\",\"size\":0,\"mtime\":%lld}}", (long long)time(NULL));
|
||||
jv_free(root);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_files_rename(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *pj = jv_obj_get(root, "path");
|
||||
jv_t *pnn = jv_obj_get(root, "new_name");
|
||||
if (!pj || pj->type != J_STR || !pnn || pnn->type != J_STR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 path/new_name"); }
|
||||
if (strchr(pnn->str, '/')) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "new_name 仅文件名"); }
|
||||
char abs[1024], newabs[1400];
|
||||
if (normalize_path(pj->str, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||
/* 新路径 = 原路径的**父目录** + new_name(new_name 仅文件名,不含分隔符) */
|
||||
char parent[1024];
|
||||
snprintf(parent, sizeof parent, "%s", abs);
|
||||
char *sl = strrchr(parent, '/');
|
||||
if (!sl || sl == parent) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "无法重命名根路径"); }
|
||||
*sl = 0;
|
||||
if (!parent[0]) snprintf(parent, sizeof parent, "%s", TF_ROOT); /* abs 顶层(如 /tmp/mptf/x)→ 父=根 */
|
||||
snprintf(newabs, sizeof newabs, "%s/%s", parent, pnn->str);
|
||||
/* newabs 复校白名单(new_name 无 /,父目录已校验;防御) */
|
||||
if (!path_in_whitelist(newabs)) { jv_free(root); return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸"); }
|
||||
if (rename(abs, newabs) < 0) { jv_free(root); return send_err(sock, 500, "INTERNAL", "重命名失败"); }
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"item\":{\"name\":");
|
||||
sA_str(&s, pnn->str);
|
||||
sA(&s, ",\"type\":\"");
|
||||
struct stat st;
|
||||
if (stat(newabs, &st) == 0 && S_ISDIR(st.st_mode)) sA(&s, "dir"); else sA(&s, "file");
|
||||
sA(&s, "\",\"size\":%lld,\"mtime\":%lld}}", (long long)st.st_size, (long long)st.st_mtime);
|
||||
jv_free(root);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_files_delete(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char path[1024];
|
||||
if (!qparam(r->query, "path", path, sizeof path)) return send_err(sock, 400, "BAD_REQUEST", "缺 path");
|
||||
char recursive[8] = "0";
|
||||
qparam(r->query, "recursive", recursive, sizeof recursive);
|
||||
char abs[1024];
|
||||
if (normalize_path(path, abs, sizeof abs) < 0 || !path_in_whitelist(abs)) return send_err(sock, 403, "FORBIDDEN", "白名单外/路径逃逸");
|
||||
if (!strcmp(abs, TF_ROOT)) return send_err(sock, 403, "FORBIDDEN", "禁止删除卡根");
|
||||
struct stat st;
|
||||
if (stat(abs, &st) < 0) return send_err(sock, 404, "NOT_FOUND", "路径不存在");
|
||||
if (S_ISDIR(st.st_mode) && atoi(recursive) != 1) {
|
||||
DIR *d = opendir(abs);
|
||||
int nonempty = 0;
|
||||
if (d) {
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
nonempty = 1; break;
|
||||
}
|
||||
closedir(d);
|
||||
}
|
||||
if (nonempty) return send_err(sock, 409, "CONFLICT", "目录非空(需 recursive=1)");
|
||||
}
|
||||
if (rm_recursive(abs) < 0) return send_err(sock, 500, "INTERNAL", "删除失败");
|
||||
return send_json(sock, 200, "{\"removed\":true}");
|
||||
}
|
||||
|
||||
int multipart_extract_file(const req_t *r, const char *outpath)
|
||||
{
|
||||
const char *b = strstr(r->content_type, "boundary=");
|
||||
if (!b) return -1;
|
||||
b += 9;
|
||||
if (*b == '"') b++;
|
||||
char boundary[128]; int bi = 0;
|
||||
while (b[bi] && b[bi] != ';' && b[bi] != '"' && b[bi] != '\r' && bi < 120) { boundary[bi] = b[bi]; bi++; }
|
||||
boundary[bi] = 0;
|
||||
char dmark[160];
|
||||
snprintf(dmark, sizeof dmark, "--%s", boundary);
|
||||
const char *p = r->body;
|
||||
while (p < r->body + r->body_len) {
|
||||
const char *fs = strstr(p, dmark);
|
||||
if (!fs) break;
|
||||
fs += strlen(dmark);
|
||||
if (*fs == '-') break;
|
||||
if (*fs == '\r' || *fs == '\n') fs++;
|
||||
const char *he = strstr(fs, "\r\n\r\n");
|
||||
if (!he) break;
|
||||
char hdr[512];
|
||||
size_t hl = (size_t)(he - fs); if (hl >= sizeof hdr) hl = sizeof hdr - 1;
|
||||
memcpy(hdr, fs, hl); hdr[hl] = 0;
|
||||
int isfile = 0;
|
||||
char *save = NULL;
|
||||
for (char *ln = strtok_r(hdr, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||||
char *l = ln; while (*l == '\r' || *l == ' ' || *l == '\t') l++;
|
||||
if (strncasecmp(l, "Content-Disposition:", 20) == 0) {
|
||||
if (strstr(l, "name=\"file\"")) isfile = 1;
|
||||
}
|
||||
}
|
||||
const char *ds = he + 4;
|
||||
const char *de = mem_find(ds, (size_t)((r->body + r->body_len) - ds), dmark);
|
||||
if (!de) de = r->body + r->body_len;
|
||||
while (de > ds && (de[-1] == '\r' || de[-1] == '\n')) de--;
|
||||
if (isfile) {
|
||||
int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
if (fd < 0) return -1;
|
||||
write_full(fd, ds, (size_t)(de - ds));
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
p = de;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
/* mp_fonts.c — 字体库域:tar.gz 导入/导出 + 列表(与 .mpk 同构,走 busybox gzip|tar 管道)。 */
|
||||
#include "mp.h"
|
||||
|
||||
#define FONT_EXTRACT_MAX (32u * 1024u * 1024u) /* 三件套解压后总大小上限(防 gzip 炸弹) */
|
||||
|
||||
|
||||
void fonts_root(char *buf, size_t cap) { snprintf(buf, cap, "%s/MeowOS/fonts", TF_ROOT); }
|
||||
|
||||
int safe_dir_segment(const char *s, char *out, size_t cap)
|
||||
{
|
||||
if (!s || !s[0]) return -1;
|
||||
if (!strcmp(s, ".") || !strcmp(s, "..")) return -1;
|
||||
for (const char *p = s; *p; p++) {
|
||||
unsigned char c = (unsigned char)*p;
|
||||
if (c == '/' || c == '\\' || c < 0x20 || c == 0x7f) return -1;
|
||||
}
|
||||
if (strlen(s) + 1 > cap) return -1;
|
||||
snprintf(out, cap, "%s", s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int unsafe_path(const char *s)
|
||||
{
|
||||
return s && strpbrk(s, " ;`$'\"&|<>") != NULL;
|
||||
}
|
||||
|
||||
int has_dotdot_comp(const char *p)
|
||||
{
|
||||
const char *s = p;
|
||||
while (*s) {
|
||||
if (s[0] == '.' && s[1] == '.' &&
|
||||
(s == p || s[-1] == '/') && (s[2] == 0 || s[2] == '/'))
|
||||
return 1;
|
||||
s++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sanitize_member(const char *in, char *out, int n)
|
||||
{
|
||||
if (!in || !*in) return -1;
|
||||
const char *p = in;
|
||||
while (*p == '/' || (p[0] == '.' && p[1] == '/')) p++;
|
||||
if (*p == '/' || !*p) return -1;
|
||||
if (has_dotdot_comp(p)) return -1;
|
||||
if (unsafe_path(p)) return -1;
|
||||
size_t l = strlen(p);
|
||||
if (l + 1 > (size_t)n) return -1;
|
||||
memcpy(out, p, l + 1); /* memcpy 而非 snprintf:p 可能来自长行缓冲,防 -Wformat-truncation 误报 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int mkdir_p(const char *path)
|
||||
{
|
||||
char buf[1600];
|
||||
snprintf(buf, sizeof buf, "%s", path);
|
||||
for (char *p = buf + 1; *p; p++) {
|
||||
if (*p == '/') { *p = 0; mkdir(buf, 0755); *p = '/'; }
|
||||
}
|
||||
return mkdir(buf, 0755) == 0 || errno == EEXIST ? 0 : -1;
|
||||
}
|
||||
|
||||
const jv_t *font_json_str(const jv_t *root, const char *key)
|
||||
{
|
||||
const jv_t *v = jv_obj_get(root, key);
|
||||
if (v && v->type == J_STR && v->str) return v;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int api_fonts_list(int sock)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char root[1100];
|
||||
fonts_root(root, sizeof root);
|
||||
DIR *fd = opendir(root);
|
||||
if (!fd) return send_json(sock, 200, "{\"fonts\":[]}");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"fonts\":[");
|
||||
int first = 1;
|
||||
struct dirent *fe;
|
||||
while ((fe = readdir(fd))) {
|
||||
if (fe->d_name[0] == '.') continue;
|
||||
char famdir[1700];
|
||||
snprintf(famdir, sizeof famdir, "%s/%s", root, fe->d_name);
|
||||
struct stat fst;
|
||||
if (stat(famdir, &fst) < 0 || !S_ISDIR(fst.st_mode)) continue;
|
||||
DIR *wd = opendir(famdir);
|
||||
if (!wd) continue;
|
||||
struct dirent *we;
|
||||
while ((we = readdir(wd))) {
|
||||
if (we->d_name[0] == '.') continue;
|
||||
char wdir[2200];
|
||||
snprintf(wdir, sizeof wdir, "%s/%s", famdir, we->d_name);
|
||||
struct stat wst;
|
||||
if (stat(wdir, &wst) < 0 || !S_ISDIR(wst.st_mode)) continue;
|
||||
char pp[2560], pi[2560], pj[2560];
|
||||
snprintf(pp, sizeof pp, "%s/font.png", wdir);
|
||||
snprintf(pi, sizeof pi, "%s/font.idx", wdir);
|
||||
snprintf(pj, sizeof pj, "%s/font.json", wdir);
|
||||
if (access(pp, R_OK) || access(pi, R_OK) || access(pj, R_OK)) continue;
|
||||
/* 解析 font.json(可选增强字段) */
|
||||
char name[256] = "", tier[64] = "";
|
||||
long long gc = 0;
|
||||
char jbuf[4096];
|
||||
if (read_small_file(pj, jbuf, sizeof jbuf) >= 0) {
|
||||
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
|
||||
if (jr) {
|
||||
const jv_t *v = font_json_str(jr, "name");
|
||||
if (v) snprintf(name, sizeof name, "%s", v->str);
|
||||
v = font_json_str(jr, "tier");
|
||||
if (v) snprintf(tier, sizeof tier, "%s", v->str);
|
||||
else { v = font_json_str(jr, "charset"); if (v) snprintf(tier, sizeof tier, "%s", v->str); }
|
||||
v = jv_obj_get(jr, "glyph_count");
|
||||
if (v && v->type == J_NUM) gc = v->inum;
|
||||
jv_free(jr);
|
||||
}
|
||||
}
|
||||
if (!name[0]) snprintf(name, sizeof name, "%s", fe->d_name);
|
||||
if (!first) sA(&s, ",");
|
||||
sA(&s, "{\"family\":");
|
||||
sA_str(&s, fe->d_name);
|
||||
sA(&s, ",\"weight\":");
|
||||
sA_str(&s, we->d_name);
|
||||
sA(&s, ",\"name\":");
|
||||
sA_str(&s, name);
|
||||
sA(&s, ",\"tier\":");
|
||||
sA_str(&s, tier);
|
||||
sA(&s, ",\"glyph_count\":%lld,\"mtime\":%lld}", gc, (long long)wst.st_mtime);
|
||||
first = 0;
|
||||
}
|
||||
closedir(wd);
|
||||
}
|
||||
closedir(fd);
|
||||
sA(&s, "]}");
|
||||
return send_json(sock, 200, s.b ? s.b : "{\"fonts\":[]}");
|
||||
}
|
||||
|
||||
int sha256_file_hex(const char *path, char out[65])
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
mp_sha256_ctx c;
|
||||
mp_sha256_init(&c);
|
||||
uint8_t buf[8192];
|
||||
ssize_t n;
|
||||
while ((n = read(fd, buf, sizeof buf)) > 0) mp_sha256_update(&c, buf, (size_t)n);
|
||||
close(fd);
|
||||
if (n < 0) return -1;
|
||||
uint8_t h[32];
|
||||
mp_sha256_final(&c, h);
|
||||
auth_hex(h, sizeof h, out);
|
||||
out[64] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_fonts_import(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char force[8] = "0";
|
||||
qparam(r->query, "force", force, sizeof force);
|
||||
long uni = (long)time(NULL);
|
||||
char tmpdir[1500], tmpgz[1500], stg[1500];
|
||||
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
|
||||
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
|
||||
snprintf(tmpgz, sizeof tmpgz, "%s/tmp/mp_font_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
|
||||
snprintf(stg, sizeof stg, "%s/tmp/mp_font_stg_%d_%ld", TF_CARD_DIR, (int)getpid(), uni);
|
||||
|
||||
/* ① 写 multipart file 到临时 tar.gz */
|
||||
if (multipart_extract_file(r, tmpgz) < 0)
|
||||
return send_err(sock, 400, "BAD_REQUEST", "缺字体库 tar.gz(multipart 字段 file)");
|
||||
|
||||
/* ② 成员名预校验:gzip -dc | tar -t → 每名 sanitize,且恰为三件套平铺(拒绝对/.. / 多出条目) */
|
||||
{
|
||||
char cmd[3200];
|
||||
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -t 2>/dev/null", tmpgz);
|
||||
FILE *pf = popen(cmd, "r");
|
||||
if (!pf) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "无法列 tar 成员"); }
|
||||
int bad = 0, have_png = 0, have_idx = 0, have_json = 0, nent = 0;
|
||||
char line[512];
|
||||
while (fgets(line, sizeof line, pf)) {
|
||||
char *e = strchr(line, '\n'); if (e) *e = 0;
|
||||
if (!line[0]) continue;
|
||||
char san[128];
|
||||
if (sanitize_member(line, san, sizeof san) < 0) { bad = 1; break; }
|
||||
if (!strcmp(san, "font.png")) have_png = 1;
|
||||
else if (!strcmp(san, "font.idx")) have_idx = 1;
|
||||
else if (!strcmp(san, "font.json")) have_json = 1;
|
||||
else { bad = 1; break; }
|
||||
nent++;
|
||||
}
|
||||
pclose(pf);
|
||||
if (bad || nent != 3 || !have_png || !have_idx || !have_json) {
|
||||
unlink(tmpgz);
|
||||
return send_err(sock, 400, "BAD_REQUEST", "tar 缺三件套或含非法成员(拒绝绝对路径/../多出条目)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ③ 解压到临时目录 */
|
||||
rm_recursive(stg);
|
||||
if (mkdir_p(stg) != 0) { unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "建临时目录失败"); }
|
||||
{
|
||||
char cmd[3200];
|
||||
snprintf(cmd, sizeof cmd, "gzip -dc '%s' 2>/dev/null | tar -x -C '%s' 2>/dev/null", tmpgz, stg);
|
||||
if (system(cmd) != 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "tar 解压失败(非 gzip/tar?)"); }
|
||||
}
|
||||
|
||||
/* ④ 目录锁:恰为 3 个普通文件(lstat 拒软链/目录/设备),总大小 ≤ FONT_EXTRACT_MAX(防炸弹) */
|
||||
{
|
||||
static const char *names[3] = { "font.png", "font.idx", "font.json" };
|
||||
int seen[3] = {0,0,0}, cnt = 0, ok = 1;
|
||||
DIR *d = opendir(stg);
|
||||
if (!d) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 500, "INTERNAL", "读临时目录失败"); }
|
||||
struct dirent *de;
|
||||
while ((de = readdir(d))) {
|
||||
if (!strcmp(de->d_name, ".") || !strcmp(de->d_name, "..")) continue;
|
||||
int idx = -1;
|
||||
for (int i = 0; i < 3; i++) if (!strcmp(de->d_name, names[i])) { idx = i; break; }
|
||||
if (idx < 0) { ok = 0; break; }
|
||||
char fp[1800]; snprintf(fp, sizeof fp, "%s/%s", stg, de->d_name);
|
||||
struct stat st;
|
||||
if (lstat(fp, &st) != 0 || !S_ISREG(st.st_mode)) { ok = 0; break; } /* 拒软链/目录/设备 */
|
||||
seen[idx] = 1; cnt++;
|
||||
}
|
||||
closedir(d);
|
||||
long long tot = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (!seen[i]) { ok = 0; break; }
|
||||
char fp[1600]; snprintf(fp, sizeof fp, "%s/%s", stg, names[i]);
|
||||
struct stat st;
|
||||
if (stat(fp, &st) != 0) { ok = 0; break; }
|
||||
tot += (long long)st.st_size;
|
||||
}
|
||||
if (!ok || cnt != 3 || tot > (long long)FONT_EXTRACT_MAX) {
|
||||
rm_recursive(stg); unlink(tmpgz);
|
||||
return send_err(sock, 400, "BAD_REQUEST", "tar 内容非法(缺件/软链/多出条目/解压后超大)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ⑤ 读 font.json → family/weight(可解析硬失败) */
|
||||
char jpath[1600]; snprintf(jpath, sizeof jpath, "%s/font.json", stg);
|
||||
char jbuf[8192];
|
||||
if (read_small_file(jpath, jbuf, sizeof jbuf) < 0) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 缺失"); }
|
||||
jv_t *jr = jv_parse(jbuf, (int)strlen(jbuf));
|
||||
if (!jr) { rm_recursive(stg); unlink(tmpgz); return send_err(sock, 400, "BAD_REQUEST", "font.json 解析失败"); }
|
||||
char family[128] = "font", weight[64] = "regular";
|
||||
const jv_t *v = font_json_str(jr, "family");
|
||||
if (v) snprintf(family, sizeof family, "%s", v->str);
|
||||
else { v = font_json_str(jr, "name"); if (v) snprintf(family, sizeof family, "%s", v->str); }
|
||||
v = font_json_str(jr, "weight");
|
||||
if (v) snprintf(weight, sizeof weight, "%s", v->str);
|
||||
char fam_s[128], wt_s[64];
|
||||
if (safe_dir_segment(family, fam_s, sizeof fam_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json family 非法"); }
|
||||
if (safe_dir_segment(weight, wt_s, sizeof wt_s) < 0) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 422, "UNSUPPORTED", "font.json weight 非法"); }
|
||||
snprintf(family, sizeof family, "%s", fam_s);
|
||||
snprintf(weight, sizeof weight, "%s", wt_s);
|
||||
|
||||
/* ⑥ 目标目录 + 覆盖冲突(409,除非 force=1) */
|
||||
char root[1100], target[1500];
|
||||
fonts_root(root, sizeof root);
|
||||
snprintf(target, sizeof target, "%s/%s/%s", root, family, weight);
|
||||
if (strcmp(force, "1") != 0) {
|
||||
static const char *names3[] = { "font.png", "font.idx", "font.json" };
|
||||
char tp[1600]; int conflicted = 0;
|
||||
for (int i = 0; i < 3; i++) { snprintf(tp, sizeof tp, "%s/%s", target, names3[i]); if (access(tp, F_OK) == 0) { conflicted = 1; break; } }
|
||||
if (conflicted) { jv_free(jr); rm_recursive(stg); unlink(tmpgz); return send_err(sock, 409, "CONFLICT", "目标字库已存在,请确认覆盖"); }
|
||||
}
|
||||
|
||||
/* ⑦ sha256 仅 WARN(对齐 font.json 约定,不硬失败) */
|
||||
char warn[256] = "";
|
||||
{
|
||||
const jv_t *wp = font_json_str(jr, "sha256_png");
|
||||
const jv_t *wi = font_json_str(jr, "sha256_idx");
|
||||
char need[65], got[65];
|
||||
if (wp && wp->str[0]) {
|
||||
snprintf(need, sizeof need, "%s", wp->str);
|
||||
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.png", stg);
|
||||
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
|
||||
snprintf(warn, sizeof warn, "sha256_png 不匹配(源库校验失败,但已导入)");
|
||||
}
|
||||
if (wi && wi->str[0] && !warn[0]) {
|
||||
snprintf(need, sizeof need, "%s", wi->str);
|
||||
char fp[1600]; snprintf(fp, sizeof fp, "%s/font.idx", stg);
|
||||
if (sha256_file_hex(fp, got) == 0 && strcasecmp(need, got) != 0)
|
||||
snprintf(warn, sizeof warn, "sha256_idx 不匹配(源库校验失败,但已导入)");
|
||||
}
|
||||
}
|
||||
|
||||
/* ⑧ 建目标目录 + rename 三件套(原子替换) */
|
||||
{
|
||||
char p1[1400];
|
||||
snprintf(p1, sizeof p1, "%s/%s", root, family);
|
||||
mkdir(p1, 0755);
|
||||
mkdir(target, 0755);
|
||||
}
|
||||
{
|
||||
static const char *names3[] = { "font.png", "font.idx", "font.json" };
|
||||
int ok = 1;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char sp[1600], tp[1600];
|
||||
snprintf(sp, sizeof sp, "%s/%s", stg, names3[i]);
|
||||
snprintf(tp, sizeof tp, "%s/%s", target, names3[i]);
|
||||
if (rename(sp, tp) < 0) ok = 0;
|
||||
}
|
||||
if (!ok) { rm_recursive(stg); unlink(tmpgz); jv_free(jr); return send_err(sock, 500, "INTERNAL", "写入字体文件失败(磁盘满?)"); }
|
||||
}
|
||||
rm_recursive(stg);
|
||||
unlink(tmpgz);
|
||||
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"family\":");
|
||||
sA_str(&s, family);
|
||||
sA(&s, ",\"weight\":");
|
||||
sA_str(&s, weight);
|
||||
sA(&s, ",\"imported\":true,\"hint\":\"字体已导入。请进设备「设置 → 字体」页,字体列表在进入时自动重扫生效。\",\"warning\":");
|
||||
sA_str(&s, warn[0] ? warn : "");
|
||||
sA(&s, "}");
|
||||
jv_free(jr);
|
||||
return send_json(sock, 200, s.b ? s.b : "");
|
||||
}
|
||||
|
||||
int api_fonts_export(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char family[128], weight[64];
|
||||
if (!qparam(r->query, "family", family, sizeof family)) family[0] = 0;
|
||||
if (!qparam(r->query, "weight", weight, sizeof weight)) weight[0] = 0;
|
||||
char f[128], w[64];
|
||||
if (safe_dir_segment(family, f, sizeof f) < 0) return send_err(sock, 400, "BAD_REQUEST", "family 非法");
|
||||
if (!weight[0]) snprintf(w, sizeof w, "%s", "regular");
|
||||
else if (safe_dir_segment(weight, w, sizeof w) < 0) return send_err(sock, 400, "BAD_REQUEST", "weight 非法");
|
||||
char root[1100], dir[1500];
|
||||
fonts_root(root, sizeof root);
|
||||
snprintf(dir, sizeof dir, "%s/%s/%s", root, f, w);
|
||||
static const char *names3[] = { "font.png", "font.idx", "font.json" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
char ap[1600];
|
||||
snprintf(ap, sizeof ap, "%s/%s", dir, names3[i]);
|
||||
struct stat st;
|
||||
if (stat(ap, &st) < 0) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "font."); sA_str(&s, names3[i]);
|
||||
sA(&s, " 缺失(字库 <");
|
||||
sA_str(&s, f); sA(&s, "/"); sA_str(&s, w); sA(&s, "> 不完整)");
|
||||
int rc = send_err(sock, 404, "NOT_FOUND", s.b ? s.b : "字体三件套缺失");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
}
|
||||
/* 打包:tar -cf - -C <dir> 三件套 | gzip -c > <tmp>,落临时文件求 Content-Length 再流式下发 */
|
||||
long uni = (long)time(NULL);
|
||||
char tmpdir[1500], tmp[1500];
|
||||
snprintf(tmpdir, sizeof tmpdir, "%s/tmp", TF_CARD_DIR);
|
||||
if (mkdir_p(tmpdir) != 0) return send_err(sock, 500, "INTERNAL", "建临时目录失败");
|
||||
snprintf(tmp, sizeof tmp, "%s/tmp/mp_font_exp_%d_%ld.tar.gz", TF_CARD_DIR, (int)getpid(), uni);
|
||||
char cmd[3600];
|
||||
snprintf(cmd, sizeof cmd, "tar -cf - -C '%s' font.png font.idx font.json 2>/dev/null | gzip -c > '%s' 2>/dev/null", dir, tmp);
|
||||
if (system(cmd) != 0) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包失败"); }
|
||||
struct stat st;
|
||||
if (stat(tmp, &st) < 0 || !S_ISREG(st.st_mode)) { unlink(tmp); return send_err(sock, 500, "INTERNAL", "打包文件不可读"); }
|
||||
char fname[220];
|
||||
snprintf(fname, sizeof fname, "%s-%s.font.tar.gz", f, w);
|
||||
char extra[300];
|
||||
snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", fname);
|
||||
if (send_head(sock, 200, "application/gzip", (long long)st.st_size, extra) < 0) { unlink(tmp); return -1; }
|
||||
int fd = open(tmp, O_RDONLY);
|
||||
if (fd < 0) { unlink(tmp); return -1; }
|
||||
char buf[8192];
|
||||
ssize_t n;
|
||||
while ((n = read(fd, buf, sizeof buf)) > 0)
|
||||
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); unlink(tmp); return -1; }
|
||||
close(fd);
|
||||
unlink(tmp);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
/* mp_ipc.c — IPC 帧读写 + meow_svc_perf/power/netmgr 客户端(单点 IPC 收发)。 */
|
||||
#include "mp.h"
|
||||
|
||||
const char *g_sock_perf = PERF_SOCK;
|
||||
const char *g_sock_power = POWER_SOCK;
|
||||
const char *g_sock_launcher = LAUNCHER_SOCK;
|
||||
const char *g_sock_audio = AUDIO_SOCK;
|
||||
const char *g_sock_net = NET_SOCK;
|
||||
const char *g_sock_pm = PM_SOCK;
|
||||
|
||||
|
||||
int x_read_full(int fd, uint8_t *buf, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t r = read(fd, buf + off, n - off);
|
||||
if (r <= 0) return -1;
|
||||
off += (size_t)r;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int x_write_full(int fd, const uint8_t *buf, size_t n)
|
||||
{
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t w = write(fd, buf + off, n - off);
|
||||
if (w <= 0) return -1;
|
||||
off += (size_t)w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int 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, rl = len;
|
||||
uint16_t ver = MEOW_IPC_VERSION, t = type;
|
||||
memcpy(hdr + 0, &magic, 4);
|
||||
memcpy(hdr + 4, &ver, 2);
|
||||
memcpy(hdr + 6, &t, 2);
|
||||
memcpy(hdr + 8, &req_id, 4);
|
||||
memcpy(hdr + 12, &rl, 4);
|
||||
if (x_write_full(fd, hdr, MEOW_IPC_HDR_LEN) < 0) return -1;
|
||||
if (len && payload && x_write_full(fd, payload, len) < 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int recv_frame(int fd, uint16_t *type, uint32_t *req_id, uint8_t *buf, uint32_t cap)
|
||||
{
|
||||
uint8_t hdr[MEOW_IPC_HDR_LEN];
|
||||
if (x_read_full(fd, hdr, sizeof hdr) < 0) return -1;
|
||||
uint32_t magic, rlen, rid;
|
||||
uint16_t ver, t;
|
||||
memcpy(&magic, hdr, 4);
|
||||
memcpy(&ver, hdr + 4, 2);
|
||||
memcpy(&t, hdr + 6, 2);
|
||||
memcpy(&rid, hdr + 8, 4);
|
||||
memcpy(&rlen, hdr + 12, 4);
|
||||
if (magic != MEOW_IPC_MAGIC || ver != MEOW_IPC_VERSION) return -1;
|
||||
if (rlen > MEOW_IPC_MAX) return -1;
|
||||
if (type) *type = t;
|
||||
if (req_id) *req_id = rid;
|
||||
uint32_t n = (rlen < cap) ? rlen : cap;
|
||||
if (n && x_read_full(fd, buf, n) < 0) return -1;
|
||||
/* 若实际更长,丢弃剩余 */
|
||||
for (uint32_t i = n; i < rlen; i++) { uint8_t c; if (x_read_full(fd, &c, 1) < 0) return -1; }
|
||||
return (int)n;
|
||||
}
|
||||
|
||||
int ipc_connect_tmo(const char *sock, int tmo_ms)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (fd < 0) return -1;
|
||||
struct sockaddr_un a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.sun_family = AF_UNIX;
|
||||
strncpy(a.sun_path, sock, sizeof a.sun_path - 1);
|
||||
int fl = fcntl(fd, F_GETFL, 0);
|
||||
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
|
||||
int rc = connect(fd, (struct sockaddr *)&a, sizeof a);
|
||||
if (rc < 0 && errno == EINPROGRESS) {
|
||||
struct pollfd pf = { fd, POLLOUT, 0 };
|
||||
int pr = poll(&pf, 1, tmo_ms);
|
||||
if (pr <= 0) { close(fd); return -1; }
|
||||
int soerr = 0;
|
||||
socklen_t sl = sizeof soerr;
|
||||
getsockopt(fd, SOL_SOCKET, SO_ERROR, &soerr, &sl);
|
||||
if (soerr != 0) { close(fd); return -1; }
|
||||
} else if (rc < 0) { close(fd); return -1; }
|
||||
fcntl(fd, F_SETFL, fl);
|
||||
/* 读/写超时兜底(正常服务应即时响应) */
|
||||
struct timeval tv = { tmo_ms / 1000, (tmo_ms % 1000) * 1000 };
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv);
|
||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv);
|
||||
return fd;
|
||||
}
|
||||
|
||||
int ipc_hello(int fd)
|
||||
{
|
||||
m_hello_t h;
|
||||
memset(&h, 0, sizeof h);
|
||||
strncpy(h.app_id, "os.meow.panel", sizeof h.app_id - 1);
|
||||
h.version = 1;
|
||||
if (send_frame(fd, MSG_HELLO, 1, &h, sizeof h) < 0) return -1;
|
||||
uint8_t buf[64];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < (int)sizeof(m_ack_t)) return -1;
|
||||
if (type != MSG_ACK) return -1;
|
||||
m_ack_t a;
|
||||
memcpy(&a, buf, sizeof a);
|
||||
return (a.code == MEOW_OK) ? 0 : -1;
|
||||
}
|
||||
|
||||
const char *perf_code_str(int code)
|
||||
{
|
||||
switch (code) {
|
||||
case MEOW_OK: return ""; /* 不应使用 */
|
||||
case MEOW_ERR_INVAL: return "参数非法";
|
||||
case MEOW_ERR_IO: return "读取失败";
|
||||
case MEOW_ERR_NODEV: return "设备不存在";
|
||||
default: return "服务端错误";
|
||||
}
|
||||
}
|
||||
|
||||
int perf_subset_for_path(const char *path)
|
||||
{
|
||||
if (!strcmp(path, "/api/perf")) return PERF_SUBSET_PERF;
|
||||
if (!strcmp(path, "/api/system")) return 0; /* system 需 uptime/storage,取全量 */
|
||||
return 0; /* /api/status */
|
||||
}
|
||||
|
||||
int ipc_perf_query(int subset, char *out, int cap)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_perf, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return MEOW_ERR_NODEV;
|
||||
int rc = MEOW_ERR_INTERNAL;
|
||||
char msg[128] = "";
|
||||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; snprintf(msg, sizeof msg, "握手中断:性能服务未就绪"); goto done; }
|
||||
m_perf_query_t q;
|
||||
memset(&q, 0, sizeof q);
|
||||
q.subset = (uint32_t)subset;
|
||||
if (send_frame(fd, MSG_PERF_QUERY, 2, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "发送查询失败"); goto done; }
|
||||
uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < (int)sizeof(m_perf_result_t)) { rc = MEOW_ERR_IO; snprintf(msg, sizeof msg, "应答帧过短"); goto done; }
|
||||
if (type != MSG_PERF_RESULT) { rc = MEOW_ERR_INVAL; snprintf(msg, sizeof msg, "非预期应答类型 0x%04x", type); goto done; }
|
||||
m_perf_result_t r;
|
||||
memcpy(&r, buf, sizeof r);
|
||||
if (r.code != MEOW_OK) { rc = (int)r.code; snprintf(msg, sizeof msg, "性能服务错误: %s", perf_code_str((int)r.code)); goto done; }
|
||||
uint16_t jl = r.json_len;
|
||||
if (jl > sizeof r.json - 1) jl = (uint16_t)(sizeof r.json - 1);
|
||||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||||
memcpy(out, r.json, jl);
|
||||
out[jl] = 0;
|
||||
rc = MEOW_OK;
|
||||
done:
|
||||
close(fd);
|
||||
/* 服务不可达/非 OK 由调用方据此返回错误;绝不静默失败、绝不降级直连 /proc。 */
|
||||
return rc;
|
||||
}
|
||||
|
||||
const char *power_code_str(int code)
|
||||
{
|
||||
switch (code) {
|
||||
case MEOW_OK: return "";
|
||||
case MEOW_ERR_PERM: return "无权限(需 root)";
|
||||
case MEOW_ERR_INVAL: return "未知命令";
|
||||
case MEOW_ERR_NODEV: return "设备/能力不支持";
|
||||
case MEOW_ERR_NOENT: return "缺少升级文件";
|
||||
case MEOW_ERR_IO: return "读写失败";
|
||||
default: return "电源服务错误";
|
||||
}
|
||||
}
|
||||
|
||||
int ipc_power_action(uint32_t cmd, uint32_t flag, const char *arg, m_power_status_t *out_st)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_power, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return MEOW_ERR_NODEV;
|
||||
int rc = MEOW_ERR_INTERNAL;
|
||||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||||
m_power_action_t a;
|
||||
memset(&a, 0, sizeof a);
|
||||
a.cmd = cmd;
|
||||
a.flag = flag;
|
||||
if (arg) snprintf(a.arg, sizeof a.arg, "%s", arg);
|
||||
if (send_frame(fd, MSG_POWER_ACTION, 3, &a, sizeof a) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||||
uint8_t buf[256];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < (int)sizeof(m_power_status_t)) { rc = MEOW_ERR_IO; goto done; }
|
||||
if (type != MSG_POWER_STATUS) { rc = MEOW_ERR_INVAL; goto done; }
|
||||
m_power_status_t st;
|
||||
memcpy(&st, buf, sizeof st);
|
||||
if (out_st) *out_st = st;
|
||||
rc = (int)st.code;
|
||||
done:
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void svc_msg_kv(const char *msg, const char *key, char *out, size_t cap)
|
||||
{
|
||||
out[0] = 0;
|
||||
if (!msg || !key) return;
|
||||
size_t kl = strlen(key);
|
||||
if (strncmp(msg, key, kl) == 0 && msg[kl] == '=' && msg[kl + 1])
|
||||
snprintf(out, cap, "%s", msg + kl + 1);
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/* mp_json.c — 最简 JSON 解析器/序列化器(jv_t 树;零第三方库)。
|
||||
* 模块化拆分自 meowpanel.c(接口/行为不变)。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
int json_strict_balanced(const char *b, size_t n)
|
||||
{
|
||||
int depth = 0, instr = 0, esc = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
char c = b[i];
|
||||
if (instr) {
|
||||
if (esc) esc = 0;
|
||||
else if (c == '\\') esc = 1;
|
||||
else if (c == '"') instr = 0;
|
||||
continue;
|
||||
}
|
||||
if (c == '"') { instr = 1; continue; }
|
||||
if (c == '{' || c == '[') depth++;
|
||||
else if (c == '}' || c == ']') { if (--depth < 0) return 0; }
|
||||
}
|
||||
return depth == 0 && !instr;
|
||||
}
|
||||
|
||||
jv_t *jv_alloc(int type)
|
||||
{
|
||||
jv_t *v = calloc(1, sizeof *v);
|
||||
if (v) v->type = type;
|
||||
return v;
|
||||
}
|
||||
|
||||
void jv_free(jv_t *v)
|
||||
{
|
||||
if (!v) return;
|
||||
jv_t *c = v->child;
|
||||
while (c) {
|
||||
jv_t *nx = c->next;
|
||||
if (v->type == J_ARR) { jv_free(c); } /* 数组元素是完整值(可为对象/嵌套数组),须整体递归释放 */
|
||||
else { jv_free(c->child); free(c->str); free(c); } /* 对象:pair 链 → 释放 value + key + pair 节点 */
|
||||
c = nx;
|
||||
}
|
||||
free(v->str);
|
||||
free(v);
|
||||
}
|
||||
|
||||
void jvp_skip_ws(jvp_t *t)
|
||||
{
|
||||
while (t->p < t->end && (*t->p == ' ' || *t->p == '\t' || *t->p == '\n' || *t->p == '\r')) t->p++;
|
||||
}
|
||||
|
||||
char *jv_parse_string(jvp_t *t)
|
||||
{
|
||||
if (t->p >= t->end || *t->p != '"') { t->err = 1; return NULL; }
|
||||
t->p++;
|
||||
sbuf_t s = {0};
|
||||
while (t->p < t->end && *t->p != '"') {
|
||||
unsigned char c = (unsigned char)*t->p++;
|
||||
if (c == '\\') {
|
||||
if (t->p >= t->end) { t->err = 1; break; }
|
||||
char e = *t->p++;
|
||||
switch (e) {
|
||||
case 'n': sA(&s, "\n"); break;
|
||||
case 't': sA(&s, "\t"); break;
|
||||
case 'r': sA(&s, "\r"); break;
|
||||
case 'b': sA(&s, "\b"); break;
|
||||
case 'f': sA(&s, "\f"); break;
|
||||
case 'u': {
|
||||
if (t->end - t->p < 4) { t->err = 1; sbuf_free(&s); return NULL; }
|
||||
unsigned int cp = 0;
|
||||
for (int i = 0; i < 4; i++) { char h = *t->p++; cp <<= 4; if (h>='0'&&h<='9') cp|=h-'0'; else if (h>='a'&&h<='f') cp|=h-'a'+10; else if (h>='A'&&h<='F') cp|=h-'A'+10; else { t->err=1; sbuf_free(&s); return NULL; } }
|
||||
/* 只支持 BMP 常见字符;代理对按 UTF-8 重算(简化:cp<0x80 单字节,否则保守转发) */
|
||||
if (cp < 0x80) sA(&s, "%c", (int)cp);
|
||||
else if (cp < 0x800) { sA(&s, "%c", 0xc0|(cp>>6)); sA(&s, "%c", 0x80|(cp&0x3f)); }
|
||||
else { sA(&s, "%c", 0xe0|(cp>>12)); sA(&s, "%c", 0x80|((cp>>6)&0x3f)); sA(&s, "%c", 0x80|(cp&0x3f)); }
|
||||
break; }
|
||||
default: sA(&s, "%c", (int)e); break;
|
||||
}
|
||||
} else if (c < 0x20) { t->err = 1; break; }
|
||||
else sA(&s, "%c", (int)c);
|
||||
}
|
||||
if (t->p >= t->end || *t->p != '"') { t->err = 1; sbuf_free(&s); return NULL; }
|
||||
t->p++;
|
||||
if (!s.b) { s.b = calloc(1, 1); }
|
||||
return s.b;
|
||||
}
|
||||
|
||||
jv_t *jv_parse_object(jvp_t *t)
|
||||
{
|
||||
t->p++;
|
||||
jv_t *obj = jv_alloc(J_OBJ);
|
||||
if (!obj) { t->err = 1; return NULL; }
|
||||
jv_t **tail = &obj->child;
|
||||
jvp_skip_ws(t);
|
||||
if (t->p < t->end && *t->p == '}') { t->p++; return obj; }
|
||||
for (;;) {
|
||||
jvp_skip_ws(t);
|
||||
if (t->p >= t->end) { t->err = 1; jv_free(obj); return NULL; }
|
||||
char *key = jv_parse_string(t);
|
||||
if (!key) { jv_free(obj); return NULL; }
|
||||
jvp_skip_ws(t);
|
||||
if (t->p >= t->end || *t->p != ':') { t->err = 1; free(key); jv_free(obj); return NULL; }
|
||||
t->p++;
|
||||
jv_t *val = jv_parse_value(t);
|
||||
if (!val) { t->err = 1; free(key); jv_free(obj); return NULL; }
|
||||
jv_t *pair = calloc(1, sizeof *pair);
|
||||
if (!pair) { t->err = 1; free(key); jv_free(val); jv_free(obj); return NULL; }
|
||||
pair->type = J_OBJ;
|
||||
pair->str = key;
|
||||
pair->child = val;
|
||||
*tail = pair;
|
||||
tail = &pair->next;
|
||||
jvp_skip_ws(t);
|
||||
if (t->p < t->end && *t->p == ',') { t->p++; continue; }
|
||||
if (t->p < t->end && *t->p == '}') { t->p++; return obj; }
|
||||
t->err = 1;
|
||||
jv_free(obj);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
jv_t *jv_parse_array(jvp_t *t)
|
||||
{
|
||||
t->p++;
|
||||
jv_t *arr = jv_alloc(J_ARR);
|
||||
if (!arr) { t->err = 1; return NULL; }
|
||||
jv_t **tail = &arr->child;
|
||||
jvp_skip_ws(t);
|
||||
if (t->p < t->end && *t->p == ']') { t->p++; return arr; }
|
||||
for (;;) {
|
||||
jv_t *v = jv_parse_value(t);
|
||||
if (!v) { jv_free(arr); return NULL; }
|
||||
*tail = v;
|
||||
tail = &v->next;
|
||||
jvp_skip_ws(t);
|
||||
if (t->p < t->end && *t->p == ',') { t->p++; continue; }
|
||||
if (t->p < t->end && *t->p == ']') { t->p++; return arr; }
|
||||
t->err = 1;
|
||||
jv_free(arr);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
jv_t *jv_parse_value(jvp_t *t)
|
||||
{
|
||||
jvp_skip_ws(t);
|
||||
if (t->p >= t->end) { t->err = 1; return NULL; }
|
||||
char c = *t->p;
|
||||
if (c == '{') return jv_parse_object(t);
|
||||
if (c == '[') return jv_parse_array(t);
|
||||
if (c == '"') {
|
||||
char *s = jv_parse_string(t);
|
||||
if (!s) return NULL;
|
||||
jv_t *v = jv_alloc(J_STR);
|
||||
if (!v) { free(s); t->err = 1; return NULL; }
|
||||
v->str = s;
|
||||
return v;
|
||||
}
|
||||
if (!strncmp(t->p, "true", 4)) { t->p += 4; jv_t *v = jv_alloc(J_BOOL); if (v) v->bval = 1; return v; }
|
||||
if (!strncmp(t->p, "false", 5)) { t->p += 5; return jv_alloc(J_BOOL); }
|
||||
if (!strncmp(t->p, "null", 4)) { t->p += 4; return jv_alloc(J_NULL); }
|
||||
/* number */
|
||||
const char *q = t->p;
|
||||
int isint = 1;
|
||||
while (q < t->end && (*q == '-' || *q == '+' || *q == '.' || *q == 'e' || *q == 'E' || isdigit((unsigned char)*q))) {
|
||||
if (*q == '.' || *q == 'e' || *q == 'E') isint = 0;
|
||||
q++;
|
||||
}
|
||||
if (q == t->p) { t->err = 1; return NULL; }
|
||||
char tmp[64];
|
||||
size_t dlen = (size_t)(q - t->p);
|
||||
if (dlen >= sizeof tmp) dlen = sizeof tmp - 1;
|
||||
memcpy(tmp, t->p, dlen);
|
||||
tmp[dlen] = 0;
|
||||
t->p = q;
|
||||
jv_t *v = jv_alloc(J_NUM);
|
||||
if (!v) { t->err = 1; return NULL; }
|
||||
v->dnum = strtod(tmp, NULL);
|
||||
v->inum = (long long)strtoll(tmp, NULL, 10);
|
||||
(void)isint;
|
||||
return v;
|
||||
}
|
||||
|
||||
jv_t *jv_parse(const char *text, int len)
|
||||
{
|
||||
jvp_t t;
|
||||
t.p = text;
|
||||
t.end = text + len;
|
||||
t.err = 0;
|
||||
jv_t *v = jv_parse_value(&t);
|
||||
if (!v || t.err) { if (v) jv_free(v); return NULL; }
|
||||
return v;
|
||||
}
|
||||
|
||||
jv_t *jv_obj_get(const jv_t *obj, const char *key)
|
||||
{
|
||||
if (!obj || obj->type != J_OBJ) return NULL;
|
||||
for (const jv_t *p = obj->child; p; p = p->next)
|
||||
if (p->str && !strcmp(p->str, key)) return p->child;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void jv_to_json(sbuf_t *s, const jv_t *v)
|
||||
{
|
||||
if (!v) { sA(s, "null"); return; }
|
||||
switch (v->type) {
|
||||
case J_NULL: sA(s, "null"); break;
|
||||
case J_BOOL: sA(s, v->bval ? "true" : "false"); break;
|
||||
case J_NUM:
|
||||
if (v->dnum == (double)v->inum) sA(s, "%lld", v->inum);
|
||||
else sA(s, "%g", v->dnum);
|
||||
break;
|
||||
case J_STR: sA_str(s, v->str ? v->str : ""); break;
|
||||
case J_ARR:
|
||||
sA(s, "[");
|
||||
{ int f = 1; for (const jv_t *e = v->child; e; e = e->next) { if (!f) sA(s, ","); f = 0; jv_to_json(s, e); } }
|
||||
sA(s, "]");
|
||||
break;
|
||||
case J_OBJ:
|
||||
sA(s, "{");
|
||||
{ int f2 = 1;
|
||||
for (const jv_t *p = v->child; p; p = p->next) {
|
||||
if (!f2) { sA(s, ","); }
|
||||
f2 = 0;
|
||||
sA_str(s, p->str); sA(s, ":"); jv_to_json(s, p->child);
|
||||
} }
|
||||
sA(s, "}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int is_hex(char c) { return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'); }
|
||||
|
||||
int hexval(char c) { if (c >= '0' && c <= '9') return c - '0'; if (c >= 'a' && c <= 'f') return c - 'a' + 10; return c - 'A' + 10; }
|
||||
@@ -0,0 +1,126 @@
|
||||
/* mp_key.c — 远程控制域:按键注入(写 /dev/input/event* EV_KEY,物理键红线内)。 */
|
||||
#include "mp.h"
|
||||
|
||||
#define PWR_KEYDEV "/dev/input/event0"
|
||||
#define ADC_KEYDEV "/dev/input/event2"
|
||||
|
||||
|
||||
int keycode_to_device(int code, const char **dev)
|
||||
{
|
||||
switch (code) {
|
||||
case KEY_POWER: *dev = PWR_KEYDEV; return KEY_POWER;
|
||||
case KEY_PREVIOUSSONG: *dev = PWR_KEYDEV; return KEY_PREVIOUSSONG;
|
||||
case KEY_VOLUMEUP: *dev = ADC_KEYDEV; return KEY_VOLUMEUP;
|
||||
case KEY_VOLUMEDOWN: *dev = ADC_KEYDEV; return KEY_VOLUMEDOWN;
|
||||
case KEY_NEXTSONG: *dev = ADC_KEYDEV; return KEY_NEXTSONG;
|
||||
case KEY_PLAYPAUSE: *dev = ADC_KEYDEV; return KEY_PLAYPAUSE;
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
int inject_key_event(const char *dev, int code, int down)
|
||||
{
|
||||
int fd = open(dev, O_WRONLY | O_NONBLOCK);
|
||||
if (fd < 0) return -1;
|
||||
struct input_event ev;
|
||||
memset(&ev, 0, sizeof ev);
|
||||
ev.type = EV_KEY;
|
||||
ev.code = code;
|
||||
ev.value = down ? 1 : 0;
|
||||
int ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev);
|
||||
if (ok) {
|
||||
memset(&ev, 0, sizeof ev);
|
||||
ev.type = EV_SYN;
|
||||
ev.code = SYN_REPORT;
|
||||
ev.value = 0;
|
||||
ok = (write(fd, &ev, sizeof ev) == (ssize_t)sizeof ev);
|
||||
}
|
||||
close(fd);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
int inject_key_release(int code, int press_ms)
|
||||
{
|
||||
const char *dev = NULL;
|
||||
int kc = keycode_to_device(code, &dev);
|
||||
if (kc < 0) return -1;
|
||||
int release_ms = press_ms > 0 ? press_ms : 0;
|
||||
if (inject_key_event(dev, code, 1) < 0) return -1;
|
||||
if (release_ms > 0) usleep((useconds_t)release_ms * 1000);
|
||||
else usleep(80 * 1000);
|
||||
if (inject_key_event(dev, code, 0) < 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_key_single(int sock, int code, const req_t *r)
|
||||
{
|
||||
int press_ms = 0;
|
||||
char pressbuf[16] = "tap";
|
||||
const char *press = pressbuf;
|
||||
if (r->body && r->body[0]) {
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (root) {
|
||||
jv_t *p = jv_obj_get(root, "press");
|
||||
if (p && p->type == J_STR) snprintf(pressbuf, sizeof pressbuf, "%s", p->str);
|
||||
jv_t *m = jv_obj_get(root, "ms");
|
||||
if (m && m->type == J_NUM) press_ms = (int)m->inum;
|
||||
jv_free(root);
|
||||
}
|
||||
}
|
||||
if (!strcmp(press, "hold"))
|
||||
return send_err(sock, 501, "NOT_IMPLEMENTED", "hold 持续注入需常驻通道(阶段 D),后端未实现");
|
||||
if (strcmp(press, "tap") && strcmp(press, "long"))
|
||||
return send_err(sock, 400, "BAD_REQUEST", "press 非法");
|
||||
if (press_ms <= 0) {
|
||||
if (!strcmp(press, "long")) press_ms = 3000;
|
||||
}
|
||||
/* 物理键红线:禁止映射 返回(158)/主页(172)/确认/exit(APPS.md)。 */
|
||||
if (code == 158 || code == 172 || code == 32 || code == 28 || code == 96)
|
||||
return send_err(sock, 403, "FORBIDDEN", "该键码(返回/主页/确认)按物理键红线禁止映射");
|
||||
const char *tmpdev = NULL;
|
||||
int dispatchable = (keycode_to_device(code, &tmpdev) >= 0);
|
||||
if (!dispatchable)
|
||||
return send_err(sock, 422, "UNSUPPORTED", "不支持的键码(仅音量/电源/播放/上下曲)");
|
||||
if (inject_key_release(code, press_ms) < 0)
|
||||
return send_err(sock, 500, "INTERNAL", "按键注入失败(无法打开输入设备)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"code\":%d,\"press\":\"%s\",\"dispatched\":true}", code, press);
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int api_key_batch(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *keys = jv_obj_get(root, "keys");
|
||||
if (!keys || keys->type != J_ARR) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺 keys 数组"); }
|
||||
sbuf_t s = {0};
|
||||
int first = 1;
|
||||
for (const jv_t *k = keys->child; k; k = k->next) {
|
||||
if (k->type != J_OBJ) continue;
|
||||
jv_t *c = jv_obj_get(k, "code");
|
||||
jv_t *p = jv_obj_get(k, "press");
|
||||
jv_t *m = jv_obj_get(k, "ms");
|
||||
if (!c || c->type != J_NUM) continue;
|
||||
int code = (int)c->inum;
|
||||
if (code == 158 || code == 172) continue; /* 红线跳过 */
|
||||
int press_ms = (m && m->type == J_NUM) ? (int)m->inum : 0;
|
||||
if (p && p->type == J_STR && !strcmp(p->str, "long") && press_ms <= 0) press_ms = 3000;
|
||||
const char *tmpdev = NULL;
|
||||
if (keycode_to_device(code, &tmpdev) < 0) continue;
|
||||
if (inject_key_release(code, press_ms) < 0) continue;
|
||||
if (!first) sA(&s, ",");
|
||||
first = 0;
|
||||
sA(&s, "%d", code);
|
||||
}
|
||||
jv_free(root);
|
||||
sbuf_t o = {0};
|
||||
if (first) sA(&o, "{\"dispatched\":[]}");
|
||||
else sA(&o, "{\"dispatched\":[%s]}", s.b);
|
||||
int ret = send_json(sock, 200, o.b ? o.b : "");
|
||||
sbuf_free(&s); sbuf_free(&o);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/* mp_logs.c — 日志域:/api/logs 列表 + 读 + 崩溃汇总(meow_crash 落盘格式)。 */
|
||||
#include "mp.h"
|
||||
|
||||
#define CRASH_TAIL_MAX (256u * 1024u) /* 崩溃日志单次读取上限 */
|
||||
#define CRASH_ENT_MAX 64 /* 实体汇总上限 */
|
||||
#define CRASH_DMESG_TAIL 5 /* dmesg 保留最近行数 */
|
||||
|
||||
|
||||
const char *log_group(const char *name)
|
||||
{
|
||||
if (!strncmp(name, "crash_", 6)) return "crash";
|
||||
if (!strncmp(name, "ams_", 4)) return "ams";
|
||||
if (!strncmp(name, "audio_monitor_", 14)) return "audio_monitor";
|
||||
if (!strncmp(name, "net_", 4)) return "net";
|
||||
if (!strncmp(name, "power_", 6)) return "power";
|
||||
if (!strncmp(name, "meow_", 5)) return "system";
|
||||
return "app";
|
||||
}
|
||||
|
||||
int api_logs_list(int sock)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
DIR *d = opendir(LOGS_DIR);
|
||||
if (!d) return send_err(sock, 404, "NOT_FOUND", "日志目录不存在");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dir\":");
|
||||
sA_str(&s, LOGS_DIR);
|
||||
sA(&s, ",\"files\":[");
|
||||
int first = 1;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (!strcmp(e->d_name, ".") || !strcmp(e->d_name, "..")) continue;
|
||||
if (is_sensitive_name(e->d_name)) continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||||
if (!first) sA(&s, ",");
|
||||
first = 0;
|
||||
sA(&s, "{\"name\":");
|
||||
sA_str(&s, e->d_name);
|
||||
sA(&s, ",\"size\":%lld,\"mtime\":%lld,\"group\":", (long long)st.st_size, (long long)st.st_mtime);
|
||||
sA_str(&s, log_group(e->d_name));
|
||||
sA(&s, "}");
|
||||
}
|
||||
closedir(d);
|
||||
sA(&s, "]}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_logs_read(int sock, const req_t *r)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
char file[512], lines[16];
|
||||
if (!qparam(r->query, "file", file, sizeof file))
|
||||
return send_err(sock, 400, "BAD_REQUEST", "缺 file 参数");
|
||||
int maxlines = 200;
|
||||
if (qparam(r->query, "lines", lines, sizeof lines)) {
|
||||
int n = atoi(lines);
|
||||
if (n > 0) maxlines = n;
|
||||
if (maxlines > LOG_MAX_LINES) maxlines = LOG_MAX_LINES;
|
||||
}
|
||||
/* 防逃逸:file 相对 LOGS_DIR,禁止 .. 与绝对路径 */
|
||||
if (file[0] == '/' || strstr(file, "..")) return send_err(sock, 403, "FORBIDDEN", "文件路径逃逸");
|
||||
if (is_sensitive_name(strrchr(file, '/') ? strrchr(file, '/') + 1 : file))
|
||||
return send_err(sock, 403, "FORBIDDEN", "敏感文件");
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, file);
|
||||
int fd = open(full, O_RDONLY);
|
||||
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "日志文件不存在");
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
|
||||
/* 逐行读,保留末尾 maxlines 行 */
|
||||
const char *buf = malloc((size_t)st.st_size + 1);
|
||||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||||
ssize_t n = read(fd, (char *)buf, (size_t)st.st_size);
|
||||
close(fd);
|
||||
if (n < 0) { free((void *)buf); return send_err(sock, 500, "INTERNAL", "读日志失败"); }
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"file\":");
|
||||
sA_str(&s, file);
|
||||
sA(&s, ",\"lines\":[");
|
||||
/* 直接扫一遍,保留末尾 maxlines 行文本 */
|
||||
const char *p = buf;
|
||||
const char *end = buf + n;
|
||||
int total = 0, truncated = 0;
|
||||
int gotlines = 0;
|
||||
int linecnt = 0;
|
||||
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
|
||||
if (linecnt > 0 && end[-1] != '\n') linecnt++;
|
||||
int startrow = linecnt - maxlines;
|
||||
if (startrow < 0) startrow = 0;
|
||||
if (startrow > 0) truncated = 1;
|
||||
int rowidx = 0;
|
||||
p = buf;
|
||||
while (p < end) {
|
||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char *line_end = nl ? nl : end;
|
||||
if (rowidx >= startrow) {
|
||||
if (gotlines) sA(&s, ",");
|
||||
/* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */
|
||||
sA_str_n(&s, p, (size_t)(line_end - p));
|
||||
gotlines++;
|
||||
}
|
||||
rowidx++;
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
total = linecnt;
|
||||
free((void *)buf);
|
||||
sA(&s, "],\"total\":%d,\"truncated\":%s}", total, truncated ? "true" : "false");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_logs_crash(int sock)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
/* 找最新 crash_*.log(按 mtime);无 → 空汇总(不 404,不谎报)。 */
|
||||
DIR *d = opendir(LOGS_DIR);
|
||||
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}");
|
||||
char best[512] = ""; long best_mt = -1;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (strncmp(e->d_name, "crash_", 6) != 0 || e->d_name[0] == '.') continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||||
if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
|
||||
}
|
||||
closedir(d);
|
||||
if (!best[0])
|
||||
return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}");
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
|
||||
int fd = open(full, O_RDONLY);
|
||||
struct stat st;
|
||||
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { if (fd >= 0) close(fd); return send_err(sock, 500, "INTERNAL", "崩溃日志不可读"); }
|
||||
off_t off = st.st_size > (off_t)CRASH_TAIL_MAX ? st.st_size - (off_t)CRASH_TAIL_MAX : 0;
|
||||
size_t buflen = (size_t)(st.st_size - off);
|
||||
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"count\":0,\"dmesg_count\":0,\"dmesg_tail\":[],\"entities\":[]}"); }
|
||||
if (off) (void)lseek(fd, off, SEEK_SET);
|
||||
char *buf = malloc(buflen + 1);
|
||||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||||
ssize_t n = read(fd, buf, buflen);
|
||||
close(fd);
|
||||
if (n < 0) { free(buf); return send_err(sock, 500, "INTERNAL", "读崩溃日志失败"); }
|
||||
buf[n] = 0;
|
||||
|
||||
/* 每实体聚合(svc 行);dmesg 单独计数 + 留最近几行。
|
||||
* 有界缓冲(det ≤ 127 字符)保证 snprintf 到 name/detail 不越界且 -Wformat-truncation 零告警。 */
|
||||
struct { char name[128]; int count; long long last_epoch; char kind[8]; int code; char detail[160]; } ents[CRASH_ENT_MAX];
|
||||
int entn = 0, svc_count = 0, dmesg_count = 0;
|
||||
char dmesg_tail[CRASH_DMESG_TAIL][128]; int dmesg_tail_n = 0;
|
||||
char *ln = buf;
|
||||
while (*ln && (*ln == '\n')) ln++;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
long long epoch = 0, up = 0; char src[16] = "", det[128] = "";
|
||||
int f = sscanf(ln, "%lld %lld %15s %127[^\n]", &epoch, &up, src, det);
|
||||
if (f >= 3) {
|
||||
if (!strcmp(src, "svc")) {
|
||||
svc_count++;
|
||||
/* det = `<name> exit=<code>` 或 `<name> signal=<sig>`;name 为第一 token(就地切分) */
|
||||
char name[128] = "", kind[8] = ""; int code = 0, k2 = 0;
|
||||
char *sp = strchr(det, ' ');
|
||||
if (sp) {
|
||||
*sp = 0;
|
||||
snprintf(name, sizeof name, "%s", det); /* det ≤127 → name[128] 不截断 */
|
||||
char *kp = sp + 1;
|
||||
if (!strncmp(kp, "exit=", 5)) { snprintf(kind, sizeof kind, "exit"); code = atoi(kp + 5); k2 = 1; }
|
||||
else if (!strncmp(kp, "signal=", 7)) { snprintf(kind, sizeof kind, "sig"); code = atoi(kp + 7); k2 = 1; }
|
||||
else { snprintf(kind, sizeof kind, "crash"); k2 = 0; }
|
||||
} else {
|
||||
snprintf(name, sizeof name, "%s", det);
|
||||
snprintf(kind, sizeof kind, "crash");
|
||||
}
|
||||
/* 聚合到 ents(按 name 匹配;超限则丢弃剩余陌生实体,仅计入总数) */
|
||||
int found = -1;
|
||||
for (int i = 0; i < entn; i++) if (!strcmp(ents[i].name, name)) { found = i; break; }
|
||||
if (found < 0 && entn < CRASH_ENT_MAX) {
|
||||
found = entn++;
|
||||
snprintf(ents[found].name, sizeof ents[found].name, "%s", name);
|
||||
ents[found].count = 0; ents[found].last_epoch = 0;
|
||||
ents[found].kind[0] = 0; ents[found].code = 0; ents[found].detail[0] = 0;
|
||||
}
|
||||
if (found >= 0) {
|
||||
ents[found].count++;
|
||||
if (epoch >= ents[found].last_epoch) {
|
||||
ents[found].last_epoch = epoch;
|
||||
snprintf(ents[found].kind, sizeof ents[found].kind, "%s", k2 ? kind : "crash");
|
||||
ents[found].code = code;
|
||||
snprintf(ents[found].detail, sizeof ents[found].detail, "%s", det);
|
||||
}
|
||||
}
|
||||
} else if (!strcmp(src, "dmesg")) {
|
||||
dmesg_count++;
|
||||
if (dmesg_tail_n < CRASH_DMESG_TAIL) snprintf(dmesg_tail[dmesg_tail_n++], sizeof dmesg_tail[0], "%s", det);
|
||||
else { /* 前移一行,末尾放最新 */ for (int i = 0; i < dmesg_tail_n - 1; i++) memcpy(dmesg_tail[i], dmesg_tail[i + 1], sizeof dmesg_tail[0]); snprintf(dmesg_tail[dmesg_tail_n - 1], sizeof dmesg_tail[0], "%s", det); }
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
while (*ln == '\n') ln++;
|
||||
}
|
||||
free(buf);
|
||||
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"file\":");
|
||||
sA_str(&s, best);
|
||||
sA(&s, ",\"count\":%d,\"dmesg_count\":%d,\"dmesg_tail\":[", svc_count, dmesg_count);
|
||||
for (int i = 0; i < dmesg_tail_n; i++) { if (i) sA(&s, ","); sA_str(&s, dmesg_tail[i]); }
|
||||
sA(&s, "],\"entities\":[");
|
||||
int first = 1;
|
||||
for (int i = 0; i < entn; i++) {
|
||||
if (!ents[i].count) continue;
|
||||
if (!first) sA(&s, ",");
|
||||
first = 0;
|
||||
sA(&s, "{\"name\":");
|
||||
sA_str(&s, ents[i].name);
|
||||
sA(&s, ",\"source\":\"svc\",\"count\":%d,\"last_epoch\":%lld,\"last_kind\":", ents[i].count, ents[i].last_epoch);
|
||||
sA_str(&s, ents[i].kind[0] ? ents[i].kind : "crash");
|
||||
sA(&s, ",\"last_code\":%d,\"last_detail\":", ents[i].code);
|
||||
sA_str(&s, ents[i].detail);
|
||||
/* 最近崩溃时间(epoch → 可读);strftime 本地时区,失败回退 epoch */
|
||||
char tbuf[32] = ""; time_t t = (time_t)ents[i].last_epoch;
|
||||
struct tm tmv;
|
||||
if (ents[i].last_epoch > 0 && localtime_r(&t, &tmv)) strftime(tbuf, sizeof tbuf, "%Y-%m-%d %H:%M:%S", &tmv);
|
||||
sA(&s, ",\"last_time\":");
|
||||
sA_str(&s, tbuf);
|
||||
sA(&s, "}");
|
||||
}
|
||||
sA(&s, "]}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,440 @@
|
||||
/* mp_net.c — 网络管理域:meow_netmgrd IPC(状态/连接/DNS/ifaces/policy,期2/期3)。 */
|
||||
#include "mp.h"
|
||||
|
||||
#define NET_LOG_TAIL_MAX (256 * 1024) /* net 日志 tail 读取上限 */
|
||||
|
||||
|
||||
int net_ipc(uint16_t req_type, const void *req, uint32_t req_len,
|
||||
uint16_t *ret_type, uint32_t *code, uint16_t *meta,
|
||||
char *out, size_t cap)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_net, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return -1;
|
||||
uint8_t buf[MEOW_IPC_MAX];
|
||||
uint16_t type = 0; uint32_t rid = 0;
|
||||
int rlen = -1;
|
||||
if (send_frame(fd, req_type, 0x1020, req, req_len) == 0)
|
||||
rlen = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
close(fd);
|
||||
if (rlen <= 0) return -1;
|
||||
if (ret_type) *ret_type = type;
|
||||
/* m_net_*_result 统一布局:code(u32)|len(u16)|meta/pad(u16)|content[len],content 偏移 8 */
|
||||
uint32_t c = 0; uint16_t ln = 0, mv = 0;
|
||||
memcpy(&c, buf, 4); memcpy(&ln, buf + 4, 2); memcpy(&mv, buf + 6, 2);
|
||||
if (code) *code = c;
|
||||
if (meta) *meta = mv;
|
||||
if ((size_t)ln >= cap) ln = (uint16_t)(cap - 1);
|
||||
if ((size_t)(8 + ln) < sizeof buf) buf[8 + ln] = 0;
|
||||
memcpy(out, buf + 8, ln);
|
||||
out[ln] = 0;
|
||||
return (int)ln;
|
||||
}
|
||||
|
||||
int api_net_status(int sock)
|
||||
{
|
||||
uint16_t t; uint32_t code; char json[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &t, &code, NULL, json, sizeof json);
|
||||
if (n <= 0 || t != MSG_NET_STATUS_RESULT || code != MEOW_OK || !json[0])
|
||||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||||
if (!utf8_valid_n(json, strlen(json))) /* 上游坏字节(含 ssid)→ 不返回,降级 */
|
||||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务应答包含非法字节");
|
||||
return send_json(sock, 200, json);
|
||||
}
|
||||
|
||||
int net_conns_parse_body(sbuf_t *s, const char *txt)
|
||||
{
|
||||
const char *p = txt;
|
||||
int first = 1, n = 0;
|
||||
while (p && *p) {
|
||||
const char *nl = strchr(p, '\n');
|
||||
size_t ll = nl ? (size_t)(nl - p) : strlen(p);
|
||||
if (ll == 0) { p = nl ? nl + 1 : p + 1; continue; }
|
||||
char line[384];
|
||||
if (ll >= sizeof line) ll = sizeof line - 1;
|
||||
memcpy(line, p, ll); line[ll] = 0;
|
||||
char *f[6]; int fn = 0; char *cur = line;
|
||||
while (fn < 6) {
|
||||
f[fn++] = cur;
|
||||
char *bar = strchr(cur, '|');
|
||||
if (!bar) break;
|
||||
*bar = 0; cur = bar + 1;
|
||||
}
|
||||
if (fn >= 6) {
|
||||
if (!first) sA(s, ",");
|
||||
first = 0;
|
||||
sA(s, "{\"entity\":"); sA_str(s, f[0]);
|
||||
sA(s, ",\"local\":"); sA_str(s, f[1]);
|
||||
sA(s, ",\"remote\":"); sA_str(s, f[2]);
|
||||
sA(s, ",\"state\":"); sA_str(s, f[3]);
|
||||
sA(s, ",\"age\":%lld,\"domain\":", atoll(f[4]));
|
||||
sA_str(s, strcmp(f[5], "-") == 0 ? "" : f[5]);
|
||||
sA(s, "}");
|
||||
n++;
|
||||
}
|
||||
p = nl ? nl + 1 : p + ll;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
int api_net_conns(int sock, const char *query)
|
||||
{
|
||||
m_net_conns_req_t q; memset(&q, 0, sizeof q);
|
||||
char ent[128];
|
||||
if (qparam(query, "entity", ent, sizeof ent))
|
||||
strncpy(q.entity, ent, sizeof q.entity - 1);
|
||||
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_CONNS_REQ, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_CONNS_RESULT || code != MEOW_OK)
|
||||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||||
sbuf_t a = {0};
|
||||
int count = net_conns_parse_body(&a, txt);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"truncated\":%s,\"count\":%d,\"conns\":[", meta ? "true" : "false", count);
|
||||
if (a.n) sA_append(&s, a.b, a.n);
|
||||
sA(&s, "]}");
|
||||
sbuf_free(&a);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int net_dns_obj_fill(sbuf_t *s)
|
||||
{
|
||||
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
|
||||
strcpy(q.cmd, "status");
|
||||
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_DNS_RESULT) return -1;
|
||||
if (code != MEOW_OK) return -2;
|
||||
/* txt: `dns mode=%s enabled=%d upstream=%s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d` */
|
||||
char mode[32] = "", upstream[64] = "";
|
||||
int enabled = 0, ch = 0, ct = 0, rmod = 0;
|
||||
unsigned long long qq = 0, deny = 0, first = 0;
|
||||
(void)sscanf(txt, "dns mode=%31s enabled=%d upstream=%63s cache=%d/%d q=%llu deny=%llu first=%llu resolv_modified=%d",
|
||||
mode, &enabled, upstream, &ch, &ct, &qq, &deny, &first, &rmod);
|
||||
sA(s, "{\"mode\":");
|
||||
sA_str(s, mode);
|
||||
sA(s, ",\"enabled\":%s,\"upstream\":", enabled ? "true" : "false");
|
||||
sA_str(s, upstream);
|
||||
sA(s, ",\"cache_hits\":%d,\"cache_total\":%d,\"queries\":%llu,\"denies\":%llu,\"first_seen\":%llu,\"resolv_modified\":%s}",
|
||||
ch, ct, qq, deny, first, rmod ? "true" : "false");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_net_dns(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
int rc = net_dns_obj_fill(&s);
|
||||
if (rc == -1) { sbuf_free(&s); return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用"); }
|
||||
if (rc == -2) { sbuf_free(&s); return send_err(sock, 503, "NET_NODEV", "DNS 代理不可用"); }
|
||||
int out = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return out;
|
||||
}
|
||||
|
||||
int net_block_fill(sbuf_t *s)
|
||||
{
|
||||
uint16_t nt; uint32_t ncode; char njson[MEOW_IPC_MAX];
|
||||
int nn = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &nt, &ncode, NULL, njson, sizeof njson);
|
||||
if (nn <= 0 || nt != MSG_NET_STATUS_RESULT || ncode != MEOW_OK || !njson[0]) return -1;
|
||||
/* 状态 JSON 为对象:剥外层大括号,拼入 DNS 子键(DNS 不可用则省略该键,仍回 net 状态,不谎报)。 */
|
||||
size_t l = strlen(njson);
|
||||
while (l > 0 && (njson[l - 1] == ' ' || njson[l - 1] == '\t' || njson[l - 1] == '\n' || njson[l - 1] == '\r')) njson[--l] = 0;
|
||||
if (l < 2 || njson[0] != '{' || njson[l - 1] != '}') return -1;
|
||||
if (!utf8_valid_n(njson, l)) return -1; /* 任一上游(netmgrd 含 ssid)产出非法 UTF-8 → 整块降级,不拖垮整页 */
|
||||
njson[l - 1] = 0; /* 去尾部 } */
|
||||
sbuf_t dns = {0};
|
||||
int drc = net_dns_obj_fill(&dns);
|
||||
sA(s, ",\"net\":{");
|
||||
sA(s, "%s", njson + 1); /* 去首 { 的状态内层 */
|
||||
if (drc == 0 && dns.b) sA(s, ",\"dns\":%s", dns.b);
|
||||
sA(s, "}"); /* net 块只开了一个 {,一个 } 收尾(批2 曾写 }} 多一括号,浏览器 Extra data 破页 2026-09-02) */
|
||||
sbuf_free(&dns);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_net_dns_flush(int sock)
|
||||
{
|
||||
m_net_dns_ctl_t q; memset(&q, 0, sizeof q);
|
||||
strcpy(q.cmd, "flush");
|
||||
uint16_t t; uint32_t code; uint16_t meta; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_DNS_CTL, &q, sizeof q, &t, &code, &meta, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_DNS_RESULT)
|
||||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||||
if (code == (uint32_t)MEOW_ERR_PERM)
|
||||
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "DNS flush 需系统身份");
|
||||
if (code != MEOW_OK)
|
||||
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS flush 失败");
|
||||
/* txt: `dns cache flushed (N entries)` */
|
||||
int entries = 0;
|
||||
(void)sscanf(txt, "dns cache flushed (%d entries)", &entries);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"flushed\":true,\"entries\":%d}", entries);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_net_log(int sock)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
DIR *d = opendir(LOGS_DIR);
|
||||
if (!d) return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||||
char best[512] = ""; long best_mt = -1;
|
||||
struct dirent *e;
|
||||
while ((e = readdir(d))) {
|
||||
if (strncmp(e->d_name, "net_", 4) != 0) continue;
|
||||
if (e->d_name[0] == '.') continue;
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, e->d_name);
|
||||
struct stat st;
|
||||
if (stat(full, &st) < 0 || !S_ISREG(st.st_mode)) continue;
|
||||
if (st.st_mtime > best_mt) { best_mt = st.st_mtime; snprintf(best, sizeof best, "%s", e->d_name); }
|
||||
}
|
||||
closedir(d);
|
||||
if (!best[0])
|
||||
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", LOGS_DIR, best);
|
||||
int fd = open(full, O_RDONLY);
|
||||
struct stat st;
|
||||
if (fd < 0 || fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) {
|
||||
if (fd >= 0) close(fd);
|
||||
return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}");
|
||||
}
|
||||
off_t off = st.st_size > NET_LOG_TAIL_MAX ? st.st_size - NET_LOG_TAIL_MAX : 0;
|
||||
size_t buflen = (size_t)(st.st_size - off);
|
||||
if (buflen == 0) { close(fd); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
|
||||
if (off) (void)lseek(fd, off, SEEK_SET);
|
||||
char *buf = malloc(buflen + 1);
|
||||
if (!buf) { close(fd); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||||
ssize_t n = read(fd, buf, buflen);
|
||||
close(fd);
|
||||
if (n <= 0) { free(buf); return send_json(sock, 200, "{\"file\":\"\",\"lines\":[],\"total\":0,\"truncated\":false}"); }
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"file\":");
|
||||
sA_str(&s, best);
|
||||
sA(&s, ",\"lines\":[");
|
||||
const char *p = buf, *end = buf + n;
|
||||
int linecnt = 0;
|
||||
for (const char *c = buf; c < end; c++) if (*c == '\n') linecnt++;
|
||||
if (linecnt > 0 && end[-1] != '\n') linecnt++;
|
||||
int maxlines = LOG_MAX_LINES;
|
||||
int startrow = linecnt - maxlines;
|
||||
if (startrow < 0) startrow = 0;
|
||||
int truncated = (off > 0 || startrow > 0) ? 1 : 0;
|
||||
int rowidx = 0, gotlines = 0;
|
||||
p = buf;
|
||||
while (p < end) {
|
||||
const char *nl = memchr(p, '\n', (size_t)(end - p));
|
||||
const char *line_end = nl ? nl : end;
|
||||
if (rowidx >= startrow) {
|
||||
if (gotlines) sA(&s, ",");
|
||||
/* sA_str_n:转义 + UTF-8 消毒(任意长度安全;勿 sA("%s")——单次超 511B 会截断坏 JSON) */
|
||||
sA_str_n(&s, p, (size_t)(line_end - p));
|
||||
gotlines++;
|
||||
}
|
||||
rowidx++;
|
||||
if (!nl) break;
|
||||
p = nl + 1;
|
||||
}
|
||||
free(buf);
|
||||
sA(&s, "],\"total\":%d,\"truncated\":%s}", linecnt, truncated ? "true" : "false");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_net_ifaces(int sock)
|
||||
{
|
||||
m_net_iface_req_t q; memset(&q, 0, sizeof q);
|
||||
uint16_t t; uint32_t code; char arr[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_IFACE_REQ, &q, sizeof q, &t, &code, NULL, arr, sizeof arr);
|
||||
if (n <= 0 || t != MSG_NET_IFACE_RESULT || code != MEOW_OK || !arr[0])
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
/* 上游接口数组由 netmgrd iface_json_obj 直出(name/kind/ip/gw/rx/tx,ASCII,无 ssid),仍校验坏字节。 */
|
||||
if (!utf8_valid_n(arr, strlen(arr)))
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
|
||||
/* 取 rssi + rfkill + dns:用 0xE0 状态 JSON(单一权威,含 wifi.rssi/rfkill[]/bt.dns[]),jv 提取。 */
|
||||
uint16_t st; uint32_t scode; char sj[MEOW_IPC_MAX];
|
||||
int sn2 = net_ipc(MSG_NET_STATUS_REQ, NULL, 0, &st, &scode, NULL, sj, sizeof sj);
|
||||
long long rssi = -9999; int got_rssi = 0;
|
||||
sbuf_t rfkill = {0}, dns = {0};
|
||||
if (sn2 > 0 && st == MSG_NET_STATUS_RESULT && scode == MEOW_OK && utf8_valid_n(sj, strlen(sj))) {
|
||||
jv_t *jr = jv_parse(sj, (int)strlen(sj));
|
||||
if (jr) {
|
||||
jv_t *w = jv_obj_get(jr, "wifi");
|
||||
jv_t *r = w ? jv_obj_get(w, "rssi") : NULL;
|
||||
if (r && r->type == J_NUM) { rssi = r->inum; got_rssi = 1; }
|
||||
jv_t *rf = jv_obj_get(jr, "rfkill");
|
||||
if (rf && rf->type == J_ARR) jv_to_json(&rfkill, rf);
|
||||
jv_t *bt = jv_obj_get(jr, "bt");
|
||||
jv_t *dn = bt ? jv_obj_get(bt, "dns") : NULL;
|
||||
if (dn && dn->type == J_ARR) jv_to_json(&dns, dn);
|
||||
jv_free(jr);
|
||||
}
|
||||
}
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"available\":true,\"rssi\":");
|
||||
if (got_rssi) sA(&s, "%lld", rssi); else sA(&s, "null");
|
||||
sA(&s, ",\"rfkill\":%s,\"dns\":%s,\"ifaces\":%s}", rfkill.b ? rfkill.b : "[]", dns.b ? dns.b : "[]", arr);
|
||||
sbuf_free(&rfkill); sbuf_free(&dns);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
void net_policy_parse(const char *txt, sbuf_t *s)
|
||||
{
|
||||
char lvl[24] = "", enf[24] = "dns_only";
|
||||
const char *p = txt;
|
||||
sA(s, "{\"available\":true,\"enforce\":");
|
||||
/* 首行找 enforce=... */
|
||||
/* enforce 后面紧跟全角「(」多字节说明,须在非 ASCII 字节处停(否则会吞「(SOCK_DESTROY...)」)。 */
|
||||
const char *en = strstr(p, "enforce=");
|
||||
if (en) {
|
||||
en += 8;
|
||||
int i = 0; while (en[i] && en[i] != ' ' && en[i] != '\n' && (unsigned char)en[i] < 0x80 && i < (int)sizeof enf - 1) { enf[i] = en[i]; i++; }
|
||||
enf[i] = 0;
|
||||
}
|
||||
const char *lg = strstr(p, "log_level=");
|
||||
if (lg) {
|
||||
lg += 10;
|
||||
int i = 0; while (lg[i] && lg[i] != ' ' && lg[i] != '\n' && (unsigned char)lg[i] < 0x80 && i < (int)sizeof lvl - 1) { lvl[i] = lg[i]; i++; }
|
||||
lvl[i] = 0;
|
||||
}
|
||||
sA_str(s, enf);
|
||||
sA(s, ",\"log_level\":");
|
||||
sA_str(s, lvl);
|
||||
sA(s, ",\"rules\":[");
|
||||
int first = 1;
|
||||
const char *ln = txt;
|
||||
while (ln && *ln) {
|
||||
const char *nl = strchr(ln, '\n');
|
||||
size_t ll = nl ? (size_t)(nl - ln) : strlen(ln);
|
||||
if (ll == 0) { ln = nl ? nl + 1 : ln + ll; continue; }
|
||||
/* 跳注释行(以 # 开头)与空;数据行为 `<entity> net=... domain=...`(entity 含冒号无空格) */
|
||||
const char *c = ln;
|
||||
while (c < ln + ll && (*c == ' ' || *c == '\t')) c++;
|
||||
int is_comment = (c < ln + ll && *c == '#');
|
||||
int is_none = (strncmp(c, "(none)", 6) == 0 || strncmp(c, "(allow)", 7) == 0);
|
||||
if (!is_comment && !is_none && c < ln + ll) {
|
||||
/* entity = 首个空白符之前 */
|
||||
char entity[64] = ""; size_t ei = 0;
|
||||
const char *e = c;
|
||||
while (e < ln + ll && *e != ' ' && *e != '\t' && ei < sizeof entity - 1) { entity[ei++] = *e++; }
|
||||
entity[ei] = 0;
|
||||
/* net=block|allow 与 domain=+h1,-h2 从 entity 之后扫 */
|
||||
char net[8] = "allow"; int has_net = 0;
|
||||
char allow_h[512] = "", deny_h[512] = "";
|
||||
const char *k = e;
|
||||
if (k < ln + ll) {
|
||||
const char *seg = k;
|
||||
while (seg < ln + ll && (*seg == ' ' || *seg == '\t')) seg++;
|
||||
const char *nh = strstr(seg, "net=");
|
||||
if (nh) { nh += 4; int i2 = 0; while (nh[i2] && nh[i2] != ' ' && nh[i2] != '\n' && i2 < 7) { net[i2] = nh[i2]; i2++; } net[i2] = 0; has_net = 1; }
|
||||
const char *dh = strstr(seg, "domain=");
|
||||
if (dh) {
|
||||
dh += 7;
|
||||
/* 逗号分隔;+ 前缀=allow,- 前缀=deny。主机列表内部以 ',' 拼接(供拆分回数组)。 */
|
||||
size_t ao = 0, do2 = 0;
|
||||
const char *h = dh;
|
||||
while (*h && *h != ' ' && *h != '\n' && *h != '\r') {
|
||||
if (*h == '+') {
|
||||
h++;
|
||||
if (ao && ao < sizeof allow_h - 1) allow_h[ao++] = ',';
|
||||
while (*h && *h != ',' && *h != ' ' && *h != '\n' && ao < sizeof allow_h - 1) allow_h[ao++] = *h++;
|
||||
} else if (*h == '-') {
|
||||
h++;
|
||||
if (do2 && do2 < sizeof deny_h - 1) deny_h[do2++] = ',';
|
||||
while (*h && *h != ',' && *h != ' ' && *h != '\n' && do2 < sizeof deny_h - 1) deny_h[do2++] = *h++;
|
||||
} else h++;
|
||||
if (*h == ',') h++;
|
||||
}
|
||||
allow_h[ao] = 0; deny_h[do2] = 0;
|
||||
}
|
||||
if (!first) sA(s, ",");
|
||||
first = 0;
|
||||
sA(s, "{\"entity\":"); sA_str(s, entity);
|
||||
sA(s, ",\"net\":"); sA_str(s, has_net ? net : "");
|
||||
sA(s, ",\"has_net\":%s", has_net ? "true" : "false");
|
||||
sA(s, ",\"allow_hosts\":[");
|
||||
{ const char *hp = allow_h; int hf = 1;
|
||||
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
|
||||
if (!hf) { sA(s, ","); }
|
||||
hf = 0; sA_str_n(s, hp, hl);
|
||||
hp = comma ? comma + 1 : hp + hl; } }
|
||||
sA(s, "],\"deny_hosts\":[");
|
||||
{ const char *hp = deny_h; int hf = 1;
|
||||
while (hp && *hp) { const char *comma = strchr(hp, ','); size_t hl = comma ? (size_t)(comma - hp) : strlen(hp);
|
||||
if (!hf) { sA(s, ","); }
|
||||
hf = 0; sA_str_n(s, hp, hl);
|
||||
hp = comma ? comma + 1 : hp + hl; } }
|
||||
sA(s, "]}");
|
||||
}
|
||||
}
|
||||
ln = nl ? nl + 1 : ln + ll;
|
||||
}
|
||||
sA(s, "]}");
|
||||
}
|
||||
|
||||
int api_net_policy_get(int sock)
|
||||
{
|
||||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||||
q.op = NET_POLICY_SHOW;
|
||||
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_POLICY_RESULT || code != MEOW_OK)
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
if (!utf8_valid_n(txt, strlen(txt))) /* 实体名/域名坏字节 → 降级 */
|
||||
return send_json(sock, 200, "{\"available\":false}");
|
||||
sbuf_t s = {0};
|
||||
net_policy_parse(txt, &s);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_net_policy_set(int sock, const char *body, size_t body_len)
|
||||
{
|
||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(body, (int)body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jo = jv_obj_get(root, "op"), *je = jv_obj_get(root, "entity"), *ja = jv_obj_get(root, "arg");
|
||||
char op[16] = "set_net", entity[64] = "", arg[64] = "";
|
||||
if (jo && jo->type == J_STR && jo->str) snprintf(op, sizeof op, "%s", jo->str);
|
||||
if (je && je->type == J_STR && je->str) snprintf(entity, sizeof entity, "%s", je->str);
|
||||
if (ja && ja->type == J_STR && ja->str) snprintf(arg, sizeof arg, "%s", ja->str);
|
||||
jv_free(root);
|
||||
if (!entity[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 entity(app:<pkg>/svc:<svc>/proc:<comm>)");
|
||||
if (!entity[0] || strchr(entity, '/')) return send_err(sock, 422, "UNSUPPORTED", "entity 非法");
|
||||
int nop = 0;
|
||||
if (!strcmp(op, "set_net")) nop = NET_POLICY_SET_NET;
|
||||
else if (!strcmp(op, "set_domain")) nop = NET_POLICY_SET_DOMAIN;
|
||||
else if (!strcmp(op, "del")) nop = NET_POLICY_DEL;
|
||||
else return send_err(sock, 422, "UNSUPPORTED", "op 须为 set_net|set_domain|del");
|
||||
if (nop != NET_POLICY_DEL && !arg[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 arg");
|
||||
|
||||
m_net_policy_t q; memset(&q, 0, sizeof q);
|
||||
q.op = (uint32_t)nop;
|
||||
snprintf(q.entity, sizeof q.entity, "%.47s", entity); /* entity[48] 留一个 NUL;防截断告警 */
|
||||
snprintf(q.arg, sizeof q.arg, "%.63s", arg); /* arg[64] 同理 */
|
||||
uint16_t t; uint32_t code; char txt[MEOW_IPC_MAX];
|
||||
int n = net_ipc(MSG_NET_POLICY, &q, sizeof q, &t, &code, NULL, txt, sizeof txt);
|
||||
if (n <= 0 || t != MSG_NET_POLICY_RESULT)
|
||||
return send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||||
if (code == (uint32_t)MEOW_ERR_PERM)
|
||||
return send_err(sock, 403, "FORBIDDEN", txt[0] ? txt : "策略写入需系统身份,面板未被白名单?");
|
||||
if (code == (uint32_t)MEOW_ERR_INVAL)
|
||||
return send_err(sock, 422, "UNSUPPORTED", txt[0] ? txt : "策略参数非法");
|
||||
if (code != MEOW_OK)
|
||||
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "策略写入失败");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"accepted\":true,\"message\":");
|
||||
sA_str(&s, txt);
|
||||
sA(&s, "}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
/* mp_notif.c — 通知域:优先 launcher IPC 活跃集,回退 TF 归档(查询/撤/清)。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
int notif_csv_field(const char **pp, char *out, int outcap)
|
||||
{
|
||||
const char *p = *pp;
|
||||
int o = 0;
|
||||
if (*p == '"') {
|
||||
p++;
|
||||
while (*p && *p != '"') {
|
||||
if (*p == '"' && p[1] == '"') {
|
||||
if (o < outcap - 1) out[o++] = '"';
|
||||
p += 2;
|
||||
} else {
|
||||
if (o < outcap - 1) out[o++] = *p;
|
||||
p++;
|
||||
}
|
||||
}
|
||||
if (*p == '"') p++;
|
||||
} else {
|
||||
while (*p && *p != ',' && *p != '\n' && *p != '\r') {
|
||||
if (o < outcap - 1) out[o++] = *p;
|
||||
p++;
|
||||
}
|
||||
}
|
||||
if (o < outcap) { out[o] = 0; }
|
||||
else if (outcap > 0) { out[outcap - 1] = 0; }
|
||||
if (*p == ',') p++;
|
||||
*pp = p;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int notif_parse_line(const char *line, notif_rec_t *r)
|
||||
{
|
||||
const char *p = line;
|
||||
char f1[32], f2[16], f6[8], f7[16], f11[8], f12[8], f13[8];
|
||||
char app[40], icon[64], cat[40], sum[96], body[128], tgt[64];
|
||||
notif_csv_field(&p, f1, sizeof f1);
|
||||
notif_csv_field(&p, f2, sizeof f2);
|
||||
notif_csv_field(&p, app, sizeof app);
|
||||
notif_csv_field(&p, icon, sizeof icon);
|
||||
notif_csv_field(&p, cat, sizeof cat);
|
||||
notif_csv_field(&p, f6, sizeof f6);
|
||||
notif_csv_field(&p, f7, sizeof f7);
|
||||
notif_csv_field(&p, sum, sizeof sum);
|
||||
notif_csv_field(&p, body, sizeof body);
|
||||
notif_csv_field(&p, tgt, sizeof tgt);
|
||||
notif_csv_field(&p, f11, sizeof f11);
|
||||
notif_csv_field(&p, f12, sizeof f12);
|
||||
notif_csv_field(&p, f13, sizeof f13);
|
||||
r->ms = strtoll(f1, NULL, 10);
|
||||
r->id = (unsigned)strtoul(f2, NULL, 10);
|
||||
r->urgency = atoi(f6);
|
||||
r->progress = atoi(f7);
|
||||
r->ongoing = atoi(f11);
|
||||
r->media = atoi(f12);
|
||||
r->dismissed = atoi(f13);
|
||||
snprintf(r->app_id, sizeof r->app_id, "%s", app);
|
||||
snprintf(r->app_icon, sizeof r->app_icon, "%s", icon);
|
||||
snprintf(r->category, sizeof r->category, "%s", cat);
|
||||
snprintf(r->summary, sizeof r->summary, "%s", sum);
|
||||
snprintf(r->body, sizeof r->body, "%s", body);
|
||||
snprintf(r->target, sizeof r->target, "%s", tgt);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int notif_load_list(notif_rec_t *recs, int maxrec)
|
||||
{
|
||||
FILE *f = fopen(g_notif_log, "r");
|
||||
if (!f) return -1;
|
||||
int total = 0, head = 0;
|
||||
char line[1400];
|
||||
notif_rec_t r;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
if (notif_parse_line(line, &r) != 0) continue;
|
||||
recs[head] = r;
|
||||
head = (head + 1) % maxrec;
|
||||
if (total < maxrec) total++;
|
||||
}
|
||||
fclose(f);
|
||||
if (total < maxrec) return total; /* 未满窗:recs[0..total-1] 即 oldest..newest */
|
||||
/* 满窗:oldest 在 recs[head],重排为 oldest..newest 连续 */
|
||||
notif_rec_t tmp[NOTIF_PREVIEW_MAX];
|
||||
for (int i = 0; i < maxrec; i++) tmp[i] = recs[(head + i) % maxrec];
|
||||
memcpy(recs, tmp, (size_t)maxrec * sizeof recs[0]);
|
||||
return total;
|
||||
}
|
||||
|
||||
const char *notif_app_name(const char *app_id)
|
||||
{
|
||||
if (!strcmp(app_id, "system")) return "系统";
|
||||
return app_id;
|
||||
}
|
||||
|
||||
void notif_emit_item(sbuf_t *out, const notif_rec_t *r)
|
||||
{
|
||||
sA(out, "{\"id\":%u,\"app_id\":", r->id);
|
||||
sA_str(out, r->app_id);
|
||||
sA(out, ",\"app_name\":");
|
||||
sA_str(out, notif_app_name(r->app_id));
|
||||
sA(out, ",\"app_icon\":");
|
||||
sA_str(out, r->app_icon);
|
||||
sA(out, ",\"category\":");
|
||||
sA_str(out, r->category);
|
||||
sA(out, ",\"summary\":");
|
||||
sA_str(out, r->summary);
|
||||
sA(out, ",\"body\":");
|
||||
sA_str(out, r->body);
|
||||
sA(out, ",\"urgency\":%d,\"ts\":%lld,\"progress\":", r->urgency, r->ms > 0 ? r->ms / 1000 : 0);
|
||||
if (r->progress >= 0) sA(out, "%d", r->progress); else sA(out, "null");
|
||||
sA(out, ",\"ongoing\":%s,\"media\":%s,\"target_activity\":",
|
||||
r->ongoing ? "true" : "false", r->media ? "true" : "false");
|
||||
sA_str(out, r->target);
|
||||
sA(out, ",\"dismissed\":%s}", r->dismissed ? "true" : "false");
|
||||
}
|
||||
|
||||
int notif_ipc_query(notif_rec_t *recs, int maxrec)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return -1;
|
||||
int cnt = -1;
|
||||
if (ipc_hello(fd) < 0) goto done;
|
||||
if (send_frame(fd, MSG_NOTIFY_QUERY, 8, NULL, 0) < 0) goto done;
|
||||
uint8_t buf[MEOW_IPC_MAX + MEOW_IPC_HDR_LEN];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < (int)sizeof(m_notify_query_result_t)) goto done;
|
||||
if (type != MSG_NOTIFY_QUERY_RESULT) goto done;
|
||||
m_notify_query_result_t qr;
|
||||
memcpy(&qr, buf, sizeof qr);
|
||||
if (qr.code != MEOW_OK) goto done;
|
||||
uint16_t tl = qr.text_len;
|
||||
if (tl >= sizeof qr.text) tl = (uint16_t)(sizeof qr.text - 1);
|
||||
if (tl == 0) { cnt = 0; goto done; } /* 无活跃通知 */
|
||||
jv_t *arr = jv_parse(qr.text, tl);
|
||||
if (!arr) goto done;
|
||||
if (arr->type != J_ARR) { jv_free(arr); goto done; }
|
||||
int c = 0;
|
||||
for (jv_t *e = arr->child; e && c < maxrec; e = e->next) {
|
||||
if (e->type != J_OBJ) continue;
|
||||
notif_rec_t r; memset(&r, 0, sizeof r);
|
||||
r.progress = -1;
|
||||
r.urgency = 0;
|
||||
jv_t *v;
|
||||
if ((v = jv_obj_get(e, "id")) && v->type == J_NUM) r.id = (unsigned)v->inum;
|
||||
if ((v = jv_obj_get(e, "app_id")) && v->type == J_STR) snprintf(r.app_id, sizeof r.app_id, "%s", v->str);
|
||||
if ((v = jv_obj_get(e, "app_icon")) && v->type == J_STR) snprintf(r.app_icon, sizeof r.app_icon, "%s", v->str);
|
||||
if ((v = jv_obj_get(e, "category")) && v->type == J_STR) snprintf(r.category, sizeof r.category, "%s", v->str);
|
||||
if ((v = jv_obj_get(e, "summary")) && v->type == J_STR) snprintf(r.summary, sizeof r.summary, "%s", v->str);
|
||||
if ((v = jv_obj_get(e, "body")) && v->type == J_STR) snprintf(r.body, sizeof r.body, "%s", v->str);
|
||||
if ((v = jv_obj_get(e, "target_activity")) && v->type == J_STR) snprintf(r.target, sizeof r.target, "%s", v->str);
|
||||
if ((v = jv_obj_get(e, "urgency")) && v->type == J_NUM) r.urgency = (int)v->inum;
|
||||
/* ts 为 epoch 毫秒 → 存入 r.ms(emit 时 /1000 出秒,与 TF 同口径) */
|
||||
if ((v = jv_obj_get(e, "ts")) && v->type == J_NUM) r.ms = (long long)v->inum;
|
||||
if ((v = jv_obj_get(e, "progress")) && v->type == J_NUM) r.progress = (int)v->inum; /* <0=无;json 无该键则 -1 */
|
||||
if ((v = jv_obj_get(e, "ongoing")) && v->type == J_NUM) r.ongoing = (int)v->inum;
|
||||
if ((v = jv_obj_get(e, "media")) && v->type == J_NUM) r.media = (int)v->inum;
|
||||
recs[c++] = r;
|
||||
}
|
||||
jv_free(arr);
|
||||
/* 按 ms 升序(oldest..newest),与 TF 路径一致(emit 时倒序遍历= newest first)。 */
|
||||
for (int i = 1; i < c; i++) { notif_rec_t key = recs[i]; int j = i - 1;
|
||||
while (j >= 0 && recs[j].ms > key.ms) { recs[j + 1] = recs[j]; j--; } recs[j + 1] = key; }
|
||||
cnt = c;
|
||||
done:
|
||||
close(fd);
|
||||
return cnt;
|
||||
}
|
||||
|
||||
int notif_ipc_dismiss(unsigned id)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return -1;
|
||||
int rc = -1;
|
||||
if (ipc_hello(fd) < 0) goto done;
|
||||
uint32_t nid = (uint32_t)id;
|
||||
if (send_frame(fd, MSG_NOTIFY_DISMISS, 9, &nid, sizeof nid) < 0) goto done;
|
||||
uint8_t buf[64];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < (int)sizeof(m_ack_t)) goto done;
|
||||
if (type != MSG_ACK) goto done;
|
||||
m_ack_t a; memcpy(&a, buf, sizeof a);
|
||||
rc = (a.code == MEOW_OK) ? 0 : -1;
|
||||
done:
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int notif_ipc_dismiss_all(int *out_count)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_launcher, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return -1;
|
||||
int rc = -1;
|
||||
if (ipc_hello(fd) < 0) goto done;
|
||||
if (send_frame(fd, MSG_NOTIFY_DISMISS_ALL, 10, NULL, 0) < 0) goto done;
|
||||
uint8_t buf[64];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < (int)sizeof(m_ack_t)) goto done;
|
||||
if (type != MSG_ACK) goto done;
|
||||
m_ack_t a; memcpy(&a, buf, sizeof a);
|
||||
if (a.code == MEOW_OK) { if (out_count) *out_count = (int)a.status; rc = 0; }
|
||||
done:
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_notifications(int sock)
|
||||
{
|
||||
notif_rec_t recs[NOTIF_PREVIEW_MAX];
|
||||
const char *source = "ipc";
|
||||
int n = notif_ipc_query(recs, NOTIF_PREVIEW_MAX);
|
||||
if (n < 0) { /* IPC 不可达 → 回退 TF 归档 */
|
||||
n = notif_load_list(recs, NOTIF_PREVIEW_MAX);
|
||||
source = (n < 0) ? "none" : "tf";
|
||||
if (n < 0) n = 0;
|
||||
}
|
||||
sbuf_t arr = {0};
|
||||
int first = 1;
|
||||
for (int i = n - 1; i >= 0; i--) { /* newest first */
|
||||
if (!first) sA(&arr, ",");
|
||||
first = 0;
|
||||
notif_emit_item(&arr, &recs[i]);
|
||||
}
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"count\":%d,\"source\":\"%s\",\"items\":[", n, source);
|
||||
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||||
sA(&s, "],\"notifications\":[");
|
||||
if (arr.n) sA_append(&s, arr.b, arr.n);
|
||||
sA(&s, "]}");
|
||||
sbuf_free(&arr);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int notif_rewrite(int mode, unsigned del_id)
|
||||
{
|
||||
if (!g_notif_log || !g_notif_log[0]) return -1;
|
||||
char tmppath[1400];
|
||||
snprintf(tmppath, sizeof tmppath, "%s.panel.tmp", g_notif_log);
|
||||
FILE *in = fopen(g_notif_log, "r");
|
||||
if (!in) return 0;
|
||||
FILE *out = fopen(tmppath, "w");
|
||||
if (!out) { fclose(in); return -1; }
|
||||
int removed = 0;
|
||||
char line[1400];
|
||||
while (fgets(line, sizeof line, in)) {
|
||||
notif_rec_t r;
|
||||
int parsed = (notif_parse_line(line, &r) == 0);
|
||||
int drop = 0;
|
||||
if (mode == 1) {
|
||||
if (!parsed) drop = 1;
|
||||
else drop = !(r.ongoing || r.media || r.progress >= 0 || !strcmp(r.category, "media"));
|
||||
} else {
|
||||
if (parsed && r.id == del_id) drop = 1;
|
||||
}
|
||||
if (drop) removed++;
|
||||
else fputs(line, out);
|
||||
}
|
||||
fclose(in);
|
||||
if (fclose(out) != 0) return -1;
|
||||
if (rename(tmppath, g_notif_log) < 0) { unlink(tmppath); return -1; }
|
||||
return removed;
|
||||
}
|
||||
|
||||
int api_notification_delete(int sock, unsigned id)
|
||||
{
|
||||
/* 优先 IPC 联动 launcher 内存(launcher 撤下时自动归档留档);IPC 不可达/未受理回退 TF 重写。 */
|
||||
if (notif_ipc_dismiss(id) == 0) {
|
||||
char d[64];
|
||||
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
|
||||
return send_json(sock, 200, d);
|
||||
}
|
||||
int removed = notif_rewrite(0, id);
|
||||
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||||
if (removed == 0) return send_err(sock, 404, "NOT_FOUND", "通知不存在");
|
||||
char d[64];
|
||||
snprintf(d, sizeof d, "{\"id\":%u,\"dismissed\":true}", id);
|
||||
return send_json(sock, 200, d);
|
||||
}
|
||||
|
||||
int api_notification_dismiss_all(int sock)
|
||||
{
|
||||
int cleared = 0;
|
||||
if (notif_ipc_dismiss_all(&cleared) == 0) { /* 联动 launcher 内存,返回清除数 */
|
||||
char d[64];
|
||||
snprintf(d, sizeof d, "{\"dismissed\":%d}", cleared);
|
||||
return send_json(sock, 200, d);
|
||||
}
|
||||
int removed = notif_rewrite(1, 0); /* IPC 不可达 → 回退重写 TF 日志 */
|
||||
if (removed < 0) return send_err(sock, 500, "INTERNAL", "写通知存储失败");
|
||||
char d[64];
|
||||
snprintf(d, sizeof d, "{\"dismissed\":%d}", removed);
|
||||
return send_json(sock, 200, d);
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/* mp_perf.c — 性能/系统域:/api/perf,/api/system,action,pin-unlock(经 meow_svc_perf IPC 聚合)。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
int api_perf(int sock, const char *path)
|
||||
{
|
||||
char json[4096];
|
||||
int rc = ipc_perf_query(perf_subset_for_path(path), json, sizeof json);
|
||||
if (rc != MEOW_OK || !utf8_valid_n(json, strlen(json))) /* 上游坏字节 → 不返回,降级 */
|
||||
return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
|
||||
return send_json(sock, 200, json);
|
||||
}
|
||||
|
||||
int perf_extract_network(const char *json, char *out, int cap)
|
||||
{
|
||||
const char *p = json;
|
||||
for (;;) {
|
||||
p = strstr(p, "\"network\"");
|
||||
if (!p) return -1;
|
||||
const char *q = p + 9; /* strlen("\"network\"") */
|
||||
while (*q == ' ' || *q == '\t') q++;
|
||||
if (*q == ':') { p = q + 1; break; } /* 顶层 network 键 */
|
||||
p += 9; /* 嵌套同名键跳过 */
|
||||
}
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (*p != '{') return -1;
|
||||
int depth = 0, in_str = 0, esc = 0;
|
||||
const char *q;
|
||||
for (q = p; *q; q++) {
|
||||
char ch = *q;
|
||||
if (in_str) {
|
||||
if (esc) esc = 0;
|
||||
else if (ch == '\\') esc = 1;
|
||||
else if (ch == '"') in_str = 0;
|
||||
continue;
|
||||
}
|
||||
if (ch == '"') { in_str = 1; continue; }
|
||||
if (ch == '{') depth++;
|
||||
else if (ch == '}') { depth--; if (depth == 0) { q++; break; } }
|
||||
}
|
||||
if (depth != 0) return -1;
|
||||
size_t len = (size_t)(q - p);
|
||||
if (len == 0) return -1;
|
||||
if (len >= (size_t)cap) len = (size_t)cap - 1;
|
||||
memcpy(out, p, len); out[len] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_system(int sock)
|
||||
{
|
||||
char json[4096];
|
||||
int rc = ipc_perf_query(0, json, sizeof json);
|
||||
if (rc != MEOW_OK) return send_err(sock, 500, "INTERNAL", "性能服务不可用(IPC 连接/应答失败)");
|
||||
jv_t *root = jv_parse(json, (int)strlen(json));
|
||||
if (!root) return send_err(sock, 500, "INTERNAL", "性能服务 JSON 解析失败");
|
||||
jv_t *jsys = jv_obj_get(root, "system");
|
||||
jv_t *jupt = jv_obj_get(root, "uptime_sec");
|
||||
jv_t *jst = jv_obj_get(root, "storage");
|
||||
jv_t *jtf = jv_obj_get(root, "tfcard");
|
||||
const char *ver = "-", *host = "-", *ker = "-", *arch = "-";
|
||||
if (jsys && jsys->type == J_OBJ) {
|
||||
jv_t *v = jv_obj_get(jsys, "version"); if (v && v->type == J_STR) ver = v->str;
|
||||
v = jv_obj_get(jsys, "hostname"); if (v && v->type == J_STR) host = v->str;
|
||||
v = jv_obj_get(jsys, "kernel"); if (v && v->type == J_STR) ker = v->str;
|
||||
v = jv_obj_get(jsys, "arch"); if (v && v->type == J_STR) arch = v->str;
|
||||
}
|
||||
long long upt = jupt ? (long long)jupt->dnum : 0;
|
||||
/* boot default:读权威 /data/meow_boot_default(0=meow 默认, 1=hiby),失败回退 meow */
|
||||
int bdf = -1;
|
||||
{
|
||||
char b[16];
|
||||
FILE *f = fopen(BOOT_DEFAULT_FILE, "r");
|
||||
if (f) { if (fgets(b, sizeof b, f)) bdf = atoi(b); fclose(f); }
|
||||
}
|
||||
const char *bootname = bdf < 0 ? "meow" : (bdf ? "hiby" : "meow");
|
||||
/* 当前运行模式:读 /tmp/meow_boot_mode(meow/hiby),缺失回退 default(PANEL_ENHANCE_PLAN 批1 前端读 boot.mode) */
|
||||
char modebuf[16] = {0};
|
||||
{
|
||||
FILE *f = fopen("/tmp/meow_boot_mode", "r");
|
||||
if (f) {
|
||||
if (fgets(modebuf, sizeof modebuf, f)) {
|
||||
char *p = modebuf;
|
||||
while (*p) { if (*p == '\n' || *p == '\r' || *p == ' ') { *p = 0; break; } p++; }
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
}
|
||||
const char *bootmode = (modebuf[0] == 'm' || modebuf[0] == 'h') ? modebuf : bootname;
|
||||
char host_local[80];
|
||||
snprintf(host_local, sizeof host_local, "%s.local", host);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"version\":");
|
||||
sA_str(&s, ver);
|
||||
sA(&s, ",\"hostname\":");
|
||||
sA_str(&s, host);
|
||||
sA(&s, ",\"kernel\":");
|
||||
sA_str(&s, ker);
|
||||
sA(&s, ",\"arch\":");
|
||||
sA_str(&s, arch);
|
||||
sA(&s, ",\"uptime_sec\":%lld,\"boot\":{\"default\":", upt);
|
||||
sA_str(&s, bootname);
|
||||
sA(&s, ",\"mode\":");
|
||||
sA_str(&s, bootmode);
|
||||
sA(&s, "},\"network\":");
|
||||
char netbuf[1024];
|
||||
/* 透传 perf 权威 network 段(wifi/bluetooth/hostname_local);含非法 UTF-8(如坏 ssid)→ 回退,不嵌坏字节 */
|
||||
if (perf_extract_network(json, netbuf, sizeof netbuf) == 0 && utf8_valid_n(netbuf, strlen(netbuf))) {
|
||||
sA(&s, "%s", netbuf);
|
||||
} else {
|
||||
/* 回退:perf 未含 network 段 → 仅 hostname_local(保持原语义,不崩) */
|
||||
sA(&s, "{\"hostname_local\":");
|
||||
sA_str(&s, host_local);
|
||||
sA(&s, "}");
|
||||
}
|
||||
sA(&s, ",\"storage\":{");
|
||||
if (jst && jst->type == J_OBJ) {
|
||||
jv_t *t = jv_obj_get(jst, "total"); long long tot = t ? (long long)t->dnum : 0;
|
||||
jv_t *u = jv_obj_get(jst, "used"); long long usd = u ? (long long)u->dnum : 0;
|
||||
jv_t *a = jv_obj_get(jst, "avail"); long long ava = a ? (long long)a->dnum : 0;
|
||||
sA(&s, "\"root\":{\"total\":%lld,\"used\":%lld,\"avail\":%lld}", tot, usd, ava);
|
||||
} else {
|
||||
sA(&s, "\"root\":{\"total\":0,\"used\":0,\"avail\":0}");
|
||||
}
|
||||
sA(&s, ",\"tfcard\":{");
|
||||
if (jtf && jtf->type == J_OBJ) {
|
||||
jv_t *pr = jv_obj_get(jtf, "present"); int present = pr && pr->bval;
|
||||
long long tot = 0, usd = 0, ava = 0;
|
||||
jv_t *t = jv_obj_get(jtf, "total"); if (t) tot = (long long)t->dnum;
|
||||
jv_t *u = jv_obj_get(jtf, "used"); if (u) usd = (long long)u->dnum;
|
||||
jv_t *a = jv_obj_get(jtf, "avail"); if (a) ava = (long long)a->dnum;
|
||||
sA(&s, "\"present\":%s,\"total\":%lld,\"used\":%lld,\"avail\":%lld}", present ? "true" : "false", tot, usd, ava);
|
||||
} else {
|
||||
sA(&s, "\"present\":false,\"total\":0,\"used\":0,\"avail\":0}");
|
||||
}
|
||||
sA(&s, "},\"ota\":{\"current\":");
|
||||
sA_str(&s, ver);
|
||||
sA(&s, ",\"latest\":");
|
||||
sA_str(&s, ver);
|
||||
sA(&s, ",\"available\":false,\"url\":null}");
|
||||
/* 重启原因 / 升级状态:svc_power 读级查询(POWER_REBOOT_REASON(8)/POWER_UPGRADE_STATUS(9),只读、
|
||||
* 任意调用者)。st.msg 分别为 "coldboot=reboot|shutdown|unknown" 与 "upgrade=yes|no"。
|
||||
* 前端批1-B 已留位(读 d.reboot_reason / d.upgrade_status),此处点亮。查询失败保持空串(不谎报)。 */
|
||||
char rreason[48] = "", ustatus[32] = "";
|
||||
{
|
||||
m_power_status_t st;
|
||||
memset(&st, 0, sizeof st);
|
||||
if (ipc_power_action(POWER_REBOOT_REASON, 0, NULL, &st) == MEOW_OK)
|
||||
svc_msg_kv(st.msg, "coldboot", rreason, sizeof rreason);
|
||||
memset(&st, 0, sizeof st);
|
||||
if (ipc_power_action(POWER_UPGRADE_STATUS, 0, NULL, &st) == MEOW_OK)
|
||||
svc_msg_kv(st.msg, "upgrade", ustatus, sizeof ustatus);
|
||||
}
|
||||
sA(&s, ",\"reboot_reason\":");
|
||||
sA_str(&s, rreason);
|
||||
sA(&s, ",\"upgrade_status\":");
|
||||
sA_str(&s, ustatus);
|
||||
sA(&s, "}");
|
||||
jv_free(root);
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int api_pin_unlock_once(int sock)
|
||||
{
|
||||
const char *path = "/tmp/meow_pin_unlock_once";
|
||||
FILE *f = fopen(path, "w");
|
||||
if (!f) return send_err(sock, 500, "INTERNAL", "无法写入一次性解锁标志");
|
||||
fputs("1\n", f);
|
||||
fclose(f);
|
||||
return send_json(sock, 200, "{\"unlock_once\":true}");
|
||||
}
|
||||
|
||||
int api_system_action(int sock, const char *which, const req_t *r)
|
||||
{
|
||||
(void)r;
|
||||
uint32_t cmd;
|
||||
if (!strcmp(which, "reboot")) cmd = POWER_REBOOT;
|
||||
else if (!strcmp(which, "shutdown")) cmd = POWER_SHUTDOWN;
|
||||
else if (!strcmp(which, "ota")) cmd = POWER_UPGRADE;
|
||||
else return send_err(sock, 404, "NOT_FOUND", "未知系统动作");
|
||||
m_power_status_t st;
|
||||
memset(&st, 0, sizeof st);
|
||||
int rc = ipc_power_action(cmd, 0, NULL, &st);
|
||||
if (rc == MEOW_OK) return send_json(sock, 200, "{\"accepted\":true}");
|
||||
/* 系统动作类服务端通常「先应答受理再执行」;此处若返回非 OK 则说明服务未受理 */
|
||||
const char *why = st.msg[0] ? st.msg : power_code_str(rc);
|
||||
if (rc == MEOW_ERR_PERM) return send_err(sock, 403, "FORBIDDEN", "电源服务拒绝(非 root 调用)");
|
||||
if (rc == MEOW_ERR_NODEV) return send_err(sock, 503, "NO_CARD", "升级文件缺失或服务不可达");
|
||||
if (rc == MEOW_ERR_NOENT || rc == MEOW_ERR_IO) return send_err(sock, 503, "NO_CARD", why);
|
||||
return send_err(sock, 500, "INTERNAL", why);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/* mp_power.c — 功耗管理域:meow_powermgr IPC(/api/power*,policy,window,events)。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
int ipc_pm_query(uint32_t subset, char *out, int cap)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return MEOW_ERR_NODEV;
|
||||
int rc = MEOW_ERR_INTERNAL;
|
||||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||||
m_pm_query_t q;
|
||||
memset(&q, 0, sizeof q);
|
||||
q.subset = subset;
|
||||
if (send_frame(fd, MSG_PM_QUERY, 0x31, &q, sizeof q) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||||
uint8_t buf[MEOW_IPC_MAX];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
|
||||
if (type != MSG_PM_RESULT) { rc = MEOW_ERR_INVAL; goto done; }
|
||||
uint32_t c; memcpy(&c, buf, 4);
|
||||
if (c != MEOW_OK) { rc = (int)c; goto done; }
|
||||
/* powermgr 发**紧凑**负载:json 从偏移 12 起(json_len 在偏移 8);同 m_pm_result_t 布局,兼容全量/紧凑。 */
|
||||
uint16_t jl; memcpy(&jl, buf + 8, 2);
|
||||
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
|
||||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||||
memcpy(out, buf + 12, jl);
|
||||
out[jl] = 0;
|
||||
rc = MEOW_OK;
|
||||
done:
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_power(int sock)
|
||||
{
|
||||
char js[MEOW_IPC_MAX];
|
||||
int rc = ipc_pm_query(0, js, sizeof js);
|
||||
if (rc != MEOW_OK || !utf8_valid_n(js, strlen(js))) /* 上游坏字节 → 不返回,降级 */
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用");
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
|
||||
int ipc_pm_send(uint16_t req_type, const void *pl, uint32_t plen,
|
||||
uint16_t *rtype, char *out, int cap)
|
||||
{
|
||||
int fd = ipc_connect_tmo(g_sock_pm, IPC_TIMEOUT_MS);
|
||||
if (fd < 0) return MEOW_ERR_NODEV;
|
||||
int rc = MEOW_ERR_INTERNAL;
|
||||
if (rtype) *rtype = 0;
|
||||
if (ipc_hello(fd) < 0) { rc = MEOW_ERR_INTERNAL; goto done; }
|
||||
if (send_frame(fd, req_type, 0x41, pl, plen) < 0) { rc = MEOW_ERR_IO; goto done; }
|
||||
uint8_t buf[MEOW_IPC_MAX];
|
||||
uint16_t type; uint32_t rid;
|
||||
int n = recv_frame(fd, &type, &rid, buf, sizeof buf);
|
||||
if (n <= 0) { rc = MEOW_ERR_IO; goto done; }
|
||||
if (rtype) *rtype = type;
|
||||
if (type == MSG_ERR) {
|
||||
m_err_t e; memcpy(&e, buf, sizeof e);
|
||||
rc = (int)e.code; goto done;
|
||||
}
|
||||
if (n < 12) { rc = MEOW_ERR_IO; goto done; } /* code(4)+subset(4)+json_len(2)+pad(2) */
|
||||
uint32_t c; memcpy(&c, buf, 4);
|
||||
if (c != MEOW_OK) { rc = (int)c; goto done; }
|
||||
uint16_t jl; memcpy(&jl, buf + 8, 2);
|
||||
if (jl > MEOW_IPC_MAX - 12) jl = (uint16_t)(MEOW_IPC_MAX - 12);
|
||||
if ((size_t)jl >= (size_t)cap) jl = (uint16_t)(cap - 1);
|
||||
memcpy(out, buf + 12, jl); out[jl] = 0;
|
||||
rc = (int)jl;
|
||||
done:
|
||||
close(fd);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_power_policy_get(int sock)
|
||||
{
|
||||
char js[MEOW_IPC_MAX];
|
||||
uint16_t rt = 0;
|
||||
int rc = ipc_pm_send(MSG_PM_POLICY_GET, NULL, 0, &rt, js, sizeof js);
|
||||
if (rc <= 0)
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
||||
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
|
||||
int api_power_policy_set(int sock, const char *body, size_t body_len)
|
||||
{
|
||||
if (!body || !body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(body, (int)body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *ji = jv_obj_get(root, "item"), *jv = jv_obj_get(root, "value");
|
||||
char item[20] = "", val[40] = "";
|
||||
if (ji && ji->type == J_STR && ji->str) snprintf(item, sizeof item, "%s", ji->str);
|
||||
if (jv && jv->type == J_STR && jv->str) snprintf(val, sizeof val, "%s", jv->str);
|
||||
else if (jv && jv->type == J_NUM) snprintf(val, sizeof val, "%lld", jv->inum);
|
||||
jv_free(root);
|
||||
if (!item[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 item(profile/low-pct/crit-pct/bri-cap/scr-cap)");
|
||||
if (!val[0]) return send_err(sock, 422, "UNSUPPORTED", "缺 value");
|
||||
|
||||
m_pm_policy_set_t s; memset(&s, 0, sizeof s);
|
||||
snprintf(s.item, sizeof s.item, "%.15s", item); /* item[16] 留一个 NUL;防越界截断告警 */
|
||||
snprintf(s.val, sizeof s.val, "%.31s", val); /* val[32] 同理 */
|
||||
char js[MEOW_IPC_MAX];
|
||||
uint16_t rt = 0;
|
||||
int rc = ipc_pm_send(MSG_PM_POLICY_SET, &s, sizeof s, &rt, js, sizeof js);
|
||||
if (rc <= 0) {
|
||||
if (rt == MSG_ERR) /* 权限不足:powermgr 拒绝(面板 root 仅当被白名单才放行;如实回传) */
|
||||
return send_err(sock, 403, "FORBIDDEN", rc == MEOW_ERR_PERM ? "设备身份不允许写电源策略" : "电源策略写入被拒绝");
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务不可用(powermgr 未运行/IPC 不可达)");
|
||||
}
|
||||
if (rt != MSG_PM_RESULT || !utf8_valid_n(js, strlen(js)))
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||||
/* 服务端对非法值回 ok:false(HTTP 层 200,code 仍 OK);按其 why 映射 422。 */
|
||||
if (strstr(js, "\"ok\":false")) {
|
||||
char why[96] = "";
|
||||
jv_t *jr = jv_parse(js, (int)strlen(js));
|
||||
if (jr) { jv_t *w = jv_obj_get(jr, "why"); if (w && w->type == J_STR && w->str) snprintf(why, sizeof why, "%s", w->str); jv_free(jr); }
|
||||
return send_err(sock, 422, "UNSUPPORTED", why[0] ? why : "非法电源策略值");
|
||||
}
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
|
||||
int api_power_window(int sock, uint16_t req_type, uint16_t want_type, const char *unavail)
|
||||
{
|
||||
uint32_t count = 0;
|
||||
char js[MEOW_IPC_MAX];
|
||||
uint16_t rt = 0;
|
||||
int rc = ipc_pm_send(req_type, &count, sizeof count, &rt, js, sizeof js);
|
||||
if (rc <= 0)
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", unavail);
|
||||
if (rt != want_type || !utf8_valid_n(js, strlen(js)))
|
||||
return send_err(sock, 503, "POWER_UNAVAILABLE", "电源管理服务应答包含非法字节");
|
||||
return send_json(sock, 200, js);
|
||||
}
|
||||
|
||||
int api_power_history(int sock)
|
||||
{ return api_power_window(sock, MSG_PM_HISTORY, MSG_PM_HISTORY_RESULT, "电源历史采样不可用(powermgr 未运行)"); }
|
||||
|
||||
int api_power_events(int sock)
|
||||
{ return api_power_window(sock, MSG_PM_EVENTS, MSG_PM_EVENTS_RESULT, "电源事件不可用(powermgr 未运行)"); }
|
||||
@@ -0,0 +1,277 @@
|
||||
/* mp_screen.c — 实时画面域:/api/screen.jpeg(fb0→JPEG,增量帧)+ 触摸注入 /api/input/tap。 */
|
||||
#include "mp.h"
|
||||
|
||||
static uint8_t *g_screen_prev = NULL; /* 上一帧紧凑 RGB565 */
|
||||
static int g_screen_pw = 0, g_screen_ph = 0;
|
||||
static int64_t g_last_self_inject_ms = 0;
|
||||
#define TOUCH_ACTIVE_MS 5000
|
||||
|
||||
|
||||
int screen_read_frame(const char *dev, uint8_t **out, int *w, int *h)
|
||||
{
|
||||
int fd = open(dev, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
unsigned char vinfo[160], finfo[68];
|
||||
memset(vinfo, 0, sizeof vinfo);
|
||||
memset(finfo, 0, sizeof finfo);
|
||||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0 ||
|
||||
ioctl(fd, FBIOGET_FSCREENINFO, finfo) < 0) { close(fd); return -1; }
|
||||
uint32_t xres = *(uint32_t *)(vinfo + 0);
|
||||
uint32_t yres = *(uint32_t *)(vinfo + 4);
|
||||
uint32_t yoff = *(uint32_t *)(vinfo + 20);
|
||||
uint32_t bpp = *(uint32_t *)(vinfo + 24);
|
||||
uint32_t line = *(uint32_t *)(finfo + 44);
|
||||
if (!line) line = xres * (bpp / 8);
|
||||
if (bpp != 16 || xres == 0 || yres == 0 || line < xres * 2) { close(fd); return -1; }
|
||||
size_t framelen = (size_t)line * (size_t)yres;
|
||||
uint8_t *frame = (uint8_t *)malloc(framelen);
|
||||
if (!frame) { close(fd); return -1; }
|
||||
ssize_t got = pread(fd, frame, framelen, (off_t)line * (off_t)yoff);
|
||||
close(fd);
|
||||
if (got != (ssize_t)framelen) { free(frame); return -1; }
|
||||
/* 若 line != xres*2(行尾填充),压缩为紧凑 RGB565(xres*yres*2) */
|
||||
if (line != xres * 2) {
|
||||
size_t cplen = (size_t)xres * 2;
|
||||
for (uint32_t y = 0; y < yres; y++)
|
||||
memmove(frame + (size_t)y * cplen, frame + (size_t)y * line, cplen);
|
||||
}
|
||||
*out = frame; *w = (int)xres; *h = (int)yres;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int fb_dims(const char *dev, int *w, int *h)
|
||||
{
|
||||
int fd = open(dev, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
unsigned char vinfo[160];
|
||||
memset(vinfo, 0, sizeof vinfo);
|
||||
if (ioctl(fd, FBIOGET_VSCREENINFO, vinfo) < 0) { close(fd); return -1; }
|
||||
uint32_t xres = *(uint32_t *)(vinfo + 0);
|
||||
uint32_t yres = *(uint32_t *)(vinfo + 4);
|
||||
close(fd);
|
||||
if (!xres || !yres) return -1;
|
||||
*w = (int)xres; *h = (int)yres;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void screen_prev_store(const uint8_t *frame, int w, int h)
|
||||
{
|
||||
size_t need = (size_t)w * (size_t)h * 2;
|
||||
if (w != g_screen_pw || h != g_screen_ph) {
|
||||
free(g_screen_prev);
|
||||
g_screen_prev = NULL;
|
||||
g_screen_pw = w; g_screen_ph = h;
|
||||
g_screen_prev = (uint8_t *)malloc(need);
|
||||
}
|
||||
if (g_screen_prev) memcpy(g_screen_prev, frame, need);
|
||||
}
|
||||
|
||||
int screen_diff_blocks(const uint8_t *cur, const uint8_t *prev, int w, int h,
|
||||
int *bx, int *by, int *bw, int *bh, int *total_blocks)
|
||||
{
|
||||
const int B = SCREEN_BLOCK;
|
||||
int nbx = (w + B - 1) / B, nby = (h + B - 1) / B;
|
||||
if (total_blocks) *total_blocks = nbx * nby;
|
||||
int changed = 0;
|
||||
int minx = nbx, miny = nby, maxx = -1, maxy = -1;
|
||||
for (int byi = 0; byi < nby; byi++) {
|
||||
for (int bxi = 0; bxi < nbx; bxi++) {
|
||||
int x0 = bxi * B, y0 = byi * B;
|
||||
int wdt = B, hgt = B;
|
||||
if (x0 + wdt > w) wdt = w - x0;
|
||||
if (y0 + hgt > h) hgt = h - y0;
|
||||
int diff = 0;
|
||||
for (int y = 0; y < hgt && !diff; y++) {
|
||||
const uint8_t *a = cur + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2;
|
||||
const uint8_t *b = prev + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2;
|
||||
if (memcmp(a, b, (size_t)wdt * 2) != 0) diff = 1;
|
||||
}
|
||||
if (diff) {
|
||||
changed++;
|
||||
if (bxi < minx) minx = bxi;
|
||||
if (byi < miny) miny = byi;
|
||||
if (bxi > maxx) maxx = bxi;
|
||||
if (byi > maxy) maxy = byi;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (changed) { *bx = minx; *by = miny; *bw = maxx - minx + 1; *bh = maxy - miny + 1; }
|
||||
return changed;
|
||||
}
|
||||
|
||||
int api_screen_jpeg(int sock, const req_t *r)
|
||||
{
|
||||
int q = 70; /* 默认质量(体积/清晰折中) */
|
||||
char qv[8];
|
||||
if (qparam(r->query, "q", qv, sizeof qv)) {
|
||||
int t = atoi(qv);
|
||||
if (t >= 1 && t <= 100) q = t;
|
||||
}
|
||||
char dv[8];
|
||||
int delta = (qparam(r->query, "delta", dv, sizeof dv) && atoi(dv) == 1);
|
||||
|
||||
const char *dev = getenv("MEOW_FB_DEV"); if (!dev || !dev[0]) dev = FB_DEV;
|
||||
uint8_t *frame = NULL; int w = 0, h = 0;
|
||||
if (screen_read_frame(dev, &frame, &w, &h) != 0)
|
||||
return send_err(sock, 500, "INTERNAL", "读取帧缓冲失败(/dev/fb0)");
|
||||
|
||||
if (!delta) { /* 默认整帧(不破坏原行为) */
|
||||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||||
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
|
||||
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
|
||||
screen_prev_store(frame, w, h); /* 维护 delta 基线 */
|
||||
int ret = send_raw(sock, 200, "image/jpeg", jpeg, jlen);
|
||||
free(jpeg);
|
||||
free(frame);
|
||||
return ret;
|
||||
}
|
||||
|
||||
/* ---- delta 模式 ---- */
|
||||
const int B = SCREEN_BLOCK;
|
||||
int changed = 0, bx = 0, by = 0, bw = 0, bh = 0, total = 0;
|
||||
int force_full = (!g_screen_prev || g_screen_pw != w || g_screen_ph != h);
|
||||
if (!force_full) {
|
||||
changed = screen_diff_blocks(frame, g_screen_prev, w, h, &bx, &by, &bw, &bh, &total);
|
||||
if (bw * B >= w && bh * B >= h) force_full = 1; /* 全屏变化 → 直接发整帧 */
|
||||
} else {
|
||||
total = ((w + B - 1) / B) * ((h + B - 1) / B);
|
||||
}
|
||||
screen_prev_store(frame, w, h); /* 基线更新为当前帧 */
|
||||
|
||||
char hdr[224];
|
||||
int ret = -1;
|
||||
if (changed == 0 && !force_full) { /* 无变化:空 body,前端跳过叠加 */
|
||||
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
|
||||
"X-Meow-Changed: 0\r\nX-Meow-Blocks: %d", w, h, total);
|
||||
ret = send_head(sock, 200, "image/jpeg", 0, hdr);
|
||||
free(frame);
|
||||
return ret;
|
||||
}
|
||||
if (force_full) { /* 冷藏起/几何变化/全屏变化 → 整帧 */
|
||||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||||
int rc = mp_jpeg_encode_rgb565(frame, w, h, q, &jpeg, &jlen);
|
||||
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
|
||||
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 1\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
|
||||
"X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, changed ? changed : total, total);
|
||||
ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr);
|
||||
if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1;
|
||||
free(jpeg);
|
||||
free(frame);
|
||||
return ret;
|
||||
}
|
||||
/* 子图 = 变化块 bbox 外接矩形(8 对齐,钳到屏内),重编码为独立 JPEG 并以响应头回传偏移。 */
|
||||
int x0 = bx * B, y0 = by * B;
|
||||
int sw = bw * B, sh = bh * B;
|
||||
if (x0 + sw > w) sw = w - x0;
|
||||
if (y0 + sh > h) sh = h - y0;
|
||||
uint8_t *sub = (uint8_t *)malloc((size_t)sw * (size_t)sh * 2);
|
||||
if (!sub) { free(frame); return send_err(sock, 500, "INTERNAL", "内存不足"); }
|
||||
for (int y = 0; y < sh; y++)
|
||||
memcpy(sub + (size_t)y * (size_t)sw * 2,
|
||||
frame + ((size_t)(y0 + y) * (size_t)w + (size_t)x0) * 2,
|
||||
(size_t)sw * 2);
|
||||
uint8_t *jpeg = NULL; size_t jlen = 0;
|
||||
int rc = mp_jpeg_encode_rgb565(sub, sw, sh, q, &jpeg, &jlen);
|
||||
free(sub);
|
||||
if (rc != 0) { free(frame); return send_err(sock, 500, "INTERNAL", "JPEG 编码失败"); }
|
||||
snprintf(hdr, sizeof hdr, "X-Meow-Delta: 1\r\nX-Meow-Full: 0\r\nX-Meow-W: %d\r\nX-Meow-H: %d\r\n"
|
||||
"X-Meow-Sub-X: %d\r\nX-Meow-Sub-Y: %d\r\nX-Meow-Sub-W: %d\r\nX-Meow-Sub-H: %d\r\n"
|
||||
"X-Meow-Changed: %d\r\nX-Meow-Blocks: %d", w, h, x0, y0, sw, sh, changed, total);
|
||||
ret = send_head(sock, 200, "image/jpeg", (long long)jlen, hdr);
|
||||
if (ret == 0 && jlen && write_full(sock, jpeg, jlen) < 0) ret = -1;
|
||||
free(jpeg);
|
||||
free(frame);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int64_t touch_last_local_ms(const char *dev)
|
||||
{
|
||||
int fd = open(dev, O_RDONLY | O_NONBLOCK);
|
||||
if (fd < 0) return 0;
|
||||
int64_t last = 0;
|
||||
unsigned char ev[16];
|
||||
int64_t inject_deadline = g_last_self_inject_ms + 200; /* 注入事件时间 ≈ 注入时刻 */
|
||||
while (read(fd, ev, sizeof ev) == (ssize_t)sizeof ev) {
|
||||
int64_t sec = (int64_t)(*(int32_t *)(ev + 0));
|
||||
int64_t usec = (int64_t)(*(int32_t *)(ev + 4));
|
||||
int64_t t = sec * 1000 + usec / 1000;
|
||||
if (t <= inject_deadline) continue; /* 自注入,跳过 */
|
||||
if (t > last) last = t;
|
||||
}
|
||||
close(fd);
|
||||
return last;
|
||||
}
|
||||
|
||||
int inject_tap(const char *dev, int x, int y)
|
||||
{
|
||||
int fd = open(dev, O_WRONLY);
|
||||
if (fd < 0) return -1;
|
||||
struct input_event ev;
|
||||
#define __WE(t_, c_, v_) do { memset(&ev, 0, sizeof ev); ev.type = (t_); ev.code = (c_); \
|
||||
ev.value = (v_); if (write(fd, &ev, sizeof ev) != (ssize_t)sizeof ev) { close(fd); return -1; } } while (0)
|
||||
/* 事件顺序:先 BTN_TOUCH=1(按下边沿),后 ABS 坐标 —— launcher/meow_touch 在按下边沿
|
||||
* 会重置坐标 (-1),只在 down 之后到来的首个有效 ABS 才锁原点(goodix 时序免疫)。若先发
|
||||
* ABS 再发 BTN_TOUCH,原点永不锁定 → 抬手表被判为无效 tap(只高亮不启动)。 */
|
||||
__WE(EV_KEY, BTN_TOUCH, 1);
|
||||
__WE(EV_ABS, ABS_MT_SLOT, 0);
|
||||
__WE(EV_ABS, ABS_MT_TRACKING_ID, 7);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_X, x);
|
||||
__WE(EV_ABS, ABS_MT_POSITION_Y, y);
|
||||
__WE(EV_ABS, ABS_MT_TOUCH_MAJOR, 6);
|
||||
__WE(EV_ABS, ABS_X, x);
|
||||
__WE(EV_ABS, ABS_Y, y);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
usleep(100000); /* 按住 100ms 再抬起,抬起到按下的边沿被 launcher 视为一次 tap */
|
||||
__WE(EV_KEY, BTN_TOUCH, 0);
|
||||
__WE(EV_ABS, ABS_MT_TRACKING_ID, -1);
|
||||
__WE(EV_SYN, SYN_REPORT, 0);
|
||||
#undef __WE
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int api_input_tap(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *jx = jv_obj_get(root, "x");
|
||||
jv_t *jy = jv_obj_get(root, "y");
|
||||
if (!jx || jx->type != J_NUM || !jy || jy->type != J_NUM) {
|
||||
jv_free(root);
|
||||
return send_err(sock, 400, "BAD_REQUEST", "缺 x/y(数值)");
|
||||
}
|
||||
int x = (int)jx->inum, y = (int)jy->inum;
|
||||
jv_free(root);
|
||||
|
||||
const char *dev = getenv("MEOW_TOUCH_DEV"); if (!dev || !dev[0]) dev = TOUCH_DEV;
|
||||
/* 坐标 clamp 到屏幕范围(前端已按 480x720 映射,此处再兜底) */
|
||||
int sw = 480, sh = 720;
|
||||
{ int fx = 0, fy = 0; const char *fbdev = getenv("MEOW_FB_DEV"); if (!fbdev || !fbdev[0]) fbdev = FB_DEV;
|
||||
if (fb_dims(fbdev, &fx, &fy) == 0) { sw = fx; sh = fy; } }
|
||||
if (x < 0) x = 0;
|
||||
if (x >= sw) x = sw - 1;
|
||||
if (y < 0) y = 0;
|
||||
if (y >= sh) y = sh - 1;
|
||||
|
||||
/* 仲裁:本机最近触摸后 5s 内忽略注入 */
|
||||
int64_t now = now_mono_ms();
|
||||
int64_t last = touch_last_local_ms(dev);
|
||||
if (last > 0 && (now - last) < TOUCH_ACTIVE_MS) {
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dispatched\":false,\"reason\":\"local_touch_active\",\"cooldown_ms\":%lld}",
|
||||
(long long)(TOUCH_ACTIVE_MS - (now - last)));
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
|
||||
g_last_self_inject_ms = now_mono_ms();
|
||||
if (inject_tap(dev, x, y) < 0)
|
||||
return send_err(sock, 500, "INTERNAL", "触摸注入失败(无法打开触摸设备)");
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"dispatched\":true,\"x\":%d,\"y\":%d}", x, y);
|
||||
int ret = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return ret;
|
||||
}
|
||||
@@ -0,0 +1,618 @@
|
||||
/* mp_server.c — HTTP 服务器/路由 + 静态托管 + main(单线程 + poll 串行)。 */
|
||||
#include "mp.h"
|
||||
|
||||
static char g_webroot[512];
|
||||
static int g_port = DEFAULT_PORT;
|
||||
|
||||
|
||||
void req_free(req_t *r) { free(r->body); r->body = NULL; r->body_len = 0; }
|
||||
|
||||
int read_request(int sock, req_t *r)
|
||||
{
|
||||
char *buf = malloc(MAX_HTTP_HEADER);
|
||||
if (!buf) return -1;
|
||||
size_t hn = 0;
|
||||
int deadline_ms = 5000;
|
||||
ptrdiff_t hend = -1;
|
||||
for (;;) {
|
||||
struct pollfd pf = { sock, POLLIN, 0 };
|
||||
int pr = poll(&pf, 1, deadline_ms);
|
||||
if (pr <= 0) { free(buf); return -1; }
|
||||
ssize_t rd = read(sock, buf + hn, MAX_HTTP_HEADER - 1 - hn);
|
||||
if (rd <= 0) { free(buf); return -1; }
|
||||
hn += (size_t)rd;
|
||||
buf[hn] = 0;
|
||||
hend = find_header_end(buf, hn);
|
||||
if (hend >= 0) break;
|
||||
if (hn >= MAX_HTTP_HEADER - 2) { free(buf); return -1; }
|
||||
}
|
||||
/* 头结束在 hend,body 起点在 hend+4 */
|
||||
char *hdr = buf;
|
||||
char *line_end = strstr(hdr, "\r\n");
|
||||
if (!line_end) { free(buf); return -1; }
|
||||
*line_end = 0;
|
||||
char method[8], pathraw[512], proto[16];
|
||||
if (sscanf(hdr, "%7s %511s %15s", method, pathraw, proto) < 2) { free(buf); return -1; }
|
||||
snprintf(r->method, sizeof r->method, "%s", method);
|
||||
split_path_query(pathraw, r->path, sizeof r->path, r->query, sizeof r->query);
|
||||
size_t clen = 0;
|
||||
char *hp = line_end + 2;
|
||||
while (hp < buf + hend && strncmp(hp, "\r\n", 2)) {
|
||||
char *he = strstr(hp, "\r\n");
|
||||
if (!he || he > buf + hend) break;
|
||||
*he = 0;
|
||||
if (!strncasecmp(hp, "Content-Length:", 15)) clen = (size_t)strtoul(hp + 15, NULL, 10);
|
||||
else if (!strncasecmp(hp, "Content-Type:", 13)) {
|
||||
char *v = hp + 13;
|
||||
while (*v == ' ') v++;
|
||||
snprintf(r->content_type, sizeof r->content_type, "%s", v);
|
||||
}
|
||||
else if (!strncasecmp(hp, "Authorization:", 14)) {
|
||||
char *v = hp + 14;
|
||||
while (*v == ' ') v++;
|
||||
snprintf(r->authorization, sizeof r->authorization, "%s", v);
|
||||
}
|
||||
else if (!strncasecmp(hp, "X-MeowToken:", 12)) {
|
||||
char *v = hp + 12;
|
||||
while (*v == ' ') v++;
|
||||
snprintf(r->x_meow_token, sizeof r->x_meow_token, "%s", v);
|
||||
}
|
||||
else if (!strncasecmp(hp, "Cookie:", 7)) {
|
||||
char *v = hp + 7;
|
||||
while (*v == ' ') v++;
|
||||
snprintf(r->cookie, sizeof r->cookie, "%s", v);
|
||||
}
|
||||
hp = he + 2;
|
||||
}
|
||||
char *body_start = buf + hend + 4; /* body 实际起点 */
|
||||
size_t buffered = (hn > (size_t)(body_start - buf)) ? (hn - (size_t)(body_start - buf)) : 0;
|
||||
/* 上传(multipart)走更严上限:14MB 内存约束下不整读超 MAX_UPLOAD_BODY。超限 → -2(413)。
|
||||
* 字体库 tar.gz 端点例外:gbk/full 字库 png+idx 可能 10-20MB,放宽到 MAX_FONT_UPLOAD(32MB)。 */
|
||||
size_t cap = MAX_HTTP_BODY;
|
||||
if (strncasecmp(r->content_type, "multipart/form-data", 19) == 0)
|
||||
cap = (!strcmp(r->path, "/api/fonts/import")) ? MAX_FONT_UPLOAD : MAX_UPLOAD_BODY;
|
||||
if (clen > cap) { free(buf); return -2; }
|
||||
if (clen == 0) { free(buf); r->body = NULL; r->body_len = 0; return 0; }
|
||||
char *body = malloc(clen + 1);
|
||||
if (!body) { free(buf); return -1; }
|
||||
size_t got = 0;
|
||||
if (buffered > 0) {
|
||||
size_t take = buffered < clen ? buffered : clen;
|
||||
memcpy(body, body_start, take);
|
||||
got = take;
|
||||
}
|
||||
free(buf);
|
||||
while (got < clen) {
|
||||
struct pollfd pf = { sock, POLLIN, 0 };
|
||||
int pr = poll(&pf, 1, 5000);
|
||||
if (pr <= 0) { free(body); return -1; }
|
||||
ssize_t rd = read(sock, body + got, clen - got);
|
||||
if (rd <= 0) { free(body); return -1; }
|
||||
got += (size_t)rd;
|
||||
}
|
||||
body[clen] = 0;
|
||||
r->body = body;
|
||||
r->body_len = clen;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int write_full(int sock, const void *b, size_t n)
|
||||
{
|
||||
const char *p = (const char *)b;
|
||||
size_t off = 0;
|
||||
while (off < n) {
|
||||
ssize_t w = write(sock, p + off, n - off);
|
||||
if (w <= 0) return -1;
|
||||
off += (size_t)w;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_head(int sock, int status, const char *ctype, long long clen, const char *extra)
|
||||
{
|
||||
char h[512];
|
||||
const char *sts = "OK";
|
||||
switch (status) {
|
||||
case 200: sts = "OK"; break;
|
||||
case 201: sts = "Created"; break;
|
||||
case 202: sts = "Accepted"; break;
|
||||
case 400: sts = "Bad Request"; break;
|
||||
case 401: sts = "Unauthorized"; break;
|
||||
case 403: sts = "Forbidden"; break;
|
||||
case 404: sts = "Not Found"; break;
|
||||
case 405: sts = "Method Not Allowed"; break;
|
||||
case 409: sts = "Conflict"; break;
|
||||
case 413: sts = "Payload Too Large"; break;
|
||||
case 415: sts = "Unsupported Media Type"; break;
|
||||
case 422: sts = "Unprocessable Entity"; break;
|
||||
case 500: sts = "Internal Server Error"; break;
|
||||
case 501: sts = "Not Implemented"; break;
|
||||
case 503: sts = "Service Unavailable"; break;
|
||||
default: sts = "OK"; break;
|
||||
}
|
||||
snprintf(h, sizeof h, "HTTP/1.1 %d %s\r\nServer: MeowPanel/%s\r\nContent-Type: %s\r\nContent-Length: %lld\r\nConnection: close\r\n%s%s\r\n",
|
||||
status, sts, PANEL_VERSION, ctype, clen, extra ? extra : "", extra ? "\r\n" : "");
|
||||
return write_full(sock, h, strlen(h));
|
||||
}
|
||||
|
||||
int send_raw(int sock, int status, const char *ctype, const void *body, size_t bodylen)
|
||||
{
|
||||
if (send_head(sock, status, ctype, (long long)bodylen, NULL) < 0) return -1;
|
||||
if (bodylen && write_full(sock, body, bodylen) < 0) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int send_ok(int sock, const char *data)
|
||||
{
|
||||
static const char prefix[] = "{\"ok\":true,\"data\":";
|
||||
size_t pl = sizeof prefix - 1; /* 18 */
|
||||
size_t dl = strlen(data);
|
||||
size_t tot = pl + dl + 1;
|
||||
char *b = malloc(tot + 1);
|
||||
if (!b) {
|
||||
const char *e = "{\"ok\":false,\"error\":{\"code\":\"INTERNAL\",\"message\":\"内存不足\"}}";
|
||||
return send_raw(sock, 500, "application/json; charset=utf-8", e, strlen(e));
|
||||
}
|
||||
memcpy(b, prefix, pl);
|
||||
memcpy(b + pl, data, dl);
|
||||
b[pl + dl] = '}';
|
||||
b[pl + dl + 1] = 0;
|
||||
int rc = send_raw(sock, 200, "application/json; charset=utf-8", b, tot);
|
||||
free(b);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int send_err(int sock, int status, const char *code, const char *msg)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"ok\":false,\"error\":{\"code\":");
|
||||
sA_str(&s, code ? code : "INTERNAL");
|
||||
sA(&s, ",\"message\":");
|
||||
sA_str(&s, msg ? msg : "");
|
||||
sA(&s, "}}");
|
||||
int rc = send_raw(sock, status, "application/json; charset=utf-8", s.b, s.n);
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int send_json(int sock, int status, const char *data)
|
||||
{
|
||||
if (status == 200) return send_ok(sock, data);
|
||||
return send_raw(sock, status, "application/json; charset=utf-8", data, strlen(data));
|
||||
}
|
||||
|
||||
int send_file(int sock, int status, const char *ctype, const char *abspath, const char *filename)
|
||||
{
|
||||
int fd = open(abspath, O_RDONLY);
|
||||
if (fd < 0) return send_err(sock, 404, "NOT_FOUND", "文件不存在");
|
||||
struct stat st;
|
||||
if (fstat(fd, &st) < 0 || !S_ISREG(st.st_mode)) { close(fd); return send_err(sock, 404, "NOT_FOUND", "非普通文件"); }
|
||||
char extra[256];
|
||||
if (filename) snprintf(extra, sizeof extra, "Content-Disposition: attachment; filename=\"%s\"", filename);
|
||||
else extra[0] = 0;
|
||||
if (send_head(sock, status, ctype, (long long)st.st_size, extra[0] ? extra : NULL) < 0) { close(fd); return -1; }
|
||||
char buf[8192];
|
||||
ssize_t n;
|
||||
while ((n = read(fd, buf, sizeof buf)) > 0) {
|
||||
if (write_full(sock, buf, (size_t)n) < 0) { close(fd); return -1; }
|
||||
}
|
||||
close(fd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *web_root(void)
|
||||
{
|
||||
if (g_webroot[0]) return g_webroot;
|
||||
char exe[512];
|
||||
ssize_t n = readlink("/proc/self/exe", exe, sizeof exe - 1);
|
||||
if (n <= 0) { snprintf(g_webroot, sizeof g_webroot, "."); return g_webroot; }
|
||||
exe[n] = 0;
|
||||
char *slash = strrchr(exe, '/');
|
||||
if (slash) *slash = 0;
|
||||
snprintf(g_webroot, sizeof g_webroot, "%s", exe[0] ? exe : ".");
|
||||
return g_webroot;
|
||||
}
|
||||
|
||||
const char *mime_for(const char *path)
|
||||
{
|
||||
const char *ext = strrchr(path, '.');
|
||||
if (!ext) return "application/octet-stream";
|
||||
if (!strcasecmp(ext, ".html")) return "text/html; charset=utf-8";
|
||||
if (!strcasecmp(ext, ".css")) return "text/css; charset=utf-8";
|
||||
if (!strcasecmp(ext, ".js")) return "application/javascript; charset=utf-8";
|
||||
if (!strcasecmp(ext, ".json")) return "application/json; charset=utf-8";
|
||||
if (!strcasecmp(ext, ".png")) return "image/png";
|
||||
if (!strcasecmp(ext, ".ico")) return "image/x-icon";
|
||||
if (!strcasecmp(ext, ".svg")) return "image/svg+xml";
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
int serve_static(int sock, const char *path)
|
||||
{
|
||||
const char *root = web_root();
|
||||
char rel[512];
|
||||
/* / → index.html;/assets/<f> → <f>;/<f> → <f>(meowpanel.css/js 等) */
|
||||
if (!strcmp(path, "/")) snprintf(rel, sizeof rel, "index.html");
|
||||
else if (!strncmp(path, "/assets/", 8)) snprintf(rel, sizeof rel, "%s", path + 8);
|
||||
else snprintf(rel, sizeof rel, "%s", path + 1);
|
||||
if (strstr(rel, "..") || rel[0] == '/') return send_err(sock, 403, "FORBIDDEN", "路径逃逸");
|
||||
/* 只允许安全文件(防读到二进制/任意文件) */
|
||||
char full[1024];
|
||||
snprintf(full, sizeof full, "%s/%s", root, rel);
|
||||
/* 敏感名拒绝 */
|
||||
const char *base = strrchr(full, '/');
|
||||
base = base ? base + 1 : full;
|
||||
if (!strcmp(base, "meowpanel") || !strcmp(base, "meowpanel_host")
|
||||
|| !strcmp(base, "meowpanel.session") ) return send_err(sock, 403, "FORBIDDEN", "禁止访问可执行/会话文件");
|
||||
FILE *f = fopen(full, "rb");
|
||||
if (!f) return send_err(sock, 404, "NOT_FOUND", "资源不存在");
|
||||
fclose(f);
|
||||
return send_file(sock, 200, mime_for(full), full, NULL);
|
||||
}
|
||||
|
||||
int handle(int sock, req_t *r)
|
||||
{
|
||||
const char *p = r->path;
|
||||
if (strncmp(p, "/api/", 5) != 0) {
|
||||
if (strcmp(r->method, "GET") != 0 && strcmp(r->method, "HEAD") != 0)
|
||||
return send_err(sock, 405, "METHOD_NOT_ALLOWED", "静态资源仅支持 GET");
|
||||
return serve_static(sock, p);
|
||||
}
|
||||
|
||||
/* ---- 可选鉴权门禁(启用后除 /api/auth 系列外全 /api/ 端点需会话) ---- */
|
||||
if (require_auth(sock, r) != 1) return 0;
|
||||
|
||||
/* ---- 鉴权端点(登录/设置/登出/状态:公开——访问面板即需登录,须先经此建立会话) ---- */
|
||||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/login")) return api_auth_login(sock, r);
|
||||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/setup")) return api_auth_setup(sock, r);
|
||||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/auth/logout")) return api_auth_logout(sock, r);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/auth/status")) return api_auth_status(sock, r);
|
||||
|
||||
/* ---- 性能/状态 ---- */
|
||||
if (!strcmp(r->method, "GET") && (!strcmp(p, "/api/status") || !strcmp(p, "/api/perf")))
|
||||
return api_perf(sock, p);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/system"))
|
||||
return api_system(sock);
|
||||
|
||||
/* ---- 字体库(导入/导出/列表) ---- */
|
||||
if (!strcmp(r->method, "POST") && !strcmp(p, "/api/fonts/import")) return api_fonts_import(sock, r);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts")) return api_fonts_list(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/fonts/export")) return api_fonts_export(sock, r);
|
||||
|
||||
/* ---- 音频状态(meow_audiod 快照,只读;含 rms/peak/lvlseq 电平键;monitor=实时监控日志尾读) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio")) return api_audio(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/audio/monitor")) return api_audio_monitor(sock, r->query);
|
||||
|
||||
/* ---- 批1 只读增强:功耗/进程服务/首页聚合(均走 PIN 鉴权读级) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power")) return api_power(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/policy")) return api_power_policy_get(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/history")) return api_power_history(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/power/events")) return api_power_events(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/ams")) return api_ams(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/svcd")) return api_svcd(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/overview")) return api_overview(sock);
|
||||
if (!strcmp(r->method, "GET")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/ams/", p, seg, sizeof seg) && seg[0])
|
||||
return api_ams_history(sock, seg); /* GET /api/ams/:pkg → am history(崩溃根因) */
|
||||
}
|
||||
|
||||
/* ---- 网络管理(meow_netmgrd 期2/期3 透传:读级任意调用者;flush/policy 写级走 require_auth) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/status")) return api_net_status(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/ifaces")) return api_net_ifaces(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/policy")) return api_net_policy_get(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/conns")) return api_net_conns(sock, r->query);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/dns")) return api_net_dns(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/net/log")) return api_net_log(sock);
|
||||
|
||||
/* ---- 实时画面(阶段 C) ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/screen.jpeg"))
|
||||
return api_screen_jpeg(sock, r);
|
||||
|
||||
/* ---- 系统动作 ---- */
|
||||
if (!strcmp(r->method, "POST")) {
|
||||
if (!strcmp(p, "/api/system/reboot")) return api_system_action(sock, "reboot", r);
|
||||
if (!strcmp(p, "/api/system/shutdown")) return api_system_action(sock, "shutdown", r);
|
||||
if (!strcmp(p, "/api/system/ota")) return api_system_action(sock, "ota", r);
|
||||
if (!strcmp(p, "/api/system/pin-unlock-once")) return api_pin_unlock_once(sock);
|
||||
if (!strcmp(p, "/api/debug/report_bad_json")) {
|
||||
/* 前端 NOT_JSON 自动回传浏览器收到的原始字节(抓间歇性破包) */
|
||||
FILE *df = fopen("/tmp/overview_client_bad.txt", "w");
|
||||
if (df && r->body && r->body_len) fwrite(r->body, 1, r->body_len, df);
|
||||
if (df) fclose(df);
|
||||
panel_log("debug: client bad json captured %zu bytes -> /tmp/overview_client_bad.txt", (size_t)r->body_len);
|
||||
return send_json(sock, 200, "{\"captured\":true}");
|
||||
}
|
||||
if (!strcmp(p, "/api/settings")) return api_settings_batch(sock, r);
|
||||
if (!strcmp(p, "/api/settings/restore")) return api_settings_restore(sock);
|
||||
if (!strcmp(p, "/api/theme")) return api_theme_put(sock, r);
|
||||
if (!strcmp(p, "/api/files/upload")) return api_files_upload(sock, r);
|
||||
if (!strcmp(p, "/api/files/mkdir")) return api_files_mkdir(sock, r);
|
||||
if (!strcmp(p, "/api/files/rename")) return api_files_rename(sock, r);
|
||||
if (!strcmp(p, "/api/apps/install")) return api_apps_install(sock, r);
|
||||
if (!strcmp(p, "/api/notifications/dismiss_all") || !strcmp(p, "/api/notifications/clear")) return api_notification_dismiss_all(sock);
|
||||
if (!strcmp(p, "/api/net/dns/flush")) return api_net_dns_flush(sock);
|
||||
if (!strcmp(p, "/api/net/policy")) return api_net_policy_set(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/power/policy")) return api_power_policy_set(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/audio/route")) return api_audio_route(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/audio/volume")) return api_audio_volume(sock, r->body, r->body_len);
|
||||
if (!strcmp(p, "/api/key")) return api_key_batch(sock, r);
|
||||
if (!strcmp(p, "/api/input/tap")) return api_input_tap(sock, r);
|
||||
char seg[256];
|
||||
if (path_segment("/api/key/", p, seg, sizeof seg))
|
||||
return api_key_single(sock, atoi(seg), r);
|
||||
/* POST /api/ams/<pkg>/start|kill(管启/停;require_auth 已护,kill 前台前端二次确认) */
|
||||
if (!strncmp(p, "/api/ams/", 9)) {
|
||||
size_t pl = strlen(p);
|
||||
if (pl > 5 && !strcmp(p + pl - 6, "/start")) {
|
||||
char pkg[256];
|
||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||
return api_ams_ctl(sock, pkg, 0);
|
||||
} else if (pl > 4 && !strcmp(p + pl - 5, "/kill")) {
|
||||
char pkg[256];
|
||||
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
|
||||
return api_ams_ctl(sock, pkg, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 设置 ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/settings")) return api_settings_get_all(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/theme")) return api_theme_get(sock);
|
||||
if (!strcmp(r->method, "PUT")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/settings/", p, seg, sizeof seg))
|
||||
return api_settings_put(sock, seg, r);
|
||||
}
|
||||
if (!strcmp(r->method, "GET")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/settings/", p, seg, sizeof seg))
|
||||
return api_settings_get_any(sock, seg);
|
||||
}
|
||||
|
||||
/* ---- 文件 ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files")) return api_files_list(sock, r);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/read")) return api_files_read(sock, r);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/files/download")) return api_files_download(sock, r);
|
||||
if (!strcmp(r->method, "DELETE") && !strcmp(p, "/api/files")) return api_files_delete(sock, r);
|
||||
|
||||
/* ---- 应用 ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/apps")) return api_apps_list(sock);
|
||||
if (!strcmp(r->method, "GET")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/apps/", p, seg, sizeof seg)) {
|
||||
/* :pkg/export 结尾(path_segment 已把 seg 截为 pkg,这里检查原 path 尾缀) */
|
||||
size_t pl = strlen(p);
|
||||
int is_export = (pl >= 7 && !strcmp(p + pl - 7, "/export"));
|
||||
if (is_export) return api_apps_export(sock, seg);
|
||||
return api_apps_info(sock, seg);
|
||||
}
|
||||
}
|
||||
if (!strcmp(r->method, "DELETE")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/apps/", p, seg, sizeof seg))
|
||||
return api_apps_remove(sock, seg, r);
|
||||
}
|
||||
|
||||
/* ---- 日志 ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs")) return api_logs_list(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/crash")) return api_logs_crash(sock);
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/logs/read")) return api_logs_read(sock, r);
|
||||
|
||||
/* ---- 通知 ---- */
|
||||
if (!strcmp(r->method, "GET") && !strcmp(p, "/api/notifications")) return api_notifications(sock);
|
||||
if (!strcmp(r->method, "DELETE")) {
|
||||
char seg[256];
|
||||
if (path_segment("/api/notifications/", p, seg, sizeof seg)) {
|
||||
unsigned nid = (unsigned)strtoul(seg, NULL, 10);
|
||||
return api_notification_delete(sock, nid);
|
||||
}
|
||||
}
|
||||
|
||||
return send_err(sock, 404, "NOT_FOUND", "端点不存在");
|
||||
}
|
||||
|
||||
int read_port_config(void)
|
||||
{
|
||||
/* 配置文件 meowpanel.conf(web 根目录),格式 key=value,取 port */
|
||||
char path[1024];
|
||||
snprintf(path, sizeof path, "%s/meowpanel.conf", web_root());
|
||||
FILE *f = fopen(path, "r");
|
||||
if (f) {
|
||||
char line[160];
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue;
|
||||
char *k = line; while (*k == ' ' || *k == '\t') k++;
|
||||
if (!strncmp(k, "port", 4) && k[4] == '=') g_port = atoi(eq + 1);
|
||||
}
|
||||
fclose(f);
|
||||
}
|
||||
const char *env = getenv("MEOWPORT");
|
||||
if (env && env[0]) g_port = atoi(env);
|
||||
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
|
||||
return g_port;
|
||||
}
|
||||
|
||||
void handle_conn(int sock)
|
||||
{
|
||||
req_t r;
|
||||
memset(&r, 0, sizeof r);
|
||||
int rr = read_request(sock, &r);
|
||||
if (rr != 0) {
|
||||
if (rr == -2) send_err(sock, 413, "PAYLOAD_TOO_LARGE", "请求体过大(超过上传上限)");
|
||||
close(sock);
|
||||
return;
|
||||
}
|
||||
handle(sock, &r);
|
||||
req_free(&r);
|
||||
close(sock);
|
||||
}
|
||||
|
||||
int panel_selftest(void)
|
||||
{
|
||||
const char *trunc = "流媒体客户\xE7"; /* 事故里被腰斩的显示名(16B,末字节为「端」首字节) */
|
||||
const char *oknat = "流媒体客户端"; /* 完整合法名(18B,回归确认不误伤) */
|
||||
size_t n = strlen(trunc);
|
||||
|
||||
/* 1a) utf8_sanitize:截断串必被改写,长度保持,产物合法 UTF-8 */
|
||||
char san[64];
|
||||
if (!utf8_sanitize(san, trunc, n)) { printf("SELFTEST FAIL: utf8_sanitize 未改写截断串\n"); return 1; }
|
||||
if (strlen(san) != n) { printf("SELFTEST FAIL: utf8_sanitize 长度变化 %zu->%zu\n", n, strlen(san)); return 1; }
|
||||
if (!utf8_valid_n(san, strlen(san))) { printf("SELFTEST FAIL: utf8_sanitize 产物含非法字节\n"); return 1; }
|
||||
|
||||
/* 1b) 完整合法名不得被改写(误伤检测) */
|
||||
char san2[64];
|
||||
if (utf8_sanitize(san2, oknat, strlen(oknat)) != 0 || strcmp(san2, oknat) != 0) {
|
||||
printf("SELFTEST FAIL: utf8_sanitize 误改合法 UTF-8\n"); return 1;
|
||||
}
|
||||
|
||||
/* 2) sA_str 嵌入:产物合法 UTF-8 且 jv_parse 完整解析 */
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"pkg\":");
|
||||
sA_str(&s, "com.example.trunc");
|
||||
sA(&s, ",\"name\":");
|
||||
sA_str(&s, trunc);
|
||||
sA(&s, ",\"state\":");
|
||||
sA_str(&s, "running");
|
||||
sA(&s, "}");
|
||||
if (!s.b) { printf("SELFTEST FAIL: sA_str 输出空\n"); return 1; }
|
||||
if (!utf8_valid_n(s.b, s.n)) { printf("SELFTEST FAIL: sA_str 产物含非法字节\n"); sbuf_free(&s); return 1; }
|
||||
jv_t *r = jv_parse(s.b, (int)s.n);
|
||||
if (!r) { printf("SELFTEST FAIL: sA_str 产物 JSON 无法解析\n"); sbuf_free(&s); return 1; }
|
||||
jv_t *nm = jv_obj_get(r, "name");
|
||||
if (!nm || nm->type != J_STR) { printf("SELFTEST FAIL: sA_str 产物缺 name\n"); jv_free(r); sbuf_free(&s); return 1; }
|
||||
jv_free(r);
|
||||
sbuf_free(&s);
|
||||
|
||||
/* 3) ams_fill 桩:模拟 APP 行 parse 出的截断名(join_tokens 后)→ apps 数组可解析 */
|
||||
sbuf_t a = {0};
|
||||
sA(&a, "{\"available\":true,\"count\":1,\"apps\":[{\"pkg\":");
|
||||
sA_str(&a, "com.example.trunc");
|
||||
sA(&a, ",\"name\":");
|
||||
sA_str(&a, trunc);
|
||||
sA(&a, ",\"state\":");
|
||||
sA_str(&a, "running");
|
||||
sA(&a, ",\"pid\":123}]}");
|
||||
if (!a.b || !utf8_valid_n(a.b, a.n)) { printf("SELFTEST FAIL: ams_fill 桩产物含非法字节/为空\n"); sbuf_free(&a); return 1; }
|
||||
jv_t *ar = jv_parse(a.b, (int)a.n);
|
||||
if (!ar) { printf("SELFTEST FAIL: ams_fill 桩产物 JSON 无法解析\n"); sbuf_free(&a); return 1; }
|
||||
jv_free(ar);
|
||||
sbuf_free(&a);
|
||||
|
||||
/* 4) net_policy_parse:喂含坏字节域名(截断「段」字)+ allow/deny 混合 → 产物合法 JSON、无非法字节。
|
||||
* 域名/实体名都走 sA_str_n 消毒(坏字节→'?'),故解析器产物必为合法 JSON。 */
|
||||
{
|
||||
static const char ptext[] =
|
||||
"# 策略(NETMGR_DESIGN §6)log_level=debug enforce=dns_only(SOCK_DESTROY 在本内核为 no-op)\n"
|
||||
"app:com.example.trunc net=block domain=+good.example,-bad\xE7\n"
|
||||
"svc:meowpanel net=allow domain=+svc.local\n";
|
||||
sbuf_t p = {0};
|
||||
net_policy_parse(ptext, &p);
|
||||
if (!p.b || !utf8_valid_n(p.b, p.n)) { printf("SELFTEST FAIL: net_policy_parse 产物含非法字节/为空\n"); sbuf_free(&p); return 1; }
|
||||
jv_t *pr = jv_parse(p.b, (int)p.n);
|
||||
if (!pr) { printf("SELFTEST FAIL: net_policy_parse 产物 JSON 无法解析\n"); sbuf_free(&p); return 1; }
|
||||
jv_t *rules = jv_obj_get(pr, "rules");
|
||||
if (!rules || rules->type != J_ARR || !rules->child) { printf("SELFTEST FAIL: net_policy_parse 缺 rules 数组\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||
jv_t *enf = jv_obj_get(pr, "enforce");
|
||||
if (!enf || enf->type != J_STR || !enf->str || strcmp(enf->str, "dns_only") != 0) { printf("SELFTEST FAIL: net_policy_parse enforce 未如实透传 dns_only\n"); jv_free(pr); sbuf_free(&p); return 1; }
|
||||
jv_free(pr);
|
||||
sbuf_free(&p);
|
||||
}
|
||||
|
||||
/* 5) ams_history_parse:喂 EVT 行 + 截断崩溃 detail(含坏字节)→ available:false(降级,绝不发非法 JSON)。
|
||||
* detail 合法时 → 合法 JSON 可解析。 */
|
||||
{
|
||||
char bad[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc 崩溃在函数 foo\xE7\n";
|
||||
sbuf_t h = {0};
|
||||
ams_history_parse(bad, &h, "com.example.trunc");
|
||||
if (!h.b || strstr(h.b, "available\":false") == NULL) { printf("SELFTEST FAIL: ams_history_parse 截断 detail 未降级 available:false\n"); sbuf_free(&h); return 1; }
|
||||
sbuf_free(&h);
|
||||
|
||||
char good[] = "# amd history com.example.trunc\nEVT 1786523400 123 crash com.example.trunc SIGSEGV at addr 0x10\nEVT 1786523500 124 ams_start com.example.trunc ok forwarded\n";
|
||||
sbuf_t h2 = {0};
|
||||
ams_history_parse(good, &h2, "com.example.trunc");
|
||||
if (!h2.b || !utf8_valid_n(h2.b, h2.n) || !jv_parse(h2.b, (int)h2.n)) { printf("SELFTEST FAIL: ams_history_parse 合法 detail 无法解析/含非法字节\n"); sbuf_free(&h2); return 1; }
|
||||
sbuf_free(&h2);
|
||||
}
|
||||
|
||||
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 新增端点池 → 合法 JSON + 无非法字节\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
/* 参数:-p PORT(可选)。默认端口 8080。 */
|
||||
int port = DEFAULT_PORT;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
if (!strcmp(argv[i], "--selftest")) return panel_selftest();
|
||||
if (!strcmp(argv[i], "-p") && i + 1 < argc) port = atoi(argv[++i]);
|
||||
else if (!strcmp(argv[i], "--sock-perf") && i + 1 < argc) g_sock_perf = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-power") && i + 1 < argc) g_sock_power = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-launcher") && i + 1 < argc) g_sock_launcher = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-audio") && i + 1 < argc) g_sock_audio = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-net") && i + 1 < argc) g_sock_net = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock-pm") && i + 1 < argc) g_sock_pm = argv[++i];
|
||||
else if (!strcmp(argv[i], "--sock") && i + 1 < argc) { g_sock_perf = argv[++i]; g_sock_power = g_sock_perf; }
|
||||
else if (!strcmp(argv[i], "-h") || !strcmp(argv[i], "--help")) {
|
||||
printf("MeowPanel backend %s\n", PANEL_VERSION);
|
||||
printf("usage: meowpanel [-p PORT] [--sock-perf P] [--sock-power P] [--sock-launcher P] [--sock-audio P] [--sock-net P] [--sock-pm P]\n");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
init_tf_paths(); /* 先定卡根/派生路径(env 可覆盖,宿主测试用) */
|
||||
const char *env = getenv("MEOW_IPC_PERF_SOCK");
|
||||
if (env && env[0]) g_sock_perf = env;
|
||||
env = getenv("MEOW_IPC_POWER_SOCK");
|
||||
if (env && env[0]) g_sock_power = env;
|
||||
env = getenv("MEOW_IPC_LAUNCHER_SOCK");
|
||||
if (env && env[0]) g_sock_launcher = env;
|
||||
env = getenv("MEOW_AUDIO_SOCK");
|
||||
if (env && env[0]) g_sock_audio = env; /* 与 audiod CLI 同 env(宿主/qemu 测试用) */
|
||||
env = getenv("MEOW_NET_SOCK");
|
||||
if (env && env[0]) g_sock_net = env; /* 与 netmgrd/meow_net 同 env(宿主/qemu 测试用) */
|
||||
env = getenv("MEOW_PM_SOCK");
|
||||
if (env && env[0]) g_sock_pm = env; /* 与 powermgr/meow_power 同 env(宿主/qemu 测试用) */
|
||||
env = getenv("MEOW_AUTH_TTL");
|
||||
if (env && env[0]) g_auth_ttl = (time_t)atol(env); /* 会话 TTL 覆盖(宿主测试用) */
|
||||
env = getenv("MEOW_SETTINGS_FILE");
|
||||
if (env && env[0]) g_settings_file = env; /* 设置文件覆盖(宿主测试用;设备默认 /usr/data/meow/settings.conf) */
|
||||
|
||||
(void)read_port_config(); /* 应用 meowpanel.conf 的 port(env/-p 优先) */
|
||||
/* 优先级:-p 已设则保留;否则用 conf/env(read_port_config 会写 g_port) */
|
||||
if (port != DEFAULT_PORT) g_port = port;
|
||||
else g_port = read_port_config();
|
||||
/* 再按 env MEOWPORT 覆盖 conf(-p 最高) */
|
||||
if (port == DEFAULT_PORT) {
|
||||
const char *ep = getenv("MEOWPORT");
|
||||
if (ep && ep[0]) g_port = atoi(ep);
|
||||
}
|
||||
if (g_port <= 0 || g_port > 65535) g_port = DEFAULT_PORT;
|
||||
|
||||
signal(SIGPIPE, SIG_IGN);
|
||||
web_root();
|
||||
panel_log("MeowPanel %s listening on port %d (web root %s)", PANEL_VERSION, g_port, web_root());
|
||||
|
||||
int lfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (lfd < 0) { panel_log("socket fail: %s", strerror(errno)); return 1; }
|
||||
int on = 1;
|
||||
setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof on);
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof addr);
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
addr.sin_port = htons((uint16_t)g_port);
|
||||
if (bind(lfd, (struct sockaddr *)&addr, sizeof addr) < 0) {
|
||||
panel_log("bind :%d fail: %s", g_port, strerror(errno));
|
||||
close(lfd);
|
||||
return 1;
|
||||
}
|
||||
if (listen(lfd, 16) < 0) { panel_log("listen fail: %s", strerror(errno)); close(lfd); return 1; }
|
||||
|
||||
for (;;) {
|
||||
int c = accept(lfd, NULL, NULL);
|
||||
if (c < 0) { if (errno == EINTR) continue; continue; }
|
||||
handle_conn(c);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,596 @@
|
||||
/* mp_store.c — 设置镜像(settings.conf)+ 主题 + /api/settings*,/api/theme*。权威来源:meow_settings.c g_defs(逐键一致)。 */
|
||||
#include "mp.h"
|
||||
|
||||
const char *g_settings_file = SETTINGS_FILE;
|
||||
static const setdef_t g_sets[] = {
|
||||
/* 显示 */
|
||||
{ "display.brightness", ST_INT, "101", 1, 100, NULL, NULL, "显示" },
|
||||
{ "display.brightness_min", ST_INT, "1", 0, 255, NULL, NULL, "显示" },
|
||||
{ "display.screen_off_mode", ST_ENUM, "panel",0,0, "panel,black", "面板下电,背光全黑", "显示" },
|
||||
{ "display.screen_off_timeout", ST_INT, "30", 0, 86400,NULL, NULL, "显示" },
|
||||
{ "display.refresh_rate", ST_ENUM, "60", 0,0, "60,30,15", "60Hz,30Hz,15Hz", "显示" },
|
||||
{ "display.refresh_always_max", ST_BOOL, "0", 0,0, NULL, NULL, "显示" },
|
||||
{ "display.theme", ST_ENUM, "dark", 0,0, "dark,light", "深色,浅色", "显示" },
|
||||
{ "display.theme_mode", ST_ENUM, "manual",0,0, "manual,auto", "手动,时段自动", "显示" },
|
||||
{ "display.theme_dark_start", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
|
||||
{ "display.theme_dark_end", ST_INT, "0", 0, 1439, NULL, NULL, "显示" },
|
||||
{ "display.background", ST_STR, "black",0,0, NULL, NULL, "显示" },
|
||||
{ "display.multi_font", ST_INT, "0", 0, 1, NULL, NULL, "显示" },
|
||||
/* 电源 */
|
||||
{ "power.longpress_menu_ms", ST_INT, "3000", 0,0, NULL, NULL, "电源" },
|
||||
{ "power.forceoff_ms", ST_INT, "8000", 0,0, NULL, NULL, "电源" },
|
||||
{ "power.defeat_hotkey", ST_STR, "power_voldown",0,0, NULL, NULL, "电源" },
|
||||
/* 开机 */
|
||||
{ "boot.default", ST_ENUM, "meow", 0,0, "meow,hiby", "MeowOS,HiByOS", "开机" },
|
||||
{ "boot.menu_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "开机" },
|
||||
{ "boot.timeout_ms", ST_INT, "5000", 0,0, NULL, NULL, "开机" },
|
||||
/* 输入 */
|
||||
{ "input.hotkey_seq", ST_STR, "volup,volup,volup",0,0, NULL, NULL, "输入" },
|
||||
{ "input.hotkey_window_ms", ST_INT, "2500", 0,0, NULL, NULL, "输入" },
|
||||
{ "input.touch_arm_delay_ms", ST_INT, "700", 0,0, NULL, NULL, "输入" },
|
||||
{ "input.swipe_invert", ST_BOOL, "0", 0,0, NULL, NULL, "输入" },
|
||||
/* 字体/主题 */
|
||||
{ "font.file", ST_STR, "MeowOS/fonts/default/regular/font.png",0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.class_cjk", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.class_cjk_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.class_num", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.class_latin", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.class_latin_sym", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.class_other", ST_STR, "", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.load_full", ST_INT, "0", 0,1, NULL, NULL, "字体主题" },
|
||||
{ "font.size.title", ST_INT, "28", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "font.size.body", ST_INT, "20", 0,0, NULL, NULL, "字体主题" },
|
||||
{ "ui.style", ST_ENUM, "md3", 0,0, "md3,miuix", "Material You,MIUIx", "字体主题" },
|
||||
/* 通知 */
|
||||
{ "notify.center_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "通知" },
|
||||
{ "notify.app_quota", ST_INT, "16", 0,0, NULL, NULL, "通知" },
|
||||
{ "notify.categories", ST_INT, "4", 1,4, NULL, NULL, "通知" },
|
||||
{ "notify.category_quota", ST_INT, "4", 1,16, NULL, NULL, "通知" },
|
||||
{ "notify.media_quota", ST_INT, "1", 1,1, NULL, NULL, "通知" },
|
||||
{ "notify.dnd", ST_BOOL, "0", 0,0, NULL, NULL, "通知" },
|
||||
/* 日志 */
|
||||
{ "log.level", ST_ENUM, "info", 0,0, "error,warn,info,debug", "错误,警告,信息,调试", "日志" },
|
||||
{ "log.to_card", ST_BOOL, "1", 0,0, NULL, NULL, "日志" },
|
||||
/* 音频 */
|
||||
{ "audio.volume", ST_INT, "60", 0,100, NULL, NULL, "音频" },
|
||||
{ "audio.volume_max", ST_INT, "100", 0,100, NULL, NULL, "音频" },
|
||||
{ "audio.volume_step", ST_INT, "1", 1,5, NULL, NULL, "音频" },
|
||||
{ "audio.output", ST_STR, "hw:0,0",0,0, NULL, NULL, "音频" },
|
||||
/* 网络 */
|
||||
{ "network.wifi_enabled", ST_BOOL, "1", 0,0, NULL, NULL, "网络" },
|
||||
{ "network.bluetooth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "网络" },
|
||||
/* 系统 */
|
||||
{ "system.hostname", ST_STR, "meowos",0,0, NULL, NULL, "系统" },
|
||||
/* 面板(MeowPanel 自身,访问级 PIN 鉴权)
|
||||
* panel.pin = 4 位数字 PIN 的 sha256+salt 哈希(`<salt_hex>:<sha256_hex(salt||pin)>`,绝不存明文)。
|
||||
* 与未来 MeowOS 锁屏 PIN 同源:锁屏键 `system.pin` 预留——本后端只读面板键 panel.pin;将来锁屏启用时
|
||||
* 以 system.pin 为准并做同源迁移(panel.pin 视为其缓存/别名),此处仅留兼容注记,不实现。
|
||||
* panel.auth_enabled/panel.auth_pass 为旧密码机制,保留兼容迁移(见鉴权段注释),新代码以 PIN 为准。 */
|
||||
{ "panel.pin", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||||
{ "panel.auth_enabled", ST_BOOL, "0", 0,0, NULL, NULL, "面板" },
|
||||
{ "panel.auth_pass", ST_STR, "", 0,0, NULL, NULL, "面板" },
|
||||
};
|
||||
#define SETS_COUNT ((int)(sizeof g_sets / sizeof g_sets[0]))
|
||||
|
||||
|
||||
const setdef_t *setdef_find(const char *key) {
|
||||
for (int i = 0; i < SETS_COUNT; i++)
|
||||
if (!strcmp(g_sets[i].key, key)) return &g_sets[i];
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void settings_mkdir_p(void)
|
||||
{
|
||||
/* 逐级创建 settings.conf 所在目录(mkdir -p 语义,容忍已存在)。 */
|
||||
char tmp[1024];
|
||||
snprintf(tmp, sizeof tmp, "%s", g_settings_file);
|
||||
char *slash = strrchr(tmp, '/');
|
||||
if (!slash || slash == tmp) return; /* 无目录或根部 */
|
||||
*slash = 0;
|
||||
if (!tmp[0]) return;
|
||||
char buf[1024]; buf[0] = 0;
|
||||
size_t i = 0;
|
||||
if (tmp[0] == '/') buf[i++] = '/';
|
||||
const char *s = tmp + (tmp[0] == '/' ? 1 : 0);
|
||||
while (*s) {
|
||||
const char *tom = strchr(s, '/');
|
||||
size_t l = tom ? (size_t)(tom - s) : strlen(s);
|
||||
if (l) {
|
||||
if (i && buf[i - 1] != '/') buf[i++] = '/';
|
||||
if (i + l >= sizeof buf) break;
|
||||
memcpy(buf + i, s, l);
|
||||
i += l;
|
||||
buf[i] = 0;
|
||||
mkdir(buf, 0777);
|
||||
}
|
||||
if (!tom) break;
|
||||
s = tom + 1;
|
||||
}
|
||||
}
|
||||
|
||||
char *settings_get_value(const char *key)
|
||||
{
|
||||
FILE *f = fopen(g_settings_file, "r");
|
||||
if (!f) return NULL;
|
||||
size_t kl = strlen(key);
|
||||
char line[256];
|
||||
char *found = NULL;
|
||||
while (fgets(line, sizeof line, f)) {
|
||||
char *eq = strchr(line, '=');
|
||||
if (!eq) continue;
|
||||
char *k = line;
|
||||
while (*k == ' ' || *k == '\t') k++;
|
||||
long n = (long)(eq - k);
|
||||
while (n > 0 && (k[n - 1] == ' ' || k[n - 1] == '\t')) n--;
|
||||
if ((size_t)n != kl || strncmp(k, key, kl) != 0) continue;
|
||||
found = eq + 1;
|
||||
break;
|
||||
}
|
||||
fclose(f);
|
||||
if (!found) return NULL;
|
||||
while (*found == ' ' || *found == '\t') found++;
|
||||
char *e = found + strlen(found);
|
||||
while (e > found && (e[-1] == '\n' || e[-1] == '\r' || e[-1] == ' ' || e[-1] == '\t')) *--e = 0;
|
||||
return strdup(found);
|
||||
}
|
||||
|
||||
int settings_get_int(const char *key, int defv)
|
||||
{
|
||||
char *v = settings_get_value(key);
|
||||
if (!v) return defv;
|
||||
int r = atoi(v);
|
||||
free(v);
|
||||
return r;
|
||||
}
|
||||
|
||||
int brightness_clamp(int v)
|
||||
{
|
||||
int mn = settings_get_int("display.brightness_min", 1);
|
||||
if (mn < 1) mn = 1;
|
||||
if (mn > 100) mn = 100;
|
||||
if (v < mn) v = mn;
|
||||
if (v > 100) v = 100;
|
||||
return v;
|
||||
}
|
||||
|
||||
int settings_write_one(const char *key, const char *val)
|
||||
{
|
||||
settings_mkdir_p(); /* 目录缺失自动建(与 meow_settings_save 同;失败由 fopen 兜底) */
|
||||
FILE *f = fopen(g_settings_file, "r");
|
||||
char tmp[2048];
|
||||
size_t n = 0;
|
||||
int c;
|
||||
if (f) {
|
||||
while (n < sizeof tmp - 1 && (c = fgetc(f)) != EOF) tmp[n++] = (char)c;
|
||||
fclose(f);
|
||||
}
|
||||
tmp[n] = 0;
|
||||
FILE *nf = fopen(g_settings_file, "w");
|
||||
if (!nf) return -1;
|
||||
size_t klen = strlen(key);
|
||||
int found = 0;
|
||||
if (n == 0) fprintf(nf, "%s\n", SETTINGS_HDR);
|
||||
/* 逐行重写:头照抄;命中 key 行替换;其余照抄。匹配容忍 `key = value` 两侧空格(与 settings_get_value 同口径)。 */
|
||||
char *save = NULL;
|
||||
for (char *ln = strtok_r(tmp, "\n", &save); ln; ln = strtok_r(NULL, "\n", &save)) {
|
||||
char *p = ln;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
char *eq = strchr(p, '=');
|
||||
if (eq) {
|
||||
char *k = p;
|
||||
size_t km = (size_t)(eq - k);
|
||||
while (km > 0 && (k[km - 1] == ' ' || k[km - 1] == '\t')) km--;
|
||||
if (km == klen && strncmp(k, key, klen) == 0) {
|
||||
fprintf(nf, "%s = %s\n", key, val);
|
||||
found = 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
fprintf(nf, "%s\n", ln);
|
||||
}
|
||||
if (!found) fprintf(nf, "%s = %s\n", key, val);
|
||||
fclose(nf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int settings_coerce(const setdef_t *d, const char *input, char *out, size_t outcap)
|
||||
{
|
||||
switch (d->type) {
|
||||
case ST_INT: {
|
||||
int v = atoi(input);
|
||||
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
|
||||
else {
|
||||
if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
|
||||
else if (v < d->min) v = d->min;
|
||||
}
|
||||
snprintf(out, outcap, "%d", v);
|
||||
return 0;
|
||||
}
|
||||
case ST_BOOL: {
|
||||
char *q = (char *)input;
|
||||
while (*q == ' ' || *q == '\t') q++;
|
||||
int b = 0;
|
||||
if (!strcasecmp(q, "true") || !strcasecmp(q, "yes") || !strcasecmp(q, "on")) b = 1;
|
||||
else { long v = strtol(q, NULL, 10); if (q[0] && v != 0) b = 1; }
|
||||
snprintf(out, outcap, "%d", b ? 1 : 0);
|
||||
return 0;
|
||||
}
|
||||
case ST_ENUM: {
|
||||
const char *names = d->enum_names;
|
||||
if (!names || !names[0]) { snprintf(out, outcap, "%s", input); return 0; }
|
||||
int cnt = csv_count(names);
|
||||
char tok[128];
|
||||
/* 接受名字或索引(向后兼容) */
|
||||
int idx = -1;
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
csv_at(names, i, tok, sizeof tok);
|
||||
if (!strcmp(tok, input)) { idx = i; break; }
|
||||
}
|
||||
if (idx < 0) {
|
||||
int n = atoi(input);
|
||||
if (n >= 0 && n < cnt) idx = n;
|
||||
}
|
||||
if (idx < 0) return -1; /* 非法枚举 */
|
||||
csv_at(names, idx, tok, sizeof tok);
|
||||
snprintf(out, outcap, "%s", tok);
|
||||
return 0;
|
||||
}
|
||||
case ST_STR:
|
||||
strncpy(out, input, outcap - 1);
|
||||
out[outcap - 1] = 0;
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int settings_reset_all(void)
|
||||
{
|
||||
settings_mkdir_p();
|
||||
FILE *nf = fopen(g_settings_file, "w");
|
||||
if (!nf) return -1;
|
||||
fprintf(nf, "%s\n", SETTINGS_HDR);
|
||||
for (int i = 0; i < SETS_COUNT; i++)
|
||||
fprintf(nf, "%s = %s\n", g_sets[i].key, g_sets[i].def);
|
||||
fclose(nf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void enum_options_json(sbuf_t *s, const char *names, const char *labels)
|
||||
{
|
||||
sA(s, "[");
|
||||
int cnt = csv_count(names);
|
||||
for (int i = 0; i < cnt; i++) {
|
||||
char nb[128], lb[128];
|
||||
csv_at(names, i, nb, sizeof nb);
|
||||
csv_at(labels, i, lb, sizeof lb);
|
||||
if (i) sA(s, ",");
|
||||
sA(s, "{\"name\":");
|
||||
sA_str(s, nb);
|
||||
sA(s, ",\"label\":");
|
||||
sA_str(s, lb[0] ? lb : nb);
|
||||
sA(s, "}");
|
||||
}
|
||||
sA(s, "]");
|
||||
}
|
||||
|
||||
void settings_item_json(sbuf_t *s, const setdef_t *d, const char *val)
|
||||
{
|
||||
const char *tn = "str";
|
||||
if (d->type == ST_INT) tn = "int";
|
||||
else if (d->type == ST_BOOL) tn = "bool";
|
||||
else if (d->type == ST_ENUM) tn = "enum";
|
||||
sA(s, "{\"key\":");
|
||||
sA_str(s, d->key);
|
||||
sA(s, ",\"type\":\"%s\",\"value\":", tn);
|
||||
if (d->type == ST_INT) {
|
||||
int v = atoi(val);
|
||||
if (!strcmp(d->key, "display.brightness")) v = brightness_clamp(v);
|
||||
else if (d->max > 0) { if (v < d->min) v = d->min; if (v > d->max) v = d->max; }
|
||||
else if (v < d->min) v = d->min;
|
||||
sA(s, "%d", v);
|
||||
sA(s, ",\"min\":%d,\"max\":%d", d->min, int_meta_max(d));
|
||||
} else if (d->type == ST_BOOL) {
|
||||
sA(s, "%s", str_truthy(val) ? "true" : "false");
|
||||
} else {
|
||||
if (!strcmp(d->key, "panel.auth_pass") || !strcmp(d->key, "panel.pin")) {
|
||||
/* 密码/PIN 哈希绝不出现在响应里:value 恒空串,附 set 标志(是否已设) */
|
||||
sA(s, "\"\",\"set\":%s", val[0] ? "true" : "false");
|
||||
} else {
|
||||
sA_str(s, val);
|
||||
}
|
||||
}
|
||||
sA(s, ",\"def\":");
|
||||
sA_str(s, d->def);
|
||||
if (d->type == ST_ENUM) {
|
||||
sA(s, ",\"options\":");
|
||||
enum_options_json(s, d->enum_names, d->enum_labels);
|
||||
sA(s, ",\"default_name\":");
|
||||
sA_str(s, d->def);
|
||||
}
|
||||
sA(s, "}");
|
||||
}
|
||||
|
||||
char *settings_current_value(const setdef_t *d)
|
||||
{
|
||||
char *v = settings_get_value(d->key);
|
||||
if (v) return v;
|
||||
return strdup(d->def);
|
||||
}
|
||||
|
||||
int api_settings_get_any(int sock, const char *key)
|
||||
{
|
||||
const setdef_t *d = setdef_find(key);
|
||||
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
|
||||
char *val = settings_current_value(d);
|
||||
sbuf_t s = {0};
|
||||
settings_item_json(&s, d, val);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
free(val);
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_settings_get_all(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"items\":[");
|
||||
char groups[16][32];
|
||||
int ngroups = 0;
|
||||
for (int i = 0; i < SETS_COUNT; i++) {
|
||||
const setdef_t *d = &g_sets[i];
|
||||
char *val = settings_current_value(d);
|
||||
if (i) sA(&s, ",");
|
||||
settings_item_json(&s, d, val);
|
||||
free(val);
|
||||
if (d->group && d->group[0]) {
|
||||
int found = 0;
|
||||
for (int g = 0; g < ngroups; g++) if (!strcmp(groups[g], d->group)) { found = 1; break; }
|
||||
if (!found && ngroups < 16) snprintf(groups[ngroups++], 32, "%s", d->group);
|
||||
}
|
||||
}
|
||||
sA(&s, "],\"count\":%d,\"groups\":[", SETS_COUNT);
|
||||
for (int g = 0; g < ngroups; g++) {
|
||||
if (g) sA(&s, ",");
|
||||
sA_str(&s, groups[g]);
|
||||
}
|
||||
sA(&s, "]}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_settings_put(int sock, const char *key, const req_t *r)
|
||||
{
|
||||
const setdef_t *d = setdef_find(key);
|
||||
if (!d) return send_err(sock, 404, "NOT_FOUND", "未知设置项");
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
jv_t *val = jv_obj_get(root, "value");
|
||||
if (!val) { jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "缺少 value 字段"); }
|
||||
char raw[256];
|
||||
switch (val->type) {
|
||||
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
|
||||
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
|
||||
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
|
||||
default: jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "value 类型不支持");
|
||||
}
|
||||
jv_free(root);
|
||||
char hashbuf[200];
|
||||
/* panel.pin:4 位数字 PIN,只落带盐哈希;空串=清除(回未设状态);非法(非 4 位数字)→ 422 */
|
||||
if (!strcmp(key, "panel.pin")) {
|
||||
if (raw[0]) {
|
||||
if (!pin_validate(raw)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
|
||||
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
|
||||
return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
|
||||
} else {
|
||||
hashbuf[0] = 0; /* 清空 → 回未设状态 */
|
||||
}
|
||||
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||||
pin_lock_reset();
|
||||
sbuf_t s = {0};
|
||||
settings_item_json(&s, d, hashbuf);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
/* panel.auth_pass:只落带盐哈希,绝不存明文;空串=清除密码 */
|
||||
if (!strcmp(key, "panel.auth_pass")) {
|
||||
if (raw[0]) {
|
||||
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0)
|
||||
return send_err(sock, 500, "INTERNAL", "生成密码哈希失败");
|
||||
} else {
|
||||
hashbuf[0] = 0; /* 清空 → 禁用密码 */
|
||||
}
|
||||
if (settings_write_one(key, hashbuf) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||||
sbuf_t s = {0};
|
||||
settings_item_json(&s, d, hashbuf);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
/* panel.auth_enabled(旧机制,兼容迁移):置 1 前须已有访问屏障(PIN 或旧密码) */
|
||||
if (!strcmp(key, "panel.auth_enabled")) {
|
||||
char coerced[256];
|
||||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0)
|
||||
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
|
||||
if (!strcmp(coerced, "1") && !pin_set() && !auth_pass_set())
|
||||
return send_err(sock, 422, "UNSUPPORTED", "请先设置面板 PIN 或密码");
|
||||
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||||
sbuf_t s = {0};
|
||||
settings_item_json(&s, d, coerced);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
char coerced[256];
|
||||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
|
||||
return send_err(sock, 422, "UNSUPPORTED", "值域/枚举非法");
|
||||
}
|
||||
if (settings_write_one(key, coerced) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||||
/* boot.default 同步权威文件(同 meow_settings_save) */
|
||||
if (!strcmp(key, "boot.default")) {
|
||||
int v = (!strcmp(coerced, "hiby")) ? 1 : 0;
|
||||
FILE *bf = fopen(BOOT_DEFAULT_FILE, "w");
|
||||
if (bf) { fprintf(bf, "%d\n", v); fclose(bf); }
|
||||
}
|
||||
sbuf_t s = {0};
|
||||
settings_item_json(&s, d, coerced);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_settings_batch(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root || root->type != J_OBJ) { if (root) jv_free(root); return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON 对象"); }
|
||||
sbuf_t ok = {0}, fail = {0};
|
||||
int nok = 0, nfail = 0;
|
||||
for (const jv_t *p = root->child; p; p = p->next) {
|
||||
const setdef_t *d = setdef_find(p->str);
|
||||
if (!d) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"NOT_FOUND\",\"message\":\"未知设置项\"}"); nfail++; continue; }
|
||||
jv_t *val = p->child;
|
||||
char raw[256];
|
||||
switch (val->type) {
|
||||
case J_NUM: snprintf(raw, sizeof raw, "%lld", val->inum); break;
|
||||
case J_STR: snprintf(raw, sizeof raw, "%s", val->str); break;
|
||||
case J_BOOL: snprintf(raw, sizeof raw, "%d", val->bval ? 1 : 0); break;
|
||||
default: if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"BAD_REQUEST\",\"message\":\"非法值类型\"}"); nfail++; continue;
|
||||
}
|
||||
/* panel.pin:只落带盐哈希(空=清除);非法(非 4 位数字)→ 422 */
|
||||
if (!strcmp(p->str, "panel.pin")) {
|
||||
char hashbuf[200];
|
||||
if (raw[0]) {
|
||||
if (!pin_validate(raw)) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"PIN 必须为 4 位数字\"}"); nfail++; continue; }
|
||||
if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成 PIN 哈希失败\"}"); nfail++; continue; }
|
||||
} else hashbuf[0] = 0;
|
||||
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
|
||||
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
|
||||
continue;
|
||||
}
|
||||
/* panel.auth_pass:只落带盐哈希(空=清除);panel.auth_enabled:启用前须已有密码 */
|
||||
if (!strcmp(p->str, "panel.auth_pass")) {
|
||||
char hashbuf[200];
|
||||
if (raw[0]) { if (auth_make_hash(raw, hashbuf, sizeof hashbuf) < 0) { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"生成密码哈希失败\"}"); nfail++; continue; } }
|
||||
else hashbuf[0] = 0;
|
||||
if (settings_write_one(p->str, hashbuf) == 0) { if (nok) sA(&ok, ","); sA_str(&ok, p->str); nok++; }
|
||||
else { if (nfail) sA(&fail, ","); sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}"); nfail++; }
|
||||
continue;
|
||||
}
|
||||
char coerced[256];
|
||||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) {
|
||||
if (nfail) sA(&fail, ",");
|
||||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"值域/枚举非法\"}");
|
||||
nfail++;
|
||||
continue;
|
||||
}
|
||||
/* panel.auth_enabled(迁移):启用前须已有访问屏障(PIN 或旧密码) */
|
||||
if (!strcmp(p->str, "panel.auth_enabled") && !strcmp(coerced, "1") && !pin_set() && !auth_pass_set()) {
|
||||
if (nfail) sA(&fail, ",");
|
||||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"UNSUPPORTED\",\"message\":\"请先设置面板 PIN 或密码\"}");
|
||||
nfail++;
|
||||
continue;
|
||||
}
|
||||
if (settings_write_one(p->str, coerced) == 0) {
|
||||
if (nok) sA(&ok, ",");
|
||||
sA_str(&ok, p->str);
|
||||
nok++;
|
||||
} else {
|
||||
if (nfail) sA(&fail, ",");
|
||||
sA(&fail, "{\"key\":"); sA_str(&fail, p->str); sA(&fail, ",\"code\":\"INTERNAL\",\"message\":\"写失败\"}");
|
||||
nfail++;
|
||||
}
|
||||
}
|
||||
jv_free(root);
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"updated\":[");
|
||||
if (ok.b) sA(&s, "%s", ok.b);
|
||||
sA(&s, "],\"failed\":[");
|
||||
if (fail.b) sA(&s, "%s", fail.b);
|
||||
sA(&s, "]}");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s); sbuf_free(&ok); sbuf_free(&fail);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_settings_restore(int sock)
|
||||
{
|
||||
if (settings_reset_all() < 0) return send_err(sock, 500, "INTERNAL", "恢复失败");
|
||||
return send_json(sock, 200, "{\"reset\":true}");
|
||||
}
|
||||
|
||||
void theme_json(sbuf_t *s)
|
||||
{
|
||||
const setdef_t *dst = setdef_find("ui.style");
|
||||
const setdef_t *dth = setdef_find("display.theme");
|
||||
const setdef_t *dbg = setdef_find("display.background");
|
||||
char *style = settings_current_value(dst);
|
||||
char *theme = settings_current_value(dth);
|
||||
char *bg = settings_current_value(dbg);
|
||||
/* 当前值 → 中文标签(csv 里同下标查 label) */
|
||||
char slabel[128] = "", tlabel[128] = "";
|
||||
for (int i = 0; i < csv_count(dst->enum_names); i++) {
|
||||
char nb[128], lb[128];
|
||||
csv_at(dst->enum_names, i, nb, sizeof nb);
|
||||
csv_at(dst->enum_labels, i, lb, sizeof lb);
|
||||
if (!strcmp(nb, style)) { snprintf(slabel, sizeof slabel, "%s", lb[0] ? lb : nb); break; }
|
||||
}
|
||||
for (int i = 0; i < csv_count(dth->enum_names); i++) {
|
||||
char nb[128], lb[128];
|
||||
csv_at(dth->enum_names, i, nb, sizeof nb);
|
||||
csv_at(dth->enum_labels, i, lb, sizeof lb);
|
||||
if (!strcmp(nb, theme)) { snprintf(tlabel, sizeof tlabel, "%s", lb[0] ? lb : nb); break; }
|
||||
}
|
||||
sA(s, "{\"style\":");
|
||||
sA_str(s, style);
|
||||
sA(s, ",\"theme\":");
|
||||
sA_str(s, theme);
|
||||
sA(s, ",\"background\":");
|
||||
sA_str(s, bg);
|
||||
sA(s, ",\"style_label\":");
|
||||
sA_str(s, slabel);
|
||||
sA(s, ",\"theme_label\":");
|
||||
sA_str(s, tlabel);
|
||||
sA(s, ",\"style_options\":");
|
||||
enum_options_json(s, dst->enum_names, dst->enum_labels);
|
||||
sA(s, ",\"theme_options\":");
|
||||
enum_options_json(s, dth->enum_names, dth->enum_labels);
|
||||
sA(s, ",\"background_options\":[{\"name\":\"black\",\"label\":\"黑\"},{\"name\":\"blue\",\"label\":\"深蓝\"},{\"name\":\"green\",\"label\":\"墨绿\"}]}");
|
||||
free(style); free(theme); free(bg);
|
||||
}
|
||||
|
||||
int api_theme_get(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
theme_json(&s);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_theme_put(int sock, const req_t *r)
|
||||
{
|
||||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 body");
|
||||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||||
/* 允许 style/theme/background 任意子集 */
|
||||
const char *keys[3] = { "style", "theme", "background" };
|
||||
const char *setkeys[3] = { "ui.style", "display.theme", "display.background" };
|
||||
for (int i = 0; i < 3; i++) {
|
||||
jv_t *v = jv_obj_get(root, keys[i]);
|
||||
if (!v) continue;
|
||||
const setdef_t *d = setdef_find(setkeys[i]);
|
||||
if (!d) continue;
|
||||
char raw[256];
|
||||
if (v->type == J_STR) snprintf(raw, sizeof raw, "%s", v->str);
|
||||
else if (v->type == J_NUM) snprintf(raw, sizeof raw, "%lld", v->inum);
|
||||
else continue;
|
||||
char coerced[256];
|
||||
if (settings_coerce(d, raw, coerced, sizeof coerced) < 0) { jv_free(root); return send_err(sock, 422, "UNSUPPORTED", "非法值"); }
|
||||
settings_write_one(setkeys[i], coerced);
|
||||
}
|
||||
jv_free(root);
|
||||
return api_theme_get(sock);
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/* mp_svcd.c — 服务表域:svcd 状态解析 + /api/svcd + 首页聚合 /api/overview。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
const char *svcd_bin(void)
|
||||
{
|
||||
const char *e = getenv("MEOW_SVCD_BIN");
|
||||
if (e && e[0]) return e;
|
||||
if (!access(SVCD_BIN, X_OK)) return SVCD_BIN;
|
||||
return SVCD_BIN_CARD;
|
||||
}
|
||||
|
||||
void svcd_fill(sbuf_t *s)
|
||||
{
|
||||
char out[16384];
|
||||
char cmd[300];
|
||||
snprintf(cmd, sizeof cmd, "%s status all 2>/dev/null", svcd_bin());
|
||||
int code = run_cmd(cmd, out, sizeof out);
|
||||
if (code != 0) { sA(s, "{\"available\":false}"); return; }
|
||||
panel_strip_ansi(out);
|
||||
char *toks[256];
|
||||
sbuf_t svcs = {0};
|
||||
int first = 1, count = 0;
|
||||
char *ln = out;
|
||||
while (*ln) {
|
||||
char *nl = strchr(ln, '\n');
|
||||
if (nl) *nl = 0;
|
||||
int n = txt_tokens(ln, toks, 256);
|
||||
/* 数据行:NAME STATE PID RESTART BACKOFF TRIGGER SRC EXIT EXIT_TIME EXEC...
|
||||
* state=tok[1] 须为合法状态;exec=join(tok[9..n))。 */
|
||||
if (n >= 10) {
|
||||
const char *st = toks[1];
|
||||
int is_known = (!strcmp(st,"running") || !strcmp(st,"stopped") ||
|
||||
!strcmp(st,"stopping") || !strcmp(st,"disabled") || !strcmp(st,"failed"));
|
||||
if (is_known) {
|
||||
char exec[512];
|
||||
join_tokens(toks, 9, n, exec, sizeof exec);
|
||||
int restarts = 0;
|
||||
{ const char *slash = strchr(toks[3], '/'); (void)slash; restarts = atoi(toks[3]); }
|
||||
if (!first) sA(&svcs, ",");
|
||||
first = 0; count++;
|
||||
sA(&svcs, "{\"name\":"); sA_str(&svcs, toks[0]);
|
||||
sA(&svcs, ",\"state\":"); sA_str(&svcs, toks[1]);
|
||||
sA(&svcs, ",\"pid\":%d", atoi(toks[2]));
|
||||
sA(&svcs, ",\"restart\":%d", restarts);
|
||||
sA(&svcs, ",\"restart_str\":");
|
||||
sA_str(&svcs, toks[3]);
|
||||
sA(&svcs, ",\"backoff_ms\":%d", atoi(toks[4]));
|
||||
sA(&svcs, ",\"trigger\":");
|
||||
sA_str(&svcs, toks[5]);
|
||||
sA(&svcs, ",\"src\":");
|
||||
sA_str(&svcs, toks[6]);
|
||||
sA(&svcs, ",\"exit\":");
|
||||
sA_str(&svcs, toks[7]);
|
||||
sA(&svcs, ",\"exit_time\":");
|
||||
sA_str(&svcs, toks[8]);
|
||||
sA(&svcs, ",\"exec\":");
|
||||
sA_str(&svcs, exec);
|
||||
sA(&svcs, "}");
|
||||
}
|
||||
}
|
||||
if (nl) { *nl = '\n'; ln = nl + 1; } else break;
|
||||
}
|
||||
sA(s, "{\"available\":true,\"count\":%d,\"services\":[%s]}", count, svcs.b ? svcs.b : "");
|
||||
sbuf_free(&svcs);
|
||||
}
|
||||
|
||||
int api_svcd(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
svcd_fill(&s);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{\"available\":false}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int api_overview(int sock)
|
||||
{
|
||||
sbuf_t s = {0};
|
||||
sA(&s, "{\"ts\":%lld,", (long long)time(NULL));
|
||||
char pb[4096];
|
||||
if (ipc_perf_query(0, pb, sizeof pb) == MEOW_OK && utf8_valid_n(pb, strlen(pb)))
|
||||
sA(&s, "\"perf\":%s,", pb);
|
||||
else sA(&s, "\"perf\":{\"available\":false},");
|
||||
char pw[MEOW_IPC_MAX];
|
||||
if (ipc_pm_query(0, pw, sizeof pw) == MEOW_OK && utf8_valid_n(pw, strlen(pw)))
|
||||
sA(&s, "\"power\":%s,", pw);
|
||||
else sA(&s, "\"power\":{\"available\":false},");
|
||||
/* 最近异常耗电/充放电/亮灭屏/背光跳变事件(摘要 top2 进 overview,完整列表走 /api/power/events)。 */
|
||||
{
|
||||
uint32_t ecnt = 2;
|
||||
char ej[MEOW_IPC_MAX];
|
||||
uint16_t ert = 0;
|
||||
int ert_rc = ipc_pm_send(MSG_PM_EVENTS, &ecnt, sizeof ecnt, &ert, ej, sizeof ej);
|
||||
if (ert_rc > 0 && ert == MSG_PM_EVENTS_RESULT && utf8_valid_n(ej, strlen(ej)))
|
||||
sA(&s, "\"events\":%s,", ej);
|
||||
else
|
||||
sA(&s, "\"events\":{\"available\":false},");
|
||||
}
|
||||
sA(&s, "\"audio\":");
|
||||
audio_fill(&s);
|
||||
if (net_block_fill(&s) == 0)
|
||||
; /* net 块已追加(状态 + 合并的 dns 子键) */
|
||||
else
|
||||
sA(&s, ",\"net\":{\"available\":false}");
|
||||
sA(&s, ",\"ams\":");
|
||||
ams_fill(&s);
|
||||
sA(&s, ",\"svcd\":");
|
||||
svcd_fill(&s);
|
||||
sA(&s, "}");
|
||||
/* 服务端自校验:发出前 jv_parse 自查,坏了留证+回 500(防「非 JSON 响应」破页,2026-09-02 间歇性事故)
|
||||
* 加严:内嵌 NUL(strlen 截断浏览器收残体)/非法 UTF-8(浏览器 JSON.parse 拒收,宽松解析器放行)*/
|
||||
jv_t *chk = jv_parse(s.b ? s.b : "{}", (int)(s.b ? s.n : 2));
|
||||
size_t slen = s.b ? strlen(s.b) : 0;
|
||||
int nul_inside = s.b && (slen != s.n); /* 内嵌 NUL:strlen < s.n */
|
||||
int utf8_bad = s.b && !utf8_valid_n(s.b, s.n);
|
||||
int unbalanced = s.b && !json_strict_balanced(s.b, s.n); /* 尾随垃圾/多括号(jv_parse 容忍,浏览器不容忍) */
|
||||
if (s.b && (nul_inside || utf8_bad || unbalanced)) { chk = NULL; }
|
||||
if (!chk && s.b) {
|
||||
FILE *df = fopen("/tmp/overview_bad.json", "w");
|
||||
if (df) { fwrite(s.b, 1, s.n, df); fclose(df); }
|
||||
panel_log("overview: SELF-CHECK fail (nul=%d utf8_bad=%d unbalanced=%d n=%zu strlen=%zu), raw dumped /tmp/overview_bad.json", nul_inside, utf8_bad, unbalanced, s.n, slen);
|
||||
sbuf_free(&s);
|
||||
return send_err(sock, 500, "INTERNAL", "overview 聚合输出未通过 JSON 自校验(原始数据已留 /tmp/overview_bad.json)");
|
||||
}
|
||||
if (chk) jv_free(chk);
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||||
sbuf_free(&s);
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
/* mp_util.c — 通用工具:有界字符串(sbuf)/JSON 字符串消毒(UTF-8)/内存搜索/命令管道/路径与 token 工具。 */
|
||||
#include "mp.h"
|
||||
|
||||
|
||||
int sb_reserve(sbuf_t *s, size_t extra)
|
||||
{
|
||||
if (s->n + extra + 1 <= s->cap) return 0;
|
||||
size_t ncap = s->cap ? s->cap : 256;
|
||||
while (ncap < s->n + extra + 1) ncap *= 2;
|
||||
char *nb = realloc(s->b, ncap);
|
||||
if (!nb) return -1;
|
||||
s->b = nb;
|
||||
s->cap = ncap;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void sA(sbuf_t *s, const char *fmt, ...)
|
||||
{
|
||||
char tmp[512];
|
||||
va_list ap, ap2;
|
||||
va_start(ap, fmt);
|
||||
va_copy(ap2, ap);
|
||||
int w = vsnprintf(tmp, sizeof tmp, fmt, ap);
|
||||
va_end(ap);
|
||||
if (w < 0) { va_end(ap2); return; }
|
||||
if ((size_t)w < sizeof tmp) {
|
||||
if (sb_reserve(s, (size_t)w) < 0) { va_end(ap2); return; }
|
||||
memcpy(s->b + s->n, tmp, (size_t)w);
|
||||
s->n += (size_t)w;
|
||||
} else {
|
||||
/* 超长 %s 等:格式化结果超出固定缓冲,动态分配后再追加(防截断坏 JSON) */
|
||||
char *big = malloc((size_t)w + 1);
|
||||
if (!big) { va_end(ap2); return; }
|
||||
int w2 = vsnprintf(big, (size_t)w + 1, fmt, ap2);
|
||||
va_end(ap2);
|
||||
if (w2 < 0) { free(big); return; }
|
||||
if (sb_reserve(s, (size_t)w2) < 0) { free(big); return; }
|
||||
memcpy(s->b + s->n, big, (size_t)w2);
|
||||
s->n += (size_t)w2;
|
||||
free(big);
|
||||
}
|
||||
s->b[s->n] = 0;
|
||||
}
|
||||
|
||||
int utf8_seq_len(unsigned char lead)
|
||||
{
|
||||
if (lead >= 0xC2 && lead <= 0xDF) return 2; /* C2+:规避 overlong C0/C1 */
|
||||
if (lead >= 0xE0 && lead <= 0xEF) return 3; /* 3 字节(含代理区,由 seq_ok 剔除) */
|
||||
if (lead >= 0xF0 && lead <= 0xF4) return 4; /* F5+ 超 U+10FFFF,剔除 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
int utf8_seq_ok(const char *p, int len)
|
||||
{
|
||||
unsigned char c0 = (unsigned char)p[0];
|
||||
for (int k = 1; k < len; k++) {
|
||||
unsigned char ck = (unsigned char)p[k];
|
||||
if (ck == 0 || (ck & 0xC0) != 0x80) return 0; /* 截断/非续字节 */
|
||||
}
|
||||
unsigned long cp;
|
||||
if (len == 2) cp = ((unsigned long)(c0 & 0x1F) << 6) | ((unsigned char)p[1] & 0x3F);
|
||||
else if (len == 3) cp = ((unsigned long)(c0 & 0x0F) << 12) | (((unsigned char)p[1] & 0x3F) << 6) | ((unsigned char)p[2] & 0x3F);
|
||||
else cp = ((unsigned long)(c0 & 0x07) << 18) | (((unsigned char)p[1] & 0x3F) << 12) | (((unsigned char)p[2] & 0x3F) << 6) | ((unsigned char)p[3] & 0x3F);
|
||||
if (len == 2 && cp < 0x80) return 0; /* overlong(C0/C1 已由 seq_len 拒,双保险) */
|
||||
if (len == 3 && cp < 0x800) return 0; /* overlong E0 80.. */
|
||||
if (len == 4 && cp < 0x10000) return 0; /* overlong F0 80.. */
|
||||
if (cp >= 0xD800 && cp <= 0xDFFF) return 0; /* 代理对(JS 字符串不允许孤立) */
|
||||
if (cp > 0x10FFFF) return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int utf8_valid_n(const char *s, size_t n)
|
||||
{
|
||||
size_t i = 0;
|
||||
while (i < n) {
|
||||
unsigned char c = (unsigned char)s[i];
|
||||
if (c < 0x80) { i++; continue; }
|
||||
int len = utf8_seq_len(c);
|
||||
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(s + i, len)) return 0;
|
||||
i += (size_t)len;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int utf8_sanitize(char *dst, const char *src, size_t n)
|
||||
{
|
||||
size_t i = 0, j = 0;
|
||||
int changed = 0;
|
||||
while (i < n) {
|
||||
unsigned char c = (unsigned char)src[i];
|
||||
if (c < 0x80) { dst[j++] = (char)c; i++; continue; }
|
||||
int len = utf8_seq_len(c);
|
||||
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(src + i, len)) {
|
||||
dst[j++] = '?'; changed = 1; i++; continue; /* 逐字节替换,前进 1 重对齐 */
|
||||
}
|
||||
memcpy(dst + j, src + i, (size_t)len);
|
||||
j += (size_t)len; i += (size_t)len;
|
||||
}
|
||||
dst[j] = 0;
|
||||
return changed;
|
||||
}
|
||||
|
||||
void sA_str_n(sbuf_t *s, const char *v, size_t n)
|
||||
{
|
||||
sA(s, "\"");
|
||||
size_t i = 0;
|
||||
while (i < n) {
|
||||
unsigned char c = (unsigned char)v[i];
|
||||
if (c == '"' || c == '\\') { sA(s, "\\%c", (int)c); i++; }
|
||||
else if (c < 0x20) { sA(s, "\\u%04x", c); i++; }
|
||||
else if (c < 0x80) { sA(s, "%c", (int)c); i++; }
|
||||
else {
|
||||
int len = utf8_seq_len(c);
|
||||
if (len == 0 || i + (size_t)len > n || !utf8_seq_ok(v + i, len)) {
|
||||
sA(s, "?"); i++; /* 非法/截断序列 → 逐字节替换为 '?',前进 1 重对齐 */
|
||||
} else {
|
||||
for (int k = 0; k < len; k++) sA(s, "%c", (int)(unsigned char)v[i + k]);
|
||||
i += (size_t)len;
|
||||
}
|
||||
}
|
||||
}
|
||||
sA(s, "\"");
|
||||
}
|
||||
|
||||
void sA_str(sbuf_t *s, const char *v)
|
||||
{
|
||||
if (!v) v = "";
|
||||
sA_str_n(s, v, strlen(v));
|
||||
}
|
||||
|
||||
void sA_append(sbuf_t *s, const char *b, size_t n)
|
||||
{
|
||||
if (!b || n == 0) return;
|
||||
if (sb_reserve(s, n) < 0) return;
|
||||
memcpy(s->b + s->n, b, n);
|
||||
s->n += n;
|
||||
s->b[s->n] = 0;
|
||||
}
|
||||
|
||||
void sbuf_free(sbuf_t *s) { free(s->b); s->b = NULL; s->n = s->cap = 0; }
|
||||
|
||||
void panel_strip_ansi(char *s)
|
||||
{
|
||||
char *r = s, *w = s;
|
||||
while (*r) {
|
||||
if (*r == '\033' && r[1] == '[') {
|
||||
r += 2;
|
||||
while (*r && !(*r >= '@' && *r <= '~')) r++;
|
||||
if (*r) r++;
|
||||
} else { *w++ = *r++; }
|
||||
}
|
||||
*w = 0;
|
||||
}
|
||||
|
||||
int txt_tokens(char *buf, char **toks, int maxtoks)
|
||||
{
|
||||
int n = 0;
|
||||
char *p = buf;
|
||||
while (*p && n < maxtoks) {
|
||||
while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++;
|
||||
if (!*p) break;
|
||||
toks[n++] = p;
|
||||
while (*p && *p != ' ' && *p != '\t' && *p != '\n' && *p != '\r') p++;
|
||||
if (*p) *p++ = 0;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void join_tokens(char **toks, int from, int to, char *out, size_t cap)
|
||||
{
|
||||
size_t o = 0;
|
||||
for (int i = from; i < to; i++) {
|
||||
size_t need = strlen(toks[i]) + (o ? 1 : 0);
|
||||
if (o + need >= cap) { out[cap - 1] = 0; return; }
|
||||
if (o) out[o++] = ' ';
|
||||
memcpy(out + o, toks[i], strlen(toks[i]));
|
||||
o += strlen(toks[i]);
|
||||
}
|
||||
out[o] = 0;
|
||||
}
|
||||
|
||||
int read_small_file(const char *path, char *buf, int cap)
|
||||
{
|
||||
int fd = open(path, O_RDONLY);
|
||||
if (fd < 0) return -1;
|
||||
int n = 0;
|
||||
while (n < cap - 1) {
|
||||
ssize_t r = read(fd, buf + n, (size_t)(cap - 1 - n));
|
||||
if (r <= 0) break;
|
||||
n += (int)r;
|
||||
}
|
||||
close(fd);
|
||||
buf[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
void url_decode(const char *in, char *buf, int buflen)
|
||||
{
|
||||
int o = 0;
|
||||
for (int i = 0; in[i] && o < buflen - 1; i++) {
|
||||
if (in[i] == '%' && is_hex(in[i + 1]) && is_hex(in[i + 2])) {
|
||||
buf[o++] = (char)((hexval(in[i + 1]) << 4) | hexval(in[i + 2]));
|
||||
i += 2;
|
||||
} else if (in[i] == '+') buf[o++] = ' ';
|
||||
else buf[o++] = in[i];
|
||||
}
|
||||
buf[o] = 0;
|
||||
}
|
||||
|
||||
int qparam(const char *query, const char *key, char *out, int outcap)
|
||||
{
|
||||
size_t kl = strlen(key);
|
||||
const char *p = query;
|
||||
while (p && *p) {
|
||||
const char *amp = strchr(p, '&');
|
||||
size_t seg = amp ? (size_t)(amp - p) : strlen(p);
|
||||
if (seg >= kl + 1 && strncmp(p, key, kl) == 0 && p[kl] == '=') {
|
||||
char tmp[1024];
|
||||
size_t vlen = seg - kl - 1;
|
||||
if (vlen >= sizeof tmp) vlen = sizeof tmp - 1;
|
||||
memcpy(tmp, p + kl + 1, vlen);
|
||||
tmp[vlen] = 0;
|
||||
url_decode(tmp, out, outcap);
|
||||
return 1;
|
||||
}
|
||||
if (!amp) break;
|
||||
p = amp + 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int path_segment(const char *prefix, const char *full, char *out, int outcap)
|
||||
{
|
||||
size_t pl = strlen(prefix);
|
||||
if (strncmp(full, prefix, pl) != 0) return 0;
|
||||
const char *rest = full + pl;
|
||||
if (*rest == '/') rest++;
|
||||
if (!*rest) return 0;
|
||||
size_t l = strlen(rest);
|
||||
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
|
||||
memcpy(out, rest, l);
|
||||
out[l] = 0;
|
||||
/* 去掉后续 / 段(取第一段) */
|
||||
char *slash = strchr(out, '/');
|
||||
if (slash) *slash = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void split_path_query(const char *src, char *path, int plc, char *query, int qlc)
|
||||
{
|
||||
const char *q = strchr(src, '?');
|
||||
if (q) {
|
||||
size_t pl = (size_t)(q - src);
|
||||
if (pl >= (size_t)plc) pl = (size_t)plc - 1;
|
||||
memcpy(path, src, pl);
|
||||
path[pl] = 0;
|
||||
snprintf(query, qlc, "%s", q + 1);
|
||||
} else {
|
||||
snprintf(path, plc, "%s", src);
|
||||
query[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ptrdiff_t find_header_end(const char *buf, size_t n)
|
||||
{
|
||||
if (n < 4) return -1;
|
||||
for (size_t i = 0; i + 3 < n; i++) {
|
||||
if (buf[i] == '\r' && buf[i + 1] == '\n' && buf[i + 2] == '\r' && buf[i + 3] == '\n')
|
||||
return (ptrdiff_t)i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char *mem_find(const void *hay, size_t hlen, const char *needle)
|
||||
{
|
||||
size_t nl = strlen(needle);
|
||||
if (!nl || hlen < nl) return NULL;
|
||||
const char *h = (const char *)hay;
|
||||
for (size_t i = 0; i + nl <= hlen; i++) {
|
||||
if (h[i] == needle[0] && memcmp(h + i, needle, nl) == 0)
|
||||
return h + i;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void panel_log(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
fprintf(stderr, "[MeowPanel] ");
|
||||
vfprintf(stderr, fmt, ap);
|
||||
fprintf(stderr, "\n");
|
||||
va_end(ap);
|
||||
}
|
||||
|
||||
int str_truthy(const char *v)
|
||||
{
|
||||
char q[32];
|
||||
snprintf(q, sizeof q, "%s", v);
|
||||
char *p = q;
|
||||
while (*p == ' ' || *p == '\t') p++;
|
||||
if (!strcasecmp(p, "true") || !strcasecmp(p, "yes") || !strcasecmp(p, "on")) return 1;
|
||||
long n = strtol(p, NULL, 10);
|
||||
return (p[0] && n != 0);
|
||||
}
|
||||
|
||||
int int_meta_max(const setdef_t *d)
|
||||
{
|
||||
if (d->max > 0) return d->max;
|
||||
int dn = atoi(d->def);
|
||||
if (dn > 0) return dn * 2 + 100;
|
||||
return 100;
|
||||
}
|
||||
|
||||
const char *csv_at(const char *s, int idx, char *buf, int buflen)
|
||||
{
|
||||
buf[0] = 0;
|
||||
if (!s || !s[0]) return buf;
|
||||
const char *p = s;
|
||||
int i = 0;
|
||||
for (;;) {
|
||||
const char *start = p;
|
||||
while (*p && *p != ',') p++;
|
||||
if (i == idx) {
|
||||
size_t k = (size_t)(p - start);
|
||||
if (k >= (size_t)buflen) k = (size_t)buflen - 1;
|
||||
memcpy(buf, start, k);
|
||||
buf[k] = 0;
|
||||
break;
|
||||
}
|
||||
if (!*p) break;
|
||||
p++;
|
||||
i++;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
int csv_count(const char *s)
|
||||
{
|
||||
if (!s || !s[0]) return 0;
|
||||
int n = 1;
|
||||
for (const char *p = s; *p; p++) if (*p == ',') n++;
|
||||
return n;
|
||||
}
|
||||
|
||||
int run_cmd(const char *cmd, char *out, int outcap)
|
||||
{
|
||||
FILE *f = popen(cmd, "r");
|
||||
if (!f) return -1;
|
||||
size_t off = 0;
|
||||
while (off < (size_t)outcap - 1) {
|
||||
size_t rd = fread(out + off, 1, (size_t)outcap - 1 - off, f);
|
||||
if (rd == 0) break;
|
||||
off += rd;
|
||||
}
|
||||
out[off] = 0;
|
||||
int st = pclose(f);
|
||||
int code = -1;
|
||||
if (WIFEXITED(st)) code = WEXITSTATUS(st);
|
||||
return code;
|
||||
}
|
||||
|
||||
int64_t now_mono_ms(void)
|
||||
{
|
||||
struct timespec ts;
|
||||
clock_gettime(CLOCK_MONOTONIC, &ts);
|
||||
return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
|
||||
}
|
||||
@@ -61,13 +61,9 @@
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2" fill="var(--bg)"/><circle cx="15" cy="12" r="2" fill="var(--bg)"/><circle cx="7" cy="18" r="2" fill="var(--bg)"/></svg></span>
|
||||
<span class="nav-label">设置</span>
|
||||
</button>
|
||||
<button class="nav-item" data-route="remote" title="远程控制">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="7" y="3" width="10" height="18" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="15" r="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M10 7h4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg></span>
|
||||
<span class="nav-label">远程</span>
|
||||
</button>
|
||||
<button class="nav-item" data-route="screen" title="实时画面 / 点击遥控">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="12" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M8 21h8M12 17v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="8.5" cy="11" r="1.2" fill="currentColor"/><path d="M12 11l3.5 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg></span>
|
||||
<span class="nav-label">屏幕</span>
|
||||
<button class="nav-item" data-route="screen" title="屏幕 · 遥控(实时画面 + 遥控器)">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><rect x="3" y="5" width="14" height="11" rx="2" fill="none" stroke="currentColor" stroke-width="2"/><path d="M7 21h6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="7.5" cy="10.5" r="1.1" fill="currentColor"/><path d="M10 10.5l3 3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" transform="rotate(-45 11.5 12)"/><rect x="18" y="4" width="4.5" height="4.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="18" y="11" width="4.5" height="4.5" rx="1" fill="none" stroke="currentColor" stroke-width="1.8"/><rect x="18" y="18" width="4.5" height="2.5" rx="1" fill="currentColor"/></svg></span>
|
||||
<span class="nav-label">屏幕·遥控</span>
|
||||
</button>
|
||||
<button class="nav-item" data-route="proc" title="进程 / 服务(meow_svcd + meow_amsd)">
|
||||
<span class="nav-ico"><svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16" stroke="currentColor" stroke-width="2" stroke-linecap="round"/><circle cx="9" cy="6" r="2.2" fill="var(--bg)"/><circle cx="15" cy="12" r="2.2" fill="var(--bg)"/><circle cx="7" cy="18" r="2.2" fill="var(--bg)"/></svg></span>
|
||||
@@ -171,6 +167,9 @@
|
||||
|
||||
<div id="toast" class="toast" hidden></div>
|
||||
|
||||
<!-- 前端模块化拆分(共享框架 → 各页渲染 → 入口路由),按顺序加载 -->
|
||||
<script src="mp_shared.js"></script>
|
||||
<script src="mp_pages.js"></script>
|
||||
<script src="meowpanel.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -293,6 +293,28 @@ input[type=range]{ width:100%; accent-color:var(--primary); }
|
||||
.key-btn:hover{ background:var(--hover); }
|
||||
.key-btn .k-ico{ font-size:20px; display:block; margin-bottom:4px; }
|
||||
|
||||
/* 屏幕·遥控 合并页:左screen右遥控板 */
|
||||
.screen-remote{ display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; }
|
||||
.screen-col{ flex:1 1 340px; min-width:280px; max-width:520px; }
|
||||
.remote-col{ flex:0 1 260px; min-width:220px; }
|
||||
.remote-col .card{ padding:14px 16px; }
|
||||
.remote-pad{ display:flex; flex-direction:column; gap:12px; }
|
||||
.remote-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:10px; }
|
||||
.remote-btn{
|
||||
display:flex; flex-direction:column; align-items:center; justify-content:center; gap:6px;
|
||||
padding:12px 6px; border:none; border-radius:var(--radius-m);
|
||||
background:var(--surface-variant); color:var(--on-surface);
|
||||
border:var(--outline-w) solid var(--outline-variant);
|
||||
cursor:pointer; font:inherit; text-align:center; transition:background var(--speed);
|
||||
}
|
||||
.remote-btn:hover{ background:var(--hover); }
|
||||
.remote-btn .r-ico{ width:26px; height:26px; display:block; }
|
||||
.remote-btn .r-ico svg{ width:26px; height:26px; display:block; }
|
||||
.remote-btn .r-lbl{ font-size:12px; line-height:1.2; }
|
||||
.remote-bright{ display:flex; align-items:center; gap:8px; }
|
||||
.remote-bright .r-lbl{ font-size:12px; color:var(--on-surface-variant); white-space:nowrap; }
|
||||
.remote-bright input{ flex:1; }
|
||||
|
||||
/* 危险区 */
|
||||
.danger-box{ border:1px solid var(--error); border-radius:var(--radius-l); padding:16px 18px; }
|
||||
.danger-box .card-title{ color:var(--error); }
|
||||
|
||||
+27
-2745
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,467 @@
|
||||
/* mp_shared.js — MeowPanel 前端共享框架:全局状态 + API 客户端 + 公共工具 + 主题/弹窗/登录/滑条。
|
||||
* 由 meowpanel.js 拆分而来(接口/行为不变)。作为经典脚本在 meowpanel.js 之前加载,暴露 window.MP。 */
|
||||
(function () {
|
||||
'use strict';
|
||||
var MP = window.MP = window.MP || {};
|
||||
const $ = (sel, root) => (root || document).querySelector(sel);
|
||||
const $$ = (sel, root) => Array.prototype.slice.call((root || document).querySelectorAll(sel));
|
||||
const sparkRms = [], sparkPeak = [];
|
||||
const SPARK_MAX = 30;
|
||||
const state = {
|
||||
style: 'md3', mode: 'dark', // 已解析的风格/明暗(应用到 <html> data-style/data-mode)
|
||||
devStyle: null, devTheme: null, // 设备 settings.conf 的 ui.style / display.theme(默认来源)
|
||||
conn: 'unknown',
|
||||
filesRel: '/', filesAbs: null,
|
||||
perfTimer: null, // 性能页轮询
|
||||
route: 'perf',
|
||||
unauth: false, // 鉴权未通过(弹登录页)
|
||||
downloadNonce: 0
|
||||
};
|
||||
let toastTimer = null;
|
||||
let authMode = 'login'; // 'login' | 'setup'
|
||||
let volTimer = null;
|
||||
const saveSetting = debounce(async function (key, value, type) {
|
||||
try {
|
||||
await api('PUT', '/api/settings/' + key, { value: value });
|
||||
toast('已保存 ' + key + ' = ' + value);
|
||||
} catch (e) { toast('保存失败:' + errText(e)); }
|
||||
}, 400);
|
||||
const LOG_GROUP_NAME = { all: '全部', system: '系统服务', app: 'App', audio_monitor: 'audio_monitor', power: 'power', net: 'net', crash: 'crash', ams: 'ams' };
|
||||
|
||||
function storageGet(k) { try { return localStorage.getItem(k); } catch (e) { return null; } }
|
||||
|
||||
function storageSet(k, v) { try { localStorage.setItem(k, v); } catch (e) {} }
|
||||
|
||||
function storageDel(k) { try { localStorage.removeItem(k); } catch (e) {} }
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, m => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
||||
}[m]));
|
||||
}
|
||||
|
||||
function fmtBytes(n) {
|
||||
if (n == null || isNaN(n)) return '—';
|
||||
const u = ['B', 'KB', 'MB', 'GB', 'TB']; let v = Number(n), i = 0;
|
||||
while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; }
|
||||
return (i === 0 ? v : v.toFixed(1)) + ' ' + u[i];
|
||||
}
|
||||
|
||||
function fmtUptime(sec) {
|
||||
if (sec == null || isNaN(sec)) return '—';
|
||||
sec = Math.floor(sec);
|
||||
const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60);
|
||||
return (d > 0 ? d + ' 天 ' : '') + h + ' 小时 ' + m + ' 分';
|
||||
}
|
||||
|
||||
function fmtTime(ts) { return ts ? new Date(ts * 1000).toLocaleString() : '—'; }
|
||||
|
||||
function debounce(fn, ms) { let t = null; return function () { const a = arguments, c = this; clearTimeout(t); t = setTimeout(() => fn.apply(c, a), ms); }; }
|
||||
|
||||
function pct(p) { return Math.max(0, Math.min(100, p)); }
|
||||
|
||||
function deciToDb(v) { return (v == null || v <= -990) ? null : v / 10; }
|
||||
|
||||
function fmtDb(v) {
|
||||
const db = deciToDb(v);
|
||||
if (db == null) return '−∞';
|
||||
return (db > 0 ? '+' : '') + db.toFixed(1);
|
||||
}
|
||||
|
||||
function dbPct(v) { // -60..0 dBFS 映射到 0..100% 条宽
|
||||
const db = deciToDb(v);
|
||||
if (db == null) return 0;
|
||||
return Math.max(0, Math.min(100, (db + 60) / 60 * 100));
|
||||
}
|
||||
|
||||
function isAvail(b) { return !!(b && b.available !== false); }
|
||||
|
||||
function sparklineHtml() {
|
||||
return '<svg class="lvl-spark" viewBox="0 0 100 46" preserveAspectRatio="none" aria-hidden="true">' +
|
||||
'<line class="grid" x1="0" y1="46" x2="100" y2="46"/>' +
|
||||
'<line class="grid" x1="0" y1="23" x2="100" y2="23"/>' +
|
||||
'<polyline class="spk-rms" points="" fill="none" stroke="#00bfa5" stroke-width="1.6" vector-effect="non-scaling-stroke"/>' +
|
||||
'<polyline class="spk-peak" points="" fill="none" stroke="#ffb300" stroke-width="1.6" vector-effect="non-scaling-stroke"/>' +
|
||||
'</svg>';
|
||||
}
|
||||
|
||||
function sparklinePoints(arr) {
|
||||
const n = arr.length;
|
||||
if (!n) return '';
|
||||
return arr.map((v, i) => {
|
||||
const x = (i / (SPARK_MAX - 1)) * 100;
|
||||
return x.toFixed(1) + ',' + (46 - Math.max(0, Math.min(100, v)) / 100 * 40).toFixed(1);
|
||||
}).join(' ');
|
||||
}
|
||||
|
||||
function pushSpark(v, arr) { // deci-dB → 0..100 百分比入队(<=30 点)
|
||||
arr.push(dbPct(v));
|
||||
if (arr.length > SPARK_MAX) arr.shift();
|
||||
}
|
||||
|
||||
function drawSpark(rootEl) {
|
||||
const svg = rootEl && rootEl.querySelector('.lvl-spark');
|
||||
if (!svg) return;
|
||||
const r = svg.querySelector('.spk-rms'), p = svg.querySelector('.spk-peak');
|
||||
if (r) r.setAttribute('points', sparklinePoints(sparkRms));
|
||||
if (p) p.setAttribute('points', sparklinePoints(sparkPeak));
|
||||
}
|
||||
|
||||
function resetSpark() { sparkRms.length = 0; sparkPeak.length = 0; }
|
||||
|
||||
function levelMeterHtml() {
|
||||
return '<div class="lvl">' +
|
||||
'<div class="lvl-row"><span class="lvl-lbl">RMS</span>' +
|
||||
'<div class="lvl-meter"><div class="lvl-fill rms" style="width:0%"></div></div>' +
|
||||
'<span class="lvl-val" data-val="rms">−∞</span></div>' +
|
||||
'<div class="lvl-row"><span class="lvl-lbl">PEAK</span>' +
|
||||
'<div class="lvl-meter"><div class="lvl-fill peak" style="width:0%"></div></div>' +
|
||||
'<span class="lvl-val" data-val="peak">−∞</span></div>' +
|
||||
'<div class="lvl-scale"><span>-60</span><span>-40</span><span>-20</span><span>0 dBFS</span></div>' +
|
||||
sparklineHtml() + '</div>';
|
||||
}
|
||||
|
||||
function updateLevelMeter(rootEl, d) {
|
||||
if (!rootEl) return;
|
||||
const rms = (d && d.available) ? d.rms : null;
|
||||
const peak = (d && d.available) ? d.peak : null;
|
||||
const fr = rootEl.querySelector('.lvl-fill.rms');
|
||||
const fp = rootEl.querySelector('.lvl-fill.peak');
|
||||
const vr = rootEl.querySelector('.lvl-val[data-val="rms"]');
|
||||
const vp = rootEl.querySelector('.lvl-val[data-val="peak"]');
|
||||
if (fr) fr.style.width = dbPct(rms) + '%';
|
||||
if (fp) fp.style.width = dbPct(peak) + '%';
|
||||
if (vr) vr.textContent = fmtDb(rms);
|
||||
if (vp) vp.textContent = fmtDb(peak);
|
||||
pushSpark(rms, sparkRms); pushSpark(peak, sparkPeak);
|
||||
drawSpark(rootEl);
|
||||
}
|
||||
|
||||
function homeKv(k, v) {
|
||||
return '<div class="home-kv"><span class="k">' + esc(k) + '</span><span class="v">' + v + '</span></div>';
|
||||
}
|
||||
|
||||
function homeUnavailable() { return '<div class="home-sub">— 数据不可用</div>'; }
|
||||
|
||||
function homeCard(title, body) { return '<div class="card"><h3 class="card-title">' + esc(title) + '</h3>' + body + '</div>'; }
|
||||
|
||||
function modePill(m) {
|
||||
if (!m) return '<span class="home-chip mut">—</span>';
|
||||
const cls = (m === 'meow') ? 'ok' : ((m === 'hiby') ? 'warn' : 'mut');
|
||||
return '<span class="home-chip ' + cls + '">' + esc(m) + '</span>';
|
||||
}
|
||||
|
||||
function setConn(ok) {
|
||||
state.conn = ok ? 'ok' : 'bad';
|
||||
const dot = $('#conn-dot');
|
||||
dot.dataset.state = state.conn;
|
||||
$('#conn-text').textContent = ok ? '在线' : '离线';
|
||||
$('#offline-banner').hidden = !!ok;
|
||||
}
|
||||
|
||||
function dbg() {
|
||||
const a = Array.from(arguments);
|
||||
console.log.apply(console, ['[mp]'].concat(a));
|
||||
}
|
||||
|
||||
function dbgErr() {
|
||||
const a = Array.from(arguments);
|
||||
console.warn.apply(console, ['[mp][ERR]'].concat(a));
|
||||
}
|
||||
|
||||
async function api(method, path, body) {
|
||||
let res;
|
||||
const headers = {};
|
||||
const token = storageGet('meowpanel.token');
|
||||
if (token) headers['X-MeowToken'] = token;
|
||||
const init = { method: method || 'GET' };
|
||||
if (body instanceof FormData) {
|
||||
init.body = body;
|
||||
} else if (body !== undefined && body !== null) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
init.body = JSON.stringify(body);
|
||||
}
|
||||
if (Object.keys(headers).length) init.headers = headers;
|
||||
const t0 = performance.now();
|
||||
try {
|
||||
res = await fetch(path, init);
|
||||
} catch (e) {
|
||||
dbgErr(method || 'GET', path, '网络层失败:', e && e.message);
|
||||
setConn(false);
|
||||
throw { code: 'NETWORK', message: '后端未连接', status: 0 };
|
||||
}
|
||||
const ms = Math.round(performance.now() - t0);
|
||||
/* 先读原文再解析:非 JSON 时能拿到原始内容前 300 字节进控制台 */
|
||||
let text = '';
|
||||
try { text = await res.text(); } catch (e) { /* 读体失败走 NOT_JSON */ }
|
||||
let json = null;
|
||||
try { json = text ? JSON.parse(text) : null; } catch (e) { json = null; }
|
||||
if (!json || typeof json.ok !== 'boolean') {
|
||||
// 静态服务器返回 HTML / 404 等
|
||||
dbgErr(method || 'GET', path, 'HTTP', res.status, ms + 'ms', '非 JSON 响应, len=' + text.length, '头:', text.slice(0, 200), '尾:', text.slice(-200));
|
||||
try { fetch('/api/debug/report_bad_json', { method: 'POST', headers: { 'Content-Type': 'text/plain', 'X-MeowToken': storageGet('meowpanel.token') || '' }, body: text }); } catch (e2) {}
|
||||
setConn(res.ok ? true : false);
|
||||
throw { code: 'NOT_JSON', message: '后端返回了非 JSON 响应(HTTP ' + res.status + ')', status: res.status };
|
||||
}
|
||||
if (!json.ok) {
|
||||
setConn(true);
|
||||
const code = (json.error && json.error.code) || 'ERROR';
|
||||
dbgErr(method || 'GET', path, 'HTTP', res.status, ms + 'ms', '后端错误:', code, (json.error && json.error.message) || '');
|
||||
if (code === 'UNAUTHORIZED') authRequired(); // 会话失效/未认证 → 弹登录页
|
||||
throw { code: code, message: (json.error && json.error.message) || ('后端错误 ' + code), status: res.status };
|
||||
}
|
||||
if (ms > 800) dbg(method || 'GET', path, '慢响应', ms + 'ms');
|
||||
setConn(true);
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function apiDownload(path) {
|
||||
let res;
|
||||
const headers = {};
|
||||
const token = storageGet('meowpanel.token');
|
||||
if (token) headers['X-MeowToken'] = token;
|
||||
try { res = await fetch(path, Object.keys(headers).length ? { headers: headers } : undefined); }
|
||||
catch (e) { setConn(false); throw { code: 'NETWORK', message: '后端未连接' }; }
|
||||
if (!res.ok) { setConn(true); throw { code: 'HTTP_' + res.status, message: '下载失败(HTTP ' + res.status + ')' }; }
|
||||
setConn(true);
|
||||
return URL.createObjectURL(await res.blob());
|
||||
}
|
||||
|
||||
function toast(msg, ms) {
|
||||
const t = $('#toast');
|
||||
t.textContent = msg; t.hidden = false;
|
||||
clearTimeout(toastTimer);
|
||||
toastTimer = setTimeout(() => { t.hidden = true; }, ms || 2600);
|
||||
}
|
||||
|
||||
function confirmDlg(title, body, danger) {
|
||||
return new Promise(res => {
|
||||
$('#modal-title').textContent = title || '确认';
|
||||
$('#modal-body').textContent = body || '';
|
||||
const yes = $('#modal-yes'), no = $('#modal-no');
|
||||
yes.className = 'btn btn-primary' + (danger ? ' btn-danger' : '');
|
||||
const scrim = $('#modal'); scrim.hidden = false;
|
||||
const done = v => { scrim.hidden = true; yes.onclick = no.onclick = scrim.onclick = null; res(v); };
|
||||
yes.onclick = () => done(true);
|
||||
no.onclick = () => done(false);
|
||||
scrim.onclick = e => { if (e.target === scrim) done(false); };
|
||||
});
|
||||
}
|
||||
|
||||
function loginErr(msg) { const err = $('#login-err'); if (err) { err.textContent = msg; err.hidden = false; } }
|
||||
|
||||
function authRequired() { // 任一路由收到 401 → 弹登录页(会话失效/未认证)
|
||||
state.unauth = true;
|
||||
refreshAuthGate();
|
||||
}
|
||||
|
||||
async function refreshAuthGate() { // 查状态决定「首次设置」还是「登录」
|
||||
let st = null;
|
||||
try { st = await api('GET', '/api/auth/status'); } catch (e) { st = null; }
|
||||
if (st && st.session) { state.unauth = false; hideLogin(); return; }
|
||||
if (st && !st.pin_set && !st.legacy) showLogin('setup');
|
||||
else showLogin('login');
|
||||
}
|
||||
|
||||
function showLogin(mode) {
|
||||
state.unauth = true;
|
||||
authMode = mode || 'login';
|
||||
const lg = $('#login'); if (lg) lg.hidden = false;
|
||||
const sub = $('#login-sub');
|
||||
const pin2 = $('#login-pin2');
|
||||
const btn = $('#login-btn');
|
||||
if (authMode === 'setup') {
|
||||
if (sub) sub.textContent = '首次使用:请设置一个 4 位数字 PIN 作为本面板访问口令。';
|
||||
if (pin2) pin2.hidden = false;
|
||||
if (btn) btn.textContent = '设 置';
|
||||
} else {
|
||||
if (sub) sub.textContent = '此面板已开启访问鉴权,请输入 4 位 PIN。';
|
||||
if (pin2) pin2.hidden = true;
|
||||
if (btn) btn.textContent = '登 录';
|
||||
}
|
||||
const ip = $('#login-pin'); if (ip) { ip.value = ''; ip.focus(); }
|
||||
const ip2 = $('#login-pin2'); if (ip2 && authMode !== 'setup') ip2.value = '';
|
||||
const err = $('#login-err'); if (err) err.hidden = true;
|
||||
}
|
||||
|
||||
function hideLogin() { const lg = $('#login'); if (lg) lg.hidden = true; }
|
||||
|
||||
async function doLogin() {
|
||||
const pin = ($('#login-pin').value || '').trim();
|
||||
const err = $('#login-err'); if (err) err.hidden = true;
|
||||
if (!pin) { loginErr('请输入 PIN'); return; }
|
||||
if (authMode === 'setup') {
|
||||
const pin2 = ($('#login-pin2').value || '').trim();
|
||||
if (pin.length !== 4 || /[^0-9]/.test(pin)) { loginErr('PIN 必须为 4 位数字'); return; }
|
||||
if (pin !== pin2) { loginErr('两次输入的 PIN 不一致'); return; }
|
||||
const btn = $('#login-btn'); if (btn) btn.disabled = true;
|
||||
try {
|
||||
await api('POST', '/api/auth/setup', { pin: pin });
|
||||
toast('PIN 已设置,请登录');
|
||||
showLogin('login');
|
||||
} catch (e2) { loginErr((e2 && e2.message) ? e2.message : '设置失败'); }
|
||||
finally { if (btn) btn.disabled = false; }
|
||||
return;
|
||||
}
|
||||
const btn = $('#login-btn'); if (btn) btn.disabled = true;
|
||||
try {
|
||||
const d = await api('POST', '/api/auth/login', { pin: pin });
|
||||
storageSet('meowpanel.token', d.token);
|
||||
state.unauth = false;
|
||||
hideLogin();
|
||||
toast('已登录');
|
||||
if (started) route(); else startApp();
|
||||
} catch (e) {
|
||||
if (e && (e.code === 'LOCKED' || e.status === 429)) loginErr(e.message || '错误次数过多,已锁定,请稍后重试');
|
||||
else loginErr((e && e.message) ? e.message : '登录失败');
|
||||
} finally {
|
||||
const b = $('#login-btn'); if (b) b.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function bindLogin() {
|
||||
const ok = $('#login-btn'); if (ok) ok.addEventListener('click', doLogin);
|
||||
const ip = $('#login-pin'); if (ip) ip.addEventListener('keydown', e => { if (e.key === 'Enter') doLogin(); });
|
||||
// PIN 输入:只允许数字,最长 4 位
|
||||
[ip, $('#login-pin2')].forEach(el => { if (el) el.addEventListener('input', () => { el.value = el.value.replace(/[^0-9]/g, '').slice(0, 4); }); });
|
||||
}
|
||||
|
||||
function passwordDlg(title, body) {
|
||||
return new Promise(res => {
|
||||
$('#pwdlg-title').textContent = title || '设置面板密码';
|
||||
$('#pwdlg-body').textContent = body || '请输入新密码(留空则清除密码)。';
|
||||
const inp = $('#pwdlg-input'); inp.value = ''; inp.focus();
|
||||
const scrim = $('#pwdlg'); scrim.hidden = false;
|
||||
const okb = $('#pwdlg-ok'), cancel = $('#pwdlg-cancel');
|
||||
const done = v => { scrim.hidden = true; okb.onclick = cancel.onclick = scrim.onclick = inp.onkeydown = null; res(v); };
|
||||
okb.onclick = () => done(inp.value);
|
||||
cancel.onclick = () => done(null);
|
||||
scrim.onclick = e => { if (e.target === scrim) done(null); };
|
||||
inp.onkeydown = e => { if (e.key === 'Enter') done(inp.value); if (e.key === 'Escape') done(null); };
|
||||
});
|
||||
}
|
||||
|
||||
function bindPwDlg() { /* #content 点击委托在 bindSettings 里处理 pass 按钮;此处无需额外绑定 */ }
|
||||
|
||||
function browserMode() {
|
||||
try { return (window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches) ? 'light' : 'dark'; }
|
||||
catch (e) { return 'dark'; }
|
||||
}
|
||||
|
||||
function storedStyle() { const v = storageGet('meowpanel.style'); return (v === 'md3' || v === 'miuix') ? v : null; }
|
||||
|
||||
function storedMode() { const v = storageGet('meowpanel.mode'); return (v === 'system' || v === 'browser' || v === 'light' || v === 'dark') ? v : null; }
|
||||
|
||||
function resolveStyle() { return storedStyle() || state.devStyle || 'md3'; }
|
||||
|
||||
function resolveMode() {
|
||||
const m = storedMode() || 'system';
|
||||
if (m === 'light' || m === 'dark') return m;
|
||||
if (m === 'system') return state.devTheme || browserMode(); // 设备未知时回退浏览器偏好
|
||||
return browserMode();
|
||||
}
|
||||
|
||||
function devStyleLabel() { return state.devStyle ? (state.devStyle === 'miuix' ? 'MIUIx' : 'Material You') : '未知(后端离线)'; }
|
||||
|
||||
function devThemeLabel() { return state.devTheme ? (state.devTheme === 'light' ? '浅色' : '深色') : '未知(后端离线)'; }
|
||||
|
||||
function applyTheme() {
|
||||
const style = resolveStyle(), mode = resolveMode();
|
||||
state.style = style; state.mode = mode;
|
||||
const root = document.documentElement;
|
||||
root.dataset.style = style; root.dataset.mode = mode;
|
||||
$$('#style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style));
|
||||
$$('#theme-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === mode));
|
||||
// 设置页「主题」卡(仅在该页渲染时存在)
|
||||
$$('#set-style-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.style === style));
|
||||
$$('#set-mode-seg .seg-btn').forEach(b => b.classList.toggle('active', b.dataset.mode === (storedMode() || 'system')));
|
||||
const rst = $('#set-style-reset'); if (rst) rst.disabled = !storedStyle();
|
||||
}
|
||||
|
||||
async function initTheme() {
|
||||
// 旧版键迁移:meowpanel.theme(dark/light)→ meowpanel.mode
|
||||
const old = storageGet('meowpanel.theme');
|
||||
if (old) {
|
||||
if (!storedMode()) storageSet('meowpanel.mode', old === 'light' ? 'light' : 'dark');
|
||||
storageDel('meowpanel.theme');
|
||||
}
|
||||
applyTheme();
|
||||
// 浏览器日/夜切换:模式为 browser(或 system 且设备主题未知)时实时跟随
|
||||
try {
|
||||
if (window.matchMedia) {
|
||||
const mq = matchMedia('(prefers-color-scheme: dark)');
|
||||
const onChange = function () {
|
||||
const m = storedMode() || 'system';
|
||||
if (m === 'browser' || (m === 'system' && !state.devTheme)) applyTheme();
|
||||
};
|
||||
if (mq.addEventListener) mq.addEventListener('change', onChange);
|
||||
else if (mq.addListener) mq.addListener(onChange);
|
||||
}
|
||||
} catch (e) {}
|
||||
// 设备默认值(ui.style/display.theme)——未覆盖时的默认来源
|
||||
try {
|
||||
const t = await api('GET', '/api/theme');
|
||||
state.devStyle = (t.style === 'md3' || t.style === 'miuix') ? t.style : null;
|
||||
state.devTheme = (t.theme === 'light' || t.theme === 'dark') ? t.theme : null;
|
||||
applyTheme();
|
||||
if (state.route === 'settings') renderSettings(); // 刷新「主题」卡的设备当前值提示
|
||||
} catch (e) { /* 后端离线,保持本地/浏览器偏好 */ }
|
||||
}
|
||||
|
||||
function bindThemeUi() {
|
||||
$('#style-seg').addEventListener('click', function (e) {
|
||||
const b = e.target.closest('.seg-btn'); if (!b) return;
|
||||
storageSet('meowpanel.style', b.dataset.style);
|
||||
applyTheme();
|
||||
sendTheme({ style: b.dataset.style });
|
||||
});
|
||||
$('#theme-seg').addEventListener('click', function (e) {
|
||||
const b = e.target.closest('.seg-btn'); if (!b) return;
|
||||
storageSet('meowpanel.mode', b.dataset.mode); // 顶栏=显式明/暗
|
||||
applyTheme();
|
||||
sendTheme({ theme: b.dataset.mode });
|
||||
});
|
||||
}
|
||||
|
||||
function emptyState(msg, code) {
|
||||
const t = code === 'NO_CARD' ? '未识别到 TF 卡' : (code === 'NOT_IMPLEMENTED' ? '该功能后端尚未实现' : msg);
|
||||
return '<div class="empty">' + esc(t) + '</div>';
|
||||
}
|
||||
|
||||
function errText(e) {
|
||||
if (e && e.code === 'NO_CARD') return '未识别到 TF 卡';
|
||||
if (e && e.code === 'NOT_IMPLEMENTED') return '该功能后端尚未实现';
|
||||
return (e && e.message) ? e.message : '未知错误';
|
||||
}
|
||||
|
||||
function barHtml(p) {
|
||||
p = pct(p);
|
||||
const c = p >= 90 ? 'danger' : (p >= 70 ? 'warn' : '');
|
||||
return '<div class="bar ' + c + '"><span style="width:' + p + '%"></span></div>';
|
||||
}
|
||||
|
||||
function stat(label, valueHtml, sub, barPct) {
|
||||
return '<div class="stat"><div class="stat-label">' + esc(label) + '</div>' + valueHtml +
|
||||
(sub ? '<div class="stat-sub">' + sub + '</div>' : '') +
|
||||
(barPct != null ? barHtml(barPct) : '') + '</div>';
|
||||
}
|
||||
|
||||
function slur(step) { // step: {key,label,min,max,val}
|
||||
return '<div class="card"><h3 class="card-title">' + esc(step.label) + '</h3>' +
|
||||
'<div class="field-row"><span class="muted" style="width:44px">' + step.val + '</span>' +
|
||||
'<input type="range" min="' + step.min + '" max="' + step.max + '" value="' + step.val + '" data-slider data-key="' + esc(step.key) + '">' +
|
||||
'<span class="muted" style="width:44px;text-align:right">' + step.max + '</span></div></div>';
|
||||
}
|
||||
|
||||
function bindSlider() {
|
||||
$('#content').addEventListener('input', function (e) {
|
||||
const r = e.target.closest('[data-slider]');
|
||||
if (!r) return;
|
||||
const row = r.closest('.field-row');
|
||||
if (row) row.querySelector('.muted').textContent = r.value;
|
||||
saveSetting(r.dataset.key, +r.value, 'int');
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(MP, { $, $$, LOG_GROUP_NAME, SPARK_MAX, api, apiDownload, applyTheme, authMode, authRequired, barHtml, bindLogin, bindPwDlg, bindSlider, bindThemeUi, browserMode, confirmDlg, dbPct, dbg, dbgErr, debounce, deciToDb, devStyleLabel, devThemeLabel, doLogin, drawSpark, emptyState, errText, esc, fmtBytes, fmtDb, fmtTime, fmtUptime, hideLogin, homeCard, homeKv, homeUnavailable, initTheme, isAvail, levelMeterHtml, loginErr, modePill, passwordDlg, pct, pushSpark, refreshAuthGate, resetSpark, resolveMode, resolveStyle, saveSetting, setConn, showLogin, slur, sparkPeak, sparkRms, sparklineHtml, sparklinePoints, stat, state, storageDel, storageGet, storageSet, storedMode, storedStyle, toast, toastTimer, updateLevelMeter, volTimer });
|
||||
})();
|
||||
@@ -38,6 +38,7 @@ typedef struct meow_audio_state {
|
||||
int paused; /* 当前流是否暂停 */
|
||||
int route_port; /* 当前路由口(-1 未知) */
|
||||
int vol; /* 当前音量 0-100 */
|
||||
int dsd; /* 当前流是否为 DoP DSD(fmt==AUDIO_FMT_DOP 且 audiod 已授权) */
|
||||
} meow_audio_state;
|
||||
|
||||
/* 被抢占回调:by_app=抢占者 app_id(非空)。ctx 由 on_preempt 注册时传入。 */
|
||||
@@ -48,6 +49,10 @@ typedef void (*meow_audio_preempt_fn)(const char *by_app, void *ctx);
|
||||
* MEOW_ERR_NODEV=无可用 PCM 设备;MEOW_ERR_* 常量本身即负值)。open 后客户端须按内部记录的
|
||||
* grant 格式产出 PCM。 */
|
||||
int meow_audio_open(int rate, int ch, int fmt);
|
||||
/* 开流(带 flags)。flags=AUDIO_FLAG_DSD 且 fmt=AUDIO_FMT_DOP 时为 DoP DSD 流(A3+DSD 定案):
|
||||
* App 侧须把 DSD 解码封装为 DoP(每样本 32-bit 容器:16bit DSD 数据 + 标记 + 0),audiod 只透传
|
||||
* (不引 dsd2pcm、不做 DOP_EN 之外的任何处理)。DSD 打不开/不支持 → open 返回负错误码(报错关流)。 */
|
||||
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags);
|
||||
/* 写 PCM。pcm=native fmt/ch 样本(open 传的参数),frames=帧数;内部归一成 grant 并分批发送。
|
||||
* 返回写入帧数;<0=流已死(被抢占/audiod 断开),App 应重新 open。阻塞(socket 背压)。 */
|
||||
int meow_audio_write(const void *pcm, int frames);
|
||||
@@ -57,6 +62,10 @@ int meow_audio_pause(int sid, int paused);
|
||||
int meow_audio_close(int sid);
|
||||
/* 查询 audiod 快照(填充 out)。返回 0 成功;<0 失败。 */
|
||||
int meow_audio_query(meow_audio_state *out);
|
||||
/* 设置音量 0-100(audiod 唯一权威:写 DAC numid=1/2 + DSD_VOLUME 双组保切换连续 + 持久化
|
||||
* settings.conf audio.volume,与 launcher/watchdog 同源同键)。BT 输出目标时音量由 AVRCP 侧控,
|
||||
* audiod 不下发 DAC 反转。返回 0 成功;<0 失败(audiod 不在)。 */
|
||||
int meow_audio_set_volume(int pct);
|
||||
/* 注册被抢占回调(可选;内部读线程分发)。重复调用幂等。 */
|
||||
void meow_audio_on_preempt(meow_audio_preempt_fn cb, void *ctx);
|
||||
|
||||
|
||||
@@ -22,8 +22,11 @@ meow_img *meow_img_load_file(const char *path); /* 内部读文件 + stb 解
|
||||
meow_img *meow_img_load_mem(const void *buf, int len);
|
||||
void meow_img_free(meow_img *img);
|
||||
|
||||
int meow_img_w(meow_img *img);
|
||||
int meow_img_h(meow_img *img);
|
||||
int meow_img_w(const meow_img *img);
|
||||
int meow_img_h(const meow_img *img);
|
||||
/* 访问器:取 RGBA 像素指针(4 通道 straight alpha;供取色/直读像素)。meow_img 对外 opaque,
|
||||
* 本函数为其唯一像素读入口;NULL 返回 NULL。 */
|
||||
const unsigned char *meow_img_pixels(const meow_img *img);
|
||||
|
||||
void meow_ui_img(int x, int y, meow_img *img); /* 原始尺寸 */
|
||||
/* 内存瘦身:最近邻等比缩放到最长边 ≤maxdim,返回新图;原图已 ≤maxdim 返回 NULL。
|
||||
|
||||
@@ -1,21 +1,16 @@
|
||||
/*
|
||||
* meow_img_color.c — MeowOS SDK 封面双色渐变取色实现(见 meow_img_color.h 契约)。
|
||||
*
|
||||
* 独立源码,不并入 meow_ui.a(铁律:避免并发重编/四 App 重链)。纯函数,无任何 meow_* 调用,
|
||||
* 宿主单测零链接重(仅需本 .c + meow_img_color.h 及其 include 链)。
|
||||
*
|
||||
* 布局耦合说明:meow_img 为 opaque(对外仅前向声明,结构体私有于 meow_ui_img.c)。为在不动
|
||||
* meow_ui.a 二进制的前提下直读封面像素(批5 取色),本 TU 按 meow_ui_img.c 自 A1 起稳定的
|
||||
* `struct meow_img { int w, h; unsigned char *rgba; };` 未布局局部补全。两 TU 各持同一定义,
|
||||
* 链接后字段偏移一致(本项目内 ABI 稳定)。主会话下次重建 SDK 时可上收为 meow_ui_img.c 的
|
||||
* 正式访问器,届时本补全即删除。
|
||||
* 独立源码,不并入 meow_ui.a(铁律:避免并发重编/四 App 重链)。取色算法纯函数(无 GPU/浮点/堆)。
|
||||
* 唯一 meow_* 依赖:仅 meow_img_pixels(SDK 正式访问器,meow_ui_img.c §4),并经 meow_img_w/h 取宽高
|
||||
* ——三者读 meow_img 的唯一入口,本 TU 不再局部补全其结构体(agent-402 遗留点已上收)。App/宿主
|
||||
* 链入 meow_img_color.c 时需能解析 meow_img_pixels/w/h(宿主测试已链 meow_ui_img.c,实际 App 恒链
|
||||
* meow_ui.a)。
|
||||
*/
|
||||
#include "meow_img_color.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
struct meow_img { int w, h; unsigned char *rgba; }; /* 见上:局部补全(与 meow_ui_img.c 一致) */
|
||||
|
||||
#define MIC_BUCKETS 4 /* 每通道色阶数 ⇒ 4×4×4=64 桶 */
|
||||
#define MIC_NBUCKET (MIC_BUCKETS*MIC_BUCKETS*MIC_BUCKETS)
|
||||
#define MIC_ALPHA_MIN 64 /* 跳过近透明(alpha<64) */
|
||||
@@ -120,8 +115,10 @@ int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
|
||||
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second)
|
||||
{
|
||||
if (!im) return -1;
|
||||
/* 局部补全 struct 视图直读像素(布局与 meow_ui_img.c 一致,见顶部说明)。 */
|
||||
return meow_img_duocolor_rgba(im->rgba, im->w, im->h, main, second);
|
||||
/* 经 SDK 正式访问器读像素(meow_img opaque;meow_img_pixels 为唯一读入口)。 */
|
||||
const unsigned char *rgba = meow_img_pixels(im);
|
||||
if (!rgba) return -1;
|
||||
return meow_img_duocolor_rgba(rgba, meow_img_w(im), meow_img_h(im), main, second);
|
||||
}
|
||||
|
||||
void meow_img_color_derive(uint32_t main, uint32_t second, const meow_theme *th,
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
* `#include "meow_img_color.h"` 并在构建时单链 meow_img_color.c(宿主单测同法)。
|
||||
*
|
||||
* 约束(0 warning / 最小侵入):
|
||||
* - 不修改 meow_ui.a 任何成员(meow_img 保持 opaque 对外)。为直读封面像素,本 TU 按
|
||||
* meow_ui_img.c 自 A1 起稳定的 `struct meow_img { int w,h; unsigned char *rgba; }` 布局
|
||||
* 局部补全(见 .c 顶部说明)。主会话下次重建 SDK 时可上收为 meow_ui_img.c 的正式访问器。
|
||||
* - 纯函数:meow_img_duocolor_rgba / meow_img_duocolor / meow_img_color_derive 不调用任何
|
||||
* meow_* 符号,宿主单测可零链接重(仅需本头 + 本 .c)。
|
||||
* - meow_img 对外保持 opaque;像素直读经 SDK 正式访问器 `meow_img_pixels`(meow_ui_img.c §4,
|
||||
* 已上收「局部补全结构体」遗留),宽高经 `meow_img_w`/`meow_img_h`。本 TU 不再局部补全布局。
|
||||
* - 取色算法纯函数:meow_img_duocolor_rgba / meow_img_color_derive 纯 C、无 meow_* 依赖;
|
||||
* meow_img_duocolor(句柄版)仅经上述三访问器读,不触碰 meow_ui.a 内部实现。宿主单测链
|
||||
* meow_img_color.c + meow_ui_img.c 即可。
|
||||
*
|
||||
* 色彩语义:
|
||||
* - 输入 RGBA(stb_image 输出,4 通道 straight alpha),w×h,最多遍历 ~3.6k 采样(内部自适
|
||||
@@ -37,7 +37,7 @@ extern "C" {
|
||||
int meow_img_duocolor_rgba(const uint8_t *rgba, int w, int h,
|
||||
uint32_t *main, uint32_t *second);
|
||||
|
||||
/* 从 meow_img 句柄取主色+次色(内部直读 rgba 缓冲,见 .c 布局说明)。返回 0=成功;-1=失败。 */
|
||||
/* 从 meow_img 句柄取主色+次色(经 meow_img_pixels 读像素,见 .c)。返回 0=成功;-1=失败。 */
|
||||
int meow_img_duocolor(const meow_img *im, uint32_t *main, uint32_t *second);
|
||||
|
||||
/* 由主/次色派生播放页 + 歌词页封面主题(纯函数,可宿主单测)。
|
||||
|
||||
@@ -229,7 +229,8 @@ static int fmt_bps(int fmt)
|
||||
case AUDIO_FMT_S16: return 2;
|
||||
case AUDIO_FMT_S24:
|
||||
case AUDIO_FMT_S32:
|
||||
case AUDIO_FMT_FLOAT: return 4;
|
||||
case AUDIO_FMT_FLOAT:
|
||||
case AUDIO_FMT_DOP: return 4; /* DOP=32-bit DoP 容器(16bit DSD+标记+0),同 S32 宽 */
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
@@ -262,7 +263,7 @@ static int send_write_chunk(const uint8_t *p, uint32_t len)
|
||||
|
||||
/* ---- 公开 API ---- */
|
||||
|
||||
int meow_audio_open(int rate, int ch, int fmt)
|
||||
int meow_audio_open_ex(int rate, int ch, int fmt, uint32_t flags)
|
||||
{
|
||||
/* env 检查:调试直开 */
|
||||
const char *de = getenv("MEOW_AUDIO_DIRECT");
|
||||
@@ -270,7 +271,10 @@ int meow_audio_open(int rate, int ch, int fmt)
|
||||
|
||||
pthread_mutex_lock(&g_mu);
|
||||
if (g_has_stream) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_BUSY; }
|
||||
if (ch > 2 || ch < 1 || rate <= 0 || fmt < AUDIO_FMT_S16 || fmt > AUDIO_FMT_FLOAT || fmt == AUDIO_FMT_FLOAT) {
|
||||
int dsd = (fmt == AUDIO_FMT_DOP) && (flags & AUDIO_FLAG_DSD);
|
||||
int fmt_ok = (fmt == AUDIO_FMT_S16 || fmt == AUDIO_FMT_S24 ||
|
||||
fmt == AUDIO_FMT_S32 || fmt == AUDIO_FMT_DOP);
|
||||
if (ch > 2 || ch < 1 || rate <= 0 || !fmt_ok || (fmt == AUDIO_FMT_DOP && !dsd)) {
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return MEOW_ERR_INVAL;
|
||||
}
|
||||
@@ -297,7 +301,7 @@ int meow_audio_open(int rate, int ch, int fmt)
|
||||
m_audio_open_t o;
|
||||
memset(&o, 0, sizeof o);
|
||||
snprintf(o.app_id, sizeof o.app_id, "%s", getenv("MEOW_LOG_APP") ? getenv("MEOW_LOG_APP") : "app");
|
||||
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt;
|
||||
o.rate = (uint32_t)rate; o.ch = (uint16_t)ch; o.fmt = (uint16_t)fmt; o.flags = flags;
|
||||
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1); /* 单调递增(单个挂起) */
|
||||
g_pending = req;
|
||||
g_resp_len = 0;
|
||||
@@ -322,6 +326,35 @@ int meow_audio_open(int rate, int ch, int fmt)
|
||||
return (int)g_sid;
|
||||
}
|
||||
|
||||
int meow_audio_open(int rate, int ch, int fmt)
|
||||
{
|
||||
return meow_audio_open_ex(rate, ch, fmt, 0);
|
||||
}
|
||||
|
||||
int meow_audio_set_volume(int pct)
|
||||
{
|
||||
if (pct < 0) pct = 0;
|
||||
if (pct > 100) pct = 100;
|
||||
pthread_mutex_lock(&g_mu);
|
||||
int fd = conn_locked();
|
||||
if (fd < 0) { pthread_mutex_unlock(&g_mu); return MEOW_ERR_NOENT; }
|
||||
m_audio_volume_t v; v.pct = (int32_t)pct;
|
||||
uint32_t req = (g_pending == 0xFFFFFFFF) ? 1 : (g_pending + 1);
|
||||
g_pending = req;
|
||||
if (send_frame(g_fd, MSG_AUDIO_VOLUME, req, &v, sizeof v) != 0) {
|
||||
mark_dead_locked(); pthread_mutex_unlock(&g_mu); return MEOW_ERR_IO;
|
||||
}
|
||||
/* 等 ACK(3s 超时;audiod 停摆时透明失败标记流死) */
|
||||
struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); ts.tv_sec += 3;
|
||||
int rc = 0;
|
||||
while (g_pending == req) {
|
||||
if (pthread_cond_timedwait(&g_cond, &g_mu, &ts) != 0) { rc = MEOW_ERR_TIMEOUT; break; }
|
||||
}
|
||||
if (g_resp_type != MSG_ACK) rc = (rc == 0) ? MEOW_ERR_IO : rc;
|
||||
pthread_mutex_unlock(&g_mu);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int meow_audio_write(const void *pcm, int frames)
|
||||
{
|
||||
if (!pcm || frames <= 0) return 0;
|
||||
@@ -429,8 +462,10 @@ static int state_text_parse(const char *t, meow_audio_state *o)
|
||||
if (strncmp(p + 4, "S16", 3) == 0) f = AUDIO_FMT_S16;
|
||||
else if (strncmp(p + 4, "S24", 3) == 0) f = AUDIO_FMT_S24;
|
||||
else if (strncmp(p + 4, "S32", 3) == 0) f = AUDIO_FMT_S32;
|
||||
else if (strncmp(p + 4, "DoP", 3) == 0) f = AUDIO_FMT_DOP;
|
||||
o->fmt = (uint16_t)f;
|
||||
}
|
||||
if ((p = strstr(t, "dsd="))) o->dsd = atoi(p + 4);
|
||||
if ((p = strstr(t, "written="))) o->written_frames = (uint64_t)strtoull(p + 8, NULL, 10);
|
||||
if ((p = strstr(t, "played="))) o->played_frames = (uint64_t)strtoull(p + 7, NULL, 10);
|
||||
if ((p = strstr(t, "underrun="))) o->underrun = (uint32_t)atoi(p + 9);
|
||||
|
||||
@@ -49,8 +49,12 @@ void meow_img_free(meow_img *im)
|
||||
free(im);
|
||||
}
|
||||
|
||||
int meow_img_w(meow_img *im) { return im ? im->w : 0; }
|
||||
int meow_img_h(meow_img *im) { return im ? im->h : 0; }
|
||||
int meow_img_w(const meow_img *im) { return im ? im->w : 0; }
|
||||
int meow_img_h(const meow_img *im) { return im ? im->h : 0; }
|
||||
|
||||
/* 访问器:取 RGBA 像素指针(meow_img 对外 opaque,本函数为唯一像素读入口;供取色/直读像素)。
|
||||
* NULL 返回 NULL。 */
|
||||
const unsigned char *meow_img_pixels(const meow_img *im) { return im ? im->rgba : NULL; }
|
||||
|
||||
/* 最近邻采样缩放(cover=1 居中裁方),返回 dw*dh*4 临时 RGBA 缓冲 */
|
||||
static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
|
||||
@@ -70,13 +74,16 @@ static unsigned char *scale_buf(const meow_img *im, int dw, int dh, int cover)
|
||||
s = sx < sy ? sx : sy; /* 源像素/目标像素(铺满 → 取小) */
|
||||
float oxs = ((float)im->w - (float)dw * s) * 0.5f; /* 居中裁剪起点(源像素,≥0) */
|
||||
float oys = ((float)im->h - (float)dh * s) * 0.5f;
|
||||
if (oxs < 0) oxs = 0; if (oys < 0) oys = 0;
|
||||
if (oxs < 0) oxs = 0;
|
||||
if (oys < 0) oys = 0;
|
||||
for (int dy = 0; dy < dh; dy++) {
|
||||
int syi = (int)((float)dy * s + oys);
|
||||
if (syi < 0) syi = 0; if (syi >= im->h) syi = im->h - 1;
|
||||
if (syi < 0) syi = 0;
|
||||
if (syi >= im->h) syi = im->h - 1;
|
||||
for (int dx = 0; dx < dw; dx++) {
|
||||
int sxi = (int)((float)dx * s + oxs);
|
||||
if (sxi < 0) sxi = 0; if (sxi >= im->w) sxi = im->w - 1;
|
||||
if (sxi < 0) sxi = 0;
|
||||
if (sxi >= im->w) sxi = im->w - 1;
|
||||
memcpy(out + ((size_t)dy * dw + dx) * 4,
|
||||
im->rgba + ((size_t)syi * im->w + sxi) * 4, 4);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
# ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)----
|
||||
patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|8de51144538d698c3ff82ee6651b8b1a|引导选单 M6.25(开机菜单/第二屏亮度按用户设置恢复:显示前读 settings.conf 写回,方案A)
|
||||
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|6d2ddb25442a6cdd81cd87a44300f676|M6.33 ①移除播放页底部歌词占位符(已有独立歌词页,删NP_LYRIC_Y+占位块,播放页底部干净) ②封面'空→复原'恢复修复(meta_changed补artwork检测+np_cover_load缓存去重改为空也重载+歌词页补载封面);宿主test_np_cover 14项PASS,0warning;链sdk meow_lyric/lyric_view/meow_img_color
|
||||
patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|27e10b8cf02f6a6f66729cc3f4b817b5|M6.34 批①后台常驻框架+宽限期(meow_bgplay/meow_media/app_launcher/internal.h,MEOW_BG_GRACE_MS)+链新meow_ui.a(sdk meow_lyric/lyric_view/meow_img_color,meow_img_pixels访问器);0warning
|
||||
patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|34ddbf0275526eaf9be988134845c3af|应用看门狗 M6.23(亮屏亮度按用户设置 display.brightness 恢复,修 panel ON 固定60跳变)
|
||||
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)
|
||||
@@ -38,8 +38,8 @@ patch_v2/meow_svc_power/meow_svc_power|/usr/bin/meow_svc_power|39ae3aa255695a97f
|
||||
patch_v2/meow_verify/meow_verify|/usr/bin/meow_verify|6c2e8ed24aebe5caf41e7aed391312d5|II期验签 2B(多公钥+v2签名块 agent-221)
|
||||
patch_v2/meow_crash/meow_crash|/usr/bin/meow_crash|f02c4f45099157dc388b3d10b443aa57|系统级崩溃捕获 (agent-187) + log升级: 清理前缀扩 perf_/hang_ 承接1GB纪律 (337)
|
||||
patch_v2/meow_svc_perf/meow_svc_perf|/usr/bin/meow_svc_perf|566f0615698efe32c1f90a9c62f09148|性能专项服务 M2.75 + log升级(337:卡死快照+perf基线) + OOM治理C(348:Cached/Dirty监控降载, /tmp/meow_cachegov_c.state单点)
|
||||
patch_v2/meow_audiod/meow_audiod|/usr/bin/meow_audiod|553c8fc0d3f6ad216303c2fc449d05e2|独占音频守护 M6.31(M6.5 315回收+蓝牙P2 OUT_BT输出目标+libasound直连pcm.bluealsa glibc-dynamic脱离dlopen+bt_dev DEV自动识别当前A2DP设备)
|
||||
patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|7a9cd32fbd6400564a7f98230f95c63d|audiod 用户侧 CLI M6.30(+蓝牙P2 audio_output/OUT_BT codec 展示;monitor 实时电平表/文本电平条;status 补 rms/peak)
|
||||
patch_v2/meow_audiod/meow_audiod|/usr/bin/meow_audiod|9f47c8fdce1fc92d718bcbb0044dd2e9|独占音频守护 M6.34(A3+DSD:24/32bit高清byte-exact偏好S32+独立AUDIO_FMT_DOP DoP透传+音量收编v2双组写PCM/DSD_VOLUME+监控键;glibc-dynamic链libasound;0warning)
|
||||
patch_v2/meow_audiod/meow_audio|/usr/bin/meow_audio|6864ec38850c7dd103095bf43de77843|audiod 用户侧 CLI M6.34(+蓝牙P2 audio_output/OUT_BT codec 展示;monitor 实时电平表/文本电平条;status 补 rms/peak;0warning)
|
||||
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_powermgr|1ef2f8afc07acc7aa09dee9f7149aba8|功耗管理专项服务期2 M6.1(期1+history/top/events+mp2731 current_now 实测充电功率+WiFi 未关联档/BT rfkill 细化+系数路径 /data/meow/powermgr/;IPC 0xD0-D7)
|
||||
patch_v2/meow_powermgr/meow_powermgr|/usr/bin/meow_power|1ef2f8afc07acc7aa09dee9f7149aba8|meow_powermgr 的 CLI 形态(同二进制双形态,argv[0]=meow_power 即 CLI:status/estimate/model/history/top/events,policy 期3桩)
|
||||
patch_v2/meow_netmgrd/meow_netmgrd|/usr/bin/meow_netmgrd|9c345b2e43138bc0299c749ce0f6bb5d|网络管理服务期6 M6.28(蓝牙P1 BT_CTL权威收口:hcitool con判连接/paired-devices补名/a2dp详情/connect-disconnect-pair-unpair-scan)
|
||||
|
||||
Reference in new issue
Block a user