Files

486 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""WSL 环境校验与命令执行封装。
提供:
- WslCheckResult : 单项检查结果
- WslEnvironmentReport: 完整校验报告
- check_wsl_environment(): 执行全部检查项
- run_wsl_command() : 在指定 distro 中执行命令
"""
from __future__ import annotations
import os
import shutil
import subprocess
import threading
import time
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
# ---------------------------------------------------------------------------
# 数据结构
# ---------------------------------------------------------------------------
@dataclass
class WslCheckResult:
name: str
ok: bool
detail: str = ""
skipped: bool = False
@dataclass
class WslEnvironmentReport:
overall_ok: bool
distro: str
checks: List[WslCheckResult] = field(default_factory=list)
message: str = ""
def to_dict(self) -> Dict[str, Any]:
return {
"overall_ok": self.overall_ok,
"distro": self.distro,
"message": self.message,
"checks": [
{
"name": c.name,
"ok": c.ok,
"detail": c.detail,
"skipped": c.skipped,
}
for c in self.checks
],
}
# ---------------------------------------------------------------------------
# 低层命令执行
# ---------------------------------------------------------------------------
def _decode_subprocess_output(data: bytes) -> str:
if not data:
return ""
encodings: List[str] = []
if data.count(b"\x00") * 4 >= max(1, len(data)):
encodings.extend(["utf-16-le", "utf-16-be"])
encodings.extend(["utf-8", "gbk"])
for encoding in encodings:
try:
return data.decode(encoding).replace("\ufeff", "").replace("\x00", "").strip()
except UnicodeDecodeError:
continue
return data.decode("utf-8", errors="replace").replace("\ufeff", "").replace("\x00", "").strip()
def _find_wsl_executable() -> str:
return shutil.which("wsl.exe") or ""
def _run_windows_command(
args: List[str],
timeout: int = 30,
env: Optional[Dict[str, str]] = None,
) -> Tuple[int, str, str]:
proc_env = os.environ.copy()
if env:
proc_env.update(env)
result = subprocess.run(
args,
capture_output=True,
text=False,
timeout=timeout,
env=proc_env,
check=False,
)
return (
result.returncode,
_decode_subprocess_output(result.stdout),
_decode_subprocess_output(result.stderr),
)
def _run_windows_command_stream(
args: List[str],
timeout: int = 30,
env: Optional[Dict[str, str]] = None,
stdout_callback: Optional[Callable[[str], None]] = None,
stderr_callback: Optional[Callable[[str], None]] = None,
) -> Tuple[int, str, str]:
proc_env = os.environ.copy()
if env:
proc_env.update(env)
stdout_parts: List[str] = []
stderr_parts: List[str] = []
try:
proc = subprocess.Popen(
args,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=False,
env=proc_env,
)
except FileNotFoundError:
return -2, "", "wsl.exe not found"
except Exception as exc:
return -3, "", str(exc)
def _drain(stream: Any, parts: List[str], callback: Optional[Callable[[str], None]]) -> None:
for raw_line in iter(stream.readline, b""):
text = _decode_subprocess_output(raw_line)
if not text:
continue
parts.append(text)
if callback:
try:
callback(text)
except Exception:
pass
threads = [
threading.Thread(target=_drain, args=(proc.stdout, stdout_parts, stdout_callback), daemon=True),
threading.Thread(target=_drain, args=(proc.stderr, stderr_parts, stderr_callback), daemon=True),
]
for thread in threads:
thread.start()
timed_out = False
deadline = time.monotonic() + max(1, int(timeout or 30))
while proc.poll() is None:
if time.monotonic() >= deadline:
timed_out = True
try:
proc.kill()
except Exception:
pass
break
time.sleep(0.2)
try:
returncode = proc.wait(timeout=10)
except subprocess.TimeoutExpired:
returncode = -1
for thread in threads:
thread.join(timeout=5)
stdout = "\n".join(stdout_parts)
stderr = "\n".join(stderr_parts)
if timed_out:
timeout_text = f"command timed out ({timeout}s)"
stderr = f"{stderr}\n{timeout_text}".strip()
return -1, stdout, stderr
return returncode, stdout, stderr
def run_wsl_command(
cmd: str,
distro: Optional[str] = None,
timeout: int = 30,
env: Optional[Dict[str, str]] = None,
) -> Tuple[int, str, str]:
"""在 WSL distro 中执行 bash 命令。"""
wsl_exe = _find_wsl_executable()
if not wsl_exe:
return -2, "", "wsl.exe 未找到"
wsl_args = [wsl_exe]
if distro:
wsl_args += ["-d", distro]
wsl_args += ["bash", "-lc", cmd]
try:
return _run_windows_command(wsl_args, timeout=timeout, env=env)
except subprocess.TimeoutExpired:
return -1, "", f"命令超时({timeout}s"
except FileNotFoundError:
return -2, "", "wsl.exe 未找到"
except Exception as exc:
return -3, "", str(exc)
def run_wsl_command_stream(
cmd: str,
distro: Optional[str] = None,
timeout: int = 30,
env: Optional[Dict[str, str]] = None,
stdout_callback: Optional[Callable[[str], None]] = None,
stderr_callback: Optional[Callable[[str], None]] = None,
) -> Tuple[int, str, str]:
"""Run a WSL bash command and stream decoded stdout/stderr lines to callbacks."""
wsl_exe = _find_wsl_executable()
if not wsl_exe:
return -2, "", "wsl.exe not found"
wsl_args = [wsl_exe]
if distro:
wsl_args += ["-d", distro]
wsl_args += ["bash", "-lc", cmd]
return _run_windows_command_stream(
wsl_args,
timeout=timeout,
env=env,
stdout_callback=stdout_callback,
stderr_callback=stderr_callback,
)
def run_wsl_exec(
argv: Sequence[str],
distro: Optional[str] = None,
timeout: int = 30,
env: Optional[Dict[str, str]] = None,
) -> Tuple[int, str, str]:
"""Execute a structured argv in WSL via ``wsl.exe --exec``."""
wsl_exe = _find_wsl_executable()
if not wsl_exe:
return -2, "", "wsl.exe not found"
normalized_argv = [str(part) for part in argv if str(part)]
if not normalized_argv:
return -3, "", "WSL argv is empty"
wsl_args = [wsl_exe]
if distro:
wsl_args += ["-d", distro]
wsl_args += ["--exec", *normalized_argv]
try:
return _run_windows_command(wsl_args, timeout=timeout, env=env)
except subprocess.TimeoutExpired:
return -1, "", f"command timed out ({timeout}s)"
except FileNotFoundError:
return -2, "", "wsl.exe not found"
except Exception as exc:
return -3, "", str(exc)
def windows_path_to_wsl(win_path: str, distro: Optional[str] = None) -> str:
"""将 Windows 路径转换为 WSL 路径(调用 wslpath)。"""
if not win_path:
return ""
rc, stdout, _ = run_wsl_exec(["wslpath", "-u", win_path], distro=distro, timeout=10)
return stdout if rc == 0 else ""
# ---------------------------------------------------------------------------
# 环境校验
# ---------------------------------------------------------------------------
def check_wsl_environment(
distro: str,
python_cmd: str = "/home/administrator/miniconda3/envs/isce2/bin/python",
stripmap_app_path: str = "", # stripmapApp.py 的 WSL 绝对路径
pipeline_script_path: str = "", # run_lt1_dinsar_pipeline.py 的 WSL 绝对路径
dem_path_win: str = "",
orbit_dir_win: str = "",
output_dir_win: str = "",
smoke_test: bool = False,
) -> WslEnvironmentReport:
"""执行全部 WSL 环境校验,返回 WslEnvironmentReport。"""
checks: List[WslCheckResult] = []
def add(name: str, ok: bool, detail: str = "", skipped: bool = False) -> None:
checks.append(WslCheckResult(name=name, ok=ok, detail=detail, skipped=skipped))
deferred_checks = [
"WSL2 支持",
"Distro 存在",
"Distro 可启动",
"bash -lc 可执行",
"Python 可执行",
"ISCE2 可 import",
"astropy.convolution import",
"cv2 import",
"scipy import",
"stripmapApp 存在",
"生产脚本存在",
"DEM 路径可读",
"轨道目录可读",
"输出目录可写",
"Windows→WSL 路径转换",
"Smoke Test",
]
# 1. wsl.exe 是否存在
wsl_exe = _find_wsl_executable()
wsl_installed = bool(wsl_exe)
add("WSL 已安装", wsl_installed, wsl_exe or "wsl.exe 未找到")
if not wsl_installed:
for name in deferred_checks:
add(name, False, "wsl.exe 未找到,跳过", skipped=True)
return WslEnvironmentReport(
overall_ok=False, distro=distro, checks=checks,
message="WSL 未安装,后续检查跳过",
)
# 2. WSL 服务可访问
rc, stdout, stderr = _run_windows_command([wsl_exe, "--list", "--quiet"], timeout=15)
service_ok = rc == 0
service_detail = stdout or stderr or "WSL 服务可访问"
add("WSL 服务可访问", service_ok, service_detail)
if not service_ok:
for name in deferred_checks:
add(name, False, f"WSL 服务不可访问,跳过:{service_detail}", skipped=True)
return WslEnvironmentReport(
overall_ok=False,
distro=distro,
checks=checks,
message=f"WSL 服务不可访问:{service_detail}",
)
# 3. WSL2 支持
try:
rc, raw, err = _run_windows_command([wsl_exe, "--list", "--verbose"], timeout=15)
verbose_lines = [line.strip() for line in raw.splitlines() if line.strip()]
wsl2_ok = rc == 0 and any(line.endswith(" 2") or line.endswith("\t2") for line in verbose_lines)
add("WSL2 支持", wsl2_ok, raw or err or ("检测到 WSL2 版本" if wsl2_ok else "未检测到 WSL2"))
except Exception as exc:
add("WSL2 支持", False, str(exc))
# 4. 目标 distro 存在
try:
raw = stdout
distro_exists = distro.lower() in raw.lower()
add("Distro 存在", distro_exists,
f"'{distro}' 已找到" if distro_exists else f"'{distro}' 未在列表中")
except Exception as exc:
distro_exists = False
add("Distro 存在", False, str(exc))
if not distro_exists:
for name in deferred_checks[2:]:
add(name, False, "Distro 不存在,跳过", skipped=True)
return WslEnvironmentReport(
overall_ok=False, distro=distro, checks=checks,
message=f"Distro '{distro}' 不存在",
)
# 4. Distro 可启动
rc, _, err = run_wsl_command("echo alive", distro=distro, timeout=15)
distro_ok = rc == 0
add("Distro 可启动", distro_ok, err if not distro_ok else "启动正常")
# 5. bash -lc 可执行
rc, out, err = run_wsl_command("echo bash_ok", distro=distro, timeout=10)
bash_ok = rc == 0 and "bash_ok" in out
add("bash -lc 可执行", bash_ok, err if not bash_ok else "bash 正常")
# 6. Python 可执行
rc, out, err = run_wsl_command(f"{python_cmd} --version", distro=distro, timeout=15)
py_ok = rc == 0
add("Python 可执行", py_ok, out or err)
# 7. ISCE2 可 import
rc, out, err = run_wsl_command(
f'{python_cmd} -c "import isce; print(isce.__version__)"',
distro=distro, timeout=30,
)
isce_ok = rc == 0
add("ISCE2 可 import", isce_ok, out or err)
rc, out, err = run_wsl_command(
f'{python_cmd} -c "from astropy.convolution import convolve; print(\'astropy_ok\')"',
distro=distro, timeout=30,
)
astropy_ok = rc == 0 and "astropy_ok" in out
add("astropy.convolution import", astropy_ok, out or err)
rc, out, err = run_wsl_command(
f'{python_cmd} -c "import cv2; print(\'cv2_ok\')"',
distro=distro, timeout=30,
)
cv2_ok = rc == 0 and "cv2_ok" in out
add("cv2 import", cv2_ok, out or err)
rc, out, err = run_wsl_command(
f'{python_cmd} -c "import scipy; print(\'scipy_ok\')"',
distro=distro, timeout=30,
)
scipy_ok = rc == 0 and "scipy_ok" in out
add("scipy import", scipy_ok, out or err)
# 8. stripmapApp.py 存在(全路径检查)
if stripmap_app_path:
rc, out, err = run_wsl_command(
f"test -f '{stripmap_app_path}' && echo found",
distro=distro, timeout=10,
)
app_ok = rc == 0 and "found" in out
add("stripmapApp 存在", app_ok,
stripmap_app_path if app_ok else f"{stripmap_app_path} 未找到")
else:
add("stripmapApp 存在", False, "ISCE2_STRIPMAP_APP 未配置", skipped=True)
# 9. 生产流水线脚本存在
if pipeline_script_path:
rc, out, err = run_wsl_command(
f"test -f '{pipeline_script_path}' && echo found",
distro=distro, timeout=10,
)
script_ok = rc == 0 and "found" in out
add("生产脚本存在", script_ok,
pipeline_script_path if script_ok else f"{pipeline_script_path} 未找到")
else:
add("生产脚本存在", False, "ISCE2_PIPELINE_SCRIPT 未配置", skipped=True)
# 10. DEM 路径可读
if dem_path_win:
wsl_dem = windows_path_to_wsl(dem_path_win, distro=distro)
rc, _, err = run_wsl_command(f"test -r '{wsl_dem}'", distro=distro, timeout=10)
add("DEM 路径可读", rc == 0, wsl_dem or err)
else:
add("DEM 路径可读", False, "ISCE2_DEM_PATH 未配置", skipped=True)
# 11. 轨道目录可读
if orbit_dir_win:
wsl_orbit = windows_path_to_wsl(orbit_dir_win, distro=distro)
rc, _, err = run_wsl_command(f"test -r '{wsl_orbit}'", distro=distro, timeout=10)
add("轨道目录可读", rc == 0, wsl_orbit or err)
else:
add("轨道目录可读", False, "ORBIT_POOL_ISCE2 未配置", skipped=True)
# 12. 输出目录可写
if output_dir_win:
wsl_out = windows_path_to_wsl(output_dir_win, distro=distro)
rc, _, err = run_wsl_command(f"test -w '{wsl_out}'", distro=distro, timeout=10)
add("输出目录可写", rc == 0, wsl_out or err)
else:
add("输出目录可写", False, "ISCE2_OUTPUT_ROOT 未配置", skipped=True)
# 13. Windows→WSL 路径转换
test_win = os.environ.get("TEMP", r"C:\Windows\Temp")
wsl_converted = windows_path_to_wsl(test_win, distro=distro)
path_ok = wsl_converted.startswith("/mnt/")
add("Windows→WSL 路径转换", path_ok,
f"{test_win}{wsl_converted}" if wsl_converted else "转换失败")
# 14. Smoke test(可选)
if smoke_test:
rc, out, err = run_wsl_command(
f'{python_cmd} -c "import isce; from isceobj.Sensor import Lutan1; print(\'smoke_ok\')"',
distro=distro, timeout=60,
)
smoke_ok = rc == 0 and "smoke_ok" in out
add("Smoke Test", smoke_ok, out or err)
else:
add("Smoke Test", True, "已跳过(ISCE2_SMOKE_TEST_ENABLED=false", skipped=True)
# 汇总
critical = [c for c in checks if not c.skipped]
overall_ok = all(c.ok for c in critical)
failed_names = [c.name for c in critical if not c.ok]
message = "所有检查通过" if overall_ok else f"以下检查未通过:{', '.join(failed_names)}"
return WslEnvironmentReport(
overall_ok=overall_ok,
distro=distro,
checks=checks,
message=message,
)