proxyctl 0.4.5__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.5 → proxyctl-0.4.7}/PKG-INFO +2 -2
- {proxyctl-0.4.5 → proxyctl-0.4.7}/README.md +1 -1
- {proxyctl-0.4.5 → proxyctl-0.4.7}/man/proxyctl.1 +1 -1
- {proxyctl-0.4.5 → proxyctl-0.4.7}/pyproject.toml +1 -1
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/cli.py +65 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/explain.py +9 -4
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/status.py +17 -4
- {proxyctl-0.4.5 → proxyctl-0.4.7}/.gitignore +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/LICENSE +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/__init__.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/_io.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/audit.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/builtin_plugins/__init__.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/builtin_plugins/connectivity_basic.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/builtin_plugins/corp_network.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/check.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/completion.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/core/__init__.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/core/plugin.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/engine/__init__.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/engine/base.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/engine/mihomo.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/engine/singbox.py +0 -0
- {proxyctl-0.4.5 → proxyctl-0.4.7}/src/proxyctl/subscription.py +0 -0
- {proxyctl-0.4.5 → 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))
|
|
@@ -119,7 +119,9 @@ def _t_engine(backend, config) -> TopicCard:
|
|
|
119
119
|
"topic": "engine",
|
|
120
120
|
"summary": (
|
|
121
121
|
f"代理引擎(后端)。当前:{backend.name}。"
|
|
122
|
-
"支持 mihomo
|
|
122
|
+
"支持 mihomo(首发,端到端验证)/ sing-box(预留,未端到端验证 — "
|
|
123
|
+
"类 / 路径 / audit / trace 解析已实现,但完整启停闭环未跑过生产);"
|
|
124
|
+
"通过 proxyctl engine <name> 切换后端,"
|
|
123
125
|
"通过 proxyctl mode <tun|proxy> 切换流量入站方式。"
|
|
124
126
|
),
|
|
125
127
|
"file": f"{backend.config_file}",
|
|
@@ -997,7 +999,7 @@ COMMANDS_META: list[dict] = [
|
|
|
997
999
|
"supports_json": False, "side_effects": ["config-write"],
|
|
998
1000
|
"supports_dry_run": True,
|
|
999
1001
|
"needs_sudo": True, "interactive": False,
|
|
1000
|
-
"exit_codes": [0, 1, 2
|
|
1002
|
+
"exit_codes": [0, 1, 2],
|
|
1001
1003
|
"examples": ["proxyctl mode tun", "proxyctl mode tun --dry-run"]},
|
|
1002
1004
|
{"name": "engine", "group": "config", "summary": "切换代理引擎后端",
|
|
1003
1005
|
"args": [{"name": "target", "choices": ["mihomo", "singbox"], "required": False}],
|
|
@@ -1441,7 +1443,7 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1441
1443
|
as_json = GLOBAL_FLAGS_REF().get("json", False)
|
|
1442
1444
|
|
|
1443
1445
|
# 从 cli.py 复用 service_running,但为避免循环 import 在这里独立判断
|
|
1444
|
-
from proxyctl.cli import service_running, get_mode
|
|
1446
|
+
from proxyctl.cli import service_running, get_mode, get_engine_version
|
|
1445
1447
|
port = config.get("proxy_port", 7890)
|
|
1446
1448
|
api_base = config.get("api_base", "http://127.0.0.1:9090")
|
|
1447
1449
|
|
|
@@ -1480,6 +1482,7 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1480
1482
|
"hint": hint,
|
|
1481
1483
|
# informational fields (W15 in 0.3.0):
|
|
1482
1484
|
"engine": backend.name,
|
|
1485
|
+
"engine_version": get_engine_version(backend.name), # v0.4.7+, None 即未知
|
|
1483
1486
|
"mode": mode_str,
|
|
1484
1487
|
"port": port,
|
|
1485
1488
|
"config_path": _io_proxyctl_config_path(),
|
|
@@ -1495,8 +1498,10 @@ def cmd_doctor(args: list, backend, config) -> None:
|
|
|
1495
1498
|
sys.exit(code)
|
|
1496
1499
|
|
|
1497
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 ""
|
|
1498
1503
|
print(f"{BOLD}proxyctl doctor{NC} ({score}/{len(flags)}) "
|
|
1499
|
-
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}")
|
|
1500
1505
|
print(f" {icon(engine_up)} engine_up ({backend.name} 服务运行中)")
|
|
1501
1506
|
print(f" {icon(port_listen)} port_listen (127.0.0.1:{port})")
|
|
1502
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
|