1
0
forked from bot/app

新增observer类和开发调试器

This commit is contained in:
远野千束 2024-08-12 04:45:59 +08:00
parent 83325e63ea
commit c9157f0e2c
20 changed files with 222 additions and 214 deletions

View File

@ -1,4 +1,4 @@
FROM python:3.11-bullseye
FROM swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.10-slim-bullseye
ENV TZ Asia/Shanghai

View File

@ -1,7 +1,6 @@
nonebot:
host: 127.0.0.1
port: 20216
superusers: ["1234"]
command_start: ["", "/"]
nickname: [ "liteyuki-dev" ]
default_language: zh

9
docs/deployment/cfg.txt Normal file
View File

@ -0,0 +1,9 @@
# note
开发者选项
allow_update: true # 是否允许更新
log_level: "INFO" # 日志等级
log_icon: true # 是否显示日志等级图标(某些控制台字体不可用)
auto_report: true # 是否自动上报问题给轻雪服务器
auto_update: true # 是否自动更新轻雪每天4点检查更新
safe_mode: false # 安全模式,开启后将不会加载任何第三方插件
dev_mode: false # 开发者模式,开启后将会启动看门狗

View File

@ -25,14 +25,16 @@ __all__ = [
class LiteyukiBot:
def __init__(self, *args, **kwargs):
print_logo()
global _BOT_INSTANCE
_BOT_INSTANCE = self # 引用
self.lifespan = Lifespan()
self.config: dict[str, Any] = kwargs
self.init(**self.config) # 初始化
logger.info("Liteyuki is initializing...")
self.lifespan = Lifespan()
self.process_manager: ProcessManager = ProcessManager(bot=self)
self.loop = asyncio.new_event_loop()
@ -41,19 +43,7 @@ class LiteyukiBot:
self.stop_event = threading.Event()
self.call_restart_count = 0
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")
load_plugins("liteyuki/plugins") # 加载轻雪插件
logger.info("Liteyuki is initializing...")
def run(self):
"""
@ -75,7 +65,6 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
Returns:
"""
if self.call_restart_count < 1:
executable = sys.executable
args = sys.argv
@ -103,16 +92,10 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
Returns:
"""
logger.info(f"Stopping process {name}...")
self.loop.create_task(self.lifespan.before_process_shutdown()) # 重启前钩子
self.loop.create_task(self.lifespan.before_process_shutdown()) # 停止前钩子
self.loop.create_task(self.lifespan.before_shutdown()) # 重启前钩子
self.loop.create_task(self.lifespan.before_shutdown()) # 停止前钩子
# if name:
# self.process_manager.terminate(name)
# else:
# self.process_manager.terminate_all()
if name:
if name is not None:
chan_active = get_channel(f"{name}-active")
chan_active.send(1)
else:
@ -158,17 +141,6 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
"""
return self.lifespan.on_after_start(func)
def on_before_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止前的函数为子进程停止时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_shutdown(func)
def on_after_shutdown(self, func: LIFESPAN_FUNC):
"""
注册停止后的函数未实现
@ -180,9 +152,20 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
"""
return self.lifespan.on_after_shutdown(func)
def on_before_restart(self, func: LIFESPAN_FUNC):
def on_before_process_shutdown(self, func: LIFESPAN_FUNC):
"""
注册重启前的函数为子进程重启时调用
注册进程停止前的函数为子进程停止时调用
Args:
func:
Returns:
"""
return self.lifespan.on_before_process_shutdown(func)
def on_before_process_restart(self, func: LIFESPAN_FUNC):
"""
注册进程重启前的函数为子进程重启时调用
Args:
func:
@ -190,7 +173,7 @@ $$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
"""
return self.lifespan.on_before_restart(func)
return self.lifespan.on_before_process_restart(func)
def on_after_restart(self, func: LIFESPAN_FUNC):
"""
@ -228,13 +211,14 @@ def get_bot() -> Optional[LiteyukiBot]:
LiteyukiBot: 当前的轻雪实例
"""
if IS_MAIN_PROCESS:
if _BOT_INSTANCE is None:
raise RuntimeError("Liteyuki instance not initialized.")
return _BOT_INSTANCE
else:
# 从多进程上下文中获取
pass
raise RuntimeError("Can't get bot instance in sub process.")
def get_config(key: str, default: Any = None) -> Any:
def get_config(key: str = None, default: Any = None) -> Any:
"""
获取配置
Args:
@ -244,7 +228,9 @@ def get_config(key: str, default: Any = None) -> Any:
Returns:
Any: 配置值
"""
return _BOT_INSTANCE.config.get(key, default)
if key is None:
return get_bot().config
return get_bot().config.get(key, default)
def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any = None) -> Any:
@ -258,11 +244,24 @@ def get_config_with_compat(key: str, compat_keys: tuple[str], default: Any = Non
Returns:
Any: 配置值
"""
if key in _BOT_INSTANCE.config:
return _BOT_INSTANCE.config[key]
if key in get_bot().config:
return get_bot().config[key]
for compat_key in compat_keys:
if compat_key in _BOT_INSTANCE.config:
if compat_key in get_bot().config:
logger.warning(f"Config key {compat_key} will be deprecated, use {key} instead.")
return _BOT_INSTANCE.config[compat_key]
return get_bot().config[compat_key]
return default
def print_logo():
print("\033[34m" + r"""
__ ______ ________ ________ __ __ __ __ __ __ ______
/ | / |/ |/ |/ \ / |/ | / |/ | / |/ |
$$ | $$$$$$/ $$$$$$$$/ $$$$$$$$/ $$ \ /$$/ $$ | $$ |$$ | /$$/ $$$$$$/
$$ | $$ | $$ | $$ |__ $$ \/$$/ $$ | $$ |$$ |/$$/ $$ |
$$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |$$ $$< $$ |
$$ | $$ | $$ | $$$$$/ $$$$/ $$ | $$ |$$$$$ \ $$ |
$$ |_____ _$$ |_ $$ | $$ |_____ $$ | $$ \__$$ |$$ |$$ \ _$$ |_
$$ |/ $$ | $$ | $$ | $$ | $$ $$/ $$ | $$ |/ $$ |
$$$$$$$$/ $$$$$$/ $$/ $$$$$$$$/ $$/ $$$$$$/ $$/ $$/ $$$$$$/
""" + "\033[0m")

View File

@ -29,10 +29,10 @@ class Lifespan:
self._before_start_funcs: list[LIFESPAN_FUNC] = []
self._after_start_funcs: list[LIFESPAN_FUNC] = []
self._before_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_process_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._after_shutdown_funcs: list[LIFESPAN_FUNC] = []
self._before_restart_funcs: list[LIFESPAN_FUNC] = []
self._before_process_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_restart_funcs: list[LIFESPAN_FUNC] = []
self._after_nonebot_init_funcs: list[LIFESPAN_FUNC] = []
@ -73,7 +73,7 @@ class Lifespan:
self._after_start_funcs.append(func)
return func
def on_before_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
def on_before_process_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册停止前的函数
Args:
@ -81,7 +81,7 @@ class Lifespan:
Returns:
LIFESPAN_FUNC:
"""
self._before_shutdown_funcs.append(func)
self._before_process_shutdown_funcs.append(func)
return func
def on_after_shutdown(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
@ -97,7 +97,7 @@ class Lifespan:
self._after_shutdown_funcs.append(func)
return func
def on_before_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
def on_before_process_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
"""
注册重启时的函数
Args:
@ -105,7 +105,7 @@ class Lifespan:
Returns:
LIFESPAN_FUNC:
"""
self._before_restart_funcs.append(func)
self._before_process_restart_funcs.append(func)
return func
def on_after_restart(self, func: LIFESPAN_FUNC) -> LIFESPAN_FUNC:
@ -147,13 +147,13 @@ class Lifespan:
logger.debug("Running after_start functions")
await self._run_funcs(self._after_start_funcs)
async def before_shutdown(self) -> None:
async def before_process_shutdown(self) -> None:
"""
停止前
Returns:
"""
logger.debug("Running before_shutdown functions")
await self._run_funcs(self._before_shutdown_funcs)
await self._run_funcs(self._before_process_shutdown_funcs)
async def after_shutdown(self) -> None:
"""
@ -163,13 +163,13 @@ class Lifespan:
logger.debug("Running after_shutdown functions")
await self._run_funcs(self._after_shutdown_funcs)
async def before_restart(self) -> None:
async def before_process_restart(self) -> None:
"""
重启前
Returns:
"""
logger.debug("Running before_restart functions")
await self._run_funcs(self._before_restart_funcs)
await self._run_funcs(self._before_process_restart_funcs)
async def after_restart(self) -> None:
"""

View File

@ -53,7 +53,6 @@ class ProcessManager:
should_exit = False
while not should_exit:
chan_active = get_channel(f"{name}-active")
chan_passive = get_channel(f"{name}-passive")
process = Process(target=self.targets[name][0], args=self.targets[name][1],
kwargs=self.targets[name][2])
self.processes[name] = process
@ -62,15 +61,19 @@ class ProcessManager:
# 0退出 1重启
data = chan_active.receive()
if data == 1:
# 重启
if self.is_process_alive(name):
logger.info(f"Restarting process {name}")
asyncio.run(self.bot.lifespan.before_shutdown())
asyncio.run(self.bot.lifespan.before_restart())
asyncio.run(self.bot.lifespan.before_process_shutdown())
asyncio.run(self.bot.lifespan.before_process_restart())
self.terminate(name)
break
else:
logger.warning(f"Process {name} is not restartable, cannot restart.")
elif data == 0:
logger.info(f"Stopping process {name}")
asyncio.run(self.bot.lifespan.before_shutdown())
asyncio.run(self.bot.lifespan.before_process_shutdown())
should_exit = True
self.terminate(name)
else:
@ -129,3 +132,17 @@ class ProcessManager:
def terminate_all(self):
for name in self.targets:
self.terminate(name)
def is_process_alive(self, name: str) -> bool:
"""
检查进程是否存活
Args:
name:
Returns:
"""
if name not in self.targets:
raise logger.warning(f"Process {name} not found.")
process = self.processes[name]
return process.is_alive()

View File

@ -1,5 +1,92 @@
# -*- coding: utf-8 -*-
"""
此模块用于注册观察者函数使用watchdog监控文件变化并重启bot
启用该模块需要在配置文件中设置`dev_mode`为True
"""
import functools
import time
from typing import Callable, TypeAlias
from watchdog.events import FileSystemEvent, FileSystemEventHandler
from watchdog.observers import Observer
from liteyuki import get_bot, get_config, logger
liteyuki_bot = get_bot()
CALLBACK_FUNC: TypeAlias = Callable[[FileSystemEvent], None] # 位置1为FileSystemEvent
FILTER_FUNC: TypeAlias = Callable[[FileSystemEvent], bool] # 位置1为FileSystemEvent
observer = Observer()
def debounce(wait):
"""
防抖函数
"""
def decorator(func):
def wrapper(*args, **kwargs):
nonlocal last_call_time
current_time = time.time()
if (current_time - last_call_time) > wait:
last_call_time = current_time
return func(*args, **kwargs)
last_call_time = None
return wrapper
return decorator
if get_config("dev_mode", False):
logger.debug("Liteyuki Reload enabled, watching for file changes...")
observer.start()
class CodeModifiedHandler(FileSystemEventHandler):
"""
import watchdog
Handler for code file changes
"""
@debounce(1)
def on_modified(self, event):
raise NotImplementedError("on_modified must be implemented")
def on_created(self, event):
self.on_modified(event)
def on_deleted(self, event):
self.on_modified(event)
def on_moved(self, event):
self.on_modified(event)
def on_any_event(self, event):
self.on_modified(event)
def on_file_system_event(directories: tuple[str], recursive: bool = True, event_filter: FILTER_FUNC = None) -> Callable[[CALLBACK_FUNC], CALLBACK_FUNC]:
"""
注册文件系统变化监听器
Args:
directories: 监听目录们
recursive: 是否递归监听子目录
event_filter: 事件过滤器, 返回True则执行回调函数
Returns:
装饰器装饰一个函数在接收到数据后执行
"""
def decorator(func: CALLBACK_FUNC) -> CALLBACK_FUNC:
def wrapper(event: FileSystemEvent):
if event_filter is not None and not event_filter(event):
return
func(event)
code_modified_handler = CodeModifiedHandler()
code_modified_handler.on_modified = wrapper
for directory in directories:
observer.schedule(code_modified_handler, directory, recursive=recursive)
return func
return decorator

View File

@ -9,22 +9,8 @@ Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Software: PyCharm
"""
import sys
import loguru
from typing import TYPE_CHECKING
logger = loguru.logger
if TYPE_CHECKING:
# avoid sphinx autodoc resolve annotation failed
# because loguru module do not have `Logger` class actually
from loguru import Record
def default_filter(record: "Record"):
"""默认的日志过滤器,根据 `config.log_level` 配置改变日志等级。"""
log_level = record["extra"].get("nonebot_log_level", "INFO")
levelno = logger.level(log_level).no if isinstance(log_level, str) else log_level
return record["level"].no >= levelno
from loguru import logger
# DEBUG日志格式
debug_format: str = (
@ -50,35 +36,26 @@ def get_format(level: str) -> str:
return default_format
logger = loguru.logger.bind()
def init_log(config: dict):
"""
在语言加载完成后执行
Returns:
"""
global logger
logger.remove()
logger.add(
sys.stdout,
level=0,
diagnose=False,
filter=default_filter,
format=get_format(config.get("log_level", "INFO")),
)
show_icon = config.get("log_icon", True)
logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}DEBUG")
logger.level("INFO", color="<normal>", icon=f"{'' if show_icon else ''}INFO")
logger.level("SUCCESS", color="<green>", icon=f"{'' if show_icon else ''}SUCCESS")
logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}WARNING")
logger.level("ERROR", color="<red>", icon=f"{'' if show_icon else ''}ERROR")
# debug = lang.get("log.debug", default="==DEBUG")
# info = lang.get("log.info", default="===INFO")
# success = lang.get("log.success", default="SUCCESS")
# warning = lang.get("log.warning", default="WARNING")
# error = lang.get("log.error", default="==ERROR")
#
# logger.level("DEBUG", color="<blue>", icon=f"{'🐛' if show_icon else ''}{debug}")
# logger.level("INFO", color="<normal>", icon=f"{'' if show_icon else ''}{info}")
# logger.level("SUCCESS", color="<green>", icon=f"{'✅' if show_icon else ''}{success}")
# logger.level("WARNING", color="<yellow>", icon=f"{'⚠️' if show_icon else ''}{warning}")
# logger.level("ERROR", color="<red>", icon=f"{'⭕' if show_icon else ''}{error}")
init_log(config={})

View File

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/11 下午8:50
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""
from liteyuki.core import IS_MAIN_PROCESS
from liteyuki.plugin import PluginMetadata
from .observer import *
__plugin_meta__ = PluginMetadata(
name="代码热重载监视",
)
if IS_MAIN_PROCESS:
config = get_config("liteyuki.reload")

View File

@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/11 下午10:01
@Author : snowykami
@Email : snowykami@outlook.com
@File : observer.py
@Software: PyCharm
"""
from watchdog.events import FileSystemEventHandler
from watchdog.observers import Observer
from liteyuki import get_config, logger, get_bot
liteyuki_bot = get_bot()
if get_config("debug", False):
src_directories = (
"src/nonebot_plugins",
"src/utils",
)
src_excludes_extensions = (
"pyc",
)
logger.debug("Liteyuki Reload enabled, watching for file changes...")
class CodeModifiedHandler(FileSystemEventHandler):
"""
Handler for code file changes
"""
def on_modified(self, event):
if event.src_path.endswith(
src_excludes_extensions) or event.is_directory or "__pycache__" in event.src_path:
return
logger.info(f"{event.src_path} modified, reloading bot...")
liteyuki_bot.restart_process("nonebot")
code_modified_handler = CodeModifiedHandler()
observer = Observer()
for directory in src_directories:
observer.schedule(code_modified_handler, directory, recursive=True)
observer.start()

View File

@ -15,14 +15,13 @@ from liteyuki.plugin import PluginMetadata
from liteyuki import get_bot
from liteyuki.comm import Channel, set_channel
from liteyuki.core import IS_MAIN_PROCESS
from .nb_utils import adapter_manager, driver_manager
__plugin_meta__ = PluginMetadata(
name="NoneBot2启动器",
)
liteyuki = get_bot()
def nb_run(chan_active: "Channel", chan_passive: "Channel", **kwargs):
"""
@ -50,6 +49,11 @@ def nb_run(chan_active: "Channel", chan_passive: "Channel", **kwargs):
if IS_MAIN_PROCESS:
from .dev_reloader import *
liteyuki = get_bot()
@liteyuki.on_after_start
def start_run_nonebot():
liteyuki.process_manager.add_target(name="nonebot", target=nb_run, args=(), kwargs=liteyuki.config)

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
"""
NoneBot 开发环境重载监视器
"""
import os.path
from liteyuki.dev import observer
from liteyuki import get_bot, logger
from watchdog.events import FileSystemEvent
liteyuki = get_bot()
exclude_extensions = (".pyc", ".pyo")
@observer.on_file_system_event(
directories=("src/nonebot_plugins",),
event_filter=lambda event: not event.src_path.endswith(exclude_extensions) and ("__pycache__" not in event.src_path ) and os.path.isfile(event.src_path)
)
def restart_nonebot_process(event: FileSystemEvent):
logger.debug(f"File {event.src_path} changed, reloading nonebot...")
liteyuki.restart_process("nonebot")

View File

@ -5,5 +5,5 @@ from liteyuki import LiteyukiBot
from liteyuki.config import load_config_in_default
if __name__ == "__main__":
bot = LiteyukiBot(**load_config_in_default())
bot = LiteyukiBot(**load_config_in_default(no_waring=True))
bot.run()

View File

@ -21,6 +21,7 @@ pytz~=2024.1
PyYAML~=6.0.1
pillow~=10.0.0
starlette~=0.36.3
toml~=0.10.2
loguru~=0.7.2
importlib_metadata~=7.0.2
requests~=2.31.0

View File

@ -2,8 +2,6 @@ from nonebot.plugin import PluginMetadata
from .core import *
from .loader import *
from .dev import *
__author__ = "snowykami"
__plugin_meta__ = PluginMetadata(
name="轻雪核心插件",
@ -18,6 +16,5 @@ __plugin_meta__ = PluginMetadata(
from ..utils.base.language import Language, get_default_lang_code
sys_lang = Language(get_default_lang_code())
nonebot.logger.info(sys_lang.get("main.current_language", LANG=sys_lang.get("language.name")))

View File

@ -1,36 +0,0 @@
import nonebot
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
from liteyuki.bot import get_bot
from src.utils.base import reload
from src.utils.base.config import get_config
from src.utils.base.resource import load_resources
if get_config("debug", False):
liteyuki_bot = get_bot()
res_directories = (
"src/resources",
"resources",
)
class ResourceModifiedHandler(FileSystemEventHandler):
"""
Handler for resource file changes
"""
def on_modified(self, event):
nonebot.logger.info(f"{event.src_path} modified, reloading resource...")
load_resources()
resource_modified_handle = ResourceModifiedHandler()
observer = Observer()
for directory in res_directories:
observer.schedule(resource_modified_handle, directory, recursive=True)
observer.start()

View File

@ -16,7 +16,6 @@ load_resources()
init_log()
driver = get_driver()
liteyuki_bot = get_bot()
@driver.on_startup

View File

@ -29,13 +29,12 @@ def _():
logger.info("生命周期监控器:准备启动")
@bot.on_before_shutdown
@bot.on_before_process_shutdown
def _():
print(get_channel("main"))
logger.info("生命周期监控器:准备停止")
@bot.on_before_restart
@bot.on_before_process_restart
def _():
logger.info("生命周期监控器:准备重启")

View File

@ -21,4 +21,3 @@ __plugin_meta__ = PluginMetadata(
"default_enable" : True,
}
)

View File

@ -2,8 +2,8 @@ import os
from pydantic import Field
from .data import Database, LiteModel, Database
print("导入数据库模块")
from .data import Database, LiteModel
DATA_PATH = "data/liteyuki"
user_db: Database = Database(os.path.join(DATA_PATH, "users.ldb"))
group_db: Database = Database(os.path.join(DATA_PATH, "groups.ldb"))