/* 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; }