镜像自地址
https://github.com/JGZYES/ParlzPackageManger.git
已同步 2026-09-11 05:42:44 +08:00
feat: pmm doctor / diagnose — one-shot health check with actionable fixes
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.
这个提交包含在:
+2
-2
@@ -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\":\"详情\"}";
|
||||
|
||||
|
||||
+169
@@ -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=\"<dir>:$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 <名字> <registry 地址>,或用 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<drive> ", 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();
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
|
||||
@@ -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 个问题"
|
||||
}
|
||||
|
||||
在新工单中引用
屏蔽一个用户