Files
MeowOS-SDK/sdk/meow_ui_log.c
T
AskaEth c0396e40ce sync(SDK): 增量同步主仓 M6.37–M6.39 至公开 SDK 仓
sdk/:
- 同步 meow_ui_widget/input/gfx/lazygrid/audio/media/img 等源码与头
  (SDK 默认列表/容器控件默认 MD3 — meow_list_set_container 可回退;
   全局 App 内底部上滑回桌面 — meow_ui_input_is_home_swipe 系统保留手势;
   LazyGrid/LazyColumn/LazyRow MD3 化;叠加让位自抑改 app_id 判定;
   DSD 音频格式等)
- 新增独立模块源码 meow_img_color / meow_lyric / meow_lyric_view 及对应宿主 test_*.c
- 保留公开仓自包含适配:vendored meow_font.c/meow_font.h/meow_notif.h 不动;
  build_sdk.sh 自包含版本不动;meow_ui_gfx.c 仅改 meow_font.h include 路径;
  meow_ui_internal.h 去除 FreeType 内部声明
- meow_ipc.h 同步到主仓 launcher 版本(含 AUDIO_FMT_DOP/媒体歌词/蓝牙输出协议)
- 不含 FreeType 矢量字 meow_ft_*(需 freetype vendor,公开仓以位图 meow_font
  自包含);不含 mipsel 二进制 / .o / .a / 固件 / 密钥 / devdocs

examples/hello/:
- 新增 ui/demo/sdk2/sdk2.c;build_hello.sh 纳入该文件(保持自包含)

docs / README / .gitignore:
- docs/MEOW_APP_SDK.md 补 §12.5(默认 MD3 容器 + 底部上滑回桌面 + 两个新 API)
- README 能力清单补「默认 MD3 容器」与「底部上滑回桌面」
- .gitignore 补新增宿主测试二进制

验证:sdk/meow_ui.a 与 examples/hello/hello 交叉编译均 0 warning;工作区无生成物。
2026-09-04 18:05:36 +08:00

335 lines
14 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);
}
/* 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;
}