ShiErFeng/shierfeng-fastapi-backend/cli/runtime/ops/support.py
2026-07-24 09:43:10 +08:00

242 lines
8.7 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.

import os
import platform
import socket
import time
from dataclasses import dataclass
from importlib import metadata
from typing import Any
from cli.exit_codes import RUNTIME_ERROR
from .gateway import OperationsInfrastructureGateway
@dataclass(frozen=True)
class OperationsDependencySpec:
"""
运维依赖检查项定义。
:param package_name: CLI 展示用包名
:param distribution: Python 分发名称
:param required: 是否为必需依赖
:param version: 显式版本,存在时跳过环境读取
"""
package_name: str
distribution: str
required: bool
version: str | None = None
class OperationsDependencyInspector:
"""
运维依赖检查器。
该对象负责核心依赖声明、版本探测与依赖检查结果构建,
使运行时 facade 不再直接维护细碎依赖定义。
"""
@staticmethod
def read_package_version(distribution_name: str) -> str | None:
"""
读取指定 Python 包的已安装版本号。
:param distribution_name: Python 包分发名称
:return: 已安装版本号,未安装时返回 `None`
"""
try:
return metadata.version(distribution_name)
except metadata.PackageNotFoundError:
return None
@staticmethod
def build_dependency_specs(*, include_dev: bool = False) -> list[OperationsDependencySpec]:
"""
构建当前依赖检查项列表。
:param include_dev: 是否附带开发阶段依赖
:return: 依赖检查项列表
"""
dependency_specs = [
OperationsDependencySpec('python', '', True, platform.python_version()),
OperationsDependencySpec('fastapi', 'fastapi', True),
OperationsDependencySpec('uvicorn', 'uvicorn', True),
OperationsDependencySpec('sqlalchemy', 'SQLAlchemy', True),
OperationsDependencySpec('alembic', 'alembic', True),
OperationsDependencySpec('redis', 'redis', True),
OperationsDependencySpec('pydantic', 'pydantic', True),
OperationsDependencySpec('pydantic-settings', 'pydantic-settings', True),
OperationsDependencySpec('typer', 'typer', True),
]
if include_dev:
dependency_specs.extend(
[
OperationsDependencySpec('pytest', 'pytest', False),
OperationsDependencySpec('ruff', 'ruff', False),
]
)
return dependency_specs
def inspect(self, *, include_dev: bool = False) -> dict[str, Any]:
"""
读取 CLI 和后端运行所依赖的核心 Python 包版本。
:param include_dev: 是否附带开发阶段依赖
:return: 依赖检查结果
"""
packages: dict[str, dict[str, Any]] = {}
missing_required: list[str] = []
for dependency_spec in self.build_dependency_specs(include_dev=include_dev):
version = dependency_spec.version or self.read_package_version(dependency_spec.distribution)
installed = bool(version)
package_payload = {
'installed': installed,
'version': version or '',
'required': dependency_spec.required,
}
if dependency_spec.distribution:
package_payload['distribution'] = dependency_spec.distribution
packages[dependency_spec.package_name] = package_payload
if dependency_spec.required and not installed:
missing_required.append(dependency_spec.package_name)
if missing_required:
return {
'ok': False,
'message': '存在缺失的核心运行依赖',
'missingRequired': missing_required,
'includeDev': include_dev,
'packages': packages,
'exit_code': RUNTIME_ERROR,
}
return {
'ok': True,
'message': '核心运行依赖已安装',
'missingRequired': [],
'includeDev': include_dev,
'packages': packages,
}
class OperationsServerInfoSupport:
"""
运维服务器信息支持对象。
该对象负责 CLI 兜底服务器信息构建与 IP 解析逻辑,
避免主运行时服务继续承载大量平台采集细节。
:param infrastructure_gateway: 运维基础设施网关
"""
def __init__(self, infrastructure_gateway: OperationsInfrastructureGateway) -> None:
"""
初始化服务器信息支持对象。
:param infrastructure_gateway: 运维基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
def resolve_server_ip(self, hostname: str) -> str:
"""
解析服务器 IP优先使用主机名解析失败时回退到网卡地址。
:param hostname: 当前主机名
:return: 可用的 IPv4 地址
"""
psutil = self.infrastructure_gateway.get_psutil_module()
try:
resolved_ip = socket.gethostbyname(hostname)
if resolved_ip:
return resolved_ip
except OSError:
pass
for interface_addresses in psutil.net_if_addrs().values():
for address_info in interface_addresses:
if (
address_info.family == socket.AF_INET
and address_info.address
and not address_info.address.startswith('127.')
):
return address_info.address
return '127.0.0.1'
def build_server_info_fallback(self) -> dict[str, Any]:
"""
构建 CLI 使用的服务器运行信息兜底数据。
:return: 与原服务输出结构兼容的服务器运行信息字典
"""
psutil = self.infrastructure_gateway.get_psutil_module()
bytes2human = self.infrastructure_gateway.get_bytes2human()
cpu_num = psutil.cpu_count(logical=True)
cpu_usage_percent = psutil.cpu_times_percent()
memory_info = psutil.virtual_memory()
hostname = socket.gethostname()
current_process = psutil.Process(os.getpid())
start_time_stamp = current_process.create_time()
current_time_stamp = time.time()
difference = current_time_stamp - start_time_stamp
days = int(difference // (24 * 60 * 60))
hours = int((difference % (24 * 60 * 60)) // (60 * 60))
minutes = int((difference % (60 * 60)) // 60)
process_memory_info = current_process.memory_info()
sys_files: list[dict[str, Any]] = []
for partition in psutil.disk_partitions():
try:
disk_usage = psutil.disk_usage(partition.mountpoint)
except Exception:
continue
mountpoint = partition.mountpoint.replace('\\', '')
sys_files.append(
{
'dirName': partition.device,
'sysTypeName': partition.fstype,
'typeName': f'本地固定磁盘({mountpoint}',
'total': bytes2human(disk_usage.total),
'used': bytes2human(disk_usage.used),
'free': bytes2human(disk_usage.free),
'usage': f'{disk_usage.percent}%',
}
)
return {
'cpu': {
'cpuNum': cpu_num,
'used': cpu_usage_percent.user,
'sys': cpu_usage_percent.system,
'free': cpu_usage_percent.idle,
},
'mem': {
'total': bytes2human(memory_info.total),
'used': bytes2human(memory_info.used),
'free': bytes2human(memory_info.free),
'usage': memory_info.percent,
},
'sys': {
'computerIp': self.resolve_server_ip(hostname),
'computerName': platform.node(),
'osArch': platform.machine(),
'osName': platform.platform(),
'userDir': os.getcwd(),
},
'py': {
'name': current_process.name(),
'version': platform.python_version(),
'startTime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp)),
'runTime': f'{days}{hours}小时{minutes}分钟',
'home': current_process.exe(),
'total': bytes2human(memory_info.available),
'used': bytes2human(process_memory_info.rss),
'free': bytes2human(memory_info.available - process_memory_info.rss),
'usage': round((process_memory_info.rss / memory_info.available) * 100, 2),
},
'sysFiles': sys_files,
}