feat(crash): meow_crash 系统级崩溃捕获 (agent-187) — svcd 服务异常退出上报(AF_UNIX best-effort,不崩svcd)+dmesg增量扫segfault/oops; 落盘 TF MeowOS/logs/crash_<bootts>.log 无卡回退/tmp+迁移; CLI dump/tail; S89 预置 crash.conf(主会话补)

meow_crash 62781084; meow_svcd da6ebc66→605646c4(仅+68行上报挂点); 宿主5场景全过(上报链路/dmesg增量/无卡迁移/铁律验证/CLI)
This commit is contained in:
MeowOS committed 2026-08-31 04:39:34 +08:00
1 parent 243148e440
commit cf86cbd387
10 files changed
+885 -1

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# build_crash.sh — meow_crash 交叉编译(mipsel 静态)+ 宿主编译可选。
#
# 目标:R3 Pro II / MeowOS(mipsel-linux-muslsf,静态)。meow_crash 极简无外部依赖,
# 仅 POSIX + Linux 基础接口(同一套自管道/poll/AF_UNIX 模式,同 meow_svcd)。
#
# 用法:
# ./build_crash.sh # 交叉编译 → patch_v2/meow_crash/meow_crash(mipsel 静态)
# ./build_crash.sh host # 宿主编译 → patch_v2/meow_crash/meow_crash_host(集成测试用)
set -e
cd "$(dirname "$0")"
ROOT="$HOME/Project/R3ProII"
CC="$ROOT/.tools/zig-mipsel-cc"
if [ "$1" = "host" ]; then
HOST_CC="${CC_HOST:-cc}"
"$HOST_CC" -Os -Wall -Wextra meow_crash.c -o meow_crash_host
echo "== host build =="
ls -la meow_crash_host
file meow_crash_host
exit 0
fi
"$CC" -Os -Wall -Wextra -static meow_crash.c -o meow_crash
echo "== meow_crash (mipsel static) =="
ls -la meow_crash
md5sum meow_crash
file meow_crash
BIN
View File
Binary file not shown.
+676
View File
@@ -0,0 +1,676 @@
/* meow_crash.c — MeowOS 系统级崩溃捕获守护进程(二期组件 meow_crash)。
*
* 定位(用户定案,见 FRAMEWORK_DESIGN §10 / APPS.md §4.5 /
* devdocs/plan/SYS_BASELINE.md / devdocs/plan/APP_LOG_MANAGER.md):
* - svcd 托管的小守护(板载 `/usr/bin/meow_crash` + services.d 板载 conf,
* trigger=boot);svcd fork+exec 直接拉起,故本进程【前台常驻】(不 daemonize)。
* - 捕获源:
* ① svcd 服务异常退出(非 0 / 被信号杀)——svcd 在 reap_children 检测到异常
* 退出时经 AF_UNIX `/tmp/meow_crash.sock` 单行上报(SVC 行,best-effort);
* ② 内核 oops / 应用崩溃——周期性扫描 dmesg 尾部【新增】,记录上次读取偏移,
* 增量捕获含 "segfault"/"oops" 的行(v1 以 ① 为主,② 为辅助;不强求扫 core)。
* - 落盘:TF 卡 `MeowOS/logs/crash_<bootts>.log` 追加写(一行一条:时间戳/uptime/
* 来源/详情);无卡 → `/tmp/meow_crash.log`(256KB 滚动);卡出现后自动迁移
* (语义对齐 launcher log_poll_card);总量并入 logs/ 1GB 上限纪律(按 mtime 删最旧)。
* - CLI:`dump`(打印本 boot 崩溃清单)/ `tail N`;root 免鉴权。读日志文件,
* 不依赖守护进程在线。
* - 铁律:svcd 死亡通知全程非阻塞、可失败(connect 失败即放弃,绝不重试);
* 服务崩绝不能带崩 svcd(本进程只读 svcd 的一条消息做落盘,无共享内存)。
*
* 编译(mipsel 静态,见 build_crash.sh):
* ~/Project/R3ProII/.tools/zig-mipsel-cc -Os -Wall -Wextra -static meow_crash.c -o meow_crash
* 宿主编译(集成测试):gcc -Os -Wall -Wextra meow_crash.c -o meow_crash_host
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <time.h>
#include <dirent.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <poll.h>
/* ---- 默认路径 / 常量 ---- */
#define CRASH_CARD_DEFAULT "/usr/data/mnt/sd_0" /* TF 卡挂载点 */
#define CRASH_LOGS_REL "MeowOS/logs" /* 卡上日志目录(同 launcher) */
#define CRASH_SOCK_DEFAULT "/tmp/meow_crash.sock" /* svcd 崩溃上报 socket */
#define CRASH_TMP_LOG "/tmp/meow_crash.log" /* 无卡回退(tmpfs) */
#define CRASH_TMP_MAX (256u * 1024u) /* tmp 滚动上限(对齐 boot_buf) */
#define CRASH_LOG_SIZE_LIMIT (1u << 30) /* logs/ 总量 1GB 纪律 */
#define CRASH_LINE_MAX 1024 /* 单行上限 */
#define CRASH_SCAN_MS 1000 /* poll_card/dmesg 扫描间隔 */
#define CRASH_TICK_MAX_MS 1000 /* poll 上限 */
#define CRASH_CLEANUP_MS 60000 /* 1GB 清理节流 */
#define CRASH_PATH_MAX 512
#define CRASH_SOCK_PATH_MAX 108
#define CRASH_BOOTTS_LEN 64
#define CRASH_DMESG_MAX (128u * 1024u) /* dmesg 单次抓取上限 */
#define CRASH_SEEN_MAX 64 /* 关键字行去重环深度 */
#define CRASH_SIGTERM_CHAR 'T'
/* ---- 全局状态 ---- */
static char g_card[256] = CRASH_CARD_DEFAULT;
static char g_sock[CRASH_SOCK_PATH_MAX] = CRASH_SOCK_DEFAULT;
static char g_bootts[CRASH_BOOTTS_LEN] = ""; /* "%Y%m%d_%H%M%S"(去 meow_ 前缀) */
static int g_log_fd = -1; /* 卡日志 fd;-1=未开(落 /tmp) */
static int g_on_card = 0;
static int g_shutdown = 0;
static int g_sigpipe[2] = {-1, -1}; /* 信号自管道 */
static int g_listen_fd = -1;
/* dmesg 增量扫描状态 */
static char g_dmesg_prev[CRASH_DMESG_MAX]; /* 上次完整输出(前缀比对) */
static size_t g_dmesg_prev_len = 0;
static int g_seen_idx = 0; /* 去重环游标 */
static char g_seen[CRASH_SEEN_MAX][CRASH_LINE_MAX]; /* 最近关键字行(防环回重复) */
static unsigned long long now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (unsigned long long)ts.tv_sec * 1000 + (unsigned long long)ts.tv_nsec / 1000000;
}
static int safe_strcpy(char *dst, size_t cap, const char *src)
{
if (!dst || cap == 0) return -1;
int n = snprintf(dst, cap, "%s", src ? src : "");
return (n >= 0 && (size_t)n < cap) ? 0 : -1;
}
/* /proc/uptime 首字段 → 秒(整数)。失败返回 0。 */
static long read_uptime_sec(void)
{
int fd = open("/proc/uptime", O_RDONLY);
if (fd < 0) return 0;
char b[64];
ssize_t n = read(fd, b, sizeof b - 1);
close(fd);
if (n <= 0) return 0;
b[n] = '\0';
return (long)atof(b);
}
/* 本 boot 时间戳 "%Y%m%d_%H%M%S":复用 /tmp/meow_bootts(launcher 缓存),
* 同一 boot 会话跨进程共用。读不到则按 墙钟 - uptime 计算,并以 launcher 同款
* "meow_%Y%m%d_%H%M%S" 格式缓存,保证 launcher/meow_crash 取到同一 bootts。 */
static void boot_ts(char *out, size_t n)
{
out[0] = '\0';
int fd = open("/tmp/meow_bootts", O_RDONLY);
if (fd >= 0) {
char b[CRASH_BOOTTS_LEN];
ssize_t r = read(fd, b, sizeof b - 1);
close(fd);
if (r > 0) {
b[r] = '\0';
char *p = b;
if (strncmp(p, "meow_", 5) == 0) p += 5; /* 剥 launcher 前缀,只留 ts */
snprintf(out, n, "%s", p);
if (out[0]) return;
}
}
time_t now = time(NULL);
time_t up = (time_t)read_uptime_sec();
time_t boot = now - up;
struct tm tm;
localtime_r(&boot, &tm);
strftime(out, n, "%Y%m%d_%H%M%S", &tm);
char cached[CRASH_BOOTTS_LEN];
snprintf(cached, sizeof cached, "meow_%s", out);
fd = open("/tmp/meow_bootts", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) { ssize_t w = write(fd, cached, strlen(cached)); (void)w; close(fd); }
}
/* ---- 卡可用性 / 日志打开 ---- */
static int card_available(void)
{
const char *nc = getenv("MEOW_CRASH_NOCARD");
if (nc && nc[0] == '1') return 0; /* 测试钩子:强制无卡 */
char logs[CRASH_PATH_MAX];
if (snprintf(logs, sizeof logs, "%s/%s", g_card, CRASH_LOGS_REL)
>= (int)sizeof logs) return 0;
/* 默认卡路径 → 必须真实挂载(防把 rootfs 当卡);被 --card/env 覆写则视为普通目录
* (供宿主集成测试把 --card 指向临时目录即可走「有卡」路径)。 */
if (strcmp(g_card, CRASH_CARD_DEFAULT) == 0) {
FILE *f = fopen("/proc/mounts", "r");
if (!f) return 0;
char needle[CRASH_PATH_MAX];
char line[CRASH_PATH_MAX];
snprintf(needle, sizeof needle, " %s ", g_card);
int found = 0;
while (fgets(line, sizeof line, f)) {
if (strstr(line, needle)) { found = 1; break; }
}
fclose(f);
if (!found) return 0;
}
(void)mkdir(logs, 0755);
return access(logs, W_OK) == 0;
}
/* 把 logs/ 下超过 1GB 的部分按 mtime 删最旧(统计 meow_* 与 crash_*)。 */
static void log_cleanup_old(void)
{
char logs[CRASH_PATH_MAX];
if (snprintf(logs, sizeof logs, "%s/%s", g_card, CRASH_LOGS_REL)
>= (int)sizeof logs) return;
DIR *d = opendir(logs);
if (!d) return;
struct lf { char name[64]; long long size; long long mtime; } files[128];
int n = 0;
struct dirent *de;
while ((de = readdir(d)) != NULL && n < 128) {
const char *nm = de->d_name;
if (strncmp(nm, "meow_", 5) != 0 && strncmp(nm, "crash_", 6) != 0) continue;
char path[CRASH_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s", logs, nm) >= (int)sizeof path) continue;
struct stat st;
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) continue;
size_t nl = strlen(nm);
if (nl >= sizeof files[n].name) nl = sizeof files[n].name - 1;
memcpy(files[n].name, nm, nl);
files[n].name[nl] = '\0';
files[n].size = (long long)st.st_size;
files[n].mtime = (long long)st.st_mtime;
n++;
}
closedir(d);
long long total = 0;
for (int i = 0; i < n; i++) total += files[i].size;
/* 选择排序:mtime 升序(最旧在前) */
for (int i = 0; i < n; i++)
for (int j = i + 1; j < n; j++)
if (files[j].mtime < files[i].mtime) {
struct lf t = files[i]; files[i] = files[j]; files[j] = t;
}
int deleted = 0;
for (int i = 0; i < n && total > (long long)CRASH_LOG_SIZE_LIMIT; i++) {
char path[CRASH_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s", logs, files[i].name)
>= (int)sizeof path) continue;
if (unlink(path) == 0) { total -= files[i].size; deleted++; }
}
if (deleted)
fprintf(stderr, "[meow_crash] log_cleanup deleted %d old log(s)\n", deleted);
}
/* 打开卡日志 crash_<bootts>.log。成功返回 0(置 g_log_fd/g_on_card)。 */
static int log_open_card(void)
{
if (g_log_fd >= 0) return 0;
if (!card_available()) return -1;
char ts[CRASH_BOOTTS_LEN];
boot_ts(ts, sizeof ts);
char path[CRASH_PATH_MAX];
if (snprintf(path, sizeof path, "%s/%s/crash_%s.log", g_card, CRASH_LOGS_REL, ts)
>= (int)sizeof path) return -1;
int fd = open(path, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd < 0) return -1;
g_log_fd = fd;
g_on_card = 1;
return 0;
}
/* 追加一行:优先卡日志;卡日志未开/写失败 → /tmp(256KB 滚动)。 */
static void append_line(const char *line)
{
if (g_log_fd >= 0) {
ssize_t w = write(g_log_fd, line, strlen(line));
if (w < 0) { close(g_log_fd); g_log_fd = -1; g_on_card = 0; }
else return;
}
/* /tmp 回退(tmpfs,滚动到 256KB) */
struct stat st;
if (stat(CRASH_TMP_LOG, &st) == 0 && st.st_size > (long long)CRASH_TMP_MAX)
unlink(CRASH_TMP_LOG);
int fd = open(CRASH_TMP_LOG, O_WRONLY | O_APPEND | O_CREAT, 0644);
if (fd >= 0) { ssize_t w = write(fd, line, strlen(line)); (void)w; close(fd); }
}
/* 卡出现后把 /tmp 缓冲迁移进卡日志(语义对齐 launcher log_poll_card)。 */
static void poll_card(void)
{
if (g_log_fd >= 0) return;
if (log_open_card() != 0) return;
int fd = open(CRASH_TMP_LOG, O_RDONLY);
if (fd >= 0) {
char chunk[1024];
ssize_t cn;
while ((cn = read(fd, chunk, sizeof chunk)) > 0) {
if (write(g_log_fd, chunk, cn) < 0) {
close(fd);
g_log_fd = -1; g_on_card = 0;
return;
}
}
close(fd);
unlink(CRASH_TMP_LOG);
fprintf(stderr, "[meow_crash] /tmp buffered crash log moved to card\n");
}
}
/* 以当前时间/uptime 落一条「自身来源」事件(dmesg 用)。 */
static void emit_event(const char *source, const char *detail)
{
char line[CRASH_LINE_MAX];
long epoch = (long)time(NULL);
long up = read_uptime_sec();
int l = snprintf(line, sizeof line, "%ld %ld %s %s\n", epoch, up, source, detail);
if (l > 0) append_line(line);
}
/* 落 svcd 上报的来源事件(用 svcd 拍下的 epoch/uptime,即崩溃时刻)。 */
static void emit_svc(const char *detail, long epoch, long up)
{
char line[CRASH_LINE_MAX];
int l = snprintf(line, sizeof line, "%ld %ld svc %s\n", epoch, up, detail);
if (l > 0) append_line(line);
}
/* ---- 崩溃上报 socket(svcd → meow_crash) ---- */
/* 解析一行 svcd 死亡通知:SVC <name> <exit|sig> <num> <epoch> <uptime> */
static void handle_conn(int fd)
{
char buf[CRASH_LINE_MAX];
ssize_t n = read(fd, buf, sizeof buf - 1);
if (n <= 0) return;
buf[n] = '\0';
while (n > 0 && (buf[n - 1] == '\n' || buf[n - 1] == '\r')) buf[--n] = '\0';
if (n == 0) return;
char name[128], kind[8];
int num;
long epoch, up;
if (sscanf(buf, "SVC %127s %7s %d %ld %ld", name, kind, &num, &epoch, &up) == 5) {
char detail[CRASH_LINE_MAX];
if (strcmp(kind, "exit") == 0) snprintf(detail, sizeof detail, "%s exit=%d", name, num);
else if (strcmp(kind, "sig") == 0) snprintf(detail, sizeof detail, "%s signal=%d", name, num);
else return; /* 未知 kind,丢弃 */
emit_svc(detail, epoch, up);
}
}
/* ---- dmesg 增量扫描(来源②) ---- */
static char *read_dmesg(void)
{
const char *src = getenv("MEOW_CRASH_DMESG_FILE");
if (src && src[0]) { /* 测试钩子:从文件读假 dmesg */
FILE *f = fopen(src, "r");
if (!f) return NULL;
char *out = malloc(CRASH_DMESG_MAX);
if (!out) { fclose(f); return NULL; }
int off = 0;
while (off < (int)CRASH_DMESG_MAX - 1) {
int r = (int)fread(out + off, 1, CRASH_DMESG_MAX - 1 - off, f);
if (r <= 0) break;
off += r;
}
fclose(f);
out[off] = '\0';
return out;
}
FILE *f = popen("PATH=/bin:/sbin:/usr/bin:/usr/sbin dmesg 2>/dev/null", "r");
if (!f) return NULL;
char *out = malloc(CRASH_DMESG_MAX);
if (!out) { pclose(f); return NULL; }
int off = 0;
while (off < (int)CRASH_DMESG_MAX - 1) {
int r = (int)fread(out + off, 1, CRASH_DMESG_MAX - 1 - off, f);
if (r <= 0) break;
off += r;
}
pclose(f);
out[off] = '\0';
return out;
}
/* 行内是否含 "segfault"/"oops"(大小写不敏感)。 */
static int is_crash_line(const char *s)
{
static const char *keys[] = {"segfault", "oops"};
size_t len = strlen(s);
for (int k = 0; k < 2; k++) {
size_t kl = strlen(keys[k]);
for (size_t i = 0; i + kl <= len; i++) {
size_t j = 0;
int m = 1;
for (; j < kl; j++) {
char c = s[i + j];
if (c >= 'A' && c <= 'Z') c += (char)('a' - 'A');
if (c != keys[k][j]) { m = 0; break; }
}
if (m) return 1;
}
}
return 0;
}
/* 去重环:行已在环内则返回 0(不重复落盘),否则入环返回 1。 */
static int seen_push(const char *line)
{
for (int i = 0; i < CRASH_SEEN_MAX; i++)
if (g_seen[i][0] && strcmp(g_seen[i], line) == 0) return 0;
snprintf(g_seen[g_seen_idx], CRASH_LINE_MAX, "%s", line);
g_seen_idx = (g_seen_idx + 1) % CRASH_SEEN_MAX;
return 1;
}
/* 扫描 dmesg 尾部新增:记录上次读取偏移(g_dmesg_prev_len),正常追加只扫新增段;
* 环回/替换则全量扫并靠去重环防重复。 */
static void scan_dmesg(void)
{
char *cur = read_dmesg();
if (!cur) return;
const char *start = cur;
if (g_dmesg_prev_len > 0) {
size_t curlen = strlen(cur);
if (curlen >= g_dmesg_prev_len &&
memcmp(cur, g_dmesg_prev, g_dmesg_prev_len) == 0) {
start = cur + g_dmesg_prev_len; /* 正常追加:只扫新增 */
} else {
start = cur; /* 环回/替换:全量扫 + 去重 */
}
}
const char *p = start;
while (*p) {
const char *nl = strchr(p, '\n');
size_t linelen = nl ? (size_t)(nl - p) : strlen(p);
if (linelen > 0) {
char tmp[CRASH_LINE_MAX];
size_t cl = linelen < CRASH_LINE_MAX - 1 ? linelen : CRASH_LINE_MAX - 1;
memcpy(tmp, p, cl);
tmp[cl] = '\0';
while (cl > 0 && tmp[cl - 1] == '\r') tmp[--cl] = '\0'; /* 去尾 \r */
if (is_crash_line(tmp)) {
if (seen_push(tmp)) emit_event("dmesg", tmp);
}
}
if (!nl) break;
p = nl + 1;
}
size_t curlen = strlen(cur);
size_t keep = curlen < CRASH_DMESG_MAX - 1 ? curlen : CRASH_DMESG_MAX - 1;
memcpy(g_dmesg_prev, cur, keep);
g_dmesg_prev[keep] = '\0';
g_dmesg_prev_len = keep;
free(cur);
}
/* ---- 信号 / socket ---- */
static void sig_handler(int sig)
{
g_shutdown = 1;
char c = CRASH_SIGTERM_CHAR;
ssize_t w = write(g_sigpipe[1], &c, 1);
(void)w;
(void)sig;
}
static int setup_signals(void)
{
if (pipe(g_sigpipe) != 0) return -1;
int rf = fcntl(g_sigpipe[0], F_GETFL, 0);
if (rf >= 0) fcntl(g_sigpipe[0], F_SETFL, rf | O_NONBLOCK);
int wf = fcntl(g_sigpipe[1], F_GETFL, 0);
if (wf >= 0) fcntl(g_sigpipe[1], F_SETFL, wf | O_NONBLOCK);
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = sig_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
signal(SIGPIPE, SIG_IGN);
return 0;
}
static int setup_socket(void)
{
unlink(g_sock);
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) { fprintf(stderr, "[meow_crash] socket: %s\n", strerror(errno)); return -1; }
struct sockaddr_un a;
memset(&a, 0, sizeof a);
a.sun_family = AF_UNIX;
safe_strcpy(a.sun_path, sizeof a.sun_path, g_sock);
if (bind(fd, (struct sockaddr *)&a, sizeof a) != 0) {
fprintf(stderr, "[meow_crash] bind %s: %s\n", g_sock, strerror(errno));
close(fd);
return -1;
}
(void)chmod(g_sock, 0666);
if (listen(fd, 8) != 0) {
fprintf(stderr, "[meow_crash] listen: %s\n", strerror(errno));
close(fd);
return -1;
}
g_listen_fd = fd;
return 0;
}
static int run_daemon(void)
{
if (setup_signals() != 0) return 1;
if (setup_socket() != 0) return 1;
boot_ts(g_bootts, sizeof g_bootts);
log_open_card(); /* 有卡即写卡;否则落 /tmp */
log_cleanup_old();
fprintf(stderr, "[meow_crash] up: card=%s sock=%s bootts=%s log=%s\n",
g_card, g_sock, g_bootts, g_on_card ? "card" : CRASH_TMP_LOG);
struct pollfd pfds[2];
nfds_t nfds = 2;
unsigned long long last_scan = 0;
unsigned long long last_cleanup = 0;
while (!g_shutdown) {
pfds[0].fd = g_listen_fd; pfds[0].events = POLLIN; pfds[0].revents = 0;
pfds[1].fd = g_sigpipe[0]; pfds[1].events = POLLIN; pfds[1].revents = 0;
int r = poll(pfds, nfds, (int)CRASH_TICK_MAX_MS);
if (r < 0) {
if (errno == EINTR) continue;
fprintf(stderr, "[meow_crash] poll: %s\n", strerror(errno));
break;
}
if (g_shutdown) break;
if (pfds[0].revents & POLLIN) {
int cfd = accept(g_listen_fd, NULL, NULL);
if (cfd >= 0) { handle_conn(cfd); close(cfd); }
}
if (pfds[1].revents & POLLIN) {
char tmp[64];
while (read(g_sigpipe[0], tmp, sizeof tmp) > 0) { /* drain */ }
}
if (now_ms() - last_scan >= (unsigned long long)CRASH_SCAN_MS) {
last_scan = now_ms();
poll_card(); /* 卡出现 → 迁移 /tmp 缓冲 */
scan_dmesg(); /* 增量捕获 dmesg 新增 oops/segfault */
}
if (now_ms() - last_cleanup >= (unsigned long long)CRASH_CLEANUP_MS) {
last_cleanup = now_ms();
log_cleanup_old(); /* 总量并入 logs/ 1GB 上限 */
}
}
if (g_listen_fd >= 0) close(g_listen_fd);
unlink(g_sock);
if (g_log_fd >= 0) close(g_log_fd);
fprintf(stderr, "[meow_crash] shutdown\n");
return 0;
}
/* ---- CLI:dump / tail(读日志文件,免鉴权,不依赖守护在线) ---- */
/* 定位本 boot 崩溃日志文件(卡优先,无卡/不存在回退 /tmp)。返回 0=找到可读文件。 */
static int find_log_path(char *out, size_t n)
{
if (card_available()) {
char ts[CRASH_BOOTTS_LEN];
boot_ts(ts, sizeof ts);
if (snprintf(out, n, "%s/%s/crash_%s.log", g_card, CRASH_LOGS_REL, ts) < (int)n
&& access(out, R_OK) == 0) return 0;
}
if (access(CRASH_TMP_LOG, R_OK) == 0) { snprintf(out, n, "%s", CRASH_TMP_LOG); return 0; }
return -1;
}
/* 解析行首三字段:epoch / uptime / source,返回剩余 detail 指针。原地改写入参(写 NUL)。 */
static int parse_parts(char *line, time_t *epoch, long *up, char *src, size_t srcn, char **rest)
{
char *p = line;
char *sp = strchr(p, ' ');
if (!sp) return -1;
*sp = '\0';
*epoch = (time_t)atol(p);
p = sp + 1;
sp = strchr(p, ' ');
if (!sp) return -1;
*sp = '\0';
*up = atol(p);
p = sp + 1;
sp = strchr(p, ' ');
if (!sp) return -1;
*sp = '\0';
snprintf(src, srcn, "%s", p);
p = sp + 1;
*rest = p;
return 0;
}
/* 打印时把 epoch 转可读时间戳。 */
static void print_reformatted(char *line)
{
time_t epoch;
long up;
char src[16], *rest;
if (parse_parts(line, &epoch, &up, src, sizeof src, &rest) == 0) {
struct tm tm;
char tbuf[32];
if (localtime_r(&epoch, &tm)) strftime(tbuf, sizeof tbuf, "%Y-%m-%d %H:%M:%S", &tm);
else snprintf(tbuf, sizeof tbuf, "%lld", (long long)epoch);
printf("%s up=%lds [%s] %s", tbuf, up, src, rest);
} else {
fputs(line, stdout);
}
}
static int cmd_dump(void)
{
char path[CRASH_PATH_MAX];
if (find_log_path(path, sizeof path) != 0) { printf("(no crash events this boot)\n"); return 0; }
printf("# crash log: %s\n", path);
FILE *f = fopen(path, "r");
if (!f) { printf("(cannot open %s)\n", path); return 0; }
char line[CRASH_LINE_MAX];
int cnt = 0;
while (fgets(line, sizeof line, f)) { print_reformatted(line); cnt++; }
fclose(f);
if (cnt == 0) printf("(empty)\n");
return 0;
}
static int cmd_tail(int n)
{
char path[CRASH_PATH_MAX];
if (find_log_path(path, sizeof path) != 0) { printf("(no crash events this boot)\n"); return 0; }
FILE *f = fopen(path, "r");
if (!f) { printf("(cannot open %s)\n", path); return 0; }
if (n <= 0) n = 20;
if (n > 100) n = 100;
enum { CAP = 100 };
char ring[CAP][CRASH_LINE_MAX];
int cnt = 0;
char line[CRASH_LINE_MAX];
while (fgets(line, sizeof line, f)) {
snprintf(ring[cnt % CAP], CRASH_LINE_MAX, "%s", line);
cnt++;
}
fclose(f);
int avail = cnt < CAP ? cnt : CAP; /* 环内实际存的条数 */
int shown = cnt < CAP ? cnt : n;
int startat;
if (cnt < CAP) startat = 0;
else startat = cnt - n;
if (startat < 0) startat = 0;
if (shown > avail) shown = avail;
char buf[CRASH_LINE_MAX];
for (int i = 0; i < shown; i++) {
snprintf(buf, sizeof buf, "%s", ring[(startat + i) % CAP]);
print_reformatted(buf);
}
return 0;
}
static void usage(const char *a0)
{
fprintf(stderr,
"usage: %s [options] [command]\n"
" commands: dump | tail [N] (root 免鉴权;无命令=启动守护进程)\n"
" options:\n"
" --card <path> TF 卡挂载点 (default %s)\n"
" --sock <path> 崩溃上报 socket (default %s)\n"
" -h, --help\n",
a0, CRASH_CARD_DEFAULT, CRASH_SOCK_DEFAULT);
}
int main(int argc, char **argv)
{
const char *cmd_tok[8];
int cmd_n = 0;
for (int i = 1; i < argc; i++) {
const char *a = argv[i];
if (strcmp(a, "--card") == 0 && i + 1 < argc) safe_strcpy(g_card, sizeof g_card, argv[++i]);
else if (strcmp(a, "--sock") == 0 && i + 1 < argc) safe_strcpy(g_sock, sizeof g_sock, argv[++i]);
else if (strcmp(a, "-h") == 0 || strcmp(a, "--help") == 0) { usage(argv[0]); return 0; }
else if (a[0] == '-') { usage(argv[0]); return 2; }
else if (cmd_n < 8) cmd_tok[cmd_n++] = a;
}
const char *e;
if ((e = getenv("MEOW_CRASH_CARD")) && e[0]) safe_strcpy(g_card, sizeof g_card, e);
if ((e = getenv("MEOW_CRASH_SOCK")) && e[0]) safe_strcpy(g_sock, sizeof g_sock, e);
setvbuf(stdout, NULL, _IONBF, 0);
if (cmd_n == 0) return run_daemon(); /* svcd 空参拉起 → 前台常驻 */
if (strcmp(cmd_tok[0], "dump") == 0) return cmd_dump();
if (strcmp(cmd_tok[0], "tail") == 0) {
int n = 20;
if (cmd_n > 1) n = atoi(cmd_tok[1]);
return cmd_tail(n);
}
usage(argv[0]);
return 2;
}
+16
View File
@@ -0,0 +1,16 @@
# meow_crash.conf — meow_crash 板载服务配置(services.d,trigger=boot)
# 部署目标:/usr/data/meow/services.d/meow_crash.conf(Data 区,首启预置)
#
# 字段语义与 meow_svcd 一致:
# exec/args/enabled/restart/backoff_ms/max_restart/trigger/cap
# - trigger=boot:板载服务恒存在即启动(拔卡保留)。
# - restart=on-fail:崩溃后退避重启(默认 backoff 2000ms,最多 5 次)。
# - caps:本机平台未定,保留为空。
exec=/usr/bin/meow_crash
args=
enabled=1
restart=on-fail
backoff_ms=2000
max_restart=5
trigger=boot
cap=
+81
View File
@@ -0,0 +1,81 @@
# meow_crash — MeowOS 系统级崩溃捕获(二期组件)
> 状态:设计落盘 + 实现(agent 任务)。`meow_crash` 是 svcd 托管的小守护,负责把
> svcd 服务的异常退出与内核 oops/应用崩溃记录到日志,供事后排查。
> 定位见 FRAMEWORK_DESIGN §10(Supervisor 树 / 崩溃隔离)、APPS.md §4.5(统一日志系统)、
> devdocs/plan/SYS_BASELINE.md(svcd 子进程化,监护挂点)、devdocs/plan/APP_LOG_MANAGER.md。
## 1. 角色
`meow_crash` = 崩溃事件**记录器**(只读一条上报消息落盘,无共享内存、无 dlopen),
由 svcd 以 `fork+exec+setpgid` 托管(`/usr/bin/meow_crash` + `services.d/meow_crash.conf`
trigger=boot)。svcd 托管服务均为前台常驻(svcd waitpid),故 meow_crash
**不 daemonize**,svcd 崩则它是子进程被 waitpid 感知;它崩则 svcd 按 `on-fail` 重启它,
且其崩溃不影响 svcd(崩溃隔离)。
## 2. 捕获源
### ① svcd 服务异常退出(主源)
svcd 在 `reap_children()` 检测到**异常退出**(非 0 退出码 / 被信号杀,且非受控
stop/restart)时,经 AF_UNIX `/tmp/meow_crash.sock` 发**单行**文本(best-effort):
```
SVC <service_name> <exit|sig> <num> <epoch_sec> <uptime_sec>
```
- 全程非阻塞:客户端 socket 置 O_NONBLOCK;connect 失败即放弃(可短 poll POLLOUT
≤150ms 用于本地 listen backlog 排空,绝不重试)。**svcd 绝不被此带崩。**
- meow_crash 不在线 → 该条丢弃(符合「若 crash 不在线则跳过」)。
### ② 内核 oops / 应用崩溃(辅助,v1 不强求)
周期性(1s)`dmesg` 抓取,记录上次读取偏移(`g_dmesg_prev_len`),只扫**新增**段,
捕获含 `segfault`/`oops`(大小写不敏感)的行。环回/替换时全量扫并靠去重环防重复。
v1 不扫描 `/tmp` core 转储(不强求,留作后续)。
## 3. 落盘
- 有卡:`<card>/MeowOS/logs/crash_<bootts>.log` 追加写。`<bootts>` 复用
`/tmp/meow_bootts`(launcher 缓存的 boot 时间戳,`meow_` 前缀剥掉),同一 boot
会话跨进程共用;读不到则按 墙钟-uptime 计算并缓存。
- 无卡/写失败:`/tmp/meow_crash.log`(tmpfs,256KB 滚动,超出即重建)。
- 卡出现:`poll_card()` 把 /tmp 缓冲整段搬入卡日志后删除(对齐 launcher
`log_poll_card` 语义)。
- 总量:`log_cleanup_old()` 统计 logs/ 下 `meow_*` 与 `crash_*`,超过 1GB 按 mtime
删最旧(与 launcher 的 LOG_SIZE_LIMIT 同纪律,launcher 只管 meow_*,本守护并管)。
一行一条格式:
```
<epoch_sec> <uptime_sec> <source> <detail...>
```
- `source`:`svc`(来自 svcd)或 `dmesg`(内核行)。
- `detail`:`<name> exit=<code>` / `<name> signal=<sig>` / 内核行原文。
## 4. CLI(root 免鉴权,读文件,不依赖守护在线)
| 命令 | 说明 |
|---|---|
| `meow_crash dump` | 打印本 boot 崩溃清单(卡 `crash_<bootts>.log`;无卡/无文件回退 /tmp) |
| `meow_crash tail [N]` | 打印最近 N 条(默认 20,上限 100) |
| `meow_crash`(无命令) | svcd 拉起 → 前台常驻守护 |
`dump`/`tail` 把 epoch 转可读 `YYYY-MM-DD HH:MM:SS` 展示。
## 5. 测试钩子(宿主集成测试)
| 钩子 | 作用 |
|---|---|
| `--card <path>` / `MEOW_CRASH_CARD` | 覆写卡挂载点(宿主指向临时目录即走「有卡」路径) |
| `--sock <path>` / `MEOW_CRASH_SOCK` | 覆写上报 socket |
| `MEOW_CRASH_NOCARD=1` | 强制无卡(验证 /tmp 回退 + 后续迁移) |
| `MEOW_CRASH_DMESG_FILE=<path>` | 从文件读「假 dmesg」(宿主注入内核行) |
## 6. 边界 / 诚实标注
- **v1 不扫描 core 转储**(任务:「不强求」)。当前只做 svcd 上报 + dmesg 增量。
- **svcd 上报的 skip 语义**:受控 stop(stop_requested)与 restart 命令
(restart_pending)的退出**不**上报,只记真实异常退出(避免把管理员停服务当崩溃)。
- **dmesg 捕获**依赖设备 `dmesg` 可用(busybox;root)。宿主用 `MEOW_CRASH_DMESG_FILE`
注入。首扫会捕获本 boot 已存在的 crash 行(内核环在重启时清空,不会跨 boot 混入)。
- **固件打包需知 / 板载 conf 预置**:本任务按约束未改 `S89meow_svcd` 与固件脚本。
需在**打包/首启预置**阶段把 `patch_v2/meow_crash/meow_crash.conf` 落到
`/usr/data/meow/services.d/meow_crash.conf`(参照 S89 对 mdnsd/perf/power 的预置方式)。
Binary file not shown.