From 71eb33b756da32f7dde4a518e00fbc6bd39066d4 Mon Sep 17 00:00:00 2001 From: JGZYES Date: Sat, 5 Sep 2026 20:44:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20pmm=20doctor=20/=20diagnose=20=E2=80=94?= =?UTF-8?q?=20one-shot=20health=20check=20with=20actionable=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Checks and prints fixes for the common 'installed but not effective' cases: - which binary pmm resolves to vs the running one (PATH/hash -r). - Windows /mnt leaks in PATH (a Windows-side pmm inherited by WSL). - each registry mirror's reachability (flags a broken default mirror). - installed-package integrity (missing main binary under root/bin). - stale language pack (missing new i18n keys -> re-run setting lang). Read-only; returns non-zero when issues are found. Also adds desc.doctor / msg.doctor-issues keys and syncs the zh-CN/en-US language packs. --- src/i18n.c | 4 +- src/main.c | 169 ++++++++++++++++++++++++++++++++++++ web/mirror/lang/en-US.pjson | 4 +- web/mirror/lang/zh-CN.pjson | 4 +- 4 files changed, 177 insertions(+), 4 deletions(-) diff --git a/src/i18n.c b/src/i18n.c index 91c3e92..663fac4 100644 --- a/src/i18n.c +++ b/src/i18n.c @@ -54,7 +54,7 @@ static const char *kBuiltinZh = "\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\"," "\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %s\"," "\"msg.checksum-ok\":\"%s 校验 OK: %s\",\"msg.checksum-mismatch\":\"校验不匹配 (%s)!\"," -"\"msg.resolving-dep\":\"正在解析依赖 %s%s%s\",\"msg.dep-satisfied\":\"依赖 %s%s%s 已装/被提供,跳过\",\"msg.registry-ok\":\"注册表 sha256 OK: %s\",\"msg.cache-cleared\":\"已清理缓存: %d 个文件,释放 %.1f KB\",\"msg.self-up-to-date\":\"当前 v%s 已是最新(v%s),跳过升级\"," +"\"msg.resolving-dep\":\"正在解析依赖 %s%s%s\",\"msg.dep-satisfied\":\"依赖 %s%s%s 已装/被提供,跳过\",\"msg.registry-ok\":\"注册表 sha256 OK: %s\",\"msg.cache-cleared\":\"已清理缓存: %d 个文件,释放 %.1f KB\",\"msg.doctor-issues\":\"doctor: 发现 %d 个问题\",\"msg.self-up-to-date\":\"当前 v%s 已是最新(v%s),跳过升级\"," "\"msg.path-exists\":\"PATH 已包含 PMM 目录\",\"msg.err.registry-not-found\":\"找不到软件包 '%s'\"," "\"msg.pmm-self-replaced\":\"pmm 已更新到 %s\",\"msg.pmm-self-hint\":\"无法覆盖 %s(需 root/管理员);新版本在 %s。请将其加入 PATH 并在当前 shell 运行 hash -r\"," "\"msg.err.usage\":\"用法\",\"msg.err.unknown-cmd\":\"未知命令 '%s' (试试 'pmm help')\"," @@ -63,7 +63,7 @@ static const char *kBuiltinZh = "\"msg.err.no-registry-mirror\":\"没有配置注册表镜像\",\"msg.err.cannot-write\":\"无法写入 %s\"," "\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\"," "\"desc.upgrade\":\"升级\",\"desc.mirror\":\"镜像源\",\"desc.search\":\"搜索\"," -"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.clean\":\"清理缓存\",\"desc.help\":\"帮助\"," +"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.clean\":\"清理缓存\",\"desc.doctor\":\"诊断\",\"desc.help\":\"帮助\"," "\"opt.p-drive\":\"安装盘符\",\"opt.no-color\":\"禁用颜色\",\"opt.quiet\":\"静默\"," "\"opt.verbose\":\"详情\"}"; diff --git a/src/main.c b/src/main.c index fab6bcb..48f4ccb 100644 --- a/src/main.c +++ b/src/main.c @@ -878,6 +878,171 @@ static char *registry_latest_version(const char *pkg) { return out; } +/* pmm doctor / diagnose — one-shot health check that prints actionable fixes + * for the common "install didn't take effect" issues: which binary resolves, + * PATH ordering / Windows leak, a flaky mirror, installed-package integrity and + * a stale language pack. Read-only: it never changes anything. */ +static int cmd_doctor(void) { + char home[1024]; + pmm_config_dir(home, sizeof(home)); + int issues = 0; + + /* ---- 1. which binary does `pmm` resolve to & is it the running one ---- */ + pmm_info("— 二进制解析 —\n"); + const char *self = pmm_self_path(); + printf(" 当前运行: %s\n", self && *self ? self : "(unknown)"); + char resolved[1400] = ""; +#ifndef _WIN32 + FILE *f = popen("command -v pmm 2>/dev/null", "r"); + if (f) { if (fgets(resolved, sizeof(resolved), f)) { size_t l = strlen(resolved); + while (l && (resolved[l-1]=='\n' || resolved[l-1]=='\r')) resolved[--l]=0; } pclose(f); } +#else + FILE *f = popen("where pmm 2>nul", "r"); + if (f) { if (fgets(resolved, sizeof(resolved), f)) { size_t l = strlen(resolved); + while (l && (resolved[l-1]=='\n' || resolved[l-1]=='\r')) resolved[--l]=0; } pclose(f); } +#endif + if (resolved[0]) { + printf(" PATH 解析: %s\n", resolved); + if (self && *self && strcmp(resolved, self) == 0) { + printf(" [OK] 命中的就是当前运行的 pmm\n"); + } else { + pmm_warn(" [X] PATH 里另有一个 pmm (%s),而当前运行的是 %s\n", resolved, self ? self : "?"); + printf(" 修复: hash -r;若仍不对,把 %s 所在目录放到 PATH 前面(export PATH=\":$PATH\")\n", + self ? self : ""); + issues++; + } + } else { + pmm_warn(" [X] PATH 中找不到 pmm(请把安装目录加入 PATH)\n"); + issues++; + } + + /* ---- 2. Windows /mnt PATH leak ---- */ + pmm_info("— PATH 顺序 / Windows 泄漏 —\n"); + { + const char *path = getenv("PATH"); + char tmp[16384]; + snprintf(tmp, sizeof(tmp), "%s", path ? path : ""); + int leak = 0; + char *save = NULL; + for (char *t = strtok(tmp, ":"); t; t = strtok(NULL, ":")) { + if (strncmp(t, "/mnt/", 5) == 0 && strstr(t, "pmm")) { + pmm_warn(" [X] PATH 含 Windows 泄漏目录: %s\n", t); + printf(" 说明: Windows 侧装的 pmm 被 WSL 继承。修复: 在 Windows 移除该 PATH 项," + "或将 ~/.local/bin 放到其前面\n"); + leak = 1; issues++; + } + } + if (!leak) printf(" [OK] PATH 未发现 /mnt 下的 pmm 泄漏\n"); + } + + /* ---- 3. mirror reachability ---- */ + pmm_info("— 镜像源可达性 —\n"); + { + char path2[1200]; + snprintf(path2, sizeof(path2), "%s/mirror.ini", home); + Ini *ini = ini_load(path2); + if (!ini || !ini->head) { + pmm_warn(" [X] 未配置镜像 (%s)\n", path2); + printf(" 修复: pmm setting mirror add <名字> ,或用 install.sh 生成默认镜像\n"); + issues++; + } else { + printf(" %s\n", path2); + char lastsec[512] = ""; + int okcount = 0, total = 0, defbad = 0; + for (IniEntry *e = ini->head; e; e = e->next) { + if (!*e->section) continue; + if (strcmp(e->section, lastsec) == 0) continue; + strncpy(lastsec, e->section, sizeof(lastsec)-1); + const char *reg = ini_get(ini, e->section, "registry"); + const char *pri = ini_get(ini, e->section, "priority"); + const char *def = ini_get(ini, e->section, "default"); + if (!reg || !*reg) continue; + total++; + char url[2048]; + snprintf(url, sizeof(url), "%s/packages.json", reg); + int st = 0; + char *body = http_get(url, &st); + int ok = (body && st != 404 && st != 403 && st != 503 && st != 0); + free(body); + int isdef = def && strcmp(def, "true") == 0; + printf(" [%s] pri=%s %s%s %s\n", e->section, pri ? pri : "-", reg, + isdef ? " (default)" : "", ok ? "OK" : st == 404 ? "404/not-found" + : st == 0 ? "unreachable" : "HTTP->" ); + if (ok) okcount++; else if (isdef) defbad = 1; + } + ini_free(ini); + printf(" 结果: %d/%d 可达\n", okcount, total); + if (defbad) { pmm_warn(" [X] 默认/优先镜像不可达 — 修复: pmm setting mirror <健康镜像名>\n"); issues++; } + if (total == 0) { pmm_warn(" [X] 没有可探测的镜像\n"); issues++; } + } + } + + /* ---- 4. installed package integrity ---- */ + pmm_info("— 已装包完整性 —\n"); + { + char db[1200], root[1200]; + snprintf(db, sizeof(db), "%s/installed", home); + snprintf(root, sizeof(root), "%s/root", home); + char files[128][1400]; + int n = list_info_files(db, files, 128); + if (n == 0) { printf(" (无已装包)\n"); } + else { + int bad = 0; + for (int i = 0; i < n; i++) { + char info[4096]; + FILE *fp = fopen(files[i], "rb"); + if (!fp) continue; + size_t g = fread(info, 1, sizeof(info)-1, fp); fclose(fp); info[g] = 0; + char pkg[256], ver[128]; + installed_version(info, pkg, sizeof(pkg), ver, sizeof(ver)); + if (!pkg[0]) continue; + char bin[1400]; + snprintf(bin, sizeof(bin), "%s/bin/%s", root, pkg); + FILE *bf = fopen(bin, "rb"); + if (!bf) { + pmm_warn(" [X] %s %s 缺可执行主文件 %s\n", pkg, ver ? ver : "", bin); + printf(" 修复: pmm remove %s && pmm install %s\n", pkg, pkg); + bad++; issues++; + } else { + fclose(bf); + printf(" [OK] %s %s\n", pkg, ver ? ver : ""); + } + } + if (bad == 0) printf(" (%d 个已装包均完整)\n", n - bad); + } + } + + /* ---- 5. language pack ---- */ + pmm_info("— 语言包 —\n"); + { + char loc[64]; + snprintf(loc, sizeof(loc), "%s", pmm_lang_locale()); + char lp[1400]; + snprintf(lp, sizeof(lp), "%s/lang/%s.pjson", home, loc); + FILE *fp = fopen(lp, "rb"); + if (!fp) { pmm_warn(" [X] 语言包 %s 缺失 — 修复: pmm setting lang %s\n", lp, loc); issues++; } + else { + char buf[65536]; size_t g = fread(buf, 1, sizeof(buf)-1, fp); fclose(fp); buf[g] = 0; + JsonValue *root = json_parse(buf); + if (!root) { pmm_warn(" [X] 语言包 %s 无法解析 — 修复: pmm setting lang %s\n", lp, loc); issues++; } + else { + static const char *probes[] = { "msg.self-up-to-date", "desc.clean", "msg.err.conflict", NULL }; + int miss = 0; + for (int i = 0; probes[i]; i++) if (!json_str(root, probes[i])) miss++; + if (miss) { + pmm_warn(" [X] 语言包偏旧 (%s),缺 %d 个新 key — 修复: pmm setting lang %s\n", lp, miss, loc); + issues++; + } else printf(" [OK] %s\n", lp); + json_free(root); + } + } + } + + printf("\n"); + pmm_success("%s", issues == 0 ? "doctor: 一切正常 ✔" : pmm_tr_fmt("msg.doctor-issues", issues)); + return issues == 0 ? 0 : 1; +} + static void print_help(void) { printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch()); printf("%s\n", pmm_tr("help.usage")); @@ -890,6 +1055,7 @@ static void print_help(void) { printf(" %-32s%s\n", "pmm setting mirror ... ", pmm_tr("desc.mirror")); printf(" %-32s%s\n", "pmm self-update ", pmm_tr("desc.self-update")); printf(" %-32s%s\n", "pmm clean ", pmm_tr("desc.clean")); + printf(" %-32s%s\n", "pmm doctor ", pmm_tr("desc.doctor")); printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help")); printf("\n%s\n", pmm_tr("help.options")); printf(" %-32s%s\n", "-p ", pmm_tr("opt.p-drive")); @@ -1205,6 +1371,9 @@ int main(int argc, char **argv) { if (strcmp(argv[1], "clean") == 0) return cmd_clean(); + if (strcmp(argv[1], "doctor") == 0 || strcmp(argv[1], "diagnose") == 0) + return cmd_doctor(); + if (strcmp(argv[1], "update") == 0) return cmd_update(); diff --git a/web/mirror/lang/en-US.pjson b/web/mirror/lang/en-US.pjson index 68ab6b4..1c28509 100644 --- a/web/mirror/lang/en-US.pjson +++ b/web/mirror/lang/en-US.pjson @@ -128,5 +128,7 @@ "msg.dep-satisfied": "dependency %s%s%s already installed/provided, skipped", "msg.err.conflict": "cannot install %s: conflicts with installed %s", "msg.cache-cleared": "cache cleaned: %d files, freed %.1f KB", - "msg.self-up-to-date": "already up to date (v%s, latest v%s), skipping upgrade" + "msg.self-up-to-date": "already up to date (v%s, latest v%s), skipping upgrade", + "desc.doctor": "diagnose", + "msg.doctor-issues": "doctor: found %d issue(s)" } diff --git a/web/mirror/lang/zh-CN.pjson b/web/mirror/lang/zh-CN.pjson index 13fe0fc..e5eca52 100644 --- a/web/mirror/lang/zh-CN.pjson +++ b/web/mirror/lang/zh-CN.pjson @@ -128,5 +128,7 @@ "msg.dep-satisfied": "依赖 %s%s%s 已装/被提供,跳过", "msg.err.conflict": "无法安装 %s:与已装的 %s 冲突", "msg.cache-cleared": "已清理缓存: %d 个文件,释放 %.1f KB", - "msg.self-up-to-date": "当前 v%s 已是最新(v%s),跳过升级" + "msg.self-up-to-date": "当前 v%s 已是最新(v%s),跳过升级", + "desc.doctor": "诊断", + "msg.doctor-issues": "doctor: 发现 %d 个问题" }