fix(launcherd): M42 包安装入口改委托 nyai — 删内联解包/验签/部署,复用 M35 安全链

- app_launcher.c 删掉 meow_pkg_install 旧内联实现(文件名推包名、硬要 .mpk 后缀、
  rm -rf 目标目录直解、失败不回滚 .bak_、不拒 ./..、系统包大小写敏感比较);
  导出保留签名逻辑不动(验签仍只 shell-out meow_verify)。
- 新增 pkg_install.c:fork+exec /usr/bin/nyai install <路径>(argv 直传不经 shell;
  MEOW_NYAI_BIN 覆写,缺省不走 PATH)。包身份/暂存/清理/防降级/大小写全由 nyai 闭环。
- stdout/stderr 分两条管道收:nyai 进度是缓冲 stdout,错误是即时 stderr,合流会把
  真错误顶掉(真机首测即踩到);错误文案只取 stderr 尾行。
- start/poll 两段式(+busy 查询)供 UI 主循环按帧驱动不假死;同步封装 20ms 切片轮询,
  180s 超时 SIGTERM→SIGKILL,不用时钟 ABI(M40 time32 教训)。
- main_menu --pkg-install 失败打 stderr 可读文案、退出码统一 0/1。
- test_pkg_install.c:44 断言(路径预检含任意文件名可装/目录/不可读/控制字符/超长、
  错误文案矩阵、argv 直传、start 立即返回、并发拒装、超时、nyai 缺失)。
- 重链 meow_launcherd 894940B/cea0a735(链 meow_ui.a 86800633,0 warning);
  fw_content.conf 同步钉值。
This commit is contained in:
AskaEth committed 2026-11-11 16:37:00 +08:00
1 parent ebd64d07e1
commit 58cdb73bf8
8 files changed
+654 -108

No files matched your search

+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;
}