- 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 同步钉值。
330 lines
13 KiB
C
330 lines
13 KiB
C
/* 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;
|
||
}
|