Merge branch 'feat/m42-launcherd-pkg-install-to-nyai'

This commit is contained in:
AskaEth committed 2026-11-11 17:08:54 +08:00
commit 3f22158fc2
8 files changed
+654 -108

No files matched your search

+2 -102
View File
@@ -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/<pkg>/data/<pkg>.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/<pkg>/。
* 约定 .mpk = tar.gz,顶层即包目录(含 app.json);包名 = 文件名茎 <pkg>.mpk。
* 同名先备份;解压后校验 app.json;随后数据 init(MeowData/<pkg>/{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 <pkg>.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/<pkg>/;同名先备份(.bak_<pid>),再解压,再校验 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/<pkg>/{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/<pkg>/data/<pkg>.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/<pkg>/(数据目录本就由 env 指向 MeowData,MeowAPPs/<pkg> 下无
* data/cache 实体,直接全打包)→ <pkg>.mpk。返回 0 ok。 */
int meow_pkg_export(const char *pkg, const char *out_path)
+1 -1
View File
@@ -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 \
+5 -3
View File
@@ -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.mpk>] [--pkg-export <pkg> <out.mpk>]\n"
" [--pkg-install <file>] [--pkg-export <pkg> <out.mpk>]\n"
" [--ipc-only N] [--ipc-sock <path>] [--appinfo <app.json>]\n"
" [--fontscan <card_root>]\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 后期)。 */
@@ -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);
Binary file not shown.
+329
View File
@@ -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;
}
+299
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <sys/stat.h>
#include <sys/types.h>
#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;
}
+1 -1
View File
@@ -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)