镜像自地址
https://github.com/JGZYES/ParlzPackageManger.git
已同步 2026-09-11 08:52:45 +08:00
feat: real dependency resolver + pmm clean + self-update version check
Dependency resolver (install.c): - Parse name (op ver) ranges; reuse spec_match for version checking. - installed_satisfies(): skip a dep already satisfied by an installed package (own name or an installed Provides), matching the version constraint - no more needless re-installs. - any_installed_conflict(): reject an install whose Conflicts: list names something already installed (checked in pdm_install_file before extracting). - Topological order stays dep-first via recursion + dep_seen dedup set (deps are installed before the parent package, diamonds installed once). pmm clean (main.c): - New command clears the cache dir (~/.pmm/cache incl. registry snapshots), keeps the dir, reports files/size freed. self-update (main.c): - Before installing, compare PMM_VERSION against the registry's top-level 'version' (mirror bumps it each release); skip with 'already up to date' when current >= latest (avoids a pointless re-download/install). Also: i18n keys for the new messages; help text lists pmm clean.
这个提交包含在:
+3
-3
@@ -53,16 +53,16 @@ static const char *kBuiltinZh =
|
|||||||
"\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\","
|
"\"msg.hash-sha256\":\"sha256: %s\",\"msg.hash-sha1\":\"sha1: %s\","
|
||||||
"\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %s\","
|
"\"msg.installing\":\"正在安装 %s ...\",\"msg.installed\":\"已安装 %s\","
|
||||||
"\"msg.checksum-ok\":\"%s 校验 OK: %s\",\"msg.checksum-mismatch\":\"校验不匹配 (%s)!\","
|
"\"msg.checksum-ok\":\"%s 校验 OK: %s\",\"msg.checksum-mismatch\":\"校验不匹配 (%s)!\","
|
||||||
"\"msg.resolving-dep\":\"正在解析依赖 %s\",\"msg.registry-ok\":\"注册表 sha256 OK: %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.path-exists\":\"PATH 已包含 PMM 目录\",\"msg.err.registry-not-found\":\"找不到软件包 '%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.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')\","
|
"\"msg.err.usage\":\"用法\",\"msg.err.unknown-cmd\":\"未知命令 '%s' (试试 'pmm help')\","
|
||||||
"\"msg.err.download-failed\":\"下载语言包 %s 失败\",\"msg.err.invalid-locale\":\"无效语言 %s\","
|
"\"msg.err.download-failed\":\"下载语言包 %s 失败\",\"msg.err.invalid-locale\":\"无效语言 %s\","
|
||||||
"\"msg.err.not-found\":\"找不到软件包 '%s'\",\"msg.err.failed-install\":\"安装 %s 失败\","
|
"\"msg.err.not-found\":\"找不到软件包 '%s'\",\"msg.err.failed-install\":\"安装 %s 失败\",\"msg.err.conflict\":\"无法安装 %s:与已装的 %s 冲突\","
|
||||||
"\"msg.err.no-registry-mirror\":\"没有配置注册表镜像\",\"msg.err.cannot-write\":\"无法写入 %s\","
|
"\"msg.err.no-registry-mirror\":\"没有配置注册表镜像\",\"msg.err.cannot-write\":\"无法写入 %s\","
|
||||||
"\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\","
|
"\"desc.install\":\"安装\",\"desc.remove\":\"卸载\",\"desc.update\":\"刷新索引\","
|
||||||
"\"desc.upgrade\":\"升级\",\"desc.mirror\":\"镜像源\",\"desc.search\":\"搜索\","
|
"\"desc.upgrade\":\"升级\",\"desc.mirror\":\"镜像源\",\"desc.search\":\"搜索\","
|
||||||
"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.help\":\"帮助\","
|
"\"desc.info\":\"详情\",\"desc.self-update\":\"更新自身\",\"desc.clean\":\"清理缓存\",\"desc.help\":\"帮助\","
|
||||||
"\"opt.p-drive\":\"安装盘符\",\"opt.no-color\":\"禁用颜色\",\"opt.quiet\":\"静默\","
|
"\"opt.p-drive\":\"安装盘符\",\"opt.no-color\":\"禁用颜色\",\"opt.quiet\":\"静默\","
|
||||||
"\"opt.verbose\":\"详情\"}";
|
"\"opt.verbose\":\"详情\"}";
|
||||||
|
|
||||||
|
|||||||
+127
@@ -911,9 +911,136 @@ int install_from_registry(const char *name, const char *spec, const char *mirror
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- installed-package DB queries (dependency solving) ---------- */
|
||||||
|
|
||||||
|
/* Extract the first value of `key` from a control/.info text buffer. Returns a
|
||||||
|
* malloc'd copy or NULL. Multi-value fields (Depends/Provides/Conflicts) are
|
||||||
|
* returned as their single comma-separated line. */
|
||||||
|
static char *ctl_field(const char *ctl, const char *key) {
|
||||||
|
size_t klen = strlen(key);
|
||||||
|
const char *line = ctl;
|
||||||
|
while (line && *line) {
|
||||||
|
const char *eol = strchr(line, '\n');
|
||||||
|
size_t ll = eol ? (size_t)(eol - line) : strlen(line);
|
||||||
|
if (ll > klen && strncasecmp(line, key, klen) == 0 && line[klen] == ':') {
|
||||||
|
const char *v = line + klen + 1;
|
||||||
|
while (v < line + ll && (*v == ' ' || *v == '\t')) v++;
|
||||||
|
size_t vl = (size_t)(line + ll - v);
|
||||||
|
while (vl && (v[vl-1] == ' ' || v[vl-1] == '\r') ) vl--;
|
||||||
|
char *out = malloc(vl + 1);
|
||||||
|
if (!out) return NULL;
|
||||||
|
memcpy(out, v, vl); out[vl] = '\0';
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
line = eol ? eol + 1 : NULL;
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static char *read_text_file(const char *path) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return NULL;
|
||||||
|
fseek(f, 0, SEEK_END); long n = ftell(f); fseek(f, 0, SEEK_SET);
|
||||||
|
if (n < 0) { fclose(f); return NULL; }
|
||||||
|
char *buf = malloc((size_t)n + 1);
|
||||||
|
if (!buf) { fclose(f); return NULL; }
|
||||||
|
size_t rd = fread(buf, 1, (size_t)n, f); fclose(f);
|
||||||
|
buf[rd] = '\0';
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comma/space-separated Provides or Conflicts list contains `name`? */
|
||||||
|
static int field_has(const char *fieldval, const char *name) {
|
||||||
|
if (!fieldval || !*fieldval) return 0;
|
||||||
|
size_t nl = strlen(name);
|
||||||
|
const char *p = fieldval;
|
||||||
|
while (*p) {
|
||||||
|
while (*p == ' ' || *p == '\t' || *p == ',') p++;
|
||||||
|
const char *s = p;
|
||||||
|
while (*p && *p != ',' && *p != ' ' && *p != '\t') p++;
|
||||||
|
if ((size_t)(p - s) == nl && strncasecmp(s, name, nl) == 0) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Does an installed package provide `name` (as its own Package, or via a
|
||||||
|
* Provides: entry)? If found, store its installed Version into `ver_out`.
|
||||||
|
* Returns 1 if `name` is satisfied by something installed, else 0. */
|
||||||
|
static int installed_provides(const char *name, char *ver_out, size_t vs) {
|
||||||
|
char home[1024];
|
||||||
|
pmm_config_dir(home, sizeof(home));
|
||||||
|
char db[1200];
|
||||||
|
snprintf(db, sizeof(db), "%s/installed", home);
|
||||||
|
(void)ver_out; (void)vs;
|
||||||
|
#ifndef _WIN32
|
||||||
|
DIR *d = opendir(db);
|
||||||
|
if (!d) return 0;
|
||||||
|
int hit = 0;
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(d)) != NULL) {
|
||||||
|
size_t ln = strlen(e->d_name);
|
||||||
|
if (ln < 5 || strcmp(e->d_name + ln - 5, ".info") != 0) continue;
|
||||||
|
char path[1400];
|
||||||
|
snprintf(path, sizeof(path), "%s/%s", db, e->d_name);
|
||||||
|
char *info = read_text_file(path);
|
||||||
|
if (!info) continue;
|
||||||
|
char *pkg = ctl_field(info, "Package");
|
||||||
|
char *ver = ctl_field(info, "Version");
|
||||||
|
char *prov = ctl_field(info, "Provides");
|
||||||
|
if (pkg && strcmp(pkg, name) == 0) {
|
||||||
|
if (ver_out && ver) snprintf(ver_out, vs, "%s", ver);
|
||||||
|
hit = 1;
|
||||||
|
} else if (field_has(prov, name)) {
|
||||||
|
if (ver_out && ver) snprintf(ver_out, vs, "%s", ver);
|
||||||
|
hit = 1;
|
||||||
|
}
|
||||||
|
free(pkg); free(ver); free(prov); free(info);
|
||||||
|
if (hit) break;
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
return hit;
|
||||||
|
#else
|
||||||
|
return 0; /* conservative: never claim a dep is satisfied on Windows */
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Is the requested dep (name + optional version spec) already satisfied by an
|
||||||
|
* installed package (own name or a Provides), matching the version range?
|
||||||
|
* Returns 1 = satisfied (skip install), 0 = must install. */
|
||||||
|
static int installed_satisfies(const char *depName, const char *spec) {
|
||||||
|
char ver[128] = "";
|
||||||
|
if (!installed_provides(depName, ver, sizeof(ver)) || !ver[0]) return 0;
|
||||||
|
if (!spec || !*spec) return 1;
|
||||||
|
return spec_match(ver, spec) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Does `conflicts` (a comma list) name anything already installed? Used to
|
||||||
|
* reject an install that would clash with the current environment. */
|
||||||
|
int any_installed_conflict(const char *conflicts) {
|
||||||
|
if (!conflicts || !*conflicts) return 0;
|
||||||
|
char *dup = strdup(conflicts), *p = dup;
|
||||||
|
int hit = 0;
|
||||||
|
while (p && *p) {
|
||||||
|
char *end = strchr(p, ',');
|
||||||
|
if (end) *end = '\0';
|
||||||
|
char *t = p; while (*t == ' ' || *t == '\t') t++;
|
||||||
|
if (*t && installed_provides(t, NULL, 0)) { hit = 1; break; }
|
||||||
|
if (!end) break;
|
||||||
|
p = end + 1;
|
||||||
|
}
|
||||||
|
free(dup);
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
static int install_dep_spec(const char *depstr) {
|
static int install_dep_spec(const char *depstr) {
|
||||||
char name[256], spec[256];
|
char name[256], spec[256];
|
||||||
if (parse_dep(depstr, name, sizeof(name), spec, sizeof(spec)) != 0) return -1;
|
if (parse_dep(depstr, name, sizeof(name), spec, sizeof(spec)) != 0) return -1;
|
||||||
|
/* If this dep is already satisfied by an installed package (its own
|
||||||
|
* name or a Provides), don't re-install it. */
|
||||||
|
if (installed_satisfies(name, spec)) {
|
||||||
|
pmm_info("%s", pmm_tr_fmt("msg.dep-satisfied", name, spec[0] ? " " : "", spec));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
for (int i = 0; i < dep_seen_n; i++)
|
for (int i = 0; i < dep_seen_n; i++)
|
||||||
if (strcmp(dep_seen[i], name) == 0) return 0; /* cycle / duplicate */
|
if (strcmp(dep_seen[i], name) == 0) return 0; /* cycle / duplicate */
|
||||||
if (dep_seen_n < 128) dep_seen[dep_seen_n++] = strdup(name);
|
if (dep_seen_n < 128) dep_seen[dep_seen_n++] = strdup(name);
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ extern int pmm_yes;
|
|||||||
/* Resolve a comma-separated Depends list ("a (>= 1.0), b") from the registry. */
|
/* Resolve a comma-separated Depends list ("a (>= 1.0), b") from the registry. */
|
||||||
int pmm_install_dep_list(const char *list);
|
int pmm_install_dep_list(const char *list);
|
||||||
|
|
||||||
|
/* Does a comma-separated Conflicts list name anything already installed?
|
||||||
|
* (dependency solver helper, also called from the .pdm installer). */
|
||||||
|
int any_installed_conflict(const char *conflicts);
|
||||||
|
|
||||||
/* Install from a remote registry (apt-style multi-mirror fallback):
|
/* Install from a remote registry (apt-style multi-mirror fallback):
|
||||||
* looks up package `name` in the configured registry mirrors by priority.
|
* looks up package `name` in the configured registry mirrors by priority.
|
||||||
* If `version` is non-NULL, fetches {registry}/{name}/{version}.json, otherwise
|
* If `version` is non-NULL, fetches {registry}/{name}/{version}.json, otherwise
|
||||||
|
|||||||
+85
@@ -809,6 +809,75 @@ static int cmd_mirror(int argc, char **argv) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static unsigned long long g_clean_size;
|
||||||
|
static int g_clean_count;
|
||||||
|
|
||||||
|
/* Recursively delete every entry under `path`, keeping `path` itself and
|
||||||
|
* accumulating the number/size of files removed. Used by `pmm clean`. */
|
||||||
|
static void clean_rm(const char *path) {
|
||||||
|
#ifdef _WIN32
|
||||||
|
char patt[1200]; snprintf(patt, sizeof(patt), "%s\\*", path);
|
||||||
|
WIN32_FIND_DATAA fd; HANDLE h = FindFirstFileA(patt, &fd);
|
||||||
|
if (h != INVALID_HANDLE_VALUE) {
|
||||||
|
do {
|
||||||
|
if (strcmp(fd.cFileName, ".") == 0 || strcmp(fd.cFileName, "..") == 0) continue;
|
||||||
|
char full[1400]; snprintf(full, sizeof(full), "%s\\%s", path, fd.cFileName);
|
||||||
|
if (fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
|
||||||
|
clean_rm(full);
|
||||||
|
RemoveDirectoryA(full);
|
||||||
|
} else {
|
||||||
|
ULONGLONG sz = ((ULONGLONG)fd.nFileSizeHigh << 32) | fd.nFileSizeLow;
|
||||||
|
g_clean_size += (unsigned long long)sz; g_clean_count++;
|
||||||
|
RemoveFileA(full);
|
||||||
|
}
|
||||||
|
} while (FindNextFileA(h, &fd));
|
||||||
|
FindClose(h);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
DIR *d = opendir(path);
|
||||||
|
if (d) {
|
||||||
|
struct dirent *e;
|
||||||
|
while ((e = readdir(d)) != NULL) {
|
||||||
|
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
|
||||||
|
char full[1400]; snprintf(full, sizeof(full), "%s/%s", path, e->d_name);
|
||||||
|
struct stat st;
|
||||||
|
if (stat(full, &st) == 0) {
|
||||||
|
if (S_ISDIR(st.st_mode)) { clean_rm(full); rmdir(full); }
|
||||||
|
else { g_clean_size += (unsigned long long)st.st_size; g_clean_count++; remove(full); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(d);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pmm clean — drop everything under the cache dir (~/.pmm/cache) but keep it. */
|
||||||
|
static int cmd_clean(void) {
|
||||||
|
char cache[1024];
|
||||||
|
pmm_cache_dir(cache, sizeof(cache));
|
||||||
|
g_clean_size = 0; g_clean_count = 0;
|
||||||
|
clean_rm(cache);
|
||||||
|
pmm_success("%s", pmm_tr_fmt("msg.cache-cleared", g_clean_count, (double)g_clean_size / 1024.0));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the registry "latest" version of `pkg`. We use the top-level
|
||||||
|
* `version` field of `<pkg>.json` (the mirror bumps it on every release) rather
|
||||||
|
* than walking the variants array. Returns a malloc'd string, or NULL. */
|
||||||
|
static char *registry_latest_version(const char *pkg) {
|
||||||
|
char rel[512]; snprintf(rel, sizeof(rel), "%s.json", pkg);
|
||||||
|
int status = 0;
|
||||||
|
char *body = registry_fetch(rel, &status);
|
||||||
|
if (!body) return NULL;
|
||||||
|
JsonValue *root = json_parse(body);
|
||||||
|
free(body);
|
||||||
|
if (!root) return NULL;
|
||||||
|
const char *best = json_str(root, "version");
|
||||||
|
char *out = best ? strdup(best) : NULL;
|
||||||
|
json_free(root);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
static void print_help(void) {
|
static void print_help(void) {
|
||||||
printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch());
|
printf("PMM %s (%s)\n\n", PMM_VERSION, pmm_detect_arch());
|
||||||
printf("%s\n", pmm_tr("help.usage"));
|
printf("%s\n", pmm_tr("help.usage"));
|
||||||
@@ -820,6 +889,7 @@ static void print_help(void) {
|
|||||||
printf(" %-32s%s\n", "pmm upgrade [--yes] ", pmm_tr("desc.upgrade"));
|
printf(" %-32s%s\n", "pmm upgrade [--yes] ", pmm_tr("desc.upgrade"));
|
||||||
printf(" %-32s%s\n", "pmm mirror ... ", pmm_tr("desc.mirror"));
|
printf(" %-32s%s\n", "pmm mirror ... ", pmm_tr("desc.mirror"));
|
||||||
printf(" %-32s%s\n", "pmm self-update ", pmm_tr("desc.self-update"));
|
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 version | help ", pmm_tr("desc.help"));
|
printf(" %-32s%s\n", "pmm version | help ", pmm_tr("desc.help"));
|
||||||
printf("\n%s\n", pmm_tr("help.options"));
|
printf("\n%s\n", pmm_tr("help.options"));
|
||||||
printf(" %-32s%s\n", "-p<drive> ", pmm_tr("opt.p-drive"));
|
printf(" %-32s%s\n", "-p<drive> ", pmm_tr("opt.p-drive"));
|
||||||
@@ -1099,6 +1169,18 @@ int main(int argc, char **argv) {
|
|||||||
PmmConfig cfg; MirrorSel mirror;
|
PmmConfig cfg; MirrorSel mirror;
|
||||||
load_config(&cfg);
|
load_config(&cfg);
|
||||||
load_mirror(&cfg, &mirror);
|
load_mirror(&cfg, &mirror);
|
||||||
|
/* If the running binary already matches the newest registry version,
|
||||||
|
* skip the (re)install. */
|
||||||
|
char *latest = registry_latest_version("pmm");
|
||||||
|
if (latest) {
|
||||||
|
if (cmp_version(PMM_VERSION, latest) >= 0) {
|
||||||
|
pmm_info("%s", pmm_tr_fmt("msg.self-up-to-date", PMM_VERSION, latest));
|
||||||
|
free(latest); free(cfg.registry_url); free(cfg.mirror_name);
|
||||||
|
free(mirror.name); free(mirror.api_base);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
free(latest);
|
||||||
|
}
|
||||||
pmm_info("self-update -> installing latest pmm (%s/%s)\n",
|
pmm_info("self-update -> installing latest pmm (%s/%s)\n",
|
||||||
pmm_os_name(pmm_detect_os()), pmm_detect_arch());
|
pmm_os_name(pmm_detect_os()), pmm_detect_arch());
|
||||||
int rc = install_from_registry("pmm", NULL, mirror.name);
|
int rc = install_from_registry("pmm", NULL, mirror.name);
|
||||||
@@ -1110,6 +1192,9 @@ int main(int argc, char **argv) {
|
|||||||
return rc == 0 ? 0 : 1;
|
return rc == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strcmp(argv[1], "clean") == 0)
|
||||||
|
return cmd_clean();
|
||||||
|
|
||||||
if (strcmp(argv[1], "update") == 0)
|
if (strcmp(argv[1], "update") == 0)
|
||||||
return cmd_update();
|
return cmd_update();
|
||||||
|
|
||||||
|
|||||||
@@ -540,6 +540,15 @@ int pdm_install_file(const char *pdmfile) {
|
|||||||
chdir_restore(); remove(tmpname); return -1;
|
chdir_restore(); remove(tmpname); return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* reject an install that would conflict with a package already present */
|
||||||
|
char *confl = control_get(ctl, "Conflicts");
|
||||||
|
if (confl && *confl && any_installed_conflict(confl)) {
|
||||||
|
pmm_error("%s", pmm_tr_fmt("msg.err.conflict", pkg, confl));
|
||||||
|
free(confl); free(pkg); if (ver) free(ver);
|
||||||
|
chdir_restore(); remove(tmpname); return -1;
|
||||||
|
}
|
||||||
|
free(confl);
|
||||||
|
|
||||||
/* extract data into root (relative target: cwd is already the pm dir) */
|
/* extract data into root (relative target: cwd is already the pm dir) */
|
||||||
const char *tgt = flat ? "." : "root";
|
const char *tgt = flat ? "." : "root";
|
||||||
/* If the package is pmm itself, its exe is the currently running binary and
|
/* If the package is pmm itself, its exe is the currently running binary and
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户