/* 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 = ` exit=` 或 ` signal=`;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; }