diff --git a/patch_v2/meow_launcherd/app_launcher.c b/patch_v2/meow_launcherd/app_launcher.c index bc9a835..9720b9a 100644 --- a/patch_v2/meow_launcherd/app_launcher.c +++ b/patch_v2/meow_launcherd/app_launcher.c @@ -1044,7 +1044,8 @@ void cont_player(void) /* ================================================================== */ -/* M2.12 MeowAPPs 包数据目录 + .mpk 接口 */ +/* M2.12 MeowAPPs 包数据目录 + .mpk 导出接口 */ +/* 安装入口 meow_pkg_install M42 起移到 pkg_install.c(委托 nyai)。 */ /* exFAT 无符号链接 → 数据目录用环境变量(MEOW_DATA_DIR/CACHE_DIR) */ /* 传址给应用,不做 symlink(APPS.md 附录 v2 修订)。 */ /* ================================================================== */ @@ -1127,16 +1128,6 @@ static int pkg_json_field_mpk(const char *mpk, const char *key, char *out, int n return 0; } -/* 安装成功后,把原签 .mpk 另存 MeowData//data/.mpk.src(导出保留签名)。 */ -static void pkg_save_src(const char *mpk, const char *pkg) -{ - char dst[320], cmd[720]; - snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowData/%s/data/%s.mpk.src", pkg, pkg); - snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' 2>/dev/null && " - "cp '%s' '%s' 2>/dev/null", pkg, mpk, dst); - if (system(cmd) != 0) logmsg("pkg_install: save .mpk.src fail for %s", pkg); -} - /* 导出保留签名:缓存 .mpk.src 存在 + 验签通过 + 其 app.json 版本 == 已装 → 直接复制(不二次签名)。 * 成功 0;否则 -1(调用方现场重打包,无签名)。 */ static int pkg_export_preserve_sig(const char *pkg, const char *dst) @@ -1157,97 +1148,6 @@ static int pkg_export_preserve_sig(const char *pkg, const char *dst) return 0; } -/* .mpk 安装(本期接口/CLI + 文档;UI 后期):解压 .mpk → MeowAPPs//。 - * 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 .mpk。 - * 同名先备份;解压后校验 app.json;随后数据 init(MeowData//{data,cache})。 - * 返回 0 ok / 负=错误。 */ -int meow_pkg_install(const char *mpk_path) -{ - if (!mpk_path || access(mpk_path, R_OK) != 0) { logmsg("pkg_install: bad mpk %s", mpk_path?mpk_path:"(null)"); return -1; } - if (!sd_device_present()) { logmsg("pkg_install: no card"); return -1; } - if (strpbrk(mpk_path, " ;`$'\"&|<>") != NULL) { logmsg("pkg_install: unsafe path"); return -2; } - /* 包名 = 文件名茎(去掉 .mpk) */ - const char *bn = strrchr(mpk_path, '/'); - bn = bn ? bn + 1 : mpk_path; - const char *dot = strrchr(bn, '.'); - char pkg[64]; - if (!dot || strcmp(dot, ".mpk") != 0) { logmsg("pkg_install: need .mpk"); return -1; } - int len = (int)(dot - bn); - if (len <= 0 || len >= (int)sizeof pkg) { logmsg("pkg_install: bad pkg name"); return -1; } - memcpy(pkg, bn, len); pkg[len] = 0; - /* 目标 MeowAPPs//;同名先备份(.bak_),再解压,再校验 app.json */ - char dst[256], bak[256], cmd[760]; - snprintf(dst, sizeof dst, "/usr/data/mnt/sd_0/MeowAPPs/%s", pkg); - snprintf(bak, sizeof bak, "%s.bak_%d", dst, (int)getpid()); - int replacing = (access(dst, F_OK) == 0); - - /* II 期:签名分级先于解包(MPK_SIGNATURE_DESIGN §5.2:先验签再解包,失败不落盘)。 - * 有签名必须验签(失败拒装);os.meow.* 必须有有效签名;无签名第三方放行(告警)。 */ - int vr = pkg_verify_sig(mpk_path, pkg); - int sysp = (strncmp(pkg, "os.meow.", 8) == 0); - if (vr == 1) { logmsg("pkg_install: sig invalid, reject %s", pkg); return -3; } - else if (vr == 2) { - if (sysp) { logmsg("pkg_install: sys pkg %s must be signed, rejected", pkg); return -3; } - logmsg("pkg_install: %s unsigned (3rd party allowed)", pkg); - } - else if (vr != 0) { - if (sysp) { logmsg("pkg_install: meow_verify unavailable rc=%d, sys pkg reject", vr); return -3; } - logmsg("pkg_install: meow_verify unavailable rc=%d, %s unverified (3rd party)", vr, pkg); - } - - /* 防降级门禁(与 nyai 同规范,APPS.md §3.8):仅按 version_code 比较。 - * 新版 code < 已装 → 拒;相等 → 允许重装;任一侧缺 version_code → 拒。 */ - if (replacing) { - char nc[32], oc[32]; - long long ncode = -1, ocode = -1; - int nok = 0, ok2 = 0; - if (pkg_json_field_mpk(mpk_path, "version_code", nc, sizeof nc) == 0) { - char *ep; long long v = strtoll(nc, &ep, 10); - if (ep != nc && *ep == '\0') { ncode = v; nok = 1; } - } - char instj[280]; - snprintf(instj, sizeof instj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg); - if (pkg_json_field(instj, "version_code", oc, sizeof oc) == 0) { - char *ep; long long v = strtoll(oc, &ep, 10); - if (ep != oc && *ep == '\0') { ocode = v; ok2 = 1; } - } - if (!nok || !ok2) { logmsg("pkg_install: %s version_code missing, reject", pkg); return -4; } - if (ncode < ocode) { logmsg("pkg_install: downgrade %s new %lld < old %lld, reject", pkg, ncode, ocode); return -4; } - } - - /* BusyBox tar 无 -z:用 gzip -dc | tar -x 处理 tar.gz。 */ - snprintf(cmd, sizeof cmd, - "rm -rf '%s' 2>/dev/null; " - "[ -d '%s' ] && mv '%s' '%s' 2>/dev/null; " - "mkdir -p '%s' && gzip -dc '%s' | tar -x -C '%s' 2>/dev/null && " - "[ -f '%s/app.json' ]", bak, dst, dst, bak, dst, mpk_path, dst, dst); - if (system(cmd) != 0) { logmsg("pkg_install: extract/validate fail for %s", pkg); return -1; } - - /* II 期:app.json.package 绑定(== 包名,禁改包名/目录改名;防第三方面具系统包)。 */ - { - char appj[280], ph[64]; - snprintf(appj, sizeof appj, "/usr/data/mnt/sd_0/MeowAPPs/%s/app.json", pkg); - if (pkg_json_field(appj, "package", ph, sizeof ph) == 0 && strcmp(ph, pkg) != 0) { - logmsg("pkg_install: app.json.package %s != %s, reject", ph, pkg); - char rb[760]; - snprintf(rb, sizeof rb, "rm -rf '%s' 2>/dev/null; [ -d '%s' ] && mv '%s' '%s' 2>/dev/null", - dst, bak, bak, dst); - system(rb); - return -3; - } - } - - /* 数据 init:MeowData//{data,cache} */ - snprintf(cmd, sizeof cmd, "mkdir -p '/usr/data/mnt/sd_0/MeowData/%s/data' " - "'/usr/data/mnt/sd_0/MeowData/%s/cache' 2>/dev/null", pkg, pkg); - system(cmd); - /* 有效签 → 原签 .mpk 另存 MeowData//data/.mpk.src(导出保留签名) */ - if (vr == 0) pkg_save_src(mpk_path, pkg); - logmsg("pkg_install: %s installed (old backed up %s)", pkg, bak); - return 0; -} - - /* .mpk 导出:打包 MeowAPPs//(数据目录本就由 env 指向 MeowData,MeowAPPs/ 下无 * data/cache 实体,直接全打包)→ .mpk。返回 0 ok。 */ int meow_pkg_export(const char *pkg, const char *out_path) diff --git a/patch_v2/meow_launcherd/build_launcher.sh b/patch_v2/meow_launcherd/build_launcher.sh index 0fd9f60..9352cbd 100755 --- a/patch_v2/meow_launcherd/build_launcher.sh +++ b/patch_v2/meow_launcherd/build_launcher.sh @@ -24,7 +24,7 @@ SYSROOT="$ROOT/.tools/mipsel-linux-muslsf-cross/mipsel-linux-muslsf" [ -f "../sdk/meow_ui.a" ] || { echo "!! 缺 ../sdk/meow_ui.a,先跑 sdk/build_sdk.sh"; exit 1; } "$CC" -I"../sdk" -Os -static -Wall -Wextra \ main_menu.c statusbar.c fb_compositor.c settings_page.c power_menu.c \ - notif_center.c app_launcher.c audio.c backlight.c net.c about.c \ + notif_center.c app_launcher.c pkg_install.c audio.c backlight.c net.c about.c \ subpage.c wifi_page.c bt_page.c netmgr_ipc.c now_playing.c bg_page.c \ font_theme.c keys.c util.c meow_dep.c key_router.c \ meow_touch.c meow_settings.c meow_notif.c meow_media.c meow_bgplay.c \ diff --git a/patch_v2/meow_launcherd/main_menu.c b/patch_v2/meow_launcherd/main_menu.c index 1636111..3ec8a30 100644 --- a/patch_v2/meow_launcherd/main_menu.c +++ b/patch_v2/meow_launcherd/main_menu.c @@ -2132,7 +2132,7 @@ static void usage(void) " [--sim-exit-tap N] [--sim-power-hold N]\n" " [--sim-power-pick N] [--sim-no-exec]\n" " [--dumpkeys N] [--scan] [--sysdump]\n" - " [--pkg-install ] [--pkg-export ]\n" + " [--pkg-install ] [--pkg-export ]\n" " [--ipc-only N] [--ipc-sock ] [--appinfo ]\n" " [--fontscan ]\n"); } @@ -2253,10 +2253,12 @@ int main(int argc, char **argv) } if (pkg_install_mpk) { - /* M2.12 .mpk 安装接口(CLI 钩子;UI 后期)。安全、离开主循环。 */ + /* M42:安装委托 nyai(唯一权威实现;包身份只看包内 app.json,文件名/后缀不参与)。 + * 失败必须给可读原因(nyai stderr 尾行),不能静默;退出码统一 0/1。 */ int r = meow_pkg_install(pkg_install_mpk); printf("pkg_install %s -> %d\n", pkg_install_mpk, r); - return r; + if (r != 0) fprintf(stderr, "pkg_install failed: %s\n", meow_pkg_install_error()); + return r == 0 ? 0 : 1; } if (pkg_export_name && pkg_export_out) { /* M2.12 .mpk 导出接口(CLI 钩子;UI 后期)。 */ diff --git a/patch_v2/meow_launcherd/meow_launcher_internal.h b/patch_v2/meow_launcherd/meow_launcher_internal.h index 12c6568..2cf9ce8 100644 --- a/patch_v2/meow_launcherd/meow_launcher_internal.h +++ b/patch_v2/meow_launcherd/meow_launcher_internal.h @@ -787,7 +787,23 @@ void meow_panel_off(void); void meow_panel_on(void); void meow_parse_app_json(FILE *f, struct app *a); int meow_pkg_export(const char *pkg, const char *out_path); -int meow_pkg_install(const char *mpk_path); +/* ---- M42:包安装(唯一实现 = /usr/bin/nyai,launcherd 只做入口预检 + 异步执行 + 错误文案)---- + * 安装流的解包/清单/footer/验签(meow_verify)/防降级/部署/清理全在 nyai;本模块不解析包内容。 + * nyai 路径 env MEOW_NYAI_BIN 覆写(缺省 /usr/bin/nyai,不依赖 PATH)。 */ +#define MEOW_PKG_E_PATH (-1) /* 入口路径预检失败(文案见 meow_pkg_install_error) */ +#define MEOW_PKG_E_NYAI (-2) /* nyai 不可执行 / pipe/fork 失败 */ +#define MEOW_PKG_E_FAIL (-3) /* nyai 返回非 0 */ +#define MEOW_PKG_E_TIMEOUT (-4) /* 超时并已终止子进程 */ +#define MEOW_PKG_E_BUSY (-5) /* 已有一次安装在跑 */ +#define MEOW_PKG_INSTALL_TIMEOUT_MS 180000 +int meow_pkg_install(const char *path); /* 同步:0=成功;<0=MEOW_PKG_E_* */ +int meow_pkg_install_start(const char *path); /* 异步启动:0=已启动;<0 同左 */ +int meow_pkg_install_poll(int *rc); /* 1=运行中 0=结束(*rc) -1=无任务 */ +int meow_pkg_install_busy(void); /* 1=有安装在跑(UI 据此禁双击) */ +const char *meow_pkg_install_error(void); /* 最近一次失败的可读文案(永不 NULL) */ +/* 纯逻辑(宿主单测 test_pkg_install.c):路径预检 / 退出码→可读文案 */ +int meow_pkg_install_check_path(const char *path, char *err, int n); +void meow_pkg_install_error_text(int rc, const char *detail, char *out, int n); void meow_sys_light(void); void meow_sys_refresh(void); void meow_toggle_panel(void); diff --git a/patch_v2/meow_launcherd/meow_launcherd b/patch_v2/meow_launcherd/meow_launcherd index 90440f7..fb0fb84 100755 Binary files a/patch_v2/meow_launcherd/meow_launcherd and b/patch_v2/meow_launcherd/meow_launcherd differ diff --git a/patch_v2/meow_launcherd/pkg_install.c b/patch_v2/meow_launcherd/pkg_install.c new file mode 100644 index 0000000..7a567ce --- /dev/null +++ b/patch_v2/meow_launcherd/pkg_install.c @@ -0,0 +1,329 @@ +/* pkg_install.c — M42: launcherd 侧包安装入口 = fork+exec `nyai install <包路径>`。 + * + * 背景:M35 把 nyai 改成「包身份以已签名 app.json 清单为准 + SD 暂存 + 必清理」后, + * launcherd 的 meow_pkg_install() 仍是旧内联实现(文件名推包名、硬要 .mpk 后缀、 + * 解压前 rm -rf 目标目录、失败不回滚备份、不拒 . / .. 、系统包大小写敏感比较), + * 会在 launcherd 入口 100% 复现原事故。本模块删掉那套内联解包,改委托 nyai: + * 解包→读清单→footer 一致性→验签(唯一权威 meow_verify)→防降级→rename 部署→清理 + * 全部只有 nyai 一份实现,launcherd 只负责: + * ① 入口路径预检(纯逻辑,宿主可测); + * ② 启动子进程并把 stdout/stderr 分别收进有界缓冲(失败给 UI 可读文案); + * ③ 两段式 start/poll,安装期间调用方(UI 主循环)可按帧驱动、不假死。 + * + * stdout/stderr 必须分开收:nyai 的进度走 stdout(管道 → 全缓冲,进程退出时才刷), + * 错误走 stderr(无缓冲,立即出)。合成一路时「最后一行」会被刷出的旧进度覆盖, + * 真机首测即踩到(降到 code 8 的包报错被「签名校验通过」顶掉)。错误文案只看 stderr。 + * + * nyai 二进制:env MEOW_NYAI_BIN 覆写(宿主测试/非常规部署),缺省 /usr/bin/nyai + * (固件 fw_content.conf 安装点,与 MEOW_VERIFY_BIN 同口径,不依赖 PATH)。 + */ +#include "meow_launcher_internal.h" + +#define NYAI_BIN_DEFAULT "/usr/bin/nyai" +#define OUT_KEEP 1024 /* 每条流保留尾部字节数(够放 nyai 的最后几条信息) */ + +static pid_t g_pid = -1; /* >0 = 安装子进程在跑 */ +static int g_fd_out = -1; /* stdout 读端 */ +static int g_fd_err = -1; /* stderr 读端 */ +static char g_buf_out[OUT_KEEP + 1]; +static int g_len_out; +static char g_buf_err[OUT_KEEP + 1]; +static int g_len_err; +static char g_err[256]; /* 最近一次失败的可读文案 */ + +/* ---------------- 纯逻辑(宿主单测:test_pkg_install.c) ---------------- */ + +/* 取字符串里最后一条非空行(去尾部空白;超长取尾部)。0=有内容 -1=空。 */ +static int last_line_of(const char *s, char *out, int n) +{ + if (!s || !*s || !out || n <= 0) return -1; + size_t L = strlen(s); + while (L > 0 && (s[L-1] == '\n' || s[L-1] == '\r' || s[L-1] == ' ' || s[L-1] == '\t')) L--; + if (L == 0) { out[0] = 0; return -1; } + size_t start = L; + while (start > 0 && s[start-1] != '\n') start--; + size_t len = L - start; + if (len >= (size_t)n) { start += len - (size_t)(n - 1); len = (size_t)(n - 1); } + memcpy(out, s + start, len); + out[len] = 0; + return 0; +} + +/* 入口路径预检:0=可以交给 nyai;<0=拒装(err 填可读文案)。纯逻辑,宿主单测。 + * 只判「有没有这个文件、能不能读、有没有控制字符」——包身份一律由 nyai 从包内 + * app.json 取,**不要求 .mpk 后缀、不看文件名**(M35 定案)。 */ +int meow_pkg_install_check_path(const char *path, char *err, int n) +{ + char dummy[8]; + if (!err || n <= 0) { err = dummy; n = (int)sizeof dummy; } + err[0] = 0; + if (!path || !*path) { snprintf(err, n, "包路径为空"); return -1; } + if (strlen(path) >= 480) { snprintf(err, n, "包路径过长"); return -1; } + for (const char *p = path; *p; p++) { + unsigned char c = (unsigned char)*p; + if (c < 0x20 || c == 0x7f) { snprintf(err, n, "包路径含控制字符"); return -1; } + } + struct stat st; + if (stat(path, &st) != 0) { snprintf(err, n, "找不到包文件 %s", path); return -1; } + if (!S_ISREG(st.st_mode)) { snprintf(err, n, "不是普通文件 %s", path); return -1; } + if (access(path, R_OK) != 0) { snprintf(err, n, "包文件不可读 %s", path); return -1; } + return 0; +} + +/* 退出码 + 输出尾行 → 一条可读文案。rc 可为 nyai 原始退出码(>0 / >=128)或本模块 + * 的 MEOW_PKG_E_* 负码。纯逻辑,宿主单测(文案不许为空、不许越界)。 */ +void meow_pkg_install_error_text(int rc, const char *detail, char *out, int n) +{ + if (!out || n <= 0) return; + out[0] = 0; + if (rc == 0) return; + char line[192]; + if (last_line_of(detail, line, sizeof line) != 0) line[0] = 0; + + if (rc == MEOW_PKG_E_BUSY) { + snprintf(out, n, "已有安装在运行,请稍后再试"); + } else if (rc == MEOW_PKG_E_PATH) { + snprintf(out, n, "%s", line[0] ? line : "安装参数无效"); + } else if (rc == MEOW_PKG_E_NYAI) { + snprintf(out, n, "包管理器 nyai 不可用%s%s", line[0] ? ": " : "", line); + } else if (rc == MEOW_PKG_E_TIMEOUT) { + snprintf(out, n, "安装超时(nyai 未在限时内结束)%s%s", line[0] ? ": " : "", line); + } else if (rc >= 128) { + snprintf(out, n, "安装失败(nyai 被信号 %d 终止)", rc - 128); + } else if (line[0]) { + snprintf(out, n, "%s", line); + } else if (rc > 0) { + snprintf(out, n, "安装失败(nyai 退出码 %d)", rc); + } else { + snprintf(out, n, "安装失败(错误码 %d)", rc); + } +} + +/* ---------------- 输出缓冲 ---------------- */ + +static void buf_put(char *buf, int *len, const char *s, int n) +{ + if (n <= 0) return; + if (n >= OUT_KEEP) { + memcpy(buf, s + n - OUT_KEEP, OUT_KEEP - 1); + buf[OUT_KEEP - 1] = 0; + *len = OUT_KEEP - 1; + return; + } + if (*len + n > OUT_KEEP - 1) { + int drop = *len + n - (OUT_KEEP - 1); + memmove(buf, buf + drop, (size_t)(*len - drop)); + *len -= drop; + } + memcpy(buf + *len, s, (size_t)n); + *len += n; + buf[*len] = 0; +} + +/* 读干一条流当前可用字节;*eof=1 表示写端已关。非阻塞读,调用方按帧驱动。 */ +static void drain_fd(int *fd, char *buf, int *len, int *eof) +{ + if (*fd < 0) { if (eof) *eof = 1; return; } + for (;;) { + char b[256]; + ssize_t r = read(*fd, b, sizeof b); + if (r > 0) { buf_put(buf, len, b, (int)r); continue; } + if (r == 0) { if (eof) *eof = 1; break; } + if (errno == EINTR) continue; + if (errno == EAGAIN || errno == EWOULDBLOCK) break; + if (eof) *eof = 1; + break; + } +} + +/* 子进程退出后把残留数据读干(此时无写端,读空即 EOF),然后关 fd。 */ +static void drain_finish(int *fd, char *buf, int *len) +{ + if (*fd < 0) return; + for (;;) { + char b[256]; + ssize_t r = read(*fd, b, sizeof b); + if (r > 0) { buf_put(buf, len, b, (int)r); continue; } + if (r < 0 && errno == EINTR) continue; + break; + } + close(*fd); + *fd = -1; +} + +/* ---------------- 启动 / 轮询 / 同步封装 ---------------- */ + +const char *meow_pkg_install_error(void) +{ + return g_err; +} + +int meow_pkg_install_busy(void) +{ + return g_pid > 0; +} + +/* 启动一次安装(不等待)。0=已启动;<0=MEOW_PKG_E_*(g_err 已填)。 */ +int meow_pkg_install_start(const char *path) +{ + char perr[256]; + if (g_pid > 0) { + meow_pkg_install_error_text(MEOW_PKG_E_BUSY, NULL, g_err, sizeof g_err); + return MEOW_PKG_E_BUSY; + } + if (meow_pkg_install_check_path(path, perr, sizeof perr) != 0) { + snprintf(g_err, sizeof g_err, "%s", perr); + logmsg("pkg_install: rejected path: %s", perr); + return MEOW_PKG_E_PATH; + } + + const char *bin = getenv("MEOW_NYAI_BIN"); + if (!bin || !*bin) bin = NYAI_BIN_DEFAULT; + if (access(bin, X_OK) != 0) { + snprintf(g_err, sizeof g_err, "包管理器 nyai 不可用: 无法执行 %s", bin); + logmsg("pkg_install: nyai unavailable at %s", bin); + return MEOW_PKG_E_NYAI; + } + + int po[2], pe[2]; + if (pipe(po) != 0) { + snprintf(g_err, sizeof g_err, "安装无法启动: pipe 失败 (%s)", strerror(errno)); + return MEOW_PKG_E_NYAI; + } + if (pipe(pe) != 0) { + close(po[0]); close(po[1]); + snprintf(g_err, sizeof g_err, "安装无法启动: pipe 失败 (%s)", strerror(errno)); + return MEOW_PKG_E_NYAI; + } + /* 读端不进子进程,且轮询按帧读不阻塞主循环。 */ + fcntl(po[0], F_SETFD, FD_CLOEXEC); + fcntl(po[0], F_SETFL, O_NONBLOCK); + fcntl(pe[0], F_SETFD, FD_CLOEXEC); + fcntl(pe[0], F_SETFL, O_NONBLOCK); + + pid_t pid = fork(); + if (pid < 0) { + close(po[0]); close(po[1]); close(pe[0]); close(pe[1]); + snprintf(g_err, sizeof g_err, "安装无法启动: fork 失败 (%s)", strerror(errno)); + logmsg("pkg_install: fork failed"); + return MEOW_PKG_E_NYAI; + } + if (pid == 0) { + /* 子进程:stdout/stderr 分两条管道,再 exec nyai(argv 直传,不经 shell)。 */ + dup2(po[1], 1); + dup2(pe[1], 2); + if (po[1] > 2) close(po[1]); + if (pe[1] > 2) close(pe[1]); + execl(bin, "nyai", "install", path, (char *)0); + { + char m[160]; + int l = snprintf(m, sizeof m, "nyai: 无法执行 %s\n", bin); + if (l > 0) { ssize_t w = write(2, m, (size_t)l); (void)w; } + } + _exit(127); + } + close(po[1]); + close(pe[1]); + g_pid = pid; + g_fd_out = po[0]; + g_fd_err = pe[0]; + g_buf_out[0] = g_buf_err[0] = 0; + g_len_out = g_len_err = 0; + g_err[0] = 0; + logmsg("pkg_install: exec %s install '%s' (pid %d)", bin, path, (int)pid); + return 0; +} + +/* 轮询:1=运行中;0=已结束(*rc = 退出码,信号为 128+sig);-1=没有安装任务。 */ +int meow_pkg_install_poll(int *rc) +{ + if (g_pid <= 0) return -1; + int eo = 0, ee = 0; + drain_fd(&g_fd_out, g_buf_out, &g_len_out, &eo); + drain_fd(&g_fd_err, g_buf_err, &g_len_err, &ee); + if (eo) { close(g_fd_out); g_fd_out = -1; } + if (ee) { close(g_fd_err); g_fd_err = -1; } + + int st = 0; + pid_t w = waitpid(g_pid, &st, WNOHANG); + if (w < 0 && errno == EINTR) return 1; + if (w == 0) return 1; + + drain_finish(&g_fd_out, g_buf_out, &g_len_out); + drain_finish(&g_fd_err, g_buf_err, &g_len_err); + + int code; + if (w < 0) { + code = -1; + } else if (WIFEXITED(st)) { + code = WEXITSTATUS(st); + } else if (WIFSIGNALED(st)) { + code = 128 + WTERMSIG(st); + } else { + code = -1; + } + g_pid = -1; + if (rc) *rc = code; + + if (code != 0) { + /* 错误文案只看 stderr(stdout 是缓冲进度,退出时才刷,会把真错误顶掉); + * stderr 空才退回 stdout 尾行。 */ + meow_pkg_install_error_text(code, g_buf_err[0] ? g_buf_err : g_buf_out, + g_err, sizeof g_err); + logmsg("pkg_install: failed rc=%d (%s)", code, g_err); + } else { + char line[192]; + if (last_line_of(g_buf_out, line, sizeof line) == 0) + logmsg("pkg_install: ok (%s)", line); + else + logmsg("pkg_install: ok"); + } + return 0; +} + +/* 同步安装(CLI 入口用):start + 轮询到结束。0=成功;<0=MEOW_PKG_E_*。 + * 超时默认 180s(MEOW_NYAI_TIMEOUT_MS 覆写,仅测试/调试),超时先 SIGTERM 再 SIGKILL。 + * 轮询切片用固定 20ms usleep 计数,不依赖时钟 ABI(M40:mipsel time32/time64 混用教训)。 */ +int meow_pkg_install(const char *path) +{ + int r = meow_pkg_install_start(path); + if (r != 0) return r; + + int timeout_ms = MEOW_PKG_INSTALL_TIMEOUT_MS; + const char *tv = getenv("MEOW_NYAI_TIMEOUT_MS"); + if (tv && *tv) { int v = atoi(tv); if (v > 0) timeout_ms = v; } + int ticks = timeout_ms / 20; + if (ticks < 1) ticks = 1; + + for (int i = 0; ; i++) { + int rc = 0; + int p = meow_pkg_install_poll(&rc); + if (p == 0) return (rc == 0) ? 0 : MEOW_PKG_E_FAIL; + if (p < 0) { + if (!g_err[0]) snprintf(g_err, sizeof g_err, "安装失败(无安装任务)"); + return MEOW_PKG_E_FAIL; + } + if (i >= ticks) break; + usleep(20000); + } + + /* 超时:SIGTERM 给 nyai 走自身清理(stg_cleanup 的 signal 钩子),2s 后 SIGKILL 兜底。 */ + int finished = 0; + if (g_pid > 0) kill(g_pid, SIGTERM); + for (int i = 0; i < 100; i++) { + int rc = 0; + if (meow_pkg_install_poll(&rc) == 0) { finished = 1; break; } + usleep(20000); + } + if (!finished && g_pid > 0) { + kill(g_pid, SIGKILL); + for (int i = 0; i < 100 && g_pid > 0; i++) { + int rc = 0; + meow_pkg_install_poll(&rc); + if (g_pid > 0) usleep(20000); + } + } + snprintf(g_err, sizeof g_err, "安装超时(超过 %d 秒未完成,已终止)", timeout_ms / 1000); + logmsg("pkg_install: timeout after %d ms, child killed", timeout_ms); + return MEOW_PKG_E_TIMEOUT; +} diff --git a/patch_v2/meow_launcherd/test_pkg_install.c b/patch_v2/meow_launcherd/test_pkg_install.c new file mode 100644 index 0000000..035fde3 --- /dev/null +++ b/patch_v2/meow_launcherd/test_pkg_install.c @@ -0,0 +1,299 @@ +/* test_pkg_install.c — M42 宿主单测:安装入口路径预检 + 错误文案 + exec 合同(假 nyai)。 + * + * 编译运行(在 patch_v2/meow_launcherd 下,需 -I../sdk 因内部头含 SDK 声明): + * gcc -Wall -Wextra -Os -I../sdk test_pkg_install.c pkg_install.c -o test_pkg_install && ./test_pkg_install + * 全 PASS -> exit 0。 + * + * 覆盖: + * A. 路径预检:任意文件名/后缀可装(M35 定案:身份只来自包内 app.json)、目录/缺失/不可读/ + * 控制字符/超长一律拒。 + * B. 错误文案:nyai 退出码 + 输出尾行 → 单条可读文案(不空、不越界)。 + * C. exec 合同:argv 直传(不经 shell,带空格/引号的路径原样到达)、退出码映射、 + * start/poll 非阻塞(start 立即返回)、并发拒装、超时kill。 + * 假 nyai 用 MEOW_NYAI_BIN 注入(与 MEOW_VERIFY_BIN 同口径),不碰真机/真包。 + */ +#include +#include +#include +#include +#include +#include +#include +#include "meow_launcher_internal.h" + +/* pkg_install.c 依赖的 launcher 日志:宿主打桩(宿主无 util.c)。 */ +void logmsg(const char *fmt, ...) +{ + (void)fmt; +} + +static int g_pass = 0, g_fail = 0; +static void check(const char *name, int ok) +{ + if (ok) { g_pass++; printf("PASS %s\n", name); } + else { g_fail++; printf("FAIL %s\n", name); } +} + +static long long now_ms_mono(void) +{ + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static char g_dir[512]; + +/* 在临时目录写一个文件,返回其路径(buf)。 */ +static const char *mkfile(const char *name, const char *content, char *buf, int n) +{ + snprintf(buf, n, "%s/%s", g_dir, name); + FILE *f = fopen(buf, "w"); + if (!f) return NULL; + if (content) fputs(content, f); + fclose(f); + return buf; +} + +/* 写一个可执行假 nyai。 */ +static const char *mkscript(const char *name, const char *body, char *buf, int n) +{ + if (!mkfile(name, body, buf, n)) return NULL; + chmod(buf, 0755); + return buf; +} + +/* ---------------- A. 路径预检 ---------------- */ +static void test_path_check(void) +{ + char err[256], f[512]; + + check("A1 null -> reject", meow_pkg_install_check_path(NULL, err, sizeof err) != 0); + check("A1 null -> has text", err[0] != 0); + err[0] = 0; + check("A2 empty -> reject", meow_pkg_install_check_path("", err, sizeof err) != 0 && err[0]); + err[0] = 0; + check("A3 missing -> reject", meow_pkg_install_check_path("/nonexistent/x.mpk", err, sizeof err) != 0); + check("A3 missing -> 找不到", strstr(err, "找不到") != NULL); + err[0] = 0; + check("A4 directory -> reject", meow_pkg_install_check_path(g_dir, err, sizeof err) != 0); + check("A4 directory -> 不是普通文件", strstr(err, "不是普通文件") != NULL); + err[0] = 0; + check("A5 dot -> reject", meow_pkg_install_check_path(".", err, sizeof err) != 0); + check("A6 dotdot -> reject", meow_pkg_install_check_path("..", err, sizeof err) != 0); + err[0] = 0; + check("A7 control char -> reject", + meow_pkg_install_check_path("/tmp/a\x01" "b", err, sizeof err) != 0 && + strstr(err, "控制字符") != NULL); + { + char longp[700]; + memset(longp, 'a', sizeof longp - 1); + longp[0] = '/'; longp[sizeof longp - 1] = 0; + err[0] = 0; + check("A8 too long -> reject", + meow_pkg_install_check_path(longp, err, sizeof err) != 0 && + strstr(err, "过长") != NULL); + } + + /* 关键回归:任意文件名/无 .mpk 后缀都可装(m35 事故:改名后装不上)。 */ + if (mkfile("weird name-v0.1.7.bin", "x", f, sizeof f)) + check("A9 any name/no .mpk -> accept", + meow_pkg_install_check_path(f, err, sizeof err) == 0); + else check("A9 any name/no .mpk -> accept", 0); + if (mkfile("OS.MEOW.READER", "x", f, sizeof f)) + check("A10 uppercase name -> accept (nyai 拒,入口不按名字判)", + meow_pkg_install_check_path(f, err, sizeof err) == 0); + else check("A10 uppercase name -> accept (nyai 拒,入口不按名字判)", 0); + + if (mkfile("noread.bin", "x", f, sizeof f)) { + chmod(f, 0000); + err[0] = 0; + check("A11 unreadable -> reject", + meow_pkg_install_check_path(f, err, sizeof err) != 0 && + strstr(err, "不可读") != NULL); + chmod(f, 0644); + } else check("A11 unreadable -> reject", 0); +} + +/* ---------------- B. 错误文案 ---------------- */ +static void test_error_text(void) +{ + char out[256]; + + meow_pkg_install_error_text(0, "whatever", out, sizeof out); + check("B1 rc=0 -> empty", out[0] == 0); + + meow_pkg_install_error_text(1, + "Reading package lists... Done\r\nnyai: install: 不允许降级: 新版本 1 < 已装 2\n\n", + out, sizeof out); + check("B2 last non-empty line", strcmp(out, "nyai: install: 不允许降级: 新版本 1 < 已装 2") == 0); + + meow_pkg_install_error_text(7, "", out, sizeof out); + check("B3 no output -> exit code text", strstr(out, "退出码 7") != NULL); + + meow_pkg_install_error_text(137, "x", out, sizeof out); + check("B4 signal -> 128+sig text", strstr(out, "信号 9") != NULL); + + meow_pkg_install_error_text(MEOW_PKG_E_TIMEOUT, NULL, out, sizeof out); + check("B5 timeout text", strstr(out, "超时") != NULL); + + meow_pkg_install_error_text(MEOW_PKG_E_NYAI, "无法执行 /usr/bin/nyai", out, sizeof out); + check("B6 nyai unavailable text", strstr(out, "不可用") && strstr(out, "/usr/bin/nyai")); + + meow_pkg_install_error_text(MEOW_PKG_E_BUSY, NULL, out, sizeof out); + check("B7 busy text", strstr(out, "已有安装") != NULL); + + meow_pkg_install_error_text(MEOW_PKG_E_PATH, "找不到包文件 /x", out, sizeof out); + check("B8 path text", strcmp(out, "找不到包文件 /x") == 0); + + { + char tiny[24]; + memset(tiny, 0x5a, sizeof tiny); + meow_pkg_install_error_text(1, + "nyai: 这是一个非常非常长的错误信息,用来验证截断不会越界写坏栈\n", tiny, 16); + int guard = 1; + for (int i = 16; i < (int)sizeof tiny; i++) if (tiny[i] != 0x5a) guard = 0; + check("B9 truncation bounded", strlen(tiny) < 16 && guard); + } + meow_pkg_install_error_text(1, " \n\t\n", out, sizeof out); + check("B10 whitespace-only detail -> generic", out[0] != 0 && strstr(out, "退出码 1")); +} + +/* ---------------- C. exec 合同(假 nyai) ---------------- */ +static void test_exec(void) +{ + char script[512], pkg[512], log[512], err[256]; + + /* C1: 失败透传 —— stderr 尾行进错误文案,退出码映射 MEOW_PKG_E_FAIL */ + if (mkscript("nyai_fail.sh", + "#!/bin/sh\n" + "echo \"Reading package lists... Done\"\n" + "echo \"nyai: 假失败:测试包损坏\" >&2\n" + "exit 7\n", script, sizeof script) && + mkfile("pkg-fail.bin", "x", pkg, sizeof pkg)) { + setenv("MEOW_NYAI_BIN", script, 1); + int r = meow_pkg_install(pkg); + check("C1 exit!=0 -> MEOW_PKG_E_FAIL", r == MEOW_PKG_E_FAIL); + check("C1 stderr tail surfaced", strstr(meow_pkg_install_error(), "假失败") != NULL); + check("C1 not busy after finish", meow_pkg_install_busy() == 0); + } else check("C1 exit!=0 -> MEOW_PKG_E_FAIL", 0); + + /* C1b: 真机回归 —— nyai 的 stdout 是管道全缓冲(退出时才刷),错误走 stderr 先到。 + * 合成一路时「最后一行」会把真错误顶成旧进度;错误文案必须取 stderr。 */ + if (mkscript("nyai_buforder.sh", + "#!/bin/sh\n" + "echo \"nyai: install: 不允许降级: 新版本 code 8 < 已装 code 9\" >&2\n" + "echo \"Reading package lists... Done\"\n" + "echo \"nyai: 签名校验通过 (package=os.meow.reader)\"\n" + "exit 1\n", script, sizeof script) && + mkfile("pkg-buforder.bin", "x", pkg, sizeof pkg)) { + setenv("MEOW_NYAI_BIN", script, 1); + int r = meow_pkg_install(pkg); + check("C1b fail rc", r == MEOW_PKG_E_FAIL); + check("C1b stderr beats buffered stdout", + strstr(meow_pkg_install_error(), "不允许降级") != NULL && + strstr(meow_pkg_install_error(), "签名校验通过") == NULL); + } else check("C1b stderr beats buffered stdout", 0); + + /* C2: argv 直传(不经 shell):带空格/单引号的路径原样到 $2;$1 必须是 install */ + snprintf(log, sizeof log, "%s/argv.log", g_dir); + if (mkscript("nyai_argv.sh", + "#!/bin/sh\n" + "printf '%s|%s\\n' \"$1\" \"$2\" > \"$M42_ARGV_LOG\"\n" + "exit 0\n", script, sizeof script) && + mkfile("name with space 'q'.bin", "x", pkg, sizeof pkg)) { + setenv("MEOW_NYAI_BIN", script, 1); + setenv("M42_ARGV_LOG", log, 1); + int r = meow_pkg_install(pkg); + check("C2 success rc=0", r == 0); + FILE *f = fopen(log, "r"); + char line[600] = ""; + if (f) { if (!fgets(line, sizeof line, f)) line[0] = 0; fclose(f); } + char want[700]; + snprintf(want, sizeof want, "install|%s\n", pkg); + check("C2 argv passed verbatim (no shell)", strcmp(line, want) == 0); + unsetenv("M42_ARGV_LOG"); + } else check("C2 success rc=0", 0); + + /* C3: start 立即返回 + 并发拒装(UI 不假死的关键合同) */ + if (mkscript("nyai_slow.sh", "#!/bin/sh\nsleep 0.4\nexit 0\n", script, sizeof script) && + mkfile("pkg-slow.bin", "x", pkg, sizeof pkg)) { + setenv("MEOW_NYAI_BIN", script, 1); + long long t0 = now_ms_mono(); + int r = meow_pkg_install_start(pkg); + long long dt = now_ms_mono() - t0; + check("C3 start returns fast", r == 0 && dt < 150); + check("C3 busy while running", meow_pkg_install_busy() == 1); + int r2 = meow_pkg_install_start(pkg); + check("C3 second start -> BUSY", r2 == MEOW_PKG_E_BUSY); + check("C3 busy error text", strstr(meow_pkg_install_error(), "已有安装") != NULL); + int rc = -1, p = 1, spins = 0; + while ((p = meow_pkg_install_poll(&rc)) == 1 && spins++ < 500) usleep(10000); + check("C3 poll finish rc=0", p == 0 && rc == 0); + check("C3 no longer busy", meow_pkg_install_busy() == 0); + } else check("C3 start returns fast", 0); + + /* C4: 超时 —— 子进程被终止,返回 TIMEOUT 且不拖住调用方 */ + if (mkscript("nyai_hang.sh", "#!/bin/sh\nexec sleep 5\n", script, sizeof script) && + mkfile("pkg-hang.bin", "x", pkg, sizeof pkg)) { + setenv("MEOW_NYAI_BIN", script, 1); + setenv("MEOW_NYAI_TIMEOUT_MS", "100", 1); + long long t0 = now_ms_mono(); + int r = meow_pkg_install(pkg); + long long dt = now_ms_mono() - t0; + check("C4 timeout -> MEOW_PKG_E_TIMEOUT", r == MEOW_PKG_E_TIMEOUT); + check("C4 timeout bounded (<3s)", dt < 3000); + check("C4 timeout text", strstr(meow_pkg_install_error(), "超时") != NULL); + unsetenv("MEOW_NYAI_TIMEOUT_MS"); + } else check("C4 timeout -> MEOW_PKG_E_TIMEOUT", 0); + + /* C5: nyai 不存在 → 明确报错,不回落旧内联实现、不静默 */ + setenv("MEOW_NYAI_BIN", "/nonexistent/nyai", 1); + if (mkfile("pkg-ok.bin", "x", pkg, sizeof pkg)) { + err[0] = 0; + int r = meow_pkg_install(pkg); + check("C5 missing nyai -> MEOW_PKG_E_NYAI", r == MEOW_PKG_E_NYAI); + check("C5 missing nyai -> readable", strstr(meow_pkg_install_error(), "不可用") != NULL); + } else check("C5 missing nyai -> MEOW_PKG_E_NYAI", 0); + + /* C6: nyai 存在但不可执行 → 同 E_NYAI */ + if (mkfile("nyai_noexec.sh", "#!/bin/sh\nexit 0\n", script, sizeof script) && + mkfile("pkg-ok2.bin", "x", pkg, sizeof pkg)) { + chmod(script, 0644); + setenv("MEOW_NYAI_BIN", script, 1); + check("C6 non-executable nyai -> MEOW_PKG_E_NYAI", + meow_pkg_install(pkg) == MEOW_PKG_E_NYAI); + chmod(script, 0755); + } else check("C6 non-executable nyai -> MEOW_PKG_E_NYAI", 0); + + /* C7: 入口路径预检先于 exec(坏路径不启动子进程) */ + unsetenv("MEOW_NYAI_BIN"); + check("C7 bad path -> MEOW_PKG_E_PATH", + meow_pkg_install("/nonexistent/x.mpk") == MEOW_PKG_E_PATH); +} + +int main(void) +{ + const char *tmp = getenv("TMPDIR"); + if (!tmp || !*tmp) tmp = "/tmp"; + snprintf(g_dir, sizeof g_dir, "%s/m42pkg-XXXXXX", tmp); + if (!mkdtemp(g_dir)) { + /* 退路:当前目录(宿主 CI 也能跑) */ + snprintf(g_dir, sizeof g_dir, "./m42pkg-XXXXXX"); + if (!mkdtemp(g_dir)) { printf("FAIL cannot create temp dir\n"); return 1; } + } + printf("tmp=%s\n", g_dir); + + test_path_check(); + test_error_text(); + test_exec(); + + /* 清理(先恢复文件权限,避免删不掉) */ + { + char cmd[700]; + snprintf(cmd, sizeof cmd, "chmod -R u+rwx '%s' 2>/dev/null; rm -rf '%s'", g_dir, g_dir); + if (system(cmd) != 0) printf("warn: cleanup %s failed\n", g_dir); + } + + printf("\n%d PASS, %d FAIL\n", g_pass, g_fail); + return g_fail == 0 ? 0 : 1; +} diff --git a/scripts/build/fw_content.conf b/scripts/build/fw_content.conf index c3c6345..a23cd42 100644 --- a/scripts/build/fw_content.conf +++ b/scripts/build/fw_content.conf @@ -28,7 +28,7 @@ # ---- 用户态/服务守护二进制(usr/bin,全部 chmod 755 静态 MIPS)---- patch_v2/bootmenu/meow_bootmenu|/usr/bin/meow_bootmenu|4441822d87e76bfce2ce584604ac3830|引导选单 M6.25(开机菜单/第二屏亮度按用户设置恢复:显示前读 settings.conf 写回,方案A) -patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|605e13a9d154d179f4131e0c5e85c90f|M40 电源键/锁屏链修复(App 前台电源菜单补齐 §10.5:notif 全抑制+帧恢复+SIGCONT 排空+标记翻回,关闭后不再停留菜单画面;show_power_menu 恢复进入前背景帧+重绘挪到首轮按键采样后;activity 标记排空窗+会话入口清理+manual_off 去单向闩锁;锁屏电源改按下边沿+进场 300ms,熄屏退出也恢复底层帧;SIGCONT 前写 /tmp/meow_app_repaint), 链 meow_ui.a 86800633;继承 M39 点击输入门(M39 触摸守卫/MENU_TOUCH_GUARD_MS 150ms + SDK 启动排空);继承 M6.46 杀后台修复+SDK 时钟 ABI 修复(M11/M40);0warning +patch_v2/meow_launcherd/meow_launcherd|/usr/bin/meow_launcherd|cea0a73514f28f2aca419fe7835f78d6|M42 包安装入口改委托 nyai(删 app_launcher.c 内联解包/验签/部署,新增 pkg_install.c:fork+exec /usr/bin/nyai install <路径>,包身份只来自包内 app.json、不强制 .mpk 后缀、SD 暂存+rename+必清理/拒 ./..、大小写绕过全由 nyai 一站闭合;MEOW_NYAI_BIN 覆写;stdout/stderr 分流抓取取 stderr 尾行做可读错误;start/poll 两段式不阻塞主循环+180s 超时;CLI 失败退 1 并打 stderr 文案), 链 meow_ui.a 86800633;继承 M40 电源键/锁屏链修复(App 前台电源菜单补齐 §10.5:notif 全抑制+帧恢复+SIGCONT 排空+标记翻回,关闭后不再停留菜单画面;show_power_menu 恢复进入前背景帧+重绘挪到首轮按键采样后;activity 标记排空窗+会话入口清理+manual_off 去单向闩锁;锁屏电源改按下边沿+进场 300ms,熄屏退出也恢复底层帧;SIGCONT 前写 /tmp/meow_app_repaint);继承 M39 点击输入门(M39 触摸守卫/MENU_TOUCH_GUARD_MS 150ms + SDK 启动排空);继承 M6.46 杀后台修复+SDK 时钟 ABI 修复(M11/M40);0warning patch_v2/meow_watchdog/meow_watchdog|/usr/bin/meow_watchdog|37c57e0feb9cd220978bfd5deff75a1f|应用看门狗 M40:o32 time32 取时/睡眠(修 held 天文数字→单击误判长按)、电源阈值读 power.longpress_menu_ms/forceoff_ms(缺省 3000/8000,与 launcher 主屏统一);继承 M20 亮屏亮度按 settings 恢复(四支决策不再回退固定 60) patch_v2/meow_svcd/meow_svcd|/usr/bin/meow_svcd|605646c4cc93f2baf29071ea6bc870ea|服务管理器 M2.77(+崩溃上报 meow_crash agent-187) mips-repo/bins/meow_mdns/bin/meow_mdns|/usr/bin/meow_mdns|d6b0151bda26768f19952dae1342610d|mDNS 应答器 M2.78(启动竞态+换IP重检 agent-194)