refactor(panel): 前后端按模块拆分+屏幕/控制合并页 — 后端meowpanel.c 6372行拆src/*.c+mp.h(235函数原型,20端点output byte-identical,0warning)+前端拆mp_shared/mp_pages/meowpanel三层;屏幕·遥控合并(左屏实时画面+右MD3 SVG矢量按钮无emoji);重编后热更TF卡

This commit is contained in:
AskaEth committed 2026-09-04 00:56:13 +08:00
1 parent f3ae06d7cd
commit 2dc4cad454
26 files changed
+9317 -2755

No files matched your search

+241
View File
@@ -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;
}