mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-29 19:37:54 +08:00
198 lines
7.7 KiB
Python
198 lines
7.7 KiB
Python
"""
|
||
插件依赖检查与安装运行时服务。
|
||
"""
|
||
|
||
import time
|
||
from collections.abc import Callable
|
||
from dataclasses import dataclass
|
||
from typing import Any, Optional
|
||
|
||
from app.runtime.extensions.plugin.registry import PluginRegistry
|
||
from app.runtime.extensions.plugin.system import PluginSystemServices
|
||
from app.schemas.plugin import PluginInstance, PluginRuntimeStatus
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class PluginDependencyInstallResult:
|
||
"""
|
||
记录插件依赖检查结果,区分无缺失、安装成功和安装失败。
|
||
|
||
:param missing: 检测到缺失或不满足版本约束的依赖声明
|
||
:param success: 依赖已满足或安装成功时为 True,安装失败时为 False
|
||
"""
|
||
|
||
missing: list[str]
|
||
success: bool
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class PluginDependencyClassification:
|
||
"""
|
||
按当前源码和 Python 环境划分已安装插件。
|
||
|
||
:param ready: 源码存在且依赖满足的插件标识
|
||
:param missing_dependencies: 源码存在但依赖不满足的插件标识
|
||
:param missing_source: 缺少可加载源码的插件标识
|
||
"""
|
||
|
||
ready: tuple[str, ...]
|
||
missing_dependencies: tuple[str, ...]
|
||
missing_source: tuple[str, ...]
|
||
|
||
|
||
class PluginDependencyService:
|
||
"""
|
||
执行缺失插件依赖的发现和安装,不参与插件生命周期。
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
*,
|
||
system: Callable[[], PluginSystemServices],
|
||
instances: Optional[Callable[[], dict[str, PluginInstance]]] = None,
|
||
loadable_hosts: Optional[Callable[[], set[str]]] = None,
|
||
registry: Optional[PluginRegistry] = None,
|
||
log: Any,
|
||
) -> None:
|
||
"""
|
||
保存插件系统、应装载实例、应装载本体和运行状态端口。
|
||
"""
|
||
self._system = system
|
||
self._instances = instances or (lambda: {})
|
||
self._loadable_hosts = loadable_hosts
|
||
self._registry = registry
|
||
self._logger = log
|
||
|
||
def _begin_missing_install(self, missing: list[str]) -> Optional[float]:
|
||
"""
|
||
统一无缺失短路、安装清单日志和耗时起点。
|
||
"""
|
||
if not missing:
|
||
return None
|
||
self._logger.debug(f"检测到缺失的依赖项: {missing}")
|
||
self._logger.info(f"开始安装缺失的依赖项,共 {len(missing)} 个...")
|
||
return time.time()
|
||
|
||
def _complete_missing_install(
|
||
self,
|
||
missing: list[str],
|
||
success: bool,
|
||
started_at: float,
|
||
) -> PluginDependencyInstallResult:
|
||
"""
|
||
统一安装结果、耗时和成功或失败日志分类。
|
||
"""
|
||
elapsed = time.time() - started_at
|
||
if success:
|
||
self._logger.info(
|
||
f"已完成 {len(missing)} 个依赖项安装,总耗时:{elapsed:.2f} 秒"
|
||
)
|
||
else:
|
||
self._logger.warning(
|
||
f"存在缺失依赖项安装失败,请尝试手动安装,总耗时:{elapsed:.2f} 秒"
|
||
)
|
||
return PluginDependencyInstallResult(missing=missing, success=success)
|
||
|
||
def install_missing_with_status(self) -> PluginDependencyInstallResult:
|
||
"""
|
||
安装缺失依赖并返回安装器的明确结果。
|
||
"""
|
||
installer = self._system().dependency
|
||
missing = installer.find_missing()
|
||
started_at = self._begin_missing_install(missing)
|
||
if started_at is None:
|
||
return PluginDependencyInstallResult(missing=[], success=True)
|
||
success, _message = installer.install(missing)
|
||
return self._complete_missing_install(missing, success, started_at)
|
||
|
||
def install_missing(self) -> list[str]:
|
||
"""
|
||
安装当前环境缺失的插件依赖并保持历史列表返回合同。
|
||
"""
|
||
return self.install_missing_with_status().missing
|
||
|
||
async def async_install_missing_with_status(self) -> PluginDependencyInstallResult:
|
||
"""
|
||
在异步启动链中恢复缺失依赖,确保安装子进程可取消。
|
||
"""
|
||
installer = self._system().dependency
|
||
missing = await installer.async_find_missing()
|
||
started_at = self._begin_missing_install(missing)
|
||
if started_at is None:
|
||
return PluginDependencyInstallResult(missing=[], success=True)
|
||
success, _message = await installer.async_install(missing)
|
||
return self._complete_missing_install(missing, success, started_at)
|
||
|
||
def classify_plugins(self) -> PluginDependencyClassification:
|
||
"""
|
||
分类应当装载的物理插件,并把源码结论映射到应当装载的虚拟实例。
|
||
|
||
物理插件那一层按安装清单划分,而安装清单只回答「包在不在磁盘上」;分类结果
|
||
随后会被逐个 ``start()``,因此这里必须先按启用位过滤掉停用的本体,否则停用
|
||
的插件会在开机与配置热重载时被重新拉起来,启用位形同虚设。三个桶一起过滤:
|
||
停用的本体不应装载;启用分身仍按过滤前的共享源码结论分类。依赖安装只覆盖
|
||
存在启用本体或分身的源码插件,由启动层向安装器注入该集合。
|
||
"""
|
||
ready, missing_dependencies, missing_source = (
|
||
self._system().dependency.classify_plugins()
|
||
)
|
||
# 分身只共享源码和依赖,本体的停用位不能让启用分身丢失源码结论。
|
||
source_ready = set(ready)
|
||
source_pending = set(missing_dependencies)
|
||
loadable = self._loadable_hosts() if self._loadable_hosts is not None else None
|
||
if loadable is not None:
|
||
ready = [plugin_id for plugin_id in ready if plugin_id in loadable]
|
||
missing_dependencies = [
|
||
plugin_id for plugin_id in missing_dependencies if plugin_id in loadable
|
||
]
|
||
missing_source = [
|
||
plugin_id for plugin_id in missing_source if plugin_id in loadable
|
||
]
|
||
ready = list(ready)
|
||
missing_dependencies = list(missing_dependencies)
|
||
missing_source = list(missing_source)
|
||
for instance in self._instances().values():
|
||
if instance.source_plugin_id in source_ready:
|
||
ready.append(instance.instance_id)
|
||
elif instance.source_plugin_id in source_pending:
|
||
missing_dependencies.append(instance.instance_id)
|
||
else:
|
||
missing_source.append(instance.instance_id)
|
||
return PluginDependencyClassification(
|
||
ready=tuple(ready),
|
||
missing_dependencies=tuple(missing_dependencies),
|
||
missing_source=tuple(missing_source),
|
||
)
|
||
|
||
def apply_classification(
|
||
self,
|
||
classification: PluginDependencyClassification,
|
||
) -> None:
|
||
"""
|
||
把依赖分类写入唯一注册表,已激活插件保持当前状态。
|
||
"""
|
||
if self._registry is None:
|
||
raise RuntimeError("插件依赖状态注册表尚未装配")
|
||
running_ids = set(self._registry.running_ids())
|
||
for plugin_id in classification.missing_source:
|
||
self._registry.set_runtime_status(
|
||
plugin_id,
|
||
PluginRuntimeStatus.SOURCE_MISSING,
|
||
)
|
||
for plugin_id in classification.missing_dependencies:
|
||
self._registry.set_runtime_status(
|
||
plugin_id,
|
||
PluginRuntimeStatus.DEPENDENCY_PENDING,
|
||
)
|
||
for plugin_id in classification.ready:
|
||
current_status = self._registry.runtime_status(plugin_id)
|
||
if (
|
||
plugin_id in running_ids
|
||
and current_status is not PluginRuntimeStatus.DEPENDENCY_PENDING
|
||
):
|
||
continue
|
||
self._registry.set_runtime_status(
|
||
plugin_id,
|
||
PluginRuntimeStatus.READY,
|
||
)
|