Files
MeowOS-For-R3P2/patch_v2/meowpanel/src/mp_server.c
T
AskaEth 7c7d891570 feat: M6.44+M6.45 收编 — 真机 0.8.45 现场保全(reader章节/key_router按键路由/WiFi连接修复/ncm bench+IME修复)
脏工作区现场收编:devdocs/local/m6_44_45_dirty_tracked.patch(46 个已跟踪文本文件,
+3090/-585, 已对 M6.43 基线干净适配) + m6_44_45_untracked_src.tar.gz 中 patch_v2/ 下
13 个新增源文件, 合成单个提交, 内容与容器工作树(R3ProII/patch_v2)逐字节一致。

- reader 章节: 新增 core/rdchapter.{h,c} — TXT 章节规范解析器, 扫描 512KB 码位对齐段文件
  逐段流式识别标题行(行首缩进≤4/显示宽≤40/行内标点≤1 三重约束防误判), 输出标题+全书字符
  偏移+段号/段内偏移+阿拉伯章号(中文数字归一), 首章前内容归"卷首"; book/reader/rdenc/
  rdlayout/settings 接入章节跳转与进度锚点, 附 chapter_smoke/turn_repro/build_turn_repro.sh
- key_router 按键路由: 新增 meow_launcherd/key_router.{h,c} — launcher-UI 页物理键统一路由
  (P1 仅音量键), 把读取边沿+防抖+长按 held+ramp 推进从 5 个会话收敛为进程内单开;
  各页每圈 key_router_pump() 取防抖后边沿, 动作(调音量/光标导航)仍由调用页决定,
  保留 run_menu 的 meow_mode 分支; App 前台 volume_page 武装时边沿抑制;
  main_menu/now_playing/notif_center/settings_page/subpage 接入, 附 test_key_router_vol/test_np_keys
- WiFi 连接修复: meow_netmgrd connect 有界等待 wpa_state=COMPLETED(普通 10s/隐藏网络 15s,
  env 可调)修设置页连接无响应; list_networks/scan_results 的 \xNN 转义还原始字节, 修中文 SSID
  永不命中致新建同名 open 记录顶影; launcher netmgr_ipc 配读超时; 附 test_wifi_connect(桩
  wpa_cli 驱动 static wifi_connect_ex), test_netmgrd.sh 补用例; wifi_page 同步
- ncm bench: 新增 bench/lyric_sim.c、bench/plpage_sim.c, vol_sync_sim 与 ncm core/app 同步
- SDK IME 修复: shift 大小写、K_ENTER 只提交不关、阻塞 IME 循环喂 on_tick 不被遮挡、
  IME 打开 ~200ms TAP 冷却窗吞 stray-TAP; 附 sdk/test_ime_fix.c
- 同期: meow_audiod 音量异步 ACK 线程(代际防 fd/槽复用竞态)、watchdog 音量收编(首选 audiod
  权威, settings.conf 覆盖路径)、panel 文件搜索(F1-④)与蓝牙只读详情(F1-③)、各类 build 脚本同步

范围红线: third_party/legado-E-main 原开发者未跟踪, 本次不入库(保留 devdocs/local tar 参考);
二进制不入库(后续重编); devdocs/ 不入库。59 文件全部在 patch_v2/ 下, 无 third_party/devdocs。
验证: git apply --check 干净; 59 文件 md5 与容器 patch_v2 逐一比对 59/59 一致(含
key_router.c/rdchapter.c/meow_ui_ime.c/test_wifi_connect.c 抽查); 可执行位保守
(check_lgpl_release.sh 100755, build_turn_repro.sh 100644 与容器一致)。
2026-11-04 20:02:28 +08:00

