/* * meow_ui_log.c — MeowOS 应用统一日志 API(M2.52 版)。 * * 背景:此前 app 各自造轮子(如 hello.c hellolog() 自读 MEOW_LOG_DIR + snprintf + * fopen/fprintf),重复、易错、无分离/级别。本文件实现 SDK 统一 meow_log(): * * - app 一行调用 meow_log(MEOW_LOG_INFO, "start")(见 meow_ui.h); * - SDK 启动时读 env:MEOW_LOG_DIR(日志目录,launcher 已注入到 logs/,含 app 分层)、 * MEOW_LOG_APP(app_id,无则 "app")、MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写 * `/<启动时间>_[._].log`(目录自动 mkdir -p), * 按 app 分目录 + 每次启动新文件(不同启动=不同文件,同一次启动内 O_APPEND 追加), * 级别过滤(lvl < level 丢弃); * - 格式:` [] message\n`(meow_ui_time_ms() 取毫秒); * - 无 log_dir(无卡)/目录不可建/写失败 → 降级 stderr,不阻塞 app 不崩。 * * 上限(用户定,防撑爆 TF 卡): * - 单文件 ≥ MEOW_LOG_FILE_MAX(2MB)→ 关当前 fd,开新文件并追加序号 `_`; * - 每 App 日志总量 ≥ MEOW_LOG_TOTAL_MAX(50MB)→ 按期删最旧文件(按 mtime), * 节流(每 MEOW_LOG_CLEANUP_EVERY 次写入检查一次,另在首次打开/每次轮转时检查), * 避免高频日志时逐行 readdir/stat。 * * 约定:与 meow_ui_gfx.c 同风格(M2.x 中文注释)。线程安全(pthread_mutex,launcher * 当前台/后台进程都会在已链 -lpthread 的应用里调用)。 */ #include "meow_ui.h" #include #include #include #include #include #include #include #include #include #include #include #include #include /* 单行消息上限(超长截断) */ #define MEOW_LOG_LINE_MAX 1024 /* 日志上限:单文件 2MB 开新文件;每 App 总量 50MB 删最旧 */ #define MEOW_LOG_FILE_MAX (2 * 1024 * 1024) #define MEOW_LOG_TOTAL_MAX (50 * 1024 * 1024) #define MEOW_LOG_CLEANUP_MAX 128 /* 一次清理最多扫描文件数 */ #define MEOW_LOG_CLEANUP_EVERY 2048 /* 每 N 次写入做一次总量清理(节流) */ /* 级别字符串表(与 meow_log_level 序号对齐) */ static const char *g_lvl_str[] = { "DEBUG", "INFO", "WARN", "ERROR" }; /* 内部状态(各字段约定见头注释) */ static pthread_mutex_t g_log_mu = PTHREAD_MUTEX_INITIALIZER; static pthread_once_t g_log_once = PTHREAD_ONCE_INIT; static char g_app_id[64] = "app"; /* env MEOW_LOG_APP,无则 "app" */ static char g_log_dir[256] = ""; /* env MEOW_LOG_DIR,无/空则降级 stdio */ static meow_log_level g_level = MEOW_LOG_INFO; /* 默认 INFO */ /* M2.52:日志文件为 `/_[._].log`;M2.x 简化后 MEOW_LOG_DIR 已含 app 分层 * (launcher 注入 logs/)。g_log_base 存不含序号后缀的路径前缀 `/_`; * g_log_seq 为轮转序号(0=首个文件无后缀;>0 追加 `_`)。两者均在 log_init() * (pthread_once 那次)里生成一次。 */ static char g_log_base[384] = ""; static char g_log_file[512] = ""; static int g_log_seq = 0; static int g_log_fd = -1; /* 当前日志 fd;-1=未开 */ static int g_log_fallback = 0; /* 1=无卡/写失败,降级 stderr */ static int g_log_open_tried = 0; /* 是否已尝试开首个文件(只试一次,失败则降级) */ static unsigned long g_write_count = 0; /* 累计写入次数(周期总量清理节流) */ static void log_open(void); /* 前向声明(log_sig_exit 在其定义之前使用) */ /* 由 g_log_base + g_log_seq 重建当前文件路径(seq=0 无后缀,>0 追加 _)。 */ static void log_rebuild_file(void) { if (g_log_seq <= 0) snprintf(g_log_file, sizeof g_log_file, "%s.log", g_log_base); else snprintf(g_log_file, sizeof g_log_file, "%s_%d.log", g_log_base, g_log_seq); } /* 一次性初始化:读 env 决定 app_id/log_dir/level;构建 g_log_base 与首个 g_log_file。 * (pthread_once 保证仅一次。) */ static void log_init(void) { const char *s; /* app_id:env MEOW_LOG_APP,无则 "app" */ s = getenv("MEOW_LOG_APP"); if (s && s[0]) snprintf(g_app_id, sizeof g_app_id, "%s", s); /* log_dir:env MEOW_LOG_DIR,无/空则 ""(降级 stdio) */ s = getenv("MEOW_LOG_DIR"); if (s && s[0]) snprintf(g_log_dir, sizeof g_log_dir, "%s", s); /* M2.52:构建完整前缀 `/_`(M2.x 简化:MEOW_LOG_DIR 已含 app 分层)。 * ts 用 localtime 的 %Y%m%d_%H%M%S(启动时刻);尾拼 _ 防同秒重复启动撞名, * 从而每次启动=新文件,同一次启动内 O_APPEND 追加。若 log_dir 为空(无卡) * 则跳过(log_open 降级 stderr)。 */ if (g_log_dir[0]) { char ts[32]; time_t now = time(NULL); struct tm tmnow; localtime_r(&now, &tmnow); strftime(ts, sizeof ts, "%Y%m%d_%H%M%S", &tmnow); snprintf(g_log_base, sizeof g_log_base, "%s/%s_%d", g_log_dir, ts, (int)getpid()); log_rebuild_file(); } /* level:env MEOW_LOG_LEVEL 可选覆盖(系统下发;默认 INFO) */ s = getenv("MEOW_LOG_LEVEL"); if (s && s[0]) { if (strcmp(s, "DEBUG") == 0) g_level = MEOW_LOG_DEBUG; else if (strcmp(s, "WARN") == 0) g_level = MEOW_LOG_WARN; else if (strcmp(s, "ERROR") == 0) g_level = MEOW_LOG_ERROR; else g_level = MEOW_LOG_INFO; } } /* 退出日志兜底(M2.78):App 被 watchdog 返回键 SIGTERM 时走默认终止,跳过正常 * 路径的 meow_log("exit"),日志只剩 start。SDK 在 meow_ui_init 安装本处理器: * SIGTERM/SIGINT → 直接 write 一条 exit:(绕过 stdio 缓冲)→ exit(0)(干净码)。 * App 自装处理器(如 hello 画 BYE)会覆盖本处理器,其 exit(0) 路径不受影响。 */ static void log_sig_exit(int sig) { if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); } if (g_log_fd >= 0) { char line[128]; int n = snprintf(line, sizeof line, "%llu [%s] INFO exit:%s\n", (unsigned long long)meow_ui_time_ms(), g_app_id, sig == SIGTERM ? "sigterm" : "sigint"); if (n > 0) { ssize_t w = write(g_log_fd, line, (size_t)n); (void)w; } } exit(0); } void meow_log_install_exit_handler(void) { signal(SIGTERM, log_sig_exit); signal(SIGINT, log_sig_exit); } /* M2.87:SIGUSR1 无害护栏(防线冗余)。watchdog 对「左缘起点+右滑」发 SIGUSR1 当返回事件; * SDK 框架 App(framework=1)自己经 on_swipe 处理左缘返回(meow_ui_input.c),不应被 SIGUSR1 * 终止。但看门狗对 framework 判定有误/竞态/测试注入等**任何**路径漏进 SIGUSR1,默认动作都会 * 杀死 App(ncm signal=16 崩溃根因之一)。本节安装一个不做任何致命动作的处理器:记日志+忽略 * ——框架 App 的返回/退出语义由 SDK on_swipe 统一决策,SIGUSR1 仅作「绝不杀 App」的保活护栏, * 不重复触发返回(避免与 on_swipe 双重返回)。App 若自装 SIGUSR1 处理器(hello/ncm 均未装, * 裸沉浸 App 自绘处理)会覆盖本处理器;`--sim-swipe` 测试路径也随之被吞。 */ static void sigusr1_guard(int sig) { (void)sig; if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); } char line[128]; int n = snprintf(line, sizeof line, "%llu [%s] WARN sigusr1: ignored (guard)\n", (unsigned long long)meow_ui_time_ms(), g_app_id); if (n > 0) { if (g_log_fd >= 0) { ssize_t w = write(g_log_fd, line, (size_t)n); (void)w; } else { ssize_t w = write(2, line, (size_t)n); (void)w; } /* 无日志目录:写 stderr,护栏可观测 */ } /* 不 exit、不返回处理:返回交给 on_swipe/SDK。这里仅记录并吞掉。 */ } void meow_ui_install_sigusr1_guard(void) { struct sigaction sa; memset(&sa, 0, sizeof sa); sa.sa_handler = sigusr1_guard; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; /* 不设 SA_RESTART:被中断的 poll/read 返回按 EINTR 处理后继续 */ sigaction(SIGUSR1, &sa, NULL); } /* mkdir -p:逐分量 mkdir(已存在 EEXIST 视为成功)。返回 0=成功,-1=失败。 */ static int log_mkdir_p(const char *path) { char tmp[256]; size_t len, i; if (!path || !path[0] || strlen(path) >= sizeof tmp) return -1; len = strlen(path); memcpy(tmp, path, len + 1); if (tmp[len - 1] == '/') tmp[len - 1] = '\0'; /* 去尾斜杠 */ for (i = 1; i <= strlen(tmp); i++) { if (tmp[i] == '/' || tmp[i] == '\0') { char ch = tmp[i]; tmp[i] = '\0'; if (tmp[0] && mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1; tmp[i] = ch; } } return 0; } /* M2.52:对 ``(M2.x 简化后已含 app 分层)增量 mkdir -p,并打开 g_log_file * (O_APPEND|O_CREAT;同一次启动内追加,不同启动=不同文件)。失败置 g_log_fallback。 * 首次由 meow_log 经 g_log_open_tried 守卫调用;轮转后由 meow_log 再次调用(目录已存在, * mkdir_p 幂等)。仅在持有 g_log_mu 时调用(g_log_base 已构建)。 */ static void log_open(void) { char logs[512]; if (!g_log_dir[0] || !g_log_file[0]) { g_log_fallback = 1; return; } if (snprintf(logs, sizeof logs, "%s", g_log_dir) >= (int)sizeof logs) { g_log_fallback = 1; return; } if (log_mkdir_p(logs) != 0) { g_log_fallback = 1; return; } g_log_fd = open(g_log_file, O_WRONLY | O_APPEND | O_CREAT, 0644); if (g_log_fd < 0) g_log_fallback = 1; } /* 清理用文件记录(按 mtime 升序删最旧) */ typedef struct { char name[384]; time_t mtime; long size; } log_spill_t; static int log_spill_cmp(const void *a, const void *b) { const log_spill_t *fa = a, *fb = b; if (fa->mtime < fb->mtime) return -1; if (fa->mtime > fb->mtime) return 1; return 0; } /* 每 App 总量清理:扫描 `/`(M2.x 简化后已含 app 分层)下所有 .log(跳过当前正写的 * g_log_file),累计大小,超过 MEOW_LOG_TOTAL_MAX 则按 mtime 从最旧删到 ≤ 阈值。 * 只规整本 App 目录(进程隔离),节流调用(见 meow_log)。仅在持有 g_log_mu 时调用。 */ static void log_cleanup_old(void) { DIR *d; struct dirent *e; char logs[512], path[384]; log_spill_t files[MEOW_LOG_CLEANUP_MAX]; int n = 0, i; long total = 0; if (!g_log_dir[0] || !g_log_file[0]) return; if (snprintf(logs, sizeof logs, "%s", g_log_dir) >= (int)sizeof logs) return; d = opendir(logs); if (!d) return; while ((e = readdir(d)) != NULL) { struct stat st; size_t nl; if (e->d_name[0] == '.') continue; nl = strlen(e->d_name); if (nl < 5 || strcmp(e->d_name + nl - 4, ".log") != 0) continue; /* 只看 .log */ if (snprintf(path, sizeof path, "%s/%s", logs, e->d_name) >= (int)sizeof path) continue; if (strcmp(path, g_log_file) == 0) continue; /* 跳过当前正写文件,避免删 unlink 后仍写 */ if (stat(path, &st) != 0) continue; if (!S_ISREG(st.st_mode)) continue; if (n >= MEOW_LOG_CLEANUP_MAX) break; snprintf(files[n].name, sizeof files[n].name, "%s", path); files[n].mtime = st.st_mtime; files[n].size = (long)st.st_size; total += (long)st.st_size; n++; } closedir(d); if (total <= MEOW_LOG_TOTAL_MAX) return; qsort(files, (size_t)n, sizeof files[0], log_spill_cmp); for (i = 0; i < n && total > MEOW_LOG_TOTAL_MAX; i++) { if (unlink(files[i].name) == 0) total -= files[i].size; } } void meow_log(meow_log_level lvl, const char *fmt, ...) { char body[MEOW_LOG_LINE_MAX]; char line[MEOW_LOG_LINE_MAX + 96]; const char *lvl_s; va_list ap; int n; pthread_once(&g_log_once, log_init); pthread_mutex_lock(&g_log_mu); if (lvl < g_level) { pthread_mutex_unlock(&g_log_mu); return; } /* 级别过滤 */ va_start(ap, fmt); vsnprintf(body, sizeof body, fmt, ap); va_end(ap); body[sizeof body - 1] = '\0'; if (!g_log_open_tried) { g_log_open_tried = 1; log_open(); } if (g_log_fd >= 0 && g_write_count == 0) log_cleanup_old(); /* 首次打开即清一次 */ /* 级别名钳位(防 app 传越界枚举值触发 OOB 读) */ lvl_s = (lvl >= 0 && lvl <= MEOW_LOG_ERROR) ? g_lvl_str[lvl] : g_lvl_str[MEOW_LOG_ERROR]; if (g_log_fd >= 0) { struct stat fst; /* 单文件超限 → 轮转开新文件(_ 递增);fstat 每次写前检查(日志低频,开销可忽略) */ if (fstat(g_log_fd, &fst) == 0 && fst.st_size >= (off_t)MEOW_LOG_FILE_MAX) { close(g_log_fd); g_log_fd = -1; g_log_seq++; log_rebuild_file(); log_open(); if (g_log_fd >= 0) log_cleanup_old(); /* 轮转后顺带清一次 */ } if (g_log_fd >= 0) { n = snprintf(line, sizeof line, "%llu [%s] %s %s\n", (unsigned long long)meow_ui_time_ms(), g_app_id, lvl_s, body); if (n > 0) { ssize_t wr = write(g_log_fd, line, (size_t)n); (void)wr; } } /* 周期总量清理(节流,避免高频日志逐行 readdir/stat) */ if (++g_write_count % MEOW_LOG_CLEANUP_EVERY == 0) log_cleanup_old(); } else { /* 降级 stderr(无卡/写失败):不阻塞 app,不崩 */ fprintf(stderr, "[%s] %s %s\n", g_app_id, lvl_s, body); } pthread_mutex_unlock(&g_log_mu); } void meow_log_set_level(meow_log_level lvl) { pthread_once(&g_log_once, log_init); pthread_mutex_lock(&g_log_mu); g_level = lvl; pthread_mutex_unlock(&g_log_mu); } meow_log_level meow_log_get_level(void) { meow_log_level l; pthread_once(&g_log_once, log_init); pthread_mutex_lock(&g_log_mu); l = g_level; pthread_mutex_unlock(&g_log_mu); return l; }