mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-04-25 19:43:38 +00:00
update workbench example
update aksk desc print res query logs Fix skills exec error with action type Update Skills Update Skills addr Change uni-lab. to leap-lab. Support unit in pylabrobot Support async func. change to leap-lab backend. Support feedback interval. Reduce cocurrent lags. fix create_resource_with_slot update unilabos_formulation & batch-submit-exp scale multi exec thread up to 48 update handle creation api fit cocurrent gap add running status debounce allow non @topic_config support update skill add placeholder keys always free 提交实验技能 disable samples correct sample demo ret value 新增试剂reagent update registry 新增manual_confirm add workstation creation skill add virtual_sample_demo 样品追踪测试设备 add external devices param fix registry upload missing type fast registry load minor fix on skill & registry stripe ros2 schema desc add create-device-skill new registry system backwards to yaml remove not exist resource new registry sys exp. support with add device correct raise create resource error ret info fix revert ret info fix fix prcxi check add create_resource schema re signal host ready event add websocket connection timeout and improve reconnection logic add open_timeout parameter to websocket connection add TimeoutError and InvalidStatus exception handling implement exponential backoff for reconnection attempts simplify reconnection logic flow
This commit is contained in:
@@ -6,20 +6,180 @@
|
||||
import argparse
|
||||
import importlib
|
||||
import locale
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
from unilabos.utils.banner_print import print_status
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 底层安装工具
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _is_chinese_locale() -> bool:
|
||||
try:
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
return bool(lang and ("zh" in lang.lower() or "chinese" in lang.lower()))
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
_USE_UV: Optional[bool] = None
|
||||
|
||||
|
||||
def _has_uv() -> bool:
|
||||
global _USE_UV
|
||||
if _USE_UV is None:
|
||||
_USE_UV = shutil.which("uv") is not None
|
||||
return _USE_UV
|
||||
|
||||
|
||||
def _install_packages(
|
||||
packages: List[str],
|
||||
upgrade: bool = False,
|
||||
label: str = "",
|
||||
) -> bool:
|
||||
"""
|
||||
安装/升级一组包。优先 uv pip install,回退 sys pip。
|
||||
逐个安装,任意一个失败不影响后续包。
|
||||
|
||||
Returns:
|
||||
True if all succeeded, False otherwise.
|
||||
"""
|
||||
if not packages:
|
||||
return True
|
||||
|
||||
is_chinese = _is_chinese_locale()
|
||||
use_uv = _has_uv()
|
||||
failed: List[str] = []
|
||||
|
||||
for pkg in packages:
|
||||
action_word = "升级" if upgrade else "安装"
|
||||
if label:
|
||||
print_status(f"[{label}] 正在{action_word} {pkg}...", "info")
|
||||
else:
|
||||
print_status(f"正在{action_word} {pkg}...", "info")
|
||||
|
||||
if use_uv:
|
||||
cmd = ["uv", "pip", "install"]
|
||||
if upgrade:
|
||||
cmd.append("--upgrade")
|
||||
cmd.append(pkg)
|
||||
if is_chinese:
|
||||
cmd.extend(["--index-url", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||
else:
|
||||
cmd = [sys.executable, "-m", "pip", "install"]
|
||||
if upgrade:
|
||||
cmd.append("--upgrade")
|
||||
cmd.append(pkg)
|
||||
if is_chinese:
|
||||
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
if result.returncode == 0:
|
||||
installer = "uv" if use_uv else "pip"
|
||||
print_status(f"✓ {pkg} {action_word}成功 (via {installer})", "success")
|
||||
else:
|
||||
stderr_short = result.stderr.strip().split("\n")[-1] if result.stderr else "unknown error"
|
||||
print_status(f"× {pkg} {action_word}失败: {stderr_short}", "error")
|
||||
failed.append(pkg)
|
||||
except subprocess.TimeoutExpired:
|
||||
print_status(f"× {pkg} {action_word}超时 (300s)", "error")
|
||||
failed.append(pkg)
|
||||
except Exception as e:
|
||||
print_status(f"× {pkg} {action_word}异常: {e}", "error")
|
||||
failed.append(pkg)
|
||||
|
||||
if failed:
|
||||
print_status(f"有 {len(failed)} 个包操作失败: {', '.join(failed)}", "error")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# requirements.txt 安装(可多次调用)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def install_requirements_txt(req_path: str | Path, label: str = "") -> bool:
|
||||
"""
|
||||
读取一个 requirements.txt 文件,检查缺失的包并安装。
|
||||
|
||||
Args:
|
||||
req_path: requirements.txt 文件路径
|
||||
label: 日志前缀标签(如 "device_package_sim")
|
||||
|
||||
Returns:
|
||||
True if all ok, False if any install failed.
|
||||
"""
|
||||
req_path = Path(req_path)
|
||||
if not req_path.exists():
|
||||
return True
|
||||
|
||||
tag = label or req_path.parent.name
|
||||
print_status(f"[{tag}] 检查依赖: {req_path}", "info")
|
||||
|
||||
reqs: List[str] = []
|
||||
with open(req_path, "r", encoding="utf-8") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line and not line.startswith("#") and not line.startswith("-"):
|
||||
reqs.append(line)
|
||||
|
||||
if not reqs:
|
||||
return True
|
||||
|
||||
missing: List[str] = []
|
||||
for req in reqs:
|
||||
pkg_import = req.split(">=")[0].split("==")[0].split("<")[0].split("[")[0].split(">")[0].strip()
|
||||
pkg_import = pkg_import.replace("-", "_")
|
||||
try:
|
||||
importlib.import_module(pkg_import)
|
||||
except ImportError:
|
||||
missing.append(req)
|
||||
|
||||
if not missing:
|
||||
print_status(f"[{tag}] ✓ 依赖检查通过 ({len(reqs)} 个包)", "success")
|
||||
return True
|
||||
|
||||
print_status(f"[{tag}] 缺失 {len(missing)} 个依赖: {', '.join(missing)}", "warning")
|
||||
return _install_packages(missing, label=tag)
|
||||
|
||||
|
||||
def check_device_package_requirements(devices_dirs: list[str]) -> bool:
|
||||
"""
|
||||
检查 --devices 指定的所有外部设备包目录中的 requirements.txt。
|
||||
对每个目录查找 requirements.txt(先在目录内找,再在父目录找)。
|
||||
"""
|
||||
if not devices_dirs:
|
||||
return True
|
||||
|
||||
all_ok = True
|
||||
for d in devices_dirs:
|
||||
d_path = Path(d).resolve()
|
||||
req_file = d_path / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
req_file = d_path.parent / "requirements.txt"
|
||||
if not req_file.exists():
|
||||
continue
|
||||
if not install_requirements_txt(req_file, label=d_path.name):
|
||||
all_ok = False
|
||||
|
||||
return all_ok
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# UniLabOS 核心环境检查
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EnvironmentChecker:
|
||||
"""环境检查器"""
|
||||
|
||||
def __init__(self):
|
||||
# 定义必需的包及其安装名称的映射
|
||||
self.required_packages = {
|
||||
# 包导入名 : pip安装名
|
||||
# "pymodbus.framer.FramerType": "pymodbus==3.9.2",
|
||||
"websockets": "websockets",
|
||||
"msgcenterpy": "msgcenterpy",
|
||||
"orjson": "orjson",
|
||||
@@ -28,33 +188,17 @@ class EnvironmentChecker:
|
||||
"crcmod": "crcmod-plus",
|
||||
}
|
||||
|
||||
# 特殊安装包(需要特殊处理的包)
|
||||
self.special_packages = {"pylabrobot": "git+https://github.com/Xuwznln/pylabrobot.git"}
|
||||
|
||||
# 包版本要求(包名: 最低版本)
|
||||
self.version_requirements = {
|
||||
"msgcenterpy": "0.1.8", # msgcenterpy 最低版本要求
|
||||
"msgcenterpy": "0.1.8",
|
||||
}
|
||||
|
||||
self.missing_packages = []
|
||||
self.failed_installs = []
|
||||
self.packages_need_upgrade = []
|
||||
|
||||
# 检测系统语言
|
||||
self.is_chinese = self._is_chinese_locale()
|
||||
|
||||
def _is_chinese_locale(self) -> bool:
|
||||
"""检测系统是否为中文环境"""
|
||||
try:
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
if lang and ("zh" in lang.lower() or "chinese" in lang.lower()):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
self.missing_packages: List[tuple] = []
|
||||
self.failed_installs: List[tuple] = []
|
||||
self.packages_need_upgrade: List[tuple] = []
|
||||
|
||||
def check_package_installed(self, package_name: str) -> bool:
|
||||
"""检查包是否已安装"""
|
||||
try:
|
||||
importlib.import_module(package_name)
|
||||
return True
|
||||
@@ -62,7 +206,6 @@ class EnvironmentChecker:
|
||||
return False
|
||||
|
||||
def get_package_version(self, package_name: str) -> str | None:
|
||||
"""获取已安装包的版本"""
|
||||
try:
|
||||
module = importlib.import_module(package_name)
|
||||
return getattr(module, "__version__", None)
|
||||
@@ -70,88 +213,32 @@ class EnvironmentChecker:
|
||||
return None
|
||||
|
||||
def compare_version(self, current: str, required: str) -> bool:
|
||||
"""
|
||||
比较版本号
|
||||
Returns:
|
||||
True: current >= required
|
||||
False: current < required
|
||||
"""
|
||||
try:
|
||||
current_parts = [int(x) for x in current.split(".")]
|
||||
required_parts = [int(x) for x in required.split(".")]
|
||||
|
||||
# 补齐长度
|
||||
max_len = max(len(current_parts), len(required_parts))
|
||||
current_parts.extend([0] * (max_len - len(current_parts)))
|
||||
required_parts.extend([0] * (max_len - len(required_parts)))
|
||||
|
||||
return current_parts >= required_parts
|
||||
except Exception:
|
||||
return True # 如果无法比较,假设版本满足要求
|
||||
|
||||
def install_package(self, package_name: str, pip_name: str, upgrade: bool = False) -> bool:
|
||||
"""安装包"""
|
||||
try:
|
||||
action = "升级" if upgrade else "安装"
|
||||
print_status(f"正在{action} {package_name} ({pip_name})...", "info")
|
||||
|
||||
# 构建安装命令
|
||||
cmd = [sys.executable, "-m", "pip", "install"]
|
||||
|
||||
# 如果是升级操作,添加 --upgrade 参数
|
||||
if upgrade:
|
||||
cmd.append("--upgrade")
|
||||
|
||||
cmd.append(pip_name)
|
||||
|
||||
# 如果是中文环境,使用清华镜像源
|
||||
if self.is_chinese:
|
||||
cmd.extend(["-i", "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"])
|
||||
|
||||
# 执行安装
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) # 5分钟超时
|
||||
|
||||
if result.returncode == 0:
|
||||
print_status(f"✓ {package_name} {action}成功", "success")
|
||||
return True
|
||||
else:
|
||||
print_status(f"× {package_name} {action}失败: {result.stderr}", "error")
|
||||
return False
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
print_status(f"× {package_name} {action}超时", "error")
|
||||
return False
|
||||
except Exception as e:
|
||||
print_status(f"× {package_name} {action}异常: {str(e)}", "error")
|
||||
return False
|
||||
|
||||
def upgrade_package(self, package_name: str, pip_name: str) -> bool:
|
||||
"""升级包"""
|
||||
return self.install_package(package_name, pip_name, upgrade=True)
|
||||
return True
|
||||
|
||||
def check_all_packages(self) -> bool:
|
||||
"""检查所有必需的包"""
|
||||
print_status("开始检查环境依赖...", "info")
|
||||
|
||||
# 检查常规包
|
||||
for import_name, pip_name in self.required_packages.items():
|
||||
if not self.check_package_installed(import_name):
|
||||
self.missing_packages.append((import_name, pip_name))
|
||||
else:
|
||||
# 检查版本要求
|
||||
if import_name in self.version_requirements:
|
||||
current_version = self.get_package_version(import_name)
|
||||
required_version = self.version_requirements[import_name]
|
||||
elif import_name in self.version_requirements:
|
||||
current_version = self.get_package_version(import_name)
|
||||
required_version = self.version_requirements[import_name]
|
||||
if current_version and not self.compare_version(current_version, required_version):
|
||||
print_status(
|
||||
f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})",
|
||||
"warning",
|
||||
)
|
||||
self.packages_need_upgrade.append((import_name, pip_name))
|
||||
|
||||
if current_version:
|
||||
if not self.compare_version(current_version, required_version):
|
||||
print_status(
|
||||
f"{import_name} 版本过低 (当前: {current_version}, 需要: >={required_version})",
|
||||
"warning",
|
||||
)
|
||||
self.packages_need_upgrade.append((import_name, pip_name))
|
||||
|
||||
# 检查特殊包
|
||||
for package_name, install_url in self.special_packages.items():
|
||||
if not self.check_package_installed(package_name):
|
||||
self.missing_packages.append((package_name, install_url))
|
||||
@@ -170,7 +257,6 @@ class EnvironmentChecker:
|
||||
return False
|
||||
|
||||
def install_missing_packages(self, auto_install: bool = True) -> bool:
|
||||
"""安装缺失的包"""
|
||||
if not self.missing_packages and not self.packages_need_upgrade:
|
||||
return True
|
||||
|
||||
@@ -178,62 +264,36 @@ class EnvironmentChecker:
|
||||
if self.missing_packages:
|
||||
print_status("缺失以下包:", "warning")
|
||||
for import_name, pip_name in self.missing_packages:
|
||||
print_status(f" - {import_name} (pip install {pip_name})", "warning")
|
||||
print_status(f" - {import_name} ({pip_name})", "warning")
|
||||
if self.packages_need_upgrade:
|
||||
print_status("需要升级以下包:", "warning")
|
||||
for import_name, pip_name in self.packages_need_upgrade:
|
||||
print_status(f" - {import_name} (pip install --upgrade {pip_name})", "warning")
|
||||
print_status(f" - {import_name} ({pip_name})", "warning")
|
||||
return False
|
||||
|
||||
# 安装缺失的包
|
||||
if self.missing_packages:
|
||||
print_status(f"开始自动安装 {len(self.missing_packages)} 个缺失的包...", "info")
|
||||
pkgs = [pip_name for _, pip_name in self.missing_packages]
|
||||
if not _install_packages(pkgs, label="unilabos"):
|
||||
self.failed_installs.extend(self.missing_packages)
|
||||
|
||||
success_count = 0
|
||||
for import_name, pip_name in self.missing_packages:
|
||||
if self.install_package(import_name, pip_name):
|
||||
success_count += 1
|
||||
else:
|
||||
self.failed_installs.append((import_name, pip_name))
|
||||
|
||||
print_status(f"✓ 成功安装 {success_count}/{len(self.missing_packages)} 个包", "success")
|
||||
|
||||
# 升级需要更新的包
|
||||
if self.packages_need_upgrade:
|
||||
print_status(f"开始自动升级 {len(self.packages_need_upgrade)} 个包...", "info")
|
||||
pkgs = [pip_name for _, pip_name in self.packages_need_upgrade]
|
||||
if not _install_packages(pkgs, upgrade=True, label="unilabos"):
|
||||
self.failed_installs.extend(self.packages_need_upgrade)
|
||||
|
||||
upgrade_success_count = 0
|
||||
for import_name, pip_name in self.packages_need_upgrade:
|
||||
if self.upgrade_package(import_name, pip_name):
|
||||
upgrade_success_count += 1
|
||||
else:
|
||||
self.failed_installs.append((import_name, pip_name))
|
||||
|
||||
print_status(f"✓ 成功升级 {upgrade_success_count}/{len(self.packages_need_upgrade)} 个包", "success")
|
||||
|
||||
if self.failed_installs:
|
||||
print_status(f"有 {len(self.failed_installs)} 个包操作失败:", "error")
|
||||
for import_name, pip_name in self.failed_installs:
|
||||
print_status(f" - {import_name} ({pip_name})", "error")
|
||||
return False
|
||||
|
||||
return True
|
||||
return not self.failed_installs
|
||||
|
||||
def verify_installation(self) -> bool:
|
||||
"""验证安装结果"""
|
||||
if not self.missing_packages and not self.packages_need_upgrade:
|
||||
return True
|
||||
|
||||
print_status("验证安装结果...", "info")
|
||||
|
||||
failed_verification = []
|
||||
|
||||
# 验证新安装的包
|
||||
for import_name, pip_name in self.missing_packages:
|
||||
if not self.check_package_installed(import_name):
|
||||
failed_verification.append((import_name, pip_name))
|
||||
|
||||
# 验证升级的包
|
||||
for import_name, pip_name in self.packages_need_upgrade:
|
||||
if not self.check_package_installed(import_name):
|
||||
failed_verification.append((import_name, pip_name))
|
||||
@@ -270,17 +330,14 @@ def check_environment(auto_install: bool = True, show_details: bool = True) -> b
|
||||
"""
|
||||
checker = EnvironmentChecker()
|
||||
|
||||
# 检查包
|
||||
if checker.check_all_packages():
|
||||
return True
|
||||
|
||||
# 安装缺失的包
|
||||
if not checker.install_missing_packages(auto_install):
|
||||
if show_details:
|
||||
print_status("请手动安装缺失的包后重新启动程序", "error")
|
||||
return False
|
||||
|
||||
# 验证安装
|
||||
if not checker.verify_installation():
|
||||
if show_details:
|
||||
print_status("安装验证失败,请检查网络连接或手动安装", "error")
|
||||
@@ -290,14 +347,12 @@ def check_environment(auto_install: bool = True, show_details: bool = True) -> b
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 命令行参数解析
|
||||
parser = argparse.ArgumentParser(description="UniLabOS 环境依赖检查工具")
|
||||
parser.add_argument("--no-auto-install", action="store_true", help="仅检查环境,不自动安装缺失的包")
|
||||
parser.add_argument("--silent", action="store_true", help="静默模式,不显示详细信息")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# 执行环境检查
|
||||
auto_install = not args.no_auto_install
|
||||
show_details = not args.silent
|
||||
|
||||
|
||||
Reference in New Issue
Block a user