Files
MoviePilot/app/runtime/event/binding.py
T
jxxghp 27ae1b5290 fix: 插件重载窗口期装饰器包装类方法误判直调导致 missing event TypeError
事件绑定解析在 owner_class 为 None 时仅靠限定名区分自由函数与类方法声明,
装饰器包装的方法限定名含 <locals> 被误判为自由函数 unbound 直调,把 event
吞进 self 触发 TypeError。局部作用域限定名无法区分包装方法与局部自由函数,
按调用约定兜底:签名首参为 self/cls 才视为类方法声明跳过执行。
2026-08-20 08:51:37 +08:00

175 lines
6.8 KiB
Python

"""事件处理器声明到运行实例的显式绑定解析。"""
from __future__ import annotations
import inspect
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional, Type
from app.runtime.event.registry import EventRegistry
from app.runtime.log import logger
@dataclass(frozen=True, slots=True)
class EventHandlerBinding:
"""描述上层运行时为某个事件处理器提供的实例绑定。"""
instance: Optional[Any]
owner_name: str
run_sync_in_threadpool: bool = False
HandlerInstanceResolver = Callable[
[Type[Any]], Optional[EventHandlerBinding]
]
class EventBindingResolver:
"""只通过已登记 resolver 把类处理器绑定到托管运行实例。"""
def __init__(
self,
*,
lock: Any,
resolvers: Callable[[], dict[str, HandlerInstanceResolver]],
) -> None:
"""绑定 resolver 存储,并记录未命中的处理器用于启动诊断。"""
self._lock = lock
self._resolvers = resolvers
self._unresolved: set[str] = set()
def register(self, name: str, resolver: HandlerInstanceResolver) -> None:
"""注册或替换命名实例解析器。"""
with self._lock:
self._resolvers()[name] = resolver
def unresolved_handlers(self) -> tuple[str, ...]:
"""返回本进程中未被显式 resolver 接管的类处理器。"""
with self._lock:
return tuple(sorted(self._unresolved))
@staticmethod
def parse_handler_names(handler: Callable) -> tuple[str, str]:
"""解析处理器限定名中的类名和方法名。"""
names = handler.__qualname__.split(".")
if len(names) < 2:
return "", names[0]
return names[0], names[1]
@staticmethod
def is_class_method_declaration(handler: Callable) -> bool:
"""判断处理器是否声明在类体内(限定名含类前缀或签名首参为 self/cls)。
模块级顶层自由函数的限定名不含 ``.``;类方法、装饰器包装和嵌套函数
的限定名含 ``.``。局部作用域自由函数(如测试内联 handler)限定名形如
``func.<locals>.handler``,与装饰器包装方法 ``SiteStatistic.<locals>.
wrapper`` 无法靠限定名区分,需按调用约定兜底:签名首参为 self/cls
才视为类方法声明。模块卸载后残留的类方法一旦被 unbound 直调,会把
event 吞进 self 触发 missing event TypeError,因此必须跳过等待重载自愈。
"""
parts = handler.__qualname__.split(".")
if len(parts) < 2:
return False
if "<locals>" not in parts:
return True
try:
parameters = list(inspect.signature(handler).parameters.values())
except (TypeError, ValueError):
return True
if not parameters:
return True
return parameters[0].name in ("self", "cls")
@staticmethod
def owner_class(handler: Callable) -> Optional[Type[Any]]:
"""从处理器对象本身解析声明类,不按字符串动态导入模块。"""
if inspect.ismethod(handler):
owner = handler.__self__
return owner if isinstance(owner, type) else type(owner)
module = inspect.getmodule(handler)
if not module:
return None
owner: Any = module
for part in handler.__qualname__.split(".")[:-1]:
if part == "<locals>":
return None
owner = getattr(owner, part, None)
if owner is None:
return None
return owner if isinstance(owner, type) else None
def _record_unresolved(self, identifier: str, reason: str) -> None:
"""首次未命中时记录告警,避免重载窗口内重复刷屏。"""
with self._lock:
first_miss = identifier not in self._unresolved
self._unresolved.add(identifier)
if first_miss:
logger.warning(reason, identifier)
def resolve(
self,
handler: Callable,
) -> Optional[tuple[Callable, EventHandlerBinding, str, str]]:
"""通过显式 resolver 解析当前实例方法;自由函数直接返回。"""
owner_class = self.owner_class(handler)
method_name = getattr(
handler,
"__name__",
self.parse_handler_names(handler)[1],
)
if owner_class is None:
# 插件重载会先清除模块缓存,窗口期内残留的旧类方法声明无法定位声明类;
# 此时直接调用原始函数会绕过实例绑定(旧签名可能与事件调用约定不一致),
# 因此按未绑定处理跳过,等待重载完成后按新 handler 注册自愈。
if self.is_class_method_declaration(handler):
self._record_unresolved(
EventRegistry.handler_identifier(handler),
"事件处理器所属模块已卸载或声明类不可解析,跳过执行:%s",
)
return None
binding = EventHandlerBinding(
instance=None,
owner_name=EventRegistry.handler_identifier(handler),
run_sync_in_threadpool=True,
)
return handler, binding, "", method_name
with self._lock:
resolvers = tuple(self._resolvers().items())
binding = None
resolver_name = ""
for name, resolver in resolvers:
candidate = resolver(owner_class)
if candidate is not None:
binding = candidate
resolver_name = name
break
if binding is None:
self._record_unresolved(
EventRegistry.handler_identifier(handler),
"事件处理器未绑定显式 resolver,已跳过:%s",
)
return None
logger.debug(
"事件处理器绑定:%s -> %s",
EventRegistry.handler_identifier(handler),
resolver_name,
)
if binding.instance is None:
return None
method = getattr(binding.instance, method_name, None)
if not callable(method):
fallback_name = self.parse_handler_names(handler)[1]
method = getattr(binding.instance, fallback_name, None)
if fallback_name == method_name or not callable(method):
logger.warning(
"事件处理器 %s 无法解析为实例方法 %s.%s,跳过执行",
EventRegistry.handler_identifier(handler),
owner_class.__name__,
method_name,
)
return None
method_name = fallback_name
return method, binding, owner_class.__name__, method_name