Files
MeowOS-For-R3P2/patch_v2/sdk/meow_ui_log.c
T

305 lines
12 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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);
}
/* 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;
}