828 lines
43 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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/ams/bg")) return api_ams_bg(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/bt")) return api_net_bt(sock); /* F1-③ 蓝牙只读详情(0xE6 读级聚合) */
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);
if (!strcmp(p, "/api/input/gesture")) return api_input_gesture(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|killfg(管启/停;require_auth 已护,kill 前台前端二次确认;
* killfg=前台救命强制终止:am kill 优先,launcher 拒→直接 pid 信号,前端二次确认+标注前台) */
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 > 7 && !strcmp(p + pl - 7, "/killfg")) {
char pkg[256];
if (path_segment("/api/ams/", p, pkg, sizeof pkg))
return api_ams_kill_fg(sock, pkg);
} 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, "GET") && !strcmp(p, "/api/files/search")) return api_files_search(sock, r); /* F1-④ 搜索(只读) */
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_t *pwe = jv_obj_get(pr, "policy_write_enabled"); /* guard③ 状态键必须为布尔,且拼读无括号错位 */
if (!pwe || pwe->type != J_BOOL) { printf("SELFTEST FAIL: net_policy_parse 缺/损坏 policy_write_enabled\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);
}
/* 6) ams_bg_parse:喂含 bg/fg 混合 state 的 am list 文本 → 仅 bg 条目、合法 JSON,
* cpu_pct/rss_kb 为 null(读不到/首采样)或数字——绝不编造不一致字段。 */
{
char list[] =
"# amd register+state\n"
"APP os.meow.a 名A bg 1 1 0 clean 0\n"
"APP os.meow.b 名B fg 9999992 1 0 clean 0\n"
"APP os.meow.c 名C bg 9999991 2 1 exit 1\n";
sbuf_t b = {0};
ams_bg_parse(list, &b);
if (!b.b || !utf8_valid_n(b.b, b.n)) { printf("SELFTEST FAIL: ams_bg_parse 产物非法字节/为空\n"); sbuf_free(&b); return 1; }
jv_t *br = jv_parse(b.b, (int)b.n);
if (!br) { printf("SELFTEST FAIL: ams_bg_parse 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
jv_t *bapps = jv_obj_get(br, "apps");
if (!bapps || bapps->type != J_ARR) { printf("SELFTEST FAIL: ams_bg_parse 缺 apps 数组\n"); jv_free(br); sbuf_free(&b); return 1; }
int bcnt = 0;
for (jv_t *e = bapps->child; e; e = e->next) {
bcnt++;
jv_t *st = jv_obj_get(e, "state");
if (!st || st->type != J_STR || strcmp(st->str, "bg") != 0) { printf("SELFTEST FAIL: ams_bg_parse 条目 state 非 bg\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *cp = jv_obj_get(e, "cpu_pct");
jv_t *rs = jv_obj_get(e, "rss_kb");
if (!cp || (cp->type != J_NULL && cp->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse cpu_pct 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; }
if (!rs || (rs->type != J_NULL && rs->type != J_NUM)) { printf("SELFTEST FAIL: ams_bg_parse rss_kb 非 null/数字\n"); jv_free(br); sbuf_free(&b); return 1; }
}
if (bcnt != 2) { printf("SELFTEST FAIL: ams_bg_parse bg 条目应 2 个,实际 %d\n", bcnt); jv_free(br); sbuf_free(&b); return 1; }
jv_free(br);
sbuf_free(&b);
}
/* 7) net_locked_entity:防自锁实体精确匹配(svc:/proc:/裸名);非关键实体放行(guard①)。 */
{
struct { const char *e; int want; } c[] = {
{ "svc:dropbear", 1 }, { "proc:netmgrd", 1 }, { "udhcpc", 1 },
{ "svc:sysui", 1 }, { "svc:wpa_supplicant", 1 }, { "svcd", 1 }, { "init", 1 },
{ "app:com.example", 0 }, { "svc:avahi", 0 }, { "proc:sh", 0 },
{ "app:com.netmgrd.helper", 0 }, /* 含子串但不等于裸名,不得误判 */
{ "", 0 }, { NULL, 0 }
};
for (int i = 0; c[i].e; i++) {
int got = net_locked_entity(c[i].e);
if (got != c[i].want) { printf("SELFTEST FAIL: net_locked_entity(%s)=%d 期望 %d\n", c[i].e, got, c[i].want); return 1; }
}
}
/* 8) policy 默认关 gate(guard③):无设置文件 → panel_policy_write_enabled()==0(默认关);
* settings_write_one("panel.policy_enabled","1") 后 ==1(写临时 settings 文件验证,测完清理)。 */
{
const char *saved = g_settings_file;
char tmpf[] = "/tmp/meowpanel_policy_gate_XXXXXX";
int tfd = mkstemp(tmpf);
if (tfd < 0) { printf("SELFTEST FAIL: mkstemp 失败\n"); return 1; }
close(tfd);
g_settings_file = tmpf;
int before = panel_policy_write_enabled();
if (settings_write_one("panel.policy_enabled", "1") != 0) {
printf("SELFTEST FAIL: 写 panel.policy_enabled 失败\n"); unlink(tmpf); g_settings_file = saved; return 1;
}
int after = panel_policy_write_enabled();
unlink(tmpf);
g_settings_file = saved;
if (before != 0 || after != 1) { printf("SELFTEST FAIL: policy gate 默认关/开启 %d->%d\n", before, after); return 1; }
}
/* 9) power_policy_append_flag:对象 JSON 剥尾拼入 policy_write_enabled 后仍可解析、键为布尔、原字段保留
* ——防括号错位重演(overview 教训)。测试在无 panel.policy_enabled 的临时 settings 下进行(期望 false=默认关)。 */
{
const char *saved = g_settings_file;
char tmpf[] = "/tmp/meowpanel_policy_append_XXXXXX";
int tfd = mkstemp(tmpf);
if (tfd >= 0) { close(tfd); g_settings_file = tmpf; }
sbuf_t p = {0};
if (power_policy_append_flag(&p, "{\"profile\":\"balanced\",\"enacted\":true") != 0) {
printf("SELFTEST FAIL: power_policy_append_flag 拼接失败\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
}
if (!utf8_valid_n(p.b, p.n) || !strstr(p.b, "\"policy_write_enabled\":false")) {
printf("SELFTEST FAIL: power_policy_append_flag 产物异常\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1;
}
jv_t *pr = jv_parse(p.b, (int)p.n);
if (!pr) { printf("SELFTEST FAIL: power_policy_append_flag 产物 JSON 无法解析\n"); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
jv_t *pwe = jv_obj_get(pr, "policy_write_enabled");
if (!pwe || pwe->type != J_BOOL || pwe->bval != 0) { printf("SELFTEST FAIL: power_policy_append_flag policy_write_enabled 非 false(默认关)\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
jv_t *pfl = jv_obj_get(pr, "profile");
if (!pfl || pfl->type != J_STR || strcmp(pfl->str, "balanced") != 0) { printf("SELFTEST FAIL: power_policy_append_flag 丢失原字段\n"); jv_free(pr); sbuf_free(&p); if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; } return 1; }
jv_free(pr);
sbuf_free(&p);
if (tfd >= 0) { unlink(tmpf); g_settings_file = saved; }
}
/* 10) net_bt_aggregate(F1-③):0xE6 五读文本聚合 → 合法 JSON + 字段断言;
* 设备名含截断坏字节 → sA_str_n 消毒为 '?' 且产物仍合法;子文本 NULL → 空数组;status NULL → available:false。 */
{
sbuf_t b = {0};
const char *st = "bt available=1 powered=1 address=4C:BC:98:B0:98:DB paired=2 connected=1";
const char *pa = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 流媒体客户\xE7\n";
const char *co = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\n";
const char *a2 = "A2DP 33:DE:2E:21:AD:30 codec=sbc rate=44100 channels=2 bits=16 acquired=1 role=src\n";
const char *dv = "Device 33:DE:2E:21:AD:30 MOONDROP Pudding\nDevice 17:0A:15:AA:BB:CC 17OP15\n";
if (net_bt_aggregate(st, pa, co, a2, dv, &b) != 0) { printf("SELFTEST FAIL: net_bt_aggregate 正常文本返回非 0\n"); sbuf_free(&b); return 1; }
if (!b.b || !utf8_valid_n(b.b, b.n) || !json_strict_balanced(b.b, b.n)) { printf("SELFTEST FAIL: net_bt_aggregate 产物非法字节/括号错位\n"); sbuf_free(&b); return 1; }
jv_t *br = jv_parse(b.b, (int)b.n);
if (!br) { printf("SELFTEST FAIL: net_bt_aggregate 产物 JSON 无法解析\n"); sbuf_free(&b); return 1; }
jv_t *pw = jv_obj_get(br, "powered");
if (!pw || pw->type != J_BOOL || !pw->bval) { printf("SELFTEST FAIL: net_bt_aggregate powered 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *pr = jv_obj_get(br, "paired");
if (!pr || pr->type != J_ARR || !pr->child || !pr->child->next || pr->child->next->next) { printf("SELFTEST FAIL: net_bt_aggregate paired 应 2 条\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *cn = jv_obj_get(br, "connected");
if (!cn || cn->type != J_ARR || !cn->child || cn->child->next) { printf("SELFTEST FAIL: net_bt_aggregate connected 应 1 条\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *a2p = jv_obj_get(br, "a2dp");
if (!a2p || a2p->type != J_ARR || !a2p->child) { printf("SELFTEST FAIL: net_bt_aggregate 缺 a2dp 条目\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *cd = jv_obj_get(a2p->child, "codec");
if (!cd || cd->type != J_STR || !cd->str || strcmp(cd->str, "sbc") != 0) { printf("SELFTEST FAIL: net_bt_aggregate a2dp codec 非 sbc\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_t *aq = jv_obj_get(a2p->child, "acquired");
if (!aq || aq->type != J_BOOL || !aq->bval) { printf("SELFTEST FAIL: net_bt_aggregate a2dp acquired 非 true\n"); jv_free(br); sbuf_free(&b); return 1; }
jv_free(br);
sbuf_free(&b);
/* status NULL → available:false;子文本 NULL → 空数组 */
sbuf_t b2 = {0};
net_bt_aggregate(NULL, NULL, NULL, NULL, NULL, &b2);
if (!b2.b || strstr(b2.b, "\"available\":false") == NULL) { printf("SELFTEST FAIL: net_bt_aggregate status NULL 未降级 available:false\n"); sbuf_free(&b2); return 1; }
sbuf_free(&b2);
sbuf_t b3 = {0};
net_bt_aggregate(st, NULL, NULL, NULL, NULL, &b3);
jv_t *b3r = b3.b ? jv_parse(b3.b, (int)b3.n) : NULL;
jv_t *b3p = b3r ? jv_obj_get(b3r, "paired") : NULL;
if (!b3p || b3p->type != J_ARR || b3p->child) { printf("SELFTEST FAIL: net_bt_aggregate 子文本 NULL 应为空数组\n"); if (b3r) jv_free(b3r); sbuf_free(&b3); return 1; }
jv_free(b3r);
sbuf_free(&b3);
}
/* 11) files_search_json(F1-④):沙盒卡根递归搜索(限深/限量/类型过滤/rel 回显)→ 合法 JSON + 计数断言;
* 白名单外起点 → -1;name 空 → -3;非法 type → -4。 */
{
char tmpl[] = "/tmp/mp_search_XXXXXX";
char *root = mkdtemp(tmpl);
if (!root) { printf("SELFTEST FAIL: mkdtemp 失败\n"); return 1; }
char saved_root[1024];
snprintf(saved_root, sizeof saved_root, "%s", g_tf_root_buf);
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", root);
char d1[1200], d2[1200], f1[1200], f2[1200], f3[1200];
snprintf(d1, sizeof d1, "%s/MeowOS", root);
snprintf(d2, sizeof d2, "%s/MeowOS/sub", root);
snprintf(f1, sizeof f1, "%s/MeowOS/Test Song.mp3", root);
snprintf(f2, sizeof f2, "%s/MeowOS/another.txt", root);
snprintf(f3, sizeof f3, "%s/MeowOS/sub/x Song2.log", root);
int setup_ok = (mkdir(d1, 0755) == 0 && mkdir(d2, 0755) == 0);
const char *mk[3] = { f1, f2, f3 };
for (int i = 0; i < 3 && setup_ok; i++) {
FILE *f = fopen(mk[i], "w");
if (!f) { setup_ok = 0; break; }
fputs("x", f); fclose(f);
}
if (!setup_ok) { printf("SELFTEST FAIL: 搜索沙盒构建失败\n"); snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root); rm_recursive(root); rmdir(root); return 1; }
int fails = 0;
sbuf_t s = {0};
if (files_search_json(d1, "song", NULL, 0, &s) != 0) { printf("SELFTEST FAIL: files_search_json 正常搜索返回非 0\n"); fails = 1; }
jv_t *sr = (!fails && s.b) ? jv_parse(s.b, (int)s.n) : NULL;
if (!fails && (!sr || !utf8_valid_n(s.b, s.n) || !json_strict_balanced(s.b, s.n))) { printf("SELFTEST FAIL: files_search_json 产物非法\n"); fails = 1; }
jv_t *cnt = (!fails && sr) ? jv_obj_get(sr, "count") : NULL;
if (!fails && (!cnt || cnt->type != J_NUM || cnt->inum != 2)) { printf("SELFTEST FAIL: files_search_json song 应中 2 条(递归命中子目录)\n"); fails = 1; }
jv_t *its = (!fails && sr) ? jv_obj_get(sr, "items") : NULL;
jv_t *rel0 = (its && its->child) ? jv_obj_get(its->child, "rel") : NULL;
if (!fails && (!rel0 || rel0->type != J_STR || !rel0->str || strncmp(rel0->str, "/MeowOS/", 8) != 0)) { printf("SELFTEST FAIL: files_search_json rel 未按卡根相对回显\n"); fails = 1; }
if (sr) jv_free(sr);
sbuf_free(&s);
/* type=dir 过滤:建 Songs 目录应仅中目录 */
char d3[1200];
snprintf(d3, sizeof d3, "%s/MeowOS/Songs", root);
if (!fails && mkdir(d3, 0755) != 0) { printf("SELFTEST FAIL: 沙盒 Songs 目录创建失败\n"); fails = 1; }
sbuf_t s2 = {0};
if (!fails && files_search_json(d1, "song", "dir", 0, &s2) == 0) {
jv_t *sr2 = jv_parse(s2.b, (int)s2.n);
jv_t *c2 = sr2 ? jv_obj_get(sr2, "count") : NULL;
jv_t *i2 = sr2 ? jv_obj_get(sr2, "items") : NULL;
jv_t *t0 = (i2 && i2->child) ? jv_obj_get(i2->child, "type") : NULL;
if (!c2 || c2->type != J_NUM || c2->inum != 1 || !t0 || t0->type != J_STR || strcmp(t0->str, "dir") != 0) {
printf("SELFTEST FAIL: files_search_json type=dir 应仅中 1 个目录\n"); fails = 1;
}
if (sr2) jv_free(sr2);
} else if (!fails) { printf("SELFTEST FAIL: files_search_json type=dir 搜索失败\n"); fails = 1; }
sbuf_free(&s2);
/* 无匹配 → count 0;白名单外 → -1;空 name → -3;非法 type → -4 */
sbuf_t s3 = {0};
if (!fails && files_search_json(d1, "zzzz", NULL, 0, &s3) == 0) {
if (!strstr(s3.b, "\"count\":0")) { printf("SELFTEST FAIL: files_search_json 无匹配应 count 0\n"); fails = 1; }
} else if (!fails) { printf("SELFTEST FAIL: files_search_json 无匹配搜索失败\n"); fails = 1; }
sbuf_free(&s3);
sbuf_t s4 = {0};
if (!fails && files_search_json("/etc", "x", NULL, 0, &s4) != -1) { printf("SELFTEST FAIL: files_search_json 白名单外未拒\n"); fails = 1; }
if (!fails && files_search_json(d1, "", NULL, 0, &s4) != -3) { printf("SELFTEST FAIL: files_search_json 空 name 未拒\n"); fails = 1; }
if (!fails && files_search_json(d1, "x", "bogus", 0, &s4) != -4) { printf("SELFTEST FAIL: files_search_json 非法 type 未拒\n"); fails = 1; }
sbuf_free(&s4);
snprintf(g_tf_root_buf, sizeof g_tf_root_buf, "%s", saved_root);
rm_recursive(root);
rmdir(root);
if (fails) return 1;
}
printf("SELFTEST PASS: 截断 UTF-8「流媒体客户\\xE7」+ net/power/ams 端点池 + 蓝牙 0xE6 聚合 + 文件搜索 → 合法 JSON + 无非法字节;防自锁实体 + policy 门禁 默认关/开启 校验通过\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;
}