mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-19 05:03:57 +08:00
332 lines
13 KiB
Python
332 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.config import settings
|
||
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,
|
||
MessageChannel,
|
||
ModuleType,
|
||
OtherModulesType,
|
||
StorageSchema,
|
||
)
|
||
|
||
|
||
class ModuleManager(metaclass=Singleton):
|
||
"""以 Capability Runtime 管理宿主模块,并保留旧插件同步查询合同。"""
|
||
|
||
SubType = Union[
|
||
DownloaderType,
|
||
MediaServerType,
|
||
MessageChannel,
|
||
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()
|