242 lines
11 KiB
C
242 lines
11 KiB
C
/* 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;
|
||
}
|