├── .gitignore ├── LICENSE ├── README.md ├── nonebot_plugin_reboot ├── __init__.py ├── command.py ├── config.py └── reloader.py └── pyproject.toml /.gitignore: -------------------------------------------------------------------------------- 1 | test.py 2 | poetry.lock 3 | # Created by https://www.toptal.com/developers/gitignore/api/python 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 5 | 6 | ### Python ### 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # End of https://www.toptal.com/developers/gitignore/api/python -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 18870 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nonebot-plugin-reboot 2 | ![](https://img.shields.io/badge/Development-Inactive-inactive) ![](https://img.shields.io/badge/PullRequests-Welcome-success) 3 | 4 | 用命令重启 bot 5 | 6 | 7 | 8 | ## :warning:注意事项 9 | **必须要有** `bot.py` 10 | 新版 nb-cli 默认不生成 bot.py,需要在 nb 菜单里选择 `生成机器人的入口文件` / `Generate entry file of your bot.` 生成一个,不需要修改。 ~~不要再去群里问bot.py在哪了~~ 11 | 12 | **不兼容** `fastapi_reload`,见 [#1](https://github.com/18870/nonebot-plugin-reboot/issues/1)、[#2](https://github.com/18870/nonebot-plugin-reboot/issues/2)。 13 | 不推荐使用 nb-cli 的 `--reload` 参数,这个插件是 `--reload` 在生产环境中的替代品。 14 | 15 | 重启时直接对子进程使用 `process.terminate()`,如果你的其他插件启动了子进程,请确保它们能在设定的等待时间内正确关闭子进程,否则子进程会变成孤立进程。 16 | :warning: Windows 下因系统 API 的限制进程会直接被杀死, **没有** 等待时间。 17 | 18 |
19 | 20 | 插件依赖于 `multiprocessing` `spawn` 生成子进程方式工作,支持由 nb-cli 生成的 bot.py,以及其他在加载插件后调用 `nonebot.run()` 的启动方式。 21 | 22 | 23 | ## 安装 24 | 通过 nb-cli 安装: 25 | `nb plugin install nonebot-plugin-reboot` 26 | 27 | 新版 nb-cli 默认不生成 bot.py,需要在 nb 菜单里选择 `生成机器人的入口文件` / `Generate entry file of your bot.` 生成一个,不需要修改。 28 | 29 | 30 | ## 使用 31 | **超级用户**向机器人**私聊**发送**命令** `重启`, `reboot` 或 `restart` 32 | > :warning: 注意命令的 `COMMAND_START`. 33 | > 例如 /重启 、 /reboot 、 /restart 34 | 35 | 36 | ## 配置项 37 | `reboot_load_command`: `bool` 38 | - 加载内置的 `onebot v11` 重启命令 39 | - 可以通过命令 `重启` `reboot` `restart` 触发重启 40 | - 默认值: `True` 41 | 42 | `reboot_grace_time_limit`: `int` 43 | - 收到重启命令后等待进程退出的最长时间,超时会强制杀进程 44 | - 在 Windows 下没有等待时间,会直接杀进程 45 | - ~~真寻从ctrl+c到彻底退出居然要六秒~~ 46 | - 默认值: `20` 47 | 48 | 49 | ## `bot.py` 50 | 因为使用了 `spawn` 方式启动子进程,默认情况下会加载两次插件,如果你觉得这不是问题可以忽略,也不建议你在不懂的情况下修改 `bot.py`。 51 | 52 | 推荐的写法是将 插件加载部分 和 启动部分 分开,以避免插件在主进程和子进程都加载一遍,见 [#6](https://github.com/18870/nonebot-plugin-reboot/issues/6) 53 | 54 | ~~真寻启动居然要20秒~~ 55 | 56 | 57 | 58 | 59 | ## API 60 | ```python 61 | require("nonebot_plugin_reboot") 62 | from nonebot_plugin_reboot import Reloader 63 | Reloader.reload(delay=5) # 可选参数 5秒后触发重启 64 | ``` 65 | 66 | 67 | ## 依赖 68 | - `nonebot2 >= 2.0.0beta.2` 69 | 70 | 启用 `reboot_load_command` 时需要以下依赖 71 | - `nonebot-adapter-onebot` -------------------------------------------------------------------------------- /nonebot_plugin_reboot/__init__.py: -------------------------------------------------------------------------------- 1 | from .config import plugin_config, Config 2 | __plugin_name__ = "nonebot_plugin_reboot [重启 bot]" 3 | __plugin_des__ = "用命令重启机器人" 4 | __plugin_usage__ = "" 5 | __plugin_author__ = "18870 " 6 | __plugin_homepage__ = "https://github.com/18870/nonebot-plugin-reboot" 7 | 8 | try: 9 | from nonebot.plugin import PluginMetadata 10 | __plugin_meta__ = PluginMetadata( 11 | name=__plugin_name__, 12 | description=__plugin_des__, 13 | usage=__plugin_usage__, 14 | type="application", 15 | homepage=__plugin_homepage__, 16 | config=Config, 17 | supported_adapters={"~onebot.v11"}, 18 | extra={ 19 | "author": __plugin_author__, 20 | } 21 | ) 22 | except ImportError: 23 | pass 24 | 25 | from .reloader import Reloader 26 | 27 | if plugin_config.reboot_load_command: 28 | from .command import reboot_matcher -------------------------------------------------------------------------------- /nonebot_plugin_reboot/command.py: -------------------------------------------------------------------------------- 1 | from nonebot import on_command 2 | from nonebot.adapters.onebot.v11 import PrivateMessageEvent 3 | from nonebot.permission import SUPERUSER 4 | from .reloader import Reloader 5 | 6 | reboot_matcher = on_command( 7 | cmd="重启", 8 | aliases={"reboot", "restart"}, 9 | permission=SUPERUSER, 10 | priority=1, 11 | block=True 12 | ) 13 | 14 | @reboot_matcher.handle() 15 | async def _(event: PrivateMessageEvent): 16 | Reloader.reload() 17 | -------------------------------------------------------------------------------- /nonebot_plugin_reboot/config.py: -------------------------------------------------------------------------------- 1 | from typing import List, Optional 2 | 3 | from nonebot import get_driver 4 | from pydantic import BaseSettings 5 | 6 | 7 | class Config(BaseSettings): 8 | reboot_load_command: bool = True 9 | reboot_grace_time_limit: int = 20 10 | 11 | class Config: 12 | extra = "ignore" 13 | 14 | 15 | global_config = get_driver().config 16 | plugin_config = Config(**global_config.dict()) 17 | -------------------------------------------------------------------------------- /nonebot_plugin_reboot/reloader.py: -------------------------------------------------------------------------------- 1 | import threading 2 | from multiprocessing import get_context 3 | 4 | import nonebot 5 | from nonebot import logger 6 | 7 | from .config import plugin_config 8 | 9 | _nb_run = nonebot.run 10 | 11 | 12 | class Reloader: 13 | event: threading.Event = None 14 | 15 | @classmethod 16 | def reload(cls, delay: int = 0): 17 | if cls.event is None: 18 | raise RuntimeError() 19 | if delay > 0: 20 | threading.Timer(delay, function=cls.event.set).start() 21 | return 22 | cls.event.set() 23 | 24 | 25 | def _run(ev: threading.Event, *args, **kwargs): 26 | Reloader.event = ev 27 | _nb_run(*args, **kwargs) 28 | 29 | 30 | def run(*args, **kwargs): 31 | should_exit = False 32 | ctx = get_context("spawn") 33 | while not should_exit: 34 | event = ctx.Event() 35 | process = ctx.Process( 36 | target=_run, 37 | args=( 38 | event, 39 | *args, 40 | ), 41 | kwargs=kwargs, 42 | ) 43 | process.start() 44 | while not should_exit: 45 | if event.wait(1): 46 | logger.info("Receive reboot event") 47 | process.terminate() 48 | process.join(plugin_config.reboot_grace_time_limit) 49 | if process.is_alive(): 50 | logger.warning( 51 | f"Cannot shutdown gracefully in {plugin_config.reboot_grace_time_limit} second, force kill process." 52 | ) 53 | process.kill() 54 | break 55 | elif process.is_alive(): 56 | continue 57 | else: 58 | # Process stoped without setting event 59 | should_exit = True 60 | 61 | 62 | nonebot.run = run 63 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "nonebot_plugin_reboot" 3 | version = "0.1.4" 4 | description = "Reboot your bot by using command" 5 | license = "MIT" 6 | authors = ["18870 "] 7 | readme = "README.md" 8 | repository = "https://github.com/18870/nonebot-plugin-reboot" 9 | keywords = ["nonebot"] 10 | 11 | [tool.poetry.dependencies] 12 | python = "^3.7.3" 13 | nonebot2 = "^2.0.0-beta.2" 14 | nonebot-adapter-onebot = { version = "^2.1.0", optional = true } 15 | 16 | [tool.poetry.dev-dependencies] 17 | pytest = "^5.2" 18 | 19 | [build-system] 20 | requires = ["poetry-core>=1.0.0"] 21 | build-backend = "poetry.core.masonry.api" 22 | --------------------------------------------------------------------------------