• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC) • 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库) • 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo 增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
280 lines
11 KiB
C
280 lines
11 KiB
C
/*
|
||
* 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(日志目录)、MEOW_LOG_APP(app_id,无则 "app")、
|
||
* MEOW_LOG_LEVEL(本 app 级别,无则默认 INFO)→ 写
|
||
* `<log_dir>/apps/<app_id>/logs/<启动时间>_<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 <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>/apps/<app_id>/logs/<ts>_<pid>[._<n>].log`。
|
||
* g_log_base 存不含序号后缀的路径前缀 `<dir>/apps/<app>/logs/<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; /* 累计写入次数(周期总量清理节流) */
|
||
|
||
/* 由 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>/apps/<app_id>/logs/<ts>_<pid>`。
|
||
* 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/apps/%s/logs/%s_%d",
|
||
g_log_dir, g_app_id, 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;
|
||
}
|
||
}
|
||
|
||
/* 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>/apps/<app_id>/logs` 增量 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/apps/%s/logs", g_log_dir, g_app_id) >= (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>/apps/<app_id>/logs/` 下所有 .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/apps/%s/logs", g_log_dir, g_app_id)
|
||
>= (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;
|
||
}
|