mcp查看与skill目录探测防卡死:只读链路_guard限时(本机8s/WSL20s)超时回菜单+WSL唤醒每5秒打点+dirs先展示两端目录存在性/数量/名字摘要并分端探测本机与WSL的API,单元与菜单e2e全绿
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user