Skip to content

Commit 3b13065

Browse files
authored
Merge pull request jxxghp#4987 from Aqr-K/refactor/plugin-monitor
2 parents 3f6c35d + db2a952 commit 3b13065

File tree

2 files changed

+340
-16
lines changed

2 files changed

+340
-16
lines changed

app/core/plugin.py

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from app.log import logger
2828
from app.schemas.types import EventType, SystemConfigKey
2929
from app.utils.crypto import RSAUtils
30-
from app.utils.limit import rate_limit_window
30+
from app.utils.debouncer import debounce
3131
from app.utils.object import ObjectUtils
3232
from app.utils.singleton import Singleton
3333
from app.utils.string import StringUtils
@@ -47,33 +47,62 @@ def on_modified(self, event):
4747
if not event_path.name.endswith(".py") or "pycache" in event_path.parts:
4848
return
4949

50-
# 读取插件根目录下的__init__.py文件,读取class XXXX(_PluginBase)的类名
50+
# 防抖模式下处理文件修改事件
51+
self._handle_modification(event_path)
52+
53+
@debounce(interval=1.0, leading=False, source="PluginMonitorHandler", enable_logging=False)
54+
def _handle_modification(self, event_path: Path):
55+
"""
56+
处理文件修改事件
57+
:param event_path:
58+
:return:
59+
"""
60+
logger.debug(f"防抖计时结束,开始处理文件修改事件: {event_path}")
61+
# 解析插件ID
62+
pid = self._get_plugin_id_from_path(event_path)
63+
if not pid:
64+
logger.debug(f"文件不属于任何有效插件,已忽略: {event_path}")
65+
return
66+
67+
# 触发重载
68+
self.__reload_plugin(pid)
69+
70+
@staticmethod
71+
def _get_plugin_id_from_path(event_path: Path) -> Optional[str]:
72+
"""
73+
根据文件路径解析出插件的ID。
74+
:param event_path: 被修改文件的 Path 对象。
75+
:return: 插件ID字符串,如果不是有效插件文件则返回 None。
76+
"""
5177
try:
5278
plugins_root = settings.ROOT_PATH / "app" / "plugins"
5379
# 确保修改的文件在 plugins 目录下
5480
if plugins_root not in event_path.parents:
55-
return
56-
# 获取插件目录路径,没有找到__init__.py时,说明不是有效包,跳过插件重载
57-
# 插件重载目前没有支持app/plugins/plugin/package/__init__.py的场景,这里也不做支持
81+
return None
82+
83+
# 找到插件的根目录
5884
plugin_dir = event_path.parent
85+
while plugin_dir.parent != plugins_root:
86+
plugin_dir = plugin_dir.parent
87+
if plugin_dir == plugins_root: # 防止无限循环
88+
break
89+
5990
init_file = plugin_dir / "__init__.py"
6091
if not init_file.exists():
61-
logger.debug(f"{plugin_dir} 下没有找到 __init__.py,跳过插件重载")
62-
return
92+
return None
6393

94+
# 读取 __init__.py 文件,查找插件主类名
6495
with open(init_file, "r", encoding="utf-8") as f:
65-
lines = f.readlines()
66-
pid = None
67-
for line in lines:
68-
if line.startswith("class") and "(_PluginBase)" in line:
69-
pid = line.split("class ")[1].split("(_PluginBase)")[0].strip()
70-
if pid:
71-
self.__reload_plugin(pid)
96+
for line in f:
97+
if line.startswith("class") and "(_PluginBase)" in line:
98+
# 解析出类名作为插件ID
99+
return line.split("class ")[1].split("(_PluginBase)")[0].strip()
100+
return None
72101
except Exception as e:
73-
logger.error(f"插件文件修改后重载出错:{str(e)}")
102+
logger.error(f"从路径解析插件ID时出错: {e}")
103+
return None
74104

