mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-03 13:55:00 +08:00
fix(event): preserve handler identity after module unload
This commit is contained in:
@@ -29,11 +29,19 @@ class EventRegistry:
|
||||
self._disabled_handlers = disabled_handlers
|
||||
self._disabled_classes = disabled_classes
|
||||
|
||||
@staticmethod
|
||||
def _module_name(target: Callable | type) -> str:
|
||||
"""优先读取对象保留的模块名,避免卸载缓存后事件标识漂移。"""
|
||||
module_name = getattr(target, "__module__", None)
|
||||
if module_name:
|
||||
return module_name
|
||||
module = inspect.getmodule(target)
|
||||
return module.__name__ if module else "unknown_module"
|
||||
|
||||
@staticmethod
|
||||
def handler_identifier(target: Callable | type) -> str:
|
||||
"""返回包含模块和限定名的稳定处理器标识。"""
|
||||
module = inspect.getmodule(target)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
module_name = EventRegistry._module_name(target)
|
||||
return f"{module_name}.{target.__qualname__}"
|
||||
|
||||
@classmethod
|
||||
@@ -46,8 +54,7 @@ class EventRegistry:
|
||||
qualname_parts = handler.__qualname__.split(".")
|
||||
if len(qualname_parts) <= 1:
|
||||
return None
|
||||
module = inspect.getmodule(handler)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
module_name = cls._module_name(handler)
|
||||
return f"{module_name}.{'.'.join(qualname_parts[:-1])}"
|
||||
|
||||
def is_handler_enabled(self, handler: Callable) -> bool:
|
||||
|
||||
@@ -10,7 +10,9 @@ from app.runtime.event.binding import (
|
||||
EventBindingResolver,
|
||||
EventHandlerBinding,
|
||||
)
|
||||
from app.runtime.event.dispatch import EventDispatcher
|
||||
from app.runtime.event.errors import EventErrorPolicy
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.events import Event
|
||||
from app.runtime.extensions.plugin import manager as plugin_manager_module
|
||||
from app.runtime.extensions.plugin.manager import PluginManager
|
||||
@@ -85,14 +87,69 @@ def test_unloaded_module_class_handler_is_skipped() -> None:
|
||||
resolvers=lambda: {},
|
||||
)
|
||||
assert binding.resolve(residual_handler) is None
|
||||
# 模块卸载后 identifier 回退为 unknown_module 前缀,与线上日志一致
|
||||
assert binding.unresolved_handlers() == (
|
||||
"unknown_module._ResidualPlugin.reload",
|
||||
f"{fake_name}._ResidualPlugin.reload",
|
||||
)
|
||||
finally:
|
||||
sys.modules.pop(fake_name, None)
|
||||
|
||||
|
||||
def test_quiesced_handler_stays_disabled_after_module_unload() -> None:
|
||||
"""事件快照中的旧 handler 在模块卸载后仍应按已禁用状态跳过。"""
|
||||
fake_name = "tests._fake_quiesced_plugin"
|
||||
fake_module = types.ModuleType(fake_name)
|
||||
sys.modules[fake_name] = fake_module
|
||||
try:
|
||||
exec(
|
||||
"class _QuiescedPlugin:\n"
|
||||
" def reload(self, event):\n"
|
||||
" raise AssertionError('quiesced handler must not run')\n",
|
||||
fake_module.__dict__,
|
||||
)
|
||||
residual_handler = fake_module._QuiescedPlugin.reload
|
||||
broadcast_subscribers: dict = {}
|
||||
disabled_handlers: set[str] = set()
|
||||
disabled_classes: set[str] = set()
|
||||
registry = EventRegistry(
|
||||
lock=threading.Lock(),
|
||||
broadcast_subscribers=lambda: broadcast_subscribers,
|
||||
chain_subscribers=lambda: {},
|
||||
disabled_handlers=lambda: disabled_handlers,
|
||||
disabled_classes=lambda: disabled_classes,
|
||||
)
|
||||
registry.add(EventType.PluginReload, residual_handler, 0)
|
||||
registry.disable(fake_module._QuiescedPlugin)
|
||||
del sys.modules[fake_name]
|
||||
|
||||
assert EventRegistry.handler_identifier(residual_handler) == (
|
||||
f"{fake_name}._QuiescedPlugin.reload"
|
||||
)
|
||||
assert EventRegistry.handler_class_identifier(residual_handler) == (
|
||||
f"{fake_name}._QuiescedPlugin"
|
||||
)
|
||||
assert not registry.is_handler_enabled(residual_handler)
|
||||
|
||||
binding = EventBindingResolver(
|
||||
lock=threading.Lock(),
|
||||
resolvers=lambda: {},
|
||||
)
|
||||
dispatcher = EventDispatcher(
|
||||
registry=registry,
|
||||
binding_resolver=binding,
|
||||
event_factory=Event,
|
||||
error_handler=lambda **_kwargs: None,
|
||||
async_handle_sink=lambda _handle: True,
|
||||
sync_handle_sink=lambda _callback, _args: True,
|
||||
)
|
||||
dispatcher.dispatch_broadcast_strict(
|
||||
Event(EventType.PluginReload, {"plugin_id": "_QuiescedPlugin"}),
|
||||
lambda _awaitable: None,
|
||||
)
|
||||
assert binding.unresolved_handlers() == ()
|
||||
finally:
|
||||
sys.modules.pop(fake_name, None)
|
||||
|
||||
|
||||
def test_unloaded_module_decorator_wrapped_method_is_skipped() -> None:
|
||||
"""装饰器包装的类方法限定名含 <locals>,模块卸载后也必须跳过而非直调。"""
|
||||
fake_name = "tests._fake_unloaded_decorated_plugin"
|
||||
@@ -121,7 +178,7 @@ def test_unloaded_module_decorator_wrapped_method_is_skipped() -> None:
|
||||
)
|
||||
assert binding.resolve(residual_handler) is None
|
||||
assert binding.unresolved_handlers() == (
|
||||
"unknown_module._deco.<locals>.wrapper",
|
||||
f"{fake_name}._deco.<locals>.wrapper",
|
||||
)
|
||||
finally:
|
||||
sys.modules.pop(fake_name, None)
|
||||
|
||||
Reference in New Issue
Block a user