Add 'sdk/' from commit '2e7561d74f75de05dd7fb362f203f600afb451d7'

git-subtree-dir: sdk
git-subtree-mainline: 17a972d46f
git-subtree-split: 2e7561d74f
This commit is contained in:
MeowOS committed 2026-09-01 21:08:40 +08:00
commit f44e3bc6d0
85 files changed
+23811

No files matched your search

+334
View File
@@ -0,0 +1,334 @@
/*
* 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_id>,含 app 分层)、
* MEOW_LOG_APP(app_id,无则 "app")、MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写
* `<log_dir>/<启动时间>_<pid>[._<n>].log`(目录自动 mkdir -p),
* 按 app 分目录 + 每次启动新文件(不同启动=不同文件,同一次启动内 O_APPEND 追加),
* 级别过滤(lvl < level 丢弃);
* - 格式:`<monotonic_ms> [<app_id>] <LEVEL> message\n`(meow_ui_time_ms() 取毫秒);
* - 无 log_dir(无卡)/目录不可建/写失败 → 降级 stderr,不阻塞 app 不崩。
*
* 上限(用户定,防撑爆 TF 卡):
* - 单文件 ≥ MEOW_LOG_FILE_MAX(2MB)→ 关当前 fd,开新文件并追加序号 `_<n>`;
* - 每 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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <pthread.h>
#include <signal.h>
#include <time.h>
/* 单行消息上限(超长截断) */
#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_dir>/<ts>_<pid>[._<n>].log`;M2.x 简化后 MEOW_LOG_DIR 已含 app 分层
* (launcher 注入 logs/<app_id>)。g_log_base 存不含序号后缀的路径前缀 `<dir>/<ts>_<pid>`;
* g_log_seq 为轮转序号(0=首个文件无后缀;>0 追加 `_<n>`)。两者均在 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 追加 _<n>)。 */
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:构建完整前缀 `<log_dir>/<ts>_<pid>`(M2.x 简化:MEOW_LOG_DIR 已含 app 分层)。
* ts 用 localtime 的 %Y%m%d_%H%M%S(启动时刻);尾拼 _<pid> 防同秒重复启动撞名,
* 从而每次启动=新文件,同一次启动内 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:<sig>(绕过 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:对 `<log_dir>`(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 总量清理:扫描 `<log_dir>/`(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;
/* 单文件超限 → 轮转开新文件(_<n> 递增);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;
}