feat(plugin): support auto bot injection and file-based hot reload
This commit is contained in:
@@ -1,20 +1,115 @@
|
||||
import asyncio
|
||||
import threading
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable, Awaitable, List, Dict
|
||||
from typing import Callable, Awaitable, List, Dict, Optional, Any
|
||||
|
||||
|
||||
class AsyncJob:
|
||||
def __init__(self):
|
||||
self.tasks: List[Callable[[], Awaitable]] = []
|
||||
self._jobs: Dict[str, Dict[str, Any]] = {}
|
||||
self._running_tasks: Dict[str, asyncio.Task] = {}
|
||||
self._running = False
|
||||
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
||||
self._stop_event: Optional[asyncio.Event] = None
|
||||
self._lock = threading.RLock()
|
||||
self._job_seq = 0
|
||||
|
||||
def _next_job_id(self) -> str:
|
||||
with self._lock:
|
||||
self._job_seq += 1
|
||||
return f"job-{self._job_seq}"
|
||||
|
||||
@staticmethod
|
||||
def _infer_owner(func: Callable) -> Optional[Any]:
|
||||
owner = getattr(func, "__self__", None)
|
||||
if owner is not None:
|
||||
return owner
|
||||
|
||||
closure = getattr(func, "__closure__", None) or []
|
||||
for cell in closure:
|
||||
try:
|
||||
value = cell.cell_contents
|
||||
except ValueError:
|
||||
continue
|
||||
if all(hasattr(value, attr) for attr in ("initialize", "start", "stop")):
|
||||
return value
|
||||
return None
|
||||
|
||||
def _register(self, func: Callable, wrapper: Callable[[], Awaitable], trigger: str):
|
||||
owner = self._infer_owner(func)
|
||||
job_id = self._next_job_id()
|
||||
with self._lock:
|
||||
self._jobs[job_id] = {
|
||||
"func": func,
|
||||
"wrapper": wrapper,
|
||||
"trigger": trigger,
|
||||
"owner_id": id(owner) if owner is not None else None,
|
||||
"owner_name": owner.__class__.__name__ if owner is not None else None,
|
||||
}
|
||||
if self._running and self._loop and self._loop.is_running():
|
||||
self._loop.call_soon_threadsafe(self._start_job_in_loop, job_id)
|
||||
|
||||
def _start_job_in_loop(self, job_id: str):
|
||||
job = self._jobs.get(job_id)
|
||||
if not job or job_id in self._running_tasks:
|
||||
return
|
||||
|
||||
async def runner():
|
||||
try:
|
||||
await job["wrapper"]()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[AsyncJob] 任务异常退出: {job_id}, trigger={job.get('trigger')}, error={e}")
|
||||
|
||||
task = asyncio.create_task(runner(), name=f"async_job:{job_id}")
|
||||
self._running_tasks[job_id] = task
|
||||
task.add_done_callback(lambda _task, _id=job_id: self._running_tasks.pop(_id, None))
|
||||
|
||||
def _cancel_job_in_loop(self, job_id: str):
|
||||
task = self._running_tasks.pop(job_id, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
|
||||
def remove_job(self, job_id: str) -> bool:
|
||||
with self._lock:
|
||||
existed = job_id in self._jobs or job_id in self._running_tasks
|
||||
self._jobs.pop(job_id, None)
|
||||
loop = self._loop
|
||||
running = self._running
|
||||
|
||||
if running and loop and loop.is_running():
|
||||
loop.call_soon_threadsafe(self._cancel_job_in_loop, job_id)
|
||||
else:
|
||||
task = self._running_tasks.pop(job_id, None)
|
||||
if task:
|
||||
task.cancel()
|
||||
return existed
|
||||
|
||||
def remove_jobs_by_owner(self, owner: Any) -> int:
|
||||
owner_id = id(owner)
|
||||
with self._lock:
|
||||
job_ids = [job_id for job_id, meta in self._jobs.items() if meta.get("owner_id") == owner_id]
|
||||
|
||||
removed = 0
|
||||
for job_id in job_ids:
|
||||
if self.remove_job(job_id):
|
||||
removed += 1
|
||||
return removed
|
||||
|
||||
def every_seconds(self, seconds: int):
|
||||
def decorator(func: Callable):
|
||||
async def wrapper():
|
||||
while True:
|
||||
await func()
|
||||
try:
|
||||
await func()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[AsyncJob] every_seconds 任务执行异常: {e}")
|
||||
await asyncio.sleep(seconds)
|
||||
|
||||
self.tasks.append(wrapper)
|
||||
self._register(func, wrapper, f"every_seconds({seconds})")
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -27,18 +122,29 @@ class AsyncJob:
|
||||
|
||||
def at_times(self, time_list: List[str]):
|
||||
def decorator(func: Callable):
|
||||
parsed_times = [datetime.strptime(t, "%H:%M").time() for t in time_list]
|
||||
|
||||
async def wrapper():
|
||||
while True:
|
||||
now = datetime.now()
|
||||
for t in time_list:
|
||||
target = datetime.strptime(t, "%H:%M").replace(year=now.year, month=now.month, day=now.day)
|
||||
if target < now:
|
||||
targets = []
|
||||
for t in parsed_times:
|
||||
target = datetime.combine(now.date(), t)
|
||||
if target <= now:
|
||||
target += timedelta(days=1)
|
||||
wait_seconds = (target - now).total_seconds()
|
||||
await asyncio.sleep(wait_seconds)
|
||||
await func()
|
||||
targets.append(target)
|
||||
|
||||
self.tasks.append(wrapper)
|
||||
next_target = min(targets)
|
||||
wait_seconds = (next_target - now).total_seconds()
|
||||
await asyncio.sleep(max(wait_seconds, 0))
|
||||
try:
|
||||
await func()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[AsyncJob] at_times 任务执行异常: {e}")
|
||||
|
||||
self._register(func, wrapper, f"at_times({time_list})")
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -64,9 +170,14 @@ class AsyncJob:
|
||||
|
||||
sleep_secs = (target_dt - now).total_seconds()
|
||||
await asyncio.sleep(sleep_secs)
|
||||
await func()
|
||||
try:
|
||||
await func()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[AsyncJob] every_weekday_time 任务执行异常: {e}")
|
||||
|
||||
self.tasks.append(wrapper)
|
||||
self._register(func, wrapper, f"every_weekday_time({weekday}, {time_str})")
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -91,9 +202,14 @@ class AsyncJob:
|
||||
|
||||
sleep_secs = (target_dt - now).total_seconds()
|
||||
await asyncio.sleep(sleep_secs)
|
||||
await func()
|
||||
try:
|
||||
await func()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[AsyncJob] every_week_time 任务执行异常: {e}")
|
||||
|
||||
self.tasks.append(wrapper)
|
||||
self._register(func, wrapper, f"every_week_time({weekday}, {time_str})")
|
||||
return func
|
||||
|
||||
return decorator
|
||||
@@ -128,15 +244,43 @@ class AsyncJob:
|
||||
|
||||
sleep_secs = (target_dt - now).total_seconds()
|
||||
await asyncio.sleep(sleep_secs)
|
||||
await func()
|
||||
try:
|
||||
await func()
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception as e:
|
||||
print(f"[AsyncJob] every_month_last_day_time 任务执行异常: {e}")
|
||||
|
||||
self.tasks.append(wrapper)
|
||||
self._register(func, wrapper, f"every_month_last_day_time({time_str})")
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
async def run_all(self):
|
||||
await asyncio.gather(*(task() for task in self.tasks))
|
||||
with self._lock:
|
||||
self._running = True
|
||||
self._loop = asyncio.get_running_loop()
|
||||
self._stop_event = asyncio.Event()
|
||||
job_ids = list(self._jobs.keys())
|
||||
|
||||
for job_id in job_ids:
|
||||
self._start_job_in_loop(job_id)
|
||||
|
||||
await self._stop_event.wait()
|
||||
|
||||
def stop_all(self):
|
||||
with self._lock:
|
||||
self._running = False
|
||||
loop = self._loop
|
||||
self._jobs.clear()
|
||||
self._job_seq = 0
|
||||
stop_event = self._stop_event
|
||||
|
||||
if loop and loop.is_running():
|
||||
for job_id in list(self._running_tasks.keys()):
|
||||
loop.call_soon_threadsafe(self._cancel_job_in_loop, job_id)
|
||||
if stop_event:
|
||||
loop.call_soon_threadsafe(stop_event.set)
|
||||
|
||||
|
||||
# 全局唯一 job 管理器
|
||||
|
||||
Reference in New Issue
Block a user