mcp查看与skill目录探测防卡死:只读链路_guard限时(本机8s/WSL20s)超时回菜单+WSL唤醒每5秒打点+dirs先展示两端目录存在性/数量/名字摘要并分端探测本机与WSL的API,单元与菜单e2e全绿

This commit is contained in:
ypshidifu
2026-09-24 19:23:46 +08:00
parent 547cb56518
commit b827bbbd1d
4 changed files with 233 additions and 44 deletions
+97 -13
View File
@@ -23,8 +23,10 @@ import base64
import datetime
import json
import os
import queue
import re
import sys
import threading
import urllib.error
import urllib.parse
import urllib.request
@@ -127,6 +129,61 @@ def _wsl_running(distro: str) -> bool:
return distro in text
# ---------------------------------------------------------------- 读操作防卡死
# WSL UNC 路径(\\wsl.localhost\...)在 VM 半死 / 9p 无响应时,Path.exists() /
# read_text 会在系统调用层无限挂起,且 Ctrl+C 打不断(Python 信号要等阻塞的
# 系统调用返回才处理)—— 表现为「CLI 进入 mcp 查看直接卡死,只能强杀进程」。
# 只读链路统一放进 daemon 工作线程限时等待:超时打印原因并返回菜单;
# 主线程等的是 queue.get(timeout)(Python 层等待),Ctrl+C 随时能打断。
def _guard(fn, timeout: float):
"""在工作线程里跑 fn,主线程最多等 timeout 秒。
返回 (True, 结果) 或 (False, 错误描述);错误描述为 'TIMEOUT' 表示超时。"""
q: "queue.Queue" = queue.Queue(maxsize=1)
def _run() -> None:
try:
q.put((True, fn()))
except Exception as exc: # noqa: BLE001
q.put((False, f"{type(exc).__name__}: {exc}"))
threading.Thread(target=_run, daemon=True).start()
try:
return q.get(timeout=timeout)
except queue.Empty:
return False, "TIMEOUT"
def _read_timeout(target: str) -> float:
"""只读链路超时:本机文件秒回给 8s;WSL UNC 冷路径首访给 20s。"""
return 20.0 if target == "wsl" else 8.0
def _print_read_fail(what, res, tmo: float) -> int:
"""_guard 读失败的统一出口(超时 / 异常都回菜单,不挂死)。"""
if res == "TIMEOUT":
print(f"读取超时(>{tmo:g}s):{what}")
print(" WSL UNC 路径无响应时不再无限挂起;请检查 wsl.exe 状态后重试。")
else:
print(f"读取失败:{res}")
return 2
def _run_with_beats(fn, interval: float = 5.0):
"""长操作期间每 interval 秒打一个进度点,避免用户把「等待」当成「卡死」。"""
stop = threading.Event()
def _beat() -> None:
while not stop.wait(interval):
print(" .", end="", flush=True)
threading.Thread(target=_beat, daemon=True).start()
try:
return fn()
finally:
stop.set()
print(flush=True)
def fs_view(p: str, target: str, distro: str) -> str:
"""mcpcfg 内部路径 -> 当前系统可访问的视图。
Windows 操作 WSL 目标时,/root/... 必须换成 \\\\wsl.localhost\\<distro>\\...,
@@ -148,8 +205,11 @@ def wsl_gate(args, need_write: bool = False) -> str | None:
if _wsl_running(args.distro):
return None
verb = "读配置" if not need_write else "写配置"
print(f"WSL({args.distro})未运行,正在唤醒以便{verb}(可能需要十几秒)...", flush=True)
_wsl_safe_run(["wsl.exe", "-d", args.distro, "--", "true"], timeout=45)
print(f"WSL({args.distro})未运行,正在唤醒以便{verb}"
f"(可能需要十几秒,期间每 5 秒打一个点)...", flush=True)
_run_with_beats(
lambda: _wsl_safe_run(["wsl.exe", "-d", args.distro, "--", "true"], timeout=45)
)
if _wsl_running(args.distro):
print("WSL 已就绪。", flush=True)
return None
@@ -585,8 +645,10 @@ def _stamp() -> str:
return datetime.datetime.now().strftime("%Y%m%d%H%M%S")
def print_state(cfg: dict) -> bool:
has = active_has_mcp(cfg.get("active"))
def print_state(cfg: dict, has: bool | None = None) -> bool:
# has 由调用方经 _guard 预读(UNC 半死时活配置读取会挂,不能在这里裸读)
if has is None:
has = active_has_mcp(cfg.get("active"))
print(f"模式:{MODE_LABEL.get(cfg['mode'], cfg['mode'])}(mcpcfg.txt「模式」键)")
print(f"MCP :{'已开启(配置含 mcp.servers)' if has else '已关闭(安静,无 mcp 配置)'}")
# 展示用原文(用户在 mcpcfg.txt 里看到的路径),内部访问已走视图翻译
@@ -604,8 +666,15 @@ def cmd_status(args) -> int:
return 2
path = resolve_cfg_file(args.target, args.distro)
print(f"配置文件:{path}")
cfg = read_cfg(path, args.target, args.distro)
print_state(cfg)
tmo = _read_timeout(args.target)
# 读链全线走 _guard:mcpcfg / 活配置任意一处 UNC 无响应都限时返回,不卡死
ok, cfg = _guard(lambda: read_cfg(path, args.target, args.distro), tmo)
if not ok:
return _print_read_fail(path, cfg, tmo)
ok, has = _guard(lambda: active_has_mcp(cfg.get("active")), tmo)
if not ok:
return _print_read_fail(cfg.get("active") or "活配置", has, tmo)
print_state(cfg, has)
return 0 if not cfg.get("error") else 2
@@ -617,7 +686,10 @@ def _switch(args, want_on: bool) -> int:
return 2
path = resolve_cfg_file(args.target, args.distro)
print(f"配置文件:{path}")
cfg = read_cfg(path, args.target, args.distro)
tmo = _read_timeout(args.target)
ok, cfg = _guard(lambda: read_cfg(path, args.target, args.distro), tmo)
if not ok:
return _print_read_fail(path, cfg, tmo)
if cfg.get("error"):
print(f"失败:{cfg['error']}")
return 2
@@ -628,10 +700,12 @@ def _switch(args, want_on: bool) -> int:
print("如需使用,请把模式改成「自动」或「手动」。")
return 3
has = active_has_mcp(cfg["active"])
ok, has = _guard(lambda: active_has_mcp(cfg["active"]), tmo)
if not ok:
return _print_read_fail(cfg.get("active") or "活配置", has, tmo)
if has == want_on:
print(f"无需切换:MCP 已是{'开启' if want_on else '关闭(安静)'}状态。")
print_state(cfg)
print_state(cfg, has)
return 0
swap_files(cfg["on_path"], cfg["quiet_path"])
@@ -693,12 +767,18 @@ def cmd_mode(args) -> int:
print(err)
return 2
path = resolve_cfg_file(args.target, args.distro)
if not path.exists():
tmo = _read_timeout(args.target)
ok, ex = _guard(lambda: path.exists(), tmo)
if not ok:
return _print_read_fail(path, ex, tmo)
if not ex:
print(f"配置文件不存在:{path}")
return 2
if not args.value:
cfg = read_cfg(path, args.target, args.distro)
ok, cfg = _guard(lambda: read_cfg(path, args.target, args.distro), tmo)
if not ok:
return _print_read_fail(path, cfg, tmo)
cur = MODE_LABEL.get(cfg.get("mode", "auto"), cfg.get("mode", "auto"))
print(f"当前模式:{cur}")
print("可选值:自动 / 手动 / 关闭(也支持 auto / manual / off)")
@@ -720,11 +800,15 @@ def cmd_backup(args) -> int:
print(err)
return 2
path = resolve_cfg_file(args.target, args.distro)
if not path.exists():
tmo = _read_timeout(args.target)
ok, data = _guard(lambda: path.read_bytes() if path.exists() else None, tmo)
if not ok:
return _print_read_fail(path, data, tmo)
if data is None:
print(f"配置文件不存在:{path}")
return 2
bak = path.with_name(path.name + f".bak-{_stamp()}")
bak.write_bytes(path.read_bytes())
bak.write_bytes(data)
print(f"已备份:{bak}")
return 0