proxyctl 0.4.6__tar.gz → 0.4.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {proxyctl-0.4.6 → proxyctl-0.4.7}/PKG-INFO +2 -2
- {proxyctl-0.4.6 → proxyctl-0.4.7}/README.md +1 -1
- {proxyctl-0.4.6 → proxyctl-0.4.7}/man/proxyctl.1 +1 -1
- {proxyctl-0.4.6 → proxyctl-0.4.7}/pyproject.toml +1 -1
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/cli.py +65 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/explain.py +5 -2
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/status.py +17 -4
- {proxyctl-0.4.6 → proxyctl-0.4.7}/.gitignore +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/LICENSE +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/_io.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/check.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/completion.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/subscription.py +0 -0
- {proxyctl-0.4.6 → proxyctl-0.4.7}/src/proxyctl/trace.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: proxyctl
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.7
|
|
4
4
|
Summary: Proxy configuration lifecycle management for macOS and Linux
|
|
5
5
|
Project-URL: Homepage, https://github.com/crhan/proxyctl
|
|
6
6
|
Project-URL: Issues, https://github.com/crhan/proxyctl/issues
|
|
@@ -193,7 +193,7 @@ uv tool install proxyctl # uv(推荐)
|
|
|
193
193
|
pipx install proxyctl # 或 pipx
|
|
194
194
|
pip install --user proxyctl # 或 pip
|
|
195
195
|
|
|
196
|
-
proxyctl --version # → proxyctl v0.4.
|
|
196
|
+
proxyctl --version # → proxyctl v0.4.7
|
|
197
197
|
proxyctl --help
|
|
198
198
|
```
|
|
199
199
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
mode|audit|trace|bench|dns-lock|dns-unlock]
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import functools
|
|
9
10
|
import json
|
|
10
11
|
import os
|
|
11
12
|
import platform
|
|
@@ -250,6 +251,69 @@ def run_out(cmd: list, *, sudo: bool = False) -> str:
|
|
|
250
251
|
return r.stdout.strip() if r.returncode == 0 else ""
|
|
251
252
|
|
|
252
253
|
|
|
254
|
+
@functools.lru_cache(maxsize=4)
|
|
255
|
+
def get_engine_version(backend_name: str) -> dict | None:
|
|
256
|
+
"""运行 `<backend> -v` 解析引擎版本信息。
|
|
257
|
+
|
|
258
|
+
proxyctl 是引擎生命周期管理器,但之前没在任何 status 里显示引擎版本——
|
|
259
|
+
导致 debug TUIC / DNS 问题时无法快速判定"是不是引擎版本 regression"。
|
|
260
|
+
本函数填补此缺陷(v0.4.7+)。
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
backend_name: "mihomo" / "singbox" / 等等。
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
dict 或 None(binary 找不到 / -v 失败 / 解析失败):
|
|
267
|
+
{
|
|
268
|
+
"binary": "/opt/homebrew/bin/mihomo",
|
|
269
|
+
"version": "1.19.25",
|
|
270
|
+
"platform": "darwin arm64",
|
|
271
|
+
"go_version": "1.26.3",
|
|
272
|
+
"build_date": "2026-05-16",
|
|
273
|
+
"raw": "Mihomo Meta 1.19.25 darwin arm64 with go1.26.3 2026-05-16T14:37:07Z",
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
mihomo -v 输出范本:
|
|
277
|
+
Mihomo Meta 1.19.25 darwin arm64 with go1.26.3 2026-05-16T14:37:07Z
|
|
278
|
+
Use tags: with_gvisor
|
|
279
|
+
|
|
280
|
+
sing-box version 输出范本 (供后续扩展):
|
|
281
|
+
sing-box version 1.10.0
|
|
282
|
+
Environment: go1.22.5 darwin/arm64
|
|
283
|
+
Tags: ...
|
|
284
|
+
|
|
285
|
+
LRU cache 4 项:避免每次 status 都跑 subprocess(同进程内常量)。
|
|
286
|
+
"""
|
|
287
|
+
import shutil
|
|
288
|
+
import re
|
|
289
|
+
binary = shutil.which(backend_name)
|
|
290
|
+
if not binary:
|
|
291
|
+
return None
|
|
292
|
+
try:
|
|
293
|
+
r = subprocess.run([binary, "-v"], capture_output=True, text=True, timeout=3)
|
|
294
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
295
|
+
return None
|
|
296
|
+
out = (r.stdout or "").strip()
|
|
297
|
+
if not out or r.returncode != 0:
|
|
298
|
+
return None
|
|
299
|
+
raw = out.split("\n", 1)[0].strip()
|
|
300
|
+
info: dict = {"binary": binary, "raw": raw}
|
|
301
|
+
# 匹配 mihomo: "Mihomo Meta 1.19.25 darwin arm64 with go1.26.3 2026-05-16T14:37:07Z"
|
|
302
|
+
m = re.match(
|
|
303
|
+
r"^\S+\s+\S+\s+(\S+)\s+(\S+\s+\S+)\s+with\s+go(\S+)\s+(\S+)",
|
|
304
|
+
raw,
|
|
305
|
+
)
|
|
306
|
+
if m:
|
|
307
|
+
info["version"] = m.group(1)
|
|
308
|
+
info["platform"] = m.group(2)
|
|
309
|
+
info["go_version"] = m.group(3)
|
|
310
|
+
info["build_date"] = m.group(4).split("T")[0]
|
|
311
|
+
else:
|
|
312
|
+
# 兜底:能拿到 raw 就行,version 字段留空让 agent 自己处理
|
|
313
|
+
info["version"] = None
|
|
314
|
+
return info
|
|
315
|
+
|
|
316
|
+
|
|
253
317
|
def wait_port(port: int, timeout: float = 10.0) -> bool:
|
|
254
318
|
"""轮询直到 127.0.0.1:port 就绪,超时返回 False。"""
|
|
255
319
|
deadline = time.monotonic() + timeout
|
|
@@ -1584,6 +1648,7 @@ def cmd_version_print() -> None:
|
|
|
1584
1648
|
"lifecycle_dry_run": True, # 0.4.2: start/stop/restart/recover --dry-run
|
|
1585
1649
|
"version_subcommand": True, # 0.4.2: `proxyctl version` 子命令
|
|
1586
1650
|
"status_subscription": True, # 0.4.4: status envelope.data.subscription
|
|
1651
|
+
"engine_version": True, # 0.4.7: status/doctor data.engine_version (mihomo -v)
|
|
1587
1652
|
},
|
|
1588
1653
|
}
|
|
1589
1654
|
_io.emit_json(_io.envelope("version", data=data))
|
|
@@ -1443,7 +1443,7 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1443
1443
|
as_json = GLOBAL_FLAGS_REF().get("json", False)
|
|
1444
1444
|
|
|
1445
1445
|
# 从 cli.py 复用 service_running,但为避免循环 import 在这里独立判断
|
|
1446
|
-
from proxyctl.cli import service_running, get_mode
|
|
1446
|
+
from proxyctl.cli import service_running, get_mode, get_engine_version
|
|
1447
1447
|
port = config.get("proxy_port", 7890)
|
|
1448
1448
|
api_base = config.get("api_base", "http://127.0.0.1:9090")
|
|
1449
1449
|
|
|
@@ -1482,6 +1482,7 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1482
1482
|
"hint": hint,
|
|
1483
1483
|
# informational fields (W15 in 0.3.0):
|
|
1484
1484
|
"engine": backend.name,
|
|
1485
|
+
"engine_version": get_engine_version(backend.name), # v0.4.7+, None 即未知
|
|
1485
1486
|
"mode": mode_str,
|
|
1486
1487
|
"port": port,
|
|
1487
1488
|
"config_path": _io_proxyctl_config_path(),
|
|
@@ -1497,8 +1498,10 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1497
1498
|
sys.exit(code)
|
|
1498
1499
|
|
|
1499
1500
|
icon = lambda b: f"{GREEN}✓{NC}" if b else f"{RED}✗{NC}"
|
|
1501
|
+
ev = data.get("engine_version") or {}
|
|
1502
|
+
ev_tag = f" v{ev['version']}" if ev.get("version") else ""
|
|
1500
1503
|
print(f"{BOLD}proxyctl doctor{NC} ({score}/{len(flags)}) "
|
|
1501
|
-
f"{DIM}engine={backend.name} mode={mode_str} port={port}{NC}")
|
|
1504
|
+
f"{DIM}engine={backend.name}{ev_tag} mode={mode_str} port={port}{NC}")
|
|
1502
1505
|
print(f" {icon(engine_up)} engine_up ({backend.name} 服务运行中)")
|
|
1503
1506
|
print(f" {icon(port_listen)} port_listen (127.0.0.1:{port})")
|
|
1504
1507
|
print(f" {icon(dns_ok)} dns_ok (系统 DNS 含 127.0.0.1)")
|
|
@@ -16,6 +16,7 @@ GREEN = "\033[0;32m"
|
|
|
16
16
|
YELLOW = "\033[0;33m"
|
|
17
17
|
CYAN = "\033[0;36m"
|
|
18
18
|
BOLD = "\033[1m"
|
|
19
|
+
DIM = "\033[2m"
|
|
19
20
|
NC = "\033[0m"
|
|
20
21
|
|
|
21
22
|
from proxyctl._io import maybe_disable_module_colors as _pc_maybe_disable
|
|
@@ -68,7 +69,10 @@ def _ifconfig_ip(iface: str) -> str:
|
|
|
68
69
|
# ── 数据采集函数(可并发) ────────────────────────────────────────────────────
|
|
69
70
|
|
|
70
71
|
def _gather_engine(engine) -> dict:
|
|
71
|
-
"""采集引擎进程信息:PID
|
|
72
|
+
"""采集引擎进程信息:PID、运行次数、运行时间、版本(v0.4.7+)。"""
|
|
73
|
+
from proxyctl.cli import get_engine_version
|
|
74
|
+
version_info = get_engine_version(engine.name) # None / dict
|
|
75
|
+
|
|
72
76
|
if IS_LINUX:
|
|
73
77
|
# systemd --user:用 systemctl show 获取 PID
|
|
74
78
|
r = subprocess.run(
|
|
@@ -83,7 +87,8 @@ def _gather_engine(engine) -> dict:
|
|
|
83
87
|
r2 = subprocess.run(["ps", "-o", "etime=", "-p", pid],
|
|
84
88
|
capture_output=True, text=True)
|
|
85
89
|
etime = r2.stdout.strip()
|
|
86
|
-
return {"pid": pid, "runs": runs, "daemon_up": daemon_up,
|
|
90
|
+
return {"pid": pid, "runs": runs, "daemon_up": daemon_up,
|
|
91
|
+
"etime": etime, "version": version_info}
|
|
87
92
|
|
|
88
93
|
# macOS: launchctl
|
|
89
94
|
pid = _launchctl_pid(engine.label)
|
|
@@ -94,7 +99,8 @@ def _gather_engine(engine) -> dict:
|
|
|
94
99
|
r = subprocess.run(["ps", "-o", "etime=", "-p", pid],
|
|
95
100
|
capture_output=True, text=True)
|
|
96
101
|
etime = r.stdout.strip()
|
|
97
|
-
return {"pid": pid, "runs": runs, "daemon_up": daemon_up,
|
|
102
|
+
return {"pid": pid, "runs": runs, "daemon_up": daemon_up,
|
|
103
|
+
"etime": etime, "version": version_info}
|
|
98
104
|
|
|
99
105
|
|
|
100
106
|
def _gather_ports(claude_proxy_label: str,
|
|
@@ -312,7 +318,14 @@ def _print_engine(engine, mode: str, d_engine: dict, d_ports: dict):
|
|
|
312
318
|
else:
|
|
313
319
|
mode_tag = f"{YELLOW}{mode}{NC}"
|
|
314
320
|
|
|
315
|
-
|
|
321
|
+
# 引擎名 + 模式 + 版本号(v0.4.7+,引擎 binary 找不到时只显示名字)
|
|
322
|
+
ver = d_engine.get("version") or {}
|
|
323
|
+
ver_str = ""
|
|
324
|
+
if ver.get("version"):
|
|
325
|
+
ver_str = f" {DIM}v{ver['version']}{NC}"
|
|
326
|
+
if ver.get("build_date"):
|
|
327
|
+
ver_str += f" {DIM}({ver['build_date']}, go{ver.get('go_version','?')}){NC}"
|
|
328
|
+
print(f"{BOLD}引擎{NC} {GREEN}{engine.name}{NC}{ver_str} · {mode_tag}")
|
|
316
329
|
|
|
317
330
|
if d_engine["daemon_up"]:
|
|
318
331
|
runs_str = (f" {YELLOW}runs={d_engine['runs']}{NC}"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|