mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-08 17:08:35 +08:00
fix(plugin): suppress delayed install reloads (#6484)
This commit is contained in:
@@ -3,6 +3,7 @@ import concurrent.futures
|
|||||||
import inspect
|
import inspect
|
||||||
import posixpath
|
import posixpath
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import (
|
from typing import (
|
||||||
@@ -167,6 +168,8 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
|||||||
"""插件管理器"""
|
"""插件管理器"""
|
||||||
CONFIG_WATCH = {"DEV", "PLUGIN_AUTO_RELOAD", "PLUGIN_LOCAL_REPO_PATHS"}
|
CONFIG_WATCH = {"DEV", "PLUGIN_AUTO_RELOAD", "PLUGIN_LOCAL_REPO_PATHS"}
|
||||||
AGENT_TOOLS_BUILD_MAX_ATTEMPTS = 3
|
AGENT_TOOLS_BUILD_MAX_ATTEMPTS = 3
|
||||||
|
# 略长于 watchfiles 默认 debounce,吸收包写入完成后才交付的延迟批次。
|
||||||
|
MONITOR_SETTLE_SECONDS = 2.0
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""初始化插件注册表、缓存和开发模式监控状态。"""
|
"""初始化插件注册表、缓存和开发模式监控状态。"""
|
||||||
@@ -213,6 +216,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
|||||||
self._recent_local_sync: Dict[str, float] = {}
|
self._recent_local_sync: Dict[str, float] = {}
|
||||||
self._monitor_suppression_lock = threading.Lock()
|
self._monitor_suppression_lock = threading.Lock()
|
||||||
self._suppressed_monitor_plugins: Dict[str, int] = {}
|
self._suppressed_monitor_plugins: Dict[str, int] = {}
|
||||||
|
self._monitor_suppressed_until: Dict[str, float] = {}
|
||||||
self._plugin_paths = PluginPathResolver(
|
self._plugin_paths = PluginPathResolver(
|
||||||
runtime_root=get_runtime_setting('ROOT_PATH') / "app" / "plugins",
|
runtime_root=get_runtime_setting('ROOT_PATH') / "app" / "plugins",
|
||||||
running=lambda: self._running_plugins,
|
running=lambda: self._running_plugins,
|
||||||
@@ -724,7 +728,7 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
|||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def suppress_plugin_monitor(self, plugin_id: str):
|
def suppress_plugin_monitor(self, plugin_id: str):
|
||||||
"""在插件目录原子更新期间阻止文件监控抢先重载半成品。"""
|
"""在插件包写入及其文件事件收敛期间阻止监控重复重载。"""
|
||||||
with self.mutation("更新插件包"):
|
with self.mutation("更新插件包"):
|
||||||
normalized_id = plugin_id.lower()
|
normalized_id = plugin_id.lower()
|
||||||
with self._monitor_suppression_lock:
|
with self._monitor_suppression_lock:
|
||||||
@@ -738,13 +742,25 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
|||||||
count = self._suppressed_monitor_plugins.get(normalized_id, 0)
|
count = self._suppressed_monitor_plugins.get(normalized_id, 0)
|
||||||
if count <= 1:
|
if count <= 1:
|
||||||
self._suppressed_monitor_plugins.pop(normalized_id, None)
|
self._suppressed_monitor_plugins.pop(normalized_id, None)
|
||||||
|
self._monitor_suppressed_until[normalized_id] = (
|
||||||
|
time.monotonic() + self.MONITOR_SETTLE_SECONDS
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._suppressed_monitor_plugins[normalized_id] = count - 1
|
self._suppressed_monitor_plugins[normalized_id] = count - 1
|
||||||
|
|
||||||
def is_plugin_monitor_suppressed(self, plugin_id: str) -> bool:
|
def is_plugin_monitor_suppressed(self, plugin_id: str) -> bool:
|
||||||
"""判断指定插件是否处于安装或替换写入阶段。"""
|
"""判断插件是否处于包写入或延迟文件事件收敛阶段。"""
|
||||||
with self._monitor_suppression_lock:
|
with self._monitor_suppression_lock:
|
||||||
return self._suppressed_monitor_plugins.get(plugin_id.lower(), 0) > 0
|
normalized_id = plugin_id.lower()
|
||||||
|
if self._suppressed_monitor_plugins.get(normalized_id, 0) > 0:
|
||||||
|
return True
|
||||||
|
suppressed_until = self._monitor_suppressed_until.get(normalized_id)
|
||||||
|
if suppressed_until is None:
|
||||||
|
return False
|
||||||
|
if time.monotonic() < suppressed_until:
|
||||||
|
return True
|
||||||
|
self._monitor_suppressed_until.pop(normalized_id, None)
|
||||||
|
return False
|
||||||
|
|
||||||
def remove_plugin(self, plugin_id: str):
|
def remove_plugin(self, plugin_id: str):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1007,9 +1007,6 @@
|
|||||||
"app/schemas/message.py": {
|
"app/schemas/message.py": {
|
||||||
"I001": 1
|
"I001": 1
|
||||||
},
|
},
|
||||||
"app/schemas/plugin.py": {
|
|
||||||
"I001": 1
|
|
||||||
},
|
|
||||||
"app/schemas/response.py": {
|
"app/schemas/response.py": {
|
||||||
"I001": 1
|
"I001": 1
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -907,8 +907,11 @@ def test_monitor_reload_refreshes_source_and_instance_routes(monkeypatch) -> Non
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_plugin_monitor_suppression_is_reference_counted(monkeypatch) -> None:
|
def test_plugin_monitor_suppression_covers_delayed_write_events(
|
||||||
"""同一插件的重叠写入必须等最后一个事务退出后才解除监控抑制。"""
|
monkeypatch,
|
||||||
|
tmp_path,
|
||||||
|
) -> None:
|
||||||
|
"""重叠写入退出后的延迟批次不重复重载,后续真实修改仍可重载。"""
|
||||||
_reset_plugin_manager()
|
_reset_plugin_manager()
|
||||||
reset_plugin_system()
|
reset_plugin_system()
|
||||||
_patch_runtime_settings(
|
_patch_runtime_settings(
|
||||||
@@ -917,7 +920,26 @@ def test_plugin_monitor_suppression_is_reference_counted(monkeypatch) -> None:
|
|||||||
PLUGIN_AUTO_RELOAD=False,
|
PLUGIN_AUTO_RELOAD=False,
|
||||||
ROOT_PATH=MagicMock(),
|
ROOT_PATH=MagicMock(),
|
||||||
)
|
)
|
||||||
|
now = [100.0]
|
||||||
|
monkeypatch.setattr(plugin_manager_module.time, "monotonic", lambda: now[0])
|
||||||
manager = PluginManager()
|
manager = PluginManager()
|
||||||
|
reload_plugin = MagicMock()
|
||||||
|
monitor = PluginChangeMonitor(
|
||||||
|
runtime_root=tmp_path,
|
||||||
|
local_roots=lambda: [],
|
||||||
|
stop_event=threading.Event(),
|
||||||
|
recent_sync={},
|
||||||
|
federated_change=lambda _path: None,
|
||||||
|
runtime_plugin=lambda _path: "DemoPlugin",
|
||||||
|
local_candidate=lambda _path: None,
|
||||||
|
sync_local=MagicMock(),
|
||||||
|
reload_plugin=reload_plugin,
|
||||||
|
dependency_manifest_status=lambda _path: None,
|
||||||
|
watch=lambda *_args, **_kwargs: (),
|
||||||
|
log=MagicMock(),
|
||||||
|
monitor_suppressed=manager.is_plugin_monitor_suppressed,
|
||||||
|
)
|
||||||
|
changes = {("modified", str(tmp_path / "demo" / "plugin.py"))}
|
||||||
|
|
||||||
with manager.suppress_plugin_monitor("DemoPlugin"):
|
with manager.suppress_plugin_monitor("DemoPlugin"):
|
||||||
assert manager.is_plugin_monitor_suppressed("demoplugin") is True
|
assert manager.is_plugin_monitor_suppressed("demoplugin") is True
|
||||||
@@ -925,7 +947,12 @@ def test_plugin_monitor_suppression_is_reference_counted(monkeypatch) -> None:
|
|||||||
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True
|
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True
|
||||||
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True
|
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is True
|
||||||
|
|
||||||
assert manager.is_plugin_monitor_suppressed("DemoPlugin") is False
|
monitor._process_changes(changes)
|
||||||
|
reload_plugin.assert_not_called()
|
||||||
|
|
||||||
|
now[0] += manager.MONITOR_SETTLE_SECONDS
|
||||||
|
monitor._process_changes(changes)
|
||||||
|
reload_plugin.assert_called_once_with("DemoPlugin")
|
||||||
_reset_plugin_manager()
|
_reset_plugin_manager()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user