mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
334 lines
13 KiB
Python
334 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import threading
|
|
from typing import Any, Generator, List, Optional, Tuple, Union
|
|
|
|
from app.foundation.reflection import ObjectUtils
|
|
from app.foundation.singleton import Singleton
|
|
from app.runtime.capabilities.model import (
|
|
CapabilityLifecycleState,
|
|
CapabilityObservation,
|
|
CapabilitySpec,
|
|
)
|
|
from app.runtime.capabilities.runtime import CapabilityRuntime
|
|
from app.runtime.settings import RuntimeSettingsCompat
|
|
|
|
settings = RuntimeSettingsCompat()
|
|
from app.runtime.events import Event, EventHandlerBinding, eventmanager
|
|
from app.runtime.extensions.host_module_adapter import (
|
|
HOST_MODULE_KIND,
|
|
HostModuleAdapter,
|
|
build_host_module_registry,
|
|
capture_host_module_config,
|
|
should_run_host_module,
|
|
)
|
|
from app.runtime.log import logger
|
|
from app.schemas.types import (
|
|
DownloaderType,
|
|
EventType,
|
|
MediaRecognizeType,
|
|
MediaServerType,
|
|
NotificationChannel,
|
|
ModuleType,
|
|
OtherModulesType,
|
|
StorageSchema,
|
|
)
|
|
|
|
|
|
class ModuleManager(metaclass=Singleton):
|
|
"""以 Capability Runtime 管理宿主模块,并保留旧插件同步查询合同。"""
|
|
|
|
SubType = Union[
|
|
DownloaderType,
|
|
MediaServerType,
|
|
NotificationChannel,
|
|
StorageSchema,
|
|
OtherModulesType,
|
|
MediaRecognizeType,
|
|
]
|
|
|
|
def __init__(self) -> None:
|
|
"""发现 data-only manifest,并按当前配置激活所需宿主模块。"""
|
|
self._lock = threading.RLock()
|
|
self._lifecycle_lock = threading.RLock()
|
|
self._modules: dict[str, type] = {}
|
|
self._running_modules: dict[str, Any] = {}
|
|
registry = build_host_module_registry()
|
|
self._runtime = CapabilityRuntime(
|
|
registry,
|
|
adapters={HOST_MODULE_KIND: HostModuleAdapter()},
|
|
observer=self._observe_transition,
|
|
)
|
|
# pkgutil 的既有发现顺序按一级包名稳定排列,兼容视图继续保持该顺序。
|
|
self._specs = tuple(
|
|
sorted(self._runtime.list_specs(), key=lambda item: item.source.parent.name)
|
|
)
|
|
eventmanager.register_handler_instance_resolver(
|
|
"modules",
|
|
self.resolve_event_handler_instance,
|
|
)
|
|
eventmanager.add_event_listener(
|
|
EventType.ConfigChanged,
|
|
self.handle_config_changed,
|
|
)
|
|
self.load_modules()
|
|
|
|
@staticmethod
|
|
def _observe_transition(observation: CapabilityObservation) -> None:
|
|
"""把 Runtime 的稳定转换结果接入现有日志面。"""
|
|
if observation.outcome == "failed":
|
|
logger.error(
|
|
"Host Module %s %s 失败:%s",
|
|
observation.capability_id,
|
|
observation.operation,
|
|
observation.error,
|
|
)
|
|
elif observation.outcome == "succeeded":
|
|
logger.debug(
|
|
"Host Module %s %s 完成,generation=%s,耗时=%.2fms",
|
|
observation.capability_id,
|
|
observation.operation,
|
|
observation.generation,
|
|
observation.duration_ms,
|
|
)
|
|
|
|
@staticmethod
|
|
def _event_changed_keys(event: Optional[Event]) -> set[str]:
|
|
"""兼容对象和 dict 两种配置事件载荷。"""
|
|
if not event:
|
|
return set()
|
|
event_data = event.event_data
|
|
if isinstance(event_data, dict):
|
|
keys = event_data.get("key", set())
|
|
else:
|
|
keys = getattr(event_data, "key", set())
|
|
if isinstance(keys, str):
|
|
return {keys}
|
|
return {str(key) for key in (keys or set())}
|
|
|
|
def _remember_materialized(self, module_id: str, implementation: type) -> type:
|
|
"""更新旧 `_modules` 视图,但不改变能力资源生命周期。"""
|
|
with self._lock:
|
|
self._modules[module_id] = implementation
|
|
return implementation
|
|
|
|
def _consumer_materialized_class(self, spec: CapabilitySpec) -> Optional[type]:
|
|
"""识别插件显式旧导入产生的真实类,不触发新的 Python import。"""
|
|
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
|
module = sys.modules.get(module_name)
|
|
namespace = getattr(module, "__dict__", None) if module is not None else None
|
|
if not isinstance(namespace, dict):
|
|
return None
|
|
implementation = namespace.get(symbol_name)
|
|
return implementation if isinstance(implementation, type) else None
|
|
|
|
def _refresh_running_projection(self) -> None:
|
|
"""从 Runtime 已发布实例重建插件可见的运行模块字典。"""
|
|
running = {
|
|
spec.id: instance
|
|
for spec in self._specs
|
|
if (instance := self._runtime.get_running(spec.id)) is not None
|
|
}
|
|
with self._lock:
|
|
self._running_modules = running
|
|
|
|
def resolve_event_handler_instance(
|
|
self,
|
|
owner_class: type,
|
|
) -> Optional[EventHandlerBinding]:
|
|
"""按 canonical class identity 绑定当前 generation,停止态阻断 fallback 构造。"""
|
|
for spec in self._specs:
|
|
with self._lock:
|
|
implementation = self._modules.get(spec.id)
|
|
if implementation is None:
|
|
implementation = self._consumer_materialized_class(spec)
|
|
if implementation is not None:
|
|
self._remember_materialized(spec.id, implementation)
|
|
# 同步 Runtime 的物化观测,但不创建或启动实例。
|
|
self._runtime.snapshot(spec.id)
|
|
if implementation is not owner_class:
|
|
continue
|
|
return EventHandlerBinding(
|
|
instance=self._runtime.get_running(spec.id),
|
|
owner_name=str(spec.metadata["name"]),
|
|
)
|
|
return None
|
|
|
|
def _reconcile(
|
|
self,
|
|
*,
|
|
reason: str,
|
|
changed_keys: Optional[set[str]] = None,
|
|
reload_running: bool = False,
|
|
) -> None:
|
|
"""以一次配置快照串行协调需要启动、重载或停止的能力。"""
|
|
with self._lifecycle_lock:
|
|
selected = tuple(
|
|
spec
|
|
for spec in self._specs
|
|
if changed_keys is None or changed_keys.intersection(spec.watch)
|
|
)
|
|
snapshot = capture_host_module_config(selected)
|
|
for spec in selected:
|
|
desired = should_run_host_module(spec, snapshot)
|
|
running = self._runtime.get_running(spec.id)
|
|
try:
|
|
if desired and running is None:
|
|
instance = self._runtime.activate(
|
|
spec.id,
|
|
reason=reason,
|
|
retry=True,
|
|
)
|
|
self._remember_materialized(spec.id, type(instance))
|
|
elif desired and running is not None and reload_running:
|
|
instance = self._runtime.reload(spec.id, reason=reason)
|
|
self._remember_materialized(spec.id, type(instance))
|
|
elif not desired and running is not None:
|
|
self._runtime.stop(spec.id, reason=reason)
|
|
except Exception:
|
|
# 单能力失败由 Runtime 完整记录;其它无依赖能力继续 reconcile。
|
|
continue
|
|
self._refresh_running_projection()
|
|
|
|
def load_modules(self) -> None:
|
|
"""按当前配置启动未运行模块;已运行模块保持当前 generation。"""
|
|
self._reconcile(reason="module_manager_load")
|
|
|
|
def handle_config_changed(self, event: Event) -> None:
|
|
"""配置变更时仅协调 watch 命中的能力,并保证单一生命周期 writer。"""
|
|
changed_keys = self._event_changed_keys(event)
|
|
if not changed_keys:
|
|
return
|
|
self._reconcile(
|
|
reason="config_changed",
|
|
changed_keys=changed_keys,
|
|
reload_running=True,
|
|
)
|
|
|
|
def stop(self) -> None:
|
|
"""停止全部运行模块但保留 Runtime,使旧插件可随后再次 load。"""
|
|
logger.info("正在停止所有模块...")
|
|
with self._lifecycle_lock:
|
|
for spec in reversed(self._specs):
|
|
snapshot = self._runtime.snapshot(spec.id)
|
|
if (
|
|
self._runtime.get_running(spec.id) is None
|
|
and snapshot.lifecycle is not CapabilityLifecycleState.FAILED
|
|
):
|
|
continue
|
|
try:
|
|
self._runtime.stop(spec.id, reason="module_manager_stop")
|
|
except Exception:
|
|
continue
|
|
self._refresh_running_projection()
|
|
logger.info("所有模块停止完成")
|
|
|
|
def shutdown(self) -> None:
|
|
"""进程关闭时不可逆停止 Runtime,阻止并发能力重新发布。"""
|
|
logger.info("正在关闭模块运行时...")
|
|
with self._lifecycle_lock:
|
|
self._runtime.shutdown(reason="application_shutdown")
|
|
self._refresh_running_projection()
|
|
logger.info("模块运行时关闭完成")
|
|
|
|
def reload(self) -> None:
|
|
"""保留旧插件可观察的 stop、load、ModuleReload 同步顺序。"""
|
|
with self._lifecycle_lock:
|
|
self.stop()
|
|
self.load_modules()
|
|
eventmanager.send_event(etype=EventType.ModuleReload, data={})
|
|
|
|
def test(self, modleid: str) -> Tuple[bool, str]:
|
|
"""测试已运行模块;未启用模块保持旧合同返回 `(False, "")`。"""
|
|
module = self.get_running_module(modleid)
|
|
if module is None:
|
|
return False, ""
|
|
if hasattr(module, "test") and ObjectUtils.check_method(module.test):
|
|
result = module.test()
|
|
return result if result else (False, "")
|
|
return True, "模块不支持测试"
|
|
|
|
@staticmethod
|
|
def check_setting(setting: Optional[tuple]) -> bool:
|
|
"""保留旧模块开关的 truthy 与 membership 判定语义。"""
|
|
if not setting:
|
|
return True
|
|
switch, value = setting
|
|
option = getattr(settings, switch)
|
|
if not option:
|
|
return False
|
|
if value is True:
|
|
return True
|
|
return value in option
|
|
|
|
def get_running_module(self, module_id: str) -> Any:
|
|
"""根据模块 ID 返回已发布的运行实例,不触发物化。"""
|
|
if not module_id or self._runtime.get_spec(module_id) is None:
|
|
return None
|
|
return self._runtime.get_running(module_id)
|
|
|
|
def _running_snapshot(self) -> tuple[Any, ...]:
|
|
"""直接读取 Runtime 发布视图,转换期间不暴露旧或候选实例。"""
|
|
return tuple(
|
|
instance
|
|
for spec in self._specs
|
|
if (instance := self._runtime.get_running(spec.id)) is not None
|
|
)
|
|
|
|
def get_running_modules(self, method: str) -> Generator:
|
|
"""返回实现了指定方法的运行模块快照。"""
|
|
for module in self._running_snapshot():
|
|
candidate = getattr(module, method, None)
|
|
if callable(candidate) and ObjectUtils.check_method(candidate):
|
|
yield module
|
|
|
|
def get_running_type_modules(self, module_type: ModuleType) -> Generator:
|
|
"""返回指定类型的运行模块快照。"""
|
|
for module in self._running_snapshot():
|
|
if module.get_type() == module_type:
|
|
yield module
|
|
|
|
def get_running_subtype_module(self, module_subtype: SubType) -> Generator:
|
|
"""返回指定子类型的运行模块快照。"""
|
|
for module in self._running_snapshot():
|
|
if module.get_subtype() == module_subtype:
|
|
yield module
|
|
|
|
def get_module(self, module_id: str) -> Any:
|
|
"""显式物化并返回 canonical 模块类;失败保持旧合同返回 None。"""
|
|
if not module_id or self._runtime.get_spec(module_id) is None:
|
|
return None
|
|
with self._lock:
|
|
implementation = self._modules.get(module_id)
|
|
if implementation is not None:
|
|
return implementation
|
|
try:
|
|
implementation = self._runtime.materialize(
|
|
module_id,
|
|
reason="compat_get_module",
|
|
retry=True,
|
|
)
|
|
except Exception:
|
|
return None
|
|
return self._remember_materialized(module_id, implementation)
|
|
|
|
def get_modules(self) -> dict[str, type]:
|
|
"""兼容性显式物化全部真实类;单个失败不阻断其它模块。"""
|
|
for spec in self._specs:
|
|
self.get_module(spec.id)
|
|
with self._lock:
|
|
return dict(self._modules)
|
|
|
|
def get_module_ids(self) -> List[str]:
|
|
"""从 manifest 返回全部模块 ID,不物化实现。"""
|
|
return [spec.id for spec in self._specs]
|
|
|
|
def list_specs(self) -> tuple[CapabilitySpec, ...]:
|
|
"""返回全部轻量模块声明,包含物化或启动失败的能力。"""
|
|
return self._specs
|
|
|
|
def get_specs(self) -> tuple[CapabilitySpec, ...]:
|
|
"""兼容内部调用命名,返回与 `list_specs` 相同的声明快照。"""
|
|
return self.list_specs()
|