diff --git a/patch_v2/meow_crash/build_crash.sh b/patch_v2/meow_crash/build_crash.sh new file mode 100755 index 0000000..548d088 --- /dev/null +++ b/patch_v2/meow_crash/build_crash.sh @@ -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 diff --git a/patch_v2/meow_crash/meow_crash b/patch_v2/meow_crash/meow_crash new file mode 100755 index 0000000..ead5c0f Binary files /dev/null and b/patch_v2/meow_crash/meow_crash differ diff --git a/patch_v2/meow_crash/meow_crash.c b/patch_v2/meow_crash/meow_crash.c new file mode 100644 index 0000000..a17e589 --- /dev/null +++ b/patch_v2/meow_crash/meow_crash.c @@ -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_.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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ---- 默认路径 / 常量 ---- */ +#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_.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 */ +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 TF 卡挂载点 (default %s)\n" + " --sock 崩溃上报 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; +} diff --git a/patch_v2/meow_crash/meow_crash.conf b/patch_v2/meow_crash/meow_crash.conf new file mode 100644 index 0000000..0cf0f94 --- /dev/null +++ b/patch_v2/meow_crash/meow_crash.conf @@ -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= diff --git a/patch_v2/meow_crash/meow_crash.md b/patch_v2/meow_crash/meow_crash.md new file mode 100644 index 0000000..a6109e9 --- /dev/null +++ b/patch_v2/meow_crash/meow_crash.md @@ -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 +``` + +- 全程非阻塞:客户端 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. 落盘 + +- 有卡:`/MeowOS/logs/crash_.log` 追加写。`` 复用 + `/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_*,本守护并管)。 + +一行一条格式: +``` + +``` +- `source`:`svc`(来自 svcd)或 `dmesg`(内核行)。 +- `detail`:` exit=` / ` signal=` / 内核行原文。 + +## 4. CLI(root 免鉴权,读文件,不依赖守护在线) + +| 命令 | 说明 | +|---|---| +| `meow_crash dump` | 打印本 boot 崩溃清单(卡 `crash_.log`;无卡/无文件回退 /tmp) | +| `meow_crash tail [N]` | 打印最近 N 条(默认 20,上限 100) | +| `meow_crash`(无命令) | svcd 拉起 → 前台常驻守护 | + +`dump`/`tail` 把 epoch 转可读 `YYYY-MM-DD HH:MM:SS` 展示。 + +## 5. 测试钩子(宿主集成测试) + +| 钩子 | 作用 | +|---|---| +| `--card ` / `MEOW_CRASH_CARD` | 覆写卡挂载点(宿主指向临时目录即走「有卡」路径) | +| `--sock ` / `MEOW_CRASH_SOCK` | 覆写上报 socket | +| `MEOW_CRASH_NOCARD=1` | 强制无卡(验证 /tmp 回退 + 后续迁移) | +| `MEOW_CRASH_DMESG_FILE=` | 从文件读「假 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 的预置方式)。 diff --git a/patch_v2/meow_crash/meow_crash_host b/patch_v2/meow_crash/meow_crash_host new file mode 100755 index 0000000..fa468d8 Binary files /dev/null and b/patch_v2/meow_crash/meow_crash_host differ diff --git a/patch_v2/meow_svcd/S89meow_svcd b/patch_v2/meow_svcd/S89meow_svcd index 1721f16..7f4a4b3 100755 --- a/patch_v2/meow_svcd/S89meow_svcd +++ b/patch_v2/meow_svcd/S89meow_svcd @@ -94,6 +94,22 @@ EOF chmod 644 /usr/data/meow/services.d/meow_svc_power.conf echo -n "provisioned power.conf " fi + if [ -f /usr/data/meow/services.d/meow_crash.conf ]; then + echo -n "crash.conf exists " + else + cat > /usr/data/meow/services.d/meow_crash.conf <<'EOF' +exec=/usr/bin/meow_crash +args= +enabled=1 +restart=on-fail +backoff_ms=2000 +max_restart=5 +trigger=boot +cap= +EOF + chmod 644 /usr/data/meow/services.d/meow_crash.conf + echo -n "provisioned crash.conf " + fi else echo -n "skip svc confs (/usr/data/meow not writable) " fi diff --git a/patch_v2/meow_svcd/meow_svcd b/patch_v2/meow_svcd/meow_svcd index 1d2ddf5..a86acda 100755 Binary files a/patch_v2/meow_svcd/meow_svcd and b/patch_v2/meow_svcd/meow_svcd differ diff --git a/patch_v2/meow_svcd/meow_svcd.c b/patch_v2/meow_svcd/meow_svcd.c index 8eb18fb..4474504 100644 --- a/patch_v2/meow_svcd/meow_svcd.c +++ b/patch_v2/meow_svcd/meow_svcd.c @@ -97,6 +97,8 @@ #define SVCD_SOCK_PATH_MAX 108 #define SVCD_SIGCHLD_CHAR 'C' /* 自管道字节(拉起 poll) */ #define SVCD_SIGTERM_CHAR 'T' +#define SVCD_CRASH_SOCK "/tmp/meow_crash.sock" /* meow_crash 崩溃上报 socket(best-effort) */ +#define SVCD_CRASH_POLL_MS 150 /* 上报 connect 的排空等待上限(绝不重试) */ /* ---- ANSI 终端配色(status 回显用;仅客户端 stdout 为 TTY 时保留,非 TTY 去色) ---- */ #define AN_RESET "\033[0m" @@ -532,6 +534,65 @@ static void sig_term(int sig) (void)w; } +/* /proc/uptime 首字段 → 秒(整数)。失败返回 0。 */ +static long svcd_uptime(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); +} + +/* 服务异常退出 → 上报 meow_crash(best-effort,绝不阻塞/重试 svcd)。 + * + * 铁律:服务崩不崩 svcd。上报全程非阻塞: + * - 客户端 socket 置 O_NONBLOCK; + * - connect 失败即放弃;若 EINPROGRESS/EWOULDBLOCK,短 poll(POLLOUT) ≤150ms 排空 + * 本地 listen backlog,仍不可写即丢弃; + * - 单行小消息一次 write(失败即弃),绝不重试。 + * meow_crash 不在线(connect ECONNREFUSED 等)→ 静默丢,不影响 svcd 任何状态。 + */ +static void notify_crash(const char *name, int status) +{ + const char *kind; + int num; + if (WIFSIGNALED(status)) { kind = "sig"; num = WTERMSIG(status); } + else { kind = "exit"; num = WIFEXITED(status) ? WEXITSTATUS(status) : 1; } + + char msg[256]; + snprintf(msg, sizeof msg, "SVC %s %s %d %ld %ld\n", + name, kind, num, (long)time(NULL), svcd_uptime()); + + int fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (fd < 0) return; + int fl = fcntl(fd, F_GETFL, 0); + if (fl >= 0) fcntl(fd, F_SETFL, fl | O_NONBLOCK); + + struct sockaddr_un a; + memset(&a, 0, sizeof a); + a.sun_family = AF_UNIX; + safe_strcpy(a.sun_path, sizeof a.sun_path, SVCD_CRASH_SOCK); + + if (connect(fd, (struct sockaddr *)&a, sizeof a) != 0) { + if (errno == EINPROGRESS || errno == EWOULDBLOCK) { + struct pollfd pf; + pf.fd = fd; pf.events = POLLOUT; pf.revents = 0; + int r = poll(&pf, 1, SVCD_CRASH_POLL_MS); + if (r <= 0 || !(pf.revents & POLLOUT)) { close(fd); return; } + } else { + close(fd); + return; + } + } + ssize_t w = write(fd, msg, strlen(msg)); + (void)w; + close(fd); +} + /* 排空 SIGCHLD 自管道,waitpid 匹配所有已退出子进程。 */ static void reap_children(void) { @@ -545,7 +606,13 @@ static void reap_children(void) for (int i = 0; i < g_nsvc; i++) if (g_svcs[i].pid == pid) { idx = i; break; } if (idx < 0) { svcd_log("reap unknown pid=%d", (int)pid); continue; } - apply_exit(&g_svcs[idx], status); + svc_t *s = &g_svcs[idx]; + /* 服务异常退出(非 0 / 被信号杀)且非受控 stop/restart → 上报 meow_crash。 + * 必须在 apply_exit 前判(apply_exit 会清 stop_requested/restart_pending)。 */ + int failed = !(WIFEXITED(status) && WEXITSTATUS(status) == 0); + if (failed && !s->stop_requested && !s->restart_pending) + notify_crash(s->name, status); + apply_exit(s, status); } build_status_file(); } diff --git a/patch_v2/meow_svcd/meow_svcd_host b/patch_v2/meow_svcd/meow_svcd_host index 201815e..65fa79f 100755 Binary files a/patch_v2/meow_svcd/meow_svcd_host and b/patch_v2/meow_svcd/meow_svcd_host differ