MeowOS for HiBy R3 Pro II — 公共发布版本(历史已重置)

• 全新单提交:清除含敏感信息的旧历史(sudo 口令、内网 IP、SSH 密钥路径、热点 SSID、设备 MAC)
• 本地开发文档/测试记录/构建脚本全部移入 gitignored 的 devdocs/ 与 scripts/build/(不入库)
• 保留:MeowOS 源码(launcher/watchdog/bootmenu/meow_svcd/SDK)、项目规范文档、mips-repo
增补组件:SDK 统一日志+通知 API、meow_svcd 服务管理器、mips-repo 软件仓库
This commit is contained in:
MeowOS Dev committed 2026-08-30 01:12:34 +08:00
commit 245bee2946
136 files changed
+32730

No files matched your search

+279
View File
@@ -0,0 +1,279 @@
/*
* 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;
}