feat(panel, PANEL_ENHANCE_PLAN 批2): 崩溃/日志中心 + 音频写 + 后端补缺口
后端(meowpanel.c 0-warning,-Werror 通过): - /api/system 补 reboot_reason/upgrade_status(svc_power 读级 POWER_REBOOT_REASON(8)/UPGRADE_STATUS(9)) - /api/overview net 块并入 DNS 统计(首页严格单请求;DNS 不可用省略 dns 键不谎报) - /api/audio 补 vol_max(读 settings audio.volume_max) - POST /api/audio/route(仅 0=auto/2=3.5/3=4.4,拒 1/4/5;写级 PIN 已护) - POST /api/audio/volume(clamp 到 vol_max,防炸耳红线;写级 PIN 已护) - GET /api/logs/crash(崩溃汇总:每实体崩溃数+最近时间/信号 + dmesg 计数;256KB/64 实体/5 行上限) - /api/logs 文件列表加 group 来源分组 - 顺带补回 批1-B 前端读取的 boot.mode /tmp/meow_boot_mode(此前遗留未提交) 前端(web/meowpanel.js 双主题): - 音频页:路由选择器(自动/3.5/4.4)+ 音量滑条(上限 vol_max、平方锥度非线性标注、确认弹窗) - 日志页:崩溃汇总卡 + 崩溃尾巴一键看 stdout 尾部 + 按来源分组 + 前缀过滤 - 首页:netCard 改读 overview net.dns,去掉单独 /api/net/dns 验证:宿主编译 0 warning + -Werror;自建桩测试 24/24(crash 解析/路由 1|4|5 拒 422/音量 clamp 到 50/ reboot_reason+upgrade_status 值/overview net.dns);headless Chromium 渲染 audio/logs/home/system/ light 主题均正确。md5(meowpanel)=aed2ea17a34614acaa26dcbc8965ebb1。
This commit is contained in:
1 parent
34f2834704
commit
33d3059038
5 files changed
+547
-59
No files matched your search
+325
-22
@@ -115,6 +115,7 @@ static const char *g_notif_log = NULL; /* 通知转储日志:<card>/notificat
|
||||
#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 覆盖) */
|
||||
|
||||
/* 白名单子目录(TF 卡根下) */
|
||||
static const char *g_allowed_dirs[] = { "MeowOS", "Music", "MeowData", "MeowAPPs", "book" };
|
||||
@@ -926,6 +927,17 @@ done:
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* 从 svc_power 状态文本取 `k=v` 的 v(如 "coldboot=reboot" → "reboot";key 须精确匹配带 '=')。
|
||||
* 读级命令(POWER_REBOOT_REASON/UPGRADE_STATUS)的 st.msg 即此形态。 */
|
||||
static 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);
|
||||
}
|
||||
|
||||
/* ---------------- 音频状态:meow_audiod IPC(MSG_AUDIO_QUERY→MSG_AUDIO_STATE) ----------------
|
||||
* audiod 无 MSG_HELLO 握手(见 patch_v2/meow_audiod/meow_audio.c CLI:直连发空负载 QUERY,
|
||||
* 收 STATE 文本帧——"k=v" 空格分隔、NUL 结尾)。audiod 不在/超时/应答异常一律回
|
||||
@@ -957,6 +969,16 @@ static long long audio_kv_ll(const char *txt, const char *key, long long defv)
|
||||
return strtoll(b, NULL, 10);
|
||||
}
|
||||
|
||||
/* 音频音量上限:读 settings.conf 的 audio.volume_max(权威键,与 launcher/watchdog/svc_power/sdk 同用);
|
||||
* 缺省回 PANEL_VOL_MAX_DEFAULT(=MEOW_VOL_MAX_DEFAULT 100)。面板写音量时据此 clamp(防炸耳红线)。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* 音频快照填充:把 `{"available":...}` 追加进 sbuf(供 /api/audio 单发与 /api/overview 聚合共用)。
|
||||
* audiod 不在/超时/应答异常一律 `{"available":false}`(HTTP 200,不报错)——面板/首页据此降级,绝不假数据。 */
|
||||
static void audio_fill(sbuf_t *s)
|
||||
@@ -988,8 +1010,8 @@ static void audio_fill(sbuf_t *s)
|
||||
sA(s, ",\"port\":%lld,\"vol\":%lld",
|
||||
audio_kv_ll(txt, "port", -1), audio_kv_ll(txt, "vol", -1));
|
||||
/* 电平监控(261):rms/peak deci-dB(-990=-inf),lvlseq=快照代际;透传时别漏(ABI 追加键)。 */
|
||||
sA(s, ",\"rms\":%lld,\"peak\":%lld,\"lvlseq\":%lld}",
|
||||
audio_kv_ll(txt, "rms", -990), audio_kv_ll(txt, "peak", -990), audio_kv_ll(txt, "lvlseq", 0));
|
||||
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());
|
||||
}
|
||||
|
||||
static int api_audio(int sock)
|
||||
@@ -1001,6 +1023,74 @@ static int api_audio(int sock)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---- 音频写命令(写级,PIN 已护):route/volume 直连 audiod(无 MSG_HELLO 握手),发帧→收 MSG_ACK。
|
||||
* audiod 对非法 port 也回 MEOW_OK(内部仅记录不应用,见 meow_audiod.c MSG_AUDIO_ROUTE),
|
||||
* 故面板在**发出前**自行校验,绝不放 1(lineout)/4(SPDIF)/5(audiod 内部强制重烧值)。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* POST /api/audio/route → 手动切输出路由。仅接受 0=auto/2=3.5/3=4.4;
|
||||
* **绝不接受 1/4/5**(4=SPDIF、5=audiod 内部强制重烧 FPGA 值)。写级:前端切换前确认弹窗(选错口→无声)。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* POST /api/audio/volume → 写音量。**必须 clamp 到系统 vol_max**(读 settings.conf `audio.volume_max`,
|
||||
* 缺省 100;设备当前按 plan 为 50)。audiod 内部仅 clamp 0-100、无 vol_max 防护——面板此处是唯一把守方
|
||||
* (防炸耳红线:平方锥度下靠近 100 极响,M2.96 曾有 dac 写 0=满幅事故)。 */
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* ---------------- 功耗管理:meow_powermgr IPC(MSG_PM_QUERY→MSG_PM_RESULT) ----------------
|
||||
* 客户端序列同 perf/power:connect → MSG_HELLO → 读 ACK → MSG_PM_QUERY(subset) → MSG_PM_RESULT(JSON)。
|
||||
* powermgr 不在/超时/应答异常 → 负错误码(调用方转 503 POWER_UNAVAILABLE;overview 里转 available:false)。 */
|
||||
@@ -1162,33 +1252,64 @@ static int api_net_conns(int sock, const char *query)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* GET /api/net/dns → 0xEC status(netmgrd 对 status/stats 同输出)→ DNS 代理统计 JSON。 */
|
||||
static int api_net_dns(int sock)
|
||||
/* DNS 代理统计对象 JSON 到 s(即 `{...}` 本体,不含外层 "dns" 键)。
|
||||
* 返回 0=成功;-1=传输不可达(netmgrd 不在);-2=code!=OK(DNS 代理不可用)。
|
||||
* 供 /api/net/dns 单发与 /api/overview 的 net 块复用(首页严格单请求)。 */
|
||||
static 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 send_err(sock, 503, "NET_UNAVAILABLE", "网络管理服务不可用");
|
||||
if (code != MEOW_OK)
|
||||
return send_err(sock, 503, "NET_NODEV", txt[0] ? txt : "DNS 代理不可用");
|
||||
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);
|
||||
sbuf_t s = {0};
|
||||
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}",
|
||||
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");
|
||||
int rc = send_json(sock, 200, s.b ? s.b : "");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* GET /api/net/dns → 0xEC status(netmgrd 对 status/stats 同输出)→ DNS 代理统计 JSON。 */
|
||||
static 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 rc;
|
||||
return out;
|
||||
}
|
||||
|
||||
/* overview 用的 net 块:netmgrd 状态 JSON + 本面板 DNS 统计(并入 "dns" 子键),首页可严格单请求。
|
||||
* 返回 0=成功(已追加 `,"net":{...}`);-1=netmgrd 状态不可用(调用方回 available:false,绝不假数据)。 */
|
||||
static 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;
|
||||
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, "}}");
|
||||
sbuf_free(&dns);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* POST /api/net/dns/flush → 0xEC flush(写级:HTTP 层走 require_auth;IPC 层由 netmgrd 依身份判定)。 */
|
||||
@@ -1473,10 +1594,8 @@ static int api_overview(int sock)
|
||||
else sA(&s, "\"power\":{\"available\":false},");
|
||||
sA(&s, "\"audio\":");
|
||||
audio_fill(&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])
|
||||
sA(&s, ",\"net\":%s", njson);
|
||||
if (net_block_fill(&s) == 0)
|
||||
; /* net 块已追加(状态 + 合并的 dns 子键) */
|
||||
else
|
||||
sA(&s, ",\"net\":{\"available\":false}");
|
||||
sA(&s, ",\"ams\":");
|
||||
@@ -3287,6 +3406,19 @@ static int api_system(int sock)
|
||||
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};
|
||||
@@ -3300,6 +3432,8 @@ static int api_system(int sock)
|
||||
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];
|
||||
if (perf_extract_network(json, netbuf, sizeof netbuf) == 0) {
|
||||
@@ -3334,7 +3468,25 @@ static int api_system(int sock)
|
||||
sA_str(&s, ver);
|
||||
sA(&s, ",\"latest\":");
|
||||
sA_str(&s, ver);
|
||||
sA(&s, ",\"available\":false,\"url\":null}}");
|
||||
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);
|
||||
@@ -3437,6 +3589,21 @@ static int api_key_batch(int sock, const req_t *r)
|
||||
}
|
||||
|
||||
/* ---------------- 日志 ---------------- */
|
||||
|
||||
/* 日志来源分组(批2 崩溃/日志中心):按文件名前缀归组,供前端分组展示 + 前缀过滤。
|
||||
* meow_* → system(MeowOS 服务); crash_* → crash; ams_* → ams; audio_monitor_* → audio_monitor;
|
||||
* net_* → net; power_* → power; 其余 → app(App/stdout 兜底)。 */
|
||||
static 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";
|
||||
}
|
||||
|
||||
static int api_logs_list(int sock)
|
||||
{
|
||||
if (!tf_present()) return send_err(sock, 503, "NO_CARD", "未识别到 TF 卡");
|
||||
@@ -3459,7 +3626,9 @@ static int api_logs_list(int sock)
|
||||
first = 0;
|
||||
sA(&s, "{\"name\":");
|
||||
sA_str(&s, e->d_name);
|
||||
sA(&s, ",\"size\":%lld,\"mtime\":%lld}", (long long)st.st_size, (long long)st.st_mtime);
|
||||
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, "]}");
|
||||
@@ -3544,6 +3713,137 @@ static int api_logs_read(int sock, const req_t *r)
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* ---- 崩溃汇总(批2 崩溃/日志中心):解析最新 crash_<bootts>.log(meow_crash 落盘格式)
|
||||
* 一行一条:`<epoch_sec> <uptime_sec> <svc|dmesg> <detail>`;svc 的 detail=`<name> exit=<code>`/`<name> signal=<sig>`,
|
||||
* dmesg 的 detail=内核行原文。聚合出「每实体崩溃数 + 最近崩溃时间/信号」。只读、无副作用。 */
|
||||
#define CRASH_TAIL_MAX (256u * 1024u) /* 崩溃日志单次读取上限(内存约束,不整读大文件) */
|
||||
#define CRASH_ENT_MAX 64 /* 实体汇总上限 */
|
||||
#define CRASH_DMESG_TAIL 5 /* dmesg 保留最近行数 */
|
||||
|
||||
static 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;
|
||||
}
|
||||
|
||||
/* ---------------- 通知(数据源 = 优先 launcher 内存活跃集,回退 TF 归档) ----------------
|
||||
* 数据源优先级(MEOWPANEL_PROTOCOL §3.8 / 任务):
|
||||
* 1) launcher IPC 内存活跃集(实时)—— `MSG_NOTIFY_QUERY`(0x25)→`MSG_NOTIFY_QUERY_RESULT`(0x27)
|
||||
@@ -5106,6 +5406,8 @@ static int handle(int sock, req_t *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/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];
|
||||
@@ -5153,6 +5455,7 @@ static int handle(int sock, req_t *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);
|
||||
|
||||
/* ---- 通知 ---- */
|
||||
|
||||
Reference in new issue
Block a user