diff --git a/font/meow_font.c b/font/meow_font.c index 9283978..410ca41 100644 --- a/font/meow_font.c +++ b/font/meow_font.c @@ -122,26 +122,13 @@ static unsigned char *read_file(const char *path, size_t *out_len) { return buf; } -/* ---------------- zlib inflate ---------------- */ -static int inflate_mem(const unsigned char *src, size_t srclen, - unsigned char *dst, size_t dstlen, size_t *out) { - mzf_stream s; - memset(&s, 0, sizeof(s)); - s.next_in = (mzf_Bytef *)src; - s.avail_in = (mzf_uInt)srclen; - s.next_out = (mzf_Bytef *)dst; - s.avail_out = (mzf_uInt)dstlen; - s.zalloc = 0; s.zfree = 0; s.opaque = 0; - if (inflateInit_(&s, zlibVersion(), (int)sizeof(s)) != MZF_OK) - return -1; - int ret = inflate(&s, MZF_FINISH); - *out = (size_t)(dstlen - s.avail_out); - inflateEnd(&s); - return (ret == MZF_STREAM_END) ? 0 : -1; -} - -/* ---------------- 最小 PNG 解码(灰度 non-interlaced) ---------------- */ -/* 返回打包像素面(调用方释放),并输出 width/height/depth/plane_rowbytes。失败返回 NULL。 */ +/* ---------------- 最小 PNG 解码(灰度 non-interlaced) ---------------- + * 流式/分块解码,消除「整幅原始扫描行 raw + 整段 idat 拷贝」双份驻留: + * - idat 不再另外 malloc 拷贝,直接以 png 文件缓冲内 IDAT 区间作 zlib 输入; + * - 逐行 inflate 到单行小缓冲 rowbuf(rowbytes+1 字节),随即反滤波写入打包面 out; + * 不再分配整幅 raw(= 扫描行数×(rowbytes+1))——对大字库这是 6-16MB 级的一次性峰值。 + * 大字库 gbk 解码面本身很大(行数×rowbytes),流式后 load 峰值 ≈ 文件缓冲 + idx + 打包面, + * 而非 文件缓冲 + idat + raw + 打包面。*/ static unsigned char *png_decode_gray(const unsigned char *png, size_t len, unsigned *out_w, unsigned *out_h, unsigned *out_depth, size_t *out_rowbytes) { @@ -150,14 +137,14 @@ static unsigned char *png_decode_gray(const unsigned char *png, size_t len, size_t i = 8; unsigned w = 0, h = 0, depth = 0, color = 0, interlace = 1, compression = 1, filt = 1; int have_ihdr = 0; - size_t idat_len = 0; - unsigned char *idat = NULL; + size_t idat_off = 0, idat_len = 0; /* IDAT 在 png 内的连续区间(偏移/长度) */ + int have_idat = 0; while (i + 8 <= len) { uint32_t clen = be32(png + i); const unsigned char *type = png + i + 4; if (i + 8 + (size_t)clen > len) break; if (memcmp(type, "IHDR", 4) == 0) { - if (clen < 13) { free(idat); return NULL; } + if (clen < 13) return NULL; w = be32(png + i + 8); h = be32(png + i + 12); depth = png[i + 16]; @@ -167,43 +154,61 @@ static unsigned char *png_decode_gray(const unsigned char *png, size_t len, interlace = png[i + 20]; have_ihdr = 1; } else if (memcmp(type, "IDAT", 4) == 0) { - unsigned char *na = (unsigned char *)realloc(idat, idat_len + clen); - if (!na) { free(idat); return NULL; } - idat = na; - memcpy(idat + idat_len, png + i + 8, clen); - idat_len += clen; + if (!have_idat) { idat_off = i + 8; idat_len = clen; have_idat = 1; } + else { + /* 要求 IDAT 连续(合法 PNG/单段压缩满足;gen_font.py 亦单段) */ + if (idat_off + idat_len != i + 8) return NULL; + idat_len += clen; + } } else if (memcmp(type, "IEND", 4) == 0) { break; } i += 12 + (size_t)clen; } - if (!have_ihdr || !idat || + if (!have_ihdr || !have_idat || color != 0 || compression != 0 || filt != 0 || interlace != 0 || - (depth != 4 && depth != 8) || w == 0 || h == 0) { - free(idat); + (depth != 4 && depth != 8) || w == 0 || h == 0) return NULL; - } size_t rowbytes = ((size_t)w * depth + 7) / 8; size_t scanlen = rowbytes + 1; /* 每扫描行 = 1 filter 字节 + rowbytes 像素 */ - size_t rawlen = scanlen * h; - unsigned char *raw = (unsigned char *)malloc(rawlen); - if (!raw) { free(idat); return NULL; } - size_t outlen = 0; - if (inflate_mem(idat, idat_len, raw, rawlen, &outlen) != 0 || outlen != rawlen) { - free(raw); free(idat); return NULL; + size_t rawlen = scanlen * h; /* 期望的解压总量(校验用,不据此分配) */ + + unsigned char *out = (unsigned char *)malloc(rowbytes * h); + if (!out) return NULL; + unsigned char *rowbuf = (unsigned char *)malloc(scanlen); + if (!rowbuf) { free(out); return NULL; } + + mzf_stream s; + memset(&s, 0, sizeof(s)); + s.next_in = (mzf_Bytef *)(png + idat_off); + s.avail_in = (mzf_uInt)idat_len; + s.zalloc = 0; s.zfree = 0; s.opaque = 0; + if (inflateInit_(&s, zlibVersion(), (int)sizeof(s)) != MZF_OK) { + free(out); free(rowbuf); return NULL; } - free(idat); - /* 反滤波(bpp=1:灰度 depth 4|8) → out 为每行 rowbytes 的打包像素 */ - size_t outlen_px = rowbytes * h; - unsigned char *out = (unsigned char *)malloc(outlen_px); - if (!out) { free(raw); return NULL; } const unsigned bpp = 1; - for (size_t y = 0; y < h; y++) { - unsigned char ft = raw[y * scanlen]; - const unsigned char *c = raw + y * scanlen + 1; - unsigned char *dst = out + y * rowbytes; - unsigned char *prev = (y == 0) ? NULL : out + (y - 1) * rowbytes; + size_t produced = 0; + int ok = 1, stream_end = 0; + for (unsigned y = 0; y < h; y++) { + /* 逐行填满 rowbuf(zlib 可能分批产出,内层循环补足到 scanlen) */ + size_t filled = 0; + while (filled < scanlen) { + s.next_out = rowbuf + filled; + s.avail_out = (mzf_uInt)(scanlen - filled); + int ret = inflate(&s, MZF_NO_FLUSH); + size_t got = (scanlen - filled) - s.avail_out; + filled += got; + if (ret == MZF_STREAM_END) { stream_end = 1; break; } + if (ret != MZF_OK || got == 0) { ok = 0; break; } + } + if (!ok || filled != scanlen) { ok = 0; break; } + + /* 反滤波(bpp=1:灰度 depth 4|8)→ out 为每行 rowbytes 的打包像素 */ + unsigned char ft = rowbuf[0]; + const unsigned char *c = rowbuf + 1; + unsigned char *dst = out + (size_t)y * rowbytes; + unsigned char *prev = (y == 0) ? NULL : out + (size_t)(y - 1) * rowbytes; for (size_t x = 0; x < rowbytes; x++) { unsigned char left = (x >= bpp) ? dst[x - bpp] : 0; unsigned char up = prev ? prev[x] : 0; @@ -223,11 +228,17 @@ static unsigned char *png_decode_gray(const unsigned char *png, size_t len, dst[x] = v + pred; break; } - default: free(raw); free(out); return NULL; + default: ok = 0; break; } } + if (!ok) break; + produced += scanlen; + if (stream_end) break; } - free(raw); + inflateEnd(&s); + free(rowbuf); + if (!ok || produced != rawlen) { free(out); return NULL; } + *out_w = w; *out_h = h; *out_depth = depth; *out_rowbytes = rowbytes; return out; } @@ -493,6 +504,18 @@ void meow_font_unload(void) { for (int i = 0; i < MEOW_FCLS_COUNT; i++) g_class_slot[i] = 0; } +#ifdef MEOW_FONT_ENABLE_DEBUG +/* 宿主调试/校验钩子:取某槽已装载(打包)像素面 + 几何。仅宿主单测用, + * mipsel 构建不含 MEOW_FONT_ENABLE_DEBUG,无此符号、零开销。 */ +const unsigned char *meow_font_debug_plane(int slot, unsigned *w, unsigned *h, + unsigned *depth, size_t *rowbytes) { + meow_font_slot *st = &g_slots[slot]; + if (!st->loaded) return NULL; + *w = st->plane_w; *h = st->plane_h; *depth = st->depth; *rowbytes = st->plane_rowbytes; + return st->plane; +} +#endif + int meow_font_load_slot(int slot, const char *png_path, const char *idx_path) { if (slot < 0 || slot >= MEOW_MAX_SLOTS) return -9; meow_font_unload_slot(slot); @@ -672,7 +695,23 @@ void meow_font_drawtext_outline(uint16_t *fb, int x, int y, const char *utf8, meow_font_drawtext(fb, x, y, utf8, color, size); } -/* ---------------- 多字体配置(去重 + ≤4 套上限,launcher 与 SDK 共用) ---------------- */ +/* ---------------- 多字体配置(去重 + ≤3 套上限 + 内存预算,launcher 与 SDK 共用) ---------------- */ +/* 多字体全部槽(含 slot0)「打包像素面」总字节预算:超出时新增类别字库被拒、回退全局并用 + * *truncated=1 通知调用方 WARN。设定依据(真机 56MB,launcher+App 常态化紧张): + * slot0 若为 gbk 大字库,其 plane 已 ≈16.6MiB(>预算)→ 自动拒绝任何额外类别字库, + * 杜绝「大字库 + 多字体」双驻留 OOM;slot0 为 common(≈4.2MiB)时允许 1-3 个额外常用类 + * (+小类拉丁/数字等),封顶后单进程字库总驻留可控制在 ~16MiB 内。 + * 值 16 MiB:56MiB 总内存 − launcher 基线(估 26-28MiB)− 应用运行时 → 字库红利带。 */ +#define MEOW_MULTIFONT_MEM_BUDGET (16u * 1024 * 1024) + +static size_t mfont_total_plane_bytes(void) { + size_t t = 0; + for (int i = 0; i < MEOW_MAX_SLOTS; i++) { + meow_font_slot *st = &g_slots[i]; + if (st->loaded) t += (size_t)st->plane_rowbytes * st->plane_h; + } + return t; +} int meow_font_config_multifont(int on, const char *global_png, const char *global_idx, const meow_font_class_cfg cfg[MEOW_FCLS_COUNT], @@ -716,6 +755,14 @@ int meow_font_config_multifont(int on, g_class_slot[c] = 0; continue; } + /* 内存预算:装载成功后,若全部槽 plane 总字节超预算 → 卸载该新槽 + 回退全局 + 截断通知。 + * 防「大字库 slot0 + 多个大字库类别字库」同时驻留(真机 56MB 会 OOM)。 */ + if (mfont_total_plane_bytes() > MEOW_MULTIFONT_MEM_BUDGET) { + meow_font_unload_slot(slot); + if (truncated) *truncated = 1; + g_class_slot[c] = 0; + continue; + } d_png[d_n] = png; d_slot[d_n] = slot; d_n++; g_class_slot[c] = slot; } diff --git a/font/meow_font.h b/font/meow_font.h index 7860d54..431cf32 100644 --- a/font/meow_font.h +++ b/font/meow_font.h @@ -84,8 +84,11 @@ int meow_font_has_glyph(uint32_t cp); * meow_font_drawtext/width_text/line_height/... 在 meow_font_enable_multifont(1) 开启时 * 逐字符分类 → 用该类别对应槽的字库取字形与度量;缺该类别字库/未加载 → 回退 slot 0。 * 关闭(默认)时一律走 slot 0,行为与单字体完全一致(零额外开销)。 - * 同 family 去重、≤4 套上限由 meow_font_config_multifont() 统一处理(launcher 与 SDK 共用)。 */ -#define MEOW_MAX_SLOTS 5 /* slot0(全局) + ≤4 套类别字库;超出则截断并置 *truncated=1(调用方 WARN) */ + * 同 family 去重、类别字库 ≤3 套上限 + 打包面内存预算由 meow_font_config_multifont() 统一处理(launcher 与 SDK 共用)。 */ +#define MEOW_MAX_SLOTS 4 /* slot0(全局) + ≤3 套类别字库。真机 56MB:多字体每套字库都驻留整幅 + * 打包面(common≈4.2MiB / gbk≈16.6MiB),5 套太多会叠加 OOM;故硬上限 + * 收到 4,且 meow_font_config_multifont 另按 meow_font.c 内 + * MEOW_MULTIFONT_MEM_BUDGET 做「总打包面字节」预算(超出→新增类别回退全局)。 */ int meow_font_load_slot(int slot, const char *png_path, const char *idx_path); /* 0=成功,负值失败 */ void meow_font_unload_slot(int slot); diff --git a/font/test_budget.c b/font/test_budget.c new file mode 100644 index 0000000..d9ceb89 --- /dev/null +++ b/font/test_budget.c @@ -0,0 +1,39 @@ +/* + * test_budget.c — 宿主校验 meow_font_config_multifont 的「打包面内存预算」: + * slot0=common(≈4.2MiB)+ 给 CJK 类别装 gbk(≈16.6MiB)→ 合计 > MEOW_MULTIFONT_MEM_BUDGET + * → gbk 应被拒绝(distinct=1,truncated=1),防止大字库 + 多字体双驻留 OOM。 + * 用法:gcc -I. test_budget.c meow_font.c -lz -o test_budget + * ./test_budget + */ +#include "meow_font.h" +#include +#include + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (!(cond)) { g_fail++; fprintf(stderr, "FAIL: %s\n", msg); } \ + else printf("ok : %s\n", msg); \ +} while (0) + +int main(int argc, char **argv) { + if (argc < 5) { fprintf(stderr, "usage: %s \n", argv[0]); return 2; } + int r = meow_font_load(argv[1], argv[2]); /* slot0 = common */ + CHECK(r == 0, "load slot0 common"); + meow_font_class_cfg cfg[MEOW_FCLS_COUNT]; + memset(cfg, 0, sizeof cfg); + cfg[MEOW_FCLS_CJK].png = argv[3]; /* CJK 类别 → gbk 大字库 */ + cfg[MEOW_FCLS_CJK].idx = argv[4]; + int distinct = 0, truncated = 0; + r = meow_font_config_multifont(1, argv[1], argv[2], cfg, &distinct, &truncated); + CHECK(r == 0, "config_multifont ok"); + printf(" distinct=%d truncated=%d\n", distinct, truncated); + /* slot0 common 4.2MiB + gbk 16.6MiB = 20.8MiB > 16MiB 预算 → gbk 被拒 → distinct 仅全局 */ + CHECK(distinct == 1, "distinct==1 (gbk rejected by MEM_BUDGET)"); + CHECK(truncated == 1, "truncated==1 (budget exceeded -> class fell back to global)"); + /* gbk 槽应未驻留:distinct==1(仅全局 common)即证明 gbk 未被 commit; + * 注意 common 4k 亦含「中」,故不能以 has_glyph(中)==0 断言——改以「common 仍在」佐证。 */ + CHECK(meow_font_has_glyph('A') == 1, "has_glyph(A)==1 (common still there)"); + meow_font_unload(); + printf("\nRESULT: %s\n", g_fail ? "FAIL" : "PASS"); + return g_fail ? 1 : 0; +} diff --git a/font/test_mem_trace.c b/font/test_mem_trace.c new file mode 100644 index 0000000..0758d7f --- /dev/null +++ b/font/test_mem_trace.c @@ -0,0 +1,119 @@ +/* + * test_mem_trace.c — MeowOS 字库加载内存驻留量测(宿主导出单测)。 + * + * 目的:精确量化 meow_font_load / meow_font_unload / meow_font_reload_tier: + * (1) load 过程中堆峰值(含 PNG 解码中间:文件缓冲 + idat + 原始扫描行 + 打包面); + * (2) load 后稳态驻留(plane + glyphs); + * (3) unload 后 live 归零(free 真正归还、无碎片驻留)。 + * + * 机制:-Wl,--wrap=malloc/free/calloc/realloc 把 meow_font.c 的分配纳入统计。 + * __wrap_* 用「指针→字节数」记账表精确统计 live / peak(无论 libc 是否支持 usable_size)。 + * + * 用法: + * gcc -I. -O0 -g test_mem_trace.c meow_font.c -lz \ + * -Wl,--wrap=malloc -Wl,--wrap=free -Wl,--wrap=calloc -Wl,--wrap=realloc \ + * -o test_mem_trace + * ./test_mem_trace + * 退出码 0=全过;非 0=某断言失败。 + */ +#define _GNU_SOURCE +#include "meow_font.h" +#include +#include +#include +#include +#include + +/* ---------------- 指针→大小 记账表 ---------------- */ +#define TREESZ 4096 +static struct { void *p; size_t sz; } g_tab[TREESZ]; +static size_t g_live = 0, g_peak = 0; +static int g_tracking = 0; + +static void note_add(void *p, size_t sz) { + if (!g_tracking || !p) return; + g_live += sz; + if (g_live > g_peak) g_peak = g_live; + for (int i = 0; i < TREESZ; i++) if (!g_tab[i].p) { g_tab[i].p = p; g_tab[i].sz = sz; return; } +} +static void note_sub(void *p) { + if (!g_tracking || !p) return; + for (int i = 0; i < TREESZ; i++) { + if (g_tab[i].p == p) { g_live -= g_tab[i].sz; g_tab[i].p = NULL; g_tab[i].sz = 0; return; } + } +} +static size_t note_size(void *p) { + if (!p) return 0; + for (int i = 0; i < TREESZ; i++) if (g_tab[i].p == p) return g_tab[i].sz; + return 0; +} + +void *__real_malloc(size_t n); +void *__real_calloc(size_t n, size_t s); +void *__real_realloc(void *p, size_t n); +void __real_free(void *p); + +void *__wrap_malloc(size_t n) { void *p = __real_malloc(n); if (p) note_add(p, n); return p; } +void *__wrap_calloc(size_t n, size_t s) { void *p = __real_calloc(n, s); if (p) note_add(p, n * s); return p; } +void *__wrap_realloc(void *p, size_t n) { + if (!p) { void *np = __real_realloc(NULL, n); if (np) note_add(np, n); return np; } + size_t old = note_size(p); + void *np = __real_realloc(p, n); + if (np) { note_sub(p); note_add(np, n); } + else { /* realloc 失败:原指针仍有效,表项保留 */ } + (void)old; + return np; +} +void __wrap_free(void *p) { if (p) note_sub(p); __real_free(p); } + +static void reset_peak(void) { g_peak = g_live; } + +static int g_fail = 0; +#define CHECK(cond, msg) do { \ + if (!(cond)) { g_fail++; fprintf(stderr, "FAIL: %s (live=%zu peak=%zu)\n", msg, g_live, g_peak); } \ + else printf("ok : %s\n", msg); \ +} while (0) + +static double MiB(size_t b) { return (double)b / 1048576.0; } +static double KiB(size_t b) { return (double)b / 1024.0; } + +int main(int argc, char **argv) { + if (argc < 3) { fprintf(stderr, "usage: %s \n", argv[0]); return 2; } + const char *png = argv[1], *idx = argv[2]; + + g_tracking = 1; + reset_peak(); + printf("== meow_font 内存量测(精确指针记账)==\n"); + printf("png=%s idx=%s\n", png, idx); + + int r = meow_font_load(png, idx); + CHECK(r == 0, "meow_font_load ok"); + printf(" >> load 后稳态驻留 live = %.2f MiB (%.2f KiB)\n", MiB(g_live), KiB(g_live)); + printf(" >> load 期间堆峰值 peak = %.2f MiB (%.2f KiB)\n", MiB(g_peak), KiB(g_peak)); + + static uint16_t fb[480 * 320]; + meow_font_set_fb(480, 320, 480 * 2); + printf(" drawtext smoke...\n"); + meow_font_drawtext(fb, 0, 0, "中文测试 Abc 123,", 0xFFFF, 28); + meow_font_drawtext(fb, 0, 60, "试渲染第20号 face 中文", 0xF800, 20); + printf(" drawtext 后 live = %.2f MiB\n", MiB(g_live)); + + /* 反复 load/unload 验证无泄漏累增(每次 load 后的 live 应一致,unload 后归零) */ + size_t live_after_load[3]; + for (int k = 0; k < 3; k++) { + meow_font_unload(); + if (g_live != 0) { printf(" [iter %d] unload 后 live=%zu (非0! -> leak)\n", k, g_live); } + meow_font_load(png, idx); + live_after_load[k] = g_live; + } + printf(" 三次 load 后 live = %.2f / %.2f / %.2f MiB\n", + MiB(live_after_load[0]), MiB(live_after_load[1]), MiB(live_after_load[2])); + CHECK(live_after_load[0] == live_after_load[1] && live_after_load[1] == live_after_load[2], + "三轮回 load/unload 稳定(无泄漏累增)"); + meow_font_unload(); + printf(" >> unload 后 live = %.2f MiB (峰值 %.2f MiB)\n", MiB(g_live), MiB(g_peak)); + CHECK(g_live == 0, "unload 后 live 归零(free 真正归还)"); + + printf("\nRESULT: %s\n", g_fail ? "FAIL" : "PASS"); + return g_fail ? 1 : 0; +} diff --git a/font/test_plane_dump.c b/font/test_plane_dump.c new file mode 100644 index 0000000..d539d80 --- /dev/null +++ b/font/test_plane_dump.c @@ -0,0 +1,32 @@ +/* + * test_plane_dump.c — 宿主解码正确性校验:把 meow_font 解码出的打包像素面 dump 成二进制, + * 供 Python 参考解码(zlib IDAT + 反滤波)逐字节比对,验证流式解码与参考解码完全一致。 + * 需以 -DMEOW_FONT_ENABLE_DEBUG 编译 meow_font.c 暴露 meow_font_debug_plane。 + */ +#include "meow_font.h" +#include +#include + +/* 宿主调试钩子:定义于 meow_font.c(MEOW_FONT_ENABLE_DEBUG 下才编译);此测试自带声明。 */ +extern const unsigned char *meow_font_debug_plane(int slot, unsigned *w, unsigned *h, + unsigned *depth, size_t *rowbytes); + +int main(int argc, char **argv) { + if (argc < 4) { fprintf(stderr, "usage: %s \n", argv[0]); return 2; } + int r = meow_font_load(argv[1], argv[2]); + if (r != 0) { fprintf(stderr, "load failed %d\n", r); return 1; } + unsigned w, h, depth; size_t rb; + const unsigned char *p = meow_font_debug_plane(0, &w, &h, &depth, &rb); + if (!p) { fprintf(stderr, "no plane\n"); return 1; } + size_t total = (size_t)rb * h; + FILE *f = fopen(argv[3], "wb"); + if (!f) { return 1; } + /* 先写出几何头(小端),再写平面字节 */ + unsigned hdr[5] = { w, h, depth, (unsigned)rb, (unsigned)total }; + fwrite(hdr, sizeof(unsigned), 5, f); + fwrite(p, 1, total, f); + fclose(f); + printf("dumped w=%u h=%u depth=%u rowbytes=%zu bytes=%zu\n", w, h, depth, rb, total); + meow_font_unload(); + return 0; +} diff --git a/patch_v2/bootmenu/S90meow_bootmenu b/patch_v2/bootmenu/S90meow_bootmenu index f26ae7f..ecbaed5 100755 --- a/patch_v2/bootmenu/S90meow_bootmenu +++ b/patch_v2/bootmenu/S90meow_bootmenu @@ -8,10 +8,11 @@ # * M2: meow_bootmenu is launched with --fork, so: # 选 HibyOS -> bootmenu forks /usr/bin/hiby_player (最小启动契约), # parent becomes supervisor/热键守护, blocks rcS. -# 选 MeowOS -> bootmenu exec /usr/bin/meow_launcherd (MeowOS 基础主屏), -# blocks rcS. -# Either way rcS is blocked by S90 until整机重启/热键切换, so S92 (兜底 patch) -# is normally never reached; SSH was moved to S84 so it is already online. +# 选 MeowOS -> launcher 生命周期已交 meow_sysui(S70 起):sysui 在 → bootmenu +# 让 sysui 拉起 launcher 并让位退出(rcS 推进,S92 见 mode=meow 跳过 +# 播放器);sysui 不在 → bootmenu 照旧 exec /usr/bin/meow_launcherd(兜底)。 +# HibyOS 模式 rcS 被 S90 阻塞;MeowOS(sysui 托管)rcS 推进到 S92(兜底 patch,mode=meow 跳过)。 +# SSH was moved to S84 so it is already online. # * rcS runs S??* (non-.sh) as fork-ed subprocesses, so `exec` here replaces # ONLY this script's process. # * 红线: 启动前清 /data/recovery_all、禁经 hiby_player.sh、禁 killall、 diff --git a/patch_v2/bootmenu/meow_bootmenu b/patch_v2/bootmenu/meow_bootmenu index e0e062e..7fd84e7 100755 Binary files a/patch_v2/bootmenu/meow_bootmenu and b/patch_v2/bootmenu/meow_bootmenu differ diff --git a/patch_v2/bootmenu/meow_bootmenu.c b/patch_v2/bootmenu/meow_bootmenu.c index 70ab59c..191b092 100644 --- a/patch_v2/bootmenu/meow_bootmenu.c +++ b/patch_v2/bootmenu/meow_bootmenu.c @@ -35,8 +35,12 @@ * ============================================================================ * WHY (设计见 BOOT_SELECTOR_DESIGN.md §3) * ============================================================================ - * - 开机最先显示选单(HibyOS / MeowOS),选择后 fork 播放器(HibyOS)或 - * exec MeowOS 主屏(MeowOS);HibyOS 模式热键(电源+音量减)杀播放器接管。 + * - 开机最先显示选单(HibyOS / MeowOS),选择后 fork 播放器(HibyOS)或进入 + * MeowOS 主屏;HibyOS 模式热键(电源+音量减)杀播放器接管。 + * - M2.83 launcher 监管迁移:MeowOS 主屏(meow_launcherd)生命周期已交 meow_sysui + * (S70 起)。故选 MeowOS 时——sysui 在 → 让 sysui 拉起/监管 launcher,本进程让位退出 + * (rcS 推进,S92 见 mode=meow 跳过播放器);sysui 不在 → 本进程照旧 exec launcher(兜底, + * 防开不了机)。选 HibyOS 时 sysui 在 → 让 sysui 停 launcher(不与播放器抢 fb0)。 * - 本二进制 = 选单本体(M0)。M0 只做二进制自检:SSH 推 /data 独立跑, * **不进启动链**(不碰 S90/S92/播放器/SSH 顺序)。 * @@ -898,8 +902,81 @@ static int sim_confirm_done = 0; static int sim_hk_done = 0; static int sim_hotkey_strict = 0; /* M2.14:--sim-hotkey-strict = 让 sim 热键走武装延迟/忙判门(测 pending) */ +/* ====================================================================== + * M2.83 launcher 监管迁移:launcher 生命周期由 bootmenu 移交给 meow_sysui + * - MeowOS 模式:sysui 在 → 让 sysui 拉起/监管 launcher,bootmenu 让位不 exec + * (rcS 推进 → S92 见 mode=meow 跳过播放器);sysui 不在 → bootmenu 照旧 exec launcher(兜底)。 + * - HibyOS 模式:sysui 在 → 让 sysui 停 launcher(会话级 user_stopped,不自动重启),避免与播放器抢 fb0。 + * 双保险:sysui 拉 + launcher 崩 sysui 重启(restart=always);bootmenu 仅在 sysui 不在时兜底。 + * ====================================================================== */ +static int sysui_active(void) +{ + /* sysui daemon 是否在跑:/tmp/meow_sysui.pid 存活,且 /proc//comm=meow_sysui(防 pid 复用误判) */ + FILE *f = fopen("/tmp/meow_sysui.pid", "r"); + if (!f) return 0; + long pid = -1; + int n = fscanf(f, "%ld", &pid); + fclose(f); + if (n != 1 || pid <= 1) return 0; + if (kill((pid_t)pid, 0) != 0) return 0; /* ESRCH → 不在 */ + char comm[64] = {0}, path[64]; + snprintf(path, sizeof path, "/proc/%ld/comm", pid); + int fd = open(path, O_RDONLY); + if (fd >= 0) { + ssize_t r = read(fd, comm, sizeof comm - 1); + close(fd); + if (r > 0) { + char *nl = strchr(comm, '\n'); + if (nl) *nl = '\0'; + if (strncmp(comm, "meow_sysui", 10) != 0) return 0; + } + } + return 1; +} + +static void sysui_cmd_launcher(const char *op) +{ + /* 调 /usr/bin/meow_sysui start|stop launcher(best-effort:sysui 缺失/失效 → 无害退出)。 + * 等 CLI 返回(CLI 连 socket 快速回),避免僵尸;失败不致命。 */ + pid_t pid = fork(); + if (pid < 0) { + fprintf(stderr, "bootmenu: sysui %s launcher: fork failed: %s\n", op, strerror(errno)); + return; + } + if (pid == 0) { + execl("/usr/bin/meow_sysui", "/usr/bin/meow_sysui", op, "launcher", (char *)NULL); + _exit(127); /* sysui 缺失/不可执行 → 无害退出(fallback 路径会用本地 exec) */ + } + int st = 0; + (void)waitpid(pid, &st, 0); +} + +static int enter_meow_launcher(void) +{ + /* MeowOS 决策点:返回 0=已让位(launcher 由 sysui 继续管理,调用方正常退出/让 rcS 推进); + * 否则已 exec launcher(本函数不返回)。 */ + if (sysui_active()) { + fprintf(stderr, "bootmenu: sysui owns launcher -> hand off (start via sysui)\n"); + fflush(NULL); + sysui_cmd_launcher("start"); + return 0; + } + /* 回退:sysui 不在 → bootmenu 照旧 exec launcher(防开不了机) */ + fprintf(stderr, "bootmenu: no sysui -> exec %s (fallback)\n", LAUNCHER_BIN); + fflush(NULL); + execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); + fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); + _exit(127); +} + static void fork_hiby_player(void) { + /* M2.83 迁移:HibyOS 模式下 launcher 不应与播放器抢 fb0——sysui 在则让 sysui 停 launcher */ + if (sysui_active()) { + fprintf(stderr, "bootmenu: HibyOS selected -> stop launcher via sysui\n"); + fflush(NULL); + sysui_cmd_launcher("stop"); + } pid_t pid = fork(); if (pid < 0) { fprintf(stderr, "fork player failed: %s\n", strerror(errno)); return; } if (pid == 0) { @@ -1278,13 +1355,17 @@ static void bootmenu_supervisor(int player_pid) } player_alive = 0; } - /* 播放器已死:面板已在确认框点亮(brightness=saved)。写 mode=meow, - * 然后 exec MeowOS 基础主屏(接管 fb0 画 buffer0+pan0)。 */ + /* 播放器已死:面板已在确认框点亮(brightness=saved)。写 mode=meow。 + * M2.83 迁移:MeowOS 基础主屏(launcher)已交 sysui 托管——sysui 在则让 sysui + * 拉起 launcher(本 supervisor 让位退出,rcS 推进,S92 见 mode=meow 跳过播放器); + * sysui 不在则照旧 exec launcher(兜底)。 */ write_mode(0); - fprintf(stderr, "supervisor: exec /usr/bin/meow_launcherd\n"); - fflush(NULL); - execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); - fprintf(stderr, "supervisor: exec launcher failed: %s\n", strerror(errno)); + if (enter_meow_launcher() == 0) { + fprintf(stderr, "supervisor: launcher handed off to sysui, supervisor exits\n"); + fflush(NULL); + _exit(0); + } + /* 不可达:enter_meow_launcher 在无 sysui 时已 exec launcher(不返回) */ _exit(127); } else { /* ===== 否 / -1:留在 HibyOS ===== @@ -1528,15 +1609,14 @@ static int run_menu(int sim_sel, int do_fork) fb_restore_player_frame(); fb_close(); /* M2(--fork):HibyOS→fork 播放器(父进程转监督者,阻塞 rcS); - * MeowOS→exec meow_launcherd(MeowOS 基础主屏,接管 fb0)。 */ + * MeowOS→sysui 在则让 sysui 拉起 launcher 并让位(rcS 推进,S92 见 mode=meow 跳过播放器); + * sysui 不在则 exec meow_launcherd(MeowOS 基础主屏,接管 fb0;兜底)。返回=已让位。 */ if (do_fork) { if (choice == 1) { fork_hiby_player(); /* 父进程陷入 supervisor,永不返回 */ } else { set_boot_default_meow(); /* Bug 1:选 MeowOS → 持久为开机默认 */ - execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); - fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); - _exit(127); + enter_meow_launcher(); /* 让位返回 0,或 exec launcher(不返回) */ } } return 0; /* M0/M1(无 --fork):选单退出,rcS 推进 */ @@ -1556,7 +1636,8 @@ static void usage(void) " --boot-only 跳过选单,直接取默认(boot_default)\n" " --timeout-ms N 覆写超时毫秒(测试钩子)\n" " --fork M2 开启:选 HibyOS 时 fork 播放器并转监督者;\n" - " 选 MeowOS 时 exec /usr/bin/meow_launcherd(默认关)\n" + " 选 MeowOS 时 sysui 在→让 sysui 拉起 launcher(本进程让位退出),\n" + " 否则 exec /usr/bin/meow_launcherd(默认关)\n" " --sim-hotkey N 监督者启动 N ms 后模拟热键(M2 真机 verif)\n" " --sim-confirm Y|N 确认框打开 2s 后自动答 Y=是 / N=否\n" " --version 打印版本并退出\n", @@ -1617,9 +1698,7 @@ int main(int argc, char **argv) if (choice == 1) fork_hiby_player(); else { set_boot_default_meow(); /* Bug 1:选 MeowOS → 持久为开机默认 */ - execv(LAUNCHER_BIN, (char *[]){ "meow_launcherd", NULL }); - fprintf(stderr, "bootmenu: exec launcher failed: %s\n", strerror(errno)); - _exit(127); + enter_meow_launcher(); /* sysui 在→让位返回 0;否则 exec launcher(不返回) */ } } return 0; diff --git a/patch_v2/meow_sysui/S70meow_sysui b/patch_v2/meow_sysui/S70meow_sysui index 93a6c54..5a4e76b 100644 --- a/patch_v2/meow_sysui/S70meow_sysui +++ b/patch_v2/meow_sysui/S70meow_sysui @@ -4,12 +4,13 @@ # # 位置:**最早**(早于 S84ssh / S89meow_svcd / S90meow_bootmenu)。 # * 定案启动链:meow_sysui 先起(S70)→ meow_svcd(由 sysui 拉起)→ 各项服务子进程。 -# * S90meow_bootmenu 里 `exec /usr/bin/meow_bootmenu --fork` 会阻塞 rcS,故任何 S90 之后 -# 的 init.d 都不执行——sysui 必须排在 S90 之前(S70 满足)。 +# * S90meow_bootmenu `exec /usr/bin/meow_bootmenu --fork`:HibyOS 模式 fork 播放器并转监督者, +# 阻塞 rcS;MeowOS 模式在 sysui 托管下让位退出(rcS 推进,S92 见 mode=meow 跳过播放器)。 +# 故 sysui 必须排在 S90 之前(S70 满足)——launcher 已在 S70 由 sysui 拉起。 # # 职责:监管系统进程(launcher / meow_svcd / meow_mdns / …),一个崩只拉起它、绝不级联。 # 本脚本只做三件事: -# 1) 预置板载监管配置 /usr/data/meow/sysui.d/*.conf(首启建,已存在不覆盖); +# 1) 预置板载监管配置 /usr/data/meow/sysui.d/*.conf(首启建;除 launcher.conf 迁移升级外已存在不覆盖); # 2) 拉起 meow_sysui(默认后台 daemon); # 3) stop 时优雅停 sysui(其会 SIGTERM 被监管的 svcd 并收尸)。 # @@ -39,24 +40,37 @@ EOF chmod 644 /usr/data/meow/sysui.d/svcd.conf echo -n "provisioned svcd.conf " fi - # launcher 当前由 S90meow_bootmenu 的 exec 拥有生命周期;待 bootmenu 让位后交 sysui。 - # 此处仅登记(enabled=0),避免双重拉起。mdns 是 svcd 的服务子进程,由 svcd 负责, - # 本阶段不直接监管(或作兜底条目,默认关)。 + # launcher 监管(M2.83 迁移):已交 sysui 托管——bootmenu 在 MeowOS 模式让位(sysui 在则 + # 不再 exec launcher,lifecycle 归 sysui:崩→sysui 按 restart=always 重启;bootmenu 仅在 + # sysui 不在时兜底 exec)。故此处 enabled=1。检测到旧版(enabled=0,bootmenu 时代)→ 升级。 + # mdns 是 svcd 的服务子进程,由 svcd 负责,本阶段不直接监管(或作兜底条目,默认关)。 + LAUNCHER_UPGRADE='' if [ -f /usr/data/meow/sysui.d/launcher.conf ]; then - echo -n "launcher.conf exists " - else + if grep -qE '^[[:space:]]*enabled[[:space:]]*=[[:space:]]*0([[:space:]]|#|$)' /usr/data/meow/sysui.d/launcher.conf; then + LAUNCHER_UPGRADE=1 + else + echo -n "launcher.conf exists " + fi + fi + if [ -n "$LAUNCHER_UPGRADE" ] || [ ! -f /usr/data/meow/sysui.d/launcher.conf ]; then cat > /usr/data/meow/sysui.d/launcher.conf <<'EOF' -# 预留:launcher(系统 UI 渲染宿主)。现网由 S90meow_bootmenu exec 拥有 → 启用前需 bootmenu 让位。 -# 真把 launcher 交给 sysui 监管后,把 enabled=1(并停用 bootmenu 的 exec launcher 路径)。 +# meow_sysui 监管条目:launcher(系统 UI 渲染宿主)。系统进程总监 → launcher 被监管。 +# 语义同 services.d:key=value,'#'/';' 注释,未知键忽略。无 trigger——enabled=1 即常驻监护。 +# M2.83 launcher 监管迁移:bootmenu 在 MeowOS 模式让位(sysui 在则不 exec launcher), +# launcher 生命周期归 sysui:崩→sysui 按 restart=always 重启;bootmenu 仅在 sysui 不在时兜底。 exec=/usr/bin/meow_launcherd args= -enabled=0 +enabled=1 restart=always backoff_ms=3000 max_restart=5 EOF chmod 644 /usr/data/meow/sysui.d/launcher.conf - echo -n "provisioned launcher.conf " + if [ -n "$LAUNCHER_UPGRADE" ]; then + echo -n "upgraded launcher.conf (enabled 0->1) " + else + echo -n "provisioned launcher.conf " + fi fi else echo -n "skip provision (/usr/data/meow not writable) " diff --git a/patch_v2/meow_sysui/meow_sysui.md b/patch_v2/meow_sysui/meow_sysui.md index 4281dff..55cd63f 100644 --- a/patch_v2/meow_sysui/meow_sysui.md +++ b/patch_v2/meow_sysui/meow_sysui.md @@ -16,9 +16,10 @@ - **只监护自己 fork+exec 出来的进程**:`waitpid` 需为父进程,这是「一个崩只拉起它」的可靠前提; sysui **不做**「adopt 外部 pid」式监护(那只 probe `kill(pid,0)`、无法真正 waitpid/重启)。 -- 要监管什么由配置决定;**默认配置只开启 `meow_svcd`**(符合定案「sysui 总监 → svcd 被监管 → - 服务为 svcd 子进程」)。`launcher` 当前由 S90meow_bootmenu 的 `exec` 拥有,配置中登记但 - `enabled=0`(避免双重拉起;真正移交需 bootmenu 让位,见 §6)。 +- 要监管什么由配置决定;**默认配置开启 `meow_svcd` + `meow_launcherd`**(`launcher` 于 M2.83 + launcher 监管迁移落地:由 S70 预置 `launcher.conf` 且 `enabled=1`,bootmenu 在 MeowOS 模式让位)。 + bootmenu 仅在 sysui 不在时兜底 exec launcher;HibyOS 模式下 bootmenu 会让 sysui 停 launcher + (不与播放器抢 fb0)。详见 §6。 - `meow_mdns` 是 svcd 的服务子进程(svcd 负责 spawn/restart),sysui 本阶段**不直接监管**它 (避免与 svcd 双重管理);`§10.1` 的「mdns 崩→svcd 拉起(或 sysui 兜底)」——兜底可后续 在 `sysui.d/` 加一条 meow_mdns 条目实现(默认关)。 @@ -111,9 +112,14 @@ meow_sysui pid ## 6. 迁移说明 / 边界(诚实标注) -- **launcher 监管**:现网 launcher 由 `S90meow_bootmenu` 的 `exec` 拉起(bootmenu 拥有其生命周期)。 - sysui **本阶段不拉起 launcher**(`sysui.d/launcher.conf` 默认 `enabled=0` + 注释); - launcher 真正归 sysui 需 bootmenu 让位(后续迁移)。sysui 代码完全支持加一条 launcher 条目即可。 +- **launcher 监管(✅ M2.83 已落地)**:launcher 生命周期已由 `S90meow_bootmenu` 的 `exec` 移交 + sysui。`sysui.d/launcher.conf` 现 `enabled=1`(`restart=always`/`backoff_ms=3000`/`max_restart=5`)。 + - sysui 在 S70 拉起并监管 launcher(崩→按策略重启,稳定 30s 清零次数,超 max_restart→FAILED 停)。 + - bootmenu(`meow_bootmenu.c`)MeowOS 模式:sysui 在 → `meow_sysui start launcher` + 让位退出 + (不 exec launcher);sysui 不在 → bootmenu 照旧 exec launcher(兜底,防开不了机)。 + - bootmenu HibyOS 模式:sysui 在 → `meow_sysui stop launcher`(会话级 user_stopped)再 fork 播放器, + 避免 launcher 与播放器抢 fb0。 + - 已部署设备兼容:S70 检测旧 `launcher.conf` `enabled=0` → 升级为 `enabled=1`。 - **mdns 监管**:由 svcd 负责;sysui 兜底条目默认关。 ## 7. 编译 @@ -125,6 +131,8 @@ meow_sysui pid ## 后续(不在本阶段) -- launcher 正式移交 sysui 监管(依赖 bootmenu 让位)。 - `MeowOS/sysui.d/` TF 双源扩展(复用 svcd 板载优先语义)。 - 与 meow_watchdog / 崩溃捕获的职责对齐(见 WORK_STATE SDK 缺失清单 #5)。 +- launcher 已在 S70 由 sysui 拉起(bootmenu 让位,见 §6);**真机待验证**:①MeowOS 模式 bootmenu + 让位后 rcS 推进到 S92 是否稳定跳过播放器 ②HibyOS 模式 `sysui stop launcher` 与播放器 fork 的 + 短暂共存无 fb0 冲突 ③launcher 在 S70 早启动(先于选单)与 boot_menu=1 选单的 fb 共存。