Files
MeowOS-For-R3P2/patch_v2/meow_launcherd/pkg_install.c
T
AskaEth 58cdb73bf8 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 同步钉值。
2026-11-11 16:37:00 +08:00

330 lines
13 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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;
}