75105
@staticmethod
76-
@rate_limit_window(max_calls=1, window_seconds=2, source="PluginMonitor", enable_logging=False)
77106
def __reload_plugin(pid):
78107
"""
79108
重新加载插件

app/utils/debounce.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import asyncio
2+
import functools
3+
import inspect
4+
from abc import ABC, abstractmethod
5+
from threading import Timer, Lock
6+
from typing import Callable, Any, Optional
7+
8+
from app.log import logger
9+
10+
11+
class BaseDebouncer(ABC):
12+
"""
13+
防抖器的抽象基类。定义了防抖器的基本接口和日志功能。
14+
所有防抖器实现类必须继承此类并实现其抽象方法。
15+
"""
16+
def __init__(self, func: Callable, interval: float, *,
17+
leading: bool = False, enable_logging: bool = False, source: str = ""):
18+
"""
19+
初始化防抖器实例。
20+
:param func: 要防抖的函数或协程
21+
:param interval: 防抖间隔,单位秒
22+
:param leading: 是否启用前沿模式
23+
:param enable_logging: 是否启用日志记录
24+
:param source: 日志来源标识
25+
"""
26+
self.func = func
27+
self.interval = interval
28+
self.leading = leading
29+
self.enable_logging = enable_logging
30+
self.source = source
31+
32+
@abstractmethod
33+
def __call__(self, *args, **kwargs) -> None:
34+
"""
35+
定义防抖调用的契约,子类必须实现。
36+
"""
37+
pass
38+
39+
@abstractmethod
40+
def cancel(self) -> None:
41+
"""
42+
定义取消挂起调用的契约,子类必须实现。
43+
"""
44+
pass
45+
46+
def format_log(self, message: str) -> str:
47+
"""
48+
格式化日志消息,加入 source 前缀。
49+
"""
50+
return f"[{self.source}] {message}" if self.source else message
51+
52+
def log(self, level: str, message: str):
53+
"""
54+
根据日志级别记录日志。
55+
"""
56+
if self.enable_logging:
57+
log_method = getattr(logger, level, logger.debug)
58+
log_method(self.format_log(message))
59+
60+
def log_debug(self, message: str):
61+
"""
62+
记录调试日志。
63+
"""
64+
self.log("debug", message)
65+
66+
def log_info(self, message: str):
67+
"""
68+
记录信息日志。
69+
"""
70+
self.log("info", message)
71+
72+
def log_warning(self, message: str):
73+
"""
74+
记录警告日志。
75+
"""
76+
self.log("warning", message)
77+
78+
def error(self, message: str):
79+
"""
80+
记录错误日志。
81+
"""
82+
self.log("error", message)
83+
84+
def critical(self, message: str):
85+
"""
86+
记录严重错误日志。
87+
"""
88+
self.log("critical", message)
89+
90+
91+
class Debouncer(BaseDebouncer):
92+
"""
93+
同步防抖实现类
94+
"""
95+
96+
def __init__(self, *args, **kwargs):
97+
"""
98+
初始化防抖器实例。
99+
"""
100+
super().__init__(*args, **kwargs)
101+
self.timer: Optional[Timer] = None
102+
self.lock = Lock()
103+
# 用于前沿模式,标记是否处于“冷却”或“不应期”
104+
self.is_cooling_down = False
105+
106+
def __call__(self, *args, **kwargs) -> None:
107+
"""
108+
调用防抖函数。
109+
:param args:
110+
:param kwargs:
111+
:return:
112+
"""
113+
with self.lock:
114+
if self.leading:
115+
self._call_leading(*args, **kwargs)
116+
else:
117+
self._call_trailing(*args, **kwargs)
118+
119+
def _call_leading(self, *args, **kwargs):
120+
"""
121+
前沿模式的逻辑。
122+
"""
123+
# 如果不在冷却期,则立即执行
124+
if not self.is_cooling_down:
125+
self.log_info("前沿模式: 立即执行函数。")
126+
self.func(*args, **kwargs)
127+
128+
# 无论是否执行,都重置冷却计时器
129+
if self.timer and self.timer.is_alive():
130+
self.timer.cancel()
131+
132+
# 设置自己进入冷却期
133+
self.is_cooling_down = True
134+
135+
# 在间隔结束后,将冷却状态解除
136+
self.timer = Timer(self.interval, self._end_cool_down)
137+
self.timer.start()
138+
self.log_debug(f"前沿模式: 进入 {self.interval} 秒的冷却期。")
139+
140+
def _end_cool_down(self):
141+
"""
142+
计时器到期后,解除冷却状态
143+
"""
144+
with self.lock:
145+
self.is_cooling_down = False
146+
self.log_debug("前沿模式: 冷却时间结束,可以再次立即执行。")
147+
148+
def _call_trailing(self, *args, **kwargs):
149+
"""
150+
后沿模式的逻辑。
151+
"""
152+
# 【日志点】记录计时器被重置
153+
if self.timer and self.timer.is_alive():
154+
self.timer.cancel()
155+
self.log_debug("后沿模式: 检测到新的调用,已重置计时器。")
156+
157+
def execute():
158+
self.log_info("后沿模式: 计时结束,开始执行函数。")
159+
self.func(*args, **kwargs)
160+
161+
self.timer = Timer(self.interval, execute)
162+
self.timer.start()
163+
self.log_debug(f"后沿模式: 计时器已启动,将在 {self.interval} 秒后执行。")
164+
165+
def cancel(self) -> None:
166+
"""
167+
取消任何挂起的调用,并重置状态。
168+
"""
169+
with self.lock:
170+
if self.timer and self.timer.is_alive():
171+
self.timer.cancel()
172+
self.timer = None
173+
self.log_info("防抖器被手动取消。")
174+
self.is_cooling_down = False
175+
176+
177+
class AsyncDebouncer(BaseDebouncer):
178+
"""
179+
异步防抖实现类。
180+
"""
181+
def __init__(self, *args, **kwargs):
182+
"""
183+
初始化异步防抖器实例。
184+
"""
185+
super().__init__(*args, **kwargs)
186+
self.task: Optional[asyncio.Task] = None
187+
self.lock = asyncio.Lock()
188+
self.is_cooling_down = False
189+
190+
async def __call__(self, *args, **kwargs) -> None:
191+
"""
192+
异步调用防抖函数。
193+
"""
194+
async with self.lock:
195+
if self.leading:
196+
await self._call_leading(*args, **kwargs)
197+
else:
198+
await self._call_trailing(*args, **kwargs)
199+
200+
async def _call_leading(self, *args, **kwargs):
201+
"""
202+
前沿模式的逻辑。
203+
"""
204+
if not self.is_cooling_down:
205+
self.log_info("前沿模式 (async): 立即执行协程。")
206+
await self.func(*args, **kwargs)
207+
208+
if self.task and not self.task.done():
209+
self.task.cancel()
210+
211+
self.is_cooling_down = True
212+
self.task = asyncio.create_task(self._end_cool_down())
213+
self.log_debug(f"前沿模式 (async): 进入 {self.interval} 秒的冷却期。")
214+
215+
async def _end_cool_down(self):
216+
"""
217+
计时器到期后,解除冷却状态
218+
"""
219+
await asyncio.sleep(self.interval)
220+
async with self.lock:
221+
self.is_cooling_down = False
222+
self.log_debug("前沿模式 (async): 冷却时间结束。")
223+
224+
async def _call_trailing(self, *args, **kwargs):
225+
"""
226+
后沿模式的逻辑。
227+
"""
228+
if self.task and not self.task.done():
229+
self.task.cancel()
230+
self.log_debug("后沿模式 (async): 检测到新的调用,已取消旧任务。")
231+
232+
self.task = asyncio.create_task(self._delayed_execute(*args, **kwargs))
233+
self.log_debug(f"后沿模式 (async): 任务已创建,将在 {self.interval} 秒后执行。")
234+
235+
async def _delayed_execute(self, *args, **kwargs):
236+
"""
237+
延迟执行实际的协程函数。
238+
"""
239+
try:
240+
await asyncio.sleep(self.interval)
241+
self.log_info("后沿模式 (async): 延迟结束,开始执行协程。")
242+
await self.func(*args, **kwargs)
243+
except asyncio.CancelledError:
244+
# 任务被取消是正常行为,无需处理
245+
pass
246+
247+
async def cancel(self) -> None:
248+
"""
249+
取消任何挂起的调用,并重置状态。
250+
"""
251+
async with self.lock:
252+
if self.task and not self.task.done():
253+
self.task.cancel()
254+
self.task = None
255+
self.log_info("异步防抖器被手动取消。")
256+
self.is_cooling_down = False
257+
258+
259+
def debounce(interval: float, *, leading: bool = False,
260+
enable_logging: bool = False, source: str = "") -> Callable:
261+
"""
262+
支持同步和异步的防抖装饰器工厂。
263+
"""
264+
265+
def decorator(func: Callable) -> Callable:
266+
# 检查函数类型,并选择合适的引擎
267+
if inspect.iscoroutinefunction(func):
268+
# 异步函数,使用 AsyncDebouncer
269+
instance = AsyncDebouncer(func, interval,
270+
leading=leading,
271+
enable_logging=enable_logging,
272+
source=source)
273+
274+
@functools.wraps(func)
275+
async def async_wrapper(*args, **kwargs) -> Any:
276+
await instance(*args, **kwargs)
277+
278+
async_wrapper.cancel = instance.cancel
279+
return async_wrapper
280+
281+
else:
282+
# 同步函数,使用 Debouncer
283+
instance = Debouncer(func, interval,
284+
leading=leading,
285+
enable_logging=enable_logging,
286+
source=source)
287+
288+
@functools.wraps(func)
289+
def wrapper(*args, **kwargs) -> Any:
290+
instance(*args, **kwargs)
291+
292+
wrapper.cancel = instance.cancel
293+
return wrapper
294+
295+
return decorator

0 commit comments

Comments
 (0)